Skip to main content
Pre-release

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.0a2). 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.

For AI assistants

Homeostat ships a homeostat command, so an assistant with a shell can write, check and read scenarios without a Python session and without the network: homeostat describe (what exists, with parameters), homeostat examples, homeostat validate scenario.yaml (every problem at once, each with a fix; warnings for things that are valid but change nothing, such as a misspelt port), homeostat explain scenario.yaml (what plant you described, in plain language), homeostat diagnose scenario.yaml (checks the scenario's expect: section: what the run should show, with the numbers behind each verdict), homeostat diff a.yaml b.yaml (what an edit changed), homeostat run scenario.yaml --out out/ (a summary and the data), homeostat schema (a JSON Schema with every template's parameters), homeostat eval (nine "write a scenario that does X" tasks that measure how well an assistant does all this), homeostat skill --install (a Claude Code skill for the workflow) and homeostat guide (the documentation, bundled in the package). The commands that report take --json. Scenarios may be YAML or JSON. See Working with AI assistants; llms.txt indexes the site.

Concepts

  • Core and plugins. homeostat.core is a pure math layer that knows nothing about hardware. Domain libraries such as process (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:

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.0a2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for homeostat 0.1.0a2
File Size Uploaded Provenance
homeostat-0.1.0a2.tar.gz 245.0 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for homeostat 0.1.0a2
File Interpreter ABI Platform
homeostat-0.1.0a2-py3-none-any.whl Python 3 none any Details

Total release size: 484.8 kB

Release files / homeostat-0.1.0a2.tar.gz

Download URL homeostat-0.1.0a2.tar.gz
Size 245.0 kB
Tags Source
SHA-256 checksum
How to use checksums
0d8ed5f3d79e092a70e10a42f791fe95afa7d95d03072c830b5179385be580fb
BLAKE2b-256 checksum
How to use checksums
8c07ceaf0387102a40bfa69c963f472ed946066dd1a905fe354f46ceaf5220ca
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 Oct 2, 2026.

Transparency log

Release files / homeostat-0.1.0a2-py3-none-any.whl

Download URL homeostat-0.1.0a2-py3-none-any.whl
Size 239.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
af2bc50f5414b9d68a415cab268c70d3e8398cbda57801d07ff0c5ba0fdc9649
BLAKE2b-256 checksum
How to use checksums
aa592f619370b50f909cfa45a9052634faffc0aad96207980f2a8b7616adee5b
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 Oct 2, 2026.

Transparency log

Security for homeostat 0.1.0a2

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.

Select from 2 files

Attestation for homeostat-0.1.0a2.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 Oct 2, 2026.

Transparency log

A 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.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page