Simulators¶
Audience: developers, CI, anyone running capa without the rig in front of them — and operators training new students on a laptop.
Scope: the sim family under capa.devices.sim — why sims exist, the signal-generator schema, what's faithful to real hardware, and what is not.
Why sim adapters exist¶
Sims unblock every UI iteration, test, and demo from hardware availability.
Each sim adapter mirrors the corresponding real adapter's DeviceAdapter
Protocol exactly — same capabilities, same emission shapes, same
SourceRecord.shape tag. Any procedure or sink that works against a sim
works against the real adapter, and the device_records/<adapter>.parquet
file inside a sim bundle is indistinguishable from one produced by the
real recorder.
What sims are not: a model of the physics. They emit deterministic, operator-scripted signals. They will not catch a calibration mistake, a loose thermocouple, or a flow controller that drifts under back-pressure.
Naming convention¶
Hardware configs sit side-by-side in configs/hardware/:
| Naming | Purpose |
|---|---|
sim_*.toml (e.g. sim_capa.toml, sim_minimal.toml) |
All-sim configs; no [devices] entry references a real adapter. Safe to run on any machine. |
*_real.toml (e.g. watlow_real.toml, webcam_real.toml) |
Single-family real configs; the others stay sim. Useful for bringing up one device at a time. |
capa_real_full.toml, capa_real_partial_*.toml |
Rig-shaped real configs. |
The pattern is conventional, not enforced — capa loads any TOML the user points at. Sticking to it keeps the difference between "I am testing without hardware" and "I am running the rig" visible in the file name.
Signal generators¶
Sim adapters do not roll their own value generators; they call into
capa.devices.sim._signals.
A signal is a stateless callable (t_s) -> float keyed off
RunClock
time — pure, deterministic, and testable.
Four kinds ship today:
kind |
Shape | Required keys | Notes |
|---|---|---|---|
constant |
value |
value |
Holds value forever. |
sine |
offset + amplitude·sin(2π·f·t + phase) |
amplitude, frequency_hz |
offset and phase default to 0. |
ramp |
linear start → end over duration_s, then hold |
start, end, duration_s |
Clamps at both ends. |
step |
before until at_s, then after |
before, after, at_s |
Discrete jump at at_s. |
Sim configs in TOML carry the spec, not the callable:
[devices.params.signals."process_value/1"]
kind = "ramp"
start = 30.0
end = 600.0
duration_s = 120.0
The adapter's from_params classmethod materializes specs into
SignalFn instances at construction. Adding a new kind means
registering it in _SIGNAL_KINDS in
_signals.py
and writing the dataclass — no adapter-side changes are needed.
Worked example: a CAPA-shaped sim run¶
The shipping sim_capa.toml
covers every channel group the
capa_pyrolysis profile requires.
Here is the heater portion:
[[devices]]
name = "heater"
adapter = "capa.devices.sim.watlow_sim"
[devices.params.signals."process_value/1"]
kind = "ramp"
start = 30.0
end = 600.0
duration_s = 0.2
[devices.params.signals."setpoint/1"]
kind = "constant"
value = 600.0
At t = 0 the simulated PV reads 30 °C; at t ≥ 0.2 s it holds at
600 °C. The setpoint is constant at 600 °C from the first tick. This is
deliberately a fast ramp — the sim_capa.toml defaults are tuned for
the test suite. For UI demos, set duration_s to a more human-scale
60–300 s.
The signal keys are adapter-specific — Watlow uses
"<parameter>/<instance>"; Alicat uses underscored frame-field names
(Mass_Flow, Abs_Press); Sartorius takes a single mass_signal;
NI-DAQ uses display-name channels. The per-device pages list each
adapter's accepted key schema.
Per-sim summary¶
Every real adapter has a sim peer. The sim mirrors the Protocol —
including the Capability flags it advertises — so the UI cannot tell
them apart by feature surface.
| Sim adapter | Mirrors | Emission shape | Signal-key schema | Use it to exercise |
|---|---|---|---|---|
capa.devices.sim.watlow_sim |
Watlow | long_row |
{"<parameter>/<instance>": spec} |
Heater PV/SP rendering, setpoint command path, ramp UI. |
capa.devices.sim.alicat_sim |
Alicat | wide_row |
{"<Frame_Field>": spec} |
Flow plots, gas-select UI, totalizer surfaces. |
capa.devices.sim.sartorius_sim |
Sartorius | single_value_row |
single mass_signal spec |
Mass loss curves, tare/zero plumbing, stability flag propagation. |
capa.devices.sim.nidaq_polled_sim |
NI-DAQ polled | wide_row |
{"<channel>": spec} |
TC channels, polled binding kind. |
capa.devices.sim.nidaq_block_sim |
NI-DAQ block | block |
{"<channel>": spec} |
Block sidecar, kHz path, downsampled channels. |
capa.devices.sim.flir_ir_sim |
FLIR IR | FrameReceipt (camera path) |
n/a — synthetic gradient frames | Camera-task wiring, manifest cameras block, frame-index parquet round-trip without an Atlas install. |
Each per-device page has a "Sim equivalent" subsection covering the
fields specific to its sim — what tick_period_s defaults to, which
Capability flags are advertised, and any sim-only escape hatches
(e.g. wire_temperature_unit=None on the Watlow sim suppresses the
drift check).
Sim-only environment flags¶
Two environment variables exist purely to drive sim adapters into states that don't occur naturally under a clean apply. They are documentation-tooling and test-suite knobs; do not set them in normal operator use.
| Variable | Effect |
|---|---|
CAPA_SIM_OPEN_DELAY_MS |
Sleep this many ms inside open(). Holds the connection strip in CONNECTING long enough to screenshot it. |
CAPA_SIM_OPEN_FAIL |
Raise AdapterError from open(). Forces the strip into FAILED. |
What sims do not model¶
This is the operationally important list — read it before you trust a green sim run as a green real run.
- Serial-bus contention. Sim adapters declare
sim:<name>resource_ids, so each gets its own worker. Real Watlow/Alicat/ Sartorius devices sharing a serial multi-drop bus share one worker that serializes I/O. A multi-device deadlock or starvation visible only under shared-bus serialization will not reproduce against sims. - The Sartorius cold-open race. The real balance pays a multi-
second cost on first
open()while it waits for a stable reading window; the sim opens instantly. See Sartorius § Cold-open race. - Hardware timing jitter. Sims tick on an
anyio.sleep. The real Watlow polls take ~50 ms per parameter; real DAQmx blocks land on the onboard clock; real cameras emit at their UVC/Atlas frame cadence. Sim ticks have no equivalent jitter. - Auto-reconnect paths.
auto_reconnect=Trueon real adapters swallows transient*ConnectionErrors and retries on the next tick. Sims do not raise those errors, so the recovery code is not exercised by sim runs alone. - NI-DAQ block sidecar / TDMS passthrough. The block sim emits a
block-shaped
SourceRecordso the bundle plumbing exercises, but the high-rate sidecar path is exercised at sim-friendly rates, not at the kHz rates real chassis produce. - Camera SDK quirks.
flir_ir_simwrites a deterministic capa-private "fake-csq" file with its own magic header. It is not a real FLIR FFF/.csq— a downstream tool that parses real Atlas output will reject it (intentionally — capa ships its own extractor for the sim format alongside the real one). - UVC/dshow probe results. The webcam sim (where used) does not
enumerate real OS camera APIs; the dshow
list_optionsresolution probe only runs against real devices.
When a behavior is sim-only, the engine flags it: snapshot pings
declare the family and the adapter id, so a bundle's
manifest.json carries the sim family label and any post-hoc
analysis can detect that the run was synthetic.
See also¶
- Devices overview — adapter contract and emission shapes.
- Hardware TOML — how the
[[devices]]block is parsed. - Channel bindings — which binding kinds consume which sim emissions.
- The shipping configs in
configs/hardware/— every example referenced above is a real file you can run.