Exchange-coupled bilayers (SAF)#

In this notebook we compute the dispersion relation of two 20 nm thick permalloy layers separated with a 2 nm non-magnetic space. The ferromagnetic layers are antiferromagnetically coupled via bilinear interlayer-exchange coupling. This system exhibits a dispersion asymmetry for the modes propagating perpendicular to the magnetization, as first predicted in Ref. 1.

[1]:
import tetrax as tx

d = 20
s = 2

sample = tx.Sample(
    tx.geometries.layer.bilayer(
        thickness1=d,
        thickness2=d,
        spacing=s,
        cell_sizes=1
    ),
    name="AFM_bilayer"
)

sample.material["Msat"] = 800e3 # A/m
sample.material["Aex"] = 11e-12 # J/m
sample.material["J1"] = -3e-4 # J/m^2
sample.material["gamma"] = 1.760859644e11 # rad/Ts

Setting initial state antiparallel#

In order to set the initial antiparallel state perpendicular to the propagation (\(z\)) direction, we use the alternating_layers function provided in the tetrax.vectorfields module.

[2]:
sample.mag = tx.vectorfields.alternating_layers(sample.xyz, sample.xyz.y[sample.mesh.boundary_nodes],
                                                direction=(1,0,0))
sample.show(scale = 5)
/Users/attilak/micromag/tetrax-tests/fresh_environement_test/venv/lib/python3.13/site-packages/traittypes/traittypes.py:97: UserWarning: Given trait value dtype "float32" does not match required type "float32". A coerced copy has been created.
  warnings.warn(
[2]:

AFM state

To calculate the eigenmodes, we need to use a small external field, since for \(k=0\) the spectrum of an AFM bilayer contains a soft mode with zero frequency (\(f(0)=0\)) that leads to problems when solving the eigenvalue problem numerically. Applying a small field will resolve this issue, changing the mode frequency only by a very small amount.

[3]:
sample.external_field = (1e-5, 0, 0)
spectrum = tx.experiments.eigenmodes(sample, kmin=-50e6, kmax=50e6, num_k=201, num_modes=5, num_cpus=-1)
100%|█████████████████| 101/101 [00:02<00:00, 41.73it/s]

Easy way to plot the dispersion using the plot method of the spectrum#

[4]:
spectrum.plot(n=[0,1], renderer="notebook") # plotting the first 2 modes using the argument n=[0,1]

Dispersion of the acustical and optical branches#

The two lowest modest f0 and f1 can be understood as the optical and acoustic modes of two macrospins. Ref. 1 provided an analytical theory for these two modes, assuming a fully homogeneous profile along the thickness direction of each layer. We will use this analytical theory as a comparison.

[5]:
from scipy.constants import mu_0
import numpy as np

gamma = sample.material["gamma"].average


Msat = sample.material["Msat"].average
Aex = sample.material["Aex"].average
J_bl = sample.material["J1"].average
lex = np.sqrt(2*Aex/(mu_0*Msat**2))

def zeta(k,d,s):
    return np.piecewise(k, [k == 0, k !=0], [1, np.sinh(k*d/2)/(k*d/2) *np.exp(-np.abs(k)*d/2)])

def g(k,d,s):
    return mu_0 *Msat *(zeta(k,d,s))**2 * np.exp(-np.abs(k)*s) *k*d/2

def p(k,d,s):
    return  mu_0*Msat*k**2 *lex**2 + mu_0*Msat*(1-zeta(k,d,s))

def q(k,d,s):
    return  mu_0*Msat*k**2 *lex**2 + mu_0*Msat*zeta(k,d,s)

def f1(k,d,s, J_bl=0):
    C_j = (J_bl)/(Msat*d)
    return gamma/(2*np.pi)*(g(k,d,s) + np.sqrt((p(k,d,s) - g(np.abs(k),d,s))*(q(k,d,s)-g(np.abs(k),d,s)-2*C_j)))

def f2(k,d,s, J_bl=0):
    C_j = (J_bl)/(Msat*d)
    #print(C_j)
    return gamma/(2*np.pi)*(-g(k,d,s) + np.sqrt((q(k,d,s) + g(np.abs(k),d,s))*(p(k,d,s)+g(np.abs(k),d,s)-2*C_j)))

This analytical model is not fully applicable for film thicknesses much larger than the material’s dipole-exchange length and for modes with an inhomogeneous profile. Therefore, we also compare the results of TetraX with the results of a finite-difference dynamic-matrix method by Gallardo for the two lowest modes, presented in Ref. 2.

[6]:
gallardo20 = tx.fetch_reference_data("example_dispersion_gallardo_exchange_coupled_bilayers_20nm")

Plotting the analytic theory and finite-difference data together with the data of TetraX shows perfect agreement between finite-difference and finite-element numerics. For larger wave vectors there are increasing deviations between numerics and analytical theory. These devitations come from dipolar modifications to the mode profiles that are not captured in the analytic theory.

[7]:
from plotly import graph_objects as go

fig = go.Figure()

k_ = np.linspace(-50e6, 50e6, 200)

fig.add_trace(
                go.Scatter(
                    x=-k_*1e-6,
                    y=f1(k_,d*1e-9,s*1e-9, J_bl=-3e-4)*1e-9,
                    mode="lines",
                    name="f0",
                    line = dict(color='red', width=1, dash='dash'),
                    legendgroup="0",
                    legendgrouptitle_text="Theory Ref. 1",
                )
            )

fig.add_trace(
                go.Scatter(
                    x=-k_*1e-6,
                    y=f2(k_,d*1e-9,s*1e-9, J_bl=-3e-4)*1e-9,
                    mode="lines",
                    name="f1",
                    line = dict(color='royalblue', width=1, dash='dash'),
                    legendgroup="0",
                    legendgrouptitle_text="Theory Ref. 1",
                )
            )




for n, c in enumerate(["red", "royalblue"]):
    fig.add_trace(
                    go.Scatter(
                        x=gallardo20['k (rad/m)']*1e-6,
                        y=gallardo20[f'f{n} (Hz)']*1e-9,
                        mode="markers",
                        name=f"f{n}",
                        marker=dict(color=c, symbol="x"),
                        legendgroup="2",
                        legendgrouptitle_text="Numeric Ref. 2",
                    )
                )


for n, c in enumerate(["red", "royalblue", "purple", "orange"]):
    fig.add_trace(
                go.Scatter(
                    x=spectrum.k*1e-6,
                    y=spectrum.frequencies(n=n)*1e-9,
                    mode="lines",
                    name=f"f{n}",
                    line = dict(color=c),
                    legendgroup="1",
                    legendgrouptitle_text="TetraX",
                )
            )


fig.update_xaxes(title_text="k (rad/µm)", exponentformat="power")
fig.update_yaxes(title_text="f (GHz)", exponentformat="power")

fig.update_layout(
        template="simple_white",
        height=600,
        width=600,
        hoverlabel={"bordercolor": "rgba(255,255,255,1)"},
    )

fig.show(renderer="notebook") # remove this line when running on your computer