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(fixed-step RK1–RK4 or variable-step RK45/RK23),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. - 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".
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. One rate for everything — the step the integrator picks; see Solver mathematics — the forms, the discretizations and the stepping scheme.
Loops: one edge is always one cycle late#
Ports keep their last written value, and a block reads its inputs at the moment it is solved. Around a loop the blocks are solved in some order, so whichever block comes first reads a value its source has not yet written this cycle — the source's value from the previous cycle. Exactly one edge of every loop is one sample stale, in addition to any delay blocks the loop already contains. Consequences a user sees:
- A unit-feedback loop through a Unit Delay settles at half tempo. 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. The plot below is that loop in both tools. - Comparing a feedback 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 delay block at all is not reported as an algebraic loop. It simply runs
with the one implicit delay:
u → Subtract → Gain 0.5 → back to Subtractproducedy = 0.5, 0.25, 0.375, 0.3125, …after the step (run below), which is the delayed loop's answer in Simulink; Simulink would refuse or algebraically solve the delay-free diagram (y = u/3). The diagnostic exists inICoreBlockSolverEnvironment::assignSolverOrderand is switched off. - 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? You cannot choose it. The build orders blocks by walking
backwards from every sink — a block none of whose outputs is connected — through input
connections; a block's order is one more than its latest source, and a source the walk has
already visited from that block is skipped, which is where a loop opens (assignSolverOrders,
ICoreModelBuild.cpp; assignSolverOrder, ICoreBlockSolverEnvironment.cpp). A diagram with
no sink keeps creation order. Placing the same four blocks in a different order, or adding a
Scope, moved the stale edge — the Unit Delay's output port then showed y[k−2] instead of
y[k−1] — but the Gain output was identical in every variant: the total delay around the loop
is what is fixed, and no setting in the UI steers it.
<!-- 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.
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): Gain output identical to the table
above in both; the Unit Delay's output column became 0, 0, 0.5, 0.5, 0.25, 0.25, … from
sample 10 (it now reads the Gain one cycle late) where the original showed
0, 0.5, 0.5, 0.25, 0.25, ….
Delay-free loop (algloop: Step → Subtract → Gain 0.5 → Subtract), no diagnostic, exit 0:
k: 10 11 12 13 14 15 16 17
y: 0.5 0.25 0.375 0.3125 0.34375 0.32812 0.33594 0.33203
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 visited-set cycle break, 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.