ADR Suggestion
Dependent Parameters
#10
Replies: 6 comments 6 replies
-
Note that in order to update the part of the GUI that displays the changed parameter, you must decorate the setter and getter of the parameter and emit the parameter changed signal, as shown in the example below: from PySide6.QtCore import QObject, Signal, Slot, Property
class Model(QObject):
definedChanged = Signal()
def __init__(self):
self._defined = False
@Property(bool, notify=definedChanged)
def defined(self):
return self._defined
@defined.setter
def defined(self, newValue):
if self._defined == newValue:
return
self._defined = newValue
self.definedChanged.emit() Now every time you assign a new value to the property |
Beta Was this translation helpful? Give feedback.
-
To my understanding we would also need functionality to inform the independent parameter when a new dependent parameter is made dependent. |
Beta Was this translation helpful? Give feedback.
-
Overall, this is a great suggestion. I have a few comments:
|
Beta Was this translation helpful? Give feedback.
-
Another comment: in this implementation, you are using the Observer pattern, where all dependent parameters act as observers subscribing to changes in the independent parameters. What is the advantage of this approach compared to, for example, implementing a separate class that manages all dependencies centrally, so that individual parameters do not need to know about each other? |
Beta Was this translation helpful? Give feedback.
-
One more question. Let's use one of the diagrams you showed earlier: flowchart LR;
A-->|update|B;
B-->|update|C;
C-->|update|D;
D-->|update|B;
Do you have any mechanism to handle the case where, for example, parameter |
Beta Was this translation helpful? Give feedback.
-
What is the reason for having |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
General
It can be beneficial in a model to have Parameters which are defined through a relation to other parameters/descriptors. These dependent parameters are defined by their relation and thus should not be fitted during minimization. Their value should be updated when the values of the independent parameters which they depend on are changed. It should be possible to easily convert a dependent parameter to a dependent parameter and vice versa.
Current Implementation
Currently, this is handled by the "constraints" objects which are created separately and then assigned to the parameter, such as:
The "constraints" object can only constrain according to a select few operations and only 1 parameter to another.
Proposed Implementation
The concept of a dependent
Parameter
can be implemented using the generic "Observer" coding pattern, with all the dependent parameters being observers subscribing to the independent parameters.To allow for great flexibility in the type of possible dependencies, the update of a dependent parameter can be done using the
asteval
python interpreter with its functionality limited to only arithmetic and logical operation, and its symtable including only the independent parameters.The observer pattern
The observer pattern is a fairly simple pattern including only 4 basic methods and 1 attribute:
This is the basic construct of the observed object, here namely the independent parameters. Since a dependent
Parameter
can use aDescriptorNumber
for its relation, this part of the observer pattern should be implemented on theDescriptorNumber
Since only
Parameters
can be a dependent parameter, the_update
method is defined in theParameter
class.Avoiding cyclic dependencies
The problem
Since parameters can be made dependent after they're created, it is possible to end up in an infinite loop where a dependent parameters update triggers another dependent parameters update which in turn triggers the first parameters update and so on:
The solution
It is possible to avoid this by simply requiring that dependent parameters only depend on independent parameters, but it is easy to imagine a use-case where a dependency on a dependency is beneficial. A better solution instead is to use update_ids. The concept is simple:
If an update is triggered by a manual change, such as a value change of an independent parameter, the update gets assigned a unique id.
This unique id is then passed to the observers along with the unique_name of the object that triggered the update;
In the observers
_update
method, the update id is checked against an internal dictionary for the updating object. If the stored update id for the updating object is different from the new one, set the stored update id to the new one and proceed with the update, and notify its own observers, passing along the update_id, otherwise raise an error warning that a cyclic dependency has been detected:This implementation allows for dependencies of dependencies and will detect if a cyclic dependency has been created.
An edge-case
While this implementation does allow for dependencies of dependencies, it will also flag some perfectly valid dependency trees as cyclic dependencies, such as:
In the dependency-tree above, Parameter D gets updated by Parameter C twice with the same update id, first when A directly updates C which in turns trigger the update of D, and then again when A updates B, which updates C which again updates D.
This dependency-tree is perfectly valid, but will be flagged as a cyclic dependency because Parameter D gets updated twice by Parameter C with the same update id.
Making a dependent
Parameter
There will be 2 ways to make a dependent
Parameter
. An already createdParameter
can be made dependent by using themake_dependent_on
method:Or by using the class method
from_dependency
to construct it directly:In either case, the
Parameter
s internal attribute_independent
will be set toFalse
, which will lock all the setter methods and disable the parameter for fitting.In addition, since a "fixed" and "dependent" parameter does not make sense, the
fixed
attribute will also be set toFalse
.The
_independent
attribute is a read-only attribute, meaning its setter simply raises a error. To make a dependent parameter independent, one has to use themake_independent()
method.This is done to ensure that the object is properly detached from the list of observers for all its independent parameters:
For the same reason, to change the dependency of an already dependent parameter, one has to use the
make_dependent_on
method again.The
dependency_expression
evaluation withasteval
When a parameter is made dependent, a python interpreter object is created with
asteval
and attached to it. We useasteval
to limit the interpreters functionality to only arithmetic and logical expressions, to avoid many of the potential safety issues with embedded interpreters: https://lmfit.github.io/asteval/motivation.html.The
asteval
interpreter has no access to the local or global namespace, so in order to useParameters
orDescriptorNumbers
in it, these first have to be added to the interpreters symtable. This is the purpose of the optional argumentdependency_map
, which is a dictionary mapping the names used in thedependency_expression
to existing objects.After the
dependency_map
has been added to theasteval
interpreters symtable, thedependency_expression
is evaluated with it.If the resulting output is either a
DescriptorNumber
or aParameter
, the dependent parameters attributes are updated to the ones of the output:If the resulting output is not a
DescriptorNumber
or aParameter
, an error is raised.Note that the min and max values are simply set to the value of the output if the output is a
DescriptorNumber
.Unique names in the
dependency_expression
To improve the ease of use, especially when the parameters to otherwise add to the
dependency_map
is hidden away behind many layers of objects, or if its destination is simply unknown, we also allow the use of unique_names in thedependency_expression
:To use unique_names in the
dependency_expression
they must be encapsulated by either "" or '' (depending on which were used to define thedependency_expression
string). When unique_names are used, the objects does not need to be added to thedependency_map
argument. Mixtures ofdependency_map
arguments and unique_names can be used.This functionality is provided by the internal
_process_dependency_unique_names
method, which uses regex to scan thedependency_expression
for unique_names, checks if these exists in theglobal_object
and then adds them to the internal_dependency_map
:Note that the unique_names are pre-fixed and pre-pended with double underscores '__' in the
_dependency_map
and that thedependency_expression
has the unique_names replaced internally with these new names instead. This is done in an attempt to avoid name clashes with names in the argumentdependency_map
.Arithmetic and logic dependencies
To provide extremely flexible and user-friendly dependencies, we rely on the arithmetic operations between
Parameters
andDescriptorNumbers
, as described in #54. This allows for many different kinds of complicated dependency expressions:We use a very minimal configuration of the
asteval
interpreter, allowing only arithmetic operations in thedependency_expression
, but we also open up for ternary operations, to allow for complicated logical dependencies:Beta Was this translation helpful? Give feedback.
All reactions