-
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
Merged
Merged
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
de923ea
Added desired units and tests
henrikjacobsenfys 585305e
small update
henrikjacobsenfys 9202613
Update based on PR comments
henrikjacobsenfys 5d5199f
Added a few tests
henrikjacobsenfys 05be062
Update based on PR comments
henrikjacobsenfys 5826b23
Added a small test
henrikjacobsenfys 5b69f11
Update unit to desired_unit in dependencies
henrikjacobsenfys 28530a2
linting
henrikjacobsenfys 7f32db5
Merge branch 'develop' into dependent-parameter-units
henrikjacobsenfys fa76085
Small fixes
henrikjacobsenfys 0ca4b3d
Merge branch 'dependent-parameter-units' of https://github.com/easysc…
henrikjacobsenfys 9c5da62
Add serialization test
henrikjacobsenfys f899325
fix serialization
henrikjacobsenfys File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -121,23 +121,33 @@ 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. | ||
|
|
||
| :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: | ||
|
|
@@ -158,11 +168,20 @@ 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, | ||
| 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,6 +202,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 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.') | ||
|
|
@@ -212,13 +234,17 @@ def make_dependent_on(self, dependency_expression: str, dependency_map: Optional | |
| '_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) | ||
|
|
||
| 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, | ||
|
|
@@ -289,6 +315,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 +343,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 +508,28 @@ 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.') | ||
|
|
||
| if unit_str is not None: | ||
| try: | ||
| old_unit_for_message = self.unit | ||
| self._convert_unit(unit_str) | ||
| except Exception as e: | ||
| raise UnitError(f'Failed to convert unit from {old_unit_for_message} to {unit_str}: {e}') | ||
|
|
||
| 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: | ||
| """ | ||
|
|
@@ -580,6 +640,9 @@ def as_dict(self, skip: Optional[List[str]] = None) -> Dict[str, Any]: | |
| # Save the dependency expression | ||
| raw_dict['_dependency_string'] = self._clean_dependency_string | ||
|
|
||
| if self._desired_unit is not None: | ||
| raw_dict['_desired_unit'] = self._desired_unit | ||
|
|
||
| # Mark that this parameter is dependent | ||
| raw_dict['_independent'] = self._independent | ||
|
|
||
|
|
@@ -648,6 +711,7 @@ def from_dict(cls, obj_dict: dict) -> 'Parameter': | |
| 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) | ||
| desired_unit = raw_dict.pop('_desired_unit', None) | ||
| # 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__) | ||
|
|
@@ -659,6 +723,7 @@ def from_dict(cls, obj_dict: dict) -> 'Parameter': | |
| param._pending_dependency_map_serializer_ids = dependency_map_serializer_ids | ||
| # Keep parameter as independent initially - will be made dependent after all objects are loaded | ||
| param._independent = True | ||
| param._pending_desired_unit = desired_unit | ||
|
|
||
| return param | ||
|
|
||
|
|
@@ -874,7 +939,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] | ||
| else: | ||
|
|
@@ -1017,13 +1087,18 @@ def resolve_pending_dependencies(self) -> None: | |
|
|
||
| # 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, | ||
| desired_unit=self._pending_desired_unit, | ||
| ) | ||
| except Exception as 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_desired_unit') | ||
|
|
||
| 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.""" | ||
|
|
||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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 :)