Coverage for local_installation/dynasor/post_processing/x_ray_form_factors.py: 90%

47 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2024-12-21 12:02 +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 * ``'international-iv-1974'`` corresponds to Table 2.2B from 

38 *International Tables for X-ray Crystallography, Vol. IV*, 

39 The Kynoch Press: Birmingham, England, 1974. This parametrization uses 

40 four exponentials (:math:`k=4`) and extends up to 

41 :math:`2.0\,\mathrm{Å}^{-1}`. 

42 

43 In practice differences are expected to be insignificant. It is unlikely that 

44 you have to deviate from the default, which is ``'waasmaier-1995'``. 

45 

46 Parameters 

47 ---------- 

48 atom_types 

49 List of atomic species for which to retrieve scattering lengths. 

50 source 

51 Source to use for parametrization of the form factors :math:`f_0(q)`. 

52 Allowed values are ``'waasmaier-1995'`` and ``'international-iv-1974'`` 

53 (see above). 

54 """ 

55 

56 def __init__( 

57 self, 

58 atom_types: List[str], 

59 source: str = 'waasmaier-1995' 

60 ): 

61 self._source = source 

62 form_factors = self._read_form_factors(source) 

63 # Select the relevant species 

64 form_factors = form_factors[form_factors.index.isin(atom_types)] # Atom species is index 

65 self._form_factors = form_factors 

66 

67 # Check if any of the fetched form factors are missing, 

68 # indicating that it is missing in the experimental database. 

69 for s in atom_types: 

70 row = form_factors[form_factors.index == s] 

71 if row.empty: 

72 if s == 'H': 

73 # Manually insert values for H such that the form factor is 

74 # zero and raise a warning, since it is such a common element. 

75 warn('No parametrization for H. Setting form factor for H to zero.') 

76 values = [['DUMMY', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]] 

77 form_factors = concat([DataFrame(data=values, 

78 index=['H'], 

79 columns=form_factors.columns), 

80 form_factors]) 

81 continue 

82 raise ValueError('Missing tabulated values ' 

83 f'for requested species {s}.') 

84 

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

86 supports_currents = False 

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

88 

89 def get_weight_coh(self, atom_type, q_norm): 

90 """Get the coherent weight for a given atom type and q-vector norm.""" 

91 return self._compute_f0(self._weights_coh[atom_type], q_norm, self._source) 

92 

93 def _compute_f0( 

94 self, 

95 coefficients: Dict, 

96 q_norm: float, 

97 source: str): 

98 r"""Compute f_0(q) based on the chosen parametrization. 

99 There are two possible parametrizations of f_0(q) to choose from, 

100 both based on a sum of exponentials of the form 

101 

102 .. math:: 

103 

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

105 

106 Parameters 

107 ---------- 

108 coefficients 

109 Parametrization parameters, read from the corresponding source file. 

110 q_norm 

111 The |q|-value at which to evaluate the form factor. 

112 source 

113 Either 'waasmaier-1995' or 'international-iv-1974', 

114 corresponding to the two available sources for the 

115 parametrization of the :math:`f_0(q)` term of the form factors. 

116 """ 

117 if source == 'waasmaier-1995': 117 ↛ 133line 117 didn't jump to line 133, because the condition on line 117 was never false

118 s = q_norm / (4 * pi) # q in dynasor is q = 4 pi sin(theta) / lambda. 

119 s_squared = s*s # s = sin(theta) / lambda in the Waasmaier paper. 

120 if abs(s) > 6.0: 

121 warn('Waasmaier parametrization is not reliable for q ' 

122 'above 75.398 rad/Å (corresponding to s=6.0 1/Å)') 

123 # Very verbose and repetitive, but I think it is more readable 

124 # than a reduction over the Series object. 

125 # Suggestions for a nice & readable solution are welcome. 

126 f0 = coefficients['c'] + \ 

127 coefficients['a1'] * exp(- coefficients['b1'] * s_squared) + \ 

128 coefficients['a2'] * exp(- coefficients['b2'] * s_squared) + \ 

129 coefficients['a3'] * exp(- coefficients['b3'] * s_squared) + \ 

130 coefficients['a4'] * exp(- coefficients['b4'] * s_squared) + \ 

131 coefficients['a5'] * exp(- coefficients['b5'] * s_squared) 

132 return f0 

133 elif source == 'international-iv-1974': 

134 raise NotImplementedError('The ITC 1974 parametrization has not been implemented yet.') 

135 else: 

136 raise ValueError(f'Unknown source {source}') 

137 

138 def _read_form_factors(self, source: str) -> DataFrame: 

139 r""" 

140 Extracts the parametrization for the form factors :math:`f_0(q)`, 

141 based on either of two sources. 

142 

143 Parameters 

144 ---------- 

145 source 

146 Either 'waasmaier-1995' or 'international-iv-1974', 

147 corresponding to the two available sources for the 

148 parametrization of the :math:`f_0(q)` term of the form factors. 

149 """ 

150 if source == 'waasmaier-1995': 150 ↛ 158line 150 didn't jump to line 158, because the condition on line 150 was never false

151 data_file = files(__package__) / \ 

152 'form-factors/x-ray-parameters-waasmaier-kirfel-1995.json' 

153 with open(data_file) as fp: 

154 coefficients = json.load(fp) 

155 

156 form_factors = DataFrame.from_dict(coefficients) 

157 form_factors.index.names = ['species'] 

158 elif source == 'international-iv-1974': 

159 raise NotImplementedError('The ITC 1974 parametrization has not been implemented yet.') 

160 else: 

161 raise ValueError(f'Unknown source {source}') 

162 

163 return form_factors