# This file is part of xrayutilities.
#
# xrayutilities is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, see <http://www.gnu.org/licenses/>.
#
# Copyright (c) 2016-2024 Dominik Kriegner <dominik.kriegner@gmail.com>
import abc
import copy
import math as pymath
import numpy
import scipy.constants as constants
import scipy.integrate as integrate
import scipy.interpolate as interpolate
from scipy.special import erf, j0
from .. import config, utilities
from ..exception import InputError
from ..experiment import Experiment
from ..materials import Amorphous
from ..math import NormGauss1d, NormLorentz1d, heaviside, solve_quartic
from .smaterials import Layer, LayerStack
[docs]
def startdelta(start, delta, num):
end = start + delta * (num - 1)
return numpy.linspace(start, end, int(num))
[docs]
class Model:
"""
generic model class from which further models can be derived from
"""
[docs]
def __init__(self, experiment, **kwargs):
"""
constructor for a generical simulation model.
currently only the experiment class describing the diffraction geometry
is stored in the base class
Parameters
----------
experiment : Experiment
class describing the diffraction geometry, energy and wavelength of
the model
resolution_width : float, optional
defines the width of the resolution
I0 : float, optional
the primary beam flux/intensity
background : float, optional
the background added to the simulation
energy : float or str
the experimental energy in eV
resolution_type : {'Gauss', 'Lorentz'}, optional
type of resolution function, default: Gauss
"""
local_fit_params = {'resolution_width': 'width of the resolution',
'I0': 'primary beam intensity',
'background': 'background intensity',
'energy': 'x-ray energy in eV'}
if not hasattr(self, 'fit_paramnames'):
self.fit_paramnames = []
self.fit_paramnames += local_fit_params
valid_kwargs = copy.copy(local_fit_params)
valid_kwargs['resolution_type'] = 'resolution function typ'
utilities.check_kwargs(kwargs, valid_kwargs,
self.__class__.__name__)
if experiment:
self.exp = experiment
else:
self.exp = Experiment([1, 0, 0], [0, 0, 1])
self.resolution_width = kwargs.get('resolution_width', 0)
self.resolution_type = kwargs.get('resolution_type', 'Gauss')
self.I0 = kwargs.get('I0', 1)
self.background = kwargs.get('background', 0)
if 'energy' in kwargs:
self.energy = kwargs['energy']
@property
def energy(self):
return self.exp.energy
@energy.setter
def energy(self, en):
self.exp.energy = en
[docs]
def convolute_resolution(self, x, y):
"""
convolve simulation result with a resolution function
Parameters
----------
x : array-like
x-values of the simulation, units of x also decide about the unit
of the resolution_width parameter
y : array-like
y-values of the simulation
Returns
-------
array-like
convoluted y-data with same shape as y
"""
if self.resolution_width == 0:
return y
dx = numpy.mean(numpy.gradient(x))
nres = int(20 * numpy.abs(self.resolution_width / dx))
xres = startdelta(-10*self.resolution_width, dx, nres + 1)
# the following works only exactly for equally spaced data points
if self.resolution_type == 'Gauss':
fres = NormGauss1d
else:
fres = NormLorentz1d
resf = fres(xres, numpy.mean(xres), self.resolution_width)
resf /= numpy.sum(resf) # proper normalization for discrete conv.
# pad y to avoid edge effects
interp = interpolate.InterpolatedUnivariateSpline(x, y, k=1)
nextmin = numpy.ceil(nres/2.)
nextpos = numpy.floor(nres/2.)
xext = numpy.concatenate(
(startdelta(x[0]-dx, -dx, nextmin)[-1::-1],
x,
startdelta(x[-1]+dx, dx, nextpos)))
ypad = numpy.asarray(interp(xext))
return numpy.convolve(ypad, resf, mode='valid')
[docs]
def scale_simulation(self, y):
"""
scale simulation result with primary beam flux/intensity and add a
background.
Parameters
----------
y : array-like
y-values of the simulation
Returns
-------
array-like
scaled y-values
"""
return y * self.I0 + self.background
[docs]
class LayerModel(Model, utilities.ABC):
"""
generic model class from which further thin film models can be derived from
"""
[docs]
def __init__(self, *args, **kwargs):
"""
constructor for a thin film model. The arguments consist of a
LayerStack or individual Layer(s). Optional parameters are specified
in the keyword arguments.
Parameters
----------
*args : LayerStack or Layers
either one LayerStack or several Layer objects can be given
**kwargs : dict
optional parameters for the simulation. ones not listed below are
forwarded to the superclass.
experiment : Experiment, optional
class containing geometry and energy of the experiment.
polarization: {'S', 'P', 'both'}, optional
polarization of the x-ray beam. If set to 'both' also Cmono, the
polarization factor of the monochromator should be set
Cmono : float, optional
polarization factor of the monochromator
"""
exp = kwargs.pop('experiment', None)
self.polarization = kwargs.pop('polarization', 'S')
self.Cmono = kwargs.pop('Cmono', 1)
super().__init__(exp, **kwargs)
self.lstack_params = []
self.lstack_structural_params = False
self.xlabelstr = 'x (1)'
if len(args) == 1:
if isinstance(args[0], Layer):
self.lstack = LayerStack(
f"Stack for {self.__class__.__name__}", *args)
else:
self.lstack = args[0]
else:
self.lstack = LayerStack(
f"Stack for {self.__class__.__name__}", *args)
[docs]
@abc.abstractmethod
def simulate(self):
"""
abstract method that every implementation of a LayerModel has to
override.
"""
def _create_return(self, x, E, ai=None, af=None, Ir=None,
rettype='intensity'):
"""
function to create the return value of a simulation. by default only
the diffracted intensity is returned. However, optionally also the
incidence and exit angle as well as the reflected intensity can be
returned.
Parameters
----------
x : array-like
independent coordinate value for the convolution with the
resolution function
E : array-like
electric field amplitude (complex)
ai, af : array-like, optional
incidence and exit angle of the XRD beam (in radians)
Ir : array-like, optional
reflected intensity
rettype : {'intensity', 'field', 'all'}, optional
type of the return value. 'intensity' (default): returns the
diffracted beam flux convoluted with the resolution function;
'field': returns the electric field (complex) without convolution
with the resolution function, 'all': returns the electric field,
ai, af (both in degree), and the reflected intensity.
Returns
-------
return value depends on value of rettype.
"""
if rettype == 'field':
ret = E
elif rettype == 'all':
ret = (E, numpy.degrees(ai), numpy.degrees(af), Ir)
else: # default: rettype == 'intensity'
ret = self.scale_simulation(
self.convolute_resolution(x, numpy.abs(E)**2))
return ret
[docs]
def get_polarizations(self):
"""
return list of polarizations which should be calculated
"""
if self.polarization == 'both':
return ('S', 'P')
return (self.polarization, )
[docs]
def join_polarizations(self, Is, Ip):
"""
method to calculate the total diffracted intensity from the intensities
of S and P-polarization.
"""
if self.polarization == 'both':
ret = (Is + self.Cmono * Ip) / (1 + self.Cmono)
else:
if self.polarization == 'S':
ret = Is
else:
ret = Ip
return ret
[docs]
class KinematicalModel(LayerModel):
"""
Kinematical diffraction model for specular and off-specular qz-scans. The
model calculates the kinematical contribution of one (hkl) Bragg peak,
however considers the variation of the structure factor for different 'q'.
The surface geometry is specified using the Experiment-object given to the
constructor.
"""
[docs]
def __init__(self, *args, **kwargs):
"""
constructor for a kinematic thin film model. The arguments consist of a
LayerStack or individual Layer(s). Optional parameters are specified in
the keyword arguments.
Parameters
----------
*args : LayerStack or Layers
either one LayerStack or several Layer objects can be given
**kwargs : dict
optional parameters; also see LayerModel/Model.
experiment : Experiment
Experiment class containing geometry and energy of the experiment.
"""
super().__init__(*args, **kwargs)
self.lstack_params += ['thickness', ]
self.lstack_structural_params = True
self.xlabelstr = r'momentum transfer $Q_z$ ($\mathrm{\AA^{-1}}$)'
# precalc optical properties
self._init_en = 0
self.init_chi0()
[docs]
def init_chi0(self):
"""
calculates the needed optical parameters for the simulation. If any of
the materials/layers is changing its properties this function needs to
be called again before another correct simulation is made. (Changes of
thickness does NOT require this!)
"""
if self._init_en != self.energy: # recalc properties if energy changed
self.chi0 = numpy.asarray([layer.material.chi0(en=self.energy)
for layer in self.lstack])
self._init_en = self.energy
def _prepare_kincalculation(self, qz, hkl):
"""
prepare kinematic calculation by calculating some helper values
"""
rel = constants.physical_constants['classical electron radius'][0]
rel *= 1e10
k = self.exp.k0
# determine q-inplane
t = self.exp._transform
ql0 = t(self.lstack[0].material.Q(*hkl))
qinp = numpy.sqrt(ql0[0]**2 + ql0[1]**2)
# calculate needed angles
qv = numpy.asarray([t.inverse((ql0[0], ql0[1], q)) for q in qz])
Q = numpy.linalg.norm(qv, axis=1)
theta = numpy.arcsin(Q / (2 * k))
domega = numpy.arctan2(qinp, qz)
alphai, alphaf = (theta + domega, theta - domega)
# calculate structure factors
f = numpy.empty((len(self.lstack), len(qz)), dtype=complex)
fhkl = numpy.empty(len(self.lstack), dtype=complex)
for i, l in enumerate(self.lstack):
m = l.material
fhkl[i] = m.StructureFactor(m.Q(*hkl), en=self.energy) /\
m.lattice.UnitCellVolume()
f[i, :] = m.StructureFactorForQ(qv, en0=self.energy) /\
m.lattice.UnitCellVolume()
E = numpy.zeros(len(qz), dtype=complex)
return rel, alphai, alphaf, f, fhkl, E, t
def _get_qz(self, qz, alphai, alphaf, chi0, absorption, refraction):
k = self.exp.k0
q = qz.astype(complex)
if absorption and not refraction:
q += 1j * k * numpy.imag(chi0) / \
numpy.sin((alphai + alphaf) / 2)
if refraction:
q = k * (numpy.sqrt(numpy.sin(alphai)**2 + chi0) +
numpy.sqrt(numpy.sin(alphaf)**2 + chi0))
return q
[docs]
def simulate(self, qz, hkl, absorption=False, refraction=False,
rettype='intensity'):
"""
performs the actual kinematical diffraction calculation on the Qz
positions specified considering the contribution from a single Bragg
peak.
Parameters
----------
qz : array-like
simulation positions along qz
hkl : list or tuple
Miller indices of the Bragg peak whos truncation rod should be
calculated
absorption : bool, optional
flag to tell if absorption correction should be used
refraction : bool, optional
flag to tell if basic refraction correction should be performed. If
refraction is True absorption correction is also included
independent of the absorption flag.
rettype : {'intensity', 'field', 'all'}
type of the return value. 'intensity' (default): returns the
diffracted beam flux convoluted with the resolution function;
'field': returns the electric field (complex) without convolution
with the resolution function, 'all': returns the electric field,
ai, af (both in degree), and the reflected intensity.
Returns
-------
array-like
return value depends on the setting of `rettype`, by default only
the calculate intensity is returned
"""
self.init_chi0()
rel, ai, af, _, fhkl, E, t = self._prepare_kincalculation(qz, hkl)
# calculate interface positions
z = numpy.zeros(len(self.lstack))
for i, l in enumerate(self.lstack[-1:0:-1]):
z[-i-2] = z[-i-1] - l.thickness
# perform kinematical calculation
for i, l in enumerate(self.lstack):
q = self._get_qz(qz, ai, af, self.chi0[i], absorption, refraction)
q -= t(l.material.Q(*hkl))[-1]
if l.thickness == numpy.inf:
E += fhkl[i] * numpy.exp(-1j * z[i] * q) / (1j * q)
else:
E += fhkl[i] * numpy.exp(-1j * q * z[i]) * \
(1 - numpy.exp(1j * q * l.thickness)) / (1j * q)
wf = numpy.sqrt(heaviside(ai) * heaviside(af) * rel**2 /
(numpy.sin(ai) * numpy.sin(af))) * E
return self._create_return(qz, wf, ai, af, rettype=rettype)
[docs]
class KinematicalMultiBeamModel(KinematicalModel):
"""
Kinematical diffraction model for specular and off-specular qz-scans. The
model calculates the kinematical contribution of several Bragg peaks on
the truncation rod and considers the variation of the structure factor.
In order to use a analytical description for the kinematic diffraction
signal all layer thicknesses are changed to a multiple of the respective
lattice parameter along qz. Therefore this description only works for (001)
surfaces.
"""
[docs]
def __init__(self, *args, **kwargs):
"""
constructor for a kinematic thin film model. The arguments consist of a
LayerStack or individual Layer(s). Optional parameters are specified in
the keyword arguments.
Parameters
----------
*args : LayerStack or Layers
either one LayerStack or several Layer objects can be given
**kwargs : dict
optional parameters. see also LayerModel/Model.
experiment : Experiment
Experiment class containing geometry and energy of the experiment.
surface_hkl : list or tuple
Miller indices of the surface (default: (0, 0, 1))
"""
self.surface_hkl = kwargs.pop('surface_hkl', (0, 0, 1))
super().__init__(*args, **kwargs)
[docs]
def simulate(self, qz, hkl, absorption=False, refraction=True,
rettype='intensity'):
"""
performs the actual kinematical diffraction calculation on the Qz
positions specified considering the contribution from a full
truncation rod
Parameters
----------
qz : array-like
simulation positions along qz
hkl : list or tuple
Miller indices of the Bragg peak whos truncation rod should be
calculated
absorption : bool, optional
flag to tell if absorption correction should be used
refraction : bool, optional,
flag to tell if basic refraction correction should be performed. If
refraction is True absorption correction is also included
independent of the absorption flag.
rettype : {'intensity', 'field', 'all'}
type of the return value. 'intensity' (default): returns the
diffracted beam flux convoluted with the resolution function;
'field': returns the electric field (complex) without convolution
with the resolution function, 'all': returns the electric field,
ai, af (both in degree), and the reflected intensity.
Returns
-------
array-like
return value depends on the setting of `rettype`, by default only
the calculate intensity is returned
"""
self.init_chi0()
rel, ai, af, f, _, E, t = self._prepare_kincalculation(qz, hkl)
# calculate interface positions for integer unit-cell thickness
z = numpy.zeros(len(self.lstack))
for i, l in enumerate(self.lstack[-1:0:-1]):
lat = l.material.lattice
a3 = t(lat.GetPoint(*self.surface_hkl))[-1]
n3 = l.thickness // a3
z[-i-2] = z[-i-1] - a3 * n3
if config.VERBOSITY >= config.INFO_LOW and \
numpy.abs(l.thickness/a3 - n3) > 0.01:
print('XU.KinematicMultiBeamModel: %s thickness changed from'
' %.2fA to %.2fA (%d UCs)' % (l.name, l.thickness,
a3 * n3, n3))
# perform kinematical calculation
for i, l in enumerate(self.lstack):
q = self._get_qz(qz, ai, af, self.chi0[i], absorption, refraction)
lat = l.material.lattice
a3 = t(lat.GetPoint(*self.surface_hkl))[-1]
if l.thickness == numpy.inf:
E += f[i, :] * a3 * numpy.exp(-1j * z[i] * q) /\
(1 - numpy.exp(1j * q * a3))
else:
n3 = l.thickness // a3
E += f[i, :] * a3 * numpy.exp(-1j * z[i] * q) * \
(1 - numpy.exp(1j * q * a3 * n3)) /\
(1 - numpy.exp(1j * q * a3))
wf = numpy.sqrt(heaviside(ai) * heaviside(af) * rel**2 /
(numpy.sin(ai) * numpy.sin(af))) * E
return self._create_return(qz, wf, ai, af, rettype=rettype)
[docs]
class SimpleDynamicalCoplanarModel(KinematicalModel):
"""
Dynamical diffraction model for specular and off-specular qz-scans.
Calculation of the flux of reflected and diffracted waves for general
asymmetric coplanar diffraction from an arbitrary pseudomorphic multilayer
is performed by a simplified 2-beam theory (2 tiepoints, S and P
polarizations)
No restrictions are made for the surface orientation.
The first layer in the model is always assumed to be the semiinfinite
substrate indepentent of its given thickness
Note:
This model should not be used in real life scenarios since the made
approximations severely fail for distances far from the reference
position.
"""
[docs]
def __init__(self, *args, **kwargs):
"""
constructor for a diffraction model. The arguments consist of a
LayerStack or individual Layer(s). Optional parameters are specified
in the keyword arguments.
Parameters
----------
*args : LayerStack or Layers
either one LayerStack or several Layer objects can be given
**kwargs: dict
optional parameters for the simulation
I0 : float, optional
the primary beam intensity
background : float, optional
the background added to the simulation
resolution_width : float, optional
the width of the resolution (deg)
polarization: {'S', 'P', 'both'}
polarization of the x-ray beam. If set to 'both' also Cmono, the
polarization factor of the monochromator should be set
Cmono : float, optional
polarization factor of the monochromator
energy : float or str
the experimental energy in eV
experiment : Experiment
Experiment class containing geometry of the sample; surface
orientation!
"""
if not hasattr(self, 'fit_paramnames'):
self.fit_paramnames = []
self.fit_paramnames += ['Cmono', ]
super().__init__(*args, **kwargs)
self.xlabelstr = 'incidence angle (deg)'
self.hkl = None
self.chih = None
self.chimh = None
[docs]
def set_hkl(self, *hkl):
"""
To speed up future calculations of the same Bragg peak optical
parameters can be pre-calculated using this function.
Parameters
----------
hkl : list or tuple
Miller indices of the Bragg peak for the calculation
"""
if hkl != (None, ):
newhkl = []
if len(hkl[0]) == len(self.lstack):
# assume one hkl is given for each layer
try:
for indices in hkl[0]:
if len(indices) == 3:
newhkl.append(indices)
else:
break
except TypeError:
pass
if len(newhkl) != len(self.lstack):
if len(hkl) < 3:
hkl = hkl[0]
if len(hkl) < 3:
raise InputError("need 3 Miller indices")
newhkl = [hkl,] * len(self.lstack)
newhkl = numpy.asarray(newhkl)
else:
newhkl = self.hkl
if self.energy != self._init_en or numpy.any(newhkl != self.hkl):
self.hkl = newhkl
self._init_en = self.energy
# calculate chih
self.chih = {'S': [], 'P': []}
self.chimh = {'S': [], 'P': []}
for hkl, lay in zip(self.hkl, self.lstack):
q = lay.material.Q(hkl)
thetaB = numpy.arcsin(numpy.linalg.norm(q) / 2 / self.exp.k0)
ch = lay.material.chih(q, en=self.energy, polarization='S')
self.chih['S'].append(-ch[0] + 1j*ch[1])
self.chih['P'].append((-ch[0] + 1j*ch[1]) *
numpy.abs(numpy.cos(2*thetaB)))
if not lay.material.lattice.iscentrosymmetric:
ch = lay.material.chih(-q, en=self.energy,
polarization='S')
self.chimh['S'].append(-ch[0] + 1j*ch[1])
self.chimh['P'].append((-ch[0] + 1j*ch[1]) *
numpy.abs(numpy.cos(2*thetaB)))
for pol in ('S', 'P'):
self.chih[pol] = numpy.asarray(self.chih[pol])
self.chimh[pol] = numpy.asarray(self.chimh[pol])
def _prepare_dyncalculation(self, geometry):
"""
prepare dynamical calculation by calculating some helper values
"""
t = self.exp._transform
# use hkl of substrate
ql0 = t(self.lstack[0].material.Q(*self.hkl[0]))
hx = numpy.sqrt(ql0[0]**2 + ql0[1]**2)
if geometry == 'lo_hi':
hx = -hx
# calculate vertical diffraction vector components and strain
hz = numpy.zeros(len(self.lstack))
for i, (hkl, l) in enumerate(zip(self.hkl, self.lstack)):
hz[i] = t(l.material.Q(*hkl))[2]
return t, hx, hz
[docs]
def simulate(self, alphai, hkl=None, geometry='hi_lo', idxref=1):
"""
performs the actual diffraction calculation for the specified
incidence angles.
Parameters
----------
alphai : array-like
vector of incidence angles (deg)
hkl : list or tuple, optional
Miller indices of the diffraction vector (preferable use set_hkl
method to speed up repeated calculations of the same peak!)
geometry : {'hi_lo', 'lo_hi'}, optional
'hi_lo' for grazing exit (default) and 'lo_hi' for grazing
incidence
idxref : int, optional
index of the reference layer. In order to get accurate peak
position of the film peak you want this to be the index of the film
peak (default: 1). For the substrate use 0.
Returns
-------
array-like
vector of intensities of the diffracted signal
"""
self.set_hkl(hkl)
# return values
Ih = {'S': numpy.zeros(len(alphai)), 'P': numpy.zeros(len(alphai))}
_, hx, hz = self._prepare_dyncalculation(geometry)
epsilon = (hz[idxref] - hz) / hz
k = self.exp.k0
thetaB = numpy.arcsin(numpy.sqrt(hx**2 + hz[idxref]**2) / 2 / k)
# asymmetry angle
asym = numpy.arctan2(hx, hz[idxref])
gamma0 = numpy.sin(asym + thetaB)
gammah = numpy.sin(asym - thetaB)
# deviation of the incident beam from the kinematical maximum
eta = numpy.radians(alphai) - thetaB - asym
xs = None # avoid linting error in code below
for pol in self.get_polarizations():
x = numpy.zeros(len(alphai), dtype=complex)
for i, l in enumerate(self.lstack):
beta = (2 * eta * numpy.sin(2 * thetaB) +
self.chi0[i] * (1 - gammah / gamma0) -
2 * gammah * (gamma0 - gammah) * epsilon[i])
y = beta / 2 / numpy.sqrt(self.chih[pol][i] *
self.chimh[pol][i]) /\
numpy.sqrt(numpy.abs(gammah) / gamma0)
c1 = -numpy.sqrt(self.chih[pol][i] / self.chih[pol][i] *
gamma0 / numpy.abs(gammah)) *\
(y + numpy.sqrt(y**2 - 1))
c2 = -numpy.sqrt(self.chih[pol][i] / self.chimh[pol][i] *
gamma0 / numpy.abs(gammah)) *\
(y - numpy.sqrt(y**2 - 1))
kz2mkz1 = k * numpy.sqrt(self.chih[pol][i] *
self.chimh[pol][i] / gamma0 /
numpy.abs(gammah)) *\
numpy.sqrt(y**2 - 1)
if i == 0: # substrate
pp = numpy.abs(gammah) / gamma0 * numpy.abs(c1)**2
m = pp < 1
x[m] = c1[m]
m = pp >= 1
x[m] = c2[m]
else: # layers
cphi = numpy.exp(1j * kz2mkz1 * l.thickness)
x = (c1 * c2 * (cphi - 1) + xs * (c1 - cphi * c2)) /\
(cphi * c1 - c2 + xs * (1 - cphi))
xs = x
Ih[pol] = numpy.abs(x)**2 * numpy.abs(gammah) / gamma0
ret = self.join_polarizations(Ih['S'], Ih['P'])
return self.scale_simulation(self.convolute_resolution(alphai, ret))
[docs]
class DynamicalModel(SimpleDynamicalCoplanarModel):
"""
Dynamical diffraction model for specular and off-specular qz-scans.
Calculation of the flux of reflected and diffracted waves for general
asymmetric coplanar diffraction from an arbitrary pseudomorphic multilayer
is performed by a generalized 2-beam theory (4 tiepoints, S and P
polarizations)
The first layer in the model is always assumed to be the semiinfinite
substrate indepentent of its given thickness
"""
[docs]
def simulate(self, alphai, hkl=None, geometry='hi_lo',
rettype='intensity'):
"""
performs the actual diffraction calculation for the specified
incidence angles and uses an analytic solution for the quartic
dispersion equation
Parameters
----------
alphai : array-like
vector of incidence angles (deg)
hkl : list or tuple, optional
Miller indices of the diffraction vector (preferable use set_hkl
method to speed up repeated calculations of the same peak!)
geometry : {'hi_lo', 'lo_hi'}, optional
'hi_lo' for grazing exit (default) and 'lo_hi' for grazing
incidence
rettype : {'intensity', 'field', 'all'}, optional
type of the return value. 'intensity' (default): returns the
diffracted beam flux convoluted with the resolution function;
'field': returns the electric field (complex) without convolution
with the resolution function, 'all': returns the electric field,
ai, af (both in degree), and the reflected intensity.
Returns
-------
array-like
vector of intensities of the diffracted signal, possibly changed
return value due the rettype setting!
"""
if len(self.get_polarizations()) > 1 and rettype != "intensity":
raise ValueError('XU:DynamicalModel: return type (%s) not '
'supported with multiple polarizations!')
rettype = 'intensity'
self.set_hkl(hkl)
# return values
Ih = {'S': numpy.zeros(len(alphai)), 'P': numpy.zeros(len(alphai))}
Ir = {'S': numpy.zeros(len(alphai)), 'P': numpy.zeros(len(alphai))}
_, hx, hz = self._prepare_dyncalculation(geometry)
k = self.exp.k0
kc = k * numpy.sqrt(1 + self.chi0)
ai = numpy.radians(alphai)
Kix = k * numpy.cos(ai)
Kiz = -k * numpy.sin(ai)
Khz = numpy.sqrt(k**2 - (Kix + hx)**2)
pp = Khz / k
mask = numpy.logical_and(pp > 0, pp < 1)
ah = numpy.zeros(len(ai)) # exit angles
ah[mask] = numpy.arcsin(pp[mask])
nal = len(ai)
Ps = None # make linter happy
for pol in self.get_polarizations():
if pol == 'S':
CC = numpy.ones(nal)
else:
CC = abs(numpy.cos(ai+ah))
pom = k**4 * self.chih['S'] * self.chimh['S']
if config.VERBOSITY >= config.INFO_ALL:
print(f'XU.DynamicalModel: calc. {pol}-polarization...')
M = numpy.zeros((nal, 4, 4), dtype=complex)
for j in range(4):
M[:, j, j] = numpy.ones(nal)
for i, l in enumerate(self.lstack[-1::-1]):
jL = len(self.lstack) - 1 - i
A4 = numpy.ones(nal)
A3 = 2 * hz[jL] * numpy.ones(nal)
A2 = (Kix + hx)**2 + hz[jL]**2 + Kix**2 - 2 * kc[jL]**2
A1 = 2 * hz[jL] * (Kix**2 - kc[jL]**2)
A0 = (Kix**2 - kc[jL]**2) *\
((Kix + hx)**2 + hz[jL]**2 - kc[jL]**2) - pom[jL] * CC**2
X = solve_quartic(A4, A3, A2, A1, A0)
X = numpy.asarray(X).T
kz = numpy.zeros((nal, 4), dtype=complex)
kz[:, :2] = X[numpy.imag(X) <= 0].reshape(nal, 2)
kz[:, 2:] = X[numpy.imag(X) > 0].reshape(nal, 2)
P = numpy.zeros((nal, 4, 4), dtype=complex)
phi = numpy.zeros((nal, 4, 4), dtype=complex)
c = ((Kix**2)[:, numpy.newaxis] + kz**2 - kc[jL]**2) / k**2 /\
self.chimh['S'][jL] / CC[:, numpy.newaxis]
if jL > 0:
for j in range(4):
phi[:, j, j] = numpy.exp(1j * kz[:, j] * l.thickness)
else:
phi = numpy.tile(numpy.identity(4), (nal, 1, 1))
P[:, 0, :] = numpy.ones((nal, 4))
P[:, 1, :] = c
P[:, 2, :] = kz
P[:, 3, :] = c * (kz + hz[jL])
if i == 0:
R = numpy.copy(P)
else:
temp = numpy.linalg.inv(Ps)
try:
R = numpy.matmul(temp, P)
except AttributeError:
R = numpy.einsum('...ij,...jk', temp, P)
try:
M = numpy.matmul(numpy.matmul(M, R), phi)
except AttributeError:
M = numpy.einsum('...ij,...jk',
numpy.einsum('...ij,...jk', M, R), phi)
Ps = numpy.copy(P)
B = numpy.zeros((nal, 4, 4), dtype=complex)
B[..., :2] = M[..., :2]
B[:, 0, 2] = -numpy.ones(nal)
B[:, 1, 3] = -numpy.ones(nal)
B[:, 2, 2] = Kiz
B[:, 3, 3] = -Khz
C = numpy.zeros((nal, 4))
C[:, 0] = numpy.ones(nal)
C[:, 2] = Kiz
E = numpy.einsum('...ij,...j', numpy.linalg.inv(B), C)
Ir[pol] = numpy.abs(E[:, 2])**2 # reflected intensity
Ih[pol] = numpy.abs(E[:, 3])**2 * numpy.abs(Khz / Kiz) * mask
if len(self.get_polarizations()) > 1 and rettype == "intensity":
ret = numpy.sqrt(self.join_polarizations(Ih['S'], Ih['P']))
else:
ret = E[:, 3] * numpy.sqrt(numpy.abs(Khz / Kiz) * mask)
return self._create_return(alphai, ret, ai, ah, Ir, rettype=rettype)
[docs]
class SpecularReflectivityModel(LayerModel):
"""
model for specular reflectivity calculations
"""
[docs]
def __init__(self, *args, **kwargs):
"""
constructor for a reflectivity model. The arguments consist of a
LayerStack or individual Layer(s). Optional parameters are specified
in the keyword arguments.
Parameters
----------
args : LayerStack or Layers
either one LayerStack or several Layer objects can be given
kwargs: dict
optional parameters for the simulation; supported are:
I0 : float, optional
the primary beam intensity
background : float, optional
the background added to the simulation
sample_width : float, optional
width of the sample along the beam
beam_width : float, optional
beam width in the same units as the sample width
beam_shape : str, optional
beam_shape can be either 'hat' (default) or 'gaussian'. beam_width
will be accordingly interpreted as width of the hat function or
sigma of the Gaussian function.
offset : float, optional
angular offset of the incidence angle (deg)
resolution_width : float, optional
width of the resolution (deg)
energy : float or str
x-ray energy in eV
"""
if not hasattr(self, 'fit_paramnames'):
self.fit_paramnames = []
self.fit_paramnames += ['sample_width', 'beam_width', 'offset']
self.sample_width = kwargs.pop('sample_width', numpy.inf)
self.beam_width = kwargs.pop('beam_width', 0)
self.beam_shape = kwargs.pop('beam_shape', 'hat')
if self.beam_shape not in ['hat', 'gaussian']:
raise ValueError("invalid value for keyword argument beam_shape:"
"valid are 'hat' and 'gaussian'")
self.offset = kwargs.pop('offset', 0)
super().__init__(*args, **kwargs)
self.lstack_params += ['thickness', 'roughness', 'density']
self.xlabelstr = 'incidence angle (deg)'
# precalc optical properties
self._init_en = 0
self.init_cd()
[docs]
def init_cd(self):
"""
calculates the needed optical parameters for the simulation. If any of
the materials/layers is changing its properties this function needs to
be called again before another correct simulation is made. (Changes of
thickness and roughness do NOT require this!)
"""
if self._init_en != self.energy:
self.cd = numpy.asarray([-layer.material.chi0(en=self.energy)/2
for layer in self.lstack])
self._init_en = self.energy
[docs]
def simulate(self, alphai):
"""
performs the actual reflectivity calculation for the specified
incidence angles
Parameters
----------
alphai : array-like
vector of incidence angles
Returns
-------
array-like
vector of intensities of the reflectivity signal
"""
self.init_cd()
ns, np = (len(self.lstack), len(alphai))
lai = alphai - self.offset
# get layer properties
t = numpy.asarray([layer.thickness for layer in self.lstack])
sig = numpy.asarray([layer.roughness for layer in self.lstack])
rho = numpy.asarray([layer.density/layer.material.density
for layer in self.lstack])
sai = numpy.sin(numpy.radians(lai))
if self.beam_width > 0:
if self.beam_shape == 'hat':
shape = self.sample_width * sai / self.beam_width
shape[shape > 1] = 1
else:
shape = erf(self.sample_width * sai / 2 / pymath.sqrt(2) /
self.beam_width)
else:
shape = numpy.ones(np)
ETs = numpy.ones(np, dtype=complex)
ERs = numpy.zeros(np, dtype=complex)
ks = -self.exp.k0 * numpy.sqrt(sai**2 - 2 * self.cd[0] * rho[0])
for i in range(ns):
if i < ns-1:
k = -self.exp.k0 * numpy.sqrt(sai**2 - 2 *
self.cd[i+1] * rho[i+1])
phi = numpy.exp(1j * k * t[i+1])
else:
k = -self.exp.k0 * sai
phi = numpy.ones(np)
r = (k - ks) / (k + ks) * numpy.exp(-2 * sig[i]**2 * k * ks)
ET = phi * (ETs + r * ERs)
ER = (r * ETs + ERs) / phi
ETs = ET
ERs = ER
ks = k
R = shape * abs(ER / ET)**2
return self.scale_simulation(self.convolute_resolution(lai, R))
[docs]
def densityprofile(self, nz, plot=False, individual_layers=False):
"""
calculates the electron density of the layerstack from the thickness
and roughness of the individual layers
Parameters
----------
nz : int
number of values on which the profile should be calculated
plot : bool, optional
flag to tell if a plot of the profile should be created
individual_layers : bool, optional
return the density contributions of all layers as additional return
value.
Returns
-------
z : array-like
z-coordinates, z = 0 corresponds to the surface
eprof : array-like
electron profile
layereprof : 2D array, optional
electron profile of every sublayer with shape (nlayer, nz). This
is only returned when individual_layers=True
"""
if plot:
try:
from matplotlib import pyplot as plt
except ImportError:
plot = False
if config.VERBOSITY >= config.INFO_LOW:
print("XU.simpack: Warning: plot "
"functionality not available")
rel = constants.physical_constants['classical electron radius'][0]
rel *= 1e10
nl = len(self.lstack)
# get layer properties
t = numpy.asarray([layer.thickness for layer in self.lstack])
sig = numpy.asarray([layer.roughness for layer in self.lstack])
rho = numpy.asarray([layer.density/layer.material.density
for layer in self.lstack])
delta = numpy.real(self.cd)
totT = numpy.sum(t[1:])
zmin = -1.1 * totT - 10 * sig[0]
zmax = 5 * sig[-1]
z = numpy.linspace(zmin, zmax, nz)
pre_factor = 2 * numpy.pi / self.exp.wavelength**2 / rel * 1e24
# generate delta-rho values and interface positions
zz = numpy.zeros(nl)
dr = numpy.zeros(nl)
dr[-1] = delta[-1] * rho[-1] * pre_factor
for i in range(nl-1, 0, -1):
zz[i-1] = zz[i] - t[i]
dr[i-1] = delta[i-1] * rho[i-1] * pre_factor
# calculate profile from contribution of all layers
w = numpy.zeros((nl, nz))
for i in range(nl):
# top interface
if pymath.isclose(sig[i], 0):
sup = numpy.heaviside(zz[i] - z, 0.5)
else:
sup = (1 + erf((zz[i] - z) / sig[i] / numpy.sqrt(2))) / 2
# bottom interface
if i == 0:
zdownint = zz[0] - t[0]
sigint = 0
else:
zdownint = zz[i-1]
sigint = sig[i-1]
if pymath.isclose(sigint, 0):
sdown = numpy.heaviside(zdownint - z, 0.5)
else:
sdown = (1 + erf((zdownint - z) / sigint / numpy.sqrt(2))) / 2
w[i, :] = sup - sdown
layer_prof = dr[:, numpy.newaxis] * w[:, ...]
prof = numpy.sum(layer_prof, axis=0)
if plot:
plt.figure('XU:density_profile', figsize=(5, 3))
for i, layer in enumerate(self.lstack):
plt.plot(z, layer_prof[i, :], ':r', lw=1, label=layer.name)
plt.plot(z, prof, '-k', lw=2, label='electron density')
plt.xlabel(r'z ($\mathrm{\AA}$)')
plt.ylabel(r'electron density (e$^-$ cm$^{-3}$)')
plt.tight_layout()
if individual_layers:
return z, prof, layer_prof
return z, prof
[docs]
class DynamicalReflectivityModel(SpecularReflectivityModel):
"""
model for Dynamical Specular Reflectivity Simulations.
It uses the transfer Matrix methods as given in chapter 3
"Daillant, J., & Gibaud, A. (2008). X-ray and Neutron Reflectivity"
"""
[docs]
def __init__(self, *args, **kwargs):
"""
constructor for a reflectivity model. The arguments consist of a
LayerStack or individual Layer(s). Optional parameters are specified
in the keyword arguments.
Parameters
----------
args : LayerStack or Layers
either one LayerStack or several Layer objects can be given
kwargs: dict
optional parameters for the simulation; supported are:
I0 : float, optional
the primary beam intensity
background : float, optional
the background added to the simulation
sample_width : float, optional
width of the sample along the beam
beam_width : float, optional
beam width in the same units as the sample width
resolution_width : float, optional
width of the resolution (deg)
energy : float or str
x-ray energy in eV
polarization: ['P', 'S']
x-ray polarization
"""
kwargs.setdefault('polarization', 'P')
super().__init__(*args, **kwargs)
self._init_en_opt = 0
self._setOpticalConstants()
def _setOpticalConstants(self):
if self._init_en_opt != self.energy:
self.n_indices = numpy.asarray(
[layer.material.idx_refraction(en=self.energy)
for layer in self.lstack])
# append n = 1 for vacuum
self.n_indices = numpy.append(self.n_indices, 1)[::-1]
self._init_en_opt = self.energy
def _getTransferMatrices(self, alphai):
"""
Calculation of Refraction and Translation Matrices per angle per layer.
"""
# Set heights for each layer
heights = numpy.asarray([layer.thickness for layer in self.lstack[1:]])
heights = numpy.cumsum(heights)[::-1]
heights = numpy.insert(heights, 0, 0.) # first interface is at z=0
# set K-vector in each layer
kz_angles = -self.exp.k0 * numpy.sqrt(numpy.asarray(
[n**2 - numpy.cos(numpy.radians(alphai))**2
for n in self.n_indices]).T)
# set Roughness for each layer
roughness = numpy.asarray([layer.roughness
for layer in self.lstack[1:]])[::-1]
roughness = numpy.insert(roughness, 0, 0.) # first interface is at z=0
# Roughness is approximated by a Gaussian Statistics model modification
# of the transfer matrix elements using Groce-Nevot factors (GNF).
GNF_factor_P = numpy.asarray(
[[numpy.exp(-(kz_next - kz)**2 * (rough**2) / 2)
for (kz, kz_next, rough) in zip(kz_1angle, kz_1angle[1:],
roughness[1:])]
for kz_1angle in kz_angles])
GNF_factor_M = numpy.asarray(
[[numpy.exp(-(kz_next + kz) ** 2 * (rough ** 2) / 2)
for (kz, kz_next, rough) in zip(kz_1angle, kz_1angle[1:],
roughness[1:])]
for kz_1angle in kz_angles])
if self.polarization == 'S':
p_factor_angles = numpy.asarray(
[[(kz + kz_next) / (2 * kz)
for kz, kz_next in zip(kz_1angle, kz_1angle[1:])]
for kz_1angle in kz_angles])
m_factor_angles = numpy.asarray(
[[(kz - kz_next) / (2 * kz)
for kz, kz_next in zip(kz_1angle, kz_1angle[1:])]
for kz_1angle in kz_angles])
else:
p_factor_angles = numpy.asarray(
[[(n_next**2*kz + n**2*kz_next) / (2*n_next**2*kz)
for (kz, kz_next, n, n_next) in zip(kz_1angle, kz_1angle[1:],
self.n_indices,
self.n_indices[1:])]
for kz_1angle in kz_angles])
m_factor_angles = numpy.asarray(
[[(n_next**2*kz - n**2*kz_next) / (2*n_next**2*kz)
for (kz, kz_next, n, n_next) in zip(kz_1angle, kz_1angle[1:],
self.n_indices,
self.n_indices[1:])]
for kz_1angle in kz_angles])
# Translation Matrices dim = (angle, layer, 2, 2)
T_matrices = numpy.asarray(
[[([numpy.exp(-1.j*kz*height), 0], [0, numpy.exp(1.j*kz*height)])
for kz, height in zip(kz_1angle, heights)]
for kz_1angle in kz_angles])
R_matrices = numpy.asarray(
[[([p, m], [m, p]) for p, m in zip(P_fact, M_fact)]
for (P_fact, M_fact) in zip(p_factor_angles, m_factor_angles)])
for R_mat, GNF_P, GNF_M in zip(R_matrices, GNF_factor_P, GNF_factor_M):
R_mat[0, 0] = R_mat[0, 0] * GNF_P
R_mat[0, 1] = R_mat[0, 1] * GNF_M
R_mat[1, 0] = R_mat[1, 0] * GNF_M
R_mat[1, 1] = R_mat[1, 1] * GNF_P
return T_matrices, R_matrices
[docs]
def simulate(self, alphai):
"""
Simulates the Dynamical Reflectivity as a function of angle of
incidence
Parameters
----------
alphai : array-like
vector of incidence angles
Returns
-------
reflectivity: array-like
vector of intensities of the reflectivity signal
transmitivity: array-like
vector of intensities of the transmitted signal
"""
self._setOpticalConstants()
lai = alphai - self.offset
# Get Refraction and Translation Matrices for each angle of incidence
if lai[0] < 1.e-5:
lai[0] = 1.e-5 # cutoff
T_matrices, R_matrices = self._getTransferMatrices(lai)
# Calculate the Transfer Matrix
M_angles = numpy.zeros((lai.size, 2, 2), dtype=numpy.complex128)
for (angle, R), T in zip(enumerate(R_matrices), T_matrices):
pairwiseRT = [numpy.dot(t, r) for r, t in zip(R, T)]
M = numpy.identity(2, dtype=numpy.complex128)
for pair in pairwiseRT:
M = numpy.dot(M, pair)
M_angles[angle] = M
# Reflectance and Transmittance
R = numpy.array([numpy.abs((M[0, 1] / M[1, 1]))**2 for M in M_angles])
T = numpy.array([numpy.abs((1. / M[1, 1]))**2 for M in M_angles])
return R, T
[docs]
def scanEnergy(self, energies, angle):
# TODO: this is quite inefficient, too many calls to internal functions
# TODO: DO not return normalized refelctivity
"""
Simulates the Dynamical Reflectivity as a function of photon energy at
fixed angle.
Parameters
----------
energies: numpy.ndarray or list
photon energies (in eV).
angle : float
fixed incidence angle
Returns
-------
reflectivity: array-like
vector of intensities of the reflectivity signal
transmitivity: array-like
vector of intensities of the transmitted signal
"""
R_energies, T_energies = numpy.array([]), numpy.array([])
for energy in energies:
self.energy = energy
self._setOpticalConstants()
T_matrices, R_matrices = self._getTransferMatrices([angle, 0])
T_matrix = T_matrices[0]
R_matrix = R_matrices[0]
pairwiseRT = [numpy.dot(t, r) for r, t in zip(R_matrix, T_matrix)]
M = numpy.identity(2, dtype=numpy.complex128)
for pair in pairwiseRT:
M = numpy.dot(M, pair)
R = numpy.abs(M[0, 1] / M[1, 1]) ** 2
T = numpy.abs(1. / M[1, 1]) ** 2
R_energies = numpy.append(R_energies, R)
T_energies = numpy.append(T_energies, T)
return R_energies, T_energies
[docs]
class ResonantReflectivityModel(SpecularReflectivityModel):
"""
model for specular reflectivity calculations
CURRENTLY UNDER DEVELOPEMENT! DO NOT USE!
"""
[docs]
def __init__(self, *args, **kwargs):
"""
constructor for a reflectivity model. The arguments consist of a
LayerStack or individual Layer(s). Optional parameters are specified
in the keyword arguments.
Parameters
----------
args : LayerStack or Layers
either one LayerStack or several Layer objects can be given
kwargs: dict
optional parameters for the simulation; supported are:
I0 : float, optional
the primary beam intensity
background : float, optional
the background added to the simulation
sample_width : float, optional
width of the sample along the beam
beam_width : float, optional
beam width in the same units as the sample width
resolution_width : float, optional
width of the resolution (deg)
energy : float or str
x-ray energy in eV
polarization: ['P', 'S']
x-ray polarization
"""
super().__init__(*args, **kwargs)
[docs]
def simulate(self, alphai):
"""
performs the actual reflectivity calculation for the specified
incidence angles
Parameters
----------
alphai : array-like
vector of incidence angles
Returns
-------
array-like
vector of intensities of the reflectivity signal
"""
self.init_cd()
ns, np = (len(self.lstack), len(alphai))
lai = alphai - self.offset
# get layer properties
t = numpy.asarray([layer.thickness for layer in self.lstack])
sig = numpy.asarray([layer.roughness for layer in self.lstack])
rho = numpy.asarray([layer.density/layer.material.density
for layer in self.lstack])
cd = self.cd
qzvec = 4 * numpy.pi * numpy.sin(numpy.radians(lai)) /\
utilities.en2lam(self.energy)
qvec = numpy.array([[0., 0., qz] for qz in qzvec])
chihP = numpy.array(
[[layer.material.chih(q, en=self.energy,
polarization=self.polarization)
for q in qvec]
for layer in self.lstack])
if self.polarization in ['S', 'P']:
cd = cd + chihP
else:
cd = cd
sai = numpy.sin(numpy.radians(lai))
if self.beam_width > 0:
shape = self.sample_width * sai / self.beam_width
shape[shape > 1] = 1
else:
shape = numpy.ones(np)
ETs = numpy.ones(np, dtype=complex)
ERs = numpy.zeros(np, dtype=complex)
ks = -self.exp.k0 * numpy.sqrt(sai**2 - 2 * cd[0] * rho[0])
for i in range(ns):
if i < ns-1:
k = -self.exp.k0 * numpy.sqrt(sai**2 - 2 * cd[i+1] * rho[i+1])
phi = numpy.exp(1j * k * t[i+1])
else:
k = -self.exp.k0 * sai
phi = numpy.ones(np)
r = (k - ks) / (k + ks) * numpy.exp(-2 * sig[i]**2 * k * ks)
ET = phi * (ETs + r * ERs)
ER = (r * ETs + ERs) / phi
ETs = ET
ERs = ER
ks = k
R = shape * abs(ER / ET)**2
return self.scale_simulation(self.convolute_resolution(lai, R))
[docs]
class DiffuseReflectivityModel(SpecularReflectivityModel):
"""
model for diffuse reflectivity calculations
The 'simulate' method calculates the diffuse reflectivity on the specular
rod in coplanar geometry in analogy to the SpecularReflectivityModel.
The 'simulate_map' method calculates the diffuse reflectivity for a 2D set
of Q-positions. This method can also calculate the intensity for other
geometries, like GISAXS with constant incidence angle or a quasi
omega/2theta scan in GISAXS geometry.
"""
[docs]
def __init__(self, *args, **kwargs):
"""
constructor for a reflectivity model. The arguments consist of a
LayerStack or individual Layer(s). Optional parameters are specified
in the keyword arguments.
Parameters
----------
args : LayerStack or Layers
either one LayerStack or several Layer objects can be given
kwargs : dict
optional parameters for the simulation; supported are:
I0 : float, optional
the primary beam intensity
background : float, optional
the background added to the simulation
sample_width : float, optional
width of the sample along the beam
beam_width : float, optional
beam width in the same units as the sample width
resolution_width : float, optional
defines the width of the resolution (deg)
energy : float, optional
sets the experimental energy (eV)
H : float, optional
Hurst factor defining the fractal dimension of the roughness (0..1,
very slow for H != 1 or H != 0.5), default: 1
vert_correl : float, optional
vertical correlation length in angstrom, 0 means full replication
vert_nu : float, optional
exponent in the vertical correlation function
method : int, optional
1..simple DWBA (default), 2..full DWBA (slower)
vert_int : int, optional
0..no integration over the vertical divergence, 1..with integration
over the vertical divergence
qL_zero : float, optional
value of inplane q-coordinate which can be considered 0, using
method 2 it is important to avoid exact 0 and this value will be
used instead
"""
if not hasattr(self, 'fit_paramnames'):
self.fit_paramnames = []
self.fit_paramnames += ['H', 'vert_correl', 'vert_nu']
self.H = kwargs.pop('H', 1)
self.vert_correl = kwargs.pop('vert_correl', 0)
self.vert_nu = kwargs.pop('vert_nu', 0)
self.method = kwargs.pop('method', 1)
self.vert = kwargs.pop('vert_int', 0)
self.qL_zero = kwargs.pop('qL_zero', 5e-5)
super().__init__(*args, **kwargs)
self.lstack_params += ['lat_correl', ]
def _get_layer_prop(self):
"""
helper function to obtain layer properties needed for all types of
simulations
"""
nl = len(self.lstack)
self.init_cd()
t = numpy.asarray([float(layer.thickness)
for layer in self.lstack[nl:0:-1]])
sig = [float(layer.roughness) for layer in self.lstack[nl::-1]]
rho = [layer.density/layer.material.density
for layer in self.lstack[nl::-1]]
delta = self.cd * numpy.asarray(rho)
xiL = [float(layer.lat_correl) for layer in self.lstack[nl::-1]]
return t, sig, rho, delta, xiL
[docs]
def simulate(self, alphai):
"""
performs the actual diffuse reflectivity calculation for the specified
incidence angles. This method always uses the coplanar geometry
independent of the one set during the initialization.
Parameters
----------
alphai : array-like
vector of incidence angles
Returns
-------
array-like
vector of intensities of the reflectivity signal
"""
lai = alphai - self.offset
# get layer properties
t, sig, _, delta, xiL = self._get_layer_prop()
deltaA = numpy.sum(delta[:-1]*t)/numpy.sum(t)
lam = utilities.en2lam(self.energy)
if self.method == 2:
qL = [-abs(self.qL_zero), abs(self.qL_zero)]
else:
qL = [0, ]
qz = 4 * numpy.pi / lam * numpy.sin(numpy.radians(lai))
R = self._xrrdiffv2(lam, delta, t, sig, xiL, self.H, self.vert_correl,
self.vert_nu, None, qL, qz, self.sample_width,
self.beam_width, 1e-4, 1000, deltaA, self.method,
1, self.vert)
R = R.mean(axis=0)
return self.scale_simulation(self.convolute_resolution(lai, R))
[docs]
def simulate_map(self, qL, qz):
"""
performs diffuse reflectivity calculation for the rectangular grid of
reciprocal space positions define by qL and qz. This method uses the
method and geometry set during the initialization of the class.
Parameters
----------
qL : array-like
lateral coordinate in reciprocal space (vector with NqL components)
qz : array-like
vertical coordinate in reciprocal space (vector with Nqz
components)
Returns
-------
array-like
matrix of intensities of the reflectivity signal, with shape
(len(qL), len(qz))
"""
# get layer properties
t, sig, _, delta, xiL = self._get_layer_prop()
deltaA = numpy.sum(delta[:-1]*t)/numpy.sum(t)
lam = utilities.en2lam(self.energy)
localqL = numpy.copy(qL)
if self.method == 2:
localqL[qL == 0] = self.qL_zero
R = self._xrrdiffv2(lam, delta, t, sig, xiL, self.H, self.vert_correl,
self.vert_nu, None, localqL, qz, self.sample_width,
self.beam_width, 1e-4, 1000, deltaA, self.method,
1, self.vert)
return self.scale_simulation(R)
def _xrrdiffv2(self, lam, delta, thick, sigma, xiL, H, xiV, nu, alphai, qL,
qz, samplewidth, beamwidth, eps, nmax, deltaA, method, scan,
vert):
"""
simulation of diffuse reflectivity from a rough multilayer. Exact or
simplified DWBA, fractal roughness model, Ming model of the vertical
correlation, various scattering geometries
The used incidence and exit angles are stored in _smap_alphai,
_smap_alphaf
Parameters
----------
lam : float
x-ray wavelength in angstrom
delta : list or array-like
vector with the 1-n values (N+1 components, 1st component..layer at
the free surface, last component..substrate)
thick : list or array-like
vector with thicknesses (N components)
sigma : list or array-like
vector with rms roughnesses (N+1 components)
xiL : list or array-like
vector with lateral correlation lengths (N+1 components)
H : float
Hurst factor (scalar)
xiV : float
vertical correlation: 0..full replication, > 0 and nu > 0.. see
below, > 0 and nu = 0..vertical correlation length
nu : float
exponent in the vertical correlation function
exp(-abs(z_m-z_n)*(qL/max(qL))**nu/xiV)
alphai : float
incidence angle (scalar for scan=2, ignored for scan=1, 3)
qL : array-like
lateral coordinate in reciprocal space (vector with NqL components)
qz : array-like
vertical coordinate in reciprocal space (vector with Nqz
components)
samplewidth : float
width of the irradiated sample area (scalar), =0..the irradiated
are is assumed constant
beamwidth : float
width of the primary beam
eps : float
small number
nmax : int
max number of terms in the Taylor series of the lateral correlation
function
deltaA : complex
effective value of 1-n in simple DWBA (ignored for method=2)
method : int
1..simple DWBA, 2..full DWBA
scan : int
1..standard coplanar geometry, 2..standard GISAXS geometry with
constant incidence angle, =3..quasi omega/2theta scan in GISAXS
geometry (incidence and central-exit angles are equal)
vert : int
0..no integration over the vertical divergence, 1..with integration
over the vertical divergence
Returns
-------
diffint : array-like
diffuse reflectivity intensity matrix
"""
# worker function definitions
def coherent(alphai, K, delta, thick, N, NqL, Nqz):
"""
calculate coherent reflection/transmission signal of a multilayer
Parameters
----------
alphai : array-like
matrix of incidence angles in radians (NqL x Nqz components)
K : float
x-ray wave-vector (2*pi/lambda)
delta : array-like
vector with the 1-n values (N+1 components, 1st
component..layer at the free surface, last
component..substrate)
thick : array-like
vector with thicknesses (N components)
N : int
number layers in the stack
NqL : int
number of lateral q-points to calculate
Nqz : int
number of vertical q-points to calculate
Returns
-------
T, R, R0, k0, kz : array-like
transmission, reflection, surface reflection, z-component of
k-vector, z-component of k-vector in the material.
"""
k0 = -K * numpy.sin(alphai)
kz = numpy.zeros((N+1, NqL, Nqz), dtype=complex)
T = numpy.zeros((N+1, NqL, Nqz), dtype=complex)
R = numpy.zeros((N+1, NqL, Nqz), dtype=complex)
for jn in range(N+1):
kz[jn, ...] = -K * numpy.sqrt(numpy.sin(alphai)**2 -
2 * delta[jn])
T[N, ...] = numpy.ones((NqL, Nqz), dtype=complex)
kzs = kz[N, ...] # kz in substrate
for jn in range(N-1, -1, -1):
kzn = kz[jn, ...]
tF = 2 * kzn / (kzn + kzs)
rF = (kzn - kzs) / (kzn + kzs)
phi = numpy.exp(1j * kzn * thick[jn])
T[jn, ...] = phi / tF * (T[jn+1, ...] + rF * R[jn+1, ...])
R[jn, ...] = 1 / phi / tF * (rF * T[jn+1, ...] + R[jn+1, ...])
kzs = numpy.copy(kzn)
tF = 2 * k0 / (k0 + kzn)
rF = (k0 - kzn) / (k0 + kzn)
T0 = 1 / tF * (T[0, ...] + rF * R[0, ...])
R0 = 1 / tF * (rF * T[0, ...] + R[0, ...])
T /= T0
R /= T0
R0 /= T0
return T, R, R0, k0, kz
def correl(a, b, L, H, eps, nmax, vert, K, NqL, Nqz, isurf):
"""
correlation function
Parameters
----------
a : array-like
lateral correlation parameter
b : array-like
vertical correlation parameter
L : float
lateral correlation length
H : float
Hurst factor (scalar)
eps : float
small number (decides integration cut-off), typical 1e-3
nmax : int
max number of terms in the Taylor series of the lateral
correlation function
vert : int
flag to tell decide if integration over vertical divergence is
used: 0..no integration, 1..with integration
K : float
length of the x-ray wave-vector (2*pi/lambda)
NqL : int
number of lateral q-points to calculate
Nqz : int
number of vertical q-points to calculate
isurf : array-like
array with NqL, Nqz flags to tell if there is a positive
incidence and exit angle
Returns
-------
psi : array-like
correlation function
"""
psi = numpy.zeros((NqL, Nqz), dtype=complex)
if H in (0.5, 1):
dpsi = numpy.zeros_like(psi, dtype=complex)
m = isurf > 0
n = 1
s = numpy.copy(b)
errm = numpy.inf
if H == 1 and vert == 0:
def f(a, n):
return numpy.exp(-a**2/4/n) / 2 / n**2
elif H == 0.5 and vert == 0:
def f(a, n):
return 1. / (n**2 + a**2)**(3/2.)
elif H == 1 and vert == 1:
def f(a, n):
return numpy.sqrt(numpy.pi/n**3) * numpy.exp(-a**2/4/n)
elif H == 0.5 and vert == 1:
def f(a, n):
return 2. / (n**2 + a**2)
while errm > eps and n < nmax:
dpsi[m] = s[m] * f(a[m], n)
if n > 1:
errm = abs(numpy.max(dpsi[m] / psi[m]))
psi[m] += dpsi[m]
s[m] *= b[m]/float(n)
n += 1
else:
if vert == 0:
kern = kernel
else:
kern = kernelvert
for jL in range(NqL):
for jz in range(Nqz):
if isurf[jL, jz] == 1:
xmax = (-numpy.log(eps / b[jL, jz]))**(1/(2*H))
psi[jL, jz] = cquad(kern, 0.0, numpy.real(xmax),
epsrel=eps, epsabs=0,
limit=nmax, args=(a[jL, jz],
b[jL, jz], H))
if vert == 0:
psi *= 2 * numpy.pi * L ** 2
else:
psi *= 2 * numpy.pi * L / K
return psi
def kernelvert(x, a, b, H):
"""
integration kernel with vertical integration
Parameters
----------
x : float or array-like
independent parameter of the function
a : float
lateral correlation parameter
b : complex
vertical correlation parameter
H : float
Hurst factor (scalar)
Returns
-------
float or arraylike
"""
w = numpy.exp(b * numpy.exp(-x**(2*H))) - 1
F = 2 * numpy.cos(a*x) * w
return F
def kernel(x, a, b, H):
"""
integration kernel without vertical integration
Parameters
----------
x : float or array-like
independent parameter of the function
a : float
lateral correlation parameter
b : complex
vertical correlation parameter
H : float
Hurst factor (scalar)
Returns
-------
float or arraylike
"""
w = numpy.exp(b * numpy.exp(-x**(2*H))) - 1
F = x * j0(a*x) * w
return F
def cquad(func, a, b, **kwargs):
"""
complex quadrature by spliting real and imaginary part using scipy
"""
def real_func(*args):
return numpy.real(func(*args))
def imag_func(*args):
return numpy.imag(func(*args))
real_integral = integrate.quad(real_func, a, b, **kwargs)
imag_integral = integrate.quad(imag_func, a, b, **kwargs)
return real_integral[0] + 1j*imag_integral[0]
# begin of _xrrdiffv2
K = 2 * numpy.pi / lam
N = len(thick)
NqL = len(qL)
Nqz = len(qz)
QZ, QL = numpy.meshgrid(qz, qL)
# scan types:
if scan == 1: # coplanar geometry
Q = numpy.sqrt(QL**2 + QZ**2)
QP = numpy.abs(QL)
th = numpy.arcsin(Q / 2 / K)
om = numpy.arctan2(QL, QZ)
ALPHAI = th + om
ALPHAF = th - om
elif scan == 2: # GISAXS geometry with constant incidence angle
ALPHAI = numpy.radians(alphai) * numpy.ones((NqL, Nqz))
ALPHAF = numpy.arcsin(QZ / K - numpy.sin(numpy.radians(alphai)))
PHI = numpy.arcsin(QL / K / numpy.cos(ALPHAF))
QP = K * numpy.sqrt(numpy.cos(ALPHAF)**2 + numpy.cos(ALPHAI)**2 -
2*numpy.cos(ALPHAF) * numpy.cos(ALPHAI) *
numpy.cos(PHI))
elif scan == 3: # with quasi omega/2theta scan in GISAXS geometry
ALPHAI = numpy.arcsin(QZ * (K - numpy.sqrt(K**2 - QL**2)) / QL**2)
ALPHAF = numpy.arcsin(QZ / K - numpy.sin(ALPHAI))
PHI = numpy.arcsin(QL / K / numpy.cos(ALPHAF))
QP = K * numpy.sqrt(numpy.cos(ALPHAF)**2 + numpy.cos(ALPHAI)**2 -
2*numpy.cos(ALPHAF) * numpy.cos(ALPHAI) *
numpy.cos(PHI))
else:
raise ValueError("Invalid value of parameter 'scan'")
# removing the values under the horizon
isurf = heaviside(ALPHAI) * heaviside(ALPHAF)
# non-disturbed states:
if method == 1:
k01 = -K * numpy.sin(ALPHAI)
kz1 = -K * numpy.sqrt(numpy.sin(ALPHAI)**2 - 2*deltaA)
k02 = -K * numpy.sin(ALPHAF)
kz2 = -K * numpy.sqrt(numpy.sin(ALPHAF)**2 - 2*deltaA)
T1 = 2 * k01 / (k01 + kz1)
T2 = 2 * k02 / (k02 + kz2)
R01 = (k01 - kz1) / (k01 + kz1)
R02 = (k02 - kz2) / (k02+kz2)
R1 = numpy.zeros((NqL, Nqz), dtype=complex)
R2 = numpy.copy(R1)
nproc = 1
else: # method == 2
T1, R1, R01, k01, kz1 = coherent(ALPHAI, K, delta, thick, N,
NqL, Nqz)
T2, R2, R02, k02, kz2 = coherent(ALPHAF, K, delta, thick, N,
NqL, Nqz)
nproc = 4
# sample surface
if beamwidth > 0:
S = samplewidth * numpy.sin(ALPHAI) / beamwidth
S[S > 1] = 1
else:
S = 1
# z-coordinates
z = numpy.zeros(N+1)
for jn in range(1, N+1):
z[jn] = z[jn-1] - thick[jn-1]
# calculation of the deltas
delt = numpy.zeros(N+1, dtype=complex)
for jn in range(N+1):
if jn == 0:
delt[jn] = delta[jn]
if jn > 0:
delt[jn] = delta[jn] - delta[jn-1]
# double sum over interfaces
result = numpy.zeros((NqL, Nqz))
for jn in range(N+1):
# if method == 1 and (H == 1 or H == 0.5):
# print(jn)
if nu != 0 or xiV == 0:
jmdol = 1
else:
jmdol = numpy.argmin(numpy.abs(z - (z[jn] -
xiV * numpy.log(eps))))
for ja in range(nproc):
if method == 1:
Qn = -kz1 - kz2
An = T1 * T2 * numpy.exp(-1j*Qn*z[jn])
else: # method == 2
if ja == 0:
An = T1[jn, ...] * T2[jn, ...]
Qn = -kz1[jn, ...] - kz2[jn, ...]
elif ja == 1:
An = T1[jn, ...] * R2[jn, ...]
Qn = -kz1[jn, ...] + kz2[jn, ...]
elif ja == 2:
An = R1[jn, ...] * T2[jn, ...]
Qn = kz1[jn, ...] - kz2[jn, ...]
elif ja == 3:
An = R1[jn, ...] * R2[jn, ...]
Qn = kz1[jn, ...] + kz2[jn, ...]
else:
raise ValueError("ja must be in range(4)")
for jm in range(jmdol, jn+1):
if jm == jn:
weight = 1
else:
weight = 2
# if method == 1 and (H != 0.5 and H != 1) and ja==1:
# print(jn, jm)
# vertical correlation function:
if xiV > 0:
CV = numpy.exp(-abs(z[jn] - z[jm]) *
(QP/numpy.max(QP))**nu / xiV)
else:
CV = 1
# effective values of sigma and lateral correl. length:
try:
LP = ((float(xiL[jn])**(-2*H) +
float(xiL[jm])**(-2*H)) / 2) ** (-1 / 2 / H)
except ZeroDivisionError:
LP = 0
sig = pymath.sqrt(sigma[jn] * sigma[jm])
for jb in range(nproc):
if method == 1:
Qm = -kz1 - kz2
Am = T1 * T2 * numpy.exp(-1j * Qm * z[jm])
else: # method == 2
# if H != 0.5 or H != 1:
# print(ja, jb, jn, jm)
if jb == 0:
Am = T1[jm, ...] * T2[jm, ...]
Qm = -kz1[jm, ...] - kz2[jm, ...]
elif jb == 1:
Am = T1[jm, ...] * R2[jm, ...]
Qm = -kz1[jm, ...] + kz2[jm, ...]
elif jb == 2:
Am = R1[jm, ...] * T2[jm, ...]
Qm = kz1[jm, ...] - kz2[jm, ...]
elif jb == 3:
Am = R1[jm, ...] * R2[jm, ...]
Qm = +kz1[jm, ...] + kz2[jm, ...]
else:
raise ValueError("ja must be in range(4)")
# lateral correlation function:
Psi = correl(QP*LP, Qn*numpy.conj(Qm)*sig**2, LP, H,
eps, nmax, vert, K, NqL, Nqz, isurf)
result += numpy.real(CV * delt[jn] *
numpy.exp(-Qn**2 *
sigma[jn]**2/2) /
Qn * An *
numpy.conj(delt[jm] *
numpy.exp(-Qm**2*sigma[jm]**2/2) /
Qm * Am) * Psi) * weight
result[isurf == 0] = 0
self._smap_R01 = R01 * isurf
self._smap_R02 = R02 * isurf
self._smap_alphai = numpy.degrees(ALPHAI*isurf)
self._smap_alphaf = numpy.degrees(ALPHAF*isurf)
return result * S * K**4 / (16*numpy.pi**2)
[docs]
def effectiveDensitySlicing(layerstack, step, roughness=0, cutoff=1e-5):
"""
Function to slice a LayerStack into many amorphous sublayers for effective
density modelling of X-ray reflectivity of thin and rough multilayers. The
resulting LayerStack will consist of perfectly smooth layers with average
density/composition resulting from an error-function like transition
between the rough layers of the initial stack. At the surface an vacuum
layer is automatically added to the initial stack.
Parameters
----------
layerstack : initial LayerStack, can contain only Amorhous layers!
step : thickness (in angstrom) of the slices in the returned
LayerStack
roughness : roughness of the created sublayers (in angstrom)
cutoff : layers with relative weights below this value will be ignored
Returns
-------
LayerStack
"""
thickness = numpy.asarray([lay.thickness for lay in layerstack])
sigmas = numpy.asarray([lay.roughness for lay in layerstack])
if thickness[0] == pymath.inf:
thickness_all_layers = numpy.sum(thickness[1:])
else:
thickness_all_layers = numpy.sum(thickness)
margin = max(numpy.sum([lay.roughness for lay in layerstack]),
5 * layerstack[0].roughness,
5 * layerstack[-1].roughness)
pos_inter = numpy.zeros(len(layerstack))
for i in range(len(layerstack)-1, 0, -1):
pos_inter[i-1] = pos_inter[i] - thickness[i]
# calculate the weight for every sublayer + vacuum above
z = numpy.arange(-(thickness_all_layers + margin), margin, step)
W = {}
zeta = {}
for i in range(len(layerstack)+1):
W[i] = numpy.zeros_like(z)
if i < (len(layerstack) - 1):
zeta[i] = (sigmas[i+1] * pos_inter[i] + sigmas[i] *
pos_inter[i+1]) / (sigmas[i] + sigmas[i+1])
# first and last Weight-functions will be treated seperately
W[0][:] = 1 / 2 * (1 - erf(
(z - pos_inter[0]) / (pymath.sqrt(2) * sigmas[0])))
W[len(layerstack)][:] = 1 / 2 * (1 + erf(
(z - pos_inter[len(layerstack) - 1]) / (pymath.sqrt(2) *
sigmas[len(layerstack) - 1])))
# Weight-functions in between
for idxl in range(1, len(layerstack)):
maskz = (z <= zeta[idxl - 1])
W[idxl][maskz] = 1 / 2 * \
(1 + erf((z[maskz] - pos_inter[idxl - 1]) /
(pymath.sqrt(2) * sigmas[idxl - 1])))
imaskz = numpy.logical_not(maskz)
W[idxl][imaskz] = 1 / 2 * \
(1 - erf((z[imaskz] - pos_inter[idxl]) /
(pymath.sqrt(2) * sigmas[idxl])))
# normalize weight functions
wsum = numpy.add.reduce(list(W.values()))
for k in W:
W[k] /= wsum
# generate new sliced layerstack
sls = LayerStack(f'sliced LayerStack (step={step:.2f}Ang)')
if thickness[0] == pymath.inf:
sls.append(Layer(layerstack[0].material, pymath.inf,
roughness=roughness))
for idxp in range(len(z)):
# create compound material for all contributing layers
atoms = []
elements = []
density = 0
for idxl, l in enumerate(layerstack):
if W[idxl][idxp] > cutoff:
for at, occ in l.material.base:
if at in elements:
i = elements.index(at)
atoms[i] = (at, atoms[i][1] + occ * W[idxl][idxp])
else:
atoms.append((at, occ * W[idxl][idxp]))
elements.append(at)
density += W[idxl][idxp] * l.density
if density != 0:
mat = Amorphous(f"slice {idxp}", density, atoms=atoms)
sls.append(Layer(mat, step, roughness=roughness))
return sls