--- jupytext: text_representation: extension: .md format_name: myst format_version: 0.13 jupytext_version: 1.17.1 kernelspec: name: python3 display_name: Python 3 (ipykernel) language: python --- # Experiment configuration file ```{code-cell} ipython3 :tags: [remove-cell] from pprint import pprint from phileas.parsing import load_iteration_tree_from_yaml_file ``` The experiment configuration file describes the instruments required by an experiment, their parameter space and how to iterate through it. ```{code-block} yaml :caption: Typical `experiment.yaml` content description: This experiment does this and that instrument1: interface: actuator1 parameter1: value1 parameter2: value2 instrument2: interface: sensor1 parameter: value connections: - instrument3 instrument3: parameter: value ``` ## Purpose This file should contain most of the *parameters* required by an experiment, be it to do it for the first time or to replicate or share it. This includes the parameters of the instruments used on the bench, how they are connected together, and any other information useful for the experimenter (description, expected results, manual configurations, *etc*.). ## Syntax It is a YAML file, containing a top level mapping. During the initialization of an {py:class}`~phileas.factory.ExperimentFactory`, it is parsed to an {py:class}`~phileas.iteration.IterationTree` which is then stored in its {py:attr}`~phileas.factory.ExperimentFactory.experiment_config` attribute. In order to ease the representation of the different iteration methods and leaves, custom YAML types are supported. Whereas YAML only supports lists, mapping, integer, floats, booleans, strings and none values, this file can also represent Phileas objects. They are identified by a `!` prefix. For example, a sequence of values can be represented by `!sequence [1, 2, 3]`. :::{seealso} See [](#custom-yaml-types) for more details about the supported types. ::: ## Instrument entries Each of the mapping entries with an `interface` key represents an experiment instrument. Its value is matched with the {py:attr}`~phileas.factory.Loader.interfaces` values of bench instruments' loaders. Think of it this way: - in a bench configuration file, the set of available instruments is listed; - in the experiment configuration file, you list the requirements of an instrument (*eg*. it must be an oscilloscope); - Phileas takes care of providing you with a suitable instrument. The other non-reserved entries of the instrument represent an {py:class}`~phileas.iteration.IterationTree` which contains the set of its configurations. You can use the methods {py:meth}`~phileas.factory.ExperimentFactory.configure_instrument` and {py:meth}`~phileas.factory.ExperimentFactory.configure_experiment` of {py:class}`~phileas.factory.ExperimentFactory` to apply them. Internally, they use {py:meth}`phileas.factory.Loader.configure`. ## Bench instrument filtering Multiple bench instruments can have the same interface. For example, there might be multiple motors or lights on the same bench. In this case, they would all expose the same interfaces, which would prevent Phileas from knowing which instrument to provide you. This issue can be solved by the `filter` entry. It contains a set of attributes and values to be matched against the bench instruments attributes. Consider the following bench file: ```{code-block} yaml :caption: `bench.yaml` with instruments exposing the same interfaces light-motor: loader: brandXX-modelXX moves: light probe-motor: loader: brandYY-modelYY moves: probe ``` Let us say that only the probe motor is used, while the light remains stationary. Both loaders `brandXX-modelXX` and `brandYY-modelYY` expose the same `motor` interface. Using filters, this experiment configuration file selects only `probe-motor`: ```{code-block} yaml :caption: `experiment.yaml` using filters to chose a bench instrument probe-position: interface: motor filter: moves: probe x: 0.5 y: 1.2 z: 0 ``` `probe-position` is matched to `probe-motor`, because it is the only suitable bench instrument with the attribute `moves = probe`. ## Connections An experiment is represented by its instruments, how they are configured, but also how they interact together. An experiment configuration file can represent those interactions with so-called *connections*, which are the edges in the global interaction graph of the experiment, where instruments are nodes. The optional top-level `connections` entry contains the list of the interaction graph edges. Each edge contains: - a mandatory origin, stored in the `from` entry, - a mandatory destination, stored in the `to` entry, - and, optionally, arbitrary data stored in its `attributes` entry. The origin and destination of an edge are usually instruments. You can suffix them with *ports*, separating them with dots. For example, `oscilloscope.chA` states that the connections involves the A channel of the oscilloscope. ```{code-block} yaml :caption: Global connections definition connections: - from: probe to: oscilloscope.chA - from: holder to: probe attributes: holds ``` In this example, the probe is connected to the channel A of the oscilloscope, and is hold by `holder`. However, it is not always convenient to define the experiment graph in a distinct entry, separating it from the instruments. In these cases, you can define interactions directly in the entry of an instrument. The instrument entry `connections` can be used similarly as the top-level entry. However, you can use a simplified notation: if the origin or destination of an edge is the instrument itself, you can simply omit the instrument name, starting directly at its first port. ```{code-block} yaml :caption: Instrument connections definition temperature-measurement: interface: oscilloscope connections: - from: chA to: temperature-probe ``` This examples states that channel A of the oscilloscope is connected to the temperature probe. This experiment graph is built by Phileas, but it does not use it. You can access it through {py:meth}`~phileas.factory.ExperimentFactory.get_experiment_graph`. ## Documentation Phileas ignores all the top-level entries which are not mappings with key `interface`. It is advised to document the experiment with them. For example, you can add a `description` entry, which gives a unique `name` to the experiment, describes the tested `hypothesis`, its `measurements` and the `expected-results`. This whole file is parsed and made available in the factory {py:attr}`~phileas.factory.ExperimentFactory.experiment_config`. You can then retrieve that information in simple nested Python `dict` and `list` by using {py:meth}`~phileas.iteration.IterationTree.to_pseudo_data_tree`. ## Custom YAML types The experiment configuration file is parsed to an {py:class}`~phileas.iteration.IterationTree`, which is built using the different Python classes defined in {py:mod}`~phileas.iteration`. Custom YAML data types can be used to represent these. They are identified by a `!` prefix. ### Cartesian product By default, a YAML mapping or sequence is parsed as a cartesian product: ```{code-cell} ipython3 :tags: [hide-input] content = """ parameter1: !sequence [1, 2, 3] parameter2: !sequence [a, b, c] """ tree = load_iteration_tree_from_yaml_file(content) print(f"{content}\n\t\t|\n\t\tv\n") pprint(tree) ``` You can also directly invoke `!product`, which allows to set the node parameters, prefixed by an underscore: ```{code-cell} ipython3 :tags: [hide-input] content = """ !product _lazy: true _snake: true parameter1: !sequence [1, 2, 3] parameter2: !sequence [a, b, c] """ tree = load_iteration_tree_from_yaml_file(content) print(f"{content}\n\t\t|\n\t\tv\n") pprint(tree) ``` ### Other iteration methods Similarly, you can use `!union`, `!pick` and `!configurations` to build {py:class}`~phileas.iteration.Union`, {py:class}`~phileas.iteration.Pick` and {py:class}`~phileas.iteration.Configurations`. ### Unary nodes `!shuffle` builds {py:class}`~phileas.iteration.Shuffle` node, and requires its only child to be stored in the `child` key: ```{code-cell} ipython3 :tags: [hide-input] content = """ !shuffle child: !sequence [1, 2, 3] """ tree = load_iteration_tree_from_yaml_file(content) print(f"{content}\n\t\t|\n\t\tv\n") pprint(tree) ``` For now, transform nodes are not supported. You have to add them to the iteration tree manually, from a Python script. ### Iteration leaves `!sequence` builds {py:class}`~phileas.iteration.Sequence` nodes. It can be used in the short form `!sequence [a, b, c]`, or its long form ```{code-block} yaml !sequence elements: [a, b, c] default: z ``` `!range` is often used, as it represents different kinds of numeric ranges. It expects a `start` and `end` parameters, and either - `steps`, the total number of points in the range, or - `resolution`, the maximum distance between two points in the range. `!random` represent random distribution, and is parsed to {py:class}`~phileas.iteration.NumpyRNG`. It expects a `distribution` argument, which is the string name of a numpy distribution. You can find them in {py:class}`numpy.random.Generator`. Its parameters are given in the `parameters` field, as a mapping. You can additionally give it a finite `size`, and a `default` value. If you want to generate arbitrarily big integers, you can't use `!random`, as Numpy does not support generating them. Instead, you can use `!random_uniform_bigint` to generate uniformly sampled integers. It takes two mandatory arguments `low` and `high`, which are the inclusive bounds of the generated interval. You can generate prime numbers with `!random_prime`, which has the same arguments as `!random_uniform_bigint`. ## Template generation A bench template file can be generated automatically by Phileas. See `python -m phileas generate experiment -h` for the available options.