# 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-2023 Dominik Kriegner <dominik.kriegner@gmail.com>
import collections.abc
import copy
import numbers
import numpy
from .. import utilities
from ..materials import Crystal, PseudomorphicMaterial
from ..math import CoordinateTransform, Transform
def _multiply(a, b):
"""
implement multiplication of SMaterial and MaterialList with integer
"""
if not isinstance(b, int):
raise TypeError(
"unsupported operand type(s) for *: "
"'%s' and '%s'" % (type(a), type(b))
)
if b < 1:
raise ValueError("multiplication factor needs to be positive!")
m = MaterialList("%d * (%s)" % (b, a.name), a)
for _ in range(b - 1):
m.append(copy.deepcopy(a))
return m
[docs]
class SMaterial:
"""
Simulation Material. Extends the xrayutilities Materials by properties
needed for simulations
"""
[docs]
def __init__(self, material, name=None, **kwargs):
"""
initialize a simulation material by specifiying its Material and
optional other properties
Parameters
----------
material : Material (Crystal, or Amorphous)
Material object containing optical/crystal properties of for the
simulation; a deepcopy is used internally.
name : str, optional
name of the material used in the simulations
kwargs : dict
optional properties of the material needed for the simulation
"""
if name is not None:
self.name = utilities.makeNaturalName(name, check=True)
else:
self.name = utilities.makeNaturalName(material.name, check=True)
self.material = copy.deepcopy(material)
for kw in kwargs:
setattr(self, kw, kwargs[kw])
@property
def material(self):
return self._material
@material.setter
def material(self, material):
self._material = material
if isinstance(material, Crystal):
self._structural_params = []
# make lattice parameters attributes
for param, value in material.lattice.free_parameters.items():
self._structural_params.append(param)
setattr(self, param, value)
# make attributes from atom positions
for i, wp in enumerate(material.lattice._wbase):
if wp[1][1] is not None:
for j, p in enumerate(wp[1][1]):
name = "_".join(
("at%d" % i, wp[0].name, wp[1][0], str(j), "pos")
)
self._structural_params.append(name)
setattr(self, name, p)
# make attributes from atom occupations
for i, wp in enumerate(material.lattice._wbase):
name = "_".join(
("at%d" % i, wp[0].name, wp[1][0], "occupation")
)
self._structural_params.append(name)
setattr(self, name, wp[2])
# make attributes from Debye waller exponents
for i, wp in enumerate(material.lattice._wbase):
name = "_".join(("at%d" % i, wp[0].name, wp[1][0], "biso"))
self._structural_params.append(name)
setattr(self, name, wp[3])
def __setattr__(self, name, value):
object.__setattr__(self, name, value)
if hasattr(self, "material"):
if isinstance(self.material, Crystal):
if name in self.material.lattice.free_parameters:
setattr(self.material.lattice, name, value)
if name.startswith("at"):
nsplit = name.split("_")
idx = int(nsplit[0][2:])
wp = self.material.lattice._wbase[idx]
# wyckoff position parameter
if nsplit[-1] == "pos":
pidx = int(nsplit[-2])
wyckpos = (wp[1][0], list(wp[1][1]))
wyckpos[1][pidx] = value
self.material.lattice._wbase[idx] = (
wp[0],
wyckpos,
wp[2],
wp[3],
)
# site occupation
if nsplit[-1] == "occupation":
self.material.lattice._wbase[idx] = (
wp[0],
wp[1],
value,
wp[3],
)
# site DW exponent
if nsplit[-1] == "biso":
self.material.lattice._wbase[idx] = (
wp[0],
wp[1],
wp[2],
value,
)
def __radd__(self, other):
return MaterialList(f"{other.name} + {self.name}", other, self)
def __add__(self, other):
return MaterialList(f"{self.name} + {other.name}", self, other)
def __mul__(self, other):
return _multiply(self, other)
__rmul__ = __mul__
def __repr__(self):
s = f"{self.__class__.__name__}-{self.name} ("
for k in self.__dict__:
if k not in ("name", "_material", "_structural_params"):
v = getattr(self, k)
if isinstance(v, numbers.Number):
s += f"{k}: {v:.5g}, "
else:
s += f"{k}: {v}, "
return s + ")"
[docs]
class MaterialList(collections.abc.MutableSequence):
"""
class representing the basics of a list of materials for simulations within
xrayutilities. It extends the built in list type.
"""
[docs]
def __init__(self, name, *args):
if not isinstance(name, str):
raise TypeError("'name' argument must be a string")
self.name = name
self.list = list()
self.namelist = list()
self.extend(list(args))
[docs]
def check(self, v):
if not isinstance(v, SMaterial):
raise TypeError(
"%s can only contain SMaterial as entries!"
% self.__class__.__name__
)
def _set_unique_name(self, v):
if v.name in self.namelist:
splitname = v.name.split("_")
if len(splitname) > 1:
try:
num = int(splitname[-1])
basename = "_".join(splitname[:-1])
except ValueError:
num = 1
basename = v.name
else:
num = 1
basename = v.name
name = f"{basename}_{num:d}"
while name in self.namelist:
num += 1
name = f"{basename}_{num:d}"
v.name = name
return v.name
def __len__(self):
return len(self.list)
def __getitem__(self, i):
return self.list[i]
def __delitem__(self, i):
del self.list[i]
def __setitem__(self, i, v):
self.check(v)
self.namelist[i] = self._set_unique_name(v)
self.list[i] = v
[docs]
def insert(self, i, v):
if isinstance(v, MaterialList):
vs = v
else:
vs = [
v,
]
for j, val in enumerate(vs):
self.check(val)
self.namelist.insert(i + j, self._set_unique_name(val))
self.list.insert(i + j, val)
def __radd__(self, other):
ml = MaterialList(f"{other.name} + {self.name}")
ml.append(other)
ml.append(self)
return ml
def __add__(self, other):
ml = MaterialList(f"{self.name} + {other.name}")
ml.append(self)
ml.append(other)
return ml
def __mul__(self, other):
return _multiply(self, other)
__rmul__ = __mul__
def __str__(self):
layer = ",\n ".join([str(entry) for entry in self.list])
s = f"{self.name} [\n {layer}\n]"
return s
def __repr__(self):
return self.name
[docs]
class Layer(SMaterial):
"""
Object describing part of a thin film sample. The properties of a layer
are :
Attributes
----------
material : Material (Crystal or Amorhous)
an xrayutilties material describing optical and crystal properties of
the thin film
thickness : float
film thickness in angstrom
"""
_valid_init_kwargs = {
"name": "Custom name of the Layer",
"roughness": "root mean square roughness",
"density": "density in kg/m^3",
"relaxation": "degree of relaxation",
"lat_correl": "lateral correlation length",
}
[docs]
def __init__(self, material, thickness, **kwargs):
"""
constructor for the material saving its properties
Parameters
----------
material : Material (Crystal or Amorhous)
an xrayutilties material describing optical and crystal properties
of the thin film
thickness : float
film thickness in angstrom
kwargs : dict
optional keyword arguments with further layer properties.
roughness : float, optional
root mean square roughness of the top interface in angstrom
density : float, optional
density of the material in kg/m^3; If not specified the density of
the material will be used.
relaxation : float, optional
the degree of relaxation in case of crystalline thin films
lat_correl : float, optional
the lateral correlation length for diffuse reflectivity
calculations
"""
utilities.check_kwargs(
kwargs, self._valid_init_kwargs, self.__class__.__name__
)
kwargs["thickness"] = thickness
super().__init__(material, **kwargs)
def __getattr__(self, name):
"""
return default values for properties if they were not set
"""
if name == "density":
return self.material.density
if name == "roughness":
return 0
if name == "lat_correl":
return numpy.inf
if name == "relaxation":
return 1
return super().__getattribute__(name)
[docs]
class LayerStack(MaterialList):
"""
extends the built in list type to enable building a stack of Layer by
various methods.
"""
[docs]
def check(self, v):
if not isinstance(v, Layer):
raise TypeError("LayerStack can only contain Layer as entries!")
[docs]
class CrystalStack(LayerStack):
"""
extends the built in list type to enable building a stack of crystalline
Layers by various methods.
"""
[docs]
def check(self, v):
super().check(v)
if not isinstance(v.material, Crystal):
raise TypeError(
"CrystalStack can only contain crystalline Layers as entries!"
)
[docs]
class GradedLayerStack(CrystalStack):
"""
generates a sequence of layers with a gradient in chemical composition
"""
[docs]
def __init__(self, alloy, xfrom, xto, nsteps, thickness, **kwargs):
"""
constructor for a graded buffer of the material 'alloy' with chemical
composition from 'xfrom' to 'xto' with 'nsteps' number of sublayers.
The total thickness of the graded buffer is 'thickness'
Parameters
----------
alloy : function
Alloy function which allows to create a material with chemical
composition 'x' by alloy(x)
xfrom, xto : float
chemical composition from the bottom to top
nsteps : int
number of sublayers in the graded buffer
thickness : float
total thickness of the graded stack
"""
nfrom = alloy(xfrom).name
nto = alloy(xto).name
super().__init__("(" + nfrom + "-" + nto + ")")
for x in numpy.linspace(xfrom, xto, nsteps):
layer = Layer(alloy(x), thickness / nsteps, **kwargs)
self.append(layer)
[docs]
class PseudomorphicStack001(CrystalStack):
"""
generate a sequence of pseudomorphic crystalline Layers. Surface
orientation is assumed to be 001 and materials must be cubic/tetragonal.
"""
trans = Transform(numpy.identity(3))
[docs]
def make_epitaxial(self, i):
"""Make the i-th sublayer pseudomorphic to the layer below."""
layer = self.list[i]
if i == 0:
return
psub = self.list[i - 1].material
mpseudo = PseudomorphicMaterial(
psub, layer.material, layer.relaxation, trans=self.trans
)
self.list[i].material = mpseudo
def __delitem__(self, i):
del self.list[i]
for j in range(i, len(self)):
self.make_epitaxial(j)
def __setitem__(self, i, v):
self.check(v)
self.namelist[i] = self._set_unique_name(v)
self.list[i] = v
for j in range(i, len(self)):
self.make_epitaxial(j)
[docs]
def insert(self, i, v):
if isinstance(v, MaterialList):
vs = v
else:
vs = [
v,
]
for j, val in enumerate(vs):
self.check(val)
self.namelist.insert(i + j, self._set_unique_name(val))
self.list.insert(i + j, copy.copy(val))
for k in range(i + j, len(self)):
self.make_epitaxial(k)
[docs]
class PseudomorphicStack111(PseudomorphicStack001):
"""
generate a sequence of pseudomorphic crystalline Layers. Surface
orientation is assumed to be 111 and materials must be cubic.
"""
trans = CoordinateTransform((1, -1, 0), (1, 1, -2), (1, 1, 1))
[docs]
class Powder(SMaterial):
"""
Object describing part of a powder sample. The properties of a powder
are:
Attributes
----------
material : Crystal
an xrayutilties material (Crystal) describing optical and crystal
properties of the powder
volume : float
powder's volume (in pseudo units, since only the relative volume enters
the calculation)
crystallite_size_lor : float, optional
Lorentzian crystallite size fwhm (m)
crystallite_size_gauss : float, optional
Gaussian crystallite size fwhm (m)
strain_lor : float, optional
extra peak width proportional to tan(theta)
strain_gauss : float, optional
extra peak width proportional to tan(theta)
preferred_orientation : tuple, optional
HKL of the preferred orientation
preferred_orientation_factor : float, optional
March-Dollase preferred orientation factor: < 1 for platy crystallits ,
> 1 for rod-like crystallites, and = 1 for random orientation of
crystallites.
"""
_valid_init_kwargs = {
"name": "Custom name of the Powder",
"crystallite_size_lor": "Lorentzian crystallite size",
"crystallite_size_gauss": "Gaussian crystallite size",
"strain_lor": "microstrain broadening",
"strain_gauss": "microstrain broadening",
"preferred_orientation": "HKL of the preferred orientation",
"preferred_orientation_factor": "March-Dollase preferred orientation factor",
}
[docs]
def __init__(self, material, volume, **kwargs):
"""
constructor for the material saving its properties
Parameters
----------
material : Crystal
an xrayutilties material (Crystal) describing optical and crystal
properties of the powder
volume : float
powder's volume (in pseudo units, since only the relative volume
enters the calculation)
kwargs : dict
optional keyword arguments with further powder properties.
crystallite_size_lor : float, optional
Lorentzian crystallite size fwhm (m)
crystallite_size_gauss : float, optional
Gaussian crystallite size fwhm (m)
strain_lor, strain_gauss : float, optional
extra peak width proportional to tan(theta);
typically interpreted as microstrain broadening
"""
utilities.check_kwargs(
kwargs, self._valid_init_kwargs, self.__class__.__name__
)
kwargs["volume"] = volume
super().__init__(material, **kwargs)
[docs]
class PowderList(MaterialList):
"""
extends the built in list type to enable building a list of Powder
by various methods.
"""
[docs]
def check(self, v):
if not isinstance(v, Powder):
raise TypeError("PowderList can only contain Powder as entries!")