Coverage for local_installation/dynasor/post_processing/x_ray_form_factors.py: 95%
60 statements
« prev ^ index » next coverage.py v7.3.2, created at 2025-04-16 06:13 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2025-04-16 06:13 +0000
1import json
2from importlib.resources import files
3from typing import List, Dict
4from warnings import warn
6from numpy import exp, abs, pi
7from pandas import DataFrame, concat
8from .weights import Weights
11class XRayFormFactors(Weights):
12 r"""This class generates sample weights corresponding to X-ray form factors,
13 specifically the non-dispersion corrected parametrized form factors.
14 In general, the form factor for may be written as
16 .. math::
17 f(q, \omega) = f_0(q) + f'(q, \omega) + if''(q, \omega)
19 where :math:`q` is a scalar, corresponding to the norm
20 of the desired reciprocal lattice point.
22 The weights generated by this class corresponds to :math:`f_0(q)`.
23 There are two possible parametrizations of :math:`f_0(q)`
24 to choose from, both based on a sum of exponentials of the form
26 .. math::
28 f_0(q) = \sum_{i=1}^k a_i \exp(-b_i q^2) + c.
30 Two parametrizations are available:
32 * ``'waasmaier-1995'`` corresponds to Table 1 from D. Waasmaier, A. Kirfel,
33 Acta Crystallographica Section A **51**, 416 (1995);
34 `doi: 10.1107/S0108767394013292 <https://doi.org/10.1107/S0108767394013292>`_.
35 This parametrization uses five exponentials (:math:`k=5`) and extends up to
36 :math:`6.0\,\mathrm{Å}^{-1}`.
37 * ``'itc-2006'`` corresponds to Table 6.1.1.4. from
38 *International Tables for Crystallography, Volume C: Mathematical, physical
39 and chemical tables* (2006);
40 `doi: 10.1107/97809553602060000103 <https://doi.org/10.1107/97809553602060000103>`_.
41 This parametrization use sfour exponentials (:math:`k=4`) and extends up to
42 :math:`2.0\,\mathrm{Å}^{-1}`.
44 In practice differences are expected to be insignificant. It is unlikely that
45 you have to deviate from the default, which is ``'waasmaier-1995'``.
47 Parameters
48 ----------
49 atom_types
50 List of atomic species for which to retrieve scattering lengths.
51 source
52 Source to use for parametrization of the form factors :math:`f_0(q)`.
53 Allowed values are ``'waasmaier-1995'`` and ``'itc-2006'``
54 (see above).
55 """
57 def __init__(
58 self,
59 atom_types: List[str],
60 source: str = 'waasmaier-1995'
61 ):
62 self._source = source
63 form_factors = self._read_form_factors(source)
64 # Select the relevant species
65 form_factors = form_factors[form_factors.index.isin(atom_types)] # Atom species is index
66 self._form_factors = form_factors
68 # Check if any of the fetched form factors are missing,
69 # indicating that it is missing in the experimental database.
70 for s in atom_types:
71 row = form_factors[form_factors.index == s]
72 if row.empty:
73 if s == 'H':
74 # Manually insert values for H such that the form factor is
75 # zero and raise a warning, since it is such a common element.
76 warn('No parametrization for H. Setting form factor for H to zero.')
77 values = [['DUMMY', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]
78 form_factors = concat([DataFrame(data=values,
79 index=['H'],
80 columns=form_factors.columns),
81 form_factors])
82 continue
83 raise ValueError('Missing tabulated values '
84 f'for requested species {s}.')
86 weights_coh = form_factors.to_dict(orient='index')
87 supports_currents = False
88 super().__init__(weights_coh, None, supports_currents=supports_currents)
90 def get_weight_coh(self, atom_type, q_norm):
91 """Get the coherent weight for a given atom type and q-vector norm."""
92 return self._compute_f0(self._weights_coh[atom_type], q_norm, self._source)
94 def _compute_f0(
95 self,
96 coefficients: Dict,
97 q_norm: float,
98 source: str):
99 r"""Compute :math:`f_0(q)` based on the chosen parametrization.
100 There are two possible parametrizations of :math:`f_0(q)` to choose from,
101 both based on a sum of exponentials of the form
103 .. math::
105 f_0(q) = \sum_{i=1}^k a_i * exp(-b_i * s**2) + c
107 where :math:`s = sin(theta) / lambda`.
108 Parameters
109 ----------
110 coefficients
111 Parametrization parameters, read from the corresponding source file.
112 q_norm
113 The |q|-value at which to evaluate the form factor.
114 source
115 Either 'waasmaier-1995' or 'itc-2006',
116 corresponding to the two available sources for the
117 parametrization of the :math:`f_0(q)` term of the form factors.
118 """
119 s = q_norm / (4 * pi) # q in dynasor is q = 4 pi sin(theta) / lambda.
120 s_squared = s*s # s = sin(theta) / lambda
121 if source == 'waasmaier-1995':
122 if abs(s) > 6.0:
123 warn('Waasmaier parametrization is not reliable for q '
124 'above 75.398 rad/Å (corresponding to s=6.0 1/Å)')
125 return self._get_f0(coefficients, s_squared, nmax=5)
126 elif source == 'itc-2006': 126 ↛ 132line 126 didn't jump to line 132, because the condition on line 126 was never false
127 if abs(s) > 2.0:
128 warn('ITC.C parametrization is not reliable for q '
129 'above 25.132 rad/Å (corresponding to s=2.0 1/Å)')
130 return self._get_f0(coefficients, s_squared, nmax=4)
131 else:
132 raise ValueError(f'Unknown source {source}')
134 def _get_f0(self, coefficients, s_squared, nmax=5):
135 r"""
136 Compute parametrisations of :math:`f_0(q)` on the form
138 .. math::
140 f_0(q) = \sum_{i=1}^k a_i * exp(-b_i * s**2) + c
142 where :math:`s = sin(theta) / lambda`.
143 """
144 f0 = coefficients['c']
145 for i in range(1, nmax+1):
146 f0 += coefficients[f'a{i}'] * exp(-coefficients[f'b{i}'] * s_squared)
147 return f0
149 def _read_form_factors(self, source: str) -> DataFrame:
150 r"""
151 Extracts the parametrization for the form factors :math:`f_0(q)`,
152 based on either of two sources.
154 Parameters
155 ----------
156 source
157 Either 'waasmaier-1995' or 'itc-2006',
158 corresponding to the two available sources for the
159 parametrization of the :math:`f_0(q)` term of the form factors.
160 """
161 if source == 'waasmaier-1995':
162 data_file = files(__package__) / \
163 'form-factors/x-ray-parameters-waasmaier-kirfel-1995.json'
164 with open(data_file) as fp:
165 coefficients = json.load(fp)
167 form_factors = DataFrame.from_dict(coefficients)
168 form_factors.index.names = ['species']
169 elif source == 'itc-2006': 169 ↛ 184line 169 didn't jump to line 184, because the condition on line 169 was never false
170 data_file = files(__package__) / \
171 'form-factors/x-ray-parameters-itcc-2006.json'
172 with open(data_file) as fp:
173 coefficients = json.load(fp)
175 form_factors = DataFrame.from_dict(coefficients)
176 # There are two entries for H, one calculated using
177 # Hartree-Fock and one calculated with SDS.
178 # Both parametrizations are similar. We'll
179 # use HF since that has been used for the majority
180 # of species.
181 form_factors.drop(index='0', inplace=True) # H SDS is the first row
182 form_factors = form_factors.set_index('species')
183 else:
184 raise ValueError(f'Unknown source {source}')
186 return form_factors