mirror of https://github.com/m-labs/artiq.git
language/core: add docstrings
This commit is contained in:
parent
f5167f21fb
commit
7e45cd62ef
|
@ -1,3 +1,5 @@
|
||||||
|
"""Core ARTIQ extensions to the Python language."""
|
||||||
|
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from fractions import Fraction
|
from fractions import Fraction
|
||||||
from copy import copy
|
from copy import copy
|
||||||
|
@ -6,6 +8,29 @@ from artiq.language import units
|
||||||
|
|
||||||
|
|
||||||
class int64(int):
|
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
|
pass
|
||||||
|
|
||||||
def _make_int64_op_method(int_method):
|
def _make_int64_op_method(int_method):
|
||||||
|
@ -33,14 +58,88 @@ for _op_name in ("add", "sub", "mul", "floordiv", "mod",
|
||||||
|
|
||||||
|
|
||||||
def round64(x):
|
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))
|
return int64(round(x))
|
||||||
|
|
||||||
|
|
||||||
def array(element, count):
|
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)]
|
return [copy(element) for i in range(count)]
|
||||||
|
|
||||||
|
|
||||||
class AutoContext:
|
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 = ""
|
parameters = ""
|
||||||
implicit_core = True
|
implicit_core = True
|
||||||
|
|
||||||
|
@ -62,13 +161,24 @@ class AutoContext:
|
||||||
self.build()
|
self.build()
|
||||||
|
|
||||||
def get_missing_value(self, parameter):
|
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:
|
try:
|
||||||
return getattr(self, parameter)
|
return getattr(self, parameter)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
return self.mvs.get_missing_value(parameter)
|
return self.mvs.get_missing_value(parameter)
|
||||||
|
|
||||||
def build(self):
|
def build(self):
|
||||||
""" Overload this function to add sub-experiments"""
|
"""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
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@ -76,6 +186,25 @@ _KernelFunctionInfo = namedtuple("_KernelFunctionInfo", "core_name k_function")
|
||||||
|
|
||||||
|
|
||||||
def kernel(arg):
|
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):
|
if isinstance(arg, str):
|
||||||
def real_decorator(k_function):
|
def real_decorator(k_function):
|
||||||
def run_on_core(exp, *k_args, **k_kwargs):
|
def run_on_core(exp, *k_args, **k_kwargs):
|
||||||
|
@ -108,6 +237,12 @@ _time_manager = _DummyTimeManager()
|
||||||
|
|
||||||
|
|
||||||
def set_time_manager(time_manager):
|
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
|
global _time_manager
|
||||||
_time_manager = time_manager
|
_time_manager = time_manager
|
||||||
|
|
||||||
|
@ -121,6 +256,10 @@ _syscall_manager = _DummySyscallManager()
|
||||||
|
|
||||||
|
|
||||||
def set_syscall_manager(syscall_manager):
|
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
|
global _syscall_manager
|
||||||
_syscall_manager = syscall_manager
|
_syscall_manager = syscall_manager
|
||||||
|
|
||||||
|
@ -130,6 +269,10 @@ kernel_globals = "sequential", "parallel", "delay", "now", "at", "syscall"
|
||||||
|
|
||||||
|
|
||||||
class _Sequential:
|
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):
|
def __enter__(self):
|
||||||
_time_manager.enter_sequential()
|
_time_manager.enter_sequential()
|
||||||
|
|
||||||
|
@ -139,6 +282,14 @@ sequential = _Sequential()
|
||||||
|
|
||||||
|
|
||||||
class _Parallel:
|
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):
|
def __enter__(self):
|
||||||
_time_manager.enter_parallel()
|
_time_manager.enter_parallel()
|
||||||
|
|
||||||
|
@ -148,16 +299,33 @@ parallel = _Parallel()
|
||||||
|
|
||||||
|
|
||||||
def delay(duration):
|
def delay(duration):
|
||||||
|
"""Increases the RTIO time by the given amount.
|
||||||
|
|
||||||
|
"""
|
||||||
_time_manager.take_time(duration)
|
_time_manager.take_time(duration)
|
||||||
|
|
||||||
|
|
||||||
def now():
|
def now():
|
||||||
|
"""Retrieves the current RTIO time, in microcycles.
|
||||||
|
|
||||||
|
"""
|
||||||
return _time_manager.get_time()
|
return _time_manager.get_time()
|
||||||
|
|
||||||
|
|
||||||
def at(time):
|
def at(time):
|
||||||
|
"""Sets the RTIO time to the specified absolute value.
|
||||||
|
|
||||||
|
"""
|
||||||
_time_manager.set_time(time)
|
_time_manager.set_time(time)
|
||||||
|
|
||||||
|
|
||||||
def syscall(*args):
|
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)
|
return _syscall_manager.do(*args)
|
||||||
|
|
Loading…
Reference in New Issue