Lucipy Synchronous Client¶
An Synchronous Hybrid Controller Python Client for REDAC/LUCIDAC.
This is a minimal python client with focus on interactive python prompt usage.
This client implementation does not feature strong typing, dataclasses, asynchronous functions. Instead, it implements a blocking API and tries to mimic the way how the Model-1 Hybrid Controller interface worked (in particular in the PyAnalog research code).
This is a single file implementation focussing on portability and minimal
dependencies. If you have pyserial
installed, it will be used to allow
serial connections, otherwise it runs with python “batteries included” to
connect to TCP/IP targets.
In particular, there are no dependencies on any other lucipy module, except
detect
. In particular, this code is completely independent from
the circuits
module which can serve to simplify to set up the
data structure required by LUCIDAC.set_circuit()
.
The main class to use from this module is LUCIDAC
.
-
class
lucipy.synchc.
LUCIDAC
(endpoint_url=None, auto_reconnect=True)[source]¶ This kind of class is known as HybridController in other codes. It serves as primary entry point for the lucipy code.
The constructor has a variety of variants how to be called:
- Parameters
endpoint_url –
If no endpoint (either as string or Endpoint class instance) is provided, will lookup the environment variable
LUCIDAC_ENDPOINT
.If neither an endpoint nor the environment variable is set, autodetection is applied and the first connection is chosen. Note that if no LUCIDAC is attached via USB serial, the zeroconf detection will require a few hundred milliseconds, depending on your network.
For details, see Endpoints and Autodetection.
auto_reconnect – Whether reconnect in case of loss connection (TODO: Move to parameter
?reconnect
in the endpoint URL syntax)
-
hc_mac
¶ Ethernet Mac address of Microcontroller, required for the circuit entity hierarchy
-
run_config
¶ Storage for stateful preparation of runs.
-
daq_config
¶ Storage for stateful preparation of runs.
-
circuit_options
¶ Storage for additional configuration options which go next to each set_circuit call
-
close
()[source]¶ Closes connection to LUCIDAC. This will close the socket and should be used when destructing the object. In most scripts it should be not neccessary to call
close
explicitely becaue the connection is considered ephermal for the script runtime. However, in unit testing conditions it might be interesting to close a connection in order to be able to reuse the socket ports later.
-
query
(msg_type, msg={}, ignore_run_state_change=True)[source]¶ Sends a query and waits for the answer, returns that answer
-
slurp
()[source]¶ Flushes the input buffer in the socket.
Returns all messages read as a list. This is useful when we lost the remote state (wrongly tracked) and have to clean up.
Note
Calling this method should not be possible in regular usage. In particular, TCP connections are normally “clean”. In contrast, serial terminals are ephermal from the Microcontrollers point of view, so it might be neccessary to flush the connection in particular at beginning of a connection.
-
login
(user=None, password=None)[source]¶ Login to the system. If no credentials are given, falls back to the one from the endpoint URL.
-
static
determine_idal_ic_time_from_k0s
(mIntConfig)[source]¶ Given a MIntBlock configuration, which looks like
[{k:1000,ic:...},{k:10000,ic:...}...]
, determines the ideal ic time, in nanoseconds
-
set_circuit
(carrier_config, **further_commands)[source]¶ This sets a carrier level configuration. The
circuits
module and in particular thegenerate()
method can help to produce these configuration data structures.Typically a configuration looks a bit like
{"/0": {"/U": [....], "/C":[....], ...}}
, i.e. the entities for a single cluster. There is only one cluster in LUCIDAC.- Parameters
reset_before – Reset circuit configuration on device before setting the new one. Pass
False
for incrementally updating the configuration.sh_kludge – Make a SH Track-Inject cycle (after potential reset) before applying the configuration on the LUCIDAC.
calibrate... – Perform the device calibration scheme. Currently disabled.
Note
This also determines the ideal IC time if that has not been set before (either manually or in a previous run).
-
static
resolve_path
(path, config={})[source]¶ Provides the notational sugar for
set_by_path()
.The path must point to an entity and not to an element. An entity corresponds to a class on the firmware side which can receive configuration “elements”. The path can be provided as string, such as
/0/C
or0/C
or as array of strings such as["0", "C"]
.Since this client has no information about the entity structure and the element configuration, it cannot accept paths which also include elements, such as
/0/M0/elements/0/ic
for setting the first integrator value. However, as a “nicety” it allows to generate a nested directory structure by introducing a “double slash” notation, which is/0/M0//elements/0/ic
, or as array,["0", "M0", "", "elements", "0", "ic"]
. Internally, such a call is then translated to the path0/M0
and the configuration dictionary is wrapped as{ "elements": {"0": {"ic": actual_config } } }
. This way, one can set actual elements very conveniently.>>> LUCIDAC.resolve_path("/0/M0") (['0', 'M0'], {}) >>> LUCIDAC.resolve_path("foo/bar", "baz") (['foo', 'bar'], 'baz') >>> LUCIDAC.resolve_path("foo/bar//baz", "kaz") (['foo', 'bar'], {'baz': 'kaz'}) >>> LUCIDAC.resolve_path("foo/bar//bax/bay/baz", {"bir":"bur"}) (['foo', 'bar'], {'bax': {'bay': {'baz': {'bir': 'bur'}}}})
In the following real world example, all notations are equvialent:
>>> a = LUCIDAC.resolve_path(["0", "M0"], {"elements":{"0": {"ic":0.23, "k":100} } }) >>> b = LUCIDAC.resolve_path("/0/M0", {"elements":{"0": {"ic":0.23, "k":100} } }) >>> c = LUCIDAC.resolve_path("/0/M0//elements/0", {"ic":0.23, "k":100}) >>> print(a) (['0', 'M0'], {'elements': {'0': {'ic': 0.23, 'k': 100}}}) >>> assert a == b and b == c
-
set_by_path
(path, config)[source]¶ Set element configuration by path.
This is a fine-granular alternative to
set_circuit()
. For the meaning ofpath
, seeresolve_path()
.Note that the path is always relative to the carrier, not the cluster. That means most of the time you want to address the cluster just by ascending with the entity path “0”.
When providing entities as list, do not prepend entities with a slash, i.e. do not write
[..., "/M0", ...]
but just[..., "M0", ...]
. Slash-prefixed entitiy names happen to take place only in the configuration dictionary (second parameter).All these following lines are equivalent formulations for the same aim:
>>> hc = LUCIDAC("emu:/") >>> hc.set_by_path(["0", "M0"], {"elements":{0: {"ic":0.23, "k":100} } }) >>> hc.set_by_path("/0/M0", {"elements":{0: {"ic":0.23, "k":100} } }) >>> hc.set_by_path("/0/M0//elements/0", {"ic":0.23, "k":100}) >>> hc.get_circuit()["config"]["/0"]["/M0"]["elements"]["0"] {'ic': 0.23, 'k': 100}
-
signal_generator
(dac=None)[source]¶ - Args dac
Digital analog converter outputs, as there are two a list with two floats (normalized [-1,+1]) is expected
-
set_op_time
(*, ns=0, us=0, ms=0, sec=0, k0fast=0, k0slow=0, unlimited=False)[source]¶ Sets OP-Time with clear units. Returns computed value, which is just the sum of all arguments.
Consider the limitations reported in
start_run()
.Note that this function signature is somewhat comparable to python builtin datetime.timedelta, however Python’s timedelta has only microseconds resolution which is not as highly-resolved as in LUCIDAC.
- Parameters
ns – nanoseconds
us – microseconds
ms – milliseconds
sec – seconds
k0fast – units of k0fast, i.e. the fast integrators. Measuring time in this units means that within the time
k0fast=1`
an integrator integrates a constant 1 from initial value 0 to final value 1.k0slow – units of k0slow. A slow integrators computes within time
k0slow=1
a constant 1 from initial vlaue 0 to final value 1.unlimted – Infinite OP-Time (will only stop once being send the
stop_run
command)
-
allowed_sample_rates
= [1, 2, 4, 5, 8, 10, 16, 20, 25, 32, 40, 50, 64, 80, 100, 125, 160, 200, 250, 320, 400, 500, 625, 800, 1000, 1250, 1600, 2000, 2500, 3125, 4000, 5000, 6250, 8000, 10000, 12500, 15625, 20000, 25000, 31250, 40000, 50000, 62500, 100000, 125000, 200000, 250000, 500000, 1000000]¶ Sample rates per second accepted by the system.
-
set_daq
(*, num_channels=None, sample_op=None, sample_op_end=None, sample_rate=None)[source]¶ - Parameters
num_channels – Data aquisition specific - number of channels to sample. Between 0 (no data aquisition) and 8 (all channels)
sample_op – Sample a first point exactly when optime starts
sample_op_end – Sample a last point exactly when optime ends
sample_rate – Number of samples per second. Note that not all numbers are supported. A client side check is performed.
-
set_run
(*, halt_on_external_trigger=None, halt_on_overload=None, ic_time=None, op_time=None, unlimited_op_time=None, streaming=None, repetitive=None)[source]¶ Set basic configuration for any upcoming run. This will be the baseline for any run-specific configuration. See also
set_op_time()
for a more practical way of setting the op_time. See alsostart_run()
for actually starting a run.- Parameters
halt_on_external_trigger – Whether halt the run if external input triggers
halt_on_overload – Whether halt the run if overload occurs during computation
ic_time – Request time to load initial conditions, in nanoseconds. This time depends on the k0 factors. However, it is rarely neccessary to tune this parameter, once useful values have been used. If not set, a value derived from the (first time in object lifetime) configuration set will be used.
op_time – Request time for simulation, in nanoseconds. This is the most important option for this method. Note that by a current limitation in the hardware, only op_times < 1sec are supported.
-
start_run
(clear_queue=True, **run_and_daq_config) → lucipy.synchc.Run[source]¶ Start a run on the LUCIDAC. A run is a IC/OP cycle. See
Run
for details. In order to configurer the run, useset_run()
andset_daq()
before calling this method.- Returns
a Run object which allows to read in the DAQ data.
- Parameters
clear_queue –
Clear queue before submitting, making sure any leftover repetitive run is wiped. This is equivalent to calling
stop_run()
beforethis method.
-
run
(**kwargs) → lucipy.synchc.Run[source]¶ Alias for
start_run()
. See there for details.
-
class
lucipy.synchc.
Run
(hc)[source]¶ A run is a IC-OP-HALT sequence on a LUCIDAC with measurement (data aquisition with the analog-digital-converters) ongoing during the OP phase. Therefore, a run generates data. This results in the remote site to send data “out of bounds”, i.e. without that we have requested these data. The job of this class is to proper model how to receive these data. An instance of this class is returned by
LUCIDAC.start_run()
. This instance is properly “handled off” by callingdata()
and then wiping it.-
next_data
() → Iterator[List[float]][source]¶ Reads next dataset from DAQ (data aquisiton) which is streamed during the run. A call to this function yields a single dataset once arrived. It returns when the run is finished and raises a LocalError if it doesn’t properly stop.
The shape of data returned by this call is determined by the requested number of DAQ channels, which is between 0 (= no data will be returned at all, not even an empty directory per dataset) and 8 (= all channels). That is, the following invariant holds:
>>> lines = run.next_data() >>> assert all(hc.daq_config["num_channels"] == len(line) for line in lines)
This invariant is also asserted within the method.
-
data
(empty_is_fine=False) → List[float][source]¶ Returns all measurement data (evolution data on the ADCs) during a run.
This is basically a synchronous wait until the run finishes.
The shape of data returned by this call is basically
NUM_SAMPLING_POINTS x NUM_CHANNELS
. Since this is a uniform array, users can easily use numpy to postprocess and extract what they are interested in. Typically, you want to trace the evolution of particular channels, for instance>>> data = np.array(run.data()) >>> x, y, z = data[:,0], data[:,1], data[:,2] >>> plt.plot(x)
See also
next_data()
and example application codes.- Parameters
empty_is_fine – Whether to raise when no data have been aquired or happily return an empty array. Raising a
LocalError
(i.e. the default option) is most likely more what you want because you then don’t have to write error handling code for an empty array. Otherwise a later access on something onnp.array(run.data())
will most likely result in anIndexError: index 0 is out of bounds for axis 0 with size 0
or similar.
-
-
class
lucipy.synchc.
LUCIGroup
(master: lucipy.synchc.LUCIDAC, *minions: lucipy.synchc.LUCIDAC)[source]¶ Group of LUCIDACs in a master/minion setup. Usage is like
>>> gru = LUCIDAC("tcp://foo") >>> kevin = LUCIDAC("tcp://bar") >>> bob = LUCIDAC("tcp://baz") >>> group = LUCIGroup(gru, kevin, bob) >>> group.set_circuit(...) >>> group.start_run() ...
-
exception
lucipy.synchc.
RemoteError
(recv_envelope)[source]¶ A RemotError represents a returned error message by the endpoint. Typically this means the LUCIDAC HybridController could not decode a message, there is some typing or logic problem.
The error message from the server always contains an integer error code and a human readable string error message.
In order to trace the error at the firmware side, both the code and string can be helpful. You can grep the firwmare code with the string. For the code, you have to understand how they are computed server side or consult the firmware documentation.
-
exception
lucipy.synchc.
LocalError
[source]¶ A LocalError represents a logic flaw at the client side, i.e. within the lucipy code. It did not expect some message from the server or could not deserialize messages received.
Note that next to LocalErrors you can always receive things such as SocketError, OSError or similiar low level connection problems (for instance: OSError “no route to host”)