The physics: the Drag-Based Model (DBM)
Once a CME clears the corona, its dominant interaction with the environment is aerodynamic-like drag against the ambient solar wind. The Drag-Based Model assumes the CME’s acceleration is proportional to the square of its velocity relative to the solar wind:
$$
\frac{dv}{dt} = -\gamma ,(v - w),\lvert v - w \rvert
$$
where $v$ is the CME’s speed, $w$ is the (assumed constant) ambient solar wind speed, and $\gamma$ is a drag parameter that lumps together the CME’s cross-section, mass, and the solar wind density. A CME faster than the wind ($v_0 > w$) decelerates toward $w$; a CME slower than the wind gets pushed along.
For the common case $v_0 > w$, this ODE has a closed-form solution — no numerical integration required:
$$
v(t) = w + \frac{v_0 - w}{1 + \gamma (v_0 - w),t}
$$
$$
r(t) = r_0 + w,t + \frac{1}{\gamma}\ln!\big(1 + \gamma (v_0 - w),t\big)
$$
Here $r_0$ is the heliocentric distance where we start tracking the CME (typically ~20 solar radii, the outer edge of a coronagraph’s field of view), and $r(t)$ is its distance from the Sun at time $t$. The arrival time at Earth is the $t$ for which $r(t) = 1\ \text{AU}$ — a transcendental equation with no closed-form inverse, but trivial to solve numerically.
The optimization problem
Given a catalog of $N$ past CME events, each with a known initial speed $v_{0,i}$ (from coronagraph tracking) and a known observed arrival time $T_{\text{obs},i}$ (from in-situ spacecraft detection), we want to find the drag parameter $\gamma$ and ambient wind speed $w$ that best explain the historical record. This is a nonlinear least-squares fit:
$$
\min_{\gamma,, w} ; J(\gamma, w) = \sum_{i=1}^{N} \Big(T_{\text{pred},i}(\gamma, w) - T_{\text{obs},i}\Big)^2
$$
where $T_{\text{pred},i}(\gamma, w)$ is obtained by solving $r(t) = 1,\text{AU}$ for event $i$ under trial parameters $(\gamma, w)$. Once fitted, $(\gamma, w)$ let us forecast the arrival time of any new CME from its initial speed alone.
Why a naive implementation would be slow
The textbook way to evaluate $T_{\text{pred},i}(\gamma,w)$ is to numerically integrate the ODE (e.g. with scipy.integrate.solve_ivp) until an event trigger fires at $r = 1,\text{AU}$. The problem is that a gradient-based optimizer like L-BFGS-B evaluates the objective function dozens to hundreds of times, and each evaluation needs the transit time for every event in the catalog. That’s (optimizer iterations) × (catalog size) separate adaptive-step ODE integrations — for a catalog of a few dozen events and a few hundred optimizer evaluations, that’s easily tens of thousands of solver calls, each with Python-level overhead.
Since the DBM has an exact closed-form solution for $r(t)$ and $v(t)$, we can skip the ODE solver entirely and instead root-find $t$ directly from the algebraic formula using vectorized Newton–Raphson — solving all events in the catalog simultaneously as NumPy arrays, with zero Python-level loops over events during the search. This turns tens of thousands of solver calls into a handful of array operations, which is the version implemented below.
Full source code
1 | import numpy as np |
Code walkthrough
Section 1 — constants and catalog. All distances are worked in kilometers internally to avoid unit juggling inside the physics equations; we only convert to hours or solar radii when it’s time to print or plot. GAMMA_TRUE and W_TRUE exist purely to generate a believable synthetic catalog — in a real deployment you would instead load $v_0$ and observed arrival times from a CME/ICME catalog (e.g. CDAW LASCO CME catalog cross-matched with the Richardson & Cane ICME list) and skip this step entirely.
Section 2 — the analytic model. dbm_distance and dbm_speed implement the closed-form solutions derived above. The np.clip(..., 1e-6, None) inside dbm_distance guards the logarithm against a zero or negative argument in degenerate corner cases (e.g. during the grid search in Section 7, where some (gamma, w) combinations are far from physically realistic). transit_time is the key performance trick: instead of integrating an ODE, it root-finds $t$ such that $r(t) = 1,\text{AU}$ using Newton–Raphson, updating an entire array of events per iteration (t is a NumPy array, one entry per CME). Twenty-five iterations converge to sub-second accuracy given the well-behaved (monotonic, single-root) shape of $r(t)$ here.
Section 3 — synthetic dataset. We compute the “true” transit time for each event and add Gaussian noise (±3 hours, 1σ) to emulate the natural scatter of real in-situ arrival detections.
Section 4 — the optimization itself. This is the heart of the article. sse is the objective function $J(\gamma, w)$ from the formulation above; predict_hours wraps transit_time for convenience. Two details matter here:
- Parameter scaling. $\gamma$ is physically of order $10^{-8}$–$10^{-7}$ while $w$ is of order $10^2$–$10^3$. Handing L-BFGS-B two parameters that differ by eight orders of magnitude produces terrible, ill-conditioned gradient steps. We rescale by working with
gamma_scaled = gamma * 1e7internally, so both parameters sit roughly in $[0, 5]$ and $[250, 600]$ — comparable orders of magnitude. - Multi-start optimization. The loss surface (visualized in Plot 2) isn’t perfectly convex — it has a shallow shelf at high $\gamma$ that a single gradient descent run can get trapped against a parameter boundary. Running L-BFGS-B from six different starting points and keeping the best result is a cheap, standard safeguard against this kind of local-minimum trap, and it reliably recovers parameters close to the ground truth here.
Section 5 — reporting. A plain-text summary of the fitted vs. true parameters, the fit’s RMSE in hours, and a per-event table of observed vs. predicted transit times with residuals.
Sections 6–8 — visualization, covered in detail below.
Visualizing the results
Plot 1 (2D scatter) plots observed vs. predicted transit time for every event, colored by initial CME speed, with a dashed diagonal reference line. Points sitting on the diagonal are well-predicted; the spread around it reflects both the injected noise and any residual model misfit. The color coding is a quick visual check for speed-dependent bias — if fast CMEs systematically fell above the line and slow ones below it (or vice versa), that would suggest the drag model itself is missing some physics rather than just needing better-tuned parameters.

Plot 2 (3D loss landscape) is the most diagnostic figure in the article. It renders $\log_{10}(J(\gamma, w) + 1)$ as a surface over the $(\gamma, w)$ plane, with the fitted optimum marked in red. The log transform is necessary because $J$ grows extremely fast away from the optimum (spanning several orders of magnitude), which would otherwise flatten the interesting region near the minimum into an invisible sliver. The surface shows a clear basin around the true parameters and a rising shelf toward high $\gamma$/low $w$ — exactly the kind of feature that justifies the multi-start strategy from Section 4.

Plot 3 (3D trajectories) shows heliocentric distance vs. time for a subsample of events (evenly sampled across the speed range for readability), using the fitted $(\gamma, w)$. The $y$-axis is initial speed $v_0$, so this is effectively a family of trajectory curves “fanned out” by launch speed — fast CMEs (yellow) climb steeply and reach 1 AU quickly; slow ones (purple) crawl outward and take much longer. This is a useful sanity check that the fitted drag parameter produces physically sensible deceleration/acceleration behavior across the whole observed speed range, not just at the mean.

Console output
=== Drag-Based Model fit result ===
gamma (fitted): 1.8680e-08 km^-1 [true: 2.0000e-08]
w (fitted): 405.28 km/s [true: 400.00]
RMSE : 2.751 hours
Converged : True, iterations: 14
v0 [km/s] Observed [h] Predicted [h] Residual [h]
986.9 48.11 52.19 4.08
1735.9 37.97 38.36 0.39
1451.6 40.54 42.33 1.79
1278.3 47.51 45.36 -2.15
702.8 61.20 63.05 1.86
702.8 59.68 63.06 3.37
575.5 76.20 71.14 -5.06
1626.0 40.38 39.78 -0.60
1281.4 46.71 45.30 -1.41
1420.5 39.80 42.83 3.03
526.8 74.31 75.39 1.08
1760.9 39.69 38.06 -1.63
1582.2 38.20 40.38 2.18
776.0 61.66 59.59 -2.07
736.4 60.49 61.39 0.90
738.4 61.32 61.29 -0.03
895.5 54.27 55.03 0.76
1182.2 54.06 47.32 -6.74
Takeaways
Recasting CME arrival time forecasting as a parameter-fitting optimization problem makes the physics and the numerics cleanly separable: the drag-based model captures the physics, and a standard nonlinear least-squares solver handles the fitting. The biggest practical win here came not from a fancier optimizer, but from replacing an ODE integrator with the model’s own closed-form solution — turning an $O(\text{iterations} \times \text{events})$ integration cost into a handful of vectorized array operations, while a simple multi-start strategy kept the fit from settling into a boundary artifact of the loss landscape. The same catalog-fitting-plus-forecasting workflow generalizes directly to more sophisticated propagation models (e.g. drag plus interplanetary magnetic flux rope deflection) without changing the optimization machinery at all.





















