This release is a pre-release and may not be stable for production use.
homeostat
Homeostat generates process-industry time series with full ground truth. Instead of simulating physics, it composes abstract data generating processes: stochastic sources, dynamic operators, control loops, instrument models and timed events. Because the control loops are closed, a disturbance or fault shows up where it does in a real plant: often not on the controlled variable, but on the valve.
Every run returns the data a historian would see, the true values behind it, and the ground truth: the causal graph, each signal's role, the control loops, and a log of every event.
Status: an alpha of v0.1 (0.1.0a1). Named after W. Ross Ashby's 1948 Homeostat. Documentation: homeostat.kausalflow.com/docs.
Install
pip install --pre "homeostat[fast]"
The fast extra adds Numba: the engine then compiles each plant into one function and runs long histories hundreds of times faster, with the same results. Without it, Homeostat needs only NumPy, pandas, Pydantic and PyYAML. --pre is needed while the releases are alphas.
To work on the source, clone the repository and run uv sync.
Quickstart
Describe a plant and a timeline in YAML:
seed: 42
duration: 1d
dt: 1s
library: process@1
units:
- {id: TIC-101, template: temperature_loop, sp: 80}
exogenous:
- target: TIC-101.feed_temp # an unmeasured disturbance
source: {kind: ou, mean: 25, std: 3, tau: 2h}
interventions:
- {at: 6h, target: TIC-101.sp, value: 85, profile: ramp, over: 30min}
- {at: 12h, target: TIC-101, fault: valve_stiction, magnitude: 3}
Then run it:
import homeostat
run = homeostat.simulate("scenario.yaml")
run.observed # what a historian records: TT-101, TIC-101.sp, TIC-101.out, TIC-101.mode
run.truth # true values of every signal, including the unmeasured disturbance
run.measured # instrument outputs, which the controllers read
run.meta # signal table, loops and roles, causal graph, event log, reproducibility
Errors say what is wrong, where, and how to fix it:
[E_TARGET_REGULATED] interventions[0]: 'TIC-101.cv' is the regulated variable of loop TIC-101; nobody can set it directly. Fix: Change its reference instead: target 'TIC-101.sp'.
homeostat.catalog() lists every operator, template, sensor preset and fault alias.
Example
examples/disturbance_rejection.yaml runs a twin experiment: two lanes share the same noise, one with the controller in automatic and one in manual. The difference between them is the pure effect of the control loop.
uv run python examples/disturbance_rejection.py
examples/plant_history.yaml generates two months of history from a plan:
regimes:
grade_A: {TIC-101.sp: 80, TIC-101.plant.K: 0.8}
grade_B: {TIC-101.sp: 88, TIC-101.plant.K: 0.9}
degradation:
- {target: TIC-101, kind: fouling, rate: 0.01}
- {target: TIC-101, kind: sensor_drift, rate: 0.03}
plan:
start: grade_A
production:
- {at: 10d, to: grade_B, over: 6h}
maintenance:
- {at: 20d, task: clean, target: TIC-101}
- {at: 30d, task: calibrate, target: TIC-101}
- {at: 40d, task: stop, target: TIC-101, duration: 2d}
Degradations grow with operation (fouling faster at higher temperatures), maintenance resets them, and every change lands in run.meta["events"] and run.meta["timeline"].
A plan can also run itself. Instead of listing every change, describe how changes happen; each drawn plant gets its own explicit, recorded plan (examples/self_running_plant.yaml, six months in about 30 s):
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}}}
faults:
- {target: TIC-101, every: 40d, kinds: {sensor_bias: 0.4, sensor_noise: 0.3, valve_stiction: 0.3}}
Process units and product quality
Besides flow, level and temperature loops, the process library has a mixer, a heat_exchanger and a reactor. Units connect through their ports, and material takes time to travel: transit and residence times follow the flow. The reactor models the product property the way quality-prediction work sees it: the true property depends on the temperature, the feed ratio and the catalyst's activity, and the lab reports it every 4 hours, an hour late.
units:
- {id: MX-101, template: mixer}
- {id: TIC-301, template: reactor}
exogenous:
- {target: TIC-301.flow, from: MX-101.flow} # the reactor is fed by the mixer
- {target: TIC-301.ratio, from: MX-101.out} # and the mixture sets its feed ratio
degradation:
- {target: TIC-301, kind: catalyst_decay, rate: 0.02}
See examples/product_quality.yaml: the true property is run.truth["TIC-301.q_product"] and the lab reading is run.measured["AT-301"].
A lab result describes material that passed through the plant at earlier, flow-dependent times. homeostat.labelers.lineage gives that alignment exactly:
from homeostat.labelers import lineage
lin = lineage("examples/product_quality.yaml", sample="AT-301")
lin.table # per lab result: when it was reported and sampled, when its material entered MX-101 and TIC-301, and the residence times
Plant families
Any number or choice in a config can be a distribution. homeostat.family() draws many plants and runs them as lanes of one batch, which costs little more than one plant:
family: {plants: 48}
units:
- id: TIC-101
template: temperature_loop
K: {lognormal: {median: 0.8, sigma: 0.2}}
theta: {uniform: [30s, 3min]}
tuning: simc # each plant's controller is tuned from its own model
detune: {lognormal: {median: 1.3, sigma: 0.4}}
fam = homeostat.family("examples/plant_family.yaml") # 48 plants x 30 days in about 15 s
fam.draws() # every drawn value, per plant and address
fam.run(7) # plant 7 as an ordinary Run
Each drawn value comes from its own random stream keyed by its address in the config, so adding a distribution never changes the others. homeostat.simulate() on a config with distributions runs one drawn plant and records the draws in run.meta["trace"].
Labels
homeostat.labelers derives ground truth that real data cannot provide:
from homeostat.labelers import settle_times, visibility
labels = visibility("scenario.yaml") # one twin lane per fault, on the same noise
labels.summary() # when each fault first becomes detectable, and on which signal
settle_times(run) # how long each loop takes to settle after each regime transition
A transmitter bias, for example, never shows on the transmitter's own reading, because the loop holds that reading at its setpoint; it shows on the controller output instead. Faults too small to detect are labelled undetectable, so a benchmark does not count them as misses.
Concepts
- Core and plugins.
homeostat.coreis a pure math layer that knows nothing about hardware. Domain libraries such asprocess(flow, level and temperature loops, ISA tags, sensor presets, fault aliases) are plugins that compile down to core operators and events. - Single writer. Every signal has exactly one writer. A regulated variable can only be changed through its setpoint; a controller output only through manual mode.
- Events. Planned changes and faults are the same thing: timed changes with an action (set, shift, scale) and a profile (step, ramp). Faults target parameters, which then become nodes of the causal graph.
- Reproducibility. The same config, seed, package version and NumPy version give identical data. Random streams are keyed by operator names, so adding an operator never changes the others' noise, and twin runs stay in sync.
Tutorial
A reactor–mixer plant with hidden conditions builds a plant step by step: feed flow loops, ratio control, a stirred reactor whose product depends on states nobody measures, a mixer, and a four-day production plan. It then shows why the product misses its plan while every controller holds its setpoint.
Documentation
homeostat.kausalflow.com/docs:
- Getting started, tutorials, guides and concepts.
- Reference: the scenario schema, operators, the process library and error codes, generated from the package's metadata.
The documentation's source is in docs/, built with Zensical (uv run zensical serve previews it).
Development
uv run pytest
Integration tests that take minutes, such as the tutorial's four-day run, are marked slow and skipped by default:
uv run pytest -m slow
Metadata
Release files for homeostat 0.1.0a1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| homeostat-0.1.0a1.tar.gz | 129.3 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| homeostat-0.1.0a1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 237.7 kB
Release files / homeostat-0.1.0a1.tar.gz
| Download URL | homeostat-0.1.0a1.tar.gz |
|---|---|
| Size | 129.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d57d2b05d71fb07ec99f92aed3cf801498782d6c3a702c36dc67c445c62a8982
|
|
BLAKE2b-256 checksum How to use checksums |
f4c88d654425eb0b04960818232331310dcd82dc213797302255ea2c4f389fd3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 30, 2026.
Transparency logRelease files / homeostat-0.1.0a1-py3-none-any.whl
| Download URL | homeostat-0.1.0a1-py3-none-any.whl |
|---|---|
| Size | 108.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
24173cd46e80d1ff9bd924dee0ed61062ca386e327f711c2940db8112f425dad
|
|
BLAKE2b-256 checksum How to use checksums |
8149dc4fd72741c8852a3d6ac5d7688cfcf53f763c58d2f04145c45b4567f2d7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 30, 2026.
Transparency logSecurity for homeostat 0.1.0a1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for homeostat-0.1.0a1.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 30, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.