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.
parameter_legendDictionary showing which swept parameter lives in which array index.
fixed_parameter_valuesValues for each fixed parameter used in the solve.
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,
stream, parameter_legend and fixed_parameter_values;
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 (output 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 a summary metric is calculated and saved. If unset, one summary is calculated and saved at the end of the integration.sample_summaries_everycontrols how frequently measurements used to calculate the summary metrics are taken.
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"])