Electronic load SCPI automation for battery discharge and PSU transient tests
By Alex Hernandez · · 14 min read


To automate a battery discharge with a programmable DC electronic load, select constant-current mode over SCPI, set the discharge current, turn the input on, and log voltage and current at a fixed interval while integrating amp-hours. Stop at the cell's cutoff voltage in your script, and set the load's own stop condition as a backstop in case the script dies.
This guide builds that test in Python with PyVISA, then adds a power-supply transient test and a shutdown path that holds when the script does not. Commands come from three vendor manuals: the B&K Precision 8600 programming manual, the Rigol DL3000 programming guide and Keysight's EL30000 Series user's guide (edition 3, October 2023). For resource strings, error queues and timeouts, start with SCPI instrument automation with Python; this post assumes that ground.
What do CC, CR, CV and CP modes do on an electronic load?
An electronic load sinks current from a device under test and regulates one quantity while the others move. All three loads in this guide pick the quantity with the same SCPI node, FUNCtion with CURRent, RESistance, VOLTage or POWer; B&K and Rigol accept an optional SOURce prefix (the Rigol column below keeps it), and Keysight appends a channel list such as (@1). The application columns below follow Chroma's 63600 datasheet, which lists uses for each mode.
| Mode | The load holds constant | Battery tests | Power-supply tests |
|---|---|---|---|
| CC | Current | Discharge time and life cycle at a fixed rate | Load and cross regulation of a CV supply |
| CR | Resistance; current falls with voltage | — | Current-limit point, soft start |
| CV | Its own input voltage | Simulating a battery for charger tests | Current limit of a foldback supply |
| CP | Power; current rises as voltage falls | Capacity and capacity life cycle | Output power versus efficiency curves |
Two warnings follow from the table. In CC, the load tries to pull the set current whatever the source can deliver; Rigol's guide notes that a CC setting above what the device can supply short-circuits it. In CP, current climbs as a battery's voltage falls, so a constant-power discharge ends at a higher current than it started. B&K's 8600 user manual adds the rule that covers both: do not exceed the battery manufacturer's maximum discharge rate.
Chroma's 63600 also offers a constant-impedance (CZ) mode, and its datasheet describes a timing function that sets a final voltage and timeout for battery discharge, measuring from 2 ms to 100,000 s.
Which SCPI commands do B&K Precision, Rigol and Keysight loads use?
The vocabulary overlaps more than it differs. The table maps one CC battery test across the B&K Precision 8600, the Rigol DL3000 and the Keysight EL30000, with each command checked against the manuals above.
| Task | B&K Precision 8600 | Rigol DL3000 | Keysight EL30000 |
|---|---|---|---|
| Select CC mode | FUNC CURR | :SOUR:FUNC CURR | FUNC CURR, (@1) |
| Set the current to 2 A | CURR 2 | :SOUR:CURR 2 | CURR 2, (@1) |
| Turn the input on | INP ON | :SOUR:INP ON | INP ON, (@1) |
| Current slew rate | CURR:SLEW 0.1 (A/µs) | :SOUR:CURR:SLEW 0.1 (A/µs) | CURR:SLEW <rate>, (@1) |
| Turn-on voltage (Von) | VOLT:ON 2.9 and VOLT:LATC ON | :SOUR:CURR:VON 2.9 | VOLT:INH:VON 2.9, (@1) and VOLT:INH:VON:MODE LIVE, (@1) |
| Measure voltage | MEAS:VOLT? | :MEAS:VOLT? | MEAS:VOLT? (@1) |
| Firmware discharge stop | Battery test from the front panel; over SCPI, Von with latch on | :SOUR:FUNC:MODE BATT, :SOUR:BATT:VST, :SOUR:BATT:VEN ON | BATT:CUTO:VOLT, BATT:CUTO:VOLT:STAT 1, BATT ON |
| Interfaces | USBTMC, RS-232, GPIB on B models | USB, LAN (standard or optional by model), RS-232, GPIB through a USB adapter | USB; LAN and optional GPIB on the EL34143A and EL34243A |
Three differences break scripts that look portable.
Units. B&K and Rigol program current slew in amps per microsecond. Keysight's user's guide labels its CURR:SLEW example in amps per second; confirm the unit in the EL30000 programming guide before sending a number, because the same digits can mean rates a million times apart. Transient timing differs too: B&K takes a width followed by a separate unit token (CURR:TRAN:AWID 5 mS), while Rigol takes widths in milliseconds and frequency in kilohertz.
Spellings. Rigol's guide spells the battery subsystem header BATTary and the mode parameter BATTery. SCPI accepts only the exact short or long form, so use the short form BATT for both and the question disappears.
Vendor examples drift. In Keysight's user's guide, the battery example described as a 3.85 V cutoff sends BATT:CUTO:VOLT 0.015, (@1). Copy command syntax from the manual and values from your test plan, then read every setpoint back.
Chroma's 63600 is a mainframe of up to five modules and ten channels behind one GPIB address, so its scripts address a channel as well as an instrument. Take its commands from Chroma's own programming manual rather than from this table.
How do I run a battery discharge test with an electronic load?
A discharge test has four parts: a constant current chosen from the cell's datasheet, a cutoff voltage from the same datasheet, a sampling interval, and a log. Two details decide whether the numbers mean anything.
Measure at the cell. At 2 A, 25 mΩ in each lead drops 100 mV across the pair, and a load measuring at its own terminals sees the cell that much lower than it is. The test then stops early and reports less capacity than the cell holds. Run separate sense leads to the cell terminals and turn remote sense on (:SOUR:SENS ON on the Rigol, REM:SENS 1 on the B&K). Rigol's guide warns that with sense enabled and the leads unconnected, the load cannot measure the voltage accurately.
Stop twice. The script owns the cutoff, because it can debounce, log the reason and leave a record. The load owns a backstop set slightly lower, because it keeps working when the script does not. On the Rigol, battery mode provides that backstop: :SOUR:FUNC:MODE BATT hands regulation to the battery commands, :SOUR:BATT sets the discharge current, and stop voltage, capacity and time each have a value and an enable switch.
"""CC discharge on a Rigol DL3000: software cutoff, firmware backstop, CSV log."""
import csv
import signal
import sys
import time
import pyvisa
I_DISCHARGE = 2.0 # A, within the cell's rated discharge current
V_CUTOFF = 3.00 # V, software cutoff from the cell datasheet
V_BACKSTOP = 2.90 # V, firmware stop just below the software cutoff
MAX_S = 3 * 3600 # s, firmware discharge-time stop
PERIOD_S = 1.0 # s between samples
DEBOUNCE = 3 # consecutive readings at or below V_CUTOFF before stopping
def on_sigterm(signum, frame):
raise SystemExit(f"signal {signum}") # turns SIGTERM into an exception, so finally runs
def check_errors(load) -> None:
code, _, message = load.query(":SYST:ERR?").partition(",")
if int(code) != 0:
raise RuntimeError(f"load error {code}: {message}")
def configure(load) -> None:
load.write("*RST") # factory defaults; clears the error queue
load.write(":SOUR:FUNC:MODE BATT") # regulation set by the battery commands
load.write(f":SOUR:BATT {I_DISCHARGE}") # discharge current
load.write(f":SOUR:BATT:VST {V_BACKSTOP}") # firmware stop voltage
load.write(":SOUR:BATT:VEN ON")
load.write(f":SOUR:BATT:TIM {MAX_S}") # firmware stop time, seconds
load.write(":SOUR:BATT:TEN ON")
load.write(":SOUR:SENS ON") # needs sense leads at the cell
check_errors(load)
for query, want in ((":SOUR:BATT?", I_DISCHARGE), (":SOUR:BATT:VST?", V_BACKSTOP)):
got = float(load.query(query))
if abs(got - want) > 0.005:
raise RuntimeError(f"{query} read back {got}, expected {want}")
def safe_off(load) -> None:
"""Turn the input off and confirm it. Runs during cleanup, so it never raises."""
try:
load.write(":SOUR:INP OFF")
state = load.query(":SOUR:INP?").strip()
if state != "0":
print(f"WARNING: input reports {state} after INP OFF", file=sys.stderr)
except Exception as exc:
print(f"WARNING: load unreachable, only firmware stops remain: {exc}", file=sys.stderr)
def run(resource: str, path: str) -> str:
signal.signal(signal.SIGTERM, on_sigterm)
rm = pyvisa.ResourceManager()
load = rm.open_resource(
resource, read_termination="\n", write_termination="\n", timeout=5_000
)
try:
configure(load)
with open(path, "w", newline="") as f:
log = csv.writer(f)
log.writerow(["t_s", "volts", "amps", "ah"])
load.write(":SOUR:INP ON")
t0 = last = time.monotonic()
ah, amps_last, below = 0.0, 0.0, 0
while True:
volts = float(load.query(":MEAS:VOLT?"))
amps = float(load.query(":MEAS:CURR?"))
now = time.monotonic()
ah += (amps + amps_last) / 2 * (now - last) / 3600 # trapezoid rule
amps_last, last = amps, now
log.writerow([f"{now - t0:.3f}", volts, amps, f"{ah:.6f}"])
f.flush() # a crash loses at most one row
below = below + 1 if volts <= V_CUTOFF else 0
if below >= DEBOUNCE:
reason = "software cutoff"
break
if now - t0 > 5 * PERIOD_S and amps < 0.05 * I_DISCHARGE:
reason = "load stopped sinking (firmware stop, Von or protection)"
break
time.sleep(PERIOD_S)
counted = load.query(":MEAS:CAP?").strip() # the load's own count; stops are set in mAh
return f"{reason}: {ah:.4f} Ah integrated, load counted {counted}"
finally:
safe_off(load) # runs on return, exceptions, Ctrl-C and SIGTERM
load.close()
rm.close()
if __name__ == "__main__":
print(run(sys.argv[1], "discharge.csv")) # resource string from rm.list_resources()Most of the script exists to make the stop reliable.
- The cutoff is debounced. A single low reading from noise or a load step should not end a three-hour test, so the script waits for three consecutive samples. That delays the stop by up to three sample periods. It is also why the firmware backstop sits below the software cutoff: at the same voltage, the firmware would end every test before the script could record why.
- A collapsed current is a stop condition. If the battery-mode stop, a protection trip or a Von setting turns the load off, voltage readings alone can look normal while nothing is being discharged. Watching current catches every one of those cases without knowing which fired.
- Time comes from
time.monotonic(). Wall-clock time can jump when the PC syncs its clock; a monotonic clock cannot, and the amp-hour integral depends on every interval. - Two capacity numbers. The script integrates its own amp-hours and also reads the load's count with
:MEAS:CAP?. If they disagree by more than the sampling error, find out why before trusting either. - Settings are read back.
configure()reads the discharge current and backstop voltage after writing them, which catches a typo or a value the load clamped.
The same structure ports to the other two loads by swapping commands from the table. On the Keysight EL30000, the user's guide sequence is FUNC CURR, CURR, BATT:CUTO:VOLT:STAT 1, BATT:CUTO:VOLT, BATT ON and then INP ON, each with , (@1); the guide states that the load cuts off its input when the condition triggers, with capacity up to 100,000 Ah and duration up to 100,000 s.
What does Von latch do on an electronic load?
Von is the turn-on voltage: the load does not sink current until its input rises above it. It looks like a cutoff, and on some loads it can act as one, but the word "latch" means opposite things on two of the three loads in this guide.
| Load and setting | Below Von after the test starts | Source |
|---|---|---|
| B&K 8600, Von latch on | Stops sinking and turns the input off | 8600 user manual |
| B&K 8600, Von latch off | Keeps sinking, input stays on | 8600 user manual |
Keysight EL30000, Live (VOLT:INH:VON:MODE LIVE) | Turns the input off, back on when voltage reaches Von | EL30000 user's guide |
| Keysight EL30000, Latched | Keeps sinking | EL30000 user's guide |
Rigol DL3000, :SOUR:CURR:VON | Stops sinking below Von, sinks above it | DL3000 programming guide |
B&K's latch holds the input off. Keysight's holds the sinking on. A battery makes the difference matter: once the load stops drawing current, the cell's voltage recovers toward its open-circuit value, crosses Von again, and a live threshold turns the load back on. Near the end of discharge, that cycles the load on and off instead of ending the test. B&K also notes a delay of under one second between crossing Von and the input changing state.
For a discharge, use a stop that stays stopped: Rigol's or Keysight's battery stop, or B&K's Von with latch on. Keep a live Von for holding the load off until a supply's output comes up.
How do I stop an electronic load safely when a script fails?
A finally block covers exceptions and Ctrl-C. It does not cover everything.
Signals. Python's default signal handlers turn SIGINT into KeyboardInterrupt and ignore SIGPIPE. A SIGTERM from a service manager or a CI job timeout ends the process without running finally, which is why discharge.py converts it to SystemExit. Nothing in Python catches SIGKILL, a power cut or a crashed PC.
Lost connections. If USB or the network drops mid-test, safe_off() cannot reach the load. The script warns and exits, and the load keeps sinking until something inside it says stop. That is the case the firmware backstops exist for, and it is why each one should be set before the input turns on.
On the B&K 8600, which has no battery subsystem in its programming manual, the backstops are Von, overcurrent protection and the input timer:
from discharge import check_errors # one SYST:ERR? read; the B&K answers in the same format
def set_backstops(load) -> None:
"""load: an open PyVISA resource for the 8600, opened as in bk8600_transient.py."""
for command in (
"VOLT:ON 2.9", # Von, volts
"VOLT:LATC ON", # below Von: stop sinking and turn the input off
"CURR:PROT:LEV 2.5", # soft overcurrent level, amps
"CURR:PROT:DEL 1", # seconds above the level before the input turns off
"CURR:PROT:STAT ON",
"INP:TIM:DEL 10800", # input turns off this many seconds after it turns on
"INP:TIM ON",
):
load.write(command)
check_errors(load) # B&K answers "0, No Error" when the queue is emptyB&K's user manual confirms that once the timer delay passes, the input is disabled, with a range up to 60,000 seconds. Protection trips latch on both B&K and Keysight. Clear them (PROT:CLE on the B&K, INP:PROT:CLE (@1) on the Keysight) only after removing the condition that caused the trip, as both manuals require, and record the trip in the test log rather than clearing it silently. Both manuals also say a clear returns the input to its state before the trip, so clearing during a test resumes sinking.
After any stop, read the error queue, query the input state, and write the end reason into the log next to the data. A capacity number without its stop reason is not a result.
How do I automate a power supply load transient test?
A load transient test steps a supply's output current between two levels at a controlled slew rate and watches the output voltage dip and recover. The load makes the step; an oscilloscope on the output makes the measurement, because the load's own meter averages. The B&K 8600 averages 2 to 16 readings, set with SENS:AVER:COUN.
Match the slew to the transient specification in the regulator's datasheet, since the response depends on it. At 0.1 A/µs, a 2 A step takes 20 µs. This setup follows the command order of B&K's continuous-transient example, with smaller levels and longer widths:
import pyvisa
from discharge import check_errors
rm = pyvisa.ResourceManager()
load = rm.open_resource("GPIB0::5::INSTR", # B models; address set in the System menu (0 to 30)
read_termination="\n", write_termination="\n", timeout=5_000)
try:
for command in (
"*RST", # CC mode, transient off, slew at maximum
"FUNC CURR",
"CURR:SLEW:POS 0.1", # A/us, within the model's slew specification
"CURR:SLEW:NEG 0.1",
"CURR:TRAN:MODE CONT", # repeat A, B, A, B after the trigger
"CURR:TRAN:ALEV 0.5", # amps
"CURR:TRAN:AWID 5 mS", # width, then a separate unit token
"CURR:TRAN:BLEV 2.5",
"CURR:TRAN:BWID 5 mS", # 10 ms period: 100 Hz at 50 percent duty
"TRAN ON",
):
load.write(command)
check_errors(load)
load.write("INP ON")
load.write("TRIG:IMM") # start the pulse train
input("Capture the output on the scope, then press Enter to stop.")
finally:
load.write("INP OFF") # the input first, in case the next write fails
load.write("TRAN OFF")
load.close()
rm.close()In continuous mode the frequency is 1/(A width + B width) and the duty cycle is A width over the period, per B&K's manual. B&K also offers PULS, one pulse per trigger, and TOGG, which switches levels on each trigger. Rigol uses the same three mode names, with widths in milliseconds and frequency in kilohertz; its guide notes that the digital I/O trigger output (an option on the DL3021 and DL3031) can trigger an oscilloscope, which lines up each capture with a load edge. Keysight's example sets frequency in hertz and duty cycle in percent (TRAN:FREQ 50, (@1) and TRAN:DCYC 10.5, (@1)). For a full regulator test plan built on these steps, see DC-DC converter validation and the power rail validation plan; battery-pack work continues in BMS validation testing.
How do I run this battery discharge in Galois with Évariste?
Évariste, the agent in the Galois platform, runs the same discharge without discharge.py, on the real bench through the galois-edge daemon. Open it from the app sidebar (Ctrl+Shift+E) beside the bench's project. AI test automation for hardware benches explains the draft-and-approve model.
Find the load and its driver. Ask Évariste to "List connected instruments"; it lists the instruments on your team's edges and reads the load's profile commands. If your DL3000 has no profile, upload the programming guide linked above and Évariste generates one. Review it before it is deployed to the edge and bound to the load: battery commands in the short form BATT, slew in A/µs, and input on flagged dangerous.
State the objective. The numbers are the ones discharge.py takes from the cell datasheet:
Create a CC discharge sequence for the Rigol DL3000. Reset, then battery mode at 2 A with remote sense on. Stop at the cell's 3.00 V cutoff and after 3 hours. Read back the discharge current and stop voltage and fail if either is off by more than 0.005. Turn the input on, then record cell voltage, current and the load's capacity count every 5 minutes until the time stop. End by turning the input off and checking that it reads 0.
The sequence reads the cell at checkpoints rather than polling it, so the cutoff is the load's battery stop, at 3.00 V instead of 2.90 V, backed by the time stop.
Review the draft. Évariste returns a draft of named profile commands:
name: "CC discharge, 2 A to 3.00 V"
steps:
# reset, battery mode, 2 A, both stops and their enables, remote sense elided
- name: "Read back discharge current"
type: numeric_limit
config:
instrument_id: "load"
command_name: "get_battery_current"
low_limit: 1.995
high_limit: 2.005
unit: "A"
comparison: "GELE"
# "Read back stop voltage", 2.995 to 3.005 V, elided
- name: "Input on"
type: action
config:
instrument_id: "load"
command_name: "input_on"
- name: "Wait 5 min"
type: wait
config: { duration_ms: 300000 }
- name: "Cell voltage at 5 min"
type: measure
config:
instrument_id: "load"
command_name: "measure_voltage"
unit: "V"
# current and capacity at 5 min, then each checkpoint to 180 min, elided
- name: "Input off"
type: action
config:
instrument_id: "load"
command_name: "input_off"
- name: "Input reads off"
type: string_value
config:
instrument_id: "load"
command_name: "get_input_state"
expected_value: "0"A draft does not run until an engineer approves it. Check each command against the DL3000 guide, both stops against the cell datasheet, remote sense on, and the input-off steps last. A failed step is recorded and the run moves on, so a bad read-back would not hold the input off as configure() does; ask Évariste to split the setup and read-backs into their own sequence, or to put the input-on step behind Condition steps on the read-backs. Edit in conversation or the sequence builder; each change is a new version with history and a diff, and a settled one can be production-locked. How to review an AI-generated test plan has the full checklist.
Run it. Connect the sense leads at the cell, run the setup sequence and, once it passes, start the discharge with the cell's serial number; galois-edge runs the steps while Monitor shows voltage and current live. Canceling skips the remaining steps, input off included, so turn the input off from the conversation, where dangerous commands wait for your confirmation. If the edge loses the load, the load's own stops end the discharge.
Read the results. Each step is recorded with its measured value, limits, pass or fail, raw command and response, instrument, operator, DUT serial and timestamps. Ask Évariste which stop ended the discharge: current at zero before the 180-minute checkpoint means the voltage stop or a protection trip. Have it read the error queue before anything is cleared, then compare this cell's capacity with earlier runs.
Report. "Generate a test report from the last run" builds a PDF or HTML report from a LaTeX template. Add the stop reason in the report editor, then share it, to Slack for example.
You no longer write or maintain configure(), safe_off(), the sampling loop, the CSV log or a report script. The current and cutoff from the cell datasheet, the review, the approval, the sense leads and bench safety stay with you. The B&K transient test follows the same flow, and the scope capture stays yours.
| Step | Code path (this guide) | Galois with Évariste |
|---|---|---|
| Connect | open_resource(), terminations, timeout | "List connected instruments" |
| Driver | SCPI copied from the DL3000 guide | Profile generated from the guide, reviewed |
| Configure | configure() | Setup sequence drafted from the objective |
| Read back | abs(got - want) > 0.005 raises before input on | Same-tolerance limit steps in a setup run |
| Cutoff | Debounced 3.00 V; firmware 2.90 V and 3 hours | Load's stops at 3.00 V and 3 hours |
| Sample and log | 1 s loop, CSV row per sample | Checkpoint steps; Monitor live |
| Shutdown | finally: safe_off(), SIGTERM handler | Input-off steps; the load's stops as backstop |
| Review | Code review of discharge.py | Draft reviewed, versioned with diffs, approved |
| Interpret | Integrated Ah against :MEAS:CAP? | Which stop fired; capacity across cells |
| Report | A report script you maintain | Generated report, shareable to Slack |
Where Galois fits
The scripts above are enough for one load on one bench. As benches multiply, the same questions from every instrument return: which load is where, who changed the backstop, and which run produced this capacity number.
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.
The daemon discovers instruments over GPIB, USB, LAN and serial. Galois ships 573 instrument profiles across 135 manufacturers in its instrument library, including electronic loads such as the B&K Precision 8600, the Chroma 63600 and the Keysight EL34143A. A profile is YAML: commands, parameter types and ranges, and an is_dangerous flag. Instruments without a profile still accept raw SCPI. Existing PyVISA code like discharge.py reaches a load on another machine by changing one line, pyvisa.ResourceManager("@galois"), as the PyVISA backend docs describe; the Galois and PyVISA comparison sets the two side by side. The Évariste walkthrough above runs the same test with no script to port.
When an agent drives a load through the daemon's MCP server, each profile command becomes a typed tool whose out-of-range values are rejected before any SCPI reaches the wire, and commands marked dangerous carry a hint that lets MCP clients ask for confirmation. The generic send_scpi tool bypasses profile validation, so treat it as you would a raw terminal. MCP for lab instruments explains who can reach those tools. LLM instrument safety goes further on letting agents near power hardware. Either way, the firmware backstop stays: no software layer replaces a stop that lives inside the load.
Galois sequences keep a fuller record than the CSV above: for every step, the SCPI sent, the raw response, the measured value and its limits. To try the daemon on your own bench, start with the quickstart.
Frequently asked questions
- What is the difference between CC, CR, CV and CP mode on an electronic load?
- In constant current (CC) the load sinks a fixed current, in constant resistance (CR) it draws current in proportion to voltage like a resistor, in constant voltage (CV) it sinks whatever current holds its input at a set voltage, and in constant power (CP) it draws more current as voltage falls. Over SCPI the mode is usually selected with FUNCtion CURRent, RESistance, VOLTage or POWer.
- What is Von on an electronic load?
- Von is the turn-on voltage: the load does not sink current until its input rises above it. What happens when the voltage falls back below Von depends on the vendor and a latch setting. On a B&K Precision 8600 with Von latch on, the input turns off. On a Keysight EL30000 in live mode, the input turns off and turns back on when the voltage recovers.
- How do I stop a battery discharge at a cutoff voltage?
- Use two stops. In your script, turn the load input off after several consecutive readings at or below the cell's cutoff voltage. In the load, set a firmware stop slightly lower, such as the Rigol DL3000 battery mode stop voltage (:SOUR:BATT:VST with :SOUR:BATT:VEN ON) or the Keysight EL30000 battery cutoff (BATT:CUTO:VOLT), so the test ends even if the PC or the script fails.
- What units does an electronic load use for slew rate?
- It varies by vendor, so read the programming manual before sending a number. The B&K Precision 8600 and Rigol DL3000 program current slew in amps per microsecond. Keysight's EL30000 user's guide labels its CURR:SLEW example in amps per second. Transient timing also differs: B&K takes a unit token after each width, and Rigol takes widths in milliseconds and frequency in kilohertz.
- Can I run a battery discharge test without writing Python?
- Yes. In Galois, you give Évariste, the agent in the Galois platform, the objective with your cell's limits, such as a 2 A discharge on a Rigol DL3000 to a 3.00 V cutoff with a 3-hour time stop, and it drafts a versioned sequence; if the load has no profile, it generates one from the programming guide you upload. You review and approve both, run the test on the bench through galois-edge, and Évariste reads the per-step results and generates the test report.
Compare
Docs
Bring Galois to your bench.
The daemon is Apache-2.0, free forever. Enterprise runs in your cloud or on-prem.