From 1a2f50852a3daedbe2dd82c936046dcede5fd7a4 Mon Sep 17 00:00:00 2001 From: timlichtenberg Date: Mon, 26 Oct 2026 01:27:21 +0100 Subject: [PATCH 01/15] Explain the SPIDER Validation tab for new students Each Validation page opens with what it checks, and each case says why the check exists, what it tests, how the right answer is known and why it matters, with the reference, tolerance, test id and command in a collapsed box. The pages name the outputs PROTEUS reads from SPIDER and state that PROTEUS does not use the volatile and reaction parts, and Known limit lines name shared constants and untested paths. --- docs/Validation/atmosphere.md | 32 ++++++++---- docs/Validation/bc.md | 51 +++++++++++++------ docs/Validation/energy.md | 70 ++++++++++++++++++-------- docs/Validation/eos.md | 57 +++++++++++++++------ docs/Validation/eos_adamswilliamson.md | 32 ++++++++---- docs/Validation/eos_composite.md | 32 ++++++++---- docs/Validation/eos_lookup.md | 37 ++++++++++---- docs/Validation/ic.md | 70 ++++++++++++++++++-------- docs/Validation/index.md | 9 +++- docs/Validation/interp.md | 32 ++++++++---- docs/Validation/matprop.md | 32 ++++++++---- docs/Validation/mesh.md | 32 ++++++++---- docs/Validation/reaction.md | 36 +++++++++---- docs/Validation/rheologicalfront.md | 32 ++++++++---- docs/Validation/rhs.md | 34 +++++++++---- docs/Validation/twophase.md | 32 ++++++++---- 16 files changed, 447 insertions(+), 173 deletions(-) diff --git a/docs/Validation/atmosphere.md b/docs/Validation/atmosphere.md index 5202f947..dadfbfbe 100644 --- a/docs/Validation/atmosphere.md +++ b/docs/Validation/atmosphere.md @@ -1,19 +1,33 @@ # atmosphere.c: Surface energy balance -**Source under test**: `atmosphere.c` +## What this page checks -## Cases +This page checks the surface radiative energy balance in `atmosphere.c`, which models heat exchange. +The atmosphere module calculates the thermal flux, which is exchanged between the planetary surface and space. +Validation means that we test the grey-body radiation routine against the analytical Stefan-Boltzmann law, which provides an exact reference. +If the surface radiation is wrong, the cooling rate of the magma ocean is incorrect. +The section below details the validation check, its tolerance, and its test command. -### Grey-body flux matches Stefan-Boltzmann limit +## Grey-body flux matches Stefan-Boltzmann limit -**Reference-pinned test**: `tests/test_atmosphere.py::test_grey_body_flux_matches_stefan_boltzmann_limit` +**Why this check exists.** Numerical implementations of radiative flux can suffer from incorrect temperature exponents, or from inverted signs. A bug in the surface flux calculation leads to unphysical planetary cooling rates, without raising a runtime error. -**Reference**: Analytical limit: the grey-body flux F = emissivity sigma (T_surf^4 - T_eqm^4) with emissivity 1 and T_eqm = 273 K (Stefan-Boltzmann). +**What it tests.** The test evaluates the grey-body surface flux F = emissivity sigma (T_surf^4 - T_eqm^4), with emissivity 1, and equilibrium temperature T_eqm = 273 K. It compares the reported surface flux against the Stefan-Boltzmann law, evaluated at the reported surface temperature. The test guards against exponent errors, where a T^3 exponent slip changes the flux by orders of magnitude at magma-ocean temperatures. The flux must remain positive (outward), and bounded to the 1e2 to 1e9 W/m2 regime. -**Tolerance**: rel 1e-6 against the reported surface temperature, using the code constant sigma = 5.670367e-8 W m-2 K-4. +**How we know the right answer.** The grey-body Stefan-Boltzmann law provides an exact analytical reference for thermal radiation, which governs ideal emitters. An exponent slip to third power changes the flux by orders of magnitude, which easily violates the tolerance of rel 1e-6. -**Command**: `make`, then `pytest -o addopts= tests/test_atmosphere.py::test_grey_body_flux_matches_stefan_boltzmann_limit` +**Why it matters.** The atmosphere module computes the surface energy balance, which drives planetary mantle cooling. PROTEUS reads the surface flux `Fatm` as `F_int`, and surface temperature `temperature_surface` as `T_magma`, to couple the interior to the atmosphere. -**Discrimination guards**: A T^3 exponent slip changes the flux by orders of magnitude at magma-ocean temperatures; the flux is positive (outward) and bounded to the 1e2 to 1e9 W/m2 regime; monotone decline of flux and surface temperature over the first outputs. +**Known limit.** The test evaluates the analytical formula using the code constant sigma = 5.670367e-8 W m-2 K-4. An error in `constants.c` would affect both values, and is not detected by this comparison. -The atmosphere module computes the surface energy balance that drives the cooling. The grey-body branch is pinned against the Stefan-Boltzmann law evaluated from the same output state, and the secular cooling trend is asserted across output times. +??? info "Test details" + - Reference: Analytical limit: the grey-body flux F = emissivity sigma (T_surf^4 - T_eqm^4) with emissivity 1 and T_eqm = 273 K (Stefan-Boltzmann). + - Tolerance: rel 1e-6 against the reported surface temperature, using the code constant sigma = 5.670367e-8 W m-2 K-4. + - Test id: `tests/test_atmosphere.py::test_grey_body_flux_matches_stefan_boltzmann_limit` + - Command: `make`, then `pytest -o addopts= tests/test_atmosphere.py::test_grey_body_flux_matches_stefan_boltzmann_limit` + - Companion test: `tests/test_atmosphere.py::test_surface_cools_and_flux_decays_across_macro_steps` confirms monotone decline of flux and surface temperature over outputs. + +## Related code + +- `atmosphere.c`: `set_atmosphere_emissivity_and_flux`, `set_surface_temperature_from_flux`. +- `tests/test_atmosphere.py`: `test_grey_body_flux_matches_stefan_boltzmann_limit`, `test_surface_cools_and_flux_decays_across_macro_steps`. diff --git a/docs/Validation/bc.md b/docs/Validation/bc.md index ca35e588..d14733b7 100644 --- a/docs/Validation/bc.md +++ b/docs/Validation/bc.md @@ -1,31 +1,52 @@ # bc.c: Boundary conditions -**Source under test**: `bc.c` +## What this page checks -## Cases +This page checks the boundary condition implementations in `bc.c`, which control surface and bottom boundaries. +The boundary condition routines specify how energy and entropy enter, or leave, the planetary mantle. +Validation means that we test boundary condition options against analytical identities, and against prescribed inputs. +If boundary condition dispatch fails, energy conservation is violated at the surface, or at the core-mantle boundary. +The sections below detail two validation cases, their tolerances, and their test commands. -### Prescribed-flux BC echoes value and inverts emissivity +## Prescribed-flux BC echoes value and inverts emissivity -**Reference-pinned test**: `tests/test_bc.py::test_prescribed_flux_bc_echoes_value_and_inverts_emissivity` +**Why this check exists.** Surface boundary routines can misinterpret options, swap boundary condition flags, or miscalculate equivalent emissivities. A bug in prescribed boundary dispatch causes the interior to use an incorrect surface heat flux, without failing explicitly. -**Reference**: Analytical identity: with SURFACE_BC 4 the surface flux equals the prescribed value, and the reported emissivity is its grey-body inversion. +**What it tests.** The test evaluates boundary condition SURFACE_BC 4, where the surface flux equals the prescribed value. The reported emissivity must equal the grey-body inversion value. The test guards against wrong dispatch, confirming that the grey-body flux at the surface temperature differs from the prescribed value by more than 10 percent, and the emissivity differs from 1 by more than 0.5. The flux must be positive, and lie between 1e2 and 1e9 W/m^2. Sibling test `test_mantle_cools_from_the_top_between_surface_and_cmb` asserts temperature ordering across the mantle, and near-zero surface pressure. -**Tolerance**: rel 1e-9 on the flux echo; rel 1e-6 on the emissivity inversion. +**How we know the right answer.** Under SURFACE_BC 4, reproducing the prescribed flux is an analytical identity. The test checks the flux echo to rel 1e-9, and the inverted emissivity to rel 1e-6. -**Command**: `make`, then `pytest -o addopts= tests/test_bc.py::test_prescribed_flux_bc_echoes_value_and_inverts_emissivity` +**Why it matters.** PROTEUS passes the atmospheric radiative flux into SPIDER using option `-SURFACE_BC 4` and `-surface_bc_value`, so SPIDER must echo this flux exactly. -**Discrimination guards**: The grey-body flux at the same surface temperature differs from the prescribed value by more than 10 percent (dispatch guard), and the emissivity differs from 1 by more than 0.5; the flux is positive and between 1e2 and 1e9 W/m^2. The sibling test `test_mantle_cools_from_the_top_between_surface_and_cmb` (same file) asserts the temperature ordering across the mantle (CMB hotter than surface) and the near-zero surface pressure on the default configuration. +**Known limit.** This test validates prescribed flux injection, but does not test coupled atmospheric radiative transfer within SPIDER itself. -The boundary-condition dispatch is pinned through the prescribed-flux pathway PROTEUS uses in coupled runs: the value handed in through the option must reappear identically in the output, with the emissivity consistently inverted from it. +??? info "Test details" + - Reference: Analytical identity: with SURFACE_BC 4 the surface flux equals the prescribed value, and the reported emissivity is its grey-body inversion. + - Tolerance: rel 1e-9 on the flux echo; rel 1e-6 on the emissivity inversion. + - Test id: `tests/test_bc.py::test_prescribed_flux_bc_echoes_value_and_inverts_emissivity` + - Command: `make`, then `pytest -o addopts= tests/test_bc.py::test_prescribed_flux_bc_echoes_value_and_inverts_emissivity` + - Companion test: `tests/test_bc.py::test_mantle_cools_from_the_top_between_surface_and_cmb` verifies that the core-mantle boundary remains hotter than the surface. -### Constant-entropy BC pins the initial surface +## Constant-entropy BC pins the initial surface -**Reference-pinned test**: `tests/test_bc.py::test_constant_entropy_bc_pins_the_initial_surface` +**Why this check exists.** Fixed boundary values can be overwritten by initial rebalancing steps, or by stencil operations. If the surface boundary assignment fails, the mantle profile starts from an uncalibrated thermodynamic state. -**Reference**: Analytical identity: the constant-entropy boundary writes ic_surface_entropy (2550 J/kg/K) onto the surface basic node, preserved through the steady-state energy solve into the initial output. +**What it tests.** The test evaluates the constant-entropy boundary option, which writes `ic_surface_entropy` (2550 J/kg/K) onto the surface basic node. This value is preserved through the steady-state energy solve into the initial output. The test discriminates this setting against the default configuration surface value (2599.7 J/kg/K from the 2600 adiabat), which sits about 50 J/kg/K away. Sibling test `test_prescribed_core_flux_slows_cmb_cooling` checks CORE_BC dispatch, CMB entropy difference, and doubled CMB heat flux. -**Tolerance**: rel 1e-10 (assigned, not solved). +**How we know the right answer.** Preserving a specified boundary value is an exact identity. Because the value is assigned directly, it matches to rel 1e-10. -**Command**: `make`, then `pytest -o addopts= tests/test_bc.py::test_constant_entropy_bc_pins_the_initial_surface` +**Why it matters.** Setting fixed surface or core entropies allows controlled numerical experiments and comparisons with simplified interior profiles. -**Discrimination guards**: The default configuration's surface value (2599.7 J/kg/K from the 2600 adiabat) sits about 50 J/kg/K away; positivity and table-range bounds on the core-side value after the steady rebalance. The sibling test `test_prescribed_core_flux_slows_cmb_cooling` (same file) compares against the core-cooling baseline and discriminates the CORE_BC dispatch by the sign of the CMB entropy difference and the doubled CMB heat flux. +**Known limit.** For constant-entropy boundary conditions, the surface value is assigned directly rather than determined by a physical flux balance. + +??? info "Test details" + - Reference: Analytical identity: the constant-entropy boundary writes ic_surface_entropy (2550 J/kg/K) onto the surface basic node, preserved through the steady-state energy solve into the initial output. + - Tolerance: rel 1e-10 (assigned, not solved). + - Test id: `tests/test_bc.py::test_constant_entropy_bc_pins_the_initial_surface` + - Command: `make`, then `pytest -o addopts= tests/test_bc.py::test_constant_entropy_bc_pins_the_initial_surface` + - Companion test: `tests/test_bc.py::test_prescribed_core_flux_slows_cmb_cooling` verifies that core heat flux slows cooling at the core-mantle boundary. + +## Related code + +- `bc.c`: `set_surface_flux_from_atmosphere`, `set_surface_entropy_constant`, `set_cmb_entropy_constant`. +- `tests/test_bc.py`: `test_prescribed_flux_bc_echoes_value_and_inverts_emissivity`, `test_constant_entropy_bc_pins_the_initial_surface`. diff --git a/docs/Validation/energy.md b/docs/Validation/energy.md index f13da5a6..4dacde2c 100644 --- a/docs/Validation/energy.md +++ b/docs/Validation/energy.md @@ -1,43 +1,71 @@ # energy.c: Energy fluxes -**Source under test**: `energy.c` +## What this page checks -## Cases +This page checks energy flux assembly and internal heating routines in `energy.c`, which control mantle energy transport. +The energy module calculates convective, conductive, and diffusive fluxes, alongside radiogenic and tidal heating. +Validation means that we test flux assembly against analytical superposition limits, decay laws, and steady balances. +If flux calculation or source terms are wrong, mantle cooling rates and core heat extraction are wrong. +The sections below detail three validation cases, their tolerances, and their test commands. -### Disabled transport mechanisms carry zero flux +## Disabled transport mechanisms carry zero flux -**Reference-pinned test**: `tests/test_energy.py::test_disabled_transport_mechanisms_carry_zero_flux` +**Why this check exists.** Runtime configuration flags can fail to disconnect disabled flux routines, or leak residual values into total energy balances. A failure in flux superposition produces incorrect thermal gradients without raising an error. -**Reference**: Analytical limit: a disabled transport mechanism carries zero flux, and with only conduction enabled the total flux equals the conductive flux at every node except the surface node, which carries the grey-body boundary flux. +**What it tests.** The test evaluates the flux superposition architecture when specific transport terms are switched off. A disabled transport mechanism must hold zero flux, within abs 1e-20 W/m^2, which is at least 17 decades below active fluxes. With conduction off, convection yields 9e8 to 2e9 W/m^2. In a conduction-only run, conduction yields 1.9e-3 to 1.3e-2 W/m^2. Total flux equals conductive flux to rel 1e-12 at every interior node. The surface node differs by more than 1e3 W/m^2. -**Tolerance**: abs 1e-20 W/m^2 on the disabled fluxes, at least 17 decades below the active fluxes (convection 9e8 to 2e9 W/m^2 with conduction off; conduction 1.9e-3 to 1.3e-2 W/m^2 in the conduction-only run); rel 1e-12 on the conduction-only identity, and the surface node differs by more than 1e3 W/m^2. +**How we know the right answer.** Superposition of linear or disjoint flux mechanisms is an analytical identity. Inactive transport terms must vanish to rounding precision, while active terms reproduce isolated components. -**Command**: `make`, then `pytest -o addopts= tests/test_energy.py::test_disabled_transport_mechanisms_carry_zero_flux` +**Why it matters.** SPIDER models mantle cooling by summing the transport terms, so a leaking term changes every flux of the run. -**Discrimination guards**: Convection stays active when conduction is off (wrong-path guard); at the surface node the grey-body boundary flux differs from the conductive flux, so the identity holds at the interior nodes only; a scale bound on the full-physics flux. The sibling test in the same file asserts that the total flux is the sum of the components at the interior nodes. +**Known limit.** For the conduction-only test, the identity holds at interior nodes only, because the surface node transmits the grey-body boundary flux. -The flux assembly is validated through its superposition structure: each transport term can be switched off through its runtime option, and the total must reproduce the remaining terms to output precision. This pins the dispatch wiring between the options, the individual flux routines, and the total used by the time integration. +??? info "Test details" + - Reference: Analytical limit: a disabled transport mechanism carries zero flux, and with only conduction enabled the total flux equals the conductive flux at every node except the surface node, which carries the grey-body boundary flux. + - Tolerance: abs 1e-20 W/m^2 on the disabled fluxes, at least 17 decades below the active fluxes (convection 9e8 to 2e9 W/m^2 with conduction off; conduction 1.9e-3 to 1.3e-2 W/m^2 in the conduction-only run); rel 1e-12 on the conduction-only identity, and the surface node differs by more than 1e3 W/m^2. + - Test id: `tests/test_energy.py::test_disabled_transport_mechanisms_carry_zero_flux` + - Command: `make`, then `pytest -o addopts= tests/test_energy.py::test_disabled_transport_mechanisms_carry_zero_flux` + - Companion test: `tests/test_energy.py::test_total_flux_assembles_from_transport_components` verifies that total flux equals the sum of transport components at interior basic nodes. -### Radiogenic heating pins two isotope decay +## Radiogenic heating pins two isotope decay -**Reference-pinned test**: `tests/test_energy.py::test_radiogenic_heating_pins_two_isotope_decay` +**Why this check exists.** Radioactive decay models can apply incorrect decay constants, confuse isotope abundances, or miscalculate exponential time evolution. A bug in heating terms leads to wrong internal heat production over geological timescales. -**Reference**: Analytical limit: radiogenic heating is the sum over isotopes of concentration times abundance times heat production, decaying as 2^(-(t - t0)/half_life). A two-isotope configuration with 100-year and 200-year half-lives pins the sum at t = 0 and the weighted decay factor 0.2506 after 200 years. +**What it tests.** The test evaluates radiogenic heating as the sum of concentration times abundance times heat production over isotopes, decaying as 2^(-(t - t0)/half_life). A two-isotope test configuration with 100-year and 200-year half-lives pins the sum at t = 0, and checks the weighted decay factor 0.2506 after 200 years. A dropped isotope shifts the initial sum by 2.4e-3 relative. The two-isotope factor is discriminated from single-isotope values 0.25 and 0.5, and from the no-decay value 1.0. -**Tolerance**: rel 1e-6 on the initial sum, rel 1e-4 on the decay factor. +**How we know the right answer.** Radioactive decay follows the analytical exponential decay law. The initial sum is checked to rel 1e-6, and the combined decay factor matches to rel 1e-4. -**Command**: `make`, then `pytest -o addopts= tests/test_energy.py::test_radiogenic_heating_pins_two_isotope_decay` +**Why it matters.** Radiogenic decay supplies essential internal heating that delays mantle freezing. PROTEUS reads integrated radiogenic heating `F_radio` (in `ReadSPIDER` of `spider.py`) into whole-planet energy budgets. -**Discrimination guards**: A dropped isotope shifts the sum by 2.4e-3 relative; the two-isotope decay factor is resolved from the single-isotope values 0.25 and 0.5 and from the no-decay value 1.0. +**Known limit.** This test pins decay mechanics for synthetic isotopes, but does not benchmark geological isotope concentrations. -### Steady-state IC balances interior and surface +??? info "Test details" + - Reference: Analytical limit: radiogenic heating is the sum over isotopes of concentration times abundance times heat production, decaying as 2^(-(t - t0)/half_life). A two-isotope configuration with 100-year and 200-year half-lives pins the sum at t = 0 and the weighted decay factor 0.2506 after 200 years. + - Tolerance: rel 1e-6 on the initial sum, rel 1e-4 on the decay factor. + - Test id: `tests/test_energy.py::test_radiogenic_heating_pins_two_isotope_decay` + - Command: `make`, then `pytest -o addopts= tests/test_energy.py::test_radiogenic_heating_pins_two_isotope_decay` + - Companion test: `tests/test_energy.py::test_prescribed_tidal_heating_is_uniform_and_steady` verifies uniform spatial distribution of tidal heating. -**Reference-pinned test**: `tests/test_energy.py::test_steady_state_ic_balances_interior_and_surface` +## Steady-state IC balances interior and surface -**Reference**: Analytical limit: the steady-state initial condition equalises the energy flow through every basic node and closes the surface radiation balance sigma (T_surf^4 - teqm^4) with the Stefan-Boltzmann constant of constants.c. +**Why this check exists.** Steady-state solvers can terminate at false equilibria, or fail to balance heat flow across layers. An uncalibrated initial profile introduces unphysical thermal transients at the start of a simulation. -**Tolerance**: Relative energy-flow spread below 1e-8 (observed 2e-11); grey-body pin at rel 1e-5. +**What it tests.** The test evaluates the steady-state initial condition, which equalises energy flow through every basic node, and closes the surface radiation balance sigma (T_surf^4 - teqm^4) with the Stefan-Boltzmann constant of constants.c. A T^3 exponent slip misses the surface flux by roughly a factor of the surface temperature (about 2000 here). -**Command**: `make`, then `pytest -o addopts= tests/test_energy.py::test_steady_state_ic_balances_interior_and_surface` +**How we know the right answer.** Conservation of energy in steady state requires equal energy flow through every mantle shell, and balanced radiation at the surface. Energy-flow relative spread remains below 1e-8 (observed 2e-11), and the grey-body balance matches to rel 1e-5. -**Discrimination guards**: A T^3 exponent slip misses the flux by roughly a factor of the surface temperature (about 2000 here); sign and scale bounds on the flux. +**Why it matters.** The steady-state initial condition starts a run from a profile in energy balance. + +**Known limit.** The test uses the Stefan-Boltzmann constant from `constants.c`, so an error in the constant definition is not isolated by this check. + +??? info "Test details" + - Reference: Analytical limit: the steady-state initial condition equalises the energy flow through every basic node and closes the surface radiation balance sigma (T_surf^4 - teqm^4) with the Stefan-Boltzmann constant of constants.c. + - Tolerance: Relative energy-flow spread below 1e-8 (observed 2e-11); grey-body pin at rel 1e-5. + - Test id: `tests/test_energy.py::test_steady_state_ic_balances_interior_and_surface` + - Command: `make`, then `pytest -o addopts= tests/test_energy.py::test_steady_state_ic_balances_interior_and_surface` + - Companion test: `tests/test_energy.py::test_surface_node_flux_is_the_grey_body_atmospheric_flux` verifies that surface node flux equals the atmospheric grey-body value. + +## Related code + +- `energy.c`: `set_Etot`, `append_Jcond`, `append_Jconv`, `append_Hradio`, `solve_for_steady_state_energy_interior`. +- `tests/test_energy.py`: `test_disabled_transport_mechanisms_carry_zero_flux`, `test_radiogenic_heating_pins_two_isotope_decay`, `test_steady_state_ic_balances_interior_and_surface`. diff --git a/docs/Validation/eos.md b/docs/Validation/eos.md index 0e75067e..dd1cebde 100644 --- a/docs/Validation/eos.md +++ b/docs/Validation/eos.md @@ -1,31 +1,58 @@ # eos.c: EOS interface: setup, viscosity, phase boundaries -**Source under test**: `eos.c` +## What this page checks -## Cases +This page checks the equation of state interface in `eos.c`, which coordinates material property evaluations. +`eos.c` manages common option parsing for equations of state, loads phase boundaries, and computes composition-dependent viscosity. +Validation means that we compare boundary interpolations and viscosity shifts against independent implementations and published formulas. +If phase boundaries or viscosity calculations are wrong, the melt fraction and convection velocity in the mantle are incorrect. +The sections below detail two validation cases, their tolerances, and their test commands. -### Phase boundary matches independent table read +## Phase boundary matches independent table read -**Reference-pinned test**: `tests/test_eos.py::test_phase_boundary_matches_independent_table_read` +**Why this check exists.** Table reading routines can swap solidus and liquidus arrays, misplace table indices, or apply incorrect unit conversions. A wrong phase boundary misidentifies the melting state of the mantle without an error message. -**Reference**: Cross-implementation cross-check of the loaded melt phase boundary against an independent read of liquidus_A11_H13.dat (Andrault et al. 2011 liquidus with the Hirschmann 2013 solidus companion). +**What it tests.** The test evaluates the loaded melt phase boundary against an independent read of `liquidus_A11_H13.dat` (Andrault et al. 2011 liquidus with the Hirschmann 2013 solidus companion). It checks interpolation at the P = 0 table edge, a mid-mantle node, and an off-node deep point. The solidus value at 10 GPa (about 1524 J/kg/K) sits far from the pinned liquidus value (about 2128 J/kg/K), guarding against swapped boundaries. Sibling test `test_viscosity_and_conductivity_dispatch_per_phase` pins per-phase transport dispatch with a 19-decade melt to solid contrast. -**Tolerance**: rel 1e-12 at the P = 0 table edge, a mid-mantle node, and an off-node deep point. +**How we know the right answer.** The independent reader evaluates the raw table file using Python numpy interpolation. The C interpolation reproduces these reference values to rel 1e-12. -**Command**: `make tests_c`, then `pytest -o addopts= tests/test_eos.py::test_phase_boundary_matches_independent_table_read` +**Why it matters.** The phase boundary determines where melting occurs throughout the mantle. SPIDER evaluates this boundary to compute local melt fraction and global melt volume. -**Discrimination guards**: The solidus value at 10 GPa (about 1524 J/kg/K) sits far from the pinned liquidus value (about 2128 J/kg/K), so a swapped-boundary regression fails; sign and scale guards bound the entropies; a companion test pins the per-phase viscosity and conductivity dispatch with a 19-decade melt/solid contrast. +**Known limit.** The independent verification reads the same source table file, checking interpolation rather than physical accuracy of the underlying experimental dataset. -eos.c owns the option parsing shared by all EOS implementations and loads the phase boundaries. The boundary interpolation is compared against numpy.interp on the same file, and the per-phase transport constants configured in the options file are recovered exactly from the evaluation. +??? info "Test details" + - Reference: Cross-implementation cross-check of the loaded melt phase boundary against an independent read of liquidus_A11_H13.dat (Andrault et al. 2011 liquidus with the Hirschmann 2013 solidus companion). + - Tolerance: rel 1e-12 at the P = 0 table edge, a mid-mantle node, and an off-node deep point. + - Test id: `tests/test_eos.py::test_phase_boundary_matches_independent_table_read` + - Command: `make tests_c`, then `pytest -o addopts= tests/test_eos.py::test_phase_boundary_matches_independent_table_read` + - Companion test: `tests/test_eos.py::test_viscosity_and_conductivity_dispatch_per_phase` verifies that melt and solid phases receive distinct transport properties. -### Compositional viscosity matches Spaargaren et al. (2020) +## Compositional viscosity matches Spaargaren et al. (2020) -**Reference-pinned test**: `tests/test_eos.py::test_compositional_viscosity_matches_spaargaren` +**Why this check exists.** Piecewise empirical formulas can suffer from transcription typos in branch coefficients, incorrect branch selection, or missing signs. An incorrect viscosity prefactor alters convective vigour by orders of magnitude. -**Reference**: The piecewise Mg/Si log10 viscosity prefactor of Spaargaren et al. (2020). The test re-encodes the published coefficients independently of eos.c and additionally pins the two branch-pair shifts as hand-derived literals (0.2074 and -1.35803), so a transcription error in either copy of the formula surfaces. +**What it tests.** The test evaluates the piecewise Mg/Si log10 viscosity prefactor of Spaargaren et al. (2020). It re-encodes the published coefficients independently, and pins two branch-pair shifts as hand-derived literals (0.2074 and -1.35803). The test exercises two configurations that shift in opposite directions across more than 1.5 decades, covering all four piecewise branches. Sibling test `test_activation_terms_shape_the_viscosity_profile` pins the zero-pressure limit and pressure-scale damping direction. -**Tolerance**: abs 1e-9 on the evaluated shifts (closed-form option arithmetic); abs 1e-4 on the literal transcription pins. +**How we know the right answer.** Spaargaren et al. (2020) define the formula analytically. The shifts match option arithmetic to abs 1e-9, and the hand-derived literal transcription pins match to abs 1e-4. -**Command**: `make tests_c`, then `pytest -o addopts= tests/test_eos.py::test_compositional_viscosity_matches_spaargaren` +**Why it matters.** Compositional controls on mantle viscosity determine how mantle convective vigor responds to planetary bulk composition. -**Discrimination guards**: The two configurations shift in opposite directions across more than 1.5 decades, covering all four piecewise branches; a collapsed piecewise or a disabled compositional term fails, and the bare-constant edge case pins the term switched off. The activation-term test (same file) pins the zero-pressure limit exactly and the pressure-scale damping direction. +**Known limit.** The benchmark tests numerical implementation of the parameterisation, but does not test mineralogical uncertainties in mantle rheology. + +??? info "Test details" + - Reference: The piecewise Mg/Si log10 viscosity prefactor of Spaargaren et al. (2020). The test re-encodes the published coefficients independently of eos.c and additionally pins the two branch-pair shifts as hand-derived literals (0.2074 and -1.35803), so a transcription error in either copy of the formula surfaces. + - Tolerance: abs 1e-9 on the evaluated shifts (closed-form option arithmetic); abs 1e-4 on the literal transcription pins. + - Test id: `tests/test_eos.py::test_compositional_viscosity_matches_spaargaren` + - Command: `make tests_c`, then `pytest -o addopts= tests/test_eos.py::test_compositional_viscosity_matches_spaargaren` + - Companion test: `tests/test_eos.py::test_activation_terms_shape_the_viscosity_profile` checks activation volume and energy scalings in the viscosity model. + +## Related code + +- `eos.c`: `EOSSetUpFromOptions`, `EOSGetPhaseBoundary`, `EOSEvalSetViscosity`. +- `tests/test_eos.py`: `test_phase_boundary_matches_independent_table_read`, `test_compositional_viscosity_matches_spaargaren`. + +## References + +- Andrault, D. et al. (2011). Solidus and liquidus for deep mantle minerals. +- Hirschmann, M. M. (2013). Iron and volatile partitioning during mantle melting. +- Spaargaren, R. J. et al. (2020). The influence of bulk composition on the rheology of rocky exoplanet mantles. diff --git a/docs/Validation/eos_adamswilliamson.md b/docs/Validation/eos_adamswilliamson.md index 5b59fbdf..4946c8f2 100644 --- a/docs/Validation/eos_adamswilliamson.md +++ b/docs/Validation/eos_adamswilliamson.md @@ -1,19 +1,33 @@ # eos_adamswilliamson.c: Adams-Williamson analytic density -**Source under test**: `eos_adamswilliamson.c` +## What this page checks -## Cases +This page checks the Adams-Williamson model in `eos_adamswilliamson.c`, which builds planetary structure. +The Adams-Williamson model describes how mantle density increases with depth. +Validation means that we check computed profiles against the exact analytical formula. +If the density profile is incorrect, mantle mass, pressure gradients, and shell volumes are wrong. +The text below details the check, its tolerance, and its test command. -### Density matches the closed-form profile +## Density matches the closed-form profile -**Reference-pinned test**: `tests/test_eos_adamswilliamson.py::test_density_matches_the_closed_form_profile` +**Why this check exists.** Analytical codes can suffer from sign errors in compressibility, or from wrong fit inputs. A bug in density calculations distorts the pressure field and mesh geometry without an error. -**Reference**: Analytical limit: rho(P) = rhos - P beta / g in closed form, with the blackbody50 PREM lower-mantle fit parameters. +**What it tests.** The test evaluates the formula rho(P) = rhos - P beta / g, using parameters fitted to the PREM lower mantle for blackbody50. It checks density values at the surface (P = 0), at 10 GPa, and at the core-mantle boundary pressure. A sign-flipped compressibility shifts the 10 GPa density by more than 200 kg/m3, which the test guards against. Sibling test `test_si_density_invariant_under_nondimensionalisation_choice` confirms that physical SI density outputs stay unchanged under the nondimensionalisation choice. -**Tolerance**: rel 1e-12 at the surface (P = 0), 10 GPa, and the approximate core-mantle boundary pressure. +**How we know the right answer.** The Adams-Williamson formula provides an exact closed-form density profile. Numerical values in C agree with the analytical profile to rel 1e-12, reducing at the surface to the exact configured surface density. -**Command**: `make tests_c`, then `pytest -o addopts= tests/test_eos_adamswilliamson.py::test_density_matches_the_closed_form_profile` +**Why it matters.** The Adams-Williamson EOS supplies the static structural profile used by SPIDER native mesh code (`parameters.c` and `mesh.c`). -**Discrimination guards**: A sign-flipped compressibility shifts the 10 GPa density by more than 200 kg/m3; monotonicity under compression and the silicate density range bound the profile; SI invariance under the nondimensionalisation choice is asserted separately. +**Known limit.** The Adams-Williamson model assumes an idealized profile, rather than a multi-phase EOS. -The Adams-Williamson EOS provides the static structural profile used by the native mesh. Its density is a closed-form expression, so the evaluation is pinned to rounding accuracy, with the surface limit reducing exactly to the configured surface density. +??? info "Test details" + - Reference: Analytical limit: rho(P) = rhos - P beta / g in closed form, with the blackbody50 PREM lower-mantle fit parameters. + - Tolerance: rel 1e-12 at the surface (P = 0), 10 GPa, and the approximate core-mantle boundary pressure. + - Test id: `tests/test_eos_adamswilliamson.py::test_density_matches_the_closed_form_profile` + - Command: `make tests_c`, then `pytest -o addopts= tests/test_eos_adamswilliamson.py::test_density_matches_the_closed_form_profile` + - Companion test: `tests/test_eos_adamswilliamson.py::test_si_density_invariant_under_nondimensionalisation_choice` asserts SI invariance under the nondimensionalisation choice. + +## Related code + +- `eos_adamswilliamson.c`: `EOSCreate_AdamsWilliamson`, `EOSEval_AdamsWilliamson`. +- `tests/test_eos_adamswilliamson.py`: `test_density_matches_the_closed_form_profile`, `test_si_density_invariant_under_nondimensionalisation_choice`. diff --git a/docs/Validation/eos_composite.md b/docs/Validation/eos_composite.md index ddf91b9d..f4f17570 100644 --- a/docs/Validation/eos_composite.md +++ b/docs/Validation/eos_composite.md @@ -1,19 +1,33 @@ # eos_composite.c: Two-phase composite blending -**Source under test**: `eos_composite.c` +## What this page checks -## Cases +This page checks the two-phase composite equation of state in `eos_composite.c`, which blends melt and solid properties. +The composite EOS bridges pure melt and solid lookups across the melting interval. +Validation means that we test the fusion entropy and melt fraction interpolation against exact analytical definitions. +If the composite blending is wrong, mantle phase fractions, density jumps, and latent heat release are wrong. +The section below details the validation check, its tolerance, and its test command. -### Fusion equals liquidus minus solidus and phi is linear in entropy +## Fusion equals liquidus minus solidus and phi is linear in entropy -**Reference-pinned test**: `tests/test_eos_composite.py::test_fusion_equals_liquidus_minus_solidus_and_phi_is_linear_in_entropy` +**Why this check exists.** Composite blending routines can use wrong denominators, miscalculate melting intervals, or flip boundary bounds. A bug in melt fraction interpolation produces wrong phase mixtures throughout the mantle. -**Reference**: Analytical limit: by construction of the two-phase region, fusion = S_liq - S_sol and phi = (S - S_sol)/fusion between the phase boundaries. +**What it tests.** The test evaluates the analytical limit in the two-phase region: fusion = S_liq - S_sol, and phi = (S - S_sol)/fusion between phase boundaries. It tests the melt fraction at one quarter of the melting interval at 10 GPa. A wrong-denominator regression (using liquidus entropy instead of fusion) shifts phi from 0.25 to about 0.07, which is far beyond tolerance. The fusion entropy lies between 100 and 1500 J/kg/K (603.6 J/kg/K at 10 GPa for the dK09 tables). Sibling tests pin the clamp of the melt fraction between 0 and 1 and the pure-phase limits against single-phase lookups. -**Tolerance**: rel 1e-10 on the fusion identity; rel 1e-6 on the melt fraction at one quarter of the melting interval at 10 GPa. +**How we know the right answer.** By mathematical construction of the linear entropy interpolation, the melt fraction and fusion entropy satisfy exact definitions. The fusion identity matches to rel 1e-10, and the fractional melt position matches to rel 1e-6. -**Command**: `make tests_c`, then `pytest -o addopts= tests/test_eos_composite.py::test_fusion_equals_liquidus_minus_solidus_and_phi_is_linear_in_entropy` +**Why it matters.** The composite EOS determines the local melt fraction and effective thermodynamic properties of partially molten mantle shells. -**Discrimination guards**: A wrong-denominator regression (liquidus entropy instead of fusion) shifts phi from 0.25 to about 0.07, far beyond tolerance; the fusion entropy lies between 100 and 1500 J/kg/K (603.6 J/kg/K at 10 GPa for the dK09 tables); companion tests pin the [0, 1] clamp and the pure-phase limits against the single-phase lookups. +**Known limit.** This test pins the linear entropy lever rule, but does not benchmark non-linear eutectic melting curves. -The composite EOS blends the melt and solid lookups across the melting interval. The melt fraction definition, the fusion entropy, and the recovery of the pure end-members away from the interval are each pinned; the truncation contract is made visible through the untruncated diagnostic value. +??? info "Test details" + - Reference: Analytical limit: by construction of the two-phase region, fusion = S_liq - S_sol and phi = (S - S_sol)/fusion between the phase boundaries. + - Tolerance: rel 1e-10 on the fusion identity; rel 1e-6 on the melt fraction at one quarter of the melting interval at 10 GPa. + - Test id: `tests/test_eos_composite.py::test_fusion_equals_liquidus_minus_solidus_and_phi_is_linear_in_entropy` + - Command: `make tests_c`, then `pytest -o addopts= tests/test_eos_composite.py::test_fusion_equals_liquidus_minus_solidus_and_phi_is_linear_in_entropy` + - Companion test: `tests/test_eos_composite.py::test_phase_fraction_bounded_and_truncation_contract_visible` asserts that the melt fraction is clamped between 0 and 1, and the visibility of untruncated values. + +## Related code + +- `eos_composite.c`: `EOSCreate_Composite`, `EOSEval_Composite`. +- `tests/test_eos_composite.py`: `test_fusion_equals_liquidus_minus_solidus_and_phi_is_linear_in_entropy`, `test_phase_fraction_bounded_and_truncation_contract_visible`, `test_pure_phase_limits_recover_the_single_phase_lookups`. diff --git a/docs/Validation/eos_lookup.md b/docs/Validation/eos_lookup.md index 2be8ff6e..26b55531 100644 --- a/docs/Validation/eos_lookup.md +++ b/docs/Validation/eos_lookup.md @@ -1,19 +1,38 @@ # eos_lookup.c: Tabulated P-S equation of state -**Source under test**: `eos_lookup.c` +## What this page checks -## Cases +This page checks the tabulated thermodynamic lookup in `eos_lookup.c`, which reads mineral tables. +`eos_lookup.c` bilinearly reads thermodynamic properties from pressure-entropy grids. +Validation means that we check C table reads against an independent Python code. +If table reads fail, melt density and temperature profiles across the mantle are incorrect. +The section below details the check, its tolerance, and its test command. -### Melt density matches independent bilinear evaluation +## Melt density matches independent bilinear evaluation -**Reference-pinned test**: `tests/test_eos_lookup.py::test_melt_density_matches_independent_bilinear_evaluation` +**Why this check exists.** Table grid reads can suffer from indexing offsets, wrong corner weights, or swapped column scales. A bug in table reading shifts density values without triggering a runtime error. -**Reference**: Cross-implementation cross-check: an independent numpy bilinear evaluation of the shipped melt density table (1TPa-dK09-elec-free set: de Koker and Stixrude 2009 properties as assembled for Bower et al. 2018). +**What it tests.** The test evaluates the shipped melt density table (1TPa-dK09-elec-free set: de Koker and Stixrude 2009 properties as assembled for Bower et al. 2018). It compares C outputs with an independent Python bilinear check at three off-node (P, S) probe points spanning 7.3 to 89 GPa. The test guards against reading the wrong table: solid-table density at the same probes differs by more than 1 percent. Positive values and the 2000 to 8000 kg/m3 silicate range bound the density scale. Sibling test `test_si_outputs_invariant_under_nondimensionalisation_choice` confirms consistent SI outputs under scaling choices. -**Tolerance**: rel 1e-9 at three off-node (P, S) probe points spanning 7.3 to 89 GPa. +**How we know the right answer.** The table is independently parsed with Python numpy routines, sharing no code with the C side. Matching values pin file parsing, column scaling, index search, and bilinear weighting to rel 1e-9. -**Command**: `make tests_c`, then `pytest -o addopts= tests/test_eos_lookup.py::test_melt_density_matches_independent_bilinear_evaluation` +**Why it matters.** Lookup tables supply thermodynamic properties to mantle transport and structure routines. -**Discrimination guards**: The solid-table density at the same probes differs by more than 1 percent (wrong-table guard); positivity and the 2000 to 8000 kg/m3 silicate range bound the scale; a separate test asserts SI outputs are invariant under the choice of nondimensionalisation. +**Known limit.** The test compares table reads against an independent Python read of the same table file, but does not benchmark the underlying ab initio mineral physics. -The lookup EOS is validated against a re-implementation of the same mathematical operation that shares no code with the C side: the table file is parsed and bilinearly interpolated with numpy primitives. Agreement pins file parsing, per-column scaling, index search, and weighting in one comparison. +??? info "Test details" + - Reference: Cross-implementation cross-check: an independent numpy bilinear evaluation of the shipped melt density table (1TPa-dK09-elec-free set: de Koker and Stixrude 2009 properties as assembled for Bower et al. 2018). + - Tolerance: rel 1e-9 at three off-node (P, S) probe points spanning 7.3 to 89 GPa. + - Test id: `tests/test_eos_lookup.py::test_melt_density_matches_independent_bilinear_evaluation` + - Command: `make tests_c`, then `pytest -o addopts= tests/test_eos_lookup.py::test_melt_density_matches_independent_bilinear_evaluation` + - Companion test: `tests/test_eos_lookup.py::test_thermodynamic_quantities_positive_and_rho_monotonic_in_pressure` asserts positive thermodynamic quantities and monotonic density with pressure. + +## Related code + +- `eos_lookup.c`: `EOSCreate_Lookup`, `EOSEval_Lookup`, `SetInterp2dValue`. +- `tests/test_eos_lookup.py`: `test_melt_density_matches_independent_bilinear_evaluation`, `test_thermodynamic_quantities_positive_and_rho_monotonic_in_pressure`. + +## References + +- de Koker, N. and Stixrude, L. (2009). Self-consistent thermodynamic properties of liquid MgSiO3. +- Bower, D. J. et al. (2018). Numerical methods for magma ocean evolution. diff --git a/docs/Validation/ic.md b/docs/Validation/ic.md index 163987d0..997189bc 100644 --- a/docs/Validation/ic.md +++ b/docs/Validation/ic.md @@ -1,43 +1,71 @@ # ic.c: Initial condition -**Source under test**: `ic.c` +## What this page checks -## Cases +This page checks initial condition setup in `ic.c`, which builds starting mantle and volatile states. +`ic.c` establishes the initial adiabat profile, sets volatile inventories from mass fractions, and converts ocean-mole specifications. +Validation means that we compare initial profiles and chemical inventories against analytical limits and mass-balance identities. +If initial conditions are set incorrectly, simulations start from wrong thermal profiles, or from incorrect volatile inventories. +The sections below detail three validation cases, their tolerances, and their test commands. -### Prescribed adiabat entropy sets the initial profile +## Prescribed adiabat entropy sets the initial profile -**Reference-pinned test**: `tests/test_ic.py::test_prescribed_adiabat_entropy_sets_the_initial_profile` +**Why this check exists.** Initial condition routines can apply gradient ramps incorrectly, misinterpret input entropy parameters, or fail to write values to staggered mesh nodes. A bug in initial condition setup shifts the starting temperature structure, and the later cooling is wrong too. -**Reference**: Analytical limit: the initial condition prescribes the adiabat entropy (2600 J/kg/K at the top staggered node) with the configured small gradient ramp. +**What it tests.** The test evaluates the prescribed adiabat entropy (2600 J/kg/K at the top staggered node) with the configured gradient ramp. The profile mean lies between the adiabat entropy and that value plus |ic_dsdr| times mantle depth. A run with ic_adiabat_entropy = 2400 shifts the mean by 200 J/kg/K to rel 1e-9, guarding against ignored option inputs. Entropy is positive, with its mean between 1e3 and 1e4 J/kg/K. Top-node entropy matches at t = 0 to rel 1e-3 (observed exact). Sibling test `test_initial_adiabat_is_nearly_flat_until_cooling_imprints_structure` checks that subsequent cooling imprints thermal structure. -**Tolerance**: rel 1e-3 at the top node (observed exact); the profile mean lies between the adiabat entropy and that value plus |ic_dsdr| times the mantle depth; the 2400 J/kg/K run shifts the mean by 200 J/kg/K to rel 1e-9. +**How we know the right answer.** Prescribing an adiabat provides an analytical limit, which defines an idealized reference. The top-node value, and the mean profile shift, match exact option arithmetic. -**Command**: `make`, then `pytest -o addopts= tests/test_ic.py::test_prescribed_adiabat_entropy_sets_the_initial_profile` +**Why it matters.** The initial adiabat sets the starting thermal state of the magma ocean. SPIDER integrates cooling forward from this baseline. -**Discrimination guards**: A run with ic_adiabat_entropy = 2400 shifts the profile by exactly 200 J/kg/K (wrong-value discrimination); the entropy is positive and its mean between 1e3 and 1e4 J/kg/K. The sibling test `test_initial_adiabat_is_nearly_flat_until_cooling_imprints_structure` (same file) checks that cooling at later outputs breaks the initial flatness, so the IC is not a fixed point of the output pipeline. +**Known limit.** The check tests initialization on the staggered grid, but does not benchmark external non-adiabatic initial profiles. -The initial condition writes the prescribed adiabat onto the mesh. The top-node entropy, the integrated gradient ramp, and the response to a different prescribed value are pinned from the t = 0 output. +??? info "Test details" + - Reference: Analytical limit: the initial condition prescribes the adiabat entropy (2600 J/kg/K at the top staggered node) with the configured small gradient ramp. + - Tolerance: rel 1e-3 at the top node (observed exact); the profile mean lies between the adiabat entropy and that value plus |ic_dsdr| times the mantle depth; the 2400 J/kg/K run shifts the mean by 200 J/kg/K to rel 1e-9. + - Test id: `tests/test_ic.py::test_prescribed_adiabat_entropy_sets_the_initial_profile` + - Command: `make`, then `pytest -o addopts= tests/test_ic.py::test_prescribed_adiabat_entropy_sets_the_initial_profile` + - Companion test: `tests/test_ic.py::test_initial_adiabat_is_nearly_flat_until_cooling_imprints_structure` verifies that cooling breaks initial profile uniformity. -### Abundance IC realises the requested inventory +## Abundance IC realises the requested inventory -**Reference-pinned test**: `tests/test_ic.py::test_abundance_ic_realises_the_requested_inventory` +**Why this check exists.** Volatile partition routines can lose element mass during initial solver iterations, or misapply ppm conversions. A bug in volatile setup gives wrong initial volatile budgets in the atmosphere and the interior. -**Reference**: Mass-balance identity through the initial partial-pressure solve: initial_kg equals the requested ppm of the mantle mass per volatile, and the realised reservoirs carry the requested hydrogen and carbon mole totals (conserved by the water and carbon dioxide reactions). +**What it tests.** The test evaluates mass-balance closure through the initial partial-pressure solve. The quantity initial_kg equals requested ppm of mantle mass per volatile. Realised reservoirs hold requested hydrogen and carbon mole totals. Volatile oxygen drifts by about 2e-3 relative because reactions exchange O with the melt fO2 buffer, and realised H2O reservoirs sit about 3.6e-3 relative off bare requests. Discrimination thresholds sit at 1e-4, far below observed signals, while inert networks match to 1e-9. -**Tolerance**: rel 1e-9 on the stored inventories and the H and C totals (observed below 1e-15). +**How we know the right answer.** Closed-system mass conservation requires total moles of conserved elements to match requested inputs. Stored inventories and H and C totals match to rel 1e-9 (observed below 1e-15). -**Command**: `make`, then `pytest -o addopts= tests/test_ic.py::test_abundance_ic_realises_the_requested_inventory` +**Why it matters.** SPIDER's own volatile partitioning starts from this allocation. PROTEUS leaves the volatile part of SPIDER unused and computes outgassing with its own modules. -**Discrimination guards**: The volatile oxygen total drifts by about 2e-3 relative against the request because the reactions exchange O with the melt's fO2 buffer, and the realised H2O reservoirs sit about 3.6e-3 relative off the bare request; the test thresholds sit at 1e-4, at least twenty-fold below the observed signals, while an inert reaction network would match the request to the 1e-9 the H and C totals meet. +**Known limit.** Volatile oxygen mass is not strictly conserved by volatile species alone because the melt iron buffer exchanges oxygen with dissolved gases. -### Ocean-moles IC converts moles to mass +??? info "Test details" + - Reference: Mass-balance identity through the initial partial-pressure solve: initial_kg equals the requested ppm of the mantle mass per volatile, and the realised reservoirs carry the requested hydrogen and carbon mole totals (conserved by the water and carbon dioxide reactions). + - Tolerance: rel 1e-9 on the stored inventories and the H and C totals (observed below 1e-15). + - Test id: `tests/test_ic.py::test_abundance_ic_realises_the_requested_inventory` + - Command: `make`, then `pytest -o addopts= tests/test_ic.py::test_abundance_ic_realises_the_requested_inventory` + - Companion test: `tests/test_ic.py::test_abundance_and_ocean_mole_solves_agree` verifies consistency between abundance and ocean-mole initialization paths. -**Reference-pinned test**: `tests/test_ic.py::test_ocean_moles_ic_converts_moles_to_mass` +## Ocean-moles IC converts moles to mass -**Reference**: The OCEAN_MOLES constant of constants.c (7.68894973907177e22 mol per Earth ocean): initial_kg equals moles times OCEAN_MOLES times the volatile molar mass. +**Why this check exists.** Unit conversions from ocean equivalents can apply wrong molar masses, or miscalculate moles-to-mass constants. A conversion error scales volatile mass by large factors, leading to invalid atmospheric pressures. -**Tolerance**: rel 1e-9 (closed-form product of stored constants). +**What it tests.** The test evaluates the OCEAN_MOLES constant of constants.c (7.68894973907177e22 mol per Earth ocean). It verifies that initial_kg equals moles times OCEAN_MOLES times volatile molar mass. Reading CO2 inventory with CO molar mass shifts expected mass by 36 percent, which guards against swapped species indices. The test checks positivity and scale bounds on CO2 inventory, the smallest in ocean equivalents (0.0087 oceans; H2 is smaller in mass). -**Command**: `make`, then `pytest -o addopts= tests/test_ic.py::test_ocean_moles_ic_converts_moles_to_mass` +**How we know the right answer.** Moles to mass conversion is a closed-form product of defined constants. The evaluated mass matches to rel 1e-9. -**Discrimination guards**: Reading the CO2 inventory with the CO molar mass shifts the expected mass by 36 percent; positivity and scale bounds on the CO2 inventory, the smallest in ocean equivalents (0.0087 oceans; H2 is smaller in mass). +**Why it matters.** Setting volatile budgets in Earth ocean equivalents allows direct comparison with terrestrial water and carbon inventories in planetary models. + +**Known limit.** The test takes the numerical constant OCEAN_MOLES from `constants.c`, verifying conversion consistency rather than the definition of terrestrial ocean mass. + +??? info "Test details" + - Reference: The OCEAN_MOLES constant of constants.c (7.68894973907177e22 mol per Earth ocean): initial_kg equals moles times OCEAN_MOLES times the volatile molar mass. + - Tolerance: rel 1e-9 (closed-form product of stored constants). + - Test id: `tests/test_ic.py::test_ocean_moles_ic_converts_moles_to_mass` + - Command: `make`, then `pytest -o addopts= tests/test_ic.py::test_ocean_moles_ic_converts_moles_to_mass` + - Companion test: `tests/test_ic.py::test_abundance_and_ocean_mole_solves_agree` confirms that ocean-mole and ppm abundance formulations yield identical states. + +## Related code + +- `ic.c`: `set_ic_interior`, `set_ic_atmosphere`, `set_ic_interior_entropy`, `set_ic_atmosphere_from_initial_total_abundance`, `set_ic_atmosphere_from_ocean_moles`. +- `tests/test_ic.py`: `test_prescribed_adiabat_entropy_sets_the_initial_profile`, `test_abundance_ic_realises_the_requested_inventory`, `test_ocean_moles_ic_converts_moles_to_mass`. diff --git a/docs/Validation/index.md b/docs/Validation/index.md index 0b55a6a3..232d56a2 100644 --- a/docs/Validation/index.md +++ b/docs/Validation/index.md @@ -1,6 +1,13 @@ # Validation -Each physics source of SPIDER has at least one `reference_pinned` test that compares the code with a published benchmark, an analytical limit or an independent implementation. The pages of this tab list these cases, one page per C source file, grouped by the process they describe. Each case gives its reference, its tolerance, its test id and the command that runs it. A full-run regression against the frozen blackbody50 reference complements these cases (`tests/test_regression.py`). +## What this page checks + +This page introduces the scientific validation suite for SPIDER, which tests each physics module. +Validation means that we compare model outputs against analytical limits, laboratory calibrations, and independent implementations. +Each physics source file has at least one pinned test, with a stated tolerance, reference value, and discrimination guards. +If these components are wrong, the interior thermal evolution that SPIDER computes, alone or inside PROTEUS, is wrong. +The table below lists each validation case, organized by physical process. +A full-run regression against the frozen blackbody50 reference complements these cases (`tests/test_regression.py`). Tests in the `unit` tier drive the small C test executables in `tests/c` (`make tests_c`); tests in the `smoke` tier run the `spider` binary (`make`) on short configurations. See [Testing SPIDER](../How-to/test.md) for the tiers and the commands. diff --git a/docs/Validation/interp.md b/docs/Validation/interp.md index 21ebbfb4..cce6e648 100644 --- a/docs/Validation/interp.md +++ b/docs/Validation/interp.md @@ -1,19 +1,33 @@ # interp.c: 1-D and 2-D lookup interpolation -**Source under test**: `interp.c` +## What this page checks -## Cases +This page checks interpolation routines in `interp.c`, which evaluate tabulated material properties. +`interp.c` provides one-dimensional and two-dimensional linear table interpolation. +Validation means that we test interpolator behavior against affine data with exact analytical solutions. +If table interpolation fails, material properties across the mantle are evaluated incorrectly. +The section below details the validation check, its tolerance, and its test command. -### Affine data reproduced exactly including last node +## Affine data reproduced exactly including last node -**Reference-pinned test**: `tests/test_interp.py::test_affine_data_reproduced_exactly_including_last_node` +**Why this check exists.** Table interpolators can suffer from index off-by-one errors, boundary truncation bugs, or incorrect linear weighting. An interpolation bug distorts lookups across all equation of state tables. -**Reference**: Analytical limit: piecewise-linear interpolation reproduces affine data exactly. +**What it tests.** The test evaluates piecewise-linear interpolation on synthetic affine data y = 2x + 1 at on-node, off-node, and exact-endpoint queries. Nearest-neighbour values at bracketing nodes (5 and 7 around the pinned 6) sit far outside tolerance. Sign and range guards bound the pinned value. A companion test pins the chord value 6.5 on y = x**2, confirming that higher-order schemes would fail. -**Tolerance**: rel 1e-14 on y = 2x + 1 at on-node, off-node, and exact-endpoint queries. +**How we know the right answer.** Piecewise-linear interpolation reproduces affine data exactly by mathematical definition. Numerical queries match analytical values to rel 1e-14 across nodes, interior intervals, and endpoints. -**Command**: `make tests_c`, then `pytest -o addopts= tests/test_interp.py::test_affine_data_reproduced_exactly_including_last_node` +**Why it matters.** The interpolation layer feeds every equation of state evaluation in SPIDER (`eos.c` and `eos_lookup.c`). -**Discrimination guards**: Nearest-neighbour values at the bracketing nodes (5 and 7 around the pinned 6) sit far outside the tolerance; sign and range guards bound the pinned value; a companion test pins the chord value 6.5 on y = x**2 so a higher-order scheme would fail. +**Known limit.** Out-of-range queries clamp to table edges rather than extrapolating beyond table boundaries. -The interpolation layer feeds every EOS table evaluation, so its contract (linear between nodes, clamped outside the table range, exact at the nodes including the last one) is pinned on synthetic tables whose values are known in closed form. Out-of-range clamping rather than extrapolation is asserted on both sides of the table. +??? info "Test details" + - Reference: Analytical limit: piecewise-linear interpolation reproduces affine data exactly. + - Tolerance: rel 1e-14 on y = 2x + 1 at on-node, off-node, and exact-endpoint queries. + - Test id: `tests/test_interp.py::test_affine_data_reproduced_exactly_including_last_node` + - Command: `make tests_c`, then `pytest -o addopts= tests/test_interp.py::test_affine_data_reproduced_exactly_including_last_node` + - Companion test: `tests/test_interp.py::test_out_of_range_queries_clamp_to_table_edges` asserts out-of-range clamping at table edges. + +## Related code + +- `interp.c`: `Interp1dCreateAndSet`, `Interp2dCreateAndSet`, `SetInterp1dValue`, `SetInterp2dValue`. +- `tests/test_interp.py`: `test_affine_data_reproduced_exactly_including_last_node`, `test_out_of_range_queries_clamp_to_table_edges`, `test_curved_data_follows_the_piecewise_linear_contract`. diff --git a/docs/Validation/matprop.md b/docs/Validation/matprop.md index d31335b5..0d40e0a6 100644 --- a/docs/Validation/matprop.md +++ b/docs/Validation/matprop.md @@ -1,19 +1,33 @@ # matprop.c: Blended material properties -**Source under test**: `matprop.c` +## What this page checks -## Cases +This page checks material property blending in `matprop.c`, which computes transport coefficients throughout the mantle. +`matprop.c` blends melt and solid properties over the melting interval to determine local viscosity and conductivities. +Validation means that we check blended viscosity against configured end-member values in pure phases. +If material property blending is wrong, mantle convective heat transport and viscosity structures are wrong. +The text below details the check, its tolerance, and its test command. -### Viscosity recovers configured end members +## Viscosity recovers configured end members -**Reference-pinned test**: `tests/test_matprop.py::test_viscosity_recovers_configured_end_members` +**Why this check exists.** Rheological blending functions can apply incorrect weighting formulas, swap melt and solid limits, or invert transition bounds. A bug in property blending causes convection solvers to experience wrong viscosities. -**Reference**: Analytical limit: away from the rheological transition the blended viscosity recovers the configured end-members (melt_log10visc = 2, solid_log10visc = 21). +**What it tests.** The test evaluates the analytical limit where blended viscosity recovers configured end-members away from the rheological transition (melt_log10visc = 2, solid_log10visc = 21). It checks two extreme states: the hot initial state of the blackbody50 run (t = 0, where all 50 nodes are molten), and a cold 0-step run started from a 1600 J/kg/K adiabat (where at least 5 nodes are solid). The recovered end-members must differ by more than 10 decades, guarding against swapped phases. Sibling test `test_material_properties_positive_and_conductivity_constant` asserts positive values for density, heat capacity, conductivity, and expansivity, alongside configured 4.0 W/m/K conductivity. -**Tolerance**: abs 0.01 on log10(viscosity) at the strongly molten nodes and abs 0.05 at the strongly solid nodes. +**How we know the right answer.** Away from the partial-melting interval, blended material properties must approach pure-phase limits asymptotically. Viscosity matches configured values to abs 0.01 on log10(viscosity) at molten nodes and abs 0.05 at solid nodes. -**Command**: `make`, then `pytest -o addopts= tests/test_matprop.py::test_viscosity_recovers_configured_end_members` +**Why it matters.** Mantle viscosity governs convective heat transport. PROTEUS reads the viscosity array `visc_b` from SPIDER. -**Discrimination guards**: The recovered end-members differ by more than 10 decades (swapped-phase guard); all 50 nodes are molten in the hot state and at least 5 solid in the cold one; the viscosity is positive. The sibling test `test_material_properties_positive_and_conductivity_constant` (same file) asserts the positivity of density, heat capacity, conductivity and expansivity at every node and the configured 4.0 W/m/K conductivity. +**Known limit.** The check evaluates asymptotic recovery at extreme states, but does not benchmark the shape of the rheological front through the transition. -Material properties are blended across the melting interval from the per-phase EOS evaluations. The viscosity end-members and the viscosity positivity are pinned on two states: the hot initial state of the short blackbody50 run (t = 0, every node molten) and a cold 0-step run started from a 1600 J/kg/K adiabat (solid nodes); the sibling test `test_material_properties_positive_and_conductivity_constant` checks positivity and the constant conductivity on the full run. +??? info "Test details" + - Reference: Analytical limit: away from the rheological transition the blended viscosity recovers the configured end-members (melt_log10visc = 2, solid_log10visc = 21). + - Tolerance: abs 0.01 on log10(viscosity) at the strongly molten nodes and abs 0.05 at the strongly solid nodes. + - Test id: `tests/test_matprop.py::test_viscosity_recovers_configured_end_members` + - Command: `make`, then `pytest -o addopts= tests/test_matprop.py::test_viscosity_recovers_configured_end_members` + - Companion test: `tests/test_matprop.py::test_material_properties_positive_and_conductivity_constant` asserts positive values for properties and constant conductivity. + +## Related code + +- `matprop.c`: `set_capacitance_staggered`, `set_matprop_basic`. +- `tests/test_matprop.py`: `test_viscosity_recovers_configured_end_members`, `test_material_properties_positive_and_conductivity_constant`. diff --git a/docs/Validation/mesh.md b/docs/Validation/mesh.md index 622d37e1..007fefe1 100644 --- a/docs/Validation/mesh.md +++ b/docs/Validation/mesh.md @@ -1,19 +1,33 @@ # mesh.c: Static mesh and pressure structure -**Source under test**: `mesh.c` +## What this page checks -## Cases +This page checks static radial mesh and pressure profile construction in `mesh.c`. +The mesh module sets up basic and staggered radial nodes, shell areas, shell volumes, and gravitational pressures. +Validation means that we test the native pressure profile against the closed-form Adams-Williamson solution. +If mesh construction fails, shell geometry and pressure fields in the mantle are distorted. +The section below details the validation check, its tolerance, and its test command. -### Pressure profile matches Adams-Williamson closed form +## Pressure profile matches Adams-Williamson closed form -**Reference-pinned test**: `tests/test_mesh.py::test_pressure_profile_matches_adams_williamson_closed_form` +**Why this check exists.** Radial grid generation can suffer from incorrect coordinate spacing, sign errors in hydrostatic integrals, or wrong radial bounds. An error in mesh setup makes the pressure and volume arrays of every physics module wrong. -**Reference**: Analytical limit: the Adams-Williamson pressure profile P(r) = -(rhos g / beta)(exp(beta (R - r)) - 1) in closed form. +**What it tests.** The test evaluates the native Adams-Williamson pressure profile P(r) = -(rhos g / beta)(exp(beta (R - r)) - 1) in closed form. It checks the basic-node pressures through the mantle. A sign-flipped beta and the linear small-beta approximation both fall far outside tolerance, confirming proper discrimination. Sibling test `test_mesh_orderings_interleave_and_shell_masses_close` checks geometric node interleaving and shell mass conservation. Sibling test `test_external_linear_density_mesh_replaces_the_aw_profile` confirms that external meshes replace native profiles cleanly. -**Tolerance**: rel 1e-12 on the basic-node pressures (observed 6e-15; mesh.c evaluates the same closed form, so the agreement is limited by the round trip through the JSON output). +**How we know the right answer.** Under the Adams-Williamson assumptions, the hydrostatic equation integrates to an exact analytical expression. Computed pressures agree with the closed form to rel 1e-12 (observed 6e-15, limited by precision in JSON serialization). -**Command**: `make`, then `pytest -o addopts= tests/test_mesh.py::test_pressure_profile_matches_adams_williamson_closed_form` +**Why it matters.** The mesh module builds the static spatial and pressure framework that every interior routine in SPIDER evaluates. PROTEUS reads radius arrays `radius_b` and pressure arrays `pressure_s` (in `ReadSPIDER` of `spider.py`). -**Discrimination guards**: Sign and monotonicity of the pressure with depth; the core-mantle boundary pressure is bounded to its physical order of magnitude; a sign-flipped beta and the linear small-beta approximation both fall far outside tolerance; the external-mesh pathway is verified to actually differ from the native profile when supplied. +**Known limit.** The native profile assumes an idealized Adams-Williamson compressibility model rather than self-consistent mineralogical structures. -The mesh module builds the static pressure and geometry arrays every physics term runs on. The native profile is pinned against the AW closed form and the geometric consistency between basic and staggered nodes (interleaving, shell masses, monotonicity) is asserted, including for an externally supplied non-AW mesh. +??? info "Test details" + - Reference: Analytical limit: the Adams-Williamson pressure profile P(r) = -(rhos g / beta)(exp(beta (R - r)) - 1) in closed form. + - Tolerance: rel 1e-12 on the basic-node pressures (observed 6e-15; mesh.c evaluates the same closed form, so the agreement is limited by the round trip through the JSON output). + - Test id: `tests/test_mesh.py::test_pressure_profile_matches_adams_williamson_closed_form` + - Command: `make`, then `pytest -o addopts= tests/test_mesh.py::test_pressure_profile_matches_adams_williamson_closed_form` + - Companion test: `tests/test_mesh.py::test_mesh_orderings_interleave_and_shell_masses_close` verifies node interleaving and shell mass conservation. + +## Related code + +- `mesh.c`: `set_mesh`. +- `tests/test_mesh.py`: `test_pressure_profile_matches_adams_williamson_closed_form`, `test_mesh_orderings_interleave_and_shell_masses_close`. diff --git a/docs/Validation/reaction.md b/docs/Validation/reaction.md index 7a7c72d8..77892467 100644 --- a/docs/Validation/reaction.md +++ b/docs/Validation/reaction.md @@ -1,19 +1,37 @@ # reaction.c: Interior chemical reactions -**Source under test**: `reaction.c` +## What this page checks -## Cases +This page checks volatile chemical reactions in `reaction.c`, which models volatile speciation. +`reaction.c` calculates chemical equilibrium and volatile transfers between gas pairs. +Validation means that we check elemental conservation in reacting pairs against exact stoichiometric balances. +If reaction bookkeeping is incorrect, volatile elements appear or disappear during mantle degassing. +The text below details the check, its tolerance, and its test command. -### Elements conserved across each reaction pair +## Elements conserved across each reaction pair -**Reference-pinned test**: `tests/test_reaction.py::test_elements_conserved_across_each_reaction_pair` +**Why this check exists.** Reaction network solvers can violate elemental stoichiometry, misapply molar conversions, or swap reactant signs. A bug in reaction bookkeeping alters total carbon or hydrogen mass during runs. -**Reference**: Analytical conservation: closed-system elemental budgets across the H2O/H2 and CO2/CO reaction pairs of the Bower et al. (2021) three-ocean hydrogen configuration. +**What it tests.** The test evaluates closed-system elemental budgets for H2O/H2 and CO2/CO reaction pairs of the Bower et al. (2021) three-ocean hydrogen configuration. It checks hydrogen and carbon transfer balances after one 1000-year step. The test guards reaction signs (H2O produced from H2, CO2 from CO in this step), and bounds H2O transfer between 1e16 and 1e20 kg, below 1 percent of H2O inventory. Sibling tests verify reservoir closure to rel 1e-10 (`test_reservoir_closure_for_every_volatile`), and inventory closure to rel 1e-5 (`test_reaction_transfer_balances_the_initial_inventory`). -**Tolerance**: rel 1e-3 on the hydrogen and carbon transfer balances after one 1000-year step (observed at round-off level, below 1e-15). +**How we know the right answer.** Stoichiometric conservation in closed chemical systems requires identical element mass before and after reaction. Balances match to rel 1e-3 (observed below 1e-15). -**Command**: `make`, then `pytest -o addopts= tests/test_reaction.py::test_elements_conserved_across_each_reaction_pair` +**Why it matters.** In a SPIDER run with volatiles, the reaction module sets the volatile chemistry of the magma ocean and its atmosphere. PROTEUS does not use the reaction part of SPIDER. -**Discrimination guards**: Sign structure of the transfers (H2O produced from H2, CO2 from CO in this step); the H2O transfer lies between 1e16 and 1e20 kg and below 1 percent of the H2O inventory. Sibling tests in the same file check the per-volatile reservoir closure to rel 1e-10 (`test_reservoir_closure_for_every_volatile`) and the inventory closure to rel 1e-5 (`test_reaction_transfer_balances_the_initial_inventory`). +**Known limit.** The check tests conservation for carbon and hydrogen pairs, but volatile oxygen exchanges with the melt iron buffer. -The reaction module exchanges mass between volatile pairs subject to elemental conservation. The reaction_kg bookkeeping is pinned so that the element handed off by one partner reappears exactly in the stoichiometric fraction of the other. +??? info "Test details" + - Reference: Analytical conservation: closed-system elemental budgets across the H2O/H2 and CO2/CO reaction pairs of the Bower et al. (2021) three-ocean hydrogen configuration. + - Tolerance: rel 1e-3 on the hydrogen and carbon transfer balances after one 1000-year step (observed at round-off level, below 1e-15). + - Test id: `tests/test_reaction.py::test_elements_conserved_across_each_reaction_pair` + - Command: `make`, then `pytest -o addopts= tests/test_reaction.py::test_elements_conserved_across_each_reaction_pair` + - Companion test: `tests/test_reaction.py::test_reservoir_closure_for_every_volatile` checks per-volatile mass closure between melt and gas reservoirs. + +## Related code + +- `reaction.c`: `get_log10_modified_equilibrium_constant`, `get_reaction_quotient_products`, `get_reaction_quotient_reactants`. +- `tests/test_reaction.py`: `test_elements_conserved_across_each_reaction_pair`, `test_reservoir_closure_for_every_volatile`, `test_reaction_transfer_balances_the_initial_inventory`. + +## References + +- Bower, D. J. et al. (2021). Retention of water in the deep interiors of magma ocean planets. diff --git a/docs/Validation/rheologicalfront.md b/docs/Validation/rheologicalfront.md index 02488e55..cf983764 100644 --- a/docs/Validation/rheologicalfront.md +++ b/docs/Validation/rheologicalfront.md @@ -1,19 +1,33 @@ # rheologicalfront.c: Rheological front tracking -**Source under test**: `rheologicalfront.c` +## What this page checks -## Cases +This page checks front tracking in `rheologicalfront.c`, which locates the solidification boundary. +`rheologicalfront.c` detects the critical melt fraction threshold where mantle rheology shifts from liquid-like to solid-like behavior. +Validation means that we check reported front indices and depths against independent crossings of the melt fraction profile. +If front tracking fails, the dynamic boundary between convective magma ocean and solid mantle is misplaced. +The section below details the validation check, its tolerance, and its test command. -### Front location is consistent with melt fraction profile +## Front location is consistent with melt fraction profile -**Reference-pinned test**: `tests/test_rheologicalfront.py::test_front_location_is_consistent_with_melt_fraction_profile` +**Why this check exists.** Front finding routines can misplace grid crossings, apply incorrect search directions, or invert coordinate signs. A bug in front tracking misidentifies the boundary between solid and fluid mantle. -**Reference**: Analytical self-consistency: the reported front index matches the phi_critical = 0.4 crossing of the melt-fraction profile, and the depth equals the surface radius minus the front radius. +**What it tests.** The test evaluates analytical self-consistency: reported front index matches the phi_critical = 0.4 crossing of the melt-fraction profile, and depth equals surface radius minus front radius. It checks that the front depth is positive and bounded by mantle depth, with front pressure located inside the mantle range. Sibling tests pin the un-formed-front limit at t = 0, where the front reports the core-mantle boundary by convention (`test_unformed_front_reports_the_cmb_at_the_fully_molten_start`), and upward migration during bottom-up solidification (`test_front_rises_from_the_cmb_as_the_mantle_solidifies`). -**Tolerance**: Front index within one node of the recomputed crossing; rel 1e-6 on the depth identity. +**How we know the right answer.** The front location is recomputed independently from the discrete melt-fraction profile and geometry arrays. The depth identity matches to rel 1e-6, and the index matches to one node of the recomputed crossing. -**Command**: `make`, then `pytest -o addopts= tests/test_rheologicalfront.py::test_front_location_is_consistent_with_melt_fraction_profile` +**Why it matters.** The rheological front marks the viscosity transition that PROTEUS reads as dynamic front depth (`RF_depth` in `ReadSPIDER` of `spider.py`). -**Discrimination guards**: Depth positivity and the mantle-depth bound; front pressure within the mantle range. Sibling tests in the same file pin the un-formed-front limit at t = 0, where the front reports the core-mantle boundary by convention (`test_unformed_front_reports_the_cmb_at_the_fully_molten_start`), and the upward migration of the front as the mantle solidifies bottom-up (`test_front_rises_from_the_cmb_as_the_mantle_solidifies`). +**Known limit.** Due to discrete spatial grid intervals, the front index is resolved to one node of the interpolated profile crossing. -The rheological front marks the transition between the low- and high-viscosity regimes that PROTEUS reads as the dynamic front depth. Its location is recomputed independently from the melt-fraction profile and the geometry arrays of the same output. +??? info "Test details" + - Reference: Analytical self-consistency: the reported front index matches the phi_critical = 0.4 crossing of the melt-fraction profile, and the depth equals the surface radius minus the front radius. + - Tolerance: Front index within one node of the recomputed crossing; rel 1e-6 on the depth identity. + - Test id: `tests/test_rheologicalfront.py::test_front_location_is_consistent_with_melt_fraction_profile` + - Command: `make`, then `pytest -o addopts= tests/test_rheologicalfront.py::test_front_location_is_consistent_with_melt_fraction_profile` + - Companion test: `tests/test_rheologicalfront.py::test_front_rises_from_the_cmb_as_the_mantle_solidifies` verifies upward front migration as the mantle solidifies. + +## Related code + +- `rheologicalfront.c`: `get_crossover_index`. +- `tests/test_rheologicalfront.py`: `test_front_location_is_consistent_with_melt_fraction_profile`, `test_front_rises_from_the_cmb_as_the_mantle_solidifies`, `test_unformed_front_reports_the_cmb_at_the_fully_molten_start`. diff --git a/docs/Validation/rhs.md b/docs/Validation/rhs.md index a6b0ae0f..475848a8 100644 --- a/docs/Validation/rhs.md +++ b/docs/Validation/rhs.md @@ -1,24 +1,38 @@ # rhs.c: Entropy time-derivative assembly -**Source under test**: `rhs.c` +## What this page checks -## Cases +This page checks the right-hand side time-derivative assembly in `rhs.c`, which governs thermal evolution. +`rhs.c` compiles flux divergences and internal heating source terms into local entropy tendencies dS/dt. +Validation means that we check reported time derivatives against finite-difference slopes between consecutive output snapshots. +If right-hand side evaluation is incorrect, the numerical timestepper integrates along an erroneous trajectory. +The section below details the validation check, its tolerance, and its test command. -### Reported dS/dt matches finite difference of outputs +## Reported dS/dt matches finite difference of outputs -**Reference-pinned test**: `tests/test_rhs.py::test_reported_dsdt_matches_finite_difference_of_outputs` +**Why this check exists.** Time derivative assembly can misplace spatial differencing weights, drop heating source terms, or reverse flux divergence signs. An error in derivative evaluation causes unphysical energy drift during timestepping. -**Reference**: Analytical self-consistency: the reported dS/dt matches the finite-difference slope of consecutive outputs over 10-year macro steps. +**What it tests.** The test evaluates the reported dS/dt against the finite-difference slope of consecutive outputs over 10-year macro steps. It asserts cooling signs at each node, and bounds the largest |dS/dt| between 1e-12 and 1e-4 J/kg/K/s. This bound rejects unscaled annual rates, while finite differencing catches nondimensional values (around 3e-5 here). Sibling test `test_cooling_sign_structure_and_finiteness` checks that radiogenic and tidal source arrays remain zero and that entropy declines smoothly in the upper molten layer. -**Tolerance**: rel 2e-2 at every staggered node (observed at most 0.65 percent, midpoint rule over 10 years). +**How we know the right answer.** In the limit of small timesteps, the discrete difference between output snapshots approaches the instantaneous time derivative. The reported derivative matches the finite-difference rate to rel 2e-2 at staggered nodes (observed at most 0.65 percent). -**Command**: `make`, then `pytest -o addopts= tests/test_rhs.py::test_reported_dsdt_matches_finite_difference_of_outputs` (add `--validation-plots` to write the plot pair) +**Why it matters.** The right-hand side assembly provides the core time derivative that PETSc integrates to advance mantle thermal evolution in SPIDER. ![Reported dS/dt and the slope of S between the 10 and 20 yr outputs against radius, and their relative difference with the tolerance](../assets/validation/rhs_dsdt_finite_difference.svg#only-light){ width="80%" } ![Reported dS/dt and the slope of S between the 10 and 20 yr outputs against radius, and their relative difference with the tolerance](../assets/validation/rhs_dsdt_finite_difference_dark.svg#only-dark){ width="80%" } -Top: the cooling rate -dS/dt that rhs.c reports, as the mean of the 10 and 20 yr outputs (line), and the slope of the entropy between these outputs (dashed), at every staggered node of the blackbody50 run. Bottom: their relative difference per node (dots) and the tolerance of the assertion (dashed); the largest difference is 0.65 percent. +**How to read the plot.** Top: the cooling rate -dS/dt that rhs.c reports, as the mean of the 10 and 20 yr outputs (line), and the slope of the entropy between these outputs (dashed), at every staggered node of the blackbody50 run. Bottom: their relative difference per node (dots) and the tolerance of the assertion (dashed); the largest difference is 0.65 percent. -**Discrimination guards**: Cooling sign at every node; the largest |dS/dt| lies between 1e-12 and 1e-4 J/kg/K/s, so a per-year value fails (a nondimensional value, about 3e-5 here, passes this bound; the finite-difference comparison catches it). The sibling test in the same file checks the zero radiogenic and tidal source arrays and the near-uniform decline in the upper molten layer. +**Known limit.** Discrete finite differencing over 10-year intervals introduces numerical truncation error of up to 0.65 percent relative to instantaneous continuous derivatives. -rhs.c assembles the entropy tendency from the flux divergence and sources; it is validated as the actual derivative of the trajectory the timestepper follows, by comparing the reported tendency against the slope between closely spaced outputs. +??? info "Test details" + - Reference: Analytical self-consistency: the reported dS/dt matches the finite-difference slope of consecutive outputs over 10-year macro steps. + - Tolerance: rel 2e-2 at every staggered node (observed at most 0.65 percent, midpoint rule over 10 years). + - Test id: `tests/test_rhs.py::test_reported_dsdt_matches_finite_difference_of_outputs` + - Command: `make`, then `pytest -o addopts= tests/test_rhs.py::test_reported_dsdt_matches_finite_difference_of_outputs` (add `--validation-plots` to write the plot pair) + - Companion test: `tests/test_rhs.py::test_cooling_sign_structure_and_finiteness` asserts cooling signs and finiteness of time derivatives. + +## Related code + +- `rhs.c`: `RHSFunction`. +- `tests/test_rhs.py`: `test_reported_dsdt_matches_finite_difference_of_outputs`, `test_cooling_sign_structure_and_finiteness`. diff --git a/docs/Validation/twophase.md b/docs/Validation/twophase.md index 2bb76ea7..7a5355bb 100644 --- a/docs/Validation/twophase.md +++ b/docs/Validation/twophase.md @@ -1,19 +1,33 @@ # twophase.c: Global melt and solid mass bookkeeping -**Source under test**: `twophase.c` +## What this page checks -## Cases +This page checks two-phase mass bookkeeping in `twophase.c`, which tracks global magma ocean freezing. +`twophase.c` integrates local melt fractions over shell masses to compute total liquid and solid mantle masses. +Validation means that we check global melt fractions against analytical mass-weighted sums and mass closure. +If two-phase bookkeeping is wrong, global melt fractions and mantle phase inventories are incorrect. +The text below details the check, its tolerance, and its test command. -### Global melt fraction is the mass-weighted profile mean +## Global melt fraction is the mass-weighted profile mean -**Reference-pinned test**: `tests/test_twophase.py::test_global_melt_fraction_is_the_mass_weighted_profile_mean` +**Why this check exists.** Mass sum routines can omit geometric shell weighting, confuse node centers with edges, or violate mass balance. A bug in mass bookkeeping produces inaccurate global melt inventories during cooling. -**Reference**: Analytical identity: the global melt fraction equals Mliq/Mmantle and the mass-weighted mean of the staggered melt-fraction profile. +**What it tests.** The test evaluates the analytical identity: global melt fraction equals Mliq/Mmantle and the mass-weighted mean of the staggered melt-fraction profile. It checks a part-solidified 1200-year state. An unweighted profile mean differs by more than 1e-3, confirming that the test guards spatial weighting. Global melt fraction must lie between 0 and 1 and, at 1200 yr, between 0.2 and 0.9. Sibling tests verify mantle mass closure Mliq + Msol = Mmantle at every output to rel 1e-12 (`test_mantle_mass_closure_at_every_output_time`), and monotone solidification under cooling (`test_solidification_proceeds_monotonically_under_cooling`). -**Tolerance**: rel 1e-10 on both identities at the part-solidified 1200-year state. +**How we know the right answer.** By mass balance, total melt mass divided by mantle mass must identically equal the mass-weighted mean of the radial melt fraction profile. Both identities match to rel 1e-10. -**Command**: `make`, then `pytest -o addopts= tests/test_twophase.py::test_global_melt_fraction_is_the_mass_weighted_profile_mean` +**Why it matters.** Global melt fraction and mantle phase masses are primary outputs read by PROTEUS (`M_mantle_liquid`, `M_mantle_solid`, and `Phi_global` in `ReadSPIDER` of `spider.py`). -**Discrimination guards**: An unweighted profile mean differs by more than 1e-3 (weighting guard); the global melt fraction lies between 0 and 1 and, at 1200 yr, between 0.2 and 0.9. Sibling tests in the same file check the mantle mass closure Mliq + Msol = Mmantle at every output to rel 1e-12 (`test_mantle_mass_closure_at_every_output_time`) and the monotone solidification under cooling with the mantle mass constant (`test_solidification_proceeds_monotonically_under_cooling`). +**Known limit.** The identity holds for spherical shell mass weighting, assuming hydrostatic radial shells. -The two-phase bookkeeping integrates the melt distribution into the global masses PROTEUS consumes. Closure, monotone solidification, and the equivalence of the three melt-fraction definitions are pinned across the full frozen-reference run. +??? info "Test details" + - Reference: Analytical identity: the global melt fraction equals Mliq/Mmantle and the mass-weighted mean of the staggered melt-fraction profile. + - Tolerance: rel 1e-10 on both identities at the part-solidified 1200-year state. + - Test id: `tests/test_twophase.py::test_global_melt_fraction_is_the_mass_weighted_profile_mean` + - Command: `make`, then `pytest -o addopts= tests/test_twophase.py::test_global_melt_fraction_is_the_mass_weighted_profile_mean` + - Companion test: `tests/test_twophase.py::test_mantle_mass_closure_at_every_output_time` asserts mass conservation closure Mliq + Msol = Mmantle across outputs. + +## Related code + +- `twophase.c`: `set_Mliq`, `set_Msol`, `set_dMliqdt`, `set_rheological_front`. +- `tests/test_twophase.py`: `test_global_melt_fraction_is_the_mass_weighted_profile_mean`, `test_mantle_mass_closure_at_every_output_time`, `test_solidification_proceeds_monotonically_under_cooling`. From b5aae39131f71aa6c117bce5bbacaf588f2b349b Mon Sep 17 00:00:00 2001 From: timlichtenberg Date: Mon, 26 Oct 2026 01:38:22 +0100 Subject: [PATCH 02/15] Limit the SPIDER validation pages to what each test checks The right-hand-side, rheological-front, boundary, mesh, material, reaction, composite and lookup pages name only the faults their tests catch and state the shared or same-rule comparisons as known limits. The rheological-front page names the PROTEUS field it feeds, the copied constants are described as copies, the unsourced reference lists are removed, and the observed flux ranges are marked as observed. --- docs/Validation/atmosphere.md | 4 ++-- docs/Validation/bc.md | 5 +++-- docs/Validation/energy.md | 4 ++-- docs/Validation/eos.md | 10 ++-------- docs/Validation/eos_composite.md | 2 +- docs/Validation/eos_lookup.md | 7 +------ docs/Validation/ic.md | 6 +++--- docs/Validation/index.md | 4 ++-- docs/Validation/interp.md | 4 ++-- docs/Validation/matprop.md | 4 ++-- docs/Validation/mesh.md | 2 +- docs/Validation/reaction.md | 6 +----- docs/Validation/rheologicalfront.md | 6 +++--- docs/Validation/rhs.md | 4 ++-- docs/Validation/twophase.md | 2 +- 15 files changed, 28 insertions(+), 42 deletions(-) diff --git a/docs/Validation/atmosphere.md b/docs/Validation/atmosphere.md index dadfbfbe..1d41d2e8 100644 --- a/docs/Validation/atmosphere.md +++ b/docs/Validation/atmosphere.md @@ -18,7 +18,7 @@ The section below details the validation check, its tolerance, and its test comm **Why it matters.** The atmosphere module computes the surface energy balance, which drives planetary mantle cooling. PROTEUS reads the surface flux `Fatm` as `F_int`, and surface temperature `temperature_surface` as `T_magma`, to couple the interior to the atmosphere. -**Known limit.** The test evaluates the analytical formula using the code constant sigma = 5.670367e-8 W m-2 K-4. An error in `constants.c` would affect both values, and is not detected by this comparison. +**Known limit.** The test uses its own copy of the source value, sigma = 5.670367e-8 W m-2 K-4. A change of the constant in the code fails the test, but a value that is wrong in both places does not. PROTEUS uses this grey-body branch only with `surface_bc_mode = 'grey_body'`; its default is a prescribed flux (`SURFACE_BC 4`). ??? info "Test details" - Reference: Analytical limit: the grey-body flux F = emissivity sigma (T_surf^4 - T_eqm^4) with emissivity 1 and T_eqm = 273 K (Stefan-Boltzmann). @@ -29,5 +29,5 @@ The section below details the validation check, its tolerance, and its test comm ## Related code -- `atmosphere.c`: `set_atmosphere_emissivity_and_flux`, `set_surface_temperature_from_flux`. +- `atmosphere.c`: `set_atmosphere_emissivity_and_flux`. - `tests/test_atmosphere.py`: `test_grey_body_flux_matches_stefan_boltzmann_limit`, `test_surface_cools_and_flux_decays_across_macro_steps`. diff --git a/docs/Validation/bc.md b/docs/Validation/bc.md index d14733b7..65ad69db 100644 --- a/docs/Validation/bc.md +++ b/docs/Validation/bc.md @@ -10,7 +10,7 @@ The sections below detail two validation cases, their tolerances, and their test ## Prescribed-flux BC echoes value and inverts emissivity -**Why this check exists.** Surface boundary routines can misinterpret options, swap boundary condition flags, or miscalculate equivalent emissivities. A bug in prescribed boundary dispatch causes the interior to use an incorrect surface heat flux, without failing explicitly. +**Why this check exists.** With a prescribed surface flux, the reported flux must equal the prescribed value and the emissivity must be its grey-body inversion. A wrong boundary-condition (BC) branch or a wrong inversion gives a wrong reported flux or emissivity without an error message. **What it tests.** The test evaluates boundary condition SURFACE_BC 4, where the surface flux equals the prescribed value. The reported emissivity must equal the grey-body inversion value. The test guards against wrong dispatch, confirming that the grey-body flux at the surface temperature differs from the prescribed value by more than 10 percent, and the emissivity differs from 1 by more than 0.5. The flux must be positive, and lie between 1e2 and 1e9 W/m^2. Sibling test `test_mantle_cools_from_the_top_between_surface_and_cmb` asserts temperature ordering across the mantle, and near-zero surface pressure. @@ -18,7 +18,7 @@ The sections below detail two validation cases, their tolerances, and their test **Why it matters.** PROTEUS passes the atmospheric radiative flux into SPIDER using option `-SURFACE_BC 4` and `-surface_bc_value`, so SPIDER must echo this flux exactly. -**Known limit.** This test validates prescribed flux injection, but does not test coupled atmospheric radiative transfer within SPIDER itself. +**Known limit.** The test reads only the reported `Fatm` and emissivity; it does not check that the interior total flux at the surface (`Jtot` at node 0, set in `bc.c`) uses the prescribed value. The `SURFACE_BC` switch is in `atmosphere.c`. ??? info "Test details" - Reference: Analytical identity: with SURFACE_BC 4 the surface flux equals the prescribed value, and the reported emissivity is its grey-body inversion. @@ -45,6 +45,7 @@ The sections below detail two validation cases, their tolerances, and their test - Test id: `tests/test_bc.py::test_constant_entropy_bc_pins_the_initial_surface` - Command: `make`, then `pytest -o addopts= tests/test_bc.py::test_constant_entropy_bc_pins_the_initial_surface` - Companion test: `tests/test_bc.py::test_prescribed_core_flux_slows_cmb_cooling` verifies that core heat flux slows cooling at the core-mantle boundary. + - The test also checks that the core-side entropy lies between 2400 and 2700 J/kg/K. ## Related code diff --git a/docs/Validation/energy.md b/docs/Validation/energy.md index 4dacde2c..a1b64ac8 100644 --- a/docs/Validation/energy.md +++ b/docs/Validation/energy.md @@ -12,7 +12,7 @@ The sections below detail three validation cases, their tolerances, and their te **Why this check exists.** Runtime configuration flags can fail to disconnect disabled flux routines, or leak residual values into total energy balances. A failure in flux superposition produces incorrect thermal gradients without raising an error. -**What it tests.** The test evaluates the flux superposition architecture when specific transport terms are switched off. A disabled transport mechanism must hold zero flux, within abs 1e-20 W/m^2, which is at least 17 decades below active fluxes. With conduction off, convection yields 9e8 to 2e9 W/m^2. In a conduction-only run, conduction yields 1.9e-3 to 1.3e-2 W/m^2. Total flux equals conductive flux to rel 1e-12 at every interior node. The surface node differs by more than 1e3 W/m^2. +**What it tests.** The test evaluates the flux superposition architecture when specific transport terms are switched off. A disabled transport mechanism must hold zero flux, within abs 1e-20 W/m^2, which is at least 17 decades below active fluxes. With conduction off, convection gives 9e8 to 2e9 W/m^2 (observed; the test asserts more than 1e3). In a conduction-only run, conduction gives 1.9e-3 to 1.3e-2 W/m^2 (observed). Total flux equals conductive flux to rel 1e-12 at every interior node. The surface node differs by more than 1e3 W/m^2. **How we know the right answer.** Superposition of linear or disjoint flux mechanisms is an analytical identity. Inactive transport terms must vanish to rounding precision, while active terms reproduce isolated components. @@ -56,7 +56,7 @@ The sections below detail three validation cases, their tolerances, and their te **Why it matters.** The steady-state initial condition starts a run from a profile in energy balance. -**Known limit.** The test uses the Stefan-Boltzmann constant from `constants.c`, so an error in the constant definition is not isolated by this check. +**Known limit.** The test uses its own copy of the Stefan-Boltzmann constant of the source, so a value that is wrong in both places does not fail it. ??? info "Test details" - Reference: Analytical limit: the steady-state initial condition equalises the energy flow through every basic node and closes the surface radiation balance sigma (T_surf^4 - teqm^4) with the Stefan-Boltzmann constant of constants.c. diff --git a/docs/Validation/eos.md b/docs/Validation/eos.md index dd1cebde..1b869bb8 100644 --- a/docs/Validation/eos.md +++ b/docs/Validation/eos.md @@ -16,7 +16,7 @@ The sections below detail two validation cases, their tolerances, and their test **How we know the right answer.** The independent reader evaluates the raw table file using Python numpy interpolation. The C interpolation reproduces these reference values to rel 1e-12. -**Why it matters.** The phase boundary determines where melting occurs throughout the mantle. SPIDER evaluates this boundary to compute local melt fraction and global melt volume. +**Why it matters.** The phase boundary determines where melting occurs throughout the mantle. SPIDER evaluates this boundary to compute the local and global melt fraction. **Known limit.** The independent verification reads the same source table file, checking interpolation rather than physical accuracy of the underlying experimental dataset. @@ -37,7 +37,7 @@ The sections below detail two validation cases, their tolerances, and their test **Why it matters.** Compositional controls on mantle viscosity determine how mantle convective vigor responds to planetary bulk composition. -**Known limit.** The benchmark tests numerical implementation of the parameterisation, but does not test mineralogical uncertainties in mantle rheology. +**Known limit.** The test re-encodes the coefficients as in `eos.c`, so a coefficient that is wrong in both copies is not caught; the 2 hand-derived literals catch a transcription error in either copy. ??? info "Test details" - Reference: The piecewise Mg/Si log10 viscosity prefactor of Spaargaren et al. (2020). The test re-encodes the published coefficients independently of eos.c and additionally pins the two branch-pair shifts as hand-derived literals (0.2074 and -1.35803), so a transcription error in either copy of the formula surfaces. @@ -50,9 +50,3 @@ The sections below detail two validation cases, their tolerances, and their test - `eos.c`: `EOSSetUpFromOptions`, `EOSGetPhaseBoundary`, `EOSEvalSetViscosity`. - `tests/test_eos.py`: `test_phase_boundary_matches_independent_table_read`, `test_compositional_viscosity_matches_spaargaren`. - -## References - -- Andrault, D. et al. (2011). Solidus and liquidus for deep mantle minerals. -- Hirschmann, M. M. (2013). Iron and volatile partitioning during mantle melting. -- Spaargaren, R. J. et al. (2020). The influence of bulk composition on the rheology of rocky exoplanet mantles. diff --git a/docs/Validation/eos_composite.md b/docs/Validation/eos_composite.md index f4f17570..81a95c6e 100644 --- a/docs/Validation/eos_composite.md +++ b/docs/Validation/eos_composite.md @@ -18,7 +18,7 @@ The section below details the validation check, its tolerance, and its test comm **Why it matters.** The composite EOS determines the local melt fraction and effective thermodynamic properties of partially molten mantle shells. -**Known limit.** This test pins the linear entropy lever rule, but does not benchmark non-linear eutectic melting curves. +**Known limit.** S_liq, S_sol and the fusion entropy come from the code's own outputs, so the test checks internal consistency: the melt fraction is compared with (S - S_sol)/fusion from those values, not with a pinned 0.25. ??? info "Test details" - Reference: Analytical limit: by construction of the two-phase region, fusion = S_liq - S_sol and phi = (S - S_sol)/fusion between the phase boundaries. diff --git a/docs/Validation/eos_lookup.md b/docs/Validation/eos_lookup.md index 26b55531..064430e0 100644 --- a/docs/Validation/eos_lookup.md +++ b/docs/Validation/eos_lookup.md @@ -5,7 +5,7 @@ This page checks the tabulated thermodynamic lookup in `eos_lookup.c`, which reads mineral tables. `eos_lookup.c` bilinearly reads thermodynamic properties from pressure-entropy grids. Validation means that we check C table reads against an independent Python code. -If table reads fail, melt density and temperature profiles across the mantle are incorrect. +If table reads fail, the density and the other tabulated properties of the mantle are wrong; the test compares the density only. The section below details the check, its tolerance, and its test command. ## Melt density matches independent bilinear evaluation @@ -31,8 +31,3 @@ The section below details the check, its tolerance, and its test command. - `eos_lookup.c`: `EOSCreate_Lookup`, `EOSEval_Lookup`, `SetInterp2dValue`. - `tests/test_eos_lookup.py`: `test_melt_density_matches_independent_bilinear_evaluation`, `test_thermodynamic_quantities_positive_and_rho_monotonic_in_pressure`. - -## References - -- de Koker, N. and Stixrude, L. (2009). Self-consistent thermodynamic properties of liquid MgSiO3. -- Bower, D. J. et al. (2018). Numerical methods for magma ocean evolution. diff --git a/docs/Validation/ic.md b/docs/Validation/ic.md index 997189bc..0a8eba0d 100644 --- a/docs/Validation/ic.md +++ b/docs/Validation/ic.md @@ -10,7 +10,7 @@ The sections below detail three validation cases, their tolerances, and their te ## Prescribed adiabat entropy sets the initial profile -**Why this check exists.** Initial condition routines can apply gradient ramps incorrectly, misinterpret input entropy parameters, or fail to write values to staggered mesh nodes. A bug in initial condition setup shifts the starting temperature structure, and the later cooling is wrong too. +**Why this check exists.** Initial condition (IC) routines can misinterpret input entropy parameters or fail to write values to the staggered mesh nodes; a gradient ramp error shows only if the profile mean leaves 2600 to 2628.7 J/kg/K. A bug in initial condition setup shifts the starting temperature structure, and the later cooling is wrong too. **What it tests.** The test evaluates the prescribed adiabat entropy (2600 J/kg/K at the top staggered node) with the configured gradient ramp. The profile mean lies between the adiabat entropy and that value plus |ic_dsdr| times mantle depth. A run with ic_adiabat_entropy = 2400 shifts the mean by 200 J/kg/K to rel 1e-9, guarding against ignored option inputs. Entropy is positive, with its mean between 1e3 and 1e4 J/kg/K. Top-node entropy matches at t = 0 to rel 1e-3 (observed exact). Sibling test `test_initial_adiabat_is_nearly_flat_until_cooling_imprints_structure` checks that subsequent cooling imprints thermal structure. @@ -56,14 +56,14 @@ The sections below detail three validation cases, their tolerances, and their te **Why it matters.** Setting volatile budgets in Earth ocean equivalents allows direct comparison with terrestrial water and carbon inventories in planetary models. -**Known limit.** The test takes the numerical constant OCEAN_MOLES from `constants.c`, verifying conversion consistency rather than the definition of terrestrial ocean mass. +**Known limit.** The test uses its own copy of the OCEAN_MOLES constant of the source, so it checks the conversion, not the definition of the terrestrial ocean mass; a value that is wrong in both places passes. ??? info "Test details" - Reference: The OCEAN_MOLES constant of constants.c (7.68894973907177e22 mol per Earth ocean): initial_kg equals moles times OCEAN_MOLES times the volatile molar mass. - Tolerance: rel 1e-9 (closed-form product of stored constants). - Test id: `tests/test_ic.py::test_ocean_moles_ic_converts_moles_to_mass` - Command: `make`, then `pytest -o addopts= tests/test_ic.py::test_ocean_moles_ic_converts_moles_to_mass` - - Companion test: `tests/test_ic.py::test_abundance_and_ocean_mole_solves_agree` confirms that ocean-mole and ppm abundance formulations yield identical states. + - Companion test: `tests/test_ic.py::test_abundance_and_ocean_mole_solves_agree` confirms that the ocean-mole and ppm abundance formulations give the same partial pressures to rel 5e-3. ## Related code diff --git a/docs/Validation/index.md b/docs/Validation/index.md index 232d56a2..18105a2b 100644 --- a/docs/Validation/index.md +++ b/docs/Validation/index.md @@ -3,8 +3,8 @@ ## What this page checks This page introduces the scientific validation suite for SPIDER, which tests each physics module. -Validation means that we compare model outputs against analytical limits, laboratory calibrations, and independent implementations. -Each physics source file has at least one pinned test, with a stated tolerance, reference value, and discrimination guards. +Validation means that we compare model outputs against analytical limits and independent implementations. +Each physics source file has at least one pinned test, with a stated tolerance and reference value. If these components are wrong, the interior thermal evolution that SPIDER computes, alone or inside PROTEUS, is wrong. The table below lists each validation case, organized by physical process. A full-run regression against the frozen blackbody50 reference complements these cases (`tests/test_regression.py`). diff --git a/docs/Validation/interp.md b/docs/Validation/interp.md index cce6e648..221f934c 100644 --- a/docs/Validation/interp.md +++ b/docs/Validation/interp.md @@ -16,9 +16,9 @@ The section below details the validation check, its tolerance, and its test comm **How we know the right answer.** Piecewise-linear interpolation reproduces affine data exactly by mathematical definition. Numerical queries match analytical values to rel 1e-14 across nodes, interior intervals, and endpoints. -**Why it matters.** The interpolation layer feeds every equation of state evaluation in SPIDER (`eos.c` and `eos_lookup.c`). +**Why it matters.** The interpolation layer feeds the tabulated equation-of-state lookups in SPIDER (`eos_lookup.c`). -**Known limit.** Out-of-range queries clamp to table edges rather than extrapolating beyond table boundaries. +**Known limit.** The case covers 1-D interpolation with unit scalings; the 2-D bilinear case (`test_bilinear_plane_exact_everywhere_including_grid_corner`) and per-column scaling are not part of it. ??? info "Test details" - Reference: Analytical limit: piecewise-linear interpolation reproduces affine data exactly. diff --git a/docs/Validation/matprop.md b/docs/Validation/matprop.md index 0d40e0a6..5d806812 100644 --- a/docs/Validation/matprop.md +++ b/docs/Validation/matprop.md @@ -10,7 +10,7 @@ The text below details the check, its tolerance, and its test command. ## Viscosity recovers configured end members -**Why this check exists.** Rheological blending functions can apply incorrect weighting formulas, swap melt and solid limits, or invert transition bounds. A bug in property blending causes convection solvers to experience wrong viscosities. +**Why this check exists.** Rheological blending functions can swap the melt and solid limits; the test samples only nearly pure nodes, so a wrong weighting inside the melting interval is not caught. A bug in property blending causes convection solvers to experience wrong viscosities. **What it tests.** The test evaluates the analytical limit where blended viscosity recovers configured end-members away from the rheological transition (melt_log10visc = 2, solid_log10visc = 21). It checks two extreme states: the hot initial state of the blackbody50 run (t = 0, where all 50 nodes are molten), and a cold 0-step run started from a 1600 J/kg/K adiabat (where at least 5 nodes are solid). The recovered end-members must differ by more than 10 decades, guarding against swapped phases. Sibling test `test_material_properties_positive_and_conductivity_constant` asserts positive values for density, heat capacity, conductivity, and expansivity, alongside configured 4.0 W/m/K conductivity. @@ -25,7 +25,7 @@ The text below details the check, its tolerance, and its test command. - Tolerance: abs 0.01 on log10(viscosity) at the strongly molten nodes and abs 0.05 at the strongly solid nodes. - Test id: `tests/test_matprop.py::test_viscosity_recovers_configured_end_members` - Command: `make`, then `pytest -o addopts= tests/test_matprop.py::test_viscosity_recovers_configured_end_members` - - Companion test: `tests/test_matprop.py::test_material_properties_positive_and_conductivity_constant` asserts positive values for properties and constant conductivity. + - Companion test: `tests/test_matprop.py::test_material_properties_positive_and_conductivity_constant` asserts positive values for the properties and the viscosity, and a constant conductivity. ## Related code diff --git a/docs/Validation/mesh.md b/docs/Validation/mesh.md index 007fefe1..2f86a7dc 100644 --- a/docs/Validation/mesh.md +++ b/docs/Validation/mesh.md @@ -10,7 +10,7 @@ The section below details the validation check, its tolerance, and its test comm ## Pressure profile matches Adams-Williamson closed form -**Why this check exists.** Radial grid generation can suffer from incorrect coordinate spacing, sign errors in hydrostatic integrals, or wrong radial bounds. An error in mesh setup makes the pressure and volume arrays of every physics module wrong. +**Why this check exists.** Radial grid generation can suffer from sign errors in the hydrostatic integral or wrong radial bounds; the test evaluates the expected pressure at the code's own radii, so a spacing error is not caught. An error in mesh setup makes the pressure and volume arrays of every physics module wrong. **What it tests.** The test evaluates the native Adams-Williamson pressure profile P(r) = -(rhos g / beta)(exp(beta (R - r)) - 1) in closed form. It checks the basic-node pressures through the mantle. A sign-flipped beta and the linear small-beta approximation both fall far outside tolerance, confirming proper discrimination. Sibling test `test_mesh_orderings_interleave_and_shell_masses_close` checks geometric node interleaving and shell mass conservation. Sibling test `test_external_linear_density_mesh_replaces_the_aw_profile` confirms that external meshes replace native profiles cleanly. diff --git a/docs/Validation/reaction.md b/docs/Validation/reaction.md index 77892467..229c1c2c 100644 --- a/docs/Validation/reaction.md +++ b/docs/Validation/reaction.md @@ -10,7 +10,7 @@ The text below details the check, its tolerance, and its test command. ## Elements conserved across each reaction pair -**Why this check exists.** Reaction network solvers can violate elemental stoichiometry, misapply molar conversions, or swap reactant signs. A bug in reaction bookkeeping alters total carbon or hydrogen mass during runs. +**Why this check exists.** Reaction bookkeeping can violate elemental stoichiometry, misapply molar conversions or swap reactant signs; conservation holds for any equilibrium constant, so a wrong equilibrium constant is not caught. A bug in reaction bookkeeping alters total carbon or hydrogen mass during runs. **What it tests.** The test evaluates closed-system elemental budgets for H2O/H2 and CO2/CO reaction pairs of the Bower et al. (2021) three-ocean hydrogen configuration. It checks hydrogen and carbon transfer balances after one 1000-year step. The test guards reaction signs (H2O produced from H2, CO2 from CO in this step), and bounds H2O transfer between 1e16 and 1e20 kg, below 1 percent of H2O inventory. Sibling tests verify reservoir closure to rel 1e-10 (`test_reservoir_closure_for_every_volatile`), and inventory closure to rel 1e-5 (`test_reaction_transfer_balances_the_initial_inventory`). @@ -31,7 +31,3 @@ The text below details the check, its tolerance, and its test command. - `reaction.c`: `get_log10_modified_equilibrium_constant`, `get_reaction_quotient_products`, `get_reaction_quotient_reactants`. - `tests/test_reaction.py`: `test_elements_conserved_across_each_reaction_pair`, `test_reservoir_closure_for_every_volatile`, `test_reaction_transfer_balances_the_initial_inventory`. - -## References - -- Bower, D. J. et al. (2021). Retention of water in the deep interiors of magma ocean planets. diff --git a/docs/Validation/rheologicalfront.md b/docs/Validation/rheologicalfront.md index cf983764..ae52cf7c 100644 --- a/docs/Validation/rheologicalfront.md +++ b/docs/Validation/rheologicalfront.md @@ -10,15 +10,15 @@ The section below details the validation check, its tolerance, and its test comm ## Front location is consistent with melt fraction profile -**Why this check exists.** Front finding routines can misplace grid crossings, apply incorrect search directions, or invert coordinate signs. A bug in front tracking misidentifies the boundary between solid and fluid mantle. +**Why this check exists.** The front depth must follow from the front radius, and the front must lie inside the mantle. A wrong depth formula, an inverted coordinate sign or a front outside the mantle gives a wrong front without an error message. **What it tests.** The test evaluates analytical self-consistency: reported front index matches the phi_critical = 0.4 crossing of the melt-fraction profile, and depth equals surface radius minus front radius. It checks that the front depth is positive and bounded by mantle depth, with front pressure located inside the mantle range. Sibling tests pin the un-formed-front limit at t = 0, where the front reports the core-mantle boundary by convention (`test_unformed_front_reports_the_cmb_at_the_fully_molten_start`), and upward migration during bottom-up solidification (`test_front_rises_from_the_cmb_as_the_mantle_solidifies`). **How we know the right answer.** The front location is recomputed independently from the discrete melt-fraction profile and geometry arrays. The depth identity matches to rel 1e-6, and the index matches to one node of the recomputed crossing. -**Why it matters.** The rheological front marks the viscosity transition that PROTEUS reads as dynamic front depth (`RF_depth` in `ReadSPIDER` of `spider.py`). +**Why it matters.** PROTEUS reads `Phi_global` from this front block (`rheological_front_phi` in `ReadSPIDER` of `spider.py`). -**Known limit.** Due to discrete spatial grid intervals, the front index is resolved to one node of the interpolated profile crossing. +**Known limit.** The test recomputes the crossing with the same rule as `get_crossover_index` and allows one node of difference, so a one-node misplacement passes; the code takes the first node below `phi_critical` without interpolation. PROTEUS takes `RF_depth` from the dynamic front (`rheological_front_dynamic`, a crossing of the regime at 1.5), which this test does not cover. ??? info "Test details" - Reference: Analytical self-consistency: the reported front index matches the phi_critical = 0.4 crossing of the melt-fraction profile, and the depth equals the surface radius minus the front radius. diff --git a/docs/Validation/rhs.md b/docs/Validation/rhs.md index 475848a8..86be9393 100644 --- a/docs/Validation/rhs.md +++ b/docs/Validation/rhs.md @@ -10,7 +10,7 @@ The section below details the validation check, its tolerance, and its test comm ## Reported dS/dt matches finite difference of outputs -**Why this check exists.** Time derivative assembly can misplace spatial differencing weights, drop heating source terms, or reverse flux divergence signs. An error in derivative evaluation causes unphysical energy drift during timestepping. +**Why this check exists.** The time derivative that SPIDER reports must be the one the time stepper integrates. A unit or scaling slip in the reported value, or a gross sign flip, makes the reported rate disagree with the trajectory without an error message. **What it tests.** The test evaluates the reported dS/dt against the finite-difference slope of consecutive outputs over 10-year macro steps. It asserts cooling signs at each node, and bounds the largest |dS/dt| between 1e-12 and 1e-4 J/kg/K/s. This bound rejects unscaled annual rates, while finite differencing catches nondimensional values (around 3e-5 here). Sibling test `test_cooling_sign_structure_and_finiteness` checks that radiogenic and tidal source arrays remain zero and that entropy declines smoothly in the upper molten layer. @@ -23,7 +23,7 @@ The section below details the validation check, its tolerance, and its test comm **How to read the plot.** Top: the cooling rate -dS/dt that rhs.c reports, as the mean of the 10 and 20 yr outputs (line), and the slope of the entropy between these outputs (dashed), at every staggered node of the blackbody50 run. Bottom: their relative difference per node (dots) and the tolerance of the assertion (dashed); the largest difference is 0.65 percent. -**Known limit.** Discrete finite differencing over 10-year intervals introduces numerical truncation error of up to 0.65 percent relative to instantaneous continuous derivatives. +**Known limit.** The trajectory comes from the same right-hand side that reports dS/dt, so a wrong right-hand side gives a consistent wrong trajectory and passes; dS/dt is not compared with an independent physical value. The run has no heating, so a dropped heating term cannot show. Finite differencing over 10-year intervals adds a truncation error of up to 0.65 percent. ??? info "Test details" - Reference: Analytical self-consistency: the reported dS/dt matches the finite-difference slope of consecutive outputs over 10-year macro steps. diff --git a/docs/Validation/twophase.md b/docs/Validation/twophase.md index 7a5355bb..b8ae8a2d 100644 --- a/docs/Validation/twophase.md +++ b/docs/Validation/twophase.md @@ -18,7 +18,7 @@ The text below details the check, its tolerance, and its test command. **Why it matters.** Global melt fraction and mantle phase masses are primary outputs read by PROTEUS (`M_mantle_liquid`, `M_mantle_solid`, and `Phi_global` in `ReadSPIDER` of `spider.py`). -**Known limit.** The identity holds for spherical shell mass weighting, assuming hydrostatic radial shells. +**Known limit.** The identity is definitional and uses the code's own `mass_s` and `phi_s`, so a wrong shell mass or melt fraction passes. ??? info "Test details" - Reference: Analytical identity: the global melt fraction equals Mliq/Mmantle and the mass-weighted mean of the staggered melt-fraction profile. From d6c3a19b61a2c0224456d63566a362ab236a1648 Mon Sep 17 00:00:00 2001 From: timlichtenberg Date: Mon, 26 Oct 2026 01:59:46 +0100 Subject: [PATCH 03/15] Move the SPIDER untested faults into known limits The mesh, material, IC and reaction pages keep caught faults in their first lead-in and state the rest as known limits. The interpolation page names all its callers, the Spaargaren limit states what the 2 literals catch, the rheological-front page says which PROTEUS fields come from other blocks, and the observed flux ranges and copied constants are labelled as such. --- docs/Validation/atmosphere.md | 4 ++-- docs/Validation/bc.md | 5 ++--- docs/Validation/energy.md | 8 ++++---- docs/Validation/eos.md | 2 +- docs/Validation/eos_lookup.md | 2 +- docs/Validation/ic.md | 6 +++--- docs/Validation/interp.md | 2 +- docs/Validation/matprop.md | 6 +++--- docs/Validation/mesh.md | 4 ++-- docs/Validation/reaction.md | 4 ++-- docs/Validation/rheologicalfront.md | 4 ++-- docs/Validation/twophase.md | 2 +- 12 files changed, 24 insertions(+), 25 deletions(-) diff --git a/docs/Validation/atmosphere.md b/docs/Validation/atmosphere.md index 1d41d2e8..56584d47 100644 --- a/docs/Validation/atmosphere.md +++ b/docs/Validation/atmosphere.md @@ -18,11 +18,11 @@ The section below details the validation check, its tolerance, and its test comm **Why it matters.** The atmosphere module computes the surface energy balance, which drives planetary mantle cooling. PROTEUS reads the surface flux `Fatm` as `F_int`, and surface temperature `temperature_surface` as `T_magma`, to couple the interior to the atmosphere. -**Known limit.** The test uses its own copy of the source value, sigma = 5.670367e-8 W m-2 K-4. A change of the constant in the code fails the test, but a value that is wrong in both places does not. PROTEUS uses this grey-body branch only with `surface_bc_mode = 'grey_body'`; its default is a prescribed flux (`SURFACE_BC 4`). +**Known limit.** The test uses its own copy of the value in `constants.c`, sigma = 5.670367e-8 W m-2 K-4. A change of the constant in the code larger than the tolerance fails the test, but a value that is wrong in both places does not. PROTEUS uses this grey-body branch only with `surface_bc_mode = 'grey_body'`; its default is a prescribed flux (`SURFACE_BC 4`). ??? info "Test details" - Reference: Analytical limit: the grey-body flux F = emissivity sigma (T_surf^4 - T_eqm^4) with emissivity 1 and T_eqm = 273 K (Stefan-Boltzmann). - - Tolerance: rel 1e-6 against the reported surface temperature, using the code constant sigma = 5.670367e-8 W m-2 K-4. + - Tolerance: rel 1e-6 against the reported surface temperature, using the test's copy of the code value sigma = 5.670367e-8 W m-2 K-4. - Test id: `tests/test_atmosphere.py::test_grey_body_flux_matches_stefan_boltzmann_limit` - Command: `make`, then `pytest -o addopts= tests/test_atmosphere.py::test_grey_body_flux_matches_stefan_boltzmann_limit` - Companion test: `tests/test_atmosphere.py::test_surface_cools_and_flux_decays_across_macro_steps` confirms monotone decline of flux and surface temperature over outputs. diff --git a/docs/Validation/bc.md b/docs/Validation/bc.md index 65ad69db..8cae80ee 100644 --- a/docs/Validation/bc.md +++ b/docs/Validation/bc.md @@ -18,7 +18,7 @@ The sections below detail two validation cases, their tolerances, and their test **Why it matters.** PROTEUS passes the atmospheric radiative flux into SPIDER using option `-SURFACE_BC 4` and `-surface_bc_value`, so SPIDER must echo this flux exactly. -**Known limit.** The test reads only the reported `Fatm` and emissivity; it does not check that the interior total flux at the surface (`Jtot` at node 0, set in `bc.c`) uses the prescribed value. The `SURFACE_BC` switch is in `atmosphere.c`. +**Known limit.** The test reads only the reported `Fatm` and emissivity; it does not check that the interior total flux at the surface (`Jtot` at node 0, set in `bc.c`) uses the prescribed value. `SURFACE_BC` is read in `atmosphere.c`, `bc.c` and `energy.c`. ??? info "Test details" - Reference: Analytical identity: with SURFACE_BC 4 the surface flux equals the prescribed value, and the reported emissivity is its grey-body inversion. @@ -41,11 +41,10 @@ The sections below detail two validation cases, their tolerances, and their test ??? info "Test details" - Reference: Analytical identity: the constant-entropy boundary writes ic_surface_entropy (2550 J/kg/K) onto the surface basic node, preserved through the steady-state energy solve into the initial output. - - Tolerance: rel 1e-10 (assigned, not solved). + - Tolerance: rel 1e-10 (assigned, not solved); the core-side entropy between 2400 and 2700 J/kg/K. - Test id: `tests/test_bc.py::test_constant_entropy_bc_pins_the_initial_surface` - Command: `make`, then `pytest -o addopts= tests/test_bc.py::test_constant_entropy_bc_pins_the_initial_surface` - Companion test: `tests/test_bc.py::test_prescribed_core_flux_slows_cmb_cooling` verifies that core heat flux slows cooling at the core-mantle boundary. - - The test also checks that the core-side entropy lies between 2400 and 2700 J/kg/K. ## Related code diff --git a/docs/Validation/energy.md b/docs/Validation/energy.md index a1b64ac8..57f4fc24 100644 --- a/docs/Validation/energy.md +++ b/docs/Validation/energy.md @@ -12,9 +12,9 @@ The sections below detail three validation cases, their tolerances, and their te **Why this check exists.** Runtime configuration flags can fail to disconnect disabled flux routines, or leak residual values into total energy balances. A failure in flux superposition produces incorrect thermal gradients without raising an error. -**What it tests.** The test evaluates the flux superposition architecture when specific transport terms are switched off. A disabled transport mechanism must hold zero flux, within abs 1e-20 W/m^2, which is at least 17 decades below active fluxes. With conduction off, convection gives 9e8 to 2e9 W/m^2 (observed; the test asserts more than 1e3). In a conduction-only run, conduction gives 1.9e-3 to 1.3e-2 W/m^2 (observed). Total flux equals conductive flux to rel 1e-12 at every interior node. The surface node differs by more than 1e3 W/m^2. +**What it tests.** The test evaluates the flux superposition architecture when specific transport terms are switched off. A disabled transport mechanism must hold zero flux, within abs 1e-20 W/m^2, which is at least 17 decades below the observed active fluxes. With conduction off, convection gives 9e8 to 2e9 W/m^2 (observed; the test asserts more than 1e3). In a conduction-only run, conduction gives 1.9e-3 to 1.3e-2 W/m^2 (observed). Total flux equals conductive flux to rel 1e-12 at every interior node. The surface node differs by more than 1e3 W/m^2. -**How we know the right answer.** Superposition of linear or disjoint flux mechanisms is an analytical identity. Inactive transport terms must vanish to rounding precision, while active terms reproduce isolated components. +**How we know the right answer.** Superposition of disjoint flux mechanisms is an analytical identity: an inactive transport term must vanish to rounding precision, and in the conduction-only run the total flux must equal the conductive flux. The magnitude of the convective flux is not compared with a reference. **Why it matters.** SPIDER models mantle cooling by summing the transport terms, so a leaking term changes every flux of the run. @@ -22,7 +22,7 @@ The sections below detail three validation cases, their tolerances, and their te ??? info "Test details" - Reference: Analytical limit: a disabled transport mechanism carries zero flux, and with only conduction enabled the total flux equals the conductive flux at every node except the surface node, which carries the grey-body boundary flux. - - Tolerance: abs 1e-20 W/m^2 on the disabled fluxes, at least 17 decades below the active fluxes (convection 9e8 to 2e9 W/m^2 with conduction off; conduction 1.9e-3 to 1.3e-2 W/m^2 in the conduction-only run); rel 1e-12 on the conduction-only identity, and the surface node differs by more than 1e3 W/m^2. + - Tolerance: abs 1e-20 W/m^2 on the disabled fluxes, at least 17 decades below the observed active fluxes (observed: convection 9e8 to 2e9 W/m^2 with conduction off, conduction 1.9e-3 to 1.3e-2 W/m^2 in the conduction-only run); rel 1e-12 on the conduction-only identity, and the surface node differs by more than 1e3 W/m^2. - Test id: `tests/test_energy.py::test_disabled_transport_mechanisms_carry_zero_flux` - Command: `make`, then `pytest -o addopts= tests/test_energy.py::test_disabled_transport_mechanisms_carry_zero_flux` - Companion test: `tests/test_energy.py::test_total_flux_assembles_from_transport_components` verifies that total flux equals the sum of transport components at interior basic nodes. @@ -56,7 +56,7 @@ The sections below detail three validation cases, their tolerances, and their te **Why it matters.** The steady-state initial condition starts a run from a profile in energy balance. -**Known limit.** The test uses its own copy of the Stefan-Boltzmann constant of the source, so a value that is wrong in both places does not fail it. +**Known limit.** The test uses its own copy of the Stefan-Boltzmann constant in `constants.c`, so a value that is wrong in both places does not fail it. ??? info "Test details" - Reference: Analytical limit: the steady-state initial condition equalises the energy flow through every basic node and closes the surface radiation balance sigma (T_surf^4 - teqm^4) with the Stefan-Boltzmann constant of constants.c. diff --git a/docs/Validation/eos.md b/docs/Validation/eos.md index 1b869bb8..b88bc4c8 100644 --- a/docs/Validation/eos.md +++ b/docs/Validation/eos.md @@ -37,7 +37,7 @@ The sections below detail two validation cases, their tolerances, and their test **Why it matters.** Compositional controls on mantle viscosity determine how mantle convective vigor responds to planetary bulk composition. -**Known limit.** The test re-encodes the coefficients as in `eos.c`, so a coefficient that is wrong in both copies is not caught; the 2 hand-derived literals catch a transcription error in either copy. +**Known limit.** The test re-encodes the coefficients as in `eos.c`. The 2 hand-derived literals catch an error that is in both copies at the 4 compositions they probe; a coefficient that affects neither literal is not caught. ??? info "Test details" - Reference: The piecewise Mg/Si log10 viscosity prefactor of Spaargaren et al. (2020). The test re-encodes the published coefficients independently of eos.c and additionally pins the two branch-pair shifts as hand-derived literals (0.2074 and -1.35803), so a transcription error in either copy of the formula surfaces. diff --git a/docs/Validation/eos_lookup.md b/docs/Validation/eos_lookup.md index 064430e0..5fa0c145 100644 --- a/docs/Validation/eos_lookup.md +++ b/docs/Validation/eos_lookup.md @@ -10,7 +10,7 @@ The section below details the check, its tolerance, and its test command. ## Melt density matches independent bilinear evaluation -**Why this check exists.** Table grid reads can suffer from indexing offsets, wrong corner weights, or swapped column scales. A bug in table reading shifts density values without triggering a runtime error. +**Why this check exists.** Table grid reads can suffer from indexing offsets or wrong corner weights. A bug in table reading shifts the density without an error message; the test checks the density column only, so a wrong scale on another column is not caught. **What it tests.** The test evaluates the shipped melt density table (1TPa-dK09-elec-free set: de Koker and Stixrude 2009 properties as assembled for Bower et al. 2018). It compares C outputs with an independent Python bilinear check at three off-node (P, S) probe points spanning 7.3 to 89 GPa. The test guards against reading the wrong table: solid-table density at the same probes differs by more than 1 percent. Positive values and the 2000 to 8000 kg/m3 silicate range bound the density scale. Sibling test `test_si_outputs_invariant_under_nondimensionalisation_choice` confirms consistent SI outputs under scaling choices. diff --git a/docs/Validation/ic.md b/docs/Validation/ic.md index 0a8eba0d..7c5eb64e 100644 --- a/docs/Validation/ic.md +++ b/docs/Validation/ic.md @@ -10,7 +10,7 @@ The sections below detail three validation cases, their tolerances, and their te ## Prescribed adiabat entropy sets the initial profile -**Why this check exists.** Initial condition (IC) routines can misinterpret input entropy parameters or fail to write values to the staggered mesh nodes; a gradient ramp error shows only if the profile mean leaves 2600 to 2628.7 J/kg/K. A bug in initial condition setup shifts the starting temperature structure, and the later cooling is wrong too. +**Why this check exists.** Initial condition (IC) routines can misinterpret input entropy parameters or fail to write values to the staggered mesh nodes. A bug in IC setup shifts the starting temperature structure, and the later cooling is wrong too. **What it tests.** The test evaluates the prescribed adiabat entropy (2600 J/kg/K at the top staggered node) with the configured gradient ramp. The profile mean lies between the adiabat entropy and that value plus |ic_dsdr| times mantle depth. A run with ic_adiabat_entropy = 2400 shifts the mean by 200 J/kg/K to rel 1e-9, guarding against ignored option inputs. Entropy is positive, with its mean between 1e3 and 1e4 J/kg/K. Top-node entropy matches at t = 0 to rel 1e-3 (observed exact). Sibling test `test_initial_adiabat_is_nearly_flat_until_cooling_imprints_structure` checks that subsequent cooling imprints thermal structure. @@ -18,7 +18,7 @@ The sections below detail three validation cases, their tolerances, and their te **Why it matters.** The initial adiabat sets the starting thermal state of the magma ocean. SPIDER integrates cooling forward from this baseline. -**Known limit.** The check tests initialization on the staggered grid, but does not benchmark external non-adiabatic initial profiles. +**Known limit.** A gradient ramp error shows only if the profile mean leaves 2600 to 2628.7 J/kg/K. The check does not cover non-adiabatic initial profiles. ??? info "Test details" - Reference: Analytical limit: the initial condition prescribes the adiabat entropy (2600 J/kg/K at the top staggered node) with the configured small gradient ramp. @@ -56,7 +56,7 @@ The sections below detail three validation cases, their tolerances, and their te **Why it matters.** Setting volatile budgets in Earth ocean equivalents allows direct comparison with terrestrial water and carbon inventories in planetary models. -**Known limit.** The test uses its own copy of the OCEAN_MOLES constant of the source, so it checks the conversion, not the definition of the terrestrial ocean mass; a value that is wrong in both places passes. +**Known limit.** The test uses its own copy of the OCEAN_MOLES constant in `constants.c`, so it checks the conversion, not the definition of the terrestrial ocean mass; a value that is wrong in both places passes. ??? info "Test details" - Reference: The OCEAN_MOLES constant of constants.c (7.68894973907177e22 mol per Earth ocean): initial_kg equals moles times OCEAN_MOLES times the volatile molar mass. diff --git a/docs/Validation/interp.md b/docs/Validation/interp.md index 221f934c..c4c2394b 100644 --- a/docs/Validation/interp.md +++ b/docs/Validation/interp.md @@ -16,7 +16,7 @@ The section below details the validation check, its tolerance, and its test comm **How we know the right answer.** Piecewise-linear interpolation reproduces affine data exactly by mathematical definition. Numerical queries match analytical values to rel 1e-14 across nodes, interior intervals, and endpoints. -**Why it matters.** The interpolation layer feeds the tabulated equation-of-state lookups in SPIDER (`eos_lookup.c`). +**Why it matters.** The interpolation layer feeds the phase boundary in `eos.c`, the tabulated lookups in `eos_lookup.c`, and tables in `bc.c`, `ic.c` and `parameters.c`. **Known limit.** The case covers 1-D interpolation with unit scalings; the 2-D bilinear case (`test_bilinear_plane_exact_everywhere_including_grid_corner`) and per-column scaling are not part of it. diff --git a/docs/Validation/matprop.md b/docs/Validation/matprop.md index 5d806812..c5a82bf3 100644 --- a/docs/Validation/matprop.md +++ b/docs/Validation/matprop.md @@ -10,7 +10,7 @@ The text below details the check, its tolerance, and its test command. ## Viscosity recovers configured end members -**Why this check exists.** Rheological blending functions can swap the melt and solid limits; the test samples only nearly pure nodes, so a wrong weighting inside the melting interval is not caught. A bug in property blending causes convection solvers to experience wrong viscosities. +**Why this check exists.** Rheological blending functions can swap the melt and solid limits. A bug in property blending causes the convection solver to use wrong viscosities. **What it tests.** The test evaluates the analytical limit where blended viscosity recovers configured end-members away from the rheological transition (melt_log10visc = 2, solid_log10visc = 21). It checks two extreme states: the hot initial state of the blackbody50 run (t = 0, where all 50 nodes are molten), and a cold 0-step run started from a 1600 J/kg/K adiabat (where at least 5 nodes are solid). The recovered end-members must differ by more than 10 decades, guarding against swapped phases. Sibling test `test_material_properties_positive_and_conductivity_constant` asserts positive values for density, heat capacity, conductivity, and expansivity, alongside configured 4.0 W/m/K conductivity. @@ -18,14 +18,14 @@ The text below details the check, its tolerance, and its test command. **Why it matters.** Mantle viscosity governs convective heat transport. PROTEUS reads the viscosity array `visc_b` from SPIDER. -**Known limit.** The check evaluates asymptotic recovery at extreme states, but does not benchmark the shape of the rheological front through the transition. +**Known limit.** The test samples only nearly pure nodes, so a wrong weighting inside the melting interval is not caught. It does not check the shape of the transition through the melting interval. ??? info "Test details" - Reference: Analytical limit: away from the rheological transition the blended viscosity recovers the configured end-members (melt_log10visc = 2, solid_log10visc = 21). - Tolerance: abs 0.01 on log10(viscosity) at the strongly molten nodes and abs 0.05 at the strongly solid nodes. - Test id: `tests/test_matprop.py::test_viscosity_recovers_configured_end_members` - Command: `make`, then `pytest -o addopts= tests/test_matprop.py::test_viscosity_recovers_configured_end_members` - - Companion test: `tests/test_matprop.py::test_material_properties_positive_and_conductivity_constant` asserts positive values for the properties and the viscosity, and a constant conductivity. + - Companion test: `tests/test_matprop.py::test_material_properties_positive_and_conductivity_constant` asserts positive values for density, heat capacity and thermal expansion, and a constant conductivity; the main test asserts a positive viscosity. ## Related code diff --git a/docs/Validation/mesh.md b/docs/Validation/mesh.md index 2f86a7dc..e6ed7a0c 100644 --- a/docs/Validation/mesh.md +++ b/docs/Validation/mesh.md @@ -10,7 +10,7 @@ The section below details the validation check, its tolerance, and its test comm ## Pressure profile matches Adams-Williamson closed form -**Why this check exists.** Radial grid generation can suffer from sign errors in the hydrostatic integral or wrong radial bounds; the test evaluates the expected pressure at the code's own radii, so a spacing error is not caught. An error in mesh setup makes the pressure and volume arrays of every physics module wrong. +**Why this check exists.** Radial grid generation can suffer from sign errors in the hydrostatic integral or a wrong surface radius. An error in mesh setup makes the pressure and volume arrays of every physics module wrong. **What it tests.** The test evaluates the native Adams-Williamson pressure profile P(r) = -(rhos g / beta)(exp(beta (R - r)) - 1) in closed form. It checks the basic-node pressures through the mantle. A sign-flipped beta and the linear small-beta approximation both fall far outside tolerance, confirming proper discrimination. Sibling test `test_mesh_orderings_interleave_and_shell_masses_close` checks geometric node interleaving and shell mass conservation. Sibling test `test_external_linear_density_mesh_replaces_the_aw_profile` confirms that external meshes replace native profiles cleanly. @@ -18,7 +18,7 @@ The section below details the validation check, its tolerance, and its test comm **Why it matters.** The mesh module builds the static spatial and pressure framework that every interior routine in SPIDER evaluates. PROTEUS reads radius arrays `radius_b` and pressure arrays `pressure_s` (in `ReadSPIDER` of `spider.py`). -**Known limit.** The native profile assumes an idealized Adams-Williamson compressibility model rather than self-consistent mineralogical structures. +**Known limit.** The test evaluates the expected pressure at the code's own radii, so a spacing error or a wrong core-mantle boundary radius is caught only by the loose bound on the maximum pressure. The profile assumes an idealised Adams-Williamson compressibility rather than a self-consistent mineral structure. ??? info "Test details" - Reference: Analytical limit: the Adams-Williamson pressure profile P(r) = -(rhos g / beta)(exp(beta (R - r)) - 1) in closed form. diff --git a/docs/Validation/reaction.md b/docs/Validation/reaction.md index 229c1c2c..4aba9f0f 100644 --- a/docs/Validation/reaction.md +++ b/docs/Validation/reaction.md @@ -10,7 +10,7 @@ The text below details the check, its tolerance, and its test command. ## Elements conserved across each reaction pair -**Why this check exists.** Reaction bookkeeping can violate elemental stoichiometry, misapply molar conversions or swap reactant signs; conservation holds for any equilibrium constant, so a wrong equilibrium constant is not caught. A bug in reaction bookkeeping alters total carbon or hydrogen mass during runs. +**Why this check exists.** Reaction bookkeeping can violate elemental stoichiometry, misapply molar conversions or swap reactant signs. A bug in reaction bookkeeping changes the total carbon or hydrogen mass during a run. **What it tests.** The test evaluates closed-system elemental budgets for H2O/H2 and CO2/CO reaction pairs of the Bower et al. (2021) three-ocean hydrogen configuration. It checks hydrogen and carbon transfer balances after one 1000-year step. The test guards reaction signs (H2O produced from H2, CO2 from CO in this step), and bounds H2O transfer between 1e16 and 1e20 kg, below 1 percent of H2O inventory. Sibling tests verify reservoir closure to rel 1e-10 (`test_reservoir_closure_for_every_volatile`), and inventory closure to rel 1e-5 (`test_reaction_transfer_balances_the_initial_inventory`). @@ -18,7 +18,7 @@ The text below details the check, its tolerance, and its test command. **Why it matters.** In a SPIDER run with volatiles, the reaction module sets the volatile chemistry of the magma ocean and its atmosphere. PROTEUS does not use the reaction part of SPIDER. -**Known limit.** The check tests conservation for carbon and hydrogen pairs, but volatile oxygen exchanges with the melt iron buffer. +**Known limit.** The check covers carbon and hydrogen; volatile oxygen exchanges with the iron buffer of the melt. From reading the code (not tested by a mutation), conservation holds for any equilibrium constant, so a wrong equilibrium constant is not caught. ??? info "Test details" - Reference: Analytical conservation: closed-system elemental budgets across the H2O/H2 and CO2/CO reaction pairs of the Bower et al. (2021) three-ocean hydrogen configuration. diff --git a/docs/Validation/rheologicalfront.md b/docs/Validation/rheologicalfront.md index ae52cf7c..9f4e957f 100644 --- a/docs/Validation/rheologicalfront.md +++ b/docs/Validation/rheologicalfront.md @@ -14,9 +14,9 @@ The section below details the validation check, its tolerance, and its test comm **What it tests.** The test evaluates analytical self-consistency: reported front index matches the phi_critical = 0.4 crossing of the melt-fraction profile, and depth equals surface radius minus front radius. It checks that the front depth is positive and bounded by mantle depth, with front pressure located inside the mantle range. Sibling tests pin the un-formed-front limit at t = 0, where the front reports the core-mantle boundary by convention (`test_unformed_front_reports_the_cmb_at_the_fully_molten_start`), and upward migration during bottom-up solidification (`test_front_rises_from_the_cmb_as_the_mantle_solidifies`). -**How we know the right answer.** The front location is recomputed independently from the discrete melt-fraction profile and geometry arrays. The depth identity matches to rel 1e-6, and the index matches to one node of the recomputed crossing. +**How we know the right answer.** The test recomputes the front location in Python, with the same rule, from the melt-fraction profile and the geometry arrays. The depth identity matches to rel 1e-6, and the index matches to one node of the recomputed crossing. -**Why it matters.** PROTEUS reads `Phi_global` from this front block (`rheological_front_phi` in `ReadSPIDER` of `spider.py`). +**Why it matters.** The front block marks where the mantle turns from mostly molten to mostly solid. PROTEUS reads neither its index nor its depth: it takes `Phi_global` from the same output block and `RF_depth` from the dynamic front. **Known limit.** The test recomputes the crossing with the same rule as `get_crossover_index` and allows one node of difference, so a one-node misplacement passes; the code takes the first node below `phi_critical` without interpolation. PROTEUS takes `RF_depth` from the dynamic front (`rheological_front_dynamic`, a crossing of the regime at 1.5), which this test does not cover. diff --git a/docs/Validation/twophase.md b/docs/Validation/twophase.md index b8ae8a2d..2401cee1 100644 --- a/docs/Validation/twophase.md +++ b/docs/Validation/twophase.md @@ -18,7 +18,7 @@ The text below details the check, its tolerance, and its test command. **Why it matters.** Global melt fraction and mantle phase masses are primary outputs read by PROTEUS (`M_mantle_liquid`, `M_mantle_solid`, and `Phi_global` in `ReadSPIDER` of `spider.py`). -**Known limit.** The identity is definitional and uses the code's own `mass_s` and `phi_s`, so a wrong shell mass or melt fraction passes. +**Known limit.** The identity compares 2 code paths that share the code's own `mass_s` and `phi_s`, so a wrong shell mass or melt fraction passes. ??? info "Test details" - Reference: Analytical identity: the global melt fraction equals Mliq/Mmantle and the mass-weighted mean of the staggered melt-fraction profile. From e0ef680ce5ec33fb6df3d9722e53b7f453308ebd Mon Sep 17 00:00:00 2001 From: timlichtenberg Date: Mon, 26 Oct 2026 02:05:03 +0100 Subject: [PATCH 04/15] State the remaining SPIDER test bounds exactly The mesh, reaction and material pages state the bounds that do catch a wrong core-mantle radius, equilibrium constant or blend, the IC limit names the entropy gradient option, and the rheological-front page defines the convection regime flag. --- docs/Validation/atmosphere.md | 2 +- docs/Validation/energy.md | 2 +- docs/Validation/ic.md | 2 +- docs/Validation/matprop.md | 4 ++-- docs/Validation/mesh.md | 4 ++-- docs/Validation/reaction.md | 2 +- docs/Validation/rheologicalfront.md | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/Validation/atmosphere.md b/docs/Validation/atmosphere.md index 56584d47..91f64c33 100644 --- a/docs/Validation/atmosphere.md +++ b/docs/Validation/atmosphere.md @@ -22,7 +22,7 @@ The section below details the validation check, its tolerance, and its test comm ??? info "Test details" - Reference: Analytical limit: the grey-body flux F = emissivity sigma (T_surf^4 - T_eqm^4) with emissivity 1 and T_eqm = 273 K (Stefan-Boltzmann). - - Tolerance: rel 1e-6 against the reported surface temperature, using the test's copy of the code value sigma = 5.670367e-8 W m-2 K-4. + - Tolerance: rel 1e-6 on the flux, computed from the reported surface temperature using the test's copy of the code value sigma = 5.670367e-8 W m-2 K-4. - Test id: `tests/test_atmosphere.py::test_grey_body_flux_matches_stefan_boltzmann_limit` - Command: `make`, then `pytest -o addopts= tests/test_atmosphere.py::test_grey_body_flux_matches_stefan_boltzmann_limit` - Companion test: `tests/test_atmosphere.py::test_surface_cools_and_flux_decays_across_macro_steps` confirms monotone decline of flux and surface temperature over outputs. diff --git a/docs/Validation/energy.md b/docs/Validation/energy.md index 57f4fc24..6d3abe1a 100644 --- a/docs/Validation/energy.md +++ b/docs/Validation/energy.md @@ -56,7 +56,7 @@ The sections below detail three validation cases, their tolerances, and their te **Why it matters.** The steady-state initial condition starts a run from a profile in energy balance. -**Known limit.** The test uses its own copy of the Stefan-Boltzmann constant in `constants.c`, so a value that is wrong in both places does not fail it. +**Known limit.** The test uses its own copy of the Stefan-Boltzmann constant in `constants.c`, so a value that is wrong in both places does not fail it; a change in the code larger than the tolerance does. ??? info "Test details" - Reference: Analytical limit: the steady-state initial condition equalises the energy flow through every basic node and closes the surface radiation balance sigma (T_surf^4 - teqm^4) with the Stefan-Boltzmann constant of constants.c. diff --git a/docs/Validation/ic.md b/docs/Validation/ic.md index 7c5eb64e..c7950a5d 100644 --- a/docs/Validation/ic.md +++ b/docs/Validation/ic.md @@ -18,7 +18,7 @@ The sections below detail three validation cases, their tolerances, and their te **Why it matters.** The initial adiabat sets the starting thermal state of the magma ocean. SPIDER integrates cooling forward from this baseline. -**Known limit.** A gradient ramp error shows only if the profile mean leaves 2600 to 2628.7 J/kg/K. The check does not cover non-adiabatic initial profiles. +**Known limit.** An error in the entropy gradient of the initial profile (`ic_dsdr`) is caught only if it moves the profile mean outside 2600 to 2628.7 J/kg/K. The check does not cover non-adiabatic initial profiles. ??? info "Test details" - Reference: Analytical limit: the initial condition prescribes the adiabat entropy (2600 J/kg/K at the top staggered node) with the configured small gradient ramp. diff --git a/docs/Validation/matprop.md b/docs/Validation/matprop.md index c5a82bf3..64c67162 100644 --- a/docs/Validation/matprop.md +++ b/docs/Validation/matprop.md @@ -12,13 +12,13 @@ The text below details the check, its tolerance, and its test command. **Why this check exists.** Rheological blending functions can swap the melt and solid limits. A bug in property blending causes the convection solver to use wrong viscosities. -**What it tests.** The test evaluates the analytical limit where blended viscosity recovers configured end-members away from the rheological transition (melt_log10visc = 2, solid_log10visc = 21). It checks two extreme states: the hot initial state of the blackbody50 run (t = 0, where all 50 nodes are molten), and a cold 0-step run started from a 1600 J/kg/K adiabat (where at least 5 nodes are solid). The recovered end-members must differ by more than 10 decades, guarding against swapped phases. Sibling test `test_material_properties_positive_and_conductivity_constant` asserts positive values for density, heat capacity, conductivity, and expansivity, alongside configured 4.0 W/m/K conductivity. +**What it tests.** The test evaluates the analytical limit where blended viscosity recovers configured end-members away from the rheological transition (melt_log10visc = 2, solid_log10visc = 21). It checks two extreme states: the hot initial state of the blackbody50 run (t = 0, where all 50 nodes are molten), and a cold 0-step run started from a 1600 J/kg/K adiabat (where at least 5 nodes are solid). The recovered end-members must differ by more than 10 decades, guarding against swapped phases. Sibling test `test_material_properties_positive_and_conductivity_constant` asserts positive values for density, heat capacity and thermal expansion, and a constant conductivity, alongside configured 4.0 W/m/K conductivity. **How we know the right answer.** Away from the partial-melting interval, blended material properties must approach pure-phase limits asymptotically. Viscosity matches configured values to abs 0.01 on log10(viscosity) at molten nodes and abs 0.05 at solid nodes. **Why it matters.** Mantle viscosity governs convective heat transport. PROTEUS reads the viscosity array `visc_b` from SPIDER. -**Known limit.** The test samples only nearly pure nodes, so a wrong weighting inside the melting interval is not caught. It does not check the shape of the transition through the melting interval. +**Known limit.** The main test samples only nearly pure nodes, so a wrong weighting inside the melting interval is not caught there. The companion test `test_viscosity_rises_monotonically_across_rheological_transition` checks that the viscosity rises steadily across the transition, by more than 3 decades, but not its shape. ??? info "Test details" - Reference: Analytical limit: away from the rheological transition the blended viscosity recovers the configured end-members (melt_log10visc = 2, solid_log10visc = 21). diff --git a/docs/Validation/mesh.md b/docs/Validation/mesh.md index e6ed7a0c..00735e4f 100644 --- a/docs/Validation/mesh.md +++ b/docs/Validation/mesh.md @@ -18,11 +18,11 @@ The section below details the validation check, its tolerance, and its test comm **Why it matters.** The mesh module builds the static spatial and pressure framework that every interior routine in SPIDER evaluates. PROTEUS reads radius arrays `radius_b` and pressure arrays `pressure_s` (in `ReadSPIDER` of `spider.py`). -**Known limit.** The test evaluates the expected pressure at the code's own radii, so a spacing error or a wrong core-mantle boundary radius is caught only by the loose bound on the maximum pressure. The profile assumes an idealised Adams-Williamson compressibility rather than a self-consistent mineral structure. +**Known limit.** The test evaluates the expected pressure at the code's own radii, so a spacing error is not caught. A wrong core-mantle boundary radius is caught only if it moves the maximum pressure out of 5e10 to 5e11 Pa. The profile assumes an idealised Adams-Williamson compressibility rather than a self-consistent mineral structure. ??? info "Test details" - Reference: Analytical limit: the Adams-Williamson pressure profile P(r) = -(rhos g / beta)(exp(beta (R - r)) - 1) in closed form. - - Tolerance: rel 1e-12 on the basic-node pressures (observed 6e-15; mesh.c evaluates the same closed form, so the agreement is limited by the round trip through the JSON output). + - Tolerance: rel 1e-12 on the basic-node pressures (observed 6e-15; `mesh.c` evaluates the same closed form through `eos_adamswilliamson.c`, so the agreement is limited by the round trip through the JSON output). - Test id: `tests/test_mesh.py::test_pressure_profile_matches_adams_williamson_closed_form` - Command: `make`, then `pytest -o addopts= tests/test_mesh.py::test_pressure_profile_matches_adams_williamson_closed_form` - Companion test: `tests/test_mesh.py::test_mesh_orderings_interleave_and_shell_masses_close` verifies node interleaving and shell mass conservation. diff --git a/docs/Validation/reaction.md b/docs/Validation/reaction.md index 4aba9f0f..f1bb3d06 100644 --- a/docs/Validation/reaction.md +++ b/docs/Validation/reaction.md @@ -18,7 +18,7 @@ The text below details the check, its tolerance, and its test command. **Why it matters.** In a SPIDER run with volatiles, the reaction module sets the volatile chemistry of the magma ocean and its atmosphere. PROTEUS does not use the reaction part of SPIDER. -**Known limit.** The check covers carbon and hydrogen; volatile oxygen exchanges with the iron buffer of the melt. From reading the code (not tested by a mutation), conservation holds for any equilibrium constant, so a wrong equilibrium constant is not caught. +**Known limit.** The check covers carbon and hydrogen; volatile oxygen exchanges with the iron buffer of the melt. The test bounds the direction and the size of each transfer, so a wrong equilibrium constant fails only if it reverses a reaction or moves more than 1 % of the water inventory. ??? info "Test details" - Reference: Analytical conservation: closed-system elemental budgets across the H2O/H2 and CO2/CO reaction pairs of the Bower et al. (2021) three-ocean hydrogen configuration. diff --git a/docs/Validation/rheologicalfront.md b/docs/Validation/rheologicalfront.md index 9f4e957f..dd34dbec 100644 --- a/docs/Validation/rheologicalfront.md +++ b/docs/Validation/rheologicalfront.md @@ -18,7 +18,7 @@ The section below details the validation check, its tolerance, and its test comm **Why it matters.** The front block marks where the mantle turns from mostly molten to mostly solid. PROTEUS reads neither its index nor its depth: it takes `Phi_global` from the same output block and `RF_depth` from the dynamic front. -**Known limit.** The test recomputes the crossing with the same rule as `get_crossover_index` and allows one node of difference, so a one-node misplacement passes; the code takes the first node below `phi_critical` without interpolation. PROTEUS takes `RF_depth` from the dynamic front (`rheological_front_dynamic`, a crossing of the regime at 1.5), which this test does not cover. +**Known limit.** The test recomputes the crossing with the same rule as `get_crossover_index` and allows one node of difference, so a one-node misplacement passes; the code takes the first node below `phi_critical` without interpolation. PROTEUS takes `RF_depth` from the dynamic front (`rheological_front_dynamic`, the crossing of the convection regime flag at 1.5, between inviscid (2) and viscous (1) convection), which this test does not cover. ??? info "Test details" - Reference: Analytical self-consistency: the reported front index matches the phi_critical = 0.4 crossing of the melt-fraction profile, and the depth equals the surface radius minus the front radius. From 875dd3822176870211f62bad73308ad6091fc4c1 Mon Sep 17 00:00:00 2001 From: timlichtenberg Date: Mon, 26 Oct 2026 17:07:53 +0100 Subject: [PATCH 05/15] Plot the atmosphere, boundary and energy validation cases --- docs/Validation/atmosphere.md | 7 +- docs/Validation/bc.md | 14 +- docs/Validation/energy.md | 21 +- .../validation/atmosphere_grey_body_flux.svg | 1878 ++++++++++++++++ .../atmosphere_grey_body_flux_dark.svg | 1878 ++++++++++++++++ .../bc_constant_entropy_surface.svg | 1628 ++++++++++++++ .../bc_constant_entropy_surface_dark.svg | 1628 ++++++++++++++ docs/assets/validation/bc_prescribed_flux.svg | 1666 +++++++++++++++ .../validation/bc_prescribed_flux_dark.svg | 1666 +++++++++++++++ .../validation/energy_disabled_transport.svg | 1613 ++++++++++++++ .../energy_disabled_transport_dark.svg | 1613 ++++++++++++++ .../validation/energy_radiogenic_decay.svg | 1809 ++++++++++++++++ .../energy_radiogenic_decay_dark.svg | 1809 ++++++++++++++++ .../validation/energy_steady_state_ic.svg | 1898 +++++++++++++++++ .../energy_steady_state_ic_dark.svg | 1898 +++++++++++++++++ tests/test_atmosphere.py | 24 +- tests/test_bc.py | 61 +- tests/test_energy.py | 79 +- 18 files changed, 21174 insertions(+), 16 deletions(-) create mode 100644 docs/assets/validation/atmosphere_grey_body_flux.svg create mode 100644 docs/assets/validation/atmosphere_grey_body_flux_dark.svg create mode 100644 docs/assets/validation/bc_constant_entropy_surface.svg create mode 100644 docs/assets/validation/bc_constant_entropy_surface_dark.svg create mode 100644 docs/assets/validation/bc_prescribed_flux.svg create mode 100644 docs/assets/validation/bc_prescribed_flux_dark.svg create mode 100644 docs/assets/validation/energy_disabled_transport.svg create mode 100644 docs/assets/validation/energy_disabled_transport_dark.svg create mode 100644 docs/assets/validation/energy_radiogenic_decay.svg create mode 100644 docs/assets/validation/energy_radiogenic_decay_dark.svg create mode 100644 docs/assets/validation/energy_steady_state_ic.svg create mode 100644 docs/assets/validation/energy_steady_state_ic_dark.svg diff --git a/docs/Validation/atmosphere.md b/docs/Validation/atmosphere.md index 91f64c33..1d2a3553 100644 --- a/docs/Validation/atmosphere.md +++ b/docs/Validation/atmosphere.md @@ -20,11 +20,16 @@ The section below details the validation check, its tolerance, and its test comm **Known limit.** The test uses its own copy of the value in `constants.c`, sigma = 5.670367e-8 W m-2 K-4. A change of the constant in the code larger than the tolerance fails the test, but a value that is wrong in both places does not. PROTEUS uses this grey-body branch only with `surface_bc_mode = 'grey_body'`; its default is a prescribed flux (`SURFACE_BC 4`). +![Surface flux of the blackbody50 run at 200 yr on the grey-body law](../assets/validation/atmosphere_grey_body_flux.svg#only-light){ width="80%" } +![Surface flux of the blackbody50 run at 200 yr on the grey-body law](../assets/validation/atmosphere_grey_body_flux_dark.svg#only-dark){ width="80%" } + +**How to read the plot.** The x-axis is the surface temperature and the y-axis the surface flux on a log scale. The line is the grey-body law $\epsilon\sigma(T^4 - T_\mathrm{eqm}^4)$ with emissivity 1 and $T_\mathrm{eqm}$ = 273 K, and the dashed line the $T^3$ form that the test rejects (context). The circle is the asserted Fatm of the blackbody50 run at 200 yr, at its reported surface temperature. A pass means that the circle lies on the line (relative 1e-6). + ??? info "Test details" - Reference: Analytical limit: the grey-body flux F = emissivity sigma (T_surf^4 - T_eqm^4) with emissivity 1 and T_eqm = 273 K (Stefan-Boltzmann). - Tolerance: rel 1e-6 on the flux, computed from the reported surface temperature using the test's copy of the code value sigma = 5.670367e-8 W m-2 K-4. - Test id: `tests/test_atmosphere.py::test_grey_body_flux_matches_stefan_boltzmann_limit` - - Command: `make`, then `pytest -o addopts= tests/test_atmosphere.py::test_grey_body_flux_matches_stefan_boltzmann_limit` + - Command: `make`, then `pytest -o addopts= tests/test_atmosphere.py::test_grey_body_flux_matches_stefan_boltzmann_limit` (add `--validation-plots` to write the plot pair) - Companion test: `tests/test_atmosphere.py::test_surface_cools_and_flux_decays_across_macro_steps` confirms monotone decline of flux and surface temperature over outputs. ## Related code diff --git a/docs/Validation/bc.md b/docs/Validation/bc.md index 8cae80ee..3e2cf93c 100644 --- a/docs/Validation/bc.md +++ b/docs/Validation/bc.md @@ -20,11 +20,16 @@ The sections below detail two validation cases, their tolerances, and their test **Known limit.** The test reads only the reported `Fatm` and emissivity; it does not check that the interior total flux at the surface (`Jtot` at node 0, set in `bc.c`) uses the prescribed value. `SURFACE_BC` is read in `atmosphere.c`, `bc.c` and `energy.c`. +![Reported surface flux of the prescribed-flux run against the grey-body law with emissivity 1 and with the reported emissivity](../assets/validation/bc_prescribed_flux.svg#only-light){ width="80%" } +![Reported surface flux of the prescribed-flux run against the grey-body law with emissivity 1 and with the reported emissivity](../assets/validation/bc_prescribed_flux_dark.svg#only-dark){ width="80%" } + +**How to read the plot.** The x-axis is the surface temperature and the y-axis the surface flux on a log scale. The dotted line is the prescribed flux, 1e5 W/m$^2$, and the circle the asserted Fatm at the reported surface temperature. The solid line is the grey-body law with emissivity 1, which the test requires to be more than 10 % away at that temperature. The dashed line is the grey-body law with the asserted inverted emissivity (about 0.12). A pass means that the circle sits on the dotted line and on the dashed line, far below the solid one. + ??? info "Test details" - Reference: Analytical identity: with SURFACE_BC 4 the surface flux equals the prescribed value, and the reported emissivity is its grey-body inversion. - Tolerance: rel 1e-9 on the flux echo; rel 1e-6 on the emissivity inversion. - Test id: `tests/test_bc.py::test_prescribed_flux_bc_echoes_value_and_inverts_emissivity` - - Command: `make`, then `pytest -o addopts= tests/test_bc.py::test_prescribed_flux_bc_echoes_value_and_inverts_emissivity` + - Command: `make`, then `pytest -o addopts= tests/test_bc.py::test_prescribed_flux_bc_echoes_value_and_inverts_emissivity` (add `--validation-plots` to write the plot pair) - Companion test: `tests/test_bc.py::test_mantle_cools_from_the_top_between_surface_and_cmb` verifies that the core-mantle boundary remains hotter than the surface. ## Constant-entropy BC pins the initial surface @@ -39,11 +44,16 @@ The sections below detail two validation cases, their tolerances, and their test **Known limit.** For constant-entropy boundary conditions, the surface value is assigned directly rather than determined by a physical flux balance. +![Initial entropy profiles with the constant-entropy boundary and with the default boundary](../assets/validation/bc_constant_entropy_surface.svg#only-light){ width="80%" } +![Initial entropy profiles with the constant-entropy boundary and with the default boundary](../assets/validation/bc_constant_entropy_surface_dark.svg#only-dark){ width="80%" } + +**How to read the plot.** The x-axis is the radius and the y-axis the entropy at the basic nodes at $t$ = 0. The solid line is the asserted profile of the constant-entropy run and the dashed line the default run (context, its surface value is the 2599.7 that the test requires to be more than 40 away). The circle is the asserted surface node and the hollow square the option value 2550 J kg$^{-1}$ K$^{-1}$. A pass means that the circle sits in the square and the deep end of the solid line lies between 2400 and 2700. + ??? info "Test details" - Reference: Analytical identity: the constant-entropy boundary writes ic_surface_entropy (2550 J/kg/K) onto the surface basic node, preserved through the steady-state energy solve into the initial output. - Tolerance: rel 1e-10 (assigned, not solved); the core-side entropy between 2400 and 2700 J/kg/K. - Test id: `tests/test_bc.py::test_constant_entropy_bc_pins_the_initial_surface` - - Command: `make`, then `pytest -o addopts= tests/test_bc.py::test_constant_entropy_bc_pins_the_initial_surface` + - Command: `make`, then `pytest -o addopts= tests/test_bc.py::test_constant_entropy_bc_pins_the_initial_surface` (add `--validation-plots` to write the plot pair) - Companion test: `tests/test_bc.py::test_prescribed_core_flux_slows_cmb_cooling` verifies that core heat flux slows cooling at the core-mantle boundary. ## Related code diff --git a/docs/Validation/energy.md b/docs/Validation/energy.md index 6d3abe1a..c77ea480 100644 --- a/docs/Validation/energy.md +++ b/docs/Validation/energy.md @@ -20,11 +20,16 @@ The sections below detail three validation cases, their tolerances, and their te **Known limit.** For the conduction-only test, the identity holds at interior nodes only, because the surface node transmits the grey-body boundary flux. +![Flux profiles of the conduction-off and the conduction-only runs on a symmetric log axis](../assets/validation/energy_disabled_transport.svg#only-light){ width="80%" } +![Flux profiles of the conduction-off and the conduction-only runs on a symmetric log axis](../assets/validation/energy_disabled_transport_dark.svg#only-dark){ width="80%" } + +**How to read the plot.** The x-axis is the radius and the y-axis the flux at the basic nodes on a symmetric log axis (linear between $-10^{-3}$ and $10^{-3}$). In the run with conduction off, the solid line is the active convective flux and the dashed line the asserted conductive flux, 0. In the run with conduction only, the dash-dotted line is the conductive flux and the dotted line the largest of the asserted convective, mixing and separation fluxes at each node, 0. A pass means that the dashed and dotted lines lie at 0 (within 1e-20 W/m$^2$) while the other 2 stay far above. + ??? info "Test details" - Reference: Analytical limit: a disabled transport mechanism carries zero flux, and with only conduction enabled the total flux equals the conductive flux at every node except the surface node, which carries the grey-body boundary flux. - Tolerance: abs 1e-20 W/m^2 on the disabled fluxes, at least 17 decades below the observed active fluxes (observed: convection 9e8 to 2e9 W/m^2 with conduction off, conduction 1.9e-3 to 1.3e-2 W/m^2 in the conduction-only run); rel 1e-12 on the conduction-only identity, and the surface node differs by more than 1e3 W/m^2. - Test id: `tests/test_energy.py::test_disabled_transport_mechanisms_carry_zero_flux` - - Command: `make`, then `pytest -o addopts= tests/test_energy.py::test_disabled_transport_mechanisms_carry_zero_flux` + - Command: `make`, then `pytest -o addopts= tests/test_energy.py::test_disabled_transport_mechanisms_carry_zero_flux` (add `--validation-plots` to write the plot pair) - Companion test: `tests/test_energy.py::test_total_flux_assembles_from_transport_components` verifies that total flux equals the sum of transport components at interior basic nodes. ## Radiogenic heating pins two isotope decay @@ -39,11 +44,16 @@ The sections below detail three validation cases, their tolerances, and their te **Known limit.** This test pins decay mechanics for synthetic isotopes, but does not benchmark geological isotope concentrations. +![Radiogenic heating of the 2-isotope run against time](../assets/validation/energy_radiogenic_decay.svg#only-light){ width="80%" } +![Radiogenic heating of the 2-isotope run against time](../assets/validation/energy_radiogenic_decay_dark.svg#only-dark){ width="80%" } + +**How to read the plot.** The x-axis is the time and the y-axis the specific radiogenic heating on a log scale. The solid line is the sum of the 2 isotopes in closed form, and the dashed and dotted lines are al26 and k40 alone (context). The circles are the asserted Hradio of the run at 0 and 200 yr. A pass means that both circles lie on the solid line: the sum at $t$ = 0 within relative 1e-6, and the ratio of the 2 within relative 1e-4 of 0.2506. + ??? info "Test details" - Reference: Analytical limit: radiogenic heating is the sum over isotopes of concentration times abundance times heat production, decaying as 2^(-(t - t0)/half_life). A two-isotope configuration with 100-year and 200-year half-lives pins the sum at t = 0 and the weighted decay factor 0.2506 after 200 years. - Tolerance: rel 1e-6 on the initial sum, rel 1e-4 on the decay factor. - Test id: `tests/test_energy.py::test_radiogenic_heating_pins_two_isotope_decay` - - Command: `make`, then `pytest -o addopts= tests/test_energy.py::test_radiogenic_heating_pins_two_isotope_decay` + - Command: `make`, then `pytest -o addopts= tests/test_energy.py::test_radiogenic_heating_pins_two_isotope_decay` (add `--validation-plots` to write the plot pair) - Companion test: `tests/test_energy.py::test_prescribed_tidal_heating_is_uniform_and_steady` verifies uniform spatial distribution of tidal heating. ## Steady-state IC balances interior and surface @@ -58,11 +68,16 @@ The sections below detail three validation cases, their tolerances, and their te **Known limit.** The test uses its own copy of the Stefan-Boltzmann constant in `constants.c`, so a value that is wrong in both places does not fail it; a change in the code larger than the tolerance does. +![Energy flow against radius and surface flux on the grey-body law for the steady-state initial condition](../assets/validation/energy_steady_state_ic.svg#only-light){ width="80%" } +![Energy flow against radius and surface flux on the grey-body law for the steady-state initial condition](../assets/validation/energy_steady_state_ic_dark.svg#only-dark){ width="80%" } + +**How to read the plot.** Left: the asserted energy flow Etot at every basic node of the steady-state initial condition against radius; a pass means a flat line (relative spread below 1e-8). Right: the grey-body law $\sigma(T^4 - 273^4)$ (line) and the $T^3$ form that the test rejects (dashed) against surface temperature, with the asserted Fatm at the reported surface temperature (circle); a pass means that the circle lies on the line (relative 1e-5). + ??? info "Test details" - Reference: Analytical limit: the steady-state initial condition equalises the energy flow through every basic node and closes the surface radiation balance sigma (T_surf^4 - teqm^4) with the Stefan-Boltzmann constant of constants.c. - Tolerance: Relative energy-flow spread below 1e-8 (observed 2e-11); grey-body pin at rel 1e-5. - Test id: `tests/test_energy.py::test_steady_state_ic_balances_interior_and_surface` - - Command: `make`, then `pytest -o addopts= tests/test_energy.py::test_steady_state_ic_balances_interior_and_surface` + - Command: `make`, then `pytest -o addopts= tests/test_energy.py::test_steady_state_ic_balances_interior_and_surface` (add `--validation-plots` to write the plot pair) - Companion test: `tests/test_energy.py::test_surface_node_flux_is_the_grey_body_atmospheric_flux` verifies that surface node flux equals the atmospheric grey-body value. ## Related code diff --git a/docs/assets/validation/atmosphere_grey_body_flux.svg b/docs/assets/validation/atmosphere_grey_body_flux.svg new file mode 100644 index 00000000..0847bbb7 --- /dev/null +++ b/docs/assets/validation/atmosphere_grey_body_flux.svg @@ -0,0 +1,1878 @@ + + + + + + + + test: tests/test_atmosphere.py::test_grey_body_flux_matches_stefan_boltzmann_limit; command: pytest tests/test_atmosphere.py::test_grey_body_flux_matches_stefan_boltzmann_limit --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "log"], "categories": [null, null], "lines": [[41, 1000.0, 3000.0, 2000.0000000000002, 350.0000000000001, 4.751192123886938, 6.662066404532349, 5.87445583474423, 0.32232199735896977], [41, 1000.0, 3000.0, 2000.0000000000002, 350.0000000000001, 1.74468369961667, 3.184647537460785, 2.5925113173524847, 0.24261720300383938], [1, 1856.9326339109864, 1856.9326339109864, 1856.9326339109864, 0.0, 5.828592830319474, 5.828592830319474, 5.828592830319474, 0.0]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} + image/svg+xml + + + proteus-mpl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/validation/atmosphere_grey_body_flux_dark.svg b/docs/assets/validation/atmosphere_grey_body_flux_dark.svg new file mode 100644 index 00000000..bb33051c --- /dev/null +++ b/docs/assets/validation/atmosphere_grey_body_flux_dark.svg @@ -0,0 +1,1878 @@ + + + + + + + + test: tests/test_atmosphere.py::test_grey_body_flux_matches_stefan_boltzmann_limit; command: pytest tests/test_atmosphere.py::test_grey_body_flux_matches_stefan_boltzmann_limit --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "log"], "categories": [null, null], "lines": [[41, 1000.0, 3000.0, 2000.0000000000002, 350.0000000000001, 4.751192123886938, 6.662066404532349, 5.87445583474423, 0.32232199735896977], [41, 1000.0, 3000.0, 2000.0000000000002, 350.0000000000001, 1.74468369961667, 3.184647537460785, 2.5925113173524847, 0.24261720300383938], [1, 1856.9326339109864, 1856.9326339109864, 1856.9326339109864, 0.0, 5.828592830319474, 5.828592830319474, 5.828592830319474, 0.0]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} + image/svg+xml + + + proteus-mpl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/validation/bc_constant_entropy_surface.svg b/docs/assets/validation/bc_constant_entropy_surface.svg new file mode 100644 index 00000000..027c89f7 --- /dev/null +++ b/docs/assets/validation/bc_constant_entropy_surface.svg @@ -0,0 +1,1628 @@ + + + + + + + + test: tests/test_bc.py::test_constant_entropy_bc_pins_the_initial_surface; command: pytest tests/test_bc.py::test_constant_entropy_bc_pins_the_initial_surface --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[50, 3504.049999999999, 6371.000000000025, 4866.723471494237, -498.5081967134211, 2550.0, 2600.5237791118197, 2599.1344454301648, 1.0534501058374135], [50, 3504.049999999999, 6371.000000000025, 4866.723471494237, -498.5081967134211, 2599.6657540951796, 2628.335074489891, 2614.708476203073, 4.985054105351737], [1, 6371.000000000025, 6371.000000000025, 6371.000000000025, 0.0, 2550.0, 2550.0, 2550.0, 0.0], [1, 6371.000000000025, 6371.000000000025, 6371.000000000025, 0.0, 2550.0, 2550.0, 2550.0, 0.0]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} + image/svg+xml + + + proteus-mpl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/validation/bc_constant_entropy_surface_dark.svg b/docs/assets/validation/bc_constant_entropy_surface_dark.svg new file mode 100644 index 00000000..4a443a7d --- /dev/null +++ b/docs/assets/validation/bc_constant_entropy_surface_dark.svg @@ -0,0 +1,1628 @@ + + + + + + + + test: tests/test_bc.py::test_constant_entropy_bc_pins_the_initial_surface; command: pytest tests/test_bc.py::test_constant_entropy_bc_pins_the_initial_surface --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[50, 3504.049999999999, 6371.000000000025, 4866.723471494237, -498.5081967134211, 2550.0, 2600.5237791118197, 2599.1344454301648, 1.0534501058374135], [50, 3504.049999999999, 6371.000000000025, 4866.723471494237, -498.5081967134211, 2599.6657540951796, 2628.335074489891, 2614.708476203073, 4.985054105351737], [1, 6371.000000000025, 6371.000000000025, 6371.000000000025, 0.0, 2550.0, 2550.0, 2550.0, 0.0], [1, 6371.000000000025, 6371.000000000025, 6371.000000000025, 0.0, 2550.0, 2550.0, 2550.0, 0.0]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} + image/svg+xml + + + proteus-mpl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/validation/bc_prescribed_flux.svg b/docs/assets/validation/bc_prescribed_flux.svg new file mode 100644 index 00000000..c1816cfb --- /dev/null +++ b/docs/assets/validation/bc_prescribed_flux.svg @@ -0,0 +1,1666 @@ + + + + + + + + test: tests/test_bc.py::test_prescribed_flux_bc_echoes_value_and_inverts_emissivity; command: pytest tests/test_bc.py::test_prescribed_flux_bc_echoes_value_and_inverts_emissivity --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "log"], "categories": [null, null], "lines": [[41, 1000.0, 3000.0, 2000.0000000000002, 350.0000000000001, 4.751192123886938, 6.662066404532349, 5.87445583474423, 0.32232199735896977], [41, 1000.0, 3000.0, 2000.0000000000002, 350.0000000000001, 3.8250806660866896, 5.735954946732101, 4.948344376943983, 0.3223219973589699], [2, 0.0, 1.0, 0.5, 0.5, 5.0, 5.0, 5.0, 0.0], [1, 1964.1090825259737, 1964.1090825259737, 1964.1090825259737, 0.0, 5.0, 5.0, 5.0, 0.0]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} + image/svg+xml + + + proteus-mpl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/validation/bc_prescribed_flux_dark.svg b/docs/assets/validation/bc_prescribed_flux_dark.svg new file mode 100644 index 00000000..02a07836 --- /dev/null +++ b/docs/assets/validation/bc_prescribed_flux_dark.svg @@ -0,0 +1,1666 @@ + + + + + + + + test: tests/test_bc.py::test_prescribed_flux_bc_echoes_value_and_inverts_emissivity; command: pytest tests/test_bc.py::test_prescribed_flux_bc_echoes_value_and_inverts_emissivity --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "log"], "categories": [null, null], "lines": [[41, 1000.0, 3000.0, 2000.0000000000002, 350.0000000000001, 4.751192123886938, 6.662066404532349, 5.87445583474423, 0.32232199735896977], [41, 1000.0, 3000.0, 2000.0000000000002, 350.0000000000001, 3.8250806660866896, 5.735954946732101, 4.948344376943983, 0.3223219973589699], [2, 0.0, 1.0, 0.5, 0.5, 5.0, 5.0, 5.0, 0.0], [1, 1964.1090825259737, 1964.1090825259737, 1964.1090825259737, 0.0, 5.0, 5.0, 5.0, 0.0]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} + image/svg+xml + + + proteus-mpl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/validation/energy_disabled_transport.svg b/docs/assets/validation/energy_disabled_transport.svg new file mode 100644 index 00000000..e23903c0 --- /dev/null +++ b/docs/assets/validation/energy_disabled_transport.svg @@ -0,0 +1,1613 @@ + + + + + + + + test: tests/test_energy.py::test_disabled_transport_mechanisms_carry_zero_flux; command: pytest tests/test_energy.py::test_disabled_transport_mechanisms_carry_zero_flux --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "symlog"], "categories": [null, null], "lines": [[50, 3504.049999999999, 6371.000000000025, 4866.723471494237, -498.5081967134211, 899083099.5253087, 2034826212.643107, 1615442839.586597, 166101675.0076686], [50, 3504.049999999999, 6371.000000000025, 4866.723471494237, -498.5081967134211, 0.0, 0.0, 0.0, 0.0], [50, 3504.049999999999, 6371.000000000025, 4866.723471494237, -498.5081967134211, 0.0018859082962793844, 0.012501514909557785, 0.002936927209303042, -0.0007039860647283159], [50, 3504.049999999999, 6371.000000000025, 4866.723471494237, -498.5081967134211, 0.0, 0.0, 0.0, 0.0]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} + image/svg+xml + + + proteus-mpl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/validation/energy_disabled_transport_dark.svg b/docs/assets/validation/energy_disabled_transport_dark.svg new file mode 100644 index 00000000..abb36090 --- /dev/null +++ b/docs/assets/validation/energy_disabled_transport_dark.svg @@ -0,0 +1,1613 @@ + + + + + + + + test: tests/test_energy.py::test_disabled_transport_mechanisms_carry_zero_flux; command: pytest tests/test_energy.py::test_disabled_transport_mechanisms_carry_zero_flux --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "symlog"], "categories": [null, null], "lines": [[50, 3504.049999999999, 6371.000000000025, 4866.723471494237, -498.5081967134211, 899083099.5253087, 2034826212.643107, 1615442839.586597, 166101675.0076686], [50, 3504.049999999999, 6371.000000000025, 4866.723471494237, -498.5081967134211, 0.0, 0.0, 0.0, 0.0], [50, 3504.049999999999, 6371.000000000025, 4866.723471494237, -498.5081967134211, 0.0018859082962793844, 0.012501514909557785, 0.002936927209303042, -0.0007039860647283159], [50, 3504.049999999999, 6371.000000000025, 4866.723471494237, -498.5081967134211, 0.0, 0.0, 0.0, 0.0]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} + image/svg+xml + + + proteus-mpl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/validation/energy_radiogenic_decay.svg b/docs/assets/validation/energy_radiogenic_decay.svg new file mode 100644 index 00000000..25d62e48 --- /dev/null +++ b/docs/assets/validation/energy_radiogenic_decay.svg @@ -0,0 +1,1809 @@ + + + + + + + + test: tests/test_energy.py::test_radiogenic_heating_pins_two_isotope_decay; command: pytest tests/test_energy.py::test_radiogenic_heating_pins_two_isotope_decay --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "log"], "categories": [null, null], "lines": [[61, 0.0, 300.0, 149.99999999999997, 51.666666666666664, -6.347659722553264, -5.446510199960762, -5.897246696803468, -0.15520348240208787], [61, 0.0, 300.0, 149.99999999999997, 51.666666666666664, -6.350665141287858, -5.447575154295914, -5.899120147791887, -0.1555321644263902], [61, 0.0, 300.0, 149.99999999999997, 51.666666666666664, -8.509040887327892, -8.05749589383192, -8.283268390579906, -0.07776608221319525], [2, 0.0, 200.0, 100.0, 100.0, -6.047507841992551, -5.446510199960762, -5.747009020976656, -0.3004988210158945]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} + image/svg+xml + + + proteus-mpl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/validation/energy_radiogenic_decay_dark.svg b/docs/assets/validation/energy_radiogenic_decay_dark.svg new file mode 100644 index 00000000..ac17e403 --- /dev/null +++ b/docs/assets/validation/energy_radiogenic_decay_dark.svg @@ -0,0 +1,1809 @@ + + + + + + + + test: tests/test_energy.py::test_radiogenic_heating_pins_two_isotope_decay; command: pytest tests/test_energy.py::test_radiogenic_heating_pins_two_isotope_decay --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "log"], "categories": [null, null], "lines": [[61, 0.0, 300.0, 149.99999999999997, 51.666666666666664, -6.347659722553264, -5.446510199960762, -5.897246696803468, -0.15520348240208787], [61, 0.0, 300.0, 149.99999999999997, 51.666666666666664, -6.350665141287858, -5.447575154295914, -5.899120147791887, -0.1555321644263902], [61, 0.0, 300.0, 149.99999999999997, 51.666666666666664, -8.509040887327892, -8.05749589383192, -8.283268390579906, -0.07776608221319525], [2, 0.0, 200.0, 100.0, 100.0, -6.047507841992551, -5.446510199960762, -5.747009020976656, -0.3004988210158945]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} + image/svg+xml + + + proteus-mpl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/validation/energy_steady_state_ic.svg b/docs/assets/validation/energy_steady_state_ic.svg new file mode 100644 index 00000000..f509eff2 --- /dev/null +++ b/docs/assets/validation/energy_steady_state_ic.svg @@ -0,0 +1,1898 @@ + + + + + + + + test: tests/test_energy.py::test_steady_state_ic_balances_interior_and_surface; command: pytest tests/test_energy.py::test_steady_state_ic_balances_interior_and_surface --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[50, 3504.049999999999, 6371.000000000025, 4866.723471494237, -498.5081967134211, 4.2448235883394636e+20, 4.2448235884226e+20, 4.244823588377694e+20, 390105126.100485]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}, {"scales": ["linear", "log"], "categories": [null, null], "lines": [[21, 1000.0, 3000.0, 2000.0000000000002, 366.6666666666668, 4.751192123886938, 6.662066404532349, 5.870112634619891, 0.33865659990514185], [21, 1000.0, 3000.0, 2000.0000000000002, 366.6666666666668, 1.74468369961667, 3.184647537460785, 2.5892010970536017, 0.2549363219132293], [1, 1957.478201888133, 1957.478201888133, 1957.478201888133, 0.0, 5.9202345718217115, 5.9202345718217115, 5.9202345718217115, 0.0]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} + image/svg+xml + + + proteus-mpl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/validation/energy_steady_state_ic_dark.svg b/docs/assets/validation/energy_steady_state_ic_dark.svg new file mode 100644 index 00000000..26cb2b3f --- /dev/null +++ b/docs/assets/validation/energy_steady_state_ic_dark.svg @@ -0,0 +1,1898 @@ + + + + + + + + test: tests/test_energy.py::test_steady_state_ic_balances_interior_and_surface; command: pytest tests/test_energy.py::test_steady_state_ic_balances_interior_and_surface --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[50, 3504.049999999999, 6371.000000000025, 4866.723471494237, -498.5081967134211, 4.2448235883394636e+20, 4.2448235884226e+20, 4.244823588377694e+20, 390105126.100485]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}, {"scales": ["linear", "log"], "categories": [null, null], "lines": [[21, 1000.0, 3000.0, 2000.0000000000002, 366.6666666666668, 4.751192123886938, 6.662066404532349, 5.870112634619891, 0.33865659990514185], [21, 1000.0, 3000.0, 2000.0000000000002, 366.6666666666668, 1.74468369961667, 3.184647537460785, 2.5892010970536017, 0.2549363219132293], [1, 1957.478201888133, 1957.478201888133, 1957.478201888133, 0.0, 5.9202345718217115, 5.9202345718217115, 5.9202345718217115, 0.0]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} + image/svg+xml + + + proteus-mpl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/test_atmosphere.py b/tests/test_atmosphere.py index f9f220d8..d8c1c75b 100644 --- a/tests/test_atmosphere.py +++ b/tests/test_atmosphere.py @@ -28,7 +28,7 @@ @pytest.mark.physics_invariant @pytest.mark.reference_pinned -def test_grey_body_flux_matches_stefan_boltzmann_limit(blackbody_short): +def test_grey_body_flux_matches_stefan_boltzmann_limit(blackbody_short, validation_plot): """The reported surface flux equals the grey-body analytical limit. Anchor: the Stefan-Boltzmann grey-body flux @@ -57,6 +57,28 @@ def test_grey_body_flux_matches_stefan_boltzmann_limit(blackbody_short): # nondimensional value leaking through unscaled would sit near unity. assert 1e2 < fatm < 1e9 + def figure(): + import matplotlib.pyplot as plt + import numpy as np + + t = np.linspace(1000.0, 3000.0, 41) + fig, ax = plt.subplots(figsize=(6, 4)) + ax.plot( + t, + EMISSIVITY * SIGMA * (t**4 - TEQM**4), + label='Grey body, $\\epsilon\\sigma(T^4 - T_\\mathrm{eqm}^4)$', + ) + ax.plot(t, EMISSIVITY * SIGMA * (t**3 - TEQM**3), '--', label='$T^3$ form') + ax.plot([t_surf], [fatm], 'o', zorder=3, label='Reported Fatm at 200 yr') + ax.set_yscale('log') + ax.set_xlabel('Surface temperature [K]') + ax.set_ylabel('Surface flux [W m$^{-2}$]') + ax.legend(fontsize=8) + fig.tight_layout() + return fig + + validation_plot(figure, 'atmosphere_grey_body_flux') + @pytest.mark.physics_invariant def test_surface_cools_and_flux_decays_across_macro_steps(blackbody_short): diff --git a/tests/test_bc.py b/tests/test_bc.py index 451a1a11..74ee05db 100644 --- a/tests/test_bc.py +++ b/tests/test_bc.py @@ -28,7 +28,9 @@ @pytest.mark.physics_invariant @pytest.mark.reference_pinned -def test_prescribed_flux_bc_echoes_value_and_inverts_emissivity(cached_spider_run): +def test_prescribed_flux_bc_echoes_value_and_inverts_emissivity( + cached_spider_run, validation_plot +): """SURFACE_BC 4 fixes the outgoing flux at the prescribed SI value. Anchor: analytical identity. The prescribed flux must appear @@ -64,6 +66,29 @@ def test_prescribed_flux_bc_echoes_value_and_inverts_emissivity(cached_spider_ru assert fatm > 0 assert 1e2 < fatm < 1e9 + def figure(): + import matplotlib.pyplot as plt + + t = np.linspace(1000.0, 3000.0, 41) + fig, ax = plt.subplots(figsize=(6, 4)) + ax.plot(t, SIGMA * (t**4 - TEQM**4), label='Grey body, emissivity 1') + ax.plot( + t, + emissivity * SIGMA * (t**4 - TEQM**4), + '--', + label=f'Grey body, reported emissivity {emissivity:.3f}', + ) + ax.axhline(PRESCRIBED_FLUX, ls=':', label='Prescribed flux 1e5 W m$^{-2}$') + ax.plot([t_surf], [fatm], 'o', zorder=3, label='Reported Fatm') + ax.set_yscale('log') + ax.set_xlabel('Surface temperature [K]') + ax.set_ylabel('Surface flux [W m$^{-2}$]') + ax.legend(fontsize=8) + fig.tight_layout() + return fig + + validation_plot(figure, 'bc_prescribed_flux') + @pytest.mark.physics_invariant def test_mantle_cools_from_the_top_between_surface_and_cmb(blackbody_short): @@ -153,10 +178,14 @@ def entropy_bc_run(cached_spider_run): """ return cached_spider_run( overrides=( - '-nstepsmacro', '1', - '-dtmacro', '10', - '-ic_surface_entropy', '2550', - '-ic_core_entropy', '2650', + '-nstepsmacro', + '1', + '-dtmacro', + '10', + '-ic_surface_entropy', + '2550', + '-ic_core_entropy', + '2650', '-ic_steady_state_energy', ), name='entropy_bc_steady', @@ -165,7 +194,9 @@ def entropy_bc_run(cached_spider_run): @pytest.mark.physics_invariant @pytest.mark.reference_pinned -def test_constant_entropy_bc_pins_the_initial_surface(entropy_bc_run, blackbody_short): +def test_constant_entropy_bc_pins_the_initial_surface( + entropy_bc_run, blackbody_short, validation_plot +): """ic_surface_entropy fixes the surface basic node exactly. Anchor: analytical identity. The constant-entropy boundary writes @@ -190,3 +221,21 @@ def test_constant_entropy_bc_pins_the_initial_surface(entropy_bc_run, blackbody_ # and within the entropy range of the loaded tables. assert np.all(s_b > 0) assert 2400.0 < s_b[-1] < 2700.0 # J/kg/K + + def figure(): + import matplotlib.pyplot as plt + + r_km = data_si(read_output(entropy_bc_run, 0), 'radius_b') / 1e3 + r_def = data_si(read_output(blackbody_short, 0), 'radius_b') / 1e3 + fig, ax = plt.subplots(figsize=(6, 4)) + ax.plot(r_km, s_b, label='Constant-entropy boundary, $t$ = 0') + ax.plot(r_def, s_b_default, '--', label='Default boundary, $t$ = 0') + ax.plot([r_km[0]], [2550.0], 's', mfc='none', ms=10, label='ic_surface_entropy = 2550') + ax.plot([r_km[0]], [s_b[0]], 'o', zorder=3, label='Surface node') + ax.set_xlabel('Radius [km]') + ax.set_ylabel('Entropy [J kg$^{-1}$ K$^{-1}$]') + ax.legend(fontsize=8) + fig.tight_layout() + return fig + + validation_plot(figure, 'bc_constant_entropy_surface') diff --git a/tests/test_energy.py b/tests/test_energy.py index 41d9bd32..446ef2c2 100644 --- a/tests/test_energy.py +++ b/tests/test_energy.py @@ -61,7 +61,9 @@ def test_total_flux_assembles_from_transport_components(blackbody_short): @pytest.mark.physics_invariant @pytest.mark.reference_pinned -def test_disabled_transport_mechanisms_carry_zero_flux(blackbody_short, cached_spider_run): +def test_disabled_transport_mechanisms_carry_zero_flux( + blackbody_short, cached_spider_run, validation_plot +): """Each disabled transport mechanism contributes exactly zero flux. Analytical anchor: a transport mechanism switched off by its option @@ -117,6 +119,29 @@ def test_disabled_transport_mechanisms_carry_zero_flux(blackbody_short, cached_s jtot_full = data_si(read_output(blackbody_short, 0), 'Jtot_b') assert 1e6 < np.abs(jtot_full).max() < 1e12 # W/m^2 + def figure(): + import matplotlib.pyplot as plt + + r_nc = data_si(doc_nc, 'radius_b') / 1e3 + r_co = data_si(doc_co, 'radius_b') / 1e3 + off = np.max( + [np.abs(data_si(doc_co, n)) for n in ('Jconv_b', 'Jmix_b', 'Jgrav_b')], axis=0 + ) + fig, ax = plt.subplots(figsize=(6, 4)) + ax.plot(r_nc, jconv_nc, label='Conduction off: Jconv') + ax.plot(r_nc, jcond_nc, '--', label='Conduction off: Jcond') + ax.plot(r_co, jcond_co, '-.', label='Conduction only: Jcond') + ax.plot(r_co, off, ':', label='Conduction only: Jconv, Jmix, Jgrav') + ax.set_yscale('symlog', linthresh=1e-3) + ax.set_yticks([0.0, 1e-2, 1e2, 1e6, 1e10]) + ax.set_xlabel('Radius [km]') + ax.set_ylabel('Flux [W m$^{-2}$]') + ax.legend(fontsize=8) + fig.tight_layout() + return fig + + validation_plot(figure, 'energy_disabled_transport') + @pytest.mark.physics_invariant def test_surface_node_flux_is_the_grey_body_atmospheric_flux(blackbody_short): @@ -212,7 +237,7 @@ def heating_run(cached_spider_run): @pytest.mark.physics_invariant @pytest.mark.reference_pinned -def test_radiogenic_heating_pins_two_isotope_decay(heating_run): +def test_radiogenic_heating_pins_two_isotope_decay(heating_run, validation_plot): """Radiogenic heating sums both isotopes and follows exponential decay. Anchor: analytical limit. Each radionuclide contributes @@ -250,6 +275,31 @@ def test_radiogenic_heating_pins_two_isotope_decay(heating_run): # Sign guard: heat production is strictly positive. assert np.all(h_0 > 0) and np.all(h_200 > 0) + def figure(): + import matplotlib.pyplot as plt + + t = np.linspace(0.0, 300.0, 61) + al26, k40 = H_AL26 * 2.0 ** (-t / 100.0), H_K40 * 2.0 ** (-t / 200.0) + fig, ax = plt.subplots(figsize=(6, 4)) + ax.plot(t, al26 + k40, label='Sum of the 2 isotopes') + ax.plot(t, al26, '--', label='al26 alone, half-life 100 yr') + ax.plot(t, k40, ':', label='k40 alone, half-life 200 yr') + ax.plot( + [0.0, 200.0], + [h_0[0], h_200[0]], + 'o', + zorder=3, + label='Reported Hradio at 0 and 200 yr', + ) + ax.set_yscale('log') + ax.set_xlabel('Time [yr]') + ax.set_ylabel('Radiogenic heating [W kg$^{-1}$]') + ax.legend(fontsize=8) + fig.tight_layout() + return fig + + validation_plot(figure, 'energy_radiogenic_decay') + @pytest.mark.physics_invariant def test_prescribed_tidal_heating_is_uniform_and_steady(heating_run): @@ -343,7 +393,7 @@ def steady_state_run(cached_spider_run): @pytest.mark.physics_invariant @pytest.mark.reference_pinned -def test_steady_state_ic_balances_interior_and_surface(steady_state_run): +def test_steady_state_ic_balances_interior_and_surface(steady_state_run, validation_plot): """The steady-state IC equalises the energy flow through the mantle. Anchor: analytical limit. With -ic_steady_state_energy the surface @@ -375,3 +425,26 @@ def test_steady_state_ic_balances_interior_and_surface(steady_state_run): # Sign and scale guards. assert fatm > 0 assert 1e2 < fatm < 1e9 # W/m^2 + + def figure(): + import matplotlib.pyplot as plt + + r_km = data_si(doc, 'radius_b') / 1e3 + t = np.linspace(1000.0, 3000.0, 21) + fig, (left, right) = plt.subplots(1, 2, figsize=(8, 3.6)) + left.plot(r_km, etot) + left.set_ylim(0.0, 1.5 * etot.max()) + left.set_yticks([0.0, etot.max()]) + left.set_xlabel('Radius [km]') + left.set_ylabel('Etot [W]') + right.plot(t, sigma * (t**4 - 273.0**4), label='$\\sigma(T^4 - 273^4)$') + right.plot(t, sigma * (t**3 - 273.0**3), '--', label='$T^3$ form') + right.plot([t_surf], [fatm], 'o', zorder=3, label='Reported Fatm') + right.set_yscale('log') + right.set_xlabel('Surface temperature [K]') + right.set_ylabel('Surface flux [W m$^{-2}$]') + right.legend(fontsize=8) + fig.tight_layout() + return fig + + validation_plot(figure, 'energy_steady_state_ic') From 3b8105c6d2e4d65cca7a26b9bf50d64994b90188 Mon Sep 17 00:00:00 2001 From: timlichtenberg Date: Mon, 26 Oct 2026 17:11:27 +0100 Subject: [PATCH 06/15] Plot the equation-of-state validation cases --- docs/Validation/eos.md | 14 +- docs/Validation/eos_adamswilliamson.md | 7 +- docs/Validation/eos_composite.md | 7 +- docs/Validation/eos_lookup.md | 7 +- .../eos_adamswilliamson_density.svg | 1578 +++++++++++++++ .../eos_adamswilliamson_density_dark.svg | 1578 +++++++++++++++ .../eos_composite_melt_fraction.svg | 1759 +++++++++++++++++ .../eos_composite_melt_fraction_dark.svg | 1759 +++++++++++++++++ .../validation/eos_lookup_melt_density.svg | 1284 ++++++++++++ .../eos_lookup_melt_density_dark.svg | 1284 ++++++++++++ docs/assets/validation/eos_phase_boundary.svg | 1595 +++++++++++++++ .../validation/eos_phase_boundary_dark.svg | 1595 +++++++++++++++ .../validation/eos_spaargaren_viscosity.svg | 1284 ++++++++++++ .../eos_spaargaren_viscosity_dark.svg | 1284 ++++++++++++ tests/test_eos.py | 95 +- tests/test_eos_adamswilliamson.py | 37 +- tests/test_eos_composite.py | 38 +- tests/test_eos_lookup.py | 21 +- 18 files changed, 15195 insertions(+), 31 deletions(-) create mode 100644 docs/assets/validation/eos_adamswilliamson_density.svg create mode 100644 docs/assets/validation/eos_adamswilliamson_density_dark.svg create mode 100644 docs/assets/validation/eos_composite_melt_fraction.svg create mode 100644 docs/assets/validation/eos_composite_melt_fraction_dark.svg create mode 100644 docs/assets/validation/eos_lookup_melt_density.svg create mode 100644 docs/assets/validation/eos_lookup_melt_density_dark.svg create mode 100644 docs/assets/validation/eos_phase_boundary.svg create mode 100644 docs/assets/validation/eos_phase_boundary_dark.svg create mode 100644 docs/assets/validation/eos_spaargaren_viscosity.svg create mode 100644 docs/assets/validation/eos_spaargaren_viscosity_dark.svg diff --git a/docs/Validation/eos.md b/docs/Validation/eos.md index b88bc4c8..2972fd58 100644 --- a/docs/Validation/eos.md +++ b/docs/Validation/eos.md @@ -20,11 +20,16 @@ The sections below detail two validation cases, their tolerances, and their test **Known limit.** The independent verification reads the same source table file, checking interpolation rather than physical accuracy of the underlying experimental dataset. +![Liquidus entropy against pressure from the table and from eos.c at 3 probes](../assets/validation/eos_phase_boundary.svg#only-light){ width="80%" } +![Liquidus entropy against pressure from the table and from eos.c at 3 probes](../assets/validation/eos_phase_boundary_dark.svg#only-dark){ width="80%" } + +**How to read the plot.** The x-axis is the pressure and the y-axis the entropy of the melt phase boundary. The line is the liquidus table `liquidus_A11_H13.dat` read with numpy (every 10th node, context). The circles are the asserted values of eos.c at the 3 probes: the table edge at 0, a mid-mantle node at 10 GPa and an off-node point at 65.5 GPa. The cross is the solidus at 10 GPa, which the test requires to be more than 100 away. A pass means that the circles lie on the line (relative 1e-12). + ??? info "Test details" - Reference: Cross-implementation cross-check of the loaded melt phase boundary against an independent read of liquidus_A11_H13.dat (Andrault et al. 2011 liquidus with the Hirschmann 2013 solidus companion). - Tolerance: rel 1e-12 at the P = 0 table edge, a mid-mantle node, and an off-node deep point. - Test id: `tests/test_eos.py::test_phase_boundary_matches_independent_table_read` - - Command: `make tests_c`, then `pytest -o addopts= tests/test_eos.py::test_phase_boundary_matches_independent_table_read` + - Command: `make tests_c`, then `pytest -o addopts= tests/test_eos.py::test_phase_boundary_matches_independent_table_read` (add `--validation-plots` to write the plot pair) - Companion test: `tests/test_eos.py::test_viscosity_and_conductivity_dispatch_per_phase` verifies that melt and solid phases receive distinct transport properties. ## Compositional viscosity matches Spaargaren et al. (2020) @@ -39,11 +44,16 @@ The sections below detail two validation cases, their tolerances, and their test **Known limit.** The test re-encodes the coefficients as in `eos.c`. The 2 hand-derived literals catch an error that is in both copies at the 4 compositions they probe; a coefficient that affects neither literal is not caught. +![Spaargaren viscosity prefactor against Mg/Si with the eos.c shifts](../assets/validation/eos_spaargaren_viscosity.svg#only-light){ width="80%" } +![Spaargaren viscosity prefactor against Mg/Si with the eos.c shifts](../assets/validation/eos_spaargaren_viscosity_dark.svg#only-dark){ width="80%" } + +**How to read the plot.** The x-axis is Mg/Si and the y-axis the log10 viscosity prefactor. The line is the piecewise prefactor of Spaargaren et al. (2020) as the test re-encodes it (context), and the hollow squares are its values at the 2 reference compositions, 1.6 and 0.9. The circles are each reference value plus the asserted shift of eos.c, the melt log10 viscosity minus the configured 2.0, at 1.4 and at 1.2. A pass means that the circles lie on the line (absolute 1e-9), 1 above and 1 below its reference. + ??? info "Test details" - Reference: The piecewise Mg/Si log10 viscosity prefactor of Spaargaren et al. (2020). The test re-encodes the published coefficients independently of eos.c and additionally pins the two branch-pair shifts as hand-derived literals (0.2074 and -1.35803), so a transcription error in either copy of the formula surfaces. - Tolerance: abs 1e-9 on the evaluated shifts (closed-form option arithmetic); abs 1e-4 on the literal transcription pins. - Test id: `tests/test_eos.py::test_compositional_viscosity_matches_spaargaren` - - Command: `make tests_c`, then `pytest -o addopts= tests/test_eos.py::test_compositional_viscosity_matches_spaargaren` + - Command: `make tests_c`, then `pytest -o addopts= tests/test_eos.py::test_compositional_viscosity_matches_spaargaren` (add `--validation-plots` to write the plot pair) - Companion test: `tests/test_eos.py::test_activation_terms_shape_the_viscosity_profile` checks activation volume and energy scalings in the viscosity model. ## Related code diff --git a/docs/Validation/eos_adamswilliamson.md b/docs/Validation/eos_adamswilliamson.md index 4946c8f2..d36b889e 100644 --- a/docs/Validation/eos_adamswilliamson.md +++ b/docs/Validation/eos_adamswilliamson.md @@ -20,11 +20,16 @@ The text below details the check, its tolerance, and its test command. **Known limit.** The Adams-Williamson model assumes an idealized profile, rather than a multi-phase EOS. +![Adams-Williamson density against pressure from the closed form and from the code](../assets/validation/eos_adamswilliamson_density.svg#only-light){ width="80%" } +![Adams-Williamson density against pressure from the closed form and from the code](../assets/validation/eos_adamswilliamson_density_dark.svg#only-dark){ width="80%" } + +**How to read the plot.** The x-axis is the pressure and the y-axis the density. The line is the closed form $\rho_s - P\beta/g$ and the dashed line the form with a sign-flipped $\beta$, which the test rejects (context). The circles are the asserted densities of eos_adamswilliamson.c at the surface, at 10 GPa and at 135 GPa. A pass means that the circles lie on the line (relative 1e-12). + ??? info "Test details" - Reference: Analytical limit: rho(P) = rhos - P beta / g in closed form, with the blackbody50 PREM lower-mantle fit parameters. - Tolerance: rel 1e-12 at the surface (P = 0), 10 GPa, and the approximate core-mantle boundary pressure. - Test id: `tests/test_eos_adamswilliamson.py::test_density_matches_the_closed_form_profile` - - Command: `make tests_c`, then `pytest -o addopts= tests/test_eos_adamswilliamson.py::test_density_matches_the_closed_form_profile` + - Command: `make tests_c`, then `pytest -o addopts= tests/test_eos_adamswilliamson.py::test_density_matches_the_closed_form_profile` (add `--validation-plots` to write the plot pair) - Companion test: `tests/test_eos_adamswilliamson.py::test_si_density_invariant_under_nondimensionalisation_choice` asserts SI invariance under the nondimensionalisation choice. ## Related code diff --git a/docs/Validation/eos_composite.md b/docs/Validation/eos_composite.md index 81a95c6e..ce7d11bf 100644 --- a/docs/Validation/eos_composite.md +++ b/docs/Validation/eos_composite.md @@ -20,11 +20,16 @@ The section below details the validation check, its tolerance, and its test comm **Known limit.** S_liq, S_sol and the fusion entropy come from the code's own outputs, so the test checks internal consistency: the melt fraction is compared with (S - S_sol)/fusion from those values, not with a pinned 0.25. +![Melt fraction against entropy at 10 GPa between the solidus and the liquidus](../assets/validation/eos_composite_melt_fraction.svg#only-light){ width="80%" } +![Melt fraction against entropy at 10 GPa between the solidus and the liquidus](../assets/validation/eos_composite_melt_fraction_dark.svg#only-dark){ width="80%" } + +**How to read the plot.** The x-axis is the entropy at 10 GPa and the y-axis the melt fraction. The dotted lines are the solidus and liquidus that eos_composite.c reports, and the solid line is $(S - S_\mathrm{sol}) / (S_\mathrm{liq} - S_\mathrm{sol})$ between them (context). The dashed line is the form with the liquidus entropy as the denominator, which the test rejects. The circle is the asserted melt fraction at 1675.34 J kg$^{-1}$ K$^{-1}$, a quarter of the way through the interval. A pass means that the circle lies on the solid line at 0.25. + ??? info "Test details" - Reference: Analytical limit: by construction of the two-phase region, fusion = S_liq - S_sol and phi = (S - S_sol)/fusion between the phase boundaries. - Tolerance: rel 1e-10 on the fusion identity; rel 1e-6 on the melt fraction at one quarter of the melting interval at 10 GPa. - Test id: `tests/test_eos_composite.py::test_fusion_equals_liquidus_minus_solidus_and_phi_is_linear_in_entropy` - - Command: `make tests_c`, then `pytest -o addopts= tests/test_eos_composite.py::test_fusion_equals_liquidus_minus_solidus_and_phi_is_linear_in_entropy` + - Command: `make tests_c`, then `pytest -o addopts= tests/test_eos_composite.py::test_fusion_equals_liquidus_minus_solidus_and_phi_is_linear_in_entropy` (add `--validation-plots` to write the plot pair) - Companion test: `tests/test_eos_composite.py::test_phase_fraction_bounded_and_truncation_contract_visible` asserts that the melt fraction is clamped between 0 and 1, and the visibility of untruncated values. ## Related code diff --git a/docs/Validation/eos_lookup.md b/docs/Validation/eos_lookup.md index 5fa0c145..1b670263 100644 --- a/docs/Validation/eos_lookup.md +++ b/docs/Validation/eos_lookup.md @@ -20,11 +20,16 @@ The section below details the check, its tolerance, and its test command. **Known limit.** The test compares table reads against an independent Python read of the same table file, but does not benchmark the underlying ab initio mineral physics. +![Melt density from eos_lookup.c against an independent bilinear read of the table](../assets/validation/eos_lookup_melt_density.svg#only-light){ width="80%" } +![Melt density from eos_lookup.c against an independent bilinear read of the table](../assets/validation/eos_lookup_melt_density_dark.svg#only-dark){ width="80%" } + +**How to read the plot.** The x-axis is the melt density from an independent bilinear read of `density_melt.dat` in numpy and the y-axis the density from eos_lookup.c, at the 3 off-node probes from 7.3 to 89 GPa. The circles are the asserted C values and the line is 1:1. The crosses are the solid-table densities at the same probes, which the test requires to differ by more than 1 %. A pass means that the circles lie on the line (relative 1e-9). + ??? info "Test details" - Reference: Cross-implementation cross-check: an independent numpy bilinear evaluation of the shipped melt density table (1TPa-dK09-elec-free set: de Koker and Stixrude 2009 properties as assembled for Bower et al. 2018). - Tolerance: rel 1e-9 at three off-node (P, S) probe points spanning 7.3 to 89 GPa. - Test id: `tests/test_eos_lookup.py::test_melt_density_matches_independent_bilinear_evaluation` - - Command: `make tests_c`, then `pytest -o addopts= tests/test_eos_lookup.py::test_melt_density_matches_independent_bilinear_evaluation` + - Command: `make tests_c`, then `pytest -o addopts= tests/test_eos_lookup.py::test_melt_density_matches_independent_bilinear_evaluation` (add `--validation-plots` to write the plot pair) - Companion test: `tests/test_eos_lookup.py::test_thermodynamic_quantities_positive_and_rho_monotonic_in_pressure` asserts positive thermodynamic quantities and monotonic density with pressure. ## Related code diff --git a/docs/assets/validation/eos_adamswilliamson_density.svg b/docs/assets/validation/eos_adamswilliamson_density.svg new file mode 100644 index 00000000..2a886824 --- /dev/null +++ b/docs/assets/validation/eos_adamswilliamson_density.svg @@ -0,0 +1,1578 @@ + + + + + + + + test: tests/test_eos_adamswilliamson.py::test_density_matches_the_closed_form_profile; command: pytest tests/test_eos_adamswilliamson.py::test_density_matches_the_closed_form_profile --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[29, 0.0, 140.0, 70.0, 24.999999999999993, 4078.95095544, 5635.099805780001, 4857.025380610001, 277.8837232749998], [29, 0.0, 140.0, 70.0, 24.999999999999993, 2522.8021050999996, 4078.95095544, 3300.87653027, -277.8837232750003], [3, 0.0, 135.0, 48.33333333333333, 45.0, 4078.9509554399997, 5579.523061125001, 4616.192820438334, 500.19070189500036]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} + image/svg+xml + + + proteus-mpl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/validation/eos_adamswilliamson_density_dark.svg b/docs/assets/validation/eos_adamswilliamson_density_dark.svg new file mode 100644 index 00000000..2941f191 --- /dev/null +++ b/docs/assets/validation/eos_adamswilliamson_density_dark.svg @@ -0,0 +1,1578 @@ + + + + + + + + test: tests/test_eos_adamswilliamson.py::test_density_matches_the_closed_form_profile; command: pytest tests/test_eos_adamswilliamson.py::test_density_matches_the_closed_form_profile --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[29, 0.0, 140.0, 70.0, 24.999999999999993, 4078.95095544, 5635.099805780001, 4857.025380610001, 277.8837232749998], [29, 0.0, 140.0, 70.0, 24.999999999999993, 2522.8021050999996, 4078.95095544, 3300.87653027, -277.8837232750003], [3, 0.0, 135.0, 48.33333333333333, 45.0, 4078.9509554399997, 5579.523061125001, 4616.192820438334, 500.19070189500036]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} + image/svg+xml + + + proteus-mpl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/validation/eos_composite_melt_fraction.svg b/docs/assets/validation/eos_composite_melt_fraction.svg new file mode 100644 index 00000000..96edc95b --- /dev/null +++ b/docs/assets/validation/eos_composite_melt_fraction.svg @@ -0,0 +1,1759 @@ + + + + + + + + test: tests/test_eos_composite.py::test_fusion_equals_liquidus_minus_solidus_and_phi_is_linear_in_entropy; command: pytest tests/test_eos_composite.py::test_fusion_equals_liquidus_minus_solidus_and_phi_is_linear_in_entropy --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[41, 1424.429525104198, 2228.0708088514702, 1826.2501669778342, 140.63722465577274, 0.0, 1.0, 0.5000000000000002, 0.21041707468443874], [41, 1424.429525104198, 2228.0708088514702, 1826.2501669778342, 140.63722465577274, 0.0, 0.3306474957602706, 0.14525621553989565, 0.06288645486497857], [2, 1524.429525104198, 1524.429525104198, 1524.429525104198, 0.0, 0.0, 1.0, 0.5, 0.5], [2, 2128.0708088514702, 2128.0708088514702, 2128.0708088514702, 0.0, 0.0, 1.0, 0.5, 0.5], [1, 1675.34, 1675.34, 1675.34, 0.0, 0.2500002550504544, 0.2500002550504544, 0.2500002550504544, 0.0]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} + image/svg+xml + + + proteus-mpl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/validation/eos_composite_melt_fraction_dark.svg b/docs/assets/validation/eos_composite_melt_fraction_dark.svg new file mode 100644 index 00000000..d0cc956e --- /dev/null +++ b/docs/assets/validation/eos_composite_melt_fraction_dark.svg @@ -0,0 +1,1759 @@ + + + + + + + + test: tests/test_eos_composite.py::test_fusion_equals_liquidus_minus_solidus_and_phi_is_linear_in_entropy; command: pytest tests/test_eos_composite.py::test_fusion_equals_liquidus_minus_solidus_and_phi_is_linear_in_entropy --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[41, 1424.429525104198, 2228.0708088514702, 1826.2501669778342, 140.63722465577274, 0.0, 1.0, 0.5000000000000002, 0.21041707468443874], [41, 1424.429525104198, 2228.0708088514702, 1826.2501669778342, 140.63722465577274, 0.0, 0.3306474957602706, 0.14525621553989565, 0.06288645486497857], [2, 1524.429525104198, 1524.429525104198, 1524.429525104198, 0.0, 0.0, 1.0, 0.5, 0.5], [2, 2128.0708088514702, 2128.0708088514702, 2128.0708088514702, 0.0, 0.0, 1.0, 0.5, 0.5], [1, 1675.34, 1675.34, 1675.34, 0.0, 0.2500002550504544, 0.2500002550504544, 0.2500002550504544, 0.0]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} + image/svg+xml + + + proteus-mpl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/validation/eos_lookup_melt_density.svg b/docs/assets/validation/eos_lookup_melt_density.svg new file mode 100644 index 00000000..ae3e13d0 --- /dev/null +++ b/docs/assets/validation/eos_lookup_melt_density.svg @@ -0,0 +1,1284 @@ + + + + + + + + test: tests/test_eos_lookup.py::test_melt_density_matches_independent_bilinear_evaluation; command: pytest tests/test_eos_lookup.py::test_melt_density_matches_independent_bilinear_evaluation --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[2, 2815.723424408976, 4861.406423840278, 3838.5649241246265, 1022.841499715651, 2815.723424408976, 4861.406423840278, 3838.5649241246265, 1022.841499715651], [3, 2963.919394114712, 4629.910879847884, 3732.790715908866, 555.3304952443907, 2963.9193941147114, 4629.910880141561, 3732.7907160067575, 555.3304953422833], [3, 2963.919394114712, 4629.910879847884, 3732.790715908866, 555.3304952443907, 3513.286158465391, 4576.966230769531, 3968.1957573787977, 354.56002410137995]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} + image/svg+xml + + + proteus-mpl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/validation/eos_lookup_melt_density_dark.svg b/docs/assets/validation/eos_lookup_melt_density_dark.svg new file mode 100644 index 00000000..ec381c17 --- /dev/null +++ b/docs/assets/validation/eos_lookup_melt_density_dark.svg @@ -0,0 +1,1284 @@ + + + + + + + + test: tests/test_eos_lookup.py::test_melt_density_matches_independent_bilinear_evaluation; command: pytest tests/test_eos_lookup.py::test_melt_density_matches_independent_bilinear_evaluation --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[2, 2815.723424408976, 4861.406423840278, 3838.5649241246265, 1022.841499715651, 2815.723424408976, 4861.406423840278, 3838.5649241246265, 1022.841499715651], [3, 2963.919394114712, 4629.910879847884, 3732.790715908866, 555.3304952443907, 2963.9193941147114, 4629.910880141561, 3732.7907160067575, 555.3304953422833], [3, 2963.919394114712, 4629.910879847884, 3732.790715908866, 555.3304952443907, 3513.286158465391, 4576.966230769531, 3968.1957573787977, 354.56002410137995]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} + image/svg+xml + + + proteus-mpl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/validation/eos_phase_boundary.svg b/docs/assets/validation/eos_phase_boundary.svg new file mode 100644 index 00000000..d3c47af2 --- /dev/null +++ b/docs/assets/validation/eos_phase_boundary.svg @@ -0,0 +1,1595 @@ + + + + + + + + test: tests/test_eos.py::test_phase_boundary_matches_independent_table_read; command: pytest tests/test_eos.py::test_phase_boundary_matches_independent_table_read --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[16, 0.0, 150.0, 75.0, 28.333333333333332, 1959.279037855336, 2632.641177281688, 2358.1775330396, 112.08506609211442], [1, 10.0, 10.0, 10.0, 0.0, 1524.43, 1524.43, 1524.43, 0.0], [3, 0.0, 65.5, 25.166666666666668, 21.833333333333332, 1959.279037855336, 2333.346925578552, 2140.2322574284526, 124.68929590773877]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} + image/svg+xml + + + proteus-mpl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/validation/eos_phase_boundary_dark.svg b/docs/assets/validation/eos_phase_boundary_dark.svg new file mode 100644 index 00000000..4b53baf4 --- /dev/null +++ b/docs/assets/validation/eos_phase_boundary_dark.svg @@ -0,0 +1,1595 @@ + + + + + + + + test: tests/test_eos.py::test_phase_boundary_matches_independent_table_read; command: pytest tests/test_eos.py::test_phase_boundary_matches_independent_table_read --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[16, 0.0, 150.0, 75.0, 28.333333333333332, 1959.279037855336, 2632.641177281688, 2358.1775330396, 112.08506609211442], [1, 10.0, 10.0, 10.0, 0.0, 1524.43, 1524.43, 1524.43, 0.0], [3, 0.0, 65.5, 25.166666666666668, 21.833333333333332, 1959.279037855336, 2333.346925578552, 2140.2322574284526, 124.68929590773877]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} + image/svg+xml + + + proteus-mpl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/validation/eos_spaargaren_viscosity.svg b/docs/assets/validation/eos_spaargaren_viscosity.svg new file mode 100644 index 00000000..4caebd9e --- /dev/null +++ b/docs/assets/validation/eos_spaargaren_viscosity.svg @@ -0,0 +1,1284 @@ + + + + + + + + test: tests/test_eos.py::test_compositional_viscosity_matches_spaargaren; command: pytest tests/test_eos.py::test_compositional_viscosity_matches_spaargaren --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[57, 0.6, 2.0, 1.2999999999999998, 0.24166666666666659, -2.0, 0.6913333333333334, -1.0515555555555556, -0.5642391395154552], [2, 0.9, 1.6, 1.25, -0.35000000000000003, -2.0, 0.17283333333333328, -0.9135833333333334, 1.0864166666666666], [2, 1.2, 1.4, 1.3, -0.09999999999999995, -1.7926000000000002, -1.1852, -1.4889000000000003, 0.3037]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} + image/svg+xml + + + proteus-mpl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/validation/eos_spaargaren_viscosity_dark.svg b/docs/assets/validation/eos_spaargaren_viscosity_dark.svg new file mode 100644 index 00000000..1fccb564 --- /dev/null +++ b/docs/assets/validation/eos_spaargaren_viscosity_dark.svg @@ -0,0 +1,1284 @@ + + + + + + + + test: tests/test_eos.py::test_compositional_viscosity_matches_spaargaren; command: pytest tests/test_eos.py::test_compositional_viscosity_matches_spaargaren --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[57, 0.6, 2.0, 1.2999999999999998, 0.24166666666666659, -2.0, 0.6913333333333334, -1.0515555555555556, -0.5642391395154552], [2, 0.9, 1.6, 1.25, -0.35000000000000003, -2.0, 0.17283333333333328, -0.9135833333333334, 1.0864166666666666], [2, 1.2, 1.4, 1.3, -0.09999999999999995, -1.7926000000000002, -1.1852, -1.4889000000000003, 0.3037]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} + image/svg+xml + + + proteus-mpl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/test_eos.py b/tests/test_eos.py index a55abe7e..21ab6d29 100644 --- a/tests/test_eos.py +++ b/tests/test_eos.py @@ -31,9 +31,12 @@ def _eval(c_test, prefix, p_list, s_list): 'test_eos', ( *OPTS, - '-eos_prefix', prefix, - '-P_si', ','.join(str(p) for p in p_list), - '-S_si', ','.join(str(s) for s in s_list), + '-eos_prefix', + prefix, + '-P_si', + ','.join(str(p) for p in p_list), + '-S_si', + ','.join(str(s) for s in s_list), ), ) @@ -60,7 +63,7 @@ def test_viscosity_and_conductivity_dispatch_per_phase(c_test): @pytest.mark.physics_invariant @pytest.mark.reference_pinned -def test_phase_boundary_matches_independent_table_read(c_test): +def test_phase_boundary_matches_independent_table_read(c_test, validation_plot): """The loaded melt phase boundary reproduces the A11_H13 liquidus table. Cross-implementation cross-check: tests/_table_utils.py reads @@ -85,6 +88,30 @@ def test_phase_boundary_matches_independent_table_read(c_test): assert out['phase_boundary_S'][1] > 0 assert 1.5e3 < out['phase_boundary_S'][1] < 4.0e3 + def figure(): + import matplotlib.pyplot as plt + + keep = xa <= 1.5e11 + fig, ax = plt.subplots(figsize=(6, 4)) + ax.plot( + xa[keep][::10] / 1e9, ya[keep][::10], label='liquidus_A11_H13.dat, read by numpy' + ) + ax.plot([10.0], [1524.43], 'x', ms=10, label='Solidus at 10 GPa') + ax.plot( + np.array(probes) / 1e9, + out['phase_boundary_S'], + 'o', + zorder=3, + label='eos.c phase boundary at the 3 probes', + ) + ax.set_xlabel('Pressure [GPa]') + ax.set_ylabel('Entropy [J kg$^{-1}$ K$^{-1}$]') + ax.legend(fontsize=8) + fig.tight_layout() + return fig + + validation_plot(figure, 'eos_phase_boundary') + def _spaargaren_prefactor(mg_si): """log10 viscosity prefactor of Spaargaren et al. (2020), as in eos.c.""" @@ -99,7 +126,7 @@ def _spaargaren_prefactor(mg_si): @pytest.mark.physics_invariant @pytest.mark.reference_pinned -def test_compositional_viscosity_matches_spaargaren(c_test): +def test_compositional_viscosity_matches_spaargaren(c_test, validation_plot): """The Mg/Si viscosity prefactor reproduces Spaargaren et al. (2020). Anchor: the piecewise log10 prefactor of Spaargaren et al. (2020). @@ -109,12 +136,8 @@ def test_compositional_viscosity_matches_spaargaren(c_test): shift adds to the configured melt log10visc of 2.0. """ probe = ((1.0e10,), (2600.0,)) - high = _eval_with( - c_test, ('-melt_visc_comp', '1.4', '-melt_visc_ref_comp', '1.6'), *probe - ) - low = _eval_with( - c_test, ('-melt_visc_comp', '1.2', '-melt_visc_ref_comp', '0.9'), *probe - ) + high = _eval_with(c_test, ('-melt_visc_comp', '1.4', '-melt_visc_ref_comp', '1.6'), *probe) + low = _eval_with(c_test, ('-melt_visc_comp', '1.2', '-melt_visc_ref_comp', '0.9'), *probe) shift_high = _spaargaren_prefactor(1.4) - _spaargaren_prefactor(1.6) shift_low = _spaargaren_prefactor(1.2) - _spaargaren_prefactor(0.9) @@ -140,6 +163,37 @@ def test_compositional_viscosity_matches_spaargaren(c_test): base = _eval(c_test, 'melt', *probe) assert base['log10visc'][0] == pytest.approx(2.0, abs=1e-10) + def figure(): + import matplotlib.pyplot as plt + + x = np.linspace(0.6, 2.0, 57) + fig, ax = plt.subplots(figsize=(6, 4)) + ax.plot( + x, + [_spaargaren_prefactor(xi) for xi in x], + label='Spaargaren et al. (2020) prefactor', + ) + ax.plot( + [1.6, 0.9], + [_spaargaren_prefactor(1.6), _spaargaren_prefactor(0.9)], + 's', + mfc='none', + ms=10, + label='Reference compositions', + ) + code = [ + _spaargaren_prefactor(1.6) + high['log10visc'][0] - 2.0, + _spaargaren_prefactor(0.9) + low['log10visc'][0] - 2.0, + ] + ax.plot([1.4, 1.2], code, 'o', zorder=3, label='Reference + eos.c shift') + ax.set_xlabel('Mg/Si') + ax.set_ylabel('log$_{10}$ viscosity prefactor') + ax.legend(fontsize=8) + fig.tight_layout() + return fig + + validation_plot(figure, 'eos_spaargaren_viscosity') + def _eval_with(c_test, extra, p_list, s_list): """Evaluate the melt EOS with additional viscosity options.""" @@ -148,9 +202,12 @@ def _eval_with(c_test, extra, p_list, s_list): ( *OPTS, *extra, - '-eos_prefix', 'melt', - '-P_si', ','.join(str(p) for p in p_list), - '-S_si', ','.join(str(s) for s in s_list), + '-eos_prefix', + 'melt', + '-P_si', + ','.join(str(p) for p in p_list), + '-S_si', + ','.join(str(s) for s in s_list), ), ) @@ -169,9 +226,7 @@ def test_activation_terms_shape_the_viscosity_profile(c_test): probes_p = (0.0, 6.55e10) # Pa; surface limit and deep mantle probes_s = (2600.0, 2600.0) # J/kg/K - vol = _eval_with( - c_test, ('-melt_activation_volume', '1.0e-6'), probes_p, probes_s - ) + vol = _eval_with(c_test, ('-melt_activation_volume', '1.0e-6'), probes_p, probes_s) # Edge limit: no pressure, no activation-volume contribution; the # bare constant 2.0 returns exactly. assert vol['log10visc'][0] == pytest.approx(2.0, abs=1e-10) @@ -185,8 +240,10 @@ def test_activation_terms_shape_the_viscosity_profile(c_test): damped = _eval_with( c_test, ( - '-melt_activation_volume', '1.0e-6', - '-melt_activation_volume_pressure_scale', '1.0e10', + '-melt_activation_volume', + '1.0e-6', + '-melt_activation_volume_pressure_scale', + '1.0e10', ), probes_p, probes_s, diff --git a/tests/test_eos_adamswilliamson.py b/tests/test_eos_adamswilliamson.py index 27a8d9f2..3cc655a9 100644 --- a/tests/test_eos_adamswilliamson.py +++ b/tests/test_eos_adamswilliamson.py @@ -33,12 +33,16 @@ def _aw_eval(c_test, p_list, extra=()): 'test_eos', ( *OPTS, - '-eos_type', 'adamswilliamson', - '-eos_prefix', 'adams_williamson', - '-P_si', ','.join(str(p) for p in p_list), + '-eos_type', + 'adamswilliamson', + '-eos_prefix', + 'adams_williamson', + '-P_si', + ','.join(str(p) for p in p_list), # Entropy is unused by the AW density but the executable # requires paired probe points. - '-S_si', ','.join('2600.0' for _ in range(n)), + '-S_si', + ','.join('2600.0' for _ in range(n)), *extra, ), ) @@ -46,7 +50,7 @@ def _aw_eval(c_test, p_list, extra=()): @pytest.mark.physics_invariant @pytest.mark.reference_pinned -def test_density_matches_the_closed_form_profile(c_test): +def test_density_matches_the_closed_form_profile(c_test, validation_plot): """The AW density reproduces rho(P) = rhos - P * beta / g exactly. Analytical limit: the Adams-Williamson equation of state is a closed @@ -71,6 +75,29 @@ def test_density_matches_the_closed_form_profile(c_test): assert out['rho'][0] < out['rho'][1] < out['rho'][2] assert all(3.0e3 < rho < 7.0e3 for rho in out['rho']) + def figure(): + import matplotlib.pyplot as plt + import numpy as np + + p = np.linspace(0.0, 1.4e11, 29) + fig, ax = plt.subplots(figsize=(6, 4)) + ax.plot(p / 1e9, RHOS - p * BETA / GRAVITY, label=r'$\rho_s - P\beta/g$') + ax.plot(p / 1e9, RHOS + p * BETA / GRAVITY, '--', label=r'Sign-flipped $\beta$') + ax.plot( + np.array(probes) / 1e9, + out['rho'], + 'o', + zorder=3, + label='eos_adamswilliamson.c at 3 probes', + ) + ax.set_xlabel('Pressure [GPa]') + ax.set_ylabel('Density [kg m$^{-3}$]') + ax.legend(fontsize=8) + fig.tight_layout() + return fig + + validation_plot(figure, 'eos_adamswilliamson_density') + @pytest.mark.physics_invariant def test_si_density_invariant_under_nondimensionalisation_choice(c_test): diff --git a/tests/test_eos_composite.py b/tests/test_eos_composite.py index 4b9da94b..82600eef 100644 --- a/tests/test_eos_composite.py +++ b/tests/test_eos_composite.py @@ -62,7 +62,9 @@ def test_phase_fraction_bounded_and_truncation_contract_visible(c_test): @pytest.mark.physics_invariant @pytest.mark.reference_pinned -def test_fusion_equals_liquidus_minus_solidus_and_phi_is_linear_in_entropy(c_test): +def test_fusion_equals_liquidus_minus_solidus_and_phi_is_linear_in_entropy( + c_test, validation_plot +): """Fusion entropy and melt fraction follow their defining relations. Analytical limit: by construction of the two-phase region, @@ -90,6 +92,40 @@ def test_fusion_equals_liquidus_minus_solidus_and_phi_is_linear_in_entropy(c_tes # few hundred J/kg/K, not a few or a few thousand. assert 100.0 < fusion < 1500.0 + def figure(): + import matplotlib.pyplot as plt + import numpy as np + + s = np.linspace(sol - 100.0, liq + 100.0, 41) + fig, ax = plt.subplots(figsize=(6, 4)) + ax.plot( + s, + np.clip((s - sol) / fusion, 0.0, 1.0), + label='$(S - S_\\mathrm{sol}) / (S_\\mathrm{liq} - S_\\mathrm{sol})$', + ) + ax.plot( + s, + np.clip((s - sol) / liq, 0.0, 1.0), + '--', + label='$(S - S_\\mathrm{sol}) / S_\\mathrm{liq}$', + ) + ax.axvline(sol, ls=':', label='Solidus and liquidus, eos_composite.c') + ax.axvline(liq, ls=':') + ax.plot( + [s_probe], + [out['phase_fraction'][0]], + 'o', + zorder=3, + label='Melt fraction at 1675.34', + ) + ax.set_xlabel('Entropy at 10 GPa [J kg$^{-1}$ K$^{-1}$]') + ax.set_ylabel('Melt fraction') + ax.legend(fontsize=8) + fig.tight_layout() + return fig + + validation_plot(figure, 'eos_composite_melt_fraction') + @pytest.mark.physics_invariant def test_pure_phase_limits_recover_the_single_phase_lookups(c_test): diff --git a/tests/test_eos_lookup.py b/tests/test_eos_lookup.py index 9c6ca7ff..a33d5766 100644 --- a/tests/test_eos_lookup.py +++ b/tests/test_eos_lookup.py @@ -50,7 +50,7 @@ def _melt_eval(c_test, p_list, s_list, extra=()): @pytest.mark.physics_invariant @pytest.mark.reference_pinned -def test_melt_density_matches_independent_bilinear_evaluation(c_test): +def test_melt_density_matches_independent_bilinear_evaluation(c_test, validation_plot): """The C lookup reproduces an independent bilinear read of the same table. Cross-implementation cross-check: tests/_table_utils.py evaluates @@ -73,6 +73,25 @@ def test_melt_density_matches_independent_bilinear_evaluation(c_test): assert all(rho > 0 for rho in out['rho']) assert all(2.0e3 < rho < 8.0e3 for rho in out['rho']) + def figure(): + import matplotlib.pyplot as plt + + lo, hi = ( + 0.95 * min(expected + list(solid['rho'])), + 1.05 * max(expected + list(solid['rho'])), + ) + fig, ax = plt.subplots(figsize=(6, 4)) + ax.plot([lo, hi], [lo, hi], label='1:1') + ax.plot(expected, out['rho'], 'o', zorder=3, label='C melt density at the 3 probes') + ax.plot(expected, solid['rho'], 'x', ms=10, label='Solid table at the same probes') + ax.set_xlabel('Bilinear read of density_melt.dat [kg m$^{-3}$]') + ax.set_ylabel('eos_lookup.c [kg m$^{-3}$]') + ax.legend(fontsize=8) + fig.tight_layout() + return fig + + validation_plot(figure, 'eos_lookup_melt_density') + @pytest.mark.physics_invariant def test_thermodynamic_quantities_positive_and_rho_monotonic_in_pressure(c_test): From 091bee730997fa42e46ed2f6d15ff7fc308dc82f Mon Sep 17 00:00:00 2001 From: timlichtenberg Date: Mon, 26 Oct 2026 17:23:22 +0100 Subject: [PATCH 07/15] Plot the interpolation, viscosity, mesh, reaction, front and melt-fraction cases --- docs/Validation/interp.md | 7 +- docs/Validation/matprop.md | 7 +- docs/Validation/mesh.md | 7 +- docs/Validation/reaction.md | 7 +- docs/Validation/rheologicalfront.md | 7 +- docs/Validation/twophase.md | 7 +- docs/assets/validation/interp_affine.svg | 1262 +++++++++++++ docs/assets/validation/interp_affine_dark.svg | 1262 +++++++++++++ .../matprop_viscosity_end_members.svg | 1602 ++++++++++++++++ .../matprop_viscosity_end_members_dark.svg | 1602 ++++++++++++++++ .../validation/mesh_pressure_profile.svg | 1632 +++++++++++++++++ .../validation/mesh_pressure_profile_dark.svg | 1632 +++++++++++++++++ .../validation/reaction_element_balance.svg | 1262 +++++++++++++ .../reaction_element_balance_dark.svg | 1262 +++++++++++++ .../validation/rheologicalfront_location.svg | 1436 +++++++++++++++ .../rheologicalfront_location_dark.svg | 1436 +++++++++++++++ .../twophase_global_melt_fraction.svg | 1500 +++++++++++++++ .../twophase_global_melt_fraction_dark.svg | 1500 +++++++++++++++ tests/test_interp.py | 30 +- tests/test_matprop.py | 29 +- tests/test_mesh.py | 33 +- tests/test_reaction.py | 33 +- tests/test_rheologicalfront.py | 34 +- tests/test_twophase.py | 22 +- 24 files changed, 17595 insertions(+), 16 deletions(-) create mode 100644 docs/assets/validation/interp_affine.svg create mode 100644 docs/assets/validation/interp_affine_dark.svg create mode 100644 docs/assets/validation/matprop_viscosity_end_members.svg create mode 100644 docs/assets/validation/matprop_viscosity_end_members_dark.svg create mode 100644 docs/assets/validation/mesh_pressure_profile.svg create mode 100644 docs/assets/validation/mesh_pressure_profile_dark.svg create mode 100644 docs/assets/validation/reaction_element_balance.svg create mode 100644 docs/assets/validation/reaction_element_balance_dark.svg create mode 100644 docs/assets/validation/rheologicalfront_location.svg create mode 100644 docs/assets/validation/rheologicalfront_location_dark.svg create mode 100644 docs/assets/validation/twophase_global_melt_fraction.svg create mode 100644 docs/assets/validation/twophase_global_melt_fraction_dark.svg diff --git a/docs/Validation/interp.md b/docs/Validation/interp.md index c4c2394b..5e7364d3 100644 --- a/docs/Validation/interp.md +++ b/docs/Validation/interp.md @@ -20,11 +20,16 @@ The section below details the validation check, its tolerance, and its test comm **Known limit.** The case covers 1-D interpolation with unit scalings; the 2-D bilinear case (`test_bilinear_plane_exact_everywhere_including_grid_corner`) and per-column scaling are not part of it. +![Piecewise-linear interpolation of y = 2x + 1 at 4 queries](../assets/validation/interp_affine.svg#only-light){ width="80%" } +![Piecewise-linear interpolation of y = 2x + 1 at 4 queries](../assets/validation/interp_affine_dark.svg#only-dark){ width="80%" } + +**How to read the plot.** The hollow squares are the 11 table nodes of $y = 2x + 1$ and the line is the affine function. The circles are the asserted values of interp.c at the 4 queries $x$ = 0, 2.5, 7 and 10 (the last node). The crosses at $x$ = 2.5 are the 2 nearest node values, 5 and 7, which the test requires to be more than 0.5 away. A pass means that the circles lie on the line (relative 1e-14). + ??? info "Test details" - Reference: Analytical limit: piecewise-linear interpolation reproduces affine data exactly. - Tolerance: rel 1e-14 on y = 2x + 1 at on-node, off-node, and exact-endpoint queries. - Test id: `tests/test_interp.py::test_affine_data_reproduced_exactly_including_last_node` - - Command: `make tests_c`, then `pytest -o addopts= tests/test_interp.py::test_affine_data_reproduced_exactly_including_last_node` + - Command: `make tests_c`, then `pytest -o addopts= tests/test_interp.py::test_affine_data_reproduced_exactly_including_last_node` (add `--validation-plots` to write the plot pair) - Companion test: `tests/test_interp.py::test_out_of_range_queries_clamp_to_table_edges` asserts out-of-range clamping at table edges. ## Related code diff --git a/docs/Validation/matprop.md b/docs/Validation/matprop.md index 64c67162..c34ec5ab 100644 --- a/docs/Validation/matprop.md +++ b/docs/Validation/matprop.md @@ -20,11 +20,16 @@ The text below details the check, its tolerance, and its test command. **Known limit.** The main test samples only nearly pure nodes, so a wrong weighting inside the melting interval is not caught there. The companion test `test_viscosity_rises_monotonically_across_rheological_transition` checks that the viscosity rises steadily across the transition, by more than 3 decades, but not its shape. +![log10 viscosity against radius for the hot molten and the cold initial condition](../assets/validation/matprop_viscosity_end_members.svg#only-light){ width="80%" } +![log10 viscosity against radius for the hot molten and the cold initial condition](../assets/validation/matprop_viscosity_end_members_dark.svg#only-dark){ width="80%" } + +**How to read the plot.** The x-axis is the radius and the y-axis the log10 viscosity at the basic nodes. The solid line is the hot initial condition, all 50 nodes molten, asserted at the melt end member 2 (dashed, within 0.01). The dash-dotted line is the cold initial condition at 1600 J kg$^{-1}$ K$^{-1}$, and the circles its asserted nodes with a melt fraction below $10^{-3}$, at the solid end member 21 (dotted, within 0.05). A pass means that the solid line lies on the dashed line and the circles on the dotted line. + ??? info "Test details" - Reference: Analytical limit: away from the rheological transition the blended viscosity recovers the configured end-members (melt_log10visc = 2, solid_log10visc = 21). - Tolerance: abs 0.01 on log10(viscosity) at the strongly molten nodes and abs 0.05 at the strongly solid nodes. - Test id: `tests/test_matprop.py::test_viscosity_recovers_configured_end_members` - - Command: `make`, then `pytest -o addopts= tests/test_matprop.py::test_viscosity_recovers_configured_end_members` + - Command: `make`, then `pytest -o addopts= tests/test_matprop.py::test_viscosity_recovers_configured_end_members` (add `--validation-plots` to write the plot pair) - Companion test: `tests/test_matprop.py::test_material_properties_positive_and_conductivity_constant` asserts positive values for density, heat capacity and thermal expansion, and a constant conductivity; the main test asserts a positive viscosity. ## Related code diff --git a/docs/Validation/mesh.md b/docs/Validation/mesh.md index 00735e4f..86b88571 100644 --- a/docs/Validation/mesh.md +++ b/docs/Validation/mesh.md @@ -20,11 +20,16 @@ The section below details the validation check, its tolerance, and its test comm **Known limit.** The test evaluates the expected pressure at the code's own radii, so a spacing error is not caught. A wrong core-mantle boundary radius is caught only if it moves the maximum pressure out of 5e10 to 5e11 Pa. The profile assumes an idealised Adams-Williamson compressibility rather than a self-consistent mineral structure. +![Basic-node pressure against radius on the Adams-Williamson closed form](../assets/validation/mesh_pressure_profile.svg#only-light){ width="80%" } +![Basic-node pressure against radius on the Adams-Williamson closed form](../assets/validation/mesh_pressure_profile_dark.svg#only-dark){ width="80%" } + +**How to read the plot.** The x-axis is the radius and the y-axis the pressure. The line is the Adams-Williamson closed form (context), and the dashed and dotted lines are the constant-density profile and the form with a sign-flipped $\beta$, which the test rejects at the core-mantle boundary. The circles are the asserted pressures of mesh.c at the basic nodes. A pass means that the circles lie on the line (relative 1e-12). + ??? info "Test details" - Reference: Analytical limit: the Adams-Williamson pressure profile P(r) = -(rhos g / beta)(exp(beta (R - r)) - 1) in closed form. - Tolerance: rel 1e-12 on the basic-node pressures (observed 6e-15; `mesh.c` evaluates the same closed form through `eos_adamswilliamson.c`, so the agreement is limited by the round trip through the JSON output). - Test id: `tests/test_mesh.py::test_pressure_profile_matches_adams_williamson_closed_form` - - Command: `make`, then `pytest -o addopts= tests/test_mesh.py::test_pressure_profile_matches_adams_williamson_closed_form` + - Command: `make`, then `pytest -o addopts= tests/test_mesh.py::test_pressure_profile_matches_adams_williamson_closed_form` (add `--validation-plots` to write the plot pair) - Companion test: `tests/test_mesh.py::test_mesh_orderings_interleave_and_shell_masses_close` verifies node interleaving and shell mass conservation. ## Related code diff --git a/docs/Validation/reaction.md b/docs/Validation/reaction.md index f1bb3d06..61bf4afd 100644 --- a/docs/Validation/reaction.md +++ b/docs/Validation/reaction.md @@ -20,11 +20,16 @@ The text below details the check, its tolerance, and its test command. **Known limit.** The check covers carbon and hydrogen; volatile oxygen exchanges with the iron buffer of the melt. The test bounds the direction and the size of each transfer, so a wrong equilibrium constant fails only if it reverses a reaction or moves more than 1 % of the water inventory. +![Hydrogen and carbon moved by the H2O/H2 and CO2/CO reactions in 1000 years](../assets/validation/reaction_element_balance.svg#only-light){ width="80%" } +![Hydrogen and carbon moved by the H2O/H2 and CO2/CO reactions in 1000 years](../assets/validation/reaction_element_balance_dark.svg#only-dark){ width="80%" } + +**How to read the plot.** The bars are the asserted element masses moved in the first 1000 yr, on a log scale. For hydrogen: the hydrogen lost by H$_2$ against the hydrogen in the H$_2$O gained; for carbon: the carbon lost by CO against the carbon in the CO$_2$ gained. A pass means that the 2 bars of each pair have the same height (relative 1e-3). + ??? info "Test details" - Reference: Analytical conservation: closed-system elemental budgets across the H2O/H2 and CO2/CO reaction pairs of the Bower et al. (2021) three-ocean hydrogen configuration. - Tolerance: rel 1e-3 on the hydrogen and carbon transfer balances after one 1000-year step (observed at round-off level, below 1e-15). - Test id: `tests/test_reaction.py::test_elements_conserved_across_each_reaction_pair` - - Command: `make`, then `pytest -o addopts= tests/test_reaction.py::test_elements_conserved_across_each_reaction_pair` + - Command: `make`, then `pytest -o addopts= tests/test_reaction.py::test_elements_conserved_across_each_reaction_pair` (add `--validation-plots` to write the plot pair) - Companion test: `tests/test_reaction.py::test_reservoir_closure_for_every_volatile` checks per-volatile mass closure between melt and gas reservoirs. ## Related code diff --git a/docs/Validation/rheologicalfront.md b/docs/Validation/rheologicalfront.md index dd34dbec..06fb2309 100644 --- a/docs/Validation/rheologicalfront.md +++ b/docs/Validation/rheologicalfront.md @@ -20,11 +20,16 @@ The section below details the validation check, its tolerance, and its test comm **Known limit.** The test recomputes the crossing with the same rule as `get_crossover_index` and allows one node of difference, so a one-node misplacement passes; the code takes the first node below `phi_critical` without interpolation. PROTEUS takes `RF_depth` from the dynamic front (`rheological_front_dynamic`, the crossing of the convection regime flag at 1.5, between inviscid (2) and viscous (1) convection), which this test does not cover. +![Melt fraction profile at 1200 yr with the critical melt fraction and the reported front](../assets/validation/rheologicalfront_location.svg#only-light){ width="80%" } +![Melt fraction profile at 1200 yr with the critical melt fraction and the reported front](../assets/validation/rheologicalfront_location_dark.svg#only-dark){ width="80%" } + +**How to read the plot.** The x-axis is the radius and the y-axis the melt fraction at the staggered nodes at 1200 yr. The dashed line is $\phi_c$ = 0.4. The hollow square is the first node below $\phi_c$ that the test finds itself, and the circle the asserted front radius, the planet radius minus the reported depth. A pass means that the circle lies within 1 node of the square on the dashed line. + ??? info "Test details" - Reference: Analytical self-consistency: the reported front index matches the phi_critical = 0.4 crossing of the melt-fraction profile, and the depth equals the surface radius minus the front radius. - Tolerance: Front index within one node of the recomputed crossing; rel 1e-6 on the depth identity. - Test id: `tests/test_rheologicalfront.py::test_front_location_is_consistent_with_melt_fraction_profile` - - Command: `make`, then `pytest -o addopts= tests/test_rheologicalfront.py::test_front_location_is_consistent_with_melt_fraction_profile` + - Command: `make`, then `pytest -o addopts= tests/test_rheologicalfront.py::test_front_location_is_consistent_with_melt_fraction_profile` (add `--validation-plots` to write the plot pair) - Companion test: `tests/test_rheologicalfront.py::test_front_rises_from_the_cmb_as_the_mantle_solidifies` verifies upward front migration as the mantle solidifies. ## Related code diff --git a/docs/Validation/twophase.md b/docs/Validation/twophase.md index 2401cee1..afabf28c 100644 --- a/docs/Validation/twophase.md +++ b/docs/Validation/twophase.md @@ -20,11 +20,16 @@ The text below details the check, its tolerance, and its test command. **Known limit.** The identity compares 2 code paths that share the code's own `mass_s` and `phi_s`, so a wrong shell mass or melt fraction passes. +![Melt fraction profile at 1200 yr with the reported global melt fraction and its means](../assets/validation/twophase_global_melt_fraction.svg#only-light){ width="80%" } +![Melt fraction profile at 1200 yr with the reported global melt fraction and its means](../assets/validation/twophase_global_melt_fraction_dark.svg#only-dark){ width="80%" } + +**How to read the plot.** The x-axis is the radius and the y-axis the melt fraction at the staggered nodes at 1200 yr (solid line, context). The dash-dotted line is the asserted reported phi_global, the dashed line the mass-weighted mean of the profile and the hollow square Mliq / Mmantle; the dotted line is the unweighted mean, which the test requires to be more than 1e-3 away. A pass means that the dash-dotted line, the dashed line and the square coincide (relative 1e-10), above the dotted line. + ??? info "Test details" - Reference: Analytical identity: the global melt fraction equals Mliq/Mmantle and the mass-weighted mean of the staggered melt-fraction profile. - Tolerance: rel 1e-10 on both identities at the part-solidified 1200-year state. - Test id: `tests/test_twophase.py::test_global_melt_fraction_is_the_mass_weighted_profile_mean` - - Command: `make`, then `pytest -o addopts= tests/test_twophase.py::test_global_melt_fraction_is_the_mass_weighted_profile_mean` + - Command: `make`, then `pytest -o addopts= tests/test_twophase.py::test_global_melt_fraction_is_the_mass_weighted_profile_mean` (add `--validation-plots` to write the plot pair) - Companion test: `tests/test_twophase.py::test_mantle_mass_closure_at_every_output_time` asserts mass conservation closure Mliq + Msol = Mmantle across outputs. ## Related code diff --git a/docs/assets/validation/interp_affine.svg b/docs/assets/validation/interp_affine.svg new file mode 100644 index 00000000..b08969f0 --- /dev/null +++ b/docs/assets/validation/interp_affine.svg @@ -0,0 +1,1262 @@ + + + + + + + + test: tests/test_interp.py::test_affine_data_reproduced_exactly_including_last_node; command: pytest tests/test_interp.py::test_affine_data_reproduced_exactly_including_last_node --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[11, 0.0, 10.0, 5.0, 2.0, 1.0, 21.0, 11.0, 4.000000000000001], [2, 0.0, 10.0, 5.0, 5.0, 1.0, 21.0, 11.0, 10.0], [2, 2.5, 2.5, 2.5, 0.0, 5.0, 7.0, 6.0, 1.0], [4, 0.0, 10.0, 4.875, 2.875, 1.0, 21.0, 10.75, 5.749999999999999]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} + image/svg+xml + + + proteus-mpl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/validation/interp_affine_dark.svg b/docs/assets/validation/interp_affine_dark.svg new file mode 100644 index 00000000..a460591a --- /dev/null +++ b/docs/assets/validation/interp_affine_dark.svg @@ -0,0 +1,1262 @@ + + + + + + + + test: tests/test_interp.py::test_affine_data_reproduced_exactly_including_last_node; command: pytest tests/test_interp.py::test_affine_data_reproduced_exactly_including_last_node --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[11, 0.0, 10.0, 5.0, 2.0, 1.0, 21.0, 11.0, 4.000000000000001], [2, 0.0, 10.0, 5.0, 5.0, 1.0, 21.0, 11.0, 10.0], [2, 2.5, 2.5, 2.5, 0.0, 5.0, 7.0, 6.0, 1.0], [4, 0.0, 10.0, 4.875, 2.875, 1.0, 21.0, 10.75, 5.749999999999999]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} + image/svg+xml + + + proteus-mpl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/validation/matprop_viscosity_end_members.svg b/docs/assets/validation/matprop_viscosity_end_members.svg new file mode 100644 index 00000000..4d516056 --- /dev/null +++ b/docs/assets/validation/matprop_viscosity_end_members.svg @@ -0,0 +1,1602 @@ + + + + + + + + test: tests/test_matprop.py::test_viscosity_recovers_configured_end_members; command: pytest tests/test_matprop.py::test_viscosity_recovers_configured_end_members --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[2, 0.0, 1.0, 0.5, 0.5, 2.0, 2.0, 2.0, 0.0], [2, 0.0, 1.0, 0.5, 0.5, 21.0, 21.0, 21.0, 0.0], [50, 3504.049999999999, 6371.000000000025, 4866.723471494237, -498.5081967134211, 2.0, 2.0000031629043207, 2.0000000745089728, 7.397292215463855e-08], [50, 3504.049999999999, 6371.000000000025, 4866.723471494237, -498.5081967134211, 4.810682817520478, 20.999981533005712, 20.344882225060445, 0.5264402793220248], [11, 3504.049999999999, 4014.858048715899, 3755.890928534191, -102.18843550412676, 20.967925855330165, 20.999981533005712, 20.99405498434106, 0.004804474448901152]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} + image/svg+xml + + + proteus-mpl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/validation/matprop_viscosity_end_members_dark.svg b/docs/assets/validation/matprop_viscosity_end_members_dark.svg new file mode 100644 index 00000000..ca4866f9 --- /dev/null +++ b/docs/assets/validation/matprop_viscosity_end_members_dark.svg @@ -0,0 +1,1602 @@ + + + + + + + + test: tests/test_matprop.py::test_viscosity_recovers_configured_end_members; command: pytest tests/test_matprop.py::test_viscosity_recovers_configured_end_members --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[2, 0.0, 1.0, 0.5, 0.5, 2.0, 2.0, 2.0, 0.0], [2, 0.0, 1.0, 0.5, 0.5, 21.0, 21.0, 21.0, 0.0], [50, 3504.049999999999, 6371.000000000025, 4866.723471494237, -498.5081967134211, 2.0, 2.0000031629043207, 2.0000000745089728, 7.397292215463855e-08], [50, 3504.049999999999, 6371.000000000025, 4866.723471494237, -498.5081967134211, 4.810682817520478, 20.999981533005712, 20.344882225060445, 0.5264402793220248], [11, 3504.049999999999, 4014.858048715899, 3755.890928534191, -102.18843550412676, 20.967925855330165, 20.999981533005712, 20.99405498434106, 0.004804474448901152]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} + image/svg+xml + + + proteus-mpl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/validation/mesh_pressure_profile.svg b/docs/assets/validation/mesh_pressure_profile.svg new file mode 100644 index 00000000..cf9bfe7e --- /dev/null +++ b/docs/assets/validation/mesh_pressure_profile.svg @@ -0,0 +1,1632 @@ + + + + + + + + test: tests/test_mesh.py::test_pressure_profile_matches_adams_williamson_closed_form; command: pytest tests/test_mesh.py::test_pressure_profile_matches_adams_williamson_closed_form --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[31, 3504.049999999999, 6371.0, 4937.525, 509.6800000000001, 0.0, 137.72200033065965, 65.33154783201554, -24.44661372592499], [31, 3504.049999999999, 6371.0, 4937.525, 509.6800000000001, 0.0, 116.94148441698714, 58.470742208493554, -20.789597229686603], [31, 3504.049999999999, 6371.0, 4937.525, 509.6800000000001, -0.0, 100.13964549558324, 52.6361382573186, -17.775484136187046], [50, 3504.049999999999, 6371.000000000025, 4866.723471494237, -498.5081967134211, -9.777928437855393e-13, 137.7220003306597, 68.70525724786313, 24.019045559984068]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} + image/svg+xml + + + proteus-mpl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/validation/mesh_pressure_profile_dark.svg b/docs/assets/validation/mesh_pressure_profile_dark.svg new file mode 100644 index 00000000..0997202e --- /dev/null +++ b/docs/assets/validation/mesh_pressure_profile_dark.svg @@ -0,0 +1,1632 @@ + + + + + + + + test: tests/test_mesh.py::test_pressure_profile_matches_adams_williamson_closed_form; command: pytest tests/test_mesh.py::test_pressure_profile_matches_adams_williamson_closed_form --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[31, 3504.049999999999, 6371.0, 4937.525, 509.6800000000001, 0.0, 137.72200033065965, 65.33154783201554, -24.44661372592499], [31, 3504.049999999999, 6371.0, 4937.525, 509.6800000000001, 0.0, 116.94148441698714, 58.470742208493554, -20.789597229686603], [31, 3504.049999999999, 6371.0, 4937.525, 509.6800000000001, -0.0, 100.13964549558324, 52.6361382573186, -17.775484136187046], [50, 3504.049999999999, 6371.000000000025, 4866.723471494237, -498.5081967134211, -9.777928437855393e-13, 137.7220003306597, 68.70525724786313, 24.019045559984068]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} + image/svg+xml + + + proteus-mpl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/validation/reaction_element_balance.svg b/docs/assets/validation/reaction_element_balance.svg new file mode 100644 index 00000000..2eaa67d8 --- /dev/null +++ b/docs/assets/validation/reaction_element_balance.svg @@ -0,0 +1,1262 @@ + + + + + + + + test: tests/test_reaction.py::test_elements_conserved_across_each_reaction_pair; command: pytest tests/test_reaction.py::test_elements_conserved_across_each_reaction_pair --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "log"], "categories": [null, null], "lines": [], "collections": [], "values": [], "value_scales": [], "patches": [[8, -0.4, 1.4, 0.5, 0.22857142857142854, 17.299802458049594, 18.009374483106537, 17.654588470578066, 0.10136743215099084]], "images": [], "image_scales": [], "extents": []}]} + image/svg+xml + + + proteus-mpl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/validation/reaction_element_balance_dark.svg b/docs/assets/validation/reaction_element_balance_dark.svg new file mode 100644 index 00000000..fc0146e9 --- /dev/null +++ b/docs/assets/validation/reaction_element_balance_dark.svg @@ -0,0 +1,1262 @@ + + + + + + + + test: tests/test_reaction.py::test_elements_conserved_across_each_reaction_pair; command: pytest tests/test_reaction.py::test_elements_conserved_across_each_reaction_pair --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "log"], "categories": [null, null], "lines": [], "collections": [], "values": [], "value_scales": [], "patches": [[8, -0.4, 1.4, 0.5, 0.22857142857142854, 17.299802458049594, 18.009374483106537, 17.654588470578066, 0.10136743215099084]], "images": [], "image_scales": [], "extents": []}]} + image/svg+xml + + + proteus-mpl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/validation/rheologicalfront_location.svg b/docs/assets/validation/rheologicalfront_location.svg new file mode 100644 index 00000000..f83c19f0 --- /dev/null +++ b/docs/assets/validation/rheologicalfront_location.svg @@ -0,0 +1,1436 @@ + + + + + + + + test: tests/test_rheologicalfront.py::test_front_location_is_consistent_with_melt_fraction_profile; command: pytest tests/test_rheologicalfront.py::test_front_location_is_consistent_with_melt_fraction_profile --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[49, 3528.420367522105, 6337.615609804658, 4865.232006967509, -488.80990442400326, 0.3895779609783869, 0.705065248642743, 0.4906220970877643, -0.03989934434113623], [2, 0.0, 1.0, 0.5, 0.5, 0.4, 0.4, 0.4, 0.0], [1, 3677.3667380927054, 3677.3667380927054, 3677.3667380927054, 0.0, 0.3995477556123374, 0.3995477556123374, 0.3995477556123374, 0.0], [1, 3702.6299093603534, 3702.6299093603534, 3702.6299093603534, 0.0, 0.4, 0.4, 0.4, 0.0]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} + image/svg+xml + + + proteus-mpl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/validation/rheologicalfront_location_dark.svg b/docs/assets/validation/rheologicalfront_location_dark.svg new file mode 100644 index 00000000..147653c8 --- /dev/null +++ b/docs/assets/validation/rheologicalfront_location_dark.svg @@ -0,0 +1,1436 @@ + + + + + + + + test: tests/test_rheologicalfront.py::test_front_location_is_consistent_with_melt_fraction_profile; command: pytest tests/test_rheologicalfront.py::test_front_location_is_consistent_with_melt_fraction_profile --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[49, 3528.420367522105, 6337.615609804658, 4865.232006967509, -488.80990442400326, 0.3895779609783869, 0.705065248642743, 0.4906220970877643, -0.03989934434113623], [2, 0.0, 1.0, 0.5, 0.5, 0.4, 0.4, 0.4, 0.0], [1, 3677.3667380927054, 3677.3667380927054, 3677.3667380927054, 0.0, 0.3995477556123374, 0.3995477556123374, 0.3995477556123374, 0.0], [1, 3702.6299093603534, 3702.6299093603534, 3702.6299093603534, 0.0, 0.4, 0.4, 0.4, 0.0]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} + image/svg+xml + + + proteus-mpl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/validation/twophase_global_melt_fraction.svg b/docs/assets/validation/twophase_global_melt_fraction.svg new file mode 100644 index 00000000..1f86d1ab --- /dev/null +++ b/docs/assets/validation/twophase_global_melt_fraction.svg @@ -0,0 +1,1500 @@ + + + + + + + + test: tests/test_twophase.py::test_global_melt_fraction_is_the_mass_weighted_profile_mean; command: pytest tests/test_twophase.py::test_global_melt_fraction_is_the_mass_weighted_profile_mean --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[49, 3528.420367522105, 6337.615609804658, 4865.232006967509, -488.80990442400326, 0.3895779609783869, 0.705065248642743, 0.4906220970877643, -0.03989934434113623], [2, 0.0, 1.0, 0.5, 0.5, 0.5129281701005098, 0.5129281701005098, 0.5129281701005098, 0.0], [2, 0.0, 1.0, 0.5, 0.5, 0.5129281701005103, 0.5129281701005103, 0.5129281701005103, 0.0], [2, 0.0, 1.0, 0.5, 0.5, 0.4906220970877643, 0.4906220970877643, 0.4906220970877643, 0.0], [1, 4865.232006967509, 4865.232006967509, 4865.232006967509, 0.0, 0.51292817010051, 0.51292817010051, 0.51292817010051, 0.0]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} + image/svg+xml + + + proteus-mpl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/validation/twophase_global_melt_fraction_dark.svg b/docs/assets/validation/twophase_global_melt_fraction_dark.svg new file mode 100644 index 00000000..5ba5d86a --- /dev/null +++ b/docs/assets/validation/twophase_global_melt_fraction_dark.svg @@ -0,0 +1,1500 @@ + + + + + + + + test: tests/test_twophase.py::test_global_melt_fraction_is_the_mass_weighted_profile_mean; command: pytest tests/test_twophase.py::test_global_melt_fraction_is_the_mass_weighted_profile_mean --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[49, 3528.420367522105, 6337.615609804658, 4865.232006967509, -488.80990442400326, 0.3895779609783869, 0.705065248642743, 0.4906220970877643, -0.03989934434113623], [2, 0.0, 1.0, 0.5, 0.5, 0.5129281701005098, 0.5129281701005098, 0.5129281701005098, 0.0], [2, 0.0, 1.0, 0.5, 0.5, 0.5129281701005103, 0.5129281701005103, 0.5129281701005103, 0.0], [2, 0.0, 1.0, 0.5, 0.5, 0.4906220970877643, 0.4906220970877643, 0.4906220970877643, 0.0], [1, 4865.232006967509, 4865.232006967509, 4865.232006967509, 0.0, 0.51292817010051, 0.51292817010051, 0.51292817010051, 0.0]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} + image/svg+xml + + + proteus-mpl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/test_interp.py b/tests/test_interp.py index 08dd2824..cb6822a1 100644 --- a/tests/test_interp.py +++ b/tests/test_interp.py @@ -52,7 +52,9 @@ def plane_table_2d(tmp_path_factory): @pytest.mark.physics_invariant @pytest.mark.reference_pinned -def test_affine_data_reproduced_exactly_including_last_node(c_test, linear_table_1d): +def test_affine_data_reproduced_exactly_including_last_node( + c_test, linear_table_1d, validation_plot +): """Piecewise-linear interpolation reproduces y = 2x + 1 exactly. Analytical limit: linear interpolation is exact on affine data at @@ -75,6 +77,32 @@ def test_affine_data_reproduced_exactly_including_last_node(c_test, linear_table assert out['y_1d'][1] > 0 assert 1.0 < out['y_1d'][1] < 21.0 + def figure(): + import matplotlib.pyplot as plt + + nodes = list(range(11)) + fig, ax = plt.subplots(figsize=(6, 4)) + ax.plot( + nodes, + [2 * x + 1 for x in nodes], + 's', + mfc='none', + ms=8, + label='Table nodes of $y = 2x + 1$', + ) + ax.plot([0.0, 10.0], [1.0, 21.0], label='$y = 2x + 1$') + ax.plot([2.5, 2.5], [5.0, 7.0], 'x', ms=10, label='Nearest nodes at $x$ = 2.5') + ax.plot( + [0.0, 2.5, 7.0, 10.0], out['y_1d'], 'o', zorder=3, label='interp.c at the 4 queries' + ) + ax.set_xlabel('$x$') + ax.set_ylabel('$y$') + ax.legend(fontsize=8) + fig.tight_layout() + return fig + + validation_plot(figure, 'interp_affine') + @pytest.mark.physics_invariant def test_out_of_range_queries_clamp_to_table_edges(c_test, linear_table_1d): diff --git a/tests/test_matprop.py b/tests/test_matprop.py index f89d0924..87ced9c9 100644 --- a/tests/test_matprop.py +++ b/tests/test_matprop.py @@ -25,7 +25,9 @@ @pytest.mark.physics_invariant @pytest.mark.reference_pinned -def test_viscosity_recovers_configured_end_members(blackbody_short, cached_spider_run): +def test_viscosity_recovers_configured_end_members( + blackbody_short, cached_spider_run, validation_plot +): """The phase-blended viscosity reaches both configured end members. Analytical anchor: in the single-phase limits the two-phase blend @@ -77,6 +79,31 @@ def test_viscosity_recovers_configured_end_members(blackbody_short, cached_spide assert np.all(visc_hot > 0) assert np.all(visc_cold > 0) + def figure(): + import matplotlib.pyplot as plt + + r_hot = data_si(doc_hot, 'radius_b') / 1e3 + r_cold = data_si(doc_cold, 'radius_b') / 1e3 + fig, ax = plt.subplots(figsize=(6, 4)) + ax.axhline(MELT_LOG10_VISC, ls='--', label='melt_log10visc = 2') + ax.axhline(SOLID_LOG10_VISC, ls=':', label='solid_log10visc = 21') + ax.plot(r_hot, np.log10(visc_hot), label='Hot IC, all molten') + ax.plot(r_cold, np.log10(visc_cold), '-.', label='Cold IC, 1600 J kg$^{-1}$ K$^{-1}$') + ax.plot( + r_cold[solid], + np.log10(visc_cold[solid]), + 'o', + zorder=3, + label='Nodes with $\\phi < 10^{-3}$', + ) + ax.set_xlabel('Radius [km]') + ax.set_ylabel('log$_{10}$ viscosity [Pa s]') + ax.legend(fontsize=8) + fig.tight_layout() + return fig + + validation_plot(figure, 'matprop_viscosity_end_members') + @pytest.mark.physics_invariant def test_material_properties_positive_and_conductivity_constant(blackbody_full): diff --git a/tests/test_mesh.py b/tests/test_mesh.py index 0bbeb49c..e080d818 100644 --- a/tests/test_mesh.py +++ b/tests/test_mesh.py @@ -34,7 +34,9 @@ @pytest.mark.physics_invariant @pytest.mark.reference_pinned -def test_pressure_profile_matches_adams_williamson_closed_form(blackbody_short): +def test_pressure_profile_matches_adams_williamson_closed_form( + blackbody_short, validation_plot +): """Basic-node pressures reproduce the analytical Adams-Williamson profile. Analytical anchor: the hydrostatic Adams-Williamson solution @@ -71,6 +73,35 @@ def test_pressure_profile_matches_adams_williamson_closed_form(blackbody_short): assert abs(p_cmb - linear) > 0.1 * p_cmb assert abs(p_cmb - flipped_beta) > 0.1 * p_cmb + def figure(): + import matplotlib.pyplot as plt + + r = np.linspace(radius_b[-1], RADIUS, 31) + depth = RADIUS - r + fig, ax = plt.subplots(figsize=(6, 4)) + ax.plot( + r / 1e3, + -RHOS * GRAVITY / BETA * (np.exp(BETA * depth) - 1.0) / 1e9, + label='Adams-Williamson closed form', + ) + ax.plot(r / 1e3, RHOS * abs(GRAVITY) * depth / 1e9, '--', label='Constant density') + ax.plot( + r / 1e3, + RHOS * GRAVITY / BETA * (np.exp(-BETA * depth) - 1.0) / 1e9, + ':', + label=r'Sign-flipped $\beta$', + ) + ax.plot( + radius_b / 1e3, pressure_b / 1e9, 'o', ms=3, zorder=3, label='mesh.c basic nodes' + ) + ax.set_xlabel('Radius [km]') + ax.set_ylabel('Pressure [GPa]') + ax.legend(fontsize=8) + fig.tight_layout() + return fig + + validation_plot(figure, 'mesh_pressure_profile') + def _aw_mass_antiderivative(r: np.ndarray) -> np.ndarray: """Antiderivative of 4*pi*rho_AW(r)*r^2 for the shell-mass integral.""" diff --git a/tests/test_reaction.py b/tests/test_reaction.py index 7db8d271..1919a16b 100644 --- a/tests/test_reaction.py +++ b/tests/test_reaction.py @@ -118,7 +118,7 @@ def test_reaction_transfer_balances_the_initial_inventory(reaction_run): @pytest.mark.physics_invariant @pytest.mark.reference_pinned -def test_elements_conserved_across_each_reaction_pair(reaction_run): +def test_elements_conserved_across_each_reaction_pair(reaction_run, validation_plot): """Hydrogen and carbon move between partners without loss. Analytical anchor: closed-system elemental conservation for the @@ -156,6 +156,31 @@ def test_elements_conserved_across_each_reaction_pair(reaction_run): assert 1e16 < abs(h2o['reaction_kg']) < 1e20 assert abs(h2o['reaction_kg']) < 0.01 * h2o['initial_kg'] + def figure(): + import matplotlib.pyplot as plt + + fig, ax = plt.subplots(figsize=(6, 4)) + ax.bar( + [-0.2, 0.8], + [h2['reaction_kg'], c_lost_by_co], + width=0.4, + label='Lost by H$_2$ (H), by CO (C)', + ) + ax.bar( + [0.2, 1.2], + [h_from_h2o, c_gained_by_co2], + width=0.4, + label='Gained in H$_2$O (H), in CO$_2$ (C)', + ) + ax.set_xticks([0, 1], ['Hydrogen', 'Carbon']) + ax.set_yscale('log') + ax.set_ylabel('Mass moved in 1000 yr [kg]') + ax.legend(fontsize=8) + fig.tight_layout() + return fig + + validation_plot(figure, 'reaction_element_balance') + # The full named-reaction library configuration: seven volatiles and # the IVTANTHERMO water, IVTANTHERMO carbon dioxide, IVTANTHERMO @@ -197,8 +222,7 @@ def _library_element_moles(doc, element): for volatile in LIBRARY_VOLATILES: block = doc['atmosphere'][volatile] kg = sum( - float(field_si(block[key])[0]) - for key in ('liquid_kg', 'solid_kg', 'atmosphere_kg') + float(field_si(block[key])[0]) for key in ('liquid_kg', 'solid_kg', 'atmosphere_kg') ) moles = kg / LIBRARY_MOLAR_MASS[volatile] total += moles * LIBRARY_STOICHIOMETRY[volatile].get(element, 0) @@ -232,8 +256,7 @@ def test_reaction_library_conserves_h_c_n(library_run): # requested 20 percent molar share in place. n2_block = doc_0['atmosphere']['N2'] n2_kg = sum( - float(field_si(n2_block[key])[0]) - for key in ('liquid_kg', 'solid_kg', 'atmosphere_kg') + float(field_si(n2_block[key])[0]) for key in ('liquid_kg', 'solid_kg', 'atmosphere_kg') ) n_total = _library_element_moles(doc_0, 'N') assert 2.0 * n2_kg / LIBRARY_MOLAR_MASS['N2'] > 0.99 * n_total diff --git a/tests/test_rheologicalfront.py b/tests/test_rheologicalfront.py index 6373ce15..90c3e98c 100644 --- a/tests/test_rheologicalfront.py +++ b/tests/test_rheologicalfront.py @@ -35,7 +35,9 @@ def _front_scalars(doc: dict) -> dict: @pytest.mark.physics_invariant @pytest.mark.reference_pinned -def test_front_location_is_consistent_with_melt_fraction_profile(blackbody_full): +def test_front_location_is_consistent_with_melt_fraction_profile( + blackbody_full, validation_plot +): """The reported front sits where phi_s crosses phi_critical. Anchor: analytical self-consistency with the melt-fraction profile @@ -75,6 +77,36 @@ def test_front_location_is_consistent_with_melt_fraction_profile(blackbody_full) assert 0 < front['depth'] < MANTLE_DEPTH assert 0 < front['pressure'] < pressure_b.max() + def figure(): + import matplotlib.pyplot as plt + + r_s = data_si(doc, 'radius_s') / 1e3 + fig, ax = plt.subplots(figsize=(6, 4)) + ax.plot(r_s, phi_s, label='Melt fraction at 1200 yr') + ax.axhline(PHI_CRITICAL, ls='--', label='$\\phi_c$ = 0.4') + ax.plot( + [r_s[crossing]], + [phi_s[crossing]], + 's', + mfc='none', + ms=10, + label='First node below $\\phi_c$', + ) + ax.plot( + [(radius_b[0] - front['depth']) / 1e3], + [PHI_CRITICAL], + 'o', + zorder=3, + label='Reported front radius', + ) + ax.set_xlabel('Radius [km]') + ax.set_ylabel('Melt fraction') + ax.legend(fontsize=8) + fig.tight_layout() + return fig + + validation_plot(figure, 'rheologicalfront_location') + @pytest.mark.physics_invariant def test_front_rises_from_the_cmb_as_the_mantle_solidifies(blackbody_full): diff --git a/tests/test_twophase.py b/tests/test_twophase.py index 68d60fb7..a8cd55d1 100644 --- a/tests/test_twophase.py +++ b/tests/test_twophase.py @@ -70,7 +70,9 @@ def test_solidification_proceeds_monotonically_under_cooling(blackbody_full): @pytest.mark.physics_invariant @pytest.mark.reference_pinned -def test_global_melt_fraction_is_the_mass_weighted_profile_mean(blackbody_full): +def test_global_melt_fraction_is_the_mass_weighted_profile_mean( + blackbody_full, validation_plot +): """phi_global equals both Mliq/Mmantle and the mass-weighted phi_s mean. Analytical identity: the global melt fraction reported with the @@ -98,3 +100,21 @@ def test_global_melt_fraction_is_the_mass_weighted_profile_mean(blackbody_full): # Boundedness and scale guards. assert 0.0 <= phi_reported <= 1.0 assert 0.2 < phi_reported < 0.9 + + def figure(): + import matplotlib.pyplot as plt + + r_s = data_si(doc, 'radius_s') / 1e3 + fig, ax = plt.subplots(figsize=(6, 4)) + ax.plot(r_s, phi_s, label='Melt fraction at 1200 yr') + ax.axhline(phi_reported, ls='-.', label='Reported phi_global') + ax.axhline(weighted, ls='--', label='Mass-weighted mean') + ax.axhline(unweighted, ls=':', label='Unweighted mean') + ax.plot([r_s.mean()], [m_liq / m_man], 's', mfc='none', ms=10, label='Mliq / Mmantle') + ax.set_xlabel('Radius [km]') + ax.set_ylabel('Melt fraction') + ax.legend(fontsize=8) + fig.tight_layout() + return fig + + validation_plot(figure, 'twophase_global_melt_fraction') From c6a2a404d8cfe581d7c2e0bf99fdef4f4f4c79fd Mon Sep 17 00:00:00 2001 From: timlichtenberg Date: Mon, 26 Oct 2026 17:30:08 +0100 Subject: [PATCH 08/15] Require proteus-mpl 1.5.0 in the test jobs The committed validation plots are written and compared with the proteus-mpl 1.5.0 fixture. --- .github/workflows/ci.yml | 4 ++-- .github/workflows/nightly.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6eba33ec..b23ef936 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,7 +59,7 @@ jobs: key: ${{ runner.os }}-petsc-63b72503 fail-on-cache-miss: true - name: Install Python requirements - run: pip install -r py/requirements.txt pytest pytest-timeout ruff gcovr 'proteus-mpl>=1.4.0' + run: pip install -r py/requirements.txt pytest pytest-timeout ruff gcovr 'proteus-mpl>=1.5.0' - name: Build with coverage instrumentation run: | make -j CFLAGS_EXTRA="--coverage" @@ -149,7 +149,7 @@ jobs: steps: - uses: actions/checkout@v6 - name: Install Python requirements - run: pip install -r py/requirements.txt pytest pytest-timeout 'proteus-mpl>=1.4.0' + run: pip install -r py/requirements.txt pytest pytest-timeout 'proteus-mpl>=1.5.0' - name: Run installer run: ./tools/get_spider.sh - name: Check the installer produced a runnable binary diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index e4f0d17d..1e20023d 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -32,7 +32,7 @@ jobs: ./configure --with-fc=0 --with-cxx=0 --with-cc=gcc --download-sundials2 --download-mpich --download-f2cblaslapack PETSC_ARCH=arch-test make all - name: Install Python requirements - run: pip install -r py/requirements.txt pytest pytest-timeout gcovr 'proteus-mpl>=1.4.0' + run: pip install -r py/requirements.txt pytest pytest-timeout gcovr 'proteus-mpl>=1.5.0' - name: Build with coverage instrumentation run: | make -j CFLAGS_EXTRA="--coverage" From e71833a23a8631c24a16bed6406944ff3b36f68a Mon Sep 17 00:00:00 2001 From: timlichtenberg Date: Mon, 26 Oct 2026 19:06:01 +0100 Subject: [PATCH 09/15] Plot the initial-condition cases The adiabat, abundance and ocean-moles cases draw their asserted profile, inventories and element ratios, written with the PETSc build of the CI jobs. --- docs/Validation/ic.md | 21 +- .../validation/ic_abundance_inventory.svg | 1552 +++++++++++++++++ .../ic_abundance_inventory_dark.svg | 1552 +++++++++++++++++ docs/assets/validation/ic_adiabat_profile.svg | 1536 ++++++++++++++++ .../validation/ic_adiabat_profile_dark.svg | 1536 ++++++++++++++++ docs/assets/validation/ic_ocean_moles.svg | 1409 +++++++++++++++ .../assets/validation/ic_ocean_moles_dark.svg | 1409 +++++++++++++++ tests/test_ic.py | 85 +- 8 files changed, 9088 insertions(+), 12 deletions(-) create mode 100644 docs/assets/validation/ic_abundance_inventory.svg create mode 100644 docs/assets/validation/ic_abundance_inventory_dark.svg create mode 100644 docs/assets/validation/ic_adiabat_profile.svg create mode 100644 docs/assets/validation/ic_adiabat_profile_dark.svg create mode 100644 docs/assets/validation/ic_ocean_moles.svg create mode 100644 docs/assets/validation/ic_ocean_moles_dark.svg diff --git a/docs/Validation/ic.md b/docs/Validation/ic.md index c7950a5d..ba9b792d 100644 --- a/docs/Validation/ic.md +++ b/docs/Validation/ic.md @@ -20,11 +20,16 @@ The sections below detail three validation cases, their tolerances, and their te **Known limit.** An error in the entropy gradient of the initial profile (`ic_dsdr`) is caught only if it moves the profile mean outside 2600 to 2628.7 J/kg/K. The check does not cover non-adiabatic initial profiles. +![Initial entropy profiles for ic_adiabat_entropy 2600 and 2400](../assets/validation/ic_adiabat_profile.svg#only-light){ width="80%" } +![Initial entropy profiles for ic_adiabat_entropy 2600 and 2400](../assets/validation/ic_adiabat_profile_dark.svg#only-dark){ width="80%" } + +**How to read the plot.** The x-axis is the radius and the y-axis the entropy at the staggered nodes at $t$ = 0. The solid line is the asserted profile for ic_adiabat_entropy = 2600 and the dashed line the rerun with 2400, which the test requires to be 200 J kg$^{-1}$ K$^{-1}$ lower on average. The circle is the asserted top staggered node at 2600, the dotted line the mean of the 2600 profile and the grey band its asserted range, 2600 to 2628.7. A pass means that the circle sits at 2600, the dotted line inside the band, and the dashed line 200 below the solid one. + ??? info "Test details" - Reference: Analytical limit: the initial condition prescribes the adiabat entropy (2600 J/kg/K at the top staggered node) with the configured small gradient ramp. - Tolerance: rel 1e-3 at the top node (observed exact); the profile mean lies between the adiabat entropy and that value plus |ic_dsdr| times the mantle depth; the 2400 J/kg/K run shifts the mean by 200 J/kg/K to rel 1e-9. - Test id: `tests/test_ic.py::test_prescribed_adiabat_entropy_sets_the_initial_profile` - - Command: `make`, then `pytest -o addopts= tests/test_ic.py::test_prescribed_adiabat_entropy_sets_the_initial_profile` + - Command: `make`, then `pytest -o addopts= tests/test_ic.py::test_prescribed_adiabat_entropy_sets_the_initial_profile` (add `--validation-plots` to write the plot pair) - Companion test: `tests/test_ic.py::test_initial_adiabat_is_nearly_flat_until_cooling_imprints_structure` verifies that cooling breaks initial profile uniformity. ## Abundance IC realises the requested inventory @@ -39,11 +44,16 @@ The sections below detail three validation cases, their tolerances, and their te **Known limit.** Volatile oxygen mass is not strictly conserved by volatile species alone because the melt iron buffer exchanges oxygen with dissolved gases. +![Requested and realised volatile masses and the realised over requested element moles](../assets/validation/ic_abundance_inventory.svg#only-light){ width="80%" } +![Requested and realised volatile masses and the realised over requested element moles](../assets/validation/ic_abundance_inventory_dark.svg#only-dark){ width="80%" } + +**How to read the plot.** Left: the grey bars are the requested masses of the 4 volatiles, the requested ppm of the mantle mass, which the test asserts to equal the stored initial_kg (relative 1e-9); the circles are the realised reservoirs after the initial partial-pressure solve, on a log scale. The reactions move mass between the partners, so H$_2$ and CO$_2$ end below their bars and H$_2$O and CO above. Right: the realised over the requested moles of H, C and O, with the dashed line at 1. The test asserts that H and C sit on 1 (relative 1e-9) and that O departs by more than 1e-4 (here 2.2e-3), because the reactions exchange oxygen with the fO$_2$ buffer of the melt. A pass means that the H and C circles sit on the line and the O circle above it. + ??? info "Test details" - Reference: Mass-balance identity through the initial partial-pressure solve: initial_kg equals the requested ppm of the mantle mass per volatile, and the realised reservoirs carry the requested hydrogen and carbon mole totals (conserved by the water and carbon dioxide reactions). - Tolerance: rel 1e-9 on the stored inventories and the H and C totals (observed below 1e-15). - Test id: `tests/test_ic.py::test_abundance_ic_realises_the_requested_inventory` - - Command: `make`, then `pytest -o addopts= tests/test_ic.py::test_abundance_ic_realises_the_requested_inventory` + - Command: `make`, then `pytest -o addopts= tests/test_ic.py::test_abundance_ic_realises_the_requested_inventory` (add `--validation-plots` to write the plot pair) - Companion test: `tests/test_ic.py::test_abundance_and_ocean_mole_solves_agree` verifies consistency between abundance and ocean-mole initialization paths. ## Ocean-moles IC converts moles to mass @@ -58,11 +68,16 @@ The sections below detail three validation cases, their tolerances, and their te **Known limit.** The test uses its own copy of the OCEAN_MOLES constant in `constants.c`, so it checks the conversion, not the definition of the terrestrial ocean mass; a value that is wrong in both places passes. +![Stored initial inventories against moles times OCEAN_MOLES times molar mass](../assets/validation/ic_ocean_moles.svg#only-light){ width="80%" } +![Stored initial inventories against moles times OCEAN_MOLES times molar mass](../assets/validation/ic_ocean_moles_dark.svg#only-dark){ width="80%" } + +**How to read the plot.** The bars are the closed-form masses, moles times OCEAN_MOLES times molar mass, of the 4 volatiles on a log scale, and the circles the stored initial_kg of the run, which the test asserts to match to relative 1e-9. The cross is the CO$_2$ inventory with the CO molar mass, which the test requires to be more than 30 % away. A pass means that each circle sits on the top of its bar and the cross lies well below the CO$_2$ bar. + ??? info "Test details" - Reference: The OCEAN_MOLES constant of constants.c (7.68894973907177e22 mol per Earth ocean): initial_kg equals moles times OCEAN_MOLES times the volatile molar mass. - Tolerance: rel 1e-9 (closed-form product of stored constants). - Test id: `tests/test_ic.py::test_ocean_moles_ic_converts_moles_to_mass` - - Command: `make`, then `pytest -o addopts= tests/test_ic.py::test_ocean_moles_ic_converts_moles_to_mass` + - Command: `make`, then `pytest -o addopts= tests/test_ic.py::test_ocean_moles_ic_converts_moles_to_mass` (add `--validation-plots` to write the plot pair) - Companion test: `tests/test_ic.py::test_abundance_and_ocean_mole_solves_agree` confirms that the ocean-mole and ppm abundance formulations give the same partial pressures to rel 5e-3. ## Related code diff --git a/docs/assets/validation/ic_abundance_inventory.svg b/docs/assets/validation/ic_abundance_inventory.svg new file mode 100644 index 00000000..b2e0058e --- /dev/null +++ b/docs/assets/validation/ic_abundance_inventory.svg @@ -0,0 +1,1552 @@ + + + + + + + + test: tests/test_ic.py::test_abundance_ic_realises_the_requested_inventory; command: pytest tests/test_ic.py::test_abundance_ic_realises_the_requested_inventory --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "log"], "categories": [null, null], "lines": [[4, 0.0, 3.0, 1.5, 0.8333333333333333, 18.073893106258954, 21.528734140550494, 19.697505524109076, -0.303193072566026]], "collections": [], "values": [], "value_scales": [], "patches": [[8, -0.3, 3.3000000000000003, 1.5, 0.6714285714285714, 18.40225394065729, 21.52719267726559, 19.819505635896935, -0.2768932611453794]], "images": [], "image_scales": [], "extents": []}, {"scales": ["linear", "linear"], "categories": [null, null], "lines": [[2, 0.0, 1.0, 0.5, 0.5, 1.0, 1.0, 1.0, 0.0], [3, 0.0, 2.0, 1.0, 0.6666666666666666, 0.9999999999999996, 1.0021916362087129, 1.000730545402904, 0.0007305454029043654]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} + image/svg+xml + + + proteus-mpl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/validation/ic_abundance_inventory_dark.svg b/docs/assets/validation/ic_abundance_inventory_dark.svg new file mode 100644 index 00000000..e1e083a1 --- /dev/null +++ b/docs/assets/validation/ic_abundance_inventory_dark.svg @@ -0,0 +1,1552 @@ + + + + + + + + test: tests/test_ic.py::test_abundance_ic_realises_the_requested_inventory; command: pytest tests/test_ic.py::test_abundance_ic_realises_the_requested_inventory --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "log"], "categories": [null, null], "lines": [[4, 0.0, 3.0, 1.5, 0.8333333333333333, 18.073893106258954, 21.528734140550494, 19.697505524109076, -0.303193072566026]], "collections": [], "values": [], "value_scales": [], "patches": [[8, -0.3, 3.3000000000000003, 1.5, 0.6714285714285714, 18.40225394065729, 21.52719267726559, 19.819505635896935, -0.2768932611453794]], "images": [], "image_scales": [], "extents": []}, {"scales": ["linear", "linear"], "categories": [null, null], "lines": [[2, 0.0, 1.0, 0.5, 0.5, 1.0, 1.0, 1.0, 0.0], [3, 0.0, 2.0, 1.0, 0.6666666666666666, 0.9999999999999996, 1.0021916362087129, 1.000730545402904, 0.0007305454029043654]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} + image/svg+xml + + + proteus-mpl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/validation/ic_adiabat_profile.svg b/docs/assets/validation/ic_adiabat_profile.svg new file mode 100644 index 00000000..23d93efc --- /dev/null +++ b/docs/assets/validation/ic_adiabat_profile.svg @@ -0,0 +1,1536 @@ + + + + + + + + test: tests/test_ic.py::test_prescribed_adiabat_entropy_sets_the_initial_profile; command: pytest tests/test_ic.py::test_prescribed_adiabat_entropy_sets_the_initial_profile --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[49, 3528.420367522105, 6337.615609804658, 4865.232006967509, -488.80990442400326, 2600.0, 2628.0920394894074, 2614.7238571696594, 4.888112596199659], [49, 3528.420367522105, 6337.615609804658, 4865.232006967509, -488.80990442400326, 2400.0, 2428.0920394894065, 2414.723857169659, 4.888112596199573], [2, 0.0, 1.0, 0.5, 0.5, 2614.723857169659, 2614.723857169659, 2614.723857169659, 0.0], [1, 6337.615609804658, 6337.615609804658, 6337.615609804658, 0.0, 2600.0, 2600.0, 2600.0, 0.0]], "collections": [], "values": [], "value_scales": [], "patches": [[5, 0.0, 1.0, 0.4, -0.1, 2600.0, 2628.6695, 2611.4678, 2.86695000000004]], "images": [], "image_scales": [], "extents": []}]} + image/svg+xml + + + proteus-mpl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/validation/ic_adiabat_profile_dark.svg b/docs/assets/validation/ic_adiabat_profile_dark.svg new file mode 100644 index 00000000..c53f89db --- /dev/null +++ b/docs/assets/validation/ic_adiabat_profile_dark.svg @@ -0,0 +1,1536 @@ + + + + + + + + test: tests/test_ic.py::test_prescribed_adiabat_entropy_sets_the_initial_profile; command: pytest tests/test_ic.py::test_prescribed_adiabat_entropy_sets_the_initial_profile --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[49, 3528.420367522105, 6337.615609804658, 4865.232006967509, -488.80990442400326, 2600.0, 2628.0920394894074, 2614.7238571696594, 4.888112596199659], [49, 3528.420367522105, 6337.615609804658, 4865.232006967509, -488.80990442400326, 2400.0, 2428.0920394894065, 2414.723857169659, 4.888112596199573], [2, 0.0, 1.0, 0.5, 0.5, 2614.723857169659, 2614.723857169659, 2614.723857169659, 0.0], [1, 6337.615609804658, 6337.615609804658, 6337.615609804658, 0.0, 2600.0, 2600.0, 2600.0, 0.0]], "collections": [], "values": [], "value_scales": [], "patches": [[5, 0.0, 1.0, 0.4, -0.1, 2600.0, 2628.6695, 2611.4678, 2.86695000000004]], "images": [], "image_scales": [], "extents": []}]} + image/svg+xml + + + proteus-mpl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/validation/ic_ocean_moles.svg b/docs/assets/validation/ic_ocean_moles.svg new file mode 100644 index 00000000..d3705c5e --- /dev/null +++ b/docs/assets/validation/ic_ocean_moles.svg @@ -0,0 +1,1409 @@ + + + + + + + + test: tests/test_ic.py::test_ocean_moles_ic_converts_moles_to_mass; command: pytest tests/test_ic.py::test_ocean_moles_ic_converts_moles_to_mass --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "log"], "categories": [null, null], "lines": [[4, 0.0, 3.0, 1.5, 0.8333333333333333, 18.402519302574248, 21.527114312099553, 19.819573513053378, -0.32297968438944646], [1, 2.0, 2.0, 2.0, 0.0, 19.27269938347041, 19.27269938347041, 19.27269938347041, 0.0]], "collections": [], "values": [], "value_scales": [], "patches": [[8, -0.3, 3.3000000000000003, 1.5, 0.6714285714285714, 18.402519302574248, 21.527114312099553, 19.819573513053378, -0.27683972947666996]], "images": [], "image_scales": [], "extents": []}]} + image/svg+xml + + + proteus-mpl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/validation/ic_ocean_moles_dark.svg b/docs/assets/validation/ic_ocean_moles_dark.svg new file mode 100644 index 00000000..3786b292 --- /dev/null +++ b/docs/assets/validation/ic_ocean_moles_dark.svg @@ -0,0 +1,1409 @@ + + + + + + + + test: tests/test_ic.py::test_ocean_moles_ic_converts_moles_to_mass; command: pytest tests/test_ic.py::test_ocean_moles_ic_converts_moles_to_mass --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "log"], "categories": [null, null], "lines": [[4, 0.0, 3.0, 1.5, 0.8333333333333333, 18.402519302574248, 21.527114312099553, 19.819573513053378, -0.32297968438944646], [1, 2.0, 2.0, 2.0, 0.0, 19.27269938347041, 19.27269938347041, 19.27269938347041, 0.0]], "collections": [], "values": [], "value_scales": [], "patches": [[8, -0.3, 3.3000000000000003, 1.5, 0.6714285714285714, 18.402519302574248, 21.527114312099553, 19.819573513053378, -0.27683972947666996]], "images": [], "image_scales": [], "extents": []}]} + image/svg+xml + + + proteus-mpl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/test_ic.py b/tests/test_ic.py index ae9f7bf9..cd38ec1c 100644 --- a/tests/test_ic.py +++ b/tests/test_ic.py @@ -29,7 +29,7 @@ @pytest.mark.physics_invariant @pytest.mark.reference_pinned def test_prescribed_adiabat_entropy_sets_the_initial_profile( - blackbody_short, cached_spider_run + blackbody_short, cached_spider_run, validation_plot ): """The IC recovers ic_adiabat_entropy at the top staggered node. @@ -52,7 +52,8 @@ def test_prescribed_adiabat_entropy_sets_the_initial_profile( # The ic_dsdr ramp bounds the mean from above: the mean must lie # between the top value and top + |ic_dsdr| * mantle depth # (2600 to 2628.7 J/kg/K; observed offset ~14.7 J/kg/K). - assert IC_ENTROPY < s_s.mean() < IC_ENTROPY + abs(IC_DSDR) * MANTLE_DEPTH + mean_hi = IC_ENTROPY + abs(IC_DSDR) * MANTLE_DEPTH + assert IC_ENTROPY < s_s.mean() < mean_hi # Wrong-value discrimination: rerunning the IC with # -ic_adiabat_entropy 2400 shifts every staggered node by exactly @@ -69,6 +70,29 @@ def test_prescribed_adiabat_entropy_sets_the_initial_profile( assert np.all(s_s > 0) assert 1e3 < s_s.mean() < 1e4 + def figure(): + import matplotlib.pyplot as plt + + r_km = data_si(doc, 'radius_s') / 1e3 + fig, ax = plt.subplots(figsize=(6, 4)) + ax.axhspan( + IC_ENTROPY, + mean_hi, + alpha=0.15, + label='Asserted range of the mean', + ) + ax.plot(r_km, s_s, label='ic_adiabat_entropy 2600') + ax.plot(r_km, s_24, '--', label='ic_adiabat_entropy 2400') + ax.axhline(s_s.mean(), ls=':', label='Mean of the 2600 profile') + ax.plot([r_km[0]], [s_s[0]], 'o', zorder=3, label='Top staggered node') + ax.set_xlabel('Radius [km]') + ax.set_ylabel('Entropy [J kg$^{-1}$ K$^{-1}$]') + ax.legend(fontsize=8) + fig.tight_layout() + return fig + + validation_plot(figure, 'ic_adiabat_profile') + @pytest.mark.physics_invariant def test_initial_adiabat_is_nearly_flat_until_cooling_imprints_structure(blackbody_short): @@ -178,7 +202,7 @@ def abundance_ic_run(cached_spider_run): @pytest.mark.physics_invariant @pytest.mark.reference_pinned -def test_abundance_ic_realises_the_requested_inventory(abundance_ic_run): +def test_abundance_ic_realises_the_requested_inventory(abundance_ic_run, validation_plot): """The abundance IC conserves the requested elemental inventories. Anchor: mass-balance identity through the initial-pressure solve. @@ -207,18 +231,16 @@ def test_abundance_ic_realises_the_requested_inventory(abundance_ic_run): # The solve conserves H and C: totals match the request (observed below # 1e-15 relative; rel=1e-9 leaves platform headroom). + moles = {e: (_element_moles(realised, e), _element_moles(requested, e)) for e in 'HCO'} for element in ('H', 'C'): - assert _element_moles(realised, element) == pytest.approx( - _element_moles(requested, element), rel=1e-9 - ) + assert moles[element][0] == pytest.approx(moles[element][1], rel=1e-9) # Oxygen exchange: the realised volatile O inventory departs from # the requested one (observed 2.2e-3 relative; the threshold sits # twentyfold below) because the fO2 buffer participates in both # reactions. An inert fO2 pathway would conserve O to the same # 1e-9 the H and C totals meet. - o_req = _element_moles(requested, 'O') - o_real = _element_moles(realised, 'O') + o_real, o_req = moles['O'] assert abs(o_real - o_req) > 1e-4 * o_req # Edge case: the reactions moved water mass at the IC (observed @@ -226,6 +248,28 @@ def test_abundance_ic_realises_the_requested_inventory(abundance_ic_run): # off the bare request. assert abs(realised['H2O'] - requested['H2O']) > 1e-4 * requested['H2O'] + def figure(): + import matplotlib.pyplot as plt + + names = list(ABUNDANCE_PPM) + x = np.arange(len(names)) + fig, (left, right) = plt.subplots(1, 2, figsize=(8, 3.6)) + left.bar(x, [requested[n] for n in names], width=0.6, alpha=0.35, label='Requested') + left.plot(x, [realised[n] for n in names], 'o', zorder=3, label='Realised reservoirs') + left.set_xticks(x, names) + left.set_yscale('log') + left.set_ylabel('Mass [kg]') + left.legend(fontsize=8) + ratio = [moles[e][0] / moles[e][1] for e in 'HCO'] + right.axhline(1.0, ls='--') + right.plot([0, 1, 2], ratio, 'o') + right.set_xticks([0, 1, 2], ['H', 'C', 'O']) + right.set_ylabel('Realised / requested moles') + fig.tight_layout() + return fig + + validation_plot(figure, 'ic_abundance_inventory') + @pytest.fixture(scope='module') def ocean_moles_ic_run(cached_spider_run): @@ -260,7 +304,7 @@ def ocean_moles_ic_run(cached_spider_run): @pytest.mark.physics_invariant @pytest.mark.reference_pinned -def test_ocean_moles_ic_converts_moles_to_mass(ocean_moles_ic_run): +def test_ocean_moles_ic_converts_moles_to_mass(ocean_moles_ic_run, validation_plot): """IC_ATMOSPHERE 4 converts Earth-ocean mole counts to inventory mass. Anchor: the OCEAN_MOLES constant in constants.c (7.68894974e22 mol @@ -270,11 +314,14 @@ def test_ocean_moles_ic_converts_moles_to_mass(ocean_moles_ic_run): """ doc = read_output(ocean_moles_ic_run, 0) + stored, closed = [], [] for volatile, oceans in MOLES_OCEANS.items(): res = _volatile_reservoirs_kg(doc, volatile) expected = oceans * OCEAN_MOLES * MOLAR_MASS[volatile] # kg # rel=1e-9: a closed-form product of three stored constants. assert res['initial_kg'] == pytest.approx(expected, rel=1e-9) + stored.append(res['initial_kg']) + closed.append(expected) # Molar-mass discrimination: reading the CO2 inventory with the CO # molar mass shifts the expected mass by the mass ratio (36%), far @@ -288,6 +335,26 @@ def test_ocean_moles_ic_converts_moles_to_mass(ocean_moles_ic_run): assert 0 < co2['initial_kg'] < 1e21 # kg assert np.isfinite(co2['initial_kg']) + def figure(): + import matplotlib.pyplot as plt + + names = list(MOLES_OCEANS) + x = np.arange(len(names)) + fig, ax = plt.subplots(figsize=(6, 4)) + ax.bar(x, closed, width=0.6, label='Moles $\\times$ OCEAN_MOLES $\\times$ molar mass') + ax.plot(x, stored, 'o', zorder=3, label='Stored initial_kg') + ax.plot( + [names.index('CO2')], [wrong], 'x', ms=10, label='CO$_2$ with the CO molar mass' + ) + ax.set_xticks(x, names) + ax.set_yscale('log') + ax.set_ylabel('Initial inventory [kg]') + ax.legend(fontsize=8) + fig.tight_layout() + return fig + + validation_plot(figure, 'ic_ocean_moles') + @pytest.mark.physics_invariant def test_abundance_and_ocean_mole_solves_agree(abundance_ic_run, ocean_moles_ic_run): From 41075cb982414ead2014c1b57d4651c33d4d663c Mon Sep 17 00:00:00 2001 From: timlichtenberg Date: Mon, 26 Oct 2026 19:06:01 +0100 Subject: [PATCH 10/15] Draw run output at display precision and name plotted values once Run output moves by up to 5e-6 between PETSc builds, more than the stale-plot check allows. The atmosphere, energy, reaction, front and melt-fraction plots now draw only asserted values, rounded to 2 or 3 significant digits with at least 100 times the measured spread to the next rounding step, and closed forms on fixed grids. Every figure reads the same names, functions and arrays as its asserts, so a changed reference or tolerance moves the plot. The remaining run plots are written with the PETSc build of the CI jobs. --- docs/Validation/atmosphere.md | 2 +- docs/Validation/energy.md | 6 +- docs/Validation/reaction.md | 2 +- docs/Validation/rheologicalfront.md | 6 +- docs/Validation/twophase.md | 6 +- .../validation/atmosphere_grey_body_flux.svg | 4 +- .../atmosphere_grey_body_flux_dark.svg | 4 +- .../bc_constant_entropy_surface.svg | 78 +- .../bc_constant_entropy_surface_dark.svg | 78 +- docs/assets/validation/bc_prescribed_flux.svg | 58 +- .../validation/bc_prescribed_flux_dark.svg | 58 +- .../validation/energy_disabled_transport.svg | 2193 ++++++++--------- .../energy_disabled_transport_dark.svg | 2193 ++++++++--------- .../validation/energy_steady_state_ic.svg | 2 +- .../energy_steady_state_ic_dark.svg | 2 +- .../validation/reaction_element_balance.svg | 631 ++--- .../reaction_element_balance_dark.svg | 631 ++--- .../validation/rheologicalfront_location.svg | 2101 ++++++++-------- .../rheologicalfront_location_dark.svg | 2101 ++++++++-------- .../twophase_global_melt_fraction.svg | 2087 +++++++--------- .../twophase_global_melt_fraction_dark.svg | 2087 +++++++--------- tests/_plot_utils.py | 28 + tests/test_atmosphere.py | 14 +- tests/test_bc.py | 18 +- tests/test_energy.py | 61 +- tests/test_eos.py | 34 +- tests/test_eos_adamswilliamson.py | 12 +- tests/test_eos_composite.py | 19 +- tests/test_interp.py | 26 +- tests/test_mesh.py | 21 +- tests/test_reaction.py | 6 +- tests/test_rheologicalfront.py | 26 +- tests/test_twophase.py | 25 +- 33 files changed, 6939 insertions(+), 7681 deletions(-) create mode 100644 tests/_plot_utils.py diff --git a/docs/Validation/atmosphere.md b/docs/Validation/atmosphere.md index 1d2a3553..968f9dd2 100644 --- a/docs/Validation/atmosphere.md +++ b/docs/Validation/atmosphere.md @@ -23,7 +23,7 @@ The section below details the validation check, its tolerance, and its test comm ![Surface flux of the blackbody50 run at 200 yr on the grey-body law](../assets/validation/atmosphere_grey_body_flux.svg#only-light){ width="80%" } ![Surface flux of the blackbody50 run at 200 yr on the grey-body law](../assets/validation/atmosphere_grey_body_flux_dark.svg#only-dark){ width="80%" } -**How to read the plot.** The x-axis is the surface temperature and the y-axis the surface flux on a log scale. The line is the grey-body law $\epsilon\sigma(T^4 - T_\mathrm{eqm}^4)$ with emissivity 1 and $T_\mathrm{eqm}$ = 273 K, and the dashed line the $T^3$ form that the test rejects (context). The circle is the asserted Fatm of the blackbody50 run at 200 yr, at its reported surface temperature. A pass means that the circle lies on the line (relative 1e-6). +**How to read the plot.** The x-axis is the surface temperature and the y-axis the surface flux on a log scale. The line is the grey-body law $\epsilon\sigma(T^4 - T_\mathrm{eqm}^4)$ with emissivity 1 and $T_\mathrm{eqm}$ = 273 K, and the dashed line the $T^3$ form that the test rejects (context). The circle is the asserted Fatm of the blackbody50 run at 200 yr, at its reported surface temperature; values shown to 3 significant digits, because the run output moves by about 1e-6 between PETSc builds. A pass means that the circle lies on the line (relative 1e-6). ??? info "Test details" - Reference: Analytical limit: the grey-body flux F = emissivity sigma (T_surf^4 - T_eqm^4) with emissivity 1 and T_eqm = 273 K (Stefan-Boltzmann). diff --git a/docs/Validation/energy.md b/docs/Validation/energy.md index c77ea480..9af657d7 100644 --- a/docs/Validation/energy.md +++ b/docs/Validation/energy.md @@ -20,10 +20,10 @@ The sections below detail three validation cases, their tolerances, and their te **Known limit.** For the conduction-only test, the identity holds at interior nodes only, because the surface node transmits the grey-body boundary flux. -![Flux profiles of the conduction-off and the conduction-only runs on a symmetric log axis](../assets/validation/energy_disabled_transport.svg#only-light){ width="80%" } -![Flux profiles of the conduction-off and the conduction-only runs on a symmetric log axis](../assets/validation/energy_disabled_transport_dark.svg#only-dark){ width="80%" } +![Largest flux of each transport term in the conduction-off and the conduction-only runs](../assets/validation/energy_disabled_transport.svg#only-light){ width="80%" } +![Largest flux of each transport term in the conduction-off and the conduction-only runs](../assets/validation/energy_disabled_transport_dark.svg#only-dark){ width="80%" } -**How to read the plot.** The x-axis is the radius and the y-axis the flux at the basic nodes on a symmetric log axis (linear between $-10^{-3}$ and $10^{-3}$). In the run with conduction off, the solid line is the active convective flux and the dashed line the asserted conductive flux, 0. In the run with conduction only, the dash-dotted line is the conductive flux and the dotted line the largest of the asserted convective, mixing and separation fluxes at each node, 0. A pass means that the dashed and dotted lines lie at 0 (within 1e-20 W/m$^2$) while the other 2 stay far above. +**How to read the plot.** Each circle is the largest absolute flux over the basic nodes of one run, on a symmetric log axis (linear between $-10^{-3}$ and $10^{-3}$); values shown to 2 significant digits, because the run output moves by up to a few 1e-6 between PETSc builds. The first 2 circles are asserted to be 0, to an absolute tolerance of 1e-20 W/m$^2$: the conductive flux with conduction off, and the convective, mixing and separation fluxes with conduction only. The last 2 are asserted to be above $10^3$ W/m$^2$ (dashed line): the convective flux with conduction off, and the gap between the total and the conductive flux at the top node with conduction only. A pass means that the first 2 circles sit at 0 and the last 2 above the dashed line. ??? info "Test details" - Reference: Analytical limit: a disabled transport mechanism carries zero flux, and with only conduction enabled the total flux equals the conductive flux at every node except the surface node, which carries the grey-body boundary flux. diff --git a/docs/Validation/reaction.md b/docs/Validation/reaction.md index 61bf4afd..32f9c3c2 100644 --- a/docs/Validation/reaction.md +++ b/docs/Validation/reaction.md @@ -23,7 +23,7 @@ The text below details the check, its tolerance, and its test command. ![Hydrogen and carbon moved by the H2O/H2 and CO2/CO reactions in 1000 years](../assets/validation/reaction_element_balance.svg#only-light){ width="80%" } ![Hydrogen and carbon moved by the H2O/H2 and CO2/CO reactions in 1000 years](../assets/validation/reaction_element_balance_dark.svg#only-dark){ width="80%" } -**How to read the plot.** The bars are the asserted element masses moved in the first 1000 yr, on a log scale. For hydrogen: the hydrogen lost by H$_2$ against the hydrogen in the H$_2$O gained; for carbon: the carbon lost by CO against the carbon in the CO$_2$ gained. A pass means that the 2 bars of each pair have the same height (relative 1e-3). +**How to read the plot.** The bars are the asserted element masses moved in the first 1000 yr, on a log scale; values shown to 2 significant digits, because the run output moves by about 5e-6 between PETSc builds. For hydrogen: the hydrogen lost by H$_2$ against the hydrogen in the H$_2$O gained; for carbon: the carbon lost by CO against the carbon in the CO$_2$ gained. A pass means that the 2 bars of each pair have the same height (relative 1e-3). ??? info "Test details" - Reference: Analytical conservation: closed-system elemental budgets across the H2O/H2 and CO2/CO reaction pairs of the Bower et al. (2021) three-ocean hydrogen configuration. diff --git a/docs/Validation/rheologicalfront.md b/docs/Validation/rheologicalfront.md index 06fb2309..0c6fb449 100644 --- a/docs/Validation/rheologicalfront.md +++ b/docs/Validation/rheologicalfront.md @@ -20,10 +20,10 @@ The section below details the validation check, its tolerance, and its test comm **Known limit.** The test recomputes the crossing with the same rule as `get_crossover_index` and allows one node of difference, so a one-node misplacement passes; the code takes the first node below `phi_critical` without interpolation. PROTEUS takes `RF_depth` from the dynamic front (`rheological_front_dynamic`, the crossing of the convection regime flag at 1.5, between inviscid (2) and viscous (1) convection), which this test does not cover. -![Melt fraction profile at 1200 yr with the critical melt fraction and the reported front](../assets/validation/rheologicalfront_location.svg#only-light){ width="80%" } -![Melt fraction profile at 1200 yr with the critical melt fraction and the reported front](../assets/validation/rheologicalfront_location_dark.svg#only-dark){ width="80%" } +![Radii of the 2 nodes on each side of the critical melt fraction and of the reported front at 1200 yr](../assets/validation/rheologicalfront_location.svg#only-light){ width="80%" } +![Radii of the 2 nodes on each side of the critical melt fraction and of the reported front at 1200 yr](../assets/validation/rheologicalfront_location_dark.svg#only-dark){ width="80%" } -**How to read the plot.** The x-axis is the radius and the y-axis the melt fraction at the staggered nodes at 1200 yr. The dashed line is $\phi_c$ = 0.4. The hollow square is the first node below $\phi_c$ that the test finds itself, and the circle the asserted front radius, the planet radius minus the reported depth. A pass means that the circle lies within 1 node of the square on the dashed line. +**How to read the plot.** The y-axis is the radius. The squares are the 2 staggered nodes that the test finds itself at 1200 yr: the last node with a melt fraction at or above $\phi_c$ = 0.4 (0.403) and the first node below it (0.3995). The circle is the asserted front radius, the planet radius minus the reported depth; radii shown to 3 significant digits. The melt fractions are not drawn: the first node lies 4.5e-4 below $\phi_c$, and the run output moves by about 3e-6 between PETSc builds, so no rounding of them is stable. A pass means that the circle lies between the 2 squares (the test allows 1 node either way). ??? info "Test details" - Reference: Analytical self-consistency: the reported front index matches the phi_critical = 0.4 crossing of the melt-fraction profile, and the depth equals the surface radius minus the front radius. diff --git a/docs/Validation/twophase.md b/docs/Validation/twophase.md index afabf28c..12171141 100644 --- a/docs/Validation/twophase.md +++ b/docs/Validation/twophase.md @@ -20,10 +20,10 @@ The text below details the check, its tolerance, and its test command. **Known limit.** The identity compares 2 code paths that share the code's own `mass_s` and `phi_s`, so a wrong shell mass or melt fraction passes. -![Melt fraction profile at 1200 yr with the reported global melt fraction and its means](../assets/validation/twophase_global_melt_fraction.svg#only-light){ width="80%" } -![Melt fraction profile at 1200 yr with the reported global melt fraction and its means](../assets/validation/twophase_global_melt_fraction_dark.svg#only-dark){ width="80%" } +![Reported global melt fraction at 1200 yr against its 3 definitions](../assets/validation/twophase_global_melt_fraction.svg#only-light){ width="80%" } +![Reported global melt fraction at 1200 yr against its 3 definitions](../assets/validation/twophase_global_melt_fraction_dark.svg#only-dark){ width="80%" } -**How to read the plot.** The x-axis is the radius and the y-axis the melt fraction at the staggered nodes at 1200 yr (solid line, context). The dash-dotted line is the asserted reported phi_global, the dashed line the mass-weighted mean of the profile and the hollow square Mliq / Mmantle; the dotted line is the unweighted mean, which the test requires to be more than 1e-3 away. A pass means that the dash-dotted line, the dashed line and the square coincide (relative 1e-10), above the dotted line. +**How to read the plot.** The circles are the global melt fraction at 1200 yr in 4 forms: the reported phi_global, Mliq / Mmantle and the mass-weighted mean of the staggered-node melt fraction, which the test requires to agree (relative 1e-10), and the unweighted mean, which it requires to be more than 1e-3 away. The grey band is the reported value $\pm$ 1e-3. Values shown to 2 significant digits, because the run output moves by about 2.5e-6 between PETSc builds. A pass means that the first 3 circles lie in the band and the last one outside it. ??? info "Test details" - Reference: Analytical identity: the global melt fraction equals Mliq/Mmantle and the mass-weighted mean of the staggered melt-fraction profile. diff --git a/docs/assets/validation/atmosphere_grey_body_flux.svg b/docs/assets/validation/atmosphere_grey_body_flux.svg index 0847bbb7..533907ab 100644 --- a/docs/assets/validation/atmosphere_grey_body_flux.svg +++ b/docs/assets/validation/atmosphere_grey_body_flux.svg @@ -6,7 +6,7 @@ - test: tests/test_atmosphere.py::test_grey_body_flux_matches_stefan_boltzmann_limit; command: pytest tests/test_atmosphere.py::test_grey_body_flux_matches_stefan_boltzmann_limit --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "log"], "categories": [null, null], "lines": [[41, 1000.0, 3000.0, 2000.0000000000002, 350.0000000000001, 4.751192123886938, 6.662066404532349, 5.87445583474423, 0.32232199735896977], [41, 1000.0, 3000.0, 2000.0000000000002, 350.0000000000001, 1.74468369961667, 3.184647537460785, 2.5925113173524847, 0.24261720300383938], [1, 1856.9326339109864, 1856.9326339109864, 1856.9326339109864, 0.0, 5.828592830319474, 5.828592830319474, 5.828592830319474, 0.0]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} + test: tests/test_atmosphere.py::test_grey_body_flux_matches_stefan_boltzmann_limit; command: pytest tests/test_atmosphere.py::test_grey_body_flux_matches_stefan_boltzmann_limit --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "log"], "categories": [null, null], "lines": [[41, 1000.0, 3000.0, 2000.0000000000002, 350.0000000000001, 4.751192123886938, 6.662066404532349, 5.87445583474423, 0.32232199735896977], [41, 1000.0, 3000.0, 2000.0000000000002, 350.0000000000001, 1.74468369961667, 3.184647537460785, 2.5925113173524847, 0.24261720300383938], [1, 1860.0, 1860.0, 1860.0, 0.0, 5.82865989653532, 5.82865989653532, 5.82865989653532, 0.0]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} image/svg+xml @@ -1352,7 +1352,7 @@ z " style="stroke: #e0a32e"/> - + diff --git a/docs/assets/validation/atmosphere_grey_body_flux_dark.svg b/docs/assets/validation/atmosphere_grey_body_flux_dark.svg index bb33051c..f6de678d 100644 --- a/docs/assets/validation/atmosphere_grey_body_flux_dark.svg +++ b/docs/assets/validation/atmosphere_grey_body_flux_dark.svg @@ -6,7 +6,7 @@ - test: tests/test_atmosphere.py::test_grey_body_flux_matches_stefan_boltzmann_limit; command: pytest tests/test_atmosphere.py::test_grey_body_flux_matches_stefan_boltzmann_limit --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "log"], "categories": [null, null], "lines": [[41, 1000.0, 3000.0, 2000.0000000000002, 350.0000000000001, 4.751192123886938, 6.662066404532349, 5.87445583474423, 0.32232199735896977], [41, 1000.0, 3000.0, 2000.0000000000002, 350.0000000000001, 1.74468369961667, 3.184647537460785, 2.5925113173524847, 0.24261720300383938], [1, 1856.9326339109864, 1856.9326339109864, 1856.9326339109864, 0.0, 5.828592830319474, 5.828592830319474, 5.828592830319474, 0.0]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} + test: tests/test_atmosphere.py::test_grey_body_flux_matches_stefan_boltzmann_limit; command: pytest tests/test_atmosphere.py::test_grey_body_flux_matches_stefan_boltzmann_limit --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "log"], "categories": [null, null], "lines": [[41, 1000.0, 3000.0, 2000.0000000000002, 350.0000000000001, 4.751192123886938, 6.662066404532349, 5.87445583474423, 0.32232199735896977], [41, 1000.0, 3000.0, 2000.0000000000002, 350.0000000000001, 1.74468369961667, 3.184647537460785, 2.5925113173524847, 0.24261720300383938], [1, 1860.0, 1860.0, 1860.0, 0.0, 5.82865989653532, 5.82865989653532, 5.82865989653532, 0.0]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} image/svg+xml @@ -1352,7 +1352,7 @@ z " style="stroke: #e0a32e"/> - + diff --git a/docs/assets/validation/bc_constant_entropy_surface.svg b/docs/assets/validation/bc_constant_entropy_surface.svg index 027c89f7..99087237 100644 --- a/docs/assets/validation/bc_constant_entropy_surface.svg +++ b/docs/assets/validation/bc_constant_entropy_surface.svg @@ -6,7 +6,7 @@ - test: tests/test_bc.py::test_constant_entropy_bc_pins_the_initial_surface; command: pytest tests/test_bc.py::test_constant_entropy_bc_pins_the_initial_surface --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[50, 3504.049999999999, 6371.000000000025, 4866.723471494237, -498.5081967134211, 2550.0, 2600.5237791118197, 2599.1344454301648, 1.0534501058374135], [50, 3504.049999999999, 6371.000000000025, 4866.723471494237, -498.5081967134211, 2599.6657540951796, 2628.335074489891, 2614.708476203073, 4.985054105351737], [1, 6371.000000000025, 6371.000000000025, 6371.000000000025, 0.0, 2550.0, 2550.0, 2550.0, 0.0], [1, 6371.000000000025, 6371.000000000025, 6371.000000000025, 0.0, 2550.0, 2550.0, 2550.0, 0.0]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} + test: tests/test_bc.py::test_constant_entropy_bc_pins_the_initial_surface; command: pytest tests/test_bc.py::test_constant_entropy_bc_pins_the_initial_surface --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[50, 3504.049999999999, 6371.000000000025, 4866.723471494237, -498.5081967134211, 2550.0, 2600.52378010737, 2599.1344457313517, 1.0534502256706388], [50, 3504.049999999999, 6371.000000000025, 4866.723471494237, -498.5081967134211, 2599.6657540951796, 2628.335074489891, 2614.708476203073, 4.985054105351737], [1, 6371.000000000025, 6371.000000000025, 6371.000000000025, 0.0, 2550.0, 2550.0, 2550.0, 0.0], [1, 6371.000000000025, 6371.000000000025, 6371.000000000025, 0.0, 2550.0, 2550.0, 2550.0, 0.0]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} image/svg+xml @@ -1067,50 +1067,50 @@ L 360.396563 87.620352 L 353.828366 87.607045 L 347.29202 87.593861 L 340.787495 87.580785 -L 334.314777 87.567801 +L 334.314777 87.5678 L 327.87386 87.55489 -L 321.464749 87.542041 -L 315.087463 87.529239 -L 308.742032 87.516474 +L 321.464749 87.54204 +L 315.087463 87.529238 +L 308.742032 87.516473 L 302.4285 87.503736 L 296.146924 87.491017 -L 289.897375 87.478309 -L 283.679937 87.465605 -L 277.49471 87.452899 -L 271.341812 87.440185 -L 265.221374 87.427458 +L 289.897375 87.478308 +L 283.679937 87.465604 +L 277.49471 87.452898 +L 271.341812 87.440184 +L 265.221374 87.427457 L 259.133544 87.414712 -L 253.078491 87.401945 -L 247.0564 87.389151 -L 241.067475 87.376326 -L 235.111942 87.363467 -L 229.190047 87.35057 +L 253.078491 87.401944 +L 247.0564 87.38915 +L 241.067475 87.376325 +L 235.111942 87.363466 +L 229.190047 87.350569 L 223.302059 87.337632 -L 217.44827 87.324651 -L 211.628996 87.311623 -L 205.844579 87.298546 -L 200.095386 87.285417 -L 194.381815 87.272234 -L 188.704291 87.258995 +L 217.44827 87.32465 +L 211.628996 87.311622 +L 205.844579 87.298545 +L 200.095386 87.285416 +L 194.381815 87.272233 +L 188.704291 87.258994 L 183.063271 87.245697 -L 177.459244 87.23234 -L 171.892732 87.218922 -L 166.364293 87.20544 -L 160.874524 87.191894 -L 155.424057 87.178283 -L 150.01357 87.164606 -L 144.643779 87.150862 -L 139.315448 87.137051 -L 134.029386 87.123173 -L 128.786454 87.109228 -L 123.587561 87.095216 -L 118.43367 87.081132 -L 113.325802 87.066928 -L 108.265035 87.052191 -L 103.252506 87.033243 -L 98.289418 86.975733 -L 93.377035 86.632437 -L 88.516692 86.334144 +L 177.459244 87.232339 +L 171.892732 87.218921 +L 166.364293 87.205439 +L 160.874524 87.191893 +L 155.424057 87.178282 +L 150.01357 87.164605 +L 144.643779 87.150861 +L 139.315448 87.13705 +L 134.029386 87.123172 +L 128.786454 87.109227 +L 123.587561 87.095215 +L 118.43367 87.08113 +L 113.325802 87.066927 +L 108.265035 87.052189 +L 103.252506 87.033241 +L 98.289418 86.975732 +L 93.377035 86.632435 +L 88.516692 86.334141 " clip-path="url(#p27b8481a00)" style="fill: none; stroke: #10151b; stroke-width: 2.2; stroke-linecap: square"/> diff --git a/docs/assets/validation/bc_constant_entropy_surface_dark.svg b/docs/assets/validation/bc_constant_entropy_surface_dark.svg index 4a443a7d..07d5b015 100644 --- a/docs/assets/validation/bc_constant_entropy_surface_dark.svg +++ b/docs/assets/validation/bc_constant_entropy_surface_dark.svg @@ -6,7 +6,7 @@ - test: tests/test_bc.py::test_constant_entropy_bc_pins_the_initial_surface; command: pytest tests/test_bc.py::test_constant_entropy_bc_pins_the_initial_surface --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[50, 3504.049999999999, 6371.000000000025, 4866.723471494237, -498.5081967134211, 2550.0, 2600.5237791118197, 2599.1344454301648, 1.0534501058374135], [50, 3504.049999999999, 6371.000000000025, 4866.723471494237, -498.5081967134211, 2599.6657540951796, 2628.335074489891, 2614.708476203073, 4.985054105351737], [1, 6371.000000000025, 6371.000000000025, 6371.000000000025, 0.0, 2550.0, 2550.0, 2550.0, 0.0], [1, 6371.000000000025, 6371.000000000025, 6371.000000000025, 0.0, 2550.0, 2550.0, 2550.0, 0.0]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} + test: tests/test_bc.py::test_constant_entropy_bc_pins_the_initial_surface; command: pytest tests/test_bc.py::test_constant_entropy_bc_pins_the_initial_surface --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[50, 3504.049999999999, 6371.000000000025, 4866.723471494237, -498.5081967134211, 2550.0, 2600.52378010737, 2599.1344457313517, 1.0534502256706388], [50, 3504.049999999999, 6371.000000000025, 4866.723471494237, -498.5081967134211, 2599.6657540951796, 2628.335074489891, 2614.708476203073, 4.985054105351737], [1, 6371.000000000025, 6371.000000000025, 6371.000000000025, 0.0, 2550.0, 2550.0, 2550.0, 0.0], [1, 6371.000000000025, 6371.000000000025, 6371.000000000025, 0.0, 2550.0, 2550.0, 2550.0, 0.0]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} image/svg+xml @@ -1067,50 +1067,50 @@ L 360.396563 87.620352 L 353.828366 87.607045 L 347.29202 87.593861 L 340.787495 87.580785 -L 334.314777 87.567801 +L 334.314777 87.5678 L 327.87386 87.55489 -L 321.464749 87.542041 -L 315.087463 87.529239 -L 308.742032 87.516474 +L 321.464749 87.54204 +L 315.087463 87.529238 +L 308.742032 87.516473 L 302.4285 87.503736 L 296.146924 87.491017 -L 289.897375 87.478309 -L 283.679937 87.465605 -L 277.49471 87.452899 -L 271.341812 87.440185 -L 265.221374 87.427458 +L 289.897375 87.478308 +L 283.679937 87.465604 +L 277.49471 87.452898 +L 271.341812 87.440184 +L 265.221374 87.427457 L 259.133544 87.414712 -L 253.078491 87.401945 -L 247.0564 87.389151 -L 241.067475 87.376326 -L 235.111942 87.363467 -L 229.190047 87.35057 +L 253.078491 87.401944 +L 247.0564 87.38915 +L 241.067475 87.376325 +L 235.111942 87.363466 +L 229.190047 87.350569 L 223.302059 87.337632 -L 217.44827 87.324651 -L 211.628996 87.311623 -L 205.844579 87.298546 -L 200.095386 87.285417 -L 194.381815 87.272234 -L 188.704291 87.258995 +L 217.44827 87.32465 +L 211.628996 87.311622 +L 205.844579 87.298545 +L 200.095386 87.285416 +L 194.381815 87.272233 +L 188.704291 87.258994 L 183.063271 87.245697 -L 177.459244 87.23234 -L 171.892732 87.218922 -L 166.364293 87.20544 -L 160.874524 87.191894 -L 155.424057 87.178283 -L 150.01357 87.164606 -L 144.643779 87.150862 -L 139.315448 87.137051 -L 134.029386 87.123173 -L 128.786454 87.109228 -L 123.587561 87.095216 -L 118.43367 87.081132 -L 113.325802 87.066928 -L 108.265035 87.052191 -L 103.252506 87.033243 -L 98.289418 86.975733 -L 93.377035 86.632437 -L 88.516692 86.334144 +L 177.459244 87.232339 +L 171.892732 87.218921 +L 166.364293 87.205439 +L 160.874524 87.191893 +L 155.424057 87.178282 +L 150.01357 87.164605 +L 144.643779 87.150861 +L 139.315448 87.13705 +L 134.029386 87.123172 +L 128.786454 87.109227 +L 123.587561 87.095215 +L 118.43367 87.08113 +L 113.325802 87.066927 +L 108.265035 87.052189 +L 103.252506 87.033241 +L 98.289418 86.975732 +L 93.377035 86.632435 +L 88.516692 86.334141 " clip-path="url(#p27b8481a00)" style="fill: none; stroke: #f2f5f7; stroke-width: 2.2; stroke-linecap: square"/> diff --git a/docs/assets/validation/bc_prescribed_flux.svg b/docs/assets/validation/bc_prescribed_flux.svg index c1816cfb..f9699798 100644 --- a/docs/assets/validation/bc_prescribed_flux.svg +++ b/docs/assets/validation/bc_prescribed_flux.svg @@ -6,7 +6,7 @@ - test: tests/test_bc.py::test_prescribed_flux_bc_echoes_value_and_inverts_emissivity; command: pytest tests/test_bc.py::test_prescribed_flux_bc_echoes_value_and_inverts_emissivity --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "log"], "categories": [null, null], "lines": [[41, 1000.0, 3000.0, 2000.0000000000002, 350.0000000000001, 4.751192123886938, 6.662066404532349, 5.87445583474423, 0.32232199735896977], [41, 1000.0, 3000.0, 2000.0000000000002, 350.0000000000001, 3.8250806660866896, 5.735954946732101, 4.948344376943983, 0.3223219973589699], [2, 0.0, 1.0, 0.5, 0.5, 5.0, 5.0, 5.0, 0.0], [1, 1964.1090825259737, 1964.1090825259737, 1964.1090825259737, 0.0, 5.0, 5.0, 5.0, 0.0]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} + test: tests/test_bc.py::test_prescribed_flux_bc_echoes_value_and_inverts_emissivity; command: pytest tests/test_bc.py::test_prescribed_flux_bc_echoes_value_and_inverts_emissivity --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "log"], "categories": [null, null], "lines": [[41, 1000.0, 3000.0, 2000.0000000000002, 350.0000000000001, 4.751192123886938, 6.662066404532349, 5.87445583474423, 0.32232199735896977], [41, 1000.0, 3000.0, 2000.0000000000002, 350.0000000000001, 3.8250806745985106, 5.735954955243923, 4.948344385455804, 0.3223219973589698], [2, 0.0, 1.0, 0.5, 0.5, 5.0, 5.0, 5.0, 0.0], [1, 1964.109072905828, 1964.109072905828, 1964.109072905828, 0.0, 5.0, 5.0, 5.0, 0.0]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} image/svg+xml @@ -696,18 +696,18 @@ z - - + - + @@ -716,18 +716,18 @@ L 400.757949 131.054327 - - + - + - + @@ -809,7 +809,7 @@ L -2 0 - + @@ -823,21 +823,21 @@ L -2 0 - + - + - + @@ -865,7 +865,7 @@ L -2 0 - + @@ -879,7 +879,7 @@ L -2 0 - + @@ -914,7 +914,7 @@ L -2 0 - + @@ -1071,7 +1071,7 @@ z L 81.928951 142.288303 L 89.705268 136.716525 L 97.481585 131.396173 -L 105.257902 126.305126 +L 105.257902 126.305127 L 113.034219 121.424134 L 120.810536 116.736322 L 128.586853 112.22681 @@ -1079,14 +1079,14 @@ L 136.36317 107.882399 L 144.139487 103.691326 L 151.915804 99.643056 L 159.692121 95.728116 -L 167.468438 91.937953 +L 167.468438 91.937954 L 175.244755 88.264821 L 183.021072 84.701675 L 190.797389 81.242095 -L 198.573706 77.880209 +L 198.573706 77.88021 L 206.350023 74.610635 L 214.12634 71.428423 -L 221.902657 68.329014 +L 221.902657 68.329015 L 229.678974 65.3082 L 237.455291 62.362084 L 245.231609 59.487054 @@ -1125,38 +1125,38 @@ L 151.915804 163.228573 L 159.692121 159.313633 L 167.468438 155.52347 L 175.244755 151.850337 -L 183.021072 148.287192 +L 183.021072 148.287191 L 190.797389 144.827612 L 198.573706 141.465726 L 206.350023 138.196151 L 214.12634 135.013939 L 221.902657 131.914531 L 229.678974 128.893717 -L 237.455291 125.947601 +L 237.455291 125.9476 L 245.231609 123.07257 L 253.007926 120.265271 L 260.784243 117.522579 L 268.56056 114.841584 L 276.336877 112.219567 L 284.113194 109.653987 -L 291.889511 107.142462 +L 291.889511 107.142461 L 299.665828 104.682758 L 307.442145 102.27278 L 315.218462 99.910554 L 322.994779 97.594223 L 330.771096 95.322037 L 338.547413 93.092343 -L 346.32373 90.903581 +L 346.32373 90.90358 L 354.100047 88.754272 L 361.876364 86.64302 L 369.652681 84.5685 -L 377.428998 82.529455 +L 377.428998 82.529454 L 385.205315 80.52469 " clip-path="url(#pd9fbaa4b04)" style="fill: none; stroke-dasharray: 8.14,3.52; stroke-dashoffset: 0; stroke: #e23d28; stroke-width: 2.2"/> - @@ -1184,7 +1184,7 @@ z " style="stroke: #e0a32e"/> - + diff --git a/docs/assets/validation/bc_prescribed_flux_dark.svg b/docs/assets/validation/bc_prescribed_flux_dark.svg index 02a07836..b3a27d93 100644 --- a/docs/assets/validation/bc_prescribed_flux_dark.svg +++ b/docs/assets/validation/bc_prescribed_flux_dark.svg @@ -6,7 +6,7 @@ - test: tests/test_bc.py::test_prescribed_flux_bc_echoes_value_and_inverts_emissivity; command: pytest tests/test_bc.py::test_prescribed_flux_bc_echoes_value_and_inverts_emissivity --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "log"], "categories": [null, null], "lines": [[41, 1000.0, 3000.0, 2000.0000000000002, 350.0000000000001, 4.751192123886938, 6.662066404532349, 5.87445583474423, 0.32232199735896977], [41, 1000.0, 3000.0, 2000.0000000000002, 350.0000000000001, 3.8250806660866896, 5.735954946732101, 4.948344376943983, 0.3223219973589699], [2, 0.0, 1.0, 0.5, 0.5, 5.0, 5.0, 5.0, 0.0], [1, 1964.1090825259737, 1964.1090825259737, 1964.1090825259737, 0.0, 5.0, 5.0, 5.0, 0.0]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} + test: tests/test_bc.py::test_prescribed_flux_bc_echoes_value_and_inverts_emissivity; command: pytest tests/test_bc.py::test_prescribed_flux_bc_echoes_value_and_inverts_emissivity --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "log"], "categories": [null, null], "lines": [[41, 1000.0, 3000.0, 2000.0000000000002, 350.0000000000001, 4.751192123886938, 6.662066404532349, 5.87445583474423, 0.32232199735896977], [41, 1000.0, 3000.0, 2000.0000000000002, 350.0000000000001, 3.8250806745985106, 5.735954955243923, 4.948344385455804, 0.3223219973589698], [2, 0.0, 1.0, 0.5, 0.5, 5.0, 5.0, 5.0, 0.0], [1, 1964.109072905828, 1964.109072905828, 1964.109072905828, 0.0, 5.0, 5.0, 5.0, 0.0]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} image/svg+xml @@ -696,18 +696,18 @@ z - - + - + @@ -716,18 +716,18 @@ L 400.757949 131.054327 - - + - + - + @@ -809,7 +809,7 @@ L -2 0 - + @@ -823,21 +823,21 @@ L -2 0 - + - + - + @@ -865,7 +865,7 @@ L -2 0 - + @@ -879,7 +879,7 @@ L -2 0 - + @@ -914,7 +914,7 @@ L -2 0 - + @@ -1071,7 +1071,7 @@ z L 81.928951 142.288303 L 89.705268 136.716525 L 97.481585 131.396173 -L 105.257902 126.305126 +L 105.257902 126.305127 L 113.034219 121.424134 L 120.810536 116.736322 L 128.586853 112.22681 @@ -1079,14 +1079,14 @@ L 136.36317 107.882399 L 144.139487 103.691326 L 151.915804 99.643056 L 159.692121 95.728116 -L 167.468438 91.937953 +L 167.468438 91.937954 L 175.244755 88.264821 L 183.021072 84.701675 L 190.797389 81.242095 -L 198.573706 77.880209 +L 198.573706 77.88021 L 206.350023 74.610635 L 214.12634 71.428423 -L 221.902657 68.329014 +L 221.902657 68.329015 L 229.678974 65.3082 L 237.455291 62.362084 L 245.231609 59.487054 @@ -1125,38 +1125,38 @@ L 151.915804 163.228573 L 159.692121 159.313633 L 167.468438 155.52347 L 175.244755 151.850337 -L 183.021072 148.287192 +L 183.021072 148.287191 L 190.797389 144.827612 L 198.573706 141.465726 L 206.350023 138.196151 L 214.12634 135.013939 L 221.902657 131.914531 L 229.678974 128.893717 -L 237.455291 125.947601 +L 237.455291 125.9476 L 245.231609 123.07257 L 253.007926 120.265271 L 260.784243 117.522579 L 268.56056 114.841584 L 276.336877 112.219567 L 284.113194 109.653987 -L 291.889511 107.142462 +L 291.889511 107.142461 L 299.665828 104.682758 L 307.442145 102.27278 L 315.218462 99.910554 L 322.994779 97.594223 L 330.771096 95.322037 L 338.547413 93.092343 -L 346.32373 90.903581 +L 346.32373 90.90358 L 354.100047 88.754272 L 361.876364 86.64302 L 369.652681 84.5685 -L 377.428998 82.529455 +L 377.428998 82.529454 L 385.205315 80.52469 " clip-path="url(#pd9fbaa4b04)" style="fill: none; stroke-dasharray: 8.14,3.52; stroke-dashoffset: 0; stroke: #e23d28; stroke-width: 2.2"/> - @@ -1184,7 +1184,7 @@ z " style="stroke: #e0a32e"/> - + diff --git a/docs/assets/validation/energy_disabled_transport.svg b/docs/assets/validation/energy_disabled_transport.svg index e23903c0..a7cdcd1f 100644 --- a/docs/assets/validation/energy_disabled_transport.svg +++ b/docs/assets/validation/energy_disabled_transport.svg @@ -1,12 +1,12 @@ - + - test: tests/test_energy.py::test_disabled_transport_mechanisms_carry_zero_flux; command: pytest tests/test_energy.py::test_disabled_transport_mechanisms_carry_zero_flux --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "symlog"], "categories": [null, null], "lines": [[50, 3504.049999999999, 6371.000000000025, 4866.723471494237, -498.5081967134211, 899083099.5253087, 2034826212.643107, 1615442839.586597, 166101675.0076686], [50, 3504.049999999999, 6371.000000000025, 4866.723471494237, -498.5081967134211, 0.0, 0.0, 0.0, 0.0], [50, 3504.049999999999, 6371.000000000025, 4866.723471494237, -498.5081967134211, 0.0018859082962793844, 0.012501514909557785, 0.002936927209303042, -0.0007039860647283159], [50, 3504.049999999999, 6371.000000000025, 4866.723471494237, -498.5081967134211, 0.0, 0.0, 0.0, 0.0]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} + test: tests/test_energy.py::test_disabled_transport_mechanisms_carry_zero_flux; command: pytest tests/test_energy.py::test_disabled_transport_mechanisms_carry_zero_flux --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "symlog"], "categories": [null, null], "lines": [[2, 0.0, 1.0, 0.5, 0.5, 1000.0, 1000.0, 1000.0, 0.0], [4, 0.0, 3.0, 1.5, 0.8333333333333333, 0.0, 2000000000.0, 500207500.0, 166874166.66666663]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} image/svg+xml @@ -21,28 +21,28 @@ - - - + @@ -51,335 +51,476 @@ L 0 3.5 " style="stroke: #3e4a55; stroke-width: 0.8"/> - + - - + + - - - + + + + + + - - - - + + + + + + + + + + - + - + - - + + - + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - - - - + + - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + - - + + - + - + - + - + + + + - - + + - + - + - + - + - - + + - + - + - + - + @@ -724,19 +846,58 @@ L 387.491356 125.31335 - - + + - + - + - + - + + + + @@ -744,19 +905,19 @@ L 387.491356 68.866675 - - + + - + - + - + - + @@ -765,83 +926,97 @@ L 387.491356 12.42 - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + + + + + + + + + + + + + - + - + - + - - + @@ -956,300 +1125,147 @@ z - - - - - - - - - - - + + - - + + + + + + + + + + + - - + + - - - + + + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - - - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + diff --git a/docs/assets/validation/energy_disabled_transport_dark.svg b/docs/assets/validation/energy_disabled_transport_dark.svg index abb36090..33bc34ce 100644 --- a/docs/assets/validation/energy_disabled_transport_dark.svg +++ b/docs/assets/validation/energy_disabled_transport_dark.svg @@ -1,12 +1,12 @@ - + - test: tests/test_energy.py::test_disabled_transport_mechanisms_carry_zero_flux; command: pytest tests/test_energy.py::test_disabled_transport_mechanisms_carry_zero_flux --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "symlog"], "categories": [null, null], "lines": [[50, 3504.049999999999, 6371.000000000025, 4866.723471494237, -498.5081967134211, 899083099.5253087, 2034826212.643107, 1615442839.586597, 166101675.0076686], [50, 3504.049999999999, 6371.000000000025, 4866.723471494237, -498.5081967134211, 0.0, 0.0, 0.0, 0.0], [50, 3504.049999999999, 6371.000000000025, 4866.723471494237, -498.5081967134211, 0.0018859082962793844, 0.012501514909557785, 0.002936927209303042, -0.0007039860647283159], [50, 3504.049999999999, 6371.000000000025, 4866.723471494237, -498.5081967134211, 0.0, 0.0, 0.0, 0.0]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} + test: tests/test_energy.py::test_disabled_transport_mechanisms_carry_zero_flux; command: pytest tests/test_energy.py::test_disabled_transport_mechanisms_carry_zero_flux --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "symlog"], "categories": [null, null], "lines": [[2, 0.0, 1.0, 0.5, 0.5, 1000.0, 1000.0, 1000.0, 0.0], [4, 0.0, 3.0, 1.5, 0.8333333333333333, 0.0, 2000000000.0, 500207500.0, 166874166.66666663]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} image/svg+xml @@ -21,28 +21,28 @@ - - - + @@ -51,335 +51,476 @@ L 0 3.5 " style="stroke: #9fb0be; stroke-width: 0.8"/> - + - - + + - - - + + + + + + - - - - + + + + + + + + + + - + - + - - + + - + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - - - - + + - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + - - + + - + - + - + - + + + + - - + + - + - + - + - + - - + + - + - + - + - + @@ -724,19 +846,58 @@ L 387.491356 125.31335 - - + + - + - + - + - + + + + @@ -744,19 +905,19 @@ L 387.491356 68.866675 - - + + - + - + - + - + @@ -765,83 +926,97 @@ L 387.491356 12.42 - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + + + + + + + + + + + + + - + - + - + - - + @@ -956,300 +1125,147 @@ z - - - - - - - - - - - + + - - + + + + + + + + + + + - - + + - - - + + + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - - - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + diff --git a/docs/assets/validation/energy_steady_state_ic.svg b/docs/assets/validation/energy_steady_state_ic.svg index f509eff2..96dbd91b 100644 --- a/docs/assets/validation/energy_steady_state_ic.svg +++ b/docs/assets/validation/energy_steady_state_ic.svg @@ -6,7 +6,7 @@ - test: tests/test_energy.py::test_steady_state_ic_balances_interior_and_surface; command: pytest tests/test_energy.py::test_steady_state_ic_balances_interior_and_surface --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[50, 3504.049999999999, 6371.000000000025, 4866.723471494237, -498.5081967134211, 4.2448235883394636e+20, 4.2448235884226e+20, 4.244823588377694e+20, 390105126.100485]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}, {"scales": ["linear", "log"], "categories": [null, null], "lines": [[21, 1000.0, 3000.0, 2000.0000000000002, 366.6666666666668, 4.751192123886938, 6.662066404532349, 5.870112634619891, 0.33865659990514185], [21, 1000.0, 3000.0, 2000.0000000000002, 366.6666666666668, 1.74468369961667, 3.184647537460785, 2.5892010970536017, 0.2549363219132293], [1, 1957.478201888133, 1957.478201888133, 1957.478201888133, 0.0, 5.9202345718217115, 5.9202345718217115, 5.9202345718217115, 0.0]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} + test: tests/test_energy.py::test_steady_state_ic_balances_interior_and_surface; command: pytest tests/test_energy.py::test_steady_state_ic_balances_interior_and_surface --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[50, 3504.049999999999, 6371.000000000025, 4866.723471494237, -498.5081967134211, 4.2448235883394715e+20, 4.244823588517078e+20, 4.2448235883793836e+20, 553284279.2787571]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}, {"scales": ["linear", "log"], "categories": [null, null], "lines": [[21, 1000.0, 3000.0, 2000.0000000000002, 366.6666666666668, 4.751192123886938, 6.662066404532349, 5.870112634619891, 0.33865659990514185], [21, 1000.0, 3000.0, 2000.0000000000002, 366.6666666666668, 1.74468369961667, 3.184647537460785, 2.5892010970536017, 0.2549363219132293], [1, 1957.478201888133, 1957.478201888133, 1957.478201888133, 0.0, 5.9202345718217115, 5.9202345718217115, 5.9202345718217115, 0.0]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} image/svg+xml diff --git a/docs/assets/validation/energy_steady_state_ic_dark.svg b/docs/assets/validation/energy_steady_state_ic_dark.svg index 26cb2b3f..03a91c8d 100644 --- a/docs/assets/validation/energy_steady_state_ic_dark.svg +++ b/docs/assets/validation/energy_steady_state_ic_dark.svg @@ -6,7 +6,7 @@ - test: tests/test_energy.py::test_steady_state_ic_balances_interior_and_surface; command: pytest tests/test_energy.py::test_steady_state_ic_balances_interior_and_surface --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[50, 3504.049999999999, 6371.000000000025, 4866.723471494237, -498.5081967134211, 4.2448235883394636e+20, 4.2448235884226e+20, 4.244823588377694e+20, 390105126.100485]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}, {"scales": ["linear", "log"], "categories": [null, null], "lines": [[21, 1000.0, 3000.0, 2000.0000000000002, 366.6666666666668, 4.751192123886938, 6.662066404532349, 5.870112634619891, 0.33865659990514185], [21, 1000.0, 3000.0, 2000.0000000000002, 366.6666666666668, 1.74468369961667, 3.184647537460785, 2.5892010970536017, 0.2549363219132293], [1, 1957.478201888133, 1957.478201888133, 1957.478201888133, 0.0, 5.9202345718217115, 5.9202345718217115, 5.9202345718217115, 0.0]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} + test: tests/test_energy.py::test_steady_state_ic_balances_interior_and_surface; command: pytest tests/test_energy.py::test_steady_state_ic_balances_interior_and_surface --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[50, 3504.049999999999, 6371.000000000025, 4866.723471494237, -498.5081967134211, 4.2448235883394715e+20, 4.244823588517078e+20, 4.2448235883793836e+20, 553284279.2787571]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}, {"scales": ["linear", "log"], "categories": [null, null], "lines": [[21, 1000.0, 3000.0, 2000.0000000000002, 366.6666666666668, 4.751192123886938, 6.662066404532349, 5.870112634619891, 0.33865659990514185], [21, 1000.0, 3000.0, 2000.0000000000002, 366.6666666666668, 1.74468369961667, 3.184647537460785, 2.5892010970536017, 0.2549363219132293], [1, 1957.478201888133, 1957.478201888133, 1957.478201888133, 0.0, 5.9202345718217115, 5.9202345718217115, 5.9202345718217115, 0.0]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} image/svg+xml diff --git a/docs/assets/validation/reaction_element_balance.svg b/docs/assets/validation/reaction_element_balance.svg index 2eaa67d8..19123406 100644 --- a/docs/assets/validation/reaction_element_balance.svg +++ b/docs/assets/validation/reaction_element_balance.svg @@ -1,12 +1,12 @@ - + - test: tests/test_reaction.py::test_elements_conserved_across_each_reaction_pair; command: pytest tests/test_reaction.py::test_elements_conserved_across_each_reaction_pair --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "log"], "categories": [null, null], "lines": [], "collections": [], "values": [], "value_scales": [], "patches": [[8, -0.4, 1.4, 0.5, 0.22857142857142854, 17.299802458049594, 18.009374483106537, 17.654588470578066, 0.10136743215099084]], "images": [], "image_scales": [], "extents": []}]} + test: tests/test_reaction.py::test_elements_conserved_across_each_reaction_pair; command: pytest tests/test_reaction.py::test_elements_conserved_across_each_reaction_pair --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "log"], "categories": [null, null], "lines": [], "collections": [], "values": [], "value_scales": [], "patches": [[8, -0.4, 1.4, 0.5, 0.22857142857142854, 17.30102999566398, 18.0, 17.65051499783199, 0.09985285776228747]], "images": [], "image_scales": [], "extents": []}]} image/svg+xml @@ -21,28 +21,28 @@ - - - + @@ -51,12 +51,12 @@ L 0 3.5 " style="stroke: #3e4a55; stroke-width: 0.8"/> - + - + - + - + - + - + @@ -409,12 +409,12 @@ L -3.5 0 " style="stroke: #3e4a55; stroke-width: 0.8"/> - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + - + - - - - - - - - - - - - - - + + + + + - - + + - + - - - - - - - - - - - - + + + + + - - + + - + - - - - - - - - - - - - + + + + + - - + + - + - - + + - + - - - - - - - - - - - - + + + + + - - + + - + - - + + - + - - + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - +" clip-path="url(#p22f0acee3c)" style="fill: #10151b"/> - +" clip-path="url(#p22f0acee3c)" style="fill: #10151b"/> - +" clip-path="url(#p22f0acee3c)" style="fill: #e23d28"/> - +" clip-path="url(#p22f0acee3c)" style="fill: #e23d28"/> - - - - + - + - - + - + - + + diff --git a/docs/assets/validation/reaction_element_balance_dark.svg b/docs/assets/validation/reaction_element_balance_dark.svg index fc0146e9..ac02f579 100644 --- a/docs/assets/validation/reaction_element_balance_dark.svg +++ b/docs/assets/validation/reaction_element_balance_dark.svg @@ -1,12 +1,12 @@ - + - test: tests/test_reaction.py::test_elements_conserved_across_each_reaction_pair; command: pytest tests/test_reaction.py::test_elements_conserved_across_each_reaction_pair --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "log"], "categories": [null, null], "lines": [], "collections": [], "values": [], "value_scales": [], "patches": [[8, -0.4, 1.4, 0.5, 0.22857142857142854, 17.299802458049594, 18.009374483106537, 17.654588470578066, 0.10136743215099084]], "images": [], "image_scales": [], "extents": []}]} + test: tests/test_reaction.py::test_elements_conserved_across_each_reaction_pair; command: pytest tests/test_reaction.py::test_elements_conserved_across_each_reaction_pair --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "log"], "categories": [null, null], "lines": [], "collections": [], "values": [], "value_scales": [], "patches": [[8, -0.4, 1.4, 0.5, 0.22857142857142854, 17.30102999566398, 18.0, 17.65051499783199, 0.09985285776228747]], "images": [], "image_scales": [], "extents": []}]} image/svg+xml @@ -21,28 +21,28 @@ - - - + @@ -51,12 +51,12 @@ L 0 3.5 " style="stroke: #9fb0be; stroke-width: 0.8"/> - + - + - + - + - + - + @@ -409,12 +409,12 @@ L -3.5 0 " style="stroke: #9fb0be; stroke-width: 0.8"/> - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + - + - - - - - - - - - - - - - - + + + + + - - + + - + - - - - - - - - - - - - + + + + + - - + + - + - - - - - - - - - - - - + + + + + - - + + - + - - + + - + - - - - - - - - - - - - + + + + + - - + + - + - - + + - + - - + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - +" clip-path="url(#p22f0acee3c)" style="fill: #f2f5f7"/> - +" clip-path="url(#p22f0acee3c)" style="fill: #f2f5f7"/> - +" clip-path="url(#p22f0acee3c)" style="fill: #e23d28"/> - +" clip-path="url(#p22f0acee3c)" style="fill: #e23d28"/> - - - - + - + - - + - + - + + diff --git a/docs/assets/validation/rheologicalfront_location.svg b/docs/assets/validation/rheologicalfront_location.svg index f83c19f0..9fcc254a 100644 --- a/docs/assets/validation/rheologicalfront_location.svg +++ b/docs/assets/validation/rheologicalfront_location.svg @@ -1,12 +1,12 @@ - + - test: tests/test_rheologicalfront.py::test_front_location_is_consistent_with_melt_fraction_profile; command: pytest tests/test_rheologicalfront.py::test_front_location_is_consistent_with_melt_fraction_profile --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[49, 3528.420367522105, 6337.615609804658, 4865.232006967509, -488.80990442400326, 0.3895779609783869, 0.705065248642743, 0.4906220970877643, -0.03989934434113623], [2, 0.0, 1.0, 0.5, 0.5, 0.4, 0.4, 0.4, 0.0], [1, 3677.3667380927054, 3677.3667380927054, 3677.3667380927054, 0.0, 0.3995477556123374, 0.3995477556123374, 0.3995477556123374, 0.0], [1, 3702.6299093603534, 3702.6299093603534, 3702.6299093603534, 0.0, 0.4, 0.4, 0.4, 0.0]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} + test: tests/test_rheologicalfront.py::test_front_location_is_consistent_with_melt_fraction_profile; command: pytest tests/test_rheologicalfront.py::test_front_location_is_consistent_with_melt_fraction_profile --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[2, 0.0, 2.0, 1.0, 1.0, 3680.0, 3730.0, 3705.0, -25.000000000000018], [1, 1.0, 1.0, 1.0, 0.0, 3700.0, 3700.0, 3700.0, 0.0]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} image/svg+xml @@ -21,28 +21,28 @@ - - - + @@ -51,314 +51,27 @@ L 0 3.5 " style="stroke: #3e4a55; stroke-width: 0.8"/> - + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + - - + + - + - + - - - + + + - + + + - - - - + + + + - - + + - + - + - - - - - - - + + + + + + + + + + - - + + - + - + - - - - - - - + + + + + + + + + + - - + + - + - + - - - - - - - + + + + + + + + + + - - + + - + - + - - - - - - - + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - + + - + - + - - - - - - - - - - + + + + + + + - - - + + + - - - - - - - - + - + - - - - - - - - - - - - - + + + + + + + + + + + - - - - - - - + - +" style="stroke: #10151b; stroke-linejoin: miter"/> - - + + + - - - + - +" style="stroke: #e23d28"/> - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - - - + + + - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - + - + - + - + + @@ -1429,8 +1272,8 @@ z - - + + diff --git a/docs/assets/validation/rheologicalfront_location_dark.svg b/docs/assets/validation/rheologicalfront_location_dark.svg index 147653c8..af440346 100644 --- a/docs/assets/validation/rheologicalfront_location_dark.svg +++ b/docs/assets/validation/rheologicalfront_location_dark.svg @@ -1,12 +1,12 @@ - + - test: tests/test_rheologicalfront.py::test_front_location_is_consistent_with_melt_fraction_profile; command: pytest tests/test_rheologicalfront.py::test_front_location_is_consistent_with_melt_fraction_profile --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[49, 3528.420367522105, 6337.615609804658, 4865.232006967509, -488.80990442400326, 0.3895779609783869, 0.705065248642743, 0.4906220970877643, -0.03989934434113623], [2, 0.0, 1.0, 0.5, 0.5, 0.4, 0.4, 0.4, 0.0], [1, 3677.3667380927054, 3677.3667380927054, 3677.3667380927054, 0.0, 0.3995477556123374, 0.3995477556123374, 0.3995477556123374, 0.0], [1, 3702.6299093603534, 3702.6299093603534, 3702.6299093603534, 0.0, 0.4, 0.4, 0.4, 0.0]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} + test: tests/test_rheologicalfront.py::test_front_location_is_consistent_with_melt_fraction_profile; command: pytest tests/test_rheologicalfront.py::test_front_location_is_consistent_with_melt_fraction_profile --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[2, 0.0, 2.0, 1.0, 1.0, 3680.0, 3730.0, 3705.0, -25.000000000000018], [1, 1.0, 1.0, 1.0, 0.0, 3700.0, 3700.0, 3700.0, 0.0]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} image/svg+xml @@ -21,28 +21,28 @@ - - - + @@ -51,314 +51,27 @@ L 0 3.5 " style="stroke: #9fb0be; stroke-width: 0.8"/> - + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + - - + + - + - + - - - + + + - + + + - - - - + + + + - - + + - + - + - - - - - - - + + + + + + + + + + - - + + - + - + - - - - - - - + + + + + + + + + + - - + + - + - + - - - - - - - + + + + + + + + + + - - + + - + - + - - - - - - - + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - + + - + - + - - - - - - - - - - + + + + + + + - - - + + + - - - - - - - - + - + - - - - - - - - - - - - - + + + + + + + + + + + - - - - - - - + - +" style="stroke: #f2f5f7; stroke-linejoin: miter"/> - - + + + - - - + - +" style="stroke: #e23d28"/> - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - - - + + + - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - + - + - + - + + @@ -1429,8 +1272,8 @@ z - - + + diff --git a/docs/assets/validation/twophase_global_melt_fraction.svg b/docs/assets/validation/twophase_global_melt_fraction.svg index 1f86d1ab..7b229e87 100644 --- a/docs/assets/validation/twophase_global_melt_fraction.svg +++ b/docs/assets/validation/twophase_global_melt_fraction.svg @@ -1,12 +1,12 @@ - + - test: tests/test_twophase.py::test_global_melt_fraction_is_the_mass_weighted_profile_mean; command: pytest tests/test_twophase.py::test_global_melt_fraction_is_the_mass_weighted_profile_mean --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[49, 3528.420367522105, 6337.615609804658, 4865.232006967509, -488.80990442400326, 0.3895779609783869, 0.705065248642743, 0.4906220970877643, -0.03989934434113623], [2, 0.0, 1.0, 0.5, 0.5, 0.5129281701005098, 0.5129281701005098, 0.5129281701005098, 0.0], [2, 0.0, 1.0, 0.5, 0.5, 0.5129281701005103, 0.5129281701005103, 0.5129281701005103, 0.0], [2, 0.0, 1.0, 0.5, 0.5, 0.4906220970877643, 0.4906220970877643, 0.4906220970877643, 0.0], [1, 4865.232006967509, 4865.232006967509, 4865.232006967509, 0.0, 0.51292817010051, 0.51292817010051, 0.51292817010051, 0.0]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} + test: tests/test_twophase.py::test_global_melt_fraction_is_the_mass_weighted_profile_mean; command: pytest tests/test_twophase.py::test_global_melt_fraction_is_the_mass_weighted_profile_mean --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[4, 0.0, 3.0, 1.5, 0.8333333333333333, 0.49, 0.51, 0.505, -0.005000000000000039]], "collections": [], "values": [], "value_scales": [], "patches": [[5, 0.0, 1.0, 0.4, -0.1, 0.509, 0.511, 0.5098, 0.00020000000000000565]], "images": [], "image_scales": [], "extents": []}]} image/svg+xml @@ -21,28 +21,28 @@ - - - + @@ -51,314 +51,341 @@ L 0 3.5 " style="stroke: #3e4a55; stroke-width: 0.8"/> - + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - - + + + + + + + + - - - - - + + + + + + + + + + + + + + + + + + + - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + - - + + - + - + - - - + + + + + + - + + - - + + - + - + - - - + + + + + + - + + - - + + - + - + - - - + + + + - - + + - + - + - - - + + + - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - + - + - - - + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + - - - - - + + - - + + - - + + - + - +" style="stroke: #10151b"/> - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - + + + + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + diff --git a/docs/assets/validation/twophase_global_melt_fraction_dark.svg b/docs/assets/validation/twophase_global_melt_fraction_dark.svg index 5ba5d86a..78cf8f23 100644 --- a/docs/assets/validation/twophase_global_melt_fraction_dark.svg +++ b/docs/assets/validation/twophase_global_melt_fraction_dark.svg @@ -1,12 +1,12 @@ - + - test: tests/test_twophase.py::test_global_melt_fraction_is_the_mass_weighted_profile_mean; command: pytest tests/test_twophase.py::test_global_melt_fraction_is_the_mass_weighted_profile_mean --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[49, 3528.420367522105, 6337.615609804658, 4865.232006967509, -488.80990442400326, 0.3895779609783869, 0.705065248642743, 0.4906220970877643, -0.03989934434113623], [2, 0.0, 1.0, 0.5, 0.5, 0.5129281701005098, 0.5129281701005098, 0.5129281701005098, 0.0], [2, 0.0, 1.0, 0.5, 0.5, 0.5129281701005103, 0.5129281701005103, 0.5129281701005103, 0.0], [2, 0.0, 1.0, 0.5, 0.5, 0.4906220970877643, 0.4906220970877643, 0.4906220970877643, 0.0], [1, 4865.232006967509, 4865.232006967509, 4865.232006967509, 0.0, 0.51292817010051, 0.51292817010051, 0.51292817010051, 0.0]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} + test: tests/test_twophase.py::test_global_melt_fraction_is_the_mass_weighted_profile_mean; command: pytest tests/test_twophase.py::test_global_melt_fraction_is_the_mass_weighted_profile_mean --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[4, 0.0, 3.0, 1.5, 0.8333333333333333, 0.49, 0.51, 0.505, -0.005000000000000039]], "collections": [], "values": [], "value_scales": [], "patches": [[5, 0.0, 1.0, 0.4, -0.1, 0.509, 0.511, 0.5098, 0.00020000000000000565]], "images": [], "image_scales": [], "extents": []}]} image/svg+xml @@ -21,28 +21,28 @@ - - - + @@ -51,314 +51,341 @@ L 0 3.5 " style="stroke: #9fb0be; stroke-width: 0.8"/> - + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - - + + + + + + + + - - - - - + + + + + + + + + + + + + + + + + + + - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + - - + + - + - + - - - + + + + + + - + + - - + + - + - + - - - + + + + + + - + + - - + + - + - + - - - + + + + - - + + - + - + - - - + + + - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - + - + - - - + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + - - - - - + + - - + + - - + + - + - +" style="stroke: #f2f5f7"/> - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - + + + + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + diff --git a/tests/_plot_utils.py b/tests/_plot_utils.py new file mode 100644 index 00000000..2e963ed1 --- /dev/null +++ b/tests/_plot_utils.py @@ -0,0 +1,28 @@ +"""Display rounding for validation plots that draw values of a SPIDER run.""" + +from __future__ import annotations + +import numpy as np + + +def shown(value, digits=3): + """Round to significant digits, so that round-off between builds does not move a mark. + + The plot check compares the committed plot to about 1e-6 of a coordinate's range, + while run output moves by up to a few 1e-6 between PETSc builds and platforms. + + Parameters + ---------- + value : float or array_like + Value or values from the run. + digits : int + Significant digits kept. + + Returns + ------- + float or numpy.ndarray + The rounded value, a float for a scalar input. + """ + a = np.asarray(value, dtype=float) + out = np.array([float(f'{v:.{digits}g}') for v in a.ravel()]).reshape(a.shape) + return float(out) if out.ndim == 0 else out diff --git a/tests/test_atmosphere.py b/tests/test_atmosphere.py index d8c1c75b..5f6de287 100644 --- a/tests/test_atmosphere.py +++ b/tests/test_atmosphere.py @@ -15,6 +15,7 @@ import pytest from tests._json_utils import atmosphere_si, read_output +from tests._plot_utils import shown pytestmark = [pytest.mark.smoke, pytest.mark.timeout(120)] @@ -41,7 +42,10 @@ def test_grey_body_flux_matches_stefan_boltzmann_limit(blackbody_short, validati t_surf = atmosphere_si(doc, 'temperature_surface') # K fatm = atmosphere_si(doc, 'Fatm') # W/m2 - expected = EMISSIVITY * SIGMA * (t_surf**4 - TEQM**4) + def grey_law(t, power=4): + return EMISSIVITY * SIGMA * (t**power - TEQM**power) + + expected = grey_law(t_surf) # rel=1e-6: the run reproduces the closed form to machine precision; # 1e-6 leaves headroom for compiler value reordering only. assert fatm == pytest.approx(expected, rel=1e-6) @@ -49,7 +53,7 @@ def test_grey_body_flux_matches_stefan_boltzmann_limit(blackbody_short, validati # Exponent guard: a T^3 slip at T_surf ~ 1860 K gives ~360 W/m2 # instead of ~6.7e5 W/m2, off by three orders of magnitude and far # beyond the pin tolerance. - wrong_cubed = EMISSIVITY * SIGMA * (t_surf**3 - TEQM**3) + wrong_cubed = grey_law(t_surf, 3) assert abs(fatm - wrong_cubed) > 0.5 * fatm # Sign guard: the hot interior radiates outward. assert fatm > 0 @@ -65,11 +69,11 @@ def figure(): fig, ax = plt.subplots(figsize=(6, 4)) ax.plot( t, - EMISSIVITY * SIGMA * (t**4 - TEQM**4), + grey_law(t), label='Grey body, $\\epsilon\\sigma(T^4 - T_\\mathrm{eqm}^4)$', ) - ax.plot(t, EMISSIVITY * SIGMA * (t**3 - TEQM**3), '--', label='$T^3$ form') - ax.plot([t_surf], [fatm], 'o', zorder=3, label='Reported Fatm at 200 yr') + ax.plot(t, grey_law(t, 3), '--', label='$T^3$ form') + ax.plot([shown(t_surf)], [shown(fatm)], 'o', zorder=3, label='Reported Fatm at 200 yr') ax.set_yscale('log') ax.set_xlabel('Surface temperature [K]') ax.set_ylabel('Surface flux [W m$^{-2}$]') diff --git a/tests/test_bc.py b/tests/test_bc.py index 74ee05db..cfb858f2 100644 --- a/tests/test_bc.py +++ b/tests/test_bc.py @@ -55,7 +55,10 @@ def test_prescribed_flux_bc_echoes_value_and_inverts_emissivity( # temperature would give ~8.4e5 W/m2, more than a factor of eight # above the prescribed value, so the pin discriminates the # SURFACE_BC dispatch and not a coincidental grey-body agreement. - grey = SIGMA * (t_surf**4 - TEQM**4) + def grey_law(t): + return SIGMA * (t**4 - TEQM**4) + + grey = grey_law(t_surf) assert abs(PRESCRIBED_FLUX - grey) > 0.1 * grey # Emissivity inversion pin: ~0.12 here, clearly away from the # configured emissivity0 = 1.0 of the grey-body branch. @@ -71,10 +74,10 @@ def figure(): t = np.linspace(1000.0, 3000.0, 41) fig, ax = plt.subplots(figsize=(6, 4)) - ax.plot(t, SIGMA * (t**4 - TEQM**4), label='Grey body, emissivity 1') + ax.plot(t, grey_law(t), label='Grey body, emissivity 1') ax.plot( t, - emissivity * SIGMA * (t**4 - TEQM**4), + emissivity * grey_law(t), '--', label=f'Grey body, reported emissivity {emissivity:.3f}', ) @@ -209,12 +212,13 @@ def test_constant_entropy_bc_pins_the_initial_surface( s_b = data_si(read_output(entropy_bc_run, 0), 'S_b') # J/kg/K # rel=1e-10: the value is assigned, not solved for. - assert s_b[0] == pytest.approx(2550.0, rel=1e-10) + s_surface = 2550.0 + assert s_b[0] == pytest.approx(s_surface, rel=1e-10) # Discrimination guard: the default IC surface value differs by # ~50 J/kg/K, five orders above the pin tolerance. s_b_default = data_si(read_output(blackbody_short, 0), 'S_b') - assert abs(s_b_default[0] - 2550.0) > 40.0 # J/kg/K + assert abs(s_b_default[0] - s_surface) > 40.0 # J/kg/K # The core-side prescription seeds the deep profile before the # steady-state solve rebalances it; the result must stay physical @@ -230,7 +234,9 @@ def figure(): fig, ax = plt.subplots(figsize=(6, 4)) ax.plot(r_km, s_b, label='Constant-entropy boundary, $t$ = 0') ax.plot(r_def, s_b_default, '--', label='Default boundary, $t$ = 0') - ax.plot([r_km[0]], [2550.0], 's', mfc='none', ms=10, label='ic_surface_entropy = 2550') + ax.plot( + [r_km[0]], [s_surface], 's', mfc='none', ms=10, label='ic_surface_entropy = 2550' + ) ax.plot([r_km[0]], [s_b[0]], 'o', zorder=3, label='Surface node') ax.set_xlabel('Radius [km]') ax.set_ylabel('Entropy [J kg$^{-1}$ K$^{-1}$]') diff --git a/tests/test_energy.py b/tests/test_energy.py index 446ef2c2..1fb0e353 100644 --- a/tests/test_energy.py +++ b/tests/test_energy.py @@ -15,6 +15,7 @@ import pytest from tests._json_utils import atmosphere_si, data_si, read_output +from tests._plot_utils import shown pytestmark = [pytest.mark.smoke, pytest.mark.timeout(120)] @@ -85,7 +86,8 @@ def test_disabled_transport_mechanisms_carry_zero_flux( # Wrong-path guard: convection must remain active, otherwise the # zero above would only prove a dead run. jconv_nc = data_si(doc_nc, 'Jconv_b') # W/m^2 - assert np.abs(jconv_nc).max() > 1e3 + active_min = 1e3 + assert np.abs(jconv_nc).max() > active_min # Everything but conduction off: Jtot reduces to Jcond alone. cond_only = cached_spider_run( @@ -111,7 +113,7 @@ def test_disabled_transport_mechanisms_carry_zero_flux( np.testing.assert_allclose(jtot_co[1:], jcond_co[1:], rtol=1e-12, atol=1e-20) # Edge case, surface node: the grey-body boundary flux (8e5 W/m^2) # overrides the tiny conductive flux (0.01 W/m^2) at index 0. - assert abs(jtot_co[0] - jcond_co[0]) > 1e3 + assert abs(jtot_co[0] - jcond_co[0]) > active_min # Scale guard on the full-physics reference state: the peak total # flux at the fully molten IC is convective and large (observed @@ -122,21 +124,31 @@ def test_disabled_transport_mechanisms_carry_zero_flux( def figure(): import matplotlib.pyplot as plt - r_nc = data_si(doc_nc, 'radius_b') / 1e3 - r_co = data_si(doc_co, 'radius_b') / 1e3 - off = np.max( - [np.abs(data_si(doc_co, n)) for n in ('Jconv_b', 'Jmix_b', 'Jgrav_b')], axis=0 + off = max(np.abs(data_si(doc_co, n)).max() for n in ('Jconv_b', 'Jmix_b', 'Jgrav_b')) + names = [ + 'Off: Jcond', + 'Only conduction:\nJconv, Jmix, Jgrav', + 'Off: Jconv', + 'Only conduction:\nJtot - Jcond, top', + ] + values = shown( + [ + np.abs(jcond_nc).max(), + off, + np.abs(jconv_nc).max(), + abs(jtot_co[0] - jcond_co[0]), + ], + digits=2, ) fig, ax = plt.subplots(figsize=(6, 4)) - ax.plot(r_nc, jconv_nc, label='Conduction off: Jconv') - ax.plot(r_nc, jcond_nc, '--', label='Conduction off: Jcond') - ax.plot(r_co, jcond_co, '-.', label='Conduction only: Jcond') - ax.plot(r_co, off, ':', label='Conduction only: Jconv, Jmix, Jgrav') + ax.axhline(active_min, ls='--', label='Lower bound of the 2 active fluxes, 1e3') + ax.plot(range(4), values, 'o', zorder=3, label='Largest |flux| over the nodes') + ax.set_xticks(range(4), names, fontsize=8) ax.set_yscale('symlog', linthresh=1e-3) ax.set_yticks([0.0, 1e-2, 1e2, 1e6, 1e10]) - ax.set_xlabel('Radius [km]') + ax.set_ylim(-1e-3, 1e12) ax.set_ylabel('Flux [W m$^{-2}$]') - ax.legend(fontsize=8) + ax.legend(fontsize=8, loc='upper left') fig.tight_layout() return fig @@ -246,8 +258,13 @@ def test_radiogenic_heating_pins_two_isotope_decay(heating_run, validation_plot) 3.57676e-6 W/kg, and after 200 years (two al26 half-lives, one k40 half-life) the total falls to 0.2506 of its initial value. """ + t_late = 200 + + def isotopes(t): + return H_AL26 * 2.0 ** (-t / 100.0), H_K40 * 2.0 ** (-t / 200.0) + h_0 = data_si(read_output(heating_run, 0), 'Hradio_s') # W/kg - h_200 = data_si(read_output(heating_run, 200), 'Hradio_s') # W/kg + h_200 = data_si(read_output(heating_run, t_late), 'Hradio_s') # W/kg # Radiogenic heating is spatially uniform: the same isotopes and # concentration heat every shell (rel 1e-12 is roundoff headroom). @@ -261,7 +278,7 @@ def test_radiogenic_heating_pins_two_isotope_decay(heating_run, validation_plot) assert abs(h_0[0] - H_AL26) > 1e-3 * h_0[0] # Decay pin: the two-isotope weighted decay factor after 200 years. - expected_ratio = (H_AL26 * 0.25 + H_K40 * 0.5) / (H_AL26 + H_K40) + expected_ratio = sum(isotopes(t_late)) / sum(isotopes(0.0)) ratio = h_200[0] / h_0[0] # rel=1e-4: the run writes output exactly at t = 200 years, so the # closed-form factor applies with only output rounding on top. @@ -279,13 +296,13 @@ def figure(): import matplotlib.pyplot as plt t = np.linspace(0.0, 300.0, 61) - al26, k40 = H_AL26 * 2.0 ** (-t / 100.0), H_K40 * 2.0 ** (-t / 200.0) + al26, k40 = isotopes(t) fig, ax = plt.subplots(figsize=(6, 4)) ax.plot(t, al26 + k40, label='Sum of the 2 isotopes') ax.plot(t, al26, '--', label='al26 alone, half-life 100 yr') ax.plot(t, k40, ':', label='k40 alone, half-life 200 yr') ax.plot( - [0.0, 200.0], + [0.0, t_late], [h_0[0], h_200[0]], 'o', zorder=3, @@ -415,13 +432,17 @@ def test_steady_state_ic_balances_interior_and_surface(steady_state_run, validat # Grey-body pin with the sigma hard-coded in constants.c. sigma = 5.670367e-8 # W m^-2 K^-4 - expected = sigma * (t_surf**4 - 273.0**4) + + def grey_law(t, power=4): + return sigma * (t**power - 273.0**power) + + expected = grey_law(t_surf) # rel=1e-5: the radiation balance is solved to snes_rtol 1e-9; the # margin absorbs output rounding of T_surf. assert fatm == pytest.approx(expected, rel=1e-5) # Exponent guard: a T^3 slip misses by roughly a factor of T_surf # (~2000 at the ~1960 K of this state), far beyond any tolerance. - assert abs(fatm - sigma * (t_surf**3 - 273.0**3)) > 0.5 * fatm + assert abs(fatm - grey_law(t_surf, 3)) > 0.5 * fatm # Sign and scale guards. assert fatm > 0 assert 1e2 < fatm < 1e9 # W/m^2 @@ -437,8 +458,8 @@ def figure(): left.set_yticks([0.0, etot.max()]) left.set_xlabel('Radius [km]') left.set_ylabel('Etot [W]') - right.plot(t, sigma * (t**4 - 273.0**4), label='$\\sigma(T^4 - 273^4)$') - right.plot(t, sigma * (t**3 - 273.0**3), '--', label='$T^3$ form') + right.plot(t, grey_law(t), label='$\\sigma(T^4 - 273^4)$') + right.plot(t, grey_law(t, 3), '--', label='$T^3$ form') right.plot([t_surf], [fatm], 'o', zorder=3, label='Reported Fatm') right.set_yscale('log') right.set_xlabel('Surface temperature [K]') diff --git a/tests/test_eos.py b/tests/test_eos.py index 21ab6d29..9a56f2c3 100644 --- a/tests/test_eos.py +++ b/tests/test_eos.py @@ -82,7 +82,8 @@ def test_phase_boundary_matches_independent_table_read(c_test, validation_plot): assert out['phase_boundary_S'][0] == pytest.approx(ya[0], rel=1e-12) # Wrong-table guard: the solidus at 10 GPa sits near 1524 J/kg/K, # far below the liquidus value near 2128 J/kg/K. - assert abs(out['phase_boundary_S'][1] - 1524.43) > 100.0 + solidus_10gpa = 1524.43 + assert abs(out['phase_boundary_S'][1] - solidus_10gpa) > 100.0 # Sign and scale guards: liquidus entropies of the dK09 set are # positive and of order a few thousand J/kg/K. assert out['phase_boundary_S'][1] > 0 @@ -96,7 +97,7 @@ def figure(): ax.plot( xa[keep][::10] / 1e9, ya[keep][::10], label='liquidus_A11_H13.dat, read by numpy' ) - ax.plot([10.0], [1524.43], 'x', ms=10, label='Solidus at 10 GPa') + ax.plot([probes[1] / 1e9], [solidus_10gpa], 'x', ms=10, label='Solidus at 10 GPa') ax.plot( np.array(probes) / 1e9, out['phase_boundary_S'], @@ -136,11 +137,16 @@ def test_compositional_viscosity_matches_spaargaren(c_test, validation_plot): shift adds to the configured melt log10visc of 2.0. """ probe = ((1.0e10,), (2600.0,)) - high = _eval_with(c_test, ('-melt_visc_comp', '1.4', '-melt_visc_ref_comp', '1.6'), *probe) - low = _eval_with(c_test, ('-melt_visc_comp', '1.2', '-melt_visc_ref_comp', '0.9'), *probe) + comp_high, ref_high, comp_low, ref_low, base_log = 1.4, 1.6, 1.2, 0.9, 2.0 + high = _eval_with( + c_test, ('-melt_visc_comp', comp_high, '-melt_visc_ref_comp', ref_high), *probe + ) + low = _eval_with( + c_test, ('-melt_visc_comp', comp_low, '-melt_visc_ref_comp', ref_low), *probe + ) - shift_high = _spaargaren_prefactor(1.4) - _spaargaren_prefactor(1.6) - shift_low = _spaargaren_prefactor(1.2) - _spaargaren_prefactor(0.9) + shift_high = _spaargaren_prefactor(comp_high) - _spaargaren_prefactor(ref_high) + shift_low = _spaargaren_prefactor(comp_low) - _spaargaren_prefactor(ref_low) # Transcription pins: the helper re-encodes the published piecewise # coefficients independently of eos.c; these literals (hand-derived # from Spaargaren et al. 2020) guard the helper itself, so a shared @@ -149,8 +155,8 @@ def test_compositional_viscosity_matches_spaargaren(c_test, validation_plot): assert shift_low == pytest.approx(-1.35803, abs=1e-4) # abs=1e-9: the prefactor is a closed-form expression of the two # option values, so only roundoff enters. - assert high['log10visc'][0] == pytest.approx(2.0 + shift_high, abs=1e-9) - assert low['log10visc'][0] == pytest.approx(2.0 + shift_low, abs=1e-9) + assert high['log10visc'][0] == pytest.approx(base_log + shift_high, abs=1e-9) + assert low['log10visc'][0] == pytest.approx(base_log + shift_low, abs=1e-9) # Branch discrimination: the two configurations shift in opposite # directions and differ by more than 1.5 decades, so a collapsed @@ -161,7 +167,7 @@ def test_compositional_viscosity_matches_spaargaren(c_test, validation_plot): # Edge case: with the compositional term disabled (no visc_comp # option) the melt viscosity is the bare configured constant. base = _eval(c_test, 'melt', *probe) - assert base['log10visc'][0] == pytest.approx(2.0, abs=1e-10) + assert base['log10visc'][0] == pytest.approx(base_log, abs=1e-10) def figure(): import matplotlib.pyplot as plt @@ -174,18 +180,18 @@ def figure(): label='Spaargaren et al. (2020) prefactor', ) ax.plot( - [1.6, 0.9], - [_spaargaren_prefactor(1.6), _spaargaren_prefactor(0.9)], + [ref_high, ref_low], + [_spaargaren_prefactor(ref_high), _spaargaren_prefactor(ref_low)], 's', mfc='none', ms=10, label='Reference compositions', ) code = [ - _spaargaren_prefactor(1.6) + high['log10visc'][0] - 2.0, - _spaargaren_prefactor(0.9) + low['log10visc'][0] - 2.0, + _spaargaren_prefactor(ref_high) + high['log10visc'][0] - base_log, + _spaargaren_prefactor(ref_low) + low['log10visc'][0] - base_log, ] - ax.plot([1.4, 1.2], code, 'o', zorder=3, label='Reference + eos.c shift') + ax.plot([comp_high, comp_low], code, 'o', zorder=3, label='Reference + eos.c shift') ax.set_xlabel('Mg/Si') ax.set_ylabel('log$_{10}$ viscosity prefactor') ax.legend(fontsize=8) diff --git a/tests/test_eos_adamswilliamson.py b/tests/test_eos_adamswilliamson.py index 3cc655a9..50f3722a 100644 --- a/tests/test_eos_adamswilliamson.py +++ b/tests/test_eos_adamswilliamson.py @@ -61,14 +61,18 @@ def test_density_matches_the_closed_form_profile(c_test, validation_plot): """ probes = (0.0, 1.0e10, 1.35e11) out = _aw_eval(c_test, probes) - expected = [RHOS - p * BETA / GRAVITY for p in probes] + + def aw(p, sign=-1.0): + return RHOS + sign * p * BETA / GRAVITY + + expected = [aw(p) for p in probes] assert out['rho'] == pytest.approx(expected, rel=1e-12) # Surface limit: exactly the configured surface density. assert out['rho'][0] == pytest.approx(RHOS, rel=1e-12) # Sign-of-beta guard: a sign-flipped compressibility would give # 3967.8 kg/m^3 at 10 GPa instead of 4190.1; both sit far outside # the tolerance. - wrong_sign = RHOS + 1.0e10 * BETA / GRAVITY + wrong_sign = aw(probes[1], sign=1.0) assert abs(out['rho'][1] - wrong_sign) > 100.0 # Monotonicity under compression and physical scale (silicate # mantle densities, thousands of kg/m^3). @@ -81,8 +85,8 @@ def figure(): p = np.linspace(0.0, 1.4e11, 29) fig, ax = plt.subplots(figsize=(6, 4)) - ax.plot(p / 1e9, RHOS - p * BETA / GRAVITY, label=r'$\rho_s - P\beta/g$') - ax.plot(p / 1e9, RHOS + p * BETA / GRAVITY, '--', label=r'Sign-flipped $\beta$') + ax.plot(p / 1e9, aw(p), label=r'$\rho_s - P\beta/g$') + ax.plot(p / 1e9, aw(p, sign=1.0), '--', label=r'Sign-flipped $\beta$') ax.plot( np.array(probes) / 1e9, out['rho'], diff --git a/tests/test_eos_composite.py b/tests/test_eos_composite.py index 82600eef..fb677403 100644 --- a/tests/test_eos_composite.py +++ b/tests/test_eos_composite.py @@ -74,20 +74,27 @@ def test_fusion_equals_liquidus_minus_solidus_and_phi_is_linear_in_entropy( (fusion, about 604 J/kg/K here) from the plausible wrong one (the liquidus entropy, about 2128 J/kg/K). """ - out = _composite_eval(c_test, (1675.34,)) + s_probe = 1675.34 + out = _composite_eval(c_test, (s_probe,)) liq = out['liquidus_S'][0] sol = out['solidus_S'][0] fusion = out['fusion'][0] assert fusion == pytest.approx(liq - sol, rel=1e-10) # Liquidus must sit above solidus for the loaded tables. assert liq > sol + # The probe entropy is S_solidus + 0.25 * fusion for the shipped # tables (1524.43 + 151.0); a wrong-denominator regression would # return (S - sol)/liq of about 0.071 instead of 0.25. - s_probe = 1675.34 - expected_phi = (s_probe - sol) / fusion + def phi_law(s): + return (s - sol) / fusion + + def wrong_phi(s): + return (s - sol) / liq + + expected_phi = phi_law(s_probe) assert out['phase_fraction'][0] == pytest.approx(expected_phi, rel=1e-6) - assert abs(out['phase_fraction'][0] - (s_probe - sol) / liq) > 0.15 + assert abs(out['phase_fraction'][0] - wrong_phi(s_probe)) > 0.15 # Scale guard: the fusion entropy of the dK09 tables at 10 GPa is a # few hundred J/kg/K, not a few or a few thousand. assert 100.0 < fusion < 1500.0 @@ -100,12 +107,12 @@ def figure(): fig, ax = plt.subplots(figsize=(6, 4)) ax.plot( s, - np.clip((s - sol) / fusion, 0.0, 1.0), + np.clip(phi_law(s), 0.0, 1.0), label='$(S - S_\\mathrm{sol}) / (S_\\mathrm{liq} - S_\\mathrm{sol})$', ) ax.plot( s, - np.clip((s - sol) / liq, 0.0, 1.0), + np.clip(wrong_phi(s), 0.0, 1.0), '--', label='$(S - S_\\mathrm{sol}) / S_\\mathrm{liq}$', ) diff --git a/tests/test_interp.py b/tests/test_interp.py index cb6822a1..08e03111 100644 --- a/tests/test_interp.py +++ b/tests/test_interp.py @@ -62,17 +62,23 @@ def test_affine_data_reproduced_exactly_including_last_node( table node exactly, the edge where an index search may step out of the array. """ + slope, intercept, queries = 2.0, 1.0, (0.0, 2.5, 7.0, 10.0) + + def affine(x): + return slope * x + intercept + out = c_test( 'test_interp', - ('-file_1d', str(linear_table_1d), '-x_1d', '0.0,2.5,7.0,10.0'), + ('-file_1d', str(linear_table_1d), '-x_1d', ','.join(str(q) for q in queries)), ) - assert out['y_1d'] == pytest.approx([1.0, 6.0, 15.0, 21.0], rel=1e-14) + assert out['y_1d'] == pytest.approx([affine(q) for q in queries], rel=1e-14) # The derivative of the affine data is the constant slope 2. - assert out['dydx_1d'] == pytest.approx([2.0] * 4, rel=1e-14) + assert out['dydx_1d'] == pytest.approx([slope] * 4, rel=1e-14) # Nearest-neighbour discrimination: at x = 2.5 the neighbouring node # values are 5 and 7; both sit far outside the tolerance around 6. - assert abs(out['y_1d'][1] - 5.0) > 0.5 - assert abs(out['y_1d'][1] - 7.0) > 0.5 + neighbours = (affine(2.0), affine(3.0)) + assert abs(out['y_1d'][1] - neighbours[0]) > 0.5 + assert abs(out['y_1d'][1] - neighbours[1]) > 0.5 # Sign and scale guards on the pinned interior value. assert out['y_1d'][1] > 0 assert 1.0 < out['y_1d'][1] < 21.0 @@ -84,17 +90,19 @@ def figure(): fig, ax = plt.subplots(figsize=(6, 4)) ax.plot( nodes, - [2 * x + 1 for x in nodes], + [affine(x) for x in nodes], 's', mfc='none', ms=8, label='Table nodes of $y = 2x + 1$', ) - ax.plot([0.0, 10.0], [1.0, 21.0], label='$y = 2x + 1$') - ax.plot([2.5, 2.5], [5.0, 7.0], 'x', ms=10, label='Nearest nodes at $x$ = 2.5') ax.plot( - [0.0, 2.5, 7.0, 10.0], out['y_1d'], 'o', zorder=3, label='interp.c at the 4 queries' + [queries[0], queries[-1]], + [affine(queries[0]), affine(queries[-1])], + label='$y = 2x + 1$', ) + ax.plot([queries[1]] * 2, neighbours, 'x', ms=10, label='Nearest nodes at $x$ = 2.5') + ax.plot(queries, out['y_1d'], 'o', zorder=3, label='interp.c at the 4 queries') ax.set_xlabel('$x$') ax.set_ylabel('$y$') ax.legend(fontsize=8) diff --git a/tests/test_mesh.py b/tests/test_mesh.py index e080d818..754dcfe8 100644 --- a/tests/test_mesh.py +++ b/tests/test_mesh.py @@ -47,7 +47,16 @@ def test_pressure_profile_matches_adams_williamson_closed_form( radius_b = data_si(doc, 'radius_b') # m pressure_b = data_si(doc, 'pressure_b') # Pa - expected = -RHOS * GRAVITY / BETA * (np.exp(BETA * (RADIUS - radius_b)) - 1.0) + def p_aw(depth): + return -RHOS * GRAVITY / BETA * (np.exp(BETA * depth) - 1.0) + + def p_linear(depth): + return RHOS * abs(GRAVITY) * depth + + def p_flipped(depth): + return RHOS * GRAVITY / BETA * (np.exp(-BETA * depth) - 1.0) + + expected = p_aw(RADIUS - radius_b) # rtol=1e-12: mesh.c evaluates the same closed form, so agreement is # limited only by the nondimensional round-trip through the JSON # output (observed max rel 6e-15, so the pin has 100x headroom). @@ -68,8 +77,8 @@ def test_pressure_profile_matches_adams_williamson_closed_form( # 27% low, both vastly beyond the 1e-12 pin tolerance. p_cmb = pressure_b[-1] depth_cmb = RADIUS - radius_b[-1] - linear = RHOS * abs(GRAVITY) * depth_cmb - flipped_beta = RHOS * GRAVITY / BETA * (np.exp(-BETA * depth_cmb) - 1.0) + linear = p_linear(depth_cmb) + flipped_beta = p_flipped(depth_cmb) assert abs(p_cmb - linear) > 0.1 * p_cmb assert abs(p_cmb - flipped_beta) > 0.1 * p_cmb @@ -81,13 +90,13 @@ def figure(): fig, ax = plt.subplots(figsize=(6, 4)) ax.plot( r / 1e3, - -RHOS * GRAVITY / BETA * (np.exp(BETA * depth) - 1.0) / 1e9, + p_aw(depth) / 1e9, label='Adams-Williamson closed form', ) - ax.plot(r / 1e3, RHOS * abs(GRAVITY) * depth / 1e9, '--', label='Constant density') + ax.plot(r / 1e3, p_linear(depth) / 1e9, '--', label='Constant density') ax.plot( r / 1e3, - RHOS * GRAVITY / BETA * (np.exp(-BETA * depth) - 1.0) / 1e9, + p_flipped(depth) / 1e9, ':', label=r'Sign-flipped $\beta$', ) diff --git a/tests/test_reaction.py b/tests/test_reaction.py index 1919a16b..54cb7690 100644 --- a/tests/test_reaction.py +++ b/tests/test_reaction.py @@ -14,6 +14,7 @@ import pytest from tests._json_utils import field_si, read_output +from tests._plot_utils import shown pytestmark = [pytest.mark.smoke, pytest.mark.timeout(120)] @@ -162,18 +163,19 @@ def figure(): fig, ax = plt.subplots(figsize=(6, 4)) ax.bar( [-0.2, 0.8], - [h2['reaction_kg'], c_lost_by_co], + shown([h2['reaction_kg'], c_lost_by_co], digits=2), width=0.4, label='Lost by H$_2$ (H), by CO (C)', ) ax.bar( [0.2, 1.2], - [h_from_h2o, c_gained_by_co2], + shown([h_from_h2o, c_gained_by_co2], digits=2), width=0.4, label='Gained in H$_2$O (H), in CO$_2$ (C)', ) ax.set_xticks([0, 1], ['Hydrogen', 'Carbon']) ax.set_yscale('log') + ax.set_ylim(1e16, 1e19) ax.set_ylabel('Mass moved in 1000 yr [kg]') ax.legend(fontsize=8) fig.tight_layout() diff --git a/tests/test_rheologicalfront.py b/tests/test_rheologicalfront.py index 90c3e98c..1e6e6b0c 100644 --- a/tests/test_rheologicalfront.py +++ b/tests/test_rheologicalfront.py @@ -17,6 +17,7 @@ import pytest from tests._json_utils import data_si, field_si, read_output +from tests._plot_utils import shown pytestmark = [pytest.mark.smoke, pytest.mark.timeout(120)] @@ -80,27 +81,22 @@ def test_front_location_is_consistent_with_melt_fraction_profile( def figure(): import matplotlib.pyplot as plt - r_s = data_si(doc, 'radius_s') / 1e3 + r_s = shown(data_si(doc, 'radius_s')[[crossing - 1, crossing]] / 1e3) fig, ax = plt.subplots(figsize=(6, 4)) - ax.plot(r_s, phi_s, label='Melt fraction at 1200 yr') - ax.axhline(PHI_CRITICAL, ls='--', label='$\\phi_c$ = 0.4') + ax.plot([0, 2], r_s, 's', mfc='none', ms=10, label='Staggered nodes') ax.plot( - [r_s[crossing]], - [phi_s[crossing]], - 's', - mfc='none', - ms=10, - label='First node below $\\phi_c$', - ) - ax.plot( - [(radius_b[0] - front['depth']) / 1e3], - [PHI_CRITICAL], + [1], + [shown((radius_b[0] - front['depth']) / 1e3)], 'o', zorder=3, label='Reported front radius', ) - ax.set_xlabel('Radius [km]') - ax.set_ylabel('Melt fraction') + ax.set_xticks( + [0, 1, 2], + ['Last node,\n$\\phi \\geq \\phi_c$', 'Front', 'First node,\n$\\phi < \\phi_c$'], + ) + ax.set_xlim(-0.5, 2.5) + ax.set_ylabel('Radius [km]') ax.legend(fontsize=8) fig.tight_layout() return fig diff --git a/tests/test_twophase.py b/tests/test_twophase.py index a8cd55d1..539ced36 100644 --- a/tests/test_twophase.py +++ b/tests/test_twophase.py @@ -14,6 +14,7 @@ import pytest from tests._json_utils import atmosphere_si, data_si, field_si, read_output +from tests._plot_utils import shown pytestmark = [pytest.mark.smoke, pytest.mark.timeout(120)] @@ -86,7 +87,8 @@ def test_global_melt_fraction_is_the_mass_weighted_profile_mean( phi_reported = float(field_si(doc['rheological_front_phi']['phi_global'])[0]) m_liq = atmosphere_si(doc, 'mass_liquid') m_man = atmosphere_si(doc, 'mass_mantle') - assert phi_reported == pytest.approx(m_liq / m_man, rel=1e-10) + liquid_share = m_liq / m_man + assert phi_reported == pytest.approx(liquid_share, rel=1e-10) phi_s = data_si(doc, 'phi_s') mass_s = data_si(doc, 'mass_s') @@ -96,7 +98,8 @@ def test_global_melt_fraction_is_the_mass_weighted_profile_mean( # the plain average of phi_s differs from the mass-weighted mean by # more than the tolerance. unweighted = float(phi_s.mean()) - assert abs(phi_reported - unweighted) > 1e-3 + apart = 1e-3 + assert abs(phi_reported - unweighted) > apart # Boundedness and scale guards. assert 0.0 <= phi_reported <= 1.0 assert 0.2 < phi_reported < 0.9 @@ -104,16 +107,16 @@ def test_global_melt_fraction_is_the_mass_weighted_profile_mean( def figure(): import matplotlib.pyplot as plt - r_s = data_si(doc, 'radius_s') / 1e3 + names = ['Reported', 'Mliq / Mmantle', 'Mass-weighted', 'Unweighted'] + values = shown([phi_reported, liquid_share, weighted, unweighted], digits=2) fig, ax = plt.subplots(figsize=(6, 4)) - ax.plot(r_s, phi_s, label='Melt fraction at 1200 yr') - ax.axhline(phi_reported, ls='-.', label='Reported phi_global') - ax.axhline(weighted, ls='--', label='Mass-weighted mean') - ax.axhline(unweighted, ls=':', label='Unweighted mean') - ax.plot([r_s.mean()], [m_liq / m_man], 's', mfc='none', ms=10, label='Mliq / Mmantle') - ax.set_xlabel('Radius [km]') - ax.set_ylabel('Melt fraction') - ax.legend(fontsize=8) + ax.axhspan( + values[0] - apart, values[0] + apart, alpha=0.2, label='Reported value $\\pm$ 1e-3' + ) + ax.plot(range(4), values, 'o', zorder=3, label='Melt fraction at 1200 yr') + ax.set_xticks(range(4), names, fontsize=9) + ax.set_ylabel('Global melt fraction') + ax.legend(fontsize=8, loc='center left') fig.tight_layout() return fig From 154db2c8ddb33f435199704a31e5b42d196fafb3 Mon Sep 17 00:00:00 2001 From: timlichtenberg Date: Mon, 26 Oct 2026 20:51:44 +0100 Subject: [PATCH 11/15] Give the rhs plot its data summary and tighten the read texts The finite-difference plot is written again with its data summary, so the stale-plot check covers it. The read texts keep each sentence under 25 words, define their symbols, use the cell-boundary and cell-centre names of the mesh nodes, and state that the asserts check the unrounded values. --- docs/Validation/atmosphere.md | 2 +- docs/Validation/bc.md | 4 ++-- docs/Validation/energy.md | 6 +++--- docs/Validation/eos.md | 4 ++-- docs/Validation/eos_adamswilliamson.md | 2 +- docs/Validation/eos_composite.md | 2 +- docs/Validation/eos_lookup.md | 2 +- docs/Validation/ic.md | 6 +++--- docs/Validation/interp.md | 2 +- docs/Validation/matprop.md | 2 +- docs/Validation/mesh.md | 2 +- docs/Validation/reaction.md | 2 +- docs/Validation/rheologicalfront.md | 2 +- docs/Validation/rhs.md | 2 +- docs/Validation/twophase.md | 2 +- docs/assets/validation/rhs_dsdt_finite_difference.svg | 2 +- docs/assets/validation/rhs_dsdt_finite_difference_dark.svg | 2 +- 17 files changed, 23 insertions(+), 23 deletions(-) diff --git a/docs/Validation/atmosphere.md b/docs/Validation/atmosphere.md index 968f9dd2..d03008e5 100644 --- a/docs/Validation/atmosphere.md +++ b/docs/Validation/atmosphere.md @@ -23,7 +23,7 @@ The section below details the validation check, its tolerance, and its test comm ![Surface flux of the blackbody50 run at 200 yr on the grey-body law](../assets/validation/atmosphere_grey_body_flux.svg#only-light){ width="80%" } ![Surface flux of the blackbody50 run at 200 yr on the grey-body law](../assets/validation/atmosphere_grey_body_flux_dark.svg#only-dark){ width="80%" } -**How to read the plot.** The x-axis is the surface temperature and the y-axis the surface flux on a log scale. The line is the grey-body law $\epsilon\sigma(T^4 - T_\mathrm{eqm}^4)$ with emissivity 1 and $T_\mathrm{eqm}$ = 273 K, and the dashed line the $T^3$ form that the test rejects (context). The circle is the asserted Fatm of the blackbody50 run at 200 yr, at its reported surface temperature; values shown to 3 significant digits, because the run output moves by about 1e-6 between PETSc builds. A pass means that the circle lies on the line (relative 1e-6). +**How to read the plot.** The x-axis is the surface temperature and the y-axis the surface flux on a log scale. The line is the grey-body law $\epsilon\sigma(T^4 - T_\mathrm{eqm}^4)$, with emissivity $\epsilon$ = 1 and equilibrium temperature $T_\mathrm{eqm}$ = 273 K. Here $\sigma$ is the Stefan-Boltzmann constant, and the dashed line is the $T^3$ form, which the test rejects. The circle is the asserted surface flux Fatm of the blackbody50 run at 200 yr, at its reported surface temperature. Its values are shown to 3 significant digits, because the run output moves by about 1e-6 between builds of the PETSc library. A pass means that the circle lies on the line; the test asserts relative 1e-6 on the unrounded values. ??? info "Test details" - Reference: Analytical limit: the grey-body flux F = emissivity sigma (T_surf^4 - T_eqm^4) with emissivity 1 and T_eqm = 273 K (Stefan-Boltzmann). diff --git a/docs/Validation/bc.md b/docs/Validation/bc.md index 3e2cf93c..8b8442a3 100644 --- a/docs/Validation/bc.md +++ b/docs/Validation/bc.md @@ -23,7 +23,7 @@ The sections below detail two validation cases, their tolerances, and their test ![Reported surface flux of the prescribed-flux run against the grey-body law with emissivity 1 and with the reported emissivity](../assets/validation/bc_prescribed_flux.svg#only-light){ width="80%" } ![Reported surface flux of the prescribed-flux run against the grey-body law with emissivity 1 and with the reported emissivity](../assets/validation/bc_prescribed_flux_dark.svg#only-dark){ width="80%" } -**How to read the plot.** The x-axis is the surface temperature and the y-axis the surface flux on a log scale. The dotted line is the prescribed flux, 1e5 W/m$^2$, and the circle the asserted Fatm at the reported surface temperature. The solid line is the grey-body law with emissivity 1, which the test requires to be more than 10 % away at that temperature. The dashed line is the grey-body law with the asserted inverted emissivity (about 0.12). A pass means that the circle sits on the dotted line and on the dashed line, far below the solid one. +**How to read the plot.** The x-axis is the surface temperature and the y-axis the surface flux on a log scale. The dotted line is the prescribed flux, 1e5 W/m$^2$, and the circle the asserted surface flux Fatm at the reported surface temperature. The solid line is the grey-body law with emissivity 1; the test requires it to be more than 10 % away at that temperature. The dashed line is the grey-body law with the asserted inverted emissivity, about 0.12. A pass means that the circle sits on the dotted and the dashed line, far below the solid one. ??? info "Test details" - Reference: Analytical identity: with SURFACE_BC 4 the surface flux equals the prescribed value, and the reported emissivity is its grey-body inversion. @@ -47,7 +47,7 @@ The sections below detail two validation cases, their tolerances, and their test ![Initial entropy profiles with the constant-entropy boundary and with the default boundary](../assets/validation/bc_constant_entropy_surface.svg#only-light){ width="80%" } ![Initial entropy profiles with the constant-entropy boundary and with the default boundary](../assets/validation/bc_constant_entropy_surface_dark.svg#only-dark){ width="80%" } -**How to read the plot.** The x-axis is the radius and the y-axis the entropy at the basic nodes at $t$ = 0. The solid line is the asserted profile of the constant-entropy run and the dashed line the default run (context, its surface value is the 2599.7 that the test requires to be more than 40 away). The circle is the asserted surface node and the hollow square the option value 2550 J kg$^{-1}$ K$^{-1}$. A pass means that the circle sits in the square and the deep end of the solid line lies between 2400 and 2700. +**How to read the plot.** The x-axis is the radius and the y-axis the entropy at the basic nodes (the cell boundaries of the mesh) at $t$ = 0. The solid line is the asserted profile of the run with a constant-entropy surface boundary. The dashed line is the default run (context); its surface entropy, 2599.7 J kg$^{-1}$ K$^{-1}$, must differ from 2550 by more than 40. The circle is the asserted surface node, and the hollow square the value 2550 J kg$^{-1}$ K$^{-1}$ of the option `ic_surface_entropy`. A pass means that the circle sits in the square and the deep end of the solid line lies between 2400 and 2700. ??? info "Test details" - Reference: Analytical identity: the constant-entropy boundary writes ic_surface_entropy (2550 J/kg/K) onto the surface basic node, preserved through the steady-state energy solve into the initial output. diff --git a/docs/Validation/energy.md b/docs/Validation/energy.md index 9af657d7..f747d7fe 100644 --- a/docs/Validation/energy.md +++ b/docs/Validation/energy.md @@ -23,7 +23,7 @@ The sections below detail three validation cases, their tolerances, and their te ![Largest flux of each transport term in the conduction-off and the conduction-only runs](../assets/validation/energy_disabled_transport.svg#only-light){ width="80%" } ![Largest flux of each transport term in the conduction-off and the conduction-only runs](../assets/validation/energy_disabled_transport_dark.svg#only-dark){ width="80%" } -**How to read the plot.** Each circle is the largest absolute flux over the basic nodes of one run, on a symmetric log axis (linear between $-10^{-3}$ and $10^{-3}$); values shown to 2 significant digits, because the run output moves by up to a few 1e-6 between PETSc builds. The first 2 circles are asserted to be 0, to an absolute tolerance of 1e-20 W/m$^2$: the conductive flux with conduction off, and the convective, mixing and separation fluxes with conduction only. The last 2 are asserted to be above $10^3$ W/m$^2$ (dashed line): the convective flux with conduction off, and the gap between the total and the conductive flux at the top node with conduction only. A pass means that the first 2 circles sit at 0 and the last 2 above the dashed line. +**How to read the plot.** Each circle is the largest absolute flux over the basic nodes of one run, on a symmetric log axis (linear between $-10^{-3}$ and $10^{-3}$). Values are shown to 2 significant digits, because the run output moves by up to a few 1e-6 between builds of the PETSc library. The first 2 circles are asserted to be 0, to 1e-20 W/m$^2$. They are the conductive flux with conduction off, and the convective, mixing and separation fluxes with conduction only. The last 2 circles are asserted to be above $10^3$ W/m$^2$ (dashed line). They are the convective flux with conduction off, and the gap between total and conductive flux at the top node with conduction only. A pass means that the first 2 circles sit at 0 and the last 2 above the dashed line. ??? info "Test details" - Reference: Analytical limit: a disabled transport mechanism carries zero flux, and with only conduction enabled the total flux equals the conductive flux at every node except the surface node, which carries the grey-body boundary flux. @@ -47,7 +47,7 @@ The sections below detail three validation cases, their tolerances, and their te ![Radiogenic heating of the 2-isotope run against time](../assets/validation/energy_radiogenic_decay.svg#only-light){ width="80%" } ![Radiogenic heating of the 2-isotope run against time](../assets/validation/energy_radiogenic_decay_dark.svg#only-dark){ width="80%" } -**How to read the plot.** The x-axis is the time and the y-axis the specific radiogenic heating on a log scale. The solid line is the sum of the 2 isotopes in closed form, and the dashed and dotted lines are al26 and k40 alone (context). The circles are the asserted Hradio of the run at 0 and 200 yr. A pass means that both circles lie on the solid line: the sum at $t$ = 0 within relative 1e-6, and the ratio of the 2 within relative 1e-4 of 0.2506. +**How to read the plot.** The x-axis is the time and the y-axis the specific radiogenic heating on a log scale. The solid line is the sum of both isotopes in closed form. The dashed and dotted lines are the isotopes `al26` and `k40` alone, with test half-lives of 100 and 200 yr (context). The circles are the asserted heating Hradio of the run at 0 and 200 yr. A pass means that both lie on the solid line. The test asserts the value at $t$ = 0 to relative 1e-6. It asserts the ratio of the 200 yr to the 0 yr value as 0.2506, to relative 1e-4. ??? info "Test details" - Reference: Analytical limit: radiogenic heating is the sum over isotopes of concentration times abundance times heat production, decaying as 2^(-(t - t0)/half_life). A two-isotope configuration with 100-year and 200-year half-lives pins the sum at t = 0 and the weighted decay factor 0.2506 after 200 years. @@ -71,7 +71,7 @@ The sections below detail three validation cases, their tolerances, and their te ![Energy flow against radius and surface flux on the grey-body law for the steady-state initial condition](../assets/validation/energy_steady_state_ic.svg#only-light){ width="80%" } ![Energy flow against radius and surface flux on the grey-body law for the steady-state initial condition](../assets/validation/energy_steady_state_ic_dark.svg#only-dark){ width="80%" } -**How to read the plot.** Left: the asserted energy flow Etot at every basic node of the steady-state initial condition against radius; a pass means a flat line (relative spread below 1e-8). Right: the grey-body law $\sigma(T^4 - 273^4)$ (line) and the $T^3$ form that the test rejects (dashed) against surface temperature, with the asserted Fatm at the reported surface temperature (circle); a pass means that the circle lies on the line (relative 1e-5). +**How to read the plot.** Left: the asserted energy flow Etot at every basic node of the steady-state initial condition, against radius. A pass means a flat line. The test asserts a relative spread below 1e-8. Right: the grey-body law $\sigma(T^4 - 273^4)$, with $\sigma$ the Stefan-Boltzmann constant (line), and the $T^3$ form that the test rejects (dashed). The circle is the asserted surface flux Fatm at the reported surface temperature. A pass means that the circle lies on the line (relative 1e-5). ??? info "Test details" - Reference: Analytical limit: the steady-state initial condition equalises the energy flow through every basic node and closes the surface radiation balance sigma (T_surf^4 - teqm^4) with the Stefan-Boltzmann constant of constants.c. diff --git a/docs/Validation/eos.md b/docs/Validation/eos.md index 2972fd58..4a2953df 100644 --- a/docs/Validation/eos.md +++ b/docs/Validation/eos.md @@ -23,7 +23,7 @@ The sections below detail two validation cases, their tolerances, and their test ![Liquidus entropy against pressure from the table and from eos.c at 3 probes](../assets/validation/eos_phase_boundary.svg#only-light){ width="80%" } ![Liquidus entropy against pressure from the table and from eos.c at 3 probes](../assets/validation/eos_phase_boundary_dark.svg#only-dark){ width="80%" } -**How to read the plot.** The x-axis is the pressure and the y-axis the entropy of the melt phase boundary. The line is the liquidus table `liquidus_A11_H13.dat` read with numpy (every 10th node, context). The circles are the asserted values of eos.c at the 3 probes: the table edge at 0, a mid-mantle node at 10 GPa and an off-node point at 65.5 GPa. The cross is the solidus at 10 GPa, which the test requires to be more than 100 away. A pass means that the circles lie on the line (relative 1e-12). +**How to read the plot.** The x-axis is the pressure and the y-axis the entropy of the liquidus, the melt phase boundary. The line is the liquidus table `liquidus_A11_H13.dat` read with numpy (every 10th node, context). The circles are the asserted values of eos.c at 3 probes. These are the table edge at 0 GPa, a table node at 10 GPa, and an off-node point at 65.5 GPa. The cross is the solidus at 10 GPa, 1524.43 J kg$^{-1}$ K$^{-1}$, which must differ from the code value by more than 100. A pass means that the circles lie on the line (relative 1e-12). ??? info "Test details" - Reference: Cross-implementation cross-check of the loaded melt phase boundary against an independent read of liquidus_A11_H13.dat (Andrault et al. 2011 liquidus with the Hirschmann 2013 solidus companion). @@ -47,7 +47,7 @@ The sections below detail two validation cases, their tolerances, and their test ![Spaargaren viscosity prefactor against Mg/Si with the eos.c shifts](../assets/validation/eos_spaargaren_viscosity.svg#only-light){ width="80%" } ![Spaargaren viscosity prefactor against Mg/Si with the eos.c shifts](../assets/validation/eos_spaargaren_viscosity_dark.svg#only-dark){ width="80%" } -**How to read the plot.** The x-axis is Mg/Si and the y-axis the log10 viscosity prefactor. The line is the piecewise prefactor of Spaargaren et al. (2020) as the test re-encodes it (context), and the hollow squares are its values at the 2 reference compositions, 1.6 and 0.9. The circles are each reference value plus the asserted shift of eos.c, the melt log10 viscosity minus the configured 2.0, at 1.4 and at 1.2. A pass means that the circles lie on the line (absolute 1e-9), 1 above and 1 below its reference. +**How to read the plot.** The x-axis is the Mg/Si ratio and the y-axis the log10 viscosity prefactor. The line is the piecewise prefactor of Spaargaren et al. (2020), as the test re-encodes it (context). The hollow squares are its values at the 2 reference compositions, 1.6 and 0.9. The circles are each reference value plus the asserted shift of eos.c, at 1.4 and at 1.2. The shift is the melt log10 viscosity minus the configured 2.0. A pass means that the circles lie on the line (absolute 1e-9), the one at 1.4 above its square and the one at 1.2 below. ??? info "Test details" - Reference: The piecewise Mg/Si log10 viscosity prefactor of Spaargaren et al. (2020). The test re-encodes the published coefficients independently of eos.c and additionally pins the two branch-pair shifts as hand-derived literals (0.2074 and -1.35803), so a transcription error in either copy of the formula surfaces. diff --git a/docs/Validation/eos_adamswilliamson.md b/docs/Validation/eos_adamswilliamson.md index d36b889e..04bd58a5 100644 --- a/docs/Validation/eos_adamswilliamson.md +++ b/docs/Validation/eos_adamswilliamson.md @@ -23,7 +23,7 @@ The text below details the check, its tolerance, and its test command. ![Adams-Williamson density against pressure from the closed form and from the code](../assets/validation/eos_adamswilliamson_density.svg#only-light){ width="80%" } ![Adams-Williamson density against pressure from the closed form and from the code](../assets/validation/eos_adamswilliamson_density_dark.svg#only-dark){ width="80%" } -**How to read the plot.** The x-axis is the pressure and the y-axis the density. The line is the closed form $\rho_s - P\beta/g$ and the dashed line the form with a sign-flipped $\beta$, which the test rejects (context). The circles are the asserted densities of eos_adamswilliamson.c at the surface, at 10 GPa and at 135 GPa. A pass means that the circles lie on the line (relative 1e-12). +**How to read the plot.** The x-axis is the pressure and the y-axis the density. The line is the closed form $\rho_s - P\beta/g$, with surface density $\rho_s$, compressibility parameter $\beta$ and gravity $g$. The dashed line is the form with a sign-flipped $\beta$, which the test rejects (context). The circles are the asserted densities of eos_adamswilliamson.c at the surface, at 10 GPa and at 135 GPa. A pass means that the circles lie on the line (relative 1e-12). ??? info "Test details" - Reference: Analytical limit: rho(P) = rhos - P beta / g in closed form, with the blackbody50 PREM lower-mantle fit parameters. diff --git a/docs/Validation/eos_composite.md b/docs/Validation/eos_composite.md index ce7d11bf..6e34c2b2 100644 --- a/docs/Validation/eos_composite.md +++ b/docs/Validation/eos_composite.md @@ -23,7 +23,7 @@ The section below details the validation check, its tolerance, and its test comm ![Melt fraction against entropy at 10 GPa between the solidus and the liquidus](../assets/validation/eos_composite_melt_fraction.svg#only-light){ width="80%" } ![Melt fraction against entropy at 10 GPa between the solidus and the liquidus](../assets/validation/eos_composite_melt_fraction_dark.svg#only-dark){ width="80%" } -**How to read the plot.** The x-axis is the entropy at 10 GPa and the y-axis the melt fraction. The dotted lines are the solidus and liquidus that eos_composite.c reports, and the solid line is $(S - S_\mathrm{sol}) / (S_\mathrm{liq} - S_\mathrm{sol})$ between them (context). The dashed line is the form with the liquidus entropy as the denominator, which the test rejects. The circle is the asserted melt fraction at 1675.34 J kg$^{-1}$ K$^{-1}$, a quarter of the way through the interval. A pass means that the circle lies on the solid line at 0.25. +**How to read the plot.** The x-axis is the entropy $S$ at 10 GPa and the y-axis the melt fraction. The dotted lines are the solidus and liquidus entropies $S_\mathrm{sol}$ and $S_\mathrm{liq}$ that eos_composite.c reports. The solid line is $(S - S_\mathrm{sol}) / (S_\mathrm{liq} - S_\mathrm{sol})$ between them (context). The dashed line is the form with the liquidus entropy as the denominator, which the test rejects. The circle is the asserted melt fraction at 1675.34 J kg$^{-1}$ K$^{-1}$, a quarter of the way through the interval. A pass means that the circle lies on the solid line at 0.25. ??? info "Test details" - Reference: Analytical limit: by construction of the two-phase region, fusion = S_liq - S_sol and phi = (S - S_sol)/fusion between the phase boundaries. diff --git a/docs/Validation/eos_lookup.md b/docs/Validation/eos_lookup.md index 1b670263..2bed734f 100644 --- a/docs/Validation/eos_lookup.md +++ b/docs/Validation/eos_lookup.md @@ -23,7 +23,7 @@ The section below details the check, its tolerance, and its test command. ![Melt density from eos_lookup.c against an independent bilinear read of the table](../assets/validation/eos_lookup_melt_density.svg#only-light){ width="80%" } ![Melt density from eos_lookup.c against an independent bilinear read of the table](../assets/validation/eos_lookup_melt_density_dark.svg#only-dark){ width="80%" } -**How to read the plot.** The x-axis is the melt density from an independent bilinear read of `density_melt.dat` in numpy and the y-axis the density from eos_lookup.c, at the 3 off-node probes from 7.3 to 89 GPa. The circles are the asserted C values and the line is 1:1. The crosses are the solid-table densities at the same probes, which the test requires to differ by more than 1 %. A pass means that the circles lie on the line (relative 1e-9). +**How to read the plot.** The x-axis is the melt density from an independent bilinear read of `density_melt.dat` in numpy. The y-axis is the density from eos_lookup.c, at the 3 off-node probes from 7.3 to 89 GPa. The circles are the asserted C values, and the line is 1:1. The crosses are the solid-table densities at the same probes, which must differ by more than 1 %. A pass means that the circles lie on the line (relative 1e-9). ??? info "Test details" - Reference: Cross-implementation cross-check: an independent numpy bilinear evaluation of the shipped melt density table (1TPa-dK09-elec-free set: de Koker and Stixrude 2009 properties as assembled for Bower et al. 2018). diff --git a/docs/Validation/ic.md b/docs/Validation/ic.md index ba9b792d..e8394338 100644 --- a/docs/Validation/ic.md +++ b/docs/Validation/ic.md @@ -23,7 +23,7 @@ The sections below detail three validation cases, their tolerances, and their te ![Initial entropy profiles for ic_adiabat_entropy 2600 and 2400](../assets/validation/ic_adiabat_profile.svg#only-light){ width="80%" } ![Initial entropy profiles for ic_adiabat_entropy 2600 and 2400](../assets/validation/ic_adiabat_profile_dark.svg#only-dark){ width="80%" } -**How to read the plot.** The x-axis is the radius and the y-axis the entropy at the staggered nodes at $t$ = 0. The solid line is the asserted profile for ic_adiabat_entropy = 2600 and the dashed line the rerun with 2400, which the test requires to be 200 J kg$^{-1}$ K$^{-1}$ lower on average. The circle is the asserted top staggered node at 2600, the dotted line the mean of the 2600 profile and the grey band its asserted range, 2600 to 2628.7. A pass means that the circle sits at 2600, the dotted line inside the band, and the dashed line 200 below the solid one. +**How to read the plot.** The x-axis is the radius and the y-axis the entropy at the staggered nodes (the cell centres of the mesh) at $t$ = 0. The solid line is the asserted profile for `ic_adiabat_entropy` = 2600. The dashed line is the rerun with 2400, which must be 200 J kg$^{-1}$ K$^{-1}$ lower on average. The circle is the asserted top staggered node at 2600. The dotted line is the mean of the 2600 profile, and the grey band its asserted range, 2600 to 2628.7. A pass means that the circle sits at 2600, the dotted line inside the band, and the dashed line 200 below the solid one. ??? info "Test details" - Reference: Analytical limit: the initial condition prescribes the adiabat entropy (2600 J/kg/K at the top staggered node) with the configured small gradient ramp. @@ -47,7 +47,7 @@ The sections below detail three validation cases, their tolerances, and their te ![Requested and realised volatile masses and the realised over requested element moles](../assets/validation/ic_abundance_inventory.svg#only-light){ width="80%" } ![Requested and realised volatile masses and the realised over requested element moles](../assets/validation/ic_abundance_inventory_dark.svg#only-dark){ width="80%" } -**How to read the plot.** Left: the grey bars are the requested masses of the 4 volatiles, the requested ppm of the mantle mass, which the test asserts to equal the stored initial_kg (relative 1e-9); the circles are the realised reservoirs after the initial partial-pressure solve, on a log scale. The reactions move mass between the partners, so H$_2$ and CO$_2$ end below their bars and H$_2$O and CO above. Right: the realised over the requested moles of H, C and O, with the dashed line at 1. The test asserts that H and C sit on 1 (relative 1e-9) and that O departs by more than 1e-4 (here 2.2e-3), because the reactions exchange oxygen with the fO$_2$ buffer of the melt. A pass means that the H and C circles sit on the line and the O circle above it. +**How to read the plot.** Left: the grey bars are the requested masses of the 4 volatiles, which the test asserts to equal the stored `initial_kg` (relative 1e-9). The circles are the realised reservoirs after the initial partial-pressure solve, on a log scale. The reactions move mass, so H$_2$ and CO$_2$ end below their bars and H$_2$O and CO above. Right: the realised over the requested moles of H, C and O, with the dashed line at 1. A pass means that H and C sit on the line (relative 1e-9) and O departs from it by more than 1e-4. Here O lies 2.2e-3 above, because the reactions exchange oxygen with the oxygen-fugacity (fO$_2$) buffer of the melt. ??? info "Test details" - Reference: Mass-balance identity through the initial partial-pressure solve: initial_kg equals the requested ppm of the mantle mass per volatile, and the realised reservoirs carry the requested hydrogen and carbon mole totals (conserved by the water and carbon dioxide reactions). @@ -71,7 +71,7 @@ The sections below detail three validation cases, their tolerances, and their te ![Stored initial inventories against moles times OCEAN_MOLES times molar mass](../assets/validation/ic_ocean_moles.svg#only-light){ width="80%" } ![Stored initial inventories against moles times OCEAN_MOLES times molar mass](../assets/validation/ic_ocean_moles_dark.svg#only-dark){ width="80%" } -**How to read the plot.** The bars are the closed-form masses, moles times OCEAN_MOLES times molar mass, of the 4 volatiles on a log scale, and the circles the stored initial_kg of the run, which the test asserts to match to relative 1e-9. The cross is the CO$_2$ inventory with the CO molar mass, which the test requires to be more than 30 % away. A pass means that each circle sits on the top of its bar and the cross lies well below the CO$_2$ bar. +**How to read the plot.** The bars are the closed-form masses of the 4 volatiles on a log scale: moles times `OCEAN_MOLES` (moles per Earth ocean) times molar mass. The circles are the stored `initial_kg` of the run, which the test asserts to match to relative 1e-9. The cross is the CO$_2$ inventory with the CO molar mass, about 36 % lower, which the test rejects. A pass means that each circle sits on the top of its bar and the cross lies below the CO$_2$ bar. ??? info "Test details" - Reference: The OCEAN_MOLES constant of constants.c (7.68894973907177e22 mol per Earth ocean): initial_kg equals moles times OCEAN_MOLES times the volatile molar mass. diff --git a/docs/Validation/interp.md b/docs/Validation/interp.md index 5e7364d3..578f2812 100644 --- a/docs/Validation/interp.md +++ b/docs/Validation/interp.md @@ -23,7 +23,7 @@ The section below details the validation check, its tolerance, and its test comm ![Piecewise-linear interpolation of y = 2x + 1 at 4 queries](../assets/validation/interp_affine.svg#only-light){ width="80%" } ![Piecewise-linear interpolation of y = 2x + 1 at 4 queries](../assets/validation/interp_affine_dark.svg#only-dark){ width="80%" } -**How to read the plot.** The hollow squares are the 11 table nodes of $y = 2x + 1$ and the line is the affine function. The circles are the asserted values of interp.c at the 4 queries $x$ = 0, 2.5, 7 and 10 (the last node). The crosses at $x$ = 2.5 are the 2 nearest node values, 5 and 7, which the test requires to be more than 0.5 away. A pass means that the circles lie on the line (relative 1e-14). +**How to read the plot.** The hollow squares are the 11 table nodes of $y = 2x + 1$, and the line is this affine function. The circles are the asserted values of interp.c at the 4 queries $x$ = 0, 2.5, 7 and 10 (the last node). The crosses at $x$ = 2.5 are the 2 nearest node values, 5 and 7. The test requires the code value 6 to be more than 0.5 from each. A pass means that the circles lie on the line (relative 1e-14). ??? info "Test details" - Reference: Analytical limit: piecewise-linear interpolation reproduces affine data exactly. diff --git a/docs/Validation/matprop.md b/docs/Validation/matprop.md index c34ec5ab..03ee55fe 100644 --- a/docs/Validation/matprop.md +++ b/docs/Validation/matprop.md @@ -23,7 +23,7 @@ The text below details the check, its tolerance, and its test command. ![log10 viscosity against radius for the hot molten and the cold initial condition](../assets/validation/matprop_viscosity_end_members.svg#only-light){ width="80%" } ![log10 viscosity against radius for the hot molten and the cold initial condition](../assets/validation/matprop_viscosity_end_members_dark.svg#only-dark){ width="80%" } -**How to read the plot.** The x-axis is the radius and the y-axis the log10 viscosity at the basic nodes. The solid line is the hot initial condition, all 50 nodes molten, asserted at the melt end member 2 (dashed, within 0.01). The dash-dotted line is the cold initial condition at 1600 J kg$^{-1}$ K$^{-1}$, and the circles its asserted nodes with a melt fraction below $10^{-3}$, at the solid end member 21 (dotted, within 0.05). A pass means that the solid line lies on the dashed line and the circles on the dotted line. +**How to read the plot.** The x-axis is the radius and the y-axis the log10 viscosity in Pa s at the basic nodes (the cell boundaries of the mesh). The solid line is the hot initial condition, with all 50 nodes molten. It is asserted at the melt end member, the configured melt viscosity of $10^2$ Pa s (dashed, to 0.01 in log10). The dash-dotted line is the cold initial condition at 1600 J kg$^{-1}$ K$^{-1}$. Its circles are the asserted nodes with a melt fraction below $10^{-3}$, at the solid end member $10^{21}$ Pa s (dotted, to 0.05). A pass means that the solid line lies on the dashed line and the circles on the dotted line. ??? info "Test details" - Reference: Analytical limit: away from the rheological transition the blended viscosity recovers the configured end-members (melt_log10visc = 2, solid_log10visc = 21). diff --git a/docs/Validation/mesh.md b/docs/Validation/mesh.md index 86b88571..f71ac456 100644 --- a/docs/Validation/mesh.md +++ b/docs/Validation/mesh.md @@ -23,7 +23,7 @@ The section below details the validation check, its tolerance, and its test comm ![Basic-node pressure against radius on the Adams-Williamson closed form](../assets/validation/mesh_pressure_profile.svg#only-light){ width="80%" } ![Basic-node pressure against radius on the Adams-Williamson closed form](../assets/validation/mesh_pressure_profile_dark.svg#only-dark){ width="80%" } -**How to read the plot.** The x-axis is the radius and the y-axis the pressure. The line is the Adams-Williamson closed form (context), and the dashed and dotted lines are the constant-density profile and the form with a sign-flipped $\beta$, which the test rejects at the core-mantle boundary. The circles are the asserted pressures of mesh.c at the basic nodes. A pass means that the circles lie on the line (relative 1e-12). +**How to read the plot.** The x-axis is the radius and the y-axis the pressure. The line is the Adams-Williamson closed form (context). The dashed and dotted lines are the constant-density profile and the form with a sign-flipped compressibility parameter $\beta$. The test rejects both at the core-mantle boundary. The circles are the asserted pressures of mesh.c at the basic nodes (the cell boundaries of the mesh). A pass means that the circles lie on the line (relative 1e-12). ??? info "Test details" - Reference: Analytical limit: the Adams-Williamson pressure profile P(r) = -(rhos g / beta)(exp(beta (R - r)) - 1) in closed form. diff --git a/docs/Validation/reaction.md b/docs/Validation/reaction.md index 32f9c3c2..482b6e7d 100644 --- a/docs/Validation/reaction.md +++ b/docs/Validation/reaction.md @@ -23,7 +23,7 @@ The text below details the check, its tolerance, and its test command. ![Hydrogen and carbon moved by the H2O/H2 and CO2/CO reactions in 1000 years](../assets/validation/reaction_element_balance.svg#only-light){ width="80%" } ![Hydrogen and carbon moved by the H2O/H2 and CO2/CO reactions in 1000 years](../assets/validation/reaction_element_balance_dark.svg#only-dark){ width="80%" } -**How to read the plot.** The bars are the asserted element masses moved in the first 1000 yr, on a log scale; values shown to 2 significant digits, because the run output moves by about 5e-6 between PETSc builds. For hydrogen: the hydrogen lost by H$_2$ against the hydrogen in the H$_2$O gained; for carbon: the carbon lost by CO against the carbon in the CO$_2$ gained. A pass means that the 2 bars of each pair have the same height (relative 1e-3). +**How to read the plot.** The bars are the asserted element masses moved in the first 1000 yr, on a log scale. Values are shown to 2 significant digits, because the run output moves by about 5e-6 between builds of the PETSc library. For hydrogen, the bars are the hydrogen lost by H$_2$ and the hydrogen in the H$_2$O gained. For carbon, they are the carbon lost by CO and the carbon in the CO$_2$ gained. A pass means that the 2 bars of each pair have the same height; the test asserts relative 1e-3 on the unrounded values. ??? info "Test details" - Reference: Analytical conservation: closed-system elemental budgets across the H2O/H2 and CO2/CO reaction pairs of the Bower et al. (2021) three-ocean hydrogen configuration. diff --git a/docs/Validation/rheologicalfront.md b/docs/Validation/rheologicalfront.md index 0c6fb449..0da64aa3 100644 --- a/docs/Validation/rheologicalfront.md +++ b/docs/Validation/rheologicalfront.md @@ -23,7 +23,7 @@ The section below details the validation check, its tolerance, and its test comm ![Radii of the 2 nodes on each side of the critical melt fraction and of the reported front at 1200 yr](../assets/validation/rheologicalfront_location.svg#only-light){ width="80%" } ![Radii of the 2 nodes on each side of the critical melt fraction and of the reported front at 1200 yr](../assets/validation/rheologicalfront_location_dark.svg#only-dark){ width="80%" } -**How to read the plot.** The y-axis is the radius. The squares are the 2 staggered nodes that the test finds itself at 1200 yr: the last node with a melt fraction at or above $\phi_c$ = 0.4 (0.403) and the first node below it (0.3995). The circle is the asserted front radius, the planet radius minus the reported depth; radii shown to 3 significant digits. The melt fractions are not drawn: the first node lies 4.5e-4 below $\phi_c$, and the run output moves by about 3e-6 between PETSc builds, so no rounding of them is stable. A pass means that the circle lies between the 2 squares (the test allows 1 node either way). +**How to read the plot.** The y-axis is the radius, shown to 3 significant digits. The squares are the 2 staggered nodes (cell centres of the mesh) that the test finds itself at 1200 yr. They are the last node at or above the critical melt fraction $\phi_c = 0.4$ (0.403), and the first node below it (0.3995). The circle is the asserted front radius, the planet radius minus the reported depth. The melt fractions are not drawn: the first node is only 4.5e-4 below $\phi_c$, so a rounded value is not stable between builds. A pass means that the circle lies at most 1 node from the squares; in this run it lies between them. ??? info "Test details" - Reference: Analytical self-consistency: the reported front index matches the phi_critical = 0.4 crossing of the melt-fraction profile, and the depth equals the surface radius minus the front radius. diff --git a/docs/Validation/rhs.md b/docs/Validation/rhs.md index 86be9393..98242249 100644 --- a/docs/Validation/rhs.md +++ b/docs/Validation/rhs.md @@ -21,7 +21,7 @@ The section below details the validation check, its tolerance, and its test comm ![Reported dS/dt and the slope of S between the 10 and 20 yr outputs against radius, and their relative difference with the tolerance](../assets/validation/rhs_dsdt_finite_difference.svg#only-light){ width="80%" } ![Reported dS/dt and the slope of S between the 10 and 20 yr outputs against radius, and their relative difference with the tolerance](../assets/validation/rhs_dsdt_finite_difference_dark.svg#only-dark){ width="80%" } -**How to read the plot.** Top: the cooling rate -dS/dt that rhs.c reports, as the mean of the 10 and 20 yr outputs (line), and the slope of the entropy between these outputs (dashed), at every staggered node of the blackbody50 run. Bottom: their relative difference per node (dots) and the tolerance of the assertion (dashed); the largest difference is 0.65 percent. +**How to read the plot.** Top: the cooling rate -dS/dt that rhs.c reports, as the mean of the 10 and 20 yr outputs (line). The dashed line is the slope of the entropy between these outputs, at every staggered node of the blackbody50 run. Bottom: their relative difference per node (dots) and the tolerance of the assertion (dashed); the largest difference is 0.65 percent. **Known limit.** The trajectory comes from the same right-hand side that reports dS/dt, so a wrong right-hand side gives a consistent wrong trajectory and passes; dS/dt is not compared with an independent physical value. The run has no heating, so a dropped heating term cannot show. Finite differencing over 10-year intervals adds a truncation error of up to 0.65 percent. diff --git a/docs/Validation/twophase.md b/docs/Validation/twophase.md index 12171141..bf8ebbe6 100644 --- a/docs/Validation/twophase.md +++ b/docs/Validation/twophase.md @@ -23,7 +23,7 @@ The text below details the check, its tolerance, and its test command. ![Reported global melt fraction at 1200 yr against its 3 definitions](../assets/validation/twophase_global_melt_fraction.svg#only-light){ width="80%" } ![Reported global melt fraction at 1200 yr against its 3 definitions](../assets/validation/twophase_global_melt_fraction_dark.svg#only-dark){ width="80%" } -**How to read the plot.** The circles are the global melt fraction at 1200 yr in 4 forms: the reported phi_global, Mliq / Mmantle and the mass-weighted mean of the staggered-node melt fraction, which the test requires to agree (relative 1e-10), and the unweighted mean, which it requires to be more than 1e-3 away. The grey band is the reported value $\pm$ 1e-3. Values shown to 2 significant digits, because the run output moves by about 2.5e-6 between PETSc builds. A pass means that the first 3 circles lie in the band and the last one outside it. +**How to read the plot.** The circles are the global melt fraction at 1200 yr in 4 forms. The first 3 are the reported `phi_global`, the liquid over mantle mass Mliq / Mmantle, and the mass-weighted mean of the staggered-node melt fraction. The test requires these 3 to agree (relative 1e-10), and the unweighted mean to be more than 1e-3 away. The grey band is the reported value $\pm$ 1e-3. Values are shown to 2 significant digits, because the run output moves by about 2.5e-6 between builds of the PETSc library. A pass means that the first 3 circles lie in the band and the last outside it; the test checks the unrounded values. ??? info "Test details" - Reference: Analytical identity: the global melt fraction equals Mliq/Mmantle and the mass-weighted mean of the staggered melt-fraction profile. diff --git a/docs/assets/validation/rhs_dsdt_finite_difference.svg b/docs/assets/validation/rhs_dsdt_finite_difference.svg index 7cad4ec9..821a8c2c 100644 --- a/docs/assets/validation/rhs_dsdt_finite_difference.svg +++ b/docs/assets/validation/rhs_dsdt_finite_difference.svg @@ -6,7 +6,7 @@ - test: tests/test_rhs.py::test_reported_dsdt_matches_finite_difference_of_outputs; command: pytest tests/test_rhs.py::test_reported_dsdt_matches_finite_difference_of_outputs --validation-plots + test: tests/test_rhs.py::test_reported_dsdt_matches_finite_difference_of_outputs; command: pytest tests/test_rhs.py::test_reported_dsdt_matches_finite_difference_of_outputs --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[49, 3528.420367522105, 6337.615609804658, 4865.232006967509, -488.80990442400326, 2.26778410522055e-08, 2.3037609191603335e-08, 2.3016097688843457e-08, -3.956771722936898e-12], [49, 3528.420367522105, 6337.615609804658, 4865.232006967509, -488.80990442400326, 2.2580465746253024e-08, 2.2892264545409143e-08, 2.2872173017027516e-08, -3.2532819653545098e-12]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}, {"scales": ["linear", "log"], "categories": [null, null], "lines": [[49, 3528.420367522105, 6337.615609804658, 4865.232006967509, -488.80990442400326, -2.3671528717895494, -2.1842403282860112, -2.204508940493961, -0.002654770803033463], [2, 0.0, 1.0, 0.5, 0.5, -1.6989700043360187, -1.6989700043360187, -1.6989700043360187, 0.0]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} image/svg+xml diff --git a/docs/assets/validation/rhs_dsdt_finite_difference_dark.svg b/docs/assets/validation/rhs_dsdt_finite_difference_dark.svg index af2fc8dc..156a5b92 100644 --- a/docs/assets/validation/rhs_dsdt_finite_difference_dark.svg +++ b/docs/assets/validation/rhs_dsdt_finite_difference_dark.svg @@ -6,7 +6,7 @@ - test: tests/test_rhs.py::test_reported_dsdt_matches_finite_difference_of_outputs; command: pytest tests/test_rhs.py::test_reported_dsdt_matches_finite_difference_of_outputs --validation-plots + test: tests/test_rhs.py::test_reported_dsdt_matches_finite_difference_of_outputs; command: pytest tests/test_rhs.py::test_reported_dsdt_matches_finite_difference_of_outputs --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[49, 3528.420367522105, 6337.615609804658, 4865.232006967509, -488.80990442400326, 2.26778410522055e-08, 2.3037609191603335e-08, 2.3016097688843457e-08, -3.956771722936898e-12], [49, 3528.420367522105, 6337.615609804658, 4865.232006967509, -488.80990442400326, 2.2580465746253024e-08, 2.2892264545409143e-08, 2.2872173017027516e-08, -3.2532819653545098e-12]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}, {"scales": ["linear", "log"], "categories": [null, null], "lines": [[49, 3528.420367522105, 6337.615609804658, 4865.232006967509, -488.80990442400326, -2.3671528717895494, -2.1842403282860112, -2.204508940493961, -0.002654770803033463], [2, 0.0, 1.0, 0.5, 0.5, -1.6989700043360187, -1.6989700043360187, -1.6989700043360187, 0.0]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} image/svg+xml From 9825be8ad033bfaf08db5d5db31ab1ece68f5fab Mon Sep 17 00:00:00 2001 From: timlichtenberg Date: Mon, 26 Oct 2026 21:07:27 +0100 Subject: [PATCH 12/15] Clarify the energy, front and rhs read texts The disabled-transport note separates the 3 maxima from the top-node gap, the front note states the 1-node allowance of the assert, and the rhs note gives its pass condition. --- docs/Validation/energy.md | 6 +++--- docs/Validation/rheologicalfront.md | 2 +- docs/Validation/rhs.md | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/Validation/energy.md b/docs/Validation/energy.md index f747d7fe..656e4d1e 100644 --- a/docs/Validation/energy.md +++ b/docs/Validation/energy.md @@ -23,7 +23,7 @@ The sections below detail three validation cases, their tolerances, and their te ![Largest flux of each transport term in the conduction-off and the conduction-only runs](../assets/validation/energy_disabled_transport.svg#only-light){ width="80%" } ![Largest flux of each transport term in the conduction-off and the conduction-only runs](../assets/validation/energy_disabled_transport_dark.svg#only-dark){ width="80%" } -**How to read the plot.** Each circle is the largest absolute flux over the basic nodes of one run, on a symmetric log axis (linear between $-10^{-3}$ and $10^{-3}$). Values are shown to 2 significant digits, because the run output moves by up to a few 1e-6 between builds of the PETSc library. The first 2 circles are asserted to be 0, to 1e-20 W/m$^2$. They are the conductive flux with conduction off, and the convective, mixing and separation fluxes with conduction only. The last 2 circles are asserted to be above $10^3$ W/m$^2$ (dashed line). They are the convective flux with conduction off, and the gap between total and conductive flux at the top node with conduction only. A pass means that the first 2 circles sit at 0 and the last 2 above the dashed line. +**How to read the plot.** The circles are fluxes on a symmetric log axis (linear between $-10^{-3}$ and $10^{-3}$). The first 3 are the largest absolute flux over the basic nodes of one run. Values are shown to 2 significant digits, because the run output moves by up to a few 1e-6 between builds of the PETSc library. The first 2 circles are asserted to be 0, to 1e-20 W/m$^2$. They are the conductive flux with conduction off, and the convective, mixing and separation fluxes with conduction only. The last 2 circles are asserted to be above $10^3$ W/m$^2$ (dashed line). They are the convective flux with conduction off, and the gap between total and conductive flux at the top node with conduction only. A pass means that the first 2 circles sit at 0 and the last 2 above the dashed line. ??? info "Test details" - Reference: Analytical limit: a disabled transport mechanism carries zero flux, and with only conduction enabled the total flux equals the conductive flux at every node except the surface node, which carries the grey-body boundary flux. @@ -47,7 +47,7 @@ The sections below detail three validation cases, their tolerances, and their te ![Radiogenic heating of the 2-isotope run against time](../assets/validation/energy_radiogenic_decay.svg#only-light){ width="80%" } ![Radiogenic heating of the 2-isotope run against time](../assets/validation/energy_radiogenic_decay_dark.svg#only-dark){ width="80%" } -**How to read the plot.** The x-axis is the time and the y-axis the specific radiogenic heating on a log scale. The solid line is the sum of both isotopes in closed form. The dashed and dotted lines are the isotopes `al26` and `k40` alone, with test half-lives of 100 and 200 yr (context). The circles are the asserted heating Hradio of the run at 0 and 200 yr. A pass means that both lie on the solid line. The test asserts the value at $t$ = 0 to relative 1e-6. It asserts the ratio of the 200 yr to the 0 yr value as 0.2506, to relative 1e-4. +**How to read the plot.** The x-axis is the time and the y-axis the specific radiogenic heating on a log scale. The solid line is the sum of both isotopes in closed form. The dashed and dotted lines are the isotopes `al26` and `k40` alone, with test half-lives of 100 and 200 yr (context). The circles are the asserted heating Hradio of the run at 0 and 200 yr. A pass means that both lie on the solid line. The test asserts the value at $t$ = 0 to relative 1e-6. It asserts the ratio of the 200 yr to the 0 yr value, about 0.2506, to relative 1e-4. ??? info "Test details" - Reference: Analytical limit: radiogenic heating is the sum over isotopes of concentration times abundance times heat production, decaying as 2^(-(t - t0)/half_life). A two-isotope configuration with 100-year and 200-year half-lives pins the sum at t = 0 and the weighted decay factor 0.2506 after 200 years. @@ -71,7 +71,7 @@ The sections below detail three validation cases, their tolerances, and their te ![Energy flow against radius and surface flux on the grey-body law for the steady-state initial condition](../assets/validation/energy_steady_state_ic.svg#only-light){ width="80%" } ![Energy flow against radius and surface flux on the grey-body law for the steady-state initial condition](../assets/validation/energy_steady_state_ic_dark.svg#only-dark){ width="80%" } -**How to read the plot.** Left: the asserted energy flow Etot at every basic node of the steady-state initial condition, against radius. A pass means a flat line. The test asserts a relative spread below 1e-8. Right: the grey-body law $\sigma(T^4 - 273^4)$, with $\sigma$ the Stefan-Boltzmann constant (line), and the $T^3$ form that the test rejects (dashed). The circle is the asserted surface flux Fatm at the reported surface temperature. A pass means that the circle lies on the line (relative 1e-5). +**How to read the plot.** Left: the asserted energy flow Etot at every basic node of the steady-state initial condition, against radius. A pass means a flat line. The test asserts a relative spread below 1e-8. Right: the solid line is the grey-body law $\sigma(T^4 - 273^4)$, with $\sigma$ the Stefan-Boltzmann constant. Here 273 K is the equilibrium temperature. The dashed line is the rejected $T^3$ form. The circle is the asserted surface flux Fatm at the reported surface temperature. A pass means that the circle lies on the line (relative 1e-5). ??? info "Test details" - Reference: Analytical limit: the steady-state initial condition equalises the energy flow through every basic node and closes the surface radiation balance sigma (T_surf^4 - teqm^4) with the Stefan-Boltzmann constant of constants.c. diff --git a/docs/Validation/rheologicalfront.md b/docs/Validation/rheologicalfront.md index 0da64aa3..326f4389 100644 --- a/docs/Validation/rheologicalfront.md +++ b/docs/Validation/rheologicalfront.md @@ -23,7 +23,7 @@ The section below details the validation check, its tolerance, and its test comm ![Radii of the 2 nodes on each side of the critical melt fraction and of the reported front at 1200 yr](../assets/validation/rheologicalfront_location.svg#only-light){ width="80%" } ![Radii of the 2 nodes on each side of the critical melt fraction and of the reported front at 1200 yr](../assets/validation/rheologicalfront_location_dark.svg#only-dark){ width="80%" } -**How to read the plot.** The y-axis is the radius, shown to 3 significant digits. The squares are the 2 staggered nodes (cell centres of the mesh) that the test finds itself at 1200 yr. They are the last node at or above the critical melt fraction $\phi_c = 0.4$ (0.403), and the first node below it (0.3995). The circle is the asserted front radius, the planet radius minus the reported depth. The melt fractions are not drawn: the first node is only 4.5e-4 below $\phi_c$, so a rounded value is not stable between builds. A pass means that the circle lies at most 1 node from the squares; in this run it lies between them. +**How to read the plot.** The y-axis is the radius, shown to 3 significant digits. The squares are the 2 staggered nodes (cell centres of the mesh) that the test finds itself at 1200 yr. They are the last node at or above the critical melt fraction $\phi_c = 0.4$ (0.403), and the first node below it (0.3995). The circle is the asserted front radius, the planet radius minus the reported depth. The melt fractions are not drawn: the first node is only 4.5e-4 below $\phi_c$, so a rounded value is not stable between builds. A pass means that the front node is at most 1 node from the first node below $\phi_c$. In this run the circle lies between the squares. ??? info "Test details" - Reference: Analytical self-consistency: the reported front index matches the phi_critical = 0.4 crossing of the melt-fraction profile, and the depth equals the surface radius minus the front radius. diff --git a/docs/Validation/rhs.md b/docs/Validation/rhs.md index 98242249..3083cf12 100644 --- a/docs/Validation/rhs.md +++ b/docs/Validation/rhs.md @@ -21,7 +21,7 @@ The section below details the validation check, its tolerance, and its test comm ![Reported dS/dt and the slope of S between the 10 and 20 yr outputs against radius, and their relative difference with the tolerance](../assets/validation/rhs_dsdt_finite_difference.svg#only-light){ width="80%" } ![Reported dS/dt and the slope of S between the 10 and 20 yr outputs against radius, and their relative difference with the tolerance](../assets/validation/rhs_dsdt_finite_difference_dark.svg#only-dark){ width="80%" } -**How to read the plot.** Top: the cooling rate -dS/dt that rhs.c reports, as the mean of the 10 and 20 yr outputs (line). The dashed line is the slope of the entropy between these outputs, at every staggered node of the blackbody50 run. Bottom: their relative difference per node (dots) and the tolerance of the assertion (dashed); the largest difference is 0.65 percent. +**How to read the plot.** Top: the cooling rate -dS/dt that rhs.c reports, as the mean of the 10 and 20 yr outputs (line). The dashed line is the slope of the entropy between these outputs, at every staggered node of the blackbody50 run. Bottom: their relative difference per node (dots) and the tolerance of the assertion (dashed); the largest difference is 0.65 percent. A pass means that, at every node, the dot lies below the dashed line. **Known limit.** The trajectory comes from the same right-hand side that reports dS/dt, so a wrong right-hand side gives a consistent wrong trajectory and passes; dS/dt is not compared with an independent physical value. The run has no heating, so a dropped heating term cannot show. Finite differencing over 10-year intervals adds a truncation error of up to 0.65 percent. From a69523516ff6baf26d799b2eba70d4c5784a038e Mon Sep 17 00:00:00 2001 From: timlichtenberg Date: Mon, 26 Oct 2026 23:25:00 +0100 Subject: [PATCH 13/15] Draw the asserted dS/dt scale in the rhs validation plot The dS/dt profiles of the run differ by about 7e-4 between the macOS and Linux builds, which the stale-plot check rejects. The plot now draws the largest |dS/dt| at 1 significant digit against the asserted window from 1e-12 to 1e-4; the page names the finite-difference comparison that the plot does not show. --- docs/Validation/rhs.md | 6 +- .../validation/rhs_dsdt_finite_difference.svg | 1673 +++++------------ .../rhs_dsdt_finite_difference_dark.svg | 1673 +++++------------ tests/test_rhs.py | 26 +- 4 files changed, 1038 insertions(+), 2340 deletions(-) diff --git a/docs/Validation/rhs.md b/docs/Validation/rhs.md index 3083cf12..e1d71e7b 100644 --- a/docs/Validation/rhs.md +++ b/docs/Validation/rhs.md @@ -18,10 +18,10 @@ The section below details the validation check, its tolerance, and its test comm **Why it matters.** The right-hand side assembly provides the core time derivative that PETSc integrates to advance mantle thermal evolution in SPIDER. -![Reported dS/dt and the slope of S between the 10 and 20 yr outputs against radius, and their relative difference with the tolerance](../assets/validation/rhs_dsdt_finite_difference.svg#only-light){ width="80%" } -![Reported dS/dt and the slope of S between the 10 and 20 yr outputs against radius, and their relative difference with the tolerance](../assets/validation/rhs_dsdt_finite_difference_dark.svg#only-dark){ width="80%" } +![Largest reported dS/dt magnitude of the run against the asserted window](../assets/validation/rhs_dsdt_finite_difference.svg#only-light){ width="80%" } +![Largest reported dS/dt magnitude of the run against the asserted window](../assets/validation/rhs_dsdt_finite_difference_dark.svg#only-dark){ width="80%" } -**How to read the plot.** Top: the cooling rate -dS/dt that rhs.c reports, as the mean of the 10 and 20 yr outputs (line). The dashed line is the slope of the entropy between these outputs, at every staggered node of the blackbody50 run. Bottom: their relative difference per node (dots) and the tolerance of the assertion (dashed); the largest difference is 0.65 percent. A pass means that, at every node, the dot lies below the dashed line. +**How to read the plot.** The y-axis is the largest cooling rate |dS/dt| of the blackbody50 run, on a log scale. The circle is this asserted value, the mean of the 10 and 20 yr outputs, shown to 1 significant digit. The grey band is the asserted window from 1e-12 to 1e-4 W/kg/K. The node profiles are not drawn, because they move by about 7e-4 (relative) between the macOS and Linux builds. The main comparison, with the slope of the specific entropy S between the outputs to relative 2e-2 at every node, is not drawn either. A pass means that the circle lies in the grey band. **Known limit.** The trajectory comes from the same right-hand side that reports dS/dt, so a wrong right-hand side gives a consistent wrong trajectory and passes; dS/dt is not compared with an independent physical value. The run has no heating, so a dropped heating term cannot show. Finite differencing over 10-year intervals adds a truncation error of up to 0.65 percent. diff --git a/docs/assets/validation/rhs_dsdt_finite_difference.svg b/docs/assets/validation/rhs_dsdt_finite_difference.svg index 821a8c2c..dc0c2630 100644 --- a/docs/assets/validation/rhs_dsdt_finite_difference.svg +++ b/docs/assets/validation/rhs_dsdt_finite_difference.svg @@ -1,12 +1,12 @@ - + - test: tests/test_rhs.py::test_reported_dsdt_matches_finite_difference_of_outputs; command: pytest tests/test_rhs.py::test_reported_dsdt_matches_finite_difference_of_outputs --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[49, 3528.420367522105, 6337.615609804658, 4865.232006967509, -488.80990442400326, 2.26778410522055e-08, 2.3037609191603335e-08, 2.3016097688843457e-08, -3.956771722936898e-12], [49, 3528.420367522105, 6337.615609804658, 4865.232006967509, -488.80990442400326, 2.2580465746253024e-08, 2.2892264545409143e-08, 2.2872173017027516e-08, -3.2532819653545098e-12]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}, {"scales": ["linear", "log"], "categories": [null, null], "lines": [[49, 3528.420367522105, 6337.615609804658, 4865.232006967509, -488.80990442400326, -2.3671528717895494, -2.1842403282860112, -2.204508940493961, -0.002654770803033463], [2, 0.0, 1.0, 0.5, 0.5, -1.6989700043360187, -1.6989700043360187, -1.6989700043360187, 0.0]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} + test: tests/test_rhs.py::test_reported_dsdt_matches_finite_difference_of_outputs; command: pytest tests/test_rhs.py::test_reported_dsdt_matches_finite_difference_of_outputs --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "log"], "categories": [null, null], "lines": [[1, 0.0, 0.0, 0.0, 0.0, -7.698970004336019, -7.698970004336019, -7.698970004336019, 0.0]], "collections": [], "values": [], "value_scales": [], "patches": [[5, 0.0, 1.0, 0.4, -0.1, -12.0, -4.0, -8.8, 0.7999999999999998]], "images": [], "image_scales": [], "extents": []}]} image/svg+xml @@ -21,121 +21,135 @@ - - - - + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - + + + + + + + + + + + + + + - - - - - + + + - - - - + - + - - - + + + - - - - - - + + + + + - - - + + + - + - + - - - + + + + + + + + + + + + + + + + + + + + + + + - - - - + + + + - - - + + + - + - + - - - + + + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - + + diff --git a/docs/assets/validation/rhs_dsdt_finite_difference_dark.svg b/docs/assets/validation/rhs_dsdt_finite_difference_dark.svg index 156a5b92..dd89dddf 100644 --- a/docs/assets/validation/rhs_dsdt_finite_difference_dark.svg +++ b/docs/assets/validation/rhs_dsdt_finite_difference_dark.svg @@ -1,12 +1,12 @@ - + - test: tests/test_rhs.py::test_reported_dsdt_matches_finite_difference_of_outputs; command: pytest tests/test_rhs.py::test_reported_dsdt_matches_finite_difference_of_outputs --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[49, 3528.420367522105, 6337.615609804658, 4865.232006967509, -488.80990442400326, 2.26778410522055e-08, 2.3037609191603335e-08, 2.3016097688843457e-08, -3.956771722936898e-12], [49, 3528.420367522105, 6337.615609804658, 4865.232006967509, -488.80990442400326, 2.2580465746253024e-08, 2.2892264545409143e-08, 2.2872173017027516e-08, -3.2532819653545098e-12]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}, {"scales": ["linear", "log"], "categories": [null, null], "lines": [[49, 3528.420367522105, 6337.615609804658, 4865.232006967509, -488.80990442400326, -2.3671528717895494, -2.1842403282860112, -2.204508940493961, -0.002654770803033463], [2, 0.0, 1.0, 0.5, 0.5, -1.6989700043360187, -1.6989700043360187, -1.6989700043360187, 0.0]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} + test: tests/test_rhs.py::test_reported_dsdt_matches_finite_difference_of_outputs; command: pytest tests/test_rhs.py::test_reported_dsdt_matches_finite_difference_of_outputs --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "log"], "categories": [null, null], "lines": [[1, 0.0, 0.0, 0.0, 0.0, -7.698970004336019, -7.698970004336019, -7.698970004336019, 0.0]], "collections": [], "values": [], "value_scales": [], "patches": [[5, 0.0, 1.0, 0.4, -0.1, -12.0, -4.0, -8.8, 0.7999999999999998]], "images": [], "image_scales": [], "extents": []}]} image/svg+xml @@ -21,121 +21,135 @@ - - - - + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - + + + + + + + + + + + + + + - - - - - + + + - - - - + - + - - - + + + - - - - - - + + + + + - - - + + + - + - + - - - + + + + + + + + + + + + + + + + + + + + + + + - - - - + + + + - - - + + + - + - + - - - + + + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - + + diff --git a/tests/test_rhs.py b/tests/test_rhs.py index 55e86fee..556e8298 100644 --- a/tests/test_rhs.py +++ b/tests/test_rhs.py @@ -14,6 +14,7 @@ import pytest from tests._json_utils import data_si, read_output +from tests._plot_utils import shown pytestmark = [pytest.mark.smoke, pytest.mark.timeout(120)] @@ -59,25 +60,20 @@ def test_reported_dsdt_matches_finite_difference_of_outputs(blackbody_dt10, vali # Scale guard: early magma-ocean cooling rates are of order # 1e-8 J/kg/K/s here; four decades either way flags a unit slip # (e.g. per year instead of per second). - assert 1e-12 < np.max(np.abs(midpoint)) < 1e-4 + scale_lo, scale_hi = 1e-12, 1e-4 + peak = np.max(np.abs(midpoint)) + assert scale_lo < peak < scale_hi def figure(): import matplotlib.pyplot as plt - radius_km = data_si(d1, 'radius_s') / 1e3 - fig, (top, bottom) = plt.subplots(2, 1, sharex=True, figsize=(6.4, 5.6)) - top.plot(radius_km, -midpoint, label='reported, midpoint of 10 and 20 yr') - top.plot(radius_km, -fd_slope, '--', label='slope of S between the outputs') - top.set_ylabel('-dS/dt [W/kg/K]') - top.legend() - rel = np.abs(fd_slope - midpoint) / np.abs(midpoint) - bottom.plot(radius_km, rel, 'o', ms=3) - bottom.axhline(rtol, color=plt.rcParams['text.color'], ls='--', label='tolerance') - bottom.set(yscale='log', xlabel='Radius [km]', ylabel='Rel. difference') - bottom.set_ylim(min(1e-3, max(rel.min() / 2, 1e-16)), 1e-1) - bottom.legend(loc='lower right') - for ax in (top, bottom): - ax.minorticks_off() + fig, ax = plt.subplots(figsize=(4.8, 4)) + ax.axhspan(scale_lo, scale_hi, alpha=0.15, label='Asserted window, 1e-12 to 1e-4') + ax.plot([0.0], [shown(peak, 1)], 'o', zorder=3, label='Largest |dS/dt| of the run') + ax.set(yscale='log', ylim=(1e-14, 1e-2), xlim=(-1.0, 1.0), ylabel='|dS/dt| [W/kg/K]') + ax.set_xticks([]) + ax.minorticks_off() + ax.legend(loc='upper right', fontsize=8) fig.tight_layout() return fig From a9839beb333b71cedbe63f2228a84f5a7d611d87 Mon Sep 17 00:00:00 2001 From: timlichtenberg Date: Mon, 26 Oct 2026 23:59:45 +0100 Subject: [PATCH 14/15] Name asserted tolerances in the validation plot notes Each How to read the plot note names the tolerance its test asserts when the plot cannot show it, and the energy, eos and rheological front notes stay within 6 sentences. The disabled-transport test asserts and draws the same named maxima, and the affine interpolation plot reads its nodes from the table fixture. The radiogenic heating sum is drawn as a wide pale line under the al26 line, the ocean-moles bars are grey so the stored values stand out, and 2 clipped axis labels are shorter. --- docs/Validation/bc.md | 4 +- docs/Validation/energy.md | 6 +- docs/Validation/eos.md | 2 +- docs/Validation/eos_composite.md | 2 +- docs/Validation/ic.md | 2 +- docs/Validation/rheologicalfront.md | 2 +- .../validation/energy_radiogenic_decay.svg | 414 ++++++++--------- .../energy_radiogenic_decay_dark.svg | 414 ++++++++--------- .../validation/ic_abundance_inventory.svg | 432 ++++++++---------- .../ic_abundance_inventory_dark.svg | 432 ++++++++---------- docs/assets/validation/ic_ocean_moles.svg | 10 +- .../assets/validation/ic_ocean_moles_dark.svg | 10 +- tests/test_energy.py | 28 +- tests/test_ic.py | 12 +- tests/test_interp.py | 7 +- 15 files changed, 789 insertions(+), 988 deletions(-) diff --git a/docs/Validation/bc.md b/docs/Validation/bc.md index 8b8442a3..cabe4d79 100644 --- a/docs/Validation/bc.md +++ b/docs/Validation/bc.md @@ -23,7 +23,7 @@ The sections below detail two validation cases, their tolerances, and their test ![Reported surface flux of the prescribed-flux run against the grey-body law with emissivity 1 and with the reported emissivity](../assets/validation/bc_prescribed_flux.svg#only-light){ width="80%" } ![Reported surface flux of the prescribed-flux run against the grey-body law with emissivity 1 and with the reported emissivity](../assets/validation/bc_prescribed_flux_dark.svg#only-dark){ width="80%" } -**How to read the plot.** The x-axis is the surface temperature and the y-axis the surface flux on a log scale. The dotted line is the prescribed flux, 1e5 W/m$^2$, and the circle the asserted surface flux Fatm at the reported surface temperature. The solid line is the grey-body law with emissivity 1; the test requires it to be more than 10 % away at that temperature. The dashed line is the grey-body law with the asserted inverted emissivity, about 0.12. A pass means that the circle sits on the dotted and the dashed line, far below the solid one. +**How to read the plot.** The x-axis is the surface temperature and the y-axis the surface flux on a log scale. The dotted line is the prescribed flux, 1e5 W/m$^2$, and the circle the asserted surface flux Fatm (relative 1e-9) at the reported surface temperature. The solid line is the grey-body law with emissivity 1; the test requires it to be more than 10 % away at that temperature. The dashed line is the grey-body law with the inverted emissivity, about 0.12, asserted to relative 1e-6. A pass means that the circle sits on the dotted and the dashed line, far below the solid one. ??? info "Test details" - Reference: Analytical identity: with SURFACE_BC 4 the surface flux equals the prescribed value, and the reported emissivity is its grey-body inversion. @@ -47,7 +47,7 @@ The sections below detail two validation cases, their tolerances, and their test ![Initial entropy profiles with the constant-entropy boundary and with the default boundary](../assets/validation/bc_constant_entropy_surface.svg#only-light){ width="80%" } ![Initial entropy profiles with the constant-entropy boundary and with the default boundary](../assets/validation/bc_constant_entropy_surface_dark.svg#only-dark){ width="80%" } -**How to read the plot.** The x-axis is the radius and the y-axis the entropy at the basic nodes (the cell boundaries of the mesh) at $t$ = 0. The solid line is the asserted profile of the run with a constant-entropy surface boundary. The dashed line is the default run (context); its surface entropy, 2599.7 J kg$^{-1}$ K$^{-1}$, must differ from 2550 by more than 40. The circle is the asserted surface node, and the hollow square the value 2550 J kg$^{-1}$ K$^{-1}$ of the option `ic_surface_entropy`. A pass means that the circle sits in the square and the deep end of the solid line lies between 2400 and 2700. +**How to read the plot.** The x-axis is the radius and the y-axis the entropy at the basic nodes (the cell boundaries of the mesh) at $t$ = 0. The solid line is the asserted profile of the run with a constant-entropy surface boundary. The dashed line is the default run (context); its surface entropy, 2599.7 J kg$^{-1}$ K$^{-1}$, must differ from 2550 by more than 40. The circle is the asserted surface node, and the hollow square the value 2550 J kg$^{-1}$ K$^{-1}$ of the option `ic_surface_entropy`. A pass means that the circle sits in the square (relative 1e-10) and the deep end of the solid line lies between 2400 and 2700. ??? info "Test details" - Reference: Analytical identity: the constant-entropy boundary writes ic_surface_entropy (2550 J/kg/K) onto the surface basic node, preserved through the steady-state energy solve into the initial output. diff --git a/docs/Validation/energy.md b/docs/Validation/energy.md index 656e4d1e..00f9c119 100644 --- a/docs/Validation/energy.md +++ b/docs/Validation/energy.md @@ -23,7 +23,7 @@ The sections below detail three validation cases, their tolerances, and their te ![Largest flux of each transport term in the conduction-off and the conduction-only runs](../assets/validation/energy_disabled_transport.svg#only-light){ width="80%" } ![Largest flux of each transport term in the conduction-off and the conduction-only runs](../assets/validation/energy_disabled_transport_dark.svg#only-dark){ width="80%" } -**How to read the plot.** The circles are fluxes on a symmetric log axis (linear between $-10^{-3}$ and $10^{-3}$). The first 3 are the largest absolute flux over the basic nodes of one run. Values are shown to 2 significant digits, because the run output moves by up to a few 1e-6 between builds of the PETSc library. The first 2 circles are asserted to be 0, to 1e-20 W/m$^2$. They are the conductive flux with conduction off, and the convective, mixing and separation fluxes with conduction only. The last 2 circles are asserted to be above $10^3$ W/m$^2$ (dashed line). They are the convective flux with conduction off, and the gap between total and conductive flux at the top node with conduction only. A pass means that the first 2 circles sit at 0 and the last 2 above the dashed line. +**How to read the plot.** The circles are the largest absolute flux over the basic nodes. The axis is symmetric log, linear between $-10^{-3}$ and $10^{-3}$. Jcond, Jconv, Jmix and Jgrav are the conductive, convective, mixing and separation fluxes, and Jtot is their sum. Tick labels name each case. Values are shown to 2 significant digits, because the run output moves by up to a few 1e-6 between builds of the PETSc library. A pass means that the first 2 circles sit at 0 (1e-20 W/m$^2$) and the last 2 above the dashed line at $10^3$ W/m$^2$. ??? info "Test details" - Reference: Analytical limit: a disabled transport mechanism carries zero flux, and with only conduction enabled the total flux equals the conductive flux at every node except the surface node, which carries the grey-body boundary flux. @@ -47,7 +47,7 @@ The sections below detail three validation cases, their tolerances, and their te ![Radiogenic heating of the 2-isotope run against time](../assets/validation/energy_radiogenic_decay.svg#only-light){ width="80%" } ![Radiogenic heating of the 2-isotope run against time](../assets/validation/energy_radiogenic_decay_dark.svg#only-dark){ width="80%" } -**How to read the plot.** The x-axis is the time and the y-axis the specific radiogenic heating on a log scale. The solid line is the sum of both isotopes in closed form. The dashed and dotted lines are the isotopes `al26` and `k40` alone, with test half-lives of 100 and 200 yr (context). The circles are the asserted heating Hradio of the run at 0 and 200 yr. A pass means that both lie on the solid line. The test asserts the value at $t$ = 0 to relative 1e-6. It asserts the ratio of the 200 yr to the 0 yr value, about 0.2506, to relative 1e-4. +**How to read the plot.** The x-axis is the time and the y-axis the specific radiogenic heating on a log scale. The wide pale line is the closed-form sum of the 2 isotopes. The dashed and dotted lines are the isotopes `al26` and `k40` alone, with test half-lives of 100 and 200 yr (context). The dashed line runs inside the wide line, because `al26` gives more than 99 % of the heating. The circles are the asserted Hradio at 0 and 200 yr: the value at 0 to relative 1e-6, and their ratio, about 0.2506, to 1e-4. A pass means that both circles lie on the wide line. ??? info "Test details" - Reference: Analytical limit: radiogenic heating is the sum over isotopes of concentration times abundance times heat production, decaying as 2^(-(t - t0)/half_life). A two-isotope configuration with 100-year and 200-year half-lives pins the sum at t = 0 and the weighted decay factor 0.2506 after 200 years. @@ -71,7 +71,7 @@ The sections below detail three validation cases, their tolerances, and their te ![Energy flow against radius and surface flux on the grey-body law for the steady-state initial condition](../assets/validation/energy_steady_state_ic.svg#only-light){ width="80%" } ![Energy flow against radius and surface flux on the grey-body law for the steady-state initial condition](../assets/validation/energy_steady_state_ic_dark.svg#only-dark){ width="80%" } -**How to read the plot.** Left: the asserted energy flow Etot at every basic node of the steady-state initial condition, against radius. A pass means a flat line. The test asserts a relative spread below 1e-8. Right: the solid line is the grey-body law $\sigma(T^4 - 273^4)$, with $\sigma$ the Stefan-Boltzmann constant. Here 273 K is the equilibrium temperature. The dashed line is the rejected $T^3$ form. The circle is the asserted surface flux Fatm at the reported surface temperature. A pass means that the circle lies on the line (relative 1e-5). +**How to read the plot.** Left: the asserted energy flow Etot at every basic node of the steady-state initial condition, against radius. A pass means a flat line, with a relative spread below 1e-8. Right: the solid line is the grey-body law $\sigma(T^4 - 273^4)$. Here $\sigma$ is the Stefan-Boltzmann constant and 273 K the equilibrium temperature. The dashed line is the rejected $T^3$ form. The circle is the asserted surface flux Fatm at the reported surface temperature, which must lie on the solid line (relative 1e-5). ??? info "Test details" - Reference: Analytical limit: the steady-state initial condition equalises the energy flow through every basic node and closes the surface radiation balance sigma (T_surf^4 - teqm^4) with the Stefan-Boltzmann constant of constants.c. diff --git a/docs/Validation/eos.md b/docs/Validation/eos.md index 4a2953df..28f6b3df 100644 --- a/docs/Validation/eos.md +++ b/docs/Validation/eos.md @@ -47,7 +47,7 @@ The sections below detail two validation cases, their tolerances, and their test ![Spaargaren viscosity prefactor against Mg/Si with the eos.c shifts](../assets/validation/eos_spaargaren_viscosity.svg#only-light){ width="80%" } ![Spaargaren viscosity prefactor against Mg/Si with the eos.c shifts](../assets/validation/eos_spaargaren_viscosity_dark.svg#only-dark){ width="80%" } -**How to read the plot.** The x-axis is the Mg/Si ratio and the y-axis the log10 viscosity prefactor. The line is the piecewise prefactor of Spaargaren et al. (2020), as the test re-encodes it (context). The hollow squares are its values at the 2 reference compositions, 1.6 and 0.9. The circles are each reference value plus the asserted shift of eos.c, at 1.4 and at 1.2. The shift is the melt log10 viscosity minus the configured 2.0. A pass means that the circles lie on the line (absolute 1e-9), the one at 1.4 above its square and the one at 1.2 below. +**How to read the plot.** The x-axis is the Mg/Si ratio and the y-axis the log10 viscosity prefactor. The line is the piecewise prefactor of Spaargaren et al. (2020), as the test re-encodes it (context). The hollow squares are its values at the 2 reference compositions, 1.6 and 0.9. The circles are each reference value plus the asserted shift of eos.c (melt log10 viscosity minus the configured 2.0), at 1.4 and 1.2. A pass means that the circles lie on the line (absolute 1e-9), the one at 1.4 above its square and the one at 1.2 below. ??? info "Test details" - Reference: The piecewise Mg/Si log10 viscosity prefactor of Spaargaren et al. (2020). The test re-encodes the published coefficients independently of eos.c and additionally pins the two branch-pair shifts as hand-derived literals (0.2074 and -1.35803), so a transcription error in either copy of the formula surfaces. diff --git a/docs/Validation/eos_composite.md b/docs/Validation/eos_composite.md index 6e34c2b2..435f13e8 100644 --- a/docs/Validation/eos_composite.md +++ b/docs/Validation/eos_composite.md @@ -23,7 +23,7 @@ The section below details the validation check, its tolerance, and its test comm ![Melt fraction against entropy at 10 GPa between the solidus and the liquidus](../assets/validation/eos_composite_melt_fraction.svg#only-light){ width="80%" } ![Melt fraction against entropy at 10 GPa between the solidus and the liquidus](../assets/validation/eos_composite_melt_fraction_dark.svg#only-dark){ width="80%" } -**How to read the plot.** The x-axis is the entropy $S$ at 10 GPa and the y-axis the melt fraction. The dotted lines are the solidus and liquidus entropies $S_\mathrm{sol}$ and $S_\mathrm{liq}$ that eos_composite.c reports. The solid line is $(S - S_\mathrm{sol}) / (S_\mathrm{liq} - S_\mathrm{sol})$ between them (context). The dashed line is the form with the liquidus entropy as the denominator, which the test rejects. The circle is the asserted melt fraction at 1675.34 J kg$^{-1}$ K$^{-1}$, a quarter of the way through the interval. A pass means that the circle lies on the solid line at 0.25. +**How to read the plot.** The x-axis is the entropy $S$ at 10 GPa and the y-axis the melt fraction. The dotted lines are the solidus and liquidus entropies $S_\mathrm{sol}$ and $S_\mathrm{liq}$ that eos_composite.c reports. The solid line is $(S - S_\mathrm{sol}) / (S_\mathrm{liq} - S_\mathrm{sol})$ between them (context). The dashed line is the form with the liquidus entropy as the denominator, which the test rejects. The circle is the asserted melt fraction at 1675.34 J kg$^{-1}$ K$^{-1}$, about a quarter of the way through the interval. A pass means that the circle lies on the solid line (relative 1e-6), here at about 0.25. ??? info "Test details" - Reference: Analytical limit: by construction of the two-phase region, fusion = S_liq - S_sol and phi = (S - S_sol)/fusion between the phase boundaries. diff --git a/docs/Validation/ic.md b/docs/Validation/ic.md index e8394338..7bf47453 100644 --- a/docs/Validation/ic.md +++ b/docs/Validation/ic.md @@ -23,7 +23,7 @@ The sections below detail three validation cases, their tolerances, and their te ![Initial entropy profiles for ic_adiabat_entropy 2600 and 2400](../assets/validation/ic_adiabat_profile.svg#only-light){ width="80%" } ![Initial entropy profiles for ic_adiabat_entropy 2600 and 2400](../assets/validation/ic_adiabat_profile_dark.svg#only-dark){ width="80%" } -**How to read the plot.** The x-axis is the radius and the y-axis the entropy at the staggered nodes (the cell centres of the mesh) at $t$ = 0. The solid line is the asserted profile for `ic_adiabat_entropy` = 2600. The dashed line is the rerun with 2400, which must be 200 J kg$^{-1}$ K$^{-1}$ lower on average. The circle is the asserted top staggered node at 2600. The dotted line is the mean of the 2600 profile, and the grey band its asserted range, 2600 to 2628.7. A pass means that the circle sits at 2600, the dotted line inside the band, and the dashed line 200 below the solid one. +**How to read the plot.** The x-axis is the radius and the y-axis the entropy at the staggered nodes (the cell centres of the mesh) at $t$ = 0. The solid line is the asserted profile for `ic_adiabat_entropy` = 2600. The dashed line is the rerun with 2400, whose mean must be 200 J kg$^{-1}$ K$^{-1}$ lower (relative 1e-9). The circle is the asserted top staggered node, 2600 to relative 1e-3. The dotted line is the mean of the 2600 profile, and the grey band its asserted range, 2600 to 2628.7. A pass means that the circle sits at 2600, the dotted line inside the band, and the dashed line 200 below the solid one. ??? info "Test details" - Reference: Analytical limit: the initial condition prescribes the adiabat entropy (2600 J/kg/K at the top staggered node) with the configured small gradient ramp. diff --git a/docs/Validation/rheologicalfront.md b/docs/Validation/rheologicalfront.md index 326f4389..fce8c7aa 100644 --- a/docs/Validation/rheologicalfront.md +++ b/docs/Validation/rheologicalfront.md @@ -23,7 +23,7 @@ The section below details the validation check, its tolerance, and its test comm ![Radii of the 2 nodes on each side of the critical melt fraction and of the reported front at 1200 yr](../assets/validation/rheologicalfront_location.svg#only-light){ width="80%" } ![Radii of the 2 nodes on each side of the critical melt fraction and of the reported front at 1200 yr](../assets/validation/rheologicalfront_location_dark.svg#only-dark){ width="80%" } -**How to read the plot.** The y-axis is the radius, shown to 3 significant digits. The squares are the 2 staggered nodes (cell centres of the mesh) that the test finds itself at 1200 yr. They are the last node at or above the critical melt fraction $\phi_c = 0.4$ (0.403), and the first node below it (0.3995). The circle is the asserted front radius, the planet radius minus the reported depth. The melt fractions are not drawn: the first node is only 4.5e-4 below $\phi_c$, so a rounded value is not stable between builds. A pass means that the front node is at most 1 node from the first node below $\phi_c$. In this run the circle lies between the squares. +**How to read the plot.** The y-axis is the radius, shown to 3 significant digits. The squares are the 2 staggered nodes (cell centres) at 1200 yr on each side of the critical melt fraction $\phi_c = 0.4$ (0.403 and 0.3995). The circle is the asserted front radius, the planet radius minus the reported depth. The test asserts the reported depth and pressure against the basic-node arrays to relative 1e-6. The melt fractions are not drawn: the first node is only 4.5e-4 below $\phi_c$, so a rounded value is not stable between builds. A pass means that the front node is at most 1 node from the first node below $\phi_c$; here the circle lies between the squares. ??? info "Test details" - Reference: Analytical self-consistency: the reported front index matches the phi_critical = 0.4 crossing of the melt-fraction profile, and the depth equals the surface radius minus the front radius. diff --git a/docs/assets/validation/energy_radiogenic_decay.svg b/docs/assets/validation/energy_radiogenic_decay.svg index 25d62e48..39034c84 100644 --- a/docs/assets/validation/energy_radiogenic_decay.svg +++ b/docs/assets/validation/energy_radiogenic_decay.svg @@ -42,7 +42,7 @@ z +" clip-path="url(#pea763f2328)" style="fill: none; stroke: #d2dae1; stroke-width: 0.8; stroke-linecap: square"/> @@ -96,7 +96,7 @@ z +" clip-path="url(#pea763f2328)" style="fill: none; stroke: #d2dae1; stroke-width: 0.8; stroke-linecap: square"/> @@ -147,7 +147,7 @@ z +" clip-path="url(#pea763f2328)" style="fill: none; stroke: #d2dae1; stroke-width: 0.8; stroke-linecap: square"/> @@ -184,7 +184,7 @@ z +" clip-path="url(#pea763f2328)" style="fill: none; stroke: #d2dae1; stroke-width: 0.8; stroke-linecap: square"/> @@ -204,7 +204,7 @@ L 234.952047 7.2 +" clip-path="url(#pea763f2328)" style="fill: none; stroke: #d2dae1; stroke-width: 0.8; stroke-linecap: square"/> @@ -251,7 +251,7 @@ z +" clip-path="url(#pea763f2328)" style="fill: none; stroke: #d2dae1; stroke-width: 0.8; stroke-linecap: square"/> @@ -271,7 +271,7 @@ L 337.489651 7.2 +" clip-path="url(#pea763f2328)" style="fill: none; stroke: #d2dae1; stroke-width: 0.8; stroke-linecap: square"/> @@ -500,7 +500,7 @@ z +" clip-path="url(#pea763f2328)" style="fill: none; stroke: #d2dae1; stroke-width: 0.8; stroke-linecap: square"/> @@ -584,7 +584,7 @@ z +" clip-path="url(#pea763f2328)" style="fill: none; stroke: #d2dae1; stroke-width: 0.8; stroke-linecap: square"/> @@ -621,7 +621,7 @@ z +" clip-path="url(#pea763f2328)" style="fill: none; stroke: #d2dae1; stroke-width: 0.8; stroke-linecap: square"/> @@ -872,34 +872,26 @@ L -2 0 - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + @@ -1274,11 +1149,11 @@ L 352.870292 67.574818 L 357.997172 68.529174 L 363.124052 69.483479 L 368.250932 70.437731 -L 373.377812 71.391931 +L 373.377813 71.391931 L 378.504693 72.346076 L 383.631573 73.300167 L 388.758453 74.254202 -" clip-path="url(#p1e155a85fc)" style="fill: none; stroke: #10151b; stroke-width: 2.2; stroke-linecap: square"/> +" clip-path="url(#pea763f2328)" style="fill: none; stroke: #10151b; stroke-opacity: 0.35; stroke-width: 6; stroke-linecap: square"/> +" clip-path="url(#pea763f2328)" style="fill: none; stroke-dasharray: 8.14,3.52; stroke-dashoffset: 0; stroke: #e23d28; stroke-width: 2.2"/> +" clip-path="url(#pea763f2328)" style="fill: none; stroke-dasharray: 2.2,3.63; stroke-dashoffset: 0; stroke: #e0a32e; stroke-width: 2.2"/> - + @@ -1442,7 +1317,7 @@ z +" style="fill: none; stroke: #10151b; stroke-opacity: 0.35; stroke-width: 6; stroke-linecap: square"/> @@ -1504,6 +1379,33 @@ L 3258 3264 L 3258 0 L 2778 0 z +" transform="scale(0.015625)"/> + + - + @@ -1802,7 +1764,7 @@ z - + diff --git a/docs/assets/validation/energy_radiogenic_decay_dark.svg b/docs/assets/validation/energy_radiogenic_decay_dark.svg index ac17e403..4fa1f6da 100644 --- a/docs/assets/validation/energy_radiogenic_decay_dark.svg +++ b/docs/assets/validation/energy_radiogenic_decay_dark.svg @@ -42,7 +42,7 @@ z +" clip-path="url(#pea763f2328)" style="fill: none; stroke: #1a2230; stroke-width: 0.8; stroke-linecap: square"/> @@ -96,7 +96,7 @@ z +" clip-path="url(#pea763f2328)" style="fill: none; stroke: #1a2230; stroke-width: 0.8; stroke-linecap: square"/> @@ -147,7 +147,7 @@ z +" clip-path="url(#pea763f2328)" style="fill: none; stroke: #1a2230; stroke-width: 0.8; stroke-linecap: square"/> @@ -184,7 +184,7 @@ z +" clip-path="url(#pea763f2328)" style="fill: none; stroke: #1a2230; stroke-width: 0.8; stroke-linecap: square"/> @@ -204,7 +204,7 @@ L 234.952047 7.2 +" clip-path="url(#pea763f2328)" style="fill: none; stroke: #1a2230; stroke-width: 0.8; stroke-linecap: square"/> @@ -251,7 +251,7 @@ z +" clip-path="url(#pea763f2328)" style="fill: none; stroke: #1a2230; stroke-width: 0.8; stroke-linecap: square"/> @@ -271,7 +271,7 @@ L 337.489651 7.2 +" clip-path="url(#pea763f2328)" style="fill: none; stroke: #1a2230; stroke-width: 0.8; stroke-linecap: square"/> @@ -500,7 +500,7 @@ z +" clip-path="url(#pea763f2328)" style="fill: none; stroke: #1a2230; stroke-width: 0.8; stroke-linecap: square"/> @@ -584,7 +584,7 @@ z +" clip-path="url(#pea763f2328)" style="fill: none; stroke: #1a2230; stroke-width: 0.8; stroke-linecap: square"/> @@ -621,7 +621,7 @@ z +" clip-path="url(#pea763f2328)" style="fill: none; stroke: #1a2230; stroke-width: 0.8; stroke-linecap: square"/> @@ -872,34 +872,26 @@ L -2 0 - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + @@ -1274,11 +1149,11 @@ L 352.870292 67.574818 L 357.997172 68.529174 L 363.124052 69.483479 L 368.250932 70.437731 -L 373.377812 71.391931 +L 373.377813 71.391931 L 378.504693 72.346076 L 383.631573 73.300167 L 388.758453 74.254202 -" clip-path="url(#p1e155a85fc)" style="fill: none; stroke: #f2f5f7; stroke-width: 2.2; stroke-linecap: square"/> +" clip-path="url(#pea763f2328)" style="fill: none; stroke: #f2f5f7; stroke-opacity: 0.35; stroke-width: 6; stroke-linecap: square"/> +" clip-path="url(#pea763f2328)" style="fill: none; stroke-dasharray: 8.14,3.52; stroke-dashoffset: 0; stroke: #e23d28; stroke-width: 2.2"/> +" clip-path="url(#pea763f2328)" style="fill: none; stroke-dasharray: 2.2,3.63; stroke-dashoffset: 0; stroke: #e0a32e; stroke-width: 2.2"/> - + @@ -1442,7 +1317,7 @@ z +" style="fill: none; stroke: #f2f5f7; stroke-opacity: 0.35; stroke-width: 6; stroke-linecap: square"/> @@ -1504,6 +1379,33 @@ L 3258 3264 L 3258 0 L 2778 0 z +" transform="scale(0.015625)"/> + + - + @@ -1802,7 +1764,7 @@ z - + diff --git a/docs/assets/validation/ic_abundance_inventory.svg b/docs/assets/validation/ic_abundance_inventory.svg index b2e0058e..5d4e6585 100644 --- a/docs/assets/validation/ic_abundance_inventory.svg +++ b/docs/assets/validation/ic_abundance_inventory.svg @@ -1,7 +1,7 @@ - + @@ -21,18 +21,18 @@ - - @@ -40,9 +40,9 @@ z - + @@ -51,12 +51,12 @@ L 0 3.5 " style="stroke: #3e4a55; stroke-width: 0.8"/> - + - + - + - + - + @@ -161,18 +161,18 @@ L 137.796355 7.2 - + - + - + - + - + - + @@ -233,9 +233,9 @@ L 237.926566 7.2 - + @@ -244,12 +244,12 @@ L -3.5 0 " style="stroke: #3e4a55; stroke-width: 0.8"/> - + - + - + - + - + - + - + - + @@ -438,18 +438,18 @@ L 261.957816 99.644933 - + - + - + @@ -465,216 +465,216 @@ L -2 0 " style="stroke: #3e4a55; stroke-width: 0.6"/> - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - +" clip-path="url(#p7c7986d760)" style="fill: #10151b; opacity: 0.35"/> - +" clip-path="url(#p7c7986d760)" style="fill: #10151b; opacity: 0.35"/> - +" clip-path="url(#p7c7986d760)" style="fill: #10151b; opacity: 0.35"/> - +" clip-path="url(#p7c7986d760)" style="fill: #10151b; opacity: 0.35"/> - - @@ -910,22 +910,22 @@ C -1.81853 3.131218 -0.928211 3.5 0 3.5 z " style="stroke: #10151b"/> - - - - - + + + + + - + - + - - + - - + - + - + - + - + - + - + - + - + @@ -1283,18 +1283,18 @@ L 542.827549 7.2 - + - + - + - + - + - + - + - + - + @@ -1393,18 +1393,18 @@ L 551.839268 118.646622 - + - + - + @@ -1416,18 +1416,18 @@ L 551.839268 75.82266 - + - + - + @@ -1438,115 +1438,51 @@ L 551.839268 32.998698 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + - + - - - - + + + + - - - - + + - - + + diff --git a/docs/assets/validation/ic_abundance_inventory_dark.svg b/docs/assets/validation/ic_abundance_inventory_dark.svg index e1e083a1..fd97d075 100644 --- a/docs/assets/validation/ic_abundance_inventory_dark.svg +++ b/docs/assets/validation/ic_abundance_inventory_dark.svg @@ -1,7 +1,7 @@ - + @@ -21,18 +21,18 @@ - - @@ -40,9 +40,9 @@ z - + @@ -51,12 +51,12 @@ L 0 3.5 " style="stroke: #9fb0be; stroke-width: 0.8"/> - + - + - + - + - + @@ -161,18 +161,18 @@ L 137.796355 7.2 - + - + - + - + - + - + @@ -233,9 +233,9 @@ L 237.926566 7.2 - + @@ -244,12 +244,12 @@ L -3.5 0 " style="stroke: #9fb0be; stroke-width: 0.8"/> - + - + - + - + - + - + - + - + @@ -438,18 +438,18 @@ L 261.957816 99.644933 - + - + - + @@ -465,216 +465,216 @@ L -2 0 " style="stroke: #9fb0be; stroke-width: 0.6"/> - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - +" clip-path="url(#p7c7986d760)" style="fill: #f2f5f7; opacity: 0.35"/> - +" clip-path="url(#p7c7986d760)" style="fill: #f2f5f7; opacity: 0.35"/> - +" clip-path="url(#p7c7986d760)" style="fill: #f2f5f7; opacity: 0.35"/> - +" clip-path="url(#p7c7986d760)" style="fill: #f2f5f7; opacity: 0.35"/> - - @@ -910,22 +910,22 @@ C -1.81853 3.131218 -0.928211 3.5 0 3.5 z " style="stroke: #f2f5f7"/> - - - - - + + + + + - + - + - - + - - + - + - + - + - + - + - + - + - + @@ -1283,18 +1283,18 @@ L 542.827549 7.2 - + - + - + - + - + - + - + - + - + @@ -1393,18 +1393,18 @@ L 551.839268 118.646622 - + - + - + @@ -1416,18 +1416,18 @@ L 551.839268 75.82266 - + - + - + @@ -1438,115 +1438,51 @@ L 551.839268 32.998698 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + - + - - - - + + + + - - - - + + - - + + diff --git a/docs/assets/validation/ic_ocean_moles.svg b/docs/assets/validation/ic_ocean_moles.svg index d3705c5e..b9fb0f10 100644 --- a/docs/assets/validation/ic_ocean_moles.svg +++ b/docs/assets/validation/ic_ocean_moles.svg @@ -904,7 +904,7 @@ L 131.485007 69945.473152 L 131.485007 17.894545 L 79.342694 17.894545 z -" clip-path="url(#pf4f4f83d4f)" style="fill: #10151b"/> +" clip-path="url(#pf4f4f83d4f)" style="fill: #10151b; opacity: 0.35"/> +" clip-path="url(#pf4f4f83d4f)" style="fill: #10151b; opacity: 0.35"/> +" clip-path="url(#pf4f4f83d4f)" style="fill: #10151b; opacity: 0.35"/> +" clip-path="url(#pf4f4f83d4f)" style="fill: #10151b; opacity: 0.35"/> @@ -1251,7 +1251,7 @@ L 249.599268 43.2 L 249.599268 37.6 L 233.599268 37.6 z -" style="fill: #10151b"/> +" style="fill: #10151b; opacity: 0.35"/> diff --git a/docs/assets/validation/ic_ocean_moles_dark.svg b/docs/assets/validation/ic_ocean_moles_dark.svg index 3786b292..28a99cbf 100644 --- a/docs/assets/validation/ic_ocean_moles_dark.svg +++ b/docs/assets/validation/ic_ocean_moles_dark.svg @@ -904,7 +904,7 @@ L 131.485007 69945.473152 L 131.485007 17.894545 L 79.342694 17.894545 z -" clip-path="url(#pf4f4f83d4f)" style="fill: #f2f5f7"/> +" clip-path="url(#pf4f4f83d4f)" style="fill: #f2f5f7; opacity: 0.35"/> +" clip-path="url(#pf4f4f83d4f)" style="fill: #f2f5f7; opacity: 0.35"/> +" clip-path="url(#pf4f4f83d4f)" style="fill: #f2f5f7; opacity: 0.35"/> +" clip-path="url(#pf4f4f83d4f)" style="fill: #f2f5f7; opacity: 0.35"/> @@ -1251,7 +1251,7 @@ L 249.599268 43.2 L 249.599268 37.6 L 233.599268 37.6 z -" style="fill: #f2f5f7"/> +" style="fill: #f2f5f7; opacity: 0.35"/> diff --git a/tests/test_energy.py b/tests/test_energy.py index 1fb0e353..d54c059d 100644 --- a/tests/test_energy.py +++ b/tests/test_energy.py @@ -82,12 +82,13 @@ def test_disabled_transport_mechanisms_carry_zero_flux( jcond_nc = data_si(doc_nc, 'Jcond_b') # W/m^2 # atol=1e-20 W/m^2 sits 29 decades below the active fluxes in this # state (9e8 to 2e9 W/m^2); the observed values are exactly zero. - np.testing.assert_allclose(jcond_nc, 0.0, atol=1e-20) + jcond_nc_max = np.abs(jcond_nc).max() + assert jcond_nc_max <= 1e-20 # Wrong-path guard: convection must remain active, otherwise the # zero above would only prove a dead run. - jconv_nc = data_si(doc_nc, 'Jconv_b') # W/m^2 + jconv_nc_max = np.abs(data_si(doc_nc, 'Jconv_b')).max() # W/m^2 active_min = 1e3 - assert np.abs(jconv_nc).max() > active_min + assert jconv_nc_max > active_min # Everything but conduction off: Jtot reduces to Jcond alone. cond_only = cached_spider_run( @@ -104,8 +105,10 @@ def test_disabled_transport_mechanisms_carry_zero_flux( name='conduction_only', ) doc_co = read_output(cond_only, 0) - for name in ('Jconv_b', 'Jmix_b', 'Jgrav_b'): - np.testing.assert_allclose(data_si(doc_co, name), 0.0, atol=1e-20) + off_max = np.max( + [np.abs(data_si(doc_co, n)).max() for n in ('Jconv_b', 'Jmix_b', 'Jgrav_b')] + ) + assert off_max <= 1e-20 jtot_co = data_si(doc_co, 'Jtot_b') # W/m^2 jcond_co = data_si(doc_co, 'Jcond_b') # W/m^2 # Interior nodes: the assembly collapses to the conductive flux with @@ -113,7 +116,8 @@ def test_disabled_transport_mechanisms_carry_zero_flux( np.testing.assert_allclose(jtot_co[1:], jcond_co[1:], rtol=1e-12, atol=1e-20) # Edge case, surface node: the grey-body boundary flux (8e5 W/m^2) # overrides the tiny conductive flux (0.01 W/m^2) at index 0. - assert abs(jtot_co[0] - jcond_co[0]) > active_min + top_gap = abs(jtot_co[0] - jcond_co[0]) + assert top_gap > active_min # Scale guard on the full-physics reference state: the peak total # flux at the fully molten IC is convective and large (observed @@ -124,7 +128,6 @@ def test_disabled_transport_mechanisms_carry_zero_flux( def figure(): import matplotlib.pyplot as plt - off = max(np.abs(data_si(doc_co, n)).max() for n in ('Jconv_b', 'Jmix_b', 'Jgrav_b')) names = [ 'Off: Jcond', 'Only conduction:\nJconv, Jmix, Jgrav', @@ -132,12 +135,7 @@ def figure(): 'Only conduction:\nJtot - Jcond, top', ] values = shown( - [ - np.abs(jcond_nc).max(), - off, - np.abs(jconv_nc).max(), - abs(jtot_co[0] - jcond_co[0]), - ], + [jcond_nc_max, off_max, jconv_nc_max, top_gap], digits=2, ) fig, ax = plt.subplots(figsize=(6, 4)) @@ -298,7 +296,7 @@ def figure(): t = np.linspace(0.0, 300.0, 61) al26, k40 = isotopes(t) fig, ax = plt.subplots(figsize=(6, 4)) - ax.plot(t, al26 + k40, label='Sum of the 2 isotopes') + ax.plot(t, al26 + k40, lw=6, alpha=0.35, label='Sum of the 2 isotopes') ax.plot(t, al26, '--', label='al26 alone, half-life 100 yr') ax.plot(t, k40, ':', label='k40 alone, half-life 200 yr') ax.plot( @@ -310,7 +308,7 @@ def figure(): ) ax.set_yscale('log') ax.set_xlabel('Time [yr]') - ax.set_ylabel('Radiogenic heating [W kg$^{-1}$]') + ax.set_ylabel('Heating [W kg$^{-1}$]') ax.legend(fontsize=8) fig.tight_layout() return fig diff --git a/tests/test_ic.py b/tests/test_ic.py index cd38ec1c..0d42e4d5 100644 --- a/tests/test_ic.py +++ b/tests/test_ic.py @@ -253,7 +253,7 @@ def figure(): names = list(ABUNDANCE_PPM) x = np.arange(len(names)) - fig, (left, right) = plt.subplots(1, 2, figsize=(8, 3.6)) + fig, (left, right) = plt.subplots(1, 2, figsize=(8, 4.2)) left.bar(x, [requested[n] for n in names], width=0.6, alpha=0.35, label='Requested') left.plot(x, [realised[n] for n in names], 'o', zorder=3, label='Realised reservoirs') left.set_xticks(x, names) @@ -264,7 +264,7 @@ def figure(): right.axhline(1.0, ls='--') right.plot([0, 1, 2], ratio, 'o') right.set_xticks([0, 1, 2], ['H', 'C', 'O']) - right.set_ylabel('Realised / requested moles') + right.set_ylabel('Mole ratio') fig.tight_layout() return fig @@ -341,7 +341,13 @@ def figure(): names = list(MOLES_OCEANS) x = np.arange(len(names)) fig, ax = plt.subplots(figsize=(6, 4)) - ax.bar(x, closed, width=0.6, label='Moles $\\times$ OCEAN_MOLES $\\times$ molar mass') + ax.bar( + x, + closed, + width=0.6, + alpha=0.35, + label='Moles $\\times$ OCEAN_MOLES $\\times$ molar mass', + ) ax.plot(x, stored, 'o', zorder=3, label='Stored initial_kg') ax.plot( [names.index('CO2')], [wrong], 'x', ms=10, label='CO$_2$ with the CO molar mass' diff --git a/tests/test_interp.py b/tests/test_interp.py index 08e03111..764d80dd 100644 --- a/tests/test_interp.py +++ b/tests/test_interp.py @@ -85,12 +85,13 @@ def affine(x): def figure(): import matplotlib.pyplot as plt + import numpy as np - nodes = list(range(11)) + nodes = np.loadtxt(linear_table_1d) fig, ax = plt.subplots(figsize=(6, 4)) ax.plot( - nodes, - [affine(x) for x in nodes], + nodes[:, 0], + nodes[:, 1], 's', mfc='none', ms=8, From ad6b7a1572b369df7d8b019641abb6d735fbba86 Mon Sep 17 00:00:00 2001 From: timlichtenberg Date: Tue, 27 Oct 2026 00:31:37 +0100 Subject: [PATCH 15/15] Define terms and symbols on the validation pages The validation index defines blackbody50, PROTEUS, the basic and staggered nodes, IC and BC, and each page defines its symbols with units before the formula that uses them. Axis units are named in the plot notes, the energy page lists the 4 heat fluxes, and the bc cases follow the order of the index. The Adams-Williamson page states the 100 kg/m3 gap the test asserts, the rheological front page calls its check self-consistent, the Spaargaren prefactor is attributed as in eos.c, and the abundance note marks the reservoir directions as not asserted. --- docs/Validation/atmosphere.md | 6 +- docs/Validation/bc.md | 52 ++++---- docs/Validation/energy.md | 14 +-- docs/Validation/eos.md | 14 +-- docs/Validation/eos_adamswilliamson.md | 4 +- docs/Validation/eos_composite.md | 2 +- docs/Validation/ic.md | 6 +- docs/Validation/index.md | 10 +- docs/Validation/matprop.md | 2 +- docs/Validation/mesh.md | 6 +- docs/Validation/rheologicalfront.md | 4 +- docs/Validation/rhs.md | 12 +- docs/Validation/twophase.md | 2 +- .../validation/rhs_dsdt_finite_difference.svg | 112 +++++++++--------- .../rhs_dsdt_finite_difference_dark.svg | 112 +++++++++--------- tests/test_rhs.py | 2 +- 16 files changed, 181 insertions(+), 179 deletions(-) diff --git a/docs/Validation/atmosphere.md b/docs/Validation/atmosphere.md index d03008e5..3f3963ae 100644 --- a/docs/Validation/atmosphere.md +++ b/docs/Validation/atmosphere.md @@ -12,18 +12,18 @@ The section below details the validation check, its tolerance, and its test comm **Why this check exists.** Numerical implementations of radiative flux can suffer from incorrect temperature exponents, or from inverted signs. A bug in the surface flux calculation leads to unphysical planetary cooling rates, without raising a runtime error. -**What it tests.** The test evaluates the grey-body surface flux F = emissivity sigma (T_surf^4 - T_eqm^4), with emissivity 1, and equilibrium temperature T_eqm = 273 K. It compares the reported surface flux against the Stefan-Boltzmann law, evaluated at the reported surface temperature. The test guards against exponent errors, where a T^3 exponent slip changes the flux by orders of magnitude at magma-ocean temperatures. The flux must remain positive (outward), and bounded to the 1e2 to 1e9 W/m2 regime. +**What it tests.** The surface flux $F$ (W/m$^2$) of a grey body depends on the emissivity $\epsilon$ (0 to 1), the Stefan-Boltzmann constant $\sigma$ (W m$^{-2}$ K$^{-4}$), the surface temperature $T_\mathrm{surf}$ and the equilibrium temperature $T_\mathrm{eqm}$ (both in K). The test evaluates $F = \epsilon\sigma(T_\mathrm{surf}^4 - T_\mathrm{eqm}^4)$ with $\epsilon$ = 1 and $T_\mathrm{eqm}$ = 273 K, the values of the blackbody50 run (the standard 50-node test run, `tests/opts/blackbody50.opts`). It compares the reported surface flux against the Stefan-Boltzmann law, evaluated at the reported surface temperature. The test guards against exponent errors, where a T^3 exponent slip changes the flux by orders of magnitude at magma-ocean temperatures. The flux must remain positive (outward), and bounded to the 1e2 to 1e9 W/m2 regime. **How we know the right answer.** The grey-body Stefan-Boltzmann law provides an exact analytical reference for thermal radiation, which governs ideal emitters. An exponent slip to third power changes the flux by orders of magnitude, which easily violates the tolerance of rel 1e-6. **Why it matters.** The atmosphere module computes the surface energy balance, which drives planetary mantle cooling. PROTEUS reads the surface flux `Fatm` as `F_int`, and surface temperature `temperature_surface` as `T_magma`, to couple the interior to the atmosphere. -**Known limit.** The test uses its own copy of the value in `constants.c`, sigma = 5.670367e-8 W m-2 K-4. A change of the constant in the code larger than the tolerance fails the test, but a value that is wrong in both places does not. PROTEUS uses this grey-body branch only with `surface_bc_mode = 'grey_body'`; its default is a prescribed flux (`SURFACE_BC 4`). +**Known limit.** The test uses its own copy of the value in `constants.c`, sigma = 5.670367e-8 W m-2 K-4. A change of the constant in the code larger than the tolerance fails the test, but a value that is wrong in both places does not. PROTEUS uses this grey-body branch only with `surface_bc_mode = 'grey_body'`; its default is a prescribed flux (the SPIDER option `SURFACE_BC 4`). ![Surface flux of the blackbody50 run at 200 yr on the grey-body law](../assets/validation/atmosphere_grey_body_flux.svg#only-light){ width="80%" } ![Surface flux of the blackbody50 run at 200 yr on the grey-body law](../assets/validation/atmosphere_grey_body_flux_dark.svg#only-dark){ width="80%" } -**How to read the plot.** The x-axis is the surface temperature and the y-axis the surface flux on a log scale. The line is the grey-body law $\epsilon\sigma(T^4 - T_\mathrm{eqm}^4)$, with emissivity $\epsilon$ = 1 and equilibrium temperature $T_\mathrm{eqm}$ = 273 K. Here $\sigma$ is the Stefan-Boltzmann constant, and the dashed line is the $T^3$ form, which the test rejects. The circle is the asserted surface flux Fatm of the blackbody50 run at 200 yr, at its reported surface temperature. Its values are shown to 3 significant digits, because the run output moves by about 1e-6 between builds of the PETSc library. A pass means that the circle lies on the line; the test asserts relative 1e-6 on the unrounded values. +**How to read the plot.** The x-axis is the surface temperature in K and the y-axis the surface flux in W/m$^2$ on a log scale. The line is the grey-body law $\epsilon\sigma(T^4 - T_\mathrm{eqm}^4)$, with emissivity $\epsilon$ = 1 and equilibrium temperature $T_\mathrm{eqm}$ = 273 K. Here $\sigma$ is the Stefan-Boltzmann constant, and the dashed line is the $T^3$ form, which the test rejects. The circle is the asserted surface flux Fatm of the blackbody50 run at 200 yr, at its reported surface temperature. Its values are shown to 3 significant digits, because the run output moves by about 1e-6 between builds of the PETSc library. A pass means that the circle lies on the line; the test asserts relative 1e-6 on the unrounded values. ??? info "Test details" - Reference: Analytical limit: the grey-body flux F = emissivity sigma (T_surf^4 - T_eqm^4) with emissivity 1 and T_eqm = 273 K (Stefan-Boltzmann). diff --git a/docs/Validation/bc.md b/docs/Validation/bc.md index cabe4d79..7c75b3b5 100644 --- a/docs/Validation/bc.md +++ b/docs/Validation/bc.md @@ -2,12 +2,36 @@ ## What this page checks -This page checks the boundary condition implementations in `bc.c`, which control surface and bottom boundaries. +This page checks the boundary condition (BC) implementations in `bc.c`, which control surface and bottom boundaries. The boundary condition routines specify how energy and entropy enter, or leave, the planetary mantle. Validation means that we test boundary condition options against analytical identities, and against prescribed inputs. -If boundary condition dispatch fails, energy conservation is violated at the surface, or at the core-mantle boundary. +If boundary condition dispatch fails, energy conservation is violated at the surface, or at the core-mantle boundary (CMB). The SPIDER options `SURFACE_BC` and `CORE_BC` select the boundary condition at the surface and at the CMB. The sections below detail two validation cases, their tolerances, and their test commands. +## Constant-entropy BC pins the initial surface + +**Why this check exists.** Fixed boundary values can be overwritten by initial rebalancing steps, or by stencil operations. If the surface boundary assignment fails, the mantle profile starts from an uncalibrated thermodynamic state. + +**What it tests.** The test evaluates the constant-entropy boundary option, which writes `ic_surface_entropy` (2550 J/kg/K) onto the surface basic node (the outermost cell boundary). This value is preserved through the steady-state energy solve into the initial output. The test discriminates this setting against the default configuration surface value (2599.7 J/kg/K from the adiabat, the constant-entropy profile, at 2600), which sits about 50 J/kg/K away. Sibling test `test_prescribed_core_flux_slows_cmb_cooling` checks CORE_BC dispatch, CMB entropy difference, and doubled CMB heat flux. + +**How we know the right answer.** Preserving a specified boundary value is an exact identity. Because the value is assigned directly, it matches to rel 1e-10. + +**Why it matters.** Setting fixed surface or core entropies allows controlled numerical experiments and comparisons with simplified interior profiles. + +**Known limit.** For constant-entropy boundary conditions, the surface value is assigned directly rather than determined by a physical flux balance. + +![Initial entropy profiles with the constant-entropy boundary and with the default boundary](../assets/validation/bc_constant_entropy_surface.svg#only-light){ width="80%" } +![Initial entropy profiles with the constant-entropy boundary and with the default boundary](../assets/validation/bc_constant_entropy_surface_dark.svg#only-dark){ width="80%" } + +**How to read the plot.** The x-axis is the radius and the y-axis the entropy at the basic nodes (the cell boundaries of the mesh) at $t$ = 0. The solid line is the asserted profile of the run with a constant-entropy surface boundary. The dashed line is the default run (context); its surface entropy, 2599.7 J kg$^{-1}$ K$^{-1}$, must differ from 2550 by more than 40. The circle is the asserted surface node, and the hollow square the value 2550 J kg$^{-1}$ K$^{-1}$ of the option `ic_surface_entropy`. A pass means that the circle sits in the square (relative 1e-10) and the deep end of the solid line lies between 2400 and 2700. + +??? info "Test details" + - Reference: Analytical identity: the constant-entropy boundary writes ic_surface_entropy (2550 J/kg/K) onto the surface basic node, preserved through the steady-state energy solve into the initial output. + - Tolerance: rel 1e-10 (assigned, not solved); the core-side entropy between 2400 and 2700 J/kg/K. + - Test id: `tests/test_bc.py::test_constant_entropy_bc_pins_the_initial_surface` + - Command: `make`, then `pytest -o addopts= tests/test_bc.py::test_constant_entropy_bc_pins_the_initial_surface` (add `--validation-plots` to write the plot pair) + - Companion test: `tests/test_bc.py::test_prescribed_core_flux_slows_cmb_cooling` verifies that core heat flux slows cooling at the core-mantle boundary. + ## Prescribed-flux BC echoes value and inverts emissivity **Why this check exists.** With a prescribed surface flux, the reported flux must equal the prescribed value and the emissivity must be its grey-body inversion. A wrong boundary-condition (BC) branch or a wrong inversion gives a wrong reported flux or emissivity without an error message. @@ -32,30 +56,6 @@ The sections below detail two validation cases, their tolerances, and their test - Command: `make`, then `pytest -o addopts= tests/test_bc.py::test_prescribed_flux_bc_echoes_value_and_inverts_emissivity` (add `--validation-plots` to write the plot pair) - Companion test: `tests/test_bc.py::test_mantle_cools_from_the_top_between_surface_and_cmb` verifies that the core-mantle boundary remains hotter than the surface. -## Constant-entropy BC pins the initial surface - -**Why this check exists.** Fixed boundary values can be overwritten by initial rebalancing steps, or by stencil operations. If the surface boundary assignment fails, the mantle profile starts from an uncalibrated thermodynamic state. - -**What it tests.** The test evaluates the constant-entropy boundary option, which writes `ic_surface_entropy` (2550 J/kg/K) onto the surface basic node. This value is preserved through the steady-state energy solve into the initial output. The test discriminates this setting against the default configuration surface value (2599.7 J/kg/K from the 2600 adiabat), which sits about 50 J/kg/K away. Sibling test `test_prescribed_core_flux_slows_cmb_cooling` checks CORE_BC dispatch, CMB entropy difference, and doubled CMB heat flux. - -**How we know the right answer.** Preserving a specified boundary value is an exact identity. Because the value is assigned directly, it matches to rel 1e-10. - -**Why it matters.** Setting fixed surface or core entropies allows controlled numerical experiments and comparisons with simplified interior profiles. - -**Known limit.** For constant-entropy boundary conditions, the surface value is assigned directly rather than determined by a physical flux balance. - -![Initial entropy profiles with the constant-entropy boundary and with the default boundary](../assets/validation/bc_constant_entropy_surface.svg#only-light){ width="80%" } -![Initial entropy profiles with the constant-entropy boundary and with the default boundary](../assets/validation/bc_constant_entropy_surface_dark.svg#only-dark){ width="80%" } - -**How to read the plot.** The x-axis is the radius and the y-axis the entropy at the basic nodes (the cell boundaries of the mesh) at $t$ = 0. The solid line is the asserted profile of the run with a constant-entropy surface boundary. The dashed line is the default run (context); its surface entropy, 2599.7 J kg$^{-1}$ K$^{-1}$, must differ from 2550 by more than 40. The circle is the asserted surface node, and the hollow square the value 2550 J kg$^{-1}$ K$^{-1}$ of the option `ic_surface_entropy`. A pass means that the circle sits in the square (relative 1e-10) and the deep end of the solid line lies between 2400 and 2700. - -??? info "Test details" - - Reference: Analytical identity: the constant-entropy boundary writes ic_surface_entropy (2550 J/kg/K) onto the surface basic node, preserved through the steady-state energy solve into the initial output. - - Tolerance: rel 1e-10 (assigned, not solved); the core-side entropy between 2400 and 2700 J/kg/K. - - Test id: `tests/test_bc.py::test_constant_entropy_bc_pins_the_initial_surface` - - Command: `make`, then `pytest -o addopts= tests/test_bc.py::test_constant_entropy_bc_pins_the_initial_surface` (add `--validation-plots` to write the plot pair) - - Companion test: `tests/test_bc.py::test_prescribed_core_flux_slows_cmb_cooling` verifies that core heat flux slows cooling at the core-mantle boundary. - ## Related code - `bc.c`: `set_surface_flux_from_atmosphere`, `set_surface_entropy_constant`, `set_cmb_entropy_constant`. diff --git a/docs/Validation/energy.md b/docs/Validation/energy.md index 00f9c119..51f1bcbf 100644 --- a/docs/Validation/energy.md +++ b/docs/Validation/energy.md @@ -3,8 +3,8 @@ ## What this page checks This page checks energy flux assembly and internal heating routines in `energy.c`, which control mantle energy transport. -The energy module calculates convective, conductive, and diffusive fluxes, alongside radiogenic and tidal heating. -Validation means that we test flux assembly against analytical superposition limits, decay laws, and steady balances. +The energy module calculates 4 heat fluxes, alongside radiogenic and tidal heating. Jcond is conduction, and Jconv is convection, driven by the entropy gradient. Jmix is mixing of melt and solid, active only where both phases exist. Jgrav is the separation of melt from solid under gravity. +Validation means that we test flux assembly against analytical superposition limits, decay laws, and the balance of the steady-state initial condition (IC). If flux calculation or source terms are wrong, mantle cooling rates and core heat extraction are wrong. The sections below detail three validation cases, their tolerances, and their test commands. @@ -12,7 +12,7 @@ The sections below detail three validation cases, their tolerances, and their te **Why this check exists.** Runtime configuration flags can fail to disconnect disabled flux routines, or leak residual values into total energy balances. A failure in flux superposition produces incorrect thermal gradients without raising an error. -**What it tests.** The test evaluates the flux superposition architecture when specific transport terms are switched off. A disabled transport mechanism must hold zero flux, within abs 1e-20 W/m^2, which is at least 17 decades below the observed active fluxes. With conduction off, convection gives 9e8 to 2e9 W/m^2 (observed; the test asserts more than 1e3). In a conduction-only run, conduction gives 1.9e-3 to 1.3e-2 W/m^2 (observed). Total flux equals conductive flux to rel 1e-12 at every interior node. The surface node differs by more than 1e3 W/m^2. +**What it tests.** The test evaluates the flux superposition architecture when specific transport terms are switched off. A disabled transport mechanism must hold zero flux at every basic node (cell boundary), within abs 1e-20 W/m^2, which is at least 17 decades below the observed active fluxes. With conduction off, convection gives 9e8 to 2e9 W/m^2 (observed; the test asserts more than 1e3). In a conduction-only run, conduction gives 1.9e-3 to 1.3e-2 W/m^2 (observed). Total flux equals conductive flux to rel 1e-12 at every interior node. The surface node differs by more than 1e3 W/m^2. **How we know the right answer.** Superposition of disjoint flux mechanisms is an analytical identity: an inactive transport term must vanish to rounding precision, and in the conduction-only run the total flux must equal the conductive flux. The magnitude of the convective flux is not compared with a reference. @@ -36,7 +36,7 @@ The sections below detail three validation cases, their tolerances, and their te **Why this check exists.** Radioactive decay models can apply incorrect decay constants, confuse isotope abundances, or miscalculate exponential time evolution. A bug in heating terms leads to wrong internal heat production over geological timescales. -**What it tests.** The test evaluates radiogenic heating as the sum of concentration times abundance times heat production over isotopes, decaying as 2^(-(t - t0)/half_life). A two-isotope test configuration with 100-year and 200-year half-lives pins the sum at t = 0, and checks the weighted decay factor 0.2506 after 200 years. A dropped isotope shifts the initial sum by 2.4e-3 relative. The two-isotope factor is discriminated from single-isotope values 0.25 and 0.5, and from the no-decay value 1.0. +**What it tests.** The test evaluates radiogenic heating as the sum of concentration times abundance times heat production over isotopes, decaying as 2^(-(t - t0)/half_life), with t0 the start time of each isotope (0 here). A two-isotope test configuration with 100-year and 200-year half-lives pins the sum at t = 0, and checks the weighted decay factor 0.2506 after 200 years. A dropped isotope shifts the initial sum by 2.4e-3 relative. The two-isotope factor is discriminated from the single-isotope values: 0.25 for al26 alone (2 half-lives in 200 years) and 0.5 for k40 alone (1 half-life). It also differs from the no-decay value 1.0. **How we know the right answer.** Radioactive decay follows the analytical exponential decay law. The initial sum is checked to rel 1e-6, and the combined decay factor matches to rel 1e-4. @@ -47,7 +47,7 @@ The sections below detail three validation cases, their tolerances, and their te ![Radiogenic heating of the 2-isotope run against time](../assets/validation/energy_radiogenic_decay.svg#only-light){ width="80%" } ![Radiogenic heating of the 2-isotope run against time](../assets/validation/energy_radiogenic_decay_dark.svg#only-dark){ width="80%" } -**How to read the plot.** The x-axis is the time and the y-axis the specific radiogenic heating on a log scale. The wide pale line is the closed-form sum of the 2 isotopes. The dashed and dotted lines are the isotopes `al26` and `k40` alone, with test half-lives of 100 and 200 yr (context). The dashed line runs inside the wide line, because `al26` gives more than 99 % of the heating. The circles are the asserted Hradio at 0 and 200 yr: the value at 0 to relative 1e-6, and their ratio, about 0.2506, to 1e-4. A pass means that both circles lie on the wide line. +**How to read the plot.** The x-axis is the time in yr and the y-axis the specific radiogenic heating in W/kg on a log scale. The wide pale line is the closed-form sum of the 2 isotopes. The dashed and dotted lines are the isotopes `al26` and `k40` alone, with test half-lives of 100 and 200 yr (context). The dashed line runs inside the wide line, because `al26` gives more than 99 % of the heating. The circles are the asserted Hradio at 0 and 200 yr: the value at 0 to relative 1e-6, and their ratio, about 0.2506, to 1e-4. A pass means that both circles lie on the wide line. ??? info "Test details" - Reference: Analytical limit: radiogenic heating is the sum over isotopes of concentration times abundance times heat production, decaying as 2^(-(t - t0)/half_life). A two-isotope configuration with 100-year and 200-year half-lives pins the sum at t = 0 and the weighted decay factor 0.2506 after 200 years. @@ -60,9 +60,9 @@ The sections below detail three validation cases, their tolerances, and their te **Why this check exists.** Steady-state solvers can terminate at false equilibria, or fail to balance heat flow across layers. An uncalibrated initial profile introduces unphysical thermal transients at the start of a simulation. -**What it tests.** The test evaluates the steady-state initial condition, which equalises energy flow through every basic node, and closes the surface radiation balance sigma (T_surf^4 - teqm^4) with the Stefan-Boltzmann constant of constants.c. A T^3 exponent slip misses the surface flux by roughly a factor of the surface temperature (about 2000 here). +**What it tests.** The test evaluates the steady-state initial condition of the blackbody50 run, which has no internal heating. It equalises the energy flow Etot (W) through every basic node, and closes the surface radiation balance $\sigma(T_\mathrm{surf}^4 - T_\mathrm{eqm}^4)$. Here $\sigma$ is the Stefan-Boltzmann constant of constants.c and $T_\mathrm{eqm}$ = 273 K the equilibrium temperature. A $T^3$ exponent slip changes the surface flux by a factor equal to the surface temperature in kelvin, about 2000 here (about 1960 K). -**How we know the right answer.** Conservation of energy in steady state requires equal energy flow through every mantle shell, and balanced radiation at the surface. Energy-flow relative spread remains below 1e-8 (observed 2e-11), and the grey-body balance matches to rel 1e-5. +**How we know the right answer.** In this configuration without internal heating, conservation of energy in steady state requires equal energy flow through every mantle shell, and balanced radiation at the surface. Energy-flow relative spread remains below 1e-8 (observed 2e-11), and the grey-body balance matches to rel 1e-5. **Why it matters.** The steady-state initial condition starts a run from a profile in energy balance. diff --git a/docs/Validation/eos.md b/docs/Validation/eos.md index 28f6b3df..10d64f3d 100644 --- a/docs/Validation/eos.md +++ b/docs/Validation/eos.md @@ -4,7 +4,7 @@ This page checks the equation of state interface in `eos.c`, which coordinates material property evaluations. `eos.c` manages common option parsing for equations of state, loads phase boundaries, and computes composition-dependent viscosity. -Validation means that we compare boundary interpolations and viscosity shifts against independent implementations and published formulas. +Validation means that we compare boundary interpolations and viscosity shifts against independent implementations and closed-form formulas. If phase boundaries or viscosity calculations are wrong, the melt fraction and convection velocity in the mantle are incorrect. The sections below detail two validation cases, their tolerances, and their test commands. @@ -12,7 +12,7 @@ The sections below detail two validation cases, their tolerances, and their test **Why this check exists.** Table reading routines can swap solidus and liquidus arrays, misplace table indices, or apply incorrect unit conversions. A wrong phase boundary misidentifies the melting state of the mantle without an error message. -**What it tests.** The test evaluates the loaded melt phase boundary against an independent read of `liquidus_A11_H13.dat` (Andrault et al. 2011 liquidus with the Hirschmann 2013 solidus companion). It checks interpolation at the P = 0 table edge, a mid-mantle node, and an off-node deep point. The solidus value at 10 GPa (about 1524 J/kg/K) sits far from the pinned liquidus value (about 2128 J/kg/K), guarding against swapped boundaries. Sibling test `test_viscosity_and_conductivity_dispatch_per_phase` pins per-phase transport dispatch with a 19-decade melt to solid contrast. +**What it tests.** The phase boundaries are the liquidus, the entropy above which the mantle is fully molten, and the solidus, below which it is fully solid. The test evaluates the loaded liquidus against an independent read of `liquidus_A11_H13.dat` (Andrault et al. 2011 liquidus with the Hirschmann 2013 solidus companion). It checks interpolation at the P = 0 table edge, a mid-mantle node, and an off-node deep point. The solidus entropy at 10 GPa (about 1524 J/kg/K) sits far from the pinned liquidus entropy (about 2128 J/kg/K), guarding against swapped boundaries. Sibling test `test_viscosity_and_conductivity_dispatch_per_phase` pins per-phase transport dispatch with a 19-decade melt to solid contrast. **How we know the right answer.** The independent reader evaluates the raw table file using Python numpy interpolation. The C interpolation reproduces these reference values to rel 1e-12. @@ -23,7 +23,7 @@ The sections below detail two validation cases, their tolerances, and their test ![Liquidus entropy against pressure from the table and from eos.c at 3 probes](../assets/validation/eos_phase_boundary.svg#only-light){ width="80%" } ![Liquidus entropy against pressure from the table and from eos.c at 3 probes](../assets/validation/eos_phase_boundary_dark.svg#only-dark){ width="80%" } -**How to read the plot.** The x-axis is the pressure and the y-axis the entropy of the liquidus, the melt phase boundary. The line is the liquidus table `liquidus_A11_H13.dat` read with numpy (every 10th node, context). The circles are the asserted values of eos.c at 3 probes. These are the table edge at 0 GPa, a table node at 10 GPa, and an off-node point at 65.5 GPa. The cross is the solidus at 10 GPa, 1524.43 J kg$^{-1}$ K$^{-1}$, which must differ from the code value by more than 100. A pass means that the circles lie on the line (relative 1e-12). +**How to read the plot.** The x-axis is the pressure in GPa and the y-axis the entropy of the liquidus in J/kg/K. The line is the liquidus table `liquidus_A11_H13.dat` read with numpy (every 10th node, context). The circles are the asserted values of eos.c at 3 probes. These are the table edge at 0 GPa, a table node at 10 GPa, and an off-node point at 65.5 GPa. The cross is the solidus at 10 GPa, 1524.43 J kg$^{-1}$ K$^{-1}$, which must differ from the code value by more than 100. A pass means that the circles lie on the line (relative 1e-12). ??? info "Test details" - Reference: Cross-implementation cross-check of the loaded melt phase boundary against an independent read of liquidus_A11_H13.dat (Andrault et al. 2011 liquidus with the Hirschmann 2013 solidus companion). @@ -36,9 +36,9 @@ The sections below detail two validation cases, their tolerances, and their test **Why this check exists.** Piecewise empirical formulas can suffer from transcription typos in branch coefficients, incorrect branch selection, or missing signs. An incorrect viscosity prefactor alters convective vigour by orders of magnitude. -**What it tests.** The test evaluates the piecewise Mg/Si log10 viscosity prefactor of Spaargaren et al. (2020). It re-encodes the published coefficients independently, and pins two branch-pair shifts as hand-derived literals (0.2074 and -1.35803). The test exercises two configurations that shift in opposite directions across more than 1.5 decades, covering all four piecewise branches. Sibling test `test_activation_terms_shape_the_viscosity_profile` pins the zero-pressure limit and pressure-scale damping direction. +**What it tests.** The test evaluates the piecewise Mg/Si log10 viscosity prefactor of Spaargaren et al. (2020). It re-encodes the coefficients independently of `eos.c`, and pins two branch-pair shifts as hand-derived literals (0.2074 and -1.35803). The comment in `eos.c` says the expressions were worked out in an MSc thesis (2018) and are explained in Spaargaren et al. (2020). The test exercises two configurations that shift in opposite directions across more than 1.5 decades, covering all four piecewise branches. Sibling test `test_activation_terms_shape_the_viscosity_profile` checks the pressure and temperature dependence: no shift at P = 0, a positive shift at depth, its damping by the pressure scale, and the sign of the temperature term. -**How we know the right answer.** Spaargaren et al. (2020) define the formula analytically. The shifts match option arithmetic to abs 1e-9, and the hand-derived literal transcription pins match to abs 1e-4. +**How we know the right answer.** The prefactor is a piecewise closed form in Mg/Si. The shifts match option arithmetic to abs 1e-9, and the hand-derived literal transcription pins match to abs 1e-4. **Why it matters.** Compositional controls on mantle viscosity determine how mantle convective vigor responds to planetary bulk composition. @@ -50,11 +50,11 @@ The sections below detail two validation cases, their tolerances, and their test **How to read the plot.** The x-axis is the Mg/Si ratio and the y-axis the log10 viscosity prefactor. The line is the piecewise prefactor of Spaargaren et al. (2020), as the test re-encodes it (context). The hollow squares are its values at the 2 reference compositions, 1.6 and 0.9. The circles are each reference value plus the asserted shift of eos.c (melt log10 viscosity minus the configured 2.0), at 1.4 and 1.2. A pass means that the circles lie on the line (absolute 1e-9), the one at 1.4 above its square and the one at 1.2 below. ??? info "Test details" - - Reference: The piecewise Mg/Si log10 viscosity prefactor of Spaargaren et al. (2020). The test re-encodes the published coefficients independently of eos.c and additionally pins the two branch-pair shifts as hand-derived literals (0.2074 and -1.35803), so a transcription error in either copy of the formula surfaces. + - Reference: The piecewise Mg/Si log10 viscosity prefactor of Spaargaren et al. (2020). The test re-encodes the coefficients independently of eos.c and additionally pins the two branch-pair shifts as hand-derived literals (0.2074 and -1.35803), so a transcription error in either copy of the formula surfaces. - Tolerance: abs 1e-9 on the evaluated shifts (closed-form option arithmetic); abs 1e-4 on the literal transcription pins. - Test id: `tests/test_eos.py::test_compositional_viscosity_matches_spaargaren` - Command: `make tests_c`, then `pytest -o addopts= tests/test_eos.py::test_compositional_viscosity_matches_spaargaren` (add `--validation-plots` to write the plot pair) - - Companion test: `tests/test_eos.py::test_activation_terms_shape_the_viscosity_profile` checks activation volume and energy scalings in the viscosity model. + - Companion test: `tests/test_eos.py::test_activation_terms_shape_the_viscosity_profile` checks the pressure and temperature dependence of the viscosity (no shift at P = 0, a positive shift at depth, damping by the pressure scale, and the sign of the temperature term). ## Related code diff --git a/docs/Validation/eos_adamswilliamson.md b/docs/Validation/eos_adamswilliamson.md index 04bd58a5..e17b8dde 100644 --- a/docs/Validation/eos_adamswilliamson.md +++ b/docs/Validation/eos_adamswilliamson.md @@ -12,7 +12,7 @@ The text below details the check, its tolerance, and its test command. **Why this check exists.** Analytical codes can suffer from sign errors in compressibility, or from wrong fit inputs. A bug in density calculations distorts the pressure field and mesh geometry without an error. -**What it tests.** The test evaluates the formula rho(P) = rhos - P beta / g, using parameters fitted to the PREM lower mantle for blackbody50. It checks density values at the surface (P = 0), at 10 GPa, and at the core-mantle boundary pressure. A sign-flipped compressibility shifts the 10 GPa density by more than 200 kg/m3, which the test guards against. Sibling test `test_si_density_invariant_under_nondimensionalisation_choice` confirms that physical SI density outputs stay unchanged under the nondimensionalisation choice. +**What it tests.** The model has 3 parameters: the surface density $\rho_s$ (kg/m$^3$), a compressibility parameter $\beta$ (1/m) and the gravity $g$ (m/s$^2$, negative by the SPIDER sign convention). The test evaluates the density $\rho(P) = \rho_s - P\beta/g$ at pressure $P$, with the values of the blackbody50 run (the standard 50-node test run, `tests/opts/blackbody50.opts`), fitted to the lower mantle of the Preliminary Reference Earth Model (PREM). It checks the density at the surface (P = 0), at 10 GPa, and at about 135 GPa, the core-mantle boundary pressure. A sign-flipped $\beta$ gives 3967.8 instead of 4190.1 kg/m$^3$ at 10 GPa; the test asserts a gap above 100 kg/m$^3$. Sibling test `test_si_density_invariant_under_nondimensionalisation_choice` confirms that physical SI density outputs stay unchanged under the nondimensionalisation choice. **How we know the right answer.** The Adams-Williamson formula provides an exact closed-form density profile. Numerical values in C agree with the analytical profile to rel 1e-12, reducing at the surface to the exact configured surface density. @@ -23,7 +23,7 @@ The text below details the check, its tolerance, and its test command. ![Adams-Williamson density against pressure from the closed form and from the code](../assets/validation/eos_adamswilliamson_density.svg#only-light){ width="80%" } ![Adams-Williamson density against pressure from the closed form and from the code](../assets/validation/eos_adamswilliamson_density_dark.svg#only-dark){ width="80%" } -**How to read the plot.** The x-axis is the pressure and the y-axis the density. The line is the closed form $\rho_s - P\beta/g$, with surface density $\rho_s$, compressibility parameter $\beta$ and gravity $g$. The dashed line is the form with a sign-flipped $\beta$, which the test rejects (context). The circles are the asserted densities of eos_adamswilliamson.c at the surface, at 10 GPa and at 135 GPa. A pass means that the circles lie on the line (relative 1e-12). +**How to read the plot.** The x-axis is the pressure in GPa and the y-axis the density in kg/m$^3$. The line is the closed form $\rho_s - P\beta/g$, with surface density $\rho_s$, compressibility parameter $\beta$ and gravity $g$. The dashed line is the form with a sign-flipped $\beta$, which the test rejects (context). The circles are the asserted densities of eos_adamswilliamson.c at the surface, at 10 GPa and at 135 GPa. A pass means that the circles lie on the line (relative 1e-12). ??? info "Test details" - Reference: Analytical limit: rho(P) = rhos - P beta / g in closed form, with the blackbody50 PREM lower-mantle fit parameters. diff --git a/docs/Validation/eos_composite.md b/docs/Validation/eos_composite.md index 435f13e8..f7431e57 100644 --- a/docs/Validation/eos_composite.md +++ b/docs/Validation/eos_composite.md @@ -12,7 +12,7 @@ The section below details the validation check, its tolerance, and its test comm **Why this check exists.** Composite blending routines can use wrong denominators, miscalculate melting intervals, or flip boundary bounds. A bug in melt fraction interpolation produces wrong phase mixtures throughout the mantle. -**What it tests.** The test evaluates the analytical limit in the two-phase region: fusion = S_liq - S_sol, and phi = (S - S_sol)/fusion between phase boundaries. It tests the melt fraction at one quarter of the melting interval at 10 GPa. A wrong-denominator regression (using liquidus entropy instead of fusion) shifts phi from 0.25 to about 0.07, which is far beyond tolerance. The fusion entropy lies between 100 and 1500 J/kg/K (603.6 J/kg/K at 10 GPa for the dK09 tables). Sibling tests pin the clamp of the melt fraction between 0 and 1 and the pure-phase limits against single-phase lookups. +**What it tests.** At a given pressure, the mantle is fully solid below the solidus entropy $S_\mathrm{sol}$ and fully molten above the liquidus entropy $S_\mathrm{liq}$ (both in J/kg/K). Between them, the melt fraction $\phi$ runs from 0 to 1, and the entropy of fusion is the width of this interval. The test checks the analytical limit in this two-phase region: fusion = $S_\mathrm{liq} - S_\mathrm{sol}$, and $\phi = (S - S_\mathrm{sol})$/fusion at the specific entropy $S$. It tests the melt fraction at one quarter of the melting interval at 10 GPa. A wrong-denominator regression (using liquidus entropy instead of fusion) shifts phi from 0.25 to about 0.07, which is far beyond tolerance. The fusion entropy lies between 100 and 1500 J/kg/K (603.6 J/kg/K at 10 GPa for the dK09 tables). Sibling tests pin the clamp of the melt fraction between 0 and 1 and the pure-phase limits against single-phase lookups. **How we know the right answer.** By mathematical construction of the linear entropy interpolation, the melt fraction and fusion entropy satisfy exact definitions. The fusion identity matches to rel 1e-10, and the fractional melt position matches to rel 1e-6. diff --git a/docs/Validation/ic.md b/docs/Validation/ic.md index 7bf47453..19dc1f06 100644 --- a/docs/Validation/ic.md +++ b/docs/Validation/ic.md @@ -10,9 +10,9 @@ The sections below detail three validation cases, their tolerances, and their te ## Prescribed adiabat entropy sets the initial profile -**Why this check exists.** Initial condition (IC) routines can misinterpret input entropy parameters or fail to write values to the staggered mesh nodes. A bug in IC setup shifts the starting temperature structure, and the later cooling is wrong too. +**Why this check exists.** Initial condition (IC) routines can misinterpret input entropy parameters or fail to write values to the staggered nodes (the cell centres of the mesh). A bug in IC setup shifts the starting temperature structure, and the later cooling is wrong too. -**What it tests.** The test evaluates the prescribed adiabat entropy (2600 J/kg/K at the top staggered node) with the configured gradient ramp. The profile mean lies between the adiabat entropy and that value plus |ic_dsdr| times mantle depth. A run with ic_adiabat_entropy = 2400 shifts the mean by 200 J/kg/K to rel 1e-9, guarding against ignored option inputs. Entropy is positive, with its mean between 1e3 and 1e4 J/kg/K. Top-node entropy matches at t = 0 to rel 1e-3 (observed exact). Sibling test `test_initial_adiabat_is_nearly_flat_until_cooling_imprints_structure` checks that subsequent cooling imprints thermal structure. +**What it tests.** An adiabat is a profile of constant specific entropy. The test evaluates the prescribed adiabat entropy (2600 J/kg/K at the top staggered node, the top cell centre) with the configured gradient ramp `ic_dsdr`. The profile mean lies between the adiabat entropy and that value plus |ic_dsdr| times mantle depth. A run with ic_adiabat_entropy = 2400 shifts the mean by 200 J/kg/K to rel 1e-9, guarding against ignored option inputs. Entropy is positive, with its mean between 1e3 and 1e4 J/kg/K. Top-node entropy matches at t = 0 to rel 1e-3 (observed exact). Sibling test `test_initial_adiabat_is_nearly_flat_until_cooling_imprints_structure` checks that subsequent cooling imprints thermal structure. **How we know the right answer.** Prescribing an adiabat provides an analytical limit, which defines an idealized reference. The top-node value, and the mean profile shift, match exact option arithmetic. @@ -47,7 +47,7 @@ The sections below detail three validation cases, their tolerances, and their te ![Requested and realised volatile masses and the realised over requested element moles](../assets/validation/ic_abundance_inventory.svg#only-light){ width="80%" } ![Requested and realised volatile masses and the realised over requested element moles](../assets/validation/ic_abundance_inventory_dark.svg#only-dark){ width="80%" } -**How to read the plot.** Left: the grey bars are the requested masses of the 4 volatiles, which the test asserts to equal the stored `initial_kg` (relative 1e-9). The circles are the realised reservoirs after the initial partial-pressure solve, on a log scale. The reactions move mass, so H$_2$ and CO$_2$ end below their bars and H$_2$O and CO above. Right: the realised over the requested moles of H, C and O, with the dashed line at 1. A pass means that H and C sit on the line (relative 1e-9) and O departs from it by more than 1e-4. Here O lies 2.2e-3 above, because the reactions exchange oxygen with the oxygen-fugacity (fO$_2$) buffer of the melt. +**How to read the plot.** Left: the grey bars are the requested masses of the 4 volatiles, which the test asserts to equal the stored `initial_kg` (relative 1e-9). The circles are the realised reservoirs after the initial partial-pressure solve, on a log scale. In this run (not asserted), the reactions move H$_2$ and CO$_2$ below their bars and H$_2$O and CO above. Right: the realised over the requested moles of H, C and O, with the dashed line at 1. A pass means that H and C sit on the line (relative 1e-9) and O departs from it by more than 1e-4. Here O lies 2.2e-3 above, because the reactions exchange oxygen with the oxygen-fugacity (fO$_2$) buffer of the melt. ??? info "Test details" - Reference: Mass-balance identity through the initial partial-pressure solve: initial_kg equals the requested ppm of the mantle mass per volatile, and the realised reservoirs carry the requested hydrogen and carbon mole totals (conserved by the water and carbon dioxide reactions). diff --git a/docs/Validation/index.md b/docs/Validation/index.md index 18105a2b..cb68d804 100644 --- a/docs/Validation/index.md +++ b/docs/Validation/index.md @@ -2,13 +2,19 @@ ## What this page checks -This page introduces the scientific validation suite for SPIDER, which tests each physics module. +This page lists the validation cases of SPIDER: each case checks 1 physics source file against a known answer. Validation means that we compare model outputs against analytical limits and independent implementations. Each physics source file has at least one pinned test, with a stated tolerance and reference value. -If these components are wrong, the interior thermal evolution that SPIDER computes, alone or inside PROTEUS, is wrong. +If these components are wrong, the interior thermal evolution that SPIDER computes, alone or inside [PROTEUS](https://proteus-framework.org/PROTEUS), is wrong. PROTEUS is the coupling framework that runs SPIDER as its interior module. The table below lists each validation case, organized by physical process. A full-run regression against the frozen blackbody50 reference complements these cases (`tests/test_regression.py`). +Terms used on every page: + +- **blackbody50**: the standard 50-node test run of SPIDER, set by `tests/opts/blackbody50.opts`, with a grey-body surface (`SURFACE_BC 1`) and no internal heating. +- **Basic nodes** and **staggered nodes**: the 2 sets of points of the radial mesh. Basic nodes are the cell boundaries, staggered nodes the cell centres. +- **IC** and **BC**: initial condition and boundary condition. + Tests in the `unit` tier drive the small C test executables in `tests/c` (`make tests_c`); tests in the `smoke` tier run the `spider` binary (`make`) on short configurations. See [Testing SPIDER](../How-to/test.md) for the tiers and the commands. ## Cases by process diff --git a/docs/Validation/matprop.md b/docs/Validation/matprop.md index 03ee55fe..a627f3aa 100644 --- a/docs/Validation/matprop.md +++ b/docs/Validation/matprop.md @@ -12,7 +12,7 @@ The text below details the check, its tolerance, and its test command. **Why this check exists.** Rheological blending functions can swap the melt and solid limits. A bug in property blending causes the convection solver to use wrong viscosities. -**What it tests.** The test evaluates the analytical limit where blended viscosity recovers configured end-members away from the rheological transition (melt_log10visc = 2, solid_log10visc = 21). It checks two extreme states: the hot initial state of the blackbody50 run (t = 0, where all 50 nodes are molten), and a cold 0-step run started from a 1600 J/kg/K adiabat (where at least 5 nodes are solid). The recovered end-members must differ by more than 10 decades, guarding against swapped phases. Sibling test `test_material_properties_positive_and_conductivity_constant` asserts positive values for density, heat capacity and thermal expansion, and a constant conductivity, alongside configured 4.0 W/m/K conductivity. +**What it tests.** The test evaluates the analytical limit where blended viscosity recovers configured end-members away from the rheological transition (melt_log10visc = 2, solid_log10visc = 21). It checks two extreme states: the hot initial state of the blackbody50 run (the standard 50-node test run, `tests/opts/blackbody50.opts`) at t = 0 ( where all 50 nodes are molten), and a cold 0-step run started from a 1600 J/kg/K adiabat (where at least 5 nodes are solid). The recovered end-members must differ by more than 10 decades, guarding against swapped phases. Sibling test `test_material_properties_positive_and_conductivity_constant` asserts positive values for density, heat capacity and thermal expansion, and a constant conductivity, alongside configured 4.0 W/m/K conductivity. **How we know the right answer.** Away from the partial-melting interval, blended material properties must approach pure-phase limits asymptotically. Viscosity matches configured values to abs 0.01 on log10(viscosity) at molten nodes and abs 0.05 at solid nodes. diff --git a/docs/Validation/mesh.md b/docs/Validation/mesh.md index f71ac456..f03a8b79 100644 --- a/docs/Validation/mesh.md +++ b/docs/Validation/mesh.md @@ -3,7 +3,7 @@ ## What this page checks This page checks static radial mesh and pressure profile construction in `mesh.c`. -The mesh module sets up basic and staggered radial nodes, shell areas, shell volumes, and gravitational pressures. +The mesh module sets up the basic nodes (cell boundaries) and staggered nodes (cell centres) of the radial mesh, shell areas, shell volumes, and gravitational pressures. Validation means that we test the native pressure profile against the closed-form Adams-Williamson solution. If mesh construction fails, shell geometry and pressure fields in the mantle are distorted. The section below details the validation check, its tolerance, and its test command. @@ -12,7 +12,7 @@ The section below details the validation check, its tolerance, and its test comm **Why this check exists.** Radial grid generation can suffer from sign errors in the hydrostatic integral or a wrong surface radius. An error in mesh setup makes the pressure and volume arrays of every physics module wrong. -**What it tests.** The test evaluates the native Adams-Williamson pressure profile P(r) = -(rhos g / beta)(exp(beta (R - r)) - 1) in closed form. It checks the basic-node pressures through the mantle. A sign-flipped beta and the linear small-beta approximation both fall far outside tolerance, confirming proper discrimination. Sibling test `test_mesh_orderings_interleave_and_shell_masses_close` checks geometric node interleaving and shell mass conservation. Sibling test `test_external_linear_density_mesh_replaces_the_aw_profile` confirms that external meshes replace native profiles cleanly. +**What it tests.** The profile uses the planet radius $R$ = 6371 km, the radius $r$, the surface density $\rho_s$ (kg/m$^3$), the gravity $g$ (m/s$^2$, negative by the SPIDER sign convention) and a compressibility parameter $\beta$ (1/m). The test evaluates the native Adams-Williamson pressure $P(r) = -(\rho_s g/\beta)(\exp(\beta(R - r)) - 1)$ in closed form. It checks the pressures at the basic nodes through the mantle. A sign-flipped beta and the linear small-beta approximation both fall far outside tolerance, confirming proper discrimination. Sibling test `test_mesh_orderings_interleave_and_shell_masses_close` checks geometric node interleaving and shell mass conservation. Sibling test `test_external_linear_density_mesh_replaces_the_aw_profile` confirms that external meshes replace native profiles cleanly. **How we know the right answer.** Under the Adams-Williamson assumptions, the hydrostatic equation integrates to an exact analytical expression. Computed pressures agree with the closed form to rel 1e-12 (observed 6e-15, limited by precision in JSON serialization). @@ -23,7 +23,7 @@ The section below details the validation check, its tolerance, and its test comm ![Basic-node pressure against radius on the Adams-Williamson closed form](../assets/validation/mesh_pressure_profile.svg#only-light){ width="80%" } ![Basic-node pressure against radius on the Adams-Williamson closed form](../assets/validation/mesh_pressure_profile_dark.svg#only-dark){ width="80%" } -**How to read the plot.** The x-axis is the radius and the y-axis the pressure. The line is the Adams-Williamson closed form (context). The dashed and dotted lines are the constant-density profile and the form with a sign-flipped compressibility parameter $\beta$. The test rejects both at the core-mantle boundary. The circles are the asserted pressures of mesh.c at the basic nodes (the cell boundaries of the mesh). A pass means that the circles lie on the line (relative 1e-12). +**How to read the plot.** The x-axis is the radius in km and the y-axis the pressure in GPa. The line is the Adams-Williamson closed form (context). The dashed and dotted lines are the constant-density profile and the form with a sign-flipped compressibility parameter $\beta$. The test rejects both at the core-mantle boundary. The circles are the asserted pressures of mesh.c at the basic nodes (the cell boundaries of the mesh). A pass means that the circles lie on the line (relative 1e-12). ??? info "Test details" - Reference: Analytical limit: the Adams-Williamson pressure profile P(r) = -(rhos g / beta)(exp(beta (R - r)) - 1) in closed form. diff --git a/docs/Validation/rheologicalfront.md b/docs/Validation/rheologicalfront.md index fce8c7aa..263a4799 100644 --- a/docs/Validation/rheologicalfront.md +++ b/docs/Validation/rheologicalfront.md @@ -4,7 +4,7 @@ This page checks front tracking in `rheologicalfront.c`, which locates the solidification boundary. `rheologicalfront.c` detects the critical melt fraction threshold where mantle rheology shifts from liquid-like to solid-like behavior. -Validation means that we check reported front indices and depths against independent crossings of the melt fraction profile. +Validation means that we check the reported front index and depth against the crossing of the melt-fraction profile that the test finds with the same scan, a self-consistency check. If front tracking fails, the dynamic boundary between convective magma ocean and solid mantle is misplaced. The section below details the validation check, its tolerance, and its test command. @@ -23,7 +23,7 @@ The section below details the validation check, its tolerance, and its test comm ![Radii of the 2 nodes on each side of the critical melt fraction and of the reported front at 1200 yr](../assets/validation/rheologicalfront_location.svg#only-light){ width="80%" } ![Radii of the 2 nodes on each side of the critical melt fraction and of the reported front at 1200 yr](../assets/validation/rheologicalfront_location_dark.svg#only-dark){ width="80%" } -**How to read the plot.** The y-axis is the radius, shown to 3 significant digits. The squares are the 2 staggered nodes (cell centres) at 1200 yr on each side of the critical melt fraction $\phi_c = 0.4$ (0.403 and 0.3995). The circle is the asserted front radius, the planet radius minus the reported depth. The test asserts the reported depth and pressure against the basic-node arrays to relative 1e-6. The melt fractions are not drawn: the first node is only 4.5e-4 below $\phi_c$, so a rounded value is not stable between builds. A pass means that the front node is at most 1 node from the first node below $\phi_c$; here the circle lies between the squares. +**How to read the plot.** The y-axis is the radius, shown to 3 significant digits. The squares are the 2 staggered nodes (cell centres) at 1200 yr on each side of the critical melt fraction $\phi_c = 0.4$ (0.403 and 0.3995). The circle is the asserted front radius, the planet radius minus the reported depth. The test asserts the reported depth and pressure against the arrays at the basic nodes (cell boundaries) to relative 1e-6. The melt fractions are not drawn: the first node is only 4.5e-4 below $\phi_c$, so a rounded value is not stable between builds. A pass means that the front node is at most 1 node from the first node below $\phi_c$; here the circle lies between the squares. ??? info "Test details" - Reference: Analytical self-consistency: the reported front index matches the phi_critical = 0.4 crossing of the melt-fraction profile, and the depth equals the surface radius minus the front radius. diff --git a/docs/Validation/rhs.md b/docs/Validation/rhs.md index e1d71e7b..0f8500b7 100644 --- a/docs/Validation/rhs.md +++ b/docs/Validation/rhs.md @@ -3,7 +3,7 @@ ## What this page checks This page checks the right-hand side time-derivative assembly in `rhs.c`, which governs thermal evolution. -`rhs.c` compiles flux divergences and internal heating source terms into local entropy tendencies dS/dt. +`rhs.c` compiles flux divergences and internal heating source terms into the rate of change dS/dt of the specific entropy S (J/kg/K) at each node. This right-hand side is the function that the time stepper integrates. Validation means that we check reported time derivatives against finite-difference slopes between consecutive output snapshots. If right-hand side evaluation is incorrect, the numerical timestepper integrates along an erroneous trajectory. The section below details the validation check, its tolerance, and its test command. @@ -12,18 +12,18 @@ The section below details the validation check, its tolerance, and its test comm **Why this check exists.** The time derivative that SPIDER reports must be the one the time stepper integrates. A unit or scaling slip in the reported value, or a gross sign flip, makes the reported rate disagree with the trajectory without an error message. -**What it tests.** The test evaluates the reported dS/dt against the finite-difference slope of consecutive outputs over 10-year macro steps. It asserts cooling signs at each node, and bounds the largest |dS/dt| between 1e-12 and 1e-4 J/kg/K/s. This bound rejects unscaled annual rates, while finite differencing catches nondimensional values (around 3e-5 here). Sibling test `test_cooling_sign_structure_and_finiteness` checks that radiogenic and tidal source arrays remain zero and that entropy declines smoothly in the upper molten layer. +**What it tests.** A macro step is the interval between 2 outputs; here it is 10 years. The test evaluates the reported dS/dt of the blackbody50 run (the standard 50-node test run, `tests/opts/blackbody50.opts`) against the finite-difference slope of the outputs at 10 and 20 yr. At each node, it asserts cooling (dS/dt < 0). It also bounds the largest |dS/dt| between 1e-12 and 1e-4 J/kg/K/s. The run gives about 2e-8 J/kg/K/s; the same rate, per year instead of per second, is about 0.7 and fails the upper bound. Sibling test `test_cooling_sign_structure_and_finiteness` checks that radiogenic and tidal source arrays remain zero and that entropy declines smoothly in the upper molten layer. -**How we know the right answer.** In the limit of small timesteps, the discrete difference between output snapshots approaches the instantaneous time derivative. The reported derivative matches the finite-difference rate to rel 2e-2 at staggered nodes (observed at most 0.65 percent). +**How we know the right answer.** In the limit of small timesteps, the discrete difference between output snapshots approaches the instantaneous time derivative. The test asserts that the reported derivative matches the finite-difference rate to rel 2e-2 at the staggered nodes (cell centres). The observed difference is 0.43 to 0.65 percent, the truncation error of the 10-year slope. **Why it matters.** The right-hand side assembly provides the core time derivative that PETSc integrates to advance mantle thermal evolution in SPIDER. +**Known limit.** The trajectory comes from the same right-hand side that reports dS/dt, so a wrong right-hand side gives a consistent wrong trajectory and passes; dS/dt is not compared with an independent physical value. The run has no heating, so a dropped heating term cannot show. Finite differencing over 10-year intervals adds a truncation error of up to 0.65 percent. + ![Largest reported dS/dt magnitude of the run against the asserted window](../assets/validation/rhs_dsdt_finite_difference.svg#only-light){ width="80%" } ![Largest reported dS/dt magnitude of the run against the asserted window](../assets/validation/rhs_dsdt_finite_difference_dark.svg#only-dark){ width="80%" } -**How to read the plot.** The y-axis is the largest cooling rate |dS/dt| of the blackbody50 run, on a log scale. The circle is this asserted value, the mean of the 10 and 20 yr outputs, shown to 1 significant digit. The grey band is the asserted window from 1e-12 to 1e-4 W/kg/K. The node profiles are not drawn, because they move by about 7e-4 (relative) between the macOS and Linux builds. The main comparison, with the slope of the specific entropy S between the outputs to relative 2e-2 at every node, is not drawn either. A pass means that the circle lies in the grey band. - -**Known limit.** The trajectory comes from the same right-hand side that reports dS/dt, so a wrong right-hand side gives a consistent wrong trajectory and passes; dS/dt is not compared with an independent physical value. The run has no heating, so a dropped heating term cannot show. Finite differencing over 10-year intervals adds a truncation error of up to 0.65 percent. +**How to read the plot.** The y-axis is the largest cooling rate |dS/dt| of the run in J/kg/K/s, on a log scale. The circle is this asserted value, the mean of the 10 and 20 yr outputs, shown to 1 significant digit. The grey band is the asserted window from 1e-12 to 1e-4 J/kg/K/s. The node profiles are not drawn, because they move by about 7e-4 (relative) between the macOS and Linux builds. The main comparison, with the slope of the specific entropy S between the outputs to relative 2e-2 at every node, is not drawn either. A pass means that the circle lies in the grey band. ??? info "Test details" - Reference: Analytical self-consistency: the reported dS/dt matches the finite-difference slope of consecutive outputs over 10-year macro steps. diff --git a/docs/Validation/twophase.md b/docs/Validation/twophase.md index bf8ebbe6..efc6a130 100644 --- a/docs/Validation/twophase.md +++ b/docs/Validation/twophase.md @@ -12,7 +12,7 @@ The text below details the check, its tolerance, and its test command. **Why this check exists.** Mass sum routines can omit geometric shell weighting, confuse node centers with edges, or violate mass balance. A bug in mass bookkeeping produces inaccurate global melt inventories during cooling. -**What it tests.** The test evaluates the analytical identity: global melt fraction equals Mliq/Mmantle and the mass-weighted mean of the staggered melt-fraction profile. It checks a part-solidified 1200-year state. An unweighted profile mean differs by more than 1e-3, confirming that the test guards spatial weighting. Global melt fraction must lie between 0 and 1 and, at 1200 yr, between 0.2 and 0.9. Sibling tests verify mantle mass closure Mliq + Msol = Mmantle at every output to rel 1e-12 (`test_mantle_mass_closure_at_every_output_time`), and monotone solidification under cooling (`test_solidification_proceeds_monotonically_under_cooling`). +**What it tests.** The test evaluates the analytical identity: the global melt fraction equals the liquid mass Mliq over the mantle mass Mmantle, and the mass-weighted mean of the melt fraction at the staggered nodes (cell centres). It checks a part-solidified 1200-year state. An unweighted profile mean differs by more than 1e-3, confirming that the test guards spatial weighting. Global melt fraction must lie between 0 and 1 and, at 1200 yr, between 0.2 and 0.9. Sibling tests verify mantle mass closure Mliq + Msol = Mmantle at every output to rel 1e-12 (`test_mantle_mass_closure_at_every_output_time`), and monotone solidification under cooling (`test_solidification_proceeds_monotonically_under_cooling`). **How we know the right answer.** By mass balance, total melt mass divided by mantle mass must identically equal the mass-weighted mean of the radial melt fraction profile. Both identities match to rel 1e-10. diff --git a/docs/assets/validation/rhs_dsdt_finite_difference.svg b/docs/assets/validation/rhs_dsdt_finite_difference.svg index dc0c2630..60d8731b 100644 --- a/docs/assets/validation/rhs_dsdt_finite_difference.svg +++ b/docs/assets/validation/rhs_dsdt_finite_difference.svg @@ -382,8 +382,8 @@ L 320.938706 12.42 - - + + - + - - - - - - - + + + + + + + + + @@ -684,37 +713,6 @@ L 3520 1402 L 1114 1402 L 1114 1862 z -" transform="scale(0.015625)"/> - - - + + - + - - - - - - - + + + + + + + + + @@ -684,37 +713,6 @@ L 3520 1402 L 1114 1402 L 1114 1862 z -" transform="scale(0.015625)"/> -