Circuit notation and Compilation

Lucipy ships with a number of tools to manipulate the LUCIDAC interconnection matrix (also known as UCI matrix). In particular, the circuits package provides a grammar for connecting analog computing elements in the LUCIDAC.

A grammar for describing circuits

The circuits package allows to set up an analog computation circuit with a number of simple methods which define a “grammar” in terms of a traditional object oriented “subject verb object” notation. For instance, circuit.connect(a,b) has the quite obvious meaning to tell the circuit to connect a to b (see connect() for details). Other “literal methods” are for instance circuits.probe(a) which tells that a can be externally probed with an oscilloscope (see probe()) or circuits.measure(a) which tells that a shall be internally measured by the data acquisition (analog to digital converters, ADCs; see measure()).

Note

Note that setting up circuits is, by purpose, intrinsically decoupled from configuring the actual hardware. Therefore, when working on a Circuit class as in the examples given in the previous paragraph, this currently has no immediate effect on the hardware. Instead, the new analog wiring is only written out when calling the set_circuit() methods as in lucidac.set_circuit(circuit.generate()).

Future variants of this code may add an immediate variant which writes out Routes and other configuration as soon as they are defined.

The typical usage of the Circuit class shall be demonstrated on the Lorenz attractor example (see also Example circuits). It can be basically entered as element connection diagram :

from lucipy import Circuit

lorenz = Circuit()

x   = lorenz.int(ic=-1)
y   = lorenz.int()
z   = lorenz.int()
mxy = lorenz.mul()   # -x*y
xs  = lorenz.mul()   # +x*s
c   = lorenz.const()

lorenz.connect(x,  x, weight=-1)
lorenz.connect(y,  x, weight=+1.8)

lorenz.connect(x, mxy.a)
lorenz.connect(y, mxy.b)

lorenz.connect(mxy, z, weight=-1.5)
lorenz.connect(z,   z, weight=-0.2667)

lorenz.connect(x, xs.a, weight=-1)
lorenz.connect(z, xs.b, weight=+2.67)
lorenz.connect(c, xs.b, weight=-1)

lorenz.connect(xs, y, weight=-1.536)
lorenz.connect(y,  y, weight=-0.1)

Internally, the library stores Route tuples:

>> print(lorenz)
Routing([Route(uin=8, lane=0, coeff=-1, iout=8),
Route(uin=9, lane=1, coeff=1.8, iout=8),
Route(uin=8, lane=2, coeff=1, iout=0),
Route(uin=9, lane=3, coeff=1, iout=1),
Route(uin=0, lane=4, coeff=-1.5, iout=10),
Route(uin=10, lane=5, coeff=-0.2667, iout=10),
Route(uin=8, lane=14, coeff=-1, iout=2),
Route(uin=10, lane=15, coeff=2.67, iout=3),
Route(uin=4, lane=16, coeff=-1, iout=3),
Route(uin=1, lane=17, coeff=-1.536, iout=9),
Route(uin=9, lane=18, coeff=-0.1, iout=9),
Route(uin=8, lane=8, coeff=0, iout=6),
Route(uin=9, lane=9, coeff=0, iout=6)])

By concept, there is no (internal) symbolic representation but instead immediate destruction of any symbolics to the integer indices of what they enumerate We call this “early compilation” and distinguish it from a “late compilation” which tries to retain an intermediate representation as long as possible. In particular, this “compiler” does a pick-and-place as soon as possible (not delayed) and thus intentionally cannot do any kind of optimization. It is, after all, intentionally a very simple compiler which tries to do its scope as good as possible.

The “pseudo-symbolic” input format can be easily “decompiled” with reverse():

>> print(lorenz.reverse())
Connection(Int0, Int0, weight=-1),
Connection(Int1, Int0, weight=1.8),
Connection(Int0, Mul0.a),
Connection(Int1, Mul0.a),
Connection(Mul0, Int2, weight=-1.5),
Connection(Int2, Int2, weight=-0.2667),
Connection(Int0, Mul1.a, weight=-1),
Connection(Int2, Mul1.a, weight=2.67),
Connection(Const0, Mul1.a, weight=-1),
Connection(Mul1, Int1, weight=-1.536),
Connection(Int1, Int1, weight=-0.1),
Connection(Int0, Mul3.a, weight=0),
Connection(Int1, Mul3.a, weight=0)

Fundamental building blocks

The fundamental building block of this circuit description language is the Route. As written in the corresponding API docs, a Conncetion() is just a function that produces a Route which is not yet placed (some codes also refer to this as “logical routes”).

The other fundamental ingredient are actual element descriptions, for instance for the Integrator (Int) or Multiplier (Mul). In lucipy, these classes always represent routed “physical” computing elements, i.e. they describe actual and really existing computing elements. The code currently has no concept for unrouted computing elements. This makes sense if you keep in mind that by intention this code tries to place early and allocate on a greedy basis, something which one can do for a simple system such as LUCIDAC with it’s all-to-all connectivity.

A guiding principle at the design of this library was to minimize the amount of code users have to write. This is done by providing one big class interface with the Circuit class which itself inherits a number of more specialized classes that deal with the particular parts of the hardware model.

Ideally, users have only to import this single class in order to work with the package. Instances of all classes described in this section can be obtained with various helper methods. For instance, the Reservoir class does the accounting (greedy place and routing) and thus hands out instances of the computing elements. This is just one of many base classes of a Circuit. Other examples are the route() and connect() methods of the Routing class which hand out (and register) Route and Conncetion().

Import and Export formats

This section shall provide an overview about the various import and export formats available in the lucipy.circuits package. In case of an export, a method converts the internal routing list representation to some other, typically equivalent representation. In case of an import, a non-native representation is re-interpreted as a route of lists.

JSON configuration format

The most important format is the LUCIDAC protocol format (see the REDAC communication protocol in the LUCIDAC/REDAC firmware documentation). It is a sparse JSON format and lucipy is able to import to and export from this format by emitting or reading the corresponding python nested dictionary/list data structures which can easily be serialized to/from JSON with the python-included json package.

The import form/export to this format is the most important one in the whole package. The export is provided by generate() and the import is provided by load(). Here is an example how to export the Lorenz circuit given in the sections above:

>> lorenz.generate()
{'/U': {'outputs': [8,
   9,
   8,
   9,
   0,
   10,
   None,
   None,
   8,
   9,
   None,
   None,
   None,
   None,
   8,
   10,
   4,
   1,
   9,
   None,
   None,
   None,
   None,
   None,
   None,
   None,
   None,
   None,
   None,
   None,
   None,
   None]},
'/C': {'elements': [-1,
   1.8,
   1,
   1,
   -1.5,
   -0.2667,
   0,
   0,
   0,
   0,
   0,
   0,
   0,
   0,
   -1,
   2.67,
   -1,
   -1.536,
   -0.1,
   0,
   0,
   0,
   0,
   0,
   0,
   0,
   0,
   0,
   0,
   0,
   0,
   0]},
'/I': {'outputs': [[2],
   [3],
   [14],
   [15, 16],
   [],
   [],
   [8, 9],
   [],
   [0, 1],
   [17, 18],
   [4, 5],
   [],
   [],
   [],
   [],
   []]},
'/M0': {'elements': [{'k': 10000, 'ic': -1},
   {'k': 10000, 'ic': 0},
   {'k': 10000, 'ic': 0},
   {'k': 10000, 'ic': 0.0},
   {'k': 10000, 'ic': 0.0},
   {'k': 10000, 'ic': 0.0},
   {'k': 10000, 'ic': 0.0},
   {'k': 10000, 'ic': 0.0}]},
'/M1': {}}

This makes it easy to straight forwardly program a LUCIDAC by writing

from lucipy import LUCIDAC, Circuit
lorenz = Circuit()
# ... the circuit from above ...

hc = LUCIDAC()
hc.set_config(lorenz.generate())
hc.start_run() # ...

Numpy matrix formats

A single 16x16 interconnect matrix A can be generated which describes the system interconnection in a traditional matrix scheme inputs = A * outputs, see Circuit simulation for details.

This matrix incorporates the interconncets, weights and implicit sums between Math blocks. It can not properly represent the external I/O. Usage requires the numpy package. What follows is a usage example:

>> import numpy as np
>> np.set_printoptions(edgeitems=30, linewidth=1000, suppress=True)
>> lorenz.to_dense_matrix().shape
(16,16)
>> lorenz.to_dense_matrix()
array([[ 0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  1.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ],
       [ 0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  1.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ],
       [ 0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    , -1.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ],
       [ 0.    ,  0.    ,  0.    ,  0.    , -1.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  2.67  ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ],
       [ 0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ],
       [ 0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ],
       [ 0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ],
       [ 0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ],
       [ 0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    , -1.    ,  1.8   ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ],
       [ 0.    , -1.536 ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    , -0.1   ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ],
       [-1.5   ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    , -0.2667,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ],
       [ 0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ],
       [ 0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ],
       [ 0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ],
       [ 0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ],
       [ 0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ,  0.    ]])

Other formats exist, for instance to export the internal U, C and I matrices seperately.

pybrid interface

This is an export to the pybrid DSL (see comparison for details):

>> print(lorenz.to_pybrid_cli())
set-alias * carrier

set-element-config carrier/0/M0/0 ic -1
set-element-config carrier/0/M0/0 k -1
set-element-config carrier/0/M0/1 k 0
set-element-config carrier/0/M0/2 k 0
set-element-config carrier/0/M0/3 k 0.0
set-element-config carrier/0/M0/4 k 0.0
set-element-config carrier/0/M0/5 k 0.0
set-element-config carrier/0/M0/6 k 0.0
set-element-config carrier/0/M0/7 k 0.0
route -- carrier/0  8  0  -1.000  8
route -- carrier/0  9  1   1.800  8
route -- carrier/0  8  2   1.000  0
route -- carrier/0  9  3   1.000  1
route -- carrier/0  0  4  -1.500 10
route -- carrier/0 10  5  -0.267 10
route -- carrier/0  8 14  -1.000  2
route -- carrier/0 10 15   2.670  3
route -- carrier/0  4 16  -1.000  3
route -- carrier/0  1 17  -1.536  9
route -- carrier/0  9 18  -0.100  9
route -- carrier/0  8  8   0.000  6
route -- carrier/0  9  9   0.000  6
# run --op-time 500000

Sympy interface

Here comes an export to a Sympy system:

>> lorenz.to_sympy()
[Eq(m_0(t), -i_0(t)*i_1(t)),
Eq(m_1(t), (2.67*i_2(t) - 1.0)*i_0(t)),
Eq(Derivative(i_0(t), t), -i_0(t) + 1.8*i_1(t)),
Eq(Derivative(i_1(t), t), -0.1*i_1(t) - 1.536*m_1(t)),
Eq(Derivative(i_2(t), t), -0.2667*i_2(t) - 1.5*m_0(t))]

This can be tailored in order to have something which can be straightforwardly numerically solved:

>> [ eq.rhs for eq in lorenz.to_sympy(int_names="xyz", subst_mul=True, no_func_t=True) ]
[-x + 1.8*y, -1.536*x*(2.67*z - 1.0) - 0.1*y, 1.5*x*y - 0.2667*z]

Random system generation

For testing and documentation purposes, there is Circuit.random() which creates random routes. This simplifies writing unit tests (see also Developer notes).

API Docs

Lucipy Circuits: A shim over the routes.

This class structure provides a minimal level of user-friendlyness to allow route-based programming on the LUCIDAC. This effectively means a paradigm where one connects elements from math blocks to each other, with a coefficient inbetween. And the assignment throught the UCI matrix is done greedily by “first come, first serve” without any constraints.

The code is heavily inspired by the LUCIGUI lucisim typescript compiler, which is however much bigger and creates an AST before mapping.

In contrast, the approach demonstrated here is not even enough for REV0 connecting ExtIn/ADC/etc. But it makes it very simple and transparent to work with routes and setup the circuit configuration low level.

lucipy.circuits.window(seq, n=2)[source]

Returns a sliding window (of width n) over data from the iterable

lucipy.circuits.next_free(occupied: List[bool], criterion=None, append_to: int = None) → Optional[int][source]

Looks for the first False value within a list of truth values.

>>> next_free([1,1,0,1,0,0]) # using ints instead of booleans for brevety
2

If no more value is free in list, it can append up to a given value

>>> next_free([True]*4, append_to=3) # None, nothing free
>>> next_free([True]*4, append_to=6)
4
Parameters

criterion – Callback which gets the potential value and can decide whether this value is fine. Can be used for constraining an acceptable next free lane.

class lucipy.circuits.Int(id, out, a)

Integrator, integrates input “a”, output to “out”

a

Alias for field number 2

id

Alias for field number 0

out

Alias for field number 1

class lucipy.circuits.Mul(id, out, a, b)

Multiplier, multiplies inputs “a” with “b”, output to “out”

a

Alias for field number 2

b

Alias for field number 3

id

Alias for field number 0

out

Alias for field number 1

class lucipy.circuits.Id(id, out, a)

Identity element, just passes input “a” to output “out”

a

Alias for field number 2

id

Alias for field number 0

out

Alias for field number 1

class lucipy.circuits.Const(id, out)

Constant giver. output on (cross lane “out”)

id

Alias for field number 0

out

Alias for field number 1

lucipy.circuits.Front

Front panel input/output (ACL_IN/ACL_OUT) on lane “lane”

alias of lucipy.circuits.Out

lucipy.circuits.Ele

type of any kind of element defined so far

alias of Union[lucipy.circuits.Int, lucipy.circuits.Mul, lucipy.circuits.Const, lucipy.circuits.Id, lucipy.circuits.Out]

class lucipy.circuits.DefaultLUCIDAC[source]

This describes a default LUCIDAC REV1 setup with

  • M0 = MIntBlock (8 integrators)

  • M1 = MMulBlock (4 multipliers and ID-lanes)

In particular, note that the clanes of M0 and M1 have switched during the transition from REV0 to REV1 hardware.

classmethod make(t: Union[lucipy.circuits.Int, lucipy.circuits.Mul, lucipy.circuits.Const, lucipy.circuits.Id, lucipy.circuits.Out], idx)[source]

A factory for the actual elements.

>>> DefaultLUCIDAC().make(Int, 1)
Int(id=1, out=1, a=1)
>>> DefaultLUCIDAC().make(Mul, 3)
Mul(id=3, out=11, a=14, b=15)
static populated()[source]

An unsorted list of all allocatable computing elements

classmethod resolve_mout(idx, reservoir)[source]

Simplistic way to map mul block outputs to a reservoir. This is sensitive on M0 and M1 block positions.

class lucipy.circuits.Reservoir(allocation=None, **kwargs)[source]

This is basically the entities list, tracking which one is already handed out (“allocated”) or not.

Note that the Mul/Int classes only hold some integers. In contrast, the configurable properties of the stateful computing element (Integrator) is managed by the MIntBlock class below.

alloc(t: Union[lucipy.circuits.Int, lucipy.circuits.Mul, lucipy.circuits.Const, lucipy.circuits.Id, lucipy.circuits.Out], id=None)[source]

Allocate computing elements.

>>> r = Reservoir()
>>> r.alloc(Int,1)
Int(id=1, out=1, a=1)
>>> r.alloc(Int)
Int(id=0, out=0, a=0)
>>> r.alloc(Int)
Int(id=2, out=2, a=2)
int(id=None)[source]

Allocate an Integrator. If you pass an id, allocate that particular integrator.

mul(id=None)[source]

Allocate a Multiplier. If you pass an id, allocate that particular multiplier.

const(id=None)[source]

Allocates a constant. Also turns on the constant giver scheme in general.

identity(id=None)[source]

Allocates one Identity element

ints(count)[source]

Allocate count many integrators

muls(count)[source]

Allocate count many multipliers

identities(count)[source]

Allocate count many identifiers

front_panel(id=None)[source]

Allocates an ACL_IN/ACL_OUT Front panel input/output

class lucipy.circuits.Route(uin, lane, coeff, iout)[source]

Routes are the essential building block of this circuit API. A list of routes is a way to describe the sparse system matrix (UCI-matrix). Sparse matrix values A[i,j] can be described with lists of 3-tuples (i,j,A), i.e. the position and value. This is what (uin,iout,coeff) basically describe. Additionally, there is the lane which describes the internal structure of the UCI matrix. The maximum of 32 lanes corresponds to the fact that only 32 elements within the system matrix can be nonzero.

Python allows for any types for the tuple values. Regularly, instances of the computing elements (:class:Int, :class:Mul, etc) are used at the uin and iout slots.

This compiler knows the concept of “not yet placed” routes. This are routes where lane == None. Some of our codes refer to such routes as “logical” in contrast to “physically” placed routes. In this code, unplaced routes are called “Connection”, i.e. they can be generated with the Connection() function.

do_not_connect

iout constant in order to not connect.

resolve()[source]

Resolve elements at uin/iout to numbers. This only works if they have the attributes a/b/out. For more fancy elements, it is subject of the caller to resolve them properly.

lucipy.circuits.Connection(source: Union[lucipy.circuits.Int, lucipy.circuits.Mul, lucipy.circuits.Const, lucipy.circuits.Id, lucipy.circuits.Out, int], target: Union[lucipy.circuits.Int, lucipy.circuits.Mul, lucipy.circuits.Const, lucipy.circuits.Id, lucipy.circuits.Out, int], weight=1)[source]

Syntactic sugar for a “logical route”, i.e. a Route without a lane.

>>> r = Reservoir()
>>> I1, M1 = r.int(), r.mul()
>>> Connection(M1.a, I1.out)
Route(uin=8, lane=None, coeff=1, iout=0)
>>> Connection(M1, I1)
Route(uin=Mul(id=0, out=8, a=8, b=9), lane=None, coeff=1, iout=Int(id=0, out=0, a=0))
class lucipy.circuits.MIntBlock(**kwargs)[source]

Stateful configuration about all the MIntBlock.

generate()[source]

Returns configuration for the MIntBlock

load(config)[source]

Inverts what generate() is doing.

>>> b = MIntBlock()
>>> b.randomize()
>>> config = b.generate()
>>> c = MIntBlock()
>>> c.load(config) == c # chainable
True
>>> assert b.ics == c.ics
>>> assert b.k0s == c.k0s
to_pybrid_cli()[source]

Generate the Pybrid-CLI commands as string out of this Route representation

class lucipy.circuits.Probes(acl_select=None, adc_channels=None, **kwargs)[source]

Models the LUCIDAC Carrier ADC channels and Front Panel inputs (ACL_select).

set_acl_select(acl_select)[source]

Overwrite the internal/external selection. Do not use – Use Routing.front_input() instead.

set_adc_channels(adc_channels)[source]

Overwrite the ADC Channel requests. Do not use – Use Routing.probe() instead.

measure(source: Union[lucipy.circuits.Int, lucipy.circuits.Mul, lucipy.circuits.Const, lucipy.circuits.Id, lucipy.circuits.Out, int], adc_channel=None)[source]

Syntactic sugar to set an adc_channel.

generate()[source]

Returns carrier configuration, not cluster configuration

class lucipy.circuits.UCI(U, C, I)

Just a tuple collecting U, C, I matrices without determining their actual representation. The representation could be lists, numpy arrays, etc. depending on the use.

C

Alias for field number 1

I

Alias for field number 2

U

Alias for field number 0

class lucipy.circuits.Routing(routes: List[lucipy.circuits.Route] = None, accept_dirty=False, **kwargs)[source]

This class provides a route-tuple like interface to the UCI block and generates the Output-centric matrix configuration at the end. Most likely, as a user you don’t want to intiate a Routing instance but a Circuit instance instead.

Parameters

accept_dirty – Flag for allowing to add “dirty” Routes, i.e. illegal Routes, without raising at add(). Instead, you can use sanity_check() later to see the problems once more. May be useful for importing existing circuits.

available_lanes()[source]

Returns a list of lane indices generally available in the LUCIDAC (independent of their allocation). If you set the class attribute lanes_constraint, their values will be used.

>>> r = Routing()
>>> r.lanes_constraint = [3, 7, 17]
>>> r.connect(0, 8)
Route(uin=0, lane=3, coeff=1, iout=8)
>>> r.connect(3, 7)
Route(uin=3, lane=7, coeff=1, iout=7)
>>> r.connect(12, 14)
Route(uin=12, lane=17, coeff=1, iout=14)
>>> r.connect(7, 9)
Traceback (most recent call last):
...
ValueError: All [3, 7, 17] available lanes occupied, no more connections possible.
randomize(num_lanes=32, max_coeff=10, seed=None)[source]

Appends random routes.

Parameters
  • num_lanes – How many lanes to fill up. By default fills up all lanes.

  • max_coeff – Maximal coefficient magnitudes. +1 or +10 are useful values for LUCIDAC.

  • seed – For reproducability (as in unit tests), this calls random.seed. A large integer may be a suitable argument.

Returns

The instance, i.e. is chainable.

use_constant(use_constant=True)[source]

Activates/Configures the system’s constant giver source. A constant giver allows to use constant numbers within the computation. A constant then can be connected for instance to an integrator or multiplier or summed with other values or constants.

In LUCIDAC, there is one constant source in the U-Block which can configured with these values:

  • True or 1.0 or 1: Generates the constant +1

  • 0.1 generates the constant +0.1

  • False removes the overall constant

Do not use None for turning off the constant, as this is the default value for not passing the constant request at all to the LUCIDAC.

The constant can be further modified by the coefficient in the lane. This allows for (up and down) scaling and sign inversion of the constant.

This method only registers the use of the constant. In order to make use of it, routes using Constants have to be added. As the constants are only available at certain clane/lane positions, you should make use of the Reservoir.const() method in order to obtain a Constant object and connect it with connect(), which will figure out a suitable lane.

probe(source: Union[lucipy.circuits.Int, lucipy.circuits.Mul, lucipy.circuits.Const, lucipy.circuits.Id, lucipy.circuits.Out, int], front_port=None, weight=1)[source]

Syntactic sugar to route a port to the front panel output. The name indicates that an oscilloscope probe shall be connected to this port. If no port is given, will count up.

If you need a weight, use connect() directly as in circuit.connect(source, circuit.front_output(front_port), weight=1.23).

Parameters

front_output – Integer describing the front port number (0..7)

Returns

The generated Route (is also added)

See also Circuit.measure() for putting a singal to ADC/DAQ.

Instead of using this function, you can also add the route by yourself:

front_input(id, use_front=True)[source]

Make use of a front panel input (or use internal lane instead, if use_front=False) Note that front panel ports are always outputs and that they come behind the C-block, thus no coefficient can be used.

You should not use this function when describing circuits, since it won’t give you control over the I-block configuration. Instead, you should route front inputs:

>>> c = Circuit()
>>> i0 = c.int() # integrator 0
>>> fp0 = c.front_panel(0) # front panel input/output 0
>>> c.connect(fp0, i0) # connect front panel input to integrator 0
Route(uin=-1, lane=24, coeff=1, iout=0)
next_free_lane(constraint=None)[source]

Allocates and returns the next free lane in the circuit.

Parameters

constraint – can be a callback which gets a candidate lane and can accept it as suitable or not. This is useful for instance for the constant sources or ACL_IN/OUT features which are only available on certain lanes or lane combinations.

add(route_or_list_of_routes: Union[lucipy.circuits.Route, List[lucipy.circuits.Route]])[source]

Main method for adding one or multiple routes to the internal route list.

If a route containing Elements or a Connection (i.e. a Route with lane=None) is given, this function will do an “immediate pick and place”. So if you want, this is the compiler component in this class. If a “final” route is given, with only numeric arguments, there is not much to do.

As a matter of principle, this function never corrects data it receives. That is, it does a certain amount of proof/integrity checking which yields ValueErrors if we complain about:

  • Checking whether a Constant can be routed that way

  • Checking if the requested lane is already in use

  • Checking whether values are out of bounds (i.e. lanes, coefficients, etc)

If you don’t want this function to raise on invalid data, set the class attribute accept_dirty=True. This option can also be passed to the constructor.

connect(source: Union[lucipy.circuits.Int, lucipy.circuits.Mul, lucipy.circuits.Const, lucipy.circuits.Id, lucipy.circuits.Out, int], target: Union[lucipy.circuits.Int, lucipy.circuits.Mul, lucipy.circuits.Const, lucipy.circuits.Id, lucipy.circuits.Out, int], weight=1)[source]

Syntactic sugar for adding a Connection(). :returns: The generated Route.

route(uin, lane, coeff, iout)[source]

Syntactic sugar for adding a Route. :returns: The generated Route.

routes2input()lucipy.circuits.UCI[source]

Converts the list of routes to an input matrix representation. In this format, the U/C/I matrix are each represented as a list of 32 numbers. This is basically an array-of-structures to structure-of-array (AoS-to-SoA) operation and inspired by the lucicon circuit compiler (part of lucigui). See generate() for the actual high-level method for the spare matrix format used by the LUCIDAC protocol.

Warning

The C matrix values in this representation are floats within [-20,+20] and not yet rescaled (as in REV1 hardware). See coeff_upscale() for the code which shifts this information.

static input2output(inmat, keep_arrays=True)[source]

Input/Output centric format conversion. Relevant only for C block. Maps Array<int|None,32> onto Array<Array<int>|int, 16>.

Example:

>>> iouts_inputs = [ r.iout for r in Routing().randomize(seed=37311).routes ]
>>> print(iouts_inputs)
[7, 5, 2, 9, 2, 15, 7, 1, 5, 7, 6, 1, 5, 0, 10, 7, 13, 3, 9, 9, 10, 1, 9, 2, 1, 7, 12, 8, 10, 10, 1, 3]
>>> Routing.input2output(iouts_inputs) 
[[13],
 [7, 11, 21, 24, 30],
 [2, 4, 23],
 [17, 31],
 [],
 [1, 8, 12],
 [10],
 [0, 6, 9, 15, 25],
 [27],
 [3, 18, 19, 22],
 [14, 20, 28, 29],
 [],
 [26],
 [16],
 [],
 [5]]
static output2input(outmat)[source]

Input/Output centric format conversion. Relevant only for C block. Maps Array<Array<int>|int,16> onto Array<int|None,32>.

This is obviously the inverse of input2output:

>>> iouts_inputs  = [ r.iout for r in Routing().randomize(seed=75164).routes ]
>>> iouts_outputs = Routing.input2output(iouts_inputs)
>>> again_inputs  = Routing.output2input(iouts_outputs)
>>> iouts_inputs == again_inputs
True
Returns

List of size 32.

sanity_check(also_print=True)[source]

Performs a number of plausibilty and sanity checks on the given circuit. These are:

  1. General data type check on the given routes (out of bounds, etc)

  2. For computing elements with more then one input (currently only the multipliers): Checks whether no input or all are used.

The sanity check is the last bastion between ill-defined data and a writeout to LUCIDAC, which will probably complain in a less comprehensive way.

Returns

A list of human readable messages as strings. Empty list means no warning.

Examples on errnous circuits:

>>> a = Circuit()
>>> a.routes.append(Route(17, -32, -24.0, None))
>>> warnings_as_list = a.sanity_check()
Sanity check warning: Route contains None values.
Sanity check warning: Uin out of range in Route(uin=17, lane=-32, coeff=-24.0, iout=None)
Sanity check warning: Lane out of range in Route(uin=17, lane=-32, coeff=-24.0, iout=None)
Sanity check warning: Coefficient out of range in Route(uin=17, lane=-32, coeff=-24.0, iout=None)
Sanity check warning: Iout out of range in Route(uin=17, lane=-32, coeff=-24.0, iout=None)

Obviously, in the example above the problem started in the first place because the user accessed the routes atribute instead of using the route() or add() methods, which already do a good part of the checking. In general, never access the routes directly for writing.

The situation is more difficult when at routing time the problem is not detectable but later it is:

>>> b = Circuit()
>>> i = b.int()
>>> m = b.mul()
>>> b.connect(i, m.a)
Route(uin=0, lane=0, coeff=1, iout=8)
>>> warnigns_as_list = b.sanity_check()
Sanity check warning: Multiplier 0 (counting from 0) has input but output is not used (clane=8 does not go to some route.uin).
Sanity check warning: Warning: Multiplier 0 is in use but connection B is empty

There are a number of mis-uses which this checker cannot detect, by design. This is, for example, when inputs and outputs are mixed up:

>>> c = Circuit()
>>> correct0 = c.connect(i, m.a)
>>> correct1 = c.connect(i, m.b)
>>> errnous = c.connect(m.a, i.out)
>>> c.sanity_check() == []
True

The reason for this is because in this example, the explicit member access m.a or i.out resolves to integers and the checker cannot find out whether the indices where given intentionally or by accident.

generate(sanity_check=True)[source]

Generate the configuration data structure required by the JSON protocol, which is that “output-centric configuration”. This is a major function of the class. You can use the data structure generated here immediately to produce JSON configuration files as standalone, for instance by passing the output of this function to json.dumps(circuit.generate()).

Generates configuration for the Carrier.

Note

The C-Matrix values here are correctly scaled.

load(carrier_config)[source]

The inverse of generate. Useful for loading an existing circuit. You can load an existing standalone JSON configuration file by using this function.

The following code snippet shows that load() and generate() are really inverse to each other, allowing to first export and then import the same circuit:

>>> a = Routing().randomize()
>>> b = Routing().load(a.generate(sanity_check=False))
>>> a.routes == b.routes
True

Note

Also accepts a carrier-level configuration. Does not accept a configuration message including the JSON Envelope right now.

Currently does not yet properly reads ACL_IN requests.

to_pybrid_cli()[source]

Generate the Pybrid-CLI commands as string out of this Route representation.

to_dense_matrix(sanity_check=True)[source]

Generates a dense numpy matrix for the UCI block, i.e. a real-valued 16x16 matrix with bounded values [-20,20] where at most 32 entries are non-zero.

to_dense_matrices(sanity_check=True)lucipy.circuits.UCI[source]

Generates the three matrices U, C, I as dense numpy matrices. C and I are properly scaled as in the real system (upscaling happens in I).

Returns

3-Tuple of numpy matrices for U, C, I, in this order.

Note that one way to reproduce to_dense_matrix() is just by computing I.dot(C.dot(U)) on the output of this function.

>>> import numpy as np
>>> c = Circuit().randomize()
>>> U, C, I = c.to_dense_matrices(sanity_check=False) # skipping for doctesting
>>> assert np.all(I.dot(C.dot(U)) == c.to_dense_matrix(sanity_check=False)) == True

Limitations: This omits all information about acl_select and the constant giver scheme.

to_sympy(int_names=None, subst_mul=False, no_func_t=False)[source]

Creates an ODE system in sympy based on the circuit.

Parameters
  • int_names – Allows to overwrite the names of the integators. By default they are called i_0, i_1, … i_7. By providing a list such as [“x”, “y”, “z”], these names will be taken. If the list is shorter then length 8, it will be filled up with the default names.

  • subst_mul – Substitute multiplications, getting rid of explicit Eq(m_0, …) statements. Useful for using the equation set within an ODE solver because the state vector is exactly the same length of the entries.

  • no_func_t – Write f instead of f(t) on the RHS, allowing for denser notations, helps also in some solver contexts.

Warning

This method is part of the Routing class and therefore knows nothing about the MIntBlock settings, in particular not the k0. You have to apply different k0 values by yourself if neccessary! This can be as easy as to multiply the equations rhs with the appropriate k0 factor.

Further limitations:

  • Will just ignore ACL_IN (Front panel in/out)

Example for making use of the output within a scipy solver:

from sympy import latex, lambdify, symbols
from scipy.integrate import solve_ivp
xyz = list("xyz")
eqs = lucidac_circuit.to_sympy(xyz, subst_mul=True, no_func_t=True)
print(eqs)
print(latex(eqs))
rhs = [ e.rhs for e in eqs ]
x,y,z = symbols(xyz)
f = lambdify((x,y,z), rhs)
f(1,2,3) # works
sol = solve_ivp(f, (0, 10), [1,2,3])

Limitations: This omits all information about acl_select and the constant giver scheme.

reverse()[source]

Trivially “reverse engineer” a circuit based on routes. Tries to output valid python code as string.

Limitations: This omits all information about acl_select and the constant giver scheme.

class lucipy.circuits.Circuit(routes: List[lucipy.circuits.Route] = [], accept_dirty=False)[source]

The Circuit class collects all reconfigurable properties of a LUCIDAC. The class joins all features from it’s parents in a mixin idiom. (This means that, if you really want to, you also can use the individual features on its own. But there should be no need to do so)

This class provides, for instance, an improved version of the Reservoir.int() method which also sets the integrator state (initial value and time factors) in one method call.

Most of all, this function generates the final configuration format required for LUCIDAC.

However, the mixin idiom also has it’s limitations. For instance, for the Reservoir, there will be no “backwards syncing”, i.e. if routes are added manually without using the Reservoir, it’s bookkeeping is no more working. Example:

>>> circ = Circuit()
>>> circ.connect(0, 0) # defacto connects M0 first output to M0 first input
Route(uin=0, lane=0, coeff=1, iout=0)
>>> I = circ.int()
>>> print(I)
Int(id=0, out=0, a=0)
>>> circ.connect(I, I)
Route(uin=0, lane=1, coeff=1, iout=0)

One could expect in this example that circ.int() hands out the second integrator (id=1) but it does not. The registry only knows about how often it was called and does not know at all how the computing elements are used in the Routing.

int(*, id=None, ic=0, slow=False)[source]

Allocate an Integrator and set it’s initial conditions and k0 factor at the same time.

load(config_message)[source]

Loads the configuration which generate() spills out. The full circle.

randomize(num_lanes=32, max_coeff=10, seed=None)[source]

Add random configurations. This function is basically only for testing. See Routing.randomize() for the paramters.

Parameters
  • num_lanes – How many lanes to fill up. By default fills up all lanes.

  • max_coeff – Maximal coefficient magnitudes. +1 or +10 are useful values for LUCIDAC.

  • seed – For reproducability (as in unit tests), this calls random.seed. A large integer may be a suitable argument.

Returns

The instance, i.e. is chainable.

generate(skip=None, sanity_check=True)[source]

Returns the data structure required by the LUCIDAC set_config call for a given carrier, not cluster. The output of this function can be straightforwardly fed into LUCIDAC.set_circuit.

The LUCIDAC cluster is always part of the carrier level configuration:

>>> "/0" in Circuit().randomize().generate(sanity_check=False)
True
Parameters
  • skip – An entity (within LUCIDAC, at Cluster level) to skip, for instance “/M1”

  • sanity_check – Whether to carry out a sanity check (results in printouts)

write(hc, **args)[source]

Shorthand to write out configuration to hybrid controller

to_json(**args)[source]

Shorthand to get JSON. Typically you don’t need this

to_ascii_art(full_Cblock=False)[source]

Creates an “ASCII art” of the LUCIDAC including the current configuration.

Includes: U, C, I, Mblock configuration

Does not yet include:

  • ACL IN/OUT

  • Constant givers

to_pybrid_cli()[source]

Pybrid code generation including both the MIntBlock and Routing.