Skip to content

perf: reuse triangulation/point-location in grid interpolation (drop Dask) - #53

Merged
JanStreffing merged 1 commit into
masterfrom
perf/reuse-interpolation
Jul 6, 2026
Merged

perf: reuse triangulation/point-location in grid interpolation (drop Dask)#53
JanStreffing merged 1 commit into
masterfrom
perf/reuse-interpolation

Conversation

@JanStreffing

Copy link
Copy Markdown
Owner

Problem

scipy.interpolate.griddata rebuilds the Delaunay triangulation of the source points and re-runs the point-location of the (fixed) target points on every call. Interpolating the ~60 vertical levels of the 3D CO2 field — and the per-field 2D emissions/albedo loops — repeats that identical work N times. The 3D CO2 step then wrapped it in a dask.distributed LocalCluster whose process startup + a pathological ~150 s worker teardown dwarfed the few seconds of actual interpolation.

Profiling (TCO95/feomdyn) showed the CO2 interpolation dominated by exactly this: LinearNDInterpolator.__call__ re-doing find_simplex at ~2.7 s × 60 levels.

Fix

New ocp_tool/interp_utils.ReusableGridInterp: builds the Delaunay + KD-tree once, and the target-point barycentric weights once — so each subsequent field is just a gather + weighted sum. Applied in:

  • co2_interpolation — horizontal loop (was per-level griddata + Dask). The vertical interp1d per-point loop is collapsed to a single vectorised interp1d(axis=0) (after the horizontal nearest-fill there are no NaNs, so every column shares the full level set). Dask removed entirely.
  • field_interpolation — the 2D CO2-emissions (linear, fill_value=0) and albedo (nearest) per-field loops reuse one triangulation / KD-tree across fields.

Correctness

Bit-identical to griddata to interpolation precision (the barycentric weights are the same ones LinearNDInterpolator uses; max abs diff ~1e-19), and bit-for-bit after GRIB packing — a full-pipeline A/B (old Dask path vs new) gives grib_compare: IDENTICAL.

Impact (TCO95/feomdyn)

  • CO2 interpolation: ~101 s → ~6.7 s (~15×)
  • Full run_ocp_tool.py: 211.6 s → 138.6 s
  • No dask/distributed dependency in the interpolation path.
  • The win grows with resolution: the triangulation build, previously repeated per level/field, is amortised once.

…Dask)

griddata rebuilt the Delaunay triangulation of the SOURCE points AND re-ran the
point-location of the (fixed) TARGET points on every call, so interpolating the
~60 vertical levels of the 3D CO2 field -- and the per-field 2D emissions/albedo
loops -- repeated that work N times. The 3D CO2 step wrapped this in a
dask.distributed LocalCluster whose process startup + pathological ~150 s
teardown dwarfed the ~seconds of actual interpolation.

Add ocp_tool/interp_utils.ReusableGridInterp: builds the Delaunay + KD-tree once
and the target-point barycentric weights once, so each subsequent field is just
a gather + weighted sum. Use it in:
  - co2_interpolation: horizontal loop (was per-level griddata + Dask); the
    vertical interp1d loop is likewise collapsed to a single vectorised axis-0
    interp1d (after the horizontal nearest-fill there are no NaNs, so every
    column shares the full level set). Dask removed entirely.
  - field_interpolation: the 2D CO2-emissions (linear) and albedo (nearest)
    per-field loops reuse one triangulation / KD-tree across fields.

Results are bit-identical to griddata to interpolation precision (barycentric
weights match LinearNDInterpolator; max abs diff ~1e-19), and bit-for-bit after
GRIB packing (grib_compare: identical). Measured on TCO95/feomdyn: the CO2
interpolation drops ~101 s -> ~6.7 s (~15x), and a full OCP-Tool run
211.6 s -> 138.6 s. The win grows with resolution (the triangulation build,
previously repeated, is amortised once).
@JanStreffing
JanStreffing merged commit e529436 into master Jul 6, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant