Skip to content

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 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 physicalRole is Input — plus any simulator-only Internal tags 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, AutoMode and 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]

The header identifies the profile and pins it to a device-type version range:

schemaVersion: 1
deviceType: consystence.pump.centrifugal
deviceTypeVersion: ">=1.0.0"
KeyMeaning
schemaVersionProfile schema version (currently 1).
deviceTypeThe dot-id of the type this profile drives (must match the manifest).
deviceTypeVersionSemver 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.

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: false

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.5

The v1 schema defines a fixed library of eight named models (adding one requires a schema version bump):

ModelParametersBehaviour
firstOrderLagsteadyState, timeConstant (seconds)Tag relaxes exponentially towards steadyState with the given time constant.
secondOrderUnderdampedsteadyState, naturalFrequency (rad/s), dampingRatioSecond-order response towards steadyState — overshoot and ringing when the damping ratio is below 1.
integratorgain, inputExpressionAccumulates gain × input per second — levels, totals, run-hours.
deadTimedelay (seconds), sourceExpressionReplays the source value after a fixed transport delay.
rateLimittarget, rate (units/second)Tag moves towards target at a constant rate (used for valve Position towards PositionSetpoint).
hysteresisinputExpression, upperThreshold, lowerThreshold, outputHigh, outputLowLatches to outputHigh when the input crosses the upper threshold, back to outputLow below the lower — deadband switching.
saturationinputExpression, min, maxClamps the input expression into [min, max].
lookupTableinputExpression, 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 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 position
algebraicRelations:
- tag: OpenLimit
expression: "Position >= 98 ? 1 : 0"
- tag: ClosedLimit
expression: "Position <= 2 ? 1 : 0"
- tag: Moving
expression: "abs(PositionSetpoint - Position) > 1 ? 1 : 0"

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"

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: false

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.3

Named fault definitions for testing alarms, AI explanations, 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: 3

Affect 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.

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: pumpFlowOutput

A site-scoped simulation coordinator 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 exactly as live readings do — 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. Both exist today:

  • PLC-in-the-loop (edge) — the simulator writes simulated input values into a controller’s input variables through the edge, so the controller’s own logic runs against simulated inputs. The controller’s outputs are driven by that logic, as they are in normal running, so this mode is for bench and virtual controllers that are not connected to plant. It is off by default on every edge and limited to the controllers that edge’s configuration allows. Starting it is a plant write, so only a site where Consystence is the operator surface admits it, and a display-only site refuses it. Simulation switching inside the Standard Library IO module blocks, within the controller scan, is the planned successor (roadmap). See PLCnext border gateway and controller 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.

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.

  • Creating device types — where simulation.yaml lives in the type bundle.
  • Tags — physicalRole, dataType, and access, which decide what a profile may write.
  • Engineering model — controller → module → channel → tag, the structure simulated tags belong to.