Skip to content

Commit 1e6ed7c

Browse files
add new module for computing mud
1 parent 5242805 commit 1e6ed7c

File tree

1 file changed

+100
-0
lines changed

1 file changed

+100
-0
lines changed
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import numpy as np
2+
from scipy.optimize import dual_annealing
3+
from scipy.signal import convolve
4+
5+
from diffpy.utils.parsers.loaddata import loadData
6+
7+
8+
def _top_hat(x, slit_width):
9+
"""
10+
create a top-hat function, return 1.0 for values within the specified slit width and 0 otherwise
11+
"""
12+
return np.where((x >= -slit_width) & (x <= slit_width), 1.0, 0)
13+
14+
15+
def _model_function(x, diameter, x0, I0, mud, slope):
16+
"""
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)
19+
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
23+
"""
24+
min_radius = -diameter / 2
25+
max_radius = diameter / 2
26+
h = x - x0
27+
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],
31+
)
32+
return (I0 - slope * x) * np.exp(-mud / diameter * length)
33+
34+
35+
def _extend_x_and_convolve(x, diameter, slit_width, x0, I0, mud, slope):
36+
"""
37+
extend x values and I values for padding (so that we don't have tails in convolution), then perform convolution
38+
(note that the convolved I values are the same as modeled I values if slit width is close to 0)
39+
"""
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)
46+
I_convolved = I_extended # this takes care of the case where slit width is close to 0
47+
if kernel.sum() != 0:
48+
kernel /= kernel.sum()
49+
I_convolved = convolve(I_extended, kernel, mode="same")
50+
padding_length = len(x_left_pad)
51+
return I_convolved[padding_length:-padding_length]
52+
53+
54+
def _objective_function(params, x, observed_data):
55+
"""
56+
compute the objective function for fitting a model to the observed/experimental data
57+
by minimizing the sum of squared residuals between the observed data and the convolved model data
58+
"""
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)
61+
residuals = observed_data - convolved_model_data
62+
return np.sum(residuals**2)
63+
64+
65+
def _compute_single_mud(x_data, I_data):
66+
"""
67+
perform dual annealing optimization and extract the parameters
68+
"""
69+
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]
73+
(1e-5, I_data.max()), # I0: [small positive value, max observed intensity]
74+
(1e-5, 20), # muD: [small positive value, upper bound]
75+
(-10000, 10000), # slope: [lower bound, upper bound]
76+
]
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)
80+
residuals = I_data - convolved_fitted_signal
81+
rmse = np.sqrt(np.mean(residuals**2))
82+
return mud, rmse
83+
84+
85+
def compute_mud(filepath):
86+
"""
87+
compute the best-fit mu*D value from a z-scan file
88+
89+
Parameters
90+
----------
91+
filepath str
92+
the path to the z-scan file
93+
94+
Returns
95+
-------
96+
a float contains the best-fit mu*D value
97+
"""
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])
100+
return best_mud

0 commit comments

Comments
 (0)