Self-running plans¶
A long history needs many changes. Instead of listing each one, a scenario can describe how changes happen: which grade follows which, how long campaigns last, how often maintenance recurs, how often faults strike. When the scenario is loaded, each description is expanded into an explicit plan, and every value it draws is recorded.
This is examples/self_running_plant.yaml, six months of a temperature loop:
# Six months of a plant that runs itself: nobody lists the grade changes, the maintenance or
# the faults. The production plan is a random succession of grades, cleaning and calibration
# recur with jitter, and faults arrive at random. Drawing the plan (homeostat.prepare) turns
# all of it into explicit, recorded events; another seed or plant index gives another history.
seed: 13
duration: 180d
dt: 60s
library: process@1
units:
- {id: TIC-101, template: temperature_loop}
exogenous:
- target: TIC-101.feed_temp
unit: degC
source: {kind: ou, mean: 25, std: 2, tau: 12h}
regimes:
grade_A: {TIC-101.sp: 80}
grade_B: {TIC-101.sp: 88}
grade_C: {TIC-101.sp: 75}
degradation:
- {target: TIC-101, kind: fouling, rate: 0.008}
- {target: TIC-101, kind: sensor_drift, rate: 0.02}
- {target: TIC-101, kind: valve_wear, rate: 0.03}
plan:
start: grade_A
production:
next: {grade_A: {grade_B: 0.6, grade_C: 0.4}, grade_B: {grade_A: 0.8, grade_C: 0.2}, grade_C: {grade_A: 1}}
campaign: {lognormal: {median: 9d, sigma: 0.4}}
over: 6h
maintenance:
- {task: clean, target: TIC-101, every: 25d, jitter: {normal: {mean: 0, sd: 3d}}}
- {task: calibrate, target: TIC-101, every: 45d, jitter: {uniform: [-2d, 2d]}}
- {task: replace_valve, target: TIC-101, every: 120d}
faults:
- target: TIC-101
every: 40d
kinds: {sensor_bias: 0.4, sensor_noise: 0.3, valve_stiction: 0.3}
magnitude: {sensor_bias: {normal: {mean: 0, sd: 1.5}}, valve_stiction: {uniform: [1, 4]}}
after: 3d
output:
every: 5min
Production¶
plan.production may be a description instead of a list:
plan:
start: grade_A
production:
cycle: [grade_A, grade_B, grade_C] # repeat in this order
campaign: {lognormal: {median: 8d, sigma: 0.3}} # length of each campaign, drawn each time
over: 6h # ramp of each transition
cyclerepeats the regimes in order. Ornextgives transition probabilities from each regime, for random successions:next: {grade_A: {grade_B: 0.7, grade_C: 0.3}, grade_B: {grade_A: 1}, grade_C: {grade_A: 1}}.campaignis the length of each campaign: a duration, a distribution drawn for each campaign, or a mapping from regime to either.overis the ramp of every transition.
Campaigns must last a positive time, and weights must be positive and finite; a generator that cannot make a plan is an error (E_BAD_GENERATOR) that names the key.
Recurring maintenance¶
An entry of plan.maintenance with every recurs:
plan:
maintenance:
- {task: clean, target: TIC-101, every: 20d, first: 10d, jitter: {normal: {mean: 0, sd: 2d}}}
first is the first occurrence (default: every), and jitter shifts each occurrence by a drawn amount. Entries without every stay as they are.
Random faults¶
A top-level faults section describes faults as Poisson processes:
faults:
- target: [TIC-101, FIC-201] # one of these units is chosen for each fault
every: 30d # mean time between faults
kinds: {sensor_bias: 0.5, valve_stiction: 0.3, sensor_noise: 0.2}
magnitude: {sensor_bias: {normal: {mean: 0, sd: 1.5}}} # optional, per kind
after: 1d # optional: no faults before
Each arrival becomes an entry of interventions, with its kind, unit and magnitude drawn.
What gets recorded¶
Expansion happens when a plant is drawn, before validation, so the rest of Homeostat only sees an ordinary scenario with explicit transitions, tasks and faults. homeostat.prepare(config).scenario shows the expanded plan.
Every generated value comes from a random stream keyed by its address in the config and the plant, for example plan/production/campaign/3 or faults/0/2/kind, and is recorded in run.meta["trace"] with its distribution and log-probability. The same config, seed and plant always give the same plan, and changing one generator never changes another's draws.
homeostat.prepare(config, plant=5) draws plant 5; a plant family draws many.
Feasibility¶
A generated plan can ask for something a plant cannot do, for example a setpoint its valve cannot reach. homeostat.labelers.feasibility(run) reports, per lane and loop, whether each loop starts at its setpoint without a saturated output, and a family records the result in Family.lanes["feasible"].