Skip to content

Give the padded surface node its EOS density and Gauss gravity - #115

Merged
timlichtenberg merged 19 commits into
mainfrom
tl/surface-node-eos-gravity
Oct 23, 2026
Merged

timlichtenberg merged 19 commits into
mainfrom
tl/surface-node-eos-gravity

Conversation

@timlichtenberg

@timlichtenberg timlichtenberg commented Oct 23, 2026 •

Copy link
Copy Markdown
Member

Description

Closes #96.

PROTEUS reads the padded node in FormingWorlds/PROTEUS#1045.

The outward integration stops when the pressure crosses zero inside the last shell, and the nodes past the stop are padded with P = 0 and the stop mass. Those nodes had density 0 and the gravity of the stop node. A padded node now gets:

  • the EOS density at target_surface_pressure, at its temperature, in the layer of the stop mass (the volatile profile only in the mantle);
  • the gravity G m_stop / r² at its own radius;
  • with temperature_mode = 'adiabatic_from_cmb' at adiabat blend 1, the adiabat continued in ln P from the last node with P > 0 to the target surface pressure. Surface-anchored modes keep surface_temperature, and a result accepted mid-ramp keeps the linear profile at R.

The invariants now read every node: positive and density_eos include the padded nodes (looked up at the target surface pressure), and padding requires P = 0, the stop mass and g r² = G m_stop. Three places outside the profile now see the padded density: the midpoint shell integrals of solve_miscible_interior and solve_strong_partition, and the density seed of a retry, which now starts from the EOS density at the padded node; process_flow.md describes the integrals. On input/default.toml the live nodes and every scalar are unchanged (table below). A strong-partition or miscible solve with melting curves can move its converged partition; no comparison here covers that. PROTEUS reaches neither: it refuses global_miscibility at config load, and only zalmoxis/output.py calls solve_strong_partition.

Physics impact on input/default.toml (my run, wall_timeout 1e6, base 51e1705 vs this branch; both accept strict with 1 padded node, 149 of 150). Every other result entry is equal.

Output at the padded node 149 Before After
density [kg/m³] 0 2323.14866525
gravity [m/s²] 9.3613768 9.35745032
g r² / (G m) - 1 4.2e-4 0.0

Validation of changes

macOS, Python 3.12.

  • The 6 changed test files at the branch head: 306 passed; 4 tests in tests/test_invariants.py failed in my checkout only because it lacks the Chabrier H, PALEOS H2O and Seager iron tables. The nightly runs them in its accept_solves (1) and accept_solves_long (3) jobs.
  • pytest tests/test_jax_event_termination.py at the head: 8 passed. Its test_pad_holds_the_event_state is marked slow only, and no job of nightly.yml selects such a test, so it does not run in CI; I ran it locally.
  • The 4 PALEOS smoke tests of tests/test_surface_node.py force a padded solve and assert exactly one padded node; they run in the nightly main job.
  • Nightly on the head, 6 of 6 jobs green: https://github.com/FormingWorlds/Zalmoxis/actions/runs/39934707760

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

When the structure integration stops inside the last shell, the nodes past the stop are the surface. The returned density there is now rho_EOS at the target surface pressure and the node's temperature, from the layer of the stop mass, instead of zero, so a consumer that integrates the density over the nodes no longer loses half of the outermost shell. The Picard iteration and its convergence test are unchanged; only the returned profile differs at the padded nodes.
A padded node keeps zero pressure and the mass at the stop, carries the gravity of that mass at its own radius and the EOS density at the target surface pressure; positive and density_eos read it, the latter with the lookup at the target pressure. The test sphere and the padding helper build their surface node that way.
The padded nodes past a surface stop are the surface: positive and density_eos now read them, density_eos with the lookup at the target surface pressure, so a zero or non-EOS surface density fails the run. padding keeps zero pressure and the stop mass but no longer requires the gravity at the stop: no mass lies above the stop, so a padded node carries the gravity of the stop mass at its own radius.
…dius

No mass lies above a surface stop, so each padded node has g r^2 = G m_stop. The stop-pad tests, a Hypothesis test over the stop state and the surface pressure limit, the invariant test of a padded node with the gravity at the stop and the real padded solve now check that.
No mass lies above a surface stop, so pad_after_stop now fills the padded nodes with g = G m_stop / r^2 at each node instead of the gravity at the stop, which was too large by about 2 (R - r_stop) / R at the outer radius. The gravity invariant reads every node but the centre, the padded nodes included. The JAX padding test now expects the same fill.

The docs state the residual: a consumer that integrates the padded surface density over the nodes counts the shell between the stop and the outer radius, which the enclosed mass does not include.
…node

A CMB-anchored adiabat sets the surface temperature, so a padded node takes the adiabat at the target pressure, not the configured surface_temperature. The surface lookup reads the target pressure, the node temperatures and the layer of the stop mass, with the volatile profile only in the mantle, and stops the solve on a density that is not finite. padding holds the padded gravity to G m_stop / r^2 also at the Picard tolerance. A surface stop at the centre keeps the stop gravity there.
…its lookup

With the adiabat integrated from the core-mantle boundary, the configured surface_temperature is not the temperature at the surface, so a padded node now takes the adiabat at the target surface pressure, which keeps its temperature and EOS density continuous with the node below; surface-anchored modes keep surface_temperature. The surface lookup is one helper that stops the solve on a density that is not finite, as the Picard update does.

padding now holds the padded gravity to G m_stop / r^2 itself, so the gravity at the stop is refused also at the Picard tolerance; two invariant tests set the padded gravity to match their mass or expect padding. The docs name the midpoint mantle integrals among the sums that count the shell above the stop.
…olerance

A padded node of a CMB-anchored adiabat takes the adiabat continued from the node below to the target pressure, checked against an independent integration in ln P; the continuation is exact for nabla_ad linear in ln P and holds T without a gradient or a pressure drop. A non-finite surface density is returned for the invariants rather than raised, so a Newton trial does not stop the solve. padding refuses a padded gravity 1e-9 off G m_stop / r^2.
The interpolated adiabat only held the temperature of the last node with positive pressure, which on a 50-layer grid sits about 4 GPa below the surface. At result assembly a padded node of a CMB-anchored adiabat now takes the adiabat integrated in ln P from that node down to the target surface pressure with the EOS nabla_ad; surface-anchored modes, an external temperature function and temperature arrays keep their own temperature. The temperature closures are back to their form on main, so the iteration is unchanged.

The surface lookup returns a non-finite density as it is: in an accepted result it breaks positive, and a Newton trial no longer stops the solve. The docs describe the continuation and how the midpoint mantle integrals take the shell that crosses the stop.
A CMB run accepted before the adiabat blend reaches 1 keeps the linear profile at R for its padded node. The continuation reaches the exact solution of a T-dependent nabla_ad only with the predicted midpoint temperature, holds T when the midpoint gradient is missing and returns a non-finite start temperature as it is. Both padded lookups receive the arguments of calculate_mixed_density_batch after the mixture, in that order.
…p arguments

The continuation to the surface now runs only once the adiabat blend has reached 1; a CMB run accepted earlier in the ramp keeps the linear profile at R, as its other nodes are not on the adiabat either. Both padded lookups take one argument tuple in the order of calculate_mixed_density_batch, and the continuation reorders it for get_mixed_nabla_ad; it returns a non-finite start temperature as it is.
… and the hold

On a PALEOS 2-phase mantle with its melting curves and condensed_rho_scale apart from binodal_T_scale, both padded lookups get every argument of the solve in place. A CMB run accepted at blend 0.25, 0.5 or 0.75 keeps the linear profile at R. The continuation keeps the temperature of its completed steps when nabla_ad goes missing, logs one warning naming the pressure, and holds T when the midpoint gradient is negative.
…ntion

The continuation logs one warning naming the temperature and the pressure from which it holds T. The docs state that a result accepted before the adiabat blend reaches 1 keeps the linear profile at the outer radius, so its padded temperature can lie above the point below it, with the jumps of a 50-point 1 Earth mass run.
…ng message

A padded node of the surface-anchored adiabat of input/default.toml keeps surface_temperature. The continuation stops at 100 K under a gradient that would take T lower. The padding error names the expected mass and the gravity G m / r^2 of the node it reports.
The node-temperature tests map every batch EOS call to a solved pressure profile; a solve that pads makes one more lookup at the target surface pressure, which matches no profile and is not a density update, so the spy now passes it through.
…ound-off

Whether a converged solve stops inside the last shell depends on round-off: the analytic solve padded on macOS and not on the Linux runner. The real-solve tests now lower each root of the central-pressure search by 1e-5, so the integration stops tens of metres below R within the accepted pressure residual and the outer node is padded on every platform. The mid-ramp case at blend 0.75 is dropped: its max_iter accept returns a best pass that is not padded.
A strict accept allows a surface pressure residual up to pressure_tolerance (1e9 Pa), so lowering the central-pressure root by 1e-5 leaves some converged solves unpadded on Linux (P[-1] 11 MPa at 1 atm target). The surface residual is not monotone in the central pressure at that scale (a root that pads can stop padding 1e-5 lower), so the wrapper now takes the root itself when it pads, or the root lowered by the smallest of 1e-5 to 1e-2 whose residual is that of a padded solve, and records any root it cannot pad; every test asserts exactly one padded node and no miss. The 300 s wall clock is lifted in these solves, since its accept returns a best pass that need not be padded.
Padded nodes take the gravity G m_stop / r^2 at their own radius, so the test asserts that at every padded node and replaces the stop-radius check, which returned the outer node by construction, with a bound from the mass gained past the last live node: at most at the density of that node, it places the smallest stop radius inside the shell.
@codecov

codecov Bot commented Oct 23, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.24%. Comparing base (51e1705) to head (7b8118f).
✅ All tests successful. No failed tests found.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #115      +/-   ##
==========================================
+ Coverage   96.22%   96.24%   +0.02%     
==========================================
  Files          39       39              
  Lines        5692     5729      +37     
  Branches      952      958       +6     
==========================================
+ Hits         5477     5514      +37     
  Misses        105      105              
  Partials      110      110              
Flag Coverage Δ
nightly 96.19% <100.00%> (+0.02%) ⬆️

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

Files with missing lines Coverage Δ
src/zalmoxis/invariants.py 99.16% <100.00%> (+0.04%) ⬆️
src/zalmoxis/jax_eos/solver.py 100.00% <ø> (ø)
src/zalmoxis/solver.py 97.92% <100.00%> (+0.06%) ⬆️
src/zalmoxis/structure_model.py 100.00% <100.00%> (ø)

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.

🟡 Changes recommended

Positive padded densities reused as seeds can prevent inner density convergence and trigger unnecessary pressure solves.

1 open issue
What changed in this PR

This PR addresses #96 by giving padded surface nodes physically meaningful density and gravity for downstream consumers such as PROTEUS and Aragog.

Changes:

  • Returns EOS density at the target surface pressure and gravity at each padded radius.
  • Continues fully blended CMB-anchored adiabats to the padded surface.
  • Extends invariant checks, regression tests, and documentation for the updated padding behavior.
File Description
tests/​test_surface_stop_pad.py Checks radius-dependent padded gravity and stop-mass preservation.
tests/​test_surface_node.py Adds padded-solve density, gravity, and temperature tests.
tests/​test_surface_density.py Tests surface EOS lookup and adiabat continuation helpers.
tests/​test_node_temperature_arrays.py Distinguishes padded lookups from interior density updates.
tests/​test_jax_wrapper_branches.py Updates JAX padding expectations.
tests/​test_jax_event_termination.py Checks padded gravity and bounds the event location.
tests/​test_invariants.py Adds padded density and gravity invariant tests.
src/​zalmoxis/​structure_model.py Computes padded gravity from stop mass and node radius.
src/​zalmoxis/​solver.py Adds surface density lookup and CMB-adiabat continuation.
src/​zalmoxis/​jax_eos/​solver.py Updates the padding contract in documentation only.
src/​zalmoxis/​invariants.py Includes padded nodes in density and gravity checks.
docs/​How-to/​usage.md Clarifies padded output values.
docs/​Explanations/​process_flow.md Explains padding physics and downstream integral effects.

Review effort: Balanced

Comment thread src/zalmoxis/solver.py
@timlichtenberg
timlichtenberg merged commit a60b732 into main Oct 23, 2026
13 checks passed
timlichtenberg added a commit to FormingWorlds/PROTEUS that referenced this pull request Oct 23, 2026
The JAX-path rebuild in zalmoxis_solver looks a node with P = 0 past a surface stop up at target_surface_pressure, as Zalmoxis does since FormingWorlds/Zalmoxis#115, instead of at 0 Pa. The docstring of _resolve_zalmoxis_anchor_temperatures states the padded node's temperature.
@timlichtenberg
timlichtenberg deleted the tl/surface-node-eos-gravity branch October 23, 2026 17:02
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.

Zero density at the outer node of a converged structure

2 participants