nibcq.ParallelDCIR

class nibcq.ParallelDCIR(leader: nibcq._device.Device, followers: collections.abc.Sequence[nibcq._device.Device], test_parameters: nibcq._dcir.DCIRTestParameters, multi_device_type: nibcq.enums.MultiDeviceMode = MultiDeviceMode.PARALLEL)

Bases: nibcq.measurement.ParallelMeasurement, nibcq._dcir.DCIR

Defines a DCIR measurement with multiple ELoads working in parallel.

This class enables DC internal resistance measurements using multiple Electronic Load devices connected in parallel to achieve higher current draw than a single ELoad can provide.

DCIR measurements in general does NOT have switching support, but implementing one to the parallel implementation is highly discouraged due to hardware current limitations of switch matrices (typically limited to ~2A). Parallel DCIR is designed for direct-connection, high-current testing scenarios.

The measurement applies a two-phase discharge sequence (20% then 100% of the configured max load current) distributed evenly across all devices. Internal resistance is calculated from the voltage and current differences using Ohm’s law: R = (V1 - V2) / (I1 - I2), where subscripts 1 and 2 refer to the light-load (20%) and heavy-load (100%) phases respectively.

Key Features:
  • Multiple ELoads synchronized via hardware triggers (PXI backplane).

  • Current capacity scales linearly with number of devices (e.g., 3 ELoads = 3x current).

  • Voltage measured from leader device (remote sense for accuracy).

  • Current contributions summed across all devices.

Hardware Requirements:
  • All devices must be NI PXIe-4051 or compatible Electronic Loads.

  • Devices must be in the same PXI chassis for trigger routing.

  • One device designated as leader, others as followers.

Example

>>> leader_device = Device.create(DeviceFamily.ELOAD, "PXI1Slot2")
>>> follower1 = Device.create(DeviceFamily.ELOAD, "PXI1Slot3")
>>>
>>> params = DCIRTestParameters(
...     max_load_current=2.0,
...     powerline_frequency=PowerlineFrequency.FREQ_50_HZ,
... )
>>>
>>> parallel_dcir = ParallelDCIR(
...     leader=leader_device,
...     followers=[follower1],
...     test_parameters=params,
... )
>>>
>>> result = parallel_dcir.run()
>>> print(f"DC Resistance: {result} Ohms")
Parameters:
property measurement_data: None | nibcq.measurement.SMUMeasurement

Get the processed measurement data from the last DCIR test.

Returns voltage and current measurement data collected during the two-phase discharge sequence, combined across all devices. This data includes all samples from both discharge periods and can be used for detailed analysis or custom calculations.

Returns:

Processed and multidevice-combined measurement data containing

voltage_values and current_values tuples, combined across all devices. The tone_frequency is set to 0 for DC measurements. Returns None if the multidevice aggregation of measurements has not been performed yet.

Return type:

SMUMeasurement

get_all_devices()

Yield every parallel device, followers first, leader last.

This generator avoids storing a redundant copy of the device references that already live in ParallelMeasurement’s fields containing the followers and the leader.

Yields:

Device – The next device in followers-then-leader order.

property all_measurement_data: collections.abc.Iterator[tuple[nibcq._device.Device, SMUMeasurement | None]]

Get raw per-device measurements collected during the last parallel run.

Returns:

Iterator of

(device, measurement) pairs in followers-then-leader order. Each measurement entry is None until data is fetched.

Return type:

Iterator[tuple[Device, SMUMeasurement | None]]

Example

>>> for device, measurement in parallel_measurement.all_measurement_data:
...     if measurement is None:
...         print(f"{device.product}: no data yet")
...     else:
...         print(f"{device.product}: {len(measurement.voltage_values)} samples")
property test_parameters: TestParameters

Get the current test parameters for the measurement.

Returns the configuration parameters that define how the measurement should be performed, including settings like powerline frequency and other measurement-specific parameters.

Returns:

The current test parameters configuration

Return type:

TestParameters

Examples

>>> measurement = Measurement(device)
>>> params = measurement.test_parameters
>>> print(params.powerline_frequency)
PowerlineFrequency.FREQ_60_HZ
property result: float | SMUResult | LCRMeasurementResult | tuple[SMUResult, Ellipsis] | tuple[LCRMeasurementResult, Ellipsis] | tuple[tuple[nibcq.switch.SMUCellData, SMUResult], Ellipsis] | tuple[tuple[nibcq.switch.SMUCellData, tuple[SMUResult, Ellipsis]], Ellipsis] | tuple[tuple[str, tuple[datetime.datetime, datetime.datetime, float]], Ellipsis] | Any

Get the result of the last measurement.

Returns the measurement result from the most recent measurement operation. The result type depends on the specific measurement implementation and whether switching was used:

  • Single measurements:
    • float for OCV and DCIR

    • SMUResult for ACIR

    • tuple[SMUResult, …] for EIS

  • Switching measurements:
    • tuple[tuple[SMUCellData, SMUResult], …] for ACIR with switching

    • tuple[tuple[SMUCellData, tuple[SMUResult, …]], …] for EIS with switching

    • tuple[tuple[str, tuple[datetime, datetime, float]], …] for OCV with switching

Returns:

float | SMUResult | tuple[SMUResult, …] | tuple[tuple[SMUCellData, SMUResult], …] | tuple[tuple[SMUCellData, tuple[SMUResult, …]], …] | tuple[tuple[str, tuple[datetime, datetime, float]], …] | Any: The measurement result(s). For switching measurements, returns a tuple of tuples where each inner tuple contains the cell data and its corresponding results.

Raises:

RuntimeError – If no measurement has been performed yet or if the result is not available

Return type:

Union[float, SMUResult, LCRMeasurementResult, tuple[SMUResult, Ellipsis], tuple[LCRMeasurementResult, Ellipsis], tuple[tuple[nibcq.switch.SMUCellData, SMUResult], Ellipsis], tuple[tuple[nibcq.switch.SMUCellData, tuple[SMUResult, Ellipsis]], Ellipsis], tuple[tuple[str, tuple[datetime.datetime, datetime.datetime, float]], Ellipsis], Any]

Examples

>>> measurement.run(test_parameters)
>>> result = measurement.result
>>> print(f"Measurement result: {result}")
>>>
>>> # For switching measurements
>>> switching_results = measurement.run_with_switching(compensation)
>>> for cell_data, cell_results in measurement.result:
...     print(f"Cell {cell_data.cell_serial_number}: {len(cell_results)} results")
DEVICE_FAMILY: nibcq.enums.DeviceFamily = None

Device family for DCIR measurements.

Type:

DeviceFamily

property compensation: Compensation | None

Get the compensation object for error correction.

Returns:

The compensation data, or None if no compensation is applied.

Return type:

Compensation | None

run(compensation: Compensation | None = None, **kwargs)

Run the measurement process (Template Method).

Defines the invariant measurement workflow:

  1. Optionally set compensation from argument.

  2. Store any extra keyword arguments in _run_kwargs so hook methods can access them.

  3. Validate preconditions (temperature, etc.) — before locking.

  4. Lock all device sessions.

  5. Execute the workflow (configure → measure → calculate → compensate).

  6. Release locks.

  7. Clear _run_kwargs.

  8. Return result.

Subclasses customize behavior by overriding the abstract/hook methods: _configure, _measure, _calculate, _apply_compensation, _validate_preconditions, _lock_sessions, _run_workflow.

Parameters:
  • compensation (Compensation | None) – Optional compensation data for error correction. If provided, sets self._compensation before running. If None, uses the previously set self._compensation (which may also be None, meaning no compensation is applied).

  • kwargs (Any) – Additional keyword arguments (**kwargs) forwarded to hook methods via self._run_kwargs. Subclasses may read specific keys from this dict inside their _run_workflow or other hooks.

Returns:

The measurement result (type depends on the specific subclass).

Return type:

Any

property acceptable_temperature_delta: float

Get the acceptable temperature delta for compensation validation.

Returns the maximum allowed temperature difference from the device’s temperature capability. This is a pass-through property that delegates to the underlying TemperatureCapability.

Returns:

The acceptable temperature delta in degrees, or NaN if no temperature capability

Return type:

float

Examples

>>> measurement = EIS(device)
>>> measurement.acceptable_temperature_delta = 2.5
>>> delta = measurement.acceptable_temperature_delta
property temperature: float

Get the latest temperature reading from the device.

Returns:

The most recent temperature measurement, or NaN if no temperature capability

Return type:

float

property temperature_range: CenteredRange

Get the latest temperature reading from the device, coupled with the user-set delta.

Returns:

A CenteredRange representing the most recent temperature measurement (NaN if not available), along with the acceptable temperature delta (NaN if not set).

Return type:

CenteredRange

measure_temperature() CenteredRange

Get a new temperature reading from the device.

Returns:

Current temperature reading, or NaN if no temperature capability

Return type:

CenteredRange

validate_temperature(target_temperature: CenteredRange) bool

Validate the current temperature against the compensation file’s target.

Delegates to the device’s temperature capability for validation. The capability handles all validation logic including checking if thermocouple is configured, using overridden delta values if set, and printing appropriate warnings.

Parameters:

target_temperature (CenteredRange) – The target temperature parameters for validation

Returns:

True if thermocouple is configured and temperature is within range.

False if thermocouple is not configured (capability missing or not set up), or if target temperature/delta is NaN.

Return type:

bool

Raises:

TemperatureError – If the current temperature exceeds the target ± delta range (only raised when capability is configured)

Examples

>>> measurement = EIS(device)
>>> measurement.measure_temperature()
>>> target = compensation.temperature_parameter
>>> is_valid = measurement.validate_temperature(target)