From c05821739d82f839b023a2e26dbfa7605c11e035 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Sun, 10 May 2026 21:11:05 +0200 Subject: [PATCH 01/20] Add name property to ModelComponents, DiffusionModels and ComponentCollection and update tests --- docs/docs/tutorials/components.ipynb | 2 +- .../sample_model/component_collection.py | 58 +++++------------ .../components/damped_harmonic_oscillator.py | 10 ++- .../sample_model/components/delta_function.py | 18 ++++-- .../sample_model/components/exponential.py | 25 +++++--- .../components/expression_component.py | 23 ++++--- .../sample_model/components/gaussian.py | 19 ++++-- .../sample_model/components/lorentzian.py | 19 ++++-- .../components/model_component.py | 10 ++- .../sample_model/components/polynomial.py | 17 +++-- .../sample_model/components/voigt.py | 24 ++++--- .../brownian_translational_diffusion.py | 51 ++++++++++----- .../diffusion_model/diffusion_model_base.py | 19 +++--- .../jump_translational_diffusion.py | 62 +++++++++++++------ .../easydynamics/analysis/test_analysis.py | 40 +++++++----- .../components/test_delta_function.py | 14 +++-- .../components/test_exponential.py | 9 +-- .../sample_model/components/test_gaussian.py | 18 ++++-- .../components/test_lorentzian.py | 15 +++-- .../components/test_polynomial.py | 15 +++-- .../sample_model/components/test_voigt.py | 12 ++-- .../test_brownian_translational_diffusion.py | 9 +++ .../test_jump_translational_diffusion.py | 9 +++ .../sample_model/test_component_collection.py | 22 ++++--- .../sample_model/test_model_base.py | 26 ++++---- 25 files changed, 338 insertions(+), 208 deletions(-) diff --git a/docs/docs/tutorials/components.ipynb b/docs/docs/tutorials/components.ipynb index 55570c35..0996e434 100644 --- a/docs/docs/tutorials/components.ipynb +++ b/docs/docs/tutorials/components.ipynb @@ -168,7 +168,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.13" + "version": "3.14.4" } }, "nbformat": 4, diff --git a/src/easydynamics/sample_model/component_collection.py b/src/easydynamics/sample_model/component_collection.py index f5f1a45a..5e60c834 100644 --- a/src/easydynamics/sample_model/component_collection.py +++ b/src/easydynamics/sample_model/component_collection.py @@ -8,41 +8,44 @@ import numpy as np import scipp as sc -from easyscience.base_classes.model_base import ModelBase from easyscience.variable import DescriptorBase from easyscience.variable import Parameter +from easydynamics.base_classes.easydynamics_modelbase import EasyDynamicsModelBase from easydynamics.sample_model.components.model_component import ModelComponent if TYPE_CHECKING: from easydynamics.utils.utils import Numeric -class ComponentCollection(ModelBase): +class ComponentCollection(EasyDynamicsModelBase): """ Collection of model components representing a sample, background or resolution model. """ def __init__( self, + components: list[ModelComponent] | None = None, unit: str | sc.Unit = 'meV', + name: str = 'ComponentCollection', display_name: str | None = 'MyComponentCollection', unique_name: str | None = None, - components: list[ModelComponent] | None = None, ) -> None: """ Initialize a new ComponentCollection. Parameters ---------- + components : list[ModelComponent] | None, default=None + Initial model components to add to the ComponentCollection. unit : str | sc.Unit, default='meV' Unit of the collection. + name : str, default='ComponentCollection' + Name of the collection. display_name : str | None, default='MyComponentCollection' Display name of the collection. unique_name : str | None, default=None Unique name of the collection. - components : list[ModelComponent] | None, default=None - Initial model components to add to the ComponentCollection. Raises ------ @@ -50,13 +53,13 @@ def __init__( If unit is not a string or sc.Unit, or if components is not a list of ModelComponent. """ - super().__init__(display_name=display_name, unique_name=unique_name) + super().__init__( + unit=unit, + name=name, + display_name=display_name, + unique_name=unique_name, + ) - 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__}' - ) - self._unit = unit self._components = [] # Add initial components if provided. Used for serialization. @@ -142,39 +145,6 @@ def is_empty(self, _value: bool) -> None: 'whether the collection has components.' ) - @property - def unit(self) -> str | sc.Unit | None: - """ - Get the unit of the ComponentCollection. - - Returns - ------- - str | sc.Unit | None - The unit of the ComponentCollection, which is the same as the unit of its components. - """ - return self._unit - - @unit.setter - def unit(self, _unit_str: str) -> None: - """ - Unit is read-only and cannot be set directly. - - Parameters - ---------- - _unit_str : str - The unit to set (ignored). - - Raises - ------ - AttributeError - Always raised since unit is read-only. - """ - - 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.' - ) - def convert_unit(self, unit: str | sc.Unit) -> None: """ Convert the unit of the ComponentCollection and all its components. diff --git a/src/easydynamics/sample_model/components/damped_harmonic_oscillator.py b/src/easydynamics/sample_model/components/damped_harmonic_oscillator.py index d74b9116..17047b53 100644 --- a/src/easydynamics/sample_model/components/damped_harmonic_oscillator.py +++ b/src/easydynamics/sample_model/components/damped_harmonic_oscillator.py @@ -30,6 +30,7 @@ def __init__( center: Numeric | Parameter = 1.0, width: Numeric | Parameter = 1.0, unit: str | sc.Unit = 'meV', + name: str = 'DampedHarmonicOscillator', display_name: str | None = 'DampedHarmonicOscillator', unique_name: str | None = None, ) -> None: @@ -47,6 +48,8 @@ def __init__( default, 1.0. unit : str | sc.Unit, default='meV' Unit of the parameters. + name : str, default='DampedHarmonicOscillator' + Name of the component for indexing. display_name : str | None, default='DampedHarmonicOscillator' Display name of the component. unique_name : str | None, default=None @@ -55,6 +58,7 @@ def __init__( """ super().__init__( + name=name, display_name=display_name, unique_name=unique_name, unit=unit, @@ -217,7 +221,9 @@ def __repr__(self) -> str: A string representation of the Damped Harmonic Oscillator. """ return ( - f'DampedHarmonicOscillator(display_name = {self.display_name}, ' + f'DampedHarmonicOscillator(name = {self.name}, display_name = {self.display_name}, ' f'unit = {self._unit},\n ' - f'area = {self.area},\n center = {self.center},\n width = {self.width})' + f' area = {self.area},\n ' + f' center = {self.center},\n ' + f' width = {self.width})' ) diff --git a/src/easydynamics/sample_model/components/delta_function.py b/src/easydynamics/sample_model/components/delta_function.py index e3720f8b..beeddca7 100644 --- a/src/easydynamics/sample_model/components/delta_function.py +++ b/src/easydynamics/sample_model/components/delta_function.py @@ -33,6 +33,7 @@ def __init__( center: Numeric | Parameter | None = None, area: Numeric | Parameter = 1.0, unit: str | sc.Unit = 'meV', + name: str = 'DeltaFunction', display_name: str | None = 'DeltaFunction', unique_name: str | None = None, ) -> None: @@ -42,11 +43,13 @@ def __init__( Parameters ---------- center : Numeric | Parameter | None, default=None - Center of the delta function. If None. + Center of the delta function. If None, it will be centered at 0 and fixed. area : Numeric | Parameter, default=1.0 Total area under the curve. unit : str | sc.Unit, default='meV' Unit of the parameters. + name : str, default='DeltaFunction' + Name of the component for indexing. display_name : str | None, default='DeltaFunction' Name of the component. unique_name : str | None, default=None @@ -55,15 +58,16 @@ def __init__( """ # Validate inputs and create Parameters if not given super().__init__( - display_name=display_name, unit=unit, + name=name, + display_name=display_name, unique_name=unique_name, ) # These methods live in ValidationMixin - area = self._create_area_parameter(area=area, name=display_name, unit=self._unit) + area = self._create_area_parameter(area=area, name=name, unit=self._unit) center = self._create_center_parameter( - center=center, name=display_name, fix_if_none=True, unit=self._unit + center=center, name=name, fix_if_none=True, unit=self._unit ) self._area = area @@ -196,6 +200,8 @@ def __repr__(self) -> str: """ return ( - f'DeltaFunction(unique_name = {self.unique_name}, unit = {self._unit},\n' - f'area = {self.area},\n center = {self.center})' + f'DeltaFunction(name = {self.name}, display_name = {self.display_name}, ' + f'unit = {self.unit},\n' + f' area = {self.area},\n' + f' center = {self.center})' ) diff --git a/src/easydynamics/sample_model/components/exponential.py b/src/easydynamics/sample_model/components/exponential.py index f06e8138..288e380e 100644 --- a/src/easydynamics/sample_model/components/exponential.py +++ b/src/easydynamics/sample_model/components/exponential.py @@ -29,6 +29,7 @@ def __init__( center: Numeric | Parameter | None = None, rate: Numeric | Parameter = 1.0, unit: str | sc.Unit = 'meV', + name: str = 'Exponential', display_name: str | None = 'Exponential', unique_name: str | None = None, ) -> None: @@ -45,10 +46,12 @@ def __init__( Decay or growth constant of the Exponential. unit : str | sc.Unit, default='meV' Unit of the parameters. + name : str, default='Exponential' + Name of the component for indexing. display_name : str | None, default='Exponential' Name of the component. unique_name : str | None, default=None - Unique name of the component. if None, a unique_name is automatically generated. By + Unique name of the component. If None, a unique_name is automatically generated. By default, None. Raises @@ -60,8 +63,9 @@ def __init__( """ # Validate inputs and create Parameters if not given super().__init__( - display_name=display_name, unit=unit, + name=name, + display_name=display_name, unique_name=unique_name, ) @@ -72,12 +76,10 @@ def __init__( if not np.isfinite(amplitude): raise ValueError('amplitude must be a finite number or a Parameter') - amplitude = Parameter( - name=display_name + ' amplitude', value=float(amplitude), unit=unit - ) + amplitude = Parameter(name=name + ' amplitude', value=float(amplitude), unit=unit) center = self._create_center_parameter( - center=center, name=display_name, fix_if_none=True, unit=self._unit + center=center, name=name, fix_if_none=True, unit=self._unit ) if not isinstance(rate, (Parameter, Numeric)): @@ -87,7 +89,7 @@ def __init__( if not np.isfinite(rate): raise ValueError('rate must be a finite number or a Parameter') - rate = Parameter(name=display_name + ' rate', value=float(rate), unit='1/' + str(unit)) + rate = Parameter(name=name + ' rate', value=float(rate), unit='1/' + str(unit)) self._amplitude = amplitude self._center = center @@ -270,5 +272,10 @@ def __repr__(self) -> str: A string representation of the Exponential. """ - return f'Exponential(unique_name = {self.unique_name}, unit = {self._unit},\n \ - amplitude = {self.amplitude},\n center = {self.center},\n rate = {self.rate})' + return ( + f'Exponential(name = {self.name}, display_name = {self.display_name}, ' + f'unit = {self._unit},\n ' + f' amplitude = {self.amplitude},\n ' + f' center = {self.center},\n ' + f' rate = {self.rate})' + ) diff --git a/src/easydynamics/sample_model/components/expression_component.py b/src/easydynamics/sample_model/components/expression_component.py index 79ce73ef..9e27db8b 100644 --- a/src/easydynamics/sample_model/components/expression_component.py +++ b/src/easydynamics/sample_model/components/expression_component.py @@ -75,6 +75,7 @@ def __init__( expression: str, parameters: dict[str, Numeric] | None = None, unit: str | sc.Unit = 'meV', + name: str = 'Expression', display_name: str | None = 'Expression', unique_name: str | None = None, ) -> None: @@ -89,6 +90,8 @@ def __init__( Dictionary of parameter names and their initial values. unit : str | sc.Unit, default='meV' Unit of the output. + name : str, default='Expression' + Name of the component for indexing. display_name : str | None, default='Expression' Display name for the component. unique_name : str | None, default=None @@ -101,7 +104,7 @@ def __init__( TypeError If any parameter value is not numeric. """ - super().__init__(unit=unit, display_name=display_name, unique_name=unique_name) + super().__init__(unit=unit, name=name, display_name=display_name, unique_name=unique_name) if 'np.' in expression: raise ValueError( @@ -334,12 +337,18 @@ def __dir__(self) -> list[str]: return super().__dir__() + list(self._parameters.keys()) def __repr__(self) -> str: - """Repr function.""" + """ + Repr function. + + Returns + ------- + str + String representation of the ExpressionComponent. + """ param_str = ', '.join(f'{k}={v.value}' for k, v in self._parameters.items()) return ( - f'{self.__class__.__name__}(\n' - f" expr='{self._expression_str}',\n" - f' unit={self._unit},\n' - f' parameters={{ {param_str} }}\n' - f')' + f'ExpressionComponent(name={self.name}, display_name={self.display_name}, ' + f'unit={self._unit},\n' + f" expr='{self._expression_str}',\n" + f' parameters={{ {param_str} }} )' ) diff --git a/src/easydynamics/sample_model/components/gaussian.py b/src/easydynamics/sample_model/components/gaussian.py index 4e9629c1..57bea198 100644 --- a/src/easydynamics/sample_model/components/gaussian.py +++ b/src/easydynamics/sample_model/components/gaussian.py @@ -37,6 +37,7 @@ def __init__( center: Numeric | Parameter | None = None, width: Numeric | Parameter = 1.0, unit: str | sc.Unit = 'meV', + name: str = 'Gaussian', display_name: str | None = 'Gaussian', unique_name: str | None = None, ) -> None: @@ -53,6 +54,8 @@ def __init__( Standard deviation. unit : str | sc.Unit, default='meV' Unit of the parameters. + name : str, default='Gaussian' + Name of the component for indexing. display_name : str | None, default='Gaussian' Name of the component. unique_name : str | None, default=None @@ -61,17 +64,18 @@ def __init__( """ # Validate inputs and create Parameters if not given super().__init__( - display_name=display_name, unit=unit, + name=name, + display_name=display_name, unique_name=unique_name, ) # These methods live in ValidationMixin - area = self._create_area_parameter(area=area, name=display_name, unit=self._unit) + area = self._create_area_parameter(area=area, name=name, unit=self._unit) center = self._create_center_parameter( - center=center, name=display_name, fix_if_none=True, unit=self._unit + center=center, name=name, fix_if_none=True, unit=self._unit ) - width = self._create_width_parameter(width=width, name=display_name, unit=self._unit) + width = self._create_width_parameter(width=width, name=name, unit=self._unit) self._area = area self._center = center @@ -225,6 +229,9 @@ def __repr__(self) -> str: """ return ( - f'Gaussian(unique_name = {self.unique_name}, unit = {self._unit},\n' - f'area = {self.area},\n center = {self.center},\n width = {self.width})' + f'Gaussian(name = {self.name}, display_name = {self.display_name}, ' + f'unit = {self._unit},\n' + f' area = {self.area},\n' + f' center = {self.center},\n' + f' width = {self.width})' ) diff --git a/src/easydynamics/sample_model/components/lorentzian.py b/src/easydynamics/sample_model/components/lorentzian.py index d86b1ed6..f2e62815 100644 --- a/src/easydynamics/sample_model/components/lorentzian.py +++ b/src/easydynamics/sample_model/components/lorentzian.py @@ -34,6 +34,7 @@ def __init__( center: Numeric | Parameter | None = None, width: Numeric | Parameter = 1.0, unit: str | sc.Unit = 'meV', + name: str = 'Lorentzian', display_name: str | None = 'Lorentzian', unique_name: str | None = None, ) -> None: @@ -50,6 +51,8 @@ def __init__( Half width at half maximum (HWHM). unit : str | sc.Unit, default='meV' Unit of the parameters. + name : str, default='Lorentzian' + Name of the component for indexing. display_name : str | None, default='Lorentzian' Name of the component. unique_name : str | None, default=None @@ -58,17 +61,18 @@ def __init__( """ super().__init__( - display_name=display_name, unit=unit, + name=name, + display_name=display_name, unique_name=unique_name, ) # These methods live in ValidationMixin - area = self._create_area_parameter(area=area, name=display_name, unit=self._unit) + area = self._create_area_parameter(area=area, name=name, unit=self._unit) center = self._create_center_parameter( - center=center, name=display_name, fix_if_none=True, unit=self._unit + center=center, name=name, fix_if_none=True, unit=self._unit ) - width = self._create_width_parameter(width=width, name=display_name, unit=self._unit) + width = self._create_width_parameter(width=width, name=name, unit=self._unit) self._area = area self._center = center @@ -216,6 +220,9 @@ def __repr__(self) -> str: A string representation of the Lorentzian. """ return ( - f'Lorentzian(unique_name = {self.unique_name}, unit = {self._unit},\n' - f'area = {self.area},\n center = {self.center},\n width = {self.width})' + f'Lorentzian(name = {self.name}, display_name = {self.display_name}, ' + f'unit = {self._unit},\n' + f' area = {self.area},\n' + f' center = {self.center},\n' + f' width = {self.width})' ) diff --git a/src/easydynamics/sample_model/components/model_component.py b/src/easydynamics/sample_model/components/model_component.py index 6d0f9a0e..52a8992e 100644 --- a/src/easydynamics/sample_model/components/model_component.py +++ b/src/easydynamics/sample_model/components/model_component.py @@ -20,6 +20,7 @@ class ModelComponent(EasyDynamicsModelBase): def __init__( self, unit: str | sc.Unit = 'meV', + name: str = 'ModelComponent', display_name: str | None = None, unique_name: str | None = None, ) -> None: @@ -30,12 +31,19 @@ def __init__( ---------- unit : str | sc.Unit, default='meV' The unit of the model component. + name : str, default='ModelComponent' + The name of the model component for indexing. display_name : str | None, default=None A human-readable name for the component. unique_name : str | None, default=None A unique identifier for the component. """ - super().__init__(unit=unit, display_name=display_name, unique_name=unique_name) + super().__init__( + unit=unit, + name=name, + display_name=display_name, + unique_name=unique_name, + ) self._unit = unit @property diff --git a/src/easydynamics/sample_model/components/polynomial.py b/src/easydynamics/sample_model/components/polynomial.py index cd3cd997..eabfc2e4 100644 --- a/src/easydynamics/sample_model/components/polynomial.py +++ b/src/easydynamics/sample_model/components/polynomial.py @@ -31,6 +31,7 @@ def __init__( self, coefficients: Sequence[Numeric | Parameter] = (0.0,), unit: str | sc.Unit = 'meV', + name: str = 'Polynomial', display_name: str | None = 'Polynomial', unique_name: str | None = None, ) -> None: @@ -43,6 +44,8 @@ def __init__( Coefficients c0, c1, ..., cN. unit : str | sc.Unit, default='meV' Unit of the Polynomial component. + name : str, default='Polynomial' + Name of the component for indexing. display_name : str | None, default='Polynomial' Display name of the Polynomial component. unique_name : str | None, default=None @@ -58,7 +61,12 @@ def __init__( If coefficients is an empty sequence. """ - super().__init__(display_name=display_name, unit=unit, unique_name=unique_name) + super().__init__( + unit=unit, + name=name, + display_name=display_name, + unique_name=unique_name, + ) if not isinstance(coefficients, (list, tuple, np.ndarray)): raise TypeError( @@ -77,7 +85,7 @@ def __init__( if isinstance(coef, Parameter): param = coef elif isinstance(coef, Numeric): - param = Parameter(name=f'{display_name}_c{i}', value=float(coef)) + param = Parameter(name=f'{name}_c{i}', value=float(coef)) else: raise TypeError('Each coefficient must be either a numeric value or a Parameter.') self._coefficients.append(param) @@ -264,6 +272,7 @@ def __repr__(self) -> str: coeffs_str = ', '.join(f'{param.name}={param.value}' for param in self._coefficients) return ( - f'Polynomial(unique_name = {self.unique_name}, ' - f'unit = {self._unit},\n coefficients = [{coeffs_str}])' + f'Polynomial(name = {self.name}, display_name = {self.display_name}, ' + f'unit = {self._unit},\n' + f' coefficients = [{coeffs_str}])' ) diff --git a/src/easydynamics/sample_model/components/voigt.py b/src/easydynamics/sample_model/components/voigt.py index 4aafd03b..11acb544 100644 --- a/src/easydynamics/sample_model/components/voigt.py +++ b/src/easydynamics/sample_model/components/voigt.py @@ -34,6 +34,7 @@ def __init__( gaussian_width: Numeric | Parameter = 1.0, lorentzian_width: Numeric | Parameter = 1.0, unit: str | sc.Unit = 'meV', + name: str = 'Voigt', display_name: str | None = 'Voigt', unique_name: str | None = None, ) -> None: @@ -52,6 +53,8 @@ def __init__( Half width at half max (HWHM) of the Lorentzian part. unit : str | sc.Unit, default='meV' Unit of the parameters. + name : str, default='Voigt' + Name of the component for indexing. display_name : str | None, default='Voigt' Display name of the component. unique_name : str | None, default=None @@ -60,25 +63,26 @@ def __init__( """ super().__init__( - display_name=display_name, unit=unit, + name=name, + display_name=display_name, unique_name=unique_name, ) # These methods live in ValidationMixin - area = self._create_area_parameter(area=area, name=display_name, unit=self._unit) + area = self._create_area_parameter(area=area, name=name, unit=self._unit) center = self._create_center_parameter( - center=center, name=display_name, fix_if_none=True, unit=self._unit + center=center, name=name, fix_if_none=True, unit=self._unit ) gaussian_width = self._create_width_parameter( width=gaussian_width, - name=display_name, + name=name, param_name='gaussian_width', unit=self._unit, ) lorentzian_width = self._create_width_parameter( width=lorentzian_width, - name=display_name, + name=name, param_name='lorentzian_width', unit=self._unit, ) @@ -261,9 +265,9 @@ def __repr__(self) -> str: """ return ( - f'Voigt(unique_name = {self.unique_name}, unit = {self._unit},\n' - f'area = {self.area},\n' - f'center = {self.center},\n' - f'gaussian_width = {self.gaussian_width},\n' - f'lorentzian_width = {self.lorentzian_width})' + f'Voigt(name = {self.name}, display_name = {self.display_name}, unit = {self._unit},\n' + f' area = {self.area},\n' + f' center = {self.center},\n' + f' gaussian_width = {self.gaussian_width},\n' + f' lorentzian_width = {self.lorentzian_width})' ) 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 b3872d9a..41e2743a 100644 --- a/src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py +++ b/src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py @@ -26,7 +26,9 @@ class BrownianTranslationalDiffusion(DiffusionModelBase): have units of 1/angstrom. Creates ComponentCollections with Lorentzian components for given Q-values. - Example: >>>Q=np.linspace(0.5,2,7) >>>energy=np.linspace(-2, 2, 501) >>>scale=1.0 + Examples + -------- + >>>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=BrownianTranslationalDiffusion(display_name="DiffusionModel", >>>scale=scale, diffusion_coefficient= diffusion_coefficient) @@ -36,28 +38,31 @@ class BrownianTranslationalDiffusion(DiffusionModelBase): def __init__( self, - display_name: str | None = 'BrownianTranslationalDiffusion', - unique_name: str | None = None, - unit: str | sc.Unit = 'meV', scale: Numeric = 1.0, diffusion_coefficient: Numeric = 1.0, + unit: str | sc.Unit = 'meV', + name: str = 'BrownianTranslationalDiffusion', + display_name: str | None = 'BrownianTranslationalDiffusion', + unique_name: str | None = None, ) -> None: """ Initialize a new BrownianTranslationalDiffusion model. Parameters ---------- + scale : Numeric, default=1.0 + Scale factor for the diffusion model. Must be a non-negative number. + diffusion_coefficient : Numeric, default=1.0 + Diffusion coefficient D in m^2/s. + unit : str | sc.Unit, default='meV' + Unit of the diffusion model. Must be convertible to meV. + name : str, default='BrownianTranslationalDiffusion' + Name of the diffusion model. display_name : str | None, default='BrownianTranslationalDiffusion' Display name of the diffusion model. unique_name : str | None, default=None Unique name of the diffusion model. If None, a unique name will be generated. By default, None. - unit : str | sc.Unit, default='meV' - Unit of the diffusion model. Must be convertible to meV. - scale : Numeric, default=1.0 - Scale factor for the diffusion model. Must be a non-negative number. - diffusion_coefficient : Numeric, default=1.0 - Diffusion coefficient D in m^2/s. Raises ------ @@ -78,10 +83,11 @@ def __init__( min=0.0, ) super().__init__( - display_name=display_name, - unique_name=unique_name, unit=unit, scale=scale, + name=name, + display_name=display_name, + unique_name=unique_name, ) self._hbar = hbar self._angstrom = angstrom @@ -191,6 +197,7 @@ def calculate_QISF(self, Q: Q_type) -> np.ndarray: def create_component_collections( self, Q: Q_type, + component_name: str = 'Brownian diffusion', component_display_name: str = 'Brownian diffusion', ) -> list[ComponentCollection]: r""" @@ -201,13 +208,15 @@ def create_component_collections( ---------- Q : Q_type Scattering vector values. + component_name : str, default='Brownian diffusion' + Name of the Brownian diffusion component. component_display_name : str, default='Brownian diffusion' - Name of the Lorentzian component. + Display name of the Brownian diffusion component. Raises ------ TypeError - If component_display_name is not a string. + If component_display_name is not a string. If component_name is not a string. Returns ------- @@ -219,6 +228,9 @@ def create_component_collections( Q = _validate_and_convert_Q(Q) if not isinstance(component_display_name, str): + raise TypeError('component_display_name must be a string.') + + if not isinstance(component_name, str): raise TypeError('component_name must be a string.') component_collection_list = [None] * len(Q) @@ -231,10 +243,13 @@ def create_component_collections( # 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 + name=f'{self.name}_Q{Q_value:.2f}', + display_name=f'{self.display_name}_Q{Q_value:.2f}', + unit=self.unit, ) lorentzian_component = Lorentzian( + name=component_name, display_name=component_display_name, unit=self.unit, ) @@ -356,6 +371,8 @@ def __repr__(self) -> str: String representation of the BrownianTranslationalDiffusion model. """ return ( - f'BrownianTranslationalDiffusion(display_name={self.display_name},' - f'diffusion_coefficient={self.diffusion_coefficient}, scale={self.scale})' + f'BrownianTranslationalDiffusion(name={self.name}, ' + f'display_name={self.display_name}, \n' + f' diffusion_coefficient={self.diffusion_coefficient}, \n' + f' scale={self.scale})' ) 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 0e83351e..36d3d644 100644 --- a/src/easydynamics/sample_model/diffusion_model/diffusion_model_base.py +++ b/src/easydynamics/sample_model/diffusion_model/diffusion_model_base.py @@ -15,25 +15,28 @@ class DiffusionModelBase(EasyDynamicsModelBase): def __init__( self, - display_name: str | None = 'MyDiffusionModel', - unique_name: str | None = None, scale: Numeric = 1.0, unit: str | sc.Unit = 'meV', + name: str = 'DiffusionModel', + display_name: str | None = 'MyDiffusionModel', + unique_name: str | None = None, ) -> None: """ Initialize a new DiffusionModel. Parameters ---------- + scale : Numeric, default=1.0 + Scale factor for the diffusion model. Must be a non-negative number. + unit : str | sc.Unit, default='meV' + Unit of the diffusion model. Must be convertible to meV. + name : str, default='DiffusionModel' + Name of the diffusion model. display_name : str | None, default='MyDiffusionModel' Display name of the diffusion model. unique_name : str | None, default=None Unique name of the diffusion model. If None, a unique name will be generated. By default, None. - scale : Numeric, default=1.0 - Scale factor for the diffusion model. Must be a non-negative number. - unit : str | sc.Unit, default='meV' - Unit of the diffusion model. Must be convertible to meV. Raises ------ @@ -56,7 +59,7 @@ def __init__( scale = Parameter(name='scale', value=float(scale), fixed=False, min=0.0, unit=unit) - super().__init__(display_name=display_name, unique_name=unique_name, unit=unit) + super().__init__(unit=unit, name=name, display_name=display_name, unique_name=unique_name) self._scale = scale # ------------------------------------------------------------------ @@ -97,7 +100,7 @@ def scale(self, scale: Numeric) -> None: if float(scale) < 0: raise ValueError('scale must be non-negative.') - self._scale.value = scale + self._scale.value = float(scale) # ------------------------------------------------------------------ # dunder methods 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 346ce152..0d4b4441 100644 --- a/src/easydynamics/sample_model/diffusion_model/jump_translational_diffusion.py +++ b/src/easydynamics/sample_model/diffusion_model/jump_translational_diffusion.py @@ -29,40 +29,53 @@ class JumpTranslationalDiffusion(DiffusionModelBase): units of 1/angstrom. Creates ComponentCollections with Lorentzian components for given Q-values. - Example: >>> Q = np.linspace(0.5, 2, 7) >>> energy = np.linspace(-2, 2, 501) >>> scale = 1.0 - >>> diffusion_coefficient = 2.4e-9 # m^2/s >>> relaxation_time = 1.0 # ps >>> - diffusion_model=JumpTranslationalDiffusion( >>> scale = scale, diffusion_coefficient = - (diffusion_coefficient,) >>> relaxation_time=relaxation_time) >>> component_collections= >>> - diffusion_model.create_component_collections(Q) See also the tutorials.. + Examples + -------- + >>> Q = np.linspace(0.5, 2, 7) + >>> energy = np.linspace(-2, 2, 501) + >>> scale = 1.0 + >>> diffusion_coefficient = 2.4e-9 # m^2/s + >>> relaxation_time = 1.0 # ps + >>> diffusion_model = JumpTranslationalDiffusion( + ... scale=scale, + ... diffusion_coefficient=diffusion_coefficient, + ... relaxation_time=relaxation_time, + ... ) + >>> component_collections = diffusion_model.create_component_collections(Q) + + See also the tutorials.. """ 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, + unit: str | sc.Unit = 'meV', + name: str = 'JumpTranslationalDiffusion', + display_name: str | None = 'JumpTranslationalDiffusion', + unique_name: str | None = None, ) -> None: """ Initialize a new JumpTranslationalDiffusion model. Parameters ---------- - display_name : str | None, default='JumpTranslationalDiffusion' - Display name of the diffusion model. - unique_name : str | None, default=None - Unique name of the diffusion model. If None, a unique name will be generated. By - default, None. - unit : str | sc.Unit, default='meV' - Unit of the diffusion model. Must be convertible to meV. scale : Numeric, default=1.0 Scale factor for the diffusion model. Must be a non-negative number. diffusion_coefficient : Numeric, default=1.0 Diffusion coefficient D in m^2/s. relaxation_time : Numeric, default=1.0 Relaxation time t in ps. + unit : str | sc.Unit, default='meV' + Unit of the diffusion model. Must be convertible to meV. + name : str, default='JumpTranslationalDiffusion' + Name of the diffusion model. + display_name : str | None, default='JumpTranslationalDiffusion' + Display name of the diffusion model. + unique_name : str | None, default=None + Unique name of the diffusion model. If None, a unique name will be generated. By + default, None. Raises ------ @@ -70,6 +83,7 @@ def __init__( If scale, diffusion_coefficient, or relaxation_time are not numbers. """ super().__init__( + name=name, display_name=display_name, unique_name=unique_name, unit=unit, @@ -251,6 +265,7 @@ def calculate_QISF(self, Q: Q_type) -> np.ndarray: def create_component_collections( self, Q: Q_type, + component_name: str = 'Jump translational diffusion', component_display_name: str = 'Jump translational diffusion', ) -> list[ComponentCollection]: """ @@ -260,13 +275,15 @@ def create_component_collections( ---------- Q : Q_type Scattering vector in 1/angstrom. Can be a single value or an array of values. + component_name : str, default='Jump translational diffusion' + Name of the Jump Diffusion Lorentzian component. component_display_name : str, default='Jump translational diffusion' Name of the Jump Diffusion Lorentzian component. Raises ------ TypeError - If component_display_name is not a string. + If component_display_name is not a string. If component_name is not a string. Returns ------- @@ -276,6 +293,9 @@ def create_component_collections( Q = _validate_and_convert_Q(Q) if not isinstance(component_display_name, str): + raise TypeError('component_display_name must be a string.') + + if not isinstance(component_name, str): raise TypeError('component_name must be a string.') component_collection_list = [None] * len(Q) @@ -288,10 +308,13 @@ 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 + name=f'{self.name}_Q{Q_value:.2f}', + display_name=f'{self.display_name}_Q{Q_value:.2f}', + unit=self.unit, ) lorentzian_component = Lorentzian( + name=component_name, display_name=component_display_name, unit=self.unit, ) @@ -415,6 +438,7 @@ def __repr__(self) -> str: String representation of the JumpTranslationalDiffusion model. """ return ( - f'JumpTranslationalDiffusion(display_name={self.display_name}, ' - f'diffusion_coefficient={self._diffusion_coefficient}, scale={self._scale})' + f'JumpTranslationalDiffusion(name={self.name}, display_name={self.display_name},\n ' + f' diffusion_coefficient={self._diffusion_coefficient}, \n' + f' scale={self._scale})' ) diff --git a/tests/unit/easydynamics/analysis/test_analysis.py b/tests/unit/easydynamics/analysis/test_analysis.py index 64949fa6..cf9594db 100644 --- a/tests/unit/easydynamics/analysis/test_analysis.py +++ b/tests/unit/easydynamics/analysis/test_analysis.py @@ -30,7 +30,9 @@ def analysis(self): data_array = sc.DataArray(data=data, coords={'Q': Q, 'energy': energy}) experiment = Experiment(data=data_array) - sample_model = SampleModel(components=Gaussian(), display_name='Gaussian') + sample_model = SampleModel( + components=Gaussian(name='GaussianName'), display_name='Gaussian' + ) instrument_model = InstrumentModel() return Analysis( @@ -55,7 +57,9 @@ def analysis_single_Q(self): experiment = Experiment(data=data_array) experiment.rebin({'Q': 1}) - sample_model = SampleModel(components=Gaussian(), display_name='Gaussian') + sample_model = SampleModel( + components=Gaussian(name='GaussianName'), display_name='Gaussian' + ) instrument_model = InstrumentModel() return Analysis( @@ -400,19 +404,21 @@ def test_data_and_model_to_datagroup_include_components_not_bool_raises(self, an def test_parameters_to_dataset(self, analysis): # WHEN - analysis.sample_model.append_component(Gaussian(display_name='Gaussian2', area=0.5)) + analysis.sample_model.append_component( + Gaussian(name='Gaussian2Name', display_name='Gaussian2', area=0.5) + ) # THEN parameters_dataset = analysis.parameters_to_dataset() # EXPECT assert isinstance(parameters_dataset, sc.Dataset) parameter_names = [ - 'Gaussian area', - 'Gaussian center', - 'Gaussian width', - 'Gaussian2 area', - 'Gaussian2 center', - 'Gaussian2 width', + 'GaussianName area', + 'GaussianName center', + 'GaussianName width', + 'Gaussian2Name area', + 'Gaussian2Name center', + 'Gaussian2Name width', 'energy_offset', ] for parameter_name in parameter_names: @@ -422,7 +428,9 @@ def test_parameters_to_dataset(self, analysis): def test_parameters_to_dataset_different_units(self, analysis): # WHEN - analysis.sample_model.append_component(Gaussian(display_name='Gaussian2', area=0.5)) + analysis.sample_model.append_component( + Gaussian(name='Gaussian2Name', display_name='Gaussian2', area=0.5) + ) # Convert the unit of a component to eV. analysis.sample_model.get_component_collection(Q_index=1).components[0].convert_unit('eV') @@ -433,12 +441,12 @@ def test_parameters_to_dataset_different_units(self, analysis): # EXPECT assert isinstance(parameters_dataset, sc.Dataset) parameter_names = [ - 'Gaussian area', - 'Gaussian center', - 'Gaussian width', - 'Gaussian2 area', - 'Gaussian2 center', - 'Gaussian2 width', + 'GaussianName area', + 'GaussianName center', + 'GaussianName width', + 'Gaussian2Name area', + 'Gaussian2Name center', + 'Gaussian2Name width', 'energy_offset', ] for parameter_name in parameter_names: diff --git a/tests/unit/easydynamics/sample_model/components/test_delta_function.py b/tests/unit/easydynamics/sample_model/components/test_delta_function.py index be1e33a0..0747731b 100644 --- a/tests/unit/easydynamics/sample_model/components/test_delta_function.py +++ b/tests/unit/easydynamics/sample_model/components/test_delta_function.py @@ -15,7 +15,13 @@ class TestDeltaFunction: @pytest.fixture def delta_function(self): - return DeltaFunction(display_name='TestDeltaFunction', area=2.0, center=0.5, unit='meV') + return DeltaFunction( + name='DeltaFunctionName', + display_name='TestDeltaFunction', + area=2.0, + center=0.5, + unit='meV', + ) def test_init_no_inputs(self): # WHEN THEN @@ -178,8 +184,8 @@ def test_get_all_parameters(self, delta_function: DeltaFunction): assert len(params) == 2 assert all(isinstance(param, Parameter) for param in params) expected_names = { - 'TestDeltaFunction area', - 'TestDeltaFunction center', + 'DeltaFunctionName area', + 'DeltaFunctionName center', } actual_names = {param.name for param in params} assert actual_names == expected_names @@ -215,7 +221,7 @@ def test_repr(self, delta_function: DeltaFunction): # EXPECT assert 'DeltaFunction' in repr_str - assert 'unique_name = DeltaFunction' in repr_str + assert 'name = DeltaFunctionName' in repr_str assert 'unit = meV' in repr_str assert 'area =' in repr_str assert 'center =' in repr_str diff --git a/tests/unit/easydynamics/sample_model/components/test_exponential.py b/tests/unit/easydynamics/sample_model/components/test_exponential.py index 2a824532..97df632e 100644 --- a/tests/unit/easydynamics/sample_model/components/test_exponential.py +++ b/tests/unit/easydynamics/sample_model/components/test_exponential.py @@ -15,6 +15,7 @@ class TestExponential: @pytest.fixture def exponential(self): return Exponential( + name='ExponentialName', display_name='TestExponential', amplitude=2.0, center=0.5, @@ -156,9 +157,9 @@ def test_get_all_parameters(self, exponential: Exponential): assert all(isinstance(param, Parameter) for param in params) expected_names = { - 'TestExponential amplitude', - 'TestExponential center', - 'TestExponential rate', + 'ExponentialName amplitude', + 'ExponentialName center', + 'ExponentialName rate', } actual_names = {param.name for param in params} @@ -226,7 +227,7 @@ def test_repr(self, exponential: Exponential): # THEN EXPECT assert 'Exponential' in repr_str - assert 'unique_name = Exponential' in repr_str + assert 'name = ExponentialName' in repr_str assert 'unit = meV' in repr_str assert 'amplitude =' in repr_str assert 'center =' in repr_str diff --git a/tests/unit/easydynamics/sample_model/components/test_gaussian.py b/tests/unit/easydynamics/sample_model/components/test_gaussian.py index ef339cc2..9bf0b13d 100644 --- a/tests/unit/easydynamics/sample_model/components/test_gaussian.py +++ b/tests/unit/easydynamics/sample_model/components/test_gaussian.py @@ -14,7 +14,14 @@ class TestGaussian: @pytest.fixture def gaussian(self): - return Gaussian(display_name='TestGaussian', area=2.0, center=0.5, width=0.6, unit='meV') + return Gaussian( + name='GaussianName', + display_name='TestGaussian', + area=2.0, + center=0.5, + width=0.6, + unit='meV', + ) def test_init_no_inputs(self): # WHEN THEN @@ -164,9 +171,9 @@ def test_get_all_parameters(self, gaussian: Gaussian): assert all(isinstance(param, Parameter) for param in params) expected_names = { - 'TestGaussian area', - 'TestGaussian center', - 'TestGaussian width', + 'GaussianName area', + 'GaussianName center', + 'GaussianName width', } actual_names = {param.name for param in params} assert actual_names == expected_names @@ -208,6 +215,7 @@ def test_copy(self, gaussian: Gaussian): # EXPECT assert gaussian_copy is not gaussian assert gaussian_copy.display_name == gaussian.display_name + assert gaussian_copy.name == gaussian.name assert gaussian_copy.area.value == gaussian.area.value assert gaussian_copy.area.fixed == gaussian.area.fixed @@ -231,7 +239,7 @@ def test_repr(self, gaussian: Gaussian): repr_str = repr(gaussian) # EXPECT assert 'Gaussian' in repr_str - assert 'unique_name = Gaussian' in repr_str + assert 'name = GaussianName' in repr_str assert 'unit = meV' in repr_str assert 'area =' in repr_str assert 'center =' in repr_str diff --git a/tests/unit/easydynamics/sample_model/components/test_lorentzian.py b/tests/unit/easydynamics/sample_model/components/test_lorentzian.py index 4ce1e1db..3c7809c6 100644 --- a/tests/unit/easydynamics/sample_model/components/test_lorentzian.py +++ b/tests/unit/easydynamics/sample_model/components/test_lorentzian.py @@ -15,7 +15,12 @@ class TestLorentzian: @pytest.fixture def lorentzian(self): return Lorentzian( - display_name='TestLorentzian', area=2.0, center=0.5, width=0.6, unit='meV' + name='LorentzianName', + display_name='TestLorentzian', + area=2.0, + center=0.5, + width=0.6, + unit='meV', ) def test_init_no_inputs(self): @@ -163,9 +168,9 @@ def test_get_all_parameters(self, lorentzian: Lorentzian): assert len(params) == 3 assert all(isinstance(param, Parameter) for param in params) expected_names = { - 'TestLorentzian area', - 'TestLorentzian center', - 'TestLorentzian width', + 'LorentzianName area', + 'LorentzianName center', + 'LorentzianName width', } actual_names = {param.name for param in params} assert actual_names == expected_names @@ -218,7 +223,7 @@ def test_repr(self, lorentzian: Lorentzian): # EXPECT assert 'Lorentzian' in repr_str - assert 'unique_name = Lorentzian' in repr_str + assert 'name = LorentzianName' in repr_str assert 'unit = meV' in repr_str assert 'area =' in repr_str assert 'center =' in repr_str diff --git a/tests/unit/easydynamics/sample_model/components/test_polynomial.py b/tests/unit/easydynamics/sample_model/components/test_polynomial.py index 5e42e677..3111c00a 100644 --- a/tests/unit/easydynamics/sample_model/components/test_polynomial.py +++ b/tests/unit/easydynamics/sample_model/components/test_polynomial.py @@ -14,7 +14,11 @@ class TestPolynomial: @pytest.fixture def polynomial(self): - return Polynomial(display_name='TestPolynomial', coefficients=[1.0, -2.0, 3.0]) + return Polynomial( + name='PolynomialName', + display_name='TestPolynomial', + coefficients=[1.0, -2.0, 3.0], + ) def test_init_no_inputs(self): # WHEN THEN @@ -150,9 +154,9 @@ def test_get_all_parameters(self, polynomial: Polynomial): assert len(params) == 3 assert all(isinstance(param, Parameter) for param in params) expected_names = { - 'TestPolynomial_c0', - 'TestPolynomial_c1', - 'TestPolynomial_c2', + 'PolynomialName_c0', + 'PolynomialName_c1', + 'PolynomialName_c2', } actual_names = {param.name for param in params} assert actual_names == expected_names @@ -193,6 +197,5 @@ def test_repr(self, polynomial: Polynomial): repr_str = repr(polynomial) # EXPECT - assert 'Polynomial' in repr_str - assert 'unique_name = Polynomial' in repr_str + assert 'name = PolynomialName' in repr_str assert 'coefficients =' in repr_str diff --git a/tests/unit/easydynamics/sample_model/components/test_voigt.py b/tests/unit/easydynamics/sample_model/components/test_voigt.py index 5bf866d8..4d28c73a 100644 --- a/tests/unit/easydynamics/sample_model/components/test_voigt.py +++ b/tests/unit/easydynamics/sample_model/components/test_voigt.py @@ -16,6 +16,7 @@ class TestVoigt: @pytest.fixture def voigt(self): return Voigt( + name='VoigtName', display_name='TestVoigt', area=2.0, center=0.5, @@ -234,7 +235,6 @@ def test_evaluate(self, voigt: Voigt): def test_center_is_fixed_if_init_to_None(self): # WHEN THEN test_voigt = Voigt( - display_name='TestVoigt', area=2.0, center=None, gaussian_width=0.6, @@ -265,10 +265,10 @@ def test_get_all_parameters(self, voigt: Voigt): assert len(params) == 4 assert all(isinstance(param, Parameter) for param in params) expected_names = { - 'TestVoigt area', - 'TestVoigt center', - 'TestVoigt gaussian_width', - 'TestVoigt lorentzian_width', + 'VoigtName area', + 'VoigtName center', + 'VoigtName gaussian_width', + 'VoigtName lorentzian_width', } actual_names = {param.name for param in params} assert actual_names == expected_names @@ -318,7 +318,7 @@ def test_repr(self, voigt: Voigt): # EXPECT assert 'Voigt' in repr_str - assert 'unique_name = Voigt' in repr_str + assert 'name = VoigtName' in repr_str assert 'unit = meV' in repr_str assert 'area =' in repr_str assert 'center =' in repr_str 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 be25890e..8c813ec7 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 @@ -171,6 +171,15 @@ def test_create_component_collections_component_name_must_be_string( ): # WHEN THEN EXPECT with pytest.raises(TypeError, match=r'component_name must be a string.'): + brownian_diffusion_model.create_component_collections( + Q=np.array([0.1, 0.2, 0.3]), component_name=123 + ) + + def test_create_component_collections_component_display_name_must_be_string( + self, brownian_diffusion_model + ): + # WHEN THEN EXPECT + with pytest.raises(TypeError, match=r'component_display_name must be a string.'): brownian_diffusion_model.create_component_collections( Q=np.array([0.1, 0.2, 0.3]), component_display_name=123 ) 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 index 9388a2db..a2b8022a 100644 --- 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 @@ -209,6 +209,15 @@ def test_create_component_collections_component_name_must_be_string( ): # WHEN THEN EXPECT with pytest.raises(TypeError, match=r'component_name must be a string.'): + jump_diffusion_model.create_component_collections( + Q=np.array([0.1, 0.2, 0.3]), component_name=123 + ) + + def test_create_component_collections_component_display_name_must_be_string( + self, jump_diffusion_model + ): + # WHEN THEN EXPECT + with pytest.raises(TypeError, match=r'component_display_name must be a string.'): jump_diffusion_model.create_component_collections( Q=np.array([0.1, 0.2, 0.3]), component_display_name=123 ) diff --git a/tests/unit/easydynamics/sample_model/test_component_collection.py b/tests/unit/easydynamics/sample_model/test_component_collection.py index 692486ff..a50dca06 100644 --- a/tests/unit/easydynamics/sample_model/test_component_collection.py +++ b/tests/unit/easydynamics/sample_model/test_component_collection.py @@ -19,6 +19,7 @@ class TestComponentCollection: def component_collection(self): model = ComponentCollection(display_name='TestComponentCollection') component1 = Gaussian( + name='TestGaussian1Name', display_name='TestGaussian1', area=1.0, center=0.0, @@ -27,6 +28,7 @@ def component_collection(self): unique_name='TestGaussian1', ) component2 = Lorentzian( + name='TestLorentzian1Name', display_name='TestLorentzian1', area=2.0, center=1.0, @@ -345,12 +347,12 @@ def test_get_all_parameters(self, component_collection): assert len(parameters) == 6 expected_names = { - 'TestGaussian1 area', - 'TestGaussian1 center', - 'TestGaussian1 width', - 'TestLorentzian1 area', - 'TestLorentzian1 center', - 'TestLorentzian1 width', + 'TestGaussian1Name area', + 'TestGaussian1Name center', + 'TestGaussian1Name width', + 'TestLorentzian1Name area', + 'TestLorentzian1Name center', + 'TestLorentzian1Name width', } actual_names = {param.name for param in parameters} assert actual_names == expected_names @@ -380,10 +382,10 @@ def test_get_fit_parameters(self, component_collection): assert len(fit_parameters) == 4 expected_names = { - 'TestGaussian1 center', - 'TestGaussian1 width', - 'TestLorentzian1 area', - 'TestLorentzian1 center', + 'TestGaussian1Name center', + 'TestGaussian1Name width', + 'TestLorentzian1Name area', + 'TestLorentzian1Name center', } actual_names = {param.name for param in fit_parameters} assert actual_names == expected_names diff --git a/tests/unit/easydynamics/sample_model/test_model_base.py b/tests/unit/easydynamics/sample_model/test_model_base.py index 5ad066a2..34fea244 100644 --- a/tests/unit/easydynamics/sample_model/test_model_base.py +++ b/tests/unit/easydynamics/sample_model/test_model_base.py @@ -18,6 +18,7 @@ class TestModelBase: @pytest.fixture def model_base(self): component1 = Gaussian( + name='TestGaussian1Name', display_name='TestGaussian1', area=1.0, center=0.0, @@ -25,6 +26,7 @@ def model_base(self): unit='meV', ) component2 = Lorentzian( + name='TestLorentzian1Name', display_name='TestLorentzian1', area=2.0, center=1.0, @@ -126,12 +128,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', + 'TestGaussian1Name area', + 'TestGaussian1Name center', + 'TestGaussian1Name width', + 'TestLorentzian1Name area', + 'TestLorentzian1Name center', + 'TestLorentzian1Name width', } retrieved_var_display_names = {var.display_name for var in all_vars} @@ -145,12 +147,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', + 'TestGaussian1Name area', + 'TestGaussian1Name center', + 'TestGaussian1Name width', + 'TestLorentzian1Name area', + 'TestLorentzian1Name center', + 'TestLorentzian1Name width', } retrieved_var_display_names = {var.display_name for var in all_vars} From c1e7108b51c94e1269682ad29af1878ae70a280d Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Sun, 10 May 2026 23:15:39 +0200 Subject: [PATCH 02/20] fixi tutorials and require name inot None --- docs/docs/tutorials/tutorial0_basics.ipynb | 4 +- .../tutorials/tutorial0_more_advanced.ipynb | 8 ++-- docs/docs/tutorials/tutorial1_brownian.ipynb | 12 +++--- .../tutorials/tutorial2_nanoparticles.ipynb | 18 ++++----- .../base_classes/easydynamics_base.py | 37 ++++++++++--------- .../base_classes/easydynamics_modelbase.py | 33 +++++++++-------- .../components/damped_harmonic_oscillator.py | 10 ++--- .../sample_model/components/delta_function.py | 6 +-- .../sample_model/components/exponential.py | 6 +-- .../components/expression_component.py | 4 +- .../sample_model/components/gaussian.py | 4 +- .../sample_model/components/lorentzian.py | 8 ++-- .../sample_model/components/polynomial.py | 4 +- .../sample_model/components/voigt.py | 4 +- .../base_classes/test_easydynamics_base.py | 22 +++++------ .../test_easydynamics_modelbase.py | 24 +++++------- .../test_damped_harmonic_oscillator.py | 15 +++++--- 17 files changed, 111 insertions(+), 108 deletions(-) diff --git a/docs/docs/tutorials/tutorial0_basics.ipynb b/docs/docs/tutorials/tutorial0_basics.ipynb index 2d34368f..cdb688c0 100644 --- a/docs/docs/tutorials/tutorial0_basics.ipynb +++ b/docs/docs/tutorials/tutorial0_basics.ipynb @@ -123,7 +123,7 @@ " \n", "\n", "\n", - "In this data we see a single Gaussian shaped peak and a background that seems to be zero on average. We now want to fit this data, e.g. to determine how the Gaussian changes with $Q$. We define a `Gaussian` like this:" + "In this data we see a single Gaussian shaped peak and a background that seems to be zero on average. We now want to fit this data, e.g. to determine how the Gaussian changes with $Q$. We define a `Gaussian` like this. The `name` will soon be used for indexing, while the `display_name` is what is displayed in figures. By defalut, `display_name` is the same as `name`." ] }, { @@ -133,7 +133,7 @@ "metadata": {}, "outputs": [], "source": [ - "gaussian = sm.Gaussian(display_name='Gaussian', area=1, width=0.05)" + "gaussian = sm.Gaussian(name='Gaussian', area=1, width=0.05)" ] }, { diff --git a/docs/docs/tutorials/tutorial0_more_advanced.ipynb b/docs/docs/tutorials/tutorial0_more_advanced.ipynb index 49cb8963..9397b976 100644 --- a/docs/docs/tutorials/tutorial0_more_advanced.ipynb +++ b/docs/docs/tutorials/tutorial0_more_advanced.ipynb @@ -73,9 +73,9 @@ "metadata": {}, "outputs": [], "source": [ - "gaussian = sm.Gaussian(display_name='Gaussian', area=3, width=0.05)\n", - "lorentzian = sm.Lorentzian(display_name='Lorentzian', area=2, width=0.3)\n", - "dho = sm.DampedHarmonicOscillator(display_name='DHO', area=1.5, width=0.2, center=1.5)\n", + "gaussian = sm.Gaussian(name='Gaussian', area=3, width=0.05)\n", + "lorentzian = sm.Lorentzian(name='Lorentzian', area=2, width=0.3)\n", + "dho = sm.DampedHarmonicOscillator(name='DHO', area=1.5, width=0.2, center=1.5)\n", "\n", "collection = sm.ComponentCollection()\n", "collection.append_component(gaussian)\n", @@ -245,7 +245,7 @@ "id": "af4103fb", "metadata": {}, "source": [ - "The fit looks very good. We can again get a list of the parameters for this fit by accesing the corresponding `Analysis1d` object. Note ethat the Gaussian and Lorentzian centers are both zero, but that the `energy_offset` is non-zero." + "The fit looks very good. We can again get a list of the parameters for this fit by accesing the corresponding `Analysis1d` object. Note that the Gaussian and Lorentzian centers are both zero, but that the `energy_offset` is non-zero." ] }, { diff --git a/docs/docs/tutorials/tutorial1_brownian.ipynb b/docs/docs/tutorials/tutorial1_brownian.ipynb index 413ce036..977a86db 100644 --- a/docs/docs/tutorials/tutorial1_brownian.ipynb +++ b/docs/docs/tutorials/tutorial1_brownian.ipynb @@ -118,7 +118,7 @@ "metadata": {}, "outputs": [], "source": [ - "delta_function = DeltaFunction(display_name='DeltaFunction', area=1)\n", + "delta_function = DeltaFunction(name='DeltaFunction', area=1)\n", "sample_model = SampleModel(components=delta_function)" ] }, @@ -142,7 +142,7 @@ "outputs": [], "source": [ "resolution_components = ComponentCollection()\n", - "res_gauss = Gaussian(width=0.1, area=1, display_name='Res. Gauss')\n", + "res_gauss = Gaussian(width=0.1, area=1, name='Res. Gauss')\n", "res_gauss.area.fixed = True\n", "resolution_components.append_component(res_gauss)\n", "resolution_model = ResolutionModel(components=resolution_components)" @@ -350,8 +350,8 @@ "metadata": {}, "outputs": [], "source": [ - "delta_function = DeltaFunction(display_name='DeltaFunction', area=0.2)\n", - "lorentzian = Lorentzian(display_name='Lorentzian', area=0.5, width=0.3)\n", + "delta_function = DeltaFunction(name='DeltaFunction', area=0.2)\n", + "lorentzian = Lorentzian(name='Lorentzian', area=0.5, width=0.3)\n", "component_collection = ComponentCollection(\n", " components=[delta_function, lorentzian],\n", ")\n", @@ -497,12 +497,12 @@ "metadata": {}, "outputs": [], "source": [ - "delta_function = DeltaFunction(display_name='DeltaFunction', area=0.2)\n", + "delta_function = DeltaFunction(name='DeltaFunction', area=0.2)\n", "component_collection = ComponentCollection(\n", " components=[delta_function],\n", ")\n", "diffusion_model = BrownianTranslationalDiffusion(\n", - " display_name='Brownian Translational Diffusion', diffusion_coefficient=2.4e-9, scale=0.5\n", + " name='Brownian Translational Diffusion', diffusion_coefficient=2.4e-9, scale=0.5\n", ")\n", "\n", "sample_model = SampleModel(\n", diff --git a/docs/docs/tutorials/tutorial2_nanoparticles.ipynb b/docs/docs/tutorials/tutorial2_nanoparticles.ipynb index 42eb15ed..a881db87 100644 --- a/docs/docs/tutorials/tutorial2_nanoparticles.ipynb +++ b/docs/docs/tutorials/tutorial2_nanoparticles.ipynb @@ -262,15 +262,15 @@ "outputs": [], "source": [ "sample_model = SampleModel()\n", - "water_delta_function = DeltaFunction(display_name='Water delta function', area=100)\n", - "water_lorentzian = Lorentzian(display_name='Water Lorentzian', area=10, width=0.2)\n", + "water_delta_function = DeltaFunction(name='Water delta function', area=100)\n", + "water_lorentzian = Lorentzian(name='Water Lorentzian', area=10, width=0.2)\n", "sample_model.append_component(water_delta_function)\n", "sample_model.append_component(water_lorentzian)\n", "sample_model.temperature = 150\n", "\n", "\n", "background_model = BackgroundModel()\n", - "polynomial = Polynomial(coefficients=[0.15])\n", + "polynomial = Polynomial(name='Polynomial', coefficients=[0.15])\n", "polynomial.coefficients[0].min = 0.0\n", "background_model.components = polynomial\n", "\n", @@ -396,21 +396,21 @@ "source": [ "# Now make a new analysis with this sample model\n", "mag_sample_model = SampleModel()\n", - "water_delta_function = DeltaFunction(display_name='Water delta function', area=100)\n", - "water_lorentzian = Lorentzian(display_name='Water Lorentzian', area=100, width=0.2)\n", + "water_delta_function = DeltaFunction(name='Water delta function', area=100)\n", + "water_lorentzian = Lorentzian(name='Water Lorentzian', area=100, width=0.2)\n", "mag_sample_model.append_component(water_delta_function)\n", "mag_sample_model.append_component(water_lorentzian)\n", "\n", "# Add all the magnetic components\n", - "DHO1 = DampedHarmonicOscillator(display_name='DHO1', area=5, center=0.35, width=0.2)\n", - "DHO2 = DampedHarmonicOscillator(display_name='DHO2', area=1, center=1.1, width=0.1)\n", - "mag_lorz = Lorentzian(display_name='Magnetic Lorentzian', area=30, width=0.01)\n", + "DHO1 = DampedHarmonicOscillator(name='DHO1', area=5, center=0.35, width=0.2)\n", + "DHO2 = DampedHarmonicOscillator(name='DHO2', area=1, center=1.1, width=0.1)\n", + "mag_lorz = Lorentzian(name='Magnetic Lorentzian', area=30, width=0.01)\n", "mag_sample_model.append_component(DHO1)\n", "mag_sample_model.append_component(DHO2)\n", "mag_sample_model.append_component(mag_lorz)\n", "\n", "background_model = BackgroundModel()\n", - "polynomial = Polynomial(coefficients=[0.15])\n", + "polynomial = Polynomial(name='Polynomial', coefficients=[0.15])\n", "background_model.components = polynomial\n", "\n", "instrument_model = InstrumentModel(\n", diff --git a/src/easydynamics/base_classes/easydynamics_base.py b/src/easydynamics/base_classes/easydynamics_base.py index 1f38c9d3..a5ee70bb 100644 --- a/src/easydynamics/base_classes/easydynamics_base.py +++ b/src/easydynamics/base_classes/easydynamics_base.py @@ -9,8 +9,8 @@ class EasyDynamicsBase(NewBase): def __init__( self, - name: str | None = 'MyEasyDynamicsModel', - display_name: str | None = 'MyEasyDynamicsModel', + name: str = 'MyEasyDynamicsModel', + display_name: str | None = None, unique_name: str | None = None, ) -> None: """ @@ -18,52 +18,55 @@ def __init__( Parameters ---------- - name : str | None, default='MyEasyDynamicsModel' + name : str, default='MyEasyDynamicsModel' Name of the model. - display_name : str | None, default='MyEasyDynamicsModel' - Display name of the model. + display_name : str | None, default=None + Display name of the model. If None, the name will be used. unique_name : str | None, default=None Unique name of the model. If None, a unique name will be generated. Raises ------ TypeError - If name is not a string or None. + If name is not a string. """ - super().__init__(display_name=display_name, unique_name=unique_name) - - if name is not None and not isinstance(name, str): - raise TypeError('Name must be a string or None.') + if not isinstance(name, str): + raise TypeError('Name must be a string.') self._name = name + if display_name is None: + display_name = name + + super().__init__(display_name=display_name, unique_name=unique_name) + @property - def name(self) -> str | None: + def name(self) -> str: """ Get the name of the model. Returns ------- - str | None + str The name of the model. """ return self._name @name.setter - def name(self, name_str: str | None) -> None: + def name(self, name_str: str) -> None: """ Set the name of the model. Parameters ---------- - name_str : str | None + name_str : str The new name to set. Raises ------ TypeError - If name_str is not a string or None. + If name_str is not a string. """ - if name_str is not None and not isinstance(name_str, str): - raise TypeError('Name must be a string or None.') + if not isinstance(name_str, str): + raise TypeError('Name must be a string.') self._name = name_str diff --git a/src/easydynamics/base_classes/easydynamics_modelbase.py b/src/easydynamics/base_classes/easydynamics_modelbase.py index bf266086..8d21b2b1 100644 --- a/src/easydynamics/base_classes/easydynamics_modelbase.py +++ b/src/easydynamics/base_classes/easydynamics_modelbase.py @@ -13,8 +13,8 @@ class EasyDynamicsModelBase(ModelBase): def __init__( self, unit: str | sc.Unit = 'meV', - name: str | None = 'MyEasyDynamicsModel', - display_name: str | None = 'MyEasyDynamicsModel', + name: str = 'MyEasyDynamicsModel', + display_name: str | None = None, unique_name: str | None = None, ) -> None: """ @@ -24,25 +24,28 @@ def __init__( ---------- unit : str | sc.Unit, default='meV' Unit of the model. - name : str | None, default='MyEasyDynamicsModel' + name : str, default='MyEasyDynamicsModel' Name of the model. - display_name : str | None, default='MyEasyDynamicsModel' - Display name of the model. + display_name : str | None, default=None + Display name of the model. If None, the name will be used. unique_name : str | None, default=None Unique name of the model. If None, a unique name will be generated. Raises ------ TypeError - If name is not a string or None. + If name is not a string. """ + if not isinstance(name, str): + raise TypeError('Name must be a string.') + self._name = name + + if display_name is None: + display_name = name + super().__init__(display_name=display_name, unique_name=unique_name) self._unit = _validate_unit(unit) - if name is not None and not isinstance(name, str): - raise TypeError('Name must be a string or None.') - self._name = name - @property def unit(self) -> str | sc.Unit | None: """ @@ -77,13 +80,13 @@ def unit(self, _unit_str: str) -> None: ) @property - def name(self) -> str | None: + def name(self) -> str: """ Get the name of the model. Returns ------- - str | None + str The name of the model. """ return self._name @@ -101,9 +104,9 @@ def name(self, name_str: str) -> None: Raises ------ TypeError - If name_str is not a string or None. + If name_str is not a string. """ - if name_str is not None and not isinstance(name_str, str): - raise TypeError('Name must be a string or None.') + if not isinstance(name_str, str): + raise TypeError('Name must be a string.') self._name = name_str diff --git a/src/easydynamics/sample_model/components/damped_harmonic_oscillator.py b/src/easydynamics/sample_model/components/damped_harmonic_oscillator.py index 17047b53..86fd6c0b 100644 --- a/src/easydynamics/sample_model/components/damped_harmonic_oscillator.py +++ b/src/easydynamics/sample_model/components/damped_harmonic_oscillator.py @@ -31,7 +31,7 @@ def __init__( width: Numeric | Parameter = 1.0, unit: str | sc.Unit = 'meV', name: str = 'DampedHarmonicOscillator', - display_name: str | None = 'DampedHarmonicOscillator', + display_name: str | None = None, unique_name: str | None = None, ) -> None: """ @@ -50,7 +50,7 @@ def __init__( Unit of the parameters. name : str, default='DampedHarmonicOscillator' Name of the component for indexing. - display_name : str | None, default='DampedHarmonicOscillator' + display_name : str | None, default=None Display name of the component. unique_name : str | None, default=None Unique name of the component. If None, a unique_name is automatically generated. By @@ -65,16 +65,16 @@ def __init__( ) # These methods live in ValidationMixin - area = self._create_area_parameter(area=area, name=display_name, unit=self._unit) + area = self._create_area_parameter(area=area, name=name, unit=self._unit) center = self._create_center_parameter( center=center, - name=display_name, + name=name, fix_if_none=False, unit=self._unit, enforce_minimum_center=True, ) - width = self._create_width_parameter(width=width, name=display_name, unit=self._unit) + width = self._create_width_parameter(width=width, name=name, unit=self._unit) self._area = area self._center = center diff --git a/src/easydynamics/sample_model/components/delta_function.py b/src/easydynamics/sample_model/components/delta_function.py index beeddca7..d63780fa 100644 --- a/src/easydynamics/sample_model/components/delta_function.py +++ b/src/easydynamics/sample_model/components/delta_function.py @@ -34,7 +34,7 @@ def __init__( area: Numeric | Parameter = 1.0, unit: str | sc.Unit = 'meV', name: str = 'DeltaFunction', - display_name: str | None = 'DeltaFunction', + display_name: str | None = None, unique_name: str | None = None, ) -> None: """ @@ -50,8 +50,8 @@ def __init__( Unit of the parameters. name : str, default='DeltaFunction' Name of the component for indexing. - display_name : str | None, default='DeltaFunction' - Name of the component. + display_name : str | None, default=None + Display name of the component. unique_name : str | None, default=None Unique name of the component. If None, a unique_name is automatically generated. By default, None. diff --git a/src/easydynamics/sample_model/components/exponential.py b/src/easydynamics/sample_model/components/exponential.py index 288e380e..b331b162 100644 --- a/src/easydynamics/sample_model/components/exponential.py +++ b/src/easydynamics/sample_model/components/exponential.py @@ -30,7 +30,7 @@ def __init__( rate: Numeric | Parameter = 1.0, unit: str | sc.Unit = 'meV', name: str = 'Exponential', - display_name: str | None = 'Exponential', + display_name: str | None = None, unique_name: str | None = None, ) -> None: """ @@ -48,8 +48,8 @@ def __init__( Unit of the parameters. name : str, default='Exponential' Name of the component for indexing. - display_name : str | None, default='Exponential' - Name of the component. + display_name : str | None, default=None + Display name of the component. unique_name : str | None, default=None Unique name of the component. If None, a unique_name is automatically generated. By default, None. diff --git a/src/easydynamics/sample_model/components/expression_component.py b/src/easydynamics/sample_model/components/expression_component.py index 9e27db8b..29c2bee6 100644 --- a/src/easydynamics/sample_model/components/expression_component.py +++ b/src/easydynamics/sample_model/components/expression_component.py @@ -76,7 +76,7 @@ def __init__( parameters: dict[str, Numeric] | None = None, unit: str | sc.Unit = 'meV', name: str = 'Expression', - display_name: str | None = 'Expression', + display_name: str | None = None, unique_name: str | None = None, ) -> None: """ @@ -92,7 +92,7 @@ def __init__( Unit of the output. name : str, default='Expression' Name of the component for indexing. - display_name : str | None, default='Expression' + display_name : str | None, default=None Display name for the component. unique_name : str | None, default=None Unique name for the component. diff --git a/src/easydynamics/sample_model/components/gaussian.py b/src/easydynamics/sample_model/components/gaussian.py index 57bea198..54cfe97c 100644 --- a/src/easydynamics/sample_model/components/gaussian.py +++ b/src/easydynamics/sample_model/components/gaussian.py @@ -38,7 +38,7 @@ def __init__( width: Numeric | Parameter = 1.0, unit: str | sc.Unit = 'meV', name: str = 'Gaussian', - display_name: str | None = 'Gaussian', + display_name: str | None = None, unique_name: str | None = None, ) -> None: """ @@ -56,7 +56,7 @@ def __init__( Unit of the parameters. name : str, default='Gaussian' Name of the component for indexing. - display_name : str | None, default='Gaussian' + display_name : str | None, default=None Name of the component. unique_name : str | None, default=None Unique name of the component. if None, a unique_name is automatically generated. By diff --git a/src/easydynamics/sample_model/components/lorentzian.py b/src/easydynamics/sample_model/components/lorentzian.py index f2e62815..81453fb8 100644 --- a/src/easydynamics/sample_model/components/lorentzian.py +++ b/src/easydynamics/sample_model/components/lorentzian.py @@ -35,7 +35,7 @@ def __init__( width: Numeric | Parameter = 1.0, unit: str | sc.Unit = 'meV', name: str = 'Lorentzian', - display_name: str | None = 'Lorentzian', + display_name: str | None = None, unique_name: str | None = None, ) -> None: """ @@ -46,15 +46,15 @@ def __init__( area : Numeric | Parameter, default=1.0 Area of the Lorentzian. center : Numeric | Parameter | None, default=None - Center of the Lorentzian. If None. + Center of the Lorentzian. If None, defaults to 0 and is fixed. width : Numeric | Parameter, default=1.0 Half width at half maximum (HWHM). unit : str | sc.Unit, default='meV' Unit of the parameters. name : str, default='Lorentzian' Name of the component for indexing. - display_name : str | None, default='Lorentzian' - Name of the component. + display_name : str | None, default=None + Display name for the component. unique_name : str | None, default=None Unique name of the component. If None, a unique_name is automatically generated. By default, None. diff --git a/src/easydynamics/sample_model/components/polynomial.py b/src/easydynamics/sample_model/components/polynomial.py index eabfc2e4..03e7eae7 100644 --- a/src/easydynamics/sample_model/components/polynomial.py +++ b/src/easydynamics/sample_model/components/polynomial.py @@ -32,7 +32,7 @@ def __init__( coefficients: Sequence[Numeric | Parameter] = (0.0,), unit: str | sc.Unit = 'meV', name: str = 'Polynomial', - display_name: str | None = 'Polynomial', + display_name: str | None = None, unique_name: str | None = None, ) -> None: """ @@ -46,7 +46,7 @@ def __init__( Unit of the Polynomial component. name : str, default='Polynomial' Name of the component for indexing. - display_name : str | None, default='Polynomial' + display_name : str | None, default=None Display name of the Polynomial component. unique_name : str | None, default=None Unique name of the component. If None, a unique_name is automatically generated. By diff --git a/src/easydynamics/sample_model/components/voigt.py b/src/easydynamics/sample_model/components/voigt.py index 11acb544..5c5787b6 100644 --- a/src/easydynamics/sample_model/components/voigt.py +++ b/src/easydynamics/sample_model/components/voigt.py @@ -35,7 +35,7 @@ def __init__( lorentzian_width: Numeric | Parameter = 1.0, unit: str | sc.Unit = 'meV', name: str = 'Voigt', - display_name: str | None = 'Voigt', + display_name: str | None = None, unique_name: str | None = None, ) -> None: """ @@ -55,7 +55,7 @@ def __init__( Unit of the parameters. name : str, default='Voigt' Name of the component for indexing. - display_name : str | None, default='Voigt' + display_name : str | None, default=None Display name of the component. unique_name : str | None, default=None Unique name of the component. If None, a unique_name is automatically generated. By diff --git a/tests/unit/easydynamics/base_classes/test_easydynamics_base.py b/tests/unit/easydynamics/base_classes/test_easydynamics_base.py index 90d62c1a..78c5c936 100644 --- a/tests/unit/easydynamics/base_classes/test_easydynamics_base.py +++ b/tests/unit/easydynamics/base_classes/test_easydynamics_base.py @@ -20,22 +20,20 @@ def test_initialization(self, easy_dynamics_base): # WHEN THEN EXPECT assert easy_dynamics_base.name == 'TestModel' - assert easy_dynamics_base.display_name == 'MyEasyDynamicsModel' + assert easy_dynamics_base.display_name == 'TestModel' assert easy_dynamics_base.unique_name is not None def test_init_raises_type_error_for_invalid_name(self): """Test that initializing with an invalid name raises a TypeError.""" # WHEN THEN EXPECT - with pytest.raises(TypeError, match=r'Name must be a string or None.'): + with pytest.raises(TypeError, match=r'Name must be a string.'): EasyDynamicsBase(name=123) # Not a string - def test_init_name_can_be_none(self): - """Test that initializing with name as None works correctly.""" + def test_init_name_cannot_be_none(self): + """Test that initializing with name as None raises a TypeError.""" # WHEN THEN EXPECT - model = EasyDynamicsBase(name=None) - - # THEN EXPECT - assert model.name is None + with pytest.raises(TypeError, match=r'Name must be a string.'): + EasyDynamicsBase(name=None) def test_name_setter_and_getter(self, easy_dynamics_base): """Test that the name setter and getter work correctly.""" @@ -49,10 +47,8 @@ def test_name_setter_and_getter(self, easy_dynamics_base): assert easy_dynamics_base.name == 'NewName' # THEN - easy_dynamics_base.name = None - - # EXPECT - assert easy_dynamics_base.name is None + with pytest.raises(TypeError, match=r'Name must be a string.'): + easy_dynamics_base.name = None @pytest.mark.parametrize( 'invalid_name', @@ -66,5 +62,5 @@ def test_name_setter_and_getter(self, easy_dynamics_base): def test_name_setter_invalid_type(self, easy_dynamics_base, invalid_name): """Test that setting the name to an invalid type raises a TypeError.""" # WHEN THEN EXPECT - with pytest.raises(TypeError, match=r'Name must be a string or None.'): + with pytest.raises(TypeError, match=r'Name must be a string.'): easy_dynamics_base.name = invalid_name diff --git a/tests/unit/easydynamics/base_classes/test_easydynamics_modelbase.py b/tests/unit/easydynamics/base_classes/test_easydynamics_modelbase.py index c2d30f71..6767640a 100644 --- a/tests/unit/easydynamics/base_classes/test_easydynamics_modelbase.py +++ b/tests/unit/easydynamics/base_classes/test_easydynamics_modelbase.py @@ -20,22 +20,20 @@ def test_initialization(self, easy_dynamics_modelbase): # WHEN THEN EXPECT assert easy_dynamics_modelbase.name == 'TestModel' - assert easy_dynamics_modelbase.display_name == 'MyEasyDynamicsModel' + assert easy_dynamics_modelbase.display_name == 'TestModel' assert easy_dynamics_modelbase.unique_name is not None def test_init_raises_type_error_for_invalid_name(self): """Test that initializing with an invalid name raises a TypeError.""" # WHEN THEN EXPECT - with pytest.raises(TypeError, match=r'Name must be a string or None.'): + with pytest.raises(TypeError, match=r'Name must be a string.'): EasyDynamicsModelBase(name=123) # Not a string - def test_init_name_can_be_none(self): - """Test that initializing with name as None works correctly.""" + def test_init_name_cannot_be_none(self): + """Test that initializing with name as None raises a TypeError.""" # WHEN THEN EXPECT - model = EasyDynamicsModelBase(name=None) - - # THEN EXPECT - assert model.name is None + with pytest.raises(TypeError, match=r'Name must be a string.'): + EasyDynamicsModelBase(name=None) def test_name_setter_and_getter(self, easy_dynamics_modelbase): """Test that the name setter and getter work correctly.""" @@ -48,11 +46,9 @@ def test_name_setter_and_getter(self, easy_dynamics_modelbase): # EXPECT assert easy_dynamics_modelbase.name == 'NewName' - # THEN - easy_dynamics_modelbase.name = None - - # EXPECT - assert easy_dynamics_modelbase.name is None + # THEN EXPECT + with pytest.raises(TypeError, match=r'Name must be a string.'): + easy_dynamics_modelbase.name = None @pytest.mark.parametrize( 'invalid_name', @@ -66,7 +62,7 @@ def test_name_setter_and_getter(self, easy_dynamics_modelbase): def test_name_setter_invalid_type(self, easy_dynamics_modelbase, invalid_name): """Test that setting the name to an invalid type raises a TypeError.""" # WHEN THEN EXPECT - with pytest.raises(TypeError, match=r'Name must be a string or None.'): + with pytest.raises(TypeError, match=r'Name must be a string.'): easy_dynamics_modelbase.name = invalid_name def test_unit_property(self, easy_dynamics_modelbase): diff --git a/tests/unit/easydynamics/sample_model/components/test_damped_harmonic_oscillator.py b/tests/unit/easydynamics/sample_model/components/test_damped_harmonic_oscillator.py index 79e1913f..a420fa40 100644 --- a/tests/unit/easydynamics/sample_model/components/test_damped_harmonic_oscillator.py +++ b/tests/unit/easydynamics/sample_model/components/test_damped_harmonic_oscillator.py @@ -15,7 +15,12 @@ class TestDampedHarmonicOscillator: @pytest.fixture def dho(self): return DampedHarmonicOscillator( - display_name='TestDHO', area=2.0, center=1.5, width=0.3, unit='meV' + name='TestDHOName', + display_name='TestDHO', + area=2.0, + center=1.5, + width=0.3, + unit='meV', ) def test_init_no_inputs(self): @@ -164,9 +169,9 @@ def test_get_all_parameters(self, dho: DampedHarmonicOscillator): assert len(params) == 3 assert all(isinstance(param, Parameter) for param in params) expected_names = { - 'TestDHO area', - 'TestDHO center', - 'TestDHO width', + 'TestDHOName area', + 'TestDHOName center', + 'TestDHOName width', } actual_names = {param.name for param in params} assert actual_names == expected_names @@ -219,7 +224,7 @@ def test_repr(self, dho: DampedHarmonicOscillator): # EXPECT assert 'DampedHarmonicOscillator' in repr_str - assert 'name = TestDHO' in repr_str + assert 'name = TestDHOName' in repr_str assert 'unit = meV' in repr_str assert 'area =' in repr_str assert 'center =' in repr_str From bf1a1d6b819c9e8506fb39b07482d6ed7bf8c4b1 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Sun, 10 May 2026 23:21:39 +0200 Subject: [PATCH 03/20] Fix typo in docstring --- .../brownian_translational_diffusion.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) 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 41e2743a..a3d43ab7 100644 --- a/src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py +++ b/src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py @@ -28,12 +28,14 @@ class BrownianTranslationalDiffusion(DiffusionModelBase): Examples -------- - >>>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=BrownianTranslationalDiffusion(display_name="DiffusionModel", - >>>scale=scale, diffusion_coefficient= diffusion_coefficient) - >>>component_collections=diffusion_model.create_component_collections(Q) See also the - tutorials. + >>> 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=BrownianTranslationalDiffusion(name="DiffusionModel", + >>> scale=scale, diffusion_coefficient= diffusion_coefficient,) + >>> component_collections = diffusion_model.create_component_collections(Q) + + See also the tutorials. """ def __init__( From 94ca0a7f79700cee6ba21a086e8be892fee3c3f4 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Mon, 11 May 2026 08:50:57 +0200 Subject: [PATCH 04/20] PR comments --- .github/workflows/test.yml | 8 ++++---- src/easydynamics/sample_model/component_collection.py | 4 ++-- .../sample_model/components/model_component.py | 1 - 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 774edf78..7697a98a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -229,11 +229,11 @@ jobs: exit 1 fi - whl_url="file://$(python -c 'import os,sys; print(os.path.abspath(sys.argv[1]))' "${whl_path[0]}")" - - echo "Adding easydynamics from: $whl_url" - pixi add --pypi "easydynamics[dev] @ ${whl_url}" + whl_path=$(python -c 'import os,sys; print(os.path.relpath(os.path.abspath(sys.argv[1]), os.getcwd()))' "${whl_path[0]}") + echo "Adding easydynamics from local wheel: $whl_path" + pixi add --pypi "easydynamics[dev] @ ${whl_path}" + echo "Exiting pixi project directory" cd .. done diff --git a/src/easydynamics/sample_model/component_collection.py b/src/easydynamics/sample_model/component_collection.py index 5e60c834..61bf1057 100644 --- a/src/easydynamics/sample_model/component_collection.py +++ b/src/easydynamics/sample_model/component_collection.py @@ -28,7 +28,7 @@ def __init__( components: list[ModelComponent] | None = None, unit: str | sc.Unit = 'meV', name: str = 'ComponentCollection', - display_name: str | None = 'MyComponentCollection', + display_name: str | None = None, unique_name: str | None = None, ) -> None: """ @@ -42,7 +42,7 @@ def __init__( Unit of the collection. name : str, default='ComponentCollection' Name of the collection. - display_name : str | None, default='MyComponentCollection' + display_name : str | None, default=None Display name of the collection. unique_name : str | None, default=None Unique name of the collection. diff --git a/src/easydynamics/sample_model/components/model_component.py b/src/easydynamics/sample_model/components/model_component.py index 52a8992e..84c88453 100644 --- a/src/easydynamics/sample_model/components/model_component.py +++ b/src/easydynamics/sample_model/components/model_component.py @@ -44,7 +44,6 @@ def __init__( display_name=display_name, unique_name=unique_name, ) - self._unit = unit @property def unit(self) -> str: From 6b8a39af4593a0ebc897ad79af65c36805e185e8 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Mon, 11 May 2026 11:02:21 +0200 Subject: [PATCH 05/20] fix repr --- .../diffusion_model/diffusion_model_base.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) 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 36d3d644..e755aed1 100644 --- a/src/easydynamics/sample_model/diffusion_model/diffusion_model_base.py +++ b/src/easydynamics/sample_model/diffusion_model/diffusion_model_base.py @@ -28,11 +28,11 @@ def __init__( ---------- scale : Numeric, default=1.0 Scale factor for the diffusion model. Must be a non-negative number. - unit : str | sc.Unit, default='meV' + unit : str | sc.Unit, default="meV" Unit of the diffusion model. Must be convertible to meV. - name : str, default='DiffusionModel' + name : str, default="DiffusionModel" Name of the diffusion model. - display_name : str | None, default='MyDiffusionModel' + display_name : str | None, default="MyDiffusionModel" Display name of the diffusion model. unique_name : str | None, default=None Unique name of the diffusion model. If None, a unique name will be generated. By @@ -115,4 +115,8 @@ def __repr__(self) -> str: str String representation of the DiffusionModel. """ - return f'{self.__class__.__name__}(display_name={self.display_name}, unit={self.unit})' + return ( + f'{self.__class__.__name__}(name={self.name}, display_name={self.display_name}, ' + f'unit={self.unit}), \n' + f' scale={self.scale})' + ) From 6330cca0efe87cd9bb941a13a459cdb717b37870 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Fri, 15 May 2026 09:44:54 +0200 Subject: [PATCH 06/20] pixi update --- pixi.lock | 407 +++++++++++++++++++++++++++--------------------------- 1 file changed, 205 insertions(+), 202 deletions(-) diff --git a/pixi.lock b/pixi.lock index 47a318c6..92f2af3c 100644 --- a/pixi.lock +++ b/pixi.lock @@ -20,7 +20,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.4.0-py314h680f03e_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.5.0-py314h680f03e_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.3.0-hbca2aae_1.conda @@ -132,7 +132,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/pyzmq-27.1.0-py312hda471dd_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/returns-0.27.0-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 @@ -181,7 +181,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/98/ba/fc20faf5b2bb04615fa906f8daecfe896e6f7a56b34debd93c4a4d63e6e5/copier-9.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/12/bbce9472f489cb5c4c23b0d13e5c59c37c1aab11b7ac637dfe6bbdccebe7/copier-9.15.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d7/51/ec641c26e6dca1b25a7d2035ba6ecb7c884ef1a100a9e42fbe4ce4405139/coverage-7.14.0-cp314-cp314-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 @@ -194,7 +194,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/4a/8a/4cb9367a86f2b9526727ee94e5e6a3d86f9f7d7d947927b444e5bcd56a89/easyscience-2.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/28/63/cd0c3b26afe60995a5295f37c246a93d454023726c3261cfbb3559969bb9/fonttools-4.62.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/36/e1/a8933a72c45a87177fbde2696e0d0755c8c9062f8c077a961c6215fa27b1/fonttools-4.63.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl - pypi: https://files.pythonhosted.org/packages/d5/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-py2.py3-none-any.whl @@ -220,7 +220,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/32/5d/f7e914f7d9325abff4057cee62c0fa70263683189f774473cbfb534cd13b/matplotlib-3.10.9-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/71/d6/48f5b9e44e2e760855d7b489b1317cd7620e82dcb73197961e5cc1391348/mdit_py_plugins-0.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl @@ -261,19 +261,19 @@ environments: - pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/87/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f7/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7e/85/545a951eecc270fcd688288c600017e2050a1aacb56c711d208586d3e470/pymdown_extensions-10.21.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/6f/a05a317a66fee0aad270011461f1a63a453ed12471249f172f7d2e2bc7b4/python_discovery-1.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e8/39/c61d193b8a1daaa8977f7dea9e8d8ba866e02ea7b65d32f6861693aa4c12/ruff-0.15.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e8/31/bf1a0803d077e679cfeee5f2f67290a0fa79c7385b5d9a8c17b9db2c48f0/ruff-0.15.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/43/fe/ad0ecbe2393cb690a4b3100a8fea47ecfdb49f6e06f40cf2f626635adc0c/scipp-26.3.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl @@ -288,7 +288,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/34/a9dbe051de88a63eb7408ea66630bac38e72f7f6077d4be58737106860d9/virtualenv-21.3.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl @@ -305,7 +305,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.4.0-py314h680f03e_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.5.0-py314h680f03e_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.3.0-hbca2aae_1.conda @@ -413,7 +413,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyzmq-27.1.0-py312h022ad19_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/returns-0.27.0-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 @@ -462,7 +462,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/98/ba/fc20faf5b2bb04615fa906f8daecfe896e6f7a56b34debd93c4a4d63e6e5/copier-9.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/12/bbce9472f489cb5c4c23b0d13e5c59c37c1aab11b7ac637dfe6bbdccebe7/copier-9.15.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f3/9b/4165a1d56ddc302a0e2d518fd9d412a4fd0b57562618c78c5f21c57194f5/coverage-7.14.0-cp314-cp314-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 @@ -475,7 +475,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/4a/8a/4cb9367a86f2b9526727ee94e5e6a3d86f9f7d7d947927b444e5bcd56a89/easyscience-2.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/36/f0/2888cdac391807d68d90dcb16ef858ddc1b5309bfc6966195a459dd326e2/fonttools-4.62.1-cp314-cp314-macosx_10_15_universal2.whl + - pypi: https://files.pythonhosted.org/packages/27/d2/23d25e3f247b328be58d04a4c9f894178a0d1eda7d42867cfb388adaf416/fonttools-4.63.0-cp314-cp314-macosx_10_15_universal2.whl - pypi: https://files.pythonhosted.org/packages/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/d5/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-py2.py3-none-any.whl @@ -501,7 +501,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/41/86/86231232fff41c9f8e4a1a7d7a597d349a02527109c3af7d618366122139/matplotlib-3.10.9-cp314-cp314-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/71/d6/48f5b9e44e2e760855d7b489b1317cd7620e82dcb73197961e5cc1391348/mdit_py_plugins-0.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl @@ -542,19 +542,19 @@ environments: - pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/87/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f7/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7e/85/545a951eecc270fcd688288c600017e2050a1aacb56c711d208586d3e470/pymdown_extensions-10.21.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/6f/a05a317a66fee0aad270011461f1a63a453ed12471249f172f7d2e2bc7b4/python_discovery-1.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/aa/a4/f828e9718d3dce1f5f11c39c4f65afd32783c8b2aebb2e3d259e492c47bd/ruff-0.15.12-py3-none-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/d3/19/43f5f2e568dddde567fc41f8471f9432c09563e19d3e617a48cfa52f8f0a/ruff-0.15.13-py3-none-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/0f/0e/0eb94e64f5badef67f11fe1e448dde2a44f00940d8949f4adf71d560552e/scipp-26.3.1-cp314-cp314-macosx_14_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl @@ -569,7 +569,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/34/a9dbe051de88a63eb7408ea66630bac38e72f7f6077d4be58737106860d9/virtualenv-21.3.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz - pypi: https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl @@ -585,7 +585,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.4.0-py314h680f03e_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.5.0-py314h680f03e_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.3.0-hbca2aae_1.conda @@ -679,7 +679,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.3-py314h2359020_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/pyzmq-27.1.0-py312h343a6d4_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/returns-0.27.0-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 @@ -704,9 +704,9 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.7.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_34.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_34.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_34.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_36.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_36.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_36.conda - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.7.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-25.10.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda @@ -733,7 +733,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/98/ba/fc20faf5b2bb04615fa906f8daecfe896e6f7a56b34debd93c4a4d63e6e5/copier-9.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/12/bbce9472f489cb5c4c23b0d13e5c59c37c1aab11b7ac637dfe6bbdccebe7/copier-9.15.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f9/58/6e1b8f52fdc3184b47dc5037f5070d83a3d11042db1594b02d2a44d786c8/coverage-7.14.0-cp314-cp314-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 @@ -746,7 +746,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/4a/8a/4cb9367a86f2b9526727ee94e5e6a3d86f9f7d7d947927b444e5bcd56a89/easyscience-2.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/6b/67/74b070029043186b5dd13462c958cb7c7f811be0d2e634309d9a1ffb1505/fonttools-4.62.1-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/c3/d4/98078064ccc76b45cb0f6c002452011e93c4bd26f6850344f0951cc1fe89/fonttools-4.63.0-cp314-cp314-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/d5/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-py2.py3-none-any.whl @@ -772,7 +772,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ab/1a/5a4f747a8b271cbb024946d2dd3c913ab5032ba430626f8c3528ada96b4b/matplotlib-3.10.9-cp314-cp314-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/71/d6/48f5b9e44e2e760855d7b489b1317cd7620e82dcb73197961e5cc1391348/mdit_py_plugins-0.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl @@ -813,19 +813,19 @@ environments: - pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/87/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f7/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7e/85/545a951eecc270fcd688288c600017e2050a1aacb56c711d208586d3e470/pymdown_extensions-10.21.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/6f/a05a317a66fee0aad270011461f1a63a453ed12471249f172f7d2e2bc7b4/python_discovery-1.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/33/f1/9614e03e1cdcbf9437570b5400ced8a720b5db22b28d8e0f1bda429f660d/ruff-0.15.12-py3-none-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/ab/a6/870a3e8a50590bb92be184ad928c2922f088b00d9dc5c5ec7b924ee08c22/ruff-0.15.13-py3-none-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/1f/28/3f8aa247d29d010547d52207395cb057ebd0a40b88f64bc1dbac9e17a729/scipp-26.3.1-cp314-cp314-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl @@ -840,7 +840,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/34/a9dbe051de88a63eb7408ea66630bac38e72f7f6077d4be58737106860d9/virtualenv-21.3.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl @@ -866,7 +866,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/backports.zstd-1.4.0-py312h90b7ffd_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/backports.zstd-1.5.0-py312h90b7ffd_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.3.0-hbca2aae_1.conda @@ -979,7 +979,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/pyzmq-27.1.0-py312hda471dd_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/returns-0.27.0-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 @@ -1028,7 +1028,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.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/98/ba/fc20faf5b2bb04615fa906f8daecfe896e6f7a56b34debd93c4a4d63e6e5/copier-9.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/12/bbce9472f489cb5c4c23b0d13e5c59c37c1aab11b7ac637dfe6bbdccebe7/copier-9.15.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/69/ff/6699e7b71e60d3049eb2bdcbc95ee3f35707b2b0e48f32e9e63d3ce30c08/coverage-7.14.0-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 @@ -1041,7 +1041,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/4a/8a/4cb9367a86f2b9526727ee94e5e6a3d86f9f7d7d947927b444e5bcd56a89/easyscience-2.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/9b/8a/99c8b3c3888c5c474c08dbfd7c8899786de9604b727fcefb055b42c84bba/fonttools-4.62.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/77/c7/2342da9830e3e9d4870305ca5d2091d2a83284f2953079b7bdd3b5e029d8/fonttools-4.63.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl - pypi: https://files.pythonhosted.org/packages/d5/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-py2.py3-none-any.whl @@ -1067,7 +1067,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/32/91/d024616abdba99e83120e07a20658976f6a343646710760c4a51df126029/matplotlib-3.10.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/71/d6/48f5b9e44e2e760855d7b489b1317cd7620e82dcb73197961e5cc1391348/mdit_py_plugins-0.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl @@ -1108,19 +1108,19 @@ environments: - pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/87/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f7/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7e/85/545a951eecc270fcd688288c600017e2050a1aacb56c711d208586d3e470/pymdown_extensions-10.21.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/6f/a05a317a66fee0aad270011461f1a63a453ed12471249f172f7d2e2bc7b4/python_discovery-1.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e8/39/c61d193b8a1daaa8977f7dea9e8d8ba866e02ea7b65d32f6861693aa4c12/ruff-0.15.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e8/31/bf1a0803d077e679cfeee5f2f67290a0fa79c7385b5d9a8c17b9db2c48f0/ruff-0.15.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/1e/e7/cd78635d0ece7e4d3393f2c1d2ebabf6ff4bd615da142891b1d42ad58abf/scipp-26.3.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl @@ -1135,7 +1135,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/34/a9dbe051de88a63eb7408ea66630bac38e72f7f6077d4be58737106860d9/virtualenv-21.3.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl @@ -1152,7 +1152,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda - - conda: https://conda.anaconda.org/conda-forge/osx-arm64/backports.zstd-1.4.0-py312h87c4bb7_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/backports.zstd-1.5.0-py312h87c4bb7_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.3.0-hbca2aae_1.conda @@ -1259,7 +1259,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyzmq-27.1.0-py312h022ad19_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/returns-0.27.0-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 @@ -1308,7 +1308,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.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/98/ba/fc20faf5b2bb04615fa906f8daecfe896e6f7a56b34debd93c4a4d63e6e5/copier-9.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/12/bbce9472f489cb5c4c23b0d13e5c59c37c1aab11b7ac637dfe6bbdccebe7/copier-9.15.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/34/23/35c7aea1274aef7525bdd2dc92f710bdde6d11652239d71d1ec450067939/coverage-7.14.0-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 @@ -1321,7 +1321,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/4a/8a/4cb9367a86f2b9526727ee94e5e6a3d86f9f7d7d947927b444e5bcd56a89/easyscience-2.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/47/d4/dbacced3953544b9a93088cc10ef2b596d348c983d5c67a404fa41ec51ba/fonttools-4.62.1-cp312-cp312-macosx_10_13_universal2.whl + - pypi: https://files.pythonhosted.org/packages/08/ef/b3c6b9b5be2f82416d73fe2ed2e96e2793cd80e7510bd6a17ca79cdd88ec/fonttools-4.63.0-cp312-cp312-macosx_10_13_universal2.whl - pypi: https://files.pythonhosted.org/packages/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/d5/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-py2.py3-none-any.whl @@ -1347,7 +1347,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b7/18/4880dd762e40cd360c1bf06e890c5a97b997e91cb324602b1a19950ad5ce/matplotlib-3.10.9-cp312-cp312-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/71/d6/48f5b9e44e2e760855d7b489b1317cd7620e82dcb73197961e5cc1391348/mdit_py_plugins-0.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl @@ -1388,19 +1388,19 @@ environments: - pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/87/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f7/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7e/85/545a951eecc270fcd688288c600017e2050a1aacb56c711d208586d3e470/pymdown_extensions-10.21.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/6f/a05a317a66fee0aad270011461f1a63a453ed12471249f172f7d2e2bc7b4/python_discovery-1.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/aa/a4/f828e9718d3dce1f5f11c39c4f65afd32783c8b2aebb2e3d259e492c47bd/ruff-0.15.12-py3-none-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/d3/19/43f5f2e568dddde567fc41f8471f9432c09563e19d3e617a48cfa52f8f0a/ruff-0.15.13-py3-none-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/44/7b/537a61906eac58d94131273084d21d4eb219f5453f0ed30de3aca580a2b4/scipp-26.3.1-cp312-cp312-macosx_14_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl @@ -1415,7 +1415,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/34/a9dbe051de88a63eb7408ea66630bac38e72f7f6077d4be58737106860d9/virtualenv-21.3.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl @@ -1431,7 +1431,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/backports.zstd-1.4.0-py312h06d0912_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/backports.zstd-1.5.0-py312h06d0912_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.3.0-hbca2aae_1.conda @@ -1524,7 +1524,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.3-py312h05f76fc_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/pyzmq-27.1.0-py312h343a6d4_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/returns-0.27.0-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 @@ -1549,9 +1549,9 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.7.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_34.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_34.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_34.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_36.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_36.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_36.conda - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.7.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-25.10.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda @@ -1578,7 +1578,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-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/98/ba/fc20faf5b2bb04615fa906f8daecfe896e6f7a56b34debd93c4a4d63e6e5/copier-9.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/12/bbce9472f489cb5c4c23b0d13e5c59c37c1aab11b7ac637dfe6bbdccebe7/copier-9.15.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/85/19/93853133df2cb371083285ef6a93982a0173e7a233b0f61373ba9fd30eb2/coverage-7.14.0-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 @@ -1591,7 +1591,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/4a/8a/4cb9367a86f2b9526727ee94e5e6a3d86f9f7d7d947927b444e5bcd56a89/easyscience-2.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/28/b1/0c2ab56a16f409c6c8a68816e6af707827ad5d629634691ff60a52879792/fonttools-4.62.1-cp312-cp312-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/87/36/cccb9bc2a6ab63d1b2980374f0dca72ce95ae267c9b4cfe77455bb70d0d4/fonttools-4.63.0-cp312-cp312-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/d5/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-py2.py3-none-any.whl @@ -1617,7 +1617,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/a1/4571fc46e7702de8d0c2dc54ad1b2f8e29328dea3ee90831181f7353d93c/matplotlib-3.10.9-cp312-cp312-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/71/d6/48f5b9e44e2e760855d7b489b1317cd7620e82dcb73197961e5cc1391348/mdit_py_plugins-0.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl @@ -1658,19 +1658,19 @@ environments: - pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/87/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f7/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7e/85/545a951eecc270fcd688288c600017e2050a1aacb56c711d208586d3e470/pymdown_extensions-10.21.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/6f/a05a317a66fee0aad270011461f1a63a453ed12471249f172f7d2e2bc7b4/python_discovery-1.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/33/f1/9614e03e1cdcbf9437570b5400ced8a720b5db22b28d8e0f1bda429f660d/ruff-0.15.12-py3-none-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/ab/a6/870a3e8a50590bb92be184ad928c2922f088b00d9dc5c5ec7b924ee08c22/ruff-0.15.13-py3-none-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/1a/1f/86b4d15221096cb5500bcd73bf350745749e3ba056cdd7a7f75f126f154e/scipp-26.3.1-cp312-cp312-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/a2/84/dc08d77fbf3d87d3ee27f6a0c6dcce1de5829a64f2eae85a0ecc1f0daa73/scipy-1.17.1-cp312-cp312-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl @@ -1685,7 +1685,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/34/a9dbe051de88a63eb7408ea66630bac38e72f7f6077d4be58737106860d9/virtualenv-21.3.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl @@ -1711,7 +1711,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.4.0-py314h680f03e_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.5.0-py314h680f03e_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.3.0-hbca2aae_1.conda @@ -1823,7 +1823,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/pyzmq-27.1.0-py312hda471dd_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/returns-0.27.0-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 @@ -1872,7 +1872,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/98/ba/fc20faf5b2bb04615fa906f8daecfe896e6f7a56b34debd93c4a4d63e6e5/copier-9.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/12/bbce9472f489cb5c4c23b0d13e5c59c37c1aab11b7ac637dfe6bbdccebe7/copier-9.15.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d7/51/ec641c26e6dca1b25a7d2035ba6ecb7c884ef1a100a9e42fbe4ce4405139/coverage-7.14.0-cp314-cp314-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 @@ -1885,7 +1885,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/4a/8a/4cb9367a86f2b9526727ee94e5e6a3d86f9f7d7d947927b444e5bcd56a89/easyscience-2.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/28/63/cd0c3b26afe60995a5295f37c246a93d454023726c3261cfbb3559969bb9/fonttools-4.62.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/36/e1/a8933a72c45a87177fbde2696e0d0755c8c9062f8c077a961c6215fa27b1/fonttools-4.63.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl - pypi: https://files.pythonhosted.org/packages/d5/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-py2.py3-none-any.whl @@ -1911,7 +1911,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/32/5d/f7e914f7d9325abff4057cee62c0fa70263683189f774473cbfb534cd13b/matplotlib-3.10.9-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - - pypi: https://files.pythonhosted.org/packages/71/d6/48f5b9e44e2e760855d7b489b1317cd7620e82dcb73197961e5cc1391348/mdit_py_plugins-0.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl @@ -1952,19 +1952,19 @@ environments: - pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/87/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f7/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7e/85/545a951eecc270fcd688288c600017e2050a1aacb56c711d208586d3e470/pymdown_extensions-10.21.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/6f/a05a317a66fee0aad270011461f1a63a453ed12471249f172f7d2e2bc7b4/python_discovery-1.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/e8/39/c61d193b8a1daaa8977f7dea9e8d8ba866e02ea7b65d32f6861693aa4c12/ruff-0.15.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/e8/31/bf1a0803d077e679cfeee5f2f67290a0fa79c7385b5d9a8c17b9db2c48f0/ruff-0.15.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/43/fe/ad0ecbe2393cb690a4b3100a8fea47ecfdb49f6e06f40cf2f626635adc0c/scipp-26.3.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl @@ -1979,7 +1979,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/34/a9dbe051de88a63eb7408ea66630bac38e72f7f6077d4be58737106860d9/virtualenv-21.3.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl - pypi: https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl @@ -1996,7 +1996,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.4.0-py314h680f03e_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.5.0-py314h680f03e_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.3.0-hbca2aae_1.conda @@ -2104,7 +2104,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyzmq-27.1.0-py312h022ad19_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/returns-0.27.0-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 @@ -2153,7 +2153,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/98/ba/fc20faf5b2bb04615fa906f8daecfe896e6f7a56b34debd93c4a4d63e6e5/copier-9.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/12/bbce9472f489cb5c4c23b0d13e5c59c37c1aab11b7ac637dfe6bbdccebe7/copier-9.15.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f3/9b/4165a1d56ddc302a0e2d518fd9d412a4fd0b57562618c78c5f21c57194f5/coverage-7.14.0-cp314-cp314-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 @@ -2166,7 +2166,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/4a/8a/4cb9367a86f2b9526727ee94e5e6a3d86f9f7d7d947927b444e5bcd56a89/easyscience-2.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/36/f0/2888cdac391807d68d90dcb16ef858ddc1b5309bfc6966195a459dd326e2/fonttools-4.62.1-cp314-cp314-macosx_10_15_universal2.whl + - pypi: https://files.pythonhosted.org/packages/27/d2/23d25e3f247b328be58d04a4c9f894178a0d1eda7d42867cfb388adaf416/fonttools-4.63.0-cp314-cp314-macosx_10_15_universal2.whl - pypi: https://files.pythonhosted.org/packages/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/d5/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-py2.py3-none-any.whl @@ -2192,7 +2192,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/41/86/86231232fff41c9f8e4a1a7d7a597d349a02527109c3af7d618366122139/matplotlib-3.10.9-cp314-cp314-macosx_11_0_arm64.whl - - pypi: https://files.pythonhosted.org/packages/71/d6/48f5b9e44e2e760855d7b489b1317cd7620e82dcb73197961e5cc1391348/mdit_py_plugins-0.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl @@ -2233,19 +2233,19 @@ environments: - pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/87/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f7/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7e/85/545a951eecc270fcd688288c600017e2050a1aacb56c711d208586d3e470/pymdown_extensions-10.21.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/6f/a05a317a66fee0aad270011461f1a63a453ed12471249f172f7d2e2bc7b4/python_discovery-1.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/aa/a4/f828e9718d3dce1f5f11c39c4f65afd32783c8b2aebb2e3d259e492c47bd/ruff-0.15.12-py3-none-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/d3/19/43f5f2e568dddde567fc41f8471f9432c09563e19d3e617a48cfa52f8f0a/ruff-0.15.13-py3-none-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/0f/0e/0eb94e64f5badef67f11fe1e448dde2a44f00940d8949f4adf71d560552e/scipp-26.3.1-cp314-cp314-macosx_14_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl @@ -2260,7 +2260,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/34/a9dbe051de88a63eb7408ea66630bac38e72f7f6077d4be58737106860d9/virtualenv-21.3.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz - pypi: https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl @@ -2276,7 +2276,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.4.0-py314h680f03e_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.5.0-py314h680f03e_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.3.0-hbca2aae_1.conda @@ -2370,7 +2370,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.3-py314h2359020_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/pyzmq-27.1.0-py312h343a6d4_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/returns-0.27.0-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 @@ -2395,9 +2395,9 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.7.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_34.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_34.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_34.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_36.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_36.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_36.conda - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.7.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-25.10.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda @@ -2424,7 +2424,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/98/ba/fc20faf5b2bb04615fa906f8daecfe896e6f7a56b34debd93c4a4d63e6e5/copier-9.15.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0d/12/bbce9472f489cb5c4c23b0d13e5c59c37c1aab11b7ac637dfe6bbdccebe7/copier-9.15.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/f9/58/6e1b8f52fdc3184b47dc5037f5070d83a3d11042db1594b02d2a44d786c8/coverage-7.14.0-cp314-cp314-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 @@ -2437,7 +2437,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/4a/8a/4cb9367a86f2b9526727ee94e5e6a3d86f9f7d7d947927b444e5bcd56a89/easyscience-2.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/6b/67/74b070029043186b5dd13462c958cb7c7f811be0d2e634309d9a1ffb1505/fonttools-4.62.1-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/c3/d4/98078064ccc76b45cb0f6c002452011e93c4bd26f6850344f0951cc1fe89/fonttools-4.63.0-cp314-cp314-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/33/b2/986d1220f6ee931e338d272bc1f3ec02cfe5f9b5fad84e95afdad57f1ebc/format_docstring-0.2.7-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/d5/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-py2.py3-none-any.whl @@ -2463,7 +2463,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ab/1a/5a4f747a8b271cbb024946d2dd3c913ab5032ba430626f8c3528ada96b4b/matplotlib-3.10.9-cp314-cp314-win_amd64.whl - - pypi: https://files.pythonhosted.org/packages/71/d6/48f5b9e44e2e760855d7b489b1317cd7620e82dcb73197961e5cc1391348/mdit_py_plugins-0.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/76/8e/56ccb09c7232a55403a7637caa21922f3b65901a37f5e8bdb405d0de0946/mike-2.2.0-py3-none-any.whl @@ -2504,19 +2504,19 @@ environments: - pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/87/6f/cc2b231dc78d8c3aaa674a676db190b8f8071c50134af8f8cf39b9b8e8df/pydoclint-0.8.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/f7/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7e/85/545a951eecc270fcd688288c600017e2050a1aacb56c711d208586d3e470/pymdown_extensions-10.21.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b7/6f/a05a317a66fee0aad270011461f1a63a453ed12471249f172f7d2e2bc7b4/python_discovery-1.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/93/f7/d00d9b4a0313a6be3a3e0818e6375e15da6d7076f4ae47d1324e7ca986a1/radon-6.0.1-py2.py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/33/f1/9614e03e1cdcbf9437570b5400ced8a720b5db22b28d8e0f1bda429f660d/ruff-0.15.12-py3-none-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/ab/a6/870a3e8a50590bb92be184ad928c2922f088b00d9dc5c5ec7b924ee08c22/ruff-0.15.13-py3-none-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/1f/28/3f8aa247d29d010547d52207395cb057ebd0a40b88f64bc1dbac9e17a729/scipp-26.3.1-cp314-cp314-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl @@ -2531,7 +2531,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/b7/ee/e9c95cda829131f71a8dff5ce0406059fd16e591c074414e31ada19ba7c3/validate_pyproject-0.25-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f4/34/a9dbe051de88a63eb7408ea66630bac38e72f7f6077d4be58737106860d9/virtualenv-21.3.3-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/3f/0e/fa3b193432cfc60c93b42f3be03365f5f909d2b3ea410295cf36df739e31/widgetsnbextension-4.0.15-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl @@ -2557,7 +2557,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.4.0-py314h680f03e_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.5.0-py314h680f03e_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.3.0-hbca2aae_1.conda @@ -2658,7 +2658,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/pyzmq-27.1.0-py312hda471dd_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/returns-0.27.0-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 @@ -2703,9 +2703,9 @@ environments: - pypi: https://files.pythonhosted.org/packages/f2/f2/728f041460f1b9739b85ee23b45fa5a505962ea11fd85bdbe2a02b021373/darkdetect-0.8.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/5a/88/34a0ccc7b9d4713fe6f484c678e69ee6c8146b4fab811d16994b939c65c7/easydynamics-0.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4b/0c/1f04cec38c1921cef464a99b57abc7b8e588a9ae5350378bc45330a60d7f/easydynamics-0.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4a/8a/4cb9367a86f2b9526727ee94e5e6a3d86f9f7d7d947927b444e5bcd56a89/easyscience-2.3.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/28/63/cd0c3b26afe60995a5295f37c246a93d454023726c3261cfbb3559969bb9/fonttools-4.62.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/36/e1/a8933a72c45a87177fbde2696e0d0755c8c9062f8c077a961c6215fa27b1/fonttools-4.63.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl - pypi: https://files.pythonhosted.org/packages/f1/16/d905e7f53e661ce2c24686c38048d8e2b750ffc4350009d41c4e6c6c9826/h5py-3.16.0-cp314-cp314-manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/12/b3/88c0ef22878c86035f058df0ac6c171319ffd0aa52a406455ed3a3847566/ipympl-0.10.0-py3-none-any.whl @@ -2750,7 +2750,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.4.0-py314h680f03e_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.5.0-py314h680f03e_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.3.0-hbca2aae_1.conda @@ -2848,7 +2848,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyzmq-27.1.0-py312h022ad19_2.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/returns-0.27.0-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 @@ -2893,9 +2893,9 @@ environments: - pypi: https://files.pythonhosted.org/packages/f2/f2/728f041460f1b9739b85ee23b45fa5a505962ea11fd85bdbe2a02b021373/darkdetect-0.8.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/5a/88/34a0ccc7b9d4713fe6f484c678e69ee6c8146b4fab811d16994b939c65c7/easydynamics-0.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4b/0c/1f04cec38c1921cef464a99b57abc7b8e588a9ae5350378bc45330a60d7f/easydynamics-0.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4a/8a/4cb9367a86f2b9526727ee94e5e6a3d86f9f7d7d947927b444e5bcd56a89/easyscience-2.3.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/36/f0/2888cdac391807d68d90dcb16ef858ddc1b5309bfc6966195a459dd326e2/fonttools-4.62.1-cp314-cp314-macosx_10_15_universal2.whl + - pypi: https://files.pythonhosted.org/packages/27/d2/23d25e3f247b328be58d04a4c9f894178a0d1eda7d42867cfb388adaf416/fonttools-4.63.0-cp314-cp314-macosx_10_15_universal2.whl - pypi: https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/6a/b7/9366ed44ced9b7ef357ab48c94205280276db9d7f064aa3012a97227e966/h5py-3.16.0-cp314-cp314-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/12/b3/88c0ef22878c86035f058df0ac6c171319ffd0aa52a406455ed3a3847566/ipympl-0.10.0-py3-none-any.whl @@ -2939,7 +2939,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.3.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-26.1.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_1.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.4.0-py314h680f03e_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.5.0-py314h680f03e_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.3.0-hbca2aae_1.conda @@ -3032,7 +3032,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.3-py314h2359020_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/pyzmq-27.1.0-py312h343a6d4_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/returns-0.27.0-pyhc364b38_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 @@ -3057,9 +3057,9 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.7.0-pyhd8ed1ab_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_34.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_34.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_34.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_36.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_36.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_36.conda - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.7.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-25.10.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda @@ -3083,9 +3083,9 @@ environments: - pypi: https://files.pythonhosted.org/packages/f2/f2/728f041460f1b9739b85ee23b45fa5a505962ea11fd85bdbe2a02b021373/darkdetect-0.8.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c7/a0/5ff05d1919ca249508012cad89f08fdc6cfbdaa15b41651c5fe6dffaf1d3/dfo_ls-1.6.5-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/5a/88/34a0ccc7b9d4713fe6f484c678e69ee6c8146b4fab811d16994b939c65c7/easydynamics-0.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4b/0c/1f04cec38c1921cef464a99b57abc7b8e588a9ae5350378bc45330a60d7f/easydynamics-0.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/4a/8a/4cb9367a86f2b9526727ee94e5e6a3d86f9f7d7d947927b444e5bcd56a89/easyscience-2.3.1-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/6b/67/74b070029043186b5dd13462c958cb7c7f811be0d2e634309d9a1ffb1505/fonttools-4.62.1-cp314-cp314-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/c3/d4/98078064ccc76b45cb0f6c002452011e93c4bd26f6850344f0951cc1fe89/fonttools-4.63.0-cp314-cp314-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/3e/14/615a450205e1b56d16c6783f5ccd116cde05550faad70ae077c955654a75/h5py-3.16.0-cp314-cp314-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/12/b3/88c0ef22878c86035f058df0ac6c171319ffd0aa52a406455ed3a3847566/ipympl-0.10.0-py3-none-any.whl @@ -3497,9 +3497,9 @@ packages: - pkg:pypi/babel?source=hash-mapping size: 7684321 timestamp: 1772555330347 -- conda: https://conda.anaconda.org/conda-forge/linux-64/backports.zstd-1.4.0-py312h90b7ffd_0.conda - sha256: e8c83696e6529ac1909a96690c58624bb376312fd0768409380cd9b05e248c9b - md5: 542da724e75cdeef19e29cca23935c25 +- conda: https://conda.anaconda.org/conda-forge/linux-64/backports.zstd-1.5.0-py312h90b7ffd_0.conda + sha256: a2b08a4e5e549b5f67c38edffd175437e2208547a7e67b5fa5373b67ef419e50 + md5: b31dba71fe091e7201826e57e0f7b261 depends: - python - libgcc >=14 @@ -3508,22 +3508,23 @@ packages: - python_abi 3.12.* *_cp312 license: BSD-3-Clause AND MIT AND EPL-2.0 purls: - - pkg:pypi/backports-zstd?source=hash-mapping - size: 238360 - timestamp: 1777848717715 -- conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.4.0-py314h680f03e_0.conda + - pkg:pypi/backports-zstd?source=compressed-mapping + size: 239928 + timestamp: 1778594049826 +- conda: https://conda.anaconda.org/conda-forge/noarch/backports.zstd-1.5.0-py314h680f03e_0.conda noarch: generic - sha256: de1755a35258eb1b59f2288559bbf0b76da60bd2fa6cd6f768ead442f85bd666 - md5: b712198b257f378e9bd8cde277218296 + sha256: a1c97297e867776760489537bc5ae36fa83a154be30e3b79385a39ca4cb058fe + md5: 1133126d840e75287d83947be3fc3e71 depends: - python >=3.14 license: BSD-3-Clause AND MIT AND EPL-2.0 - purls: [] - size: 7546 - timestamp: 1777848733980 -- conda: https://conda.anaconda.org/conda-forge/osx-arm64/backports.zstd-1.4.0-py312h87c4bb7_0.conda - sha256: 7dbd64d3f06622ef8286be6dfceeb8e6008450fb4e6d9309fbb908b12f3937ff - md5: 95a833465ec45ac1e8f2ed1aaba8ec37 + purls: + - pkg:pypi/backports-zstd?source=compressed-mapping + size: 7533 + timestamp: 1778594057496 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/backports.zstd-1.5.0-py312h87c4bb7_0.conda + sha256: a492dcf07b1c58797b3192f11aef7e3beb18ec91646d6a5acfe5c6e61e66118d + md5: 6ec306e02579965dc9c01092a5f4ce4c depends: - python - __osx >=11.0 @@ -3531,12 +3532,12 @@ packages: - python_abi 3.12.* *_cp312 license: BSD-3-Clause AND MIT AND EPL-2.0 purls: - - pkg:pypi/backports-zstd?source=hash-mapping - size: 239305 - timestamp: 1777848727027 -- conda: https://conda.anaconda.org/conda-forge/win-64/backports.zstd-1.4.0-py312h06d0912_0.conda - sha256: 71caf40c0fdeb11fafaac639e6e6f9120112aa105a7a5e9dfb5b4b06db9ca97a - md5: 77d0a2bdd46dd8d502bb27eb80353fcd + - pkg:pypi/backports-zstd?source=compressed-mapping + size: 240840 + timestamp: 1778594074672 +- conda: https://conda.anaconda.org/conda-forge/win-64/backports.zstd-1.5.0-py312h06d0912_0.conda + sha256: 55173c22b24fd257851f2967d4b0256172be3455bd5246b6b7a5c21eb0863f98 + md5: 891112b1a79fc9800317c5d56e056a8b depends: - python - vc >=14.3,<15 @@ -3546,9 +3547,9 @@ packages: - python_abi 3.12.* *_cp312 license: BSD-3-Clause AND MIT AND EPL-2.0 purls: - - pkg:pypi/backports-zstd?source=hash-mapping - size: 237107 - timestamp: 1777848740547 + - pkg:pypi/backports-zstd?source=compressed-mapping + size: 238601 + timestamp: 1778594083648 - pypi: https://files.pythonhosted.org/packages/36/da/87912ddec6e06feffbaa3d7aa18fc6352bee2e8f1fee185d7d1690f8f4e8/backrefs-7.0-py312-none-any.whl name: backrefs version: '7.0' @@ -4191,10 +4192,10 @@ packages: - pytest-xdist ; extra == 'test-no-images' - wurlitzer ; extra == 'test-no-images' requires_python: '>=3.11' -- pypi: https://files.pythonhosted.org/packages/98/ba/fc20faf5b2bb04615fa906f8daecfe896e6f7a56b34debd93c4a4d63e6e5/copier-9.15.0-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/0d/12/bbce9472f489cb5c4c23b0d13e5c59c37c1aab11b7ac637dfe6bbdccebe7/copier-9.15.1-py3-none-any.whl name: copier - version: 9.15.0 - sha256: 0f59c2ea36df42f3ded85c091c3f1e2c8d3814b537504f0abc8c2e508f7e013d + version: 9.15.1 + sha256: 040164686e45e7a841dcd4ae39b01e27093ff91242be3563cae883c4e24c55cc requires_dist: - colorama>=0.4.6 - dunamai>=1.7.0 @@ -4459,10 +4460,10 @@ packages: - importlib-metadata>=1.6.0 ; python_full_version < '3.8' - packaging>=20.9 requires_python: '>=3.5' -- pypi: https://files.pythonhosted.org/packages/5a/88/34a0ccc7b9d4713fe6f484c678e69ee6c8146b4fab811d16994b939c65c7/easydynamics-0.5.1-py3-none-any.whl +- pypi: ./ name: easydynamics - version: 0.5.1 - sha256: 35970c56932b2cb34e6f2d7fa508ef67978c4311dd9922529bf006b612f54e8a + version: 0.5.1+dev10 + sha256: 03e3e912335d4cddf2e70179da113fb35cb4cad133450b3a9e8583aaf5927e6e requires_dist: - darkdetect - easyscience @@ -4507,10 +4508,10 @@ packages: - validate-pyproject[all] ; extra == 'dev' - versioningit ; extra == 'dev' requires_python: '>=3.12' -- pypi: ./ +- pypi: https://files.pythonhosted.org/packages/4b/0c/1f04cec38c1921cef464a99b57abc7b8e588a9ae5350378bc45330a60d7f/easydynamics-0.6.0-py3-none-any.whl name: easydynamics - version: 0.5.1+dev4 - sha256: 03e3e912335d4cddf2e70179da113fb35cb4cad133450b3a9e8583aaf5927e6e + version: 0.6.0 + sha256: 05c54e85d48be3f91a4e9b3c921b159d4ae5281584c0943a56a90f04b9fe1fa2 requires_dist: - darkdetect - easyscience @@ -4642,10 +4643,10 @@ packages: version: 3.29.0 sha256: 96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258 requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/28/63/cd0c3b26afe60995a5295f37c246a93d454023726c3261cfbb3559969bb9/fonttools-4.62.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/08/ef/b3c6b9b5be2f82416d73fe2ed2e96e2793cd80e7510bd6a17ca79cdd88ec/fonttools-4.63.0-cp312-cp312-macosx_10_13_universal2.whl name: fonttools - version: 4.62.1 - sha256: 8d337fdd49a79b0d51c4da87bc38169d21c3abbf0c1aa9367eff5c6656fb6dae + version: 4.63.0 + sha256: 37dd23e621e3b0aef1baa70a303b80aaf38449632cfc8fd2a55fb285bbccfc02 requires_dist: - lxml>=4.0 ; extra == 'lxml' - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'woff' @@ -4676,10 +4677,10 @@ packages: - skia-pathops>=0.5.0 ; extra == 'all' - uharfbuzz>=0.45.0 ; extra == 'all' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/28/b1/0c2ab56a16f409c6c8a68816e6af707827ad5d629634691ff60a52879792/fonttools-4.62.1-cp312-cp312-win_amd64.whl +- pypi: https://files.pythonhosted.org/packages/27/d2/23d25e3f247b328be58d04a4c9f894178a0d1eda7d42867cfb388adaf416/fonttools-4.63.0-cp314-cp314-macosx_10_15_universal2.whl name: fonttools - version: 4.62.1 - sha256: 9e7863e10b3de72376280b515d35b14f5eeed639d1aa7824f4cf06779ec65e42 + version: 4.63.0 + sha256: fd1e3094f42d806d3d7c79162fc59e5910fcbe3a7360c385b8da969bc4493745 requires_dist: - lxml>=4.0 ; extra == 'lxml' - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'woff' @@ -4710,10 +4711,10 @@ packages: - skia-pathops>=0.5.0 ; extra == 'all' - uharfbuzz>=0.45.0 ; extra == 'all' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/36/f0/2888cdac391807d68d90dcb16ef858ddc1b5309bfc6966195a459dd326e2/fonttools-4.62.1-cp314-cp314-macosx_10_15_universal2.whl +- pypi: https://files.pythonhosted.org/packages/36/e1/a8933a72c45a87177fbde2696e0d0755c8c9062f8c077a961c6215fa27b1/fonttools-4.63.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl name: fonttools - version: 4.62.1 - sha256: fa1d16210b6b10a826d71bed68dd9ec24a9e218d5a5e2797f37c573e7ec215ca + version: 4.63.0 + sha256: 308f957cdeaf8abe4e5f2f124902ef405448af92c90f80e302a3b771c2e6116b requires_dist: - lxml>=4.0 ; extra == 'lxml' - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'woff' @@ -4744,10 +4745,10 @@ packages: - skia-pathops>=0.5.0 ; extra == 'all' - uharfbuzz>=0.45.0 ; extra == 'all' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/47/d4/dbacced3953544b9a93088cc10ef2b596d348c983d5c67a404fa41ec51ba/fonttools-4.62.1-cp312-cp312-macosx_10_13_universal2.whl +- pypi: https://files.pythonhosted.org/packages/77/c7/2342da9830e3e9d4870305ca5d2091d2a83284f2953079b7bdd3b5e029d8/fonttools-4.63.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl name: fonttools - version: 4.62.1 - sha256: 90365821debbd7db678809c7491ca4acd1e0779b9624cdc6ddaf1f31992bf974 + version: 4.63.0 + sha256: 58dc6bb86a78d782f00f9190ca02c119cf5bbe2807536e361e18d42019f877d8 requires_dist: - lxml>=4.0 ; extra == 'lxml' - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'woff' @@ -4778,10 +4779,10 @@ packages: - skia-pathops>=0.5.0 ; extra == 'all' - uharfbuzz>=0.45.0 ; extra == 'all' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/6b/67/74b070029043186b5dd13462c958cb7c7f811be0d2e634309d9a1ffb1505/fonttools-4.62.1-cp314-cp314-win_amd64.whl +- pypi: https://files.pythonhosted.org/packages/87/36/cccb9bc2a6ab63d1b2980374f0dca72ce95ae267c9b4cfe77455bb70d0d4/fonttools-4.63.0-cp312-cp312-win_amd64.whl name: fonttools - version: 4.62.1 - sha256: 1eecc128c86c552fb963fe846ca4e011b1be053728f798185a1687502f6d398e + version: 4.63.0 + sha256: 59ac449f8cca9b4ffa08d2e7bbadad87ce710d69d1eda5c3c1ce579baa987272 requires_dist: - lxml>=4.0 ; extra == 'lxml' - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'woff' @@ -4812,10 +4813,10 @@ packages: - skia-pathops>=0.5.0 ; extra == 'all' - uharfbuzz>=0.45.0 ; extra == 'all' requires_python: '>=3.10' -- pypi: https://files.pythonhosted.org/packages/9b/8a/99c8b3c3888c5c474c08dbfd7c8899786de9604b727fcefb055b42c84bba/fonttools-4.62.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/c3/d4/98078064ccc76b45cb0f6c002452011e93c4bd26f6850344f0951cc1fe89/fonttools-4.63.0-cp314-cp314-win_amd64.whl name: fonttools - version: 4.62.1 - sha256: 149f7d84afca659d1a97e39a4778794a2f83bf344c5ee5134e09995086cc2392 + version: 4.63.0 + sha256: 7d782fac32985914c351556f68ac0855391572bcd87de50e05970d3cd4c96fc5 requires_dist: - lxml>=4.0 ; extra == 'lxml' - brotli>=1.0.1 ; platform_python_implementation == 'CPython' and extra == 'woff' @@ -6690,13 +6691,13 @@ packages: license: BSD-3-Clause license_family: BSD purls: - - pkg:pypi/matplotlib-inline?source=compressed-mapping + - pkg:pypi/matplotlib-inline?source=hash-mapping size: 15725 timestamp: 1778264403247 -- pypi: https://files.pythonhosted.org/packages/71/d6/48f5b9e44e2e760855d7b489b1317cd7620e82dcb73197961e5cc1391348/mdit_py_plugins-0.6.0-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl name: mdit-py-plugins - version: 0.6.0 - sha256: f7e7a25d8b616fee99cb1e330da73451d11a8061baf39bb9663ab9ce0e005b90 + version: 0.6.1 + sha256: 214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d requires_dist: - markdown-it-py>=2.0.0,<5.0.0 - pre-commit ; extra == 'code-style' @@ -8602,10 +8603,10 @@ packages: - pkg:pypi/pygments?source=hash-mapping size: 893031 timestamp: 1774796815820 -- pypi: https://files.pythonhosted.org/packages/f7/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/7e/85/545a951eecc270fcd688288c600017e2050a1aacb56c711d208586d3e470/pymdown_extensions-10.21.3-py3-none-any.whl name: pymdown-extensions - version: 10.21.2 - sha256: 5c0fd2a2bea14eb39af8ff284f1066d898ab2187d81b889b75d46d4348c01638 + version: 10.21.3 + sha256: d7a5d08014fc571e80ca21dd6f854e31f94c489800350564d55d15b3c41e76b6 requires_dist: - markdown>=3.6 - pyyaml @@ -8918,10 +8919,10 @@ packages: - pkg:pypi/python-dateutil?source=hash-mapping size: 233310 timestamp: 1751104122689 -- pypi: https://files.pythonhosted.org/packages/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/b7/6f/a05a317a66fee0aad270011461f1a63a453ed12471249f172f7d2e2bc7b4/python_discovery-1.3.1-py3-none-any.whl name: python-discovery - version: 1.3.0 - sha256: 441d9ced3dfce36e113beb35ca302c71c7ef06f3c0f9c227a0b9bb3bd49b9e9f + version: 1.3.1 + sha256: ed188687ebb3b82c01a17cd5ac62fc94d9f6487a7f1a0f9dfe89753fec91039c requires_dist: - filelock>=3.15.4 - platformdirs>=4.3.6,<5 @@ -8929,6 +8930,8 @@ packages: - sphinx-autodoc-typehints>=3.6.3 ; extra == 'docs' - sphinx>=9.1 ; extra == 'docs' - sphinxcontrib-mermaid>=2 ; extra == 'docs' + - sphinxcontrib-towncrier>=0.4 ; extra == 'docs' + - towncrier>=25.8 ; extra == 'docs' - covdefaults>=2.3 ; extra == 'testing' - coverage>=7.5.4 ; extra == 'testing' - pytest-mock>=3.14 ; extra == 'testing' @@ -9313,9 +9316,9 @@ packages: - pkg:pypi/referencing?source=hash-mapping size: 51788 timestamp: 1760379115194 -- conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.0-pyhcf101f3_0.conda - sha256: 4487fdb341537e2df47159ed8e546add99080974c52d5b2dc2a710910619115a - md5: a5985537dab1ba7034b5ff4ea22e2fa9 +- conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.34.1-pyhcf101f3_0.conda + sha256: 22ffa6f214829b9fb2daac5b25886cca73bd8de1fb9523791ce39478d60a58f7 + md5: 18a1731937516054803c047e1b39dc04 depends: - python >=3.10 - certifi >=2023.5.7 @@ -9328,8 +9331,8 @@ packages: license: Apache-2.0 purls: - pkg:pypi/requests?source=hash-mapping - size: 68658 - timestamp: 1778534036810 + size: 68706 + timestamp: 1778712882916 - conda: https://conda.anaconda.org/conda-forge/noarch/returns-0.27.0-pyhc364b38_0.conda sha256: 3b45efeae771f1a20307b36ecdb3a8911a89c05382836b50c62b0a99d8d3dfd8 md5: da94ff04d97ec5efc42cbe5da3c43a84 @@ -9473,20 +9476,20 @@ packages: - pkg:pypi/rpds-py?source=hash-mapping size: 235780 timestamp: 1764543046065 -- pypi: https://files.pythonhosted.org/packages/33/f1/9614e03e1cdcbf9437570b5400ced8a720b5db22b28d8e0f1bda429f660d/ruff-0.15.12-py3-none-win_amd64.whl +- pypi: https://files.pythonhosted.org/packages/ab/a6/870a3e8a50590bb92be184ad928c2922f088b00d9dc5c5ec7b924ee08c22/ruff-0.15.13-py3-none-win_amd64.whl name: ruff - version: 0.15.12 - sha256: c87a162d61ab3adca47c03f7f717c68672edec7d1b5499e652331780fe74950d + version: 0.15.13 + sha256: 7064884d442b7d477b4e7473d12da7f08851d2b1982763c5d3f388a19468a1a4 requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/aa/a4/f828e9718d3dce1f5f11c39c4f65afd32783c8b2aebb2e3d259e492c47bd/ruff-0.15.12-py3-none-macosx_11_0_arm64.whl +- pypi: https://files.pythonhosted.org/packages/d3/19/43f5f2e568dddde567fc41f8471f9432c09563e19d3e617a48cfa52f8f0a/ruff-0.15.13-py3-none-macosx_11_0_arm64.whl name: ruff - version: 0.15.12 - sha256: fe87510d000220aa1ed530d4448a7c696a0cae1213e5ec30e5874287b66557b5 + version: 0.15.13 + sha256: 1c26d2f66163deeb6e08d8b39fbbe983ce3c71cea06a6d7591cfd1421793c629 requires_python: '>=3.7' -- pypi: https://files.pythonhosted.org/packages/e8/39/c61d193b8a1daaa8977f7dea9e8d8ba866e02ea7b65d32f6861693aa4c12/ruff-0.15.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl +- pypi: https://files.pythonhosted.org/packages/e8/31/bf1a0803d077e679cfeee5f2f67290a0fa79c7385b5d9a8c17b9db2c48f0/ruff-0.15.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl name: ruff - version: 0.15.12 - sha256: 83b2f4f2f3b1026b5fb449b467d9264bf22067b600f7b6f41fc5958909f449d0 + version: 0.15.13 + sha256: cc411dfebe5eebe55ce041c6ae080eb7668955e866daa2fbb16692a784f1c4ca requires_python: '>=3.7' - pypi: https://files.pythonhosted.org/packages/0f/0e/0eb94e64f5badef67f11fe1e448dde2a44f00940d8949f4adf71d560552e/scipp-26.3.1-cp314-cp314-macosx_14_0_arm64.whl name: scipp @@ -10370,9 +10373,9 @@ packages: - tomli>=1.2.1 ; python_full_version < '3.11' and extra == 'all' - validate-pyproject-schema-store ; extra == 'store' requires_python: '>=3.8' -- conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_34.conda - sha256: 9dc40c2610a6e6727d635c62cced5ef30b7b30123f5ef67d6139e23d21744b3a - md5: 1e610f2416b6acdd231c5f573d754a0f +- conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_36.conda + sha256: 7c86d8ed3ac473c3e4dde0dd05aeb1f3189a26ad66c0e250f6cf4018e73358f2 + md5: 3466ff4a8753003eeb173f508d3d5a49 depends: - vc14_runtime >=14.44.35208 track_features: @@ -10380,33 +10383,33 @@ packages: license: BSD-3-Clause license_family: BSD purls: [] - size: 19356 - timestamp: 1767320221521 -- conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_34.conda - sha256: 02732f953292cce179de9b633e74928037fa3741eb5ef91c3f8bae4f761d32a5 - md5: 37eb311485d2d8b2c419449582046a42 + size: 19989 + timestamp: 1778688080106 +- conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_36.conda + sha256: 902984f2282859a76d764d80d74f873df7c7749117cfac15c5106e086fb2b772 + md5: 65f5c81f2796961fcfd808eee8e73596 depends: - ucrt >=10.0.20348.0 - - vcomp14 14.44.35208 h818238b_34 + - vcomp14 14.44.35208 h818238b_36 constrains: - - vs2015_runtime 14.44.35208.* *_34 + - vs2015_runtime 14.44.35208.* *_36 license: LicenseRef-MicrosoftVisualCpp2015-2022Runtime license_family: Proprietary purls: [] - size: 683233 - timestamp: 1767320219644 -- conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_34.conda - sha256: 878d5d10318b119bd98ed3ed874bd467acbe21996e1d81597a1dbf8030ea0ce6 - md5: 242d9f25d2ae60c76b38a5e42858e51d + size: 683790 + timestamp: 1778688078434 +- conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_36.conda + sha256: 0cd5b905ab2b5e9fcb170fe8801b64917effef8e3a73ffd9b2cc4c3ee387f09c + md5: 4aa1884260877bd57d16070d20271e2d depends: - ucrt >=10.0.20348.0 constrains: - - vs2015_runtime 14.44.35208.* *_34 + - vs2015_runtime 14.44.35208.* *_36 license: LicenseRef-MicrosoftVisualCpp2015-2022Runtime license_family: Proprietary purls: [] - size: 115235 - timestamp: 1767320173250 + size: 115995 + timestamp: 1778688058077 - pypi: https://files.pythonhosted.org/packages/1c/59/964ecb8008722d27d8a835baea81f56a91cea8e097b3be992bc6ccde6367/versioningit-3.3.0-py3-none-any.whl name: versioningit version: 3.3.0 @@ -10426,17 +10429,17 @@ packages: - mypy ; extra == 'test' - pretend ; extra == 'test' - pytest ; extra == 'test' -- pypi: https://files.pythonhosted.org/packages/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/f4/34/a9dbe051de88a63eb7408ea66630bac38e72f7f6077d4be58737106860d9/virtualenv-21.3.3-py3-none-any.whl name: virtualenv - version: 21.3.1 - sha256: d1a71cf58f2f9228fff23a1f6ec15d39785c6b32e03658d104974247145edd35 + version: 21.3.3 + sha256: 7d5987d8369e098e41406efb780a3d4ca79280097293899e351a6407ee153ab3 requires_dist: - distlib>=0.3.7,<1 - filelock>=3.24.2,<4 ; python_full_version >= '3.10' - filelock>=3.16.1,<=3.19.1 ; python_full_version < '3.10' - importlib-metadata>=6.6 ; python_full_version < '3.8' - platformdirs>=3.9.1,<5 - - python-discovery>=1.2.2 + - python-discovery>=1.3.1 - typing-extensions>=4.13.2 ; python_full_version < '3.11' requires_python: '>=3.8' - pypi: https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl From ace132b9d7d99b5249425b68b4feb618655f390b Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Fri, 15 May 2026 11:38:45 +0200 Subject: [PATCH 07/20] Implement easydynamics_list --- .../base_classes/easydynamics_base.py | 46 +--- .../base_classes/easydynamics_list.py | 211 ++++++++++++++++++ .../base_classes/easydynamics_modelbase.py | 21 +- src/easydynamics/base_classes/name_mixin.py | 74 ++++++ 4 files changed, 302 insertions(+), 50 deletions(-) create mode 100644 src/easydynamics/base_classes/easydynamics_list.py create mode 100644 src/easydynamics/base_classes/name_mixin.py diff --git a/src/easydynamics/base_classes/easydynamics_base.py b/src/easydynamics/base_classes/easydynamics_base.py index a5ee70bb..e8c9500f 100644 --- a/src/easydynamics/base_classes/easydynamics_base.py +++ b/src/easydynamics/base_classes/easydynamics_base.py @@ -3,13 +3,15 @@ from easyscience.base_classes.new_base import NewBase +from easydynamics.base_classes.name_mixin import NameMixin -class EasyDynamicsBase(NewBase): + +class EasyDynamicsBase(NewBase, NameMixin): """Base class for all EasyDynamics classes.""" def __init__( self, - name: str = 'MyEasyDynamicsModel', + name: str = "MyEasyDynamicsModel", display_name: str | None = None, unique_name: str | None = None, ) -> None: @@ -30,43 +32,9 @@ def __init__( TypeError If name is not a string. """ - if not isinstance(name, str): - raise TypeError('Name must be a string.') - self._name = name + NameMixin.__init__(self, name=name) if display_name is None: - display_name = name - - super().__init__(display_name=display_name, unique_name=unique_name) - - @property - def name(self) -> str: - """ - Get the name of the model. - - Returns - ------- - str - The name of the model. - """ - return self._name - - @name.setter - def name(self, name_str: str) -> None: - """ - Set the name of the model. - - Parameters - ---------- - name_str : str - The new name to set. - - Raises - ------ - TypeError - If name_str is not a string. - """ + display_name = self.name - if not isinstance(name_str, str): - raise TypeError('Name must be a string.') - self._name = name_str + NewBase.__init__(self, display_name=display_name, unique_name=unique_name) diff --git a/src/easydynamics/base_classes/easydynamics_list.py b/src/easydynamics/base_classes/easydynamics_list.py new file mode 100644 index 00000000..1806a820 --- /dev/null +++ b/src/easydynamics/base_classes/easydynamics_list.py @@ -0,0 +1,211 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +from __future__ import annotations + +from collections.abc import Iterable +from typing import Any +from typing import TypeVar + +from easyscience.base_classes.easy_list import EasyList + +from easydynamics.base_classes.name_mixin import NameMixin + +from .new_base import NewBase + +ProtectedType_ = TypeVar("ProtectedType", bound=NewBase) + + +class EasyDynamicsList(EasyList, NameMixin): + """Base class for all EasyDynamics lists.""" + + def __init__( + self, + *args: ProtectedType_ | list[ProtectedType_], + protected_types: list[type[NewBase]] | type[NewBase] | None = None, + name: str = "MyEasyDynamicsList", + display_name: str | None = None, + unique_name: str | None = None, + **kwargs: Any, # noqa: ANN401 + ) -> None: + """ + Initialize the EasyDynamicsList. + + Parameters + ---------- + args : ProtectedType_ | list[ProtectedType_] + Initial items to add to the list. Can be a single item or a list of items. Each item must be an instance of one of the protected types. + protected_types : list[type[NewBase]] | type[NewBase] | None, optional + Types that are allowed in the list. Can be a single NewBase subclass or a list of them. If None, defaults to [NewBase]. + name : str, default='MyEasyDynamicsList' + Name of the list. + display_name : str | None, default=None + Display name of the list. If None, the name will be used. + unique_name : str | None, default=None + Unique name of the list. If None, a unique name will be generated. + kwargs : Any + Additional keyword arguments to pass to the EasyList constructor. + + """ + NameMixin.__init__(self, name=name) + + if display_name is None: + display_name = self.name + + EasyList.__init__( + self, + *args, + protected_types=protected_types, + display_name=display_name, + unique_name=unique_name, + **kwargs, + ) + + # ------------------------------------------------------------------ + # Methods + # ------------------------------------------------------------------ + + def insert(self, index: int, value: ProtectedType_) -> None: + """ + Insert an item into the list at a specific index. + + Parameters + ---------- + index : int + The index at which to insert the item. + value : ProtectedType_ + The item to insert. Must be an instance of one of the protected types. + """ + self._check_name_unique(value) + super().insert(index, value) + value.lock_name() + + def append(self, value: ProtectedType_) -> None: + """ + Append an item to the end of the list. + Parameters + ---------- + value : ProtectedType_ + The item to append. Must be an instance of one of the protected types. + """ + self._check_name_unique(value) + super().append(value) + value.lock_name() + + def extend(self, values: Iterable[ProtectedType_]) -> None: + """ + Extend the list by appending elements from the iterable. + Parameters + ---------- + values : Iterable[ProtectedType_] + An iterable of items to append. Each item must be an instance of one of the protected types. + """ + values = list(values) + self._check_name_unique(values) + for v in values: + self.append(v) + + def pop(self, idx: int) -> ProtectedType_: + """ + Remove and return an item at a specific index. + + Parameters + ---------- + idx : int + The index at which to pop the item. + + Returns + ------- + ProtectedType_ + The item that was popped. + """ + item = self[idx] + item.unlock_name() + return super().pop(idx) + + # ------------------------------------------------------------------ + # Private methods + # ------------------------------------------------------------------ + + def _check_name_unique(self, obj: NewBase | Iterable[NewBase]) -> None: + """Check that the name of an object is unique in the list. + Parameters + ---------- + obj : NewBase | Iterable[NewBase] + Object or objects to check. Can be a single object or an iterable of objects. + Raises + ------ + ValueError + If the name of the object is not unique in the list. + """ + + items = [obj] if isinstance(obj, NewBase) else list(obj) + + get_key = self._get_key + new_names = [get_key(item) for item in items] + + if len(new_names) != len(set(new_names)): + raise ValueError(f"Duplicate names in {obj} detected.") + + existing_names = {get_key(o) for o in self._data} + + conflict = existing_names.intersection(new_names) + if conflict: + name = next(iter(conflict)) + raise ValueError(f'Name "{name}" already exists in list.') + + def _get_key(self, obj: NewBase) -> str: + """Get the name of an object. + + Parameters + ---------- + obj : NewBase + Object to get the key for. + + Returns + ------- + str + The name of the object. + """ + return obj.name + + # ------------------------------------------------------------------ + # dunder methods + # ------------------------------------------------------------------ + + def __setitem__( + self, idx: int | slice, value: ProtectedType_ | Iterable[ProtectedType_] + ) -> None: + """ + Set an item in the list at a specific index. + + Parameters + ---------- + idx : int | slice + The index at which to set the item. + value : ProtectedType_ | Iterable[ProtectedType_] + The item or items to set. Must be an instance of one of the protected types or an iterable of protected types. + """ + self._check_name_unique(value) + super().__setitem__(idx, value) + if isinstance(idx, slice): + for v in value: + v.lock_name() + else: + value.lock_name() + + def __delitem__(self, idx: int | slice) -> None: + """ + Delete an item from the list at a specific index. + + Parameters + ---------- + idx : int | slice + The index at which to delete the item. + """ + if isinstance(idx, int): + self[idx].unlock_name() + elif isinstance(idx, slice): + for item in self[idx]: + item.unlock_name() + super().__delitem__(idx) diff --git a/src/easydynamics/base_classes/easydynamics_modelbase.py b/src/easydynamics/base_classes/easydynamics_modelbase.py index 8d21b2b1..9e4e0565 100644 --- a/src/easydynamics/base_classes/easydynamics_modelbase.py +++ b/src/easydynamics/base_classes/easydynamics_modelbase.py @@ -4,16 +4,17 @@ import scipp as sc from easyscience.base_classes import ModelBase +from easydynamics.base_classes.name_mixin import NameMixin from easydynamics.utils.utils import _validate_unit -class EasyDynamicsModelBase(ModelBase): +class EasyDynamicsModelBase(ModelBase, NameMixin): """Base class for all EasyDynamics models.""" def __init__( self, - unit: str | sc.Unit = 'meV', - name: str = 'MyEasyDynamicsModel', + unit: str | sc.Unit = "meV", + name: str = "MyEasyDynamicsModel", display_name: str | None = None, unique_name: str | None = None, ) -> None: @@ -36,14 +37,12 @@ def __init__( TypeError If name is not a string. """ - if not isinstance(name, str): - raise TypeError('Name must be a string.') - self._name = name + NameMixin.__init__(self, name=name) if display_name is None: - display_name = name + display_name = self.name - super().__init__(display_name=display_name, unique_name=unique_name) + ModelBase.__init__(self, display_name=display_name, unique_name=unique_name) self._unit = _validate_unit(unit) @property @@ -75,8 +74,8 @@ def unit(self, _unit_str: str) -> None: Always raised to indicate that the unit is read-only. """ 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." ) @property @@ -108,5 +107,5 @@ def name(self, name_str: str) -> None: """ if not isinstance(name_str, str): - raise TypeError('Name must be a string.') + raise TypeError("Name must be a string.") self._name = name_str diff --git a/src/easydynamics/base_classes/name_mixin.py b/src/easydynamics/base_classes/name_mixin.py new file mode 100644 index 00000000..6b2c8cec --- /dev/null +++ b/src/easydynamics/base_classes/name_mixin.py @@ -0,0 +1,74 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + + +class NameMixin: + """Mixin class to add name functionality to EasyDynamics classes.""" + + def __init__( + self, + name: str = "MyEasyDynamicsModel", + ) -> None: + """ + Initialize the NameMixin. + + Parameters + ---------- + name : str, default='MyEasyDynamicsModel' + Name of the model. + + Raises + ------ + TypeError + If name is not a string. + """ + if not isinstance(name, str): + raise TypeError("Name must be a string.") + self._name = name + self._name_lock_count = 0 + + @property + def name(self) -> str: + """ + Get the name of the model. + + Returns + ------- + str + The name of the model. + """ + return self._name + + @name.setter + def name(self, name_str: str) -> None: + """ + Set the name of the model. + + Parameters + ---------- + name_str : str + The new name to set. + + Raises + ------ + TypeError + If name_str is not a string. + """ + + if self._name_lock_count > 0: + raise AttributeError("Cannot change name while object is in a list.") + + if not isinstance(name_str, str): + raise TypeError("Name must be a string.") + self._name = name_str + + def lock_name(self) -> None: + """Prevent the name from being modified.""" + self._name_lock_count += 1 + + def unlock_name(self) -> None: + """Allow the name to be modified if no containers remain.""" + if self._name_lock_count == 0: + raise RuntimeError("Name lock count is already zero.") + + self._name_lock_count -= 1 From c5a583187c25274ba1736285b01961a8bb7b84a4 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Fri, 15 May 2026 13:57:54 +0200 Subject: [PATCH 08/20] update component_collection, fix some tests --- .../base_classes/easydynamics_list.py | 33 +- .../sample_model/component_collection.py | 289 +++++------------ .../diffusion_model/diffusion_model_base.py | 32 +- .../test_fitting_with_diffusion_model.py | 36 +-- .../convolution/test_convolution.py | 205 +++++++----- .../convolution/test_numerical_convolution.py | 64 ++-- .../test_numerical_convolution_base.py | 200 +++++++----- .../sample_model/test_background_model.py | 40 +-- .../sample_model/test_component_collection.py | 295 ++++++++---------- .../sample_model/test_model_base.py | 118 +++---- .../sample_model/test_resolution_model.py | 64 ++-- .../sample_model/test_sample_model.py | 160 +++++----- 12 files changed, 751 insertions(+), 785 deletions(-) diff --git a/src/easydynamics/base_classes/easydynamics_list.py b/src/easydynamics/base_classes/easydynamics_list.py index 1806a820..4fe704a0 100644 --- a/src/easydynamics/base_classes/easydynamics_list.py +++ b/src/easydynamics/base_classes/easydynamics_list.py @@ -8,11 +8,10 @@ from typing import TypeVar from easyscience.base_classes.easy_list import EasyList +from easyscience.base_classes.new_base import NewBase from easydynamics.base_classes.name_mixin import NameMixin -from .new_base import NewBase - ProtectedType_ = TypeVar("ProtectedType", bound=NewBase) @@ -76,6 +75,7 @@ def insert(self, index: int, value: ProtectedType_) -> None: value : ProtectedType_ The item to insert. Must be an instance of one of the protected types. """ + self._validate_type(value) self._check_name_unique(value) super().insert(index, value) value.lock_name() @@ -88,6 +88,7 @@ def append(self, value: ProtectedType_) -> None: value : ProtectedType_ The item to append. Must be an instance of one of the protected types. """ + self._validate_type(value) self._check_name_unique(value) super().append(value) value.lock_name() @@ -100,7 +101,12 @@ def extend(self, values: Iterable[ProtectedType_]) -> None: values : Iterable[ProtectedType_] An iterable of items to append. Each item must be an instance of one of the protected types. """ + if not isinstance(values, Iterable): + raise TypeError("Values must be an iterable.") values = list(values) + + for v in values: + self._validate_type(v) self._check_name_unique(values) for v in values: self.append(v) @@ -119,6 +125,8 @@ def pop(self, idx: int) -> ProtectedType_: ProtectedType_ The item that was popped. """ + if not isinstance(idx, int): + raise TypeError("Index must be an integer.") item = self[idx] item.unlock_name() return super().pop(idx) @@ -169,6 +177,27 @@ def _get_key(self, obj: NewBase) -> str: """ return obj.name + def _validate_type(self, value: Any) -> None: + """ + Validate that a value is an instance of one of the protected types. + + Parameters + ---------- + value : Any + The value to validate. + + Raises + ------ + TypeError + If the value is not an instance of one of the protected types. + """ + + if not isinstance(value, tuple(self._protected_types)): + allowed = ", ".join(t.__name__ for t in self._protected_types) + raise TypeError( + f"Value must be an instance of type: {allowed}. Got {type(value).__name__} instead." # noqa: E501 + ) + # ------------------------------------------------------------------ # dunder methods # ------------------------------------------------------------------ diff --git a/src/easydynamics/sample_model/component_collection.py b/src/easydynamics/sample_model/component_collection.py index 61bf1057..bc059026 100644 --- a/src/easydynamics/sample_model/component_collection.py +++ b/src/easydynamics/sample_model/component_collection.py @@ -11,6 +11,7 @@ from easyscience.variable import DescriptorBase from easyscience.variable import Parameter +from easydynamics.base_classes.easydynamics_list import EasyDynamicsList from easydynamics.base_classes.easydynamics_modelbase import EasyDynamicsModelBase from easydynamics.sample_model.components.model_component import ModelComponent @@ -18,16 +19,16 @@ from easydynamics.utils.utils import Numeric -class ComponentCollection(EasyDynamicsModelBase): +class ComponentCollection(EasyDynamicsList, EasyDynamicsModelBase): """ Collection of model components representing a sample, background or resolution model. """ def __init__( self, - components: list[ModelComponent] | None = None, - unit: str | sc.Unit = 'meV', - name: str = 'ComponentCollection', + components: ModelComponent | list[ModelComponent] | None = None, + unit: str | sc.Unit = "meV", + name: str = "ComponentCollection", display_name: str | None = None, unique_name: str | None = None, ) -> None: @@ -36,7 +37,7 @@ def __init__( Parameters ---------- - components : list[ModelComponent] | None, default=None + components : ModelComponent | list[ModelComponent] | None, default=None Initial model components to add to the ComponentCollection. unit : str | sc.Unit, default='meV' Unit of the collection. @@ -53,66 +54,40 @@ def __init__( If unit is not a string or sc.Unit, or if components is not a list of ModelComponent. """ - super().__init__( + EasyDynamicsModelBase.__init__( + self, unit=unit, name=name, display_name=display_name, unique_name=unique_name, ) - self._components = [] - - # 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.') + if isinstance(components, ModelComponent): + components = [components] + elif not isinstance(components, list): + raise TypeError( + f"components must be a ModelComponent or a list of ModelComponent, got {type(components).__name__} instead." # noqa: E501 + ) for comp in components: - self.append_component(comp) + if not isinstance(comp, ModelComponent): + raise TypeError( + f"All items in components must be instances of ModelComponent, got {type(comp).__name__} instead." # noqa: E501 + ) + + EasyDynamicsList.__init__( + self, + *(components or []), + protected_types=ModelComponent, + name=name, + display_name=display_name, + unique_name=unique_name, + ) # ------------------------------------------------------------------ # Properties # ------------------------------------------------------------------ - @property - def components(self) -> list[ModelComponent]: - """ - Get the list of components in the collection. - - Returns - ------- - list[ModelComponent] - The components in the collection. - """ - - return list(self._components) - - @components.setter - def components(self, components: list[ModelComponent]) -> None: - """ - Set the list of components in the collection. - - Parameters - ---------- - components : list[ModelComponent] - The new list of components. - - Raises - ------ - TypeError - If components is not a list of ModelComponent. - """ - - if not isinstance(components, list): - 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.' - ) - - self._components = components - @property def is_empty(self) -> bool: """ @@ -123,7 +98,7 @@ def is_empty(self) -> bool: bool True if the collection has no components, False otherwise. """ - return not self._components + return not self @is_empty.setter def is_empty(self, _value: bool) -> None: @@ -141,8 +116,8 @@ def is_empty(self, _value: bool) -> None: Always raised since is_empty is read-only. """ raise AttributeError( - 'is_empty is a read-only property that indicates ' - 'whether the collection has components.' + "is_empty is a read-only property that indicates " + "whether the collection has components." ) def convert_unit(self, unit: str | sc.Unit) -> None: @@ -163,18 +138,20 @@ def convert_unit(self, unit: str | sc.Unit) -> None: """ if not isinstance(unit, (str, sc.Unit)): - raise TypeError(f'Unit must be a string or sc.Unit, got {type(unit).__name__}') + raise TypeError( + f"Unit must be a string or sc.Unit, got {type(unit).__name__}" + ) old_unit = self._unit try: - for component in self.components: + for component in self: component.convert_unit(unit) self._unit = unit except Exception as e: # Attempt to rollback on failure try: - for component in self.components: + for component in self: component.convert_unit(old_unit) except Exception: # noqa: S110 pass # Best effort rollback @@ -194,133 +171,11 @@ def append_component(self, component: ModelComponent | ComponentCollection) -> N component : ModelComponent | ComponentCollection The component to append. If a ComponentCollection is provided, all of its components will be appended. - - Raises - ------ - TypeError - If component is not a ModelComponent or ComponentCollection. - ValueError - If a component with the same unique name already exists in the collection. """ - if not isinstance(component, (ModelComponent, ComponentCollection)): - raise TypeError( - 'Component must be an instance of ModelComponent or ComponentCollection. ' - f'Got {type(component).__name__} instead.' - ) - if isinstance(component, ModelComponent): - components = (component,) if isinstance(component, ComponentCollection): - components = component.components - - 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()}' - ) - - self._components.append(comp) - - def remove_component(self, unique_name: str) -> None: - """ - Remove a component from the collection by its unique name. - - Parameters - ---------- - unique_name : str - Unique name of the component to remove. - - Raises - ------ - TypeError - If unique_name is not a string. - KeyError - If no component with the given unique name exists in the collection. - """ - - if not isinstance(unique_name, str): - raise TypeError('Component name must be a string.') - - for comp in self._components: - if comp.unique_name == unique_name: - self._components.remove(comp) - return - - 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()}' - ) - - @property - def components(self) -> list[ModelComponent]: - """ - Get the list of components in the collection. - - Returns - ------- - list[ModelComponent] - The components in the collection. - """ - return list(self._components) - - @components.setter - def components(self, components: list[ModelComponent]) -> None: - """ - Set the components in the collection. - - Parameters - ---------- - components : list[ModelComponent] - The new components in the collection. - - Raises - ------ - TypeError - If components is not a list of ModelComponent. - """ - if not isinstance(components, list): - 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.' - ) - - self._components = components - - @property - def is_empty(self) -> bool: - """ - Returns True if the collection has no components, otherwise False. - - Returns - ------- - bool - True if the collection has no components, otherwise False. - """ - return not self._components - - @is_empty.setter - def is_empty(self, _value: bool) -> None: - """ - Is_empty is read-only. - - Parameters - ---------- - _value : bool - Ignored. - - Raises - ------ - AttributeError - Always raised since is_empty is read-only. - """ - raise AttributeError( - 'is_empty is a read-only property that indicates ' - 'whether the collection has components.' - ) + self.extend(component) + else: + self.append(component) def list_component_names(self) -> list[str]: """ @@ -329,14 +184,10 @@ def list_component_names(self) -> list[str]: Returns ------- list[str] - List of unique names of the components in the collection. + List of names of the components in the collection. """ - return [component.unique_name for component in self._components] - - def clear_components(self) -> None: - """Remove all components.""" - self._components.clear() + return [component.name for component in self] def normalize_area(self) -> None: """ @@ -350,29 +201,29 @@ def normalize_area(self) -> None: If there are no components in the model or if the total area is zero or not finite, which would prevent normalization. """ - if not self.components: - raise ValueError('No components in the model to normalize.') + if not self: + 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'): + for component in self: + 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"Component '{component.name}' does not have an 'area' attribute " + f"and will be skipped in normalization.", UserWarning, stacklevel=2, ) 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 @@ -391,9 +242,11 @@ def get_all_variables(self) -> list[DescriptorBase]: 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 for var in component.get_all_variables()] - 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. @@ -408,14 +261,14 @@ def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) Evaluated model values. """ - if not self.components: + if not self: return np.zeros_like(x) - return sum(component.evaluate(x) for component in self.components) + return sum(component.evaluate(x) for component in self) def evaluate_component( self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray, - unique_name: str, + name: str, ) -> np.ndarray: """ Evaluate a single component by name. @@ -424,34 +277,34 @@ def evaluate_component( ---------- x : Numeric | list | np.ndarray | sc.Variable | sc.DataArray Energy axis. - unique_name : str - Component unique name. + name : str + Component name. Raises ------ ValueError If there are no components in the model. TypeError - If unique_name is not a string. + If name is not a string. KeyError - If no component with the given unique name exists in the collection. + If no component with the given name exists in the collection. Returns ------- np.ndarray Evaluated values for the specified component. """ - if not self.components: - raise ValueError('No components in the model to evaluate.') + if not self: + raise ValueError("No components in the model to evaluate.") - if not isinstance(unique_name, str): + if not isinstance(name, str): raise TypeError( - f'Component unique name must be a string, got {type(unique_name)} instead.' + f"Component name must be a string, got {type(name)} instead." ) - matches = [comp for comp in self.components if comp.unique_name == unique_name] + matches = [comp for comp in self if comp.name == name] if not matches: - raise KeyError(f"No component named '{unique_name}' exists.") + raise KeyError(f"No component named '{name}' exists.") component = matches[0] @@ -487,11 +340,11 @@ def __contains__(self, item: str | ModelComponent) -> bool: """ if isinstance(item, str): - # Check by component unique name - return any(comp.unique_name == item for comp in self.components) + # Check by component name + return any(comp.name == item for comp in self) if isinstance(item, ModelComponent): # Check by component instance - return any(comp is item for comp in self.components) + return any(comp is item for comp in self) return False def __repr__(self) -> str: @@ -503,6 +356,6 @@ def __repr__(self) -> str: str String representation of the ComponentCollection. """ - comp_names = ', '.join(c.unique_name for c in self.components) or 'No components' + comp_names = ", ".join(c.name for c in self) or "No components" - return f"" + return f"" 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 95732680..0671e09c 100644 --- a/src/easydynamics/sample_model/diffusion_model/diffusion_model_base.py +++ b/src/easydynamics/sample_model/diffusion_model/diffusion_model_base.py @@ -16,9 +16,9 @@ class DiffusionModelBase(EasyDynamicsModelBase): def __init__( self, scale: Numeric = 1.0, - unit: str | sc.Unit = 'meV', - name: str = 'DiffusionModel', - display_name: str | None = 'MyDiffusionModel', + unit: str | sc.Unit = "meV", + name: str = "DiffusionModel", + display_name: str | None = "MyDiffusionModel", unique_name: str | None = None, ) -> None: """ @@ -47,19 +47,23 @@ def __init__( """ try: - test = DescriptorNumber(name='test', value=1, unit=unit) - test.convert_unit('meV') + test = DescriptorNumber(name="test", value=1, unit=unit) + test.convert_unit("meV") except Exception as e: raise UnitError( - f'Invalid unit: {unit}. Unit must be a string or scipp Unit and convertible to meV.' # noqa: E501 + f"Invalid unit: {unit}. Unit must be a string or scipp Unit and convertible to meV." # noqa: E501 ) from e if not isinstance(scale, Numeric): - raise TypeError('scale must be a number.') + raise TypeError("scale must be a number.") - scale = Parameter(name='scale', value=float(scale), fixed=False, min=0.0, unit=unit) + scale = Parameter( + name="scale", value=float(scale), fixed=False, min=0.0, unit=unit + ) - super().__init__(unit=unit, name=name, display_name=display_name, unique_name=unique_name) + super().__init__( + unit=unit, name=name, display_name=display_name, unique_name=unique_name + ) self._scale = scale # ------------------------------------------------------------------ @@ -96,10 +100,10 @@ def scale(self, scale: Numeric) -> None: If scale is negative. """ if not isinstance(scale, Numeric): - raise TypeError('scale must be a number.') + raise TypeError("scale must be a number.") if float(scale) < 0: - raise ValueError('scale must be non-negative.') + raise ValueError("scale must be non-negative.") self._scale.value = float(scale) # ------------------------------------------------------------------ @@ -116,7 +120,7 @@ def __repr__(self) -> str: String representation of the DiffusionModel. """ return ( - f'{self.__class__.__name__}(name={self.name}, display_name={self.display_name}, ' - f'unit={self.unit}), \n' - f' scale={self.scale})' + f"{self.__class__.__name__}(name={self.name}, display_name={self.display_name}, " + f"unit={self.unit}), \n" + f" scale={self.scale})" ) diff --git a/tests/integration/fitting/test_fitting_with_diffusion_model.py b/tests/integration/fitting/test_fitting_with_diffusion_model.py index 0b3abc82..84e303d3 100644 --- a/tests/integration/fitting/test_fitting_with_diffusion_model.py +++ b/tests/integration/fitting/test_fitting_with_diffusion_model.py @@ -23,20 +23,20 @@ class TestFittingWithDiffusionModel: def test_fitting_with_diffusion_model(self): # Load the vanadium data - vanadium_experiment = Experiment('Vanadium') + vanadium_experiment = Experiment("Vanadium") file_path = pooch.retrieve( - url='https://github.com/easyscience/dynamics-lib/raw/refs/heads/master/docs/docs/tutorials/data/vanadium_data_example.h5', - known_hash='16cc1b327c303feeb88fb9dda5390dc4880b62396b1793f98c6fef0b27c7b873', + url="https://github.com/easyscience/dynamics-lib/raw/refs/heads/master/docs/docs/tutorials/data/vanadium_data_example.h5", + known_hash="16cc1b327c303feeb88fb9dda5390dc4880b62396b1793f98c6fef0b27c7b873", ) vanadium_experiment.load_hdf5(filename=file_path) - delta_function = DeltaFunction(display_name='DeltaFunction', area=1) + delta_function = DeltaFunction(display_name="DeltaFunction", area=1) sample_model = SampleModel(components=delta_function) resolution_components = ComponentCollection() - res_gauss = Gaussian(width=0.1, area=1, display_name='Res. Gauss') + res_gauss = Gaussian(width=0.1, area=1, name="Res. Gauss") res_gauss.area.fixed = True resolution_components.append_component(res_gauss) resolution_model = ResolutionModel(components=resolution_components) @@ -49,31 +49,31 @@ def test_fitting_with_diffusion_model(self): ) vanadium_analysis = Analysis( - display_name='Vanadium Full Analysis', + display_name="Vanadium Full Analysis", experiment=vanadium_experiment, sample_model=sample_model, instrument_model=instrument_model, ) fit_result_independent_single_Q = vanadium_analysis.fit( - fit_method='independent', Q_index=5 + fit_method="independent", Q_index=5 ) assert fit_result_independent_single_Q.success assert fit_result_independent_single_Q.chi2 < 75.0 assert fit_result_independent_single_Q.reduced_chi2 < 0.4 - diffusion_experiment = Experiment('Diffusion') + diffusion_experiment = Experiment("Diffusion") file_path = pooch.retrieve( - url='https://github.com/easyscience/dynamics-lib/raw/refs/heads/master/docs/docs/tutorials/data/diffusion_data_example.h5', - known_hash='5fe846b19aacbda8b8b936eb2e5310d025dc56c25b0b353521e7d6b921f229ab', + url="https://github.com/easyscience/dynamics-lib/raw/refs/heads/master/docs/docs/tutorials/data/diffusion_data_example.h5", + known_hash="5fe846b19aacbda8b8b936eb2e5310d025dc56c25b0b353521e7d6b921f229ab", ) diffusion_experiment.load_hdf5(filename=file_path) - delta_function = DeltaFunction(display_name='DeltaFunction', area=0.2) - lorentzian = Lorentzian(display_name='Lorentzian', area=0.5, width=0.3) + delta_function = DeltaFunction(display_name="DeltaFunction", area=0.2) + lorentzian = Lorentzian(display_name="Lorentzian", area=0.5, width=0.3) component_collection = ComponentCollection( components=[delta_function, lorentzian], ) @@ -91,13 +91,13 @@ def test_fitting_with_diffusion_model(self): instrument_model.resolution_model.fix_all_parameters() diffusion_analysis = Analysis( - display_name='Diffusion Analysis', + display_name="Diffusion Analysis", experiment=diffusion_experiment, sample_model=sample_model, instrument_model=instrument_model, ) - fit_result = diffusion_analysis.fit(fit_method='independent') + fit_result = diffusion_analysis.fit(fit_method="independent") assert fit_result[0].success assert fit_result[0].chi2 < 43.0 @@ -107,12 +107,12 @@ def test_fitting_with_diffusion_model(self): # Diffusion model ############### - delta_function = DeltaFunction(display_name='DeltaFunction', area=0.2) + delta_function = DeltaFunction(display_name="DeltaFunction", area=0.2) component_collection = ComponentCollection( components=[delta_function], ) diffusion_model = BrownianTranslationalDiffusion( - display_name='Brownian Translational Diffusion', + display_name="Brownian Translational Diffusion", diffusion_coefficient=2.4e-9, scale=0.5, ) @@ -130,7 +130,7 @@ def test_fitting_with_diffusion_model(self): ) diffusion_model_analysis = Analysis( - display_name='Diffusion Full Analysis', + display_name="Diffusion Full Analysis", experiment=diffusion_experiment, sample_model=sample_model, instrument_model=instrument_model, @@ -138,7 +138,7 @@ def test_fitting_with_diffusion_model(self): diffusion_model_analysis.instrument_model.resolution_model.fix_all_parameters() - fit_result = diffusion_model_analysis.fit(fit_method='simultaneous') + fit_result = diffusion_model_analysis.fit(fit_method="simultaneous") assert fit_result[0].success assert fit_result[0].chi2 < 56.0 diff --git a/tests/unit/easydynamics/convolution/test_convolution.py b/tests/unit/easydynamics/convolution/test_convolution.py index f3aff801..3716b724 100644 --- a/tests/unit/easydynamics/convolution/test_convolution.py +++ b/tests/unit/easydynamics/convolution/test_convolution.py @@ -26,23 +26,25 @@ class TestConvolution: @pytest.fixture def default_convolution(self): energy = np.linspace(-10, 10, 5001) - sample_components = ComponentCollection(display_name='ComponentCollection') + sample_components = ComponentCollection(display_name="ComponentCollection") sample_components.append_component( - Gaussian(display_name='Gaussian1', area=2.0, center=0.1, width=0.4) + Gaussian(name="Gaussian1", area=2.0, center=0.1, width=0.4) ) sample_components.append_component( - DampedHarmonicOscillator(display_name='DHO1', area=2.0, center=1.0, width=0.1) + DampedHarmonicOscillator( + display_name="DHO1", area=2.0, center=1.0, width=0.1 + ) ) sample_components.append_component( - DeltaFunction(display_name='Delta1', area=2.0, center=0.3) + DeltaFunction(display_name="Delta1", area=2.0, center=0.3) ) - resolution_components = ComponentCollection(display_name='ResolutionModel') + resolution_components = ComponentCollection(display_name="ResolutionModel") resolution_components.append_component( - Gaussian(display_name='GaussianRes', area=3.0, center=0.2, width=0.5) + Gaussian(name="GaussianRes", area=3.0, center=0.2, width=0.5) ) return Convolution( @@ -54,10 +56,10 @@ def default_convolution(self): @pytest.fixture def convolution_with_components(self): energy = np.linspace(-10, 10, 5001) - sample_components = Gaussian(display_name='Gaussian1', area=2.0, center=0.1, width=0.4) + sample_components = Gaussian(name="Gaussian1", area=2.0, center=0.1, width=0.4) resolution_components = Gaussian( - display_name='GaussianRes', area=3.0, center=0.2, width=0.5 + name="GaussianRes", area=3.0, center=0.2, width=0.5 ) return Convolution( @@ -71,33 +73,48 @@ def test_init(self, default_convolution): # WHEN THEN EXPECT assert isinstance(default_convolution, Convolution) assert isinstance(default_convolution.energy, sc.Variable) - assert np.allclose(default_convolution.energy.values, np.linspace(-10, 10, 5001)) + assert np.allclose( + default_convolution.energy.values, np.linspace(-10, 10, 5001) + ) assert isinstance(default_convolution._sample_components, ComponentCollection) - assert isinstance(default_convolution._resolution_components, ComponentCollection) + assert isinstance( + default_convolution._resolution_components, ComponentCollection + ) assert default_convolution.upsample_factor == 5 assert default_convolution.extension_factor == pytest.approx(0.2) assert default_convolution.temperature is None - assert default_convolution.unit == 'meV' - assert default_convolution.detailed_balance_settings.normalize_detailed_balance is True + assert default_convolution.unit == "meV" + assert ( + default_convolution.detailed_balance_settings.normalize_detailed_balance + is True + ) assert isinstance(default_convolution._energy_grid, EnergyGrid) - assert isinstance(default_convolution._analytical_sample_components, ComponentCollection) + assert isinstance( + default_convolution._analytical_sample_components, ComponentCollection + ) assert ( default_convolution._analytical_sample_components.components[0] is default_convolution.sample_components.components[0] ) - assert isinstance(default_convolution._numerical_sample_components, ComponentCollection) + assert isinstance( + default_convolution._numerical_sample_components, ComponentCollection + ) assert ( default_convolution._numerical_sample_components.components[0] is default_convolution.sample_components.components[1] ) - assert isinstance(default_convolution._delta_sample_components, ComponentCollection) + assert isinstance( + default_convolution._delta_sample_components, ComponentCollection + ) assert ( default_convolution._delta_sample_components.components[0] is default_convolution.sample_components.components[2] ) - assert default_convolution.convolution_settings.convolution_plan_is_valid is True + assert ( + default_convolution.convolution_settings.convolution_plan_is_valid is True + ) assert default_convolution._reactions_enabled is True def test_init_components(self, convolution_with_components): @@ -105,13 +122,19 @@ def test_init_components(self, convolution_with_components): # WHEN THEN EXPECT assert isinstance(convolution_with_components, Convolution) assert isinstance(convolution_with_components.energy, sc.Variable) - assert np.allclose(convolution_with_components.energy.values, np.linspace(-10, 10, 5001)) - assert isinstance(convolution_with_components._sample_components, ComponentCollection) - assert isinstance(convolution_with_components._resolution_components, ComponentCollection) + assert np.allclose( + convolution_with_components.energy.values, np.linspace(-10, 10, 5001) + ) + assert isinstance( + convolution_with_components._sample_components, ComponentCollection + ) + assert isinstance( + convolution_with_components._resolution_components, ComponentCollection + ) assert convolution_with_components.upsample_factor == 5 assert convolution_with_components.extension_factor == pytest.approx(0.2) assert convolution_with_components.temperature is None - assert convolution_with_components.unit == 'meV' + assert convolution_with_components.unit == "meV" assert ( convolution_with_components.detailed_balance_settings.normalize_detailed_balance is True @@ -136,7 +159,10 @@ def test_init_components(self, convolution_with_components): convolution_with_components._delta_sample_components, ComponentCollection ) assert convolution_with_components._delta_sample_components.is_empty - assert convolution_with_components.convolution_settings.convolution_plan_is_valid is True + assert ( + convolution_with_components.convolution_settings.convolution_plan_is_valid + is True + ) assert convolution_with_components._reactions_enabled is True def test_convolution_plan_is_built_when_invalid(self, default_convolution): @@ -148,7 +174,7 @@ def test_convolution_plan_is_built_when_invalid(self, default_convolution): conv.convolution_settings.convolution_plan_is_valid = False # THEN EXPECT - with patch.object(conv, '_build_convolution_plan') as build_plan: + with patch.object(conv, "_build_convolution_plan") as build_plan: conv.convolution() build_plan.assert_called_once() @@ -162,7 +188,7 @@ def test_convolution_calls_analytical_convolver(self, default_convolution): # THEN EXPECT with patch.object( - conv._analytical_convolver, 'convolution', return_value=np.array([1.0]) + conv._analytical_convolver, "convolution", return_value=np.array([1.0]) ) as analytical_conv: conv.convolution() analytical_conv.assert_called_once() @@ -177,7 +203,7 @@ def test_convolution_calls_numerical_convolver(self, default_convolution): # THEN EXPECT with patch.object( - conv._numerical_convolver, 'convolution', return_value=np.array([1.0]) + conv._numerical_convolver, "convolution", return_value=np.array([1.0]) ) as numerical_conv: conv.convolution() numerical_conv.assert_called_once() @@ -193,23 +219,25 @@ def test_convolution_calls_convolve_delta_functions(self, default_convolution): # THEN EXPECT with patch.object( conv, - '_convolve_delta_functions', + "_convolve_delta_functions", return_value=np.array([1.0]), ) as delta_eval: conv.convolution() delta_eval.assert_called_once() @pytest.mark.parametrize( - 'analytical_component', + "analytical_component", [True, False], - ids=['with_analytical', 'without_analytical'], + ids=["with_analytical", "without_analytical"], ) @pytest.mark.parametrize( - 'numerical_component', + "numerical_component", [True, False], - ids=['with_numerical', 'without_numerical'], + ids=["with_numerical", "without_numerical"], + ) + @pytest.mark.parametrize( + "delta_component", [True, False], ids=["with_delta", "without_delta"] ) - @pytest.mark.parametrize('delta_component', [True, False], ids=['with_delta', 'without_delta']) def test_convolution_calls_correct_methods( self, default_convolution, @@ -228,13 +256,13 @@ def test_convolution_calls_correct_methods( if analytical_component: sample_components.append_component( - Gaussian(display_name='Gaussian', area=1.0, center=0.0, width=0.1) + Gaussian(name="Gaussian", area=1.0, center=0.0, width=0.1) ) if numerical_component: sample_components.append_component( DampedHarmonicOscillator( - display_name='DampedHarmonicOscillator', + display_name="DampedHarmonicOscillator", area=1.0, center=1.0, width=0.1, @@ -243,10 +271,12 @@ def test_convolution_calls_correct_methods( if delta_component: sample_components.append_component( - DeltaFunction(display_name='DeltaFunction', area=1.0, center=0.0) + DeltaFunction(display_name="DeltaFunction", area=1.0, center=0.0) ) - conv.sample_components = sample_components # This updates the internal sample models + conv.sample_components = ( + sample_components # This updates the internal sample models + ) conv._build_convolution_plan() # Ensure the plan is built with the new components # THEN @@ -254,21 +284,21 @@ def test_convolution_calls_correct_methods( # component type is not present. if analytical_component: patch_analytical = patch.object( - conv._analytical_convolver, 'convolution', return_value=np.array([1.0]) + conv._analytical_convolver, "convolution", return_value=np.array([1.0]) ) else: patch_analytical = nullcontext() if numerical_component: patch_numerical = patch.object( - conv._numerical_convolver, 'convolution', return_value=np.array([1.0]) + conv._numerical_convolver, "convolution", return_value=np.array([1.0]) ) else: patch_numerical = nullcontext() patch_delta = patch.object( conv, - '_convolve_delta_functions', + "_convolve_delta_functions", return_value=np.array([1.0]), ) @@ -313,7 +343,7 @@ def test_convolve_delta_functions(self, default_convolution): expected_center = 0.3 + 0.2 # Delta center + Resolution center expected_width = 0.5 # Resolution width expected_values = Gaussian( - display_name='ExpectedGaussian', + name="ExpectedGaussian", area=expected_area, center=expected_center, width=expected_width, @@ -323,10 +353,10 @@ def test_convolve_delta_functions(self, default_convolution): # List of analytic functions analytic_functions: ClassVar[list[object]] = [ - Gaussian(display_name='G', area=1.0, center=0.0, width=0.1), - Lorentzian(display_name='L', area=1.0, center=0.0, width=0.1), + Gaussian(name="G", area=1.0, center=0.0, width=0.1), + Lorentzian(display_name="L", area=1.0, center=0.0, width=0.1), Voigt( - display_name='V', + display_name="V", area=1.0, center=0.0, gaussian_width=0.1, @@ -336,8 +366,8 @@ def test_convolve_delta_functions(self, default_convolution): # List of non-analytic functions non_analytic_functions: ClassVar[list[object]] = [ - DampedHarmonicOscillator(display_name='DHO', area=1.0, center=1.0, width=0.1), - Polynomial(display_name='P', coefficients=[1.0, 0.0, 0.0]), + DampedHarmonicOscillator(display_name="DHO", area=1.0, center=1.0, width=0.1), + Polynomial(display_name="P", coefficients=[1.0, 0.0, 0.0]), ] all_functions_except_delta: ClassVar[list[object]] = [ @@ -347,12 +377,14 @@ def test_convolve_delta_functions(self, default_convolution): all_functions: ClassVar[list[object]] = [ *all_functions_except_delta, - DeltaFunction(display_name='Delta', area=1.0, center=0.0), + DeltaFunction(display_name="Delta", area=1.0, center=0.0), ] - @pytest.mark.parametrize('function1', all_functions, ids=lambda f: f.__class__.__name__) @pytest.mark.parametrize( - 'function2', all_functions_except_delta, ids=lambda f: f.__class__.__name__ + "function1", all_functions, ids=lambda f: f.__class__.__name__ + ) + @pytest.mark.parametrize( + "function2", all_functions_except_delta, ids=lambda f: f.__class__.__name__ ) def test_check_if_pair_is_analytic(self, default_convolution, function1, function2): """ @@ -375,20 +407,22 @@ def test_check_if_pair_is_analytic(self, default_convolution, function1, functio expected = is_analytic1 and is_analytic2 assert result == expected - def test_check_if_pair_is_analytic_raises_with_delta_in_resolution(self, default_convolution): + def test_check_if_pair_is_analytic_raises_with_delta_in_resolution( + self, default_convolution + ): """ Test that _check_if_pair_is_analytic raises TypeError when resolution component is DeltaFunction. """ # WHEN conv = default_convolution - sample_component = Gaussian(display_name='G', area=1.0, center=0.0, width=0.1) - resolution_component = DeltaFunction(display_name='Delta', area=1.0, center=0.0) + sample_component = Gaussian(name="G", area=1.0, center=0.0, width=0.1) + resolution_component = DeltaFunction(display_name="Delta", area=1.0, center=0.0) # THEN EXPECT with pytest.raises( TypeError, - match='This is not supported', + match="This is not supported", ): conv._check_if_pair_is_analytic( sample_component=sample_component, @@ -396,18 +430,18 @@ def test_check_if_pair_is_analytic_raises_with_delta_in_resolution(self, default ) @pytest.mark.parametrize( - 'sample_component,resolution_component', + "sample_component,resolution_component", [ ( - 'NotAModelComponent', - Gaussian(display_name='G', area=1.0, center=0.0, width=0.1), + "NotAModelComponent", + Gaussian(name="G", area=1.0, center=0.0, width=0.1), ), ( - Gaussian(display_name='G', area=1.0, center=0.0, width=0.1), - 'NotAModelComponent', + Gaussian(name="G", area=1.0, center=0.0, width=0.1), + "NotAModelComponent", ), ], - ids=['invalid_sample_component', 'invalid_resolution_component'], + ids=["invalid_sample_component", "invalid_resolution_component"], ) def test_check_if_pair_is_analytic_raises_with_invalid_types( self, default_convolution, sample_component, resolution_component @@ -422,7 +456,7 @@ def test_check_if_pair_is_analytic_raises_with_invalid_types( # THEN EXPECT with pytest.raises( TypeError, - match='must be a ModelComponent', + match="must be a ModelComponent", ): conv._check_if_pair_is_analytic( sample_component=sample_component, @@ -430,18 +464,20 @@ def test_check_if_pair_is_analytic_raises_with_invalid_types( ) @pytest.mark.parametrize( - 'analytical_component', + "analytical_component", [True, False], - ids=['with_analytical', 'without_analytical'], + ids=["with_analytical", "without_analytical"], ) @pytest.mark.parametrize( - 'numerical_component', + "numerical_component", [True, False], - ids=['with_numerical', 'without_numerical'], + ids=["with_numerical", "without_numerical"], + ) + @pytest.mark.parametrize( + "delta_component", [True, False], ids=["with_delta", "without_delta"] ) - @pytest.mark.parametrize('delta_component', [True, False], ids=['with_delta', 'without_delta']) @pytest.mark.parametrize( - 'temperature', [None, 100], ids=['with_temperature', 'without_temperature'] + "temperature", [None, 100], ids=["with_temperature", "without_temperature"] ) def test_build_convolution_plan( self, @@ -462,13 +498,13 @@ def test_build_convolution_plan( if analytical_component: sample_components.append_component( - Gaussian(display_name='Gaussian', area=1.0, center=0.0, width=0.1) + Gaussian(name="Gaussian", area=1.0, center=0.0, width=0.1) ) if numerical_component: sample_components.append_component( DampedHarmonicOscillator( - display_name='DampedHarmonicOscillator', + display_name="DampedHarmonicOscillator", area=1.0, center=1.0, width=0.1, @@ -477,11 +513,13 @@ def test_build_convolution_plan( if delta_component: sample_components.append_component( - DeltaFunction(display_name='DeltaFunction', area=1.0, center=0.0) + DeltaFunction(display_name="DeltaFunction", area=1.0, center=0.0) ) # THEN - conv.sample_components = sample_components # This updates the internal sample models + conv.sample_components = ( + sample_components # This updates the internal sample models + ) if temperature is not None: conv.temperature = temperature conv._build_convolution_plan() @@ -490,14 +528,20 @@ def test_build_convolution_plan( assert isinstance(conv._analytical_sample_components, ComponentCollection) if analytical_component and not temperature: assert len(conv._analytical_sample_components.components) == 1 - assert conv._analytical_sample_components.components[0].display_name == 'Gaussian' + assert ( + conv._analytical_sample_components.components[0].display_name + == "Gaussian" + ) else: assert len(conv._analytical_sample_components.components) == 0 assert isinstance(conv._delta_sample_components, ComponentCollection) if delta_component: assert len(conv._delta_sample_components.components) == 1 - assert conv._delta_sample_components.components[0].display_name == 'DeltaFunction' + assert ( + conv._delta_sample_components.components[0].display_name + == "DeltaFunction" + ) else: assert len(conv._delta_sample_components.components) == 0 @@ -508,7 +552,7 @@ def test_build_convolution_plan( assert len(conv._numerical_sample_components.components) == 1 assert ( conv._numerical_sample_components.components[0].display_name - == 'DampedHarmonicOscillator' + == "DampedHarmonicOscillator" ) else: assert len(conv._numerical_sample_components.components) == 0 @@ -520,19 +564,22 @@ def test_build_convolution_plan( expected_numerical_count += 1 if analytical_component: expected_numerical_count += 1 - assert len(conv._numerical_sample_components.components) == expected_numerical_count + assert ( + len(conv._numerical_sample_components.components) + == expected_numerical_count + ) assert conv.convolution_settings.convolution_plan_is_valid is True @pytest.mark.parametrize( - 'analytical_component', + "analytical_component", [True, False], - ids=['with_analytical', 'without_analytical'], + ids=["with_analytical", "without_analytical"], ) @pytest.mark.parametrize( - 'numerical_component', + "numerical_component", [True, False], - ids=['with_numerical', 'without_numerical'], + ids=["with_numerical", "without_numerical"], ) def test_set_convolvers( self, @@ -551,13 +598,13 @@ def test_set_convolvers( if analytical_component: sample_components.append_component( - Gaussian(display_name='Gaussian', area=1.0, center=0.0, width=0.1) + Gaussian(name="Gaussian", area=1.0, center=0.0, width=0.1) ) if numerical_component: sample_components.append_component( DampedHarmonicOscillator( - display_name='DampedHarmonicOscillator', + display_name="DampedHarmonicOscillator", area=1.0, center=1.0, width=0.1, @@ -596,14 +643,14 @@ def test_setattr_does_not_invalidate_plan_for_non_tracked_attribute( old_delta_id = id(conv._delta_sample_components) # THEN (NOT in _invalidate_plan_on_change) - conv.display_name = 'new_name' + conv.display_name = "new_name" # EXPECT assert conv.convolution_settings.convolution_plan_is_valid is True assert id(conv._analytical_sample_components) == old_plan_id assert id(conv._numerical_sample_components) == old_numerical_id assert id(conv._delta_sample_components) == old_delta_id - assert conv.display_name == 'new_name' + assert conv.display_name == "new_name" def test_setattr_invalidates_plan_for_tracked_attribute( self, diff --git a/tests/unit/easydynamics/convolution/test_numerical_convolution.py b/tests/unit/easydynamics/convolution/test_numerical_convolution.py index 7b6ada94..d9deabb8 100644 --- a/tests/unit/easydynamics/convolution/test_numerical_convolution.py +++ b/tests/unit/easydynamics/convolution/test_numerical_convolution.py @@ -17,13 +17,13 @@ class TestNumericalConvolution: @pytest.fixture def default_numerical_convolution(self): energy = np.linspace(-10, 10, 5001) - sample_components = ComponentCollection(display_name='ComponentCollection') + sample_components = ComponentCollection(display_name="ComponentCollection") sample_components.append_component( - Gaussian(display_name='Gaussian1', area=2.0, center=0.1, width=0.4) + Gaussian(name="Gaussian1", area=2.0, center=0.1, width=0.4) ) - resolution_components = ComponentCollection(display_name='ResolutionModel') + resolution_components = ComponentCollection(display_name="ResolutionModel") resolution_components.append_component( - Gaussian(display_name='GaussianRes', area=3.0, center=0.2, width=0.5) + Gaussian(name="GaussianRes", area=3.0, center=0.2, width=0.5) ) return NumericalConvolution( @@ -40,22 +40,26 @@ def test_init(self, default_numerical_convolution): # WHEN THEN EXPECT assert isinstance(default_numerical_convolution, NumericalConvolution) assert isinstance(default_numerical_convolution.energy, sc.Variable) - assert np.allclose(default_numerical_convolution.energy.values, np.linspace(-10, 10, 5001)) - assert isinstance(default_numerical_convolution._sample_components, ComponentCollection) + assert np.allclose( + default_numerical_convolution.energy.values, np.linspace(-10, 10, 5001) + ) + assert isinstance( + default_numerical_convolution._sample_components, ComponentCollection + ) assert isinstance( default_numerical_convolution._resolution_components, ComponentCollection ) assert default_numerical_convolution.upsample_factor == 5 assert default_numerical_convolution.extension_factor == pytest.approx(0.2) assert default_numerical_convolution.temperature is None - assert default_numerical_convolution.unit == 'meV' + assert default_numerical_convolution.unit == "meV" assert ( default_numerical_convolution.detailed_balance_settings.normalize_detailed_balance is True ) assert isinstance(default_numerical_convolution._energy_grid, EnergyGrid) - @pytest.mark.parametrize('upsample_factor', [None, 5]) + @pytest.mark.parametrize("upsample_factor", [None, 5]) def test_convolution(self, default_numerical_convolution, upsample_factor): """ Test that convolution of two Gaussians produces the @@ -67,13 +71,15 @@ def test_convolution(self, default_numerical_convolution, upsample_factor): result = default_numerical_convolution.convolution() # EXPECT - expected_area = 2.0 * 3.0 # area of sample_components * area of resolution_components + expected_area = ( + 2.0 * 3.0 + ) # area of sample_components * area of resolution_components expected_center = ( 0.1 + 0.2 + 0.4 ) # center of sample_components + center of resolution_components expected_width = np.sqrt(0.4**2 + 0.5**2) # sqrt(width_sample^2 + width_res^2) expected_result = Gaussian( - display_name='ExpectedConvolution', + name="ExpectedConvolution", area=expected_area, center=expected_center, width=expected_width, @@ -102,8 +108,12 @@ def test_convolution_with_temperature( resolution_vals = default_numerical_convolution._resolution_components.evaluate( default_numerical_convolution.energy.values ) - DBF = detailed_balance_factor(energy=default_numerical_convolution.energy, temperature=5.0) - expected_result = fftconvolve(sample_valds * DBF, resolution_vals, mode='same') * ( + DBF = detailed_balance_factor( + energy=default_numerical_convolution.energy, temperature=5.0 + ) + expected_result = fftconvolve( + sample_valds * DBF, resolution_vals, mode="same" + ) * ( default_numerical_convolution.energy.values[1] - default_numerical_convolution.energy.values[0] ) @@ -111,7 +121,7 @@ def test_convolution_with_temperature( assert np.allclose(result, expected_result, rtol=1e-4) @pytest.mark.parametrize( - 'plan_valid, suppress_warnings, use_db, upsample', + "plan_valid, suppress_warnings, use_db, upsample", [ (True, True, False, None), (False, True, False, None), @@ -121,12 +131,12 @@ def test_convolution_with_temperature( (False, False, True, 10), ], ids=[ - 'plan_valid=True, suppress_warnings=True, use_db=False, upsample=None', - 'plan_valid=False, suppress_warnings=True, use_db=False, upsample=None', - 'plan_valid=True, suppress_warnings=False, use_db=False, upsample=None', - 'plan_valid=True, suppress_warnings=False, use_db=True, upsample=None', - 'plan_valid=True, suppress_warnings=False, use_db=True, upsample=10', - 'plan_valid=False, suppress_warnings=False, use_db=True, upsample=10', + "plan_valid=True, suppress_warnings=True, use_db=False, upsample=None", + "plan_valid=False, suppress_warnings=True, use_db=False, upsample=None", + "plan_valid=True, suppress_warnings=False, use_db=False, upsample=None", + "plan_valid=True, suppress_warnings=False, use_db=True, upsample=None", + "plan_valid=True, suppress_warnings=False, use_db=True, upsample=10", + "plan_valid=False, suppress_warnings=False, use_db=True, upsample=10", ], ) def test_convolution_branches( @@ -166,20 +176,22 @@ def fake_create_energy_grid(): def fake_check_width_thresholds(*args, **kwargs): check_width_calls.append((args, kwargs)) - monkeypatch.setattr(conv, '_create_energy_grid', fake_create_energy_grid) - monkeypatch.setattr(conv, '_check_width_thresholds', fake_check_width_thresholds) + monkeypatch.setattr(conv, "_create_energy_grid", fake_create_energy_grid) + monkeypatch.setattr( + conv, "_check_width_thresholds", fake_check_width_thresholds + ) # --- Simplify numerics --- dense = conv._energy_grid.energy_dense monkeypatch.setattr( conv.sample_components, - 'evaluate', + "evaluate", lambda x: np.ones_like(dense), # noqa: ARG005 ) monkeypatch.setattr( conv.resolution_components, - 'evaluate', + "evaluate", lambda x: np.ones_like(dense), # noqa: ARG005 ) @@ -191,12 +203,12 @@ def fake_db(*args, **kwargs): # noqa: ARG001 return np.ones_like(dense) monkeypatch.setattr( - 'easydynamics.convolution.numerical_convolution.detailed_balance_factor', + "easydynamics.convolution.numerical_convolution.detailed_balance_factor", fake_db, ) monkeypatch.setattr( - 'easydynamics.convolution.numerical_convolution.fftconvolve', + "easydynamics.convolution.numerical_convolution.fftconvolve", lambda a, b, mode: np.ones_like(dense), # noqa: ARG005 ) @@ -207,7 +219,7 @@ def fake_interp(*args, **kwargs): # noqa: ARG001 interp_called = True return np.ones_like(conv.energy.values) - monkeypatch.setattr(np, 'interp', fake_interp) + monkeypatch.setattr(np, "interp", fake_interp) # THEN result = conv.convolution() diff --git a/tests/unit/easydynamics/convolution/test_numerical_convolution_base.py b/tests/unit/easydynamics/convolution/test_numerical_convolution_base.py index d349d591..ef20e96f 100644 --- a/tests/unit/easydynamics/convolution/test_numerical_convolution_base.py +++ b/tests/unit/easydynamics/convolution/test_numerical_convolution_base.py @@ -18,8 +18,8 @@ class TestNumericalConvolutionBase: @pytest.fixture def default_numerical_convolution_base(self): energy = np.linspace(-10, 10, 101) - sample_components = ComponentCollection(display_name='ComponentCollection') - resolution_components = ComponentCollection(display_name='ResolutionModel') + sample_components = ComponentCollection(display_name="ComponentCollection") + resolution_components = ComponentCollection(display_name="ResolutionModel") return NumericalConvolutionBase( energy=energy, @@ -48,7 +48,7 @@ def test_init(self, default_numerical_convolution_base): assert default_numerical_convolution_base.upsample_factor == 5 assert default_numerical_convolution_base.extension_factor == pytest.approx(0.2) assert default_numerical_convolution_base.temperature is None - assert default_numerical_convolution_base.unit == 'meV' + assert default_numerical_convolution_base.unit == "meV" assert ( default_numerical_convolution_base.detailed_balance_settings.normalize_detailed_balance is True @@ -62,13 +62,17 @@ def test_init_with_custom_parameters(self): """ # WHEN energy = np.linspace(-5, 5, 50) - sample_components = ComponentCollection(display_name='ComponentCollection') - resolution_components = ComponentCollection(display_name='ResolutionModel') - resolution_settings = ConvolutionSettings(upsample_factor=10, extension_factor=0.5) + sample_components = ComponentCollection(display_name="ComponentCollection") + resolution_components = ComponentCollection(display_name="ResolutionModel") + resolution_settings = ConvolutionSettings( + upsample_factor=10, extension_factor=0.5 + ) temperature = 300.0 - temperature_unit = 'K' - detailed_balance_settings = DetailedBalanceSettings(normalize_detailed_balance=False) - unit = 'meV' + temperature_unit = "K" + detailed_balance_settings = DetailedBalanceSettings( + normalize_detailed_balance=False + ) + unit = "meV" # THEN numerical_convolution_base = NumericalConvolutionBase( @@ -92,46 +96,51 @@ def test_init_with_custom_parameters(self): numerical_convolution_base.detailed_balance_settings.normalize_detailed_balance is False ) - assert numerical_convolution_base.detailed_balance_settings is detailed_balance_settings + assert ( + numerical_convolution_base.detailed_balance_settings + is detailed_balance_settings + ) assert isinstance(numerical_convolution_base._energy_grid, EnergyGrid) @pytest.mark.parametrize( - 'invalid_input, expected_exception, match', + "invalid_input, expected_exception, match", [ # temperature ( - {'temperature': 'invalid_temperature'}, + {"temperature": "invalid_temperature"}, TypeError, - r'Temperature must be None, a number or a Parameter.', + r"Temperature must be None, a number or a Parameter.", ), # temperature_unit ( - {'temperature_unit': 123}, + {"temperature_unit": 123}, TypeError, - r'Temperature_unit must be a string or sc.Unit.', + r"Temperature_unit must be a string or sc.Unit.", ), # detailed_balance_settings ( - {'detailed_balance_settings': 'invalid_settings'}, + {"detailed_balance_settings": "invalid_settings"}, TypeError, - r'detailed_balance_settings must be a DetailedBalanceSettings instance.', + r"detailed_balance_settings must be a DetailedBalanceSettings instance.", ), ], ids=[ - 'temperature_invalid_type', - 'temperature_unit_invalid_type', - 'detailed_balance_settings_invalid_type', + "temperature_invalid_type", + "temperature_unit_invalid_type", + "detailed_balance_settings_invalid_type", ], ) - def test_init_raises_for_invalid_input(self, invalid_input, expected_exception, match): + def test_init_raises_for_invalid_input( + self, invalid_input, expected_exception, match + ): """ Test that initialization raises appropriate exceptions for invalid input parameters. """ # WHEN energy = np.linspace(-5, 5, 50) - sample_components = ComponentCollection(display_name='ComponentCollection') - resolution_components = ComponentCollection(display_name='ResolutionModel') + sample_components = ComponentCollection(display_name="ComponentCollection") + resolution_components = ComponentCollection(display_name="ResolutionModel") # THEN EXPECT with pytest.raises(expected_exception, match=match): @@ -172,17 +181,17 @@ def test_energy_setter(self, default_numerical_convolution_base): default_numerical_convolution_base._create_energy_grid() # EXPECT - assert default_numerical_convolution_base._energy_grid.energy_dense.shape[0] == round( - 201 * default_numerical_convolution_base.upsample_factor - ) + assert default_numerical_convolution_base._energy_grid.energy_dense.shape[ + 0 + ] == round(201 * default_numerical_convolution_base.upsample_factor) @pytest.mark.parametrize( - 'new_upsample_factor, expected_size', + "new_upsample_factor, expected_size", [ (10, (101 * 10)), (None, 101), ], - ids=['upsample_10', 'no_upsampling'], + ids=["upsample_10", "no_upsampling"], ) def test_upsample_factor_setter( self, @@ -209,18 +218,19 @@ def test_upsample_factor_setter( # EXPECT: correct factor + grid size assert default_numerical_convolution_base.upsample_factor == new_upsample_factor assert ( - default_numerical_convolution_base._energy_grid.energy_dense.shape[0] == expected_size + default_numerical_convolution_base._energy_grid.energy_dense.shape[0] + == expected_size ) @pytest.mark.parametrize( - 'invalid_upsample_factor, expected_exception', + "invalid_upsample_factor, expected_exception", [ (-1, ValueError), # numeric < 1 → ValueError (0, ValueError), # numeric < 1 → ValueError (1.0, ValueError), # numeric = 1 → ValueError - ('invalid', TypeError), # non-numeric → TypeError + ("invalid", TypeError), # non-numeric → TypeError ], - ids=['negative', 'zero', 'one', 'string'], + ids=["negative", "zero", "one", "string"], ) def test_upsample_setter_raises( self, @@ -260,7 +270,9 @@ def test_extension_factor_setter(self, default_numerical_convolution_base): default_numerical_convolution_base._create_energy_grid() # EXPECT - assert default_numerical_convolution_base.extension_factor == new_extension_factor + assert ( + default_numerical_convolution_base.extension_factor == new_extension_factor + ) expected_span = 20 + (0.5 * 20) # original span + extension assert np.isclose( default_numerical_convolution_base._energy_grid.energy_span_dense, @@ -268,12 +280,12 @@ def test_extension_factor_setter(self, default_numerical_convolution_base): ) @pytest.mark.parametrize( - 'invalid_extension_factor, expected_exception', + "invalid_extension_factor, expected_exception", [ (-0.1, ValueError), # negative → ValueError - ('invalid', TypeError), # non-numeric → TypeError + ("invalid", TypeError), # non-numeric → TypeError ], - ids=['negative', 'string'], + ids=["negative", "string"], ) def test_extension_factor_setter_raises( self, @@ -290,16 +302,18 @@ def test_extension_factor_setter_raises( with pytest.raises( expected_exception, ): - default_numerical_convolution_base.extension_factor = invalid_extension_factor + default_numerical_convolution_base.extension_factor = ( + invalid_extension_factor + ) @pytest.mark.parametrize( - 'temperature_input, expected_value', + "temperature_input, expected_value", [ (1, 1.0), (100.0, 100.0), - (Parameter(name='TempParam', value=250.0, unit='K'), 250.0), + (Parameter(name="TempParam", value=250.0, unit="K"), 250.0), ], - ids=['int', 'float', 'parameter'], + ids=["int", "float", "parameter"], ) def test_temperature_setter( self, default_numerical_convolution_base, temperature_input, expected_value @@ -312,7 +326,7 @@ def test_temperature_setter( # THEN EXPECT assert default_numerical_convolution_base.temperature.value == expected_value - assert default_numerical_convolution_base.temperature.unit == 'K' + assert default_numerical_convolution_base.temperature.unit == "K" def test_temperature_setter_none(self, default_numerical_convolution_base): """ @@ -332,7 +346,7 @@ def test_temperature_setter_does_not_replace_parameter( exists does not create a new Parameter. """ # WHEN - temp_param = Parameter(name='TempParam', value=300.0, unit='K') + temp_param = Parameter(name="TempParam", value=300.0, unit="K") default_numerical_convolution_base.temperature = temp_param # THEN @@ -340,17 +354,21 @@ def test_temperature_setter_does_not_replace_parameter( # EXPECT assert default_numerical_convolution_base.temperature is temp_param - assert default_numerical_convolution_base.temperature.value == pytest.approx(350.0) + assert default_numerical_convolution_base.temperature.value == pytest.approx( + 350.0 + ) def test_temperature_setter_raises(self, default_numerical_convolution_base): """ Test that setting an invalid temperature raises TypeError. """ # WHEN THEN EXPECT - with pytest.raises(TypeError, match='Temperature must be'): - default_numerical_convolution_base.temperature = 'invalid_temperature' + with pytest.raises(TypeError, match="Temperature must be"): + default_numerical_convolution_base.temperature = "invalid_temperature" - def test_normalize_detailed_balance_setter(self, default_numerical_convolution_base): + def test_normalize_detailed_balance_setter( + self, default_numerical_convolution_base + ): """ Test setting normalize_detailed_balance to False. """ @@ -365,18 +383,22 @@ def test_normalize_detailed_balance_setter(self, default_numerical_convolution_b is False ) - def test_normalize_detailed_balance_setter_raises(self, default_numerical_convolution_base): + def test_normalize_detailed_balance_setter_raises( + self, default_numerical_convolution_base + ): """ Test that setting an invalid normalize_detailed_balance raises TypeError. """ # WHEN THEN EXPECT - with pytest.raises(TypeError, match='normalize_detailed_balance must be'): + with pytest.raises(TypeError, match="normalize_detailed_balance must be"): default_numerical_convolution_base.detailed_balance_settings.normalize_detailed_balance = ( # noqa: E501 - 'invalid' + "invalid" ) - def test_detailed_balance_settings_property(self, default_numerical_convolution_base): + def test_detailed_balance_settings_property( + self, default_numerical_convolution_base + ): # WHEN new_settings = DetailedBalanceSettings( use_detailed_balance=False, normalize_detailed_balance=False @@ -386,15 +408,21 @@ def test_detailed_balance_settings_property(self, default_numerical_convolution_ default_numerical_convolution_base.detailed_balance_settings = new_settings # EXPECT - assert default_numerical_convolution_base.detailed_balance_settings is new_settings + assert ( + default_numerical_convolution_base.detailed_balance_settings is new_settings + ) - def test_detailed_balance_settings_setter_invalid(self, default_numerical_convolution_base): + def test_detailed_balance_settings_setter_invalid( + self, default_numerical_convolution_base + ): # WHEN / THEN / EXPECT with pytest.raises( TypeError, - match='detailed_balance_settings must be a DetailedBalanceSettings', + match="detailed_balance_settings must be a DetailedBalanceSettings", ): - default_numerical_convolution_base.detailed_balance_settings = 'invalid_settings' + default_numerical_convolution_base.detailed_balance_settings = ( + "invalid_settings" + ) def test_convolution_settings_setter_valid( self, @@ -413,16 +441,16 @@ def test_convolution_settings_setter_valid( assert new_settings.convolution_plan_is_valid is False @pytest.mark.parametrize( - 'value, expected_exception, match', + "value, expected_exception, match", [ - (None, TypeError, 'must be a ConvolutionSettings instance'), - ('settings', TypeError, 'must be a ConvolutionSettings instance'), - (123, TypeError, 'must be a ConvolutionSettings instance'), + (None, TypeError, "must be a ConvolutionSettings instance"), + ("settings", TypeError, "must be a ConvolutionSettings instance"), + (123, TypeError, "must be a ConvolutionSettings instance"), ], ids=[ - 'none', - 'string', - 'int', + "none", + "string", + "int", ], ) def test_convolution_settings_setter_invalid( @@ -488,11 +516,11 @@ def test_create_energy_grid_upsample_none_non_uniform_raises( default_numerical_convolution_base.upsample_factor = None with pytest.raises( ValueError, - match='Input array `energy` must be uniformly spaced if upsample_factor is not given', + match="Input array `energy` must be uniformly spaced if upsample_factor is not given", ): default_numerical_convolution_base._create_energy_grid() - @pytest.mark.parametrize('num_points', [100, 101], ids=['even', 'odd']) + @pytest.mark.parametrize("num_points", [100, 101], ids=["even", "odd"]) def test_create_energy_grid_upsample_and_extension( self, default_numerical_convolution_base, num_points ): @@ -532,7 +560,9 @@ def test_create_energy_grid_upsample_and_extension( else: assert np.isclose(energy_grid.energy_even_length_offset, 0.0) - def test_create_energy_grid_non_centered_energy(self, default_numerical_convolution_base): + def test_create_energy_grid_non_centered_energy( + self, default_numerical_convolution_base + ): """ Test creating energy grid when input energy is not centered around zero. The centered energy grid should be shifted @@ -570,17 +600,17 @@ def test_check_width_large_threshold(self, default_numerical_convolution_base): """ # WHEN wide_gaussian = Gaussian( - display_name='ComponentCollection', area=1.0, center=0.0, width=15.0 + name="ComponentCollection", area=1.0, center=0.0, width=15.0 ) # THEN EXPECT with pytest.warns( UserWarning, - match='Increase extension_factor to improve', + match="Increase extension_factor to improve", ): default_numerical_convolution_base._check_width_thresholds( model=wide_gaussian, - model_name='ComponentCollection', + model_name="ComponentCollection", ) def test_check_width_small_threshold(self, default_numerical_convolution_base): @@ -590,17 +620,17 @@ def test_check_width_small_threshold(self, default_numerical_convolution_base): """ # WHEN narrow_gaussian = Gaussian( - display_name='ComponentCollection', area=1.0, center=0.0, width=0.000001 + name="ComponentCollection", area=1.0, center=0.0, width=0.000001 ) # THEN EXPECT with pytest.warns( UserWarning, - match='Increase upsample_factor to improve', + match="Increase upsample_factor to improve", ): default_numerical_convolution_base._check_width_thresholds( model=narrow_gaussian, - model_name='ComponentCollection', + model_name="ComponentCollection", ) def test_check_width_no_warnings(self, default_numerical_convolution_base): @@ -611,15 +641,15 @@ def test_check_width_no_warnings(self, default_numerical_convolution_base): """ # WHEN good_gaussian = Gaussian( - display_name='ComponentCollection', area=1.0, center=0.0, width=1.0 + name="ComponentCollection", area=1.0, center=0.0, width=1.0 ) - sample_components = ComponentCollection(display_name='ComponentCollection') + sample_components = ComponentCollection(display_name="ComponentCollection") sample_components.append_component(good_gaussian) # THEN EXPECT default_numerical_convolution_base._check_width_thresholds( model=sample_components, - model_name='ComponentCollection', + model_name="ComponentCollection", ) def test_repr(self, default_numerical_convolution_base): @@ -630,19 +660,19 @@ def test_repr(self, default_numerical_convolution_base): repr_str = repr(default_numerical_convolution_base) # THEN EXPECT - assert 'NumericalConvolutionBase(' in repr_str - assert 'energy=array of shape' in repr_str - assert '(101,' in repr_str # correct shape + assert "NumericalConvolutionBase(" in repr_str + assert "energy=array of shape" in repr_str + assert "(101," in repr_str # correct shape # Sample and resolution models - assert 'ComponentCollection' in repr_str - assert 'Components: No components' in repr_str - assert 'sample_components=' in repr_str - assert 'resolution_components=' in repr_str + assert "ComponentCollection" in repr_str + assert "Components: No components" in repr_str + assert "sample_components=" in repr_str + assert "resolution_components=" in repr_str # Important parameters - assert 'unit=meV' in repr_str - assert 'upsample_factor=5' in repr_str - assert 'extension_factor=0.2' in repr_str - assert 'temperature=None' in repr_str - assert 'normalize_detailed_balance=True' in repr_str + assert "unit=meV" in repr_str + assert "upsample_factor=5" in repr_str + assert "extension_factor=0.2" in repr_str + assert "temperature=None" in repr_str + assert "normalize_detailed_balance=True" in repr_str diff --git a/tests/unit/easydynamics/sample_model/test_background_model.py b/tests/unit/easydynamics/sample_model/test_background_model.py index cacf1198..ceebda88 100644 --- a/tests/unit/easydynamics/sample_model/test_background_model.py +++ b/tests/unit/easydynamics/sample_model/test_background_model.py @@ -14,26 +14,26 @@ class TestBackgroundModel: @pytest.fixture def background_model(self): component1 = Gaussian( - display_name='TestGaussian1', + 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) return BackgroundModel( - display_name='InitModel', + display_name="InitModel", components=component_collection, - unit='meV', + unit="meV", Q=np.array([1.0, 2.0, 3.0]), ) @@ -42,30 +42,32 @@ def test_init(self, background_model): model = background_model # 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])) @pytest.mark.parametrize( - 'invalid_component, expected_error_msg', + "invalid_component, expected_error_msg", [ - ('invalid_component', 'must be '), - (123, 'must be '), - (45.6, 'must be '), + ("invalid_component", "must be "), + (123, "must be "), + (45.6, "must be "), ( - [Gaussian(), 'invalid_in_list'], - 'must be ', + [Gaussian(), "invalid_in_list"], + "must be ", ), ], ids=[ - 'string', - 'int', - 'float', - 'list_with_invalid', + "string", + "int", + "float", + "list_with_invalid", ], ) - def test_init_raises_with_invalid_components(self, invalid_component, expected_error_msg): + def test_init_raises_with_invalid_components( + self, invalid_component, expected_error_msg + ): # WHEN / THEN / EXPECT with pytest.raises( TypeError, diff --git a/tests/unit/easydynamics/sample_model/test_component_collection.py b/tests/unit/easydynamics/sample_model/test_component_collection.py index a50dca06..e96b0752 100644 --- a/tests/unit/easydynamics/sample_model/test_component_collection.py +++ b/tests/unit/easydynamics/sample_model/test_component_collection.py @@ -17,24 +17,22 @@ class TestComponentCollection: @pytest.fixture def component_collection(self): - model = ComponentCollection(display_name='TestComponentCollection') + model = ComponentCollection(display_name="TestComponentCollection") component1 = Gaussian( - name='TestGaussian1Name', - display_name='TestGaussian1', + name="TestGaussian1Name", + display_name="TestGaussian1", area=1.0, center=0.0, width=1.0, - unit='meV', - unique_name='TestGaussian1', + unit="meV", ) component2 = Lorentzian( - name='TestLorentzian1Name', - display_name='TestLorentzian1', + name="TestLorentzian1Name", + display_name="TestLorentzian1", area=2.0, center=1.0, width=0.5, - unit='meV', - unique_name='TestLorentzian1', + unit="meV", ) model.append_component(component1) model.append_component(component2) @@ -42,43 +40,49 @@ def component_collection(self): def test_init(self): # WHEN THEN - component_collection = ComponentCollection(display_name='InitModel') + component_collection = ComponentCollection(display_name="InitModel") # EXPECT - assert component_collection.display_name == 'InitModel' - assert component_collection.components == [] + assert component_collection.display_name == "InitModel" + assert not component_collection def test_init_with_components(self): # WHEN THEN component1 = Gaussian( - display_name='TestGaussian1', area=1.0, center=0.0, width=1.0, unit='meV' + name="TestGaussian1", area=1.0, center=0.0, width=1.0, unit="meV" ) component2 = Lorentzian( - display_name='TestLorentzian1', area=2.0, center=1.0, width=0.5, unit='meV' + display_name="TestLorentzian1", area=2.0, center=1.0, width=0.5, unit="meV" ) component_collection = ComponentCollection( - display_name='InitModel', components=[component1, component2] + display_name="InitModel", components=[component1, component2] ) # EXPECT - assert component_collection.display_name == 'InitModel' - assert len(component_collection.components) == 2 - assert component_collection.components[0] is component1 - assert component_collection.components[1] is component2 + assert component_collection.display_name == "InitModel" + assert len(component_collection) == 2 + assert component_collection[0] is component1 + assert component_collection[1] is component2 def test_init_with_invalid_components_raises(self): # WHEN THEN EXPECT - with pytest.raises(TypeError, match='Component must be'): - ComponentCollection(components=['NotAComponent']) + with pytest.raises( + TypeError, + match="All items in components must be instances of ModelComponent", + ): + ComponentCollection(components=["NotAComponent"]) def test_init_with_invalid_list_of_components_raises(self): # WHEN THEN EXPECT - with pytest.raises(TypeError, match='components must be a list of'): - ComponentCollection(components='NotAList') + with pytest.raises( + TypeError, + match="components must be a ModelComponent or a list of ModelComponent", + ): + ComponentCollection(components="NotAList") def test_init_with_invalid_unit_raises(self): # WHEN THEN EXPECT - with pytest.raises(TypeError, match='unit must be'): + with pytest.raises(TypeError, match="unit must be"): ComponentCollection(unit=123) # ───── Component Management ───── @@ -86,89 +90,56 @@ def test_init_with_invalid_unit_raises(self): def test_append_component(self, component_collection): # WHEN component = Gaussian( - display_name='TestComponent', area=1.0, center=0.0, width=1.0, unit='meV' + name="TestComponent", area=1.0, center=0.0, width=1.0, unit="meV" ) # THEN component_collection.append_component(component) # EXPECT - assert component_collection.components[-1] is component + assert component_collection[-1] is component def test_append_component_collection(self, component_collection): # WHEN component = Gaussian( - display_name='TestComponent', area=1.0, center=0.0, width=1.0, unit='meV' + name="TestComponent", area=1.0, center=0.0, width=1.0, unit="meV" ) component_collection2 = ComponentCollection() component_collection2.append_component(component) # THEN component_collection.append_component(component_collection2) # EXPECT - assert component_collection.components[-1] is component + assert component_collection[-1] is component def test_append_existing_component_raises(self, component_collection): # WHEN THEN - component = component_collection.components[0] + component = component_collection[0] # EXPECT - with pytest.raises(ValueError, match='is already in the collection'): + with pytest.raises(ValueError, match="already exists in list"): component_collection.append_component(component) def test_append_invalid_component_raises(self, component_collection): # WHEN THEN EXPECT - with pytest.raises(TypeError, match='Component must be '): - component_collection.append_component('NotAComponent') - - def test_remove_component(self, component_collection): - # WHEN THEN - component_collection.remove_component('TestGaussian1') - # EXPECT - assert 'TestGaussian1' not in component_collection.components - - def test_remove_component_raises(self, component_collection): - # WHEN THEN EXPECT - with pytest.raises(TypeError, match='Component name must be a string'): - component_collection.remove_component(123) - - def test_remove_nonexistent_component_raises(self, component_collection): - # WHEN THEN EXPECT - with pytest.raises(KeyError, match="No component named 'NonExistentComponent' exists"): - component_collection.remove_component('NonExistentComponent') + with pytest.raises(TypeError, match="Value must be an instance of type"): + component_collection.append_component("NotAComponent") def test_getitem(self, component_collection): # WHEN component = Gaussian( - display_name='TestComponent', area=1.0, center=0.0, width=1.0, unit='meV' + name="TestComponent", area=1.0, center=0.0, width=1.0, unit="meV" ) # THEN component_collection.append_component(component) # EXPECT - assert component_collection.components[-1] is component - - def test_component_setter(self, component_collection): - # WHEN - new_components = [Lorentzian()] - # THEN - component_collection.components = new_components - # EXPECT - assert len(component_collection.components) == 1 - assert component_collection.components[0] is new_components[0] - - def test_component_setter_invalid_raises(self, component_collection): - # WHEN THEN EXPECT - with pytest.raises(TypeError, match=r' must be instances of ModelComponent.'): - component_collection.components = ['NotAComponent'] - - with pytest.raises(TypeError, match='components must be a list of'): - component_collection.components = 'NotAList' + assert component_collection[-1] is component def test_is_empty(self): # WHEN THEN - component_collection = ComponentCollection(display_name='EmptyModel') + component_collection = ComponentCollection(display_name="EmptyModel") # EXPECT assert component_collection.is_empty is True # WHEN THEN component = Gaussian( - display_name='TestComponent', area=1.0, center=0.0, width=1.0, unit='meV' + name="TestComponent", area=1.0, center=0.0, width=1.0, unit="meV" ) component_collection.append_component(component) # EXPECT @@ -176,37 +147,27 @@ def test_is_empty(self): def test_is_empty_setter(self, component_collection): # WHEN THEN EXPECT - with pytest.raises(AttributeError, match=r'is_empty is a read-only property.'): + with pytest.raises(AttributeError, match=r"is_empty is a read-only property."): component_collection.is_empty = True - def test_component_setter_empty_list(self, component_collection): - component_collection.components = [] - assert component_collection.components == [] - def test_list_component_names(self, component_collection): # WHEN THEN components = component_collection.list_component_names() # EXPECT assert len(components) == 2 - assert components[0] == 'TestGaussian1' - assert components[1] == 'TestLorentzian1' - - def test_clear_components(self, component_collection): - # WHEN THEN - component_collection.clear_components() - # EXPECT - assert len(component_collection.components) == 0 + assert components[0] == "TestGaussian1Name" + assert components[1] == "TestLorentzian1Name" def test_convert_unit(self, component_collection): # WHEN THEN - component_collection.convert_unit('eV') + component_collection.convert_unit("eV") # EXPECT - for component in component_collection.components: - assert component.unit == 'eV' + for component in component_collection: + assert component.unit == "eV" def test_convert_unit_incorrect_unit_raises(self, component_collection): # WHEN THEN EXPECT - with pytest.raises(TypeError, match=r'Unit must be a string or sc.Unit'): + with pytest.raises(TypeError, match=r"Unit must be a string or sc.Unit"): component_collection.convert_unit(123) def test_convert_unit_failure_rolls_back(self, component_collection): @@ -214,46 +175,46 @@ def test_convert_unit_failure_rolls_back(self, component_collection): # Introduce a faulty component that will fail conversion class FaultyComponent(Gaussian): def convert_unit(self, _unit: str) -> None: - raise RuntimeError('Conversion failed.') + raise RuntimeError("Conversion failed.") faulty_component = FaultyComponent( - display_name='FaultyComponent', area=1.0, center=0.0, width=1.0, unit='meV' + name="FaultyComponent", area=1.0, center=0.0, width=1.0, unit="meV" ) component_collection.append_component(faulty_component) original_units = { - component.display_name: component.unit for component in component_collection.components + component.name: component.unit for component in component_collection } # EXPECT - with pytest.raises(RuntimeError, match=r'Conversion failed.'): - component_collection.convert_unit('eV') + with pytest.raises(RuntimeError, match=r"Conversion failed."): + component_collection.convert_unit("eV") # Check that all components have their original units - for component in component_collection.components: - assert component.unit == original_units[component.display_name] + for component in component_collection: + assert component.unit == original_units[component.name] def test_set_unit(self, component_collection): # WHEN THEN EXPECT with pytest.raises( AttributeError, - match=r'Unit is read-only. Use convert_unit to change the unit', + match=r"Unit is read-only. Use convert_unit to change the unit", ): - component_collection.unit = 'eV' + component_collection.unit = "eV" def test_evaluate(self, component_collection): # WHEN x = np.linspace(-5, 5, 100) result = component_collection.evaluate(x) # EXPECT - expected_result = component_collection.components[0].evaluate( - x - ) + component_collection.components[1].evaluate(x) + expected_result = component_collection[0].evaluate(x) + component_collection[ + 1 + ].evaluate(x) np.testing.assert_allclose(result, expected_result, rtol=1e-5) def test_evaluate_no_components_returns_zero(self): # WHEN THEN - component_collection = ComponentCollection(display_name='EmptyModel') + component_collection = ComponentCollection(display_name="EmptyModel") x = np.linspace(-5, 5, 100) # EXPECT result = component_collection.evaluate(x) @@ -263,12 +224,12 @@ def test_evaluate_no_components_returns_zero(self): def test_evaluate_component(self, component_collection): # WHEN THEN x = np.linspace(-5, 5, 100) - result1 = component_collection.evaluate_component(x, 'TestGaussian1') - result2 = component_collection.evaluate_component(x, 'TestLorentzian1') + result1 = component_collection.evaluate_component(x, "TestGaussian1Name") + result2 = component_collection.evaluate_component(x, "TestLorentzian1Name") # EXPECT - expected_result1 = component_collection.components[0].evaluate(x) - expected_result2 = component_collection.components[1].evaluate(x) + expected_result1 = component_collection[0].evaluate(x) + expected_result2 = component_collection[1].evaluate(x) np.testing.assert_allclose(result1, expected_result1, rtol=1e-5) np.testing.assert_allclose(result2, expected_result2, rtol=1e-5) @@ -277,16 +238,20 @@ def test_evaluate_nonexistent_component_raises(self, component_collection): x = np.linspace(-5, 5, 100) # THEN EXPECT - with pytest.raises(KeyError, match="No component named 'NonExistentComponent' exists"): - component_collection.evaluate_component(x, 'NonExistentComponent') + with pytest.raises( + KeyError, match="No component named 'NonExistentComponent' exists" + ): + component_collection.evaluate_component(x, "NonExistentComponent") def test_evaluate_component_no_components_raises(self): # WHEN THEN - component_collection = ComponentCollection(display_name='EmptyModel') + component_collection = ComponentCollection(display_name="EmptyModel") x = np.linspace(-5, 5, 100) # EXPECT - with pytest.raises(ValueError, match=r'No components in the model to evaluate.'): - component_collection.evaluate_component(x, 'AnyComponent') + with pytest.raises( + ValueError, match=r"No components in the model to evaluate." + ): + component_collection.evaluate_component(x, "AnyComponent") def test_evaluate_component_invalid_name_type_raises(self, component_collection): # WHEN @@ -295,7 +260,7 @@ def test_evaluate_component_invalid_name_type_raises(self, component_collection) # THEN EXPECT with pytest.raises( TypeError, - match=r"Component unique name must be a string, got instead.", + match=r"Component name must be a string, got instead.", ): component_collection.evaluate_component(x, 123) @@ -312,28 +277,34 @@ def test_normalize_area(self, component_collection): def test_normalize_area_no_components_raises(self): # WHEN THEN - component_collection = ComponentCollection(display_name='EmptyModel') + component_collection = ComponentCollection(display_name="EmptyModel") # EXPECT - with pytest.raises(ValueError, match=r'No components in the model to normalize.'): + with pytest.raises( + ValueError, match=r"No components in the model to normalize." + ): component_collection.normalize_area() @pytest.mark.parametrize( - 'area_value', + "area_value", [np.nan, 0.0, np.inf], - ids=['NaN area', 'Zero area', 'Infinite area'], + ids=["NaN area", "Zero area", "Infinite area"], ) - def test_normalize_area_not_finite_area_raises(self, component_collection, area_value): + def test_normalize_area_not_finite_area_raises( + self, component_collection, area_value + ): # WHEN THEN - component_collection.components[0].area = area_value - component_collection.components[1].area = area_value + component_collection[0].area = area_value + component_collection[1].area = area_value # EXPECT - with pytest.raises(ValueError, match=r'cannot normalize'): + with pytest.raises(ValueError, match=r"cannot normalize"): component_collection.normalize_area() def test_normalize_area_non_area_component_warns(self, component_collection): # WHEN - component1 = Polynomial(display_name='TestPolynomial', coefficients=[1, 2, 3], unit='meV') + component1 = Polynomial( + display_name="TestPolynomial", coefficients=[1, 2, 3], unit="meV" + ) component_collection.append_component(component1) # THEN EXPECT @@ -347,19 +318,19 @@ def test_get_all_parameters(self, component_collection): assert len(parameters) == 6 expected_names = { - 'TestGaussian1Name area', - 'TestGaussian1Name center', - 'TestGaussian1Name width', - 'TestLorentzian1Name area', - 'TestLorentzian1Name center', - 'TestLorentzian1Name width', + "TestGaussian1Name area", + "TestGaussian1Name center", + "TestGaussian1Name width", + "TestLorentzian1Name area", + "TestLorentzian1Name center", + "TestLorentzian1Name width", } actual_names = {param.name for param in parameters} assert actual_names == expected_names assert all(isinstance(param, Parameter) for param in parameters) def test_get_parameters_no_components(self): - component_collection = ComponentCollection(display_name='EmptyModel') + component_collection = ComponentCollection(display_name="EmptyModel") # WHEN THEN parameters = component_collection.get_all_parameters() # EXPECT @@ -369,10 +340,10 @@ def test_get_fit_parameters(self, component_collection): # WHEN # Fix one parameter and make another dependent - component_collection.components[0].area.fixed = True - component_collection.components[1].width.make_dependent_on( - 'comp1_width', - {'comp1_width': component_collection.components[0].width}, + component_collection[0].area.fixed = True + component_collection[1].width.make_dependent_on( + "comp1_width", + {"comp1_width": component_collection[0].width}, ) # THEN @@ -382,10 +353,10 @@ def test_get_fit_parameters(self, component_collection): assert len(fit_parameters) == 4 expected_names = { - 'TestGaussian1Name center', - 'TestGaussian1Name width', - 'TestLorentzian1Name area', - 'TestLorentzian1Name center', + "TestGaussian1Name center", + "TestGaussian1Name width", + "TestLorentzian1Name area", + "TestLorentzian1Name center", } actual_names = {param.name for param in fit_parameters} assert actual_names == expected_names @@ -407,18 +378,18 @@ def test_fix_and_free_all_parameters(self, component_collection): assert param.fixed is False def test_contains(self, component_collection): - assert 'TestGaussian1' in component_collection - assert 'TestLorentzian1' in component_collection - assert 'NonExistentComponent' not in component_collection + assert "TestGaussian1Name" in component_collection + assert "TestLorentzian1Name" in component_collection + assert "NonExistentComponent" not in component_collection - gaussian_component = component_collection.components[0] - lorentzian_component = component_collection.components[1] + gaussian_component = component_collection[0] + lorentzian_component = component_collection[1] assert gaussian_component in component_collection assert lorentzian_component in component_collection # WHEN THEN fake_component = Gaussian( - display_name='FakeGaussian', area=1.0, center=0.0, width=1.0, unit='meV' + name="FakeGaussian", area=1.0, center=0.0, width=1.0, unit="meV" ) # EXPECT assert fake_component not in component_collection @@ -428,24 +399,24 @@ def test_repr_contains_name_and_components(self, component_collection): # WHEN THEN rep = repr(component_collection) # EXPECT - assert 'ComponentCollection' in rep - assert 'TestGaussian' in rep + assert "ComponentCollection" in rep + assert "TestGaussian1Name" in rep def test_to_dict(self, component_collection): - # WHEN THEN + # WHEN model_dict = component_collection.to_dict() # EXPECT - assert model_dict['display_name'] == component_collection.display_name - assert model_dict['unit'] == component_collection.unit - assert len(model_dict['components']) == len(component_collection.components) + assert model_dict["display_name"] == component_collection.display_name + assert model_dict["unit"] == component_collection.unit + assert len(model_dict["components"]) == len(component_collection) for comp, comp_dict in zip( - component_collection.components, model_dict['components'], strict=True + component_collection, model_dict["components"], strict=True ): - assert comp_dict['@class'] == type(comp).__name__ - assert comp_dict['display_name'] == comp.display_name - assert comp_dict['unit'] == comp.unit + assert comp_dict["@class"] == type(comp).__name__ + assert comp_dict["display_name"] == comp.display_name + assert comp_dict["unit"] == comp.unit def test_from_dict(self, component_collection): # WHEN @@ -456,19 +427,18 @@ def test_from_dict(self, component_collection): # EXPECT assert new_model.display_name == component_collection.display_name - assert len(new_model.components) == len(component_collection.components) + assert len(new_model) == len(component_collection) - # Compare each component and its parameters - for orig_comp, new_comp in zip( - component_collection.components, new_model.components, strict=True - ): + for orig_comp, new_comp in zip(component_collection, new_model, strict=True): assert type(new_comp) is type(orig_comp) assert new_comp.display_name == orig_comp.display_name assert new_comp.unit == orig_comp.unit orig_params = orig_comp.get_all_parameters() new_params = new_comp.get_all_parameters() + assert len(orig_params) == len(new_params) + for param_orig, param_new in zip(orig_params, new_params, strict=True): assert param_new.name == param_orig.name assert param_new.value == param_orig.value @@ -476,13 +446,12 @@ def test_from_dict(self, component_collection): def test_copy(self, component_collection): # WHEN - component_collection.temperature = 300 - component_collection.components[0].area.min = 0.5 - component_collection.components[0].area.fixed = True - component_collection.components[0].area.max = 5.0 - component_collection.components[1].width.min = 0.1 - component_collection.components[1].width.fixed = True - component_collection.components[1].width.max = 2.0 + component_collection[0].area.min = 0.5 + component_collection[0].area.fixed = True + component_collection[0].area.max = 5.0 + component_collection[1].width.min = 0.1 + component_collection[1].width.fixed = True + component_collection[1].width.max = 2.0 # THEN model_copy = copy(component_collection) @@ -490,11 +459,11 @@ def test_copy(self, component_collection): # EXPECT collection-level checks assert model_copy is not component_collection assert model_copy.display_name == component_collection.display_name - assert len(model_copy.components) == len(component_collection.components) + assert len(model_copy) == len(component_collection) # EXPECT: deep copy, same order for orig_comp, copied_comp in zip( - component_collection.components, model_copy.components, strict=True + component_collection, model_copy, strict=True ): # New object assert copied_comp is not orig_comp diff --git a/tests/unit/easydynamics/sample_model/test_model_base.py b/tests/unit/easydynamics/sample_model/test_model_base.py index 34fea244..810e5d7e 100644 --- a/tests/unit/easydynamics/sample_model/test_model_base.py +++ b/tests/unit/easydynamics/sample_model/test_model_base.py @@ -18,28 +18,28 @@ class TestModelBase: @pytest.fixture def model_base(self): component1 = Gaussian( - name='TestGaussian1Name', - display_name='TestGaussian1', + name="TestGaussian1Name", + name="TestGaussian1", area=1.0, center=0.0, width=1.0, - unit='meV', + unit="meV", ) component2 = Lorentzian( - name='TestLorentzian1Name', - display_name='TestLorentzian1', + name="TestLorentzian1Name", + 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) return ModelBase( - display_name='InitModel', + display_name="InitModel", components=component_collection, - unit='meV', + unit="meV", Q=np.array([1.0, 2.0, 3.0]), ) @@ -48,8 +48,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])) @@ -57,9 +57,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 @@ -90,7 +90,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): @@ -103,9 +103,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' + assert collection.components[1].display_name == "TestLorentzian1" def test_fix_free_all_parameters(self, model_base): # WHEN @@ -128,12 +128,12 @@ def test_get_all_variables(self, model_base): # THEN expected_var_display_names = { - 'TestGaussian1Name area', - 'TestGaussian1Name center', - 'TestGaussian1Name width', - 'TestLorentzian1Name area', - 'TestLorentzian1Name center', - 'TestLorentzian1Name width', + "TestGaussian1Name area", + "TestGaussian1Name center", + "TestGaussian1Name width", + "TestLorentzian1Name area", + "TestLorentzian1Name center", + "TestLorentzian1Name width", } retrieved_var_display_names = {var.display_name for var in all_vars} @@ -147,12 +147,12 @@ def test_get_all_variables_with_Q_index(self, model_base): # THEN expected_var_display_names = { - 'TestGaussian1Name area', - 'TestGaussian1Name center', - 'TestGaussian1Name width', - 'TestLorentzian1Name area', - 'TestLorentzian1Name center', - 'TestLorentzian1Name width', + "TestGaussian1Name area", + "TestGaussian1Name center", + "TestGaussian1Name width", + "TestLorentzian1Name area", + "TestLorentzian1Name center", + "TestLorentzian1Name width", } retrieved_var_display_names = {var.display_name for var in all_vars} @@ -164,7 +164,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) @@ -172,9 +172,9 @@ 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_get_component_collection(self, model_base): # WHEN THEN @@ -186,21 +186,21 @@ def test_get_component_collection_invalid_index_type_raises(self, model_base): # WHEN THEN EXPECT with pytest.raises( TypeError, - match='Q_index must be an int, got str', + match="Q_index must be an int, got str", ): - model_base.get_component_collection(Q_index='invalid_index') + model_base.get_component_collection(Q_index="invalid_index") def test_get_component_collection_invalid_index_raises(self, model_base): # WHEN THEN EXPECT with pytest.raises( IndexError, - match='Q_index 5 is out of bounds for ', + match="Q_index 5 is out of bounds for ", ): model_base.get_component_collection(Q_index=5) 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) @@ -210,7 +210,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 @@ -239,43 +239,43 @@ 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 '): - model_base.append_component('invalid_component') + with pytest.raises(TypeError, match=" must be "): + 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(UnitError): - model_base.convert_unit('invalid_unit') + model_base.convert_unit("invalid_unit") def test_convert_unit_incorrect_unit_raises(self, model_base): # WHEN THEN EXPECT - with pytest.raises(TypeError, match=r'Unit must be a string or sc.Unit'): + with pytest.raises(TypeError, match=r"Unit must be a string or sc.Unit"): model_base.convert_unit(123) 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 @@ -301,23 +301,23 @@ 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_raises_if_Q_is_not_similar(self, model_base): # WHEN / THEN / EXPECT - with pytest.raises(ValueError, match='New Q values are not similar to'): + with pytest.raises(ValueError, match="New Q values are not similar to"): model_base.Q = [10.0, 20.0, 30.0] @pytest.mark.parametrize( - 'new_Q', + "new_Q", [ [1.0, 2.0, 3.0], np.array([1.0, 2.0, 3.0]), - sc.Variable(dims=['Q'], values=[1.0, 2.0, 3.0], unit='1/angstrom'), + sc.Variable(dims=["Q"], values=[1.0, 2.0, 3.0], unit="1/angstrom"), ], - ids=['list', 'numpy_array', 'scipp_variable'], + ids=["list", "numpy_array", "scipp_variable"], ) def test_Q_setter_with_similar_Q(self, model_base, new_Q): # WHEN @@ -357,7 +357,7 @@ def test_clear_Q(self, model_base): def test_clear_Q_raises_without_confirm(self, model_base): # WHEN / THEN / EXPECT - with pytest.raises(ValueError, match='Clearing Q values requires confirmation'): + with pytest.raises(ValueError, match="Clearing Q values requires confirmation"): model_base.clear_Q() def test_normalize_area(self, model_base): @@ -368,7 +368,9 @@ def test_normalize_area(self, model_base): # EXPECT for collection in model_base._component_collections: - total_area = sum(component.area.value for component in collection.components) + total_area = sum( + component.area.value for component in collection.components + ) assert total_area == pytest.approx(1.0) def test_repr(self, model_base): @@ -376,7 +378,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/sample_model/test_resolution_model.py b/tests/unit/easydynamics/sample_model/test_resolution_model.py index 92f8a391..418c18a2 100644 --- a/tests/unit/easydynamics/sample_model/test_resolution_model.py +++ b/tests/unit/easydynamics/sample_model/test_resolution_model.py @@ -16,26 +16,26 @@ class TestResolutionModel: @pytest.fixture def resolution_model(self): component1 = Gaussian( - display_name='TestGaussian1', + 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) return ResolutionModel( - display_name='InitModel', + display_name="InitModel", components=component_collection, - unit='meV', + unit="meV", Q=np.array([1.0, 2.0, 3.0]), ) @@ -44,34 +44,36 @@ def test_init(self, resolution_model): model = resolution_model # 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])) @pytest.mark.parametrize( - 'invalid_component, expected_error_msg', + "invalid_component, expected_error_msg", [ - ('invalid_component', 'must be '), - (123, 'must be '), - (45.6, 'must be '), - (DeltaFunction(), 'cannot be a DeltaFunction'), - (Polynomial(), 'cannot be a Polynomial'), + ("invalid_component", "must be "), + (123, "must be "), + (45.6, "must be "), + (DeltaFunction(), "cannot be a DeltaFunction"), + (Polynomial(), "cannot be a Polynomial"), ( - [Gaussian(), 'invalid_in_list'], - 'must be ', + [Gaussian(), "invalid_in_list"], + "must be ", ), ], ids=[ - 'string', - 'int', - 'float', - 'DeltaFunction', - 'Polynomial', - 'list_with_invalid', + "string", + "int", + "float", + "DeltaFunction", + "Polynomial", + "list_with_invalid", ], ) - def test_init_raises_with_invalid_components(self, invalid_component, expected_error_msg): + def test_init_raises_with_invalid_components( + self, invalid_component, expected_error_msg + ): # WHEN / THEN / EXPECT with pytest.raises( TypeError, @@ -89,7 +91,7 @@ def test_init_raises_with_invalid_components(self, invalid_component, expected_e def test_append_and_remove_and_clear_component(self, resolution_model): # WHEN - new_component = Gaussian(unique_name='NewGaussian') + new_component = Gaussian(unique_name="NewGaussian") # THEN resolution_model.append_component(new_component) @@ -99,7 +101,7 @@ def test_append_and_remove_and_clear_component(self, resolution_model): assert resolution_model.components[-1] is new_component # THEN - resolution_model.remove_component('NewGaussian') + resolution_model.remove_component("NewGaussian") # EXPECT assert len(resolution_model.components) == 2 @@ -127,26 +129,28 @@ def test_append_component_collection(self, resolution_model): assert resolution_model.components[-1] is new_component2 @pytest.mark.parametrize( - 'invalid_component', + "invalid_component", [ DeltaFunction(), Polynomial(), ], - ids=['DeltaFunction', 'Polynomial'], + ids=["DeltaFunction", "Polynomial"], ) - def test_append_invalid_component_type_raises(self, resolution_model, invalid_component): + def test_append_invalid_component_type_raises( + self, resolution_model, invalid_component + ): # WHEN / THEN / EXPECT # appending a single component with pytest.raises( TypeError, - match='cannot be ', + match="cannot be ", ): resolution_model.append_component(invalid_component) # appending a collection with invalid component with pytest.raises( TypeError, - match='cannot be ', + match="cannot be ", ): collection = ComponentCollection() collection.append_component(invalid_component) diff --git a/tests/unit/easydynamics/sample_model/test_sample_model.py b/tests/unit/easydynamics/sample_model/test_sample_model.py index 712dfa8b..21677175 100644 --- a/tests/unit/easydynamics/sample_model/test_sample_model.py +++ b/tests/unit/easydynamics/sample_model/test_sample_model.py @@ -22,34 +22,34 @@ class TestSampleModel: @pytest.fixture def sample_model(self): component1 = Gaussian( - display_name='TestGaussian1', - unique_name='TestGaussian1', + name="TestGaussian1", + unique_name="TestGaussian1", area=1.0, center=0.0, width=1.0, - unit='meV', + unit="meV", ) component2 = Lorentzian( - display_name='TestLorentzian1', - unique_name='TestLorentzian1', + display_name="TestLorentzian1", + unique_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) diffusion_model = BrownianTranslationalDiffusion( - display_name='DiffusionModel', unique_name='DiffusionModel' + display_name="DiffusionModel", unique_name="DiffusionModel" ) return SampleModel( - display_name='InitModel', + display_name="InitModel", components=component_collection, diffusion_models=diffusion_model, - unit='meV', + unit="meV", Q=np.array([1.0, 2.0, 3.0]), temperature=10.0, ) @@ -60,8 +60,8 @@ def test_init(self, sample_model): model = sample_model # EXPECT - assert model.display_name == 'InitModel' - assert model.unit == 'meV' + assert model.display_name == "InitModel" + assert model.unit == "meV" assert len(model.components) == 2 assert isinstance(model.diffusion_models, list) assert len(model.diffusion_models) == 1 @@ -95,40 +95,42 @@ def test_init_custom_input(self): assert sample_model.detailed_balance_settings is detailed_balance_settings @pytest.mark.parametrize( - 'invalid_input, expected_exception, match', + "invalid_input, expected_exception, match", [ # diffusion_models ( - {'diffusion_models': 'invalid_diffusion_model'}, + {"diffusion_models": "invalid_diffusion_model"}, TypeError, - 'diffusion_models must be a DiffusionModelBase', + "diffusion_models must be a DiffusionModelBase", ), # temperature ( - {'temperature': 'invalid_temperature'}, + {"temperature": "invalid_temperature"}, TypeError, - 'temperature must be a number or None', + "temperature must be a number or None", ), ( - {'temperature': -5.0}, + {"temperature": -5.0}, ValueError, - 'temperature must be non-negative', + "temperature must be non-negative", ), # detailed_balance_settings ( - {'detailed_balance_settings': 'invalid_settings'}, + {"detailed_balance_settings": "invalid_settings"}, TypeError, - 'detailed_balance_settings must be a DetailedBalanceSettings or None', + "detailed_balance_settings must be a DetailedBalanceSettings or None", ), ], ids=[ - 'diffusion_models_invalid_type', - 'temperature_not_numeric', - 'temperature_negative', - 'detailed_balance_settings_invalid_type', + "diffusion_models_invalid_type", + "temperature_not_numeric", + "temperature_negative", + "detailed_balance_settings_invalid_type", ], ) - def test_init_raises_for_invalid_input(self, invalid_input, expected_exception, match): + def test_init_raises_for_invalid_input( + self, invalid_input, expected_exception, match + ): """ Test that initialization raises appropriate exceptions for invalid input parameters. @@ -141,7 +143,7 @@ def test_append_and_remove_and_clear_diffusion_model(self, sample_model): # WHEN model = sample_model new_diffusion_model = BrownianTranslationalDiffusion( - unique_name='new_diffusion_model', + unique_name="new_diffusion_model", ) # THEN @@ -152,7 +154,7 @@ def test_append_and_remove_and_clear_diffusion_model(self, sample_model): assert model.diffusion_models[1] is new_diffusion_model # THEN - model.remove_diffusion_model('new_diffusion_model') + model.remove_diffusion_model("new_diffusion_model") # EXPECT assert len(model.diffusion_models) == 1 @@ -166,17 +168,17 @@ def test_append_diffusion_model_raises_with_invalid_type(self, sample_model): # WHEN / THEN / EXPECT with pytest.raises( TypeError, - match='diffusion_model must be a DiffusionModelBase', + match="diffusion_model must be a DiffusionModelBase", ): - sample_model.append_diffusion_model('invalid_diffusion_model') + sample_model.append_diffusion_model("invalid_diffusion_model") def test_remove_diffusion_model_raises_with_invalid_name(self, sample_model): # WHEN / THEN / EXPECT with pytest.raises( ValueError, - match='No DiffusionModel', + match="No DiffusionModel", ): - sample_model.remove_diffusion_model('non_existent_model') + sample_model.remove_diffusion_model("non_existent_model") def test_diffusion_model_setter(self, sample_model): # WHEN @@ -206,19 +208,21 @@ def test_diffusion_model_setter(self, sample_model): assert model.diffusion_models[0] is new_diffusion_model1 @pytest.mark.parametrize( - 'invalid_value', + "invalid_value", [ - 'invalid_diffusion_model', + "invalid_diffusion_model", 123, - [BrownianTranslationalDiffusion(), 'invalid_diffusion_model'], + [BrownianTranslationalDiffusion(), "invalid_diffusion_model"], ], - ids=['string', 'integer', 'list with invalid type'], + ids=["string", "integer", "list with invalid type"], ) - def test_diffusion_model_setter_raises_with_invalid_type(self, invalid_value, sample_model): + def test_diffusion_model_setter_raises_with_invalid_type( + self, invalid_value, sample_model + ): # WHEN / THEN / EXPECT with pytest.raises( TypeError, - match='diffusion_models must be ', + match="diffusion_models must be ", ): sample_model.diffusion_models = invalid_value @@ -245,20 +249,22 @@ def test_temperature_setter(self, sample_model): assert model.temperature.value == pytest.approx(0.0) @pytest.mark.parametrize( - 'invalid_value', + "invalid_value", [ - 'invalid_temperature', + "invalid_temperature", [1, 2, 3], - {'temp': 10}, + {"temp": 10}, -5.0, ], - ids=['string', 'list', 'dict', 'negative'], + ids=["string", "list", "dict", "negative"], ) - def test_temperature_setter_raises_with_invalid_type(self, invalid_value, sample_model): + def test_temperature_setter_raises_with_invalid_type( + self, invalid_value, sample_model + ): # WHEN / THEN / EXPECT with pytest.raises( (TypeError, ValueError), - match=r'temperature must be a number or None|temperature must be non-negative', + match=r"temperature must be a number or None|temperature must be non-negative", ): sample_model.temperature = invalid_value @@ -266,7 +272,7 @@ def test_temperature_unit_setter_raises(self, sample_model): # WHEN / THEN / EXPECT with pytest.raises( AttributeError, - match='Temperature_unit is read-only', + match="Temperature_unit is read-only", ): sample_model.temperature_unit = 123 @@ -275,10 +281,10 @@ def test_convert_temperature_unit(self, sample_model): model = sample_model # THEN - model.convert_temperature_unit('mK') + model.convert_temperature_unit("mK") # EXPECT - assert model.temperature_unit == 'mK' + assert model.temperature_unit == "mK" assert model.temperature.value == 10 * 1000 def test_convert_temperature_unit_raises_with_no_temperature(self, sample_model): @@ -289,9 +295,9 @@ def test_convert_temperature_unit_raises_with_no_temperature(self, sample_model) # THEN / EXPECT with pytest.raises( ValueError, - match='Temperature is not set, cannot convert unit', + match="Temperature is not set, cannot convert unit", ): - model.convert_temperature_unit('mK') + model.convert_temperature_unit("mK") def test_convert_temperature_unit_raises_with_invalid_unit(self, sample_model): # WHEN @@ -300,9 +306,9 @@ def test_convert_temperature_unit_raises_with_invalid_unit(self, sample_model): # THEN / EXPECT with pytest.raises( UnitError, - match='Failed to', + match="Failed to", ): - model.convert_temperature_unit('invalid_unit') + model.convert_temperature_unit("invalid_unit") def test_normalize_detailed_balance_setter(self, sample_model): # WHEN @@ -320,13 +326,15 @@ def test_normalize_detailed_balance_setter(self, sample_model): # EXPECT assert model.normalize_detailed_balance is True - def test_normalize_detailed_balance_setter_raises_with_invalid_type(self, sample_model): + def test_normalize_detailed_balance_setter_raises_with_invalid_type( + self, sample_model + ): # WHEN / THEN / EXPECT with pytest.raises( TypeError, - match='normalize_detailed_balance must be True or False', + match="normalize_detailed_balance must be True or False", ): - sample_model.normalize_detailed_balance = 'invalid_value' + sample_model.normalize_detailed_balance = "invalid_value" def test_use_detailed_balance_setter(self, sample_model): # WHEN @@ -348,9 +356,9 @@ def test_use_detailed_balance_setter_raises_with_invalid_type(self, sample_model # WHEN / THEN / EXPECT with pytest.raises( TypeError, - match='use_detailed_balance must be True or False', + match="use_detailed_balance must be True or False", ): - sample_model.use_detailed_balance = 'invalid_value' + sample_model.use_detailed_balance = "invalid_value" def test_detailed_balance_settings_property(self, sample_model): # WHEN @@ -370,9 +378,9 @@ def test_detailed_balance_settings_setter_invalid(self, sample_model): # WHEN / THEN / EXPECT with pytest.raises( TypeError, - match='detailed_balance_settings must be a DetailedBalanceSettings', + match="detailed_balance_settings must be a DetailedBalanceSettings", ): - sample_model.detailed_balance_settings = 'invalid_settings' + sample_model.detailed_balance_settings = "invalid_settings" def test_evaluate_calls_dbf(self, sample_model): # WHEN @@ -386,7 +394,9 @@ def test_evaluate_calls_dbf(self, sample_model): sample_model._component_collections = [collection1, collection2] - with patch('easydynamics.sample_model.sample_model.detailed_balance_factor') as mock_dbf: + with patch( + "easydynamics.sample_model.sample_model.detailed_balance_factor" + ) as mock_dbf: mock_dbf.return_value = np.array([10.0, 10.0, 10.0]) # simplified DBF # THEN result = sample_model.evaluate(x) @@ -409,14 +419,14 @@ def test_evaluate_calls_dbf(self, sample_model): np.testing.assert_allclose(result[1], np.array([4.0, 5.0, 6.0]) * 10.0) @pytest.mark.parametrize( - 'temperature, use_detailed_balance', + "temperature, use_detailed_balance", [ (None, True), # DB disabled because temperature is None (300.0, False), # DB disabled explicitly ], ids=[ - 'temperature_none', - 'use_detailed_balance_false', + "temperature_none", + "use_detailed_balance_false", ], ) def test_evaluate_doesnt_call_dbf_when_disabled( @@ -436,7 +446,9 @@ def test_evaluate_doesnt_call_dbf_when_disabled( sample_model.temperature = temperature sample_model.use_detailed_balance = use_detailed_balance - with patch('easydynamics.sample_model.sample_model.detailed_balance_factor') as mock_dbf: + with patch( + "easydynamics.sample_model.sample_model.detailed_balance_factor" + ) as mock_dbf: mock_dbf.return_value = np.array([10.0, 10.0, 10.0]) # simplified DBF # THEN result = sample_model.evaluate(x) @@ -462,11 +474,11 @@ def test_generate_component_collections(self, sample_model): for collection in sample_model._component_collections: assert isinstance(collection, ComponentCollection) assert len(collection.components) == 3 # 3 components - assert collection.components[0].display_name == 'TestGaussian1' + assert collection.components[0].display_name == "TestGaussian1" assert collection.components[0].area.value == pytest.approx(1.0) - assert collection.components[1].display_name == 'TestLorentzian1' + assert collection.components[1].display_name == "TestLorentzian1" assert collection.components[1].area.value == pytest.approx(2.0) - assert collection.components[2].display_name == 'Brownian diffusion' + assert collection.components[2].display_name == "Brownian diffusion" assert isinstance(collection.components[2], Lorentzian) def test_get_all_variables(self, sample_model): @@ -477,7 +489,9 @@ def test_get_all_variables(self, sample_model): # EXPECT # Should include temperature and variables from diffusion model - expected_num_vars = 3 * 3 * 3 # 3 components, each with 3 parameters, across 3 Q values + expected_num_vars = ( + 3 * 3 * 3 + ) # 3 components, each with 3 parameters, across 3 Q values expected_num_vars += 2 # diffusion model has 2 parameters expected_num_vars += 1 # temperature variable @@ -499,10 +513,10 @@ def test_repr(self, sample_model): repr_str = repr(sample_model) # THEN / EXPECT - assert 'SampleModel' in repr_str - assert 'unit=' in repr_str - assert 'Q = ' in repr_str - assert 'components' in repr_str - assert 'diffusion_models' in repr_str - assert 'temperature' in repr_str - assert 'normalize_detailed_balance' in repr_str + assert "SampleModel" in repr_str + assert "unit=" in repr_str + assert "Q = " in repr_str + assert "components" in repr_str + assert "diffusion_models" in repr_str + assert "temperature" in repr_str + assert "normalize_detailed_balance" in repr_str From d2b15be0689397c30c5cbcbf5b789e04dea05754 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Sat, 16 May 2026 13:01:47 +0200 Subject: [PATCH 09/20] Fix serialisation for componentcollection --- .../docs/tutorials/component_collection.ipynb | 6 +- .../base_classes/easydynamics_base.py | 16 +- .../base_classes/easydynamics_list.py | 51 ++++-- .../base_classes/easydynamics_modelbase.py | 46 +----- src/easydynamics/base_classes/name_mixin.py | 7 + .../sample_model/component_collection.py | 78 ++++++--- src/easydynamics/sample_model/model_base.py | 62 +++---- .../test_brownian_translational_diffusion.py | 148 ++++++++++------- .../test_jump_translational_diffusion.py | 155 ++++++++++-------- .../sample_model/test_model_base.py | 28 ++-- .../sample_model/test_sample_model.py | 10 +- 11 files changed, 349 insertions(+), 258 deletions(-) diff --git a/docs/docs/tutorials/component_collection.ipynb b/docs/docs/tutorials/component_collection.ipynb index 5a957afc..fe108648 100644 --- a/docs/docs/tutorials/component_collection.ipynb +++ b/docs/docs/tutorials/component_collection.ipynb @@ -56,7 +56,7 @@ "y = component_collection.evaluate(x)\n", "plt.plot(x, y, label='Component collection')\n", "\n", - "for component in component_collection.components:\n", + "for component in component_collection:\n", " y = component.evaluate(x)\n", " plt.plot(x, y, label=component.display_name)\n", "\n", @@ -67,7 +67,7 @@ ], "metadata": { "kernelspec": { - "display_name": "easydynamics_newbase", + "display_name": "Python 3", "language": "python", "name": "python3" }, @@ -81,7 +81,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.12" + "version": "3.14.4" } }, "nbformat": 4, diff --git a/src/easydynamics/base_classes/easydynamics_base.py b/src/easydynamics/base_classes/easydynamics_base.py index e8c9500f..7d799daa 100644 --- a/src/easydynamics/base_classes/easydynamics_base.py +++ b/src/easydynamics/base_classes/easydynamics_base.py @@ -4,9 +4,10 @@ from easyscience.base_classes.new_base import NewBase from easydynamics.base_classes.name_mixin import NameMixin +from typing import Any -class EasyDynamicsBase(NewBase, NameMixin): +class EasyDynamicsBase(NameMixin, NewBase): """Base class for all EasyDynamics classes.""" def __init__( @@ -14,6 +15,7 @@ def __init__( name: str = "MyEasyDynamicsModel", display_name: str | None = None, unique_name: str | None = None, + **kwargs: Any, # noqa: ANN401 ) -> None: """ Initialize the EasyDynamicsBase. @@ -32,9 +34,13 @@ def __init__( TypeError If name is not a string. """ - NameMixin.__init__(self, name=name) if display_name is None: - display_name = self.name - - NewBase.__init__(self, display_name=display_name, unique_name=unique_name) + display_name = name + + super().__init__( + name=name, + display_name=display_name, + unique_name=unique_name, + **kwargs, + ) diff --git a/src/easydynamics/base_classes/easydynamics_list.py b/src/easydynamics/base_classes/easydynamics_list.py index 4fe704a0..b2c2ff40 100644 --- a/src/easydynamics/base_classes/easydynamics_list.py +++ b/src/easydynamics/base_classes/easydynamics_list.py @@ -15,14 +15,13 @@ ProtectedType_ = TypeVar("ProtectedType", bound=NewBase) -class EasyDynamicsList(EasyList, NameMixin): +class EasyDynamicsList(EasyList): """Base class for all EasyDynamics lists.""" def __init__( self, *args: ProtectedType_ | list[ProtectedType_], protected_types: list[type[NewBase]] | type[NewBase] | None = None, - name: str = "MyEasyDynamicsList", display_name: str | None = None, unique_name: str | None = None, **kwargs: Any, # noqa: ANN401 @@ -46,13 +45,11 @@ def __init__( Additional keyword arguments to pass to the EasyList constructor. """ - NameMixin.__init__(self, name=name) if display_name is None: - display_name = self.name + display_name = unique_name - EasyList.__init__( - self, + super().__init__( *args, protected_types=protected_types, display_name=display_name, @@ -111,25 +108,49 @@ def extend(self, values: Iterable[ProtectedType_]) -> None: for v in values: self.append(v) - def pop(self, idx: int) -> ProtectedType_: + def pop(self, index: int | str = -1) -> ProtectedType_: + """Remove and return an item at the given index or name. + + :param index: Index or unique_name of the item to remove + :return: The removed item """ - Remove and return an item at a specific index. + if isinstance(index, int): + return self._data.pop(index) + elif isinstance(index, str): + for i, item in enumerate(self._data): + if self._get_key(item) == index: + return self._data.pop(i) + raise KeyError(f'No item with unique name "{index}" found') + else: + raise TypeError("Index must be an int or str") + + def pop(self, index: int | str = -1) -> ProtectedType_: + """ + Remove and return an item at a specific index or name. Parameters ---------- - idx : int - The index at which to pop the item. + index : int | str + The index or name at which to pop the item. Returns ------- ProtectedType_ The item that was popped. """ - if not isinstance(idx, int): - raise TypeError("Index must be an integer.") - item = self[idx] - item.unlock_name() - return super().pop(idx) + if isinstance(index, int): + item = self[index] + item.unlock_name() + return self._data.pop(index) + elif isinstance(index, str): + for i, item in enumerate(self._data): + if self._get_key(item) == index: + item = self[i] + item.unlock_name() + return self._data.pop(i) + raise KeyError(f'No item with name "{index}" found') + else: + raise TypeError("Index must be an int or str") # ------------------------------------------------------------------ # Private methods diff --git a/src/easydynamics/base_classes/easydynamics_modelbase.py b/src/easydynamics/base_classes/easydynamics_modelbase.py index 9e4e0565..5c092729 100644 --- a/src/easydynamics/base_classes/easydynamics_modelbase.py +++ b/src/easydynamics/base_classes/easydynamics_modelbase.py @@ -8,7 +8,7 @@ from easydynamics.utils.utils import _validate_unit -class EasyDynamicsModelBase(ModelBase, NameMixin): +class EasyDynamicsModelBase(NameMixin, ModelBase): """Base class for all EasyDynamics models.""" def __init__( @@ -17,6 +17,7 @@ def __init__( name: str = "MyEasyDynamicsModel", display_name: str | None = None, unique_name: str | None = None, + **kwargs, ) -> None: """ Initialize the EasyDynamicsModelBase. @@ -37,12 +38,15 @@ def __init__( TypeError If name is not a string. """ - NameMixin.__init__(self, name=name) - if display_name is None: - display_name = self.name + display_name = name + + super().__init__( + name=name, + display_name=display_name, + unique_name=unique_name, + ) - ModelBase.__init__(self, display_name=display_name, unique_name=unique_name) self._unit = _validate_unit(unit) @property @@ -77,35 +81,3 @@ def unit(self, _unit_str: str) -> None: 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." ) - - @property - def name(self) -> str: - """ - Get the name of the model. - - Returns - ------- - str - The name of the model. - """ - return self._name - - @name.setter - def name(self, name_str: str) -> None: - """ - Set the name of the model. - - Parameters - ---------- - name_str : str - The new name to set. - - Raises - ------ - TypeError - If name_str is not a string. - """ - - if not isinstance(name_str, str): - raise TypeError("Name must be a string.") - self._name = name_str diff --git a/src/easydynamics/base_classes/name_mixin.py b/src/easydynamics/base_classes/name_mixin.py index 6b2c8cec..656bf54a 100644 --- a/src/easydynamics/base_classes/name_mixin.py +++ b/src/easydynamics/base_classes/name_mixin.py @@ -2,12 +2,17 @@ # SPDX-License-Identifier: BSD-3-Clause +from typing import Any + + class NameMixin: """Mixin class to add name functionality to EasyDynamics classes.""" def __init__( self, + *args: Any, name: str = "MyEasyDynamicsModel", + **kwargs: Any, # noqa: ANN401 ) -> None: """ Initialize the NameMixin. @@ -22,6 +27,8 @@ def __init__( TypeError If name is not a string. """ + + super().__init__(*args, **kwargs) if not isinstance(name, str): raise TypeError("Name must be a string.") self._name = name diff --git a/src/easydynamics/sample_model/component_collection.py b/src/easydynamics/sample_model/component_collection.py index bc059026..9933e964 100644 --- a/src/easydynamics/sample_model/component_collection.py +++ b/src/easydynamics/sample_model/component_collection.py @@ -3,11 +3,13 @@ from __future__ import annotations +import importlib import warnings from typing import TYPE_CHECKING import numpy as np import scipp as sc +from easyscience.io.serializer_base import SerializerBase from easyscience.variable import DescriptorBase from easyscience.variable import Parameter @@ -53,32 +55,29 @@ def __init__( TypeError If unit is not a string or sc.Unit, or if components is not a list of ModelComponent. """ - - EasyDynamicsModelBase.__init__( - self, - unit=unit, - name=name, - display_name=display_name, - unique_name=unique_name, - ) - - if components is not None: - if isinstance(components, ModelComponent): - components = [components] - elif not isinstance(components, list): + if components is None: + components = [] + if isinstance(components, ModelComponent): + components = [components] + elif not isinstance(components, list): + raise TypeError( + f"components must be a ModelComponent or a list of ModelComponent, got {type(components).__name__} instead." # noqa: E501 + ) + for comp in components: + if not isinstance(comp, ModelComponent): raise TypeError( - f"components must be a ModelComponent or a list of ModelComponent, got {type(components).__name__} instead." # noqa: E501 + f"All items in components must be instances of ModelComponent, got {type(comp).__name__} instead." # noqa: E501 ) - for comp in components: - if not isinstance(comp, ModelComponent): - raise TypeError( - f"All items in components must be instances of ModelComponent, got {type(comp).__name__} instead." # noqa: E501 - ) EasyDynamicsList.__init__( self, - *(components or []), + *components, protected_types=ModelComponent, + ) + + EasyDynamicsModelBase.__init__( + self, + unit=unit, name=name, display_name=display_name, unique_name=unique_name, @@ -359,3 +358,42 @@ def __repr__(self) -> str: comp_names = ", ".join(c.name for c in self) or "No components" return f"" + + def to_dict(self) -> dict: + return { + "@module": self.__class__.__module__, + "@class": self.__class__.__name__, + "unit": str(self.unit), + "name": self.name, + "display_name": self.display_name, + "components": [c.to_dict() for c in self._data], + } + + @classmethod + def from_dict(cls, obj_dict: dict) -> ComponentCollection: + + def deserialise_component(d): + module = importlib.import_module(d["@module"]) + cls = getattr(module, d["@class"]) + return cls.from_dict(d) + + components = [deserialise_component(c) for c in obj_dict.get("components", [])] + + return cls( + components=components, + unit=obj_dict.get("unit", "meV"), + name=obj_dict.get("name", "ComponentCollection"), + display_name=obj_dict.get("display_name"), + ) + + def __copy__(self) -> ComponentCollection: + """ + Create a deep copy of the ComponentCollection. + + Returns + ------- + ComponentCollection + A deep copy of the ComponentCollection. + """ + + return self.from_dict(self.to_dict()) diff --git a/src/easydynamics/sample_model/model_base.py b/src/easydynamics/sample_model/model_base.py index e76d4370..6ca886c0 100644 --- a/src/easydynamics/sample_model/model_base.py +++ b/src/easydynamics/sample_model/model_base.py @@ -24,9 +24,9 @@ class ModelBase(EasyDynamicsModelBase): 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, ) -> None: @@ -63,8 +63,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() @@ -100,8 +100,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" ) return [collection.evaluate(x) for collection in self._component_collections] @@ -120,21 +120,21 @@ def append_component(self, component: ModelComponent | ComponentCollection) -> N self._components.append_component(component) self._on_components_change() - def remove_component(self, unique_name: str) -> None: + def remove_component(self, name: str) -> None: """ - Remove a ModelComponent from the SampleModel by its unique name. + Remove a ModelComponent from the SampleModel by its name. Parameters ---------- - unique_name : str - The unique name of the ModelComponent to remove. + name : str + The name of the ModelComponent to remove. """ - self._components.remove_component(unique_name) + self._components.pop(name) self._on_components_change() def clear_components(self) -> None: """Clear all ModelComponents from the SampleModel.""" - self._components.clear_components() + self._components.clear() self._on_components_change() # ------------------------------------------------------------------ @@ -170,8 +170,8 @@ def unit(self, _unit_str: str) -> None: Always raised to indicate that the unit is read-only. """ 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." ) @property @@ -184,7 +184,7 @@ def components(self) -> list[ModelComponent]: list[ModelComponent] The components of the SampleModel. """ - return self._components.components + return self._components @components.setter def components(self, value: ModelComponent | ComponentCollection | None) -> None: @@ -202,7 +202,9 @@ def components(self, value: ModelComponent | ComponentCollection | None) -> None If value is not a ModelComponent, ComponentCollection, or None. """ 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: @@ -250,8 +252,8 @@ def Q(self, value: Q_type | None) -> None: if len(old_Q) != len(new_Q) or not np.allclose(old_Q, new_Q): raise ValueError( - 'New Q values are not similar to the old ones. ' - 'To change Q values, first run clear_Q().' + "New Q values are not similar to the old ones. " + "To change Q values, first run clear_Q()." ) def clear_Q(self, confirm: bool = False) -> None: @@ -271,7 +273,7 @@ def clear_Q(self, confirm: bool = False) -> None: """ if not confirm: raise ValueError( - 'Clearing Q values requires confirmation. Set confirm=True to proceed.' + "Clearing Q values requires confirmation. Set confirm=True to proceed." ) self._Q = None self._on_Q_change() @@ -300,7 +302,9 @@ def convert_unit(self, unit: str | sc.Unit) -> None: old_unit = self._unit if not isinstance(unit, (str, sc.Unit)): - raise TypeError(f'Unit must be a string or sc.Unit, got {type(unit).__name__}') + raise TypeError( + f"Unit must be a string or sc.Unit, got {type(unit).__name__}" + ) try: for component in self.components: component.convert_unit(unit) @@ -359,11 +363,13 @@ 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 @@ -390,11 +396,11 @@ def get_component_collection(self, Q_index: int) -> ComponentCollection: The ComponentCollection at the. """ if not isinstance(Q_index, int): - raise TypeError(f'Q_index must be an int, got {type(Q_index).__name__}') + 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)}' + 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] @@ -440,6 +446,6 @@ def __repr__(self) -> str: A string representation of the ModelBase. """ 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}" ) 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 8c813ec7..e93eaca0 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 @@ -12,9 +12,9 @@ BrownianTranslationalDiffusion, ) -hbar_1 = DescriptorNumber('hbar', 1.0) -hbar = DescriptorNumber.from_scipp('hbar', scipp_hbar) -angstrom = DescriptorNumber('angstrom', 1e-10, unit='m') +hbar_1 = DescriptorNumber("hbar", 1.0) +hbar = DescriptorNumber.from_scipp("hbar", scipp_hbar) +angstrom = DescriptorNumber("angstrom", 1e-10, unit="m") class TestBrownianTranslationalDiffusion: @@ -24,68 +24,82 @@ def brownian_diffusion_model(self): def test_init_default(self, brownian_diffusion_model): # WHEN THEN EXPECT - assert brownian_diffusion_model.display_name == 'BrownianTranslationalDiffusion' - assert brownian_diffusion_model.unit == 'meV' + assert brownian_diffusion_model.display_name == "BrownianTranslationalDiffusion" + assert brownian_diffusion_model.unit == "meV" assert brownian_diffusion_model.scale.value == pytest.approx(1.0) - assert brownian_diffusion_model.diffusion_coefficient.value == pytest.approx(1.0) + assert brownian_diffusion_model.diffusion_coefficient.value == pytest.approx( + 1.0 + ) @pytest.mark.parametrize( - 'kwargs,expected_exception, expected_message', + "kwargs,expected_exception, expected_message", [ ( { - 'unit': 123, - 'scale': 1.0, - 'diffusion_coefficient': 1.0, + "unit": 123, + "scale": 1.0, + "diffusion_coefficient": 1.0, }, UnitError, - 'Invalid unit', + "Invalid unit", ), ( { - 'unit': 123, - 'scale': 'invalid', - 'diffusion_coefficient': 1.0, + "unit": 123, + "scale": "invalid", + "diffusion_coefficient": 1.0, }, TypeError, - 'scale must be a number', + "scale must be a number", ), ( { - 'unit': 123, - 'scale': 1.0, - 'diffusion_coefficient': 'invalid', + "unit": 123, + "scale": 1.0, + "diffusion_coefficient": "invalid", }, TypeError, - 'diffusion_coefficient must be a number', + "diffusion_coefficient must be a number", ), ], ) - def test_input_type_validation_raises(self, kwargs, expected_exception, expected_message): + 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) + BrownianTranslationalDiffusion( + display_name="BrownianTranslationalDiffusion", **kwargs + ) def test_diffusion_coefficient_setter(self, brownian_diffusion_model): # WHEN brownian_diffusion_model.diffusion_coefficient = 3.0 # THEN EXPECT - assert brownian_diffusion_model.diffusion_coefficient.value == pytest.approx(3.0) + assert brownian_diffusion_model.diffusion_coefficient.value == pytest.approx( + 3.0 + ) def test_diffusion_coefficient_setter_raises(self, brownian_diffusion_model): # WHEN THEN EXPECT - with pytest.raises(TypeError, match=r'diffusion_coefficient must be a number.'): - brownian_diffusion_model.diffusion_coefficient = 'invalid' # Invalid type + with pytest.raises(TypeError, match=r"diffusion_coefficient must be a number."): + brownian_diffusion_model.diffusion_coefficient = "invalid" # Invalid type - def test_diffusion_coefficient_setter_negative_raises(self, brownian_diffusion_model): + def test_diffusion_coefficient_setter_negative_raises( + self, brownian_diffusion_model + ): # WHEN THEN EXPECT - with pytest.raises(ValueError, match=r'diffusion_coefficient must be non-negative.'): - brownian_diffusion_model.diffusion_coefficient = -1.0 # Invalid negative value + with pytest.raises( + ValueError, match=r"diffusion_coefficient must be non-negative." + ): + brownian_diffusion_model.diffusion_coefficient = ( + -1.0 + ) # Invalid negative value def test_calculate_width_type_error(self, brownian_diffusion_model): # WHEN THEN EXPECT - with pytest.raises(TypeError, match='Q must be '): - brownian_diffusion_model.calculate_width(Q='invalid') # Invalid type + with pytest.raises(TypeError, match="Q must be "): + brownian_diffusion_model.calculate_width(Q="invalid") # Invalid type def test_calculate_width(self, brownian_diffusion_model): # WHEN @@ -99,8 +113,8 @@ def test_calculate_width(self, brownian_diffusion_model): 1 * sc.Unit(brownian_diffusion_model.diffusion_coefficient.unit) * scipp_hbar - / (1 * sc.Unit('Å') ** 2), - 'meV', + / (1 * sc.Unit("Å") ** 2), + "meV", ) expected_widths = 1.0 * unit_conversion_factor.value * (Q_values**2) np.testing.assert_allclose(widths, expected_widths, rtol=1e-5) @@ -113,13 +127,13 @@ def test_calculate_EISF(self, brownian_diffusion_model): EISF = brownian_diffusion_model.calculate_EISF(Q_values) # EXPECT - expected_EISHF = np.zeros_like(Q_values) - np.testing.assert_array_equal(EISF, expected_EISHF) + expected_EISF = np.zeros_like(Q_values) + np.testing.assert_array_equal(EISF, expected_EISF) def test_calculate_EISF_type_error(self, brownian_diffusion_model): # WHEN THEN EXPECT - with pytest.raises(TypeError, match='Q must be '): - brownian_diffusion_model.calculate_EISF(Q='invalid') # Invalid type + with pytest.raises(TypeError, match="Q must be "): + brownian_diffusion_model.calculate_EISF(Q="invalid") # Invalid type def test_calculate_QISF(self, brownian_diffusion_model): # WHEN @@ -134,34 +148,36 @@ def test_calculate_QISF(self, brownian_diffusion_model): def test_calculate_QISF_type_error(self, brownian_diffusion_model): # WHEN THEN EXPECT - with pytest.raises(TypeError, match='Q must be '): - brownian_diffusion_model.calculate_QISF(Q='invalid') # Invalid type + with pytest.raises(TypeError, match="Q must be "): + brownian_diffusion_model.calculate_QISF(Q="invalid") # Invalid type @pytest.mark.parametrize( - 'Q', + "Q", [ (0.5), ([1.0, 2.0, 3.0]), (np.array([1.0, 2.0, 3.0])), ], ids=[ - 'python_scalar', - 'python_list', - 'numpy_array', + "python_scalar", + "python_list", + "numpy_array", ], ) def test_create_component_collections(self, brownian_diffusion_model, Q): # WHEN # THEN - component_collections = brownian_diffusion_model.create_component_collections(Q=Q) + component_collections = brownian_diffusion_model.create_component_collections( + Q=Q + ) # EXPECT expected_widths = brownian_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 len(model) == 1 + component = model[0] assert component.width.unit == brownian_diffusion_model.unit assert np.isclose(component.width.value, expected_widths[model_index]) assert component.width.independent is False @@ -170,7 +186,7 @@ def test_create_component_collections_component_name_must_be_string( self, brownian_diffusion_model ): # WHEN THEN EXPECT - with pytest.raises(TypeError, match=r'component_name must be a string.'): + with pytest.raises(TypeError, match=r"component_name must be a string."): brownian_diffusion_model.create_component_collections( Q=np.array([0.1, 0.2, 0.3]), component_name=123 ) @@ -179,19 +195,25 @@ def test_create_component_collections_component_display_name_must_be_string( self, brownian_diffusion_model ): # WHEN THEN EXPECT - with pytest.raises(TypeError, match=r'component_display_name must be a string.'): + with pytest.raises( + TypeError, match=r"component_display_name must be a string." + ): brownian_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, brownian_diffusion_model): # WHEN THEN EXPECT - with pytest.raises(TypeError, match='Q must be a '): - brownian_diffusion_model.create_component_collections(Q='invalid') # Invalid type + with pytest.raises(TypeError, match="Q must be a "): + brownian_diffusion_model.create_component_collections( + Q="invalid" + ) # Invalid type - def test_create_component_collections_Q_1dimensional_error(self, brownian_diffusion_model): + def test_create_component_collections_Q_1dimensional_error( + self, brownian_diffusion_model + ): # WHEN THEN EXPECT - with pytest.raises(ValueError, match=r'Q must be a 1-dimensional array.'): + with pytest.raises(ValueError, match=r"Q must be a 1-dimensional array."): brownian_diffusion_model.create_component_collections( Q=np.array([[0.1, 0.2], [0.3, 0.4]]) ) # Invalid shape @@ -201,35 +223,37 @@ def test_write_width_dependency_expression(self, brownian_diffusion_model): expression = brownian_diffusion_model._write_width_dependency_expression(0.5) # EXPECT - expected_expression = 'hbar * D* 0.5 **2*1/(angstrom**2)' + expected_expression = "hbar * D* 0.5 **2*1/(angstrom**2)" assert expression == expected_expression def test_write_width_dependency_map_expression(self, brownian_diffusion_model): # WHEN THEN - expression_map = brownian_diffusion_model._write_width_dependency_map_expression() + expression_map = ( + brownian_diffusion_model._write_width_dependency_map_expression() + ) # EXPECT expected_map = { - 'D': brownian_diffusion_model.diffusion_coefficient, - 'hbar': brownian_diffusion_model._hbar, - 'angstrom': brownian_diffusion_model._angstrom, + "D": brownian_diffusion_model.diffusion_coefficient, + "hbar": brownian_diffusion_model._hbar, + "angstrom": brownian_diffusion_model._angstrom, } assert expression_map == expected_map def test_write_width_dependency_expression_raises(self, brownian_diffusion_model): - with pytest.raises(TypeError, match='Q must be a float'): - brownian_diffusion_model._write_width_dependency_expression('invalid') + with pytest.raises(TypeError, match="Q must be a float"): + brownian_diffusion_model._write_width_dependency_expression("invalid") def test_write_area_dependency_expression_raises(self, brownian_diffusion_model): - with pytest.raises(TypeError, match='QISF must be a float'): - brownian_diffusion_model._write_area_dependency_expression('invalid') + with pytest.raises(TypeError, match="QISF must be a float"): + brownian_diffusion_model._write_area_dependency_expression("invalid") def test_repr(self, brownian_diffusion_model): # WHEN THEN repr_str = repr(brownian_diffusion_model) # EXPECT - assert 'BrownianTranslationalDiffusion' in repr_str - assert 'diffusion_coefficient' in repr_str - assert 'scale=' in repr_str + assert "BrownianTranslationalDiffusion" in repr_str + assert "diffusion_coefficient" in repr_str + assert "scale=" in repr_str 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 index a2b8022a..3055b667 100644 --- 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 @@ -12,9 +12,9 @@ JumpTranslationalDiffusion, ) -hbar_1 = DescriptorNumber('hbar', 1.0) -hbar = DescriptorNumber.from_scipp('hbar', scipp_hbar) -angstrom = DescriptorNumber('angstrom', 1e-10, unit='m') +hbar_1 = DescriptorNumber("hbar", 1.0) +hbar = DescriptorNumber.from_scipp("hbar", scipp_hbar) +angstrom = DescriptorNumber("angstrom", 1e-10, unit="m") class TestJumpTranslationalDiffusion: @@ -24,60 +24,64 @@ def jump_diffusion_model(self): 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.display_name == "JumpTranslationalDiffusion" + assert jump_diffusion_model.unit == "meV" assert jump_diffusion_model.scale.value == pytest.approx(1.0) assert jump_diffusion_model.diffusion_coefficient.value == pytest.approx(1.0) assert jump_diffusion_model.relaxation_time.value == pytest.approx(1.0) @pytest.mark.parametrize( - 'kwargs,expected_exception, expected_message', + "kwargs,expected_exception, expected_message", [ ( { - 'unit': 123, - 'scale': 1.0, - 'diffusion_coefficient': 1.0, - 'relaxation_time': 1.0, + "unit": 123, + "scale": 1.0, + "diffusion_coefficient": 1.0, + "relaxation_time": 1.0, }, UnitError, - 'Invalid unit', + "Invalid unit", ), ( { - 'unit': 'meV', - 'scale': 'invalid', - 'diffusion_coefficient': 1.0, - 'relaxation_time': 1.0, + "unit": "meV", + "scale": "invalid", + "diffusion_coefficient": 1.0, + "relaxation_time": 1.0, }, TypeError, - 'scale must be a number', + "scale must be a number", ), ( { - 'unit': 'meV', - 'scale': 1.0, - 'diffusion_coefficient': 'invalid', - 'relaxation_time': 1.0, + "unit": "meV", + "scale": 1.0, + "diffusion_coefficient": "invalid", + "relaxation_time": 1.0, }, TypeError, - 'diffusion_coefficient must be a number', + "diffusion_coefficient must be a number", ), ( { - 'unit': 'meV', - 'scale': 1.0, - 'diffusion_coefficient': 1.0, - 'relaxation_time': 'invalid', + "unit": "meV", + "scale": 1.0, + "diffusion_coefficient": 1.0, + "relaxation_time": "invalid", }, TypeError, - 'relaxation_time must be a number', + "relaxation_time must be a number", ), ], ) - def test_input_type_validation_raises(self, kwargs, expected_exception, expected_message): + 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) + JumpTranslationalDiffusion( + display_name="JumpTranslationalDiffusion", **kwargs + ) def test_diffusion_coefficient_setter(self, jump_diffusion_model): # WHEN @@ -88,12 +92,14 @@ def test_diffusion_coefficient_setter(self, jump_diffusion_model): def test_diffusion_coefficient_setter_raises(self, jump_diffusion_model): # WHEN THEN EXPECT - with pytest.raises(TypeError, match=r'diffusion_coefficient must be a number.'): - jump_diffusion_model.diffusion_coefficient = 'invalid' # Invalid type + with pytest.raises(TypeError, match=r"diffusion_coefficient must be a number."): + jump_diffusion_model.diffusion_coefficient = "invalid" # Invalid type def test_diffusion_coefficient_setter_negative_raises(self, jump_diffusion_model): # WHEN THEN EXPECT - with pytest.raises(ValueError, match=r'diffusion_coefficient must be non-negative.'): + with pytest.raises( + ValueError, match=r"diffusion_coefficient must be non-negative." + ): jump_diffusion_model.diffusion_coefficient = -1.0 # Invalid negative value def test_relaxation_time_setter(self, jump_diffusion_model): @@ -105,39 +111,42 @@ def test_relaxation_time_setter(self, jump_diffusion_model): def test_relaxation_time_setter_raises(self, jump_diffusion_model): # WHEN THEN EXPECT - with pytest.raises(TypeError, match=r'relaxation_time must be a number.'): - jump_diffusion_model.relaxation_time = 'invalid' # Invalid type + with pytest.raises(TypeError, match=r"relaxation_time must be a number."): + jump_diffusion_model.relaxation_time = "invalid" # Invalid type def test_relaxation_time_setter_negative_raises(self, jump_diffusion_model): # WHEN THEN EXPECT - with pytest.raises(ValueError, match=r'relaxation_time must be non-negative.'): + with pytest.raises(ValueError, match=r"relaxation_time must be non-negative."): jump_diffusion_model.relaxation_time = -1.0 # Invalid negative value 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 + 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' + "instead of our Parameters" # WHEN - Q_values = sc.linspace('Q', 0.5, 1.5, num=6, unit='1/angstrom') + 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 + 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') + denominator = denominator.to(unit="1") # EXPECT - expected_widths = scipp_hbar * diffusion_coefficient_sc * (Q_values**2) / (1 + denominator) + expected_widths = ( + scipp_hbar * diffusion_coefficient_sc * (Q_values**2) / (1 + denominator) + ) expected_widths = expected_widths.to(unit=jump_diffusion_model.unit) @@ -156,8 +165,8 @@ def test_calculate_EISF(self, jump_diffusion_model): 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 + 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 @@ -172,20 +181,20 @@ def test_calculate_QISF(self, jump_diffusion_model): 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 + with pytest.raises(TypeError, match="Q must be "): + jump_diffusion_model.calculate_QISF(Q="invalid") # Invalid type @pytest.mark.parametrize( - 'Q', + "Q", [ (0.5), ([1.0, 2.0, 3.0]), (np.array([1.0, 2.0, 3.0])), ], ids=[ - 'python_scalar', - 'python_list', - 'numpy_array', + "python_scalar", + "python_list", + "numpy_array", ], ) def test_create_component_collections(self, jump_diffusion_model, Q): @@ -198,8 +207,8 @@ def test_create_component_collections(self, jump_diffusion_model, Q): 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 len(model) == 1 + component = model[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 @@ -208,7 +217,7 @@ def test_create_component_collections_component_name_must_be_string( self, jump_diffusion_model ): # WHEN THEN EXPECT - with pytest.raises(TypeError, match=r'component_name must be a string.'): + with pytest.raises(TypeError, match=r"component_name must be a string."): jump_diffusion_model.create_component_collections( Q=np.array([0.1, 0.2, 0.3]), component_name=123 ) @@ -217,19 +226,25 @@ def test_create_component_collections_component_display_name_must_be_string( self, jump_diffusion_model ): # WHEN THEN EXPECT - with pytest.raises(TypeError, match=r'component_display_name must be a string.'): + with pytest.raises( + TypeError, match=r"component_display_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 + 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): + def test_create_component_collections_Q_1dimensional_error( + self, jump_diffusion_model + ): # WHEN THEN EXPECT - with pytest.raises(ValueError, match=r'Q must be a 1-dimensional array.'): + with pytest.raises(ValueError, match=r"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 @@ -240,7 +255,7 @@ def test_write_width_dependency_expression(self, jump_diffusion_model): # EXPECT expected_expression = ( - 'hbar * D* 0.5 **2/(angstrom**2)/(1 + (D * t* 0.5 **2/(angstrom**2)))' + "hbar * D* 0.5 **2/(angstrom**2)/(1 + (D * t* 0.5 **2/(angstrom**2)))" ) assert expression == expected_expression @@ -250,27 +265,27 @@ def test_write_width_dependency_map_expression(self, jump_diffusion_model): # 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, + "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') + 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') + 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 + 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 810e5d7e..322d8f01 100644 --- a/tests/unit/easydynamics/sample_model/test_model_base.py +++ b/tests/unit/easydynamics/sample_model/test_model_base.py @@ -19,7 +19,7 @@ class TestModelBase: def model_base(self): component1 = Gaussian( name="TestGaussian1Name", - name="TestGaussian1", + display_name="TestGaussian1", area=1.0, center=0.0, width=1.0, @@ -101,11 +101,11 @@ def test_generate_component_collections_with_Q(self, model_base): assert len(model_base._component_collections) == len(model_base.Q) for collection in model_base._component_collections: assert isinstance(collection, ComponentCollection) - assert len(collection.components) == 2 - assert isinstance(collection.components[0], Gaussian) - assert collection.components[0].display_name == "TestGaussian1" - assert isinstance(collection.components[1], Lorentzian) - assert collection.components[1].display_name == "TestLorentzian1" + assert len(collection) == 2 + assert isinstance(collection[0], Gaussian) + assert collection[0].display_name == "TestGaussian1" + assert isinstance(collection[1], Lorentzian) + assert collection[1].display_name == "TestLorentzian1" def test_fix_free_all_parameters(self, model_base): # WHEN @@ -200,7 +200,7 @@ def test_get_component_collection_invalid_index_raises(self, model_base): def test_append_and_remove_and_clear_component(self, model_base): # WHEN - new_component = Gaussian(unique_name="NewGaussian") + new_component = Gaussian(name="NewGaussian") # THEN model_base.append_component(new_component) @@ -275,7 +275,7 @@ def test_convert_unit_incorrect_unit_raises(self, model_base): def test_components_setter(self, model_base): # WHEN - new_component = Lorentzian(unique_name="NewLorentzian") + new_component = Lorentzian(name="NewLorentzian") model_base.components = new_component # THEN / EXPECT @@ -331,9 +331,11 @@ def test_Q_setter_with_similar_Q(self, model_base, new_Q): def test_Q_setter_with_none(self, model_base): # WHEN - model_base.Q = None old_Q = model_base.Q + # THEN + model_base.Q = None + # THEN / EXPECT assert model_base.Q is old_Q @@ -350,9 +352,11 @@ def test_Q_setter_when_current_Q_is_none(self, model_base): def test_clear_Q(self, model_base): # WHEN + # + # THEN model_base.clear_Q(confirm=True) - # THEN / EXPECT + # EXPECT assert model_base.Q is None def test_clear_Q_raises_without_confirm(self, model_base): @@ -368,9 +372,7 @@ def test_normalize_area(self, model_base): # EXPECT for collection in model_base._component_collections: - total_area = sum( - component.area.value for component in collection.components - ) + total_area = sum(component.area.value for component in collection) assert total_area == pytest.approx(1.0) def test_repr(self, model_base): diff --git a/tests/unit/easydynamics/sample_model/test_sample_model.py b/tests/unit/easydynamics/sample_model/test_sample_model.py index 21677175..e4124db3 100644 --- a/tests/unit/easydynamics/sample_model/test_sample_model.py +++ b/tests/unit/easydynamics/sample_model/test_sample_model.py @@ -22,16 +22,16 @@ class TestSampleModel: @pytest.fixture def sample_model(self): component1 = Gaussian( - name="TestGaussian1", - unique_name="TestGaussian1", + name="TestGaussian1Name", + display_name="TestGaussian1Display", area=1.0, center=0.0, width=1.0, unit="meV", ) component2 = Lorentzian( - display_name="TestLorentzian1", - unique_name="TestLorentzian1", + name="TestLorentzian1Name", + display_name="TestLorentzian1Display", area=2.0, center=1.0, width=0.5, @@ -42,7 +42,7 @@ def sample_model(self): component_collection.append_component(component2) diffusion_model = BrownianTranslationalDiffusion( - display_name="DiffusionModel", unique_name="DiffusionModel" + display_name="DiffusionModelDisplay", name="DiffusionModelName" ) return SampleModel( From 4087629f2a35479b5bb147f9ee2cc4a9fa636b7a Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Sat, 16 May 2026 14:09:53 +0200 Subject: [PATCH 10/20] fix tests --- src/easydynamics/analysis/analysis1d.py | 6 +- .../base_classes/easydynamics_base.py | 11 +- .../base_classes/easydynamics_list.py | 80 +++--- .../base_classes/easydynamics_modelbase.py | 13 +- src/easydynamics/base_classes/name_mixin.py | 32 ++- .../convolution/analytical_convolution.py | 7 +- src/easydynamics/convolution/convolution.py | 12 +- .../convolution/numerical_convolution_base.py | 2 +- .../sample_model/component_collection.py | 78 +++--- .../brownian_translational_diffusion.py | 13 +- .../diffusion_model/diffusion_model_base.py | 32 +-- src/easydynamics/sample_model/model_base.py | 48 ++-- .../sample_model/resolution_model.py | 5 +- src/easydynamics/sample_model/sample_model.py | 31 ++- .../test_fitting_with_diffusion_model.py | 36 +-- .../easydynamics/analysis/test_analysis.py | 12 +- .../easydynamics/analysis/test_analysis1d.py | 6 +- .../analysis/test_analysis_base.py | 16 +- .../convolution/test_convolution.py | 240 +++++++----------- .../convolution/test_convolution_base.py | 4 +- .../convolution/test_numerical_convolution.py | 64 ++--- .../test_numerical_convolution_base.py | 204 +++++++-------- .../test_brownian_translational_diffusion.py | 140 +++++----- .../test_jump_translational_diffusion.py | 151 +++++------ .../sample_model/test_background_model.py | 40 ++- .../sample_model/test_component_collection.py | 194 ++++++-------- .../sample_model/test_model_base.py | 114 ++++----- .../sample_model/test_resolution_model.py | 68 +++-- .../sample_model/test_sample_model.py | 175 ++++++------- 29 files changed, 826 insertions(+), 1008 deletions(-) diff --git a/src/easydynamics/analysis/analysis1d.py b/src/easydynamics/analysis/analysis1d.py index a97d0681..b3f0ad85 100644 --- a/src/easydynamics/analysis/analysis1d.py +++ b/src/easydynamics/analysis/analysis1d.py @@ -866,13 +866,11 @@ def _create_components_dataset_single_Q( A dictionary of component names to their corresponding sc.DataArrays. """ scipp_arrays = {} - sample_components = self.sample_model.get_component_collection( - Q_index=self.Q_index - ).components + sample_components = self.sample_model.get_component_collection(Q_index=self.Q_index) background_components = self.instrument_model.background_model.get_component_collection( Q_index=self.Q_index - ).components + ) if energy is None: energy = self._masked_energy diff --git a/src/easydynamics/base_classes/easydynamics_base.py b/src/easydynamics/base_classes/easydynamics_base.py index 7d799daa..cf100ae5 100644 --- a/src/easydynamics/base_classes/easydynamics_base.py +++ b/src/easydynamics/base_classes/easydynamics_base.py @@ -1,10 +1,10 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause + from easyscience.base_classes.new_base import NewBase from easydynamics.base_classes.name_mixin import NameMixin -from typing import Any class EasyDynamicsBase(NameMixin, NewBase): @@ -12,10 +12,10 @@ class EasyDynamicsBase(NameMixin, NewBase): def __init__( self, - name: str = "MyEasyDynamicsModel", + name: str = 'MyEasyDynamicsModel', display_name: str | None = None, unique_name: str | None = None, - **kwargs: Any, # noqa: ANN401 + **kwargs: object, ) -> None: """ Initialize the EasyDynamicsBase. @@ -28,6 +28,8 @@ def __init__( Display name of the model. If None, the name will be used. unique_name : str | None, default=None Unique name of the model. If None, a unique name will be generated. + **kwargs : object + Additional keyword arguments to pass to the parent class. Raises ------ @@ -35,6 +37,9 @@ def __init__( If name is not a string. """ + if not isinstance(name, str): + raise TypeError(f'Name must be a string, got {type(name)}') + if display_name is None: display_name = name diff --git a/src/easydynamics/base_classes/easydynamics_list.py b/src/easydynamics/base_classes/easydynamics_list.py index b2c2ff40..9c322ac5 100644 --- a/src/easydynamics/base_classes/easydynamics_list.py +++ b/src/easydynamics/base_classes/easydynamics_list.py @@ -10,9 +10,7 @@ from easyscience.base_classes.easy_list import EasyList from easyscience.base_classes.new_base import NewBase -from easydynamics.base_classes.name_mixin import NameMixin - -ProtectedType_ = TypeVar("ProtectedType", bound=NewBase) +ProtectedType_ = TypeVar('ProtectedType', bound=NewBase) class EasyDynamicsList(EasyList): @@ -31,19 +29,18 @@ def __init__( Parameters ---------- - args : ProtectedType_ | list[ProtectedType_] - Initial items to add to the list. Can be a single item or a list of items. Each item must be an instance of one of the protected types. - protected_types : list[type[NewBase]] | type[NewBase] | None, optional - Types that are allowed in the list. Can be a single NewBase subclass or a list of them. If None, defaults to [NewBase]. - name : str, default='MyEasyDynamicsList' - Name of the list. + *args : ProtectedType_ | list[ProtectedType_] + Initial items to add to the list. Can be a single item or a list of items. Each item + must be an instance of one of the protected types. + protected_types : list[type[NewBase]] | type[NewBase] | None, default=None + Types that are allowed in the list. Can be a single NewBase subclass or a list of them. + If None, defaults to [NewBase]. display_name : str | None, default=None Display name of the list. If None, the name will be used. unique_name : str | None, default=None Unique name of the list. If None, a unique name will be generated. - kwargs : Any + **kwargs : Any Additional keyword arguments to pass to the EasyList constructor. - """ if display_name is None: @@ -93,13 +90,21 @@ def append(self, value: ProtectedType_) -> None: def extend(self, values: Iterable[ProtectedType_]) -> None: """ Extend the list by appending elements from the iterable. + Parameters ---------- values : Iterable[ProtectedType_] - An iterable of items to append. Each item must be an instance of one of the protected types. + An iterable of items to append. Each item must be an instance of one of the protected + types. + + Raises + ------ + TypeError + If values is not an iterable or if any item in values is not an instance of one of the + protected types. """ if not isinstance(values, Iterable): - raise TypeError("Values must be an iterable.") + raise TypeError('Values must be an iterable.') values = list(values) for v in values: @@ -108,56 +113,47 @@ def extend(self, values: Iterable[ProtectedType_]) -> None: for v in values: self.append(v) - def pop(self, index: int | str = -1) -> ProtectedType_: - """Remove and return an item at the given index or name. - - :param index: Index or unique_name of the item to remove - :return: The removed item - """ - if isinstance(index, int): - return self._data.pop(index) - elif isinstance(index, str): - for i, item in enumerate(self._data): - if self._get_key(item) == index: - return self._data.pop(i) - raise KeyError(f'No item with unique name "{index}" found') - else: - raise TypeError("Index must be an int or str") - def pop(self, index: int | str = -1) -> ProtectedType_: """ Remove and return an item at a specific index or name. Parameters ---------- - index : int | str + index : int | str, default=-1 The index or name at which to pop the item. Returns ------- ProtectedType_ The item that was popped. + + Raises + ------ + TypeError + If index is not an int or str. + KeyError + If index is a str and no item with that name is found. """ if isinstance(index, int): item = self[index] item.unlock_name() return self._data.pop(index) - elif isinstance(index, str): + if isinstance(index, str): for i, item in enumerate(self._data): if self._get_key(item) == index: item = self[i] item.unlock_name() return self._data.pop(i) raise KeyError(f'No item with name "{index}" found') - else: - raise TypeError("Index must be an int or str") + raise TypeError('Index must be an int or str') # ------------------------------------------------------------------ # Private methods # ------------------------------------------------------------------ def _check_name_unique(self, obj: NewBase | Iterable[NewBase]) -> None: - """Check that the name of an object is unique in the list. + """ + Check that the name of an object is unique in the list. Parameters ---------- obj : NewBase | Iterable[NewBase] @@ -174,7 +170,7 @@ def _check_name_unique(self, obj: NewBase | Iterable[NewBase]) -> None: new_names = [get_key(item) for item in items] if len(new_names) != len(set(new_names)): - raise ValueError(f"Duplicate names in {obj} detected.") + raise ValueError(f'Duplicate names in {obj} detected.') existing_names = {get_key(o) for o in self._data} @@ -184,7 +180,8 @@ def _check_name_unique(self, obj: NewBase | Iterable[NewBase]) -> None: raise ValueError(f'Name "{name}" already exists in list.') def _get_key(self, obj: NewBase) -> str: - """Get the name of an object. + """ + Get the name of an object. Parameters ---------- @@ -198,13 +195,13 @@ def _get_key(self, obj: NewBase) -> str: """ return obj.name - def _validate_type(self, value: Any) -> None: + def _validate_type(self, value: object) -> None: """ Validate that a value is an instance of one of the protected types. Parameters ---------- - value : Any + value : object The value to validate. Raises @@ -214,9 +211,9 @@ def _validate_type(self, value: Any) -> None: """ if not isinstance(value, tuple(self._protected_types)): - allowed = ", ".join(t.__name__ for t in self._protected_types) + allowed = ', '.join(t.__name__ for t in self._protected_types) raise TypeError( - f"Value must be an instance of type: {allowed}. Got {type(value).__name__} instead." # noqa: E501 + f'Value must be an instance of type: {allowed}. Got {type(value).__name__} instead.' # noqa: E501 ) # ------------------------------------------------------------------ @@ -234,7 +231,8 @@ def __setitem__( idx : int | slice The index at which to set the item. value : ProtectedType_ | Iterable[ProtectedType_] - The item or items to set. Must be an instance of one of the protected types or an iterable of protected types. + The item or items to set. Must be an instance of one of the protected types or an + iterable of protected types. """ self._check_name_unique(value) super().__setitem__(idx, value) diff --git a/src/easydynamics/base_classes/easydynamics_modelbase.py b/src/easydynamics/base_classes/easydynamics_modelbase.py index 5c092729..31a7338a 100644 --- a/src/easydynamics/base_classes/easydynamics_modelbase.py +++ b/src/easydynamics/base_classes/easydynamics_modelbase.py @@ -13,11 +13,10 @@ class EasyDynamicsModelBase(NameMixin, ModelBase): def __init__( self, - unit: str | sc.Unit = "meV", - name: str = "MyEasyDynamicsModel", + unit: str | sc.Unit = 'meV', + name: str = 'MyEasyDynamicsModel', display_name: str | None = None, unique_name: str | None = None, - **kwargs, ) -> None: """ Initialize the EasyDynamicsModelBase. @@ -38,6 +37,10 @@ def __init__( TypeError If name is not a string. """ + + if not isinstance(name, str): + raise TypeError(f'Name must be a string, got {type(name)}') + if display_name is None: display_name = name @@ -78,6 +81,6 @@ def unit(self, _unit_str: str) -> None: Always raised to indicate that the unit is read-only. """ 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.' ) diff --git a/src/easydynamics/base_classes/name_mixin.py b/src/easydynamics/base_classes/name_mixin.py index 656bf54a..086bdcf7 100644 --- a/src/easydynamics/base_classes/name_mixin.py +++ b/src/easydynamics/base_classes/name_mixin.py @@ -2,25 +2,26 @@ # SPDX-License-Identifier: BSD-3-Clause -from typing import Any - - class NameMixin: """Mixin class to add name functionality to EasyDynamics classes.""" def __init__( self, - *args: Any, - name: str = "MyEasyDynamicsModel", - **kwargs: Any, # noqa: ANN401 + *args: object, + name: str = 'MyEasyDynamicsModel', + **kwargs: object, ) -> None: """ Initialize the NameMixin. Parameters ---------- + *args : object + Positional arguments to pass to the parent class. name : str, default='MyEasyDynamicsModel' Name of the model. + **kwargs : object + Keyword arguments to pass to the parent class. Raises ------ @@ -30,7 +31,7 @@ def __init__( super().__init__(*args, **kwargs) if not isinstance(name, str): - raise TypeError("Name must be a string.") + raise TypeError('Name must be a string.') self._name = name self._name_lock_count = 0 @@ -58,15 +59,17 @@ def name(self, name_str: str) -> None: Raises ------ + AttributeError + If the name is locked due to being in a list. TypeError If name_str is not a string. """ if self._name_lock_count > 0: - raise AttributeError("Cannot change name while object is in a list.") + raise AttributeError('Cannot change name while object is in a list.') if not isinstance(name_str, str): - raise TypeError("Name must be a string.") + raise TypeError('Name must be a string.') self._name = name_str def lock_name(self) -> None: @@ -74,8 +77,15 @@ def lock_name(self) -> None: self._name_lock_count += 1 def unlock_name(self) -> None: - """Allow the name to be modified if no containers remain.""" + """ + Allow the name to be modified if no containers remain. + + Raises + ------ + RuntimeError + If the name lock count is already zero. + """ if self._name_lock_count == 0: - raise RuntimeError("Name lock count is already zero.") + raise RuntimeError('Name lock count is already zero.') self._name_lock_count -= 1 diff --git a/src/easydynamics/convolution/analytical_convolution.py b/src/easydynamics/convolution/analytical_convolution.py index ce4933b2..5ab305cf 100644 --- a/src/easydynamics/convolution/analytical_convolution.py +++ b/src/easydynamics/convolution/analytical_convolution.py @@ -94,15 +94,12 @@ def convolution( self.energy. """ - sample_components = self.sample_components.components - resolution_components = self.resolution_components.components - total = np.zeros_like(self.energy.values, dtype=float) - for sample_component in sample_components: + for sample_component in self.sample_components: # Go through resolution components, # adding analytical contributions - for resolution_component in resolution_components: + for resolution_component in self.resolution_components: contrib = self._convolute_analytic_pair( sample_component=sample_component, resolution_component=resolution_component, diff --git a/src/easydynamics/convolution/convolution.py b/src/easydynamics/convolution/convolution.py index 63bd66c6..8a11e05f 100644 --- a/src/easydynamics/convolution/convolution.py +++ b/src/easydynamics/convolution/convolution.py @@ -139,7 +139,7 @@ def convolution( total += self._numerical_convolver.convolution() # Delta function components - if self._delta_sample_components.components: + if self._delta_sample_components: total += self._convolve_delta_functions() return total @@ -159,7 +159,7 @@ def _convolve_delta_functions(self) -> np.ndarray: * self._resolution_components.evaluate( self.energy_with_offset.values - delta.center.value ) - for delta in self._delta_sample_components.components + for delta in self._delta_sample_components ) def _check_if_pair_is_analytic( @@ -221,7 +221,7 @@ def _build_convolution_plan(self) -> None: delta_sample_components = ComponentCollection() numerical_sample_components = ComponentCollection() - for sample_component in self._sample_components.components: + for sample_component in self._sample_components: # If delta function, put in delta sample model and go to the # next component if isinstance(sample_component, DeltaFunction): @@ -242,7 +242,7 @@ def _build_convolution_plan(self) -> None: # this sample component pair_is_analytic = [ self._check_if_pair_is_analytic(sample_component, resolution_component) - for resolution_component in self._resolution_components.components + for resolution_component in self._resolution_components ] # If all resolution components can be convolved analytically # with this sample component, add it to analytical @@ -268,7 +268,7 @@ def _set_convolvers(self) -> None: convolution method. """ - if self._analytical_sample_components.components: + if self._analytical_sample_components: self._analytical_convolver = AnalyticalConvolution( energy=self.energy, energy_offset=self.energy_offset, @@ -278,7 +278,7 @@ def _set_convolvers(self) -> None: else: self._analytical_convolver = None - if self._numerical_sample_components.components: + if self._numerical_sample_components: self._numerical_convolver = NumericalConvolution( energy=self.energy, energy_offset=self.energy_offset, diff --git a/src/easydynamics/convolution/numerical_convolution_base.py b/src/easydynamics/convolution/numerical_convolution_base.py index 7001e32b..9a6e80cd 100644 --- a/src/easydynamics/convolution/numerical_convolution_base.py +++ b/src/easydynamics/convolution/numerical_convolution_base.py @@ -429,7 +429,7 @@ def _check_width_thresholds( """ # Handle ComponentCollection or ModelComponent - components = model.components if isinstance(model, ComponentCollection) else [model] + components = model if isinstance(model, ComponentCollection) else [model] for comp in components: if hasattr(comp, 'width'): diff --git a/src/easydynamics/sample_model/component_collection.py b/src/easydynamics/sample_model/component_collection.py index 9933e964..8f6eb18c 100644 --- a/src/easydynamics/sample_model/component_collection.py +++ b/src/easydynamics/sample_model/component_collection.py @@ -9,7 +9,6 @@ import numpy as np import scipp as sc -from easyscience.io.serializer_base import SerializerBase from easyscience.variable import DescriptorBase from easyscience.variable import Parameter @@ -29,8 +28,8 @@ class ComponentCollection(EasyDynamicsList, EasyDynamicsModelBase): def __init__( self, components: ModelComponent | list[ModelComponent] | None = None, - unit: str | sc.Unit = "meV", - name: str = "ComponentCollection", + unit: str | sc.Unit = 'meV', + name: str = 'ComponentCollection', display_name: str | None = None, unique_name: str | None = None, ) -> None: @@ -61,12 +60,12 @@ def __init__( components = [components] elif not isinstance(components, list): raise TypeError( - f"components must be a ModelComponent or a list of ModelComponent, got {type(components).__name__} instead." # noqa: E501 + f'components must be a ModelComponent or a list of ModelComponent, got {type(components).__name__} instead.' # noqa: E501 ) for comp in components: if not isinstance(comp, ModelComponent): raise TypeError( - f"All items in components must be instances of ModelComponent, got {type(comp).__name__} instead." # noqa: E501 + f'All items in components must be instances of ModelComponent, got {type(comp).__name__} instead.' # noqa: E501 ) EasyDynamicsList.__init__( @@ -115,8 +114,8 @@ def is_empty(self, _value: bool) -> None: Always raised since is_empty is read-only. """ raise AttributeError( - "is_empty is a read-only property that indicates " - "whether the collection has components." + 'is_empty is a read-only property that indicates ' + 'whether the collection has components.' ) def convert_unit(self, unit: str | sc.Unit) -> None: @@ -137,9 +136,7 @@ def convert_unit(self, unit: str | sc.Unit) -> None: """ if not isinstance(unit, (str, sc.Unit)): - raise TypeError( - f"Unit must be a string or sc.Unit, got {type(unit).__name__}" - ) + raise TypeError(f'Unit must be a string or sc.Unit, got {type(unit).__name__}') old_unit = self._unit @@ -201,28 +198,28 @@ def normalize_area(self) -> None: which would prevent normalization. """ if not self: - 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: - if hasattr(component, "area"): + if hasattr(component, 'area'): area_params.append(component.area) total_area += component.area else: warnings.warn( f"Component '{component.name}' does not have an 'area' attribute " - f"and will be skipped in normalization.", + f'and will be skipped in normalization.', UserWarning, stacklevel=2, ) 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 @@ -243,9 +240,7 @@ def get_all_variables(self) -> list[DescriptorBase]: return [var for component in self for var in component.get_all_variables()] - 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. @@ -294,12 +289,10 @@ def evaluate_component( Evaluated values for the specified component. """ if not self: - raise ValueError("No components in the model to evaluate.") + raise ValueError('No components in the model to evaluate.') if not isinstance(name, str): - raise TypeError( - f"Component name must be a string, got {type(name)} instead." - ) + raise TypeError(f'Component name must be a string, got {type(name)} instead.') matches = [comp for comp in self if comp.name == name] if not matches: @@ -355,35 +348,46 @@ def __repr__(self) -> str: str String representation of the ComponentCollection. """ - comp_names = ", ".join(c.name for c in self) or "No components" + comp_names = ', '.join(c.name for c in self) or 'No components' return f"" def to_dict(self) -> dict: return { - "@module": self.__class__.__module__, - "@class": self.__class__.__name__, - "unit": str(self.unit), - "name": self.name, - "display_name": self.display_name, - "components": [c.to_dict() for c in self._data], + '@module': self.__class__.__module__, + '@class': self.__class__.__name__, + 'unit': str(self.unit), + 'name': self.name, + 'display_name': self.display_name, + 'components': [c.to_dict() for c in self._data], } @classmethod def from_dict(cls, obj_dict: dict) -> ComponentCollection: - def deserialise_component(d): - module = importlib.import_module(d["@module"]) - cls = getattr(module, d["@class"]) + def deserialise_component(d: dict) -> ModelComponent: + """ + Deserialise a component from its dictionary representation. + Parameters + ---------- + d : dict + The dictionary representation of the component. + Returns + ------- + ModelComponent + The deserialised component. + """ + module = importlib.import_module(d['@module']) + cls = getattr(module, d['@class']) return cls.from_dict(d) - components = [deserialise_component(c) for c in obj_dict.get("components", [])] + components = [deserialise_component(c) for c in obj_dict.get('components', [])] return cls( components=components, - unit=obj_dict.get("unit", "meV"), - name=obj_dict.get("name", "ComponentCollection"), - display_name=obj_dict.get("display_name"), + unit=obj_dict.get('unit', 'meV'), + name=obj_dict.get('name', 'ComponentCollection'), + display_name=obj_dict.get('display_name'), ) def __copy__(self) -> ComponentCollection: 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 a3d43ab7..0883db57 100644 --- a/src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py +++ b/src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py @@ -200,7 +200,7 @@ def create_component_collections( self, Q: Q_type, component_name: str = 'Brownian diffusion', - component_display_name: str = 'Brownian diffusion', + component_display_name: str | None = None, ) -> list[ComponentCollection]: r""" Create ComponentCollection components for the Brownian translational diffusion model at @@ -212,7 +212,7 @@ def create_component_collections( Scattering vector values. component_name : str, default='Brownian diffusion' Name of the Brownian diffusion component. - component_display_name : str, default='Brownian diffusion' + component_display_name : str | None, default=None Display name of the Brownian diffusion component. Raises @@ -229,12 +229,15 @@ def create_component_collections( """ Q = _validate_and_convert_Q(Q) - if not isinstance(component_display_name, str): - raise TypeError('component_display_name must be a string.') - if not isinstance(component_name, str): raise TypeError('component_name must be a string.') + if component_display_name is None: + component_display_name = component_name + + if not isinstance(component_display_name, str): + raise TypeError('component_display_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. 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 0671e09c..95732680 100644 --- a/src/easydynamics/sample_model/diffusion_model/diffusion_model_base.py +++ b/src/easydynamics/sample_model/diffusion_model/diffusion_model_base.py @@ -16,9 +16,9 @@ class DiffusionModelBase(EasyDynamicsModelBase): def __init__( self, scale: Numeric = 1.0, - unit: str | sc.Unit = "meV", - name: str = "DiffusionModel", - display_name: str | None = "MyDiffusionModel", + unit: str | sc.Unit = 'meV', + name: str = 'DiffusionModel', + display_name: str | None = 'MyDiffusionModel', unique_name: str | None = None, ) -> None: """ @@ -47,23 +47,19 @@ def __init__( """ try: - test = DescriptorNumber(name="test", value=1, unit=unit) - test.convert_unit("meV") + test = DescriptorNumber(name='test', value=1, unit=unit) + test.convert_unit('meV') except Exception as e: raise UnitError( - f"Invalid unit: {unit}. Unit must be a string or scipp Unit and convertible to meV." # noqa: E501 + f'Invalid unit: {unit}. Unit must be a string or scipp Unit and convertible to meV.' # noqa: E501 ) from e if not isinstance(scale, Numeric): - raise TypeError("scale must be a number.") + raise TypeError('scale must be a number.') - scale = Parameter( - name="scale", value=float(scale), fixed=False, min=0.0, unit=unit - ) + scale = Parameter(name='scale', value=float(scale), fixed=False, min=0.0, unit=unit) - super().__init__( - unit=unit, name=name, display_name=display_name, unique_name=unique_name - ) + super().__init__(unit=unit, name=name, display_name=display_name, unique_name=unique_name) self._scale = scale # ------------------------------------------------------------------ @@ -100,10 +96,10 @@ def scale(self, scale: Numeric) -> None: If scale is negative. """ if not isinstance(scale, Numeric): - raise TypeError("scale must be a number.") + raise TypeError('scale must be a number.') if float(scale) < 0: - raise ValueError("scale must be non-negative.") + raise ValueError('scale must be non-negative.') self._scale.value = float(scale) # ------------------------------------------------------------------ @@ -120,7 +116,7 @@ def __repr__(self) -> str: String representation of the DiffusionModel. """ return ( - f"{self.__class__.__name__}(name={self.name}, display_name={self.display_name}, " - f"unit={self.unit}), \n" - f" scale={self.scale})" + f'{self.__class__.__name__}(name={self.name}, display_name={self.display_name}, ' + f'unit={self.unit}), \n' + f' scale={self.scale})' ) diff --git a/src/easydynamics/sample_model/model_base.py b/src/easydynamics/sample_model/model_base.py index 6ca886c0..4895dc6b 100644 --- a/src/easydynamics/sample_model/model_base.py +++ b/src/easydynamics/sample_model/model_base.py @@ -24,9 +24,9 @@ class ModelBase(EasyDynamicsModelBase): 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, ) -> None: @@ -63,8 +63,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() @@ -100,8 +100,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' ) return [collection.evaluate(x) for collection in self._component_collections] @@ -170,8 +170,8 @@ def unit(self, _unit_str: str) -> None: Always raised to indicate that the unit is read-only. """ 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.' ) @property @@ -202,9 +202,7 @@ def components(self, value: ModelComponent | ComponentCollection | None) -> None If value is not a ModelComponent, ComponentCollection, or None. """ 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: @@ -252,8 +250,8 @@ def Q(self, value: Q_type | None) -> None: if len(old_Q) != len(new_Q) or not np.allclose(old_Q, new_Q): raise ValueError( - "New Q values are not similar to the old ones. " - "To change Q values, first run clear_Q()." + 'New Q values are not similar to the old ones. ' + 'To change Q values, first run clear_Q().' ) def clear_Q(self, confirm: bool = False) -> None: @@ -273,7 +271,7 @@ def clear_Q(self, confirm: bool = False) -> None: """ if not confirm: raise ValueError( - "Clearing Q values requires confirmation. Set confirm=True to proceed." + 'Clearing Q values requires confirmation. Set confirm=True to proceed.' ) self._Q = None self._on_Q_change() @@ -302,9 +300,7 @@ def convert_unit(self, unit: str | sc.Unit) -> None: old_unit = self._unit if not isinstance(unit, (str, sc.Unit)): - raise TypeError( - f"Unit must be a string or sc.Unit, got {type(unit).__name__}" - ) + raise TypeError(f'Unit must be a string or sc.Unit, got {type(unit).__name__}') try: for component in self.components: component.convert_unit(unit) @@ -363,13 +359,11 @@ 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 @@ -396,11 +390,11 @@ def get_component_collection(self, Q_index: int) -> ComponentCollection: The ComponentCollection at the. """ if not isinstance(Q_index, int): - raise TypeError(f"Q_index must be an int, got {type(Q_index).__name__}") + 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)}" + 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] @@ -446,6 +440,6 @@ def __repr__(self) -> str: A string representation of the ModelBase. """ 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}' ) diff --git a/src/easydynamics/sample_model/resolution_model.py b/src/easydynamics/sample_model/resolution_model.py index 3d2385e7..4f35cc30 100644 --- a/src/easydynamics/sample_model/resolution_model.py +++ b/src/easydynamics/sample_model/resolution_model.py @@ -67,10 +67,7 @@ def append_component(self, component: ModelComponent | ComponentCollection) -> N TypeError If the component is a DeltaFunction or Polynomial. """ - if isinstance(component, ComponentCollection): - components = component.components - else: - components = (component,) + components = component if isinstance(component, ComponentCollection) else (component,) for comp in components: if isinstance(comp, (DeltaFunction, Polynomial)): diff --git a/src/easydynamics/sample_model/sample_model.py b/src/easydynamics/sample_model/sample_model.py index fdf8eb09..bcb598f7 100644 --- a/src/easydynamics/sample_model/sample_model.py +++ b/src/easydynamics/sample_model/sample_model.py @@ -146,28 +146,28 @@ def append_diffusion_model(self, diffusion_model: DiffusionModelBase) -> None: self._diffusion_models.append(diffusion_model) self._generate_component_collections() - def remove_diffusion_model(self, name: 'str') -> None: + def remove_diffusion_model(self, name: str) -> None: """ - Remove a DiffusionModel from the SampleModel by unique name. + Remove a DiffusionModel from the SampleModel by name. Parameters ---------- - name : 'str' - The unique name of the DiffusionModel to remove. + name : str + The name of the DiffusionModel to remove. Raises ------ ValueError - If no DiffusionModel with the given unique name is found. + If no DiffusionModel with the given name is found. """ for i, dm in enumerate(self._diffusion_models): - if dm.unique_name == name: + if dm.name == name: del self._diffusion_models[i] self._generate_component_collections() return raise ValueError( - f'No DiffusionModel with unique name {name} found. \n' - f'The available unique names are: {[dm.unique_name for dm in self._diffusion_models]}' + f'No DiffusionModel with name {name} found. \n' + f'The available names are: {[dm.name for dm in self._diffusion_models]}' ) def clear_diffusion_models(self) -> None: @@ -508,16 +508,21 @@ def _generate_component_collections(self) -> None: """ super()._generate_component_collections() - if self._Q is None: + if self.Q is None: return # Generate components from diffusion models # and add to component collections for diffusion_model in self._diffusion_models: - diffusion_collections = diffusion_model.create_component_collections(Q=self._Q) + diffusion_collections = diffusion_model.create_component_collections( + Q=self.Q, + component_name=diffusion_model.name, + ) for target, source in zip( - self._component_collections, diffusion_collections, strict=True + self._component_collections, + diffusion_collections, + strict=True, ): - for component in source.components: + for component in source: target.append_component(component) def _on_diffusion_models_change(self) -> None: @@ -540,7 +545,7 @@ def __repr__(self) -> str: return ( f'{self.__class__.__name__}(unique_name={self.unique_name}, unit={self.unit}), ' - f'Q = {self.Q}, ' + f'Q = {self.Q}, \n ' f'components = {self.components}, diffusion_models = {self.diffusion_models}, ' f'temperature = {self.temperature}, ' f'detailed_balance_settings = {self.detailed_balance_settings}' diff --git a/tests/integration/fitting/test_fitting_with_diffusion_model.py b/tests/integration/fitting/test_fitting_with_diffusion_model.py index 84e303d3..f9912c24 100644 --- a/tests/integration/fitting/test_fitting_with_diffusion_model.py +++ b/tests/integration/fitting/test_fitting_with_diffusion_model.py @@ -23,20 +23,20 @@ class TestFittingWithDiffusionModel: def test_fitting_with_diffusion_model(self): # Load the vanadium data - vanadium_experiment = Experiment("Vanadium") + vanadium_experiment = Experiment('Vanadium') file_path = pooch.retrieve( - url="https://github.com/easyscience/dynamics-lib/raw/refs/heads/master/docs/docs/tutorials/data/vanadium_data_example.h5", - known_hash="16cc1b327c303feeb88fb9dda5390dc4880b62396b1793f98c6fef0b27c7b873", + url='https://github.com/easyscience/dynamics-lib/raw/refs/heads/master/docs/docs/tutorials/data/vanadium_data_example.h5', + known_hash='16cc1b327c303feeb88fb9dda5390dc4880b62396b1793f98c6fef0b27c7b873', ) vanadium_experiment.load_hdf5(filename=file_path) - delta_function = DeltaFunction(display_name="DeltaFunction", area=1) + delta_function = DeltaFunction(display_name='DeltaFunction', area=1) sample_model = SampleModel(components=delta_function) resolution_components = ComponentCollection() - res_gauss = Gaussian(width=0.1, area=1, name="Res. Gauss") + res_gauss = Gaussian(width=0.1, area=1, name='Res. Gauss') res_gauss.area.fixed = True resolution_components.append_component(res_gauss) resolution_model = ResolutionModel(components=resolution_components) @@ -49,31 +49,31 @@ def test_fitting_with_diffusion_model(self): ) vanadium_analysis = Analysis( - display_name="Vanadium Full Analysis", + display_name='Vanadium Full Analysis', experiment=vanadium_experiment, sample_model=sample_model, instrument_model=instrument_model, ) fit_result_independent_single_Q = vanadium_analysis.fit( - fit_method="independent", Q_index=5 + fit_method='independent', Q_index=5 ) assert fit_result_independent_single_Q.success assert fit_result_independent_single_Q.chi2 < 75.0 assert fit_result_independent_single_Q.reduced_chi2 < 0.4 - diffusion_experiment = Experiment("Diffusion") + diffusion_experiment = Experiment('Diffusion') file_path = pooch.retrieve( - url="https://github.com/easyscience/dynamics-lib/raw/refs/heads/master/docs/docs/tutorials/data/diffusion_data_example.h5", - known_hash="5fe846b19aacbda8b8b936eb2e5310d025dc56c25b0b353521e7d6b921f229ab", + url='https://github.com/easyscience/dynamics-lib/raw/refs/heads/master/docs/docs/tutorials/data/diffusion_data_example.h5', + known_hash='5fe846b19aacbda8b8b936eb2e5310d025dc56c25b0b353521e7d6b921f229ab', ) diffusion_experiment.load_hdf5(filename=file_path) - delta_function = DeltaFunction(display_name="DeltaFunction", area=0.2) - lorentzian = Lorentzian(display_name="Lorentzian", area=0.5, width=0.3) + delta_function = DeltaFunction(display_name='DeltaFunction', area=0.2) + lorentzian = Lorentzian(display_name='Lorentzian', area=0.5, width=0.3) component_collection = ComponentCollection( components=[delta_function, lorentzian], ) @@ -91,13 +91,13 @@ def test_fitting_with_diffusion_model(self): instrument_model.resolution_model.fix_all_parameters() diffusion_analysis = Analysis( - display_name="Diffusion Analysis", + display_name='Diffusion Analysis', experiment=diffusion_experiment, sample_model=sample_model, instrument_model=instrument_model, ) - fit_result = diffusion_analysis.fit(fit_method="independent") + fit_result = diffusion_analysis.fit(fit_method='independent') assert fit_result[0].success assert fit_result[0].chi2 < 43.0 @@ -107,12 +107,12 @@ def test_fitting_with_diffusion_model(self): # Diffusion model ############### - delta_function = DeltaFunction(display_name="DeltaFunction", area=0.2) + delta_function = DeltaFunction(display_name='DeltaFunction', area=0.2) component_collection = ComponentCollection( components=[delta_function], ) diffusion_model = BrownianTranslationalDiffusion( - display_name="Brownian Translational Diffusion", + display_name='Brownian Translational Diffusion', diffusion_coefficient=2.4e-9, scale=0.5, ) @@ -130,7 +130,7 @@ def test_fitting_with_diffusion_model(self): ) diffusion_model_analysis = Analysis( - display_name="Diffusion Full Analysis", + display_name='Diffusion Full Analysis', experiment=diffusion_experiment, sample_model=sample_model, instrument_model=instrument_model, @@ -138,7 +138,7 @@ def test_fitting_with_diffusion_model(self): diffusion_model_analysis.instrument_model.resolution_model.fix_all_parameters() - fit_result = diffusion_model_analysis.fit(fit_method="simultaneous") + fit_result = diffusion_model_analysis.fit(fit_method='simultaneous') assert fit_result[0].success assert fit_result[0].chi2 < 56.0 diff --git a/tests/unit/easydynamics/analysis/test_analysis.py b/tests/unit/easydynamics/analysis/test_analysis.py index cf9594db..209a6d92 100644 --- a/tests/unit/easydynamics/analysis/test_analysis.py +++ b/tests/unit/easydynamics/analysis/test_analysis.py @@ -433,7 +433,7 @@ def test_parameters_to_dataset_different_units(self, analysis): ) # Convert the unit of a component to eV. - analysis.sample_model.get_component_collection(Q_index=1).components[0].convert_unit('eV') + analysis.sample_model.get_component_collection(Q_index=1)[0].convert_unit('eV') # THEN parameters_dataset = analysis.parameters_to_dataset() @@ -782,14 +782,14 @@ def test_create_components_dataset_raises(self, analysis): def test_create_components_dataset(self, analysis): # WHEN # Add another component so that there are two components - analysis.sample_model.append_component(Gaussian(display_name='Gaussian2', area=0.5)) + analysis.sample_model.append_component(Gaussian(name='Gaussian2', area=0.5)) # THEN components_dataset = analysis._create_components_dataset(add_background=True) # THEN EXPECT assert isinstance(components_dataset, sc.Dataset) - component_names = [comp.display_name for comp in analysis.sample_model.components] + component_names = [comp.name for comp in analysis.sample_model.components] for component_name in component_names: assert component_name in components_dataset assert 'Q' in components_dataset[component_name].dims @@ -800,16 +800,14 @@ def test_create_components_dataset_single_Q(self, analysis_single_Q): # WHEN # Add another component so that there are two components - analysis_single_Q.sample_model.append_component( - Gaussian(display_name='Gaussian2', area=0.5) - ) + analysis_single_Q.sample_model.append_component(Gaussian(name='Gaussian2', area=0.5)) # THEN components_dataset = analysis_single_Q._create_components_dataset(add_background=True) # THEN EXPECT assert isinstance(components_dataset, sc.Dataset) - component_names = [comp.display_name for comp in analysis_single_Q.sample_model.components] + component_names = [comp.name for comp in analysis_single_Q.sample_model.components] for component_name in component_names: assert component_name in components_dataset assert 'Q' in components_dataset[component_name].dims diff --git a/tests/unit/easydynamics/analysis/test_analysis1d.py b/tests/unit/easydynamics/analysis/test_analysis1d.py index 49f08465..8de35cf7 100644 --- a/tests/unit/easydynamics/analysis/test_analysis1d.py +++ b/tests/unit/easydynamics/analysis/test_analysis1d.py @@ -876,7 +876,8 @@ def test_create_components_dataset_single_Q( sample_component.display_name = 'sample_comp' sample_collection = MagicMock() - sample_collection.components = [sample_component] + sample_collection.__iter__.return_value = iter([sample_component]) + sample_collection.__len__.return_value = 1 analysis1d.sample_model.get_component_collection = MagicMock( return_value=sample_collection @@ -887,7 +888,8 @@ def test_create_components_dataset_single_Q( background_component.display_name = 'background_comp' background_collection = MagicMock() - background_collection.components = [background_component] + background_collection.__iter__.return_value = iter([background_component]) + background_collection.__len__.return_value = 1 analysis1d.instrument_model.background_model.get_component_collection = MagicMock( return_value=background_collection diff --git a/tests/unit/easydynamics/analysis/test_analysis_base.py b/tests/unit/easydynamics/analysis/test_analysis_base.py index 079404e4..120e0af0 100644 --- a/tests/unit/easydynamics/analysis/test_analysis_base.py +++ b/tests/unit/easydynamics/analysis/test_analysis_base.py @@ -45,8 +45,8 @@ def analysis_base_with_components(self): experiment = Experiment(data=data_array) - comp1 = Gaussian(area=1, width=2, center=3) - comp2 = Gaussian(area=4, width=5, center=6) + comp1 = Gaussian(name='Gaussian1', area=1, width=2, center=3) + comp2 = Gaussian(name='Gaussian2', area=4, width=5, center=6) sample_model = SampleModel() sample_model.append_component(comp1) sample_model.append_component(comp2) @@ -408,9 +408,7 @@ def test_get_parameters_near_bounds_no_bounds(self, analysis_base_with_component def test_get_parameters_near_bounds_at_bounds(self, analysis_base_with_components): # WHEN - components = analysis_base_with_components.sample_model.get_component_collection( - Q_index=0 - ).components + components = analysis_base_with_components.sample_model.get_component_collection(Q_index=0) components[0].area.min = 1.0 components[1].center.max = 6.0 @@ -425,9 +423,7 @@ def test_get_parameters_near_bounds_at_bounds(self, analysis_base_with_component def test_get_parameters_near_bounds_with_tolerances(self, analysis_base_with_components): # WHEN - components = analysis_base_with_components.sample_model.get_component_collection( - Q_index=0 - ).components + components = analysis_base_with_components.sample_model.get_component_collection(Q_index=0) components[0].area.min = 0.99999 components[1].center.max = 6.00001 @@ -472,9 +468,7 @@ def test_get_parameters_near_bounds_errors( def test_not_finite_parameters(self, analysis_base_with_components): # WHEN - components = analysis_base_with_components.sample_model.get_component_collection( - Q_index=0 - ).components + components = analysis_base_with_components.sample_model.get_component_collection(Q_index=0) components[0].area.value = np.inf components[1].center.value = np.nan diff --git a/tests/unit/easydynamics/convolution/test_convolution.py b/tests/unit/easydynamics/convolution/test_convolution.py index 3716b724..59191f7c 100644 --- a/tests/unit/easydynamics/convolution/test_convolution.py +++ b/tests/unit/easydynamics/convolution/test_convolution.py @@ -26,25 +26,21 @@ class TestConvolution: @pytest.fixture def default_convolution(self): energy = np.linspace(-10, 10, 5001) - sample_components = ComponentCollection(display_name="ComponentCollection") + sample_components = ComponentCollection(display_name='ComponentCollection') sample_components.append_component( - Gaussian(name="Gaussian1", area=2.0, center=0.1, width=0.4) + Gaussian(name='Gaussian1', area=2.0, center=0.1, width=0.4) ) sample_components.append_component( - DampedHarmonicOscillator( - display_name="DHO1", area=2.0, center=1.0, width=0.1 - ) + DampedHarmonicOscillator(name='DHO1', area=2.0, center=1.0, width=0.1) ) - sample_components.append_component( - DeltaFunction(display_name="Delta1", area=2.0, center=0.3) - ) + sample_components.append_component(DeltaFunction(name='Delta1', area=2.0, center=0.3)) - resolution_components = ComponentCollection(display_name="ResolutionModel") + resolution_components = ComponentCollection(name='ResolutionModel') resolution_components.append_component( - Gaussian(name="GaussianRes", area=3.0, center=0.2, width=0.5) + Gaussian(name='GaussianRes', area=3.0, center=0.2, width=0.5) ) return Convolution( @@ -56,11 +52,9 @@ def default_convolution(self): @pytest.fixture def convolution_with_components(self): energy = np.linspace(-10, 10, 5001) - sample_components = Gaussian(name="Gaussian1", area=2.0, center=0.1, width=0.4) + sample_components = Gaussian(name='Gaussian1', area=2.0, center=0.1, width=0.4) - resolution_components = Gaussian( - name="GaussianRes", area=3.0, center=0.2, width=0.5 - ) + resolution_components = Gaussian(name='GaussianRes', area=3.0, center=0.2, width=0.5) return Convolution( energy=energy, @@ -73,48 +67,33 @@ def test_init(self, default_convolution): # WHEN THEN EXPECT assert isinstance(default_convolution, Convolution) assert isinstance(default_convolution.energy, sc.Variable) - assert np.allclose( - default_convolution.energy.values, np.linspace(-10, 10, 5001) - ) + assert np.allclose(default_convolution.energy.values, np.linspace(-10, 10, 5001)) assert isinstance(default_convolution._sample_components, ComponentCollection) - assert isinstance( - default_convolution._resolution_components, ComponentCollection - ) + assert isinstance(default_convolution._resolution_components, ComponentCollection) assert default_convolution.upsample_factor == 5 assert default_convolution.extension_factor == pytest.approx(0.2) assert default_convolution.temperature is None - assert default_convolution.unit == "meV" - assert ( - default_convolution.detailed_balance_settings.normalize_detailed_balance - is True - ) + assert default_convolution.unit == 'meV' + assert default_convolution.detailed_balance_settings.normalize_detailed_balance is True assert isinstance(default_convolution._energy_grid, EnergyGrid) - assert isinstance( - default_convolution._analytical_sample_components, ComponentCollection - ) + assert isinstance(default_convolution._analytical_sample_components, ComponentCollection) assert ( - default_convolution._analytical_sample_components.components[0] - is default_convolution.sample_components.components[0] - ) - assert isinstance( - default_convolution._numerical_sample_components, ComponentCollection + default_convolution._analytical_sample_components[0] + is default_convolution.sample_components[0] ) + assert isinstance(default_convolution._numerical_sample_components, ComponentCollection) assert ( - default_convolution._numerical_sample_components.components[0] - is default_convolution.sample_components.components[1] + default_convolution._numerical_sample_components[0] + is default_convolution.sample_components[1] ) - assert isinstance( - default_convolution._delta_sample_components, ComponentCollection - ) - assert ( - default_convolution._delta_sample_components.components[0] - is default_convolution.sample_components.components[2] - ) + assert isinstance(default_convolution._delta_sample_components, ComponentCollection) assert ( - default_convolution.convolution_settings.convolution_plan_is_valid is True + default_convolution._delta_sample_components[0] + is default_convolution.sample_components[2] ) + assert default_convolution.convolution_settings.convolution_plan_is_valid is True assert default_convolution._reactions_enabled is True def test_init_components(self, convolution_with_components): @@ -122,19 +101,13 @@ def test_init_components(self, convolution_with_components): # WHEN THEN EXPECT assert isinstance(convolution_with_components, Convolution) assert isinstance(convolution_with_components.energy, sc.Variable) - assert np.allclose( - convolution_with_components.energy.values, np.linspace(-10, 10, 5001) - ) - assert isinstance( - convolution_with_components._sample_components, ComponentCollection - ) - assert isinstance( - convolution_with_components._resolution_components, ComponentCollection - ) + assert np.allclose(convolution_with_components.energy.values, np.linspace(-10, 10, 5001)) + assert isinstance(convolution_with_components._sample_components, ComponentCollection) + assert isinstance(convolution_with_components._resolution_components, ComponentCollection) assert convolution_with_components.upsample_factor == 5 assert convolution_with_components.extension_factor == pytest.approx(0.2) assert convolution_with_components.temperature is None - assert convolution_with_components.unit == "meV" + assert convolution_with_components.unit == 'meV' assert ( convolution_with_components.detailed_balance_settings.normalize_detailed_balance is True @@ -146,8 +119,8 @@ def test_init_components(self, convolution_with_components): ComponentCollection, ) assert ( - convolution_with_components._analytical_sample_components.components[0] - is convolution_with_components.sample_components.components[0] + convolution_with_components._analytical_sample_components[0] + is convolution_with_components.sample_components[0] ) assert isinstance( convolution_with_components._numerical_sample_components, @@ -159,10 +132,7 @@ def test_init_components(self, convolution_with_components): convolution_with_components._delta_sample_components, ComponentCollection ) assert convolution_with_components._delta_sample_components.is_empty - assert ( - convolution_with_components.convolution_settings.convolution_plan_is_valid - is True - ) + assert convolution_with_components.convolution_settings.convolution_plan_is_valid is True assert convolution_with_components._reactions_enabled is True def test_convolution_plan_is_built_when_invalid(self, default_convolution): @@ -174,7 +144,7 @@ def test_convolution_plan_is_built_when_invalid(self, default_convolution): conv.convolution_settings.convolution_plan_is_valid = False # THEN EXPECT - with patch.object(conv, "_build_convolution_plan") as build_plan: + with patch.object(conv, '_build_convolution_plan') as build_plan: conv.convolution() build_plan.assert_called_once() @@ -188,7 +158,7 @@ def test_convolution_calls_analytical_convolver(self, default_convolution): # THEN EXPECT with patch.object( - conv._analytical_convolver, "convolution", return_value=np.array([1.0]) + conv._analytical_convolver, 'convolution', return_value=np.array([1.0]) ) as analytical_conv: conv.convolution() analytical_conv.assert_called_once() @@ -203,7 +173,7 @@ def test_convolution_calls_numerical_convolver(self, default_convolution): # THEN EXPECT with patch.object( - conv._numerical_convolver, "convolution", return_value=np.array([1.0]) + conv._numerical_convolver, 'convolution', return_value=np.array([1.0]) ) as numerical_conv: conv.convolution() numerical_conv.assert_called_once() @@ -219,25 +189,23 @@ def test_convolution_calls_convolve_delta_functions(self, default_convolution): # THEN EXPECT with patch.object( conv, - "_convolve_delta_functions", + '_convolve_delta_functions', return_value=np.array([1.0]), ) as delta_eval: conv.convolution() delta_eval.assert_called_once() @pytest.mark.parametrize( - "analytical_component", + 'analytical_component', [True, False], - ids=["with_analytical", "without_analytical"], + ids=['with_analytical', 'without_analytical'], ) @pytest.mark.parametrize( - "numerical_component", + 'numerical_component', [True, False], - ids=["with_numerical", "without_numerical"], - ) - @pytest.mark.parametrize( - "delta_component", [True, False], ids=["with_delta", "without_delta"] + ids=['with_numerical', 'without_numerical'], ) + @pytest.mark.parametrize('delta_component', [True, False], ids=['with_delta', 'without_delta']) def test_convolution_calls_correct_methods( self, default_convolution, @@ -256,13 +224,13 @@ def test_convolution_calls_correct_methods( if analytical_component: sample_components.append_component( - Gaussian(name="Gaussian", area=1.0, center=0.0, width=0.1) + Gaussian(name='Gaussian', area=1.0, center=0.0, width=0.1) ) if numerical_component: sample_components.append_component( DampedHarmonicOscillator( - display_name="DampedHarmonicOscillator", + display_name='DampedHarmonicOscillator', area=1.0, center=1.0, width=0.1, @@ -271,12 +239,10 @@ def test_convolution_calls_correct_methods( if delta_component: sample_components.append_component( - DeltaFunction(display_name="DeltaFunction", area=1.0, center=0.0) + DeltaFunction(display_name='DeltaFunction', area=1.0, center=0.0) ) - conv.sample_components = ( - sample_components # This updates the internal sample models - ) + conv.sample_components = sample_components # This updates the internal sample models conv._build_convolution_plan() # Ensure the plan is built with the new components # THEN @@ -284,21 +250,21 @@ def test_convolution_calls_correct_methods( # component type is not present. if analytical_component: patch_analytical = patch.object( - conv._analytical_convolver, "convolution", return_value=np.array([1.0]) + conv._analytical_convolver, 'convolution', return_value=np.array([1.0]) ) else: patch_analytical = nullcontext() if numerical_component: patch_numerical = patch.object( - conv._numerical_convolver, "convolution", return_value=np.array([1.0]) + conv._numerical_convolver, 'convolution', return_value=np.array([1.0]) ) else: patch_numerical = nullcontext() patch_delta = patch.object( conv, - "_convolve_delta_functions", + '_convolve_delta_functions', return_value=np.array([1.0]), ) @@ -343,7 +309,7 @@ def test_convolve_delta_functions(self, default_convolution): expected_center = 0.3 + 0.2 # Delta center + Resolution center expected_width = 0.5 # Resolution width expected_values = Gaussian( - name="ExpectedGaussian", + name='ExpectedGaussian', area=expected_area, center=expected_center, width=expected_width, @@ -353,10 +319,10 @@ def test_convolve_delta_functions(self, default_convolution): # List of analytic functions analytic_functions: ClassVar[list[object]] = [ - Gaussian(name="G", area=1.0, center=0.0, width=0.1), - Lorentzian(display_name="L", area=1.0, center=0.0, width=0.1), + Gaussian(name='G', area=1.0, center=0.0, width=0.1), + Lorentzian(display_name='L', area=1.0, center=0.0, width=0.1), Voigt( - display_name="V", + display_name='V', area=1.0, center=0.0, gaussian_width=0.1, @@ -366,8 +332,8 @@ def test_convolve_delta_functions(self, default_convolution): # List of non-analytic functions non_analytic_functions: ClassVar[list[object]] = [ - DampedHarmonicOscillator(display_name="DHO", area=1.0, center=1.0, width=0.1), - Polynomial(display_name="P", coefficients=[1.0, 0.0, 0.0]), + DampedHarmonicOscillator(display_name='DHO', area=1.0, center=1.0, width=0.1), + Polynomial(display_name='P', coefficients=[1.0, 0.0, 0.0]), ] all_functions_except_delta: ClassVar[list[object]] = [ @@ -377,14 +343,12 @@ def test_convolve_delta_functions(self, default_convolution): all_functions: ClassVar[list[object]] = [ *all_functions_except_delta, - DeltaFunction(display_name="Delta", area=1.0, center=0.0), + DeltaFunction(display_name='Delta', area=1.0, center=0.0), ] + @pytest.mark.parametrize('function1', all_functions, ids=lambda f: f.__class__.__name__) @pytest.mark.parametrize( - "function1", all_functions, ids=lambda f: f.__class__.__name__ - ) - @pytest.mark.parametrize( - "function2", all_functions_except_delta, ids=lambda f: f.__class__.__name__ + 'function2', all_functions_except_delta, ids=lambda f: f.__class__.__name__ ) def test_check_if_pair_is_analytic(self, default_convolution, function1, function2): """ @@ -407,22 +371,20 @@ def test_check_if_pair_is_analytic(self, default_convolution, function1, functio expected = is_analytic1 and is_analytic2 assert result == expected - def test_check_if_pair_is_analytic_raises_with_delta_in_resolution( - self, default_convolution - ): + def test_check_if_pair_is_analytic_raises_with_delta_in_resolution(self, default_convolution): """ Test that _check_if_pair_is_analytic raises TypeError when resolution component is DeltaFunction. """ # WHEN conv = default_convolution - sample_component = Gaussian(name="G", area=1.0, center=0.0, width=0.1) - resolution_component = DeltaFunction(display_name="Delta", area=1.0, center=0.0) + sample_component = Gaussian(name='G', area=1.0, center=0.0, width=0.1) + resolution_component = DeltaFunction(display_name='Delta', area=1.0, center=0.0) # THEN EXPECT with pytest.raises( TypeError, - match="This is not supported", + match='This is not supported', ): conv._check_if_pair_is_analytic( sample_component=sample_component, @@ -430,18 +392,18 @@ def test_check_if_pair_is_analytic_raises_with_delta_in_resolution( ) @pytest.mark.parametrize( - "sample_component,resolution_component", + 'sample_component,resolution_component', [ ( - "NotAModelComponent", - Gaussian(name="G", area=1.0, center=0.0, width=0.1), + 'NotAModelComponent', + Gaussian(name='G', area=1.0, center=0.0, width=0.1), ), ( - Gaussian(name="G", area=1.0, center=0.0, width=0.1), - "NotAModelComponent", + Gaussian(name='G', area=1.0, center=0.0, width=0.1), + 'NotAModelComponent', ), ], - ids=["invalid_sample_component", "invalid_resolution_component"], + ids=['invalid_sample_component', 'invalid_resolution_component'], ) def test_check_if_pair_is_analytic_raises_with_invalid_types( self, default_convolution, sample_component, resolution_component @@ -456,7 +418,7 @@ def test_check_if_pair_is_analytic_raises_with_invalid_types( # THEN EXPECT with pytest.raises( TypeError, - match="must be a ModelComponent", + match='must be a ModelComponent', ): conv._check_if_pair_is_analytic( sample_component=sample_component, @@ -464,20 +426,18 @@ def test_check_if_pair_is_analytic_raises_with_invalid_types( ) @pytest.mark.parametrize( - "analytical_component", + 'analytical_component', [True, False], - ids=["with_analytical", "without_analytical"], + ids=['with_analytical', 'without_analytical'], ) @pytest.mark.parametrize( - "numerical_component", + 'numerical_component', [True, False], - ids=["with_numerical", "without_numerical"], + ids=['with_numerical', 'without_numerical'], ) + @pytest.mark.parametrize('delta_component', [True, False], ids=['with_delta', 'without_delta']) @pytest.mark.parametrize( - "delta_component", [True, False], ids=["with_delta", "without_delta"] - ) - @pytest.mark.parametrize( - "temperature", [None, 100], ids=["with_temperature", "without_temperature"] + 'temperature', [None, 100], ids=['with_temperature', 'without_temperature'] ) def test_build_convolution_plan( self, @@ -498,13 +458,13 @@ def test_build_convolution_plan( if analytical_component: sample_components.append_component( - Gaussian(name="Gaussian", area=1.0, center=0.0, width=0.1) + Gaussian(name='Gaussian', area=1.0, center=0.0, width=0.1) ) if numerical_component: sample_components.append_component( DampedHarmonicOscillator( - display_name="DampedHarmonicOscillator", + name='DampedHarmonicOscillator', area=1.0, center=1.0, width=0.1, @@ -513,13 +473,11 @@ def test_build_convolution_plan( if delta_component: sample_components.append_component( - DeltaFunction(display_name="DeltaFunction", area=1.0, center=0.0) + DeltaFunction(name='DeltaFunction', area=1.0, center=0.0) ) # THEN - conv.sample_components = ( - sample_components # This updates the internal sample models - ) + conv.sample_components = sample_components # This updates the internal sample models if temperature is not None: conv.temperature = temperature conv._build_convolution_plan() @@ -527,35 +485,26 @@ def test_build_convolution_plan( # EXPECT assert isinstance(conv._analytical_sample_components, ComponentCollection) if analytical_component and not temperature: - assert len(conv._analytical_sample_components.components) == 1 - assert ( - conv._analytical_sample_components.components[0].display_name - == "Gaussian" - ) + assert len(conv._analytical_sample_components) == 1 + assert conv._analytical_sample_components[0].name == 'Gaussian' else: - assert len(conv._analytical_sample_components.components) == 0 + assert len(conv._analytical_sample_components) == 0 assert isinstance(conv._delta_sample_components, ComponentCollection) if delta_component: - assert len(conv._delta_sample_components.components) == 1 - assert ( - conv._delta_sample_components.components[0].display_name - == "DeltaFunction" - ) + assert len(conv._delta_sample_components) == 1 + assert conv._delta_sample_components[0].name == 'DeltaFunction' else: - assert len(conv._delta_sample_components.components) == 0 + assert len(conv._delta_sample_components) == 0 assert isinstance(conv._numerical_sample_components, ComponentCollection) if not temperature: if numerical_component: - assert len(conv._numerical_sample_components.components) == 1 - assert ( - conv._numerical_sample_components.components[0].display_name - == "DampedHarmonicOscillator" - ) + assert len(conv._numerical_sample_components) == 1 + assert conv._numerical_sample_components[0].name == 'DampedHarmonicOscillator' else: - assert len(conv._numerical_sample_components.components) == 0 + assert len(conv._numerical_sample_components) == 0 else: # analytical and numerical components go to numerical when # temperature is set @@ -564,22 +513,19 @@ def test_build_convolution_plan( expected_numerical_count += 1 if analytical_component: expected_numerical_count += 1 - assert ( - len(conv._numerical_sample_components.components) - == expected_numerical_count - ) + assert len(conv._numerical_sample_components) == expected_numerical_count assert conv.convolution_settings.convolution_plan_is_valid is True @pytest.mark.parametrize( - "analytical_component", + 'analytical_component', [True, False], - ids=["with_analytical", "without_analytical"], + ids=['with_analytical', 'without_analytical'], ) @pytest.mark.parametrize( - "numerical_component", + 'numerical_component', [True, False], - ids=["with_numerical", "without_numerical"], + ids=['with_numerical', 'without_numerical'], ) def test_set_convolvers( self, @@ -598,13 +544,13 @@ def test_set_convolvers( if analytical_component: sample_components.append_component( - Gaussian(name="Gaussian", area=1.0, center=0.0, width=0.1) + Gaussian(name='Gaussian', area=1.0, center=0.0, width=0.1) ) if numerical_component: sample_components.append_component( DampedHarmonicOscillator( - display_name="DampedHarmonicOscillator", + display_name='DampedHarmonicOscillator', area=1.0, center=1.0, width=0.1, @@ -643,14 +589,14 @@ def test_setattr_does_not_invalidate_plan_for_non_tracked_attribute( old_delta_id = id(conv._delta_sample_components) # THEN (NOT in _invalidate_plan_on_change) - conv.display_name = "new_name" + conv.display_name = 'new_name' # EXPECT assert conv.convolution_settings.convolution_plan_is_valid is True assert id(conv._analytical_sample_components) == old_plan_id assert id(conv._numerical_sample_components) == old_numerical_id assert id(conv._delta_sample_components) == old_delta_id - assert conv.display_name == "new_name" + assert conv.display_name == 'new_name' def test_setattr_invalidates_plan_for_tracked_attribute( self, diff --git a/tests/unit/easydynamics/convolution/test_convolution_base.py b/tests/unit/easydynamics/convolution/test_convolution_base.py index eceb8d7d..a300890b 100644 --- a/tests/unit/easydynamics/convolution/test_convolution_base.py +++ b/tests/unit/easydynamics/convolution/test_convolution_base.py @@ -51,8 +51,8 @@ def test_init_with_model_component(self): assert np.allclose(convolution_base.energy.values, np.linspace(-10, 10, 100)) assert isinstance(convolution_base.sample_components, ComponentCollection) assert isinstance(convolution_base.resolution_components, ComponentCollection) - assert convolution_base.sample_components.components[0] == sample_component - assert convolution_base.resolution_components.components[0] == resolution_component + assert convolution_base.sample_components[0] == sample_component + assert convolution_base.resolution_components[0] == resolution_component def test_init_energy_numerical_none_offset(self): # WHEN diff --git a/tests/unit/easydynamics/convolution/test_numerical_convolution.py b/tests/unit/easydynamics/convolution/test_numerical_convolution.py index d9deabb8..755381c2 100644 --- a/tests/unit/easydynamics/convolution/test_numerical_convolution.py +++ b/tests/unit/easydynamics/convolution/test_numerical_convolution.py @@ -17,13 +17,13 @@ class TestNumericalConvolution: @pytest.fixture def default_numerical_convolution(self): energy = np.linspace(-10, 10, 5001) - sample_components = ComponentCollection(display_name="ComponentCollection") + sample_components = ComponentCollection(display_name='ComponentCollection') sample_components.append_component( - Gaussian(name="Gaussian1", area=2.0, center=0.1, width=0.4) + Gaussian(name='Gaussian1', area=2.0, center=0.1, width=0.4) ) - resolution_components = ComponentCollection(display_name="ResolutionModel") + resolution_components = ComponentCollection(display_name='ResolutionModel') resolution_components.append_component( - Gaussian(name="GaussianRes", area=3.0, center=0.2, width=0.5) + Gaussian(name='GaussianRes', area=3.0, center=0.2, width=0.5) ) return NumericalConvolution( @@ -40,26 +40,22 @@ def test_init(self, default_numerical_convolution): # WHEN THEN EXPECT assert isinstance(default_numerical_convolution, NumericalConvolution) assert isinstance(default_numerical_convolution.energy, sc.Variable) - assert np.allclose( - default_numerical_convolution.energy.values, np.linspace(-10, 10, 5001) - ) - assert isinstance( - default_numerical_convolution._sample_components, ComponentCollection - ) + assert np.allclose(default_numerical_convolution.energy.values, np.linspace(-10, 10, 5001)) + assert isinstance(default_numerical_convolution._sample_components, ComponentCollection) assert isinstance( default_numerical_convolution._resolution_components, ComponentCollection ) assert default_numerical_convolution.upsample_factor == 5 assert default_numerical_convolution.extension_factor == pytest.approx(0.2) assert default_numerical_convolution.temperature is None - assert default_numerical_convolution.unit == "meV" + assert default_numerical_convolution.unit == 'meV' assert ( default_numerical_convolution.detailed_balance_settings.normalize_detailed_balance is True ) assert isinstance(default_numerical_convolution._energy_grid, EnergyGrid) - @pytest.mark.parametrize("upsample_factor", [None, 5]) + @pytest.mark.parametrize('upsample_factor', [None, 5]) def test_convolution(self, default_numerical_convolution, upsample_factor): """ Test that convolution of two Gaussians produces the @@ -71,15 +67,13 @@ def test_convolution(self, default_numerical_convolution, upsample_factor): result = default_numerical_convolution.convolution() # EXPECT - expected_area = ( - 2.0 * 3.0 - ) # area of sample_components * area of resolution_components + expected_area = 2.0 * 3.0 # area of sample_components * area of resolution_components expected_center = ( 0.1 + 0.2 + 0.4 ) # center of sample_components + center of resolution_components expected_width = np.sqrt(0.4**2 + 0.5**2) # sqrt(width_sample^2 + width_res^2) expected_result = Gaussian( - name="ExpectedConvolution", + name='ExpectedConvolution', area=expected_area, center=expected_center, width=expected_width, @@ -108,12 +102,8 @@ def test_convolution_with_temperature( resolution_vals = default_numerical_convolution._resolution_components.evaluate( default_numerical_convolution.energy.values ) - DBF = detailed_balance_factor( - energy=default_numerical_convolution.energy, temperature=5.0 - ) - expected_result = fftconvolve( - sample_valds * DBF, resolution_vals, mode="same" - ) * ( + DBF = detailed_balance_factor(energy=default_numerical_convolution.energy, temperature=5.0) + expected_result = fftconvolve(sample_valds * DBF, resolution_vals, mode='same') * ( default_numerical_convolution.energy.values[1] - default_numerical_convolution.energy.values[0] ) @@ -121,7 +111,7 @@ def test_convolution_with_temperature( assert np.allclose(result, expected_result, rtol=1e-4) @pytest.mark.parametrize( - "plan_valid, suppress_warnings, use_db, upsample", + 'plan_valid, suppress_warnings, use_db, upsample', [ (True, True, False, None), (False, True, False, None), @@ -131,12 +121,12 @@ def test_convolution_with_temperature( (False, False, True, 10), ], ids=[ - "plan_valid=True, suppress_warnings=True, use_db=False, upsample=None", - "plan_valid=False, suppress_warnings=True, use_db=False, upsample=None", - "plan_valid=True, suppress_warnings=False, use_db=False, upsample=None", - "plan_valid=True, suppress_warnings=False, use_db=True, upsample=None", - "plan_valid=True, suppress_warnings=False, use_db=True, upsample=10", - "plan_valid=False, suppress_warnings=False, use_db=True, upsample=10", + 'plan_valid=True, suppress_warnings=True, use_db=False, upsample=None', + 'plan_valid=False, suppress_warnings=True, use_db=False, upsample=None', + 'plan_valid=True, suppress_warnings=False, use_db=False, upsample=None', + 'plan_valid=True, suppress_warnings=False, use_db=True, upsample=None', + 'plan_valid=True, suppress_warnings=False, use_db=True, upsample=10', + 'plan_valid=False, suppress_warnings=False, use_db=True, upsample=10', ], ) def test_convolution_branches( @@ -176,22 +166,20 @@ def fake_create_energy_grid(): def fake_check_width_thresholds(*args, **kwargs): check_width_calls.append((args, kwargs)) - monkeypatch.setattr(conv, "_create_energy_grid", fake_create_energy_grid) - monkeypatch.setattr( - conv, "_check_width_thresholds", fake_check_width_thresholds - ) + monkeypatch.setattr(conv, '_create_energy_grid', fake_create_energy_grid) + monkeypatch.setattr(conv, '_check_width_thresholds', fake_check_width_thresholds) # --- Simplify numerics --- dense = conv._energy_grid.energy_dense monkeypatch.setattr( conv.sample_components, - "evaluate", + 'evaluate', lambda x: np.ones_like(dense), # noqa: ARG005 ) monkeypatch.setattr( conv.resolution_components, - "evaluate", + 'evaluate', lambda x: np.ones_like(dense), # noqa: ARG005 ) @@ -203,12 +191,12 @@ def fake_db(*args, **kwargs): # noqa: ARG001 return np.ones_like(dense) monkeypatch.setattr( - "easydynamics.convolution.numerical_convolution.detailed_balance_factor", + 'easydynamics.convolution.numerical_convolution.detailed_balance_factor', fake_db, ) monkeypatch.setattr( - "easydynamics.convolution.numerical_convolution.fftconvolve", + 'easydynamics.convolution.numerical_convolution.fftconvolve', lambda a, b, mode: np.ones_like(dense), # noqa: ARG005 ) @@ -219,7 +207,7 @@ def fake_interp(*args, **kwargs): # noqa: ARG001 interp_called = True return np.ones_like(conv.energy.values) - monkeypatch.setattr(np, "interp", fake_interp) + monkeypatch.setattr(np, 'interp', fake_interp) # THEN result = conv.convolution() diff --git a/tests/unit/easydynamics/convolution/test_numerical_convolution_base.py b/tests/unit/easydynamics/convolution/test_numerical_convolution_base.py index ef20e96f..a084b898 100644 --- a/tests/unit/easydynamics/convolution/test_numerical_convolution_base.py +++ b/tests/unit/easydynamics/convolution/test_numerical_convolution_base.py @@ -18,8 +18,8 @@ class TestNumericalConvolutionBase: @pytest.fixture def default_numerical_convolution_base(self): energy = np.linspace(-10, 10, 101) - sample_components = ComponentCollection(display_name="ComponentCollection") - resolution_components = ComponentCollection(display_name="ResolutionModel") + sample_components = ComponentCollection(display_name='ComponentCollection') + resolution_components = ComponentCollection(display_name='ResolutionModel') return NumericalConvolutionBase( energy=energy, @@ -48,7 +48,7 @@ def test_init(self, default_numerical_convolution_base): assert default_numerical_convolution_base.upsample_factor == 5 assert default_numerical_convolution_base.extension_factor == pytest.approx(0.2) assert default_numerical_convolution_base.temperature is None - assert default_numerical_convolution_base.unit == "meV" + assert default_numerical_convolution_base.unit == 'meV' assert ( default_numerical_convolution_base.detailed_balance_settings.normalize_detailed_balance is True @@ -62,17 +62,13 @@ def test_init_with_custom_parameters(self): """ # WHEN energy = np.linspace(-5, 5, 50) - sample_components = ComponentCollection(display_name="ComponentCollection") - resolution_components = ComponentCollection(display_name="ResolutionModel") - resolution_settings = ConvolutionSettings( - upsample_factor=10, extension_factor=0.5 - ) + sample_components = ComponentCollection(display_name='ComponentCollection') + resolution_components = ComponentCollection(display_name='ResolutionModel') + resolution_settings = ConvolutionSettings(upsample_factor=10, extension_factor=0.5) temperature = 300.0 - temperature_unit = "K" - detailed_balance_settings = DetailedBalanceSettings( - normalize_detailed_balance=False - ) - unit = "meV" + temperature_unit = 'K' + detailed_balance_settings = DetailedBalanceSettings(normalize_detailed_balance=False) + unit = 'meV' # THEN numerical_convolution_base = NumericalConvolutionBase( @@ -96,51 +92,46 @@ def test_init_with_custom_parameters(self): numerical_convolution_base.detailed_balance_settings.normalize_detailed_balance is False ) - assert ( - numerical_convolution_base.detailed_balance_settings - is detailed_balance_settings - ) + assert numerical_convolution_base.detailed_balance_settings is detailed_balance_settings assert isinstance(numerical_convolution_base._energy_grid, EnergyGrid) @pytest.mark.parametrize( - "invalid_input, expected_exception, match", + 'invalid_input, expected_exception, match', [ # temperature ( - {"temperature": "invalid_temperature"}, + {'temperature': 'invalid_temperature'}, TypeError, - r"Temperature must be None, a number or a Parameter.", + r'Temperature must be None, a number or a Parameter.', ), # temperature_unit ( - {"temperature_unit": 123}, + {'temperature_unit': 123}, TypeError, - r"Temperature_unit must be a string or sc.Unit.", + r'Temperature_unit must be a string or sc.Unit.', ), # detailed_balance_settings ( - {"detailed_balance_settings": "invalid_settings"}, + {'detailed_balance_settings': 'invalid_settings'}, TypeError, - r"detailed_balance_settings must be a DetailedBalanceSettings instance.", + r'detailed_balance_settings must be a DetailedBalanceSettings instance.', ), ], ids=[ - "temperature_invalid_type", - "temperature_unit_invalid_type", - "detailed_balance_settings_invalid_type", + 'temperature_invalid_type', + 'temperature_unit_invalid_type', + 'detailed_balance_settings_invalid_type', ], ) - def test_init_raises_for_invalid_input( - self, invalid_input, expected_exception, match - ): + def test_init_raises_for_invalid_input(self, invalid_input, expected_exception, match): """ Test that initialization raises appropriate exceptions for invalid input parameters. """ # WHEN energy = np.linspace(-5, 5, 50) - sample_components = ComponentCollection(display_name="ComponentCollection") - resolution_components = ComponentCollection(display_name="ResolutionModel") + sample_components = ComponentCollection(display_name='ComponentCollection') + resolution_components = ComponentCollection(display_name='ResolutionModel') # THEN EXPECT with pytest.raises(expected_exception, match=match): @@ -181,17 +172,17 @@ def test_energy_setter(self, default_numerical_convolution_base): default_numerical_convolution_base._create_energy_grid() # EXPECT - assert default_numerical_convolution_base._energy_grid.energy_dense.shape[ - 0 - ] == round(201 * default_numerical_convolution_base.upsample_factor) + assert default_numerical_convolution_base._energy_grid.energy_dense.shape[0] == round( + 201 * default_numerical_convolution_base.upsample_factor + ) @pytest.mark.parametrize( - "new_upsample_factor, expected_size", + 'new_upsample_factor, expected_size', [ (10, (101 * 10)), (None, 101), ], - ids=["upsample_10", "no_upsampling"], + ids=['upsample_10', 'no_upsampling'], ) def test_upsample_factor_setter( self, @@ -218,19 +209,18 @@ def test_upsample_factor_setter( # EXPECT: correct factor + grid size assert default_numerical_convolution_base.upsample_factor == new_upsample_factor assert ( - default_numerical_convolution_base._energy_grid.energy_dense.shape[0] - == expected_size + default_numerical_convolution_base._energy_grid.energy_dense.shape[0] == expected_size ) @pytest.mark.parametrize( - "invalid_upsample_factor, expected_exception", + 'invalid_upsample_factor, expected_exception', [ (-1, ValueError), # numeric < 1 → ValueError (0, ValueError), # numeric < 1 → ValueError (1.0, ValueError), # numeric = 1 → ValueError - ("invalid", TypeError), # non-numeric → TypeError + ('invalid', TypeError), # non-numeric → TypeError ], - ids=["negative", "zero", "one", "string"], + ids=['negative', 'zero', 'one', 'string'], ) def test_upsample_setter_raises( self, @@ -270,9 +260,7 @@ def test_extension_factor_setter(self, default_numerical_convolution_base): default_numerical_convolution_base._create_energy_grid() # EXPECT - assert ( - default_numerical_convolution_base.extension_factor == new_extension_factor - ) + assert default_numerical_convolution_base.extension_factor == new_extension_factor expected_span = 20 + (0.5 * 20) # original span + extension assert np.isclose( default_numerical_convolution_base._energy_grid.energy_span_dense, @@ -280,12 +268,12 @@ def test_extension_factor_setter(self, default_numerical_convolution_base): ) @pytest.mark.parametrize( - "invalid_extension_factor, expected_exception", + 'invalid_extension_factor, expected_exception', [ (-0.1, ValueError), # negative → ValueError - ("invalid", TypeError), # non-numeric → TypeError + ('invalid', TypeError), # non-numeric → TypeError ], - ids=["negative", "string"], + ids=['negative', 'string'], ) def test_extension_factor_setter_raises( self, @@ -302,18 +290,16 @@ def test_extension_factor_setter_raises( with pytest.raises( expected_exception, ): - default_numerical_convolution_base.extension_factor = ( - invalid_extension_factor - ) + default_numerical_convolution_base.extension_factor = invalid_extension_factor @pytest.mark.parametrize( - "temperature_input, expected_value", + 'temperature_input, expected_value', [ (1, 1.0), (100.0, 100.0), - (Parameter(name="TempParam", value=250.0, unit="K"), 250.0), + (Parameter(name='TempParam', value=250.0, unit='K'), 250.0), ], - ids=["int", "float", "parameter"], + ids=['int', 'float', 'parameter'], ) def test_temperature_setter( self, default_numerical_convolution_base, temperature_input, expected_value @@ -326,7 +312,7 @@ def test_temperature_setter( # THEN EXPECT assert default_numerical_convolution_base.temperature.value == expected_value - assert default_numerical_convolution_base.temperature.unit == "K" + assert default_numerical_convolution_base.temperature.unit == 'K' def test_temperature_setter_none(self, default_numerical_convolution_base): """ @@ -346,7 +332,7 @@ def test_temperature_setter_does_not_replace_parameter( exists does not create a new Parameter. """ # WHEN - temp_param = Parameter(name="TempParam", value=300.0, unit="K") + temp_param = Parameter(name='TempParam', value=300.0, unit='K') default_numerical_convolution_base.temperature = temp_param # THEN @@ -354,21 +340,17 @@ def test_temperature_setter_does_not_replace_parameter( # EXPECT assert default_numerical_convolution_base.temperature is temp_param - assert default_numerical_convolution_base.temperature.value == pytest.approx( - 350.0 - ) + assert default_numerical_convolution_base.temperature.value == pytest.approx(350.0) def test_temperature_setter_raises(self, default_numerical_convolution_base): """ Test that setting an invalid temperature raises TypeError. """ # WHEN THEN EXPECT - with pytest.raises(TypeError, match="Temperature must be"): - default_numerical_convolution_base.temperature = "invalid_temperature" + with pytest.raises(TypeError, match='Temperature must be'): + default_numerical_convolution_base.temperature = 'invalid_temperature' - def test_normalize_detailed_balance_setter( - self, default_numerical_convolution_base - ): + def test_normalize_detailed_balance_setter(self, default_numerical_convolution_base): """ Test setting normalize_detailed_balance to False. """ @@ -383,22 +365,18 @@ def test_normalize_detailed_balance_setter( is False ) - def test_normalize_detailed_balance_setter_raises( - self, default_numerical_convolution_base - ): + def test_normalize_detailed_balance_setter_raises(self, default_numerical_convolution_base): """ Test that setting an invalid normalize_detailed_balance raises TypeError. """ # WHEN THEN EXPECT - with pytest.raises(TypeError, match="normalize_detailed_balance must be"): + with pytest.raises(TypeError, match='normalize_detailed_balance must be'): default_numerical_convolution_base.detailed_balance_settings.normalize_detailed_balance = ( # noqa: E501 - "invalid" + 'invalid' ) - def test_detailed_balance_settings_property( - self, default_numerical_convolution_base - ): + def test_detailed_balance_settings_property(self, default_numerical_convolution_base): # WHEN new_settings = DetailedBalanceSettings( use_detailed_balance=False, normalize_detailed_balance=False @@ -408,21 +386,15 @@ def test_detailed_balance_settings_property( default_numerical_convolution_base.detailed_balance_settings = new_settings # EXPECT - assert ( - default_numerical_convolution_base.detailed_balance_settings is new_settings - ) + assert default_numerical_convolution_base.detailed_balance_settings is new_settings - def test_detailed_balance_settings_setter_invalid( - self, default_numerical_convolution_base - ): + def test_detailed_balance_settings_setter_invalid(self, default_numerical_convolution_base): # WHEN / THEN / EXPECT with pytest.raises( TypeError, - match="detailed_balance_settings must be a DetailedBalanceSettings", + match='detailed_balance_settings must be a DetailedBalanceSettings', ): - default_numerical_convolution_base.detailed_balance_settings = ( - "invalid_settings" - ) + default_numerical_convolution_base.detailed_balance_settings = 'invalid_settings' def test_convolution_settings_setter_valid( self, @@ -441,16 +413,16 @@ def test_convolution_settings_setter_valid( assert new_settings.convolution_plan_is_valid is False @pytest.mark.parametrize( - "value, expected_exception, match", + 'value, expected_exception, match', [ - (None, TypeError, "must be a ConvolutionSettings instance"), - ("settings", TypeError, "must be a ConvolutionSettings instance"), - (123, TypeError, "must be a ConvolutionSettings instance"), + (None, TypeError, 'must be a ConvolutionSettings instance'), + ('settings', TypeError, 'must be a ConvolutionSettings instance'), + (123, TypeError, 'must be a ConvolutionSettings instance'), ], ids=[ - "none", - "string", - "int", + 'none', + 'string', + 'int', ], ) def test_convolution_settings_setter_invalid( @@ -516,11 +488,11 @@ def test_create_energy_grid_upsample_none_non_uniform_raises( default_numerical_convolution_base.upsample_factor = None with pytest.raises( ValueError, - match="Input array `energy` must be uniformly spaced if upsample_factor is not given", + match='Input array `energy` must be uniformly spaced if upsample_factor is not given', ): default_numerical_convolution_base._create_energy_grid() - @pytest.mark.parametrize("num_points", [100, 101], ids=["even", "odd"]) + @pytest.mark.parametrize('num_points', [100, 101], ids=['even', 'odd']) def test_create_energy_grid_upsample_and_extension( self, default_numerical_convolution_base, num_points ): @@ -560,9 +532,7 @@ def test_create_energy_grid_upsample_and_extension( else: assert np.isclose(energy_grid.energy_even_length_offset, 0.0) - def test_create_energy_grid_non_centered_energy( - self, default_numerical_convolution_base - ): + def test_create_energy_grid_non_centered_energy(self, default_numerical_convolution_base): """ Test creating energy grid when input energy is not centered around zero. The centered energy grid should be shifted @@ -599,18 +569,16 @@ def test_check_width_large_threshold(self, default_numerical_convolution_base): too large compared to energy grid span. """ # WHEN - wide_gaussian = Gaussian( - name="ComponentCollection", area=1.0, center=0.0, width=15.0 - ) + wide_gaussian = Gaussian(name='ComponentCollection', area=1.0, center=0.0, width=15.0) # THEN EXPECT with pytest.warns( UserWarning, - match="Increase extension_factor to improve", + match='Increase extension_factor to improve', ): default_numerical_convolution_base._check_width_thresholds( model=wide_gaussian, - model_name="ComponentCollection", + model_name='ComponentCollection', ) def test_check_width_small_threshold(self, default_numerical_convolution_base): @@ -620,17 +588,17 @@ def test_check_width_small_threshold(self, default_numerical_convolution_base): """ # WHEN narrow_gaussian = Gaussian( - name="ComponentCollection", area=1.0, center=0.0, width=0.000001 + name='ComponentCollection', area=1.0, center=0.0, width=0.000001 ) # THEN EXPECT with pytest.warns( UserWarning, - match="Increase upsample_factor to improve", + match='Increase upsample_factor to improve', ): default_numerical_convolution_base._check_width_thresholds( model=narrow_gaussian, - model_name="ComponentCollection", + model_name='ComponentCollection', ) def test_check_width_no_warnings(self, default_numerical_convolution_base): @@ -640,16 +608,14 @@ def test_check_width_no_warnings(self, default_numerical_convolution_base): ComponentCollection components are checked correctly. """ # WHEN - good_gaussian = Gaussian( - name="ComponentCollection", area=1.0, center=0.0, width=1.0 - ) - sample_components = ComponentCollection(display_name="ComponentCollection") + good_gaussian = Gaussian(name='ComponentCollection', area=1.0, center=0.0, width=1.0) + sample_components = ComponentCollection(display_name='ComponentCollection') sample_components.append_component(good_gaussian) # THEN EXPECT default_numerical_convolution_base._check_width_thresholds( model=sample_components, - model_name="ComponentCollection", + model_name='ComponentCollection', ) def test_repr(self, default_numerical_convolution_base): @@ -660,19 +626,19 @@ def test_repr(self, default_numerical_convolution_base): repr_str = repr(default_numerical_convolution_base) # THEN EXPECT - assert "NumericalConvolutionBase(" in repr_str - assert "energy=array of shape" in repr_str - assert "(101," in repr_str # correct shape + assert 'NumericalConvolutionBase(' in repr_str + assert 'energy=array of shape' in repr_str + assert '(101,' in repr_str # correct shape # Sample and resolution models - assert "ComponentCollection" in repr_str - assert "Components: No components" in repr_str - assert "sample_components=" in repr_str - assert "resolution_components=" in repr_str + assert 'ComponentCollection' in repr_str + assert 'Components: No components' in repr_str + assert 'sample_components=' in repr_str + assert 'resolution_components=' in repr_str # Important parameters - assert "unit=meV" in repr_str - assert "upsample_factor=5" in repr_str - assert "extension_factor=0.2" in repr_str - assert "temperature=None" in repr_str - assert "normalize_detailed_balance=True" in repr_str + assert 'unit=meV' in repr_str + assert 'upsample_factor=5' in repr_str + assert 'extension_factor=0.2' in repr_str + assert 'temperature=None' in repr_str + assert 'normalize_detailed_balance=True' in repr_str 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 e93eaca0..4ffd2a5e 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 @@ -12,9 +12,9 @@ BrownianTranslationalDiffusion, ) -hbar_1 = DescriptorNumber("hbar", 1.0) -hbar = DescriptorNumber.from_scipp("hbar", scipp_hbar) -angstrom = DescriptorNumber("angstrom", 1e-10, unit="m") +hbar_1 = DescriptorNumber('hbar', 1.0) +hbar = DescriptorNumber.from_scipp('hbar', scipp_hbar) +angstrom = DescriptorNumber('angstrom', 1e-10, unit='m') class TestBrownianTranslationalDiffusion: @@ -24,82 +24,68 @@ def brownian_diffusion_model(self): def test_init_default(self, brownian_diffusion_model): # WHEN THEN EXPECT - assert brownian_diffusion_model.display_name == "BrownianTranslationalDiffusion" - assert brownian_diffusion_model.unit == "meV" + assert brownian_diffusion_model.display_name == 'BrownianTranslationalDiffusion' + assert brownian_diffusion_model.unit == 'meV' assert brownian_diffusion_model.scale.value == pytest.approx(1.0) - assert brownian_diffusion_model.diffusion_coefficient.value == pytest.approx( - 1.0 - ) + assert brownian_diffusion_model.diffusion_coefficient.value == pytest.approx(1.0) @pytest.mark.parametrize( - "kwargs,expected_exception, expected_message", + 'kwargs,expected_exception, expected_message', [ ( { - "unit": 123, - "scale": 1.0, - "diffusion_coefficient": 1.0, + 'unit': 123, + 'scale': 1.0, + 'diffusion_coefficient': 1.0, }, UnitError, - "Invalid unit", + 'Invalid unit', ), ( { - "unit": 123, - "scale": "invalid", - "diffusion_coefficient": 1.0, + 'unit': 123, + 'scale': 'invalid', + 'diffusion_coefficient': 1.0, }, TypeError, - "scale must be a number", + 'scale must be a number', ), ( { - "unit": 123, - "scale": 1.0, - "diffusion_coefficient": "invalid", + 'unit': 123, + 'scale': 1.0, + 'diffusion_coefficient': 'invalid', }, TypeError, - "diffusion_coefficient must be a number", + 'diffusion_coefficient must be a number', ), ], ) - def test_input_type_validation_raises( - self, kwargs, expected_exception, expected_message - ): + 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 - ) + BrownianTranslationalDiffusion(display_name='BrownianTranslationalDiffusion', **kwargs) def test_diffusion_coefficient_setter(self, brownian_diffusion_model): # WHEN brownian_diffusion_model.diffusion_coefficient = 3.0 # THEN EXPECT - assert brownian_diffusion_model.diffusion_coefficient.value == pytest.approx( - 3.0 - ) + assert brownian_diffusion_model.diffusion_coefficient.value == pytest.approx(3.0) def test_diffusion_coefficient_setter_raises(self, brownian_diffusion_model): # WHEN THEN EXPECT - with pytest.raises(TypeError, match=r"diffusion_coefficient must be a number."): - brownian_diffusion_model.diffusion_coefficient = "invalid" # Invalid type + with pytest.raises(TypeError, match=r'diffusion_coefficient must be a number.'): + brownian_diffusion_model.diffusion_coefficient = 'invalid' # Invalid type - def test_diffusion_coefficient_setter_negative_raises( - self, brownian_diffusion_model - ): + def test_diffusion_coefficient_setter_negative_raises(self, brownian_diffusion_model): # WHEN THEN EXPECT - with pytest.raises( - ValueError, match=r"diffusion_coefficient must be non-negative." - ): - brownian_diffusion_model.diffusion_coefficient = ( - -1.0 - ) # Invalid negative value + with pytest.raises(ValueError, match=r'diffusion_coefficient must be non-negative.'): + brownian_diffusion_model.diffusion_coefficient = -1.0 # Invalid negative value def test_calculate_width_type_error(self, brownian_diffusion_model): # WHEN THEN EXPECT - with pytest.raises(TypeError, match="Q must be "): - brownian_diffusion_model.calculate_width(Q="invalid") # Invalid type + with pytest.raises(TypeError, match='Q must be '): + brownian_diffusion_model.calculate_width(Q='invalid') # Invalid type def test_calculate_width(self, brownian_diffusion_model): # WHEN @@ -113,8 +99,8 @@ def test_calculate_width(self, brownian_diffusion_model): 1 * sc.Unit(brownian_diffusion_model.diffusion_coefficient.unit) * scipp_hbar - / (1 * sc.Unit("Å") ** 2), - "meV", + / (1 * sc.Unit('Å') ** 2), + 'meV', ) expected_widths = 1.0 * unit_conversion_factor.value * (Q_values**2) np.testing.assert_allclose(widths, expected_widths, rtol=1e-5) @@ -132,8 +118,8 @@ def test_calculate_EISF(self, brownian_diffusion_model): def test_calculate_EISF_type_error(self, brownian_diffusion_model): # WHEN THEN EXPECT - with pytest.raises(TypeError, match="Q must be "): - brownian_diffusion_model.calculate_EISF(Q="invalid") # Invalid type + with pytest.raises(TypeError, match='Q must be '): + brownian_diffusion_model.calculate_EISF(Q='invalid') # Invalid type def test_calculate_QISF(self, brownian_diffusion_model): # WHEN @@ -148,29 +134,27 @@ def test_calculate_QISF(self, brownian_diffusion_model): def test_calculate_QISF_type_error(self, brownian_diffusion_model): # WHEN THEN EXPECT - with pytest.raises(TypeError, match="Q must be "): - brownian_diffusion_model.calculate_QISF(Q="invalid") # Invalid type + with pytest.raises(TypeError, match='Q must be '): + brownian_diffusion_model.calculate_QISF(Q='invalid') # Invalid type @pytest.mark.parametrize( - "Q", + 'Q', [ (0.5), ([1.0, 2.0, 3.0]), (np.array([1.0, 2.0, 3.0])), ], ids=[ - "python_scalar", - "python_list", - "numpy_array", + 'python_scalar', + 'python_list', + 'numpy_array', ], ) def test_create_component_collections(self, brownian_diffusion_model, Q): # WHEN # THEN - component_collections = brownian_diffusion_model.create_component_collections( - Q=Q - ) + component_collections = brownian_diffusion_model.create_component_collections(Q=Q) # EXPECT expected_widths = brownian_diffusion_model.calculate_width(Q) @@ -186,7 +170,7 @@ def test_create_component_collections_component_name_must_be_string( self, brownian_diffusion_model ): # WHEN THEN EXPECT - with pytest.raises(TypeError, match=r"component_name must be a string."): + with pytest.raises(TypeError, match=r'component_name must be a string.'): brownian_diffusion_model.create_component_collections( Q=np.array([0.1, 0.2, 0.3]), component_name=123 ) @@ -195,25 +179,19 @@ def test_create_component_collections_component_display_name_must_be_string( self, brownian_diffusion_model ): # WHEN THEN EXPECT - with pytest.raises( - TypeError, match=r"component_display_name must be a string." - ): + with pytest.raises(TypeError, match=r'component_display_name must be a string.'): brownian_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, brownian_diffusion_model): # WHEN THEN EXPECT - with pytest.raises(TypeError, match="Q must be a "): - brownian_diffusion_model.create_component_collections( - Q="invalid" - ) # Invalid type + with pytest.raises(TypeError, match='Q must be a '): + brownian_diffusion_model.create_component_collections(Q='invalid') # Invalid type - def test_create_component_collections_Q_1dimensional_error( - self, brownian_diffusion_model - ): + def test_create_component_collections_Q_1dimensional_error(self, brownian_diffusion_model): # WHEN THEN EXPECT - with pytest.raises(ValueError, match=r"Q must be a 1-dimensional array."): + with pytest.raises(ValueError, match=r'Q must be a 1-dimensional array.'): brownian_diffusion_model.create_component_collections( Q=np.array([[0.1, 0.2], [0.3, 0.4]]) ) # Invalid shape @@ -223,37 +201,35 @@ def test_write_width_dependency_expression(self, brownian_diffusion_model): expression = brownian_diffusion_model._write_width_dependency_expression(0.5) # EXPECT - expected_expression = "hbar * D* 0.5 **2*1/(angstrom**2)" + expected_expression = 'hbar * D* 0.5 **2*1/(angstrom**2)' assert expression == expected_expression def test_write_width_dependency_map_expression(self, brownian_diffusion_model): # WHEN THEN - expression_map = ( - brownian_diffusion_model._write_width_dependency_map_expression() - ) + expression_map = brownian_diffusion_model._write_width_dependency_map_expression() # EXPECT expected_map = { - "D": brownian_diffusion_model.diffusion_coefficient, - "hbar": brownian_diffusion_model._hbar, - "angstrom": brownian_diffusion_model._angstrom, + 'D': brownian_diffusion_model.diffusion_coefficient, + 'hbar': brownian_diffusion_model._hbar, + 'angstrom': brownian_diffusion_model._angstrom, } assert expression_map == expected_map def test_write_width_dependency_expression_raises(self, brownian_diffusion_model): - with pytest.raises(TypeError, match="Q must be a float"): - brownian_diffusion_model._write_width_dependency_expression("invalid") + with pytest.raises(TypeError, match='Q must be a float'): + brownian_diffusion_model._write_width_dependency_expression('invalid') def test_write_area_dependency_expression_raises(self, brownian_diffusion_model): - with pytest.raises(TypeError, match="QISF must be a float"): - brownian_diffusion_model._write_area_dependency_expression("invalid") + with pytest.raises(TypeError, match='QISF must be a float'): + brownian_diffusion_model._write_area_dependency_expression('invalid') def test_repr(self, brownian_diffusion_model): # WHEN THEN repr_str = repr(brownian_diffusion_model) # EXPECT - assert "BrownianTranslationalDiffusion" in repr_str - assert "diffusion_coefficient" in repr_str - assert "scale=" in repr_str + assert 'BrownianTranslationalDiffusion' in repr_str + assert 'diffusion_coefficient' in repr_str + assert 'scale=' in repr_str 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 index 3055b667..d1fbf8ff 100644 --- 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 @@ -12,9 +12,9 @@ JumpTranslationalDiffusion, ) -hbar_1 = DescriptorNumber("hbar", 1.0) -hbar = DescriptorNumber.from_scipp("hbar", scipp_hbar) -angstrom = DescriptorNumber("angstrom", 1e-10, unit="m") +hbar_1 = DescriptorNumber('hbar', 1.0) +hbar = DescriptorNumber.from_scipp('hbar', scipp_hbar) +angstrom = DescriptorNumber('angstrom', 1e-10, unit='m') class TestJumpTranslationalDiffusion: @@ -24,64 +24,60 @@ def jump_diffusion_model(self): 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.display_name == 'JumpTranslationalDiffusion' + assert jump_diffusion_model.unit == 'meV' assert jump_diffusion_model.scale.value == pytest.approx(1.0) assert jump_diffusion_model.diffusion_coefficient.value == pytest.approx(1.0) assert jump_diffusion_model.relaxation_time.value == pytest.approx(1.0) @pytest.mark.parametrize( - "kwargs,expected_exception, expected_message", + 'kwargs,expected_exception, expected_message', [ ( { - "unit": 123, - "scale": 1.0, - "diffusion_coefficient": 1.0, - "relaxation_time": 1.0, + 'unit': 123, + 'scale': 1.0, + 'diffusion_coefficient': 1.0, + 'relaxation_time': 1.0, }, UnitError, - "Invalid unit", + 'Invalid unit', ), ( { - "unit": "meV", - "scale": "invalid", - "diffusion_coefficient": 1.0, - "relaxation_time": 1.0, + 'unit': 'meV', + 'scale': 'invalid', + 'diffusion_coefficient': 1.0, + 'relaxation_time': 1.0, }, TypeError, - "scale must be a number", + 'scale must be a number', ), ( { - "unit": "meV", - "scale": 1.0, - "diffusion_coefficient": "invalid", - "relaxation_time": 1.0, + 'unit': 'meV', + 'scale': 1.0, + 'diffusion_coefficient': 'invalid', + 'relaxation_time': 1.0, }, TypeError, - "diffusion_coefficient must be a number", + 'diffusion_coefficient must be a number', ), ( { - "unit": "meV", - "scale": 1.0, - "diffusion_coefficient": 1.0, - "relaxation_time": "invalid", + 'unit': 'meV', + 'scale': 1.0, + 'diffusion_coefficient': 1.0, + 'relaxation_time': 'invalid', }, TypeError, - "relaxation_time must be a number", + 'relaxation_time must be a number', ), ], ) - def test_input_type_validation_raises( - self, kwargs, expected_exception, expected_message - ): + 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 - ) + JumpTranslationalDiffusion(display_name='JumpTranslationalDiffusion', **kwargs) def test_diffusion_coefficient_setter(self, jump_diffusion_model): # WHEN @@ -92,14 +88,12 @@ def test_diffusion_coefficient_setter(self, jump_diffusion_model): def test_diffusion_coefficient_setter_raises(self, jump_diffusion_model): # WHEN THEN EXPECT - with pytest.raises(TypeError, match=r"diffusion_coefficient must be a number."): - jump_diffusion_model.diffusion_coefficient = "invalid" # Invalid type + with pytest.raises(TypeError, match=r'diffusion_coefficient must be a number.'): + jump_diffusion_model.diffusion_coefficient = 'invalid' # Invalid type def test_diffusion_coefficient_setter_negative_raises(self, jump_diffusion_model): # WHEN THEN EXPECT - with pytest.raises( - ValueError, match=r"diffusion_coefficient must be non-negative." - ): + with pytest.raises(ValueError, match=r'diffusion_coefficient must be non-negative.'): jump_diffusion_model.diffusion_coefficient = -1.0 # Invalid negative value def test_relaxation_time_setter(self, jump_diffusion_model): @@ -111,42 +105,39 @@ def test_relaxation_time_setter(self, jump_diffusion_model): def test_relaxation_time_setter_raises(self, jump_diffusion_model): # WHEN THEN EXPECT - with pytest.raises(TypeError, match=r"relaxation_time must be a number."): - jump_diffusion_model.relaxation_time = "invalid" # Invalid type + with pytest.raises(TypeError, match=r'relaxation_time must be a number.'): + jump_diffusion_model.relaxation_time = 'invalid' # Invalid type def test_relaxation_time_setter_negative_raises(self, jump_diffusion_model): # WHEN THEN EXPECT - with pytest.raises(ValueError, match=r"relaxation_time must be non-negative."): + with pytest.raises(ValueError, match=r'relaxation_time must be non-negative.'): jump_diffusion_model.relaxation_time = -1.0 # Invalid negative value 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 + 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" + 'instead of our Parameters' # WHEN - Q_values = sc.linspace("Q", 0.5, 1.5, num=6, unit="1/angstrom") + 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) + 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") + denominator = denominator.to(unit='1') # EXPECT - expected_widths = ( - scipp_hbar * diffusion_coefficient_sc * (Q_values**2) / (1 + denominator) - ) + expected_widths = scipp_hbar * diffusion_coefficient_sc * (Q_values**2) / (1 + denominator) expected_widths = expected_widths.to(unit=jump_diffusion_model.unit) @@ -165,8 +156,8 @@ def test_calculate_EISF(self, jump_diffusion_model): 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 + 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 @@ -181,20 +172,20 @@ def test_calculate_QISF(self, jump_diffusion_model): 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 + with pytest.raises(TypeError, match='Q must be '): + jump_diffusion_model.calculate_QISF(Q='invalid') # Invalid type @pytest.mark.parametrize( - "Q", + 'Q', [ (0.5), ([1.0, 2.0, 3.0]), (np.array([1.0, 2.0, 3.0])), ], ids=[ - "python_scalar", - "python_list", - "numpy_array", + 'python_scalar', + 'python_list', + 'numpy_array', ], ) def test_create_component_collections(self, jump_diffusion_model, Q): @@ -217,7 +208,7 @@ def test_create_component_collections_component_name_must_be_string( self, jump_diffusion_model ): # WHEN THEN EXPECT - with pytest.raises(TypeError, match=r"component_name must be a string."): + with pytest.raises(TypeError, match=r'component_name must be a string.'): jump_diffusion_model.create_component_collections( Q=np.array([0.1, 0.2, 0.3]), component_name=123 ) @@ -226,25 +217,19 @@ def test_create_component_collections_component_display_name_must_be_string( self, jump_diffusion_model ): # WHEN THEN EXPECT - with pytest.raises( - TypeError, match=r"component_display_name must be a string." - ): + with pytest.raises(TypeError, match=r'component_display_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 + 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 - ): + def test_create_component_collections_Q_1dimensional_error(self, jump_diffusion_model): # WHEN THEN EXPECT - with pytest.raises(ValueError, match=r"Q must be a 1-dimensional array."): + with pytest.raises(ValueError, match=r'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 @@ -255,7 +240,7 @@ def test_write_width_dependency_expression(self, jump_diffusion_model): # EXPECT expected_expression = ( - "hbar * D* 0.5 **2/(angstrom**2)/(1 + (D * t* 0.5 **2/(angstrom**2)))" + 'hbar * D* 0.5 **2/(angstrom**2)/(1 + (D * t* 0.5 **2/(angstrom**2)))' ) assert expression == expected_expression @@ -265,27 +250,27 @@ def test_write_width_dependency_map_expression(self, jump_diffusion_model): # 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, + '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") + 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") + 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 + 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_background_model.py b/tests/unit/easydynamics/sample_model/test_background_model.py index ceebda88..a698a1bf 100644 --- a/tests/unit/easydynamics/sample_model/test_background_model.py +++ b/tests/unit/easydynamics/sample_model/test_background_model.py @@ -14,26 +14,26 @@ class TestBackgroundModel: @pytest.fixture def background_model(self): component1 = Gaussian( - name="TestGaussian1", + 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) return BackgroundModel( - display_name="InitModel", + display_name='InitModel', components=component_collection, - unit="meV", + unit='meV', Q=np.array([1.0, 2.0, 3.0]), ) @@ -42,32 +42,30 @@ def test_init(self, background_model): model = background_model # 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])) @pytest.mark.parametrize( - "invalid_component, expected_error_msg", + 'invalid_component, expected_error_msg', [ - ("invalid_component", "must be "), - (123, "must be "), - (45.6, "must be "), + ('invalid_component', 'must be '), + (123, 'must be '), + (45.6, 'must be '), ( - [Gaussian(), "invalid_in_list"], - "must be ", + [Gaussian(), 'invalid_in_list'], + 'must be ', ), ], ids=[ - "string", - "int", - "float", - "list_with_invalid", + 'string', + 'int', + 'float', + 'list_with_invalid', ], ) - def test_init_raises_with_invalid_components( - self, invalid_component, expected_error_msg - ): + def test_init_raises_with_invalid_components(self, invalid_component, expected_error_msg): # WHEN / THEN / EXPECT with pytest.raises( TypeError, diff --git a/tests/unit/easydynamics/sample_model/test_component_collection.py b/tests/unit/easydynamics/sample_model/test_component_collection.py index e96b0752..c806f061 100644 --- a/tests/unit/easydynamics/sample_model/test_component_collection.py +++ b/tests/unit/easydynamics/sample_model/test_component_collection.py @@ -17,22 +17,22 @@ class TestComponentCollection: @pytest.fixture def component_collection(self): - model = ComponentCollection(display_name="TestComponentCollection") + model = ComponentCollection(display_name='TestComponentCollection') component1 = Gaussian( - name="TestGaussian1Name", - display_name="TestGaussian1", + name='TestGaussian1Name', + display_name='TestGaussian1', area=1.0, center=0.0, width=1.0, - unit="meV", + unit='meV', ) component2 = Lorentzian( - name="TestLorentzian1Name", - display_name="TestLorentzian1", + name='TestLorentzian1Name', + display_name='TestLorentzian1', area=2.0, center=1.0, width=0.5, - unit="meV", + unit='meV', ) model.append_component(component1) model.append_component(component2) @@ -40,26 +40,24 @@ def component_collection(self): def test_init(self): # WHEN THEN - component_collection = ComponentCollection(display_name="InitModel") + component_collection = ComponentCollection(display_name='InitModel') # EXPECT - assert component_collection.display_name == "InitModel" + assert component_collection.display_name == 'InitModel' assert not component_collection def test_init_with_components(self): # WHEN THEN - component1 = Gaussian( - name="TestGaussian1", area=1.0, center=0.0, width=1.0, unit="meV" - ) + component1 = Gaussian(name='TestGaussian1', area=1.0, center=0.0, width=1.0, unit='meV') component2 = Lorentzian( - display_name="TestLorentzian1", area=2.0, center=1.0, width=0.5, unit="meV" + display_name='TestLorentzian1', area=2.0, center=1.0, width=0.5, unit='meV' ) component_collection = ComponentCollection( - display_name="InitModel", components=[component1, component2] + display_name='InitModel', components=[component1, component2] ) # EXPECT - assert component_collection.display_name == "InitModel" + assert component_collection.display_name == 'InitModel' assert len(component_collection) == 2 assert component_collection[0] is component1 assert component_collection[1] is component2 @@ -68,30 +66,28 @@ def test_init_with_invalid_components_raises(self): # WHEN THEN EXPECT with pytest.raises( TypeError, - match="All items in components must be instances of ModelComponent", + match='All items in components must be instances of ModelComponent', ): - ComponentCollection(components=["NotAComponent"]) + ComponentCollection(components=['NotAComponent']) def test_init_with_invalid_list_of_components_raises(self): # WHEN THEN EXPECT with pytest.raises( TypeError, - match="components must be a ModelComponent or a list of ModelComponent", + match='components must be a ModelComponent or a list of ModelComponent', ): - ComponentCollection(components="NotAList") + ComponentCollection(components='NotAList') def test_init_with_invalid_unit_raises(self): # WHEN THEN EXPECT - with pytest.raises(TypeError, match="unit must be"): + with pytest.raises(TypeError, match='unit must be'): ComponentCollection(unit=123) # ───── Component Management ───── def test_append_component(self, component_collection): # WHEN - component = Gaussian( - name="TestComponent", area=1.0, center=0.0, width=1.0, unit="meV" - ) + component = Gaussian(name='TestComponent', area=1.0, center=0.0, width=1.0, unit='meV') # THEN component_collection.append_component(component) # EXPECT @@ -99,9 +95,7 @@ def test_append_component(self, component_collection): def test_append_component_collection(self, component_collection): # WHEN - component = Gaussian( - name="TestComponent", area=1.0, center=0.0, width=1.0, unit="meV" - ) + component = Gaussian(name='TestComponent', area=1.0, center=0.0, width=1.0, unit='meV') component_collection2 = ComponentCollection() component_collection2.append_component(component) # THEN @@ -113,19 +107,17 @@ def test_append_existing_component_raises(self, component_collection): # WHEN THEN component = component_collection[0] # EXPECT - with pytest.raises(ValueError, match="already exists in list"): + with pytest.raises(ValueError, match='already exists in list'): component_collection.append_component(component) def test_append_invalid_component_raises(self, component_collection): # WHEN THEN EXPECT - with pytest.raises(TypeError, match="Value must be an instance of type"): - component_collection.append_component("NotAComponent") + with pytest.raises(TypeError, match='Value must be an instance of type'): + component_collection.append_component('NotAComponent') def test_getitem(self, component_collection): # WHEN - component = Gaussian( - name="TestComponent", area=1.0, center=0.0, width=1.0, unit="meV" - ) + component = Gaussian(name='TestComponent', area=1.0, center=0.0, width=1.0, unit='meV') # THEN component_collection.append_component(component) # EXPECT @@ -133,21 +125,19 @@ def test_getitem(self, component_collection): def test_is_empty(self): # WHEN THEN - component_collection = ComponentCollection(display_name="EmptyModel") + component_collection = ComponentCollection(display_name='EmptyModel') # EXPECT assert component_collection.is_empty is True # WHEN THEN - component = Gaussian( - name="TestComponent", area=1.0, center=0.0, width=1.0, unit="meV" - ) + component = Gaussian(name='TestComponent', area=1.0, center=0.0, width=1.0, unit='meV') component_collection.append_component(component) # EXPECT assert component_collection.is_empty is False def test_is_empty_setter(self, component_collection): # WHEN THEN EXPECT - with pytest.raises(AttributeError, match=r"is_empty is a read-only property."): + with pytest.raises(AttributeError, match=r'is_empty is a read-only property.'): component_collection.is_empty = True def test_list_component_names(self, component_collection): @@ -155,19 +145,19 @@ def test_list_component_names(self, component_collection): components = component_collection.list_component_names() # EXPECT assert len(components) == 2 - assert components[0] == "TestGaussian1Name" - assert components[1] == "TestLorentzian1Name" + assert components[0] == 'TestGaussian1Name' + assert components[1] == 'TestLorentzian1Name' def test_convert_unit(self, component_collection): # WHEN THEN - component_collection.convert_unit("eV") + component_collection.convert_unit('eV') # EXPECT for component in component_collection: - assert component.unit == "eV" + assert component.unit == 'eV' def test_convert_unit_incorrect_unit_raises(self, component_collection): # WHEN THEN EXPECT - with pytest.raises(TypeError, match=r"Unit must be a string or sc.Unit"): + with pytest.raises(TypeError, match=r'Unit must be a string or sc.Unit'): component_collection.convert_unit(123) def test_convert_unit_failure_rolls_back(self, component_collection): @@ -175,20 +165,18 @@ def test_convert_unit_failure_rolls_back(self, component_collection): # Introduce a faulty component that will fail conversion class FaultyComponent(Gaussian): def convert_unit(self, _unit: str) -> None: - raise RuntimeError("Conversion failed.") + raise RuntimeError('Conversion failed.') faulty_component = FaultyComponent( - name="FaultyComponent", area=1.0, center=0.0, width=1.0, unit="meV" + name='FaultyComponent', area=1.0, center=0.0, width=1.0, unit='meV' ) component_collection.append_component(faulty_component) - original_units = { - component.name: component.unit for component in component_collection - } + original_units = {component.name: component.unit for component in component_collection} # EXPECT - with pytest.raises(RuntimeError, match=r"Conversion failed."): - component_collection.convert_unit("eV") + with pytest.raises(RuntimeError, match=r'Conversion failed.'): + component_collection.convert_unit('eV') # Check that all components have their original units for component in component_collection: @@ -198,23 +186,21 @@ def test_set_unit(self, component_collection): # WHEN THEN EXPECT with pytest.raises( AttributeError, - match=r"Unit is read-only. Use convert_unit to change the unit", + match=r'Unit is read-only. Use convert_unit to change the unit', ): - component_collection.unit = "eV" + component_collection.unit = 'eV' def test_evaluate(self, component_collection): # WHEN x = np.linspace(-5, 5, 100) result = component_collection.evaluate(x) # EXPECT - expected_result = component_collection[0].evaluate(x) + component_collection[ - 1 - ].evaluate(x) + expected_result = component_collection[0].evaluate(x) + component_collection[1].evaluate(x) np.testing.assert_allclose(result, expected_result, rtol=1e-5) def test_evaluate_no_components_returns_zero(self): # WHEN THEN - component_collection = ComponentCollection(display_name="EmptyModel") + component_collection = ComponentCollection(display_name='EmptyModel') x = np.linspace(-5, 5, 100) # EXPECT result = component_collection.evaluate(x) @@ -224,8 +210,8 @@ def test_evaluate_no_components_returns_zero(self): def test_evaluate_component(self, component_collection): # WHEN THEN x = np.linspace(-5, 5, 100) - result1 = component_collection.evaluate_component(x, "TestGaussian1Name") - result2 = component_collection.evaluate_component(x, "TestLorentzian1Name") + result1 = component_collection.evaluate_component(x, 'TestGaussian1Name') + result2 = component_collection.evaluate_component(x, 'TestLorentzian1Name') # EXPECT expected_result1 = component_collection[0].evaluate(x) @@ -238,20 +224,16 @@ def test_evaluate_nonexistent_component_raises(self, component_collection): x = np.linspace(-5, 5, 100) # THEN EXPECT - with pytest.raises( - KeyError, match="No component named 'NonExistentComponent' exists" - ): - component_collection.evaluate_component(x, "NonExistentComponent") + with pytest.raises(KeyError, match="No component named 'NonExistentComponent' exists"): + component_collection.evaluate_component(x, 'NonExistentComponent') def test_evaluate_component_no_components_raises(self): # WHEN THEN - component_collection = ComponentCollection(display_name="EmptyModel") + component_collection = ComponentCollection(display_name='EmptyModel') x = np.linspace(-5, 5, 100) # EXPECT - with pytest.raises( - ValueError, match=r"No components in the model to evaluate." - ): - component_collection.evaluate_component(x, "AnyComponent") + with pytest.raises(ValueError, match=r'No components in the model to evaluate.'): + component_collection.evaluate_component(x, 'AnyComponent') def test_evaluate_component_invalid_name_type_raises(self, component_collection): # WHEN @@ -277,34 +259,28 @@ def test_normalize_area(self, component_collection): def test_normalize_area_no_components_raises(self): # WHEN THEN - component_collection = ComponentCollection(display_name="EmptyModel") + component_collection = ComponentCollection(display_name='EmptyModel') # EXPECT - with pytest.raises( - ValueError, match=r"No components in the model to normalize." - ): + with pytest.raises(ValueError, match=r'No components in the model to normalize.'): component_collection.normalize_area() @pytest.mark.parametrize( - "area_value", + 'area_value', [np.nan, 0.0, np.inf], - ids=["NaN area", "Zero area", "Infinite area"], + ids=['NaN area', 'Zero area', 'Infinite area'], ) - def test_normalize_area_not_finite_area_raises( - self, component_collection, area_value - ): + def test_normalize_area_not_finite_area_raises(self, component_collection, area_value): # WHEN THEN component_collection[0].area = area_value component_collection[1].area = area_value # EXPECT - with pytest.raises(ValueError, match=r"cannot normalize"): + with pytest.raises(ValueError, match=r'cannot normalize'): component_collection.normalize_area() def test_normalize_area_non_area_component_warns(self, component_collection): # WHEN - component1 = Polynomial( - display_name="TestPolynomial", coefficients=[1, 2, 3], unit="meV" - ) + component1 = Polynomial(display_name='TestPolynomial', coefficients=[1, 2, 3], unit='meV') component_collection.append_component(component1) # THEN EXPECT @@ -318,19 +294,19 @@ def test_get_all_parameters(self, component_collection): assert len(parameters) == 6 expected_names = { - "TestGaussian1Name area", - "TestGaussian1Name center", - "TestGaussian1Name width", - "TestLorentzian1Name area", - "TestLorentzian1Name center", - "TestLorentzian1Name width", + 'TestGaussian1Name area', + 'TestGaussian1Name center', + 'TestGaussian1Name width', + 'TestLorentzian1Name area', + 'TestLorentzian1Name center', + 'TestLorentzian1Name width', } actual_names = {param.name for param in parameters} assert actual_names == expected_names assert all(isinstance(param, Parameter) for param in parameters) def test_get_parameters_no_components(self): - component_collection = ComponentCollection(display_name="EmptyModel") + component_collection = ComponentCollection(display_name='EmptyModel') # WHEN THEN parameters = component_collection.get_all_parameters() # EXPECT @@ -342,8 +318,8 @@ def test_get_fit_parameters(self, component_collection): # Fix one parameter and make another dependent component_collection[0].area.fixed = True component_collection[1].width.make_dependent_on( - "comp1_width", - {"comp1_width": component_collection[0].width}, + 'comp1_width', + {'comp1_width': component_collection[0].width}, ) # THEN @@ -353,10 +329,10 @@ def test_get_fit_parameters(self, component_collection): assert len(fit_parameters) == 4 expected_names = { - "TestGaussian1Name center", - "TestGaussian1Name width", - "TestLorentzian1Name area", - "TestLorentzian1Name center", + 'TestGaussian1Name center', + 'TestGaussian1Name width', + 'TestLorentzian1Name area', + 'TestLorentzian1Name center', } actual_names = {param.name for param in fit_parameters} assert actual_names == expected_names @@ -378,9 +354,9 @@ def test_fix_and_free_all_parameters(self, component_collection): assert param.fixed is False def test_contains(self, component_collection): - assert "TestGaussian1Name" in component_collection - assert "TestLorentzian1Name" in component_collection - assert "NonExistentComponent" not in component_collection + assert 'TestGaussian1Name' in component_collection + assert 'TestLorentzian1Name' in component_collection + assert 'NonExistentComponent' not in component_collection gaussian_component = component_collection[0] lorentzian_component = component_collection[1] @@ -388,9 +364,7 @@ def test_contains(self, component_collection): assert lorentzian_component in component_collection # WHEN THEN - fake_component = Gaussian( - name="FakeGaussian", area=1.0, center=0.0, width=1.0, unit="meV" - ) + fake_component = Gaussian(name='FakeGaussian', area=1.0, center=0.0, width=1.0, unit='meV') # EXPECT assert fake_component not in component_collection assert 123 not in component_collection # Invalid type @@ -399,24 +373,22 @@ def test_repr_contains_name_and_components(self, component_collection): # WHEN THEN rep = repr(component_collection) # EXPECT - assert "ComponentCollection" in rep - assert "TestGaussian1Name" in rep + assert 'ComponentCollection' in rep + assert 'TestGaussian1Name' in rep def test_to_dict(self, component_collection): # WHEN model_dict = component_collection.to_dict() # EXPECT - assert model_dict["display_name"] == component_collection.display_name - assert model_dict["unit"] == component_collection.unit - assert len(model_dict["components"]) == len(component_collection) + assert model_dict['display_name'] == component_collection.display_name + assert model_dict['unit'] == component_collection.unit + assert len(model_dict['components']) == len(component_collection) - for comp, comp_dict in zip( - component_collection, model_dict["components"], strict=True - ): - assert comp_dict["@class"] == type(comp).__name__ - assert comp_dict["display_name"] == comp.display_name - assert comp_dict["unit"] == comp.unit + for comp, comp_dict in zip(component_collection, model_dict['components'], strict=True): + assert comp_dict['@class'] == type(comp).__name__ + assert comp_dict['display_name'] == comp.display_name + assert comp_dict['unit'] == comp.unit def test_from_dict(self, component_collection): # WHEN @@ -462,9 +434,7 @@ def test_copy(self, component_collection): assert len(model_copy) == len(component_collection) # EXPECT: deep copy, same order - for orig_comp, copied_comp in zip( - component_collection, model_copy, strict=True - ): + for orig_comp, copied_comp in zip(component_collection, model_copy, strict=True): # New object assert copied_comp is not orig_comp diff --git a/tests/unit/easydynamics/sample_model/test_model_base.py b/tests/unit/easydynamics/sample_model/test_model_base.py index 322d8f01..7f568c5b 100644 --- a/tests/unit/easydynamics/sample_model/test_model_base.py +++ b/tests/unit/easydynamics/sample_model/test_model_base.py @@ -18,28 +18,28 @@ class TestModelBase: @pytest.fixture def model_base(self): component1 = Gaussian( - name="TestGaussian1Name", - display_name="TestGaussian1", + name='TestGaussian1Name', + display_name='TestGaussian1', area=1.0, center=0.0, width=1.0, - unit="meV", + unit='meV', ) component2 = Lorentzian( - name="TestLorentzian1Name", - display_name="TestLorentzian1", + name='TestLorentzian1Name', + 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) return ModelBase( - display_name="InitModel", + display_name='InitModel', components=component_collection, - unit="meV", + unit='meV', Q=np.array([1.0, 2.0, 3.0]), ) @@ -48,8 +48,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])) @@ -57,9 +57,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 @@ -90,7 +90,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): @@ -103,9 +103,9 @@ def test_generate_component_collections_with_Q(self, model_base): assert isinstance(collection, ComponentCollection) assert len(collection) == 2 assert isinstance(collection[0], Gaussian) - assert collection[0].display_name == "TestGaussian1" + assert collection[0].display_name == 'TestGaussian1' assert isinstance(collection[1], Lorentzian) - assert collection[1].display_name == "TestLorentzian1" + assert collection[1].display_name == 'TestLorentzian1' def test_fix_free_all_parameters(self, model_base): # WHEN @@ -128,12 +128,12 @@ def test_get_all_variables(self, model_base): # THEN expected_var_display_names = { - "TestGaussian1Name area", - "TestGaussian1Name center", - "TestGaussian1Name width", - "TestLorentzian1Name area", - "TestLorentzian1Name center", - "TestLorentzian1Name width", + 'TestGaussian1Name area', + 'TestGaussian1Name center', + 'TestGaussian1Name width', + 'TestLorentzian1Name area', + 'TestLorentzian1Name center', + 'TestLorentzian1Name width', } retrieved_var_display_names = {var.display_name for var in all_vars} @@ -147,12 +147,12 @@ def test_get_all_variables_with_Q_index(self, model_base): # THEN expected_var_display_names = { - "TestGaussian1Name area", - "TestGaussian1Name center", - "TestGaussian1Name width", - "TestLorentzian1Name area", - "TestLorentzian1Name center", - "TestLorentzian1Name width", + 'TestGaussian1Name area', + 'TestGaussian1Name center', + 'TestGaussian1Name width', + 'TestLorentzian1Name area', + 'TestLorentzian1Name center', + 'TestLorentzian1Name width', } retrieved_var_display_names = {var.display_name for var in all_vars} @@ -164,7 +164,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) @@ -172,9 +172,9 @@ 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_get_component_collection(self, model_base): # WHEN THEN @@ -186,21 +186,21 @@ def test_get_component_collection_invalid_index_type_raises(self, model_base): # WHEN THEN EXPECT with pytest.raises( TypeError, - match="Q_index must be an int, got str", + match='Q_index must be an int, got str', ): - model_base.get_component_collection(Q_index="invalid_index") + model_base.get_component_collection(Q_index='invalid_index') def test_get_component_collection_invalid_index_raises(self, model_base): # WHEN THEN EXPECT with pytest.raises( IndexError, - match="Q_index 5 is out of bounds for ", + match='Q_index 5 is out of bounds for ', ): model_base.get_component_collection(Q_index=5) def test_append_and_remove_and_clear_component(self, model_base): # WHEN - new_component = Gaussian(name="NewGaussian") + new_component = Gaussian(name='NewGaussian') # THEN model_base.append_component(new_component) @@ -210,7 +210,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 @@ -239,43 +239,43 @@ 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 "): - model_base.append_component("invalid_component") + with pytest.raises(TypeError, match=' must be '): + 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(UnitError): - model_base.convert_unit("invalid_unit") + model_base.convert_unit('invalid_unit') def test_convert_unit_incorrect_unit_raises(self, model_base): # WHEN THEN EXPECT - with pytest.raises(TypeError, match=r"Unit must be a string or sc.Unit"): + with pytest.raises(TypeError, match=r'Unit must be a string or sc.Unit'): model_base.convert_unit(123) def test_components_setter(self, model_base): # WHEN - new_component = Lorentzian(name="NewLorentzian") + new_component = Lorentzian(name='NewLorentzian') model_base.components = new_component # THEN / EXPECT @@ -301,23 +301,23 @@ 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_raises_if_Q_is_not_similar(self, model_base): # WHEN / THEN / EXPECT - with pytest.raises(ValueError, match="New Q values are not similar to"): + with pytest.raises(ValueError, match='New Q values are not similar to'): model_base.Q = [10.0, 20.0, 30.0] @pytest.mark.parametrize( - "new_Q", + 'new_Q', [ [1.0, 2.0, 3.0], np.array([1.0, 2.0, 3.0]), - sc.Variable(dims=["Q"], values=[1.0, 2.0, 3.0], unit="1/angstrom"), + sc.Variable(dims=['Q'], values=[1.0, 2.0, 3.0], unit='1/angstrom'), ], - ids=["list", "numpy_array", "scipp_variable"], + ids=['list', 'numpy_array', 'scipp_variable'], ) def test_Q_setter_with_similar_Q(self, model_base, new_Q): # WHEN @@ -361,7 +361,7 @@ def test_clear_Q(self, model_base): def test_clear_Q_raises_without_confirm(self, model_base): # WHEN / THEN / EXPECT - with pytest.raises(ValueError, match="Clearing Q values requires confirmation"): + with pytest.raises(ValueError, match='Clearing Q values requires confirmation'): model_base.clear_Q() def test_normalize_area(self, model_base): @@ -380,7 +380,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/sample_model/test_resolution_model.py b/tests/unit/easydynamics/sample_model/test_resolution_model.py index 418c18a2..9febca15 100644 --- a/tests/unit/easydynamics/sample_model/test_resolution_model.py +++ b/tests/unit/easydynamics/sample_model/test_resolution_model.py @@ -16,26 +16,26 @@ class TestResolutionModel: @pytest.fixture def resolution_model(self): component1 = Gaussian( - name="TestGaussian1", + 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) return ResolutionModel( - display_name="InitModel", + display_name='InitModel', components=component_collection, - unit="meV", + unit='meV', Q=np.array([1.0, 2.0, 3.0]), ) @@ -44,36 +44,34 @@ def test_init(self, resolution_model): model = resolution_model # 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])) @pytest.mark.parametrize( - "invalid_component, expected_error_msg", + 'invalid_component, expected_error_msg', [ - ("invalid_component", "must be "), - (123, "must be "), - (45.6, "must be "), - (DeltaFunction(), "cannot be a DeltaFunction"), - (Polynomial(), "cannot be a Polynomial"), + ('invalid_component', 'must be '), + (123, 'must be '), + (45.6, 'must be '), + (DeltaFunction(), 'cannot be a DeltaFunction'), + (Polynomial(), 'cannot be a Polynomial'), ( - [Gaussian(), "invalid_in_list"], - "must be ", + [Gaussian(), 'invalid_in_list'], + 'must be ', ), ], ids=[ - "string", - "int", - "float", - "DeltaFunction", - "Polynomial", - "list_with_invalid", + 'string', + 'int', + 'float', + 'DeltaFunction', + 'Polynomial', + 'list_with_invalid', ], ) - def test_init_raises_with_invalid_components( - self, invalid_component, expected_error_msg - ): + def test_init_raises_with_invalid_components(self, invalid_component, expected_error_msg): # WHEN / THEN / EXPECT with pytest.raises( TypeError, @@ -91,7 +89,7 @@ def test_init_raises_with_invalid_components( def test_append_and_remove_and_clear_component(self, resolution_model): # WHEN - new_component = Gaussian(unique_name="NewGaussian") + new_component = Gaussian(name='NewGaussian') # THEN resolution_model.append_component(new_component) @@ -101,7 +99,7 @@ def test_append_and_remove_and_clear_component(self, resolution_model): assert resolution_model.components[-1] is new_component # THEN - resolution_model.remove_component("NewGaussian") + resolution_model.remove_component('NewGaussian') # EXPECT assert len(resolution_model.components) == 2 @@ -115,8 +113,8 @@ def test_append_and_remove_and_clear_component(self, resolution_model): def test_append_component_collection(self, resolution_model): # WHEN new_collection = ComponentCollection() - new_component1 = Lorentzian() - new_component2 = Gaussian() + new_component1 = Lorentzian(name='NewLorentzian') + new_component2 = Gaussian(name='NewGaussian') new_collection.append_component(new_component1) new_collection.append_component(new_component2) @@ -129,28 +127,26 @@ def test_append_component_collection(self, resolution_model): assert resolution_model.components[-1] is new_component2 @pytest.mark.parametrize( - "invalid_component", + 'invalid_component', [ DeltaFunction(), Polynomial(), ], - ids=["DeltaFunction", "Polynomial"], + ids=['DeltaFunction', 'Polynomial'], ) - def test_append_invalid_component_type_raises( - self, resolution_model, invalid_component - ): + def test_append_invalid_component_type_raises(self, resolution_model, invalid_component): # WHEN / THEN / EXPECT # appending a single component with pytest.raises( TypeError, - match="cannot be ", + match='cannot be ', ): resolution_model.append_component(invalid_component) # appending a collection with invalid component with pytest.raises( TypeError, - match="cannot be ", + match='cannot be ', ): collection = ComponentCollection() collection.append_component(invalid_component) diff --git a/tests/unit/easydynamics/sample_model/test_sample_model.py b/tests/unit/easydynamics/sample_model/test_sample_model.py index e4124db3..337266f5 100644 --- a/tests/unit/easydynamics/sample_model/test_sample_model.py +++ b/tests/unit/easydynamics/sample_model/test_sample_model.py @@ -22,34 +22,34 @@ class TestSampleModel: @pytest.fixture def sample_model(self): component1 = Gaussian( - name="TestGaussian1Name", - display_name="TestGaussian1Display", + name='TestGaussian1Name', + display_name='TestGaussian1Display', area=1.0, center=0.0, width=1.0, - unit="meV", + unit='meV', ) component2 = Lorentzian( - name="TestLorentzian1Name", - display_name="TestLorentzian1Display", + name='TestLorentzian1Name', + display_name='TestLorentzian1Display', 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) diffusion_model = BrownianTranslationalDiffusion( - display_name="DiffusionModelDisplay", name="DiffusionModelName" + display_name='DiffusionModelDisplay', name='DiffusionModelName' ) return SampleModel( - display_name="InitModel", + display_name='InitModel', components=component_collection, diffusion_models=diffusion_model, - unit="meV", + unit='meV', Q=np.array([1.0, 2.0, 3.0]), temperature=10.0, ) @@ -60,8 +60,8 @@ def test_init(self, sample_model): model = sample_model # EXPECT - assert model.display_name == "InitModel" - assert model.unit == "meV" + assert model.display_name == 'InitModel' + assert model.unit == 'meV' assert len(model.components) == 2 assert isinstance(model.diffusion_models, list) assert len(model.diffusion_models) == 1 @@ -95,42 +95,40 @@ def test_init_custom_input(self): assert sample_model.detailed_balance_settings is detailed_balance_settings @pytest.mark.parametrize( - "invalid_input, expected_exception, match", + 'invalid_input, expected_exception, match', [ # diffusion_models ( - {"diffusion_models": "invalid_diffusion_model"}, + {'diffusion_models': 'invalid_diffusion_model'}, TypeError, - "diffusion_models must be a DiffusionModelBase", + 'diffusion_models must be a DiffusionModelBase', ), # temperature ( - {"temperature": "invalid_temperature"}, + {'temperature': 'invalid_temperature'}, TypeError, - "temperature must be a number or None", + 'temperature must be a number or None', ), ( - {"temperature": -5.0}, + {'temperature': -5.0}, ValueError, - "temperature must be non-negative", + 'temperature must be non-negative', ), # detailed_balance_settings ( - {"detailed_balance_settings": "invalid_settings"}, + {'detailed_balance_settings': 'invalid_settings'}, TypeError, - "detailed_balance_settings must be a DetailedBalanceSettings or None", + 'detailed_balance_settings must be a DetailedBalanceSettings or None', ), ], ids=[ - "diffusion_models_invalid_type", - "temperature_not_numeric", - "temperature_negative", - "detailed_balance_settings_invalid_type", + 'diffusion_models_invalid_type', + 'temperature_not_numeric', + 'temperature_negative', + 'detailed_balance_settings_invalid_type', ], ) - def test_init_raises_for_invalid_input( - self, invalid_input, expected_exception, match - ): + def test_init_raises_for_invalid_input(self, invalid_input, expected_exception, match): """ Test that initialization raises appropriate exceptions for invalid input parameters. @@ -143,7 +141,7 @@ def test_append_and_remove_and_clear_diffusion_model(self, sample_model): # WHEN model = sample_model new_diffusion_model = BrownianTranslationalDiffusion( - unique_name="new_diffusion_model", + name='new_diffusion_model', ) # THEN @@ -154,7 +152,7 @@ def test_append_and_remove_and_clear_diffusion_model(self, sample_model): assert model.diffusion_models[1] is new_diffusion_model # THEN - model.remove_diffusion_model("new_diffusion_model") + model.remove_diffusion_model('new_diffusion_model') # EXPECT assert len(model.diffusion_models) == 1 @@ -168,23 +166,23 @@ def test_append_diffusion_model_raises_with_invalid_type(self, sample_model): # WHEN / THEN / EXPECT with pytest.raises( TypeError, - match="diffusion_model must be a DiffusionModelBase", + match='diffusion_model must be a DiffusionModelBase', ): - sample_model.append_diffusion_model("invalid_diffusion_model") + sample_model.append_diffusion_model('invalid_diffusion_model') def test_remove_diffusion_model_raises_with_invalid_name(self, sample_model): # WHEN / THEN / EXPECT with pytest.raises( ValueError, - match="No DiffusionModel", + match='No DiffusionModel', ): - sample_model.remove_diffusion_model("non_existent_model") + sample_model.remove_diffusion_model('non_existent_model') def test_diffusion_model_setter(self, sample_model): # WHEN model = sample_model - new_diffusion_model1 = BrownianTranslationalDiffusion() - new_diffusion_model2 = BrownianTranslationalDiffusion() + new_diffusion_model1 = BrownianTranslationalDiffusion(name='new_diffusion_model1') + new_diffusion_model2 = BrownianTranslationalDiffusion(name='new_diffusion_model2') # THEN model.diffusion_models = [new_diffusion_model1, new_diffusion_model2] @@ -208,21 +206,22 @@ def test_diffusion_model_setter(self, sample_model): assert model.diffusion_models[0] is new_diffusion_model1 @pytest.mark.parametrize( - "invalid_value", + 'invalid_value', [ - "invalid_diffusion_model", + 'invalid_diffusion_model', 123, - [BrownianTranslationalDiffusion(), "invalid_diffusion_model"], + [ + BrownianTranslationalDiffusion(name='valid_diffusion_model'), + 'invalid_diffusion_model', + ], ], - ids=["string", "integer", "list with invalid type"], + ids=['string', 'integer', 'list with invalid type'], ) - def test_diffusion_model_setter_raises_with_invalid_type( - self, invalid_value, sample_model - ): + def test_diffusion_model_setter_raises_with_invalid_type(self, invalid_value, sample_model): # WHEN / THEN / EXPECT with pytest.raises( TypeError, - match="diffusion_models must be ", + match='diffusion_models must be ', ): sample_model.diffusion_models = invalid_value @@ -249,22 +248,20 @@ def test_temperature_setter(self, sample_model): assert model.temperature.value == pytest.approx(0.0) @pytest.mark.parametrize( - "invalid_value", + 'invalid_value', [ - "invalid_temperature", + 'invalid_temperature', [1, 2, 3], - {"temp": 10}, + {'temp': 10}, -5.0, ], - ids=["string", "list", "dict", "negative"], + ids=['string', 'list', 'dict', 'negative'], ) - def test_temperature_setter_raises_with_invalid_type( - self, invalid_value, sample_model - ): + def test_temperature_setter_raises_with_invalid_type(self, invalid_value, sample_model): # WHEN / THEN / EXPECT with pytest.raises( (TypeError, ValueError), - match=r"temperature must be a number or None|temperature must be non-negative", + match=r'temperature must be a number or None|temperature must be non-negative', ): sample_model.temperature = invalid_value @@ -272,7 +269,7 @@ def test_temperature_unit_setter_raises(self, sample_model): # WHEN / THEN / EXPECT with pytest.raises( AttributeError, - match="Temperature_unit is read-only", + match='Temperature_unit is read-only', ): sample_model.temperature_unit = 123 @@ -281,10 +278,10 @@ def test_convert_temperature_unit(self, sample_model): model = sample_model # THEN - model.convert_temperature_unit("mK") + model.convert_temperature_unit('mK') # EXPECT - assert model.temperature_unit == "mK" + assert model.temperature_unit == 'mK' assert model.temperature.value == 10 * 1000 def test_convert_temperature_unit_raises_with_no_temperature(self, sample_model): @@ -295,9 +292,9 @@ def test_convert_temperature_unit_raises_with_no_temperature(self, sample_model) # THEN / EXPECT with pytest.raises( ValueError, - match="Temperature is not set, cannot convert unit", + match='Temperature is not set, cannot convert unit', ): - model.convert_temperature_unit("mK") + model.convert_temperature_unit('mK') def test_convert_temperature_unit_raises_with_invalid_unit(self, sample_model): # WHEN @@ -306,9 +303,9 @@ def test_convert_temperature_unit_raises_with_invalid_unit(self, sample_model): # THEN / EXPECT with pytest.raises( UnitError, - match="Failed to", + match='Failed to', ): - model.convert_temperature_unit("invalid_unit") + model.convert_temperature_unit('invalid_unit') def test_normalize_detailed_balance_setter(self, sample_model): # WHEN @@ -326,15 +323,13 @@ def test_normalize_detailed_balance_setter(self, sample_model): # EXPECT assert model.normalize_detailed_balance is True - def test_normalize_detailed_balance_setter_raises_with_invalid_type( - self, sample_model - ): + def test_normalize_detailed_balance_setter_raises_with_invalid_type(self, sample_model): # WHEN / THEN / EXPECT with pytest.raises( TypeError, - match="normalize_detailed_balance must be True or False", + match='normalize_detailed_balance must be True or False', ): - sample_model.normalize_detailed_balance = "invalid_value" + sample_model.normalize_detailed_balance = 'invalid_value' def test_use_detailed_balance_setter(self, sample_model): # WHEN @@ -356,9 +351,9 @@ def test_use_detailed_balance_setter_raises_with_invalid_type(self, sample_model # WHEN / THEN / EXPECT with pytest.raises( TypeError, - match="use_detailed_balance must be True or False", + match='use_detailed_balance must be True or False', ): - sample_model.use_detailed_balance = "invalid_value" + sample_model.use_detailed_balance = 'invalid_value' def test_detailed_balance_settings_property(self, sample_model): # WHEN @@ -378,9 +373,9 @@ def test_detailed_balance_settings_setter_invalid(self, sample_model): # WHEN / THEN / EXPECT with pytest.raises( TypeError, - match="detailed_balance_settings must be a DetailedBalanceSettings", + match='detailed_balance_settings must be a DetailedBalanceSettings', ): - sample_model.detailed_balance_settings = "invalid_settings" + sample_model.detailed_balance_settings = 'invalid_settings' def test_evaluate_calls_dbf(self, sample_model): # WHEN @@ -394,9 +389,7 @@ def test_evaluate_calls_dbf(self, sample_model): sample_model._component_collections = [collection1, collection2] - with patch( - "easydynamics.sample_model.sample_model.detailed_balance_factor" - ) as mock_dbf: + with patch('easydynamics.sample_model.sample_model.detailed_balance_factor') as mock_dbf: mock_dbf.return_value = np.array([10.0, 10.0, 10.0]) # simplified DBF # THEN result = sample_model.evaluate(x) @@ -419,14 +412,14 @@ def test_evaluate_calls_dbf(self, sample_model): np.testing.assert_allclose(result[1], np.array([4.0, 5.0, 6.0]) * 10.0) @pytest.mark.parametrize( - "temperature, use_detailed_balance", + 'temperature, use_detailed_balance', [ (None, True), # DB disabled because temperature is None (300.0, False), # DB disabled explicitly ], ids=[ - "temperature_none", - "use_detailed_balance_false", + 'temperature_none', + 'use_detailed_balance_false', ], ) def test_evaluate_doesnt_call_dbf_when_disabled( @@ -446,9 +439,7 @@ def test_evaluate_doesnt_call_dbf_when_disabled( sample_model.temperature = temperature sample_model.use_detailed_balance = use_detailed_balance - with patch( - "easydynamics.sample_model.sample_model.detailed_balance_factor" - ) as mock_dbf: + with patch('easydynamics.sample_model.sample_model.detailed_balance_factor') as mock_dbf: mock_dbf.return_value = np.array([10.0, 10.0, 10.0]) # simplified DBF # THEN result = sample_model.evaluate(x) @@ -473,13 +464,13 @@ def test_generate_component_collections(self, sample_model): assert len(sample_model._component_collections) == 3 # 3 Q values for collection in sample_model._component_collections: assert isinstance(collection, ComponentCollection) - assert len(collection.components) == 3 # 3 components - assert collection.components[0].display_name == "TestGaussian1" - assert collection.components[0].area.value == pytest.approx(1.0) - assert collection.components[1].display_name == "TestLorentzian1" - assert collection.components[1].area.value == pytest.approx(2.0) - assert collection.components[2].display_name == "Brownian diffusion" - assert isinstance(collection.components[2], Lorentzian) + assert len(list(collection)) == 3 # 3 components + assert collection[0].name == 'TestGaussian1Name' + assert collection[0].area.value == pytest.approx(1.0) + assert collection[1].name == 'TestLorentzian1Name' + assert collection[1].area.value == pytest.approx(2.0) + assert collection[2].name == 'DiffusionModelName' + assert isinstance(collection[2], Lorentzian) def test_get_all_variables(self, sample_model): # WHEN @@ -489,9 +480,7 @@ def test_get_all_variables(self, sample_model): # EXPECT # Should include temperature and variables from diffusion model - expected_num_vars = ( - 3 * 3 * 3 - ) # 3 components, each with 3 parameters, across 3 Q values + expected_num_vars = 3 * 3 * 3 # 3 components, each with 3 parameters, across 3 Q values expected_num_vars += 2 # diffusion model has 2 parameters expected_num_vars += 1 # temperature variable @@ -513,10 +502,10 @@ def test_repr(self, sample_model): repr_str = repr(sample_model) # THEN / EXPECT - assert "SampleModel" in repr_str - assert "unit=" in repr_str - assert "Q = " in repr_str - assert "components" in repr_str - assert "diffusion_models" in repr_str - assert "temperature" in repr_str - assert "normalize_detailed_balance" in repr_str + assert 'SampleModel' in repr_str + assert 'unit=' in repr_str + assert 'Q = ' in repr_str + assert 'components' in repr_str + assert 'diffusion_models' in repr_str + assert 'temperature' in repr_str + assert 'normalize_detailed_balance' in repr_str From c0deb852b9cad9cd79f0a829473d8c23c016169f Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Sat, 16 May 2026 17:31:31 +0200 Subject: [PATCH 11/20] Update some tutorials --- docs/docs/tutorials/tutorial0_basics.ipynb | 16 ++-- .../tutorials/tutorial0_more_advanced.ipynb | 6 +- docs/docs/tutorials/tutorial1_brownian.ipynb | 76 +++++++--------- .../base_classes/easydynamics_base.py | 6 +- .../base_classes/easydynamics_list.py | 17 ++-- .../base_classes/easydynamics_modelbase.py | 14 +-- src/easydynamics/base_classes/name_mixin.py | 10 +-- .../sample_model/component_collection.py | 90 ++++++++++++------- 8 files changed, 127 insertions(+), 108 deletions(-) diff --git a/docs/docs/tutorials/tutorial0_basics.ipynb b/docs/docs/tutorials/tutorial0_basics.ipynb index f82781f9..f28f460f 100644 --- a/docs/docs/tutorials/tutorial0_basics.ipynb +++ b/docs/docs/tutorials/tutorial0_basics.ipynb @@ -24,9 +24,6 @@ "\n", "import easydynamics as edyn\n", "import easydynamics.sample_model as sm\n", - "from easydynamics.analysis import Analysis\n", - "from easydynamics.analysis import ParameterAnalysis\n", - "from easydynamics.analysis.parameter_analysis import FitBinding\n", "\n", "# Make the plots interactive\n", "%matplotlib widget" @@ -201,7 +198,7 @@ "metadata": {}, "outputs": [], "source": [ - "analysis = Analysis(\n", + "analysis = edyn.Analysis(\n", " experiment=experiment,\n", " sample_model=model,\n", ")" @@ -382,7 +379,8 @@ "outputs": [], "source": [ "energy = sc.linspace('energy', -3.5, 3.5, num=1001, unit='meV')\n", - "data_and_model = analysis.data_and_model_to_datagroup(energy=energy)" + "data_and_model = analysis.data_and_model_to_datagroup(energy=energy)\n", + "data_and_model" ] }, { @@ -400,11 +398,11 @@ "metadata": {}, "outputs": [], "source": [ - "fit_func = sm.Polynomial(coefficients=[3.7, -0.5], display_name='Straight line')\n", + "fit_func = sm.Polynomial(coefficients=[3.7, -0.5], name='Straight line', display_name='Straight line')\n", "\n", - "binding = FitBinding(parameter_name='Gaussian area', model=fit_func)\n", + "binding = edyn.FitBinding(parameter_name='Gaussian area', model=fit_func)\n", "\n", - "parameter_analysis = ParameterAnalysis(\n", + "parameter_analysis = edyn.ParameterAnalysis(\n", " parameters=analysis,\n", " bindings=[binding],\n", ")" @@ -468,7 +466,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "in16b", "language": "python", "name": "python3" }, diff --git a/docs/docs/tutorials/tutorial0_more_advanced.ipynb b/docs/docs/tutorials/tutorial0_more_advanced.ipynb index 70a54d2d..1852e72c 100644 --- a/docs/docs/tutorials/tutorial0_more_advanced.ipynb +++ b/docs/docs/tutorials/tutorial0_more_advanced.ipynb @@ -311,13 +311,13 @@ "outputs": [], "source": [ "gauss_fit_func = sm.Polynomial(\n", - " coefficients=[3.7, -0.5], unit='1/angstrom', display_name='Gauss area fit'\n", + " coefficients=[3.7, -0.5], unit='1/angstrom', name='Gauss area fit'\n", ")\n", "dho_area_fit_func = sm.Polynomial(\n", - " coefficients=[2.0, 0.12], unit='1/angstrom', display_name='DHO area fit'\n", + " coefficients=[2.0, 0.12], unit='1/angstrom', name='DHO area fit'\n", ")\n", "dho_center_fit_func = sm.Polynomial(\n", - " coefficients=[1.1, 0.2], unit='1/angstrom', display_name='DHO center fit'\n", + " coefficients=[1.1, 0.2], unit='1/angstrom', name='DHO center fit'\n", ")\n", "\n", "binding1 = edyn.FitBinding(parameter_name='Gaussian area', model=gauss_fit_func)\n", diff --git a/docs/docs/tutorials/tutorial1_brownian.ipynb b/docs/docs/tutorials/tutorial1_brownian.ipynb index 9985bc16..3341067f 100644 --- a/docs/docs/tutorials/tutorial1_brownian.ipynb +++ b/docs/docs/tutorials/tutorial1_brownian.ipynb @@ -8,9 +8,7 @@ "# Brownian Diffusion\n", "We here show how to set up an Analysis object and use it to first fit an artificial vanadium measurement to obtain the resolution. Next, we use the fitted resolution to fit an artificial measurement of a model with diffusion and some elastic scattering. \n", "\n", - "We extract and plot the relevant parameters. Finally, we show how to fit directly to the diffusion model.\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." + "We extract and plot the relevant parameters and fit them to a diffusion model. Finally, we show how to fit all the data simultaneously to the diffusion model." ] }, { @@ -23,20 +21,8 @@ "# Imports\n", "import pooch\n", "\n", - "from easydynamics.analysis.analysis import Analysis\n", - "from easydynamics.analysis.parameter_analysis import FitBinding\n", - "from easydynamics.analysis.parameter_analysis import ParameterAnalysis\n", - "from easydynamics.experiment import Experiment\n", - "from easydynamics.sample_model import BrownianTranslationalDiffusion\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 Lorentzian\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", - "from easydynamics.sample_model.sample_model import SampleModel\n", + "import easydynamics as edyn\n", + "import easydynamics.sample_model as sm\n", "\n", "# Make the plots interactive\n", "%matplotlib widget" @@ -60,7 +46,7 @@ "outputs": [], "source": [ "# Load the vanadium data\n", - "vanadium_experiment = Experiment('Vanadium')\n", + "vanadium_experiment = edyn.Experiment('Vanadium')\n", "\n", "file_path = pooch.retrieve(\n", " url='https://github.com/easyscience/dynamics-lib/raw/refs/heads/master/docs/docs/tutorials/data/vanadium_data_example.h5',\n", @@ -120,8 +106,8 @@ "metadata": {}, "outputs": [], "source": [ - "delta_function = DeltaFunction(name='DeltaFunction', area=1)\n", - "sample_model = SampleModel(components=delta_function)" + "delta_function = sm.DeltaFunction(name='DeltaFunction', area=1)\n", + "sample_model = sm.SampleModel(components=delta_function)" ] }, { @@ -143,11 +129,11 @@ "metadata": {}, "outputs": [], "source": [ - "resolution_components = ComponentCollection()\n", - "res_gauss = Gaussian(width=0.1, area=1, name='Res. Gauss')\n", + "resolution_components = sm.ComponentCollection()\n", + "res_gauss = sm.Gaussian(width=0.1, area=1, name='Res. Gauss')\n", "res_gauss.area.fixed = True\n", "resolution_components.append_component(res_gauss)\n", - "resolution_model = ResolutionModel(components=resolution_components)" + "resolution_model = sm.ResolutionModel(components=resolution_components)" ] }, { @@ -165,7 +151,7 @@ "metadata": {}, "outputs": [], "source": [ - "background_model = BackgroundModel(components=Polynomial(coefficients=[0.001]))" + "background_model = sm.BackgroundModel(components=sm.Polynomial(coefficients=[0.001]))" ] }, { @@ -183,7 +169,7 @@ "metadata": {}, "outputs": [], "source": [ - "instrument_model = InstrumentModel(\n", + "instrument_model = sm.InstrumentModel(\n", " resolution_model=resolution_model,\n", " background_model=background_model,\n", ")" @@ -204,7 +190,7 @@ "metadata": {}, "outputs": [], "source": [ - "vanadium_analysis = Analysis(\n", + "vanadium_analysis = edyn.Analysis(\n", " display_name='Vanadium Full Analysis',\n", " experiment=vanadium_experiment,\n", " sample_model=sample_model,\n", @@ -317,7 +303,7 @@ "metadata": {}, "outputs": [], "source": [ - "diffusion_experiment = Experiment('Diffusion')\n", + "diffusion_experiment = edyn.Experiment('Diffusion')\n", "\n", "file_path = pooch.retrieve(\n", " url='https://github.com/easyscience/dynamics-lib/raw/refs/heads/master/docs/docs/tutorials/data/diffusion_data_example.h5',\n", @@ -352,17 +338,17 @@ "metadata": {}, "outputs": [], "source": [ - "delta_function = DeltaFunction(name='DeltaFunction', area=0.2)\n", - "lorentzian = Lorentzian(name='Lorentzian', area=0.5, width=0.3)\n", - "component_collection = ComponentCollection(\n", + "delta_function = sm.DeltaFunction(name='DeltaFunction', area=0.2)\n", + "lorentzian = sm.Lorentzian(name='Lorentzian', area=0.5, width=0.3)\n", + "component_collection = sm.ComponentCollection(\n", " components=[delta_function, lorentzian],\n", ")\n", "\n", - "sample_model = SampleModel(\n", + "sample_model = sm.SampleModel(\n", " components=component_collection,\n", ")\n", "\n", - "background_model = BackgroundModel(components=Polynomial(coefficients=[0.001]))" + "background_model = sm.BackgroundModel(components=sm.Polynomial(coefficients=[0.001]))" ] }, { @@ -380,14 +366,14 @@ "metadata": {}, "outputs": [], "source": [ - "instrument_model = InstrumentModel(\n", + "instrument_model = sm.InstrumentModel(\n", " background_model=background_model,\n", " resolution_model=vanadium_analysis.instrument_model.resolution_model,\n", ")\n", "instrument_model.resolution_model.fix_all_parameters()\n", "instrument_model.normalize_resolution()\n", "\n", - "diffusion_analysis = Analysis(\n", + "diffusion_analysis = edyn.Analysis(\n", " display_name='Diffusion Analysis',\n", " experiment=diffusion_experiment,\n", " sample_model=sample_model,\n", @@ -496,17 +482,17 @@ "metadata": {}, "outputs": [], "source": [ - "brownian_diffusion_model = BrownianTranslationalDiffusion(\n", - " display_name='Brownian Translational Diffusion', diffusion_coefficient=2.4e-9, scale=0.5\n", + "brownian_diffusion_model = sm.BrownianTranslationalDiffusion(\n", + " name='Brownian Translational Diffusion', diffusion_coefficient=2.4e-9, scale=0.5\n", ")\n", "\n", - "binding = FitBinding(\n", + "binding = edyn.FitBinding(\n", " parameter_name='Lorentzian',\n", " model=brownian_diffusion_model,\n", " modes=['area', 'width'],\n", ")\n", "\n", - "parameter_analysis = ParameterAnalysis(\n", + "parameter_analysis = edyn.ParameterAnalysis(\n", " parameters=diffusion_analysis,\n", " bindings=[binding],\n", ")" @@ -596,20 +582,20 @@ "metadata": {}, "outputs": [], "source": [ - "delta_function = DeltaFunction(name='DeltaFunction', area=0.2)\n", - "component_collection = ComponentCollection(\n", + "delta_function = sm.DeltaFunction(name='DeltaFunction', area=0.2)\n", + "component_collection = sm.ComponentCollection(\n", " components=[delta_function],\n", ")\n", - "diffusion_model = BrownianTranslationalDiffusion(\n", + "diffusion_model = sm.BrownianTranslationalDiffusion(\n", " name='Brownian Translational Diffusion', diffusion_coefficient=2.4e-9, scale=0.5\n", ")\n", "\n", - "sample_model = SampleModel(\n", + "sample_model = sm.SampleModel(\n", " components=component_collection,\n", " diffusion_models=diffusion_model,\n", ")\n", "\n", - "background_model = BackgroundModel(components=Polynomial(coefficients=[0.001]))" + "background_model = sm.BackgroundModel(components=sm.Polynomial(coefficients=[0.001]))" ] }, { @@ -619,7 +605,7 @@ "metadata": {}, "outputs": [], "source": [ - "instrument_model = InstrumentModel(\n", + "instrument_model = sm.InstrumentModel(\n", " background_model=background_model,\n", " resolution_model=vanadium_analysis.instrument_model.resolution_model,\n", ")" @@ -640,7 +626,7 @@ "metadata": {}, "outputs": [], "source": [ - "diffusion_model_analysis = Analysis(\n", + "diffusion_model_analysis = edyn.Analysis(\n", " display_name='Diffusion Full Analysis',\n", " experiment=diffusion_experiment,\n", " sample_model=sample_model,\n", diff --git a/src/easydynamics/base_classes/easydynamics_base.py b/src/easydynamics/base_classes/easydynamics_base.py index cf100ae5..3305f14f 100644 --- a/src/easydynamics/base_classes/easydynamics_base.py +++ b/src/easydynamics/base_classes/easydynamics_base.py @@ -12,7 +12,8 @@ class EasyDynamicsBase(NameMixin, NewBase): def __init__( self, - name: str = 'MyEasyDynamicsModel', + *args: object, + name: str = "MyEasyDynamicsModel", display_name: str | None = None, unique_name: str | None = None, **kwargs: object, @@ -38,12 +39,13 @@ def __init__( """ if not isinstance(name, str): - raise TypeError(f'Name must be a string, got {type(name)}') + raise TypeError(f"Name must be a string, got {type(name)}") if display_name is None: display_name = name super().__init__( + *args, name=name, display_name=display_name, unique_name=unique_name, diff --git a/src/easydynamics/base_classes/easydynamics_list.py b/src/easydynamics/base_classes/easydynamics_list.py index 9c322ac5..26e12324 100644 --- a/src/easydynamics/base_classes/easydynamics_list.py +++ b/src/easydynamics/base_classes/easydynamics_list.py @@ -4,13 +4,12 @@ from __future__ import annotations from collections.abc import Iterable -from typing import Any from typing import TypeVar from easyscience.base_classes.easy_list import EasyList from easyscience.base_classes.new_base import NewBase -ProtectedType_ = TypeVar('ProtectedType', bound=NewBase) +ProtectedType_ = TypeVar("ProtectedType", bound=NewBase) class EasyDynamicsList(EasyList): @@ -22,7 +21,7 @@ def __init__( protected_types: list[type[NewBase]] | type[NewBase] | None = None, display_name: str | None = None, unique_name: str | None = None, - **kwargs: Any, # noqa: ANN401 + **kwargs: object, ) -> None: """ Initialize the EasyDynamicsList. @@ -39,7 +38,7 @@ def __init__( Display name of the list. If None, the name will be used. unique_name : str | None, default=None Unique name of the list. If None, a unique name will be generated. - **kwargs : Any + **kwargs : object Additional keyword arguments to pass to the EasyList constructor. """ @@ -104,7 +103,7 @@ def extend(self, values: Iterable[ProtectedType_]) -> None: protected types. """ if not isinstance(values, Iterable): - raise TypeError('Values must be an iterable.') + raise TypeError("Values must be an iterable.") values = list(values) for v in values: @@ -145,7 +144,7 @@ def pop(self, index: int | str = -1) -> ProtectedType_: item.unlock_name() return self._data.pop(i) raise KeyError(f'No item with name "{index}" found') - raise TypeError('Index must be an int or str') + raise TypeError("Index must be an int or str") # ------------------------------------------------------------------ # Private methods @@ -170,7 +169,7 @@ def _check_name_unique(self, obj: NewBase | Iterable[NewBase]) -> None: new_names = [get_key(item) for item in items] if len(new_names) != len(set(new_names)): - raise ValueError(f'Duplicate names in {obj} detected.') + raise ValueError(f"Duplicate names in {obj} detected.") existing_names = {get_key(o) for o in self._data} @@ -211,9 +210,9 @@ def _validate_type(self, value: object) -> None: """ if not isinstance(value, tuple(self._protected_types)): - allowed = ', '.join(t.__name__ for t in self._protected_types) + allowed = ", ".join(t.__name__ for t in self._protected_types) raise TypeError( - f'Value must be an instance of type: {allowed}. Got {type(value).__name__} instead.' # noqa: E501 + f"Value must be an instance of type: {allowed}. Got {type(value).__name__} instead." # noqa: E501 ) # ------------------------------------------------------------------ diff --git a/src/easydynamics/base_classes/easydynamics_modelbase.py b/src/easydynamics/base_classes/easydynamics_modelbase.py index 31a7338a..c4a55da0 100644 --- a/src/easydynamics/base_classes/easydynamics_modelbase.py +++ b/src/easydynamics/base_classes/easydynamics_modelbase.py @@ -13,10 +13,12 @@ class EasyDynamicsModelBase(NameMixin, ModelBase): def __init__( self, - unit: str | sc.Unit = 'meV', - name: str = 'MyEasyDynamicsModel', + *args: object, + unit: str | sc.Unit = "meV", + name: str = "MyEasyDynamicsModel", display_name: str | None = None, unique_name: str | None = None, + **kwargs: object, ) -> None: """ Initialize the EasyDynamicsModelBase. @@ -39,15 +41,17 @@ def __init__( """ if not isinstance(name, str): - raise TypeError(f'Name must be a string, got {type(name)}') + raise TypeError(f"Name must be a string, got {type(name)}") if display_name is None: display_name = name super().__init__( + *args, name=name, display_name=display_name, unique_name=unique_name, + **kwargs, ) self._unit = _validate_unit(unit) @@ -81,6 +85,6 @@ def unit(self, _unit_str: str) -> None: Always raised to indicate that the unit is read-only. """ 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." ) diff --git a/src/easydynamics/base_classes/name_mixin.py b/src/easydynamics/base_classes/name_mixin.py index 086bdcf7..b04add4e 100644 --- a/src/easydynamics/base_classes/name_mixin.py +++ b/src/easydynamics/base_classes/name_mixin.py @@ -8,7 +8,7 @@ class NameMixin: def __init__( self, *args: object, - name: str = 'MyEasyDynamicsModel', + name: str = "MyEasyDynamicsModel", **kwargs: object, ) -> None: """ @@ -31,7 +31,7 @@ def __init__( super().__init__(*args, **kwargs) if not isinstance(name, str): - raise TypeError('Name must be a string.') + raise TypeError("Name must be a string.") self._name = name self._name_lock_count = 0 @@ -66,10 +66,10 @@ def name(self, name_str: str) -> None: """ if self._name_lock_count > 0: - raise AttributeError('Cannot change name while object is in a list.') + raise AttributeError("Cannot change name while object is in a list.") if not isinstance(name_str, str): - raise TypeError('Name must be a string.') + raise TypeError("Name must be a string.") self._name = name_str def lock_name(self) -> None: @@ -86,6 +86,6 @@ def unlock_name(self) -> None: If the name lock count is already zero. """ if self._name_lock_count == 0: - raise RuntimeError('Name lock count is already zero.') + raise RuntimeError("Name lock count is already zero.") self._name_lock_count -= 1 diff --git a/src/easydynamics/sample_model/component_collection.py b/src/easydynamics/sample_model/component_collection.py index 8f6eb18c..73ab8308 100644 --- a/src/easydynamics/sample_model/component_collection.py +++ b/src/easydynamics/sample_model/component_collection.py @@ -22,14 +22,38 @@ class ComponentCollection(EasyDynamicsList, EasyDynamicsModelBase): """ - Collection of model components representing a sample, background or resolution model. + Collection of model components. + + Examples + -------- + Create a ComponentCollection with two components: + >>> import easydynamics.sample_model as sm + >>> component1 = sm.Gaussian(name='Gaussian1', area=1.0, width=1.0) + >>> component2 = sm.Lorentzian(name='Lorentzian1', area=2.0, width=0.5) + >>> collection = sm.ComponentCollection(components=[component1, component2]) + + Append a component to the collection: + >>> component3 = sm.Gaussian(name='Gaussian2', area=0.5, width=0.8) + >>> collection.append(component3) + + Evaluate the collection at a given energy axis: + >>> import numpy as np + >>> x = np.linspace(-5, 5, 100) + >>> values = collection.evaluate(x) + + Remove a component by name: + >>> collection.remove('Gaussian1') + + List component names: + >>> collection.list_component_names() + ['Lorentzian1', 'Gaussian2'] """ def __init__( self, components: ModelComponent | list[ModelComponent] | None = None, - unit: str | sc.Unit = 'meV', - name: str = 'ComponentCollection', + unit: str | sc.Unit = "meV", + name: str = "ComponentCollection", display_name: str | None = None, unique_name: str | None = None, ) -> None: @@ -60,12 +84,12 @@ def __init__( components = [components] elif not isinstance(components, list): raise TypeError( - f'components must be a ModelComponent or a list of ModelComponent, got {type(components).__name__} instead.' # noqa: E501 + f"components must be a ModelComponent or a list of ModelComponent, got {type(components).__name__} instead." # noqa: E501 ) for comp in components: if not isinstance(comp, ModelComponent): raise TypeError( - f'All items in components must be instances of ModelComponent, got {type(comp).__name__} instead.' # noqa: E501 + f"All items in components must be instances of ModelComponent, got {type(comp).__name__} instead." # noqa: E501 ) EasyDynamicsList.__init__( @@ -114,8 +138,8 @@ def is_empty(self, _value: bool) -> None: Always raised since is_empty is read-only. """ raise AttributeError( - 'is_empty is a read-only property that indicates ' - 'whether the collection has components.' + "is_empty is a read-only property that indicates " + "whether the collection has components." ) def convert_unit(self, unit: str | sc.Unit) -> None: @@ -136,7 +160,9 @@ def convert_unit(self, unit: str | sc.Unit) -> None: """ if not isinstance(unit, (str, sc.Unit)): - raise TypeError(f'Unit must be a string or sc.Unit, got {type(unit).__name__}') + raise TypeError( + f"Unit must be a string or sc.Unit, got {type(unit).__name__}" + ) old_unit = self._unit @@ -198,28 +224,28 @@ def normalize_area(self) -> None: which would prevent normalization. """ if not self: - 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: - if hasattr(component, 'area'): + if hasattr(component, "area"): area_params.append(component.area) total_area += component.area else: warnings.warn( f"Component '{component.name}' does not have an 'area' attribute " - f'and will be skipped in normalization.', + f"and will be skipped in normalization.", UserWarning, stacklevel=2, ) 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 @@ -240,7 +266,9 @@ def get_all_variables(self) -> list[DescriptorBase]: return [var for component in self for var in component.get_all_variables()] - 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. @@ -289,10 +317,12 @@ def evaluate_component( Evaluated values for the specified component. """ if not self: - raise ValueError('No components in the model to evaluate.') + raise ValueError("No components in the model to evaluate.") if not isinstance(name, str): - raise TypeError(f'Component name must be a string, got {type(name)} instead.') + raise TypeError( + f"Component name must be a string, got {type(name)} instead." + ) matches = [comp for comp in self if comp.name == name] if not matches: @@ -348,18 +378,18 @@ def __repr__(self) -> str: str String representation of the ComponentCollection. """ - comp_names = ', '.join(c.name for c in self) or 'No components' + comp_names = ", ".join(c.name for c in self) or "No components" return f"" def to_dict(self) -> dict: return { - '@module': self.__class__.__module__, - '@class': self.__class__.__name__, - 'unit': str(self.unit), - 'name': self.name, - 'display_name': self.display_name, - 'components': [c.to_dict() for c in self._data], + "@module": self.__class__.__module__, + "@class": self.__class__.__name__, + "unit": str(self.unit), + "name": self.name, + "display_name": self.display_name, + "components": [c.to_dict() for c in self._data], } @classmethod @@ -377,17 +407,17 @@ def deserialise_component(d: dict) -> ModelComponent: ModelComponent The deserialised component. """ - module = importlib.import_module(d['@module']) - cls = getattr(module, d['@class']) + module = importlib.import_module(d["@module"]) + cls = getattr(module, d["@class"]) return cls.from_dict(d) - components = [deserialise_component(c) for c in obj_dict.get('components', [])] + components = [deserialise_component(c) for c in obj_dict.get("components", [])] return cls( components=components, - unit=obj_dict.get('unit', 'meV'), - name=obj_dict.get('name', 'ComponentCollection'), - display_name=obj_dict.get('display_name'), + unit=obj_dict.get("unit", "meV"), + name=obj_dict.get("name", "ComponentCollection"), + display_name=obj_dict.get("display_name"), ) def __copy__(self) -> ComponentCollection: From 086f42ba79b21427bfca7089db62c5e341e18ef5 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Sat, 16 May 2026 20:28:37 +0200 Subject: [PATCH 12/20] Tutorials --- docs/docs/tutorials/tutorial0_basics.ipynb | 6 +- .../tutorials/tutorial0_more_advanced.ipynb | 8 +- .../tutorials/tutorial2_nanoparticles.ipynb | 118 +++++++++--------- .../base_classes/easydynamics_base.py | 8 +- .../base_classes/easydynamics_list.py | 12 +- .../base_classes/easydynamics_modelbase.py | 18 +-- src/easydynamics/base_classes/name_mixin.py | 10 +- .../sample_model/component_collection.py | 64 +++++----- 8 files changed, 119 insertions(+), 125 deletions(-) diff --git a/docs/docs/tutorials/tutorial0_basics.ipynb b/docs/docs/tutorials/tutorial0_basics.ipynb index f28f460f..dffa627c 100644 --- a/docs/docs/tutorials/tutorial0_basics.ipynb +++ b/docs/docs/tutorials/tutorial0_basics.ipynb @@ -380,7 +380,7 @@ "source": [ "energy = sc.linspace('energy', -3.5, 3.5, num=1001, unit='meV')\n", "data_and_model = analysis.data_and_model_to_datagroup(energy=energy)\n", - "data_and_model" + "print(data_and_model)" ] }, { @@ -398,7 +398,9 @@ "metadata": {}, "outputs": [], "source": [ - "fit_func = sm.Polynomial(coefficients=[3.7, -0.5], name='Straight line', display_name='Straight line')\n", + "fit_func = sm.Polynomial(\n", + " coefficients=[3.7, -0.5], name='Straight line', display_name='Straight line'\n", + ")\n", "\n", "binding = edyn.FitBinding(parameter_name='Gaussian area', model=fit_func)\n", "\n", diff --git a/docs/docs/tutorials/tutorial0_more_advanced.ipynb b/docs/docs/tutorials/tutorial0_more_advanced.ipynb index 1852e72c..2ebc7120 100644 --- a/docs/docs/tutorials/tutorial0_more_advanced.ipynb +++ b/docs/docs/tutorials/tutorial0_more_advanced.ipynb @@ -310,12 +310,8 @@ "metadata": {}, "outputs": [], "source": [ - "gauss_fit_func = sm.Polynomial(\n", - " coefficients=[3.7, -0.5], unit='1/angstrom', name='Gauss area fit'\n", - ")\n", - "dho_area_fit_func = sm.Polynomial(\n", - " coefficients=[2.0, 0.12], unit='1/angstrom', name='DHO area fit'\n", - ")\n", + "gauss_fit_func = sm.Polynomial(coefficients=[3.7, -0.5], unit='1/angstrom', name='Gauss area fit')\n", + "dho_area_fit_func = sm.Polynomial(coefficients=[2.0, 0.12], unit='1/angstrom', name='DHO area fit')\n", "dho_center_fit_func = sm.Polynomial(\n", " coefficients=[1.1, 0.2], unit='1/angstrom', name='DHO center fit'\n", ")\n", diff --git a/docs/docs/tutorials/tutorial2_nanoparticles.ipynb b/docs/docs/tutorials/tutorial2_nanoparticles.ipynb index a881db87..0352e264 100644 --- a/docs/docs/tutorials/tutorial2_nanoparticles.ipynb +++ b/docs/docs/tutorials/tutorial2_nanoparticles.ipynb @@ -42,18 +42,8 @@ "import pooch\n", "import scipp as sc\n", "\n", - "from easydynamics.analysis.analysis import Analysis\n", - "from easydynamics.experiment import Experiment\n", - "from easydynamics.sample_model import ComponentCollection\n", - "from easydynamics.sample_model import DampedHarmonicOscillator\n", - "from easydynamics.sample_model import DeltaFunction\n", - "from easydynamics.sample_model import Gaussian\n", - "from easydynamics.sample_model import Lorentzian\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", - "from easydynamics.sample_model.sample_model import SampleModel\n", + "import easydynamics as edyn\n", + "import easydynamics.sample_model as sm\n", "from easydynamics.utils.utils import hbar\n", "\n", "# Make the plots interactive\n", @@ -75,7 +65,7 @@ "metadata": {}, "outputs": [], "source": [ - "resolution_experiment = Experiment(display_name='Nanoparticles, 1.5 K')\n", + "resolution_experiment = edyn.Experiment(display_name='Nanoparticles, 1.5 K')\n", "\n", "file_path = pooch.retrieve(\n", " url='https://github.com/easyscience/dynamics-lib/raw/refs/heads/master/docs/docs/tutorials/data/nano_1p5K.h5',\n", @@ -145,29 +135,29 @@ "metadata": {}, "outputs": [], "source": [ - "delta_function = DeltaFunction(area=100)\n", - "res_sample_model = SampleModel(components=delta_function)\n", + "delta_function = sm.DeltaFunction(area=100)\n", + "res_sample_model = sm.SampleModel(components=delta_function)\n", "\n", - "res_resolution_model = ResolutionModel()\n", - "res_components = ComponentCollection()\n", - "res_gauss = Gaussian(area=1, width=0.02)\n", + "res_resolution_model = sm.ResolutionModel()\n", + "res_components = sm.ComponentCollection()\n", + "res_gauss = sm.Gaussian(area=1, width=0.02)\n", "res_gauss.area.fixed = True\n", "\n", "res_components.append_component(res_gauss)\n", "res_resolution_model.components = res_components\n", "\n", - "background_model = BackgroundModel()\n", - "polynomial = Polynomial(coefficients=[1.5])\n", + "background_model = sm.BackgroundModel()\n", + "polynomial = sm.Polynomial(coefficients=[1.5])\n", "polynomial.coefficients[0].min = 0.0\n", "background_model.components = polynomial\n", "\n", "\n", - "res_instrument_model = InstrumentModel(\n", + "res_instrument_model = sm.InstrumentModel(\n", " resolution_model=res_resolution_model,\n", " background_model=background_model,\n", ")\n", "\n", - "res_analysis = Analysis(\n", + "res_analysis = edyn.Analysis(\n", " experiment=resolution_experiment,\n", " sample_model=res_sample_model,\n", " instrument_model=res_instrument_model,\n", @@ -210,10 +200,10 @@ "metadata": {}, "outputs": [], "source": [ - "experiment = Experiment(display_name='Nanoparticles, 150 K')\n", + "experiment = edyn.Experiment(display_name='Nanoparticles, 150 K')\n", "\n", "\n", - "resolution_experiment = Experiment(display_name='Nanoparticles, 1.5 K')\n", + "resolution_experiment = edyn.Experiment(display_name='Nanoparticles, 1.5 K')\n", "\n", "file_path = pooch.retrieve(\n", " url='https://github.com/easyscience/dynamics-lib/raw/refs/heads/master/docs/docs/tutorials/data/nano_150K.h5',\n", @@ -261,21 +251,21 @@ "metadata": {}, "outputs": [], "source": [ - "sample_model = SampleModel()\n", - "water_delta_function = DeltaFunction(name='Water delta function', area=100)\n", - "water_lorentzian = Lorentzian(name='Water Lorentzian', area=10, width=0.2)\n", + "sample_model = sm.SampleModel()\n", + "water_delta_function = sm.DeltaFunction(name='Water delta function', area=100)\n", + "water_lorentzian = sm.Lorentzian(name='Water Lorentzian', area=10, width=0.2)\n", "sample_model.append_component(water_delta_function)\n", "sample_model.append_component(water_lorentzian)\n", "sample_model.temperature = 150\n", "\n", "\n", - "background_model = BackgroundModel()\n", - "polynomial = Polynomial(name='Polynomial', coefficients=[0.15])\n", + "background_model = sm.BackgroundModel()\n", + "polynomial = sm.Polynomial(name='Polynomial', coefficients=[0.15])\n", "polynomial.coefficients[0].min = 0.0\n", "background_model.components = polynomial\n", "\n", "\n", - "instrument_model = InstrumentModel(\n", + "instrument_model = sm.InstrumentModel(\n", " background_model=background_model,\n", " resolution_model=res_analysis.instrument_model.resolution_model,\n", ")\n", @@ -283,7 +273,7 @@ "instrument_model.normalize_resolution()\n", "\n", "\n", - "analysis = Analysis(\n", + "analysis = edyn.Analysis(\n", " experiment=experiment, sample_model=sample_model, instrument_model=instrument_model\n", ")\n", "\n", @@ -344,7 +334,9 @@ "source": [ "We calculate a simple average of the relevant parameters, and fix these in our `Analysis` object at all Q. We plot the resulting model and see that it indeed fits well at low $Q$. At higher $Q$ it obviosuly does not describe all the signal, since there is magnetic scattering there as well.\n", "\n", - "The `DeltaFunction` is the first component (index 0), and the Lorentzian is the second component (index 1). It will soon be possible to refer to components by name as well as index. It will also be made easier to fix parameters at multiple $Q$, but for now we do it manually." + "To access the components we can either use their index, or more conveniently, their name as shown below.\n", + "\n", + "(It will be made easier to fix parameters at multiple $Q$, but for now we do it manually.)" ] }, { @@ -354,23 +346,23 @@ "metadata": {}, "outputs": [], "source": [ - "delta_0 = analysis.sample_model.get_component_collection(Q_index=0).components[0]\n", - "delta_1 = analysis.sample_model.get_component_collection(Q_index=1).components[0]\n", + "delta_0 = analysis.sample_model.get_component_collection(Q_index=0)['Water delta function']\n", + "delta_1 = analysis.sample_model.get_component_collection(Q_index=1)['Water delta function']\n", "delta_area = (delta_0.area + delta_1.area) / 2\n", "\n", "\n", - "lorz_0 = analysis.sample_model.get_component_collection(Q_index=0).components[1]\n", - "lorz_1 = analysis.sample_model.get_component_collection(Q_index=1).components[1]\n", + "lorz_0 = analysis.sample_model.get_component_collection(Q_index=0)['Water Lorentzian']\n", + "lorz_1 = analysis.sample_model.get_component_collection(Q_index=1)['Water Lorentzian']\n", "lorz_area = (lorz_0.area + lorz_1.area) / 2\n", "lorz_width = (lorz_0.width + lorz_1.width) / 2\n", "\n", "\n", "for Q_index in range(analysis.sample_model.Q.size):\n", - " delta = analysis.sample_model.get_component_collection(Q_index=Q_index).components[0]\n", + " delta = analysis.sample_model.get_component_collection(Q_index=Q_index)['Water delta function']\n", " delta.area = delta_area.value\n", " delta.area.fixed = True\n", "\n", - " lorz = analysis.sample_model.get_component_collection(Q_index=Q_index).components[1]\n", + " lorz = analysis.sample_model.get_component_collection(Q_index=Q_index)['Water Lorentzian']\n", " lorz.area = lorz_area.value\n", " lorz.width = lorz_width.value\n", " lorz.area.fixed = True\n", @@ -395,25 +387,25 @@ "outputs": [], "source": [ "# Now make a new analysis with this sample model\n", - "mag_sample_model = SampleModel()\n", - "water_delta_function = DeltaFunction(name='Water delta function', area=100)\n", - "water_lorentzian = Lorentzian(name='Water Lorentzian', area=100, width=0.2)\n", + "mag_sample_model = sm.SampleModel()\n", + "water_delta_function = sm.DeltaFunction(name='Water delta function', area=100)\n", + "water_lorentzian = sm.Lorentzian(name='Water Lorentzian', area=100, width=0.2)\n", "mag_sample_model.append_component(water_delta_function)\n", "mag_sample_model.append_component(water_lorentzian)\n", "\n", "# Add all the magnetic components\n", - "DHO1 = DampedHarmonicOscillator(name='DHO1', area=5, center=0.35, width=0.2)\n", - "DHO2 = DampedHarmonicOscillator(name='DHO2', area=1, center=1.1, width=0.1)\n", - "mag_lorz = Lorentzian(name='Magnetic Lorentzian', area=30, width=0.01)\n", + "DHO1 = sm.DampedHarmonicOscillator(name='DHO1', area=5, center=0.35, width=0.2)\n", + "DHO2 = sm.DampedHarmonicOscillator(name='DHO2', area=1, center=1.1, width=0.1)\n", + "mag_lorz = sm.Lorentzian(name='Magnetic Lorentzian', area=30, width=0.01)\n", "mag_sample_model.append_component(DHO1)\n", "mag_sample_model.append_component(DHO2)\n", "mag_sample_model.append_component(mag_lorz)\n", "\n", - "background_model = BackgroundModel()\n", - "polynomial = Polynomial(name='Polynomial', coefficients=[0.15])\n", + "background_model = sm.BackgroundModel()\n", + "polynomial = sm.Polynomial(name='Polynomial', coefficients=[0.15])\n", "background_model.components = polynomial\n", "\n", - "instrument_model = InstrumentModel(\n", + "instrument_model = sm.InstrumentModel(\n", " background_model=background_model,\n", " resolution_model=res_analysis.instrument_model.resolution_model,\n", ")\n", @@ -421,7 +413,7 @@ "instrument_model.normalize_resolution()\n", "\n", "# Create the analysis object\n", - "mag_analysis = Analysis(\n", + "mag_analysis = edyn.Analysis(\n", " experiment=experiment, sample_model=mag_sample_model, instrument_model=instrument_model\n", ")" ] @@ -442,11 +434,13 @@ "outputs": [], "source": [ "for Q_index in range(mag_analysis.sample_model.Q.size):\n", - " delta = mag_analysis.sample_model.get_component_collection(Q_index=Q_index).components[0]\n", + " delta = mag_analysis.sample_model.get_component_collection(Q_index=Q_index)[\n", + " 'Water delta function'\n", + " ]\n", " delta.area = delta_area.value\n", " delta.area.fixed = True\n", "\n", - " lorz = mag_analysis.sample_model.get_component_collection(Q_index=Q_index).components[1]\n", + " lorz = mag_analysis.sample_model.get_component_collection(Q_index=Q_index)['Water Lorentzian']\n", " lorz.area = lorz_area.value\n", " lorz.width = lorz_width.value\n", " lorz.area.fixed = True\n", @@ -469,9 +463,11 @@ "outputs": [], "source": [ "for Q_index in [0, 1]:\n", - " DHO1 = mag_analysis.sample_model.get_component_collection(Q_index=Q_index).components[2]\n", - " DHO2 = mag_analysis.sample_model.get_component_collection(Q_index=Q_index).components[3]\n", - " lorz = mag_analysis.sample_model.get_component_collection(Q_index=Q_index).components[4]\n", + " DHO1 = mag_analysis.sample_model.get_component_collection(Q_index=Q_index)['DHO1']\n", + " DHO2 = mag_analysis.sample_model.get_component_collection(Q_index=Q_index)['DHO2']\n", + " lorz = mag_analysis.sample_model.get_component_collection(Q_index=Q_index)[\n", + " 'Magnetic Lorentzian'\n", + " ]\n", "\n", " DHO1.area = 0.0\n", " DHO1.center = 1.0\n", @@ -524,8 +520,8 @@ "metadata": {}, "outputs": [], "source": [ - "DHO2_highQ = mag_analysis.sample_model.get_component_collection(Q_index=3).components[3]\n", - "DHO2_lowQ = mag_analysis.sample_model.get_component_collection(Q_index=2).components[3]\n", + "DHO2_highQ = mag_analysis.sample_model.get_component_collection(Q_index=3)['DHO2']\n", + "DHO2_lowQ = mag_analysis.sample_model.get_component_collection(Q_index=2)['DHO2']\n", "\n", "DHO2_lowQ.width.make_dependent_on('a', {'a': DHO2_highQ.width})\n", "DHO2_lowQ.center.make_dependent_on('a', {'a': DHO2_highQ.center})\n", @@ -549,8 +545,8 @@ "metadata": {}, "outputs": [], "source": [ - "width1 = mag_analysis.sample_model.get_component_collection(Q_index=2).components[4].width\n", - "width2 = mag_analysis.sample_model.get_component_collection(Q_index=3).components[4].width\n", + "width1 = mag_analysis.sample_model.get_component_collection(Q_index=2)['Magnetic Lorentzian'].width\n", + "width2 = mag_analysis.sample_model.get_component_collection(Q_index=3)['Magnetic Lorentzian'].width\n", "\n", "width = (width1 + width2) / 2\n", "print(width1)\n", @@ -576,11 +572,11 @@ "metadata": {}, "outputs": [], "source": [ - "E_minus_003 = mag_analysis.sample_model.get_component_collection(Q_index=2).components[3].center\n", - "E_minus_101 = mag_analysis.sample_model.get_component_collection(Q_index=3).components[3].center\n", + "E_minus_003 = mag_analysis.sample_model.get_component_collection(Q_index=2)['DHO2'].center\n", + "E_minus_101 = mag_analysis.sample_model.get_component_collection(Q_index=3)['DHO2'].center\n", "\n", - "E_plus_003 = mag_analysis.sample_model.get_component_collection(Q_index=2).components[2].center\n", - "E_plus_101 = mag_analysis.sample_model.get_component_collection(Q_index=3).components[2].center\n", + "E_plus_003 = mag_analysis.sample_model.get_component_collection(Q_index=2)['DHO1'].center\n", + "E_plus_101 = mag_analysis.sample_model.get_component_collection(Q_index=3)['DHO1'].center\n", "\n", "print(E_minus_003)\n", "print(E_minus_101)\n", diff --git a/src/easydynamics/base_classes/easydynamics_base.py b/src/easydynamics/base_classes/easydynamics_base.py index 3305f14f..e86d567d 100644 --- a/src/easydynamics/base_classes/easydynamics_base.py +++ b/src/easydynamics/base_classes/easydynamics_base.py @@ -13,7 +13,7 @@ class EasyDynamicsBase(NameMixin, NewBase): def __init__( self, *args: object, - name: str = "MyEasyDynamicsModel", + name: str = 'MyEasyDynamicsModel', display_name: str | None = None, unique_name: str | None = None, **kwargs: object, @@ -23,7 +23,9 @@ def __init__( Parameters ---------- - name : str, default='MyEasyDynamicsModel' + *args : object + Positional arguments to pass to the parent class. + name : str, default="MyEasyDynamicsModel" Name of the model. display_name : str | None, default=None Display name of the model. If None, the name will be used. @@ -39,7 +41,7 @@ def __init__( """ if not isinstance(name, str): - raise TypeError(f"Name must be a string, got {type(name)}") + raise TypeError(f'Name must be a string, got {type(name)}') if display_name is None: display_name = name diff --git a/src/easydynamics/base_classes/easydynamics_list.py b/src/easydynamics/base_classes/easydynamics_list.py index 26e12324..090257de 100644 --- a/src/easydynamics/base_classes/easydynamics_list.py +++ b/src/easydynamics/base_classes/easydynamics_list.py @@ -9,7 +9,7 @@ from easyscience.base_classes.easy_list import EasyList from easyscience.base_classes.new_base import NewBase -ProtectedType_ = TypeVar("ProtectedType", bound=NewBase) +ProtectedType_ = TypeVar('ProtectedType', bound=NewBase) class EasyDynamicsList(EasyList): @@ -103,7 +103,7 @@ def extend(self, values: Iterable[ProtectedType_]) -> None: protected types. """ if not isinstance(values, Iterable): - raise TypeError("Values must be an iterable.") + raise TypeError('Values must be an iterable.') values = list(values) for v in values: @@ -144,7 +144,7 @@ def pop(self, index: int | str = -1) -> ProtectedType_: item.unlock_name() return self._data.pop(i) raise KeyError(f'No item with name "{index}" found') - raise TypeError("Index must be an int or str") + raise TypeError('Index must be an int or str') # ------------------------------------------------------------------ # Private methods @@ -169,7 +169,7 @@ def _check_name_unique(self, obj: NewBase | Iterable[NewBase]) -> None: new_names = [get_key(item) for item in items] if len(new_names) != len(set(new_names)): - raise ValueError(f"Duplicate names in {obj} detected.") + raise ValueError(f'Duplicate names in {obj} detected.') existing_names = {get_key(o) for o in self._data} @@ -210,9 +210,9 @@ def _validate_type(self, value: object) -> None: """ if not isinstance(value, tuple(self._protected_types)): - allowed = ", ".join(t.__name__ for t in self._protected_types) + allowed = ', '.join(t.__name__ for t in self._protected_types) raise TypeError( - f"Value must be an instance of type: {allowed}. Got {type(value).__name__} instead." # noqa: E501 + f'Value must be an instance of type: {allowed}. Got {type(value).__name__} instead.' # noqa: E501 ) # ------------------------------------------------------------------ diff --git a/src/easydynamics/base_classes/easydynamics_modelbase.py b/src/easydynamics/base_classes/easydynamics_modelbase.py index c4a55da0..73e92304 100644 --- a/src/easydynamics/base_classes/easydynamics_modelbase.py +++ b/src/easydynamics/base_classes/easydynamics_modelbase.py @@ -14,8 +14,8 @@ class EasyDynamicsModelBase(NameMixin, ModelBase): def __init__( self, *args: object, - unit: str | sc.Unit = "meV", - name: str = "MyEasyDynamicsModel", + unit: str | sc.Unit = 'meV', + name: str = 'MyEasyDynamicsModel', display_name: str | None = None, unique_name: str | None = None, **kwargs: object, @@ -25,14 +25,18 @@ def __init__( Parameters ---------- - unit : str | sc.Unit, default='meV' + *args : object + Positional arguments to pass to the parent class. + unit : str | sc.Unit, default="meV" Unit of the model. - name : str, default='MyEasyDynamicsModel' + name : str, default="MyEasyDynamicsModel" Name of the model. display_name : str | None, default=None Display name of the model. If None, the name will be used. unique_name : str | None, default=None Unique name of the model. If None, a unique name will be generated. + **kwargs : object + Additional keyword arguments to pass to the parent class. Raises ------ @@ -41,7 +45,7 @@ def __init__( """ if not isinstance(name, str): - raise TypeError(f"Name must be a string, got {type(name)}") + raise TypeError(f'Name must be a string, got {type(name)}') if display_name is None: display_name = name @@ -85,6 +89,6 @@ def unit(self, _unit_str: str) -> None: Always raised to indicate that the unit is read-only. """ 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.' ) diff --git a/src/easydynamics/base_classes/name_mixin.py b/src/easydynamics/base_classes/name_mixin.py index b04add4e..086bdcf7 100644 --- a/src/easydynamics/base_classes/name_mixin.py +++ b/src/easydynamics/base_classes/name_mixin.py @@ -8,7 +8,7 @@ class NameMixin: def __init__( self, *args: object, - name: str = "MyEasyDynamicsModel", + name: str = 'MyEasyDynamicsModel', **kwargs: object, ) -> None: """ @@ -31,7 +31,7 @@ def __init__( super().__init__(*args, **kwargs) if not isinstance(name, str): - raise TypeError("Name must be a string.") + raise TypeError('Name must be a string.') self._name = name self._name_lock_count = 0 @@ -66,10 +66,10 @@ def name(self, name_str: str) -> None: """ if self._name_lock_count > 0: - raise AttributeError("Cannot change name while object is in a list.") + raise AttributeError('Cannot change name while object is in a list.') if not isinstance(name_str, str): - raise TypeError("Name must be a string.") + raise TypeError('Name must be a string.') self._name = name_str def lock_name(self) -> None: @@ -86,6 +86,6 @@ def unlock_name(self) -> None: If the name lock count is already zero. """ if self._name_lock_count == 0: - raise RuntimeError("Name lock count is already zero.") + raise RuntimeError('Name lock count is already zero.') self._name_lock_count -= 1 diff --git a/src/easydynamics/sample_model/component_collection.py b/src/easydynamics/sample_model/component_collection.py index 73ab8308..11e6574a 100644 --- a/src/easydynamics/sample_model/component_collection.py +++ b/src/easydynamics/sample_model/component_collection.py @@ -52,8 +52,8 @@ class ComponentCollection(EasyDynamicsList, EasyDynamicsModelBase): def __init__( self, components: ModelComponent | list[ModelComponent] | None = None, - unit: str | sc.Unit = "meV", - name: str = "ComponentCollection", + unit: str | sc.Unit = 'meV', + name: str = 'ComponentCollection', display_name: str | None = None, unique_name: str | None = None, ) -> None: @@ -84,12 +84,12 @@ def __init__( components = [components] elif not isinstance(components, list): raise TypeError( - f"components must be a ModelComponent or a list of ModelComponent, got {type(components).__name__} instead." # noqa: E501 + f'components must be a ModelComponent or a list of ModelComponent, got {type(components).__name__} instead.' # noqa: E501 ) for comp in components: if not isinstance(comp, ModelComponent): raise TypeError( - f"All items in components must be instances of ModelComponent, got {type(comp).__name__} instead." # noqa: E501 + f'All items in components must be instances of ModelComponent, got {type(comp).__name__} instead.' # noqa: E501 ) EasyDynamicsList.__init__( @@ -138,8 +138,8 @@ def is_empty(self, _value: bool) -> None: Always raised since is_empty is read-only. """ raise AttributeError( - "is_empty is a read-only property that indicates " - "whether the collection has components." + 'is_empty is a read-only property that indicates ' + 'whether the collection has components.' ) def convert_unit(self, unit: str | sc.Unit) -> None: @@ -160,9 +160,7 @@ def convert_unit(self, unit: str | sc.Unit) -> None: """ if not isinstance(unit, (str, sc.Unit)): - raise TypeError( - f"Unit must be a string or sc.Unit, got {type(unit).__name__}" - ) + raise TypeError(f'Unit must be a string or sc.Unit, got {type(unit).__name__}') old_unit = self._unit @@ -224,28 +222,28 @@ def normalize_area(self) -> None: which would prevent normalization. """ if not self: - 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: - if hasattr(component, "area"): + if hasattr(component, 'area'): area_params.append(component.area) total_area += component.area else: warnings.warn( f"Component '{component.name}' does not have an 'area' attribute " - f"and will be skipped in normalization.", + f'and will be skipped in normalization.', UserWarning, stacklevel=2, ) 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 @@ -266,9 +264,7 @@ def get_all_variables(self) -> list[DescriptorBase]: return [var for component in self for var in component.get_all_variables()] - 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. @@ -317,12 +313,10 @@ def evaluate_component( Evaluated values for the specified component. """ if not self: - raise ValueError("No components in the model to evaluate.") + raise ValueError('No components in the model to evaluate.') if not isinstance(name, str): - raise TypeError( - f"Component name must be a string, got {type(name)} instead." - ) + raise TypeError(f'Component name must be a string, got {type(name)} instead.') matches = [comp for comp in self if comp.name == name] if not matches: @@ -378,18 +372,18 @@ def __repr__(self) -> str: str String representation of the ComponentCollection. """ - comp_names = ", ".join(c.name for c in self) or "No components" + comp_names = ', '.join(c.name for c in self) or 'No components' return f"" def to_dict(self) -> dict: return { - "@module": self.__class__.__module__, - "@class": self.__class__.__name__, - "unit": str(self.unit), - "name": self.name, - "display_name": self.display_name, - "components": [c.to_dict() for c in self._data], + '@module': self.__class__.__module__, + '@class': self.__class__.__name__, + 'unit': str(self.unit), + 'name': self.name, + 'display_name': self.display_name, + 'components': [c.to_dict() for c in self._data], } @classmethod @@ -407,17 +401,17 @@ def deserialise_component(d: dict) -> ModelComponent: ModelComponent The deserialised component. """ - module = importlib.import_module(d["@module"]) - cls = getattr(module, d["@class"]) + module = importlib.import_module(d['@module']) + cls = getattr(module, d['@class']) return cls.from_dict(d) - components = [deserialise_component(c) for c in obj_dict.get("components", [])] + components = [deserialise_component(c) for c in obj_dict.get('components', [])] return cls( components=components, - unit=obj_dict.get("unit", "meV"), - name=obj_dict.get("name", "ComponentCollection"), - display_name=obj_dict.get("display_name"), + unit=obj_dict.get('unit', 'meV'), + name=obj_dict.get('name', 'ComponentCollection'), + display_name=obj_dict.get('display_name'), ) def __copy__(self) -> ComponentCollection: From 7621d125db6d56f35cc2acd69c8028c1c59ee01b Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Sat, 16 May 2026 20:55:01 +0200 Subject: [PATCH 13/20] Update data creation notebook --- docs/docs/tutorials/data/create_fake_data.ipynb | 10 +++++----- src/easydynamics/base_classes/easydynamics_base.py | 2 +- .../base_classes/easydynamics_modelbase.py | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/docs/tutorials/data/create_fake_data.ipynb b/docs/docs/tutorials/data/create_fake_data.ipynb index 34ddd5f7..e4ed0ee8 100644 --- a/docs/docs/tutorials/data/create_fake_data.ipynb +++ b/docs/docs/tutorials/data/create_fake_data.ipynb @@ -46,7 +46,7 @@ "for i in range(Q.size):\n", " components = model.get_component_collection(i)\n", " offset = 0.0\n", - " components.components[0].area = 3.79 - 0.2 * Q[i].value\n", + " components[0].area = 3.79 - 0.2 * Q[i].value\n", "\n", "energy = sc.linspace(start=-3.0, stop=3.0, num=756, unit='meV', dim='energy')\n", "\n", @@ -86,14 +86,14 @@ "energy = sc.linspace(start=-3.0, stop=3.0, num=756, unit='meV', dim='energy')\n", "intensity_values = np.zeros((Q.size, energy.size))\n", "rng = np.random.default_rng()\n", - "noise = rng.normal(loc=0.0, scale=0.35, size=intensity_dataarray.shape)\n", + "noise = rng.normal(loc=0.0, scale=0.35, size=intensity_values.shape)\n", "\n", "for i in range(Q.size):\n", " components = model.get_component_collection(i)\n", " offset = sc.scalar(value=rng.uniform(0.05, 0.15), unit='meV')\n", - " components.components[0].area = 3.79 - 0.2 * Q[i].value\n", - " components.components[2].center = 1.4 + Q[i].value / 10.0\n", - " components.components[2].area = 2.45 + Q[i].value / 10.0\n", + " components[0].area = 3.79 - 0.2 * Q[i].value\n", + " components[2].center = 1.4 + Q[i].value / 10.0\n", + " components[2].area = 2.45 + Q[i].value / 10.0\n", "\n", " intensity_values[i, :] = components.evaluate(x=energy.values - offset.value)\n", "\n", diff --git a/src/easydynamics/base_classes/easydynamics_base.py b/src/easydynamics/base_classes/easydynamics_base.py index e86d567d..530ed117 100644 --- a/src/easydynamics/base_classes/easydynamics_base.py +++ b/src/easydynamics/base_classes/easydynamics_base.py @@ -25,7 +25,7 @@ def __init__( ---------- *args : object Positional arguments to pass to the parent class. - name : str, default="MyEasyDynamicsModel" + name : str, default='MyEasyDynamicsModel' Name of the model. display_name : str | None, default=None Display name of the model. If None, the name will be used. diff --git a/src/easydynamics/base_classes/easydynamics_modelbase.py b/src/easydynamics/base_classes/easydynamics_modelbase.py index 73e92304..6d002f71 100644 --- a/src/easydynamics/base_classes/easydynamics_modelbase.py +++ b/src/easydynamics/base_classes/easydynamics_modelbase.py @@ -27,9 +27,9 @@ def __init__( ---------- *args : object Positional arguments to pass to the parent class. - unit : str | sc.Unit, default="meV" + unit : str | sc.Unit, default='meV' Unit of the model. - name : str, default="MyEasyDynamicsModel" + name : str, default='MyEasyDynamicsModel' Name of the model. display_name : str | None, default=None Display name of the model. If None, the name will be used. From 77cd153f2368122e09d5319a9239556a9bb69780 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Sat, 16 May 2026 21:04:20 +0200 Subject: [PATCH 14/20] Update componentcollection example --- docs/docs/tutorials/component_collection.ipynb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/docs/tutorials/component_collection.ipynb b/docs/docs/tutorials/component_collection.ipynb index fe108648..10fde6d3 100644 --- a/docs/docs/tutorials/component_collection.ipynb +++ b/docs/docs/tutorials/component_collection.ipynb @@ -61,7 +61,11 @@ " plt.plot(x, y, label=component.display_name)\n", "\n", "plt.legend()\n", - "plt.show()" + "plt.show()\n", + "\n", + "# Accessing components by name\n", + "gaussian_component = component_collection['Gaussian']\n", + "print(gaussian_component)" ] } ], From 502be74ac8e4ca0bf25fb85b4ae5895dd69b1d86 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Mon, 18 May 2026 09:15:49 +0200 Subject: [PATCH 15/20] test name_mixin --- docs/docs/tutorials/components.ipynb | 2 +- src/easydynamics/base_classes/name_mixin.py | 23 +++- .../base_classes/test_name_mixin.py | 110 ++++++++++++++++++ 3 files changed, 128 insertions(+), 7 deletions(-) create mode 100644 tests/unit/easydynamics/base_classes/test_name_mixin.py diff --git a/docs/docs/tutorials/components.ipynb b/docs/docs/tutorials/components.ipynb index 4934347b..145ddf06 100644 --- a/docs/docs/tutorials/components.ipynb +++ b/docs/docs/tutorials/components.ipynb @@ -15,7 +15,7 @@ { "cell_type": "code", "execution_count": null, - "id": "1", + "id": "df408006", "metadata": {}, "outputs": [], "source": [ diff --git a/src/easydynamics/base_classes/name_mixin.py b/src/easydynamics/base_classes/name_mixin.py index 086bdcf7..6011a664 100644 --- a/src/easydynamics/base_classes/name_mixin.py +++ b/src/easydynamics/base_classes/name_mixin.py @@ -8,7 +8,7 @@ class NameMixin: def __init__( self, *args: object, - name: str = 'MyEasyDynamicsModel', + name: str = "MyEasyDynamicsModel", **kwargs: object, ) -> None: """ @@ -31,7 +31,7 @@ def __init__( super().__init__(*args, **kwargs) if not isinstance(name, str): - raise TypeError('Name must be a string.') + raise TypeError("Name must be a string.") self._name = name self._name_lock_count = 0 @@ -65,11 +65,11 @@ def name(self, name_str: str) -> None: If name_str is not a string. """ - if self._name_lock_count > 0: - raise AttributeError('Cannot change name while object is in a list.') + if self.is_name_locked(): + raise AttributeError("Cannot change name while object is in a list.") if not isinstance(name_str, str): - raise TypeError('Name must be a string.') + raise TypeError("Name must be a string.") self._name = name_str def lock_name(self) -> None: @@ -86,6 +86,17 @@ def unlock_name(self) -> None: If the name lock count is already zero. """ if self._name_lock_count == 0: - raise RuntimeError('Name lock count is already zero.') + raise RuntimeError("Name lock count is already zero.") self._name_lock_count -= 1 + + def is_name_locked(self) -> bool: + """ + Check if the name is currently locked. + + Returns + ------- + bool + True if the name is locked, False otherwise. + """ + return self._name_lock_count > 0 diff --git a/tests/unit/easydynamics/base_classes/test_name_mixin.py b/tests/unit/easydynamics/base_classes/test_name_mixin.py new file mode 100644 index 00000000..b6ad0cc1 --- /dev/null +++ b/tests/unit/easydynamics/base_classes/test_name_mixin.py @@ -0,0 +1,110 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + +import pytest + +from easydynamics.base_classes.name_mixin import NameMixin + + +class TestNameMixin: + """Tests for the NameMixin class.""" + + @pytest.fixture + def name_mixin(self): + """Fixture for creating an instance of NameMixin.""" + + return NameMixin(name="TestModel") + + def test_initialization(self, name_mixin): + """Test that the NameMixin is initialized correctly.""" + + # WHEN THEN EXPECT + assert name_mixin.name == "TestModel" + assert name_mixin.display_name == "TestModel" + assert name_mixin.unique_name is not None + assert name_mixin.is_name_locked() is False + + def test_init_raises_type_error_for_invalid_name(self): + """Test that initializing with an invalid name raises a TypeError.""" + # WHEN THEN EXPECT + with pytest.raises(TypeError, match=r"Name must be a string."): + NameMixin(name=123) # Not a string + + def test_init_name_cannot_be_none(self): + """Test that initializing with name as None raises a TypeError.""" + # WHEN THEN EXPECT + with pytest.raises(TypeError, match=r"Name must be a string."): + NameMixin(name=None) + + def test_name_setter_and_getter(self, name_mixin): + """Test that the name setter and getter work correctly.""" + # WHEN THEN EXPECT + assert name_mixin.name == "TestModel" + + # THEN + name_mixin.name = "NewName" + + # EXPECT + assert name_mixin.name == "NewName" + + # THEN + with pytest.raises(TypeError, match=r"Name must be a string."): + name_mixin.name = None + + @pytest.mark.parametrize( + "invalid_name", + [ + 123, # Not a string + [1, 2, 3], # Not a string + {"name": "Test"}, # Not a string + ], + ids=["integer", "list", "dict"], + ) + def test_name_setter_invalid_type(self, name_mixin, invalid_name): + """Test that setting the name to an invalid type raises a TypeError.""" + # WHEN THEN EXPECT + with pytest.raises(TypeError, match=r"Name must be a string."): + name_mixin.name = invalid_name + + def test_name_locking(self, name_mixin): + """Test that the name locking mechanism works correctly.""" + # WHEN THEN EXPECT + assert name_mixin.is_name_locked() is False + + # Lock and unlock the name + # THEN + name_mixin.lock_name() + + # EXPECT + assert name_mixin.is_name_locked() is True + + # THEN + name_mixin.unlock_name() + + # EXPECT + assert name_mixin.is_name_locked() is False + + # unlock an already unlocked name should raise an error + # THEN EXPECT + with pytest.raises(RuntimeError, match=r"Name lock count is already zero."): + name_mixin.unlock_name() + + # locking twice should require unlocking twice + # THEN + name_mixin.lock_name() + name_mixin.lock_name() + + # THEN EXPECT + assert name_mixin.is_name_locked() is True + + # THEN + name_mixin.unlock_name() + + # EXPECT + assert name_mixin.is_name_locked() is True + + # THEN + name_mixin.unlock_name() + + # EXPECT + assert name_mixin.is_name_locked() is False From 4a3bb49dfeb9032b5e252947c670ce959f28ecd3 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Mon, 18 May 2026 09:16:29 +0200 Subject: [PATCH 16/20] more name_mixin test --- .../unit/easydynamics/base_classes/test_name_mixin.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/unit/easydynamics/base_classes/test_name_mixin.py b/tests/unit/easydynamics/base_classes/test_name_mixin.py index b6ad0cc1..dc5f1152 100644 --- a/tests/unit/easydynamics/base_classes/test_name_mixin.py +++ b/tests/unit/easydynamics/base_classes/test_name_mixin.py @@ -108,3 +108,14 @@ def test_name_locking(self, name_mixin): # EXPECT assert name_mixin.is_name_locked() is False + + def test_name_setter_raises_when_locked(self, name_mixin): + """Test that setting the name while it is locked raises an AttributeError.""" + # WHEN THEN EXPECT + name_mixin.lock_name() + + # THEN EXPECT + with pytest.raises( + AttributeError, match=r"Cannot change name while object is in a list." + ): + name_mixin.name = "AnotherName" From 2117a1b877460977d61575277519bac3352d05ad Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Mon, 18 May 2026 13:39:25 +0200 Subject: [PATCH 17/20] Some tests --- docs/docs/tutorials/components.ipynb | 2 +- .../base_classes/easydynamics_list.py | 25 +- .../base_classes/test_easydynamics_list.py | 222 ++++++++++++++++++ .../base_classes/test_name_mixin.py | 2 - 4 files changed, 236 insertions(+), 15 deletions(-) create mode 100644 tests/unit/easydynamics/base_classes/test_easydynamics_list.py diff --git a/docs/docs/tutorials/components.ipynb b/docs/docs/tutorials/components.ipynb index 145ddf06..0466030a 100644 --- a/docs/docs/tutorials/components.ipynb +++ b/docs/docs/tutorials/components.ipynb @@ -14,7 +14,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "df408006", "metadata": {}, "outputs": [], diff --git a/src/easydynamics/base_classes/easydynamics_list.py b/src/easydynamics/base_classes/easydynamics_list.py index 090257de..f950f861 100644 --- a/src/easydynamics/base_classes/easydynamics_list.py +++ b/src/easydynamics/base_classes/easydynamics_list.py @@ -9,7 +9,7 @@ from easyscience.base_classes.easy_list import EasyList from easyscience.base_classes.new_base import NewBase -ProtectedType_ = TypeVar('ProtectedType', bound=NewBase) +ProtectedType_ = TypeVar("ProtectedType", bound=NewBase) class EasyDynamicsList(EasyList): @@ -84,7 +84,7 @@ def append(self, value: ProtectedType_) -> None: self._validate_type(value) self._check_name_unique(value) super().append(value) - value.lock_name() + # super eventually calls insert, which locks the name def extend(self, values: Iterable[ProtectedType_]) -> None: """ @@ -103,7 +103,7 @@ def extend(self, values: Iterable[ProtectedType_]) -> None: protected types. """ if not isinstance(values, Iterable): - raise TypeError('Values must be an iterable.') + raise TypeError("Values must be an iterable.") values = list(values) for v in values: @@ -134,17 +134,17 @@ def pop(self, index: int | str = -1) -> ProtectedType_: If index is a str and no item with that name is found. """ if isinstance(index, int): - item = self[index] + item = self._data.pop(index) item.unlock_name() - return self._data.pop(index) + return item if isinstance(index, str): for i, item in enumerate(self._data): if self._get_key(item) == index: - item = self[i] - item.unlock_name() - return self._data.pop(i) + return_item = self._data.pop(i) + return_item.unlock_name() + return return_item raise KeyError(f'No item with name "{index}" found') - raise TypeError('Index must be an int or str') + raise TypeError("Index must be an int or str") # ------------------------------------------------------------------ # Private methods @@ -169,7 +169,7 @@ def _check_name_unique(self, obj: NewBase | Iterable[NewBase]) -> None: new_names = [get_key(item) for item in items] if len(new_names) != len(set(new_names)): - raise ValueError(f'Duplicate names in {obj} detected.') + raise ValueError(f"Duplicate names in {obj} detected.") existing_names = {get_key(o) for o in self._data} @@ -210,9 +210,9 @@ def _validate_type(self, value: object) -> None: """ if not isinstance(value, tuple(self._protected_types)): - allowed = ', '.join(t.__name__ for t in self._protected_types) + allowed = ", ".join(t.__name__ for t in self._protected_types) raise TypeError( - f'Value must be an instance of type: {allowed}. Got {type(value).__name__} instead.' # noqa: E501 + f"Value must be an instance of type: {allowed}. Got {type(value).__name__} instead." # noqa: E501 ) # ------------------------------------------------------------------ @@ -233,6 +233,7 @@ def __setitem__( The item or items to set. Must be an instance of one of the protected types or an iterable of protected types. """ + self._check_name_unique(value) super().__setitem__(idx, value) if isinstance(idx, slice): diff --git a/tests/unit/easydynamics/base_classes/test_easydynamics_list.py b/tests/unit/easydynamics/base_classes/test_easydynamics_list.py new file mode 100644 index 00000000..1625e699 --- /dev/null +++ b/tests/unit/easydynamics/base_classes/test_easydynamics_list.py @@ -0,0 +1,222 @@ +import pytest + +from easydynamics.base_classes.easydynamics_list import EasyDynamicsList +from easydynamics.sample_model import Gaussian +from easydynamics.sample_model import Lorentzian +from easydynamics.sample_model.components.model_component import ModelComponent + + +class TestEasyDynamicsList: + """Tests for the EasyDynamicsList class.""" + + @pytest.fixture + def easy_dynamics_list(self): + """Fixture for creating an instance of EasyDynamicsList.""" + gaussian = Gaussian(name="Gaussian") + lorentzian = Lorentzian(name="Lorentzian") + return EasyDynamicsList( + gaussian, + lorentzian, + protected_types=ModelComponent, + display_name="TestList", + ) + + def test_initialization(self, easy_dynamics_list): + """Test that the EasyDynamicsList is initialized correctly.""" + # WHEN THEN EXPECT + assert easy_dynamics_list.display_name == "TestList" + assert len(easy_dynamics_list) == 2 + assert isinstance(easy_dynamics_list[0], Gaussian) + assert isinstance(easy_dynamics_list[1], Lorentzian) + + def test_initialization_invalid_type(self): + """Test that initializing with an invalid type raises a TypeError.""" + # WHEN THEN EXPECT + with pytest.raises(TypeError): + EasyDynamicsList("Not a ModelComponent", protected_types=ModelComponent) + + def test_initialization_invalid_type_in_list(self): + """Test that initializing with a list containing an invalid type raises a TypeError.""" + # WHEN THEN EXPECT + with pytest.raises(TypeError): + EasyDynamicsList( + [Gaussian(name="Gaussian"), "Not a ModelComponent"], + protected_types=ModelComponent, + ) + + def test_init_locks_name(self, easy_dynamics_list): + """Test that the name is locked.""" + # WHEN THEN EXPECT + with pytest.raises(AttributeError): + easy_dynamics_list[0].name = "NewName" + + def test_insert(self, easy_dynamics_list): + """Test that the insert method works correctly.""" + # WHEN + new_gaussian = Gaussian(name="NewGaussian") + + # THEN + easy_dynamics_list.insert(1, new_gaussian) + + # EXPECT + assert len(easy_dynamics_list) == 3 + assert isinstance(easy_dynamics_list[1], Gaussian) + assert easy_dynamics_list[1].name == "NewGaussian" + + def test_insert_invalid_type(self, easy_dynamics_list): + """Test that inserting an invalid type raises a TypeError.""" + # WHEN THEN EXPECT + with pytest.raises(TypeError): + easy_dynamics_list.insert(0, "Not a ModelComponent") + + def test_insert_locks_name(self, easy_dynamics_list): + """Test that the name of the inserted item is locked.""" + # WHEN + new_gaussian = Gaussian(name="NewGaussian") + + # THEN + easy_dynamics_list.insert(1, new_gaussian) + + # EXPECT + with pytest.raises(AttributeError): + easy_dynamics_list[1].name = "AnotherName" + + def test_insert_repeated_name(self, easy_dynamics_list): + """Test that inserting an item with a repeated name raises a ValueError.""" + # WHEN + new_gaussian = Gaussian(name="Gaussian") + # THEN EXPECT + with pytest.raises(ValueError): + easy_dynamics_list.insert(1, new_gaussian) + + def test_append(self, easy_dynamics_list): + """Test that the append method works correctly.""" + # WHEN + new_lorentzian = Lorentzian(name="NewLorentzian") + + # THEN + easy_dynamics_list.append(new_lorentzian) + + # EXPECT + assert len(easy_dynamics_list) == 3 + assert isinstance(easy_dynamics_list[2], Lorentzian) + assert easy_dynamics_list[2].name == "NewLorentzian" + + def test_append_invalid_type(self, easy_dynamics_list): + """Test that appending an invalid type raises a TypeError.""" + # WHEN THEN EXPECT + with pytest.raises(TypeError): + easy_dynamics_list.append("Not a ModelComponent") + + def test_append_locks_name(self, easy_dynamics_list): + """Test that the name of the appended item is locked.""" + # WHEN + new_lorentzian = Lorentzian(name="NewLorentzian") + + # THEN + easy_dynamics_list.append(new_lorentzian) + + # EXPECT + with pytest.raises(AttributeError): + easy_dynamics_list[2].name = "AnotherName" + + def test_append_repeated_name(self, easy_dynamics_list): + """Test that appending an item with a repeated name raises a ValueError.""" + # WHEN + new_lorentzian = Lorentzian(name="Lorentzian") + # THEN EXPECT + with pytest.raises(ValueError): + easy_dynamics_list.append(new_lorentzian) + + def test_extend(self, easy_dynamics_list): + """Test that the extend method works correctly.""" + # WHEN + new_gaussian = Gaussian(name="NewGaussian") + new_lorentzian = Lorentzian(name="NewLorentzian") + + # THEN + easy_dynamics_list.extend([new_gaussian, new_lorentzian]) + + # EXPECT + assert len(easy_dynamics_list) == 4 + assert isinstance(easy_dynamics_list[2], Gaussian) + assert isinstance(easy_dynamics_list[3], Lorentzian) + assert easy_dynamics_list[2].name == "NewGaussian" + assert easy_dynamics_list[3].name == "NewLorentzian" + + def test_extend_invalid_type(self, easy_dynamics_list): + """Test that extending with an invalid type raises a TypeError.""" + # WHEN THEN EXPECT + with pytest.raises(TypeError): + easy_dynamics_list.extend(["Not a ModelComponent"]) + + def test_extend_non_iterable(self, easy_dynamics_list): + """Test that extending with a non-iterable raises a TypeError.""" + # WHEN THEN EXPECT + with pytest.raises(TypeError): + easy_dynamics_list.extend("Not an iterable") + + def test_extend_locks_names(self, easy_dynamics_list): + """Test that the names of the extended items are locked.""" + # WHEN + new_gaussian = Gaussian(name="NewGaussian") + new_lorentzian = Lorentzian(name="NewLorentzian") + + # THEN + easy_dynamics_list.extend([new_gaussian, new_lorentzian]) + + # EXPECT + with pytest.raises(AttributeError): + easy_dynamics_list[2].name = "AnotherName" + with pytest.raises(AttributeError): + easy_dynamics_list[3].name = "AnotherName" + + def test_extend_repeated_names(self, easy_dynamics_list): + """Test that extending with items that have repeated names raises a ValueError.""" + # WHEN + new_gaussian = Gaussian(name="NewGaussian") + new_lorentzian = Lorentzian(name="Lorentzian") + # THEN EXPECT + with pytest.raises(ValueError): + easy_dynamics_list.extend([new_gaussian, new_lorentzian]) + + def test_extend_repeated_names_in_values(self, easy_dynamics_list): + """Test that extending with items that have repeated names among themselves raises a ValueError.""" + # WHEN + new_gaussian1 = Gaussian(name="NewGaussian") + new_gaussian2 = Gaussian(name="NewGaussian") + # THEN EXPECT + with pytest.raises(ValueError): + easy_dynamics_list.extend([new_gaussian1, new_gaussian2]) + + def test_pop(self, easy_dynamics_list): + """Test that the pop method works correctly.""" + # WHEN THEN + popped_item = easy_dynamics_list.pop(0) + + # EXPECT + assert isinstance(popped_item, Gaussian) + assert popped_item.name == "Gaussian" + assert len(easy_dynamics_list) == 1 + assert popped_item.is_name_locked() is False + + # WHEN THEN + popped_item = easy_dynamics_list.pop("Lorentzian") + + # EXPECT + assert isinstance(popped_item, Lorentzian) + assert popped_item.name == "Lorentzian" + assert len(easy_dynamics_list) == 0 + assert popped_item.is_name_locked() is False + + def test_pop_invalid_index_type(self, easy_dynamics_list): + """Test that popping with an invalid index type raises a TypeError.""" + # WHEN THEN EXPECT + with pytest.raises(TypeError): + easy_dynamics_list.pop(1.5) + + def test_pop_nonexistent_name(self, easy_dynamics_list): + """Test that popping with a nonexistent name raises a KeyError.""" + # WHEN THEN EXPECT + with pytest.raises(KeyError, match=r'No item with name "Nonexistent" found'): + easy_dynamics_list.pop("Nonexistent") diff --git a/tests/unit/easydynamics/base_classes/test_name_mixin.py b/tests/unit/easydynamics/base_classes/test_name_mixin.py index dc5f1152..e7d110b5 100644 --- a/tests/unit/easydynamics/base_classes/test_name_mixin.py +++ b/tests/unit/easydynamics/base_classes/test_name_mixin.py @@ -20,8 +20,6 @@ def test_initialization(self, name_mixin): # WHEN THEN EXPECT assert name_mixin.name == "TestModel" - assert name_mixin.display_name == "TestModel" - assert name_mixin.unique_name is not None assert name_mixin.is_name_locked() is False def test_init_raises_type_error_for_invalid_name(self): From 44f5ca3263db32b8c853abcc9ec6d7c23ae916bf Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Mon, 18 May 2026 14:21:51 +0200 Subject: [PATCH 18/20] Allow multiple items with same name --- .../tutorials/tutorial2_nanoparticles.ipynb | 1 + .../base_classes/easydynamics_list.py | 200 +++++++++--------- src/easydynamics/base_classes/name_mixin.py | 43 +--- src/easydynamics/exceptions.py | 9 + .../base_classes/test_easydynamics_list.py | 126 +++-------- .../base_classes/test_name_mixin.py | 79 ++----- .../sample_model/test_component_collection.py | 4 +- 7 files changed, 155 insertions(+), 307 deletions(-) create mode 100644 src/easydynamics/exceptions.py diff --git a/docs/docs/tutorials/tutorial2_nanoparticles.ipynb b/docs/docs/tutorials/tutorial2_nanoparticles.ipynb index 0352e264..2fb4fc4a 100644 --- a/docs/docs/tutorials/tutorial2_nanoparticles.ipynb +++ b/docs/docs/tutorials/tutorial2_nanoparticles.ipynb @@ -521,6 +521,7 @@ "outputs": [], "source": [ "DHO2_highQ = mag_analysis.sample_model.get_component_collection(Q_index=3)['DHO2']\n", + "\n", "DHO2_lowQ = mag_analysis.sample_model.get_component_collection(Q_index=2)['DHO2']\n", "\n", "DHO2_lowQ.width.make_dependent_on('a', {'a': DHO2_highQ.width})\n", diff --git a/src/easydynamics/base_classes/easydynamics_list.py b/src/easydynamics/base_classes/easydynamics_list.py index f950f861..e1d12b6c 100644 --- a/src/easydynamics/base_classes/easydynamics_list.py +++ b/src/easydynamics/base_classes/easydynamics_list.py @@ -3,13 +3,19 @@ from __future__ import annotations -from collections.abc import Iterable +import warnings from typing import TypeVar from easyscience.base_classes.easy_list import EasyList -from easyscience.base_classes.new_base import NewBase -ProtectedType_ = TypeVar("ProtectedType", bound=NewBase) +from easydynamics.base_classes.easydynamics_base import EasyDynamicsBase +from easydynamics.base_classes.easydynamics_modelbase import EasyDynamicsModelBase +from easydynamics.exceptions import AmbiguousNameError + +# Need to fix type hints for protected types... +ProtectedType_ = TypeVar('ProtectedType', bound=EasyDynamicsBase | EasyDynamicsModelBase) + +ProtectedType = type[EasyDynamicsBase | EasyDynamicsModelBase] class EasyDynamicsList(EasyList): @@ -18,7 +24,7 @@ class EasyDynamicsList(EasyList): def __init__( self, *args: ProtectedType_ | list[ProtectedType_], - protected_types: list[type[NewBase]] | type[NewBase] | None = None, + protected_types: list[ProtectedType] | ProtectedType | None = None, display_name: str | None = None, unique_name: str | None = None, **kwargs: object, @@ -31,9 +37,10 @@ def __init__( *args : ProtectedType_ | list[ProtectedType_] Initial items to add to the list. Can be a single item or a list of items. Each item must be an instance of one of the protected types. - protected_types : list[type[NewBase]] | type[NewBase] | None, default=None - Types that are allowed in the list. Can be a single NewBase subclass or a list of them. - If None, defaults to [NewBase]. + protected_types : list[ProtectedType] | ProtectedType | None, default=None + Types that are allowed in the list. Can be a single EasyDynamicsBase or + EasyDynamicsModelBase subclass or a list of them. If None, defaults to + [EasyDynamicsBase]. display_name : str | None, default=None Display name of the list. If None, the name will be used. unique_name : str | None, default=None @@ -54,7 +61,7 @@ def __init__( ) # ------------------------------------------------------------------ - # Methods + # List methods # ------------------------------------------------------------------ def insert(self, index: int, value: ProtectedType_) -> None: @@ -68,10 +75,21 @@ def insert(self, index: int, value: ProtectedType_) -> None: value : ProtectedType_ The item to insert. Must be an instance of one of the protected types. """ + + # Overwritten to update warning self._validate_type(value) - self._check_name_unique(value) + if value in self: + warnings.warn( + ( + f'Item with name "{self._get_key(value)}" already ' + f'in EasyDynamicsList, it will be ignored' + ), + UserWarning, + stacklevel=2, + ) + return + super().insert(index, value) - value.lock_name() def append(self, value: ProtectedType_) -> None: """ @@ -82,35 +100,7 @@ def append(self, value: ProtectedType_) -> None: The item to append. Must be an instance of one of the protected types. """ self._validate_type(value) - self._check_name_unique(value) super().append(value) - # super eventually calls insert, which locks the name - - def extend(self, values: Iterable[ProtectedType_]) -> None: - """ - Extend the list by appending elements from the iterable. - - Parameters - ---------- - values : Iterable[ProtectedType_] - An iterable of items to append. Each item must be an instance of one of the protected - types. - - Raises - ------ - TypeError - If values is not an iterable or if any item in values is not an instance of one of the - protected types. - """ - if not isinstance(values, Iterable): - raise TypeError("Values must be an iterable.") - values = list(values) - - for v in values: - self._validate_type(v) - self._check_name_unique(values) - for v in values: - self.append(v) def pop(self, index: int | str = -1) -> ProtectedType_: """ @@ -133,58 +123,62 @@ def pop(self, index: int | str = -1) -> ProtectedType_: KeyError If index is a str and no item with that name is found. """ + + # Overwritten to update warning if isinstance(index, int): - item = self._data.pop(index) - item.unlock_name() - return item + return self._data.pop(index) if isinstance(index, str): for i, item in enumerate(self._data): if self._get_key(item) == index: - return_item = self._data.pop(i) - return_item.unlock_name() - return return_item + return self._data.pop(i) raise KeyError(f'No item with name "{index}" found') - raise TypeError("Index must be an int or str") + raise TypeError('Index must be an int or str') # ------------------------------------------------------------------ - # Private methods + # Other methods # ------------------------------------------------------------------ - def _check_name_unique(self, obj: NewBase | Iterable[NewBase]) -> None: + def names(self) -> list[str]: """ - Check that the name of an object is unique in the list. - Parameters - ---------- - obj : NewBase | Iterable[NewBase] - Object or objects to check. Can be a single object or an iterable of objects. - Raises - ------ - ValueError - If the name of the object is not unique in the list. - """ - - items = [obj] if isinstance(obj, NewBase) else list(obj) + Get a list of the names of all items in the list. - get_key = self._get_key - new_names = [get_key(item) for item in items] + Returns + ------- + list[str] + A list of the names of all items in the list. + """ + return [self._get_key(item) for item in self._data] - if len(new_names) != len(set(new_names)): - raise ValueError(f"Duplicate names in {obj} detected.") + def duplicate_names(self) -> list[str]: + """ + Get a list of duplicate names in the list. - existing_names = {get_key(o) for o in self._data} + Returns + ------- + list[str] + A list of duplicate names in the list. + """ + seen = set() + duplicates = set() + for item in self._data: + name = self._get_key(item) + if name in seen: + duplicates.add(name) + else: + seen.add(name) + return list(duplicates) - conflict = existing_names.intersection(new_names) - if conflict: - name = next(iter(conflict)) - raise ValueError(f'Name "{name}" already exists in list.') + # ------------------------------------------------------------------ + # Private methods + # ------------------------------------------------------------------ - def _get_key(self, obj: NewBase) -> str: + def _get_key(self, obj: EasyDynamicsBase | EasyDynamicsModelBase) -> str: """ Get the name of an object. Parameters ---------- - obj : NewBase + obj : EasyDynamicsBase | EasyDynamicsModelBase Object to get the key for. Returns @@ -210,50 +204,50 @@ def _validate_type(self, value: object) -> None: """ if not isinstance(value, tuple(self._protected_types)): - allowed = ", ".join(t.__name__ for t in self._protected_types) + allowed = ', '.join(t.__name__ for t in self._protected_types) raise TypeError( - f"Value must be an instance of type: {allowed}. Got {type(value).__name__} instead." # noqa: E501 + f'Value must be an instance of type: {allowed}. Got {type(value).__name__} instead.' # noqa: E501 ) # ------------------------------------------------------------------ # dunder methods # ------------------------------------------------------------------ - - def __setitem__( - self, idx: int | slice, value: ProtectedType_ | Iterable[ProtectedType_] - ) -> None: + def __getitem__( + self, idx: int | slice | str + ) -> ProtectedType_ | EasyDynamicsList[ProtectedType_]: """ - Set an item in the list at a specific index. + Get an item by index, slice, or unique_name. Parameters ---------- - idx : int | slice - The index at which to set the item. - value : ProtectedType_ | Iterable[ProtectedType_] - The item or items to set. Must be an instance of one of the protected types or an - iterable of protected types. - """ + idx : int | slice | str + Index, slice, or name of the item to get. - self._check_name_unique(value) - super().__setitem__(idx, value) - if isinstance(idx, slice): - for v in value: - v.lock_name() - else: - value.lock_name() - - def __delitem__(self, idx: int | slice) -> None: - """ - Delete an item from the list at a specific index. + Returns + ------- + ProtectedType_ | EasyDynamicsList[ProtectedType_] + The item at the specified index or name, or a new EasyDynamicsList if a slice is + provided. - Parameters - ---------- - idx : int | slice - The index at which to delete the item. + Raises + ------ + TypeError + If idx is not an int, slice, or str. + KeyError + If idx is a str and no item with that name is found. + AmbiguousNameError + If idx is a str and multiple items with that name are found. """ if isinstance(idx, int): - self[idx].unlock_name() - elif isinstance(idx, slice): - for item in self[idx]: - item.unlock_name() - super().__delitem__(idx) + return self._data[idx] + if isinstance(idx, slice): + return self.__class__(self._data[idx], protected_types=self._protected_types) + if isinstance(idx, str): + matches = [r for r in self._data if self._get_key(r) == idx] + if len(matches) == 1: + return matches[0] + if len(matches) > 1: + raise AmbiguousNameError(idx, matches) + + raise KeyError(f'No item with name "{idx}" found') + raise TypeError('Index must be an int, slice, or str') diff --git a/src/easydynamics/base_classes/name_mixin.py b/src/easydynamics/base_classes/name_mixin.py index 6011a664..1c5378a1 100644 --- a/src/easydynamics/base_classes/name_mixin.py +++ b/src/easydynamics/base_classes/name_mixin.py @@ -8,7 +8,7 @@ class NameMixin: def __init__( self, *args: object, - name: str = "MyEasyDynamicsModel", + name: str = 'MyEasyDynamicsModel', **kwargs: object, ) -> None: """ @@ -18,7 +18,7 @@ def __init__( ---------- *args : object Positional arguments to pass to the parent class. - name : str, default='MyEasyDynamicsModel' + name : str, default="MyEasyDynamicsModel" Name of the model. **kwargs : object Keyword arguments to pass to the parent class. @@ -31,9 +31,8 @@ def __init__( super().__init__(*args, **kwargs) if not isinstance(name, str): - raise TypeError("Name must be a string.") + raise TypeError('Name must be a string.') self._name = name - self._name_lock_count = 0 @property def name(self) -> str: @@ -59,44 +58,10 @@ def name(self, name_str: str) -> None: Raises ------ - AttributeError - If the name is locked due to being in a list. TypeError If name_str is not a string. """ - if self.is_name_locked(): - raise AttributeError("Cannot change name while object is in a list.") - if not isinstance(name_str, str): - raise TypeError("Name must be a string.") + raise TypeError('Name must be a string.') self._name = name_str - - def lock_name(self) -> None: - """Prevent the name from being modified.""" - self._name_lock_count += 1 - - def unlock_name(self) -> None: - """ - Allow the name to be modified if no containers remain. - - Raises - ------ - RuntimeError - If the name lock count is already zero. - """ - if self._name_lock_count == 0: - raise RuntimeError("Name lock count is already zero.") - - self._name_lock_count -= 1 - - def is_name_locked(self) -> bool: - """ - Check if the name is currently locked. - - Returns - ------- - bool - True if the name is locked, False otherwise. - """ - return self._name_lock_count > 0 diff --git a/src/easydynamics/exceptions.py b/src/easydynamics/exceptions.py new file mode 100644 index 00000000..e21f30a2 --- /dev/null +++ b/src/easydynamics/exceptions.py @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + + +class AmbiguousNameError(Exception): + def __init__(self, name: str, matches: list[str]) -> None: + self.name = name + self.matches = matches + super().__init__(f"Ambiguous name '{name}' matches {len(matches)} elements: {matches}") diff --git a/tests/unit/easydynamics/base_classes/test_easydynamics_list.py b/tests/unit/easydynamics/base_classes/test_easydynamics_list.py index 1625e699..3c640244 100644 --- a/tests/unit/easydynamics/base_classes/test_easydynamics_list.py +++ b/tests/unit/easydynamics/base_classes/test_easydynamics_list.py @@ -1,3 +1,6 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + import pytest from easydynamics.base_classes.easydynamics_list import EasyDynamicsList @@ -12,19 +15,19 @@ class TestEasyDynamicsList: @pytest.fixture def easy_dynamics_list(self): """Fixture for creating an instance of EasyDynamicsList.""" - gaussian = Gaussian(name="Gaussian") - lorentzian = Lorentzian(name="Lorentzian") + gaussian = Gaussian(name='Gaussian') + lorentzian = Lorentzian(name='Lorentzian') return EasyDynamicsList( gaussian, lorentzian, protected_types=ModelComponent, - display_name="TestList", + display_name='TestList', ) def test_initialization(self, easy_dynamics_list): """Test that the EasyDynamicsList is initialized correctly.""" # WHEN THEN EXPECT - assert easy_dynamics_list.display_name == "TestList" + assert easy_dynamics_list.display_name == 'TestList' assert len(easy_dynamics_list) == 2 assert isinstance(easy_dynamics_list[0], Gaussian) assert isinstance(easy_dynamics_list[1], Lorentzian) @@ -33,27 +36,21 @@ def test_initialization_invalid_type(self): """Test that initializing with an invalid type raises a TypeError.""" # WHEN THEN EXPECT with pytest.raises(TypeError): - EasyDynamicsList("Not a ModelComponent", protected_types=ModelComponent) + EasyDynamicsList('Not a ModelComponent', protected_types=ModelComponent) def test_initialization_invalid_type_in_list(self): """Test that initializing with a list containing an invalid type raises a TypeError.""" # WHEN THEN EXPECT with pytest.raises(TypeError): EasyDynamicsList( - [Gaussian(name="Gaussian"), "Not a ModelComponent"], + [Gaussian(name='Gaussian'), 'Not a ModelComponent'], protected_types=ModelComponent, ) - def test_init_locks_name(self, easy_dynamics_list): - """Test that the name is locked.""" - # WHEN THEN EXPECT - with pytest.raises(AttributeError): - easy_dynamics_list[0].name = "NewName" - def test_insert(self, easy_dynamics_list): """Test that the insert method works correctly.""" # WHEN - new_gaussian = Gaussian(name="NewGaussian") + new_gaussian = Gaussian(name='NewGaussian') # THEN easy_dynamics_list.insert(1, new_gaussian) @@ -61,38 +58,30 @@ def test_insert(self, easy_dynamics_list): # EXPECT assert len(easy_dynamics_list) == 3 assert isinstance(easy_dynamics_list[1], Gaussian) - assert easy_dynamics_list[1].name == "NewGaussian" + assert easy_dynamics_list[1].name == 'NewGaussian' def test_insert_invalid_type(self, easy_dynamics_list): """Test that inserting an invalid type raises a TypeError.""" # WHEN THEN EXPECT with pytest.raises(TypeError): - easy_dynamics_list.insert(0, "Not a ModelComponent") + easy_dynamics_list.insert(0, 'Not a ModelComponent') - def test_insert_locks_name(self, easy_dynamics_list): - """Test that the name of the inserted item is locked.""" + def test_insert_repeated_name(self, easy_dynamics_list): + """Test that inserting an item with a repeated name works""" # WHEN - new_gaussian = Gaussian(name="NewGaussian") + new_gaussian = Gaussian(name='Gaussian') # THEN easy_dynamics_list.insert(1, new_gaussian) # EXPECT - with pytest.raises(AttributeError): - easy_dynamics_list[1].name = "AnotherName" - - def test_insert_repeated_name(self, easy_dynamics_list): - """Test that inserting an item with a repeated name raises a ValueError.""" - # WHEN - new_gaussian = Gaussian(name="Gaussian") - # THEN EXPECT - with pytest.raises(ValueError): - easy_dynamics_list.insert(1, new_gaussian) + assert len(easy_dynamics_list) == 3 + assert easy_dynamics_list[1] is new_gaussian def test_append(self, easy_dynamics_list): """Test that the append method works correctly.""" # WHEN - new_lorentzian = Lorentzian(name="NewLorentzian") + new_lorentzian = Lorentzian(name='NewLorentzian') # THEN easy_dynamics_list.append(new_lorentzian) @@ -100,39 +89,19 @@ def test_append(self, easy_dynamics_list): # EXPECT assert len(easy_dynamics_list) == 3 assert isinstance(easy_dynamics_list[2], Lorentzian) - assert easy_dynamics_list[2].name == "NewLorentzian" + assert easy_dynamics_list[2].name == 'NewLorentzian' def test_append_invalid_type(self, easy_dynamics_list): """Test that appending an invalid type raises a TypeError.""" # WHEN THEN EXPECT with pytest.raises(TypeError): - easy_dynamics_list.append("Not a ModelComponent") - - def test_append_locks_name(self, easy_dynamics_list): - """Test that the name of the appended item is locked.""" - # WHEN - new_lorentzian = Lorentzian(name="NewLorentzian") - - # THEN - easy_dynamics_list.append(new_lorentzian) - - # EXPECT - with pytest.raises(AttributeError): - easy_dynamics_list[2].name = "AnotherName" - - def test_append_repeated_name(self, easy_dynamics_list): - """Test that appending an item with a repeated name raises a ValueError.""" - # WHEN - new_lorentzian = Lorentzian(name="Lorentzian") - # THEN EXPECT - with pytest.raises(ValueError): - easy_dynamics_list.append(new_lorentzian) + easy_dynamics_list.append('Not a ModelComponent') def test_extend(self, easy_dynamics_list): """Test that the extend method works correctly.""" # WHEN - new_gaussian = Gaussian(name="NewGaussian") - new_lorentzian = Lorentzian(name="NewLorentzian") + new_gaussian = Gaussian(name='NewGaussian') + new_lorentzian = Lorentzian(name='NewLorentzian') # THEN easy_dynamics_list.extend([new_gaussian, new_lorentzian]) @@ -141,53 +110,20 @@ def test_extend(self, easy_dynamics_list): assert len(easy_dynamics_list) == 4 assert isinstance(easy_dynamics_list[2], Gaussian) assert isinstance(easy_dynamics_list[3], Lorentzian) - assert easy_dynamics_list[2].name == "NewGaussian" - assert easy_dynamics_list[3].name == "NewLorentzian" + assert easy_dynamics_list[2].name == 'NewGaussian' + assert easy_dynamics_list[3].name == 'NewLorentzian' def test_extend_invalid_type(self, easy_dynamics_list): """Test that extending with an invalid type raises a TypeError.""" # WHEN THEN EXPECT with pytest.raises(TypeError): - easy_dynamics_list.extend(["Not a ModelComponent"]) + easy_dynamics_list.extend(['Not a ModelComponent']) def test_extend_non_iterable(self, easy_dynamics_list): """Test that extending with a non-iterable raises a TypeError.""" # WHEN THEN EXPECT with pytest.raises(TypeError): - easy_dynamics_list.extend("Not an iterable") - - def test_extend_locks_names(self, easy_dynamics_list): - """Test that the names of the extended items are locked.""" - # WHEN - new_gaussian = Gaussian(name="NewGaussian") - new_lorentzian = Lorentzian(name="NewLorentzian") - - # THEN - easy_dynamics_list.extend([new_gaussian, new_lorentzian]) - - # EXPECT - with pytest.raises(AttributeError): - easy_dynamics_list[2].name = "AnotherName" - with pytest.raises(AttributeError): - easy_dynamics_list[3].name = "AnotherName" - - def test_extend_repeated_names(self, easy_dynamics_list): - """Test that extending with items that have repeated names raises a ValueError.""" - # WHEN - new_gaussian = Gaussian(name="NewGaussian") - new_lorentzian = Lorentzian(name="Lorentzian") - # THEN EXPECT - with pytest.raises(ValueError): - easy_dynamics_list.extend([new_gaussian, new_lorentzian]) - - def test_extend_repeated_names_in_values(self, easy_dynamics_list): - """Test that extending with items that have repeated names among themselves raises a ValueError.""" - # WHEN - new_gaussian1 = Gaussian(name="NewGaussian") - new_gaussian2 = Gaussian(name="NewGaussian") - # THEN EXPECT - with pytest.raises(ValueError): - easy_dynamics_list.extend([new_gaussian1, new_gaussian2]) + easy_dynamics_list.extend('Not an iterable') def test_pop(self, easy_dynamics_list): """Test that the pop method works correctly.""" @@ -196,18 +132,16 @@ def test_pop(self, easy_dynamics_list): # EXPECT assert isinstance(popped_item, Gaussian) - assert popped_item.name == "Gaussian" + assert popped_item.name == 'Gaussian' assert len(easy_dynamics_list) == 1 - assert popped_item.is_name_locked() is False # WHEN THEN - popped_item = easy_dynamics_list.pop("Lorentzian") + popped_item = easy_dynamics_list.pop('Lorentzian') # EXPECT assert isinstance(popped_item, Lorentzian) - assert popped_item.name == "Lorentzian" + assert popped_item.name == 'Lorentzian' assert len(easy_dynamics_list) == 0 - assert popped_item.is_name_locked() is False def test_pop_invalid_index_type(self, easy_dynamics_list): """Test that popping with an invalid index type raises a TypeError.""" @@ -219,4 +153,4 @@ def test_pop_nonexistent_name(self, easy_dynamics_list): """Test that popping with a nonexistent name raises a KeyError.""" # WHEN THEN EXPECT with pytest.raises(KeyError, match=r'No item with name "Nonexistent" found'): - easy_dynamics_list.pop("Nonexistent") + easy_dynamics_list.pop('Nonexistent') diff --git a/tests/unit/easydynamics/base_classes/test_name_mixin.py b/tests/unit/easydynamics/base_classes/test_name_mixin.py index e7d110b5..5ea93163 100644 --- a/tests/unit/easydynamics/base_classes/test_name_mixin.py +++ b/tests/unit/easydynamics/base_classes/test_name_mixin.py @@ -13,107 +13,52 @@ class TestNameMixin: def name_mixin(self): """Fixture for creating an instance of NameMixin.""" - return NameMixin(name="TestModel") + return NameMixin(name='TestModel') def test_initialization(self, name_mixin): """Test that the NameMixin is initialized correctly.""" # WHEN THEN EXPECT - assert name_mixin.name == "TestModel" - assert name_mixin.is_name_locked() is False + assert name_mixin.name == 'TestModel' def test_init_raises_type_error_for_invalid_name(self): """Test that initializing with an invalid name raises a TypeError.""" # WHEN THEN EXPECT - with pytest.raises(TypeError, match=r"Name must be a string."): + with pytest.raises(TypeError, match=r'Name must be a string.'): NameMixin(name=123) # Not a string def test_init_name_cannot_be_none(self): """Test that initializing with name as None raises a TypeError.""" # WHEN THEN EXPECT - with pytest.raises(TypeError, match=r"Name must be a string."): + with pytest.raises(TypeError, match=r'Name must be a string.'): NameMixin(name=None) def test_name_setter_and_getter(self, name_mixin): """Test that the name setter and getter work correctly.""" # WHEN THEN EXPECT - assert name_mixin.name == "TestModel" + assert name_mixin.name == 'TestModel' # THEN - name_mixin.name = "NewName" + name_mixin.name = 'NewName' # EXPECT - assert name_mixin.name == "NewName" + assert name_mixin.name == 'NewName' # THEN - with pytest.raises(TypeError, match=r"Name must be a string."): + with pytest.raises(TypeError, match=r'Name must be a string.'): name_mixin.name = None @pytest.mark.parametrize( - "invalid_name", + 'invalid_name', [ 123, # Not a string [1, 2, 3], # Not a string - {"name": "Test"}, # Not a string + {'name': 'Test'}, # Not a string ], - ids=["integer", "list", "dict"], + ids=['integer', 'list', 'dict'], ) def test_name_setter_invalid_type(self, name_mixin, invalid_name): """Test that setting the name to an invalid type raises a TypeError.""" # WHEN THEN EXPECT - with pytest.raises(TypeError, match=r"Name must be a string."): + with pytest.raises(TypeError, match=r'Name must be a string.'): name_mixin.name = invalid_name - - def test_name_locking(self, name_mixin): - """Test that the name locking mechanism works correctly.""" - # WHEN THEN EXPECT - assert name_mixin.is_name_locked() is False - - # Lock and unlock the name - # THEN - name_mixin.lock_name() - - # EXPECT - assert name_mixin.is_name_locked() is True - - # THEN - name_mixin.unlock_name() - - # EXPECT - assert name_mixin.is_name_locked() is False - - # unlock an already unlocked name should raise an error - # THEN EXPECT - with pytest.raises(RuntimeError, match=r"Name lock count is already zero."): - name_mixin.unlock_name() - - # locking twice should require unlocking twice - # THEN - name_mixin.lock_name() - name_mixin.lock_name() - - # THEN EXPECT - assert name_mixin.is_name_locked() is True - - # THEN - name_mixin.unlock_name() - - # EXPECT - assert name_mixin.is_name_locked() is True - - # THEN - name_mixin.unlock_name() - - # EXPECT - assert name_mixin.is_name_locked() is False - - def test_name_setter_raises_when_locked(self, name_mixin): - """Test that setting the name while it is locked raises an AttributeError.""" - # WHEN THEN EXPECT - name_mixin.lock_name() - - # THEN EXPECT - with pytest.raises( - AttributeError, match=r"Cannot change name while object is in a list." - ): - name_mixin.name = "AnotherName" diff --git a/tests/unit/easydynamics/sample_model/test_component_collection.py b/tests/unit/easydynamics/sample_model/test_component_collection.py index c806f061..e495eb05 100644 --- a/tests/unit/easydynamics/sample_model/test_component_collection.py +++ b/tests/unit/easydynamics/sample_model/test_component_collection.py @@ -103,11 +103,11 @@ def test_append_component_collection(self, component_collection): # EXPECT assert component_collection[-1] is component - def test_append_existing_component_raises(self, component_collection): + def test_append_existing_component_warns(self, component_collection): # WHEN THEN component = component_collection[0] # EXPECT - with pytest.raises(ValueError, match='already exists in list'): + with pytest.warns(UserWarning, match='it will be ignored'): component_collection.append_component(component) def test_append_invalid_component_raises(self, component_collection): From efcc1cebe199f7b04fbefdce47e5c1905c2ddeda Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Tue, 19 May 2026 13:29:47 +0200 Subject: [PATCH 19/20] add tests --- .../base_classes/easydynamics_list.py | 10 +- src/easydynamics/base_classes/name_mixin.py | 2 +- .../base_classes/test_easydynamics_list.py | 103 ++++++++++++++++++ .../sample_model/test_component_collection.py | 12 +- tests/unit/easydynamics/test_exceptions.py | 29 +++++ 5 files changed, 150 insertions(+), 6 deletions(-) create mode 100644 tests/unit/easydynamics/test_exceptions.py diff --git a/src/easydynamics/base_classes/easydynamics_list.py b/src/easydynamics/base_classes/easydynamics_list.py index e1d12b6c..de6be332 100644 --- a/src/easydynamics/base_classes/easydynamics_list.py +++ b/src/easydynamics/base_classes/easydynamics_list.py @@ -100,6 +100,8 @@ def append(self, value: ProtectedType_) -> None: The item to append. Must be an instance of one of the protected types. """ self._validate_type(value) + + # append calls insert which checks for duplicates super().append(value) def pop(self, index: int | str = -1) -> ProtectedType_: @@ -138,7 +140,7 @@ def pop(self, index: int | str = -1) -> ProtectedType_: # Other methods # ------------------------------------------------------------------ - def names(self) -> list[str]: + def get_names(self) -> list[str]: """ Get a list of the names of all items in the list. @@ -147,9 +149,9 @@ def names(self) -> list[str]: list[str] A list of the names of all items in the list. """ - return [self._get_key(item) for item in self._data] + return [item.name for item in self._data] - def duplicate_names(self) -> list[str]: + def get_duplicate_names(self) -> list[str]: """ Get a list of duplicate names in the list. @@ -161,7 +163,7 @@ def duplicate_names(self) -> list[str]: seen = set() duplicates = set() for item in self._data: - name = self._get_key(item) + name = item.name if name in seen: duplicates.add(name) else: diff --git a/src/easydynamics/base_classes/name_mixin.py b/src/easydynamics/base_classes/name_mixin.py index 1c5378a1..608ce561 100644 --- a/src/easydynamics/base_classes/name_mixin.py +++ b/src/easydynamics/base_classes/name_mixin.py @@ -18,7 +18,7 @@ def __init__( ---------- *args : object Positional arguments to pass to the parent class. - name : str, default="MyEasyDynamicsModel" + name : str, default='MyEasyDynamicsModel' Name of the model. **kwargs : object Keyword arguments to pass to the parent class. diff --git a/tests/unit/easydynamics/base_classes/test_easydynamics_list.py b/tests/unit/easydynamics/base_classes/test_easydynamics_list.py index 3c640244..3d81ec60 100644 --- a/tests/unit/easydynamics/base_classes/test_easydynamics_list.py +++ b/tests/unit/easydynamics/base_classes/test_easydynamics_list.py @@ -4,6 +4,7 @@ import pytest from easydynamics.base_classes.easydynamics_list import EasyDynamicsList +from easydynamics.exceptions import AmbiguousNameError from easydynamics.sample_model import Gaussian from easydynamics.sample_model import Lorentzian from easydynamics.sample_model.components.model_component import ModelComponent @@ -78,6 +79,15 @@ def test_insert_repeated_name(self, easy_dynamics_list): assert len(easy_dynamics_list) == 3 assert easy_dynamics_list[1] is new_gaussian + def test_insert_repeated_component_warns(self, easy_dynamics_list): + """Test that inserting a repeated component raises a warning.""" + # WHEN THEN EXPECT + with pytest.warns(UserWarning, match=r'already in EasyDynamicsList'): + easy_dynamics_list.insert(1, easy_dynamics_list[0]) + + assert len(easy_dynamics_list) == 2 + assert easy_dynamics_list[1] is not easy_dynamics_list[0] + def test_append(self, easy_dynamics_list): """Test that the append method works correctly.""" # WHEN @@ -97,6 +107,14 @@ def test_append_invalid_type(self, easy_dynamics_list): with pytest.raises(TypeError): easy_dynamics_list.append('Not a ModelComponent') + def test_append_repeated_component_warns(self, easy_dynamics_list): + """Test that appending a repeated component raises a warning.""" + # WHEN THEN EXPECT + with pytest.warns(UserWarning, match=r'already in EasyDynamicsList'): + easy_dynamics_list.append(easy_dynamics_list[0]) + + assert len(easy_dynamics_list) == 2 + def test_extend(self, easy_dynamics_list): """Test that the extend method works correctly.""" # WHEN @@ -125,6 +143,14 @@ def test_extend_non_iterable(self, easy_dynamics_list): with pytest.raises(TypeError): easy_dynamics_list.extend('Not an iterable') + def test_extend_repeated_component_warns(self, easy_dynamics_list): + """Test that extending with a repeated component raises a warning.""" + # WHEN THEN EXPECT + with pytest.warns(UserWarning, match=r'already in EasyDynamicsList'): + easy_dynamics_list.extend([easy_dynamics_list[0]]) + + assert len(easy_dynamics_list) == 2 + def test_pop(self, easy_dynamics_list): """Test that the pop method works correctly.""" # WHEN THEN @@ -154,3 +180,80 @@ def test_pop_nonexistent_name(self, easy_dynamics_list): # WHEN THEN EXPECT with pytest.raises(KeyError, match=r'No item with name "Nonexistent" found'): easy_dynamics_list.pop('Nonexistent') + + def test_names(self, easy_dynamics_list): + """Test that the names method returns the correct list of names.""" + # WHEN THEN EXPECT + assert easy_dynamics_list.get_names() == ['Gaussian', 'Lorentzian'] + + def test_duplicate_names_no_duplicates(self, easy_dynamics_list): + """Test that the get_duplicate_names method returns an empty list + when there are no duplicates.""" + # WHEN THEN EXPECT + assert easy_dynamics_list.get_duplicate_names() == [] + + def test_duplicate_names_with_duplicates(self, easy_dynamics_list): + """Test that the get_duplicate_names method returns the correct + list of duplicate names.""" + # WHEN + new_gaussian = Gaussian(name='Gaussian') + easy_dynamics_list.append(new_gaussian) + + # THEN EXPECT + assert easy_dynamics_list.get_duplicate_names() == ['Gaussian'] + + def test_getitem_int(self, easy_dynamics_list): + """Test getting an item by integer index.""" + # WHEN + + # THEN + item = easy_dynamics_list[0] + + # EXPECT + assert isinstance(item, Gaussian) + assert item.name == 'Gaussian' + + def test_getitem_slice(self, easy_dynamics_list): + """Test getting items by slice.""" + # WHEN + + # THEN + sliced = easy_dynamics_list[:1] + + # EXPECT + assert isinstance(sliced, EasyDynamicsList) + assert len(sliced) == 1 + assert isinstance(sliced[0], Gaussian) + assert sliced[0].name == 'Gaussian' + + def test_getitem_name(self, easy_dynamics_list): + """Test getting an item by name.""" + # WHEN + + # THEN + item = easy_dynamics_list['Gaussian'] + + # EXPECT + assert isinstance(item, Gaussian) + assert item.name == 'Gaussian' + + def test_getitem_nonexistent_name(self, easy_dynamics_list): + """Test getting an item with a nonexistent name raises KeyError.""" + # WHEN THEN EXPECT + with pytest.raises(KeyError, match=r'No item with name "Nonexistent" found'): + easy_dynamics_list['Nonexistent'] + + def test_getitem_ambiguous_name(self, easy_dynamics_list): + """Test getting an item with duplicate names raises AmbiguousNameError.""" + # WHEN + easy_dynamics_list.append(Gaussian(name='Gaussian')) + + # THEN EXPECT + with pytest.raises(AmbiguousNameError): + easy_dynamics_list['Gaussian'] + + def test_getitem_invalid_type(self, easy_dynamics_list): + """Test getting an item with an invalid index type raises TypeError.""" + # WHEN THEN EXPECT + with pytest.raises(TypeError, match=r'Index must be an int, slice, or str'): + easy_dynamics_list[1.5] diff --git a/tests/unit/easydynamics/sample_model/test_component_collection.py b/tests/unit/easydynamics/sample_model/test_component_collection.py index e495eb05..14835ec3 100644 --- a/tests/unit/easydynamics/sample_model/test_component_collection.py +++ b/tests/unit/easydynamics/sample_model/test_component_collection.py @@ -46,11 +46,21 @@ def test_init(self): assert component_collection.display_name == 'InitModel' assert not component_collection + def test_init_with_component(self): + # WHEN THEN + component1 = Gaussian(name='TestGaussian1', area=1.0, center=0.0, width=1.0, unit='meV') + component_collection = ComponentCollection(display_name='InitModel', components=component1) + + # EXPECT + assert component_collection.display_name == 'InitModel' + assert len(component_collection) == 1 + assert component_collection[0] is component1 + def test_init_with_components(self): # WHEN THEN component1 = Gaussian(name='TestGaussian1', area=1.0, center=0.0, width=1.0, unit='meV') component2 = Lorentzian( - display_name='TestLorentzian1', area=2.0, center=1.0, width=0.5, unit='meV' + name='TestLorentzian1', area=2.0, center=1.0, width=0.5, unit='meV' ) component_collection = ComponentCollection( display_name='InitModel', components=[component1, component2] diff --git a/tests/unit/easydynamics/test_exceptions.py b/tests/unit/easydynamics/test_exceptions.py new file mode 100644 index 00000000..494f6f7c --- /dev/null +++ b/tests/unit/easydynamics/test_exceptions.py @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause + + +from easydynamics.exceptions import AmbiguousNameError + + +class TestAmbiguousNameError: + def test_initialization(self): + name = 'test' + matches = ['test1', 'test2', 'test3'] + + error = AmbiguousNameError(name, matches) + + assert error.name == name + assert error.matches == matches + assert str(error) == ( + "Ambiguous name 'test' matches 3 elements: ['test1', 'test2', 'test3']" + ) + + def test_empty_matches(self): + name = 'unknown' + matches = [] + + error = AmbiguousNameError(name, matches) + + assert error.name == name + assert error.matches == matches + assert str(error) == ("Ambiguous name 'unknown' matches 0 elements: []") From 9ca833ef82050a8b730822643f057b29693b4e88 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Tue, 19 May 2026 13:50:41 +0200 Subject: [PATCH 20/20] fix type hint --- docs/docs/tutorials/components.ipynb | 2 +- src/easydynamics/base_classes/easydynamics_list.py | 11 ++++------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/docs/docs/tutorials/components.ipynb b/docs/docs/tutorials/components.ipynb index 0466030a..145ddf06 100644 --- a/docs/docs/tutorials/components.ipynb +++ b/docs/docs/tutorials/components.ipynb @@ -14,7 +14,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "df408006", "metadata": {}, "outputs": [], diff --git a/src/easydynamics/base_classes/easydynamics_list.py b/src/easydynamics/base_classes/easydynamics_list.py index de6be332..b45248b3 100644 --- a/src/easydynamics/base_classes/easydynamics_list.py +++ b/src/easydynamics/base_classes/easydynamics_list.py @@ -12,19 +12,16 @@ from easydynamics.base_classes.easydynamics_modelbase import EasyDynamicsModelBase from easydynamics.exceptions import AmbiguousNameError -# Need to fix type hints for protected types... -ProtectedType_ = TypeVar('ProtectedType', bound=EasyDynamicsBase | EasyDynamicsModelBase) +ProtectedType_ = TypeVar('T', bound=EasyDynamicsBase | EasyDynamicsModelBase) -ProtectedType = type[EasyDynamicsBase | EasyDynamicsModelBase] - -class EasyDynamicsList(EasyList): +class EasyDynamicsList(EasyList[ProtectedType_]): """Base class for all EasyDynamics lists.""" def __init__( self, *args: ProtectedType_ | list[ProtectedType_], - protected_types: list[ProtectedType] | ProtectedType | None = None, + protected_types: (list[type[ProtectedType_]] | type[ProtectedType_] | None) = None, display_name: str | None = None, unique_name: str | None = None, **kwargs: object, @@ -37,7 +34,7 @@ def __init__( *args : ProtectedType_ | list[ProtectedType_] Initial items to add to the list. Can be a single item or a list of items. Each item must be an instance of one of the protected types. - protected_types : list[ProtectedType] | ProtectedType | None, default=None + protected_types : list[type[ProtectedType_]] | type[ProtectedType_] | None, default=None Types that are allowed in the list. Can be a single EasyDynamicsBase or EasyDynamicsModelBase subclass or a list of them. If None, defaults to [EasyDynamicsBase].