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 cases rounding errors are to be ignored and accepted.
For the vast majority of numbers that occur in our experiments, we
assume and expect implicit rounding (parameters, scan variables, fit
results, measurements).

class Core:
    cycle = Unit()
    freq = 80*MHz

    @kernel
    def delay_cycles(self, n):
        # ...

    @force_fold
    def coerce(self, v, to=None):
        if isintance(v, to):
            return v
        if isinstance(v, ns.base_unit):
            if to is None:
                to = self.cycle
            return round(v*self.freq)*to
        if ...

    @force_fold
    def delay(self, t):
        t = self.coerce(t, to=self.cycle)
        self.delay_cycles(t.value)

Use:

n = core.coerce(1.2345*ns) # [n] = core.cycle
for i in range(1000):
    core.delay(n)
f = dds.coerce(345*MHz)
t = dds.coerce(core.coerce(1000*n, to=ns))
phi = dds.coerce(f*t, to=rad) # [f*t] = dds.ftw*dds.cycle
dds.phase(pi + phi) # implicit coercion to [phi'] = dds.ptw

in the worst case if necessary.

I like the basic units system including physical and device units but
the conversion/coercion should generally be a function and rounding
should be automatic and the silent default. People have been living with
that just fine and know when they need a bigger boat. The pain of having
to @short_units, round(), and *self.core.ns all the time is too much.

Robert.
_______________________________________________
ARTIQ mailing list
https://ssl.serverraum.org/lists/listinfo/artiq
Migen/MiSoC: please use de...@lists.m-labs.hk instead.

Reply via email to