From 5885b2a5a80115a4cd39ca43314e417578a03609 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Tue, 24 Feb 2026 14:54:20 +0100 Subject: [PATCH 01/12] Update docstrings of ModelComponent --- .../components/model_component.py | 104 +++++++++++++----- 1 file changed, 77 insertions(+), 27 deletions(-) diff --git a/src/easydynamics/sample_model/components/model_component.py b/src/easydynamics/sample_model/components/model_component.py index 1ffdd0ce..214c0ab5 100644 --- a/src/easydynamics/sample_model/components/model_component.py +++ b/src/easydynamics/sample_model/components/model_component.py @@ -16,11 +16,21 @@ class ModelComponent(ModelBase): - """Abstract base class for all model components.""" + """ + Abstract base class for all model components. + + Args: + unit (str or sc.Unit): The unit of the model component. + Default is 'meV'. + display_name (str, optional): A human-readable name for the + component. Default is None. + unique_name (str, optional): A unique identifier for the + component. Default is None. + """ def __init__( self, - unit: str | sc.Unit = 'meV', + unit: str | sc.Unit = "meV", display_name: str | None = None, unique_name: str | None = None, ): @@ -30,38 +40,69 @@ def __init__( @property def unit(self) -> str: - """Get the unit. + """ + Get the unit. - :return: Unit as a string. + Returns: + str: The unit of the model component. """ return str(self._unit) @unit.setter def unit(self, unit_str: str) -> None: + """ + Unit is read-only. Use convert_unit to change the unit between + allowed types or create a new ModelComponent with the desired + unit. + + Args: + unit_str (str): The new unit to set. + + 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.' + f"Unit is read-only. Use convert_unit to change the unit between allowed types " + f"or create a new {self.__class__.__name__} with the desired unit." ) ) # noqa: E501 def fix_all_parameters(self): - """Fix all parameters in the model component.""" + """ + Fix all parameters in the model component. + """ pars = self.get_fittable_parameters() for p in pars: p.fixed = True def free_all_parameters(self): - """Free all parameters in the model component.""" + """ + Free all parameters in the model component. + """ for p in self.get_fittable_parameters(): p.fixed = False def _prepare_x_for_evaluate( self, x: Numeric | List[Numeric] | np.ndarray | sc.Variable | sc.DataArray ) -> np.ndarray: - """Prepare the input x for evaluation by handling units and + """ + Prepare the input x for evaluation by handling units and converting to a numpy array. + + Args: + x (Numeric or List[Numeric] or np.ndarray or sc.Variable or + sc.DataArray): The input data to prepare. + + Returns: + np.ndarray: The prepared input data as a numpy array. + + Raises: + ValueError: If x contains NaN or infinite values, or if a + sc.DataArray has more than one coordinate. + UnitError: If x has incompatible units that cannot be + converted to the component's unit. """ # Handle units @@ -70,10 +111,10 @@ def _prepare_x_for_evaluate( coords = dict(x.coords) ncoords = len(coords) if ncoords != 1: - coord_names = ', '.join(coords.keys()) + coord_names = ", ".join(coords.keys()) raise ValueError( - f'scipp.DataArray must have exactly one coordinate to be used as input `x`. ' - f'Found {ncoords} coordinates: {coord_names}.' + f"scipp.DataArray must have exactly one coordinate to be used as input `x`. " + f"Found {ncoords} coordinates: {coord_names}." ) # get the coordinate, it's a sc.Variable coord_name, coord_obj = next(iter(coords.items())) @@ -91,15 +132,15 @@ def _prepare_x_for_evaluate( self.convert_unit(x.unit.name) except Exception as e: raise UnitError( - f'Input x has unit {x.unit}, but {self.__class__.__name__} component \ + f"Input x has unit {x.unit}, but {self.__class__.__name__} component \ has unit {self._unit}. \ - Failed to convert {self.__class__.__name__} to {x.unit}.' + Failed to convert {self.__class__.__name__} to {x.unit}." ) from e warnings.warn( - f'Input x has unit {x.unit}, but {self.__class__.__name__} component \ + f"Input x has unit {x.unit}, but {self.__class__.__name__} component \ has unit {self_unit_for_warning}. \ - Converting {self.__class__.__name__} to {x.unit}.' + Converting {self.__class__.__name__} to {x.unit}." ) else: x_in = x @@ -110,25 +151,29 @@ def _prepare_x_for_evaluate( x_in = np.array(x_in) if any(np.isnan(x_in)): - raise ValueError('Input x contains NaN values.') + raise ValueError("Input x contains NaN values.") if any(np.isinf(x_in)): - raise ValueError('Input x contains infinite values.') + raise ValueError("Input x contains infinite values.") return np.sort(x_in) @staticmethod def validate_unit(unit) -> None: - """Raise TypeError if unit is not allowed (string or - sc.Unit). + """ + Validate that the unit is either a string or a scipp Unit. + + Raises: + TypeError: If unit is not a string or scipp Unit. """ if unit is not None and not isinstance(unit, (str, sc.Unit)): raise TypeError( - f'unit must be None, a string, or a scipp Unit, got {type(unit).__name__}' + f"unit must be None, a string, or a scipp Unit, got {type(unit).__name__}" ) def convert_unit(self, unit: str | sc.Unit): - """Convert the unit of the Parameters in the component. + """ + Convert the unit of the Parameters in the component. Args: unit (str or sc.Unit): The new unit to convert to. @@ -144,18 +189,23 @@ def convert_unit(self, unit: str | sc.Unit): # Attempt to rollback on failure try: for p in pars: - if hasattr(p, 'convert_unit'): + if hasattr(p, "convert_unit"): p.convert_unit(old_unit) except Exception: # noqa: S110 pass # Best effort rollback raise e @abstractmethod - def evaluate(self, x: Numeric | sc.Variable) -> np.ndarray: - """Evaluate the model component at input x. + def evaluate( + self, x: Numeric | List[Numeric] | np.ndarray | sc.Variable | sc.DataArray + ) -> np.ndarray: + """ + Abstract method to evaluate the model component at input x. Must + be implemented by subclasses. Args: - x (Numeric | sc.Variable): Input values. + x (Numeric or List[Numeric] or np.ndarray or sc.Variable or + sc.DataArray): Input values. Returns: np.ndarray: Evaluated function values. @@ -163,4 +213,4 @@ def evaluate(self, x: Numeric | sc.Variable) -> np.ndarray: pass def __repr__(self): - return f'{self.__class__.__name__}(unique_name={self.unique_name}, unit={self._unit})' + return f"{self.__class__.__name__}(unique_name={self.unique_name}, unit={self._unit})" From c2856d27bbb0ce2765979399c85f9886693795d3 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Tue, 24 Feb 2026 16:23:23 +0100 Subject: [PATCH 02/12] Update DHO and delta function docstring --- .../components/damped_harmonic_oscillator.py | 124 +++++++++++++----- .../sample_model/components/delta_function.py | 62 ++++++--- 2 files changed, 135 insertions(+), 51 deletions(-) diff --git a/src/easydynamics/sample_model/components/damped_harmonic_oscillator.py b/src/easydynamics/sample_model/components/damped_harmonic_oscillator.py index 2bedeb76..380f8e5b 100644 --- a/src/easydynamics/sample_model/components/damped_harmonic_oscillator.py +++ b/src/easydynamics/sample_model/components/damped_harmonic_oscillator.py @@ -14,19 +14,22 @@ class DampedHarmonicOscillator(CreateParametersMixin, ModelComponent): - """ - Damped Harmonic Oscillator (DHO). - 2*area*center^2*width/pi / ( (x^2 - center^2)^2 + (2*width*x)^2 ) + r""" + Model of a Damped Harmonic Oscillator (DHO). + + The intensity is given by + $I(x) = 2*A*x_0^2*\gamma/\pi / ( (x^2 - x_0^2)^2 + (2*\gamma*x)^2 )$ + Args: - display_name (str): Display name of the component. + area (Int or float): Area under the curve. center (Int or float): Resonance frequency, approximately the - peak position. + peak position. width (Int or float): Damping constant, approximately the - half width at half max (HWHM) of the peaks. - area (Int or float): Area under the curve. + half width at half max (HWHM) of the peaks. unit (str or sc.Unit): Unit of the parameters. - Defaults to "meV". + Defaults to "meV". + display_name (str): Display name of the component. """ def __init__( @@ -34,8 +37,8 @@ def __init__( area: Numeric | Parameter = 1.0, center: Numeric | Parameter = 1.0, width: Numeric | Parameter = 1.0, - unit: str | sc.Unit = 'meV', - display_name: str | None = 'DampedHarmonicOscillator', + unit: str | sc.Unit = "meV", + display_name: str | None = "DampedHarmonicOscillator", unique_name: str | None = None, ): super().__init__( @@ -45,7 +48,9 @@ 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=display_name, unit=self._unit + ) center = self._create_center_parameter( center=center, name=display_name, @@ -54,7 +59,9 @@ def __init__( 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=display_name, unit=self._unit + ) self._area = area self._center = center @@ -62,62 +69,115 @@ def __init__( @property def area(self) -> Parameter: - """Get the area parameter.""" + """ + Get the area parameter. + + Returns: + Parameter: The area parameter. + """ return self._area @area.setter def area(self, value: Numeric) -> None: - """Set the area parameter value.""" + """ + Set the value of the area parameter. + """ if not isinstance(value, Numeric): - raise TypeError('area must be a number') + raise TypeError("area must be a number") self._area.value = value @property def center(self) -> Parameter: - """Get the center parameter.""" + """ + Get the center parameter. + + Returns: + Parameter: The center parameter. + """ return self._center @center.setter def center(self, value: Numeric) -> None: - """Set the center parameter value.""" + """ + Set the value of the center parameter. + + Args: + value (Numeric): The new value for the center parameter. + + Raises: + TypeError: If the value is not a number. + ValueError: If the value is not positive. + """ if not isinstance(value, Numeric): - raise TypeError('center must be a number') + raise TypeError("center must be a number") if value <= 0: - raise ValueError('center must be positive') + raise ValueError("center must be positive") self._center.value = value @property def width(self) -> Parameter: - """Get the width parameter.""" + """ + Get the width parameter. + + Returns: + Parameter: The width parameter. + """ return self._width @width.setter def width(self, value: Numeric) -> None: - """Set the width parameter value.""" + """ + Set the value of the width parameter. + + Args: + value (Numeric): The new value for the width parameter. + + Raises: + TypeError: If the value is not a number. + ValueError: If the value is not positive. + """ if not isinstance(value, Numeric): - raise TypeError('width must be a number') + raise TypeError("width must be a number") self._width.value = value - def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray: - """Evaluate the Damped Harmonic Oscillator at the given x - values. + def evaluate( + self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray + ) -> np.ndarray: + r""" + Evaluate the Damped Harmonic Oscillator at the given x values. If x is a scipp Variable, the unit of the DHO will be converted - to match x. The DHO evaluates to - 2*area*center^2*width/pi / ((x^2 - center^2)^2 + (2*width*x)^2) + to match x. The intensity is given by $I(x) = + 2*A*x_0^2*\gamma/\pi / ( (x^2 - x_0^2)^2 + (2*\gamma*x)^2 )$ + + Args: + x (Numeric or list or np.ndarray or sc.Variable or + sc.DataArray): + The x values at which to evaluate the DHO. + + Returns: + np.ndarray: The intensity of the DHO at the given x values. """ x = self._prepare_x_for_evaluate(x) normalization = 2 * self.center.value**2 * self.width.value / np.pi # No division by zero here, width>0 enforced in setter - denominator = (x**2 - self.center.value**2) ** 2 + (2 * self.width.value * x) ** 2 + denominator = (x**2 - self.center.value**2) ** 2 + ( + 2 * self.width.value * x + ) ** 2 return self.area.value * normalization / (denominator) def __repr__(self): - return ( - f'DampedHarmonicOscillator(display_name = {self.display_name}, unit = {self._unit},\n \ - area = {self.area},\n center = {self.center},\n width = {self.width})' - ) + """ + Return a string representation of the Damped Harmonic + Oscillator. + + Returns: + str: A string representation of the Damped Harmonic + Oscillator. + """ + return f"DampedHarmonicOscillator(display_name = {self.display_name}, unit = {self._unit},\n \ + area = {self.area},\n center = {self.center},\n width = {self.width})" diff --git a/src/easydynamics/sample_model/components/delta_function.py b/src/easydynamics/sample_model/components/delta_function.py index 7e302886..32fb05b5 100644 --- a/src/easydynamics/sample_model/components/delta_function.py +++ b/src/easydynamics/sample_model/components/delta_function.py @@ -16,28 +16,30 @@ class DeltaFunction(CreateParametersMixin, ModelComponent): - """Delta function. Evaluates to zero everywhere, except in - convolutions, where it acts as an identity. This is handled in the - ResolutionHandler. If the center is not provided, it will be - centered at 0 and fixed, which is typically what you want in QENS. + """Delta function. + + Evaluates to zero everywhere, except in convolutions, where it acts + as an identity. This is handled by the Convolution method. If the + center is not provided, it will be centered at 0 and fixed, which is + typically what you want in QENS. Args: - center (Int or float or None): Center of the delta function. - If None, defaults to 0 and is fixed. + center (Int or float or None): Center of the delta function. If + None, defaults to 0 and is fixed. area (Int or float): Total area under the curve. unit (str or sc.Unit): Unit of the parameters. - Defaults to "meV". + Defaults to "meV". display_name (str): Name of the component. unique_name (str or None): Unique name of the component. - If None, a unique_name is automatically generated. + If None, a unique_name is automatically generated. """ def __init__( self, center: None | Numeric | Parameter = None, area: Numeric | Parameter = 1.0, - unit: str | sc.Unit = 'meV', - display_name: str | None = 'DeltaFunction', + unit: str | sc.Unit = "meV", + display_name: str | None = "DeltaFunction", unique_name: str | None = None, ): # Validate inputs and create Parameters if not given @@ -48,7 +50,9 @@ 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=display_name, unit=self._unit + ) center = self._create_center_parameter( center=center, name=display_name, fix_if_none=True, unit=self._unit ) @@ -58,19 +62,37 @@ def __init__( @property def area(self) -> Parameter: - """Get the area parameter.""" + """ + Get the area parameter. + + Returns: + Parameter: The area parameter. + """ return self._area @area.setter def area(self, value: Numeric) -> None: - """Set the area parameter value.""" + """ + Set the value of the area parameter. + + Args: + value (Numeric): The new value for the area parameter. + + Raises: + TypeError: If the value is not a number. + """ if not isinstance(value, Numeric): - raise TypeError('area must be a number') + raise TypeError("area must be a number") self._area.value = value @property def center(self) -> Parameter: - """Get the center parameter.""" + """ + Get the center parameter. + + Returns: + Parameter: The center parameter. + """ return self._center @center.setter @@ -80,10 +102,12 @@ def center(self, value: Numeric | None) -> None: value = 0.0 self._center.fixed = True if not isinstance(value, Numeric): - raise TypeError('center must be a number') + raise TypeError("center must be a number") self._center.value = value - 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 Delta function at the given x values. The Delta function evaluates to zero everywhere, except at the @@ -121,5 +145,5 @@ def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) return model def __repr__(self): - return f'DeltaFunction(unique_name = {self.unique_name}, unit = {self._unit},\n \ - area = {self.area},\n center = {self.center}' + return f"DeltaFunction(unique_name = {self.unique_name}, unit = {self._unit},\n \ + area = {self.area},\n center = {self.center}" From 2006fe136adc52276f8eb182bbebdcd13821a980 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Wed, 25 Feb 2026 08:34:45 +0100 Subject: [PATCH 03/12] More docstrings --- .../components/damped_harmonic_oscillator.py | 22 ++++++-- .../sample_model/components/delta_function.py | 54 ++++++++++++++++--- 2 files changed, 63 insertions(+), 13 deletions(-) diff --git a/src/easydynamics/sample_model/components/damped_harmonic_oscillator.py b/src/easydynamics/sample_model/components/damped_harmonic_oscillator.py index 380f8e5b..debbee67 100644 --- a/src/easydynamics/sample_model/components/damped_harmonic_oscillator.py +++ b/src/easydynamics/sample_model/components/damped_harmonic_oscillator.py @@ -22,14 +22,26 @@ class DampedHarmonicOscillator(CreateParametersMixin, ModelComponent): Args: - area (Int or float): Area under the curve. - center (Int or float): Resonance frequency, approximately the + area (Int | float): Area under the curve. + center (Int | float): Resonance frequency, approximately the peak position. - width (Int or float): Damping constant, approximately the + width (Int | float): Damping constant, approximately the half width at half max (HWHM) of the peaks. - unit (str or sc.Unit): Unit of the parameters. + unit (str | sc.Unit): Unit of the parameters. Defaults to "meV". - display_name (str): Display name of the component. + display_name (str | None): Display name of the component. + unique_name (str | None): Unique name of the component. + If None, a unique_name is automatically generated. + + Attributes: + area (Parameter): Area under the curve. + center (Parameter): Resonance frequency, approximately the + peak position. + width (Parameter): Damping constant, approximately the + half width at half max (HWHM) of the peaks. + unit (str | sc.Unit): Unit of the parameters. + display_name (str | None): Display name of the component. + unique_name (str | None): Unique name of the component. """ def __init__( diff --git a/src/easydynamics/sample_model/components/delta_function.py b/src/easydynamics/sample_model/components/delta_function.py index 32fb05b5..ac5e2488 100644 --- a/src/easydynamics/sample_model/components/delta_function.py +++ b/src/easydynamics/sample_model/components/delta_function.py @@ -16,7 +16,8 @@ class DeltaFunction(CreateParametersMixin, ModelComponent): - """Delta function. + """ + Delta function. Evaluates to zero everywhere, except in convolutions, where it acts as an identity. This is handled by the Convolution method. If the @@ -24,14 +25,21 @@ class DeltaFunction(CreateParametersMixin, ModelComponent): typically what you want in QENS. Args: - center (Int or float or None): Center of the delta function. If + center (Int | float | None): Center of the delta function. If None, defaults to 0 and is fixed. - area (Int or float): Total area under the curve. - unit (str or sc.Unit): Unit of the parameters. + area (Int | float): Total area under the curve. + unit (str | sc.Unit): Unit of the parameters. Defaults to "meV". - display_name (str): Name of the component. - unique_name (str or None): Unique name of the component. + display_name (str | None): Name of the component. + unique_name (str | None): Unique name of the component. If None, a unique_name is automatically generated. + + Attributes: + center (Parameter): Center of the delta function. + area (Parameter): Total area under the curve. + unit (str | sc.Unit): Unit of the parameters. + display_name (str | None): Name of the component. + unique_name (str | None): Unique name of the component. """ def __init__( @@ -68,6 +76,7 @@ def area(self) -> Parameter: Returns: Parameter: The area parameter. """ + return self._area @area.setter @@ -81,6 +90,7 @@ def area(self, value: Numeric) -> None: Raises: TypeError: If the value is not a number. """ + if not isinstance(value, Numeric): raise TypeError("area must be a number") self._area.value = value @@ -93,11 +103,22 @@ def center(self) -> Parameter: Returns: Parameter: The center parameter. """ + return self._center @center.setter def center(self, value: Numeric | None) -> None: - """Set the center parameter value.""" + """ + Set the center parameter value. + + Args: + value (Numeric | None): The new value for the center + parameter. If None, defaults to 0 and is fixed. + + Raises: + TypeError: If the value is not a number or None. + """ + if value is None: value = 0.0 self._center.fixed = True @@ -108,11 +129,21 @@ def center(self, value: Numeric | None) -> None: def evaluate( self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray ) -> np.ndarray: - """Evaluate the Delta function at the given x values. + """ + Evaluate the Delta function at the given x values. The Delta function evaluates to zero everywhere, except at the center. Its numerical integral is equal to the area. It acts as an identity in convolutions. + + Args: + x (Numeric | list | np.ndarray | sc.Variable | + sc.DataArray): + The x values at which to evaluate the Delta function. + + Returns: + np.ndarray: The evaluated Delta function at the given x + values. """ # x assumed sorted, 1D numpy array @@ -145,5 +176,12 @@ def evaluate( return model def __repr__(self): + """ + Return a string representation of the Delta function. + + Returns: + str: A string representation of the Delta function. + """ + return f"DeltaFunction(unique_name = {self.unique_name}, unit = {self._unit},\n \ area = {self.area},\n center = {self.center}" From 1685475bbff888560c36c7d6d6cf83bd278f4809 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Fri, 27 Feb 2026 09:25:46 +0100 Subject: [PATCH 04/12] gaussian docstring --- .../components/damped_harmonic_oscillator.py | 20 +-- .../sample_model/components/delta_function.py | 2 +- .../sample_model/components/gaussian.py | 136 +++++++++++++----- 3 files changed, 116 insertions(+), 42 deletions(-) diff --git a/src/easydynamics/sample_model/components/damped_harmonic_oscillator.py b/src/easydynamics/sample_model/components/damped_harmonic_oscillator.py index debbee67..57df353e 100644 --- a/src/easydynamics/sample_model/components/damped_harmonic_oscillator.py +++ b/src/easydynamics/sample_model/components/damped_harmonic_oscillator.py @@ -18,12 +18,14 @@ class DampedHarmonicOscillator(CreateParametersMixin, ModelComponent): Model of a Damped Harmonic Oscillator (DHO). The intensity is given by - $I(x) = 2*A*x_0^2*\gamma/\pi / ( (x^2 - x_0^2)^2 + (2*\gamma*x)^2 )$ + $I(x) = 2*A*x_0^2*\gamma/\pi / ( (x^2-x_0^2)^2 + (2*\gamma*x)^2 )$, + where $A$ is the area, $x_0$ is the center, and $\gamma$ is the + width. Args: - area (Int | float): Area under the curve. - center (Int | float): Resonance frequency, approximately the + area (Int | float): Area under the curve. center (Int | float): + Resonance frequency, approximately the peak position. width (Int | float): Damping constant, approximately the half width at half max (HWHM) of the peaks. @@ -34,14 +36,14 @@ class DampedHarmonicOscillator(CreateParametersMixin, ModelComponent): If None, a unique_name is automatically generated. Attributes: - area (Parameter): Area under the curve. - center (Parameter): Resonance frequency, approximately the + area (Parameter): Area under the curve. center (Parameter): + Resonance frequency, approximately the peak position. width (Parameter): Damping constant, approximately the half width at half max (HWHM) of the peaks. - unit (str | sc.Unit): Unit of the parameters. - display_name (str | None): Display name of the component. - unique_name (str | None): Unique name of the component. + unit (str | sc.Unit): Unit of the parameters. display_name (str + | None): Display name of the component. unique_name (str | + None): Unique name of the component. """ def __init__( @@ -182,7 +184,7 @@ def evaluate( return self.area.value * normalization / (denominator) - def __repr__(self): + def __repr__(self) -> str: """ Return a string representation of the Damped Harmonic Oscillator. diff --git a/src/easydynamics/sample_model/components/delta_function.py b/src/easydynamics/sample_model/components/delta_function.py index ac5e2488..005a5ebc 100644 --- a/src/easydynamics/sample_model/components/delta_function.py +++ b/src/easydynamics/sample_model/components/delta_function.py @@ -175,7 +175,7 @@ def evaluate( return model - def __repr__(self): + def __repr__(self) -> str: """ Return a string representation of the Delta function. diff --git a/src/easydynamics/sample_model/components/gaussian.py b/src/easydynamics/sample_model/components/gaussian.py index 196b4f23..00e017d3 100644 --- a/src/easydynamics/sample_model/components/gaussian.py +++ b/src/easydynamics/sample_model/components/gaussian.py @@ -15,21 +15,23 @@ class Gaussian(CreateParametersMixin, ModelComponent): """ - Gaussian function: - area/(width*sqrt(2pi)) * exp(-0.5*((x - center)/width)^2) - If the center is not provided, it will be centered at 0 and fixed, - which is typically what you want in QENS. + Model of a Gaussian function. + + The intensity is given by $I(x) = \frac{A}{\\sigma \\sqrt{2\\pi}} + e^{-\frac{1}{2} \\left(\frac{x - x_0}{\\sigma}\right)^2}$, + where $A$ is the area, $x_0$ is the center, and $\\sigma$ is the + width. If the center is not provided, it will be centered at 0 and + fixed, which is typically what you want in QENS. Args: - area (Int, float or Parameter): Area of the Gaussian. - center (Int, float, None or Parameter): Center of the Gaussian. - If None, defaults to 0 and is fixed - width (Int, float or Parameter): Standard deviation. - unit (str or sc.Unit): Unit of the parameters. - Defaults to "meV". - display_name (str): Name of the component. - unique_name (str or None): Unique name of the component. - If None, a unique_name is automatically generated. + area (Int | float | Parameter): Area of the Gaussian. + center (Int | float | None | Parameter): Center of the Gaussian. + If None, defaults to 0 and is fixed + width (Int | float | Parameter): Standard deviation. + unit (str | sc.Unit): Unit of the parameters. Defaults to "meV". + display_name (str | None): Name of the component. + unique_name (str | None): Unique name of the component. if None, + a unique_name is automatically generated. """ def __init__( @@ -37,8 +39,8 @@ def __init__( area: Numeric | Parameter = 1.0, center: Numeric | Parameter | None = None, width: Numeric | Parameter = 1.0, - unit: str | sc.Unit = 'meV', - display_name: str | None = 'Gaussian', + unit: str | sc.Unit = "meV", + display_name: str | None = "Gaussian", unique_name: str | None = None, ): # Validate inputs and create Parameters if not given @@ -49,11 +51,15 @@ 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=display_name, unit=self._unit + ) center = self._create_center_parameter( center=center, name=display_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=display_name, unit=self._unit + ) self._area = area self._center = center @@ -61,50 +67,108 @@ def __init__( @property def area(self) -> Parameter: - """Get the area parameter.""" + """ + Get the area parameter. + + Returns: + Parameter: The area parameter. + """ + return self._area @area.setter def area(self, value: Numeric) -> None: - """Set the area parameter value.""" + """ + Set the value of the area parameter. + + Args: + value (Numeric): The new value for the area parameter. + + Raises: + TypeError: If the value is not a number. + """ + if not isinstance(value, Numeric): - raise TypeError('area must be a number') + raise TypeError("area must be a number") self._area.value = value @property def center(self) -> Parameter: - """Get the center parameter.""" + """ + Get the center parameter. + + Returns: + Parameter: The center parameter. + """ + return self._center @center.setter def center(self, value: Numeric) -> None: - """Set the center parameter value.""" + """ + Set the center parameter value. + + Args: + value (Numeric | None): The new value for the center + parameter. If None, defaults to 0 and is fixed. + + Raises: + TypeError: If the value is not a number or None. + """ + if value is None: value = 0.0 self._center.fixed = True if not isinstance(value, Numeric): - raise TypeError('center must be a number') + raise TypeError("center must be a number") self._center.value = value @property def width(self) -> Parameter: - """Get the width parameter.""" + """ + Get the width parameter. + + Returns: + Parameter: The width parameter. + """ return self._width @width.setter def width(self, value: Numeric) -> None: - """Set the width parameter value.""" + """ + Set the center parameter value. + + Args: + value (Numeric | None): The new value for the center + parameter. If None, defaults to 0 and is fixed. + + Raises: + TypeError: If the value is not a number or None. + """ if not isinstance(value, Numeric): - raise TypeError('width must be a number') + raise TypeError("width must be a number") self._width.value = value - 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 Gaussian at the given x values. If x is a scipp Variable, the unit of the Gaussian will be converted to match x. - The Gaussian evaluates to - area/(width*sqrt(2pi)) * exp(-0.5*((x - center)/width)^2) + The intensity is given by $I(x) = \frac{A} + {\\sigma \\sqrt{2\\pi}} + e^{-\frac{1}{2} \\left(\frac{x - x_0}{\\sigma}\right)^2}$, + + Args: + x (Numeric or list or np.ndarray or sc.Variable or + sc.DataArray): + The x values at which to evaluate the Gaussian. + + Returns: + np.ndarray: The intensity of the Gaussian at the given x + values. """ x = self._prepare_x_for_evaluate(x) @@ -114,6 +178,14 @@ def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) return self.area.value * normalization * np.exp(exponent) - def __repr__(self): - return f'Gaussian(unique_name = {self.unique_name}, unit = {self._unit},\n \ - area = {self.area},\n center = {self.center},\n width = {self.width})' + def __repr__(self) -> str: + """ + Return a string representation of the Gaussian. + + Returns: + str: A string representation of the Gaussian. + + """ + + return f"Gaussian(unique_name = {self.unique_name}, unit = {self._unit},\n \ + area = {self.area},\n center = {self.center},\n width = {self.width})" From f6aff891bff28fc9d877689fbd5b09076072ffc5 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Fri, 27 Feb 2026 11:41:12 +0100 Subject: [PATCH 05/12] Update ModelComponent docstrings and add a few tests --- .../components/damped_harmonic_oscillator.py | 69 +++++----- .../sample_model/components/delta_function.py | 41 +++--- .../sample_model/components/gaussian.py | 64 +++++----- .../sample_model/components/lorentzian.py | 111 ++++++++++++---- .../sample_model/components/mixins.py | 37 +++--- .../components/model_component.py | 88 ++++++------- .../sample_model/components/polynomial.py | 75 ++++++++++- .../sample_model/components/voigt.py | 118 +++++++++++++++--- tests/conftest.py | 10 +- .../sample_model/components/test_gaussian.py | 5 + .../components/test_lorentzian.py | 5 + .../sample_model/components/test_voigt.py | 13 ++ 12 files changed, 425 insertions(+), 211 deletions(-) diff --git a/src/easydynamics/sample_model/components/damped_harmonic_oscillator.py b/src/easydynamics/sample_model/components/damped_harmonic_oscillator.py index 57df353e..ab45ff05 100644 --- a/src/easydynamics/sample_model/components/damped_harmonic_oscillator.py +++ b/src/easydynamics/sample_model/components/damped_harmonic_oscillator.py @@ -14,8 +14,7 @@ class DampedHarmonicOscillator(CreateParametersMixin, ModelComponent): - r""" - Model of a Damped Harmonic Oscillator (DHO). + r"""Model of a Damped Harmonic Oscillator (DHO). The intensity is given by $I(x) = 2*A*x_0^2*\gamma/\pi / ( (x^2-x_0^2)^2 + (2*\gamma*x)^2 )$, @@ -51,8 +50,8 @@ def __init__( area: Numeric | Parameter = 1.0, center: Numeric | Parameter = 1.0, width: Numeric | Parameter = 1.0, - unit: str | sc.Unit = "meV", - display_name: str | None = "DampedHarmonicOscillator", + unit: str | sc.Unit = 'meV', + display_name: str | None = 'DampedHarmonicOscillator', unique_name: str | None = None, ): super().__init__( @@ -62,9 +61,7 @@ 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=display_name, unit=self._unit) center = self._create_center_parameter( center=center, name=display_name, @@ -73,9 +70,7 @@ def __init__( 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=display_name, unit=self._unit) self._area = area self._center = center @@ -83,8 +78,7 @@ def __init__( @property def area(self) -> Parameter: - """ - Get the area parameter. + """Get the area parameter. Returns: Parameter: The area parameter. @@ -93,17 +87,14 @@ def area(self) -> Parameter: @area.setter def area(self, value: Numeric) -> None: - """ - Set the value of the area parameter. - """ + """Set the value of the area parameter.""" if not isinstance(value, Numeric): - raise TypeError("area must be a number") + raise TypeError('area must be a number') self._area.value = value @property def center(self) -> Parameter: - """ - Get the center parameter. + """Get the center parameter. Returns: Parameter: The center parameter. @@ -112,8 +103,7 @@ def center(self) -> Parameter: @center.setter def center(self, value: Numeric) -> None: - """ - Set the value of the center parameter. + """Set the value of the center parameter. Args: value (Numeric): The new value for the center parameter. @@ -123,16 +113,15 @@ def center(self, value: Numeric) -> None: ValueError: If the value is not positive. """ if not isinstance(value, Numeric): - raise TypeError("center must be a number") + raise TypeError('center must be a number') - if value <= 0: - raise ValueError("center must be positive") + if float(value) <= 0: + raise ValueError('center must be positive') self._center.value = value @property def width(self) -> Parameter: - """ - Get the width parameter. + """Get the width parameter. Returns: Parameter: The width parameter. @@ -141,8 +130,7 @@ def width(self) -> Parameter: @width.setter def width(self, value: Numeric) -> None: - """ - Set the value of the width parameter. + """Set the value of the width parameter. Args: value (Numeric): The new value for the width parameter. @@ -152,14 +140,16 @@ def width(self, value: Numeric) -> None: ValueError: If the value is not positive. """ if not isinstance(value, Numeric): - raise TypeError("width must be a number") + raise TypeError('width must be a number') + + if float(value) <= 0: + raise ValueError('width must be positive') + self._width.value = value - def evaluate( - self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray - ) -> np.ndarray: - r""" - Evaluate the Damped Harmonic Oscillator at the given x values. + def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray: + r"""Evaluate the Damped Harmonic Oscillator at the given x + values. If x is a scipp Variable, the unit of the DHO will be converted to match x. The intensity is given by $I(x) = @@ -178,20 +168,19 @@ def evaluate( normalization = 2 * self.center.value**2 * self.width.value / np.pi # No division by zero here, width>0 enforced in setter - denominator = (x**2 - self.center.value**2) ** 2 + ( - 2 * self.width.value * x - ) ** 2 + denominator = (x**2 - self.center.value**2) ** 2 + (2 * self.width.value * x) ** 2 return self.area.value * normalization / (denominator) def __repr__(self) -> str: - """ - Return a string representation of the Damped Harmonic + """Return a string representation of the Damped Harmonic Oscillator. Returns: str: A string representation of the Damped Harmonic Oscillator. """ - return f"DampedHarmonicOscillator(display_name = {self.display_name}, unit = {self._unit},\n \ - area = {self.area},\n center = {self.center},\n width = {self.width})" + return ( + f'DampedHarmonicOscillator(display_name = {self.display_name}, unit = {self._unit},\n \ + area = {self.area},\n center = {self.center},\n width = {self.width})' + ) diff --git a/src/easydynamics/sample_model/components/delta_function.py b/src/easydynamics/sample_model/components/delta_function.py index 005a5ebc..849ca5dd 100644 --- a/src/easydynamics/sample_model/components/delta_function.py +++ b/src/easydynamics/sample_model/components/delta_function.py @@ -16,8 +16,7 @@ class DeltaFunction(CreateParametersMixin, ModelComponent): - """ - Delta function. + """Delta function. Evaluates to zero everywhere, except in convolutions, where it acts as an identity. This is handled by the Convolution method. If the @@ -46,8 +45,8 @@ def __init__( self, center: None | Numeric | Parameter = None, area: Numeric | Parameter = 1.0, - unit: str | sc.Unit = "meV", - display_name: str | None = "DeltaFunction", + unit: str | sc.Unit = 'meV', + display_name: str | None = 'DeltaFunction', unique_name: str | None = None, ): # Validate inputs and create Parameters if not given @@ -58,9 +57,7 @@ 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=display_name, unit=self._unit) center = self._create_center_parameter( center=center, name=display_name, fix_if_none=True, unit=self._unit ) @@ -70,8 +67,7 @@ def __init__( @property def area(self) -> Parameter: - """ - Get the area parameter. + """Get the area parameter. Returns: Parameter: The area parameter. @@ -81,8 +77,7 @@ def area(self) -> Parameter: @area.setter def area(self, value: Numeric) -> None: - """ - Set the value of the area parameter. + """Set the value of the area parameter. Args: value (Numeric): The new value for the area parameter. @@ -92,13 +87,12 @@ def area(self, value: Numeric) -> None: """ if not isinstance(value, Numeric): - raise TypeError("area must be a number") + raise TypeError('area must be a number') self._area.value = value @property def center(self) -> Parameter: - """ - Get the center parameter. + """Get the center parameter. Returns: Parameter: The center parameter. @@ -108,8 +102,7 @@ def center(self) -> Parameter: @center.setter def center(self, value: Numeric | None) -> None: - """ - Set the center parameter value. + """Set the center parameter value. Args: value (Numeric | None): The new value for the center @@ -123,14 +116,11 @@ def center(self, value: Numeric | None) -> None: value = 0.0 self._center.fixed = True if not isinstance(value, Numeric): - raise TypeError("center must be a number") + raise TypeError('center must be a number') self._center.value = value - def evaluate( - self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray - ) -> np.ndarray: - """ - Evaluate the Delta function at the given x values. + def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray: + """Evaluate the Delta function at the given x values. The Delta function evaluates to zero everywhere, except at the center. Its numerical integral is equal to the area. It acts as @@ -176,12 +166,11 @@ def evaluate( return model def __repr__(self) -> str: - """ - Return a string representation of the Delta function. + """Return a string representation of the Delta function. Returns: str: A string representation of the Delta function. """ - return f"DeltaFunction(unique_name = {self.unique_name}, unit = {self._unit},\n \ - area = {self.area},\n center = {self.center}" + return f'DeltaFunction(unique_name = {self.unique_name}, unit = {self._unit},\n \ + area = {self.area},\n center = {self.center}' diff --git a/src/easydynamics/sample_model/components/gaussian.py b/src/easydynamics/sample_model/components/gaussian.py index 00e017d3..771a24c0 100644 --- a/src/easydynamics/sample_model/components/gaussian.py +++ b/src/easydynamics/sample_model/components/gaussian.py @@ -14,8 +14,7 @@ class Gaussian(CreateParametersMixin, ModelComponent): - """ - Model of a Gaussian function. + """Model of a Gaussian function. The intensity is given by $I(x) = \frac{A}{\\sigma \\sqrt{2\\pi}} e^{-\frac{1}{2} \\left(\frac{x - x_0}{\\sigma}\right)^2}$, @@ -32,6 +31,14 @@ class Gaussian(CreateParametersMixin, ModelComponent): display_name (str | None): Name of the component. unique_name (str | None): Unique name of the component. if None, a unique_name is automatically generated. + + Attributes: + area (Parameter): Area of the Gaussian. + center (Parameter): Center of the Gaussian. + width (Parameter): Standard deviation of the Gaussian. + unit (str | sc.Unit): Unit of the parameters. + display_name (str | None): Name of the component. + unique_name (str | None): Unique name of the component. """ def __init__( @@ -39,8 +46,8 @@ def __init__( area: Numeric | Parameter = 1.0, center: Numeric | Parameter | None = None, width: Numeric | Parameter = 1.0, - unit: str | sc.Unit = "meV", - display_name: str | None = "Gaussian", + unit: str | sc.Unit = 'meV', + display_name: str | None = 'Gaussian', unique_name: str | None = None, ): # Validate inputs and create Parameters if not given @@ -51,15 +58,11 @@ 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=display_name, unit=self._unit) center = self._create_center_parameter( center=center, name=display_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=display_name, unit=self._unit) self._area = area self._center = center @@ -67,8 +70,7 @@ def __init__( @property def area(self) -> Parameter: - """ - Get the area parameter. + """Get the area parameter. Returns: Parameter: The area parameter. @@ -78,8 +80,7 @@ def area(self) -> Parameter: @area.setter def area(self, value: Numeric) -> None: - """ - Set the value of the area parameter. + """Set the value of the area parameter. Args: value (Numeric): The new value for the area parameter. @@ -89,13 +90,12 @@ def area(self, value: Numeric) -> None: """ if not isinstance(value, Numeric): - raise TypeError("area must be a number") + raise TypeError('area must be a number') self._area.value = value @property def center(self) -> Parameter: - """ - Get the center parameter. + """Get the center parameter. Returns: Parameter: The center parameter. @@ -105,8 +105,7 @@ def center(self) -> Parameter: @center.setter def center(self, value: Numeric) -> None: - """ - Set the center parameter value. + """Set the center parameter value. Args: value (Numeric | None): The new value for the center @@ -120,13 +119,12 @@ def center(self, value: Numeric) -> None: value = 0.0 self._center.fixed = True if not isinstance(value, Numeric): - raise TypeError("center must be a number") + raise TypeError('center must be a number') self._center.value = value @property def width(self) -> Parameter: - """ - Get the width parameter. + """Get the width parameter (standard deviation). Returns: Parameter: The width parameter. @@ -135,18 +133,22 @@ def width(self) -> Parameter: @width.setter def width(self, value: Numeric) -> None: - """ - Set the center parameter value. + """Set the width parameter value. Args: - value (Numeric | None): The new value for the center - parameter. If None, defaults to 0 and is fixed. + value (Numeric | None): The new value for the width + parameter. Raises: TypeError: If the value is not a number or None. + ValueError: If the value is not positive. """ if not isinstance(value, Numeric): - raise TypeError("width must be a number") + raise TypeError('width must be a number') + + if float(value) <= 0: + raise ValueError('width must be positive') + self._width.value = value def evaluate( @@ -179,13 +181,11 @@ def evaluate( return self.area.value * normalization * np.exp(exponent) def __repr__(self) -> str: - """ - Return a string representation of the Gaussian. + """Return a string representation of the Gaussian. Returns: str: A string representation of the Gaussian. - """ - return f"Gaussian(unique_name = {self.unique_name}, unit = {self._unit},\n \ - area = {self.area},\n center = {self.center},\n width = {self.width})" + return f'Gaussian(unique_name = {self.unique_name}, unit = {self._unit},\n \ + area = {self.area},\n center = {self.center},\n width = {self.width})' diff --git a/src/easydynamics/sample_model/components/lorentzian.py b/src/easydynamics/sample_model/components/lorentzian.py index 8b5c6ab7..05c8f696 100644 --- a/src/easydynamics/sample_model/components/lorentzian.py +++ b/src/easydynamics/sample_model/components/lorentzian.py @@ -14,22 +14,34 @@ class Lorentzian(CreateParametersMixin, ModelComponent): - """ - Lorentzian function: - area*width / (pi * ( (x - center)^2 + width^2 ) ) - If the center is not provided, it will be centered at 0 and fixed, - which is typically what you want in QENS. + """Model of a Lorentzian function. + + The intensity is given by $I(x) = \frac{A}{\\pi} \frac{\\Gamma}{(x - + x_0)^2 + \\Gamma^2}$, where $A$ is the area, $x_0$ is the center, + and $\\Gamma$ is the half width at half maximum (HWHM). + + If the center is not provided, it will be centered at 0 + and fixed, which is typically what you want in QENS. Args: - area (Int, float or Parameter): Area of the Lorentzian. - center (Int, float, None or Parameter): Peak center. - If None, defaults to 0 and is fixed. - width (Int, float or Parameter): - Half Width at Half Maximum (HWHM) - unit (str or sc.Unit): Unit of the parameters. Defaults to "meV" - display_name (str): Display name of the component. - unique_name (str or None): Unique name of the component. - If None, a unique_name is automatically generated. + area (Int | float | Parameter): Area of the Lorentzian. + center (Int | float | None | Parameter): Center of the + Lorentzian. If None, defaults to 0 and is fixed + width (Int | float | Parameter): Half width at half maximum + (HWHM). + unit (str | sc.Unit): Unit of the parameters. Defaults to "meV". + display_name (str | None): Name of the component. + unique_name (str | None): Unique name of the component. if None, + a unique_name is automatically generated. + + Attributes: + area (Parameter): Area of the Lorentzian. + center (Parameter): Center of the Lorentzian. + width (Parameter): Half width at half maximum (HWHM) of the + Lorentzian. + unit (str | sc.Unit): Unit of the parameters. + display_name (str | None): Name of the component. + unique_name (str | None): Unique name of the component. """ def __init__( @@ -60,24 +72,48 @@ def __init__( @property def area(self) -> Parameter: - """Get the area parameter.""" + """Get the area parameter. + + Returns: + Parameter: The area parameter. + """ return self._area @area.setter def area(self, value: Numeric) -> None: - """Set the area parameter value.""" + """Set the value of the area parameter. + + Args: + value (Numeric): The new value for the area parameter. + + Raises: + TypeError: If the value is not a number. + """ if not isinstance(value, Numeric): raise TypeError('area must be a number') self._area.value = value @property def center(self) -> Parameter: - """Get the center parameter.""" + """Get the center parameter. + + Returns: + Parameter: The center parameter. + """ return self._center @center.setter def center(self, value: Numeric | None) -> None: - """Set the center parameter value.""" + """Set the value of the center parameter. + + Args: + value (Numeric | None): The new value for the center + parameter. If None, defaults to 0 and is fixed. + + Raises: + TypeError: If the value is not a number or None. + """ + if value is None: value = 0.0 self._center.fixed = True @@ -87,14 +123,30 @@ def center(self, value: Numeric | None) -> None: @property def width(self) -> Parameter: - """Get the width parameter.""" + """Get the width parameter (HWHM). + + Returns: + Parameter: The width parameter. + """ return self._width @width.setter def width(self, value: Numeric) -> None: - """Set the width parameter value.""" + """Set the width parameter value (HWHM). + + Args: + value (Numeric | None): The new value for the width + parameter. + + Raises: + TypeError: If the value is not a number or None. + ValueError: If the value is not positive. + """ if not isinstance(value, Numeric): raise TypeError('width must be a number') + + if float(value) <= 0: + raise ValueError('width must be positive') self._width.value = value def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray: @@ -102,8 +154,18 @@ def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) If x is a scipp Variable, the unit of the Lorentzian will be converted to match x. - The Lorentzian evaluates to - area*width / (pi * ( (x - center)^2 + width^2 ) ) + $I(x) = \frac{A}{\\pi} \frac{\\Gamma}{(x - + x_0)^2 + \\Gamma^2}$, where $A$ is the area, $x_0$ is the + center, and $\\Gamma$ is the half width at half maximum (HWHM). + + Args: + x (Numeric or list or np.ndarray or sc.Variable or + sc.DataArray): + The x values at which to evaluate the Lorentzian. + + Returns: + np.ndarray: The intensity of the Lorentzian at the given x + values. """ x = self._prepare_x_for_evaluate(x) @@ -114,5 +176,10 @@ def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) return self.area.value * normalization / denominator def __repr__(self): + """Return a string representation of the Lorentzian. + + Returns: + str: A string representation of the Lorentzian. + """ return f'Lorentzian(unique_name = {self.unique_name}, unit = {self._unit},\n \ area = {self.area},\n center = {self.center},\n width = {self.width})' diff --git a/src/easydynamics/sample_model/components/mixins.py b/src/easydynamics/sample_model/components/mixins.py index 9c98e7e8..b8bb8b47 100644 --- a/src/easydynamics/sample_model/components/mixins.py +++ b/src/easydynamics/sample_model/components/mixins.py @@ -36,14 +36,17 @@ def _create_area_parameter( If the area is negative, a warning is raised. If the area is non-negative, its minimum is set to 0 to avoid it accidentally becoming negative during fitting. - args: - area (Numeric or Parameter): The area value or Parameter. + + Args: + area (Numeric | Parameter): The area value or Parameter. name (str): The name of the model component. - unit (str or sc.Unit): The unit of the area Parameter. + unit (str | sc.Unit): The unit of the area Parameter. minimum_area (float): The minimum allowed area. - returns: + + Returns: Parameter: The validated area Parameter. - raises: + + Raises: TypeError: If area is not a number or a Parameter. Warning: If area is negative. """ @@ -76,16 +79,18 @@ def _create_center_parameter( """Validate and convert a number to a Parameter describing the center of a function. - args: - center (Numeric, Parameter, or None): The center value or - Parameter. + Args: + center (Numeric | Parameter | None): The center value or + Parameter. name (str): The name of the model component. fix_if_none (bool): Whether to fix the center Parameter - if center is None. - unit (str or sc.Unit): The unit of the center Parameter. - returns: + if center is None. + unit (str | sc.Unit): The unit of the center Parameter. + + Returns: Parameter: The validated center Parameter. - raises: + + Raises: TypeError: If center is not None, a number, or a Parameter. """ if center is not None and not isinstance(center, (Numeric, Parameter)): @@ -118,15 +123,17 @@ def _create_width_parameter( """Validate and convert a number to a Parameter describing the width of a function. - args: + Args: width (Numeric or Parameter): The width value or Parameter. name (str): The name of the model component. param_name (str): The name of the width parameter. unit (str or sc.Unit): The unit of the width Parameter. minimum_width (float): The minimum allowed width. - returns: + + Returns: Parameter: The validated width Parameter. - raises: + + Raises: TypeError: If width is not a number or a Parameter. ValueError: If width is non-positive. """ diff --git a/src/easydynamics/sample_model/components/model_component.py b/src/easydynamics/sample_model/components/model_component.py index 214c0ab5..b5aab33c 100644 --- a/src/easydynamics/sample_model/components/model_component.py +++ b/src/easydynamics/sample_model/components/model_component.py @@ -16,21 +16,27 @@ class ModelComponent(ModelBase): - """ - Abstract base class for all model components. + """Abstract base class for all model components. Args: - unit (str or sc.Unit): The unit of the model component. + unit (str | sc.Unit): The unit of the model component. Default is 'meV'. - display_name (str, optional): A human-readable name for the + display_name (str | None): A human-readable name for the component. Default is None. - unique_name (str, optional): A unique identifier for the + unique_name (str | None): A unique identifier for the component. Default is None. + + Attributes: + unit (str): The unit of the model component. + display_name (str | None): A human-readable name for the + component. + unique_name (str | None): A unique identifier for the + component. """ def __init__( self, - unit: str | sc.Unit = "meV", + unit: str | sc.Unit = 'meV', display_name: str | None = None, unique_name: str | None = None, ): @@ -40,8 +46,7 @@ def __init__( @property def unit(self) -> str: - """ - Get the unit. + """Get the unit. Returns: str: The unit of the model component. @@ -50,10 +55,9 @@ def unit(self) -> str: @unit.setter def unit(self, unit_str: str) -> None: - """ - Unit is read-only. Use convert_unit to change the unit between - allowed types or create a new ModelComponent with the desired - unit. + """Unit is read-only. Use convert_unit to change the unit + between allowed types or create a new ModelComponent with the + desired unit. Args: unit_str (str): The new unit to set. @@ -63,37 +67,32 @@ def unit(self, unit_str: str) -> None: """ raise AttributeError( ( - f"Unit is read-only. Use convert_unit to change the unit between allowed types " - f"or create a new {self.__class__.__name__} with the desired unit." + f'Unit is read-only. Use convert_unit to change the unit between allowed types ' + f'or create a new {self.__class__.__name__} with the desired unit.' ) ) # noqa: E501 def fix_all_parameters(self): - """ - Fix all parameters in the model component. - """ + """Fix all parameters in the model component.""" pars = self.get_fittable_parameters() for p in pars: p.fixed = True def free_all_parameters(self): - """ - Free all parameters in the model component. - """ + """Free all parameters in the model component.""" for p in self.get_fittable_parameters(): p.fixed = False def _prepare_x_for_evaluate( self, x: Numeric | List[Numeric] | np.ndarray | sc.Variable | sc.DataArray ) -> np.ndarray: - """ - Prepare the input x for evaluation by handling units and + """Prepare the input x for evaluation by handling units and converting to a numpy array. Args: - x (Numeric or List[Numeric] or np.ndarray or sc.Variable or - sc.DataArray): The input data to prepare. + x (Numeric | List[Numeric] | np.ndarray | sc.Variable | + sc.DataArray): The input data to prepare. Returns: np.ndarray: The prepared input data as a numpy array. @@ -111,10 +110,10 @@ def _prepare_x_for_evaluate( coords = dict(x.coords) ncoords = len(coords) if ncoords != 1: - coord_names = ", ".join(coords.keys()) + coord_names = ', '.join(coords.keys()) raise ValueError( - f"scipp.DataArray must have exactly one coordinate to be used as input `x`. " - f"Found {ncoords} coordinates: {coord_names}." + f'scipp.DataArray must have exactly one coordinate to be used as input `x`. ' + f'Found {ncoords} coordinates: {coord_names}.' ) # get the coordinate, it's a sc.Variable coord_name, coord_obj = next(iter(coords.items())) @@ -132,15 +131,15 @@ def _prepare_x_for_evaluate( self.convert_unit(x.unit.name) except Exception as e: raise UnitError( - f"Input x has unit {x.unit}, but {self.__class__.__name__} component \ + f'Input x has unit {x.unit}, but {self.__class__.__name__} component \ has unit {self._unit}. \ - Failed to convert {self.__class__.__name__} to {x.unit}." + Failed to convert {self.__class__.__name__} to {x.unit}.' ) from e warnings.warn( - f"Input x has unit {x.unit}, but {self.__class__.__name__} component \ + f'Input x has unit {x.unit}, but {self.__class__.__name__} component \ has unit {self_unit_for_warning}. \ - Converting {self.__class__.__name__} to {x.unit}." + Converting {self.__class__.__name__} to {x.unit}.' ) else: x_in = x @@ -151,29 +150,27 @@ def _prepare_x_for_evaluate( x_in = np.array(x_in) if any(np.isnan(x_in)): - raise ValueError("Input x contains NaN values.") + raise ValueError('Input x contains NaN values.') if any(np.isinf(x_in)): - raise ValueError("Input x contains infinite values.") + raise ValueError('Input x contains infinite values.') return np.sort(x_in) @staticmethod def validate_unit(unit) -> None: - """ - Validate that the unit is either a string or a scipp Unit. + """Validate that the unit is either a string or a scipp Unit. Raises: TypeError: If unit is not a string or scipp Unit. """ if unit is not None and not isinstance(unit, (str, sc.Unit)): raise TypeError( - f"unit must be None, a string, or a scipp Unit, got {type(unit).__name__}" + f'unit must be None, a string, or a scipp Unit, got {type(unit).__name__}' ) def convert_unit(self, unit: str | sc.Unit): - """ - Convert the unit of the Parameters in the component. + """Convert the unit of the Parameters in the component. Args: unit (str or sc.Unit): The new unit to convert to. @@ -189,7 +186,7 @@ def convert_unit(self, unit: str | sc.Unit): # Attempt to rollback on failure try: for p in pars: - if hasattr(p, "convert_unit"): + if hasattr(p, 'convert_unit'): p.convert_unit(old_unit) except Exception: # noqa: S110 pass # Best effort rollback @@ -199,9 +196,8 @@ def convert_unit(self, unit: str | sc.Unit): def evaluate( self, x: Numeric | List[Numeric] | np.ndarray | sc.Variable | sc.DataArray ) -> np.ndarray: - """ - Abstract method to evaluate the model component at input x. Must - be implemented by subclasses. + """Abstract method to evaluate the model component at input x. + Must be implemented by subclasses. Args: x (Numeric or List[Numeric] or np.ndarray or sc.Variable or @@ -213,4 +209,10 @@ def evaluate( pass def __repr__(self): - return f"{self.__class__.__name__}(unique_name={self.unique_name}, unit={self._unit})" + """Return a string representation of the ModelComponent. + + Returns: + str: A string representation of the ModelComponent. + """ + + return f'{self.__class__.__name__}(unique_name={self.unique_name}, unit={self._unit})' diff --git a/src/easydynamics/sample_model/components/polynomial.py b/src/easydynamics/sample_model/components/polynomial.py index 43777807..f7bb74d9 100644 --- a/src/easydynamics/sample_model/components/polynomial.py +++ b/src/easydynamics/sample_model/components/polynomial.py @@ -18,15 +18,26 @@ class Polynomial(ModelComponent): - """Polynomial function component. c0 + c1*x + c2*x^2 + ... + cN*x^N. + """Polynomial function component. + + The intensity is given by $I(x) = + c_0 + c_1 x + c_2 x^2 + ... + c_N x^N$, where $C_i$ are the + coefficients. Args: coefficients (list or tuple): Coefficients c0, c1, ..., cN - representing f(x) = c0 + c1*x + c2*x^2 + ... + cN*x^N unit (str or sc.Unit): Unit of the Polynomial component. display_name (str): Display name of the Polynomial component. unique_name (str or None): Unique name of the component. If None, a unique_name is automatically generated. + + Attributes: + coefficients (list of Parameter): Coefficients of the polynomial + as Parameters. + unit (str): Unit of the Polynomial component. + display_name (str): Display name of the Polynomial component. + unique_name (str or None): Unique name of the component. + If None, a unique_name is automatically generated. """ def __init__( @@ -68,14 +79,29 @@ def __init__( def coefficients(self) -> list[Parameter]: """Get the coefficients of the polynomial as a list of Parameters. + + Returns: + list[Parameter]: The coefficients of the polynomial. """ return list(self._coefficients) @coefficients.setter def coefficients(self, coeffs: Sequence[Numeric | Parameter]) -> None: - """Replace the coefficients. + """Set the coefficients of the polynomial. Length must match current number of coefficients. + + Args: + coeffs (Sequence[Numeric | Parameter]): New coefficients as + a sequence of numbers or Parameters. + + Raises: + TypeError: If coeffs is not a sequence of numbers or + Parameters. + ValueError: If the length of coeffs does not match the + existing number of coefficients. + TypeError: If any item in coeffs is not a number or + Parameter. """ if not isinstance(coeffs, (list, tuple, np.ndarray)): raise TypeError( @@ -95,14 +121,28 @@ def coefficients(self, coeffs: Sequence[Numeric | Parameter]) -> None: raise TypeError('Each coefficient must be either a numeric value or a Parameter.') def coefficient_values(self) -> list[float]: - """Get the coefficients of the polynomial as a list.""" + """Get the coefficients of the polynomial as a list. + + Returns: + list[float]: The coefficient values of the polynomial. + """ coefficient_list = [param.value for param in self._coefficients] return coefficient_list def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray: """Evaluate the Polynomial at the given x values. - The Polynomial evaluates to c0 + c1*x + c2*x^2 + ... + cN*x^N + $I(x) = c_0 + c_1 x + c_2 x^2 + ... + c_N x^N$, where $C_i$ are + the coefficients. + + Args: + x (Numeric | list | np.ndarray | sc.Variable | + sc.DataArray): + The x values at which to evaluate the Polynomial. + + Returns: + np.ndarray: The evaluated Polynomial at the given x + values. """ x = self._prepare_x_for_evaluate(x) @@ -121,11 +161,25 @@ def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) @property def degree(self) -> int: - """Return the degree of the polynomial.""" + """Get the degree of the polynomial. + + Returns: + int: The degree of the polynomial. + """ return len(self._coefficients) - 1 @degree.setter def degree(self, value: int) -> None: + """The degree is determined by the number of coefficients and + cannot be set directly. + + Args: + value (int): The new degree of the polynomial. + + Raises: + AttributeError: Always raised since degree cannot be set + directly. + """ raise AttributeError( 'The degree of the polynomial is determined by the number of coefficients \ and cannot be set directly.' @@ -144,6 +198,9 @@ def convert_unit(self, unit: str | sc.Unit): Args: unit (str or sc.Unit): The target unit to convert to. + + Raises: + UnitError: If the provided unit is not a string or sc.Unit. """ if not isinstance(unit, (str, sc.Unit)): @@ -162,6 +219,12 @@ def convert_unit(self, unit: str | sc.Unit): self._unit = unit def __repr__(self) -> str: + """Return a string representation of the Polynomial. + + Returns: + str: A string representation of the Polynomial. + """ + coeffs_str = ', '.join(f'{param.name}={param.value}' for param in self._coefficients) return f'Polynomial(unique_name = {self.unique_name}, \ unit = {self._unit},\n coefficients = [{coeffs_str}])' diff --git a/src/easydynamics/sample_model/components/voigt.py b/src/easydynamics/sample_model/components/voigt.py index fcd05fb2..41be9e9c 100644 --- a/src/easydynamics/sample_model/components/voigt.py +++ b/src/easydynamics/sample_model/components/voigt.py @@ -19,17 +19,30 @@ class Voigt(CreateParametersMixin, ModelComponent): center is not provided, it will be centered at 0 and fixed, which is typically what you want in QENS. + Use scipy.special.voigt_profile to evaluate the Voigt profile. + Args: - area (Int or float): Total area under the curve. - center (Int or float or None): Center of the Voigt profile. - gaussian_width (Int or float): Standard deviation of the - Gaussian part. - lorentzian_width (Int or float): Half width at half max (HWHM) - of the Lorentzian part. - unit (str or sc.Unit): Unit of the parameters. Defaults to "meV" - display_name (str): Display name of the component. - unique_name (str or None): Unique name of the component. - If None, a unique_name is automatically generated. + area (Int | float): Total area under the curve. + center (Int | float | None): Center of the Voigt profile. + gaussian_width (Int | float): Standard deviation of the + Gaussian part. + lorentzian_width (Int | float): Half width at half max (HWHM) + of the Lorentzian part. + unit (str | sc.Unit): Unit of the parameters. Defaults to "meV" + display_name (str | None): Display name of the component. + unique_name (str | None): Unique name of the component. + If None, a unique_name is automatically generated. + + Attributes: + area (Parameter): Total area under the curve. + center (Parameter): Center of the Voigt profile. + gaussian_width (Parameter): Standard deviation of the Gaussian + part. + lorentzian_width (Parameter): Half width at half max (HWHM) of + the Lorentzian part. + unit (str | sc.Unit): Unit of the parameters. + display_name (str | None): Display name of the component. + unique_name (str | None): Unique name of the component. """ def __init__( @@ -73,24 +86,47 @@ def __init__( @property def area(self) -> Parameter: - """Get the area parameter.""" + """Get the area parameter. + + Returns: + Parameter: The area parameter. + """ return self._area @area.setter def area(self, value: Numeric) -> None: - """Set the area parameter value.""" + """Set the value of the area parameter. + + Args: + value (Numeric): The new value for the area parameter. + + Raises: + TypeError: If the value is not a number. + """ if not isinstance(value, Numeric): raise TypeError('area must be a number') self._area.value = value @property def center(self) -> Parameter: - """Get the center parameter.""" + """Get the center parameter. + + Returns: + Parameter: The center parameter. + """ return self._center @center.setter def center(self, value: Numeric | None) -> None: - """Set the center parameter value.""" + """Set the value of the center parameter. + + Args: + value (Numeric | None): The new value for the center + parameter. If None, defaults to 0 and is fixed. + + Raises: + TypeError: If the value is not a number. + """ if value is None: value = 0.0 self._center.fixed = True @@ -100,26 +136,56 @@ def center(self, value: Numeric | None) -> None: @property def gaussian_width(self) -> Parameter: - """Get the width parameter.""" + """Get the Gaussian width parameter. + + Returns: + Parameter: The Gaussian width parameter. + """ return self._gaussian_width @gaussian_width.setter def gaussian_width(self, value: Numeric) -> None: - """Set the width parameter value.""" + """Set the width parameter value. + + Args: + value (Numeric | None): The new value for the width + parameter. + + Raises: + TypeError: If the value is not a number or None. + ValueError: If the value is not positive. + """ if not isinstance(value, Numeric): raise TypeError('gaussian_width must be a number') + if float(value) <= 0: + raise ValueError('gaussian_width must be positive') self._gaussian_width.value = value @property def lorentzian_width(self) -> Parameter: - """Get the width parameter.""" + """Get the Lorentzian width parameter (HWHM). + + Returns: + Parameter: The Lorentzian width parameter. + """ return self._lorentzian_width @lorentzian_width.setter def lorentzian_width(self, value: Numeric) -> None: - """Set the width parameter value.""" + """Set the value of the Lorentzian width parameter. + + Args: + value (Numeric): The new value for the Lorentzian width + parameter. + + Raises: + TypeError: If the value is not a number. + ValueError: If the value is not positive. + """ if not isinstance(value, Numeric): raise TypeError('lorentzian_width must be a number') + if float(value) <= 0: + raise ValueError('lorentzian_width must be positive') self._lorentzian_width.value = value def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray: @@ -130,6 +196,16 @@ def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) a Gaussian with sigma gaussian_width and a Lorentzian with half width at half max lorentzian_width, centered at center, with area equal to area. + + + Args: + x (Numeric or list or np.ndarray or sc.Variable or + sc.DataArray): + The x values at which to evaluate the Voigt. + + Returns: + np.ndarray: The intensity of the Voigt at the given x + values. """ x = self._prepare_x_for_evaluate(x) @@ -141,6 +217,12 @@ def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) ) def __repr__(self): + """Return a string representation of the Voigt. + + Returns: + str: A string representation of the Voigt. + """ + return f'Voigt(unique_name = {self.unique_name}, unit = {self._unit},\n \ area = {self.area},\n \ center = {self.center},\n \ diff --git a/tests/conftest.py b/tests/conftest.py index d11735d3..e19e577c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,20 +5,12 @@ # TODO: remove once weakref bug is fixed -# import easyscience.global_object -# import pytest - - -# @pytest.fixture(autouse=True) -# def reset_global_object(): -# easyscience.global_object.map._clear() - from unittest.mock import patch import pytest -@pytest.fixture(autouse=True) +@pytest.fixture(autouse=False) def patch_easyscience_map(): """Patch the problematic Map methods.""" from easyscience.global_object.map import Map diff --git a/tests/unit/easydynamics/sample_model/components/test_gaussian.py b/tests/unit/easydynamics/sample_model/components/test_gaussian.py index c96eacc3..6699c6d0 100644 --- a/tests/unit/easydynamics/sample_model/components/test_gaussian.py +++ b/tests/unit/easydynamics/sample_model/components/test_gaussian.py @@ -124,6 +124,11 @@ def test_property_setters( with pytest.raises(TypeError, match=invalid_message): setattr(gaussian, prop, invalid_value) + def test_width_must_be_positive(self, gaussian: Gaussian): + # WHEN THEN EXPECT + with pytest.raises(ValueError, match='width must be positive'): + gaussian.width = -0.5 + def test_evaluate(self, gaussian: Gaussian): # WHEN x = np.array([0.0, 0.5, 1.0]) diff --git a/tests/unit/easydynamics/sample_model/components/test_lorentzian.py b/tests/unit/easydynamics/sample_model/components/test_lorentzian.py index 7757a8ab..34b26cca 100644 --- a/tests/unit/easydynamics/sample_model/components/test_lorentzian.py +++ b/tests/unit/easydynamics/sample_model/components/test_lorentzian.py @@ -128,6 +128,11 @@ def test_property_setters( with pytest.raises(TypeError, match=invalid_message): setattr(lorentzian, prop, invalid_value) + def test_width_must_be_positive(self, lorentzian: Lorentzian): + # WHEN THEN EXPECT + with pytest.raises(ValueError, match='width must be positive'): + lorentzian.width = -0.5 + def test_evaluate(self, lorentzian: Lorentzian): # WHEN x = np.array([0.0, 0.5, 1.0]) diff --git a/tests/unit/easydynamics/sample_model/components/test_voigt.py b/tests/unit/easydynamics/sample_model/components/test_voigt.py index a2515a47..9094aedc 100644 --- a/tests/unit/easydynamics/sample_model/components/test_voigt.py +++ b/tests/unit/easydynamics/sample_model/components/test_voigt.py @@ -196,6 +196,19 @@ def test_property_setters( with pytest.raises(TypeError, match=invalid_message): setattr(voigt, prop, invalid_value) + def test_gaussian_width_must_be_positive(self, voigt: Voigt): + # WHEN THEN + with pytest.raises(ValueError, match='gaussian_width must be positive'): + voigt.gaussian_width = -0.6 + + def test_lorentzian_width_must_be_positive(self, voigt: Voigt): + # WHEN THEN + with pytest.raises( + ValueError, + match='lorentzian_width must be positive', + ): + voigt.lorentzian_width = -0.7 + def test_center_is_fixed_if_set_to_None(self, voigt: Voigt): # WHEN assert voigt.center.fixed is False From cfa89bb0aa300746dfb54b45dd04011ab71d2312 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Fri, 27 Feb 2026 12:07:22 +0100 Subject: [PATCH 06/12] Update detailed balance --- src/easydynamics/utils/detailed_balance.py | 66 ++++++++++++++-------- 1 file changed, 44 insertions(+), 22 deletions(-) diff --git a/src/easydynamics/utils/detailed_balance.py b/src/easydynamics/utils/detailed_balance.py index f82d3c89..46d75b9d 100644 --- a/src/easydynamics/utils/detailed_balance.py +++ b/src/easydynamics/utils/detailed_balance.py @@ -30,32 +30,31 @@ def _detailed_balance_factor( divide_by_temperature: bool = True, ) -> np.ndarray: """ - Compute the detailed balance factor (DBF): - DBF(E, T) = E*(n(E)+1)=E / (1 - exp(-E / (kB*T))), - where n(E) is the Bose-Einstein distribution. - If divide_by_temperature is True, - the result is normalized by kB*T to have value 1 at E=0. + Compute the detailed balance factor (DBF): $DBF(E, T) = E*(n(E)+1)=E + / (1 - exp(-E / (kB*T)))$, where $n(E)$ is the Bose-Einstein + distribution, $E$ is the energy transfer, and $T$ is the + temperature. $k_B$ is the Boltzmann constant. If + divide_by_temperature is True, the result is normalized by kB*T to + have value 1 at E=0. Args: - energy : number, list, np.ndarray, or scipp Variable. - If number, assumed to be in meV unless energy_unit is set. - Energy transfer - T : number, scipp Variable, or Parameter. - If number, assumed to be in K unless temperature_unit is set. - Temperature - energy_unit : str, optional - Unit for energy if energy is given as a number or list. - Default is 'meV' - temperature_unit : str, optional - Unit for temperature if temperature is given as a number. - Default is 'K' - divide_by_temperature : True or False, optional - If True, divide the result by kB*T to make it dimensionless - and have value 1 at E=0. Default is True. + energy (number, list, np.ndarray, | scipp Variable). The energy + transfer. If number, assumed to be in meV unless energy_unit + is set. + temperature (number, scipp Variable, or Parameter). If number, + assumed to be in K unless temperature_unit is set. + energy_unit (str | sc.Unit |None): Unit for energy if energy is + given as a number or list. Default is 'meV' + temperature_unit (str | sc.Unit |None): Unit for temperature if + temperature is given as a number. Default is 'K' + divide_by_temperature (bool | None): If True, divide the result + by $k_B*T$ to make it dimensionless and have value 1 at E=0. + Default is True. Returns: - DBF : np.ndarray TODO: change to sc.Variable? - Detailed balance factor + DBF (np.ndarray) Detailed balance factor evaluated at the given + energy and temperature. + TODO: change to sc.Variable? Examples -------- @@ -178,6 +177,29 @@ def _convert_to_scipp_variable( ) -> sc.Variable: """Convert various input types to a scipp Variable with proper units. + + Args: + value (int | float | list | np.ndarray | Parameter | + sc.Variable): + The value to convert. Can be a number, list, numpy array, + Parameter, or scipp Variable. If a number or list, the unit + must be specified in the unit argument. + name (str): The name of the variable, used for error messages. + unit (str | None): The unit to use if value is a number or list. + Must be specified if value is a number or list. Ignored if + value is a Parameter or sc.Variable, which have their own + units. + + Raises: + TypeError: If value is not one of the accepted types, or if unit + is not a string when needed. + ValueError: If value is a number or list and unit is not + provided. + UnitError: If the provided unit is invalid. + + Returns: + sc.Variable: The input value converted to a scipp Variable with + appropriate units. """ if isinstance(value, sc.Variable): return value From fea49faafc405cdc0bd595bf8d32659b1dc1d809 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Fri, 27 Feb 2026 12:38:50 +0100 Subject: [PATCH 07/12] add DHO test --- .../components/test_damped_harmonic_oscillator.py | 5 +++++ 1 file changed, 5 insertions(+) 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 0b10a43e..e77f521e 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 @@ -138,6 +138,11 @@ def test_center_setter_negative_raises(self, dho: DampedHarmonicOscillator): with pytest.raises(ValueError, match='center must be positive'): dho.center = -1.0 + def test_width_must_be_positive(self, dho: DampedHarmonicOscillator): + # WHEN THEN EXPECT + with pytest.raises(ValueError, match='width must be positive'): + dho.width = -0.5 + def test_evaluate(self, dho: DampedHarmonicOscillator): # WHEN x = np.array([0.0, 1.5, 3.0]) From 239f1c412d690d28b45e3e26b08d211c33f97dfd Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Fri, 27 Feb 2026 12:56:46 +0100 Subject: [PATCH 08/12] Change test to use the weakref fix fixture --- tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index e19e577c..0bca2e2a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,7 +10,7 @@ import pytest -@pytest.fixture(autouse=False) +@pytest.fixture(autouse=True) def patch_easyscience_map(): """Patch the problematic Map methods.""" from easyscience.global_object.map import Map From 0fb597960050c33da39faf7088caad4883ac5d9c Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Mon, 2 Mar 2026 11:43:44 +0100 Subject: [PATCH 09/12] Try to fix equations in docs --- docs/mkdocs.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 88b1bda5..1add55a8 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -96,6 +96,8 @@ markdown_extensions: - footnotes - pymdownx.blocks.caption - pymdownx.details + - pymdownx.arithmatex: + generic: true - pymdownx.emoji: emoji_index: !!python/name:material.extensions.emoji.twemoji emoji_generator: !!python/name:material.extensions.emoji.to_svg From 4e851890b499e1dc4bbc5fa1562516f720b00fb7 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Mon, 2 Mar 2026 13:25:09 +0100 Subject: [PATCH 10/12] Update doc strings to have proper equations --- docs/mkdocs.yml | 9 ++- .../components/damped_harmonic_oscillator.py | 30 +++++--- .../sample_model/components/delta_function.py | 6 +- .../sample_model/components/gaussian.py | 76 ++++++++++++------- .../sample_model/components/lorentzian.py | 26 ++++--- .../components/model_component.py | 4 +- .../sample_model/components/polynomial.py | 21 +++-- src/easydynamics/sample_model/sample_model.py | 2 +- 8 files changed, 109 insertions(+), 65 deletions(-) diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 1add55a8..b0b2fd61 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -86,6 +86,8 @@ extra_css: - assets/stylesheets/extra.css extra_javascript: - assets/javascripts/extra.js + - javascripts/mathjax.js + - https://unpkg.com/mathjax@3/es5/tex-mml-chtml.js # A list of extensions beyond the ones that MkDocs uses by default (meta, toc, tables, and fenced_code) markdown_extensions: @@ -94,10 +96,10 @@ markdown_extensions: - attr_list - def_list - footnotes - - pymdownx.blocks.caption - - pymdownx.details - pymdownx.arithmatex: generic: true + - pymdownx.blocks.caption + - pymdownx.details - pymdownx.emoji: emoji_index: !!python/name:material.extensions.emoji.twemoji emoji_generator: !!python/name:material.extensions.emoji.to_svg @@ -134,7 +136,7 @@ plugins: allow_errors: false include_source: true include_requirejs: true # Required for Plotly - custom_mathjax_url: 'https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.7/latest.js?config=TeX-AMS_CHTML-full,Safe' + # custom_mathjax_url: 'https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.7/latest.js?config=TeX-AMS_CHTML-full,Safe' ignore_h1_titles: true # Use titles defined in the nav section below remove_tag_config: remove_input_tags: @@ -145,6 +147,7 @@ plugins: paths: ['src'] # Change 'src' to your actual sources directory options: docstring_style: google + render_markdown: true group_by_category: false heading_level: 1 show_root_heading: true diff --git a/src/easydynamics/sample_model/components/damped_harmonic_oscillator.py b/src/easydynamics/sample_model/components/damped_harmonic_oscillator.py index ab45ff05..05f2300b 100644 --- a/src/easydynamics/sample_model/components/damped_harmonic_oscillator.py +++ b/src/easydynamics/sample_model/components/damped_harmonic_oscillator.py @@ -17,7 +17,10 @@ class DampedHarmonicOscillator(CreateParametersMixin, ModelComponent): r"""Model of a Damped Harmonic Oscillator (DHO). The intensity is given by - $I(x) = 2*A*x_0^2*\gamma/\pi / ( (x^2-x_0^2)^2 + (2*\gamma*x)^2 )$, + $$ + I(x) = \frac{2 A x_0^2 \gamma}{\pi \left( (x^2 - x_0^2)^2 + + (2 \gamma x)^2 \right)}, + $$ where $A$ is the area, $x_0$ is the center, and $\gamma$ is the width. @@ -35,14 +38,14 @@ class DampedHarmonicOscillator(CreateParametersMixin, ModelComponent): If None, a unique_name is automatically generated. Attributes: - area (Parameter): Area under the curve. center (Parameter): - Resonance frequency, approximately the + area (Parameter): Area under the curve. + center (Parameter): Resonance frequency, approximately the peak position. width (Parameter): Damping constant, approximately the half width at half max (HWHM) of the peaks. - unit (str | sc.Unit): Unit of the parameters. display_name (str - | None): Display name of the component. unique_name (str | - None): Unique name of the component. + unit (str | sc.Unit): Unit of the parameters. + display_name (str | None): Display name of the component. + unique_name (str | None): Unique name of the component. """ def __init__( @@ -152,13 +155,18 @@ def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) values. If x is a scipp Variable, the unit of the DHO will be converted - to match x. The intensity is given by $I(x) = - 2*A*x_0^2*\gamma/\pi / ( (x^2 - x_0^2)^2 + (2*\gamma*x)^2 )$ + to match x. The intensity is given by + $$ + I(x) = \frac{2 A x_0^2 \gamma}{\pi \left( (x^2 - x_0^2)^2 + + (2 \gamma x)^2 \right)}, + $$ + where $A$ is the area, $x_0$ is the center, and $\gamma$ is the + width. Args: - x (Numeric or list or np.ndarray or sc.Variable or - sc.DataArray): - The x values at which to evaluate the DHO. + x (Numeric | list | np.ndarray | sc.Variable | + sc.DataArray): The x values at which to evaluate the + DHO. Returns: np.ndarray: The intensity of the DHO at the given x values. diff --git a/src/easydynamics/sample_model/components/delta_function.py b/src/easydynamics/sample_model/components/delta_function.py index 849ca5dd..6cc1faca 100644 --- a/src/easydynamics/sample_model/components/delta_function.py +++ b/src/easydynamics/sample_model/components/delta_function.py @@ -128,12 +128,12 @@ def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) Args: x (Numeric | list | np.ndarray | sc.Variable | - sc.DataArray): - The x values at which to evaluate the Delta function. + sc.DataArray): The x values at which to evaluate the + Delta function. Returns: np.ndarray: The evaluated Delta function at the given x - values. + values. """ # x assumed sorted, 1D numpy array diff --git a/src/easydynamics/sample_model/components/gaussian.py b/src/easydynamics/sample_model/components/gaussian.py index 771a24c0..967d510e 100644 --- a/src/easydynamics/sample_model/components/gaussian.py +++ b/src/easydynamics/sample_model/components/gaussian.py @@ -14,31 +14,42 @@ class Gaussian(CreateParametersMixin, ModelComponent): - """Model of a Gaussian function. - - The intensity is given by $I(x) = \frac{A}{\\sigma \\sqrt{2\\pi}} - e^{-\frac{1}{2} \\left(\frac{x - x_0}{\\sigma}\right)^2}$, - where $A$ is the area, $x_0$ is the center, and $\\sigma$ is the - width. If the center is not provided, it will be centered at 0 and - fixed, which is typically what you want in QENS. - - Args: - area (Int | float | Parameter): Area of the Gaussian. - center (Int | float | None | Parameter): Center of the Gaussian. - If None, defaults to 0 and is fixed - width (Int | float | Parameter): Standard deviation. - unit (str | sc.Unit): Unit of the parameters. Defaults to "meV". - display_name (str | None): Name of the component. - unique_name (str | None): Unique name of the component. if None, - a unique_name is automatically generated. - - Attributes: - area (Parameter): Area of the Gaussian. - center (Parameter): Center of the Gaussian. - width (Parameter): Standard deviation of the Gaussian. - unit (str | sc.Unit): Unit of the parameters. - display_name (str | None): Name of the component. - unique_name (str | None): Unique name of the component. + r"""Test Model of a Gaussian function. + + The intensity is given by + + $$ + I(x) = \frac{A}{\sigma \sqrt{2\pi}} + \exp\left( + -\frac{1}{2} + \left(\frac{x - x_0}{\sigma}\right)^2 + \right) + $$ + + where $A$ is the area, $x_0$ is the center, and $\sigma$ is the + width. + + If the center is not provided, it will be centered at 0 and + fixed, which is typically what you want in QENS. + + Args: + area (Int | float | Parameter): Area of the Gaussian. + center (Int | float | None | Parameter): Center of the + Gaussian. If None, defaults to 0 and is fixed. + width (Int | float | Parameter): Standard deviation. + unit (str | sc.Unit): Unit of the parameters. Defaults to + "meV". + display_name (str | None): Name of the component. + unique_name (str | None): Unique name of the component. if + None, a unique_name is automatically generated. + + Attributes: + area (Parameter): Area of the Gaussian. + center (Parameter): Center of the Gaussian. + width (Parameter): Standard deviation of the Gaussian. + unit (str | sc.Unit): Unit of the parameters. + display_name (str | None): Name of the component. + unique_name (str | None): Unique name of the component. """ def __init__( @@ -159,9 +170,18 @@ def evaluate( If x is a scipp Variable, the unit of the Gaussian will be converted to match x. - The intensity is given by $I(x) = \frac{A} - {\\sigma \\sqrt{2\\pi}} - e^{-\frac{1}{2} \\left(\frac{x - x_0}{\\sigma}\right)^2}$, + The intensity is given by + $$ + I(x) = \frac{A}{\\sigma \\sqrt{2\\pi}} + \\exp\\left( + -\frac{1}{2} + \\left(\frac{x - x_0}{\\sigma}\right)^2 + \right) + $$ + + where $A$ is the area, $x_0$ is the center, and $\\sigma$ is the + width. + Args: x (Numeric or list or np.ndarray or sc.Variable or diff --git a/src/easydynamics/sample_model/components/lorentzian.py b/src/easydynamics/sample_model/components/lorentzian.py index 05c8f696..86ab6563 100644 --- a/src/easydynamics/sample_model/components/lorentzian.py +++ b/src/easydynamics/sample_model/components/lorentzian.py @@ -14,11 +14,14 @@ class Lorentzian(CreateParametersMixin, ModelComponent): - """Model of a Lorentzian function. + r"""Model of a Lorentzian function. - The intensity is given by $I(x) = \frac{A}{\\pi} \frac{\\Gamma}{(x - - x_0)^2 + \\Gamma^2}$, where $A$ is the area, $x_0$ is the center, - and $\\Gamma$ is the half width at half maximum (HWHM). + The intensity is given by + $$ + I(x) = \frac{A}{\pi} \frac{\Gamma}{(x - x_0)^2 + \Gamma^2}, + $$ + where $A$ is the area, $x_0$ is the center, and $\Gamma$ is the + half width at half maximum (HWHM). If the center is not provided, it will be centered at 0 and fixed, which is typically what you want in QENS. @@ -150,13 +153,18 @@ def width(self, value: Numeric) -> None: self._width.value = value def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray: - """Evaluate the Lorentzian at the given x values. + r"""Evaluate the Lorentzian at the given x values. If x is a scipp Variable, the unit of the Lorentzian will be - converted to match x. - $I(x) = \frac{A}{\\pi} \frac{\\Gamma}{(x - - x_0)^2 + \\Gamma^2}$, where $A$ is the area, $x_0$ is the - center, and $\\Gamma$ is the half width at half maximum (HWHM). + converted to match x. The intensity is given by + + $$ + I(x) = \frac{A}{\pi} \frac{\Gamma}{(x - + x_0)^2 + \Gamma^2}, + $$ + + where $A$ is the area, $x_0$ is the center, and $\Gamma$ is + the half width at half maximum (HWHM). Args: x (Numeric or list or np.ndarray or sc.Variable or diff --git a/src/easydynamics/sample_model/components/model_component.py b/src/easydynamics/sample_model/components/model_component.py index b5aab33c..470e2cb2 100644 --- a/src/easydynamics/sample_model/components/model_component.py +++ b/src/easydynamics/sample_model/components/model_component.py @@ -200,8 +200,8 @@ def evaluate( Must be implemented by subclasses. Args: - x (Numeric or List[Numeric] or np.ndarray or sc.Variable or - sc.DataArray): Input values. + x (Numeric | list[Numeric] | np.ndarray | sc.Variable | + sc.DataArray): Input values. Returns: np.ndarray: Evaluated function values. diff --git a/src/easydynamics/sample_model/components/polynomial.py b/src/easydynamics/sample_model/components/polynomial.py index f7bb74d9..a56b0278 100644 --- a/src/easydynamics/sample_model/components/polynomial.py +++ b/src/easydynamics/sample_model/components/polynomial.py @@ -18,11 +18,13 @@ class Polynomial(ModelComponent): - """Polynomial function component. + r"""Polynomial function component. - The intensity is given by $I(x) = - c_0 + c_1 x + c_2 x^2 + ... + c_N x^N$, where $C_i$ are the - coefficients. + The intensity is given by + $$ + I(x) = c_0 + c_1 x + c_2 x^2 + ... + c_N x^N, + $$ + where $C_i$ are the coefficients. Args: coefficients (list or tuple): Coefficients c0, c1, ..., cN @@ -132,17 +134,20 @@ def coefficient_values(self) -> list[float]: def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray: """Evaluate the Polynomial at the given x values. - $I(x) = c_0 + c_1 x + c_2 x^2 + ... + c_N x^N$, where $C_i$ are - the coefficients. + The intensity is given by + $$ + I(x) = c_0 + c_1 x + c_2 x^2 + ... + c_N x^N, + $$ + where $C_i$ are the coefficients. Args: x (Numeric | list | np.ndarray | sc.Variable | - sc.DataArray): + sc.DataArray): The x values at which to evaluate the Polynomial. Returns: np.ndarray: The evaluated Polynomial at the given x - values. + values. """ x = self._prepare_x_for_evaluate(x) diff --git a/src/easydynamics/sample_model/sample_model.py b/src/easydynamics/sample_model/sample_model.py index 346bd7a4..ba21e869 100644 --- a/src/easydynamics/sample_model/sample_model.py +++ b/src/easydynamics/sample_model/sample_model.py @@ -115,7 +115,7 @@ def append_diffusion_model(self, diffusion_model: DiffusionModelBase) -> None: Args: diffusion_model (DiffusionModelBase): The DiffusionModel - to append. + to append. """ if not isinstance(diffusion_model, DiffusionModelBase): From 57eef2550c6f4b5e8654b7399f24c21a8c2325e9 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Tue, 3 Mar 2026 15:06:13 +0100 Subject: [PATCH 11/12] fix DHO --- .../components/damped_harmonic_oscillator.py | 40 +++++++++++-------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/src/easydynamics/sample_model/components/damped_harmonic_oscillator.py b/src/easydynamics/sample_model/components/damped_harmonic_oscillator.py index 05f2300b..37fdd661 100644 --- a/src/easydynamics/sample_model/components/damped_harmonic_oscillator.py +++ b/src/easydynamics/sample_model/components/damped_harmonic_oscillator.py @@ -26,8 +26,8 @@ class DampedHarmonicOscillator(CreateParametersMixin, ModelComponent): Args: - area (Int | float): Area under the curve. center (Int | float): - Resonance frequency, approximately the + area (Int | float): Area under the curve. + center (Int | float): Resonance frequency, approximately the peak position. width (Int | float): Damping constant, approximately the half width at half max (HWHM) of the peaks. @@ -53,8 +53,8 @@ def __init__( area: Numeric | Parameter = 1.0, center: Numeric | Parameter = 1.0, width: Numeric | Parameter = 1.0, - unit: str | sc.Unit = 'meV', - display_name: str | None = 'DampedHarmonicOscillator', + unit: str | sc.Unit = "meV", + display_name: str | None = "DampedHarmonicOscillator", unique_name: str | None = None, ): super().__init__( @@ -64,7 +64,9 @@ 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=display_name, unit=self._unit + ) center = self._create_center_parameter( center=center, name=display_name, @@ -73,7 +75,9 @@ def __init__( 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=display_name, unit=self._unit + ) self._area = area self._center = center @@ -92,7 +96,7 @@ def area(self) -> Parameter: def area(self, value: Numeric) -> None: """Set the value of the area parameter.""" if not isinstance(value, Numeric): - raise TypeError('area must be a number') + raise TypeError("area must be a number") self._area.value = value @property @@ -116,10 +120,10 @@ def center(self, value: Numeric) -> None: ValueError: If the value is not positive. """ if not isinstance(value, Numeric): - raise TypeError('center must be a number') + raise TypeError("center must be a number") if float(value) <= 0: - raise ValueError('center must be positive') + raise ValueError("center must be positive") self._center.value = value @property @@ -143,14 +147,16 @@ def width(self, value: Numeric) -> None: ValueError: If the value is not positive. """ if not isinstance(value, Numeric): - raise TypeError('width must be a number') + raise TypeError("width must be a number") if float(value) <= 0: - raise ValueError('width must be positive') + raise ValueError("width must be positive") self._width.value = value - 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: r"""Evaluate the Damped Harmonic Oscillator at the given x values. @@ -176,7 +182,9 @@ def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) normalization = 2 * self.center.value**2 * self.width.value / np.pi # No division by zero here, width>0 enforced in setter - denominator = (x**2 - self.center.value**2) ** 2 + (2 * self.width.value * x) ** 2 + denominator = (x**2 - self.center.value**2) ** 2 + ( + 2 * self.width.value * x + ) ** 2 return self.area.value * normalization / (denominator) @@ -188,7 +196,5 @@ def __repr__(self) -> str: str: A string representation of the Damped Harmonic Oscillator. """ - return ( - f'DampedHarmonicOscillator(display_name = {self.display_name}, unit = {self._unit},\n \ - area = {self.area},\n center = {self.center},\n width = {self.width})' - ) + return f"DampedHarmonicOscillator(display_name = {self.display_name}, unit = {self._unit},\n \ + area = {self.area},\n center = {self.center},\n width = {self.width})" From c6327280267276058b0864e6fd918e88581d6dc3 Mon Sep 17 00:00:00 2001 From: henrikjacobsenfys Date: Tue, 3 Mar 2026 20:39:48 +0100 Subject: [PATCH 12/12] respond the PR comments --- .../components/damped_harmonic_oscillator.py | 36 ++++++++----------- .../sample_model/components/gaussian.py | 12 +++---- .../sample_model/components/lorentzian.py | 2 +- .../sample_model/components/polynomial.py | 2 +- .../sample_model/components/voigt.py | 4 +-- src/easydynamics/utils/detailed_balance.py | 21 ++++++----- 6 files changed, 35 insertions(+), 42 deletions(-) diff --git a/src/easydynamics/sample_model/components/damped_harmonic_oscillator.py b/src/easydynamics/sample_model/components/damped_harmonic_oscillator.py index 37fdd661..f85eb93b 100644 --- a/src/easydynamics/sample_model/components/damped_harmonic_oscillator.py +++ b/src/easydynamics/sample_model/components/damped_harmonic_oscillator.py @@ -53,8 +53,8 @@ def __init__( area: Numeric | Parameter = 1.0, center: Numeric | Parameter = 1.0, width: Numeric | Parameter = 1.0, - unit: str | sc.Unit = "meV", - display_name: str | None = "DampedHarmonicOscillator", + unit: str | sc.Unit = 'meV', + display_name: str | None = 'DampedHarmonicOscillator', unique_name: str | None = None, ): super().__init__( @@ -64,9 +64,7 @@ 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=display_name, unit=self._unit) center = self._create_center_parameter( center=center, name=display_name, @@ -75,9 +73,7 @@ def __init__( 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=display_name, unit=self._unit) self._area = area self._center = center @@ -96,7 +92,7 @@ def area(self) -> Parameter: def area(self, value: Numeric) -> None: """Set the value of the area parameter.""" if not isinstance(value, Numeric): - raise TypeError("area must be a number") + raise TypeError('area must be a number') self._area.value = value @property @@ -120,10 +116,10 @@ def center(self, value: Numeric) -> None: ValueError: If the value is not positive. """ if not isinstance(value, Numeric): - raise TypeError("center must be a number") + raise TypeError('center must be a number') if float(value) <= 0: - raise ValueError("center must be positive") + raise ValueError('center must be positive') self._center.value = value @property @@ -147,16 +143,14 @@ def width(self, value: Numeric) -> None: ValueError: If the value is not positive. """ if not isinstance(value, Numeric): - raise TypeError("width must be a number") + raise TypeError('width must be a number') if float(value) <= 0: - raise ValueError("width must be positive") + raise ValueError('width must be positive') self._width.value = value - 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: r"""Evaluate the Damped Harmonic Oscillator at the given x values. @@ -182,9 +176,7 @@ def evaluate( normalization = 2 * self.center.value**2 * self.width.value / np.pi # No division by zero here, width>0 enforced in setter - denominator = (x**2 - self.center.value**2) ** 2 + ( - 2 * self.width.value * x - ) ** 2 + denominator = (x**2 - self.center.value**2) ** 2 + (2 * self.width.value * x) ** 2 return self.area.value * normalization / (denominator) @@ -196,5 +188,7 @@ def __repr__(self) -> str: str: A string representation of the Damped Harmonic Oscillator. """ - return f"DampedHarmonicOscillator(display_name = {self.display_name}, unit = {self._unit},\n \ - area = {self.area},\n center = {self.center},\n width = {self.width})" + return ( + f'DampedHarmonicOscillator(display_name = {self.display_name}, unit = {self._unit},\n \ + area = {self.area},\n center = {self.center},\n width = {self.width})' + ) diff --git a/src/easydynamics/sample_model/components/gaussian.py b/src/easydynamics/sample_model/components/gaussian.py index 967d510e..d10d6978 100644 --- a/src/easydynamics/sample_model/components/gaussian.py +++ b/src/easydynamics/sample_model/components/gaussian.py @@ -14,7 +14,7 @@ class Gaussian(CreateParametersMixin, ModelComponent): - r"""Test Model of a Gaussian function. + r"""Model of a Gaussian function. The intensity is given by @@ -166,20 +166,20 @@ def evaluate( self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray, ) -> np.ndarray: - """Evaluate the Gaussian at the given x values. + r"""Evaluate the Gaussian at the given x values. If x is a scipp Variable, the unit of the Gaussian will be converted to match x. The intensity is given by $$ - I(x) = \frac{A}{\\sigma \\sqrt{2\\pi}} - \\exp\\left( + I(x) = \frac{A}{\sigma \sqrt{2\pi}} + \exp\left( -\frac{1}{2} - \\left(\frac{x - x_0}{\\sigma}\right)^2 + \left(\frac{x - x_0}{\sigma}\right)^2 \right) $$ - where $A$ is the area, $x_0$ is the center, and $\\sigma$ is the + where $A$ is the area, $x_0$ is the center, and $\sigma$ is the width. diff --git a/src/easydynamics/sample_model/components/lorentzian.py b/src/easydynamics/sample_model/components/lorentzian.py index 86ab6563..28685f98 100644 --- a/src/easydynamics/sample_model/components/lorentzian.py +++ b/src/easydynamics/sample_model/components/lorentzian.py @@ -34,7 +34,7 @@ class Lorentzian(CreateParametersMixin, ModelComponent): (HWHM). unit (str | sc.Unit): Unit of the parameters. Defaults to "meV". display_name (str | None): Name of the component. - unique_name (str | None): Unique name of the component. if None, + unique_name (str | None): Unique name of the component. If None, a unique_name is automatically generated. Attributes: diff --git a/src/easydynamics/sample_model/components/polynomial.py b/src/easydynamics/sample_model/components/polynomial.py index a56b0278..fa475b2c 100644 --- a/src/easydynamics/sample_model/components/polynomial.py +++ b/src/easydynamics/sample_model/components/polynomial.py @@ -132,7 +132,7 @@ def coefficient_values(self) -> list[float]: return coefficient_list def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray: - """Evaluate the Polynomial at the given x values. + r"""Evaluate the Polynomial at the given x values. The intensity is given by $$ diff --git a/src/easydynamics/sample_model/components/voigt.py b/src/easydynamics/sample_model/components/voigt.py index 41be9e9c..dc0c3315 100644 --- a/src/easydynamics/sample_model/components/voigt.py +++ b/src/easydynamics/sample_model/components/voigt.py @@ -15,7 +15,7 @@ class Voigt(CreateParametersMixin, ModelComponent): - """Voigt profile, a convolution of Gaussian and Lorentzian. If the + r"""Voigt profile, a convolution of Gaussian and Lorentzian. If the center is not provided, it will be centered at 0 and fixed, which is typically what you want in QENS. @@ -189,7 +189,7 @@ def lorentzian_width(self, value: Numeric) -> None: self._lorentzian_width.value = value def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray: - """Evaluate the Voigt at the given x values. + r"""Evaluate the Voigt at the given x values. If x is a scipp Variable, the unit of the Voigt will be converted to match x. The Voigt evaluates to the convolution of diff --git a/src/easydynamics/utils/detailed_balance.py b/src/easydynamics/utils/detailed_balance.py index 46d75b9d..8afcd47c 100644 --- a/src/easydynamics/utils/detailed_balance.py +++ b/src/easydynamics/utils/detailed_balance.py @@ -38,11 +38,12 @@ def _detailed_balance_factor( have value 1 at E=0. Args: - energy (number, list, np.ndarray, | scipp Variable). The energy + energy (number | list | np.ndarray | scipp.Variable): The energy transfer. If number, assumed to be in meV unless energy_unit is set. - temperature (number, scipp Variable, or Parameter). If number, - assumed to be in K unless temperature_unit is set. + temperature (number | scipp.Variable | Parameter): The + temperature. If number, assumed to be in K unless + temperature_unit is set. energy_unit (str | sc.Unit |None): Unit for energy if energy is given as a number or list. Default is 'meV' temperature_unit (str | sc.Unit |None): Unit for temperature if @@ -52,9 +53,8 @@ def _detailed_balance_factor( Default is True. Returns: - DBF (np.ndarray) Detailed balance factor evaluated at the given - energy and temperature. - TODO: change to sc.Variable? + DBF (np.ndarray): Detailed balance factor evaluated at the + given energy and temperature. Examples -------- @@ -180,10 +180,9 @@ def _convert_to_scipp_variable( Args: value (int | float | list | np.ndarray | Parameter | - sc.Variable): - The value to convert. Can be a number, list, numpy array, - Parameter, or scipp Variable. If a number or list, the unit - must be specified in the unit argument. + sc.Variable): The value to convert. Can be a number, list, + numpy array, Parameter, or scipp Variable. If a number or + list, the unit must be specified in the unit argument. name (str): The name of the variable, used for error messages. unit (str | None): The unit to use if value is a number or list. Must be specified if value is a number or list. Ignored if @@ -199,7 +198,7 @@ def _convert_to_scipp_variable( Returns: sc.Variable: The input value converted to a scipp Variable with - appropriate units. + appropriate units. """ if isinstance(value, sc.Variable): return value