-
Notifications
You must be signed in to change notification settings - Fork 2
Added desired units and tests #188
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
de923ea
585305e
9202613
5d5199f
05be062
5826b23
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -121,23 +121,29 @@ 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, | ||
| unit: str | sc.Unit | None = None, | ||
| **kwargs, | ||
| ) -> Parameter: # noqa: E501 | ||
| """ | ||
| Create a dependent Parameter directly from a dependency expression. | ||
|
|
||
| :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 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, unit=unit) | ||
| return parameter | ||
|
|
||
| def _update(self) -> None: | ||
|
|
@@ -158,11 +164,17 @@ def _update(self) -> None: | |
| ) # 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.') | ||
|
|
||
| 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, 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. | ||
|
Comment on lines
+176
to
179
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's no validation on
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added validation. |
||
|
|
||
|
|
@@ -183,6 +195,9 @@ 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 unit: | ||
| The desired unit of the dependent parameter. If None, the 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.') | ||
|
|
@@ -219,6 +234,9 @@ 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 unit is not None and not (isinstance(unit, str) or isinstance(unit, sc.Unit)): | ||
| raise TypeError('`unit` must be a string representing a valid unit.') | ||
| self._desired_unit = unit | ||
| # List of allowed python constructs for the asteval interpreter | ||
| asteval_config = { | ||
| 'import': False, | ||
|
|
@@ -289,6 +307,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,6 +335,7 @@ 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.') | ||
|
|
||
|
|
@@ -470,6 +500,21 @@ 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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is missing a check on the type of |
||
| self._update() | ||
|
|
||
| @property | ||
| def min(self) -> numbers.Number: | ||
| """ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -136,6 +136,85 @@ def test_make_dependent_on(self, normal_parameter: Parameter): | |
| normal_parameter.value == 4 | ||
| self.compare_parameters(normal_parameter, 2*independent_parameter) | ||
|
|
||
|
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's no test verifying that
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point! Expanded the |
||
| 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}, 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}, 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}, 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}, unit=123) | ||
|
|
||
|
|
||
| def test_parameter_from_dependency(self, normal_parameter: Parameter): | ||
| # When Then | ||
| dependent_parameter = Parameter.from_dependency( | ||
|
|
@@ -159,6 +238,57 @@ def test_parameter_from_dependency(self, normal_parameter: Parameter): | |
| # 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', | ||
| 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' | ||
|
|
||
| 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_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', | ||
| unit = "s", | ||
| ) | ||
|
|
||
|
|
||
| def test_dependent_parameter_with_unique_name(self, clear, normal_parameter: Parameter): | ||
| # When Then | ||
| dependent_parameter = Parameter.from_dependency( | ||
|
|
@@ -471,6 +601,7 @@ def test_dependent_parameter_dependency_map_setter(self, normal_parameter: Param | |
| with pytest.raises(AttributeError): | ||
| dependent_parameter.dependency_map = {'a': normal_parameter} | ||
|
|
||
|
|
||
| def test_min(self, parameter: Parameter): | ||
| # When Then Expect | ||
| assert parameter.min == 0 | ||
|
|
@@ -535,6 +666,72 @@ 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 | ||
| parameter.fixed = True | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_convert_unitdoesn't catch any exceptions and we aren't checking anything here. Maybe add some exception handling so it doesn't bubble up potentially leaving the parameter in a weird state?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is on purpose. To keep the
_updatemethod as fast as possible, all the checks are made in themake_dependent_onmethod. If this method succeeds, then the_updatewill also always succeed.This is also how we did it for the other updates :)