Step Execution Model
Per-step runtime semantics for begin, evaluate, commit, and output handling.
Use this section when you are wiring TDSE Runtime into a simulator step loop. It explains what each step call means, how trial state differs from committed state, how retries behave when a trial is rejected, and what to record when the loop does not behave as expected.
Related Chapters For lifecycle and shutdown semantics, see Runtime Lifecycle. For concurrency rules, see Concurrency and Shutdown.
If the lifecycle section answers "who owns the handle," this section answers "what exact state is
the handle in between begin, query calls, commit, and dr?"
For most customer integrations, this section is the contract source for one of these three host patterns:
| Host pattern | What to focus on here |
|---|---|
| fixed-step solver loop | canonical step order and prime-step pattern |
| adaptive / trial-rejecting solver loop | rejected-trial semantics and state snapshots |
| real-time / HIL loop | committed-state discipline, dr timing, and one-handle-per-thread rules |
The Three Invariants
If you remember only three step-loop rules, remember these:
-
tdse_step_begin(...)creates a trial context at one exact(t, dt). -
These trial query calls are query-only within that trial context:
tdse_step_op(...)tdse_step_hr(...)tdse_step_ir(...)
-
tdse_step_commit(...)is the only call that advances committed history.
Everything else in this chapter follows from those three rules.
Canonical Step Order
Per ordinary step, the runtime flow is:
tdse_step_begin(model, t, dt)tdse_step_op(model, op_out)as neededtdse_step_hr(model, hr_out)tdse_step_ir(model, ir_out)when IR exists and is still in range- host-side trial solve or trial update
tdse_step_commit(model, primary_accepted)if the trial is accepted- optional
tdse_step_dr(model, dr_out)after commit
tdse_step_op(...) is an instantaneous operator query. Hosts may:
- fetch it once and cache it under LTI assumptions
- fetch it per step when host policy wants explicit freshness
Host/TDSE ownership split in this loop:
- the host owns
t,dt, trial acceptance, solver state, and the acceptedprimaryvector - TDSE owns trial-local query state, committed history, and the resulting
op/hr/ir/drsurfaces
Prime-Step Pattern
The standard integration pattern primes at n = -1:
tdse_step_begin(m, t0 - dt, dt);
tdse_step_op(m, &op_square);
tdse_step_commit(m, primary_minus1);
Why this matters:
- it establishes committed history before the first ordinary simulation step
- it makes later
hrevaluation align with the expected discrete-time history model - it gives
dra valid first committed state immediately after prime
If you skip the prime step without redesigning initialization assumptions, the first visible issue often looks numerical even though the real bug is committed-history alignment.
Prime-step design review question:
- what exact accepted primary vector should the runtime treat as the prehistory state?
If that answer is still implicit, the integration is not really complete.
Mathematical Contract
The trial-side relation is:
y_trial = op * primary_trial + hr + ir
Where:
opis the instantaneous operatorhris the delayed-history contributioniris the independent-response contribution
The committed direct-response relation is:
dr[n] = op * primary_accepted[n]
Legality Matrix
This is the fastest table to use during code review or triage:
| API | Requires Live Handle | Requires Active Trial Step | Requires Prior Commit | Advances State |
|---|---|---|---|---|
tdse_step_begin(...) | yes | no, unless exact re-entry | no | creates or re-enters trial context |
tdse_step_op(...) | yes | yes | no | no |
tdse_step_hr(...) | yes | yes | no | no |
tdse_step_ir(...) | yes | yes | no | no |
tdse_step_commit(...) | yes | yes | no | yes |
tdse_step_dr(...) | yes | no | yes | no |
What tdse_model_state_info(...) Means During A Step Loop
The most useful runtime snapshot during integration is tdse_model_state_info(...).
Its high-value fields are:
| Field | Operational Meaning |
|---|---|
step_active | nonzero while a trial step is currently active |
has_committed_step | nonzero after at least one successful commit |
committed_steps | number of accepted commits |
committed_t / committed_dt | time coordinates of the latest committed step |
active_t / active_dt | active trial-step coordinates, or zero when no trial is active |
sim_time | accumulated committed simulation time |
dr_last_valid | whether tdse_step_dr(...) is currently queryable |
This snapshot reports dynamic execution state only. It is the quickest way to distinguish these three cases:
- no trial has started yet
- a trial is active but not committed
- a committed step exists and
dris valid
State Snapshot Transition Table
The manual previously named these fields, but host teams usually need the exact transition pattern. Use this table as the expected runtime fingerprint.
| Phase | step_active | has_committed_step | committed_steps | active_t / active_dt | committed_t / committed_dt | dr_last_valid |
|---|---|---|---|---|---|---|
| newly created handle | 0 | 0 | 0 | 0 / 0 | 0 / 0 | 0 |
after begin(t, dt) | 1 | unchanged | unchanged | t / dt | unchanged | unchanged |
| after trial queries only | 1 | unchanged | unchanged | t / dt | unchanged | unchanged |
after accepted commit(...) | 0 | 1 | increments by 1 | 0 / 0 | becomes the just-committed t / dt | 1 |
| after rejected trial with no commit | 1 until caller leaves the active trial lifecycle | unchanged | unchanged | still the active trial coordinates | unchanged | unchanged |
after dr(...) | 0 | 1 | unchanged | 0 / 0 | unchanged | 1 |
Two practical readings matter:
- if
step_active=1andcommitted_stepsis not moving, the runtime is still in a trial context - if
dr_last_valid=1, at least one accepted commit already exists and no new commit is required just to read the latest committed direct response
begin Semantics And Re-Entry
tdse_step_begin(...) does three user-visible things:
- validates that the handle and
(t, dt)are legal - freezes execution-affecting runtime controls on the first successful begin
- records the active trial coordinates and marks
step_active=1
The important re-entry rule is strict:
-
re-entering
tdse_step_begin(...)while a trial is already active is valid only whentanddtmatch the already-active trial step within runtime tolerance -
mismatched re-entry fails with
TDSE_ERR_INVALID_STATE
This means begin is not a generic "start over" button.
It is an idempotent re-entry only for the same trial coordinates.
What Re-Entry Is For
Exact re-entry is useful when:
- a host wrapper retries a query path but is still on the same trial step
- instrumentation or layered call sites may invoke the same "ensure step active" helper twice
It is not valid for:
- changing
tmid-trial - changing
dtmid-trial - silently converting a rejected trial into a different step without resolving the active state
Rejected Trials And Retry Semantics
Rejected trials are normal in real host integrations. The runtime contract is intentionally simple:
- start the trial with
begin - query
op,hr, and optionalir - host decides the trial solve is not acceptable
- do not call
commit - treat committed history as unchanged
The one rule that matters most is this:
- no
commitmeans no committed-state movement
Operational consequences:
committed_stepsdoes not increasecommitted_tandcommitted_dtdo not changesim_timedoes not advancedr_last_validstays tied to the previous committed step
How To Retry Cleanly
If the host rejects the trial and wants to try again:
- keep the retry anchored to the same step coordinates if the same trial is being reconsidered
- do not pretend a new committed step exists
- do not read the lack of state movement as a runtime failure; it is the intended rejection model
If the host instead wants a different (t, dt), it must follow the documented lifecycle rather
than mismatched begin re-entry.
Worked Snapshot Trace
This is the most useful support trace to memorize.
Before Prime
Expected snapshot:
step_active=0has_committed_step=0committed_steps=0dr_last_valid=0
Interpretation:
- no committed history exists yet
dr(...)is invalid at this point
After Prime begin(t0 - dt, dt)
Expected snapshot:
step_active=1active_t=t0 - dtactive_dt=dthas_committed_step=0
Interpretation:
- the runtime now has an active trial context
- committed history still does not exist until
commit
After Prime commit(primary_minus1)
Expected snapshot:
step_active=0has_committed_step=1committed_steps=1committed_t=t0 - dtcommitted_dt=dtdr_last_valid=1
Interpretation:
- history now exists
dr(...)is legal- the first ordinary step can use a properly anchored committed past
During Ordinary Step n
After begin(t_n, dt) and before commit(...):
step_active=1active_t=t_nactive_dt=dtcommitted_stepsstill equals the previous accepted countcommitted_tstill points to the previous accepted step
This is the point where support should ask:
- are we still in a legitimate trial?
- is the host expecting committed values too early?
After Accepted Commit At Step n
Expected snapshot:
step_active=0committed_stepsincrements by onecommitted_t=t_ncommitted_dt=dtsim_timeincreases bydtdr_last_valid=1
Interpretation:
- committed history has advanced
- future
hrcalls will now see the newly accepted state in their delayed-history basis
After Rejected Trial At Step n
If the host decides not to commit:
committed_stepsremains unchangedcommitted_tremains the previous committed timesim_timeremains unchangeddr_last_validstill refers to the previous committed step
This is the exact fingerprint of "trial rejected, committed history preserved."
Worked Host Pattern
The intended control split is:
- Runtime supplies
op,hr, and optionalir - host code computes or solves the trial equation
- host decides whether the trial primary vector is accepted
- Runtime advances history only when the host commits
This is why Runtime remains auditable inside larger simulator loops.
Minimal Integration Recipes
Use the smallest recipe that matches your host:
| Host type | Minimal recipe |
|---|---|
| fixed-step simulator | prime once, then begin -> op/hr/ir -> host solve -> commit -> dr |
| adaptive simulator | same loop, but host may reject a trial and skip commit |
| RT/HIL loop | fixed accepted-step policy, one handle per execution thread, dr only after commit |
For code review, the key question is not "did the host call the APIs" but "which side owns acceptance, timing, and history movement?"
C Example
tdse_step_begin(model, t, dt);
tdse_step_op(model, &op);
tdse_step_hr(model, hr);
tdse_status_t ir_st = tdse_step_ir(model, ir);
if (ir_st == TDSE_ERR_IR_STEP_OUT_OF_RANGE) {
/* Host policy decides whether this is a hard stop or planned IR horizon exit. */
}
solve_trial(op, hr, ir, primary_trial, y_trial);
if (trial_is_accepted(primary_trial, y_trial)) {
tdse_step_commit(model, primary_trial);
tdse_step_dr(model, dr);
}
The key handbook reading is:
- the host owns acceptance
- the runtime owns committed-history mutation
Fixed-Step Host Skeleton
This is the simplest integration that should be proven before any adaptive or real-time embellishment:
prime_once(model, t0, dt, primary_minus1);
for (size_t n = 0; n < nsteps; ++n) {
tdse_step_begin(model, t0 + n * dt, dt);
tdse_step_op(model, &op);
tdse_step_hr(model, hr);
tdse_step_ir(model, ir); /* when IR exists */
host_solve_trial(op, hr, ir, primary_trial, y_trial);
tdse_step_commit(model, primary_trial);
tdse_step_dr(model, dr);
}
If this path is not already stable, stop here before adding retries, adaptive
dt, or multi-threading.
Adaptive-Step Ownership Rule
For adaptive hosts, keep one rule explicit in design notes:
- the host may retry or reject a trial
- TDSE must not see history advancement unless the host actually commits
That single rule prevents most accidental state-drift bugs.
IR Horizon Behavior Inside The Loop
tdse_step_ir(...) is query-only, but it is still a contract boundary.
If the current step time lies beyond configured IR support, the API returns
TDSE_ERR_IR_STEP_OUT_OF_RANGE.
Read that status as:
- the runtime is telling you the packaged
IRsequence does not extend to this step - the runtime is not silently extrapolating
IR
What to record:
- archive
committed_steps,committed_t,dt, and modelir_nsteps - verify whether the host advanced beyond the packaged horizon by design or by mistake
Rectangular Versus Square Operator View
tdse_step_op(...) supports:
- square view:
np x np - full rectangular view:
nq x np
Recommended practice:
- choose one operator-view policy per integration
- document that policy in the host design
- do not switch views ad hoc across call sites without a clear reason
Using the wrong view usually appears later as a shape or interpretation bug, not at the moment the host design drifted.
What to Capture for Step Incidents
When a step-loop issue shows up, collect these details in this order:
- failing API name and returned status
tdse_model_state_info(...)tdse_model_last_error_info(...)- exact
t,dt, and host step index - whether the host intended to accept or reject the trial
- whether prime was performed
- whether the host expected square or rectangular
op
This sequence usually resolves three common ambiguities immediately:
- lifecycle misuse versus numerical rejection
- missing prime versus wrong-history interpretation
IRhorizon exit versus unrelated step failure
Common Step-Loop Mistakes
| Mistake | Why It Is Wrong | Correct Pattern |
|---|---|---|
calling dr during an active trial step | dr is post-commit only | query dr after commit, with no active trial step |
treating hr as state-advancing | hr is query-only | call commit to advance history |
| skipping prime without redesigning history assumptions | later history alignment shifts | prime at n = -1 or document a different initialization contract |
re-entering begin with different t or dt | active trial context must remain one exact step | finish the active trial lifecycle before changing coordinates |
| assuming rejected trial implies hidden state rollback logic | no commit means no advancement happened | read snapshots and keep retry logic explicit |
| mixing square and rectangular operator views ad hoc | host matrix assumptions drift | choose and document one operator-view policy |
Anti-Patterns
Avoid these integration patterns even if they seem harmless in local testing:
- using
state_infoonly after failures instead of also learning the normal expected snapshot - inferring committed advancement from successful
hrorirqueries - treating
dr_last_valid=1as proof that the current trial was committed - retrying a rejected trial by changing
(t, dt)under an already-active trial step - explaining a history bug as "numerical noise" before verifying prime and commit sequencing
