Skip to content

Examples

The following examples are also available in the examples directory.

Load and save data

This example shows how to load/save methods and measurements and how to inspect the data.

load_save_data.py
from pathlib import Path

import pandas as pd

import pypalmsens as ps

examples_dir = Path(__file__).parent

# load a method file
method = ps.load_method_file(examples_dir / 'PSDummyCell_LSV.psmethod', as_method=True)

# save the method file
ps.save_method_file(examples_dir / 'PSDummyCell_LSV_copy.psmethod', method)

# load a session file
measurements = ps.load_session_file(examples_dir / 'Demo CV DPV EIS IS-C electrode.pssession')

for measurement in measurements:
    print(f'loaded measurement: {measurement.title}, {measurement.timestamp}')
    print(f'number of curves: {len(measurement.curves)}')
    for curve in measurement.curves:
        print(f'curve title: {curve.title}')
        print(f'number of points: {len(curve.x_array)}')
        print(f'number of peaks: {len(curve.peaks)}')
    print(f'Has EIS fit results: {"yes" if len(measurement.eis_fit) > 0 else "no"}')

# save the session file
ps.save_session_file(
    examples_dir / 'Demo CV DPV EIS IS-C electrode_copy.pssession', [measurements[0]]
)

# convert measurments to pandas dataframes
frames = []
frame_names = []

for measurement in measurements:
    dataset = measurement.dataset

    frames.append(dataset.to_dataframe())
    frame_names.append(measurement.title)

df = pd.concat(frames, keys=frame_names)
print(df)

Manual control

This example shows how to discover devices, establish a connection and control an instrument manually.

manual_control.py
import pypalmsens as ps

instruments = ps.discover()
print(instruments)

with ps.connect(instruments[0]) as manager:
    manager.set_cell(True)
    print('cell enabled')

    manager.set_potential(1)
    print('set potential to 1V')

    manager.set_current_range('1mA')
    print('set cell to to 1mA currrent range')

    current = manager.read_current()
    print(f'current = {current} µA')

    manager.set_cell(False)
    print('cell disabled')

Manual control async

This example shows how to discover devices, establish a connection and control an instrument manually using the asynchronous instrument manager.

manual_control_async.py
import asyncio

import pypalmsens as ps


async def main():
    instruments = await ps.discover_async()
    print(instruments)

    async with await ps.connect_async(instruments[0]) as manager:
        await manager.set_cell(True)
        print('cell enabled')

        await manager.set_potential(1)
        print('set potential to 1V')

        await manager.set_current_range('1mA')
        print('set cell to to 1mA currrent range')

        current = await manager.read_current()
        print(f'current = {current} µA')

        await manager.set_cell(False)
        print('cell disabled')


asyncio.run(main())

Measure CA

This example shows how to set up and run a chronoamperometry measurement.

measurement_CA.py
import pypalmsens as ps


def new_data_callback(data):
    print(data.last_datapoint())


instruments = ps.discover()
print(instruments)

with ps.connect(instruments[0]) as manager:
    serial = manager.get_instrument_serial()
    print(serial)

    # Chronoamperometry measurement using helper class
    method = ps.ChronoAmperometry(
        interval_time=0.01,
        potential=1.0,
        run_time=10.0,
    )

    measurement = manager.measure(method, callback=new_data_callback)

print(measurement)

Measure CA async

This example shows how to set up and run a chronoamperometry measurement using the asynchronous instrument manager.

measurement_CA_async.py
import asyncio

import pypalmsens as ps


def new_data_callback(data):
    print(data.last_datapoint())


async def main():
    instruments = await ps.discover_async()
    print(instruments)

    async with await ps.connect_async(instruments[0]) as manager:
        serial = await manager.get_instrument_serial()
        print(serial)

        method = ps.ChronoAmperometry(
            interval_time=0.02,
            potential=1.0,
            run_time=2.0,
        )

        measurement = await manager.measure(method, callback=new_data_callback)

    print(measurement)


asyncio.run(main())

Measure CV

This example shows how to set up and run a cyclic voltammetry measurement.

measurement_CV.py
import pypalmsens as ps


def new_data_callback(data):
    print(data.last_datapoint())


instruments = ps.discover()
print(instruments)

with ps.connect(instruments[0]) as manager:
    serial = manager.get_instrument_serial()
    print(serial)

    method = ps.CyclicVoltammetry(
        current_range={
            'max': '1A',  # 1 A range
            'min': '1uA',  # 1 µA range
            'start': '1mA',  # 1 mA range
        },
        equilibration_time=2,  # seconds
        begin_potential=-2,  # V
        vertex1_potential=-2,  # V
        vertex2_potential=3,  # V
        step_potential=0.05,  # V
        scanrate=5,  # V/s
        n_scans=3,  # number of scans
    )

    measurement = manager.measure(method, callback=new_data_callback)

print(measurement)

Measure EIS

This example shows how to set up and run a EIS measurement.

measurement_EIS.py
import pypalmsens as ps


def new_data_callback(data):
    print(data.last_datapoint())


instruments = ps.discover()
print(instruments)

with ps.connect(instruments[0]) as manager:
    serial = manager.get_instrument_serial()
    print(serial)

    method = ps.ElectrochemicalImpedanceSpectroscopy()

    measurement = manager.measure(method, callback=new_data_callback)

print(measurement)

Mixed Mode

This example shows how to set up a Mixed Mode measurement for a typicial charge / discharge cycle of a common Lithium battery. Note that the example has a reduced number of cycles and maximum run time.

mixed_mode.py
import pypalmsens as ps

# example with a single cycle with reduced charge/discharge timeouts
N_CYCLES = 1
OCP_TIME = 5
TIMEOUT = 10

# # For a complete 1000-cycle run, you could use he following parameters
# # Note that this could take a while :-)
# N_CYCLES = 1000
# OCP_TIME = 300
# TIMEOUT = 10000


def new_data_callback(data):
    print(data.last_datapoint())


instruments = ps.discover()
print(instruments)

method = ps.mixed_mode.MixedMode(
    current_range={
        'min': '1mA',  # 1 mA range
        'max': '100mA',  # 100 mA range
        'start': '100mA',  # 100 mA range
    },
    interval_time=1.0,
    cycles=N_CYCLES,
    stages=[
        {
            'stage_type': 'OpenCircuit',
            'run_time': OCP_TIME,  # s
        },
        {
            'stage_type': 'ConstantI',
            'run_time': TIMEOUT,  # s
            'current': 3.0,
            'applied_current_range': '100mA',
            'potential_limits': {'max': 4.2},
        },
        {
            'stage_type': 'ConstantE',
            'run_time': TIMEOUT,  # s
            'potential': 4.2,
            'current_limits': {'min': 50000},  # mA
        },
        {
            'stage_type': 'OpenCircuit',
            'run_time': OCP_TIME,  # s
        },
        {
            'stage_type': 'ConstantI',
            'run_time': TIMEOUT,  # s
            'current': -3.0,
            'applied_current_range': '100mA',
            'potential_limits': {'min': 2.5},
        },
    ],
)


with ps.connect(instruments[0]) as manager:
    serial = manager.get_instrument_serial()
    print(serial)

    measurement = manager.measure(method, callback=new_data_callback)

print(measurement)

MethodSCRIPT sandbox

This example shows how to set up and run a MethodSCRIPT Sandbox measurement.

measurement_MethodSCRIPT_sandbox.py
import pypalmsens as ps


def new_data_callback(data):
    print(data.last_datapoint())


script = """e
var c
var p
var e
var l
var r
var j
var o
var d
var n
set_gpio_cfg 0x0f 1
set_pgstat_chan 0
set_pgstat_mode 3
set_acquisition_frac_autoadjust 50
set_max_bandwidth 0
cell_off
set_range ba 210m
set_autoranging ba 210m 210m
set_range ab 4200m
set_autoranging ab 210m 4200m
meas_loop_ocp o 200m 1
pck_start
    pck_add o
pck_end
endloop
set_range ba 2100u
set_autoranging ba 2100n 2100u
set_range ab 4200m
set_autoranging ab 4200m 4200m
store_var d -200m ab
add_var d o
store_var n 200m ab
add_var n o
set_e d
cell_on
set_gpio 10i
meas_loop_acv p c e l r j d n 10m 200m 10m 100
pck_start
    pck_add p
    pck_add c
    pck_add e
    pck_add l
    pck_add r
    pck_add j
pck_end
endloop
on_finished:
cell_off

"""

instruments = ps.discover()
print(instruments)

with ps.connect(instruments[0]) as manager:
    serial = manager.get_instrument_serial()
    print(serial)

    method = ps.MethodScript(script=script)

    measurement = manager.measure(method, callback=new_data_callback)

print(measurement)

Status callback

This example shows how to set up a callback to read out the idle status updates (current/potential) and store the pretreatment data.

measurement_status_callback.py
import asyncio

import pypalmsens as ps
from pypalmsens.data import Status

pretreatment_data = []


def idle_status_callback(point: Status):
    if point.device_state == 'Pretreatment':
        pretreatment_data.append(
            {
                'phase': point.pretreatment_phase,
                'current': point.current,
                'potential': point.potential,
            }
        )

    print(f'{point.device_state}: {point}')


def new_data_callback(data):
    print(data.last_datapoint())


async def main():
    instruments = await ps.discover_async()
    print(instruments)

    async with await ps.connect_async(instruments[0]) as manager:
        manager.register_status_callback(idle_status_callback)

        # While sleeping, the callback reports the
        # idle current/potential every second
        await asyncio.sleep(5)

        # The status callback repurts the
        # current/potential during pretreatment phases
        method = ps.ChronoAmperometry(
            pretreatment=ps.settings.Pretreatment(
                conditioning_time=2,
                deposition_time=2,
            ),
            interval_time=0.02,
            potential=1.0,
            run_time=2.0,
        )

        measurement = await manager.measure(method, callback=new_data_callback)

        await asyncio.sleep(5)

        manager.unregister_status_callback()

    print(measurement)


asyncio.run(main())

print(pretreatment_data)

Stream data to CSV

This example shows how to set up and run a chronoamperometry measurement and write the results to a CSV file in real-time.

measurement_stream_to_csv.py
import csv

import pypalmsens as ps


def stream_to_csv_callback(data):
    for point in data.new_datapoints():
        csv_writer.writerow([point['index'], point['x'], point['y']])

        ## for EIS
        # csv_writer.writerow([point['Frequency'], point['ZRe'], point['ZIm']])


csv_file = open('test.csv', 'w', newline='')
csv_writer = csv.writer(csv_file)

instruments = ps.discover()
print(instruments)

with ps.connect(instruments[0]) as manager:
    serial = manager.get_instrument_serial()
    print(serial)

    # Chronoamperometry measurement using helper class
    method = ps.ChronoAmperometry(
        interval_time=0.004,
        potential=1.0,
        run_time=10.0,
    )

    measurement = manager.measure(method, callback=stream_to_csv_callback)

print(measurement)

csv_file.close()

SWV versus OCP

This example shows how to set up and run a square wave voltammetry measurement versus OCP.

measurement_SWV_vs_OCP.py
import pypalmsens as ps


def new_data_callback(data):
    print(data.last_datapoint())


instruments = ps.discover()
print(instruments)

with ps.connect(instruments[0]) as manager:
    method = ps.SquareWaveVoltammetry(
        pretreatment={
            'conditioning_potential': 2.0,  # V
            'conditioning_time': 2,  # seconds
        },
        versus_ocp={
            'mode': 3,  # versus begin and end potential
            'max_ocp_time': 1,  # seconds
        },
        begin_potential=-0.5,  # V
        end_potential=0.5,  # V
        step_potential=0.01,  # V
        amplitude=0.08,  # V
        frequency=50,  # Hz
    )

    measurement = manager.measure(method, callback=new_data_callback)

print(measurement)
print(f'ocp: {measurement.ocp_value}')

Multiplexer

This example shows how to set up and control a multiplexer and run consecutive and alternating multiplexer measurments.

multiplexer.py
import pypalmsens as ps


def new_data_callback(data):
    print(data.last_datapoint())


instruments = ps.discover()
print(instruments)

with ps.connect(instruments[0]) as manager:
    n_multiplexer_channels = manager.initialize_multiplexer(2)
    manager.set_mux8r2_settings()

    for channel in range(n_multiplexer_channels):
        manager.set_multiplexer_channel(channel)

    # When measuring alternatingly the selection is restricted to the first n channels
    altnernating_multiplexer_method = ps.ChronoAmperometry(
        interval_time=0.5,  # seconds
        potential=1.0,  # volts
        run_time=5.0,  # seconds
        multiplexer={
            'mode': 'alternate',  # 'none', 'consecutive', 'alternate'
            'channels': [1, 2],  # 8 channels, 1 and 2 are enabled
            'connect_sense_to_working_electrode': False,
            'combine_reference_and_counter_electrodes': False,
            'use_channel_1_reference_and_counter_electrodes': False,
            'set_unselected_channel_working_electrode': 0,
        },
    )
    measurement = manager.measure(altnernating_multiplexer_method, callback=new_data_callback)
    print(measurement)

    consecutive_multiplexer_method = ps.SquareWaveVoltammetry(
        begin_potential=-0.5,  # volts
        end_potential=0.5,  # volts
        step_potential=0.01,  # volts
        amplitude=0.1,  # volts
        frequency=10,  # hertz
        multiplexer={
            'mode': 'consecutive',  # 'none', 'consecutive', 'alternate'
            'channels': [1, 2, 7, 8],  # channels 1, 2, 7 and 8 are enabled
            'connect_sense_to_working_electrode': False,
            'combine_reference_and_counter_electrodes': False,
            'use_channel_1_reference_and_counter_electrodes': False,
            'set_unselected_channel_working_electrode': 0,
        },
    )

    measurement = manager.measure(consecutive_multiplexer_method, callback=new_data_callback)
    print(measurement)

Multichannel measurement

This example shows how to connect to a collection of instruments and run a chronoamperometry measurement on all channels simultaneously.

multichannel_measurement.py
import pypalmsens as ps


def new_data_callback(data):
    print(data.last_datapoint())


method = ps.ChronoAmperometry(
    interval_time=0.004,
    potential=1.0,
    run_time=5.0,
)

instruments = ps.discover()

print(instruments)

# run multichannel experiment with callback
with ps.InstrumentPool(instruments) as pool:
    results = pool.measure(method=method, callback=new_data_callback)

print(results)

Multichannel CSV writer

This example shows how to connect to a how to use a callback to automatically store data to a csv file while collecting data from collection of instruments.

multichannel_csv_callback.py
import asyncio
import pypalmsens as ps
import csv
import functools


def stream_to_csv_callback(data, csv_writer):
    for point in data.new_datapoints():
        csv_writer.writerow([point['index'], point['x'], point['y']])


async def stream_to_csv(manager, *, method):
    """Measure with a custom csv writer callback."""
    serial = await manager.get_instrument_serial()

    with open(f'{serial}.csv', 'w', newline='') as csv_file:
        csv_writer = csv.writer(csv_file)

        callback = functools.partial(stream_to_csv_callback, csv_writer=csv_writer)

        measurement = await manager.measure(method, callback=callback)

    print(f'Wrote data to {csv_file.name}')

    return measurement


async def main():
    method = ps.ChronoAmperometry(
        interval_time=0.004,
        potential=1.0,
        run_time=5.0,
    )

    instruments = await ps.discover_async()

    print(instruments)

    # run multichannel experiment with csv writer
    async with ps.InstrumentPoolAsync(instruments) as pool:
        results = await pool.submit(stream_to_csv, method=method)

    print(results)


asyncio.run(main())

Multichannel custom loop

This example shows how to run and set up a sequence of measurements on a collection of channels simultaneously.

multichannel_custom_loop.py
import asyncio
import pypalmsens as ps


async def custom_loop(manager, *, method, steps):
    measurements = []

    for step in steps:
        method = method.model_copy(step)
        measurements.append(await manager.measure(method))

    return measurements


async def main():
    method = ps.ChronoAmperometry(
        interval_time=0.004,
        run_time=5.0,
    )

    steps = [
        {'potential': 0.4},
        {'potential': 0.6},
        {'potential': 1.0},
    ]

    instruments = await ps.discover_async()

    print(instruments)

    async with ps.InstrumentPoolAsync(instruments) as pool:
        results = await pool.submit(custom_loop, method=method, steps=steps)

    print(results)

    for i, measurements in enumerate(results):
        ps.save_session_file(f'example-{i}.pssession', measurements)


asyncio.run(main())

Multichannel HW sync

On multi-channel devices that support it, hardware sync can be used to synchronize measurements between multiple channels. When synchronization is enabled the follower device will wait until the main channel enables synchronisation. After that, the follower and main will synchronize their measurement loop start and iterations.

This example shows how to connect to a collection of instruments and run a chronopotentiometry measurement on all channels simultaneously using hardware synchronization.

For hardware synchronization, you set use_hardware_sync = True on the method. This is the equivalent of set_channel_sync 1 in MethodSCRIPT. At the moment this only works with async.

In addition, your pool of instruments must contain:

  • channels from a single multi-channel instrument
  • the first (main) channel of the instrument
  • at least one follower channel

All instruments are prepared and put in a waiting state. The measurements are started via a hardware sync trigger on channel 1. Only channel 1 (the main channel) has hardware required to trigger the other channels.

multichannel_HW_sync.py
import asyncio
import pypalmsens as ps


async def main():
    method = ps.ChronoAmperometry(
        interval_time=0.004,
        potential=1.0,
        run_time=5.0,
    )
    method.general.use_hardware_sync = True

    instruments = await ps.discover_async()

    print(instruments)

    async with ps.InstrumentPoolAsync(instruments) as pool:
        results = await pool.measure(method)

    print(results)


asyncio.run(main())