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>;...

Note

The class mx.Config can be used to parse .cfg files. Accessing electrode configurations and mappings in C++ requires either manually parsing .cfg files or passing information from mx.Config in the Python script to the C++ script.

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

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

Figure to illustrate how stimulation electrodes relate to DAC's.

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:

  1. Power up all stimulation units,

  2. Disconnect all stimulation units from DACs

  3. 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:

  1. Power up one stimulation unit.

  2. Deliver a series of stimulation pulses.

  3. 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 an intial_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.