Skip to content

Refactor IM calculation for lazy calculation and faster RotD - #217

Open
lispandfound wants to merge 6 commits into
masterfrom
no_parallel
Open

Refactor IM calculation for lazy calculation and faster RotD#217
lispandfound wants to merge 6 commits into
masterfrom
no_parallel

Conversation

@lispandfound

@lispandfound lispandfound commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

As title describes. Reasonably large refactor aimed to address a few things:

  • Hodge-podge pandas dataframes and xarray dataset returns. Standardises now on xarray outputs because...

  • ...IM calculation is now dask aware and can hence process larger-than-memory datasets by chunking, a strategy employed in workflow upstream.

  • RotD calculation significantly speed up via the use of a clever convex hull trick to optimise the calculation of RotD.

  • RotD calculation now also returns the orientation of the obtained RotD values. Currently RotD50

  • Outstanding: Determine a sensible resolution to the orientation question for RotD50 in particular (and also RotD0, RotD100).

RotD calculations

The RotD calculations for all intensity measures are significantly speed-up over the previous implementation (and to my knowledge, any implementation globally) by pre-processing the waveforms prior to calculation. The results are a significant speed-up, 100x speed increase in RotD calculations in single threaded workloads. Given the dominance of RotD calculations in the time to run IM calc this represents a hopefully significant improvement for updates in NZGMDB. The result is verified against the naive algorithm in rust using property tests against the brute force method. A tabulation of speed-ups against the baseline.

The core observation is related to the Fundamental Theorem of Linear Programming: if we treat the RotD problem as a linear program to maximise cos(theta) * 000 + sin(theta) * 090 for a fixed theta, then we get for free that our maxima must lie on the convex hull of the waveforms in 2D. Because the convex hull is invariant of theta we can re-use the same hull for every angle, and so reduce the complexity of the problem from $180 \times N_t$ to $180 \times H$ where $H$ is the number of points in the hull. Typically $H << N_t$, of the order of 16 vs tens of thousands of timesteps which explains the speed-up. Here is a claude diagram of the process that illustrates the algorithm in action.

image
record nt brute hull speedup survivors
2024p220113_RLNS acceleration 52803 26.4 ms 158 µs 167x 0.5%
2024p220113_RLNS pSA T=1s 52803 26.1 ms 252 µs 104x 4.0%
3366146_DFHS acceleration 19707 9.73 ms 64 µs 151x 0.8%
3366146_DFHS pSA T=1s 19707 9.72 ms 133 µs 73x 9.5%
3497857_HVSC acceleration 9271 4.56 ms 32 µs 142x 0.4%
3497857_HVSC pSA T=1s 9271 4.57 ms 44 µs 104x 5.5%
3497857_PARS acceleration 12200 6.06 ms 48 µs 126x 3.4%
3497857_PARS pSA T=1s 12200 6.00 ms 58 µs 104x 5.3%
3528839_LPCC acceleration 7144 3.50 ms 28 µs 124x 2.1%
3528839_LPCC pSA T=1s 7144 3.51 ms 35 µs 101x 3.1%
nquist_freq acceleration 22201 10.9 ms 70 µs 157x 0.4%
nquist_freq pSA T=1s 22201 11.0 ms 78 µs 140x 2.0%

lispandfound added a commit that referenced this pull request Sep 11, 2026
Every failure the shared workflow reports on the previous commit, plus the
four that `no_parallel` was already failing before any of this.

**uv.lock.** The lock pinned numba 0.53.1 (March 2021), whose llvmlite 0.36
refuses to build on anything past Python 3.9, so `uv sync` died in deptry,
pytest and typecheck alike. That was the whole of the three Python job
failures on #217. Relocked, and — as asked — the rest of the lock is
upgraded along with it, dev tooling included: ruff 0.16.1 to 0.16.7, ty
0.0.66 to 0.0.80, hypothesis, coverage, qcore-utils 2026.6.1 to 2026.8.2,
numpy 2.5.1 to 2.5.3, and the transitive set behind them.

**.python-version.** uv picked the interpreter off the runner before; the
retired test.yml had pinned 3.13 through setup-python, and the shared
workflow has no such step. Pinning it here keeps CI and a local checkout on
the same interpreter.

**ruff format.** The old ruff.yml ran `ruff check` only, so two files had
never been through the formatter.

**cargo fmt.** Likewise never run. Edition 2024 sorts `use` groups
lowercase-last and no longer indents trailing comments to the previous
expression.

**deptry.** dask is declared but never imported: xarray dispatches to it
when `apply_ufunc(..., dask="parallelized")` is handed a dask-backed array,
which is what the `lazy` extra is for. Recorded as a DEP002 ignore.

**ty.** The suppression on the invalid-shape test no longer matches any
diagnostic, and ty now reports the dead directive.

**numpydoc.** Seven kernel helpers in IM/ims.py and one in
IM/snr_calculation.py had a summary line and nothing else. Written out in
the style the neighbouring `_rotd_kernel` and `_components` already use.

**lefthook.yml**, adapted from ucgmsim/nzcvm, so none of the above has to be
found by CI again. Three changes to that config beyond porting it:

- Split into pre-commit and pre-push. Upstream runs everything on pre-push
  against {staged_files}, but nothing is staged at push time: those commands
  report "no files for inspection" and skip, while the two that do not
  reference the template (clippy, rustfmt) run over the whole crate on every
  push. Autofix now runs on pre-commit over {staged_files}, where
  stage_fixed can do something, and the gates run on pre-push over
  {push_files}.
- Added ruff-format, numpydoc and rustfmt. The shared workflow gates on all
  three and the nzcvm config predates that; between them they cover most of
  what is fixed above.
- Fixed the yaml glob. `*.{yaml, yml}` has a space inside the alternation, so
  the second branch only ever matched a file literally named `* yml` and the
  hook never fired on a .yml. Now `*.{yaml,yml}`.

The vale command from the nzcvm config is deliberately left out — it lands
with the vale config itself in the next PR, rather than pointing at a
.vale.ini that does not exist yet.

**.yamllint.yml**, because that yaml hook does now fire, and yamllint's
defaults flag things Actions requires: no `---` document start, `on:` as a
mapping key, and long `${{ }}` expressions.

**.gitignore**, which had no entry for target/, *.egg-info/, .coverage,
htmlcov/, __pycache__/ or any of the tool caches. With hooks operating on
staged files, `git add -A` picking those up is a live hazard.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@lispandfound

Copy link
Copy Markdown
Contributor Author

NB: ruff, ty, etc note passing, see the PRs on top of this stack that migrate the CI workflow to the meta action and get it to pass. I have locally run tests and they pass.

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