forked from M-Labs/artiq
349 lines
10 KiB
Python
349 lines
10 KiB
Python
"""Core ARTIQ extensions to the Python language."""
|
|
|
|
from collections import namedtuple
|
|
from copy import copy
|
|
|
|
|
|
class int64(int):
|
|
"""64-bit integers for static compilation.
|
|
|
|
When this class is used instead of Python's ``int``, the static compiler
|
|
stores the corresponding variable on 64 bits instead of 32.
|
|
|
|
When used in the interpreter, it behaves as ``int`` and the results of
|
|
integer operations involving it are also ``int64`` (which matches the
|
|
size promotion rules of the static compiler). This way, it is possible to
|
|
specify 64-bit size annotations on constants that are passed to the
|
|
kernels.
|
|
|
|
Example:
|
|
|
|
>>> a = int64(1)
|
|
>>> b = int64(3) + 2
|
|
>>> isinstance(a, int64)
|
|
True
|
|
>>> isinstance(b, int64)
|
|
True
|
|
>>> a + b
|
|
6
|
|
|
|
"""
|
|
pass
|
|
|
|
def _make_int64_op_method(int_method):
|
|
def method(self, *args):
|
|
r = int_method(self, *args)
|
|
if isinstance(r, int):
|
|
r = int64(r)
|
|
return r
|
|
return method
|
|
|
|
for _op_name in ("neg", "pos", "abs", "invert", "round",
|
|
"add", "radd", "sub", "rsub", "mul", "rmul", "pow", "rpow",
|
|
"lshift", "rlshift", "rshift", "rrshift",
|
|
"and", "rand", "xor", "rxor", "or", "ror",
|
|
"floordiv", "rfloordiv", "mod", "rmod"):
|
|
_method_name = "__" + _op_name + "__"
|
|
_orig_method = getattr(int, _method_name)
|
|
setattr(int64, _method_name, _make_int64_op_method(_orig_method))
|
|
|
|
for _op_name in ("add", "sub", "mul", "floordiv", "mod",
|
|
"pow", "lshift", "rshift", "lshift",
|
|
"and", "xor", "or"):
|
|
_op_method = getattr(int, "__" + _op_name + "__")
|
|
setattr(int64, "__i" + _op_name + "__", _make_int64_op_method(_op_method))
|
|
|
|
|
|
def round64(x):
|
|
"""Rounds to a 64-bit integer.
|
|
|
|
This function is equivalent to ``int64(round(x))`` but, when targeting
|
|
static compilation, prevents overflow when the rounded value is too large
|
|
to fit in a 32-bit integer.
|
|
|
|
"""
|
|
return int64(round(x))
|
|
|
|
|
|
def array(element, count):
|
|
"""Creates an array.
|
|
|
|
The array is initialized with the value of ``element`` repeated ``count``
|
|
times. Elements can be read and written using the regular Python index
|
|
syntax.
|
|
|
|
For static compilation, ``count`` must be a fixed integer.
|
|
|
|
Arrays of arrays are supported.
|
|
|
|
"""
|
|
return [copy(element) for i in range(count)]
|
|
|
|
|
|
class AutoContext:
|
|
"""Base class to automate device and parameter management.
|
|
|
|
Drivers and experiments should in most cases overload this class to
|
|
obtain the parameters and devices (including the core device) that they
|
|
need.
|
|
|
|
This class sets all its ``__init__`` keyword arguments as attributes. It
|
|
then iterates over each element in its ``parameters`` attribute and, if
|
|
they are not already existing, requests them from ``mvs`` (Missing Value
|
|
Supplier).
|
|
|
|
A ``AutoContext`` instance can be used as MVS. If the requested parameter
|
|
is within its attributes, the value of that attribute is returned.
|
|
Otherwise, the request is forwarded to the parent MVS.
|
|
|
|
All keyword arguments are set as object attributes. This enables setting
|
|
parameters of a lower-level ``AutoContext`` object using keyword arguments
|
|
without having those explicitly listed in the upper-level ``AutoContext``
|
|
parameter list.
|
|
|
|
At the top-level, it is possible to have a MVS that issues requests to a
|
|
database and hardware management system.
|
|
|
|
:param parameters: A string containing the parameters that the object must
|
|
have. It must be a space-separated list of valid Python identifiers.
|
|
Default: empty.
|
|
:param implicit_core: Automatically adds ``core`` to the parameter list.
|
|
Default: True.
|
|
|
|
Example:
|
|
|
|
>>> class SubExperiment(AutoContext):
|
|
... parameters = "foo bar"
|
|
...
|
|
... def run():
|
|
... do_something(self.foo, self.bar)
|
|
...
|
|
>>> class MainExperiment(AutoContext):
|
|
... parameters = "bar1 bar2 offset"
|
|
...
|
|
... def build(self):
|
|
... self.exp1 = SubExperiment(self, bar=self.bar1)
|
|
... self.exp2 = SubExperiment(self, bar=self.bar2)
|
|
... self.exp3 = SubExperiment(self, bar=self.bar2 + self.offset)
|
|
...
|
|
>>> def run():
|
|
... self.exp1.run()
|
|
... self.exp2.run()
|
|
... self.exp3.run()
|
|
...
|
|
>>> # does not require a database.
|
|
>>> a = MainExperiment(foo=1, bar1=2, bar2=3, offset=0)
|
|
>>> # "foo" and "offset" are automatically retrieved from the database.
|
|
>>> b = MainExperiment(db_mvs, bar1=2, bar2=3)
|
|
|
|
"""
|
|
parameters = ""
|
|
implicit_core = True
|
|
|
|
def __init__(self, mvs=None, **kwargs):
|
|
self.mvs = mvs
|
|
for k, v in kwargs.items():
|
|
setattr(self, k, v)
|
|
|
|
parameters = self.parameters.split()
|
|
if self.implicit_core:
|
|
parameters.append("core")
|
|
for parameter in parameters:
|
|
try:
|
|
value = getattr(self, parameter)
|
|
except AttributeError:
|
|
value = self.mvs.get_missing_value(parameter)
|
|
setattr(self, parameter, value)
|
|
|
|
self.build()
|
|
|
|
def get_missing_value(self, parameter):
|
|
"""Attempts to retrieve ``parameter`` from the object's attributes.
|
|
If not present, forwards the request to the parent MVS.
|
|
|
|
The presence of this method makes ``AutoContext`` act as a MVS.
|
|
"""
|
|
try:
|
|
return getattr(self, parameter)
|
|
except AttributeError:
|
|
return self.mvs.get_missing_value(parameter)
|
|
|
|
def build(self):
|
|
"""This is called by ``__init__`` after the parameter initialization
|
|
is done.
|
|
|
|
The user may overload this method to complete the object's
|
|
initialization with all parameters available.
|
|
|
|
"""
|
|
pass
|
|
|
|
|
|
_KernelFunctionInfo = namedtuple("_KernelFunctionInfo", "core_name k_function")
|
|
|
|
|
|
def kernel(arg):
|
|
"""This decorator marks an object's method for execution on the core
|
|
device.
|
|
|
|
When a decorated method is called from the Python interpreter, the ``core``
|
|
attribute of the object is retrieved and used as core device driver. The
|
|
core device driver will typically compile, transfer and run the method
|
|
(kernel) on the device.
|
|
|
|
When kernels call another method:
|
|
- if the method is a kernel for the same core device, is it compiled
|
|
and sent in the same binary. Calls between kernels happen entirely on
|
|
the device.
|
|
- if the method is a regular Python method (not a kernel), it generates
|
|
a remote procedure call (RPC) for execution on the host.
|
|
|
|
The decorator takes an optional parameter that defaults to ``core`` and
|
|
specifies the name of the attribute to use as core device driver.
|
|
|
|
"""
|
|
if isinstance(arg, str):
|
|
def real_decorator(k_function):
|
|
def run_on_core(exp, *k_args, **k_kwargs):
|
|
getattr(exp, arg).run(k_function, ((exp,) + k_args), k_kwargs)
|
|
run_on_core.k_function_info = _KernelFunctionInfo(
|
|
core_name=arg, k_function=k_function)
|
|
return run_on_core
|
|
return real_decorator
|
|
else:
|
|
def run_on_core(exp, *k_args, **k_kwargs):
|
|
exp.core.run(arg, ((exp,) + k_args), k_kwargs)
|
|
run_on_core.k_function_info = _KernelFunctionInfo(
|
|
core_name="core", k_function=arg)
|
|
return run_on_core
|
|
|
|
|
|
class _DummyTimeManager:
|
|
def _not_implemented(self, *args, **kwargs):
|
|
raise NotImplementedError(
|
|
"Attempted to interpret kernel without a time manager")
|
|
|
|
enter_sequential = _not_implemented
|
|
enter_parallel = _not_implemented
|
|
exit = _not_implemented
|
|
take_time = _not_implemented
|
|
get_time = _not_implemented
|
|
set_time = _not_implemented
|
|
|
|
_time_manager = _DummyTimeManager()
|
|
|
|
|
|
def set_time_manager(time_manager):
|
|
"""Set the time manager used for simulating kernels by running them
|
|
directly inside the Python interpreter. The time manager responds to the
|
|
entering and leaving of parallel/sequential blocks, delays, etc. and
|
|
provides a time-stamped logging facility for events.
|
|
|
|
"""
|
|
global _time_manager
|
|
_time_manager = time_manager
|
|
|
|
|
|
class _DummySyscallManager:
|
|
def do(self, *args):
|
|
raise NotImplementedError(
|
|
"Attempted to interpret kernel without a syscall manager")
|
|
|
|
_syscall_manager = _DummySyscallManager()
|
|
|
|
|
|
def set_syscall_manager(syscall_manager):
|
|
"""Set the system call manager used for simulating the core device's
|
|
runtime in the Python interpreter.
|
|
|
|
"""
|
|
global _syscall_manager
|
|
_syscall_manager = syscall_manager
|
|
|
|
# global namespace for kernels
|
|
|
|
kernel_globals = "sequential", "parallel", "delay", "now", "at", "syscall"
|
|
|
|
|
|
class _Sequential:
|
|
"""In a sequential block, statements are executed one after another, with
|
|
the time increasing as one moves down the statement list.
|
|
|
|
"""
|
|
def __enter__(self):
|
|
_time_manager.enter_sequential()
|
|
|
|
def __exit__(self, type, value, traceback):
|
|
_time_manager.exit()
|
|
sequential = _Sequential()
|
|
|
|
|
|
class _Parallel:
|
|
"""In a parallel block, all top-level statements start their execution at
|
|
the same time.
|
|
|
|
The execution time of a parallel block is the execution time of its longest
|
|
statement. A parallel block may contain sequential blocks, which themselves
|
|
may contain parallel blocks, etc.
|
|
|
|
"""
|
|
def __enter__(self):
|
|
_time_manager.enter_parallel()
|
|
|
|
def __exit__(self, type, value, traceback):
|
|
_time_manager.exit()
|
|
parallel = _Parallel()
|
|
|
|
|
|
def delay(duration):
|
|
"""Increases the RTIO time by the given amount.
|
|
|
|
"""
|
|
_time_manager.take_time(duration)
|
|
|
|
|
|
def now():
|
|
"""Retrieves the current RTIO time, in microcycles.
|
|
|
|
"""
|
|
return _time_manager.get_time()
|
|
|
|
|
|
def at(time):
|
|
"""Sets the RTIO time to the specified absolute value.
|
|
|
|
"""
|
|
_time_manager.set_time(time)
|
|
|
|
|
|
def syscall(*args):
|
|
"""Invokes a service of the runtime.
|
|
|
|
Kernels use this function to interface to the outside world: program RTIO
|
|
events, make RPCs, etc.
|
|
|
|
Only drivers should normally use ``syscall``.
|
|
|
|
"""
|
|
return _syscall_manager.do(*args)
|
|
|
|
|
|
_encoded_exceptions = dict()
|
|
|
|
first_user_eid = 1024
|
|
|
|
|
|
def EncodedException(eid):
|
|
"""Represents exceptions on the core device, which are identified
|
|
by a single number.
|
|
|
|
"""
|
|
try:
|
|
return _encoded_exceptions[eid]
|
|
except KeyError:
|
|
class EncodedException(Exception):
|
|
def __init__(self):
|
|
Exception.__init__(self, eid)
|
|
_encoded_exceptions[eid] = EncodedException
|
|
return EncodedException
|