Skip to content

Port simple_paleobathymetry (Steps 1-4) and its distance/sediment engine into gplately - #449

Open
michaelchin wants to merge 12 commits into
masterfrom
444-simple-paleobathymetry
Open

Port simple_paleobathymetry (Steps 1-4) and its distance/sediment engine into gplately#449
michaelchin wants to merge 12 commits into
masterfrom
444-simple-paleobathymetry

Conversation

@michaelchin

@michaelchin michaelchin commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements #444 and #445 together (they turned out to be tightly coupled - see the discussion on #444).

gplately.paleobathymetry (#444) - Steps 1, 3, 4 of EarthByte's simple_paleobathymetry workflow, plus an orchestrator:

  • age_to_basement_depth() - Step 1: seafloor age -> basement depth (gdh1/rhcw18/parsons_sclater/crosby09). Ships the RHCW18 lookup table.
  • dutkiewicz_2017_sediment_thickness() - Step 3: Dutkiewicz et al. (2017) sediment-thickness polynomial fit.
  • sediment_isostatic_correction() / paleobathymetry() - Step 4: Sykes (1996) isostatic correction, combined into final paleobathymetry.
  • simple_paleobathymetry() - runs Steps 1-4 end to end, calling into gplately.sediment_thickness for Step 2.

gplately.sediment_thickness (#445) - a port of predicting-sediment-thickness's ocean_basin_proximity.py engine:

  • generate_distance_grids() - Step 2: reconstructs each ocean point backward through time and computes its lifetime-mean distance to the nearest proximity feature (e.g. passive-margin COB line segments), using pygplates.TopologicalModel and gplately's own ptt.utils.proximity_query.
  • generate_sediment_thickness_grids() - Step 3's grid-level driver, composing the above with dutkiewicz_2017_sediment_thickness().
  • Exposed on the CLI: gplately generate-distance-grids / generate-sediment-grids (aliases gdg/gsg).

Validation

  • Steps 1, 3, 4 numerically cross-checked against the original workflow's own reference code (diffs at floating-point noise, ~1e-12).
  • Step 2 + the full simple_paleobathymetry() pipeline run end-to-end against a real plate model (Müller et al. 2025, via the Plate Model Manager) with a downloaded age grid: produces physically sensible distance-to-margin (0-3000 km) and paleobathymetry (-2 to -5.5 km) values.
  • CLI (generate-distance-grids -> generate-sediment-grids) smoke-tested end-to-end against the same real data.
  • Unit tests added for all of the above (test_9_paleobathymetry.py, test_10_sediment_thickness.py), using the existing Muller2019 test fixtures.

Deliberately out of scope

  • Continent-obstacle routing in Step 2 (shortest path around continents, via the original's shortest_path.py) - distances are currently always great-circle. This is a real difference from simple_paleobathymetry's default config (use_continent_obstacles: true).
  • Topological proximity features - only static/non-topological proximity features (e.g. COB line segments) are supported.
  • Step 5 (pyBacktrack merge) - tracked separately in Integrate pyBacktrack: merge present-day and subducted paleobathymetry grids into complete grids #447.

All flagged in the relevant module docstrings.

Test plan

  • python -m pytest -vv tests-dir/pytestcases/test_9_paleobathymetry.py tests-dir/pytestcases/test_10_sediment_thickness.py tests-dir/pytestcases/test_0_imports.py - all passing.
  • Manual cross-check of Steps 1/3/4 against reference implementation.
  • Manual end-to-end run (Python API and CLI) against a real plate model, sanity-checked output ranges.
  • black --check clean on all changed/added files.
  • Full pytest -vv tests-dir/pytestcases (not run here - some fixtures need network access / take a while).

🤖 Generated with Claude Code

michaelchin and others added 2 commits September 14, 2026 14:50
Adds gplately.paleobathymetry with age_to_basement_depth() (Step 1: four
published thermal-subsidence models), dutkiewicz_2017_sediment_thickness()
(Step 3: the Dutkiewicz et al. 2017 age/distance-to-margin polynomial fit),
and sediment_isostatic_correction()/paleobathymetry() (Step 4: Sykes 1996
isostatic sediment-load correction), ported from and numerically verified
against EarthByte's simple_paleobathymetry workflow. Ships the RHCW18
age-depth lookup table used by the 'rhcw18' model.

Step 2 (lifetime-mean distance to the nearest passive continental margin)
and Step 5 (pyBacktrack merge) are deliberately not included here: Step 2
needs the proximity/obstacle-routing engine tracked in gplately#445, and
Step 5 is tracked in gplately#447. See the module docstring and the #444
comment thread for the reasoning.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…; wire into #444

Adds gplately.sediment_thickness, a port of EarthByte's
predicting-sediment-thickness ocean_basin_proximity.py engine:

- generate_distance_grids(): for each ocean point in an age grid, reconstruct
  it backward through time (topological reconstruction via
  pygplates.TopologicalModel, distance queries via gplately's own
  ptt.utils.proximity_query) and accumulate its lifetime-mean distance to the
  nearest proximity feature (e.g. passive-margin COB line segments).
- generate_sediment_thickness_grids(): combines an age grid with the distance
  grid above via gplately.paleobathymetry.dutkiewicz_2017_sediment_thickness.

Exposed on the CLI as 'gplately generate-distance-grids'/'generate-sediment-grids'
(aliases gdg/gsg), per #445's stated scope.

Not ported: continent-obstacle shortest-path routing (ocean_basin_proximity.py's
shortest_path.py) - distances are currently always great-circle; and
topological (as opposed to static) proximity features. Both are documented
gaps in the module docstring.

Validated end-to-end against a real plate model (rotations/topologies/COBs)
with a downloaded age grid: produces physically sensible distance
(0-3000 km) and paleobathymetry (-2 to -5.5 km) values. Added unit tests
using the existing Muller2019 test fixtures plus a synthetic age grid.

Also adds gplately.paleobathymetry.simple_paleobathymetry(), the Steps 1-4
orchestrator for #444, which now calls generate_distance_grids() for Step 2
instead of requiring the caller to supply a distance grid.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@michaelchin michaelchin changed the title Port simple_paleobathymetry Steps 1, 3 and 4 into gplately Port simple_paleobathymetry (Steps 1-4) and its distance/sediment engine into gplately Sep 14, 2026
michaelchin and others added 2 commits September 14, 2026 15:26
Groups them with the other gridding code (oceans.py, topology/isochron
seafloor grids) per gplately's module layout. Public API is unaffected -
everything stays re-exported from gplately/__init__.py (gplately.paleobathymetry,
gplately.simple_paleobathymetry, gplately.generate_distance_grids, etc.).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Wraps gplately.grids.paleobathymetry.simple_paleobathymetry() as a new CLI
subcommand (alias 'pb'), completing the CLI surface alongside the existing
'generate-distance-grids'/'generate-sediment-grids' (#445): previously there
was no CLI path to Steps 1/4, or to running the full pipeline in one command,
even though that one-command workflow is the actual UX simple_paleobathymetry
(#444) is modelled on.

Refactored gplately/commands/sediment_thickness.py to extract
_add_distance_arguments()/_resolve_rotation_topology_proximity_files(), now
shared between 'generate-distance-grids' and the new 'paleobathymetry'
command instead of being duplicated.

Verified end-to-end against a real plate model (muller2025) via the CLI;
also added the same real invocation to tests-dir/test-cli.sh.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@michaelchin

Copy link
Copy Markdown
Contributor Author

Added a `gplately paleobathymetry` CLI command (alias `pb`), wrapping `simple_paleobathymetry()` to run Steps 1-4 end to end in one command - previously there was CLI coverage for Steps 2/3 only (`generate-distance-grids`/`generate-sediment-grids`), with no CLI path to the full pipeline even though that's the actual one-command UX the source `simple_paleobathymetry` workflow offers.

Refactored the shared argparse/file-resolution logic out of `commands/sediment_thickness.py` so it's reused by the new command rather than duplicated. Verified end-to-end against a real plate model via the CLI, and added the same invocation to `tests-dir/test-cli.sh`.

michaelchin and others added 2 commits September 14, 2026 15:43
…r in test-cli.sh

The combined 'paleobathymetry' command already exercised generate_distance_grids()/
generate_sediment_thickness_grids() at the Python level, but not the CLI-level
handoff between running 'gdg' and 'gsg' as two separate invocations (writing to
--distance-grids-dir, then a second process reading those grids back off disk) -
a distinct code path in commands/sediment_thickness.py that wasn't covered by
either the unit tests or test-cli.sh.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds gplately.grids.pybacktrack_paleobathymetry.merge_pybacktrack_paleobathymetry(),
a thin wrapper around pybacktrack.reconstruct_paleo_bathymetry_grids()'s merge
support: computes pyBacktrack's present-day paleobathymetry and merges in
Steps 1-4's grids to also cover submerged continental crust and crust that
has since subducted. pybacktrack is an optional dependency (new
gplately[paleobathymetry] extra), imported lazily so `import gplately` never
requires it.

Wired into gplately.grids.paleobathymetry.simple_paleobathymetry() as an
opt-in final step (pybacktrack=True, requires output_directory,
static_polygon_filename, present_day_age_grid_filename - Step 5 merges the
Step 4 grids by reading them back off disk), and into the 'gplately
paleobathymetry' CLI command as --pybacktrack (--static-polygons and
--present-day-age-grid are auto-resolved from -m/--model when not given
explicitly).

Validated with the actual pybacktrack package (now installed locally):
merging pyBacktrack's output with a real Steps-1-4 run (muller2025) via both
the Python API and the CLI increases finite-cell coverage from 330 to 441
out of 703 grid points, as expected (pyBacktrack fills in the
submerged-continental-crust/subducted-crust gaps Steps 1-4 leave as NaN).
Unit tests added (skipped when pybacktrack isn't installed).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@michaelchin

Copy link
Copy Markdown
Contributor Author

Added Step 5 (pyBacktrack merge, #447) too, so this PR now covers #444, #445 and #447.

  • `gplately.grids.pybacktrack_paleobathymetry.merge_pybacktrack_paleobathymetry()` - thin wrapper around `pybacktrack.reconstruct_paleo_bathymetry_grids()`'s merge support. `pybacktrack` is an optional dependency (new `gplately[paleobathymetry]` extra), imported lazily so `import gplately` never requires it.
  • Wired into `simple_paleobathymetry()` as an opt-in step (`pybacktrack=True`) and into the CLI as `gplately paleobathymetry --pybacktrack` (`--static-polygons`/`--present-day-age-grid` auto-resolve from `-m/--model` when omitted).
  • Actually validated with the real `pybacktrack` package (I had it install cleanly in the test env): ran the full pipeline end-to-end (Python API and CLI) against `muller2025`, and the merged output goes from 330 to 441 finite cells (out of 703, at a coarse 10° test grid) - i.e. pyBacktrack is correctly filling in the submerged-continental-crust / already-subducted-crust gaps that Steps 1-4 leave as NaN, which is the whole point of Step 5.
  • Unit tests added, skipped automatically via `pytest.importorskip` when `pybacktrack` isn't installed (kept it out of the core test env since it's a large optional dependency).

Only thing still open for the whole paleobathymetry effort now is #446 (continent-contouring passive-margins use case) and the continent-obstacle routing gap noted in #445's module docstring.

Adds gplately.grids.continent_contouring, a port of EarthByte's
continent-contouring create_passive_margins.py (the one use case that
repository contains), built entirely on gplately's own already-vendored
continent-contouring engine (gplately.ptt.continent_contours.ContinentContouring):

- passive_margin_polylines(): split one continent-contour polyline into its
  passive-margin segments, by removing the parts close to a subduction zone
  (the core algorithm).
- generate_passive_margins(): the full driver - contour continents through
  time and split each contour, returning/writing the aggregated contour and
  passive-margin feature collections plus per-time continental-crust masks.

Exposed on the CLI as 'gplately generate-passive-margins' (alias gpm).

generate_passive_margins()'s output composes directly with #445's
generate_distance_grids() as an alternative to a static COB line-segment
file (its passive_margin_features is already the
pygplates.FeaturesFunctionArgument-compatible type that proximity_features
expects) - validated end-to-end against a real plate model (muller2025),
no glue code needed. Noted in sediment_thickness.py's module docstring.

Validated with real data throughout: contouring, splitting, CLI, and the
composition with generate_distance_grids all run correctly against
muller2025. Unit tests added, including hand-checked geometric cases for
the segment-splitting algorithm.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@michaelchin

Copy link
Copy Markdown
Contributor Author

Added #446 too (continent-contouring passive margins), so this PR now covers #444, #445, #446 and #447 - the whole paleobathymetry tracking effort (#448) except the continent-obstacle routing gap noted in #445's docstring.

  • `gplately.grids.continent_contouring` - port of `continent-contouring`'s `create_passive_margins.py`, built on gplately's own already-vendored `ptt.continent_contours.ContinentContouring` engine. `passive_margin_polylines()` (the core segment-splitting algorithm) + `generate_passive_margins()` (the full time-stepping driver).
  • CLI: `gplately generate-passive-margins` (alias `gpm`).
  • Validated the actual point of this: `generate_passive_margins()`'s `passive_margin_features` output composes directly with Move Predicting Sediment Thickness workflow into GPlately, expose via API/CLI #445's `generate_distance_grids()` as a dynamically-contoured alternative to a static COB file - zero glue code needed (it's already the right `pygplates.FeaturesFunctionArgument`-compatible type). Ran this composition end-to-end against `muller2025` and confirmed it produces sensible distance grids.
  • Unit tests added, including hand-verified geometric cases for the segment-splitting logic (e.g. a contour with an active-margin gap in the middle correctly splits into two separate passive-margin polylines).

Adds gplately.lib.shortest_path, a faithful port of
predicting-sediment-thickness's shortest_path.py: a spherical grid
(Grid/ObstacleGrid/DistanceGrid) that computes shortest-path distances
*around* obstacle geometries via Dijkstra's algorithm, instead of a
great-circle straight line that can cut through land.

Wires this into generate_distance_grids() as new optional arguments
(continent_obstacle_features, plate_boundary_obstacle_feature_types,
shortest_path_grid_subdivision_depth) - when continent_obstacle_features is
given, each time step builds an obstacle grid from the reconstructed
obstacles (plus, by default, mid-ocean ridge and subduction zone plate
boundary sections) and routes distances around it instead of using plain
great-circle distance. Propagated through to
gplately.grids.paleobathymetry.simple_paleobathymetry() and the CLI
(--route-around-continents / --continent-obstacles /
--shortest-path-grid-depth, on both 'generate-distance-grids' and
'paleobathymetry').

Validated: a standalone synthetic-obstacle test confirms routed distance is
strictly longer than great-circle when a straight line would cut through
the obstacle, and matches great-circle (within grid-discretisation noise)
on a clear path. Against a real plate model (muller2025) with real
coastlines, obstacle-routed distances average ~70-80km longer than
great-circle across a coarse global test grid, with a small number of
individual-point exceptions consistent with the algorithm's known
grid-interpolation smoothing (shrinks with finer grid resolution, as
expected). Also verified end-to-end via the CLI.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@michaelchin

Copy link
Copy Markdown
Contributor Author

Added continent-obstacle routing too, closing the one remaining documented gap from #445.

  • `gplately.lib.shortest_path` - a faithful port of `predicting-sediment-thickness`'s `shortest_path.py`: a spherical grid + Dijkstra's algorithm that computes shortest-path distances around obstacle geometries instead of a great-circle line that can cut through land.
  • Wired into `generate_distance_grids()` as new optional args (`continent_obstacle_features`, `plate_boundary_obstacle_feature_types`, `shortest_path_grid_subdivision_depth`), and propagated through to `simple_paleobathymetry()` and the CLI (`--route-around-continents`/`--continent-obstacles`/`--shortest-path-grid-depth`, on both `generate-distance-grids` and `paleobathymetry`).
  • Validated: a clean synthetic-obstacle unit test confirms routed distance > great-circle when blocked, ≈ great-circle when clear. Against real data (`muller2025` + real coastlines), obstacle-routed distances average ~70-80km longer than great-circle across a test grid, with a small number of individual-point exceptions that shrink as grid resolution increases - consistent with the algorithm's own known grid-interpolation smoothing behaviour, not a port defect.

With this, the only thing left from the original scope (per #448) is #446... which is also already in this PR. So as far as I can tell, everything from the original email thread is now implemented, at least at a first-pass level of completeness.

michaelchin and others added 2 commits September 14, 2026 16:50
Combines test_9_paleobathymetry.py, test_10_sediment_thickness.py,
test_11_pybacktrack_paleobathymetry.py, test_12_continent_contouring.py and
test_13_shortest_path.py into a single test_9_paleobathymetry.py, organised
into labelled sections by submodule. Renamed the three colliding
test_public_api_exports() functions (one per merged file) so all three
survive rather than the later definitions silently shadowing earlier ones.
Replaced the module-level pytest.importorskip("pybacktrack") with a
try/except + skipif marker scoped to just the two pybacktrack tests, so a
missing optional pybacktrack dependency no longer skips the whole file.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- gplately/grids/paleobathymetry.py:
  - simple_paleobathymetry() no longer crashes with "got multiple values for
    keyword argument 'max_distance_km'" when sediment_thickness_kwargs
    explicitly overrides it (a documented, valid override) - the caller's
    value now correctly wins instead of colliding with the internally
    injected clamp_distance_km.
  - age_to_basement_depth(model="rhcw18") no longer maps tiny negative ages
    (floating-point/grid-interpolation noise right at a ridge) to 0 m (sea
    level); they're now clamped to 0 before interpolation, giving the
    table's actual ridge-crest depth (~-2500 m), consistent with how gdh1/
    parsons_sclater already special-case age < 0.
  - simple_paleobathymetry(pybacktrack=True) now raises a clear TypeError if
    rotation_model is an already-constructed pygplates.RotationModel (its
    own docstring says that's an accepted rotation_model input in general,
    but pyBacktrack specifically needs raw filenames) instead of failing
    obscurely inside pybacktrack.

- gplately/grids/continent_contouring.py: passive_margin_polylines() no
  longer splits a passive-margin stretch that happens to straddle a closed
  contour ring's arbitrary start/end point into two separate output
  polylines - the ring is now rotated to start right after an active edge
  (if any) before splitting, so the array boundary never falls inside a
  passive stretch.

- gplately/commands/sediment_thickness.py, .../continent_contouring.py:
  fractional --time-step/--min-time/--max-time (all declared type=float on
  the CLI) were silently truncated via int() before building the time
  range - e.g. --time-step 0.5 crashed with "range() arg 3 must not be
  zero", and --time-step 2.5 silently became 2. Replaced with a proper
  float-aware _time_range() helper (shared between the two command
  modules). Also added the missing return_none_if_not_exist=True on
  Topologies layer lookups (a model without that layer now hits the
  intended "No rotation/topology files found" message instead of an opaque
  plate-model-manager exception), and continent_contouring.py now merges in
  a model's Cratons layer when present, matching seafloor_grids.py's
  existing continent-file resolution.

- gplately/commands/paleobathymetry.py: --pybacktrack no longer silently
  drops all but the first file when a plate model's StaticPolygons layer
  resolves to multiple files (pyBacktrack's static_polygon_filename takes
  exactly one file) - they're now merged into one temporary file instead.

- tests-dir/test-cli.sh: fixed a reference to unittest/test_seafloor_gridding.sh
  (underscores); the actual script is test-seafloor-gridding.sh (hyphens),
  so the CLI smoke test would have aborted there.

Added regression tests for the three algorithm/API-level fixes (RHCW18
ridge depth, closed-ring seam merge, max_distance_km override). Full test
suite (33 tests) still passes; CLI commands manually re-verified end to end,
including the previously-crashing fractional --time-step case.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@michaelchin

Copy link
Copy Markdown
Contributor Author

Ran a thorough multi-angle code review over the whole diff before asking for review. Fixed everything that was a genuine bug:

  • `simple_paleobathymetry()` crashed with `TypeError: got multiple values for keyword argument 'max_distance_km'` if `sediment_thickness_kwargs` used its own documented override of that name.
  • `age_to_basement_depth(model="rhcw18")` mapped tiny negative ages (common floating-point noise right at a ridge) to 0 m (sea level) instead of ridge-crest depth (~-2500 m) - the other three models already special-cased this, rhcw18 didn't.
  • `simple_paleobathymetry(pybacktrack=True)` now fails with a clear error instead of an obscure one if `rotation_model` is passed as an already-built `pygplates.RotationModel` (pyBacktrack needs raw filenames).
  • `passive_margin_polylines()` could incorrectly split a single passive-margin stretch into two output polylines when it straddled a closed contour ring's arbitrary start/end point.
  • `--time-step`/`--min-time`/`--max-time` (declared `type=float`) were silently truncated via `int()` before building the time range - `--time-step 0.5` crashed outright, `--time-step 2.5` silently became 2. Fixed with a shared float-aware helper.
  • A plate model missing a `Topologies` layer raised an opaque plate-model-manager exception instead of the intended friendly error message.
  • `generate-passive-margins` didn't merge in a model's `Cratons` layer the way `seafloor_grids.py`'s equivalent resolution does.
  • `--pybacktrack` silently dropped all but the first file when a model's `StaticPolygons` layer spans multiple files - now merges them instead.
  • `tests-dir/test-cli.sh` referenced `test_seafloor_gridding.sh` (underscores); the real file is `test-seafloor-gridding.sh` (hyphens) - would have aborted the CLI smoke test.

Added regression tests for the algorithm/API-level fixes; full suite (33 tests) passes, and I re-ran the CLI commands manually including the previously-crashing fractional `--time-step` case.

Deliberately not fixed, flagged for awareness rather than action in this PR:

  • A real performance issue in `generate_distance_grids()`: when continent-obstacle routing is on and multiple age grids are processed together, each age grid's backward-reconstruction trajectory independently rebuilds the obstacle grid and reruns Dijkstra at every overlapping time step, so total work scales roughly O(N²) in the number of age grids instead of O(N) (this is exactly what the original `ocean_basin_proximity.py` was structured to avoid, by design, and is a legitimate restructuring, not a quick fix - didn't want to rush it in alongside everything else here).
  • A batch of "duplicated code" / "could reuse a shared helper" / "could use scipy instead of hand-rolled Dijkstra" style findings across several files - real observations, but refactors rather than bugs, and `gplately/lib/shortest_path.py` in particular is a deliberately faithful port where I'd rather not deviate from the validated original.
  • Two findings (the `math.pi` fallback being counted into the distance mean, and `DistanceGrid.shortest_distance()`'s point-vs-other-geometry inconsistency) turned out to be faithful copies of the original algorithm's own behaviour, not defects introduced here - confirmed against the source I ported from.

I think this is ready for human review now.

@michaelchin
michaelchin marked this pull request as ready for review September 14, 2026 07:17
michaelchin and others added 2 commits September 14, 2026 17:29
Adds paleobathymetry (pb), generate-distance-grids (gdg),
generate-sediment-grids (gsg) and generate-passive-margins (gpm) to the CLI
docs, following the existing per-subcommand section format (description +
usage examples). Verified with a clean sphinx-build (no new warnings/errors,
sections render correctly).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@michaelchin

Copy link
Copy Markdown
Contributor Author

@jcannon-gplates

Most of the work in the PR was done by Claude Code.

@jcannon-gplates

Copy link
Copy Markdown
Contributor

Wow, this is quite some work - nice!

I'll take a look - it might be a while though.

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