Advanced Channel Configuration
This example demonstrates how to configure additional channels beyond the standard ones on the internal MAIN potentiostat. For instance, it shows how to measure PAD4 channels synchronously with the MAIN voltage and current channels.
“Synchronously measured” means that these channels are sampled at the exact same time as the MAIN channels. In contrast, asynchronous channels are sampled sequentially after the synchronous channels, which can reduce the overall sampling rate of the system.
import zahner_link as zl
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.ticker import EngFormatter
link = zl.ZahnerLinkExc("169.254.9.137", "1994")
error: zl.ErrorObject = link.connect()
if not error:
print("connected successfully")
else:
print(f"failed to connect, status: {error.get_error_code_enum()}, message: {error.get_message_formatted()}")
connected successfully
First, the available potentiostats from HardwareInfo are stored in local variables for easier access.
hardware_info_job = zl.control.GetHardwareInfoJob()
link.do_job(hardware_info_job)
hardware_info = hardware_info_job.get_job_result()
main_pot = None
external_pot = None
for pot in hardware_info.potentiostats:
print(
f"Found potentiostat: {pot.identifier} with serialnumber {pot.serialnumber} at {pot.url}"
)
if pot.identifier == "MAIN":
main_pot = pot
elif external_pot is None and pot.identifier != "MAIN":
external_pot = pot
Found potentiostat: EL1002 with serialnumber 24281 at EPC:1:EXT1:POT
Found potentiostat: MAIN with serialnumber 2557874741 at MAIN:1:POT
The HardwareSettingsHelper simplifies the creation of basic configurations. In the following steps, we will perform the standard configuration for sampling current and voltage at the MAIN potentiostat.
To do this, the helper is executed, and the resulting configuration is transferred to the job for execution.
config_for_main_pot = zl.HardwareSettingsHelper.get_config_for_potentiostat(
main_pot.serialnumber
)
# equal to for easier access to default settings for the internal main potentiostat
config_for_main_pot = zl.HardwareSettingsHelper.get_config_for_main_potentiostat()
link.do_job(
zl.control.SetHardwareSettingsJob(config=config_for_main_pot)
)
True
Polarization with Different Channel Configurations
To demonstrate the various DC configurations, a helper function has been created that measures one polarization and prints all tracks.
This allows for a comparison of the results from the different configurations.
def poga_and_print(pot_url, bias, coupling):
switch_on_job = zl.control.SwitchOnJob(
potentiostat=pot_url,
coupling=coupling,
bias=bias,
voltage_range_index=0,
compliance_range_index=0,
)
link.do_job(switch_on_job)
poga = zl.meas.PogaJob(
bias=bias,
duration=1,
output_data_rate=10,
autorange=True,
current_range=1,
)
link.do_job(poga)
link.do_job(zl.control.SwitchOffJob(potentiostat=pot_url))
poga_data = link.get_job_result_data(poga)
print(f"Tracks: {poga_data.get_dc_tracks().keys()}")
print("data per track:")
engFormat = EngFormatter(places=2, sep="")
for name, track in poga_data.get_dc_tracks().items():
print(f"trackname(dimension): {name}")
# print values with engineering format for better readability, this is optional
print(f"\t{[engFormat.format_eng(point) for point in track]}")
return
Default Configuration of the MAIN Potentiostat
In the following, you can see that there are three essential tracks in the dataset:
timevoltagecurrent
poga_and_print(main_pot.url, 0.1, zl.PotentiostatCoupling.POTENTIOSTATIC)
Tracks: dict_keys(['current', 'debug', 'direction', 'shunt', 'time', 'voltage'])
data per track:
trackname(dimension): current
['100.55µ', '100.55µ', '100.55µ', '100.55µ', '100.55µ', '100.55µ', '100.55µ', '100.55µ', '100.55µ', '100.55µ']
trackname(dimension): debug
['0.00', '0.00', '0.00', '0.00', '0.00', '0.00', '0.00', '0.00', '0.00', '0.00']
trackname(dimension): direction
['0.00', '0.00', '0.00', '0.00', '0.00', '0.00', '0.00', '0.00', '0.00', '0.00']
trackname(dimension): shunt
['6.00', '6.00', '6.00', '6.00', '6.00', '6.00', '6.00', '6.00', '6.00', '6.00']
trackname(dimension): time
['100.00m', '200.00m', '300.00m', '400.00m', '500.00m', '600.00m', '700.00m', '800.00m', '900.00m', '1.00']
trackname(dimension): voltage
['100.04m', '100.04m', '100.04m', '100.04m', '100.04m', '100.04m', '100.04m', '100.04m', '100.04m', '100.04m']
PAD4 Card Connected to the MAIN Potentiostat
The configuration is now extended by the four PAD4 channels of card 2, which are connected to the MAIN potentiostat.
You can now see that four additional channels have been added to the track prints:
pad4_2_1pad4_2_2pad4_2_3pad4_2_4
The names were set using the Pad4Connection object with the dimension parameter, as shown in the following instruction. The card and index variables are initialized in the for loops:
dimension=f"pad4_{card}_{index}"
The name for the dimension can be assigned arbitrarily and is used later to identify the channel in the measurement data. The channel name must be unique and can only be used once.
config_for_main_pot_pad4 = (
zl.HardwareSettingsHelper.get_config_for_potentiostat_with_sync_voltage_channel(
pot_serial_number=main_pot.serialnumber,
connections=[
zl.Pad4Connection(
card,
index,
dimension=f"pad4_{card}_{index}",
)
for index in range(1, 5)
for card in [2]
],
)
)
link.do_job(
zl.control.SetHardwareSettingsJob(config=config_for_main_pot_pad4)
)
poga_and_print(main_pot.url, 0.1, zl.PotentiostatCoupling.POTENTIOSTATIC)
Tracks: dict_keys(['current', 'debug', 'direction', 'pad4_2_1', 'pad4_2_2', 'pad4_2_3', 'pad4_2_4', 'shunt', 'time', 'voltage'])
data per track:
trackname(dimension): current
['102.46µ', '102.46µ', '102.46µ', '102.46µ', '102.46µ', '102.46µ', '102.46µ', '102.46µ', '102.46µ', '102.46µ']
trackname(dimension): debug
['0.00', '0.00', '0.00', '0.00', '0.00', '0.00', '0.00', '0.00', '0.00', '0.00']
trackname(dimension): direction
['0.00', '0.00', '0.00', '0.00', '0.00', '0.00', '0.00', '0.00', '0.00', '0.00']
trackname(dimension): pad4_2_1
['100.05m', '100.05m', '100.04m', '100.05m', '100.04m', '100.04m', '100.04m', '100.04m', '100.04m', '100.04m']
trackname(dimension): pad4_2_2
['100.02m', '100.02m', '100.02m', '100.02m', '100.01m', '100.02m', '100.02m', '100.02m', '100.02m', '100.02m']
trackname(dimension): pad4_2_3
['100.04m', '100.04m', '100.04m', '100.03m', '100.04m', '100.04m', '100.04m', '100.04m', '100.05m', '100.05m']
trackname(dimension): pad4_2_4
['100.01m', '100.01m', '100.01m', '100.01m', '100.01m', '100.01m', '100.01m', '100.00m', '100.00m', '100.01m']
trackname(dimension): shunt
['6.00', '6.00', '6.00', '6.00', '6.00', '6.00', '6.00', '6.00', '6.00', '6.00']
trackname(dimension): time
['100.00m', '200.00m', '300.00m', '400.00m', '500.00m', '600.00m', '700.00m', '800.00m', '900.00m', '1.00']
trackname(dimension): voltage
['100.04m', '100.04m', '100.04m', '100.04m', '100.04m', '100.04m', '100.04m', '100.04m', '100.04m', '100.04m']
PAD4 Card Connected to the External Potentiostat
The configuration is now changed so that the measurements are performed on the external potentiostat, and PAD4 card 1 is used, with its channels connected to the external potentiostat.
config_external_pot_pad4 = (
zl.HardwareSettingsHelper.get_config_for_potentiostat_with_sync_voltage_channel(
pot_serial_number=external_pot.serialnumber,
connections=[
zl.Pad4Connection(
card,
index,
dimension=f"pad4_{card}_{index}",
)
for index in range(1, 5)
for card in [1]
],
)
)
link.do_job(
zl.control.SetHardwareSettingsJob(config=config_external_pot_pad4)
)
poga_and_print(external_pot.url, 15, zl.PotentiostatCoupling.GALVANOSTATIC)
Tracks: dict_keys(['current', 'debug', 'direction', 'pad4_1_1', 'pad4_1_2', 'pad4_1_3', 'pad4_1_4', 'shunt', 'time', 'voltage'])
data per track:
trackname(dimension): current
['15.03', '15.03', '15.03', '15.03', '15.03', '15.03', '15.03', '15.03', '15.03', '15.03']
trackname(dimension): debug
['0.00', '0.00', '0.00', '0.00', '0.00', '0.00', '0.00', '0.00', '0.00', '0.00']
trackname(dimension): direction
['0.00', '0.00', '0.00', '0.00', '0.00', '0.00', '0.00', '0.00', '0.00', '0.00']
trackname(dimension): pad4_1_1
['150.08m', '150.08m', '150.08m', '150.08m', '150.08m', '150.09m', '150.09m', '150.08m', '150.08m', '150.07m']
trackname(dimension): pad4_1_2
['150.11m', '150.11m', '150.11m', '150.11m', '150.11m', '150.11m', '150.12m', '150.11m', '150.11m', '150.11m']
trackname(dimension): pad4_1_3
['150.10m', '150.10m', '150.09m', '150.10m', '150.10m', '150.11m', '150.11m', '150.10m', '150.10m', '150.10m']
trackname(dimension): pad4_1_4
['150.13m', '150.13m', '150.12m', '150.12m', '150.12m', '150.12m', '150.14m', '150.13m', '150.13m', '150.13m']
trackname(dimension): shunt
['1.00', '1.00', '1.00', '1.00', '1.00', '1.00', '1.00', '1.00', '1.00', '1.00']
trackname(dimension): time
['100.00m', '200.00m', '300.00m', '400.00m', '500.00m', '600.00m', '700.00m', '800.00m', '900.00m', '1.00']
trackname(dimension): voltage
['149.98m', '149.98m', '149.98m', '149.99m', '149.98m', '149.99m', '149.99m', '149.98m', '149.98m', '149.98m']
EIS with Different Channel Configurations
To demonstrate the various EIS configurations, an additional helper function has been created to measure an impedance spectrum with a single point and then output the various impedance traces.
To simplify the handling of impedance data, the EisDataset is available with its various data objects:
First, all impedances are output with get_impedances_data(), which returns a dictionary containing the numerator, denominator, and the corresponding impedance as values.
Next, all PathData is retrieved from the object. Then, all impedances in which the respective path is involved are output.
def eis_and_print(pot_url, bias, coupling):
switch_on_job = zl.control.SwitchOnJob(
potentiostat=pot_url,
coupling=coupling,
bias=bias,
voltage_range_index=0,
compliance_range_index=0,
)
link.do_job(switch_on_job)
frequencies_to_measure = [200]
eis_dict_table_job = zl.meas.EisFrequencyTableJob(
{
"bias": bias,
"spectrum": [
{
"frequency": freq,
"amplitude": bias * 0.1,
"pre_duration": 0.1,
"pre_waves": 1,
"meas_duration": 0.5,
"meas_waves": 3,
}
for freq in frequencies_to_measure
],
}
)
link.do_job(eis_dict_table_job)
link.do_job(zl.control.SwitchOffJob(potentiostat=pot_url))
eis_data = link.get_job_result_data(eis_dict_table_job)
def impedance_as_string(imp):
ohmFormatter = EngFormatter(unit="Ω", places=3)
degreeFormatter = EngFormatter(unit="°", places=3, sep="")
return f"|Z| = {ohmFormatter.format_eng(np.abs(imp.get_calculated_complex_impedance_track()[0]))} Phase = {degreeFormatter.format_eng(np.angle(imp.get_calculated_complex_impedance_track()[0], deg=True))}"
print("All impedances from the EIS measurement:")
for (n, d), imp in eis_data.get_impedances_data().items():
print(f"\t{n}/{d}: {impedance_as_string(imp)}")
print("\nImpedances grouped by dimension:")
paths_data = eis_data.get_paths_data()
for i, (dimension, data) in enumerate(paths_data.items()):
print(f'Dimension: "{dimension}" is involved in the following impedances:')
for (n, d), imp in data.get_impedances_data().items():
print(f"\t{n}/{d}: {impedance_as_string(imp)}")
print()
Default Configuration of the MAIN Potentiostat
This measurement shows that there is only one impedance pair.
link.do_job(
zl.control.SetHardwareSettingsJob(config=config_for_main_pot)
)
eis_and_print(main_pot.url, 0.1, zl.PotentiostatCoupling.POTENTIOSTATIC)
All impedances from the EIS measurement:
voltage/current: |Z| = 379.219 Ω Phase = -28.524°
Impedances grouped by dimension:
Dimension: "current" is involved in the following impedances:
voltage/current: |Z| = 379.219 Ω Phase = -28.524°
Dimension: "voltage" is involved in the following impedances:
voltage/current: |Z| = 379.219 Ω Phase = -28.524°
PAD4 Card Connected to the MAIN Potentiostat
The configuration is now extended by the four PAD4 channels of card 2, which are connected to the MAIN potentiostat.
As you can see, there are now four additional impedance pairs, such as pad4_2_1/current, where the PAD4 channel is divided by the MAIN current channel.
It is also apparent that only the current channel is involved in multiple impedances, while all other channels are involved only once.
link.do_job(
zl.control.SetHardwareSettingsJob(config=config_for_main_pot_pad4)
)
eis_and_print(main_pot.url, 0.1, zl.PotentiostatCoupling.POTENTIOSTATIC)
All impedances from the EIS measurement:
pad4_2_1/current: |Z| = 379.128 Ω Phase = -28.515°
pad4_2_2/current: |Z| = 379.119 Ω Phase = -28.513°
pad4_2_3/current: |Z| = 379.173 Ω Phase = -28.517°
pad4_2_4/current: |Z| = 379.127 Ω Phase = -28.506°
voltage/current: |Z| = 379.158 Ω Phase = -28.519°
Impedances grouped by dimension:
Dimension: "current" is involved in the following impedances:
pad4_2_1/current: |Z| = 379.128 Ω Phase = -28.515°
pad4_2_2/current: |Z| = 379.119 Ω Phase = -28.513°
pad4_2_3/current: |Z| = 379.173 Ω Phase = -28.517°
pad4_2_4/current: |Z| = 379.127 Ω Phase = -28.506°
voltage/current: |Z| = 379.158 Ω Phase = -28.519°
Dimension: "pad4_2_1" is involved in the following impedances:
pad4_2_1/current: |Z| = 379.128 Ω Phase = -28.515°
Dimension: "pad4_2_2" is involved in the following impedances:
pad4_2_2/current: |Z| = 379.119 Ω Phase = -28.513°
Dimension: "pad4_2_3" is involved in the following impedances:
pad4_2_3/current: |Z| = 379.173 Ω Phase = -28.517°
Dimension: "pad4_2_4" is involved in the following impedances:
pad4_2_4/current: |Z| = 379.127 Ω Phase = -28.506°
Dimension: "voltage" is involved in the following impedances:
voltage/current: |Z| = 379.158 Ω Phase = -28.519°
PAD4 Card Connected to the External Potentiostat
Now, the configuration is changed so that the measurements are performed on the external potentiostat, and PAD4 card 1 is used, with its channels connected to the external potentiostat.
link.do_job(
zl.control.SetHardwareSettingsJob(config=config_external_pot_pad4)
)
eis_and_print(external_pot.url, 15, zl.PotentiostatCoupling.GALVANOSTATIC)
All impedances from the EIS measurement:
pad4_1_1/current: |Z| = 10.070 mΩ Phase = 1.355m°
pad4_1_2/current: |Z| = 10.070 mΩ Phase = 2.078m°
pad4_1_3/current: |Z| = 10.069 mΩ Phase = 73.282µ°
pad4_1_4/current: |Z| = 10.069 mΩ Phase = 3.130m°
voltage/current: |Z| = 10.075 mΩ Phase = -1.514m°
Impedances grouped by dimension:
Dimension: "current" is involved in the following impedances:
pad4_1_1/current: |Z| = 10.070 mΩ Phase = 1.355m°
pad4_1_2/current: |Z| = 10.070 mΩ Phase = 2.078m°
pad4_1_3/current: |Z| = 10.069 mΩ Phase = 73.282µ°
pad4_1_4/current: |Z| = 10.069 mΩ Phase = 3.130m°
voltage/current: |Z| = 10.075 mΩ Phase = -1.514m°
Dimension: "pad4_1_1" is involved in the following impedances:
pad4_1_1/current: |Z| = 10.070 mΩ Phase = 1.355m°
Dimension: "pad4_1_2" is involved in the following impedances:
pad4_1_2/current: |Z| = 10.070 mΩ Phase = 2.078m°
Dimension: "pad4_1_3" is involved in the following impedances:
pad4_1_3/current: |Z| = 10.069 mΩ Phase = 73.282µ°
Dimension: "pad4_1_4" is involved in the following impedances:
pad4_1_4/current: |Z| = 10.069 mΩ Phase = 3.130m°
Dimension: "voltage" is involved in the following impedances:
voltage/current: |Z| = 10.075 mΩ Phase = -1.514m°
Impedance Between PAD4 Channels
The following example demonstrates how to calculate the impedance between PAD4 channels using the get_config_for_potentiostat_with_sync_channel_impedance() function for configuration. In this setup, the same PAD4 channel is used for the numerator and denominator.
Additionally, the pad4_shunt_resistance variable is used to specify a polynomial for the PAD4 if a current is to be measured with a PAD4 card and the voltage needs to be converted into current using the shunt value.
On the DC tracks, you can see at pad4_as_current that this is now pad4_as_voltage * pad4_shunt_resistance, since both channels are connected to the same object. The impedance is therefore 10 Ω with a phase of approximately 0°.
pad4_shunt_resistance = 0.1
config_for_main_pot_pad4_impedance = (
zl.HardwareSettingsHelper.get_config_for_potentiostat_with_sync_channel_impedance(
pot_serial_number=main_pot.serialnumber,
connections=[
zl.Pad4ImpedanceConfiguration(
numerator=zl.Pad4Connection(
2,
1,
polynomial=zl.UserPolynomial([0, 1]),
dimension=f"pad4_as_voltage",
unit="V",
),
denominator=zl.Pad4Connection(
2,
2,
polynomial=zl.UserPolynomial([0, pad4_shunt_resistance]),
dimension=f"pad4_as_current",
unit="A",
),
)
],
)
)
link.do_job(
zl.control.SetHardwareSettingsJob(config=config_for_main_pot_pad4_impedance)
)
poga_and_print(main_pot.url, 0.1, zl.PotentiostatCoupling.POTENTIOSTATIC)
eis_and_print(main_pot.url, 0.1, zl.PotentiostatCoupling.POTENTIOSTATIC)
Tracks: dict_keys(['current', 'debug', 'direction', 'pad4_as_current', 'pad4_as_voltage', 'shunt', 'time', 'voltage'])
data per track:
trackname(dimension): current
['102.50µ', '102.50µ', '102.51µ', '102.50µ', '102.50µ', '102.51µ', '102.50µ', '102.50µ', '102.50µ', '102.50µ']
trackname(dimension): debug
['0.00', '0.00', '0.00', '0.00', '0.00', '0.00', '0.00', '0.00', '0.00', '0.00']
trackname(dimension): direction
['0.00', '0.00', '0.00', '0.00', '0.00', '0.00', '0.00', '0.00', '0.00', '0.00']
trackname(dimension): pad4_as_current
['10.00m', '10.00m', '10.00m', '10.00m', '10.00m', '10.00m', '10.00m', '10.00m', '10.00m', '10.00m']
trackname(dimension): pad4_as_voltage
['100.04m', '100.04m', '100.04m', '100.04m', '100.04m', '100.04m', '100.04m', '100.04m', '100.04m', '100.04m']
trackname(dimension): shunt
['6.00', '6.00', '6.00', '6.00', '6.00', '6.00', '6.00', '6.00', '6.00', '6.00']
trackname(dimension): time
['100.00m', '200.00m', '300.00m', '400.00m', '500.00m', '600.00m', '700.00m', '800.00m', '900.00m', '1.00']
trackname(dimension): voltage
['100.04m', '100.04m', '100.04m', '100.04m', '100.04m', '100.04m', '100.04m', '100.04m', '100.04m', '100.04m']
All impedances from the EIS measurement:
pad4_as_voltage/pad4_as_current: |Z| = 9.999 Ω Phase = -6.528m°
voltage/current: |Z| = 379.180 Ω Phase = -28.521°
Impedances grouped by dimension:
Dimension: "current" is involved in the following impedances:
voltage/current: |Z| = 379.180 Ω Phase = -28.521°
Dimension: "pad4_as_current" is involved in the following impedances:
pad4_as_voltage/pad4_as_current: |Z| = 9.999 Ω Phase = -6.528m°
Dimension: "pad4_as_voltage" is involved in the following impedances:
pad4_as_voltage/pad4_as_current: |Z| = 9.999 Ω Phase = -6.528m°
Dimension: "voltage" is involved in the following impedances:
voltage/current: |Z| = 379.180 Ω Phase = -28.521°
link.disconnect()