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
ParameterdependentThere are 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.Both the
make_dependent_onandfrom_dependencymethods take the two argumentsdependency_expressionanddependency_map. Thedependency_expressionis an arithmetic or logic expression as a string which, when evaluated, should return aParameteror aDescriptorNumber. The dependentParameterevaluates this expression whenever its dependencies are updated, and copies the attributes of the returnedParameterorDescriptorNumberover to itself.The
dependency_mapis a dictionary mapping of string-object key-value pairs with the keys being the objects string representation in thedependency_expressionand the values being a reference to the actual object.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 (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_onmethod again.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.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 also allow for logical dependencies through ternary operations, such as:
Implementation Details
Dependent
Parameters 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
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 is 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
To solve this we check the validity of the dependency chain whenever making a dependent parameter by calling the
_validate_dependenciesmethod (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_nameand so on. If theunique_namespassed 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_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/ternary 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_expressionThis 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.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_expressionis faulty or if a cyclic dependency is detected by the_validate_dependenciesmethod, theParameteris reverted to its previous state, using the data stored in the_previous_dependencydictionary and the_previous_independentboolean attributes.This includes attaching/detaching itself as an observer if an already dependent
Parameterwas attempted to have itsdependency_expressionupdated.This functionality is provided by the
_revert_dependencymethod 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