Skip to content

Refuse a cell entropy outside the EOS tables with the rheology off - #98

Merged
timlichtenberg merged 13 commits into
mainfrom
tl/aragog-eos-table-guard
Nov 2, 2026
Merged

timlichtenberg merged 13 commits into
mainfrom
tl/aragog-eos-table-guard

Conversation

@timlichtenberg

@timlichtenberg timlichtenberg commented Nov 2, 2026 •

Copy link
Copy Markdown
Member

Description

With the solid rheology off, Aragog raised no error when a cell entropy left the EOS tables. A table lookup clamps to the edge, so the run went on at the edge temperature and PROTEUS reported it as completed. A test config reached -8007 J/kg/K against a table minimum of 106. A minimal.toml variant with water and carbon divided by 100, the escape stop off and the low-T extended table reached -62290 against 26.

With this change and the rheology off, a call with a cell entropy outside the global range of the loaded tables, by more than 1e-3 J/kg/K, ends with status -1 and the name EOS_TABLE_GUARD. Its energy integrals are zero and its states stop before the first one outside. With the rheology on, the existing invariant still raises. PROTEUS needs no change: its failed-call path halves the step, and a shorter step can pass.

For a user:

  • A run that stays inside the table does not change.
  • A short excursion passes on the retry with a shorter step. A drain that persists now stops the run after the retries, where it went on at the clamped edge before. The PROTEUS status file stays on Running in that case; I leave that for a later PROTEUS change.
  • A cell held at a table edge fails after about 1e-3 over its outward rate: about 1e5 yr at 1e-8 J/kg/K per yr.

The guard does not judge the basic nodes (the top node can lie outside with every cell inside), rows padded with a copied temperature inside the global range, or const_properties. Only EntropySolver.solve has it; the diffrax path in PROTEUS, off by default, does not.

I also rewrote docs/How-to/solver-method-tuning.md as a how-to with its own sections on the guard and the floor, and every number of the old page is still on it.

This is a safety net and does not treat the cause, a surface flux held over a coupling step much longer than the top cell can drain. With the guard on, the surface temperature still falls by hundreds of kelvin in one row in these runs. top_cell_dt_limit removes that fall, and it stays off by default.

Melt fraction at 25 kyr, on a test config that is not shipped (dummy atmosphere, fixed surface state, 100 yr minimum step), from each runtime_helpfile.csv:

Run Standard table Low-T extended table
main 0.7212 0.7218
this branch 0.5932 0.6318
main with top_cell_dt_limit 0.6002 0.6002

The minimal.toml variant above, with AGNI, runs to melt fraction 0.05 at 285.4 kyr on this branch, with 1 guard event, against 284.2 kyr with top_cell_dt_limit. On main it pins the magma temperature at the table edge, 67 K, from 51 kyr.

In-table behaviour: minimal_quick gives the same helpfile on main and on this branch apart from the wall clock (122 rows, 941 columns). No shipped config I ran crosses the table, with the low-T table, up to these points: minimal and minimal_quick to solidification, tutorial_earth 1.6 Myr, tutorial_venus 2.0 Myr, chili/tr1e 1.4 Myr, cmb_regression 250 kyr, all_options only 9.5 kyr. These end at a 2 h wall limit or their target, apart from the first 2.

Validation of changes

macOS, Python 3.12, CI test tables, a detached tree at dfbeea3, source imported from that tree. The commits after it change docs only:

  • pytest -m "unit and not slow" -n 3 -o addopts=: 2755 passed, 5 skipped.
  • pytest tests/test_eos_table_guard.py tests/test_eos_table_guard_decision.py: 47 passed, 1 skipped (the low-T table is not set in that run).
  • Existing floor tests that drained past the table with the floor off now assert the guard, in tests/test_eos_floor.py and tests/test_entropy_bc5_jax_parity_smoke.py.
  • ruff check src/ tests/ and ruff format --check src/ tests/: clean. tools/validate_test_structure.sh: OK.
  • Main against this branch, 12 nodes, 5 yr, quasi_steady, energy_balance and core_module, NumPy path, rheology off: 222 output arrays (time, state, energy integrals, get_state) are bit-equal. I did not compare the JAX path or the rheology on in this way; the branch tests compare a call with and without the guard on both.
  • Nightly on the tip: https://github.com/FormingWorlds/aragog/actions/runs/41484148179, all 3 jobs pass, main 3305 passed, 6 skipped, 1 xpassed.

Added lines, from pr-breakdown 65e0d18ef13606515da9fa6ecba917e0620208d1 8623fac592cc13d20e578ed853978516202bbc37:

Category Files Lines added / removed Size on disk (KiB) Compressed (KiB)
tests 4 +422 / -14 +18.1 +5.9
docs text 4 +173 / -29 +3.7 +1.3
source 2 +76 / -9 +3.3 +1.1
total 10 +671 / -52 +25.2 +8.2

Checklist

  • I have followed the contributing guidelines
  • My code follows the style guidelines of this project
  • I have performed a self-review of my code
  • My changes generate no new warnings or errors
  • I have checked that the tests still pass on my computer
  • I have updated the docs, as appropriate
  • I have added tests for these changes, as appropriate
  • I have checked that all dependencies have been updated, as required

With the rheology off, a drain past the table minimum runs on at the clamped edge temperature and reports success. These tests expect a failed status on the CVODE, BDF and JAX factory paths, read the edges from the loaded tables, and hold a call inside the tables bit-equal.
With the rheology off, a cell past the table range kept the clamped edge temperature and the call reported success, so a run could go on with a wrong melt fraction and flux. A call with an accepted cell entropy outside the loaded tables now ends with status -1 and a logged message, which lets a caller retry with a shorter step.

The edges come from the loaded tables, with a tolerance of 1e-3 J/kg/K above the drift of a state at a table edge. A refused floor call, a zero span and the rheology-on path stay as they are, and a top basic node outside the tables passes.
A failed call must hold no state outside the tables, and a start outside must say that a shorter step does not help. The tests also pin the tolerance and both edges to the loaded tables, cover the core modes and the rheology-on raise, and run a retry with a shorter span. The drain tests move to tiers that run on PR CI where they fit.
A failed call now holds its states from before the first one outside the tables, so the end state of the result and the text of the CLI agree. A start outside the tables says that a shorter step does not help. The docs state what the guard judges: the cell entropies against the global range of the tables, not the extrapolated basic nodes or the padded rows of a pressure column.
The result of a failed call must not keep a trace of states outside the tables and must not read as a success. An integrator failure keeps its own flag name, the core modes hold on CVODE and BDF, and a start outside is named at a later call too. The tests of the decision alone move to a file in the unit tier.
A failed call no longer keeps its energy trace, which held the states outside the tables, and its success field is false. The docs state what a start outside holds and what a strict-range EOS does on CVODE and on scipy.
A retry on the same solver gives the same result as a fresh call, and a call from a table edge passes when short and fails when long, on both edges. The core module joins the core modes, each with its held states checked.
A cell at a table edge fails after about the tolerance over its outward rate, root-search states count, the result of a failed call holds the time of its last state, and only the EntropySolver path has the guard.
Copilot AI balanced review requested due to automatic review settings November 2, 2026 15:36
@timlichtenberg
timlichtenberg requested a review from a team as a code owner November 2, 2026 15:36

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

No active review issues are blocking the EOS table guard change.

No open issues 🎉

What changed in this PR

This PR adds an EOS table guard to EntropySolver.solve, preventing rheology-off runs from silently continuing with cell entropy outside the loaded tables. Failed calls use the existing PROTEUS retry path.

Changes:

  • Rejects cell entropy outside the table range by more than 1e-3 J/kg/K, reports EOS_TABLE_GUARD, truncates returned states, and zeros energy integrals.
  • Adds guard and retry tests, and updates existing floor tests for the new failure behavior.
  • Documents the guard’s scope, limitations, and output diagnostics.
File Description
tests/​test_eos_table_guard.py Tests guard failures, retries, solver paths, and unchanged in-range behavior.
tests/​test_eos_table_guard_decision.py Tests range decisions, tolerance, intermediate states, and exemptions.
tests/​test_eos_floor.py Updates floor-off expectations to guard failures.
tests/​test_entropy_bc5_jax_parity_smoke.py Checks guard failure for a draining top cell under boundary condition 5.
src/​aragog/​solver/​entropy_solver.py Implements range checks and guarded failure handling.
src/​aragog/​config/​energy.py Updates floor configuration documentation to explain the guard.
docs/​Reference/​cli.md Documents the guard’s diagnostic name.
docs/​How-to/​solver-method-tuning.md Explains guard behavior, retries, and floor interactions.
docs/​How-to/​netcdf.md Documents guarded-call flags and returned time.
docs/​How-to/​configuration.md Updates configuration guidance for out-of-range entropy.

Review effort: Balanced

@codecov

codecov Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.30%. Comparing base (65e0d18) to head (8623fac).
✅ All tests successful. No failed tests found.

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##             main      #98   +/-   ##
=======================================
  Coverage   98.30%   98.30%           
=======================================
  Files          44       44           
  Lines        7960     7982   +22     
  Branches      948      952    +4     
=======================================
+ Hits         7825     7847   +22     
  Misses         63       63           
  Partials       72       72           
Flag Coverage Δ
nightly 98.30% <100.00%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/aragog/config/energy.py 100.00% <ø> (ø)
src/aragog/solver/entropy_solver.py 96.66% <100.00%> (+0.03%) ⬆️

The page now opens with what the reader wants to do, and gives each integrator, the tolerances, a stalled call, the EOS table guard and the EOS floor their own sections. Refusal rules, root location limits and lock measurements go in collapsed sections at the end. The guard text in the configuration and netCDF pages shrinks to a sentence with a link.
The lock table has one row per setup, a measured rate reads as measured, and the band of a start at the table has a name. The rule for a call that ends below the table moves to the floor refusals.
@timlichtenberg
timlichtenberg merged commit 71e6d03 into main Nov 2, 2026
20 of 22 checks passed
@timlichtenberg
timlichtenberg deleted the tl/aragog-eos-table-guard branch November 2, 2026 18:38
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