|
| 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