Skip to content

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

Open
lispandfound wants to merge 3 commits into
masterfrom
claude/hybrid-correlation-lengths-rsn8n2
Open

lispandfound wants to merge 3 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

Copy link
Copy Markdown
Contributor

Benchmarks

3736a8c against base 36a8c4a, on AMD EPYC 9V45 96-Core Processor. Times are medians over the rounds. The median change is 2.8%; changes past 5% are in bold.

Kernels

kernel base head change head IQR rounds padded cells
embed[Mw6.5] 26.1 ms 26.3 ms +1.0% 472 µs 34 448,000 (+0.0%)
circulant_draw[Mw6.5] 2.36 ms 2.52 ms +6.6% 221 µs 282
embed[Mw7.5] 84 ms 84.5 ms +0.6% 1.39 ms 12 1,204,224 (+0.0%)
circulant_draw[Mw7.5] 5.54 ms 5.73 ms +3.4% 471 µs 158
eikonal_solve 909 ms 939 ms +3.3% 21 ms 5
synthesise_pulses[impulse] 358 µs 359 µs +0.5% 18.1 µs 1307
synthesise_pulses[liu] 322 ms 331 ms +2.7% 8.96 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 6.24 s 6.21 s -0.5% 294 MiB 301 MiB +2.3%
beavan 2.26 s 2.31 s +1.9% 232 MiB 234 MiB +0.8%
colombia 5.9 s 5.99 s +1.6% 3,170 MiB 3,175 MiB +0.2%
crustal 918 ms 975 ms +6.2% 246 MiB 247 MiB +0.5%
landers 1.84 s 1.9 s +3.6% 386 MiB 389 MiB +0.8%
northridge 889 ms 916 ms +3.0% 289 MiB 290 MiB +0.6%
two_faults 1.58 s 1.64 s +3.9% 355 MiB 357 MiB +0.6%
two_faults_segments 920 ms 968 ms +5.3% 191 MiB 193 MiB +1.2%
venezuela 12.1 s 11.8 s -2.0% 5,768 MiB 5,789 MiB +0.4%

This branch has not been deployed

No deployments
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