Hyperfine parameter distributions
Hyperfine parameters can be distributed over a range of magnitudes or angles.
In order not to set a new Hyperfine
for each point in a distribution manually, Nexus allows to define distributions for hyperfine parameters.
Distributions that act on a single parameter of the hyperfine object are introduced via the Distribution
class.
Nexus has predefined distributions implemented in the library lib.distribution
, like Gaussian or Lorentzian and many more.
The Hyperfine
class has two special methods for random angular distributions in 2D or 3D.
These distributions require special treatment.
One option is to set the Hyperfine.isotropic
parameter.
It is an analytical implementation of 3D angular distributions where both, the hyperfine field and the quadrupole splitting, are randomly distributed in 3D.
In case you want
any kind of 2D angular distribution
a 3D distribution acting on only the magnetic field or the quadrupole splitting
use the Hyperfine.SetRandomDistribution()
method.
Note
Although a lib.distribution.Random
class exists, do not use this to emulate 2D or 3D angular distributions.
Two random distributions of two angles do not give a real random distribution of the angles!
Only use Hyperfine.SetRandomDistribution()
or Hyperfine.isotropic
for random angular 2D and 3D angular distributions.
A Distribution
object defines an array of delta
values and the corresponding weight
of the delta value.
The delta
values serve as an absolute difference to the target parameter.
Most of the distributions do not need to know on which parameter they act.
They just define the absolute differences to the target parameter they are applied to.
This might seem a bit weird at the beginning but actually keeps the interface simple and flexible. A distribution can be applied to any target parameter.
Internally, the delta
values are just added to the parameter they are applied to.
Assume your target parameter has a value of X. The distribution has values from -Y to Y. Then the distribution will range from X-Y to X+Y. This is how most of the predefined distributions work. They calculate a range and are then applied to any target parameter you set them to.
The delta values can also serve as absolute values when the target parameter is zero. For a distribution with delta values from X to Y and a target parameter of 0, the distribution ranges from X to Y.
Note
An experiment should not have too many distributions. Each distribution point number multiplies with the previously set distribution points in a hyperfine site. For example, having three distributions with 30 points each will result in 27000 distribution points in total for the site. In this case the calculation of the transitions takes quite long. As a rule of thumb try to stay below 1000 distribution points per experiment for reasonable speeds.
Note
For 3D random distributions in both the magnetic field and the quadrupole splitting set Hyperfine.isotropic
to True.
This will results in considerable faster calculations than setting two 3D distributions via Hyperfine.SetRandomDistribution()
.
A simple distribution
The following code should serve as the basis for the examples on distributions. Time spectra are calculated here, because effects due to distributions are better visualized in the quantum beat than in a Moessbauer spectrum.
# import packages
import nexus as nx
import numpy as np
import matplotlib.pyplot as plt
mat_Fe = nx.Material.Template(nx.lib.material.Fe)
layer_Fe = nx.Layer(id = "Fe",
material = mat_Fe,
thickness = 3000, # in nanometer
roughness = 100) # in nanometer
site = nx.Hyperfine(magnetic_field = 33,
magnetic_theta = 0,
magnetic_phi = 0)
mat_Fe.hyperfine_sites = [site]
sample = nx.Sample(layers = [layer_Fe])
beam = nx.Beam()
beam.LinearSigma()
exp = nx.Experiment(beam = beam,
objects = [sample],
isotope = nx.moessbauer.Fe57)
time_spectrum = nx.TimeSpectrum(experiment = exp,
time_length = 200,
time_step = 0.2)
time_axis, intensity = time_spectrum.Calculate()
plt.semilogy(time_axis, intensity, label = 'no dist')
Now, a distribution of the magnetic field will be added.
There are two different ways to do so.
Either you define the distribution in the initializer or you can use a Set
method to set it later.
So, either define the distribution before initializing the site
import nexus as nx
import numpy as np
import matplotlib.pyplot as plt
mat_Fe = nx.Material.Template(nx.lib.material.Fe)
layer_Fe = nx.Layer(id = "Fe",
material = mat_Fe,
thickness = 3000, # in nanometer
roughness = 100) # in nanometer
# definition of distribution
gauss_dist = nx.lib.distribution.Gaussian(points = 31,
fwhm = 1)
# initializer definition
site = nx.Hyperfine(magnetic_field = 33,
magnetic_field_dist = gauss_dist,
magnetic_theta = 0,
magnetic_phi = 0)
mat_Fe.hyperfine_sites = [site]
sample = nx.Sample(layers = [layer_Fe])
or define it via a Set method on an existing Hyperfine
object
# define a distribution from the library
# we have to define the number of points and the full width half maximum for a Gaussian distribution
gauss_dist = nx.lib.distribution.Gaussian(points = 31,
fwhm = 1) # this can also be a fittable Var
# apply the distribution to the hyperfine parameter via set method
site.SetMagneticFieldDistribution(gauss_dist)
time_axis, intensity = time_spectrum.Calculate()
# plot the results
plt.semilogy(time_axis, intensity, label = 'with dist')
plt.xlabel('time (ns)')
plt.ylabel('Intensity ($\Gamma$/ns)')
plt.legend()
plt.show()
Let’s assume the magnetization has a random distribution in the \(\sigma-\pi\) plane (in the sample plane for forward scattering) in addition to the distribution in its amplitude.
# create a random distribution of the magnetic field angles
# the target can be either the magnetic field "mag" or the "efg"
# the type are either certain 2D distributions or a 3D distribution.
# random distributions can only be applied via a Set method.
site.SetRandomDistribution(target = "mag",
type = "2Dsp", # random in the sigma-pi plane (sp)
points = 101)
plt.semilogy(*time_spectrum.Calculate(), label = 'with dist and 2D')
plt.xlabel('time (ns)')
plt.ylabel('Intensity ($\Gamma$/ns)')
plt.legend()
plt.show()
The Hyperfine
has the attribute BareHyperfines
.
It holds all actual hyperfine site implementations generated via all distributions applied to the site.
Those implementations are of type BareHyperfine
, which only hold the bare hyperfine parameters.
In order to see each actual hyperfine parameter due to distributions that belong to a hyperfine site you can use
for elem in site.BareHyperfines:
print(elem)
Predefined Distributions
The easiest is to call one of the predefined distributions. It should fit most purposes.
The lib.distribution
library holds a lot of distribution types.
For example,
# define an asymmetric Gaussian distribution from the lib.distribution.
asym_gauss_dist = nx.lib.distribution.AsymmetricGaussian(
points = 101,
hwhm_low = nx.Var(value = 0.5, min = 0, max = 2, fit = True),
hwhm_high = nx.Var(value = 1, min = 0, max = 2, fit = True))
# get the full width half maximum from the two set hwhm values
print("FWHM = {}".format(asym_gauss_dist.GetFWHM()))
# define a hyperfine site
site = nx.Hyperfine(magnetic_field = 10)
# define target parameter for the distribution
site.SetMagneticFieldDistribution(asym_gauss_dist)
# get the values and weight of the distribution
values, weight = site.GetMagneticFieldDistribution()
plt.plot(values, weight)
plt.xlabel('delta value')
plt.ylabel('weight')
plt.show()
There are two predefined distributions that must know the target value.
Those are PosGaussian
and NegGaussian
.
These distributions must therefore only be applied to the specific target parameter that it takes as an argument.
site = nx.Hyperfine(quadrupole = 0.2)
# define a Gaussian distribution with only positive values from nexus.lib.distribution.
dist = nx.lib.distribution.PosGaussian(points = 101,
fwhm = nx.Var(0.5, min = 0, max = 2, fit = True),
target_var = site.quadrupole)
# only apply this dist to the quadrupole parameter
site.SetQuadrupoleDistribution(dist)
values, weight = site.GetQuadrupoleDistribution()
plt.plot(values, weight)
plt.xlabel('delta value')
plt.ylabel('weight')
plt.show()
Random Distributions
Note
This module is optimized in version 1.2.0.
The function SetRandomDistribution(target, type, points, method, order)()
now takes two more arguments, method
and order
parameters.
The method
attribute specifies the algorithm used, either random or model.
The new random method is similar to the old random implementation, however, in the old version it used a true random sampling over the circles or spheres, which does not guarantee the correct spectrum.
Now, random sampling based on vector parameter permutation and mirroring is used, which leads to better results.
The new method model uses a group theoretical approach for optimized vector sampling.
This method reduces the number of points necessary and is the default method.
The given point number is not taken into account in this case.
For 3D (spherical) distributions an order parameter can be specified, which determines the number of vector basis sets used.
An order parameter of order=1
is sufficient for most cases.
The Hyperfine.SetRandomDistribution()
method is used to set special multi-dimensional angular distributions.
Those can be applied to either the magnetic hyperfine field or the quadrupole splitting.
The distributions itself can be 3D or 2D, the latter lies in either of the planes spanned by the basis vectors given by \(\vec{\sigma}\), \(\vec{\pi}\) and \(\vec{k}\).
site = nx.Hyperfine(magnetic_field = 33)
# define a 2D random distribution of the magnetic field
# in the plane spanned by sigma and pi vectors ("sp")
site.SetRandomDistribution(target = "mag", type = "2Dsp", points = 401)
Note
The GetXXXDistribution() methods do not work for these random distributions.
Note
It is recommended to take at least 200 distribution points for magnetic random distributions. For EFG random distributions even larger values are recommended to about 1000 points.
Here are the possible random distributions on the quadrupole splitting.
# import packages
import nexus as nx
import numpy as np
import matplotlib.pyplot as plt
mat_Fe = nx.Material.Template(nx.lib.material.Fe)
layer_Fe = nx.Layer(id = "Fe",
material = mat_Fe,
thickness = 3000, # in nanometer
roughness = 100, # in nanometer
)
site = nx.Hyperfine(quadrupole = 1.0)
mat_Fe.hyperfine_sites = [site]
sample = nx.Sample(layers = [layer_Fe])
beam = nx.Beam()
beam.LinearSigma()
exp = nx.Experiment(beam = beam,
objects = [sample],
isotope = nx.moessbauer.Fe57
)
time_spectrum = nx.TimeSpectrum(experiment = exp,
time_length = 200,
time_step = 0.2,
max_detuning = 0)
fig, axs = plt.subplots(2,2, sharex=True, sharey=True)
fig.suptitle('Random distributions efg')
site.isotropic = True
axs[0,0].semilogy(*time_spectrum.Calculate(), label = 'isotropic', color = 'orange')
site.isotropic = False
points = 1001
site.SetRandomDistribution("efg", "3D", points)
# since v 1.2.0 it is valid to write
site.SetRandomDistribution("efg", "3D")
axs[0,0].semilogy(*time_spectrum.Calculate(), label = '3D', linestyle='dashed')
axs[0,0].legend()
axs[0,0].set_ylabel('Intensity ($\Gamma$/ns)')
axs[0,0].set_ylim([1e-5, 4e-3])
site.SetRandomDistribution("efg", "2Dsp", points)
axs[0,1].semilogy(*time_spectrum.Calculate(), label = '2Dsp')
axs[0,1].legend()
site.SetRandomDistribution("efg", "2Dsk", points)
axs[1,0].semilogy(*time_spectrum.Calculate(), label = '2Dsk')
axs[1,0].legend()
axs[1,0].set_xlabel('time (ns)')
axs[1,0].set_ylabel('Intensity ($\Gamma$/ns)')
site.SetRandomDistribution("efg", "2Dpk", points)
axs[1,1].semilogy(*time_spectrum.Calculate(), label = '2Dpk')
axs[1,1].legend()
axs[1,1].set_xlabel('time (ns)')
fig.show()
and the ones for the magnetic hyperfine field
# import packages
import nexus as nx
import numpy as np
import matplotlib.pyplot as plt
mat_Fe = nx.Material.Template(nx.lib.material.Fe)
layer_Fe = nx.Layer(id = "Fe",
material = mat_Fe,
thickness = 3000, # in nanometer
roughness = 100, # in nanometer
)
site = nx.Hyperfine(magnetic_field = 33)
mat_Fe.hyperfine_sites = [site]
sample = nx.Sample(layers = [layer_Fe])
beam = nx.Beam()
beam.LinearSigma()
exp = nx.Experiment(beam = beam,
objects = [sample],
isotope = nx.moessbauer.Fe57)
time_spectrum = nx.TimeSpectrum(experiment = exp,
time_length = 200,
time_step = 0.2,
max_detuning = 0)
fig, axs = plt.subplots(2,2)
fig.suptitle('Random distributions mag')
site.isotropic = True
points = 401
axs[0,0].semilogy(*time_spectrum.Calculate(), label = 'isotropic', color = 'orange')
site.isotropic = False
site.SetRandomDistribution("mag", "3D", points)
# since v 1.2.0 it is valid to write
site.SetRandomDistribution("mag", "3D")
axs[0,0].semilogy(*time_spectrum.Calculate(), label = '3D', linestyle='dashed')
axs[0,0].legend()
axs[0,0].set_ylabel('Intensity ($\Gamma$/ns)')
site.SetRandomDistribution("mag", "2Dsp", points)
axs[0,1].semilogy(*time_spectrum.Calculate(), label = '2Dsp')
axs[0,1].legend()
site.SetRandomDistribution("mag", "2Dsk", points)
axs[1,0].semilogy(*time_spectrum.Calculate(), label = '2Dsk')
axs[1,0].legend()
axs[1,0].set_xlabel('time (ns)')
axs[1,0].set_ylabel('Intensity ($\Gamma$/ns)')
site.SetRandomDistribution("mag", "2Dpk", points)
axs[1,1].semilogy(*time_spectrum.Calculate(), label = '2Dpk')
axs[1,1].legend()
axs[1,1].set_xlabel('time (ns)')
fig.show()
Distribution from arrays
One can pass two arrays directly to a distribution. The first array is either the absolute difference or the absolute value of the hyperfine parameter. The second array is the weight.
With absolute differences
delta = np.linspace(-5, 5, 6) # absolute differences to the target value
weight = np.linspace(0.1, 1, 6) # some weight values
array_dist = nx.lib.distribution.Array(delta, weight)
site = nx.Hyperfine(magnetic_field = 30, # distribution will range from 25 to 35
magnetic_theta = 90,
magnetic_phi = 30)
site.SetMagneticFieldDistribution(array_dist)
# Print all combinations applied to the site
print(site)
# print each parameter combination due to distributions stored in site.BareHyperfines
for elem in site.BareHyperfines:
print(elem)
With absolute values
values = np.linspace(1, 30, 6) # absolute values
weight = np.square(values) # some dependence on values
array_dist = nx.lib.distribution.Array(values, weight)
array_dist.DistributionFunction()
site = nx.Hyperfine(magnetic_field = 0, # zero because absolute values are assumed
magnetic_theta = 90,
magnetic_phi = 30
)
site.SetMagneticFieldDistribution(array_dist)
print(site)
for elem in site.BareHyperfines:
print(elem)
Distribution from file
The distribution can be set with a file containing the distribution points and the weights, see
distribution.txt
.
31.2 0.3 32.2 0.5 33.2 0.8 34.2 0.3 35.2 0.1
The first column gives absolute differences or absolute values, e.g. the magnetic hyperfine field, and the second column the relative weights. Load the distribution with
# load a Distribution from a file
file_dist = nx.lib.distribution.File("distribution.txt")
site = nx.Hyperfine(magnetic_field = 0, # zero because absolute field values are given
magnetic_theta = 90,
magnetic_phi = 30)
site.SetMagneticFieldDistribution(file_dist)
for elem in site.BareHyperfines:
print(elem)
User defined distributions
User-defined distributions are advanced methods to create arbitrary distributions acting on a hyperfine site.
The user must define a new distribution and provide the implementation of the mathematical function and the possible fit parameters.
You define a child class inherited from the parent class Distribution
.
In this example, a Gaussian distribution is implemented by the user.
The \(\sigma\) value of the Gaussian should be a fittable value.
class myGaussianDist(nx.Distribution):
def __init__(self, points):
# class inheritance
super().__init__(points, "user defined Gaussian")
# define a sigma class attribute
self.sigma = nx.Var(value = 2, min = 0, max = 5, fit = True, id = "sigma")
# register fit variables
self.fit_varibales = [self.sigma]
This will define the new derived class myGaussianDist
with a class instance attribute sigma of type Var
that can be fit.
Next, implement the actual DistributionFunction()
.
The DistributionFunction()
must set the delta
values either by SetRange()
or SetDelta()
and the weight
by SetWeight()
.
Those are the only requirements on this function.
The SetRange()
method sets the delta
values symmetrically around zero over the given range, while the SetDelta()
directly sets the values.
Use numpy whenever possible for the function implementation for efficient calculations.
# define the distribution function
def DistributionFunction(self):
# prevent division by zero in fitting
if self.sigma.value == 0:
self.sigma.value = 1e-299
# set the delta values to a reasonable range
x = self.SetRange(6 * self.sigma.value)
# to process the x array with operators like +,-,*,/ in numpy, convert to numpy array
x = np.array(x)
# calculate the weight
weight = np.exp(-1/2 * np.square(x) / np.square(self.sigma.value))
# set the weight values
self.SetWeight(weight)
The complete code for the distribution is
class myGaussianDist(nx.Distribution):
def __init__(self, points):
super().__init__(points, "user defined Gaussian")
self.sigma = nx.Var(value = 2, min = 0, max = 5, fit = True, id = "sigma")
self.fit_varibales = [self.sigma]
def DistributionFunction(self):
if self.sigma.value == 0:
self.sigma.value = 1e-299
x = self.SetRange(6 * self.sigma.value)
weight = np.exp(-1/2 * np.square(x) / np.square(self.sigma.value))
self.SetWeight(weight)
my_dist = myGaussianDist(points = 101)
In order to make your class more flexible and to use various instances of it, you can pass the sigma value in the __init__
method and define a class attribute which is given to the __init__
input.
class myGaussianDist(nx.Distribution):
# put sigma to __init__ method
def __init__(self, points, sigma):
super().__init__(points, "user defined Gaussian")
# set the class attribute to sigma
self.sigma = sigma
self.fit_varibales = [sigma]
def DistributionFunction(self):
if self.sigma.value == 0:
self.sigma.value = 1e-299
x = self.SetRange(6*self.sigma.value)
weight = np.exp(-1/2*np.square(x)/(np.square(self.sigma.value)))
self.SetWeight(weight)
my_dist = myGaussianDist(points = 101, sigma = nx.Var(1, min = 0, max = 7, fit = True))
sigma
must be a Var
object in this case. float assignment will not work. To do so you can change the code to:
class myGaussianDist(nx.Distribution):
def __init__(self, points, sigma):
super().__init__(points, "user defined Gaussian")
self.sigma = sigma
self.fit_varibales = [sigma]
def DistributionFunction(self):
if self.sigma == 0:
self.sigma = 1e-299
x = self.SetRange(6*self.sigma)
weight = np.exp(-1/2*np.square(x)/(np.square(self.sigma)))
self.SetWeight(weight)
my_dist = myGaussianDist(points = 101, sigma = 1)
But sigma
will not be fittable in this case.
The user-defined functions do not need an analytical function to work.
The lib.distribution.Array
and lib.distribution.File
distributions are also based on the Distribution
parent class.
For more advanced class definitions have a look to lib.distribution
implementations.