Metadata-Version: 2.5
Name: homeostat
Version: 0.1.0a1
Summary: Process-industry time series with full ground truth, generated from abstract data generating processes.
Project-URL: Homepage, https://homeostat.kausalflow.com
Project-URL: Documentation, https://homeostat.kausalflow.com/docs/
Author: Lei Ma
License-Expression: Apache-2.0
License-File: LICENSE
License-File: NOTICE
Keywords: control loops,fault detection,ground truth,process industry,simulation,soft sensors,synthetic data,time series
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering
Classifier: Topic :: Scientific/Engineering :: Information Analysis
Requires-Python: >=3.11
Requires-Dist: numpy>=2.0
Requires-Dist: pandas>=2.2
Requires-Dist: pydantic>=2.7
Requires-Dist: pyyaml>=6.0
Provides-Extra: fast
Requires-Dist: numba>=0.61; extra == 'fast'
Description-Content-Type: text/markdown

# 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](https://homeostat.kausalflow.com/docs/).

## Install

```bash
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:

```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:

```python
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.

```bash
uv run python examples/disturbance_rejection.py
```

`examples/plant_history.yaml` generates two months of history from a plan:

```yaml
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):

```yaml
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.

```yaml
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:

```python
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:

```yaml
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}}
```

```python
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:

```python
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.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](https://homeostat.kausalflow.com/docs/tutorials/reactor-mixer-plant/) 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](https://homeostat.kausalflow.com/docs/):

- [Getting started](https://homeostat.kausalflow.com/docs/getting-started/first-scenario/), [tutorials](https://homeostat.kausalflow.com/docs/tutorials/), [guides](https://homeostat.kausalflow.com/docs/guides/) and [concepts](https://homeostat.kausalflow.com/docs/concepts/).
- [Reference](https://homeostat.kausalflow.com/docs/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](https://zensical.org) (`uv run zensical serve` previews it).

## Development

```bash
uv run pytest
```

Integration tests that take minutes, such as the tutorial's four-day run, are marked `slow` and skipped by default:

```bash
uv run pytest -m slow
```
