Bench bring-up with instro#

Check each instrument’s data as you connect a bench

When you connect a new bench one instrument at a time, check after each one that its channels reach Nominal under the names your test spec uses, at plausible values and at the rate the test spec needs. This notebook does that with three virtual instro instruments streaming to a Nominal run, so it needs no hardware.

Requirements: a default profile for the Nominal Python Client, and the packages the notebook imports, including instro. Running the notebooks explains how to set them up.

Each execution of the notebook reuses the bench’s asset. It archives what earlier executions created on the asset, so the asset holds one run: this execution’s.

Setup#

import time

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from instro.dmm import InstroDMM, MeasurementFunction
from instro.dmm.drivers import VirtualDMM
from instro.eload import InstroELoad, LoadMode
from instro.eload.drivers import VirtualELoad
from instro.lib import set_display_options as set_instro_display_options
from instro.lib.publishers import NominalCorePublisher
from instro.lib.types import BackgroundDaemonConfig
from instro.psu import InstroPSU
from instro.psu.drivers import VirtualPSU
from nominal.core import NominalClient, set_display_options
from nominal.thirdparty.pandas import datasource_to_dataframe

# Nominal's and instro's cards each have display options: show readable timestamps in both.
set_display_options(time_format="readable")
set_instro_display_options(time_format="readable")
plt.style.use("dark_background")

client = NominalClient.from_profile("default")

The bench powers a power-conversion board, the device under test (DUT):

Instrument

instro name

Wiring

Power supply

bench_psu

output 1: 12 V into the DUT. Output 2: the fixture’s 5 V rail.

DMM

dut_dmm

DC voltage across the 5 V rail

Electronic load

dut_load

sinks 1 A from the DUT’s 5 V output

Each instrument uses an instro virtual driver. The DMM’s leads were clipped to the wrong output of the power supply, so it reads 12 V.

psu_hardware = VirtualPSU(num_channels=2, load_resistance={1: 24.0, 2: 10.0})
dmm_hardware = VirtualDMM({MeasurementFunction.DC_VOLTAGE: 12.0})
eload_hardware = VirtualELoad(source_voltage=5.0, source_resistance=0.05)

The bench is a Nominal asset, and every instrument streams to the asset’s bench dataset. A run marks the bring-up session. Keep it open in Nominal during bring-up, with Open in Nominal on its card.

bench = client.get_or_create_asset_by_properties(
    {"bench_id": "bench-7", "example": "bench_bringup"}, name="Bench 7"
)
dataset = bench.get_or_create_dataset("bench", name="Bench 7 instruments")
# end=None leaves the run open until the session ends.
run = bench.create_run(
    "Bench 7 bring-up", start=pd.Timestamp.now(tz="UTC").floor("s"), end=None
)
run
Run(rid='ri.scout.cerulean-staging.run.d235de0c-1638-15db-43b5-787dbc053e13', name='Bench 7 bring-up', description='', properties=mappingproxy({}), labels=(), links=(), start=1793905812000000000, end=None, run_number=457, assets=('ri.scout.cerulean-staging.asset.cb545fdc-997d-d062-bbc0-c84979601495',), created_at=1793905812404506000, is_archived=False)

The test spec#

The test spec names each channel it reads, with the range it expects. It needs every channel at 10 Hz.

SPEC = {
    "bench_psu.ch1.voltage": (11.8, 12.2),
    "bench_psu.ch1.current": (0.45, 0.55),
    "bench_psu.ch2.voltage": (4.9, 5.1),
    "bench_psu.ch2.current": (0.45, 0.55),
    "dut_dmm.dc_voltage": (4.9, 5.1),
    "dut_load.ch1.voltage": (4.85, 5.0),
    "dut_load.ch1.current": (0.95, 1.05),
}
# Below 10 Hz, because the newest second of data may not have reached Nominal yet.
MIN_RATE_HZ = 5.0
WINDOW_S = 10


def check(window_s: float = WINDOW_S) -> pd.DataFrame:
    """Compare the last ``window_s`` seconds of the bench's data in Nominal with ``SPEC``.

    Args:
        window_s (float): Length of the window that ends now, in seconds.

    Returns:
        pandas.DataFrame: A row for each channel in ``SPEC``, and for each channel of an
        instrument ``SPEC`` doesn't name that sent data in the window. Each row has the
        channel's rate in Hz, its latest value and a status.
    """
    end = pd.Timestamp.now(tz="UTC")
    data = datasource_to_dataframe(
        dataset, start=end - pd.Timedelta(seconds=window_s), end=end
    ).dropna(axis="columns", how="all")
    # Keep the test spec's channels, and every channel of an instrument it doesn't name.
    instruments = {name.split(".")[0] for name in SPEC}
    data = data[[c for c in data if c in SPEC or c.split(".")[0] not in instruments]]
    report = pd.DataFrame(SPEC, index=["low", "high"]).T.join(
        pd.DataFrame(
            {"rate (Hz)": data.count() / window_s, "latest": data.ffill().iloc[-1]}
        ),
        how="outer",
    )
    report["status"] = np.select(
        [
            report["rate (Hz)"].isna(),
            report["low"].isna(),
            report["rate (Hz)"] < MIN_RATE_HZ,
            ~report["latest"].between(report["low"], report["high"]),
        ],
        ["no data", "not in spec", "too slow", "out of range"],
        "ok",
    )
    return report.round(3)

Bring up the bench#

Run check() after each change on the bench.

Step 1: the power supply#

psu = InstroPSU(
    name="bench_psu",
    driver=psu_hardware,
    num_channels=2,
    # Stream every value to the bench's dataset.
    publishers=[NominalCorePublisher(dataset=dataset)],
    # Read both outputs every 100 ms.
    background_config=BackgroundDaemonConfig(interval=0.1),
)
psu.open()
psu.apply(current_limit=1.0, voltage=12.0, enable=True, channel=1)
psu.apply(current_limit=1.0, voltage=5.0, enable=True, channel=2)
psu.start()
time.sleep(WINDOW_S)
check()
low high rate (Hz) latest status
bench_psu.ch1.current 0.45 0.55 9.7 0.500 ok
bench_psu.ch1.voltage 11.80 12.20 9.7 11.996 ok
bench_psu.ch2.current 0.45 0.55 9.7 0.499 ok
bench_psu.ch2.voltage 4.90 5.10 9.7 5.002 ok
dut_dmm.dc_voltage 4.90 5.10 NaN NaN no data
dut_load.ch1.current 0.95 1.05 NaN NaN no data
dut_load.ch1.voltage 4.85 5.00 NaN NaN no data

The power supply’s channels pass. The DMM and the electronic load aren’t connected yet, so their channels have no data.

Step 2: the DMM#

The DMM’s setup comes from another bench’s script, which named the DMM dmm.

dmm = InstroDMM(
    name="dmm",
    driver=dmm_hardware,
    publishers=[NominalCorePublisher(dataset=dataset)],
    background_config=BackgroundDaemonConfig(interval=0.1),
)
dmm.open()
dmm.set_measurement_function(MeasurementFunction.DC_VOLTAGE)
dmm.start()
time.sleep(WINDOW_S)
check()
low high rate (Hz) latest status
bench_psu.ch1.current 0.45 0.55 9.2 0.500 ok
bench_psu.ch1.voltage 11.80 12.20 9.2 11.994 ok
bench_psu.ch2.current 0.45 0.55 9.2 0.500 ok
bench_psu.ch2.voltage 4.90 5.10 9.2 5.005 ok
dmm.daemon_work_time NaN NaN 9.4 0.000 not in spec
dmm.dc_voltage NaN NaN 9.4 12.020 not in spec
dmm.loop_time NaN NaN 9.4 0.100 not in spec
dut_dmm.dc_voltage 4.90 5.10 NaN NaN no data
dut_load.ch1.current 0.95 1.05 NaN NaN no data
dut_load.ch1.voltage 4.85 5.00 NaN NaN no data

dmm.dc_voltage is not in spec, and dut_dmm.dc_voltage has no data. instro names every channel <instrument name>.<channel>, so rename the DMM. As on a real bench, that means editing the script and connecting the DMM again.

dmm.close()
dmm = InstroDMM(
    name="dut_dmm",
    driver=dmm_hardware,
    publishers=[NominalCorePublisher(dataset=dataset)],
    background_config=BackgroundDaemonConfig(interval=0.1),
)
dmm.open()
dmm.set_measurement_function(MeasurementFunction.DC_VOLTAGE)
dmm.start()
time.sleep(WINDOW_S)
check()
low high rate (Hz) latest status
bench_psu.ch1.current 0.45 0.55 10.0 0.498 ok
bench_psu.ch1.voltage 11.80 12.20 10.0 12.022 ok
bench_psu.ch2.current 0.45 0.55 10.0 0.500 ok
bench_psu.ch2.voltage 4.90 5.10 10.0 4.998 ok
dut_dmm.dc_voltage 4.90 5.10 9.2 11.974 out of range
dut_load.ch1.current 0.95 1.05 NaN NaN no data
dut_load.ch1.voltage 4.85 5.00 NaN NaN no data

The DMM’s data now arrives as dut_dmm.dc_voltage, but it reads 12 V where the test spec expects the 5 V rail. Move the leads to output 2. The DMM’s card shows the new reading within a second, without waiting for Nominal.

# In the simulation, moving the leads changes the voltage the virtual DMM reads.
dmm_hardware.stimulus[MeasurementFunction.DC_VOLTAGE] = 5.0
time.sleep(1)
dmm
InstroDMM(name='dut_dmm', driver=VirtualDMM, publishers=[NominalCorePublisher, DequeInMemoryPublisher], background_running=True)
# Wait for a full window of data from after the move.
time.sleep(WINDOW_S)
check()
low high rate (Hz) latest status
bench_psu.ch1.current 0.45 0.55 9.5 0.501 ok
bench_psu.ch1.voltage 11.80 12.20 9.0 11.995 ok
bench_psu.ch2.current 0.45 0.55 9.5 0.500 ok
bench_psu.ch2.voltage 4.90 5.10 9.0 5.014 ok
dut_dmm.dc_voltage 4.90 5.10 9.0 5.002 ok
dut_load.ch1.current 0.95 1.05 NaN NaN no data
dut_load.ch1.voltage 4.85 5.00 NaN NaN no data

Step 3: the electronic load#

The electronic load’s setup comes from a script that only polled it, so it has no publishers and its daemon runs at instro’s default interval.

eload = InstroELoad(name="dut_load", driver=eload_hardware)
eload.open()
# Sink a constant 1 A.
eload.set_mode(LoadMode.CC)
eload.set_level(1.0)
eload.output_enable(True)
eload.start()
time.sleep(WINDOW_S)
check()
low high rate (Hz) latest status
bench_psu.ch1.current 0.45 0.55 9.1 0.500 ok
bench_psu.ch1.voltage 11.80 12.20 9.1 11.967 ok
bench_psu.ch2.current 0.45 0.55 9.1 0.499 ok
bench_psu.ch2.voltage 4.90 5.10 9.1 5.003 ok
dut_dmm.dc_voltage 4.90 5.10 9.1 5.006 ok
dut_load.ch1.current 0.95 1.05 NaN NaN no data
dut_load.ch1.voltage 4.85 5.00 NaN NaN no data

The electronic load’s channels have no data. Its card shows that it works, but its only publisher is the in-memory buffer, so nothing goes to Nominal. Background also runs every second, where the test spec needs 10 Hz.

eload
InstroELoad(name='dut_load', driver=VirtualELoad, publishers=[DequeInMemoryPublisher], background_running=True)

Add a Nominal publisher, and set the daemon to 10 Hz:

eload.add_publisher(NominalCorePublisher(dataset=dataset))
eload.background_interval = 0.1
time.sleep(WINDOW_S)
check()
low high rate (Hz) latest status
bench_psu.ch1.current 0.45 0.55 9.6 0.501 ok
bench_psu.ch1.voltage 11.80 12.20 9.6 11.997 ok
bench_psu.ch2.current 0.45 0.55 9.6 0.501 ok
bench_psu.ch2.voltage 4.90 5.10 9.6 5.004 ok
dut_dmm.dc_voltage 4.90 5.10 9.6 4.994 ok
dut_load.ch1.current 0.95 1.05 8.8 1.002 ok
dut_load.ch1.voltage 4.85 5.00 9.3 4.978 ok

Every channel in the test spec passes the check.

End the session#

# Closing an instrument stops its daemon and flushes its stream to Nominal.
for instrument in [psu, dmm, eload]:
    instrument.close()
run.update(end=pd.Timestamp.now(tz="UTC").ceil("s"))
bench
Asset(rid='ri.scout.cerulean-staging.asset.cb545fdc-997d-d062-bbc0-c84979601495', name='Bench 7', description='Power-conversion test bench', properties=mappingproxy({'bench_id': 'bench-7', 'example': 'bench_bringup'}), labels=('bench',), created_at=1793820135019024000, is_archived=False)

The bench’s card shows its run and its dataset. Over the run, the DMM steps from 12 V to 5 V when its leads move:

session = datasource_to_dataframe(
    dataset,
    channels=[dataset.get_channel("dut_dmm.dc_voltage")],
    start=run.start,
    end=run.end,
)
session.index = pd.to_datetime(session.index)
session.plot(style=".", figsize=(12, 3), ylabel="V", legend=False);
../_images/89cc49daff138eaf4ee9fa2f9a33b2791b37d95174bb28749f061a8f1f2bba4c.png