Skip to content

Merge main and keep the mole columns in step with the trapped masses - #993

Merged
IKisvardai merged 15 commits into
ik/volatile_partitioningfrom
tl/volatile-partitioning-fixes
Oct 12, 2026
Merged

IKisvardai merged 15 commits into
ik/volatile_partitioningfrom
tl/volatile-partitioning-fixes

Conversation

@timlichtenberg

Copy link
Copy Markdown
Member

Description

@IKisvardai this is my review of #928, as changes on top of your head 4437b6f. The frozen-mantle drift from my last review is closed: with 42be63c the two debits remove the same mass, and coupled runs through the whole escape stage close for every element. What was left is in this PR, so that #928 can go in.

  • Merge of the current main. Your branch conflicted in tests/integration/golden_run.tsv, and the unit jobs of Trap volatiles in the solid mantle behind the freezing front #928 stopped at the AGNI data fetch before any test ran, because the head did not have the pin bump of Bump the AGNI pin to restore the data fetch in CI #982. The golden file is recorded again with --record-golden on the merged code: all 892 columns of main's trajectory are unchanged, and the trapping columns are added.
  • The mole columns follow the masses when trapping moves mass. _move in trapping.py changed *_kg_solid and *_kg_liquid but not *_mol_solid and *_mol_liquid. In the molten regime the next chemistry solve wrote them again; with freeze_volatiles no solve follows, so they went stale. _move now sets both from the masses.
  • trap_n_exited, trap_frac_bound and trap_n_substeps are removed, with BRANCH_UNUSABLE. No code computed them; they were 0 on every row while output.md and the schema described them as measurements.
  • Texts. trap_branch = 0 means "no front", and a freezing step with that code still buries by crystal partitioning. The validation page and the mass_thresh description say what the remelt test and the escape floor do now. trapping.py takes secs_per_year from utils.constants.
  • Four shorter spots in the outgassing code, with no change of behaviour.

One sentence of the description of #928 needs your edit. "With 'none' the run is the same as on main" has an exception now: in a mantle frozen with freeze_volatiles under escape, each element's atmosphere loses exactly the mass that escape takes from its total.

Open points that I did not change and that do not block #928; an issue for each is enough:

  • the escape rate columns with escape.reservoir = "bulk" and trapped mass (the rate share and the debit share of an element differ in a probe on hand-built rows; no run reaches it yet);
  • dummy outgassing with a noble gas and trap_mode = "front": in a probe the closure check raises for He at the first step, and no validator refuses the combination;
  • outgas.vapourise = true with trapping on, and a resume with trap_mode = "none" of a run that holds trapped mass.

With this merged into your branch and the checks of #928 green, #928 is ready to merge from my side.

Validation of changes

macOS (arm64), Python 3.12. The checks of the repository run for pull requests into main only, so they run on #928 after this merge, not here.

  • A coupled run with trap_mode = "front", freeze_volatiles and escape, to 2e4 yr, on the commit of that change. Before, on 4437b6f run to 8e5 yr, mol * mmw / kg of H2O in the solid fell to 0.58 and in the melt rose to 1.20, and 1813 of 1848 rows were off by more than 1e-3. After, no row is off, for H2O, CO2, N2 and CO in melt and solid. The control with trap_mode = "none" has no such row before or after.
  • A new unit test buries and releases mass and checks the mole columns, and the 16-step remelt test through the main loop checks them on every row. Both fail when the two new lines of _move are taken out.
  • Golden file against main: 892 columns before, 926 now; the 34 new ones are trapping columns, and no line of a column of main differs.
  • Unit tier on the tip against the branch source: 5779 passed, 11 skipped.

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

Move the AGNI pin from 2180f489 to 21691545 and regenerate docs/Reference/module_versions.md. The new commit downloads each Zenodo record file by file in src/get_data.sh, because Zenodo no longer serves a whole record as one zip; it differs from the previous pin in that one file only.

The CI set-up step runs get_data.sh from the pinned checkout, so with the old pin no job that installs AGNI reached its tests.
With the Zalmoxis structure, ExtendHelpfile warned on every row whose loop had a structure solve that the key M_int_target is not declared in GetHelpfileKeys(). The key is the mass target of that solve, and the mass-anchor check reads it directly after. It is not kept from one row to the next, because the main loop builds each row from the helpfile table, which has the schema columns only, and each solve writes the key again. So the warning pointed at nothing that is lost.

M_int_target is now on the allowlist of non-schema keys, which moves to module level as _ALLOWED_NON_SCHEMA_KEYS in utils/coupler.py. No helpfile column is added and no numeric path changes.

The test of the warning has all three allowlisted keys in its row, plus a key that extends an allowlisted name, and compares the listed keys exactly. A new static test checks that every literal hf_row[...] key written under interior_struct/ is a helpfile column, an allowlisted key or private, that each allowlisted key has a writer, and that none of them is a column.
The accretion module can take the atmospheric loss of a giant impact from the Roche et al. (2026) law as well as from Kegerreis et al. (2020), both evaluated in ZEPHYRUS 26.10.10. The new setting accretion.atmloss_law selects the law when atmloss_module = "zephyrus". Its default is "roche2026", so a config with atmloss_module = "zephyrus" and no atmloss_law switches from the Kegerreis to the Roche law; atmloss_law = "kegerreis2020" keeps the old behaviour, which is identical to before.

The Roche law takes the target mass without its atmosphere, with the atmosphere mass fraction from the PROTEUS planet; masses, radii, impact parameter and contact speed come from the impact event. The atmosphere fraction counts the rock-vapour elements, but only the volatile elements are stripped. Each impact logs the law, the atmosphere fraction, the speed ratio, gamma and the loss fraction, and one warning per impact names each parameter that is outside the fitted range of the law or at a clamp.

The loss helpers move from accretion/wrapper.py to accretion/atmloss.py. The floor of fwl-zephyrus goes to 26.10.10. The config reference, the validation page of the accretion wrapper and the coupling-loop page describe both laws and the caveat that both are fitted on H2-He atmospheres.

Closes #407.
The default of outgas.fO2_shift_IW goes from 4.0 to 2.0, two log units above the iron-wüstite buffer. input/all_options.toml, input/minimal.toml and the Earth and Venus tutorial configs take the same value, so the default, the reference config and the tutorials agree. A config that does not set the key now runs at IW+2; a config that sets the key runs as before. The first atmosphere of a run at IW+2 is much thinner than at IW+4, because the O2 partial pressure follows the buffer.

The CHILI intercomparison keeps its protocol value IW+4. tools/chili_generate.py sets the redox state of all 5 cases from one constant, PROTOCOL_FO2_SHIFT_IW, and refuses a case that lists a value of its own; the 7 files that it writes under input/chili/intercomp/ are unchanged. The 9 grid configs under input/tutorials/chili_grid/ and input/planets/toi561b.toml keep the value 4. The generator renders all 7 files before it writes any of them, so a refused case leaves the committed files as they are.

The CHILI tutorial page runs input/chili/intercomp/earth.toml and venus.toml in place of the tutorial configs, and tools/plot_chili_comparison.py names output/chili_earth/ and output/chili_venus/ in its help text. The Earth analogue tutorial page still describes the IW+4 run.
In a run with AGNI, the log had "Using fallback colour" for fe, feo, mgo, nh3, sio and sio2 each time the atmosphere chemistry plot was made, and the lower panel of that plot drew these 6 aerosols in magenta with lower-case names. AGNI writes the aerosol names of an atmosphere file in lower case, and the colour presets are keyed by the gas name.

The plot now matches each aerosol to the gas of the same file whose name is equal apart from case, takes the colour of that gas, and writes the gas formula in the legend. An aerosol with no such gas, or with 2 gases that differ only in case, keeps its own name. The colour lookup and the file reader are unchanged, and no model result changes.
With the Zalmoxis structure, the core mass was core_frac of the structure target at every structure solve. The structure target leaves out every volatile inventory, and with planet.fO2_source = "user_constant" that includes the oxygen inventory that the outgassing derives from the fO2 buffer at each call. This inventory is large over a hot magma ocean and falls as the surface cools, so the core mass of the structure followed it, although nothing moves mass through the core-mantle boundary.

The core mass target is now core_frac of the planet mass without the kept volatiles: H, C, N, S and the noble gases, and oxygen only when it is an input of the outgassing (planet.fO2_source = "from_O_budget", or outgas.module = "dummy"). Oxygen that the buffer derives is mantle mass, like rock vapour. With zalmoxis.dry_mantle = false only the atmosphere part of the kept inventories is taken off. Each structure solve at Time = 0 computes the target from its row; the target is set when the init stage ends, stored in the new helpfile column M_core_target, and read by every later solve. A giant impact adds core_frac of the impactor rock to it. Zalmoxis gets the fraction M_core_target / structure target, and the run stops with an error when the target leaves no mantle, or no ice layer in a structure that has one.

A run that resumes from a helpfile without the column continues on core_frac of the structure target of its row, logs one warning, and keeps that core mass. A stored target that is NaN, infinite or negative stops the run.

For 1 Earth mass, core_frac = 0.325 and a dry mantle, the core mass target rises by 3.7 % at fO2_shift_IW = 4 and by 0.10 % at fO2_shift_IW = 2. The helpfile column M_core is the mass inside the first grid node at or above the boundary, so it stays above the target by a part of one mantle shell. The interior config reference describes the rule and the output reference lists the new column.
A Zalmoxis + Aragog run solves the structure after the interior step of an iteration, and Aragog takes the new mesh at the start of the next iteration, where it records the compression work of the update and carries the entropy to the new mesh by mass parcel. A run that stopped between the two left an interior snapshot on the earlier structure and a helpfile row on the new one. The resumed process built Aragog on the structure of the row and never made that update, so the resumed run differed from the uninterrupted run from its first row.

On resume, aragog_resume.pending_structure_update now finds the earlier row and a mesh file of the earlier structure: the copy saved with that row, zalmoxis_output.dat.prev, or the copy saved with an older row on the same structure. Aragog is built on the earlier structure, the snapshot is restored, and the update is replayed through the helper that the running loop uses. The replay runs for core_bc = "energy_balance". With another core boundary condition, with a giant impact between the snapshot and the row, or with no mesh file of the earlier structure left, the run continues on the structure of its row as before, and one warning says so. A snapshot whose entropy does not fit the solver stops the resume with an error.

The radiogenic heat F_radio of a row is now sampled at the end of the step, the time of the other quantities of the row. It was sampled at the start of the step plus the length of the previous step. This is the only change for an uninterrupted run: the decay constant times the shift of the sample time, 5e-8 at most with K, U and Th in a run of 11 rows, and larger for a short-lived isotope such as Al-26.

The troubleshooting page has a section on when a resumed run equals the uninterrupted one, and the error index lists the two warnings.
The docs show the Earth and the orbit tutorial figures in a light and a dark variant. With a dark matplotlib style, plot_global, plot_fluxes_global and the orbit plots gave a figure that could not be read, because they fixed some neutral colours: a black total pressure line, a dark grey for the surface temperature, net flux and melt fraction curves, and white boxes behind the panel labels.

These artists now take their colour from the style when the plot is drawn. The new helper style_neutral(fade) in utils/plot.py mixes the text colour of the style towards its axes face colour; a transparent axes face stands for the figure face colour. The 3 plot files use it for the lines, labels, label boxes, legend frame and grid that were fixed before. With the default style every colour is the same as before. Species and module colours are not touched, and no model result changes.
)

New clones of the Python modules (MORS, JANUS, CALLIOPE, ZEPHYRUS, Aragog, Zalmoxis, VULCAN, Morrigan) start at the release tag of their minimum version in pyproject.toml, so an install holds the module code that PROTEUS names. tools/get_python_module.sh clones the four modules that had no setup script. Aragog and Zalmoxis still take the branch named like the PROTEUS feature branch when the module has one.

The setup scripts leave a module folder that is already there as it is: they print its state, install it editable and, for a clone behind its tag, print the commands that move it by hand. Only --force replaces a folder, through a temporary clone, and a failed step or a signal leaves the old folder in place.

proteus doctor runs nothing in these clones. A module below its minimum version gets advice, and a folder that is not a git clone of its own gets no git command. The essential data set of install.sh includes the PHOENIX grid of the default star. tools/check_floor_tags.sh checks that each minimum version has exactly one tag on its remote. tools/_get_common.sh unsets the GIT_DIR family of variables for every script that sources it.
tests/integration/golden_run.tsv is the file of main recorded again with the merged code (pytest tests/integration/test_integration_golden_run.py --record-golden): the 892 columns of main are reproduced and the 37 trapping columns are added as constant 0.
A burial or a release changes the mass of a species in the melt and in the solid. Its mole columns now follow in the same step. Before, only the next chemistry solve wrote them, so in a frozen or desiccated mantle, where no solve follows, the mole columns of the solid and the melt kept the values of the last solve while the masses moved.
…texts

trap_n_exited, trap_frac_bound and trap_n_substeps held a constant in every row: nothing in the trapping step computes them. They leave the helpfile keys, the output reference and the recorded trajectory, and the unused branch code 4 leaves compaction.py. The trajectory is recorded again; its other 926 columns are unchanged.

The description of trap_branch says that a step with code 0 can still bury mass by crystal partitioning. The mass_thresh text says that escape empties an element by its reachable mass, down to the trapped mass. The validation page states what the 16-step remelt run through the main loop ends with. The trapping step takes the length of the year from utils.constants.
The output reference lists the trapping step as a writer of the mole columns of the melt and the solid. The test of the initial stage asserts the trapped melt fraction in place of the removed columns.
ledger_total is one sum, the frozen-mantle rescale takes its fallback with or, the trapping names come in one import in wrapper.py, and atmodeller.py imports the stoichiometry table under its module name. No behaviour changes.
The 16-step remelt run through the main loop asserts that the moles of H2O and N2 in the melt and in the solid equal mass over molar mass on every row after the row that the test writes. The docstring of the move states that it covers the melt and the solid columns and leaves the mole total as it is. The compaction tests take the length of the year from utils.constants.
@timlichtenberg
timlichtenberg requested a review from a team as a code owner October 12, 2026 08:11
@IKisvardai
IKisvardai merged commit 8364bd3 into ik/volatile_partitioning Oct 12, 2026
@IKisvardai
IKisvardai deleted the tl/volatile-partitioning-fixes branch October 12, 2026 09:05
timlichtenberg added a commit that referenced this pull request Oct 12, 2026
The tree is unchanged: it already holds main at 7272543 through the squash of #993. This commit records main as a parent, so the branch merges into main without a conflict.
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