Coverage for dynasor / post_processing / electron_scattering_factors.py: 100%
61 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 re import search
3from importlib.resources import files
4from typing import Optional
5from warnings import warn
7from numpy import exp, abs, pi
8from pandas import DataFrame, concat
9from .weights import Weights
12class ElectronScatteringFactors(Weights):
13 r"""This class generates sample weights corresponding to electron scattering factors.
14 The scattering factors are parametrized as sums of exponentials
15 of the following form
17 .. math::
19 f(q) = \sum_{i=1}^k a_i \exp(-b_i s^2),
21 with :math:`s=\sin(\theta)/\lambda`, where and :math:`a_i` and :math:`b_i`
22 are fitting parameters. Note that there are two parametrizations for the elemental
23 scattering factors; one valid up to :math:`s = 2.0\,\mathrm{Å}^{-1}`
24 and the other valid up to :math:`s = 6.0\,\mathrm{Å}^{-1}`.
25 By default, the parametrization valid up to :math:`s = 6.0\,\mathrm{Å}^{-1}`
26 is used. Ionic scattering factors only have a single parametrization.
28 The parametrizations are based on the following publications:
30 * Elemental scattering factors are based on Table 1 and Table 3
31 from L.-M. Peng, G. Ren, S. L. Dudarev and M. J. Whelan,
32 Acta Crystallographica Section A **52**, 257-276 (1996);
33 `doi: 10.1107/S0108767395014371 <https://doi.org/10.1107/S0108767395014371>`_.
34 * Ionic scattering factors are based on Table 1 from
35 Lian-Mao. Peng, Acta Crystallographica Section A **54**, 481-485 (1998);
36 `doi: 10.1107/S0108767398001901 <https://doi.org/10.1107/S0108767398001901>`_.
38 Parameters
39 ----------
40 atom_types
41 List of atomic species for which to retrieve scattering lengths.
42 parametrization
43 Parametrization to use.
44 """
46 def __init__(
47 self,
48 atom_types: list[str],
49 parametrization: Optional[str] = 'smax6',
50 ):
51 self._parametrization = parametrization
52 scattering_factors = self._read_scattering_factors(self._parametrization)
53 # Select the relevant species
54 scattering_factors = scattering_factors[
55 scattering_factors.index.isin(atom_types)] # Atom species is index
56 self._scattering_factors = scattering_factors
58 # Check if any of the fetched form factors are missing,
59 # indicating that it is missing in the experimental database.
60 for s in atom_types:
61 row = scattering_factors[scattering_factors.index == s]
62 if row.empty:
63 raise ValueError('Missing tabulated values '
64 f'for requested species {s}.')
66 weights_coh = scattering_factors.to_dict(orient='index')
67 supports_currents = False
68 super().__init__(weights_coh, None, supports_currents=supports_currents)
70 def get_weight_coh(self, atom_type, q_norm):
71 """Get the coherent weight for a given atom type and q-vector norm."""
72 return self._compute_f(self._weights_coh[atom_type],
73 q_norm,
74 self._parametrization,
75 self._get_charge(atom_type))
77 @property
78 def parameters(self) -> DataFrame:
79 """Parametrization used to compute the coherent scattering factors
80 :math:`f(q)` for the selected species.
81 """
82 return self._scattering_factors
84 def _get_charge(self, atom_type: str) -> float:
85 """
86 Extracts the ionic charge from the atom type.
87 """
88 match = search(r'(\d+)?([+-])', atom_type)
89 if match:
90 magnitude = int(match.group(1)) if match.group(1) else 1
91 charge = magnitude * (1 if match.group(2) == '+' else -1)
92 return charge
93 return None # If no charge is found
95 def _compute_f(
96 self,
97 coefficients: dict,
98 q_norm: float,
99 parametrization: str,
100 charge: Optional[int] = None):
101 r"""Compute electronic scattering factors :math:`f(q)`.
102 The scattering factors are parametrized as a sum
103 of exponentials of the form
105 .. math::
107 f(q) = \sum_{i=1}^k a_i * exp(-b_i * s**2)
109 Ions are also offset by
111 .. math::
113 (m_0 e^2) / (8 \pi^2 \hbar^2) C / s^2 \approx 0.023934 C / s^2
115 where :math:`C` is the ionic charge.
117 Parameters
118 ----------
119 coefficients
120 Parametrization parameters; read from the corresponding source file.
121 q_norm
122 The :math:`|q|`-value at which to evaluate the form factor.
123 parametrization
124 If the Peng parametrization valid up to :math:`s = 6.0\,\mathrm{Å}^{-1}`
125 or :math:`s = 2.0\,\mathrm{Å}^{-1}` should be used.
126 charge
127 Integer representing the ionic charge. `None` if no charge to avoid division by zero.
128 """
129 s_max = 6.0 if parametrization == 'smax6' else 2.0
131 s = q_norm / (4 * pi) # q in dynasor is q = 4 pi sin(theta) / lambda.
132 s_squared = s*s # s = sin(theta) / lambda in the Waasmaier paper.
133 if abs(s) > s_max:
134 warn(f'Peng parametrization is not reliable for q'
135 f' above {(s_max * 4 * pi):.2f} rad/Å'
136 f' (corresponding to s={s_max} 1/Å).'
137 ' Parametrisations for ions are accurate up to s=6.0 1/Å')
138 nmax = 5
140 if charge is not None:
141 f = 0.023934 * charge / s_squared
142 else:
143 f = 0.0
144 for i in range(1, nmax+1):
145 f += coefficients[f'a{i}'] * exp(-coefficients[f'b{i}'] * s_squared)
146 return f
148 def _read_scattering_factors(self, parametrization: str) -> DataFrame:
149 r"""
150 Extracts the parametrization for the form factors :math:`f(q)`,
151 for both elemental systems and ionic species.
153 Parameters
154 ----------
155 parametrization
156 If the Peng parametrization valid up to :math:`s = 6.0\,\mathrm{Å}^{-1}`
157 or :math:`s = 2.0\,\mathrm{Å}^{-1}` should be used.
158 """
159 if parametrization == 'smax2':
160 data_file = files(__package__) / \
161 'form-factors/electron-parameters-kmax2-peng-1996.json'
162 elif parametrization == 'smax6':
163 data_file = files(__package__) / \
164 'form-factors/electron-parameters-kmax6-peng-1996.json'
165 else:
166 raise ValueError(f'Unknown parametrization {parametrization}')
168 data_file_ions = files(__package__) / \
169 'form-factors/electron-parameters-ions-peng-1998.json'
171 frames = []
172 for df in [data_file, data_file_ions]:
173 with open(df) as fp:
174 coefficients = json.load(fp)
175 frames.append(DataFrame.from_dict(coefficients))
177 scattering_factors = concat(frames)
178 scattering_factors.index.names = ['species']
179 return scattering_factors