Overview of pyarc2

Basic functionality

Most of the functionality of the library is exposed via the pyarc2.Instrument class. You would typically initialise a new Instrument instance and interact with ArC TWO through its methods. For example this is how you can read the current between two ArC TWO crosspoints.

>>> from pyarc2 import Instrument, find_ids
>>> ids = find_ids()
>>> if len(ids) == 0:
>>>     # no devices found
>>>     return
>>> # fw.bin is the firmware to load on ArC TWO
>>> arc = Instrument(ids[0], 'fw.bin')
>>> # perform a current read between channels 0 and 22
>>> # at 200 mV
>>> current = arc.read_one(0, 22, 0.2)
6.35432e-6

Instead of reading a single crosspoint one could read a whole word- or bit-line. For example this would return all currents along a single column.

>>> from pyarc2 import Instrument, find_ids
>>> ids = find_ids()
>>> if len(ids) == 0:
>>>     return
>>> arc = Instrument(ids[0], 'fw.bin')
>>> data = arc.read_slice(22, 0.2)
>>> print(type(data))
numpy.ndarray
>>> print(data.shape)
(32, )

In this particular case channel 22 is selected as the sink channel. Internally this is considered as a “wordline” so method read_slice() will measure the current along all bitlines with channel 22 as the corresponding low-voltage (typically grounded) channel. As a general guidance channels 0 to 15 and 32 to 47 are considered “bitline channels” (aka rows) whereas channels 16 to 31 and 48 to 64 considered “wordline channels” (aka columns). Currently this is hardcoded within libarc2 but might get fully configurable in the future. Presently this corresponds to a typical usecase of having channels arranged in 32×32 crossbar array fashion.

Result layout

With the exception of functions that operate on a single crosspoint most methods will report a block of data from the FPGA’s memory. Since almost always this value is a current value the raw ADC output from ArC TWO will be decoded into a meaningful value. As a general rule memory on the ArC TWO FPGA is subdivided in blocks of 256 bytes. This is essentially one value (4 bytes) per each one of the 64 available channels. Typically functions that operate on more than one device will return all 64 values or, where applicable, the 32 values that correspond to the selected word- or bitline. Functions that operate on the whole array will return a numpy ndarray with 32 rows and 32 columns (shape (2, 2)) to closely match the layout of a typical crossbar array.

Operation lifecycle and command buffer

In addition to read/pulse operations libarc2 also exposes a set of complex functions. These correspond to typical scenarios in a testing flow and although they do add complexity they are useful components in a testing toolkit. As these are typically sequential or time-dependent operations libarc2 will spawn a background thread to offload instructions to ArC TWO and gather results into an internal buffer. These will be made available as soon as they come. This is an example of generating a voltage ramp consisting of 2 pulses per step ranging from 0.0 V to 1.0 V with 0.1 V step and apply it on crosspoint (0, 22). Pulse width is 1 μs and interpulse 10 μs.

from pyarc2 import Instrument, find_ids, ReadAt, ReadAfter, \
    IdleMode, DataMode
import numpy as np
ids = find_ids()
if len(ids) == 0:
    return
arc = Instrument(ids[0], 'fw.bin')
# ensure all channels are detached from GND first
arc.connect_to_gnd(np.array([], dtype=np.uint64))
# generate the ramp instruction, do 2 programming pulses
# at each step, then read after each set of 2 pulses (a block)
# at arbitrary voltage (200 mV)
arc.generate_ramp(22, 0, 0.0, 0.1, 1.0, 1000, 10000, 2, \
    ReadAt.Arb(0.2), ReadAfter.Block)
# then switch all channels back to GND
arc.finalise_operation(IdleMode.SoftGND)
# and submit it for execution
arc.execute()
# the ramp is now being applied...
# start picking the data, we will read the wordline values as
# channel 22 is a word channel. `get_iter` will return an
# iterator on the internal output buffer which will block until
# either a new result is in or the operation has finished (and
# in that case the loop will break)
for datum in arc.get_iter(DataMode.Words):
    # datum now holds all the wordline currents. However
    # since only channel 22 is selected all other values
    # are NaN
    print(datum.shape)
    # (32, )
    # ...

There is quite a lot of information to unpack here. This is our first interaction with the command buffer. Internally ArC TWO has a command buffer that schedules instructions for execution. With the exception of methods that return a single value (essentially read or pulse read operations on one device/slice/array which return either a single value or numpy ndarray) all other commands are initially submitted to the command buffer for execution. This does not happen until execute() is called. At this point ArC TWO will go over each one of its command on the buffer and execute them sequentially. You can check if ArC TWO is executing instructions by using the busy() method. You can also block until all instructions have been executed by using the pyarc2.Instrument.wait() method.

The lifecyle of an operation typically consists of (a) releasing the channels from GND; (b) calling the necessary method; (c) grounding or re-floating the channels by selecting an idle mode and (d) calling execute(). On the example above step (a) is the call to connect_to_gnd(). This will connect all selected channels to GND, however rather unintuitively in this case no channels have been selected because the argument is an empty numpy ndarray. As this is internally a bitmask the empty array clears that bitmask, effectively releasing all channels from GND. The operation (step b) is the call to generate_ramp(). This is a complex ramp generator that optionally allows for reading devices after each pulse or block of pulses (as in this case). The final two arguments of generate_ramp() are essentially pythonic versions of Rust enums that are used as flags that control the operation of the ramp (see ReadAt and ReadAfter). As a general remark Rust enums are exposed in Python as classes with static fields (ReadAfter.Block is such an example) or static functions (ReadAt.Arb(0.2) in this case). Step (c) is reflected in the call of finalise_operation(). This method essentially sets the idle state of the channels. In this case we are setting all channels to 0.0 V (“soft” ground). See IdleMode for more. Note that up to this point no command has been executed. It’s only when execute() is called that ArC TWO starts to apply the queued instructions.

In the example above generate_ramp() generates a complex set of instructions which generates quite a lot of results. These are stored in the FPGA memory (what we call the internal output buffer). They can be retrieved independently of the state of the command buffer (ie. regardless if ArC TWO is busy executing or not) by using the get_iter() method. It will iterate over all the available values freeing memory slots from the FPGA memory as it goes. If a result is not yet available it will block until it is. The iterator will terminate if an operation has finished executing and all data is retrieved.

A note about types

The API of pyarc2 closely matches that of libarc2 and tries to wrap it as faithfully as possibly. To that extent pyarc2 is not a extremely high-level fully pythonic layer although certain provisions have been made to adapt to the specifics of the Python lax type system. At least on the functions exposed by Instrument the only valid arguments are floats, integers and numpy arrays. No automatic conversion is done between lists and numpy arrays.

Expanding functionality

pyarc2 can be used to implement new Python-facing APIs but using the low-level Rust codebase instead. This is especially relevant if you application has performance-critical parts that are bottlenecked by Python or if you want tighter control of the ArC TWO instruction pipeline. You can access the underlying Rust object that pyarc2 wraps via Instrument::inner() or Instrument::inner_mut() functions. However this does require some degree of familiarity with Rust, the Python C API and libarc2 itself.