diff --git a/src/easyscience/Objects/new_variable/descriptor_number.py b/src/easyscience/Objects/new_variable/descriptor_number.py index 1b0a3e03..4fd25709 100644 --- a/src/easyscience/Objects/new_variable/descriptor_number.py +++ b/src/easyscience/Objects/new_variable/descriptor_number.py @@ -8,6 +8,7 @@ import numpy as np import scipp as sc +from scipp import UnitError from scipp import Variable from easyscience.global_object.undo_redo import property_stack_deco @@ -58,7 +59,7 @@ def __init__( try: self._scalar = sc.scalar(float(value), unit=unit, variance=variance) except Exception as message: - raise ValueError(message) + raise UnitError(message) super().__init__( name=name, unique_name=unique_name, @@ -68,6 +69,10 @@ def __init__( parent=parent, ) + # Call convert_unit during initialization to ensure that the unit has no numbers in it, and to ensure unit consistency. + if self.unit is not None: + self.convert_unit(self._base_unit()) + @classmethod def from_scipp(cls, name: str, full_value: Variable, **kwargs) -> DescriptorNumber: """ @@ -193,8 +198,8 @@ def convert_unit(self, unit_str: str): raise TypeError(f'{unit_str=} must be a string representing a valid scipp unit') try: new_unit = sc.Unit(unit_str) - except Exception as message: - raise ValueError(message) + except UnitError as message: + raise UnitError(message) from None self._scalar = self._scalar.to(unit=new_unit) # Just to get return type right @@ -225,3 +230,170 @@ def as_dict(self) -> Dict[str, Any]: raw_dict['unit'] = str(self._scalar.unit) raw_dict['variance'] = self._scalar.variance return raw_dict + + def __add__(self, other: Union[DescriptorNumber, numbers.Number]) -> DescriptorNumber: + if isinstance(other, numbers.Number): + if self.unit != 'dimensionless': + raise UnitError("Numbers can only be added to dimensionless values") + new_value = self.full_value + other + elif type(other) is DescriptorNumber: + original_unit = other.unit + try: + other.convert_unit(self.unit) + except UnitError: + raise UnitError(f"Values with units {self.unit} and {other.unit} cannot be added") from None + new_value = self.full_value + other.full_value + other.convert_unit(original_unit) + else: + return NotImplemented + descriptor_number= DescriptorNumber.from_scipp(name=self.name, full_value=new_value) + descriptor_number.name=descriptor_number.unique_name + return descriptor_number + + + + def __radd__(self, other: numbers.Number) -> DescriptorNumber: + if isinstance(other, numbers.Number): + if self.unit != 'dimensionless': + raise UnitError("Numbers can only be added to dimensionless values") + new_value = other + self.full_value + else: + return NotImplemented + descriptor_number= DescriptorNumber.from_scipp(name=self.name, full_value=new_value) + descriptor_number.name=descriptor_number.unique_name + return descriptor_number + + def __sub__(self, other: Union[DescriptorNumber, numbers.Number]) -> DescriptorNumber: + if isinstance(other, numbers.Number): + if self.unit != 'dimensionless': + raise UnitError("Numbers can only be subtracted from dimensionless values") + new_value = self.full_value - other + elif type(other) is DescriptorNumber: + original_unit = other.unit + try: + other.convert_unit(self.unit) + except UnitError: + raise UnitError(f"Values with units {self.unit} and {other.unit} cannot be subtracted") from None + new_value = self.full_value - other.full_value + other.convert_unit(original_unit) + else: + return NotImplemented + descriptor_number= DescriptorNumber.from_scipp(name=self.name, full_value=new_value) + descriptor_number.name=descriptor_number.unique_name + return descriptor_number + + def __rsub__(self, other: numbers.Number) -> DescriptorNumber: + if isinstance(other, numbers.Number): + if self.unit != 'dimensionless': + raise UnitError("Numbers can only be subtracted from dimensionless values") + new_value = other - self.full_value + else: + return NotImplemented + descriptor= DescriptorNumber.from_scipp(name=self.name, full_value=new_value) + descriptor.name=descriptor.unique_name + return descriptor + + def __mul__(self, other: Union[DescriptorNumber, numbers.Number]) -> DescriptorNumber: + if isinstance(other, numbers.Number): + new_value = self.full_value * other + elif type(other) is DescriptorNumber: + new_value = self.full_value * other.full_value + else: + return NotImplemented + descriptor_number = DescriptorNumber.from_scipp(name=self.name, full_value=new_value) + descriptor_number.convert_unit(descriptor_number._base_unit()) + descriptor_number.name=descriptor_number.unique_name + return descriptor_number + + def __rmul__(self, other: numbers.Number) -> DescriptorNumber: + if isinstance(other, numbers.Number): + new_value = other * self.full_value + else: + return NotImplemented + descriptor_number= DescriptorNumber.from_scipp(name=self.name, full_value=new_value) + descriptor_number.name=descriptor_number.unique_name + return descriptor_number + + def __truediv__(self, other: Union[DescriptorNumber, numbers.Number]) -> DescriptorNumber: + if isinstance(other, numbers.Number): + original_other = other + if other == 0: + raise ZeroDivisionError("Cannot divide by zero") + new_value = self.full_value / other + elif type(other) is DescriptorNumber: + original_other = other.value + if original_other == 0: + raise ZeroDivisionError("Cannot divide by zero") + new_value = self.full_value / other.full_value + other.value = original_other + else: + return NotImplemented + descriptor_number = DescriptorNumber.from_scipp(name=self.name, full_value=new_value) + descriptor_number.convert_unit(descriptor_number._base_unit()) + descriptor_number.name=descriptor_number.unique_name + return descriptor_number + + def __rtruediv__(self, other: numbers.Number) -> DescriptorNumber: + if isinstance(other, numbers.Number): + if self.value == 0: + raise ZeroDivisionError("Cannot divide by zero") + new_value = other / self.full_value + else: + return NotImplemented + descriptor_number= DescriptorNumber.from_scipp(name=self.name, full_value=new_value) + descriptor_number.name=descriptor_number.unique_name + return descriptor_number + + def __pow__(self, other: Union[DescriptorNumber, numbers.Number]) -> DescriptorNumber: + if isinstance(other, numbers.Number): + exponent = other + elif type(other) is DescriptorNumber: + if other.unit != 'dimensionless': + raise UnitError("Exponents must be dimensionless") + if other.variance is not None: + raise ValueError("Exponents must not have variance") + exponent = other.value + else: + return NotImplemented + try: + new_value = self.full_value ** exponent + except Exception as message: + raise message from None + if np.isnan(new_value.value): + raise ValueError("The result of the exponentiation is not a number") + descriptor_number= DescriptorNumber.from_scipp(name=self.name, full_value=new_value) + descriptor_number.name=descriptor_number.unique_name + return descriptor_number + + def __rpow__(self, other: numbers.Number) -> numbers.Number: + if isinstance(other, numbers.Number): + if self.unit != 'dimensionless': + raise UnitError("Exponents must be dimensionless") + if self.variance is not None: + raise ValueError("Exponents must not have variance") + new_value = other ** self.value + else: + return NotImplemented + return new_value + + def __neg__(self) -> DescriptorNumber: + new_value = -self.full_value + descriptor_number= DescriptorNumber.from_scipp(name=self.name, full_value=new_value) + descriptor_number.name=descriptor_number.unique_name + return descriptor_number + + def __abs__(self) -> DescriptorNumber: + new_value = abs(self.full_value) + descriptor_number= DescriptorNumber.from_scipp(name=self.name, full_value=new_value) + descriptor_number.name=descriptor_number.unique_name + return descriptor_number + + def _base_unit(self) -> str: + string = str(self._scalar.unit) + for i, letter in enumerate(string): + if letter == "e": + if string[i:i+2] not in ["e+", "e-"]: + return string[i:] + elif letter not in ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ".", "+", "-"]: + return string[i:] + return "" \ No newline at end of file diff --git a/src/easyscience/Objects/new_variable/parameter.py b/src/easyscience/Objects/new_variable/parameter.py index 8b3c39a6..b48f6a91 100644 --- a/src/easyscience/Objects/new_variable/parameter.py +++ b/src/easyscience/Objects/new_variable/parameter.py @@ -17,6 +17,7 @@ import numpy as np import scipp as sc +from scipp import UnitError from scipp import Variable from easyscience import global_object @@ -45,8 +46,8 @@ def __init__( value: numbers.Number, unit: Optional[Union[str, sc.Unit]] = '', variance: Optional[numbers.Number] = 0.0, - min: Optional[numbers.Number] = -np.Inf, - max: Optional[numbers.Number] = np.Inf, + min: Optional[numbers.Number] = -np.inf, + max: Optional[numbers.Number] = np.inf, fixed: Optional[bool] = False, unique_name: Optional[str] = None, description: Optional[str] = None, @@ -85,8 +86,14 @@ def __init__( raise ValueError(f'{value=} can not be less than {min=}') if value > max: raise ValueError(f'{value=} can not be greater than {max=}') + + if np.isclose(min, max, rtol=1e-9, atol=0.0): + raise ValueError('The min and max bounds cannot be identical. Please use fixed=True instead to fix the value.') if not isinstance(fixed, bool): raise TypeError('`fixed` must be either True or False') + + self._min = sc.scalar(float(min), unit=unit) + self._max = sc.scalar(float(max), unit=unit) super().__init__( name=name, @@ -105,8 +112,6 @@ def __init__( weakref.finalize(self, self._callback.fdel) # Create additional fitting elements - self._min = sc.scalar(float(min), unit=unit) - self._max = sc.scalar(float(max), unit=unit) self._fixed = fixed self._enabled = enabled self._initial_scalar = copy.deepcopy(self._scalar) @@ -117,6 +122,7 @@ def __init__( } self._constraints = Constraints(builtin=builtin_constraint, user={}, virtual={}) + @property def value_no_call_back(self) -> numbers.Number: """ @@ -177,7 +183,7 @@ def value(self) -> numbers.Number: @property_stack_deco def value(self, value: numbers.Number) -> None: """ - Set the value of self. This only update the value of the scipp scalar. + Set the value of self. This only updates the value of the scipp scalar. :param value: New value of self """ @@ -246,6 +252,8 @@ def min(self, min_value: numbers.Number) -> None: """ if not isinstance(min_value, numbers.Number): raise TypeError('`min` must be a number') + if np.isclose(min_value, self._max.value, rtol=1e-9, atol=0.0): + raise ValueError('The min and max bounds cannot be identical. Please use fixed=True instead to fix the value.') if min_value <= self.value: self._min.value = min_value else: @@ -272,6 +280,8 @@ def max(self, max_value: numbers.Number) -> None: """ if not isinstance(max_value, numbers.Number): raise TypeError('`max` must be a number') + if np.isclose(max_value, self._min.value, rtol=1e-9, atol=0.0): + raise ValueError('The min and max bounds cannot be identical. Please use fixed=True instead to fix the value.') if max_value >= self.value: self._max.value = max_value else: @@ -418,5 +428,311 @@ def __repr__(self) -> str: s.append('bounds=[%s:%s]' % (repr(self.min), repr(self.max))) return '%s>' % ', '.join(s) - def __float__(self) -> float: - return float(self._scalar.value) + # Seems redundant + # def __float__(self) -> float: + # return float(self._scalar.value) + + def __add__(self, other: Union[DescriptorNumber, Parameter, numbers.Number]) -> Parameter: + if isinstance(other, numbers.Number): + if self.unit != 'dimensionless': + raise UnitError("Numbers can only be added to dimensionless values") + new_full_value = self.full_value + other + min_value = self.min + other + max_value = self.max + other + elif isinstance(other, DescriptorNumber): # Parameter inherits from DescriptorNumber and is also handled here + other_unit = other.unit + try: + other.convert_unit(self.unit) + except UnitError: + raise UnitError(f"Values with units {self.unit} and {other.unit} cannot be added") from None + new_full_value = self.full_value + other.full_value + min_value = self.min + other.min if isinstance(other, Parameter) else self.min + other.value + max_value = self.max + other.max if isinstance(other, Parameter) else self.max + other.value + other.convert_unit(other_unit) + else: + return NotImplemented + parameter=Parameter.from_scipp(name=self.name, full_value=new_full_value, min=min_value, max=max_value) + parameter.name=parameter.unique_name + return parameter + + def __radd__(self, other: Union[DescriptorNumber, numbers.Number]) -> Parameter: + if isinstance(other, numbers.Number): + if self.unit != 'dimensionless': + raise UnitError("Numbers can only be added to dimensionless values") + new_full_value = self.full_value + other + min_value = self.min + other + max_value = self.max + other + elif isinstance(other, DescriptorNumber): # Parameter inherits from DescriptorNumber and is also handled here + original_unit = self.unit + try: + self.convert_unit(other.unit) + except UnitError: + raise UnitError(f"Values with units {other.unit} and {self.unit} cannot be added") from None + new_full_value = self.full_value + other.full_value + min_value = self.min + other.value + max_value = self.max + other.value + self.convert_unit(original_unit) + else: + return NotImplemented + parameter=Parameter.from_scipp(name=self.name, full_value=new_full_value, min=min_value, max=max_value) + parameter.name=parameter.unique_name + return parameter + + def __sub__(self, other: Union[DescriptorNumber, Parameter, numbers.Number]) -> Parameter: + if isinstance(other, numbers.Number): + if self.unit != 'dimensionless': + raise UnitError("Numbers can only be subtracted from dimensionless values") + new_full_value = self.full_value - other + min_value = self.min - other + max_value = self.max - other + elif isinstance(other, DescriptorNumber): # Parameter inherits from DescriptorNumber and is also handled here + other_unit = other.unit + try: + other.convert_unit(self.unit) + except UnitError: + raise UnitError(f"Values with units {self.unit} and {other.unit} cannot be subtracted") from None + new_full_value = self.full_value - other.full_value + if isinstance(other, Parameter): + min_value = self.min - other.max if other.max != np.inf else -np.inf + max_value = self.max - other.min if other.min != -np.inf else np.inf + else: + min_value = self.min - other.value + max_value = self.max - other.value + other.convert_unit(other_unit) + else: + return NotImplemented + parameter=Parameter.from_scipp(name=self.name, full_value=new_full_value, min=min_value, max=max_value) + parameter.name=parameter.unique_name + return parameter + + def __rsub__(self, other: Union[DescriptorNumber, numbers.Number]) -> Parameter: + if isinstance(other, numbers.Number): + if self.unit != 'dimensionless': + raise UnitError("Numbers can only be subtracted from dimensionless values") + new_full_value = other - self.full_value + min_value = other - self.max + max_value = other - self.min + elif isinstance(other, DescriptorNumber): # Parameter inherits from DescriptorNumber and is also handled here + original_unit = self.unit + try: + self.convert_unit(other.unit) + except UnitError: + raise UnitError(f"Values with units {other.unit} and {self.unit} cannot be subtracted") from None + new_full_value = other.full_value - self.full_value + min_value = other.value - self.max + max_value = other.value - self.min + self.convert_unit(original_unit) + else: + return NotImplemented + parameter=Parameter.from_scipp(name=self.name, full_value=new_full_value, min=min_value, max=max_value) + parameter.name=parameter.unique_name + return parameter + + def __mul__(self, other: Union[DescriptorNumber, Parameter, numbers.Number]) -> Parameter: + if isinstance(other, numbers.Number): + new_full_value = self.full_value * other + if other == 0: + descriptor_number= DescriptorNumber.from_scipp(name=self.name, full_value=new_full_value) + descriptor_number.name=descriptor_number.unique_name + return descriptor_number + combinations = [self.min * other, self.max * other] + elif isinstance(other, DescriptorNumber): # Parameter inherits from DescriptorNumber and is also handled here + new_full_value = self.full_value * other.full_value + if other.value == 0 and type(other) is DescriptorNumber: # Only return DescriptorNumber if other is strictly 0, i.e. not a parameter # noqa: E501 + descriptor_number= DescriptorNumber.from_scipp(name=self.name, full_value=new_full_value) + descriptor_number.name=descriptor_number.unique_name + return descriptor_number + if isinstance(other, Parameter): + combinations = [] + for first, second in [(self.min, other.min), (self.min, other.max), (self.max, other.min), (self.max, other.max)]: # noqa: E501 + if first == 0 and np.isinf(second): + combinations.append(0) + elif second == 0 and np.isinf(first): + combinations.append(0) + else: + combinations.append(first * second) + else: + combinations = [self.min * other.value, self.max * other.value] + else: + return NotImplemented + min_value = min(combinations) + max_value = max(combinations) + parameter = Parameter.from_scipp(name=self.name, full_value=new_full_value, min=min_value, max=max_value) + parameter.convert_unit(parameter._base_unit()) + parameter.name=parameter.unique_name + return parameter + + def __rmul__(self, other: Union[DescriptorNumber, numbers.Number]) -> Parameter: + if isinstance(other, numbers.Number): + new_full_value = other * self.full_value + if other == 0: + descriptor_number= DescriptorNumber.from_scipp(name=self.name, full_value=new_full_value) + descriptor_number.name=descriptor_number.unique_name + return descriptor_number + combinations = [other * self.min, other * self.max] + elif isinstance(other, DescriptorNumber): # Parameter inherits from DescriptorNumber and is also handled here + new_full_value = other.full_value * self.full_value + if other.value == 0: + descriptor_number= DescriptorNumber.from_scipp(name=self.name, full_value=new_full_value) + descriptor_number.name=descriptor_number.unique_name + return descriptor_number + combinations = [self.min * other.value, self.max * other.value] + else: + return NotImplemented + min_value = min(combinations) + max_value = max(combinations) + parameter = Parameter.from_scipp(name=self.name, full_value=new_full_value, min=min_value, max=max_value) + parameter.convert_unit(parameter._base_unit()) + parameter.name=parameter.unique_name + return parameter + + def __truediv__(self, other: Union[DescriptorNumber, Parameter, numbers.Number]) -> Parameter: + if isinstance(other, numbers.Number): + if other == 0: + raise ZeroDivisionError("Cannot divide by zero") + new_full_value = self.full_value / other + combinations = [self.min / other, self.max / other] + elif isinstance(other, DescriptorNumber): # Parameter inherits from DescriptorNumber and is also handled here + other_value = other.value + if other_value == 0: + raise ZeroDivisionError("Cannot divide by zero") + new_full_value = self.full_value / other.full_value + if isinstance(other, Parameter): + if (other.min < 0 and other.max > 0): + combinations = [-np.inf, np.inf] + elif other.min == 0: + if (self.min < 0 and self.max > 0): + combinations = [-np.inf, np.inf] + elif self.min >= 0: + combinations = [self.min/other.max, np.inf] + elif self.max <= 0: + combinations = [-np.inf, self.max/other.max] + elif other.max == 0: + if (self.min < 0 and self.max > 0): + combinations = [-np.inf, np.inf] + elif self.min >= 0: + combinations = [-np.inf, self.min/other.min] + elif self.max <= 0: + combinations = [self.max/other.min, np.inf] + else: + combinations = [self.min/other.min, self.max/other.max, self.min/other.max, self.max/other.min] + else: + combinations = [self.min / other.value, self.max / other.value] + other.value = other_value + else: + return NotImplemented + min_value = min(combinations) + max_value = max(combinations) + parameter = Parameter.from_scipp(name=self.name, full_value=new_full_value, min=min_value, max=max_value) + parameter.convert_unit(parameter._base_unit()) + parameter.name=parameter.unique_name + return parameter + + def __rtruediv__(self, other: Union[DescriptorNumber, numbers.Number]) -> Parameter: + original_self = self.value + if original_self == 0: + raise ZeroDivisionError("Cannot divide by zero") + if isinstance(other, numbers.Number): + new_full_value = other / self.full_value + other_value = other + if other_value == 0: + descriptor_number= DescriptorNumber.from_scipp(name=self.name, full_value=new_full_value) + descriptor_number.name=descriptor_number.unique_name + return descriptor_number + elif isinstance(other, DescriptorNumber): # Parameter inherits from DescriptorNumber and is also handled here + new_full_value = other.full_value / self.full_value + other_value = other.value + if other_value == 0: + descriptor_number= DescriptorNumber.from_scipp(name=self.name, full_value=new_full_value) + descriptor_number.name=descriptor_number.unique_name + return descriptor_number + else: + return NotImplemented + if (self.min < 0 and self.max > 0): + combinations = [-np.inf, np.inf] + elif self.min == 0: + if other_value > 0: + combinations = [other_value/self.max, np.inf] + elif other_value < 0: + combinations = [-np.inf, other_value/self.max] + elif self.max == 0: + if other_value > 0: + combinations = [-np.inf, other_value/self.min] + elif other_value < 0: + combinations = [other_value/self.min, np.inf] + else: + combinations = [other_value / self.min, other_value / self.max] + min_value = min(combinations) + max_value = max(combinations) + parameter = Parameter.from_scipp(name=self.name, full_value=new_full_value, min=min_value, max=max_value) + parameter.convert_unit(parameter._base_unit()) + parameter.name=parameter.unique_name + self.value = original_self + return parameter + + def __pow__(self, other: Union[DescriptorNumber, numbers.Number]) -> Parameter: + if isinstance(other, numbers.Number): + exponent = other + elif type(other) is DescriptorNumber: # Strictly a DescriptorNumber, We can't raise to the power of a Parameter + if other.unit != 'dimensionless': + raise UnitError("Exponents must be dimensionless") + if other.variance is not None: + raise ValueError("Exponents must not have variance") + exponent = other.value + else: + return NotImplemented + + try: + new_full_value = self.full_value ** exponent + except Exception as message: + raise message from None + + if np.isnan(new_full_value.value): + raise ValueError("The result of the exponentiation is not a number") + if exponent == 0: + descriptor_number= DescriptorNumber.from_scipp(name=self.name, full_value=new_full_value) + descriptor_number.name=descriptor_number.unique_name + return descriptor_number + elif exponent < 0: + if self.min < 0 and self.max > 0: + combinations = [-np.inf, np.inf] + elif self.min == 0: + combinations = [self.max ** exponent, np.inf] + elif self.max == 0: + combinations = [-np.inf, self.min ** exponent] + else: + combinations = [self.min ** exponent, self.max ** exponent] + else: + combinations = [self.min ** exponent, self.max ** exponent] + if exponent % 2 == 0: + if self.min < 0 and self.max > 0: + combinations.append(0) + combinations = [abs(combination) for combination in combinations] + elif exponent % 1 != 0: + if self.min < 0: + combinations.append(0) + combinations = [combination for combination in combinations if combination >= 0] + min_value = min(combinations) + max_value = max(combinations) + parameter=Parameter.from_scipp(name=self.name, full_value=new_full_value, min=min_value, max=max_value) + parameter.name=parameter.unique_name + return parameter + + def __neg__(self) -> Parameter: + new_full_value = -self.full_value + min_value = -self.max + max_value = -self.min + parameter=Parameter.from_scipp(name=self.name, full_value=new_full_value, min=min_value, max=max_value) + parameter.name=parameter.unique_name + return parameter + + def __abs__(self) -> Parameter: + new_full_value = abs(self.full_value) + combinations = [abs(self.min), abs(self.max)] + if self.min < 0 and self.max > 0: + combinations.append(0) + min_value = min(combinations) + max_value = max(combinations) + parameter=Parameter.from_scipp(name=self.name, full_value=new_full_value, min=min_value, max=max_value) + parameter.name=parameter.unique_name + return parameter diff --git a/src/easyscience/fitting/minimizers/minimizer_base.py b/src/easyscience/fitting/minimizers/minimizer_base.py index 9836bdfb..59ef4133 100644 --- a/src/easyscience/fitting/minimizers/minimizer_base.py +++ b/src/easyscience/fitting/minimizers/minimizer_base.py @@ -16,8 +16,8 @@ import numpy as np -# causes circular import when Parameter is imported -# from easyscience.Objects.ObjectClasses import BaseObj +#causes circular import when Parameter is imported +#from easyscience.Objects.ObjectClasses import BaseObj from easyscience.Objects.Variable import Parameter from ..Constraints import ObjConstraint diff --git a/tests/unit_tests/Objects/new_variable/test_descriptor_number.py b/tests/unit_tests/Objects/new_variable/test_descriptor_number.py index c5f8a09a..75edc6dd 100644 --- a/tests/unit_tests/Objects/new_variable/test_descriptor_number.py +++ b/tests/unit_tests/Objects/new_variable/test_descriptor_number.py @@ -1,6 +1,7 @@ import pytest from unittest.mock import MagicMock import scipp as sc +from scipp import UnitError from easyscience.Objects.new_variable.descriptor_number import DescriptorNumber from easyscience import global_object @@ -56,7 +57,7 @@ def test_init_sc_unit(self): def test_init_sc_unit_unknown(self): # When Then Expect - with pytest.raises(ValueError): + with pytest.raises(UnitError): DescriptorNumber( name="name", value=1, @@ -198,4 +199,272 @@ def test_as_data_dict(self, clear, descriptor: DescriptorNumber): "url": "url", "display_name": "display_name", "unique_name": "DescriptorNumber_0", - } \ No newline at end of file + } + + @pytest.mark.parametrize("unit_string, expected", [ + ("1e+9", "dimensionless"), + ("1000", "dimensionless"), + ("10dm^2", "m^2")], + ids=["scientific_notation", "numbers", "unit_prefix"]) + def test_base_unit(self, unit_string, expected): + # When + descriptor = DescriptorNumber(name="name", value=1, unit=unit_string) + + # Then + base_unit = descriptor._base_unit() + + # Expect + assert base_unit == expected + + @pytest.mark.parametrize("test, expected", [ + (DescriptorNumber("test", 2, "m", 0.01,), DescriptorNumber("test + name", 3, "m", 0.11)), + (DescriptorNumber("test", 2, "cm", 0.01), DescriptorNumber("test + name", 102, "cm", 1000.01))], + ids=["regular", "unit_conversion"]) + def test_addition(self, descriptor: DescriptorNumber, test, expected): + # When Then + result = test + descriptor + + # Expect + assert type(result) == DescriptorNumber + assert result.name == result.unique_name + assert result.value == expected.value + assert result.unit == expected.unit + assert result.variance == expected.variance + + assert descriptor.unit == 'm' + + def test_addition_with_scalar(self): + # When + descriptor = DescriptorNumber(name="name", value=1, variance=0.1) + + # Then + result = descriptor + 1.0 + result_reverse = 1.0 + descriptor + + # Expect + assert type(result) == DescriptorNumber + assert result.name == result.unique_name + assert result.value == 2.0 + assert result.unit == "dimensionless" + assert result.variance == 0.1 + + assert type(result_reverse) == DescriptorNumber + assert result_reverse.name == result_reverse.unique_name + assert result_reverse.value == 2.0 + assert result_reverse.unit == "dimensionless" + assert result_reverse.variance == 0.1 + + @pytest.mark.parametrize("test", [1.0, DescriptorNumber("test", 2, "s",)], ids=["add_scalar_to_unit", "incompatible_units"]) + def test_addition_exception(self, descriptor: DescriptorNumber, test): + # When Then Expect + with pytest.raises(UnitError): + result = descriptor + test + with pytest.raises(UnitError): + result_reverse = test + descriptor + + @pytest.mark.parametrize("test, expected", [ + (DescriptorNumber("test", 2, "m", 0.01,), DescriptorNumber("test - name", 1, "m", 0.11)), + (DescriptorNumber("test", 2, "cm", 0.01), DescriptorNumber("test - name", -98, "cm", 1000.01))], + ids=["regular", "unit_conversion"]) + def test_subtraction(self, descriptor: DescriptorNumber, test, expected): + # When Then + result = test - descriptor + + # Expect + assert type(result) == DescriptorNumber + assert result.name == result.unique_name + assert result.value == expected.value + assert result.unit == expected.unit + assert result.variance == expected.variance + + assert descriptor.unit == 'm' + + def test_subtraction_with_scalar(self): + # When + descriptor = DescriptorNumber(name="name", value=2, variance=0.1) + + # Then + result = descriptor - 1.0 + result_reverse = 1.0 - descriptor + + # Expect + assert type(result) == DescriptorNumber + assert result.name == result.unique_name + assert result.value == 1.0 + assert result.unit == "dimensionless" + assert result.variance == 0.1 + + assert type(result_reverse) == DescriptorNumber + assert result_reverse.name == result_reverse.unique_name + assert result_reverse.value == -1.0 + assert result_reverse.unit == "dimensionless" + assert result_reverse.variance == 0.1 + + @pytest.mark.parametrize("test", [1.0, DescriptorNumber("test", 2, "s",)], ids=["sub_scalar_to_unit", "incompatible_units"]) + def test_subtraction_exception(self, descriptor: DescriptorNumber, test): + # When Then Expect + with pytest.raises(UnitError): + result = test - descriptor + with pytest.raises(UnitError): + result_reverse = descriptor - test + + @pytest.mark.parametrize("test, expected", [ + (DescriptorNumber("test", 2, "m", 0.01,), DescriptorNumber("test * name", 2, "m^2", 0.41)), + (DescriptorNumber("test", 2, "dm", 0.01), DescriptorNumber("test * name", 0.2, "m^2", 0.0041))], + ids=["regular", "base_unit_conversion"]) + def test_multiplication(self, descriptor: DescriptorNumber, test, expected): + # When Then + result = test * descriptor + + # Expect + assert type(result) == DescriptorNumber + assert result.name == result.unique_name + assert result.value == expected.value + assert result.unit == expected.unit + assert result.variance == pytest.approx(expected.variance) + + def test_multiplication_with_scalar(self, descriptor: DescriptorNumber): + # When Then + result = descriptor * 2.0 + result_reverse = 2.0 * descriptor + + # Expect + assert type(result) == DescriptorNumber + assert result.name == result.unique_name + assert result.value == 2.0 + assert result.unit == "m" + assert result.variance == 0.4 + + assert type(result_reverse) == DescriptorNumber + assert result_reverse.name == result_reverse.unique_name + assert result_reverse.value == 2.0 + assert result_reverse.unit == "m" + assert result_reverse.variance == 0.4 + + @pytest.mark.parametrize("test, expected, expected_reverse", [ + (DescriptorNumber("test", 2, "m^2", 0.01,), DescriptorNumber("name / test", 0.5, "1/m", 0.025625), DescriptorNumber("test / name", 2, "m", 0.41)), + (2, DescriptorNumber("name / 2", 0.5, "m", 0.025), DescriptorNumber("2 / name", 2, "1/m", 0.4))], + ids=["descriptorNumber", "scalar"]) + def test_division(self, descriptor: DescriptorNumber, test, expected, expected_reverse): + # When Then + result = descriptor / test + result_reverse = test / descriptor + + # Expect + assert type(result) == DescriptorNumber + assert result.name == result.unique_name + assert result.value == expected.value + assert result.unit == expected.unit + assert result.variance == pytest.approx(expected.variance) + + assert type(result_reverse) == DescriptorNumber + assert result_reverse.name == result_reverse.unique_name + assert result_reverse.value == expected_reverse.value + assert result_reverse.unit == expected_reverse.unit + assert result_reverse.variance == pytest.approx(expected_reverse.variance) + + @pytest.mark.parametrize("test", [0, DescriptorNumber("test", 0, "m", 0.01)], ids=["zero", "zero_descriptor"]) + def test_division_exception(self, descriptor: DescriptorNumber, test): + # When Then Expect + with pytest.raises(ZeroDivisionError): + result = descriptor / test + + def test_division_exception_reverse(self): + # When + descriptor = DescriptorNumber(name="name", value=0, variance=0.1) + + # Then Expect + with pytest.raises(ZeroDivisionError): + result = 2 / descriptor + + @pytest.mark.parametrize("test, expected", [ + (DescriptorNumber("test", 2), DescriptorNumber("name ** test", 4, unit="m^2", variance=1.6)), + (2, DescriptorNumber("name ** 2", 4, unit="m^2", variance=1.6)), + (-2, DescriptorNumber("name ** -2", 0.25, unit="1/m^2", variance=0.00625))], + ids=["descriptorNumber", "scalar", "negative_scalar"]) + def test_power_of_descriptor(self, test, expected): + # When + descriptor = DescriptorNumber(name="name", value=2, unit="m", variance=0.1) + + # Then + result = descriptor ** test + + # Expect + assert type(result) == DescriptorNumber + assert result.name == result.unique_name + assert result.value == expected.value + assert result.unit == expected.unit + assert result.variance == expected.variance + + def test_power_of_dimensionless_descriptor(self): + # When + descriptor = DescriptorNumber(name="name", value=2, unit="dimensionless", variance=0.1) + + # Then + result = descriptor ** 0.5 + + # Expect + assert type(result) == DescriptorNumber + assert result.name == result.unique_name + assert result.value == 1.4142135623730951 + assert result.unit == "dimensionless" + assert result.variance == pytest.approx(0.0125) + + @pytest.mark.parametrize("descriptor, exponent, exception", [ + (DescriptorNumber("name", 2), DescriptorNumber("test", 2, unit="m"), UnitError), + (DescriptorNumber("name", 2), DescriptorNumber("test", 2, variance=0.1), ValueError), + (DescriptorNumber("name", 2, unit="m"), 0.5, UnitError), + (DescriptorNumber("name", -2), 0.5, ValueError)], + ids=["descriptor_unit", "descriptor_variance", "fractional_of_unit", "fractonal_of_negative"]) + def test_power_of_descriptor_exceptions(self, descriptor, exponent, exception): + # When Then Expect + with pytest.raises(exception): + result = descriptor ** exponent + + + def test_descriptor_as_exponentiation(self): + # When + descriptor = DescriptorNumber(name="name", value=2) + + # Then + result = 2 ** descriptor + + # Expect + assert result == 4 + + @pytest.mark.parametrize("exponent, exception", [ + (DescriptorNumber("test", 2, unit="m"), UnitError), + (DescriptorNumber("test", 2, variance=0.1), ValueError)], + ids=["descriptor_unit", "descriptor_variance"]) + def test_descriptor_as_exponentiation_exception(self, exponent, exception): + # When Then Expect + with pytest.raises(exception): + result = 2 ** exponent + + def test_negation(self): + # When + descriptor = DescriptorNumber(name="name", unit="m", value=2, variance=0.1) + + # Then + result = -descriptor + + # Expect + assert type(result) == DescriptorNumber + assert result.name == result.unique_name + assert result.value == -2 + assert result.unit == "m" + assert result.variance == 0.1 + + def test_abs(self): + # When + descriptor = DescriptorNumber(name="name", unit="m", value=-2, variance=0.1) + + # Then + result = abs(descriptor) + + # Expect + assert type(result) == DescriptorNumber + assert result.name == result.unique_name + assert result.value == 2 + assert result.unit == "m" + assert result.variance == 0.1 \ No newline at end of file diff --git a/tests/unit_tests/Objects/new_variable/test_parameter.py b/tests/unit_tests/Objects/new_variable/test_parameter.py index 1fb776f8..017e76bd 100644 --- a/tests/unit_tests/Objects/new_variable/test_parameter.py +++ b/tests/unit_tests/Objects/new_variable/test_parameter.py @@ -1,8 +1,12 @@ import pytest from unittest.mock import MagicMock import scipp as sc +import numpy as np + +from scipp import UnitError from easyscience.Objects.new_variable.parameter import Parameter +from easyscience.Objects.new_variable.descriptor_number import DescriptorNumber from easyscience import global_object class TestParameter: @@ -163,9 +167,10 @@ def test_set_error_exception(self, parameter: Parameter): with pytest.raises(ValueError): parameter.error = -0.1 - def test_float(self, parameter: Parameter): - # When Then Expect - assert float(parameter) == 1.0 + # Commented out because __float__ method might be removed + # def test_float(self, parameter: Parameter): + # # When Then Expect + # assert float(parameter) == 1.0 def test_repr(self, parameter: Parameter): # When Then Expect @@ -334,4 +339,543 @@ def test_as_data_dict(self, clear, parameter: Parameter): "display_name": "display_name", "enabled": "enabled", "unique_name": "Parameter_0", - } \ No newline at end of file + } + + @pytest.mark.parametrize("test, expected, expected_reverse", [ + (Parameter("test", 2, "m", 0.01, -10, 20), Parameter("name + test", 3, "m", 0.02, -10, 30), Parameter("test + name", 3, "m", 0.02, -10, 30)), + (Parameter("test", 2, "m", 0.01), Parameter("name + test", 3, "m", 0.02, min=-np.Inf, max=np.Inf),Parameter("test + name", 3, "m", 0.02, min=-np.Inf, max=np.Inf)), + (Parameter("test", 2, "cm", 0.01, -10, 10), Parameter("name + test", 1.02, "m", 0.010001, -0.1, 10.1), Parameter("test + name", 102, "cm", 100.01, -10, 1010))], + ids=["regular", "no_bounds", "unit_conversion"]) + def test_addition_with_parameter(self, parameter : Parameter, test : Parameter, expected : Parameter, expected_reverse : Parameter): + # When + parameter._callback = property() + + # Then + result = parameter + test + result_reverse = test + parameter + + # Expect + assert result.name == result.unique_name + assert result.value == expected.value + assert result.unit == expected.unit + assert result.variance == expected.variance + assert result.min == expected.min + assert result.max == expected.max + + assert result_reverse.name == result_reverse.unique_name + assert result_reverse.value == expected_reverse.value + assert result_reverse.unit == expected_reverse.unit + assert result_reverse.variance == expected_reverse.variance + assert result_reverse.min == expected_reverse.min + assert result_reverse.max == expected_reverse.max + + assert parameter.unit == "m" + + def test_addition_with_scalar(self): + # When + parameter = Parameter(name="name", value=1, variance=0.01, min=0, max=10) + + # Then + result = parameter + 1.0 + result_reverse = 1.0 + parameter + + # Expect + assert result.name == result.unique_name + assert result.value == 2.0 + assert result.unit == "dimensionless" + assert result.variance == 0.01 + assert result.min == 1.0 + assert result.max == 11.0 + + assert result_reverse.name == result_reverse.unique_name + assert result_reverse.value == 2.0 + assert result_reverse.unit == "dimensionless" + assert result_reverse.variance == 0.01 + assert result_reverse.min == 1.0 + assert result_reverse.max == 11.0 + + def test_addition_with_descriptor_number(self, parameter : Parameter): + # When + parameter._callback = property() + descriptor_number = DescriptorNumber(name="test", value=1, variance=0.1, unit="cm") + + # Then + result = parameter + descriptor_number + result_reverse = descriptor_number + parameter + + # Expect + assert type(result) == Parameter + assert result.name == result.unique_name + assert result.value == 1.01 + assert result.unit == "m" + assert result.variance == 0.01001 + assert result.min == 0.01 + assert result.max == 10.01 + + assert type(result_reverse) == Parameter + assert result_reverse.name == result_reverse.unique_name + assert result_reverse.value == 101.0 + assert result_reverse.unit == "cm" + assert result_reverse.variance == 100.1 + assert result_reverse.min == 1 + assert result_reverse.max == 1001 + + assert parameter.unit == "m" + assert descriptor_number.unit == "cm" + + @pytest.mark.parametrize("test", [1.0, Parameter("test", 2, "s",)], ids=["add_scalar_to_unit", "incompatible_units"]) + def test_addition_exception(self, parameter : Parameter, test): + # When Then Expect + with pytest.raises(UnitError): + result = parameter + test + with pytest.raises(UnitError): + result_reverse = test + parameter + + @pytest.mark.parametrize("test, expected, expected_reverse", [ + (Parameter("test", 2, "m", 0.01, -20, 20), Parameter("name - test", -1, "m", 0.02, -20, 30), Parameter("test - name", 1, "m", 0.02, -30, 20)), + (Parameter("test", 2, "m", 0.01), Parameter("name - test", -1, "m", 0.02, min=-np.Inf, max=np.Inf),Parameter("test - name", 1, "m", 0.02, min=-np.Inf, max=np.Inf)), + (Parameter("test", 2, "cm", 0.01, -10, 10), Parameter("name - test", 0.98, "m", 0.010001, -0.1, 10.1), Parameter("test - name", -98, "cm", 100.01, -1010, 10))], + ids=["regular", "no_bounds", "unit_conversion"]) + def test_subtraction_with_parameter(self, parameter : Parameter, test : Parameter, expected : Parameter, expected_reverse : Parameter): + # When + parameter._callback = property() + + # Then + result = parameter - test + result_reverse = test - parameter + + # Expect + assert result.name == result.unique_name + assert result.value == expected.value + assert result.unit == expected.unit + assert result.variance == expected.variance + assert result.min == expected.min + assert result.max == expected.max + + assert result_reverse.name == result_reverse.unique_name + assert result_reverse.value == expected_reverse.value + assert result_reverse.unit == expected_reverse.unit + assert result_reverse.variance == expected_reverse.variance + assert result_reverse.min == expected_reverse.min + assert result_reverse.max == expected_reverse.max + + assert parameter.unit == "m" + + def test_subtraction_with_parameter_nan_cases(self): + # When + parameter = Parameter(name="name", value=1, variance=0.01, min=-np.Inf, max=np.Inf) + test = Parameter(name="test", value=2, variance=0.01, min=-np.Inf, max=np.Inf) + + # Then + result = parameter - test + result_reverse = test - parameter + + # Expect + assert result.name == result.unique_name + assert result.value == -1.0 + assert result.unit == "dimensionless" + assert result.variance == 0.02 + assert result.min == -np.Inf + assert result.max == np.Inf + + assert result_reverse.name == result_reverse.unique_name + assert result_reverse.value == 1.0 + assert result_reverse.unit == "dimensionless" + assert result_reverse.variance == 0.02 + assert result_reverse.min == -np.Inf + assert result_reverse.max == np.Inf + + def test_subtraction_with_scalar(self): + # When + parameter = Parameter(name="name", value=2, variance=0.01, min=0, max=10) + + # Then + result = parameter - 1.0 + result_reverse = 1.0 - parameter + + # Expect + assert result.name == result.unique_name + assert result.value == 1.0 + assert result.unit == "dimensionless" + assert result.variance == 0.01 + assert result.min == -1.0 + assert result.max == 9.0 + + assert result_reverse.name == result_reverse.unique_name + assert result_reverse.value == -1.0 + assert result_reverse.unit == "dimensionless" + assert result_reverse.variance == 0.01 + assert result_reverse.min == -9.0 + assert result_reverse.max == 1.0 + + def test_subtraction_with_descriptor_number(self, parameter : Parameter): + # When + parameter._callback = property() + descriptor_number = DescriptorNumber(name="test", value=1, variance=0.1, unit="cm") + + # Then + result = parameter - descriptor_number + result_reverse = descriptor_number - parameter + + # Expect + assert type(result) == Parameter + assert result.name == result.unique_name + assert result.value == 0.99 + assert result.unit == "m" + assert result.variance == 0.01001 + assert result.min == -0.01 + assert result.max == 9.99 + + assert type(result_reverse) == Parameter + assert result_reverse.name == result_reverse.unique_name + assert result_reverse.value == -99.0 + assert result_reverse.unit == "cm" + assert result_reverse.variance == 100.1 + assert result_reverse.min == -999 + assert result_reverse.max == 1 + + assert parameter.unit == "m" + assert descriptor_number.unit == "cm" + + @pytest.mark.parametrize("test", [1.0, Parameter("test", 2, "s",)], ids=["sub_scalar_to_unit", "incompatible_units"]) + def test_subtraction_exception(self, parameter : Parameter, test): + # When Then Expect + with pytest.raises(UnitError): + result = parameter - test + with pytest.raises(UnitError): + result_reverse = test - parameter + + @pytest.mark.parametrize("test, expected, expected_reverse", [ + (Parameter("test", 2, "m", 0.01, -10, 20), Parameter("name * test", 2, "m^2", 0.05, -100, 200), Parameter("test * name", 2, "m^2", 0.05, -100, 200)), + (Parameter("test", 2, "m", 0.01), Parameter("name * test", 2, "m^2", 0.05, min=-np.Inf, max=np.Inf), Parameter("test * name", 2, "m^2", 0.05, min=-np.Inf, max=np.Inf)), + (Parameter("test", 2, "dm", 0.01, -10, 20), Parameter("name * test", 0.2, "m^2", 0.0005, -10, 20), Parameter("test * name", 0.2, "m^2", 0.0005, -10, 20))], + ids=["regular", "no_bounds", "base_unit_conversion"]) + def test_multiplication_with_parameter(self, parameter : Parameter, test : Parameter, expected : Parameter, expected_reverse : Parameter): + # When + parameter._callback = property() + + # Then + result = parameter * test + result_reverse = test * parameter + + # Expect + assert result.name == result.unique_name + assert result.value == expected.value + assert result.unit == expected.unit + assert result.variance == pytest.approx(expected.variance) + assert result.min == expected.min + assert result.max == expected.max + + assert result_reverse.name == result_reverse.unique_name + assert result_reverse.value == expected_reverse.value + assert result_reverse.unit == expected_reverse.unit + assert result_reverse.variance == pytest.approx(expected_reverse.variance) + assert result_reverse.min == expected_reverse.min + assert result_reverse.max == expected_reverse.max + + @pytest.mark.parametrize("test, expected, expected_reverse", [ + (Parameter("test", 0, "", 0.01, -10, 0), Parameter("name * test", 0.0, "dimensionless", 0.01, -np.Inf, 0), Parameter("test * name", 0, "dimensionless", 0.01, -np.Inf, 0)), + (Parameter("test", 0, "", 0.01, 0, 10), Parameter("name * test", 0.0, "dimensionless", 0.01, 0, np.Inf), Parameter("test * name", 0, "dimensionless", 0.01, 0, np.Inf))], + ids=["zero_min", "zero_max"]) + def test_multiplication_with_parameter_nan_cases(self, test, expected, expected_reverse): + # When + parameter = Parameter(name="name", value=1, variance=0.01, min=1, max=np.Inf) + + # Then + result = parameter * test + result_reverse = test * parameter + + # Expect + assert result.name == result.unique_name + assert result.value == expected.value + assert result.unit == expected.unit + assert result.variance == expected.variance + assert result.min == expected.min + assert result.max == expected.max + + assert result_reverse.name == result_reverse.unique_name + assert result_reverse.value == expected_reverse.value + assert result_reverse.unit == expected_reverse.unit + assert result_reverse.variance == expected_reverse.variance + assert result_reverse.min == expected_reverse.min + assert result_reverse.max == expected_reverse.max + + @pytest.mark.parametrize("test, expected, expected_reverse", [ + (DescriptorNumber(name="test", value=2, variance=0.1, unit="cm"), Parameter("name * test", 2, "dm^2", 0.14, 0, 20), Parameter("test * name", 2, "dm^2", 0.14, 0, 20)), + (DescriptorNumber(name="test", value=0, variance=0.1, unit="cm"), DescriptorNumber("name * test", 0, "dm^2", 0.1), DescriptorNumber("test * name", 0, "dm^2", 0.1))], + ids=["regular", "zero_value"]) + def test_multiplication_with_descriptor_number(self, parameter : Parameter, test, expected, expected_reverse): + # When + parameter._callback = property() + + # Then + result = parameter * test + result_reverse = test * parameter + + # Expect + assert type(result) == type(expected) + assert result.name == result.unique_name + assert result.value == expected.value + assert result.unit == expected.unit + assert result.variance == expected.variance + if isinstance(result, Parameter): + assert result.min == expected.min + assert result.max == expected.max + + assert type(result_reverse) == type(expected_reverse) + assert result_reverse.name == result_reverse.unique_name + assert result_reverse.value == expected_reverse.value + assert result_reverse.unit == expected_reverse.unit + assert result_reverse.variance == expected_reverse.variance + if isinstance(result_reverse, Parameter): + assert result_reverse.min == expected_reverse.min + assert result_reverse.max == expected_reverse.max + + @pytest.mark.parametrize("test, expected, expected_reverse", [ + (2, Parameter("name * 2", 2, "m", 0.04, 0, 20), Parameter("2 * name", 2, "m", 0.04, 0, 20)), + (0, DescriptorNumber("name * 0", 0, "m", 0), DescriptorNumber("0 * name", 0, "m", 0))], + ids=["regular", "zero_value"]) + def test_multiplication_with_scalar(self, parameter : Parameter, test, expected, expected_reverse): + # When + parameter._callback = property() + + # Then + result = parameter * test + result_reverse = test * parameter + + # Expect + assert type(result) == type(expected) + assert result.name == result.unique_name + assert result.value == expected.value + assert result.unit == expected.unit + assert result.variance == expected.variance + if isinstance(result, Parameter): + assert result.min == expected.min + assert result.max == expected.max + + assert result_reverse.name == result_reverse.unique_name + assert result_reverse.value == expected_reverse.value + assert result_reverse.unit == expected_reverse.unit + assert result_reverse.variance == expected_reverse.variance + if isinstance(result_reverse, Parameter): + assert result_reverse.min == expected_reverse.min + assert result_reverse.max == expected_reverse.max + + @pytest.mark.parametrize("test, expected, expected_reverse", [ + (Parameter("test", 2, "s", 0.01, -10, 20), Parameter("name / test", 0.5, "m/s", 0.003125, -np.Inf, np.Inf), Parameter("test / name", 2, "s/m", 0.05, -np.Inf, np.Inf)), + (Parameter("test", 2, "s", 0.01, 0, 20), Parameter("name / test", 0.5, "m/s", 0.003125, 0.0, np.Inf), Parameter("test / name", 2, "s/m", 0.05, 0.0, np.Inf)), + (Parameter("test", -2, "s", 0.01, -10, 0), Parameter("name / test", -0.5, "m/s", 0.003125, -np.Inf, 0.0), Parameter("test / name", -2, "s/m", 0.05, -np.Inf, 0.0))], + ids=["crossing_zero", "only_positive", "only_negative"]) + def test_division_with_parameter(self, parameter : Parameter, test, expected, expected_reverse): + # When + parameter._callback = property() + + # Then + result = parameter / test + result_reverse = test / parameter + + # Expect + assert type(result) == Parameter + assert result.name == result.unique_name + assert result.value == pytest.approx(expected.value) + assert result.unit == expected.unit + assert result.variance == pytest.approx(expected.variance) + assert result.min == expected.min + assert result.max == expected.max + + assert type(result) == Parameter + assert result_reverse.name == result_reverse.unique_name + assert result_reverse.value == pytest.approx(expected_reverse.value) + assert result_reverse.unit == expected_reverse.unit + assert result_reverse.variance == pytest.approx(expected_reverse.variance) + assert result_reverse.min == expected_reverse.min + assert result_reverse.max == expected_reverse.max + + @pytest.mark.parametrize("first, second, expected", [ + (Parameter("name", 1, "m", 0.01, -10, 20), Parameter("test", -2, "s", 0.01, -10, 0), Parameter("name / test", -0.5, "m/s", 0.003125, -np.Inf, np.Inf)), + (Parameter("name", -10, "m", 0.01, -20, -10), Parameter("test", -2, "s", 0.01, -10, 0), Parameter("name / test", 5.0, "m/s", 0.065, 1, np.Inf)), + (Parameter("name", 10, "m", 0.01, 10, 20), Parameter("test", -20, "s", 0.01, -20, -10), Parameter("name / test", -0.5, "m/s", 3.125e-5, -2, -0.5))], + ids=["first_crossing_zero_second_negative_0", "both_negative_second_negative_0", "finite_limits"]) + def test_division_with_parameter_remaining_cases(self, first, second, expected): + # When Then + result = first / second + + # Expect + assert result.name == result.unique_name + assert result.value == expected.value + assert result.unit == expected.unit + assert result.variance == expected.variance + assert result.min == expected.min + assert result.max == expected.max + + @pytest.mark.parametrize("test, expected, expected_reverse", [ + (DescriptorNumber(name="test", value=2, variance=0.1, unit="s"), Parameter("name / test", 0.5, "m/s", 0.00875, 0, 5), Parameter("test / name", 2, "s/m", 0.14, 0.2, np.Inf)), + (2, Parameter("name / 2", 0.5, "m", 0.0025, 0, 5), Parameter("2 / name", 2, "m**-1", 0.04, 0.2, np.Inf))], + ids=["descriptor_number", "number"]) + def test_division_with_descriptor_number_and_number(self, parameter : Parameter, test, expected, expected_reverse): + # When + parameter._callback = property() + + # Then + result = parameter / test + result_reverse = test / parameter + + # Expect + assert type(result) == Parameter + assert result.name == result.unique_name + assert result.value == expected.value + assert result.unit == expected.unit + assert result.variance == expected.variance + assert result.min == expected.min + assert result.max == expected.max + + assert type(result_reverse) == Parameter + assert result_reverse.name == result_reverse.unique_name + assert result_reverse.value == expected_reverse.value + assert result_reverse.unit == expected_reverse.unit + assert result_reverse.variance == expected_reverse.variance + assert result_reverse.min == expected_reverse.min + assert result_reverse.max == expected_reverse.max + + @pytest.mark.parametrize("test, expected", [ + (DescriptorNumber(name="test", value=0, variance=0.1, unit="s"), DescriptorNumber("test / name", 0.0, "s/m", 0.1)), + (0, DescriptorNumber("0 / name", 0.0, "1/m", 0.0))], + ids=["descriptor_number", "number"]) + def test_zero_value_divided_by_parameter(self, parameter : Parameter, test, expected): + # When + parameter._callback = property() + + # Then + result = test / parameter + + # Expect + assert type(result) == DescriptorNumber + assert result.name == result.unique_name + assert result.value == expected.value + assert result.unit == expected.unit + assert result.variance == expected.variance + + @pytest.mark.parametrize("first, second, expected", [ + (DescriptorNumber("name", 1, "m", 0.01), Parameter("test", 2, "s", 0.1, -10, 10), Parameter("name / test", 0.5, "m/s", 0.00875, -np.Inf, np.Inf)), + (DescriptorNumber("name", -1, "m", 0.01), Parameter("test", 2, "s", 0.1, 0, 10), Parameter("name / test", -0.5, "m/s", 0.00875, -np.Inf, -0.1)), + (DescriptorNumber("name", 1, "m", 0.01), Parameter("test", -2, "s", 0.1, -10, 0), Parameter("name / test", -0.5, "m/s", 0.00875, -np.Inf, -0.1)), + (DescriptorNumber("name", -1, "m", 0.01), Parameter("test", -2, "s", 0.1, -10, 0), Parameter("name / test", 0.5, "m/s", 0.00875, 0.1, np.Inf)), + (DescriptorNumber("name", 1, "m", 0.01), Parameter("test", 2, "s", 0.1, 1, 10), Parameter("name / test", 0.5, "m/s", 0.00875, 0.1, 1))], + ids=["crossing_zero", "positive_0_with_negative", "negative_0_with_positive", "negative_0_with_negative", "finite_limits"]) + def test_division_with_descriptor_number_missing_cases(self, first, second, expected): + # When Then + result = first / second + + # Expect + assert result.name == result.unique_name + assert result.value == expected.value + assert result.unit == expected.unit + assert result.variance == expected.variance + assert result.min == expected.min + assert result.max == expected.max + + @pytest.mark.parametrize("test", [0, DescriptorNumber("test", 0, "s", 0.1)], ids=["number", "descriptor_number"]) + def test_divide_parameter_by_zero(self, parameter : Parameter, test): + # When + parameter._callback = property() + + # Then Expect + with pytest.raises(ZeroDivisionError): + result = parameter / test + + def test_divide_by_zero_value_parameter(self): + # When + descriptor = DescriptorNumber("test", 1, "s", 0.1) + parameter = Parameter("name", 0, "m", 0.01) + + # Then Expect + with pytest.raises(ZeroDivisionError): + result = descriptor / parameter + + @pytest.mark.parametrize("test, expected", [ + (3, Parameter("name ** 3", 125, "m^3", 281.25, -125, 1000)), + (2, Parameter("name ** 2", 25, "m^2", 5.0, 0, 100)), + (-1, Parameter("name ** -1", 0.2, "1/m", 8e-5, -np.Inf, np.Inf)), + (-2, Parameter("name ** -2", 0.04, "1/m^2", 1.28e-5, 0, np.Inf)), + (0, DescriptorNumber("name ** 0", 1, "dimensionless", 0)), + (DescriptorNumber("test", 2), Parameter("name ** test", 25, "m^2", 5.0, 0, 100))], + ids=["power_3", "power_2", "power_-1", "power_-2", "power_0", "power_descriptor_number"]) + def test_power_of_parameter(self, test, expected): + # When + parameter = Parameter("name", 5, "m", 0.05, -5, 10) + + # Then + result = parameter ** test + + # Expect + assert type(result) == type(expected) + assert result.name == result.unique_name + assert result.value == expected.value + assert result.unit == expected.unit + assert result.variance == expected.variance + if isinstance(result, Parameter): + assert result.min == expected.min + assert result.max == expected.max + + @pytest.mark.parametrize("test, exponent, expected", [ + (Parameter("name", 5, "m", 0.05, 0, 10), -1, Parameter("name ** -1", 0.2, "1/m", 8e-5, 0.1, np.Inf)), + (Parameter("name", -5, "m", 0.05, -5, 0), -1, Parameter("name ** -1", -0.2, "1/m", 8e-5, -np.Inf, -0.2)), + (Parameter("name", 5, "m", 0.05, 5, 10), -1, Parameter("name ** -1", 0.2, "1/m", 8e-5, 0.1, 0.2)), + (Parameter("name", -5, "m", 0.05, -10, -5), -1, Parameter("name ** -1", -0.2, "1/m", 8e-5, -0.2, -0.1)), + (Parameter("name", -5, "m", 0.05, -10, -5), -2, Parameter("name ** -2", 0.04, "1/m^2", 1.28e-5, 0.01, 0.04)), + (Parameter("name", 5, "", 0.1, 1, 10), 0.3, Parameter("name ** 0.3", 1.6206565966927624, "", 0.0009455500095853564, 1, 1.9952623149688795)), + (Parameter("name", 5, "", 0.1), 0.5, Parameter("name ** 0.5", 2.23606797749979, "", 0.005, 0, np.Inf))], + ids=["0_positive", "negative_0", "both_positive", "both_negative_invert", "both_negative_invert_square", "fractional", "fractional_negative_limit"]) + def test_power_of_diffent_parameters(self, test, exponent, expected): + # When Then + result = test ** exponent + + # Expect + assert result.name == result.unique_name + assert result.value == expected.value + assert result.unit == expected.unit + assert result.variance == expected.variance + assert result.min == expected.min + assert result.max == expected.max + + @pytest.mark.parametrize("parameter, exponent, expected", [ + (Parameter("name", 5, "m"), DescriptorNumber("test", 2, unit="s"), UnitError), + (Parameter("name", 5, "m"), DescriptorNumber("test", 2, variance=0.01), ValueError), + (Parameter("name", 5, "m"), 0.5, UnitError), + (Parameter("name", -5, ""), 0.5, ValueError),], + ids=["exponent_unit", "exponent_variance", "exponent_fractional", "negative_base_fractional"]) + def test_power_exceptions(self, parameter, exponent, expected): + # When Then Expect + with pytest.raises(expected): + result = parameter ** exponent + + def test_negation(self): + # When + parameter = Parameter("name", 5, "m", 0.05, -5, 10) + + # Then + result = -parameter + + # Expect + assert result.name == result.unique_name + assert result.value == -5 + assert result.unit == "m" + assert result.variance == 0.05 + assert result.min == -10 + assert result.max == 5 + + @pytest.mark.parametrize("test, expected", [ + (Parameter("name", -5, "m", 0.05, -10, -5), Parameter("abs(name)", 5, "m", 0.05, 5, 10)), + (Parameter("name", 5, "m", 0.05, -10, 10), Parameter("abs(name)", 5, "m", 0.05, 0, 10))], + ids=["pure_negative", "crossing_zero"]) + def test_abs(self, test, expected): + # When Then + result = abs(test) + + # Expect + assert result.name == result.unique_name + assert result.value == expected.value + assert result.unit == expected.unit + assert result.variance == expected.variance + assert result.min == expected.min + assert result.max == expected.max \ No newline at end of file