Fixed-frequency FMR of avoided level crossings#
Here we present the notebook used for results published in Ref. 1. We show you the possibility how to obtain the resonance fields for a well-defined frequency in function of wave vector. This is what can be obtained in FMR-type experiments where the microwave frequency is fixed and only the external field is swept. The system under consideration is a YIG film with 200 nm thickness, which we can model using the monolayer geometry.
Note that, for layers in TetraX, the thickness direction is the \(y\) direction, while the propagation direction is the \(z\) direction.
[1]:
import tetrax as tx
sample = tx.Sample(
tx.geometries.layer.monolayer(
thickness=200,
cell_size=5),
name="YIG_monolayer",
) # in nm
sample.material["Msat"] = 139.0e3
sample.material["Aex"] = 4.0e-12
sample.material["gamma"] = 1.760859644e11
Dispersion for a fixed field#
To get a feel for the dispersion, we compute it first for a given applied field. It can then be visualized using the built-in plot() method of the corresponding EigenResult.
[2]:
sample.mag = (1, 0, 0)
sample.external_field = (35e-3, 0, 0)
spectrum = tx.experiments.eigenmodes(sample, num_k=81,kmin=0,kmax=5e6,num_modes=3,num_cpus=-1,save_mode_profiles=False)
# remove the renderer argument when running on your computer
spectrum.plot(n=[0,1], k=[0,None], fscale="G", kscale="u", renderer="notebook")
100%|███████████████████| 81/81 [00:01<00:00, 46.71it/s]
We see that the dispersion exhibits an avoided level crossing between the first and the second mode.
Calculation of spin-wave spectra for each field#
In several experiments, the microwave frequency is fixed while only the external field is swept. In this situation, spin waves in the system appear at specific resonance fields instead of frequencies. As TetraX directly calculates the spin-wave normal modes regarding their resonance frequencies, we can use interpolation techniques to translate from frequencies to fields.
First, we must calculate the spin-wave spectra over the whole field range. The field range has finer steps close for when the corresponding resonance frequencies are close to the avoided level crossing. The following cell might take a while to execute, as many spectra need be calculated.
[3]:
import numpy as np
static_field_range = 1e-3*np.concatenate((np.arange(26,30,0.5), np.arange(30,34,0.05), np.arange(34,39,0.5))) # fields in Tesla
f0 = []
f1 = []
f2 = []
sample.mag = (1, 0, 0)
for i, B in enumerate(static_field_range):
sample.external_field = (B, 0, 0)
spectrum = tx.experiments.eigenmodes(sample, kmin=0.8e6,kmax=2.2e6,num_k=81,num_modes=3,num_cpus=-1,save_mode_profiles=False,verbose=False)
f0.append(spectrum.frequencies(n=0))
f1.append(spectrum.frequencies(n=1))
f2.append(spectrum.frequencies(n=2))
print(f"B = {int(B*1e3)} mT ({(i+1)/len(static_field_range)*100:.1f}%) done.", end="\r")
B = 38 mT (100.0%) done.
Interpolation/extrapolation to obtained resonance fields#
Now, for every single field value, we need to find the \(k_0\) and \(k_1\) so that \(f_0(k_0) = f_{RF}\) and \(f_1(k_1) = f_{RF}\), respectively. In our case \(f_{RF} = 2.8\ \mathrm{GHz}\). Notice that sometimes, we have to use extrapolation for practical purposes. However, be careful, as \(k\) points obtained far outside the calculated wave-vector range are nonsensical.
[4]:
from scipy.interpolate import interp1d
k_ = spectrum.k
f0 = np.array(f0)
f1 = np.array(f1)
k0 = []
k1 = []
kp = k_[k_ > 0]
f_RF = 2.8e9 # Hz
for i, Bext in enumerate(static_field_range):
f0_B = f0[i][k_ > 0]
f1_B = f1[i][k_ > 0]
k0.append(interp1d(f0_B, kp,fill_value="extrapolate")(f_RF))
k1.append(interp1d(f1_B, kp,fill_value="extrapolate")(f_RF))
k0 = np.array(k0)
k1 = np.array(k1)
Plotting results and comparison#
Finally, we plot the result in the valid wave-vector range and compare it with the analytical resonance fields that correspond to the zeroth-order of the perturbation theory by Kalinikos and Slavin, Ref. 2. As shown in Ref. 1, TetraX agrees with experimental data for this system.
[5]:
from plotly import graph_objects as go
def Bext_KS(k, gamma, M_s, A, T, omega, n=0, mu_0=4*np.pi*1e-7):
omega_M = gamma * mu_0 * M_s
Lambda = np.sqrt(2*A / (mu_0 *M_s**2))
k_n = np.sqrt(k**2 + (n*np.pi/T)**2)
rat = k**2 / k_n**2
kron_n = (n == 0)
P_nn = rat*(1 - (2/(1+kron_n))*rat*(1-(-1)**n *np.exp(-k*T))/(k*T))
b = omega_M * (2 * Lambda**2 * k_n**2 + 1)
delta = np.sqrt(omega_M**2 + 4*omega**2 - 4*omega_M**2*P_nn*(1-P_nn))
return np.abs((-b+delta)/(2*gamma))
fig = go.Figure()
# numerical results from the extrapolation
fig.add_trace(
go.Scatter(
x=k0*1e-6,
y=static_field_range*1e3,
mode="lines+markers",
name=f"f0",
line = dict(width=1),
legendgroup="0",
legendgrouptitle_text="TetraX",
)
)
fig.add_trace(
go.Scatter(
x=k1*1e-6,
y=static_field_range*1e3,
mode="lines+markers",
name=f"f1",
line = dict(width=1),
legendgroup="0",
legendgrouptitle_text="TetraX",
)
)
# analytical values using the Bext_KS function defined above
fig.add_trace(
go.Scatter(
x=k0*1e-6,
y=Bext_KS(k0, sample.material["gamma"].average, sample.material["Msat"].average, sample.material["Aex"].average, 200*1e-9, 2*np.pi*2.8e9, 0, 4*np.pi*1e-7)*1e3,
mode="lines",
name=f"f0",
line = dict(width=1,dash="dash"),
legendgroup="1",
legendgrouptitle_text="Theory",
)
)
fig.add_trace(
go.Scatter(
x=k1*1e-6,
y=Bext_KS(k1, sample.material["gamma"].average, sample.material["Msat"].average, sample.material["Aex"].average, 200*1e-9, 2*np.pi*2.8e9, 1, 4*np.pi*1e-7)*1e3,
mode="lines",
name=f"f1",
line = dict(width=1,dash="dash"),
legendgroup="1",
legendgrouptitle_text="Theory",
)
)
fig.update_xaxes(title_text="Wave vector, <i>k</i> (rad/µm)", exponentformat="power")
fig.update_yaxes(title_text="Resonance field <i>B</i> (mT)", exponentformat="power")
fig.update_layout(
xaxis_range=[0.8,2.2],
yaxis_range=[24,38],
template="simple_white",
height=600,
width=600,
hoverlabel={"bordercolor": "rgba(255,255,255,1)"},
)
# remove the renderer argument when running on your computer
fig.show(renderer="notebook")