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.

send(msg_type, msg={})[source]

Sets up an envelope and sends it, but does not wait for reply

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.

get_mac()[source]

Get system ethernet mac address. Is cached.

get_entities()[source]

Gets entities and determines system Mac address

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 the generate() 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).

set_config(circuit)[source]

set_config was renamed to set_circuit in later firmware versions

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 or 0/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 path 0/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 of path, see resolve_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 also start_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, use set_run() and set_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() before

this method.

run(**kwargs)lucipy.synchc.Run[source]

Alias for start_run(). See there for details.

manual_mode(to: str)[source]

manual mode control

master_for(*minions)[source]

Create a master-minion setup with at least one controlled LUCIDAC (minion). minions: type LUCIDAC

get_circuit(msg={})

Shorthand for query("get_circuit", msg), see query().

help(msg={})

Shorthand for query("help", msg), see query().

lock_acquire(msg={})

Shorthand for query("lock_acquire", msg), see query().

lock_release(msg={})

Shorthand for query("lock_release", msg), see query().

net_get(msg={})

Shorthand for query("net_get", msg), see query().

net_reset(msg={})

Shorthand for query("net_reset", msg), see query().

net_set(msg={})

Shorthand for query("net_set", msg), see query().

net_status(msg={})

Shorthand for query("net_status", msg), see query().

one_shot_daq(msg={})

Shorthand for query("one_shot_daq", msg), see query().

ping(msg={})

Shorthand for query("ping", msg), see query().

reset_circuit(msg={})

Shorthand for query("reset_circuit", msg), see query().

stop_run(msg={})

Shorthand for query("stop_run", msg), see query().

sys_ident(msg={})

Shorthand for query("sys_ident", msg), see query().

sys_reboot(msg={})

Shorthand for query("sys_reboot", msg), see query().

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 calling data() 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 on np.array(run.data()) will most likely result in an IndexError: 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”)