---
title: "Keithley SMU Automation with Python: IV Curves"
description: "Run IV sweeps on a Keithley 2450 or 2400 SMU from Python: SCPI vs TSP, current limits, source delay and NPLC, 4-wire sensing, and a safe output-off."
url: https://galoislabs.ai/blog/keithley-smu-python
author: Alex Hernandez
author_url: https://galoislabs.ai/blog/authors/alex-hernandez
published: "2026-08-28"
topic: Instrument automation
publisher: Galois Labs
---

# Keithley SMU automation with Python: IV curves on the 2450 and 2400

![A bench source-measure instrument drawn in isometric, two test leads running to a single component in a small fixture.](https://galoislabs.ai/blog/figures/bench-2.light.webp)

*FIG. 1 — SOURCE AND MEASURE, ONE DEVICE*

To run an IV curve on a Keithley 2450 from Python, open it with PyVISA, set the current limit, NPLC and sense mode, load a sweep with `SOUR:SWE:VOLT:LIN`, start it with `INIT`, and read source and measured values with `TRAC:DATA?`. Turn the output off in a `finally` block. The older 2400 uses different commands, also covered here.

Instrument commands come from Keithley's [Model 2450 reference manual](https://download.tek.com/manual/2450-901-01_D_May_2015_Ref.pdf) (Rev. D) and [Series 2400 user's manual](https://download.tek.com/manual/2400S-900-01_K-Sep2011_User.pdf) (Rev. K); the Python uses the current [PyVISA API](https://pyvisa.readthedocs.io/en/latest/api/resources.html). [SCPI instrument automation with Python](https://galoislabs.ai/blog/scpi-automation-python) covers resource strings, error queues and timeouts. This guide covers what is specific to a source-measure unit.

## Which command set should a Keithley 2450 use: SCPI, TSP or SCPI 2400?

The 2450 has three remote command sets and runs one at a time; the manual says plainly, "You cannot combine the command sets." Switch with `*LANG SCPI`, `*LANG TSP` or `*LANG SCPI2400`, then reboot. `*LANG?` reports the active set, and the factory default is SCPI.

| Command set    | Select with              | Results come back from        | Choose it when                                  |
| -------------- | ------------------------ | ----------------------------- | ----------------------------------------------- |
| SCPI (default) | `*LANG SCPI`, reboot     | Queries ending in `?`         | Writing new Python for a 2450                   |
| TSP            | `*LANG TSP`, reboot      | `print()` and `printbuffer()` | Logic runs on the instrument or across TSP-Link |
| SCPI 2400      | `*LANG SCPI2400`, reboot | Series 2400 queries           | Old 2400 code must run unchanged                |

The 2400 emulation has a cost. In it, the 2450 [gives up its 10 nA, 100 nA and 20 mV ranges, the new trigger model and scripting](https://download.tek.com/manual/2450-901-01_D_May_2015_Ref.pdf) (Appendix D), and Keithley warns that some 2400 code behaves differently. Use it to keep an old program running while you port it.

TSP, Keithley's Test Script Processor, is a scripting language built on Lua with no query commands: you send statements such as `smu.source.ilimit.level = 0.01` and read output from `print()`. Common commands such as `*IDN?` and `*LANG?` still work in TSP mode, though not inside scripts.

SCPI is the better default for Python: each step is a short command or query, which is what PyVISA, drivers and instrument profiles expect.

## How do I connect to a Keithley 2450 or 2400 from Python?

The 2450 talks GPIB, USB and LAN (Telnet on port 23, VXI-11, raw sockets on port 5025), and the first interface to send a message takes control. The Series 2400 uses GPIB or RS-232.

```python title="bench/keithley.py"
import re
from contextlib import contextmanager

import pyvisa


def open_smu(rm: pyvisa.ResourceManager, resource: str):
    """Open a Keithley 2450 or Series 2400 SMU in SCPI mode; return (smu, model)."""
    smu = rm.open_resource(
        resource, read_termination="\n", write_termination="\n", timeout=10_000
    )
    idn = smu.query("*IDN?")
    # 2450: KEITHLEY INSTRUMENTS,MODEL 2450,<serial>,<firmware>
    # 2400: KEITHLEY INSTRUMENTS INC., MODEL 2400, <serial>, <firmware>
    match = re.search(r"MODEL (24\d\d)", idn)
    if not match:
        raise ValueError(f"{resource} is not a Keithley 24xx SMU: {idn}")
    model = match.group(1)
    if model == "2450" and (lang := smu.query("*LANG?")) != "SCPI":
        raise RuntimeError(f"2450 is in {lang} mode: send *LANG SCPI, then reboot")
    smu.write("*CLS")
    return smu, model


def drain_errors(smu, limit: int = 64) -> list[str]:
    """Read SYST:ERR? until it reports code 0 (the 2450 answers 0,"No error;0,0,0")."""
    errors = []
    for _ in range(limit):
        code, _, message = smu.query("SYST:ERR?").partition(",")
        if int(code) == 0:
            return errors
        errors.append(f"{code},{message}")
    raise RuntimeError(f"error log not empty after {limit} reads: {errors}")


@contextmanager
def io_timeout(smu, seconds: float):
    """Raise the VISA timeout for one slow step, then restore it."""
    previous, smu.timeout = smu.timeout, int(seconds * 1000)
    try:
        yield smu
    finally:
        smu.timeout = previous


@contextmanager
def output_guard(smu, off_state: str = "NORM"):
    """Leave the output off on every exit path: return, exception or Ctrl-C."""
    smu.write(f"OUTP:SMOD {off_state}")  # what "off" means: NORM, HIMP, ZERO or GUAR
    try:
        yield smu
    except BaseException:
        smu.clear()        # device clear: drop any reply still in flight
        smu.write("ABOR")  # stop a running trigger model or sweep
        raise
    finally:
        smu.write("OUTP OFF")


# smu, model = open_smu(pyvisa.ResourceManager(), "TCPIP0::192.168.1.60::inst0::INSTR")
```

- **Identity differs by generation.** The manuals document `KEITHLEY INSTRUMENTS,MODEL 2450,...` for the 2450 and `KEITHLEY INSTRUMENTS INC., MODEL nnnn, ...` for the 2400, so the code matches only the model field.
- **The command set is checked first.** A 2450 left in TSP mode rejects every SCPI command in this guide.
- **The 2450 separates errors from warnings.** `SYST:ERR?` returns only errors, answering `0,"No error;0,0,0"` when none remain; warnings come from `SYST:EVEN:NEXT?`.

The guard's `except` branch runs before its `finally`, so a sweep interrupted by a timeout or Ctrl-C is aborted before the output is switched off.

## How do I set compliance (the current limit) on a Keithley 2450?

Series 2400 manuals call it compliance; the 2450 calls it the source limit. [Appendix E of the 2450 manual](https://download.tek.com/manual/2450-901-01_D_May_2015_Ref.pdf), on converting 2400 code, maps the commands:

| Setting                              | Series 2400              | Model 2450               |
| ------------------------------------ | ------------------------ | ------------------------ |
| Current limit while sourcing voltage | `SENS:CURR:PROT <amps>`  | `SOUR:VOLT:ILIM <amps>`  |
| Voltage limit while sourcing current | `SENS:VOLT:PROT <volts>` | `SOUR:CURR:VLIM <volts>` |
| Was the current limit reached?       | `SENS:CURR:PROT:TRIP?`   | `SOUR:VOLT:ILIM:TRIP?`   |
| Voltage limit after `*RST`           | 21 V                     | 21 V                     |
| Current limit after `*RST`           | 105 µA                   | 105 µA                   |

The last row is the common surprise. After `*RST`, both instruments source voltage with a 105 µA current limit. A diode sweep meant to reach 10 mA flattens at 105 µA instead, because the SMU clamps the source to stay inside the limit, and nothing raises an error. Set the limit in every setup.

Three more rules from the 2450 manual:

- **The measure range constrains the limit.** With a fixed measure range, the limit must exceed 0.1% of that range; autorange lifts the constraint.
- **A source range change can move the limit.** If the new range cannot support it, the 2450 adjusts the limit and logs a warning, which `SYST:ERR?` does not return.
- **Check every point.** `SOUR:VOLT:ILIM:TRIP?` reports the present limit state, but the 2450 also stores a source status with each reading, and bit 5 (decimal 32) means the source was limited at that point. On the 2400, bit 3 (decimal 8) of the status word marks a reading taken in compliance.

## How do I run an IV sweep on a Keithley 2450 in Python?

`SOUR:SWE:VOLT:LIN` takes start, stop and points, then optional delay, count, range type, fail-abort, dual and buffer arguments. It replaces the trigger model with the sweep, `INIT` runs it, and the sweep clears its buffer when it starts. [Keithley's sweep section](https://download.tek.com/manual/2450-901-01_D_May_2015_Ref.pdf) (page 3-61) says to set the limit, readback, protection, sense mode and terminals first.

```python title="bench/iv_2450.py"
import numpy as np

from bench.keithley import drain_errors, io_timeout, output_guard

OVP_LEVELS = (2, 5, 10, 20, 40, 60, 80, 100, 120, 140, 160, 180)  # PROT<n> volts
LIMITED, FOUR_WIRE = 32, 64  # source status bits 5 and 6


def iv_sweep_2450(smu, start: float, stop: float, points: int, ilimit: float,
                  nplc: float = 1.0, delay: float = 0.005, four_wire: bool = True):
    """Linear voltage sweep measuring current. Returns volts, amps and limited flags."""
    v_max = max(abs(start), abs(stop))
    ovp = next((f"PROT{p}" for p in OVP_LEVELS if p >= 1.2 * v_max), "NONE")
    line_hz = float(smu.query("SYST:LFR?"))  # the 2450 detects line frequency itself

    for command in (
        "*RST",                                    # the current limit is back to 105 uA
        "SOUR:FUNC VOLT",
        f"SOUR:VOLT:ILIM {ilimit}",                # compliance, before the sweep command
        f"SOUR:VOLT:PROT {ovp}",                   # caps the output if a sense lead opens
        f"SOUR:VOLT:DEL {delay}",                  # fixed source delay; autodelay off
        'SENS:FUNC "CURR"',
        "SENS:CURR:RANG:AUTO ON",
        f"SENS:CURR:NPLC {nplc}",
        f"SENS:CURR:RSEN {'ON' if four_wire else 'OFF'}",
        # start, stop, points, sweep delay, count, range type, fail-abort
        f"SOUR:SWE:VOLT:LIN {start}, {stop}, {points}, 0, 1, BEST, OFF",
    ):
        smu.write(command)
    if errors := drain_errors(smu):
        raise RuntimeError(f"setup rejected: {errors}")

    budget_s = points * (delay + 4 * nplc / line_hz) + 5  # headroom: autozero, readback
    with output_guard(smu), io_timeout(smu, budget_s):
        smu.write("INIT")   # runs the trigger model; the sweep switches the output on
        smu.query("*OPC?")  # answers once the trigger model is idle again
        n = int(smu.query('TRAC:ACT? "defbuffer1"'))
        raw = smu.query_ascii_values(
            f'TRAC:DATA? 1, {n}, "defbuffer1", SOUR, READ, SOURSTAT'
        )

    data = np.array(raw).reshape(-1, 3)  # one row per point: source, reading, status
    status = data[:, 2].astype(int)
    if four_wire and not np.all(status & FOUR_WIRE):
        raise RuntimeError("some points were measured without 4-wire sense")
    return data[:, 0], data[:, 1], (status & LIMITED) != 0


# volts, amps, limited = iv_sweep_2450(smu, 0.0, 0.8, 41, ilimit=0.01)
```

What each choice does:

- **`BEST` range** picks one fixed source range that covers every level, which Keithley says avoids overshoots. `AUTO` moves to the most sensitive range at each point; `FIXED` keeps the present range and outputs its maximum for any level beyond it.
- **Fail-abort `OFF`** completes the sweep at the limit, and the status column records where. The default, `ON`, aborts. A diode reaching the limit at high forward bias is expected; in a leakage test it may be the failure you want to stop on.
- **`*OPC?` waits for the trigger model** to go idle, so the I/O timeout covers the whole sweep. While the trigger model runs, the 2450 does not execute most commands, which is why the guard sends `ABOR` before `OUTP OFF`.
- **`TRAC:DATA?` interleaves the elements** in the order listed, one group per point. With source readback on, the default, `SOUR` is the measured source voltage, not the programmed one.
- **The four-wire check** reads bit 6 (decimal 64), which records whether four-wire sense was used for each reading.

## How do source delay and NPLC trade speed for accuracy?

**Source delay.** The 2450 applies it at the first source output and whenever the source magnitude changes, so at every step of a staircase. Autodelay, the default, depends on the current range. The manual's values for a voltage source:

| Current range  | Autodelay | With high capacitance on |
| -------------- | --------- | ------------------------ |
| 10 nA, 100 nA  | 50 ms     | 100 ms                   |
| 1 µA           | 3 ms      | 20 ms                    |
| 10 µA          | 2 ms      | 10 ms                    |
| 100 µA to 1 mA | 1 ms      | 10 ms                    |
| 10 mA to 1 A   | 1 ms      | 5 ms                     |

Setting `SOUR:VOLT:DEL` (0 to 4 seconds) turns autodelay off, so every point waits the same time; choose a delay longer than your DUT's settling time. The sweep's own delay argument adds to the source delay rather than replacing it (Keithley's example: 10 ms plus 25 ms gives 35 ms), so the code passes 0 there.

**NPLC.** Integration time is set in power-line cycles, from 0.01 to 10, default 1:

| NPLC        | At 60 Hz | At 50 Hz | Where it fits                 |
| ----------- | -------- | -------- | ----------------------------- |
| 0.01        | 0.167 ms | 0.2 ms   | Fastest sweeps, most noise    |
| 0.1         | 1.67 ms  | 2 ms     | Currents well above the noise |
| 1 (default) | 16.67 ms | 20 ms    | General IV work               |
| 10          | 166.7 ms | 200 ms   | Low-current points            |

Per the manual, shorter times read faster with more noise and fewer usable digits; longer times read slower with less noise.

Two defaults add time on top:

- **Autozero** (`SENS:CURR:AZER`) re-measures the internal ground and reference with each reading. Keithley suggests turning it off when sweep timing is critical and sending `SENS:AZER:ONCE` just before the sweep to limit drift.
- **Source readback** (`SOUR:VOLT:READ:BACK`) measures the actual source value before each DUT reading: more accurate, slower.

For the example call, 41 points at 1 NPLC on 60 Hz spend 683 ms integrating and 205 ms in delays; the timeout budget leaves room for autozero and readback.

## When do I need 4-wire (remote) sensing on an SMU?

Two-wire sensing measures voltage at the output terminals, so the reading includes the lead drop. Four-wire sensing measures at the DUT through separate sense leads, and a voltage source raises its output until the DUT sees the programmed value. Keithley guarantees the 2450's specified accuracies only with 4-wire remote sensing.

The lead error is Ohm's law. If leads and contacts add 0.2 Ω, then 100 mA drops 20 mV, 2.5% of a 0.8 V diode point; at 10 µA the drop is 2 µV. Four-wire sensing matters most for low impedances and high currents.

The 2450 sets sense per measure function, so a current measurement uses `SENS:CURR:RSEN ON`; the 2400 uses `:SYST:RSEN ON`. On the 2450, changing the sense setting turns the output off, and with the output off it uses 2-wire sensing whatever the setting.

The hazard is an open sense lead. It reads 0 V, so a voltage source raises its output to compensate. Both manuals recommend overvoltage protection: `SOUR:VOLT:PROT PROT2` through `PROT180` on the 2450, a voltage such as `:SOUR:VOLT:PROT 20` on the 2400, both off by default. Each sweep function picks the smallest level at or above 1.2 times its largest voltage.

## How do I run the same IV sweep on a Keithley 2400?

The Series 2400 builds the sweep from separate commands, adapted from the diode example in its user's manual, which sources current instead:

```python title="bench/iv_2400.py"
import numpy as np

from bench.keithley import drain_errors, io_timeout, output_guard

COMPLIANCE = 8  # status word bit 3: reading taken in real compliance
OVP_LEVELS = (20, 40, 60, 80, 100, 120, 160)  # Model 2400 levels; other 24xx differ


def iv_sweep_2400(smu, start: float, stop: float, step: float, ilimit: float,
                  nplc: float = 1.0, delay: float = 0.005, four_wire: bool = True):
    """Linear voltage sweep measuring current. Returns volts, amps and compliance flags."""
    v_max = max(abs(start), abs(stop))
    ovp = next((p for p in OVP_LEVELS if p >= 1.2 * v_max), 210)  # 210 = NONE
    for command in (
        "*RST",                                    # compliance is back to 105 uA
        ":SENS:FUNC:CONC OFF",
        ":SOUR:FUNC VOLT",
        ':SENS:FUNC "CURR"',
        f":SENS:CURR:PROT {ilimit}",               # current compliance
        f":SOUR:VOLT:PROT {ovp}",                  # caps the output if a sense lead opens
        f":SENS:CURR:NPLC {nplc}",
        f":SYST:RSEN {'ON' if four_wire else 'OFF'}",
        f":SOUR:VOLT:START {start}",
        f":SOUR:VOLT:STOP {stop}",
        f":SOUR:VOLT:STEP {step}",
        ":SOUR:VOLT:MODE SWE",                     # after START, STOP and STEP
        ":SOUR:SWE:RANG BEST",
        ":SOUR:SWE:SPAC LIN",
        f":SOUR:DEL {delay}",                      # manual delay; disables auto delay
        ":FORM:ELEM VOLT,CURR,STAT",
    ):
        smu.write(command)
    points = int(float(smu.query(":SOUR:SWE:POIN?")))
    if points > 2500:
        raise ValueError(f"{points} points: the 2400 buffer holds 2,500 readings")
    smu.write(f":TRIG:COUN {points}")              # one trigger per sweep point
    if errors := drain_errors(smu):
        raise RuntimeError(f"setup rejected: {errors}")

    line_hz = float(smu.query(":SYST:LFR?"))
    budget_s = points * (delay + 4 * nplc / line_hz) + 5
    with output_guard(smu), io_timeout(smu, budget_s):
        smu.write(":OUTP ON")                      # READ? needs the output on
        raw = smu.query_ascii_values(":READ?")     # runs the sweep, returns every point

    data = np.array(raw).reshape(-1, 3)  # one row per point: volts, amps, status
    return data[:, 0], data[:, 1], (data[:, 2].astype(int) & COMPLIANCE) != 0
```

- **The trigger count equals the points** for a single sweep: (stop − start) / step + 1, which `SOUR:SWE:POIN?` reads back. Arm count times trigger count cannot exceed the 2,500-reading buffer.
- **`SOUR:VOLT:MODE SWE` follows start, stop and step,** so the sweep is not rebuilt after each command.
- **`VOLT` is the programmed level** here, because only current is measured.
- **The output stays on.** Auto output-off is off after a reset, so the output must be on for `READ?` and stays on afterward, which the manual warns can leave hazardous voltage on the terminals. The guard turns it off.

## How do I turn a Keithley SMU output off safely?

`OUTP OFF` in a `finally` block is the minimum, and the guard adds `ABOR` for a sweep still running. What "off" means is a separate setting, the output-off state, set with `OUTP:SMOD` on both instruments. The 2450 manual describes four:

| Output-off state   | What `OUTP OFF` does                                    | Use it for                                 |
| ------------------ | ------------------------------------------------------- | ------------------------------------------ |
| `NORMal` (default) | 0 V source, current limit at 10% of range, 2-wire sense | Most passive DUTs                          |
| `HIMPedance`       | Opens the output relay                                  | Power sources, other SMUs, some capacitors |
| `ZERO`             | 0 V on the present range, usable as an ammeter          | Current at 0 V bias                        |
| `GUARd`            | 0 A if sourcing current, voltage limit at 10% of range  | Guarded setups                             |

In `NORMal`, off is still a 0 V source, so a battery or charged capacitor on the terminals still sees the SMU. Keithley recommends `HIMPedance` when the instrument is connected to a power source or another SMU, but not for tests that switch the output often, because each cycle wears the relay.

Two more checks:

- **The interlock gates the 200 V range.** Without it, the 2450's nominal output stays below ±42 V. `OUTP:INT:TRIP?` returns 1 when the interlock is asserted; check it before a high-voltage sweep.
- **Protection is not a safe-to-touch level.** Keithley says to assume hazardous voltage (more than 30 V rms) whenever the output is on.

## When is TSP the better choice?

TSP pays off when logic should run on the instrument. A script loaded with `loadscript` and `endscript` runs on the 2450 without a round trip per command, and TSP-Link synchronizes several instruments. This is the same sweep in TSP, adapted from Keithley's example, for a 2450 already switched with `*LANG TSP`:

```python title="iv_tsp.py"
from bench.keithley import io_timeout

SWEEP = """\
reset()
smu.measure.func = smu.FUNC_DC_CURRENT
smu.measure.autorange = smu.ON
smu.measure.nplc = 1
smu.measure.sense = smu.SENSE_4WIRE
smu.source.func = smu.FUNC_DC_VOLTAGE
smu.source.ilimit.level = 0.01
smu.source.protect.level = smu.PROTECT_2V
smu.source.delay = 0.005
smu.source.sweeplinear("IV", 0, 0.8, 41, 0, 1, smu.RANGE_BEST, smu.OFF)
trigger.model.initiate()
waitcomplete()"""

# smu = rm.open_resource(...)  # open_smu() expects SCPI mode
try:
    for statement in SWEEP.splitlines():
        smu.write(statement)  # TSP has no queries; print() is the only way back
    with io_timeout(smu, 30):
        raw = smu.query_ascii_values(
            "printbuffer(1, defbuffer1.n, defbuffer1.sourcevalues, defbuffer1.readings)"
        )
except BaseException:
    smu.clear()
    smu.write("trigger.model.abort()")
    raise
finally:
    smu.write("smu.source.output = smu.OFF")
```

`printbuffer()` returns one comma-separated message, which `query_ascii_values` parses. The cost is that instrument logic becomes Lua inside Python strings: typos surface on the instrument, not in your editor, and the commands no longer map onto a driver class or an instrument profile. For step-by-step bench code, SCPI stays simpler.

## How do I run this IV sweep in Galois with Évariste?

Évariste, the agent in the Galois platform, does the work of `bench/keithley.py` and `iv_sweep_2450()` from a plain-English objective. Open it from the app sidebar (Ctrl+Shift+E) beside a project and state the sweep with your datasheet's limits:

> Create an IV sweep sequence for the Keithley 2450: source 0 V to 0.8 V in 41 points with a 10 mA current limit, measure current at 1 NPLC with 4-wire sensing, a 5 ms source delay and overvoltage protection at 2 V, record every point, and leave the output off at the end.

Évariste lists the instruments on your team's edges and reads the 2450's profile commands. If the sweep needs a command the profile does not define, upload the 2450 reference manual: Évariste generates a profile from it and, after your review, deploys it to an edge and binds it to the SMU. It then drafts a sequence of named profile commands, a source step and a measure step per point. An excerpt:

```yaml title="diode_iv_2450.yaml (excerpt)"
name: "Diode IV, 0 to 0.8 V, 41 points, 10 mA limit"
steps:
  # setup elided: reset, source function VOLT, sense function CURR with autorange,
  # NPLC 1 and 4-wire sense on the current function, OVP PROT2, 5 ms source delay
  - name: "Source range 2 V, fixed for the sweep"
    type: action
    config:
      instrument_id: "smu"
      command_name: "source_voltage_range"
      parameters: { value: "2" }

  - name: "Current limit 10 mA"
    type: action
    config:
      instrument_id: "smu"
      command_name: "current_compliance"
      parameters: { value: "0.01" }

  - name: "Source 0.00 V"
    type: action
    config:
      instrument_id: "smu"
      command_name: "source_voltage"
      parameters: { value: "0" }

  - name: "Output on"
    type: action
    config:
      instrument_id: "smu"
      command_name: "output_state"
      parameters: { state: "ON" }

  - name: "Measure current at 0.00 V"
    type: measure
    config:
      instrument_id: "smu"
      command_name: "measure_current"
      unit: "A"

  # 40 more source and measure pairs, 0.02 V apart, to 0.80 V

  - name: "Output off"
    type: action
    config:
      instrument_id: "smu"
      command_name: "output_state"
      parameters: { state: "OFF" }
```

The fixed 2 V source range does the job of `BEST`: one range covers every level. `measure` steps record each current without a verdict. A point held at the limit reads about 10 mA; `iv_sweep_2450()` flags the same points from status bit 5.

The sequence lands as a draft, and a draft does not run until an engineer approves it. Check the draft and each command's profile entry against this guide: the limit must send `SOUR:VOLT:ILIM`, not the 2400's `SENS:CURR:PROT`; NPLC and sense must target the current function; overvoltage protection must be on; the 41 source levels must run from 0 V to 0.8 V; the last step must turn the output off. [How to review an AI-generated test plan](https://galoislabs.ai/blog/review-ai-generated-test-plan) lists what else to check. Ask Évariste for changes in conversation or edit in the sequence builder; every change is a new version with history and diffs.

Start the run; galois-edge executes it on the bench and Monitor shows the channels live. Stopping a run partway skips the final output-off step, so turn the output off at the 2450 or from the conversation before anyone touches the leads. Commands you send the SMU from the conversation ask for confirmation when the profile flags them as dangerous.

Each measure step records the current, the raw command and response, instrument, operator, DUT serial and timestamps. Ask Évariste which points sit at the 10 mA limit or how this diode compares with the last one; answers cite the runs they draw on. "Generate a test report from the last run" produces a PDF or HTML report from a LaTeX template; add the diode's part number and the force and sense wiring in the report editor, and results can go to Slack.

Your job is the objective, the datasheet limits, the review, the approval and the bench: force and sense wiring, a 2450 in SCPI mode, and checking that the output is off before anyone touches the leads. The helper module, sweep function, timeout budget, and the code that stores each sweep and builds its report are no longer yours to maintain.

| Step                 | Code path (this guide)                          | Galois with Évariste                                                                                                                |
| -------------------- | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| Find the SMU         | `open_smu()`: `*IDN?` and `*LANG?` checks       | "List connected instruments"                                                                                                        |
| Driver               | `bench/keithley.py` and raw SCPI strings        | Library profile, or one generated from the manual                                                                                   |
| Define the sweep     | `iv_sweep_2450(smu, 0.0, 0.8, 41, ilimit=0.01)` | Plain-English objective; Évariste drafts the sequence                                                                               |
| Range and protection | `BEST`, `SOUR:VOLT:ILIM`, `PROT2`               | Fixed 2 V range, limit and OVP steps                                                                                                |
| Review               | Code review                                     | Draft reviewed, versioned, approved                                                                                                 |
| Run                  | Call the function on the bench PC               | Run through galois-edge, watched in Monitor                                                                                         |
| Safe output-off      | `output_guard`: `ABOR`, then `OUTP OFF`         | Final output-off step; after an early stop, turn the output off yourself (`cleanup_commands` fire only when the daemon disconnects) |
| Record               | Arrays returned to the caller                   | Per-step current, raw I/O, operator, DUT serial, timestamps                                                                         |
| Interpret            | The `limited` flags you inspect                 | Évariste finds points at the limit, compares runs                                                                                   |
| Report               | Report code you write                           | Generated PDF or HTML, shared to Slack                                                                                              |

## How do I run Keithley SMU sweeps on other benches or from an agent?

The Python in this guide runs on the PC the SMU is cabled to. With several benches, the questions become where each SMU lives, who may switch it on, and what record each sweep leaves.

Galois is agent-driven test engineering for hardware teams: agents generate tests and instrument drivers, run them on real benches through the open-source galois-edge daemon, and turn the results into reports and a shared engineering record.

- **Remote PyVISA.** `pyvisa.ResourceManager("@galois")` sends each `write`, `query` and `query_ascii_values` call over HTTPS to Galois Cloud, which relays it to the galois-edge daemon on the bench PC, with no NI-VISA or local drivers on your machine. The [PyVISA backend docs](https://docs.galoislabs.ai/guides/pyvisa/) list which operations carry over, and the [typed `galois` SDK](https://docs.galoislabs.ai/guides/python-sdk/) covers notebooks.
- **Typed profiles.** Galois ships 573 instrument profiles across 135 manufacturers in its [instrument library](https://galoislabs.ai/instruments). A matched instrument gains keyword-only methods named after the profile's commands, so a misspelled parameter name fails fast instead of mapping to the wrong argument, and a profile's `cleanup_commands` run on disconnect, a natural home for `OUTP OFF`. [Declarative instrument drivers](https://galoislabs.ai/blog/declarative-instrument-drivers) explains why commands live in data.
- **Agents with limits.** Each profile command becomes a typed MCP tool, such as `keithley_2400__source_voltage`. Out-of-range values are rejected before any SCPI reaches the instrument, and commands marked `is_dangerous` are flagged so clients can ask for confirmation. [MCP for lab instruments](https://galoislabs.ai/blog/mcp-lab-instruments) explains the direct and relay access paths, and [Can an LLM safely drive lab instruments?](https://galoislabs.ai/blog/llm-instrument-safety) covers the other layers.
- **A record per step.** [Galois sequences](https://galoislabs.ai/product) store the SCPI sent, the raw response, the measured value and its limits for every step.

If one engineer owns one SMU, the functions in this guide are enough; the [Galois and PyVISA comparison](https://galoislabs.ai/compare/pyvisa) shows where that line falls. [AI test automation for hardware benches](https://galoislabs.ai/blog/ai-test-automation-hardware) covers what changes when an agent writes the sweep. For SMU sweeps in a university lab, see [lab automation for university research labs](https://galoislabs.ai/blog/research-lab-automation); for voltage and temperature corners on first silicon, see [post-silicon validation bench automation](https://galoislabs.ai/blog/post-silicon-validation-bench).

## Frequently asked questions

### Can I run a Keithley 2450 IV sweep without writing Python?

Yes. In Galois, describe the sweep to Évariste in plain English, with its voltage range, points, current limit, NPLC and 4-wire sensing. Évariste reads the 2450's profile, or generates one from the reference manual you upload, and drafts a sequence of source and measure steps that ends with the output off. The draft does not run until an engineer approves it. Then galois-edge runs it on the bench, and each point's current is recorded with the raw command and response, instrument, operator, DUT serial and timestamps. Évariste can compare runs and generate a PDF or HTML report.

### Why does my Keithley 2450 IV curve flatten at 105 µA?

Because *RST sets the current limit to 105 µA when the 2450 sources voltage, and the SMU clamps the source to stay inside the limit without raising an error. Send SOUR:VOLT:ILIM with the limit you need before the sweep command, and check each point's source status: bit 5 (decimal 32) means the source was limited at that point.

### Can a Keithley 2450 run Keithley 2400 code?

Yes. Send *LANG SCPI2400 and reboot, and the 2450 accepts most Series 2400 SCPI commands. In that mode it gives up its 10 nA, 100 nA and 20 mV ranges, the new trigger model and scripting, and Keithley notes that some 2400 code behaves differently. Send *LANG SCPI and reboot to return to the native command set.

### What is the difference between SCPI and TSP on a Keithley SMU?

SCPI mode takes text commands and answers queries that end in a question mark. TSP, Keithley's Test Script Processor, is a scripting language based on Lua: you send statements such as smu.source.ilimit.level = 0.01, get output only from print() or printbuffer(), and can store scripts on the instrument. The 2450 runs one command set at a time, chosen with *LANG and a reboot.

### What NPLC should I use for an IV curve?

Start at the default of 1 power-line cycle, which is 16.67 ms per reading at 60 Hz and 20 ms at 50 Hz. Drop to 0.1 or lower for fast sweeps where noise is acceptable, and go up to 10 for low-current points where noise dominates. Every step also pays the source delay, plus autozero and source readback time while those are on.
