Skip to content

Twin runs and lanes

A run can hold several lanes: copies of the plant that step together as one batch. Lanes make experiments cheap and exact.

Lanes and noise keys

lanes: 2
noise_keys: [0, 0]   # both lanes get exactly the same noise

Each lane draws its noise from streams keyed by its noise key; lanes with the same key share their noise. By default every lane has its own key.

Twin experiments

With shared noise, the difference between two lanes is caused only by what differs between them. examples/disturbance_rejection.yaml runs a temperature loop twice on the same feed-temperature disturbance, in automatic on lane 0 and in manual on lane 1:

# A feed-temperature disturbance is suppressed on the controlled temperature and
# shows up on the valve position instead.
#
# Two lanes share the same noise (a twin run). In lane 0 the controller stays in
# AUTO; in lane 1 it switches to MAN at t = 0 and holds its output. The difference
# between the lanes is the pure effect of the control loop.
seed: 42
duration: 1d
dt: 1s
lanes: 2
noise_keys: [0, 0]
library: process@1

units:
  - {id: TIC-101, template: temperature_loop, sp: 80}

exogenous:
  - target: TIC-101.feed_temp
    unit: degC
    source: {kind: ou, mean: 25, std: 3, tau: 2h}

interventions:
  - {at: 0, target: TIC-101.mode, value: MAN, lanes: [1]}

output:
  every: 10s
import homeostat

run = homeostat.simulate("examples/disturbance_rejection.yaml")
closed, opened = run.lane(0).truth, run.lane(1).truth

In manual, the valve holds and the disturbance passes to the temperature. In automatic, the temperature stays near its setpoint and the disturbance moves to the valve. The disturbance itself is identical in both lanes.

Changes and faults take lanes to apply to some lanes only:

interventions:
  - {at: 1h, target: TIC-101.mode, value: MAN, lanes: [1]}

Fault visibility uses the same idea: one twin lane per fault.

Many histories

Lanes with different noise keys are independent histories of the same plant, run for little more than the cost of one:

lanes: 8

run.truth then has (lane, signal) columns, and run.lane(k) gives lane k as a single-lane run with its own event log and noise key. Plant families use lanes for different plants as well.

Forking a running engine

In Python, an engine can be copied at any point and each copy continued differently:

from homeostat import Event

prepared = homeostat.prepare("scenario.yaml")
engine = prepared.engine()
engine.initialize("steady")
engine.run(3600)                      # one hour, shared by both branches

twin = engine.fork()
twin.schedule(Event(at=3600, target="TIC-101.sp", value=85))
engine.run(7200)
twin.run(7200)

Both copies continue on the same random streams, so they differ only by the change.