Skip to content

Issue34 dependent parameters #112

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

Draft
wants to merge 19 commits into
base: develop
Choose a base branch
from

Conversation

damskii9992
Copy link
Contributor

No description provided.

Copy link

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pull request does not contain a valid label. Please add one of the following labels: ['chore', 'fix', 'bugfix', 'bug', 'enhancement', 'feature', 'dependencies', 'documentation']

@damskii9992 damskii9992 added [scope] significant Breaking or major changes (MAJOR.minor.patch) dependencies Pull requests that update a dependency file, PR label labels Apr 10, 2025
@damskii9992
Copy link
Contributor Author

Have to add a deprecated label for the CI to proceed

@damskii9992 damskii9992 linked an issue Apr 10, 2025 that may be closed by this pull request
@damskii9992
Copy link
Contributor Author

Hmm, the _process_dependency_symbol_names actually does not work, because the output of globals() from a module yields the modules global scope, rather than the users. I do not see a solution to this problem and will instead try out an implementation with a dependency_map dictionary input

Copy link
Member

@rozyczko rozyczko left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

General points:

  • complexity: AST parsing and asteval.Interpreter pose significant complexity, making the code potentially harder to understand and maintain.
  • performance: the observer pattern and dependency evaluation could introduce performance overhead, especially in scenarios with many dependent parameters or frequent updates. We need to run benchmarks to see if the overhead is not too serious
  • error handling: too much reliant on exceptions, which can clutter logs, cause unwanted exits or fail to provide clear error messages in complex setups

Additionally, complex methods like _process_dependency_symbol_names and _process_dependency_unique_names need clear examples for developers to understand their usage.

:param description: A brief summary of what this object is
:param url: Lookup url for documentation/information
:param display_name: The name of the object as it should be displayed
:param enabled: Can the objects value be set
:param independent: Is the object dependent on another object?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We use the enabled state quite extensively in the libs/guis. Why did you remove this attribute?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, can you tell me what its purpose is?
How does it differ at all from "fixed" or (not) "independent"?
In all the code I could see about it, it looked like it was just an alias for not independent.

self._scalar.variance = temporary_parameter.variance
self._min.value = temporary_parameter.min
self._max.value = temporary_parameter.max
self._notify_observers()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only occurrence of this method being called is in DescriptorNumber. It is called from _notify_observers() method.
Does this mean the self._notify_observers() called here will be also called the DescriptorNumber and cause an infinite loop?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope. Remember that Parameter inherits from DescriptorNumber. The only reason the _notify_observers() methods is written in the DescriptorNumber code, is because a Parameter should be able to depend on a DescriptorNumber.
Currently there can be an infinite loop if a dependent parameter is dependent on another dependent parameter which relies on itself, since each of those parameters have the other parameter in their _observers list, but this will be handled by my unique update id suggestion

# Notify observers of the change
self._notify_observers()
else:
raise AttributeError("This parameter is not independent, its value cannot be set directly. Please make it independent first.") # noqa: E501
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to raise here? (And the methods below).
Maybe just notify the user and drop the assignment attempt.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, we raise when the user tries to set the full value. Since that is a read-only property.
For a dependent parameter, the value (and other entries) are also read-only properties, so I figured they should be handled in the same way. We could also raise a warning, but trying to set the value of a dependent parameter is an error, and that is why I flag it as such.

@damskii9992
Copy link
Contributor Author

General points:

  • complexity: AST parsing and asteval.Interpreter pose significant complexity, making the code potentially harder to understand and maintain.

As I mentioned in my own comment, the AST parsing didn't work anyways due to the global namescope not being available inside modules, so I am currently removing it in favour of a simpler (albeit more verbose) approach where the user has to pass a dictionary of the dependencies along with the string to create a dependent parameter (unless unique_names are used).

  • performance: the observer pattern and dependency evaluation could introduce performance overhead, especially in scenarios with many dependent parameters or frequent updates. We need to run benchmarks to see if the overhead is not too serious

Yes, I think this is an issue we would run into with any implementation of dependent parameters, including our current one, but we will definitely need to benchmark it. Especially with the approach I am currently implementing where each Parameter gets its own asteval interpreter, I am concerned about the potential performance issue as I am unsure of how heavy the asteval interpreter object is.

  • error handling: too much reliant on exceptions, which can clutter logs, cause unwanted exits or fail to provide clear error messages in complex setups

What do you mean? Most of the error handling I do is specifically aimed at providing clearer error messages when stuff fails.

Additionally, complex methods like _process_dependency_symbol_names and _process_dependency_unique_names need clear examples for developers to understand their usage.

I was gonna go through the implementation of the _process_dependency_symbol_names method in the ADR suggestion due to its complexity, with an example, before that implementation idea was scratched.
Is the _process_dependency_unique_names a complicated method? I find it pretty self-explanatory, but maybe I've gotten to used to my own code to see it?

Copy link
Collaborator

@elindgren elindgren left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had a look at the current implementation. I think it is very readable actually, and it seems like a lot of cases have been handled w.r.t cleanup etc. My main concern is performance because of ASTeval, but I don't see a better way of doing things. Would be interesting to see what the computational cost is for a real-life example.

Comment on lines +274 to +276
@notify_observers
def convert_unit(self, unit_str: str) -> None:
"""
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the user-facing version of convert_unit, right? So from the user's perspective nothing has changed API-wise w.r.t converting units?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, exactly. Nothing has changed from the user-perspective.

clean_dependency_string = dependency_expression
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:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

inputted_unique_names only contains global ids, right? So if no global id's are passed in the expression this will be skipped?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup. If there are no unique_names in the dependency expression, inputted_unique_names will be empty and therefore the for-loop will not run.

@@ -115,6 +154,7 @@ def value(self) -> numbers.Number:
return self._scalar.value

@value.setter
@notify_observers
@property_stack_deco
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This decorator should really be renamed to property_stack only. _deco looks weird.
Unrelated but annoying

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I agree. We can sneak that change into this PR :D

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider replacing rather than deleting the documentation on constraints.
It will be required to have this available and the best time to write the docs is when the feature is implemented...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think these docs actually work or are published anywhere. At least the contents does not appear to be similar to https://easyscience.github.io/corelib/modules/constraints/

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In fact, should I just remove the entire docs folder? There is basically nothing in it.
I still don't know exactly what our documentation plans are.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
dependencies Pull requests that update a dependency file, PR label [scope] significant Breaking or major changes (MAJOR.minor.patch)
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Replace constraints with dependent Parameters
3 participants