Skip to content

Scenario files

A scenario is a YAML file (or a Python dictionary with the same content) that describes a plant and its timeline. This guide goes through its parts; the scenario schema lists every key.

seed: 1
duration: 1h
dt: 1s
library: process@1

units:
  - {id: FIC-101, template: flow_loop, sp: 20, span: [0, 40]}
  - {id: TIC-102, template: heat_exchanger, sp: 70}

exogenous:
  - {target: TIC-102.flow, from: FIC-101.cv}     # the exchanger's flow is the loop's true flow
  - target: TIC-102.inlet_temp
    unit: degC
    source: {kind: ou, mean: 25, std: 1.5, tau: 30min}
    measured: {tag: TT-100, sensor: thermocouple, span: [0, 100]}

output:
  every: 10s

Time

duration is the length of the run and dt the base step; duration must be a whole multiple of dt. Durations are seconds (90) or strings: 250ms, 30s, 10min, 1.5h, 2d, 3d2h. Parameters are always given in physical units (a time constant in seconds, not in steps), and every operator discretizes its equations exactly, so changing dt never requires retuning.

Choose dt for the fastest dynamics in the plant. Flow loops, with time constants of a few seconds, need dt: 1s; a plant of temperature loops runs well at dt: 20s. Slower parts of a plant can update less often: see Multi-rate plants.

Units and templates

library loads a domain library; process@1 pins its major version, and a mismatch is an error. Each entry of units is an instance of one of the library's templates, with an id and any of the template's parameters:

units:
  - {id: TIC-101, template: temperature_loop, sp: 80, Kc: 3, Ti: 10min, sensor: thermocouple, span: [0, 150]}

The id sets the ISA tags: a loop TIC-101 has the transmitter reading TT-101, the valve TV-101, and the controller signals TIC-101.sp, TIC-101.mode, TIC-101.man_out and TIC-101.out. Parameters of the operators inside a unit are paths such as TIC-101.plant.K. The process library lists every template with its parameters and ports.

Loops take tuning: simc to compute the controller from the plant model instead of Kc and Ti, with detune above 1 for a slower, more robust controller.

Ports and wiring

A template's inputs from outside are its ports, for example a temperature loop's feed_temp. An exogenous entry drives a port in one of two ways:

  • source: a new signal from a source. ou is an Ornstein–Uhlenbeck process given by its mean, stationary standard deviation std and correlation time tau; ar is an autoregressive process; constant is a constant. A port whose description says it is a deviation (an upstream pressure deviation) takes mean: 0; one that is an absolute value (a feed temperature) takes its normal level.
  • from: an existing signal, which connects units. In the example above, the heat exchanger's flow is the flow loop's true flow FIC-101.cv.

A port that nothing drives is a schedule at its default value, so regimes and interventions can change it, for example a production rate.

An exogenous entry with a source whose target is not a port creates a new signal with that name, which other entries and custom operators can then use. from always feeds a port.

Measuring a signal

A template measures its own regulated variable. To measure anything else, add measured to an exogenous entry. It builds an instrument from a sensor preset: a lag, a bias, noise, quantization and, for analyzers and labs, sampling and a reporting delay. Noise and quantization are percentages of the span, so choose a span around the expected values.

exogenous:
  - target: TIC-101.feed_temp
    source: {kind: ou, mean: 25, std: 2, tau: 6h}
    measured: {tag: TT-100, sensor: thermocouple, span: [0, 100]}

The historian records every measured tag.

Custom operators

custom adds core operators directly, with their parameters as keys. The operator reference lists them. This ratio station makes a second flow loop follow the first:

custom:
  - {id: FFC-102.ratio, op: schedule, value: 0.25, unit: "1"}
  - {id: FFC-102.out, op: product, inputs: {in0: FT-101, in1: FFC-102.ratio}, unit: m3/h}

exogenous:
  - {target: FIC-102.rsp, from: FFC-102.out}   # FIC-102 follows it in mode CAS

The operator's output signal has the operator's id. A schedule holds a value that interventions and regimes can change.

Notes

Any entry of units, exogenous, interventions, custom, degradation, plan.production and plan.maintenance may carry a note: why it is there, in words. A note is kept with the scenario (also in JSON, where a comment is lost) and shown by homeostat explain.

The initial state

init sets where the run starts:

  • steady (the default): the plant is stepped with noise off, sources at their means and ageing held, until every state settles; then stochastic sources start from their stationary distributions. There is no start-up transient. The plant is settled at the sources' means, so a large random source can leave a fast loop slightly off its setpoint in the very first rows (a flow loop under a 0.3 bar pressure disturbance starts about 1.8 m3/h away): the source's first value is a draw.
  • none: every operator starts from its initial state, which for most states is zero. A temperature loop's plant starts at its base temperature (10 degC), its thermocouple reads 0 for about a lag time constant, and its controller, seeing a cold plant, opens the valve fully until the temperature arrives. The values are the operators' own initial states, not something you choose; to start somewhere you choose, see below.
  • {burn_in: 2h}: the plant runs with noise for that long before t = 0, and that part is discarded.

If a plant cannot settle, for example because a loop saturates at its initial setpoint, the error names the states that are still moving.

Starting away from the setpoint

The default start settles the plant at its starting setpoint. To begin somewhere else (a cold plant, a tank at 20 %), start settled there and change the setpoint at 0 s:

seed: 11
duration: 12h
dt: 10s
library: process@1
units:
  - {id: TIC-101, template: temperature_loop, sp: 25}
interventions:
  - {at: 0s, target: TIC-101.sp, value: 80, note: "the start-up: the plant begins settled at 25 degC and the setpoint is raised at once"}
output: {every: 1min}

The plant starts at 25 degC with its thermocouple reading 25, the controller opens the valve fully and the temperature climbs to 80 in about 15 minutes. Unlike init: none, the instruments and the controller start consistent with the plant, so the data has no artificial zeros. With plan and regimes, the same is a production transition at 0 s with over: 0s.

Output

output.every records every n-th step (a multiple of dt). output.start sets the calendar time of t = 0, and the run's index becomes timestamps:

output: {every: 1min, start: 2026-01-05T06:00:00}

Loading from Python

simulate() and prepare() accept a file path, YAML text, a dictionary or a validated Scenario:

import homeostat

run = homeostat.simulate("plant.yaml")
prepared = homeostat.prepare({"duration": "1h", "library": "process", "units": [{"id": "FIC-101", "template": "flow_loop"}]})
prepared.compiled.loops      # the loops found in the compiled graph
run = prepared.run()

prepare() validates and compiles without running. homeostat.catalog() describes every operator, template, preset, fault, degradation and task as data, for tools and AI assistants.