Time grids and adaptive refinement

The perfect-foresight transition is discretised on a time grid of N intervals over [0, T]. By default the grid is uniform; continuo can also place the nodes non-uniformly — automatically aligning them to shock reveal times, and (opt-in) adaptively refining where the solution is hard. This is orthogonal to the discretisation scheme (Discretisation schemes) and the linear backend (Linear solvers): any combination works.

Shock-aligned nodes

When a shocks block reveals a new belief at time t, that reveal is a kink in the trajectory. continuo makes the reveal time an exact grid node (each belief segment’s mesh is built with a node there), so the kink is resolved cleanly rather than smeared across the nearest interval. This is automatic and needs no configuration.

Adaptive refinement

Transitional dynamics are often fast early and slow late, or sharp only in a small part of the horizon. A uniform grid then wastes nodes on the smooth tail and under-resolves the fast part. adapt turns on a-posteriori mesh refinement: continuo solves, estimates the error, bisects the intervals where the solution curvature is largest (equidistribution), and re-solves — repeating until the estimated error falls below the tolerance (or a node / pass cap is hit). Each belief segment is refined independently, and reveal nodes are preserved.

simulate(T = 50, N = 250, adapt = 1e-6);            // refine to a 1e-6 error
simulate(T = 50, N = 250, adapt = 1e-6, monitor = residual);
model.simul(adapt=1e-6)                  # from the API
model.simul(adapt=1e-6, monitor="residual")
$ continuo model.mod --adapt 1e-6 --monitor residual

N is then the starting resolution. Refinement only ever adds nodes, so it is monotone and the reveal / terminal nodes stay fixed.

The error monitor

monitor chooses how the error is estimated for the stopping test (where to refine is always decided by the cheap curvature indicator):

residual (default)

The ODE defect of a smooth interpolant of the node values — an indicator that needs no extra solve and makes no order assumption, so it stays reliable at a kink. The cheap, robust default.

richardson

Solve once more on the bisected mesh and scale the difference by the scheme order — a calibrated error magnitude, scheme-agnostic. Costs one extra solve per pass, and assumes the nominal order, so it under-refines at a kink; prefer it for a trustworthy tolerance on a smooth solve.

(The placement-only curvature monitor has no error magnitude and so cannot drive adapt.)

The grid-adequacy diagnostic

Every solve — adaptive or not — reports an equidistribution ratio in Solution.diagnostics["equidistribution_ratio"]: the max/mean of the per-interval curvature. A value near 1 means the resolution is well balanced; a large value means it is misallocated and a non-uniform or refined grid would help. It is a cheap way to decide whether adapt is worth turning on for a given model.

When it helps (and when it doesn’t)

Adaptivity pays off when the solution is smooth but localised — a fast transient, a region of high curvature. The RBC example shows the capital transition refined from an equidistribution ratio of ~24 down to ~1.5, concentrating nodes on the fast early adjustment (the ratio measures curvature-driven placement, independent of the stopping monitor). At a genuine kink (an occasionally-binding constraint) the curvature never smooths out, so the refiner would chase it indefinitely; that is why refinement is capped, and why pinning a node at the non-smoothness (as the shock machinery does for reveals) plus raising N can be the steadier choice there.

Note

Refinement re-meshes between passes, so each pass re-analyses the sparsity pattern of its linear system (unlike the fixed-grid run, which reuses one analysis across the whole horizon). The cost is bounded by the node and pass caps.