Simulation Profiles
Every device type can ship an optional simulation.yaml alongside its manifest.yaml. The file is a declarative, data-driven description of how the device behaves — sensor readings ramp and lag, derived values are computed, disturbances add noise, and failure modes describe injectable faults. A data-driven simulator reads the profile and ticks the device forward in time — deterministic simple dynamics models overlaid with stochastic disturbance noise — so you can exercise the UI, alarms, historian, and AI advisory tools without a single piece of field hardware.
Profiles are authored as part of creating a device type and reference the same tags declared in the type’s manifest — see Tags for tag dataType, access, and physicalRole.
The ownership split: inputs vs outputs
Section titled “The ownership split: inputs vs outputs”The most important architectural rule is what the simulator is allowed to write.
- The simulator owns physical-input tags — sensor readings and external signals, i.e. tags whose manifest
physicalRoleisInput— plus any simulator-onlyInternaltags a type declares for the physical model’s own consistency. - Output tags are owned by the vPLC control logic (
physicalRole: Output). They are written by the function-block scan —Running,Speed,Fault,FaultCode,LocalMode,AutoModeand the like — and the profile must never write them.
A profile’s expressions may read output tags (for example, the centrifugal pump’s MotorCurrent steady state is "Speed / 3600 * 85", where Speed is read from the vPLC tag cache), but it only writes the input tags it declares. This keeps the simulator and the control logic cleanly separated: the vPLC decides what the equipment does, and the simulator supplies the physics the equipment’s sensors would report back.
graph LR SIM[Device simulator<br/>simulation.yaml] -->|writes Input tags<br/>MotorCurrent, BearingTemp,<br/>FlowRate, Level| CACHE[Tag cache] VPLC[vPLC control logic<br/>function-block scan] -->|writes Output tags<br/>Running, Speed, Fault| CACHE CACHE -->|Speed read back into<br/>sim expressions| SIM CACHE -->|source_class = simulated| HIST[Historian]
File structure
Section titled “File structure”The header identifies the profile and pins it to a device-type version range:
schemaVersion: 1deviceType: consystence.pump.centrifugaldeviceTypeVersion: ">=1.0.0"| Key | Meaning |
|---|---|
schemaVersion | Profile schema version (currently 1). |
deviceType | The dot-id of the type this profile drives (must match the manifest). |
deviceTypeVersion | Semver range the profile is valid for, e.g. ">=1.0.0". |
The remaining sections — all optional — are described below. In practice you almost always want initialValues, so input tags start from sensible values rather than zero.
initialValues
Section titled “initialValues”Seeds the input tags at start-up. Input tags only — output-tag initial state comes from the vPLC function block’s initialisation, not from here.
initialValues: SpeedSetpoint: 75 MotorCurrent: 0 BearingTemp: 32 FlowRate: 0 DischargePressure: 0 Interlock: falsestateScopedDynamics
Section titled “stateScopedDynamics”The core physics. Each entry binds a model to a single tag, scoped to a named device state (e.g. Running, Stopped, Opening, Closed).
stateScopedDynamics: - state: Running tag: MotorCurrent model: firstOrderLag parameters: steadyState: "Speed / 3600 * 85" # 85 A at rated 3600 rpm timeConstant: 0.5 - state: Stopped tag: MotorCurrent model: firstOrderLag parameters: steadyState: 0 timeConstant: 0.5Models
Section titled “Models”The v1 schema defines a fixed library of eight named models (adding one requires a schema version bump):
| Model | Parameters | Behaviour |
|---|---|---|
firstOrderLag | steadyState, timeConstant (seconds) | Tag relaxes exponentially towards steadyState with the given time constant. |
secondOrderUnderdamped | steadyState, naturalFrequency (rad/s), dampingRatio | Second-order response towards steadyState — overshoot and ringing when the damping ratio is below 1. |
integrator | gain, inputExpression | Accumulates gain × input per second — levels, totals, run-hours. |
deadTime | delay (seconds), sourceExpression | Replays the source value after a fixed transport delay. |
rateLimit | target, rate (units/second) | Tag moves towards target at a constant rate (used for valve Position towards PositionSetpoint). |
hysteresis | inputExpression, upperThreshold, lowerThreshold, outputHigh, outputLow | Latches to outputHigh when the input crosses the upper threshold, back to outputLow below the lower — deadband switching. |
saturation | inputExpression, min, max | Clamps the input expression into [min, max]. |
lookupTable | inputExpression, points (list of [x, y] pairs) | Piecewise-linear interpolation over the points — pump curves and other characteristic maps. |
Numeric parameters such as steadyState and target accept either a literal or an expression (see below), so steady states can track other tags — "32 + 28 * (Speed / 3600)" for bearing temperature, or "(Speed / 3600) * (Speed / 3600) * 4.5" for a head-curve discharge-pressure approximation.
These are deliberately simple lumped models — the shipped pump-library profiles restrict themselves to firstOrderLag and rateLimit. That fidelity is sufficient for demos, development, and training; the platform intentionally avoids a Simulink-grade physics dependency.
Expressions
Section titled “Expressions”Expressions are arithmetic over tag names and constants, evaluated every tick. They support the usual operators, a ternary, and helpers such as abs(...):
# motorised valve — limit switches and motion booleans derived from positionalgebraicRelations: - tag: OpenLimit expression: "Position >= 98 ? 1 : 0" - tag: ClosedLimit expression: "Position <= 2 ? 1 : 0" - tag: Moving expression: "abs(PositionSetpoint - Position) > 1 ? 1 : 0"algebraicRelations
Section titled “algebraicRelations”Pure, stateless derivations recomputed each tick from current tag values. Use these for engineering conversions and derived readings. The level sensor derives a scaled raw value and a tank volume:
algebraicRelations: - tag: RawValue expression: "(Level - RangeMin) / (RangeMax - RangeMin) * 100" unit: "%" - tag: Volume expression: "Level / 100 * TankCapacity" unit: "m3"behaviours
Section titled “behaviours”Event-driven effects triggered by tag transitions — for example, freezing motion booleans when a Fault rises:
behaviours: - trigger: tag: Fault condition: rising effects: - tag: Moving action: setTo value: false - tag: Opening action: setTo value: falsedisturbances
Section titled “disturbances”Adds realistic measurement noise to a tag. Today’s profiles use band-limited white noise with an amplitude:
disturbances: - tag: MotorCurrent type: white amplitude: 2.0 - tag: BearingTemp type: white amplitude: 0.3failureModes
Section titled “failureModes”Named fault definitions for testing alarms, AI classification, and operator response. Each mode lists the tags it affects and how:
failureModes: - id: bearing-degradation description: Progressive bearing wear; temperature rise. severity: warning affects: - tag: BearingTemp type: bias offset: 15 - id: sensor-stuck description: Level transmitter stuck at last good reading. severity: fault affects: - tag: Level type: stuckAt value: lastValueWhenFaultActivated - tag: Status type: setTo value: 3Affect types seen across the shipped profiles include bias (offset), stuckAt (value, often lastValueWhenFaultActivated), drift (rate), setTo (value), and dropout (probability, durationSeconds); the schema also defines a sixth, noise (amplitude), that no shipped profile uses yet. severity is informational, warning or fault (shipped profiles use only the latter two). A failure mode may target tag: "*" to affect all of the device’s tags (e.g. an intermittent comms dropout), and an optional progressTo names another failure mode in the profile that this one cascades into.
processCouplings
Section titled “processCouplings”Couples a device’s output into a site-level process, so one device’s behaviour can feed another’s inputs (a pump’s flow feeding a downstream tank’s level, for example). The profile declares the named value the device publishes; the site’s coupling graph routes it.
processCouplings: - output: FlowRate expression: FlowRate requires: [Running] publishesTo: pumpFlowOutputHow a profile is driven at runtime
Section titled “How a profile is driven at runtime”A site-scoped coordinator grain owns the simulation lifecycle and drives a timer-based tick (1 second by default). On each tick it advances every simulated device’s ramps and dynamics, evaluates algebraic relations, then overlays disturbance noise last (an emit-only overlay — derived tags are computed from the un-noised values), and finally resolves cross-device couplings at the site level. Changed tags flow through state evaluation and alarm processing identically to production — that is the whole point: the UI, alarms, and historian cannot tell a simulated reading from a real one, except by provenance.
The original design framed this as two modes, both of which reuse the same profile format:
- PLC-in-the-loop (edge) — the simulator injected IO values into the PLCnext gateway’s variables so the real controller’s logic executed against simulated inputs, with all field outputs forced to safe-off — used for commissioning and operator training against real control logic. This edge-injection shape is being superseded by IO-module-level simulation switching inside the controller scan. See PLCnext border gateway and PLC communication.
- Full server simulation — runs entirely in the server Orleans cluster with no edge or PLC required. Used for demos, development, and offline work.
These collapse into one model: simulation is a per-device activity inside whatever deployment topology the device sits in, so mixed-state operation — some devices real, some simulated — is a natural consequence rather than a special case.
Reaching the historian
Section titled “Reaching the historian”Simulated tag changes are persisted through the normal historian path and tagged with a provenance marker (source_class = simulated) so that simulated samples are distinguishable from live field data downstream. Queries and dashboards can therefore include or exclude simulated history explicitly.
Next steps
Section titled “Next steps”- Creating device types — where
simulation.yamllives in the type bundle. - Tags —
physicalRole,dataType, andaccess, which decide what a profile may write. - Engineering model — controller → module → channel → tag, the structure simulated tags belong to.