Skip to content

Development

Setup

With access to the repository:

git clone https://github.com/kausalflow/homeostat
cd homeostat
uv sync

Homeostat uses Python 3.11 or newer, uv for the environment and dependencies, a src layout and a hatchling build. uv.lock pins every dependency, NumPy included, which keeps generated data reproducible.

Tests

uv run pytest               # the default suite, about a minute
uv run pytest -m slow       # integration runs that take minutes, such as the tutorial's four days

Besides unit tests, the suite checks the architecture's invariants on the source itself (the core never mentions hardware; every operator documents its equations and the units of its parameters), compares a golden run, checks that the reference pages are up to date, and validates every scenario in this documentation. The tutorial's slow test checks every number the tutorial reports.

Documentation

The documentation is built with Zensical from the Markdown files in docs/:

uv run zensical serve             # preview at http://localhost:8000, rebuilt on every change
uv run zensical build --strict    # build into site/, failing on any warning

The pages in docs/reference/ that start with a "Generated" comment, docs/reference/scenario.schema.json and docs/llms.txt are generated from the package's metadata. After changing an operator, a template, the scenario schema or an error code, regenerate them:

uv run python scripts/gen_reference.py

tests/test_docs.py fails when a generated page is out of date, and when an error code is raised without a description in the generator.

Continuous integration

Two GitHub Actions workflows run on every pull request and on main:

  • CI (.github/workflows/ci.yml) runs the test suite on Python 3.11, 3.12 and 3.13, with the dependencies from uv.lock. On main, and on demand, it also runs the slow integration tests.
  • Documentation (.github/workflows/docs.yml) checks that the generated reference is current and builds the site in strict mode. From main it publishes the site to Cloudflare Pages, at homeostat.pages.dev. Publishing needs a Pages project named homeostat and two repository secrets: CLOUDFLARE_API_TOKEN (with the Cloudflare Pages: Edit permission) and CLOUDFLARE_ACCOUNT_ID.

While the repository is private, the site does not link to it; tests/test_docs.py checks this. Once it is public, add repo_url, repo_name and edit_uri to zensical.toml and drop that rule.

Architecture rules

  • The core is pure math. homeostat/core/ never mentions hardware, in code or docstrings. Domain vocabulary belongs to a domain library.
  • Plugins add vocabulary, not engine behaviour. A domain library, a labeler or a planner uses only the public core API and compiles down to core operators and events. Domain libraries register through the homeostat.libraries entry point group.
  • Invariants are enforced at compile time, with tests: a single writer per signal, no algebraic loops, controllers that read measurements, roles derived from structure, keyed random streams with a fixed number of draws per step, exact discretization in physical units.

The design notes in the repository's design/ folder explain these rules; where the accepted decisions differ from the original design, the decisions win.

Conventions

  • Keep the core small and readable, and prefer many tiny operators over one big one. An operator's output and update are pure functions of arrays with a leading lane dimension.
  • Every public operator has a docstring with its difference equation, and declares each parameter once, with its unit, through Param; the catalog, the checks and the reference pages come from that metadata.
  • Every error is an issue with a code, a location, a reason and a suggested fix.
  • Everything is written in English: code, comments, documentation and commit messages.

Adding an operator

  1. Subclass Operator in the module of its kind (sources, dynamic, feedback or observation), with an op_name, its ports, its direct feedthrough, its random numbers per step and its parameters.
  2. Write the docstring's summary and difference equation; the reference page shows them.
  3. Discretize exactly for dt in bind or at each step, so the parameters stay in physical units.
  4. Add unit tests, then regenerate the reference pages.

Adding a template

A template is a function that adds core operators to a graph and returns an Instance: the unit's signals and components. Declare its parameters (TemplateParam) and ports (Port), register it in the library's TEMPLATES, and give faults, degradations and tasks the components they need. Templates add names, tags, units and defaults, never new math.