Dependent Parameters #121
Locked
damskii9992
announced in
ADRs
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Making a
Parameter
dependentThere are 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
.Both the
make_dependent_on
andfrom_dependency
methods take the two argumentsdependency_expression
anddependency_map
. Thedependency_expression
is an arithmetic or logic expression as a string which, when evaluated, should return aParameter
or aDescriptorNumber
. The dependentParameter
evaluates this expression whenever its dependencies are updated, and copies the attributes of the returnedParameter
orDescriptorNumber
over to itself.The
dependency_map
is a dictionary mapping of string-object key-value pairs with the keys being the objects string representation in thedependency_expression
and the values being a reference to the actual object.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 (see "The observer pattern" further down) 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.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.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 also allow for logical dependencies through ternary operations, such as:
Implementation Details
Dependent
Parameter
s are 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 is 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 is 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
To solve this we check the validity of the dependency chain whenever making a dependent parameter by calling the
_validate_dependencies
method (implemented inDescriptorNumber
) before any of the parameters values are changed. This method pings all of the parameters observers (if any) passing along itsunique_name
, these parameters then ping their own observers, passing along theunique_name
and so on. If theunique_names
passed along the ping matches the objects ownunique_name
, a cyclic dependency has been detected and an error is thrown:Only in case this dependency-traversal doesn't raise an error, is the dependent parameter updated with the dependency expression. This implementation allows for dependencies of dependencies and will detect if a cyclic dependency has been created.
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/ternary 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
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
.Errors during the creation of a dependent parameter
If an error is thrown during the creation of a dependent
Parameter
, such as if the supplieddependency_expression
is faulty or if a cyclic dependency is detected by the_validate_dependencies
method, theParameter
is reverted to its previous state, using the data stored in the_previous_dependency
dictionary and the_previous_independent
boolean attributes.This includes attaching/detaching itself as an observer if an already dependent
Parameter
was attempted to have itsdependency_expression
updated.This functionality is provided by the
_revert_dependency
method which is called in the try/catch statements just before the relevant errors are raised:Link to the ADR suggestion:
#10
Major discussions were also had during the PR here:
#112
Link to ADR suggestions changing this ADR:
#122
Beta Was this translation helpful? Give feedback.
All reactions