Sample time and loops — how the simulator paces a diagram#
A run is a sequence of cycles. In each cycle the simulator visits every block once, in a fixed order decided when the model is built; a block reads its input ports, computes, and writes its output ports, and those port values persist until the block is next visited. This page states which clock each block runs on, what happens when blocks run at different rates, and the one consequence of that cycle model every feedback diagram inherits: around every loop, one edge is read one cycle late. The stepping equations themselves are on Solver mathematics — the forms, the discretizations and the stepping scheme; floating-point limits on Numerics — what the solver will and will not do.
The rules you meet#
- Every block has a
Sampling Time (s)parameter, default-1. Zero or less means "inherit"; a positive value is the block's own period. - The solver settings live in the Model Config panel (
modelConfig/setModelConfig <property> <value>in the command window, see The command window — the command engine for a user):solverType(ContinuousorDiscrete),steppingType(explicit fixed-step RK1–RK4, implicit fixed-step BE1/TR2, explicit variable-step RK45/RK23, or implicit variable-step TRBDF2 for stiff models),globalSamplingTime(default0.1s),multiRateTolerance(default1e-9s), and thetimeBudgetValidationEnabledcheck (default off). - Fixed-step time is a grid, not a sum. The clock is
start + n × Ts, nevert += Ts, so the tenth step ofTs = 0.1lands on exactly1.0and a Step block set to switch at1.0switches on sample 10 (advanceTimeByOneStep,ICoreModelSimulator.cpp). Exported code computes its clock the same way. - The first cycle is the initial condition and the last cycle is the stop time. At
startTimeevery block reports its initial state without integrating; a finite run then solves every grid point up to and includingstopTime(when the grid lands on it, within1e-9s) and none beyond, in every solver mode.stopTime = 10atTs = 0.1gives 101 cycles,t = 0 … 10. (Since 2026-09-02; the history is on Solver mathematics — the forms, the discretizations and the stepping scheme.) - Code export needs one rate. Exporting a subsystem whose blocks do not all share the
subsystem's own rate fails with "Sampling time mismatch: block '…'" listing every offender
(
ICoreModelBuild::verifyUniformSamplingTime); see Exporting code — the ten targets, what each produces, and what verification proves.
Where a block gets its rate (assignSamplingTime, ICoreBlockSolverEnvironment.cpp)#
| Block kind | Solver | Sampling Time (s) ≤ 0 | Sampling Time (s) > 0 |
|---|---|---|---|
Discrete-only block (Unit Delay, Discrete Transfer Function, Zero-Order Hold, … — the blocks under Control_Systems/Discrete and the other blocks that declare themselves discrete) | any | globalSamplingTime | the value |
| Any other block | Continuous, fixed-step | its enclosing subsystem's rate; Home's rate is globalSamplingTime, a subsystem's is its own positive Sampling Time (s) or else its parent's, top-down | the value |
| Any other block | Continuous, variable-step | the variable step | ignored — warning "Block sampling time will be overridden at each step by the global variable-step" |
| Any other block | Discrete | globalSamplingTime | ignored — warning "Block sampling time is overridden by the global sampling time" |
A non-numeric value stops the run: "Sampling time must be a positive double".
An atomic subsystem imposes its rate (since 2026-09-30). A Subsystem whose Treat as
Atomic Unit is On is Simulink's TreatAsAtomicUnit, and its positive Sampling Time (s) is
Simulink's SystemSampleTime:
- every block inside it at
≤ 0runs at that rate, under either solver — discrete-only blocks included, which inside a plain subsystem takeglobalSamplingTimeinstead, and under theDiscretesolver the other blocks too, which that solver otherwise holds toglobalSamplingTimewhatever they are set to. An atomic subsystem at-1inherits from the nearest atomic one above it, and a plain one is passed through; - a block inside at any other positive period is refused, a multiple included, as Simulink
R2026a refuses it (
InvBlkInPeriodicAtomic); so is a block with continuous states (InvBlkWithNoSTPrmInPeriodicAtomic). Both are checked once block configs are loaded, before the first step, and each offender is named with its subsystem. - a Model block (a Subsystem whose Reference Type is Model) is atomic whatever its
Treat as Atomic Unit says, and its Sampling Time (s) is its own rate. Under a fixed-step
solver that rate must be a whole multiple of the step: 2 s under a 1 s step runs, 0.5 s is
refused before the first step, as Simulink refuses it (
FixedStepNEFundStep).
A counter c = z⁻¹c + 1 inside an atomic subsystem at 2, on a model stepping at 1 and logged
at 1, answers 1 1 2 2 3 3 4, as Simulink R2026a does
(solver/an_atomic_subsystem_imposes_its_sample_time_on_its_contents). The same counter in a plain
subsystem runs at 1 but does not answer Simulink's 1 2 3 … 7: its loop through the Unit
Delay reads it one sample late, the discrete loop rule below.
⚠ Simulink ignores SystemSampleTime on a plain (virtual) subsystem, but the fixed-step
Continuous row above still hands a plain subsystem's positive rate to the non-discrete blocks
inside it, as it always has. Make the subsystem atomic when the rate is meant.
Rates are assigned once, at build time, always from Home downward — even when you run or export a subsystem — so a subsystem never sees an unassigned parent.
How different rates are stepped#
Continuous solver, fixed step (initializeSamplingTimes and
ICoreSimulatorRepeatableWindow). Every distinct block rate — plus globalSamplingTime
itself — is rounded to a multiple of multiRateTolerance. The greatest common divisor of those
multiples is the fine step; their least common multiple is a window. The simulator's clock
advances one window per step, and inside a window it sub-steps at the fine step, solving each
block at every sub-step that is a whole multiple of the block's own period, in build order.
So a Gain at 0.15 s next to Home's 0.1 s runs on a 0.05 s fine step inside a 0.3 s
window: the Gain is solved at sub-steps 0 and 3, the rest at 0, 2, 4. A rate does not have to
divide the global one; it only has to be a clean multiple of the tolerance.
Discrete solver. Every block is solved every globalSamplingTime, whatever its own
Sampling Time (s) says (measured below: a Unit Delay set to 0.2 s advanced at 0.1 s). The
block's own value is stored, and only the optional time-budget check reads it: with
timeBudgetValidationEnabled on, the first step whose length differs from the block's period
stops the run with "Invalid time budget detected at block: …".
Variable step. Continuous blocks are solved every step, at the step the integrator picks.
A discrete-only block keeps its own period: the step is shortened to land exactly on the
block's next sample hit (start + k × Ts), it is solved there and nowhere else, and its
ports hold between hits — the same behaviour as under the fixed-step window, on an irregular
clock (calculateNextStepTimeDelta, fireGlobalSolve; since 2026-09-02, before which every
block was stepped at the integrator's step). A block that is not discrete-only but sets a
positive Sampling Time (s) is still stepped every variable step, with the warning in the
table. The step is also shortened to land on every discontinuity a source announces in advance
— a Step's step time, a pulse edge, a table breakpoint — with a short approach step just before
it, so the jump is not smeared across a whole step; signal-dependent switches (Saturation,
Relay) are not located inside a step. Details on Solver mathematics — the forms, the discretizations and the stepping scheme.
Loops: where a feedback loop opens#
Every feedback loop is opened at a block with no direct feedthrough — a block whose output
at an instant does not depend on its input at that same instant: an Integrator, a strictly
proper Transfer Function, State Space or Zero-Pole, a Unit Delay, Memory, Delay or Transport
Delay, an Algebraic Constraint. The build orders the diagram so that every block runs after
everything it reads, and around a loop it drops the one constraint it may: the edge leaving
such a block (ICoreModelBuild::assignSolverOrders, since 2026-09-03; before that the loop
opened wherever a walk from the sinks happened to close, which depended on the order the
blocks were created in). What that opening costs depends on the block:
- A loop through continuous blocks under the
Continuoussolver's defaultJointcoupling has no delay at all, whatever the diagram's shape. Every continuous state is integrated together, and at every stage the state blocks that feed through nothing write their outputs from the trial state before anything reads them.Integrator (x₀ = 1) → Gain −1 → IntegratorunderRK4at0.1s givesx(1) = 0.3678798(e⁻¹to3.3e-7) with the Gain created first, and with a Scope on the diagram — both used to come out0.3584859,9.4e-3off, because the order put the Gain first and it read the Integrator one stage stale (solversuite,joint_coupling_loop_is_exact_whatever_the_solve_order). See Solver mathematics — the forms, the discretizations and the stepping scheme. - A loop closed through a discrete delay block is one sample late on that block's output
edge. In the commit pass a Unit Delay runs after its sources and its consumer has already
read its port, so the consumer sees the value it wrote in the previous cycle. With
y = 0.5 (u − z),z = y[k−1], Simulink computesy[k] = 0.5 (u[k] − y[k−1]); ICore computesy[k] = 0.5 (u[k] − y[k−2]), so every value is held for two samples and the loop settles at half tempo. The plot below is that loop in both tools. The same holds for thePer-blockcoupling and theDiscretesolver, where every block is a single visit per cycle. - Comparing such a diagram against Simulink shows an O(1) residual from the first sample after the excitation, and it is this behaviour, not a bug — no tolerance absorbs it. The same diagram exported to code reproduces ICore's own trajectory to rounding, so export verification is the check that applies to loops (see Exporting code — the ten targets, what each produces, and what verification proves).
- A loop with no such block is an algebraic loop, and the run refuses to start.
u → Subtract → Gain 0.5 → back to Subtractstops withAlgebraic loop detected: ICore Blocks/Home/Subtract -> ICore Blocks/Home/Gain -> ICore Blocks/Home/Subtract. Every block around this loop feeds its input straight through to its output, so no block can be solved first. …in the Run Diagnosis (and the same row refuses a code export). Such a loop isu = f(u)at one instant; this simulator does not solve it, and until 2026-09-03 it ran it with an implicit one-sample delay — which converged only while the loop gain stayed below one (the same diagram withGain −2grew by ×32 per step and said nothing). Break the loop with a Unit Delay or Memory (discrete), an Integrator or a strictly proper transfer function (continuous), or an Algebraic Constraint block; a subsystem in the loop counts as feeding through only if some path from one of its input gates to one of its output gates does. - The extra delay is one sample, not one second. Halving
globalSamplingTimehalves the settling time in seconds and leaves the sample-by-sample sequence identical (run below).
Which edge is the late one? Always the output of the block that opened the loop, and
nothing else about the diagram moves it: placing the same four blocks in a different order, or
adding a Scope, gives the same columns (runs below). Only the block's nature decides — an
Integrator under the Discrete solver with a Tustin discretization does feed through at run
time, but it is still the block the loop opens at, and that loop runs with the one-sample delay
instead of being refused. A block whose feedthrough depends on a parameter answers from it: a
Transport Delay whose delay rounds to zero samples, a Tapped Delay with Include Current Input
on, a Transfer Function whose numerator has the denominator's degree all feed through. A
discrete-only block that carries its state inside its own code rather than in a state space —
an odometry, a buffer, a Python Code block — is taken to hold its output, so a loop through
it opens there and runs with the one-sample delay it always had; it is never the reason a loop
is refused.
<!-- loop-delay: begin (generated fragment, shared by every page that carries it -- do not edit by hand; the loop_delay_plot sample script rewrites it) -->
Same grid, 60 samples, ICore commit 99fda4b6, MATLAB R2026a FixedStepDiscrete at 0.1 s. max |ICore − Simulink| = 0.25; max |Simulink − recursion y[k] = 0.5 (u[k] − y[k−1])| = 0 (Simulink is the recursion exactly; ICore is not).
| k | t | u[k] | y Simulink | y ICore | z ICore (Unit Delay out) |
|---|---|---|---|---|---|
| 9 | 0.9 | 0 | 0 | 0 | 0 |
| 10 | 1.0 | 1 | 0.5 | 0.5 | 0 |
| 11 | 1.1 | 1 | 0.25 | 0.5 | 0.5 |
| 12 | 1.2 | 1 | 0.375 | 0.25 | 0.5 |
| 13 | 1.3 | 1 | 0.3125 | 0.25 | 0.25 |
| 14 | 1.4 | 1 | 0.34375 | 0.375 | 0.25 |
| 15 | 1.5 | 1 | 0.328125 | 0.375 | 0.375 |
| 16 | 1.6 | 1 | 0.335938 | 0.3125 | 0.375 |
<!-- loop-delay: end -->
Traps#
- "My discrete block's Sampling Time does nothing" — the solver is
Discrete; every block steps atglobalSamplingTime. UseContinuouswith a fixed-step method to run blocks at their own rates. - "Invalid time budget detected at block: …" —
timeBudgetValidationEnabledis on and a block's own period differs from the step it was given (typically theDiscretecase above). Turn the check off or make the rates agree. - "Unable to capture all multi-rate bandwidths: N bandwidth bucket(s) for M distinct block
sampling rate(s)" — two rates round onto the same multiple of
multiRateTolerance; lower the tolerance or separate the rates (groupOrderedBlocksByBandWidth). - The run crawls after setting one block's rate — the fine step is the GCD of all rates
after rounding to
multiRateTolerance;0.1next to0.1000001gives a fine step of1e-7s and a window of 1e5 s (their least common multiple). Read frominitializeSamplingTimes, not measured. - The
Continuous/multi-rate clock only lands on window boundaries — a probe that samples the simulator clock sees one row per window (0.3s in the run below), while blocks inside the window were solved at their own sub-steps. - "Variable-step solver asked for a step of … below Min Step Size" — the error controller
wanted a smaller step than
minTimeStepallows; the step was clamped and taken, so the tolerance is not met there. LowerminTimeStepor relax the tolerances. Said once per run.
Real runs (2026-08-17)#
Binary build-mac/ICoreBlocks.app, built 2026-08-16 23:39 (≈ commit 2e126fbf), run
headless as HOME=<scratch> QT_QPA_PLATFORM=offscreen …/ICoreBlocks --console "<line>". The
loop recipe is tools/docs/samples/loop_delay.recipe; the others are three-line variants of
it kept in the scratch folder. Values are the port columns of the docsSample --recipe JSON.
docsSample --out <scratch> --recipe loop_delay.recipe --name loop_ts01 (Gain output,
Ts = 0.1, step at 1 s = sample 10):
k: 10 11 12 13 14 15 16 17 18 19
y: 0.5 0.5 0.25 0.25 0.375 0.375 0.3125 0.3125 0.34375 0.34375
setModelConfig globalSamplingTime 0.05; docsSample … --name loop_ts005 — the step is now
sample 20 and the same sequence follows it, sample for sample:
k: 20 21 22 23 24 25 26 27 28 29
t: 1.00 1.05 1.10 1.15 1.20 1.25 1.30 1.35 1.40 1.45
y: 0.5 0.5 0.25 0.25 0.375 0.375 0.3125 0.3125 0.34375 0.34375
Same recipe with the Unit Delay created before the Subtract (loop_zfirst), and with a
Scope branched onto the Gain output (loop_scope), re-run 2026-09-03 (build/ICoreBlocks.exe
at commit e2c70fb6 plus this change, RK4, Ts = 0.1, --steps 20): every column identical
to the original in both variants — the Unit Delay's output reads 0, 0.5, 0.5, 0.25, 0.25, …
from sample 10, the Gain 0.5, 0.5, 0.25, 0.25, 0.375, …. (Before that date the loop_zfirst
and loop_scope variants moved the stale edge and the Unit Delay column read
0, 0, 0.5, 0.5, 0.25, …; the loop now always opens at the Unit Delay's output.)
Delay-free loop (algloop: Step → Subtract → Gain 0.5 → Subtract), same binary — refused, no
data:
docsSample: D: unsampled (Algebraic loop detected: ICore Blocks/Home/Gain -> ICore Blocks/Home/Subtract -> ICore Blocks/Home/Gain. Every block around this loop feeds its input straight through to its output, so no block can be solved first. Break the loop with a block that has no direct feedthrough (a Unit Delay or Memory in a discrete loop, an Integrator or a strictly proper Transfer Function in a continuous one), or use an Algebraic Constraint block.)
(Until 2026-09-03 the same recipe ran and gave y = 0.5, 0.25, 0.375, 0.3125, … after the
step, the delayed loop's answer; with Gain −2 in place of 0.5 it gave
−6, −254, −8190, −262142, … — ×32 per step — and no diagnostic.)
Continuous loop Integrator (Initial Value 1) → Gain −1 → Integrator, RK4, Ts = 0.1, Joint
coupling, same binary, --steps 11 — the Integrator column, identical in all three variants
(Integrator created first with no sink; Gain created first; a Scope branched off the
Integrator), x(1) = 0.3678797744 against e⁻¹ = 0.3678794412:
t: 0 0.1 0.2 0.3 0.4 0.5 ... 1.0
x: 1 0.9048375 0.8187309014 0.740818422 0.6703202889 0.6065309344 ... 0.3678797744
and the Gain column is −x at every row, −1 included at t = 0. (Before that date the
Gain-first and Scope variants gave x(1) = 0.3584859224 and a Gain of 0 at t = 0.)
Multi-rate (mrate: Step, then Gain 2 with Sampling Time (s) = 0.15, Continuous
fixed-step at 0.1): 20 rows for the 60-step horizon, one per 0.3 s window —
t = 0, 0.3, 0.6, 0.9, …; the Gain column reads 2 from the t = 0.9 window (whose sub-steps
0.9 … 1.15 include the Gain's own solve at 1.05, after the step).
Unit Delay with Sampling Time (s) = 0.2 after a Step at 1 s (ud02):
Continuous fixed-step 0.1: rows every 0.2 s; z = 0 at t=1.0, 1 from t=1.2 (window 0.2, block solved every 0.2)
setModelConfig solverType Discrete: rows every 0.1 s; z = 0 at t=1.0, 1 from t=1.1 (block solved every 0.1)
+ setModelConfig timeBudgetValidationEnabled true:
docsSample: ud02_disc_budget: unsampled (Invalid time budget detected at block: ICore Blocks/Home/Unit Delay)
For contributors#
The invariants behind this page — the sample-time table, the graph ordering that opens a loop
at a block without direct feedthrough and refuses an algebraic one, and why feedback diagrams
opt out of Simulink parity — are stated on the simulator's contributor page
and in testingLabs/ParityLab/ICoreParityDiagramLibrary.h; the plot above is one generated
fragment shared with that page, so the two can never disagree.