Port simple_paleobathymetry (Steps 1-4) and its distance/sediment engine into gplately - #449
michaelchin wants to merge 27 commits into
Conversation
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>
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>
|
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`. |
…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>
|
Added Step 5 (pyBacktrack merge, #447) too, so this PR now covers #444, #445 and #447.
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>
|
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.
|
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>
|
Added continent-obstacle routing too, closing the one remaining documented gap from #445.
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. |
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>
|
Ran a thorough multi-angle code review over the whole diff before asking for review. Fixed everything that was a genuine bug:
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:
I think this is ready for human review now. |
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>
|
Most of the work in the PR was done by Claude Code. |
|
Wow, this is quite some work - nice! I'll take a look - it might be a while though. |
Dietmar wanted it be included in the next GPlately release. In fact, I had completely forgotten the paleobathymetry task until he reminded me yesterday. It is the reason why I suddenly asked Claude Code to do this after I've tagged v2.1.0rc0. Take your time. Either in or not in the next release is fine with me. I am also hesitate to rush it in. |
By the way, Claude wrote almost all the long comments here. You may want your Claude give you a brief summary of those long comments. I found sometimes it took some time to read the reports from Claude. |
|
@copilot resolve the merge conflicts in this pull request |
…hymetry # Conflicts: # sphinx-doc/source/command_line_interface.rst Co-authored-by: michaelchin <2688316+michaelchin@users.noreply.github.com>
Resolved by merging |
jcannon-gplates
left a comment
There was a problem hiding this comment.
Thanks @michaelchin — this is a lot of ground covered, and the parts that are straight ports are genuinely well done.
I reviewed it by diffing against the original source rather than reading the diff, since the question I care about is fidelity to the workflows rather than whether the new code is internally sensible. Worth flagging up front, because it's why most of what follows wasn't visible in your own review pass — several of these need the ancestor to see at all.
How I'd suggest we split this up. I'm happy to take nearly all of it. I wrote most of the original code, so the fixes and the decisions will be quicker for me than for you — and it saves you the tokens, which I know get tight. I'd rather not hand you a long list of things to interpret either:
-
The blocking items below — I'll fix them on this branch, unless you'd rather do them yourself. They're all small; the largest is a one-line change repeated in three places.
-
Everything under "Decisions rather than fixes" — treat as decided, they're calls about my own original code. I've written each one up so it's on the record, and I'll check with you at the time if I'm unsure about anything.
-
The follow-up issues — mine, except the test plumbing. I've drafted them all under tracking issue #457 so you don't have to write any of this up:
Issue Owner #458 Blocking fixes for this PR (checklist; closed by #449) me #459 Restore the shared time loop in generate_distance_grids()— the 5–8× winme #460 Restore the internal/output grid-spacing split and upscaling filter me #461 Restore parallelism across Steps 1–3 and Step 5 me #462 Contouring fidelity: time-dependent thresholds, buffer ramp, subduction-zone output me #463 Test plumbing: test-cli.shinto CI,pybacktrackintest-env.yml,GPLATELY_TEST_LEVELgatingyou, if you're happy to take it #464 Tests for the changed surface (the porting delta) me #465 --max_distance_threshold: help text vs. behaviourme #466 Module placement and naming (physics functions in grids/, thecontinent_contouring/continent_contoursclash)me #467 Example notebook for the workflow me Issue #463 is CI and packaging rather than workflow science, which is much more your area than mine — that's the only reason I've put your name on it. Issue #466 touches GPlately's own layout rather than the ported code, and on issue #467 I can get the notebook started but you may well want to tidy it up to match the style of the others — so say the word if you'd rather own either of those.
What's solid
lib/shortest_path.pyis a genuinely faithful port. A token-level diff against the original shows no behavioural deviations at all — constants, the 16-neighbour construction with dateline wrap and pole clamping, the quad-tree hemisphere bounding, the lazy-deletion Dijkstra heap, the inverse-distance blend inshortest_distance, and both memo caches are intact. Dropping the__main__demo block was the right call.- Steps 1/3/4: every constant and branch boundary matches, including the Dutkiewicz polynomial with its standardisation constants, and the Sykes correction.
RHCW18_age_depth.datis byte-identical to both my copy and pyBacktrack's bundled one, so Step 1 and Step 5 are genuinely consistent forrhcw18. Nice property to have. (gdh1too — I checked the port's analytic form against pyBacktrack's and they agree to 0.00 m.)- Step 2's core maths — seeding, backward stepping, retirement, the unweighted mean, the
math.pifallback, mean-only clamping — matches statement for statement. - pyBacktrack Step 5 passes every argument with the same name and value as the original, and the lazy import is right.
- Good reuse of
ptt/continent_contours.pyrather than re-implementing it.
Blocking
1. Age grids with descending latitude are sampled upside-down. Three sites: grids/sediment_thickness.py:409-420 and :515-526, grids/paleobathymetry.py:511-526. Each builds extent=(min(lon), max(lon), min(lat), max(lat)), which throws away the sign sample_grid needs. read_netcdf_grid only flips latitude when max(lon) > 180 (_grids.py:371-376), so a −180..180 north-up grid keeps descending rows, and sample_grid then reads row 0 as −90° when it is +90° (_grids.py:858-860).
Reproduced with the PR's exact sampling call — one synthetic field where age increases with latitude, written twice with the two equally valid on-disk row orders:
query latitude : [-75. -45. -15. 15. 45. 75.]
expected age (Myr) : [ 15. 45. 75. 105. 135. 165.]
PR code, lat ASC : [ 15. 45. 75. 105. 135. 165.]
PR code, lat DESC : [165. 135. 105. 75. 45. 15.] <-- exactly mirrored
signed extent, DESC : [ 15. 45. 75. 105. 135. 165.] <-- _grids.py:390-395 form
max error on the descending grid: 150.0 Myr (field spans 15-165 Myr)
It's silent — no warning, and the output keeps entirely plausible ranges. gplately's own internal callers already use the correct signed form at _grids.py:390-395, so the fix is to use (grid_lon[0], grid_lon[-1], grid_lat[0], grid_lat[-1]), or to pass realign=True. The original workflow was immune because grdtrack and xarray.interp both work off labelled coordinates.
On how likely this is to bite in practice, see the first answer under "Questions I'd flagged" below — short version, GMT- and gplately-written grids are always safe, GDAL-written ones aren't.
2. {:.0f} output filenames collide and silently overwrite. grids/paleobathymetry.py:538, grids/sediment_thickness.py:535, grids/continent_contouring.py:292 — while the distance grids correctly use {:.1f} (sediment_thickness.py:459). The format strings are inherited; what changed is the precondition. The original forced integer times, so {:.0f} was injective, whereas _time_range exists specifically to allow fractional steps. Feeding it the times from --time-step 0.5 over 0→2 Ma:
0.0 Ma -> paleobathymetry_0Ma.nc
0.5 Ma -> paleobathymetry_0Ma.nc <-- overwrites 0.0
1.0 Ma -> paleobathymetry_1Ma.nc
1.5 Ma -> paleobathymetry_2Ma.nc <-- banker's rounding
2.0 Ma -> paleobathymetry_2Ma.nc <-- overwrites 1.5
grids computed : 5 files written : 3 silently lost : 2
So paleobathymetry_0Ma.nc ends up holding the 0.5 Ma grid. The returned dict is correct, so this is disk-only and easy to miss. The distance-grid writer survives the same times unscathed because of its {:.1f} — worth making the other three match it.
3. --time-step is bound to time_increment. commands/sediment_thickness.py:146 and commands/paleobathymetry.py:92. These are two different quantities in the original: time.step chooses the output times, while time_increment=1 is hardwired for the lifetime sampling loop. --time-step 10 samples each point's distance-to-margin every 10 Myr instead of 1 Myr, and degrades the reconstruction trajectory as well. Plausible output, wrong numbers. Needs a separate --time-increment defaulting to 1.
4. The same fault again in Step 5. grids/paleobathymetry.py:584 forwards the reconstruction increment as pyBacktrack's output increment. With times [0, 10, 20] and the default time_increment=1, Step 5 asks to merge paleobathymetry_1Ma.nc through _19Ma.nc, none of which exist — after the full backtrack has already run.
5. COBs are auto-sourced from the plate model, which the original explicitly refuses to do. commands/sediment_thickness.py:82-85 falls back to plate_model.get_layer("COBs"). The original refuses this in three places — the load_plate_model docstring, a capitalised warning in config.yml ("COB POLYGONS MUST NOT BE USED — a polygon outline wraps the whole continent, including active margins, creating false passive-margin proximities"), and the _check_cob_line_segments() advisory. That polygon guard was dropped at the same time, so there is now nothing to catch it. The error message at :91-95 half-knows this already.
6. An all-NaN age grid still writes an output file. sediment_thickness.py:423-427 falls through to the write at :453-461. The original warns and continues. A bad age grid silently yields a valid-looking all-NaN .nc that Step 3 then consumes.
7. No validation of time_increment in the library (the original raises). 0 gives ZeroDivisionError at :148; negative gives an infinite loop. Only the CLI path is guarded.
8. Valid-time epsilon dropped. create_passive_margins.py writes time + 0.5*interval - 1e-4 in three places, with the comment "epsilon to avoid overlap at interval boundaries". continent_contouring.py:275-287 drops it, so adjacent time slices now overlap at every boundary when loaded into GPlates.
9. crosby09 shouldn't be accepted for Step 5. pybacktrack_paleobathymetry.py:48 maps it to pyBacktrack's AGE_TO_DEPTH_MODEL_CROSBY_2007, but those are two different models — the numbers are under "Questions I'd flagged" below. gdh1 and rhcw18 genuinely are consistent across the merge; this one isn't, and it should raise rather than silently produce a grid with a model discontinuity along the vanished-crust boundary.
10. The public API should be locked down before release, not after. Neither generate_distance_grids nor simple_paleobathymetry has a * marker, so all 11 / 16 optional parameters can be passed positionally — including shortest_path_grid_subdivision_depth, distance_threshold_radians and plate_boundary_obstacle_feature_types. That depth was a deliberate speed/quality compromise on my side and is fine to expose, but as an advanced keyword. A bare *, after age_grid_filenames_and_times is one character per signature, and it's the kind of thing that can't be added once people are calling it. Same argument for the 14 new public names being missing from __all__ (so from gplately import * omits the whole new API — the hasattr-based export tests pass and mask this), and for only 4 of the 14 reaching Sphinx.
11. Minor. commands/paleobathymetry.py:69 leaks a NamedTemporaryFile(delete=False). commands/paleobathymetry.py:119 says obstacle routing isn't included, but it is. RHCW18_age_depth.dat is vendored without a licence statement.
Decisions rather than fixes
These are all calls about code I wrote, so I've taken them rather than asking — flagging them here so they're on the record, and happy to be argued out of any of them.
-
Restore continent-obstacle routing as the default.
config.ymlshipsuse_continent_obstacles: true; the port makes it opt-in. This also means the batching regression under "Performance" is on the default path rather than an edge case. -
Provenance of
grids/continent_contouring.py. The docstring says it portscreate_passive_margins.py, but it actually descends from simple_paleobathymetry'sgenerate_continent_contours.pyafter my fix in EarthByte/simple_paleobathymetry#2 — it reproduces wording that exists only in that fix. That's good news: it inherited both corrections (the swapped area/exclusion thresholds, and.infseparation → engine default), so it never had the merge-every-continent-into-one-blob bug. Two consequences worth recording. The docstring should cite the actual ancestor and note that the defaults follow the paleobathymetry-workflow parameter set rather than continent-contouring at main. And it missed thebuffer_and_gap_mode: rampfrom that same fix — and can't express it, sincebuffer_and_gap_distance_kmsis a float where the engine accepts callables. Deep-time (pre-Pangea) contouring isn't reachable as a result; that part is issue #462 rather than this PR. -
separation_distance_threshold_radians=Noneis safe, but only by accident of style. It works because:209-213omits the kwarg so the constructor default applies. The engine treats an explicitNoneas zero. Worth a comment there so nobody "simplifies" it into a direct pass-through later. -
Keep the closed-ring seam rotation (
:97-111), but document it as a deliberate deviation. It emits one polyline where every ancestor emits two. I think it's an improvement, but it changes feature counts and per-feature lengths against the referencepassive_margin_features.gpmlz, so anyone comparing will otherwise think something is broken. It also evaluates_near_subductiontwice per arc; the memo is in issue #462.
Testing
I agree with the principle that verbatim ports carry the original's track record — shortest_path.py and the exact-constant maths don't need new tests for port fidelity. But that makes the case for concentrating effort on what actually changed, and at the moment the suite doesn't.
Classifying the ~2,200 new code lines: excluding shortest_path.py, only about a quarter is verbatim, grids/*.py is roughly 57% changed, and the numerically risky part is about 90 lines across six grid-I/O call sites — which is exactly where bug 1 lives, three times over. All the confirmed bugs are in changed or new code; none are in verbatim code.
Of the 33 tests, 3 make an exact numerical claim, 19 are sign/shape/NaN properties, and 11 are hasattr/raises/isfile. None use golden values from the original workflow, though reference outputs are committed in the source repos. Concretely: the coefficients, the spacing, and the valid-time epsilon could all change and the suite would stay green. Every distance-grid test passes a single age grid, so the batching behaviour is untestable by construction; and the fixture is written with write_netcdf_grid, which always emits ascending latitude, so bug 1 is invisible to it. test-cli.sh isn't referenced by any workflow and pybacktrack isn't in test-env.yml, so the CLI has no CI coverage and both Step 5 tests always skip.
One nuance on "verbatim needs no tests": it does where the port widened the envelope. generate_input_points_grid's floor(360/spacing) is verbatim, but only ever ran at 0.5 and 0.1 — --grid-spacing 0.3 now yields an actual spacing of 0.30025° while the filename still says 0.3d. Similarly Grid(6) was hardwired and is now a user-settable int, and proximity_geometries can now be empty or very large.
Tests covering the blocking fixes will land with those fixes here. The rest of the changed surface is written up as issue #464, and the CI/packaging side as issue #463.
Performance
Confirmed — and it's a regression introduced by the port rather than something pre-existing. ocean_basin_proximity.py is deliberately structured to batch, and says so in the API docstring, in the task-chunking comment that motivates min_num_age_grids_per_task = 10, and in the CLI help. One outer time loop services all age grids via ocean_basin_reconstructions, with the sharing at :1145-1152. grids/sediment_thickness.py:405 inverts that into a per-age-grid time loop.
Two corrections to the framing. "O(N²)" overstates it: total work is O(M·T) either way; what goes from O(T) to O(M·T) is the shared term, which my own profiling puts at 86–88% of per-time-step cost — so realistically 5× to 7.7×, not unbounded. And with obstacle routing restored as the default, this is the default path rather than an edge case.
Separately, TopologicalModel is built inside the per-time-step loop at :156-158 despite having no time dependence. That one's free.
None of the performance work blocks the merge, on the basis that the port reproduces the original workflow and just runs slower — that's the bar I'm applying. That includes the dropped internal/output grid-spacing split: that split and its upscaling filter were a speed optimisation on my side, not part of the method, so computing directly at output resolution is correct. Two things worth documenting rather than fixing: output computed directly at 0.1° won't be bit-identical to the published product, since the smoothing filter is absent; and the default grid_spacing=0.5 is coarser than the workflow's 0.1° output.
Questions I'd flagged — and the answers
Three things I couldn't settle from the code alone. I've since checked all three, so nothing here needs an answer from you.
Do any age grids in this chain actually store latitude descending? Effectively no, but not never. I swept every grid on my machine — 486 directories' worth of age grids, distance grids, published paleobathymetry and GPlates sample data — and 483 are ascending. Two of the three exceptions are cartopy's bundled sample data. The third is a real EarthByte age grid, and its history attribute says GDAL CreateCopy: GDAL writes north-up, i.e. descending, and its longitudes were outside −180..180 as well, so read_netcdf_grid's realign gate wouldn't have caught it either.
So anything written by GMT or by gplately is always safe, and anything that has been through GDAL, a GeoTIFF or a GIS is not. That downgrades bug 1 from "corrupts the shipped workflow" to "silently corrupts a plausible user-supplied input" — still worth fixing in a public API, and it means the test has to build its descending grid with netCDF4 directly, since write_netcdf_grid can't produce one.
Should crosby09 be rejected for Step 5? Yes. Steps 1–4's crosby09 is the Crosby & McKenzie (2009) empirical piecewise fit; pyBacktrack's CROSBY_2007 is the plate-cooling model from Crosby's 2007 thesis, with explicit mantle density, diffusivity and plate thickness. Different models, and the numbers say so:
age crosby09 (Steps 1-4) CROSBY_2007 (Step 5) diff
0 2652.0 m 2601.4 m +50.6 m
20 4101.0 4150.7 -49.7
75 5457.9 5517.8 -59.8
100 5369.0 5461.8 -92.9
140 5557.5 5656.8 -99.3
160 5793.7 5703.5 +90.3
max |difference| over 0-200 Ma : 99.3 m
same check for gdh1 : 0.00 m (port vs pyBacktrack, exact)
So the merged grid would carry a model discontinuity of up to ~100 m wherever present-day crust meets vanished crust. gdh1 is exact and rhcw18 shares the identical table, so it really is just this one model. That's blocking item 9 — better to raise now than to add a rejection after release.
--max_distance_threshold: help text vs. behaviour. Confirmed, and it's my bug carried over faithfully. The help says over-threshold distances are ignored and "will not contribute to mean / standard deviation"; both code paths set min_distance = math.pi and include it. The reason is that the lookup returns None for "unreachable through obstacles" and for "beyond threshold", and the unreachable case genuinely needs a value, so the two got conflated on the same line. The behaviour I actually want is π·R for unreachable and a real exclusion for over-threshold — but that changes published numbers, so for this PR I'd just make the docstring describe what the code does, and settle the semantics in issue #465 (upstream as well as here).
Three independent gaps flagged in review of #449 (four new CLI parsers, ~40 flags, and Step 5/pyBacktrack support), none of which had CI coverage: - Add tests-dir/pytestcases/test_10_paleobathymetry_cli.py: argparse-level tests (add_parser -> parse_args -> assert on the namespace) for all four new subcommands (paleobathymetry/pb, generate-distance-grids/gdg, generate-sediment-grids/gsg, generate-passive-margins/gpm), covering every flag, both aliases, and the two argparse-enforced validations (--age-depth-model's choices, --distance-grids-dir's required=True). These never call the actual command function, so they're fast (all 15 pass in well under a second) and need no network or plate model, unlike tests-dir/test-cli.sh's end-to-end coverage of the same surface. - Add pybacktrack to tests-dir/test-env.yml so CI actually exercises the two Step 5 tests instead of skipping them via HAS_PYBACKTRACK on every run. It's a noarch conda-forge package, so this doesn't add any per-platform build risk to the 15-job test matrix. - Gate the 7 tests in test_9_paleobathymetry.py that pull a full Muller2019 plate model behind GPLATELY_TEST_LEVEL, matching the pattern already used for the comparable heavy tests in test_4_rasters.py, so they no longer download on every one of the 15 matrix jobs and no longer error (rather than skip) when the network is unavailable in the default run. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…try.py Folds test_10_paleobathymetry_cli.py back into test_9_paleobathymetry.py to keep one file per feature area, per request. Left a comment where the section was merged in noting the tradeoff this gives up: these are fast, offline, fixture-free argparse-wiring checks, mixed in with the rest of the file's numeric/behavioural library tests (which often need the Muller2019 fixtures and are sometimes network-gated) -- worth revisiting if the file's dual nature becomes annoying in practice. Module-name collision note: gplately.commands.paleobathymetry is imported as paleobathymetry_cmd (etc.) to avoid shadowing the Step 4 paleobathymetry() function already imported from gplately.grids.paleobathymetry. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
convert back to "Draft" so that the tests don't run for every push. |
sample_grid() learns the row order of a grid from the sign of its extent: dy = (extent[3] - extent[2]) / (nrows - 1), and point_i = (lat - extent[2]) / dy. Three age-grid sampling sites built that extent with np.min/np.max, discarding the sign, so any grid stored with descending latitude was read upside-down -- southern ages applied to northern points, silently, with entirely plausible output. Descending latitude is what GDAL writes, and therefore what any GeoTIFF or GIS round-trip produces. Use the grid's first/last coordinates instead, which is the idiom read_netcdf_grid() already uses for its own internal resampling. Adds an offline regression test asserting that the predicted sediment thickness is unchanged when the same age field is stored the other way up, a guard test pinning the assumption that a descending grid survives the read descending, and a plate-model-gated end-to-end equivalent covering the other two sampling sites. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Output filenames carried the reconstruction time to a fixed number of decimal
places -- "{:.0f}" for the paleobathymetry, sediment-thickness and continent-mask
grids, "{:.1f}" for the mean-distance grids. That was safe in the workflows this
was ported from, because they forced whole-number times. The port added a time
range that permits fractional steps without widening the names, so
--time-step 0.5 over 0-2 Ma computed five grids and wrote three, the later times
overwriting the earlier ones with no warning.
The same two literals also reached pyBacktrack, whose Step 5 hand-off has its own
output_file_decimal_places_in_time and merge_paleo_bathymetry_file_... parameters,
both hardwired to 0 here. pyBacktrack builds its format as "{time:.Nf}", so with a
fractional step it went looking for grids nobody had written.
Add a keyword-only decimal_places_in_time to the five public entry points and to
all four CLI subcommands, threaded through to both pyBacktrack parameters so the
two halves cannot drift. Left unset, each output keeps the number of decimal
places its original workflow used -- one for the mean-distance grids, none for the
rest -- so existing filenames are unchanged. simple_paleobathymetry() forwards the
value unresolved for that reason: resolving it centrally would rename the distance
grids out from under the generate-sediment-grids subcommand that reads them back.
Where times would still collide, raise rather than overwrite, naming the two times,
the file they share and the value that would separate them. The check runs before
any reconstruction, including in the CLI, which would otherwise read every distance
grid off disk first.
Also folds the duplicated mean_distance_<spacing>d_<time>.nc format string, which
the library wrote and the CLI independently reconstructed, into one helper.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two different quantities had been bound to one CLI flag. --time-step chooses which times get output; time_increment is how finely each ocean point's lifetime is sampled while walking it backward, which the original workflow fixed at 1 Myr and never tied to the output step. Binding them meant --time-step 10 also sampled every 10 Myr instead of every 1 Myr: plausible output, wrong numbers. Give the reconstruction increment its own --time-increment flag, defaulting to 1 as in the original, on the two subcommands that actually reconstruct. The same conflation reached Step 5. pyBacktrack's time_increment is documented as "the time increment that output is generated", i.e. the output step -- it was being handed Step 2's reconstruction increment, so times [0, 10, 20] with the default increment of 1 made pyBacktrack generate 0, 1, 2 ... 20 Ma and then look for paleobathymetry_1Ma.nc, which Step 4 never wrote. It now takes the increment from the spacing of the requested times, and refuses times that are not evenly spaced, since it has no way to express those. Each age grid's walk starts at its own time snapped up onto a shared grid of multiples of the increment -- the structure that lets the original share work between age grids. Times that do not land on that grid were therefore reconstructed from the wrong starting time, so they are now rejected. That check and the snap share one rule: the times a fractional step produces are not exact (0 + 3 * 0.1 is 0.30000000000000004), and a plain ceil() sent that to 0.4, so the snap rounds first where the quotient is a whole number to within a tolerance. All of this is validated before a file is opened or a point reconstructed, including the Step 5 prerequisites, which were checked only after Steps 1-4 had already run. Note for anyone using a fractional output step: --time-step 0.5 alone now raises rather than silently sampling at 0.5 Myr, and wants --time-increment 0.5 (or finer) passed explicitly. Defaulting the increment to the output step is what this commit removes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three cases where the port accepted something the original workflow refused, or produced output where the original produced none. All three share a failure mode: a complete, plausible-looking grid that means the wrong thing. Proximity features. The CLI fell back to the plate model's COBs layer when --proximity-features was not given; the original refuses that in three separate places. A COBs layer traces the entire continent-ocean boundary, active margins included, where this workflow wants passive margins only -- so ocean points beside a subduction zone come out close to a "margin". What the layer contains also varies by model: muller2019's is line segments, while merdith2021's and the COB Terranes sets are largely polygons. The fallback is gone, and the library now refuses polygons outright, along with an empty feature list -- reachable through a proximity_feature_types filter that matches nothing, and worth catching because every point then reports half the Earth's circumference. An age grid with no input points inside it wrote a grid of NaN, where the original warns and moves on. A file full of NaN is indistinguishable from a real result until something reads it, so it is skipped instead. Both callers were taught to expect the gap: the library pipeline drops those times from the later steps, and the CLI treats one missing distance grid as that skip while still failing when none of them are there. Contour and passive-margin features were given valid times of exactly time +/- 0.5 * step, so adjacent slices both claimed the instant between them and GPlates drew two sets of margins on top of each other at every interval boundary. The original subtracts an epsilon from the begin time in three places; restored here for the two features this writes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Step 5 merges pyBacktrack's paleobathymetry into the grids Steps 1-4 produced, so both sides have to convert age to depth with the same model. "crosby09" was mapped onto pyBacktrack's AGE_TO_DEPTH_MODEL_CROSBY_2007, which is a different model despite the name: gplately's is the empirical piecewise fit of Crosby & McKenzie (2009), pyBacktrack's is the plate-cooling model of Crosby's 2007 thesis. Measured against pybacktrack 1.5.0 over 0-200 Ma they differ by up to 218 m, at 86 Ma, and by 51 m at the ridge crest, which is a step change wherever the two sources meet. For comparison the models that are kept agree to 0.41 m (gdh1) and 0.00 m (rhcw18). Drop the mapping, and record why alongside it, since a pyBacktrack constant with a matching name exists and re-adding the entry looks obviously right. A substituted RHCW18 lookup table is refused for the same reason: it changes the age-depth relationship for Steps 1-4 only, and pyBacktrack has no way to be handed the same table. Both are checked before Steps 1-4 run rather than when Step 5 starts, and model aliases are resolved first, so a valid run spelled "richards" is not refused for looking unfamiliar. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Supersedes the approach in 9381cb9, which refused the models pyBacktrack has no matching built-in for. pyBacktrack's ocean_age_to_depth_model accepts a callable as well as one of its enumerated models -- "a callable function accepting a single non-negative age parameter and returning depth (in metres)" -- so Step 5 can be handed gplately's own conversion instead of the nearest-looking built-in. Both sides then run one implementation rather than two that ought to agree. Measured against pybacktrack 1.5.0 over 0-200 Ma, depths from Step 5 now match Steps 1-4 exactly (0.00 m) for every model, where picking a built-in by name gave 0.41 m for gdh1, 218 m for crosby09 (whose CROSBY_2007 is a different model despite the name), and nothing at all for parsons_sclater. A substituted RHCW18 lookup table now reaches Step 5 as well, instead of being refused because pyBacktrack could not be given it. So the refusals are gone and parsons_sclater gains Step 5 support it never had. The callable is a functools.partial of a module-level function, not a closure: pyBacktrack pickles the model when use_all_cpus is set, and a closure does not survive that. It is memoised on the exact age, with no rounding, because pyBacktrack calls it once per ocean point per decompaction step -- 43% of calls are repeats in a 10-degree run, and the conversion otherwise runs a scalar through array machinery. In a real Step 5 run it accounts for well under 1% of the time. An unusable age-depth model, or an unreadable lookup table, is now reported before Step 2 runs rather than from Step 4 at the end of it, and whether or not Step 5 was asked for. The crosby09-vs-CROSBY_2007 measurement is kept as a test: it is now the reason for not using a built-in, rather than the reason for refusing a model. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
None of this can be changed afterwards without a breaking change, and none of it is on master yet, so this is the only free moment to get it right. Make the optional parameters of the five workflow entry points keyword-only. The signatures run to twenty parameters, and several are interchangeable by type but not by meaning -- passing topological features where proximity features go is accepted in silence and yields a plausible grid of wrong numbers. So the marker goes after the first parameter on the four whose remaining required arguments are confusable with each other, and after both required arguments on generate_sediment_thickness_grids, whose list and dict are not. Every caller in the repository already passed these by name, so nothing changed but the guarantee. Add the thirteen names the port introduced to __all__. They were imported into the package namespace but never exported, so `from gplately import *` omitted the entire new API, and add the eight of them that Sphinx was missing, grouped by workflow step rather than listed flat. The export tests could not have caught either: they asserted hasattr, which is true regardless of __all__ and of the docs. They now check all three, and skip visibly rather than silently when run against an installed package with no sphinx-doc/ beside it. A further test asserts the keyword-only property rather than a list of parameter names, so one added later is covered too. AGE_DEPTH_MODELS and DUTKIEWICZ_2017_SEDIMENT_THICKNESS carry `#:` doc-comments now, so the generated pages describe them rather than showing the builtin tuple and dict docstrings. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…#458) Four calls about code the original workflow made deliberately, three of which are only recorded in the ancestors and would be undone by anyone tidying up. Continent-obstacle routing is on by default again. The original ships use_continent_obstacles: true (config.yml:134); the port made it opt-in, so the documented workflow was not what the command ran. On by default has to tolerate a plate model with no Coastlines layer, so an implicit run falls back to straight-line distances with a warning where an explicit --route-around-continents still fails; and because every -m/--model run now reaches for that layer, a failure fetching it no longer takes down a run that never asked to route. Passing --continent-obstacles together with --no-route-around-continents is refused rather than silently ignoring the files. This makes the default path slower -- #459, #460 and #461 are where that is addressed. The module's provenance was wrong. It descends from simple_paleobathymetry's generate_continent_contours.py, not continent-contouring's create_passive_margins.py: it splits a contour by walking get_points() as the former does (generate_continent_contours.py:385) rather than get_segments() as the latter does (create_passive_margins.py:376), and it reproduces text that exists only in the former ("e.g. lakes", config.yml:113). Its defaults follow the paleobathymetry-workflow parameter set, which is what they should be judged against. separation_distance_threshold_radians is omitted rather than passed as None, and now says why: the engine reads an explicit None as zero separation, which merges nothing and differs from its own default. Folding the condition away would change the output while looking like a simplification. The closed-ring seam rotation is marked as a deliberate deviation. No ancestor rotates the ring, so where a passive stretch straddles the seam they emit two polylines for one margin and this emits one. The union of arcs is unchanged, so comparisons against a reference .gpmlz should be made on that rather than on feature counts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#458) The merged static-polygons file that --pybacktrack writes when given several StaticPolygons layers was a NamedTemporaryFile(delete=False) that nothing ever deleted -- one abandoned .gpmlz in the system temp directory per run. It now lives in a TemporaryDirectory that cleans itself up, with ignore_cleanup_errors so that a file still held open cannot turn a finished run into a traceback after every grid has already been written. The 'paleobathymetry' description told users continent-obstacle routing was "not yet included". It has been included all along, and is now the default. distance_threshold_radians said it would "reject/ignore" proximities beyond the threshold. It does not: a point with nothing in range is recorded at math.pi radians -- half the Earth's circumference -- and averaged in like any other sample, so a threshold pulls the affected means towards that maximum rather than leaving them out. The same substitution covers a point that cannot be reached around continent obstacles, because the lookup reports both cases identically. The docstring now says so, and points at #465, where whether that is the right behaviour is decided. RHCW18_age_depth.dat gains the provenance note it was vendored without. It cannot gain a licence statement: the upstream repository (github.com/freddrichards/RHCW18_Plate_Model) states no licence at all, only a request to cite two papers, so there is nothing to restate. The note records the source, both citations, that this copy is byte-identical to simple_paleobathymetry's, and that redistribution terms are unresolved -- with the three ways to resolve them, since that needs a decision rather than a paragraph. pyproject's data/*.* pattern ships it beside the table, and a test keeps the two together. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Findings from a review of the accumulated delta rather than of any one commit. No numerical defect: this is stale text, one missing check, and one caveat. Report a missing pybacktrack package before Steps 1-4 run. Every other Step 5 prerequisite is checked up front, but the package's own importability was not -- it surfaced from the import at the Step 5 call, hours later, on a machine that was never going to be able to finish. Importing it in the prerequisite block is both the check and the import. --proximity-features' help still said "required unless -m/--model's plate model provides a COBs layer". That fallback was removed two commits ago, so --help was describing a run that now raises. The comment above the Step 5 increment described an import that two reworks ago sat beneath it and no longer exists. The continent_obstacle_features docstring made the same point twice, once per commit that wrote it. The valid-time epsilon is credited properly: it comes from continent-contouring's create_passive_margins.py, while this module otherwise descends from simple_paleobathymetry's generate_continent_contours.py, which has no equivalent. Adopted from the sibling, not restored from the ancestor, and the module docstring no longer claims to owe the other file nothing. Finally, a caveat on Step 5's time round trip. pyBacktrack regenerates the output times from the increment it is given, so they must survive formatting at decimal_places_in_time. They do unless the precision sits exactly at the step's rounding boundary -- [0, 0.15, 0.3, 0.45] at one decimal place, where Step 4 writes _0.5Ma.nc and pyBacktrack asks for _0.4Ma.nc. Reachable only by passing such times directly; the CLI derives them the same way pyBacktrack does. The failure is loud, and the docstring now says how to avoid it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Upstream (github.com/freddrichards/RHCW18_Plate_Model) states no licence at all, only a request to cite two papers, so there was never a licence for GPlately to restate. Treating that citation request as the intended terms, and redistributing on that basis with the attribution already recorded beside the data. Written down so it is not re-litigated, along with what would change the answer: explicit terms appearing upstream, or a need for a licence rather than an inference -- in which case the options are to ask the authors, or to fetch the table at run time instead of vendoring it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Hi @michaelchin, the blocking items in #458 are complete, so you can mark it ready-for-review if you like. I did one Ubuntu/3.12 dispatch after the last commit. Marking it will then unlock macOS, Windows and Python 3.10/3.11/3.13. After this is merged I will work on the remaining PRs listed in #457. |
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 intogplately.sediment_thicknessfor Step 2.gplately.sediment_thickness(#445) - a port ofpredicting-sediment-thickness'socean_basin_proximity.pyengine: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), usingpygplates.TopologicalModeland gplately's ownptt.utils.proximity_query.generate_sediment_thickness_grids()- Step 3's grid-level driver, composing the above withdutkiewicz_2017_sediment_thickness().gplately generate-distance-grids/generate-sediment-grids(aliasesgdg/gsg).Validation
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.generate-distance-grids->generate-sediment-grids) smoke-tested end-to-end against the same real data.test_9_paleobathymetry.py,test_10_sediment_thickness.py), using the existing Muller2019 test fixtures.Deliberately out of scope
shortest_path.py) - distances are currently always great-circle. This is a real difference fromsimple_paleobathymetry's default config (use_continent_obstacles: true).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.black --checkclean on all changed/added files.pytest -vv tests-dir/pytestcases(not run here - some fixtures need network access / take a while).🤖 Generated with Claude Code