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
Parametercan 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
astevalpython 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
Parametercan use aDescriptorNumberfor its relation, this part of the observer pattern should be implemented on theDescriptorNumberSince only
Parameterscan be a dependent parameter, the_updatemethod is defined in theParameterclass.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
_updatemethod, 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
ParameterThere will be 2 ways to make a dependent
Parameter. An already createdParametercan be made dependent by using themake_dependent_onmethod:Or by using the class method
from_dependencyto construct it directly:In either case, the
Parameters internal attribute_independentwill 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
fixedattribute will also be set toFalse.The
_independentattribute 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_onmethod again.The
dependency_expressionevaluation withastevalWhen a parameter is made dependent, a python interpreter object is created with
astevaland attached to it. We useastevalto 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
astevalinterpreter has no access to the local or global namespace, so in order to useParametersorDescriptorNumbersin 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_expressionto existing objects.After the
dependency_maphas been added to theastevalinterpreters symtable, thedependency_expressionis evaluated with it.If the resulting output is either a
DescriptorNumberor aParameter, the dependent parameters attributes are updated to the ones of the output:If the resulting output is not a
DescriptorNumberor 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_expressionTo improve the ease of use, especially when the parameters to otherwise add to the
dependency_mapis 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_expressionthey must be encapsulated by either "" or '' (depending on which were used to define thedependency_expressionstring). When unique_names are used, the objects does not need to be added to thedependency_mapargument. Mixtures ofdependency_maparguments and unique_names can be used.This functionality is provided by the internal
_process_dependency_unique_namesmethod, which uses regex to scan thedependency_expressionfor unique_names, checks if these exists in theglobal_objectand 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_mapand that thedependency_expressionhas 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
ParametersandDescriptorNumbers, as described in #54. This allows for many different kinds of complicated dependency expressions:We use a very minimal configuration of the
astevalinterpreter, 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