Source code for dynasor.post_processing.x_ray_form_factors

import json
from importlib.resources import files
from typing import Optional
from warnings import warn

from numpy import exp, abs, pi
from pandas import DataFrame, concat
from .weights import Weights


[docs] class XRayFormFactors(Weights): r"""This class generates sample weights corresponding to X-ray form factors, specifically the non-dispersion corrected parametrized form factors. In general, the form factor may be written as .. math:: f(q, \omega) = f_0(q) + f'(q, \omega) + if''(q, \omega) where :math:`q` is a scalar, corresponding to the norm of the desired reciprocal lattice point. The weights generated by this class corresponds to :math:`f_0(q)`. There are two possible parametrizations of :math:`f_0(q)` to choose from, both based on a sum of exponentials of the form .. math:: f_0(q) = \sum_{i=1}^k a_i \exp(-b_i q^2) + c. Two parametrizations are available: * ``'waasmaier-1995'`` corresponds to Table 1 from D. Waasmaier, A. Kirfel, Acta Crystallographica Section A **51**, 416 (1995); `doi: 10.1107/S0108767394013292 <https://doi.org/10.1107/S0108767394013292>`_. This parametrization uses five exponentials (:math:`k=5`) and extends up to :math:`6.0\,\mathrm{Å}^{-1}`. * ``'itc-2006'`` corresponds to Table 6.1.1.4. from *International Tables for Crystallography, Volume C: Mathematical, physical and chemical tables* (2006); `doi: 10.1107/97809553602060000103 <https://doi.org/10.1107/97809553602060000103>`_. This parametrization uses four exponentials (:math:`k=4`) and extends up to :math:`2.0\,\mathrm{Å}^{-1}`. In practice differences are expected to be insignificant. It is unlikely that you have to deviate from the default, which is ``'waasmaier-1995'``. Parameters ---------- atom_types List of atomic species for which to retrieve scattering lengths. source Source to use for parametrization of the form factors :math:`f_0(q)`. Allowed values are ``'waasmaier-1995'`` and ``'itc-2006'`` (see above). """ def __init__( self, atom_types: list[str], source: Optional[str] = 'waasmaier-1995' ): self._source = source form_factors = self._read_form_factors(source) # Select the relevant species form_factors = form_factors[form_factors.index.isin(atom_types)] # Atom species is index self._form_factors = form_factors # Check if any of the fetched form factors are missing, # indicating that it is missing in the experimental database. for s in atom_types: row = form_factors[form_factors.index == s] if row.empty: if s == 'H': # Manually insert values for H such that the form factor is # zero and raise a warning, since it is such a common element. warn('No parametrization for H. Setting form factor for H to zero.') values = [['DUMMY', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]] form_factors = concat([DataFrame(data=values, index=['H'], columns=form_factors.columns), form_factors]) continue raise ValueError('Missing tabulated values ' f'for requested species {s}.') weights_coh = form_factors.to_dict(orient='index') supports_currents = False super().__init__(weights_coh, None, supports_currents=supports_currents)
[docs] def get_weight_coh(self, atom_type: str, q_norm: float) -> float: """Get the coherent weight for a given atom type and q-vector norm.""" return self._compute_f0(self._weights_coh[atom_type], q_norm, self._source)
@property def parameters(self) -> DataFrame: """Parametrization used to compute the coherent form factors f(q) for the selected species. """ return self._form_factors def _compute_f0( self, coefficients: dict, q_norm: float, source: str): r"""Compute :math:`f_0(q)` based on the chosen parametrization. There are two possible parametrizations of :math:`f_0(q)` to choose from, both based on a sum of exponentials of the form .. math:: f_0(q) = \sum_{i=1}^k a_i * exp(-b_i * s**2) + c where :math:`s = sin(theta) / lambda`. Parameters ---------- coefficients Parametrization parameters, read from the corresponding source file. q_norm The |q|-value at which to evaluate the form factor. source Either 'waasmaier-1995' or 'itc-2006', corresponding to the two available sources for the parametrization of the :math:`f_0(q)` term of the form factors. """ s = q_norm / (4 * pi) # q in dynasor is q = 4 pi sin(theta) / lambda. s_squared = s*s # s = sin(theta) / lambda if source == 'waasmaier-1995': if abs(s) > 6.0: warn('Waasmaier parametrization is not reliable for q ' 'above 75.398 rad/Å (corresponding to s=6.0 1/Å)') return self._get_f0(coefficients, s_squared, nmax=5) elif source == 'itc-2006': if abs(s) > 2.0: warn('ITC.C parametrization is not reliable for q ' 'above 25.132 rad/Å (corresponding to s=2.0 1/Å)') return self._get_f0(coefficients, s_squared, nmax=4) else: raise ValueError(f'Unknown source {source}') def _get_f0(self, coefficients: dict, s_squared: float, nmax: Optional[int] = 5): r""" Compute parametrizations of :math:`f_0(q)` on the form .. math:: f_0(q) = \sum_{i=1}^k a_i * exp(-b_i * s**2) + c where :math:`s = sin(theta) / lambda`. """ f0 = coefficients['c'] for i in range(1, nmax+1): f0 += coefficients[f'a{i}'] * exp(-coefficients[f'b{i}'] * s_squared) return f0 def _read_form_factors(self, source: str) -> DataFrame: r""" Extracts the parametrization for the form factors :math:`f_0(q)`, based on either of two sources. Parameters ---------- source Either 'waasmaier-1995' or 'itc-2006', corresponding to the two available sources for the parametrization of the :math:`f_0(q)` term of the form factors. """ if source == 'waasmaier-1995': data_file = files(__package__) / \ 'form-factors/x-ray-parameters-waasmaier-kirfel-1995.json' with open(data_file) as fp: coefficients = json.load(fp) form_factors = DataFrame.from_dict(coefficients) form_factors.index.names = ['species'] elif source == 'itc-2006': data_file = files(__package__) / \ 'form-factors/x-ray-parameters-itcc-2006.json' with open(data_file) as fp: coefficients = json.load(fp) form_factors = DataFrame.from_dict(coefficients) # There are two entries for H, one calculated using # Hartree-Fock and one calculated with SDS. # Both parametrizations are similar. We'll # use HF since that has been used for the majority # of species. form_factors.drop(index='0', inplace=True) # H SDS is the first row form_factors = form_factors.set_index('species') else: raise ValueError(f'Unknown source {source}') return form_factors