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
    """,
    constants={"a": 0.1, "c": 0.3},
    parameters={"b": 0.02, "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"],
    summarise_every=50.0,
)

Summaries run on two clocks. summarise_every sets the window length: each metric produces one value per window per variable. sample_summaries_every sets the measurement cadence inside each window: the metric is computed from the trajectory sampled at that interval, and it defaults to a tenth of the window length. 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.

Here summarise_every=50.0 makes one window spanning the whole run, so each metric reduces to a single number per variable per run. Shorter windows give a statistic per window instead, which is handy for tracking slow drift. If you omit summarise_every, CuBIE defaults to one whole-run window but warns you, because the derived timing forces a recompile whenever duration changes.

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"],
    summarise_every=50.0,
    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 summarise_every=50.0 produces exactly one summary window per run.

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_variables values 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.