Python API#
General#
Where can I find the Python examples?
The Python examples can be found in the example folder of the MaxLab Live installation directory.
How do I design custom electrode configurations and utilize a custom configuration file?
To design custom electrode configurations, we use the class mx.Array
and the following code, which
saves the custom configuration into a file called myConfig.cfg. When instantiating an array object,
make sure that the server is running.
1array = mx.Array('online')
2array.reset()
3array.select_electrodes( [11110, 11111, 11112] )
4array.route()
5array.save_config("myConfig.cfg")
In order to use the custom electrode configuration file, we can run the following code:
1array = mx.Array('stimulation')
2array.load_config( "myConfig.cfg" )
Custom configurations files can also be generated in Scope, by selecting a custom electrode configuration
and then clicking “Route”. The corresponding .cfg
file can then be found in ~/configs/[date]/[time].cfg
.
Warning
Currently, there is no way to distinguish between readout and stimulation electrodes when creating array configurations.
How can I prevent multiple electrodes from routing to the same channel?
This can’t be done, as the routing is conducted automatically. However, if multiple electrodes are routed to the same channel, a workaround would be to pick electrodes next to the ones already picked: This way, the chances that the electrodes will again be routed to the same channel is minimized.
What are groups
for when saving data to H5 files?
The relevant API call is mx.Saving.group_define()
. Please refer
to the documentation of the method for the standard use case. In principle, groups can be used to group
different sets of recording channels for easier post-processing. It might also be of interest to create individual
groups based on proximity of electrodes, etc. and store data associated to channels from these groups in separate locations.
Why does the value for LSB in the H5 file differ from the return value of the Python API?
The LSB found in the H5 files refers to the LSB of the readout amplifier and analog to digital converters (ADCs), while the call to
mx.query_DAC_lsb_mV()
returns the LSB of the Stimulation Unit and digital to analog converters (DACs). Note also the
difference in units: in the H5 it is given in \(\rm V\), while the API returns the value in \(\rm mV\).
Is it possible to run two python scripts simultaneously for MaxLab control?
This is not recommended as concurrent access cannot be synchronised.
Can I get the *.whl
for the maxlab
module?
You can find the wheel here: ~/MaxLab/share/python/wheels/maxlab-*-py3-none-any.whl
. You should be able to
install it into any virtual environment for development purposes. However, please be aware that you will have to update the library manually, if you install a new version of MaxLab Live.
Note
Please note that the mxwserver needs to be running to use the API.
What information is contained in the .cfg
files of the array?
The configuration file contains:
Channel number,
Electrode number,
X and Y position of the electrode,
On a separate line on the end a binary blob of data describing the state of the array to achive the desired routing.
The encoding of this information in the file is:
<channel>(<electrode>)<X>/<Y>;<channel>(<electrode>)<X>/<Y>;...
When do I need to use the mx.offset()
command?
You only need to run an offset compensation using the mx.offset()
command after initialization thought the
mx.initialize()
command. There is no need to run it again as long as you do not reinitialize the system.
Is there any difference in the API for MaxOne and MaxTwo?
There are some extra functions to deal with the fact that a wellplate has more than one chip.
The relevant API calls are: mx.set_primary_well()
and
mx.activate()
.
The command sending rate for both MaxOne and MaxTwo systems is 20’000 Hz. However, the data streaming rate for the MaxTwo system is 10’000 Hz as opposed to 20’000 Hz for the MaxOne.
The classes mx.GPIODirection
, mx.GPIOOutput
,
and mx.StatusLED
do not have relevance for the MaxTwo since the system does not
have GPIOs or a Status LED.
Voltage Stimulation#
Electrode selection and routing#
Can I record from stimulation electrodes?
Electrodes connected to a Stimulation Unit cannot record electrophysiological signals, as the Stimulation Unit fixes their potential. Instead, electrophysiological signals from nearby cells should be recorded using adjacent electrodes.
How can I stimulate with 32 Stimulation Units?
To make full use of the stimulation capabilities of the system, it is important to have a mental picture of how the hardware works. In Fig. 1 you can see an abstract representation of the individual components. We will introduce each layer step-by-step.
The first step is to select the elctrodes you want for stimulation and recording. This is done over the API
calls mx.Array.select_stimulation_electrodes()
,
and mx.Array.select_electrodes()
. For more details see
How do I select electrodes (recording and stimulation) from the electrode array?
How do I choose stimulation sites from the selected electrodes?
Once you have selected your electrodes, they need to be routed. For the routing the stimulation electrodes are given priority over the recording electrodes, but it can still happen that some of the electrodes you have selected for stimulation may not be able to be routed. This is the first potential pitfall, you have to make sure that all of the electrodes you have selected for stimulation have been routed.
Once the electrodes have been routed you need to connect them to the Stimulation Units. To do this, you call
mx.Array.connect_electrode_to_stimulation()
on each electrode you want to connect. For more details see
This is the second possible pitfall: There are 32 independent
Stimulation Units, but when you call
mx.Array.connect_electrode_to_stimulation()
you have no gurantee that the electodes you pass to it will be assigned to different Stimulation Units,
i.e. it can happen that more than one electrode gets assigned to one Stimulation Unit. For an approach on
how to solve this issue, please refer to
Once you have connected the stimulation electrodes to the Stimulation Units, you need to configure your
Stimulation Units. The relevant class for this is mx.StimulationUnit
.
For an example of how to do this you can take a look at this section
in the Python tutorial.
The method mx.StimulationUnit.dac_source()
assigns each
Stimulation Unit a DAC source. We have 3 DACs on the chip. So in total, the setup looks something like
this
Fig. 1 This figure illustrates how stimulation electrodes relate to DAC’s. In red we have marked potential issues that need to be mitigated.#
Related questions:
How do I select electrodes (recording and stimulation) from the electrode array?
We here illustrate how to select electrodes 20, 30, 40 from the electrode array.
1electrodes = [20, 30, 40]
2
3array = mx.Array('stimulation')
4array.reset()
5array.clear_selected_electrodes()
6array.select_electrodes( electrodes )
7array.route()
Other schemes are also possible. For example large consecutive blocks of electrodes or specific electrodes based on prior data analysis done e.g. in Matlab.
How do I choose stimulation sites from the selected electrodes?
After selecting the recording and stimulation electrodes, the next step involves choosing
stimulation sites from the pre-selected electrodes. Assuming the electrodes are stored as a
variable named electrodes
, and if more than 32 stimulation electrodes are selected (the
maximum number of stimulation channels), we proceed by randomly selecting a subset of 32
stimulation electrodes and routing them accordingly:
1import random
2
3stimulation_electrodes = random.sample(electrodes, 32)
4array.select_stimulation_electrodes( stimulation_electrodes )
5array.route()
If less than 32 stimulation electrodes are selected and they are saved in the
variable stimulation_electrodes
, we can directly use these electrodes as stimulation
electrodes and random selection of the stimulation electrodes is required:
1array.select_stimulation_electrodes( stimulation_electrodes )
2array.route()
Warning
The more electrodes are selected, the more issues can occur. In particular, the number of electrodes selected cannot be larger than 32, which is the number of stimulation channels.
How do I connect the Stimulation Units to the stimulation electrodes?
In order to connect the Stimulation Units to the stimulation electrodes saved under the
name of stimulation_electrodes
, we loop over the individual electrodes to connect
them to the Stimulation Units:
1stimulation_electrodes = [20, 30, 40]
2stim_buffer_to_electrode_mapping = {}
3
4for stim_el in stimulation_electrodes:
5 array.connect_electrode_to_stimulation( stim_el )
6 channel = array.query_stimulation_at_electrode( stim_el )
7 if len(stim) == 0:
8 print(f"No stimulation channel can connect to electrode: {stim_el}/{channel}")
9 else:
10 stim_buffer_to_electrode_mapping[stim_el] = int(channel)
11
12if len(set(stim_buffer_to_electrode_mapping.values())) != len(stim_buffer_to_electrode_mapping.values()):
13 # in this case multiple electrodes were connected to the same stim-buffer
14 # and thus cant be used together. In this case, just pick a neighboring electrode instead
15 # and redo this process
16 raise RuntimeError("Multiple stimulation electrodes connected to same stimulation unit.")
17
18stimulation_units = list(stim_buffer_to_electrode_mapping.values())
19array.download()
20mx.offset()
The conditional in the loop above checks if the variable stim
is an empty string or not. As can be
seen in the docstring of the method mx.Array.query_stimulation_at_electrode()
,
it returns either the index of the Stimulation Unit for a specific stimulation electrode, or an empty string. Hence,
the conditional checks if the simulation electrode is indeed connected to a Stimulation Unit.
How do I determine which Stimulation Unit is used with each electrode?
In order to determine which Stimulation Unit is used with each electrode, one needs to have
the list of stimulation electrodes available and stored. We then can use the method
query_stimulation_at_electrode()
from the created
mx.Array
object as follows:
>>> electrode_to_query = 7
>>> array.query_stimulation_at_electrode(electrode_to_query)
21
What should I do if two or more stimulation electrodes are connected to one Stimulation Unit?
If you need independent stimulation pulses on the two electrodes, but they are connected to the same stimulation unit, there are currently two ways to approach the problem. However, if you want to apply the same pulse to these two electrodes anyway, please refer to this question: How is the stimulation pulse affected if two or more electrodes are connected to one Stimulation Unit?.
Try to choose a neighboring electrode as stimulation electrode. For this the
mx.electrode_neighbors()
function might come in handy.Since the root cause of the problem is the routing and the routing is dependent on the electrode configuration, you can try to change the configuration slightly and then try again with the same stimulation electrode selection.
How is the stimulation pulse affected if two or more electrodes are connected to one Stimulation Unit?
The stimulation units are made in such a way that they can drive up to 30 electrodes simultaneously, without significantly affecting the resulting stimulation pulses. Therefore, the stimulation pulse should stay unaffected for most use-cases.
When do I need more than one DAC?
Each DAC is an independent source for the voltage needed for the stimulation. Since we have 3 DACs, you can at most do three independently configured (e.g. with different amplitudes, phases, etc.) stimulation pulses on a set of Stimulation Units simultaneously. The key word here is simultaneously, if the stimulation pulses can be delivered sequentially, then you can stimulate each electrode with a different amplitude, phase, etc. with one DAC.
Does using multiple DACs affect the latency in any way?
No, the latency is not affected at all.
How can I stimulate two different regions with different amplitudes simulataneously?
You can only control DAC0
and DAC1
simultaneously. This is a hardware limitation. To control
the two DACs simultaneously, you need to use dac_no=3
in mx.DAC
.
Lets say you want to create two bi-phasic pulses with the same phase, but different amplitude. Assuming you
have connected some electrodes to DAC0
and some others to DAC1
, then the
mx.Sequence
might looks something like this
sequence = mx.Sequence()
sequence.append(mx.DAC(3, 512-amplitude_A, 512-amplitude_B))
sequence.append(mx.DelaySamples(phase))
sequence.append(mx.DAC(3, 512+amplitude_A, 512+amplitude_B))
sequence.append(mx.DelaySamples(phase))
sequence.append(mx.DAC(3, 512, 512))
For a more detailed example, please refer to this example. Please note, in most cases this is not needed and programming the DACs sequentially is fast enough. The time between two sequential DAC changes is as low as 2-3 µs.
Note
It is not possible to stimulate any other permutation of the three DACs simultaneously. If you need to stimulate with more than two amplitudes it will not be possible to do it simultaneously. In such a case please refer to the question
How can I stimulate with more than two amplitudes as quickly as possible?
Please refer to the this example script.
How do I activate the Stimulation Units in sequence while minimizing delays?
To activate the Stimulation Units in sequence while minimizing delays, one should follow these steps:
Power up all stimulation units,
Disconnect all stimulation units from DACs
Sequentially connect and disconnect stimulation units from DACs.
The code is the following:
1all_stimulation_units = range(0, 32)
2
3for stimulation_unit in all_stimulation_units:
4 print(f"Power up stimulation unit {stimulation_unit}")
5 stim = mx.StimulationUnit(stimulation_unit)
6 mx.send(stim.power_up(True).connect(False).set_voltage_mode().dac_source(0))
7
8for stimulation_unit in all_stimulation_units:
9 print(f"Connecting stimulation unit {stimulation_unit}")
10 stim = mx.StimulationUnit(stimulation_unit)
11 mx.send(stim.connect(True))
12 # perform action on stimulation unit
13 print(f"Disconnecting stimulation unit {stimulation_unit}")
14 mx.send(stim.connect(False))
What issues might arise when powering down a Stimulation Unit?
Powering up and down a Stimulation Unit directly before and after a stimulation pulse is not recommended. First, to improve latency of the stimulation pulse and second to ensure stability of the stimulation unit.
How can I access all Stimulation Units simultaneously?
If one wishes to access all 32 Stimulation Units simultaneously, one needs to connect all the units to one DAC. this can be done the following way:
stimulation_units = range(0, 32)
for stimulation_unit in stimulation_units:
stim = mx.StimulationUnit(stimulation_unit)
mx.send(stim.connect(True).dac_source(0))
Can I change the DAC source of a Stimulation Unit?
To quickly switch DAC sources for a given Stimulation Unit you can use the
mx.StimulationUnit
class. If you create an object of that class you
will see that there is a method called
mx.StimulationUnit.dac_source()
. This method allows switching of
the DAC. As long as you make sure that all Stimulation Units have been previously powered on, the switching itself
should be very fast.
Sequences#
How many sequences can you create in a single python file?
Practically, there is no upper limit. The sequences are stored on the mxwserver
and can then individually
be sent to the device over the mx.Sequence.send()
command of the indivual
sequence or over maxlab::sendSequence()
. Since they are stored on the mxwserver
, each additional
sequence will use a little bit memory on the computer that it is running, but this is negligible. Thus, theoretically, the number and length of sequences are constraint by the memory of the computer running mxwserver
.
How can I reduce the latency between pulse-trains?
When creating a stimulation sequence like this
def single_pulse(seq, amplitude):
seq.append( maxlab.chip.DAC(0, 512-amplitude) )
seq.append( maxlab.system.DelaySamples(4) )
seq.append( maxlab.chip.DAC(0, 512+amplitude) )
seq.append( maxlab.system.DelaySamples(4) )
seq.append( maxlab.chip.DAC(0, 512) )
return seq
seq = mx.Sequence("name")
single_pulse(seq, some_amplitude)
for i in range(N):
seq.send()
the bottleneck in terms of latency is the for
-loop in python. The solution is to keep in mind that a
mx.Sequence
object can hold multiple pulses. We can thus write something like this
def single_pulse(seq, amplitude):
seq.append( maxlab.chip.DAC(0, 512-amplitude) )
seq.append( maxlab.system.DelaySamples(4) )
seq.append( maxlab.chip.DAC(0, 512+amplitude) )
seq.append( maxlab.system.DelaySamples(4) )
seq.append( maxlab.chip.DAC(0, 512) )
return seq
time_between_pulse_trains = 3000 #us
seq = maxlab.Sequence("name")
for i in range(30):
single_pulse(seq, some_amplitude)
seq.append(mx.DelaySamples(time_between_pulse_trains / 50 )) # 50us = 1/ 20'000 s^-1 = 1 / sampling_freq
seq.send()
How do I send spike trains as stimulation pulses to multiple electrodes?
Warning
In order to run the code below, one first needs to select the stimulation electrodes and connect them to the stimulation units.
We first start by powering off all the stimulation units.
1all_stimulation_units = range(0, 32)
2
3for stimulation_unit in all_stimulation_units:
4 stim = mx.StimulationUnit(stimulation_unit)
5 stim.power_up(False)
6 stim.connect(False)
7 mx.send(stim)
We will start by preparing a sequence of stimulation pulses with increasing amplitudes: 10 bits (= 29 mV), 15 bits (= 43.5 mV), 20 bits (= 58 mV), and 25 bits (= 72.5 mV). # see doc for more info…
1def append_stimulation_pulse(seq, amplitude):
2 seq.append( mx.DAC(0, 512-amplitude) )
3 seq.append( mx.DelaySamples(4) )
4 seq.append( mx.DAC(0, 512+amplitude) )
5 seq.append( mx.DelaySamples(4) )
6 seq.append( mx.DAC(0, 512) )
7 return seq
8
9amplitudes = range(10, 25, 5)
10repetitions = range(1, 30) # number of pulse repetitions
11seq = mx.Sequence()
12
13for amplitude in amplitudes:
14 for rep in repetitions:
15 append_stimulation_pulse(seq, amplitude)
16 seq.append( mx.DelaySamples(2000) )
17 seq.append( mx.DelaySamples(20000) )
As can be understood from the code above, the sequence can be of any length and amplitude combinations. However, in the case of this example, the same sequence will be sent to all stimulation units. We now are ready to deliver the pulses. This general steps are followed:
Power up one stimulation unit.
Deliver a series of stimulation pulses.
Power down the stimulation unit.
1import time
2
3stimulation_units = [2, 3, 5]
4
5for stimulation_unit in stimulation_units:
6 print("Power up stimulation unit " + str(stimulation_unit))
7 stim = mx.StimulationUnit(stimulation_unit)
8 stim.power_up(True).connect(True).set_voltage_mode().dac_source(0)
9 mx.send(stim)
10 print("Send pulse")
11 seq.send()
12 print("Power down stimulation unit " + str(stimulation_unit))
13 stim = mx.StimulationUnit(stimulation_unit).power_up(False)
14 mx.send(stim)
15 time.sleep(2)
How can I change the stimulation pattern based on the recording? Is there a Python package for that?
This question is part of the closed-loop C++ documentation and it is not doable in Python.
How can I stop an ongoing stimulation?
This is not possible: The only way is to shut down the device.
What is the most efficient way to deliver a long stimulation sequence of biphasic pulses?
The recommended way to do this is using mx.Sequence
. A possible approach
could look something like this
def append_bi_phasic_pulse(seq: mx.Sequence, amplitude: int, phase: int) -> mx.Sequence:
seq.append(mx.DAC(0, 512 - amplitude))
seq.append(mx.DelaySamples(phase))
seq.append(mx.DAC(0, 512 + amplitude))
seq.append(mx.DelaySamples(phase))
seq.append(mx.DAC(0, 512))
return seq
seq = mx.Sequence()
for _ in range(100):
append_bi_phasic_pulse(seq, amplitude=10, phase=4)
seq.append(mx.DelaySamples(200))
seq.send()
How can we independently control more than three stimulation units?
There are 3 DAC sources. The 32 Stimulation Units can be connected right before delivering the pulse and disconnected right after delivering the pulse. That way, all 32 Stimulation Units can stimulate different electrodes, with e.g. only one DAC source. In the same way, you can connect one subset of electrodes to one DAC source and another subset to another DAC source.
Does gain affect stimulation voltage in any way?
Gain only affects the recorded signals. It might thus affect the artifact of the stimulation pulse, which will appear larger (or smaller) depending on gain. The LSB which is recorded into the files refers to the recording LSB (not stimulation LSB). The stimulation LSB should typically not be changed.
If we want to change the gain, when should this happen?
Gain can be changed at any time after mx.initialize()
.
Is it feasible to use the stimulation APIs to generate a wave closer to a wave rather than a square?
A possible approach could look like this
seq = mx.Sequence()
multipliers = [0, 1, 2, 3, 2, 1]
for i in multipliers:
seq.append(mx.DAC(0, 512 - 5*i))
seq.append(mx.DelaySamples(1))
for i in multipliers:
seq.append(mx.DAC(0, 512 + 5*i))
seq.append(mx.DelaySamples(1))
seq.append(mx.DAC(0, 512))
When should I use the persistent
flag in a sequence?
The persistent
flag should be used anytime a specific sequence must outlive the execution of the Python file
in which it is created. This is most common in closed-loop applications, where a setup script must be run beforehand
to prepare various sequences. A C++ script is called afterwards to deliver those sequences. See below for an example.
Assume you create a sequence like this
import maxlab as mx
from time import sleep
seq = mx.Sequence("my_name") # equivalent to mx.Sequence("my_name", persistent=False)
seq.append(mx.Event(0, 1, 1, "text some_string_describing_an_important_event"))
You created this specific sequence so that you can label events in the raw data stream from C++.
In this case, you would want the sequence to outlive the execution of the Python file itself.
At end of the python file the destructor for all object gets called (i.e. the __del__
method of that class),
i.e. in essence the python interpreter calls del seq
.
When the del
keyword is used, the sequence gets also deleted from the mxwserver
and you will get an error
when trying to use maxlab::sendSequence()
.
To prevent the sequence
from being deleted from the mxwserver
you should pass persistent=True
to the constructor.
Events#
How can I use a mx.Event
to track the exact timing of my stimulation?
Using a mx.Sequence
to build your stimulation, you can insert a
mx.Event
right before the start of each pulse as shown in the example below:
seq = mx.Sequence()
seq.append(mx.Event(0, 1, event_counter, f"amplitude {amplitude} event_id {event_counter}"))
seq.append(mx.DAC(0, 512 - amplitude))
seq.append(mx.DelaySamples(delay_samples))
seq.append(mx.DAC(0, 512 + amplitude))
seq.append(mx.DelaySamples(delay_samples))
seq.append(mx.DAC(0, 512))
Since the entire procedure occurs within a mx.Sequence
, there will be minimal
delay between the commands and the mx.Event
will share the same frame number as the
onset of the stimulation. All events are stored and can therefore be used to identify the exact timing of each stimulation.
I’m sending events but I’m not seeing any in the raw file. What might be wrong?
Make sure to call mx.clear_events()
before you add any events to your first
sequence and to only add events to the sequence once you have started the recording via
mx.Saving.start_recording()
. Also make sure to set the event_type
to 1
.
How can I store arbitrary annotations in my events?
Looking at the documentation of mx.Event
we can see that there is a parameter called
properties
. This parameter is of type string and is interpreted as a list of space-separated key value pairs,
e.g.
>>> mx.Event(0, 1, user_id, "key0 value0 key1 value1")
will in the HDF5 file appear as a table of the form
key0 |
value0 |
key1 |
value1 |
This is quite flexible, since the value can be anything that does not contain a space.
I see a message in the mxwserver
terminal that says Event with id X doesn't exist
. How can I fix this?
This happens if you call mx.clear_events()
after you added the events to a
sequence and then send that sequence. The above call deletes all the events from the mxwserver
so when you
send the sequence the mxwserver
does not know to which event you are refering to in the sequence.
For more information on how to avoid this see
Latencies#
What is the latency for sending stimulation pulses?
This depends on the exact use-case:
- If all stimulation pulses are stored within a single sequence
Within a sequence, all commands are executed immediately after each other, leading to minimal delay and very precise timing. However, when sending a sequence, we need to ensure that all commands have been downloaded before the first one gets executed, to guarantee the precise timing. For this reason, the
mx.Sequence
class has anintial_delay
parameter, set to 5 \(\rm ms\) by default. For use-cases where low latency is needed, this parameter can be decreased, but be wary that it may lead to the above mentioned issues.
- If the stimulation protocol requires operations outside of sequences
Most operations outside of sequences, e.g. connecting an electrode to a stimulation buffer, are quite fast. However, since it cannot be guaranteed that they will be executed immediately, additional and irregular delays are to be expected.