Skip to content

Integration Results ​

CommonSolve.solve(prob, integrator) integrates an ODE problem and returns an Integrators.AbstractIntegrationResult. This page describes how to read it and how the saving options shape it. CTFlows builds on this layer; its documentation covers flows, multi-phase trajectories and control laws at a higher level.

julia
using CTSolvers: Integrators
using OrdinaryDiffEqTsit5
using SciMLBase: ODEProblem
using CommonSolve: solve

prob = ODEProblem((u, p, t) -> -u, [1.0], (0.0, 1.0))   # u(t) = exp(-t)
integ = Integrators.SciML()
r = solve(prob, integ)

Reading a result ​

AccessorReturns
Integrators.final_state(r)State at the final time
Integrators.times(r)Output time grid
Integrators.evaluate_at(r, t)State at any time t of the span
Integrators.is_dense(r)Whether evaluate_at reads a dense interpolant
Integrators.status(r)Termination status, as a Symbol
Integrators.successful(r)Whether the integration succeeded
julia
julia> Integrators.final_state(r)
1-element Vector{Float64}:
 0.3678794412565068

julia> Integrators.evaluate_at(r, 0.5)
1-element Vector{Float64}:
 0.6065306597421974

julia> Integrators.is_dense(r), Integrators.status(r), Integrators.successful(r)
(true, :Success, true)

status and successful are the same functions as CTModels.Solutions.status and CTModels.Solutions.successful.

Point vs trajectory integration ​

The integrator caches two option sets, built once at construction:

julia
rp = solve(prob, integ; options = Integrators.options_point(integ))
Integrators.times(rp)
1-element Vector{Float64}:
 1.0

These options default to :auto and are resolved this way unless you set them explicitly (dense, save_everystep, save_start).

saveat is an output grid ​

With saveat, the result's time grid is the requested grid, while the solution stays dense: evaluate_at is accurate everywhere, not only at the saved times. The problem is integrated with the same steps (and cost) as SciML's native saveat, and returns the same values on the grid.

julia
integ_grid = Integrators.SciML(saveat = 0.0:0.25:1.0)
rg = solve(prob, integ_grid)
Integrators.times(rg)
5-element Vector{Float64}:
 0.0
 0.25
 0.5
 0.75
 1.0
julia
julia> Integrators.is_dense(rg)
true

julia> Integrators.evaluate_at(rg, 0.1) ≈ [exp(-0.1)]
true

The solver steps are added to the grid only with an explicit save_everystep = true. With dense = false, SciML's native saving is used and is_dense(r) is false.

Changing the grid afterwards ​

Integrators.regrid returns a copy of a result on another output grid, read from the interpolant without integrating again:

julia
julia> Integrators.times(Integrators.regrid(r, range(0, 1; length = 5)))
5-element Vector{Float64}:
 0.0
 0.25
 0.5
 0.75
 1.0

The grid must contain at least two times, be strictly monotone in the integration direction, and lie in the integration span; otherwise regrid throws IncorrectArgument.

Multi-phase results ​

Integrators.merge combines the consecutive results of a multi-phase integration into a Integrators.PiecewiseIntegrationResult. Each phase keeps its own interpolant, so a dense multi-phase trajectory is as accurate as a single-phase one.

Below, the state jumps by +1 at t = 1:

julia
decay(u, p, t) = -u
r1 = solve(ODEProblem(decay, [1.0], (0.0, 1.0)), integ)
u1 = Integrators.final_state(r1) .+ 1    # the jump
r2 = solve(ODEProblem(decay, u1, (1.0, 2.0)), integ)
rm = Integrators.merge([r1, r2])
julia
julia> Integrators.evaluate_at(rm, 1.0)           # end of the first phase
1-element Vector{Float64}:
 0.36787944125650685

julia> Integrators.evaluate_at(rm, 1.0 + 1e-12)   # second phase, after the jump
1-element Vector{Float64}:
 1.3678794412551387

The merged trajectory is left-continuous at a switching time: at t = 1.0 it returns the end of the first phase (before the jump), and any later time belongs to the second. The switching time appears twice in times(rm). successful(rm) is true only if every phase succeeded, and status(rm) is the status of the first failed phase.

Failures and unsafe ​

By default, an unsuccessful SciML return code throws a SolverFailure. Pass unsafe = true to get the result anyway and check it yourself:

julia
r = solve(prob, integ; unsafe = true)
Integrators.successful(r) ||
    @warn "integration stopped early" Integrators.status(r)

See Error Messages Reference for the error itself.