Serializing Dependent Parameters
A Parameter can depend on other parameters through an expression (see
Parameter.make_dependent_on and Parameter.from_dependency). This
page explains how such parameters are written to a dictionary or JSON
and how their dependencies are rebuilt when they are loaded.
Quick Start
Serialize every parameter involved with to_dict:
import json
from easyscience import Parameter
a = Parameter(value=2.0, unit='m', min=0, max=10, display_name='a')
b = Parameter.from_dependency(
dependency_expression='2 * a',
dependency_map={'a': a},
display_name='b',
)
with open('parameters.json', 'w') as f:
json.dump({'a': a.to_dict(), 'b': b.to_dict()}, f, indent=2)
To load them, deserialize all parameters first. Then resolve the dependencies in a single step:
import json
from easyscience import Parameter
from easyscience.variable.parameter_dependency_resolver import (
resolve_all_parameter_dependencies,
)
with open('parameters.json') as f:
params_dict = json.load(f)
new_a = Parameter.from_dict(params_dict['a'])
new_b = Parameter.from_dict(params_dict['b'])
resolve_all_parameter_dependencies({'a': new_a, 'b': new_b})
new_a.value = 5.0
print(new_b.value) # 10.0
Always resolve after loading
Parameter.from_dict does not restore the dependency on its
own. Until resolve_all_parameter_dependencies (or
Parameter.resolve_pending_dependencies) is called, a loaded
dependent parameter behaves as an independent parameter holding the
value it had when it was saved.
Alternatively, if all you have is a dictionary of serialized parameters,
the convenience helper deserialize_and_resolve_parameters combines
both steps:
from easyscience.variable.parameter_dependency_resolver import (
deserialize_and_resolve_parameters,
)
params = deserialize_and_resolve_parameters(params_dict)
params['a'].value = 3.0
print(params['b'].value) # 6.0
What Is Written
An independent parameter is written with its constructor arguments
(value, unit, variance, min, max, fixed, description,
url, and display_name/unique_name when they were set explicitly).
A dependent parameter gets the following additional fields:
| Field | Meaning |
|---|---|
_dependency_string |
The dependency expression, e.g. "2 * a". |
_dependency_map_serializer_ids |
Maps each symbol in the expression to the serializer id of the object it refers to. |
_independent |
false, marking the parameter as dependent. |
_desired_unit |
Only present if a desired_unit was given when making the dependency. |
Any parameter that something else depends on also writes its own
__serializer_id. This is a UUID assigned the first time another
parameter depends on it. It is what _dependency_map_serializer_ids
refers to:
{
"@module": "easyscience.variable.parameter",
"@class": "Parameter",
"value": 4.0,
"unit": "m",
"display_name": "b",
"_dependency_string": "2 * a",
"_independent": false,
"_dependency_map_serializer_ids": {
"a": "a48db37b-519b-4bea-9873-aa3cc5b2ec76"
}
}
Serializer ids are independent of unique_name. A loaded parameter can
therefore receive a fresh unique name, avoiding a clash with objects
that already exist in the session, and its dependencies still find each
other.
How Loading Works
Parameter.from_dictcreates each parameter as an ordinary independent parameter and restores its__serializer_id.- For dependent parameters, the expression, the serializer-id map and the desired unit are kept on the object as pending dependency information.
resolve_all_parameter_dependencies(obj)walksobjand finds every parameter with pending information.objcan be a singleParameter, a list, tuple or dict of them, or an object exposing them as public attributes or properties. For each parameter it looks up the referenced objects by serializer id among all objects alive in the session and callsmake_dependent_on. It then removes the pending information.
Because resolution happens only after everything is loaded, parameters can be deserialized in any order. A dependent parameter may come before the parameters it depends on.
To check what still needs resolving, use
get_parameters_with_pending_dependencies:
from easyscience.variable.parameter_dependency_resolver import (
get_parameters_with_pending_dependencies,
)
pending = get_parameters_with_pending_dependencies({'a': new_a, 'b': new_b})
print(f'{len(pending)} parameter(s) still need resolving')
Errors
resolve_all_parameter_dependencies tries every pending parameter and
then raises a single ValueError listing all failures. The typical
cause is a referenced parameter that was not loaded:
ValueError: Failed to resolve dependencies for 1 parameter(s):
Failed to resolve dependencies for parameter 'Parameter_0' (display_name: 'b', serializer_id: 'unknown'): Cannot find parameter with serializer_id 'f9040c2a-...'
An invalid expression, or one that would create a cyclic dependency, is reported the same way, with the underlying error message appended.
Best Practices
- Serialize the whole dependency graph. Every parameter referenced by a dependency expression must be saved and loaded together with the parameters that depend on it.
- Load everything, then resolve once. Deserialize all parameters
first and call
resolve_all_parameter_dependencieson the collection as a whole. - Keep the referenced objects alive. Lookup goes through the global object registry, so a parameter that has been garbage-collected cannot be found.
- Do not load the same data twice in one session. Serializer ids are restored verbatim. If the original parameters, or an earlier load of the same file, are still alive, a dependency can be bound to that older object instead of the one just loaded.
Parameters nested in models
Dependency information is written by Parameter.to_dict. Calling
to_dict on a ModelBase that contains dependent parameters
does not currently go through Parameter.to_dict, so the
dependency fields are not included. Until this is addressed,
serialize dependent parameters directly as shown above.