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()