Source code for dynasor.post_processing.x_ray_form_factors

import json
from importlib.resources import files
from typing import List, Dict
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 for 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}`. * ``'international-iv-1974'`` corresponds to Table 2.2B from *International Tables for X-ray Crystallography, Vol. IV*, The Kynoch Press: Birmingham, England, 1974. 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 ``'international-iv-1974'`` (see above). """ def __init__( self, atom_types: List[str], source: 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, q_norm): """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)
def _compute_f0( self, coefficients: Dict, q_norm: float, source: str): r"""Compute f_0(q) based on the chosen parametrization. There are two possible parametrizations of 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 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 'international-iv-1974', corresponding to the two available sources for the parametrization of the :math:`f_0(q)` term of the form factors. """ if source == 'waasmaier-1995': s = q_norm / (4 * pi) # q in dynasor is q = 4 pi sin(theta) / lambda. s_squared = s*s # s = sin(theta) / lambda in the Waasmaier paper. if abs(s) > 6.0: warn('Waasmaier parametrization is not reliable for q ' 'above 75.398 rad/Å (corresponding to s=6.0 1/Å)') # Very verbose and repetitive, but I think it is more readable # than a reduction over the Series object. # Suggestions for a nice & readable solution are welcome. f0 = coefficients['c'] + \ coefficients['a1'] * exp(- coefficients['b1'] * s_squared) + \ coefficients['a2'] * exp(- coefficients['b2'] * s_squared) + \ coefficients['a3'] * exp(- coefficients['b3'] * s_squared) + \ coefficients['a4'] * exp(- coefficients['b4'] * s_squared) + \ coefficients['a5'] * exp(- coefficients['b5'] * s_squared) return f0 elif source == 'international-iv-1974': raise NotImplementedError('The ITC 1974 parametrization has not been implemented yet.') else: raise ValueError(f'Unknown source {source}') 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 'international-iv-1974', 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 == 'international-iv-1974': raise NotImplementedError('The ITC 1974 parametrization has not been implemented yet.') else: raise ValueError(f'Unknown source {source}') return form_factors