|
5 | 5 | from diffpy.utils.parsers.loaddata import loadData
|
6 | 6 |
|
7 | 7 |
|
8 |
| -def _top_hat(x, slit_width): |
| 8 | +def _top_hat(z, half_slit_width): |
9 | 9 | """
|
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 |
11 | 11 | """
|
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) |
13 | 13 |
|
14 | 14 |
|
15 |
| -def _model_function(x, diameter, x0, I0, mud, slope): |
| 15 | +def _model_function(z, diameter, z0, I0, mud, slope): |
16 | 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) |
| 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) |
19 | 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 |
| 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 |
23 | 23 | """
|
24 | 24 | min_radius = -diameter / 2
|
25 | 25 | max_radius = diameter / 2
|
26 |
| - h = x - x0 |
| 26 | + x = z - z0 |
27 | 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], |
| 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], |
31 | 31 | )
|
32 |
| - return (I0 - slope * x) * np.exp(-mud / diameter * length) |
| 32 | + return (I0 - slope * z) * np.exp(-mud / diameter * length) |
33 | 33 |
|
34 | 34 |
|
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): |
36 | 36 | """
|
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 |
38 | 38 | (note that the convolved I values are the same as modeled I values if slit width is close to 0)
|
39 | 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) |
| 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) |
46 | 46 | I_convolved = I_extended # this takes care of the case where slit width is close to 0
|
47 | 47 | if kernel.sum() != 0:
|
48 | 48 | kernel /= kernel.sum()
|
49 | 49 | I_convolved = convolve(I_extended, kernel, mode="same")
|
50 |
| - padding_length = len(x_left_pad) |
| 50 | + padding_length = len(z_left_pad) |
51 | 51 | return I_convolved[padding_length:-padding_length]
|
52 | 52 |
|
53 | 53 |
|
54 |
| -def _objective_function(params, x, observed_data): |
| 54 | +def _objective_function(params, z, observed_data): |
55 | 55 | """
|
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 |
57 | 57 | by minimizing the sum of squared residuals between the observed data and the convolved model data
|
58 | 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) |
| 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) |
61 | 61 | residuals = observed_data - convolved_model_data
|
62 | 62 | return np.sum(residuals**2)
|
63 | 63 |
|
64 | 64 |
|
65 |
| -def _compute_single_mud(x_data, I_data): |
| 65 | +def _compute_single_mud(z_data, I_data): |
66 | 66 | """
|
67 |
| - perform dual annealing optimization and extract the parameters |
| 67 | + Perform dual annealing optimization and extract the parameters |
68 | 68 | """
|
69 | 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] |
| 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] |
73 | 73 | (1e-5, I_data.max()), # I0: [small positive value, max observed intensity]
|
74 | 74 | (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] |
76 | 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) |
| 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) |
80 | 80 | residuals = I_data - convolved_fitted_signal
|
81 | 81 | rmse = np.sqrt(np.mean(residuals**2))
|
82 | 82 | return mud, rmse
|
83 | 83 |
|
84 | 84 |
|
85 | 85 | 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. |
88 | 96 |
|
89 | 97 | Parameters
|
90 | 98 | ----------
|
91 |
| - filepath str |
92 |
| - the path to the z-scan file |
| 99 | + filepath : str |
| 100 | + The path to the z-scan file. |
93 | 101 |
|
94 | 102 | Returns
|
95 | 103 | -------
|
96 |
| - a float contains the best-fit mu*D value |
| 104 | + mu*D : float |
| 105 | + The best-fit mu*D value. |
97 | 106 | """
|
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]) |
100 | 109 | return best_mud
|
0 commit comments