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

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.
  • TagsphysicalRole, dataType, and access, which decide what a profile may write.
  • Engineering model — controller → module → channel → tag, the structure simulated tags belong to.