Getting the state of an experiment bench#
To replicate an experiment, it is useful to compare the state of the original and the current experiment bench, ensuring they are as close as possible. Phileas has different tools that allow to do this.
Example bench#
Let us work with the following simple experiment bench:
1bench = """
2simulated-motors:
3 loader: phileas-mock_motors-phileas
4
5simulated-oscilloscope:
6 loader: phileas-mock_oscilloscope-phileas
7 probe: electric-field-probe
8 motors: simulated-motors
9"""
10
11experiment = """
12oscilloscope:
13 interface: oscilloscope
14 amplitude: !range
15 start: 0.1
16 end: 10
17 steps: 5
18
19iteration: !sequence [1, 2]
20"""
21
22factory = ExperimentFactory(bench, experiment)
23factory.initiate_connections()
Verifying the effectiveness of an applied configuration#
After having configured an instrument, it is possible that its effective
configuration differs from the required one. You can use
get_effective_instrument_configuration()
and
get_effective_experiment_configuration()
to know the exact state of the instrument.
In our case, the amplitude of the oscilloscope can only be a power of 10. Configuring it, and getting its effective configuration, can be done with:
1for conf in factory.experiment_config:
2 factory.configure_experiment(conf)
3 effective_conf = factory.get_effective_instrument_configuration("oscilloscope")
4
5 required_amp = conf["oscilloscope"]["amplitude"]
6 effective_amp = effective_conf["amplitude"]
7 print(f"Configuration: {conf}")
8 print(f"Amplitude, required: {required_amp}\tEffective: {effective_amp}\n")
Configuration: {'iteration': 1, 'oscilloscope': {'amplitude': 0.1}}
Amplitude, required: 0.1 Effective: 0.1
Configuration: {'iteration': 1, 'oscilloscope': {'amplitude': 2.575}}
Amplitude, required: 2.575 Effective: 1
Configuration: {'iteration': 1, 'oscilloscope': {'amplitude': 5.05}}
Amplitude, required: 5.05 Effective: 10
Configuration: {'iteration': 1, 'oscilloscope': {'amplitude': 7.525}}
Amplitude, required: 7.525 Effective: 10
Configuration: {'iteration': 1, 'oscilloscope': {'amplitude': 10.0}}
Amplitude, required: 10.0 Effective: 10
Configuration: {'iteration': 2, 'oscilloscope': {'amplitude': 0.1}}
Amplitude, required: 0.1 Effective: 0.1
Configuration: {'iteration': 2, 'oscilloscope': {'amplitude': 2.575}}
Amplitude, required: 2.575 Effective: 1
Configuration: {'iteration': 2, 'oscilloscope': {'amplitude': 5.05}}
Amplitude, required: 5.05 Effective: 10
Configuration: {'iteration': 2, 'oscilloscope': {'amplitude': 7.525}}
Amplitude, required: 7.525 Effective: 10
Configuration: {'iteration': 2, 'oscilloscope': {'amplitude': 10.0}}
Amplitude, required: 10.0 Effective: 10
Let us now consider the case where we set the lazy and snake parameters of the configuration:
1exp_conf = factory.experiment_config.with_params(lazy=True, snake=True)
2for conf in exp_conf:
3 print(conf)
{'iteration': 1, 'oscilloscope': {'amplitude': 0.1}}
{'oscilloscope': {'amplitude': 2.575}}
{'oscilloscope': {'amplitude': 5.05}}
{'oscilloscope': {'amplitude': 7.525}}
{'oscilloscope': {'amplitude': 10.0}}
{'iteration': 2}
{'oscilloscope': {'amplitude': 7.525}}
{'oscilloscope': {'amplitude': 5.05}}
{'oscilloscope': {'amplitude': 2.575}}
{'oscilloscope': {'amplitude': 0.1}}
The amplitude of the oscilloscope is not always configured. Thus, it is not
efficient to retrieve its configuration at each step of the loop. In this case,
we can give
get_effective_experiment_configuration()
an empty configuration that it then fills, ignoring missing fields:
1for conf in exp_conf:
2 factory.configure_experiment(conf)
3 effective_conf = factory.get_effective_experiment_configuration(conf)
4
5 print(f"Required: {conf}\nEffective: {effective_conf}\n")
Required: {'iteration': 1, 'oscilloscope': {'amplitude': 0.1}}
Effective: {'oscilloscope': {'amplitude': 0.1}}
Required: {'oscilloscope': {'amplitude': 2.575}}
Effective: {'oscilloscope': {'amplitude': 1}}
Required: {'oscilloscope': {'amplitude': 5.05}}
Effective: {'oscilloscope': {'amplitude': 10}}
Required: {'oscilloscope': {'amplitude': 7.525}}
Effective: {'oscilloscope': {'amplitude': 10}}
Required: {'oscilloscope': {'amplitude': 10.0}}
Effective: {'oscilloscope': {'amplitude': 10}}
Required: {'iteration': 2}
Effective: {}
Required: {'oscilloscope': {'amplitude': 7.525}}
Effective: {'oscilloscope': {'amplitude': 10}}
Required: {'oscilloscope': {'amplitude': 5.05}}
Effective: {'oscilloscope': {'amplitude': 10}}
Required: {'oscilloscope': {'amplitude': 2.575}}
Effective: {'oscilloscope': {'amplitude': 1}}
Required: {'oscilloscope': {'amplitude': 0.1}}
Effective: {'oscilloscope': {'amplitude': 0.1}}
Note
Not all loaders implement
get_effective_configuration(). They are
ignored by the methods which retrieve the effective configuration of an
instrument or an entire bench.
Dumping the complete state of an experiment bench#
Retrieving the configuration of an instrument is not sufficient to guarantee
that its state is the same as in another experiment. The experiment factory
provides the
dump_bench_state() method, whose
purpose is to build the most complete representation of the state of an
experiment bench.
1pprint(factory.dump_bench_state())
BenchState(version='0.5',
instruments={'simulated-motors': InstrumentState(loader='phileas-mock_motors-phileas',
interfaces=['2d-motors'],
configuration={},
id='mock-motors-driver:1',
state={'x': 0.0,
'y': 0.0}),
'simulated-oscilloscope': InstrumentState(loader='phileas-mock_oscilloscope-phileas',
interfaces=['oscilloscope'],
configuration={'motors': 'simulated-motors',
'probe': 'electric-field-probe'},
id='mock-oscilloscope-driver:1',
state={'amplitude': 0.1,
'bit_width': 8,
'fw_version': '12.3',
'probe': 'ElectricFieldProbe'})})
You can use the state as is. Alternatively, you can export it to YAML, allowing to save it or compare it to another saved state:
1saved_state = factory.dump_bench_state().to_yaml()
2print(saved_state)
version: '0.5'
instruments:
simulated-motors:
loader: phileas-mock_motors-phileas
interfaces:
- 2d-motors
configuration: {}
id: mock-motors-driver:1
state:
x: 0.0
y: 0.0
simulated-oscilloscope:
loader: phileas-mock_oscilloscope-phileas
interfaces:
- oscilloscope
configuration:
probe: electric-field-probe
motors: simulated-motors
id: mock-oscilloscope-driver:1
state:
probe: ElectricFieldProbe
amplitude: 0.1
bit_width: 8
fw_version: '12.3'
to_yaml() also takes an optional argument,
which is the path of a file where the state can be stored.
factory.dump_bench_state().to_yaml(Path("state.yaml"))
If you have a bench and an experiment configuration file, and a script which registers the required loaders, you can use the CLI to dump the state of the bench:
$ python -m phileas --bench bench.yaml --experiment experiment.yaml --script script.py
version: '0.5'
instruments:
simulated-motors:
loader: phileas-mock_motors-phileas
interfaces:
- 2d-motors
configuration: {}
id: mock-motors-driver:1
state:
simulated-oscilloscope:
loader: phileas-mock_oscilloscope-phileas
interfaces:
- oscilloscope
configuration:
probe: electric-field-probe
motors: simulated-motors
id: mock-oscilloscope-driver:1
state:
Note
You can use exported states to compare them, with diff for example.
Once you have stored a state, you can use it later to restore the bench:
1state = BenchState.from_yaml(saved_state)
2factory.restore_bench_state(state)
---------------------------------------------------------------------------
UnionMatchError Traceback (most recent call last)
Cell In[8], line 1
----> 1 state = BenchState.from_yaml(saved_state)
2 factory.restore_bench_state(state)
File ~/checkouts/readthedocs.org/user_builds/phileas/checkouts/stable/phileas/factory.py:432, in BenchState.from_yaml(cls, source)
429 raw = _yaml_load_parser.load(f)
431 config = dacite.Config(forward_references={"_NoDefault": None})
--> 432 return dacite.from_dict(data_class=BenchState, data=raw, config=config)
File ~/checkouts/readthedocs.org/user_builds/phileas/envs/stable/lib/python3.13/site-packages/dacite/core.py:69, in from_dict(data_class, data, config)
67 if key in data:
68 try:
---> 69 value = _build_value(type_=field_type, data=data[key], config=config)
70 except DaciteFieldError as error:
71 error.update_path(field.name)
File ~/checkouts/readthedocs.org/user_builds/phileas/envs/stable/lib/python3.13/site-packages/dacite/core.py:105, in _build_value(type_, data, config)
103 data = _build_value_for_union(union=type_, data=data, config=config)
104 elif is_generic_collection(type_):
--> 105 data = _build_value_for_collection(collection=type_, data=data, config=config)
106 elif cache(is_dataclass)(orig(type_)) and isinstance(data, Mapping):
107 data = from_dict(data_class=type_, data=data, config=config)
File ~/checkouts/readthedocs.org/user_builds/phileas/envs/stable/lib/python3.13/site-packages/dacite/core.py:150, in _build_value_for_collection(collection, data, config)
148 if isinstance(data, Mapping) and is_subclass(collection, Mapping):
149 item_type = extract_generic(collection, defaults=(Any, Any))[1]
--> 150 return data_type((key, _build_value(type_=item_type, data=value, config=config)) for key, value in data.items())
151 elif isinstance(data, tuple) and is_subclass(collection, tuple):
152 if not data:
File ~/checkouts/readthedocs.org/user_builds/phileas/envs/stable/lib/python3.13/site-packages/dacite/core.py:150, in <genexpr>(.0)
148 if isinstance(data, Mapping) and is_subclass(collection, Mapping):
149 item_type = extract_generic(collection, defaults=(Any, Any))[1]
--> 150 return data_type((key, _build_value(type_=item_type, data=value, config=config)) for key, value in data.items())
151 elif isinstance(data, tuple) and is_subclass(collection, tuple):
152 if not data:
File ~/checkouts/readthedocs.org/user_builds/phileas/envs/stable/lib/python3.13/site-packages/dacite/core.py:107, in _build_value(type_, data, config)
105 data = _build_value_for_collection(collection=type_, data=data, config=config)
106 elif cache(is_dataclass)(orig(type_)) and isinstance(data, Mapping):
--> 107 data = from_dict(data_class=type_, data=data, config=config)
108 for cast_type in config.cast:
109 if is_subclass(type_, cast_type):
File ~/checkouts/readthedocs.org/user_builds/phileas/envs/stable/lib/python3.13/site-packages/dacite/core.py:69, in from_dict(data_class, data, config)
67 if key in data:
68 try:
---> 69 value = _build_value(type_=field_type, data=data[key], config=config)
70 except DaciteFieldError as error:
71 error.update_path(field.name)
File ~/checkouts/readthedocs.org/user_builds/phileas/envs/stable/lib/python3.13/site-packages/dacite/core.py:103, in _build_value(type_, data, config)
101 return data
102 if is_union(type_):
--> 103 data = _build_value_for_union(union=type_, data=data, config=config)
104 elif is_generic_collection(type_):
105 data = _build_value_for_collection(collection=type_, data=data, config=config)
File ~/checkouts/readthedocs.org/user_builds/phileas/envs/stable/lib/python3.13/site-packages/dacite/core.py:143, in _build_value_for_union(union, data, config)
141 if not config.check_types:
142 return data
--> 143 raise UnionMatchError(field_type=union, value=data)
UnionMatchError: can not match type "dict" to any type of "instruments.state" union: typing.Union[NoneType, bool, str, int, float, dict[bool | str | int | float, ForwardRef('DataTree')], list[ForwardRef('DataTree')]]
restore_bench_state() has an
optional argument strict which, when set, prevents restoring states whose
version, instrument names, loaders, interfaces and ids do not exactly match.
1state_with_invalid_id = BenchState.from_yaml("""
2version: '0.4'
3instruments:
4 simulated-motors:
5 loader: phileas-mock_motors-phileas
6 interfaces:
7 - 2d-motors
8 configuration: {}
9 id: mock-motors-driver:2
10 state:
11 x: 0.0
12 y: 0.0
13""")
14factory.restore_bench_state(state_with_invalid_id)
Replication of the software environment of an experiment#
Given properly implemented loaders, with the same bench and experiment configuration files, Phileas can guarantee the proper replication of an experiment. However, the user is in charge of the replication of the software environment of the experiment.
For this, different tools exist, such as conda or poetry. It is recommended
that you use them, and store their lock files alongside state.yaml.