Skip to content

Commit 09d6d48

Browse files
committed
feat: added a gaussian t_r function with x-ray weighing
1 parent 1814da6 commit 09d6d48

File tree

4 files changed

+238
-10
lines changed

4 files changed

+238
-10
lines changed

glassure/fitting.py

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import numpy as np
44

5-
from .utility import calculate_weighting_factor
5+
from .utility import calculate_weighting_factor, calculate_weighting_factor_wkm
66
from .transform import calculate_fr
77
from .pattern import Pattern
88

@@ -24,10 +24,28 @@ def i_q_peak(q, n, position, sigma, composition, element_1, element_2):
2424
num_atoms = sum([val for _, val in composition.items()])
2525
c_2 = composition[element_2] / num_atoms
2626
w = calculate_weighting_factor(composition, element_1, element_2, q)
27-
return n * w / c_2 * np.sin(q * position) / (q * position) * np.exp(-q ** 2 * sigma ** 2 / 2)
27+
return (
28+
n
29+
* w
30+
/ c_2
31+
* np.sin(q * position)
32+
/ (q * position)
33+
* np.exp(-(q**2) * sigma**2 / 2)
34+
)
2835

2936

30-
def t_r_peak(r, n, position, sigma, composition, element_1, element_2, q, use_modification_fcn=False, method='fft'):
37+
def t_r_peak(
38+
r,
39+
n,
40+
position,
41+
sigma,
42+
composition,
43+
element_1,
44+
element_2,
45+
q,
46+
use_modification_fcn=False,
47+
method="fft",
48+
):
3149
"""
3250
Calculates the contribution of one element 1 - element 2 peak in real space to t(r). We assume a gaussian
3351
broadening. The math is explained in the paper about NXFit (Pickup et al. 2014, J. Appl. Cryst. 47, 1790-1796).
@@ -54,4 +72,35 @@ def t_r_peak(r, n, position, sigma, composition, element_1, element_2, q, use_mo
5472
- 'fft' solves the Fourier integral by using fast fourier transformation
5573
"""
5674
q_peak = i_q_peak(q, n, position, sigma, composition, element_1, element_2)
57-
return calculate_fr(Pattern(q, q_peak + 1), r, use_modification_fcn=use_modification_fcn, method=method).y
75+
return calculate_fr(
76+
Pattern(q, q_peak + 1),
77+
r,
78+
use_modification_fcn=use_modification_fcn,
79+
method=method,
80+
).y
81+
82+
83+
def t_r_peak_gaussian(r, n, position, sigma, composition, element_1, element_2):
84+
"""
85+
Calculates the contribution of one element 1 - element 2 peak in real space to t(r). We assume a gaussian
86+
distribution. The math is well explained in the LiquidDiffract paper (Heinen and Drewitt 2022. Physics and
87+
Chemistry of Minerals 49, no. 5: 9. https://doi.org/10.1007/s00269-022-01186-6).
88+
89+
The gaussian is weighted based on the X-ray form factors of the two elements.
90+
91+
:param r: numpy array giving the r-values for which the peak will be calculated
92+
:param n: coordination number of element 2 to element 1
93+
:param position: average distance between the two elements
94+
:param sigma: measure for broadness of distances distribution
95+
:param composition: composition: dictionary with elements as key and abundances as relative numbers
96+
:param element_1: string giving element 1
97+
:param element_2: string giving element 1
98+
99+
:return: T(r) pattern
100+
"""
101+
102+
wf = 1 / calculate_weighting_factor_wkm(composition, element_1, element_2, 0)
103+
104+
part1 = n * wf / (composition[element_2] * sigma * r * np.sqrt(2 * np.pi))
105+
part2 = np.exp(-((r - position) ** 2) / (2 * sigma**2))
106+
return part1 * part2

glassure/utility.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import scipy.constants as const
1010

1111
import lmfit
12+
import xraylib as xr
1213

1314
from .scattering_factors import (
1415
calculate_coherent_scattering_factor,
@@ -252,6 +253,129 @@ def calculate_weighting_factor(
252253
)
253254

254255

256+
def calculate_weighting_factor_wkm(
257+
composition: Composition,
258+
element_1: str,
259+
element_2: str,
260+
q: np.ndarray | float = 0.0,
261+
sf_source="hajdu",
262+
) -> float:
263+
"""
264+
Calculates the weighting factor for an element-element contribution in a given composition (e.g. for Si-O in SiO2)
265+
using the Warren-Krutter-Morningstar approximation for the form factors or effective atomic numbers (Warren et al.
266+
1936).
267+
268+
This means we calculate:
269+
.. math::
270+
W_{\alpha \beta} = \frac{c_\alpha c_\beta K_\alpha(q) K_\beta(q) (2-\delta_{\alpha \beta})}{\sum_{\alpha} c_\alpha K_\alpha(q)^2}
271+
272+
:param composition: dictionary with elements as key and abundances as relative numbers
273+
:param element_1: string giving element 1
274+
:param element_2: string giving element 2
275+
:param q: Q value or numpy array with a unit of A^-1. Default is 0.0.
276+
:param sf_source: source of the scattering factors. Possible sources are 'hajdu' and 'brown_hubbell'.
277+
:return: weighting factor
278+
"""
279+
if element_1 == element_2:
280+
factor = 1
281+
else:
282+
factor = 2
283+
284+
effective_atomic_numbers = {
285+
element: calculate_wkm_effective_atomic_number(
286+
composition, element, q, sf_source
287+
)
288+
for element in composition.keys()
289+
}
290+
291+
denominator = (
292+
np.sum(
293+
[
294+
effective_atomic_numbers[element] * c
295+
for element, c in composition.items()
296+
]
297+
)
298+
** 2
299+
)
300+
numerator = (
301+
composition[element_1]
302+
* composition[element_2]
303+
* effective_atomic_numbers[element_1]
304+
* effective_atomic_numbers[element_2]
305+
* factor
306+
)
307+
308+
return numerator / denominator
309+
310+
311+
def calculate_wkm_effective_atomic_number(
312+
composition: Composition,
313+
element: str,
314+
q: np.ndarray,
315+
sf_source="hajdu",
316+
) -> float:
317+
"""
318+
Calculates the effective atomic number for a given element using the Warren-Krutter-Morningstar approximation
319+
(Warren et al. 1936).
320+
321+
This means we calculate:
322+
.. math::
323+
K_\alpha(q) = \left\langle\frac{f_\alpha( q)}{f_e(q)}\right\rangle
324+
where :math:`f_\alpha(q)` is the form factor of the element :math:`\alpha` and :math:`f_e(q)` is the effective
325+
form factor of the composition.
326+
327+
.. math::
328+
f_e(q) = \frac{\sum_{\alpha} f_\alpha(q)}{Z_{tot}}
329+
where :math:`Z_{tot}` is the total atomic number of the compositional unit
330+
331+
:param composition: dictionary with elements as key and abundances as relative numbers
332+
:param element: string giving element for which the effective atomic number will be calculated
333+
:param q: Q value or numpy array with a unit of A^-1
334+
:param sf_source: source of the scattering factors. Possible sources are 'hajdu', 'brown_hubbell' and 'xraylib'.
335+
"""
336+
337+
effective_form_factor = calculate_effective_form_factor(composition, q, sf_source)
338+
form_factor = calculate_coherent_scattering_factor(element, q, sf_source)
339+
return np.mean(form_factor / effective_form_factor)
340+
341+
342+
def calculate_total_atomic_number(composition: Composition) -> float:
343+
"""
344+
Calculates the total atomic number of a given composition.
345+
"""
346+
return sum(
347+
[xr.SymbolToAtomicNumber(element) * c for element, c in composition.items()]
348+
)
349+
350+
351+
def calculate_effective_form_factor(
352+
composition: Composition, q: np.ndarray, sf_source="hajdu"
353+
) -> np.ndarray:
354+
"""
355+
Calculates the effective form factor for a given composition, which is given by
356+
357+
.. math::
358+
f_e(q) = \frac{\sum_{\alpha} f_\alpha(q)}{Z_{tot}}
359+
where :math:`f_\alpha(q)` is the form factor of the element :math:`\alpha` and :math:`Z_{tot}` is the total atomic
360+
number of the compositional unit
361+
362+
:param composition: dictionary with elements as key and abundances as relative numbers
363+
:param q: Q value or numpy array with a unit of A^-1
364+
:param sf_source: source of the scattering factors. Possible sources are 'hajdu', 'brown_hubbell' and 'xraylib'.
365+
366+
:return: effective form factor array
367+
"""
368+
total_atomic_number = calculate_total_atomic_number(composition)
369+
370+
form_factors = [
371+
calculate_coherent_scattering_factor(element, q, sf_source) * c
372+
for element, c in composition.items()
373+
]
374+
form_factor_sum = np.sum(form_factors, axis=0)
375+
376+
return form_factor_sum / total_atomic_number
377+
378+
255379
def normalize_composition(composition: Composition) -> dict[str, float]:
256380
"""
257381
normalizes elemental abundances to 1

tests/test_fitting.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import unittest
44
import numpy as np
55

6-
from glassure.fitting import i_q_peak, t_r_peak
6+
from glassure.fitting import i_q_peak, t_r_peak, t_r_peak_gaussian
77

88

99
class FittingTest(unittest.TestCase):
@@ -22,3 +22,12 @@ def test_t_r_peak(self):
2222
q = np.linspace(0.01, 10, 1001)
2323
peak = t_r_peak(self.r, 4, 1.6, 0.15, self.composition, 'Si', 'O', q)
2424
self.assertEqual(len(peak), len(self.r))
25+
26+
def test_t_r_peak_gaussian(self):
27+
r = np.linspace(0.1, 10, 1001)
28+
peak = t_r_peak_gaussian(r, 4, 1.6, 0.15, self.composition, 'Si', 'O')
29+
self.assertEqual(len(peak), len(r))
30+
31+
import matplotlib.pyplot as plt
32+
plt.plot(r, peak)
33+
plt.show()

tests/test_utility.py

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@
1818
calculate_s0,
1919
calculate_kn_correction,
2020
parse_str_to_composition,
21+
calculate_total_atomic_number,
22+
calculate_effective_form_factor,
23+
calculate_wkm_effective_atomic_number,
24+
calculate_weighting_factor_wkm,
2125
)
2226
from glassure import Pattern
2327

@@ -39,14 +43,12 @@ def test_convert_density_to_atoms_per_cubic_angstrom(self):
3943

4044
self.assertAlmostEqual(density_au, 0.0662, places=4)
4145

42-
4346
def test_convert_density_to_grams_per_cubic_centimeter(self):
4447
composition = {"Si": 1, "O": 2}
4548
density_au = convert_density_to_atoms_per_cubic_angstrom(composition, 2.2)
4649
density = convert_density_to_grams_per_cubic_centimeter(composition, density_au)
4750
self.assertAlmostEqual(density, 2.2, places=4)
4851

49-
5052
def test_calculate_f_mean_squared(self):
5153
q = np.linspace(0, 10)
5254
composition = {"Si": 1, "O": 2}
@@ -238,15 +240,14 @@ def test_convert_two_theta_to_q_space(self):
238240
np.max(pattern_q.x), 4 * np.pi * np.sin(25.0 / 360 * np.pi) / wavelength
239241
)
240242

241-
242243
def test_parse_string_to_composition(self):
243244
inputs = [
244245
"Si O 0.5",
245246
"(Mg)(Si O2)2.5",
246247
"[Al2O3]3",
247248
"\\{Fe3O4\\}2.5",
248249
"(H2O)2[NaCl]\\{KOH\\}0.5",
249-
"\\{[Co(NH3)4(OH)2]3[Co(CN)6]\\}2"
250+
"\\{[Co(NH3)4(OH)2]3[Co(CN)6]\\}2",
250251
]
251252

252253
outputs = [
@@ -255,9 +256,54 @@ def test_parse_string_to_composition(self):
255256
{"Al": 6, "O": 9},
256257
{"Fe": 7.5, "O": 10},
257258
{"H": 4.5, "O": 2.5, "Na": 1, "Cl": 1, "K": 0.5},
258-
{"Co": 8, "N": 36, "H": 84, "O": 12, "C": 12}
259+
{"Co": 8, "N": 36, "H": 84, "O": 12, "C": 12},
259260
]
260261

261262
for input, output in zip(inputs, outputs):
262263
parsed_formula = parse_str_to_composition(input)
263264
self.assertEqual(parsed_formula, output)
265+
266+
def test_calculate_total_atomic_number(self):
267+
def test_composition(composition, total_atomic_number):
268+
total_atomic_number = calculate_total_atomic_number(composition)
269+
self.assertEqual(total_atomic_number, total_atomic_number)
270+
271+
test_composition({"Si": 1, "O": 2}, 30)
272+
test_composition({"Ge": 1, "O": 2}, 48)
273+
test_composition({"H": 2, "O": 1}, 10)
274+
test_composition({"C": 1}, 6)
275+
276+
def test_calculate_effective_form_factor(self):
277+
q = np.linspace(0, 10, 234)
278+
composition = {"Si": 1, "O": 2}
279+
effective_form_factor = calculate_effective_form_factor(composition, q)
280+
self.assertEqual(len(q), len(effective_form_factor))
281+
282+
effective_form_factor = calculate_effective_form_factor(
283+
composition, q, sf_source="brown_hubbell"
284+
)
285+
self.assertEqual(len(q), len(effective_form_factor))
286+
287+
effective_form_factor = calculate_effective_form_factor(
288+
composition, q, sf_source="xraylib"
289+
)
290+
self.assertEqual(len(q), len(effective_form_factor))
291+
292+
effective_form_factor = calculate_effective_form_factor(
293+
composition, 0, sf_source="hajdu"
294+
)
295+
self.assertAlmostEqual(effective_form_factor, 1.0, places=3)
296+
297+
def test_calculate_wkm_form_factor(self):
298+
q = 0.0
299+
composition = {"Si": 1, "O": 2}
300+
wkm_form_factor = calculate_wkm_effective_atomic_number(composition, "Si", q)
301+
self.assertAlmostEqual(wkm_form_factor, 14.005, places=2)
302+
wkm_form_factor = calculate_wkm_effective_atomic_number(composition, "O", q)
303+
self.assertAlmostEqual(wkm_form_factor, 7.9975, places=2)
304+
305+
def test_calculate_wkm_weighting_factor(self):
306+
q = 0.0
307+
composition = {"Si": 1, "O": 2}
308+
wkm_weighting_factor = calculate_weighting_factor_wkm(composition, "Si", "O", q)
309+
print(wkm_weighting_factor)

0 commit comments

Comments
 (0)