Tutorial 2: Summaries Instead of Trajectories
A million-run batch that saves full trajectories will exhaust your GPU memory long before it runs out of compute. Usually you do not want the trajectories anyway; you want a number per run, such as the mean, the peak count, or the oscillation amplitude. This tutorial shows how to compute those statistics on the GPU during integration, so the full time series never exists anywhere.
Step 1: A system worth summarising
The same Lotka–Volterra system as Tutorial 1: Your First Parameter Sweep. Its populations oscillate, so per-run statistics are meaningful:
import numpy as np
import cubie as qb
LV = qb.create_ODE_system(
"""
dx = a*x - b*x*y
dy = -c*y + d*x*y
""",
parameters={"a": 0.1, "b": 0.02, "c": 0.3, "d": 0.01},
states={"x": 0.5, "y": 0.3},
name="LotkaVolterra",
)
Step 2: Ask for statistics, not trajectories
output_types controls what is recorded. Skip "state" and
list summary metrics instead:
result = qb.solve_ivp(
LV,
y0={"x": 0.5, "y": 0.3},
parameters={
"b": np.linspace(0.01, 0.05, 20),
"d": np.linspace(0.005, 0.02, 20),
},
method="ode45",
duration=50.0,
output_types=["mean", "max"],
sample_summaries_every=0.5,
)
Summary metrics sample the variables at a fixed sample period,
sample_summaries_every, and calculate and save the summary at
every summarise_every time units. summarise_every can be
thought of as the width of the summary window, and
sample_summaries_every the inverse of a sample rate. With
summarise_every=10.0 and sample_summaries_every=1.0, the
first "mean" is the mean of the measurements at t = 1, 2, …,
10 time-units, the second covers t = 11 through 20, and so on.
If summarise_every is left unset, a single summary is calculated
at the end of the integration (when t == duration). When you’ve
enabled summaries but left summarise_every unset, you still need
to provide a sample_summaries_every period to tell the loop how
often it should stop and measure the states. Shorter windows give a
statistic per window instead, which is handy for tracking slow drift.
There are 18 built-in metrics, including "rms", "std",
"peaks", and first/second-derivative extrema; the full table is
in Working with Results.
Step 3: Read the summaries
Summaries come out as a 3-D array indexed
[window, summary, run]. The friendliest accessor is
as_numpy_per_summary, which splits it into one array per metric,
each indexed [window, variable, run]:
per_metric = result.as_numpy_per_summary
print(per_metric["max"].shape) # (1, 2, 400)
peak_prey = per_metric["max"][0, 0, :] # window 0, x, all runs
mean_prey = per_metric["mean"][0, 0, :]
Metrics that share an accumulator are computed together on the GPU:
requesting both "max" and "min" runs one combined pass over
the data instead of two. This fusion is transparent — the result
keys are always the metric names you requested.
Step 4: Trim what you don’t need
Two more levers cut memory and time further:
result = qb.solve_ivp(
LV,
y0={"x": 0.5, "y": 0.3},
parameters={
"b": np.linspace(0.01, 0.05, 20),
"d": np.linspace(0.005, 0.02, 20),
},
method="ode45",
duration=50.0,
settling_time=20.0, # discard the transient
output_types=["mean", "max"],
sample_summaries_every=0.5,
summarise_variables=["x"], # only summarise prey
)
summarise_variables (and its time-domain sibling
save_variables) restricts recording to the variables you name.
settling_time integrates for 20 time-units before recording
starts, so start-up transients do not pollute your statistics.
Settling extends the run rather than eating into it: the solver
integrates for settling_time + duration in total, so the
recorded window is still the full 50 time-units and a single summary
is saved over the 20-70 time-unit window.
When to use which output
Full trajectories (
"state"): exploring, debugging, and plotting a handful of runs.Summaries: large sweeps, likelihood-free inference, and anything where each run reduces to features. Memory per run drops from
n_saves x n_variablesvalues to a handful.Both at once works too; the two records run at independent cadences (see Timing Parameters).
The next tutorial, Tutorial 3: Stiff Systems, Implicit Methods, and Drivers, covers systems that need implicit methods and time-dependent forcing.