← All findings

Contents

F0003: Geodesic interpolation in mepd runs about 2x faster with a numba least-squares kernel, with bit-identical paths

Status: verified · Version 1 · Jan Estrada Pabon (ORCID) · 2026-10-15 · no DOI yet

Finding. On 30 benchmark reactions (3–56 atoms; 60 calls at 12 and 30 images), mepd's geodesic interpolation with the numba kernel produced bit-identical paths in all 60 calls and ran 2.1 times faster in total (26.0 → 12.3 s); the per-call median gain was 2.4 times, falling to 1.5 times for the one 56-atom reaction.

Background

To find how a reaction happens, path-search methods such as the nudged elastic band and the growing string method start from a guess of the path between reactant and product. mepd makes that guess with geodesic interpolation (Zhu, Thompson and Martínez, 2019): it finds the shortest path between the two structures in a space where bonds stretch and bend smoothly, so atoms don't collide halfway.

Each geodesic path comes from a least-squares fit, solved with SciPy. A search may call it many times; GSM with geodesic node placement calls it at every step that adds or moves a node. The question here: can that solve be made faster without changing a single path?

Approach

The least-squares solver SciPy uses for this problem was rewritten step by step, with its inner loop compiled by Numba and doing the same floating-point operations in the same order. The matrices it needs are now assembled with array operations instead of Python loops.1

Both versions of mepd interpolated the same 30 reactions, 15 from a geodesic benchmark dataset and 15 from Reaction-QM, at 12 and at 30 images: 60 calls each.2 The two versions ran alternately, three rounds each, on one thread, and every path was compared coordinate by coordinate.3

Results

All 60 paths were identical, to the last bit, between the two versions and across all three rounds.

Before With numba
All 60 calls (median of 3 rounds) 26.0 s 12.3 s
Median call 0.38 s 0.15 s

Overall the interpolation ran 2.1 times faster. Per call the gain had a median of 2.4 times, ranging from 1.3 to 3.4 times. It shrinks with molecule size: the median was 2.6 times for the 18 reactions with up to 15 atoms and 2.1 times for the larger ones. For the single 56-atom reaction it was 1.5 times at 12 images and 1.3 times at 30. Longer paths gained less too: 2.5 times at 12 images, 2.0 times at 30.4

Time per call before and with numba, and the gain against molecule size

Details

1. What changed in the code

mepd/geodesic_interpolation2/_fast_lsq.py re-implements the path SciPy's least_squares takes here (method trf, no bounds, lsmr trust-region solver, callable sparse Jacobian), with the LSMR iterations in a Numba-compiled kernel. Other options, or a missing Numba, fall back to scipy.optimize.least_squares. The Jacobians are now assembled vectorized, directly in compressed sparse row form, and one Jacobian evaluation used only for a sparsity pattern SciPy ignores with a callable Jacobian was dropped. The kernel relies on private SciPy helpers (scipy.optimize._lsq.common); the mepd test test_geodesic_fast_lsq compares it with SciPy.

2. Which reactions

From each source set (66 Geodesic-set reactions whose reference TS survives at g-xTB; a 500-reaction Reaction-QM subset), reactions whose start and end differ by 1–4 bonds were sorted by atom count and 15 were taken at evenly spaced ranks (reproduce/original_run/select_set30.py). The set covers sizes evenly in rank, not in atoms: 18 of 30 have at most 15 atoms and only one has more than 26. All are neutral singlets with H, B, C, N, O, F, Si, P, S and Cl. The set was built to benchmark GSM variants; here it only supplies endpoints.

3. How it was timed

reproduce/time_geodesic.py calls mepd.chainhelpers.run_geodesic with only the number of images, so the interpolation's own defaults apply (friction 0.01; mepd's path methods usually pass 0.001).5 Calls ran one after another in one process with OMP_NUM_THREADS=1, after one untimed warm-up call (Numba compiles on first use; that compile time is excluded). Rounds alternated before, after, before, … in separate processes; totals varied by at most 6.2% between rounds of the same version. The machine was a shared laptop (Intel Core Ultra 9 290HX Plus, 24 logical CPUs) running other jobs, at a load average of about 8–10; absolute times depend on that, the ratio less so. Paths are compared as the exact decimal text of every float64 coordinate.

4. Size dependence

The gain falls with atom count and with images, so the totals here are weighted toward small molecules. Why it falls was not investigated; molecules above 26 atoms are represented by one reaction.

5. Where this holds

Level of theory. None; geodesic interpolation is a geometric construction with no energies.

Software.

Chemical domain.

Settings the result depends on.

Tested on 30. Per source set, reactions with 1–4 bond changes between start and end, sorted by atom count, 15 taken at evenly spaced ranks. This follows the size distribution of the source sets, which is weighted to small molecules: one reaction above 26 atoms.

Not tested.

Reproducibility: statistical. Paths: identical to data/paths.xyz.gz within 1e-8 Bohr per coordinate (bit-identical on the original machine). Timing: total speed-up at least 1.5x on a rerun (reproduce/quick_check.py); absolute times depend on the machine and its load.

6. Data

File Size sha256 What it is
data/reactions.csv 1 kB 6ed8bff6b065… The 30 reactions.
data/endpoints.xyz.gz 20 kB 5f4dfa31ec72… Multi-frame XYZ (Angstrom): start and end of each reaction. Comment line: idx, reaction_id, role (start | end), charge=, mult=.
data/paths.xyz.gz 0.5 MB e64801224054… Every image of the 60 interpolated paths (Bohr, as mepd holds them; exact float64 decimal text), from the first "after" round. Comment line: idx, nimages=, image=.
data/timings.csv 32 kB 3e6e8c385a0f… One row per call, version and round.

data/reactions.csv columns:

data/timings.csv columns:

check.py recomputes all 23 numbers in this report from these files and compares them with expected_numbers.json.

7. Reproducing it

Software. Python 3.12.14, numpy 2.5.3, scipy 1.18.1, numba 0.68.0, llvmlite 0.50.0 (mepd's uv environment).

mepd version. Git commit b366826, the start of the commit hash that identifies this exact version of the mepd code (git checkout b366826).

mepd version. Git commit 1b8d63f, the start of the commit hash that identifies this exact version of the mepd code (git checkout 1b8d63f).

Cost. About 4 minutes for all six rounds; the quick check about a minute.

Download. F0003-v1.zip holds everything below: the data and the scripts.

File What it is
check.py Recomputes every number in this report from data/. Needs only Python 3.
reproduce/quick_check.py Reruns both versions once on all 60 calls, checks every path against data/paths.xyz.gz and reports the speed-up. About a minute.
reproduce/time_geodesic.py Times one version on all 60 calls and writes its paths.
reproduce/build_tables.py Builds data/timings.csv and data/paths.xyz.gz from six time_geodesic.py runs.
reproduce/figures.py Redraws the figure. Needs matplotlib.
data/endpoints.xyz.gz Start and end structures of the 30 reactions, the only input.
reproduce/original_run/select_set30.py How the 30 reactions were picked from the two source datasets.

Check the numbers (a few seconds):

python check.py

Rerun. This needs two mepd checkouts, at commit 1b8d63f (before) and b366826 (with numba). mepd is not public yet. Use one Python environment with mepd's dependencies, including numba.

MEPD_BEFORE=/path/to/mepd-1b8d63f MEPD_AFTER=/path/to/mepd-b366826 python reproduce/quick_check.py

# the full timing, three alternating rounds (about 4 minutes):
for r in 1 2 3; do for v in before after; do
  PYTHONPATH=/path/to/mepd-$v OMP_NUM_THREADS=1 python reproduce/time_geodesic.py $v$r rerun
done; done
python reproduce/build_tables.py rerun

The paths should match data/paths.xyz.gz exactly on the same software versions. The times depend on the machine and what else it is running; compare the ratios.

Reproductions.

8. Provenance

Reconstructed: written up after the run; some of it is inferred. This is a timing script, not an mepd command, so mepd's provenance.json was not written. Code commits (both clean), package versions, CPU and run order were recorded at run time in provenance/run_environment.json. The machine load is approximate (load average read before and after the runs). The reaction selection script was recovered from the session that ran it on 2026-10-13.

9. Sources

Cited.

Data this builds on.

Software used.