forked from M-Labs/artiq
318 lines
11 KiB
Python
318 lines
11 KiB
Python
from collections import OrderedDict
|
|
from inspect import isclass
|
|
|
|
|
|
__all__ = ["NoDefault",
|
|
"FreeValue", "BooleanValue", "EnumerationValue",
|
|
"NumberValue", "StringValue",
|
|
"HasEnvironment",
|
|
"Experiment", "EnvExperiment", "is_experiment"]
|
|
|
|
|
|
class NoDefault:
|
|
"""Represents the absence of a default value."""
|
|
pass
|
|
|
|
|
|
class DefaultMissing(Exception):
|
|
"""Raised by the ``default`` method of argument processors when no default
|
|
value is available."""
|
|
pass
|
|
|
|
|
|
class _SimpleArgProcessor:
|
|
def __init__(self, default=NoDefault):
|
|
if default is not NoDefault:
|
|
self.default_value = default
|
|
|
|
def default(self):
|
|
if not hasattr(self, "default_value"):
|
|
raise DefaultMissing
|
|
return self.default_value
|
|
|
|
def process(self, x):
|
|
return x
|
|
|
|
def describe(self):
|
|
d = {"ty": self.__class__.__name__}
|
|
if hasattr(self, "default_value"):
|
|
d["default"] = self.default_value
|
|
return d
|
|
|
|
|
|
class FreeValue(_SimpleArgProcessor):
|
|
"""An argument that can be an arbitrary Python value."""
|
|
pass
|
|
|
|
|
|
class BooleanValue(_SimpleArgProcessor):
|
|
"""A boolean argument."""
|
|
pass
|
|
|
|
|
|
class EnumerationValue(_SimpleArgProcessor):
|
|
"""An argument that can take a string value among a predefined set of
|
|
values.
|
|
|
|
:param choices: A list of string representing the possible values of the
|
|
argument.
|
|
"""
|
|
def __init__(self, choices, default=NoDefault):
|
|
_SimpleArgProcessor.__init__(self, default)
|
|
assert default is NoDefault or default in choices
|
|
self.choices = choices
|
|
|
|
def describe(self):
|
|
d = _SimpleArgProcessor.describe(self)
|
|
d["choices"] = self.choices
|
|
return d
|
|
|
|
|
|
class NumberValue(_SimpleArgProcessor):
|
|
"""An argument that can take a numerical value (typically floating point).
|
|
|
|
:param unit: A string representing the unit of the value, for user
|
|
interface (UI) purposes.
|
|
:param scale: The scale of value for UI purposes. The displayed value is
|
|
divided by the scale.
|
|
:param step: The step with which the value should be modified by up/down
|
|
buttons in a UI. The default is the scale divided by 10.
|
|
:param min: The minimum value of the argument.
|
|
:param max: The maximum value of the argument.
|
|
:param ndecimals: The number of decimals a UI should use.
|
|
"""
|
|
def __init__(self, default=NoDefault, unit="", scale=1.0,
|
|
step=None, min=None, max=None, ndecimals=2):
|
|
if step is None:
|
|
step = scale/10.0
|
|
_SimpleArgProcessor.__init__(self, default)
|
|
self.unit = unit
|
|
self.scale = scale
|
|
self.step = step
|
|
self.min = min
|
|
self.max = max
|
|
self.ndecimals = ndecimals
|
|
|
|
def describe(self):
|
|
d = _SimpleArgProcessor.describe(self)
|
|
d["unit"] = self.unit
|
|
d["scale"] = self.scale
|
|
d["step"] = self.step
|
|
d["min"] = self.min
|
|
d["max"] = self.max
|
|
d["ndecimals"] = self.ndecimals
|
|
return d
|
|
|
|
|
|
class StringValue(_SimpleArgProcessor):
|
|
"""A string argument."""
|
|
pass
|
|
|
|
|
|
class HasEnvironment:
|
|
"""Provides methods to manage the environment of an experiment (devices,
|
|
parameters, results, arguments)."""
|
|
def __init__(self, device_mgr=None, dataset_mgr=None, *, parent=None,
|
|
default_arg_none=False, **kwargs):
|
|
self.requested_args = OrderedDict()
|
|
|
|
self.__device_mgr = device_mgr
|
|
self.__dataset_mgr = dataset_mgr
|
|
self.__parent = parent
|
|
self.__default_arg_none = default_arg_none
|
|
|
|
self.__kwargs = kwargs
|
|
self.__in_build = True
|
|
self.build()
|
|
self.__in_build = False
|
|
for key in self.__kwargs.keys():
|
|
if key not in self.requested_args:
|
|
raise TypeError("Got unexpected argument: " + key)
|
|
del self.__kwargs
|
|
|
|
def build(self):
|
|
"""Must be implemented by the user to request arguments.
|
|
|
|
Other initialization steps such as requesting devices and parameters
|
|
or initializing real-time results may also be performed here.
|
|
|
|
When the repository is scanned, any requested devices and parameters
|
|
are set to ``None``."""
|
|
raise NotImplementedError
|
|
|
|
def managers(self):
|
|
"""Returns the device manager and the dataset manager, in this order.
|
|
|
|
This is the same order that the constructor takes them, allowing
|
|
sub-objects to be created with this idiom to pass the environment
|
|
around: ::
|
|
|
|
sub_object = SomeLibrary(*self.managers())
|
|
"""
|
|
return self.__device_mgr, self.__dataset_mgr
|
|
|
|
def get_argument(self, key, processor=None, group=None):
|
|
"""Retrieves and returns the value of an argument.
|
|
|
|
:param key: Name of the argument.
|
|
:param processor: A description of how to process the argument, such
|
|
as instances of ``BooleanValue`` and ``NumberValue``.
|
|
:param group: An optional string that defines what group the argument
|
|
belongs to, for user interface purposes.
|
|
"""
|
|
if not self.__in_build:
|
|
raise TypeError("get_argument() should only "
|
|
"be called from build()")
|
|
if self.__parent is not None and key not in self.__kwargs:
|
|
return self.__parent.get_argument(key, processor, group)
|
|
if processor is None:
|
|
processor = FreeValue()
|
|
self.requested_args[key] = processor, group
|
|
try:
|
|
argval = self.__kwargs[key]
|
|
except KeyError:
|
|
try:
|
|
return processor.default()
|
|
except DefaultMissing:
|
|
if self.__default_arg_none:
|
|
return None
|
|
else:
|
|
raise
|
|
return processor.process(argval)
|
|
|
|
def setattr_argument(self, key, processor=None, group=None):
|
|
"""Sets an argument as attribute. The names of the argument and of the
|
|
attribute are the same."""
|
|
setattr(self, key, self.get_argument(key, processor, group))
|
|
|
|
def get_device_db(self):
|
|
"""Returns the full contents of the device database."""
|
|
if self.__parent is not None:
|
|
return self.__parent.get_device_db()
|
|
return self.__device_mgr.get_device_db()
|
|
|
|
def get_device(self, key):
|
|
"""Creates and returns a device driver."""
|
|
if self.__parent is not None:
|
|
return self.__parent.get_device(key)
|
|
if self.__device_mgr is None:
|
|
raise ValueError("Device manager not present")
|
|
return self.__device_mgr.get(key)
|
|
|
|
def setattr_device(self, key):
|
|
"""Sets a device driver as attribute. The names of the device driver
|
|
and of the attribute are the same."""
|
|
setattr(self, key, self.get_device(key))
|
|
|
|
def set_dataset(self, key, value,
|
|
broadcast=False, persist=False, save=True):
|
|
"""Sets the contents and handling modes of a dataset.
|
|
|
|
If the dataset is broadcasted, it must be PYON-serializable.
|
|
If the dataset is saved, it must be a scalar (``bool``, ``int``,
|
|
``float`` or NumPy scalar) or a NumPy array.
|
|
|
|
:param broadcast: the data is sent in real-time to the master, which
|
|
dispatches it. Returns a Notifier that can be used to mutate the
|
|
dataset.
|
|
:param persist: the master should store the data on-disk. Implies
|
|
broadcast.
|
|
:param save: the data is saved into the local storage of the current
|
|
run (archived as a HDF5 file).
|
|
"""
|
|
if self.__parent is not None:
|
|
self.__parent.set_dataset(key, value, broadcast, persist, save)
|
|
return
|
|
if self.__dataset_mgr is None:
|
|
raise ValueError("Dataset manager not present")
|
|
return self.__dataset_mgr.set(key, value, broadcast, persist, save)
|
|
|
|
def get_dataset(self, key, default=NoDefault):
|
|
"""Returns the contents of a dataset.
|
|
|
|
The local storage is searched first, followed by the master storage
|
|
(which contains the broadcasted datasets from all experiments) if the
|
|
key was not found initially.
|
|
|
|
If the dataset does not exist, returns the default value. If no default
|
|
is provided, raises ``KeyError``.
|
|
"""
|
|
if self.__parent is not None:
|
|
return self.__parent.get_dataset(key, default)
|
|
if self.__dataset_mgr is None:
|
|
raise ValueError("Dataset manager not present")
|
|
try:
|
|
return self.__dataset_mgr.get(key)
|
|
except KeyError:
|
|
if default is NoDefault:
|
|
raise
|
|
else:
|
|
return default
|
|
|
|
def setattr_dataset(self, key, default=NoDefault):
|
|
"""Sets the contents of a dataset as attribute. The names of the
|
|
dataset and of the attribute are the same."""
|
|
setattr(self, key, self.get_dataset(key, default))
|
|
|
|
|
|
class Experiment:
|
|
"""Base class for experiments.
|
|
|
|
Deriving from this class enables automatic experiment discovery in
|
|
Python modules.
|
|
"""
|
|
def prepare(self):
|
|
"""Entry point for pre-computing data necessary for running the
|
|
experiment.
|
|
|
|
Doing such computations outside of ``run`` enables more efficient
|
|
scheduling of multiple experiments that need to access the shared
|
|
hardware during part of their execution.
|
|
|
|
This method must not interact with the hardware.
|
|
"""
|
|
pass
|
|
|
|
def run(self):
|
|
"""The main entry point of the experiment.
|
|
|
|
This method must be overloaded by the user to implement the main
|
|
control flow of the experiment.
|
|
|
|
This method may interact with the hardware.
|
|
|
|
The experiment may call the scheduler's ``pause`` method while in
|
|
``run``.
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
def analyze(self):
|
|
"""Entry point for analyzing the results of the experiment.
|
|
|
|
This method may be overloaded by the user to implement the analysis
|
|
phase of the experiment, for example fitting curves.
|
|
|
|
Splitting this phase from ``run`` enables tweaking the analysis
|
|
algorithm on pre-existing data, and CPU-bound analyses to be run
|
|
overlapped with the next experiment in a pipelined manner.
|
|
|
|
This method must not interact with the hardware.
|
|
"""
|
|
pass
|
|
|
|
|
|
class EnvExperiment(Experiment, HasEnvironment):
|
|
"""Base class for experiments that use the ``HasEnvironment`` environment
|
|
manager.
|
|
|
|
Most experiment should derive from this class."""
|
|
pass
|
|
|
|
|
|
def is_experiment(o):
|
|
"""Checks if a Python object is an instantiable user experiment."""
|
|
return (isclass(o)
|
|
and issubclass(o, Experiment)
|
|
and o is not Experiment
|
|
and o is not EnvExperiment)
|