Skip to content

Coupling to PROTEUS

This page is the practical recipe for using CALLIOPE inside a PROTEUS coupled run: which TOML blocks matter, which knobs are documented where, and which combinations are known to be safe.

For the underlying control flow (per-iteration sequence diagram, what the wrapper passes to equilibrium_atmosphere(), how warm-starts are constructed, what gets written back into hf_row), see the theory page.

Minimal [outgas] block

[outgas]
module       = "calliope"     # or "atmodeller" or "dummy"
fO2_shift_IW = 0.0            # log10 shift relative to IW buffer
mass_thresh  = 1e16           # kg; doubles as the absolute mass-balance
                              # tolerance and the per-element zero threshold
T_floor      = 700.0          # K, magma temperatures below this are clipped
solver_rtol  = 1e-4           # relative tolerance on mass balance
solver_atol  = 1e-6           # fsolve step tolerance (mapped to xtol, not atol)
h2_binodal   = false          # apply Rogers+2025 H2-MgSiO3 binodal override

  [outgas.calliope]
  include_H2O = true
  include_CO2 = true
  include_N2  = true
  include_S2  = true
  include_SO2 = true
  include_H2S = true
  include_NH3 = true
  include_H2  = true
  include_CH4 = true
  include_CO  = true
  solubility  = true          # if false, force Phi_global = 0 (no melt dissolution)

The four primary species (H2O, CO2, N2, S2) must be included; the wrapper will refuse to run with any of them set to false. The seven secondary species can be disabled individually if you want a constrained sub-system (e.g. include_NH3 = false to suppress ammonia chemistry).

Choosing the elemental-budget mode

CALLIOPE needs total H, C, N, S inventories in kilograms. PROTEUS gives you two ways to specify them via [planet]:

[planet]
volatile_mode       = "elements"        # or "gas_prs"
volatile_reservoir  = "mantle"          # or "mantle+core"; sets the reservoir for ppmw

volatile_mode = "elements" (most common)

[planet.elements]
use_metallicity = false      # if true, scales C/N/S to H from solar abundances

H_mode   = "oceans"          # "oceans" | "ppmw" | "kg"
H_budget = 1.0               # interpreted per H_mode

C_mode   = "C/H"             # "C/H" | "ppmw" | "kg"
C_budget = 0.1

N_mode   = "ppmw"
N_budget = 2.0

S_mode   = "ppmw"
S_budget = 200.0

The wrapper translates these into the four budget fields CALLIOPE expects:

  • H_mode = "oceans" β†’ hydrogen_earth_oceans = H_budget
  • H_mode = "ppmw" β†’ H_kg = H_budget * 1e-6 * M_reservoir
  • H_mode = "kg" β†’ H_kg = H_budget

C, N, S follow the same pattern; C/H is interpreted as a mass ratio relative to H.

volatile_mode = "gas_prs" (initial-pressure mode)

[planet]
volatile_mode = "gas_prs"

[planet.gas_prs]
H2O = 220.0     # bar
CO2 = 100.0
N2  = 1.0
S2  = 0.01
# secondary species default to 0.0 unless explicitly set

When volatile_mode = "gas_prs", the wrapper calls get_target_from_pressures(ddict) to back out the elemental inventory implied by the prescribed initial atmosphere; on every subsequent iteration the same elemental inventory is preserved.

Selecting the fO2 dispatch

The PROTEUS schema field [planet].fO2_source selects which CALLIOPE entry point the wrapper calls. The two paths share the same physics; they differ only in which quantity is supplied as input and which is solved for.

fO2_source Input Solved for Entry point
user_constant \(\Delta\mathrm{IW}\) atmospheric + dissolved O equilibrium_atmosphere
from_O_budget total O mass \(\Delta\mathrm{IW}\) equilibrium_atmosphere_authoritative_O

Under user_constant (the default) CALLIOPE buffers the redox state to the configured outgas.fO2_shift_IW and solves the four-equation H/C/N/S mass balance. The resulting O mass is whatever the equilibrium chemistry requires at that buffer, and is written into hf_row['O_kg_total'] so the rest of PROTEUS can read it. Under from_O_budget the wrapper passes the running whole-planet O total (maintained by the PROTEUS element-budget bookkeeping) as a fifth elemental target, uses outgas.fO2_shift_IW only as an initial-guess hint, and solves a five-equation system that returns the derived \(\Delta\mathrm{IW}\) in hf_row['fO2_shift_IW_derived'].

See Coupling to PROTEUS (theory) for the per-iteration control flow and Authoritative-oxygen mode for the augmented five-residual mass balance.

Worked example: buffered fO2 mode (user_constant)

In this mode the user fixes \(\Delta\mathrm{IW}\) and the chemistry returns the O budget. Set O_mode = "ic_chemistry" so the wrapper does not pre-populate an O target: the first outgas call writes one in, and PROTEUS carries it from there.

config_version = "3.0"

[orbit]
    semimajoraxis = 1.0                  # [AU]

[planet]
    mass_tot      = 1.0                  # [M_earth]
    volatile_mode = "elements"
    fO2_source    = "user_constant"      # buffered mode (the default)

    [planet.elements]
        H_mode   = "oceans"
        H_budget = 1.0                   # [Earth oceans]
        C_mode   = "C/H"
        C_budget = 0.1                   # [C/H mass ratio]
        N_mode   = "ppmw"
        N_budget = 2.0
        S_mode   = "ppmw"
        S_budget = 200.0
        O_mode   = "ic_chemistry"        # O is derived from the chemistry
        O_budget = 0.0                   # ignored under ic_chemistry

[outgas]
    module       = "calliope"
    fO2_shift_IW = 4.0                   # [log10] redox buffer offset (input)

    [outgas.calliope]
        include_H2O = true
        include_CO2 = true
        include_N2  = true
        include_S2  = true
        include_H2  = true
        include_CH4 = true
        include_CO  = true
        include_SO2 = true
        include_H2S = true
        include_NH3 = true
        solubility  = true

What the wrapper does on the first call: builds the four-element target \((m_\mathrm{H}, m_\mathrm{C}, m_\mathrm{N}, m_\mathrm{S})\) from H_budget etc.; calls equilibrium_atmosphere with fO2_shift_IW = 4.0; reads back the four primary partial pressures, the seven secondary species, the derived O_kg_total, and writes them all into hf_row. Subsequent iterations warm-start from the previous-iteration <s>_bar values and follow the same path.

Worked example: authoritative-O mode (from_O_budget)

In this mode the user fixes the total O mass and the chemistry returns the derived \(\Delta\mathrm{IW}\). The O_mode is one of "ppmw", "kg", or "FeO_mantle_wt_pct" (never "ic_chemistry", which the config-level validator rejects when fO2_source = "from_O_budget" because the chemistry needs a target to invert against). The outgas.fO2_shift_IW value becomes the initial-guess hint for the solver, not the buffered redox state; pick a value near the expected derived \(\Delta\mathrm{IW}\) for fast convergence, but the solver tolerates a poor hint at the cost of more Monte-Carlo restarts.

config_version = "3.0"

[orbit]
    semimajoraxis = 1.0                  # [AU]

[planet]
    mass_tot      = 1.0                  # [M_earth]
    volatile_mode = "elements"
    fO2_source    = "from_O_budget"      # authoritative-O mode

    [planet.elements]
        H_mode   = "oceans"
        H_budget = 1.0                   # [Earth oceans]
        C_mode   = "C/H"
        C_budget = 0.1                   # [C/H mass ratio]
        N_mode   = "ppmw"
        N_budget = 2.0
        S_mode   = "ppmw"
        S_budget = 200.0
        O_mode   = "FeO_mantle_wt_pct"   # interpret O_budget as mantle FeO wt%
        O_budget = 8.0                   # 8.0 wt% FeO ~ Earth's modern mantle

[outgas]
    module       = "calliope"
    fO2_shift_IW = 4.0                   # [log10] initial-guess HINT, not buffer

    [outgas.calliope]
        include_H2O = true
        include_CO2 = true
        include_N2  = true
        include_S2  = true
        include_H2  = true
        include_CH4 = true
        include_CO  = true
        include_SO2 = true
        include_H2S = true
        include_NH3 = true
        solubility  = true

What the wrapper does on the first call: builds the five-element target \((m_\mathrm{H}, m_\mathrm{C}, m_\mathrm{N}, m_\mathrm{S}, m_\mathrm{O})\), with \(m_\mathrm{O}\) derived from O_mode = "FeO_mantle_wt_pct" and O_budget = 8.0 via \(m_\mathrm{O} = M_\mathrm{O}/M_\mathrm{FeO} \times \mathrm{wt\%}/100 \times M_\mathrm{mantle} \approx 0.2227 \times 0.08 \times M_\mathrm{mantle}\); calls equilibrium_atmosphere_authoritative_O with fO2_hint = 4.0; reads back the four primary partial pressures, the seven secondary species, AND fO2_shift_derived (the redox state implied by the supplied O budget), writes them all into hf_row. The derived \(\Delta\mathrm{IW}\) appears in hf_row['fO2_shift_IW_derived']. Subsequent iterations carry the same authoritative O total (modulated by PROTEUS escape bookkeeping) and the redox state can drift across the trajectory.

Choosing between the two modes

Use user_constant when Use from_O_budget when
You want a fixed redox state for a parameter sweep (Nicholls et al. 2024 3 explored seven \(\Delta\mathrm{IW}\) values this way) You want whole-planet O accounting where escape, ingassing, and the mantle FeO inventory all debit the same O reservoir
You don't have an independent constraint on the planet's O budget You have an O constraint from an FeO-content estimate, a chondritic O/Si ratio, or an observational retrieval
Buffered chemistry is good enough for your scientific question The mantle redox state is itself the unknown you're trying to infer

The two modes give bit-identical results in the cases where they should: for any \(\Delta\mathrm{IW}\) accepted by user_constant, the chemistry returns an O budget; feeding that O budget back through from_O_budget recovers the same \(\Delta\mathrm{IW}\) to within ~0.05 dex (this is the round-trip property pinned by tests/test_authoritative_O.py::TestRoundTrip and documented on the authoritative-oxygen page).

Redox state

fO2_shift_IW is in \(\log_{10}\) units relative to the O'Neill & Eggins (2002) 4 IW buffer. Under fO2_source = "user_constant" this value is the buffer offset; under fO2_source = "from_O_budget" it is only the initial-guess seed. Common reference values:

\(\Delta\mathrm{IW}\) Description
\(-5\) Highly reduced (Mercury-like; sulphur-derived estimate IW-5.4, Cartier & Wood 2019 1)
\(-3\) Reduced (e.g. enstatite-chondrite-like; Mercury Fe-based estimate IW-2.8 to IW-4.5, Cartier & Wood 2019 1)
\(-1\) Moderately reduced; near the Mars-mantle source range (Wadhwa 2001 6 places the shergottite-source mantle at \(\approx\) IW)
\(0\) At iron-wΓΌstite buffer (core formation equilibrium at depth)
\(+3.5\) Sossi et al. (2020) 5 preferred Earth's mantle \(f_{\mathrm{O}_2}\)
\(+4\) CALLIOPE PROTEUS-side default; near-modern Earth upper mantle (within FMQ\(\,\pm\,2\) per Frost & McCammon 2008 2)

Sossi et al. (2020) 5 place Earth's modern upper mantle at \(\Delta\mathrm{IW} \approx +3.5\); Frost & McCammon (2008) 2 report a broader FMQ\(\,\pm\,2\) range across mantle settings (approximately IW+1.5 to IW+5.5). CALLIOPE defaults sit at \(\Delta\mathrm{IW} = 4.0\), consistent with a modern terrestrial composition.

Solver tolerances

The PROTEUS wrapper hard-codes a few solver knobs that override the CALLIOPE library defaults:

Wrapper override Value Reason
nguess \(10^3\) The PROTEUS warm-start almost always lands on the right basin in the first attempt; \(10^3\) Monte-Carlo restarts is plenty of margin without spending wall-time on degenerate cases.
nsolve \(3 \times 10^3\) Per-restart fsolve iteration cap.
opt_solver False Skip the alternating-solver fallback; if fsolve cannot converge from a warm start, it almost always means an upstream bug rather than a basin-of-attraction problem.
print_result False Suppress the per-call INFO log; the PROTEUS main loop already prints the partial pressures.

The TOML field names map to equilibrium_atmosphere keyword arguments as follows:

TOML field Solver argument Role
solver_rtol rtol Relative tolerance on the elemental mass-balance residual.
solver_atol xtol Step tolerance on the inner fsolve Powell-hybrid iteration. The name is historical; it does not set the absolute mass-balance tolerance.
mass_thresh atol Absolute tolerance on mass balance, in kg. Also the per-element threshold below which a species' inventory is treated as zero.

Warm-start behaviour

For Time > 1 yr, the wrapper builds p_guess from the previous-iteration H2O_bar, CO2_bar, N2_bar, S2_bar rows. For Time = 0 (first call) the wrapper passes p_guess = None and lets CALLIOPE's Monte-Carlo initial-guess generator find the basin. If an element's target inventory drops below mass_thresh, the corresponding p_guess is forced to zero so that the solver does not waste iterations chasing a vanishing species.

Common pitfalls

  • Missing required volatile at startup: one of H2O / CO2 / N2 / S2 is set to include = false. Fix: re-enable it (it can still be set to a tiny initial pressure if you want to suppress its mass).
  • Could not find solution for volatile abundances mid-run: CALLIOPE exhausted its nguess Monte-Carlo restarts. Almost always caused by an upstream NaN or unphysical state in hf_row (e.g. T_magma < T_floor after AGNI failed, M_mantle = 0 after a structure-solve failure). Inspect proteus_00.log for the iteration immediately before the CALLIOPE failure; do not bump nguess blindly.
  • Atmosphere mass collapses to zero without escape accounting it: the check_desiccation gate in proteus.outgas.wrapper will refuse to mark the planet desiccated if the loss is unexplained by cumulative escape. This is by design: a real desiccation event must be supported by tracked atmospheric escape, so the gate firing means an upstream module silently zeroed the atmosphere and the failure is upstream of CALLIOPE.
  • Hydrogen inventory must be > 0: H_budget = 0 or H_mode mistyped. CALLIOPE refuses to solve a zero-H system because the speciation is degenerate.
  • Solubility off but Phi_global > 0: not an error, but the wrapper will silently force Phi_global = 0 when [outgas.calliope].solubility = false, so dissolved masses will all be zero regardless of the live melt fraction.

Next step

For what the wrapper actually does on each iteration (sequence diagram, mapping table, hf_row keys), read Coupling to PROTEUS (theory).


  1. C. Cartier, B. J. Wood, The role of reducing conditions in building Mercury, Elements, 15(1), 39–45, 2019. SciX. ↩↩

  2. D. J. Frost, C. A. McCammon, The redox state of Earth's mantle, Annual Review of Earth and Planetary Sciences, 36, 389–420, 2008. SciX. ↩↩

  3. H. Nicholls, T. Lichtenberg, D. J. Bower, R. Pierrehumbert, Magma ocean evolution at arbitrary redox state, Journal of Geophysical Research: Planets, 129, e2024JE008576, 2024. SciX. ↩

  4. H. St. C. O'Neill, S. M. Eggins, The effect of melt composition on trace element partitioning: an experimental investigation of the activity coefficients of FeO, NiO, CoO, MoO\(_2\) and MoO\(_3\) in silicate melts, Chemical Geology, 186, 151–181, 2002. SciX. ↩

  5. P. A. Sossi, A. D. Burnham, J. Badro, A. Lanzirotti, M. Newville, H. St. C. O'Neill, Redox state of Earth's magma ocean and its Venus-like early atmosphere, Science Advances, 6, eabd1387, 2020. SciX. ↩↩

  6. M. Wadhwa, Redox state of Mars' upper mantle and crust from Eu anomalies in shergottite pyroxenes, Science, 291, 1527–1530, 2001. SciX. ↩