Skip to content

Commit e8572fa

Browse files
authored
Merge pull request #146 from yucongalicechen/mudcalc
finalize `mud_calculator.py`
2 parents 7f264d2 + bd1dd40 commit e8572fa

File tree

3 files changed

+81
-49
lines changed

3 files changed

+81
-49
lines changed

news/muD.rst

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
**Added:**
2+
3+
* no news added - minor edits in mud_calculator.py
4+
5+
**Changed:**
6+
7+
* <news item>
8+
9+
**Deprecated:**
10+
11+
* <news item>
12+
13+
**Removed:**
14+
15+
* <news item>
16+
17+
**Fixed:**
18+
19+
* <news item>
20+
21+
**Security:**
22+
23+
* <news item>

src/diffpy/labpdfproc/mud_calculator.py

+52-43
Original file line numberDiff line numberDiff line change
@@ -5,96 +5,105 @@
55
from diffpy.utils.parsers.loaddata import loadData
66

77

8-
def _top_hat(x, slit_width):
8+
def _top_hat(z, half_slit_width):
99
"""
10-
create a top-hat function, return 1.0 for values within the specified slit width and 0 otherwise
10+
Create a top-hat function, return 1.0 for values within the specified slit width and 0 otherwise
1111
"""
12-
return np.where((x >= -slit_width) & (x <= slit_width), 1.0, 0)
12+
return np.where((z >= -half_slit_width) & (z <= half_slit_width), 1.0, 0.0)
1313

1414

15-
def _model_function(x, diameter, x0, I0, mud, slope):
15+
def _model_function(z, diameter, z0, I0, mud, slope):
1616
"""
17-
compute the model function with the following steps:
18-
1. Recenter x to h by subtracting x0 (so that the circle is centered at 0 and it is easier to compute length l)
17+
Compute the model function with the following steps:
18+
1. Recenter z to x by subtracting z0 (so that the circle is centered at 0 and it is easier to compute length l)
1919
2. Compute length l that is the effective length for computing intensity I = I0 * e^{-mu * l}:
20-
- For h within the diameter range, l is the chord length of the circle at position h
21-
- For h outside this range, l = 0
22-
3. Apply a linear adjustment to I0 by taking I0 as I0 - slope * x
20+
- For x within the diameter range, l is the chord length of the circle at position x
21+
- For x outside this range, l = 0
22+
3. Apply a linear adjustment to I0 by taking I0 as I0 - slope * z
2323
"""
2424
min_radius = -diameter / 2
2525
max_radius = diameter / 2
26-
h = x - x0
26+
x = z - z0
2727
length = np.piecewise(
28-
h,
29-
[h < min_radius, (min_radius <= h) & (h <= max_radius), h > max_radius],
30-
[0, lambda h: 2 * np.sqrt((diameter / 2) ** 2 - h**2), 0],
28+
x,
29+
[x < min_radius, (min_radius <= x) & (x <= max_radius), x > max_radius],
30+
[0, lambda x: 2 * np.sqrt((diameter / 2) ** 2 - x**2), 0],
3131
)
32-
return (I0 - slope * x) * np.exp(-mud / diameter * length)
32+
return (I0 - slope * z) * np.exp(-mud / diameter * length)
3333

3434

35-
def _extend_x_and_convolve(x, diameter, slit_width, x0, I0, mud, slope):
35+
def _extend_z_and_convolve(z, diameter, half_slit_width, z0, I0, mud, slope):
3636
"""
37-
extend x values and I values for padding (so that we don't have tails in convolution), then perform convolution
37+
extend z values and I values for padding (so that we don't have tails in convolution), then perform convolution
3838
(note that the convolved I values are the same as modeled I values if slit width is close to 0)
3939
"""
40-
n_points = len(x)
41-
x_left_pad = np.linspace(x.min() - n_points * (x[1] - x[0]), x.min(), n_points)
42-
x_right_pad = np.linspace(x.max(), x.max() + n_points * (x[1] - x[0]), n_points)
43-
x_extended = np.concatenate([x_left_pad, x, x_right_pad])
44-
I_extended = _model_function(x_extended, diameter, x0, I0, mud, slope)
45-
kernel = _top_hat(x_extended - x_extended.mean(), slit_width)
40+
n_points = len(z)
41+
z_left_pad = np.linspace(z.min() - n_points * (z[1] - z[0]), z.min(), n_points)
42+
z_right_pad = np.linspace(z.max(), z.max() + n_points * (z[1] - z[0]), n_points)
43+
z_extended = np.concatenate([z_left_pad, z, z_right_pad])
44+
I_extended = _model_function(z_extended, diameter, z0, I0, mud, slope)
45+
kernel = _top_hat(z_extended - z_extended.mean(), half_slit_width)
4646
I_convolved = I_extended # this takes care of the case where slit width is close to 0
4747
if kernel.sum() != 0:
4848
kernel /= kernel.sum()
4949
I_convolved = convolve(I_extended, kernel, mode="same")
50-
padding_length = len(x_left_pad)
50+
padding_length = len(z_left_pad)
5151
return I_convolved[padding_length:-padding_length]
5252

5353

54-
def _objective_function(params, x, observed_data):
54+
def _objective_function(params, z, observed_data):
5555
"""
56-
compute the objective function for fitting a model to the observed/experimental data
56+
Compute the objective function for fitting a model to the observed/experimental data
5757
by minimizing the sum of squared residuals between the observed data and the convolved model data
5858
"""
59-
diameter, slit_width, x0, I0, mud, slope = params
60-
convolved_model_data = _extend_x_and_convolve(x, diameter, slit_width, x0, I0, mud, slope)
59+
diameter, half_slit_width, z0, I0, mud, slope = params
60+
convolved_model_data = _extend_z_and_convolve(z, diameter, half_slit_width, z0, I0, mud, slope)
6161
residuals = observed_data - convolved_model_data
6262
return np.sum(residuals**2)
6363

6464

65-
def _compute_single_mud(x_data, I_data):
65+
def _compute_single_mud(z_data, I_data):
6666
"""
67-
perform dual annealing optimization and extract the parameters
67+
Perform dual annealing optimization and extract the parameters
6868
"""
6969
bounds = [
70-
(1e-5, x_data.max() - x_data.min()), # diameter: [small positive value, upper bound]
71-
(0, (x_data.max() - x_data.min()) / 2), # slit width: [0, upper bound]
72-
(x_data.min(), x_data.max()), # x0: [min x, max x]
70+
(1e-5, z_data.max() - z_data.min()), # diameter: [small positive value, upper bound]
71+
(0, (z_data.max() - z_data.min()) / 2), # half slit width: [0, upper bound]
72+
(z_data.min(), z_data.max()), # z0: [min z, max z]
7373
(1e-5, I_data.max()), # I0: [small positive value, max observed intensity]
7474
(1e-5, 20), # muD: [small positive value, upper bound]
75-
(-10000, 10000), # slope: [lower bound, upper bound]
75+
(-100000, 100000), # slope: [lower bound, upper bound]
7676
]
77-
result = dual_annealing(_objective_function, bounds, args=(x_data, I_data))
78-
diameter, slit_width, x0, I0, mud, slope = result.x
79-
convolved_fitted_signal = _extend_x_and_convolve(x_data, diameter, slit_width, x0, I0, mud, slope)
77+
result = dual_annealing(_objective_function, bounds, args=(z_data, I_data))
78+
diameter, half_slit_width, z0, I0, mud, slope = result.x
79+
convolved_fitted_signal = _extend_z_and_convolve(z_data, diameter, half_slit_width, z0, I0, mud, slope)
8080
residuals = I_data - convolved_fitted_signal
8181
rmse = np.sqrt(np.mean(residuals**2))
8282
return mud, rmse
8383

8484

8585
def compute_mud(filepath):
86-
"""
87-
compute the best-fit mu*D value from a z-scan file
86+
"""Compute the best-fit mu*D value from a z-scan file, removing the sample holder effect.
87+
88+
This function loads z-scan data and fits it to a model
89+
that convolves a top-hat function with I = I0 * e^{-mu * l}.
90+
The fitting procedure is run multiple times, and we return the best-fit parameters based on the lowest rmse.
91+
92+
The full mathematical details are described in the paper:
93+
An ad hoc Absorption Correction for Reliable Pair-Distribution Functions from Low Energy x-ray Sources,
94+
Yucong Chen, Till Schertenleib, Andrew Yang, Pascal Schouwink, Wendy L. Queen and Simon J. L. Billinge,
95+
in preparation.
8896
8997
Parameters
9098
----------
91-
filepath str
92-
the path to the z-scan file
99+
filepath : str
100+
The path to the z-scan file.
93101
94102
Returns
95103
-------
96-
a float contains the best-fit mu*D value
104+
mu*D : float
105+
The best-fit mu*D value.
97106
"""
98-
x_data, I_data = loadData(filepath, unpack=True)
99-
best_mud, _ = min((_compute_single_mud(x_data, I_data) for _ in range(10)), key=lambda pair: pair[1])
107+
z_data, I_data = loadData(filepath, unpack=True)
108+
best_mud, _ = min((_compute_single_mud(z_data, I_data) for _ in range(20)), key=lambda pair: pair[1])
100109
return best_mud

tests/test_mud_calculator.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,20 @@
33
import numpy as np
44
import pytest
55

6-
from diffpy.labpdfproc.mud_calculator import _extend_x_and_convolve, compute_mud
6+
from diffpy.labpdfproc.mud_calculator import _extend_z_and_convolve, compute_mud
77

88

99
def test_compute_mud(tmp_path):
10-
diameter, slit_width, x0, I0, mud, slope = 1, 0.1, 0, 1e5, 3, 0
11-
x_data = np.linspace(-1, 1, 50)
12-
convolved_I_data = _extend_x_and_convolve(x_data, diameter, slit_width, x0, I0, mud, slope)
10+
diameter, slit_width, z0, I0, mud, slope = 1, 0.1, 0, 1e5, 3, 0
11+
z_data = np.linspace(-1, 1, 50)
12+
convolved_I_data = _extend_z_and_convolve(z_data, diameter, slit_width, z0, I0, mud, slope)
1313

1414
directory = Path(tmp_path)
1515
file = directory / "testfile"
1616
with open(file, "w") as f:
17-
for x, I in zip(x_data, convolved_I_data):
17+
for x, I in zip(z_data, convolved_I_data):
1818
f.write(f"{x}\t{I}\n")
1919

2020
expected_mud = 3
2121
actual_mud = compute_mud(file)
22-
assert actual_mud == pytest.approx(expected_mud, rel=1e-4, abs=0.1)
22+
assert actual_mud == pytest.approx(expected_mud, rel=0.01, abs=0.1)

0 commit comments

Comments
 (0)