Instrument Integration #

Litmus does NOT provide instrument drivers. You bring your own:

Litmus provides utilities for discovery, identification, mocking, and traceability.

Quick Start with PyVISA #

Install PyVISA with the pure-Python backend (no NI-VISA or Keysight IO Libraries required):

pip install pyvisa pyvisa-py

Direct PyVISA Usage #

import pyvisa
 
# Connect to instrument
rm = pyvisa.ResourceManager('@py')  # Use pyvisa-py backend
dmm = rm.open_resource("TCPIP::192.168.1.100::INSTR")
 
# Query identity
print(dmm.query("*IDN?"))
 
# Measure voltage
voltage = float(dmm.query("MEAS:VOLT:DC?"))
print(f"Voltage: {voltage} V")
 
dmm.close()

With Station Config #

In your station YAML, reference the driver class:

# stations/bench_1.yaml
id: bench_1
name: "Test Bench 1"
 
instruments:
  dmm:
    type: dmm
    driver: pyvisa.resources.MessageBasedResource
    resource: "TCPIP::192.168.1.100::INSTR"

The pytest plugin will instantiate the driver and make it available as a fixture:

def test_voltage(dmm, logger):
    # dmm is a pyvisa MessageBasedResource
    voltage = float(dmm.query("MEAS:VOLT:DC?"))
    logger.measure("voltage", voltage)

Using PyMeasure Drivers #

PyMeasure provides high-level drivers for 100+ instruments:

pip install pymeasure
# stations/bench_1.yaml
id: bench_1
name: "Test Bench 1"
 
instruments:
  dmm:
    type: dmm
    driver: pymeasure.instruments.keysight.Keysight34461A
    resource: "TCPIP::192.168.1.100::INSTR"
 
  psu:
    type: psu
    driver: pymeasure.instruments.keysight.KeysightE36312A
    resource: "TCPIP::192.168.1.101::INSTR"
def test_output_voltage(psu, dmm, logger):
    # PyMeasure provides high-level methods
    psu.voltage = 5.0
    psu.output_enabled = True
 
    logger.measure("output_voltage", dmm.voltage_dc)
 
    psu.output_enabled = False

Mock Instruments #

For testing without hardware, Litmus provides a Mock factory that works with any class:

from pymeasure.instruments.keithley import Keithley2400
from litmus.instruments.mocks import Mock
 
# Create mock that passes isinstance checks
smu = Mock(Keithley2400, voltage=5.0, current=1.5e-6)
 
assert isinstance(smu, Keithley2400)
assert smu.voltage == 5.0

Mock Configuration #

Mock supports three value types:

from litmus.instruments.mocks import Mock
 
# Simple values - always returned
dmm = Mock(object, measure_voltage=3.31)
dmm.measure_voltage()  # Returns 3.31
 
# Dict lookup - first argument is key
inst = Mock(object, query={
    "MEAS:VOLT:DC?": "3.31",
    "MEAS:CURR:DC?": "0.1",
    "*IDN?": "Keysight,34461A,SN123,1.0",
})
inst.query("MEAS:VOLT:DC?")  # Returns "3.31"
 
# Callable - full control
inst = Mock(object, query=lambda cmd: "3.31" if "VOLT" in cmd else "0.0")

Station Mock Config #

Configure mocks in station YAML:

# stations/dev_station.yaml
id: dev_station
name: "Development Station"
 
instruments:
  dmm:
    type: dmm
    catalog_ref: generic_dmm
    mock: true  # Use mock mode
    mock_config:
      measure_dc_voltage: 3.31
      measure_dc_current: 0.1
 
  psu:
    type: psu
    catalog_ref: generic_psu
    mock: true
    mock_config:
      measure_voltage: 5.0
      measure_current: 0.25

Run with mocks:

pytest tests/ --station=dev_station --mock-instruments --dut-serial=TEST001

Discovery #

Scan for available VISA instruments:

from litmus.instruments.discovery import discover_visa, get_info_visa
 
# Find all instruments
resources = discover_visa()
# ["TCPIP::192.168.1.100::INSTR", "USB0::0x1234::0x5678::SN123::INSTR"]
 
# Get identity info
info = get_info_visa("TCPIP::192.168.1.100::INSTR")
# InstrumentInfo(manufacturer="Keysight", model="34461A", serial="SN123")

Or use the CLI:

litmus discover

Integration Patterns #

Station roles become fixtures automatically:

# Station config has dmm and psu → fixtures auto-registered
def test_output_voltage(context, psu, dmm, logger):
    psu.voltage = context.get_param("vin", 5.0)
    psu.output_enabled = True
    logger.measure("output_voltage", dmm.voltage_dc)

Custom Fixture Override #

Override auto-registered fixtures for custom setup/teardown:

# tests/conftest.py
import pytest
 
@pytest.fixture(scope="session")
def psu(instruments):
    """Custom PSU with safety defaults."""
    inst = instruments["psu"]
    inst.current_limit = 0.5  # Safety limit
    yield inst
    inst.output_enabled = False  # Always disable on teardown

Standalone Script #

#!/usr/bin/env python3
import pyvisa
from litmus.instruments.mocks import Mock
 
def measure_voltage(resource: str, mock: bool = False) -> float:
    if mock:
        dmm = Mock(object, query={"MEAS:VOLT:DC?": "3.31"})
    else:
        rm = pyvisa.ResourceManager('@py')
        dmm = rm.open_resource(resource)
 
    try:
        voltage = float(dmm.query("MEAS:VOLT:DC?"))
        return voltage
    finally:
        if not mock:
            dmm.close()
 
if __name__ == "__main__":
    import sys
    mock = "--mock" in sys.argv
    v = measure_voltage("TCPIP::192.168.1.100::INSTR", mock=mock)
    print(f"Voltage: {v} V")

Traceability #

Every measurement records which instrument took it:

Per-step instrument identity is stored as parallel arrays (one entry per instrument touched by the step) under the step_instruments_* columns:

# Each step row carries parallel arrays:
# - step_instruments_name      : ["dmm", "psu", ...]
# - step_instruments_serial    : ["SN123456", "SN789", ...]
# - step_instruments_model     : ["34461A", "E36312A", ...]
# - step_instruments_firmware  : ["1.0.2", "2.1.0", ...]
# - step_instruments_resource  : ["TCPIP::...", "GPIB::...", ...]
# (See `INSTRUMENT_ARRAY_KEYS` in litmus.data.backends._row_helpers.)

Configure calibration info on the instrument asset (instruments/<id>.yaml) and reference it from the station:

# instruments/keysight_dmm_001.yaml
id: keysight_dmm_001
catalog_ref: catalog/keysight/34461a.yaml
serial: MY12345678
calibration:
  due_date: 2024-06-15
  last_cal: 2023-06-15
  certificate: "CAL-2023-1234"
  lab: "Acme Calibration"
# stations/bench_1.yaml
instruments:
  dmm: keysight_dmm_001                            # role → instrument id
resources:
  keysight_dmm_001: "TCPIP::192.168.1.100::INSTR"  # id → VISA address

A station's instruments: block does not carry a calibration: field — that lives on the instrument asset YAML, which the loader joins to the station at session start.

Next Steps #