ArC2Control emodules
Introduction
ArC TWO Control Panel provides a mechanism for users to create their own experiments using ArC TWO. These are typically specialised control panels that are registered during program startup and are available from the main ArC TWO Control Panel Experiment dropdown menu. We call the control panels external modules or emodules. This document serves as a tutorial to help you develop your own modules.
An emodule is essentially a standalone Python project that exposes a few interfaces that are compatible with the ArC TWO Control Panel. ArC2Control emodules should be placed at the corresponding data directory for your operating system. On Windows these directories are (higher to lower priority)
%APPDATA%\arc2control\arc2emodules
C:\ProgramData\arc2control\arc2emodules
PYTHONDIR\arc2control
PYTHONDIR\data\arc2control
and on Linux these are (higher to lower priority)
~/.local/share/arc2control/arc2emodules
/usr/local/share/arc2control/arc2emodules
/usr/share/arc2control/arc2emodules
Regardless of the path you choose an empty __init__.py
file must be
created under arc2emodules
to allow custom modules to be loaded from
there.
A typical tree structure for an emodule called testmodule
located
in %APPDATA%\arc2control
would look like this
testmodule
arc2emodules
├── __init__.py ← this must exist for any emodule to be loaded
└── testmodule ← toplevel directory of our emodule
├── __init__.py ← module metadata and identifiers
├── testmodule.py ← module code with entry-point
└── another.py ← additional python source used by this module
Of course modules can span multiple files, can have compilation steps and anything else that might be required for your use case.
Anatomy of an emodule
Note
The module system for ArC TWO Control Panel heavily depends on the Qt framework and its Python bindings. Although an attempt has been made to make this guide as agnostic as possible a degree of familiarity with the Qt framework (in any language) would go a long way.
For an emodule to be loaded into ArC2Control two things must be true: (a) Its
__init__.py
must specify the entry point of the module and (b) the entry
point must derive from BaseModule
which
essentially is a custom Qt6 QWidget with some extra quality of life improvements
bolted in.
Going back to our example testmodule
emodule this is how a correct
__init__.py
would look like
# Human readable name for the module - No whitespace
MOD_NAME = 'TestModule'
# A unique identifier
MOD_TAG = 'TSM00'
# A descriptive line of text
MOD_DESCRIPTION = 'Demonstrate the emodule functionality'
# Always False for modules residing outside the arc2control repository
BUILT_IN = False
# define the main class for this emodule; the entry point
from .testmodule import TestModule
ENTRY_POINT = TestModule
It should be mentioned that the MOD_TAG
field must be a unique identifier
for your module. There are no checks for identifier clash allowing you to
possibly override built-in modules by reimplementing them as emodules to add
or alter functionality. It is recommended that you avoid overriding internal
tags unless you really know what you are doing. Things might fail spectacularly
otherwise.
Once __init__.py
is filled in you can proceed into developing the logic of
your emodule. In this example the logic is implemented in
testmodule/testmodule.py
.
testmodule/testmodule.py
from PyQt6 import QtWidgets
from arc2control.modules.base import BaseModule
from . import MOD_NAME, MOD_TAG, MOD_DESCRIPTION
class TestModule(BaseModule):
def __init__(self, arc, arcconf, vread, store, cells, mapper, parent=None):
# calling superclass constructor with all the arguments
BaseModule.__init__(self, arc, arcconf, vread, store, MOD_NAME, \
MOD_TAG, cells, mapper, parent=parent)
# build the UI
self.setupUi()
# make the button do something
self.runButton.clicked.connect(self.onRunClicked)
def setupUi(self):
self.setObjectName('TestModuleWidget')
self.gridLayout = QtWidgets.QGridLayout(self)
self.gridLayout.setContentsMargins(0, 0, 0, 0)
spacer00 = QtWidgets.QSpacerItem(0, 20, \
QtWidgets.QSizePolicy.Policy.Expanding, \
QtWidgets.QSizePolicy.Policy.Minimum)
self.gridLayout.addItem(spacer00, 2, 1, 1, 1)
spacer01 = QtWidgets.QSpacerItem(20, 0, \
QtWidgets.QSizePolicy.Policy.Minimum, \
QtWidgets.QSizePolicy.Policy.Expanding)
self.gridLayout.addItem(spacer01, 7, 0, 1, 1)
self.horizontalLayout = QtWidgets.QHBoxLayout()
spacer02 = QtWidgets.QSpacerItem(40, 20, \
QtWidgets.QSizePolicy.Policy.Expanding, \
QtWidgets.QSizePolicy.Policy.Minimum)
self.horizontalLayout.addItem(spacer02)
self.runButton = QtWidgets.QPushButton(self)
self.runButton.setObjectName("runButton")
self.horizontalLayout.addWidget(self.runButton)
spacer03 = QtWidgets.QSpacerItem(40, 20, \
QtWidgets.QSizePolicy.Policy.Expanding, \
QtWidgets.QSizePolicy.Policy.Minimum)
self.horizontalLayout.addItem(spacer03)
self.gridLayout.addLayout(self.horizontalLayout, 8, 0, 1, 2)
self.label = QtWidgets.QLabel(self)
self.label.setText("Example label")
self.gridLayout.addWidget(self.label, 0, 0, 1, 1)
def onRunClicked(self):
print('Clicked!')
Most of the code above deals with setting up the UI, it is fairly standard Qt
code. The most important part is probably the constructor of the class which
takes a bunch of arguments. These are populated by ArC2Control when you
activate the new panel on the main GUI so you should not need to deal with them
directly. They will be connected to properties by the superclass,
BaseModule
and will be available to use from
within your new emodule.
Starting ArC2Control again a new entry, TestModule, should be available from the experiment dropdown menu. Clicking add will activate the following panel and clicking Run will print “Clicked!” on your terminal.

A basic emodule panel
As it is now the Run button does not do anything useful. We will modify it to perform a current read on the specified crosspoint if an ArC TWO is available.
onRunClicked
def onRunClicked(self):
# self.cells is a set containing the current selection, we will only
# one if available
try:
cell = list(self.cells)[0]
except IndexError:
# nothing selected; break
return
# get wordline and bitline from the cell
(w, b) = (cell.w, cell.b)
# convert it into an ArC TWO channel using the self.mapper.wb2ch
# wb2ch is a dict property populated automatically by ``BaseModule``
# and points to the current channel mapper.
(high, low) = self.mapper.wb2ch[w][b]
# the current global read-out voltage; populated automatically by
# ``BaseModule``.
vread = self.readoutVoltage
# self.arc is populated by ``BaseModule`` and will return ``None`` if
# no ArC TWO is currently connected.
if self.arc is not None:
# do a current measurement
current = self.arc.read_one(low, high, vread)
# set channels back to the currently configured idle mode
# (again populated automatically by ``BaseModule``)
self.arc.finalise_operation(self.arc2Config.idleMode)
print('I = %g A' % current)
The above example performs a current read on the first selected device and prints the result in the terminal. The ArC TWO related functionality is exposed by pyarc2 so consult its documentation for specific information.
Please note that ArC TWO doesn’t have any concept of crosspoints. It will do
operations on any of the 64 channels which can be interconnected arbitrarily.
In order to convert between abstract channels and something more meaningful
like word- and bitlines a mapping mechanism is provided. Depending on your
applications and active daughterboard different mappings might be relevant. On
the code above the active cell coordinates are converted to the underlying
channel numbers via the mapper
property available on all subclass of
BaseModule
. The superclass code takes care of any changes in the mapper
introduced by the user automatically so that self.mapper
will always point
to the currently active mapping scheme. You can read more about the mapper API
or how to crate new mappings on a subsequent section.
Backgrounding operations
Warning
You should only run only one background operation at a time. Although ArC TWO provides locking semantics there is no fencing mechanism for synchronisation and running two command queues at the same time will lead to resulting data being received out of order.
The above example presents a relatively simplistic way of interacting with ArC
TWO. However for more complex operations such as a voltage ramp, for example,
implementing the logic of the operation in the callback will result into the
main GUI blocking and becoming unresponsive (this is a common issue with UI
toolkits in general). In order to avoid that and have some degree of
interactivity we should introduce background operations. These essentially
are background threads that are I/O bound (ie. slow but not very
computationally expensive). Qt provides quite a sophisticated threading library
and you can in fact start implementing operations right away with QThreads,
however you still need to monitor ArC2Control for changes in read-out voltage,
selections, etc. In order to avoid repetition a simple
BaseOperation
class is provided. Similar to
BaseModule
it will abstract away some
boilerplate code and will expose several primitives as properties. You only
need to implement its run()
method and start the operation as any other QThread. Let’s add such a class to
testmodule/testmodule.py
.
TestModuleOperation
added to testmodule/testmodule.py
import time
from arc2control.modules.base import BaseOperation
from PyQt6.QtCore import pyqtSignal
# signal emitted when a new value is received
newValue = pyqtSignal(float)
# signal emitted when the process has been completed
finished = pyqtSignal()
class TestModuleOperation(BaseOperation):
def __init__(self, parent):
super().__init__(parent=parent)
def run(self):
# pickup the first selected cell
try:
cell = list(self.cells)[0]
vread = self.readoutVoltage()
(high, low) = self.mapper.wb2ch[cell.w][cell.b]
except IndexError:
# or exit if nothing is selected
self.finished.emit()
return
for _ in range(10):
# do a current measurement
current = self.arc.read_one(low, high, vread)
# communicate the new value to the UI
self.newValue.emit(current)
self.arc.finalise_operation(self.arc2Config.idleMode)
# artificial wait time
time.sleep(1)
self.finished.emit()
There are two particular points of note on the example above. First, the run
function which implements the logic of the long running operation. This essentially
does 10 subsequent readings on the first selected device with an 1 sec delay between
them. Second it’s the introduction of custom signals as class attributes. Signals are
used to communicate the status of the background operation to the main UI. This is
similar to built-in signals such as clicked
, triggered
, etc. that are exposed
by regular UI components but they carry custom values instead. In order to pick up
a new value from the background thread one needs to connect the signal to a python
function. Let’s update our onRunClicked
function of TestModule
to launch a
measurement thread.
onRunClicked
to launch and monitor a threaddef onRunClicked(self):
# callback called whenever a new value is produced
def onNewValue(value):
print('I = %g A' % value)
# callback called when the thread exits
def onFinished():
self.operation.wait()
self.operation = None
try:
if self.operation is not None:
# an operation is currently running; abort
return
except AttributeError:
self.operation = None
self.operation = TestModuleOperation()
self.operation.newValue.connect(onNewValue)
self.operation.finished.connect(onFinished)
self.operation.start()
The code above will launch TestModuleOperation
in background. For
illustration purposes we connect the two signals emitted to two nested
functions, although these can be any type of function reference. It is
important to note that we should make sure that we should only launch one
operation at a time to avoid concurrent access to ArC TWO. In order to do that
we start the operation only when self.operation
is not defined at all or
it’s None
. Every time a new value is produced by the thread the
onNewValue
function is called which subsequently prints the value on the
terminal. When the thread is finished the onFinished
function is called.
This will first wait for the thread to actually terminate (ie. the run
function of the thread to return) and when that’s done the reference to
the operation is set to None
and the thread is finally dropped. A new
operation can now be launched if required.
Storing data
Note
You can read about the storage system in detail on a follow-up section. Familiarity with HDF5 format or the h5py module will also be helpful.
In all of the examples above we just print the data to the terminal, we do not
actually store any data. ArC2Control uses an HDF5-based file format to store
all data produced by modules, either buit-in or external. Every emodule has
access to the currently enabled dataset via the datastore
property. As
with other properties these are updated automatically whenever a change has
occurred on the main GUI.
Data is stored on disk on a single HDF5 file with an hierarchical format. The data file contains all biasing history for every device as well as the latest current and voltage values of the full crossbar. Apart from that the datastore supports two types of experiment: (a) crosspoint-bound experiments that are specific to a single crosspoint and (b) synthetic experiments that span multiple crosspoints. Scenario (a) would probably be the most common if you only do individual device characterisation.

Structure of the HDF5 data file used by ArC2Control
The data file is organised as shown in the figure above. The toplevel groups as
well as the voltage
and current
crossbar view are always created
automatically when a new dataset is created. Generally you should not need to
create HDF5 groups, or even datasets, manually as calling
make_wb_table()
or
make_synthetic_table()
will create the
necessary groups and tables automatically. Of course, if you know what you are
doing, the actual h5py.Dataset is available through the
dataset()
method. Knowing these let’s
modify TestModuleOperation
to save the data to the datastore at the end
of the operation.
TestModuleOperation
to save data at the end of the
process.import time
import numpy as np
from arc2control.modules.base import BaseOperation
from PyQt6.QtCore import pyqtSignal
from . import MOD_TAG
# signal emitted when a new value is received
newValue = pyqtSignal(float)
# signal emitted when the process has been completed
finished = pyqtSignal()
# this a numpy dtype to store data
_DTYPE = [('current', '<f4')]
class TestModuleOperation(BaseOperation):
def __init__(self, parent):
super().__init__(parent=parent)
def run(self):
# pickup the first selected cell
try:
cell = list(self.cells)[0]
vread = self.readoutVoltage()
(high, low) = self.mapper.wb2ch[cell.w][cell.b]
except IndexError:
# or exit if nothing is selected
self.finished.emit()
return
data = np.empty(shape=(10, ), dtype=_DTYPE)
for idx in range(10):
# do a current measurement
current = self.arc.read_one(low, high, vread)
data[idx] = current
# communicate the new value to the UI
self.newValue.emit(current)
self.arc.finalise_operation(self.arc2Config.idleMode)
# artificial wait time
time.sleep(1)
self.storeData(cell.w, cell.b, data)
self.finished.emit()
def storeData(self, word, bit, data):
store = self.datastore
# make a new table to store the data
dset = store.make_wb_table(word, bit, MOD_TAG, \
data.shape, _DTYPE)
# allocate values column-wise; in this case one column
for field in _DTYPE:
dset[:, field] = data[field]
The code above should be relatively straightforward: we create a numpy array to
store the readings and at the end of the process we commit it to the dataset by
column-wise assignment. It’s probably important to mention that HDF5 datasets
are strongly typed and in h5py
this is done via numpy dtypes for numpy
structured arrays. It is convenient, in this case, to allocate an array to
store the data and then broadcast it over the dataset. Of course, that’s only
one way to store data; you could, for instance, allocate a dataset at the start
of the process and save the data line-by-line which would work equally well.
The datasets require an identifier, and although you could probably use
anything, it is recommended that you use the MOD_TAG
defined when
initialising the emodule.
Communicating changes to the ArC2Control UI
So far we’ve seen how to apply an operation on a selected crosspoint and store
the resulting data in the datastore. What you may have noticed, however, the
main UI (essentially the graph panel) does not get updated with the latest
data. This happens because the main UI does not have any insight into the inner
workings of each module. In order to communicate changes to the UI and also
update the global history of a device a module would need to emit some
signals that will be picked up by the UI and update the interface. Similar to
the custom signals, finished
and newValue
, that we defined in our
TestModuleOperation
the main ArC2Control UI also defines some signals that
are globally available to modules. The full list of the
signals defined from ArC2Control is available although for emodule development
the most useful ones are (i) valueUpdate()
to update
the current voltage and current status of a crosspoint with a single value;
(ii) valueBulkUpdate()
that does the same using
numpy arrays for multiple data updates and (iii)
arc2control.signals.dataDisplayUpdate()
to request a refresh of the
plotting panels on the main UI. Let’s update our run
function to update
the crosspoint status on every loop and request a data display refresh at
the end.
TestModuleOperation
to communicate data changes
to the main ArC2Control GUI.import time
import numpy as np
from arc2control.modules.base import BaseOperation
from arc2control.signals import valueUpdate, dataDisplayUpdate
from arc2control.h5utils import OpType
from PyQt6.QtCore import pyqtSignal
from . import MOD_TAG
# signal emitted when a new value is received
newValue = pyqtSignal(float)
# signal emitted when the process has been completed
finished = pyqtSignal()
# this a numpy dtype to store data
_DTYPE = [('current', '<f4')]
class TestModuleOperation(BaseOperation):
def __init__(self, parent):
super().__init__(parent=parent)
def run(self):
# pickup the first selected cell
try:
cell = list(self.cells)[0]
vread = self.readoutVoltage()
(high, low) = self.mapper.wb2ch[cell.w][cell.b]
except IndexError:
# or exit if nothing is selected
self.finished.emit()
return
data = np.empty(shape=(10, ), dtype=_DTYPE)
for idx in range(10):
# do a current measurement
current = self.arc.read_one(low, high, vread)
data[idx] = current
# communicate the new value to the UI
self.newValue.emit(current)
self.arc.finalise_operation(self.arc2Config.idleMode)
# and update the crosspoint status; we've put 0.0
# as a pulse width since this is a Read operation
# that's also the reason why ``vpulse`` and ``vread``
# are identical
valueUpdate.emit(cell.w, cell.b, current, vread, 0.0,
vread, OpType.READ)
# artificial wait time
time.sleep(1)
# finally request a display refresh for our crosspoint
dataDisplayUpdate.emit(cell.w, cell.b)
self.storeData(cell.w, cell.b, data)
self.finished.emit()
def storeData(self, word, bit, data):
store = self.datastore
# make a new table to store the data
dset = store.make_wb_table(word, bit, MOD_TAG, \
data.shape, _DTYPE)
# allocate values column-wise; in this case one column
for field in _DTYPE:
dset[:, field] = data[field]
In the updated example the valueUpdate()
is emitted
every time a reading is done. Our module has relatively slow repetition so
updating values at every step is acceptable. However if you are pulling large
amounts of data quickly from ArC TWO then it would probably make better sense
to buffer the data and use valueBulkUpdate()
on
specific intervals instead. Finally, after the loop has finished,
arc2control.signals.dataDisplayUpdate()
is called to force refresh of the
main UI graphing panel. You can do data display updates faster or slower depending
on the amount and rate of data you are reading from ArC TWO. You should always
keep in mind that very frequent UI updates will result in your main UI thread
consuming too much CPU time so you should throttle the data display refreshes at
a reasonable level. A few displays per second is probably fine, but many thousands
will lead to massive slowdowns on the main UI thread.
Displaying data
One additional facility provided by ArC2Control emodules is the capability to display custom display and analysis widgets tied to a specific module. In the example below double-clicking on the CurveTracer entry in the device history tree will bring up a representation of the data for this specific experiment.

Double clicking a module name on the device history tree will bring up a suitable data display widget
Data display widgets can be any type of widget that derives from QWidget
.
That means you can have full featured visualisation and analysis tools for your
custom experiments. This facility is typically provided by the static method
display
provided by the main panel of the module, in this case our
TestModule
. The method must return anything that subclasses QWidget
.
or just a QWidget
. In this exampe we will add a display
method that
plots our readings.
display
function to TestModule
to plot acquired
data.from PyQt6 import QtWidgets
from arc2control.modules.base import BaseModule
from . import MOD_NAME, MOD_TAG, MOD_DESCRIPTION
import pyqtgraph as pg
class TestModule(BaseModule):
# rest of the implementation omitted for brevity
@staticmethod
def display(dataset):
current = dataset['current']
wdg = QtWidgets.QWidget()
layout = QtWidgets.QVBoxLayout()
gv = pg.GraphicsLayoutWidget()
plot = gv.addPlot()
plot.getAxis('left').setLabel('Current', units='A')
plot.getAxis('bottom').setLabel('Reading #')
plot.showGrid(x=True, y=True)
plot.plot(range(0, current.shape[0]), current, pen='r')
layout.addWidget(gv)
wdg.setLayout(layout)
return wdg
The only argument to the display
method is the dataset associated with
entry the user selected on the main GUI which is exactly the same dataset we
created with the associated functions before. In this specific example above we
construct a QWidget
, add a layout and a graphing widget. We use pyqtgraph
for plotting, which is the same library used by ArC2Control and it’s always
available to the modules. Of course, depending on your requirements, you can
use matplotlib or any other visualisation package. When the user activates
the entry on the device history tree the widget returned by display
will
be put on window and displayed to the user, pretty much identically to the
CurveTracer figure shown above.
Logging information
Regardless of the complexity of your module you will eventually have to log
information. The humble print
can get you quite far but you might need
something more structured. Both arc2control.modules.base.BaseModule
and arc2control.modules.base.BaseOperation
expose the logger
property that plugs into the main ArC2Control logging facility. You can then
log information to the console in varying degrees of severity. The verbosity of
the messages is controlled by an environment variable set before launching
ArC2Control: ARC2CTRL_LOGLEVEL
which can be any of critical
, error
,
warn
, info
, debug
with increasing degree of verbosity and
decreasing severity. At any given logging level only message of equal or higher
severity will be displayed. The default logging level is warn
. With that in
mind you can log information at varying severity levels like this
>>> self.logger.error('An error message; this will typically halt the module')
[ERROR] [TSM00] An error message; this will typically halt the module
>>> self.logger.warn('Something went wrong; results may be unreliable')
[WARNING] [TSM00] Something went wrong; results may be unreliable
>>> self.logger.info('Useful information')
[INFO] [TSM00] Useful information
>>> self.logger.debug('Detailed info about the module runtime')
[DEBUG] [TSM00] Detailed info about the module runtime
You can read more about Python’s logging facilities in the official documentation.
Final words
This concludes our tour of the module facility of ArC2Control. You can use this tooling to create your own experiments, simple or complex, to tailor the capabilities of ArC TWO to your own needs. You can find more details on the data store, channel mapping and signal APIs in the following sections.
API Reference
- class arc2control.modules.base.BaseModule(arcref, arcconf, vread, store, name, tag, cells, mapper, parent=None)
Base Module for all ArC2Control plugins. A valid ArC2 plugin _MUST_ derive from this class to be properly loaded on startup. The base class will track all UI and instrument changes and exposes the relevant values via properies and methods. The constructor for the module will be typically called with the correct arguments when the user clicks the “Add” button on the experiment panel of ArC2Control
- Parameters:
arcref – A reference to the currently connected ArC TWO
arcconf – The current ArC TWO configuration
vread – Currently configured read voltage
store – A reference to the currently opened datastore
name – The name of this module
tag – A short tag desribing this module
cells – A reference to the current crosspoint selection
mapper – A reference to the current channel mapper
- addSerializableType(typ, getter, setter)
Register the setters and getters of a non standard widgets that should be serialized with
exportToJson
. Typicallytyp
would be a custom widget andsetter
andgetter
are functions of that custom widget that load and set its state. Custom widgets must be registered with this function in order to be properly serialised to and retrieved from a file.- Parameters:
typ – The widget Python type
getter – The
get
function to retrieve its valuesetter – The
set
function to set its value
- property arc
A reference to the currently active ArC TWO instrument, or None, if no connection exists
- property arc2Config
The current ArC TWO configuration
- property cells
The currently selected cells
- property datastore
A reference to the current datastore. See
H5DataStore
.
- property description
Description of the operation of this module. This is typically displayed under the panel name in the main ArC2Control UI. Subclasses must implement this if they need to have a description visible (by default it’s empty).
- exportToJson(fname)
Export all the adjustable children of this module to a JSON file. All individual widgets must set a unique name using
setObjectName
for this to work properly. Standard Qt Widgets and custom widgets that are made up from standard widgets are dealed with automatically. For bespoke widgets, these must be registered withaddSerializableType()
.- Parameters:
fname (str) – The filename of the JSON file to export current module’s values.
- property fullModuleName
The fully qualified python class name
- loadFromJson(fname)
Load panel settings from a JSON file. Most common widget values are stored automatically but if custom widgets are present the subclass must register a setter and a getter method for the class using
addSerializableType()
.- Parameters:
fname (str) – The filename to load values from
- property logger
Returns the appropriately format logger for this module. See the python logging documentation for more.
- property mapper
The current bit/word to channel mapping configuration
- property readoutVoltage
The active read-out voltage
- class arc2control.modules.base.BaseOperation(parent=None)
A standard background operation. This is what you probably need to use if you running a long operation using ArC2. It will connect all relevant signals (configuration, mapper, cell selection, etc.) and expose their corresponding values via properties. When a thread based on this operation is started the
run
method will be called and must be implemented by all subclasses of this class.- property arc
Reference to the currently connected ArC2 instrument (if any)
- property arc2Config
The currently enabled ArC2 configuration
- property cells
A set of tuples representing the currently selected crosspoints (word, bit).
- property logger
Returns the appropriately format logger for this module. See the python logging documentation for more.
- property mapper
The currently activated channel mapper (see
ChannelMapper
).
- abstract run(self)