From 46300d662cb3ad1fc1c9729e5f7a9107535996c0 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Tue, 3 Feb 2026 10:46:07 +0100 Subject: [PATCH 01/17] initial analysis class --- docs/docs/tutorials/analysis.ipynb | 173 ++++++ pixi.lock | 2 +- src/easydynamics/analysis/__init__.py | 8 + src/easydynamics/analysis/analysis.py | 460 ++++++++++++++++ src/easydynamics/analysis/analysis1d.py | 498 ++++++++++++++++++ .../convolution/convolution_base.py | 4 + src/easydynamics/sample_model/__init__.py | 6 + 7 files changed, 1150 insertions(+), 1 deletion(-) create mode 100644 docs/docs/tutorials/analysis.ipynb create mode 100644 src/easydynamics/analysis/__init__.py create mode 100644 src/easydynamics/analysis/analysis.py create mode 100644 src/easydynamics/analysis/analysis1d.py diff --git a/docs/docs/tutorials/analysis.ipynb b/docs/docs/tutorials/analysis.ipynb new file mode 100644 index 00000000..7b843acc --- /dev/null +++ b/docs/docs/tutorials/analysis.ipynb @@ -0,0 +1,173 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "8643b10c", + "metadata": {}, + "source": [ + "asd" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bca91d3c", + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "from easydynamics.analysis.analysis1d import Analysis1d\n", + "from easydynamics.experiment import Experiment\n", + "from easydynamics.sample_model import ComponentCollection\n", + "from easydynamics.sample_model import DeltaFunction\n", + "from easydynamics.sample_model import Gaussian\n", + "from easydynamics.sample_model import Polynomial\n", + "from easydynamics.sample_model.background_model import BackgroundModel\n", + "from easydynamics.sample_model.resolution_model import ResolutionModel\n", + "from easydynamics.sample_model.sample_model import SampleModel\n", + "\n", + "%matplotlib widget" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8deca9b6", + "metadata": {}, + "outputs": [], + "source": [ + "vanadium_experiment = Experiment('Vanadium')\n", + "vanadium_experiment.load_hdf5(filename='vanadium_data_example.h5')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "41f842f0", + "metadata": {}, + "outputs": [], + "source": [ + "# Create a diffusion_model and components for the SampleModel\n", + "\n", + "# Creating components\n", + "component_collection = ComponentCollection()\n", + "delta_function = DeltaFunction(display_name='DeltaFunction', area=1)\n", + "gaussian = Gaussian(display_name='Gaussian', width=0.1, center=0.5, area=0.5)\n", + "\n", + "# Adding components to the component collection\n", + "component_collection.append_component(delta_function)\n", + "\n", + "\n", + "sample_model = SampleModel(\n", + " components=component_collection,\n", + " unit='meV',\n", + " display_name='MySampleModel',\n", + ")\n", + "\n", + "res_gauss = Gaussian(width=0.1)\n", + "res_gauss.area.fixed = True\n", + "resolution_model = ResolutionModel(components=res_gauss)\n", + "\n", + "\n", + "background_model = BackgroundModel(components=Polynomial(coefficients=[0.001]))\n", + "\n", + "my_analysis = Analysis1d(\n", + " experiment=vanadium_experiment,\n", + " sample_model=sample_model,\n", + " resolution_model=resolution_model,\n", + " background_model=background_model,\n", + " Q_index=5,\n", + ")\n", + "\n", + "my_analysis._update_models()\n", + "\n", + "\n", + "values = my_analysis.calculate()\n", + "sample_values, background_values = my_analysis.calculate_individual_components()\n", + "\n", + "plt.figure()\n", + "plt.plot(my_analysis.energy.values, values, label='Total Model')\n", + "for component_index in range(len(sample_values)):\n", + " plt.plot(\n", + " my_analysis.energy.values,\n", + " sample_values[component_index],\n", + " label=f'Sample Component {component_index}',\n", + " linestyle='--',\n", + " )\n", + "\n", + "for component_index in range(len(background_values)):\n", + " plt.plot(\n", + " my_analysis.energy.values,\n", + " background_values[component_index],\n", + " label=f'Background Component {component_index}',\n", + " linestyle=':',\n", + " )\n", + "plt.xlabel('Energy (meV)')\n", + "plt.ylabel('Intensity')\n", + "plt.title(f'Q index: {5}')\n", + "plt.legend()\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6762faba", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "02702f95", + "metadata": {}, + "outputs": [], + "source": [ + "my_analysis.plot_data_and_model()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "70091539", + "metadata": {}, + "outputs": [], + "source": [ + "my_analysis.fit()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2ad6384e", + "metadata": {}, + "outputs": [], + "source": [ + "my_analysis.plot_data_and_model()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "easydynamics_newbase", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/pixi.lock b/pixi.lock index d9461637..da8aee45 100644 --- a/pixi.lock +++ b/pixi.lock @@ -4091,7 +4091,7 @@ packages: requires_python: '>=3.5' - pypi: ./ name: easydynamics - version: 0.1.0+devdirty7 + version: 0.1.1+devdirty2 sha256: de299c914d4a865b9e2fdefa5e3947f37b1f26f73ff9087f7918ee417f3dd288 requires_dist: - darkdetect diff --git a/src/easydynamics/analysis/__init__.py b/src/easydynamics/analysis/__init__.py new file mode 100644 index 00000000..4cb511b4 --- /dev/null +++ b/src/easydynamics/analysis/__init__.py @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: 2025-2026 EasyDynamics contributors +# SPDX-License-Identifier: BSD-3-Clause + +from .analysis import Analysis + +__all__ = [ + 'Analysis', +] diff --git a/src/easydynamics/analysis/analysis.py b/src/easydynamics/analysis/analysis.py new file mode 100644 index 00000000..33d23545 --- /dev/null +++ b/src/easydynamics/analysis/analysis.py @@ -0,0 +1,460 @@ +# SPDX-FileCopyrightText: 2025-2026 EasyDynamics contributors +# SPDX-License-Identifier: BSD-3-Clause + + +import numpy as np +import plopp as pp +import scipp as sc +from easyscience.base_classes.model_base import ModelBase as EasyScienceModelBase +from easyscience.fitting.fitter import Fitter as EasyScienceFitter +from easyscience.variable import Parameter + +from easydynamics.convolution import Convolution +from easydynamics.experiment import Experiment +from easydynamics.sample_model import BackgroundModel +from easydynamics.sample_model import ResolutionModel +from easydynamics.sample_model import SampleModel + + +class Analysis(EasyScienceModelBase): + """For analysing data.""" + + def __init__( + self, + display_name: str = 'MyAnalysis', + unique_name: str | None = None, + experiment: Experiment | None = None, + sample_model: SampleModel | None = None, + resolution_model: ResolutionModel | None = None, + background_model: BackgroundModel | None = None, + energy_offset: None = None, + ): + + super().__init__(display_name=display_name, unique_name=unique_name) + + if experiment is not None and not isinstance(experiment, Experiment): + raise TypeError('experiment must be an instance of Experiment or None.') + + self._experiment = experiment + + if sample_model is not None and not isinstance(sample_model, SampleModel): + raise TypeError('sample_model must be an instance of SampleModel or None.') + sample_model.Q = self.Q + self._sample_model = sample_model + + if resolution_model is not None and not isinstance(resolution_model, ResolutionModel): + raise TypeError('resolution_model must be an instance of ResolutionModel or None.') + resolution_model.Q = self.Q + self._resolution_model = resolution_model + + if background_model is not None and not isinstance(background_model, BackgroundModel): + raise TypeError('background_model must be an instance of BackgroundModel or None.') + background_model.Q = self.Q + self._background_model = background_model + + self._convolvers = [None] * (len(self.Q) if self.Q is not None else 0) + self._update_models() + + ############# + # Properties + ############# + + @property + def experiment(self) -> Experiment | None: + """The Experiment associated with this Analysis.""" + return self._experiment + + @experiment.setter + def experiment(self, value: Experiment | None) -> None: + if value is not None and not isinstance(value, Experiment): + raise TypeError('experiment must be an instance of Experiment or None.') + self._experiment = value + self._update_models() + + @property + def sample_model(self) -> SampleModel | None: + """The SampleModel associated with this Analysis.""" + return self._sample_model + + @sample_model.setter + def sample_model(self, value: SampleModel | None) -> None: + if value is not None and not isinstance(value, SampleModel): + raise TypeError('sample_model must be an instance of SampleModel or None.') + self._sample_model = value + self._update_models() + + @property + def resolution_model(self) -> ResolutionModel | None: + """The ResolutionModel associated with this Analysis.""" + return self._resolution_model + + @resolution_model.setter + def resolution_model(self, value: ResolutionModel | None) -> None: + if value is not None and not isinstance(value, ResolutionModel): + raise TypeError('resolution_model must be an instance of ResolutionModel or None.') + self._resolution_model = value + self._update_models() + + @property + def background_model(self) -> BackgroundModel | None: + """The BackgroundModel associated with this Analysis.""" + return self._background_model + + @background_model.setter + def background_model(self, value: BackgroundModel | None) -> None: + if value is not None and not isinstance(value, BackgroundModel): + raise TypeError('background_model must be an instance of BackgroundModel or None.') + self._background_model = value + self._update_models() + + @property + def Q(self) -> sc.Variable | None: + """The Q values from the associated Experiment, if available.""" + if self.experiment is not None: + return self.experiment.Q + return None + + @Q.setter + def Q(self, value) -> None: + """Q is a read-only property derived from the Experiment.""" + raise AttributeError('Q is a read-only property derived from the Experiment.') + + @property + def energy(self) -> sc.Variable | None: + """The energy values from the associated Experiment, if + available. + """ + if self.experiment is not None: + return self.experiment.energy + return None + + @energy.setter + def energy(self, value) -> None: + """Energy is a read-only property derived from the + Experiment. + """ + raise AttributeError('energy is a read-only property derived from the Experiment.') + + # TODO: make it use experiment temperature + @property + def temperature(self) -> Parameter | None: + """The temperature from the associated Experiment, if + available. + """ + return None + + @temperature.setter + def temperature(self, value) -> None: + """Temperature is a read-only property derived from the + Experiment. + """ + raise AttributeError('temperature is a read-only property derived from the Experiment.') + + # # TODO: make it use experiment temperature + # @property def temperature(self) -> Parameter | None: """The + # temperature from the associated Experiment, if available.""" if + # self.experiment is not None: return + # self.experiment.temperature return None + + # @temperature.setter def temperature(self, value) -> None: + # """temperature is a read-only property derived from the + # Experiment.""" raise AttributeError( "temperature is a + # read-only property derived from the Experiment." ) + + ############# + # Other methods + ############# + + def calculate(self, energy: float | None, Q_index: int) -> np.ndarray: + """Calculate the model prediction for a given Q index. + + Args: + energy (float): The energy value to calculate the model for. + Q_index (int): The index of the Q value to calculate the + model for. + Returns: + sc.DataArray: The calculated model prediction. + """ + if energy is None: + energy = self.energy + + if self.sample_model is None: + sample_intensity = np.zeros_like(energy) + else: + if self.resolution_model is None: + sample_intensity = self.sample_model._component_collections[Q_index].evaluate( + energy + ) + else: + convolver = self._create_convolver(Q_index) + sample_intensity = convolver.convolution() + + if self.background_model is None: + background_intensity = np.zeros_like(energy) + else: + background_intensity = self.background_model._component_collections[Q_index].evaluate( + energy + ) + + sample_plus_background = sample_intensity + background_intensity + + return sample_plus_background + + def calculate_individual_components( + self, Q_index: int + ) -> tuple[list[np.ndarray], list[np.ndarray]]: + """Calculate the model prediction for a given Q index for each + individual component. + + Args: + Q_index (int): The index of the Q value to calculate the + model for. + Returns: + list[np.ndarray]: The calculated model predictions for each + individual component. + """ + sample_results = [] + background_results = [] + + if self.sample_model is not None: + # Calculate sample components + for component in self.sample_model._component_collections[Q_index]._components: + if self.resolution_model is None: + component_intensity = component.evaluate(self.energy) + else: + convolver = Convolution( + sample_components=component, + resolution_components=self.resolution_model._component_collections[ + Q_index + ], + energy=self.energy, + temperature=self.temperature, + ) + component_intensity = convolver.convolution() + sample_results.append(component_intensity) + + if self.background_model is not None: + # Calculate background components + for component in self.background_model._component_collections[Q_index]._components: + component_intensity = component.evaluate(self.energy) + background_results.append(component_intensity) + + return sample_results, background_results + + def calculate_all_Q(self) -> list[np.ndarray]: + """Calculate the model prediction for all Q indices. + + Returns: + list[np.ndarray]: The calculated model predictions for all Q + indices. + """ + results = [] + for Q_index in range(len(self.Q)): + result = self.calculate(Q_index) + results.append(result) + return results + + # def calculate_individual_components_all_Q( + # self, + # add_background: bool = True, + # ) -> list[tuple[list[np.ndarray], list[np.ndarray]]]: + # """Calculate the model prediction for all Q indices for each + # individual component. + + # Returns: list[tuple[list[np.ndarray], list[np.ndarray]]]: The + # calculated model predictions for each individual component + # at all Q indices. """ all_results = [] for Q_index in + # range(len(self.Q)): sample_results, background_results = + # self.calculate_individual_components( Q_index ) if + # add_background: sample_results = sample_results + + # background_results all_results.append((sample_results, + # background_results)) return all_results + + def calculate_single_component_all_Q( + self, + component_index: int, + ) -> list[np.ndarray]: + """Calculate the model prediction for all Q indices for a single + component. + + Args: + component_index (int): The index of the component + Returns: + list[np.ndarray]: The calculated model predictions for the + specified component at all Q indices. + """ + + results = [] + for Q_index in range(len(self.Q)): + if self.sample_model is not None: + component = self.sample_model._component_collections[Q_index]._components[ + component_index + ] + if self.resolution_model is None: + component_intensity = component.evaluate(self.energy) + else: + convolver = Convolution( + sample_components=component, + resolution_components=self.resolution_model._component_collections[ + Q_index + ], + energy=self.energy, + temperature=self.temperature, + ) + component_intensity = convolver.convolution() + results.append(component_intensity) + else: + results.append(np.zeros_like(self.energy)) + + model_data_array = sc.DataArray( + data=sc.array(dims=['Q', 'energy'], values=results), + coords={ + 'Q': self.Q, + 'energy': self.energy, + }, + ) + return model_data_array + + def fit(self, Q_index: int): + """Fit the model to the experimental data for a given Q index. + + Args: + Q_index (int): The index of the Q value to fit the model + to. + Returns: + FitResult: The result of the fit. + """ + if self._experiment is None: + raise ValueError('No experiment is associated with this Analysis.') + + if not isinstance(Q_index, int) or Q_index < 0 or Q_index >= len(self.Q): + raise ValueError('Q_index must be a valid index for the Q values.') + + data = self.experiment.data['Q', Q_index] + x = data.coords['energy'].values + y = data.values + e = data.variances**0.5 + + def fit_func(x_vals): + return self.calculate_theory(energy=x_vals, Q_index=Q_index) + + fitter = EasyScienceFitter( + fit_object=self, + fit_function=fit_func, + ) + + # Perform the fit + fit_result = fitter.fit(x=x, y=y, weights=1.0 / e) + + # Store result + self.fit_result = fit_result + + return fit_result + + def plot_data_and_model( + self, + plot_individual_components: bool = True, + ) -> None: + """Plot the experimental data and the model prediction. + + Args: + plot_individual_components (bool): Whether to plot + individual components. Default is True. + """ + if not isinstance(plot_individual_components, bool): + raise TypeError('plot_individual_components must be True or False.') + + model_data_array = self._create_model_data_group( + individual_components=plot_individual_components + ) + if self.experiment is None or self.experiment.data is None: + raise ValueError('Experiment data is not available for plotting.') + + from IPython.display import display + + fig = pp.slicer( + {'Data': self.experiment.data, 'Model': model_data_array}, + color={'Data': 'black', 'Model': 'red'}, + linestyle={'Data': 'none', 'Model': 'solid'}, + marker={'Data': 'o', 'Model': 'None'}, + ) + display(fig) + + ############# + # Private methods + ############# + + def _update_models(self): + """Update models based on the current experiment.""" + if self.experiment is None: + return + + for Q_index in range(len(self.Q)): + self._convolvers[Q_index] = self._create_convolver(Q_index) + + def _create_convolver(self, Q_index: int): + """Initialize and return a Convolution object for the given Q + index. + """ + # Add checks of empty sample models etc + + sample_components = self.sample_model._component_collections[Q_index] + resolution_components = self.resolution_model._component_collections[Q_index] + energy = self.energy + convolver = Convolution( + sample_components=sample_components, + resolution_components=resolution_components, + energy=energy, + temperature=self.temperature, + ) + return convolver + + def _create_model_data_group(self, individual_components=True) -> sc.DataArray: + """Create a Scipp DataArray representing the model over all Q + and energy values. + """ + if self.Q is None or self.energy is None: + raise ValueError('Q and energy must be defined in the experiment.') + + model_data = [] + for Q_index in range(len(self.Q)): + model_at_Q = self.calculate(Q_index) + model_data.append(model_at_Q) + + model_data_array = sc.DataArray( + data=sc.array(dims=['Q', 'energy'], values=model_data), + coords={ + 'Q': self.Q, + 'energy': self.energy, + }, + ) + model_group = sc.DataGroup({'Model': model_data_array}) + + # if plot_individual_components: comps = + # ana.calculate_individual_components(E) for name, + # vals in comps.items(): if name not in + # component_arrays: component_arrays[name] = + # sc.zeros_like(data) csel = + # component_arrays[name] for d, i in + # zip(loop_dims, combo): csel = csel[d, i] + # csel.values = vals fsel.values = + # ana.calculate_theory(E) + + # # Build plot group + # data_and_model = {"Data": self._experiment._data.data, + # "Model": fit_total} if plot_individual_components and + # component_arrays: data_and_model.update(component_arrays) + # data_and_model = sc.DataGroup(data_and_model) + + if individual_components: + components = self.calculate_individual_components_all_Q() + for Q_index, (sample_comps, background_comps) in enumerate(components): + for samp_index, samp_comp in enumerate(sample_comps): + model_data_array[samp_comp.display_name] = sc.zeros_like(model_data_array.data) + model_data_array[samp_comp.display_name].data[Q_index, :] = samp_comp + for back_index, back_comp in enumerate(background_comps): + model_data_array[back_comp.display_name] = sc.zeros_like(model_data_array.data) + model_data_array[back_comp.display_name].data[Q_index, :] = back_comp + + model_data_array = model_data_array + model_group # WRONG BUT LINT + return model_data_array diff --git a/src/easydynamics/analysis/analysis1d.py b/src/easydynamics/analysis/analysis1d.py new file mode 100644 index 00000000..ebad61d2 --- /dev/null +++ b/src/easydynamics/analysis/analysis1d.py @@ -0,0 +1,498 @@ +# SPDX-FileCopyrightText: 2025-2026 EasyDynamics contributors +# SPDX-License-Identifier: BSD-3-Clause + + +import numpy as np +import scipp as sc +from easyscience.base_classes.model_base import ModelBase as EasyScienceModelBase +from easyscience.fitting.fitter import Fitter as EasyScienceFitter +from easyscience.variable import DescriptorNumber +from easyscience.variable import Parameter + +from easydynamics.convolution import Convolution +from easydynamics.experiment import Experiment +from easydynamics.sample_model import BackgroundModel +from easydynamics.sample_model import ResolutionModel +from easydynamics.sample_model import SampleModel + + +class Analysis1d(EasyScienceModelBase): + """For analysing data.""" + + def __init__( + self, + display_name: str = 'MyAnalysis', + unique_name: str | None = None, + experiment: Experiment | None = None, + sample_model: SampleModel | None = None, + resolution_model: ResolutionModel | None = None, + background_model: BackgroundModel | None = None, + energy_offset: list[Parameter] | None = None, + Q_index: int | None = None, + ): + super().__init__(display_name=display_name, unique_name=unique_name) + + if experiment is not None and not isinstance(experiment, Experiment): + raise TypeError('experiment must be an instance of Experiment or None.') + + self._experiment = experiment + + if sample_model is not None and not isinstance(sample_model, SampleModel): + raise TypeError('sample_model must be an instance of SampleModel or None.') + sample_model.Q = self.Q + self._sample_model = sample_model + + if resolution_model is not None and not isinstance(resolution_model, ResolutionModel): + raise TypeError('resolution_model must be an instance of ResolutionModel or None.') + resolution_model.Q = self.Q + self._resolution_model = resolution_model + + if background_model is not None and not isinstance(background_model, BackgroundModel): + raise TypeError('background_model must be an instance of BackgroundModel or None.') + background_model.Q = self.Q + self._background_model = background_model + + self._convolvers = [None] * (len(self.Q) if self.Q is not None else 0) + self._update_models() + + if not isinstance(energy_offset, list) and energy_offset is not None: + raise TypeError('energy_offset must be a list of Parameters or None.') + + if energy_offset is not None: + if len(energy_offset) != len(self.Q): + raise ValueError('energy_offset list length must match number of Q values.') + for offset in energy_offset: + if not isinstance(offset, Parameter): + raise TypeError('Each energy_offset must be an instance of Parameter.') + else: + energy_offset = [ + Parameter(name='energy_offset', value=0.0, unit=self.sample_model.unit) + for _ in range(len(self.Q)) + ] + self._energy_offset = energy_offset + + if Q_index is not None: + if ( + not isinstance(Q_index, int) + or Q_index < 0 + or (self.Q is not None and Q_index >= len(self.Q)) + ): + raise ValueError('Q_index must be a valid index for the Q values.') + self._Q_index = Q_index + + ############# + # Properties + ############# + + @property + def experiment(self) -> Experiment | None: + """The Experiment associated with this Analysis.""" + return self._experiment + + @experiment.setter + def experiment(self, value: Experiment | None) -> None: + if value is not None and not isinstance(value, Experiment): + raise TypeError('experiment must be an instance of Experiment or None.') + self._experiment = value + self._update_models() + + @property + def sample_model(self) -> SampleModel | None: + """The SampleModel associated with this Analysis.""" + return self._sample_model + + @sample_model.setter + def sample_model(self, value: SampleModel | None) -> None: + if value is not None and not isinstance(value, SampleModel): + raise TypeError('sample_model must be an instance of SampleModel or None.') + self._sample_model = value + self._update_models() + + @property + def resolution_model(self) -> ResolutionModel | None: + """The ResolutionModel associated with this Analysis.""" + return self._resolution_model + + @resolution_model.setter + def resolution_model(self, value: ResolutionModel | None) -> None: + if value is not None and not isinstance(value, ResolutionModel): + raise TypeError('resolution_model must be an instance of ResolutionModel or None.') + self._resolution_model = value + self._update_models() + + @property + def background_model(self) -> BackgroundModel | None: + """The BackgroundModel associated with this Analysis.""" + return self._background_model + + @background_model.setter + def background_model(self, value: BackgroundModel | None) -> None: + if value is not None and not isinstance(value, BackgroundModel): + raise TypeError('background_model must be an instance of BackgroundModel or None.') + self._background_model = value + self._update_models() + + @property + def Q(self) -> sc.Variable | None: + """The Q values from the associated Experiment, if available.""" + if self.experiment is not None: + return self.experiment.Q + return None + + @Q.setter + def Q(self, value) -> None: + """Q is a read-only property derived from the Experiment.""" + raise AttributeError('Q is a read-only property derived from the Experiment.') + + @property + def energy(self) -> sc.Variable | None: + """The energy values from the associated Experiment, if + available. + """ + if self.experiment is not None: + return self.experiment.energy + return None + + @energy.setter + def energy(self, value) -> None: + """Energy is a read-only property derived from the + Experiment. + """ + raise AttributeError('energy is a read-only property derived from the Experiment.') + + @property + def temperature(self) -> Parameter | None: + """The temperature from the associated Experiment, if + available. + """ + return self.sample_model.temperature if self.sample_model is not None else None + + @temperature.setter + def temperature(self, value) -> None: + """Temperature is a read-only property derived from the + Experiment. + """ + raise AttributeError('temperature is a read-only property derived from the sample model.') + + @property + def energy_offset(self) -> list[Parameter] | None: + """Get the energy offsets for each Q value.""" + return self._energy_offset + + @energy_offset.setter + def energy_offset(self, offsets: list[Parameter] | None) -> None: + """Set the energy offsets for each Q value. + + Args: + offsets (list[Parameter] | None): The list of energy + offsets. + Raises: + TypeError: If offsets is not a list of Parameters or + None. + """ + if offsets is not None: + if len(offsets) != len(self.Q): + raise ValueError('energy_offset list length must match number of Q values.') + for offset in offsets: + if not isinstance(offset, Parameter): + raise TypeError('Each energy_offset must be an instance of Parameter.') + self._energy_offset = offsets + + @property + def Q_index(self) -> int | None: + """Get the Q index for single Q analysis.""" + return self._Q_index + + @Q_index.setter + def Q_index(self, index: int | None) -> None: + """Set the Q index for single Q analysis. + + Args: + index (int | None): The Q index. + """ + if index is not None: + if ( + not isinstance(index, int) + or index < 0 + or (self.Q is not None and index >= len(self.Q)) + ): + raise ValueError('Q_index must be a valid index for the Q values.') + self._Q_index = index + + ############# + # Other methods + ############# + + def calculate(self, energy: float | None = None) -> np.ndarray: + """Calculate the model prediction for a given Q index. + + Args: + energy (float): The energy value to calculate the model for. + Returns: + sc.DataArray: The calculated model prediction. + """ + Q_index = self.Q_index + if Q_index is None: + raise ValueError('Q_index must be set to calculate the model.') + + if energy is None: + energy = self.energy.values + + # TODO: handle units properly + energy = energy - self.energy_offset[Q_index].value + if self.sample_model is None: + sample_intensity = np.zeros_like(energy) + else: + if self.resolution_model is None: + sample_intensity = self.sample_model._component_collections[Q_index].evaluate( + energy + ) + else: + convolver = self._convolvers[Q_index] + sample_intensity = convolver.convolution() + + if self.background_model is None: + background_intensity = np.zeros_like(energy) + else: + background_intensity = self.background_model._component_collections[Q_index].evaluate( + energy + ) + + sample_plus_background = sample_intensity + background_intensity + + return sample_plus_background + + def calculate_individual_components( + self, + ) -> tuple[list[np.ndarray], list[np.ndarray]]: + """Calculate the model prediction for a given Q index for each + individual component. + + Args: + Q_index (int): The index of the Q value to calculate the + model for. + Returns: + list[np.ndarray]: The calculated model predictions for each + individual component. + """ + sample_results = [] + background_results = [] + Q_index = self.Q_index + if Q_index is None: + raise ValueError('Q_index must be set to calculate the model.') + + if self.sample_model is not None: + # Calculate sample components + for component in self.sample_model._component_collections[Q_index]._components: + if self.resolution_model is None: + component_intensity = component.evaluate(self.energy) + else: + convolver = Convolution( + sample_components=component, + resolution_components=self.resolution_model._component_collections[ + Q_index + ], + energy=self.energy, + temperature=self.temperature, + ) + component_intensity = convolver.convolution() + sample_results.append(component_intensity) + + if self.background_model is not None: + # Calculate background components + for component in self.background_model._component_collections[Q_index]._components: + component_intensity = component.evaluate(self.energy) + background_results.append(component_intensity) + + return sample_results, background_results + + def fit(self): + """Fit the model to the experimental data for a given Q index. + + Args: + Returns: + FitResult: The result of the fit. + """ + if self._experiment is None: + raise ValueError('No experiment is associated with this Analysis.') + + Q_index = self.Q_index + if Q_index is None: + raise ValueError('Q_index must be set to perform the fit.') + + data = self.experiment.data['Q', Q_index] + x = data.coords['energy'].values + y = data.values + e = data.variances**0.5 + + def fit_func(x_vals): + return self.calculate(energy=x_vals) + + fitter = EasyScienceFitter( + fit_object=self, + fit_function=fit_func, + ) + + # Perform the fit + fit_result = fitter.fit(x=x, y=y, weights=1.0 / e) + + # Store result + self.fit_result = fit_result + + return fit_result + + def plot_data_and_model( + self, + plot_individual_components: bool = True, + ) -> None: + """Plot the experimental data and the model prediction. + + Args: + plot_individual_components (bool): Whether to plot + individual components. Default is True. + """ + if not isinstance(plot_individual_components, bool): + raise TypeError('plot_individual_components must be True or False.') + + import matplotlib.pyplot as plt + + Q_index = self.Q_index + if Q_index is None: + raise ValueError('Q_index must be set to plot the data and model.') + if self.experiment is None or self.experiment.data is None: + raise ValueError('Experiment data is not available for plotting.') + data = self.experiment.data['Q', Q_index] + energy = data.coords['energy'].values + model = self.calculate(energy=energy) + plt.figure() + plt.errorbar( + energy, + data.values, + yerr=data.variances**0.5, + fmt='o', + label='Data', + color='black', + ) + plt.plot(energy, model, label='Model', color='red') + if plot_individual_components: + sample_comps, background_comps = self.calculate_individual_components() + for i, comp in enumerate(sample_comps): + plt.plot( + energy, + comp, + label=f'Sample Component {i + 1}', + linestyle='--', + ) + for i, comp in enumerate(background_comps): + plt.plot( + energy, + comp, + label=f'Background Component {i + 1}', + linestyle=':', + ) + plt.xlabel(f'Energy ({self.energy.unit})') + plt.ylabel(f'Intensity ({self.sample_model.unit})') + plt.title(f'Data and Model at Q index {Q_index}') + plt.legend() + plt.show() + # model_data_array = self._create_model_data_group( + # individual_components=plot_individual_components ) if + # self.experiment is None or self.experiment.data is None: raise + # ValueError("Experiment data is not available for plotting.") + + # from IPython.display import display + + # fig = pp.slicer( + # {"Data": self.experiment.data, "Model": model_data_array}, + # color={"Data": "black", "Model": "red"}, + # linestyle={"Data": "none", "Model": "solid"}, + # marker={"Data": "o", "Model": "None"}, + # ) + # display(fig) + + def get_all_variables(self) -> list[DescriptorNumber]: + """Get all variables used in the analysis. + + Returns: + List[Descriptor]: A list of all variables. + """ + variables = [] + if self.sample_model is not None: + variables.extend( + self.sample_model._component_collections[self.Q_index].get_all_variables() + ) + if self.resolution_model is not None: + variables.extend( + self.resolution_model._component_collections[self.Q_index].get_all_variables() + ) + if self.background_model is not None: + variables.extend( + self.background_model._component_collections[self.Q_index].get_all_variables() + ) + variables.append(self.energy_offset[self.Q_index]) + # TODO temperature and diffusion + return variables + + ############# + # Private methods + ############# + + def _update_models(self): + """Update models based on the current experiment.""" + if self.experiment is None: + return + + for Q_index in range(len(self.Q)): + self._convolvers[Q_index] = self._create_convolver(Q_index) + + def _create_convolver(self, Q_index: int): + """Initialize and return a Convolution object for the given Q + index. + """ + if self.sample_model is None or self.resolution_model is None: + raise ValueError('Both sample_model and resolution_model must be defined.') + + sample_components = self.sample_model._component_collections[Q_index] + resolution_components = self.resolution_model._component_collections[Q_index] + energy = self.energy + convolver = Convolution( + sample_components=sample_components, + resolution_components=resolution_components, + energy=energy, + temperature=self.temperature, + ) + return convolver + + def _create_model_data_group(self, individual_components=True) -> sc.DataArray: + """Create a Scipp DataArray representing the model over all Q + and energy values. + """ + if self.Q is None or self.energy is None: + raise ValueError('Q and energy must be defined in the experiment.') + + model_data = [] + for Q_index in range(len(self.Q)): + model_at_Q = self.calculate(Q_index) + model_data.append(model_at_Q) + + model_data_array = sc.DataArray( + data=sc.array(dims=['Q', 'energy'], values=model_data), + coords={ + 'Q': self.Q, + 'energy': self.energy, + }, + ) + model_group = sc.DataGroup({'Model': model_data_array}) + + if individual_components: + components = self.calculate_individual_components_all_Q() + for Q_index, (sample_comps, background_comps) in enumerate(components): + for samp_index, samp_comp in enumerate(sample_comps): + model_data_array[samp_comp.display_name] = sc.zeros_like(model_data_array.data) + model_data_array[samp_comp.display_name].data[Q_index, :] = samp_comp + for back_index, back_comp in enumerate(background_comps): + model_data_array[back_comp.display_name] = sc.zeros_like(model_data_array.data) + model_data_array[back_comp.display_name].data[Q_index, :] = back_comp + + model_data_array = model_data_array + model_group # WRONG BUT LINT + return model_data_array diff --git a/src/easydynamics/convolution/convolution_base.py b/src/easydynamics/convolution/convolution_base.py index 34eab3f4..cfe364b0 100644 --- a/src/easydynamics/convolution/convolution_base.py +++ b/src/easydynamics/convolution/convolution_base.py @@ -54,6 +54,8 @@ def __init__( raise TypeError( f'`sample_components` is an instance of {type(sample_components).__name__}, but must be a ComponentCollection or ModelComponent.' # noqa: E501 ) + if isinstance(sample_components, ModelComponent): + sample_components = ComponentCollection(components=[sample_components]) self._sample_components = sample_components if resolution_components is not None and not ( @@ -63,6 +65,8 @@ def __init__( raise TypeError( f'`resolution_components` is an instance of {type(resolution_components).__name__}, but must be a ComponentCollection or ModelComponent.' # noqa: E501 ) + if isinstance(resolution_components, ModelComponent): + resolution_components = ComponentCollection(components=[resolution_components]) self._resolution_components = resolution_components @property diff --git a/src/easydynamics/sample_model/__init__.py b/src/easydynamics/sample_model/__init__.py index 5929fc50..193ba7f5 100644 --- a/src/easydynamics/sample_model/__init__.py +++ b/src/easydynamics/sample_model/__init__.py @@ -1,6 +1,7 @@ # SPDX-FileCopyrightText: 2025-2026 EasyDynamics contributors # SPDX-License-Identifier: BSD-3-Clause +from .background_model import BackgroundModel from .component_collection import ComponentCollection from .components import DampedHarmonicOscillator from .components import DeltaFunction @@ -9,6 +10,8 @@ from .components import Polynomial from .components import Voigt from .diffusion_model.brownian_translational_diffusion import BrownianTranslationalDiffusion +from .resolution_model import ResolutionModel +from .sample_model import SampleModel __all__ = [ 'ComponentCollection', @@ -19,4 +22,7 @@ 'DampedHarmonicOscillator', 'Polynomial', 'BrownianTranslationalDiffusion', + 'SampleModel', + 'ResolutionModel', + 'BackgroundModel', ] From 739f19cf470e635409f4ff5375ebde3eb9954124 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Thu, 5 Feb 2026 20:30:14 +0100 Subject: [PATCH 02/17] Instrument model (#94) * initial instrument model * first draft of analysis * add test of model base * small changes * tests * clear notebook * respond to PR comments * Update resolution_model docstring for clarity --- docs/docs/tutorials/instrument_model.ipynb | 101 +++++ .../sample_model/component_collection.py | 2 +- .../sample_model/instrument_model.py | 304 ++++++++++++- src/easydynamics/sample_model/model_base.py | 76 +++- src/easydynamics/sample_model/sample_model.py | 11 +- .../sample_model/test_component_collection.py | 7 +- .../sample_model/test_instrument_model.py | 398 ++++++++++++++++++ .../sample_model/test_model_base.py | 50 +++ 8 files changed, 922 insertions(+), 27 deletions(-) create mode 100644 docs/docs/tutorials/instrument_model.ipynb create mode 100644 tests/unit/easydynamics/sample_model/test_instrument_model.py diff --git a/docs/docs/tutorials/instrument_model.ipynb b/docs/docs/tutorials/instrument_model.ipynb new file mode 100644 index 00000000..a56b300f --- /dev/null +++ b/docs/docs/tutorials/instrument_model.ipynb @@ -0,0 +1,101 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Instrument Model\n", + "We here introduce the InstrumentModel, which contains all information related to the instrument: the BackgroundModel, ResolutionModel and also a fittable offset in the energy transfer due to slight instrument misalignment.\n", + "\n", + "The InstrumentModel does not itself do any calculations; it is merely a container for all information about the instrument.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "\n", + "from easydynamics.sample_model import Gaussian\n", + "from easydynamics.sample_model import Polynomial\n", + "from easydynamics.sample_model.background_model import BackgroundModel\n", + "from easydynamics.sample_model.instrument_model import InstrumentModel\n", + "from easydynamics.sample_model.resolution_model import ResolutionModel\n", + "\n", + "%matplotlib widget" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3", + "metadata": {}, + "outputs": [], + "source": [ + "# Create a BackgroundModel and a ResolutionModel and add them to an\n", + "# InstrumentModel\n", + "\n", + "Q = np.linspace(0.1, 2.0, 5)\n", + "\n", + "background_model = BackgroundModel()\n", + "background_model.components = Polynomial(coefficients=[1, 0.1, 0.01])\n", + "\n", + "resolution_model = ResolutionModel()\n", + "resolution_model.append_component(Gaussian(width=0.05))\n", + "\n", + "instrument_model = InstrumentModel(\n", + " Q=Q,\n", + " resolution_model=resolution_model,\n", + " background_model=background_model,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4", + "metadata": {}, + "outputs": [], + "source": [ + "instrument_model.get_all_variables(Q_index=1)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3eca4688", + "metadata": {}, + "outputs": [], + "source": [ + "instrument_model.fix_resolution_parameters()\n", + "instrument_model.get_all_variables(Q_index=1)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "easydynamics_newbase", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/src/easydynamics/sample_model/component_collection.py b/src/easydynamics/sample_model/component_collection.py index 5978539d..586a6649 100644 --- a/src/easydynamics/sample_model/component_collection.py +++ b/src/easydynamics/sample_model/component_collection.py @@ -223,7 +223,7 @@ def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) """ if not self.components: - raise ValueError('No components in the model to evaluate.') + return np.zeros_like(x) return sum(component.evaluate(x) for component in self.components) def evaluate_component( diff --git a/src/easydynamics/sample_model/instrument_model.py b/src/easydynamics/sample_model/instrument_model.py index 9f3eb1d2..bef6bd92 100644 --- a/src/easydynamics/sample_model/instrument_model.py +++ b/src/easydynamics/sample_model/instrument_model.py @@ -1,5 +1,305 @@ # SPDX-FileCopyrightText: 2025-2026 EasyDynamics contributors # SPDX-License-Identifier: BSD-3-Clause -# instrument_model will contain resolution_model and background_model -# as well as offset +from copy import copy + +import numpy as np +import scipp as sc +from easyscience.base_classes.new_base import NewBase +from easyscience.variable import Parameter + +from easydynamics.sample_model.background_model import BackgroundModel +from easydynamics.sample_model.resolution_model import ResolutionModel +from easydynamics.utils.utils import Numeric +from easydynamics.utils.utils import Q_type +from easydynamics.utils.utils import _validate_and_convert_Q +from easydynamics.utils.utils import _validate_unit + + +class InstrumentModel(NewBase): + """InstrumentModel represents a model of the instrument in an + experiment at various Q. It can contain a model of the resolution + function for convolutions, of the background and an offset in the + energy axis. + + Parameters + ---------- + display_name : str, optional + The display name of the InstrumentModel. Default is + "MyInstrumentModel". + unique_name : str or None, optional + The unique name of the InstrumentModel. Default is None. + Q : np.ndarray, list, scipp Variable or None, optional + The Q values where the instrument is modelled. + resolution_model : ResolutionModel or None, optional + The resolution model of the instrument. If None, an empty + resolution model is created and no resolution convolution is + carried out. Default is None. + background_model : BackgroundModel or None, optional + The background model of the instrument. If None, an empty + background model is created, and the background evaluates to 0. + Default is None. + energy_offset : float, int or None, optional + Template energy offset of the instrument. Will be copied to each + Q value. If None, the energy offset will be 0. Default is None. + unit : str or sc.Unit, optional + The unit of the energy axis. Default is 'meV'. + """ + + def __init__( + self, + display_name: str = 'MyInstrumentModel', + unique_name: str | None = None, + Q: Q_type | None = None, + resolution_model: ResolutionModel | None = None, + background_model: BackgroundModel | None = None, + energy_offset: Numeric | None = None, + unit: str | sc.Unit = 'meV', + ): + super().__init__( + display_name=display_name, + unique_name=unique_name, + ) + + self._unit = _validate_unit(unit) + + if resolution_model is None: + self._resolution_model = ResolutionModel() + else: + if not isinstance(resolution_model, ResolutionModel): + raise TypeError( + f'resolution_model must be a ResolutionModel or None, ' + f'got {type(resolution_model).__name__}' + ) + self._resolution_model = resolution_model + + if background_model is None: + self._background_model = BackgroundModel() + else: + if not isinstance(background_model, BackgroundModel): + raise TypeError( + f'background_model must be a BackgroundModel or None, ' + f'got {type(background_model).__name__}' + ) + self._background_model = background_model + + if energy_offset is None: + energy_offset = 0.0 + + if not isinstance(energy_offset, Numeric): + raise TypeError('energy_offset must be a number or None') + + self._energy_offset = Parameter( + name='energy_offset', + value=float(energy_offset), + unit=self.unit, + fixed=False, + ) + self._Q = _validate_and_convert_Q(Q) + self._on_Q_change() + + # ------------------------------------------------------------- + # Properties + # ------------------------------------------------------------- + + @property + def resolution_model(self) -> ResolutionModel: + """Get the resolution model of the instrument.""" + return self._resolution_model + + @resolution_model.setter + def resolution_model(self, value: ResolutionModel): + """Set the resolution model of the instrument.""" + if not isinstance(value, ResolutionModel): + raise TypeError( + f'resolution_model must be a ResolutionModel, got {type(value).__name__}' + ) + self._resolution_model = value + self._on_resolution_model_change() + + @property + def background_model(self) -> BackgroundModel: + """The background model of the instrument.""" + return self._background_model + + @background_model.setter + def background_model(self, value: BackgroundModel): + """Set the background model of the instrument.""" + if not isinstance(value, BackgroundModel): + raise TypeError( + f'background_model must be a BackgroundModel, got {type(value).__name__}' + ) + self._background_model = value + self._on_background_model_change() + + @property + def Q(self) -> np.ndarray | None: + """Get the Q values of the InstrumentModel.""" + return self._Q + + @Q.setter + def Q(self, value: Q_type | None) -> None: + """Set the Q values of the InstrumentModel.""" + self._Q = _validate_and_convert_Q(value) + self._on_Q_change() + + @property + def unit(self) -> sc.Unit: + """Get the unit of the InstrumentModel. + + Returns + ------- + str or sc.Unit or None + """ + return self._unit + + @unit.setter + def unit(self, unit_str: str) -> None: + raise AttributeError( + ( + f'Unit is read-only. Use convert_unit to change the unit between allowed types ' + f'or create a new {self.__class__.__name__} with the desired unit.' + ) + ) # noqa: E501 + + @property + def energy_offset(self) -> Parameter: + """The energy offset template parameter of the instrument + model. + """ + return self._energy_offset + + @energy_offset.setter + def energy_offset(self, value: Numeric): + """Set the offset parameter of the instrument model.". + + Parameters + ---------- + value : float or int + The new value for the energy offset parameter. Will be + copied to all Q values. + Raises + ------ + TypeError + If value is not a number. + """ + if not isinstance(value, Numeric): + raise TypeError(f'energy_offset must be a number, got {type(value).__name__}') + self._energy_offset.value = value + + self._on_energy_offset_change() + + # -------------------------------------------------------------- + # Other methods + # -------------------------------------------------------------- + + def convert_unit(self, unit_str: str | sc.Unit) -> None: + """Convert the unit of the InstrumentModel. + + Parameters + ---------- + unit_str : str or sc.Unit + The unit to convert to. + + Raises + ------ + TypeError + If unit_str is not a string or scipp Unit. + """ + unit = _validate_unit(unit_str) + if unit is None: + raise ValueError('unit_str must be a valid unit string or scipp Unit') + + self._background_model.convert_unit(unit) + self._resolution_model.convert_unit(unit) + self._energy_offset.convert_unit(unit) + for offset in self._energy_offsets: + offset.convert_unit(unit) + + self._unit = unit + + def get_all_variables(self, Q_index: int | None = None) -> list[Parameter]: + """Get all variables in the InstrumentModel. + + Parameters + ---------- + Q_index : int | None + The index of the Q value to get variables for. If None, get + variables for all Q values. + Returns + ------- + list of Parameter + All variables in the InstrumentModel. + """ + if self._Q is None: + return [] + + if Q_index is None: + variables = [self._energy_offsets[i] for i in range(len(self._Q))] + else: + if not isinstance(Q_index, int): + raise TypeError(f'Q_index must be an int or None, got {type(Q_index).__name__}') + if Q_index < 0 or Q_index >= len(self._Q): + raise IndexError( + f'Q_index {Q_index} is out of bounds for Q of length {len(self._Q)}' + ) + variables = [self._energy_offsets[Q_index]] + + variables.extend(self._background_model.get_all_variables(Q_index=Q_index)) + variables.extend(self._resolution_model.get_all_variables(Q_index=Q_index)) + + return variables + + def fix_resolution_parameters(self) -> None: + """Fix all parameters in the resolution model.""" + self.resolution_model.fix_all_parameters() + + def free_resolution_parameters(self) -> None: + """Free all parameters in the resolution model.""" + self.resolution_model.free_all_parameters() + + # -------------------------------------------------------------- + # Private methods + # -------------------------------------------------------------- + + def _generate_energy_offsets(self) -> None: + """Generate energy offset Parameters for each Q value.""" + if self._Q is None: + self._energy_offsets = [] + return + + self._energy_offsets = [copy(self._energy_offset) for _ in self._Q] + + def _on_Q_change(self) -> None: + """Handle changes to the Q values.""" + self._generate_energy_offsets() + self._resolution_model.Q = self._Q + self._background_model.Q = self._Q + + def _on_energy_offset_change(self) -> None: + """Handle changes to the energy offset.""" + for offset in self._energy_offsets: + offset.value = self._energy_offset.value + + def _on_resolution_model_change(self) -> None: + """Handle changes to the resolution model.""" + self._resolution_model.Q = self._Q + + def _on_background_model_change(self) -> None: + """Handle changes to the background model.""" + self._background_model.Q = self._Q + + # ------------------------------------------------------------- + # Dunder methods + # ------------------------------------------------------------- + + def __repr__(self): + return ( + f'{self.__class__.__name__}(' + f'unique_name={self.unique_name!r}, ' + f'unit={self.unit}, ' + f'Q_len={None if self._Q is None else len(self._Q)}, ' + f'resolution_model={self._resolution_model!r}, ' + f'background_model={self._background_model!r}' + f')' + ) diff --git a/src/easydynamics/sample_model/model_base.py b/src/easydynamics/sample_model/model_base.py index c99e3f62..b6b8bcdd 100644 --- a/src/easydynamics/sample_model/model_base.py +++ b/src/easydynamics/sample_model/model_base.py @@ -7,6 +7,7 @@ import numpy as np import scipp as sc from easyscience.base_classes.model_base import ModelBase as EasyScienceModelBase +from easyscience.variable import Parameter from easydynamics.sample_model.component_collection import ComponentCollection from easydynamics.sample_model.components.model_component import ModelComponent @@ -106,7 +107,7 @@ def append_component(self, component: ModelComponent | ComponentCollection) -> N The ModelComponent or ComponentCollection to append. """ self._components.append_component(component) - self._generate_component_collections() + self._on_components_change() def remove_component(self, unique_name: str) -> None: """Remove a ModelComponent from the SampleModel by its unique @@ -117,12 +118,12 @@ def remove_component(self, unique_name: str) -> None: to remove. """ self._components.remove_component(unique_name) - self._generate_component_collections() + self._on_components_change() def clear_components(self) -> None: """Clear all ModelComponents from the SampleModel.""" self._components.clear_components() - self._generate_component_collections() + self._on_components_change() # ------------------------------------------------------------------ # Properties @@ -166,7 +167,7 @@ def convert_unit(self, unit: str | sc.Unit) -> None: except Exception: # noqa: S110 pass # Best effort rollback raise e - self._generate_component_collections() + self._on_components_change() @property def components(self) -> list[ModelComponent]: @@ -192,7 +193,53 @@ def Q(self) -> np.ndarray | None: def Q(self, value: Q_type | None) -> None: """Set the Q values of the SampleModel.""" self._Q = _validate_and_convert_Q(value) - self._generate_component_collections() + self._on_Q_change() + + # ------------------------------------------------------------------ + # Other methods + # ------------------------------------------------------------------ + def fix_all_parameters(self) -> None: + """Fix all Parameters in all ComponentCollections.""" + for par in self.get_all_variables(): + par.fixed = True + + def free_all_parameters(self) -> None: + """Free all Parameters in all ComponentCollections.""" + for par in self.get_all_variables(): + par.fixed = False + + def get_all_variables(self, Q_index: int | None = None) -> list[Parameter]: + """Get all Parameters and Descriptors from all + ComponentCollections in the ModelBase. Parameters Ignores the + Parameters and Descriptors in self._components as these are just + templates. + + Parameters + ---------- + Q_index : int | None + If int, get variables for the ComponentCollection at + this index. If None, get variables for all + ComponentCollections. + Returns + ------- + list[Parameter] + """ + if Q_index is None: + all_vars = [ + var + for collection in self._component_collections + for var in collection.get_all_variables() + ] + else: + if not isinstance(Q_index, int): + raise TypeError(f'Q_index must be an int or None, got {type(Q_index).__name__}') + if Q_index < 0 or Q_index >= len(self._component_collections): + raise IndexError( + f'Q_index {Q_index} is out of bounds for component collections ' + f'of length {len(self._component_collections)}' + ) + all_vars = self._component_collections[Q_index].get_all_variables() + return all_vars # ------------------------------------------------------------------ # Private methods @@ -215,20 +262,13 @@ def _generate_component_collections(self) -> None: for component in self._components.components: collection.append_component(copy(component)) - def get_all_variables(self): - """Get all Parameters and Descriptors from all - ComponentCollections in the ModelBase. - - Ignores the Parameters and Descriptors in self._components as - these are just templates. - """ + def _on_Q_change(self) -> None: + """Handle changes to the Q values.""" + self._generate_component_collections() - all_vars = [ - var - for collection in self._component_collections - for var in collection.get_all_variables() - ] - return all_vars + def _on_components_change(self) -> None: + """Handle changes to the components.""" + self._generate_component_collections() # ------------------------------------------------------------------ # dunder methods diff --git a/src/easydynamics/sample_model/sample_model.py b/src/easydynamics/sample_model/sample_model.py index af5550a9..cd1f2c2e 100644 --- a/src/easydynamics/sample_model/sample_model.py +++ b/src/easydynamics/sample_model/sample_model.py @@ -175,7 +175,7 @@ def diffusion_models( 'or None' ) self._diffusion_models = value - self._generate_component_collections() + self._on_diffusion_models_change() @property def temperature(self) -> Parameter | None: @@ -286,7 +286,7 @@ def evaluate( return y - def get_all_variables(self): + def get_all_variables(self, Q_index: int | None = None) -> list[Parameter]: """Get all Parameters and Descriptors from all ComponentCollections in the SampleModel. @@ -294,7 +294,8 @@ def get_all_variables(self): diffusion models. Ignores the Parameters and Descriptors in self._components as these are just templates. """ - all_vars = super().get_all_variables() + + all_vars = super().get_all_variables(Q_index=Q_index) if self._temperature is not None: all_vars.append(self._temperature) @@ -325,6 +326,10 @@ def _generate_component_collections(self) -> None: for component in source.components: target.append_component(component) + def _on_diffusion_models_change(self) -> None: + """Handle changes to the diffusion models.""" + self._generate_component_collections() + # ------------------------------------------------------------------ # dunder methods # ------------------------------------------------------------------ diff --git a/tests/unit/easydynamics/sample_model/test_component_collection.py b/tests/unit/easydynamics/sample_model/test_component_collection.py index 926adfa6..42a66f6a 100644 --- a/tests/unit/easydynamics/sample_model/test_component_collection.py +++ b/tests/unit/easydynamics/sample_model/test_component_collection.py @@ -216,13 +216,14 @@ def test_evaluate(self, component_collection): ) + component_collection.components[1].evaluate(x) np.testing.assert_allclose(result, expected_result, rtol=1e-5) - def test_evaluate_no_components_raises(self): + def test_evaluate_no_components_returns_zero(self): # WHEN THEN component_collection = ComponentCollection(display_name='EmptyModel') x = np.linspace(-5, 5, 100) # EXPECT - with pytest.raises(ValueError, match='No components in the model to evaluate.'): - component_collection.evaluate(x) + result = component_collection.evaluate(x) + assert np.all(result == 0.0) + assert result.shape == x.shape def test_evaluate_component(self, component_collection): # WHEN THEN diff --git a/tests/unit/easydynamics/sample_model/test_instrument_model.py b/tests/unit/easydynamics/sample_model/test_instrument_model.py new file mode 100644 index 00000000..00f036cd --- /dev/null +++ b/tests/unit/easydynamics/sample_model/test_instrument_model.py @@ -0,0 +1,398 @@ +# SPDX-FileCopyrightText: 2025-2026 EasyDynamics contributors +# SPDX-License-Identifier: BSD-3-Clause + + +from unittest.mock import MagicMock +from unittest.mock import patch + +import numpy as np +import pytest + +from easydynamics.sample_model import Gaussian +from easydynamics.sample_model import Polynomial +from easydynamics.sample_model.background_model import BackgroundModel +from easydynamics.sample_model.instrument_model import InstrumentModel +from easydynamics.sample_model.resolution_model import ResolutionModel + + +class TestInstrumentModel: + @pytest.fixture + def instrument_model(self): + Q = np.array([1.0, 2.0, 3.0]) + component1 = Polynomial(coefficients=[1.0, 2.0]) + background_model = BackgroundModel(components=component1, Q=Q) + + component2 = Gaussian() + resolution_model = ResolutionModel(components=component2, Q=Q) + + instrument_model = InstrumentModel( + display_name='TestInstrumentModel', + background_model=background_model, + resolution_model=resolution_model, + Q=Q, + ) + + return instrument_model + + @pytest.fixture + def resolution_model(self): + component = Gaussian() + resolution_model = ResolutionModel(components=component) + return resolution_model + + @pytest.fixture + def background_model(self): + component = Polynomial(coefficients=[1.0, 2.0]) + background_model = BackgroundModel(components=component) + return background_model + + def test_init(self, instrument_model): + # WHEN THEN + model = instrument_model + + # EXPECT + assert model.display_name == 'TestInstrumentModel' + np.testing.assert_array_equal(model.Q, np.array([1.0, 2.0, 3.0])) + assert isinstance(model.background_model, BackgroundModel) + assert isinstance(model.resolution_model, ResolutionModel) + np.testing.assert_array_equal(model.background_model.Q, np.array([1.0, 2.0, 3.0])) + np.testing.assert_array_equal(model.resolution_model.Q, np.array([1.0, 2.0, 3.0])) + np.testing.assert_array_equal(model.Q, np.array([1.0, 2.0, 3.0])) + + def test_init_defaults(self): + # WHEN THEN + model = InstrumentModel() + + # EXPECT + assert model.display_name == 'MyInstrumentModel' + assert isinstance(model.background_model, BackgroundModel) + assert isinstance(model.resolution_model, ResolutionModel) + assert model.Q is None + + @pytest.mark.parametrize( + 'kwargs, expected_exception, expected_message', + [ + ( + {'resolution_model': 123}, + TypeError, + 'resolution_model must be a ResolutionModel', + ), + ( + {'background_model': 'not a model'}, + TypeError, + 'background_model must be a BackgroundModel', + ), + ( + {'energy_offset': 'abc'}, + TypeError, + 'energy_offset must be a number', + ), + ( + {'unit': 123}, + TypeError, + 'unit must be', + ), + ], + ids=[ + 'invalid resolution_model', + 'invalid background_model', + 'invalid energy_offset', + 'invalid unit', + ], + ) + def test_instrument_model_init_invalid_inputs( + self, kwargs, expected_exception, expected_message + ): + with pytest.raises(expected_exception, match=expected_message): + InstrumentModel(**kwargs) + + def test_resolution_model_setter_calls_update(self, instrument_model, resolution_model): + # WHEN + instrument_model._on_resolution_model_change = MagicMock() + + # THEN + instrument_model.resolution_model = resolution_model + + # EXPECT + assert instrument_model._resolution_model is resolution_model + instrument_model._on_resolution_model_change.assert_called_once() + + def test_resolution_model_setter_raises(self, instrument_model): + # WHEN / THEN / EXPECT + with pytest.raises( + TypeError, + match='resolution_model must be a ResolutionModel', + ): + instrument_model.resolution_model = 'invalid_model' + + def test_background_model_setter_calls_update(self, instrument_model, background_model): + # WHEN + instrument_model._on_background_model_change = MagicMock() + + # THEN + instrument_model.background_model = background_model + + # EXPECT + assert instrument_model._background_model is background_model + instrument_model._on_background_model_change.assert_called_once() + + def test_background_model_setter_raises(self, instrument_model): + # WHEN / THEN / EXPECT + with pytest.raises( + TypeError, + match='background_model must be a BackgroundModel', + ): + instrument_model.background_model = 123 + + def test_Q_setter(self, instrument_model): + "Test that Q setter calls the appropriate methods." + # WHEN + new_Q = np.array([4.0, 5.0, 6.0]) + + instrument_model._on_Q_change = MagicMock() + + # THEN EXPECT + with patch( + 'easydynamics.sample_model.instrument_model._validate_and_convert_Q', + return_value=new_Q, + ) as mock_validate: + instrument_model.Q = new_Q + + np.testing.assert_array_equal(instrument_model.Q, new_Q) + mock_validate.assert_called_once_with(new_Q) + instrument_model._on_Q_change.assert_called_once() + + def test_unit_setter_raises(self, instrument_model): + # WHEN / THEN / EXPECT + with pytest.raises( + AttributeError, + match='Unit is read-only. Use convert_unit to change the unit between allowed types ', + ): + instrument_model.unit = 'meV' + + def test_energy_offset_setter(self, instrument_model): + # WHEN + instrument_model._on_energy_offset_change = MagicMock() + + # THEN + instrument_model.energy_offset = 1.0 + + # EXPECT + assert instrument_model.energy_offset.value == 1.0 + instrument_model._on_energy_offset_change.assert_called_once() + + def test_energy_offset_setter_raises(self, instrument_model): + # WHEN / THEN / EXPECT + with pytest.raises( + TypeError, + match='energy_offset must be a number', + ): + instrument_model.energy_offset = 'invalid_offset' + + def test_convert_unit_calls_all_children(self, instrument_model): + # WHEN + new_unit = 'eV' + + # THEN + # Mock downstream convert_unit calls + instrument_model._background_model.convert_unit = MagicMock() + instrument_model._resolution_model.convert_unit = MagicMock() + instrument_model._energy_offset.convert_unit = MagicMock() + for offset in instrument_model._energy_offsets: + offset.convert_unit = MagicMock() + + with patch( + 'easydynamics.sample_model.instrument_model._validate_unit', + return_value=new_unit, + ) as mock_validate: + instrument_model.convert_unit(new_unit) + + # EXPECT + mock_validate.assert_called_once_with(new_unit) + + instrument_model._background_model.convert_unit.assert_called_once_with(new_unit) + instrument_model._resolution_model.convert_unit.assert_called_once_with(new_unit) + instrument_model._energy_offset.convert_unit.assert_called_once_with(new_unit) + + for offset in instrument_model._energy_offsets: + offset.convert_unit.assert_called_once_with(new_unit) + + # final state + assert instrument_model.unit == new_unit + + def test_convert_unit_None_raises(self, instrument_model): + # WHEN / THEN / EXPECT + with pytest.raises( + ValueError, + match=' must be a valid unit', + ): + instrument_model.convert_unit(None) + + def test_fix_resolution_parameters(self, instrument_model): + # WHEN + instrument_model.resolution_model.fix_all_parameters = MagicMock() + + # THEN + instrument_model.fix_resolution_parameters() + + # EXPECT + instrument_model.resolution_model.fix_all_parameters.assert_called_once() + + def test_free_all_resolution_parameters(self, instrument_model): + # WHEN + instrument_model.resolution_model.free_all_parameters = MagicMock() + + # THEN + instrument_model.free_resolution_parameters() + + # EXPECT + instrument_model.resolution_model.free_all_parameters.assert_called_once() + + def test_get_all_variables(self, instrument_model): + # WHEN + all_vars = instrument_model.get_all_variables() + + # THEN + expected_var_names = { + 'energy_offset', + 'Polynomial_c0', + 'Polynomial_c1', + 'Gaussian area', + 'Gaussian center', + 'Gaussian width', + } + + retrieved_var_names = {var.name for var in all_vars} + + assert expected_var_names == retrieved_var_names + assert len(all_vars) == 18 + + def test_get_all_variables_no_Q(self, instrument_model): + # WHEN + instrument_model.Q = None + + # THEN + all_vars = instrument_model.get_all_variables() + + # EXPECT + assert all_vars == [] + + def test_get_all_variables_with_Q_index(self, instrument_model): + # WHEN + all_vars = instrument_model.get_all_variables(Q_index=1) + + # THEN + expected_var_names = { + 'energy_offset', + 'Polynomial_c0', + 'Polynomial_c1', + 'Gaussian area', + 'Gaussian center', + 'Gaussian width', + } + + retrieved_var_names = {var.name for var in all_vars} + + assert expected_var_names == retrieved_var_names + assert len(all_vars) == 6 + + def test_get_all_variables_with_invalid_Q_index_raises(self, instrument_model): + # WHEN / THEN / EXPECT + with pytest.raises( + IndexError, + match='Q_index 5 is out of bounds', + ): + instrument_model.get_all_variables(Q_index=5) + + def test_get_all_variables_with_nonint_Q_index_raises(self, instrument_model): + # WHEN / THEN / EXPECT + with pytest.raises( + TypeError, + match='Q_index must be an int or None, got str', + ): + instrument_model.get_all_variables(Q_index='invalid_index') + + def test_generate_energy_offsets_Q_none(self, instrument_model): + # WHEN + instrument_model._Q = None + + # THEN + instrument_model._generate_energy_offsets() + + # EXPECT + assert instrument_model._energy_offsets == [] + + def test_generate_energy_offsets(self, instrument_model): + # WHEN + instrument_model._Q = np.array([1.0, 2.0, 3.0, 4.0]) + + # THEN + instrument_model._generate_energy_offsets() + + # EXPECT + assert len(instrument_model._energy_offsets) == 4 + for offset in instrument_model._energy_offsets: + assert offset.name == 'energy_offset' + assert offset.unit == instrument_model.unit + assert offset.value == instrument_model.energy_offset.value + + def test_on_Q_change(self, instrument_model): + # WHEN + instrument_model._generate_energy_offsets = MagicMock() + new_Q = np.array([1.0, 2.0, 3.0, 4.0]) + + # THEN + instrument_model._Q = new_Q + instrument_model._on_Q_change() + + # EXPECT + instrument_model._generate_energy_offsets.assert_called_once() + instrument_model._background_model.Q = new_Q + instrument_model._resolution_model.Q = new_Q + + def test_on_energy_offset_change(self, instrument_model): + # WHEN + new_offset = 2.0 + + # THEN + instrument_model._energy_offset.value = new_offset + instrument_model._on_energy_offset_change() + + # EXPECT + for offset in instrument_model._energy_offsets: + assert offset.value == new_offset + + def test_on_resolution_model_change(self, instrument_model, resolution_model): + # WHEN + new_resolution_model = resolution_model + + # THEN + instrument_model._resolution_model = new_resolution_model + instrument_model._on_resolution_model_change() + + # EXPECT + assert instrument_model._resolution_model is new_resolution_model + + def test_on_background_model_change(self, instrument_model, background_model): + # WHEN + new_background_model = background_model + + # THEN + instrument_model._background_model = new_background_model + instrument_model._on_background_model_change() + + # EXPECT + assert instrument_model._background_model is new_background_model + + def test_repr_contains_expected_fields(self, instrument_model): + # WHEN THEN + repr_str = repr(instrument_model) + + # EXPECT + assert repr_str.startswith('InstrumentModel(') + assert f'unique_name={instrument_model.unique_name!r}' in repr_str + assert f'unit={instrument_model.unit}' in repr_str + assert 'Q_len=3' in repr_str + assert f'resolution_model={instrument_model._resolution_model!r}' in repr_str + assert f'background_model={instrument_model._background_model!r}' in repr_str + assert repr_str.endswith(')') diff --git a/tests/unit/easydynamics/sample_model/test_model_base.py b/tests/unit/easydynamics/sample_model/test_model_base.py index 31feb66a..27e38a03 100644 --- a/tests/unit/easydynamics/sample_model/test_model_base.py +++ b/tests/unit/easydynamics/sample_model/test_model_base.py @@ -113,6 +113,21 @@ def test_generate_component_collections_without_Q_warns(self, model_base): with pytest.warns(UserWarning, match='Q is not set'): model_base._generate_component_collections() + def test_fix_free_all_parameters(self, model_base): + # WHEN + model_base.fix_all_parameters() + + # THEN + for par in model_base.get_all_variables(): + assert par.fixed is True + + # WHEN + model_base.free_all_parameters() + + # THEN + for par in model_base.get_all_variables(): + assert par.fixed is False + def test_get_all_variables(self, model_base): # WHEN all_vars = model_base.get_all_variables() @@ -132,6 +147,41 @@ def test_get_all_variables(self, model_base): assert expected_var_display_names == retrieved_var_display_names assert len(all_vars) == 18 + def test_get_all_variables_with_Q_index(self, model_base): + # WHEN + all_vars = model_base.get_all_variables(Q_index=1) + + # THEN + expected_var_display_names = { + 'TestGaussian1 area', + 'TestGaussian1 center', + 'TestGaussian1 width', + 'TestLorentzian1 area', + 'TestLorentzian1 center', + 'TestLorentzian1 width', + } + + retrieved_var_display_names = {var.display_name for var in all_vars} + + assert expected_var_display_names == retrieved_var_display_names + assert len(all_vars) == 6 + + def test_get_all_variables_with_invalid_Q_index_raises(self, model_base): + # WHEN / THEN / EXPECT + with pytest.raises( + IndexError, + match='Q_index 5 is out of bounds for component collections of length 3', + ): + model_base.get_all_variables(Q_index=5) + + def test_get_all_variables_with_nonint_Q_index_raises(self, model_base): + # WHEN / THEN / EXPECT + with pytest.raises( + TypeError, + match='Q_index must be an int or None, got str', + ): + model_base.get_all_variables(Q_index='invalid_index') + def test_append_and_remove_and_clear_component(self, model_base): # WHEN new_component = Gaussian(unique_name='NewGaussian') From 78698be3032b8dbe7b7bee4bae80fac1ad963fb4 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Thu, 5 Feb 2026 20:32:47 +0100 Subject: [PATCH 03/17] Jump diffusion model (#95) * jump diffusion model * fix notebook * fix weakref? * fix weakref? * fix weakref??? * ... * weakref... * claude fixed it? * respond to PR comments * Move scale test to base model and add some formatting * fix typo * small fix --- docs/docs/tutorials/diffusion_model.ipynb | 2 - docs/docs/tutorials/sample_model.ipynb | 2 - .../sample_model/diffusion_model/__init__.py | 4 +- .../brownian_translational_diffusion.py | 73 ++-- .../diffusion_model/diffusion_model_base.py | 37 +- .../jump_translational_diffusion.py | 340 ++++++++++++++++++ src/easydynamics/sample_model/sample_model.py | 2 +- tests/conftest.py | 76 +++- .../test_brownian_translational_diffusion.py | 50 --- .../diffusion_model/test_diffusion_model.py | 12 + .../test_jump_translational_diffusion.py | 254 +++++++++++++ .../sample_model/test_model_base.py | 2 +- .../sample_model/test_resolution_model.py | 6 +- .../sample_model/test_sample_model.py | 7 +- 14 files changed, 740 insertions(+), 127 deletions(-) create mode 100644 src/easydynamics/sample_model/diffusion_model/jump_translational_diffusion.py create mode 100644 tests/unit/easydynamics/sample_model/diffusion_model/test_jump_translational_diffusion.py diff --git a/docs/docs/tutorials/diffusion_model.ipynb b/docs/docs/tutorials/diffusion_model.ipynb index 9277486e..be9ec8e1 100644 --- a/docs/docs/tutorials/diffusion_model.ipynb +++ b/docs/docs/tutorials/diffusion_model.ipynb @@ -39,13 +39,11 @@ "energy = np.linspace(-2, 2, 501)\n", "scale = 1.0\n", "diffusion_coefficient = 2.4e-9 # m^2/s\n", - "diffusion_unit = 'm**2/s'\n", "\n", "diffusion_model = BrownianTranslationalDiffusion(\n", " display_name='DiffusionModel',\n", " scale=scale,\n", " diffusion_coefficient=diffusion_coefficient,\n", - " diffusion_unit=diffusion_unit,\n", ")\n", "\n", "component_collections = diffusion_model.create_component_collections(Q)\n", diff --git a/docs/docs/tutorials/sample_model.ipynb b/docs/docs/tutorials/sample_model.ipynb index 802aff0b..5371f7df 100644 --- a/docs/docs/tutorials/sample_model.ipynb +++ b/docs/docs/tutorials/sample_model.ipynb @@ -48,12 +48,10 @@ "\n", "scale = 1.0\n", "diffusion_coefficient = 2.4e-9 # m^2/s\n", - "diffusion_unit = 'm**2/s'\n", "diffusion_model = BrownianTranslationalDiffusion(\n", " display_name='DiffusionModel',\n", " scale=scale,\n", " diffusion_coefficient=diffusion_coefficient,\n", - " diffusion_unit=diffusion_unit,\n", ")\n", "\n", "\n", diff --git a/src/easydynamics/sample_model/diffusion_model/__init__.py b/src/easydynamics/sample_model/diffusion_model/__init__.py index 6fd920dc..dc0a469c 100644 --- a/src/easydynamics/sample_model/diffusion_model/__init__.py +++ b/src/easydynamics/sample_model/diffusion_model/__init__.py @@ -2,9 +2,9 @@ # SPDX-License-Identifier: BSD-3-Clause from .brownian_translational_diffusion import BrownianTranslationalDiffusion -from .diffusion_model_base import DiffusionModelBase +from .jump_translational_diffusion import JumpTranslationalDiffusion __all__ = [ - 'DiffusionModelBase', 'BrownianTranslationalDiffusion', + 'JumpTranslationalDiffusion', ] diff --git a/src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py b/src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py index 749f8de4..d277d227 100644 --- a/src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py +++ b/src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py @@ -3,24 +3,20 @@ from typing import Dict from typing import List -from typing import Union import numpy as np import scipp as sc from easyscience.variable import DescriptorNumber from easyscience.variable import Parameter -from numpy.typing import ArrayLike from scipp.constants import hbar as scipp_hbar from easydynamics.sample_model.component_collection import ComponentCollection from easydynamics.sample_model.components import Lorentzian from easydynamics.sample_model.diffusion_model.diffusion_model_base import DiffusionModelBase +from easydynamics.utils.utils import Numeric +from easydynamics.utils.utils import Q_type from easydynamics.utils.utils import _validate_and_convert_Q -Numeric = Union[float, int] - -Q_type = np.ndarray | Numeric | list | ArrayLike - class BrownianTranslationalDiffusion(DiffusionModelBase): """Model of Brownian translational diffusion, consisting of a @@ -46,7 +42,6 @@ def __init__( unit: str | sc.Unit = 'meV', scale: Numeric = 1.0, diffusion_coefficient: Numeric = 1.0, - diffusion_unit: str = 'm**2/s', ): """Initialize a new BrownianTranslationalDiffusion model. @@ -62,65 +57,35 @@ def __init__( Defaults to "meV". scale : float or Parameter, optional Scale factor for the diffusion model. - diffusion_coefficient : float or Parameter, optional - Diffusion coefficient D. If a number is provided, - it is assumed to be in the unit given by diffusion_unit. + diffusion_coefficient : Number, optional + Diffusion coefficient D in m^2/s. Defaults to 1.0. - diffusion_unit : str, optional - Unit for the diffusion coefficient D. Default is m**2/s. - Options are 'meV*Å**2' or 'm**2/s' """ - if not isinstance(scale, (Parameter, Numeric)): + if not isinstance(scale, Numeric): raise TypeError('scale must be a number.') - if not isinstance(diffusion_coefficient, (Parameter, Numeric)): + if not isinstance(diffusion_coefficient, Numeric): raise TypeError('diffusion_coefficient must be a number.') - if not isinstance(diffusion_unit, str): - raise TypeError("diffusion_unit must be 'meV*Å**2' or 'm**2/s'.") - - if diffusion_unit == 'meV*Å**2' or diffusion_unit == 'meV*angstrom**2': - # In this case, hbar is absorbed in the unit of D - self._hbar = DescriptorNumber('hbar', 1.0) - elif diffusion_unit == 'm**2/s' or diffusion_unit == 'm^2/s': - self._hbar = DescriptorNumber.from_scipp('hbar', scipp_hbar) - else: - raise ValueError("diffusion_unit must be 'meV*Å**2' or 'm**2/s'.") - - scale = Parameter(name='scale', value=float(scale), fixed=False, min=0.0) - diffusion_coefficient = Parameter( name='diffusion_coefficient', value=float(diffusion_coefficient), fixed=False, - unit=diffusion_unit, + unit='m**2/s', ) super().__init__( display_name=display_name, unique_name=unique_name, unit=unit, + scale=scale, ) + self._hbar = DescriptorNumber.from_scipp('hbar', scipp_hbar) self._angstrom = DescriptorNumber('angstrom', 1e-10, unit='m') - self._scale = scale self._diffusion_coefficient = diffusion_coefficient - @property - def scale(self) -> Parameter: - """Get the scale parameter of the diffusion model. - - Returns - ------- - Parameter - Scale parameter. - """ - return self._scale - - @scale.setter - def scale(self, scale: Numeric) -> None: - """Set the scale parameter of the diffusion model.""" - if not isinstance(scale, (Numeric)): - raise TypeError('scale must be a number.') - self._scale.value = scale + # ------------------------------------------------------------------ + # Properties + # ------------------------------------------------------------------ @property def diffusion_coefficient(self) -> Parameter: @@ -136,10 +101,14 @@ def diffusion_coefficient(self) -> Parameter: @diffusion_coefficient.setter def diffusion_coefficient(self, diffusion_coefficient: Numeric) -> None: """Set the diffusion coefficient parameter D.""" - if not isinstance(diffusion_coefficient, (Numeric)): + if not isinstance(diffusion_coefficient, Numeric): raise TypeError('diffusion_coefficient must be a number.') self._diffusion_coefficient.value = diffusion_coefficient + # ------------------------------------------------------------------ + # Other methods + # ------------------------------------------------------------------ + def calculate_width(self, Q: Q_type) -> np.ndarray: """Calculate the half-width at half-maximum (HWHM) for the diffusion model. @@ -265,6 +234,10 @@ def create_component_collections( return component_collection_list + # ------------------------------------------------------------------ + # Private methods + # ------------------------------------------------------------------ + def _write_width_dependency_expression(self, Q: float) -> str: """Write the dependency expression for the width as a function of Q to make dependent Parameters. @@ -316,6 +289,10 @@ def _write_area_dependency_map_expression(self) -> Dict[str, DescriptorNumber]: 'scale': self.scale, } + # ------------------------------------------------------------------ + # dunder methods + # ------------------------------------------------------------------ + def __repr__(self): """String representation of the BrownianTranslationalDiffusion model. diff --git a/src/easydynamics/sample_model/diffusion_model/diffusion_model_base.py b/src/easydynamics/sample_model/diffusion_model/diffusion_model_base.py index 18b5bce8..a6711334 100644 --- a/src/easydynamics/sample_model/diffusion_model/diffusion_model_base.py +++ b/src/easydynamics/sample_model/diffusion_model/diffusion_model_base.py @@ -1,17 +1,14 @@ # SPDX-FileCopyrightText: 2025-2026 EasyDynamics contributors # SPDX-License-Identifier: BSD-3-Clause -import numpy as np import scipp as sc from easyscience.base_classes.model_base import ModelBase from easyscience.variable import DescriptorNumber -from numpy.typing import ArrayLike +from easyscience.variable import Parameter from scipp import UnitError from easydynamics.utils.utils import Numeric -Q_type = np.ndarray | Numeric | list | ArrayLike - class DiffusionModelBase(ModelBase): """Base class for constructing diffusion models.""" @@ -20,6 +17,7 @@ def __init__( self, display_name='MyDiffusionModel', unique_name: str | None = None, + scale: Numeric = 1.0, unit: str | sc.Unit = 'meV', ): """Initialize a new DiffusionModel. @@ -31,6 +29,10 @@ def __init__( unit : str or sc.Unit, optional Unit of the diffusion model. Defaults to "meV". """ + if not isinstance(scale, Numeric): + raise TypeError('scale must be a number.') + + scale = Parameter(name='scale', value=float(scale), fixed=False, min=0.0) try: test = DescriptorNumber(name='test', value=1, unit=unit) @@ -42,6 +44,11 @@ def __init__( super().__init__(display_name=display_name, unique_name=unique_name) self._unit = unit + self._scale = scale + + # ------------------------------------------------------------------ + # Properties + # ------------------------------------------------------------------ @property def unit(self) -> str: @@ -62,6 +69,28 @@ def unit(self, unit_str: str) -> None: ) ) # noqa: E501 + @property + def scale(self) -> Parameter: + """Get the scale parameter of the diffusion model. + + Returns + ------- + Parameter + Scale parameter. + """ + return self._scale + + @scale.setter + def scale(self, scale: Numeric) -> None: + """Set the scale parameter of the diffusion model.""" + if not isinstance(scale, Numeric): + raise TypeError('scale must be a number.') + self._scale.value = scale + + # ------------------------------------------------------------------ + # dunder methods + # ------------------------------------------------------------------ + def __repr__(self): """String representation of the Diffusion model.""" return f'{self.__class__.__name__}(display_name={self.display_name}, unit={self.unit})' diff --git a/src/easydynamics/sample_model/diffusion_model/jump_translational_diffusion.py b/src/easydynamics/sample_model/diffusion_model/jump_translational_diffusion.py new file mode 100644 index 00000000..8bb65480 --- /dev/null +++ b/src/easydynamics/sample_model/diffusion_model/jump_translational_diffusion.py @@ -0,0 +1,340 @@ +from typing import Dict +from typing import List + +import numpy as np +import scipp as sc +from easyscience.variable import DescriptorNumber +from easyscience.variable import Parameter +from scipp.constants import hbar as scipp_hbar + +from easydynamics.sample_model.component_collection import ComponentCollection +from easydynamics.sample_model.components import Lorentzian +from easydynamics.sample_model.diffusion_model.diffusion_model_base import DiffusionModelBase +from easydynamics.utils.utils import Numeric +from easydynamics.utils.utils import Q_type +from easydynamics.utils.utils import _validate_and_convert_Q + + +class JumpTranslationalDiffusion(DiffusionModelBase): + """Model of Jump translational diffusion, consisting of a Lorentzian + function for each Q-value, where the width is given by :math:`D + Q^2/(1+D t Q^2)`. Q is assumed to have units of 1/angstrom. Creates + ComponentCollections with Lorentzian components for given Q-values. + + Example usage: Q=np.linspace(0.5,2,7) energy=np.linspace(-2, 2, 501) + scale=1.0 diffusion_coefficient = 2.4e-9 # m^2/s + diffusion_model=JumpTranslationalDiffusion(display_name="DiffusionModel", + scale=scale, diffusion_coefficient= diffusion_coefficient) + component_collections=diffusion_model.create_component_collections(Q) + See also the examples. + """ + + def __init__( + self, + display_name: str | None = 'JumpTranslationalDiffusion', + unique_name: str | None = None, + unit: str | sc.Unit = 'meV', + scale: Numeric = 1.0, + diffusion_coefficient: Numeric = 1.0, + relaxation_time: Numeric = 1.0, + ): + """Initialize a new JumpTranslationalDiffusion model. + + Parameters + ---------- + display_name : str + Display name of the diffusion model. + unique_name : str or None + Unique name of the diffusion model. If None, a unique name + is automatically generated. + unit : str or sc.Unit, optional + Energy unit for the underlying Lorentzian components. + Defaults to "meV". + scale : float , optional + Scale factor for the diffusion model. + diffusion_coefficient : float , optional + Diffusion coefficient D in m^2/s. Defaults to 1.0. + relaxation_time : float , optional + Relaxation time t in ps. Defaults to 1.0. + """ + super().__init__( + display_name=display_name, + unique_name=unique_name, + unit=unit, + scale=scale, + ) + + if not isinstance(diffusion_coefficient, Numeric): + raise TypeError('diffusion_coefficient must be a number.') + + if not isinstance(relaxation_time, Numeric): + raise TypeError('relaxation_time must be a number.') + + diffusion_coefficient = Parameter( + name='diffusion_coefficient', + value=float(diffusion_coefficient), + fixed=False, + unit='m**2/s', + ) + + relaxation_time = Parameter( + name='relaxation_time', + value=float(relaxation_time), + fixed=False, + unit='ps', + ) + + self._hbar = DescriptorNumber.from_scipp('hbar', scipp_hbar) + self._angstrom = DescriptorNumber('angstrom', 1e-10, unit='m') + self._diffusion_coefficient = diffusion_coefficient + self._relaxation_time = relaxation_time + + ################################ + # Properties + ################################ + + @property + def diffusion_coefficient(self) -> Parameter: + """Get the diffusion coefficient parameter D. + + Returns + ------- + Parameter + Diffusion coefficient D. + """ + return self._diffusion_coefficient + + @diffusion_coefficient.setter + def diffusion_coefficient(self, diffusion_coefficient: Numeric) -> None: + """Set the diffusion coefficient parameter D.""" + if not isinstance(diffusion_coefficient, Numeric): + raise TypeError('diffusion_coefficient must be a number.') + self._diffusion_coefficient.value = diffusion_coefficient + + @property + def relaxation_time(self) -> Parameter: + """Get the relaxation time parameter t. + + Returns + ------- + Parameter + Relaxation time t. + """ + return self._relaxation_time + + @relaxation_time.setter + def relaxation_time(self, relaxation_time: Numeric) -> None: + """Set the relaxation time parameter t.""" + if not isinstance(relaxation_time, Numeric): + raise TypeError('relaxation_time must be a number.') + self._relaxation_time.value = relaxation_time + + ################################ + # Other methods + ################################ + + def calculate_width(self, Q: Q_type) -> np.ndarray: + """Calculate the half-width at half-maximum (HWHM) for the + diffusion model. Equation: :math:`\\Gamma(Q) = \\hbar D Q^2/(1+D + t Q^2)` + + Parameters + ---------- + Q : np.ndarray | Numeric | list | ArrayLike + Scattering vector in 1/angstrom + + Returns + ------- + np.ndarray + HWHM values in the unit of the model (e.g., meV). + """ + + Q = _validate_and_convert_Q(Q) + + unit_conversion_factor_numerator = ( + self._hbar * self.diffusion_coefficient / (self._angstrom**2) + ) + unit_conversion_factor_numerator.convert_unit(self.unit) + + numerator = unit_conversion_factor_numerator.value * Q**2 + + unit_conversion_factor_denominator = ( + self.diffusion_coefficient / self._angstrom**2 * self.relaxation_time + ) + unit_conversion_factor_denominator.convert_unit('dimensionless') + + denominator = 1 + unit_conversion_factor_denominator.value * Q**2 + + width = numerator / denominator + return width + + def calculate_EISF(self, Q: Q_type) -> np.ndarray: + """Calculate the Elastic Incoherent Structure Factor (EISF). + + Parameters + ---------- + Q : np.ndarray | Numeric | list | ArrayLike + Scattering vector in 1/angstrom + + Returns + ------- + np.ndarray + EISF values (dimensionless). + """ + Q = _validate_and_convert_Q(Q) + EISF = np.zeros_like(Q) + return EISF + + def calculate_QISF(self, Q: Q_type) -> np.ndarray: + """Calculate the Quasi-Elastic Incoherent Structure Factor + (QISF). + + Parameters + ---------- + Q : np.ndarray | Numeric | list | ArrayLike + Scattering vector in 1/angstrom + + Returns + ------- + np.ndarray + QISF values (dimensionless). + """ + + Q = _validate_and_convert_Q(Q) + QISF = np.ones_like(Q) + return QISF + + def create_component_collections( + self, + Q: Q_type, + component_display_name: str = 'Jump translational diffusion', + ) -> List[ComponentCollection]: + """Create ComponentCollection components for the diffusion model + at given Q values. + + Args: + ---------- + Q : Number, list, or np.ndarray + Scattering vector values. + component_display_name : str + Name of the Jump Diffusion Lorentzian component. + Returns + ------- + List[ComponentCollection] + List of ComponentCollections with Jump Diffusion + Lorentzian components. + """ + Q = _validate_and_convert_Q(Q) + + if not isinstance(component_display_name, str): + raise TypeError('component_name must be a string.') + + component_collection_list = [None] * len(Q) + # In more complex models, this is used to scale the area of the + # Lorentzians and the delta function. + QISF = self.calculate_QISF(Q) + + # Create a Lorentzian component for each Q-value, with width + # D*Q^2 and area equal to scale. No delta function, as the EISF + # is 0. + for i, Q_value in enumerate(Q): + component_collection_list[i] = ComponentCollection( + display_name=f'{self.display_name}_Q{Q_value:.2f}', unit=self.unit + ) + + lorentzian_component = Lorentzian( + display_name=component_display_name, + unit=self.unit, + ) + + # Make the width dependent on Q + dependency_expression = self._write_width_dependency_expression(Q[i]) + dependency_map = self._write_width_dependency_map_expression() + + lorentzian_component.width.make_dependent_on( + dependency_expression=dependency_expression, + dependency_map=dependency_map, + ) + + # Make the area dependent on Q + area_dependency_map = self._write_area_dependency_map_expression() + lorentzian_component.area.make_dependent_on( + dependency_expression=self._write_area_dependency_expression(QISF[i]), + dependency_map=area_dependency_map, + ) + + # Resolving the dependency can do weird things to the units, + # so we make sure it's correct. + lorentzian_component.width.convert_unit(self.unit) + component_collection_list[i].append_component(lorentzian_component) + + return component_collection_list + + ################################ + # Private methods + ################################ + + def _write_width_dependency_expression(self, Q: float) -> str: + """Write the dependency expression for the width as a function + of Q to make dependent Parameters. + + Parameters + ---------- + Q : float + Scattering vector in 1/angstrom + Returns + ------- + str + Dependency expression for the width. + """ + if not isinstance(Q, (float)): + raise TypeError('Q must be a float.') + + # Q is given as a float, so we need to add the units + return f'hbar * D* {Q} **2/(angstrom**2)/(1 + (D * t* {Q} **2/(angstrom**2)))' + + def _write_width_dependency_map_expression(self) -> Dict[str, DescriptorNumber]: + """Write the dependency map expression to make dependent + Parameters. + """ + return { + 'D': self._diffusion_coefficient, + 't': self._relaxation_time, + 'hbar': self._hbar, + 'angstrom': self._angstrom, + } + + def _write_area_dependency_expression(self, QISF: float) -> str: + """Write the dependency expression for the area to make + dependent Parameters. + + Returns + ------- + str + Dependency expression for the area. + """ + if not isinstance(QISF, (float)): + raise TypeError('QISF must be a float.') + + return f'{QISF} * scale' + + def _write_area_dependency_map_expression(self) -> Dict[str, DescriptorNumber]: + """Write the dependency map expression to make dependent + Parameters. + """ + return { + 'scale': self._scale, + } + + ################################ + # dunder methods + ################################ + + def __repr__(self): + """String representation of the JumpTranslationalDiffusion + model. + """ + return ( + f'JumpTranslationalDiffusion(display_name={self.display_name}, ' + f'diffusion_coefficient={self._diffusion_coefficient}, scale={self._scale})' + ) diff --git a/src/easydynamics/sample_model/sample_model.py b/src/easydynamics/sample_model/sample_model.py index cd1f2c2e..346bd7a4 100644 --- a/src/easydynamics/sample_model/sample_model.py +++ b/src/easydynamics/sample_model/sample_model.py @@ -5,7 +5,7 @@ import scipp as sc from easyscience.variable import Parameter -from easydynamics.sample_model.diffusion_model import DiffusionModelBase +from easydynamics.sample_model.diffusion_model.diffusion_model_base import DiffusionModelBase from easydynamics.sample_model.model_base import ModelBase from easydynamics.utils import _detailed_balance_factor from easydynamics.utils.utils import Numeric diff --git a/tests/conftest.py b/tests/conftest.py index aefc6c0b..d11735d3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,21 +5,73 @@ # TODO: remove once weakref bug is fixed -import easyscience.global_object +# import easyscience.global_object +# import pytest + + +# @pytest.fixture(autouse=True) +# def reset_global_object(): +# easyscience.global_object.map._clear() + +from unittest.mock import patch + import pytest -# from easyscience.global_object.map import Map +@pytest.fixture(autouse=True) +def patch_easyscience_map(): + """Patch the problematic Map methods.""" + from easyscience.global_object.map import Map -# @pytest.fixture(autouse=True) -# def reset_global_object(monkeypatch): -# # Before each test -# monkeypatch.setattr(easyscience.global_object, 'map', Map()) -# yield -# # After each test (cleanup) -# monkeypatch.setattr(easyscience.global_object, 'map', Map()) + # Store the original methods + original_add_vertex = Map.add_vertex + # original_vertices = Map.vertices + + def safe_add_vertex(self, obj: object, obj_type: str = None): + try: + return original_add_vertex(self, obj, obj_type) + except KeyError: + # Object was garbage collected during setup + name = obj.unique_name + # Clean up any partial state + if hasattr(self, '_Map__type_dict') and name in self._Map__type_dict: + del self._Map__type_dict[name] + if name in self._store: + del self._store[name] + + def safe_vertices(self): + """Safe version of vertices() that handles dictionary changes + during iteration.""" + max_retries = 3 + for attempt in range(max_retries): + try: + return list(self._store.keys()) + except RuntimeError as e: + if 'dictionary changed size during iteration' in str(e): + if attempt < max_retries - 1: + # Force cleanup and try again + import gc + gc.collect() + continue + else: + # Last attempt - return what we can get + try: + # Try to get keys in a different way + keys = [] + for k in list(self._store.data.keys()): + if k in self._store: + keys.append(k) + return keys + except: # noqa: E722 + return [] + else: + raise + return [] -@pytest.fixture(autouse=False) -def reset_global_object(): - easyscience.global_object.map._clear() + # Apply the patches + with ( + patch.object(Map, 'add_vertex', safe_add_vertex), + patch.object(Map, 'vertices', safe_vertices), + ): + yield diff --git a/tests/unit/easydynamics/sample_model/diffusion_model/test_brownian_translational_diffusion.py b/tests/unit/easydynamics/sample_model/diffusion_model/test_brownian_translational_diffusion.py index 7476755b..0d0963c0 100644 --- a/tests/unit/easydynamics/sample_model/diffusion_model/test_brownian_translational_diffusion.py +++ b/tests/unit/easydynamics/sample_model/diffusion_model/test_brownian_translational_diffusion.py @@ -37,7 +37,6 @@ def test_init_default(self, brownian_diffusion_model): 'unit': 123, 'scale': 1.0, 'diffusion_coefficient': 1.0, - 'diffusion_unit': 'm**2/s', }, UnitError, 'Invalid unit', @@ -47,7 +46,6 @@ def test_init_default(self, brownian_diffusion_model): 'unit': 123, 'scale': 'invalid', 'diffusion_coefficient': 1.0, - 'diffusion_unit': 'm**2/s', }, TypeError, 'scale must be a number', @@ -57,50 +55,16 @@ def test_init_default(self, brownian_diffusion_model): 'unit': 123, 'scale': 1.0, 'diffusion_coefficient': 'invalid', - 'diffusion_unit': 'm**2/s', }, TypeError, 'diffusion_coefficient must be a number', ), - ( - { - 'unit': 123, - 'scale': 1.0, - 'diffusion_coefficient': 1.0, - 'diffusion_unit': 123, - }, - TypeError, - 'diffusion_unit must be ', - ), ], ) def test_input_type_validation_raises(self, kwargs, expected_exception, expected_message): with pytest.raises(expected_exception, match=expected_message): BrownianTranslationalDiffusion(display_name='BrownianTranslationalDiffusion', **kwargs) - def test_diffusion_unit_value_error(self): - # WHEN THEN EXPECT - with pytest.raises(ValueError, match='diffusion_unit must be .'): - BrownianTranslationalDiffusion( - display_name='BrownianTranslationalDiffusion', - unit='meV', - scale=1.0, - diffusion_coefficient=1.0, - diffusion_unit='invalid_unit', - ) - - def test_scale_setter(self, brownian_diffusion_model): - # WHEN - brownian_diffusion_model.scale = 2.0 - - # THEN EXPECT - assert brownian_diffusion_model.scale.value == 2.0 - - def test_scale_setter_raises(self, brownian_diffusion_model): - # WHEN THEN EXPECT - with pytest.raises(TypeError, match='scale must be a number.'): - brownian_diffusion_model.scale = 'invalid' # Invalid type - def test_diffusion_coefficient_setter(self, brownian_diffusion_model): # WHEN brownian_diffusion_model.diffusion_coefficient = 3.0 @@ -136,20 +100,6 @@ def test_calculate_width(self, brownian_diffusion_model): expected_widths = 1.0 * unit_conversion_factor.value * (Q_values**2) np.testing.assert_allclose(widths, expected_widths, rtol=1e-5) - def test_calculate_width_diffusion_unit_mev_angstrom2(self): - # WHEN - diffusion_model = BrownianTranslationalDiffusion( - diffusion_coefficient=2.0, diffusion_unit='meV*Å**2' - ) - Q_values = np.array([0.1, 0.2, 0.3]) # Example Q values in Å^-1 - - # WHEN - widths = diffusion_model.calculate_width(Q_values) - - # THEN EXPECT - expected_widths = 2.0 * (Q_values**2) - np.testing.assert_allclose(widths, expected_widths, rtol=1e-5) - def test_calculate_EISF(self, brownian_diffusion_model): # WHEN Q_values = np.array([0.1, 0.2, 0.3]) # Example Q values in Å^-1 diff --git a/tests/unit/easydynamics/sample_model/diffusion_model/test_diffusion_model.py b/tests/unit/easydynamics/sample_model/diffusion_model/test_diffusion_model.py index e7bca65a..b8eb0956 100644 --- a/tests/unit/easydynamics/sample_model/diffusion_model/test_diffusion_model.py +++ b/tests/unit/easydynamics/sample_model/diffusion_model/test_diffusion_model.py @@ -23,3 +23,15 @@ def test_unit_setter_raises(self, diffusion_model): match='Unit is read-only. Use convert_unit to change the unit between allowed types', ): diffusion_model.unit = 'eV' + + def test_scale_setter(self, diffusion_model): + # WHEN + diffusion_model.scale = 2.0 + + # THEN EXPECT + assert diffusion_model.scale.value == 2.0 + + def test_scale_setter_raises(self, diffusion_model): + # WHEN THEN EXPECT + with pytest.raises(TypeError, match='scale must be a number.'): + diffusion_model.scale = 'invalid' # Invalid type diff --git a/tests/unit/easydynamics/sample_model/diffusion_model/test_jump_translational_diffusion.py b/tests/unit/easydynamics/sample_model/diffusion_model/test_jump_translational_diffusion.py new file mode 100644 index 00000000..90a842d6 --- /dev/null +++ b/tests/unit/easydynamics/sample_model/diffusion_model/test_jump_translational_diffusion.py @@ -0,0 +1,254 @@ +import numpy as np +import pytest +import scipp as sc +from easyscience.variable import DescriptorNumber +from scipp import UnitError +from scipp.constants import hbar as scipp_hbar + +from easydynamics.sample_model.diffusion_model.jump_translational_diffusion import ( + JumpTranslationalDiffusion, +) + +hbar_1 = DescriptorNumber('hbar', 1.0) +hbar = DescriptorNumber.from_scipp('hbar', scipp_hbar) +angstrom = DescriptorNumber('angstrom', 1e-10, unit='m') + + +class TestJumpTranslationalDiffusion: + @pytest.fixture + def jump_diffusion_model(self): + return JumpTranslationalDiffusion() + + def test_init_default(self, jump_diffusion_model): + # WHEN THEN EXPECT + assert jump_diffusion_model.display_name == 'JumpTranslationalDiffusion' + assert jump_diffusion_model.unit == 'meV' + assert jump_diffusion_model.scale.value == 1.0 + assert jump_diffusion_model.diffusion_coefficient.value == 1.0 + assert jump_diffusion_model.relaxation_time.value == 1.0 + + @pytest.mark.parametrize( + 'kwargs,expected_exception, expected_message', + [ + ( + { + 'unit': 123, + 'scale': 1.0, + 'diffusion_coefficient': 1.0, + 'relaxation_time': 1.0, + }, + UnitError, + 'Invalid unit', + ), + ( + { + 'unit': 'meV', + 'scale': 'invalid', + 'diffusion_coefficient': 1.0, + 'relaxation_time': 1.0, + }, + TypeError, + 'scale must be a number', + ), + ( + { + 'unit': 'meV', + 'scale': 1.0, + 'diffusion_coefficient': 'invalid', + 'relaxation_time': 1.0, + }, + TypeError, + 'diffusion_coefficient must be a number', + ), + ( + { + 'unit': 'meV', + 'scale': 1.0, + 'diffusion_coefficient': 1.0, + 'relaxation_time': 'invalid', + }, + TypeError, + 'relaxation_time must be a number', + ), + ], + ) + def test_input_type_validation_raises(self, kwargs, expected_exception, expected_message): + with pytest.raises(expected_exception, match=expected_message): + JumpTranslationalDiffusion(display_name='JumpTranslationalDiffusion', **kwargs) + + def test_diffusion_coefficient_setter(self, jump_diffusion_model): + # WHEN + jump_diffusion_model.diffusion_coefficient = 3.0 + + # THEN EXPECT + assert jump_diffusion_model.diffusion_coefficient.value == 3.0 + + def test_diffusion_coefficient_setter_raises(self, jump_diffusion_model): + # WHEN THEN EXPECT + with pytest.raises(TypeError, match='diffusion_coefficient must be a number.'): + jump_diffusion_model.diffusion_coefficient = 'invalid' # Invalid type + + def test_relaxation_time_setter(self, jump_diffusion_model): + # WHEN + jump_diffusion_model.relaxation_time = 2.5 + + # THEN EXPECT + assert jump_diffusion_model.relaxation_time.value == 2.5 + + def test_relaxation_time_setter_raises(self, jump_diffusion_model): + # WHEN THEN EXPECT + with pytest.raises(TypeError, match='relaxation_time must be a number.'): + jump_diffusion_model.relaxation_time = 'invalid' # Invalid type + + def test_calculate_width_type_error(self, jump_diffusion_model): + # WHEN THEN EXPECT + with pytest.raises(TypeError, match='Q must be '): + jump_diffusion_model.calculate_width(Q='invalid') # Invalid type + + def test_calculate_width(self, jump_diffusion_model): + "Test the calculation relying solely on a scipp implementation" + 'instead of our Parameters' + # WHEN + Q_values = sc.linspace('Q', 0.5, 1.5, num=6, unit='1/angstrom') + relaxation_time_sc = jump_diffusion_model.relaxation_time.value * sc.Unit( + jump_diffusion_model.relaxation_time.unit + ) + diffusion_coefficient_sc = jump_diffusion_model.diffusion_coefficient.value * sc.Unit( + jump_diffusion_model.diffusion_coefficient.unit + ) + + # THEN + widths = jump_diffusion_model.calculate_width(Q_values) + + denominator = diffusion_coefficient_sc * relaxation_time_sc * Q_values**2 + denominator = denominator.to(unit='1') + + # EXPECT + expected_widths = scipp_hbar * diffusion_coefficient_sc * (Q_values**2) / (1 + denominator) + + expected_widths = expected_widths.to(unit=jump_diffusion_model.unit) + + np.testing.assert_allclose(widths, expected_widths.values, rtol=1e-5) + + def test_calculate_EISF(self, jump_diffusion_model): + # WHEN + Q_values = np.array([0.1, 0.2, 0.3]) # Example Q values in Å^-1 + + # THEN + EISF = jump_diffusion_model.calculate_EISF(Q_values) + + # EXPECT + expected_EISF = np.zeros_like(Q_values) + np.testing.assert_array_equal(EISF, expected_EISF) + + def test_calculate_EISF_type_error(self, jump_diffusion_model): + # WHEN THEN EXPECT + with pytest.raises(TypeError, match='Q must be '): + jump_diffusion_model.calculate_EISF(Q='invalid') # Invalid type + + def test_calculate_QISF(self, jump_diffusion_model): + # WHEN + Q_values = np.array([0.1, 0.2, 0.3]) # Example Q values in Å^-1 + + # THEN + QISF = jump_diffusion_model.calculate_QISF(Q_values) + + # EXPECT + expected_QISF = np.ones_like(Q_values) + np.testing.assert_array_equal(QISF, expected_QISF) + + def test_calculate_QISF_type_error(self, jump_diffusion_model): + # WHEN THEN EXPECT + with pytest.raises(TypeError, match='Q must be '): + jump_diffusion_model.calculate_QISF(Q='invalid') # Invalid type + + @pytest.mark.parametrize( + 'Q', + [ + (0.5), + ([1.0, 2.0, 3.0]), + (np.array([1.0, 2.0, 3.0])), + ], + ids=[ + 'python_scalar', + 'python_list', + 'numpy_array', + ], + ) + def test_create_component_collections(self, jump_diffusion_model, Q): + # WHEN + + # THEN + component_collections = jump_diffusion_model.create_component_collections(Q=Q) + + # EXPECT + expected_widths = jump_diffusion_model.calculate_width(Q) + for model_index in range(len(component_collections)): + model = component_collections[model_index] + assert len(model.components) == 1 + component = model.components[0] + assert component.width.unit == jump_diffusion_model.unit + assert np.isclose(component.width.value, expected_widths[model_index]) + assert component.width.independent is False + + def test_create_component_collections_component_name_must_be_string( + self, jump_diffusion_model + ): + # WHEN THEN EXPECT + with pytest.raises(TypeError, match='component_name must be a string.'): + jump_diffusion_model.create_component_collections( + Q=np.array([0.1, 0.2, 0.3]), component_display_name=123 + ) + + def test_create_component_collections_Q_type_error(self, jump_diffusion_model): + # WHEN THEN EXPECT + with pytest.raises(TypeError, match='Q must be a '): + jump_diffusion_model.create_component_collections(Q='invalid') # Invalid type + + def test_create_component_collections_Q_1dimensional_error(self, jump_diffusion_model): + # WHEN THEN EXPECT + with pytest.raises(ValueError, match='Q must be a 1-dimensional array.'): + jump_diffusion_model.create_component_collections( + Q=np.array([[0.1, 0.2], [0.3, 0.4]]) + ) # Invalid shape + + def test_write_width_dependency_expression(self, jump_diffusion_model): + # WHEN THEN + expression = jump_diffusion_model._write_width_dependency_expression(0.5) + + # EXPECT + expected_expression = ( + 'hbar * D* 0.5 **2/(angstrom**2)/(1 + (D * t* 0.5 **2/(angstrom**2)))' + ) + assert expression == expected_expression + + def test_write_width_dependency_map_expression(self, jump_diffusion_model): + # WHEN THEN + expression_map = jump_diffusion_model._write_width_dependency_map_expression() + + # EXPECT + expected_map = { + 'D': jump_diffusion_model.diffusion_coefficient, + 't': jump_diffusion_model.relaxation_time, + 'hbar': jump_diffusion_model._hbar, + 'angstrom': jump_diffusion_model._angstrom, + } + + assert expression_map == expected_map + + def test_write_width_dependency_expression_raises(self, jump_diffusion_model): + with pytest.raises(TypeError, match='Q must be a float'): + jump_diffusion_model._write_width_dependency_expression('invalid') + + def test_write_area_dependency_expression_raises(self, jump_diffusion_model): + with pytest.raises(TypeError, match='QISF must be a float'): + jump_diffusion_model._write_area_dependency_expression('invalid') + + def test_repr(self, jump_diffusion_model): + # WHEN THEN + repr_str = repr(jump_diffusion_model) + + # EXPECT + assert 'JumpTranslationalDiffusion' in repr_str + assert 'diffusion_coefficient' in repr_str + assert 'scale=' in repr_str diff --git a/tests/unit/easydynamics/sample_model/test_model_base.py b/tests/unit/easydynamics/sample_model/test_model_base.py index 27e38a03..05591735 100644 --- a/tests/unit/easydynamics/sample_model/test_model_base.py +++ b/tests/unit/easydynamics/sample_model/test_model_base.py @@ -14,7 +14,7 @@ class TestModelBase: @pytest.fixture - def model_base(self, reset_global_object): + def model_base(self): component1 = Gaussian( display_name='TestGaussian1', area=1.0, diff --git a/tests/unit/easydynamics/sample_model/test_resolution_model.py b/tests/unit/easydynamics/sample_model/test_resolution_model.py index 120cbf9b..d45eee19 100644 --- a/tests/unit/easydynamics/sample_model/test_resolution_model.py +++ b/tests/unit/easydynamics/sample_model/test_resolution_model.py @@ -89,7 +89,7 @@ def test_init_raises_with_invalid_components(self, invalid_component, expected_e collection.append_component(invalid_component) ResolutionModel(components=collection) - def test_append_and_remove_and_clear_component(self, resolution_model, reset_global_object): + def test_append_and_remove_and_clear_component(self, resolution_model): # WHEN new_component = Gaussian(unique_name='NewGaussian') @@ -136,9 +136,7 @@ def test_append_component_collection(self, resolution_model): ], ids=['DeltaFunction', 'Polynomial'], ) - def test_append_invalid_component_type_raises( - self, resolution_model, invalid_component, reset_global_object - ): + def test_append_invalid_component_type_raises(self, resolution_model, invalid_component): # WHEN / THEN / EXPECT # appending a single component with pytest.raises( diff --git a/tests/unit/easydynamics/sample_model/test_sample_model.py b/tests/unit/easydynamics/sample_model/test_sample_model.py index 8a383ee3..e5f7a9a7 100644 --- a/tests/unit/easydynamics/sample_model/test_sample_model.py +++ b/tests/unit/easydynamics/sample_model/test_sample_model.py @@ -22,6 +22,7 @@ class TestSampleModel: def sample_model(self): component1 = Gaussian( display_name='TestGaussian1', + unique_name='TestGaussian1', area=1.0, center=0.0, width=1.0, @@ -29,6 +30,7 @@ def sample_model(self): ) component2 = Lorentzian( display_name='TestLorentzian1', + unique_name='TestLorentzian1', area=2.0, center=1.0, width=0.5, @@ -38,7 +40,9 @@ def sample_model(self): component_collection.append_component(component1) component_collection.append_component(component2) - diffusion_model = BrownianTranslationalDiffusion(display_name='DiffusionModel') + diffusion_model = BrownianTranslationalDiffusion( + display_name='DiffusionModel', unique_name='DiffusionModel' + ) sample_model = SampleModel( display_name='InitModel', @@ -52,6 +56,7 @@ def sample_model(self): return sample_model def test_init(self, sample_model): + # WHEN THEN model = sample_model From 809e3a79a3a065b66c1393239a8d2f2174990783 Mon Sep 17 00:00:00 2001 From: Andrew Sazonov Date: Fri, 6 Feb 2026 09:49:32 +0100 Subject: [PATCH 04/17] Reapply updated Copier templates and streamline developer workflow (#97) * Update pixi.lock * Reapply templates from v0.4.0 * pixi run fix-all * Switch to manual pre-commit and simplify tasks * Enable PyPI Trusted Publishing via OIDC * Reorder imports and widget magic in notebooks * Normalize notebook cell IDs --- .copier-answers.yml | 2 +- .github/actions/publish-to-pypi/action.yml | 11 +- .github/workflows/docs.yml | 7 +- .github/workflows/pypi-publish.yml | 16 +- .github/workflows/quality.yml | 8 + .github/workflows/tutorial-tests.yml | 2 +- .gitignore | 5 + .pre-commit-config.yaml | 47 ++-- README.md | 12 +- docs/docs/assets/stylesheets/extra.css | 23 +- docs/docs/installation-and-setup/index.md | 34 +-- docs/docs/introduction/index.md | 4 +- docs/docs/tutorials/components.ipynb | 3 +- docs/docs/tutorials/detailed_balance.ipynb | 6 +- docs/docs/tutorials/diffusion_model.ipynb | 2 +- docs/docs/tutorials/experiment.ipynb | 10 +- pixi.lock | 35 ++- pixi.toml | 110 +++++---- tools/update_github_labels.py | 254 +++++++++++++++++++++ 19 files changed, 437 insertions(+), 154 deletions(-) create mode 100644 tools/update_github_labels.py diff --git a/.copier-answers.yml b/.copier-answers.yml index b2bdcfa2..eeff466a 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,6 +1,6 @@ # WARNING: Do not edit this file manually. # Any changes will be overwritten by Copier. -_commit: v0.0.5 +_commit: v0.4.2 _src_path: gh:easyscience/templates app_docs_url: https://easyscience.github.io/dynamics-app app_doi: 10.5281/zenodo.18163581 diff --git a/.github/actions/publish-to-pypi/action.yml b/.github/actions/publish-to-pypi/action.yml index 522e3a02..719928d9 100644 --- a/.github/actions/publish-to-pypi/action.yml +++ b/.github/actions/publish-to-pypi/action.yml @@ -1,13 +1,14 @@ name: 'Publish to PyPI' -description: 'Publish a built distribution to PyPI using pypa/gh-action-pypi-publish' +description: 'Publish dist/ to PyPI via Trusted Publishing (OIDC)' inputs: - password: - description: 'PyPI API token (or password) for authentication' - required: true + packages_dir: + description: 'Directory containing the built packages to upload' + required: false + default: 'dist' runs: using: 'composite' steps: - uses: pypa/gh-action-pypi-publish@release/v1 with: - password: ${{ inputs.password }} + packages-dir: ${{ inputs.packages_dir }} diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 05fef7db..4056c1c0 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -107,11 +107,8 @@ jobs: - name: Pre-build site step run: pixi run python -c "import easydynamics" - # Convert Python scripts in the docs/docs/tutorials/ directory to Jupyter - # notebooks. - # This step also strips any existing output from the notebooks and - # prepares them for documentation. - - name: Convert tutorial scripts to notebooks + # Prepare the Jupyter notebooks for documentation (strip output, etc.). + - name: Prepare notebooks run: pixi run notebook-prepare # Execute all Jupyter notebooks to generate output cells (plots, tables, etc.). diff --git a/.github/workflows/pypi-publish.yml b/.github/workflows/pypi-publish.yml index 15a2c6ed..6e48e610 100644 --- a/.github/workflows/pypi-publish.yml +++ b/.github/workflows/pypi-publish.yml @@ -14,6 +14,10 @@ jobs: pypi-publish: runs-on: ubuntu-latest + permissions: + contents: read + id-token: write # IMPORTANT: this permission is mandatory for trusted publishing + steps: - name: Check-out repository uses: actions/checkout@v5 @@ -23,10 +27,18 @@ jobs: - name: Set up pixi uses: ./.github/actions/setup-pixi + # Build the Python package (to dist/ folder) - name: Create Python package run: pixi run default-build + # Publish the package to PyPI (from dist/ folder) + # Instead of publishing with personal access token, we use + # GitHub Actions OIDC to get a short-lived token from PyPI. + # New publisher must be previously configured in PyPI at + # https://pypi.org/manage/project/easydynamics/settings/publishing/ + # Use the following data: + # Owner: easyscience + # Repository name: dynamics-lib + # Workflow name: pypi-publish.yml - name: Publish to PyPI uses: ./.github/actions/publish-to-pypi - with: - password: ${{ secrets.PYPI_PASSWORD }} diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 1397f485..201dace4 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -79,6 +79,14 @@ jobs: continue-on-error: true shell: bash run: pixi run nonpy-format-check + # Check formatting of Jupyter Notebooks in the tutorials folder + - name: Prepare notebooks and check formatting + id: check_notebooks_formatting + continue-on-error: true + shell: bash + run: | + pixi run notebook-prepare + pixi run notebook-format-check # Add summary - name: Add quality checks summary diff --git a/.github/workflows/tutorial-tests.yml b/.github/workflows/tutorial-tests.yml index a3454fe7..55998847 100644 --- a/.github/workflows/tutorial-tests.yml +++ b/.github/workflows/tutorial-tests.yml @@ -46,7 +46,7 @@ jobs: shell: bash run: pixi run script-tests - - name: Convert tutorial scripts to notebooks + - name: Prepare notebooks shell: bash run: pixi run notebook-prepare diff --git a/.gitignore b/.gitignore index 7e0f2da3..f7ce4ac2 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,11 @@ __pycache__/ .venv/ .coverage +# PyInstaller +dist/ +build/ +*.spec + # MkDocs docs/site/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c3d471cd..007d2389 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,57 +1,54 @@ repos: - repo: local hooks: - # ----------------- - # Pre-commit checks - # ----------------- + # ------------- + # Manual checks + # ------------- - id: pixi-pyproject-check name: pixi run pyproject-check entry: pixi run pyproject-check language: system pass_filenames: false - stages: [pre-commit] + stages: [manual] - - id: pixi-py-lint-check-staged - name: pixi run py-lint-check-staged - entry: pixi run py-lint-check-pre + - id: pixi-py-lint-check + name: pixi run py-lint-check + entry: pixi run py-lint-check language: system pass_filenames: false - stages: [pre-commit] + stages: [manual] - - id: pixi-py-format-check-staged - name: pixi run py-format-check-staged - entry: pixi run py-format-check-pre + - id: pixi-py-format-check + name: pixi run py-format-check + entry: pixi run py-format-check language: system pass_filenames: false - stages: [pre-commit] + stages: [manual] - - id: pixi-nonpy-format-check-modified - name: pixi run nonpy-format-check-modified - entry: pixi run nonpy-format-check-modified + - id: pixi-nonpy-format-check + name: pixi run nonpy-format-check + entry: pixi run nonpy-format-check language: system pass_filenames: false - stages: [pre-commit] + stages: [manual] - id: pixi-docs-format-check name: pixi run docs-format-check entry: pixi run docs-format-check language: system pass_filenames: false - stages: [pre-commit] + stages: [manual] - # ---------------- - # Pre-push checks - # ---------------- - - id: pixi-nonpy-format-check - name: pixi run nonpy-format-check - entry: pixi run nonpy-format-check + - id: pixi-notebook-format-check + name: pixi run notebook-format-check + entry: pixi run notebook-format-check language: system pass_filenames: false - stages: [pre-push] + stages: [manual] - id: pixi-unit-tests name: pixi run unit-tests entry: pixi run unit-tests language: system pass_filenames: false - stages: [pre-push] + stages: [manual] diff --git a/README.md b/README.md index 2f55c56f..373d3828 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,18 @@

- + - + - EasyDynamics + EasyDynamics

-**EasyDynamics** is a scientific software for plotting and fitting qens -and ins powder data. +**EasyDynamics** is a scientific software for plotting and fitting QENS +and INS powder data. + + **EasyDynamics** is available both as a Python library and as a cross-platform desktop application. diff --git a/docs/docs/assets/stylesheets/extra.css b/docs/docs/assets/stylesheets/extra.css index 1c199950..a625be80 100644 --- a/docs/docs/assets/stylesheets/extra.css +++ b/docs/docs/assets/stylesheets/extra.css @@ -222,9 +222,27 @@ Adjust the margins and paddings to fit the defaults in MkDocs Material and do no width: 100% !important; display: flex !important; } + .jp-Notebook { padding: 0 !important; margin-top: -3em !important; + + /* Ensure notebook content stretches across the page */ + width: 100% !important; + max-width: 100% !important; + + /* mkdocs-material + some notebook HTML end up as flex */ + align-items: stretch !important; +} + +.jp-Notebook .jp-Cell { + /* Key: flex children often need min-width: 0 to prevent weird shrink */ + width: 100% !important; + max-width: 100% !important; + min-width: 0 !important; + + /* Removes jupyter cell paddings */ + padding-left: 0 !important; } /* Removes jupyter cell prefixes, like In[123]: */ @@ -234,11 +252,6 @@ Adjust the margins and paddings to fit the defaults in MkDocs Material and do no display: none !important; } -/* Removes jupyter cell paddings */ -.jp-Cell { - padding-left: 0 !important; -} - /* Removes jupyter output cell padding to align with input cell text */ .jp-RenderedText { padding-left: 0.85em !important; diff --git a/docs/docs/installation-and-setup/index.md b/docs/docs/installation-and-setup/index.md index 3513f6e9..420ef07e 100644 --- a/docs/docs/installation-and-setup/index.md +++ b/docs/docs/installation-and-setup/index.md @@ -8,8 +8,8 @@ icon: material/cog-box **Python 3.11** through **3.12**. To install and set up EasyDynamics, we recommend using -[**Pixi**](https://prefix.dev), a modern package manager for Windows, -macOS, and Linux. +[**Pixi**](https://pixi.prefix.dev), a modern package manager for +Windows, macOS, and Linux. !!! note "Main benefits of using Pixi" @@ -46,16 +46,9 @@ This section describes the simplest way to set up EasyDynamics using ```txt pixi add python=3.12 ``` -- Add the GNU Scientific Library (GSL) dependency: +- Add EasyDynamics to the Pixi environment from PyPI: ```txt - pixi add gsl - ``` -- Add EasyDynamics with the `visualization` extras, which include - optional dependencies used for simplified visualization of charts and - tables. This can be especially useful for running the Jupyter Notebook - examples: - ```txt - pixi add --pypi "easydynamics[visualization]" + pixi add --pypi easydynamics ``` - Add a Pixi task to run EasyDynamics commands easily: ```txt @@ -160,20 +153,7 @@ simply delete and recreate the environment. ### Installing from PyPI { #from-pypi } EasyDynamics is available on **PyPI (Python Package Index)** and can be -installed using `pip`. - -We recommend installing the latest release of EasyDynamics with the -`visualization` extras, which include optional dependencies used for -simplified visualization of charts and tables. This can be especially -useful for running the Jupyter Notebook examples. To do so, use the -following command: - -```txt -pip install 'easydynamics[visualization]' -``` - -If only the core functionality is needed, the library can be installed -simply with: +installed using `pip`. To do so, use the following command: ```txt pip install easydynamics @@ -216,10 +196,10 @@ example: pip install git+https://github.com/easyscience/dynamics-lib@develop ``` -To include extra dependencies (e.g., visualization): +To include extra dependencies (e.g., dev): ```txt -pip install 'easydynamics[visualization] @ git+https://github.com/easyscience/dynamics-lib@develop' +pip install 'easydynamics[dev] @ git+https://github.com/easyscience/dynamics-lib@develop' ``` ## How to Run Tutorials diff --git a/docs/docs/introduction/index.md b/docs/docs/introduction/index.md index 58240514..740d4b0d 100644 --- a/docs/docs/introduction/index.md +++ b/docs/docs/introduction/index.md @@ -6,8 +6,8 @@ icon: material/information-slab-circle ## Description -**EasyDynamics** is a scientific software for plotting and fitting qens -and ins powder data. +**EasyDynamics** is a scientific software for plotting and fitting QENS +and INS powder data. **EasyDynamics** is available both as a Python library and as a cross-platform desktop application. diff --git a/docs/docs/tutorials/components.ipynb b/docs/docs/tutorials/components.ipynb index 7815bcd4..83278fc4 100644 --- a/docs/docs/tutorials/components.ipynb +++ b/docs/docs/tutorials/components.ipynb @@ -21,6 +21,7 @@ "source": [ "import matplotlib.pyplot as plt\n", "import numpy as np\n", + "import scipp as sc\n", "\n", "from easydynamics.sample_model import DampedHarmonicOscillator\n", "from easydynamics.sample_model import DeltaFunction\n", @@ -105,8 +106,6 @@ "metadata": {}, "outputs": [], "source": [ - "import scipp as sc\n", - "\n", "x1 = sc.linspace(dim='x', start=-2.0, stop=2.0, num=100, unit='meV')\n", "x2 = sc.linspace(dim='x', start=-2.0 * 1e3, stop=2.0 * 1e3, num=101, unit='microeV')\n", "\n", diff --git a/docs/docs/tutorials/detailed_balance.ipynb b/docs/docs/tutorials/detailed_balance.ipynb index d09a2546..135894d3 100644 --- a/docs/docs/tutorials/detailed_balance.ipynb +++ b/docs/docs/tutorials/detailed_balance.ipynb @@ -23,11 +23,11 @@ "outputs": [], "source": [ "import matplotlib.pyplot as plt\n", - "\n", - "%matplotlib widget\n", "import numpy as np\n", "\n", - "from easydynamics.utils import _detailed_balance_factor as detailed_balance_factor" + "from easydynamics.utils import _detailed_balance_factor as detailed_balance_factor\n", + "\n", + "%matplotlib widget" ] }, { diff --git a/docs/docs/tutorials/diffusion_model.ipynb b/docs/docs/tutorials/diffusion_model.ipynb index be9ec8e1..f3d1571b 100644 --- a/docs/docs/tutorials/diffusion_model.ipynb +++ b/docs/docs/tutorials/diffusion_model.ipynb @@ -67,7 +67,7 @@ { "cell_type": "code", "execution_count": null, - "id": "a50c67ec", + "id": "3", "metadata": {}, "outputs": [], "source": [ diff --git a/docs/docs/tutorials/experiment.ipynb b/docs/docs/tutorials/experiment.ipynb index 6319c61f..f6e185df 100644 --- a/docs/docs/tutorials/experiment.ipynb +++ b/docs/docs/tutorials/experiment.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "markdown", - "id": "906b959a", + "id": "0", "metadata": {}, "source": [ "# Experiment\n", @@ -12,7 +12,7 @@ { "cell_type": "code", "execution_count": null, - "id": "c7d23add", + "id": "1", "metadata": {}, "outputs": [], "source": [ @@ -24,7 +24,7 @@ { "cell_type": "code", "execution_count": null, - "id": "2b7c5ca8", + "id": "2", "metadata": {}, "outputs": [], "source": [ @@ -38,7 +38,7 @@ { "cell_type": "code", "execution_count": null, - "id": "238ba6ee", + "id": "3", "metadata": {}, "outputs": [], "source": [ @@ -50,7 +50,7 @@ { "cell_type": "code", "execution_count": null, - "id": "bc32ab1f", + "id": "4", "metadata": {}, "outputs": [], "source": [ diff --git a/pixi.lock b/pixi.lock index da8aee45..f51bc65b 100644 --- a/pixi.lock +++ b/pixi.lock @@ -5,8 +5,6 @@ environments: - url: https://conda.anaconda.org/conda-forge/ indexes: - https://pypi.org/simple - options: - pypi-prerelease-mode: if-necessary-or-explicit packages: linux-64: - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 @@ -80,7 +78,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/b8/01/74922a1c552137c05a41fee0c61153753dddc9117d19c7c5902c146c25ab/copier-9.11.3-py3-none-any.whl - - pypi: git+https://github.com/easyscience/corelib.git#bd106537fcf522336aa0176aa6ccf215be8a5b86 + - pypi: git+https://github.com/easyscience/corelib.git?rev=develop#bd106537fcf522336aa0176aa6ccf215be8a5b86 - pypi: https://files.pythonhosted.org/packages/ea/b4/694159c15c52b9f7ec7adf49d50e5f8ee71d3e9ef38adb4445d13dd56c20/coverage-7.13.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f2/f2/728f041460f1b9739b85ee23b45fa5a505962ea11fd85bdbe2a02b021373/darkdetect-0.8.0-py3-none-any.whl @@ -335,7 +333,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/b8/01/74922a1c552137c05a41fee0c61153753dddc9117d19c7c5902c146c25ab/copier-9.11.3-py3-none-any.whl - - pypi: git+https://github.com/easyscience/corelib.git#bd106537fcf522336aa0176aa6ccf215be8a5b86 + - pypi: git+https://github.com/easyscience/corelib.git?rev=develop#bd106537fcf522336aa0176aa6ccf215be8a5b86 - pypi: https://files.pythonhosted.org/packages/ce/8a/87af46cccdfa78f53db747b09f5f9a21d5fc38d796834adac09b30a8ce74/coverage-7.13.1-cp312-cp312-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f2/f2/728f041460f1b9739b85ee23b45fa5a505962ea11fd85bdbe2a02b021373/darkdetect-0.8.0-py3-none-any.whl @@ -590,7 +588,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/b8/01/74922a1c552137c05a41fee0c61153753dddc9117d19c7c5902c146c25ab/copier-9.11.3-py3-none-any.whl - - pypi: git+https://github.com/easyscience/corelib.git#bd106537fcf522336aa0176aa6ccf215be8a5b86 + - pypi: git+https://github.com/easyscience/corelib.git?rev=develop#bd106537fcf522336aa0176aa6ccf215be8a5b86 - pypi: https://files.pythonhosted.org/packages/82/a8/6e22fdc67242a4a5a153f9438d05944553121c8f4ba70cb072af4c41362e/coverage-7.13.1-cp312-cp312-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f2/f2/728f041460f1b9739b85ee23b45fa5a505962ea11fd85bdbe2a02b021373/darkdetect-0.8.0-py3-none-any.whl @@ -838,7 +836,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/b8/01/74922a1c552137c05a41fee0c61153753dddc9117d19c7c5902c146c25ab/copier-9.11.3-py3-none-any.whl - - pypi: git+https://github.com/easyscience/corelib.git#bd106537fcf522336aa0176aa6ccf215be8a5b86 + - pypi: git+https://github.com/easyscience/corelib.git?rev=develop#bd106537fcf522336aa0176aa6ccf215be8a5b86 - pypi: https://files.pythonhosted.org/packages/fa/dc/7282856a407c621c2aad74021680a01b23010bb8ebf427cf5eacda2e876f/coverage-7.13.1-cp312-cp312-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f2/f2/728f041460f1b9739b85ee23b45fa5a505962ea11fd85bdbe2a02b021373/darkdetect-0.8.0-py3-none-any.whl @@ -1031,8 +1029,6 @@ environments: - url: https://conda.anaconda.org/conda-forge/ indexes: - https://pypi.org/simple - options: - pypi-prerelease-mode: if-necessary-or-explicit packages: linux-64: - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 @@ -1106,7 +1102,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/b8/01/74922a1c552137c05a41fee0c61153753dddc9117d19c7c5902c146c25ab/copier-9.11.3-py3-none-any.whl - - pypi: git+https://github.com/easyscience/corelib.git#bd106537fcf522336aa0176aa6ccf215be8a5b86 + - pypi: git+https://github.com/easyscience/corelib.git?rev=develop#bd106537fcf522336aa0176aa6ccf215be8a5b86 - pypi: https://files.pythonhosted.org/packages/f7/7c/347280982982383621d29b8c544cf497ae07ac41e44b1ca4903024131f55/coverage-7.13.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f2/f2/728f041460f1b9739b85ee23b45fa5a505962ea11fd85bdbe2a02b021373/darkdetect-0.8.0-py3-none-any.whl @@ -1362,7 +1358,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl - pypi: https://files.pythonhosted.org/packages/b8/01/74922a1c552137c05a41fee0c61153753dddc9117d19c7c5902c146c25ab/copier-9.11.3-py3-none-any.whl - - pypi: git+https://github.com/easyscience/corelib.git#bd106537fcf522336aa0176aa6ccf215be8a5b86 + - pypi: git+https://github.com/easyscience/corelib.git?rev=develop#bd106537fcf522336aa0176aa6ccf215be8a5b86 - pypi: https://files.pythonhosted.org/packages/b4/9b/77baf488516e9ced25fc215a6f75d803493fc3f6a1a1227ac35697910c2a/coverage-7.13.1-cp311-cp311-macosx_10_9_x86_64.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f2/f2/728f041460f1b9739b85ee23b45fa5a505962ea11fd85bdbe2a02b021373/darkdetect-0.8.0-py3-none-any.whl @@ -1618,7 +1614,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/b8/01/74922a1c552137c05a41fee0c61153753dddc9117d19c7c5902c146c25ab/copier-9.11.3-py3-none-any.whl - - pypi: git+https://github.com/easyscience/corelib.git#bd106537fcf522336aa0176aa6ccf215be8a5b86 + - pypi: git+https://github.com/easyscience/corelib.git?rev=develop#bd106537fcf522336aa0176aa6ccf215be8a5b86 - pypi: https://files.pythonhosted.org/packages/d7/cd/7ab01154e6eb79ee2fab76bf4d89e94c6648116557307ee4ebbb85e5c1bf/coverage-7.13.1-cp311-cp311-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f2/f2/728f041460f1b9739b85ee23b45fa5a505962ea11fd85bdbe2a02b021373/darkdetect-0.8.0-py3-none-any.whl @@ -1867,7 +1863,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/b8/01/74922a1c552137c05a41fee0c61153753dddc9117d19c7c5902c146c25ab/copier-9.11.3-py3-none-any.whl - - pypi: git+https://github.com/easyscience/corelib.git#bd106537fcf522336aa0176aa6ccf215be8a5b86 + - pypi: git+https://github.com/easyscience/corelib.git?rev=develop#bd106537fcf522336aa0176aa6ccf215be8a5b86 - pypi: https://files.pythonhosted.org/packages/27/56/c216625f453df6e0559ed666d246fcbaaa93f3aa99eaa5080cea1229aa3d/coverage-7.13.1-cp311-cp311-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f2/f2/728f041460f1b9739b85ee23b45fa5a505962ea11fd85bdbe2a02b021373/darkdetect-0.8.0-py3-none-any.whl @@ -2061,8 +2057,6 @@ environments: - url: https://conda.anaconda.org/conda-forge/ indexes: - https://pypi.org/simple - options: - pypi-prerelease-mode: if-necessary-or-explicit packages: linux-64: - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 @@ -2136,7 +2130,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/b8/01/74922a1c552137c05a41fee0c61153753dddc9117d19c7c5902c146c25ab/copier-9.11.3-py3-none-any.whl - - pypi: git+https://github.com/easyscience/corelib.git#bd106537fcf522336aa0176aa6ccf215be8a5b86 + - pypi: git+https://github.com/easyscience/corelib.git?rev=develop#bd106537fcf522336aa0176aa6ccf215be8a5b86 - pypi: https://files.pythonhosted.org/packages/ea/b4/694159c15c52b9f7ec7adf49d50e5f8ee71d3e9ef38adb4445d13dd56c20/coverage-7.13.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f2/f2/728f041460f1b9739b85ee23b45fa5a505962ea11fd85bdbe2a02b021373/darkdetect-0.8.0-py3-none-any.whl @@ -2391,7 +2385,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/b8/01/74922a1c552137c05a41fee0c61153753dddc9117d19c7c5902c146c25ab/copier-9.11.3-py3-none-any.whl - - pypi: git+https://github.com/easyscience/corelib.git#bd106537fcf522336aa0176aa6ccf215be8a5b86 + - pypi: git+https://github.com/easyscience/corelib.git?rev=develop#bd106537fcf522336aa0176aa6ccf215be8a5b86 - pypi: https://files.pythonhosted.org/packages/ce/8a/87af46cccdfa78f53db747b09f5f9a21d5fc38d796834adac09b30a8ce74/coverage-7.13.1-cp312-cp312-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f2/f2/728f041460f1b9739b85ee23b45fa5a505962ea11fd85bdbe2a02b021373/darkdetect-0.8.0-py3-none-any.whl @@ -2646,7 +2640,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/b8/01/74922a1c552137c05a41fee0c61153753dddc9117d19c7c5902c146c25ab/copier-9.11.3-py3-none-any.whl - - pypi: git+https://github.com/easyscience/corelib.git#bd106537fcf522336aa0176aa6ccf215be8a5b86 + - pypi: git+https://github.com/easyscience/corelib.git?rev=develop#bd106537fcf522336aa0176aa6ccf215be8a5b86 - pypi: https://files.pythonhosted.org/packages/82/a8/6e22fdc67242a4a5a153f9438d05944553121c8f4ba70cb072af4c41362e/coverage-7.13.1-cp312-cp312-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f2/f2/728f041460f1b9739b85ee23b45fa5a505962ea11fd85bdbe2a02b021373/darkdetect-0.8.0-py3-none-any.whl @@ -2894,7 +2888,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/b8/01/74922a1c552137c05a41fee0c61153753dddc9117d19c7c5902c146c25ab/copier-9.11.3-py3-none-any.whl - - pypi: git+https://github.com/easyscience/corelib.git#bd106537fcf522336aa0176aa6ccf215be8a5b86 + - pypi: git+https://github.com/easyscience/corelib.git?rev=develop#bd106537fcf522336aa0176aa6ccf215be8a5b86 - pypi: https://files.pythonhosted.org/packages/fa/dc/7282856a407c621c2aad74021680a01b23010bb8ebf427cf5eacda2e876f/coverage-7.13.1-cp312-cp312-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f2/f2/728f041460f1b9739b85ee23b45fa5a505962ea11fd85bdbe2a02b021373/darkdetect-0.8.0-py3-none-any.whl @@ -4091,7 +4085,7 @@ packages: requires_python: '>=3.5' - pypi: ./ name: easydynamics - version: 0.1.1+devdirty2 + version: 0.1.0+devdirty6 sha256: de299c914d4a865b9e2fdefa5e3947f37b1f26f73ff9087f7918ee417f3dd288 requires_dist: - darkdetect @@ -4134,7 +4128,8 @@ packages: - validate-pyproject[all] ; extra == 'dev' - versioningit ; extra == 'dev' requires_python: '>=3.11' -- pypi: git+https://github.com/easyscience/corelib.git#bd106537fcf522336aa0176aa6ccf215be8a5b86 + editable: true +- pypi: git+https://github.com/easyscience/corelib.git?rev=develop#bd106537fcf522336aa0176aa6ccf215be8a5b86 name: easyscience version: 2.1.0 requires_dist: diff --git a/pixi.toml b/pixi.toml index baa0ea35..d280c259 100644 --- a/pixi.toml +++ b/pixi.toml @@ -76,41 +76,41 @@ default = { features = ['default', 'py-max'] } [tasks] +################## # 🧪 Testing Tasks -unit-tests = 'python -m pytest tests/unit/ --color=yes --cov= --cov-report=' +################## + +unit-tests = 'python -m pytest tests/unit/ --color=yes -v' integration-tests = 'python -m pytest tests/integration/ --color=yes -n auto -v' notebook-tests = 'python -m pytest --nbmake docs/docs/tutorials/ --nbmake-timeout=600 --color=yes -n auto -v' script-tests = 'python -m pytest tools/test_scripts.py --color=yes -n auto -v' test = { depends-on = ['unit-tests'] } +########### # ✔️ Checks +########### + pyproject-check = 'python -m validate_pyproject pyproject.toml' -py-lint-check-pre = "python -m ruff check" -py-lint-check = 'pixi run py-lint-check-pre .' -py-format-check-pre = "python -m ruff format --check" -py-format-check = "pixi run py-format-check-pre ." -nonpy-format-check-pre = "npx prettier --list-different --config=prettierrc.toml" -nonpy-format-check-modified = "pixi run nonpy-format-check-pre $(git diff --diff-filter=d --name-only HEAD | grep -E '\\.(json|ya?ml|toml|md|css|html)$' || echo .)" -nonpy-format-check = "pixi run nonpy-format-check-pre ." +docs-format-check = 'docformatter --check src/ docs/docs/tutorials/' notebook-format-check = 'nbqa ruff docs/docs/tutorials/' -docs-format-check = 'docformatter src/ docs/docs/tutorials/ --check' +py-lint-check = 'ruff check src/ tests/ docs/docs/tutorials/' +py-format-check = "ruff format --check src/ tests/ docs/docs/tutorials/" +nonpy-format-check = "npx prettier --list-different --config=prettierrc.toml --ignore-unknown ." +nonpy-format-check-modified = "python tools/nonpy_prettier_modified.py" -check = { depends-on = [ - 'docs-format-check', - 'py-format-check', - 'py-lint-check', - 'nonpy-format-check-modified', -] } +check = 'pre-commit run --hook-stage manual --all-files' +########## # 🛠️ Fixes -py-lint-fix = 'pixi run py-lint-check --fix' -#py-format-fix = "python -m ruff format $(git diff --cached --name-only -- '*.py')" -py-format-fix = "python -m ruff format" -nonpy-format-fix = 'pixi run nonpy-format-check --write' -nonpy-format-fix-modified = "pixi run nonpy-format-check-modified --write" -notebook-format-fix = 'pixi run notebook-format-check --fix' -docs-format-fix = 'docformatter src/ docs/docs/tutorials/ --in-place' +########## + +docs-format-fix = 'docformatter --in-place src/ docs/docs/tutorials/' +notebook-format-fix = 'nbqa ruff --fix docs/docs/tutorials/' +py-lint-fix = 'ruff check --fix src/ tests/ docs/docs/tutorials/' +py-format-fix = "ruff format src/ tests/ docs/docs/tutorials/" +nonpy-format-fix = 'npx prettier --write --list-different --config=prettierrc.toml --ignore-unknown .' +nonpy-format-fix-modified = "python tools/nonpy_prettier_modified.py --write" success-message-fix = 'echo "✅ All code auto-formatting steps have been applied."' fix = { depends-on = [ @@ -118,10 +118,14 @@ fix = { depends-on = [ 'docs-format-fix', 'py-lint-fix', 'nonpy-format-fix', + 'notebook-format-fix', 'success-message-fix', ] } +#################### # 🧮 Code Complexity +#################### + complexity-check = 'radon cc -s src/' complexity-check-json = 'radon cc -s -j src/' maintainability-check = 'radon mi src/' @@ -129,8 +133,11 @@ maintainability-check-json = 'radon mi -j src/' raw-metrics = 'radon raw -s src/' raw-metrics-json = 'radon raw -s -j src/' +############# # 📊 Coverage -unit-tests-coverage = 'python -m pytest tests/unit/ --color=yes --cov=src/easydynamics --cov-report=term-missing' +############# + +unit-tests-coverage = 'pixi run unit-tests --cov=src/easydynamics --cov-report=term-missing' integration-tests-coverage = 'pixi run integration-tests --cov=src/easydynamics --cov-report=term-missing' docstring-coverage = 'interrogate -c pyproject.toml src/easydynamics' @@ -140,19 +147,25 @@ cov = { depends-on = [ 'integration-tests-coverage', ] } +######################## # 📓 Notebook Management +######################## + notebook-convert = 'jupytext docs/docs/tutorials/*.py --from py:percent --to ipynb' notebook-strip = 'nbstripout docs/docs/tutorials/*.ipynb' notebook-tweak = 'python tools/tweak_notebooks.py tutorials/' notebook-exec = 'python -m pytest --nbmake docs/docs/tutorials/ --nbmake-timeout=600 --overwrite --color=yes -n auto -v' notebook-prepare = { depends-on = [ - ###'notebook-convert', + #'notebook-convert', 'notebook-strip', - ###'notebook-tweak', + #'notebook-tweak', ] } +######################## # 📚 Documentation Tasks +######################## + docs-vars = "JUPYTER_PLATFORM_DIRS=1 PYTHONWARNINGS='ignore::RuntimeWarning'" docs-pre = "pixi run docs-vars python -m mkdocs" docs-serve = "pixi run docs-pre serve -f docs/mkdocs.yml" @@ -163,29 +176,19 @@ docs-build-local = "pixi run docs-build --no-directory-urls" docs-deploy-pre = 'mike deploy -F docs/mkdocs.yml --push --branch gh-pages --update-aliases --alias-type redirect' docs-set-default-pre = 'mike set-default -F docs/mkdocs.yml --push --branch gh-pages' -docs-update-assets = 'pixi run python tools/update_docs_assets.py' +docs-update-assets = 'python tools/update_docs_assets.py' +############################## # 📦 Template Management Tasks -copier-copy = "pixi run copier copy gh:easyscience/templates . --data-file ../dynamics/.copier-answers.yml --data template_type=lib" -copier-recopy = "pixi run copier recopy --data-file ../dynamics/.copier-answers.yml --data template_type=lib" -copier-update = "pixi run copier update --data-file ../dynamics/.copier-answers.yml --data template_type=lib" +############################## -# 🚀 Development & Build Tasks -default-build = 'python -m build' -dist-build = 'python -m build --wheel --outdir dist' +copier-copy = "copier copy gh:easyscience/templates . --data-file ../dynamics/.copier-answers.yml --data template_type=lib" +copier-recopy = "copier recopy --data-file ../dynamics/.copier-answers.yml --data template_type=lib" +copier-update = "copier update --data-file ../dynamics/.copier-answers.yml --data template_type=lib" -npm-config = 'npm config set registry https://registry.npmjs.org/' -prettier-install = 'npm install --no-save --no-audit --no-fund prettier prettier-plugin-toml' - -clean-pycache = "find . -type d -name '__pycache__' -prune -exec rm -rf '{}' +" -spdx-update = 'python tools/update_spdx.py' - -# Run like a real commit: staged files only (almost) -pre-commit-check = 'pre-commit run --hook-stage pre-commit' -# CI check: lint/format everything -pre-commit-check-all = 'pre-commit run --all-files --hook-stage pre-commit' -# Pre-push check: lint/format everything -pre-push-check = 'pre-commit run --all-files --hook-stage pre-push' +##################### +# 🪝 Pre-commit Hooks +##################### pre-commit-clean = 'pre-commit clean' pre-commit-install = 'pre-commit install --hook-type pre-commit --hook-type pre-push --overwrite' @@ -196,11 +199,28 @@ pre-commit-setup = { depends-on = [ 'pre-commit-install', ] } +#################################### +# 🚀 Other Development & Build Tasks +#################################### + +github-labels = 'python tools/update_github_labels.py' + +default-build = 'python -m build' +dist-build = 'python -m build --wheel --outdir dist' + +npm-config = 'npm config set registry https://registry.npmjs.org/' +prettier-install = 'npm install --no-save --no-audit --no-fund prettier prettier-plugin-toml' + +clean-pycache = "find . -type d -name '__pycache__' -prune -exec rm -rf '{}' +" +spdx-update = 'python tools/update_spdx.py' + post-install = { depends-on = [ 'npm-config', 'prettier-install', - 'pre-commit-setup', + #'pre-commit-setup', ] } +########################## # 🔗 Main Package Shortcut +########################## easydynamics = 'python -m easydynamics' diff --git a/tools/update_github_labels.py b/tools/update_github_labels.py new file mode 100644 index 00000000..a18043d0 --- /dev/null +++ b/tools/update_github_labels.py @@ -0,0 +1,254 @@ +""" +Set/update GitHub labels for current or specified easyscience +repository. + +Requires: + - gh CLI installed + - gh auth login completed + +Usage: + python update_github_labels.py + python update_github_labels.py --dry-run + python update_github_labels.py --repo easyscience/my-repo + python update_github_labels.py --repo easyscience/my-repo --dry-run +""" + +from __future__ import annotations + +import argparse +import json +import shlex +import subprocess +import sys +from dataclasses import dataclass +from typing import Iterable + + +EASYSCIENCE_ORG = 'easyscience' + + +# --- Label definitions ----------------------------------------------------------- + +BASIC_GITHUB_LABELS = [ + 'bug', + 'documentation', + 'duplicate', + 'enhancement', + 'good first issue', + 'help wanted', + 'invalid', + 'question', + 'wontfix', +] + +NEW_BASIC_LABEL_NAMES = [ + '[scope] bug', + '[scope] documentation', + '[maintainer] duplicate', + '[scope] enhancement', + '[maintainer] good first issue', + '[maintainer] help wanted', + '[maintainer] invalid', + '[maintainer] question', + '[maintainer] wontfix', +] + +SCOPE_LABELS = [ + ('bug', 'Bug report or fix (major.minor.PATCH)'), + ('documentation', 'Documentation only changes (major.minor.patch.POST)'), + ('enhancement', 'Adds/improves features (major.MINOR.patch)'), + ('maintenance', 'Code/tooling cleanup, no feature or bugfix (major.minor.PATCH)'), + ('significant', 'Breaking or major changes (MAJOR.minor.patch)'), + ('⚠️ label needed', 'Automatically added to issues and PRs without a [scope] label'), +] + +MAINTAINER_LABELS = [ + ('duplicate', 'Already reported or submitted'), + ('good first issue', 'Good entry-level issue for newcomers'), + ('help wanted', 'Needs additional help to resolve or implement'), + ('invalid', 'Invalid, incorrect or outdated'), + ('question', 'Needs clarification, discussion, or more information'), + ('wontfix', 'Will not be fixed or continued'), +] + +PRIORITY_LABELS = [ + ('lowest', 'Very low urgency'), + ('low', 'Low importance'), + ('medium', 'Normal/default priority'), + ('high', 'Should be prioritized soon'), + ('highest', 'Urgent. Needs attention ASAP'), + ('⚠️ label needed', 'Automatically added to issues without a [priority] label'), +] + +BOT_LABEL = ( + '[bot] pull request', + 'Automated release PR. Excluded from changelog/versioning', +) + +COLORS = { + 'scope': 'd73a4a', + 'maintainer': '0e8a16', + 'priority': 'fbca04', + 'bot': '5319e7', +} + + +# --- Helpers -------------------------------------------------------------------- + + +@dataclass(frozen=True) +class CmdResult: + returncode: int + stdout: str + stderr: str + + +def run_cmd(args: list[str], *, dry_run: bool, check: bool = True) -> CmdResult: + """Run a command (or print it in dry-run mode).""" + cmd_str = ' '.join(shlex.quote(a) for a in args) + + if dry_run: + print(f'{cmd_str}') + return CmdResult(0, '', '') + + proc = subprocess.run( + args, + text=True, + capture_output=True, + ) + res = CmdResult(proc.returncode, proc.stdout.strip(), proc.stderr.strip()) + + if check and proc.returncode != 0: + raise RuntimeError(f'Command failed ({proc.returncode}): {cmd_str}\n{res.stderr}') + + return res + + +def get_current_repo_name_with_owner() -> str: + res = subprocess.run( + ['gh', 'repo', 'view', '--json', 'nameWithOwner'], + text=True, + capture_output=True, + check=True, + ) + data = json.loads(res.stdout) + nwo = data.get('nameWithOwner') + if not nwo or '/' not in nwo: + raise RuntimeError('Could not determine current repository name') + return nwo + + +def try_rename_label(repo: str, old: str, new: str, *, dry_run: bool) -> None: + try: + run_cmd( + ['gh', 'label', 'edit', old, '--name', new, '--repo', repo], + dry_run=dry_run, + ) + print(f'Rename: {old!r} → {new!r}') + except Exception: + print(f'Skip rename (label not found): {old!r}') + + +def upsert_label( + repo: str, + name: str, + color: str, + description: str, + *, + dry_run: bool, +) -> None: + run_cmd( + [ + 'gh', + 'label', + 'create', + name, + '--color', + color, + '--description', + description, + '--force', + '--repo', + repo, + ], + dry_run=dry_run, + ) + print(f'Upsert label: {name!r}') + + +def upsert_group( + repo: str, + prefix: str, + color: str, + items: Iterable[tuple[str, str]], + *, + dry_run: bool, +) -> None: + for short, desc in items: + upsert_label( + repo, + f'[{prefix}] {short}', + color, + desc, + dry_run=dry_run, + ) + + +# --- Main ----------------------------------------------------------------------- + + +def main() -> int: + parser = argparse.ArgumentParser(description='Sync GitHub labels for easyscience repos') + parser.add_argument( + '--repo', + help='Target repository in the form easyscience/', + ) + parser.add_argument( + '--dry-run', + action='store_true', + help='Print actions without applying changes', + ) + args = parser.parse_args() + + if args.repo: + repo = args.repo + else: + repo = get_current_repo_name_with_owner() + + org, _ = repo.split('/', 1) + + if org.lower() != EASYSCIENCE_ORG: + print( + f"Refusing to run: repository {repo!r} is not under '{EASYSCIENCE_ORG}'.", + file=sys.stderr, + ) + return 2 + + print(f'Target repository: {repo}') + if args.dry_run: + print('Running in DRY-RUN mode (no changes will be made)\n') + + # 1) Rename basic labels + for old, new in zip(BASIC_GITHUB_LABELS, NEW_BASIC_LABEL_NAMES, strict=True): + try_rename_label(repo, old, new, dry_run=args.dry_run) + + # 2) Scope / Maintainer / Priority groups + upsert_group(repo, 'scope', COLORS['scope'], SCOPE_LABELS, dry_run=args.dry_run) + upsert_group(repo, 'maintainer', COLORS['maintainer'], MAINTAINER_LABELS, dry_run=args.dry_run) + upsert_group(repo, 'priority', COLORS['priority'], PRIORITY_LABELS, dry_run=args.dry_run) + + # 3) Bot label + upsert_label( + repo, + BOT_LABEL[0], + COLORS['bot'], + BOT_LABEL[1], + dry_run=args.dry_run, + ) + + print('\nDone.') + return 0 + + +if __name__ == '__main__': + raise SystemExit(main()) From 6d661f934d5e7451dd5f388b2e6ad85fa16f467a Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Tue, 3 Feb 2026 10:46:07 +0100 Subject: [PATCH 05/17] initial analysis class --- pixi.lock | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/pixi.lock b/pixi.lock index f51bc65b..da8aee45 100644 --- a/pixi.lock +++ b/pixi.lock @@ -5,6 +5,8 @@ environments: - url: https://conda.anaconda.org/conda-forge/ indexes: - https://pypi.org/simple + options: + pypi-prerelease-mode: if-necessary-or-explicit packages: linux-64: - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 @@ -78,7 +80,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/b8/01/74922a1c552137c05a41fee0c61153753dddc9117d19c7c5902c146c25ab/copier-9.11.3-py3-none-any.whl - - pypi: git+https://github.com/easyscience/corelib.git?rev=develop#bd106537fcf522336aa0176aa6ccf215be8a5b86 + - pypi: git+https://github.com/easyscience/corelib.git#bd106537fcf522336aa0176aa6ccf215be8a5b86 - pypi: https://files.pythonhosted.org/packages/ea/b4/694159c15c52b9f7ec7adf49d50e5f8ee71d3e9ef38adb4445d13dd56c20/coverage-7.13.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f2/f2/728f041460f1b9739b85ee23b45fa5a505962ea11fd85bdbe2a02b021373/darkdetect-0.8.0-py3-none-any.whl @@ -333,7 +335,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/b8/01/74922a1c552137c05a41fee0c61153753dddc9117d19c7c5902c146c25ab/copier-9.11.3-py3-none-any.whl - - pypi: git+https://github.com/easyscience/corelib.git?rev=develop#bd106537fcf522336aa0176aa6ccf215be8a5b86 + - pypi: git+https://github.com/easyscience/corelib.git#bd106537fcf522336aa0176aa6ccf215be8a5b86 - pypi: https://files.pythonhosted.org/packages/ce/8a/87af46cccdfa78f53db747b09f5f9a21d5fc38d796834adac09b30a8ce74/coverage-7.13.1-cp312-cp312-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f2/f2/728f041460f1b9739b85ee23b45fa5a505962ea11fd85bdbe2a02b021373/darkdetect-0.8.0-py3-none-any.whl @@ -588,7 +590,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/b8/01/74922a1c552137c05a41fee0c61153753dddc9117d19c7c5902c146c25ab/copier-9.11.3-py3-none-any.whl - - pypi: git+https://github.com/easyscience/corelib.git?rev=develop#bd106537fcf522336aa0176aa6ccf215be8a5b86 + - pypi: git+https://github.com/easyscience/corelib.git#bd106537fcf522336aa0176aa6ccf215be8a5b86 - pypi: https://files.pythonhosted.org/packages/82/a8/6e22fdc67242a4a5a153f9438d05944553121c8f4ba70cb072af4c41362e/coverage-7.13.1-cp312-cp312-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f2/f2/728f041460f1b9739b85ee23b45fa5a505962ea11fd85bdbe2a02b021373/darkdetect-0.8.0-py3-none-any.whl @@ -836,7 +838,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/b8/01/74922a1c552137c05a41fee0c61153753dddc9117d19c7c5902c146c25ab/copier-9.11.3-py3-none-any.whl - - pypi: git+https://github.com/easyscience/corelib.git?rev=develop#bd106537fcf522336aa0176aa6ccf215be8a5b86 + - pypi: git+https://github.com/easyscience/corelib.git#bd106537fcf522336aa0176aa6ccf215be8a5b86 - pypi: https://files.pythonhosted.org/packages/fa/dc/7282856a407c621c2aad74021680a01b23010bb8ebf427cf5eacda2e876f/coverage-7.13.1-cp312-cp312-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f2/f2/728f041460f1b9739b85ee23b45fa5a505962ea11fd85bdbe2a02b021373/darkdetect-0.8.0-py3-none-any.whl @@ -1029,6 +1031,8 @@ environments: - url: https://conda.anaconda.org/conda-forge/ indexes: - https://pypi.org/simple + options: + pypi-prerelease-mode: if-necessary-or-explicit packages: linux-64: - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 @@ -1102,7 +1106,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/b8/01/74922a1c552137c05a41fee0c61153753dddc9117d19c7c5902c146c25ab/copier-9.11.3-py3-none-any.whl - - pypi: git+https://github.com/easyscience/corelib.git?rev=develop#bd106537fcf522336aa0176aa6ccf215be8a5b86 + - pypi: git+https://github.com/easyscience/corelib.git#bd106537fcf522336aa0176aa6ccf215be8a5b86 - pypi: https://files.pythonhosted.org/packages/f7/7c/347280982982383621d29b8c544cf497ae07ac41e44b1ca4903024131f55/coverage-7.13.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f2/f2/728f041460f1b9739b85ee23b45fa5a505962ea11fd85bdbe2a02b021373/darkdetect-0.8.0-py3-none-any.whl @@ -1358,7 +1362,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl - pypi: https://files.pythonhosted.org/packages/b8/01/74922a1c552137c05a41fee0c61153753dddc9117d19c7c5902c146c25ab/copier-9.11.3-py3-none-any.whl - - pypi: git+https://github.com/easyscience/corelib.git?rev=develop#bd106537fcf522336aa0176aa6ccf215be8a5b86 + - pypi: git+https://github.com/easyscience/corelib.git#bd106537fcf522336aa0176aa6ccf215be8a5b86 - pypi: https://files.pythonhosted.org/packages/b4/9b/77baf488516e9ced25fc215a6f75d803493fc3f6a1a1227ac35697910c2a/coverage-7.13.1-cp311-cp311-macosx_10_9_x86_64.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f2/f2/728f041460f1b9739b85ee23b45fa5a505962ea11fd85bdbe2a02b021373/darkdetect-0.8.0-py3-none-any.whl @@ -1614,7 +1618,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/b8/01/74922a1c552137c05a41fee0c61153753dddc9117d19c7c5902c146c25ab/copier-9.11.3-py3-none-any.whl - - pypi: git+https://github.com/easyscience/corelib.git?rev=develop#bd106537fcf522336aa0176aa6ccf215be8a5b86 + - pypi: git+https://github.com/easyscience/corelib.git#bd106537fcf522336aa0176aa6ccf215be8a5b86 - pypi: https://files.pythonhosted.org/packages/d7/cd/7ab01154e6eb79ee2fab76bf4d89e94c6648116557307ee4ebbb85e5c1bf/coverage-7.13.1-cp311-cp311-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f2/f2/728f041460f1b9739b85ee23b45fa5a505962ea11fd85bdbe2a02b021373/darkdetect-0.8.0-py3-none-any.whl @@ -1863,7 +1867,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/b8/01/74922a1c552137c05a41fee0c61153753dddc9117d19c7c5902c146c25ab/copier-9.11.3-py3-none-any.whl - - pypi: git+https://github.com/easyscience/corelib.git?rev=develop#bd106537fcf522336aa0176aa6ccf215be8a5b86 + - pypi: git+https://github.com/easyscience/corelib.git#bd106537fcf522336aa0176aa6ccf215be8a5b86 - pypi: https://files.pythonhosted.org/packages/27/56/c216625f453df6e0559ed666d246fcbaaa93f3aa99eaa5080cea1229aa3d/coverage-7.13.1-cp311-cp311-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f2/f2/728f041460f1b9739b85ee23b45fa5a505962ea11fd85bdbe2a02b021373/darkdetect-0.8.0-py3-none-any.whl @@ -2057,6 +2061,8 @@ environments: - url: https://conda.anaconda.org/conda-forge/ indexes: - https://pypi.org/simple + options: + pypi-prerelease-mode: if-necessary-or-explicit packages: linux-64: - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 @@ -2130,7 +2136,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/b8/01/74922a1c552137c05a41fee0c61153753dddc9117d19c7c5902c146c25ab/copier-9.11.3-py3-none-any.whl - - pypi: git+https://github.com/easyscience/corelib.git?rev=develop#bd106537fcf522336aa0176aa6ccf215be8a5b86 + - pypi: git+https://github.com/easyscience/corelib.git#bd106537fcf522336aa0176aa6ccf215be8a5b86 - pypi: https://files.pythonhosted.org/packages/ea/b4/694159c15c52b9f7ec7adf49d50e5f8ee71d3e9ef38adb4445d13dd56c20/coverage-7.13.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f2/f2/728f041460f1b9739b85ee23b45fa5a505962ea11fd85bdbe2a02b021373/darkdetect-0.8.0-py3-none-any.whl @@ -2385,7 +2391,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/b8/01/74922a1c552137c05a41fee0c61153753dddc9117d19c7c5902c146c25ab/copier-9.11.3-py3-none-any.whl - - pypi: git+https://github.com/easyscience/corelib.git?rev=develop#bd106537fcf522336aa0176aa6ccf215be8a5b86 + - pypi: git+https://github.com/easyscience/corelib.git#bd106537fcf522336aa0176aa6ccf215be8a5b86 - pypi: https://files.pythonhosted.org/packages/ce/8a/87af46cccdfa78f53db747b09f5f9a21d5fc38d796834adac09b30a8ce74/coverage-7.13.1-cp312-cp312-macosx_10_13_x86_64.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f2/f2/728f041460f1b9739b85ee23b45fa5a505962ea11fd85bdbe2a02b021373/darkdetect-0.8.0-py3-none-any.whl @@ -2640,7 +2646,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/b8/01/74922a1c552137c05a41fee0c61153753dddc9117d19c7c5902c146c25ab/copier-9.11.3-py3-none-any.whl - - pypi: git+https://github.com/easyscience/corelib.git?rev=develop#bd106537fcf522336aa0176aa6ccf215be8a5b86 + - pypi: git+https://github.com/easyscience/corelib.git#bd106537fcf522336aa0176aa6ccf215be8a5b86 - pypi: https://files.pythonhosted.org/packages/82/a8/6e22fdc67242a4a5a153f9438d05944553121c8f4ba70cb072af4c41362e/coverage-7.13.1-cp312-cp312-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f2/f2/728f041460f1b9739b85ee23b45fa5a505962ea11fd85bdbe2a02b021373/darkdetect-0.8.0-py3-none-any.whl @@ -2888,7 +2894,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/b8/01/74922a1c552137c05a41fee0c61153753dddc9117d19c7c5902c146c25ab/copier-9.11.3-py3-none-any.whl - - pypi: git+https://github.com/easyscience/corelib.git?rev=develop#bd106537fcf522336aa0176aa6ccf215be8a5b86 + - pypi: git+https://github.com/easyscience/corelib.git#bd106537fcf522336aa0176aa6ccf215be8a5b86 - pypi: https://files.pythonhosted.org/packages/fa/dc/7282856a407c621c2aad74021680a01b23010bb8ebf427cf5eacda2e876f/coverage-7.13.1-cp312-cp312-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f2/f2/728f041460f1b9739b85ee23b45fa5a505962ea11fd85bdbe2a02b021373/darkdetect-0.8.0-py3-none-any.whl @@ -4085,7 +4091,7 @@ packages: requires_python: '>=3.5' - pypi: ./ name: easydynamics - version: 0.1.0+devdirty6 + version: 0.1.1+devdirty2 sha256: de299c914d4a865b9e2fdefa5e3947f37b1f26f73ff9087f7918ee417f3dd288 requires_dist: - darkdetect @@ -4128,8 +4134,7 @@ packages: - validate-pyproject[all] ; extra == 'dev' - versioningit ; extra == 'dev' requires_python: '>=3.11' - editable: true -- pypi: git+https://github.com/easyscience/corelib.git?rev=develop#bd106537fcf522336aa0176aa6ccf215be8a5b86 +- pypi: git+https://github.com/easyscience/corelib.git#bd106537fcf522336aa0176aa6ccf215be8a5b86 name: easyscience version: 2.1.0 requires_dist: From 02949729e8eb7e9fa2e4361ba75bf80f0d0ae624 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Fri, 6 Feb 2026 11:51:56 +0100 Subject: [PATCH 06/17] make analysis_base --- src/easydynamics/analysis/analysis1d.py | 193 +++++++++--------- src/easydynamics/analysis/analysis_base.py | 189 +++++++++++++++++ .../convolution/numerical_convolution_base.py | 72 ++++--- src/easydynamics/sample_model/__init__.py | 28 +-- .../jump_translational_diffusion.py | 64 +++--- 5 files changed, 374 insertions(+), 172 deletions(-) create mode 100644 src/easydynamics/analysis/analysis_base.py diff --git a/src/easydynamics/analysis/analysis1d.py b/src/easydynamics/analysis/analysis1d.py index ebad61d2..b27fed1e 100644 --- a/src/easydynamics/analysis/analysis1d.py +++ b/src/easydynamics/analysis/analysis1d.py @@ -11,7 +11,7 @@ from easydynamics.convolution import Convolution from easydynamics.experiment import Experiment -from easydynamics.sample_model import BackgroundModel +from easydynamics.sample_model import InstrumentModel from easydynamics.sample_model import ResolutionModel from easydynamics.sample_model import SampleModel @@ -21,63 +21,46 @@ class Analysis1d(EasyScienceModelBase): def __init__( self, - display_name: str = 'MyAnalysis', + display_name: str = "MyAnalysis", unique_name: str | None = None, experiment: Experiment | None = None, sample_model: SampleModel | None = None, - resolution_model: ResolutionModel | None = None, - background_model: BackgroundModel | None = None, - energy_offset: list[Parameter] | None = None, + instrument_model: InstrumentModel | None = None, Q_index: int | None = None, ): super().__init__(display_name=display_name, unique_name=unique_name) if experiment is not None and not isinstance(experiment, Experiment): - raise TypeError('experiment must be an instance of Experiment or None.') + raise TypeError("experiment must be an instance of Experiment or None.") self._experiment = experiment if sample_model is not None and not isinstance(sample_model, SampleModel): - raise TypeError('sample_model must be an instance of SampleModel or None.') + raise TypeError("sample_model must be an instance of SampleModel or None.") sample_model.Q = self.Q self._sample_model = sample_model - if resolution_model is not None and not isinstance(resolution_model, ResolutionModel): - raise TypeError('resolution_model must be an instance of ResolutionModel or None.') - resolution_model.Q = self.Q - self._resolution_model = resolution_model - - if background_model is not None and not isinstance(background_model, BackgroundModel): - raise TypeError('background_model must be an instance of BackgroundModel or None.') - background_model.Q = self.Q - self._background_model = background_model + if instrument_model is not None and not isinstance( + instrument_model, InstrumentModel + ): + raise TypeError( + "instrument_model must be an instance of InstrumentModel or None." + ) + if instrument_model is None: + self._instrument_model = InstrumentModel() + else: + self._instrument_model = instrument_model self._convolvers = [None] * (len(self.Q) if self.Q is not None else 0) self._update_models() - if not isinstance(energy_offset, list) and energy_offset is not None: - raise TypeError('energy_offset must be a list of Parameters or None.') - - if energy_offset is not None: - if len(energy_offset) != len(self.Q): - raise ValueError('energy_offset list length must match number of Q values.') - for offset in energy_offset: - if not isinstance(offset, Parameter): - raise TypeError('Each energy_offset must be an instance of Parameter.') - else: - energy_offset = [ - Parameter(name='energy_offset', value=0.0, unit=self.sample_model.unit) - for _ in range(len(self.Q)) - ] - self._energy_offset = energy_offset - if Q_index is not None: if ( not isinstance(Q_index, int) or Q_index < 0 or (self.Q is not None and Q_index >= len(self.Q)) ): - raise ValueError('Q_index must be a valid index for the Q values.') + raise ValueError("Q_index must be a valid index for the Q values.") self._Q_index = Q_index ############# @@ -92,7 +75,7 @@ def experiment(self) -> Experiment | None: @experiment.setter def experiment(self, value: Experiment | None) -> None: if value is not None and not isinstance(value, Experiment): - raise TypeError('experiment must be an instance of Experiment or None.') + raise TypeError("experiment must be an instance of Experiment or None.") self._experiment = value self._update_models() @@ -104,7 +87,7 @@ def sample_model(self) -> SampleModel | None: @sample_model.setter def sample_model(self, value: SampleModel | None) -> None: if value is not None and not isinstance(value, SampleModel): - raise TypeError('sample_model must be an instance of SampleModel or None.') + raise TypeError("sample_model must be an instance of SampleModel or None.") self._sample_model = value self._update_models() @@ -116,22 +99,12 @@ def resolution_model(self) -> ResolutionModel | None: @resolution_model.setter def resolution_model(self, value: ResolutionModel | None) -> None: if value is not None and not isinstance(value, ResolutionModel): - raise TypeError('resolution_model must be an instance of ResolutionModel or None.') + raise TypeError( + "resolution_model must be an instance of ResolutionModel or None." + ) self._resolution_model = value self._update_models() - @property - def background_model(self) -> BackgroundModel | None: - """The BackgroundModel associated with this Analysis.""" - return self._background_model - - @background_model.setter - def background_model(self, value: BackgroundModel | None) -> None: - if value is not None and not isinstance(value, BackgroundModel): - raise TypeError('background_model must be an instance of BackgroundModel or None.') - self._background_model = value - self._update_models() - @property def Q(self) -> sc.Variable | None: """The Q values from the associated Experiment, if available.""" @@ -142,7 +115,7 @@ def Q(self) -> sc.Variable | None: @Q.setter def Q(self, value) -> None: """Q is a read-only property derived from the Experiment.""" - raise AttributeError('Q is a read-only property derived from the Experiment.') + raise AttributeError("Q is a read-only property derived from the Experiment.") @property def energy(self) -> sc.Variable | None: @@ -158,7 +131,9 @@ def energy(self, value) -> None: """Energy is a read-only property derived from the Experiment. """ - raise AttributeError('energy is a read-only property derived from the Experiment.') + raise AttributeError( + "energy is a read-only property derived from the Experiment." + ) @property def temperature(self) -> Parameter | None: @@ -172,7 +147,9 @@ def temperature(self, value) -> None: """Temperature is a read-only property derived from the Experiment. """ - raise AttributeError('temperature is a read-only property derived from the sample model.') + raise AttributeError( + "temperature is a read-only property derived from the sample model." + ) @property def energy_offset(self) -> list[Parameter] | None: @@ -192,10 +169,14 @@ def energy_offset(self, offsets: list[Parameter] | None) -> None: """ if offsets is not None: if len(offsets) != len(self.Q): - raise ValueError('energy_offset list length must match number of Q values.') + raise ValueError( + "energy_offset list length must match number of Q values." + ) for offset in offsets: if not isinstance(offset, Parameter): - raise TypeError('Each energy_offset must be an instance of Parameter.') + raise TypeError( + "Each energy_offset must be an instance of Parameter." + ) self._energy_offset = offsets @property @@ -216,7 +197,7 @@ def Q_index(self, index: int | None) -> None: or index < 0 or (self.Q is not None and index >= len(self.Q)) ): - raise ValueError('Q_index must be a valid index for the Q values.') + raise ValueError("Q_index must be a valid index for the Q values.") self._Q_index = index ############# @@ -233,7 +214,7 @@ def calculate(self, energy: float | None = None) -> np.ndarray: """ Q_index = self.Q_index if Q_index is None: - raise ValueError('Q_index must be set to calculate the model.') + raise ValueError("Q_index must be set to calculate the model.") if energy is None: energy = self.energy.values @@ -244,9 +225,9 @@ def calculate(self, energy: float | None = None) -> np.ndarray: sample_intensity = np.zeros_like(energy) else: if self.resolution_model is None: - sample_intensity = self.sample_model._component_collections[Q_index].evaluate( - energy - ) + sample_intensity = self.sample_model._component_collections[ + Q_index + ].evaluate(energy) else: convolver = self._convolvers[Q_index] sample_intensity = convolver.convolution() @@ -254,9 +235,9 @@ def calculate(self, energy: float | None = None) -> np.ndarray: if self.background_model is None: background_intensity = np.zeros_like(energy) else: - background_intensity = self.background_model._component_collections[Q_index].evaluate( - energy - ) + background_intensity = self.background_model._component_collections[ + Q_index + ].evaluate(energy) sample_plus_background = sample_intensity + background_intensity @@ -279,11 +260,13 @@ def calculate_individual_components( background_results = [] Q_index = self.Q_index if Q_index is None: - raise ValueError('Q_index must be set to calculate the model.') + raise ValueError("Q_index must be set to calculate the model.") if self.sample_model is not None: # Calculate sample components - for component in self.sample_model._component_collections[Q_index]._components: + for component in self.sample_model._component_collections[ + Q_index + ]._components: if self.resolution_model is None: component_intensity = component.evaluate(self.energy) else: @@ -300,7 +283,9 @@ def calculate_individual_components( if self.background_model is not None: # Calculate background components - for component in self.background_model._component_collections[Q_index]._components: + for component in self.background_model._component_collections[ + Q_index + ]._components: component_intensity = component.evaluate(self.energy) background_results.append(component_intensity) @@ -314,14 +299,14 @@ def fit(self): FitResult: The result of the fit. """ if self._experiment is None: - raise ValueError('No experiment is associated with this Analysis.') + raise ValueError("No experiment is associated with this Analysis.") Q_index = self.Q_index if Q_index is None: - raise ValueError('Q_index must be set to perform the fit.') + raise ValueError("Q_index must be set to perform the fit.") - data = self.experiment.data['Q', Q_index] - x = data.coords['energy'].values + data = self.experiment.data["Q", Q_index] + x = data.coords["energy"].values y = data.values e = data.variances**0.5 @@ -352,47 +337,47 @@ def plot_data_and_model( individual components. Default is True. """ if not isinstance(plot_individual_components, bool): - raise TypeError('plot_individual_components must be True or False.') + raise TypeError("plot_individual_components must be True or False.") import matplotlib.pyplot as plt Q_index = self.Q_index if Q_index is None: - raise ValueError('Q_index must be set to plot the data and model.') + raise ValueError("Q_index must be set to plot the data and model.") if self.experiment is None or self.experiment.data is None: - raise ValueError('Experiment data is not available for plotting.') - data = self.experiment.data['Q', Q_index] - energy = data.coords['energy'].values + raise ValueError("Experiment data is not available for plotting.") + data = self.experiment.data["Q", Q_index] + energy = data.coords["energy"].values model = self.calculate(energy=energy) plt.figure() plt.errorbar( energy, data.values, yerr=data.variances**0.5, - fmt='o', - label='Data', - color='black', + fmt="o", + label="Data", + color="black", ) - plt.plot(energy, model, label='Model', color='red') + plt.plot(energy, model, label="Model", color="red") if plot_individual_components: sample_comps, background_comps = self.calculate_individual_components() for i, comp in enumerate(sample_comps): plt.plot( energy, comp, - label=f'Sample Component {i + 1}', - linestyle='--', + label=f"Sample Component {i + 1}", + linestyle="--", ) for i, comp in enumerate(background_comps): plt.plot( energy, comp, - label=f'Background Component {i + 1}', - linestyle=':', + label=f"Background Component {i + 1}", + linestyle=":", ) - plt.xlabel(f'Energy ({self.energy.unit})') - plt.ylabel(f'Intensity ({self.sample_model.unit})') - plt.title(f'Data and Model at Q index {Q_index}') + plt.xlabel(f"Energy ({self.energy.unit})") + plt.ylabel(f"Intensity ({self.sample_model.unit})") + plt.title(f"Data and Model at Q index {Q_index}") plt.legend() plt.show() # model_data_array = self._create_model_data_group( @@ -419,15 +404,21 @@ def get_all_variables(self) -> list[DescriptorNumber]: variables = [] if self.sample_model is not None: variables.extend( - self.sample_model._component_collections[self.Q_index].get_all_variables() + self.sample_model._component_collections[ + self.Q_index + ].get_all_variables() ) if self.resolution_model is not None: variables.extend( - self.resolution_model._component_collections[self.Q_index].get_all_variables() + self.resolution_model._component_collections[ + self.Q_index + ].get_all_variables() ) if self.background_model is not None: variables.extend( - self.background_model._component_collections[self.Q_index].get_all_variables() + self.background_model._component_collections[ + self.Q_index + ].get_all_variables() ) variables.append(self.energy_offset[self.Q_index]) # TODO temperature and diffusion @@ -450,7 +441,7 @@ def _create_convolver(self, Q_index: int): index. """ if self.sample_model is None or self.resolution_model is None: - raise ValueError('Both sample_model and resolution_model must be defined.') + raise ValueError("Both sample_model and resolution_model must be defined.") sample_components = self.sample_model._component_collections[Q_index] resolution_components = self.resolution_model._component_collections[Q_index] @@ -468,7 +459,7 @@ def _create_model_data_group(self, individual_components=True) -> sc.DataArray: and energy values. """ if self.Q is None or self.energy is None: - raise ValueError('Q and energy must be defined in the experiment.') + raise ValueError("Q and energy must be defined in the experiment.") model_data = [] for Q_index in range(len(self.Q)): @@ -476,23 +467,31 @@ def _create_model_data_group(self, individual_components=True) -> sc.DataArray: model_data.append(model_at_Q) model_data_array = sc.DataArray( - data=sc.array(dims=['Q', 'energy'], values=model_data), + data=sc.array(dims=["Q", "energy"], values=model_data), coords={ - 'Q': self.Q, - 'energy': self.energy, + "Q": self.Q, + "energy": self.energy, }, ) - model_group = sc.DataGroup({'Model': model_data_array}) + model_group = sc.DataGroup({"Model": model_data_array}) if individual_components: components = self.calculate_individual_components_all_Q() for Q_index, (sample_comps, background_comps) in enumerate(components): for samp_index, samp_comp in enumerate(sample_comps): - model_data_array[samp_comp.display_name] = sc.zeros_like(model_data_array.data) - model_data_array[samp_comp.display_name].data[Q_index, :] = samp_comp + model_data_array[samp_comp.display_name] = sc.zeros_like( + model_data_array.data + ) + model_data_array[samp_comp.display_name].data[ + Q_index, : + ] = samp_comp for back_index, back_comp in enumerate(background_comps): - model_data_array[back_comp.display_name] = sc.zeros_like(model_data_array.data) - model_data_array[back_comp.display_name].data[Q_index, :] = back_comp + model_data_array[back_comp.display_name] = sc.zeros_like( + model_data_array.data + ) + model_data_array[back_comp.display_name].data[ + Q_index, : + ] = back_comp model_data_array = model_data_array + model_group # WRONG BUT LINT return model_data_array diff --git a/src/easydynamics/analysis/analysis_base.py b/src/easydynamics/analysis/analysis_base.py new file mode 100644 index 00000000..5d965cae --- /dev/null +++ b/src/easydynamics/analysis/analysis_base.py @@ -0,0 +1,189 @@ +# SPDX-FileCopyrightText: 2025-2026 EasyDynamics contributors +# SPDX-License-Identifier: BSD-3-Clause + + +import scipp as sc +from easyscience.base_classes.model_base import ModelBase as EasyScienceModelBase +from easyscience.variable import Parameter + +from easydynamics.convolution import Convolution +from easydynamics.experiment import Experiment +from easydynamics.sample_model import InstrumentModel +from easydynamics.sample_model import SampleModel + + +class Analysis1Base(EasyScienceModelBase): + """For analysing data.""" + + def __init__( + self, + display_name: str = "MyAnalysis", + unique_name: str | None = None, + experiment: Experiment | None = None, + sample_model: SampleModel | None = None, + instrument_model: InstrumentModel | None = None, + ): + super().__init__(display_name=display_name, unique_name=unique_name) + + if experiment is None: + self._experiment = Experiment() + elif isinstance(experiment, Experiment): + self._experiment = experiment + else: + raise TypeError("experiment must be an instance of Experiment or None.") + + if sample_model is None: + self._sample_model = SampleModel() + elif isinstance(sample_model, SampleModel): + self._sample_model = sample_model + else: + raise TypeError("sample_model must be an instance of SampleModel or None.") + + if instrument_model is None: + self._instrument_model = InstrumentModel() + elif isinstance(instrument_model, InstrumentModel): + self._instrument_model = instrument_model + else: + raise TypeError( + "instrument_model must be an instance of InstrumentModel or None." + ) + + self._convolvers = [None] * (len(self.Q) if self.Q is not None else 0) + self._update_models() + + ############# + # Properties + ############# + + @property + def experiment(self) -> Experiment | None: + """The Experiment associated with this Analysis.""" + return self._experiment + + @experiment.setter + def experiment(self, value: Experiment) -> None: + if not isinstance(value, Experiment): + raise TypeError("experiment must be an instance of Experiment") + self._experiment = value + self._on_experiment_changed() + + @property + def sample_model(self) -> SampleModel: + """The SampleModel associated with this Analysis.""" + return self._sample_model + + @sample_model.setter + def sample_model(self, value: SampleModel) -> None: + if not isinstance(value, SampleModel): + raise TypeError("sample_model must be an instance of SampleModel") + self._sample_model = value + self._on_sample_model_changed() + + @property + def instrument_model(self) -> InstrumentModel: + """The InstrumentModel associated with this Analysis.""" + return self._instrument_model + + @instrument_model.setter + def instrument_model(self, value: InstrumentModel) -> None: + if not isinstance(value, InstrumentModel): + raise TypeError("instrument_model must be an instance of InstrumentModel") + self._instrument_model = value + self._on_instrument_model_changed() + + @property + def Q(self) -> sc.Variable | None: + """The Q values from the associated Experiment, if available.""" + if self.experiment is not None: + return self.experiment.Q + return None + + @Q.setter + def Q(self, value) -> None: + """Q is a read-only property derived from the Experiment.""" + raise AttributeError("Q is a read-only property derived from the Experiment.") + + @property + def energy(self) -> sc.Variable | None: + """The energy values from the associated Experiment, if + available. + """ + if self.experiment is not None: + return self.experiment.energy + return None + + @energy.setter + def energy(self, value) -> None: + """Energy is a read-only property derived from the + Experiment. + """ + raise AttributeError( + "energy is a read-only property derived from the Experiment." + ) + + @property + def temperature(self) -> Parameter | None: + """ + The temperature from the associated SampleModel, if available. + """ + return self.sample_model.temperature if self.sample_model is not None else None + + @temperature.setter + def temperature(self, value) -> None: + """ + Temperature is a read-only property derived from the + SampleModel. + """ + raise AttributeError( + "temperature is a read-only property derived from the sample model." + ) + + ############# + # Other methods + ############# + + ############# + # Private methods + ############# + + def _on_experiment_changed(self): + pass + + def _on_sample_model_changed(self): + pass + + def _on_instrument_model_changed(self): + pass + + # def _update_models(self): + # """Update models based on the current experiment.""" + # if self.experiment is None: + # return + + # for Q_index in range(len(self.Q)): + # self._convolvers[Q_index] = self._create_convolver(Q_index) + + def _create_convolver(self, Q_index: int): + """Initialize and return a Convolution object for the given Q + index. + """ + sample_components = self.sample_model._component_collections[Q_index] + if sample_components == []: + raise ValueError(f"Sample model has no components at Q index {Q_index}.") + + resolution_components = ( + self.instrument_model.resolution_model._component_collections[Q_index] + ) + if resolution_components == []: + raise ValueError( + f"Resolution model has no components at Q index {Q_index}." + ) + + energy = self.energy + convolver = Convolution( + sample_components=sample_components, + resolution_components=resolution_components, + energy=energy, + temperature=self.temperature, + ) + return convolver diff --git a/src/easydynamics/convolution/numerical_convolution_base.py b/src/easydynamics/convolution/numerical_convolution_base.py index ffcf0058..dd3e68e3 100644 --- a/src/easydynamics/convolution/numerical_convolution_base.py +++ b/src/easydynamics/convolution/numerical_convolution_base.py @@ -66,8 +66,8 @@ def __init__( upsample_factor: Numerical = 5, extension_factor: float = 0.2, temperature: Parameter | float | None = None, - temperature_unit: str | sc.Unit = 'K', - energy_unit: str | sc.Unit = 'meV', + temperature_unit: str | sc.Unit = "K", + energy_unit: str | sc.Unit = "meV", normalize_detailed_balance: bool = True, ): super().__init__( @@ -77,11 +77,13 @@ def __init__( energy_unit=energy_unit, ) - if temperature is not None and not isinstance(temperature, (Numerical, Parameter)): - raise TypeError('Temperature must be None, a number or a Parameter.') + if temperature is not None and not isinstance( + temperature, (Numerical, Parameter) + ): + raise TypeError("Temperature must be None, a number or a Parameter.") if not isinstance(temperature_unit, (str, sc.Unit)): - raise TypeError('Temperature_unit must be a string or sc.Unit.') + raise TypeError("Temperature_unit must be a string or sc.Unit.") self._temperature_unit = temperature_unit self._temperature = None self.temperature = temperature @@ -117,10 +119,10 @@ def upsample_factor(self, factor: Numerical) -> None: return if not isinstance(factor, Numerical): - raise TypeError('Upsample factor must be a numerical value or None.') + raise TypeError("Upsample factor must be a numerical value or None.") factor = float(factor) if factor <= 1.0: - raise ValueError('Upsample factor must be greater than 1.') + raise ValueError("Upsample factor must be greater than 1.") self._upsample_factor = factor @@ -156,9 +158,9 @@ def extension_factor(self, factor: Numerical) -> None: TypeError: If factor is not a number. """ if not isinstance(factor, Numerical): - raise TypeError('Extension factor must be a number.') + raise TypeError("Extension factor must be a number.") if factor < 0.0: - raise ValueError('Extension factor must be non-negative.') + raise ValueError("Extension factor must be non-negative.") self._extension_factor = factor # Recreate dense grid when extension factor is updated @@ -192,7 +194,7 @@ def temperature(self, temp: Parameter | float | None) -> None: self._temperature.value = float(temp) else: self._temperature = Parameter( - name='temperature', + name="temperature", value=float(temp), unit=self._temperature_unit, fixed=True, @@ -200,7 +202,7 @@ def temperature(self, temp: Parameter | float | None) -> None: elif isinstance(temp, Parameter): self._temperature = temp else: - raise TypeError('Temperature must be None, a float or a Parameter.') + raise TypeError("Temperature must be None, a float or a Parameter.") @property def normalize_detailed_balance(self) -> bool: @@ -221,7 +223,7 @@ def normalize_detailed_balance(self, normalize: bool) -> None: """ if not isinstance(normalize, bool): - raise TypeError('normalize_detailed_balance must be True or False.') + raise TypeError("normalize_detailed_balance must be True or False.") self._normalize_detailed_balance = normalize @@ -239,9 +241,9 @@ def _create_energy_grid( The dense grid created by upsampling and extending energy. The EnergyGrid has the following attributes: - energy_dense : np.ndarray + energy_dense : np.ndarray The upsampled and extended energy array. - energy_dense_centered : np.ndarray + energy_dense_centered : np.ndarray The centered version of energy_dense (used for resolution evaluation). energy_dense_step : float @@ -259,7 +261,7 @@ def _create_energy_grid( is_uniform = np.allclose(energy_diff, energy_diff[0]) if not is_uniform: raise ValueError( - 'Input array `energy` must be uniformly spaced if upsample_factor is not given.' # noqa: E501 + "Input array `energy` must be uniformly spaced if upsample_factor is not given." # noqa: E501 ) energy_dense = self.energy.values @@ -276,7 +278,7 @@ def _create_energy_grid( energy_span_dense = extended_max - extended_min if len(energy_dense) < 2: - raise ValueError('Energy array must have at least two points.') + raise ValueError("Energy array must have at least two points.") energy_dense_step = energy_dense[1] - energy_dense[0] # Handle offset for even length of energy_dense in convolution. @@ -346,35 +348,41 @@ def _check_width_thresholds( components = [model] # Treat single ModelComponent as a list for comp in components: - if hasattr(comp, 'width'): - if comp.width.value > LARGE_WIDTH_THRESHOLD * self._energy_grid.energy_span_dense: + if hasattr(comp, "width"): + if ( + comp.width.value + > LARGE_WIDTH_THRESHOLD * self._energy_grid.energy_span_dense + ): warnings.warn( f"The width of the {model_name} component '{comp.unique_name}' \ ({comp.width.value}) is large compared to the span of the input " - f'array ({self._energy_grid.energy_span_dense}). \ + f"array ({self._energy_grid.energy_span_dense}). \ This may lead to inaccuracies in the convolution. \ - Increase extension_factor to improve accuracy.', + Increase extension_factor to improve accuracy.", UserWarning, ) - if comp.width.value < SMALL_WIDTH_THRESHOLD * self._energy_grid.energy_dense_step: + if ( + comp.width.value + < SMALL_WIDTH_THRESHOLD * self._energy_grid.energy_dense_step + ): warnings.warn( f"The width of the {model_name} component '{comp.unique_name}' \ ({comp.width.value}) is small compared to the spacing of the input " - f'array ({self._energy_grid.energy_dense_step}). \ + f"array ({self._energy_grid.energy_dense_step}). \ This may lead to inaccuracies in the convolution. \ - Increase upsample_factor to improve accuracy.', + Increase upsample_factor to improve accuracy.", UserWarning, ) def __repr__(self) -> str: return ( - f'{self.__class__.__name__}(' - f'energy=array of shape {self.energy.values.shape},\n ' - f'sample_components={repr(self.sample_components)}, \n' - f'resolution_components={repr(self.resolution_components)},\n ' - f'energy_unit={self._energy_unit}, ' - f'upsample_factor={self.upsample_factor}, ' - f'extension_factor={self.extension_factor}, ' - f'temperature={self.temperature}, ' - f'normalize_detailed_balance={self.normalize_detailed_balance})' + f"{self.__class__.__name__}(" + f"energy=array of shape {self.energy.values.shape},\n " + f"sample_components={repr(self.sample_components)}, \n" + f"resolution_components={repr(self.resolution_components)},\n " + f"energy_unit={self._energy_unit}, " + f"upsample_factor={self.upsample_factor}, " + f"extension_factor={self.extension_factor}, " + f"temperature={self.temperature}, " + f"normalize_detailed_balance={self.normalize_detailed_balance})" ) diff --git a/src/easydynamics/sample_model/__init__.py b/src/easydynamics/sample_model/__init__.py index 193ba7f5..443c1982 100644 --- a/src/easydynamics/sample_model/__init__.py +++ b/src/easydynamics/sample_model/__init__.py @@ -9,20 +9,24 @@ from .components import Lorentzian from .components import Polynomial from .components import Voigt -from .diffusion_model.brownian_translational_diffusion import BrownianTranslationalDiffusion +from .diffusion_model.brownian_translational_diffusion import ( + BrownianTranslationalDiffusion, +) +from .instrument_model import InstrumentModel from .resolution_model import ResolutionModel from .sample_model import SampleModel __all__ = [ - 'ComponentCollection', - 'Gaussian', - 'Lorentzian', - 'Voigt', - 'DeltaFunction', - 'DampedHarmonicOscillator', - 'Polynomial', - 'BrownianTranslationalDiffusion', - 'SampleModel', - 'ResolutionModel', - 'BackgroundModel', + "ComponentCollection", + "Gaussian", + "Lorentzian", + "Voigt", + "DeltaFunction", + "DampedHarmonicOscillator", + "Polynomial", + "BrownianTranslationalDiffusion", + "SampleModel", + "ResolutionModel", + "BackgroundModel", + "InstrumentModel", ] diff --git a/src/easydynamics/sample_model/diffusion_model/jump_translational_diffusion.py b/src/easydynamics/sample_model/diffusion_model/jump_translational_diffusion.py index 8bb65480..286ea486 100644 --- a/src/easydynamics/sample_model/diffusion_model/jump_translational_diffusion.py +++ b/src/easydynamics/sample_model/diffusion_model/jump_translational_diffusion.py @@ -9,7 +9,9 @@ from easydynamics.sample_model.component_collection import ComponentCollection from easydynamics.sample_model.components import Lorentzian -from easydynamics.sample_model.diffusion_model.diffusion_model_base import DiffusionModelBase +from easydynamics.sample_model.diffusion_model.diffusion_model_base import ( + DiffusionModelBase, +) from easydynamics.utils.utils import Numeric from easydynamics.utils.utils import Q_type from easydynamics.utils.utils import _validate_and_convert_Q @@ -31,9 +33,9 @@ class JumpTranslationalDiffusion(DiffusionModelBase): def __init__( self, - display_name: str | None = 'JumpTranslationalDiffusion', + display_name: str | None = "JumpTranslationalDiffusion", unique_name: str | None = None, - unit: str | sc.Unit = 'meV', + unit: str | sc.Unit = "meV", scale: Numeric = 1.0, diffusion_coefficient: Numeric = 1.0, relaxation_time: Numeric = 1.0, @@ -50,11 +52,11 @@ def __init__( unit : str or sc.Unit, optional Energy unit for the underlying Lorentzian components. Defaults to "meV". - scale : float , optional + scale : float, optional Scale factor for the diffusion model. - diffusion_coefficient : float , optional + diffusion_coefficient : float, optional Diffusion coefficient D in m^2/s. Defaults to 1.0. - relaxation_time : float , optional + relaxation_time : float, optional Relaxation time t in ps. Defaults to 1.0. """ super().__init__( @@ -65,27 +67,27 @@ def __init__( ) if not isinstance(diffusion_coefficient, Numeric): - raise TypeError('diffusion_coefficient must be a number.') + raise TypeError("diffusion_coefficient must be a number.") if not isinstance(relaxation_time, Numeric): - raise TypeError('relaxation_time must be a number.') + raise TypeError("relaxation_time must be a number.") diffusion_coefficient = Parameter( - name='diffusion_coefficient', + name="diffusion_coefficient", value=float(diffusion_coefficient), fixed=False, - unit='m**2/s', + unit="m**2/s", ) relaxation_time = Parameter( - name='relaxation_time', + name="relaxation_time", value=float(relaxation_time), fixed=False, - unit='ps', + unit="ps", ) - self._hbar = DescriptorNumber.from_scipp('hbar', scipp_hbar) - self._angstrom = DescriptorNumber('angstrom', 1e-10, unit='m') + self._hbar = DescriptorNumber.from_scipp("hbar", scipp_hbar) + self._angstrom = DescriptorNumber("angstrom", 1e-10, unit="m") self._diffusion_coefficient = diffusion_coefficient self._relaxation_time = relaxation_time @@ -108,7 +110,7 @@ def diffusion_coefficient(self) -> Parameter: def diffusion_coefficient(self, diffusion_coefficient: Numeric) -> None: """Set the diffusion coefficient parameter D.""" if not isinstance(diffusion_coefficient, Numeric): - raise TypeError('diffusion_coefficient must be a number.') + raise TypeError("diffusion_coefficient must be a number.") self._diffusion_coefficient.value = diffusion_coefficient @property @@ -126,7 +128,7 @@ def relaxation_time(self) -> Parameter: def relaxation_time(self, relaxation_time: Numeric) -> None: """Set the relaxation time parameter t.""" if not isinstance(relaxation_time, Numeric): - raise TypeError('relaxation_time must be a number.') + raise TypeError("relaxation_time must be a number.") self._relaxation_time.value = relaxation_time ################################ @@ -161,7 +163,7 @@ def calculate_width(self, Q: Q_type) -> np.ndarray: unit_conversion_factor_denominator = ( self.diffusion_coefficient / self._angstrom**2 * self.relaxation_time ) - unit_conversion_factor_denominator.convert_unit('dimensionless') + unit_conversion_factor_denominator.convert_unit("dimensionless") denominator = 1 + unit_conversion_factor_denominator.value * Q**2 @@ -207,7 +209,7 @@ def calculate_QISF(self, Q: Q_type) -> np.ndarray: def create_component_collections( self, Q: Q_type, - component_display_name: str = 'Jump translational diffusion', + component_display_name: str = "Jump translational diffusion", ) -> List[ComponentCollection]: """Create ComponentCollection components for the diffusion model at given Q values. @@ -227,7 +229,7 @@ def create_component_collections( Q = _validate_and_convert_Q(Q) if not isinstance(component_display_name, str): - raise TypeError('component_name must be a string.') + raise TypeError("component_name must be a string.") component_collection_list = [None] * len(Q) # In more complex models, this is used to scale the area of the @@ -239,7 +241,7 @@ def create_component_collections( # is 0. for i, Q_value in enumerate(Q): component_collection_list[i] = ComponentCollection( - display_name=f'{self.display_name}_Q{Q_value:.2f}', unit=self.unit + display_name=f"{self.display_name}_Q{Q_value:.2f}", unit=self.unit ) lorentzian_component = Lorentzian( @@ -288,20 +290,20 @@ def _write_width_dependency_expression(self, Q: float) -> str: Dependency expression for the width. """ if not isinstance(Q, (float)): - raise TypeError('Q must be a float.') + raise TypeError("Q must be a float.") # Q is given as a float, so we need to add the units - return f'hbar * D* {Q} **2/(angstrom**2)/(1 + (D * t* {Q} **2/(angstrom**2)))' + return f"hbar * D* {Q} **2/(angstrom**2)/(1 + (D * t* {Q} **2/(angstrom**2)))" def _write_width_dependency_map_expression(self) -> Dict[str, DescriptorNumber]: """Write the dependency map expression to make dependent Parameters. """ return { - 'D': self._diffusion_coefficient, - 't': self._relaxation_time, - 'hbar': self._hbar, - 'angstrom': self._angstrom, + "D": self._diffusion_coefficient, + "t": self._relaxation_time, + "hbar": self._hbar, + "angstrom": self._angstrom, } def _write_area_dependency_expression(self, QISF: float) -> str: @@ -314,16 +316,16 @@ def _write_area_dependency_expression(self, QISF: float) -> str: Dependency expression for the area. """ if not isinstance(QISF, (float)): - raise TypeError('QISF must be a float.') + raise TypeError("QISF must be a float.") - return f'{QISF} * scale' + return f"{QISF} * scale" def _write_area_dependency_map_expression(self) -> Dict[str, DescriptorNumber]: """Write the dependency map expression to make dependent Parameters. """ return { - 'scale': self._scale, + "scale": self._scale, } ################################ @@ -335,6 +337,6 @@ def __repr__(self): model. """ return ( - f'JumpTranslationalDiffusion(display_name={self.display_name}, ' - f'diffusion_coefficient={self._diffusion_coefficient}, scale={self._scale})' + f"JumpTranslationalDiffusion(display_name={self.display_name}, " + f"diffusion_coefficient={self._diffusion_coefficient}, scale={self._scale})" ) From 824786add607a2735ac3b621de98d893fa82fb3b Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Fri, 6 Feb 2026 11:52:09 +0100 Subject: [PATCH 07/17] test things in notebook --- docs/docs/tutorials/convolution.ipynb | 150 +++++++++++++++++++++++++- 1 file changed, 145 insertions(+), 5 deletions(-) diff --git a/docs/docs/tutorials/convolution.ipynb b/docs/docs/tutorials/convolution.ipynb index 922970f9..6d864c64 100644 --- a/docs/docs/tutorials/convolution.ipynb +++ b/docs/docs/tutorials/convolution.ipynb @@ -16,7 +16,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "1", "metadata": {}, "outputs": [], @@ -37,10 +37,36 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "2", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "a41109d24dec4f28bc04854ecb5c0a21", + "version_major": 2, + "version_minor": 0 + }, + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAoAAAAHgCAYAAAA10dzkAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjcsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvTLEjVAAAAAlwSFlzAAAPYQAAD2EBqD+naQAArRFJREFUeJzs3Qd4U9X7B/BvVvdeQNlQ9t5btiCCoAiKCiiCC0XA7e+vuBAXbkQUBXGhgKACMmXI3nvvsqF00N0m+T/vaRPSnUIhSfP9+ETSm5vk5t7k5s17znmPxmw2m0FEREREbkPr6A0gIiIioluLASARERGRm2EASERERORmGAASERERuRkGgERERERuhgEgERERkZthAEhERETkZhgAEhEREbkZBoBEREREboYBIBEREZGbYQBIRERE5GYYABIRERG5GQaARERERG6GASARERGRm2EASERERORmGAASERERuRkGgERERERuhgEgERERkZthAEhERETkZhgAEhEREbkZBoBEREREboYBIBEREZGbYQBIRERE5GYYABIRERG5GQaARERERG6GASARERGRm2EASERERORmGAASERERuRkGgERERERuhgEgERERkZthAEhERETkZhgAEhEREbkZBoBEREREboYBIBEREZGbYQBITmnlypXQaDTq35L08MMPo0qVKnBmiYmJGD58OMqWLav2wejRo1EavfHGG+r1lQbyOuT1FNeJEyfUfadPn35Ttqs473dZ18/PD6VVp06d1KUk3ezjV9rOzbKf5L6y38jxGACWMkePHsXjjz+OatWqwcvLCwEBAWjXrh0+++wzpKSkwB2cPXtWfRnv2LEDrujdd99VJ8onn3wSP/74IwYPHlzguunp6erYNmnSRB3roKAg1KtXD4899hgOHDgAd2L5cpHLmjVr8txuNptRsWJFdXvv3r3hjpKTk9Vno6R/WAkJriz7Xy7e3t5o2LAhPv30U5hMJriyX375Rb0OZyIBu+xn+dznd24/fPiw9Vh89NFHDtlGcm56R28AlZwFCxZgwIAB8PT0xJAhQ1C/fn0VIMiX4QsvvIC9e/fim2++cYsA8M0331SZj8aNG+e47dtvv3X6L6N///0XrVu3xrhx44pct3///vjnn38waNAgjBgxAhkZGSrwmz9/Ptq2bYvatWvD3cgPH/nCbt++fY7lq1at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", + "text/html": [ + "\n", + "
\n", + "
\n", + " Figure\n", + "
\n", + " \n", + "
\n", + " " + ], + "text/plain": [ + "Canvas(toolbar=Toolbar(toolitems=[('Home', 'Reset original view', 'home', 'home'), ('Back', 'Back to previous …" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "# Standard example of convolution of a sample model with a\n", "# resolution model\n", @@ -83,10 +109,36 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "3", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "aeb582a159c74ff6b51ca384adb1a903", + "version_major": 2, + "version_minor": 0 + }, + "image/png": "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", + "text/html": [ + "\n", + "
\n", + "
\n", + " Figure\n", + "
\n", + " \n", + "
\n", + " " + ], + "text/plain": [ + "Canvas(toolbar=Toolbar(toolitems=[('Home', 'Reset original view', 'home', 'home'), ('Back', 'Back to previous …" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "# Use some of the extra settings for the numerical convolution\n", "sample_components = ComponentCollection()\n", @@ -145,6 +197,94 @@ "plt.ylim(0, 2.5)\n", "plt.show()" ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "c318f9b8", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "e7d510c769bb4fca9c0f54ca3f0431bd", + "version_major": 2, + "version_minor": 0 + }, + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAoAAAAHgCAYAAAA10dzkAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjcsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvTLEjVAAAAAlwSFlzAAAPYQAAD2EBqD+naQAAsJdJREFUeJzs3QV4U1cbB/B/XWmp0hZ3d3d33waMjeFsgwkb2/jGjBkTNmTCYIzh23DYkOHu7u6lUFraUvfme94T0lWhpRL7/3guSW9uck9ukps3R95jodFoNCAiIiIis2Gp7wIQERERUeFiAEhERERkZhgAEhEREZkZBoBEREREZoYBIBEREZGZYQBIREREZGYYABIRERGZGQaARERERGaGASARERGRmWEASERERGRmGAASERERmRkGgERERERmhgEgERERkZlhAEhERERkZhgAEhEREZkZBoBEREREZoYBIBEREZGZYQBIREREZGYYABIRERGZGQaARERERGaGASARERGRmWEASERERGRmGAASERERmRkGgERERERmhgEgERERkZlhAEhERERkZhgAEhEREZkZBoBEREREZoYBIBEREZGZYQBIREREZGYYABIRERGZGQaARERERGaGASARERGRmWEASERERGRmGACSQdq5cycsLCzUZX4aOnQoypQpA0MWFRWFkSNHwsfHRx2Dt956C6bo008/Vc/PFMjzkOeTWzdv3lT3nT9/foGUKzfvd9nW2dkZpqpNmzZqyU8F/fqZ2rlZjpPcV44b6R8DQBNz7do1vPLKKyhXrhzs7e3h4uKC5s2b44cffkBsbCzMwd27d9WX8cmTJ2GMvvrqK3WiHD16NBYtWoSXXnop220TEhLUa1u3bl31WhctWhTVq1fHyy+/jIsXL8Kc6L5cZNm7d2+m2zUaDUqWLKlu79GjB8xRTEyM+mzk9w8rIcGV7vjL4uDggFq1amH69OlISUmBMfvzzz/V8zAkErDLcZbPfVbn9itXrqS+Ft9//71eykiGzVrfBaD8s379evTr1w92dnYYPHgwatSooQIE+TJ87733cO7cOcyePdssAsDPPvtM1XzUqVMn3W2//fabwX8Zbd++HU2aNMHEiROfuO2zzz6Lf//9FwMHDsSoUaOQmJioAr9169ahWbNmqFKlCsyN/PCRL+wWLVqkW79r1y7cuXNHfT7M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m7y8PHPq1CmTkJBgqlevbs6fP2/sCOeV4xzDT9nXX3+tf+M8BJQNZXT47bffTFhYmJkyZYoev8WLF5uQkBCza9cuY1dlLeM333xjtm3bZnJycvRziR74lStX1u9OO/rggw+053lGRobld6+goMC5jr+fh3bGANAGMPwCTu7iJgf8gGIe3fwdECRMmDDB1K1bV7/YBg4caAka7QZDuhRXRtcyYR7vB+BLIDY21oSHh+sJHhERYcaOHev8AfP38jmGgpkxY4Zp0KCB/ljjIiA7O9vY0Z07dzTgQ3Bbu3ZtM3LkSEtw6/4Zzc/P12CvXr16WjZc5ODH9/79+8YuUlNT9SKqatWqOmzK0aNHLUPY4Ji62rRpk2ndurWujyFFduzYYeysLOWbNGmSc118HuPi4szp06eNXTmGPHGfHGXC/yij+3M6deqkZcTFiOu5aEdlLeP8+fNNixYtNHDHederVy+tILArT797rsclEM5Du6qEfyq6FpKIiIiIyg97ARMREREFGQaAREREREGGASARERFRkGEASERERBRkGAASERERBRkGgERERERBhgEgERERUZBhAEhE9AxwS8KXXnpJLl++bIv3LyEhQb766quK3g0i8hMMAInIp0aMGCGVKlV6aurbt69fv/Nz5syR+Ph4adasmc9eA/dexnt19OjRYh+PiYmRQYMG6d/Tp0/Xfbp//77P9oeIAgcDQCLyOQR7N27csEwbNmzw6Wv+888/Ptt2QUGBfPfddzJ69GjxpS5dukjHjh1l1apVTz2GmscDBw4496Fdu3bSokULWb9+vU/3iYgCAwNAIvK5atWqScOGDS1T3bp1nY+jlmvlypUycOBACQsLk1atWslPP/1k2ca5c+ekX79+UqtWLWnQoIEMGzZMbt++7Xy8V69eMnHiRJk0aZLUr19f+vTpo8uxHWyvevXq8uabb8r333+vr3fv3j35+++/pXbt2rJ582bLa23btk1q1qwpDx48KLY8P//8s5bp9ddfdy7LyMjQ7e7evVuio6OlRo0a8tZbb8kff/whO3fulFdffVVfa/DgwRpAOjx58kTmzp0rzZs31+cg4HPdHwR4GzdutDwH1qxZIy+//LKlJvXdd9+VtLS0Mh0bIgpODACJyBZmzZol77//vmRlZUlcXJwMGTJE/vzzT30MwRqCKQRWJ0+elF27dsmtW7d0fVcI7qpWrSqZmZmybNkyuXTpkrz33nsyYMAAOXPmjCQlJcm0adOc6yPIQ+7c6tWrLdvBPJ73wgsvFLuvhw4d0tq54nz++eeyaNEiOXLkiFy5ckX38dtvv5Uff/xRduzYIXv27JHU1FTn+gj+1q5dq/t7/vx5+fjjj2Xo0KFy8OBBfRzvw6NHjyxBIW7hjrKieT0kJMS5vGvXrnL8+HFdn4jIK0NE5EPDhw83ISEhpmbNmpZpzpw5znXwVTR9+nTn/MOHD3XZzp07dX727NkmNjbWst0rV67oOtnZ2Trfs2dPEx0dbVknOTnZtGvXzrJs2rRp+ry7d+/q/LFjx3T/rl+/rvO3bt0yoaGhJiMjw2OZ4uPjzahRoyzLDhw4oNvdu3evc9ncuXN1WV5ennNZUlKS6dOnj/5dVFRkwsLCzJEjRyzbGj16tElMTHTOJyQkaPkc9u3bp9vNycmxPO/MmTO6/PLlyx73nYgIQr2Hh0REzw9Nr0uXLrUsq1evnmW+Q4cOlpo5NJei+RRQe4d8NzT/usvLy5PWrVvr3+61ctnZ2fLaa69ZlqGWzH2+bdu2WqOWkpKiOXQRERHSo0cPj+UpLCzUJuXiuJYDTdVo0o6MjLQsQy0d5ObmatNu7969n8pfRG2nw6hRo7RJG2VFnh9yAnv27CktW7a0PA9NyODeXExE5I4BIBH5HAI692DFXZUqVSzzyKdDfhw8fPhQ89vmz5//1POQB+f6Os9izJgxsnjxYg0A0fw7cuRIfX1PkGN49+7dEsuBbZRULkDTcKNGjSzrIcfQtbdv06ZNNe9vypQpkp6eLsuXL3/qtR1N5uHh4aUsOREFKwaARGR7nTt3li1btuiQK6Ghpf/aioqK0g4brk6cOPHUesi5+/TTT2XhwoVy4cIFGT58uNftonbu/+ht26ZNGw308vPztUbPk8qVK2tQip7HCBSR54gcRXfoKNO4cWMNUImIvGEnECLyOXRKuHnzpmVy7cFbkg8//FBrtxITEzWAQ1MoetsiKHr8+LHH56HTx8WLFyU5OVl+/fVX2bRpk9aigWsNH3okYzw91K7FxsZqEOUNmmPRYcNTLWBpoZPJ5MmTteMHmqBRrtOnT2snEcy7QlmvXbsmn332mb4PjuZe984p2H8iopIwACQin0OvXTTVuk5vvPFGqZ//yiuvaM9eBHsIcNq3b6/DvdSpU0drxzzB0CroPYsmU+TmIQ/R0QvYtYnVMdwKcu+Qb1cSvD5qJRFQPq/Zs2fLjBkztDcwhorBsC5oEsa+u0IT8Ntvv61BZ3H7WFRUpMPXjB079rn3iYgCXyX0BKnonSAiKi+4WwaGXMEQLa7WrVunNXHXr1/XJtaSIEhDjSGaXb0FoeUFwe3WrVt1mBkiopIwB5CIAtqSJUu0J/CLL76otYhffvmlDhjtgB6zuDPJvHnztMm4NMEfvPPOO5KTk6PNsk2aNJGKhs4mruMLEhF5wxpAIgpoqNXDnTSQQ4hmVNxBZOrUqc7OJBi4GbWCGPZl+/btxQ41Q0QUaBgAEhEREQWZik9cISIiIqJyxQCQiIiIKMgwACQiIiIKMgwAiYiIiIIMA0AiIiKiIMMAkIiIiCjIMAAkIiIiCjIMAImIiIiCDANAIiIiIgku/wElHoO9N2L94gAAAABJRU5ErkJggg==", + "text/html": [ + "\n", + "
\n", + "
\n", + " Figure\n", + "
\n", + " \n", + "
\n", + " " + ], + "text/plain": [ + "Canvas(toolbar=Toolbar(toolitems=[('Home', 'Reset original view', 'home', 'home'), ('Back', 'Back to previous …" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Use some of the extra settings for the numerical convolution\n", + "sample_components = ComponentCollection()\n", + "gaussian = Gaussian(display_name='Gaussian', width=0.3, area=1)\n", + "dho = DampedHarmonicOscillator(display_name='DHO', center=1.0, width=0.3, area=2.0)\n", + "lorentzian = Lorentzian(display_name='Lorentzian', center=-1.0, width=0.2, area=1.0)\n", + "delta = DeltaFunction(display_name='Delta', center=0.4, area=0.5)\n", + "sample_components.append_component(gaussian)\n", + "# sample_components.append_component(dho)\n", + "sample_components.append_component(lorentzian)\n", + "# sample_components.append_component(delta)\n", + "\n", + "resolution_components = ComponentCollection()\n", + "resolution_gaussian = Gaussian(display_name='Resolution Gaussian', width=0.15, area=0.8)\n", + "resolution_lorentzian = Lorentzian(display_name='Resolution Lorentzian', width=0.25, area=0.2)\n", + "resolution_components.append_component(resolution_gaussian)\n", + "# resolution_components.append_component(resolution_lorentzian)\n", + "\n", + "energy = np.linspace(-2, 2, 100)\n", + "\n", + "\n", + "temperature = 10.0 # Temperature in Kelvin\n", + "offset = 0.5\n", + "upsample_factor = 5\n", + "extension_factor = 0.5\n", + "plt.figure()\n", + "plt.xlabel('Energy (meV)')\n", + "plt.ylabel('Intensity (arb. units)')\n", + "\n", + "convolver = Convolution(\n", + " sample_components=sample_components,\n", + " resolution_components=resolution_components,\n", + " energy=energy - offset,\n", + " upsample_factor=upsample_factor,\n", + " extension_factor=extension_factor,\n", + ")\n", + "y = convolver.convolution()\n", + "\n", + "\n", + "plt.plot(energy, y, label='Convoluted Model')\n", + "\n", + "plt.plot(\n", + " energy,\n", + " sample_components.evaluate(energy - offset),\n", + " label='Sample Model',\n", + " linestyle='--',\n", + ")\n", + "\n", + "plt.plot(energy, resolution_components.evaluate(energy), label='Resolution Model', linestyle=':')\n", + "plt.title('Convolution of Sample Model with Resolution Model')\n", + "\n", + "plt.legend()\n", + "plt.ylim(0, 2.5)\n", + "plt.show()" + ] } ], "metadata": { From 4a274d2158cd13f7fdf63207a3960b6eddf0ab5b Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Fri, 6 Feb 2026 12:24:18 +0100 Subject: [PATCH 08/17] reintroduce energy_offset in convolution. It's needed. --- docs/docs/tutorials/convolution.ipynb | 108 +++--------------- .../convolution/analytical_convolution.py | 51 +++++---- src/easydynamics/convolution/convolution.py | 64 ++++++----- .../convolution/convolution_base.py | 95 ++++++++++++--- .../convolution/numerical_convolution.py | 31 ++--- .../convolution/numerical_convolution_base.py | 2 + 6 files changed, 179 insertions(+), 172 deletions(-) diff --git a/docs/docs/tutorials/convolution.ipynb b/docs/docs/tutorials/convolution.ipynb index 6d864c64..b13d7973 100644 --- a/docs/docs/tutorials/convolution.ipynb +++ b/docs/docs/tutorials/convolution.ipynb @@ -16,7 +16,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "1", "metadata": {}, "outputs": [], @@ -37,36 +37,10 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "2", "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "a41109d24dec4f28bc04854ecb5c0a21", - "version_major": 2, - "version_minor": 0 - }, - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAoAAAAHgCAYAAAA10dzkAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjcsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvTLEjVAAAAAlwSFlzAAAPYQAAD2EBqD+naQAArRFJREFUeJzs3Qd4U9X7B/BvVvdeQNlQ9t5btiCCoAiKCiiCC0XA7e+vuBAXbkQUBXGhgKACMmXI3nvvsqF00N0m+T/vaRPSnUIhSfP9+ETSm5vk5t7k5s17znmPxmw2m0FEREREbkPr6A0gIiIioluLASARERGRm2EASERERORmGAASERERuRkGgERERERuhgEgERERkZthAEhERETkZhgAEhEREbkZBoBEREREboYBIBEREZGbYQBIRERE5GYYABIRERG5GQaARERERG6GASARERGRm2EASERERORmGAASERERuRkGgERERERuhgEgERERkZthAEhERETkZhgAEhEREbkZBoBEREREboYBIBEREZGbYQBIRERE5GYYABIRERG5GQaARERERG6GASARERGRm2EASERERORmGAASERERuRkGgERERERuhgEgERERkZthAEhERETkZhgAEhEREbkZBoBEREREboYBIBEREZGbYQBITmnlypXQaDTq35L08MMPo0qVKnBmiYmJGD58OMqWLav2wejRo1EavfHGG+r1lQbyOuT1FNeJEyfUfadPn35Ttqs473dZ18/PD6VVp06d1KUk3ezjV9rOzbKf5L6y38jxGACWMkePHsXjjz+OatWqwcvLCwEBAWjXrh0+++wzpKSkwB2cPXtWfRnv2LEDrujdd99VJ8onn3wSP/74IwYPHlzguunp6erYNmnSRB3roKAg1KtXD4899hgOHDgAd2L5cpHLmjVr8txuNptRsWJFdXvv3r3hjpKTk9Vno6R/WAkJriz7Xy7e3t5o2LAhPv30U5hMJriyX375Rb0OZyIBu+xn+dznd24/fPiw9Vh89NFHDtlGcm56R28AlZwFCxZgwIAB8PT0xJAhQ1C/fn0VIMiX4QsvvIC9e/fim2++cYsA8M0331SZj8aNG+e47dtvv3X6L6N///0XrVu3xrhx44pct3///vjnn38waNAgjBgxAhkZGSrwmz9/Ptq2bYvatWvD3cgPH/nCbt++fY7lq1at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", - "text/html": [ - "\n", - "
\n", - "
\n", - " Figure\n", - "
\n", - " \n", - "
\n", - " " - ], - "text/plain": [ - "Canvas(toolbar=Toolbar(toolitems=[('Home', 'Reset original view', 'home', 'home'), ('Back', 'Back to previous …" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "# Standard example of convolution of a sample model with a\n", "# resolution model\n", @@ -109,36 +83,10 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "3", "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "aeb582a159c74ff6b51ca384adb1a903", - "version_major": 2, - "version_minor": 0 - }, - "image/png": "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", - "text/html": [ - "\n", - "
\n", - "
\n", - " Figure\n", - "
\n", - " \n", - "
\n", - " " - ], - "text/plain": [ - "Canvas(toolbar=Toolbar(toolitems=[('Home', 'Reset original view', 'home', 'home'), ('Back', 'Back to previous …" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "# Use some of the extra settings for the numerical convolution\n", "sample_components = ComponentCollection()\n", @@ -161,7 +109,7 @@ "\n", "\n", "temperature = 10.0 # Temperature in Kelvin\n", - "offset = 0.5\n", + "energy_offset = 0.5\n", "upsample_factor = 5\n", "extension_factor = 0.5\n", "plt.figure()\n", @@ -171,7 +119,7 @@ "convolver = Convolution(\n", " sample_components=sample_components,\n", " resolution_components=resolution_components,\n", - " energy=energy - offset,\n", + " energy=energy - energy_offset,\n", " upsample_factor=upsample_factor,\n", " extension_factor=extension_factor,\n", " temperature=temperature,\n", @@ -184,8 +132,8 @@ "\n", "plt.plot(\n", " energy,\n", - " sample_components.evaluate(energy - offset)\n", - " * detailed_balance_factor(energy - offset, temperature),\n", + " sample_components.evaluate(energy - energy_offset)\n", + " * detailed_balance_factor(energy - energy_offset, temperature),\n", " label='Sample Model with DB',\n", " linestyle='--',\n", ")\n", @@ -200,36 +148,10 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "id": "c318f9b8", "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "e7d510c769bb4fca9c0f54ca3f0431bd", - "version_major": 2, - "version_minor": 0 - }, - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAoAAAAHgCAYAAAA10dzkAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjcsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvTLEjVAAAAAlwSFlzAAAPYQAAD2EBqD+naQAAsJdJREFUeJzs3QV4U1cbB/B/XWmp0hZ3d3d33waMjeFsgwkb2/jGjBkTNmTCYIzh23DYkOHu7u6lUFraUvfme94T0lWhpRL7/3guSW9uck9ukps3R95jodFoNCAiIiIis2Gp7wIQERERUeFiAEhERERkZhgAEhEREZkZBoBEREREZoYBIBEREZGZYQBIREREZGYYABIRERGZGQaARERERGaGASARERGRmWEASERERGRmGAASERERmRkGgERERERmhgEgERERkZlhAEhERERkZhgAEhEREZkZBoBEREREZoYBIBEREZGZYQBIREREZGYYABIRERGZGQaARERERGaGASARERGRmWEASERERGRmGAASERERmRkGgERERERmhgEgERERkZlhAEhERERkZhgAEhEREZkZBoBEREREZoYBIBEREZGZYQBIREREZGYYABIRERGZGQaARERERGaGASARERGRmWEASERERGRmGACSQdq5cycsLCzUZX4aOnQoypQpA0MWFRWFkSNHwsfHRx2Dt956C6bo008/Vc/PFMjzkOeTWzdv3lT3nT9/foGUKzfvd9nW2dkZpqpNmzZqyU8F/fqZ2rlZjpPcV44b6R8DQBNz7do1vPLKKyhXrhzs7e3h4uKC5s2b44cffkBsbCzMwd27d9WX8cmTJ2GMvvrqK3WiHD16NBYtWoSXXnop220TEhLUa1u3bl31WhctWhTVq1fHyy+/jIsXL8Kc6L5cZNm7d2+m2zUaDUqWLKlu79GjB8xRTEyM+mzk9w8rIcGV7vjL4uDggFq1amH69OlISUmBMfvzzz/V8zAkErDLcZbPfVbn9itXrqS+Ft9//71eykiGzVrfBaD8s379evTr1w92dnYYPHgwatSooQIE+TJ87733cO7cOcyePdssAsDPPvtM1XzUqVMn3W2//fabwX8Zbd++HU2aNMHEiROfuO2zzz6Lf//9FwMHDsSoUaOQmJioAr9169ahWbNmqFKlCsyN/PCRL+wWLVqkW79r1y7cuXNHfT7M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m7y8PHPq1CmTkJBgqlevbs6fP2/sCOeV4xzDT9nXX3+tf+M8BJQNZXT47bffTFhYmJkyZYoev8WLF5uQkBCza9cuY1dlLeM333xjtm3bZnJycvRziR74lStX1u9OO/rggw+053lGRobld6+goMC5jr+fh3bGANAGMPwCTu7iJgf8gGIe3fwdECRMmDDB1K1bV7/YBg4caAka7QZDuhRXRtcyYR7vB+BLIDY21oSHh+sJHhERYcaOHev8AfP38jmGgpkxY4Zp0KCB/ljjIiA7O9vY0Z07dzTgQ3Bbu3ZtM3LkSEtw6/4Zzc/P12CvXr16WjZc5ODH9/79+8YuUlNT9SKqatWqOmzK0aNHLUPY4Ji62rRpk2ndurWujyFFduzYYeysLOWbNGmSc118HuPi4szp06eNXTmGPHGfHGXC/yij+3M6deqkZcTFiOu5aEdlLeP8+fNNixYtNHDHederVy+tILArT797rsclEM5Du6qEfyq6FpKIiIiIyg97ARMREREFGQaAREREREGGASARERFRkGEASERERBRkGAASERERBRkGgERERERBhgEgERERUZBhAEhE9AxwS8KXXnpJLl++bIv3LyEhQb766quK3g0i8hMMAInIp0aMGCGVKlV6aurbt69fv/Nz5syR+Ph4adasmc9eA/dexnt19OjRYh+PiYmRQYMG6d/Tp0/Xfbp//77P9oeIAgcDQCLyOQR7N27csEwbNmzw6Wv+888/Ptt2QUGBfPfddzJ69GjxpS5dukjHjh1l1apVTz2GmscDBw4496Fdu3bSokULWb9+vU/3iYgCAwNAIvK5atWqScOGDS1T3bp1nY+jlmvlypUycOBACQsLk1atWslPP/1k2ca5c+ekX79+UqtWLWnQoIEMGzZMbt++7Xy8V69eMnHiRJk0aZLUr19f+vTpo8uxHWyvevXq8uabb8r333+vr3fv3j35+++/pXbt2rJ582bLa23btk1q1qwpDx48KLY8P//8s5bp9ddfdy7LyMjQ7e7evVuio6OlRo0a8tZbb8kff/whO3fulFdffVVfa/DgwRpAOjx58kTmzp0rzZs31+cg4HPdHwR4GzdutDwH1qxZIy+//LKlJvXdd9+VtLS0Mh0bIgpODACJyBZmzZol77//vmRlZUlcXJwMGTJE/vzzT30MwRqCKQRWJ0+elF27dsmtW7d0fVcI7qpWrSqZmZmybNkyuXTpkrz33nsyYMAAOXPmjCQlJcm0adOc6yPIQ+7c6tWrLdvBPJ73wgsvFLuvhw4d0tq54nz++eeyaNEiOXLkiFy5ckX38dtvv5Uff/xRduzYIXv27JHU1FTn+gj+1q5dq/t7/vx5+fjjj2Xo0KFy8OBBfRzvw6NHjyxBIW7hjrKieT0kJMS5vGvXrnL8+HFdn4jIK0NE5EPDhw83ISEhpmbNmpZpzpw5znXwVTR9+nTn/MOHD3XZzp07dX727NkmNjbWst0rV67oOtnZ2Trfs2dPEx0dbVknOTnZtGvXzrJs2rRp+ry7d+/q/LFjx3T/rl+/rvO3bt0yoaGhJiMjw2OZ4uPjzahRoyzLDhw4oNvdu3evc9ncuXN1WV5ennNZUlKS6dOnj/5dVFRkwsLCzJEjRyzbGj16tElMTHTOJyQkaPkc9u3bp9vNycmxPO/MmTO6/PLlyx73nYgIQr2Hh0REzw9Nr0uXLrUsq1evnmW+Q4cOlpo5NJei+RRQe4d8NzT/usvLy5PWrVvr3+61ctnZ2fLaa69ZlqGWzH2+bdu2WqOWkpKiOXQRERHSo0cPj+UpLCzUJuXiuJYDTdVo0o6MjLQsQy0d5ObmatNu7969n8pfRG2nw6hRo7RJG2VFnh9yAnv27CktW7a0PA9NyODeXExE5I4BIBH5HAI692DFXZUqVSzzyKdDfhw8fPhQ89vmz5//1POQB+f6Os9izJgxsnjxYg0A0fw7cuRIfX1PkGN49+7dEsuBbZRULkDTcKNGjSzrIcfQtbdv06ZNNe9vypQpkp6eLsuXL3/qtR1N5uHh4aUsOREFKwaARGR7nTt3li1btuiQK6Ghpf/aioqK0g4brk6cOPHUesi5+/TTT2XhwoVy4cIFGT58uNftonbu/+ht26ZNGw308vPztUbPk8qVK2tQip7HCBSR54gcRXfoKNO4cWMNUImIvGEnECLyOXRKuHnzpmVy7cFbkg8//FBrtxITEzWAQ1MoetsiKHr8+LHH56HTx8WLFyU5OVl+/fVX2bRpk9aigWsNH3okYzw91K7FxsZqEOUNmmPRYcNTLWBpoZPJ5MmTteMHmqBRrtOnT2snEcy7QlmvXbsmn332mb4PjuZe984p2H8iopIwACQin0OvXTTVuk5vvPFGqZ//yiuvaM9eBHsIcNq3b6/DvdSpU0drxzzB0CroPYsmU+TmIQ/R0QvYtYnVMdwKcu+Qb1cSvD5qJRFQPq/Zs2fLjBkztDcwhorBsC5oEsa+u0IT8Ntvv61BZ3H7WFRUpMPXjB079rn3iYgCXyX0BKnonSAiKi+4WwaGXMEQLa7WrVunNXHXr1/XJtaSIEhDjSGaXb0FoeUFwe3WrVt1mBkiopIwB5CIAtqSJUu0J/CLL76otYhffvmlDhjtgB6zuDPJvHnztMm4NMEfvPPOO5KTk6PNsk2aNJGKhs4mruMLEhF5wxpAIgpoqNXDnTSQQ4hmVNxBZOrUqc7OJBi4GbWCGPZl+/btxQ41Q0QUaBgAEhEREQWZik9cISIiIqJyxQCQiIiIKMgwACQiIiIKMgwAiYiIiIIMA0AiIiKiIMMAkIiIiCjIMAAkIiIiCjIMAImIiIiCDANAIiIiIgku/wElHoO9N2L94gAAAABJRU5ErkJggg==", - "text/html": [ - "\n", - "
\n", - "
\n", - " Figure\n", - "
\n", - " \n", - "
\n", - " " - ], - "text/plain": [ - "Canvas(toolbar=Toolbar(toolitems=[('Home', 'Reset original view', 'home', 'home'), ('Back', 'Back to previous …" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "# Use some of the extra settings for the numerical convolution\n", "sample_components = ComponentCollection()\n", @@ -252,7 +174,7 @@ "\n", "\n", "temperature = 10.0 # Temperature in Kelvin\n", - "offset = 0.5\n", + "energy_offset = 0.2\n", "upsample_factor = 5\n", "extension_factor = 0.5\n", "plt.figure()\n", @@ -262,9 +184,11 @@ "convolver = Convolution(\n", " sample_components=sample_components,\n", " resolution_components=resolution_components,\n", - " energy=energy - offset,\n", + " energy=energy,\n", " upsample_factor=upsample_factor,\n", " extension_factor=extension_factor,\n", + " energy_offset=energy_offset,\n", + " temperature=temperature,\n", ")\n", "y = convolver.convolution()\n", "\n", @@ -273,7 +197,7 @@ "\n", "plt.plot(\n", " energy,\n", - " sample_components.evaluate(energy - offset),\n", + " sample_components.evaluate(energy - energy_offset),\n", " label='Sample Model',\n", " linestyle='--',\n", ")\n", diff --git a/src/easydynamics/convolution/analytical_convolution.py b/src/easydynamics/convolution/analytical_convolution.py index cfa56c9f..c24a50f2 100644 --- a/src/easydynamics/convolution/analytical_convolution.py +++ b/src/easydynamics/convolution/analytical_convolution.py @@ -3,6 +3,7 @@ import numpy as np import scipp as sc +from easyscience.variable import Parameter from scipy.special import voigt_profile from easydynamics.convolution.convolution_base import ConvolutionBase @@ -12,8 +13,7 @@ from easydynamics.sample_model import Voigt from easydynamics.sample_model.component_collection import ComponentCollection from easydynamics.sample_model.components.model_component import ModelComponent - -Numerical = float | int +from easydynamics.utils.utils import Numeric class AnalyticalConvolution(ConvolutionBase): @@ -35,26 +35,28 @@ class AnalyticalConvolution(ConvolutionBase): # Mapping of supported component type pairs to convolution methods. # Delta functions are handled separately. _CONVOLUTIONS = { - ('Gaussian', 'Gaussian'): '_convolute_gaussian_gaussian', - ('Gaussian', 'Lorentzian'): '_convolute_gaussian_lorentzian', - ('Gaussian', 'Voigt'): '_convolute_gaussian_voigt', - ('Lorentzian', 'Lorentzian'): '_convolute_lorentzian_lorentzian', - ('Lorentzian', 'Voigt'): '_convolute_lorentzian_voigt', - ('Voigt', 'Voigt'): '_convolute_voigt_voigt', + ("Gaussian", "Gaussian"): "_convolute_gaussian_gaussian", + ("Gaussian", "Lorentzian"): "_convolute_gaussian_lorentzian", + ("Gaussian", "Voigt"): "_convolute_gaussian_voigt", + ("Lorentzian", "Lorentzian"): "_convolute_lorentzian_lorentzian", + ("Lorentzian", "Voigt"): "_convolute_lorentzian_voigt", + ("Voigt", "Voigt"): "_convolute_voigt_voigt", } def __init__( self, energy: np.ndarray | sc.Variable, - energy_unit: str | sc.Unit = 'meV', + energy_unit: str | sc.Unit = "meV", sample_components: ComponentCollection | ModelComponent | None = None, resolution_components: ComponentCollection | ModelComponent | None = None, + energy_offset: Numeric | Parameter = 0.0, ): super().__init__( energy=energy, energy_unit=energy_unit, sample_components=sample_components, resolution_components=resolution_components, + energy_offset=energy_offset, ) def convolution( @@ -142,8 +144,8 @@ def _convolute_analytic_pair( if isinstance(resolution_component, DeltaFunction): raise ValueError( - 'Analytical convolution with a delta function \ - in the resolution model is not supported.' + "Analytical convolution with a delta function \ + in the resolution model is not supported." ) # Delta function + anything --> @@ -169,8 +171,8 @@ def _convolute_analytic_pair( if func_name is None: raise ValueError( - f'Analytical convolution not supported for component pair: ' - f'{type(sample_component).__name__}, {type(resolution_component).__name__}' + f"Analytical convolution not supported for component pair: " + f"{type(sample_component).__name__}, {type(resolution_component).__name__}" ) # Call the corresponding method @@ -199,7 +201,7 @@ def _convolute_delta_any( The evaluated convolution values at self.energy. """ return sample_component.area.value * resolution_components.evaluate( - self.energy.values - sample_component.center.value + self.energy_with_offset.values - sample_component.center.value ) def _convolute_gaussian_gaussian( @@ -223,7 +225,9 @@ def _convolute_gaussian_gaussian( The evaluated convolution values at self.energy. """ - width = np.sqrt(sample_component.width.value**2 + resolution_component.width.value**2) + width = np.sqrt( + sample_component.width.value**2 + resolution_component.width.value**2 + ) area = sample_component.area.value * resolution_component.area.value @@ -284,7 +288,8 @@ def _convolute_gaussian_voigt( center = sample_component.center.value + resolution_component.center.value gaussian_width = np.sqrt( - sample_component.width.value**2 + resolution_component.gaussian_width.value**2 + sample_component.width.value**2 + + resolution_component.gaussian_width.value**2 ) lorentzian_width = resolution_component.lorentzian_width.value @@ -384,11 +389,13 @@ def _convolute_voigt_voigt( center = sample_component.center.value + resolution_component.center.value gaussian_width = np.sqrt( - sample_component.gaussian_width.value**2 + resolution_component.gaussian_width.value**2 + sample_component.gaussian_width.value**2 + + resolution_component.gaussian_width.value**2 ) lorentzian_width = ( - sample_component.lorentzian_width.value + resolution_component.lorentzian_width.value + sample_component.lorentzian_width.value + + resolution_component.lorentzian_width.value ) return self._voigt_eval( area=area, @@ -420,7 +427,7 @@ def _gaussian_eval( """ normalization = 1 / (np.sqrt(2 * np.pi) * width) - exponent = -0.5 * ((self.energy.values - center) / width) ** 2 + exponent = -0.5 * ((self.energy_with_offset.values - center) / width) ** 2 return area * normalization * np.exp(exponent) @@ -443,7 +450,7 @@ def _lorentzian_eval(self, area: float, center: float, width: float) -> np.ndarr """ normalization = width / np.pi - denominator = (self.energy.values - center) ** 2 + width**2 + denominator = (self.energy_with_offset.values - center) ** 2 + width**2 return area * normalization / denominator @@ -471,4 +478,6 @@ def _voigt_eval( The evaluated Voigt profile values at self.energy. """ - return area * voigt_profile(self.energy.values - center, gaussian_width, lorentzian_width) + return area * voigt_profile( + self.energy_with_offset.values - center, gaussian_width, lorentzian_width + ) diff --git a/src/easydynamics/convolution/convolution.py b/src/easydynamics/convolution/convolution.py index 542515e9..827087c1 100644 --- a/src/easydynamics/convolution/convolution.py +++ b/src/easydynamics/convolution/convolution.py @@ -14,8 +14,7 @@ from easydynamics.sample_model import Lorentzian from easydynamics.sample_model import Voigt from easydynamics.sample_model.components.model_component import ModelComponent - -Numerical = float | int +from easydynamics.utils.utils import Numeric class Convolution(NumericalConvolutionBase): @@ -60,16 +59,16 @@ class Convolution(NumericalConvolutionBase): # When these attributes are changed, the convolution plan # needs to be rebuilt _invalidate_plan_on_change = { - 'energy', - '_energy', - '_energy_grid', - '_sample_components', - '_resolution_components', - '_temperature', - '_upsample_factor', - '_extension_factor', - '_energy_unit', - '_normalize_detailed_balance', + "energy", + "_energy", + "_energy_grid", + "_sample_components", + "_resolution_components", + "_temperature", + "_upsample_factor", + "_extension_factor", + "_energy_unit", + "_normalize_detailed_balance", } def __init__( @@ -77,11 +76,12 @@ def __init__( energy: np.ndarray | sc.Variable, sample_components: ComponentCollection | ModelComponent, resolution_components: ComponentCollection | ModelComponent, - upsample_factor: Numerical = 5, - extension_factor: Numerical = 0.2, - temperature: Parameter | Numerical | None = None, - temperature_unit: str | sc.Unit = 'K', - energy_unit: str | sc.Unit = 'meV', + energy_offset: Numeric | Parameter = 0.0, + upsample_factor: Numeric = 5, + extension_factor: Numeric = 0.2, + temperature: Parameter | Numeric | None = None, + temperature_unit: str | sc.Unit = "K", + energy_unit: str | sc.Unit = "meV", normalize_detailed_balance: bool = True, ): self._convolution_plan_is_valid = False @@ -90,6 +90,7 @@ def __init__( energy=energy, sample_components=sample_components, resolution_components=resolution_components, + energy_offset=energy_offset, upsample_factor=upsample_factor, extension_factor=extension_factor, temperature=temperature, @@ -136,11 +137,13 @@ def convolution( def _convolve_delta_functions(self) -> np.ndarray: "Convolve delta function components of the sample model with" - 'the resolution components.' - 'No detailed balance correction is applied to delta functions.' + "the resolution components." + "No detailed balance correction is applied to delta functions." return sum( delta.area.value - * self._resolution_components.evaluate(self.energy.values - delta.center.value) + * self._resolution_components.evaluate( + self.energy_with_offset.values - delta.center.value + ) for delta in self._delta_sample_components.components ) @@ -165,19 +168,19 @@ def _check_if_pair_is_analytic( if not isinstance(sample_component, ModelComponent): raise TypeError( - f'`sample_component` is an instance of {type(sample_component).__name__}, \ - but must be a ModelComponent.' + f"`sample_component` is an instance of {type(sample_component).__name__}, \ + but must be a ModelComponent." ) if not isinstance(resolution_component, ModelComponent): raise TypeError( - f'`resolution_component` is an instance of {type(resolution_component).__name__}, \ - but must be a ModelComponent.' + f"`resolution_component` is an instance of {type(resolution_component).__name__}, \ + but must be a ModelComponent." ) if isinstance(resolution_component, DeltaFunction): raise TypeError( - 'resolution components contains delta functions. This is not supported.' + "resolution components contains delta functions. This is not supported." ) analytical_types = (Gaussian, Lorentzian, Voigt) @@ -216,7 +219,9 @@ def _build_convolution_plan(self) -> None: pair_is_analytic = [] for resolution_component in self._resolution_components.components: pair_is_analytic.append( - self._check_if_pair_is_analytic(sample_component, resolution_component) + self._check_if_pair_is_analytic( + sample_component, resolution_component + ) ) # If all resolution components can be convolved analytically # with this sample component, add it to analytical @@ -245,6 +250,7 @@ def _set_convolvers(self) -> None: if self._analytical_sample_components.components: self._analytical_convolver = AnalyticalConvolution( energy=self.energy, + energy_offset=self.energy_offset, sample_components=self._analytical_sample_components, resolution_components=self._resolution_components, ) @@ -254,6 +260,7 @@ def _set_convolvers(self) -> None: if self._numerical_sample_components.components: self._numerical_convolver = NumericalConvolution( energy=self.energy, + energy_offset=self.energy_offset, sample_components=self._numerical_sample_components, resolution_components=self._resolution_components, upsample_factor=self.upsample_factor, @@ -278,5 +285,8 @@ def __setattr__(self, name, value): if name in self._invalidate_plan_on_change: self._convolution_plan_is_valid = False - if getattr(self, '_reactions_enabled', False) and name in self._invalidate_plan_on_change: + if ( + getattr(self, "_reactions_enabled", False) + and name in self._invalidate_plan_on_change + ): self._build_convolution_plan() diff --git a/src/easydynamics/convolution/convolution_base.py b/src/easydynamics/convolution/convolution_base.py index cfe364b0..9c212d64 100644 --- a/src/easydynamics/convolution/convolution_base.py +++ b/src/easydynamics/convolution/convolution_base.py @@ -3,11 +3,11 @@ import numpy as np import scipp as sc +from easyscience.variable import Parameter from easydynamics.sample_model.component_collection import ComponentCollection from easydynamics.sample_model.components.model_component import ModelComponent - -Numerical = float | int +from easydynamics.utils.utils import Numeric class ConvolutionBase: @@ -30,29 +30,39 @@ def __init__( energy: np.ndarray | sc.Variable, sample_components: ComponentCollection | ModelComponent = None, resolution_components: ComponentCollection | ModelComponent = None, - energy_unit: str | sc.Unit = 'meV', + energy_unit: str | sc.Unit = "meV", + energy_offset: Numeric | Parameter = 0.0, ): - if isinstance(energy, Numerical): + if isinstance(energy, Numeric): energy = np.array([float(energy)]) if not isinstance(energy, (np.ndarray, sc.Variable)): - raise TypeError('Energy must be a numpy ndarray or a scipp Variable.') + raise TypeError("Energy must be a numpy ndarray or a scipp Variable.") if not isinstance(energy_unit, (str, sc.Unit)): - raise TypeError('Energy_unit must be a string or sc.Unit.') + raise TypeError("Energy_unit must be a string or sc.Unit.") if isinstance(energy, np.ndarray): - energy = sc.array(dims=['energy'], values=energy, unit=energy_unit) + energy = sc.array(dims=["energy"], values=energy, unit=energy_unit) + + if isinstance(energy_offset, Numeric): + energy_offset = Parameter( + name="energy_offset", value=float(energy_offset), unit=energy_unit + ) + + if not isinstance(energy_offset, Parameter): + raise TypeError("Energy_offset must be a number or a Parameter.") self._energy = energy self._energy_unit = energy_unit + self._energy_offset = energy_offset if sample_components is not None and not ( isinstance(sample_components, ComponentCollection) or isinstance(sample_components, ModelComponent) ): raise TypeError( - f'`sample_components` is an instance of {type(sample_components).__name__}, but must be a ComponentCollection or ModelComponent.' # noqa: E501 + f"`sample_components` is an instance of {type(sample_components).__name__}, but must be a ComponentCollection or ModelComponent." # noqa: E501 ) if isinstance(sample_components, ModelComponent): sample_components = ComponentCollection(components=[sample_components]) @@ -63,12 +73,53 @@ def __init__( or isinstance(resolution_components, ModelComponent) ): raise TypeError( - f'`resolution_components` is an instance of {type(resolution_components).__name__}, but must be a ComponentCollection or ModelComponent.' # noqa: E501 + f"`resolution_components` is an instance of {type(resolution_components).__name__}, but must be a ComponentCollection or ModelComponent." # noqa: E501 ) if isinstance(resolution_components, ModelComponent): - resolution_components = ComponentCollection(components=[resolution_components]) + resolution_components = ComponentCollection( + components=[resolution_components] + ) self._resolution_components = resolution_components + @property + def energy_offset(self) -> Parameter: + """Get the energy offset.""" + return self._energy_offset + + @energy_offset.setter + def energy_offset(self, energy_offset: Numeric | Parameter) -> None: + """Set the energy offset. + Args: + energy_offset : Number or Parameter + The energy offset to apply to the convolution. + + Raises: + TypeError: If energy_offset is not a number or a Parameter. + """ + if not isinstance(energy_offset, Parameter | Numeric): + raise TypeError("Energy_offset must be a number or a Parameter.") + + if isinstance(energy_offset, Numeric): + self._energy_offset.value = float(energy_offset) + + if isinstance(energy_offset, Parameter): + self._energy_offset = energy_offset + + @property + def energy_with_offset(self) -> sc.Variable: + """Get the energy with the offset applied.""" + energy_with_offset = self.energy.copy() + energy_with_offset.values = self.energy.values - self.energy_offset.value + return energy_with_offset + + @energy_with_offset.setter + def energy_with_offset(self, value) -> None: + """Energy with offset is a read-only property derived from + energy and energy_offset.""" + raise AttributeError( + "Energy with offset is a read-only property derived from energy and energy_offset." + ) + @property def energy(self) -> sc.Variable: """Get the energy.""" @@ -88,14 +139,18 @@ def energy(self, energy: np.ndarray) -> None: scipp Variable. """ - if isinstance(energy, Numerical): + if isinstance(energy, Numeric): energy = np.array([float(energy)]) if not isinstance(energy, (np.ndarray, sc.Variable)): - raise TypeError('Energy must be a Number, a numpy ndarray or a scipp Variable.') + raise TypeError( + "Energy must be a Number, a numpy ndarray or a scipp Variable." + ) if isinstance(energy, np.ndarray): - self._energy = sc.array(dims=['energy'], values=energy, unit=self._energy.unit) + self._energy = sc.array( + dims=["energy"], values=energy, unit=self._energy.unit + ) if isinstance(energy, sc.Variable): self._energy = energy @@ -110,8 +165,8 @@ def energy_unit(self) -> str: def energy_unit(self, unit_str: str) -> None: raise AttributeError( ( - f'Unit is read-only. Use convert_unit to change the unit between allowed types ' - f'or create a new {self.__class__.__name__} with the desired unit.' + f"Unit is read-only. Use convert_unit to change the unit between allowed types " + f"or create a new {self.__class__.__name__} with the desired unit." ) ) # noqa: E501 @@ -125,7 +180,7 @@ def convert_energy_unit(self, energy_unit: str | sc.Unit) -> None: TypeError: If energy_unit is not a string or scipp unit. """ if not isinstance(energy_unit, (str, sc.Unit)): - raise TypeError('Energy unit must be a string or scipp unit.') + raise TypeError("Energy unit must be a string or scipp unit.") self.energy = sc.to_unit(self.energy, energy_unit) self._energy_unit = energy_unit @@ -136,7 +191,9 @@ def sample_components(self) -> ComponentCollection | ModelComponent: return self._sample_components @sample_components.setter - def sample_components(self, sample_components: ComponentCollection | ModelComponent) -> None: + def sample_components( + self, sample_components: ComponentCollection | ModelComponent + ) -> None: """Set the sample model. Args: sample_components : ComponentCollection or ModelComponent @@ -148,7 +205,7 @@ def sample_components(self, sample_components: ComponentCollection | ModelCompon """ if not isinstance(sample_components, (ComponentCollection, ModelComponent)): raise TypeError( - f'`sample_components` is an instance of {type(sample_components).__name__}, but must be a ComponentCollection or ModelComponent.' # noqa: E501 + f"`sample_components` is an instance of {type(sample_components).__name__}, but must be a ComponentCollection or ModelComponent." # noqa: E501 ) self._sample_components = sample_components @@ -173,6 +230,6 @@ def resolution_components( """ if not isinstance(resolution_components, (ComponentCollection, ModelComponent)): raise TypeError( - f'`resolution_components` is an instance of {type(resolution_components).__name__}, but must be a ComponentCollection or ModelComponent.' # noqa: E501 + f"`resolution_components` is an instance of {type(resolution_components).__name__}, but must be a ComponentCollection or ModelComponent." # noqa: E501 ) self._resolution_components = resolution_components diff --git a/src/easydynamics/convolution/numerical_convolution.py b/src/easydynamics/convolution/numerical_convolution.py index 125c4451..95d75917 100644 --- a/src/easydynamics/convolution/numerical_convolution.py +++ b/src/easydynamics/convolution/numerical_convolution.py @@ -9,9 +9,10 @@ from easydynamics.convolution.numerical_convolution_base import NumericalConvolutionBase from easydynamics.sample_model.component_collection import ComponentCollection from easydynamics.sample_model.components.model_component import ModelComponent -from easydynamics.utils.detailed_balance import _detailed_balance_factor as detailed_balance_factor - -Numerical = float | int +from easydynamics.utils.detailed_balance import ( + _detailed_balance_factor as detailed_balance_factor, +) +from easydynamics.utils.utils import Numeric class NumericalConvolution(NumericalConvolutionBase): @@ -53,17 +54,19 @@ def __init__( energy: np.ndarray | sc.Variable, sample_components: ComponentCollection | ModelComponent, resolution_components: ComponentCollection | ModelComponent, - upsample_factor: Numerical = 5, - extension_factor: float = 0.2, - temperature: Parameter | float | None = None, - temperature_unit: str | sc.Unit = 'K', - energy_unit: str | sc.Unit = 'meV', + energy_offset: Numeric | Parameter = 0.0, + upsample_factor: Numeric = 5, + extension_factor: Numeric = 0.2, + temperature: Parameter | Numeric | None = None, + temperature_unit: str | sc.Unit = "K", + energy_unit: str | sc.Unit = "meV", normalize_detailed_balance: bool = True, ): super().__init__( energy=energy, sample_components=sample_components, resolution_components=resolution_components, + energy_offset=energy_offset, upsample_factor=upsample_factor, extension_factor=extension_factor, temperature=temperature, @@ -87,23 +90,25 @@ def convolution( # Give warnings if peaks are very wide or very narrow self._check_width_thresholds( model=self.sample_components, - model_name='sample model', + model_name="sample model", ) self._check_width_thresholds( model=self.resolution_components, - model_name='resolution model', + model_name="resolution model", ) # Evaluate sample model. If called via the Convolution class, # delta functions are already filtered out. sample_vals = self.sample_components.evaluate( - self._energy_grid.energy_dense - self._energy_grid.energy_even_length_offset + self._energy_grid.energy_dense + - self._energy_grid.energy_even_length_offset + - self.energy_offset.value ) # Detailed balance correction if self.temperature is not None: detailed_balance_factor_correction = detailed_balance_factor( - energy=self._energy_grid.energy_dense, + energy=self._energy_grid.energy_dense - self.energy_offset.value, temperature=self.temperature, energy_unit=self.energy.unit, divide_by_temperature=self.normalize_detailed_balance, @@ -116,7 +121,7 @@ def convolution( ) # Convolution - convolved = fftconvolve(sample_vals, resolution_vals, mode='same') + convolved = fftconvolve(sample_vals, resolution_vals, mode="same") convolved *= self._energy_grid.energy_dense_step # normalize if self.upsample_factor is not None: diff --git a/src/easydynamics/convolution/numerical_convolution_base.py b/src/easydynamics/convolution/numerical_convolution_base.py index dd3e68e3..ba40f456 100644 --- a/src/easydynamics/convolution/numerical_convolution_base.py +++ b/src/easydynamics/convolution/numerical_convolution_base.py @@ -63,6 +63,7 @@ def __init__( energy: np.ndarray | sc.Variable, sample_components: ComponentCollection | ModelComponent, resolution_components: ComponentCollection | ModelComponent, + energy_offset: Numerical | Parameter = 0.0, upsample_factor: Numerical = 5, extension_factor: float = 0.2, temperature: Parameter | float | None = None, @@ -75,6 +76,7 @@ def __init__( sample_components=sample_components, resolution_components=resolution_components, energy_unit=energy_unit, + energy_offset=energy_offset, ) if temperature is not None and not isinstance( From 6e37ec6a85feaed6ac426181f8476ab71a5e67e2 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Fri, 6 Feb 2026 15:10:40 +0100 Subject: [PATCH 09/17] Progress on Analysis --- docs/docs/tutorials/analysis.ipynb | 31 +- src/easydynamics/analysis/analysis1d old.py | 497 ++++++++++++++++++ src/easydynamics/analysis/analysis1d.py | 458 ++++++---------- src/easydynamics/analysis/analysis_base.py | 63 ++- .../sample_model/component_collection.py | 79 ++- .../sample_model/instrument_model.py | 78 ++- src/easydynamics/sample_model/model_base.py | 58 +- 7 files changed, 870 insertions(+), 394 deletions(-) create mode 100644 src/easydynamics/analysis/analysis1d old.py diff --git a/docs/docs/tutorials/analysis.ipynb b/docs/docs/tutorials/analysis.ipynb index 7b843acc..83257cda 100644 --- a/docs/docs/tutorials/analysis.ipynb +++ b/docs/docs/tutorials/analysis.ipynb @@ -26,13 +26,13 @@ "from easydynamics.sample_model.background_model import BackgroundModel\n", "from easydynamics.sample_model.resolution_model import ResolutionModel\n", "from easydynamics.sample_model.sample_model import SampleModel\n", - "\n", + "from easydynamics.sample_model.instrument_model import InstrumentModel\n", "%matplotlib widget" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "8deca9b6", "metadata": {}, "outputs": [], @@ -46,7 +46,27 @@ "execution_count": null, "id": "41f842f0", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "C:\\Users\\henrikjacobsen3\\Documents\\easyScience\\dynamics-lib\\src\\easydynamics\\sample_model\\model_base.py:253: UserWarning: Q is not set. No component collections generated\n", + " warnings.warn('Q is not set. No component collections generated', UserWarning)\n" + ] + }, + { + "ename": "TypeError", + "evalue": "Analysis1d.__init__() got an unexpected keyword argument 'resolution_model'", + "output_type": "error", + "traceback": [ + "\u001b[31m---------------------------------------------------------------------------\u001b[39m", + "\u001b[31mTypeError\u001b[39m Traceback (most recent call last)", + "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[3]\u001b[39m\u001b[32m, line 25\u001b[39m\n\u001b[32m 20\u001b[39m resolution_model = ResolutionModel(components=res_gauss)\n\u001b[32m 23\u001b[39m background_model = BackgroundModel(components=Polynomial(coefficients=[\u001b[32m0.001\u001b[39m]))\n\u001b[32m---> \u001b[39m\u001b[32m25\u001b[39m my_analysis = \u001b[43mAnalysis1d\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 26\u001b[39m \u001b[43m \u001b[49m\u001b[43mexperiment\u001b[49m\u001b[43m=\u001b[49m\u001b[43mvanadium_experiment\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 27\u001b[39m \u001b[43m \u001b[49m\u001b[43msample_model\u001b[49m\u001b[43m=\u001b[49m\u001b[43msample_model\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 28\u001b[39m \u001b[43m \u001b[49m\u001b[43mresolution_model\u001b[49m\u001b[43m=\u001b[49m\u001b[43mresolution_model\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 29\u001b[39m \u001b[43m \u001b[49m\u001b[43mbackground_model\u001b[49m\u001b[43m=\u001b[49m\u001b[43mbackground_model\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 30\u001b[39m \u001b[43m \u001b[49m\u001b[43mQ_index\u001b[49m\u001b[43m=\u001b[49m\u001b[32;43m5\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[32m 31\u001b[39m \u001b[43m)\u001b[49m\n\u001b[32m 33\u001b[39m my_analysis._update_models()\n\u001b[32m 36\u001b[39m values = my_analysis.calculate()\n", + "\u001b[31mTypeError\u001b[39m: Analysis1d.__init__() got an unexpected keyword argument 'resolution_model'" + ] + } + ], "source": [ "# Create a diffusion_model and components for the SampleModel\n", "\n", @@ -72,6 +92,11 @@ "\n", "background_model = BackgroundModel(components=Polynomial(coefficients=[0.001]))\n", "\n", + "instrument_model = InstrumentModel(\n", + " resolution_model=resolution_model,\n", + " background_model=background_model,\n", + ")\n", + "\n", "my_analysis = Analysis1d(\n", " experiment=vanadium_experiment,\n", " sample_model=sample_model,\n", diff --git a/src/easydynamics/analysis/analysis1d old.py b/src/easydynamics/analysis/analysis1d old.py new file mode 100644 index 00000000..b27fed1e --- /dev/null +++ b/src/easydynamics/analysis/analysis1d old.py @@ -0,0 +1,497 @@ +# SPDX-FileCopyrightText: 2025-2026 EasyDynamics contributors +# SPDX-License-Identifier: BSD-3-Clause + + +import numpy as np +import scipp as sc +from easyscience.base_classes.model_base import ModelBase as EasyScienceModelBase +from easyscience.fitting.fitter import Fitter as EasyScienceFitter +from easyscience.variable import DescriptorNumber +from easyscience.variable import Parameter + +from easydynamics.convolution import Convolution +from easydynamics.experiment import Experiment +from easydynamics.sample_model import InstrumentModel +from easydynamics.sample_model import ResolutionModel +from easydynamics.sample_model import SampleModel + + +class Analysis1d(EasyScienceModelBase): + """For analysing data.""" + + def __init__( + self, + display_name: str = "MyAnalysis", + unique_name: str | None = None, + experiment: Experiment | None = None, + sample_model: SampleModel | None = None, + instrument_model: InstrumentModel | None = None, + Q_index: int | None = None, + ): + super().__init__(display_name=display_name, unique_name=unique_name) + + if experiment is not None and not isinstance(experiment, Experiment): + raise TypeError("experiment must be an instance of Experiment or None.") + + self._experiment = experiment + + if sample_model is not None and not isinstance(sample_model, SampleModel): + raise TypeError("sample_model must be an instance of SampleModel or None.") + sample_model.Q = self.Q + self._sample_model = sample_model + + if instrument_model is not None and not isinstance( + instrument_model, InstrumentModel + ): + raise TypeError( + "instrument_model must be an instance of InstrumentModel or None." + ) + if instrument_model is None: + self._instrument_model = InstrumentModel() + else: + self._instrument_model = instrument_model + + self._convolvers = [None] * (len(self.Q) if self.Q is not None else 0) + self._update_models() + + if Q_index is not None: + if ( + not isinstance(Q_index, int) + or Q_index < 0 + or (self.Q is not None and Q_index >= len(self.Q)) + ): + raise ValueError("Q_index must be a valid index for the Q values.") + self._Q_index = Q_index + + ############# + # Properties + ############# + + @property + def experiment(self) -> Experiment | None: + """The Experiment associated with this Analysis.""" + return self._experiment + + @experiment.setter + def experiment(self, value: Experiment | None) -> None: + if value is not None and not isinstance(value, Experiment): + raise TypeError("experiment must be an instance of Experiment or None.") + self._experiment = value + self._update_models() + + @property + def sample_model(self) -> SampleModel | None: + """The SampleModel associated with this Analysis.""" + return self._sample_model + + @sample_model.setter + def sample_model(self, value: SampleModel | None) -> None: + if value is not None and not isinstance(value, SampleModel): + raise TypeError("sample_model must be an instance of SampleModel or None.") + self._sample_model = value + self._update_models() + + @property + def resolution_model(self) -> ResolutionModel | None: + """The ResolutionModel associated with this Analysis.""" + return self._resolution_model + + @resolution_model.setter + def resolution_model(self, value: ResolutionModel | None) -> None: + if value is not None and not isinstance(value, ResolutionModel): + raise TypeError( + "resolution_model must be an instance of ResolutionModel or None." + ) + self._resolution_model = value + self._update_models() + + @property + def Q(self) -> sc.Variable | None: + """The Q values from the associated Experiment, if available.""" + if self.experiment is not None: + return self.experiment.Q + return None + + @Q.setter + def Q(self, value) -> None: + """Q is a read-only property derived from the Experiment.""" + raise AttributeError("Q is a read-only property derived from the Experiment.") + + @property + def energy(self) -> sc.Variable | None: + """The energy values from the associated Experiment, if + available. + """ + if self.experiment is not None: + return self.experiment.energy + return None + + @energy.setter + def energy(self, value) -> None: + """Energy is a read-only property derived from the + Experiment. + """ + raise AttributeError( + "energy is a read-only property derived from the Experiment." + ) + + @property + def temperature(self) -> Parameter | None: + """The temperature from the associated Experiment, if + available. + """ + return self.sample_model.temperature if self.sample_model is not None else None + + @temperature.setter + def temperature(self, value) -> None: + """Temperature is a read-only property derived from the + Experiment. + """ + raise AttributeError( + "temperature is a read-only property derived from the sample model." + ) + + @property + def energy_offset(self) -> list[Parameter] | None: + """Get the energy offsets for each Q value.""" + return self._energy_offset + + @energy_offset.setter + def energy_offset(self, offsets: list[Parameter] | None) -> None: + """Set the energy offsets for each Q value. + + Args: + offsets (list[Parameter] | None): The list of energy + offsets. + Raises: + TypeError: If offsets is not a list of Parameters or + None. + """ + if offsets is not None: + if len(offsets) != len(self.Q): + raise ValueError( + "energy_offset list length must match number of Q values." + ) + for offset in offsets: + if not isinstance(offset, Parameter): + raise TypeError( + "Each energy_offset must be an instance of Parameter." + ) + self._energy_offset = offsets + + @property + def Q_index(self) -> int | None: + """Get the Q index for single Q analysis.""" + return self._Q_index + + @Q_index.setter + def Q_index(self, index: int | None) -> None: + """Set the Q index for single Q analysis. + + Args: + index (int | None): The Q index. + """ + if index is not None: + if ( + not isinstance(index, int) + or index < 0 + or (self.Q is not None and index >= len(self.Q)) + ): + raise ValueError("Q_index must be a valid index for the Q values.") + self._Q_index = index + + ############# + # Other methods + ############# + + def calculate(self, energy: float | None = None) -> np.ndarray: + """Calculate the model prediction for a given Q index. + + Args: + energy (float): The energy value to calculate the model for. + Returns: + sc.DataArray: The calculated model prediction. + """ + Q_index = self.Q_index + if Q_index is None: + raise ValueError("Q_index must be set to calculate the model.") + + if energy is None: + energy = self.energy.values + + # TODO: handle units properly + energy = energy - self.energy_offset[Q_index].value + if self.sample_model is None: + sample_intensity = np.zeros_like(energy) + else: + if self.resolution_model is None: + sample_intensity = self.sample_model._component_collections[ + Q_index + ].evaluate(energy) + else: + convolver = self._convolvers[Q_index] + sample_intensity = convolver.convolution() + + if self.background_model is None: + background_intensity = np.zeros_like(energy) + else: + background_intensity = self.background_model._component_collections[ + Q_index + ].evaluate(energy) + + sample_plus_background = sample_intensity + background_intensity + + return sample_plus_background + + def calculate_individual_components( + self, + ) -> tuple[list[np.ndarray], list[np.ndarray]]: + """Calculate the model prediction for a given Q index for each + individual component. + + Args: + Q_index (int): The index of the Q value to calculate the + model for. + Returns: + list[np.ndarray]: The calculated model predictions for each + individual component. + """ + sample_results = [] + background_results = [] + Q_index = self.Q_index + if Q_index is None: + raise ValueError("Q_index must be set to calculate the model.") + + if self.sample_model is not None: + # Calculate sample components + for component in self.sample_model._component_collections[ + Q_index + ]._components: + if self.resolution_model is None: + component_intensity = component.evaluate(self.energy) + else: + convolver = Convolution( + sample_components=component, + resolution_components=self.resolution_model._component_collections[ + Q_index + ], + energy=self.energy, + temperature=self.temperature, + ) + component_intensity = convolver.convolution() + sample_results.append(component_intensity) + + if self.background_model is not None: + # Calculate background components + for component in self.background_model._component_collections[ + Q_index + ]._components: + component_intensity = component.evaluate(self.energy) + background_results.append(component_intensity) + + return sample_results, background_results + + def fit(self): + """Fit the model to the experimental data for a given Q index. + + Args: + Returns: + FitResult: The result of the fit. + """ + if self._experiment is None: + raise ValueError("No experiment is associated with this Analysis.") + + Q_index = self.Q_index + if Q_index is None: + raise ValueError("Q_index must be set to perform the fit.") + + data = self.experiment.data["Q", Q_index] + x = data.coords["energy"].values + y = data.values + e = data.variances**0.5 + + def fit_func(x_vals): + return self.calculate(energy=x_vals) + + fitter = EasyScienceFitter( + fit_object=self, + fit_function=fit_func, + ) + + # Perform the fit + fit_result = fitter.fit(x=x, y=y, weights=1.0 / e) + + # Store result + self.fit_result = fit_result + + return fit_result + + def plot_data_and_model( + self, + plot_individual_components: bool = True, + ) -> None: + """Plot the experimental data and the model prediction. + + Args: + plot_individual_components (bool): Whether to plot + individual components. Default is True. + """ + if not isinstance(plot_individual_components, bool): + raise TypeError("plot_individual_components must be True or False.") + + import matplotlib.pyplot as plt + + Q_index = self.Q_index + if Q_index is None: + raise ValueError("Q_index must be set to plot the data and model.") + if self.experiment is None or self.experiment.data is None: + raise ValueError("Experiment data is not available for plotting.") + data = self.experiment.data["Q", Q_index] + energy = data.coords["energy"].values + model = self.calculate(energy=energy) + plt.figure() + plt.errorbar( + energy, + data.values, + yerr=data.variances**0.5, + fmt="o", + label="Data", + color="black", + ) + plt.plot(energy, model, label="Model", color="red") + if plot_individual_components: + sample_comps, background_comps = self.calculate_individual_components() + for i, comp in enumerate(sample_comps): + plt.plot( + energy, + comp, + label=f"Sample Component {i + 1}", + linestyle="--", + ) + for i, comp in enumerate(background_comps): + plt.plot( + energy, + comp, + label=f"Background Component {i + 1}", + linestyle=":", + ) + plt.xlabel(f"Energy ({self.energy.unit})") + plt.ylabel(f"Intensity ({self.sample_model.unit})") + plt.title(f"Data and Model at Q index {Q_index}") + plt.legend() + plt.show() + # model_data_array = self._create_model_data_group( + # individual_components=plot_individual_components ) if + # self.experiment is None or self.experiment.data is None: raise + # ValueError("Experiment data is not available for plotting.") + + # from IPython.display import display + + # fig = pp.slicer( + # {"Data": self.experiment.data, "Model": model_data_array}, + # color={"Data": "black", "Model": "red"}, + # linestyle={"Data": "none", "Model": "solid"}, + # marker={"Data": "o", "Model": "None"}, + # ) + # display(fig) + + def get_all_variables(self) -> list[DescriptorNumber]: + """Get all variables used in the analysis. + + Returns: + List[Descriptor]: A list of all variables. + """ + variables = [] + if self.sample_model is not None: + variables.extend( + self.sample_model._component_collections[ + self.Q_index + ].get_all_variables() + ) + if self.resolution_model is not None: + variables.extend( + self.resolution_model._component_collections[ + self.Q_index + ].get_all_variables() + ) + if self.background_model is not None: + variables.extend( + self.background_model._component_collections[ + self.Q_index + ].get_all_variables() + ) + variables.append(self.energy_offset[self.Q_index]) + # TODO temperature and diffusion + return variables + + ############# + # Private methods + ############# + + def _update_models(self): + """Update models based on the current experiment.""" + if self.experiment is None: + return + + for Q_index in range(len(self.Q)): + self._convolvers[Q_index] = self._create_convolver(Q_index) + + def _create_convolver(self, Q_index: int): + """Initialize and return a Convolution object for the given Q + index. + """ + if self.sample_model is None or self.resolution_model is None: + raise ValueError("Both sample_model and resolution_model must be defined.") + + sample_components = self.sample_model._component_collections[Q_index] + resolution_components = self.resolution_model._component_collections[Q_index] + energy = self.energy + convolver = Convolution( + sample_components=sample_components, + resolution_components=resolution_components, + energy=energy, + temperature=self.temperature, + ) + return convolver + + def _create_model_data_group(self, individual_components=True) -> sc.DataArray: + """Create a Scipp DataArray representing the model over all Q + and energy values. + """ + if self.Q is None or self.energy is None: + raise ValueError("Q and energy must be defined in the experiment.") + + model_data = [] + for Q_index in range(len(self.Q)): + model_at_Q = self.calculate(Q_index) + model_data.append(model_at_Q) + + model_data_array = sc.DataArray( + data=sc.array(dims=["Q", "energy"], values=model_data), + coords={ + "Q": self.Q, + "energy": self.energy, + }, + ) + model_group = sc.DataGroup({"Model": model_data_array}) + + if individual_components: + components = self.calculate_individual_components_all_Q() + for Q_index, (sample_comps, background_comps) in enumerate(components): + for samp_index, samp_comp in enumerate(sample_comps): + model_data_array[samp_comp.display_name] = sc.zeros_like( + model_data_array.data + ) + model_data_array[samp_comp.display_name].data[ + Q_index, : + ] = samp_comp + for back_index, back_comp in enumerate(background_comps): + model_data_array[back_comp.display_name] = sc.zeros_like( + model_data_array.data + ) + model_data_array[back_comp.display_name].data[ + Q_index, : + ] = back_comp + + model_data_array = model_data_array + model_group # WRONG BUT LINT + return model_data_array diff --git a/src/easydynamics/analysis/analysis1d.py b/src/easydynamics/analysis/analysis1d.py index b27fed1e..2a0b554c 100644 --- a/src/easydynamics/analysis/analysis1d.py +++ b/src/easydynamics/analysis/analysis1d.py @@ -3,20 +3,17 @@ import numpy as np -import scipp as sc -from easyscience.base_classes.model_base import ModelBase as EasyScienceModelBase from easyscience.fitting.fitter import Fitter as EasyScienceFitter from easyscience.variable import DescriptorNumber -from easyscience.variable import Parameter +from easydynamics.analysis.analysis_base import AnalysisBase from easydynamics.convolution import Convolution from easydynamics.experiment import Experiment from easydynamics.sample_model import InstrumentModel -from easydynamics.sample_model import ResolutionModel from easydynamics.sample_model import SampleModel -class Analysis1d(EasyScienceModelBase): +class Analysis1d(AnalysisBase): """For analysing data.""" def __init__( @@ -28,31 +25,13 @@ def __init__( instrument_model: InstrumentModel | None = None, Q_index: int | None = None, ): - super().__init__(display_name=display_name, unique_name=unique_name) - - if experiment is not None and not isinstance(experiment, Experiment): - raise TypeError("experiment must be an instance of Experiment or None.") - - self._experiment = experiment - - if sample_model is not None and not isinstance(sample_model, SampleModel): - raise TypeError("sample_model must be an instance of SampleModel or None.") - sample_model.Q = self.Q - self._sample_model = sample_model - - if instrument_model is not None and not isinstance( - instrument_model, InstrumentModel - ): - raise TypeError( - "instrument_model must be an instance of InstrumentModel or None." - ) - if instrument_model is None: - self._instrument_model = InstrumentModel() - else: - self._instrument_model = instrument_model - - self._convolvers = [None] * (len(self.Q) if self.Q is not None else 0) - self._update_models() + super().__init__( + display_name=display_name, + unique_name=unique_name, + experiment=experiment, + sample_model=sample_model, + instrument_model=instrument_model, + ) if Q_index is not None: if ( @@ -63,122 +42,12 @@ def __init__( raise ValueError("Q_index must be a valid index for the Q values.") self._Q_index = Q_index + self._fit_result = None + ############# # Properties ############# - @property - def experiment(self) -> Experiment | None: - """The Experiment associated with this Analysis.""" - return self._experiment - - @experiment.setter - def experiment(self, value: Experiment | None) -> None: - if value is not None and not isinstance(value, Experiment): - raise TypeError("experiment must be an instance of Experiment or None.") - self._experiment = value - self._update_models() - - @property - def sample_model(self) -> SampleModel | None: - """The SampleModel associated with this Analysis.""" - return self._sample_model - - @sample_model.setter - def sample_model(self, value: SampleModel | None) -> None: - if value is not None and not isinstance(value, SampleModel): - raise TypeError("sample_model must be an instance of SampleModel or None.") - self._sample_model = value - self._update_models() - - @property - def resolution_model(self) -> ResolutionModel | None: - """The ResolutionModel associated with this Analysis.""" - return self._resolution_model - - @resolution_model.setter - def resolution_model(self, value: ResolutionModel | None) -> None: - if value is not None and not isinstance(value, ResolutionModel): - raise TypeError( - "resolution_model must be an instance of ResolutionModel or None." - ) - self._resolution_model = value - self._update_models() - - @property - def Q(self) -> sc.Variable | None: - """The Q values from the associated Experiment, if available.""" - if self.experiment is not None: - return self.experiment.Q - return None - - @Q.setter - def Q(self, value) -> None: - """Q is a read-only property derived from the Experiment.""" - raise AttributeError("Q is a read-only property derived from the Experiment.") - - @property - def energy(self) -> sc.Variable | None: - """The energy values from the associated Experiment, if - available. - """ - if self.experiment is not None: - return self.experiment.energy - return None - - @energy.setter - def energy(self, value) -> None: - """Energy is a read-only property derived from the - Experiment. - """ - raise AttributeError( - "energy is a read-only property derived from the Experiment." - ) - - @property - def temperature(self) -> Parameter | None: - """The temperature from the associated Experiment, if - available. - """ - return self.sample_model.temperature if self.sample_model is not None else None - - @temperature.setter - def temperature(self, value) -> None: - """Temperature is a read-only property derived from the - Experiment. - """ - raise AttributeError( - "temperature is a read-only property derived from the sample model." - ) - - @property - def energy_offset(self) -> list[Parameter] | None: - """Get the energy offsets for each Q value.""" - return self._energy_offset - - @energy_offset.setter - def energy_offset(self, offsets: list[Parameter] | None) -> None: - """Set the energy offsets for each Q value. - - Args: - offsets (list[Parameter] | None): The list of energy - offsets. - Raises: - TypeError: If offsets is not a list of Parameters or - None. - """ - if offsets is not None: - if len(offsets) != len(self.Q): - raise ValueError( - "energy_offset list length must match number of Q values." - ) - for offset in offsets: - if not isinstance(offset, Parameter): - raise TypeError( - "Each energy_offset must be an instance of Parameter." - ) - self._energy_offset = offsets - @property def Q_index(self) -> int | None: """Get the Q index for single Q analysis.""" @@ -212,32 +81,37 @@ def calculate(self, energy: float | None = None) -> np.ndarray: Returns: sc.DataArray: The calculated model prediction. """ - Q_index = self.Q_index - if Q_index is None: - raise ValueError("Q_index must be set to calculate the model.") + Q_index = self._require_Q_index() if energy is None: energy = self.energy.values # TODO: handle units properly - energy = energy - self.energy_offset[Q_index].value - if self.sample_model is None: - sample_intensity = np.zeros_like(energy) - else: - if self.resolution_model is None: - sample_intensity = self.sample_model._component_collections[ - Q_index - ].evaluate(energy) - else: - convolver = self._convolvers[Q_index] - sample_intensity = convolver.convolution() - - if self.background_model is None: - background_intensity = np.zeros_like(energy) - else: - background_intensity = self.background_model._component_collections[ - Q_index - ].evaluate(energy) + + energy_offset = self.instrument_model.get_energy_offset_at_Q(Q_index).value + + # Sample + sample_components = self.sample_model.get_component_collection(Q_index) + resolution_components = ( + self.instrument_model.resolution_model.get_component_collection(Q_index) + ) + + sample_intensity = self._evaluate_sample( + sample_components=sample_components, + resolution_components=resolution_components, + energy=energy, + energy_offset=energy_offset, + ) + + # Background + background_component_collection = ( + self.instrument_model.background_model.get_component_collection(Q_index) + ) + background_intensity = self._evaluate_background( + background_components=background_component_collection, + energy=energy, + energy_offset=energy_offset, + ) sample_plus_background = sample_intensity + background_intensity @@ -245,51 +119,65 @@ def calculate(self, energy: float | None = None) -> np.ndarray: def calculate_individual_components( self, - ) -> tuple[list[np.ndarray], list[np.ndarray]]: - """Calculate the model prediction for a given Q index for each - individual component. + energy: float | None = None, + ) -> np.ndarray: + """Calculate the model prediction for a given Q index. Args: - Q_index (int): The index of the Q value to calculate the - model for. + energy (float): The energy value to calculate the model for. Returns: - list[np.ndarray]: The calculated model predictions for each - individual component. + sc.DataArray: The calculated model prediction. """ - sample_results = [] - background_results = [] - Q_index = self.Q_index - if Q_index is None: - raise ValueError("Q_index must be set to calculate the model.") - - if self.sample_model is not None: - # Calculate sample components - for component in self.sample_model._component_collections[ - Q_index - ]._components: - if self.resolution_model is None: - component_intensity = component.evaluate(self.energy) - else: - convolver = Convolution( - sample_components=component, - resolution_components=self.resolution_model._component_collections[ - Q_index - ], - energy=self.energy, - temperature=self.temperature, - ) - component_intensity = convolver.convolution() - sample_results.append(component_intensity) - - if self.background_model is not None: - # Calculate background components - for component in self.background_model._component_collections[ - Q_index - ]._components: - component_intensity = component.evaluate(self.energy) - background_results.append(component_intensity) - - return sample_results, background_results + Q_index = self._require_Q_index() + + if energy is None: + energy = self.energy.values + + # TODO: handle units properly + + energy_offset = self.instrument_model.get_energy_offset_at_Q(Q_index).value + + # Sample. Convolve with resolution if resolution components are + # present, otherwise just evaluate sample components one by one + # to get individual contributions. + sample_components = self.sample_model.get_component_collection(Q_index) + + resolution_components = ( + self.instrument_model.resolution_model.get_component_collection(Q_index) + ) + + if sample_components.is_empty: + sample_intensity = [np.zeros_like(energy)] + else: + sample_intensity = [] + for component in sample_components.components: + component_intensity = self._evaluate_sample_component( + component=component, + resolution_components=resolution_components, + energy=energy, + energy_offset=energy_offset, + ) + sample_intensity.append(component_intensity) + + # Background. Evaluate each background component separately to + # get individual contributions. + background_components = ( + self.instrument_model.background_model.get_component_collection(Q_index) + ) + + if background_components.is_empty: + background_intensity = [np.zeros_like(energy)] + else: + background_intensity = [] + for component in background_components.components: + component_intensity = self._evaluate_background_component( + component=component, + energy=energy, + energy_offset=energy_offset, + ) + background_intensity.append(component_intensity) + + return sample_intensity, background_intensity def fit(self): """Fit the model to the experimental data for a given Q index. @@ -301,9 +189,7 @@ def fit(self): if self._experiment is None: raise ValueError("No experiment is associated with this Analysis.") - Q_index = self.Q_index - if Q_index is None: - raise ValueError("Q_index must be set to perform the fit.") + Q_index = self._require_Q_index() data = self.experiment.data["Q", Q_index] x = data.coords["energy"].values @@ -322,13 +208,14 @@ def fit_func(x_vals): fit_result = fitter.fit(x=x, y=y, weights=1.0 / e) # Store result - self.fit_result = fit_result + self._fit_result = fit_result return fit_result def plot_data_and_model( self, plot_individual_components: bool = True, + add_background: bool = True, ) -> None: """Plot the experimental data and the model prediction. @@ -341,9 +228,7 @@ def plot_data_and_model( import matplotlib.pyplot as plt - Q_index = self.Q_index - if Q_index is None: - raise ValueError("Q_index must be set to plot the data and model.") + Q_index = self._require_Q_index() if self.experiment is None or self.experiment.data is None: raise ValueError("Experiment data is not available for plotting.") data = self.experiment.data["Q", Q_index] @@ -361,6 +246,9 @@ def plot_data_and_model( plt.plot(energy, model, label="Model", color="red") if plot_individual_components: sample_comps, background_comps = self.calculate_individual_components() + if add_background: + background = sum(background_comps) + sample_comps = [comp + background for comp in sample_comps] for i, comp in enumerate(sample_comps): plt.plot( energy, @@ -380,20 +268,6 @@ def plot_data_and_model( plt.title(f"Data and Model at Q index {Q_index}") plt.legend() plt.show() - # model_data_array = self._create_model_data_group( - # individual_components=plot_individual_components ) if - # self.experiment is None or self.experiment.data is None: raise - # ValueError("Experiment data is not available for plotting.") - - # from IPython.display import display - - # fig = pp.slicer( - # {"Data": self.experiment.data, "Model": model_data_array}, - # color={"Data": "black", "Model": "red"}, - # linestyle={"Data": "none", "Model": "solid"}, - # marker={"Data": "o", "Model": "None"}, - # ) - # display(fig) def get_all_variables(self) -> list[DescriptorNumber]: """Get all variables used in the analysis. @@ -401,97 +275,67 @@ def get_all_variables(self) -> list[DescriptorNumber]: Returns: List[Descriptor]: A list of all variables. """ - variables = [] - if self.sample_model is not None: - variables.extend( - self.sample_model._component_collections[ - self.Q_index - ].get_all_variables() - ) - if self.resolution_model is not None: - variables.extend( - self.resolution_model._component_collections[ - self.Q_index - ].get_all_variables() - ) - if self.background_model is not None: - variables.extend( - self.background_model._component_collections[ - self.Q_index - ].get_all_variables() - ) - variables.append(self.energy_offset[self.Q_index]) - # TODO temperature and diffusion + variables = self.sample_model.get_all_variables(Q_index=self.Q_index) + + variables.extend(self.instrument_model.get_all_variables(Q_index=self.Q_index)) + + if self._extra_parameters != []: + variables.extend(self._extra_parameters) + return variables ############# # Private methods ############# + def _evaluate_sample( + self, + sample_components, + resolution_components, + energy, + energy_offset, + ): + if resolution_components.is_empty: + return sample_components.evaluate(energy - energy_offset) + convolver = self._convolvers[self._require_Q_index()] + return convolver.convolution() - def _update_models(self): - """Update models based on the current experiment.""" - if self.experiment is None: - return - - for Q_index in range(len(self.Q)): - self._convolvers[Q_index] = self._create_convolver(Q_index) - - def _create_convolver(self, Q_index: int): - """Initialize and return a Convolution object for the given Q - index. - """ - if self.sample_model is None or self.resolution_model is None: - raise ValueError("Both sample_model and resolution_model must be defined.") - - sample_components = self.sample_model._component_collections[Q_index] - resolution_components = self.resolution_model._component_collections[Q_index] - energy = self.energy + def _evaluate_sample_component( + self, + component, + resolution_components, + energy, + energy_offset, + ): + if resolution_components.is_empty: + return component.evaluate(energy - energy_offset) convolver = Convolution( - sample_components=sample_components, + sample_components=component, resolution_components=resolution_components, energy=energy, temperature=self.temperature, + energy_offset=energy_offset, ) - return convolver + return convolver.convolution() - def _create_model_data_group(self, individual_components=True) -> sc.DataArray: - """Create a Scipp DataArray representing the model over all Q - and energy values. - """ - if self.Q is None or self.energy is None: - raise ValueError("Q and energy must be defined in the experiment.") - - model_data = [] - for Q_index in range(len(self.Q)): - model_at_Q = self.calculate(Q_index) - model_data.append(model_at_Q) - - model_data_array = sc.DataArray( - data=sc.array(dims=["Q", "energy"], values=model_data), - coords={ - "Q": self.Q, - "energy": self.energy, - }, - ) - model_group = sc.DataGroup({"Model": model_data_array}) - - if individual_components: - components = self.calculate_individual_components_all_Q() - for Q_index, (sample_comps, background_comps) in enumerate(components): - for samp_index, samp_comp in enumerate(sample_comps): - model_data_array[samp_comp.display_name] = sc.zeros_like( - model_data_array.data - ) - model_data_array[samp_comp.display_name].data[ - Q_index, : - ] = samp_comp - for back_index, back_comp in enumerate(background_comps): - model_data_array[back_comp.display_name] = sc.zeros_like( - model_data_array.data - ) - model_data_array[back_comp.display_name].data[ - Q_index, : - ] = back_comp - - model_data_array = model_data_array + model_group # WRONG BUT LINT - return model_data_array + def _evaluate_background( + self, + background_components, + energy, + energy_offset, + ): + if background_components.is_empty: + return np.zeros_like(energy) + return background_components.evaluate(energy - energy_offset) + + def _evaluate_background_component( + self, + component, + energy, + energy_offset, + ): + return component.evaluate(energy - energy_offset) + + def _require_Q_index(self) -> int: + if self._Q_index is None: + raise ValueError("Q_index must be set.") + return self._Q_index diff --git a/src/easydynamics/analysis/analysis_base.py b/src/easydynamics/analysis/analysis_base.py index 5d965cae..81d0c250 100644 --- a/src/easydynamics/analysis/analysis_base.py +++ b/src/easydynamics/analysis/analysis_base.py @@ -12,7 +12,7 @@ from easydynamics.sample_model import SampleModel -class Analysis1Base(EasyScienceModelBase): +class AnalysisBase(EasyScienceModelBase): """For analysing data.""" def __init__( @@ -22,6 +22,7 @@ def __init__( experiment: Experiment | None = None, sample_model: SampleModel | None = None, instrument_model: InstrumentModel | None = None, + extra_parameters: Parameter | list[Parameter] | None = None, ): super().__init__(display_name=display_name, unique_name=unique_name) @@ -48,8 +49,22 @@ def __init__( "instrument_model must be an instance of InstrumentModel or None." ) + if extra_parameters is not None: + if isinstance(extra_parameters, Parameter): + self._extra_parameters = [extra_parameters] + elif isinstance(extra_parameters, list) and all( + isinstance(p, Parameter) for p in extra_parameters + ): + self._extra_parameters = extra_parameters + else: + raise TypeError( + "extra_parameters must be a Parameter or a list of Parameters." + ) + else: + self._extra_parameters = [] + self._convolvers = [None] * (len(self.Q) if self.Q is not None else 0) - self._update_models() + self._on_experiment_changed() ############# # Properties @@ -146,44 +161,52 @@ def temperature(self, value) -> None: # Private methods ############# - def _on_experiment_changed(self): - pass + def _on_experiment_changed(self) -> None: + self._sample_model.Q = self.Q + self._instrument_model.Q = self.Q + self._create_convolvers() - def _on_sample_model_changed(self): - pass + def _on_sample_model_changed(self) -> None: + self._sample_model.Q = self.Q + self._create_convolvers() - def _on_instrument_model_changed(self): - pass + def _on_instrument_model_changed(self) -> None: + self._instrument_model.Q = self.Q + self._create_convolvers() - # def _update_models(self): - # """Update models based on the current experiment.""" - # if self.experiment is None: - # return + def _create_convolvers(self) -> None: + """Create Convolution objects for each Q value.""" + num_Q = len(self.Q) if self.Q is not None else 0 + self._convolvers = [self._create_convolver(i) for i in range(num_Q)] - # for Q_index in range(len(self.Q)): - # self._convolvers[Q_index] = self._create_convolver(Q_index) - - def _create_convolver(self, Q_index: int): + def _create_convolver(self, Q_index: int) -> Convolution: """Initialize and return a Convolution object for the given Q index. """ sample_components = self.sample_model._component_collections[Q_index] if sample_components == []: - raise ValueError(f"Sample model has no components at Q index {Q_index}.") + return Convolution() resolution_components = ( self.instrument_model.resolution_model._component_collections[Q_index] ) if resolution_components == []: - raise ValueError( - f"Resolution model has no components at Q index {Q_index}." - ) + return Convolution() energy = self.energy + # TODO: allow convolution options to be set. convolver = Convolution( sample_components=sample_components, resolution_components=resolution_components, energy=energy, temperature=self.temperature, + energy_offset=self.instrument_model._energy_offsets[Q_index], ) return convolver + + ############# + # Dunder methods + ############# + + def __repr__(self) -> str: + return f"AnalysisBase(display_name={self.display_name}, unique_name={self.unique_name})" diff --git a/src/easydynamics/sample_model/component_collection.py b/src/easydynamics/sample_model/component_collection.py index 586a6649..a0b1e668 100644 --- a/src/easydynamics/sample_model/component_collection.py +++ b/src/easydynamics/sample_model/component_collection.py @@ -31,8 +31,8 @@ class ComponentCollection(ModelBase): def __init__( self, - unit: str | sc.Unit = 'meV', - display_name: str = 'MyComponentCollection', + unit: str | sc.Unit = "meV", + display_name: str = "MyComponentCollection", unique_name: str | None = None, components: List[ModelComponent] | None = None, ): @@ -54,7 +54,7 @@ def __init__( if unit is not None and not isinstance(unit, (str, sc.Unit)): raise TypeError( - f'unit must be None, a string, or a scipp Unit, got {type(unit).__name__}' + f"unit must be None, a string, or a scipp Unit, got {type(unit).__name__}" ) self._unit = unit self._components = [] @@ -62,31 +62,37 @@ def __init__( # Add initial components if provided. Used for serialization. if components is not None: if not isinstance(components, list): - raise TypeError('components must be a list of ModelComponent instances.') + raise TypeError( + "components must be a list of ModelComponent instances." + ) for comp in components: self.append_component(comp) - def append_component(self, component: ModelComponent | 'ComponentCollection') -> None: + def append_component( + self, component: ModelComponent | "ComponentCollection" + ) -> None: match component: case ModelComponent(): components = (component,) case ComponentCollection(components=components): pass case _: - raise TypeError('Component must be a ModelComponent or ComponentCollection.') + raise TypeError( + "Component must be a ModelComponent or ComponentCollection." + ) for comp in components: if comp in self._components: raise ValueError( f"Component '{comp.unique_name}' is already in the collection. " - f'Existing components: {self.list_component_names()}' + f"Existing components: {self.list_component_names()}" ) self._components.append(comp) def remove_component(self, unique_name: str) -> None: if not isinstance(unique_name, str): - raise TypeError('Component name must be a string.') + raise TypeError("Component name must be a string.") for comp in self._components: if comp.unique_name == unique_name: @@ -95,8 +101,8 @@ def remove_component(self, unique_name: str) -> None: raise KeyError( f"No component named '{unique_name}' exists. " - f'Did you accidentally use the display_name? ' - f'Here is a list of the components in the collection: {self.list_component_names()}' + f"Did you accidentally use the display_name? " + f"Here is a list of the components in the collection: {self.list_component_names()}" ) @property @@ -106,16 +112,27 @@ def components(self) -> list[ModelComponent]: @components.setter def components(self, components: List[ModelComponent]) -> None: if not isinstance(components, list): - raise TypeError('components must be a list of ModelComponent instances.') + raise TypeError("components must be a list of ModelComponent instances.") for comp in components: if not isinstance(comp, ModelComponent): raise TypeError( - 'All items in components must be instances of ModelComponent. ' - f'Got {type(comp).__name__} instead.' + "All items in components must be instances of ModelComponent. " + f"Got {type(comp).__name__} instead." ) self._components = components + @property + def is_empty(self) -> bool: + return not self._components + + @is_empty.setter + def is_empty(self, value: bool) -> None: + raise AttributeError( + "is_empty is a read-only property that indicates " + "whether the collection has components." + ) + def list_component_names(self) -> List[str]: """List the names of all components in the model. @@ -135,27 +152,27 @@ def normalize_area(self) -> None: # Useful for convolutions. """Normalize the areas of all components so they sum to 1.""" if not self.components: - raise ValueError('No components in the model to normalize.') + raise ValueError("No components in the model to normalize.") area_params = [] - total_area = Parameter(name='total_area', value=0.0, unit=self._unit) + total_area = Parameter(name="total_area", value=0.0, unit=self._unit) for component in self.components: - if hasattr(component, 'area'): + if hasattr(component, "area"): area_params.append(component.area) total_area += component.area else: warnings.warn( f"Component '{component.unique_name}' does not have an 'area' attribute " - f'and will be skipped in normalization.', + f"and will be skipped in normalization.", UserWarning, ) if total_area.value == 0: - raise ValueError('Total area is zero; cannot normalize.') + raise ValueError("Total area is zero; cannot normalize.") if not np.isfinite(total_area.value): - raise ValueError('Total area is not finite; cannot normalize.') + raise ValueError("Total area is not finite; cannot normalize.") for param in area_params: param.value /= total_area.value @@ -167,7 +184,11 @@ def get_all_variables(self) -> list[DescriptorBase]: List[Parameter]: List of parameters in the component. """ - return [var for component in self.components for var in component.get_all_variables()] + return [ + var + for component in self.components + for var in component.get_all_variables() + ] @property def unit(self) -> str | sc.Unit: @@ -183,8 +204,8 @@ def unit(self) -> str | sc.Unit: def unit(self, unit_str: str) -> None: raise AttributeError( ( - f'Unit is read-only. Use convert_unit to change the unit between allowed types ' - f'or create a new {self.__class__.__name__} with the desired unit.' + f"Unit is read-only. Use convert_unit to change the unit between allowed types " + f"or create a new {self.__class__.__name__} with the desired unit." ) ) # noqa: E501 @@ -208,7 +229,9 @@ def convert_unit(self, unit: str | sc.Unit) -> None: pass # Best effort rollback raise e - def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray: + def evaluate( + self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray + ) -> np.ndarray: """Evaluate the sum of all components. Parameters @@ -246,11 +269,13 @@ def evaluate_component( Evaluated values for the specified component. """ if not self.components: - raise ValueError('No components in the model to evaluate.') + raise ValueError("No components in the model to evaluate.") if not isinstance(unique_name, str): raise TypeError( - (f'Component unique name must be a string, got {type(unique_name)} instead.') + ( + f"Component unique name must be a string, got {type(unique_name)} instead." + ) ) matches = [comp for comp in self.components if comp.unique_name == unique_name] @@ -303,6 +328,8 @@ def __repr__(self) -> str: ------- str """ - comp_names = ', '.join(c.unique_name for c in self.components) or 'No components' + comp_names = ( + ", ".join(c.unique_name for c in self.components) or "No components" + ) return f"" diff --git a/src/easydynamics/sample_model/instrument_model.py b/src/easydynamics/sample_model/instrument_model.py index bef6bd92..4c767331 100644 --- a/src/easydynamics/sample_model/instrument_model.py +++ b/src/easydynamics/sample_model/instrument_model.py @@ -48,13 +48,13 @@ class InstrumentModel(NewBase): def __init__( self, - display_name: str = 'MyInstrumentModel', + display_name: str = "MyInstrumentModel", unique_name: str | None = None, Q: Q_type | None = None, resolution_model: ResolutionModel | None = None, background_model: BackgroundModel | None = None, energy_offset: Numeric | None = None, - unit: str | sc.Unit = 'meV', + unit: str | sc.Unit = "meV", ): super().__init__( display_name=display_name, @@ -68,8 +68,8 @@ def __init__( else: if not isinstance(resolution_model, ResolutionModel): raise TypeError( - f'resolution_model must be a ResolutionModel or None, ' - f'got {type(resolution_model).__name__}' + f"resolution_model must be a ResolutionModel or None, " + f"got {type(resolution_model).__name__}" ) self._resolution_model = resolution_model @@ -78,8 +78,8 @@ def __init__( else: if not isinstance(background_model, BackgroundModel): raise TypeError( - f'background_model must be a BackgroundModel or None, ' - f'got {type(background_model).__name__}' + f"background_model must be a BackgroundModel or None, " + f"got {type(background_model).__name__}" ) self._background_model = background_model @@ -87,10 +87,10 @@ def __init__( energy_offset = 0.0 if not isinstance(energy_offset, Numeric): - raise TypeError('energy_offset must be a number or None') + raise TypeError("energy_offset must be a number or None") self._energy_offset = Parameter( - name='energy_offset', + name="energy_offset", value=float(energy_offset), unit=self.unit, fixed=False, @@ -112,7 +112,7 @@ def resolution_model(self, value: ResolutionModel): """Set the resolution model of the instrument.""" if not isinstance(value, ResolutionModel): raise TypeError( - f'resolution_model must be a ResolutionModel, got {type(value).__name__}' + f"resolution_model must be a ResolutionModel, got {type(value).__name__}" ) self._resolution_model = value self._on_resolution_model_change() @@ -127,7 +127,7 @@ def background_model(self, value: BackgroundModel): """Set the background model of the instrument.""" if not isinstance(value, BackgroundModel): raise TypeError( - f'background_model must be a BackgroundModel, got {type(value).__name__}' + f"background_model must be a BackgroundModel, got {type(value).__name__}" ) self._background_model = value self._on_background_model_change() @@ -157,8 +157,8 @@ def unit(self) -> sc.Unit: def unit(self, unit_str: str) -> None: raise AttributeError( ( - f'Unit is read-only. Use convert_unit to change the unit between allowed types ' - f'or create a new {self.__class__.__name__} with the desired unit.' + f"Unit is read-only. Use convert_unit to change the unit between allowed types " + f"or create a new {self.__class__.__name__} with the desired unit." ) ) # noqa: E501 @@ -184,7 +184,9 @@ def energy_offset(self, value: Numeric): If value is not a number. """ if not isinstance(value, Numeric): - raise TypeError(f'energy_offset must be a number, got {type(value).__name__}') + raise TypeError( + f"energy_offset must be a number, got {type(value).__name__}" + ) self._energy_offset.value = value self._on_energy_offset_change() @@ -208,7 +210,7 @@ def convert_unit(self, unit_str: str | sc.Unit) -> None: """ unit = _validate_unit(unit_str) if unit is None: - raise ValueError('unit_str must be a valid unit string or scipp Unit') + raise ValueError("unit_str must be a valid unit string or scipp Unit") self._background_model.convert_unit(unit) self._resolution_model.convert_unit(unit) @@ -238,10 +240,12 @@ def get_all_variables(self, Q_index: int | None = None) -> list[Parameter]: variables = [self._energy_offsets[i] for i in range(len(self._Q))] else: if not isinstance(Q_index, int): - raise TypeError(f'Q_index must be an int or None, got {type(Q_index).__name__}') + raise TypeError( + f"Q_index must be an int or None, got {type(Q_index).__name__}" + ) if Q_index < 0 or Q_index >= len(self._Q): raise IndexError( - f'Q_index {Q_index} is out of bounds for Q of length {len(self._Q)}' + f"Q_index {Q_index} is out of bounds for Q of length {len(self._Q)}" ) variables = [self._energy_offsets[Q_index]] @@ -258,6 +262,34 @@ def free_resolution_parameters(self) -> None: """Free all parameters in the resolution model.""" self.resolution_model.free_all_parameters() + def get_energy_offset_at_Q(self, Q_index: int) -> Parameter: + """Get the energy offset Parameter at a specific Q index. + + Parameters + ---------- + Q_index : int + The index of the Q value to get the energy offset for. + + Returns + ------- + Parameter + The energy offset Parameter at the specified Q index. + + Raises + ------ + IndexError + If Q_index is out of bounds. + """ + if self._Q is None: + raise ValueError("No Q values are set in the InstrumentModel.") + + if Q_index < 0 or Q_index >= len(self._Q): + raise IndexError( + f"Q_index {Q_index} is out of bounds for Q of length {len(self._Q)}" + ) + + return self._energy_offsets[Q_index] + # -------------------------------------------------------------- # Private methods # -------------------------------------------------------------- @@ -295,11 +327,11 @@ def _on_background_model_change(self) -> None: def __repr__(self): return ( - f'{self.__class__.__name__}(' - f'unique_name={self.unique_name!r}, ' - f'unit={self.unit}, ' - f'Q_len={None if self._Q is None else len(self._Q)}, ' - f'resolution_model={self._resolution_model!r}, ' - f'background_model={self._background_model!r}' - f')' + f"{self.__class__.__name__}(" + f"unique_name={self.unique_name!r}, " + f"unit={self.unit}, " + f"Q_len={None if self._Q is None else len(self._Q)}, " + f"resolution_model={self._resolution_model!r}, " + f"background_model={self._background_model!r}" + f")" ) diff --git a/src/easydynamics/sample_model/model_base.py b/src/easydynamics/sample_model/model_base.py index b6b8bcdd..85b74d9f 100644 --- a/src/easydynamics/sample_model/model_base.py +++ b/src/easydynamics/sample_model/model_base.py @@ -43,9 +43,9 @@ class ModelBase(EasyScienceModelBase): def __init__( self, - display_name: str = 'MyModelBase', + display_name: str = "MyModelBase", unique_name: str | None = None, - unit: str | sc.Unit | None = 'meV', + unit: str | sc.Unit | None = "meV", components: ModelComponent | ComponentCollection | None = None, Q: Q_type | None = None, ): @@ -60,8 +60,8 @@ def __init__( components, (ModelComponent, ComponentCollection) ): raise TypeError( - f'Components must be a ModelComponent, a ComponentCollection or None, ' - f'got {type(components).__name__}' + f"Components must be a ModelComponent, a ComponentCollection or None, " + f"got {type(components).__name__}" ) self._components = ComponentCollection() @@ -88,8 +88,8 @@ def evaluate( if not self._component_collections: raise ValueError( - 'No components in the model to evaluate. ' - 'Run generate_component_collections() first' + "No components in the model to evaluate. " + "Run generate_component_collections() first" ) y = [collection.evaluate(x) for collection in self._component_collections] @@ -143,8 +143,8 @@ def unit(self) -> str | sc.Unit: def unit(self, unit_str: str) -> None: raise AttributeError( ( - f'Unit is read-only. Use convert_unit to change the unit between allowed types ' - f'or create a new {self.__class__.__name__} with the desired unit.' + f"Unit is read-only. Use convert_unit to change the unit between allowed types " + f"or create a new {self.__class__.__name__} with the desired unit." ) ) # noqa: E501 @@ -178,7 +178,9 @@ def components(self) -> list[ModelComponent]: def components(self, value: ModelComponent | ComponentCollection | None) -> None: """Set the components of the SampleModel.""" if not isinstance(value, (ModelComponent, ComponentCollection, type(None))): - raise TypeError('Components must be a ModelComponent or a ComponentCollection') + raise TypeError( + "Components must be a ModelComponent or a ComponentCollection" + ) self.clear_components() if value is not None: @@ -232,15 +234,39 @@ def get_all_variables(self, Q_index: int | None = None) -> list[Parameter]: ] else: if not isinstance(Q_index, int): - raise TypeError(f'Q_index must be an int or None, got {type(Q_index).__name__}') + raise TypeError( + f"Q_index must be an int or None, got {type(Q_index).__name__}" + ) if Q_index < 0 or Q_index >= len(self._component_collections): raise IndexError( - f'Q_index {Q_index} is out of bounds for component collections ' - f'of length {len(self._component_collections)}' + f"Q_index {Q_index} is out of bounds for component collections " + f"of length {len(self._component_collections)}" ) all_vars = self._component_collections[Q_index].get_all_variables() return all_vars + def get_component_collection(self, Q_index: int) -> ComponentCollection: + """Get the ComponentCollection at the given Q index. + + Parameters + ---------- + Q_index : int + The index of the desired ComponentCollection. + + Returns + ------- + ComponentCollection + The ComponentCollection at the specified Q index. + """ + if not isinstance(Q_index, int): + raise TypeError(f"Q_index must be an int, got {type(Q_index).__name__}") + if Q_index < 0 or Q_index >= len(self._component_collections): + raise IndexError( + f"Q_index {Q_index} is out of bounds for component collections " + f"of length {len(self._component_collections)}" + ) + return self._component_collections[Q_index] + # ------------------------------------------------------------------ # Private methods # ------------------------------------------------------------------ @@ -250,7 +276,9 @@ def _generate_component_collections(self) -> None: # TODO regenerate automatically if Q or components have changed if self._Q is None: - warnings.warn('Q is not set. No component collections generated', UserWarning) + warnings.warn( + "Q is not set. No component collections generated", UserWarning + ) self._component_collections = [] return @@ -276,6 +304,6 @@ def _on_components_change(self) -> None: def __repr__(self): return ( - f'{self.__class__.__name__}(unique_name={self.unique_name}, ' - f'unit={self.unit}), Q = {self.Q}, components = {self.components}' + f"{self.__class__.__name__}(unique_name={self.unique_name}, " + f"unit={self.unit}), Q = {self.Q}, components = {self.components}" ) From 8bc60f2a1a8236e18a13bb287bb879edc06baf5a Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Sun, 8 Feb 2026 06:35:38 +0100 Subject: [PATCH 10/17] multiple parameters with same unique_name????? --- docs/docs/tutorials/analysis.ipynb | 384 ++++++++++++--- src/easydynamics/analysis/analysis old.py | 497 ++++++++++++++++++++ src/easydynamics/analysis/analysis.py | 514 ++++----------------- src/easydynamics/analysis/analysis1d.py | 169 +++---- src/easydynamics/analysis/analysis_base.py | 210 ++++++++- 5 files changed, 1185 insertions(+), 589 deletions(-) create mode 100644 src/easydynamics/analysis/analysis old.py diff --git a/docs/docs/tutorials/analysis.ipynb b/docs/docs/tutorials/analysis.ipynb index 83257cda..e1ea2973 100644 --- a/docs/docs/tutorials/analysis.ipynb +++ b/docs/docs/tutorials/analysis.ipynb @@ -27,12 +27,13 @@ "from easydynamics.sample_model.resolution_model import ResolutionModel\n", "from easydynamics.sample_model.sample_model import SampleModel\n", "from easydynamics.sample_model.instrument_model import InstrumentModel\n", + "from easydynamics.analysis.analysis import Analysis\n", "%matplotlib widget" ] }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "8deca9b6", "metadata": {}, "outputs": [], @@ -46,27 +47,150 @@ "execution_count": null, "id": "41f842f0", "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "C:\\Users\\henrikjacobsen3\\Documents\\easyScience\\dynamics-lib\\src\\easydynamics\\sample_model\\model_base.py:253: UserWarning: Q is not set. No component collections generated\n", - " warnings.warn('Q is not set. No component collections generated', UserWarning)\n" - ] - }, - { - "ename": "TypeError", - "evalue": "Analysis1d.__init__() got an unexpected keyword argument 'resolution_model'", - "output_type": "error", - "traceback": [ - "\u001b[31m---------------------------------------------------------------------------\u001b[39m", - "\u001b[31mTypeError\u001b[39m Traceback (most recent call last)", - "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[3]\u001b[39m\u001b[32m, line 25\u001b[39m\n\u001b[32m 20\u001b[39m resolution_model = ResolutionModel(components=res_gauss)\n\u001b[32m 23\u001b[39m background_model = BackgroundModel(components=Polynomial(coefficients=[\u001b[32m0.001\u001b[39m]))\n\u001b[32m---> \u001b[39m\u001b[32m25\u001b[39m my_analysis = \u001b[43mAnalysis1d\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 26\u001b[39m \u001b[43m \u001b[49m\u001b[43mexperiment\u001b[49m\u001b[43m=\u001b[49m\u001b[43mvanadium_experiment\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 27\u001b[39m \u001b[43m \u001b[49m\u001b[43msample_model\u001b[49m\u001b[43m=\u001b[49m\u001b[43msample_model\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 28\u001b[39m \u001b[43m \u001b[49m\u001b[43mresolution_model\u001b[49m\u001b[43m=\u001b[49m\u001b[43mresolution_model\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 29\u001b[39m \u001b[43m \u001b[49m\u001b[43mbackground_model\u001b[49m\u001b[43m=\u001b[49m\u001b[43mbackground_model\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 30\u001b[39m \u001b[43m \u001b[49m\u001b[43mQ_index\u001b[49m\u001b[43m=\u001b[49m\u001b[32;43m5\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[32m 31\u001b[39m \u001b[43m)\u001b[49m\n\u001b[32m 33\u001b[39m my_analysis._update_models()\n\u001b[32m 36\u001b[39m values = my_analysis.calculate()\n", - "\u001b[31mTypeError\u001b[39m: Analysis1d.__init__() got an unexpected keyword argument 'resolution_model'" - ] - } - ], + "outputs": [], + "source": [ + "# # Create a diffusion_model and components for the SampleModel\n", + "\n", + "# # Creating components\n", + "# component_collection = ComponentCollection()\n", + "# delta_function = DeltaFunction(display_name='DeltaFunction', area=1)\n", + "# gaussian = Gaussian(display_name='Gaussian', width=0.1, center=0.5, area=0.5)\n", + "\n", + "# # Adding components to the component collection\n", + "# component_collection.append_component(delta_function)\n", + "\n", + "\n", + "# sample_model = SampleModel(\n", + "# components=component_collection,\n", + "# unit='meV',\n", + "# display_name='MySampleModel',\n", + "# )\n", + "\n", + "# res_gauss = Gaussian(width=0.1)\n", + "# res_gauss.area.fixed = True\n", + "# resolution_model = ResolutionModel(components=res_gauss)\n", + "\n", + "\n", + "# background_model = BackgroundModel(components=Polynomial(coefficients=[0.001]))\n", + "\n", + "# instrument_model = InstrumentModel(\n", + "# resolution_model=resolution_model,\n", + "# background_model=background_model,\n", + "# )\n", + "\n", + "# my_analysis = Analysis1d(\n", + "# experiment=vanadium_experiment,\n", + "# sample_model=sample_model,\n", + "# instrument_model=instrument_model,\n", + "# Q_index=5,\n", + "# )\n", + "\n", + "\n", + "# values = my_analysis.calculate()\n", + "# sample_values, background_values = my_analysis.calculate_individual_components()\n", + "\n", + "# plt.figure()\n", + "# plt.plot(my_analysis.energy.values, values, label='Total Model')\n", + "# for component_index in range(len(sample_values)):\n", + "# plt.plot(\n", + "# my_analysis.energy.values,\n", + "# sample_values[component_index],\n", + "# label=f'Sample Component {component_index}',\n", + "# linestyle='--',\n", + "# )\n", + "\n", + "# for component_index in range(len(background_values)):\n", + "# plt.plot(\n", + "# my_analysis.energy.values,\n", + "# background_values[component_index],\n", + "# label=f'Background Component {component_index}',\n", + "# linestyle=':',\n", + "# )\n", + "# plt.xlabel('Energy (meV)')\n", + "# plt.ylabel('Intensity')\n", + "# plt.title(f'Q index: {5}')\n", + "# plt.legend()\n", + "# plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6762faba", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "02702f95", + "metadata": {}, + "outputs": [], + "source": [ + "# my_analysis.plot_data_and_model()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "70091539", + "metadata": {}, + "outputs": [], + "source": [ + "# my_analysis.fit()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2ad6384e", + "metadata": {}, + "outputs": [], + "source": [ + "# my_analysis.plot_data_and_model()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2dfb1f90", + "metadata": {}, + "outputs": [], + "source": [ + "# my_analysis.get_all_variables()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5afefbab", + "metadata": {}, + "outputs": [], + "source": [ + "# my_analysis.get_fit_parameters()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "465c0e1e", + "metadata": {}, + "outputs": [], + "source": [ + "# for Q_index in range(len(my_analysis.Q)):\n", + "# my_analysis.Q_index = Q_index\n", + "# my_analysis.fit()\n", + "# my_analysis.plot_data_and_model()\n", + "# print(my_analysis.get_fit_parameters())\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9bdeed2b", + "metadata": {}, + "outputs": [], "source": [ "# Create a diffusion_model and components for the SampleModel\n", "\n", @@ -97,81 +221,217 @@ " background_model=background_model,\n", ")\n", "\n", - "my_analysis = Analysis1d(\n", + "my_full_analysis = Analysis(\n", " experiment=vanadium_experiment,\n", " sample_model=sample_model,\n", - " resolution_model=resolution_model,\n", - " background_model=background_model,\n", - " Q_index=5,\n", + " instrument_model=instrument_model,\n", ")\n", "\n", - "my_analysis._update_models()\n", - "\n", - "\n", - "values = my_analysis.calculate()\n", - "sample_values, background_values = my_analysis.calculate_individual_components()\n", - "\n", - "plt.figure()\n", - "plt.plot(my_analysis.energy.values, values, label='Total Model')\n", - "for component_index in range(len(sample_values)):\n", - " plt.plot(\n", - " my_analysis.energy.values,\n", - " sample_values[component_index],\n", - " label=f'Sample Component {component_index}',\n", - " linestyle='--',\n", - " )\n", - "\n", - "for component_index in range(len(background_values)):\n", - " plt.plot(\n", - " my_analysis.energy.values,\n", - " background_values[component_index],\n", - " label=f'Background Component {component_index}',\n", - " linestyle=':',\n", - " )\n", - "plt.xlabel('Energy (meV)')\n", - "plt.ylabel('Intensity')\n", - "plt.title(f'Q index: {5}')\n", - "plt.legend()\n", - "plt.show()" + "# my_full_analysis._fit_all_Q_independently()\n", + "my_full_analysis._fit_all_Q_simultaneously()\n", + "for analysis_object in my_full_analysis._analysis_list:\n", + " analysis_object.plot_data_and_model()\n", + " print(analysis_object.get_fit_parameters())\n" ] }, { "cell_type": "code", "execution_count": null, - "id": "6762faba", + "id": "0a727fc3", "metadata": {}, "outputs": [], - "source": [] + "source": [ + "for analysis_object in my_full_analysis._analysis_list:\n", + " print(analysis_object.get_fit_parameters())\n", + "\n", + "for analysis_object in my_full_analysis._analysis_list:\n", + " print(analysis_object.get_fit_parameters()[0].unique_name)\n", + "\n" + ] }, { "cell_type": "code", "execution_count": null, - "id": "02702f95", + "id": "d0ceec1d", "metadata": {}, "outputs": [], "source": [ - "my_analysis.plot_data_and_model()" + "p1=my_full_analysis._analysis_list[1].get_fit_parameters()[0]\n", + "print(p1)\n", + "print(p1.unique_name)\n", + "p2 = my_full_analysis._analysis_list[9].get_fit_parameters()[0]\n", + "print(p2)\n", + "print(p2.unique_name)\n" ] }, { "cell_type": "code", "execution_count": null, - "id": "70091539", + "id": "d792eee3", "metadata": {}, "outputs": [], "source": [ - "my_analysis.fit()" + "\n", + "my_full_analysis.Q" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "4217d56d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Parameter_2\n", + "\n", + "Parameter_4\n", + "\n", + "Parameter_6\n", + "\n", + "Parameter_8\n", + "\n", + "Parameter_10\n", + "\n", + "Parameter_12\n", + "\n", + "Parameter_14\n", + "\n", + "Parameter_16\n", + "\n", + "Parameter_18\n", + "\n", + "Parameter_20\n", + "\n", + "Parameter_4\n", + "\n", + "Parameter_6\n", + "\n", + "Parameter_8\n", + "\n", + "Parameter_10\n", + "\n", + "Parameter_12\n", + "\n", + "Parameter_14\n", + "\n", + "Parameter_16\n", + "\n", + "Parameter_18\n", + "\n", + "Parameter_20\n", + "\n", + "Parameter_22\n", + "\n", + "Parameter_4\n", + "\n", + "Parameter_6\n", + "\n", + "Parameter_8\n", + "\n", + "Parameter_10\n", + "\n", + "Parameter_12\n", + "\n", + "Parameter_14\n", + "\n", + "Parameter_16\n", + "\n", + "Parameter_18\n", + "\n", + "Parameter_20\n", + "\n", + "Parameter_4\n", + "\n", + "Parameter_6\n", + "\n" + ] + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "from easydynamics.sample_model import ComponentCollection\n", + "from easydynamics.sample_model import DeltaFunction\n", + "from easydynamics.sample_model.model_base import ModelBase\n", + "%matplotlib widget\n", + "import numpy as np\n", + "Q=np.linspace(0.1,15,31)\n", + "component_collection = ComponentCollection()\n", + "delta_function = DeltaFunction(display_name='DeltaFunction', area=1)\n", + "\n", + "component_collection.append_component(delta_function)\n", + "\n", + "\n", + "# sample_model = SampleModel(\n", + "sample_model = ModelBase(\n", + " components=component_collection,\n", + " unit='meV',\n", + " display_name='MySampleModel',\n", + " Q=Q,\n", + ")\n", + "\n", + "\n", + "for Q_index in range(len(sample_model.Q)):\n", + " pars = sample_model.get_all_variables(Q_index=Q_index) \n", + " pars[0].value=pars[0].value+Q_index\n", + " print(pars[0].unique_name)\n", + " print(pars[0])\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "35c89ce3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Parameter_5\n", + "\n", + "Parameter_4\n", + "\n", + "Parameter_5\n", + "\n", + "Parameter_4\n" + ] + } + ], + "source": [ + "vars2=sample_model._component_collections[1].get_all_variables()\n", + "for var in vars2:\n", + " print(var)\n", + " print(var.unique_name)\n", + "\n", + "var3=sample_model._component_collections[10].get_all_variables()\n", + "for var in var3:\n", + " print(var)\n", + " print(var.unique_name)" ] }, { "cell_type": "code", "execution_count": null, - "id": "2ad6384e", + "id": "02320e75", "metadata": {}, "outputs": [], "source": [ - "my_analysis.plot_data_and_model()" + "var." ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5451bbf3", + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { diff --git a/src/easydynamics/analysis/analysis old.py b/src/easydynamics/analysis/analysis old.py new file mode 100644 index 00000000..9d9039ea --- /dev/null +++ b/src/easydynamics/analysis/analysis old.py @@ -0,0 +1,497 @@ +# SPDX-FileCopyrightText: 2025-2026 EasyDynamics contributors +# SPDX-License-Identifier: BSD-3-Clause + + +import numpy as np +import plopp as pp +import scipp as sc +from easyscience.base_classes.model_base import ModelBase as EasyScienceModelBase +from easyscience.fitting.fitter import Fitter as EasyScienceFitter +from easyscience.variable import Parameter + +from easydynamics.convolution import Convolution +from easydynamics.experiment import Experiment +from easydynamics.sample_model import BackgroundModel +from easydynamics.sample_model import ResolutionModel +from easydynamics.sample_model import SampleModel + + +class Analysis(EasyScienceModelBase): + """For analysing data.""" + + def __init__( + self, + display_name: str = "MyAnalysis", + unique_name: str | None = None, + experiment: Experiment | None = None, + sample_model: SampleModel | None = None, + resolution_model: ResolutionModel | None = None, + background_model: BackgroundModel | None = None, + energy_offset: None = None, + ): + + super().__init__(display_name=display_name, unique_name=unique_name) + + if experiment is not None and not isinstance(experiment, Experiment): + raise TypeError("experiment must be an instance of Experiment or None.") + + self._experiment = experiment + + if sample_model is not None and not isinstance(sample_model, SampleModel): + raise TypeError("sample_model must be an instance of SampleModel or None.") + sample_model.Q = self.Q + self._sample_model = sample_model + + if resolution_model is not None and not isinstance( + resolution_model, ResolutionModel + ): + raise TypeError( + "resolution_model must be an instance of ResolutionModel or None." + ) + resolution_model.Q = self.Q + self._resolution_model = resolution_model + + if background_model is not None and not isinstance( + background_model, BackgroundModel + ): + raise TypeError( + "background_model must be an instance of BackgroundModel or None." + ) + background_model.Q = self.Q + self._background_model = background_model + + self._convolvers = [None] * (len(self.Q) if self.Q is not None else 0) + self._update_models() + + ############# + # Properties + ############# + + @property + def experiment(self) -> Experiment | None: + """The Experiment associated with this Analysis.""" + return self._experiment + + @experiment.setter + def experiment(self, value: Experiment | None) -> None: + if value is not None and not isinstance(value, Experiment): + raise TypeError("experiment must be an instance of Experiment or None.") + self._experiment = value + self._update_models() + + @property + def sample_model(self) -> SampleModel | None: + """The SampleModel associated with this Analysis.""" + return self._sample_model + + @sample_model.setter + def sample_model(self, value: SampleModel | None) -> None: + if value is not None and not isinstance(value, SampleModel): + raise TypeError("sample_model must be an instance of SampleModel or None.") + self._sample_model = value + self._update_models() + + @property + def resolution_model(self) -> ResolutionModel | None: + """The ResolutionModel associated with this Analysis.""" + return self._resolution_model + + @resolution_model.setter + def resolution_model(self, value: ResolutionModel | None) -> None: + if value is not None and not isinstance(value, ResolutionModel): + raise TypeError( + "resolution_model must be an instance of ResolutionModel or None." + ) + self._resolution_model = value + self._update_models() + + @property + def background_model(self) -> BackgroundModel | None: + """The BackgroundModel associated with this Analysis.""" + return self._background_model + + @background_model.setter + def background_model(self, value: BackgroundModel | None) -> None: + if value is not None and not isinstance(value, BackgroundModel): + raise TypeError( + "background_model must be an instance of BackgroundModel or None." + ) + self._background_model = value + self._update_models() + + @property + def Q(self) -> sc.Variable | None: + """The Q values from the associated Experiment, if available.""" + if self.experiment is not None: + return self.experiment.Q + return None + + @Q.setter + def Q(self, value) -> None: + """Q is a read-only property derived from the Experiment.""" + raise AttributeError("Q is a read-only property derived from the Experiment.") + + @property + def energy(self) -> sc.Variable | None: + """The energy values from the associated Experiment, if + available. + """ + if self.experiment is not None: + return self.experiment.energy + return None + + @energy.setter + def energy(self, value) -> None: + """Energy is a read-only property derived from the + Experiment. + """ + raise AttributeError( + "energy is a read-only property derived from the Experiment." + ) + + # TODO: make it use experiment temperature + @property + def temperature(self) -> Parameter | None: + """The temperature from the associated Experiment, if + available. + """ + return None + + @temperature.setter + def temperature(self, value) -> None: + """Temperature is a read-only property derived from the + Experiment. + """ + raise AttributeError( + "temperature is a read-only property derived from the Experiment." + ) + + # # TODO: make it use experiment temperature + # @property def temperature(self) -> Parameter | None: """The + # temperature from the associated Experiment, if available.""" if + # self.experiment is not None: return + # self.experiment.temperature return None + + # @temperature.setter def temperature(self, value) -> None: + # """temperature is a read-only property derived from the + # Experiment.""" raise AttributeError( "temperature is a + # read-only property derived from the Experiment." ) + + ############# + # Other methods + ############# + + def calculate(self, energy: float | None, Q_index: int) -> np.ndarray: + """Calculate the model prediction for a given Q index. + + Args: + energy (float): The energy value to calculate the model for. + Q_index (int): The index of the Q value to calculate the + model for. + Returns: + sc.DataArray: The calculated model prediction. + """ + if energy is None: + energy = self.energy + + if self.sample_model is None: + sample_intensity = np.zeros_like(energy) + else: + if self.resolution_model is None: + sample_intensity = self.sample_model._component_collections[ + Q_index + ].evaluate(energy) + else: + convolver = self._create_convolver(Q_index) + sample_intensity = convolver.convolution() + + if self.background_model is None: + background_intensity = np.zeros_like(energy) + else: + background_intensity = self.background_model._component_collections[ + Q_index + ].evaluate(energy) + + sample_plus_background = sample_intensity + background_intensity + + return sample_plus_background + + def calculate_individual_components( + self, Q_index: int + ) -> tuple[list[np.ndarray], list[np.ndarray]]: + """Calculate the model prediction for a given Q index for each + individual component. + + Args: + Q_index (int): The index of the Q value to calculate the + model for. + Returns: + list[np.ndarray]: The calculated model predictions for each + individual component. + """ + sample_results = [] + background_results = [] + + if self.sample_model is not None: + # Calculate sample components + for component in self.sample_model._component_collections[ + Q_index + ]._components: + if self.resolution_model is None: + component_intensity = component.evaluate(self.energy) + else: + convolver = Convolution( + sample_components=component, + resolution_components=self.resolution_model._component_collections[ + Q_index + ], + energy=self.energy, + temperature=self.temperature, + ) + component_intensity = convolver.convolution() + sample_results.append(component_intensity) + + if self.background_model is not None: + # Calculate background components + for component in self.background_model._component_collections[ + Q_index + ]._components: + component_intensity = component.evaluate(self.energy) + background_results.append(component_intensity) + + return sample_results, background_results + + def calculate_all_Q(self) -> list[np.ndarray]: + """Calculate the model prediction for all Q indices. + + Returns: + list[np.ndarray]: The calculated model predictions for all Q + indices. + """ + results = [] + for Q_index in range(len(self.Q)): + result = self.calculate(Q_index) + results.append(result) + return results + + # def calculate_individual_components_all_Q( + # self, + # add_background: bool = True, + # ) -> list[tuple[list[np.ndarray], list[np.ndarray]]]: + # """Calculate the model prediction for all Q indices for each + # individual component. + + # Returns: list[tuple[list[np.ndarray], list[np.ndarray]]]: The + # calculated model predictions for each individual component + # at all Q indices. """ all_results = [] for Q_index in + # range(len(self.Q)): sample_results, background_results = + # self.calculate_individual_components( Q_index ) if + # add_background: sample_results = sample_results + + # background_results all_results.append((sample_results, + # background_results)) return all_results + + def calculate_single_component_all_Q( + self, + component_index: int, + ) -> list[np.ndarray]: + """Calculate the model prediction for all Q indices for a single + component. + + Args: + component_index (int): The index of the component + Returns: + list[np.ndarray]: The calculated model predictions for the + specified component at all Q indices. + """ + + results = [] + for Q_index in range(len(self.Q)): + if self.sample_model is not None: + component = self.sample_model._component_collections[ + Q_index + ]._components[component_index] + if self.resolution_model is None: + component_intensity = component.evaluate(self.energy) + else: + convolver = Convolution( + sample_components=component, + resolution_components=self.resolution_model._component_collections[ + Q_index + ], + energy=self.energy, + temperature=self.temperature, + ) + component_intensity = convolver.convolution() + results.append(component_intensity) + else: + results.append(np.zeros_like(self.energy)) + + model_data_array = sc.DataArray( + data=sc.array(dims=["Q", "energy"], values=results), + coords={ + "Q": self.Q, + "energy": self.energy, + }, + ) + return model_data_array + + def fit(self, Q_index: int): + """Fit the model to the experimental data for a given Q index. + + Args: + Q_index (int): The index of the Q value to fit the model + to. + Returns: + FitResult: The result of the fit. + """ + if self._experiment is None: + raise ValueError("No experiment is associated with this Analysis.") + + if not isinstance(Q_index, int) or Q_index < 0 or Q_index >= len(self.Q): + raise ValueError("Q_index must be a valid index for the Q values.") + + data = self.experiment.data["Q", Q_index] + x = data.coords["energy"].values + y = data.values + e = data.variances**0.5 + + def fit_func(x_vals): + return self.calculate_theory(energy=x_vals, Q_index=Q_index) + + fitter = EasyScienceFitter( + fit_object=self, + fit_function=fit_func, + ) + + # Perform the fit + fit_result = fitter.fit(x=x, y=y, weights=1.0 / e) + + # Store result + self.fit_result = fit_result + + return fit_result + + def plot_data_and_model( + self, + plot_individual_components: bool = True, + ) -> None: + """Plot the experimental data and the model prediction. + + Args: + plot_individual_components (bool): Whether to plot + individual components. Default is True. + """ + if not isinstance(plot_individual_components, bool): + raise TypeError("plot_individual_components must be True or False.") + + model_data_array = self._create_model_data_group( + individual_components=plot_individual_components + ) + if self.experiment is None or self.experiment.data is None: + raise ValueError("Experiment data is not available for plotting.") + + from IPython.display import display + + fig = pp.slicer( + {"Data": self.experiment.data, "Model": model_data_array}, + color={"Data": "black", "Model": "red"}, + linestyle={"Data": "none", "Model": "solid"}, + marker={"Data": "o", "Model": "None"}, + ) + display(fig) + + ############# + # Private methods + ############# + + def _update_models(self): + """Update models based on the current experiment.""" + if self.experiment is None: + return + + for Q_index in range(len(self.Q)): + self._convolvers[Q_index] = self._create_convolver(Q_index) + + def _create_convolver(self, Q_index: int): + """Initialize and return a Convolution object for the given Q + index. + """ + # Add checks of empty sample models etc + + sample_components = self.sample_model._component_collections[Q_index] + resolution_components = self.resolution_model._component_collections[Q_index] + energy = self.energy + convolver = Convolution( + sample_components=sample_components, + resolution_components=resolution_components, + energy=energy, + temperature=self.temperature, + ) + return convolver + + def _create_model_data_group(self, individual_components=True) -> sc.DataArray: + """Create a Scipp DataArray representing the model over all Q + and energy values. + """ + if self.Q is None or self.energy is None: + raise ValueError("Q and energy must be defined in the experiment.") + + model_data = [] + for Q_index in range(len(self.Q)): + model_at_Q = self.calculate(Q_index) + model_data.append(model_at_Q) + + model_data_array = sc.DataArray( + data=sc.array(dims=["Q", "energy"], values=model_data), + coords={ + "Q": self.Q, + "energy": self.energy, + }, + ) + model_group = sc.DataGroup({"Model": model_data_array}) + + # if plot_individual_components: comps = + # ana.calculate_individual_components(E) for name, + # vals in comps.items(): if name not in + # component_arrays: component_arrays[name] = + # sc.zeros_like(data) csel = + # component_arrays[name] for d, i in + # zip(loop_dims, combo): csel = csel[d, i] + # csel.values = vals fsel.values = + # ana.calculate_theory(E) + + # # Build plot group + # data_and_model = {"Data": self._experiment._data.data, + # "Model": fit_total} if plot_individual_components and + # component_arrays: data_and_model.update(component_arrays) + # data_and_model = sc.DataGroup(data_and_model) + + if individual_components: + components = self.calculate_individual_components_all_Q() + for Q_index, (sample_comps, background_comps) in enumerate(components): + for samp_index, samp_comp in enumerate(sample_comps): + model_data_array[samp_comp.display_name] = sc.zeros_like( + model_data_array.data + ) + model_data_array[samp_comp.display_name].data[ + Q_index, : + ] = samp_comp + for back_index, back_comp in enumerate(background_comps): + model_data_array[back_comp.display_name] = sc.zeros_like( + model_data_array.data + ) + model_data_array[back_comp.display_name].data[ + Q_index, : + ] = back_comp + + model_data_array = model_data_array + model_group # WRONG BUT LINT + return model_data_array + + # def _create_convolvers( + # self, energy: np.ndarray | sc.Variable | None = None + # ) -> None: + # """Create Convolution objects for each Q value.""" + # num_Q = len(self.Q) if self.Q is not None else 0 + # self._convolvers = [ + # self._create_convolver(i, energy=energy) for i in range(num_Q) + # ] diff --git a/src/easydynamics/analysis/analysis.py b/src/easydynamics/analysis/analysis.py index 33d23545..5a3f6073 100644 --- a/src/easydynamics/analysis/analysis.py +++ b/src/easydynamics/analysis/analysis.py @@ -3,458 +3,144 @@ import numpy as np -import plopp as pp -import scipp as sc -from easyscience.base_classes.model_base import ModelBase as EasyScienceModelBase -from easyscience.fitting.fitter import Fitter as EasyScienceFitter +from easyscience.fitting.multi_fitter import MultiFitter from easyscience.variable import Parameter -from easydynamics.convolution import Convolution +from easydynamics.analysis.analysis1d import Analysis1d +from easydynamics.analysis.analysis_base import AnalysisBase from easydynamics.experiment import Experiment -from easydynamics.sample_model import BackgroundModel -from easydynamics.sample_model import ResolutionModel from easydynamics.sample_model import SampleModel +from easydynamics.sample_model.instrument_model import InstrumentModel -class Analysis(EasyScienceModelBase): +class Analysis(AnalysisBase): """For analysing data.""" def __init__( self, - display_name: str = 'MyAnalysis', + display_name: str = "MyAnalysis", unique_name: str | None = None, experiment: Experiment | None = None, sample_model: SampleModel | None = None, - resolution_model: ResolutionModel | None = None, - background_model: BackgroundModel | None = None, - energy_offset: None = None, + instrument_model: InstrumentModel | None = None, + extra_parameters: ( + Parameter | list[Parameter] | list[list[Parameter]] | None + ) = None, ): - super().__init__(display_name=display_name, unique_name=unique_name) + super().__init__( + display_name=display_name, + unique_name=unique_name, + experiment=experiment, + sample_model=sample_model, + instrument_model=instrument_model, + ) if experiment is not None and not isinstance(experiment, Experiment): - raise TypeError('experiment must be an instance of Experiment or None.') + raise TypeError("experiment must be an instance of Experiment or None.") + + self._analysis_list = [] + if self.Q is not None: + for Q_index in range(len(self.Q)): + analysis = Analysis1d( + display_name=f"{self.display_name}_Q{Q_index}", + unique_name=( + f"{self.unique_name}_Q{Q_index}" if self.unique_name else None + ), + experiment=self.experiment, + sample_model=self.sample_model, + instrument_model=self.instrument_model, + extra_parameters=extra_parameters, + Q_index=Q_index, + ) + self._analysis_list.append(analysis) - self._experiment = experiment + ############# + # Properties + ############# - if sample_model is not None and not isinstance(sample_model, SampleModel): - raise TypeError('sample_model must be an instance of SampleModel or None.') - sample_model.Q = self.Q - self._sample_model = sample_model + ############# + # Other methods + ############# + def calculate(self, Q_index: int | None = None) -> list[np.ndarray]: + """Calculate model data for a specific Q index.""" - if resolution_model is not None and not isinstance(resolution_model, ResolutionModel): - raise TypeError('resolution_model must be an instance of ResolutionModel or None.') - resolution_model.Q = self.Q - self._resolution_model = resolution_model + if Q_index is None: + result = [] + for analysis in self._analysis_list: + result.append(analysis.calculate()) + return result - if background_model is not None and not isinstance(background_model, BackgroundModel): - raise TypeError('background_model must be an instance of BackgroundModel or None.') - background_model.Q = self.Q - self._background_model = background_model + if Q_index < 0 or Q_index >= len(self._analysis_list): + raise IndexError("Q_index out of range.") - self._convolvers = [None] * (len(self.Q) if self.Q is not None else 0) - self._update_models() + return self._analysis_list[Q_index].calculate() ############# - # Properties + # Private methods ############# - @property - def experiment(self) -> Experiment | None: - """The Experiment associated with this Analysis.""" - return self._experiment - - @experiment.setter - def experiment(self, value: Experiment | None) -> None: - if value is not None and not isinstance(value, Experiment): - raise TypeError('experiment must be an instance of Experiment or None.') - self._experiment = value - self._update_models() - - @property - def sample_model(self) -> SampleModel | None: - """The SampleModel associated with this Analysis.""" - return self._sample_model - - @sample_model.setter - def sample_model(self, value: SampleModel | None) -> None: - if value is not None and not isinstance(value, SampleModel): - raise TypeError('sample_model must be an instance of SampleModel or None.') - self._sample_model = value - self._update_models() - - @property - def resolution_model(self) -> ResolutionModel | None: - """The ResolutionModel associated with this Analysis.""" - return self._resolution_model - - @resolution_model.setter - def resolution_model(self, value: ResolutionModel | None) -> None: - if value is not None and not isinstance(value, ResolutionModel): - raise TypeError('resolution_model must be an instance of ResolutionModel or None.') - self._resolution_model = value - self._update_models() - - @property - def background_model(self) -> BackgroundModel | None: - """The BackgroundModel associated with this Analysis.""" - return self._background_model - - @background_model.setter - def background_model(self, value: BackgroundModel | None) -> None: - if value is not None and not isinstance(value, BackgroundModel): - raise TypeError('background_model must be an instance of BackgroundModel or None.') - self._background_model = value - self._update_models() - - @property - def Q(self) -> sc.Variable | None: - """The Q values from the associated Experiment, if available.""" - if self.experiment is not None: - return self.experiment.Q - return None - - @Q.setter - def Q(self, value) -> None: - """Q is a read-only property derived from the Experiment.""" - raise AttributeError('Q is a read-only property derived from the Experiment.') - - @property - def energy(self) -> sc.Variable | None: - """The energy values from the associated Experiment, if - available. - """ - if self.experiment is not None: - return self.experiment.energy - return None - - @energy.setter - def energy(self, value) -> None: - """Energy is a read-only property derived from the - Experiment. - """ - raise AttributeError('energy is a read-only property derived from the Experiment.') - - # TODO: make it use experiment temperature - @property - def temperature(self) -> Parameter | None: - """The temperature from the associated Experiment, if - available. - """ - return None - - @temperature.setter - def temperature(self, value) -> None: - """Temperature is a read-only property derived from the - Experiment. - """ - raise AttributeError('temperature is a read-only property derived from the Experiment.') - - # # TODO: make it use experiment temperature - # @property def temperature(self) -> Parameter | None: """The - # temperature from the associated Experiment, if available.""" if - # self.experiment is not None: return - # self.experiment.temperature return None - - # @temperature.setter def temperature(self, value) -> None: - # """temperature is a read-only property derived from the - # Experiment.""" raise AttributeError( "temperature is a - # read-only property derived from the Experiment." ) + def _fit_single_Q(self, Q_index: int) -> None: + """Fit data for a single Q index.""" - ############# - # Other methods - ############# + if Q_index < 0 or Q_index >= len(self._analysis_list): + raise IndexError("Q_index out of range.") - def calculate(self, energy: float | None, Q_index: int) -> np.ndarray: - """Calculate the model prediction for a given Q index. - - Args: - energy (float): The energy value to calculate the model for. - Q_index (int): The index of the Q value to calculate the - model for. - Returns: - sc.DataArray: The calculated model prediction. - """ - if energy is None: - energy = self.energy - - if self.sample_model is None: - sample_intensity = np.zeros_like(energy) - else: - if self.resolution_model is None: - sample_intensity = self.sample_model._component_collections[Q_index].evaluate( - energy - ) - else: - convolver = self._create_convolver(Q_index) - sample_intensity = convolver.convolution() - - if self.background_model is None: - background_intensity = np.zeros_like(energy) - else: - background_intensity = self.background_model._component_collections[Q_index].evaluate( - energy + self._analysis_list[Q_index].fit() + + def _fit_all_Q_independently(self) -> None: + """Fit data for all Q indices independently.""" + + for analysis in self._analysis_list: + analysis.fit() + + def _fit_all_Q_simultaneously(self) -> None: + """Fit data for all Q indices simultaneously.""" + + xs = [] + ys = [] + ws = [] + + for analysis in self._analysis_list: + data = analysis.experiment.data["Q", analysis.Q_index] + + x = data.coords["energy"].values + y = data.values + e = np.sqrt(data.variances) + + analysis._convolver = analysis._create_convolver( + Q_index=analysis.Q_index, + energy=x, ) - sample_plus_background = sample_intensity + background_intensity - - return sample_plus_background - - def calculate_individual_components( - self, Q_index: int - ) -> tuple[list[np.ndarray], list[np.ndarray]]: - """Calculate the model prediction for a given Q index for each - individual component. - - Args: - Q_index (int): The index of the Q value to calculate the - model for. - Returns: - list[np.ndarray]: The calculated model predictions for each - individual component. - """ - sample_results = [] - background_results = [] - - if self.sample_model is not None: - # Calculate sample components - for component in self.sample_model._component_collections[Q_index]._components: - if self.resolution_model is None: - component_intensity = component.evaluate(self.energy) - else: - convolver = Convolution( - sample_components=component, - resolution_components=self.resolution_model._component_collections[ - Q_index - ], - energy=self.energy, - temperature=self.temperature, - ) - component_intensity = convolver.convolution() - sample_results.append(component_intensity) - - if self.background_model is not None: - # Calculate background components - for component in self.background_model._component_collections[Q_index]._components: - component_intensity = component.evaluate(self.energy) - background_results.append(component_intensity) - - return sample_results, background_results - - def calculate_all_Q(self) -> list[np.ndarray]: - """Calculate the model prediction for all Q indices. - - Returns: - list[np.ndarray]: The calculated model predictions for all Q - indices. - """ - results = [] - for Q_index in range(len(self.Q)): - result = self.calculate(Q_index) - results.append(result) - return results + xs.append(x) + ys.append(y) + ws.append(1.0 / e) - # def calculate_individual_components_all_Q( - # self, - # add_background: bool = True, - # ) -> list[tuple[list[np.ndarray], list[np.ndarray]]]: - # """Calculate the model prediction for all Q indices for each - # individual component. - - # Returns: list[tuple[list[np.ndarray], list[np.ndarray]]]: The - # calculated model predictions for each individual component - # at all Q indices. """ all_results = [] for Q_index in - # range(len(self.Q)): sample_results, background_results = - # self.calculate_individual_components( Q_index ) if - # add_background: sample_results = sample_results + - # background_results all_results.append((sample_results, - # background_results)) return all_results - - def calculate_single_component_all_Q( - self, - component_index: int, - ) -> list[np.ndarray]: - """Calculate the model prediction for all Q indices for a single - component. - - Args: - component_index (int): The index of the component - Returns: - list[np.ndarray]: The calculated model predictions for the - specified component at all Q indices. - """ - - results = [] - for Q_index in range(len(self.Q)): - if self.sample_model is not None: - component = self.sample_model._component_collections[Q_index]._components[ - component_index - ] - if self.resolution_model is None: - component_intensity = component.evaluate(self.energy) - else: - convolver = Convolution( - sample_components=component, - resolution_components=self.resolution_model._component_collections[ - Q_index - ], - energy=self.energy, - temperature=self.temperature, - ) - component_intensity = convolver.convolution() - results.append(component_intensity) - else: - results.append(np.zeros_like(self.energy)) - - model_data_array = sc.DataArray( - data=sc.array(dims=['Q', 'energy'], values=results), - coords={ - 'Q': self.Q, - 'energy': self.energy, - }, - ) - return model_data_array - - def fit(self, Q_index: int): - """Fit the model to the experimental data for a given Q index. - - Args: - Q_index (int): The index of the Q value to fit the model - to. - Returns: - FitResult: The result of the fit. - """ - if self._experiment is None: - raise ValueError('No experiment is associated with this Analysis.') - - if not isinstance(Q_index, int) or Q_index < 0 or Q_index >= len(self.Q): - raise ValueError('Q_index must be a valid index for the Q values.') - - data = self.experiment.data['Q', Q_index] - x = data.coords['energy'].values - y = data.values - e = data.variances**0.5 - - def fit_func(x_vals): - return self.calculate_theory(energy=x_vals, Q_index=Q_index) - - fitter = EasyScienceFitter( - fit_object=self, - fit_function=fit_func, - ) + fit_functions = [] - # Perform the fit - fit_result = fitter.fit(x=x, y=y, weights=1.0 / e) + for analysis in self._analysis_list: - # Store result - self.fit_result = fit_result + def make_fit_func(a): + def fit_func(_): + return a._calculate() - return fit_result + return fit_func - def plot_data_and_model( - self, - plot_individual_components: bool = True, - ) -> None: - """Plot the experimental data and the model prediction. - - Args: - plot_individual_components (bool): Whether to plot - individual components. Default is True. - """ - if not isinstance(plot_individual_components, bool): - raise TypeError('plot_individual_components must be True or False.') - - model_data_array = self._create_model_data_group( - individual_components=plot_individual_components - ) - if self.experiment is None or self.experiment.data is None: - raise ValueError('Experiment data is not available for plotting.') + fit_functions.append(make_fit_func(analysis)) - from IPython.display import display + mf = MultiFitter( + fit_objects=self._analysis_list, + fit_functions=fit_functions, + ) - fig = pp.slicer( - {'Data': self.experiment.data, 'Model': model_data_array}, - color={'Data': 'black', 'Model': 'red'}, - linestyle={'Data': 'none', 'Model': 'solid'}, - marker={'Data': 'o', 'Model': 'None'}, + results = mf.fit( + x=xs, + y=ys, + weights=ws, ) - display(fig) + return results ############# - # Private methods + # Dunder methods ############# - - def _update_models(self): - """Update models based on the current experiment.""" - if self.experiment is None: - return - - for Q_index in range(len(self.Q)): - self._convolvers[Q_index] = self._create_convolver(Q_index) - - def _create_convolver(self, Q_index: int): - """Initialize and return a Convolution object for the given Q - index. - """ - # Add checks of empty sample models etc - - sample_components = self.sample_model._component_collections[Q_index] - resolution_components = self.resolution_model._component_collections[Q_index] - energy = self.energy - convolver = Convolution( - sample_components=sample_components, - resolution_components=resolution_components, - energy=energy, - temperature=self.temperature, - ) - return convolver - - def _create_model_data_group(self, individual_components=True) -> sc.DataArray: - """Create a Scipp DataArray representing the model over all Q - and energy values. - """ - if self.Q is None or self.energy is None: - raise ValueError('Q and energy must be defined in the experiment.') - - model_data = [] - for Q_index in range(len(self.Q)): - model_at_Q = self.calculate(Q_index) - model_data.append(model_at_Q) - - model_data_array = sc.DataArray( - data=sc.array(dims=['Q', 'energy'], values=model_data), - coords={ - 'Q': self.Q, - 'energy': self.energy, - }, - ) - model_group = sc.DataGroup({'Model': model_data_array}) - - # if plot_individual_components: comps = - # ana.calculate_individual_components(E) for name, - # vals in comps.items(): if name not in - # component_arrays: component_arrays[name] = - # sc.zeros_like(data) csel = - # component_arrays[name] for d, i in - # zip(loop_dims, combo): csel = csel[d, i] - # csel.values = vals fsel.values = - # ana.calculate_theory(E) - - # # Build plot group - # data_and_model = {"Data": self._experiment._data.data, - # "Model": fit_total} if plot_individual_components and - # component_arrays: data_and_model.update(component_arrays) - # data_and_model = sc.DataGroup(data_and_model) - - if individual_components: - components = self.calculate_individual_components_all_Q() - for Q_index, (sample_comps, background_comps) in enumerate(components): - for samp_index, samp_comp in enumerate(sample_comps): - model_data_array[samp_comp.display_name] = sc.zeros_like(model_data_array.data) - model_data_array[samp_comp.display_name].data[Q_index, :] = samp_comp - for back_index, back_comp in enumerate(background_comps): - model_data_array[back_comp.display_name] = sc.zeros_like(model_data_array.data) - model_data_array[back_comp.display_name].data[Q_index, :] = back_comp - - model_data_array = model_data_array + model_group # WRONG BUT LINT - return model_data_array diff --git a/src/easydynamics/analysis/analysis1d.py b/src/easydynamics/analysis/analysis1d.py index 2a0b554c..57046b4b 100644 --- a/src/easydynamics/analysis/analysis1d.py +++ b/src/easydynamics/analysis/analysis1d.py @@ -2,12 +2,14 @@ # SPDX-License-Identifier: BSD-3-Clause +from inspect import Parameter + import numpy as np +import scipp as sc from easyscience.fitting.fitter import Fitter as EasyScienceFitter from easyscience.variable import DescriptorNumber from easydynamics.analysis.analysis_base import AnalysisBase -from easydynamics.convolution import Convolution from easydynamics.experiment import Experiment from easydynamics.sample_model import InstrumentModel from easydynamics.sample_model import SampleModel @@ -24,6 +26,7 @@ def __init__( sample_model: SampleModel | None = None, instrument_model: InstrumentModel | None = None, Q_index: int | None = None, + extra_parameters: Parameter | list[Parameter] | None = None, ): super().__init__( display_name=display_name, @@ -44,6 +47,8 @@ def __init__( self._fit_result = None + self._convolver = self._create_convolver(Q_index=self.Q_index) + ############# # Properties ############# @@ -68,12 +73,13 @@ def Q_index(self, index: int | None) -> None: ): raise ValueError("Q_index must be a valid index for the Q values.") self._Q_index = index + self._on_Q_index_changed() ############# # Other methods ############# - def calculate(self, energy: float | None = None) -> np.ndarray: + def calculate(self, energy: np.ndarray | sc.Variable | None = None) -> np.ndarray: """Calculate the model prediction for a given Q index. Args: @@ -81,37 +87,23 @@ def calculate(self, energy: float | None = None) -> np.ndarray: Returns: sc.DataArray: The calculated model prediction. """ - Q_index = self._require_Q_index() - if energy is None: - energy = self.energy.values + self._convolver = self._create_convolver(Q_index=self.Q_index, energy=energy) - # TODO: handle units properly + return self._calculate() - energy_offset = self.instrument_model.get_energy_offset_at_Q(Q_index).value + def _calculate(self) -> np.ndarray: + """Calculate the model prediction for a given Q index. - # Sample - sample_components = self.sample_model.get_component_collection(Q_index) - resolution_components = ( - self.instrument_model.resolution_model.get_component_collection(Q_index) - ) + Args: + energy (float): The energy value to calculate the model for. + Returns: + sc.DataArray: The calculated model prediction. + """ - sample_intensity = self._evaluate_sample( - sample_components=sample_components, - resolution_components=resolution_components, - energy=energy, - energy_offset=energy_offset, - ) + sample_intensity = self._evaluate_sample() - # Background - background_component_collection = ( - self.instrument_model.background_model.get_component_collection(Q_index) - ) - background_intensity = self._evaluate_background( - background_components=background_component_collection, - energy=energy, - energy_offset=energy_offset, - ) + background_intensity = self._evaluate_background() sample_plus_background = sample_intensity + background_intensity @@ -119,7 +111,7 @@ def calculate(self, energy: float | None = None) -> np.ndarray: def calculate_individual_components( self, - energy: float | None = None, + energy: np.ndarray | sc.Variable | None = None, ) -> np.ndarray: """Calculate the model prediction for a given Q index. @@ -130,22 +122,10 @@ def calculate_individual_components( """ Q_index = self._require_Q_index() - if energy is None: - energy = self.energy.values - - # TODO: handle units properly - - energy_offset = self.instrument_model.get_energy_offset_at_Q(Q_index).value + energy = self._handle_energy(energy) - # Sample. Convolve with resolution if resolution components are - # present, otherwise just evaluate sample components one by one - # to get individual contributions. sample_components = self.sample_model.get_component_collection(Q_index) - resolution_components = ( - self.instrument_model.resolution_model.get_component_collection(Q_index) - ) - if sample_components.is_empty: sample_intensity = [np.zeros_like(energy)] else: @@ -153,9 +133,7 @@ def calculate_individual_components( for component in sample_components.components: component_intensity = self._evaluate_sample_component( component=component, - resolution_components=resolution_components, energy=energy, - energy_offset=energy_offset, ) sample_intensity.append(component_intensity) @@ -173,7 +151,6 @@ def calculate_individual_components( component_intensity = self._evaluate_background_component( component=component, energy=energy, - energy_offset=energy_offset, ) background_intensity.append(component_intensity) @@ -185,6 +162,12 @@ def fit(self): Args: Returns: FitResult: The result of the fit. + + Notes + ----- + The energy grid is fixed for the duration of the fit. + Convolution objects are created once and reused during + parameter optimization for performance reasons. """ if self._experiment is None: raise ValueError("No experiment is associated with this Analysis.") @@ -196,8 +179,10 @@ def fit(self): y = data.values e = data.variances**0.5 - def fit_func(x_vals): - return self.calculate(energy=x_vals) + self._convolver = self._create_convolver(Q_index=self.Q_index, energy=x) + + def fit_func(_): + return self._calculate() fitter = EasyScienceFitter( fit_object=self, @@ -279,7 +264,7 @@ def get_all_variables(self) -> list[DescriptorNumber]: variables.extend(self.instrument_model.get_all_variables(Q_index=self.Q_index)) - if self._extra_parameters != []: + if self._extra_parameters: variables.extend(self._extra_parameters) return variables @@ -287,55 +272,51 @@ def get_all_variables(self) -> list[DescriptorNumber]: ############# # Private methods ############# - def _evaluate_sample( - self, - sample_components, - resolution_components, - energy, - energy_offset, - ): - if resolution_components.is_empty: - return sample_components.evaluate(energy - energy_offset) - convolver = self._convolvers[self._require_Q_index()] - return convolver.convolution() - - def _evaluate_sample_component( - self, - component, - resolution_components, - energy, - energy_offset, - ): - if resolution_components.is_empty: - return component.evaluate(energy - energy_offset) - convolver = Convolution( - sample_components=component, - resolution_components=resolution_components, - energy=energy, - temperature=self.temperature, - energy_offset=energy_offset, - ) - return convolver.convolution() - - def _evaluate_background( - self, - background_components, - energy, - energy_offset, - ): - if background_components.is_empty: - return np.zeros_like(energy) - return background_components.evaluate(energy - energy_offset) - - def _evaluate_background_component( - self, - component, - energy, - energy_offset, - ): - return component.evaluate(energy - energy_offset) def _require_Q_index(self) -> int: + """ + Get the Q index for single Q analysis, ensuring it is set. + Raises a ValueError if the Q index is not set. + Returns: + int: The Q index. + """ if self._Q_index is None: raise ValueError("Q_index must be set.") return self._Q_index + + def _handle_energy( + self, energy: np.ndarray | sc.Variable | None + ) -> np.ndarray | sc.Variable: + """ " + Handle the energy input for evaluation methods. + + If energy is None, use the energy values from the experiment. + If energy is a sc.Variable, extract the values as a numpy array. + If energy is already a numpy array, return it as is. + + Args: + energy (np.ndarray | sc.Variable | None): The input energy values. + Returns: + np.ndarray: The energy values to use for evaluation. + """ + # TODO: handle units properly + + if energy is None: + energy = self.energy.values + + if isinstance(energy, np.ndarray): + return energy + + if isinstance(energy, sc.Variable): + return energy.values + + raise TypeError("Energy must be a numpy array, sc.Variable, or None.") + + def _on_Q_index_changed(self) -> None: + """ + Handle changes to the Q index. + + This method is called whenever the Q index is changed. It updates + the Convolution object for the new Q index. + """ + self._convolver = self._create_convolver(Q_index=self.Q_index) diff --git a/src/easydynamics/analysis/analysis_base.py b/src/easydynamics/analysis/analysis_base.py index 81d0c250..7caf89d6 100644 --- a/src/easydynamics/analysis/analysis_base.py +++ b/src/easydynamics/analysis/analysis_base.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: BSD-3-Clause +import numpy as np import scipp as sc from easyscience.base_classes.model_base import ModelBase as EasyScienceModelBase from easyscience.variable import Parameter @@ -10,6 +11,8 @@ from easydynamics.experiment import Experiment from easydynamics.sample_model import InstrumentModel from easydynamics.sample_model import SampleModel +from easydynamics.sample_model.component_collection import ComponentCollection +from easydynamics.sample_model.components.model_component import ModelComponent class AnalysisBase(EasyScienceModelBase): @@ -63,7 +66,6 @@ def __init__( else: self._extra_parameters = [] - self._convolvers = [None] * (len(self.Q) if self.Q is not None else 0) self._on_experiment_changed() ############# @@ -164,46 +166,216 @@ def temperature(self, value) -> None: def _on_experiment_changed(self) -> None: self._sample_model.Q = self.Q self._instrument_model.Q = self.Q - self._create_convolvers() def _on_sample_model_changed(self) -> None: self._sample_model.Q = self.Q - self._create_convolvers() def _on_instrument_model_changed(self) -> None: self._instrument_model.Q = self.Q - self._create_convolvers() - def _create_convolvers(self) -> None: - """Create Convolution objects for each Q value.""" - num_Q = len(self.Q) if self.Q is not None else 0 - self._convolvers = [self._create_convolver(i) for i in range(num_Q)] - - def _create_convolver(self, Q_index: int) -> Convolution: + def _create_convolver( + self, Q_index: int, energy: np.ndarray | sc.Variable | None = None + ) -> Convolution | None: """Initialize and return a Convolution object for the given Q index. """ - sample_components = self.sample_model._component_collections[Q_index] - if sample_components == []: - return Convolution() + sample_components = self.sample_model.get_component_collection(Q_index) + if sample_components.is_empty: + return None resolution_components = ( - self.instrument_model.resolution_model._component_collections[Q_index] + self.instrument_model.resolution_model.get_component_collection(Q_index) ) - if resolution_components == []: - return Convolution() - - energy = self.energy + if resolution_components.is_empty: + return None + if energy is None: + energy = self.energy # TODO: allow convolution options to be set. convolver = Convolution( sample_components=sample_components, resolution_components=resolution_components, energy=energy, temperature=self.temperature, - energy_offset=self.instrument_model._energy_offsets[Q_index], + energy_offset=self.instrument_model.get_energy_offset_at_Q(Q_index), ) return convolver + def _evaluate_components( + self, + components: ComponentCollection | ModelComponent, + energy: np.ndarray | sc.Variable | None = None, + convolver: Convolution | None = None, + convolve: bool = True, + Q_index: int | None = None, + ): + """ + Calculate the contribution of a set of components, optionally + convolving with the resolution. + If convolve is True and a Convolution object is provided, + use it to perform the convolution of the components with the + resolution. If convolve is True but no Convolution object is + provided, create a new Convolution object for the given + components and energy. If convolve is False, evaluate the + components directly without convolution. + Args: + components (ComponentCollection | ModelComponent): + The components to evaluate. + energy (np.ndarray | sc.Variable | None): + The energy values to evaluate the components for. If + None, the energy values from the experiment will be + used. + convolver (Convolution | None): + An optional Convolution object to use for convolution. + If None, a new Convolution object will be created if + convolve is True. + convolve (bool): + Whether to perform convolution with the resolution. + Default is True. + """ + if Q_index is None: + Q_index = self._require_Q_index() + energy = self._handle_energy(energy) + energy_offset = self.instrument_model.get_energy_offset_at_Q(Q_index).value + + # If there are no components, return zero + if isinstance(components, ComponentCollection) and components.is_empty: + return np.zeros_like(energy) + + # No convolution + if not convolve: + return components.evaluate(energy - energy_offset) + + resolution = self.instrument_model.resolution_model.get_component_collection( + Q_index + ) + if resolution.is_empty: + return components.evaluate(energy - energy_offset) + + # Convolution For fitting we don't want to create a new + # Convolution object at each iteration + if convolver is not None: + return convolver.convolution() + + # For evaluating individual components + conv = Convolution( + sample_components=components, + resolution_components=resolution, + energy=energy, + temperature=self.temperature, + energy_offset=energy_offset, + ) + return conv.convolution() + + def _evaluate_sample( + self, + energy: np.ndarray | sc.Variable | None = None, + Q_index: int | None = None, + ): + """ + Evaluate the sample contribution for a given Q index. + + If a Convolution object exists for the Q index, use it to + perform the convolution of the sample components with the + resolution components. If no Convolution object exists, evaluate + the sample components directly without convolution. + + Args: + energy (np.ndarray | sc.Variable | None): The energy values + to evaluate the sample contribution for. If None, the energy + values from the experiment will be used. + Returns: + np.ndarray: The evaluated sample contribution. + """ + if Q_index is None: + Q_index = self._require_Q_index() + components = self.sample_model.get_component_collection(Q_index=Q_index) + return self._evaluate_components( + components=components, + energy=energy, + convolver=self._convolver, + convolve=True, + ) + + def _evaluate_sample_component( + self, + component, + energy: np.ndarray | sc.Variable | None = None, + ): + """ + Evaluate a single sample component for a given Q index. + If a Convolution object exists for the Q index, use it to + perform the convolution of the sample component with the + resolution components. If no Convolution object exists, evaluate + the sample component directly without convolution. + Args: + component: The sample component to evaluate. + energy (np.ndarray | sc.Variable | None): The energy values + to evaluate the sample component for. If None, the energy + values from the experiment will be used. + Returns: + np.ndarray: The evaluated sample component contribution. + """ + return self._evaluate_components( + components=component, + energy=energy, + convolver=None, + convolve=True, + ) + + def _evaluate_background( + self, + energy: np.ndarray | sc.Variable | None = None, + Q_index: int | None = None, + ): + """ + Evaluate the background contribution for a given Q index. + Evaluate each background component separately to get individual + contributions. Args: + energy (np.ndarray | sc.Variable | None): The energy values + to evaluate the background contribution for. If None, the + energy values from the experiment will be used. + Returns: + np.ndarray: The evaluated background contribution. + """ + + if Q_index is None: + Q_index = self._require_Q_index() + background_components = ( + self.instrument_model.background_model.get_component_collection( + Q_index=Q_index + ) + ) + return self._evaluate_components( + components=background_components, + energy=energy, + convolver=None, + convolve=False, + ) + + def _evaluate_background_component( + self, + component, + energy: np.ndarray | sc.Variable | None = None, + ): + """ + Evaluate a single background component for a given Q index. + Evaluate the background component directly without convolution. + Args: + component: The background component to evaluate. + energy (np.ndarray | sc.Variable | None): The energy values + to evaluate the background component for. If None, the energy + values from the experiment will be used. + Returns: + np.ndarray: The evaluated background component contribution. + """ + + return self._evaluate_components( + components=component, + energy=energy, + convolver=None, + convolve=False, + ) + ############# # Dunder methods ############# From 31f96856c7426bedb0843f5cf679ad4454bff4be Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Tue, 10 Feb 2026 16:20:10 +0100 Subject: [PATCH 11/17] fitting and plotting for multiple Q --- .../analysis old parameter bug.ipynb | 384 ++++++++++++++++++ docs/docs/tutorials/analysis.ipynb | 336 ++++----------- src/easydynamics/analysis/analysis.py | 230 +++++++++-- src/easydynamics/analysis/analysis1d.py | 227 ++++++++++- src/easydynamics/analysis/analysis_base.py | 207 ---------- src/easydynamics/experiment/experiment.py | 94 ++--- src/easydynamics/sample_model/model_base.py | 11 +- src/easydynamics/utils/utils.py | 34 +- 8 files changed, 965 insertions(+), 558 deletions(-) create mode 100644 docs/docs/tutorials/analysis old parameter bug.ipynb diff --git a/docs/docs/tutorials/analysis old parameter bug.ipynb b/docs/docs/tutorials/analysis old parameter bug.ipynb new file mode 100644 index 00000000..85bddaaa --- /dev/null +++ b/docs/docs/tutorials/analysis old parameter bug.ipynb @@ -0,0 +1,384 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "8643b10c", + "metadata": {}, + "source": [ + "asd" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bca91d3c", + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "from easydynamics.analysis.analysis1d import Analysis1d\n", + "from easydynamics.experiment import Experiment\n", + "from easydynamics.sample_model import ComponentCollection\n", + "from easydynamics.sample_model import DeltaFunction\n", + "from easydynamics.sample_model import Gaussian\n", + "from easydynamics.sample_model import Polynomial\n", + "from easydynamics.sample_model.background_model import BackgroundModel\n", + "from easydynamics.sample_model.resolution_model import ResolutionModel\n", + "from easydynamics.sample_model.sample_model import SampleModel\n", + "from easydynamics.sample_model.instrument_model import InstrumentModel\n", + "from easydynamics.analysis.analysis import Analysis\n", + "%matplotlib widget" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8deca9b6", + "metadata": {}, + "outputs": [], + "source": [ + "vanadium_experiment = Experiment('Vanadium')\n", + "vanadium_experiment.load_hdf5(filename='vanadium_data_example.h5')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "41f842f0", + "metadata": {}, + "outputs": [], + "source": [ + "# # Create a diffusion_model and components for the SampleModel\n", + "\n", + "# # Creating components\n", + "# component_collection = ComponentCollection()\n", + "# delta_function = DeltaFunction(display_name='DeltaFunction', area=1)\n", + "# gaussian = Gaussian(display_name='Gaussian', width=0.1, center=0.5, area=0.5)\n", + "\n", + "# # Adding components to the component collection\n", + "# component_collection.append_component(delta_function)\n", + "\n", + "\n", + "# sample_model = SampleModel(\n", + "# components=component_collection,\n", + "# unit='meV',\n", + "# display_name='MySampleModel',\n", + "# )\n", + "\n", + "# res_gauss = Gaussian(width=0.1)\n", + "# res_gauss.area.fixed = True\n", + "# resolution_model = ResolutionModel(components=res_gauss)\n", + "\n", + "\n", + "# background_model = BackgroundModel(components=Polynomial(coefficients=[0.001]))\n", + "\n", + "# instrument_model = InstrumentModel(\n", + "# resolution_model=resolution_model,\n", + "# background_model=background_model,\n", + "# )\n", + "\n", + "# my_analysis = Analysis1d(\n", + "# experiment=vanadium_experiment,\n", + "# sample_model=sample_model,\n", + "# instrument_model=instrument_model,\n", + "# Q_index=5,\n", + "# )\n", + "\n", + "\n", + "# values = my_analysis.calculate()\n", + "# sample_values, background_values = my_analysis.calculate_individual_components()\n", + "\n", + "# plt.figure()\n", + "# plt.plot(my_analysis.energy.values, values, label='Total Model')\n", + "# for component_index in range(len(sample_values)):\n", + "# plt.plot(\n", + "# my_analysis.energy.values,\n", + "# sample_values[component_index],\n", + "# label=f'Sample Component {component_index}',\n", + "# linestyle='--',\n", + "# )\n", + "\n", + "# for component_index in range(len(background_values)):\n", + "# plt.plot(\n", + "# my_analysis.energy.values,\n", + "# background_values[component_index],\n", + "# label=f'Background Component {component_index}',\n", + "# linestyle=':',\n", + "# )\n", + "# plt.xlabel('Energy (meV)')\n", + "# plt.ylabel('Intensity')\n", + "# plt.title(f'Q index: {5}')\n", + "# plt.legend()\n", + "# plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6762faba", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "02702f95", + "metadata": {}, + "outputs": [], + "source": [ + "# my_analysis.plot_data_and_model()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "70091539", + "metadata": {}, + "outputs": [], + "source": [ + "# my_analysis.fit()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2ad6384e", + "metadata": {}, + "outputs": [], + "source": [ + "# my_analysis.plot_data_and_model()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2dfb1f90", + "metadata": {}, + "outputs": [], + "source": [ + "# my_analysis.get_all_variables()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5afefbab", + "metadata": {}, + "outputs": [], + "source": [ + "# my_analysis.get_fit_parameters()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "465c0e1e", + "metadata": {}, + "outputs": [], + "source": [ + "# for Q_index in range(len(my_analysis.Q)):\n", + "# my_analysis.Q_index = Q_index\n", + "# my_analysis.fit()\n", + "# my_analysis.plot_data_and_model()\n", + "# print(my_analysis.get_fit_parameters())\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9bdeed2b", + "metadata": {}, + "outputs": [], + "source": [ + "# Create a diffusion_model and components for the SampleModel\n", + "\n", + "# Creating components\n", + "component_collection = ComponentCollection()\n", + "delta_function = DeltaFunction(display_name='DeltaFunction', area=1)\n", + "gaussian = Gaussian(display_name='Gaussian', width=0.1, center=0.5, area=0.5)\n", + "\n", + "# Adding components to the component collection\n", + "component_collection.append_component(delta_function)\n", + "\n", + "\n", + "sample_model = SampleModel(\n", + " components=component_collection,\n", + " unit='meV',\n", + " display_name='MySampleModel',\n", + ")\n", + "\n", + "res_gauss = Gaussian(width=0.1)\n", + "res_gauss.area.fixed = True\n", + "resolution_model = ResolutionModel(components=res_gauss)\n", + "\n", + "\n", + "background_model = BackgroundModel(components=Polynomial(coefficients=[0.001]))\n", + "\n", + "instrument_model = InstrumentModel(\n", + " resolution_model=resolution_model,\n", + " background_model=background_model,\n", + ")\n", + "\n", + "my_full_analysis = Analysis(\n", + " experiment=vanadium_experiment,\n", + " sample_model=sample_model,\n", + " instrument_model=instrument_model,\n", + ")\n", + "\n", + "# my_full_analysis._fit_all_Q_independently()\n", + "my_full_analysis._fit_all_Q_simultaneously()\n", + "for analysis_object in my_full_analysis._analysis_list:\n", + " analysis_object.plot_data_and_model()\n", + " print(analysis_object.get_fit_parameters())\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0a727fc3", + "metadata": {}, + "outputs": [], + "source": [ + "for analysis_object in my_full_analysis._analysis_list:\n", + " print(analysis_object.get_fit_parameters())\n", + "\n", + "for analysis_object in my_full_analysis._analysis_list:\n", + " print(analysis_object.get_fit_parameters()[0].unique_name)\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d0ceec1d", + "metadata": {}, + "outputs": [], + "source": [ + "p1=my_full_analysis._analysis_list[1].get_fit_parameters()[0]\n", + "print(p1)\n", + "print(p1.unique_name)\n", + "p2 = my_full_analysis._analysis_list[9].get_fit_parameters()[0]\n", + "print(p2)\n", + "print(p2.unique_name)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d792eee3", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "my_full_analysis.Q" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4217d56d", + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "from easydynamics.sample_model import ComponentCollection\n", + "from easydynamics.sample_model import DeltaFunction\n", + "from easydynamics.sample_model.model_base import ModelBase\n", + "%matplotlib widget\n", + "import numpy as np\n", + "Q=np.linspace(0.1,15,31)\n", + "component_collection = ComponentCollection()\n", + "delta_function = DeltaFunction(display_name='DeltaFunction', area=1)\n", + "\n", + "component_collection.append_component(delta_function)\n", + "\n", + "\n", + "# sample_model = SampleModel(\n", + "sample_model = ModelBase(\n", + " components=component_collection,\n", + " unit='meV',\n", + " display_name='MySampleModel',\n", + " Q=Q,\n", + ")\n", + "\n", + "\n", + "for Q_index in range(len(sample_model.Q)):\n", + " pars = sample_model.get_all_variables(Q_index=Q_index) \n", + " pars[0].value=pars[0].value+Q_index\n", + "\n", + "for Q_index in range(len(sample_model.Q)):\n", + " pars = sample_model.get_all_variables(Q_index=Q_index)\n", + " print(pars[0].unique_name)\n", + " print(pars[0])\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "35c89ce3", + "metadata": {}, + "outputs": [], + "source": [ + "vars2=sample_model._component_collections[1].get_all_variables()\n", + "for var in vars2:\n", + " print(var)\n", + " print(var.unique_name)\n", + "\n", + "var3=sample_model._component_collections[10].get_all_variables()\n", + "for var in var3:\n", + " print(var)\n", + " print(var.unique_name)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "02320e75", + "metadata": {}, + "outputs": [], + "source": [ + "a=vanadium_experiment.binned_data.coords['energy']\n", + "a" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5451bbf3", + "metadata": {}, + "outputs": [], + "source": [ + "import scipp as sc\n", + "x_pixel_range = [-10, -5, 0, 5, 10]\n", + "a,b=sc.array(values=x_pixel_range, dims='x')\n", + "print(a)\n", + "print(b)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "easydynamics_newbase", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/docs/tutorials/analysis.ipynb b/docs/docs/tutorials/analysis.ipynb index e1ea2973..39b70c8e 100644 --- a/docs/docs/tutorials/analysis.ipynb +++ b/docs/docs/tutorials/analysis.ipynb @@ -49,88 +49,102 @@ "metadata": {}, "outputs": [], "source": [ - "# # Create a diffusion_model and components for the SampleModel\n", - "\n", - "# # Creating components\n", - "# component_collection = ComponentCollection()\n", - "# delta_function = DeltaFunction(display_name='DeltaFunction', area=1)\n", - "# gaussian = Gaussian(display_name='Gaussian', width=0.1, center=0.5, area=0.5)\n", + "# Example of Analysis1d with a simple sample model and instrument model\n", + "delta_function = DeltaFunction(display_name='DeltaFunction', area=1)\n", + "gaussian = Gaussian(display_name='Gaussian', width=0.1, area=1)\n", + "components = ComponentCollection(components=[delta_function, gaussian])\n", + "sample_model = SampleModel(\n", + " components=components,\n", + ")\n", "\n", - "# # Adding components to the component collection\n", - "# component_collection.append_component(delta_function)\n", + "res_gauss = Gaussian(width=0.1)\n", + "res_gauss.area.fixed = True\n", + "resolution_model = ResolutionModel(components=res_gauss)\n", "\n", "\n", - "# sample_model = SampleModel(\n", - "# components=component_collection,\n", - "# unit='meV',\n", - "# display_name='MySampleModel',\n", - "# )\n", + "background_model = BackgroundModel(components=Polynomial(coefficients=[0.001]))\n", "\n", - "# res_gauss = Gaussian(width=0.1)\n", - "# res_gauss.area.fixed = True\n", - "# resolution_model = ResolutionModel(components=res_gauss)\n", + "instrument_model = InstrumentModel(\n", + " resolution_model=resolution_model,\n", + " background_model=background_model,\n", + ")\n", "\n", + "my_analysis = Analysis1d(\n", + " experiment=vanadium_experiment,\n", + " sample_model=sample_model,\n", + " instrument_model=instrument_model,\n", + " Q_index=5,\n", + ")\n", "\n", - "# background_model = BackgroundModel(components=Polynomial(coefficients=[0.001]))\n", + "fit_result = my_analysis.fit()\n", + "my_analysis.plot_data_and_model()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6762faba", + "metadata": {}, + "outputs": [], + "source": [ + "# Example of Analysis with a simple sample model and instrument model\n", + "delta_function = DeltaFunction(display_name='DeltaFunction', area=1)\n", + "gaussian = Gaussian(display_name='Gaussian', width=0.1, area=1)\n", + "components = ComponentCollection(components=[delta_function, gaussian])\n", + "sample_model = SampleModel(\n", + " components=components,\n", + ")\n", "\n", - "# instrument_model = InstrumentModel(\n", - "# resolution_model=resolution_model,\n", - "# background_model=background_model,\n", - "# )\n", + "res_gauss = Gaussian(width=0.1)\n", + "res_gauss.area.fixed = True\n", + "resolution_model = ResolutionModel(components=res_gauss)\n", "\n", - "# my_analysis = Analysis1d(\n", - "# experiment=vanadium_experiment,\n", - "# sample_model=sample_model,\n", - "# instrument_model=instrument_model,\n", - "# Q_index=5,\n", - "# )\n", "\n", + "background_model = BackgroundModel(components=Polynomial(coefficients=[0.001]))\n", "\n", - "# values = my_analysis.calculate()\n", - "# sample_values, background_values = my_analysis.calculate_individual_components()\n", + "instrument_model = InstrumentModel(\n", + " resolution_model=resolution_model,\n", + " background_model=background_model,\n", + ")\n", "\n", - "# plt.figure()\n", - "# plt.plot(my_analysis.energy.values, values, label='Total Model')\n", - "# for component_index in range(len(sample_values)):\n", - "# plt.plot(\n", - "# my_analysis.energy.values,\n", - "# sample_values[component_index],\n", - "# label=f'Sample Component {component_index}',\n", - "# linestyle='--',\n", - "# )\n", + "my_analysis = Analysis(\n", + " experiment=vanadium_experiment,\n", + " sample_model=sample_model,\n", + " instrument_model=instrument_model,\n", + ")\n", "\n", - "# for component_index in range(len(background_values)):\n", - "# plt.plot(\n", - "# my_analysis.energy.values,\n", - "# background_values[component_index],\n", - "# label=f'Background Component {component_index}',\n", - "# linestyle=':',\n", - "# )\n", - "# plt.xlabel('Energy (meV)')\n", - "# plt.ylabel('Intensity')\n", - "# plt.title(f'Q index: {5}')\n", - "# plt.legend()\n", - "# plt.show()" + "fit_result1 = my_analysis.fit(fit_method=\"independent\", Q_index=5)" ] }, { "cell_type": "code", "execution_count": null, - "id": "6762faba", + "id": "e98e3d65", "metadata": {}, "outputs": [], - "source": [] + "source": [ + "fit_result2 = my_analysis.fit(fit_method=\"independent\")" + ] }, { "cell_type": "code", "execution_count": null, - "id": "02702f95", + "id": "af13afce", "metadata": {}, "outputs": [], "source": [ - "# my_analysis.plot_data_and_model()" + "fit_result3 = my_analysis.fit(fit_method=\"simultaneous\")\n", + "fit_result3" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "02702f95", + "metadata": {}, + "outputs": [], + "source": [] + }, { "cell_type": "code", "execution_count": null, @@ -138,7 +152,7 @@ "metadata": {}, "outputs": [], "source": [ - "# my_analysis.fit()" + "my_analysis.plot_data_and_model()" ] }, { @@ -148,7 +162,18 @@ "metadata": {}, "outputs": [], "source": [ - "# my_analysis.plot_data_and_model()" + "sample_comps, background_comps = my_analysis.analysis_list[0].calculate_individual_components()\n", + "sample_comps" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "35b0fac5", + "metadata": {}, + "outputs": [], + "source": [ + "my_analysis.sample_model" ] }, { @@ -233,205 +258,6 @@ " analysis_object.plot_data_and_model()\n", " print(analysis_object.get_fit_parameters())\n" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "0a727fc3", - "metadata": {}, - "outputs": [], - "source": [ - "for analysis_object in my_full_analysis._analysis_list:\n", - " print(analysis_object.get_fit_parameters())\n", - "\n", - "for analysis_object in my_full_analysis._analysis_list:\n", - " print(analysis_object.get_fit_parameters()[0].unique_name)\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d0ceec1d", - "metadata": {}, - "outputs": [], - "source": [ - "p1=my_full_analysis._analysis_list[1].get_fit_parameters()[0]\n", - "print(p1)\n", - "print(p1.unique_name)\n", - "p2 = my_full_analysis._analysis_list[9].get_fit_parameters()[0]\n", - "print(p2)\n", - "print(p2.unique_name)\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "d792eee3", - "metadata": {}, - "outputs": [], - "source": [ - "\n", - "my_full_analysis.Q" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "4217d56d", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Parameter_2\n", - "\n", - "Parameter_4\n", - "\n", - "Parameter_6\n", - "\n", - "Parameter_8\n", - "\n", - "Parameter_10\n", - "\n", - "Parameter_12\n", - "\n", - "Parameter_14\n", - "\n", - "Parameter_16\n", - "\n", - "Parameter_18\n", - "\n", - "Parameter_20\n", - "\n", - "Parameter_4\n", - "\n", - "Parameter_6\n", - "\n", - "Parameter_8\n", - "\n", - "Parameter_10\n", - "\n", - "Parameter_12\n", - "\n", - "Parameter_14\n", - "\n", - "Parameter_16\n", - "\n", - "Parameter_18\n", - "\n", - "Parameter_20\n", - "\n", - "Parameter_22\n", - "\n", - "Parameter_4\n", - "\n", - "Parameter_6\n", - "\n", - "Parameter_8\n", - "\n", - "Parameter_10\n", - "\n", - "Parameter_12\n", - "\n", - "Parameter_14\n", - "\n", - "Parameter_16\n", - "\n", - "Parameter_18\n", - "\n", - "Parameter_20\n", - "\n", - "Parameter_4\n", - "\n", - "Parameter_6\n", - "\n" - ] - } - ], - "source": [ - "import matplotlib.pyplot as plt\n", - "\n", - "from easydynamics.sample_model import ComponentCollection\n", - "from easydynamics.sample_model import DeltaFunction\n", - "from easydynamics.sample_model.model_base import ModelBase\n", - "%matplotlib widget\n", - "import numpy as np\n", - "Q=np.linspace(0.1,15,31)\n", - "component_collection = ComponentCollection()\n", - "delta_function = DeltaFunction(display_name='DeltaFunction', area=1)\n", - "\n", - "component_collection.append_component(delta_function)\n", - "\n", - "\n", - "# sample_model = SampleModel(\n", - "sample_model = ModelBase(\n", - " components=component_collection,\n", - " unit='meV',\n", - " display_name='MySampleModel',\n", - " Q=Q,\n", - ")\n", - "\n", - "\n", - "for Q_index in range(len(sample_model.Q)):\n", - " pars = sample_model.get_all_variables(Q_index=Q_index) \n", - " pars[0].value=pars[0].value+Q_index\n", - " print(pars[0].unique_name)\n", - " print(pars[0])\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "35c89ce3", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "Parameter_5\n", - "\n", - "Parameter_4\n", - "\n", - "Parameter_5\n", - "\n", - "Parameter_4\n" - ] - } - ], - "source": [ - "vars2=sample_model._component_collections[1].get_all_variables()\n", - "for var in vars2:\n", - " print(var)\n", - " print(var.unique_name)\n", - "\n", - "var3=sample_model._component_collections[10].get_all_variables()\n", - "for var in var3:\n", - " print(var)\n", - " print(var.unique_name)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "02320e75", - "metadata": {}, - "outputs": [], - "source": [ - "var." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "5451bbf3", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/src/easydynamics/analysis/analysis.py b/src/easydynamics/analysis/analysis.py index 5a3f6073..1bf2aa5a 100644 --- a/src/easydynamics/analysis/analysis.py +++ b/src/easydynamics/analysis/analysis.py @@ -3,6 +3,8 @@ import numpy as np +import plopp as pp +import scipp as sc from easyscience.fitting.multi_fitter import MultiFitter from easyscience.variable import Parameter @@ -11,6 +13,7 @@ from easydynamics.experiment import Experiment from easydynamics.sample_model import SampleModel from easydynamics.sample_model.instrument_model import InstrumentModel +from easydynamics.utils.utils import _in_notebook class Analysis(AnalysisBase): @@ -44,9 +47,7 @@ def __init__( for Q_index in range(len(self.Q)): analysis = Analysis1d( display_name=f"{self.display_name}_Q{Q_index}", - unique_name=( - f"{self.unique_name}_Q{Q_index}" if self.unique_name else None - ), + unique_name=(f"{self.unique_name}_Q{Q_index}"), experiment=self.experiment, sample_model=self.sample_model, instrument_model=self.instrument_model, @@ -59,55 +60,156 @@ def __init__( # Properties ############# + @property + def analysis_list(self) -> list[Analysis1d]: + """List of Analysis1d objects, one for each Q index.""" + return self._analysis_list + + @analysis_list.setter + def analysis_list(self, value: list[Analysis1d]) -> None: + """analysis_list is read-only. To change the analysis list, + modify the experiment, sample model, or instrument model.""" + + raise AttributeError( + "analysis_list is read-only. " + "To change the analysis list, modify the experiment, sample model, " + "or instrument model." + ) + ############# # Other methods ############# - def calculate(self, Q_index: int | None = None) -> list[np.ndarray]: - """Calculate model data for a specific Q index.""" + def calculate(self, Q_index: int | None = None) -> list[np.ndarray] | np.ndarray: + """Calculate model data for a specific Q index. + If Q_index is None, calculate for all Q indices and return a + list of arrays. + + Parameters: Q_index: Index of the Q value to calculate for. If + None, calculate for all Q values. + + Returns: If Q_index is None, returns a list of numpy arrays, one + for each Q index. If Q_index is an integer, returns a single + numpy array for that Q index. + """ if Q_index is None: - result = [] - for analysis in self._analysis_list: - result.append(analysis.calculate()) - return result + return [analysis.calculate() for analysis in self.analysis_list] + + self._verify_Q_index(Q_index) + return self.analysis_list[Q_index].calculate() + + def fit(self, fit_method: str = "independent", Q_index: int | None = None): + """Fit the model to the experimental data. + + Parameters: fit_method: Method to use for fitting. Options are + "independent" (fit each Q index independently, one after the + other) or "simultaneous" (fit all Q indices simultaneously). + Q_index: If fit_method is "sequential", specify which Q index to + fit. If None, fit all Q indices independently. + + Returns: Fit results, which may be a list of FitResults if + fitting independently, or a single FitResults object if fitting + simultaneously. + """ + + if fit_method == "independent": + if Q_index is not None: + return self._fit_single_Q(Q_index) + else: + return self._fit_all_Q_independently() + elif fit_method == "simultaneous": + return self._fit_all_Q_simultaneously() + else: + raise ValueError( + "Invalid fit method. Choose 'independent' or 'simultaneous'." + ) - if Q_index < 0 or Q_index >= len(self._analysis_list): - raise IndexError("Q_index out of range.") + def plot_data_and_model( + self, + plot_components: bool = True, + Q_index: int | None = None, + **kwargs, + ) -> None: + """Plot the dataset using plopp.""" + + if self.experiment.binned_data is None: + raise ValueError("No data to plot. Please load data first.") + + if not _in_notebook(): + raise RuntimeError( + "plot_data() can only be used in a Jupyter notebook environment." + ) + from IPython.display import display + + plot_kwargs_defaults = { + "title": self.display_name, + "linestyle": {"Data": "none", "Model": "-"}, + "marker": {"Data": "o", "Model": None}, + "color": {"Data": "black", "Model": "red"}, + } + # Overwrite defaults with any user-provided kwargs + plot_kwargs_defaults.update(kwargs) + data_and_model = { + "Data": self.experiment.binned_data, + "Model": self._create_model_scipp_array(), + } + + if plot_components: + components_da, background_da = ( + self._create_components_and_background_scipp_arrays() + ) + + data_and_model["Background"] = background_da + plot_kwargs_defaults["linestyle"]["Background"] = "--" + plot_kwargs_defaults["marker"]["Background"] = None - return self._analysis_list[Q_index].calculate() + for icomp in range(components_da.sizes["component"]): + Q_index = 0 + comp_name = ( + self.sample_model.get_component_collection(Q_index) + .components[icomp] + .display_name + ) + data_and_model[comp_name] = components_da["component", icomp] + plot_kwargs_defaults["linestyle"][comp_name] = "--" + plot_kwargs_defaults["marker"][comp_name] = None + + fig = pp.slicer( + data_and_model, + **plot_kwargs_defaults, + ) + display(fig) ############# # Private methods ############# - def _fit_single_Q(self, Q_index: int) -> None: + def _fit_single_Q(self, Q_index: int): """Fit data for a single Q index.""" - if Q_index < 0 or Q_index >= len(self._analysis_list): - raise IndexError("Q_index out of range.") + self._verify_Q_index(Q_index) - self._analysis_list[Q_index].fit() + return self.analysis_list[Q_index].fit() - def _fit_all_Q_independently(self) -> None: + def _fit_all_Q_independently(self): """Fit data for all Q indices independently.""" + return [analysis.fit() for analysis in self.analysis_list] - for analysis in self._analysis_list: - analysis.fit() - - def _fit_all_Q_simultaneously(self) -> None: + def _fit_all_Q_simultaneously(self): """Fit data for all Q indices simultaneously.""" xs = [] ys = [] ws = [] - for analysis in self._analysis_list: + for analysis in self.analysis_list: data = analysis.experiment.data["Q", analysis.Q_index] x = data.coords["energy"].values y = data.values e = np.sqrt(data.variances) + # Make sure the convolver is up to date for this Q index analysis._convolver = analysis._create_convolver( Q_index=analysis.Q_index, energy=x, @@ -118,9 +220,8 @@ def _fit_all_Q_simultaneously(self) -> None: ws.append(1.0 / e) fit_functions = [] - - for analysis in self._analysis_list: - + for analysis in self.analysis_list: + # Use the private method to avoid excessive checks def make_fit_func(a): def fit_func(_): return a._calculate() @@ -130,7 +231,7 @@ def fit_func(_): fit_functions.append(make_fit_func(analysis)) mf = MultiFitter( - fit_objects=self._analysis_list, + fit_objects=self.analysis_list, fit_functions=fit_functions, ) @@ -141,6 +242,83 @@ def fit_func(_): ) return results + def _verify_Q_index(self, Q_index: int) -> None: + """Verify that the provided Q_index is valid.""" + if not isinstance(Q_index, int): + raise TypeError("Q_index must be an integer.") + if Q_index < 0 or Q_index >= len(self.analysis_list): + raise IndexError("Q_index out of range.") + + def _create_model_scipp_array(self) -> sc.DataArray: + """Create a scipp array for the model""" + + model = sc.array(dims=["Q", "energy"], values=self.calculate()) + model_data_array = sc.DataArray( + data=model, + coords={"Q": self.Q, "energy": self.experiment.energy}, + ) + return model_data_array + + def _create_components_and_background_scipp_arrays( + self, + ) -> tuple[sc.DataArray, sc.DataArray]: + """ + Create: + 1) A DataArray with sample components + background + dims = (component, Q, energy) + 2) A DataArray with summed background + dims = (Q, energy) + """ + + component_values = None # List[List[np.ndarray]] + background_values = [] # List[np.ndarray] + + for analysis in self.analysis_list: + sample_comps_q, background_comps_q = ( + analysis.calculate_individual_components() + ) + + # (energy,) + background_sum_q = sum(background_comps_q) + background_values.append(background_sum_q) + + if component_values is None: + component_values = [[] for _ in range(len(sample_comps_q))] + + for icomp, sample_comp_q in enumerate(sample_comps_q): + component_values[icomp].append(sample_comp_q + background_sum_q) + + # Sample components DataArray + components_array = sc.array( + dims=["component", "Q", "energy"], + values=component_values, + ) + + components_da = sc.DataArray( + data=components_array, + coords={ + "component": sc.arange("component", len(component_values)), + "Q": self.Q, + "energy": self.experiment.energy, + }, + ) + + # Background-only DataArray + background_array = sc.array( + dims=["Q", "energy"], + values=background_values, + ) + + background_da = sc.DataArray( + data=background_array, + coords={ + "Q": self.Q, + "energy": self.experiment.energy, + }, + ) + + return components_da, background_da + ############# # Dunder methods ############# diff --git a/src/easydynamics/analysis/analysis1d.py b/src/easydynamics/analysis/analysis1d.py index 57046b4b..2c00bfda 100644 --- a/src/easydynamics/analysis/analysis1d.py +++ b/src/easydynamics/analysis/analysis1d.py @@ -7,12 +7,16 @@ import numpy as np import scipp as sc from easyscience.fitting.fitter import Fitter as EasyScienceFitter +from easyscience.fitting.minimizers.utils import FitResults from easyscience.variable import DescriptorNumber from easydynamics.analysis.analysis_base import AnalysisBase +from easydynamics.convolution.convolution import Convolution from easydynamics.experiment import Experiment from easydynamics.sample_model import InstrumentModel from easydynamics.sample_model import SampleModel +from easydynamics.sample_model.component_collection import ComponentCollection +from easydynamics.sample_model.components.model_component import ModelComponent class Analysis1d(AnalysisBase): @@ -156,7 +160,7 @@ def calculate_individual_components( return sample_intensity, background_intensity - def fit(self): + def fit(self) -> FitResults: """Fit the model to the experimental data for a given Q index. Args: @@ -235,21 +239,33 @@ def plot_data_and_model( background = sum(background_comps) sample_comps = [comp + background for comp in sample_comps] for i, comp in enumerate(sample_comps): + comp_name = ( + self.sample_model.get_component_collection(Q_index) + .components[i] + .display_name + ) plt.plot( energy, comp, - label=f"Sample Component {i + 1}", + label=comp_name, linestyle="--", ) for i, comp in enumerate(background_comps): + comp_name = ( + self.instrument_model.background_model.get_component_collection( + Q_index + ) + .components[i] + .display_name + ) plt.plot( energy, comp, - label=f"Background Component {i + 1}", + label=comp_name, linestyle=":", ) plt.xlabel(f"Energy ({self.energy.unit})") - plt.ylabel(f"Intensity ({self.sample_model.unit})") + plt.ylabel("Intensity (arb. units)") plt.title(f"Data and Model at Q index {Q_index}") plt.legend() plt.show() @@ -320,3 +336,206 @@ def _on_Q_index_changed(self) -> None: the Convolution object for the new Q index. """ self._convolver = self._create_convolver(Q_index=self.Q_index) + + def _evaluate_components( + self, + components: ComponentCollection | ModelComponent, + energy: np.ndarray | sc.Variable | None = None, + convolver: Convolution | None = None, + convolve: bool = True, + Q_index: int | None = None, + ): + """ + Calculate the contribution of a set of components, optionally + convolving with the resolution. + If convolve is True and a Convolution object is provided, + use it to perform the convolution of the components with the + resolution. If convolve is True but no Convolution object is + provided, create a new Convolution object for the given + components and energy. If convolve is False, evaluate the + components directly without convolution. + Args: + components (ComponentCollection | ModelComponent): + The components to evaluate. + energy (np.ndarray | sc.Variable | None): + The energy values to evaluate the components for. If + None, the energy values from the experiment will be + used. + convolver (Convolution | None): + An optional Convolution object to use for convolution. + If None, a new Convolution object will be created if + convolve is True. + convolve (bool): + Whether to perform convolution with the resolution. + Default is True. + """ + if Q_index is None: + Q_index = self._require_Q_index() + energy = self._handle_energy(energy) + energy_offset = self.instrument_model.get_energy_offset_at_Q(Q_index).value + + # If there are no components, return zero + if isinstance(components, ComponentCollection) and components.is_empty: + return np.zeros_like(energy) + + # No convolution + if not convolve: + return components.evaluate(energy - energy_offset) + + resolution = self.instrument_model.resolution_model.get_component_collection( + Q_index + ) + if resolution.is_empty: + return components.evaluate(energy - energy_offset) + + # Convolution For fitting we don't want to create a new + # Convolution object at each iteration + if convolver is not None: + return convolver.convolution() + + # For evaluating individual components + conv = Convolution( + sample_components=components, + resolution_components=resolution, + energy=energy, + temperature=self.temperature, + energy_offset=energy_offset, + ) + return conv.convolution() + + def _evaluate_sample( + self, + energy: np.ndarray | sc.Variable | None = None, + Q_index: int | None = None, + ): + """ + Evaluate the sample contribution for a given Q index. + + If a Convolution object exists for the Q index, use it to + perform the convolution of the sample components with the + resolution components. If no Convolution object exists, evaluate + the sample components directly without convolution. + + Args: + energy (np.ndarray | sc.Variable | None): The energy values + to evaluate the sample contribution for. If None, the energy + values from the experiment will be used. + Returns: + np.ndarray: The evaluated sample contribution. + """ + if Q_index is None: + Q_index = self._require_Q_index() + components = self.sample_model.get_component_collection(Q_index=Q_index) + return self._evaluate_components( + components=components, + energy=energy, + convolver=self._convolver, + convolve=True, + ) + + def _evaluate_sample_component( + self, + component, + energy: np.ndarray | sc.Variable | None = None, + ): + """ + Evaluate a single sample component for a given Q index. + If a Convolution object exists for the Q index, use it to + perform the convolution of the sample component with the + resolution components. If no Convolution object exists, evaluate + the sample component directly without convolution. + Args: + component: The sample component to evaluate. + energy (np.ndarray | sc.Variable | None): The energy values + to evaluate the sample component for. If None, the energy + values from the experiment will be used. + Returns: + np.ndarray: The evaluated sample component contribution. + """ + return self._evaluate_components( + components=component, + energy=energy, + convolver=None, + convolve=True, + ) + + def _evaluate_background( + self, + energy: np.ndarray | sc.Variable | None = None, + Q_index: int | None = None, + ): + """ + Evaluate the background contribution for a given Q index. + Evaluate each background component separately to get individual + contributions. Args: + energy (np.ndarray | sc.Variable | None): The energy values + to evaluate the background contribution for. If None, the + energy values from the experiment will be used. + Returns: + np.ndarray: The evaluated background contribution. + """ + + if Q_index is None: + Q_index = self._require_Q_index() + background_components = ( + self.instrument_model.background_model.get_component_collection( + Q_index=Q_index + ) + ) + return self._evaluate_components( + components=background_components, + energy=energy, + convolver=None, + convolve=False, + ) + + def _evaluate_background_component( + self, + component, + energy: np.ndarray | sc.Variable | None = None, + ): + """ + Evaluate a single background component for a given Q index. + Evaluate the background component directly without convolution. + Args: + component: The background component to evaluate. + energy (np.ndarray | sc.Variable | None): The energy values + to evaluate the background component for. If None, the energy + values from the experiment will be used. + Returns: + np.ndarray: The evaluated background component contribution. + """ + + return self._evaluate_components( + components=component, + energy=energy, + convolver=None, + convolve=False, + ) + + def _create_convolver( + self, Q_index: int, energy: np.ndarray | sc.Variable | None = None + ) -> Convolution | None: + """Initialize and return a Convolution object for the given Q + index. + """ + sample_components = self.sample_model.get_component_collection(Q_index) + if sample_components.is_empty: + return None + + resolution_components = ( + self.instrument_model.resolution_model.get_component_collection(Q_index) + ) + if resolution_components.is_empty: + return None + if energy is None: + energy = self.energy + # TODO: allow convolution options to be set. + convolver = Convolution( + sample_components=sample_components, + resolution_components=resolution_components, + energy=energy, + temperature=self.temperature, + energy_offset=self.instrument_model.get_energy_offset_at_Q(Q_index), + ) + return convolver diff --git a/src/easydynamics/analysis/analysis_base.py b/src/easydynamics/analysis/analysis_base.py index 7caf89d6..3a7d0e8b 100644 --- a/src/easydynamics/analysis/analysis_base.py +++ b/src/easydynamics/analysis/analysis_base.py @@ -2,17 +2,13 @@ # SPDX-License-Identifier: BSD-3-Clause -import numpy as np import scipp as sc from easyscience.base_classes.model_base import ModelBase as EasyScienceModelBase from easyscience.variable import Parameter -from easydynamics.convolution import Convolution from easydynamics.experiment import Experiment from easydynamics.sample_model import InstrumentModel from easydynamics.sample_model import SampleModel -from easydynamics.sample_model.component_collection import ComponentCollection -from easydynamics.sample_model.components.model_component import ModelComponent class AnalysisBase(EasyScienceModelBase): @@ -173,209 +169,6 @@ def _on_sample_model_changed(self) -> None: def _on_instrument_model_changed(self) -> None: self._instrument_model.Q = self.Q - def _create_convolver( - self, Q_index: int, energy: np.ndarray | sc.Variable | None = None - ) -> Convolution | None: - """Initialize and return a Convolution object for the given Q - index. - """ - sample_components = self.sample_model.get_component_collection(Q_index) - if sample_components.is_empty: - return None - - resolution_components = ( - self.instrument_model.resolution_model.get_component_collection(Q_index) - ) - if resolution_components.is_empty: - return None - if energy is None: - energy = self.energy - # TODO: allow convolution options to be set. - convolver = Convolution( - sample_components=sample_components, - resolution_components=resolution_components, - energy=energy, - temperature=self.temperature, - energy_offset=self.instrument_model.get_energy_offset_at_Q(Q_index), - ) - return convolver - - def _evaluate_components( - self, - components: ComponentCollection | ModelComponent, - energy: np.ndarray | sc.Variable | None = None, - convolver: Convolution | None = None, - convolve: bool = True, - Q_index: int | None = None, - ): - """ - Calculate the contribution of a set of components, optionally - convolving with the resolution. - If convolve is True and a Convolution object is provided, - use it to perform the convolution of the components with the - resolution. If convolve is True but no Convolution object is - provided, create a new Convolution object for the given - components and energy. If convolve is False, evaluate the - components directly without convolution. - Args: - components (ComponentCollection | ModelComponent): - The components to evaluate. - energy (np.ndarray | sc.Variable | None): - The energy values to evaluate the components for. If - None, the energy values from the experiment will be - used. - convolver (Convolution | None): - An optional Convolution object to use for convolution. - If None, a new Convolution object will be created if - convolve is True. - convolve (bool): - Whether to perform convolution with the resolution. - Default is True. - """ - if Q_index is None: - Q_index = self._require_Q_index() - energy = self._handle_energy(energy) - energy_offset = self.instrument_model.get_energy_offset_at_Q(Q_index).value - - # If there are no components, return zero - if isinstance(components, ComponentCollection) and components.is_empty: - return np.zeros_like(energy) - - # No convolution - if not convolve: - return components.evaluate(energy - energy_offset) - - resolution = self.instrument_model.resolution_model.get_component_collection( - Q_index - ) - if resolution.is_empty: - return components.evaluate(energy - energy_offset) - - # Convolution For fitting we don't want to create a new - # Convolution object at each iteration - if convolver is not None: - return convolver.convolution() - - # For evaluating individual components - conv = Convolution( - sample_components=components, - resolution_components=resolution, - energy=energy, - temperature=self.temperature, - energy_offset=energy_offset, - ) - return conv.convolution() - - def _evaluate_sample( - self, - energy: np.ndarray | sc.Variable | None = None, - Q_index: int | None = None, - ): - """ - Evaluate the sample contribution for a given Q index. - - If a Convolution object exists for the Q index, use it to - perform the convolution of the sample components with the - resolution components. If no Convolution object exists, evaluate - the sample components directly without convolution. - - Args: - energy (np.ndarray | sc.Variable | None): The energy values - to evaluate the sample contribution for. If None, the energy - values from the experiment will be used. - Returns: - np.ndarray: The evaluated sample contribution. - """ - if Q_index is None: - Q_index = self._require_Q_index() - components = self.sample_model.get_component_collection(Q_index=Q_index) - return self._evaluate_components( - components=components, - energy=energy, - convolver=self._convolver, - convolve=True, - ) - - def _evaluate_sample_component( - self, - component, - energy: np.ndarray | sc.Variable | None = None, - ): - """ - Evaluate a single sample component for a given Q index. - If a Convolution object exists for the Q index, use it to - perform the convolution of the sample component with the - resolution components. If no Convolution object exists, evaluate - the sample component directly without convolution. - Args: - component: The sample component to evaluate. - energy (np.ndarray | sc.Variable | None): The energy values - to evaluate the sample component for. If None, the energy - values from the experiment will be used. - Returns: - np.ndarray: The evaluated sample component contribution. - """ - return self._evaluate_components( - components=component, - energy=energy, - convolver=None, - convolve=True, - ) - - def _evaluate_background( - self, - energy: np.ndarray | sc.Variable | None = None, - Q_index: int | None = None, - ): - """ - Evaluate the background contribution for a given Q index. - Evaluate each background component separately to get individual - contributions. Args: - energy (np.ndarray | sc.Variable | None): The energy values - to evaluate the background contribution for. If None, the - energy values from the experiment will be used. - Returns: - np.ndarray: The evaluated background contribution. - """ - - if Q_index is None: - Q_index = self._require_Q_index() - background_components = ( - self.instrument_model.background_model.get_component_collection( - Q_index=Q_index - ) - ) - return self._evaluate_components( - components=background_components, - energy=energy, - convolver=None, - convolve=False, - ) - - def _evaluate_background_component( - self, - component, - energy: np.ndarray | sc.Variable | None = None, - ): - """ - Evaluate a single background component for a given Q index. - Evaluate the background component directly without convolution. - Args: - component: The background component to evaluate. - energy (np.ndarray | sc.Variable | None): The energy values - to evaluate the background component for. If None, the energy - values from the experiment will be used. - Returns: - np.ndarray: The evaluated background component contribution. - """ - - return self._evaluate_components( - components=component, - energy=energy, - convolver=None, - convolve=False, - ) - ############# # Dunder methods ############# diff --git a/src/easydynamics/experiment/experiment.py b/src/easydynamics/experiment/experiment.py index b3df2a11..c0fd4b5e 100644 --- a/src/easydynamics/experiment/experiment.py +++ b/src/easydynamics/experiment/experiment.py @@ -8,6 +8,8 @@ from scipp.io import load_hdf5 as sc_load_hdf5 from scipp.io import save_hdf5 as sc_save_hdf5 +from easydynamics.utils.utils import _in_notebook + class Experiment(NewBase): """Holds data from an experiment as a sc.DataArray along with @@ -19,7 +21,7 @@ class Experiment(NewBase): def __init__( self, - display_name: str = 'MyExperiment', + display_name: str = "MyExperiment", unique_name: str | None = None, data: sc.DataArray | str | None = None, ): @@ -37,7 +39,7 @@ def __init__( self._data = data else: raise TypeError( - f'Data must be a sc.DataArray or a filename string, not {type(data).__name__}' + f"Data must be a sc.DataArray or a filename string, not {type(data).__name__}" ) self._binned_data = ( @@ -57,7 +59,7 @@ def data(self) -> sc.DataArray | None: def data(self, value: sc.DataArray): """Set the dataset associated with this experiment.""" if not isinstance(value, sc.DataArray): - raise TypeError(f'Data must be a sc.DataArray, not {type(value).__name__}') + raise TypeError(f"Data must be a sc.DataArray, not {type(value).__name__}") self._validate_coordinates(value) self._data = value self._binned_data = ( @@ -72,33 +74,35 @@ def binned_data(self) -> sc.DataArray | None: @binned_data.setter def binned_data(self, value: sc.DataArray): """Set the binned dataset associated with this experiment.""" - raise AttributeError('binned_data is a read-only property. Use rebin() to rebin the data') + raise AttributeError( + "binned_data is a read-only property. Use rebin() to rebin the data" + ) @property def Q(self) -> sc.Variable | None: """Get the Q values from the dataset.""" if self._data is None: - warnings.warn('No data loaded.', UserWarning) + warnings.warn("No data loaded.", UserWarning) return None - return self._binned_data.coords['Q'] + return self._binned_data.coords["Q"] @Q.setter def Q(self, value: sc.Variable): """Set the Q values for the dataset.""" - raise AttributeError('Q is a read-only property derived from the data.') + raise AttributeError("Q is a read-only property derived from the data.") @property def energy(self) -> sc.Variable: """Get the energy values from the dataset.""" if self._data is None: - warnings.warn('No data loaded.', UserWarning) + warnings.warn("No data loaded.", UserWarning) return None - return self._binned_data.coords['energy'] + return self._binned_data.coords["energy"] @energy.setter def energy(self, value: sc.Variable): """Set the energy values for the dataset.""" - raise AttributeError('energy is a read-only property derived from the data.') + raise AttributeError("energy is a read-only property derived from the data.") ########### # Handle data @@ -113,19 +117,19 @@ def load_hdf5(self, filename: str, display_name: str | None = None): experiment. """ if not isinstance(filename, str): - raise TypeError(f'Filename must be a string, not {type(filename).__name__}') + raise TypeError(f"Filename must be a string, not {type(filename).__name__}") if display_name is not None: if not isinstance(display_name, str): raise TypeError( - f'Display name must be a string, not {type(display_name).__name__}' + f"Display name must be a string, not {type(display_name).__name__}" ) self.display_name = display_name loaded_data = sc_load_hdf5(filename) if not isinstance(loaded_data, sc.DataArray): raise TypeError( - f'Loaded data must be a sc.DataArray, not {type(loaded_data).__name__}' + f"Loaded data must be a sc.DataArray, not {type(loaded_data).__name__}" ) self._validate_coordinates(loaded_data) self.data = loaded_data @@ -138,13 +142,13 @@ def save_hdf5(self, filename: str | None = None): """ if filename is None: - filename = f'{self.unique_name}.h5' + filename = f"{self.unique_name}.h5" if not isinstance(filename, str): - raise TypeError(f'Filename must be a string, not {type(filename).__name__}') + raise TypeError(f"Filename must be a string, not {type(filename).__name__}") if self._data is None: - raise ValueError('No data to save.') + raise ValueError("No data to save.") dir_name = os.path.dirname(filename) if dir_name: @@ -172,31 +176,33 @@ def rebin(self, dimensions: dict[str, int | sc.Variable]) -> None: if not isinstance(dimensions, dict): raise TypeError( - 'dimensions must be a dictionary mapping dimension names ' - 'to number of bins or bin values as sc.Variable.' + "dimensions must be a dictionary mapping dimension names " + "to number of bins or bin values as sc.Variable." ) if self._data is None: - raise ValueError('No data to rebin. Please load data first.') + raise ValueError("No data to rebin. Please load data first.") binned_data = self._data.copy() dim_copy = dimensions.copy() for dim, value in dim_copy.items(): if not isinstance(dim, str): raise TypeError( - f'Dimension keys must be strings. Got {type(dim)} for {dim} instead.' + f"Dimension keys must be strings. Got {type(dim)} for {dim} instead." ) if dim not in self._data.dims: raise KeyError( f"Dimension '{dim}' not a valid dimension for rebinning. " - f'Should be one of {self._data.dims}.' + f"Should be one of {self._data.dims}." ) - if isinstance(value, float) and value.is_integer(): # I allow eg. 2.0 as well as 2 + if ( + isinstance(value, float) and value.is_integer() + ): # I allow eg. 2.0 as well as 2 value = int(value) # This line can be removed when scipp resize support # resizing with coordinates dimensions[dim] = value if not (isinstance(value, int) or isinstance(value, sc.Variable)): raise TypeError( - f'Dimension values must be integers or sc.Variable. ' + f"Dimension values must be integers or sc.Variable. " f"Got {type(value)} for dimension '{dim}' instead." ) binned_data = binned_data.bin({dim: value}) @@ -213,15 +219,17 @@ def plot_data(self, slicer=False, **kwargs) -> None: """Plot the dataset using plopp.""" if self._binned_data is None: - raise ValueError('No data to plot. Please load data first.') + raise ValueError("No data to plot. Please load data first.") - if not self._in_notebook(): - raise RuntimeError('plot_data() can only be used in a Jupyter notebook environment.') + if not _in_notebook(): + raise RuntimeError( + "plot_data() can only be used in a Jupyter notebook environment." + ) from IPython.display import display plot_kwargs_defaults = { - 'title': self.display_name, + "title": self.display_name, } # Overwrite defaults with any user-provided kwargs plot_kwargs_defaults.update(kwargs) @@ -232,7 +240,7 @@ def plot_data(self, slicer=False, **kwargs) -> None: ) else: fig = pp.plot( - self._binned_data.transpose(dims=['energy', 'Q']), + self._binned_data.transpose(dims=["energy", "Q"]), **plot_kwargs_defaults, ) display(fig) @@ -241,26 +249,6 @@ def plot_data(self, slicer=False, **kwargs) -> None: # private methods ########### - @staticmethod - def _in_notebook() -> bool: - """Check if the code is running in a Jupyter notebook. - - Returns: - bool: True if in a Jupyter notebook, False otherwise. - """ - try: - from IPython import get_ipython - - shell = get_ipython().__class__.__name__ - if shell == 'ZMQInteractiveShell': - return True # Jupyter notebook or JupyterLab - elif shell == 'TerminalInteractiveShell': - return False # Terminal IPython - else: - return False - except (NameError, ImportError): - return False # Standard Python (no IPython) - @staticmethod def _validate_coordinates(data: sc.DataArray) -> None: """Validate that required coordinates are present in the data. @@ -269,9 +257,9 @@ def _validate_coordinates(data: sc.DataArray) -> None: ValueError: If required coordinates are missing. """ if not isinstance(data, sc.DataArray): - raise TypeError('Data must be a sc.DataArray.') + raise TypeError("Data must be a sc.DataArray.") - required_coords = ['Q', 'energy'] + required_coords = ["Q", "energy"] for coord in required_coords: if coord not in data.coords: raise ValueError(f"Data is missing required coordinate: '{coord}'") @@ -297,11 +285,11 @@ def _convert_to_bin_centers(self, data: sc.DataArray) -> sc.DataArray: ########### def __repr__(self) -> str: - return f'Experiment `{self.unique_name}` with data: {self._data}' + return f"Experiment `{self.unique_name}` with data: {self._data}" - def __copy__(self) -> 'Experiment': + def __copy__(self) -> "Experiment": """Return a copy of the object.""" - temp = self.to_dict(skip=['unique_name']) + temp = self.to_dict(skip=["unique_name"]) new_obj = self.__class__.from_dict(temp) new_obj.data = self.data.copy() if self.data is not None else None return new_obj diff --git a/src/easydynamics/sample_model/model_base.py b/src/easydynamics/sample_model/model_base.py index 85b74d9f..bb5859cc 100644 --- a/src/easydynamics/sample_model/model_base.py +++ b/src/easydynamics/sample_model/model_base.py @@ -282,13 +282,10 @@ def _generate_component_collections(self) -> None: self._component_collections = [] return - self._component_collections = [ComponentCollection() for _ in self._Q] - - # Add copies of components from self._components to each - # component collection - for collection in self._component_collections: - for component in self._components.components: - collection.append_component(copy(component)) + # Will fix it for my code I think + self._component_collections = [] + for _ in self._Q: + self._component_collections.append(copy(self._components)) def _on_Q_change(self) -> None: """Handle changes to the Q values.""" diff --git a/src/easydynamics/utils/utils.py b/src/easydynamics/utils/utils.py index 576b451d..bcd44c14 100644 --- a/src/easydynamics/utils/utils.py +++ b/src/easydynamics/utils/utils.py @@ -26,7 +26,7 @@ def _validate_and_convert_Q(Q: Q_type | None) -> np.ndarray | None: if Q is None: return None if not isinstance(Q, (Numeric, list, np.ndarray, sc.Variable)): - raise TypeError('Q must be a number, list, numpy array, or scipp array.') + raise TypeError("Q must be a number, list, numpy array, or scipp array.") if isinstance(Q, Numeric): Q = np.array([Q]) @@ -34,14 +34,14 @@ def _validate_and_convert_Q(Q: Q_type | None) -> np.ndarray | None: Q = np.array(Q) if isinstance(Q, np.ndarray): if Q.ndim > 1: - raise ValueError('Q must be a 1-dimensional array.') + raise ValueError("Q must be a 1-dimensional array.") - Q = sc.array(dims=['Q'], values=Q, unit='1/angstrom') + Q = sc.array(dims=["Q"], values=Q, unit="1/angstrom") if isinstance(Q, sc.Variable): - if Q.dims != ('Q',): + if Q.dims != ("Q",): raise ValueError("Q must have a single dimension named 'Q'.") - Q = Q.to(unit='1/angstrom') + Q = Q.to(unit="1/angstrom") return Q.values @@ -64,7 +64,29 @@ def _validate_unit(unit: str | sc.Unit | None) -> sc.Unit | None: """ if unit is not None and not isinstance(unit, (str, sc.Unit)): - raise TypeError(f'unit must be None, a string, or a scipp Unit, got {type(unit).__name__}') + raise TypeError( + f"unit must be None, a string, or a scipp Unit, got {type(unit).__name__}" + ) if isinstance(unit, str): unit = sc.Unit(unit) return unit + + +def _in_notebook() -> bool: + """Check if the code is running in a Jupyter notebook. + + Returns: + bool: True if in a Jupyter notebook, False otherwise. + """ + try: + from IPython import get_ipython + + shell = get_ipython().__class__.__name__ + if shell == "ZMQInteractiveShell": + return True # Jupyter notebook or JupyterLab + elif shell == "TerminalInteractiveShell": + return False # Terminal IPython + else: + return False + except (NameError, ImportError): + return False # Standard Python (no IPython) From 4980176e50f704ea235c04a1fcd042d372891a92 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Wed, 11 Feb 2026 20:08:17 +0100 Subject: [PATCH 12/17] analysis MWP --- src/easydynamics/analysis/analysis.py | 211 ++++----- src/easydynamics/analysis/analysis1d.py | 463 +++++++++----------- src/easydynamics/analysis/analysis_base.py | 40 +- src/easydynamics/experiment/experiment.py | 8 +- src/easydynamics/sample_model/model_base.py | 19 +- 5 files changed, 353 insertions(+), 388 deletions(-) diff --git a/src/easydynamics/analysis/analysis.py b/src/easydynamics/analysis/analysis.py index 1bf2aa5a..ae7b16a7 100644 --- a/src/easydynamics/analysis/analysis.py +++ b/src/easydynamics/analysis/analysis.py @@ -5,6 +5,7 @@ import numpy as np import plopp as pp import scipp as sc +from easyscience.fitting.minimizers.utils import FitResults from easyscience.fitting.multi_fitter import MultiFitter from easyscience.variable import Parameter @@ -26,9 +27,7 @@ def __init__( experiment: Experiment | None = None, sample_model: SampleModel | None = None, instrument_model: InstrumentModel | None = None, - extra_parameters: ( - Parameter | list[Parameter] | list[list[Parameter]] | None - ) = None, + extra_parameters: Parameter | list[Parameter] | None = None, ): super().__init__( @@ -37,6 +36,7 @@ def __init__( experiment=experiment, sample_model=sample_model, instrument_model=instrument_model, + extra_parameters=extra_parameters, ) if experiment is not None and not isinstance(experiment, Experiment): @@ -51,7 +51,7 @@ def __init__( experiment=self.experiment, sample_model=self.sample_model, instrument_model=self.instrument_model, - extra_parameters=extra_parameters, + extra_parameters=self._extra_parameters, Q_index=Q_index, ) self._analysis_list.append(analysis) @@ -79,7 +79,10 @@ def analysis_list(self, value: list[Analysis1d]) -> None: ############# # Other methods ############# - def calculate(self, Q_index: int | None = None) -> list[np.ndarray] | np.ndarray: + def calculate( + self, + Q_index: int | None = None, + ) -> list[np.ndarray] | np.ndarray: """Calculate model data for a specific Q index. If Q_index is None, calculate for all Q indices and return a list of arrays. @@ -95,23 +98,38 @@ def calculate(self, Q_index: int | None = None) -> list[np.ndarray] | np.ndarray if Q_index is None: return [analysis.calculate() for analysis in self.analysis_list] - self._verify_Q_index(Q_index) + Q_index = self._verify_Q_index(Q_index) return self.analysis_list[Q_index].calculate() - def fit(self, fit_method: str = "independent", Q_index: int | None = None): + def fit( + self, + fit_method: str = "independent", + Q_index: int | None = None, + ) -> FitResults | list[FitResults]: """Fit the model to the experimental data. - Parameters: fit_method: Method to use for fitting. Options are - "independent" (fit each Q index independently, one after the - other) or "simultaneous" (fit all Q indices simultaneously). - Q_index: If fit_method is "sequential", specify which Q index to - fit. If None, fit all Q indices independently. + Parameters: + --------------- + fit_method: string, optional + Method to use for fitting. Options are "independent" (fit + each Q index independently, one after the other) or + "simultaneous" (fit all Q indices simultaneously). + Q_index: int or None, optional + If fit_method is "independent", specify which Q index to + fit. If None, fit all Q indices independently. Returns: Fit results, which may be a list of FitResults if - fitting independently, or a single FitResults object if fitting - simultaneously. + fitting independently, or a single FitResults object if + fitting simultaneously. """ + if self.Q is None: + raise ValueError( + "No Q values available for fitting. Please check the experiment data." + ) + + Q_index = self._verify_Q_index(Q_index) + if fit_method == "independent": if Q_index is not None: return self._fit_single_Q(Q_index) @@ -126,11 +144,20 @@ def fit(self, fit_method: str = "independent", Q_index: int | None = None): def plot_data_and_model( self, - plot_components: bool = True, Q_index: int | None = None, + plot_components: bool = True, + add_background: bool = True, **kwargs, ) -> None: - """Plot the dataset using plopp.""" + """Plot the data and model using plopp.""" + + if Q_index is not None: + Q_index = self._verify_Q_index(Q_index) + return self.analysis_list[Q_index].plot_data_and_model( + plot_components=plot_components, + add_background=add_background, + **kwargs, + ) if self.experiment.binned_data is None: raise ValueError("No data to plot. Please load data first.") @@ -139,6 +166,18 @@ def plot_data_and_model( raise RuntimeError( "plot_data() can only be used in a Jupyter notebook environment." ) + + if self.Q is None: + raise ValueError( + "No Q values available for plotting. Please check the experiment data." + ) + + if not isinstance(plot_components, bool): + raise TypeError("plot_components must be True or False.") + + if not isinstance(add_background, bool): + raise TypeError("add_background must be True or False.") + from IPython.display import display plot_kwargs_defaults = { @@ -147,32 +186,20 @@ def plot_data_and_model( "marker": {"Data": "o", "Model": None}, "color": {"Data": "black", "Model": "red"}, } - # Overwrite defaults with any user-provided kwargs - plot_kwargs_defaults.update(kwargs) data_and_model = { "Data": self.experiment.binned_data, - "Model": self._create_model_scipp_array(), + "Model": self._create_model_array(), } if plot_components: - components_da, background_da = ( - self._create_components_and_background_scipp_arrays() - ) - - data_and_model["Background"] = background_da - plot_kwargs_defaults["linestyle"]["Background"] = "--" - plot_kwargs_defaults["marker"]["Background"] = None + components = self._create_components_dataset(add_background=add_background) + for key in components.keys(): + data_and_model[key] = components[key] + plot_kwargs_defaults["linestyle"][key] = "--" + plot_kwargs_defaults["marker"][key] = None - for icomp in range(components_da.sizes["component"]): - Q_index = 0 - comp_name = ( - self.sample_model.get_component_collection(Q_index) - .components[icomp] - .display_name - ) - data_and_model[comp_name] = components_da["component", icomp] - plot_kwargs_defaults["linestyle"][comp_name] = "--" - plot_kwargs_defaults["marker"][comp_name] = None + # Overwrite defaults with any user-provided kwargs + plot_kwargs_defaults.update(kwargs) fig = pp.slicer( data_and_model, @@ -184,18 +211,18 @@ def plot_data_and_model( # Private methods ############# - def _fit_single_Q(self, Q_index: int): + def _fit_single_Q(self, Q_index: int) -> FitResults: """Fit data for a single Q index.""" - self._verify_Q_index(Q_index) + Q_index = self._verify_Q_index(Q_index) return self.analysis_list[Q_index].fit() - def _fit_all_Q_independently(self): + def _fit_all_Q_independently(self) -> list[FitResults]: """Fit data for all Q indices independently.""" return [analysis.fit() for analysis in self.analysis_list] - def _fit_all_Q_simultaneously(self): + def _fit_all_Q_simultaneously(self) -> FitResults: """Fit data for all Q indices simultaneously.""" xs = [] @@ -210,29 +237,15 @@ def _fit_all_Q_simultaneously(self): e = np.sqrt(data.variances) # Make sure the convolver is up to date for this Q index - analysis._convolver = analysis._create_convolver( - Q_index=analysis.Q_index, - energy=x, - ) + analysis._convolver = analysis._create_convolver() xs.append(x) ys.append(y) ws.append(1.0 / e) - fit_functions = [] - for analysis in self.analysis_list: - # Use the private method to avoid excessive checks - def make_fit_func(a): - def fit_func(_): - return a._calculate() - - return fit_func - - fit_functions.append(make_fit_func(analysis)) - mf = MultiFitter( fit_objects=self.analysis_list, - fit_functions=fit_functions, + fit_functions=self.get_fit_functions(), ) results = mf.fit( @@ -242,14 +255,14 @@ def fit_func(_): ) return results - def _verify_Q_index(self, Q_index: int) -> None: - """Verify that the provided Q_index is valid.""" - if not isinstance(Q_index, int): - raise TypeError("Q_index must be an integer.") - if Q_index < 0 or Q_index >= len(self.analysis_list): - raise IndexError("Q_index out of range.") + def get_fit_functions(self) -> list[callable]: + """ + Get fit functions for all Q indices, which can be used for + simultaneous fitting. + """ + return [analysis.as_fit_function() for analysis in self.analysis_list] - def _create_model_scipp_array(self) -> sc.DataArray: + def _create_model_array(self) -> sc.DataArray: """Create a scipp array for the model""" model = sc.array(dims=["Q", "energy"], values=self.calculate()) @@ -259,65 +272,29 @@ def _create_model_scipp_array(self) -> sc.DataArray: ) return model_data_array - def _create_components_and_background_scipp_arrays( - self, - ) -> tuple[sc.DataArray, sc.DataArray]: - """ - Create: - 1) A DataArray with sample components + background - dims = (component, Q, energy) - 2) A DataArray with summed background - dims = (Q, energy) + def _create_components_dataset(self, add_background: bool = True) -> sc.Dataset: """ + Create a scipp dataset containing the individual components of + the model for plotting. - component_values = None # List[List[np.ndarray]] - background_values = [] # List[np.ndarray] + Parameters: + --------------- + add_background: bool, optional + Whether to add background components to the sample model + components. Default is True. - for analysis in self.analysis_list: - sample_comps_q, background_comps_q = ( - analysis.calculate_individual_components() - ) - - # (energy,) - background_sum_q = sum(background_comps_q) - background_values.append(background_sum_q) - - if component_values is None: - component_values = [[] for _ in range(len(sample_comps_q))] - - for icomp, sample_comp_q in enumerate(sample_comps_q): - component_values[icomp].append(sample_comp_q + background_sum_q) - - # Sample components DataArray - components_array = sc.array( - dims=["component", "Q", "energy"], - values=component_values, - ) - - components_da = sc.DataArray( - data=components_array, - coords={ - "component": sc.arange("component", len(component_values)), - "Q": self.Q, - "energy": self.experiment.energy, - }, - ) - - # Background-only DataArray - background_array = sc.array( - dims=["Q", "energy"], - values=background_values, - ) + Returns: A scipp Dataset where each variable is a component of + the model, with dimensions "Q" and "energy". + """ + if not isinstance(add_background, bool): + raise TypeError("add_background must be True or False.") - background_da = sc.DataArray( - data=background_array, - coords={ - "Q": self.Q, - "energy": self.experiment.energy, - }, - ) + datasets = [ + analysis._create_components_dataset_single_Q(add_background=add_background) + for analysis in self.analysis_list + ] - return components_da, background_da + return sc.concat(datasets, dim="Q") ############# # Dunder methods diff --git a/src/easydynamics/analysis/analysis1d.py b/src/easydynamics/analysis/analysis1d.py index 2c00bfda..4e652aac 100644 --- a/src/easydynamics/analysis/analysis1d.py +++ b/src/easydynamics/analysis/analysis1d.py @@ -40,18 +40,11 @@ def __init__( instrument_model=instrument_model, ) - if Q_index is not None: - if ( - not isinstance(Q_index, int) - or Q_index < 0 - or (self.Q is not None and Q_index >= len(self.Q)) - ): - raise ValueError("Q_index must be a valid index for the Q values.") - self._Q_index = Q_index + self._Q_index = self._verify_Q_index(Q_index) self._fit_result = None - self._convolver = self._create_convolver(Q_index=self.Q_index) + self._convolver = self._create_convolver() ############# # Properties @@ -63,36 +56,28 @@ def Q_index(self) -> int | None: return self._Q_index @Q_index.setter - def Q_index(self, index: int | None) -> None: + def Q_index(self, value: int | None) -> None: """Set the Q index for single Q analysis. Args: index (int | None): The Q index. """ - if index is not None: - if ( - not isinstance(index, int) - or index < 0 - or (self.Q is not None and index >= len(self.Q)) - ): - raise ValueError("Q_index must be a valid index for the Q values.") - self._Q_index = index + self._Q_index = self._verify_Q_index(value) self._on_Q_index_changed() ############# # Other methods ############# - def calculate(self, energy: np.ndarray | sc.Variable | None = None) -> np.ndarray: + def calculate(self) -> np.ndarray: """Calculate the model prediction for a given Q index. + Makes sure the convolver is up to date before calculating. - Args: - energy (float): The energy value to calculate the model for. Returns: - sc.DataArray: The calculated model prediction. + np.ndarray: The calculated model prediction. """ - self._convolver = self._create_convolver(Q_index=self.Q_index, energy=energy) + self._convolver = self._create_convolver() return self._calculate() @@ -102,7 +87,7 @@ def _calculate(self) -> np.ndarray: Args: energy (float): The energy value to calculate the model for. Returns: - sc.DataArray: The calculated model prediction. + np.ndarray: The calculated model prediction. """ sample_intensity = self._evaluate_sample() @@ -113,57 +98,9 @@ def _calculate(self) -> np.ndarray: return sample_plus_background - def calculate_individual_components( - self, - energy: np.ndarray | sc.Variable | None = None, - ) -> np.ndarray: - """Calculate the model prediction for a given Q index. - - Args: - energy (float): The energy value to calculate the model for. - Returns: - sc.DataArray: The calculated model prediction. - """ - Q_index = self._require_Q_index() - - energy = self._handle_energy(energy) - - sample_components = self.sample_model.get_component_collection(Q_index) - - if sample_components.is_empty: - sample_intensity = [np.zeros_like(energy)] - else: - sample_intensity = [] - for component in sample_components.components: - component_intensity = self._evaluate_sample_component( - component=component, - energy=energy, - ) - sample_intensity.append(component_intensity) - - # Background. Evaluate each background component separately to - # get individual contributions. - background_components = ( - self.instrument_model.background_model.get_component_collection(Q_index) - ) - - if background_components.is_empty: - background_intensity = [np.zeros_like(energy)] - else: - background_intensity = [] - for component in background_components.components: - component_intensity = self._evaluate_background_component( - component=component, - energy=energy, - ) - background_intensity.append(component_intensity) - - return sample_intensity, background_intensity - def fit(self) -> FitResults: """Fit the model to the experimental data for a given Q index. - Args: Returns: FitResult: The result of the fit. @@ -183,92 +120,34 @@ def fit(self) -> FitResults: y = data.values e = data.variances**0.5 - self._convolver = self._create_convolver(Q_index=self.Q_index, energy=x) - - def fit_func(_): - return self._calculate() + # Create convolver once to reuse during fitting + self._convolver = self._create_convolver() fitter = EasyScienceFitter( fit_object=self, - fit_function=fit_func, + fit_function=self.as_fit_function(), ) - # Perform the fit fit_result = fitter.fit(x=x, y=y, weights=1.0 / e) - # Store result self._fit_result = fit_result return fit_result - def plot_data_and_model( - self, - plot_individual_components: bool = True, - add_background: bool = True, - ) -> None: - """Plot the experimental data and the model prediction. + def as_fit_function(self, x=None, **kwargs): + """ + Return self._calculate as a fit function. - Args: - plot_individual_components (bool): Whether to plot - individual components. Default is True. + The EasyScience fitter requires x as input, but + self._calculate() already uses the correct energy from the + experiment. So we ignore the x input and just return the + calculated model. """ - if not isinstance(plot_individual_components, bool): - raise TypeError("plot_individual_components must be True or False.") - import matplotlib.pyplot as plt + def fit_function(x, **kwargs): + return self._calculate() - Q_index = self._require_Q_index() - if self.experiment is None or self.experiment.data is None: - raise ValueError("Experiment data is not available for plotting.") - data = self.experiment.data["Q", Q_index] - energy = data.coords["energy"].values - model = self.calculate(energy=energy) - plt.figure() - plt.errorbar( - energy, - data.values, - yerr=data.variances**0.5, - fmt="o", - label="Data", - color="black", - ) - plt.plot(energy, model, label="Model", color="red") - if plot_individual_components: - sample_comps, background_comps = self.calculate_individual_components() - if add_background: - background = sum(background_comps) - sample_comps = [comp + background for comp in sample_comps] - for i, comp in enumerate(sample_comps): - comp_name = ( - self.sample_model.get_component_collection(Q_index) - .components[i] - .display_name - ) - plt.plot( - energy, - comp, - label=comp_name, - linestyle="--", - ) - for i, comp in enumerate(background_comps): - comp_name = ( - self.instrument_model.background_model.get_component_collection( - Q_index - ) - .components[i] - .display_name - ) - plt.plot( - energy, - comp, - label=comp_name, - linestyle=":", - ) - plt.xlabel(f"Energy ({self.energy.unit})") - plt.ylabel("Intensity (arb. units)") - plt.title(f"Data and Model at Q index {Q_index}") - plt.legend() - plt.show() + return fit_function def get_all_variables(self) -> list[DescriptorNumber]: """Get all variables used in the analysis. @@ -285,13 +164,76 @@ def get_all_variables(self) -> list[DescriptorNumber]: return variables + def plot_data_and_model( + self, + plot_components: bool = True, + add_background=True, + **kwargs, + ): + """Plot the experimental data and the model prediction for a + given Q index. + + Uses Plopp for plotting. + + Args: + add_background (bool): Whether to add the background to the + model prediction when plotting individual components. + + kwargs: Keyword arguments to pass to the plotting + function. + Returns: + A plot of the data and model. + """ + import plopp as pp + + if self.experiment.data is None: + raise ValueError("No data to plot. Please load data first.") + + data = self.experiment.data["Q", self.Q_index] + model_array = self._create_sample_scipp_array() + + component_dataset = self._create_components_dataset_single_Q( + add_background=add_background + ) + + # Create a dataset containing the data, model, and individual + # components for plotting. + data_and_model = sc.Dataset( + { + "Data": data, + "Model": model_array, + } + ) + + data_and_model = sc.merge(data_and_model, component_dataset) + plot_kwargs_defaults = { + "title": self.display_name, + "linestyle": {"Data": "none", "Model": "-"}, + "marker": {"Data": "o", "Model": None}, + "color": {"Data": "black", "Model": "red"}, + } + + if plot_components: + for comp_name in component_dataset.keys(): + plot_kwargs_defaults["linestyle"][comp_name] = "--" + plot_kwargs_defaults["marker"][comp_name] = None + + # Overwrite defaults with any user-provided kwargs + plot_kwargs_defaults.update(kwargs) + + fig = pp.plot( + data_and_model, + **plot_kwargs_defaults, + ) + return fig + ############# - # Private methods + # Private methods: small utilities ############# def _require_Q_index(self) -> int: """ - Get the Q index for single Q analysis, ensuring it is set. + Get the Q index, ensuring it is set. Raises a ValueError if the Q index is not set. Returns: int: The Q index. @@ -300,78 +242,49 @@ def _require_Q_index(self) -> int: raise ValueError("Q_index must be set.") return self._Q_index - def _handle_energy( - self, energy: np.ndarray | sc.Variable | None - ) -> np.ndarray | sc.Variable: - """ " - Handle the energy input for evaluation methods. - - If energy is None, use the energy values from the experiment. - If energy is a sc.Variable, extract the values as a numpy array. - If energy is already a numpy array, return it as is. - - Args: - energy (np.ndarray | sc.Variable | None): The input energy values. - Returns: - np.ndarray: The energy values to use for evaluation. - """ - # TODO: handle units properly - - if energy is None: - energy = self.energy.values - - if isinstance(energy, np.ndarray): - return energy - - if isinstance(energy, sc.Variable): - return energy.values - - raise TypeError("Energy must be a numpy array, sc.Variable, or None.") - def _on_Q_index_changed(self) -> None: """ Handle changes to the Q index. - This method is called whenever the Q index is changed. It updates - the Convolution object for the new Q index. + This method is called whenever the Q index is changed. It + updates the Convolution object for the new Q index. """ - self._convolver = self._create_convolver(Q_index=self.Q_index) + self._convolver = self._create_convolver() + + ############# + # Private methods: evaluation + ############# def _evaluate_components( self, components: ComponentCollection | ModelComponent, - energy: np.ndarray | sc.Variable | None = None, convolver: Convolution | None = None, convolve: bool = True, - Q_index: int | None = None, - ): + ) -> np.ndarray: """ Calculate the contribution of a set of components, optionally convolving with the resolution. - If convolve is True and a Convolution object is provided, - use it to perform the convolution of the components with the - resolution. If convolve is True but no Convolution object is - provided, create a new Convolution object for the given - components and energy. If convolve is False, evaluate the - components directly without convolution. + If convolve is True and a + Convolution object is provided (for full model evaluation), we + use it to perform the convolution of the components with the + resolution. + If convolve is True but no Convolution object is + provided, create a new Convolution object for the given + components (for individual components). + If convolve is False, evaluate the components directly without + convolution (for background). Args: components (ComponentCollection | ModelComponent): The components to evaluate. - energy (np.ndarray | sc.Variable | None): - The energy values to evaluate the components for. If - None, the energy values from the experiment will be - used. - convolver (Convolution | None): - An optional Convolution object to use for convolution. - If None, a new Convolution object will be created if - convolve is True. + convolver (Convolution | None): An optional Convolution + object to use for convolution. If None, a new + Convolution object will be created if convolve is True. convolve (bool): Whether to perform convolution with the resolution. Default is True. """ - if Q_index is None: - Q_index = self._require_Q_index() - energy = self._handle_energy(energy) + Q_index = self._require_Q_index() + energy = self.energy.values energy_offset = self.instrument_model.get_energy_offset_at_Q(Q_index).value # If there are no components, return zero @@ -388,12 +301,15 @@ def _evaluate_components( if resolution.is_empty: return components.evaluate(energy - energy_offset) - # Convolution For fitting we don't want to create a new - # Convolution object at each iteration + # If a convolver is provided, use it. This allows reusing the + # same convolver for multiple evaluations during fitting for + # performance reasons. if convolver is not None: return convolver.convolution() - # For evaluating individual components + # If no convolver is provided, create a new one. This is for + # evaluating individual components for plotting, where + # performance is not important. conv = Convolution( sample_components=components, resolution_components=resolution, @@ -403,80 +319,49 @@ def _evaluate_components( ) return conv.convolution() - def _evaluate_sample( - self, - energy: np.ndarray | sc.Variable | None = None, - Q_index: int | None = None, - ): + def _evaluate_sample(self) -> np.ndarray: """ Evaluate the sample contribution for a given Q index. - If a Convolution object exists for the Q index, use it to - perform the convolution of the sample components with the - resolution components. If no Convolution object exists, evaluate - the sample components directly without convolution. + Assumes that self._convolver is up to date. - Args: - energy (np.ndarray | sc.Variable | None): The energy values - to evaluate the sample contribution for. If None, the energy - values from the experiment will be used. Returns: np.ndarray: The evaluated sample contribution. """ - if Q_index is None: - Q_index = self._require_Q_index() + Q_index = self._require_Q_index() components = self.sample_model.get_component_collection(Q_index=Q_index) return self._evaluate_components( components=components, - energy=energy, convolver=self._convolver, convolve=True, ) def _evaluate_sample_component( self, - component, - energy: np.ndarray | sc.Variable | None = None, - ): + component: ModelComponent, + ) -> np.ndarray: """ Evaluate a single sample component for a given Q index. - If a Convolution object exists for the Q index, use it to - perform the convolution of the sample component with the - resolution components. If no Convolution object exists, evaluate - the sample component directly without convolution. + Args: component: The sample component to evaluate. - energy (np.ndarray | sc.Variable | None): The energy values - to evaluate the sample component for. If None, the energy - values from the experiment will be used. Returns: np.ndarray: The evaluated sample component contribution. """ return self._evaluate_components( components=component, - energy=energy, convolver=None, convolve=True, ) - def _evaluate_background( - self, - energy: np.ndarray | sc.Variable | None = None, - Q_index: int | None = None, - ): + def _evaluate_background(self) -> np.ndarray: """ Evaluate the background contribution for a given Q index. - Evaluate each background component separately to get individual - contributions. Args: - energy (np.ndarray | sc.Variable | None): The energy values - to evaluate the background contribution for. If None, the - energy values from the experiment will be used. + Returns: np.ndarray: The evaluated background contribution. """ - - if Q_index is None: - Q_index = self._require_Q_index() + Q_index = self._require_Q_index() background_components = ( self.instrument_model.background_model.get_component_collection( Q_index=Q_index @@ -484,41 +369,41 @@ def _evaluate_background( ) return self._evaluate_components( components=background_components, - energy=energy, convolver=None, convolve=False, ) def _evaluate_background_component( self, - component, - energy: np.ndarray | sc.Variable | None = None, - ): + component: ModelComponent, + ) -> np.ndarray: """ Evaluate a single background component for a given Q index. - Evaluate the background component directly without convolution. + Args: component: The background component to evaluate. - energy (np.ndarray | sc.Variable | None): The energy values - to evaluate the background component for. If None, the energy - values from the experiment will be used. Returns: np.ndarray: The evaluated background component contribution. """ return self._evaluate_components( components=component, - energy=energy, convolver=None, convolve=False, ) - def _create_convolver( - self, Q_index: int, energy: np.ndarray | sc.Variable | None = None - ) -> Convolution | None: - """Initialize and return a Convolution object for the given Q - index. + def _create_convolver(self) -> Convolution | None: """ + Initialize and return a Convolution object for the given Q + index. If the necessary components for convolution are not + available, return None. + + Returns: + Convolution | None: The initialized Convolution object or + None if not available. + """ + Q_index = self._require_Q_index() + sample_components = self.sample_model.get_component_collection(Q_index) if sample_components.is_empty: return None @@ -528,8 +413,7 @@ def _create_convolver( ) if resolution_components.is_empty: return None - if energy is None: - energy = self.energy + energy = self.energy # TODO: allow convolution options to be set. convolver = Convolution( sample_components=sample_components, @@ -539,3 +423,72 @@ def _create_convolver( energy_offset=self.instrument_model.get_energy_offset_at_Q(Q_index), ) return convolver + + ############# + # Private methods: create scipp arrays for plotting + ############# + + def _create_component_scipp_array( + self, + component: ModelComponent, + background: np.ndarray | None = None, + ) -> sc.DataArray: + values = self._evaluate_sample_component(component) + if background is not None: + values += background + return self._to_scipp_array(values) + + def _create_background_component_scipp_array( + self, + component: ModelComponent, + ) -> sc.DataArray: + values = self._evaluate_background_component(component) + return self._to_scipp_array(values) + + def _create_sample_scipp_array(self) -> sc.DataArray: + values = self._calculate() + return self._to_scipp_array(values) + + def _create_components_dataset_single_Q( + self, add_background: bool = True + ) -> dict[str, sc.DataArray]: + """Create sc.DataArrays for all sample and background + components.""" + scipp_arrays = {} + sample_components = self.sample_model.get_component_collection( + Q_index=self.Q_index + ).components + + background_components = ( + self.instrument_model.background_model.get_component_collection( + Q_index=self.Q_index + ).components + ) + background = self._evaluate_background() if add_background else None + for component in sample_components: + scipp_arrays[component.display_name] = self._create_component_scipp_array( + component, background=background + ) + for component in background_components: + scipp_arrays[component.display_name] = ( + self._create_background_component_scipp_array(component) + ) + return sc.Dataset(scipp_arrays) + + def _to_scipp_array(self, values: np.ndarray) -> sc.DataArray: + """ + Convert a numpy array of values to a sc.DataArray with the + correct coordinates for energy and Q. + + Args: + values (np.ndarray): The values to convert. + Returns: + sc.DataArray: The converted sc.DataArray. + """ + return sc.DataArray( + data=sc.array(dims=["energy"], values=values), + coords={ + "energy": self.energy, + "Q": self.Q[self.Q_index], + }, + ) diff --git a/src/easydynamics/analysis/analysis_base.py b/src/easydynamics/analysis/analysis_base.py index 3a7d0e8b..e6f63939 100644 --- a/src/easydynamics/analysis/analysis_base.py +++ b/src/easydynamics/analysis/analysis_base.py @@ -107,9 +107,7 @@ def instrument_model(self, value: InstrumentModel) -> None: @property def Q(self) -> sc.Variable | None: """The Q values from the associated Experiment, if available.""" - if self.experiment is not None: - return self.experiment.Q - return None + return self.experiment.Q @Q.setter def Q(self, value) -> None: @@ -121,9 +119,7 @@ def energy(self) -> sc.Variable | None: """The energy values from the associated Experiment, if available. """ - if self.experiment is not None: - return self.experiment.energy - return None + return self.experiment.energy @energy.setter def energy(self, value) -> None: @@ -160,15 +156,47 @@ def temperature(self, value) -> None: ############# def _on_experiment_changed(self) -> None: + """ + Update the Q values in the sample and instrument models when the + experiment changes. + """ self._sample_model.Q = self.Q self._instrument_model.Q = self.Q def _on_sample_model_changed(self) -> None: + """ + Update the Q values in the sample model when the sample model + changes. + """ self._sample_model.Q = self.Q def _on_instrument_model_changed(self) -> None: + """ + Update the Q values in the instrument model when the instrument + model changes. + """ self._instrument_model.Q = self.Q + def _verify_Q_index(self, Q_index: int | None) -> int | None: + """ + Verify that the Q index is valid. + + Params: + Q_index (int | None): The Q index to verify. + Returns: + int | None: The verified Q index. + Raises: + ValueError: If the Q index is not valid. + """ + if Q_index is not None: + if ( + not isinstance(Q_index, int) + or Q_index < 0 + or (self.Q is not None and Q_index >= len(self.Q)) + ): + raise ValueError("Q_index must be a valid index for the Q values.") + return Q_index + ############# # Dunder methods ############# diff --git a/src/easydynamics/experiment/experiment.py b/src/easydynamics/experiment/experiment.py index c0fd4b5e..771656b0 100644 --- a/src/easydynamics/experiment/experiment.py +++ b/src/easydynamics/experiment/experiment.py @@ -1,6 +1,4 @@ import os -import warnings -from typing import Optional import plopp as pp import scipp as sc @@ -31,7 +29,7 @@ def __init__( ) if data is None: - self._data: Optional[sc.DataArray] = None + self._data = None elif isinstance(data, str): self.load_hdf5(filename=data) elif isinstance(data, sc.DataArray): @@ -82,7 +80,7 @@ def binned_data(self, value: sc.DataArray): def Q(self) -> sc.Variable | None: """Get the Q values from the dataset.""" if self._data is None: - warnings.warn("No data loaded.", UserWarning) + # warnings.warn("No data loaded.", UserWarning) return None return self._binned_data.coords["Q"] @@ -95,7 +93,7 @@ def Q(self, value: sc.Variable): def energy(self) -> sc.Variable: """Get the energy values from the dataset.""" if self._data is None: - warnings.warn("No data loaded.", UserWarning) + # warnings.warn("No data loaded.", UserWarning) return None return self._binned_data.coords["energy"] diff --git a/src/easydynamics/sample_model/model_base.py b/src/easydynamics/sample_model/model_base.py index bb5859cc..039da8bd 100644 --- a/src/easydynamics/sample_model/model_base.py +++ b/src/easydynamics/sample_model/model_base.py @@ -1,7 +1,6 @@ # SPDX-FileCopyrightText: 2025-2026 EasyDynamics contributors # SPDX-License-Identifier: BSD-3-Clause -import warnings from copy import copy import numpy as np @@ -194,7 +193,17 @@ def Q(self) -> np.ndarray | None: @Q.setter def Q(self, value: Q_type | None) -> None: """Set the Q values of the SampleModel.""" - self._Q = _validate_and_convert_Q(value) + old_Q = self._Q + new_Q = _validate_and_convert_Q(value) + + if ( + old_Q is not None + and new_Q is not None + and len(old_Q) == len(new_Q) + and all(np.isclose(old_Q, new_Q)) + ): + return # No change in Q, so do nothing + self._Q = new_Q self._on_Q_change() # ------------------------------------------------------------------ @@ -276,9 +285,9 @@ def _generate_component_collections(self) -> None: # TODO regenerate automatically if Q or components have changed if self._Q is None: - warnings.warn( - "Q is not set. No component collections generated", UserWarning - ) + # warnings.warn( + # "Q is not set. No component collections generated", UserWarning + # ) self._component_collections = [] return From 012429ef8ad2b35ff6cb6930123a73f9b7fa0b41 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Thu, 12 Feb 2026 12:08:50 +0100 Subject: [PATCH 13/17] Add plotting of parameters and examples --- docs/docs/tutorials/analysis.ipynb | 183 ++++--- docs/docs/tutorials/analysis1d.ipynb | 104 ++++ src/easydynamics/analysis/analysis.py | 109 +++++ src/easydynamics/analysis/analysis1d old.py | 497 -------------------- src/easydynamics/analysis/analysis1d.py | 3 +- 5 files changed, 294 insertions(+), 602 deletions(-) create mode 100644 docs/docs/tutorials/analysis1d.ipynb delete mode 100644 src/easydynamics/analysis/analysis1d old.py diff --git a/docs/docs/tutorials/analysis.ipynb b/docs/docs/tutorials/analysis.ipynb index 39b70c8e..3da1411c 100644 --- a/docs/docs/tutorials/analysis.ipynb +++ b/docs/docs/tutorials/analysis.ipynb @@ -5,7 +5,10 @@ "id": "8643b10c", "metadata": {}, "source": [ - "asd" + "# Analysis\n", + "It is time to analyse some data. We here show how to set up an Analysis object and use it to first fit an artificial vanadium measurements, and next an artificial measurement of a model with diffusion and some elastic scattering.\n", + "\n", + "In the near future, it will be possible to fit the width and area of the Lorentzian to the diffusion model, as well as fitting the diffusion model directly to the data." ] }, { @@ -21,6 +24,7 @@ "from easydynamics.experiment import Experiment\n", "from easydynamics.sample_model import ComponentCollection\n", "from easydynamics.sample_model import DeltaFunction\n", + "from easydynamics.sample_model import Lorentzian\n", "from easydynamics.sample_model import Gaussian\n", "from easydynamics.sample_model import Polynomial\n", "from easydynamics.sample_model.background_model import BackgroundModel\n", @@ -28,6 +32,7 @@ "from easydynamics.sample_model.sample_model import SampleModel\n", "from easydynamics.sample_model.instrument_model import InstrumentModel\n", "from easydynamics.analysis.analysis import Analysis\n", + "from copy import copy\n", "%matplotlib widget" ] }, @@ -45,20 +50,18 @@ { "cell_type": "code", "execution_count": null, - "id": "41f842f0", + "id": "6762faba", "metadata": {}, "outputs": [], "source": [ - "# Example of Analysis1d with a simple sample model and instrument model\n", + "# Example of Analysis with a simple sample model and instrument model\n", "delta_function = DeltaFunction(display_name='DeltaFunction', area=1)\n", - "gaussian = Gaussian(display_name='Gaussian', width=0.1, area=1)\n", - "components = ComponentCollection(components=[delta_function, gaussian])\n", "sample_model = SampleModel(\n", - " components=components,\n", + " components=delta_function,\n", ")\n", "\n", "res_gauss = Gaussian(width=0.1)\n", - "res_gauss.area.fixed = True\n", + "res_gauss.area.fixed=True\n", "resolution_model = ResolutionModel(components=res_gauss)\n", "\n", "\n", @@ -69,194 +72,166 @@ " background_model=background_model,\n", ")\n", "\n", - "my_analysis = Analysis1d(\n", + "vanadium_analysis = Analysis(\n", + " display_name='Vanadium Full Analysis',\n", " experiment=vanadium_experiment,\n", " sample_model=sample_model,\n", " instrument_model=instrument_model,\n", - " Q_index=5,\n", ")\n", "\n", - "fit_result = my_analysis.fit()\n", - "my_analysis.plot_data_and_model()" + "fit_result_independent_single_Q = vanadium_analysis.fit(fit_method=\"independent\", Q_index=5)\n", + "vanadium_analysis.plot_data_and_model(Q_index=5)" ] }, { "cell_type": "code", "execution_count": null, - "id": "6762faba", + "id": "e98e3d65", "metadata": {}, "outputs": [], "source": [ - "# Example of Analysis with a simple sample model and instrument model\n", - "delta_function = DeltaFunction(display_name='DeltaFunction', area=1)\n", - "gaussian = Gaussian(display_name='Gaussian', width=0.1, area=1)\n", - "components = ComponentCollection(components=[delta_function, gaussian])\n", - "sample_model = SampleModel(\n", - " components=components,\n", - ")\n", - "\n", - "res_gauss = Gaussian(width=0.1)\n", - "res_gauss.area.fixed = True\n", - "resolution_model = ResolutionModel(components=res_gauss)\n", - "\n", - "\n", - "background_model = BackgroundModel(components=Polynomial(coefficients=[0.001]))\n", - "\n", - "instrument_model = InstrumentModel(\n", - " resolution_model=resolution_model,\n", - " background_model=background_model,\n", - ")\n", - "\n", - "my_analysis = Analysis(\n", - " experiment=vanadium_experiment,\n", - " sample_model=sample_model,\n", - " instrument_model=instrument_model,\n", - ")\n", - "\n", - "fit_result1 = my_analysis.fit(fit_method=\"independent\", Q_index=5)" + "fit_result_independent_all_Q = vanadium_analysis.fit(fit_method=\"independent\")\n", + "vanadium_analysis.plot_data_and_model()" ] }, { "cell_type": "code", "execution_count": null, - "id": "e98e3d65", + "id": "af13afce", "metadata": {}, "outputs": [], "source": [ - "fit_result2 = my_analysis.fit(fit_method=\"independent\")" + "fit_result_simultaneous = vanadium_analysis.fit(fit_method=\"simultaneous\")\n", + "fit_result_simultaneous\n", + "vanadium_analysis.plot_data_and_model()" ] }, { "cell_type": "code", "execution_count": null, - "id": "af13afce", + "id": "133e682e", "metadata": {}, "outputs": [], "source": [ - "fit_result3 = my_analysis.fit(fit_method=\"simultaneous\")\n", - "fit_result3" + "# Inspect the Parameters as a scipp Dataset\n", + "vanadium_analysis.parameters_to_dataset()\n" ] }, { "cell_type": "code", "execution_count": null, - "id": "02702f95", + "id": "dfacdf24", "metadata": {}, "outputs": [], - "source": [] + "source": [ + "# Plot some of fitted parameters as a function of Q\n", + "vanadium_analysis.plot_parameters(names=[\"DeltaFunction area\"])\n" + ] }, { "cell_type": "code", "execution_count": null, - "id": "70091539", + "id": "b6f9f316", "metadata": {}, "outputs": [], "source": [ - "my_analysis.plot_data_and_model()" + "vanadium_analysis.plot_parameters(names=[\"Gaussian width\"])" ] }, { "cell_type": "code", "execution_count": null, - "id": "2ad6384e", + "id": "3609e6c1", "metadata": {}, "outputs": [], "source": [ - "sample_comps, background_comps = my_analysis.analysis_list[0].calculate_individual_components()\n", - "sample_comps" + "# Set up the diffusion analysis with the same resolution model as the\n", + "# vanadium analysis\n", + "diffusion_experiment = Experiment('Diffusion')\n", + "diffusion_experiment.load_hdf5(filename='diffusion_data_example.h5')" ] }, { "cell_type": "code", "execution_count": null, - "id": "35b0fac5", + "id": "e685909a", "metadata": {}, "outputs": [], "source": [ - "my_analysis.sample_model" + "# We set up the model first.\n", + "delta_function = DeltaFunction(display_name='DeltaFunction', area=0.2)\n", + "lorentzian = Lorentzian(display_name='Lorentzian', area=0.5, width=0.3)\n", + "component_collection=ComponentCollection(\n", + " components=[delta_function, lorentzian],\n", + ")\n", + "sample_model = SampleModel(\n", + " components=component_collection,\n", + ")\n", + "\n", + "background_model = BackgroundModel(components=Polynomial(coefficients=[0.001]))\n", + "\n", + "instrument_model = InstrumentModel(\n", + " background_model=background_model,\n", + ")\n", + "\n", + "diffusion_analysis = Analysis(\n", + " display_name='Diffusion Full Analysis',\n", + " experiment=diffusion_experiment,\n", + " sample_model=sample_model,\n", + " instrument_model=instrument_model,\n", + ")\n", + "\n", + "# We need to hack in the resolution model from the vanadium analysis,\n", + "# since the setters and getters overwrite the model. This will be fixed\n", + "# asap.\n", + "diffusion_analysis.instrument_model._resolution_model = vanadium_analysis.instrument_model.resolution_model\n", + "diffusion_analysis.instrument_model.resolution_model.fix_all_parameters()\n", + "diffusion_analysis.plot_parameters(names=[\"Gaussian width\"])\n" ] }, { "cell_type": "code", "execution_count": null, - "id": "2dfb1f90", + "id": "c66828eb", "metadata": {}, "outputs": [], "source": [ - "# my_analysis.get_all_variables()" + "# Let us see how good the starting parameters are\n", + "diffusion_analysis.plot_data_and_model()" ] }, { "cell_type": "code", "execution_count": null, - "id": "5afefbab", + "id": "197b44c5", "metadata": {}, "outputs": [], "source": [ - "# my_analysis.get_fit_parameters()" + "# Now we fit the data and plot the result. Looks good!\n", + "diffusion_analysis.fit(fit_method=\"independent\")\n", + "diffusion_analysis.plot_data_and_model()" ] }, { "cell_type": "code", "execution_count": null, - "id": "465c0e1e", + "id": "df14b5c4", "metadata": {}, "outputs": [], "source": [ - "# for Q_index in range(len(my_analysis.Q)):\n", - "# my_analysis.Q_index = Q_index\n", - "# my_analysis.fit()\n", - "# my_analysis.plot_data_and_model()\n", - "# print(my_analysis.get_fit_parameters())\n" + "# Let us look at the most interesting fit parameters\n", + "diffusion_analysis.plot_parameters(names=[\"Lorentzian width\", \"Lorentzian area\"])" ] }, { "cell_type": "code", "execution_count": null, - "id": "9bdeed2b", + "id": "eb226c8f", "metadata": {}, "outputs": [], "source": [ - "# Create a diffusion_model and components for the SampleModel\n", - "\n", - "# Creating components\n", - "component_collection = ComponentCollection()\n", - "delta_function = DeltaFunction(display_name='DeltaFunction', area=1)\n", - "gaussian = Gaussian(display_name='Gaussian', width=0.1, center=0.5, area=0.5)\n", - "\n", - "# Adding components to the component collection\n", - "component_collection.append_component(delta_function)\n", - "\n", - "\n", - "sample_model = SampleModel(\n", - " components=component_collection,\n", - " unit='meV',\n", - " display_name='MySampleModel',\n", - ")\n", - "\n", - "res_gauss = Gaussian(width=0.1)\n", - "res_gauss.area.fixed = True\n", - "resolution_model = ResolutionModel(components=res_gauss)\n", - "\n", - "\n", - "background_model = BackgroundModel(components=Polynomial(coefficients=[0.001]))\n", - "\n", - "instrument_model = InstrumentModel(\n", - " resolution_model=resolution_model,\n", - " background_model=background_model,\n", - ")\n", - "\n", - "my_full_analysis = Analysis(\n", - " experiment=vanadium_experiment,\n", - " sample_model=sample_model,\n", - " instrument_model=instrument_model,\n", - ")\n", - "\n", - "# my_full_analysis._fit_all_Q_independently()\n", - "my_full_analysis._fit_all_Q_simultaneously()\n", - "for analysis_object in my_full_analysis._analysis_list:\n", - " analysis_object.plot_data_and_model()\n", - " print(analysis_object.get_fit_parameters())\n" + "# It will be possible to fit this to a DiffusionModel, but that will\n", + "# come later." ] } ], diff --git a/docs/docs/tutorials/analysis1d.ipynb b/docs/docs/tutorials/analysis1d.ipynb new file mode 100644 index 00000000..8a695913 --- /dev/null +++ b/docs/docs/tutorials/analysis1d.ipynb @@ -0,0 +1,104 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "8643b10c", + "metadata": {}, + "source": [ + "# Analysis1d\n", + "Sometimes, you will only be interested in a particular Q, not the full dataset. For this, use the Analysis1d object. We here show how to set it up to fit an artificial vanadium measurement." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bca91d3c", + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "from easydynamics.analysis.analysis1d import Analysis1d\n", + "from easydynamics.experiment import Experiment\n", + "from easydynamics.sample_model import ComponentCollection\n", + "from easydynamics.sample_model import DeltaFunction\n", + "from easydynamics.sample_model import Gaussian\n", + "from easydynamics.sample_model import Polynomial\n", + "from easydynamics.sample_model.background_model import BackgroundModel\n", + "from easydynamics.sample_model.resolution_model import ResolutionModel\n", + "from easydynamics.sample_model.sample_model import SampleModel\n", + "from easydynamics.sample_model.instrument_model import InstrumentModel\n", + "from easydynamics.analysis.analysis import Analysis\n", + "%matplotlib widget" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8deca9b6", + "metadata": {}, + "outputs": [], + "source": [ + "vanadium_experiment = Experiment('Vanadium')\n", + "vanadium_experiment.load_hdf5(filename='vanadium_data_example.h5')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "41f842f0", + "metadata": {}, + "outputs": [], + "source": [ + "# Example of Analysis1d with a simple sample model and instrument model\n", + "delta_function = DeltaFunction(display_name='DeltaFunction', area=1)\n", + "sample_model = SampleModel(\n", + " components=delta_function,\n", + ")\n", + "\n", + "res_gauss = Gaussian(width=0.1)\n", + "resolution_model = ResolutionModel(components=res_gauss)\n", + "\n", + "\n", + "background_model = BackgroundModel(components=Polynomial(coefficients=[0.001]))\n", + "\n", + "instrument_model = InstrumentModel(\n", + " resolution_model=resolution_model,\n", + " background_model=background_model,\n", + ")\n", + "\n", + "my_analysis = Analysis1d(\n", + " display_name='Vanadium Analysis',\n", + " experiment=vanadium_experiment,\n", + " sample_model=sample_model,\n", + " instrument_model=instrument_model,\n", + " Q_index=5,\n", + ")\n", + "\n", + "fit_result = my_analysis.fit()\n", + "my_analysis.plot_data_and_model()\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "easydynamics_newbase", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/src/easydynamics/analysis/analysis.py b/src/easydynamics/analysis/analysis.py index ae7b16a7..81921b20 100644 --- a/src/easydynamics/analysis/analysis.py +++ b/src/easydynamics/analysis/analysis.py @@ -8,6 +8,7 @@ from easyscience.fitting.minimizers.utils import FitResults from easyscience.fitting.multi_fitter import MultiFitter from easyscience.variable import Parameter +from scipp import UnitError from easydynamics.analysis.analysis1d import Analysis1d from easydynamics.analysis.analysis_base import AnalysisBase @@ -185,6 +186,7 @@ def plot_data_and_model( "linestyle": {"Data": "none", "Model": "-"}, "marker": {"Data": "o", "Model": None}, "color": {"Data": "black", "Model": "red"}, + "markerfacecolor": {"Data": "none", "Model": "none"}, } data_and_model = { "Data": self.experiment.binned_data, @@ -207,6 +209,113 @@ def plot_data_and_model( ) display(fig) + def parameters_to_dataset(self) -> sc.Dataset: + """ + Creates a scipp dataset with copies of the Parameters in the + model. Ensures unit consistency across Q. + """ + + ds = sc.Dataset(coords={"Q": self.Q}) + + # Collect all parameter names + all_names = { + param.name + for analysis in self.analysis_list + for param in analysis.get_all_parameters() + } + + # Storage + values = {name: [] for name in all_names} + variances = {name: [] for name in all_names} + units = {} + + for analysis in self.analysis_list: + pars = {p.name: p for p in analysis.get_all_parameters()} + + for name in all_names: + if name in pars: + p = pars[name] + + # Unit consistency check + if name not in units: + units[name] = p.unit + elif units[name] != p.unit: + try: + p.unit.convert(units[name]) + except Exception as e: + raise UnitError( + f"Inconsistent units for parameter '{name}': " + f"{units[name]} vs {p.unit}" + ) from e + + values[name].append(p.value) + variances[name].append(p.variance) + else: + values[name].append(np.nan) + variances[name].append(np.nan) + + # Build dataset variables + for name in all_names: + ds[name] = sc.Variable( + dims=["Q"], + values=np.asarray(values[name], dtype=float), + variances=np.asarray(variances[name], dtype=float), + unit=units.get(name, None), + ) + + return ds + + def plot_parameters( + self, + names: str | list[str] | None = None, + **kwargs, + ) -> None: + """ + Plot fitted parameters as a function of Q. + + Parameters: + --------------- + names: str or list of str + Name(s) of the parameter(s) to plot. If None, plots all + parameters. + kwargs: Additional keyword arguments passed to plopp.slicer for + customizing the plot (e.g., title, linestyle, marker, + color). + + Returns: A plopp figure. + """ + + ds = self.parameters_to_dataset() + + if not names: + names = list(ds.keys()) + + if isinstance(names, str): + names = [names] + + if not isinstance(names, list) or not all( + isinstance(name, str) for name in names + ): + raise TypeError("names must be a string or a list of strings.") + + for name in names: + if name not in ds: + raise ValueError(f"Parameter '{name}' not found in dataset.") + + data_to_plot = {name: ds[name] for name in names} + plot_kwargs_defaults = { + "linestyle": {name: "none" for name in names}, + "marker": {name: "o" for name in names}, + "markerfacecolor": {name: "none" for name in names}, + } + + plot_kwargs_defaults.update(kwargs) + fig = pp.plot( + data_to_plot, + **plot_kwargs_defaults, + ) + return fig + ############# # Private methods ############# diff --git a/src/easydynamics/analysis/analysis1d old.py b/src/easydynamics/analysis/analysis1d old.py deleted file mode 100644 index b27fed1e..00000000 --- a/src/easydynamics/analysis/analysis1d old.py +++ /dev/null @@ -1,497 +0,0 @@ -# SPDX-FileCopyrightText: 2025-2026 EasyDynamics contributors -# SPDX-License-Identifier: BSD-3-Clause - - -import numpy as np -import scipp as sc -from easyscience.base_classes.model_base import ModelBase as EasyScienceModelBase -from easyscience.fitting.fitter import Fitter as EasyScienceFitter -from easyscience.variable import DescriptorNumber -from easyscience.variable import Parameter - -from easydynamics.convolution import Convolution -from easydynamics.experiment import Experiment -from easydynamics.sample_model import InstrumentModel -from easydynamics.sample_model import ResolutionModel -from easydynamics.sample_model import SampleModel - - -class Analysis1d(EasyScienceModelBase): - """For analysing data.""" - - def __init__( - self, - display_name: str = "MyAnalysis", - unique_name: str | None = None, - experiment: Experiment | None = None, - sample_model: SampleModel | None = None, - instrument_model: InstrumentModel | None = None, - Q_index: int | None = None, - ): - super().__init__(display_name=display_name, unique_name=unique_name) - - if experiment is not None and not isinstance(experiment, Experiment): - raise TypeError("experiment must be an instance of Experiment or None.") - - self._experiment = experiment - - if sample_model is not None and not isinstance(sample_model, SampleModel): - raise TypeError("sample_model must be an instance of SampleModel or None.") - sample_model.Q = self.Q - self._sample_model = sample_model - - if instrument_model is not None and not isinstance( - instrument_model, InstrumentModel - ): - raise TypeError( - "instrument_model must be an instance of InstrumentModel or None." - ) - if instrument_model is None: - self._instrument_model = InstrumentModel() - else: - self._instrument_model = instrument_model - - self._convolvers = [None] * (len(self.Q) if self.Q is not None else 0) - self._update_models() - - if Q_index is not None: - if ( - not isinstance(Q_index, int) - or Q_index < 0 - or (self.Q is not None and Q_index >= len(self.Q)) - ): - raise ValueError("Q_index must be a valid index for the Q values.") - self._Q_index = Q_index - - ############# - # Properties - ############# - - @property - def experiment(self) -> Experiment | None: - """The Experiment associated with this Analysis.""" - return self._experiment - - @experiment.setter - def experiment(self, value: Experiment | None) -> None: - if value is not None and not isinstance(value, Experiment): - raise TypeError("experiment must be an instance of Experiment or None.") - self._experiment = value - self._update_models() - - @property - def sample_model(self) -> SampleModel | None: - """The SampleModel associated with this Analysis.""" - return self._sample_model - - @sample_model.setter - def sample_model(self, value: SampleModel | None) -> None: - if value is not None and not isinstance(value, SampleModel): - raise TypeError("sample_model must be an instance of SampleModel or None.") - self._sample_model = value - self._update_models() - - @property - def resolution_model(self) -> ResolutionModel | None: - """The ResolutionModel associated with this Analysis.""" - return self._resolution_model - - @resolution_model.setter - def resolution_model(self, value: ResolutionModel | None) -> None: - if value is not None and not isinstance(value, ResolutionModel): - raise TypeError( - "resolution_model must be an instance of ResolutionModel or None." - ) - self._resolution_model = value - self._update_models() - - @property - def Q(self) -> sc.Variable | None: - """The Q values from the associated Experiment, if available.""" - if self.experiment is not None: - return self.experiment.Q - return None - - @Q.setter - def Q(self, value) -> None: - """Q is a read-only property derived from the Experiment.""" - raise AttributeError("Q is a read-only property derived from the Experiment.") - - @property - def energy(self) -> sc.Variable | None: - """The energy values from the associated Experiment, if - available. - """ - if self.experiment is not None: - return self.experiment.energy - return None - - @energy.setter - def energy(self, value) -> None: - """Energy is a read-only property derived from the - Experiment. - """ - raise AttributeError( - "energy is a read-only property derived from the Experiment." - ) - - @property - def temperature(self) -> Parameter | None: - """The temperature from the associated Experiment, if - available. - """ - return self.sample_model.temperature if self.sample_model is not None else None - - @temperature.setter - def temperature(self, value) -> None: - """Temperature is a read-only property derived from the - Experiment. - """ - raise AttributeError( - "temperature is a read-only property derived from the sample model." - ) - - @property - def energy_offset(self) -> list[Parameter] | None: - """Get the energy offsets for each Q value.""" - return self._energy_offset - - @energy_offset.setter - def energy_offset(self, offsets: list[Parameter] | None) -> None: - """Set the energy offsets for each Q value. - - Args: - offsets (list[Parameter] | None): The list of energy - offsets. - Raises: - TypeError: If offsets is not a list of Parameters or - None. - """ - if offsets is not None: - if len(offsets) != len(self.Q): - raise ValueError( - "energy_offset list length must match number of Q values." - ) - for offset in offsets: - if not isinstance(offset, Parameter): - raise TypeError( - "Each energy_offset must be an instance of Parameter." - ) - self._energy_offset = offsets - - @property - def Q_index(self) -> int | None: - """Get the Q index for single Q analysis.""" - return self._Q_index - - @Q_index.setter - def Q_index(self, index: int | None) -> None: - """Set the Q index for single Q analysis. - - Args: - index (int | None): The Q index. - """ - if index is not None: - if ( - not isinstance(index, int) - or index < 0 - or (self.Q is not None and index >= len(self.Q)) - ): - raise ValueError("Q_index must be a valid index for the Q values.") - self._Q_index = index - - ############# - # Other methods - ############# - - def calculate(self, energy: float | None = None) -> np.ndarray: - """Calculate the model prediction for a given Q index. - - Args: - energy (float): The energy value to calculate the model for. - Returns: - sc.DataArray: The calculated model prediction. - """ - Q_index = self.Q_index - if Q_index is None: - raise ValueError("Q_index must be set to calculate the model.") - - if energy is None: - energy = self.energy.values - - # TODO: handle units properly - energy = energy - self.energy_offset[Q_index].value - if self.sample_model is None: - sample_intensity = np.zeros_like(energy) - else: - if self.resolution_model is None: - sample_intensity = self.sample_model._component_collections[ - Q_index - ].evaluate(energy) - else: - convolver = self._convolvers[Q_index] - sample_intensity = convolver.convolution() - - if self.background_model is None: - background_intensity = np.zeros_like(energy) - else: - background_intensity = self.background_model._component_collections[ - Q_index - ].evaluate(energy) - - sample_plus_background = sample_intensity + background_intensity - - return sample_plus_background - - def calculate_individual_components( - self, - ) -> tuple[list[np.ndarray], list[np.ndarray]]: - """Calculate the model prediction for a given Q index for each - individual component. - - Args: - Q_index (int): The index of the Q value to calculate the - model for. - Returns: - list[np.ndarray]: The calculated model predictions for each - individual component. - """ - sample_results = [] - background_results = [] - Q_index = self.Q_index - if Q_index is None: - raise ValueError("Q_index must be set to calculate the model.") - - if self.sample_model is not None: - # Calculate sample components - for component in self.sample_model._component_collections[ - Q_index - ]._components: - if self.resolution_model is None: - component_intensity = component.evaluate(self.energy) - else: - convolver = Convolution( - sample_components=component, - resolution_components=self.resolution_model._component_collections[ - Q_index - ], - energy=self.energy, - temperature=self.temperature, - ) - component_intensity = convolver.convolution() - sample_results.append(component_intensity) - - if self.background_model is not None: - # Calculate background components - for component in self.background_model._component_collections[ - Q_index - ]._components: - component_intensity = component.evaluate(self.energy) - background_results.append(component_intensity) - - return sample_results, background_results - - def fit(self): - """Fit the model to the experimental data for a given Q index. - - Args: - Returns: - FitResult: The result of the fit. - """ - if self._experiment is None: - raise ValueError("No experiment is associated with this Analysis.") - - Q_index = self.Q_index - if Q_index is None: - raise ValueError("Q_index must be set to perform the fit.") - - data = self.experiment.data["Q", Q_index] - x = data.coords["energy"].values - y = data.values - e = data.variances**0.5 - - def fit_func(x_vals): - return self.calculate(energy=x_vals) - - fitter = EasyScienceFitter( - fit_object=self, - fit_function=fit_func, - ) - - # Perform the fit - fit_result = fitter.fit(x=x, y=y, weights=1.0 / e) - - # Store result - self.fit_result = fit_result - - return fit_result - - def plot_data_and_model( - self, - plot_individual_components: bool = True, - ) -> None: - """Plot the experimental data and the model prediction. - - Args: - plot_individual_components (bool): Whether to plot - individual components. Default is True. - """ - if not isinstance(plot_individual_components, bool): - raise TypeError("plot_individual_components must be True or False.") - - import matplotlib.pyplot as plt - - Q_index = self.Q_index - if Q_index is None: - raise ValueError("Q_index must be set to plot the data and model.") - if self.experiment is None or self.experiment.data is None: - raise ValueError("Experiment data is not available for plotting.") - data = self.experiment.data["Q", Q_index] - energy = data.coords["energy"].values - model = self.calculate(energy=energy) - plt.figure() - plt.errorbar( - energy, - data.values, - yerr=data.variances**0.5, - fmt="o", - label="Data", - color="black", - ) - plt.plot(energy, model, label="Model", color="red") - if plot_individual_components: - sample_comps, background_comps = self.calculate_individual_components() - for i, comp in enumerate(sample_comps): - plt.plot( - energy, - comp, - label=f"Sample Component {i + 1}", - linestyle="--", - ) - for i, comp in enumerate(background_comps): - plt.plot( - energy, - comp, - label=f"Background Component {i + 1}", - linestyle=":", - ) - plt.xlabel(f"Energy ({self.energy.unit})") - plt.ylabel(f"Intensity ({self.sample_model.unit})") - plt.title(f"Data and Model at Q index {Q_index}") - plt.legend() - plt.show() - # model_data_array = self._create_model_data_group( - # individual_components=plot_individual_components ) if - # self.experiment is None or self.experiment.data is None: raise - # ValueError("Experiment data is not available for plotting.") - - # from IPython.display import display - - # fig = pp.slicer( - # {"Data": self.experiment.data, "Model": model_data_array}, - # color={"Data": "black", "Model": "red"}, - # linestyle={"Data": "none", "Model": "solid"}, - # marker={"Data": "o", "Model": "None"}, - # ) - # display(fig) - - def get_all_variables(self) -> list[DescriptorNumber]: - """Get all variables used in the analysis. - - Returns: - List[Descriptor]: A list of all variables. - """ - variables = [] - if self.sample_model is not None: - variables.extend( - self.sample_model._component_collections[ - self.Q_index - ].get_all_variables() - ) - if self.resolution_model is not None: - variables.extend( - self.resolution_model._component_collections[ - self.Q_index - ].get_all_variables() - ) - if self.background_model is not None: - variables.extend( - self.background_model._component_collections[ - self.Q_index - ].get_all_variables() - ) - variables.append(self.energy_offset[self.Q_index]) - # TODO temperature and diffusion - return variables - - ############# - # Private methods - ############# - - def _update_models(self): - """Update models based on the current experiment.""" - if self.experiment is None: - return - - for Q_index in range(len(self.Q)): - self._convolvers[Q_index] = self._create_convolver(Q_index) - - def _create_convolver(self, Q_index: int): - """Initialize and return a Convolution object for the given Q - index. - """ - if self.sample_model is None or self.resolution_model is None: - raise ValueError("Both sample_model and resolution_model must be defined.") - - sample_components = self.sample_model._component_collections[Q_index] - resolution_components = self.resolution_model._component_collections[Q_index] - energy = self.energy - convolver = Convolution( - sample_components=sample_components, - resolution_components=resolution_components, - energy=energy, - temperature=self.temperature, - ) - return convolver - - def _create_model_data_group(self, individual_components=True) -> sc.DataArray: - """Create a Scipp DataArray representing the model over all Q - and energy values. - """ - if self.Q is None or self.energy is None: - raise ValueError("Q and energy must be defined in the experiment.") - - model_data = [] - for Q_index in range(len(self.Q)): - model_at_Q = self.calculate(Q_index) - model_data.append(model_at_Q) - - model_data_array = sc.DataArray( - data=sc.array(dims=["Q", "energy"], values=model_data), - coords={ - "Q": self.Q, - "energy": self.energy, - }, - ) - model_group = sc.DataGroup({"Model": model_data_array}) - - if individual_components: - components = self.calculate_individual_components_all_Q() - for Q_index, (sample_comps, background_comps) in enumerate(components): - for samp_index, samp_comp in enumerate(sample_comps): - model_data_array[samp_comp.display_name] = sc.zeros_like( - model_data_array.data - ) - model_data_array[samp_comp.display_name].data[ - Q_index, : - ] = samp_comp - for back_index, back_comp in enumerate(background_comps): - model_data_array[back_comp.display_name] = sc.zeros_like( - model_data_array.data - ) - model_data_array[back_comp.display_name].data[ - Q_index, : - ] = back_comp - - model_data_array = model_data_array + model_group # WRONG BUT LINT - return model_data_array diff --git a/src/easydynamics/analysis/analysis1d.py b/src/easydynamics/analysis/analysis1d.py index 4e652aac..c4127960 100644 --- a/src/easydynamics/analysis/analysis1d.py +++ b/src/easydynamics/analysis/analysis1d.py @@ -209,8 +209,9 @@ def plot_data_and_model( plot_kwargs_defaults = { "title": self.display_name, "linestyle": {"Data": "none", "Model": "-"}, - "marker": {"Data": "o", "Model": None}, + "marker": {"Data": "o", "Model": "none"}, "color": {"Data": "black", "Model": "red"}, + "markerfacecolor": {"Data": "none", "Model": "none"}, } if plot_components: From 692cf663a10b848912c9f0ffd01e88b972c36cc2 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Thu, 12 Feb 2026 12:20:07 +0100 Subject: [PATCH 14/17] Update failing tests --- .../experiment/test_experiment.py | 245 +++++++----------- .../sample_model/test_model_base.py | 102 ++++---- tests/unit/easydynamics/utils/test_utils.py | 97 +++++-- 3 files changed, 217 insertions(+), 227 deletions(-) diff --git a/tests/unit/easydynamics/experiment/test_experiment.py b/tests/unit/easydynamics/experiment/test_experiment.py index 067a2017..05aa2470 100644 --- a/tests/unit/easydynamics/experiment/test_experiment.py +++ b/tests/unit/easydynamics/experiment/test_experiment.py @@ -12,12 +12,12 @@ class TestExperiment: @pytest.fixture def experiment(self): - Q = sc.linspace('Q', 0.5, 1.5, num=10, unit='1/Angstrom') - energy = sc.linspace('energy', -5, 5, num=11, unit='meV') - values = sc.array(dims=['Q', 'energy'], values=np.ones((10, 11))) - data = sc.DataArray(data=values, coords={'Q': Q, 'energy': energy}) + Q = sc.linspace("Q", 0.5, 1.5, num=10, unit="1/Angstrom") + energy = sc.linspace("energy", -5, 5, num=11, unit="meV") + values = sc.array(dims=["Q", "energy"], values=np.ones((10, 11))) + data = sc.DataArray(data=values, coords={"Q": Q, "energy": energy}) - experiment = Experiment(display_name='test_experiment', data=data) + experiment = Experiment(display_name="test_experiment", data=data) return experiment ############## @@ -27,51 +27,51 @@ def experiment(self): def test_init_array(self, experiment): "Test initialization with a Scipp DataArray" # WHEN THEN EXPECT - assert experiment.display_name == 'test_experiment' + assert experiment.display_name == "test_experiment" assert isinstance(experiment._data, sc.DataArray) - assert 'Q' in experiment._data.dims - assert 'energy' in experiment._data.dims - assert experiment._data.sizes['Q'] == 10 - assert experiment._data.sizes['energy'] == 11 + assert "Q" in experiment._data.dims + assert "energy" in experiment._data.dims + assert experiment._data.sizes["Q"] == 10 + assert experiment._data.sizes["energy"] == 11 assert sc.identical( experiment._data.data, - sc.array(dims=['Q', 'energy'], values=np.ones((10, 11))), + sc.array(dims=["Q", "energy"], values=np.ones((10, 11))), ) def test_init_string(self, tmp_path): "Test initialization with a filename string," - 'should load the file' + "should load the file" # WHEN - Q = sc.linspace('Q', 0.5, 1.5, num=10, unit='1/Angstrom') - energy = sc.linspace('energy', -5, 5, num=11, unit='meV') - values = sc.array(dims=['Q', 'energy'], values=np.ones((10, 11))) - data = sc.DataArray(data=values, coords={'Q': Q, 'energy': energy}) + Q = sc.linspace("Q", 0.5, 1.5, num=10, unit="1/Angstrom") + energy = sc.linspace("energy", -5, 5, num=11, unit="meV") + values = sc.array(dims=["Q", "energy"], values=np.ones((10, 11))) + data = sc.DataArray(data=values, coords={"Q": Q, "energy": energy}) - filename = tmp_path / 'test_experiment.h5' + filename = tmp_path / "test_experiment.h5" sc.io.save_hdf5(data, filename) # THEN - experiment = Experiment(display_name='loaded_experiment', data=str(filename)) + experiment = Experiment(display_name="loaded_experiment", data=str(filename)) # EXPECT - assert experiment.display_name == 'loaded_experiment' + assert experiment.display_name == "loaded_experiment" assert isinstance(experiment._data, sc.DataArray) - assert 'Q' in experiment._data.dims - assert 'energy' in experiment._data.dims - assert experiment._data.sizes['Q'] == 10 - assert experiment._data.sizes['energy'] == 11 + assert "Q" in experiment._data.dims + assert "energy" in experiment._data.dims + assert experiment._data.sizes["Q"] == 10 + assert experiment._data.sizes["energy"] == 11 assert sc.identical( experiment._data.data, - sc.array(dims=['Q', 'energy'], values=np.ones((10, 11))), + sc.array(dims=["Q", "energy"], values=np.ones((10, 11))), ) def test_init_no_data(self): "Test initialization with no data" # WHEN - experiment = Experiment(display_name='empty_experiment') + experiment = Experiment(display_name="empty_experiment") # THEN EXPECT - assert experiment.display_name == 'empty_experiment' + assert experiment.display_name == "empty_experiment" assert experiment._data is None def test_init_invalid_data(self): @@ -86,34 +86,34 @@ def test_init_invalid_data(self): def test_load_hdf5(self, tmp_path, experiment): "Test loading data from an HDF5 file." - 'First use scipp to save data to a file, ' - 'then load it using the method.' + "First use scipp to save data to a file, " + "then load it using the method." # WHEN # First create a file to load from - filename = tmp_path / 'test.h5' + filename = tmp_path / "test.h5" data_to_save = experiment.data sc.io.save_hdf5(data_to_save, filename) # THEN - new_experiment = Experiment(display_name='new_experiment') - new_experiment.load_hdf5(str(filename), display_name='loaded_data') + new_experiment = Experiment(display_name="new_experiment") + new_experiment.load_hdf5(str(filename), display_name="loaded_data") loaded_data = new_experiment.data # EXPECT assert sc.identical(data_to_save, loaded_data) - assert new_experiment.display_name == 'loaded_data' + assert new_experiment.display_name == "loaded_data" def test_load_hdf5_invalid_name_raises(self, experiment): "Test loading data from an HDF5 file," - 'giving the Experiment an invalid name' + "giving the Experiment an invalid name" # WHEN / THEN EXPECT with pytest.raises(TypeError): - experiment.load_hdf5('some_file.h5', display_name=123) + experiment.load_hdf5("some_file.h5", display_name=123) def test_load_hdf5_invalid_filename_raises(self, experiment): "Test loading data from an HDF5 file with an invalid filename" # WHEN / THEN EXPECT - with pytest.raises(TypeError, match='must be a string'): + with pytest.raises(TypeError, match="must be a string"): experiment.load_hdf5(123) def test_load_hdf5_invalid_file_raises(self, experiment): @@ -121,13 +121,13 @@ def test_load_hdf5_invalid_file_raises(self, experiment): # WHEN / THEN EXPECT with pytest.raises(OSError): - experiment.load_hdf5('non_existent_file.h5') + experiment.load_hdf5("non_existent_file.h5") def test_save_hdf5(self, tmp_path, experiment): "Test saving data to an HDF5 file. Load the saved file" - 'using scipp and compare to the original data.' + "using scipp and compare to the original data." # WHEN THEN - filename = tmp_path / 'saved_data.h5' + filename = tmp_path / "saved_data.h5" experiment.save_hdf5(str(filename)) # EXPECT @@ -144,25 +144,25 @@ def test_save_hdf5_default_filename(self, tmp_path, experiment, monkeypatch): experiment.save_hdf5() # EXPECT - expected_filename = tmp_path / f'{experiment.unique_name}.h5' + expected_filename = tmp_path / f"{experiment.unique_name}.h5" loaded_data = sc.io.load_hdf5(str(expected_filename)) original_data = experiment.data assert sc.identical(original_data, loaded_data) def test_save_hdf5_no_data_raises(self): "Test saving data to an HDF5 file when no data is present" - 'in the experiment' + "in the experiment" # WHEN experiment = Experiment() # THEN EXPECT with pytest.raises(ValueError): - experiment.save_hdf5('should_fail.h5') + experiment.save_hdf5("should_fail.h5") def test_save_hdf5_invalid_filename_raises(self, experiment): "Test saving data to an HDF5 file with an invalid filename" # WHEN / THEN EXPECT - with pytest.raises(TypeError, match='must be a string'): + with pytest.raises(TypeError, match="must be a string"): experiment.save_hdf5(123) def test_remove_data(self, experiment): @@ -174,11 +174,11 @@ def test_remove_data(self, experiment): assert experiment._data is None @pytest.mark.parametrize( - 'new_Q_bins, new_energy_bins', + "new_Q_bins, new_energy_bins", [ ( - sc.linspace('Q', 0.5, 1.5, num=7, unit='1/Angstrom'), - sc.linspace('energy', -5, 5, num=8, unit='meV'), + sc.linspace("Q", 0.5, 1.5, num=7, unit="1/Angstrom"), + sc.linspace("energy", -5, 5, num=8, unit="meV"), ), ( 6, @@ -189,23 +189,23 @@ def test_remove_data(self, experiment): 7.0, ), ( - sc.linspace('Q', 0.5, 1.5, num=7, unit='1/Angstrom'), + sc.linspace("Q", 0.5, 1.5, num=7, unit="1/Angstrom"), 7, ), ], - ids=['sc_bins', 'integers_bins', 'float_bins', 'mixed_bins'], + ids=["sc_bins", "integers_bins", "float_bins", "mixed_bins"], ) def test_rebin(self, experiment, new_Q_bins, new_energy_bins): "Test rebinning data in the experiment" # WHEN # THEN - experiment.rebin({'Q': new_Q_bins, 'energy': new_energy_bins}) + experiment.rebin({"Q": new_Q_bins, "energy": new_energy_bins}) # EXPECT rebinned_data = experiment.binned_data - assert rebinned_data.sizes['Q'] == 6 - assert rebinned_data.sizes['energy'] == 7 + assert rebinned_data.sizes["Q"] == 6 + assert rebinned_data.sizes["energy"] == 7 def test_rebin_no_data_raises(self): "Test rebinning data when no data is present" @@ -214,34 +214,34 @@ def test_rebin_no_data_raises(self): # THEN EXPECT with pytest.raises(ValueError): - experiment.rebin({'Q': 6, 'energy': 7}) + experiment.rebin({"Q": 6, "energy": 7}) def test_rebin_invalid_dimensions_raises(self, experiment): "Test rebinning data with invalid dimensions" # WHEN / THEN EXPECT with pytest.raises(TypeError): - experiment.rebin('invalid_dimensions') + experiment.rebin("invalid_dimensions") def test_rebin_invalid_dimension_name_raises(self, experiment): "Test rebinning data with invalid dimension name" # WHEN / THEN EXPECT - with pytest.raises(TypeError, match='Dimension keys must be strings'): - experiment.rebin({123: 6, 'energy': 7}) + with pytest.raises(TypeError, match="Dimension keys must be strings"): + experiment.rebin({123: 6, "energy": 7}) def test_rebin_dimension_not_in_data_raises(self, experiment): "Test rebinning data with a dimension not in the data" # WHEN / THEN EXPECT with pytest.raises(KeyError, match="Dimension 'time' not a valid"): - experiment.rebin({'time': 6, 'energy': 7}) + experiment.rebin({"time": 6, "energy": 7}) def test_rebin_invalid_bin_values_raises(self, experiment): "Test rebinning data with invalid bin values" # WHEN / THEN EXPECT with pytest.raises( TypeError, - match='Dimension values must be integers or', + match="Dimension values must be integers or", ): - experiment.rebin({'Q': [0.5, 1.0, 1.5], 'energy': 7}) + experiment.rebin({"Q": [0.5, 1.0, 1.5], "energy": 7}) ############## # test setters and getters @@ -271,24 +271,6 @@ def test_Q_setter_raises(self, experiment): with pytest.raises(AttributeError): experiment.Q = experiment.Q - def test_Q_getter_warns_no_data(self): - "Test that getting Q data with no data raises Warning" - # WHEN - experiment = Experiment() - - # THEN EXPECT - with pytest.warns(UserWarning, match='No data loaded'): - _ = experiment.Q - - def test_energy_getter_warns_no_data(self): - "Test that getting energy data with no data raises Warning" - # WHEN - experiment = Experiment() - - # THEN EXPECT - with pytest.warns(UserWarning, match='No data loaded'): - _ = experiment.energy - ############## # test plotting ############## @@ -297,9 +279,9 @@ def test_plot_data_success(self, experiment): "Test plotting data successfully when in notebook environment" # WHEN with ( - patch.object(Experiment, '_in_notebook', return_value=True), - patch('plopp.plot') as mock_plot, - patch('IPython.display.display') as mock_display, + patch(f"{Experiment.__module__}._in_notebook", return_value=True), + patch("plopp.plot") as mock_plot, + patch("IPython.display.display") as mock_display, ): mock_fig = MagicMock() mock_plot.return_value = mock_fig @@ -311,7 +293,7 @@ def test_plot_data_success(self, experiment): mock_plot.assert_called_once() args, kwargs = mock_plot.call_args assert sc.identical(args[0], experiment._data.transpose()) - assert kwargs['title'] == f'{experiment.display_name}' + assert kwargs["title"] == f"{experiment.display_name}" mock_display.assert_called_once_with(mock_fig) def test_plot_data_no_data_raises(self): @@ -320,18 +302,18 @@ def test_plot_data_no_data_raises(self): experiment = Experiment() # THEN EXPECT - with pytest.raises(ValueError, match='No data to plot'): + with pytest.raises(ValueError, match="No data to plot"): experiment.plot_data() def test_plot_data_not_in_notebook_raises(self, experiment): "Test plotting data raises RuntimeError" - 'when not in notebook environment' + "when not in notebook environment" # WHEN - with patch.object(Experiment, '_in_notebook', return_value=False): + with patch(f"{Experiment.__module__}._in_notebook", return_value=False): # THEN EXPECT with pytest.raises( RuntimeError, - match='plot_data\\(\\) can only be used in a Jupyter notebook environment', + match="plot_data\\(\\) can only be used in a Jupyter notebook environment", ): experiment.plot_data() @@ -339,62 +321,6 @@ def test_plot_data_not_in_notebook_raises(self, experiment): # test private methods ############## - def test_in_notebook_returns_true_for_jupyter(self, monkeypatch): - """Should return True when IPython shell is - ZMQInteractiveShell (Jupyter).""" - - # WHEN - class ZMQInteractiveShell: - __name__ = 'ZMQInteractiveShell' - - # THEN - monkeypatch.setattr('IPython.get_ipython', lambda: ZMQInteractiveShell()) - - # EXPECT - assert Experiment._in_notebook() is True - - def test_in_notebook_returns_false_for_terminal_ipython(self, monkeypatch): - """Should return False when IPython shell is - TerminalInteractiveShell.""" - - # WHEN - class TerminalInteractiveShell: - __name__ = 'TerminalInteractiveShell' - - # THEN - - monkeypatch.setattr('IPython.get_ipython', lambda: TerminalInteractiveShell()) - - # EXPECT - assert Experiment._in_notebook() is False - - def test_in_notebook_returns_false_for_unknown_shell(self, monkeypatch): - """Should return False when IPython shell type is - unrecognized.""" - - # WHEN - class UnknownShell: - __name__ = 'UnknownShell' - - # THEN - monkeypatch.setattr('IPython.get_ipython', lambda: UnknownShell()) - # EXPECT - assert Experiment._in_notebook() is False - - def test_in_notebook_returns_false_when_no_ipython(self, monkeypatch): - """Should return False when IPython is not installed or - available.""" - - # WHEN - def raise_import_error(*args, **kwargs): - raise ImportError - - # THEN - monkeypatch.setattr('builtins.__import__', raise_import_error) - - # EXPECT - assert Experiment._in_notebook() is False - def test_validate_coordinates(self, experiment): "Test that _validate_coordinates does not raise for valid data" # WHEN / THEN EXPECT @@ -402,40 +328,42 @@ def test_validate_coordinates(self, experiment): def test_validate_coordinates_raises_missing_Q(self, experiment): "Test that _validate_coordinates raises ValueError when Q coord" - 'is missing' + "is missing" # WHEN invalid_data = experiment._data.copy() - invalid_data.coords.pop('Q') + invalid_data.coords.pop("Q") # THEN EXPECT - with pytest.raises(ValueError, match='missing required coordinate'): + with pytest.raises(ValueError, match="missing required coordinate"): experiment._validate_coordinates(invalid_data) def test_validate_coordinates_raises_missing_energy(self, experiment): "Test that _validate_coordinates raises ValueError when energy" - 'coord is missing' + "coord is missing" # WHEN invalid_data = experiment._data.copy() - invalid_data.coords.pop('energy') + invalid_data.coords.pop("energy") # THEN EXPECT - with pytest.raises(ValueError, match='missing required coordinate'): + with pytest.raises(ValueError, match="missing required coordinate"): experiment._validate_coordinates(invalid_data) def test_validate_coordinates_raises_not_DataArray(self): "Test that _validate_coordinates raises TypeError when data is" - 'not a Scipp DataArray' + "not a Scipp DataArray" # WHEN THEN EXPECT - with pytest.raises(TypeError, match='must be a'): - Experiment()._validate_coordinates('not_a_data_array') + with pytest.raises(TypeError, match="must be a"): + Experiment()._validate_coordinates("not_a_data_array") def test_convert_to_bin_centers(self, experiment): "Test that _convert_to_bin_centers converts edges to centers" # WHEN - Q_edges = sc.linspace('Q', 0.0, 2.0, num=11, unit='1/Angstrom') - energy_edges = sc.linspace('energy', -6, 6, num=13, unit='meV') - values = sc.array(dims=['Q', 'energy'], values=np.ones((10, 12))) - binned_data = sc.DataArray(data=values, coords={'Q': Q_edges, 'energy': energy_edges}) + Q_edges = sc.linspace("Q", 0.0, 2.0, num=11, unit="1/Angstrom") + energy_edges = sc.linspace("energy", -6, 6, num=13, unit="meV") + values = sc.array(dims=["Q", "energy"], values=np.ones((10, 12))) + binned_data = sc.DataArray( + data=values, coords={"Q": Q_edges, "energy": energy_edges} + ) # THEN experiment._data = binned_data # Set data to avoid warnings @@ -445,8 +373,8 @@ def test_convert_to_bin_centers(self, experiment): expected_Q = 0.5 * (Q_edges[:-1] + Q_edges[1:]) expected_energy = 0.5 * (energy_edges[:-1] + energy_edges[1:]) - assert sc.identical(converted_data.coords['Q'], expected_Q) - assert sc.identical(converted_data.coords['energy'], expected_energy) + assert sc.identical(converted_data.coords["Q"], expected_Q) + assert sc.identical(converted_data.coords["energy"], expected_energy) assert sc.identical(converted_data.data, binned_data.data) ############## @@ -458,12 +386,15 @@ def test_repr(self, experiment): repr_str = repr(experiment) # THEN EXPECT - assert repr_str == f'Experiment `{experiment.unique_name}` with data: {experiment._data}' + assert ( + repr_str + == f"Experiment `{experiment.unique_name}` with data: {experiment._data}" + ) def test_copy_experiment(self, experiment): "Test copying an Experiment object." - 'The copied object should have the same attributes ' - 'but be a different object in memory.' + "The copied object should have the same attributes " + "but be a different object in memory." # WHEN copied_experiment = copy(experiment) diff --git a/tests/unit/easydynamics/sample_model/test_model_base.py b/tests/unit/easydynamics/sample_model/test_model_base.py index 05591735..692d7e42 100644 --- a/tests/unit/easydynamics/sample_model/test_model_base.py +++ b/tests/unit/easydynamics/sample_model/test_model_base.py @@ -16,26 +16,26 @@ class TestModelBase: @pytest.fixture def model_base(self): component1 = Gaussian( - display_name='TestGaussian1', + display_name="TestGaussian1", area=1.0, center=0.0, width=1.0, - unit='meV', + unit="meV", ) component2 = Lorentzian( - display_name='TestLorentzian1', + display_name="TestLorentzian1", area=2.0, center=1.0, width=0.5, - unit='meV', + unit="meV", ) component_collection = ComponentCollection() component_collection.append_component(component1) component_collection.append_component(component2) model_base = ModelBase( - display_name='InitModel', + display_name="InitModel", components=component_collection, - unit='meV', + unit="meV", Q=np.array([1.0, 2.0, 3.0]), ) @@ -46,8 +46,8 @@ def test_init(self, model_base): model = model_base # EXPECT - assert model.display_name == 'InitModel' - assert model.unit == 'meV' + assert model.display_name == "InitModel" + assert model.unit == "meV" assert len(model.components) == 2 np.testing.assert_array_equal(model.Q, np.array([1.0, 2.0, 3.0])) @@ -55,9 +55,9 @@ def test_init_raises_with_invalid_components(self): # WHEN / THEN / EXPECT with pytest.raises( TypeError, - match='Components must be ', + match="Components must be ", ): - ModelBase(components='invalid_component') + ModelBase(components="invalid_component") def test_evaluate_calls_all_component_collections(self, model_base): # WHEN @@ -88,7 +88,7 @@ def test_evaluate_no_component_collections_raises(self, model_base): model_base._component_collections = [] # THEN / EXPECT - with pytest.raises(ValueError, match='No components'): + with pytest.raises(ValueError, match="No components"): model_base.evaluate(x) def test_generate_component_collections_with_Q(self, model_base): @@ -101,17 +101,9 @@ def test_generate_component_collections_with_Q(self, model_base): assert isinstance(collection, ComponentCollection) assert len(collection.components) == 2 assert isinstance(collection.components[0], Gaussian) - assert collection.components[0].display_name == 'TestGaussian1' + assert collection.components[0].display_name == "TestGaussian1" assert isinstance(collection.components[1], Lorentzian) - assert collection.components[1].display_name == 'TestLorentzian1' - - def test_generate_component_collections_without_Q_warns(self, model_base): - # WHEN - model_base._Q = None - - # THEN / EXPECT - with pytest.warns(UserWarning, match='Q is not set'): - model_base._generate_component_collections() + assert collection.components[1].display_name == "TestLorentzian1" def test_fix_free_all_parameters(self, model_base): # WHEN @@ -134,12 +126,12 @@ def test_get_all_variables(self, model_base): # THEN expected_var_display_names = { - 'TestGaussian1 area', - 'TestGaussian1 center', - 'TestGaussian1 width', - 'TestLorentzian1 area', - 'TestLorentzian1 center', - 'TestLorentzian1 width', + "TestGaussian1 area", + "TestGaussian1 center", + "TestGaussian1 width", + "TestLorentzian1 area", + "TestLorentzian1 center", + "TestLorentzian1 width", } retrieved_var_display_names = {var.display_name for var in all_vars} @@ -153,12 +145,12 @@ def test_get_all_variables_with_Q_index(self, model_base): # THEN expected_var_display_names = { - 'TestGaussian1 area', - 'TestGaussian1 center', - 'TestGaussian1 width', - 'TestLorentzian1 area', - 'TestLorentzian1 center', - 'TestLorentzian1 width', + "TestGaussian1 area", + "TestGaussian1 center", + "TestGaussian1 width", + "TestLorentzian1 area", + "TestLorentzian1 center", + "TestLorentzian1 width", } retrieved_var_display_names = {var.display_name for var in all_vars} @@ -170,7 +162,7 @@ def test_get_all_variables_with_invalid_Q_index_raises(self, model_base): # WHEN / THEN / EXPECT with pytest.raises( IndexError, - match='Q_index 5 is out of bounds for component collections of length 3', + match="Q_index 5 is out of bounds for component collections of length 3", ): model_base.get_all_variables(Q_index=5) @@ -178,13 +170,13 @@ def test_get_all_variables_with_nonint_Q_index_raises(self, model_base): # WHEN / THEN / EXPECT with pytest.raises( TypeError, - match='Q_index must be an int or None, got str', + match="Q_index must be an int or None, got str", ): - model_base.get_all_variables(Q_index='invalid_index') + model_base.get_all_variables(Q_index="invalid_index") def test_append_and_remove_and_clear_component(self, model_base): # WHEN - new_component = Gaussian(unique_name='NewGaussian') + new_component = Gaussian(unique_name="NewGaussian") # THEN model_base.append_component(new_component) @@ -194,7 +186,7 @@ def test_append_and_remove_and_clear_component(self, model_base): assert model_base.components[-1] is new_component # THEN - model_base.remove_component('NewGaussian') + model_base.remove_component("NewGaussian") # EXPECT assert len(model_base.components) == 2 @@ -223,38 +215,40 @@ def test_append_component_collection(self, model_base): def test_append_component_invalid_type_raises(self, model_base): # WHEN / THEN / EXPECT - with pytest.raises(TypeError, match=' must be a ModelComponent or ComponentCollection'): - model_base.append_component('invalid_component') + with pytest.raises( + TypeError, match=" must be a ModelComponent or ComponentCollection" + ): + model_base.append_component("invalid_component") def test_unit_property(self, model_base): # WHEN unit = model_base.unit # THEN / EXPECT - assert unit == 'meV' + assert unit == "meV" def test_unit_setter_raises(self, model_base): # WHEN / THEN / EXPECT - with pytest.raises(AttributeError, match='Use convert_unit to change '): - model_base.unit = 'K' + with pytest.raises(AttributeError, match="Use convert_unit to change "): + model_base.unit = "K" def test_convert_unit(self, model_base): # WHEN - model_base.convert_unit('eV') + model_base.convert_unit("eV") # THEN / EXPECT - assert model_base.unit == 'eV' + assert model_base.unit == "eV" for component in model_base.components: - assert component.unit == 'eV' + assert component.unit == "eV" def test_convert_unit_invalid_raises(self, model_base): # WHEN / THEN / EXPECT with pytest.raises(Exception): - model_base.convert_unit('invalid_unit') + model_base.convert_unit("invalid_unit") def test_components_setter(self, model_base): # WHEN - new_component = Lorentzian(unique_name='NewLorentzian') + new_component = Lorentzian(unique_name="NewLorentzian") model_base.components = new_component # THEN / EXPECT @@ -280,9 +274,9 @@ def test_components_setter_invalid_raises(self, model_base): # WHEN / THEN / EXPECT with pytest.raises( TypeError, - match='Components must be ', + match="Components must be ", ): - model_base.components = 'invalid_component' + model_base.components = "invalid_component" def test_Q_setter(self, model_base): # WHEN @@ -297,7 +291,7 @@ def test_repr(self, model_base): repr_str = repr(model_base) # THEN / EXPECT - assert 'unique_name' in repr_str - assert 'unit' in repr_str - assert 'Q = ' in repr_str - assert 'components = ' in repr_str + assert "unique_name" in repr_str + assert "unit" in repr_str + assert "Q = " in repr_str + assert "components = " in repr_str diff --git a/tests/unit/easydynamics/utils/test_utils.py b/tests/unit/easydynamics/utils/test_utils.py index 97a6c36c..76c967c8 100644 --- a/tests/unit/easydynamics/utils/test_utils.py +++ b/tests/unit/easydynamics/utils/test_utils.py @@ -5,13 +5,14 @@ import pytest import scipp as sc +from easydynamics.utils.utils import _in_notebook from easydynamics.utils.utils import _validate_and_convert_Q from easydynamics.utils.utils import _validate_unit class TestValidateAndConvertQ: @pytest.mark.parametrize( - 'Q_input, expected', + "Q_input, expected", [ (1.0, np.array([1.0])), (2, np.array([2])), @@ -29,7 +30,7 @@ def test_validate_and_convert_Q_numeric_and_array(self, Q_input, expected): def test_validate_and_convert_Q_scipp_variable(self): # WHEN - Q = sc.array(dims=['Q'], values=[1.0, 2.0], unit='1/angstrom') + Q = sc.array(dims=["Q"], values=[1.0, 2.0], unit="1/angstrom") # THEN result = _validate_and_convert_Q(Q) @@ -43,29 +44,29 @@ def test_validate_and_convert_Q_none(self): assert _validate_and_convert_Q(None) is None @pytest.mark.parametrize( - 'Q_input', + "Q_input", [ - 'invalid', - {'a': 1}, + "invalid", + {"a": 1}, (1, 2), object(), ], ) def test_validate_and_convert_Q_invalid_type(self, Q_input): # WHEN THEN EXPECT - with pytest.raises(TypeError, match='Q must be a number'): + with pytest.raises(TypeError, match="Q must be a number"): _validate_and_convert_Q(Q_input) def test_validate_and_convert_Q_ndarray_wrong_dim(self): # WHEN THEN Q = np.array([[1.0, 2.0]]) # EXPECT - with pytest.raises(ValueError, match='Q must be a 1-dimensional array'): + with pytest.raises(ValueError, match="Q must be a 1-dimensional array"): _validate_and_convert_Q(Q) def test_validate_and_convert_Q_scipp_wrong_dims(self): # WHEN THEN - Q = sc.array(dims=['x'], values=[1.0, 2.0], unit='1/angstrom') + Q = sc.array(dims=["x"], values=[1.0, 2.0], unit="1/angstrom") # EXPECT with pytest.raises(ValueError, match="single dimension named 'Q'"): @@ -77,12 +78,12 @@ def test_validate_and_convert_Q_scipp_wrong_dims(self): class TestValidateUnit: @pytest.mark.parametrize( - 'unit_input', + "unit_input", [ None, - '1/angstrom', - 'meV', - sc.Unit('meV'), + "1/angstrom", + "meV", + sc.Unit("meV"), ], ) def test_validate_unit_valid(self, unit_input): @@ -94,13 +95,13 @@ def test_validate_unit_valid(self, unit_input): assert isinstance(unit, sc.Unit) def test_validate_unit_string_conversion(self): - unit = _validate_unit('meV') + unit = _validate_unit("meV") assert isinstance(unit, sc.Unit) - assert unit == sc.Unit('meV') + assert unit == sc.Unit("meV") @pytest.mark.parametrize( - 'unit_input', + "unit_input", [ 123, 45.6, @@ -110,5 +111,69 @@ def test_validate_unit_string_conversion(self): ], ) def test_validate_unit_invalid_type(self, unit_input): - with pytest.raises(TypeError, match='unit must be None, a string, or a scipp Unit'): + with pytest.raises( + TypeError, match="unit must be None, a string, or a scipp Unit" + ): _validate_unit(unit_input) + + +# ----------------------------- + + +class TestInNotebook: + + def test_in_notebook_returns_true_for_jupyter(self, monkeypatch): + """Should return True when IPython shell is + ZMQInteractiveShell (Jupyter).""" + + # WHEN + class ZMQInteractiveShell: + __name__ = "ZMQInteractiveShell" + + # THEN + monkeypatch.setattr("IPython.get_ipython", lambda: ZMQInteractiveShell()) + + # EXPECT + assert _in_notebook() is True + + def test_in_notebook_returns_false_for_terminal_ipython(self, monkeypatch): + """Should return False when IPython shell is + TerminalInteractiveShell.""" + + # WHEN + class TerminalInteractiveShell: + __name__ = "TerminalInteractiveShell" + + # THEN + + monkeypatch.setattr("IPython.get_ipython", lambda: TerminalInteractiveShell()) + + # EXPECT + assert _in_notebook() is False + + def test_in_notebook_returns_false_for_unknown_shell(self, monkeypatch): + """Should return False when IPython shell type is + unrecognized.""" + + # WHEN + class UnknownShell: + __name__ = "UnknownShell" + + # THEN + monkeypatch.setattr("IPython.get_ipython", lambda: UnknownShell()) + # EXPECT + assert _in_notebook() is False + + def test_in_notebook_returns_false_when_no_ipython(self, monkeypatch): + """Should return False when IPython is not installed or + available.""" + + # WHEN + def raise_import_error(*args, **kwargs): + raise ImportError + + # THEN + monkeypatch.setattr("builtins.__import__", raise_import_error) + + # EXPECT + assert _in_notebook() is False From c85bdb0ddebc31dbc23234ac36d4892ffc3137d8 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Thu, 5 Feb 2026 20:30:14 +0100 Subject: [PATCH 15/17] Instrument model (#94) * initial instrument model * first draft of analysis * add test of model base * small changes * tests * clear notebook * respond to PR comments * Update resolution_model docstring for clarity --- .../easydynamics/sample_model/test_model_base.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/unit/easydynamics/sample_model/test_model_base.py b/tests/unit/easydynamics/sample_model/test_model_base.py index 692d7e42..0a5ec3f5 100644 --- a/tests/unit/easydynamics/sample_model/test_model_base.py +++ b/tests/unit/easydynamics/sample_model/test_model_base.py @@ -120,6 +120,21 @@ def test_fix_free_all_parameters(self, model_base): for par in model_base.get_all_variables(): assert par.fixed is False + def test_fix_free_all_parameters(self, model_base): + # WHEN + model_base.fix_all_parameters() + + # THEN + for par in model_base.get_all_variables(): + assert par.fixed is True + + # WHEN + model_base.free_all_parameters() + + # THEN + for par in model_base.get_all_variables(): + assert par.fixed is False + def test_get_all_variables(self, model_base): # WHEN all_vars = model_base.get_all_variables() From a99d5b86933dee3f9ad8fab4c372db9e102e4431 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Tue, 3 Feb 2026 10:46:07 +0100 Subject: [PATCH 16/17] initial analysis class --- docs/docs/tutorials/analysis.ipynb | 108 ++++++++++++++++++ .../convolution/convolution_base.py | 2 + src/easydynamics/sample_model/__init__.py | 18 +++ 3 files changed, 128 insertions(+) diff --git a/docs/docs/tutorials/analysis.ipynb b/docs/docs/tutorials/analysis.ipynb index 3da1411c..27b0cdb6 100644 --- a/docs/docs/tutorials/analysis.ipynb +++ b/docs/docs/tutorials/analysis.ipynb @@ -5,10 +5,14 @@ "id": "8643b10c", "metadata": {}, "source": [ +<<<<<<< HEAD "# Analysis\n", "It is time to analyse some data. We here show how to set up an Analysis object and use it to first fit an artificial vanadium measurements, and next an artificial measurement of a model with diffusion and some elastic scattering.\n", "\n", "In the near future, it will be possible to fit the width and area of the Lorentzian to the diffusion model, as well as fitting the diffusion model directly to the data." +======= + "asd" +>>>>>>> 7b7cf5e (initial analysis class) ] }, { @@ -24,15 +28,22 @@ "from easydynamics.experiment import Experiment\n", "from easydynamics.sample_model import ComponentCollection\n", "from easydynamics.sample_model import DeltaFunction\n", +<<<<<<< HEAD "from easydynamics.sample_model import Lorentzian\n", +======= +>>>>>>> 7b7cf5e (initial analysis class) "from easydynamics.sample_model import Gaussian\n", "from easydynamics.sample_model import Polynomial\n", "from easydynamics.sample_model.background_model import BackgroundModel\n", "from easydynamics.sample_model.resolution_model import ResolutionModel\n", "from easydynamics.sample_model.sample_model import SampleModel\n", +<<<<<<< HEAD "from easydynamics.sample_model.instrument_model import InstrumentModel\n", "from easydynamics.analysis.analysis import Analysis\n", "from copy import copy\n", +======= + "\n", +>>>>>>> 7b7cf5e (initial analysis class) "%matplotlib widget" ] }, @@ -50,6 +61,7 @@ { "cell_type": "code", "execution_count": null, +<<<<<<< HEAD "id": "6762faba", "metadata": {}, "outputs": [], @@ -62,11 +74,37 @@ "\n", "res_gauss = Gaussian(width=0.1)\n", "res_gauss.area.fixed=True\n", +======= + "id": "41f842f0", + "metadata": {}, + "outputs": [], + "source": [ + "# Create a diffusion_model and components for the SampleModel\n", + "\n", + "# Creating components\n", + "component_collection = ComponentCollection()\n", + "delta_function = DeltaFunction(display_name='DeltaFunction', area=1)\n", + "gaussian = Gaussian(display_name='Gaussian', width=0.1, center=0.5, area=0.5)\n", + "\n", + "# Adding components to the component collection\n", + "component_collection.append_component(delta_function)\n", + "\n", + "\n", + "sample_model = SampleModel(\n", + " components=component_collection,\n", + " unit='meV',\n", + " display_name='MySampleModel',\n", + ")\n", + "\n", + "res_gauss = Gaussian(width=0.1)\n", + "res_gauss.area.fixed = True\n", +>>>>>>> 7b7cf5e (initial analysis class) "resolution_model = ResolutionModel(components=res_gauss)\n", "\n", "\n", "background_model = BackgroundModel(components=Polynomial(coefficients=[0.001]))\n", "\n", +<<<<<<< HEAD "instrument_model = InstrumentModel(\n", " resolution_model=resolution_model,\n", " background_model=background_model,\n", @@ -187,22 +225,77 @@ "diffusion_analysis.instrument_model._resolution_model = vanadium_analysis.instrument_model.resolution_model\n", "diffusion_analysis.instrument_model.resolution_model.fix_all_parameters()\n", "diffusion_analysis.plot_parameters(names=[\"Gaussian width\"])\n" +======= + "my_analysis = Analysis1d(\n", + " experiment=vanadium_experiment,\n", + " sample_model=sample_model,\n", + " resolution_model=resolution_model,\n", + " background_model=background_model,\n", + " Q_index=5,\n", + ")\n", + "\n", + "my_analysis._update_models()\n", + "\n", + "\n", + "values = my_analysis.calculate()\n", + "sample_values, background_values = my_analysis.calculate_individual_components()\n", + "\n", + "plt.figure()\n", + "plt.plot(my_analysis.energy.values, values, label='Total Model')\n", + "for component_index in range(len(sample_values)):\n", + " plt.plot(\n", + " my_analysis.energy.values,\n", + " sample_values[component_index],\n", + " label=f'Sample Component {component_index}',\n", + " linestyle='--',\n", + " )\n", + "\n", + "for component_index in range(len(background_values)):\n", + " plt.plot(\n", + " my_analysis.energy.values,\n", + " background_values[component_index],\n", + " label=f'Background Component {component_index}',\n", + " linestyle=':',\n", + " )\n", + "plt.xlabel('Energy (meV)')\n", + "plt.ylabel('Intensity')\n", + "plt.title(f'Q index: {5}')\n", + "plt.legend()\n", + "plt.show()" +>>>>>>> 7b7cf5e (initial analysis class) ] }, { "cell_type": "code", "execution_count": null, +<<<<<<< HEAD "id": "c66828eb", "metadata": {}, "outputs": [], "source": [ "# Let us see how good the starting parameters are\n", "diffusion_analysis.plot_data_and_model()" +======= + "id": "6762faba", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "02702f95", + "metadata": {}, + "outputs": [], + "source": [ + "my_analysis.plot_data_and_model()" +>>>>>>> 7b7cf5e (initial analysis class) ] }, { "cell_type": "code", "execution_count": null, +<<<<<<< HEAD "id": "197b44c5", "metadata": {}, "outputs": [], @@ -210,11 +303,19 @@ "# Now we fit the data and plot the result. Looks good!\n", "diffusion_analysis.fit(fit_method=\"independent\")\n", "diffusion_analysis.plot_data_and_model()" +======= + "id": "70091539", + "metadata": {}, + "outputs": [], + "source": [ + "my_analysis.fit()" +>>>>>>> 7b7cf5e (initial analysis class) ] }, { "cell_type": "code", "execution_count": null, +<<<<<<< HEAD "id": "df14b5c4", "metadata": {}, "outputs": [], @@ -232,6 +333,13 @@ "source": [ "# It will be possible to fit this to a DiffusionModel, but that will\n", "# come later." +======= + "id": "2ad6384e", + "metadata": {}, + "outputs": [], + "source": [ + "my_analysis.plot_data_and_model()" +>>>>>>> 7b7cf5e (initial analysis class) ] } ], diff --git a/src/easydynamics/convolution/convolution_base.py b/src/easydynamics/convolution/convolution_base.py index 9c212d64..be5cff06 100644 --- a/src/easydynamics/convolution/convolution_base.py +++ b/src/easydynamics/convolution/convolution_base.py @@ -79,6 +79,8 @@ def __init__( resolution_components = ComponentCollection( components=[resolution_components] ) + if isinstance(resolution_components, ModelComponent): + resolution_components = ComponentCollection(components=[resolution_components]) self._resolution_components = resolution_components @property diff --git a/src/easydynamics/sample_model/__init__.py b/src/easydynamics/sample_model/__init__.py index 443c1982..c8fc2a0e 100644 --- a/src/easydynamics/sample_model/__init__.py +++ b/src/easydynamics/sample_model/__init__.py @@ -9,14 +9,19 @@ from .components import Lorentzian from .components import Polynomial from .components import Voigt +<<<<<<< HEAD from .diffusion_model.brownian_translational_diffusion import ( BrownianTranslationalDiffusion, ) from .instrument_model import InstrumentModel +======= +from .diffusion_model.brownian_translational_diffusion import BrownianTranslationalDiffusion +>>>>>>> 7b7cf5e (initial analysis class) from .resolution_model import ResolutionModel from .sample_model import SampleModel __all__ = [ +<<<<<<< HEAD "ComponentCollection", "Gaussian", "Lorentzian", @@ -29,4 +34,17 @@ "ResolutionModel", "BackgroundModel", "InstrumentModel", +======= + 'ComponentCollection', + 'Gaussian', + 'Lorentzian', + 'Voigt', + 'DeltaFunction', + 'DampedHarmonicOscillator', + 'Polynomial', + 'BrownianTranslationalDiffusion', + 'SampleModel', + 'ResolutionModel', + 'BackgroundModel', +>>>>>>> 7b7cf5e (initial analysis class) ] From 5411db3a1f6eb6ea37951b9d07f74f8ed82e5db4 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Thu, 12 Feb 2026 13:18:32 +0100 Subject: [PATCH 17/17] fix merge conflict --- src/easydynamics/sample_model/__init__.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/easydynamics/sample_model/__init__.py b/src/easydynamics/sample_model/__init__.py index c8fc2a0e..443c1982 100644 --- a/src/easydynamics/sample_model/__init__.py +++ b/src/easydynamics/sample_model/__init__.py @@ -9,19 +9,14 @@ from .components import Lorentzian from .components import Polynomial from .components import Voigt -<<<<<<< HEAD from .diffusion_model.brownian_translational_diffusion import ( BrownianTranslationalDiffusion, ) from .instrument_model import InstrumentModel -======= -from .diffusion_model.brownian_translational_diffusion import BrownianTranslationalDiffusion ->>>>>>> 7b7cf5e (initial analysis class) from .resolution_model import ResolutionModel from .sample_model import SampleModel __all__ = [ -<<<<<<< HEAD "ComponentCollection", "Gaussian", "Lorentzian", @@ -34,17 +29,4 @@ "ResolutionModel", "BackgroundModel", "InstrumentModel", -======= - 'ComponentCollection', - 'Gaussian', - 'Lorentzian', - 'Voigt', - 'DeltaFunction', - 'DampedHarmonicOscillator', - 'Polynomial', - 'BrownianTranslationalDiffusion', - 'SampleModel', - 'ResolutionModel', - 'BackgroundModel', ->>>>>>> 7b7cf5e (initial analysis class) ]