Working with Results
Every call to solve_ivp() or
solve() returns a
SolveResult.
The SolveResult Object
Key attributes:
time_domain_array3-D NumPy array with shape
(n_time_points, n_variables, n_runs).summaries_array3-D NumPy array with shape
(n_summary_windows, n_summaries, n_runs)— one row per summary window.time1-D array of time values corresponding to the first axis of
time_domain_array.iteration_counters3-D integer array with shape
(n_time_points, 4, n_runs). The four channels are, in order: Newton iterations, Krylov iterations, accepted steps, and rejected steps since the previous save point.status_codes/status_messagesPer-run integration status.
status_codesis anint32array of bit flags (one per run,0means success);status_messagesdecodes each into a list of human-readable flag names such asMAX_NEWTON_ITERATIONS_EXCEEDED.time_domain_legendDictionary mapping column indices to variable names.
summaries_legendDictionary mapping row indices to summary labels.
streamThe kernel’s memory-manager stream the solve ran on — every launch and transfer for a solver uses this one stream. Work queued on it executes in order after the solve, so a follow-up job can be enqueued without a device-wide synchronisation.
Convenience accessors:
result.as_numpy— returns a dict of NumPy arrays.result.as_pandas— returns a dict of pandas DataFrames.result.as_numpy_per_summary— splits summaries by metric type.
Device-Resident Results
A normal solve copies the output arrays from the GPU to the host
before returning. For workflows that never read the outputs on the
host — benchmarking loops, or GPU-resident pipelines that feed solver
output straight into further device work — on_device=True skips
that copy and returns a
DeviceSolveResult of
device-array handles:
result = solver.solve(y0, params, on_device=True)
result.stream.synchronize() # results valid after this
device_state = result.state # GPU array, no host copy made
The result carries state, observables, state_summaries,
observable_summaries, iteration_counters, status_codes,
and stream; inactive outputs are None. The result is a pure
handle container — it performs no stream or memory operations
itself. Points to note:
The solve does not synchronise before returning. The handles are safe to pass to work queued on
result.streamimmediately; reading them from the host requires synchronising that stream first, then copying (e.g.result.state.copy_to_host()). If you want host results, a normal host solve is the better tool.The handles are views of the solver’s reusable output buffers: the next
solve()on the same solver overwrites them.Host-side post-processing (legends, NaN masking of failed runs) is skipped.
The batch must fit in a single chunk. A run that chunks along the run axis raises
ValueError— host arrays are the stitch target for chunked transfers.Device results need a live
Solver;solve_ivp()closes its temporary solver before returning, so it has noon_deviceoption.
Initial values and parameters may likewise be passed as device
arrays (CuPy or Numba) in (n_variables, n_runs) layout, matching
the system precision. They are wired directly into the kernel with
no host round-trip, which keeps a solve → post-process → solve
pipeline entirely on the GPU.
Output Types
Control what is saved with the output_types list (or via
output_settings on the Solver):
"state"Time-domain state trajectories.
"observables"Time-domain observable trajectories.
"time"Time point array.
"iteration_counters"Newton/Krylov iteration counts and accepted/rejected step counts per save interval.
Any summary metric name (e.g. "mean", "max", "peaks") adds
that metric to the output.
Time-Domain vs Summary Output
Time-domain saves and summary metrics operate at independent cadences:
save_everycontrols how often state snapshots are written.summarise_everycontrols how often summary metrics are calculated and saved.sample_summaries_everycontrols how often measurements are accumulated into the summary metric buffers.
Using summaries lets you extract statistics (mean, max, peaks, etc.) without ever writing the full trajectory to VRAM.
Built-in Summary Metrics
Metric |
Description |
|---|---|
|
Time-averaged value. |
|
Standard deviation over time. |
|
Root-mean-square value. |
|
Maximum value. |
|
Minimum value. |
|
Both max and min. |
|
Maximum absolute value. |
|
Positive peaks (local maxima). |
|
Negative peaks (local minima). |
|
Maximum of the first derivative. |
|
Minimum of the first derivative. |
|
Both max and min of the first derivative. |
|
Maximum of the second derivative. |
|
Minimum of the second derivative. |
|
Both max and min of the second derivative. |
|
Mean and standard deviation combined. |
|
Standard deviation and RMS combined. |
|
Mean, standard deviation, and RMS combined. |
Requesting a full set of constituent metrics that share running sums
(for example ["max", "min"] or ["mean", "std", "rms"]) is
automatically computed using the matching combined metric internally,
but each result is still reported under its requested name ("max",
"min", and so on) — the fusion is a transparent performance
optimisation, not a change to the reported legend.
Selecting Variables to Save
By default all states and observables are saved. To reduce memory and improve speed, select only the variables you need:
result = qb.solve_ivp(
system,
y0=y0,
parameters=params,
method="dormand-prince-54",
duration=10.0,
save_variables=["x"],
summarise_variables=["x", "y"],
output_types=["state", "mean", "max"],
)
Iteration Counters and Run Status
Requesting "iteration_counters" in output_types records solver
effort at every save point — useful for spotting runs where an
implicit algorithm is struggling:
counters = result.iteration_counters
newton_iters = counters[:, 0, :] # per save point, per run
krylov_iters = counters[:, 1, :]
accepted_steps = counters[:, 2, :]
rejected_steps = counters[:, 3, :]
Separately, every result carries a per-run status word. A run that hit a solver limit is flagged there:
failed = result.status_codes != 0
print(result.status_messages) # e.g. ["MAX_NEWTON_ITERATIONS_EXCEEDED"]
See Nonlinear Solvers for background on the Newton solver.
Example: Requesting Summaries
import cubie as qb
import numpy as np
# ... (system definition omitted for brevity)
result = qb.solve_ivp(
system,
y0=y0,
parameters=params,
method="dormand-prince-54",
duration=50.0,
output_types=["mean", "std"],
summarise_variables=["x"],
)
per_summary = result.as_numpy_per_summary
print("Mean of x:", per_summary["mean"])
print("Std of x:", per_summary["std"])