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)
-
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.
-
-
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 thelane
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
andiout
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 theConnection()
function.-
do_not_connect
¶ iout constant in order to not connect.
-
-
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.
-
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
-
-
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.
-
-
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 usesanity_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
or1.0
or1
: 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 withconnect()
, 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 incircuit.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). Seecoeff_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>
ontoArray<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>
ontoArray<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:
General data type check on the given routes (out of bounds, etc)
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 theroute()
oradd()
methods, which already do a good part of the checking. In general, never access theroutes
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
ori.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()
andgenerate()
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 computingI.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.
-
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)
-