Examples
Learn how to use the nibcq package in various battery cell quality testing scenarios. Each example includes a brief description and a corresponding code snippet.
Configuration and Support Files
The following examples use JSON files. Use these files to create a portable configuration.
ACIR Test Parameters Configuration File
This example file holds ACIR test parameters.
1{
2 "Nominal DUT Voltage (V)":3.6,
3 "Voltage Limit Hi (V)":4.2,
4 "Current Amplitude (A)":0.6,
5 "Number of Periods":100,
6 "Compensation Method":"Short",
7 "Power Line Frequency":50
8}
EIS Test Parameters Configuration File
This example file holds EIS test parameters.
1{
2 "Voltage Limit Hi (V)": 4.2,
3 "Nominal DUT Voltage (V)": 3.6,
4 "Compensation Method": "Short",
5 "Power Line Frequency": 50,
6 "Frequency Sweep Characteristics": [
7 {
8 "Frequency (Hz)": 0.1,
9 "Current Amplitude (A)": 0.6,
10 "Number of Periods": 2
11 },
12 {
13 "Frequency (Hz)": 1,
14 "Current Amplitude (A)": 0.6,
15 "Number of Periods": 4
16 },
17 {
18 "Frequency (Hz)": 10,
19 "Current Amplitude (A)": 0.6,
20 "Number of Periods": 4
21 },
22 {
23 "Frequency (Hz)": 20,
24 "Current Amplitude (A)": 0.6,
25 "Number of Periods": 4
26 },
27 {
28 "Frequency (Hz)": 40,
29 "Current Amplitude (A)": 0.6,
30 "Number of Periods": 4
31 },
32 {
33 "Frequency (Hz)": 50,
34 "Current Amplitude (A)": 0.6,
35 "Number of Periods": 5
36 },
37 {
38 "Frequency (Hz)": 60,
39 "Current Amplitude (A)": 0.6,
40 "Number of Periods": 6
41 },
42 {
43 "Frequency (Hz)": 80,
44 "Current Amplitude (A)": 0.6,
45 "Number of Periods": 8
46 },
47 {
48 "Frequency (Hz)": 100,
49 "Current Amplitude (A)": 0.6,
50 "Number of Periods": 10
51 },
52 {
53 "Frequency (Hz)": 120,
54 "Current Amplitude (A)": 0.6,
55 "Number of Periods": 12
56 },
57 {
58 "Frequency (Hz)": 140,
59 "Current Amplitude (A)": 0.6,
60 "Number of Periods": 14
61 },
62 {
63 "Frequency (Hz)": 200,
64 "Current Amplitude (A)": 0.6,
65 "Number of Periods": 20
66 },
67 {
68 "Frequency (Hz)": 300,
69 "Current Amplitude (A)": 0.6,
70 "Number of Periods": 30
71 },
72 {
73 "Frequency (Hz)": 400,
74 "Current Amplitude (A)": 0.6,
75 "Number of Periods": 40
76 },
77 {
78 "Frequency (Hz)": 500,
79 "Current Amplitude (A)": 0.6,
80 "Number of Periods": 50
81 },
82 {
83 "Frequency (Hz)": 600,
84 "Current Amplitude (A)": 0.6,
85 "Number of Periods": 60
86 },
87 {
88 "Frequency (Hz)": 800,
89 "Current Amplitude (A)": 0.6,
90 "Number of Periods": 80
91 },
92 {
93 "Frequency (Hz)": 1200,
94 "Current Amplitude (A)": 0.6,
95 "Number of Periods": 120
96 },
97 {
98 "Frequency (Hz)": 1400,
99 "Current Amplitude (A)": 0.6,
100 "Number of Periods": 140
101 },
102 {
103 "Frequency (Hz)": 1800,
104 "Current Amplitude (A)": 0.6,
105 "Number of Periods": 180
106 },
107 {
108 "Frequency (Hz)": 2000,
109 "Current Amplitude (A)": 0.6,
110 "Number of Periods": 200
111 },
112 {
113 "Frequency (Hz)": 3000,
114 "Current Amplitude (A)": 0.6,
115 "Number of Periods": 300
116 },
117 {
118 "Frequency (Hz)": 4000,
119 "Current Amplitude (A)": 0.6,
120 "Number of Periods": 400
121 },
122 {
123 "Frequency (Hz)": 5000,
124 "Current Amplitude (A)": 0.6,
125 "Number of Periods": 500
126 }
127 ]
128}
OCV Test Parameters Configuration File
This example file holds OCV test parameters.
1{
2 "Range":10,
3 "Aperture time (PLCs)":1,
4 "Number Of Averages":4,
5 "Powerline Frequency":"50 Hz",
6 "ADC Calibration":1
7}
SMU Switching Configuration
This switch configuration file is used by SMU-based measurement (ACIR, EIS) examples. These examples include sourcing and sensing switch resources.
1{
2 "Switches Topology": "2-Wire Quad 16x1 Mux",
3 "Cells": [
4 {
5 "Cell Serial Number": "12345a",
6 "Source Switch DUT Channel": "ch0",
7 "Sense Switch DUT Channel": "ch33",
8 "Source Channel": "com0",
9 "Sense Channel": "com2",
10 "Jig ID": "123a"
11 },
12 {
13 "Cell Serial Number": "12345b",
14 "Source Switch DUT Channel": "ch3",
15 "Sense Switch DUT Channel": "ch34",
16 "Source Channel": "com0",
17 "Sense Channel": "com2",
18 "Jig ID": "123b"
19 }
20 ]
21}
DMM switching configuration
This switch configuration file is used by DMM-based measurement (OCV) examples. These examples include sensing switch resources.
1{
2 "Switches Topology": "2-Wire Quad 16x1 Mux",
3 "Cells": [
4 "ch0",
5 "ch3"
6 ]
7}
Known Impedance Table (KIT) file
KIT files are used by the compensation-with-kit examples. This file provides short impedance values of a known resistor. These values are then used to create a short compensation file. The KIT file values are subtracted when creating the compensation file. As such, the compensation file only records the impedance of the jig.
1[
2 {
3 "Frequency (Hz)": 100.0,
4 "Compensation Value (Ohm)": "0.000002 +0 i"
5 },
6 {
7 "Frequency (Hz)": 1000.0,
8 "Compensation Value (Ohm)": "0.000002 +0.000000000001 i"
9 },
10 {
11 "Frequency (Hz)": 10000.0,
12 "Compensation Value (Ohm)": "0.000002 +0.000000001 i"
13 }
14]
ACIR Examples
ACIR Compensation File creation
This is an example showing basic ACIR compensation file creation.
First, it initializes the device with context manager. To set this up, you can use VISA names and nibcq.enums.DeviceFamily for device type, which should be SMU.
Using a Thermocouple is suggested for this step.
The test parameters are read from a config file, on the default path.
from nibcq import ACIR, ACIRTestParameters, Calibrator, Device
from nibcq.calibration import Settings
from nibcq.enums import DeviceFamily
from nibcq.temperature import ThermocoupleSettings
def main():
"""This example demonstrates how to create a compensation file for ACIR measurement.
Raises:
RuntimeError: If the device's last self calibration is not valid.
"""
# Set up the temperature for the measurement in the default way.
thermocouple_settings = ThermocoupleSettings("MyThermocouple/ai0")
# Initialize device and use it
with Device.create(
DeviceFamily.SMU,
resource_name="MySMU",
).with_temperature(thermocouple_settings, temperature_delta=2.5) as device:
# Validate the device latest self-calibration. This is optional.
if not Calibrator(device, Settings(2, 1)).last_calibration_is_valid:
raise RuntimeError("Device's last self calibration is not valid. Please run a new one!")
# Configure test parameters.
test_parameters = ACIRTestParameters.from_file("examples/resources/ACIRConfigFile.json")
# Set up a Measurement class. This will handle running the measurement.
acir_measurement = ACIR(device, test_parameters, test_frequency=1000.0)
# Create a new Compensation File
# Optional: You can use a kit_file_path if you use Short Compensation Method.
# It substarcts the values from the KIT before saving the compensation file.
print("Starting Measurement...")
new_file_path = acir_measurement.write_compensation_file(
comment="Example ACIR Compensation File"
)
# Print the path:
print(f"New ACIR Compensation file created! Path:\n\t{new_file_path}")
if __name__ == "__main__":
main()
ACIR Compensation File Creation with Known Impedance Table
ACIR example that uses SHORT compensation from a KIT file.
This example is similar to acir_create_compensation_file.py, but demonstrates creating a compensation file while using a Known Impedance Table (KIT) file. This will apply short compensation subtraction when saving the file.
Runs an ACIR measurement and saves a SHORT compensation file by passing kit_file_path to ACIR.write_compensation_file, which applies the KIT short values during the save step.
from nibcq import Calibrator, ACIR, Device, ACIRTestParameters
from nibcq.calibration import Settings
from nibcq.enums import DeviceFamily
def main():
"""Create a SHORT compensation file for ACIR using a KIT file.
Raises:
RuntimeError: If the device's last self calibration is not valid.
"""
# Initialize device and use it
with Device.create(DeviceFamily.SMU, resource_name="MySMU") as device:
# Optionally validate last calibration
if not Calibrator(device, Settings(2, 1)).last_calibration_is_valid:
raise RuntimeError("Device's last self calibration is not valid. Please run a new one!")
# Read test parameters
test_parameters = ACIRTestParameters.from_file("examples/resources/ACIRConfigFile.json")
acir_measurement = ACIR(device, test_parameters, test_frequency=1000.0)
# Create a new compensation file, applying the KIT short values during save
kit_path = "examples/resources/KIT_Short_2mOhm.json"
print("Starting Measurement to create SHORT compensation file using KIT...")
new_file_path = acir_measurement.write_compensation_file(
comment="Example SHORT compensation using KIT",
kit_file_path=kit_path,
)
# Print the path of the newly created compensation file
print(f"New SHORT compensation file created: {new_file_path}")
if __name__ == "__main__":
main()
Simple ACIR Measurement
Example showing basic ACIR functionality.
First, it initializes the device with context manager. To set this up, you can use VISA names and nibcq.enums.DeviceFamily for device type, which should be SMU.
from nibcq import Calibrator, ACIR, ACIRTestParameters, Device
from nibcq.calibration import Settings
from nibcq.enums import DeviceFamily
from nibcq.measurement import SMUResult
def main():
"""A Simple ACIR Measurement.
Raises:
RuntimeError: If the device's last self calibration is not valid.
"""
# Initialize device and use it
with Device.create(DeviceFamily.SMU, resource_name="MySMU") as device:
# Validate the device latest self-calibration. This is optional.
if not Calibrator(device, Settings(2, 1)).last_calibration_is_valid:
raise RuntimeError("Device's last self calibration is not valid. Please run a new one!")
# Read in test parameters.
test_parameters = ACIRTestParameters.from_file("examples/resources/ACIRConfigFile.json")
# Set up a Measurement class. This will handle running the measurement.
acir_measurement = ACIR(device, test_parameters, test_frequency=1000.0)
# Load Compensation File
acir_compensation = acir_measurement.load_compensation_file()
# Complete with run
print("Starting Measurement...")
result: SMUResult = acir_measurement.run(acir_compensation)
# Print the results:
print(
f"Measured Tone Frequency: {acir_measurement.measurement_data.tone_frequency} Hz\n"
f"Impedance: {result.impedance}\n"
f"Full Results:\n{result}"
)
if __name__ == "__main__":
main()
ACIR with Raw Data Saving
Example showing basic ACIR functionality with saving the raw measurement data.
First, it initializes the device with context manager. To set this up, you can use VISA names and nibcq.enums.DeviceFamily for device type, which should be SMU.
The raw measurements are saved to a JSON file.
import json
from nibcq import ACIR, ACIRTestParameters, Calibrator, Device
from nibcq.calibration import Settings
from nibcq.enums import CompensationMethod, DeviceFamily, PowerlineFrequency
from nibcq.measurement import SMUResult
def main():
"""A Simple ACIR Measurement with raw data saving.
Raises:
RuntimeError: If the device's last self calibration is not valid.
"""
# Initialize device and use it
with Device.create(DeviceFamily.SMU, resource_name="MySMU") as device:
# Validate the device latest self-calibration. This is optional.
if not Calibrator(device, Settings(2, 1)).last_calibration_is_valid:
raise RuntimeError("Device's last self calibration is not valid. Please run a new one!")
# Configure test parameters.
test_parameters = ACIRTestParameters(
voltage_limit_hi=4.2,
nominal_voltage=3.6,
current_amplitude=0.6,
number_of_periods=20,
powerline_frequency=PowerlineFrequency.FREQ_50_HZ,
compensation_method=CompensationMethod.SHORT,
)
# Set up a Measurement class. This will handle running the measurement.
acir_measurement = ACIR(device, test_parameters, test_frequency=1000.0)
# Load Compensation File
acir_compensation = acir_measurement.load_compensation_file()
# Complete with run
print("Starting Measurement...")
result: SMUResult = acir_measurement.run(acir_compensation)
# Save SMUMeasurement data to a JSON file.
file_path = "raw_acir_measurement.json"
print(f"Saving Raw data to {file_path}...")
data = {
"tone_frequency": acir_measurement.measurement_data.tone_frequency,
"voltage_values": acir_measurement.measurement_data.voltage_values,
"current_values": acir_measurement.measurement_data.current_values,
}
with open(file_path, "w") as f:
json.dump(data, f, indent=2)
# Print the results:
print(
f"Measured Tone Frequency: {acir_measurement.measurement_data.tone_frequency} Hz\n"
f"Impedance: {result.impedance}\n"
f"Full Results:\n{result}"
)
if __name__ == "__main__":
main()
ACIR with Temperature Measurement
Example showing basic ACIR functionality with additional temperature measurement.
First, it initializes the device with context manager. To set this up, you can use VISA names and nibcq.enums.DeviceFamily for device type, which should be SMU.
The temperature measurement has to be initialized with the device, but from then the device handles everything. It allows the calibration file to be validated against the current environment’s temperature measurements.
from nibcq import Calibrator, ACIR, Device, ACIRTestParameters
from nibcq.calibration import Settings
from nibcq.enums import DeviceFamily
from nibcq.measurement import SMUResult
from nibcq.temperature import ThermocoupleSettings
def main():
"""A Simple ACIR Measurement with temperature support.
Raises:
RuntimeError: If the device's last self calibration is not valid.
ValueError: If temperature validation fails.
"""
# Set up the temperature for the measurement in the default way.
thermocouple_settings = ThermocoupleSettings("MyThermocouple/ai0")
# Initialize device and use it
with Device.create(
DeviceFamily.SMU,
resource_name="MySMU",
).with_temperature(thermocouple_settings) as device:
# Validate the device latest self-calibration. This is optional.
if not Calibrator(device, Settings(2, 1)).last_calibration_is_valid:
raise RuntimeError("Device's last self calibration is not valid. Please run a new one!")
# Read in test parameters.
test_parameters = ACIRTestParameters.from_file("examples/resources/ACIRConfigFile.json")
# Set up a Measurement class. This will handle running the measurement.
acir_measurement = ACIR(device, test_parameters, test_frequency=1000.0)
# Load Compensation File
acir_compensation = acir_measurement.load_compensation_file()
# Optional - Overwrite acceptable temperature delta for compensation validation
acir_measurement.acceptable_temperature_delta = 5.0 # degrees Celsius
# Complete with run
print("Starting Measurement...")
result: SMUResult = acir_measurement.run(acir_compensation)
# Print the results:
print(
f"Measured Tone Frequency: {acir_measurement.measurement_data.tone_frequency} Hz\n"
f"Impedance: {result.impedance}\n"
f"Full Results:\n{result}"
)
# Optional - Measure temperature
acir_measurement.measure_temperature()
print(f"Current Temperature: {acir_measurement.temperature} °C")
if __name__ == "__main__":
main()
ACIR with Switching
This example demonstrates how to set up and run an ACIR measurement with switching.
First, it initializes the device with context manager. To set this up, you can use VISA names and nibcq.enums.DeviceFamily for device type, which should be SMU.
The Switch has to be initialized with the device, but from then it handles everything.
from nibcq import Calibrator, ACIR, Device, ACIRTestParameters
from nibcq.calibration import Settings
from nibcq.enums import DeviceFamily
from nibcq.measurement import SMUResult
from nibcq.switch import SwitchConfiguration
def main():
"""A Simple ACIR Measurement with switching support.
Raises:
RuntimeError: If the device's last self calibration is not valid.
"""
# Set up switching
switching_config = SwitchConfiguration.from_file("examples/resources/SMUSwitchConfig.json")
# Initialize device and use it
with Device.create(DeviceFamily.SMU, resource_name="PXIe-4139").with_switching(
config=switching_config,
sense_switch_resource_name="MySensingSwitch",
source_switch_resource_name="MySourcingSwitch",
) as device:
# Validate the device latest self-calibration. This is optional.
if not Calibrator(device, Settings(2, 1)).last_calibration_is_valid:
raise RuntimeError("Device's last self calibration is not valid. Please run a new one!")
# Read in test parameters.
test_parameters = ACIRTestParameters.from_file("examples/resources/ACIRConfigFile.json")
# Set up a Measurement class. This will handle running the measurement.
acir_measurement = ACIR(device, test_parameters, test_frequency=1000.0)
# Load Compensation File
acir_compensation = acir_measurement.load_compensation_file()
# Complete with run
print("Starting Measurement...")
cell_results: SMUResult = acir_measurement.run_with_switching(acir_compensation)
# Print the results:
for cell_index, (cell_data, result) in enumerate(cell_results):
print(f" ===== \nCell {cell_index} - {cell_data}:")
print(
f"Measured Tone Frequency: {acir_measurement.measurement_data.tone_frequency} Hz\n"
f"Impedance: {result.impedance}\n"
f"Full Results:\n{result}"
)
if __name__ == "__main__":
main()
ACIR with Switching and Temperature Measurement
Example combining ACIR measurements with both temperature monitoring and switching capabilities.
This example demonstrates a comprehensive ACIR measurement scenario, combining: - Multiple DUTs via switch matrix - Temperature measurement and validation - Compensation file loading and validation
The example shows proper error handling for temperature validation failures, which can occur when the measured temperature differs significantly from the compensation file’s target temperature.
from nibcq import ACIR, ACIRTestParameters, Calibrator, Device
from nibcq.calibration import Settings
from nibcq.enums import DeviceFamily
from nibcq.switch import SwitchConfiguration
from nibcq.temperature import ThermocoupleSettings
def main():
"""ACIR Measurement with both switching and temperature support.
Raises:
RuntimeError: If the device's last self calibration is not valid.
ValueError: If temperature validation fails for any cell.
"""
# Set up the temperature measurement
thermocouple_settings = ThermocoupleSettings("MyThermocouple/ai0")
# Set up switching configuration
switching_config = SwitchConfiguration.from_file("examples/resources/SMUSwitchConfig.json")
# Initialize device with both temperature and switching capabilities
with Device.create(
DeviceFamily.SMU,
resource_name="MySMU",
).with_temperature(thermocouple_settings).with_switching(
config=switching_config,
sense_switch_resource_name="MySensingSwitch",
source_switch_resource_name="MySourcingSwitch",
) as device:
# Validate the device latest self-calibration. This is optional.
if not Calibrator(device, Settings(2, 1)).last_calibration_is_valid:
raise RuntimeError("Device's last self calibration is not valid. Please run one!")
# Read in test parameters
test_parameters = ACIRTestParameters.from_file("examples/resources/ACIRConfigFile.json")
# Set up a Measurement class
acir_measurement = ACIR(device, test_parameters, test_frequency=1000.0)
# Load Compensation File
acir_compensation = acir_measurement.load_compensation_file()
# Optional - Overwrite acceptable temperature delta for compensation validation
acir_measurement.acceptable_temperature_delta = 5.0 # degrees Celsius
# Run Measurement with proper error handling
print("Starting Measurement with Temperature Monitoring and Switching...")
try:
results_per_cell = acir_measurement.run_with_switching(compensation=acir_compensation)
except ValueError as e:
# Temperature validation failures raise ValueError
if "temperature" in str(e).lower():
print(f"Temperature validation failed: {e}")
print("Measurement aborted. Please check environmental conditions.")
print("Continuing to retrieve any results obtained before the failure...")
results_per_cell = acir_measurement.result
else:
raise # Re-raise if it's a different ValueError
# Format and print results
for i, (cell_data, result) in enumerate(results_per_cell):
print(f"\n===== Cell {i} - {cell_data} =====")
print(
f"Measured Tone Frequency: {result.measured_frequency} Hz\n"
f"Impedance: {result.impedance}\n"
f"Full Results:\n{result}"
)
# Optional - Measure final temperature
acir_measurement.measure_temperature()
print(f"\nFinal Temperature: {acir_measurement.temperature} °C")
if __name__ == "__main__":
main()
DCIR Examples
Simple DCIR Measurement
Example showing basic DCIR functionality.
First, it initializes the device with context manager. To set this up, you can use VISA names and nibcq.enums.DeviceFamily for device type, which should be ELOAD.
from nibcq import Calibrator, DCIR, DCIRTestParameters, Device
from nibcq.calibration import Settings
from nibcq.enums import DeviceFamily, PowerlineFrequency
def main():
"""A Simple example code for DCIR Measurement.
Raises:
RuntimeError: If the device's last self calibration is not valid.
"""
# Initialize device and use it
# MyEload is a PXIe-4051
with Device.create(DeviceFamily.ELOAD, resource_name="MyEload") as device:
# Validate the device latest self-calibration. This is optional.
if not Calibrator(device, Settings(2, 1)).last_calibration_is_valid:
raise RuntimeError("Device's last self calibration is not valid. Please run a new one!")
# Define test parameters.
test_parameters = DCIRTestParameters(
powerline_frequency=PowerlineFrequency.FREQ_50_HZ,
max_load_current=0.6,
)
# Set up a Measurement class. This will handle running the measurement.
dcir_measurement = DCIR(device, test_parameters)
# Complete with run
print("Starting Measurement...")
result = dcir_measurement.run()
# Print the results:
print(f"DC Resistance: {result} Ohms\n")
if __name__ == "__main__":
main()
DCIR with Raw Data Saving
Example showing basic DCIR functionality.
First, it initializes the device with context manager. To set this up, you can use VISA names and nibcq.enums.DeviceFamily for device type, which should be ELOAD.
The raw measurements are saved to a JSON file.
import json
from nibcq import Calibrator, DCIR, DCIRTestParameters, Device
from nibcq.calibration import Settings
from nibcq.enums import DeviceFamily, PowerlineFrequency
def main():
"""A Simple example code for DCIR Measurement.
Raises:
RuntimeError: If the device's last self calibration is not valid.
"""
# Initialize device and use it
# MyEload is a PXIe-4051
with Device.create(DeviceFamily.ELOAD, resource_name="MyEload") as device:
# Validate the device latest self-calibration. This is optional.
if not Calibrator(device, Settings(2, 1)).last_calibration_is_valid:
raise RuntimeError("Device's last self calibration is not valid. Please run a new one!")
# Define test parameters.
test_parameters = DCIRTestParameters(
powerline_frequency=PowerlineFrequency.FREQ_50_HZ,
max_load_current=0.6,
)
# Set up a Measurement class. This will handle running the measurement.
dcir_measurement = DCIR(device, test_parameters)
# Complete with run
print("Starting Measurement...")
result = dcir_measurement.run()
# Save SMUMeasurement data to a JSON file.
file_path = "raw_dcir_measurement.json"
print(f"Saving Raw data to {file_path}...")
data = {
"voltage_values": dcir_measurement.measurement_data.voltage_values,
"current_values": dcir_measurement.measurement_data.current_values,
}
with open(file_path, "w") as f:
json.dump(data, f, indent=2)
# Print the results:
print(f"DC Resistance: {result} Ohms\n")
if __name__ == "__main__":
main()
EIS examples
EIS Compensation File creation
Example showing basic EIS compensation file creation.
First, it initializes the device with context manager. To set this up, you can use VISA names and nibcq.enums.DeviceFamily for device type, which should be SMU.
Using a Thermocouple is suggested for this step.
The test parameters are read from a config file, on the default path.
from nibcq import Calibrator, Device, EIS, EISTestParameters
from nibcq.calibration import Settings
from nibcq.enums import DeviceFamily
from nibcq.temperature import ThermocoupleSettings
def main():
"""Creates a new compensation file for EIS measurement.
Raises:
RuntimeError: If the device's last self calibration is not valid.
"""
thermocouple_settings = ThermocoupleSettings("MyThermocouple/ai0")
# Initializes the device with context manager.
with Device.create(
DeviceFamily.SMU,
resource_name="MySMU",
).with_temperature(thermocouple_settings, temperature_delta=2.5) as device:
# Validate the device latest self-calibration. This is optional.
if not Calibrator(device, Settings(2, 1)).last_calibration_is_valid:
raise RuntimeError("Device's last self calibration is not valid. Please run one!")
# Read in test parameters
test_parameters = EISTestParameters.from_file("examples/resources/EISConfigFile.json")
# Set up a Measurement class. This will handle running the measurement.
eis_measurement = EIS(device, test_parameters)
# Create a new Compensation File
# Optional: You can use a kit_file_path if you use Short Compensation Method.
# It substarcts the values from the KIT before saving the compensation file.
print("Starting Measurement...")
new_file_path = eis_measurement.write_compensation_file(
comment="Example EIS Compensation File",
measurement_callback=lambda smu_measurement: print(
f"Measured frequency: {smu_measurement.tone_frequency:.1f} Hz - "
f"{len(smu_measurement.voltage_values)} samples acquired"
),
)
# Print the path:
print(f"New EIS Compensation file created! Path:\n\t{new_file_path}")
if __name__ == "__main__":
main()
EIS Compensation File Creation with Known Impedance Table
Create an EIS compensation file while applying KIT (short) values.
Runs an EIS measurement and saves a compensation file by passing kit_file_path to EIS.write_compensation_file, which applies the KIT short values during the save step.
Follows the pattern in examples/eis_create_compensation_file.py, but demonstrates creating a compensation file while using a Known Impedance Table (KIT) file. This will apply short compensation subtraction when saving the file.
from nibcq import Calibrator, Device, EIS, EISTestParameters
from nibcq.calibration import Settings
from nibcq.enums import DeviceFamily
from nibcq.temperature import ThermocoupleSettings
def main():
"""EIS example that creates a compensation file using a KIT (short) file.
Raises:
RuntimeError: If the device's last self calibration is not valid.
"""
thermocouple_settings = ThermocoupleSettings("MyThermocouple/ai0")
with Device.create(
DeviceFamily.SMU,
resource_name="MySMU",
).with_temperature(thermocouple_settings, temperature_delta=2.5) as device:
# Validate calibration
if not Calibrator(device, Settings(2, 1)).last_calibration_is_valid:
raise RuntimeError("Device's last self calibration is not valid. Please run one!")
# Load test parameters and prepare measurement
test_parameters = EISTestParameters.from_file("examples/resources/EISConfigFile.json")
eis_measurement = EIS(device, test_parameters)
# KIT path to apply during saving
kit_path = "examples/resources/KIT_Short_2mOhm.json"
print("Starting EIS measurement sequence to create compensation file with KIT...")
new_file_path = eis_measurement.write_compensation_file(
comment="Example EIS compensation (KIT applied)",
kit_file_path=kit_path,
measurement_callback=lambda smu_measurement: print(
f"Measured frequency: {smu_measurement.tone_frequency:.1f} Hz - "
f"{len(smu_measurement.voltage_values)} samples acquired"
),
)
print(f"New EIS Compensation file created: {new_file_path}")
if __name__ == "__main__":
main()
Simple EIS Measurement
Example showing basic EIS functionality and plotting.
- This example runs an EIS measurement and then creates three plots using matplotlib:
Nyquist (Cole) plot: R vs -X
Bode magnitude: abs(Z) vs frequency
Bode phase: theta vs frequency
Plotting is done using matplotlib, which is an optional dependency. The required lists
are requested from the EIS measurement object using the new EIS.get_plots() method.
The values are extracted from the returned list of SMUResult objects so
no changes to the core EIS API are required to recreate the functionality.
The extracted results are then plotted in three subplots: Nyquist (Cole) plot, Bode magnitude, and Bode phase. These get the same functionality as the LabVIEW examples.
from nibcq import Calibrator, Device, EIS, EISTestParameters
from nibcq.calibration import Settings
from nibcq.enums import DeviceFamily
def main():
"""A Simple example code for EIS Measurement.
Raises:
RuntimeError: If the device's last self calibration is not valid.
"""
# Initializes the device with context manager.
with Device.create(DeviceFamily.SMU, resource_name="MySMU") as device:
# Validate the device latest self-calibration. This is optional.
if not Calibrator(device, Settings(2, 1)).last_calibration_is_valid:
raise RuntimeError("Device's last self calibration is not valid. Please run one!")
# Read in test parameters.
test_parameters = EISTestParameters.from_file("examples/resources/EISConfigFile.json")
# Set up a Measurement class. This will handle running the measurement.
eis_measurement = EIS(device, test_parameters)
# Load Compensation File
eis_compensation = eis_measurement.load_compensation_file()
# Run Measurement with callback for progress monitoring
print("Starting Measurement...")
results = eis_measurement.run(
compensation=eis_compensation,
measurement_callback=lambda smu_measurement: print(
f"Measured frequency: {smu_measurement.tone_frequency:.1f} Hz - "
f"{len(smu_measurement.voltage_values)} samples acquired"
),
)
# Format and print results
print("Resulted impedance values:")
for i, frequency in enumerate(eis_measurement.frequency_list):
print(f"Frequency: {frequency} Hz - Impedance: {results[i].impedance} Ohm")
# *** ------------------------------ ***
# * Plotting using EIS.get_plots() *
# *** ------------------------------ ***
try:
import matplotlib.pyplot as plt
except ImportError: # pragma: no cover - optional dependency for examples
print("matplotlib is not installed. Install it to see plots: pip install matplotlib")
return
# Use the new PlotSeries-based API on EIS
nyquist, magnitude, phase = eis_measurement.get_plots()
# Layout: Nyquist spans the top row (full width), magnitude and phase
# occupy the bottom-left and bottom-right respectively.
fig = plt.figure(figsize=(12, 8))
gs = fig.add_gridspec(nrows=2, ncols=2, height_ratios=[1, 1], hspace=0.4, wspace=0.3)
ax_nyq = fig.add_subplot(gs[0, :])
ax_mag = fig.add_subplot(gs[1, 0])
ax_phase = fig.add_subplot(gs[1, 1])
# Nyquist (Cole) plot: Real (R) vs -Imag ( -X ) — wide plot on top
ax_nyq.plot(nyquist.x, nyquist.y, marker="o", linestyle="-")
ax_nyq.set_xlabel("R (Ohm)")
ax_nyq.set_ylabel("-X (Ohm)")
ax_nyq.set_title("Nyquist (Cole) Plot")
ax_nyq.grid(True)
# Bode magnitude: |Z| vs frequency (linear) — bottom-left
ax_mag.plot(magnitude.x, magnitude.y, marker="o", linestyle="-")
ax_mag.set_xlabel("Frequency (Hz)")
ax_mag.set_ylabel("|Z| (Ohm)")
ax_mag.set_title("Bode Magnitude")
ax_mag.grid(True, which="both")
# Bode phase: theta vs frequency (linear) — bottom-right
ax_phase.plot(phase.x, phase.y, marker="o", linestyle="-")
ax_phase.set_xlabel("Frequency (Hz)")
ax_phase.set_ylabel("Phase (deg)")
ax_phase.set_title("Bode Phase")
ax_phase.grid(True, which="both")
plt.show()
if __name__ == "__main__":
main()
EIS with Measurement Data Saving
Example showing basic EIS functionality with saving the raw measurement data.
- This example runs an EIS measurement and then creates three plots using matplotlib:
Nyquist (Cole) plot: R vs -X
Bode magnitude: abs(Z) vs frequency
Bode phase: theta vs frequency
First, it initializes the device with context manager. To set this up, you can use VISA names and nibcq.enums.DeviceFamily for device type, which should be SMU.
The raw measurements are saved to a JSON file with the help of measurement_callback.
Plotting is done using matplotlib, which is an optional dependency. The required lists
are requested from the EIS measurement object using the new EIS.get_plots() method.
The values are extracted from the returned list of SMUResult objects so
no changes to the core EIS API are required to recreate the functionality.
The extracted results are then plotted in three subplots: Nyquist (Cole) plot, Bode magnitude, and Bode phase. These get the same functionality as the LabVIEW examples.
import json
from nibcq import Calibrator, Device, EIS, EISTestParameters, FrequencySet
from nibcq.calibration import Settings
from nibcq.enums import CompensationMethod, DeviceFamily, PowerlineFrequency
from nibcq.measurement import SMUMeasurement
class JSONSerializer:
"""Example test counter for system level tests."""
def __init__(self, list_size: float):
"""Initialize any StepCounter to start with 0, first call will result in 1."""
self.count = 0
self.max_count = list_size
def __call__(self, measurement: SMUMeasurement):
"""Increments counter and saves file."""
self.count += 1
print(
f"{len(measurement.voltage_values)} voltage and "
f"{len(measurement.current_values)} current values were measured."
)
# Save SMUMeasurement data to a JSON file.
file_path = f"raw_eis_measurement_{self.count}.json"
print(f"Saving Raw data to {file_path}...")
data = {
"tone_frequency": measurement.tone_frequency,
"voltage_values": measurement.voltage_values,
"current_values": measurement.current_values,
}
with open(file_path, "w") as f:
json.dump(data, f, indent=2)
if self.max_count:
print(f"Step {self.count}/{self.max_count} done!")
else:
print(f"Step {self.count} done!")
def main():
"""A Simple EIS Measurement with raw data saving.
Raises:
RuntimeError: If the device's last self calibration is not valid.
"""
# Initializes the device with context manager.
with Device.create(DeviceFamily.SMU, resource_name="MySMU") as device:
# Validate the device latest self-calibration. This is optional.
if not Calibrator(device, Settings(2, 1)).last_calibration_is_valid:
raise RuntimeError("Device's last self calibration is not valid. Please run a new one!")
# Configure test parameters.
test_parameters = EISTestParameters(
voltage_limit_hi=4.2,
nominal_voltage=3.6,
powerline_frequency=PowerlineFrequency.FREQ_50_HZ,
compensation_method=CompensationMethod.SHORT,
frequency_sweep_characteristics={
50.0: FrequencySet(current_amplitude=0.6, number_of_periods=5),
1000.0: FrequencySet(current_amplitude=0.6, number_of_periods=20),
},
)
# Set up a Measurement class. This will handle running the measurement.
eis_measurement = EIS(device, test_parameters)
# Load Compensation File
eis_compensation = eis_measurement.load_compensation_file()
# Run Measurement with callback for file saving and progress monitoring
print("Starting Measurement...")
results = eis_measurement.run(
compensation=eis_compensation,
measurement_callback=JSONSerializer(
list_size=len(test_parameters.frequency_sweep_characteristics)
),
)
# Format and print results
print("Resulted impedance values:")
for i, frequency in enumerate(eis_measurement.frequency_list):
print(f"Frequency: {frequency} Hz - Impedance: {results[i].impedance} Ohm")
# *** ------------------------------ ***
# * Plotting using EIS.get_plots() *
# *** ------------------------------ ***
try:
import matplotlib.pyplot as plt
except ImportError: # pragma: no cover - optional dependency for examples
print("matplotlib is not installed. Install it to see plots: pip install matplotlib")
return
# Use the new PlotSeries-based API on EIS
nyquist, magnitude, phase = eis_measurement.get_plots()
# Layout: Nyquist spans the top row (full width), magnitude and phase
# occupy the bottom-left and bottom-right respectively.
fig = plt.figure(figsize=(12, 8))
gs = fig.add_gridspec(nrows=2, ncols=2, height_ratios=[1, 1], hspace=0.4, wspace=0.3)
ax_nyq = fig.add_subplot(gs[0, :])
ax_mag = fig.add_subplot(gs[1, 0])
ax_phase = fig.add_subplot(gs[1, 1])
# Nyquist (Cole) plot: Real (R) vs -Imag ( -X ) — wide plot on top
ax_nyq.plot(nyquist.x, nyquist.y, marker="o", linestyle="-")
ax_nyq.set_xlabel("R (Ohm)")
ax_nyq.set_ylabel("-X (Ohm)")
ax_nyq.set_title("Nyquist (Cole) Plot")
ax_nyq.grid(True)
# Bode magnitude: |Z| vs frequency (linear) — bottom-left
ax_mag.plot(magnitude.x, magnitude.y, marker="o", linestyle="-")
ax_mag.set_xlabel("Frequency (Hz)")
ax_mag.set_ylabel("|Z| (Ohm)")
ax_mag.set_title("Bode Magnitude")
ax_mag.grid(True, which="both")
# Bode phase: theta vs frequency (linear) — bottom-right
ax_phase.plot(phase.x, phase.y, marker="o", linestyle="-")
ax_phase.set_xlabel("Frequency (Hz)")
ax_phase.set_ylabel("Phase (deg)")
ax_phase.set_title("Bode Phase")
ax_phase.grid(True, which="both")
plt.show()
if __name__ == "__main__":
main()
EIS with Temperature Measurement
Example showing basic EIS functionality with additional temperature measurement.
First, it initializes the device with context manager. To set this up, you can use VISA names and nibcq.enums.DeviceFamily for device type, which should be SMU.
The temperature measurement has to be initialized with the device, but from then the device handles everything. It allows the calibration file to be validated against the current environment’s temperature measurements.
- This example runs an EIS measurement and then creates three plots using matplotlib:
Nyquist (Cole) plot: R vs -X
Bode magnitude: abs(Z) vs frequency
Bode phase: theta vs frequency
Plotting is done using matplotlib, which is an optional dependency. The required lists
are requested from the EIS measurement object using the new EIS.get_plots() method.
The values are extracted from the returned list of SMUResult objects so
no changes to the core EIS API are required to recreate the functionality.
The extracted results are then plotted in three subplots: Nyquist (Cole) plot, Bode magnitude, and Bode phase. These get the same functionality as the LabVIEW examples.
from nibcq import Calibrator, Device, EIS, EISTestParameters
from nibcq.calibration import Settings
from nibcq.enums import DeviceFamily
from nibcq.temperature import ThermocoupleSettings
def main():
"""A Simple EIS Measurement with thermocouple support.
Raises:
RuntimeError: If the device's last self calibration is not valid.
ValueError: If temperature validation fails.
"""
# Set up the temperature for the measurement in the default way.
thermocouple_settings = ThermocoupleSettings("MyThermocouple/ai0")
# Initializes the device with context manager.
with Device.create(
DeviceFamily.SMU,
resource_name="MySMU",
).with_temperature(thermocouple_settings) as device:
# Validate the device latest self-calibration. This is optional.
if not Calibrator(device, Settings(2, 1)).last_calibration_is_valid:
raise RuntimeError("Device's last self calibration is not valid. Please run one!")
# Read in test parameters.
test_parameters = EISTestParameters.from_file("examples/resources/EISConfigFile.json")
# Set up a Measurement class. This will handle running the measurement.
eis_measurement = EIS(device, test_parameters)
# Load Compensation File
eis_compensation = eis_measurement.load_compensation_file()
# Optional - Overwrite acceptable temperature delta for compensation validation
eis_measurement.acceptable_temperature_delta = 5.0 # degrees Celsius
# Run Measurement with callback for progress monitoring
print("Starting Measurement...")
results = eis_measurement.run(
compensation=eis_compensation,
measurement_callback=lambda smu_measurement: print(
f"Measured frequency: {smu_measurement.tone_frequency:.1f} Hz - "
f"{len(smu_measurement.voltage_values)} samples acquired"
),
)
# Format and print results
print("Resulted impedance values:")
for i, frequency in enumerate(eis_measurement.frequency_list):
print(f"Frequency: {frequency} Hz - Impedance: {results[i].impedance} Ohm")
# Optional - Measure temperature
eis_measurement.measure_temperature()
print(f"Current Temperature: {eis_measurement.temperature} °C")
# *** ------------------------------ ***
# * Plotting using EIS.get_plots() *
# *** ------------------------------ ***
try:
import matplotlib.pyplot as plt
except ImportError: # pragma: no cover - optional dependency for examples
print("matplotlib is not installed. Install it to see plots: pip install matplotlib")
return
# Use the new PlotSeries-based API on EIS
nyquist, magnitude, phase = eis_measurement.get_plots()
# Layout: Nyquist spans the top row (full width), magnitude and phase
# occupy the bottom-left and bottom-right respectively.
fig = plt.figure(figsize=(12, 8))
gs = fig.add_gridspec(nrows=2, ncols=2, height_ratios=[1, 1], hspace=0.4, wspace=0.3)
ax_nyq = fig.add_subplot(gs[0, :])
ax_mag = fig.add_subplot(gs[1, 0])
ax_phase = fig.add_subplot(gs[1, 1])
# Nyquist (Cole) plot: Real (R) vs -Imag ( -X ) — wide plot on top
ax_nyq.plot(nyquist.x, nyquist.y, marker="o", linestyle="-")
ax_nyq.set_xlabel("R (Ohm)")
ax_nyq.set_ylabel("-X (Ohm)")
ax_nyq.set_title("Nyquist (Cole) Plot")
ax_nyq.grid(True)
# Bode magnitude: |Z| vs frequency (linear) — bottom-left
ax_mag.plot(magnitude.x, magnitude.y, marker="o", linestyle="-")
ax_mag.set_xlabel("Frequency (Hz)")
ax_mag.set_ylabel("|Z| (Ohm)")
ax_mag.set_title("Bode Magnitude")
ax_mag.grid(True, which="both")
# Bode phase: theta vs frequency (linear) — bottom-right
ax_phase.plot(phase.x, phase.y, marker="o", linestyle="-")
ax_phase.set_xlabel("Frequency (Hz)")
ax_phase.set_ylabel("Phase (deg)")
ax_phase.set_title("Bode Phase")
ax_phase.grid(True, which="both")
plt.show()
if __name__ == "__main__":
main()
EIS with Switching
This example demonstrates how to set up and run an EIS measurement with switching.
First, it initializes the device with context manager. To set this up, you can use VISA names and nibcq.enums.DeviceFamily for device type, which should be SMU.
The Switch has to be initialized with the device, but from then it handles everything.
- After measurements, the results are plotted using matplotlib in three subplots:
Nyquist (Cole) plot: R vs -X for all cells
Bode magnitude: abs(Z) vs frequency for all cells
Bode phase: theta vs frequency for all cells
Each cell is plotted with a different color and labeled for easy identification.
Plotting is done using matplotlib, which is an optional dependency. The required lists
are requested from the EIS measurement object using the new EIS.get_plots() method.
The values are extracted from the returned list of SMUResult objects so
no changes to the core EIS API are required to recreate the functionality.
The extracted results are then plotted in three subplots: Nyquist (Cole) plot, Bode magnitude, and Bode phase. These get the same functionality as the LabVIEW examples.
from nibcq import Calibrator, Device, EIS, EISTestParameters
from nibcq.calibration import Settings
from nibcq.enums import DeviceFamily
from nibcq.switch import SwitchConfiguration
def main():
"""A Simple EIS Measurement with switching support.
Raises:
RuntimeError: If the device's last self calibration is not valid.
"""
# Set up switching
switching_config = SwitchConfiguration.from_file("examples/resources/SMUSwitchConfig.json")
# Initializes the device with context manager.
with Device.create(DeviceFamily.SMU, resource_name="MySMU").with_switching(
config=switching_config,
sense_switch_resource_name="MySensingSwitch",
source_switch_resource_name="MySourcingSwitch",
) as device:
# Validate the device latest self-calibration. This is optional.
if not Calibrator(device, Settings(2, 1)).last_calibration_is_valid:
raise RuntimeError("Device's last self calibration is not valid. Please run one!")
# Read in test parameters.
test_parameters = EISTestParameters.from_file("examples/resources/EISConfigFile.json")
# Set up a Measurement class. This will handle running the measurement.
eis_measurement = EIS(device, test_parameters)
# Load Compensation File
eis_compensation = eis_measurement.load_compensation_file()
# Run Measurement with callback for progress monitoring
print("Starting Measurement...")
results_per_cell = eis_measurement.run_with_switching(
compensation=eis_compensation,
measurement_callback=lambda smu_measurement: print(
f"Measured frequency: {smu_measurement.tone_frequency:.1f} Hz - "
f"{len(smu_measurement.voltage_values)} samples acquired"
),
)
# Format and print results
for i, (cell_data, results) in enumerate(results_per_cell):
print(f" ===== \nCell {i} - {cell_data}:")
print("Resulted impedance values:")
for j, frequency in enumerate(eis_measurement.frequency_list):
print(f"Frequency: {frequency} Hz - Impedance: {results[j].impedance} Ohm")
# *** ---------------------------------------- ***
# * Plotting multiple cells using matplotlib *
# *** ---------------------------------------- ***
try:
import matplotlib.pyplot as plt
import matplotlib.cm as cm
import numpy as np
except ImportError: # pragma: no cover - optional dependency for examples
print("matplotlib is not installed. Install it to see plots: pip install matplotlib")
return
# Get plot data for all cells using the new switching-aware get_plots() method
plot_data_per_cell = eis_measurement.get_plots()
# Layout: Nyquist spans the top row (full width), magnitude and phase
# occupy the bottom-left and bottom-right respectively.
fig = plt.figure(figsize=(14, 10))
gs = fig.add_gridspec(nrows=2, ncols=2, height_ratios=[1, 1], hspace=0.4, wspace=0.3)
ax_nyq = fig.add_subplot(gs[0, :])
ax_mag = fig.add_subplot(gs[1, 0])
ax_phase = fig.add_subplot(gs[1, 1])
# Generate different colors for each cell
num_cells = len(results_per_cell)
colors = (
cm.tab10(np.linspace(0, 1, num_cells))
if num_cells <= 10
else cm.tab20(np.linspace(0, 1, num_cells))
)
# Plot each cell's data with different colors
for i, (nyquist, magnitude, phase) in enumerate(plot_data_per_cell):
# Create label for this cell based on its jig ID
cell_data, _ = results_per_cell[i] # Get cell_data from results_per_cell
cell_label = f"Cell {i} ({cell_data.cell_serial_number})"
color = colors[i]
ax_nyq.plot(
nyquist.x,
nyquist.y,
marker="o",
linestyle="-",
color=color,
label=cell_label,
alpha=0.8,
)
# Bode magnitude: |Z| vs frequency
ax_mag.plot(
magnitude.x,
magnitude.y,
marker="o",
linestyle="-",
color=color,
label=cell_label,
alpha=0.8,
)
# Bode phase: theta vs frequency
ax_phase.plot(
phase.x,
phase.y,
marker="o",
linestyle="-",
color=color,
label=cell_label,
alpha=0.8,
)
# Configure Nyquist plot
ax_nyq.set_xlabel("R (Ohm)")
ax_nyq.set_ylabel("-X (Ohm)")
ax_nyq.set_title("Nyquist (Cole) Plot - All Cells")
ax_nyq.grid(True)
ax_nyq.legend(loc="best")
# Configure Bode magnitude plot
ax_mag.set_xlabel("Frequency (Hz)")
ax_mag.set_ylabel("|Z| (Ohm)")
ax_mag.set_title("Bode Magnitude - All Cells")
ax_mag.grid(True, which="both")
ax_mag.legend()
# Configure Bode phase plot
ax_phase.set_xlabel("Frequency (Hz)")
ax_phase.set_ylabel("Phase (deg)")
ax_phase.set_title("Bode Phase - All Cells")
ax_phase.grid(True, which="both")
ax_phase.legend()
plt.show()
if __name__ == "__main__":
main()
EIS with Switching and Temperature Measurement
Example combining EIS measurements with both temperature monitoring and switching capabilities.
This example demonstrates the most comprehensive EIS measurement scenario, combining: - Multiple DUTs via switch matrix - Temperature measurement and validation - Compensation file loading and validation - Progress monitoring via callbacks - Result plotting for all cells
The example shows proper error handling for temperature validation failures, which can occur when the measured temperature differs significantly from the compensation file’s target temperature.
from nibcq import Calibrator, Device, EIS, EISTestParameters
from nibcq.calibration import Settings
from nibcq.enums import DeviceFamily
from nibcq.switch import SwitchConfiguration
from nibcq.temperature import ThermocoupleSettings
def main():
"""EIS Measurement with both switching and temperature support.
Raises:
RuntimeError: If the device's last self calibration is not valid.
"""
# Set up the temperature measurement
thermocouple_settings = ThermocoupleSettings("MyThermocouple/ai0")
# Set up switching configuration
switching_config = SwitchConfiguration.from_file("examples/resources/SMUSwitchConfig.json")
# Initialize device with both temperature and switching capabilities
with Device.create(
DeviceFamily.SMU,
resource_name="MySMU",
).with_temperature(thermocouple_settings).with_switching(
config=switching_config,
sense_switch_resource_name="MySensingSwitch",
source_switch_resource_name="MySourcingSwitch",
) as device:
# Validate the device latest self-calibration. This is optional.
if not Calibrator(device, Settings(2, 1)).last_calibration_is_valid:
raise RuntimeError("Device's last self calibration is not valid. Please run one!")
# Read in test parameters
test_parameters = EISTestParameters.from_file("examples/resources/EISConfigFile.json")
# Set up a Measurement class
eis_measurement = EIS(device, test_parameters)
# Load Compensation File
eis_compensation = eis_measurement.load_compensation_file()
# Optional - Overwrite acceptable temperature delta for compensation validation
eis_measurement.acceptable_temperature_delta = 5.0 # degrees Celsius
# Run Measurement with callback for progress monitoring
print("Starting Measurement with Temperature Monitoring and Switching...")
try:
results_per_cell = eis_measurement.run_with_switching(
compensation=eis_compensation,
measurement_callback=lambda smu_measurement: print(
f"Measured frequency: {smu_measurement.tone_frequency:.1f} Hz - "
f"{len(smu_measurement.voltage_values)} samples acquired"
),
)
except ValueError as e:
# Temperature validation failures raise ValueError
if "temperature" in str(e).lower():
print(f"Temperature validation failed: {e}")
print("Measurement aborted. Please check environmental conditions.")
print("Continuing to retrieve any results obtained before the failure...")
results_per_cell = eis_measurement.result
else:
raise # Re-raise if it's a different ValueError
# Format and print results
for i, (cell_data, results) in enumerate(results_per_cell):
print(f"\n===== Cell {i} - {cell_data} =====")
print("Resulted impedance values:")
for j, frequency in enumerate(eis_measurement.frequency_list):
print(f"Frequency: {frequency} Hz - Impedance: {results[j].impedance} Ohm")
# Optional - Measure final temperature
eis_measurement.measure_temperature()
print(f"\nFinal Temperature: {eis_measurement.temperature} °C")
# *** ---------------------------------------- ***
# * Plotting multiple cells using matplotlib *
# *** ---------------------------------------- ***
try:
import matplotlib.pyplot as plt
import matplotlib.cm as cm
import numpy as np
except ImportError: # pragma: no cover - optional dependency for examples
print("matplotlib is not installed. Install it to see plots: pip install matplotlib")
return
# Get plot data for all cells using the switching-aware get_plots() method
plot_data_per_cell = eis_measurement.get_plots()
# Layout: Nyquist spans the top row (full width), magnitude and phase
# occupy the bottom-left and bottom-right respectively.
fig = plt.figure(figsize=(14, 10))
gs = fig.add_gridspec(nrows=2, ncols=2, height_ratios=[1, 1], hspace=0.4, wspace=0.3)
ax_nyq = fig.add_subplot(gs[0, :])
ax_mag = fig.add_subplot(gs[1, 0])
ax_phase = fig.add_subplot(gs[1, 1])
# Generate different colors for each cell
num_cells = len(results_per_cell)
colors = (
cm.tab10(np.linspace(0, 1, num_cells))
if num_cells <= 10
else cm.tab20(np.linspace(0, 1, num_cells))
)
# Plot each cell's data with different colors
for i, (nyquist, magnitude, phase) in enumerate(plot_data_per_cell):
# Create label for this cell based on its jig ID
cell_data, _ = results_per_cell[i]
cell_label = f"Cell {i} ({cell_data.cell_serial_number})"
color = colors[i]
ax_nyq.plot(
nyquist.x,
nyquist.y,
marker="o",
linestyle="-",
color=color,
label=cell_label,
alpha=0.8,
)
# Bode magnitude: |Z| vs frequency
ax_mag.plot(
magnitude.x,
magnitude.y,
marker="o",
linestyle="-",
color=color,
label=cell_label,
alpha=0.8,
)
# Bode phase: theta vs frequency
ax_phase.plot(
phase.x,
phase.y,
marker="o",
linestyle="-",
color=color,
label=cell_label,
alpha=0.8,
)
# Configure Nyquist plot
ax_nyq.set_xlabel("R (Ohm)")
ax_nyq.set_ylabel("-X (Ohm)")
ax_nyq.set_title("Nyquist (Cole) Plot - All Cells with Temperature Monitoring")
ax_nyq.grid(True)
ax_nyq.legend(loc="best")
# Configure Bode magnitude plot
ax_mag.set_xlabel("Frequency (Hz)")
ax_mag.set_ylabel("|Z| (Ohm)")
ax_mag.set_title("Bode Magnitude - All Cells")
ax_mag.grid(True, which="both")
ax_mag.legend()
# Configure Bode phase plot
ax_phase.set_xlabel("Frequency (Hz)")
ax_phase.set_ylabel("Phase (deg)")
ax_phase.set_title("Bode Phase - All Cells")
ax_phase.grid(True, which="both")
ax_phase.legend()
plt.show()
if __name__ == "__main__":
main()
OCV examples
Simple OCV Measurement
Example showing basic OCV functionality.
First, it initializes the device with context manager. To set this up, you can use VISA names and nibcq.enums.DeviceFamily for device type, which should be DMM.
from nibcq import Calibrator, Device, OCV, OCVTestParameters
from nibcq.calibration import Settings
from nibcq.enums import DeviceFamily
def main():
"""A Simple OCV Measurement.
Raises:
RuntimeError: If the device's last self calibration is not valid.
"""
# Initializes the device with context manager.
with Device.create(DeviceFamily.DMM, resource_name="MyDMM") as device:
# Validate the device latest self-calibration. This is optional
calibration_settings = Settings(
temperature_delta=2,
days_to_calibration=1,
)
calibrator = Calibrator(device, calibration_settings)
if not calibrator.last_calibration_is_valid:
raise RuntimeError("Device's last self calibration is not valid. Please run one!")
# Set test parameters
test_parameters = OCVTestParameters.from_file("examples/resources/OCVConfigFile.json")
# Set up a Measurement class. This will handle running the measurement.
ocv_measurement = OCV(device, test_parameters)
# Run Measurement
print("Starting Measurement...")
start, end, voltage = ocv_measurement.run()
# Print results
print(f"Measured time: {(end-start)} msec, Measured Voltage: {voltage} V")
print(f"End Timestamp: {end.astimezone()}")
if __name__ == "__main__":
main()
OCV with Switching
This example demonstrates how to set up and run an OCV measurement with switching.
First, it initializes the device with context manager. To set this up, you can use VISA names and nibcq.enums.DeviceFamily for device type, which should be DMM.
The Switch has to be initialized with the device, but from then it handles everything.
from nibcq import Calibrator, Device, OCV, OCVTestParameters
from nibcq.calibration import Settings
from nibcq.enums import DeviceFamily, SwitchTopology, SwitchDeviceType
from nibcq.switch import SwitchConfiguration
def main():
"""An OCV Measurement with switching support.
Raises:
RuntimeError: If the device's last self calibration is not valid.
"""
# Set up switching
switching_config = SwitchConfiguration(
topology=SwitchTopology.SWITCH_2_WIRE_QUAD_16X1_MUX,
cells=["ch0", "ch1"], # Example DUT channels
)
# Initializes the device with context manager.
with Device.create(
DeviceFamily.DMM,
resource_name="MyDMM",
).with_switching(
config=switching_config,
sense_switch_resource_name="MySwitch",
dmm_terminal_channel="com0",
dmm_switch_type=SwitchDeviceType.PXIe_2530B, # Also supports PXI-2525
) as device:
# Validate the device latest self-calibration. This is optional.
if not Calibrator(device, Settings(2, 1)).last_calibration_is_valid:
raise RuntimeError("Device's last self calibration is not valid. Please run one!")
# Set test parameters
test_parameters = OCVTestParameters.from_file("examples/resources/OCVConfigFile.json")
# Set up a Measurement class. This will handle running the measurement.
ocv_measurement = OCV(device, test_parameters)
# Run Measurement with switching
print("Starting Measurement...")
results = ocv_measurement.run_with_switching()
# Print results (a list of (channel, (start, end, voltage)) tuples)
for channel, (start, end, voltage) in results:
print(f"Measured Voltage for {channel}: {voltage} V, Test time was: {end - start} msec")
if __name__ == "__main__":
main()
OCV with Measurement Data Saving
Example showing basic OCV functionality with saving the raw measurement data.
First, it initializes the device with context manager. To set this up, you can use VISA names and nibcq.enums.DeviceFamily for device type, which should be DMM.
The raw measurements are saved to a JSON file.
import json
from nibcq import Calibrator, Device, OCV, OCVTestParameters
from nibcq.calibration import Settings
from nibcq.enums import DeviceFamily, PowerlineFrequency
def main():
"""A Simple example code for OCV Measurement.
Raises:
RuntimeError: If the device's last self calibration is not valid.
"""
# Initializes the device with context manager.
with Device.create(DeviceFamily.DMM, resource_name="MyDMM") as device:
# Validate the device latest self-calibration. This is optional
calibration_settings = Settings(
temperature_delta=2,
days_to_calibration=1,
)
calibrator = Calibrator(device, calibration_settings)
if not calibrator.last_calibration_is_valid:
raise RuntimeError("Device's last self calibration is not valid. Please run one!")
# Set test parameters
test_parameters = OCVTestParameters(powerline_frequency=PowerlineFrequency.FREQ_50_HZ)
# Set up a Measurement class. This will handle running the measurement.
ocv_measurement = OCV(device, test_parameters)
# Run Measurement
print("Starting Measurement...")
start, end, voltage = ocv_measurement.run()
# Save SMUMeasurement data to a JSON file.
file_path = "raw_ocv_measurement.json"
print(f"Saving Raw data to {file_path}...")
data = {
"start_datetime": start.isoformat(),
"end_datetime": end.isoformat(),
"measured_voltage": voltage,
}
with open(file_path, "w") as f:
json.dump(data, f, indent=2)
# Print results
print(f"Measured time: {(end-start)} msec, Measured Voltage: {voltage} V")
print(f"End Timestamp: {end.astimezone()}")
if __name__ == "__main__":
main()
Other Utilities and Simulated Devices
Run Self Calibration
Example showing how to run self-calibration on a device and check its validity.
from nibcq import Calibrator, Device
from nibcq.calibration import Settings
from nibcq.enums import DeviceFamily
def main() -> None:
"""Runs self calibration on a device and prints results."""
with Device.create(DeviceFamily.SMU, resource_name="MySMU") as device:
calibration_settings = Settings(temperature_delta=2, days_to_calibration=1)
calibrator = Calibrator(device, calibration_settings)
if not calibrator.last_calibration_is_valid:
print("Device's last self calibration is not valid. Running self calibration...")
self_cal_was_run = calibrator.self_calibrate()
if self_cal_was_run:
print("Self calibration was successfully run!")
else:
print(
"Unsuccessful Self calibration!"
"Self Calibration is either unsupported, or an error has occurred."
)
else:
print("Device's last self calibration is valid. No need to run self calibration.")
if __name__ == "__main__":
main()
Simulated DMM Device
A simple example about how to create a simulated DMM device.
You have the possibility to pass options to your device. This can be used to simulate devices for example.
from nibcq import Device
from nibcq.enums import DeviceFamily
def main():
"""Creates an example simulated PXI-4071 DMM device."""
# You can pass not just the resource name, but the session options too.
simulated_instrument_name = "Test4071"
simulated_instrument_options = {
"simulate": True,
"driver_setup": {
"Model": "4071",
"BoardType": "PXI",
},
}
# You can not just create a device, you can initialize one and then connect to it.
with Device(device_family=DeviceFamily.DMM).connect(
resource_name=simulated_instrument_name,
options=simulated_instrument_options,
) as device:
# Print some information about the simulated device
print(
f"Simulated device\n"
f" - Type: {device.product}\n"
f" - Serial Number: {device.serial_number}\n"
f" - Full Serial Number: {device.full_serial_number}\n"
f" - Is it a supported DMM?: "
f"{"Yes" if Device.is_supported(DeviceFamily.DMM, device.product) else "No"}\n"
f" - Is it a supported SMU?: "
f"{"Yes" if Device.is_supported(DeviceFamily.SMU, device.product) else "No"}"
)
if __name__ == "__main__":
main()
Simulated SMU Device
A simple example about how to create a simulated SMU device.
You have the possibility to pass options to your device. This can be used to simulate devices for example.
from nibcq import Device
from nibcq.enums import DeviceFamily
def main():
"""Creates an example simulated PXIe-4139 SMU device."""
# You can pass not just the resource name, but the session options too.
simulated_instrument_name = "Test4139"
simulated_instrument_options = {
"simulate": True,
"driver_setup": {
"Model": "4139",
"BoardType": "PXIe",
},
}
# You can not just create a device, you can initialize one and then connect to it.
with Device(device_family=DeviceFamily.SMU).connect(
resource_name=simulated_instrument_name,
options=simulated_instrument_options,
) as device:
# Print some information about the simulated device
print(
f"Simulated device\n"
f" - Type: {device.product}\n"
f" - Serial Number: {device.serial_number}\n"
f" - Full Serial Number: {device.full_serial_number}\n"
f" - Is it a supported DMM?: "
f"{"Yes" if Device.is_supported(DeviceFamily.DMM, device.product) else "No"}\n"
f" - Is it a supported SMU?: "
f"{"Yes" if Device.is_supported(DeviceFamily.SMU, device.product) else "No"}"
)
if __name__ == "__main__":
main()