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