diff --git a/src/easyscience/variable/parameter.py b/src/easyscience/variable/parameter.py index 55787ad..21dc38f 100644 --- a/src/easyscience/variable/parameter.py +++ b/src/easyscience/variable/parameter.py @@ -36,13 +36,13 @@ class Parameter(DescriptorNumber): # Used by serializer # We copy the parent's _REDIRECT and modify it to avoid altering the parent's class dict _REDIRECT = DescriptorNumber._REDIRECT.copy() - _REDIRECT['callback'] = None + _REDIRECT["callback"] = None def __init__( self, name: str, value: numbers.Number, - unit: Optional[Union[str, sc.Unit]] = '', + unit: Optional[Union[str, sc.Unit]] = "", variance: Optional[numbers.Number] = 0.0, min: Optional[numbers.Number] = -np.inf, max: Optional[numbers.Number] = np.inf, @@ -76,26 +76,30 @@ def __init__( Undo/Redo functionality is implemented for the attributes `value`, `variance`, `error`, `min`, `max`, `bounds`, `fixed`, `unit` """ # noqa: E501 # Extract and ignore serialization-specific fields from kwargs - kwargs.pop('_dependency_string', None) - kwargs.pop('_dependency_map_serializer_ids', None) - kwargs.pop('_independent', None) + kwargs.pop("_dependency_string", None) + kwargs.pop("_dependency_map_serializer_ids", None) + kwargs.pop("_independent", None) if not isinstance(min, numbers.Number): - raise TypeError('`min` must be a number') + raise TypeError("`min` must be a number") if not isinstance(max, numbers.Number): - raise TypeError('`max` must be a number') + raise TypeError("`max` must be a number") if not isinstance(value, numbers.Number): - raise TypeError('`value` must be a number') + raise TypeError("`value` must be a number") if value < min: - raise ValueError(f'{value=} can not be less than {min=}') + raise ValueError(f"{value=} can not be less than {min=}") if value > max: - raise ValueError(f'{value=} can not be greater than {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.') + 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') + raise TypeError("`fixed` must be either True or False") self._independent = True - self._fixed = fixed # For fitting, but must be initialized before super().__init__ + self._fixed = ( + fixed # For fitting, but must be initialized before super().__init__ + ) self._min = sc.scalar(float(min), unit=unit) self._max = sc.scalar(float(max), unit=unit) @@ -121,7 +125,12 @@ def __init__( @classmethod def from_dependency( - cls, name: str, dependency_expression: str, dependency_map: Optional[dict] = None, **kwargs + cls, + name: str, + dependency_expression: str, + dependency_map: Optional[dict] = None, + desired_unit: str | sc.Unit | None = None, + **kwargs, ) -> Parameter: # noqa: E501 """ Create a dependent Parameter directly from a dependency expression. @@ -129,15 +138,20 @@ def from_dependency( :param name: The name of the parameter :param dependency_expression: The dependency expression to evaluate. This should be a string which can be evaluated by the ASTEval interpreter. :param dependency_map: A dictionary of dependency expression symbol name and dependency object pairs. This is inserted into the asteval interpreter to resolve dependencies. + :param desired_unit: The desired unit of the dependent parameter. :param kwargs: Additional keyword arguments to pass to the Parameter constructor. :return: A new dependent Parameter object. """ # noqa: E501 # Set default values for required parameters for the constructor, they get overwritten by the dependency anyways - default_kwargs = {'value': 0.0, 'unit': '', 'variance': 0.0, 'min': -np.inf, 'max': np.inf} + default_kwargs = {"value": 0.0, "variance": 0.0, "min": -np.inf, "max": np.inf} # Update with user-provided kwargs, to avoid errors. default_kwargs.update(kwargs) parameter = cls(name=name, **default_kwargs) - parameter.make_dependent_on(dependency_expression=dependency_expression, dependency_map=dependency_map) + parameter.make_dependent_on( + dependency_expression=dependency_expression, + dependency_map=dependency_map, + desired_unit=desired_unit, + ) return parameter def _update(self) -> None: @@ -146,23 +160,38 @@ def _update(self) -> None: """ if not self._independent: # Update the value of the parameter using the dependency interpreter - temporary_parameter = self._dependency_interpreter(self._clean_dependency_string) + temporary_parameter = self._dependency_interpreter( + self._clean_dependency_string + ) self._scalar.value = temporary_parameter.value self._scalar.unit = temporary_parameter.unit self._scalar.variance = temporary_parameter.variance self._min.value = ( - temporary_parameter.min if isinstance(temporary_parameter, Parameter) else temporary_parameter.value + temporary_parameter.min + if isinstance(temporary_parameter, Parameter) + else temporary_parameter.value ) # noqa: E501 self._max.value = ( - temporary_parameter.max if isinstance(temporary_parameter, Parameter) else temporary_parameter.value + temporary_parameter.max + if isinstance(temporary_parameter, Parameter) + else temporary_parameter.value ) # noqa: E501 self._min.unit = temporary_parameter.unit self._max.unit = temporary_parameter.unit + + if self._desired_unit is not None: + self._convert_unit(self._desired_unit) + self._notify_observers() else: - warnings.warn('This parameter is not dependent. It cannot be updated.') + warnings.warn("This parameter is not dependent. It cannot be updated.") - def make_dependent_on(self, dependency_expression: str, dependency_map: Optional[dict] = None) -> None: + def make_dependent_on( + self, + dependency_expression: str, + dependency_map: Optional[dict] = None, + desired_unit: str | sc.Unit | None = None, + ) -> None: """ Make this parameter dependent on another parameter. This will overwrite the current value, unit, variance, min and max. @@ -183,24 +212,29 @@ def make_dependent_on(self, dependency_expression: str, dependency_map: Optional A dictionary of dependency expression symbol name and dependency object pairs. This is inserted into the asteval interpreter to resolve dependencies. + :param desired_unit: + The desired unit of the dependent parameter. If None, the default unit of the dependency expression result is used. + """ # noqa: E501 if not isinstance(dependency_expression, str): - raise TypeError('`dependency_expression` must be a string representing a valid dependency expression.') + raise TypeError( + "`dependency_expression` must be a string representing a valid dependency expression." + ) if not (isinstance(dependency_map, dict) or dependency_map is None): raise TypeError( - '`dependency_map` must be a dictionary of dependencies and their' - 'corresponding names in the dependecy expression.' + "`dependency_map` must be a dictionary of dependencies and their" + "corresponding names in the dependecy expression." ) # noqa: E501 if isinstance(dependency_map, dict): for key, value in dependency_map.items(): if not isinstance(key, str): raise TypeError( - '`dependency_map` keys must be strings representing the names of' - 'the dependencies in the dependency expression.' + "`dependency_map` keys must be strings representing the names of" + "the dependencies in the dependency expression." ) # noqa: E501 if not isinstance(value, DescriptorNumber): raise TypeError( - f'`dependency_map` values must be DescriptorNumbers or Parameters. Got {type(value)} for {key}.' + f"`dependency_map` values must be DescriptorNumbers or Parameters. Got {type(value)} for {key}." ) # noqa: E501 # If we're overwriting the dependency, store the old attributes @@ -208,10 +242,11 @@ def make_dependent_on(self, dependency_expression: str, dependency_map: Optional self._previous_independent = self._independent if not self._independent: self._previous_dependency = { - '_dependency_string': self._dependency_string, - '_dependency_map': self._dependency_map, - '_dependency_interpreter': self._dependency_interpreter, - '_clean_dependency_string': self._clean_dependency_string, + "_dependency_string": self._dependency_string, + "_dependency_map": self._dependency_map, + "_dependency_interpreter": self._dependency_interpreter, + "_clean_dependency_string": self._clean_dependency_string, + "_desired_unit": self._desired_unit, } for dependency in self._dependency_map.values(): dependency._detach_observer(self) @@ -219,26 +254,33 @@ def make_dependent_on(self, dependency_expression: str, dependency_map: Optional self._independent = False self._dependency_string = dependency_expression self._dependency_map = dependency_map if dependency_map is not None else {} + if desired_unit is not None and not ( + isinstance(desired_unit, str) or isinstance(desired_unit, sc.Unit) + ): + raise TypeError( + "`desired_unit` must be a string representing a valid unit." + ) + self._desired_unit = desired_unit # List of allowed python constructs for the asteval interpreter asteval_config = { - 'import': False, - 'importfrom': False, - 'assert': False, - 'augassign': False, - 'delete': False, - 'if': True, - 'ifexp': True, - 'for': False, - 'formattedvalue': False, - 'functiondef': False, - 'print': False, - 'raise': False, - 'listcomp': False, - 'dictcomp': False, - 'setcomp': False, - 'try': False, - 'while': False, - 'with': False, + "import": False, + "importfrom": False, + "assert": False, + "augassign": False, + "delete": False, + "if": True, + "ifexp": True, + "for": False, + "formattedvalue": False, + "functiondef": False, + "print": False, + "raise": False, + "listcomp": False, + "dictcomp": False, + "setcomp": False, + "try": False, + "while": False, + "with": False, } self._dependency_interpreter = Interpreter(config=asteval_config) @@ -257,29 +299,31 @@ def make_dependent_on(self, dependency_expression: str, dependency_map: Optional value._attach_observer(self) # Check the dependency expression for errors try: - dependency_result = self._dependency_interpreter.eval(self._clean_dependency_string, raise_errors=True) + dependency_result = self._dependency_interpreter.eval( + self._clean_dependency_string, raise_errors=True + ) except NameError as message: self._revert_dependency() raise NameError( - '\nUnknown name encountered in dependecy expression:' - + '\n' - + '\n'.join(str(message).split('\n')[1:]) - + '\nPlease check your expression or add the name to the `dependency_map`' + "\nUnknown name encountered in dependecy expression:" + + "\n" + + "\n".join(str(message).split("\n")[1:]) + + "\nPlease check your expression or add the name to the `dependency_map`" ) from None except Exception as message: self._revert_dependency() raise SyntaxError( - '\nError encountered in dependecy expression:' - + '\n' - + '\n'.join(str(message).split('\n')[1:]) - + '\nPlease check your expression' + "\nError encountered in dependecy expression:" + + "\n" + + "\n".join(str(message).split("\n")[1:]) + + "\nPlease check your expression" ) from None if not isinstance(dependency_result, DescriptorNumber): error_string = self._dependency_string self._revert_dependency() raise TypeError( f'The dependency expression: "{error_string}" returned a {type(dependency_result)},' - 'it should return a Parameter or DescriptorNumber.' + "it should return a Parameter or DescriptorNumber." ) # noqa: E501 # Check for cyclic dependencies try: @@ -289,6 +333,17 @@ def make_dependent_on(self, dependency_expression: str, dependency_map: Optional raise error # Update the parameter with the dependency result self._fixed = False + + if self._desired_unit is not None: + try: + dependency_result._convert_unit(self._desired_unit) + except Exception as e: + desired_unit_for_error_message = self._desired_unit + self._revert_dependency() # also deletes self._desired_unit + raise UnitError( + f"Failed to convert unit from {dependency_result.unit} to {desired_unit_for_error_message}: {e}" + ) + self._update() def make_independent(self) -> None: @@ -306,8 +361,9 @@ def make_independent(self) -> None: del self._dependency_interpreter del self._dependency_string del self._clean_dependency_string + del self._desired_unit else: - raise AttributeError('This parameter is already independent.') + raise AttributeError("This parameter is already independent.") @property def independent(self) -> bool: @@ -321,7 +377,7 @@ def independent(self) -> bool: @independent.setter def independent(self, value: bool) -> None: raise AttributeError( - 'This property is read-only. Use `make_independent` and `make_dependent_on` to change the state of the parameter.' + "This property is read-only. Use `make_independent` and `make_dependent_on` to change the state of the parameter." ) # noqa: E501 @property @@ -334,12 +390,14 @@ def dependency_expression(self) -> str: if not self._independent: return self._dependency_string else: - raise AttributeError('This parameter is independent. It has no dependency expression.') + raise AttributeError( + "This parameter is independent. It has no dependency expression." + ) @dependency_expression.setter def dependency_expression(self, new_expression: str) -> None: raise AttributeError( - 'Dependency expression is read-only. Use `make_dependent_on` to change the dependency expression.' + "Dependency expression is read-only. Use `make_dependent_on` to change the dependency expression." ) # noqa: E501 @property @@ -352,11 +410,15 @@ def dependency_map(self) -> Dict[str, DescriptorNumber]: if not self._independent: return self._dependency_map else: - raise AttributeError('This parameter is independent. It has no dependency map.') + raise AttributeError( + "This parameter is independent. It has no dependency map." + ) @dependency_map.setter def dependency_map(self, new_map: Dict[str, DescriptorNumber]) -> None: - raise AttributeError('Dependency map is read-only. Use `make_dependent_on` to change the dependency map.') + raise AttributeError( + "Dependency map is read-only. Use `make_dependent_on` to change the dependency map." + ) @property def value_no_call_back(self) -> numbers.Number: @@ -380,7 +442,7 @@ def full_value(self) -> Variable: @full_value.setter def full_value(self, scalar: Variable) -> None: raise AttributeError( - f'Full_value is read-only. Change the value and variance seperately. Or create a new {self.__class__.__name__}.' + f"Full_value is read-only. Change the value and variance seperately. Or create a new {self.__class__.__name__}." ) # noqa: E501 @property @@ -406,7 +468,7 @@ def value(self, value: numbers.Number) -> None: """ if self._independent: if not isinstance(value, numbers.Number): - raise TypeError(f'{value=} must be a number') + raise TypeError(f"{value=} must be a number") value = float(value) if value < self._min.value: @@ -422,7 +484,9 @@ def value(self, value: numbers.Number) -> None: # Notify observers of the change self._notify_observers() else: - raise AttributeError('This is a dependent parameter, its value cannot be set directly.') + raise AttributeError( + "This is a dependent parameter, its value cannot be set directly." + ) @DescriptorNumber.variance.setter def variance(self, variance_float: float) -> None: @@ -434,7 +498,9 @@ def variance(self, variance_float: float) -> None: if self._independent: DescriptorNumber.variance.fset(self, variance_float) else: - raise AttributeError('This is a dependent parameter, its variance cannot be set directly.') + raise AttributeError( + "This is a dependent parameter, its variance cannot be set directly." + ) @DescriptorNumber.error.setter def error(self, value: float) -> None: @@ -446,7 +512,9 @@ def error(self, value: float) -> None: if self._independent: DescriptorNumber.error.fset(self, value) else: - raise AttributeError('This is a dependent parameter, its error cannot be set directly.') + raise AttributeError( + "This is a dependent parameter, its error cannot be set directly." + ) def _convert_unit(self, unit_str: str) -> None: """ @@ -470,6 +538,27 @@ def convert_unit(self, unit_str: str) -> None: """ self._convert_unit(unit_str) + def set_desired_unit(self, unit_str: str | sc.Unit | None) -> None: + """ + Set the desired unit for a dependent Parameter. This will convert the parameter to the desired unit. + + :param unit_str: The desired unit as a string. + """ + + if self._independent: + raise AttributeError( + "This is an independent parameter, desired unit can only be set for dependent parameters." + ) + if not ( + isinstance(unit_str, str) + or isinstance(unit_str, sc.Unit) + or unit_str is None + ): + raise TypeError("`unit_str` must be a string representing a valid unit.") + + self._desired_unit = unit_str + self._update() + @property def min(self) -> numbers.Number: """ @@ -491,16 +580,22 @@ def min(self, min_value: numbers.Number) -> None: """ if self._independent: if not isinstance(min_value, numbers.Number): - raise TypeError('`min` must be a 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.') + 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: - raise ValueError(f'The current value ({self.value}) is smaller than the desired min value ({min_value}).') + raise ValueError( + f"The current value ({self.value}) is smaller than the desired min value ({min_value})." + ) self._notify_observers() else: - raise AttributeError('This is a dependent parameter, its minimum value cannot be set directly.') + raise AttributeError( + "This is a dependent parameter, its minimum value cannot be set directly." + ) @property def max(self) -> numbers.Number: @@ -523,16 +618,22 @@ def max(self, max_value: numbers.Number) -> None: """ if self._independent: if not isinstance(max_value, numbers.Number): - raise TypeError('`max` must be a 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.') + 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: - raise ValueError(f'The current value ({self.value}) is greater than the desired max value ({max_value}).') + raise ValueError( + f"The current value ({self.value}) is greater than the desired max value ({max_value})." + ) self._notify_observers() else: - raise AttributeError('This is a dependent parameter, its maximum value cannot be set directly.') + raise AttributeError( + "This is a dependent parameter, its maximum value cannot be set directly." + ) @property def fixed(self) -> bool: @@ -553,14 +654,16 @@ def fixed(self, fixed: bool) -> None: :param fixed: True = fixed, False = can vary """ if not isinstance(fixed, bool): - raise ValueError(f'{fixed=} must be a boolean. Got {type(fixed)}') + raise ValueError(f"{fixed=} must be a boolean. Got {type(fixed)}") if self._independent: self._fixed = fixed else: if self._global_object.stack.enabled: # Remove the recorded change from the stack global_object.stack.pop() - raise AttributeError('This is a dependent parameter, dependent parameters cannot be fixed.') + raise AttributeError( + "This is a dependent parameter, dependent parameters cannot be fixed." + ) # Is this alias really needed? @property @@ -578,15 +681,17 @@ def as_dict(self, skip: Optional[List[str]] = None) -> Dict[str, Any]: # Add dependency information for dependent parameters if not self._independent: # Save the dependency expression - raw_dict['_dependency_string'] = self._clean_dependency_string + raw_dict["_dependency_string"] = self._clean_dependency_string # Mark that this parameter is dependent - raw_dict['_independent'] = self._independent + raw_dict["_independent"] = self._independent # Convert dependency_map to use serializer_ids - raw_dict['_dependency_map_serializer_ids'] = {} + raw_dict["_dependency_map_serializer_ids"] = {} for key, obj in self._dependency_map.items(): - raw_dict['_dependency_map_serializer_ids'][key] = obj._DescriptorNumber__serializer_id + raw_dict["_dependency_map_serializer_ids"][ + key + ] = obj._DescriptorNumber__serializer_id return raw_dict @@ -621,33 +726,37 @@ def _process_dependency_unique_names(self, dependency_expression: str): existing_unique_names = self._global_object.map.vertices() # Add the unique names of the parameters to the ASTEVAL interpreter for name in inputted_unique_names: - stripped_name = name.strip('\'"') + stripped_name = name.strip("'\"") if stripped_name not in existing_unique_names: raise ValueError( - f'A Parameter with unique_name {stripped_name} does not exist. Please check your dependency expression.' + f"A Parameter with unique_name {stripped_name} does not exist. Please check your dependency expression." ) # noqa: E501 dependent_parameter = self._global_object.map.get_item_by_key(stripped_name) if isinstance(dependent_parameter, DescriptorNumber): - self._dependency_map['__' + stripped_name + '__'] = dependent_parameter - clean_dependency_string = clean_dependency_string.replace(name, '__' + stripped_name + '__') + self._dependency_map["__" + stripped_name + "__"] = dependent_parameter + clean_dependency_string = clean_dependency_string.replace( + name, "__" + stripped_name + "__" + ) else: raise ValueError( - f'The object with unique_name {stripped_name} is not a Parameter or DescriptorNumber. ' - 'Please check your dependency expression.' + f"The object with unique_name {stripped_name} is not a Parameter or DescriptorNumber. " + "Please check your dependency expression." ) # noqa: E501 self._clean_dependency_string = clean_dependency_string @classmethod - def from_dict(cls, obj_dict: dict) -> 'Parameter': + def from_dict(cls, obj_dict: dict) -> "Parameter": """ Custom deserialization to handle parameter dependencies. Override the parent method to handle dependency information. """ # Extract dependency information before creating the parameter raw_dict = obj_dict.copy() # Don't modify the original dict - dependency_string = raw_dict.pop('_dependency_string', None) - dependency_map_serializer_ids = raw_dict.pop('_dependency_map_serializer_ids', None) - is_independent = raw_dict.pop('_independent', True) + dependency_string = raw_dict.pop("_dependency_string", None) + dependency_map_serializer_ids = raw_dict.pop( + "_dependency_map_serializer_ids", None + ) + is_independent = raw_dict.pop("_independent", True) # Note: Keep _serializer_id in the dict so it gets passed to __init__ # Create the parameter using the base class method (serializer_id is now handled in __init__) @@ -675,74 +784,104 @@ def __repr__(self) -> str: super_str = super_str[:-1] s = [] if self.fixed: - super_str += ' (fixed)' + super_str += " (fixed)" s.append(super_str) - s.append('bounds=[%s:%s]' % (repr(float(self.min)), repr(float(self.max)))) - return '%s>' % ', '.join(s) + s.append("bounds=[%s:%s]" % (repr(float(self.min)), repr(float(self.max)))) + return "%s>" % ", ".join(s) # Seems redundant # def __float__(self) -> float: # return float(self._scalar.value) - def __add__(self, other: Union[DescriptorNumber, Parameter, numbers.Number]) -> Parameter: + 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') + 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 + 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 + 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 + 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 = 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') + 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 + 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 + 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 = 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: + 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') + 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 + 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 + 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 @@ -753,47 +892,65 @@ def __sub__(self, other: Union[DescriptorNumber, Parameter, numbers.Number]) -> 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 = 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') + 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 + 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 + 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 = 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: + 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 = 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 + 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 = 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): @@ -816,7 +973,9 @@ def __mul__(self, other: Union[DescriptorNumber, Parameter, numbers.Number]) -> 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 = 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 @@ -825,14 +984,20 @@ 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 = 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 + 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 = 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] @@ -840,21 +1005,27 @@ def __rmul__(self, other: Union[DescriptorNumber, numbers.Number]) -> Parameter: 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 = 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: + def __truediv__( + self, other: Union[DescriptorNumber, Parameter, numbers.Number] + ) -> Parameter: if isinstance(other, numbers.Number): if other == 0: - raise ZeroDivisionError('Cannot divide by zero') + 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 + 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') + 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: @@ -874,7 +1045,12 @@ def __truediv__(self, other: Union[DescriptorNumber, Parameter, numbers.Number]) 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] + 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 @@ -882,7 +1058,9 @@ def __truediv__(self, other: Union[DescriptorNumber, Parameter, numbers.Number]) 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 = 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 @@ -890,19 +1068,25 @@ def __truediv__(self, other: Union[DescriptorNumber, Parameter, numbers.Number]) def __rtruediv__(self, other: Union[DescriptorNumber, numbers.Number]) -> Parameter: original_self = self.value if original_self == 0: - raise ZeroDivisionError('Cannot divide by zero') + 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 = 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 + 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 = DescriptorNumber.from_scipp( + name=self.name, full_value=new_full_value + ) descriptor_number.name = descriptor_number.unique_name return descriptor_number else: @@ -923,7 +1107,9 @@ def __rtruediv__(self, other: Union[DescriptorNumber, numbers.Number]) -> Parame 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 = 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 @@ -932,11 +1118,13 @@ def __rtruediv__(self, other: Union[DescriptorNumber, numbers.Number]) -> Parame 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') + 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') + raise ValueError("Exponents must not have variance") exponent = other.value else: return NotImplemented @@ -947,9 +1135,11 @@ def __pow__(self, other: Union[DescriptorNumber, numbers.Number]) -> Parameter: raise message from None if np.isnan(new_full_value.value): - raise ValueError('The result of the exponentiation is not a number') + 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 = DescriptorNumber.from_scipp( + name=self.name, full_value=new_full_value + ) descriptor_number.name = descriptor_number.unique_name return descriptor_number elif exponent < 0: @@ -970,10 +1160,14 @@ def __pow__(self, other: Union[DescriptorNumber, numbers.Number]) -> Parameter: elif exponent % 1 != 0: if self.min < 0: combinations.append(0) - combinations = [combination for combination in combinations if combination >= 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 = Parameter.from_scipp( + name=self.name, full_value=new_full_value, min=min_value, max=max_value + ) parameter.name = parameter.unique_name return parameter @@ -981,7 +1175,9 @@ 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 = Parameter.from_scipp( + name=self.name, full_value=new_full_value, min=min_value, max=max_value + ) parameter.name = parameter.unique_name return parameter @@ -992,7 +1188,9 @@ def __abs__(self) -> Parameter: combinations.append(0.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 = Parameter.from_scipp( + name=self.name, full_value=new_full_value, min=min_value, max=max_value + ) parameter.name = parameter.unique_name return parameter @@ -1002,12 +1200,14 @@ def resolve_pending_dependencies(self) -> None: This method should be called after all parameters have been deserialized to establish dependency relationships using serializer_ids. """ - if hasattr(self, '_pending_dependency_string'): + if hasattr(self, "_pending_dependency_string"): dependency_string = self._pending_dependency_string dependency_map = {} - if hasattr(self, '_pending_dependency_map_serializer_ids'): - dependency_map_serializer_ids = self._pending_dependency_map_serializer_ids + if hasattr(self, "_pending_dependency_map_serializer_ids"): + dependency_map_serializer_ids = ( + self._pending_dependency_map_serializer_ids + ) # Build dependency_map by looking up objects by serializer_id for key, serializer_id in dependency_map_serializer_ids.items(): @@ -1015,22 +1215,33 @@ def resolve_pending_dependencies(self) -> None: if dep_obj is not None: dependency_map[key] = dep_obj else: - raise ValueError(f"Cannot find parameter with serializer_id '{serializer_id}'") + raise ValueError( + f"Cannot find parameter with serializer_id '{serializer_id}'" + ) # Establish the dependency relationship try: - self.make_dependent_on(dependency_expression=dependency_string, dependency_map=dependency_map) + self.make_dependent_on( + dependency_expression=dependency_string, + dependency_map=dependency_map, + ) except Exception as e: - raise ValueError(f"Error establishing dependency '{dependency_string}': {e}") + raise ValueError( + f"Error establishing dependency '{dependency_string}': {e}" + ) # Clean up temporary attributes - delattr(self, '_pending_dependency_string') - delattr(self, '_pending_dependency_map_serializer_ids') + delattr(self, "_pending_dependency_string") + delattr(self, "_pending_dependency_map_serializer_ids") - def _find_parameter_by_serializer_id(self, serializer_id: str) -> Optional['DescriptorNumber']: + def _find_parameter_by_serializer_id( + self, serializer_id: str + ) -> Optional["DescriptorNumber"]: """Find a parameter by its serializer_id from all parameters in the global map.""" for obj in self._global_object.map._store.values(): - if isinstance(obj, DescriptorNumber) and hasattr(obj, '_DescriptorNumber__serializer_id'): + if isinstance(obj, DescriptorNumber) and hasattr( + obj, "_DescriptorNumber__serializer_id" + ): if obj._DescriptorNumber__serializer_id == serializer_id: return obj return None diff --git a/tests/unit_tests/variable/test_parameter.py b/tests/unit_tests/variable/test_parameter.py index 0ca8f85..5fcb9ed 100644 --- a/tests/unit_tests/variable/test_parameter.py +++ b/tests/unit_tests/variable/test_parameter.py @@ -10,6 +10,7 @@ from easyscience import global_object from easyscience import ObjBase + class TestParameter: @pytest.fixture def parameter(self) -> Parameter: @@ -28,7 +29,7 @@ def parameter(self) -> Parameter: parent=None, ) return parameter - + @pytest.fixture def normal_parameter(self) -> Parameter: parameter = Parameter( @@ -75,7 +76,7 @@ def test_init(self, parameter: Parameter): assert parameter._observers == [] def test_init_value_min_exception(self): - # When + # When mock_callback = MagicMock() value = -1 @@ -96,7 +97,7 @@ def test_init_value_min_exception(self): ) def test_init_value_max_exception(self): - # When + # When mock_callback = MagicMock() value = 100 @@ -118,162 +119,419 @@ def test_init_value_max_exception(self): def test_make_dependent_on(self, normal_parameter: Parameter): # When - independent_parameter = Parameter(name="independent", value=1, unit="m", variance=0.01, min=0, max=10) - + independent_parameter = Parameter( + name="independent", value=1, unit="m", variance=0.01, min=0, max=10 + ) + # Then - normal_parameter.make_dependent_on(dependency_expression='2*a', dependency_map={'a': independent_parameter}) + normal_parameter.make_dependent_on( + dependency_expression="2*a", dependency_map={"a": independent_parameter} + ) # Expect assert normal_parameter._independent == False - assert normal_parameter.dependency_expression == '2*a' - assert normal_parameter.dependency_map == {'a': independent_parameter} - self.compare_parameters(normal_parameter, 2*independent_parameter) + assert normal_parameter.dependency_expression == "2*a" + assert normal_parameter.dependency_map == {"a": independent_parameter} + self.compare_parameters(normal_parameter, 2 * independent_parameter) # Then independent_parameter.value = 2 # Expect normal_parameter.value == 4 - self.compare_parameters(normal_parameter, 2*independent_parameter) + self.compare_parameters(normal_parameter, 2 * independent_parameter) + + def test_dependent_parameter_make_dependent_on_with_desired_unit( + self, normal_parameter: Parameter + ): + # When + independent_parameter = Parameter( + name="independent", value=1, unit="m", variance=0.01, min=0, max=10 + ) + + # Then + normal_parameter.make_dependent_on( + dependency_expression="2*a", + dependency_map={"a": independent_parameter}, + desired_unit="cm", + ) + + # Expect + assert normal_parameter._independent == False + assert normal_parameter.dependency_expression == "2*a" + assert normal_parameter.dependency_map == {"a": independent_parameter} + + assert normal_parameter.value == 200 * independent_parameter.value + assert normal_parameter.unit == "cm" + assert ( + normal_parameter.variance == independent_parameter.variance * 4 * 10000 + ) # unit conversion from m to cm squared + assert normal_parameter.min == 200 * independent_parameter.min + assert normal_parameter.max == 200 * independent_parameter.max + assert normal_parameter._min.unit == "cm" + assert normal_parameter._max.unit == "cm" + + # Then + independent_parameter.value = 2 + + # Expect + assert normal_parameter.value == 200 * independent_parameter.value + assert normal_parameter.unit == "cm" + assert ( + normal_parameter.variance == independent_parameter.variance * 4 * 10000 + ) # unit conversion from m to cm squared + assert normal_parameter.min == 200 * independent_parameter.min + assert normal_parameter.max == 200 * independent_parameter.max + assert normal_parameter._min.unit == "cm" + assert normal_parameter._max.unit == "cm" + + # Then # Change the dependency expression and unit again + normal_parameter.make_dependent_on( + dependency_expression="3*a", + dependency_map={"a": independent_parameter}, + desired_unit="mm", + ) + + # Expect + assert normal_parameter._independent == False + assert normal_parameter.dependency_expression == "3*a" + assert normal_parameter.dependency_map == {"a": independent_parameter} + + assert normal_parameter.value == 3000 * independent_parameter.value + assert normal_parameter.unit == "mm" + assert ( + normal_parameter.variance == independent_parameter.variance * 9 * 1000000 + ) # unit conversion from m to mm squared + assert normal_parameter.min == 3000 * independent_parameter.min + assert normal_parameter.max == 3000 * independent_parameter.max + assert normal_parameter._min.unit == "mm" + assert normal_parameter._max.unit == "mm" + + # Then + independent_parameter.value = 2 + + # Expect + assert normal_parameter.value == 3000 * independent_parameter.value + assert normal_parameter.unit == "mm" + assert ( + normal_parameter.variance == independent_parameter.variance * 9 * 1000000 + ) # unit conversion from m to mm squared + assert normal_parameter.min == 3000 * independent_parameter.min + assert normal_parameter.max == 3000 * independent_parameter.max + assert normal_parameter._min.unit == "mm" + assert normal_parameter._max.unit == "mm" + + def test_dependent_parameter_make_dependent_on_with_desired_unit_incompatible_unit_raises( + self, normal_parameter: Parameter + ): + # When + independent_parameter = Parameter( + name="independent", value=1, unit="m", variance=0.01, min=0, max=10 + ) + + # Then Expect + with pytest.raises(UnitError): + normal_parameter.make_dependent_on( + dependency_expression="2*a", + dependency_map={"a": independent_parameter}, + desired_unit="s", + ) + + def test_dependent_parameter_make_dependent_on_with_incorrect_unit_raises( + self, normal_parameter: Parameter + ): + # When + independent_parameter = Parameter( + name="independent", value=1, unit="m", variance=0.01, min=0, max=10 + ) + + # Then Expect + with pytest.raises(TypeError): + normal_parameter.make_dependent_on( + dependency_expression="2*a", + dependency_map={"a": independent_parameter}, + desired_unit=123, + ) def test_parameter_from_dependency(self, normal_parameter: Parameter): # When Then dependent_parameter = Parameter.from_dependency( - name = 'dependent', - dependency_expression='2*a', - dependency_map={'a': normal_parameter}, - display_name='display_name', + name="dependent", + dependency_expression="2*a", + dependency_map={"a": normal_parameter}, + display_name="display_name", + ) + + # Expect + assert dependent_parameter._independent == False + assert dependent_parameter.dependency_expression == "2*a" + assert dependent_parameter.dependency_map == {"a": normal_parameter} + assert dependent_parameter.name == "dependent" + assert dependent_parameter.display_name == "display_name" + self.compare_parameters(dependent_parameter, 2 * normal_parameter) + + # Then + normal_parameter.value = 2 + + # Expect + self.compare_parameters(dependent_parameter, 2 * normal_parameter) + + def test_parameter_from_dependency_with_desired_unit( + self, normal_parameter: Parameter + ): + # When Then + dependent_parameter = Parameter.from_dependency( + name="dependent", + dependency_expression="2*a", + dependency_map={"a": normal_parameter}, + display_name="display_name", + desired_unit="cm", ) # Expect assert dependent_parameter._independent == False - assert dependent_parameter.dependency_expression == '2*a' - assert dependent_parameter.dependency_map == {'a': normal_parameter} - assert dependent_parameter.name == 'dependent' - assert dependent_parameter.display_name == 'display_name' - self.compare_parameters(dependent_parameter, 2*normal_parameter) + assert dependent_parameter.dependency_expression == "2*a" + assert dependent_parameter.dependency_map == {"a": normal_parameter} + assert dependent_parameter.name == "dependent" + assert dependent_parameter.display_name == "display_name" + + assert dependent_parameter.value == 200 * normal_parameter.value + assert dependent_parameter.unit == "cm" + assert ( + dependent_parameter.variance == normal_parameter.variance * 4 * 10000 + ) # unit conversion from m to cm squared + assert dependent_parameter.min == 200 * normal_parameter.min + assert dependent_parameter.max == 200 * normal_parameter.max + assert dependent_parameter._min.unit == "cm" + assert dependent_parameter._max.unit == "cm" # Then normal_parameter.value = 2 # Expect - self.compare_parameters(dependent_parameter, 2*normal_parameter) + assert dependent_parameter.value == 200 * normal_parameter.value + assert dependent_parameter.unit == "cm" + assert ( + dependent_parameter.variance == normal_parameter.variance * 4 * 10000 + ) # unit conversion from m to cm squared + assert dependent_parameter.min == 200 * normal_parameter.min + assert dependent_parameter.max == 200 * normal_parameter.max + assert dependent_parameter._min.unit == "cm" + assert dependent_parameter._max.unit == "cm" + + def test_parameter_from_dependency_with_desired_unit_incompatible_unit_raises( + self, normal_parameter: Parameter + ): + # When Then Expect + with pytest.raises(UnitError): + dependent_parameter = Parameter.from_dependency( + name="dependent", + dependency_expression="2*a", + dependency_map={"a": normal_parameter}, + display_name="display_name", + desired_unit="s", + ) - def test_dependent_parameter_with_unique_name(self, clear, normal_parameter: Parameter): + def test_dependent_parameter_with_unique_name( + self, clear, normal_parameter: Parameter + ): # When Then dependent_parameter = Parameter.from_dependency( - name = 'dependent', + name="dependent", dependency_expression='2*"Parameter_0"', ) # Expect assert dependent_parameter.dependency_expression == '2*"Parameter_0"' - assert dependent_parameter.dependency_map == {'__Parameter_0__': normal_parameter} - self.compare_parameters(dependent_parameter, 2*normal_parameter) + assert dependent_parameter.dependency_map == { + "__Parameter_0__": normal_parameter + } + self.compare_parameters(dependent_parameter, 2 * normal_parameter) # Then normal_parameter.value = 2 # Expect - self.compare_parameters(dependent_parameter, 2*normal_parameter) + self.compare_parameters(dependent_parameter, 2 * normal_parameter) - def test_process_dependency_unique_names_double_quotes(self, clear, normal_parameter: Parameter): + def test_process_dependency_unique_names_double_quotes( + self, clear, normal_parameter: Parameter + ): # When - independent_parameter = Parameter(name="independent", value=1, unit="m", variance=0.01, min=0, max=10, unique_name='Special_name') + independent_parameter = Parameter( + name="independent", + value=1, + unit="m", + variance=0.01, + min=0, + max=10, + unique_name="Special_name", + ) normal_parameter._dependency_map = {} # Then - normal_parameter._process_dependency_unique_names(dependency_expression='2*"Special_name"') + normal_parameter._process_dependency_unique_names( + dependency_expression='2*"Special_name"' + ) # Expect - assert normal_parameter._dependency_map == {'__Special_name__': independent_parameter} - assert normal_parameter._clean_dependency_string == '2*__Special_name__' - - def test_process_dependency_unique_names_single_quotes(self, clear, normal_parameter: Parameter): + assert normal_parameter._dependency_map == { + "__Special_name__": independent_parameter + } + assert normal_parameter._clean_dependency_string == "2*__Special_name__" + + def test_process_dependency_unique_names_single_quotes( + self, clear, normal_parameter: Parameter + ): # When - independent_parameter = Parameter(name="independent", value=1, unit="m", variance=0.01, min=0, max=10, unique_name='Special_name') - independent_parameter_2 = Parameter(name="independent_2", value=1, unit="m", variance=0.01, min=0, max=10, unique_name='Special_name_2') + independent_parameter = Parameter( + name="independent", + value=1, + unit="m", + variance=0.01, + min=0, + max=10, + unique_name="Special_name", + ) + independent_parameter_2 = Parameter( + name="independent_2", + value=1, + unit="m", + variance=0.01, + min=0, + max=10, + unique_name="Special_name_2", + ) normal_parameter._dependency_map = {} # Then - normal_parameter._process_dependency_unique_names(dependency_expression="'Special_name' + 'Special_name_2'") + normal_parameter._process_dependency_unique_names( + dependency_expression="'Special_name' + 'Special_name_2'" + ) # Expect - assert normal_parameter._dependency_map == {'__Special_name__': independent_parameter, - '__Special_name_2__': independent_parameter_2} - assert normal_parameter._clean_dependency_string == '__Special_name__ + __Special_name_2__' + assert normal_parameter._dependency_map == { + "__Special_name__": independent_parameter, + "__Special_name_2__": independent_parameter_2, + } + assert ( + normal_parameter._clean_dependency_string + == "__Special_name__ + __Special_name_2__" + ) - def test_process_dependency_unique_names_exception_unique_name_does_not_exist(self, clear, normal_parameter: Parameter): + def test_process_dependency_unique_names_exception_unique_name_does_not_exist( + self, clear, normal_parameter: Parameter + ): # When normal_parameter._dependency_map = {} # Then Expect - with pytest.raises(ValueError, match='A Parameter with unique_name Special_name does not exist. Please check your dependency expression.'): - normal_parameter._process_dependency_unique_names(dependency_expression='2*"Special_name"') + with pytest.raises( + ValueError, + match="A Parameter with unique_name Special_name does not exist. Please check your dependency expression.", + ): + normal_parameter._process_dependency_unique_names( + dependency_expression='2*"Special_name"' + ) - def test_process_dependency_unique_names_exception_not_a_descriptorNumber(self, clear, normal_parameter: Parameter): + def test_process_dependency_unique_names_exception_not_a_descriptorNumber( + self, clear, normal_parameter: Parameter + ): # When normal_parameter._dependency_map = {} - base_obj = ObjBase(name='ObjBase', unique_name='base_obj') + base_obj = ObjBase(name="ObjBase", unique_name="base_obj") # Then Expect - with pytest.raises(ValueError, match='The object with unique_name base_obj is not a Parameter or DescriptorNumber. Please check your dependency expression.'): - normal_parameter._process_dependency_unique_names(dependency_expression='2*"base_obj"') - - @pytest.mark.parametrize("dependency_expression, dependency_map", [ - (2, {'a': Parameter(name='a', value=1)}), - ('2*a', ['a', Parameter(name='a', value=1)]), - ('2*a', {4: Parameter(name='a', value=1)}), - ('2*a', {'a': ObjBase(name='a')}), - ], ids=["dependency_expression_not_a_string", "dependency_map_not_a_dict", "dependency_map_keys_not_strings", "dependency_map_values_not_descriptor_number"]) - def test_parameter_from_dependency_input_exceptions(self, dependency_expression, dependency_map): + with pytest.raises( + ValueError, + match="The object with unique_name base_obj is not a Parameter or DescriptorNumber. Please check your dependency expression.", + ): + normal_parameter._process_dependency_unique_names( + dependency_expression='2*"base_obj"' + ) + + @pytest.mark.parametrize( + "dependency_expression, dependency_map", + [ + (2, {"a": Parameter(name="a", value=1)}), + ("2*a", ["a", Parameter(name="a", value=1)]), + ("2*a", {4: Parameter(name="a", value=1)}), + ("2*a", {"a": ObjBase(name="a")}), + ], + ids=[ + "dependency_expression_not_a_string", + "dependency_map_not_a_dict", + "dependency_map_keys_not_strings", + "dependency_map_values_not_descriptor_number", + ], + ) + def test_parameter_from_dependency_input_exceptions( + self, dependency_expression, dependency_map + ): # When Then Expect with pytest.raises(TypeError): Parameter.from_dependency( - name = 'dependent', - dependency_expression=dependency_expression, + name="dependent", + dependency_expression=dependency_expression, dependency_map=dependency_map, ) - @pytest.mark.parametrize("dependency_expression, error", [ - ('2*a + b', NameError), - ('2*a + 3*', SyntaxError), - ('2 + 2', TypeError), - ('2*"special_name"', ValueError), - ], ids=["parameter_not_in_map", "invalid_dependency_expression", "result_not_a_descriptor_number", "unique_name_does_not_exist"]) - def test_parameter_make_dependent_on_exceptions_cleanup_previously_dependent(self, normal_parameter, dependency_expression, error): - # When - independent_parameter = Parameter(name='independent', value=10, unit='s', variance=0.02) + @pytest.mark.parametrize( + "dependency_expression, error", + [ + ("2*a + b", NameError), + ("2*a + 3*", SyntaxError), + ("2 + 2", TypeError), + ('2*"special_name"', ValueError), + ], + ids=[ + "parameter_not_in_map", + "invalid_dependency_expression", + "result_not_a_descriptor_number", + "unique_name_does_not_exist", + ], + ) + def test_parameter_make_dependent_on_exceptions_cleanup_previously_dependent( + self, normal_parameter, dependency_expression, error + ): + # When + independent_parameter = Parameter( + name="independent", value=10, unit="s", variance=0.02 + ) dependent_parameter = Parameter.from_dependency( - name= 'dependent', - dependency_expression='best', - dependency_map={'best': independent_parameter} - ) + name="dependent", + dependency_expression="best", + dependency_map={"best": independent_parameter}, + ) # Then Expect # Check that the correct error is raised with pytest.raises(error): dependent_parameter.make_dependent_on( - dependency_expression=dependency_expression, - dependency_map={'a': normal_parameter}, - ) + dependency_expression=dependency_expression, + dependency_map={"a": normal_parameter}, + ) # Check that everything is properly cleaned up assert normal_parameter._observers == [] assert dependent_parameter.independent == False - assert dependent_parameter.dependency_expression == 'best' - assert dependent_parameter.dependency_map == {'best': independent_parameter} + assert dependent_parameter.dependency_expression == "best" + assert dependent_parameter.dependency_map == {"best": independent_parameter} independent_parameter.value = 50 self.compare_parameters(dependent_parameter, independent_parameter) - def test_parameter_make_dependent_on_exceptions_cleanup_previously_independent(self, normal_parameter): - # When - independent_parameter = Parameter(name='independent', value=10, unit='s', variance=0.02) + def test_parameter_make_dependent_on_exceptions_cleanup_previously_independent( + self, normal_parameter + ): + # When + independent_parameter = Parameter( + name="independent", value=10, unit="s", variance=0.02 + ) # Then Expect # Check that the correct error is raised with pytest.raises(NameError): independent_parameter.make_dependent_on( - dependency_expression='2*a + b', - dependency_map={'a': normal_parameter}, - ) + dependency_expression="2*a + b", + dependency_map={"a": normal_parameter}, + ) # Check that everything is properly cleaned up assert normal_parameter._observers == [] assert independent_parameter.independent == True @@ -283,84 +541,88 @@ def test_parameter_make_dependent_on_exceptions_cleanup_previously_independent(s def test_dependent_parameter_updates(self, normal_parameter: Parameter): # When dependent_parameter = Parameter.from_dependency( - name = 'dependent', - dependency_expression='2*a', - dependency_map={'a': normal_parameter}, + name="dependent", + dependency_expression="2*a", + dependency_map={"a": normal_parameter}, ) # Then Expect normal_parameter.value = 2 - self.compare_parameters(dependent_parameter, 2*normal_parameter) + self.compare_parameters(dependent_parameter, 2 * normal_parameter) normal_parameter.variance = 0.02 - self.compare_parameters(dependent_parameter, 2*normal_parameter) + self.compare_parameters(dependent_parameter, 2 * normal_parameter) normal_parameter.error = 0.2 - self.compare_parameters(dependent_parameter, 2*normal_parameter) + self.compare_parameters(dependent_parameter, 2 * normal_parameter) normal_parameter.convert_unit("cm") - self.compare_parameters(dependent_parameter, 2*normal_parameter) + self.compare_parameters(dependent_parameter, 2 * normal_parameter) normal_parameter.min = 1 - self.compare_parameters(dependent_parameter, 2*normal_parameter) + self.compare_parameters(dependent_parameter, 2 * normal_parameter) normal_parameter.max = 300 - self.compare_parameters(dependent_parameter, 2*normal_parameter) + self.compare_parameters(dependent_parameter, 2 * normal_parameter) def test_dependent_parameter_indirect_updates(self, normal_parameter: Parameter): # When dependent_parameter = Parameter.from_dependency( - name = 'dependent', - dependency_expression='2*a', - dependency_map={'a': normal_parameter}, + name="dependent", + dependency_expression="2*a", + dependency_map={"a": normal_parameter}, ) dependent_parameter_2 = Parameter.from_dependency( - name = 'dependent_2', - dependency_expression='10*a', - dependency_map={'a': normal_parameter}, + name="dependent_2", + dependency_expression="10*a", + dependency_map={"a": normal_parameter}, ) dependent_parameter_3 = Parameter.from_dependency( - name = 'dependent_3', - dependency_expression='b+c', - dependency_map={'b': dependent_parameter, 'c': dependent_parameter_2}, + name="dependent_3", + dependency_expression="b+c", + dependency_map={"b": dependent_parameter, "c": dependent_parameter_2}, ) # Then normal_parameter.value = 2 # Expect - self.compare_parameters(dependent_parameter, 2*normal_parameter) - self.compare_parameters(dependent_parameter_2, 10*normal_parameter) - self.compare_parameters(dependent_parameter_3, 2*normal_parameter + 10*normal_parameter) + self.compare_parameters(dependent_parameter, 2 * normal_parameter) + self.compare_parameters(dependent_parameter_2, 10 * normal_parameter) + self.compare_parameters( + dependent_parameter_3, 2 * normal_parameter + 10 * normal_parameter + ) def test_dependent_parameter_cyclic_dependencies(self, normal_parameter: Parameter): # When dependent_parameter = Parameter.from_dependency( - name = 'dependent', - dependency_expression='2*a', - dependency_map={'a': normal_parameter}, + name="dependent", + dependency_expression="2*a", + dependency_map={"a": normal_parameter}, ) dependent_parameter_2 = Parameter.from_dependency( - name = 'dependent_2', - dependency_expression='2*b', - dependency_map={'b': dependent_parameter}, + name="dependent_2", + dependency_expression="2*b", + dependency_map={"b": dependent_parameter}, ) # Then Expect with pytest.raises(RuntimeError): - normal_parameter.make_dependent_on(dependency_expression='2*c', dependency_map={'c': dependent_parameter_2}) + normal_parameter.make_dependent_on( + dependency_expression="2*c", dependency_map={"c": dependent_parameter_2} + ) # Check that everything is properly cleaned up assert dependent_parameter_2._observers == [] assert normal_parameter.independent == True assert normal_parameter.value == 1 normal_parameter.value = 50 - self.compare_parameters(dependent_parameter_2, 4*normal_parameter) + self.compare_parameters(dependent_parameter_2, 4 * normal_parameter) def test_dependent_parameter_logical_dependency(self, normal_parameter: Parameter): # When dependent_parameter = Parameter.from_dependency( - name = 'dependent', - dependency_expression='a if a.value > 0 else -a', - dependency_map={'a': normal_parameter}, + name="dependent", + dependency_expression="a if a.value > 0 else -a", + dependency_map={"a": normal_parameter}, ) self.compare_parameters(dependent_parameter, normal_parameter) @@ -372,51 +634,59 @@ def test_dependent_parameter_logical_dependency(self, normal_parameter: Paramete def test_dependent_parameter_return_is_descriptor_number(self): # When - descriptor_number = DescriptorNumber(name='descriptor', value=1, unit='m', variance=0.01) - + descriptor_number = DescriptorNumber( + name="descriptor", value=1, unit="m", variance=0.01 + ) + # Then dependent_parameter = Parameter.from_dependency( - name = 'dependent', - dependency_expression='2*descriptor', - dependency_map={'descriptor': descriptor_number}, + name="dependent", + dependency_expression="2*descriptor", + dependency_map={"descriptor": descriptor_number}, ) # Expect - assert dependent_parameter.value == 2*descriptor_number.value + assert dependent_parameter.value == 2 * descriptor_number.value assert dependent_parameter.unit == descriptor_number.unit assert dependent_parameter.variance == 0.04 - assert dependent_parameter.min == 2*descriptor_number.value - assert dependent_parameter.max == 2*descriptor_number.value + assert dependent_parameter.min == 2 * descriptor_number.value + assert dependent_parameter.max == 2 * descriptor_number.value - def test_dependent_parameter_overwrite_dependency(self, normal_parameter: Parameter): + def test_dependent_parameter_overwrite_dependency( + self, normal_parameter: Parameter + ): # When dependent_parameter = Parameter.from_dependency( - name = 'dependent', - dependency_expression='2*a', - dependency_map={'a': normal_parameter}, + name="dependent", + dependency_expression="2*a", + dependency_map={"a": normal_parameter}, ) - self.compare_parameters(dependent_parameter, 2*normal_parameter) + self.compare_parameters(dependent_parameter, 2 * normal_parameter) # Then - normal_parameter_2 = Parameter(name='a2', value=-2, unit='m', variance=0.01, min=-10, max=0) - dependent_parameter.make_dependent_on(dependency_expression='3*a2', dependency_map={'a2': normal_parameter_2}) + normal_parameter_2 = Parameter( + name="a2", value=-2, unit="m", variance=0.01, min=-10, max=0 + ) + dependent_parameter.make_dependent_on( + dependency_expression="3*a2", dependency_map={"a2": normal_parameter_2} + ) normal_parameter.value = 3 # Expect - self.compare_parameters(dependent_parameter, 3*normal_parameter_2) - assert dependent_parameter.dependency_expression == '3*a2' - assert dependent_parameter.dependency_map == {'a2': normal_parameter_2} + self.compare_parameters(dependent_parameter, 3 * normal_parameter_2) + assert dependent_parameter.dependency_expression == "3*a2" + assert dependent_parameter.dependency_map == {"a2": normal_parameter_2} assert normal_parameter._observers == [] - + def test_make_independent(self, normal_parameter: Parameter): # When dependent_parameter = Parameter.from_dependency( - name = 'dependent', - dependency_expression='2*a', - dependency_map={'a': normal_parameter}, + name="dependent", + dependency_expression="2*a", + dependency_map={"a": normal_parameter}, ) assert dependent_parameter.independent == False - self.compare_parameters(dependent_parameter, 2*normal_parameter) + self.compare_parameters(dependent_parameter, 2 * normal_parameter) # Then dependent_parameter.make_independent() @@ -437,46 +707,52 @@ def test_independent_setter(self, normal_parameter: Parameter): with pytest.raises(AttributeError): normal_parameter.independent = False - def test_independent_parameter_dependency_expression(self, normal_parameter: Parameter): + def test_independent_parameter_dependency_expression( + self, normal_parameter: Parameter + ): # When Then Expect with pytest.raises(AttributeError): normal_parameter.dependency_expression - def test_dependent_parameter_dependency_expression_setter(self, normal_parameter: Parameter): + def test_dependent_parameter_dependency_expression_setter( + self, normal_parameter: Parameter + ): # When dependent_parameter = Parameter.from_dependency( - name = 'dependent', - dependency_expression='2*a', - dependency_map={'a': normal_parameter}, + name="dependent", + dependency_expression="2*a", + dependency_map={"a": normal_parameter}, ) # Then Expect with pytest.raises(AttributeError): - dependent_parameter.dependency_expression = '3*a' + dependent_parameter.dependency_expression = "3*a" def test_independent_parameter_dependency_map(self, normal_parameter: Parameter): # When Then Expect with pytest.raises(AttributeError): normal_parameter.dependency_map - def test_dependent_parameter_dependency_map_setter(self, normal_parameter: Parameter): + def test_dependent_parameter_dependency_map_setter( + self, normal_parameter: Parameter + ): # When dependent_parameter = Parameter.from_dependency( - name = 'dependent', - dependency_expression='2*a', - dependency_map={'a': normal_parameter}, + name="dependent", + dependency_expression="2*a", + dependency_map={"a": normal_parameter}, ) # Then Expect with pytest.raises(AttributeError): - dependent_parameter.dependency_map = {'a': normal_parameter} + dependent_parameter.dependency_map = {"a": normal_parameter} def test_min(self, parameter: Parameter): # When Then Expect assert parameter.min == 0 def test_set_min(self, parameter: Parameter): - # When Then + # When Then self.mock_callback.fget.return_value = 1.0 # Ensure fget returns a scalar value parameter.min = 0.1 @@ -487,9 +763,9 @@ def test_set_min(self, parameter: Parameter): def test_set_min_dependent_parameter(self, normal_parameter: Parameter): # When dependent_parameter = Parameter.from_dependency( - name = 'dependent', - dependency_expression='2*a', - dependency_map={'a': normal_parameter}, + name="dependent", + dependency_expression="2*a", + dependency_map={"a": normal_parameter}, ) # Then Expect @@ -502,7 +778,7 @@ def test_set_min_exception(self, parameter: Parameter): parameter.min = 10 def test_set_max(self, parameter: Parameter): - # When Then + # When Then parameter.max = 10 # Expect @@ -511,9 +787,9 @@ def test_set_max(self, parameter: Parameter): def test_set_max_dependent_parameter(self, normal_parameter: Parameter): # When dependent_parameter = Parameter.from_dependency( - name = 'dependent', - dependency_expression='2*a', - dependency_map={'a': normal_parameter}, + name="dependent", + dependency_expression="2*a", + dependency_map={"a": normal_parameter}, ) # Then Expect @@ -535,8 +811,86 @@ def test_convert_unit(self, parameter: Parameter): assert parameter._max.value == 10000 assert parameter._max.unit == "mm" + def test_set_desired_unit(self, normal_parameter: Parameter): + # When Then + dependent_parameter = Parameter.from_dependency( + name="dependent", + dependency_expression="2*a", + dependency_map={"a": normal_parameter}, + display_name="display_name", + ) + + # Then + dependent_parameter.set_desired_unit("cm") + + # Expect + + assert dependent_parameter.value == 200 * normal_parameter.value + assert dependent_parameter.unit == "cm" + assert ( + dependent_parameter.variance == normal_parameter.variance * 4 * 10000 + ) # unit conversion from m to cm squared + assert dependent_parameter.min == 200 * normal_parameter.min + assert dependent_parameter.max == 200 * normal_parameter.max + assert dependent_parameter._min.unit == "cm" + assert dependent_parameter._max.unit == "cm" + + # Then + normal_parameter.value = 2 + + # Expect + assert dependent_parameter.value == 200 * normal_parameter.value + assert dependent_parameter.unit == "cm" + assert ( + dependent_parameter.variance == normal_parameter.variance * 4 * 10000 + ) # unit conversion from m to cm squared + assert dependent_parameter.min == 200 * normal_parameter.min + assert dependent_parameter.max == 200 * normal_parameter.max + assert dependent_parameter._min.unit == "cm" + assert dependent_parameter._max.unit == "cm" + + def test_set_desired_unit_incompatible_units_raises( + self, normal_parameter: Parameter + ): + # When Then + dependent_parameter = Parameter.from_dependency( + name="dependent", + dependency_expression="2*a", + dependency_map={"a": normal_parameter}, + display_name="display_name", + ) + + # Then Expect + with pytest.raises(UnitError): + dependent_parameter.set_desired_unit("s") + + def test_set_desired_unit_independent_parameter_raises( + self, normal_parameter: Parameter + ): + # When Then Expect + with pytest.raises( + AttributeError, + match="This is an independent parameter, desired unit can only be set for dependent parameters.", + ): + normal_parameter.set_desired_unit("cm") + + def test_set_desired_unit_incorrect_unit_type_raises( + self, normal_parameter: Parameter + ): + # When Then + dependent_parameter = Parameter.from_dependency( + name="dependent", + dependency_expression="2*a", + dependency_map={"a": normal_parameter}, + display_name="display_name", + ) + + # Then Expect + with pytest.raises(TypeError, match="must be a string"): + dependent_parameter.set_desired_unit(5) + def test_set_fixed(self, parameter: Parameter): - # When Then + # When Then parameter.fixed = True # Expect @@ -545,9 +899,9 @@ def test_set_fixed(self, parameter: Parameter): def test_set_fixed_dependent_parameter(self, normal_parameter: Parameter): # When dependent_parameter = Parameter.from_dependency( - name = 'dependent', - dependency_expression='2*a', - dependency_map={'a': normal_parameter}, + name="dependent", + dependency_expression="2*a", + dependency_map={"a": normal_parameter}, ) # Then Expect @@ -563,9 +917,9 @@ def test_set_fixed_exception(self, parameter: Parameter, fixed): def test_error(self, parameter: Parameter): # When Then Expect assert parameter.error == 0.1 - + def test_set_error(self, parameter: Parameter): - # When + # When parameter.error = 10 # Then Expect @@ -584,14 +938,20 @@ def test_set_error_exception(self, parameter: Parameter): def test_repr(self, parameter: Parameter): # When Then Expect - assert repr(parameter) == "" + assert ( + repr(parameter) + == "" + ) def test_repr_fixed(self, parameter: Parameter): - # When + # When parameter.fixed = True # Then Expect - assert repr(parameter) == "" + assert ( + repr(parameter) + == "" + ) def test_value_match_callback(self, parameter: Parameter): # When @@ -600,7 +960,7 @@ def test_value_match_callback(self, parameter: Parameter): # Then Expect assert parameter.value == 1.0 assert parameter._callback.fget.call_count == 1 - + def test_value_no_match_callback(self, parameter: Parameter): # When self.mock_callback.fget.return_value = 2.0 @@ -621,14 +981,14 @@ def test_set_value(self, parameter: Parameter): # Expect parameter._callback.fset.assert_called_with(2) assert parameter._callback.fset.call_count == 1 - assert parameter._scalar == sc.scalar(2, unit='m') + assert parameter._scalar == sc.scalar(2, unit="m") def test_set_value_dependent_parameter(self, normal_parameter: Parameter): # When dependent_parameter = Parameter.from_dependency( - name = 'dependent', - dependency_expression='2*a', - dependency_map={'a': normal_parameter}, + name="dependent", + dependency_expression="2*a", + dependency_map={"a": normal_parameter}, ) # Then Expect @@ -638,14 +998,14 @@ def test_set_value_dependent_parameter(self, normal_parameter: Parameter): def test_set_full_value(self, parameter: Parameter): # When Then Expect with pytest.raises(AttributeError): - parameter.full_value = sc.scalar(2, unit='s') + parameter.full_value = sc.scalar(2, unit="s") def test_set_variance_dependent_parameter(self, normal_parameter: Parameter): # When dependent_parameter = Parameter.from_dependency( - name = 'dependent', - dependency_expression='2*a', - dependency_map={'a': normal_parameter}, + name="dependent", + dependency_expression="2*a", + dependency_map={"a": normal_parameter}, ) # Then Expect @@ -655,9 +1015,9 @@ def test_set_variance_dependent_parameter(self, normal_parameter: Parameter): def test_set_error_dependent_parameter(self, normal_parameter: Parameter): # When dependent_parameter = Parameter.from_dependency( - name = 'dependent', - dependency_expression='2*a', - dependency_map={'a': normal_parameter}, + name="dependent", + dependency_expression="2*a", + dependency_map={"a": normal_parameter}, ) # Then Expect @@ -684,13 +1044,35 @@ def test_copy(self, parameter: Parameter): assert parameter_copy._display_name == parameter._display_name assert parameter_copy._independent == parameter._independent - @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 + @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 @@ -705,7 +1087,7 @@ def test_addition_with_parameter(self, parameter : Parameter, test : Parameter, assert result.min == expected.min assert result.max == expected.max - assert result_reverse.name == result_reverse.unique_name + 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 @@ -737,10 +1119,12 @@ def test_addition_with_scalar(self): assert result_reverse.min == 1.0 assert result_reverse.max == 11.0 - def test_addition_with_descriptor_number(self, parameter : Parameter): - # When + 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") + descriptor_number = DescriptorNumber( + name="test", value=1, variance=0.1, unit="cm" + ) # Then result = parameter + descriptor_number @@ -766,21 +1150,54 @@ def test_addition_with_descriptor_number(self, parameter : Parameter): 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): + @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 + + @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 @@ -806,7 +1223,9 @@ def test_subtraction_with_parameter(self, parameter : Parameter, test : Paramete def test_subtraction_with_parameter_nan_cases(self): # When - parameter = Parameter(name="name", value=1, variance=0.01, min=-np.inf, max=np.inf) + 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 @@ -851,10 +1270,12 @@ def test_subtraction_with_scalar(self): assert result_reverse.min == -9.0 assert result_reverse.max == 1.0 - def test_subtraction_with_descriptor_number(self, parameter : Parameter): - # When + 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") + descriptor_number = DescriptorNumber( + name="test", value=1, variance=0.1, unit="cm" + ) # Then result = parameter - descriptor_number @@ -880,21 +1301,54 @@ def test_subtraction_with_descriptor_number(self, parameter : Parameter): 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): + @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 + @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 @@ -916,11 +1370,25 @@ def test_multiplication_with_parameter(self, parameter : Parameter, test : Param 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): + @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) @@ -943,12 +1411,26 @@ def test_multiplication_with_parameter_nan_cases(self, test, expected, expected_ 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 + @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 @@ -974,12 +1456,26 @@ def test_multiplication_with_descriptor_number(self, parameter : Parameter, test 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 + @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 @@ -1004,13 +1500,31 @@ def test_multiplication_with_scalar(self, parameter : Parameter, test, expected, 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 + @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 @@ -1034,11 +1548,31 @@ def test_division_with_parameter(self, parameter : Parameter, test, expected, ex 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"]) + @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 @@ -1051,12 +1585,26 @@ def test_division_with_parameter_remaining_cases(self, first, second, expected): 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 + @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 @@ -1080,12 +1628,21 @@ def test_division_with_descriptor_number_and_number(self, parameter : Parameter, 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 + @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 @@ -1098,14 +1655,46 @@ def test_zero_value_divided_by_parameter(self, parameter : Parameter, test, expe 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): + @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 @@ -1117,9 +1706,13 @@ def test_division_with_descriptor_number_missing_cases(self, first, second, expe 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 + @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 @@ -1135,20 +1728,34 @@ def test_divide_by_zero_value_parameter(self): 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"]) + @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 + # When parameter = Parameter("name", 5, "m", 0.05, -5, 10) # Then - result = parameter ** test + result = parameter**test # Expect assert type(result) == type(expected) @@ -1160,18 +1767,65 @@ def test_power_of_parameter(self, test, expected): 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"]) + @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 + result = test**exponent # Expect assert result.name == result.unique_name @@ -1181,16 +1835,33 @@ def test_power_of_diffent_parameters(self, test, exponent, expected): 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"]) + @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 + result = parameter**exponent def test_negation(self): # When @@ -1207,18 +1878,28 @@ def test_negation(self): 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"]) + @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.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 + assert result.max == expected.max