#!/usr/bin/env python3
# -*- coding: utf-8 -*-
""":py:class:`lmfit.Model` model classes and functions for various magnetism and magnetic materials models."""
# pylint: disable=invalid-name
__all__ = [
"BlochLaw",
"FMR_Power",
"Inverse_Kittel",
"KittelEquation",
"Langevin",
"blochLaw",
"fmr_power",
"inverse_kittel",
"kittelEquation",
"langevin",
]
import numpy as np
import scipy.constants as cnst
from scipy.special import zeta, gamma
from scipy.constants import k, mu_0, e, electron_mass, hbar
from scipy.signal import savgol_filter
from lmfit import Model
from lmfit.models import update_param_vals
mu_B = cnst.physical_constants["Bohr magneton"][0]
[docs]def blochLaw(T, Ms, Tc):
r"""Bloch's law for spontaneous magnetism at low temperatures.
Args:
T (array like):
Temperatures at which M has been measured (Probably in K)
Ms (float):
The magnetisation at 0K (in whtaever units M is being measured in)
Tc (float):
Curie temperature in K
Returns:
(array like):
Calculated values of M
Notes:
This fits the Bloch Law as given by:
:math:`M(T) = M_s\left[1 - \left(\frac{T}{T_C}\right)^\frac{3}{2}\right]`
This expression is only really valid in the low temperature regime.
"""
return Ms * (1 - (T / Tc) ** 1.5)
[docs]def langevin(H, M_s, m, T):
r"""Langevin function for paramagnetic M-H loops.
Args:
H (array): The applied magnetic field
M_s (float): Saturation magnetisation
m (float) is the moment of a cluster
T (float): Temperature
Returns:
Magnetic Momemnts (array).
Note:
The Langevin Function is :math:`\coth(\frac{\mu_0HM_s}{k_BT})-\frac{k_BT}{\mu_0HM_s}`.
Example:
.. plot:: samples/Fitting/langevin.py
:include-source:
:outname: langevin
"""
x = mu_0 * H * m / (k * T)
n = M_s / m
return m * n * (1.0 / np.tanh(x) - 1.0 / x)
[docs]def kittelEquation(H, g, M_s, H_k):
r"""Kittel Equation for finding ferromagnetic resonance peak in frequency with field.
Args:
H (array): Magnetic fields in A/m
g (float): g factor for the gyromagnetic radius
M_s (float): Magnetisation of sample in A/m
H_k (float): Anisotropy field term (including demagnetising factors) in A/m
Returns:
Reesonance peak frequencies in Hz
:math:`f = \frac{\gamma \mu_{0}}{2 \pi} \sqrt{\left(H + H_{k}\right) \left(H + H_{k} + M_{s}\right)}`
Example:
.. plot:: samples/Fitting/kittel.py
:include-source:
:outname: kittel
"""
gamma = g * cnst.e / (2 * cnst.m_e)
return (cnst.mu_0 * gamma / (2 * np.pi)) * np.sqrt((H + H_k) * (H + H_k + M_s))
[docs]def inverse_kittel(f, g, M_s, H_k):
r"""Rewritten Kittel equation for finding ferromagnetic resonsance in field with frequency.
Args:
f (array): Resonance Frequency in Hz
g (float): g factor for the gyromagnetic radius
M_s (float): Magnetisation of sample in A/m
H_k (float): Anisotropy field term (including demagnetising factors) in A/m
Returns:
Reesonance peak frequencies in Hz
Notes:
In practice one often measures FMR by sweepign field for constant frequency and then locates the
peak in H by fitting a suitable Lorentzian type peak. In this case, one returns a
:math:`H_{res}\pm \Delta H_{res}`
In order to make use of this data with :py:meth:`Stoner.Analysis.AnalysisMixin.lmfit` or
:py:meth:`Stoner.Analysis.AnalysisMixin.curve_fit`
it makes more sense to fit the Kittel Equation written in terms of H than frequency.
:math:`H_{res}=- H_{k} - \frac{M_{s}}{2} + \frac{1}{2 \gamma \mu_{0}} \sqrt{M_{s}^{2}
\gamma^{2} \mu_{0}^{2} + 16 \pi^{2} f^{2}}`
"""
gamma = g * cnst.e / (2 * cnst.m_e)
return (
-H_k
- M_s / 2
+ np.sqrt(M_s**2 * gamma**2 * cnst.mu_0**2 + 16 * np.pi**2 * f**2) / (2 * gamma * cnst.mu_0)
)
[docs]def fmr_power(H, H_res, Delta_H, K_1, K_2):
r"""Combine a Lorentzian and differential Lorenztion peak as measured in an FMR experiment.
Args:
H (array) magnetic field data
H_res (float): Resonance field of peak
Delta_H (float): Preak wideth
K_1, K_2 (float): Relative weighting of each component.
Returns:
Array of model absorption values.
:math:`\frac{4 \Delta_{H} K_{1} \left(H - H_{res}\right)}{\left(\Delta_{H}^{2} + 4 \left(H - H_{res}
\right)^{2}\right)^{2}} - \frac{K_{2} \left(\Delta_{H}^{2} - 4 \left(H - H_{res}\right)^{2}\right)}{
\left(\Delta_{H}^{2} + 4 \left(H - H_{res}\right)^{2}\right)^{2}}`
"""
return (
4 * Delta_H * K_1 * (H - H_res) / (Delta_H**2 + 4 * (H - H_res) ** 2) ** 2
- K_2 * (Delta_H**2 - 4 * (H - H_res) ** 2) / (Delta_H**2 + 4 * (H - H_res) ** 2) ** 2
)
[docs]class BlochLaw(Model):
r"""Bloch's law for spontaneous magnetism at low temperatures.
Args:
T (array like):
Temperature (K)
g (float):
Lande g-factor
A(float):
The echange stiffness (Jm^{-1})
Ms(float):
Saturation moment (Am^{-1})
Returns:
(array like):
Magnetisation values corresponding to the given temperatures.
Notes:
Calculates :math:`1 - \frac{\left((\Gamma(3/2) \zeta(3/2)\right)}
{(4\pi^2)} (v_{ws} / S) (k_B T / D)^{3/2}`
This is the bulk version of Bloch's law which is not fully correct for thin films. Model adapted from code
by Dr Joseph Barker <j.barker@leeds.ac.uk>
"""
display_names = ["g", "A", "M_s"]
units = ["", "Jm^{-1}", "Am^{-1}"]
def __init__(self, *args, **kwargs):
"""Configure Initial fitting function."""
super().__init__(self.blochs_law_bulk, *args, **kwargs)
self.set_param_hint("g", vary=False, value=2.0)
self.set_param_hint("A", vary=True, min=0)
self.set_param_hint("Ms", vary=True, min=0)
self.prefactor = gamma(1.5) * zeta(1.5) / (4 * np.pi**2)
[docs] def blochs_law_bulk(self, T, g, A, Ms):
r"""Bloch's Law in bulk systems.
Args:
T (array like):
Temperature (K)
g (float):
Lande g-factor
A(float):
The echange stiffness (Jm^{-1})
Ms(float):
Saturation moment (Am^{-1})
Returns:
(array like):
Magnetisation values corresponding to the given temperatures.
Notes:
Calculates :math:`1 - \frac{\left((\Gamma(3/2) \zeta(3/2)\right)}
{(4\pi^2)} (v_{ws} / S) (k_B T / D)^{3/2}`
"""
Tp = (k * Ms * T / (2 * g * mu_B * A)) ** 1.5
prefactor = self.prefactor * g * mu_B
ret = Ms - (prefactor * Tp)
return ret
[docs] def guess(self, data, x=None, **kwargs):
"""Guess some starting values."""
Ms = data.max() * 1.001
if x is not None:
Mx_max = data[x.argmax()]
y = ((Ms - Mx_max) / (self.prefactor * 2 * mu_B)) ** (2.0 / 3)
Ag = Ms * k * x.max() / (y * 2 * mu_B)
else:
Ag = 1.0
guesses = {}
guesses["Ms"] = Ms
if self.param_hints["A"]["vary"]:
A = Ag / self.param_hints["g"]["value"]
guesses["A"] = A
elif self.param_hints["g"]["vary"]:
g = Ag / self.param_hints["A"]["value"]
guesses["g"] = g
pars = self.make_params(**guesses)
return update_param_vals(pars, self.prefix, **kwargs)
class BlochLawThin(Model):
r"""Bloch's law for spontaneous magnetism at low temperatures - thin film version.
Args:
T (array like):
Temperature (K)
g (float):
Lande g-factor
A(float):
The echange stiffness (Jm^{-1})
Ms(float):
Saturation moment (Am^{-1})
Returns:
(array like):
Magnetisation values corresponding to the given temperatures.
Notes:
Calculates :math:`1 - \frac{\left((\Gamma(3/2) \zeta(3/2)\right)}
{(4\pi^2)} (v_{ws} / S) (k_B T / D)^{3/2}`
This is the bulk version of Bloch's law which is not fully correct for thin films. Model adapted from code
by Dr Joseph Barker <j.barker@leeds.ac.uk>
"""
def __init__(self, *args, **kwargs):
"""Initialise the model."""
super().__init__(self.blochs_law_thinfilm, *args, **kwargs)
self.set_param_hint("g", vary=False, value=2.0)
self.set_param_hint("A", vary=True, min=0)
self.set_param_hint("Ms", vary=True, min=0)
self.prefactor = gamma(1.5) * zeta(1.5) / (4 * np.pi**2)
def blochs_law_thinfilm(self, T, D, Bz, S, v_ws, a, nz):
r"""Thin film version of Blopch's Law.
Parameters:
T (array):
Temperature (K)
D (float):
Spin wave stiffness
Bz (float):
Applied magnetic field data measured in.
S (float):
Spin number
v_ws (float):
Wigner-Seitz volume (volume per atom)
a (float):
Lattice parameter
nz (int):
Number of atomic layers in the thin film.
Returns:
(array):
normalised M_s values.
Notes:
Bloch's Law is derived from integrating the magnon spectrum over the k-space of the sample. In a thin film
one can only integrate over the x-y plane, in the z direction one needs to do an explicit sum over the
atomic layers in the z direction.
This model was derived by Dr Joseph Barker <j.barker@leeds.ac.uk>
This is solving:
:math:`1 + \frac{v_{ws}}{S}\frac{a}{4 \pi n_z}\frac{k_B T}{D}\sum^{n_z-1}_{m=0}
ln\left[1-\exp\left( -\frac{1}{k_B T}\left(g\mu_B B_z+D\left(\frac{m \pi}{a(n_z-1)}\right)^2
\right)\right) \right]`
"""
kz_sum = sum(
np.log(1.0 - np.exp(-(D * (m * np.pi / ((nz - 1) * a)) ** 2 + Bz) / (k * T))) for m in range(0, nz - 1)
)
return 1.0 + (v_ws / (4 * np.pi * S * (nz - 1) * a)) * (k * T / D) * kz_sum
[docs]class Langevin(Model):
r"""The Langevin function for paramagnetic M-H loops.
Args:
H (array): The applied magnetic field
M_s (float): Saturation magnetisation
m (float): is the moment of a single cluster
T (float): Temperature
Returns:
Magnetic Momemnts (array).
Note:
The Langevin Function is :math:`\coth(\frac{\mu_0HM_s}{k_BT})-\frac{k_BT}{\mu_0HM_s}`.
Example:
.. plot:: samples/Fitting/langevin.py
:include-source:
:outname: langevin-class
"""
def __init__(self, *args, **kwargs):
"""Configure Initial fitting function."""
super().__init__(langevin, *args, **kwargs)
[docs] def guess(self, data, x=None, **kwargs):
r"""Guess some starting values.
M_s is taken as half the difference of the range of thew M data,
we can find m/T from the susceptibility :math:`chi= M_s \mu_o m / kT`
"""
M_s = (np.max(data) - np.min(data)) / 2.0
if x is not None:
d = np.sort(np.row_stack((x, data)))
dd = savgol_filter(d, 7, 1)
yd = dd[1] / dd[0]
chi = np.interp(np.array([0]), d[0], yd)[0]
mT = chi / M_s * (k / mu_0)
# Assume T=150K for no good reason
m = mT * 150
else:
m = 1e6 * (e * hbar) / (2 * electron_mass) # guess 1 million Bohr Magnetrons
T = 150
pars = self.make_params(M_s=M_s, m=m, T=T)
return update_param_vals(pars, self.prefix, **kwargs)
[docs]class KittelEquation(Model):
r"""Kittel Equation for finding ferromagnetic resonance peak in frequency with field.
Args:
H (array): Magnetic fields in A/m
g (float): h g factor for the gyromagnetic radius
M_s (float): Magnetisation of sample in A/m
H_k (float): Anisotropy field term (including demagnetising factors) in A/m
Returns:
Reesonance peak frequencies in Hz
:math:`f = \frac{\gamma \mu_{0}}{2 \pi} \sqrt{\left(H + H_{k}\right) \left(H + H_{k} + M_{s}\right)}`
Example:
.. plot:: samples/Fitting/kittel.py
:include-source:
:outname: kittel-class
"""
display_names = ["g", "M_s", "H_k"]
def __init__(self, *args, **kwargs):
"""Configure Initial fitting function."""
super().__init__(kittelEquation, *args, **kwargs)
[docs] def guess(self, data, x=None, **kwargs):
"""Guess parameters as gamma=2, H_k=0, M_s~(pi.f)^2/(mu_0^2.H)-H."""
g = 2
H_k = 100
gamma = g * cnst.e / (2 * cnst.m_e)
M_s = (4 * np.pi**2 * data**2 - gamma**2 * cnst.mu_0**2 * (x**2 + 2 * x * H_k + H_k**2)) / (
gamma**2 * cnst.mu_0**2 * (x + H_k)
)
M_s = np.mean(M_s)
pars = self.make_params(g=g, M_s=M_s, H_k=H_k)
pars["M_s"].min = 0
pars["g"].min = g / 100
pars["H_k"].min = 0
pars["H_k"].max = M_s.max()
return update_param_vals(pars, self.prefix, **kwargs)
[docs]class Inverse_Kittel(Model):
r"""Kittel Equation for finding ferromagnetic resonance peak in frequency with field.
Args:
H (array): Magnetic fields in A/m
g (float): h g factor for the gyromagnetic radius
M_s (float): Magnetisation of sample in A/m
H_k (float): Anisotropy field term (including demagnetising factors) in A/m
Returns:
Reesonance peak frequencies in Hz
:math:`f = \frac{\gamma \mu_{0}}{2 \pi} \sqrt{\left(H + H_{k}\right) \left(H + H_{k} + M_{s}\right)}`
"""
display_names = ["g", "M_s", "H_k"]
def __init__(self, *args, **kwargs):
"""Configure Initial fitting function."""
super().__init__(inverse_kittel, *args, **kwargs)
[docs] def guess(self, data, x=None, **kwargs):
"""Guess parameters as gamma=2, H_k=0, M_s~(pi.f)^2/(mu_0^2.H)-H."""
g = 2
H_k = 100
gamma = g * cnst.e / (2 * cnst.m_e)
M_s = (4 * np.pi**2 * x**2 - gamma**2 * cnst.mu_0**2 * (data**2 + 2 * data * H_k + H_k**2)) / (
gamma**2 * cnst.mu_0**2 * (data + H_k)
)
M_s = np.mean(M_s)
pars = self.make_params(g=g, M_s=M_s, H_k=H_k)
pars["M_s"].min = 0
pars["g"].min = g / 100
pars["H_k"].min = 0
pars["H_k"].max = M_s.max()
return update_param_vals(pars, self.prefix, **kwargs)
[docs]class FMR_Power(Model):
r"""Combine a Lorentzian and differential Lorenztion peak as measured in an FMR experiment.
Args:
H (array) magnetic field data
H_res (float): Resonance field of peak
Delta_H (float): Preak wideth
K_1, K_2 (float): Relative weighting of each component.
Returns:
Array of model absorption values.
:math:`\frac{4 \Delta_{H} K_{1} \left(H - H_{res}\right)}{\left(\Delta_{H}^{2} + 4 \left(H - H_{res}
\right)^{2}\right)^{2}} - \frac{K_{2} \left(\Delta_{H}^{2} - 4 \left(H - H_{res}\right)^{2}\right)}{\left(
\Delta_{H}^{2} + 4 \left(H - H_{res}\right)^{2}\right)^{2}}`
"""
display_names = ["H_{res}", r"\Delta_H", "K_1", "K_2"]
def __init__(self, *args, **kwargs):
"""Configure Initial fitting function."""
super().__init__(fmr_power, *args, **kwargs)
[docs] def guess(self, data, x=None, **kwargs):
r"""Guess parameters as :math:`\gamma=2, H_k=0, M_s~(\pi f)^2/\mu_0^2.H)-H`."""
if x is None:
x = np.linspace(1, len(data), len(data) + 1)
x1 = x[np.argmax(data)]
x2 = x[np.argmin(data)]
Delta_H = abs(x1 - x2)
H_res = (x1 + x2) / 2.0
y1 = np.max(data)
y2 = np.min(data)
dy = y1 - y2
K_2 = dy * (4 * np.pi * Delta_H**2) / (3 * np.sqrt(3))
ay = (y1 + y2) / 2
K_1 = ay * np.pi / Delta_H
pars = self.make_params(Delta_H=Delta_H, H_res=H_res, K_1=K_1, K_2=K_2)
pars["K_1"].min = 0
pars["K_2"].min = 0
pars["Delta_H"].min = 0
pars["H_res"].min = np.min(x)
pars["H_res"].max = np.max(x)
return update_param_vals(pars, self.prefix, **kwargs)