Skip to content

Hybrid slip correlation lengths: per-model noise families - #39

Merged
lispandfound merged 11 commits into
masterfrom
claude/hybrid-correlation-lengths-rsn8n2
Oct 29, 2026
Merged

lispandfound merged 11 commits into
masterfrom
claude/hybrid-correlation-lengths-rsn8n2

Conversation

@lispandfound

Copy link
Copy Markdown
Contributor

Summary

Adds depth-dependent ("hybrid") slip correlation lengths, after Graves & Pitarka's SCEC 2026 poster GM-218 and genslip v5.6.3 (SCEC BBP dev branch). Shallow slip can use long correlation lengths (Suzuki et al. 2022) and deep slip short ones (Mai & Beroza 2002), crossfaded between two depths. It also supports any number of families.

GP blends two fields linearly, so the latent variance drops in the transition, to 0.845 at Mw 7 with shared noise and 0.5 with independent noise. This PR blends in quadrature instead:

  • Every family filters one shared white noise, embedded on a common padded grid.
  • Families are crossfaded at equal power: loading sin(πw/2), normalised by the exact cross-correlations between families. The latent field is therefore standard normal at every cell, and the marginal transform stays exact.

The structure was settled through two architecture reviews, a devil's-advocate pass, /simplify and /code-review. Backwards compatibility was explicitly not a constraint.

Changes

  • sampling/field.py: one Sampler type for any number of covariances. sampler(grid, covariances, weights=None) embeds them at the widest padded shape and draws all of them from one seed. The stationary case is one covariance. The lru_cache on sampler is gone.
  • rupture/models.py:
    • NoiseFamily(relation, hurst, weight), with Noise = tuple[NoiseFamily, ...] and STATIONARY as the default noise.
    • embed(noise, geometry, magnitude, marginal) pre-corrects each family for the marginal, reads the weights at cell centres, and caches the result.
    • DrawnSlip.noise, GaussianRake.noise and CorrelatedPerturbation.noise let each model choose its noise. Making rake hybrid is a config change. By default rake stays stationary and the perturbation uses slip's noise, which matches genslip's slip and rake. genslip keeps the perturbation's own part stationary; set perturbation.noise to STATIONARY to reproduce that.
    • SlipField.sampler replaces latent_covariance. Rise time and the onset perturbation draw their independent part from slip's built sampler, so nothing is embedded twice.
  • rupture/source.py:
    • SegmentSource is now a moment and a rake, with a magnitude property.
    • CorrelationRelation gains strike_saturation and dip_saturation, for SEA22's min(Mw, 6.3).
    • New SUZUKI_2022 relation.
  • config.py: [[slip.noise]], [[rake.noise]] and [[perturbation.noise]] replace [source.correlation] and [segments.<name>.correlation]. A segment's noise list replaces the global one whole. Profile depth/value lists are now tuples, so profiles are hashable.
  • Examples and README are updated to match.

Breaking changes (intentional)

  • Configs with [source.correlation] no longer load. The lengths now live on each model's noise.
  • SegmentSource.covariance, SlipField.latent_covariance, segment_source's correlation arguments, and sampler.cache_clear() are removed. The cache is now embed.cache_clear().
  • Draws differ slightly from before, because stationary fields are now normalised exactly for clipped eigenvalues.

Example

[[slip.noise]]   # shallow: Suzuki et al. (2022)
relation = { strike_offset = 1.67, dip_exponent = 0.5, dip_offset = 1.69, dip_saturation = 6.3 }
weight = { type = "ramp", centre_km = 6.5, half_width_km = 1.5, shallow = 1.0, deep = 0.0 }

[[slip.noise]]   # deep: Mai & Beroza (2002), the default relation
weight = { type = "ramp", centre_km = 6.5, half_width_km = 1.5, shallow = 0.0, deep = 1.0 }

Testing

  • pytest tests: 247 passed, 1 skipped.
  • Doctests, ruff and ty pass; ty only reports the existing unresolved rerun import.
  • pytest benchmarks/test_kernels.py --benchmark-disable passes.
  • New tests/test_blend.py covers:
    • saturation;
    • the shared padded shape;
    • a cell under one family equalling that family's own draw;
    • empirical unit variance and independent real and imaginary parts through the crossfade;
    • weight validation;
    • slip versus rake noise ownership;
    • the perturbation's own noise;
    • config round trip and segment overrides.

Not in this PR

  • GP's depth-dependent along-strike side taper (taper_slip_all_r_hc).
  • A Rust kernel that generates the white noise once for all K transforms.
  • Draws with several families cost K transforms on the largest family's padded grid. At Mw 7 with SEA22 that is about 23× the stationary MB02 cost.

Generated by Claude Code

DrawnSlip gains an optional tuple of NoiseFamily, each a stationary von
Karman covariance (its own relation and Hurst exponent, or the source's)
and a weight over position. Slip's latent then comes from a Blend: every
family filters one white noise on a shared padded grid, crossfaded at
equal power and normalised by the exact cross-correlations, so the latent
stays standard normal at every cell. Rise time and the onset perturbation
draw their independent part from the same Blend; the perturbation can keep
the source's stationary covariance instead, as genslip v5.6.3 does. Rake
is unchanged.

CorrelationRelation gains saturation magnitudes, and SUZUKI_2022 is
Suzuki et al. (2022)'s relation, the shallow family of Graves & Pitarka's
hybrid. Configs take the families as [[slip.noise]].

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrujMjfczwcCciJwtErEbs
A Noise is a tuple of NoiseFamily (correlation relation, Hurst exponent,
weight over position). DrawnSlip, GaussianRake and CorrelatedPerturbation
each take one, so making rake hybrid is a config change. embed() builds a
noise on a chart, pre-corrected for a marginal, and caches it; SlipField
carries the built sampler, so rise time and the onset draw their
independent part from it without embedding again.

sampling.sampler() now embeds one or more covariances at a shared padded
shape and returns one Sampler type; the stationary case is one covariance.
Blend, BlendedSampler, Covariance.sampler and the sampler lru_cache go.
SegmentSource is a moment and a rake; [source.correlation] and its
per-segment override are replaced by [[<model>.noise]] lists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrujMjfczwcCciJwtErEbs
…comment

From /code-review: sampler() refuses weights that don't match the
covariances and the grid with a RuptureGeneratorError, and is the one
place that checks their range; embed() only asks the weights to be
finite. colombia.toml no longer claims to spell out the relation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrujMjfczwcCciJwtErEbs
@github-actions

github-actions Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Benchmarks

31f9a28 against base cafe752, on AMD EPYC 9V45 96-Core Processor. Times are medians over the rounds. The median change is 2.3%; changes past 5% are in bold.

Kernels

kernel base head change head IQR rounds padded cells
embed[Mw6.5] 28.2 ms 27.7 ms -1.8% 260 µs 35 448,000 (+0.0%)
circulant_draw[Mw6.5] 2.61 ms 2.68 ms +2.8% 250 µs 290
embed[Mw7.5] 88.3 ms 88.2 ms -0.1% 1.03 ms 9 1,204,224 (+0.0%)
circulant_draw[Mw7.5] 7.48 ms 6.55 ms -12.4% 1.27 ms 150
eikonal_solve 950 ms 937 ms -1.4% 13.2 ms 5
synthesise_pulses[impulse] 373 µs 364 µs -2.5% 17.5 µs 523
synthesise_pulses[liu] 430 ms 414 ms -3.8% 5.34 ms 5

End to end

Each round is the command line in a fresh interpreter, start-up included. Peak RSS is the largest the child process reached over the rounds.

example base time head time change base peak RSS head peak RSS change
alpine_hope 14.1 s 13.2 s -6.2% 445 MiB 371 MiB -16.6%
beavan 3.63 s 3.43 s -5.6% 440 MiB 396 MiB -10.0%
colombia 2.83 s 2.79 s -1.4% 1,124 MiB 1,128 MiB +0.4%
crustal 827 ms 810 ms -2.0% 188 MiB 190 MiB +0.6%
landers 3.27 s 3.08 s -5.7% 797 MiB 806 MiB +1.1%
northridge 1.07 s 1.05 s -1.4% 349 MiB 349 MiB -0.0%
two_faults 1.28 s 1.3 s +1.7% 261 MiB 266 MiB +1.9%
two_faults_segments 995 ms 961 ms -3.4% 199 MiB 201 MiB +1.0%
venezuela 6.23 s 6.17 s -0.9% 2,412 MiB 2,345 MiB -2.8%

claude and others added 3 commits October 28, 2026 18:46
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013rXuJ2sXciYcySSp6QeYrE
GP_HYBRID is genslip v5.6.3's default hybrid: Suzuki et al. (2022) above
5 km and Mai & Beroza (2002) below 8 km, crossfaded over 6.5 +- 1.5 km,
both at the default Hurst exponent (kfilt_gaus2 uses H = 0.75 for both).
DrawnSlip and [slip] take it by default. The onset perturbation now
defaults to STATIONARY, as genslip draws tsfac1 at the stationary
lengths; rake stays stationary.

from_realisation.py passes the speed profile's depths as a tuple, which
ty requires.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
lispandfound and others added 5 commits October 29, 2026 11:54
genslip filters its spectrum with (1 + k^2 a^2), k in cycles per km,
where the von Karman correlation C(r/a) has the spectrum
(1 + 4 pi^2 k^2 a^2), so genslip's patches are those of a length 2 pi
shorter than it states. GENSLIP_MAI_BEROZA and GENSLIP_SUZUKI_2022 are
the published relations with both offsets grown by log10(2 pi); the
defaults (GP_HYBRID, STATIONARY, and an absent [relation] table) use
them, so the generator draws exactly the patches genslip v5.6.3 does.
MAI_BEROZA and SUZUKI_2022 keep the published lengths.

The SRF round-trip test now expects a point whose pulse has no samples
to read back as not slipping, as the writer records it; the rougher
default slip draws such a point in the test scenario.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013rXuJ41EEqxDJaXNE3Zubd
@lispandfound
lispandfound merged commit cdc6f7d into master Oct 29, 2026
9 checks passed
@lispandfound
lispandfound deleted the claude/hybrid-correlation-lengths-rsn8n2 branch October 29, 2026 00:57
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.

2 participants