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 (fixed-step RK1–RK4 or variable-step RK45/RK23), 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.
  • 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".

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 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. 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 Subtract produced y = 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 in ICoreBlockSolverEnvironment::assignSolverOrder and is switched off.
  • 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? 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) -->

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.

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.