Repository navigation
Add from_mantle_redox fO₂ source: melt Fe³⁺/Fe²⁺ tracking with crystallization - #948
planetmariana wants to merge 42 commits into
Conversation
…adder SciPy returns status=1 for a terminal event, which on the Aragog scipy path can only be the per-cell step cap (_phi_cap_event_factory). That is a successful integration stopped early at the event time, not a failure: Aragog itself leaves stop_early False, and the CVODE path already normalises the equivalent root-found return (flag==2) to status=0. run_solver tested only `out.status == 0`, so it booked these valid truncated steps as failures and entered the retry ladder. The ladder cannot help: the cap's phi0 reference is captured at solve entry and each retry restores the same IC, so the crossing sits at a fixed physical time and the event re-fires in the same place however small dt gets. Every attempt failed identically, and three consecutive coupling steps of that aborted the run. Accept status=1 when dt_actual > 0; the caller already advances the coupler by dt_actual, so a truncated step carries the right end time. The dt_actual > 0 guard keeps a genuine zero-progress event retrying rather than stalling the coupler. Exhaustion messages now name the T_core-jump reason when the solver did integrate but every result was rejected by the jump guard, rather than reporting a misleading status. Also document from_mantle_redox as an fO2_source option in all_options.toml. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #948 +/- ##
==========================================
+ Coverage 94.44% 94.75% +0.31%
==========================================
Files 139 143 +4
Lines 23569 24217 +648
Branches 3944 4021 +77
==========================================
+ Hits 22259 22947 +688
+ Misses 882 879 -3
+ Partials 428 391 -37
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…into store_profile_snapshot
timlichtenberg
left a comment
There was a problem hiding this comment.
Thanks Mariana, this is a lot of careful work, and the supplement makes the model easy to follow. I ran the branch on the tutorial Earth case with a fixed-fO2 control and compared it with Schaefer et al. (2024). Before it leaves draft:
Use the Fischer et al. (2011) IW buffer, our default. The offset is taken against the O'Neill and Eggins buffer, but CALLIOPE adds it to its default Fischer et al. (2011) buffer, so the atmosphere gets a different fO2 than the tracker computes: +1.6 log units at 4240 K, +1.1 at 3000 K. Please compute the offset against Fischer et al. (2011) and test the value CALLIOPE receives.
Leave metal saturation out of this PR. With it on, Fe3+/FeT goes from 0.10 to 0.53 and the offset to IW+7, the whole-mantle outcome Schaefer et al. call implausible (p. 14), and it follows from the metal-forming cell sitting at the 5000 K validity limit. Let's focus this PR on the redox evolution without metal saturation, and come back to it in a follow-up PR once the EoS is extended far enough to allow metal saturation at the base.
A resume restarts the tracker. After a resume at 1722 yr, Fe3+/FeT goes from 0.155 to 0.03 for one step. The state needs to go into the snapshot.
Eq. 21 is also used far above its 1500 to 1900 K calibration, and the partition coefficients differ from the Schaefer et al. values above 15 GPa; a stated validity range and a benchmark against their mean curve would cover both. Please also remove proteus_update_20260929_164059_24312.log (it holds your home paths) and merge main. I can send the configs and the comparison plots.
There was a problem hiding this comment.
Thanks Mariana, the resume works now. The crystallisation bookkeeping also holds up against Schaefer et al. (2024) when I drive update_melt_redox with their layer sequence and coefficients.
On the comparison, keep the melt-mass axis. PROTEUS has no magma-ocean base that moves up: the whole mantle is a mush that freezes at all depths together. The melt mass fraction that remains is the one axis both models share. Label it as that, with their base pressure as a second axis.
The disagreement comes from D(Fe2+). 0.85 is their bulk FeO coefficient for the fertile upper mantle (p. 5). In the lower mantle and the transition zone their Fe/Mg exchange gives a smaller D(Fe2+) than D(Fe3+) (p. 11), about 0.46 against 0.84 for the bridgmanite layers in their archived output, so their Fe3+/FeT falls there while ours rises. With their bulk coefficients per layer, the coupled run stays within 0.012 of their mean down to 10 % melt. I would split the validation into a benchmark in their set-up, as a unit test, and the coupled run with the offset compared at their adiabat temperature.
The coupled run with your coefficients (orange) and with theirs (green) against their 1000 draws; (c) and (d) show the two temperature conventions and how the PROTEUS mantle freezes.
Make both sides use Hirschmann (2021). The offset is against it now, but CALLIOPE still adds the offset to Fischer et al. (2011): the atmosphere is +0.35 log units at 4263 K to -0.65 at 700 K off the tracker's fO2. CALLIOPE#39 will make Hirschmann the default there (see my comment on it); this PR then needs that CALLIOPE version as its floor, and a test of the value CALLIOPE receives.
Metal saturation stays off by default. As we discussed, the code can stay in. We work on it in a follow-up PR. With it on, my run reaches 98 bar of O2 in the first 12 yr and ends at Fe3+/FeT 0.79. Please also rename the PR to match, without the metal saturation in the title.
Smaller: a cell that remelts returns no iron to the melt (Fe3+/FeT 0.219 against 0.192 with the return in my run), the page should state the 1468 to 2197 K calibration of Eq. 21, and redox.py.cryst can go. The PR description and the redox.py docstring still describe the earlier state (the O'Neill and Eggins buffer, the metal step always on, tools/redox_step.py), and 26 comment blocks are over 3 lines. I can send the configs and the benchmark script.
from_mantle_redox fO₂ source: melt Fe³⁺/Fe²⁺ tracking with crystallization and metal saturationfrom_mantle_redox fO₂ source: melt Fe³⁺/Fe²⁺ tracking with crystallization
|
Thanks for the review @timlichtenberg these are the latest changes from your comments. I'm pinning it here now that I'm done, so please take your time and have a look once you're back in the office:
Now regarding why PROTEUS was bending down the Fe3+/FeT ratio, based on your previous comment, I found out this: Difference from Schaefer et al. (2024) late in crystallisation. PROTEUS keeps Fe3+/FeT bending down at the end, while S24 bends up. This comes from how solid forms, not from the partition coefficients:
|
timlichtenberg
left a comment
There was a problem hiding this comment.
Thanks Mariana, the remelting now returns its iron and the resume keeps the tracker, and the default path is unchanged. Two things before this can go in, and a few smaller ones.
CALLIOPE still adds the offset to Fischer et al. (2011). The tracker computes the offset against Hirschmann (2021), so the atmosphere sits -0.65 log units off the tracker's fO2 at 700 K and +0.35 at 4263 K. The current test only checks that the offset arrives in CALLIOPE, not the fO2 it then uses.
How I would fix it: wait for CALLIOPE#39 to be released, then raise the fwl-calliope floor in pyproject.toml to that release in this PR. Add a test that runs the CALLIOPE call with a known tracker state and compares the absolute log10 fO2 CALLIOPE uses with the tracker's at two temperatures, one cold and one hot. Once CALLIOPE#39 is merged, make the buffer a PROTEUS setting that the tracker and CALLIOPE both read (fO2_model on the CALLIOPE side), so the two cannot drift apart, and refuse a config where they would differ.
Late in the freeze the tracked melt holds more iron oxide than melt mass. In a run to a melt fraction of 1e-4, FeO over melt mass goes from 0.09 at the start to 0.70 at the default phi_crit of 0.01 and above 1 below a melt fraction of 0.005. Over the same stage the offset passed to CALLIOPE falls from +3.1 to -1.4, so this state sets the outgassing.
The cause is the explicit step: with a constant D below 1 it has no bound. It can also drive n_fe3_melt negative with the bulk_BSE coefficients when a step freezes most of a layer, and it leaves iron in a melt of zero mass when the last melt freezes.
How I would fix it: replace the explicit update by the per-step closed form, either batch, n(1-s)/(1-s+sD), or Rayleigh, n(1-s)^D. Both stay within [0, n] for any step and empty the melt at s = 1, so the result no longer depends on how coarse the late steps are. Then stop updating the tracker below a stated melt fraction and keep the last state, and add tests for a step that freezes the whole layer and for the melt FeO staying below the melt mass.
Smaller:
- The PR description still describes the earlier state (metal check after every step,
PHI_SOLID0.05, resume fromferric_fraction_initial, O'Neill and Eggins buffer, 5000 K cut). _reject_reserved_fO2_source(_planet.py:184) is never called, and its message says the source has no runtime path yet; it can go.- The redox pages carry dated history (
docs/Explanations/redox.md:223,:454anddocs/Validation/interior_chem/redox.md:7,:48) and a figure that has not been regenerated;redox.md:49saysmetal_saturationmust be false, while the config accepts true. - Two tests cannot fail:
test_reaction_gibbs_is_insensitive_to_the_reference_state_choicebuildsshiftedfromdirectplus and minus the same term, andtest_planet_validators.py:76asserts on its own parametrize list.
| every species must cancel. This is what lets the module carry three | ||
| polynomials instead of five. | ||
| """ | ||
| import proteus.interior_chem.disproportionation as dispro |
| below MELT_FRACTION_MIN. Fractional crystallisation with constant | ||
| D < 1 enriches the melt as F**(D - 1), so without the cutoff the melt | ||
| iron oxide exceeds the melt mass before the melt is gone.""" | ||
| import proteus.interior_chem.redox as redox |


Description
Adds
planet.fO2_source = "from_mantle_redox". Instead of a fixed offset, the atmospheric fO₂ now comes from the melt's Fe³⁺/Fe²⁺ ratio, which is tracked as the mantle crystallizes. This follows Schaefer et al. (2024, JGR Planets) and uses the Deng et al. (2020) melt equation of state.Potentially closes #653
New module
src/proteus/interior_chem/redox.py:update_melt_redox, called once per coupling step frominterior_energetics/wrapper.pyafter the interior solve.fO2lowP_H22.m. It's evaluated atT_out = max(T_magma, outgas.T_floor), the temperature CALLIOPE and atmodeller solve at.T_out. Written tofO2_shift_IW_mantle, which CALLIOPE and atmodeller use as their fO₂ constraint.PHI_SOLID). The solver's own φ is not modified.disproportionation.py: equilibrium constant, metal activity and the solve for how much metal forms.eos_deng.py: tabulated pressure integrals from the Deng et al. (2020) EOS, for both the disproportionation and the FeO → FeO₁.₅ oxidation reactions. Valid for T ≤ 5000 K (Deng's stated modelling range) and P ≤ 136 GPa.Outputs
Helpfile columns:
fO2_shift_IW_mantle,ferric_frac_mantle,a_fe_max_mantle,a_fe_max_cell_mantle,n_fe_metal_mantle,n_fe_metal_step_mantle,fe_metal_kg_mantle. The six besidesfO2_shift_IW_mantleare diagnostics;fO2_shift_IW_mantlecan be filled with zeros when resuming an older helpfile.Interior snapshots:
log10_fO2_sandfO2_top_indexare appended todata/<time>_int.nc, and also kept when the final snapshot is rewritten.data/<time>_redox.nc.Config
planet.ferric_fraction_initial(default0.1).from_mantle_redoxis rejected unlessinterior_energetics.moduleisspideroraragog.Outgassing:
outgas/wrapper.py,calliope.pyandatmodeller.pynow accept the new fO₂ source.Docs: regenerated
docs/Reference/config/anddocs/Reference/output_schema.json, and updatedinput/all_options.toml.Validation of changes
Unit tests:
tests/interior_chem/(67 tests: redox, disproportionation, Deng EOS) and the new time-step cap tests intests/interior_energetics/test_timestep.py. They cover:T_outrule for ΔIW, including a regression test that freezing the shallow melt doesn't shift ΔIW;.ncfiles;Local test run (macOS 13.4, Python 3.12.4):
tests/interior_chem,tests/utils/test_coupler.py,tests/interior_energetics/test_timestep*.py,tests/config,tests/tools/test_generate_config_reference.py. 607 passed.tests/interior_energetics/test_aragog.py, which fails 31 tests locally because of an outdatedfwl-io. Cleanmainfails the same 31 in this environment.tools/generate_config_reference.py --checkpasses.Cross-checks:
Simulation runs:
Note
Merge fix included: while merging
main, areturn outon Aragog's success path was lost, and the redox helpfile keys were duplicated. Every accepted step was then counted as a failure, and runs stopped at the first step (T_surf = 0.0 K). Fixed in 696963a; the resultingaragog.pydiffers frommainonly by the snapshot hook.Known limitations
ferric_fraction_initial.Checklist
netCDF4andscipyare already required)