Experiment base class, replace __artiq_unit__ with docstring

This commit is contained in:
Sebastien Bourdeauducq 2015-03-08 15:43:04 +01:00
parent 407477bc5a
commit f2e3dfb848
21 changed files with 144 additions and 102 deletions

View File

@ -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

View File

@ -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)

View File

@ -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")

View File

@ -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"]

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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:

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -1 +1 @@
{"flopping_freq": 1500.0294421161527}
{"flopping_freq": 1500.0164816344934}

View File

@ -1,8 +1,8 @@
from artiq import *
class DDSTest(AutoDB):
__artiq_unit__ = "DDS test"
class DDSTest(Experiment, AutoDB):
"""DDS test"""
class DBKeys:
core = Device()

View File

@ -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:

View File

@ -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()

View File

@ -1,8 +1,8 @@
from artiq import *
class PhotonHistogram(AutoDB):
__artiq_unit__ = "Photon histogram"
class PhotonHistogram(Experiment, AutoDB):
"""Photon histogram"""
class DBKeys:
core = Device()

View File

@ -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()

View File

@ -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()

View File

@ -1,8 +1,8 @@
from artiq import *
class SimpleSimulation(AutoDB):
__artiq_unit__ = "Simple simulation"
class SimpleSimulation(Experiment, AutoDB):
"""Simple simulation"""
class DBKeys:
core = Device()