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

1import json 

2from importlib.resources import files 

3from typing import Optional 

4from warnings import warn 

5 

6from numpy import exp, abs, pi 

7from pandas import DataFrame, concat 

8from .weights import Weights 

9 

10 

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 

15 

16 .. math:: 

17 f(q, \omega) = f_0(q) + f'(q, \omega) + if''(q, \omega) 

18 

19 where :math:`q` is a scalar, corresponding to the norm 

20 of the desired reciprocal lattice point. 

21 

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 

25 

26 .. math:: 

27 

28 f_0(q) = \sum_{i=1}^k a_i \exp(-b_i q^2) + c. 

29 

30 Two parametrizations are available: 

31 

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}`. 

43 

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'``. 

46 

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 """ 

56 

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 

67 

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}.') 

85 

86 weights_coh = form_factors.to_dict(orient='index') 

87 supports_currents = False 

88 super().__init__(weights_coh, None, supports_currents=supports_currents) 

89 

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) 

93 

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 

100 

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 

109 

110 .. math:: 

111 

112 f_0(q) = \sum_{i=1}^k a_i * exp(-b_i * s**2) + c 

113 

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}') 

140 

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 

144 

145 .. math:: 

146 

147 f_0(q) = \sum_{i=1}^k a_i * exp(-b_i * s**2) + c 

148 

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 

155 

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. 

160 

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) 

173 

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) 

181 

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}') 

192 

193 return form_factors