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

1import json 

2from importlib.resources import files 

3from typing import List, Dict 

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 for 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 use sfour 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: 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, 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) 

93 

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 

102 

103 .. math:: 

104 

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

106 

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

133 

134 def _get_f0(self, coefficients, s_squared, nmax=5): 

135 r""" 

136 Compute parametrisations of :math:`f_0(q)` on the form 

137 

138 .. math:: 

139 

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

141 

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 

148 

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. 

153 

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) 

166 

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) 

174 

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

185 

186 return form_factors