Reference › Sample time and loops — how the simulator paces a diagram
kind: reference#sample-time#sampling#multi-rate#loops#feedback#unit-delay#solver#simulink

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 (Continuous or Discrete), 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 (default 0.1 s), multiRateTolerance (default 1e-9 s), and the timeBudgetValidationEnabled check (default off).
  • Fixed-step time is a grid, not a sum. The clock is start + n × Ts, never t += Ts, so the tenth step of Ts = 0.1 lands on exactly 1.0 and a Step block set to switch at 1.0 switches 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 startTime every block reports its initial state without integrating; a finite run then solves every grid point up to and including stopTime (when the grid lands on it, within 1e-9 s) and none beyond, in every solver mode. stopTime = 10 at Ts = 0.1 gives 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 kindSolverSampling Time (s) ≤ 0Sampling 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)anyglobalSamplingTimethe value
Any other blockContinuous, fixed-stepits 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-downthe value
Any other blockContinuous, variable-stepthe variable stepignored — warning "Block sampling time will be overridden at each step by the global variable-step"
Any other blockDiscreteglobalSamplingTimeignored — 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 ≤ 0 runs at that rate, under either solver — discrete-only blocks included, which inside a plain subsystem take globalSamplingTime instead, and under the Discrete solver the other blocks too, which that solver otherwise holds to globalSamplingTime whatever they are set to. An atomic subsystem at -1 inherits 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 Continuous solver's default Joint coupling 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 → Integrator under RK4 at 0.1 s gives x(1) = 0.3678798 (e⁻¹ to 3.3e-7) with the Gain created first, and with a Scope on the diagram — both used to come out 0.3584859, 9.4e-3 off, because the order put the Gain first and it read the Integrator one stage stale (solver suite, 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 computes y[k] = 0.5 (u[k] − y[k−1]); ICore computes y[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 the Per-block coupling and the Discrete solver, 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 Subtract stops with Algebraic 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 is u = 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 with Gain −2 grew 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 globalSamplingTime halves 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) -->

Unit-feedback loop, y = 0.5 (u - z^-1 y): ICore vs SimulinkUnit-feedback loop, y = 0.5 (u - z^-1 y): ICore vs Simulink00.51012345t (s), Ts = 0.1valueu (Step)y Simulink R2026ay ICoreICore - Simulink

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).

ktu[k]y Simulinky ICorez ICore (Unit Delay out)
90.90000
101.010.50.50
111.110.250.50.5
121.210.3750.250.5
131.310.31250.250.25
141.410.343750.3750.25
151.510.3281250.3750.375
161.610.3359380.31250.375

<!-- loop-delay: end -->

Traps#

  • "My discrete block's Sampling Time does nothing" — the solver is Discrete; every block steps at globalSamplingTime. Use Continuous with a fixed-step method to run blocks at their own rates.
  • "Invalid time budget detected at block: …" — timeBudgetValidationEnabled is on and a block's own period differs from the step it was given (typically the Discrete case 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.1 next to 0.1000001 gives a fine step of 1e-7 s and a window of 1e5 s (their least common multiple). Read from initializeSamplingTimes, 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.3 s 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 minTimeStep allows; the step was clamped and taken, so the tolerance is not met there. Lower minTimeStep or 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.