Physical grids for stochastic simulation

Fixed-step scalar and coupled SDE simulation uses the requested dt for full intervals and shortens the final interval to reach t_end. For example, dt=0.3 over [0, 1] produces times [0, 0.3, 0.6, 0.9, 1]. Drift and stepper corrections use those interval widths, Brownian increments have the matching variances, and supported Poisson increments use the matching rates.

Where an existing method accepts n_steps, an explicit positive integer count instead selects equal intervals spanning the range. n_steps=2 over [0, 1] uses two intervals of width 0.5, regardless of the configured positive dt. The calculus configuration is not modified. Invalid or nonfinite ranges, nonpositive dt, invalid counts and unrepresentable grids reject before noise generation. Equal endpoints without an explicit count return the initial state and consume no noise; an explicit count requires a positive duration.

Grid construction uses a host float64 near-integer tolerance of 8 * eps64 * max(1, abs(duration / dt)). This avoids an extra interval caused only by representation roundoff while retaining genuine terminal remainders. The materialized grid follows JAX's configured floating precision. Nominal uniform widths are retained within timestamp representation roundoff so the established seeded uniform draw behavior remains compatible.

Paths and their coordinates

Existing array-only calls remain available. Use the corresponding result method when you also need the actual time coordinates:

import jax.numpy as jnp

from gimle.asgard.runtime.stochastic.calculus import StochasticCalculus

calculus = StochasticCalculus(dt=0.3, n_paths=2, seed=42)
result = calculus.simulate_sde_result(
    x0=0.0,
    drift_fn=lambda x, t: jnp.ones_like(x),
    diffusion_fn=lambda x, t: jnp.zeros_like(x),
    t_start=0.0,
    t_end=1.0,
)
# result.times: [0, 0.3, 0.6, 0.9, 1]
# result.paths: one path per row, with time on the last axis

simulate_coupled_sde_result, StochasticCircuitExecutor.execute_result and execute_coupled_circuit_driven_result likewise return immutable StochasticExecutionResult(paths, times) values from a single execution. The existing simulate_sde, simulate_coupled_sde and executor methods return only the path array. Public stochastic API results and fixed-step YAML storage retain execution coordinates; do not reconstruct them from the path count.

Exogenous interval drivers

The direct coupled circuit API requires one driver value for every actual interval. A shared driver has shape (interval_count,); per-path drivers have shape (n_paths, interval_count). A value applies on [t_i, t_(i+1)), including the shorter final interval. Obtain the count from the same public grid policy:

from gimle.asgard.runtime.stochastic.physical_grid import PhysicalTimeGrid

grid = PhysicalTimeGrid(0.0, 1.0, dt=0.3)
assert grid.interval_count == 4

YAML driver declarations remain unsupported under their existing contract.

Untiled iterated integrals and single-circuit expansions

Direct evaluate_iterated_integrals and internally generated evaluate_expansion integrals use the same physical-grid policy: omitted n_steps retains full configured dt intervals and a short tail; an explicit positive count selects equal intervals. Returned coordinates are absolute times, while integrals accumulate elapsed time from t_start. Unlike fixed-step initial-state simulation, these integral APIs retain their existing requirement that t_end be strictly greater than t_start.

The untiled single-circuit stream_calculus route resolves that grid before coefficient compilation. Integral updates, Brownian variances, Poisson rates, and jump sample counts use its actual widths, including the short interval. Depth zero returns the root coefficient on the actual times and consumes no randomness, including on the single jump route.

Supplied increments are already physical samples and are never rescaled. Binary integral calls accept dW with shape (n_paths, interval_count); multi-noise calls accept dW_all with shape (n_children - 1, n_paths, interval_count). Supplying both inputs or the wrong channel is rejected. Shape/channel validation also applies at depth zero. Single-circuit strategy calls accept Brownian dW; a jump circuit prepares its separate compound-Poisson channel. Precomputed integrals supplied together with time_grid remain an external-data contraction and retain those coordinates.

The direct integral kernel preserves its historical full-path Gaussian draws, including when the noise source has antithetic=True. The single jump strategy retains its existing antithetic-aware Brownian draws and Brownian/Poisson/jump split order. Uniform dyadic grids retain exact seeded compatibility; other widths follow the configured floating precision. This adoption does not change the left-point Itô or mixed midpoint-noise Stratonovich recursion, coefficient generation, or the per-interval compound-jump approximation.

Host range/count settings remain static. Direct integral/expansion calls with supplied arrays retain JIT and differentiation support when nan_check_interval=0; stateful random generation is not a pure JIT API.

Untiled coupled expansions

StochasticCircuitExecutor.execute_coupled_stream_calculus returns (paths, times) on one physical grid when tile_dt is omitted. This applies to independent equations, acyclic upstream dependencies and the automatic simultaneous-Picard fallback for cyclic dependencies. The direct simultaneous entry point uses the same policy. Omitted counts retain nominal dt intervals and a short tail; explicit positive counts select equal intervals.

Correlated Brownian sampling, every equation's iterated integrals and returned coordinates use that grid. The existing coefficient iteration, channel mapping and antithetic/correlation draw conventions are retained. The coupled drivers retain their existing supported channel layouts and equation-count limits. They still generate their one correlated array at depth zero; they do not inherit the direct integral kernel's no-noise depth-zero behavior. Nonpositive durations remain invalid. Runtime grid validation precedes coefficient evaluation and noise generation; the YAML loader still compiles equations before calling the runtime driver and retains its returned coordinates.

Tiled stream-calculus expansions

Single-circuit tiled execution, including jump circuits, now prepares one global physical grid before sampling. Coupled tiled execution does the same for independent equations and the simultaneous-Picard fallback. With omitted n_steps, [2, 3] and dt=0.3 has times [2, 2.3, 2.6, 2.9, 3]; an explicit count of three produces three equal intervals. tile_dt groups whole global intervals using the established rounded count policy. A short final interval stays in its last group and is never split or made uniform.

Each tile uses slices of the global Brownian increments, physical widths and coordinates. Jump events use rates equal to intensity times the global widths; jump sizes retain one sample per global interval. A fixed seed on the single tiled route still starts a fresh Brownian key, while an unseeded call advances the shared key once. The existing ensemble-mean expansion point, per-path offset correction and adaptive tile-count policy remain in place. The offset correction is approximate for nonlinear systems. Acyclic coupled equations with inherited multiple noise channels still reject tiling.

First-passage statistics and remaining routes

YAML analysis.first_passage reports elapsed duration from the initial observation: times[first_crossing_index] - times[0]. A path initially across the threshold has duration zero. A terminal crossing uses the actual final time, including a shortened last interval. Paths that never cross are excluded from crossing-time statistics and reported through pct_crossed.

Adaptive YAML output retains its existing explicit interpolation onto an output grid. This change does not add methods or alter diffusion/correlation support, clamping, adaptive rejection behavior or compound-jump approximations.