Coverage for dynasor / post_processing / x_ray_form_factors.py: 98%
63 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-16 12:31 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-16 12:31 +0000
1import json
2from importlib.resources import files
3from typing import Optional
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 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 uses four 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: Optional[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: str, q_norm: float) -> float:
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 @property
95 def parameters(self) -> DataFrame:
96 """Parametrization used to compute the coherent form factors
97 f(q) for the selected species.
98 """
99 return self._form_factors
101 def _compute_f0(
102 self,
103 coefficients: dict,
104 q_norm: float,
105 source: str):
106 r"""Compute :math:`f_0(q)` based on the chosen parametrization.
107 There are two possible parametrizations of :math:`f_0(q)` to choose from,
108 both based on a sum of exponentials of the form
110 .. math::
112 f_0(q) = \sum_{i=1}^k a_i * exp(-b_i * s**2) + c
114 where :math:`s = sin(theta) / lambda`.
115 Parameters
116 ----------
117 coefficients
118 Parametrization parameters, read from the corresponding source file.
119 q_norm
120 The |q|-value at which to evaluate the form factor.
121 source
122 Either 'waasmaier-1995' or 'itc-2006',
123 corresponding to the two available sources for the
124 parametrization of the :math:`f_0(q)` term of the form factors.
125 """
126 s = q_norm / (4 * pi) # q in dynasor is q = 4 pi sin(theta) / lambda.
127 s_squared = s*s # s = sin(theta) / lambda
128 if source == 'waasmaier-1995':
129 if abs(s) > 6.0:
130 warn('Waasmaier parametrization is not reliable for q '
131 'above 75.398 rad/Å (corresponding to s=6.0 1/Å)')
132 return self._get_f0(coefficients, s_squared, nmax=5)
133 elif source == 'itc-2006': 133 ↛ 139line 133 didn't jump to line 139 because the condition on line 133 was always true
134 if abs(s) > 2.0:
135 warn('ITC.C parametrization is not reliable for q '
136 'above 25.132 rad/Å (corresponding to s=2.0 1/Å)')
137 return self._get_f0(coefficients, s_squared, nmax=4)
138 else:
139 raise ValueError(f'Unknown source {source}')
141 def _get_f0(self, coefficients: dict, s_squared: float, nmax: Optional[int] = 5):
142 r"""
143 Compute parametrizations of :math:`f_0(q)` on the form
145 .. math::
147 f_0(q) = \sum_{i=1}^k a_i * exp(-b_i * s**2) + c
149 where :math:`s = sin(theta) / lambda`.
150 """
151 f0 = coefficients['c']
152 for i in range(1, nmax+1):
153 f0 += coefficients[f'a{i}'] * exp(-coefficients[f'b{i}'] * s_squared)
154 return f0
156 def _read_form_factors(self, source: str) -> DataFrame:
157 r"""
158 Extracts the parametrization for the form factors :math:`f_0(q)`,
159 based on either of two sources.
161 Parameters
162 ----------
163 source
164 Either 'waasmaier-1995' or 'itc-2006',
165 corresponding to the two available sources for the
166 parametrization of the :math:`f_0(q)` term of the form factors.
167 """
168 if source == 'waasmaier-1995':
169 data_file = files(__package__) / \
170 'form-factors/x-ray-parameters-waasmaier-kirfel-1995.json'
171 with open(data_file) as fp:
172 coefficients = json.load(fp)
174 form_factors = DataFrame.from_dict(coefficients)
175 form_factors.index.names = ['species']
176 elif source == 'itc-2006':
177 data_file = files(__package__) / \
178 'form-factors/x-ray-parameters-itcc-2006.json'
179 with open(data_file) as fp:
180 coefficients = json.load(fp)
182 form_factors = DataFrame.from_dict(coefficients)
183 # There are two entries for H, one calculated using
184 # Hartree-Fock and one calculated with SDS.
185 # Both parametrizations are similar. We'll
186 # use HF since that has been used for the majority
187 # of species.
188 form_factors.drop(index='0', inplace=True) # H SDS is the first row
189 form_factors = form_factors.set_index('species')
190 else:
191 raise ValueError(f'Unknown source {source}')
193 return form_factors