Hello,
On 08/12/2014 01:20 AM, Sébastien Bourdeauducq wrote:
I'd like to propose a new system to deal with units, quantization by the
devices, and rounding errors.
The basic idea is to expose the native integer units of the devices
directly to the user. For example, the statement:
delay(1)
represents a delay of one microcycle, which is e.g. 12.5ns on the
Papilio Pro without the SERDES, and 1ns on the KC705 with the 1GHz SERDES.
Obviously, the computation of native unit values can benefit from some
automation, to make the system easier to use and facilitate the porting
of experiments across different device setups. Thus, each driver can
define its own mapping that converts usual units (us, ms, etc.) into
native units. For example, a delay of 5 microseconds can be written as:
delay(5*self.core.units.us)
which evaluates to 5000=5*1000 (1ns periods) on the KC705, and 400=5*80
(12.5ns periods) on the Papilio Pro.
The values in unit mappings can be integer or rational. On the Papilio
Pro, core.units.ns is Fraction(2, 25) and delays that are a multiple of
12.5ns, e.g.
delay(25*self.core.units.ns)
is turned by the compiler (via constant folding) into:
delay(2)
Quoting from the last meeting's minutes, the notion of a multiplication
breaks down at least in the following three cases:
* does not help for calibrated quantities (non-linear
calibration of a dac)
* does not help for non-constant steps (floating point
pdq2 time)
* does not help for timings with physical latencies (different rising
and falling edge latencies of an AOM due to RF amps etc)
simply because the conversion between physical and device units is not
multiplicative (+ quantization). There are offsets and non-linearities.
And you can't hide that in the operator because of associativity. Thus
the general need for coercion functions. Also the same physical unit
will need different conversion routes to the same device unit depending
on the context (e.g. delay vs absolute time vs pulse duration).
If the value passed to delay() is not an integer, the compiler returns
an error. In case the user can live with an approximate value instead,
they can use the round() function:
delay(round(24*self.core.units.ns))
which is again turned into delay(2).
The error (1ns) can be computed explicitly (when done on the core
device, this would of course require rational arithmetic support):
(round(24*ns)-24*ns)/ns
Fraction(1, 1)
Since writing the whole access path to each unit (self.core.units...)
results in a heavy and unwieldy syntax, unit mappings can be associated
to function parameters. For example, the delay() function associates the
core device's unit mapping to its parameter, so it is possible to use
the short unit form, e.g.:
delay(5*us)
Associations are done using the @short_units decorator and parameter
annotations. For example:
@short_units
def pulse(self, frequency: make_frequency_mapping(1000), duration:
core.units):
defines a pulse function where the frequency parameter is expressed in
a unit system where 1000 Hz is the reference (=1), and duration uses
the mapping of self.core.units.
(NB: it is a string because function parameter annotations are evaluated
by Python at function definition time, and the value of self is not
available yet. It is the same situation as with the optional core device
selection parameter of the kernel decorator.)
-ESYNTAX. Do you mean
@short_units(frequency=make_frequency_mapping(1000), duration=..)
def pulse(self, frequency, duration):
...
?
One problem with this system occurs when a function passes one of its
parameters to different devices that potentially use different drivers
and unit mappings, e.g:
def pulse2(self, f):
self.dev_a.pulse(f)
self.dev_b.pulse(f)
Parameter annotations can help here as well; the user could write:
@short_units
def pulse2(self, f: (dev_a.units, dev_b.units)):
and the compiler checks at compile time that self.dev_a.units and
self.dev_b.units are equal.
The syntax is counterintuitive. A MHz is a MHz is a MHz. An intuitive
thing would be dds_a.ftw or core.cycle units.
The @short_units stuff is not needed as physical units are the right
thing to use here if coercion is implicit.
Of course, if the user prefers convenience over accuracy, they can also
integrate rounding into pulse2:
def pulse2(self, f_in_MHz):
self.dev_a.pulse(round(f_in_MHz*MHz))
self.dev_b.pulse(round(f_in_MHz*MHz))
Should that read f_in_MHz*self.dev_a.MHz etc?
This new system offers the following advantages:
* does not introduce rounding errors by itself.
* more transparent - lets the user access the native representation if
needed.
* since each driver defines its own scales, unnecessary use of large
integers (which would arise e.g. from representing everything in
picoseconds and driving some device that operates on the order of
milliseconds), rationals or floats is reduced.
Comments?
In virtually all