From 38cdb713dedbe7bb63075cf8a3cf010fcba66e9f Mon Sep 17 00:00:00 2001 From: Joris Snellenburg Date: Thu, 30 Sep 2021 18:51:31 +0200 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor/calc=20doas=20wit?= =?UTF-8?q?h=20irf=20(#839)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * โ™ป๏ธ Start of refactor calculate_damped_oscillation_matrix_gaussian_irf ๐Ÿ”ง Add debug plotting code to evaluate doas matrix calculation Some temporary debug plotting code to evaluate the changes in implementation during the evolution of the code during the refactor. * โ™ป๏ธ๐Ÿ‘ŒFix for reversed oscillations (negative rates) To accommodate time-reversed oscillations the sqwidth constant is manipulated so that negative rates have a -1 sign causing a flip of the erf when this constant is used in the calculation of 'b'. Co-authored-by: Ivo Co-authored-by: Sebastian Weigand * โ™ป๏ธ Refactored calc doas matrix function to be more numerically stable Results of refactoring so far; 2 iterations, same stating values: 1. ๐Ÿค” Start - Starting cost: 6.3345e+04 - End cost: 6.3345e+04 - rmse: 9.75e-01 - variable `a` going up to 1e106 - variable `osc` going up to 1e106 2. ๐Ÿฉน Fix for reversed osciallations - Starting cost: 3.0537e+04 - End cost: 3.0006e+04 - rmse: 7.02e-01 - variable `osc` bounded to [-2, 2] - variable `a` going up to 1e106 3. ๐Ÿ‘Œ More numerically stable and efficient calculation of a and b - Starting cost: 3.0537e+04 - End cost: 3.0006e+04 - rmse: 7.02e-01 - variable `osc` bounded to [-2, 2] - variable `a` bounded to [-1.5, 1.1] * ๐Ÿงน Clean up debug plotting code * ๐Ÿ“š Add comment explaining origin of 0.03 scaling factor Used in frequency conversion * ๐Ÿ“š Add docs for calculate_damped_oscillation_matrix_gaussian_irf * ๐Ÿ‘Œ Fix typo parameter, parameter, parameter * ๐Ÿ“š Improved code comments * โ™ป๏ธ Refactored by Sourcery This code wasn't touched by this PR, but will clean up the code for the future. Co-authored-by: Ivo Co-authored-by: Sebastian Weigand Co-authored-by: Sourcery AI <> --- .../damped_oscillation_megacomplex.py | 119 +++++++++++++----- 1 file changed, 87 insertions(+), 32 deletions(-) diff --git a/glotaran/builtin/megacomplexes/damped_oscillation/damped_oscillation_megacomplex.py b/glotaran/builtin/megacomplexes/damped_oscillation/damped_oscillation_megacomplex.py index d57996a6f..ba9cd3dbb 100644 --- a/glotaran/builtin/megacomplexes/damped_oscillation/damped_oscillation_megacomplex.py +++ b/glotaran/builtin/megacomplexes/damped_oscillation/damped_oscillation_megacomplex.py @@ -31,7 +31,7 @@ ) class DampedOscillationMegacomplex(Megacomplex): @model_item_validator(False) - def ensure_oscillation_paramater(self, model: Model) -> list[str]: + def ensure_oscillation_parameter(self, model: Model) -> list[str]: problems = [] @@ -58,6 +58,8 @@ def calculate_matrix( model_axis = dataset_model.get_model_axis() delta = np.abs(model_axis[1:] - model_axis[:-1]) delta_min = delta[np.argmin(delta)] + # c multiply by 0.03 to convert wavenumber (cm-1) to frequency (THz) + # where 0.03 is the product of speed of light 3*10**10 cm/s and time-unit ps (10^-12) frequency_max = 1 / (2 * 0.03 * delta_min) frequencies = np.array(self.frequencies) * 0.03 * 2 * np.pi frequencies[frequencies >= frequency_max] = np.mod( @@ -137,35 +139,34 @@ def finalize_data( phase, ) - if not is_full_model: - if self.index_dependent(dataset_model): - dataset[f"{prefix}_sin"] = ( - ( - dataset_model.get_global_dimension(), - dataset_model.get_model_dimension(), - prefix, - ), - dataset.matrix.sel(clp_label=[f"{label}_sin" for label in self.labels]).values, - ) + if self.index_dependent(dataset_model): + dataset[f"{prefix}_sin"] = ( + ( + dataset_model.get_global_dimension(), + dataset_model.get_model_dimension(), + prefix, + ), + dataset.matrix.sel(clp_label=[f"{label}_sin" for label in self.labels]).values, + ) - dataset[f"{prefix}_cos"] = ( - ( - dataset_model.get_global_dimension(), - dataset_model.get_model_dimension(), - prefix, - ), - dataset.matrix.sel(clp_label=[f"{label}_cos" for label in self.labels]).values, - ) - else: - dataset[f"{prefix}_sin"] = ( - (dataset_model.get_model_dimension(), prefix), - dataset.matrix.sel(clp_label=[f"{label}_sin" for label in self.labels]).values, - ) + dataset[f"{prefix}_cos"] = ( + ( + dataset_model.get_global_dimension(), + dataset_model.get_model_dimension(), + prefix, + ), + dataset.matrix.sel(clp_label=[f"{label}_cos" for label in self.labels]).values, + ) + else: + dataset[f"{prefix}_sin"] = ( + (dataset_model.get_model_dimension(), prefix), + dataset.matrix.sel(clp_label=[f"{label}_sin" for label in self.labels]).values, + ) - dataset[f"{prefix}_cos"] = ( - (dataset_model.get_model_dimension(), prefix), - dataset.matrix.sel(clp_label=[f"{label}_cos" for label in self.labels]).values, - ) + dataset[f"{prefix}_cos"] = ( + (dataset_model.get_model_dimension(), prefix), + dataset.matrix.sel(clp_label=[f"{label}_cos" for label in self.labels]).values, + ) @nb.jit(nopython=True, parallel=True) @@ -188,14 +189,68 @@ def calculate_damped_oscillation_matrix_gaussian_irf( shift: float, scale: float, ): + """Calculate the damped oscillation matrix taking into account a gaussian irf + + Parameters + ---------- + frequencies : np.ndarray + an array of frequencies in THz, one per oscillation + rates : np.ndarray + an array of rates, one per oscillation + model_axis : np.ndarray + the model axis (time) + center : float + the center of the gaussian IRF + width : float + the width (ฯƒ) parameter of the the IRF + shift : float + a shift parameter per item on the global axis + scale : float + the scale parameter to scale the matrix by + + Returns + ------- + np.ndarray + An array of the real and imaginary part of the oscillation matrix, + the shape being (len(model_axis), 2*len(frequencies)), with the first + half of the second dimension representing the real part, + and the other the imagine part of the oscillation + """ shifted_axis = model_axis - center - shift + # For calculations using the negative rates we use the time axis + # from the beginning up to 5 ฯƒ from the irf center + left_shifted_axis_indices = np.where(shifted_axis < 5 * width)[0] + left_shifted_axis = shifted_axis[left_shifted_axis_indices] + neg_idx = np.where(rates < 0)[0] + # For calculations using the positive rates axis we use the time axis + # from 5 ฯƒ before the irf center until the end + right_shifted_axis_indices = np.where(shifted_axis > -5 * width)[0] + right_shifted_axis = shifted_axis[right_shifted_axis_indices] + pos_idx = np.where(rates >= 0)[0] + d = width ** 2 k = rates + 1j * frequencies dk = k * d sqwidth = np.sqrt(2) * width - a = (-1 * shifted_axis[:, None] + 0.5 * dk) * k - a = np.minimum(a, 709) - a = np.exp(a) - b = 1 + erf((shifted_axis[:, None] - dk) / sqwidth) + + a = np.zeros((len(model_axis), len(rates)), dtype=np.complex128) + a[np.ix_(right_shifted_axis_indices, pos_idx)] = np.exp( + (-1 * right_shifted_axis[:, None] + 0.5 * dk[pos_idx]) * k[pos_idx] + ) + + a[np.ix_(left_shifted_axis_indices, neg_idx)] = np.exp( + (-1 * left_shifted_axis[:, None] + 0.5 * dk[neg_idx]) * k[neg_idx] + ) + + b = np.zeros((len(model_axis), len(rates)), dtype=np.complex128) + b[np.ix_(right_shifted_axis_indices, pos_idx)] = 1 + erf( + (right_shifted_axis[:, None] - dk[pos_idx]) / sqwidth + ) + # For negative rates we flip the sign of the `erf` by using `-sqwidth` in lieu of `sqwidth` + b[np.ix_(left_shifted_axis_indices, neg_idx)] = 1 + erf( + (left_shifted_axis[:, None] - dk[neg_idx]) / -sqwidth + ) + osc = a * b * scale + return np.concatenate((osc.real, osc.imag), axis=1)