Source code for udkm1Dsim.structures.atoms
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# The MIT License (MIT)
# Copyright (c) 2020 Daniel Schick
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
# OR OTHER DEALINGS IN THE SOFTWARE.
__all__ = ['Atom', 'AtomMixed']
__docformat__ = 'restructuredtext'
from .. import u, Q_
import os
import numpy as np
import scipy.constants as constants
import warnings
from tabulate import tabulate
[docs]
class Atom:
"""Atom
Smallest structural unit of which larger structures can be build.
It holds real physical properties of on the atomic level.
Args:
symbol (str): symbol of the atom.
Keyword Args:
id (str): id of the atom, may differ from symbol and/or name.
ionicity (int): ionicity of the atom.
atomic_form_factor_path (str): path to atomic form factor coeffs.
atomic_form_factor_source (str): either _henke_ or default _chantler_
magnetic_form_factor_path (str): path to magnetic form factor coeffs.
Attributes:
symbol (str): symbol of the element.
id (str): id of the atom, may differ from symbol and/or name.
name (str): name of the element (generic).
atomic_number_z (int): Z atomic number.
mass_number_a (float): A atomic mass number.
ionicity (int): ionicity of the atom.
mass (float): mass of the atom [kg].
atomic_form_factor_coeff (ndarray[float]): atomic form factor.
coefficients for energy-dependent atomic form factor.
cromer_mann_coeff (ndarray[float]): cromer-mann coefficients for
angular-dependent atomic form factor.
magnetic_form_factor_coeff (ndarray[float]): magnetic form factor
coefficients for energy-dependent magnetic form factor.
mag_amplitude (float): magnetization amplitude -1 .. 1.
mag_phi (float): phi angle of magnetization [rad].
mag_gamma (float): gamma angle of magnetization [rad].
References:
.. [1] B. L. Henke, E. M. Gullikson & J. C. Davis,
*X-Ray Interactions: Photoabsorption, Scattering,
Transmission, and Reflection at E = 50-30,000 eV, Z = 1-92*,
`Atomic Data and Nuclear Data Tables, 54(2), 181–342, (1993).
<http://www.doi.org/10.1006/adnd.1993.1013>`_
.. [2] C.T. Chantler, K. Olsen, R.A. Dragoset, J. Chang, A.R. Kishore,
S.A. Kotochigova, & D.S. Zucker,
*Detailed Tabulation of Atomic Form Factors, Photoelectric
Absorption and Scattering Cross Section, and Mass Attenuation
Coefficients for Z = 1-92 from E = 1-10 eV to E = 0.4-1.0 MeV*,
`NIST Standard Reference Database 66.
<https://dx.doi.org/10.18434/T4HS32>`_
.. [3] J. Als-Nielson, & D. McMorrow,
`Elements of Modern X-Ray Physics. New York: John Wiley &
Sons, Ltd. (2001) <http://www.doi.org/10.1002/9781119998365>`_
.. [4] D. T. Cromer & J. B. Mann, *X-ray scattering
factors computed from numerical Hartree–Fock wave functions*,
`Acta Crystallographica Section A, 24(2), 321–324 (1968).
<http://www.doi.org/10.1107/S0567739468000550>`_
"""
def __init__(self, symbol, **kwargs):
self.symbol = symbol
self.id = kwargs.get('id', symbol)
self.ionicity = kwargs.get('ionicity', 0)
self.mag_amplitude = kwargs.get('mag_amplitude', 0)
self.mag_phi = kwargs.get('mag_phi', 0*u.deg)
self.mag_gamma = kwargs.get('mag_gamma', 0*u.deg)
try:
filename = os.path.join(os.path.dirname(__file__),
'../parameters/elements.dat')
symbols = np.genfromtxt(filename, dtype='U2', usecols=(0))
elements = np.genfromtxt(filename, dtype='U15, i8, f8', usecols=(1, 2, 3))
[rowidx] = np.where(symbols == self.symbol)
element = elements[rowidx[0]]
except Exception as e:
print('Cannot load element specific data from elements data file!')
print(e)
self.name = element[0]
self.atomic_number_z = element[1]
self.mass_number_a = element[2]
self._mass = self.mass_number_a*constants.atomic_mass
self.mass = self._mass*u.kg
self.atomic_form_factor_coeff = self.read_atomic_form_factor_coeff(
filename=kwargs.get('atomic_form_factor_path', ''),
source=kwargs.get('atomic_form_factor_source', 'chantler'))
self.magnetic_form_factor_coeff = self.read_magnetic_form_factor_coeff(
filename=kwargs.get('magnetic_form_factor_path', ''))
self.cromer_mann_coeff = self.read_cromer_mann_coeff()
def __str__(self):
"""String representation of this class"""
output = {'parameter': ['id', 'symbol', 'name', 'atomic number Z', 'mass number A', 'mass',
'ionicity', 'Cromer Mann coeff', '', '',
'magn. amplitude', 'magn. phi', 'magn. gamma'],
'value': [self.id, self.symbol, self.name, self.atomic_number_z,
self.mass_number_a, '{:.4~P}'.format(self.mass), self.ionicity,
np.array_str(self.cromer_mann_coeff[0:4]),
np.array_str(self.cromer_mann_coeff[4:8]),
np.array_str(self.cromer_mann_coeff[8:]),
self.mag_amplitude, self.mag_phi, self.mag_gamma]}
return 'Atom with the following properties\n' + \
tabulate(output, colalign=('right',), tablefmt="rst", floatfmt=('.2f', '.2f'))
[docs]
def read_atomic_form_factor_coeff(self, source='chantler', filename=''):
"""read_atomic_form_factor_coeff
The coefficients for the atomic form factor :math:`f` in dependence of
the photon energy :math:`E` is read from a parameter file given by [1]_
or by [2]_ as default.
Args:
source (str, optional): source of atmoic form factors can be either
_henke_ or _chantler_. Defaults to _chantler_.
filename (str, optional): full path and filename to the atomic form
factor coefficients.
Returns:
f (ndarray[float]): atomic form factor coefficients.
"""
if not filename:
if source not in ['chantler', 'henke']:
raise ValueError('The source of the atomic form factors must be '
'either chantler or henke!')
if source == 'chantler':
sub_path = 'chantler/{:s}.cf'.format(self.symbol.lower())
elif source == 'henke':
sub_path = 'henke/{:s}.nff'.format(self.symbol.lower())
filename = os.path.join(os.path.dirname(__file__),
'../parameters/atomic_form_factors/{:s}'.format(sub_path))
try:
f = np.genfromtxt(filename, skip_header=0)
except OSError:
print('Atomic form factor file {:s} not found!'.format(filename))
raise
return f
[docs]
@u.wraps(None, (None, 'eV'), strict=False)
def get_atomic_form_factor(self, energy):
"""get_atomic_form_factor
The complex atomic form factor for the photon energy :math:`E` [eV] is
calculated by:
.. math:: f(E)=f_1 - i f_2
Convention of Ref. [3]_ (p. 11, footnote) is a negative :math:`f_2`.
Args:
energy (ndarray[float]): photon energy [eV].
Returns:
f (ndarray[complex]): energy-dependent atomic form factors.
"""
# interpolate the real and imaginary part in dependence of E
f1 = np.interp(energy, self.atomic_form_factor_coeff[:, 0],
self.atomic_form_factor_coeff[:, 1])
f2 = np.interp(energy, self.atomic_form_factor_coeff[:, 0],
self.atomic_form_factor_coeff[:, 2])
return f1 - f2*1j
[docs]
def read_cromer_mann_coeff(self):
r"""read_cromer_mann_coeff
The Cromer-Mann coefficients (Ref. [4]_) are read from a parameter file
and are returned in the following order:
.. math:: a_1\; a_2\; a_3\; a_4\; b_1\; b_2\; b_3\; b_4\; c
Returns:
cm (ndarray[float]): Cromer-Mann coefficients.
"""
filename = os.path.join(os.path.dirname(__file__),
'../parameters/atomic_form_factors/cromermann.txt')
try:
cm = np.genfromtxt(filename, skip_header=1,
usecols=(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11))
except Exception as e:
print('File {:s} not found!'.format(filename))
print(e)
return cm[(cm[:, 0] == self.atomic_number_z) & (cm[:, 1] == self.ionicity)][0]
[docs]
@u.wraps(None, (None, 'eV', 'm**-1'), strict=False)
def get_cm_atomic_form_factor(self, energy, qz):
r"""get_cm_atomic_form_factor
The atomic form factor :math:`f` is calculated in dependence of the
photon energy :math:`E` [eV] and the :math:`z`-component of the
scattering vector :math:`q_z` [Å :math:`^{-1}`] (Ref. [4]_).
Note that the Cromer-Mann coefficients are fitted for :math:`q_z` in
[Å :math:`^{-1}`]!
See Ref. [3]_ (p. 235).
.. math:: f(q_z,E) = f_{CM}(q_z) + \delta f_1(E) -i f_2(E)
:math:`f_{CM}(q_z)` is given in Ref. [4]_:
.. math::
f_{CM}(q_z) = \sum(a_i \, \exp(-b_i \, (q_z/4\pi)^2))+ c
:math:`\delta f_1(E)` is the dispersion correction:
.. math::
\delta f_1(E) = f_1(E) - \left(\sum^4_i(a_i) + c\right)
Thus:
.. math:: f(q_z,E) = \sum(a_i \, \exp(-b_i \, q_z/2\pi))
+ c + f_1(E)-i f_2(E) - \left(\sum(a_i) + c\right)
.. math:: f(q_z,E) = \sum(a_i \, \exp(-b_i \, q_z/2\pi))
+ f_1(E) -i f_2(E) - \sum(a_i)
Args:
energy (ndarray[float]): photon energy [eV].
qz (ndarray[float]): scattering vector [1/m].
Returns:
f (ndarray[complex]): energy- and qz-dependent Cromer-Mann atomic form
factors.
"""
# convert from 1/nm to 1/Å and to a real column vector
qz = np.array(qz*1e10, ndmin=2)
energy = np.array(energy, ndmin=1)
if np.size(qz, 0) != len(energy):
raise TypeError('qz need to have as many rows as energies!')
f = np.zeros_like(qz, dtype=complex)
for i, en in enumerate(energy):
_qz = qz[i, :].reshape(-1, 1)
f_cm = np.dot(self.cromer_mann_coeff[0:3],
np.exp(np.outer(-self.cromer_mann_coeff[4:7],
(_qz/(4*np.pi))**2)))
f[i, :] = f_cm + self.get_atomic_form_factor(en) -\
np.sum(self.cromer_mann_coeff[0:3])
return f
[docs]
def read_magnetic_form_factor_coeff(self, filename=''):
"""read_magnetic_form_factor_coeff
The coefficients for the magnetic form factor :math:`m` in dependence
of the photon energy :math:`E` is read from a parameter file.
Args:
filename (str): optional full path and filename to the magnetic
form factor coefficients.
Returns:
m (ndarray[float]): magnetic form factor coefficients.
"""
if not filename:
filename = os.path.join(os.path.dirname(__file__),
'../parameters/magnetic_form_factors/{:s}.mf'.format(
self.symbol))
try:
m = np.genfromtxt(filename)
except Exception as e:
print('File {:s} not found!'.format(filename))
print(e)
# return zero array
m = np.zeros([1, 3])
return m
[docs]
@u.wraps(None, (None, 'eV'), strict=False)
def get_magnetic_form_factor(self, energy):
"""get_magnetic_form_factor
The complex magnetic form factor is claculated by:
.. math:: m(E) = m_1 - i m_2
for the photon energy :math:`E` [eV].
Convention of Ref. [3]_ (p. 11, footnote) is a negative :math:`m_2`
Args:
energy (ndarray[float]): photon energy [eV].
Returns:
m (ndarray[complex]): energy-dependent magnetic form factors.
"""
# interpolate the real and imaginary part in dependence of E
m1 = np.interp(energy, self.magnetic_form_factor_coeff[:, 0],
self.magnetic_form_factor_coeff[:, 1])
m2 = np.interp(energy, self.magnetic_form_factor_coeff[:, 0],
self.magnetic_form_factor_coeff[:, 2])
return m1 - m2*1j
@property
def mag_phi(self):
return Q_(self._mag_phi, u.rad).to('deg')
@mag_phi.setter
def mag_phi(self, mag_phi):
self._mag_phi = mag_phi.to_base_units().magnitude
@property
def mag_gamma(self):
return Q_(self._mag_gamma, u.rad).to('deg')
@mag_gamma.setter
def mag_gamma(self, mag_gamma):
self._mag_gamma = mag_gamma.to_base_units().magnitude
[docs]
class AtomMixed(Atom):
"""AtomMixed
Representation of mixed atoms in alloys and stochiometric mixtures.
All properties of the included sub-atoms of class Atom are averaged
and weighted with their stochiometric ratio.
Args:
symbol (str): symbol of the atom.
Keyword Args:
id (str): id of the atom, may differ from symbol and/or name.
name (str): name of the mixed atom, default is symbol.
atomic_form_factor_path (str): path to atomic form factor coeffs.
magnetic_form_factor_path (str): path to magnetic form factor coeffs.
Attributes:
symbol (str): symbol of the element.
id (str): id of the atom, may differ from symbol and/or name.
name (str): name of the mixed atom, default is symbol.
atomic_number_z (int): Z atomic number.
mass_number_a (float): A atomic mass number.
ionicity (int): ionicity of the atom.
mass (float): mass of the atom [kg].
atomic_form_factor_coeff (ndarray[float]): atomic form factor.
coefficients for energy-dependent atomic form factor.
magnetic_form_factor_coeff (ndarray[float]): magnetic form factor
coefficients for energy-dependent magnetic form factor.
mag_amplitude (float): magnetization amplitude -1 .. 1.
mag_phi (float): phi angle of magnetization [rad].
mag_gamma (float): gamma angle of magnetization [rad].
atoms (list[Atoms]): list of Atoms.
num_atoms (int): number of atoms.
"""
def __init__(self, symbol, **kwargs):
self.symbol = symbol
self.id = kwargs.get('id', symbol)
self.name = kwargs.get('name', symbol)
self.mag_amplitude = kwargs.get('mag_amplitude', 0)
self.mag_phi = kwargs.get('mag_phi', 0*u.deg)
self.mag_gamma = kwargs.get('mag_gamma', 0*u.deg)
self.ionicity = 0
self.atomic_number_z = 0
self.mass_number_a = 0
self.mass = 0
self.atoms = []
self.num_atoms = 0
self.atomic_form_factor_coeff = self.read_atomic_form_factor_coeff(
filename=kwargs.get('atomic_form_factor_path', ''))
self.magnetic_form_factor_coeff = self.read_magnetic_form_factor_coeff(
filename=kwargs.get('magnetic_form_factor_path', ''))
def __str__(self):
"""String representation of this class"""
output = {'parameter': ['id', 'symbol', 'name', 'atomic number Z', 'mass number A', 'mass',
'ionicity', 'magn. amplitude', 'magn. phi', 'magn. gamma'],
'value': [self.id, self.symbol, self.name, self.atomic_number_z,
self.mass_number_a, '{:.4~P}'.format(self.mass), self.ionicity,
self.mag_amplitude, self.mag_phi, self.mag_gamma]}
output_atom = []
for i in range(self.num_atoms):
output_atom.append([self.atoms[i][0].name, '{:.1f} %'.format(self.atoms[i][1]*100)])
return ('AtomMixed with the following properties\n'
+ tabulate(output, colalign=('right',), tablefmt="rst", floatfmt=('.2f', '.2f'))
+ '\n{:d} Constituents:\n'.format(self.num_atoms)
+ tabulate(output_atom, colalign=('right',), floatfmt=('.2f', '.2f')))
[docs]
def add_atom(self, atom, fraction):
"""add_atom
Add an Atom instance with its stochiometric fraction and recalculate
averaged properties.
Args:
atom (Atom): atom to add.
fraction (float): fraction of the atom - sum of all fractions must
be 1.
"""
if isinstance(atom, Atom):
self.atoms.append([atom, fraction])
self.num_atoms = self.num_atoms + 1
# calculate the mixed atomic properties of the atomMixed instance
self.atomic_number_z = self.atomic_number_z + fraction * atom.atomic_number_z
self.mass_number_a = self.mass_number_a + fraction * atom.mass_number_a
self.mass = self.mass + fraction * atom.mass
self.ionicity = self.ionicity + fraction * atom.ionicity
else:
warnings.warn('Only Atom objects can be added to a MixedAtom!')
[docs]
def read_atomic_form_factor_coeff(self, filename=''):
"""read_atomic_form_factor_coeff
The coefficients for the atomic form factor :math:`f` in dependence of
the photon energy :math:`E` must be read from an external file given
by ``filename``.
Args:
filename (str, optional): full path and filename to the atomic form
factor coefficients.
Returns:
f (ndarray[float]): atomic form factor coefficients.
"""
if not filename:
return None
try:
f = np.genfromtxt(filename, skip_header=0)
except Exception as e:
print('File {:s} not found!'.format(filename))
print(e)
return f
[docs]
@u.wraps(None, (None, 'eV'), strict=False)
def get_atomic_form_factor(self, energy):
"""get_atomic_form_factor
Averaged energy dependent atomic form factor.
If ``atomic_form_factor_path`` was given on initialization this file
will be used instead.
Args:
energy (ndarray[float]): photon energy [eV].
Returns:
f (ndarray[complex]): energy-dependent atomic form factors.
"""
if self.atomic_form_factor_coeff is None:
# no external file is given
# calculate average from added atoms
f = 0
for i in range(self.num_atoms):
f += self.atoms[i][0].get_atomic_form_factor(energy) * self.atoms[i][1]
else:
f = super().get_atomic_form_factor(energy)
return f
[docs]
@u.wraps(None, (None, 'eV', 'm**-1'), strict=False)
def get_cm_atomic_form_factor(self, energy, qz):
"""get_cm_atomic_form_factor
Averaged energy and qz-dependent atomic form factors.
Args:
energy (ndarray[float]): photon energy [eV].
qz (ndarray[float]): scattering vector [1/m].
Returns:
f (ndarray[complex]): energy- and qz-dependent Cromer-Mann atomic
form factors.
"""
if self.atomic_form_factor_coeff is None:
# no external file is given
# calculate average from added atoms
f = 0
for i in range(self.num_atoms):
f += self.atoms[i][0].get_cm_atomic_form_factor(energy, qz) * self.atoms[i][1]
else:
warnings.warn('Cromer-Mann correction cannot be applied to '
'atomic form factors from external files. '
'Returning uncorrected values instead!')
f = self.get_atomic_form_factor(energy)
return f
[docs]
def read_magnetic_form_factor_coeff(self, filename=''):
"""read_magnetic_form_factor_coeff
The coefficients for the magnetic form factor :math:`m` in dependence
of the photon energy :math:`E` must be read from an external file given
by ``filename``.
Args:
filename (str): optional full path and filename to the magnetic
form factor coefficients.
Returns:
m (ndarray[float]): magnetic form factor coefficients.
"""
if not filename:
return None
try:
m = np.genfromtxt(filename)
except Exception as e:
print('File {:s} not found!'.format(filename))
print(e)
# return zero array
m = np.zeros([1, 3])
return m
[docs]
@u.wraps(None, (None, 'eV'), strict=False)
def get_magnetic_form_factor(self, energy):
"""get_magnetic_form_factor
Mixed energy dependent magnetic form factors.
Args:
energy (ndarray[float]): photon energy [eV].
Returns:
f (ndarray[complex]): energy-dependent magnetic form factors.
"""
if self.magnetic_form_factor_coeff is None:
# no external file is given
# calculate average from added atoms
m = 0
for i in range(self.num_atoms):
m += self.atoms[i][0].get_magnetic_form_factor(energy) * self.atoms[i][1]
else:
m = super().get_magnetic_form_factor(energy)
return m
@property
def mag_phi(self):
return Q_(self._mag_phi, u.rad).to('deg')
@mag_phi.setter
def mag_phi(self, mag_phi):
self._mag_phi = mag_phi.to_base_units().magnitude
@property
def mag_gamma(self):
return Q_(self._mag_gamma, u.rad).to('deg')
@mag_gamma.setter
def mag_gamma(self, mag_gamma):
self._mag_gamma = mag_gamma.to_base_units().magnitude