The step¶
Homeostat runs on a global clock with a fixed base step dt, and a batch dimension of lanes: every array holds one value per lane, and one step advances every lane.
What happens in a step¶
At each time t:
- Apply the events due at t.
- Compute every operator's outputs, in the order of the same-step dependencies.
- Record the values.
- Update every operator's state from the same inputs.
A controller's output at t therefore drives the process state at t+1: sampled-data control with a zero-order hold, as in a real control system. A real scan period or transport delay is modelled explicitly, with a controller scan or a delay operator.
The steady start¶
By default (init: steady), a run starts from a steady state rather than from arbitrary initial values:
- noise is off, and stochastic sources sit at their means;
- quantization is passed through, so the plant cannot hunt between two quantization levels;
- ageing states, such as fouling, are held at their initial values;
- the plant is stepped at t = 0 until every state stops changing, compared over windows long enough for every operator to take a turn;
- stochastic sources then start from their stationary distributions.
The run begins at the starting regime's setpoints with no start-up transient. init: none starts from the operators' initial states instead, and init: {burn_in: 2h} runs with noise for that long before t = 0 and discards it.
If the plant cannot settle, for example because a loop saturates at its initial setpoint or an integrating state is unbalanced, the error names the states that are still moving.
Multi-rate stepping¶
An operator can update less often than the base step: with a period of n steps it is discretized for its own step, updates only on its turn, holds its outputs in between, and draws random numbers only on its turn. Before its first turn (with a phase) it shows outputs from its initial state. See Multi-rate plants.
The compiled step¶
The steps between two events are a pure function of the state, so Homeostat can compile them. When Numba is installed (homeostat[fast]), the engine generates one function for the whole graph from each operator's scalar kernel, compiles it, and runs every stretch of steps between events with it; the events themselves are applied as before. The compiled step computes exactly what the Python step computes, with the same random numbers and the same order of operations, only much faster. Where numpy uses the system's math library for exp and log (for example on Apple silicon), the data are identical to the last bit; on CPUs with AVX-512, numpy uses its own vectorized versions, and the two steps agree to rounding (as the Python step alone already differs in the last bits between such CPUs): a plant of 140 operators at a 1 s step runs a simulated day in about 35 ms instead of a minute.
A graph compiles once per machine: the generated code and its machine code are kept in a cache (~/.cache/homeostat, or HOMEOSTAT_CACHE). Compiling a large plant for the first time takes a few seconds. Engine(..., compiled_step=False) or HOMEOSTAT_COMPILED_STEP=0 keeps the Python step; run.meta["reproducibility"]["step"] records which one ran.
A resumable engine¶
The engine can be advanced in pieces: run(until) steps to a time, schedule(event) adds events, and the run continues from where it stopped. result(clear=True) hands over what was recorded so far, so a long run can be collected piece by piece; snapshot() saves the engine and Engine.restore() continues it later, exactly. fork() copies the engine, random streams included, so two copies can continue differently from exactly the same state. Planners that react to the simulated state are built on this: they read the state, decide, schedule events and continue.