Simulator & Simulation Life Cycle

Overview

The Simulator is the central object that drives a pySimBlocks simulation. It takes a Model and a SimulationConfig, and executes the block diagram step by step until the end time is reached.

A simulation goes through three successive stages:

Simulation lifecycle

sim = Simulator(model, sim_cfg)  # triggers _compile()
logs = sim.run()                 # triggers initialize() then step loop

Compilation phase

_compile() is called automatically when the Simulator is instantiated. It prepares everything needed to run the simulation loop and raises immediately if the configuration is invalid.

Three things happen in sequence:

1. Execution order. The block graph is sorted topologically to determine in which order output_update() is called each step. See Execution Order and Algebraic Loops for details.

2. Task grouping. Blocks are grouped by effective sample time into Task objects. Each task owns an ordered subset of the global execution order and tracks its own activation schedule. Blocks with no explicit sample_time inherit the global dt.

3. Time manager. A FixedStepTimeManager is created and validates that all block sample times are integer multiples of dt. If any is not, an error is raised before the simulation starts.

Note

Only "fixed" step is currently supported. Variable-step raises NotImplementedError.

Initialization phase

initialize() is called once before the step loop starts, either automatically by run() or manually when using an external clock.

It iterates over all blocks in execution order, calls block.initialize(t0), then immediately propagates the block’s outputs to its downstream inputs. This ensures that every block enters the first step with consistent input values.

Once all blocks are initialized, each task refreshes its list of stateful blocks — those whose state_update() must be called each step.

Note

If any block raises during initialize(), the error is re-raised immediately with the block name, and the simulation does not start.

Simulation step

Each call to step() executes three phases in strict order, applied only to the tasks active at the current time t.

Simulation step phases

Phase 1: Output update & propagation

For each active task, blocks call output_update(t, dt) in topological order. Each block computes its output y[k] — from x[k] if it has state, from u[k] if it has direct-feedthrough, or from both. Then its outputs are immediately propagated to the inputs of downstream blocks.

The propagation happens block by block, not after all blocks have run. This ensures that each block receives up-to-date inputs before its own output_update() is called.

The topological order guarantees that for any block with direct-feedthrough, its upstream blocks have already computed their outputs at the same step. See Execution Order and Algebraic Loops for how this order is built and what happens when a cycle is detected.

Phase 2 — State update

For blocks that have internal state, state_update(t, dt) computes x[k+1] from x[k] and u[k] and stores it in next_state. Stateless blocks (e.g. Gain, Sum) skip this phase entirely.

Phase 3 — Commit

For blocks that have internal state, next_state is copied into state: x[k] x[k+1]. The simulation clock then advances: t t + dt.

After commit, the step is complete and the scheduler determines which tasks are active at the next tick.

Multi-rate scheduling

A pySimBlocks model can mix blocks running at different sample times. A controller might run at 10 ms while a slower observer runs at 50 ms, both coexisting in the same diagram.

Tasks and sample times

During compilation, blocks are grouped by effective sample time into Task objects. Each task owns the subset of blocks sharing its sample time and tracks its own activation schedule independently.

Blocks with no explicit sample_time inherit the global dt from SimulationConfig. All sample times must be integer multiples of dt — this is validated at compile time.

Scheduler activation

At each tick, the Scheduler checks which tasks are due to run. A task runs if t >= next_activation. After execution, its next_activation advances by one period.

For a model with dt=10ms, a fast task at 10 ms and a slow task at 50 ms:

t (ms)

fast task

slow task

0

runs

runs

10

runs

20

runs

30

runs

40

runs

50

runs

runs

Blocks in inactive tasks hold their last output — they are neither updated nor propagated until their next activation.