From f2e3dfb8481467718ac031ef3c63ffba95ea5f97 Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Sun, 8 Mar 2015 15:43:04 +0100 Subject: [PATCH] Experiment base class, replace __artiq_unit__ with docstring --- artiq/__init__.py | 1 + artiq/frontend/artiq_client.py | 18 +++--- artiq/frontend/artiq_run.py | 55 ++++++++++--------- artiq/gui/explorer.py | 2 +- artiq/gui/scheduler.py | 8 +-- artiq/language/experiment.py | 40 ++++++++++++++ artiq/master/repository.py | 14 +++-- artiq/master/worker_impl.py | 37 ++++++------- benchmarks/all.py | 11 ++-- benchmarks/pulse_rate.py | 4 +- benchmarks/rpc_timing.py | 4 +- benchmarks/rtio_skew.py | 4 +- doc/manual/getting_started.rst | 18 ++---- examples/master/pdb.pyon | 2 +- examples/master/repository/dds_test.py | 4 +- .../repository/flopping_f_simulation.py | 4 +- examples/master/repository/mandelbrot.py | 4 +- .../master/repository/photon_histogram.py | 4 +- examples/master/repository/transport.py | 4 +- examples/sim/al_spectroscopy.py | 4 +- examples/sim/simple_simulation.py | 4 +- 21 files changed, 144 insertions(+), 102 deletions(-) create mode 100644 artiq/language/experiment.py diff --git a/artiq/__init__.py b/artiq/__init__.py index 38b84d574..d1a05b941 100644 --- a/artiq/__init__.py +++ b/artiq/__init__.py @@ -1,4 +1,5 @@ from artiq.language.core import * +from artiq.language.experiment import Experiment from artiq.language.db import * from artiq.language.units import check_unit from artiq.language.units import ps, ns, us, ms, s diff --git a/artiq/frontend/artiq_client.py b/artiq/frontend/artiq_client.py index dadcae572..dad25112a 100755 --- a/artiq/frontend/artiq_client.py +++ b/artiq/frontend/artiq_client.py @@ -40,12 +40,13 @@ def get_argparser(): parser_add.add_argument( "-t", "--timeout", default=None, type=float, help="specify a timeout for the experiment to complete") - parser_add.add_argument("-u", "--unit", default=None, - help="unit to run") + parser_add.add_argument("-e", "--experiment", default=None, + help="experiment to run") parser_add.add_argument("--rtr-group", default=None, type=str, help="real-time result group " "(defaults to filename)") - parser_add.add_argument("file", help="file containing the unit to run") + parser_add.add_argument("file", + help="file containing the experiment to run") parser_add.add_argument("arguments", nargs="*", help="run arguments") @@ -103,7 +104,7 @@ def _action_submit(remote, args): run_params = { "file": args.file, - "unit": args.unit, + "experiment": args.experiment, "timeout": args.timeout, "arguments": arguments, "rtr_group": args.rtr_group if args.rtr_group is not None \ @@ -147,10 +148,11 @@ def _action_del_parameter(remote, args): def _show_queue(queue): clear_screen() if queue: - table = PrettyTable(["RID", "File", "Unit", "Timeout", "Arguments"]) + table = PrettyTable(["RID", "File", "Experiment", "Timeout", + "Arguments"]) for rid, run_params in queue: row = [rid, run_params["file"]] - for x in run_params["unit"], run_params["timeout"]: + for x in run_params["experiment"], run_params["timeout"]: row.append("" if x is None else x) row.append(format_arguments(run_params["arguments"])) table.add_row(row) @@ -162,13 +164,13 @@ def _show_queue(queue): def _show_timed(timed): clear_screen() if timed: - table = PrettyTable(["Next run", "TRID", "File", "Unit", + table = PrettyTable(["Next run", "TRID", "File", "Experiment", "Timeout", "Arguments"]) sp = sorted(timed.items(), key=lambda x: (x[1][0], x[0])) for trid, (next_run, run_params) in sp: row = [time.strftime("%m/%d %H:%M:%S", time.localtime(next_run)), trid, run_params["file"]] - for x in run_params["unit"], run_params["timeout"]: + for x in run_params["experiment"], run_params["timeout"]: row.append("" if x is None else x) row.append(format_arguments(run_params["arguments"])) table.add_row(row) diff --git a/artiq/frontend/artiq_run.py b/artiq/frontend/artiq_run.py index 2fe01b5e7..10f2375c1 100755 --- a/artiq/frontend/artiq_run.py +++ b/artiq/frontend/artiq_run.py @@ -3,13 +3,13 @@ import argparse import sys import time -from inspect import isclass from operator import itemgetter from itertools import chain import h5py from artiq.language.db import * +from artiq.language.experiment import is_experiment from artiq.protocols import pyon from artiq.protocols.file_db import FlatFileDB from artiq.master.worker_db import DBHub, ResultDB @@ -68,15 +68,15 @@ def get_argparser(): parser.add_argument("-p", "--pdb", default="pdb.pyon", help="parameter database file") - parser.add_argument("-e", "--elf", default=False, action="store_true", + parser.add_argument("-E", "--elf", default=False, action="store_true", help="run ELF binary") - parser.add_argument("-u", "--unit", default=None, - help="unit to run") + parser.add_argument("-e", "--experiment", default=None, + help="experiment to run") parser.add_argument("-o", "--hdf5", default=None, help="write results to specified HDF5 file" " (default: print them)") parser.add_argument("file", - help="file containing the unit to run") + help="file containing the experiment to run") parser.add_argument("arguments", nargs="*", help="run arguments") @@ -105,27 +105,31 @@ def main(): if args.arguments: print("Run arguments are not supported in ELF mode") sys.exit(1) - unit_inst = ELFRunner(dps) - unit_inst.run(args.file) + exp_inst = ELFRunner(dps) + exp_inst.run(args.file) else: module = file_import(args.file) - if args.unit is None: - units = [(k, v) for k, v in module.__dict__.items() - if isclass(v) and hasattr(v, "__artiq_unit__")] - l = len(units) + if args.experiment is None: + exps = [(k, v) for k, v in module.__dict__.items() + if is_experiment(v)] + l = len(exps) if l == 0: - print("No units found in module") + print("No experiments found in module") sys.exit(1) elif l > 1: - print("More than one unit found in module:") - for k, v in sorted(units, key=itemgetter(0)): - print(" {} ({})".format(k, v.__artiq_unit__)) - print("Use -u to specify which unit to use.") + print("More than one experiment found in module:") + for k, v in sorted(experiments, key=itemgetter(0)): + if v.__doc__ is None: + print(" {}".format(k)) + else: + print(" {} ({})".format( + k, v.__doc__.splitlines()[0].strip())) + print("Use -u to specify which experiment to use.") sys.exit(1) else: - unit = units[0][1] + exp = exps[0][1] else: - unit = getattr(module, args.unit) + exp = getattr(module, args.experiment) try: arguments = _parse_arguments(args.arguments) @@ -135,17 +139,16 @@ def main(): run_params = { "file": args.file, - "unit": args.unit, + "experiment": args.experiment, "timeout": None, "arguments": arguments } - unit_inst = unit(dbh, - scheduler=DummyScheduler(), - run_params=run_params, - **run_params["arguments"]) - unit_inst.run() - if hasattr(unit_inst, "analyze"): - unit_inst.analyze() + exp_inst = exp(dbh, + scheduler=DummyScheduler(), + run_params=run_params, + **run_params["arguments"]) + exp_inst.run() + exp_inst.analyze() if args.hdf5 is not None: f = h5py.File(args.hdf5, "w") diff --git a/artiq/gui/explorer.py b/artiq/gui/explorer.py index 95797d196..fb829bdbf 100644 --- a/artiq/gui/explorer.py +++ b/artiq/gui/explorer.py @@ -146,7 +146,7 @@ class ExplorerWindow(Window): arguments = self.controls.get_arguments() run_params = { "file": data["file"], - "unit": data["unit"], + "experiment": data["experiment"], "timeout": None, "arguments": arguments, "rtr_group": data["file"] diff --git a/artiq/gui/scheduler.py b/artiq/gui/scheduler.py index 14755000b..00e177b5f 100644 --- a/artiq/gui/scheduler.py +++ b/artiq/gui/scheduler.py @@ -12,7 +12,7 @@ class _QueueStoreSyncer(ListSyncer): def convert(self, x): rid, run_params = x row = [rid, run_params["file"]] - for e in run_params["unit"], run_params["timeout"]: + for e in run_params["experiment"], run_params["timeout"]: row.append("" if e is None else str(e)) row.append(format_arguments(run_params["arguments"])) return row @@ -27,7 +27,7 @@ class _TimedStoreSyncer(DictSyncer): next_run, run_params = x row = [time.strftime("%m/%d %H:%M:%S", time.localtime(next_run)), trid, run_params["file"]] - for e in run_params["unit"], run_params["timeout"]: + for e in run_params["experiment"], run_params["timeout"]: row.append("" if e is None else str(e)) row.append(format_arguments(run_params["arguments"])) return row @@ -57,7 +57,7 @@ class SchedulerWindow(Window): self.queue_store = Gtk.ListStore(int, str, str, str, str) self.queue_tree = Gtk.TreeView(self.queue_store) - for i, title in enumerate(["RID", "File", "Unit", "Timeout", "Arguments"]): + for i, title in enumerate(["RID", "File", "Experiment", "Timeout", "Arguments"]): renderer = Gtk.CellRendererText() column = Gtk.TreeViewColumn(title, renderer, text=i) self.queue_tree.append_column(column) @@ -81,7 +81,7 @@ class SchedulerWindow(Window): self.timed_store = Gtk.ListStore(str, int, str, str, str, str) self.timed_tree = Gtk.TreeView(self.timed_store) - for i, title in enumerate(["Next run", "TRID", "File", "Unit", + for i, title in enumerate(["Next run", "TRID", "File", "Experiment", "Timeout", "Arguments"]): renderer = Gtk.CellRendererText() column = Gtk.TreeViewColumn(title, renderer, text=i) diff --git a/artiq/language/experiment.py b/artiq/language/experiment.py new file mode 100644 index 000000000..3473dcba3 --- /dev/null +++ b/artiq/language/experiment.py @@ -0,0 +1,40 @@ +from inspect import isclass + + +class Experiment: + """Base class for experiments. + + Deriving from this class enables automatic experiment discovery in + Python modules. + """ + 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. + """ + 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 + + +def has_analyze(experiment): + """Checks if an experiment instance overloaded its ``analyze`` method.""" + return experiment.analyze.__func__ is not Experiment.analyze + + +def is_experiment(o): + """Checks if a Python object is an instantiable experiment.""" + return isclass(o) and issubclass(o, Experiment) and o is not Experiment diff --git a/artiq/master/repository.py b/artiq/master/repository.py index c465a5d27..90ef186e9 100644 --- a/artiq/master/repository.py +++ b/artiq/master/repository.py @@ -1,8 +1,8 @@ import os -from inspect import isclass from artiq.protocols.sync_struct import Notifier from artiq.tools import file_import +from artiq.language.experiment import is_experiment def scan_experiments(): @@ -14,13 +14,19 @@ def scan_experiments(): except: continue for k, v in m.__dict__.items(): - if isclass(v) and hasattr(v, "__artiq_unit__"): + if is_experiment(v): + if v.__doc__ is None: + name = k + else: + name = v.__doc__.splitlines()[0].strip() + if name[-1] == ".": + name = name[:-1] entry = { "file": os.path.join("repository", f), - "unit": k, + "experiment": k, "gui_file": getattr(v, "__artiq_gui_file__", None) } - r[v.__artiq_unit__] = entry + r[name] = entry return r diff --git a/artiq/master/worker_impl.py b/artiq/master/worker_impl.py index a251ec9f0..5f8ef4891 100644 --- a/artiq/master/worker_impl.py +++ b/artiq/master/worker_impl.py @@ -1,6 +1,5 @@ import sys import time -from inspect import isclass import traceback from artiq.protocols import pyon @@ -65,23 +64,24 @@ class Scheduler: cancel_timed = make_parent_action("scheduler_cancel_timed", "trid") -def get_unit(file, unit): +def get_exp(file, exp): module = file_import(file) - if unit is None: - units = [v for k, v in module.__dict__.items() - if isclass(v) and hasattr(v, "__artiq_unit__")] - if len(units) != 1: - raise ValueError("Found {} units in module".format(len(units))) - return units[0] + if exp is None: + exps = [v for k, v in module.__dict__.items() + if is_experiment(v)] + if len(exps) != 1: + raise ValueError("Found {} experiments in module" + .format(len(exps))) + return exps[0] else: - return getattr(module, unit) + return getattr(module, exp) def run(rid, run_params): start_time = time.localtime() - unit = get_unit(run_params["file"], run_params["unit"]) + exp = get_exp(run_params["file"], run_params["experiment"]) - realtime_results = unit.realtime_results() + realtime_results = exp.realtime_results() init_rt_results(realtime_results) realtime_results_set = set() @@ -97,13 +97,12 @@ def run(rid, run_params): dbh = DBHub(ParentDDB, ParentPDB, rdb) try: try: - unit_inst = unit(dbh, - scheduler=Scheduler, - run_params=run_params, - **run_params["arguments"]) - unit_inst.run() - if hasattr(unit_inst, "analyze"): - unit_inst.analyze() + exp_inst = exp(dbh, + scheduler=Scheduler, + run_params=run_params, + **run_params["arguments"]) + exp_inst.run() + exp_inst.analyze() except Exception: put_object({"action": "report_completed", "status": "failed", @@ -114,7 +113,7 @@ def run(rid, run_params): finally: dbh.close() - f = get_hdf5_output(start_time, rid, unit.__name__) + f = get_hdf5_output(start_time, rid, exp.__name__) try: rdb.write_hdf5(f) finally: diff --git a/benchmarks/all.py b/benchmarks/all.py index 47be6f924..d0a34263b 100644 --- a/benchmarks/all.py +++ b/benchmarks/all.py @@ -2,15 +2,16 @@ from artiq import * import pulse_rate, rtio_skew, rpc_timing -_units = [pulse_rate.PulseRate, rtio_skew.RTIOSkew, rpc_timing.RPCTiming] -class AllBenchmarks(AutoDB): - __artiq_unit__ = "All benchmarks" +_exps = [pulse_rate.PulseRate, rtio_skew.RTIOSkew, rpc_timing.RPCTiming] + +class AllBenchmarks(Experiment, AutoDB): + """All benchmarks""" def build(self): self.se = [] - for unit in _units: - self.se.append(unit(self.dbh)) + for exp in _exps: + self.se.append(exp(self.dbh)) def run(self): for se in self.se: diff --git a/benchmarks/pulse_rate.py b/benchmarks/pulse_rate.py index da0d3f34c..912cbe592 100644 --- a/benchmarks/pulse_rate.py +++ b/benchmarks/pulse_rate.py @@ -2,8 +2,8 @@ from artiq import * from artiq.coredevice.runtime_exceptions import RTIOUnderflow -class PulseRate(AutoDB): - __artiq_unit__ = "Pulse rate" +class PulseRate(Experiment, AutoDB): + """Sustained pulse rate""" class DBKeys: core = Device() diff --git a/benchmarks/rpc_timing.py b/benchmarks/rpc_timing.py index 483085534..6f98b9e84 100644 --- a/benchmarks/rpc_timing.py +++ b/benchmarks/rpc_timing.py @@ -3,8 +3,8 @@ from math import sqrt from artiq import * -class RPCTiming(AutoDB): - __artiq_unit__ = "RPC timing" +class RPCTiming(Experiment, AutoDB): + """RPC timing""" class DBKeys: core = Device() diff --git a/benchmarks/rtio_skew.py b/benchmarks/rtio_skew.py index fc86c4d24..04bd6c4e0 100644 --- a/benchmarks/rtio_skew.py +++ b/benchmarks/rtio_skew.py @@ -5,8 +5,8 @@ class PulseNotReceived(Exception): pass -class RTIOSkew(AutoDB): - __artiq_unit__ = "RTIO skew" +class RTIOSkew(Experiment, AutoDB): + """RTIO skew""" class DBKeys: core = Device() diff --git a/doc/manual/getting_started.rst b/doc/manual/getting_started.rst index 93c907011..12521c7ff 100644 --- a/doc/manual/getting_started.rst +++ b/doc/manual/getting_started.rst @@ -9,9 +9,7 @@ As a very first step, we will turn on a LED on the core device. Create a file `` from artiq import * - class LED(AutoDB): - __artiq_unit__ = "ARTIQ tutorial" - + class LED(Experiment, AutoDB): class DBKeys: core = Device() led = Device() @@ -23,8 +21,6 @@ As a very first step, we will turn on a LED on the core device. Create a file `` The central part of our code is our ``LED`` class, that derives from :class:`artiq.language.db.AutoDB`. ``AutoDB`` is part of the mechanism that attaches device drivers and retrieves parameters according to a database. Our ``DBKeys`` class lists the devices (and parameters) that ``LED`` needs in order to operate, and the names of the attributes (e.g. ``led``) are used to search the database. ``AutoDB`` replaces them with the actual device drivers (and parameter values). Finally, the ``@kernel`` decorator tells the system that the ``run`` method must be executed on the core device (instead of the host). -The ``__artiq_unit__`` attribute tells the ARTIQ tools that our class is a "unit" (an entry point for an experiment) and gives it a name. The name can be any string, and its purpose is to name the experiment in user interfaces. - Copy the files ``ddb.pyon`` and ``pdb.pyon`` (containing the device and parameter databases) from the ``examples`` folder of ARTIQ into the same directory as ``led.py`` (alternatively, you can use the ``-d`` and ``-p`` options of ``artiq_run.py``). You can open the database files using a text editor - their contents are in a human-readable format. Run your code using ``artiq_run.py``, which is part of the ARTIQ front-end tools: :: @@ -43,9 +39,7 @@ Modify the code as follows: :: def input_led_state(): return int(input("Enter desired LED state: ")) - class LED(AutoDB): - __artiq_unit__ = "ARTIQ tutorial" - + class LED(Experiment, AutoDB): class DBKeys: core = Device() led = Device() @@ -89,9 +83,7 @@ Create a new file ``rtio.py`` containing the following: :: from artiq import * - class Tutorial(AutoDB): - __artiq_unit__ = "ARTIQ tutorial" - + class Tutorial(Experiment, AutoDB): class DBKeys: core = Device() ttl0 = Device() @@ -114,9 +106,7 @@ Try reducing the period of the generated waveform until the CPU cannot keep up w def print_underflow(): print("RTIO underflow occured") - class Tutorial(AutoDB): - __artiq_unit__ = "ARTIQ tutorial" - + class Tutorial(Experiment, AutoDB): class DBKeys: core = Device() led = Device() diff --git a/examples/master/pdb.pyon b/examples/master/pdb.pyon index 4ca76a468..27f1e7665 100644 --- a/examples/master/pdb.pyon +++ b/examples/master/pdb.pyon @@ -1 +1 @@ -{"flopping_freq": 1500.0294421161527} +{"flopping_freq": 1500.0164816344934} diff --git a/examples/master/repository/dds_test.py b/examples/master/repository/dds_test.py index de342e5ea..7a18d6620 100644 --- a/examples/master/repository/dds_test.py +++ b/examples/master/repository/dds_test.py @@ -1,8 +1,8 @@ from artiq import * -class DDSTest(AutoDB): - __artiq_unit__ = "DDS test" +class DDSTest(Experiment, AutoDB): + """DDS test""" class DBKeys: core = Device() diff --git a/examples/master/repository/flopping_f_simulation.py b/examples/master/repository/flopping_f_simulation.py index b16518799..c2624695b 100644 --- a/examples/master/repository/flopping_f_simulation.py +++ b/examples/master/repository/flopping_f_simulation.py @@ -23,8 +23,8 @@ def model_numpy(xdata, F0): return r -class FloppingF(AutoDB): - __artiq_unit__ = "Flopping F simulation" +class FloppingF(Experiment, AutoDB): + """Flopping F simulation""" __artiq_gui_file__ = "flopping_f_simulation_gui.py" class DBKeys: diff --git a/examples/master/repository/mandelbrot.py b/examples/master/repository/mandelbrot.py index d3eae965e..0a0c030a6 100644 --- a/examples/master/repository/mandelbrot.py +++ b/examples/master/repository/mandelbrot.py @@ -3,8 +3,8 @@ import sys from artiq import * -class Mandelbrot(AutoDB): - __artiq_unit__ = "Mandelbrot set demo" +class Mandelbrot(Experiment, AutoDB): + """Mandelbrot set demo""" class DBKeys: core = Device() diff --git a/examples/master/repository/photon_histogram.py b/examples/master/repository/photon_histogram.py index 1e8379243..3bebef1bf 100644 --- a/examples/master/repository/photon_histogram.py +++ b/examples/master/repository/photon_histogram.py @@ -1,8 +1,8 @@ from artiq import * -class PhotonHistogram(AutoDB): - __artiq_unit__ = "Photon histogram" +class PhotonHistogram(Experiment, AutoDB): + """Photon histogram""" class DBKeys: core = Device() diff --git a/examples/master/repository/transport.py b/examples/master/repository/transport.py index 528da06cb..6bc238519 100644 --- a/examples/master/repository/transport.py +++ b/examples/master/repository/transport.py @@ -10,8 +10,8 @@ transport_data = dict( # 4 devices, 3 board each, 3 dacs each ) -class Transport(AutoDB): - __artiq_unit__ = "Transport" +class Transport(Experiment, AutoDB): + """Transport""" class DBKeys: core = Device() diff --git a/examples/sim/al_spectroscopy.py b/examples/sim/al_spectroscopy.py index 01de7ee1b..f41f09886 100644 --- a/examples/sim/al_spectroscopy.py +++ b/examples/sim/al_spectroscopy.py @@ -1,8 +1,8 @@ from artiq import * -class AluminumSpectroscopy(AutoDB): - __artiq_unit__ = "Aluminum spectroscopy (simulation)" +class AluminumSpectroscopy(Experiment, AutoDB): + """Aluminum spectroscopy (simulation)""" class DBKeys: core = Device() diff --git a/examples/sim/simple_simulation.py b/examples/sim/simple_simulation.py index 798d9a149..e0e9d2b0c 100644 --- a/examples/sim/simple_simulation.py +++ b/examples/sim/simple_simulation.py @@ -1,8 +1,8 @@ from artiq import * -class SimpleSimulation(AutoDB): - __artiq_unit__ = "Simple simulation" +class SimpleSimulation(Experiment, AutoDB): + """Simple simulation""" class DBKeys: core = Device()