From 5f25245faac889e0b24f08a4cf34baebbf047234 Mon Sep 17 00:00:00 2001 From: timlichtenberg Date: Fri, 23 Oct 2026 22:59:58 +0200 Subject: [PATCH 01/14] Move the verification pages into a Validation tab The core module verification is docs/Validation/core.md and the first-principles verification docs/Validation/numerics.md, in a Validation tab between Explanations and Reference. The old paths keep a short page that links to the new one, the links of the other pages point to the new paths, and test_core_verification_page.py reads the core page at its new path. --- docs/Explanations/core_bc.md | 4 +- docs/Explanations/core_verification.md | 550 +------------------ docs/Explanations/cvode_jax.md | 2 +- docs/Explanations/jax_jacobian_factory.md | 2 +- docs/Explanations/verification.md | 115 +--- docs/Reference/api/aragog.jax.md | 2 +- docs/Validation/core.md | 551 ++++++++++++++++++++ docs/Validation/numerics.md | 116 +++++ mkdocs.yml | 7 +- tests/test_core_verification_page.py | 2 +- tools/verification/figures/README.md | 2 +- tools/verification/run_core_verification.py | 2 +- 12 files changed, 682 insertions(+), 673 deletions(-) create mode 100644 docs/Validation/core.md create mode 100644 docs/Validation/numerics.md diff --git a/docs/Explanations/core_bc.md b/docs/Explanations/core_bc.md index b10dc07e..1c6d0629 100644 --- a/docs/Explanations/core_bc.md +++ b/docs/Explanations/core_bc.md @@ -215,7 +215,7 @@ The shell must hold the layer and lie above the inner core. The solver refuses a The resolved layer is experimental, and the solver warns once when `stratification = true`. -Against the `leeds_thermal` model, integrated alone with SciPy BDF, it reproduces a layer under a CMB flow below the conducted adiabatic flow, with heat removed, added or neither at the CMB; when the flow rises above that value the two models erode the layer differently (aragog mixes it from the CMB down, `leeds_thermal` removes it in one step of its stability check), and when the flow falls back both form a new layer of similar depth ([verification, section 13](core_verification.md#13-stable-layer-against-thermal_history)); it meets its analytic limits and resumes where it stopped, and its NumPy and JAX right-hand sides match ([section 12](core_verification.md#12-numpy-and-jax-right-hand-sides-and-the-analytic-jacobian)). +Against the `leeds_thermal` model, integrated alone with SciPy BDF, it reproduces a layer under a CMB flow below the conducted adiabatic flow, with heat removed, added or neither at the CMB; when the flow rises above that value the two models erode the layer differently (aragog mixes it from the CMB down, `leeds_thermal` removes it in one step of its stability check), and when the flow falls back both form a new layer of similar depth ([verification, section 13](../Validation/core.md#13-stable-layer-against-thermal_history)); it meets its analytic limits and resumes where it stopped, and its NumPy and JAX right-hand sides match ([section 12](../Validation/core.md#12-numpy-and-jax-right-hand-sides-and-the-analytic-jacobian)). In the solver, an eroding layer stalls CVODE at the default tolerances: the step size collapses where the mixing switches on in the eroding cells. In the SciPy BDF comparison the centre temperature and the inner-core onset have not converged at a relative tolerance of 1e-8, and the comparison runs at 1e-11; the `solver.rtol` that CVODE needs for them is not measured. @@ -230,7 +230,7 @@ Without a layer the smoke tests hold $10^{-6}$: - with Radau at an rtol of $10^{-6}$; - with CVODE at the default rtol for a hot core above the onset; -- with CVODE at an rtol of $10^{-10}$ across the inner-core onset, which closes to $1.5 \times 10^{-6}$ at the default rtol ([verification, section 9](core_verification.md#9-a-cvode-solve-across-the-inner-core-onset)). +- with CVODE at an rtol of $10^{-10}$ across the inner-core onset, which closes to $1.5 \times 10^{-6}$ at the default rtol ([verification, section 9](../Validation/core.md#9-a-cvode-solve-across-the-inner-core-onset)). ## `quasi_steady` diff --git a/docs/Explanations/core_verification.md b/docs/Explanations/core_verification.md index cb101c53..c0b1bacd 100644 --- a/docs/Explanations/core_verification.md +++ b/docs/Explanations/core_verification.md @@ -1,551 +1,3 @@ # Core module verification -This page checks the `core_module` core boundary condition ([core_bc.md](core_bc.md)) and the `aragog.core` model behind it against analytic results, published values, an independent core evolution code and coupled runs. Each section states the physics a check tests, the reference and where it comes from, the value aragog gives, a figure and what it shows, the limits of the check where it has any, and the tests that pin it. A check shows that the code solves the stated equations to the stated accuracy; whether the equations describe a real core is a separate question, which the literature comparisons in sections 5 to 8 and the limits stated with each check address. - -Every number on this page and every figure is produced by `tools/verification/run_core_verification.py`, which writes the figures to `docs/figures/vv/` and the numbers to `docs/figures/vv/core_verification_values.json`; `tests/test_core_verification_page.py` checks each number on the page against that file. The script needs the SPIDER-format EOS tables (`ARAGOG_TEST_EOS_DIR`) for sections 9 and 12 and the optional `verification` dependencies for the figure style, and runs in about eight minutes on one core. Sections 9 and 12 and the solver tests use the aragog test tables that CI uses, `spider_eos_test_data.tar.gz` of release `test-data-v1` (sha256 `e3842bc494969571428d7f978d8f70972d40a3bc5d6ac2c09f6a96808f4079fb`), 500 pressures from 0 to 150 GPa by 200 entropies; their numbers hold for these tables. Other tables give other values; section 12 compares two sets of 1350 pressures by 280 entropies that reach 650 GPa. Two kinds of number come from separate measurements and say so where they appear: the closures of the stratified call heated from above (section 13), held by its smoke test, and the expansivity differences on those two sets. Nimmo (2015) is chapter 8.02 of the *Treatise on Geophysics* wherever chapter 9.08 is not named. - -## 1. Gaussian core structure - -**What is tested.** The core density is a Gaussian in radius, - -$$ -\rho(r) = \rho_\mathrm{cen} \exp\left(-\frac{r^2}{L^2}\right), -$$ - -with $\rho_\mathrm{cen}$ the central density and $L$ the length scale, and the adiabat solves - -$$ -\frac{d \ln T}{dr} = -\frac{\alpha g}{c_p}, -$$ - -with $\alpha$ the thermal expansivity, $g$ the gravity and $c_p$ the heat capacity. By default it uses the exact gravity (`adiabat_mode = "exact"`), - -$$ -T_a(r) = T_\mathrm{cmb} \exp\left(-\frac{\alpha\, \psi(r)}{c_p}\right), -$$ - -with $\psi$ the gravitational potential, zero at the CMB, on the Gauss-Legendre panel of the pressure. The closed form (`adiabat_mode = "small_radius"`) - -$$ -T_a(r) = T_\mathrm{cmb} \exp\left(\frac{r_\mathrm{cmb}^2 - r^2}{D^2}\right), \qquad D^2 = \frac{3 c_p}{2\pi \alpha \rho_\mathrm{cen} G}, -$$ - -(Labrosse et al. 2001, eqs. 7 and 11, the adiabat to the same order in radius as their density and gravity, eq. 8) holds for gravity linear in $r$; Nimmo (2015) and thermal_history use it, so sections 5, 6, 7, 10 and 13 do too. Mass and gravity are closed-form integrals of the density; the pressure is the hydrostatic integral of $\rho g$ from the CMB inward, by 32-point Gauss-Legendre quadrature (`pressure_mode = "quadrature"`), or the third-order expansion in $r/L$ that Labrosse et al. (2001, eq. 8) use for gravity (`pressure_mode = "labrosse"`). - -**Reference.** The checks run on the Earth-like test core ($\rho_\mathrm{cen} = 12500$ kg m$^{-3}$, $L = 7200$ km, $r_\mathrm{cmb} = 3480$ km). The references are an independent adaptive quadrature of the density, the linear limit of gravity near the centre, $4\pi G \rho_\mathrm{cen} r/3$, and hydrostatic balance, $dP/dr = -\rho g$. - -**Result.** The core has a mass of 1.92e+24 kg, a CMB density of 9896 kg m$^{-3}$ and a central pressure of 356.8 GPa. On this test core, $T_\mathrm{cen}/T_\mathrm{cmb}$ is 1.374 with the exact gravity and 1.405 in the closed form. - -| Check | Relative difference | -|---|---| -| Closed-form enclosed mass and gravity against the adaptive quadrature | 3.6e-13 | -| Gravity near the centre against the linear limit | 2.2e-16 | -| Hydrostatic balance of the pressure | 2.5e-08, the error of the 50 m central difference used to test it | -| Third-order pressure against the quadrature | up to 3.0e-03, the truncation of the expansion | - -Both pressures meet the CMB anchor exactly. - -![Gaussian core structure](../figures/vv/fig_08_core_structure.png) - -**Figure 1.** Gaussian core structure of the Earth-like test core. (a) Density, gravity and pressure against radius. (b) Relative residual of hydrostatic balance $|dP/dr + \rho g|/\rho g$ for the quadrature pressure, and the relative difference between the third-order and the quadrature pressure; both panels share the radius axis. - -**What the figure shows.** In (b) the residual of hydrostatic balance stays at or below the value of the result at every radius, and the difference of the third-order pressure is flat at its truncation level and falls to zero at the CMB, where both pressures are anchored. A pressure with a wrong slope would lift the residual by orders of magnitude. The pressure sets where the melting curve is evaluated, so the inner-core radius of a `core_module` run depends on it. - -**Pinned by** `tests/test_core_profiles.py::test_density_positive_monotone_and_mass_matches_quadrature`, `::test_gravity_centre_limit_and_shell_integral` and `::test_pressure_hydrostatic_balance_and_anchor`. - -## 2. Energy identities of the core budget - -**What is tested.** The core cools at - -$$ -\frac{dT_\mathrm{cmb}}{dt} = -\frac{Q_\mathrm{cmb} - Q_\mathrm{radio}}{\tilde C(T_\mathrm{cmb})}, \qquad \tilde C = C_s + C_L + C_g, -$$ - -where the effective capacity $\tilde C$ adds the secular term, the latent heat of inner-core growth and the gravitational energy of the light elements it rejects, $Q_\mathrm{cmb}$ is the CMB heat flow and $Q_\mathrm{radio}$ the radiogenic power. The heat content $H(T)$ is defined so that - -$$ -H(T_2) - H(T_1) = \int_{T_1}^{T_2} \tilde C\, dT; -$$ - -the solver books the core heat change from it, so this identity is what ties the core ledger to the CMB heat. Inner-core growth starts at the onset temperature $T_\mathrm{on}$, where the adiabat first touches the melting curve at the centre, and ends at freeze-out $T_\mathrm{fr}$, where the whole core has crystallised and the latent and gravitational terms vanish. - -**Reference.** The reference is the integral of $\tilde C$ by a trapezoid rule on 40001 points, on the test core of section 1 with the iron-alloy curve. The comparison covers 150 K on either side of the growth band, the CMB temperatures from the onset to the freeze-out, where the inner core grows. - -**Result.** For the iron-alloy curve on the test core, onset is at 4146.7 K and freeze-out at 3688.9 K. The heat-content difference matches the integral of $\tilde C$ to 6.6e-07 of the total; the residual above the onset is the trapezoid error of that reference at the square-root cusp of the latent term, not an error of $H$. At freeze-out the capacity drops by a factor of 3.143 for the Nimmo (2015) quadratic curve and by 50.5 % for the iron-alloy curve, without the gravitational term. - -![Energy identities](../figures/vv/fig_09_core_energy_identities.png) - -**Figure 2.** Effective heat capacity of the core across inner-core growth. (a) Secular, latent and gravitational capacities and their sum against CMB temperature, with the onset and freeze-out marked. (b) Relative difference between the heat-content difference and the integral of the total capacity at 120 temperatures, normalised by the total. - -**What the figure shows.** In (a) the latent and gravitational capacities are zero outside the growth band, rise steeply below the onset, and the latent one ends in a step at the freeze-out, where the total falls back to the secular capacity. In (b) the difference stays within the value of the result at every temperature. A heat content without the latent term would differ inside the band by a share of order one. The core ledger of a `core_module` run closes only if this identity holds. - -**Pinned by** `tests/test_core_budget.py::test_heat_content_difference_is_the_capacity_integral`, `::test_latent_and_gravitational_energy_conservation` (latent and gravitational capacities integrate to the geometric latent heat and gravitational energy to below 1e-6) and `::test_freeze_out_capacity_jump`. - -## 3. CMB boundary-layer flux - -**What is tested.** For a core hotter than the mantle side of the CMB, the CMB heat flux is - -$$ -q = \frac{k\, \Delta T}{\min(\Delta r_{1/2},\, \delta)}, \qquad \delta = \left(\frac{\kappa\, \eta\, Ra_\mathrm{crit}}{\rho\, g\, \alpha\, \Delta T}\right)^{1/3}, -$$ - -where $\Delta T = T_\mathrm{core} - T_m$, $\Delta r_{1/2}$ is the distance to the bottom mantle cell and $\delta$ is the boundary-layer thickness at the critical Rayleigh number (Foley & Driscoll 2016, eq. 20 and the text below it; Thiriet et al. 2019, eqs. 13 and 14). On the convective branch $q \propto \Delta T^{4/3}$; a core colder than the mantle gains heat by conduction across the half cell only, so the flux always has the sign of $\Delta T$. - -**Reference.** The law is evaluated for a bottom cell with constant properties, over a liquid and a solid base: - -| Property | Value | -|---|---| -| Conductivity $k$ | 4 W m$^{-1}$ K$^{-1}$ | -| Density $\rho$ | 4000 kg m$^{-3}$ | -| Heat capacity $c_p$ | 1000 J kg$^{-1}$ K$^{-1}$ | -| Expansivity $\alpha$ | $3\times10^{-5}$ K$^{-1}$ | -| Gravity $g$ | 10 m s$^{-2}$ | -| Half cell $\Delta r_{1/2}$ | 10 km | -| Viscosity $\eta$ of the liquid base | 0.1 Pa s | -| Viscosity $\eta$ of the solid base | $10^{21}$ Pa s | - -The default $Ra_\mathrm{crit} = 450$ is the critical value Thiriet et al. (2019, Table 2) use for the upper boundary layer of their mantle; for the lower boundary layer they scale it with the internal Rayleigh number (their eq. 16), so here it is a parameter, `ra_crit_cmb`. - -**Result.** The convective branch has the slope 1.333 in log-log and $Ra_\mathrm{crit} = 1800$ lowers the flux by the factor 0.630 $= (450/1800)^{1/3}$. At a 100 K contrast the flux is 5.55e+04 W m$^{-2}$ over the liquid base and 0.04 W m$^{-2}$ over a solid base, where conduction across the half cell sets it; a colder core conducts at the same rate over either base, to 4.2e-15. - -![CMB boundary-layer flux](../figures/vv/fig_10_cmb_boundary_layer_flux.png) - -**Figure 3.** Magnitude of the CMB heat flux against the core-mantle temperature contrast, for a liquid and a solid bottom cell with the properties of section 3, at $Ra_\mathrm{crit} = 450$ (solid) and 1800 (dashed); the dotted line is a core colder than the mantle, which conducts across the half cell over either base. - -**What the figure shows.** The two lines of the liquid base are straight and parallel on the logarithmic axes, with the slope of the convective branch, and the dashed line lies below the solid one by the factor of the result above. The line of the solid base and the dotted line of the colder core lie on each other with the slope 1 of conduction. A wrong exponent in $\delta$ would change the slope of the liquid-base lines, and a law without the conduction floor would put the solid-base line below the dotted one. This flux sets the cooling rate of the core in a `core_module` run. - -**Limits.** The properties of this check are constants; in the solver they come from the mantle phase model at the layer's mean temperature ([core_bc.md](core_bc.md)), and sections 9, 11 and 12 test the flux in the solver. The figure plots magnitudes, so it does not show the sign of the flux; the tests below hold the sign. - -**Pinned by** `tests/test_core_boundary_layer.py` (sign, hand-computed convective value, conduction branch, $Ra_\mathrm{crit}$ scaling, continuity and slopes through zero contrast). - -## 4. Inner-core nucleation - -**What is tested.** The inner-core radius $r_\mathrm{icb}(T_\mathrm{cmb})$ is the radius where the adiabat crosses the melting curve. The adiabat and the melting curve both have zero radial slope at the centre, so just below the onset their difference is quadratic in $r$ and - -$$ -r_\mathrm{icb} \propto (T_\mathrm{on} - T_\mathrm{cmb})^{1/2}: -$$ - -$r_\mathrm{icb}^2$ grows at a finite rate per kelvin, and $dr_\mathrm{icb}/dT_\mathrm{cmb}$ diverges at the onset. The budget's latent and gravitational capacities need $dr_\mathrm{icb}/dT_\mathrm{cmb}$, which the code supplies by the implicit-function theorem as a custom derivative rule. - -**Reference.** The references are the square-root law, which is the limit at the onset, and a central difference of the radius with a step of $10^{-3}$ of the undercooling. - -**Result.** Fitted over the first 0.1 K of undercooling, the log-log slope is 0.4998; over the first kelvin the next order of the expansion lowers the slope to 0.4989. The implicit-function derivative agrees with the central difference to 1.3e-06 from $10^{-3}$ to 30 K below the onset. - -![Inner-core nucleation](../figures/vv/fig_11_inner_core_nucleation.png) - -**Figure 4.** Inner-core growth below the onset. (a) Inner-core radius against undercooling, with the square-root law through the first point. (b) Relative difference between the implicit-function derivative and a central difference. - -**What the figure shows.** In (a) the radius lies on the dashed square-root law at small undercooling and falls below it at the largest undercooling shown, the next order of the expansion. In (b) the difference between the two derivatives is largest at the smallest undercooling, next to the onset where the derivative diverges. A wrong derivative rule would show in (b) as a difference of order one. The latent and gravitational capacities use this derivative, so the cooling rate of a core with an inner core depends on it. - -**Pinned by** `tests/test_core_verification_page.py::test_inner_core_radius_grows_as_the_square_root_of_undercooling` and `tests/test_jax_dsdt_core_module.py::test_jacobian_core_column_matches_central_differences`. - -## 5. Nimmo (2015) Earth core budget - -**What is tested.** The energy and entropy terms of the budget, the cooling rate and the inner-core growth rate, on a published parameter set for Earth's core. - -**Reference.** Nimmo (2015, Table 2, p. 42) gives one parameter set for Earth's core: - -| Input | Value | -|---|---| -| Central density | 12500 kg m$^{-3}$ | -| Length scale $L$ | 7272 km | -| CMB pressure | 139 GPa | -| Expansivity $\alpha$ | $1.25\times10^{-5}$ K$^{-1}$ | -| Conductivity $k$ | 130 W m$^{-1}$ K$^{-1}$ | -| Quadratic melting curve | $T_{m0} = 2677$ K, $T_{m1} = 2.95\times10^{-12}$ Pa$^{-1}$, $T_{m2} = 8.37\times10^{-25}$ Pa$^{-2}$ | -| Latent heat | 750 kJ kg$^{-1}$ | -| Light-element density jump | 560 kg m$^{-3}$ | - -Table 4 (p. 46) gives the present-day energy and entropy budget at $T_c = 4180$ K for CMB heat flows of 15.2 and 12 TW without radiogenic heat. Table 5 (p. 46) gives the energy released since the onset, $23\times10^{28}$ J (secular 11.6, latent 6.9, gravitational 4.4), and from it an inner-core age of 0.73 Gyr under a constant 10 TW. - -**Result.** On aragog's Gaussian core with the third-order pressure, these inputs put the inner-core boundary at 1209 km and 328.5 GPa at 4180 K, against the 1220 km and 328 GPa of Table 2. - -At 15.2 TW and 4180 K: - -| quantity | Nimmo (2015) | aragog | -|---|---|---| -| $Q_s$, $Q_L$, $Q_g$ (TW) | 6.1, 5.7, 3.4 | 6.06, 5.71, 3.43 | -| $Q_k$ (TW) | 15.0 | 14.97 | -| $E_s$, $E_L$, $E_g$, $-E_k$ (MW K$^{-1}$) | 183, 327, 809, 450 | 182, 330, 820, 451 | -| $-dT_c/dt$ (K Gyr$^{-1}$) | 104 | 103.3 | -| $dr_\mathrm{icb}/dt$ (km Gyr$^{-1}$) | 1050 | 1075 | - -Every energy and entropy term and the cooling rate agree to 1.3 percent at both heat flows (at 12 TW the cooling rate is 81.6 against 82 K Gyr$^{-1}$). The inner core grows 2.4 percent faster. The growth rate is $|dr_\mathrm{icb}/dT_c|$ times the cooling rate: aragog's $|dr_\mathrm{icb}/dT_c|$ is 10406 m K$^{-1}$ against $C_r = 10100$ m K$^{-1}$ (Table 2), a factor of 1.030, and its cooling rate is 0.994 of Table 4. The CMB temperature has dropped by 59.3 K since the onset, against about 60 K (p. 43), and the melting temperature at the centre is 5808 K against 5800 K (Table 5). - -Integrating aragog's capacities from 4180 K to the onset gives 22.2 (10.98, 6.83 and 4.42) $\times10^{28}$ J and 0.70 Gyr. The secular term, the secular capacity times the drop, is 5 percent lower, and the chapter's own numbers account for most of the gap: its Table 4 secular capacity, $Q_s$ over the cooling rate, times its 60 K drop gives 11.1 $\times10^{28}$ J, 4 percent below its Table 5 value, while aragog's capacity is 1.0003 times that of Table 4 and its drop is 59.3 K. - -The ages of Table 4, 0.59 and 0.75 Gyr, are not this integral: in our reading they are the drop since the onset times the present - -$$ -\tilde Q_T = \frac{Q_\mathrm{cmb}}{-dT_c/dt}, -$$ - -over the heat flow, which on the numbers of Table 4 ($4.6\times10^{27}$ J K$^{-1}$) and a 60 K drop gives 0.58 and 0.73 Gyr, within 3 percent of the table. The same quotient on aragog's state gives 0.574 and 0.727 Gyr, and the integral 0.464 and 0.587 Gyr, shorter because the latent and gravitational capacities rise from zero at the onset. - -The bound is 2 percent on every energy and entropy term of Table 4 and on the cooling rate, at both heat flows. - -![Nimmo (2015) budget](../figures/vv/fig_12_nimmo_budget.png) - -**Figure 5.** Ratio of aragog to Nimmo (2015, Table 4) at $T_c = 4180$ K for 15.2 and 12 TW: the energy and entropy terms, the cooling and inner-core growth rates and the inner-core age. Filled circles give aragog's age as the integral of its capacity to the onset, open circles as the drop since the onset over the present cooling rate. - -**What the figure shows.** The points of the eight energy and entropy terms and of the cooling rate lie close to the line at 1, at both heat flows; the growth rate lies above it. The filled circles of the age lie far below 1 and the open circles close to it: in our reading of the chapter, the gap comes from the definition of the age, not from the budget terms. A wrong term of the budget would move its point away from 1. These terms are computed from the capacities of the `core_module` budget, which set the core cooling and the inner-core growth of a run. - -**Limits.** Four quantities are documented exceptions to the bound, each held at its measured value: the inner-core growth rate (1.024 of Table 4, from $C_r$ and the cooling rate above), the inner-core age (0.786 and 0.783 of Table 4, which divides the drop by the present cooling rate), and $W_s$ and $W_\mathrm{tot}$ (0.947 and 0.967 of Table 5, from the chapter's own secular capacity above). Like the chapter, these numbers use the small-radius adiabat of section 1 (`adiabat_mode = "small_radius"`); the solver default integrates the exact gravity. - -**Pinned by** `tests/test_core_nimmo_benchmarks.py` (the adiabat length scale, ICB state and adiabatic heat flow of the two models in Nimmo 2015, ch. 9.08, Table 2, their $C_r$ from the caption of Figure 3, p. 211 (the value of model 2 is that of ch. 8.02, Table 2), and the ICB pressure and $\tilde Q_T$ of ch. 8.02), `tests/test_core_verification_page.py::test_the_earth_budget_meets_nimmo_tables_4_and_5` and `::test_page_numbers_match_the_values`. - -## 6. Budget terms against thermal_history - -**What is tested.** The secular, latent and gravitational capacities and the conduction entropy sink, each against an independent core evolution code along one thermal history. - -**Reference.** The `leeds` core model of thermal_history (Greenwood et al. 2021) computes the same budget with polynomial profiles, a trapezoid quadrature on a radial grid and its own inner-core growth. The comparison runs it on the Nimmo (2015, Table 2) core, with: - -- the Gaussian density and adiabat given to it as the first 8 terms of their Taylor series in $r^2$ (degree 14); -- the same quadratic melting curve without a light-element depression; -- a fixed latent heat of 750 kJ kg$^{-1}$; -- complete rejection of the light element at the inner-core boundary; -- the CODATA gravitational constant aragog uses. - -A core-only history starts at a CMB temperature of 4400 K under a fixed CMB heat flow of 10 TW; aragog's capacities are evaluated on each thermal_history state. Inputs, versions and licence are in `tools/verification/data/`. - -**Result.** The secular term agrees to 1.4e-08 and the conduction entropy sink to 1.5e-07; the latent term agrees to 6.1e-04. The gravitational term differs by up to 3.8 %, because thermal_history enriches the outer core in the light element as the inner core grows (by the factor 1.039 at the end), while aragog holds the composition fixed; with that factor applied the two agree to 6.1e-04. - -![Budget terms against thermal_history](../figures/vv/fig_13_leeds_budget_terms.png) - -**Figure 6.** Relative difference of aragog's secular, latent and gravitational capacities from those of the `leeds` model of thermal_history along its 10 TW history on the same inputs; the dotted line applies thermal_history's outer-core enrichment to aragog's gravitational term. - -**What the figure shows.** The secular difference is flat over the whole history, and the latent and gravitational differences start at the inner-core onset. The gravitational difference grows as the inner core grows, and the dotted line, with the enrichment applied, falls to the level of the latent line: the enrichment explains the gap. A wrong profile integral would put a term far above these levels from its first point. The cooling rate of a `core_module` core follows from the sum of these capacities. - -**Limits.** aragog holds the composition of the outer core fixed, so its gravitational term differs from that of a model with enrichment by the amount above. The stratified layer is compared with thermal_history in [section 13](#13-stable-layer-against-thermal_history). - -**Pinned by** `tests/test_core_verification_page.py::test_budget_terms_match_the_thermal_history_table`, and on the Nimmo (2015) state by `tests/test_core_nimmo_benchmarks.py::test_budget_terms_match_thermal_history_cross_check` and `tests/test_core_entropy.py::test_entropy_capacities_match_thermal_history`. - -## 7. Dynamo criterion and field strength - -**What is tested.** The entropy budget gives the entropy production available to the dynamo, - -$$ -\Delta E = E_s + E_L + E_g + E_R - E_k, -$$ - -positive when the core can sustain a dynamo. The field strength follows the energy-flux scaling of Christensen et al. (2009, eq. 2), - -$$ -\frac{\langle B \rangle^2}{2\mu_0} = c\, f_\mathrm{ohm}\, \langle \rho \rangle^{1/3} (F q_o)^{2/3}, -$$ - -with $c = 0.63$ and the efficiency factor - -$$ -F = 0.88\, \frac{\alpha\, g_\mathrm{cmb}\, r_\mathrm{cmb}}{c_p} \quad \text{or} \quad F = 0.45\, \frac{\alpha\, g_\mathrm{cmb}\, r_\mathrm{cmb}}{c_p}, -$$ - -the first for a constant total heat flow $4\pi r^2 q_c$, the second for a convected flux that vanishes at the outer boundary (p. 168). With $f_\mathrm{ohm} = 1$, the upper end of $f_\mathrm{ohm} \le 1$ (p. 167), the field is the largest the scaling allows for that flux. - -**Reference.** The references are the two efficiency factors that Christensen et al. print for their Earth inputs ($\alpha = 1.35\times10^{-5}$ K$^{-1}$, $g = 10.7$ m s$^{-2}$, $R = 3480$ km, $c_p = 840$ J kg$^{-1}$ K$^{-1}$), Nimmo's (2015, p. 46) minimum heat flow for a present-day dynamo without radiogenic heat, and the internal field of Earth from Christensen et al.'s dipole field of 0.26 mT at the dynamo surface and their ratio of about 7 between the total and the dipole field (p. 168). - -**Result.** With Christensen et al.'s Earth inputs the two factors are 0.527 and 0.269, against the printed 0.52 and 0.27; on the Nimmo (2015) core the constant-flux factor is 0.484, lower mainly through the lower expansivity ($1.25\times10^{-5}$ K$^{-1}$; the CMB gravity, 10.6 m s$^{-2}$, accounts for under 1 percent). On that core at $T_\mathrm{cmb} = 4180$ K the adiabatic heat flow is 14.97 TW and the entropy margin vanishes at 5.14 TW, against Nimmo's minimum of about 5 TW. Between these two heat flows the margin is positive but no flux is superadiabatic, so the field estimate is zero although the entropy budget allows a dynamo. At 17 TW the core field is 1.10 mT, the order of magnitude of the 1.82 mT for Earth. - -![Dynamo scaling](../figures/vv/fig_14_dynamo_scaling.png) - -**Figure 7.** Dynamo criterion and field strength on the Nimmo (2015) core at $T_\mathrm{cmb} = 4180$ K. (a) Entropy margin against CMB heat flow, with the threshold where it vanishes. (b) Volume-averaged core field from Christensen et al. (2009, eq. 2), with Earth's estimated internal field. The field is zero up to the adiabatic heat flow $Q_k$, 15.0 TW, so also from 5.1 TW, where the margin turns positive, to $Q_k$. - -**What the figure shows.** In (a) the margin rises with the CMB heat flow and crosses zero at the threshold. In (b) the field is zero up to $Q_k$ and rises steeply above it, and crosses the dashed line of Earth's estimate inside the plotted range. A wrong sign of the conduction sink $E_k$ would move the zero crossing in (a), and a field computed from the total heat flow would be nonzero below $Q_k$ in (b). The entropy margin and the field strength that PROTEUS writes for a `core_module` run come from the two relations of this section. - -**Limits.** Christensen et al. derive the efficiency factors for Earth's core with an inner core of 0.35 of the core radius, $L = D$, constant density and thermodynamic properties and gravity linear in radius (pp. 167 and 168); aragog applies them at every inner-core size. Their $q_o$ is the effective convected flux at the inner boundary, compositional driving included, scaled to the outer radius (p. 167); aragog takes the superadiabatic part of the CMB heat flow over the CMB area, a thermal flux only, so here $q_o$ departs from their definition. With $q_o$ defined differently, the field comparison is an order-of-magnitude check only. - -Without the resolved shell (`stratification = false`) the efficiency factors of $\Delta E$ use $T_\mathrm{cmb}$, the convecting adiabat extended to the CMB. Under a stratified layer the reference of the whole-core entropy balance is the actual CMB temperature (Greenwood et al. 2021, eq. 4 and p. 9), which aragog does not resolve without the shell and which lies between $T_\mathrm{cmb}$ and $T_a(r_s)$ at the base of the layer; $\Delta E$ is then the upper end of that bracket. With the resolved shell the reference is the temperature of the top shell cell ([core_bc.md](core_bc.md#resolved-stable-layer-experimental)). - -**Pinned by** `tests/test_core_entropy.py::test_chr09_efficiency_factors_reproduce_printed_values`, `::test_field_scaling_bounds_and_earth_magnitude` and `::test_dynamo_threshold_and_margin`. - -## 8. Iron melting curve - -**What is tested.** The pure-iron melting curve is the two-branch Simon-Glatzel fit of Anzellini et al. (2013, eqs. 2 and 3, p. 466); this is the PALEOS prescription. On the $\gamma$-Fe branch, with $T_0 = 1991$ K and $P_0 = 5.2$ GPa, - -$$ -\frac{P - P_0}{27.39} = \left(\frac{T_m}{T_0}\right)^{2.38} - 1, -$$ - -and on the $\epsilon$-Fe branch, from the $\gamma$-$\epsilon$-liquid triple point at 98.5 GPa and 3712 K, - -$$ -\frac{P - P_\mathrm{TP}}{161.2} = \left(\frac{T_m}{T_\mathrm{TP}}\right)^{1.72} - 1, -$$ - -with $P$ in GPa, $T_m$ the melting temperature and $P_\mathrm{TP}$ and $T_\mathrm{TP}$ the pressure and temperature of the triple point. The two branches meet with a jump at the triple point, which a $C^2$ smootherstep over a 3 GPa half-width removes. Light elements depress the curve by the factor $1 - \mathrm{depression} \times x$. - -**Reference.** The reference is the unblended fit: the two branches as printed, and the PALEOS source for the pure-iron values. - -**Result.** The jump of the two branches at the triple point is 0.725 K; the blended curve departs from the branches by at most 0.361 K and equals them exactly outside the blend. The depression factor is 0.88 for $x = 0.1$ and a depression of 1.2. The pure-iron curve gives 4192.0 K at 136 GPa and 6229.2 K at 330 GPa. - -![Iron melting curve](../figures/vv/fig_15_iron_melting_curve.png) - -**Figure 8.** Iron melting curve. (a) Pure-iron and alloy melting temperature against pressure, with the triple point marked. (b) Blended curve minus the unblended Anzellini et al. (2013) branches around the triple point; the jump is that of the unblended fit. - -**What the figure shows.** In (a) both curves rise smoothly through the triple point, and the alloy curve lies below the pure-iron curve at every pressure. In (b) the difference from the unblended branches is zero outside the blend and largest on both sides of the triple point; its jump there is the jump of the unblended fit, so the blended curve is continuous. Without the blend, the melting temperature would jump at the triple point, and so would the inner-core radius of a core whose inner-core boundary passes that pressure. The onset temperature and the inner-core radius of a `core_module` run with the iron curve follow from it. - -**Limits.** The depression is a model choice, not part of the Anzellini fit. - -**Pinned by** `tests/test_core_melting.py::test_pure_iron_pins_against_the_paleos_source`, `::test_branch_switch_matches_unblended_outside_transition` and `::test_branch_switch_deviation_and_c1_continuity`. - -## 9. A CVODE solve across the inner-core onset - -**What is tested.** The coupled right-hand side carries the core temperature as a state, so CVODE integrates the core through the onset, where the latent capacity rises as a square root. The check compares the core heat change that the solver books with the CMB heat that it books. - -**Reference.** The quadratic melting curve of section 5 at 1.5 times its $T_{m0}$ (4015.5 K) puts the onset above the mantle base. The call starts with a core 2 K above the onset over a partly molten mantle base and lasts 4 yr. The bound is $10^{-6}$ for a core without a layer solved alone. - -**Result.** The core cools from 6414.4 K to 6409.5 K, through the onset at 6412.4 K, and grows an inner core of 244 km. At a solver rtol of $10^{-10}$ (atol stays at its floor of $10^{-8}$) the solver books the core heat change as the heat-content difference, and that change matches the CMB heat it books to 5.7e-07, within the bound. The CMB heat reconstructed from the 71 output times by the trapezoid rule matches to 5.6e-04, the error of that sampling. - -![CVODE solve across the onset](../figures/vv/fig_16_cvode_onset_ledger.png) - -**Figure 9.** A CVODE solve through the inner-core onset. (a) Core temperature against time, with the onset temperature. (b) Core heat lost from the heat-content difference and from the time integral of the CMB heat flow. (c) Relative difference between the two curves in (b). - -**What the figure shows.** In (a) the core temperature crosses the onset temperature during the call. In (b) the two curves of the heat lost lie on each other, and (c) gives their difference, which is the sampling error of the trapezoid rule over the output times and is largest at the first outputs. A core temperature that did not follow the CMB heat flow through the onset would separate the curves in (b). The core energy ledger of a `core_module` run rests on this closure. - -**Limits.** At the default rtol of $10^{-8}$ the CMB heat matches to 1.5e-06, so a call across the onset needs the tighter tolerance to meet that bound. Panel (c) does not show the closure of the booked ledger, only the sampled reconstruction. - -**Pinned by** `tests/test_entropy_solver_core_module_smoke.py::test_core_module_cvode_solve_crosses_the_inner_core_onset` and `tests/test_core_verification_page.py::test_a_cvode_solve_across_the_onset_closes_its_heat`. - -## 10. A core thermal history against thermal_history - -**What is tested.** The core-only history of section 6 tests the integrated evolution rather than the terms. - -**Reference.** aragog integrates its cooling rate under the same fixed 10 TW CMB heat flow with LSODA (SciPy `solve_ivp`) at a relative tolerance of $10^{-10}$ and an absolute tolerance of $10^{-8}$ K, and thermal_history steps it with its own fixed 1 Myr steps. - -**Result.** Over 1499 Myr the CMB temperatures differ by at most 0.119 K. The centre temperatures, on the same adiabat, differ by at most 0.163 K, the CMB difference times the adiabat ratio, and the final inner-core radii by 0.13 %, within the bounds of 0.2 K for the CMB temperature, 1 K for the centre temperature and 1 % for the radius. - -| Quantity | aragog | thermal_history | -|---|---|---| -| Inner-core nucleation (Myr) | 911 | 911, on its 1 Myr grid | -| Final inner-core radius (km) | 1122.8 | 1121.4 | - -The inner-core age is 588 Myr in both. - -Before the onset the CMB temperatures agree to 2.2e-06 K. The difference appears at the onset, 6.5e-04 K at the first step, rises to 0.024 K, changes sign at 1196 Myr and reaches its largest magnitude at the end, at 1499 Myr (aragog minus thermal_history throughout). - -![Core thermal history against thermal_history](../figures/vv/fig_17_leeds_thermal_history.png) - -**Figure 10.** Core-only thermal history under a fixed 10 TW CMB heat flow. (a) CMB temperature from aragog and thermal_history. (b) Inner-core radius. (c) Absolute difference of the CMB temperatures. - -**What the figure shows.** In (a) and (b) the two models lie on each other; the cooling slows where the inner core starts to grow. In (c) the difference is small before the onset, jumps at the onset, passes through zero at the sign change and is largest at the end. A wrong secular capacity would give a difference that grows from the start of the history, not from the onset. The CMB temperature and the inner-core radius are the outputs of the core model that a `core_module` run reports. - -**Limits.** We attribute the jump at the onset to thermal_history's explicit step, which crosses the start of the latent release in one step, and the later drift to the outer-core enrichment of section 6, which shifts the gravitational term; neither cause is tested separately. - -**Pinned by** `tests/test_core_verification_page.py::test_core_history_matches_the_thermal_history_table`. - -## 11. Coupled PROTEUS case - -**What is tested.** Coupled PROTEUS runs test the sign of the CMB flux against the core-mantle temperature contrast, the closure of the core ledger against the CMB heat, and the dependence of the CMB flux on the mantle mesh. - -**Reference.** A coupled PROTEUS run of a 1 Earth-mass planet that starts fully molten runs to mantle solidification twice, once with `core_bc = "core_module"` and once with `energy_balance`, whose CMB flux follows the mantle-side entropy gradient. The configuration is `tools/verification/data/coupled_config.toml`: - -- Zalmoxis structure; -- the CVODE solver; -- the PALEOS iron curve with a light-element depression; -- no stratification; -- the PALEOS-2phase MgSiO$_3$ mantle tables. - -The same configuration at 3 and 5 Earth masses (`planet.mass_tot`) and at 1 Earth mass with `interior_energetics.rtol = 1e-10` gives the core ledgers in `tools/verification/data/coupled_ledger_*.csv`. - -The coupled tables of the plotted columns are in `tools/verification/data/`, each with the PROTEUS and aragog commits of its run in its header. The `core_module` tables come from an inversion of the layer's temperature that agrees with the one described in [the core boundary condition](core_bc.md) to rounding, except for a layer mean temperature within about $3 \times 10^{-11}$ K of a phase-boundary temperature, where the EOS tables hold the temperature flat. - -### Flux sign - -With `core_module` no row of the 1 Earth-mass run has a CMB flux against the core-mantle temperature contrast. The same holds for the 3 and 5 Earth-mass runs and for a 1 Earth-mass run with the Wolf and Bower (2018) mantle. The table gives the rows of each run, the rows with a flux against the contrast (wrong sign), the largest contrast, the rows with zero flux and the contrast of those rows: - -| Run | Rows | Wrong sign | Largest contrast (K) | Zero flux | Contrast at zero flux (K) | -|---|---|---|---|---|---| -| 1 $M_\oplus$ | 661 | 0 | 823 | 1 | 4.8e-08 | -| 3 $M_\oplus$ | 795 | 0 | 1928 | 1 | 0 | -| 5 $M_\oplus$ | 695 | 0 | 2428 | 1 | 1.0e-06 | -| 1 $M_\oplus$, Wolf and Bower (2018) mantle | 217 | 0 | 712 | 1 | 0 | - -`coupled_flux_.csv` holds the rows of the last three runs. - -With `energy_balance` the core runs 7.7 to 10.0 K colder than the mantle side of the CMB, and the flux carries heat out of it on all 681 rows. - -With the boundary layer's properties at its mean temperature, the `core_module` core follows the mantle: it cools to 5251 K while the mantle side of the CMB cools to 4428 K, which gives the largest contrast of its row in the table, and the CMB flux is still 8.0 W m$^{-2}$ at 500 kyr. The mantle reaches a melt fraction of 0.05 at 661 kyr with `core_module` and at 665 kyr with `energy_balance`, which feeds core heat into the mantle throughout. - -![Coupled PROTEUS case](../figures/vv/fig_18_coupled_proteus.png) - -**Figure 11.** A coupled PROTEUS run to mantle solidification with the `core_module` and the `energy_balance` core boundary conditions. (a) Core temperature in both runs and the mantle side of the CMB, which cools alike in both. (b) CMB heat flux; crosses mark rows where the flux carries heat against the core-mantle temperature contrast. - -**What the figure shows.** In (a) the `energy_balance` core lies on the mantle side of the CMB, while the `core_module` core stays hotter and cools more slowly. In (b) every row of the `energy_balance` line has a cross and no row of the `core_module` line has one; the `core_module` flux starts at zero, where the core starts at the mantle temperature. A flux law with a wrong sign would put crosses on the `core_module` line. The sign of the CMB flux decides whether the core heats or cools the mantle base of a run. - -### Core ledger closure - -The table gives the cumulative residual of the core ledger relative to the cumulative CMB heat, the share of the summed absolute call residuals that comes from the call from 22 to 122 yr, and the bound for each run. The bound applies to the cumulative residual after the first kyr and at the end. - -| Run | residual at the end | largest after 1 kyr | share of the call from 22 to 122 yr | bound | -|---|---|---|---|---| -| 1 $M_\oplus$, rtol $10^{-8}$ | 1.5e-07 | 1.9e-07 | 0.04 | $10^{-6}$ | -| 1 $M_\oplus$, rtol $10^{-10}$ | 4.7e-08 | 6.1e-08 | 0.08 | $10^{-6}$ | -| 3 $M_\oplus$, rtol $10^{-8}$ | 1.8e-07 | 2.4e-07 | 0.06 | $5 \times 10^{-6}$ | -| 5 $M_\oplus$, rtol $10^{-8}$ | 1.5e-07 | 2.0e-07 | 0.09 | $5 \times 10^{-6}$ | - -The call from 22 to 122 yr gives 4 to 9 percent of the summed call residuals. The bound is $5 \times 10^{-6}$ for the last two runs of the table and $10^{-6}$ for the 1 Earth-mass runs and for a core without a layer solved alone (section 9). - -The first call of nonzero length, from 1 to 2 yr, closes to 1.5e-04 of its own CMB heat at 1 Earth mass and to 1.0e-04 to 1.1e-04 in the other runs; the source of this error is not established, and the test holds it below $3 \times 10^{-4}$. `tests/test_entropy_solver_core_module_smoke.py::test_chained_cvode_calls_close_the_core_ledger` makes calls ending at the same times on a core solved alone and closes each to $10^{-6}$; it does not reproduce this error, and the bounds of the table are checked on the stored runs, not on the current solver. - -### Cores without an inner core and the top-down case - -None of the 1, 3 and 5 Earth-mass runs grows an inner core. The cores are coldest at the end of each run, above their inner-core onsets (the budget rebuilt from the core profile of each run in `coupled_core_structure.csv`), so these runs test the secular capacity only: - -| Run | Core temperature at the end (K) | Inner-core onset (K) | -|---|---|---| -| 1 $M_\oplus$ | 5251 | 4028 | -| 3 $M_\oplus$ | 6902 | 4598 | -| 5 $M_\oplus$ | 8187 | 4266 | - -Section 9 takes a CVODE solve through the onset, and `tests/test_entropy_solver_core_module_smoke.py::test_a_cvode_call_through_onset_and_freeze_out_closes_its_heat` takes one through the onset and freeze-out, with an entropy of fusion of 0.17 J kg$^{-1}$ K$^{-1}$ and no gravitational term, so that one call crosses the whole band. - -With the Earth values of the thermal expansivity and the heat capacity in the core adiabat (below), at 3 and 5 Earth masses the melting curve reaches the adiabat at the CMB, at 5217 K and 6865 K, before it reaches it at the centre, so these cores would freeze from the top down; the order of the two temperatures depends on that adiabat. The budget books latent and gravitational heat only for an inner core that grows from the centre, so a call that enters a top-down or snow regime raises an error; the energetics of those regimes are not modelled. `::test_a_core_that_freezes_from_the_top_is_refused` takes a solver call into the top-down regime, and `tests/test_core_nimmo_benchmarks.py::test_model1_printed_parameters_break_bottom_up_topology` gives a snow state to the guard that the solver calls. The runs end above these temperatures. - -### Pressures and extrapolation - -| Run | CMB pressure (GPa) | Centre pressure (GPa) | -|---|---|---| -| 1 $M_\oplus$ | 103 | 341 | -| 3 $M_\oplus$ | 298 | 1045 | -| 5 $M_\oplus$ | 516 | 1837 | - -The structure EOS of the core is the PALEOS iron table, which spans pressures up to 100 TPa and so covers these. The melting curve is the fit of section 8, to melting points that Anzellini et al. (2013) measured from 50 to 200 GPa: it is an extrapolation at the centre of every run and at the CMB of the 3 and 5 Earth-mass runs, and so are the onset and freeze-out temperatures that follow from it there. The budget uses the Earth values of the thermal expansivity, $1.35 \times 10^{-5}$ K$^{-1}$, and the heat capacity, 840 J kg$^{-1}$ K$^{-1}$, at every mass. - -### Mesh dependence of the CMB flux - -The 1 Earth-mass `core_module` run at 40, 80, 160 and 320 mantle levels (`interior_energetics.num_levels`), and at 80 levels with `interior_energetics.rtol = 1e-10`, tests how the CMB flux depends on the mantle mesh through basal freezing, the time $t_\mathrm{bf}$ at which the melt fraction of the lowest mantle node falls through 0.5 (`coupled_mesh_convergence.csv`). At 320 levels basal freezing comes at 1327 yr, with a CMB flux of 7.48e+04 W m$^{-2}$. - -From 160 to 320 levels the changes are within the bounds of 5 % for the times and fluxes and 10 K for the temperatures: - -| Quantity | Change from 160 to 320 levels | -|---|---| -| $t_\mathrm{bf}$ | 0.62 % | -| Core temperature at $t_\mathrm{bf}$ | 1.34 K | -| Core temperature at the end | 0.11 K | -| CMB flux at 10 yr | 0.029 % | -| CMB flux at 100 yr | 0.0044 % | -| CMB flux at 1000 yr | 0.041 % | -| CMB flux at $t_\mathrm{bf}$ | 0.80 % | - -After basal freezing the flux converges too: at twice and four times $t_\mathrm{bf}$ and at 10, 100 and 500 kyr it changes by at most 1.1 % from 160 to 320 levels, by at most 4.0 % from 80 to 160 and by at most 7.8 % from 40 to 80; a flux set by conduction across the half cell would double with each doubling. At 500 kyr it is 8.27, 7.99, 8.16 and 8.12 W m$^{-2}$ at 40, 80, 160 and 320 levels, and the core cools by 370 to 376 K from $t_\mathrm{bf}$ to the end. - -The tighter tolerance at 80 levels changes the bounded fluxes and times by at most 3.1e-05 and the temperature at $t_\mathrm{bf}$ by 0.0054 K. The flux of a row is the mean over the call that ends at it, and the fluxes at a given time are linear between the row times; at 100 kyr the calls of the two runs last 1.1 and 0.16 kyr, so their fluxes there differ by 0.4 %, against 5.0e-05 with each call mean placed at the middle of its call (from the CMB heat of each call in `coupled_ledger_1me*.csv`). - -### Limits and tests of the coupled case - -**Limits.** The liquid heat capacity of the PALEOS-2phase mantle tables is too large ([PALEOS issue 6](https://github.com/maraattia/PALEOS/issues/6)), so these runs test the core ledger and the flux sign, not the mantle's cooling history. The limits of each part are stated with it: the first-call error of the ledger, the secular capacity as the only capacity tested, the top-down case, and the extrapolation of the melting curve. - -**Pinned by** `tests/test_core_verification_page.py::test_coupled_tables_reproduce_the_page`, which recomputes these numbers from the tables and holds them to the bounds above; the runs themselves need PROTEUS and are not repeated by the test suite. - -## 12. NumPy and JAX right-hand sides and the analytic Jacobian - -**What is tested.** The solver evaluates the coupled right-hand side in NumPy and, for CVODE's analytic Jacobian, in JAX; the two must agree, and the JAX Jacobian must equal the derivative of that right-hand side. The two evaluate the same equations except for the single-phase thermal expansivity: NumPy reads it from the `thermal_exp` tables, JAX derives it from the identity - -$$ -\alpha = \frac{\rho c_p}{T} \left|\frac{\partial T}{\partial P}\right|_S. -$$ - -**Reference.** Each right-hand side is the reference of the other, and a central difference of $\dot T_\mathrm{core}$ is the reference of the analytic Jacobian entry $\partial \dot T_\mathrm{core}/\partial T_\mathrm{core}$. The comparison runs on a 10-node mesh with the core in three states (inside the growth band, 100 K above the onset, and stratified with the core 50 K above the mantle), with the NumPy expansivity from the identity as well. These states put the mantle cells in the mush, where the composite expansivity applies and the tables do not enter; a fourth comparison puts the mantle above its liquidus at every node. - -**Result.** - -| Comparison | Relative difference | -|---|---| -| Right-hand sides, largest over the components, first two states | 1.4e-12 | -| Right-hand sides, largest over the components, with the layer, whose core rate is ill-conditioned (section 13) | 2.0e-10 | -| Right-hand sides over a liquid mantle, largest over the components, with the identity | 2.8e-14 | -| Right-hand sides over a liquid mantle, largest over the components, with the tables | 1.7e-04 | -| Jacobian entry against a central difference at the best step, in the growth band | 1.6e-11 | -| Jacobian entry against a central difference at the best step, with the layer | 1.5e-11 | - -The difference with the tables is the difference of the two single-phase expansivities on these tables, which NumPy reads from the tables for the mantle cells and for the boundary layer when they are present; on the two 1350 by 280 sets the same comparison, run separately from the script, gives $6.1 \times 10^{-5}$ and $2.1 \times 10^{-5}$. The best step is the one that balances truncation and rounding; above the onset the core is colder than the mantle base, the flux is on its linear conduction branch, and the difference stays at rounding for every step. - -![NumPy and JAX parity](../figures/vv/fig_19_numpy_jax_parity.png) - -**Figure 12.** Agreement of the NumPy and JAX core right-hand sides. (a) Relative difference per state component, the entropy cells first and the CMB entropy gradient and core temperature last, in three core states. (b) Relative error of a central difference of $\dot T_\mathrm{core}$ against the analytic Jacobian entry, against the difference step. - -**What the figure shows.** In (a) the stratified state has the most components, its shell cells, and the largest differences, at the level of the table above; the lines of the two other states end after the entropy cells and the two boundary slots. In (b) the error of the central difference falls with the step to a minimum and rises again at small steps, where rounding takes over; above the onset it stays at rounding. A wrong Jacobian entry would give an error that does not fall with the step. With `use_jax_jacobian` set, as in PROTEUS, CVODE builds the implicit steps of a `core_module` run on this Jacobian. - -**Limits.** In the three core states the CMB flux is the conduction across the half cell, and in runs separate from the script the numbers of this section do not change when the expansivity of the boundary layer is multiplied by 1.5 in both NumPy and JAX; `tests/test_jax_dsdt_core_module.py::test_jacobian_core_column_matches_central_differences` checks the boundary-layer flux instead, with the Jacobian columns of the core temperature and the bottom cell against central differences for a core 300 K above a base at melt fraction 0.66, where the test asserts a flux above $10^4$ W m$^{-2}$. - -**Pinned by** `tests/test_jax_dsdt_core_module.py::test_boundary_slots_match_numpy_on_a_five_node_mesh`, `::test_jacobian_core_column_matches_central_differences` and `::test_jacobian_carries_boundary_couplings`. - -## 13. Stable layer against thermal_history - -**What is tested.** The resolved layer of `stratification = true` is compared with the `leeds_thermal` model of thermal_history (Greenwood et al. 2021) on the core of section 6, which starts on the adiabat at a CMB temperature of 4400 K. The conducted adiabatic flow at the CMB is 15.8 TW. Under a CMB flow below it, the top of the core takes up more heat by conduction along the adiabat than the CMB removes, and a warm stable layer forms; the CMB removes 8 or 12 TW, adds 2 TW, or removes nothing. Under a flow above it, an existing layer erodes. - -**Reference.** aragog resolves the outer core above 0.4 $r_\text{cmb}$ in 64 cells with the default mixing constants; mixing acts only where the temperature falls outward faster than the adiabat, so a subadiabatic region conducts only. `leeds_thermal` instead moves the base of its layer by a stability check after each step. The layer depth is defined the same way on both sides: the depth below the CMB where the excess over the convecting adiabat falls to 0.1 of its value at the top, which for aragog is the centre of its top cell. The CMB temperature of aragog that is compared with `leeds_thermal` is the temperature of that cell (`core_T_top`), not the state $T_\text{cmb}$ of the convecting adiabat. aragog integrates the convecting core and the shell alone with SciPy BDF at a relative tolerance of 1e-11. - -In the four cases with a fixed flow, until the inner core of `leeds_thermal` forms at 588 Myr, its centre temperature differs by at most 0.33 K between the flows and its layer depth from 200 Myr on by at most 1.4 %; the CMB temperature depends on the flow throughout. - -![Stable layer against thermal_history](../figures/vv/fig_20_leeds_stable_layer.png) - -**Figure 13.** The resolved stable layer of aragog (solid) against the `leeds_thermal` model of thermal_history (dashed). (a) Layer depth at 0.1 of the top excess over the adiabat, (b) centre temperature and (c) inner-core radius under a CMB flow of 8 and 12 TW to 999 Myr; (d) CMB temperature under -2 and 0 TW. (e) CMB temperature and (f) layer depth in the erosion case to 399 Myr; the shaded window runs from the rise of the flow at 50 Myr to the time aragog's shell mixes completely, and aragog's depth is not drawn while the mixing acts below its top cell, where the depth at 0.1 of the top excess has no meaning. - -**What the figure shows.** In (a) to (d) the solid and dashed lines lie close to each other: the layer grows until the inner core forms and thins after it, and the CMB warms under the two flows of (d). In (e) and (f) the models differ inside the shaded window, where each removes the layer in its own way, and form a new layer of similar depth after the flow falls back. A shell that lost or gained heat would separate the centre temperatures in (b). The layer base (`core_layer_base`) and the top temperature (`core_T_top`) that a stratified `core_module` run reports come from this shell. - -### Fixed flows - -| CMB flow | max $\lvert\Delta T_\text{cmb}\rvert$ (K) | max $\lvert\Delta T_\text{cen}\rvert$ (K) | depth ratio | onset aragog (thermal_history) (Myr) | $r_\text{icb}$ at 999 Myr | -|---|---|---|---|---|---| -| 8 TW | 2.88 | 0.62 | 0.79 to 1.13 | 590 (588) | 0.20 % | -| 12 TW | 2.23 | 0.62 | 0.82 to 1.39 | 589 (588) | 0.05 % | -| -2 TW | 4.11 | 1.24 | 0.79 to 1.01 | 592 (589) | 0.24 % | -| 0 TW | 3.88 | 0.93 | 0.79 to 1.01 | 591 (589) | 0.15 % | - -For the four fixed flows the bounds are: - -- 5 K on the CMB temperature; -- 10 K on the centre temperature; -- a factor of 1.5 on the layer depth where both layers are thicker than 10 km after the first Myr; -- 2 % on the inner-core radius at the last sample (999 Myr); -- 5 % on its onset time. - -The extremes of the depth ratio fall in the first 51 Myr, while the layer is thin; from 300 Myr on the two depths agree to within 1.5 % (Figure 13a). The CMB temperature difference reaches its largest value between 36 and 69 Myr, with aragog colder. aragog reports its top cell as the CMB temperature; the centre of that cell lies 1000 m below the CMB, where the adiabat of the starting core is 0.80 K above the CMB temperature, so aragog starts that much warmer. - -### Tolerance and reproducibility - -Where BDF stalls, the integration restarts from its last accepted state; the shell runs of this section, at every tolerance and mixing constant, restart 1 time in all, a count that can differ between platforms. Between runs at 1e-11 and 1e-12 the centre temperature differs by at most 0.27 K. - -In runs separate from the script, whose values, commits and platforms are in `tools/verification/data/stable_layer_spread.json`, that difference comes from the -2 TW case: a start temperature changed in its last bit lowers its largest centre-temperature difference from thermal_history from 1.24 K to 0.99 K, the tolerance of 1e-12 to 0.99 K, and the two changed runs agree to 0.008 K. In the other cases the last-bit change moves that difference by 0.024 K or less, the inner-core radii by 2.8e-05 of themselves or less (8.3e-05 at -2 TW) and the largest late depth difference by 0.075 percentage points. On Linux the 8 TW difference of the table above is 0.69 K, 0.07 K above the value here. The centre-temperature differences therefore reproduce only to within changes of up to 0.25 K, and the page test holds them to 0.6 K at -2 TW and to 0.2 K in the other cases. - -At a relative tolerance of 1e-8 the comparison has not converged: in the run of the values file the centre temperature then differs from thermal_history by up to 8.7 K and the inner-core onset by up to 26 Myr. These two differences depend on the platform and on the tests run before in the same process: the process runs in `tools/verification/data/stable_layer_spread.json` reach 20 K and 61 Myr. - -The heat rates of the core and the shell sum to the CMB flow at every sample to 5.0e-16 of the conducted adiabatic flow or better, in every case; this is a property of the discretisation (the shell fluxes telescope and the core uses one effective capacity), not of the integration. - -Coupled to the mantle solver, a 5 yr CVODE call that heats a stratified core from above closes its heat against the CMB heat, in runs separate from the script, to $1.9 \times 10^{-6}$ or $8.2 \times 10^{-6}$, a choice a rounding-level change of the input makes ($7.7 \times 10^{-6}$ or $8.2 \times 10^{-6}$ with the exact adiabat and the boundary-layer properties at the mean temperature), because the stratified core rate moves by $1.5 \times 10^{-6}$ of itself under a $10^{-14}$ change of the shell temperatures; `tests/test_entropy_solver_core_module_smoke.py::test_a_core_heated_from_above_warms_its_top_and_closes_its_heat` holds that call to $10^{-5}$. - -### Erosion case - -In the erosion case the CMB flow is 8 TW to 50 Myr, 25 TW to 150 Myr and 8 TW again to 400 Myr. The two models remove the layer in different ways. - -In aragog the face below the top cell mixes within the first Myr of the higher flow (at 51 Myr), and a mixed region then grows down from the CMB over the remaining stable part until the whole shell mixes at 92 Myr; the heat stored in the shell at 50 Myr, 1.22e+28 J, drains through the excess of the CMB flow over the conducted adiabatic flow, 9.31 TW on average, in 41.7 Myr against the 42 Myr from 50 to 92 Myr. - -In `leeds_thermal` the layer at the CMB conducts until its excess over the adiabat at the top reaches zero at 65 Myr, where its depth at 0.1 of that excess vanishes; its stability check removes the layer base at 66 Myr. - -The CMB temperature during erosion is a model difference, without a bound: from the rise of the flow aragog's mixed top stays warmer than the conducting top of `leeds_thermal`, by up to 19.6 K at 65 Myr, when the top excess of `leeds_thermal` reaches zero, and from 92 Myr until the flow falls at 150 Myr the two CMB temperatures stay 4.75 K apart (varying by 0.003 K), an offset that forms during erosion. - -Once its shell has mixed completely, aragog's centre temperature stays within 0.003 K of that of aragog without a layer under the same flows until the flow falls, against 9.06 K below it at the rise of the flow, so the heat stored in the shell returns to the convecting core. The centre temperatures of aragog and `leeds_thermal` differ by up to 5.43 K. After the flow falls back to 8 TW a new layer forms in both models, 702 km deep in aragog and 710 km in `leeds_thermal` at 399 Myr (Figure 13e, f). - -### Mixing constants - -The two mixing constants are a model uncertainty, not a fitted quantity. Changing `layer_k_mix` or `layer_g_mix` by a factor of 10 either way changes the centre temperature of the 8 and 12 TW cases by at most 0.17 K, which the last-bit change of the start temperature moves by 0.13 K, so the comparison does not resolve this sensitivity and these changes are not reproduced (the page test checks only that each setting changes the centre temperature, by less than 1 K), and their layer depth by at most 0.3 % in the run of the values file, which the tolerance of 1e-12 lowers to 0.12 % and which in the process runs of `tools/verification/data/stable_layer_spread.json` reaches 1.0 %, so this sensitivity is not resolved either; the time at which the eroding shell mixes completely moves by 0 Myr at the sampling of the reference table. - -### Limits and tests of the stable layer - -**Limits.** This comparison runs on SciPy BDF; in the solver, an eroding layer stalls CVODE at its default tolerances (see [the core boundary condition](core_bc.md)). The limits of each part are stated with it: the reproducibility of the centre temperatures, the unconverged comparison at the looser tolerance, the CMB temperature during erosion, and the unresolved sensitivity to the mixing constants. - -**Pinned by** `tests/test_core_verification_page.py::test_the_stable_layer_meets_the_thermal_history_bounds`; the analytic limits of the shell by `tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space`, `::test_a_steady_layer_reaches_the_quasi_static_depth` and `::test_mixing_keeps_a_superadiabatic_shell_on_the_adiabat`; a resumed solve by `tests/test_entropy_solver_core_module_smoke.py::test_a_resume_restarts_the_shell_where_the_previous_solve_ended`. - -## References - -- Anzellini, S., Dewaele, A., Mezouar, M., Loubeyre, P., & Morard, G. (2013). Melting of iron at Earth's inner core boundary based on fast X-ray diffraction. *Science*, 340(6131), 464-466. https://doi.org/10.1126/science.1233514 -- Christensen, U. R., Holzwarth, V., & Reiners, A. (2009). Energy flux determines magnetic field strength of planets and stars. *Nature*, 457(7226), 167-169. https://doi.org/10.1038/nature07626 -- Foley, B. J., & Driscoll, P. E. (2016). Whole planet coupling between climate, mantle, and core: Implications for rocky planet evolution. *Geochemistry, Geophysics, Geosystems*, 17(5), 1885-1914. https://doi.org/10.1002/2015GC006210 -- Greenwood, S., Davies, C. J., & Mound, J. E. (2021). On the evolution of thermally stratified layers at the top of Earth's core. *Physics of the Earth and Planetary Interiors*, 318, 106763. https://doi.org/10.1016/j.pepi.2021.106763 -- Labrosse, S., Poirier, J.-P., & Le Mouël, J.-L. (2001). The age of the inner core. *Earth and Planetary Science Letters*, 190(3-4), 111-123. https://doi.org/10.1016/S0012-821X(01)00387-9 -- Nimmo, F. (2015). Energetics of the Core. In *Treatise on Geophysics* (2nd ed., Vol. 8, pp. 27-55). Elsevier. https://doi.org/10.1016/B978-0-444-53802-4.00139-1 -- Nimmo, F. (2015). Thermal and Compositional Evolution of the Core. In *Treatise on Geophysics* (2nd ed., Vol. 9, ch. 9.08, pp. 201-219). Elsevier. https://doi.org/10.1016/B978-0-444-53802-4.00160-3 -- Thiriet, M., Breuer, D., Michaut, C., & Plesa, A.-C. (2019). Scaling laws of convection for cooling planets in a stagnant lid regime. *Physics of the Earth and Planetary Interiors*, 286, 138-153. https://doi.org/10.1016/j.pepi.2018.11.003 -- Wolf, A. S., & Bower, D. J. (2018). An equation of state for high pressure-temperature liquids (RTpress) with application to MgSiO3 melt. *Physics of the Earth and Planetary Interiors*, 278, 59-74. https://doi.org/10.1016/j.pepi.2018.02.004 +The core module verification is the [Core](../Validation/core.md) page of the Validation tab. diff --git a/docs/Explanations/cvode_jax.md b/docs/Explanations/cvode_jax.md index d1b00821..58b41965 100644 --- a/docs/Explanations/cvode_jax.md +++ b/docs/Explanations/cvode_jax.md @@ -53,4 +53,4 @@ Use `solver_method = "radau"` or `"bdf"` when: ## Verification -The JAX RHS must produce the same numerical result as the numpy `EntropyState.update` to floating-point precision. The verification suite asserts this for every public flux contribution; see [first-principles verification](verification.md) for the parity tests and [code architecture](code_architecture.md) for the file-level layout. +The JAX RHS must produce the same numerical result as the numpy `EntropyState.update` to floating-point precision. The verification suite asserts this for every public flux contribution; see [first-principles verification](../Validation/numerics.md) for the parity tests and [code architecture](code_architecture.md) for the file-level layout. diff --git a/docs/Explanations/jax_jacobian_factory.md b/docs/Explanations/jax_jacobian_factory.md index 716c0b0f..459b5ecd 100644 --- a/docs/Explanations/jax_jacobian_factory.md +++ b/docs/Explanations/jax_jacobian_factory.md @@ -53,7 +53,7 @@ The fallback is deliberate; an exception would make the JAX extra a hard depende ## Bit-parity with the numpy path -Every flux contribution in `aragog.jax.phase` is constructed to match the numpy `EntropyState.update` to floating-point precision. The verification suite asserts equality on a per-component basis (conduction, convection, gravitational separation, mixing) for representative `(P, S, T, mesh)` tuples; see [first-principles verification](verification.md). +Every flux contribution in `aragog.jax.phase` is constructed to match the numpy `EntropyState.update` to floating-point precision. The verification suite asserts equality on a per-component basis (conduction, convection, gravitational separation, mixing) for representative `(P, S, T, mesh)` tuples; see [first-principles verification](../Validation/numerics.md). Bit-parity is the contract that lets the JAX path be the production default without breaking a regression that was tuned against numpy. If the numpy path is being used as a "ground truth" and the JAX trace diverges, the parity tests catch the drift before it lands. diff --git a/docs/Explanations/verification.md b/docs/Explanations/verification.md index 9b403f23..53ba4a4b 100644 --- a/docs/Explanations/verification.md +++ b/docs/Explanations/verification.md @@ -1,116 +1,3 @@ # First-principles verification -Aragog ships a verification test suite that exercises the entropy solver against analytic limits, conservation laws, and SPIDER-parity references. The suite runs under `pytest` from the repository root and is the primary way to confirm a build is correct. - -## Test categories - -The tests are organised by the layer they cover: - -### EOS unit tests (`tests/test_entropy_pytest.py`) - -Exercise `EntropyEOS` and `EntropyPhaseEvaluator`: - -- Property lookups (temperature, density, heat capacity, thermal expansivity, isentropic temperature gradient) at on-grid and off-grid $(P, S)$ points. -- Solidus and liquidus entropy lookups against the reference `solidus_P-S.dat` and `liquidus_P-S.dat` files. -- Smooth-clipped melt fraction $\phi(P, S)$: monotone in $S$, clamped to $[0, 1]$, $C^\infty$ across the solidus and liquidus crossings. -- The two-stage SPIDER-parity blend (cubic-Hermite or tanh smoothing followed by `combine_matprop`) for density, heat capacity, and viscosity. -- The latent-blend $c_p$ method against the linear blend. - -![EOS bilinear-interpolation Jacobian](../figures/vv/fig_03_eos_bilinear_jacobian.png) - -**Figure 1.** JAX-traced bilinear interpolation of the EOS table reproduces the numpy reference to float-64 machine precision and supplies finite, non-zero derivatives via `jax.jacrev` at on- and off-grid $(P, S)$ points. The agreement is the prerequisite for the analytic Jacobian path used by CVODE. - -### Phase evaluator tests - -Test the SPIDER-parity behaviour of `EntropyPhaseEvaluator`: - -- Phase identification at $(P, S)$ in pure solid, pure liquid, and mushy regimes. -- Viscosity tanh blend across the rheological transition $\phi_\mathrm{rheo}$. -- Gravitational-separation velocity in viscous and inviscid regimes. -- Pressure-dependent latent heat $L(P) = T_\mathrm{liq}(P) [S_\mathrm{liq}(P) - S_\mathrm{sol}(P)]$. - -### Solver integration tests - -Drive `EntropySolver` through short integrations and check the resulting profiles: - -- Standalone grey-body cooling: a fully-molten initial state cools toward the equilibrium temperature without status flag failures. -- The `set_initial_entropy`, `set_initial_dSdr_cmb`, and `set_initial_core_temperature` paths produce the expected state-vector layouts for each of the five `core_bc` modes. -- The retry-ladder hooks (`get_current_dSdr_cmb`, `_atol_sf`) round-trip cleanly. - -### Conservation tests (`tests/test_entropy_verification.py`) - -The conservation tests are the strongest class of verification because they hold under any valid run, not just specific test scenarios: - -- **Energy conservation.** $E_\mathrm{th}$ change over a closed integration matches the time-integrated surface flux to within solver tolerance. -- **Mass conservation.** Mantle mass remains constant through phase changes. -- **Grey-body cooling.** Surface flux follows $\varepsilon\sigma(T_\mathrm{top}^4 - T_\mathrm{eqm}^4)$ within tolerance once the radiative regime is established. - -![Mass-coordinate Jacobian sanity](../figures/vv/fig_07_mass_coord_jacobian.png) - -**Figure 2.** Mass-coordinate mesh Jacobian. The Newton solve that maps from mass coordinate to spatial radius (used when `mass_coordinates = true`) is monotone, smooth, and consistent with the analytic Adams-Williamson density profile. Departures here would propagate to mis-aligned cell volumes and a spurious mass-conservation residual in the integrator. - -![Radioactive decay](../figures/vv/fig_06_radio_decay.png) - -**Figure 3.** Radioactive heating $H_\mathrm{radio}(t)$ for the bundled isotope cocktail (Ruedas 2017): present-day specific powers ($^{40}\mathrm{K}$, $^{232}\mathrm{Th}$, $^{235}\mathrm{U}$, $^{238}\mathrm{U}$) decayed back to Solar System formation. Used as the source-term verification for the closed-mantle energy balance: the cumulative `step_dE_Q_radio_J` integral must match the time integral of $\rho \cdot H_\mathrm{radio}$ over each call to within solver tolerance. - -### JAX path tests (`tests/test_jax_entropy.py`) - -Verify that the JAX-backed `EntropyEOS_JAX` and `compute_fluxes` agree with the numpy path: - -- EOS parity: `EntropyEOS_JAX` returns the same temperature, density, heat capacity, thermal expansivity, $\partial T/\partial P|_S$, and conductivity as `EntropyEOS` on the same $(P, S)$ inputs. -- Phase evaluator parity: the JAX two-stage blend matches `EntropyPhaseEvaluator` to within floating-point tolerance. -- JIT and `vmap` compatibility: the JAX flux assembly is differentiable end-to-end with `jax.grad`/`jax.jacrev` and survives JIT compilation without recompilation hot-spots. -- Boundary-copy tests: `dSdr` boundary values copy from the nearest interior node (SPIDER `ic.c:450` parity) instead of being extrapolated. -- Solver parity: integrating the same problem with the JAX-RHS-driven CVODE path and the numpy RHS produces matching trajectories. - -### Numpy ↔ JAX RHS parity at representative magma-ocean states - -![Numpy ↔ JAX RHS parity](../figures/vv/fig_01_rhs_parity.png) - -**Figure 4.** Numpy $\leftrightarrow$ JAX RHS parity on an 80-cell Earth mesh at three representative magma-ocean states. *Top row, panels (a-c)*: per-cell **absolute** residual $|\dot S_\text{numpy} - \dot S_\text{jax}|$ in J/kg/K/yr; the dotted line is $\epsilon_\mathrm{mach}\,\max|\dot S|$ (the floating-point round-off floor expected on a single FP operation). *Bottom row, panels (d-f)*: per-cell **relative** residual $|\dot S_\text{numpy} - \dot S_\text{jax}|/|\dot S_\text{numpy}|$; the dotted line is float-64 machine epsilon. The three states are (a/d) initial condition (full magma ocean, $S \approx 3900$ J/kg/K, dimensionally uniform), (b/e) mid-solidification ($S = 3300 \to 3700$ J/kg/K from CMB to surface), (c/f) near-solid ($S = 3000 \to 3300$ J/kg/K). - -Per-panel residuals: - -- **Mid-solidification (b, e) and near-solid (c, f).** Interior cells sit at the float-64 round-off floor. Maximum absolute residual $\sim 3\times 10^{-7}$ J/kg/K/yr, dominated by a single spike at the surface boundary cell where the BC assembly is re-derived independently between numpy and JAX. Median relative error $\le 3\times 10^{-5}$. -- **Uniform-entropy IC (a, d).** Absolute residual $\sim 1$ J/kg/K/yr. Both paths run EOS lookup, lever-rule, and table interpolation on a uniform input; the arithmetic differs at the interpolation level. The relative error inflates because $|\dot S|$ itself is small in equilibrium. - -Numpy and JAX paths are independent re-derivations, not a bit-exact port. - -![UTBL Cardano-formula consistency](../figures/vv/fig_05_utbl_cardano.png) - -**Figure 5.** Surface-temperature correction from the upper-thermal-boundary-layer parameterisation (`solver/boundary.py::_utbl_tsurf`). The Cardano-formula solution to $b x^3 + x - T_\mathrm{int} = 0$ recovers $T_\mathrm{surf} < T_\mathrm{int}$ for the entire physical range and matches the cubic-equation residual to floating-point tolerance. Verifies the analytic surface boundary condition used when `param_utbl = true`. - -### Mesh tests (`tests/test_jax_mesh_gravity_fallback.py`) - -Verify that when the external mesh file (`eos_method = 2`) supplies an `eos_gravity` column, the JAX path interpolates the per-node gravity profile onto the basic grid rather than broadcasting a single scalar. - -## Running the verification suite - -The standalone suite uses `pytest`: - -```sh -# Run the entire suite -pytest tests/ - -# Run a specific category -pytest tests/test_entropy_pytest.py -pytest tests/test_entropy_verification.py -pytest tests/test_jax_entropy.py - -# Mark-filtered subsets -pytest -m unit tests/ # unit tests only -pytest -m smoke tests/ # smoke tests only -``` - -The JAX-path tests require `jax` and `equinox` to be installed; tests are skipped (not failed) when the optional dependencies are missing. Some tests also require the SPIDER-format EOS table directory; they skip if `FWL_DATA` is unset or does not contain the expected files. - -## Physical constraints checked - -The suite asserts the following physical invariants on every solver-driven test run: - -1. **Temperature positivity.** $T > 0$ everywhere at all integration times. EOS lookups outside the table domain raise rather than silently extrapolating. -2. **Melt fraction bounds.** $0 \le \phi \le 1$ via the smooth-clip; the assertion runs at every staggered node. -3. **Pressure positivity and monotonicity.** $P(r)$ is positive and decreases monotonically from CMB to surface. -4. **Entropy gradient continuity.** $\partial S/\partial r$ and the resulting fluxes are $C^0$ across the solidus and liquidus crossings (the smoothing functions enforce this). -5. **Energy conservation.** Within the integrator tolerance, $\Delta E_\mathrm{th}$ matches the time-integrated boundary fluxes plus internal heating sources. -6. **Mass conservation.** Total mantle mass is preserved under gravitational separation and chemical mixing (the segregation flux is divergence-free in mass). +The first-principles verification is the [Numerics](../Validation/numerics.md) page of the Validation tab. diff --git a/docs/Reference/api/aragog.jax.md b/docs/Reference/api/aragog.jax.md index 52667811..29e60475 100644 --- a/docs/Reference/api/aragog.jax.md +++ b/docs/Reference/api/aragog.jax.md @@ -2,7 +2,7 @@ The `aragog.jax` package contains JAX-traceable replicas of the EOS, phase evaluator, and dSdt right-hand side. They are used to build an analytic Jacobian via `jax.jacrev` and feed it to SUNDIALS CVODE through the registered factory in `aragog.solver.cvode_jax`. -This module is loaded only when `solver.use_jax_jacobian = true` (the production default). The numpy path in `aragog.solver.entropy_state` remains the reference implementation and is exercised by the standalone tests; both must agree to numerical precision (see [first-principles verification](../../Explanations/verification.md)). +This module is loaded only when `solver.use_jax_jacobian = true` (the production default). The numpy path in `aragog.solver.entropy_state` remains the reference implementation and is exercised by the standalone tests; both must agree to numerical precision (see [first-principles verification](../../Validation/numerics.md)). For when and why to opt into the JAX path, see [CVODE and JAX-derived Jacobians](../../Explanations/cvode_jax.md). diff --git a/docs/Validation/core.md b/docs/Validation/core.md new file mode 100644 index 00000000..a98e0da2 --- /dev/null +++ b/docs/Validation/core.md @@ -0,0 +1,551 @@ +# Core module verification + +This page checks the `core_module` core boundary condition ([core_bc.md](../Explanations/core_bc.md)) and the `aragog.core` model behind it against analytic results, published values, an independent core evolution code and coupled runs. Each section states the physics a check tests, the reference and where it comes from, the value aragog gives, a figure and what it shows, the limits of the check where it has any, and the tests that pin it. A check shows that the code solves the stated equations to the stated accuracy; whether the equations describe a real core is a separate question, which the literature comparisons in sections 5 to 8 and the limits stated with each check address. + +Every number on this page and every figure is produced by `tools/verification/run_core_verification.py`, which writes the figures to `docs/figures/vv/` and the numbers to `docs/figures/vv/core_verification_values.json`; `tests/test_core_verification_page.py` checks each number on the page against that file. The script needs the SPIDER-format EOS tables (`ARAGOG_TEST_EOS_DIR`) for sections 9 and 12 and the optional `verification` dependencies for the figure style, and runs in about eight minutes on one core. Sections 9 and 12 and the solver tests use the aragog test tables that CI uses, `spider_eos_test_data.tar.gz` of release `test-data-v1` (sha256 `e3842bc494969571428d7f978d8f70972d40a3bc5d6ac2c09f6a96808f4079fb`), 500 pressures from 0 to 150 GPa by 200 entropies; their numbers hold for these tables. Other tables give other values; section 12 compares two sets of 1350 pressures by 280 entropies that reach 650 GPa. Two kinds of number come from separate measurements and say so where they appear: the closures of the stratified call heated from above (section 13), held by its smoke test, and the expansivity differences on those two sets. Nimmo (2015) is chapter 8.02 of the *Treatise on Geophysics* wherever chapter 9.08 is not named. + +## 1. Gaussian core structure + +**What is tested.** The core density is a Gaussian in radius, + +$$ +\rho(r) = \rho_\mathrm{cen} \exp\left(-\frac{r^2}{L^2}\right), +$$ + +with $\rho_\mathrm{cen}$ the central density and $L$ the length scale, and the adiabat solves + +$$ +\frac{d \ln T}{dr} = -\frac{\alpha g}{c_p}, +$$ + +with $\alpha$ the thermal expansivity, $g$ the gravity and $c_p$ the heat capacity. By default it uses the exact gravity (`adiabat_mode = "exact"`), + +$$ +T_a(r) = T_\mathrm{cmb} \exp\left(-\frac{\alpha\, \psi(r)}{c_p}\right), +$$ + +with $\psi$ the gravitational potential, zero at the CMB, on the Gauss-Legendre panel of the pressure. The closed form (`adiabat_mode = "small_radius"`) + +$$ +T_a(r) = T_\mathrm{cmb} \exp\left(\frac{r_\mathrm{cmb}^2 - r^2}{D^2}\right), \qquad D^2 = \frac{3 c_p}{2\pi \alpha \rho_\mathrm{cen} G}, +$$ + +(Labrosse et al. 2001, eqs. 7 and 11, the adiabat to the same order in radius as their density and gravity, eq. 8) holds for gravity linear in $r$; Nimmo (2015) and thermal_history use it, so sections 5, 6, 7, 10 and 13 do too. Mass and gravity are closed-form integrals of the density; the pressure is the hydrostatic integral of $\rho g$ from the CMB inward, by 32-point Gauss-Legendre quadrature (`pressure_mode = "quadrature"`), or the third-order expansion in $r/L$ that Labrosse et al. (2001, eq. 8) use for gravity (`pressure_mode = "labrosse"`). + +**Reference.** The checks run on the Earth-like test core ($\rho_\mathrm{cen} = 12500$ kg m$^{-3}$, $L = 7200$ km, $r_\mathrm{cmb} = 3480$ km). The references are an independent adaptive quadrature of the density, the linear limit of gravity near the centre, $4\pi G \rho_\mathrm{cen} r/3$, and hydrostatic balance, $dP/dr = -\rho g$. + +**Result.** The core has a mass of 1.92e+24 kg, a CMB density of 9896 kg m$^{-3}$ and a central pressure of 356.8 GPa. On this test core, $T_\mathrm{cen}/T_\mathrm{cmb}$ is 1.374 with the exact gravity and 1.405 in the closed form. + +| Check | Relative difference | +|---|---| +| Closed-form enclosed mass and gravity against the adaptive quadrature | 3.6e-13 | +| Gravity near the centre against the linear limit | 2.2e-16 | +| Hydrostatic balance of the pressure | 2.5e-08, the error of the 50 m central difference used to test it | +| Third-order pressure against the quadrature | up to 3.0e-03, the truncation of the expansion | + +Both pressures meet the CMB anchor exactly. + +![Gaussian core structure](../figures/vv/fig_08_core_structure.png) + +**Figure 1.** Gaussian core structure of the Earth-like test core. (a) Density, gravity and pressure against radius. (b) Relative residual of hydrostatic balance $|dP/dr + \rho g|/\rho g$ for the quadrature pressure, and the relative difference between the third-order and the quadrature pressure; both panels share the radius axis. + +**What the figure shows.** In (b) the residual of hydrostatic balance stays at or below the value of the result at every radius, and the difference of the third-order pressure is flat at its truncation level and falls to zero at the CMB, where both pressures are anchored. A pressure with a wrong slope would lift the residual by orders of magnitude. The pressure sets where the melting curve is evaluated, so the inner-core radius of a `core_module` run depends on it. + +**Pinned by** `tests/test_core_profiles.py::test_density_positive_monotone_and_mass_matches_quadrature`, `::test_gravity_centre_limit_and_shell_integral` and `::test_pressure_hydrostatic_balance_and_anchor`. + +## 2. Energy identities of the core budget + +**What is tested.** The core cools at + +$$ +\frac{dT_\mathrm{cmb}}{dt} = -\frac{Q_\mathrm{cmb} - Q_\mathrm{radio}}{\tilde C(T_\mathrm{cmb})}, \qquad \tilde C = C_s + C_L + C_g, +$$ + +where the effective capacity $\tilde C$ adds the secular term, the latent heat of inner-core growth and the gravitational energy of the light elements it rejects, $Q_\mathrm{cmb}$ is the CMB heat flow and $Q_\mathrm{radio}$ the radiogenic power. The heat content $H(T)$ is defined so that + +$$ +H(T_2) - H(T_1) = \int_{T_1}^{T_2} \tilde C\, dT; +$$ + +the solver books the core heat change from it, so this identity is what ties the core ledger to the CMB heat. Inner-core growth starts at the onset temperature $T_\mathrm{on}$, where the adiabat first touches the melting curve at the centre, and ends at freeze-out $T_\mathrm{fr}$, where the whole core has crystallised and the latent and gravitational terms vanish. + +**Reference.** The reference is the integral of $\tilde C$ by a trapezoid rule on 40001 points, on the test core of section 1 with the iron-alloy curve. The comparison covers 150 K on either side of the growth band, the CMB temperatures from the onset to the freeze-out, where the inner core grows. + +**Result.** For the iron-alloy curve on the test core, onset is at 4146.7 K and freeze-out at 3688.9 K. The heat-content difference matches the integral of $\tilde C$ to 6.6e-07 of the total; the residual above the onset is the trapezoid error of that reference at the square-root cusp of the latent term, not an error of $H$. At freeze-out the capacity drops by a factor of 3.143 for the Nimmo (2015) quadratic curve and by 50.5 % for the iron-alloy curve, without the gravitational term. + +![Energy identities](../figures/vv/fig_09_core_energy_identities.png) + +**Figure 2.** Effective heat capacity of the core across inner-core growth. (a) Secular, latent and gravitational capacities and their sum against CMB temperature, with the onset and freeze-out marked. (b) Relative difference between the heat-content difference and the integral of the total capacity at 120 temperatures, normalised by the total. + +**What the figure shows.** In (a) the latent and gravitational capacities are zero outside the growth band, rise steeply below the onset, and the latent one ends in a step at the freeze-out, where the total falls back to the secular capacity. In (b) the difference stays within the value of the result at every temperature. A heat content without the latent term would differ inside the band by a share of order one. The core ledger of a `core_module` run closes only if this identity holds. + +**Pinned by** `tests/test_core_budget.py::test_heat_content_difference_is_the_capacity_integral`, `::test_latent_and_gravitational_energy_conservation` (latent and gravitational capacities integrate to the geometric latent heat and gravitational energy to below 1e-6) and `::test_freeze_out_capacity_jump`. + +## 3. CMB boundary-layer flux + +**What is tested.** For a core hotter than the mantle side of the CMB, the CMB heat flux is + +$$ +q = \frac{k\, \Delta T}{\min(\Delta r_{1/2},\, \delta)}, \qquad \delta = \left(\frac{\kappa\, \eta\, Ra_\mathrm{crit}}{\rho\, g\, \alpha\, \Delta T}\right)^{1/3}, +$$ + +where $\Delta T = T_\mathrm{core} - T_m$, $\Delta r_{1/2}$ is the distance to the bottom mantle cell and $\delta$ is the boundary-layer thickness at the critical Rayleigh number (Foley & Driscoll 2016, eq. 20 and the text below it; Thiriet et al. 2019, eqs. 13 and 14). On the convective branch $q \propto \Delta T^{4/3}$; a core colder than the mantle gains heat by conduction across the half cell only, so the flux always has the sign of $\Delta T$. + +**Reference.** The law is evaluated for a bottom cell with constant properties, over a liquid and a solid base: + +| Property | Value | +|---|---| +| Conductivity $k$ | 4 W m$^{-1}$ K$^{-1}$ | +| Density $\rho$ | 4000 kg m$^{-3}$ | +| Heat capacity $c_p$ | 1000 J kg$^{-1}$ K$^{-1}$ | +| Expansivity $\alpha$ | $3\times10^{-5}$ K$^{-1}$ | +| Gravity $g$ | 10 m s$^{-2}$ | +| Half cell $\Delta r_{1/2}$ | 10 km | +| Viscosity $\eta$ of the liquid base | 0.1 Pa s | +| Viscosity $\eta$ of the solid base | $10^{21}$ Pa s | + +The default $Ra_\mathrm{crit} = 450$ is the critical value Thiriet et al. (2019, Table 2) use for the upper boundary layer of their mantle; for the lower boundary layer they scale it with the internal Rayleigh number (their eq. 16), so here it is a parameter, `ra_crit_cmb`. + +**Result.** The convective branch has the slope 1.333 in log-log and $Ra_\mathrm{crit} = 1800$ lowers the flux by the factor 0.630 $= (450/1800)^{1/3}$. At a 100 K contrast the flux is 5.55e+04 W m$^{-2}$ over the liquid base and 0.04 W m$^{-2}$ over a solid base, where conduction across the half cell sets it; a colder core conducts at the same rate over either base, to 4.2e-15. + +![CMB boundary-layer flux](../figures/vv/fig_10_cmb_boundary_layer_flux.png) + +**Figure 3.** Magnitude of the CMB heat flux against the core-mantle temperature contrast, for a liquid and a solid bottom cell with the properties of section 3, at $Ra_\mathrm{crit} = 450$ (solid) and 1800 (dashed); the dotted line is a core colder than the mantle, which conducts across the half cell over either base. + +**What the figure shows.** The two lines of the liquid base are straight and parallel on the logarithmic axes, with the slope of the convective branch, and the dashed line lies below the solid one by the factor of the result above. The line of the solid base and the dotted line of the colder core lie on each other with the slope 1 of conduction. A wrong exponent in $\delta$ would change the slope of the liquid-base lines, and a law without the conduction floor would put the solid-base line below the dotted one. This flux sets the cooling rate of the core in a `core_module` run. + +**Limits.** The properties of this check are constants; in the solver they come from the mantle phase model at the layer's mean temperature ([core_bc.md](../Explanations/core_bc.md)), and sections 9, 11 and 12 test the flux in the solver. The figure plots magnitudes, so it does not show the sign of the flux; the tests below hold the sign. + +**Pinned by** `tests/test_core_boundary_layer.py` (sign, hand-computed convective value, conduction branch, $Ra_\mathrm{crit}$ scaling, continuity and slopes through zero contrast). + +## 4. Inner-core nucleation + +**What is tested.** The inner-core radius $r_\mathrm{icb}(T_\mathrm{cmb})$ is the radius where the adiabat crosses the melting curve. The adiabat and the melting curve both have zero radial slope at the centre, so just below the onset their difference is quadratic in $r$ and + +$$ +r_\mathrm{icb} \propto (T_\mathrm{on} - T_\mathrm{cmb})^{1/2}: +$$ + +$r_\mathrm{icb}^2$ grows at a finite rate per kelvin, and $dr_\mathrm{icb}/dT_\mathrm{cmb}$ diverges at the onset. The budget's latent and gravitational capacities need $dr_\mathrm{icb}/dT_\mathrm{cmb}$, which the code supplies by the implicit-function theorem as a custom derivative rule. + +**Reference.** The references are the square-root law, which is the limit at the onset, and a central difference of the radius with a step of $10^{-3}$ of the undercooling. + +**Result.** Fitted over the first 0.1 K of undercooling, the log-log slope is 0.4998; over the first kelvin the next order of the expansion lowers the slope to 0.4989. The implicit-function derivative agrees with the central difference to 1.3e-06 from $10^{-3}$ to 30 K below the onset. + +![Inner-core nucleation](../figures/vv/fig_11_inner_core_nucleation.png) + +**Figure 4.** Inner-core growth below the onset. (a) Inner-core radius against undercooling, with the square-root law through the first point. (b) Relative difference between the implicit-function derivative and a central difference. + +**What the figure shows.** In (a) the radius lies on the dashed square-root law at small undercooling and falls below it at the largest undercooling shown, the next order of the expansion. In (b) the difference between the two derivatives is largest at the smallest undercooling, next to the onset where the derivative diverges. A wrong derivative rule would show in (b) as a difference of order one. The latent and gravitational capacities use this derivative, so the cooling rate of a core with an inner core depends on it. + +**Pinned by** `tests/test_core_verification_page.py::test_inner_core_radius_grows_as_the_square_root_of_undercooling` and `tests/test_jax_dsdt_core_module.py::test_jacobian_core_column_matches_central_differences`. + +## 5. Nimmo (2015) Earth core budget + +**What is tested.** The energy and entropy terms of the budget, the cooling rate and the inner-core growth rate, on a published parameter set for Earth's core. + +**Reference.** Nimmo (2015, Table 2, p. 42) gives one parameter set for Earth's core: + +| Input | Value | +|---|---| +| Central density | 12500 kg m$^{-3}$ | +| Length scale $L$ | 7272 km | +| CMB pressure | 139 GPa | +| Expansivity $\alpha$ | $1.25\times10^{-5}$ K$^{-1}$ | +| Conductivity $k$ | 130 W m$^{-1}$ K$^{-1}$ | +| Quadratic melting curve | $T_{m0} = 2677$ K, $T_{m1} = 2.95\times10^{-12}$ Pa$^{-1}$, $T_{m2} = 8.37\times10^{-25}$ Pa$^{-2}$ | +| Latent heat | 750 kJ kg$^{-1}$ | +| Light-element density jump | 560 kg m$^{-3}$ | + +Table 4 (p. 46) gives the present-day energy and entropy budget at $T_c = 4180$ K for CMB heat flows of 15.2 and 12 TW without radiogenic heat. Table 5 (p. 46) gives the energy released since the onset, $23\times10^{28}$ J (secular 11.6, latent 6.9, gravitational 4.4), and from it an inner-core age of 0.73 Gyr under a constant 10 TW. + +**Result.** On aragog's Gaussian core with the third-order pressure, these inputs put the inner-core boundary at 1209 km and 328.5 GPa at 4180 K, against the 1220 km and 328 GPa of Table 2. + +At 15.2 TW and 4180 K: + +| quantity | Nimmo (2015) | aragog | +|---|---|---| +| $Q_s$, $Q_L$, $Q_g$ (TW) | 6.1, 5.7, 3.4 | 6.06, 5.71, 3.43 | +| $Q_k$ (TW) | 15.0 | 14.97 | +| $E_s$, $E_L$, $E_g$, $-E_k$ (MW K$^{-1}$) | 183, 327, 809, 450 | 182, 330, 820, 451 | +| $-dT_c/dt$ (K Gyr$^{-1}$) | 104 | 103.3 | +| $dr_\mathrm{icb}/dt$ (km Gyr$^{-1}$) | 1050 | 1075 | + +Every energy and entropy term and the cooling rate agree to 1.3 percent at both heat flows (at 12 TW the cooling rate is 81.6 against 82 K Gyr$^{-1}$). The inner core grows 2.4 percent faster. The growth rate is $|dr_\mathrm{icb}/dT_c|$ times the cooling rate: aragog's $|dr_\mathrm{icb}/dT_c|$ is 10406 m K$^{-1}$ against $C_r = 10100$ m K$^{-1}$ (Table 2), a factor of 1.030, and its cooling rate is 0.994 of Table 4. The CMB temperature has dropped by 59.3 K since the onset, against about 60 K (p. 43), and the melting temperature at the centre is 5808 K against 5800 K (Table 5). + +Integrating aragog's capacities from 4180 K to the onset gives 22.2 (10.98, 6.83 and 4.42) $\times10^{28}$ J and 0.70 Gyr. The secular term, the secular capacity times the drop, is 5 percent lower, and the chapter's own numbers account for most of the gap: its Table 4 secular capacity, $Q_s$ over the cooling rate, times its 60 K drop gives 11.1 $\times10^{28}$ J, 4 percent below its Table 5 value, while aragog's capacity is 1.0003 times that of Table 4 and its drop is 59.3 K. + +The ages of Table 4, 0.59 and 0.75 Gyr, are not this integral: in our reading they are the drop since the onset times the present + +$$ +\tilde Q_T = \frac{Q_\mathrm{cmb}}{-dT_c/dt}, +$$ + +over the heat flow, which on the numbers of Table 4 ($4.6\times10^{27}$ J K$^{-1}$) and a 60 K drop gives 0.58 and 0.73 Gyr, within 3 percent of the table. The same quotient on aragog's state gives 0.574 and 0.727 Gyr, and the integral 0.464 and 0.587 Gyr, shorter because the latent and gravitational capacities rise from zero at the onset. + +The bound is 2 percent on every energy and entropy term of Table 4 and on the cooling rate, at both heat flows. + +![Nimmo (2015) budget](../figures/vv/fig_12_nimmo_budget.png) + +**Figure 5.** Ratio of aragog to Nimmo (2015, Table 4) at $T_c = 4180$ K for 15.2 and 12 TW: the energy and entropy terms, the cooling and inner-core growth rates and the inner-core age. Filled circles give aragog's age as the integral of its capacity to the onset, open circles as the drop since the onset over the present cooling rate. + +**What the figure shows.** The points of the eight energy and entropy terms and of the cooling rate lie close to the line at 1, at both heat flows; the growth rate lies above it. The filled circles of the age lie far below 1 and the open circles close to it: in our reading of the chapter, the gap comes from the definition of the age, not from the budget terms. A wrong term of the budget would move its point away from 1. These terms are computed from the capacities of the `core_module` budget, which set the core cooling and the inner-core growth of a run. + +**Limits.** Four quantities are documented exceptions to the bound, each held at its measured value: the inner-core growth rate (1.024 of Table 4, from $C_r$ and the cooling rate above), the inner-core age (0.786 and 0.783 of Table 4, which divides the drop by the present cooling rate), and $W_s$ and $W_\mathrm{tot}$ (0.947 and 0.967 of Table 5, from the chapter's own secular capacity above). Like the chapter, these numbers use the small-radius adiabat of section 1 (`adiabat_mode = "small_radius"`); the solver default integrates the exact gravity. + +**Pinned by** `tests/test_core_nimmo_benchmarks.py` (the adiabat length scale, ICB state and adiabatic heat flow of the two models in Nimmo 2015, ch. 9.08, Table 2, their $C_r$ from the caption of Figure 3, p. 211 (the value of model 2 is that of ch. 8.02, Table 2), and the ICB pressure and $\tilde Q_T$ of ch. 8.02), `tests/test_core_verification_page.py::test_the_earth_budget_meets_nimmo_tables_4_and_5` and `::test_page_numbers_match_the_values`. + +## 6. Budget terms against thermal_history + +**What is tested.** The secular, latent and gravitational capacities and the conduction entropy sink, each against an independent core evolution code along one thermal history. + +**Reference.** The `leeds` core model of thermal_history (Greenwood et al. 2021) computes the same budget with polynomial profiles, a trapezoid quadrature on a radial grid and its own inner-core growth. The comparison runs it on the Nimmo (2015, Table 2) core, with: + +- the Gaussian density and adiabat given to it as the first 8 terms of their Taylor series in $r^2$ (degree 14); +- the same quadratic melting curve without a light-element depression; +- a fixed latent heat of 750 kJ kg$^{-1}$; +- complete rejection of the light element at the inner-core boundary; +- the CODATA gravitational constant aragog uses. + +A core-only history starts at a CMB temperature of 4400 K under a fixed CMB heat flow of 10 TW; aragog's capacities are evaluated on each thermal_history state. Inputs, versions and licence are in `tools/verification/data/`. + +**Result.** The secular term agrees to 1.4e-08 and the conduction entropy sink to 1.5e-07; the latent term agrees to 6.1e-04. The gravitational term differs by up to 3.8 %, because thermal_history enriches the outer core in the light element as the inner core grows (by the factor 1.039 at the end), while aragog holds the composition fixed; with that factor applied the two agree to 6.1e-04. + +![Budget terms against thermal_history](../figures/vv/fig_13_leeds_budget_terms.png) + +**Figure 6.** Relative difference of aragog's secular, latent and gravitational capacities from those of the `leeds` model of thermal_history along its 10 TW history on the same inputs; the dotted line applies thermal_history's outer-core enrichment to aragog's gravitational term. + +**What the figure shows.** The secular difference is flat over the whole history, and the latent and gravitational differences start at the inner-core onset. The gravitational difference grows as the inner core grows, and the dotted line, with the enrichment applied, falls to the level of the latent line: the enrichment explains the gap. A wrong profile integral would put a term far above these levels from its first point. The cooling rate of a `core_module` core follows from the sum of these capacities. + +**Limits.** aragog holds the composition of the outer core fixed, so its gravitational term differs from that of a model with enrichment by the amount above. The stratified layer is compared with thermal_history in [section 13](#13-stable-layer-against-thermal_history). + +**Pinned by** `tests/test_core_verification_page.py::test_budget_terms_match_the_thermal_history_table`, and on the Nimmo (2015) state by `tests/test_core_nimmo_benchmarks.py::test_budget_terms_match_thermal_history_cross_check` and `tests/test_core_entropy.py::test_entropy_capacities_match_thermal_history`. + +## 7. Dynamo criterion and field strength + +**What is tested.** The entropy budget gives the entropy production available to the dynamo, + +$$ +\Delta E = E_s + E_L + E_g + E_R - E_k, +$$ + +positive when the core can sustain a dynamo. The field strength follows the energy-flux scaling of Christensen et al. (2009, eq. 2), + +$$ +\frac{\langle B \rangle^2}{2\mu_0} = c\, f_\mathrm{ohm}\, \langle \rho \rangle^{1/3} (F q_o)^{2/3}, +$$ + +with $c = 0.63$ and the efficiency factor + +$$ +F = 0.88\, \frac{\alpha\, g_\mathrm{cmb}\, r_\mathrm{cmb}}{c_p} \quad \text{or} \quad F = 0.45\, \frac{\alpha\, g_\mathrm{cmb}\, r_\mathrm{cmb}}{c_p}, +$$ + +the first for a constant total heat flow $4\pi r^2 q_c$, the second for a convected flux that vanishes at the outer boundary (p. 168). With $f_\mathrm{ohm} = 1$, the upper end of $f_\mathrm{ohm} \le 1$ (p. 167), the field is the largest the scaling allows for that flux. + +**Reference.** The references are the two efficiency factors that Christensen et al. print for their Earth inputs ($\alpha = 1.35\times10^{-5}$ K$^{-1}$, $g = 10.7$ m s$^{-2}$, $R = 3480$ km, $c_p = 840$ J kg$^{-1}$ K$^{-1}$), Nimmo's (2015, p. 46) minimum heat flow for a present-day dynamo without radiogenic heat, and the internal field of Earth from Christensen et al.'s dipole field of 0.26 mT at the dynamo surface and their ratio of about 7 between the total and the dipole field (p. 168). + +**Result.** With Christensen et al.'s Earth inputs the two factors are 0.527 and 0.269, against the printed 0.52 and 0.27; on the Nimmo (2015) core the constant-flux factor is 0.484, lower mainly through the lower expansivity ($1.25\times10^{-5}$ K$^{-1}$; the CMB gravity, 10.6 m s$^{-2}$, accounts for under 1 percent). On that core at $T_\mathrm{cmb} = 4180$ K the adiabatic heat flow is 14.97 TW and the entropy margin vanishes at 5.14 TW, against Nimmo's minimum of about 5 TW. Between these two heat flows the margin is positive but no flux is superadiabatic, so the field estimate is zero although the entropy budget allows a dynamo. At 17 TW the core field is 1.10 mT, the order of magnitude of the 1.82 mT for Earth. + +![Dynamo scaling](../figures/vv/fig_14_dynamo_scaling.png) + +**Figure 7.** Dynamo criterion and field strength on the Nimmo (2015) core at $T_\mathrm{cmb} = 4180$ K. (a) Entropy margin against CMB heat flow, with the threshold where it vanishes. (b) Volume-averaged core field from Christensen et al. (2009, eq. 2), with Earth's estimated internal field. The field is zero up to the adiabatic heat flow $Q_k$, 15.0 TW, so also from 5.1 TW, where the margin turns positive, to $Q_k$. + +**What the figure shows.** In (a) the margin rises with the CMB heat flow and crosses zero at the threshold. In (b) the field is zero up to $Q_k$ and rises steeply above it, and crosses the dashed line of Earth's estimate inside the plotted range. A wrong sign of the conduction sink $E_k$ would move the zero crossing in (a), and a field computed from the total heat flow would be nonzero below $Q_k$ in (b). The entropy margin and the field strength that PROTEUS writes for a `core_module` run come from the two relations of this section. + +**Limits.** Christensen et al. derive the efficiency factors for Earth's core with an inner core of 0.35 of the core radius, $L = D$, constant density and thermodynamic properties and gravity linear in radius (pp. 167 and 168); aragog applies them at every inner-core size. Their $q_o$ is the effective convected flux at the inner boundary, compositional driving included, scaled to the outer radius (p. 167); aragog takes the superadiabatic part of the CMB heat flow over the CMB area, a thermal flux only, so here $q_o$ departs from their definition. With $q_o$ defined differently, the field comparison is an order-of-magnitude check only. + +Without the resolved shell (`stratification = false`) the efficiency factors of $\Delta E$ use $T_\mathrm{cmb}$, the convecting adiabat extended to the CMB. Under a stratified layer the reference of the whole-core entropy balance is the actual CMB temperature (Greenwood et al. 2021, eq. 4 and p. 9), which aragog does not resolve without the shell and which lies between $T_\mathrm{cmb}$ and $T_a(r_s)$ at the base of the layer; $\Delta E$ is then the upper end of that bracket. With the resolved shell the reference is the temperature of the top shell cell ([core_bc.md](../Explanations/core_bc.md#resolved-stable-layer-experimental)). + +**Pinned by** `tests/test_core_entropy.py::test_chr09_efficiency_factors_reproduce_printed_values`, `::test_field_scaling_bounds_and_earth_magnitude` and `::test_dynamo_threshold_and_margin`. + +## 8. Iron melting curve + +**What is tested.** The pure-iron melting curve is the two-branch Simon-Glatzel fit of Anzellini et al. (2013, eqs. 2 and 3, p. 466); this is the PALEOS prescription. On the $\gamma$-Fe branch, with $T_0 = 1991$ K and $P_0 = 5.2$ GPa, + +$$ +\frac{P - P_0}{27.39} = \left(\frac{T_m}{T_0}\right)^{2.38} - 1, +$$ + +and on the $\epsilon$-Fe branch, from the $\gamma$-$\epsilon$-liquid triple point at 98.5 GPa and 3712 K, + +$$ +\frac{P - P_\mathrm{TP}}{161.2} = \left(\frac{T_m}{T_\mathrm{TP}}\right)^{1.72} - 1, +$$ + +with $P$ in GPa, $T_m$ the melting temperature and $P_\mathrm{TP}$ and $T_\mathrm{TP}$ the pressure and temperature of the triple point. The two branches meet with a jump at the triple point, which a $C^2$ smootherstep over a 3 GPa half-width removes. Light elements depress the curve by the factor $1 - \mathrm{depression} \times x$. + +**Reference.** The reference is the unblended fit: the two branches as printed, and the PALEOS source for the pure-iron values. + +**Result.** The jump of the two branches at the triple point is 0.725 K; the blended curve departs from the branches by at most 0.361 K and equals them exactly outside the blend. The depression factor is 0.88 for $x = 0.1$ and a depression of 1.2. The pure-iron curve gives 4192.0 K at 136 GPa and 6229.2 K at 330 GPa. + +![Iron melting curve](../figures/vv/fig_15_iron_melting_curve.png) + +**Figure 8.** Iron melting curve. (a) Pure-iron and alloy melting temperature against pressure, with the triple point marked. (b) Blended curve minus the unblended Anzellini et al. (2013) branches around the triple point; the jump is that of the unblended fit. + +**What the figure shows.** In (a) both curves rise smoothly through the triple point, and the alloy curve lies below the pure-iron curve at every pressure. In (b) the difference from the unblended branches is zero outside the blend and largest on both sides of the triple point; its jump there is the jump of the unblended fit, so the blended curve is continuous. Without the blend, the melting temperature would jump at the triple point, and so would the inner-core radius of a core whose inner-core boundary passes that pressure. The onset temperature and the inner-core radius of a `core_module` run with the iron curve follow from it. + +**Limits.** The depression is a model choice, not part of the Anzellini fit. + +**Pinned by** `tests/test_core_melting.py::test_pure_iron_pins_against_the_paleos_source`, `::test_branch_switch_matches_unblended_outside_transition` and `::test_branch_switch_deviation_and_c1_continuity`. + +## 9. A CVODE solve across the inner-core onset + +**What is tested.** The coupled right-hand side carries the core temperature as a state, so CVODE integrates the core through the onset, where the latent capacity rises as a square root. The check compares the core heat change that the solver books with the CMB heat that it books. + +**Reference.** The quadratic melting curve of section 5 at 1.5 times its $T_{m0}$ (4015.5 K) puts the onset above the mantle base. The call starts with a core 2 K above the onset over a partly molten mantle base and lasts 4 yr. The bound is $10^{-6}$ for a core without a layer solved alone. + +**Result.** The core cools from 6414.4 K to 6409.5 K, through the onset at 6412.4 K, and grows an inner core of 244 km. At a solver rtol of $10^{-10}$ (atol stays at its floor of $10^{-8}$) the solver books the core heat change as the heat-content difference, and that change matches the CMB heat it books to 5.7e-07, within the bound. The CMB heat reconstructed from the 71 output times by the trapezoid rule matches to 5.6e-04, the error of that sampling. + +![CVODE solve across the onset](../figures/vv/fig_16_cvode_onset_ledger.png) + +**Figure 9.** A CVODE solve through the inner-core onset. (a) Core temperature against time, with the onset temperature. (b) Core heat lost from the heat-content difference and from the time integral of the CMB heat flow. (c) Relative difference between the two curves in (b). + +**What the figure shows.** In (a) the core temperature crosses the onset temperature during the call. In (b) the two curves of the heat lost lie on each other, and (c) gives their difference, which is the sampling error of the trapezoid rule over the output times and is largest at the first outputs. A core temperature that did not follow the CMB heat flow through the onset would separate the curves in (b). The core energy ledger of a `core_module` run rests on this closure. + +**Limits.** At the default rtol of $10^{-8}$ the CMB heat matches to 1.5e-06, so a call across the onset needs the tighter tolerance to meet that bound. Panel (c) does not show the closure of the booked ledger, only the sampled reconstruction. + +**Pinned by** `tests/test_entropy_solver_core_module_smoke.py::test_core_module_cvode_solve_crosses_the_inner_core_onset` and `tests/test_core_verification_page.py::test_a_cvode_solve_across_the_onset_closes_its_heat`. + +## 10. A core thermal history against thermal_history + +**What is tested.** The core-only history of section 6 tests the integrated evolution rather than the terms. + +**Reference.** aragog integrates its cooling rate under the same fixed 10 TW CMB heat flow with LSODA (SciPy `solve_ivp`) at a relative tolerance of $10^{-10}$ and an absolute tolerance of $10^{-8}$ K, and thermal_history steps it with its own fixed 1 Myr steps. + +**Result.** Over 1499 Myr the CMB temperatures differ by at most 0.119 K. The centre temperatures, on the same adiabat, differ by at most 0.163 K, the CMB difference times the adiabat ratio, and the final inner-core radii by 0.13 %, within the bounds of 0.2 K for the CMB temperature, 1 K for the centre temperature and 1 % for the radius. + +| Quantity | aragog | thermal_history | +|---|---|---| +| Inner-core nucleation (Myr) | 911 | 911, on its 1 Myr grid | +| Final inner-core radius (km) | 1122.8 | 1121.4 | + +The inner-core age is 588 Myr in both. + +Before the onset the CMB temperatures agree to 2.2e-06 K. The difference appears at the onset, 6.5e-04 K at the first step, rises to 0.024 K, changes sign at 1196 Myr and reaches its largest magnitude at the end, at 1499 Myr (aragog minus thermal_history throughout). + +![Core thermal history against thermal_history](../figures/vv/fig_17_leeds_thermal_history.png) + +**Figure 10.** Core-only thermal history under a fixed 10 TW CMB heat flow. (a) CMB temperature from aragog and thermal_history. (b) Inner-core radius. (c) Absolute difference of the CMB temperatures. + +**What the figure shows.** In (a) and (b) the two models lie on each other; the cooling slows where the inner core starts to grow. In (c) the difference is small before the onset, jumps at the onset, passes through zero at the sign change and is largest at the end. A wrong secular capacity would give a difference that grows from the start of the history, not from the onset. The CMB temperature and the inner-core radius are the outputs of the core model that a `core_module` run reports. + +**Limits.** We attribute the jump at the onset to thermal_history's explicit step, which crosses the start of the latent release in one step, and the later drift to the outer-core enrichment of section 6, which shifts the gravitational term; neither cause is tested separately. + +**Pinned by** `tests/test_core_verification_page.py::test_core_history_matches_the_thermal_history_table`. + +## 11. Coupled PROTEUS case + +**What is tested.** Coupled PROTEUS runs test the sign of the CMB flux against the core-mantle temperature contrast, the closure of the core ledger against the CMB heat, and the dependence of the CMB flux on the mantle mesh. + +**Reference.** A coupled PROTEUS run of a 1 Earth-mass planet that starts fully molten runs to mantle solidification twice, once with `core_bc = "core_module"` and once with `energy_balance`, whose CMB flux follows the mantle-side entropy gradient. The configuration is `tools/verification/data/coupled_config.toml`: + +- Zalmoxis structure; +- the CVODE solver; +- the PALEOS iron curve with a light-element depression; +- no stratification; +- the PALEOS-2phase MgSiO$_3$ mantle tables. + +The same configuration at 3 and 5 Earth masses (`planet.mass_tot`) and at 1 Earth mass with `interior_energetics.rtol = 1e-10` gives the core ledgers in `tools/verification/data/coupled_ledger_*.csv`. + +The coupled tables of the plotted columns are in `tools/verification/data/`, each with the PROTEUS and aragog commits of its run in its header. The `core_module` tables come from an inversion of the layer's temperature that agrees with the one described in [the core boundary condition](../Explanations/core_bc.md) to rounding, except for a layer mean temperature within about $3 \times 10^{-11}$ K of a phase-boundary temperature, where the EOS tables hold the temperature flat. + +### Flux sign + +With `core_module` no row of the 1 Earth-mass run has a CMB flux against the core-mantle temperature contrast. The same holds for the 3 and 5 Earth-mass runs and for a 1 Earth-mass run with the Wolf and Bower (2018) mantle. The table gives the rows of each run, the rows with a flux against the contrast (wrong sign), the largest contrast, the rows with zero flux and the contrast of those rows: + +| Run | Rows | Wrong sign | Largest contrast (K) | Zero flux | Contrast at zero flux (K) | +|---|---|---|---|---|---| +| 1 $M_\oplus$ | 661 | 0 | 823 | 1 | 4.8e-08 | +| 3 $M_\oplus$ | 795 | 0 | 1928 | 1 | 0 | +| 5 $M_\oplus$ | 695 | 0 | 2428 | 1 | 1.0e-06 | +| 1 $M_\oplus$, Wolf and Bower (2018) mantle | 217 | 0 | 712 | 1 | 0 | + +`coupled_flux_.csv` holds the rows of the last three runs. + +With `energy_balance` the core runs 7.7 to 10.0 K colder than the mantle side of the CMB, and the flux carries heat out of it on all 681 rows. + +With the boundary layer's properties at its mean temperature, the `core_module` core follows the mantle: it cools to 5251 K while the mantle side of the CMB cools to 4428 K, which gives the largest contrast of its row in the table, and the CMB flux is still 8.0 W m$^{-2}$ at 500 kyr. The mantle reaches a melt fraction of 0.05 at 661 kyr with `core_module` and at 665 kyr with `energy_balance`, which feeds core heat into the mantle throughout. + +![Coupled PROTEUS case](../figures/vv/fig_18_coupled_proteus.png) + +**Figure 11.** A coupled PROTEUS run to mantle solidification with the `core_module` and the `energy_balance` core boundary conditions. (a) Core temperature in both runs and the mantle side of the CMB, which cools alike in both. (b) CMB heat flux; crosses mark rows where the flux carries heat against the core-mantle temperature contrast. + +**What the figure shows.** In (a) the `energy_balance` core lies on the mantle side of the CMB, while the `core_module` core stays hotter and cools more slowly. In (b) every row of the `energy_balance` line has a cross and no row of the `core_module` line has one; the `core_module` flux starts at zero, where the core starts at the mantle temperature. A flux law with a wrong sign would put crosses on the `core_module` line. The sign of the CMB flux decides whether the core heats or cools the mantle base of a run. + +### Core ledger closure + +The table gives the cumulative residual of the core ledger relative to the cumulative CMB heat, the share of the summed absolute call residuals that comes from the call from 22 to 122 yr, and the bound for each run. The bound applies to the cumulative residual after the first kyr and at the end. + +| Run | residual at the end | largest after 1 kyr | share of the call from 22 to 122 yr | bound | +|---|---|---|---|---| +| 1 $M_\oplus$, rtol $10^{-8}$ | 1.5e-07 | 1.9e-07 | 0.04 | $10^{-6}$ | +| 1 $M_\oplus$, rtol $10^{-10}$ | 4.7e-08 | 6.1e-08 | 0.08 | $10^{-6}$ | +| 3 $M_\oplus$, rtol $10^{-8}$ | 1.8e-07 | 2.4e-07 | 0.06 | $5 \times 10^{-6}$ | +| 5 $M_\oplus$, rtol $10^{-8}$ | 1.5e-07 | 2.0e-07 | 0.09 | $5 \times 10^{-6}$ | + +The call from 22 to 122 yr gives 4 to 9 percent of the summed call residuals. The bound is $5 \times 10^{-6}$ for the last two runs of the table and $10^{-6}$ for the 1 Earth-mass runs and for a core without a layer solved alone (section 9). + +The first call of nonzero length, from 1 to 2 yr, closes to 1.5e-04 of its own CMB heat at 1 Earth mass and to 1.0e-04 to 1.1e-04 in the other runs; the source of this error is not established, and the test holds it below $3 \times 10^{-4}$. `tests/test_entropy_solver_core_module_smoke.py::test_chained_cvode_calls_close_the_core_ledger` makes calls ending at the same times on a core solved alone and closes each to $10^{-6}$; it does not reproduce this error, and the bounds of the table are checked on the stored runs, not on the current solver. + +### Cores without an inner core and the top-down case + +None of the 1, 3 and 5 Earth-mass runs grows an inner core. The cores are coldest at the end of each run, above their inner-core onsets (the budget rebuilt from the core profile of each run in `coupled_core_structure.csv`), so these runs test the secular capacity only: + +| Run | Core temperature at the end (K) | Inner-core onset (K) | +|---|---|---| +| 1 $M_\oplus$ | 5251 | 4028 | +| 3 $M_\oplus$ | 6902 | 4598 | +| 5 $M_\oplus$ | 8187 | 4266 | + +Section 9 takes a CVODE solve through the onset, and `tests/test_entropy_solver_core_module_smoke.py::test_a_cvode_call_through_onset_and_freeze_out_closes_its_heat` takes one through the onset and freeze-out, with an entropy of fusion of 0.17 J kg$^{-1}$ K$^{-1}$ and no gravitational term, so that one call crosses the whole band. + +With the Earth values of the thermal expansivity and the heat capacity in the core adiabat (below), at 3 and 5 Earth masses the melting curve reaches the adiabat at the CMB, at 5217 K and 6865 K, before it reaches it at the centre, so these cores would freeze from the top down; the order of the two temperatures depends on that adiabat. The budget books latent and gravitational heat only for an inner core that grows from the centre, so a call that enters a top-down or snow regime raises an error; the energetics of those regimes are not modelled. `::test_a_core_that_freezes_from_the_top_is_refused` takes a solver call into the top-down regime, and `tests/test_core_nimmo_benchmarks.py::test_model1_printed_parameters_break_bottom_up_topology` gives a snow state to the guard that the solver calls. The runs end above these temperatures. + +### Pressures and extrapolation + +| Run | CMB pressure (GPa) | Centre pressure (GPa) | +|---|---|---| +| 1 $M_\oplus$ | 103 | 341 | +| 3 $M_\oplus$ | 298 | 1045 | +| 5 $M_\oplus$ | 516 | 1837 | + +The structure EOS of the core is the PALEOS iron table, which spans pressures up to 100 TPa and so covers these. The melting curve is the fit of section 8, to melting points that Anzellini et al. (2013) measured from 50 to 200 GPa: it is an extrapolation at the centre of every run and at the CMB of the 3 and 5 Earth-mass runs, and so are the onset and freeze-out temperatures that follow from it there. The budget uses the Earth values of the thermal expansivity, $1.35 \times 10^{-5}$ K$^{-1}$, and the heat capacity, 840 J kg$^{-1}$ K$^{-1}$, at every mass. + +### Mesh dependence of the CMB flux + +The 1 Earth-mass `core_module` run at 40, 80, 160 and 320 mantle levels (`interior_energetics.num_levels`), and at 80 levels with `interior_energetics.rtol = 1e-10`, tests how the CMB flux depends on the mantle mesh through basal freezing, the time $t_\mathrm{bf}$ at which the melt fraction of the lowest mantle node falls through 0.5 (`coupled_mesh_convergence.csv`). At 320 levels basal freezing comes at 1327 yr, with a CMB flux of 7.48e+04 W m$^{-2}$. + +From 160 to 320 levels the changes are within the bounds of 5 % for the times and fluxes and 10 K for the temperatures: + +| Quantity | Change from 160 to 320 levels | +|---|---| +| $t_\mathrm{bf}$ | 0.62 % | +| Core temperature at $t_\mathrm{bf}$ | 1.34 K | +| Core temperature at the end | 0.11 K | +| CMB flux at 10 yr | 0.029 % | +| CMB flux at 100 yr | 0.0044 % | +| CMB flux at 1000 yr | 0.041 % | +| CMB flux at $t_\mathrm{bf}$ | 0.80 % | + +After basal freezing the flux converges too: at twice and four times $t_\mathrm{bf}$ and at 10, 100 and 500 kyr it changes by at most 1.1 % from 160 to 320 levels, by at most 4.0 % from 80 to 160 and by at most 7.8 % from 40 to 80; a flux set by conduction across the half cell would double with each doubling. At 500 kyr it is 8.27, 7.99, 8.16 and 8.12 W m$^{-2}$ at 40, 80, 160 and 320 levels, and the core cools by 370 to 376 K from $t_\mathrm{bf}$ to the end. + +The tighter tolerance at 80 levels changes the bounded fluxes and times by at most 3.1e-05 and the temperature at $t_\mathrm{bf}$ by 0.0054 K. The flux of a row is the mean over the call that ends at it, and the fluxes at a given time are linear between the row times; at 100 kyr the calls of the two runs last 1.1 and 0.16 kyr, so their fluxes there differ by 0.4 %, against 5.0e-05 with each call mean placed at the middle of its call (from the CMB heat of each call in `coupled_ledger_1me*.csv`). + +### Limits and tests of the coupled case + +**Limits.** The liquid heat capacity of the PALEOS-2phase mantle tables is too large ([PALEOS issue 6](https://github.com/maraattia/PALEOS/issues/6)), so these runs test the core ledger and the flux sign, not the mantle's cooling history. The limits of each part are stated with it: the first-call error of the ledger, the secular capacity as the only capacity tested, the top-down case, and the extrapolation of the melting curve. + +**Pinned by** `tests/test_core_verification_page.py::test_coupled_tables_reproduce_the_page`, which recomputes these numbers from the tables and holds them to the bounds above; the runs themselves need PROTEUS and are not repeated by the test suite. + +## 12. NumPy and JAX right-hand sides and the analytic Jacobian + +**What is tested.** The solver evaluates the coupled right-hand side in NumPy and, for CVODE's analytic Jacobian, in JAX; the two must agree, and the JAX Jacobian must equal the derivative of that right-hand side. The two evaluate the same equations except for the single-phase thermal expansivity: NumPy reads it from the `thermal_exp` tables, JAX derives it from the identity + +$$ +\alpha = \frac{\rho c_p}{T} \left|\frac{\partial T}{\partial P}\right|_S. +$$ + +**Reference.** Each right-hand side is the reference of the other, and a central difference of $\dot T_\mathrm{core}$ is the reference of the analytic Jacobian entry $\partial \dot T_\mathrm{core}/\partial T_\mathrm{core}$. The comparison runs on a 10-node mesh with the core in three states (inside the growth band, 100 K above the onset, and stratified with the core 50 K above the mantle), with the NumPy expansivity from the identity as well. These states put the mantle cells in the mush, where the composite expansivity applies and the tables do not enter; a fourth comparison puts the mantle above its liquidus at every node. + +**Result.** + +| Comparison | Relative difference | +|---|---| +| Right-hand sides, largest over the components, first two states | 1.4e-12 | +| Right-hand sides, largest over the components, with the layer, whose core rate is ill-conditioned (section 13) | 2.0e-10 | +| Right-hand sides over a liquid mantle, largest over the components, with the identity | 2.8e-14 | +| Right-hand sides over a liquid mantle, largest over the components, with the tables | 1.7e-04 | +| Jacobian entry against a central difference at the best step, in the growth band | 1.6e-11 | +| Jacobian entry against a central difference at the best step, with the layer | 1.5e-11 | + +The difference with the tables is the difference of the two single-phase expansivities on these tables, which NumPy reads from the tables for the mantle cells and for the boundary layer when they are present; on the two 1350 by 280 sets the same comparison, run separately from the script, gives $6.1 \times 10^{-5}$ and $2.1 \times 10^{-5}$. The best step is the one that balances truncation and rounding; above the onset the core is colder than the mantle base, the flux is on its linear conduction branch, and the difference stays at rounding for every step. + +![NumPy and JAX parity](../figures/vv/fig_19_numpy_jax_parity.png) + +**Figure 12.** Agreement of the NumPy and JAX core right-hand sides. (a) Relative difference per state component, the entropy cells first and the CMB entropy gradient and core temperature last, in three core states. (b) Relative error of a central difference of $\dot T_\mathrm{core}$ against the analytic Jacobian entry, against the difference step. + +**What the figure shows.** In (a) the stratified state has the most components, its shell cells, and the largest differences, at the level of the table above; the lines of the two other states end after the entropy cells and the two boundary slots. In (b) the error of the central difference falls with the step to a minimum and rises again at small steps, where rounding takes over; above the onset it stays at rounding. A wrong Jacobian entry would give an error that does not fall with the step. With `use_jax_jacobian` set, as in PROTEUS, CVODE builds the implicit steps of a `core_module` run on this Jacobian. + +**Limits.** In the three core states the CMB flux is the conduction across the half cell, and in runs separate from the script the numbers of this section do not change when the expansivity of the boundary layer is multiplied by 1.5 in both NumPy and JAX; `tests/test_jax_dsdt_core_module.py::test_jacobian_core_column_matches_central_differences` checks the boundary-layer flux instead, with the Jacobian columns of the core temperature and the bottom cell against central differences for a core 300 K above a base at melt fraction 0.66, where the test asserts a flux above $10^4$ W m$^{-2}$. + +**Pinned by** `tests/test_jax_dsdt_core_module.py::test_boundary_slots_match_numpy_on_a_five_node_mesh`, `::test_jacobian_core_column_matches_central_differences` and `::test_jacobian_carries_boundary_couplings`. + +## 13. Stable layer against thermal_history + +**What is tested.** The resolved layer of `stratification = true` is compared with the `leeds_thermal` model of thermal_history (Greenwood et al. 2021) on the core of section 6, which starts on the adiabat at a CMB temperature of 4400 K. The conducted adiabatic flow at the CMB is 15.8 TW. Under a CMB flow below it, the top of the core takes up more heat by conduction along the adiabat than the CMB removes, and a warm stable layer forms; the CMB removes 8 or 12 TW, adds 2 TW, or removes nothing. Under a flow above it, an existing layer erodes. + +**Reference.** aragog resolves the outer core above 0.4 $r_\text{cmb}$ in 64 cells with the default mixing constants; mixing acts only where the temperature falls outward faster than the adiabat, so a subadiabatic region conducts only. `leeds_thermal` instead moves the base of its layer by a stability check after each step. The layer depth is defined the same way on both sides: the depth below the CMB where the excess over the convecting adiabat falls to 0.1 of its value at the top, which for aragog is the centre of its top cell. The CMB temperature of aragog that is compared with `leeds_thermal` is the temperature of that cell (`core_T_top`), not the state $T_\text{cmb}$ of the convecting adiabat. aragog integrates the convecting core and the shell alone with SciPy BDF at a relative tolerance of 1e-11. + +In the four cases with a fixed flow, until the inner core of `leeds_thermal` forms at 588 Myr, its centre temperature differs by at most 0.33 K between the flows and its layer depth from 200 Myr on by at most 1.4 %; the CMB temperature depends on the flow throughout. + +![Stable layer against thermal_history](../figures/vv/fig_20_leeds_stable_layer.png) + +**Figure 13.** The resolved stable layer of aragog (solid) against the `leeds_thermal` model of thermal_history (dashed). (a) Layer depth at 0.1 of the top excess over the adiabat, (b) centre temperature and (c) inner-core radius under a CMB flow of 8 and 12 TW to 999 Myr; (d) CMB temperature under -2 and 0 TW. (e) CMB temperature and (f) layer depth in the erosion case to 399 Myr; the shaded window runs from the rise of the flow at 50 Myr to the time aragog's shell mixes completely, and aragog's depth is not drawn while the mixing acts below its top cell, where the depth at 0.1 of the top excess has no meaning. + +**What the figure shows.** In (a) to (d) the solid and dashed lines lie close to each other: the layer grows until the inner core forms and thins after it, and the CMB warms under the two flows of (d). In (e) and (f) the models differ inside the shaded window, where each removes the layer in its own way, and form a new layer of similar depth after the flow falls back. A shell that lost or gained heat would separate the centre temperatures in (b). The layer base (`core_layer_base`) and the top temperature (`core_T_top`) that a stratified `core_module` run reports come from this shell. + +### Fixed flows + +| CMB flow | max $\lvert\Delta T_\text{cmb}\rvert$ (K) | max $\lvert\Delta T_\text{cen}\rvert$ (K) | depth ratio | onset aragog (thermal_history) (Myr) | $r_\text{icb}$ at 999 Myr | +|---|---|---|---|---|---| +| 8 TW | 2.88 | 0.62 | 0.79 to 1.13 | 590 (588) | 0.20 % | +| 12 TW | 2.23 | 0.62 | 0.82 to 1.39 | 589 (588) | 0.05 % | +| -2 TW | 4.11 | 1.24 | 0.79 to 1.01 | 592 (589) | 0.24 % | +| 0 TW | 3.88 | 0.93 | 0.79 to 1.01 | 591 (589) | 0.15 % | + +For the four fixed flows the bounds are: + +- 5 K on the CMB temperature; +- 10 K on the centre temperature; +- a factor of 1.5 on the layer depth where both layers are thicker than 10 km after the first Myr; +- 2 % on the inner-core radius at the last sample (999 Myr); +- 5 % on its onset time. + +The extremes of the depth ratio fall in the first 51 Myr, while the layer is thin; from 300 Myr on the two depths agree to within 1.5 % (Figure 13a). The CMB temperature difference reaches its largest value between 36 and 69 Myr, with aragog colder. aragog reports its top cell as the CMB temperature; the centre of that cell lies 1000 m below the CMB, where the adiabat of the starting core is 0.80 K above the CMB temperature, so aragog starts that much warmer. + +### Tolerance and reproducibility + +Where BDF stalls, the integration restarts from its last accepted state; the shell runs of this section, at every tolerance and mixing constant, restart 1 time in all, a count that can differ between platforms. Between runs at 1e-11 and 1e-12 the centre temperature differs by at most 0.27 K. + +In runs separate from the script, whose values, commits and platforms are in `tools/verification/data/stable_layer_spread.json`, that difference comes from the -2 TW case: a start temperature changed in its last bit lowers its largest centre-temperature difference from thermal_history from 1.24 K to 0.99 K, the tolerance of 1e-12 to 0.99 K, and the two changed runs agree to 0.008 K. In the other cases the last-bit change moves that difference by 0.024 K or less, the inner-core radii by 2.8e-05 of themselves or less (8.3e-05 at -2 TW) and the largest late depth difference by 0.075 percentage points. On Linux the 8 TW difference of the table above is 0.69 K, 0.07 K above the value here. The centre-temperature differences therefore reproduce only to within changes of up to 0.25 K, and the page test holds them to 0.6 K at -2 TW and to 0.2 K in the other cases. + +At a relative tolerance of 1e-8 the comparison has not converged: in the run of the values file the centre temperature then differs from thermal_history by up to 8.7 K and the inner-core onset by up to 26 Myr. These two differences depend on the platform and on the tests run before in the same process: the process runs in `tools/verification/data/stable_layer_spread.json` reach 20 K and 61 Myr. + +The heat rates of the core and the shell sum to the CMB flow at every sample to 5.0e-16 of the conducted adiabatic flow or better, in every case; this is a property of the discretisation (the shell fluxes telescope and the core uses one effective capacity), not of the integration. + +Coupled to the mantle solver, a 5 yr CVODE call that heats a stratified core from above closes its heat against the CMB heat, in runs separate from the script, to $1.9 \times 10^{-6}$ or $8.2 \times 10^{-6}$, a choice a rounding-level change of the input makes ($7.7 \times 10^{-6}$ or $8.2 \times 10^{-6}$ with the exact adiabat and the boundary-layer properties at the mean temperature), because the stratified core rate moves by $1.5 \times 10^{-6}$ of itself under a $10^{-14}$ change of the shell temperatures; `tests/test_entropy_solver_core_module_smoke.py::test_a_core_heated_from_above_warms_its_top_and_closes_its_heat` holds that call to $10^{-5}$. + +### Erosion case + +In the erosion case the CMB flow is 8 TW to 50 Myr, 25 TW to 150 Myr and 8 TW again to 400 Myr. The two models remove the layer in different ways. + +In aragog the face below the top cell mixes within the first Myr of the higher flow (at 51 Myr), and a mixed region then grows down from the CMB over the remaining stable part until the whole shell mixes at 92 Myr; the heat stored in the shell at 50 Myr, 1.22e+28 J, drains through the excess of the CMB flow over the conducted adiabatic flow, 9.31 TW on average, in 41.7 Myr against the 42 Myr from 50 to 92 Myr. + +In `leeds_thermal` the layer at the CMB conducts until its excess over the adiabat at the top reaches zero at 65 Myr, where its depth at 0.1 of that excess vanishes; its stability check removes the layer base at 66 Myr. + +The CMB temperature during erosion is a model difference, without a bound: from the rise of the flow aragog's mixed top stays warmer than the conducting top of `leeds_thermal`, by up to 19.6 K at 65 Myr, when the top excess of `leeds_thermal` reaches zero, and from 92 Myr until the flow falls at 150 Myr the two CMB temperatures stay 4.75 K apart (varying by 0.003 K), an offset that forms during erosion. + +Once its shell has mixed completely, aragog's centre temperature stays within 0.003 K of that of aragog without a layer under the same flows until the flow falls, against 9.06 K below it at the rise of the flow, so the heat stored in the shell returns to the convecting core. The centre temperatures of aragog and `leeds_thermal` differ by up to 5.43 K. After the flow falls back to 8 TW a new layer forms in both models, 702 km deep in aragog and 710 km in `leeds_thermal` at 399 Myr (Figure 13e, f). + +### Mixing constants + +The two mixing constants are a model uncertainty, not a fitted quantity. Changing `layer_k_mix` or `layer_g_mix` by a factor of 10 either way changes the centre temperature of the 8 and 12 TW cases by at most 0.17 K, which the last-bit change of the start temperature moves by 0.13 K, so the comparison does not resolve this sensitivity and these changes are not reproduced (the page test checks only that each setting changes the centre temperature, by less than 1 K), and their layer depth by at most 0.3 % in the run of the values file, which the tolerance of 1e-12 lowers to 0.12 % and which in the process runs of `tools/verification/data/stable_layer_spread.json` reaches 1.0 %, so this sensitivity is not resolved either; the time at which the eroding shell mixes completely moves by 0 Myr at the sampling of the reference table. + +### Limits and tests of the stable layer + +**Limits.** This comparison runs on SciPy BDF; in the solver, an eroding layer stalls CVODE at its default tolerances (see [the core boundary condition](../Explanations/core_bc.md)). The limits of each part are stated with it: the reproducibility of the centre temperatures, the unconverged comparison at the looser tolerance, the CMB temperature during erosion, and the unresolved sensitivity to the mixing constants. + +**Pinned by** `tests/test_core_verification_page.py::test_the_stable_layer_meets_the_thermal_history_bounds`; the analytic limits of the shell by `tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space`, `::test_a_steady_layer_reaches_the_quasi_static_depth` and `::test_mixing_keeps_a_superadiabatic_shell_on_the_adiabat`; a resumed solve by `tests/test_entropy_solver_core_module_smoke.py::test_a_resume_restarts_the_shell_where_the_previous_solve_ended`. + +## References + +- Anzellini, S., Dewaele, A., Mezouar, M., Loubeyre, P., & Morard, G. (2013). Melting of iron at Earth's inner core boundary based on fast X-ray diffraction. *Science*, 340(6131), 464-466. https://doi.org/10.1126/science.1233514 +- Christensen, U. R., Holzwarth, V., & Reiners, A. (2009). Energy flux determines magnetic field strength of planets and stars. *Nature*, 457(7226), 167-169. https://doi.org/10.1038/nature07626 +- Foley, B. J., & Driscoll, P. E. (2016). Whole planet coupling between climate, mantle, and core: Implications for rocky planet evolution. *Geochemistry, Geophysics, Geosystems*, 17(5), 1885-1914. https://doi.org/10.1002/2015GC006210 +- Greenwood, S., Davies, C. J., & Mound, J. E. (2021). On the evolution of thermally stratified layers at the top of Earth's core. *Physics of the Earth and Planetary Interiors*, 318, 106763. https://doi.org/10.1016/j.pepi.2021.106763 +- Labrosse, S., Poirier, J.-P., & Le Mouël, J.-L. (2001). The age of the inner core. *Earth and Planetary Science Letters*, 190(3-4), 111-123. https://doi.org/10.1016/S0012-821X(01)00387-9 +- Nimmo, F. (2015). Energetics of the Core. In *Treatise on Geophysics* (2nd ed., Vol. 8, pp. 27-55). Elsevier. https://doi.org/10.1016/B978-0-444-53802-4.00139-1 +- Nimmo, F. (2015). Thermal and Compositional Evolution of the Core. In *Treatise on Geophysics* (2nd ed., Vol. 9, ch. 9.08, pp. 201-219). Elsevier. https://doi.org/10.1016/B978-0-444-53802-4.00160-3 +- Thiriet, M., Breuer, D., Michaut, C., & Plesa, A.-C. (2019). Scaling laws of convection for cooling planets in a stagnant lid regime. *Physics of the Earth and Planetary Interiors*, 286, 138-153. https://doi.org/10.1016/j.pepi.2018.11.003 +- Wolf, A. S., & Bower, D. J. (2018). An equation of state for high pressure-temperature liquids (RTpress) with application to MgSiO3 melt. *Physics of the Earth and Planetary Interiors*, 278, 59-74. https://doi.org/10.1016/j.pepi.2018.02.004 diff --git a/docs/Validation/numerics.md b/docs/Validation/numerics.md new file mode 100644 index 00000000..9b403f23 --- /dev/null +++ b/docs/Validation/numerics.md @@ -0,0 +1,116 @@ +# First-principles verification + +Aragog ships a verification test suite that exercises the entropy solver against analytic limits, conservation laws, and SPIDER-parity references. The suite runs under `pytest` from the repository root and is the primary way to confirm a build is correct. + +## Test categories + +The tests are organised by the layer they cover: + +### EOS unit tests (`tests/test_entropy_pytest.py`) + +Exercise `EntropyEOS` and `EntropyPhaseEvaluator`: + +- Property lookups (temperature, density, heat capacity, thermal expansivity, isentropic temperature gradient) at on-grid and off-grid $(P, S)$ points. +- Solidus and liquidus entropy lookups against the reference `solidus_P-S.dat` and `liquidus_P-S.dat` files. +- Smooth-clipped melt fraction $\phi(P, S)$: monotone in $S$, clamped to $[0, 1]$, $C^\infty$ across the solidus and liquidus crossings. +- The two-stage SPIDER-parity blend (cubic-Hermite or tanh smoothing followed by `combine_matprop`) for density, heat capacity, and viscosity. +- The latent-blend $c_p$ method against the linear blend. + +![EOS bilinear-interpolation Jacobian](../figures/vv/fig_03_eos_bilinear_jacobian.png) + +**Figure 1.** JAX-traced bilinear interpolation of the EOS table reproduces the numpy reference to float-64 machine precision and supplies finite, non-zero derivatives via `jax.jacrev` at on- and off-grid $(P, S)$ points. The agreement is the prerequisite for the analytic Jacobian path used by CVODE. + +### Phase evaluator tests + +Test the SPIDER-parity behaviour of `EntropyPhaseEvaluator`: + +- Phase identification at $(P, S)$ in pure solid, pure liquid, and mushy regimes. +- Viscosity tanh blend across the rheological transition $\phi_\mathrm{rheo}$. +- Gravitational-separation velocity in viscous and inviscid regimes. +- Pressure-dependent latent heat $L(P) = T_\mathrm{liq}(P) [S_\mathrm{liq}(P) - S_\mathrm{sol}(P)]$. + +### Solver integration tests + +Drive `EntropySolver` through short integrations and check the resulting profiles: + +- Standalone grey-body cooling: a fully-molten initial state cools toward the equilibrium temperature without status flag failures. +- The `set_initial_entropy`, `set_initial_dSdr_cmb`, and `set_initial_core_temperature` paths produce the expected state-vector layouts for each of the five `core_bc` modes. +- The retry-ladder hooks (`get_current_dSdr_cmb`, `_atol_sf`) round-trip cleanly. + +### Conservation tests (`tests/test_entropy_verification.py`) + +The conservation tests are the strongest class of verification because they hold under any valid run, not just specific test scenarios: + +- **Energy conservation.** $E_\mathrm{th}$ change over a closed integration matches the time-integrated surface flux to within solver tolerance. +- **Mass conservation.** Mantle mass remains constant through phase changes. +- **Grey-body cooling.** Surface flux follows $\varepsilon\sigma(T_\mathrm{top}^4 - T_\mathrm{eqm}^4)$ within tolerance once the radiative regime is established. + +![Mass-coordinate Jacobian sanity](../figures/vv/fig_07_mass_coord_jacobian.png) + +**Figure 2.** Mass-coordinate mesh Jacobian. The Newton solve that maps from mass coordinate to spatial radius (used when `mass_coordinates = true`) is monotone, smooth, and consistent with the analytic Adams-Williamson density profile. Departures here would propagate to mis-aligned cell volumes and a spurious mass-conservation residual in the integrator. + +![Radioactive decay](../figures/vv/fig_06_radio_decay.png) + +**Figure 3.** Radioactive heating $H_\mathrm{radio}(t)$ for the bundled isotope cocktail (Ruedas 2017): present-day specific powers ($^{40}\mathrm{K}$, $^{232}\mathrm{Th}$, $^{235}\mathrm{U}$, $^{238}\mathrm{U}$) decayed back to Solar System formation. Used as the source-term verification for the closed-mantle energy balance: the cumulative `step_dE_Q_radio_J` integral must match the time integral of $\rho \cdot H_\mathrm{radio}$ over each call to within solver tolerance. + +### JAX path tests (`tests/test_jax_entropy.py`) + +Verify that the JAX-backed `EntropyEOS_JAX` and `compute_fluxes` agree with the numpy path: + +- EOS parity: `EntropyEOS_JAX` returns the same temperature, density, heat capacity, thermal expansivity, $\partial T/\partial P|_S$, and conductivity as `EntropyEOS` on the same $(P, S)$ inputs. +- Phase evaluator parity: the JAX two-stage blend matches `EntropyPhaseEvaluator` to within floating-point tolerance. +- JIT and `vmap` compatibility: the JAX flux assembly is differentiable end-to-end with `jax.grad`/`jax.jacrev` and survives JIT compilation without recompilation hot-spots. +- Boundary-copy tests: `dSdr` boundary values copy from the nearest interior node (SPIDER `ic.c:450` parity) instead of being extrapolated. +- Solver parity: integrating the same problem with the JAX-RHS-driven CVODE path and the numpy RHS produces matching trajectories. + +### Numpy ↔ JAX RHS parity at representative magma-ocean states + +![Numpy ↔ JAX RHS parity](../figures/vv/fig_01_rhs_parity.png) + +**Figure 4.** Numpy $\leftrightarrow$ JAX RHS parity on an 80-cell Earth mesh at three representative magma-ocean states. *Top row, panels (a-c)*: per-cell **absolute** residual $|\dot S_\text{numpy} - \dot S_\text{jax}|$ in J/kg/K/yr; the dotted line is $\epsilon_\mathrm{mach}\,\max|\dot S|$ (the floating-point round-off floor expected on a single FP operation). *Bottom row, panels (d-f)*: per-cell **relative** residual $|\dot S_\text{numpy} - \dot S_\text{jax}|/|\dot S_\text{numpy}|$; the dotted line is float-64 machine epsilon. The three states are (a/d) initial condition (full magma ocean, $S \approx 3900$ J/kg/K, dimensionally uniform), (b/e) mid-solidification ($S = 3300 \to 3700$ J/kg/K from CMB to surface), (c/f) near-solid ($S = 3000 \to 3300$ J/kg/K). + +Per-panel residuals: + +- **Mid-solidification (b, e) and near-solid (c, f).** Interior cells sit at the float-64 round-off floor. Maximum absolute residual $\sim 3\times 10^{-7}$ J/kg/K/yr, dominated by a single spike at the surface boundary cell where the BC assembly is re-derived independently between numpy and JAX. Median relative error $\le 3\times 10^{-5}$. +- **Uniform-entropy IC (a, d).** Absolute residual $\sim 1$ J/kg/K/yr. Both paths run EOS lookup, lever-rule, and table interpolation on a uniform input; the arithmetic differs at the interpolation level. The relative error inflates because $|\dot S|$ itself is small in equilibrium. + +Numpy and JAX paths are independent re-derivations, not a bit-exact port. + +![UTBL Cardano-formula consistency](../figures/vv/fig_05_utbl_cardano.png) + +**Figure 5.** Surface-temperature correction from the upper-thermal-boundary-layer parameterisation (`solver/boundary.py::_utbl_tsurf`). The Cardano-formula solution to $b x^3 + x - T_\mathrm{int} = 0$ recovers $T_\mathrm{surf} < T_\mathrm{int}$ for the entire physical range and matches the cubic-equation residual to floating-point tolerance. Verifies the analytic surface boundary condition used when `param_utbl = true`. + +### Mesh tests (`tests/test_jax_mesh_gravity_fallback.py`) + +Verify that when the external mesh file (`eos_method = 2`) supplies an `eos_gravity` column, the JAX path interpolates the per-node gravity profile onto the basic grid rather than broadcasting a single scalar. + +## Running the verification suite + +The standalone suite uses `pytest`: + +```sh +# Run the entire suite +pytest tests/ + +# Run a specific category +pytest tests/test_entropy_pytest.py +pytest tests/test_entropy_verification.py +pytest tests/test_jax_entropy.py + +# Mark-filtered subsets +pytest -m unit tests/ # unit tests only +pytest -m smoke tests/ # smoke tests only +``` + +The JAX-path tests require `jax` and `equinox` to be installed; tests are skipped (not failed) when the optional dependencies are missing. Some tests also require the SPIDER-format EOS table directory; they skip if `FWL_DATA` is unset or does not contain the expected files. + +## Physical constraints checked + +The suite asserts the following physical invariants on every solver-driven test run: + +1. **Temperature positivity.** $T > 0$ everywhere at all integration times. EOS lookups outside the table domain raise rather than silently extrapolating. +2. **Melt fraction bounds.** $0 \le \phi \le 1$ via the smooth-clip; the assertion runs at every staggered node. +3. **Pressure positivity and monotonicity.** $P(r)$ is positive and decreases monotonically from CMB to surface. +4. **Entropy gradient continuity.** $\partial S/\partial r$ and the resulting fluxes are $C^0$ across the solidus and liquidus crossings (the smoothing functions enforce this). +5. **Energy conservation.** Within the integrator tolerance, $\Delta E_\mathrm{th}$ matches the time-integrated boundary fluxes plus internal heating sources. +6. **Mass conservation.** Total mantle mass is preserved under gravitational separation and chemical mixing (the segregation flux is divergence-free in mass). diff --git a/mkdocs.yml b/mkdocs.yml index 887798de..dc764d61 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -56,8 +56,11 @@ nav: - Diagnostics & verification: - EOS sanity checks: Explanations/eos_sanity_checks.md - Energy diagnostics: Explanations/energy_diagnostics.md - - First-principles verification: Explanations/verification.md - - Core module verification: Explanations/core_verification.md + + - Validation: + - Overview: Validation/index.md + - Core: Validation/core.md + - Numerics: Validation/numerics.md - Reference: - API reference: diff --git a/tests/test_core_verification_page.py b/tests/test_core_verification_page.py index a4917aad..fa0e669b 100644 --- a/tests/test_core_verification_page.py +++ b/tests/test_core_verification_page.py @@ -14,7 +14,7 @@ from tests.conftest import needs_eos ROOT = Path(__file__).resolve().parents[1] -PAGE = ROOT / 'docs' / 'Explanations' / 'core_verification.md' +PAGE = ROOT / 'docs' / 'Validation' / 'core.md' VALUES = json.loads( (ROOT / 'docs' / 'figures' / 'vv' / 'core_verification_values.json').read_text() ) diff --git a/tools/verification/figures/README.md b/tools/verification/figures/README.md index b495cf64..485cec80 100644 --- a/tools/verification/figures/README.md +++ b/tools/verification/figures/README.md @@ -1,6 +1,6 @@ # Verification figures -Reproducible scripts that generate the seven first-principles verification figures embedded in `docs/Explanations/verification.md`. +Reproducible scripts that generate the seven first-principles verification figures embedded in `docs/Validation/numerics.md`. Each script is a self-contained verification of one numerical or physical aspect of Aragog and writes its output to `docs/figures/vv/.{pdf,png}` (tracked in git) plus a raw-data `.npz` to `output/aragog_vv_data/` (gitignored). ## Layout diff --git a/tools/verification/run_core_verification.py b/tools/verification/run_core_verification.py index cda1032f..e4d23ccc 100644 --- a/tools/verification/run_core_verification.py +++ b/tools/verification/run_core_verification.py @@ -2,7 +2,7 @@ Writes ``docs/figures/vv/fig_08..fig_20_*.png`` (with a PDF beside each) and ``docs/figures/vv/core_verification_values.json``, the source of every number -quoted on ``docs/Explanations/core_verification.md``. The solver-level items +quoted on ``docs/Validation/core.md``. The solver-level items need the SPIDER-format EOS tables (``ARAGOG_TEST_EOS_DIR``); the Leeds ``thermal_history`` comparisons read the reference tables in ``tools/verification/data/``. From 5580fdcb497f09fc7c234a3ab91231b648c8b1a1 Mon Sep 17 00:00:00 2001 From: timlichtenberg Date: Fri, 23 Oct 2026 23:00:05 +0200 Subject: [PATCH 02/14] Add the Validation overview and the first plot pair docs/Validation/index.md lists every reference_pinned case with its process, test, reference and page. The young-layer test of the core shell writes its temperature rise against the ierfc half-space solution through the validation_plot fixture, for both of its times, and the core page shows the 0.1 Myr pair with its reference, tolerance, test id and command. proteus-mpl 1.4.0 joins the test extra and the test installs of both CI workflows, since the fixture comes from it. --- .github/workflows/ci_tests.yml | 2 +- .github/workflows/nightly.yml | 2 +- docs/Validation/core.md | 14 + docs/Validation/index.md | 35 + .../core_layer_erfc_half_space_0p03myr.svg | 1465 +++++++++++++++++ ...ore_layer_erfc_half_space_0p03myr_dark.svg | 1465 +++++++++++++++++ .../core_layer_erfc_half_space_0p1myr.svg | 1464 ++++++++++++++++ ...core_layer_erfc_half_space_0p1myr_dark.svg | 1464 ++++++++++++++++ pyproject.toml | 1 + tests/test_core_layer.py | 23 +- 10 files changed, 5931 insertions(+), 4 deletions(-) create mode 100644 docs/Validation/index.md create mode 100644 docs/assets/validation/core_layer_erfc_half_space_0p03myr.svg create mode 100644 docs/assets/validation/core_layer_erfc_half_space_0p03myr_dark.svg create mode 100644 docs/assets/validation/core_layer_erfc_half_space_0p1myr.svg create mode 100644 docs/assets/validation/core_layer_erfc_half_space_0p1myr_dark.svg diff --git a/.github/workflows/ci_tests.yml b/.github/workflows/ci_tests.yml index 7957e7f9..2d6bc495 100644 --- a/.github/workflows/ci_tests.yml +++ b/.github/workflows/ci_tests.yml @@ -76,7 +76,7 @@ jobs: - name: Install pip-only test deps (scikits-odes-sundials builds against conda SUNDIALS) run: | - pip install pytest pytest-xdist pytest-dependency pytest-timeout hypothesis ruff + pip install pytest pytest-xdist pytest-dependency pytest-timeout hypothesis ruff "proteus-mpl>=1.4.0" # Install scikits-odes-sundials directly (skip the scikits-odes # metapackage). This avoids scikits-odes-daepack, which is a # transitive Fortran dep with f2py incompatibility with numpy diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 84dd5532..9fa22daa 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -58,7 +58,7 @@ jobs: - name: Install pip-only test deps (scikits-odes-sundials builds against conda SUNDIALS) run: | - pip install pytest pytest-cov pytest-xdist pytest-dependency pytest-timeout hypothesis + pip install pytest pytest-cov pytest-xdist pytest-dependency pytest-timeout hypothesis "proteus-mpl>=1.4.0" # Direct install of scikits-odes-sundials only; aragog imports # CVODE directly from scikits_odes_sundials.cvode and skips the # daepack-pulling metapackage. See ci_tests.yml for details. diff --git a/docs/Validation/core.md b/docs/Validation/core.md index a98e0da2..77a51b0c 100644 --- a/docs/Validation/core.md +++ b/docs/Validation/core.md @@ -538,6 +538,20 @@ The two mixing constants are a model uncertainty, not a fitted quantity. Changin **Pinned by** `tests/test_core_verification_page.py::test_the_stable_layer_meets_the_thermal_history_bounds`; the analytic limits of the shell by `tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space`, `::test_a_steady_layer_reaches_the_quasi_static_depth` and `::test_mixing_keeps_a_superadiabatic_shell_on_the_adiabat`; a resumed solve by `tests/test_entropy_solver_core_module_smoke.py::test_a_resume_restarts_the_shell_where_the_previous_solve_ended`. +### Young layer against the ierfc solution + +A fixed deficit $\Delta F$ of the CMB flow below the conducted adiabatic flow, per unit area of the CMB, heats a conducting half space as $\Delta T(z, t) = (2 \Delta F / k) \sqrt{\kappa t}\, \mathrm{ierfc}(z / 2\sqrt{\kappa t})$, with $z$ the depth below the CMB and $\mathrm{ierfc}(x) = e^{-x^2}/\sqrt{\pi} - x\,\mathrm{erfc}(x)$. This is the constant-flux solution of a planar half space, the short-time limit of the fixed-gradient sphere of Greenwood et al. (2021, eq. 27). The test runs the shell with weak mixing (`k_mix = 1e-3`) under a CMB flow of 8 TW, adds to the solution the cooling that the divergence of the adiabatic conduction sets, and compares the temperature rise of the cells within $z < 6\sqrt{\kappa t}$ after 0.03 and 0.1 Myr. + +![Temperature rise below the CMB after 0.1 Myr and its difference from the ierfc solution](../assets/validation/core_layer_erfc_half_space_0p1myr.svg#only-light){ width="80%" } +![Temperature rise below the CMB after 0.1 Myr and its difference from the ierfc solution](../assets/validation/core_layer_erfc_half_space_0p1myr_dark.svg#only-dark){ width="80%" } + +Top: the expected temperature rise (line) and the rise of the cells (dots) against depth below the CMB after 0.1 Myr. Bottom: their absolute difference and the tolerance (dashed). The 0.03 Myr case writes a second pair, `core_layer_erfc_half_space_0p03myr`. + +- Reference: the constant-flux half-space solution above (analytical). +- Tolerance: 1 % of the expected rise of the top cell, 0.0098 K after 0.03 Myr and 0.020 K after 0.1 Myr; the largest differences are 0.0053 K over 9 cells and 0.0083 K over 13 cells. +- Test id: `tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space` +- Command: `pytest tests/test_core_layer.py -k erfc` (add `--validation-plots` to write the plot pairs) + ## References - Anzellini, S., Dewaele, A., Mezouar, M., Loubeyre, P., & Morard, G. (2013). Melting of iron at Earth's inner core boundary based on fast X-ray diffraction. *Science*, 340(6131), 464-466. https://doi.org/10.1126/science.1233514 diff --git a/docs/Validation/index.md b/docs/Validation/index.md new file mode 100644 index 00000000..e5128bf9 --- /dev/null +++ b/docs/Validation/index.md @@ -0,0 +1,35 @@ +# Validation + +This tab lists the tests that compare Aragog with an independent result: a published benchmark, an analytical limit or a second implementation. Each case carries the `reference_pinned` marker; the page of its process gives the reference, the tolerance and the command. + +- [Core](core.md): the core module against Nimmo (2015), the `leeds` and `leeds_thermal` models of thermal_history (Greenwood et al. 2021) and analytical limits. +- [Numerics](numerics.md): the first-principles checks of the solver, the equation of state and the NumPy and JAX right-hand sides. + +## Cases by process + +| Process | Case | Test | Reference | Page | +|---|---|---|---|---| +| Core stable layer | Young layer against the ierfc solution | `tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space` | constant-flux half space (analytical) | [Core](core.md#young-layer-against-the-ierfc-solution) | +| Core stable layer | Quasi-static depth of a steady layer | `tests/test_core_layer.py::test_a_steady_layer_reaches_the_quasi_static_depth` | conducted adiabatic flow equal to the CMB flow | [Core](core.md#13-stable-layer-against-thermal_history) | +| Core stable layer | Layers under fixed and eroding CMB flows | `tests/test_core_verification_page.py::test_the_stable_layer_meets_the_thermal_history_bounds` | `leeds_thermal` (Greenwood et al. 2021) | [Core](core.md#13-stable-layer-against-thermal_history) | +| Core energy budget | Earth budget | `tests/test_core_verification_page.py::test_the_earth_budget_meets_nimmo_tables_4_and_5` | Nimmo (2015), Tables 4 and 5 | [Core](core.md#5-nimmo-2015-earth-core-budget) | +| Core energy budget | Printed values of the two Nimmo core models | `tests/test_core_nimmo_benchmarks.py` (7 tests) | Nimmo (2015), ch. 8.02 Tables 2 and 4, ch. 9.08 Figure 3 | [Core](core.md#5-nimmo-2015-earth-core-budget) | +| Core energy budget | Budget terms | `tests/test_core_verification_page.py::test_budget_terms_match_the_thermal_history_table` | `leeds` (Greenwood et al. 2021) | [Core](core.md#6-budget-terms-against-thermal_history) | +| Core energy budget | Entropy terms | `tests/test_core_entropy.py::test_entropy_capacities_match_thermal_history` | `leeds` (Greenwood et al. 2021) | [Core](core.md#6-budget-terms-against-thermal_history) | +| Core dynamo | Efficiency factors and dynamo threshold | `tests/test_core_entropy.py::test_chr09_efficiency_factors_reproduce_printed_values`, `::test_dynamo_threshold_and_margin` | Christensen et al. (2009); Nimmo (2015) | [Core](core.md#7-dynamo-criterion-and-field-strength) | +| Core thermal history | Core history | `tests/test_core_verification_page.py::test_core_history_matches_the_thermal_history_table` | `leeds` (Greenwood et al. 2021) | [Core](core.md#10-a-core-thermal-history-against-thermal_history) | +| Core numerics | NumPy and JAX right-hand sides | `tests/test_jax_dsdt_core_module.py::test_rhs_parity_with_numpy_on_driven_state` | NumPy implementation | [Core](core.md#12-numpy-and-jax-right-hand-sides-and-the-analytic-jacobian) | +| Mantle heat transport | Theta-dependent top slope of the mixing length | `tests/test_mlt_calibration.py::test_theta_dependent_top_slope_hand_values_and_limits` | hand values and limits | no page section yet | +| Mantle rheology | Arrhenius viscosity, yield stress, NumPy and JAX kernels | `tests/test_rheology_verification.py` (5 tests) | reference limits, NumPy implementation | [Solid-state convection](../Explanations/solid_state_convection.md) | + +`pytest -m reference_pinned` runs every case. The core-page tests need the SPIDER P-S tables (`ARAGOG_TEST_EOS_DIR`, [Build tests](../How-to/build_tests.md)); the stable-layer and Earth-budget cases take minutes. + +## Plots written by the tests + +A case with a plot pair shows the tested values against their reference in a light and a dark version. The test writes the pair with the same numbers it asserts, through the `validation_plot` fixture of proteus-mpl. This command writes all pairs to `docs/assets/validation/`: + +```console +pytest -m reference_pinned --validation-plots +``` + +Without `--validation-plots` the tests assert and write nothing. The figures in `docs/figures/vv/` are made by the scripts in `tools/verification/`, not by the tests. diff --git a/docs/assets/validation/core_layer_erfc_half_space_0p03myr.svg b/docs/assets/validation/core_layer_erfc_half_space_0p03myr.svg new file mode 100644 index 00000000..86816beb --- /dev/null +++ b/docs/assets/validation/core_layer_erfc_half_space_0p03myr.svg @@ -0,0 +1,1465 @@ + + + + + + + + test: tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.03]; command: pytest 'tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.03]' --validation-plots + image/svg+xml + + + proteus-mpl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/validation/core_layer_erfc_half_space_0p03myr_dark.svg b/docs/assets/validation/core_layer_erfc_half_space_0p03myr_dark.svg new file mode 100644 index 00000000..c4fd6768 --- /dev/null +++ b/docs/assets/validation/core_layer_erfc_half_space_0p03myr_dark.svg @@ -0,0 +1,1465 @@ + + + + + + + + test: tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.03]; command: pytest 'tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.03]' --validation-plots + image/svg+xml + + + proteus-mpl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/validation/core_layer_erfc_half_space_0p1myr.svg b/docs/assets/validation/core_layer_erfc_half_space_0p1myr.svg new file mode 100644 index 00000000..2b5a67ab --- /dev/null +++ b/docs/assets/validation/core_layer_erfc_half_space_0p1myr.svg @@ -0,0 +1,1464 @@ + + + + + + + + test: tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.1]; command: pytest 'tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.1]' --validation-plots + image/svg+xml + + + proteus-mpl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/validation/core_layer_erfc_half_space_0p1myr_dark.svg b/docs/assets/validation/core_layer_erfc_half_space_0p1myr_dark.svg new file mode 100644 index 00000000..8714582f --- /dev/null +++ b/docs/assets/validation/core_layer_erfc_half_space_0p1myr_dark.svg @@ -0,0 +1,1464 @@ + + + + + + + + test: tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.1]; command: pytest 'tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.1]' --validation-plots + image/svg+xml + + + proteus-mpl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pyproject.toml b/pyproject.toml index bc3aa2fe..123c1126 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,6 +43,7 @@ dependencies = [ [project.optional-dependencies] test = [ "hypothesis>=6.100.0", + "proteus-mpl>=1.4.0", "pytest>=8.1", "pytest-cov>=1.0.0", "pytest-dependency>=0.6.0", diff --git a/tests/test_core_layer.py b/tests/test_core_layer.py index 9faadebf..ae263d09 100644 --- a/tests/test_core_layer.py +++ b/tests/test_core_layer.py @@ -68,7 +68,7 @@ def test_the_shell_heat_changes_by_the_flows_at_its_faces(): @pytest.mark.reference_pinned @pytest.mark.parametrize('t_myr', [0.03, 0.1]) -def test_a_young_layer_follows_the_erfc_solution_of_a_half_space(t_myr): +def test_a_young_layer_follows_the_erfc_solution_of_a_half_space(t_myr, validation_plot): """A fixed flow deficit Q_k - q at the top of a conducting half space raises the temperature by (2 dF / k) sqrt(kappa t) ierfc(z / 2 sqrt(kappa t)) (Carslaw and Jaeger, the planar limit of Greenwood et al. 2021, eq. 27); the cells also cool at the rate the @@ -86,7 +86,26 @@ def test_a_young_layer_follows_the_erfc_solution_of_a_half_space(t_myr): deficit = (q_k - 8e12) / (4.0 * np.pi * p.r_cmb**2) expected = 2.0 * deficit / K_CORE * length * ierfc + sink near = x < 3.0 - assert np.max(np.abs(t_shell - y0 - expected)[near]) < 1e-2 * expected[-1] + tol = 1e-2 * expected[-1] + assert np.max(np.abs(t_shell - y0 - expected)[near]) < tol + + def figure(): + import matplotlib.pyplot as plt + + depth = (p.r_cmb - np.asarray(shell.r_cells))[near] / 1e3 + rise, ref = (t_shell - y0)[near], expected[near] + fig, (top, bottom) = plt.subplots(2, 1, sharex=True, figsize=(6.4, 5.6)) + top.plot(depth, ref, label='ierfc solution') + top.plot(depth, rise, 'o', ms=3, label='Aragog') + top.set(ylabel='Temperature rise [K]') + top.legend() + bottom.plot(depth, np.abs(rise - ref), '.', ms=3) + bottom.axhline(tol, color=plt.rcParams['text.color'], ls='--') + bottom.set(xlabel='Depth below the CMB [km]', ylabel='|Difference| [K]') + fig.tight_layout() + return fig + + validation_plot(figure, f'core_layer_erfc_half_space_{t_myr:g}myr'.replace('.', 'p')) @pytest.mark.reference_pinned From 5fe03e5fc90eba6c3ec07259569784b48f12dd01 Mon Sep 17 00:00:00 2001 From: timlichtenberg Date: Fri, 23 Oct 2026 23:02:53 +0200 Subject: [PATCH 03/14] Add AGENTS.md and the agent-file check AGENTS.md holds the commands that decide whether a change is ready, the package layout, the solver and coupling facts a contributor needs and the conventions, under the shared blocks synced from PROTEUS; tests/AGENTS.md the shared test rules and the Aragog specifics (markers, the SPIDER P-S tables, the core page test, the plot fixture). Each CLAUDE.md imports AGENTS.md, and .github/copilot-instructions.md is a short summary for review tools. The broken .claude/rules link is removed. tools/agents/check_agents_md.py, the same file as in PROTEUS, checks the block hashes, the CLAUDE.md files and the size caps, and the CI test job runs it on Linux with Python 3.12. --- .claude/rules/proteus-tests.md | 1 - .github/copilot-instructions.md | 22 ++- .github/workflows/ci_tests.yml | 4 + AGENTS.md | 91 ++++++++++++ CLAUDE.md | 245 +------------------------------- tests/AGENTS.md | 45 ++++++ tests/CLAUDE.md | 1 + tools/agents/check_agents_md.py | 139 ++++++++++++++++++ 8 files changed, 302 insertions(+), 246 deletions(-) delete mode 120000 .claude/rules/proteus-tests.md mode change 120000 => 100644 .github/copilot-instructions.md create mode 100644 AGENTS.md create mode 100644 tests/AGENTS.md create mode 100644 tests/CLAUDE.md create mode 100755 tools/agents/check_agents_md.py diff --git a/.claude/rules/proteus-tests.md b/.claude/rules/proteus-tests.md deleted file mode 120000 index 4efb7b62..00000000 --- a/.claude/rules/proteus-tests.md +++ /dev/null @@ -1 +0,0 @@ -../../.claude/rules/proteus-tests.md \ No newline at end of file diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md deleted file mode 120000 index 949a29f1..00000000 --- a/.github/copilot-instructions.md +++ /dev/null @@ -1 +0,0 @@ -../CLAUDE.md \ No newline at end of file diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..47c24690 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,21 @@ +# Aragog short instructions + +The full instructions are in `AGENTS.md` (repository rules) and `tests/AGENTS.md` (test rules). This file repeats what a review or a chat needs when it cannot read them. + +## Commands + +```bash +pip install -e '.[test,jax]' +export ARAGOG_TEST_EOS_DIR=... # SPIDER P-S tables, else the EOS tests skip +pytest -m "unit and not slow" -n auto -o "addopts=" +ruff check src/ tests/ && ruff format --check src/ tests/ +bash tools/validate_test_structure.sh +python tools/agents/check_agents_md.py +``` + +## Review points + +- Every test carries exactly one tier marker (`unit`, `smoke`, `integration`, `slow`, or `skip`); a physics test asserts a conservation law, a bound or a published or analytical value, with a guard against the most plausible wrong formula. +- A change to anything PROTEUS reads (`EntropySolver`, its config fields, the output used by `proteus.interior_energetics.aragog.AragogRunner`) names the affected PROTEUS call sites in the pull request. +- Numbers on `docs/Validation/core.md` are checked by `tests/test_core_verification_page.py` against `docs/figures/vv/core_verification_values.json`; change them together. +- Comments: at most 3 lines per block; commit messages and docs describe the current code, with no tool attribution and no em or en dashes. diff --git a/.github/workflows/ci_tests.yml b/.github/workflows/ci_tests.yml index 2d6bc495..dc325353 100644 --- a/.github/workflows/ci_tests.yml +++ b/.github/workflows/ci_tests.yml @@ -106,6 +106,10 @@ jobs: ruff check src/ tests/ ruff format --check src/ tests/ + - name: Check agent instruction files + if: ${{ matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12' }} + run: python tools/agents/check_agents_md.py + - name: Pre-flight fail_under ratchet check if: ${{ matrix.os == 'ubuntu-latest' && matrix.python-version == '3.12' }} env: diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..8799334d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,91 @@ +# Aragog agent instructions + +Aragog is the interior energetics module of the PROTEUS ecosystem: a 1-D, spherically symmetric, two-phase entropy solver for the mantle, with a mixing-length closure of the convective flux at every node and the partial melt treated as a mixture of solid and melt. PROTEUS selects it as `interior_energetics.module = 'aragog'`. Before a first edit: + +- Run every command from the repository root. Tests for `src/aragog//.py` go in `tests/`; the test rules are in `tests/AGENTS.md`. +- These commands decide whether a change is ready (PR CI runs the unit tier on Linux and macOS with Python 3.12 and 3.13; the nightly runs every tier): + +```bash +pip install -e '.[test,jax]' # jax: CVODE, JAX and equinox, the production solver path +export ARAGOG_TEST_EOS_DIR=... # SPIDER P-S tables; without them the EOS tests skip +pytest -m "unit and not slow" -n auto -o "addopts=" +ruff check src/ tests/ +ruff format --check src/ tests/ +bash tools/validate_test_structure.sh +python tools/agents/check_agents_md.py +``` + +The CI takes the test tables from the release asset `test-data-v1` of this repository (`spider_eos_test_data.tar.gz`). Docs: `pip install -e '.[docs]'`, then `zensical build --clean` (Zensical, not raw mkdocs). Verification figures: `python tools/verification/figures/verify_.py` (or `aragog vnv`) and `python tools/verification/run_core_verification.py`. + + +## PROTEUS ecosystem rules + +PROTEUS couples separate module repositories into one model of the evolution of rocky planets and their atmospheres. Its config selects one module per process; the config values are the lowercase names: + +- `interior_energetics.module`: `aragog`, `spider` (or the built-in `boundary`); `interior_struct.module`: `zalmoxis`, `spider` +- `atmos_clim.module`: `agni`, `janus`, both with SOCRATES spectral radiative transfer +- `outgas.module`: `calliope`, `atmodeller`; `atmos_chem.module`: `vulcan` +- `star.module`: `mors`; `escape.module`: `zephyrus`, `boreas`; `orbit.module` (tides): `obliqua`, `lovepy` +- fwl-io downloads the reference data the modules use + +A module change to anything PROTEUS calls or reads (a function signature, a config key, an output column, a unit) can break the coupled model while the module's own tests pass. Search `src/proteus/` in PROTEUS for the name before you change it, and name the affected PROTEUS call sites in the pull request. + +### Physics and numerics + +- Units differ between and within modules: MORS and VULCAN use cgs; CALLIOPE and atmodeller work with pressures in bar; AGNI takes surface pressure in bar at setup and holds Pa inside; PROTEUS states the unit of every `hf_row` key in `GetHelpfileKeys` (`src/proteus/utils/coupler.py`: atmospheric pressures in bar, interior pressures in Pa, model time in years, orbital periods in s), and its config reference pages state each key's unit (for example stellar mass in M_sun, initial partial surface pressures in bar, stellar age in Gyr). State the unit of every physical quantity in its docstring and convert explicitly at the boundary: a unit mismatch between two modules passes the tests of both and shows only in the coupled run. +- A physics test must fail for the most plausible wrong formula: check a conservation law, a bound, a monotonicity, or a published or analytical value, and assert that the wrong result falls outside the tolerance. +- Take each physical constant from one source per repository (in Python `scipy.constants` or the module's constants file). Two retyped values of one constant differ at round-off and hide real differences between code paths. +- Do not loosen a solver tolerance, a conservation check or a clamp to make a run or a test pass. Find the cause; a loosened check also hides the next defect. + +### Validation + +A new or changed physics routine gets the validation cases named in its confirmed plan: an analytical law, a published figure or table, or a cross-code check. Each case is a `reference_pinned` test that runs the real code on real data, with no mocks, asserts the tolerance, and writes its light and dark plot through the `validation_plot` fixture of proteus-mpl. `pytest -m reference_pinned --validation-plots` makes all plots again. Line plots are SVG, maps are AVIF; each file is at most 60 kB, under `docs/assets/validation/`. Each case has a subsection on its page in the Validation tab, with the reference, the tolerance, the test id and the command. Data read from a published figure comes from 2 independent figlab reads that agree within the stated error, as a CSV with its provenance under `tests/validation/data/`. + +### Code + +- Keep an inline comment block to 2 lines or fewer, and never more than 4, because a long block drifts from the code it describes. An explanation that needs more goes in the docstring, the commit message or the pull-request description. + +### Branches and pull requests + +- Work on a feature branch `/`; `main` changes only through a reviewed pull request. +- Fill every section of the repository's pull-request template, where it has one. +- Before you push, run the checks listed at the top of this `AGENTS.md` and in `tests/AGENTS.md`. + +### Where knowledge goes + +Rules for every contributor go in the `AGENTS.md` files, the reason for a change in its commit message and pull-request description, and the scientific validation of a module in its `docs/Validation/` pages, where the repository has them. Do not add memory, notes or decision-log files to the repository: nobody maintains them, and they go stale. + + + +### Commit messages and public text + +Commit messages, pull-request text, code comments, docstrings, test names, test skip reasons, parametrize ids, log strings that ship with the code, and CI job and step names describe the change and the current state of the code. They name no tool used to write the change, carry no tool-attribution trailer, use no internal plan, phase or work-group labels, and use no em or en dashes (a page range in a citation is the exception). + + +## Architecture + +`src/aragog/`, by package: + +- `solver/`: `entropy_solver.py` (`EntropySolver`, its `from_file` constructor and the CVODE and scipy integration), `entropy_state.py` (state and right-hand side of the NumPy path), `boundary.py` (surface and CMB boundary conditions), `cvode_jax.py` (CVODE with the JAX right-hand side and analytic Jacobian, the production path). +- `eos/`: `entropy.py` (P-S table loader), `entropy_phase.py` (two-phase evaluator), `consistency.py`, `table_cache.py`. `jax/`: the JAX counterparts of the EOS, the phase evaluator and the fluxes, and `nondim.py`, shared by both paths. +- `core/`: the core module (`core_bc = 'core_module'`): profiles, budget, entropy and dynamo criterion, melting curve, CMB boundary layer, stratification and the resolved layer. +- `config/` (attrs configuration classes), `parser.py` (the dataclass parser of the standalone configs in `cfg/`), `mesh/` (fixed mesh, pressure EOS), `output/` (diagnostics), `rheology.py` and `rheology_lid.py` (solid-state rheology), `cli.py` (the `aragog` command: `run`, `inspect`, `validate`, `show-config`, `new`, `list-configs`, `vnv`). + +The state is the entropy at the mesh nodes, plus extra entries set by the core boundary condition: `core_bc` is one of `quasi_steady`, `energy_balance`, `gradient`, `bower2018`, `core_module` (`config/boundary.py`; `docs/Explanations/core_bc.md`). The heat flux is conduction, mixing-length convection (viscous and inviscid limits blended at the cell Reynolds number `RE_CRIT = 9/8`), gravitational separation (permeability regimes switching at the porosities 0.0769452 and 0.771462) and chemical mixing, with radiogenic and tidal heating. The default `solver_method = 'cvode'` with `use_jax_jacobian = true` integrates with SUNDIALS CVODE and a `jax.jacrev` Jacobian; without scikits-odes-sundials the solver falls back to scipy Radau with a warning, and without a JAX factory CVODE uses its finite-difference Jacobian. `phi_step_cap > 0` stops each call where the mean melt fraction has changed by the cap (`docs/How-to/phi-step-cap.md`). + +## PROTEUS and Zalmoxis coupling + +- PROTEUS configures Aragog from its own schema (`proteus.config._interior.Aragog`) and runs it through `proteus.interior_energetics.aragog.AragogRunner`, which calls `EntropySolver` directly; the CLI and the bundled `cfg/` files are for standalone runs only. The `hf_row` keys it reads and writes are in `docs/Explanations/proteus_coupling.md`, the recommended settings in `docs/How-to/proteus_coupling.md`. +- With `interior_struct.module = 'zalmoxis'`, the mesh comes from `/data/zalmoxis_output.dat`, and the P-S tables written by Zalmoxis must stay a rectangular grid in (P, S), since the loader builds a `RegularGridInterpolator` on it (`eos/entropy.py`). +- A resumed run compares the saved mesh radii with the mesh to `max(1 m, 1e-9 * span)`; if it fails, regenerate the mesh with the structure module. + +## Conventions + +- Imports go to the defining module (`from aragog.solver import EntropySolver`, `from aragog.eos.entropy import EntropyEOS`); there are no compatibility shims. Every `.py` file starts with `from __future__ import annotations`. `aragog/__init__.py` re-exports `EntropySolver` at its end (`# noqa: E402`) to break the import cycle with `aragog.solver`; keep it there. +- The version comes from the latest git tag through setuptools-scm (CalVer); `src/aragog/_version.py` is generated and ignored. Releases: `docs/How-to/releasing.md`. +- Style: ruff with single quotes and a line length of 96, NumPy docstrings, comments that say why. Citations in the docs prefer SciX URLs, with the DOI as fallback; never invent a bibcode. +- Docs: Zensical with `use_directory_urls: false`; a new page needs its file and its `mkdocs.yml` nav entry; a new module gets a stub under `docs/Reference/api/`. The Validation tab (`docs/Validation/`) holds the core and numerics verification pages and the overview of the `reference_pinned` cases; the script figures are in `docs/figures/vv/` (PNG on the site, PDF beside it), the plot pairs of the tests in `docs/assets/validation/`. `tests/test_core_verification_page.py` checks every tagged number of `docs/Validation/core.md` against `docs/figures/vv/core_verification_values.json`. + +## Agent files + +`AGENTS.md` and `tests/AGENTS.md` hold the instructions; each `CLAUDE.md` holds only `@AGENTS.md`, and `.github/copilot-instructions.md` is a short summary for review tools. The shared blocks come from PROTEUS (`tools/agents/sync_core.py`); edit them there, not here. `python tools/agents/check_agents_md.py` checks the block hashes, the `CLAUDE.md` files and the size caps, and CI runs it. diff --git a/CLAUDE.md b/CLAUDE.md index fecc4ff7..43c994c2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,244 +1 @@ -# Aragog AI Agent Guidelines - -Aragog is the **interior energetics module** of the PROTEUS ecosystem. -It is a 1-D, two-phase, spherically symmetric interior dynamics solver with a mixing-length convective closure. -Two design choices define Aragog's approach: - -1. Mixing-length theory (MLT) closes the convective heat flux *locally* at every radial node. -2. The partial-melt mantle is treated as a coexisting solid + melt mixture rather than a single-phase fluid with a depth cutoff. - -Together these resolve the partial-melt window between first crystallisation and final solidification. -Ecosystem-wide guidelines live in `../CLAUDE.md` (PROTEUS-level). -End-user documentation is in `docs/` (Zensical site at `proteus-framework.org/aragog/`). - -## Quick reference - -```bash -# Activate the production environment -conda activate proteus - -# Tests (fast unit tier, mirrors CI) -pytest -m "unit and not slow" -n auto - -# Full nightly tier with coverage -pytest -m "unit or smoke or slow" -n auto --cov=src/aragog - -# Lint and format (CI canonical) -ruff check src/ tests/ tools/ && ruff format --check src/ tests/ tools/ - -# Documentation (Zensical, NOT raw mkdocs) -zensical serve # live reload on docs/ and src/ -zensical build --clean # full build for CI parity - -# Regenerate V&V figures -python tools/verification/figures/verify_radio_decay.py -# Three figures need a PROTEUS-side fixture: -export PROTEUS_SCRIPTS=$HOME/git/PROTEUS/scripts -python tools/verification/figures/verify_rhs_parity.py -``` - -## Environment - -- The `proteus` conda env ships Python 3.12, numpy ≥ 2.0, JAX, equinox, and the SUNDIALS / `scikits-odes-sundials` stack. All production paths assume float64 (`jax.config.update('jax_enable_x64', True)` is set at JAX-import time inside Aragog). -- `FWL_DATA` may point at the SPIDER P-S EOS-table cache for standalone runs. PROTEUS-coupled runs do not need it; Zalmoxis writes the tables on the fly. -- Optional install groups (`pyproject.toml`): - - `[jax]` — `scikits-odes-sundials`, `jax`, `equinox`. **Required for the production solver path.** A bare `pip install -e .` falls back to scipy `Radau`/`BDF` with a finite-difference Jacobian, which is correct for short tests but step-size-fragile on multi-Myr cooling runs. - - `[test]` — `pytest`, `pytest-cov`, `pytest-xdist`, `pytest-dependency`, `pytest-timeout`, `hypothesis`. - - `[docs]` — `zensical`, `mkdocstrings[python]`, `pymdown-extensions`, `mkdocs-material`, `markdown-include`. - -## Project layout - -``` -aragog/ - src/aragog/ - __init__.py # Logger setup, CFG_DATA, EntropySolver re-export - cli.py # click subcommands: run, inspect, validate, show-config, new, list-configs, vnv - parser.py # Legacy dataclass-based TOML/dict config parser - utilities.py # tanh_weight, smooth helpers - cfg/ # Bundled standalone configs (abe_solid.toml, abe_mixed.cfg, ...) - config/ # Attrs-based config classes (newer path) - boundary.py energy.py initial_condition.py mesh.py - phases.py radionuclides.py solver.py - eos/ - entropy.py # SPIDER-format P-S table loader (RegularGridInterpolator) - entropy_phase.py # Two-phase blender (lever rule for T, harmonic mean for rho, - # latent-augmented Cp, Costa 2009 viscosity log-blend) - jax/ - eos.py # JAX bilinear EOS lookup (parity with numpy to float64 epsilon) - nondim.py # Non-dimensionalisation helpers - phase.py # JAX flux assembly: compute_mlt, compute_fluxes, compute_phase_state - solver.py # JAX RHS dSdt_energy_balance, make_radio_heating_fn, _utbl_tsurf_jax - mesh/ - fixed_mesh.py # Static finite-volume mesh; mixing_length(r) = nearest_boundary - pressure_eos.py # AdamsWilliamsonEOS (eos_method=1), UserDefinedEOS (eos_method=2) - output/ - diagnostics.py # SolverOutput dataclass, NetCDF writer (PROTEUS uses its own writer) - solver/ - boundary.py # Outer BC modes 1/4/5; CMB BCs; UTBL Cardano cubic - cvode_jax.py # build_jax_rhs_and_jacobian factory (the "Option Z" production path) - entropy_solver.py # EntropySolver class; from_file constructor; retry helpers - entropy_state.py # EntropyState + RHS assembly (numpy path), inlined MLT block - tests/ # ~366 unit + ~40 smoke + ~3 slow - conftest.py # `helper` fixture; per-file fixtures for EOS-dependent tests - tools/ - verification/ # Existing first-principles verification runners - figures/ # Reproducible V&V figure scripts (paired with docs/figures/vv/) - _style.py # Shared matplotlib style helper - verify_*.py # 7 scripts producing fig_NN_*.pdf and .png - README.md - docs/ # Zensical sources (Diataxis: How-to, Tutorials, Explanations, Reference, Community) - data/ # Reference data for tests (e.g. abe_*.dat) - output/ # Run outputs (gitignored) - mkdocs.yml # Nav (consumed by Zensical) - pyproject.toml # PEP 621 setuptools; ruff and pytest config -``` - -## Solver architecture - -Two-phase entropy ODE on a 1-D radial mesh. -The state vector length depends on the core BC mode: - -- `core_bc = 'energy_balance'` (default; SPIDER bit-parity): length $N+1$ where the extra state is $dS/dr|_\mathrm{cmb}$. -- `core_bc = 'quasi_steady'`: length $N$. -- `core_bc = 'gradient'`: length $N+2$. -- `core_bc = 'bower2018'`: experimental. -- `core_bc = 'core_module'`: length $N+2$ where the extra states are $dS/dr|_\mathrm{cmb}$ and the core temperature, plus the shell temperatures with `stratification = true`. - -The right-hand side assembles four heat-flux components plus internal heating: - -``` -F_total = F_cond + F_conv + F_grav + F_mix -H_total = H_radio + H_tidal -``` - -- `F_cond` follows Fourier's law in entropy form using EOS-tabulated $(\partial T/\partial P)_S$. -- `F_conv` is mixing-length theory: $F_\mathrm{conv} = \rho T \kappa_h \max(-\partial S/\partial r, 0)$ with $\kappa_h$ blended between viscous (Stokes-like) and inviscid (free-fall) limits via a tanh on the cell Reynolds number at $\mathrm{Re}_\mathrm{crit} = 9/8$. The MLT block is inlined in `EntropyState.update` (numpy, `entropy_state.py:441-557`) and packaged as `aragog.jax.phase.compute_mlt` (JAX). -- `F_grav` is the gravitational-separation flux. Three permeability regimes (Stokes / Rumpf-Gupte / BKC) with tanh transitions at the equal-density-ratio limits $\zeta_1 = 0.0769452$ and $\zeta_2 = 0.771462$ (Bower 2018 Eqs. 13a-c). -- `F_mix` is the SPIDER-parity bracket form of chemical mixing. - -**Production solver path: SUNDIALS CVODE + JAX analytic Jacobian.** -The `[jax]` extra installs `scikits-odes-sundials`, `jax`, and `equinox`. -The default behaviour with `solver_method = 'cvode'` and `solver.use_jax_jacobian = true` registers a JAX `jax.jacrev` factory that builds the Jacobian in a single backward pass instead of the $O(N)$ RHS calls a finite-difference Jacobian needs. -Without `scikits-odes-sundials`, the solver falls back to scipy `Radau` / `BDF`; without JAX with `use_jax_jacobian = true`, CVODE falls back to its own finite-difference Jacobian. - -**Per-call $|\Delta\Phi|$ cap.** -`phi_step_cap > 0` registers a SUNDIALS root function $g(t, y) = \mathrm{cap} - |\Phi_\mathrm{global}(t,y) - \Phi_\mathrm{global}(t_\mathrm{start})|$ that returns CVODE control at the exact step where the mass-weighted mean melt fraction first changes by `cap`. -The scipy fallbacks register the equivalent logic as a `solve_ivp` event. -Production CHILI runs use `phi_step_cap = 0.05`. - -## Standalone execution - -```python -from pathlib import Path -from aragog import aragog_file_logger -from aragog.solver import EntropySolver - -aragog_file_logger(log_dir=str(Path.cwd())) - -solver = EntropySolver.from_file( - filename='src/aragog/cfg/abe_solid.toml', - eos_dir='/path/to/eos/tables', -) -solver.initialize() -# When core_bc='energy_balance', set the CMB gradient BEFORE the entropy IC: -# solver.set_initial_dSdr_cmb(0.0) -solver.set_initial_entropy(2900.0) -solver.solve() - -out = solver.get_state() # SolverOutput dataclass -``` - -The bundled `cfg/abe_solid.toml` and `cfg/abe_mixed.cfg` are short standalone smoke setups. -The CLI (`aragog`) wraps the same path with seven subcommands: `run`, `inspect`, `validate`, `show-config`, `new`, `list-configs`, `vnv`. `aragog run` accepts `--set =` overrides without editing the TOML on disk; see `docs/Reference/cli.md` for the full reference. The CLI is a thin wrapper over the Python API; PROTEUS-coupled runs bypass it and call `EntropySolver` directly via `AragogRunner`. - -## PROTEUS integration - -When PROTEUS drives Aragog the configuration lives in PROTEUS's TOML schema, not in Aragog's bundled configs. -The PROTEUS-side attrs class is `proteus.config._interior.Aragog`, and the wrapper is `proteus.interior_energetics.aragog.AragogRunner`. - -Two call sites in PROTEUS: - -- **Init solve**: `AragogRunner.setup_solver` at `aragog.py:293`, called once before the main loop. -- **Per-iteration evolve**: `AragogRunner.run_solver` at `aragog.py:1509`, called every coupling step. The helpfile-output assembly is `_build_helpfile_output` at `aragog.py:1740`. - -`hf_row` exchange: - -- *Reads*: `Time`, `F_atm`, `T_eqm`, `R_int`, `R_core`, `gravity`, `P_surf`, `core_density`, `core_heatcap`, `M_core`, `ini_entropy` (first call only). -- *Writes*: scalar mantle state — `T_magma`, `T_core`, `Phi_global`, `Phi_global_vol`, `RF_depth`, `T_pot`, `M_mantle`, `M_mantle_liquid`, `M_mantle_solid`, `Cp_eff`, `E_th_mantle`. Echo-back / passthrough — `M_core`, `F_int`. Source powers — `F_radio`, `F_tidal`, `F_cmb`, `Q_radio_W`, `Q_tidal_W`. Energy-conservation primitives — `E_state_J`, `E_state_cons_J`, `step_dE_F_int_J`, `step_dE_F_cmb_J`, `step_dE_Q_radio_J`, `step_dE_Q_tidal_J`, `step_dE_Q_radio_cons_J`, `step_dE_Q_tidal_cons_J`, `step_solver_residual_J`. The cumulative residuals (`dE_predicted_cons_J`, `E_residual_cons_J`, `E_residual_cons_frac`, `solver_residual_J`) are computed in `proteus.utils.coupler._populate_energy_residual` from the per-call writes above. `T_surf` and `dt_actual` are NOT written by the Aragog wrapper: the former is set by `proteus.atmos_clim.wrapper`, the latter is the consumed `out.dt_actual` returned to the caller as the simulated time step. -- *Echo-back*: the wrapper recomputes `core_density` from the on-disk Zalmoxis mesh and `hf_row['M_core']` at every solve entry (`resolve_core_density` at `aragog.py:66`), mirroring SPIDER's `-rho_core` re-derivation. This survives mesh-blending fall-backs and stale-cache cases. - -Recommended PROTEUS-side knobs (in priority order): - -1. `interior_struct.module = 'zalmoxis'` + `interior_energetics.module = 'aragog'` for new production runs. -2. `interior_energetics.aragog.backend = 'jax'` (default). -3. `interior_energetics.aragog.core_bc = 'energy_balance'` (default; SPIDER bit-parity). -4. `interior_energetics.aragog.solver_method = 'cvode'` (default). -5. `interior_energetics.aragog.mass_coordinates = true` (default; required for `energy_balance`). -6. `interior_energetics.aragog.phi_step_cap = 0.05` for typical evolution; leave at `0.0` unless mushy-zone melt-fraction oscillations show up early. -7. `interior_energetics.aragog.surface_bc_mode = 'flux'` (default) for atmosphere-driven flux BC; `'grey_body'` selects mode 1 (Aragog re-evaluates $\varepsilon\sigma(T_\mathrm{top}^4 - T_\mathrm{eqm}^4)$ per CVODE sub-step). -8. `interior_energetics.{rtol, atol} = 1e-10` (schema defaults); `atol_temperature_equivalent = 1e-8` matches SPIDER. -9. `planet.prevent_warming = false`. Must stay false; the clamp is energy-non-conserving and produces a spurious $T_\mathrm{magma}$ plateau when true. - -Pitfalls: - -- *Deleted `dilatation` field*: removed on 2026-05-04 along with the explicit Φ_vol volumetric-work source (it was a divergence double-count). Any config that sets `dilatation = true` or `dilatation = false` is rejected by the schema validator. Negative-regression tests guard against re-introduction of `Q_dil_total`, `step_dE_Q_dil_J`, and the schema field. -- *Resume + ULP radius drift*: a resumed run can hit single-ULP mismatch between saved Zalmoxis radii and Aragog's mesh bounds at `solver/entropy_solver.py:91-110`. The validator tolerates `max(1 m, 1e-9 * span)` to absorb this; if it trips, regenerate the mesh by re-running the structure module. -- *The PROTEUS wrapper bypasses Aragog's standalone CLI and TOML loader.* `AragogRunner` builds `Parameters` directly from the PROTEUS schema. The bundled `abe_*.{cfg,toml}` configs are only for standalone work. - -## Zalmoxis coupling - -When `interior_struct.module = 'zalmoxis'` and `interior_energetics.module = 'aragog'`, two artefacts cross the Zalmoxis → Aragog boundary on every successful structure re-solve: - -1. **Mesh file** `/data/zalmoxis_output.dat`: five-column TSV (`r`, `P`, `rho`, `g`, `T`). Aragog reads only the first four columns inside `solver.reset()` to rebuild its mass-coordinate mesh; the temperature column is a Zalmoxis diagnostic and is ignored on the Aragog side. -2. **Aragog P-T tables** in `/data/aragog_pt/`: `{density, temperature, heat_capacity, adiabat_temp_grad, thermal_exp}_{melt, solid}.dat`. Written by `zalmoxis.eos_export.generate_aragog_pt_tables` (or `_2phase` for two-phase PALEOS), default resolution $200 \times 200$, P-range $[10^5, \min(10^{13},\ 150 M_\oplus + 200\,\mathrm{GPa})]$ Pa. - -Aragog's table loader builds `RegularGridInterpolator((P, S), ...)` over the rectangular grid (the directory name `aragog_pt/` is a legacy label; the contents follow the SPIDER P-S convention). -Phase-filtering the input PALEOS table breaks the rectangularity assumption and silently drops scipy into unstructured (linear-ND) interpolation, ~100x slower per call. - -## Import conventions - -- No backward-compatibility shims; imports are direct to the actual module. - - `from aragog import aragog_file_logger` - - `from aragog.solver import EntropySolver` - - `from aragog.eos.entropy import EntropyEOS` - - `from aragog.eos.entropy_phase import EntropyPhaseEvaluator` - - `from aragog.config import Parameters` (or `aragog.parser` for the legacy dataclass path) - - `from aragog.jax.solver import dSdt_energy_balance, make_radio_heating_fn` - - `from aragog.jax.phase import compute_mlt, compute_fluxes` -- `from __future__ import annotations` is required at the top of every `.py` file (enforced by ruff isort). -- `aragog/__init__.py` re-exports `EntropySolver` at the bottom of the file with `# noqa: E402` to break the circular import with `aragog.solver`. Do not move *that re-export* to the top. Unrelated top-of-file imports (logging, the setuptools-scm `_version` shim) are fine where they are. -- `__version__` is derived from the latest git tag via `setuptools-scm` (CalVer `YY.MM.DD`). The generated `src/aragog/_version.py` is gitignored; do not hand-edit it. Cut a release by tagging `main` and pushing the tag (see `docs/How-to/releasing.md`). - -## Testing - -- ~366 unit, ~40 smoke, ~3 slow tests. Markers from `pyproject.toml [tool.pytest.ini_options]`: - - `unit`: < 100 ms, no real solver call. - - `smoke`: one full `EntropySolver.solve()` at relaxed tolerance. - - `slow`: multi-Myr runs and tolerance-convergence studies. -- CI canonical invocations: - - Push / PR (`.github/workflows/ci_tests.yml`): `pytest -m "unit and not slow" -n auto` on Ubuntu + macOS × Python 3.11/3.12/3.13. - - Nightly (`.github/workflows/nightly.yml`, 02:30 UTC): `pytest -m "unit or smoke or integration or slow" -n auto --cov=aragog --ignore=tests/test_core_verification_page.py`, and that file alone in its own job with `-p no:xdist --cov=aragog`; a coverage job combines both and enforces the 90% PROTEUS-ecosystem ceiling via `[tool.coverage.report].fail_under`. -- The `helper` fixture (in `tests/conftest.py`) provides path-resolution helpers; module-local fixtures handle EOS-dependent setup. -- Use `pytest.approx` or `np.testing.assert_allclose` for float comparisons, never `==`. -- Tests without a marker are invisible to CI. Add `@pytest.mark.unit` (or another tier) on every new test. -- For the LLM-prompt template that encodes these rules, see `docs/How-to/build_tests.md`. - -## Documentation (Zensical) - -- The site builds with **Zensical**, not raw mkdocs. Use `zensical serve` and `zensical build --clean`. Raw `mkdocs serve` may fail on theme resolution. -- Live reload: `zensical serve` watches `docs/` and `src/` automatically. -- URLs use `use_directory_urls: false` (pages are `*.html`). -- Nav is defined in `mkdocs.yml`. New pages need both the file and the nav entry. -- mkdocstrings `:::` directives auto-generate API pages from docstrings; add a stub under `docs/Reference/api/` for any new module. -- V&V figures live in `docs/figures/vv/fig_NN_.{pdf,png}` and are reproducible from `tools/verification/figures/verify_.py`. PNGs are embedded in the docs site; PDFs are the print master. Both are tracked. - -## Code style - -- `ruff` for linting and formatting (`pyproject.toml [tool.ruff]`). -- Single quotes (configured in ruff). -- `from __future__ import annotations` required in all files. -- Line length 96 (prefer < 92). -- Run `ruff format` AND `ruff check` before committing; the local ruff (0.12.x) lags CI ruff (0.15.x) and `format` does NOT catch lint rules like `E402` misplaced imports. -- NumPy-style docstrings (Parameters / Returns / Raises / Notes). -- Comments should explain WHY, not WHAT. No project-tracking labels (T1.x, Stage X, dates of past changes); the development history belongs in commit messages, not in inline comments. -- Citations in user-facing docs prefer SciX URLs (`https://scixplorer.org/abs//abstract`); use DOI as a fallback. Never fabricate bibcodes — for un-indexed references (book chapters, German-language papers without ADS entries), cite as plain author / year / journal / page. +@AGENTS.md diff --git a/tests/AGENTS.md b/tests/AGENTS.md new file mode 100644 index 00000000..1772908f --- /dev/null +++ b/tests/AGENTS.md @@ -0,0 +1,45 @@ +# Aragog test instructions + + +## Test rules shared by the PROTEUS ecosystem + +Each test file starts with a module-level tier marker and a timeout. CI selects tests by marker, so a file without one runs in no CI job; the timeout stops a hang, it is not a target. + +```python +pytestmark = [pytest.mark.unit, pytest.mark.timeout(30)] +``` + +| tier | what it tests | timeout | +|---|---|---| +| `unit` | Python logic with the heavy physics mocked; aim for < 100 ms | 30 s | +| `smoke` | the real binaries or solvers, one step, low resolution | 60 s | +| `integration` | several modules coupled | 300 s | +| `slow` | full physics validation | 3600 s | + +Each test carries exactly one tier marker, so that the tier filters select it in the one CI job meant for it; a function marker for a second tier breaks that. Split a file whose tests need different tiers. `skip` excludes a test from every CI job. + +Every new test covers an edge case (a boundary value, an empty input, an extreme physical parameter), exercises the error contract (a documented exception with the check that no side effect ran, a guard, a clamp, or the limit input of the formula), and asserts values that do not follow trivially from the implementation. A pinned value of 1 that every exponent reproduces checks nothing. + +`python tools/check_test_quality.py --check` fails when the count of any rule rises above `tools/test_quality_baseline.json`; the offenders it prints are the first few of that rule in the tree, not necessarily yours. The rules: a file without a tier marker, a test without a docstring, a test with one assertion or none, a weak assertion as the only one (`is None`, `is not None`, `> 0`, `len(...) > 0`, `isinstance`), `==` next to a non-zero float literal, and an optional dependency imported without `pytest.importorskip`. `bash tools/validate_test_structure.sh` runs the repository's own structure check; the repository part of this file says what it checks. + +Physics tests carry markers so their coverage is tracked apart from line coverage: +- `@pytest.mark.physics_invariant` on each test function that asserts a conservation law, a bound (T > 0, fractions in [0, 1]), a monotonicity or symmetry, or a pinned value with a discrimination guard. The marker goes on the function, not the module: structural tests in the same file do not carry it. +- `@pytest.mark.reference_pinned` (together with `physics_invariant`) on a test that pins a published benchmark, an analytical limit or a cross-implementation result; cite the paper, table or figure in the docstring. +- A discrimination guard asserts that the most plausible wrong formula (a missing factor, a swapped exponent, the wrong unit) gives a result outside the tolerance of the pinned value. + +Floats: compare with `pytest.approx` or `np.testing.assert_allclose` and a tolerance you can justify from the method, not the one that makes the test pass. + +Mocks: mock at the narrowest scope (the one external call), and return physically plausible values, so the code under test runs its real branches. Set random seeds and write files only under `tmp_path`. + +A module-level constant read from an environment variable at import time does not change with `monkeypatch.setenv`; patch the constant with `monkeypatch.setattr`. + + +## Aragog specifics + +Aragog has no `tools/check_test_quality.py`; `bash tools/validate_test_structure.sh` checks that every test (by function, class or module-level `pytestmark`) carries exactly one of `unit`, `smoke`, `integration`, `slow` or `skip`, and CI runs it before the tests. Markers registered in `pyproject.toml`: `unit`, `smoke`, `integration`, `slow`, `physics_invariant`, `reference_pinned`; pytest runs with `--strict-markers`. + +- PR CI runs `pytest -m "unit and not slow" -n auto -o "addopts="`. The nightly runs `-m "unit or smoke or integration or slow"` with coverage, `tests/test_core_verification_page.py` in its own job in one process (`-p no:xdist`, its stable-layer case runs for many minutes), and combines the coverage against `fail_under = 90` in `pyproject.toml`. +- The SPIDER P-S tables come from `ARAGOG_TEST_EOS_DIR`, else `$FWL_DATA/aragog/spider_eos`, else a parity cache next to the repository (`tests/conftest.py`). Without them every test marked `needs_eos` skips: read the skip count of a run, a green run without the tables tests far less. `pytest_collection_finish` parses the tables once before the first test, outside the per-test timeouts. +- `tests/conftest.py` holds the table helpers (`entropy_eos_copy`, `entropy_eos_jax`, `make_mesh`) and the `helper` fixture (the bundled configs and the reference data in `tests/reference/`); other fixtures are in the test files. +- `tests/test_core_verification_page.py` checks each tagged number of `docs/Validation/core.md` against `docs/figures/vv/core_verification_values.json` and reruns the items of `tools/verification/run_core_verification.py` it names; a change to the page, the values file or the script goes with that test. +- Tests that take the `validation_plot` fixture need `proteus-mpl` (in the `test` extra and in the CI installs); `pytest -m reference_pinned --validation-plots` writes their plot pairs to `docs/assets/validation/`. diff --git a/tests/CLAUDE.md b/tests/CLAUDE.md new file mode 100644 index 00000000..43c994c2 --- /dev/null +++ b/tests/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/tools/agents/check_agents_md.py b/tools/agents/check_agents_md.py new file mode 100755 index 00000000..556e2ed5 --- /dev/null +++ b/tools/agents/check_agents_md.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 +"""Check the agent instruction files of a PROTEUS-ecosystem repository. + +The same file is used in every ecosystem repository; the canonical copy is +``tools/agents/check_agents_md.py`` in FormingWorlds/PROTEUS. It checks: + +* every shared block (```` to + ````) still matches the hash written by + ``sync_core.py``, which catches a local edit of the shared text (whether the + text matches PROTEUS is checked there, by ``sync_core.py --check``); +* a root ``AGENTS.md`` exists with an ``fwl-core`` block, a ``tests/AGENTS.md`` has an + ``fwl-tests-core`` block, and the byte caps: 16,000 B for the root file, + 12,000 B for a nested one; +* next to every ``AGENTS.md`` a ``CLAUDE.md`` is a regular file that imports + ``AGENTS.md`` and nothing else; with a root ``CLAUDE.md`` present, Claude Code + reads a nested ``AGENTS.md`` only through its own ``CLAUDE.md``; +* ``.github/copilot-instructions.md`` has at most 60 lines. + +Usage: ``python tools/agents/check_agents_md.py [repo_root]``. Exit status 1 +lists every failure. +""" + +from __future__ import annotations + +import hashlib +import re +import subprocess +import sys +from pathlib import Path + +ROOT_MAX_BYTES = 16_000 +NESTED_MAX_BYTES = 12_000 +COPILOT_MAX_LINES = 60 +CLAUDE_MD_TEXT = '@AGENTS.md' +REQUIRED_BLOCKS = {Path('AGENTS.md'): 'core', Path('tests/AGENTS.md'): 'tests-core'} + +BLOCK_RE = re.compile( + r'^\n' + r'(?P.*?)' + r'^$', + re.MULTILINE | re.DOTALL, +) +MARKER_RE = re.compile(r'^\s* ## PROTEUS ecosystem rules @@ -81,7 +81,7 @@ The state is the entropy at the mesh nodes, plus extra entries set by the core b ## Conventions -- Imports go to the defining module (`from aragog.solver import EntropySolver`, `from aragog.eos.entropy import EntropyEOS`); there are no compatibility shims. Every `.py` file starts with `from __future__ import annotations`. `aragog/__init__.py` re-exports `EntropySolver` at its end (`# noqa: E402`) to break the import cycle with `aragog.solver`; keep it there. +- Imports go to the defining module (`from aragog.solver import EntropySolver`, `from aragog.eos.entropy import EntropyEOS`); there are no compatibility shims. Every non-empty `.py` file starts with `from __future__ import annotations`; ruff enforces it (`I002`, `required-imports` in `pyproject.toml`). `aragog/__init__.py` re-exports `EntropySolver` at its end (`# noqa: E402`) to break the import cycle with `aragog.solver`; keep it there. - The version comes from the latest git tag through setuptools-scm (CalVer); `src/aragog/_version.py` is generated and ignored. Releases: `docs/How-to/releasing.md`. - Style: ruff with single quotes and a line length of 96, NumPy docstrings, comments that say why. Citations in the docs prefer SciX URLs, with the DOI as fallback; never invent a bibcode. - Docs: Zensical with `use_directory_urls: false`; a new page needs its file and its `mkdocs.yml` nav entry; a new module gets a stub under `docs/Reference/api/`. The Validation tab (`docs/Validation/`) holds the core and numerics verification pages and the overview of the `reference_pinned` cases; the script figures are in `docs/figures/vv/` (PNG on the site, PDF beside it), the plot pairs of the tests in `docs/assets/validation/`. `tests/test_core_verification_page.py` checks every tagged number of `docs/Validation/core.md` against `docs/figures/vv/core_verification_values.json`. diff --git a/docs/Validation/core.md b/docs/Validation/core.md index 77a51b0c..9aad5d35 100644 --- a/docs/Validation/core.md +++ b/docs/Validation/core.md @@ -540,7 +540,7 @@ The two mixing constants are a model uncertainty, not a fitted quantity. Changin ### Young layer against the ierfc solution -A fixed deficit $\Delta F$ of the CMB flow below the conducted adiabatic flow, per unit area of the CMB, heats a conducting half space as $\Delta T(z, t) = (2 \Delta F / k) \sqrt{\kappa t}\, \mathrm{ierfc}(z / 2\sqrt{\kappa t})$, with $z$ the depth below the CMB and $\mathrm{ierfc}(x) = e^{-x^2}/\sqrt{\pi} - x\,\mathrm{erfc}(x)$. This is the constant-flux solution of a planar half space, the short-time limit of the fixed-gradient sphere of Greenwood et al. (2021, eq. 27). The test runs the shell with weak mixing (`k_mix = 1e-3`) under a CMB flow of 8 TW, adds to the solution the cooling that the divergence of the adiabatic conduction sets, and compares the temperature rise of the cells within $z < 6\sqrt{\kappa t}$ after 0.03 and 0.1 Myr. +A fixed deficit $\Delta F$ of the CMB flow below the conducted adiabatic flow, per unit area of the CMB, heats a conducting half space as $\Delta T(z, t) = (2 \Delta F / k) \sqrt{\kappa t}\, \mathrm{ierfc}(z / 2\sqrt{\kappa t})$, with $z$ the depth below the CMB and $\mathrm{ierfc}(x) = e^{-x^2}/\sqrt{\pi} - x\,\mathrm{erfc}(x)$. This is the constant-flux solution of a planar half space, the planar short-time counterpart of the fixed-gradient sphere of Greenwood et al. (2021, eq. 27). The test runs the shell with weak mixing (`k_mix = 1e-3`) under a CMB flow of 8 TW, adds to the solution the cooling that the divergence of the adiabatic conduction sets, and compares the temperature rise of the cells within $z < 6\sqrt{\kappa t}$ after 0.03 and 0.1 Myr. ![Temperature rise below the CMB after 0.1 Myr and its difference from the ierfc solution](../assets/validation/core_layer_erfc_half_space_0p1myr.svg#only-light){ width="80%" } ![Temperature rise below the CMB after 0.1 Myr and its difference from the ierfc solution](../assets/validation/core_layer_erfc_half_space_0p1myr_dark.svg#only-dark){ width="80%" } @@ -549,6 +549,7 @@ Top: the expected temperature rise (line) and the rise of the cells (dots) again - Reference: the constant-flux half-space solution above (analytical). - Tolerance: 1 % of the expected rise of the top cell, 0.0098 K after 0.03 Myr and 0.020 K after 0.1 Myr; the largest differences are 0.0053 K over 9 cells and 0.0083 K over 13 cells. +- The cooling term comes from the shell's own rates (`CoreShell.rates`), not from an independent formula. Without it the 0.03 Myr case still passes (0.0064 K); the 0.1 Myr case separates it (0.0216 K against 0.020 K). - Test id: `tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space` - Command: `pytest tests/test_core_layer.py -k erfc` (add `--validation-plots` to write the plot pairs) diff --git a/docs/Validation/index.md b/docs/Validation/index.md index e5128bf9..35e74a62 100644 --- a/docs/Validation/index.md +++ b/docs/Validation/index.md @@ -1,6 +1,6 @@ # Validation -This tab lists the tests that compare Aragog with an independent result: a published benchmark, an analytical limit or a second implementation. Each case carries the `reference_pinned` marker; the page of its process gives the reference, the tolerance and the command. +This tab lists the tests that compare Aragog with an independent result: a published benchmark, an analytical limit or a second implementation. Each case carries the `reference_pinned` marker; the linked section describes it, and a case with a plot pair has a subsection with its reference, tolerance, test id and command. - [Core](core.md): the core module against Nimmo (2015), the `leeds` and `leeds_thermal` models of thermal_history (Greenwood et al. 2021) and analytical limits. - [Numerics](numerics.md): the first-principles checks of the solver, the equation of state and the NumPy and JAX right-hand sides. @@ -13,14 +13,15 @@ This tab lists the tests that compare Aragog with an independent result: a publi | Core stable layer | Quasi-static depth of a steady layer | `tests/test_core_layer.py::test_a_steady_layer_reaches_the_quasi_static_depth` | conducted adiabatic flow equal to the CMB flow | [Core](core.md#13-stable-layer-against-thermal_history) | | Core stable layer | Layers under fixed and eroding CMB flows | `tests/test_core_verification_page.py::test_the_stable_layer_meets_the_thermal_history_bounds` | `leeds_thermal` (Greenwood et al. 2021) | [Core](core.md#13-stable-layer-against-thermal_history) | | Core energy budget | Earth budget | `tests/test_core_verification_page.py::test_the_earth_budget_meets_nimmo_tables_4_and_5` | Nimmo (2015), Tables 4 and 5 | [Core](core.md#5-nimmo-2015-earth-core-budget) | -| Core energy budget | Printed values of the two Nimmo core models | `tests/test_core_nimmo_benchmarks.py` (7 tests) | Nimmo (2015), ch. 8.02 Tables 2 and 4, ch. 9.08 Figure 3 | [Core](core.md#5-nimmo-2015-earth-core-budget) | +| Core energy budget | Printed values of the two Nimmo core models | `tests/test_core_nimmo_benchmarks.py` (the `reference_pinned` tests except the cross-check) | Nimmo (2015), ch. 8.02 Tables 2 and 4, ch. 9.08 Table 2, Figure 3 and p. 212 | [Core](core.md#5-nimmo-2015-earth-core-budget) | +| Core energy budget | Model-2 budget terms at 4180 K | `tests/test_core_nimmo_benchmarks.py::test_budget_terms_match_thermal_history_cross_check` | `leeds` (Greenwood et al. 2021) | [Core](core.md#6-budget-terms-against-thermal_history) | | Core energy budget | Budget terms | `tests/test_core_verification_page.py::test_budget_terms_match_the_thermal_history_table` | `leeds` (Greenwood et al. 2021) | [Core](core.md#6-budget-terms-against-thermal_history) | | Core energy budget | Entropy terms | `tests/test_core_entropy.py::test_entropy_capacities_match_thermal_history` | `leeds` (Greenwood et al. 2021) | [Core](core.md#6-budget-terms-against-thermal_history) | | Core dynamo | Efficiency factors and dynamo threshold | `tests/test_core_entropy.py::test_chr09_efficiency_factors_reproduce_printed_values`, `::test_dynamo_threshold_and_margin` | Christensen et al. (2009); Nimmo (2015) | [Core](core.md#7-dynamo-criterion-and-field-strength) | | Core thermal history | Core history | `tests/test_core_verification_page.py::test_core_history_matches_the_thermal_history_table` | `leeds` (Greenwood et al. 2021) | [Core](core.md#10-a-core-thermal-history-against-thermal_history) | | Core numerics | NumPy and JAX right-hand sides | `tests/test_jax_dsdt_core_module.py::test_rhs_parity_with_numpy_on_driven_state` | NumPy implementation | [Core](core.md#12-numpy-and-jax-right-hand-sides-and-the-analytic-jacobian) | | Mantle heat transport | Theta-dependent top slope of the mixing length | `tests/test_mlt_calibration.py::test_theta_dependent_top_slope_hand_values_and_limits` | hand values and limits | no page section yet | -| Mantle rheology | Arrhenius viscosity, yield stress, NumPy and JAX kernels | `tests/test_rheology_verification.py` (5 tests) | reference limits, NumPy implementation | [Solid-state convection](../Explanations/solid_state_convection.md) | +| Mantle rheology | Arrhenius viscosity, yield stress, NumPy and JAX kernels | `tests/test_rheology_verification.py` (its `reference_pinned` tests) | reference limits, NumPy implementation | [Solid-state convection](../Explanations/solid_state_convection.md) | `pytest -m reference_pinned` runs every case. The core-page tests need the SPIDER P-S tables (`ARAGOG_TEST_EOS_DIR`, [Build tests](../How-to/build_tests.md)); the stable-layer and Earth-budget cases take minutes. diff --git a/mkdocs.yml b/mkdocs.yml index dc764d61..c0bcfc1c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -53,7 +53,7 @@ nav: - CVODE and JAX: Explanations/cvode_jax.md - JAX Jacobian factory: Explanations/jax_jacobian_factory.md - Melt-fraction step cap: Explanations/phi_step_cap_rationale.md - - Diagnostics & verification: + - Diagnostics: - EOS sanity checks: Explanations/eos_sanity_checks.md - Energy diagnostics: Explanations/energy_diagnostics.md diff --git a/tests/AGENTS.md b/tests/AGENTS.md index 1772908f..4e656bd3 100644 --- a/tests/AGENTS.md +++ b/tests/AGENTS.md @@ -36,10 +36,11 @@ A module-level constant read from an environment variable at import time does no ## Aragog specifics -Aragog has no `tools/check_test_quality.py`; `bash tools/validate_test_structure.sh` checks that every test (by function, class or module-level `pytestmark`) carries exactly one of `unit`, `smoke`, `integration`, `slow` or `skip`, and CI runs it before the tests. Markers registered in `pyproject.toml`: `unit`, `smoke`, `integration`, `slow`, `physics_invariant`, `reference_pinned`; pytest runs with `--strict-markers`. +Aragog has no `tools/check_test_quality.py`; `bash tools/validate_test_structure.sh` checks that every test (by function, class or module-level `pytestmark`) carries at least one of `unit`, `smoke`, `integration`, `slow` or `skip`, and CI runs it before the tests; it does not catch a second tier marker. Markers registered in `pyproject.toml`: `unit`, `smoke`, `integration`, `slow`, `physics_invariant`, `reference_pinned`; `addopts = "--strict-markers"` applies to a local run, while the CI commands pass `-o "addopts="`, so CI warns on an unregistered marker instead of failing. Most test files set no timeout and `pyproject.toml` sets no default, so the timeout part of the shared rule is not in place here yet; the nightly passes `--timeout`. - PR CI runs `pytest -m "unit and not slow" -n auto -o "addopts="`. The nightly runs `-m "unit or smoke or integration or slow"` with coverage, `tests/test_core_verification_page.py` in its own job in one process (`-p no:xdist`, its stable-layer case runs for many minutes), and combines the coverage against `fail_under = 90` in `pyproject.toml`. - The SPIDER P-S tables come from `ARAGOG_TEST_EOS_DIR`, else `$FWL_DATA/aragog/spider_eos`, else a parity cache next to the repository (`tests/conftest.py`). Without them every test marked `needs_eos` skips: read the skip count of a run, a green run without the tables tests far less. `pytest_collection_finish` parses the tables once before the first test, outside the per-test timeouts. - `tests/conftest.py` holds the table helpers (`entropy_eos_copy`, `entropy_eos_jax`, `make_mesh`) and the `helper` fixture (the bundled configs and the reference data in `tests/reference/`); other fixtures are in the test files. - `tests/test_core_verification_page.py` checks each tagged number of `docs/Validation/core.md` against `docs/figures/vv/core_verification_values.json` and reruns the items of `tools/verification/run_core_verification.py` it names; a change to the page, the values file or the script goes with that test. +- The smoke tests of the JAX solver need `diffrax`, which the nightly installs and no extra lists. - Tests that take the `validation_plot` fixture need `proteus-mpl` (in the `test` extra and in the CI installs); `pytest -m reference_pinned --validation-plots` writes their plot pairs to `docs/assets/validation/`. From 6f9e8c5e3bcd860851d3f7edcd4d4989e82443b6 Mon Sep 17 00:00:00 2001 From: timlichtenberg Date: Fri, 23 Oct 2026 23:20:25 +0200 Subject: [PATCH 05/14] Guard the ierfc case against a missing cooling term The 0.1 Myr case of the young-layer test now asserts that the ierfc profile without the cooling term misses the tolerance, and its docstring describes eq. 27 of Greenwood et al. (2021) as the page does. The Validation overview links a section only where one exists and gives the Nimmo and leeds sources of each row; the agent files state that the structure script catches only a missing tier marker, that 8 tests carry two, and that ruff enforces the future import in the linted folders. --- .github/copilot-instructions.md | 2 +- AGENTS.md | 2 +- docs/Validation/core.md | 2 +- docs/Validation/index.md | 8 ++++---- tests/AGENTS.md | 2 +- tests/test_core_layer.py | 5 ++++- tools/validate_test_structure.sh | 21 +++------------------ 7 files changed, 15 insertions(+), 27 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 8f27a759..f3cb2c4d 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -15,7 +15,7 @@ python tools/agents/check_agents_md.py ## Review points -- Every test carries one tier marker (`unit`, `smoke`, `integration`, `slow`, or `skip`); a physics test asserts a conservation law, a bound or a published or analytical value, with a guard against the most plausible wrong formula. +- Every test carries exactly one tier marker (`unit`, `smoke`, `integration`, `slow`, or `skip`; the structure script catches only a missing one); a physics test asserts a conservation law, a bound or a published or analytical value, with a guard against the most plausible wrong formula. - A change to anything PROTEUS reads (`EntropySolver`, its config fields, the output used by `proteus.interior_energetics.aragog.AragogRunner`) names the affected PROTEUS call sites in the pull request. - Numbers on `docs/Validation/core.md` are checked by `tests/test_core_verification_page.py` against `docs/figures/vv/core_verification_values.json`; change them together. - Comments: a block of 2 lines or fewer, never more than 4; commit messages and docs describe the current code, with no tool attribution and no em or en dashes. diff --git a/AGENTS.md b/AGENTS.md index 82508317..eccb8bc3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -81,7 +81,7 @@ The state is the entropy at the mesh nodes, plus extra entries set by the core b ## Conventions -- Imports go to the defining module (`from aragog.solver import EntropySolver`, `from aragog.eos.entropy import EntropyEOS`); there are no compatibility shims. Every non-empty `.py` file starts with `from __future__ import annotations`; ruff enforces it (`I002`, `required-imports` in `pyproject.toml`). `aragog/__init__.py` re-exports `EntropySolver` at its end (`# noqa: E402`) to break the import cycle with `aragog.solver`; keep it there. +- Imports go to the defining module (`from aragog.solver import EntropySolver`, `from aragog.eos.entropy import EntropyEOS`); there are no compatibility shims. Every non-empty `.py` file starts with `from __future__ import annotations`; ruff enforces it where CI lints, in `src/` and `tests/` (`I002`, `required-imports` in `pyproject.toml`). `aragog/__init__.py` re-exports `EntropySolver` at its end (`# noqa: E402`) to break the import cycle with `aragog.solver`; keep it there. - The version comes from the latest git tag through setuptools-scm (CalVer); `src/aragog/_version.py` is generated and ignored. Releases: `docs/How-to/releasing.md`. - Style: ruff with single quotes and a line length of 96, NumPy docstrings, comments that say why. Citations in the docs prefer SciX URLs, with the DOI as fallback; never invent a bibcode. - Docs: Zensical with `use_directory_urls: false`; a new page needs its file and its `mkdocs.yml` nav entry; a new module gets a stub under `docs/Reference/api/`. The Validation tab (`docs/Validation/`) holds the core and numerics verification pages and the overview of the `reference_pinned` cases; the script figures are in `docs/figures/vv/` (PNG on the site, PDF beside it), the plot pairs of the tests in `docs/assets/validation/`. `tests/test_core_verification_page.py` checks every tagged number of `docs/Validation/core.md` against `docs/figures/vv/core_verification_values.json`. diff --git a/docs/Validation/core.md b/docs/Validation/core.md index 9aad5d35..54214cba 100644 --- a/docs/Validation/core.md +++ b/docs/Validation/core.md @@ -549,7 +549,7 @@ Top: the expected temperature rise (line) and the rise of the cells (dots) again - Reference: the constant-flux half-space solution above (analytical). - Tolerance: 1 % of the expected rise of the top cell, 0.0098 K after 0.03 Myr and 0.020 K after 0.1 Myr; the largest differences are 0.0053 K over 9 cells and 0.0083 K over 13 cells. -- The cooling term comes from the shell's own rates (`CoreShell.rates`), not from an independent formula. Without it the 0.03 Myr case still passes (0.0064 K); the 0.1 Myr case separates it (0.0216 K against 0.020 K). +- The cooling term comes from the shell's own rates (`CoreShell.rates`), not from an independent formula. Without it the 0.03 Myr case still passes (0.0064 K); the 0.1 Myr case separates it (0.0216 K against 0.020 K) and asserts that it does. - Test id: `tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space` - Command: `pytest tests/test_core_layer.py -k erfc` (add `--validation-plots` to write the plot pairs) diff --git a/docs/Validation/index.md b/docs/Validation/index.md index 35e74a62..5fa9132d 100644 --- a/docs/Validation/index.md +++ b/docs/Validation/index.md @@ -1,6 +1,6 @@ # Validation -This tab lists the tests that compare Aragog with an independent result: a published benchmark, an analytical limit or a second implementation. Each case carries the `reference_pinned` marker; the linked section describes it, and a case with a plot pair has a subsection with its reference, tolerance, test id and command. +This tab lists the tests that compare Aragog with an independent result: a published benchmark, an analytical limit or a second implementation. Each case carries the `reference_pinned` marker. Where a page section exists, the table links it; a case with a plot pair has a subsection with its reference, tolerance, test id and command. - [Core](core.md): the core module against Nimmo (2015), the `leeds` and `leeds_thermal` models of thermal_history (Greenwood et al. 2021) and analytical limits. - [Numerics](numerics.md): the first-principles checks of the solver, the equation of state and the NumPy and JAX right-hand sides. @@ -13,15 +13,15 @@ This tab lists the tests that compare Aragog with an independent result: a publi | Core stable layer | Quasi-static depth of a steady layer | `tests/test_core_layer.py::test_a_steady_layer_reaches_the_quasi_static_depth` | conducted adiabatic flow equal to the CMB flow | [Core](core.md#13-stable-layer-against-thermal_history) | | Core stable layer | Layers under fixed and eroding CMB flows | `tests/test_core_verification_page.py::test_the_stable_layer_meets_the_thermal_history_bounds` | `leeds_thermal` (Greenwood et al. 2021) | [Core](core.md#13-stable-layer-against-thermal_history) | | Core energy budget | Earth budget | `tests/test_core_verification_page.py::test_the_earth_budget_meets_nimmo_tables_4_and_5` | Nimmo (2015), Tables 4 and 5 | [Core](core.md#5-nimmo-2015-earth-core-budget) | -| Core energy budget | Printed values of the two Nimmo core models | `tests/test_core_nimmo_benchmarks.py` (the `reference_pinned` tests except the cross-check) | Nimmo (2015), ch. 8.02 Tables 2 and 4, ch. 9.08 Table 2, Figure 3 and p. 212 | [Core](core.md#5-nimmo-2015-earth-core-budget) | -| Core energy budget | Model-2 budget terms at 4180 K | `tests/test_core_nimmo_benchmarks.py::test_budget_terms_match_thermal_history_cross_check` | `leeds` (Greenwood et al. 2021) | [Core](core.md#6-budget-terms-against-thermal_history) | +| Core energy budget | Printed values of the two Nimmo core models | `tests/test_core_nimmo_benchmarks.py` (the `reference_pinned` tests except the cross-check) | Nimmo (2015), ch. 9.08 Table 2, Figure 3 caption (p. 211) and p. 212; ch. 8.02 Table 2 | [Core](core.md#5-nimmo-2015-earth-core-budget) | +| Core energy budget | Model-2 budget terms at 4180 K | `tests/test_core_nimmo_benchmarks.py::test_budget_terms_match_thermal_history_cross_check` | `leeds` (Greenwood et al. 2021); Nimmo (2015), ch. 8.02 Tables 2 and 4 | [Core](core.md#6-budget-terms-against-thermal_history) | | Core energy budget | Budget terms | `tests/test_core_verification_page.py::test_budget_terms_match_the_thermal_history_table` | `leeds` (Greenwood et al. 2021) | [Core](core.md#6-budget-terms-against-thermal_history) | | Core energy budget | Entropy terms | `tests/test_core_entropy.py::test_entropy_capacities_match_thermal_history` | `leeds` (Greenwood et al. 2021) | [Core](core.md#6-budget-terms-against-thermal_history) | | Core dynamo | Efficiency factors and dynamo threshold | `tests/test_core_entropy.py::test_chr09_efficiency_factors_reproduce_printed_values`, `::test_dynamo_threshold_and_margin` | Christensen et al. (2009); Nimmo (2015) | [Core](core.md#7-dynamo-criterion-and-field-strength) | | Core thermal history | Core history | `tests/test_core_verification_page.py::test_core_history_matches_the_thermal_history_table` | `leeds` (Greenwood et al. 2021) | [Core](core.md#10-a-core-thermal-history-against-thermal_history) | | Core numerics | NumPy and JAX right-hand sides | `tests/test_jax_dsdt_core_module.py::test_rhs_parity_with_numpy_on_driven_state` | NumPy implementation | [Core](core.md#12-numpy-and-jax-right-hand-sides-and-the-analytic-jacobian) | | Mantle heat transport | Theta-dependent top slope of the mixing length | `tests/test_mlt_calibration.py::test_theta_dependent_top_slope_hand_values_and_limits` | hand values and limits | no page section yet | -| Mantle rheology | Arrhenius viscosity, yield stress, NumPy and JAX kernels | `tests/test_rheology_verification.py` (its `reference_pinned` tests) | reference limits, NumPy implementation | [Solid-state convection](../Explanations/solid_state_convection.md) | +| Mantle rheology | Arrhenius viscosity, yield stress, NumPy and JAX kernels | `tests/test_rheology_verification.py` (its `reference_pinned` tests) | reference limits, NumPy implementation | no page section yet ([Solid-state convection](../Explanations/solid_state_convection.md) explains the rheology) | `pytest -m reference_pinned` runs every case. The core-page tests need the SPIDER P-S tables (`ARAGOG_TEST_EOS_DIR`, [Build tests](../How-to/build_tests.md)); the stable-layer and Earth-budget cases take minutes. diff --git a/tests/AGENTS.md b/tests/AGENTS.md index 4e656bd3..ed21c298 100644 --- a/tests/AGENTS.md +++ b/tests/AGENTS.md @@ -36,7 +36,7 @@ A module-level constant read from an environment variable at import time does no ## Aragog specifics -Aragog has no `tools/check_test_quality.py`; `bash tools/validate_test_structure.sh` checks that every test (by function, class or module-level `pytestmark`) carries at least one of `unit`, `smoke`, `integration`, `slow` or `skip`, and CI runs it before the tests; it does not catch a second tier marker. Markers registered in `pyproject.toml`: `unit`, `smoke`, `integration`, `slow`, `physics_invariant`, `reference_pinned`; `addopts = "--strict-markers"` applies to a local run, while the CI commands pass `-o "addopts="`, so CI warns on an unregistered marker instead of failing. Most test files set no timeout and `pyproject.toml` sets no default, so the timeout part of the shared rule is not in place here yet; the nightly passes `--timeout`. +Aragog has no `tools/check_test_quality.py`; `bash tools/validate_test_structure.sh` checks that every test (by function, class or module-level `pytestmark`) carries at least one of `unit`, `smoke`, `integration`, `slow` or `skip`, and CI runs it before the tests; it does not catch a second tier marker, and 8 tests carry two today (`pytest --collect-only -m "(unit and (smoke or integration or slow)) or (smoke and (integration or slow)) or (integration and slow)"` lists them). Markers registered in `pyproject.toml`: `unit`, `smoke`, `integration`, `slow`, `physics_invariant`, `reference_pinned`; `addopts = "--strict-markers"` applies to a local run, while the CI commands pass `-o "addopts="`, so CI warns on an unregistered marker instead of failing. Most test files set no timeout and `pyproject.toml` sets no default, so the timeout part of the shared rule is not in place here yet; the nightly passes `--timeout`. - PR CI runs `pytest -m "unit and not slow" -n auto -o "addopts="`. The nightly runs `-m "unit or smoke or integration or slow"` with coverage, `tests/test_core_verification_page.py` in its own job in one process (`-p no:xdist`, its stable-layer case runs for many minutes), and combines the coverage against `fail_under = 90` in `pyproject.toml`. - The SPIDER P-S tables come from `ARAGOG_TEST_EOS_DIR`, else `$FWL_DATA/aragog/spider_eos`, else a parity cache next to the repository (`tests/conftest.py`). Without them every test marked `needs_eos` skips: read the skip count of a run, a green run without the tables tests far less. `pytest_collection_finish` parses the tables once before the first test, outside the per-test timeouts. diff --git a/tests/test_core_layer.py b/tests/test_core_layer.py index ae263d09..750913f3 100644 --- a/tests/test_core_layer.py +++ b/tests/test_core_layer.py @@ -71,7 +71,8 @@ def test_the_shell_heat_changes_by_the_flows_at_its_faces(): def test_a_young_layer_follows_the_erfc_solution_of_a_half_space(t_myr, validation_plot): """A fixed flow deficit Q_k - q at the top of a conducting half space raises the temperature by (2 dF / k) sqrt(kappa t) ierfc(z / 2 sqrt(kappa t)) (Carslaw and Jaeger, - the planar limit of Greenwood et al. 2021, eq. 27); the cells also cool at the rate the + the planar short-time counterpart of the fixed-gradient sphere of Greenwood et al. 2021, + eq. 27); the cells also cool at the rate the divergence of the adiabatic conduction sets. Earlier the top cell limits the match, later the curvature of the sphere. Weak mixing isolates the conduction: with the convecting core held, the sink would make the cells below the front mix.""" @@ -88,6 +89,8 @@ def test_a_young_layer_follows_the_erfc_solution_of_a_half_space(t_myr, validati near = x < 3.0 tol = 1e-2 * expected[-1] assert np.max(np.abs(t_shell - y0 - expected)[near]) < tol + if t_myr == 0.1: # discrimination: the ierfc profile without the cooling term misses + assert np.max(np.abs(t_shell - y0 - (expected - sink))[near]) > tol def figure(): import matplotlib.pyplot as plt diff --git a/tools/validate_test_structure.sh b/tools/validate_test_structure.sh index 65ecd4a4..ac3f6cff 100755 --- a/tools/validate_test_structure.sh +++ b/tools/validate_test_structure.sh @@ -1,22 +1,7 @@ #!/usr/bin/env bash -# Marker-validation gate for the aragog test suite. -# -# Walks every *.py file under tests/, finds every `def test_*` definition, -# and verifies that the function (or its enclosing class, or the -# module-level pytestmark) carries exactly one of: -# -# @pytest.mark.unit -# @pytest.mark.smoke -# @pytest.mark.integration -# @pytest.mark.slow -# @pytest.mark.skip -# -# Any unmarked test is printed as : and the script exits 1. -# Run from repository root: -# -# bash tools/validate_test_structure.sh -# -# CI invokes this before pytest in .github/workflows/ci_tests.yml. +# Marker check of the tests (CI runs it before pytest): every test_* function, by itself, its +# class or the module pytestmark, needs at least one of unit, smoke, integration, slow, skip. +# An unmarked test is printed as : and the script exits 1. Run from the repository root. set -e From 64826a4320d1af5e9991bdda1420f2edf34e0296 Mon Sep 17 00:00:00 2001 From: timlichtenberg Date: Fri, 23 Oct 2026 23:35:55 +0200 Subject: [PATCH 06/14] Mark the ierfc case as a physics invariant and name its guard The young-layer test carries physics_invariant next to reference_pinned, and its discrimination assert says what failed. The core page says that the test asserts the separation, tests/AGENTS.md gives the command that lists the double-marked tests without a count, and AGENTS.md says ruff enforces the future import and CI lints src/ and tests/. --- AGENTS.md | 2 +- docs/Validation/core.md | 2 +- tests/AGENTS.md | 2 +- tests/test_core_layer.py | 12 +++++++----- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index eccb8bc3..8c0e641d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -81,7 +81,7 @@ The state is the entropy at the mesh nodes, plus extra entries set by the core b ## Conventions -- Imports go to the defining module (`from aragog.solver import EntropySolver`, `from aragog.eos.entropy import EntropyEOS`); there are no compatibility shims. Every non-empty `.py` file starts with `from __future__ import annotations`; ruff enforces it where CI lints, in `src/` and `tests/` (`I002`, `required-imports` in `pyproject.toml`). `aragog/__init__.py` re-exports `EntropySolver` at its end (`# noqa: E402`) to break the import cycle with `aragog.solver`; keep it there. +- Imports go to the defining module (`from aragog.solver import EntropySolver`, `from aragog.eos.entropy import EntropyEOS`); there are no compatibility shims. Every non-empty `.py` file starts with `from __future__ import annotations`; ruff enforces it (`I002`, `required-imports` in `pyproject.toml`; CI lints `src/` and `tests/`). `aragog/__init__.py` re-exports `EntropySolver` at its end (`# noqa: E402`) to break the import cycle with `aragog.solver`; keep it there. - The version comes from the latest git tag through setuptools-scm (CalVer); `src/aragog/_version.py` is generated and ignored. Releases: `docs/How-to/releasing.md`. - Style: ruff with single quotes and a line length of 96, NumPy docstrings, comments that say why. Citations in the docs prefer SciX URLs, with the DOI as fallback; never invent a bibcode. - Docs: Zensical with `use_directory_urls: false`; a new page needs its file and its `mkdocs.yml` nav entry; a new module gets a stub under `docs/Reference/api/`. The Validation tab (`docs/Validation/`) holds the core and numerics verification pages and the overview of the `reference_pinned` cases; the script figures are in `docs/figures/vv/` (PNG on the site, PDF beside it), the plot pairs of the tests in `docs/assets/validation/`. `tests/test_core_verification_page.py` checks every tagged number of `docs/Validation/core.md` against `docs/figures/vv/core_verification_values.json`. diff --git a/docs/Validation/core.md b/docs/Validation/core.md index 54214cba..9a717825 100644 --- a/docs/Validation/core.md +++ b/docs/Validation/core.md @@ -549,7 +549,7 @@ Top: the expected temperature rise (line) and the rise of the cells (dots) again - Reference: the constant-flux half-space solution above (analytical). - Tolerance: 1 % of the expected rise of the top cell, 0.0098 K after 0.03 Myr and 0.020 K after 0.1 Myr; the largest differences are 0.0053 K over 9 cells and 0.0083 K over 13 cells. -- The cooling term comes from the shell's own rates (`CoreShell.rates`), not from an independent formula. Without it the 0.03 Myr case still passes (0.0064 K); the 0.1 Myr case separates it (0.0216 K against 0.020 K) and asserts that it does. +- The cooling term comes from the shell's own rates (`CoreShell.rates`), not from an independent formula. Without it the 0.03 Myr case still passes (0.0064 K); the 0.1 Myr case separates it (0.0216 K against 0.020 K), and the test asserts this separation. - Test id: `tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space` - Command: `pytest tests/test_core_layer.py -k erfc` (add `--validation-plots` to write the plot pairs) diff --git a/tests/AGENTS.md b/tests/AGENTS.md index ed21c298..376d0b7b 100644 --- a/tests/AGENTS.md +++ b/tests/AGENTS.md @@ -36,7 +36,7 @@ A module-level constant read from an environment variable at import time does no ## Aragog specifics -Aragog has no `tools/check_test_quality.py`; `bash tools/validate_test_structure.sh` checks that every test (by function, class or module-level `pytestmark`) carries at least one of `unit`, `smoke`, `integration`, `slow` or `skip`, and CI runs it before the tests; it does not catch a second tier marker, and 8 tests carry two today (`pytest --collect-only -m "(unit and (smoke or integration or slow)) or (smoke and (integration or slow)) or (integration and slow)"` lists them). Markers registered in `pyproject.toml`: `unit`, `smoke`, `integration`, `slow`, `physics_invariant`, `reference_pinned`; `addopts = "--strict-markers"` applies to a local run, while the CI commands pass `-o "addopts="`, so CI warns on an unregistered marker instead of failing. Most test files set no timeout and `pyproject.toml` sets no default, so the timeout part of the shared rule is not in place here yet; the nightly passes `--timeout`. +Aragog has no `tools/check_test_quality.py`; `bash tools/validate_test_structure.sh` checks that every test (by function, class or module-level `pytestmark`) carries at least one of `unit`, `smoke`, `integration`, `slow` or `skip`, and CI runs it before the tests; it does not catch a second tier marker, and some tests carry two (`pytest --collect-only -m "(unit and (smoke or integration or slow)) or (smoke and (integration or slow)) or (integration and slow)"` lists them). Markers registered in `pyproject.toml`: `unit`, `smoke`, `integration`, `slow`, `physics_invariant`, `reference_pinned`; `addopts = "--strict-markers"` applies to a local run, while the CI commands pass `-o "addopts="`, so CI warns on an unregistered marker instead of failing. Most test files set no timeout and `pyproject.toml` sets no default, so the timeout part of the shared rule is not in place here yet; the nightly passes `--timeout`. - PR CI runs `pytest -m "unit and not slow" -n auto -o "addopts="`. The nightly runs `-m "unit or smoke or integration or slow"` with coverage, `tests/test_core_verification_page.py` in its own job in one process (`-p no:xdist`, its stable-layer case runs for many minutes), and combines the coverage against `fail_under = 90` in `pyproject.toml`. - The SPIDER P-S tables come from `ARAGOG_TEST_EOS_DIR`, else `$FWL_DATA/aragog/spider_eos`, else a parity cache next to the repository (`tests/conftest.py`). Without them every test marked `needs_eos` skips: read the skip count of a run, a green run without the tables tests far less. `pytest_collection_finish` parses the tables once before the first test, outside the per-test timeouts. diff --git a/tests/test_core_layer.py b/tests/test_core_layer.py index 750913f3..6ac32cd9 100644 --- a/tests/test_core_layer.py +++ b/tests/test_core_layer.py @@ -66,16 +66,17 @@ def test_the_shell_heat_changes_by_the_flows_at_its_faces(): assert content == pytest.approx(expected, rel=1e-10, abs=0) +@pytest.mark.physics_invariant @pytest.mark.reference_pinned @pytest.mark.parametrize('t_myr', [0.03, 0.1]) def test_a_young_layer_follows_the_erfc_solution_of_a_half_space(t_myr, validation_plot): """A fixed flow deficit Q_k - q at the top of a conducting half space raises the temperature by (2 dF / k) sqrt(kappa t) ierfc(z / 2 sqrt(kappa t)) (Carslaw and Jaeger, the planar short-time counterpart of the fixed-gradient sphere of Greenwood et al. 2021, - eq. 27); the cells also cool at the rate the - divergence of the adiabatic conduction sets. Earlier the top cell limits the match, later - the curvature of the sphere. Weak mixing isolates the conduction: with the convecting - core held, the sink would make the cells below the front mix.""" + eq. 27); the cells also cool at the rate the divergence of the adiabatic conduction sets. + Earlier the top cell limits the match, later the curvature of the sphere. Weak mixing + isolates the conduction: with the convecting core held, the sink would make the cells + below the front mix.""" shell = _shell(k_mix=1e-3) p, t = shell.profiles, t_myr * MYR q_k = float(_q_ad(p, K_CORE, p.r_cmb, T_C)) @@ -90,7 +91,8 @@ def test_a_young_layer_follows_the_erfc_solution_of_a_half_space(t_myr, validati tol = 1e-2 * expected[-1] assert np.max(np.abs(t_shell - y0 - expected)[near]) < tol if t_myr == 0.1: # discrimination: the ierfc profile without the cooling term misses - assert np.max(np.abs(t_shell - y0 - (expected - sink))[near]) > tol + miss = np.max(np.abs(t_shell - y0 - (expected - sink))[near]) + assert miss > tol, 'the cooling term no longer separates the ierfc profile' def figure(): import matplotlib.pyplot as plt From 2045781c2b8d0147d717e3661ed7a72e6fb3ac23 Mon Sep 17 00:00:00 2001 From: timlichtenberg Date: Sat, 24 Oct 2026 01:20:08 +0200 Subject: [PATCH 07/14] Guard the ierfc case against the plausible wrong profiles The young-layer test now asserts at both times that erfc in place of ierfc, and a halved amplitude, miss by more than ten times the tolerance; the check that the profile without the cooling term misses stays as a second guard at 0.1 Myr. The docstring names the constant-flux half-space solution and eq. 27 of Greenwood et al. (2021) as its sources. --- tests/test_core_layer.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/tests/test_core_layer.py b/tests/test_core_layer.py index 6ac32cd9..7424bfec 100644 --- a/tests/test_core_layer.py +++ b/tests/test_core_layer.py @@ -71,12 +71,12 @@ def test_the_shell_heat_changes_by_the_flows_at_its_faces(): @pytest.mark.parametrize('t_myr', [0.03, 0.1]) def test_a_young_layer_follows_the_erfc_solution_of_a_half_space(t_myr, validation_plot): """A fixed flow deficit Q_k - q at the top of a conducting half space raises the - temperature by (2 dF / k) sqrt(kappa t) ierfc(z / 2 sqrt(kappa t)) (Carslaw and Jaeger, - the planar short-time counterpart of the fixed-gradient sphere of Greenwood et al. 2021, - eq. 27); the cells also cool at the rate the divergence of the adiabatic conduction sets. - Earlier the top cell limits the match, later the curvature of the sphere. Weak mixing - isolates the conduction: with the convecting core held, the sink would make the cells - below the front mix.""" + temperature by (2 dF / k) sqrt(kappa t) ierfc(z / 2 sqrt(kappa t)), the constant-flux + solution of a planar half space and the short-time counterpart of the fixed-gradient sphere + of Greenwood et al. 2021, eq. 27; the cells also cool at the rate the divergence of the + adiabatic conduction sets. Earlier the top cell limits the match, later the curvature of + the sphere. Weak mixing isolates the conduction: with the convecting core held, the sink + would make the cells below the front mix.""" shell = _shell(k_mix=1e-3) p, t = shell.profiles, t_myr * MYR q_k = float(_q_ad(p, K_CORE, p.r_cmb, T_C)) @@ -86,10 +86,14 @@ def test_a_young_layer_follows_the_erfc_solution_of_a_half_space(t_myr, validati x = (p.r_cmb - np.asarray(shell.r_cells)) / (2.0 * length) ierfc = np.exp(-(x**2)) / np.sqrt(np.pi) - x * erfc(x) deficit = (q_k - 8e12) / (4.0 * np.pi * p.r_cmb**2) - expected = 2.0 * deficit / K_CORE * length * ierfc + sink + amplitude = 2.0 * deficit / K_CORE * length + expected = amplitude * ierfc + sink near = x < 3.0 tol = 1e-2 * expected[-1] assert np.max(np.abs(t_shell - y0 - expected)[near]) < tol + # discrimination: erfc in place of ierfc and a halved amplitude miss by more than 10 tol + for wrong in (amplitude * erfc(x) + sink, 0.5 * amplitude * ierfc + sink): + assert np.max(np.abs(t_shell - y0 - wrong)[near]) > 10.0 * tol if t_myr == 0.1: # discrimination: the ierfc profile without the cooling term misses miss = np.max(np.abs(t_shell - y0 - (expected - sink))[near]) assert miss > tol, 'the cooling term no longer separates the ierfc profile' From 38030af7a716461844bcfb3af4d9aca9582fbf75 Mon Sep 17 00:00:00 2001 From: timlichtenberg Date: Sat, 24 Oct 2026 01:20:08 +0200 Subject: [PATCH 08/14] Correct the Validation and coupling pages against the code The young-layer case is section 14 of the core page and quotes no run result, so the statement that every number of sections 1 to 13 comes from the verification script holds; section 12 names the right-hand-side parity test. The overview gives the tolerance of each case and says which cases have their own section. The coupling page lists every hf_row key the Aragog wrapper returns, says that it writes neither T_surf nor dt_actual, and that PROTEUS refuses a dilatation key. The numerics page names the test-table lookup order, the figures README says where its seven figures are shown, and the agent files state the reference_pinned exceptions. --- AGENTS.md | 2 +- docs/Explanations/proteus_coupling.md | 7 ++---- docs/Validation/core.md | 14 ++++++----- docs/Validation/index.md | 36 ++++++++++++++------------- docs/Validation/numerics.md | 2 +- tests/AGENTS.md | 1 + tools/verification/figures/README.md | 2 +- 7 files changed, 33 insertions(+), 31 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 8c0e641d..42cb3114 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -84,7 +84,7 @@ The state is the entropy at the mesh nodes, plus extra entries set by the core b - Imports go to the defining module (`from aragog.solver import EntropySolver`, `from aragog.eos.entropy import EntropyEOS`); there are no compatibility shims. Every non-empty `.py` file starts with `from __future__ import annotations`; ruff enforces it (`I002`, `required-imports` in `pyproject.toml`; CI lints `src/` and `tests/`). `aragog/__init__.py` re-exports `EntropySolver` at its end (`# noqa: E402`) to break the import cycle with `aragog.solver`; keep it there. - The version comes from the latest git tag through setuptools-scm (CalVer); `src/aragog/_version.py` is generated and ignored. Releases: `docs/How-to/releasing.md`. - Style: ruff with single quotes and a line length of 96, NumPy docstrings, comments that say why. Citations in the docs prefer SciX URLs, with the DOI as fallback; never invent a bibcode. -- Docs: Zensical with `use_directory_urls: false`; a new page needs its file and its `mkdocs.yml` nav entry; a new module gets a stub under `docs/Reference/api/`. The Validation tab (`docs/Validation/`) holds the core and numerics verification pages and the overview of the `reference_pinned` cases; the script figures are in `docs/figures/vv/` (PNG on the site, PDF beside it), the plot pairs of the tests in `docs/assets/validation/`. `tests/test_core_verification_page.py` checks every tagged number of `docs/Validation/core.md` against `docs/figures/vv/core_verification_values.json`. +- Docs: Zensical with `use_directory_urls: false`; a new page needs its file and its `mkdocs.yml` nav entry; a new module gets a stub under `docs/Reference/api/`. The Validation tab (`docs/Validation/`) holds the core and numerics verification pages and the overview of the `reference_pinned` cases (only the young-layer case has its own section with reference, tolerance, test id and command so far; the other cases are described in the prose of their section); the script figures are in `docs/figures/vv/` (PNG on the site, PDF beside it), the plot pairs of the tests in `docs/assets/validation/`. `tests/test_core_verification_page.py` checks every tagged number of `docs/Validation/core.md` against `docs/figures/vv/core_verification_values.json`. ## Agent files diff --git a/docs/Explanations/proteus_coupling.md b/docs/Explanations/proteus_coupling.md index 964707d9..aa0b145c 100644 --- a/docs/Explanations/proteus_coupling.md +++ b/docs/Explanations/proteus_coupling.md @@ -83,19 +83,16 @@ Aragog reads the current planet boundary state from `hf_row`, runs its time step | Key | Meaning | |---|---| | `T_magma` | Mantle potential temperature [K], mass-weighted from the entropy field | -| `T_surf` | Top-of-mantle temperature [K] | -| `T_core` | Core temperature [K], evolved via the energy-balance BC | | `Phi_global` | Global melt fraction [0, 1], mass-weighted | | `F_int` | Top-of-mantle energy flux [W m$^{-2}$] | | `F_radio` | Radiogenic surface-equivalent flux [W m$^{-2}$] | | `F_tidal` | Tidal surface-equivalent flux [W m$^{-2}$] (when `heat_tidal = true`) | -| `dt_actual` | Achieved time step [yr], may be less than requested when `phi_step_cap` fires | | `E_state_cons_J` | Frozen-mass integrated mantle enthalpy [J]; pairs with `dE_predicted_cons_J` for the conservation residual | -| `step_dE_*_J` | Per-call energy budget integrals [J]: `F_int_J`, `F_cmb_J`, `Q_radio_J`, `Q_tidal_J`, plus frozen-mass `Q_radio_cons_J`, `Q_tidal_cons_J` and `solver_residual_J` | +| `step_dE_*_J` | Per-call energy budget integrals [J]: `F_int_J`, `F_cmb_J`, `Q_radio_J`, `Q_tidal_J`, `compression_J`, plus frozen-mass `Q_radio_cons_J`, `Q_tidal_cons_J` and `solver_residual_J` | PROTEUS's coupler accumulates the per-call integrals into running cumulative columns: `dE_predicted_cons_J`, `E_residual_cons_J`, `E_residual_cons_frac` (the canonical conservation residual, ~5 % closure on multi-Myr trajectories) and `solver_residual_J` (entropy-ODE LHS-RHS, ~10⁻⁷ machine-precision noise floor). See [Energy diagnostics](energy_diagnostics.md) for the frozen-mass weighting rationale. -The full set of `hf_row` keys produced by Aragog is built in `proteus.interior_energetics.aragog.AragogRunner._build_helpfile_output` (line ~1740) and emitted by `run_solver` (line ~1509). +The full set of `hf_row` keys produced by Aragog is the dict that `proteus.interior_energetics.aragog.AragogRunner._build_helpfile_output` returns, which `run_solver` hands back and the interior wrapper copies into `hf_row` key by key: `M_mantle`, `M_core`, `T_magma`, `Phi_global`, `RF_depth`, `F_int`, `M_mantle_liquid`, `M_mantle_solid`, `Phi_global_vol`, `T_pot`, `T_cmb`, `T_cmb_node`, `E_th_mantle`, `Cp_eff`, `F_radio`, `F_tidal`, `E_state_J`, `E_state_cons_J`, `F_cmb`, `Q_radio_W`, `Q_tidal_W` and the `step_*` integrals above. Aragog does not write `T_surf`, which the atmosphere wrapper sets, nor `dt_actual`: `run_solver` returns the new model time, from which PROTEUS takes the achieved step. PROTEUS has no `dilatation` option for Aragog; a configuration that sets `interior_energetics.aragog.dilatation` is refused as an unknown key. ## Initial-condition contract diff --git a/docs/Validation/core.md b/docs/Validation/core.md index 9a717825..4db3c5f4 100644 --- a/docs/Validation/core.md +++ b/docs/Validation/core.md @@ -2,7 +2,7 @@ This page checks the `core_module` core boundary condition ([core_bc.md](../Explanations/core_bc.md)) and the `aragog.core` model behind it against analytic results, published values, an independent core evolution code and coupled runs. Each section states the physics a check tests, the reference and where it comes from, the value aragog gives, a figure and what it shows, the limits of the check where it has any, and the tests that pin it. A check shows that the code solves the stated equations to the stated accuracy; whether the equations describe a real core is a separate question, which the literature comparisons in sections 5 to 8 and the limits stated with each check address. -Every number on this page and every figure is produced by `tools/verification/run_core_verification.py`, which writes the figures to `docs/figures/vv/` and the numbers to `docs/figures/vv/core_verification_values.json`; `tests/test_core_verification_page.py` checks each number on the page against that file. The script needs the SPIDER-format EOS tables (`ARAGOG_TEST_EOS_DIR`) for sections 9 and 12 and the optional `verification` dependencies for the figure style, and runs in about eight minutes on one core. Sections 9 and 12 and the solver tests use the aragog test tables that CI uses, `spider_eos_test_data.tar.gz` of release `test-data-v1` (sha256 `e3842bc494969571428d7f978d8f70972d40a3bc5d6ac2c09f6a96808f4079fb`), 500 pressures from 0 to 150 GPa by 200 entropies; their numbers hold for these tables. Other tables give other values; section 12 compares two sets of 1350 pressures by 280 entropies that reach 650 GPa. Two kinds of number come from separate measurements and say so where they appear: the closures of the stratified call heated from above (section 13), held by its smoke test, and the expansivity differences on those two sets. Nimmo (2015) is chapter 8.02 of the *Treatise on Geophysics* wherever chapter 9.08 is not named. +Every number in sections 1 to 13 and every figure there is produced by `tools/verification/run_core_verification.py`, which writes the figures to `docs/figures/vv/` and the numbers to `docs/figures/vv/core_verification_values.json`; `tests/test_core_verification_page.py` checks each number on the page against that file. The script needs the SPIDER-format EOS tables (`ARAGOG_TEST_EOS_DIR`) for sections 9 and 12 and the optional `verification` dependencies for the figure style, and runs in about eight minutes on one core. Sections 9 and 12 and the solver tests use the aragog test tables that CI uses, `spider_eos_test_data.tar.gz` of release `test-data-v1` (sha256 `e3842bc494969571428d7f978d8f70972d40a3bc5d6ac2c09f6a96808f4079fb`), 500 pressures from 0 to 150 GPa by 200 entropies; their numbers hold for these tables. Other tables give other values; section 12 compares two sets of 1350 pressures by 280 entropies that reach 650 GPa. Two kinds of number come from separate measurements and say so where they appear: the closures of the stratified call heated from above (section 13), held by its smoke test, and the expansivity differences on those two sets. Nimmo (2015) is chapter 8.02 of the *Treatise on Geophysics* wherever chapter 9.08 is not named. ## 1. Gaussian core structure @@ -469,7 +469,7 @@ The difference with the tables is the difference of the two single-phase expansi **Limits.** In the three core states the CMB flux is the conduction across the half cell, and in runs separate from the script the numbers of this section do not change when the expansivity of the boundary layer is multiplied by 1.5 in both NumPy and JAX; `tests/test_jax_dsdt_core_module.py::test_jacobian_core_column_matches_central_differences` checks the boundary-layer flux instead, with the Jacobian columns of the core temperature and the bottom cell against central differences for a core 300 K above a base at melt fraction 0.66, where the test asserts a flux above $10^4$ W m$^{-2}$. -**Pinned by** `tests/test_jax_dsdt_core_module.py::test_boundary_slots_match_numpy_on_a_five_node_mesh`, `::test_jacobian_core_column_matches_central_differences` and `::test_jacobian_carries_boundary_couplings`. +**Pinned by** `tests/test_jax_dsdt_core_module.py::test_boundary_slots_match_numpy_on_a_five_node_mesh`, `::test_jacobian_core_column_matches_central_differences`, `::test_jacobian_carries_boundary_couplings` and `::test_rhs_parity_with_numpy_on_driven_state`. ## 13. Stable layer against thermal_history @@ -538,9 +538,11 @@ The two mixing constants are a model uncertainty, not a fitted quantity. Changin **Pinned by** `tests/test_core_verification_page.py::test_the_stable_layer_meets_the_thermal_history_bounds`; the analytic limits of the shell by `tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space`, `::test_a_steady_layer_reaches_the_quasi_static_depth` and `::test_mixing_keeps_a_superadiabatic_shell_on_the_adiabat`; a resumed solve by `tests/test_entropy_solver_core_module_smoke.py::test_a_resume_restarts_the_shell_where_the_previous_solve_ended`. -### Young layer against the ierfc solution +## 14. Young layer against the ierfc solution -A fixed deficit $\Delta F$ of the CMB flow below the conducted adiabatic flow, per unit area of the CMB, heats a conducting half space as $\Delta T(z, t) = (2 \Delta F / k) \sqrt{\kappa t}\, \mathrm{ierfc}(z / 2\sqrt{\kappa t})$, with $z$ the depth below the CMB and $\mathrm{ierfc}(x) = e^{-x^2}/\sqrt{\pi} - x\,\mathrm{erfc}(x)$. This is the constant-flux solution of a planar half space, the planar short-time counterpart of the fixed-gradient sphere of Greenwood et al. (2021, eq. 27). The test runs the shell with weak mixing (`k_mix = 1e-3`) under a CMB flow of 8 TW, adds to the solution the cooling that the divergence of the adiabatic conduction sets, and compares the temperature rise of the cells within $z < 6\sqrt{\kappa t}$ after 0.03 and 0.1 Myr. +This section states the setup and the tolerances its test asserts; it quotes no run result, so the page test has no number of it to check. + +A fixed deficit $\Delta F$ of the CMB flow below the conducted adiabatic flow, per unit area of the CMB, heats a conducting half space as $\Delta T(z, t) = (2 \Delta F / k) \sqrt{\kappa t}\, \mathrm{ierfc}(z / 2\sqrt{\kappa t})$, with $z$ the depth below the CMB and $\mathrm{ierfc}(x) = e^{-x^2}/\sqrt{\pi} - x\,\mathrm{erfc}(x)$. This is the constant-flux solution of a planar half space, the short-time counterpart of the fixed-gradient sphere of Greenwood et al. (2021, eq. 27). The test runs the shell with weak mixing (`k_mix = 1e-3`) under a CMB flow of 8 TW, adds to the solution the cooling that the divergence of the adiabatic conduction sets, and compares the temperature rise of the cells within $z < 6\sqrt{\kappa t}$ after 0.03 and 0.1 Myr. ![Temperature rise below the CMB after 0.1 Myr and its difference from the ierfc solution](../assets/validation/core_layer_erfc_half_space_0p1myr.svg#only-light){ width="80%" } ![Temperature rise below the CMB after 0.1 Myr and its difference from the ierfc solution](../assets/validation/core_layer_erfc_half_space_0p1myr_dark.svg#only-dark){ width="80%" } @@ -548,8 +550,8 @@ A fixed deficit $\Delta F$ of the CMB flow below the conducted adiabatic flow, p Top: the expected temperature rise (line) and the rise of the cells (dots) against depth below the CMB after 0.1 Myr. Bottom: their absolute difference and the tolerance (dashed). The 0.03 Myr case writes a second pair, `core_layer_erfc_half_space_0p03myr`. - Reference: the constant-flux half-space solution above (analytical). -- Tolerance: 1 % of the expected rise of the top cell, 0.0098 K after 0.03 Myr and 0.020 K after 0.1 Myr; the largest differences are 0.0053 K over 9 cells and 0.0083 K over 13 cells. -- The cooling term comes from the shell's own rates (`CoreShell.rates`), not from an independent formula. Without it the 0.03 Myr case still passes (0.0064 K); the 0.1 Myr case separates it (0.0216 K against 0.020 K), and the test asserts this separation. +- Tolerance: 1 % of the expected rise of the top cell, at both times. +- Discrimination: the test asserts that $\mathrm{erfc}$ in place of $\mathrm{ierfc}$, and a halved amplitude, miss by more than 10 times the tolerance at both times. The cooling term comes from the shell's own rates (`CoreShell.rates`), not from an independent formula; the test asserts that the profile without it misses the tolerance after 0.1 Myr. - Test id: `tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space` - Command: `pytest tests/test_core_layer.py -k erfc` (add `--validation-plots` to write the plot pairs) diff --git a/docs/Validation/index.md b/docs/Validation/index.md index 5fa9132d..490d2abc 100644 --- a/docs/Validation/index.md +++ b/docs/Validation/index.md @@ -1,27 +1,29 @@ # Validation -This tab lists the tests that compare Aragog with an independent result: a published benchmark, an analytical limit or a second implementation. Each case carries the `reference_pinned` marker. Where a page section exists, the table links it; a case with a plot pair has a subsection with its reference, tolerance, test id and command. +This tab lists the tests that compare Aragog with an independent result: a published benchmark, an analytical limit or a second implementation. Each case in the table carries the `reference_pinned` marker. Where a page section exists, the table links it. - [Core](core.md): the core module against Nimmo (2015), the `leeds` and `leeds_thermal` models of thermal_history (Greenwood et al. 2021) and analytical limits. -- [Numerics](numerics.md): the first-principles checks of the solver, the equation of state and the NumPy and JAX right-hand sides. +- [Numerics](numerics.md): the first-principles checks of the solver, the equation of state and the NumPy and JAX right-hand sides; these are checks, not `reference_pinned` cases. ## Cases by process -| Process | Case | Test | Reference | Page | -|---|---|---|---|---| -| Core stable layer | Young layer against the ierfc solution | `tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space` | constant-flux half space (analytical) | [Core](core.md#young-layer-against-the-ierfc-solution) | -| Core stable layer | Quasi-static depth of a steady layer | `tests/test_core_layer.py::test_a_steady_layer_reaches_the_quasi_static_depth` | conducted adiabatic flow equal to the CMB flow | [Core](core.md#13-stable-layer-against-thermal_history) | -| Core stable layer | Layers under fixed and eroding CMB flows | `tests/test_core_verification_page.py::test_the_stable_layer_meets_the_thermal_history_bounds` | `leeds_thermal` (Greenwood et al. 2021) | [Core](core.md#13-stable-layer-against-thermal_history) | -| Core energy budget | Earth budget | `tests/test_core_verification_page.py::test_the_earth_budget_meets_nimmo_tables_4_and_5` | Nimmo (2015), Tables 4 and 5 | [Core](core.md#5-nimmo-2015-earth-core-budget) | -| Core energy budget | Printed values of the two Nimmo core models | `tests/test_core_nimmo_benchmarks.py` (the `reference_pinned` tests except the cross-check) | Nimmo (2015), ch. 9.08 Table 2, Figure 3 caption (p. 211) and p. 212; ch. 8.02 Table 2 | [Core](core.md#5-nimmo-2015-earth-core-budget) | -| Core energy budget | Model-2 budget terms at 4180 K | `tests/test_core_nimmo_benchmarks.py::test_budget_terms_match_thermal_history_cross_check` | `leeds` (Greenwood et al. 2021); Nimmo (2015), ch. 8.02 Tables 2 and 4 | [Core](core.md#6-budget-terms-against-thermal_history) | -| Core energy budget | Budget terms | `tests/test_core_verification_page.py::test_budget_terms_match_the_thermal_history_table` | `leeds` (Greenwood et al. 2021) | [Core](core.md#6-budget-terms-against-thermal_history) | -| Core energy budget | Entropy terms | `tests/test_core_entropy.py::test_entropy_capacities_match_thermal_history` | `leeds` (Greenwood et al. 2021) | [Core](core.md#6-budget-terms-against-thermal_history) | -| Core dynamo | Efficiency factors and dynamo threshold | `tests/test_core_entropy.py::test_chr09_efficiency_factors_reproduce_printed_values`, `::test_dynamo_threshold_and_margin` | Christensen et al. (2009); Nimmo (2015) | [Core](core.md#7-dynamo-criterion-and-field-strength) | -| Core thermal history | Core history | `tests/test_core_verification_page.py::test_core_history_matches_the_thermal_history_table` | `leeds` (Greenwood et al. 2021) | [Core](core.md#10-a-core-thermal-history-against-thermal_history) | -| Core numerics | NumPy and JAX right-hand sides | `tests/test_jax_dsdt_core_module.py::test_rhs_parity_with_numpy_on_driven_state` | NumPy implementation | [Core](core.md#12-numpy-and-jax-right-hand-sides-and-the-analytic-jacobian) | -| Mantle heat transport | Theta-dependent top slope of the mixing length | `tests/test_mlt_calibration.py::test_theta_dependent_top_slope_hand_values_and_limits` | hand values and limits | no page section yet | -| Mantle rheology | Arrhenius viscosity, yield stress, NumPy and JAX kernels | `tests/test_rheology_verification.py` (its `reference_pinned` tests) | reference limits, NumPy implementation | no page section yet ([Solid-state convection](../Explanations/solid_state_convection.md) explains the rheology) | +| Process | Case | Test | Reference | Tolerance | Page | +|---|---|---|---|---|---| +| Core stable layer | Young layer against the ierfc solution | `tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space` | constant-flux half space (analytical) | 1 % of the expected rise of the top cell | [Core](core.md#14-young-layer-against-the-ierfc-solution) | +| Core stable layer | Quasi-static depth of a steady layer | `tests/test_core_layer.py::test_a_steady_layer_reaches_the_quasi_static_depth` | conducted adiabatic flow equal to the CMB flow | 5 % | [Core](core.md#13-stable-layer-against-thermal_history) | +| Core stable layer | Layers under fixed and eroding CMB flows | `tests/test_core_verification_page.py::test_the_stable_layer_meets_the_thermal_history_bounds` | `leeds_thermal` (Greenwood et al. 2021) | T_cmb 5 K, T_cen 10 K, layer depth within a factor 1.5, inner core 2 % at the end, onset 5 % | [Core](core.md#13-stable-layer-against-thermal_history) | +| Core energy budget | Earth budget | `tests/test_core_verification_page.py::test_the_earth_budget_meets_nimmo_tables_4_and_5` | Nimmo (2015), Tables 4 and 5 | 2 %, four documented exceptions | [Core](core.md#5-nimmo-2015-earth-core-budget) | +| Core energy budget | Printed values of the two Nimmo core models | `tests/test_core_nimmo_benchmarks.py` (the `reference_pinned` tests except the cross-check) | Nimmo (2015), ch. 9.08 Table 2, Figure 3 caption (p. 211) and p. 212; ch. 8.02 Table 2 | 5e-4 to 3 % per printed value, Cr 5 % and 15 %, the baseline headline 5 % to 45 % | [Core](core.md#5-nimmo-2015-earth-core-budget) | +| Core energy budget | Model-2 budget terms at 4180 K | `tests/test_core_nimmo_benchmarks.py::test_budget_terms_match_thermal_history_cross_check` | `leeds` (Greenwood et al. 2021); Nimmo (2015), ch. 8.02 Tables 2 and 4 | 1e-5 to 2 % per term | [Core](core.md#6-budget-terms-against-thermal_history) | +| Core energy budget | Budget terms | `tests/test_core_verification_page.py::test_budget_terms_match_the_thermal_history_table` | `leeds` (Greenwood et al. 2021) | 1e-3 relative | [Core](core.md#6-budget-terms-against-thermal_history) | +| Core energy budget | Entropy terms | `tests/test_core_entropy.py::test_entropy_capacities_match_thermal_history` | `leeds` (Greenwood et al. 2021) | 1e-6 to 1e-4 relative | [Core](core.md#6-budget-terms-against-thermal_history) | +| Core dynamo | Efficiency factors and dynamo threshold | `tests/test_core_entropy.py::test_chr09_efficiency_factors_reproduce_printed_values`, `::test_dynamo_threshold_and_margin` | Christensen et al. (2009); Nimmo (2015) | 1e-3 relative; threshold below 6.5 TW | [Core](core.md#7-dynamo-criterion-and-field-strength) | +| Core thermal history | Core history | `tests/test_core_verification_page.py::test_core_history_matches_the_thermal_history_table` | `leeds` (Greenwood et al. 2021) | T_cmb 0.2 K, T_cen 1 K, ICB radius 1 %, the same onset | [Core](core.md#10-a-core-thermal-history-against-thermal_history) | +| Core numerics | NumPy and JAX right-hand sides | `tests/test_jax_dsdt_core_module.py::test_rhs_parity_with_numpy_on_driven_state` | NumPy implementation | 1e-8 relative | [Core](core.md#12-numpy-and-jax-right-hand-sides-and-the-analytic-jacobian) | +| Mantle heat transport | Theta-dependent top slope of the mixing length | `tests/test_mlt_calibration.py::test_theta_dependent_top_slope_hand_values_and_limits` | hand values and limits | 1e-15 to 1e-13 relative | no page section yet | +| Mantle rheology | Arrhenius viscosity, yield stress, NumPy and JAX kernels | `tests/test_rheology_verification.py` (its `reference_pinned` tests) | reference limits, NumPy implementation | 1e-12 relative (one bound 1e-9) | no page section yet ([Solid-state convection](../Explanations/solid_state_convection.md) explains the rheology) | + +The command of a case is `pytest `; only the young-layer case has a section with its own reference, tolerance, test id and command so far, and the other cases are described by the prose of their linked section. `pytest -m reference_pinned` runs every case. The core-page tests need the SPIDER P-S tables (`ARAGOG_TEST_EOS_DIR`, [Build tests](../How-to/build_tests.md)); the stable-layer and Earth-budget cases take minutes. diff --git a/docs/Validation/numerics.md b/docs/Validation/numerics.md index 9b403f23..43356d99 100644 --- a/docs/Validation/numerics.md +++ b/docs/Validation/numerics.md @@ -102,7 +102,7 @@ pytest -m unit tests/ # unit tests only pytest -m smoke tests/ # smoke tests only ``` -The JAX-path tests require `jax` and `equinox` to be installed; tests are skipped (not failed) when the optional dependencies are missing. Some tests also require the SPIDER-format EOS table directory; they skip if `FWL_DATA` is unset or does not contain the expected files. +The JAX-path tests require `jax` and `equinox` to be installed; tests are skipped (not failed) when the optional dependencies are missing. Some tests also require the SPIDER-format EOS table directory; they look in `ARAGOG_TEST_EOS_DIR`, then in `$FWL_DATA/aragog/spider_eos`, then in a parity cache next to the repository (`tests/conftest.py`), and skip when none of them exists. ## Physical constraints checked diff --git a/tests/AGENTS.md b/tests/AGENTS.md index 376d0b7b..d316faad 100644 --- a/tests/AGENTS.md +++ b/tests/AGENTS.md @@ -43,4 +43,5 @@ Aragog has no `tools/check_test_quality.py`; `bash tools/validate_test_structure - `tests/conftest.py` holds the table helpers (`entropy_eos_copy`, `entropy_eos_jax`, `make_mesh`) and the `helper` fixture (the bundled configs and the reference data in `tests/reference/`); other fixtures are in the test files. - `tests/test_core_verification_page.py` checks each tagged number of `docs/Validation/core.md` against `docs/figures/vv/core_verification_values.json` and reruns the items of `tools/verification/run_core_verification.py` it names; a change to the page, the values file or the script goes with that test. - The smoke tests of the JAX solver need `diffrax`, which the nightly installs and no extra lists. +- The shared rule pairs `reference_pinned` with `physics_invariant`; 8 of the 23 `reference_pinned` tests here carry `reference_pinned` alone (the core page tests, a JAX parity test and three single-value pins). - Tests that take the `validation_plot` fixture need `proteus-mpl` (in the `test` extra and in the CI installs); `pytest -m reference_pinned --validation-plots` writes their plot pairs to `docs/assets/validation/`. diff --git a/tools/verification/figures/README.md b/tools/verification/figures/README.md index 485cec80..c597d244 100644 --- a/tools/verification/figures/README.md +++ b/tools/verification/figures/README.md @@ -1,6 +1,6 @@ # Verification figures -Reproducible scripts that generate the seven first-principles verification figures embedded in `docs/Validation/numerics.md`. +Reproducible scripts that generate the seven first-principles verification figures; `docs/Validation/numerics.md` embeds five of them and `docs/Explanations/heat_transport.md` the other two. Each script is a self-contained verification of one numerical or physical aspect of Aragog and writes its output to `docs/figures/vv/.{pdf,png}` (tracked in git) plus a raw-data `.npz` to `output/aragog_vv_data/` (gitignored). ## Layout From be5ab9f4a09ab0ddf3bfcd1ef9b810cff74d2447 Mon Sep 17 00:00:00 2001 From: timlichtenberg Date: Sat, 24 Oct 2026 02:04:57 +0200 Subject: [PATCH 09/14] Try the proteus-mpl plot checks on Linux and macOS CI Install proteus-mpl from the tl/validation-ci commit, which renders the validation plot on every run and compares its data summary with the committed pair. The erfc pairs are made again so that they carry the summary. A session hook prints each rendered summary as a warning, so the log holds the full summary of each job for a comparison with the committed one. This branch is a trial and is not meant for main. --- .github/workflows/ci_tests.yml | 2 +- .../core_layer_erfc_half_space_0p03myr.svg | 2 +- .../core_layer_erfc_half_space_0p03myr_dark.svg | 2 +- .../core_layer_erfc_half_space_0p1myr.svg | 2 +- .../core_layer_erfc_half_space_0p1myr_dark.svg | 2 +- tests/conftest.py | 17 +++++++++++++++++ 6 files changed, 22 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci_tests.yml b/.github/workflows/ci_tests.yml index dc325353..5db63675 100644 --- a/.github/workflows/ci_tests.yml +++ b/.github/workflows/ci_tests.yml @@ -76,7 +76,7 @@ jobs: - name: Install pip-only test deps (scikits-odes-sundials builds against conda SUNDIALS) run: | - pip install pytest pytest-xdist pytest-dependency pytest-timeout hypothesis ruff "proteus-mpl>=1.4.0" + pip install pytest pytest-xdist pytest-dependency pytest-timeout hypothesis ruff "proteus-mpl @ git+https://github.com/FormingWorlds/proteus-visual-language@e95980eec3384e072aed71451aca9b5543d51a14#subdirectory=figures/proteus-mpl" # Install scikits-odes-sundials directly (skip the scikits-odes # metapackage). This avoids scikits-odes-daepack, which is a # transitive Fortran dep with f2py incompatibility with numpy diff --git a/docs/assets/validation/core_layer_erfc_half_space_0p03myr.svg b/docs/assets/validation/core_layer_erfc_half_space_0p03myr.svg index 86816beb..f156cd35 100644 --- a/docs/assets/validation/core_layer_erfc_half_space_0p03myr.svg +++ b/docs/assets/validation/core_layer_erfc_half_space_0p03myr.svg @@ -6,7 +6,7 @@ - test: tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.03]; command: pytest 'tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.03]' --validation-plots + test: tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.03]; command: pytest 'tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.03]' --validation-plots; summary: {"axes": [{"lines": [[9, 1.0, 22.190359007197433, 10.769200446038505, -0.006453914739208852, 0.9842613130845388, 0.21501919580761694], [9, 1.0, 22.190359007197433, 10.769200446038505, -0.00638833208631695, 0.9892611229897739, 0.21809951937181418]], "collections": [], "images": []}, {"lines": [[9, 1.0, 22.190359007197433, 10.769200446038505, 6.55826528919018e-05, 0.005285845252757354, 0.0030803235641972656], [2, 0.0, 1.0, 0.5, 0.009842613130845388, 0.009842613130845388, 0.009842613130845388]], "collections": [], "images": []}]} image/svg+xml diff --git a/docs/assets/validation/core_layer_erfc_half_space_0p03myr_dark.svg b/docs/assets/validation/core_layer_erfc_half_space_0p03myr_dark.svg index c4fd6768..e07e5dfe 100644 --- a/docs/assets/validation/core_layer_erfc_half_space_0p03myr_dark.svg +++ b/docs/assets/validation/core_layer_erfc_half_space_0p03myr_dark.svg @@ -6,7 +6,7 @@ - test: tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.03]; command: pytest 'tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.03]' --validation-plots + test: tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.03]; command: pytest 'tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.03]' --validation-plots; summary: {"axes": [{"lines": [[9, 1.0, 22.190359007197433, 10.769200446038505, -0.006453914739208852, 0.9842613130845388, 0.21501919580761694], [9, 1.0, 22.190359007197433, 10.769200446038505, -0.00638833208631695, 0.9892611229897739, 0.21809951937181418]], "collections": [], "images": []}, {"lines": [[9, 1.0, 22.190359007197433, 10.769200446038505, 6.55826528919018e-05, 0.005285845252757354, 0.0030803235641972656], [2, 0.0, 1.0, 0.5, 0.009842613130845388, 0.009842613130845388, 0.009842613130845388]], "collections": [], "images": []}]} image/svg+xml diff --git a/docs/assets/validation/core_layer_erfc_half_space_0p1myr.svg b/docs/assets/validation/core_layer_erfc_half_space_0p1myr.svg index 2b5a67ab..941321e8 100644 --- a/docs/assets/validation/core_layer_erfc_half_space_0p1myr.svg +++ b/docs/assets/validation/core_layer_erfc_half_space_0p1myr.svg @@ -6,7 +6,7 @@ - test: tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.1]; command: pytest 'tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.1]' --validation-plots + test: tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.1]; command: pytest 'tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.1]' --validation-plots; summary: {"axes": [{"lines": [[13, 1.0, 37.90024197979272, 17.203233993860785, -0.021575216626792508, 2.000335099899167, 0.4635526584222581], [13, 1.0, 37.90024197979272, 17.203233993860785, -0.02147987618263869, 2.0085919484035912, 0.46683775846745096]], "collections": [], "images": []}, {"lines": [[13, 1.0, 37.90024197979272, 17.203233993860785, 9.53404441538172e-05, 0.008256848504424319, 0.003285100045192855], [2, 0.0, 1.0, 0.5, 0.02000335099899167, 0.02000335099899167, 0.02000335099899167]], "collections": [], "images": []}]} image/svg+xml diff --git a/docs/assets/validation/core_layer_erfc_half_space_0p1myr_dark.svg b/docs/assets/validation/core_layer_erfc_half_space_0p1myr_dark.svg index 8714582f..3d8da784 100644 --- a/docs/assets/validation/core_layer_erfc_half_space_0p1myr_dark.svg +++ b/docs/assets/validation/core_layer_erfc_half_space_0p1myr_dark.svg @@ -6,7 +6,7 @@ - test: tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.1]; command: pytest 'tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.1]' --validation-plots + test: tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.1]; command: pytest 'tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.1]' --validation-plots; summary: {"axes": [{"lines": [[13, 1.0, 37.90024197979272, 17.203233993860785, -0.021575216626792508, 2.000335099899167, 0.4635526584222581], [13, 1.0, 37.90024197979272, 17.203233993860785, -0.02147987618263869, 2.0085919484035912, 0.46683775846745096]], "collections": [], "images": []}, {"lines": [[13, 1.0, 37.90024197979272, 17.203233993860785, 9.53404441538172e-05, 0.008256848504424319, 0.003285100045192855], [2, 0.0, 1.0, 0.5, 0.02000335099899167, 0.02000335099899167, 0.02000335099899167]], "collections": [], "images": []}]} image/svg+xml diff --git a/tests/conftest.py b/tests/conftest.py index 7be484ee..597320e4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -173,3 +173,20 @@ def get_reference_file(self, filename: str) -> AbstractContextManager[Path]: @pytest.fixture(scope='module') def helper(): return Helper() + + +@pytest.hookimpl(trylast=True) +def pytest_sessionstart(session): + """Print the data summary of every rendered validation plot, for the platform comparison.""" + import json + + from proteus_mpl import validation + + render = validation.render + + def logged(*args, **kwargs): + files, summary = render(*args, **kwargs) + warnings.warn(f'PILOT-SUMMARY {args[1]} {json.dumps(summary)}', stacklevel=2) + return files, summary + + validation.render = logged From 02703688012d44ae54f0715b62991bcf98975ea6 Mon Sep 17 00:00:00 2001 From: timlichtenberg Date: Sat, 24 Oct 2026 02:54:23 +0200 Subject: [PATCH 10/14] Try the summary format 1 of proteus-mpl Install proteus-mpl from the tl/validation-ci commit with the summary format 1 and make the pairs again. This branch is a trial and is not meant for main. --- .github/workflows/ci_tests.yml | 2 +- docs/assets/validation/core_layer_erfc_half_space_0p03myr.svg | 2 +- .../validation/core_layer_erfc_half_space_0p03myr_dark.svg | 2 +- docs/assets/validation/core_layer_erfc_half_space_0p1myr.svg | 2 +- .../validation/core_layer_erfc_half_space_0p1myr_dark.svg | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci_tests.yml b/.github/workflows/ci_tests.yml index 5db63675..55bf2762 100644 --- a/.github/workflows/ci_tests.yml +++ b/.github/workflows/ci_tests.yml @@ -76,7 +76,7 @@ jobs: - name: Install pip-only test deps (scikits-odes-sundials builds against conda SUNDIALS) run: | - pip install pytest pytest-xdist pytest-dependency pytest-timeout hypothesis ruff "proteus-mpl @ git+https://github.com/FormingWorlds/proteus-visual-language@e95980eec3384e072aed71451aca9b5543d51a14#subdirectory=figures/proteus-mpl" + pip install pytest pytest-xdist pytest-dependency pytest-timeout hypothesis ruff "proteus-mpl @ git+https://github.com/FormingWorlds/proteus-visual-language@76aec141a97cb9f633df34dd2e07ea4360a392bf#subdirectory=figures/proteus-mpl" # Install scikits-odes-sundials directly (skip the scikits-odes # metapackage). This avoids scikits-odes-daepack, which is a # transitive Fortran dep with f2py incompatibility with numpy diff --git a/docs/assets/validation/core_layer_erfc_half_space_0p03myr.svg b/docs/assets/validation/core_layer_erfc_half_space_0p03myr.svg index f156cd35..63296166 100644 --- a/docs/assets/validation/core_layer_erfc_half_space_0p03myr.svg +++ b/docs/assets/validation/core_layer_erfc_half_space_0p03myr.svg @@ -6,7 +6,7 @@ - test: tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.03]; command: pytest 'tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.03]' --validation-plots; summary: {"axes": [{"lines": [[9, 1.0, 22.190359007197433, 10.769200446038505, -0.006453914739208852, 0.9842613130845388, 0.21501919580761694], [9, 1.0, 22.190359007197433, 10.769200446038505, -0.00638833208631695, 0.9892611229897739, 0.21809951937181418]], "collections": [], "images": []}, {"lines": [[9, 1.0, 22.190359007197433, 10.769200446038505, 6.55826528919018e-05, 0.005285845252757354, 0.0030803235641972656], [2, 0.0, 1.0, 0.5, 0.009842613130845388, 0.009842613130845388, 0.009842613130845388]], "collections": [], "images": []}]} + test: tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.03]; command: pytest 'tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.03]' --validation-plots; summary: {"v": 1, "axes": [{"lines": [[9, 1.0, 22.190359007197433, 10.769200446038507, -4.400822321479442, -0.006453914739208852, 0.9842613130845388, 0.21501919580761691, 0.17521861864578445], [9, 1.0, 22.190359007197433, 10.769200446038507, -4.400822321479442, -0.00638833208631695, 0.9892611229897739, 0.21809951937181418, 0.17633153424334422]], "collections": [], "values": [], "patches": [], "images": []}, {"lines": [[9, 1.0, 22.190359007197433, 10.769200446038507, -4.400822321479442, 6.55826528919018e-05, 0.005285845252757354, 0.0030803235641972656, 0.0011129155975597724], [2, 0.0, 1.0, 0.5, 0.5, 0.009842613130845388, 0.009842613130845388, 0.009842613130845388, 0.0]], "collections": [], "values": [], "patches": [], "images": []}]} image/svg+xml diff --git a/docs/assets/validation/core_layer_erfc_half_space_0p03myr_dark.svg b/docs/assets/validation/core_layer_erfc_half_space_0p03myr_dark.svg index e07e5dfe..47a31b92 100644 --- a/docs/assets/validation/core_layer_erfc_half_space_0p03myr_dark.svg +++ b/docs/assets/validation/core_layer_erfc_half_space_0p03myr_dark.svg @@ -6,7 +6,7 @@ - test: tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.03]; command: pytest 'tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.03]' --validation-plots; summary: {"axes": [{"lines": [[9, 1.0, 22.190359007197433, 10.769200446038505, -0.006453914739208852, 0.9842613130845388, 0.21501919580761694], [9, 1.0, 22.190359007197433, 10.769200446038505, -0.00638833208631695, 0.9892611229897739, 0.21809951937181418]], "collections": [], "images": []}, {"lines": [[9, 1.0, 22.190359007197433, 10.769200446038505, 6.55826528919018e-05, 0.005285845252757354, 0.0030803235641972656], [2, 0.0, 1.0, 0.5, 0.009842613130845388, 0.009842613130845388, 0.009842613130845388]], "collections": [], "images": []}]} + test: tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.03]; command: pytest 'tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.03]' --validation-plots; summary: {"v": 1, "axes": [{"lines": [[9, 1.0, 22.190359007197433, 10.769200446038507, -4.400822321479442, -0.006453914739208852, 0.9842613130845388, 0.21501919580761691, 0.17521861864578445], [9, 1.0, 22.190359007197433, 10.769200446038507, -4.400822321479442, -0.00638833208631695, 0.9892611229897739, 0.21809951937181418, 0.17633153424334422]], "collections": [], "values": [], "patches": [], "images": []}, {"lines": [[9, 1.0, 22.190359007197433, 10.769200446038507, -4.400822321479442, 6.55826528919018e-05, 0.005285845252757354, 0.0030803235641972656, 0.0011129155975597724], [2, 0.0, 1.0, 0.5, 0.5, 0.009842613130845388, 0.009842613130845388, 0.009842613130845388, 0.0]], "collections": [], "values": [], "patches": [], "images": []}]} image/svg+xml diff --git a/docs/assets/validation/core_layer_erfc_half_space_0p1myr.svg b/docs/assets/validation/core_layer_erfc_half_space_0p1myr.svg index 941321e8..3cbb6c70 100644 --- a/docs/assets/validation/core_layer_erfc_half_space_0p1myr.svg +++ b/docs/assets/validation/core_layer_erfc_half_space_0p1myr.svg @@ -6,7 +6,7 @@ - test: tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.1]; command: pytest 'tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.1]' --validation-plots; summary: {"axes": [{"lines": [[13, 1.0, 37.90024197979272, 17.203233993860785, -0.021575216626792508, 2.000335099899167, 0.4635526584222581], [13, 1.0, 37.90024197979272, 17.203233993860785, -0.02147987618263869, 2.0085919484035912, 0.46683775846745096]], "collections": [], "images": []}, {"lines": [[13, 1.0, 37.90024197979272, 17.203233993860785, 9.53404441538172e-05, 0.008256848504424319, 0.003285100045192855], [2, 0.0, 1.0, 0.5, 0.02000335099899167, 0.02000335099899167, 0.02000335099899167]], "collections": [], "images": []}]} + test: tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.1]; command: pytest 'tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.1]' --validation-plots; summary: {"v": 1, "axes": [{"lines": [[13, 1.0, 37.90024197979272, 17.203233993860785, -7.116649974567649, -0.021575216626792508, 2.000335099899167, 0.4635526584222581, 0.349530376983387], [13, 1.0, 37.90024197979272, 17.203233993860785, -7.116649974567649, -0.02147987618263869, 2.0085919484035912, 0.466837758467451, 0.35096133223632]], "collections": [], "values": [], "patches": [], "images": []}, {"lines": [[13, 1.0, 37.90024197979272, 17.203233993860785, -7.116649974567649, 9.53404441538172e-05, 0.008256848504424319, 0.0032851000451928547, 0.0014309552529329914], [2, 0.0, 1.0, 0.5, 0.5, 0.02000335099899167, 0.02000335099899167, 0.02000335099899167, 0.0]], "collections": [], "values": [], "patches": [], "images": []}]} image/svg+xml diff --git a/docs/assets/validation/core_layer_erfc_half_space_0p1myr_dark.svg b/docs/assets/validation/core_layer_erfc_half_space_0p1myr_dark.svg index 3d8da784..8c1f058a 100644 --- a/docs/assets/validation/core_layer_erfc_half_space_0p1myr_dark.svg +++ b/docs/assets/validation/core_layer_erfc_half_space_0p1myr_dark.svg @@ -6,7 +6,7 @@ - test: tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.1]; command: pytest 'tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.1]' --validation-plots; summary: {"axes": [{"lines": [[13, 1.0, 37.90024197979272, 17.203233993860785, -0.021575216626792508, 2.000335099899167, 0.4635526584222581], [13, 1.0, 37.90024197979272, 17.203233993860785, -0.02147987618263869, 2.0085919484035912, 0.46683775846745096]], "collections": [], "images": []}, {"lines": [[13, 1.0, 37.90024197979272, 17.203233993860785, 9.53404441538172e-05, 0.008256848504424319, 0.003285100045192855], [2, 0.0, 1.0, 0.5, 0.02000335099899167, 0.02000335099899167, 0.02000335099899167]], "collections": [], "images": []}]} + test: tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.1]; command: pytest 'tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.1]' --validation-plots; summary: {"v": 1, "axes": [{"lines": [[13, 1.0, 37.90024197979272, 17.203233993860785, -7.116649974567649, -0.021575216626792508, 2.000335099899167, 0.4635526584222581, 0.349530376983387], [13, 1.0, 37.90024197979272, 17.203233993860785, -7.116649974567649, -0.02147987618263869, 2.0085919484035912, 0.466837758467451, 0.35096133223632]], "collections": [], "values": [], "patches": [], "images": []}, {"lines": [[13, 1.0, 37.90024197979272, 17.203233993860785, -7.116649974567649, 9.53404441538172e-05, 0.008256848504424319, 0.0032851000451928547, 0.0014309552529329914], [2, 0.0, 1.0, 0.5, 0.5, 0.02000335099899167, 0.02000335099899167, 0.02000335099899167, 0.0]], "collections": [], "values": [], "patches": [], "images": []}]} image/svg+xml From 930b5bcdf6f26a1b8c80ebebbbcaac511e5f8905 Mon Sep 17 00:00:00 2001 From: timlichtenberg Date: Sat, 24 Oct 2026 03:24:48 +0200 Subject: [PATCH 11/14] Try the proteus-mpl summary with insets and extents Install proteus-mpl from the newest tl/validation-ci commit and make the pairs again. This branch is a trial and is not meant for main. --- .github/workflows/ci_tests.yml | 2 +- docs/assets/validation/core_layer_erfc_half_space_0p03myr.svg | 2 +- .../validation/core_layer_erfc_half_space_0p03myr_dark.svg | 2 +- docs/assets/validation/core_layer_erfc_half_space_0p1myr.svg | 2 +- .../validation/core_layer_erfc_half_space_0p1myr_dark.svg | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci_tests.yml b/.github/workflows/ci_tests.yml index 55bf2762..623400db 100644 --- a/.github/workflows/ci_tests.yml +++ b/.github/workflows/ci_tests.yml @@ -76,7 +76,7 @@ jobs: - name: Install pip-only test deps (scikits-odes-sundials builds against conda SUNDIALS) run: | - pip install pytest pytest-xdist pytest-dependency pytest-timeout hypothesis ruff "proteus-mpl @ git+https://github.com/FormingWorlds/proteus-visual-language@76aec141a97cb9f633df34dd2e07ea4360a392bf#subdirectory=figures/proteus-mpl" + pip install pytest pytest-xdist pytest-dependency pytest-timeout hypothesis ruff "proteus-mpl @ git+https://github.com/FormingWorlds/proteus-visual-language@775724f7ff4335673aa64224e0757c6f7d83f617#subdirectory=figures/proteus-mpl" # Install scikits-odes-sundials directly (skip the scikits-odes # metapackage). This avoids scikits-odes-daepack, which is a # transitive Fortran dep with f2py incompatibility with numpy diff --git a/docs/assets/validation/core_layer_erfc_half_space_0p03myr.svg b/docs/assets/validation/core_layer_erfc_half_space_0p03myr.svg index 63296166..46ff3d5f 100644 --- a/docs/assets/validation/core_layer_erfc_half_space_0p03myr.svg +++ b/docs/assets/validation/core_layer_erfc_half_space_0p03myr.svg @@ -6,7 +6,7 @@ - test: tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.03]; command: pytest 'tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.03]' --validation-plots; summary: {"v": 1, "axes": [{"lines": [[9, 1.0, 22.190359007197433, 10.769200446038507, -4.400822321479442, -0.006453914739208852, 0.9842613130845388, 0.21501919580761691, 0.17521861864578445], [9, 1.0, 22.190359007197433, 10.769200446038507, -4.400822321479442, -0.00638833208631695, 0.9892611229897739, 0.21809951937181418, 0.17633153424334422]], "collections": [], "values": [], "patches": [], "images": []}, {"lines": [[9, 1.0, 22.190359007197433, 10.769200446038507, -4.400822321479442, 6.55826528919018e-05, 0.005285845252757354, 0.0030803235641972656, 0.0011129155975597724], [2, 0.0, 1.0, 0.5, 0.5, 0.009842613130845388, 0.009842613130845388, 0.009842613130845388, 0.0]], "collections": [], "values": [], "patches": [], "images": []}]} + test: tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.03]; command: pytest 'tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.03]' --validation-plots; summary: {"v": 1, "axes": [{"lines": [[9, 1.0, 22.190359007197433, 10.769200446038507, -4.400822321479442, -0.006453914739208852, 0.9842613130845388, 0.21501919580761691, 0.17521861864578445], [9, 1.0, 22.190359007197433, 10.769200446038507, -4.400822321479442, -0.00638833208631695, 0.9892611229897739, 0.21809951937181418, 0.17633153424334422]], "collections": [], "values": [], "patches": [], "images": [], "extents": []}, {"lines": [[9, 1.0, 22.190359007197433, 10.769200446038507, -4.400822321479442, 6.55826528919018e-05, 0.005285845252757354, 0.0030803235641972656, 0.0011129155975597724], [2, 0.0, 1.0, 0.5, 0.5, 0.009842613130845388, 0.009842613130845388, 0.009842613130845388, 0.0]], "collections": [], "values": [], "patches": [], "images": [], "extents": []}]} image/svg+xml diff --git a/docs/assets/validation/core_layer_erfc_half_space_0p03myr_dark.svg b/docs/assets/validation/core_layer_erfc_half_space_0p03myr_dark.svg index 47a31b92..b9a68e0f 100644 --- a/docs/assets/validation/core_layer_erfc_half_space_0p03myr_dark.svg +++ b/docs/assets/validation/core_layer_erfc_half_space_0p03myr_dark.svg @@ -6,7 +6,7 @@ - test: tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.03]; command: pytest 'tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.03]' --validation-plots; summary: {"v": 1, "axes": [{"lines": [[9, 1.0, 22.190359007197433, 10.769200446038507, -4.400822321479442, -0.006453914739208852, 0.9842613130845388, 0.21501919580761691, 0.17521861864578445], [9, 1.0, 22.190359007197433, 10.769200446038507, -4.400822321479442, -0.00638833208631695, 0.9892611229897739, 0.21809951937181418, 0.17633153424334422]], "collections": [], "values": [], "patches": [], "images": []}, {"lines": [[9, 1.0, 22.190359007197433, 10.769200446038507, -4.400822321479442, 6.55826528919018e-05, 0.005285845252757354, 0.0030803235641972656, 0.0011129155975597724], [2, 0.0, 1.0, 0.5, 0.5, 0.009842613130845388, 0.009842613130845388, 0.009842613130845388, 0.0]], "collections": [], "values": [], "patches": [], "images": []}]} + test: tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.03]; command: pytest 'tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.03]' --validation-plots; summary: {"v": 1, "axes": [{"lines": [[9, 1.0, 22.190359007197433, 10.769200446038507, -4.400822321479442, -0.006453914739208852, 0.9842613130845388, 0.21501919580761691, 0.17521861864578445], [9, 1.0, 22.190359007197433, 10.769200446038507, -4.400822321479442, -0.00638833208631695, 0.9892611229897739, 0.21809951937181418, 0.17633153424334422]], "collections": [], "values": [], "patches": [], "images": [], "extents": []}, {"lines": [[9, 1.0, 22.190359007197433, 10.769200446038507, -4.400822321479442, 6.55826528919018e-05, 0.005285845252757354, 0.0030803235641972656, 0.0011129155975597724], [2, 0.0, 1.0, 0.5, 0.5, 0.009842613130845388, 0.009842613130845388, 0.009842613130845388, 0.0]], "collections": [], "values": [], "patches": [], "images": [], "extents": []}]} image/svg+xml diff --git a/docs/assets/validation/core_layer_erfc_half_space_0p1myr.svg b/docs/assets/validation/core_layer_erfc_half_space_0p1myr.svg index 3cbb6c70..7a57ff00 100644 --- a/docs/assets/validation/core_layer_erfc_half_space_0p1myr.svg +++ b/docs/assets/validation/core_layer_erfc_half_space_0p1myr.svg @@ -6,7 +6,7 @@ - test: tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.1]; command: pytest 'tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.1]' --validation-plots; summary: {"v": 1, "axes": [{"lines": [[13, 1.0, 37.90024197979272, 17.203233993860785, -7.116649974567649, -0.021575216626792508, 2.000335099899167, 0.4635526584222581, 0.349530376983387], [13, 1.0, 37.90024197979272, 17.203233993860785, -7.116649974567649, -0.02147987618263869, 2.0085919484035912, 0.466837758467451, 0.35096133223632]], "collections": [], "values": [], "patches": [], "images": []}, {"lines": [[13, 1.0, 37.90024197979272, 17.203233993860785, -7.116649974567649, 9.53404441538172e-05, 0.008256848504424319, 0.0032851000451928547, 0.0014309552529329914], [2, 0.0, 1.0, 0.5, 0.5, 0.02000335099899167, 0.02000335099899167, 0.02000335099899167, 0.0]], "collections": [], "values": [], "patches": [], "images": []}]} + test: tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.1]; command: pytest 'tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.1]' --validation-plots; summary: {"v": 1, "axes": [{"lines": [[13, 1.0, 37.90024197979272, 17.203233993860785, -7.116649974567649, -0.021575216626792508, 2.000335099899167, 0.4635526584222581, 0.349530376983387], [13, 1.0, 37.90024197979272, 17.203233993860785, -7.116649974567649, -0.02147987618263869, 2.0085919484035912, 0.466837758467451, 0.35096133223632]], "collections": [], "values": [], "patches": [], "images": [], "extents": []}, {"lines": [[13, 1.0, 37.90024197979272, 17.203233993860785, -7.116649974567649, 9.53404441538172e-05, 0.008256848504424319, 0.0032851000451928547, 0.0014309552529329914], [2, 0.0, 1.0, 0.5, 0.5, 0.02000335099899167, 0.02000335099899167, 0.02000335099899167, 0.0]], "collections": [], "values": [], "patches": [], "images": [], "extents": []}]} image/svg+xml diff --git a/docs/assets/validation/core_layer_erfc_half_space_0p1myr_dark.svg b/docs/assets/validation/core_layer_erfc_half_space_0p1myr_dark.svg index 8c1f058a..828efee5 100644 --- a/docs/assets/validation/core_layer_erfc_half_space_0p1myr_dark.svg +++ b/docs/assets/validation/core_layer_erfc_half_space_0p1myr_dark.svg @@ -6,7 +6,7 @@ - test: tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.1]; command: pytest 'tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.1]' --validation-plots; summary: {"v": 1, "axes": [{"lines": [[13, 1.0, 37.90024197979272, 17.203233993860785, -7.116649974567649, -0.021575216626792508, 2.000335099899167, 0.4635526584222581, 0.349530376983387], [13, 1.0, 37.90024197979272, 17.203233993860785, -7.116649974567649, -0.02147987618263869, 2.0085919484035912, 0.466837758467451, 0.35096133223632]], "collections": [], "values": [], "patches": [], "images": []}, {"lines": [[13, 1.0, 37.90024197979272, 17.203233993860785, -7.116649974567649, 9.53404441538172e-05, 0.008256848504424319, 0.0032851000451928547, 0.0014309552529329914], [2, 0.0, 1.0, 0.5, 0.5, 0.02000335099899167, 0.02000335099899167, 0.02000335099899167, 0.0]], "collections": [], "values": [], "patches": [], "images": []}]} + test: tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.1]; command: pytest 'tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.1]' --validation-plots; summary: {"v": 1, "axes": [{"lines": [[13, 1.0, 37.90024197979272, 17.203233993860785, -7.116649974567649, -0.021575216626792508, 2.000335099899167, 0.4635526584222581, 0.349530376983387], [13, 1.0, 37.90024197979272, 17.203233993860785, -7.116649974567649, -0.02147987618263869, 2.0085919484035912, 0.466837758467451, 0.35096133223632]], "collections": [], "values": [], "patches": [], "images": [], "extents": []}, {"lines": [[13, 1.0, 37.90024197979272, 17.203233993860785, -7.116649974567649, 9.53404441538172e-05, 0.008256848504424319, 0.0032851000451928547, 0.0014309552529329914], [2, 0.0, 1.0, 0.5, 0.5, 0.02000335099899167, 0.02000335099899167, 0.02000335099899167, 0.0]], "collections": [], "values": [], "patches": [], "images": [], "extents": []}]} image/svg+xml From 7e2ca30bd667607695ac03c8a6cec786c6790dd7 Mon Sep 17 00:00:00 2001 From: timlichtenberg Date: Sat, 24 Oct 2026 04:47:37 +0200 Subject: [PATCH 12/14] Try the proteus-mpl summary with log axes in decades Install proteus-mpl from the newest tl/validation-ci commit and make the pairs again. This branch is a trial and is not meant for main. --- .github/workflows/ci_tests.yml | 2 +- docs/assets/validation/core_layer_erfc_half_space_0p03myr.svg | 2 +- .../validation/core_layer_erfc_half_space_0p03myr_dark.svg | 2 +- docs/assets/validation/core_layer_erfc_half_space_0p1myr.svg | 2 +- .../validation/core_layer_erfc_half_space_0p1myr_dark.svg | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci_tests.yml b/.github/workflows/ci_tests.yml index 623400db..e95b5698 100644 --- a/.github/workflows/ci_tests.yml +++ b/.github/workflows/ci_tests.yml @@ -76,7 +76,7 @@ jobs: - name: Install pip-only test deps (scikits-odes-sundials builds against conda SUNDIALS) run: | - pip install pytest pytest-xdist pytest-dependency pytest-timeout hypothesis ruff "proteus-mpl @ git+https://github.com/FormingWorlds/proteus-visual-language@775724f7ff4335673aa64224e0757c6f7d83f617#subdirectory=figures/proteus-mpl" + pip install pytest pytest-xdist pytest-dependency pytest-timeout hypothesis ruff "proteus-mpl @ git+https://github.com/FormingWorlds/proteus-visual-language@d08e36930d2a96909b52a05b62df244eb3208301#subdirectory=figures/proteus-mpl" # Install scikits-odes-sundials directly (skip the scikits-odes # metapackage). This avoids scikits-odes-daepack, which is a # transitive Fortran dep with f2py incompatibility with numpy diff --git a/docs/assets/validation/core_layer_erfc_half_space_0p03myr.svg b/docs/assets/validation/core_layer_erfc_half_space_0p03myr.svg index 46ff3d5f..2f3ec18b 100644 --- a/docs/assets/validation/core_layer_erfc_half_space_0p03myr.svg +++ b/docs/assets/validation/core_layer_erfc_half_space_0p03myr.svg @@ -6,7 +6,7 @@ - test: tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.03]; command: pytest 'tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.03]' --validation-plots; summary: {"v": 1, "axes": [{"lines": [[9, 1.0, 22.190359007197433, 10.769200446038507, -4.400822321479442, -0.006453914739208852, 0.9842613130845388, 0.21501919580761691, 0.17521861864578445], [9, 1.0, 22.190359007197433, 10.769200446038507, -4.400822321479442, -0.00638833208631695, 0.9892611229897739, 0.21809951937181418, 0.17633153424334422]], "collections": [], "values": [], "patches": [], "images": [], "extents": []}, {"lines": [[9, 1.0, 22.190359007197433, 10.769200446038507, -4.400822321479442, 6.55826528919018e-05, 0.005285845252757354, 0.0030803235641972656, 0.0011129155975597724], [2, 0.0, 1.0, 0.5, 0.5, 0.009842613130845388, 0.009842613130845388, 0.009842613130845388, 0.0]], "collections": [], "values": [], "patches": [], "images": [], "extents": []}]} + test: tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.03]; command: pytest 'tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.03]' --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[9, 1.0, 22.190359007197433, 10.769200446038507, -4.400822321479442, -0.006453914739208852, 0.9842613130845388, 0.21501919580761691, 0.17521861864578445], [9, 1.0, 22.190359007197433, 10.769200446038507, -4.400822321479442, -0.00638833208631695, 0.9892611229897739, 0.21809951937181418, 0.17633153424334422]], "collections": [], "values": [], "patches": [], "images": [], "extents": []}, {"scales": ["linear", "linear"], "categories": [null, null], "lines": [[9, 1.0, 22.190359007197433, 10.769200446038507, -4.400822321479442, 6.55826528919018e-05, 0.005285845252757354, 0.0030803235641972656, 0.0011129155975597724], [2, 0.0, 1.0, 0.5, 0.5, 0.009842613130845388, 0.009842613130845388, 0.009842613130845388, 0.0]], "collections": [], "values": [], "patches": [], "images": [], "extents": []}]} image/svg+xml diff --git a/docs/assets/validation/core_layer_erfc_half_space_0p03myr_dark.svg b/docs/assets/validation/core_layer_erfc_half_space_0p03myr_dark.svg index b9a68e0f..6bd0d72a 100644 --- a/docs/assets/validation/core_layer_erfc_half_space_0p03myr_dark.svg +++ b/docs/assets/validation/core_layer_erfc_half_space_0p03myr_dark.svg @@ -6,7 +6,7 @@ - test: tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.03]; command: pytest 'tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.03]' --validation-plots; summary: {"v": 1, "axes": [{"lines": [[9, 1.0, 22.190359007197433, 10.769200446038507, -4.400822321479442, -0.006453914739208852, 0.9842613130845388, 0.21501919580761691, 0.17521861864578445], [9, 1.0, 22.190359007197433, 10.769200446038507, -4.400822321479442, -0.00638833208631695, 0.9892611229897739, 0.21809951937181418, 0.17633153424334422]], "collections": [], "values": [], "patches": [], "images": [], "extents": []}, {"lines": [[9, 1.0, 22.190359007197433, 10.769200446038507, -4.400822321479442, 6.55826528919018e-05, 0.005285845252757354, 0.0030803235641972656, 0.0011129155975597724], [2, 0.0, 1.0, 0.5, 0.5, 0.009842613130845388, 0.009842613130845388, 0.009842613130845388, 0.0]], "collections": [], "values": [], "patches": [], "images": [], "extents": []}]} + test: tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.03]; command: pytest 'tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.03]' --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[9, 1.0, 22.190359007197433, 10.769200446038507, -4.400822321479442, -0.006453914739208852, 0.9842613130845388, 0.21501919580761691, 0.17521861864578445], [9, 1.0, 22.190359007197433, 10.769200446038507, -4.400822321479442, -0.00638833208631695, 0.9892611229897739, 0.21809951937181418, 0.17633153424334422]], "collections": [], "values": [], "patches": [], "images": [], "extents": []}, {"scales": ["linear", "linear"], "categories": [null, null], "lines": [[9, 1.0, 22.190359007197433, 10.769200446038507, -4.400822321479442, 6.55826528919018e-05, 0.005285845252757354, 0.0030803235641972656, 0.0011129155975597724], [2, 0.0, 1.0, 0.5, 0.5, 0.009842613130845388, 0.009842613130845388, 0.009842613130845388, 0.0]], "collections": [], "values": [], "patches": [], "images": [], "extents": []}]} image/svg+xml diff --git a/docs/assets/validation/core_layer_erfc_half_space_0p1myr.svg b/docs/assets/validation/core_layer_erfc_half_space_0p1myr.svg index 7a57ff00..9f4d19e3 100644 --- a/docs/assets/validation/core_layer_erfc_half_space_0p1myr.svg +++ b/docs/assets/validation/core_layer_erfc_half_space_0p1myr.svg @@ -6,7 +6,7 @@ - test: tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.1]; command: pytest 'tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.1]' --validation-plots; summary: {"v": 1, "axes": [{"lines": [[13, 1.0, 37.90024197979272, 17.203233993860785, -7.116649974567649, -0.021575216626792508, 2.000335099899167, 0.4635526584222581, 0.349530376983387], [13, 1.0, 37.90024197979272, 17.203233993860785, -7.116649974567649, -0.02147987618263869, 2.0085919484035912, 0.466837758467451, 0.35096133223632]], "collections": [], "values": [], "patches": [], "images": [], "extents": []}, {"lines": [[13, 1.0, 37.90024197979272, 17.203233993860785, -7.116649974567649, 9.53404441538172e-05, 0.008256848504424319, 0.0032851000451928547, 0.0014309552529329914], [2, 0.0, 1.0, 0.5, 0.5, 0.02000335099899167, 0.02000335099899167, 0.02000335099899167, 0.0]], "collections": [], "values": [], "patches": [], "images": [], "extents": []}]} + test: tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.1]; command: pytest 'tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.1]' --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[13, 1.0, 37.90024197979272, 17.203233993860785, -7.116649974567649, -0.021575216626792508, 2.000335099899167, 0.4635526584222581, 0.349530376983387], [13, 1.0, 37.90024197979272, 17.203233993860785, -7.116649974567649, -0.02147987618263869, 2.0085919484035912, 0.466837758467451, 0.35096133223632]], "collections": [], "values": [], "patches": [], "images": [], "extents": []}, {"scales": ["linear", "linear"], "categories": [null, null], "lines": [[13, 1.0, 37.90024197979272, 17.203233993860785, -7.116649974567649, 9.53404441538172e-05, 0.008256848504424319, 0.0032851000451928547, 0.0014309552529329914], [2, 0.0, 1.0, 0.5, 0.5, 0.02000335099899167, 0.02000335099899167, 0.02000335099899167, 0.0]], "collections": [], "values": [], "patches": [], "images": [], "extents": []}]} image/svg+xml diff --git a/docs/assets/validation/core_layer_erfc_half_space_0p1myr_dark.svg b/docs/assets/validation/core_layer_erfc_half_space_0p1myr_dark.svg index 828efee5..3c0255e5 100644 --- a/docs/assets/validation/core_layer_erfc_half_space_0p1myr_dark.svg +++ b/docs/assets/validation/core_layer_erfc_half_space_0p1myr_dark.svg @@ -6,7 +6,7 @@ - test: tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.1]; command: pytest 'tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.1]' --validation-plots; summary: {"v": 1, "axes": [{"lines": [[13, 1.0, 37.90024197979272, 17.203233993860785, -7.116649974567649, -0.021575216626792508, 2.000335099899167, 0.4635526584222581, 0.349530376983387], [13, 1.0, 37.90024197979272, 17.203233993860785, -7.116649974567649, -0.02147987618263869, 2.0085919484035912, 0.466837758467451, 0.35096133223632]], "collections": [], "values": [], "patches": [], "images": [], "extents": []}, {"lines": [[13, 1.0, 37.90024197979272, 17.203233993860785, -7.116649974567649, 9.53404441538172e-05, 0.008256848504424319, 0.0032851000451928547, 0.0014309552529329914], [2, 0.0, 1.0, 0.5, 0.5, 0.02000335099899167, 0.02000335099899167, 0.02000335099899167, 0.0]], "collections": [], "values": [], "patches": [], "images": [], "extents": []}]} + test: tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.1]; command: pytest 'tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.1]' --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[13, 1.0, 37.90024197979272, 17.203233993860785, -7.116649974567649, -0.021575216626792508, 2.000335099899167, 0.4635526584222581, 0.349530376983387], [13, 1.0, 37.90024197979272, 17.203233993860785, -7.116649974567649, -0.02147987618263869, 2.0085919484035912, 0.466837758467451, 0.35096133223632]], "collections": [], "values": [], "patches": [], "images": [], "extents": []}, {"scales": ["linear", "linear"], "categories": [null, null], "lines": [[13, 1.0, 37.90024197979272, 17.203233993860785, -7.116649974567649, 9.53404441538172e-05, 0.008256848504424319, 0.0032851000451928547, 0.0014309552529329914], [2, 0.0, 1.0, 0.5, 0.5, 0.02000335099899167, 0.02000335099899167, 0.02000335099899167, 0.0]], "collections": [], "values": [], "patches": [], "images": [], "extents": []}]} image/svg+xml From d48a0e2c459363070c586d18e530f146ae1c8e25 Mon Sep 17 00:00:00 2001 From: timlichtenberg Date: Sat, 24 Oct 2026 05:32:36 +0200 Subject: [PATCH 13/14] Try the proteus-mpl summary with log norms and a decade floor Install proteus-mpl from the newest tl/validation-ci commit and make the pairs again. This branch is a trial and is not meant for main. --- .github/workflows/ci_tests.yml | 2 +- docs/assets/validation/core_layer_erfc_half_space_0p03myr.svg | 2 +- .../validation/core_layer_erfc_half_space_0p03myr_dark.svg | 2 +- docs/assets/validation/core_layer_erfc_half_space_0p1myr.svg | 2 +- .../validation/core_layer_erfc_half_space_0p1myr_dark.svg | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci_tests.yml b/.github/workflows/ci_tests.yml index e95b5698..869b406c 100644 --- a/.github/workflows/ci_tests.yml +++ b/.github/workflows/ci_tests.yml @@ -76,7 +76,7 @@ jobs: - name: Install pip-only test deps (scikits-odes-sundials builds against conda SUNDIALS) run: | - pip install pytest pytest-xdist pytest-dependency pytest-timeout hypothesis ruff "proteus-mpl @ git+https://github.com/FormingWorlds/proteus-visual-language@d08e36930d2a96909b52a05b62df244eb3208301#subdirectory=figures/proteus-mpl" + pip install pytest pytest-xdist pytest-dependency pytest-timeout hypothesis ruff "proteus-mpl @ git+https://github.com/FormingWorlds/proteus-visual-language@5397d613deaff2e9675c48aa5b38c35e5f03e624#subdirectory=figures/proteus-mpl" # Install scikits-odes-sundials directly (skip the scikits-odes # metapackage). This avoids scikits-odes-daepack, which is a # transitive Fortran dep with f2py incompatibility with numpy diff --git a/docs/assets/validation/core_layer_erfc_half_space_0p03myr.svg b/docs/assets/validation/core_layer_erfc_half_space_0p03myr.svg index 2f3ec18b..d8c78b43 100644 --- a/docs/assets/validation/core_layer_erfc_half_space_0p03myr.svg +++ b/docs/assets/validation/core_layer_erfc_half_space_0p03myr.svg @@ -6,7 +6,7 @@ - test: tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.03]; command: pytest 'tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.03]' --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[9, 1.0, 22.190359007197433, 10.769200446038507, -4.400822321479442, -0.006453914739208852, 0.9842613130845388, 0.21501919580761691, 0.17521861864578445], [9, 1.0, 22.190359007197433, 10.769200446038507, -4.400822321479442, -0.00638833208631695, 0.9892611229897739, 0.21809951937181418, 0.17633153424334422]], "collections": [], "values": [], "patches": [], "images": [], "extents": []}, {"scales": ["linear", "linear"], "categories": [null, null], "lines": [[9, 1.0, 22.190359007197433, 10.769200446038507, -4.400822321479442, 6.55826528919018e-05, 0.005285845252757354, 0.0030803235641972656, 0.0011129155975597724], [2, 0.0, 1.0, 0.5, 0.5, 0.009842613130845388, 0.009842613130845388, 0.009842613130845388, 0.0]], "collections": [], "values": [], "patches": [], "images": [], "extents": []}]} + test: tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.03]; command: pytest 'tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.03]' --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[9, 1.0, 22.190359007197433, 10.769200446038507, -4.400822321479442, -0.006453914739208852, 0.9842613130845388, 0.21501919580761691, 0.17521861864578445], [9, 1.0, 22.190359007197433, 10.769200446038507, -4.400822321479442, -0.00638833208631695, 0.9892611229897739, 0.21809951937181418, 0.17633153424334422]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}, {"scales": ["linear", "linear"], "categories": [null, null], "lines": [[9, 1.0, 22.190359007197433, 10.769200446038507, -4.400822321479442, 6.55826528919018e-05, 0.005285845252757354, 0.0030803235641972656, 0.0011129155975597724], [2, 0.0, 1.0, 0.5, 0.5, 0.009842613130845388, 0.009842613130845388, 0.009842613130845388, 0.0]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} image/svg+xml diff --git a/docs/assets/validation/core_layer_erfc_half_space_0p03myr_dark.svg b/docs/assets/validation/core_layer_erfc_half_space_0p03myr_dark.svg index 6bd0d72a..b7c1dca1 100644 --- a/docs/assets/validation/core_layer_erfc_half_space_0p03myr_dark.svg +++ b/docs/assets/validation/core_layer_erfc_half_space_0p03myr_dark.svg @@ -6,7 +6,7 @@ - test: tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.03]; command: pytest 'tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.03]' --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[9, 1.0, 22.190359007197433, 10.769200446038507, -4.400822321479442, -0.006453914739208852, 0.9842613130845388, 0.21501919580761691, 0.17521861864578445], [9, 1.0, 22.190359007197433, 10.769200446038507, -4.400822321479442, -0.00638833208631695, 0.9892611229897739, 0.21809951937181418, 0.17633153424334422]], "collections": [], "values": [], "patches": [], "images": [], "extents": []}, {"scales": ["linear", "linear"], "categories": [null, null], "lines": [[9, 1.0, 22.190359007197433, 10.769200446038507, -4.400822321479442, 6.55826528919018e-05, 0.005285845252757354, 0.0030803235641972656, 0.0011129155975597724], [2, 0.0, 1.0, 0.5, 0.5, 0.009842613130845388, 0.009842613130845388, 0.009842613130845388, 0.0]], "collections": [], "values": [], "patches": [], "images": [], "extents": []}]} + test: tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.03]; command: pytest 'tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.03]' --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[9, 1.0, 22.190359007197433, 10.769200446038507, -4.400822321479442, -0.006453914739208852, 0.9842613130845388, 0.21501919580761691, 0.17521861864578445], [9, 1.0, 22.190359007197433, 10.769200446038507, -4.400822321479442, -0.00638833208631695, 0.9892611229897739, 0.21809951937181418, 0.17633153424334422]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}, {"scales": ["linear", "linear"], "categories": [null, null], "lines": [[9, 1.0, 22.190359007197433, 10.769200446038507, -4.400822321479442, 6.55826528919018e-05, 0.005285845252757354, 0.0030803235641972656, 0.0011129155975597724], [2, 0.0, 1.0, 0.5, 0.5, 0.009842613130845388, 0.009842613130845388, 0.009842613130845388, 0.0]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} image/svg+xml diff --git a/docs/assets/validation/core_layer_erfc_half_space_0p1myr.svg b/docs/assets/validation/core_layer_erfc_half_space_0p1myr.svg index 9f4d19e3..fa0602bc 100644 --- a/docs/assets/validation/core_layer_erfc_half_space_0p1myr.svg +++ b/docs/assets/validation/core_layer_erfc_half_space_0p1myr.svg @@ -6,7 +6,7 @@ - test: tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.1]; command: pytest 'tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.1]' --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[13, 1.0, 37.90024197979272, 17.203233993860785, -7.116649974567649, -0.021575216626792508, 2.000335099899167, 0.4635526584222581, 0.349530376983387], [13, 1.0, 37.90024197979272, 17.203233993860785, -7.116649974567649, -0.02147987618263869, 2.0085919484035912, 0.466837758467451, 0.35096133223632]], "collections": [], "values": [], "patches": [], "images": [], "extents": []}, {"scales": ["linear", "linear"], "categories": [null, null], "lines": [[13, 1.0, 37.90024197979272, 17.203233993860785, -7.116649974567649, 9.53404441538172e-05, 0.008256848504424319, 0.0032851000451928547, 0.0014309552529329914], [2, 0.0, 1.0, 0.5, 0.5, 0.02000335099899167, 0.02000335099899167, 0.02000335099899167, 0.0]], "collections": [], "values": [], "patches": [], "images": [], "extents": []}]} + test: tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.1]; command: pytest 'tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.1]' --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[13, 1.0, 37.90024197979272, 17.203233993860785, -7.116649974567649, -0.021575216626792508, 2.000335099899167, 0.4635526584222581, 0.349530376983387], [13, 1.0, 37.90024197979272, 17.203233993860785, -7.116649974567649, -0.02147987618263869, 2.0085919484035912, 0.466837758467451, 0.35096133223632]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}, {"scales": ["linear", "linear"], "categories": [null, null], "lines": [[13, 1.0, 37.90024197979272, 17.203233993860785, -7.116649974567649, 9.53404441538172e-05, 0.008256848504424319, 0.0032851000451928547, 0.0014309552529329914], [2, 0.0, 1.0, 0.5, 0.5, 0.02000335099899167, 0.02000335099899167, 0.02000335099899167, 0.0]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} image/svg+xml diff --git a/docs/assets/validation/core_layer_erfc_half_space_0p1myr_dark.svg b/docs/assets/validation/core_layer_erfc_half_space_0p1myr_dark.svg index 3c0255e5..fc9c2015 100644 --- a/docs/assets/validation/core_layer_erfc_half_space_0p1myr_dark.svg +++ b/docs/assets/validation/core_layer_erfc_half_space_0p1myr_dark.svg @@ -6,7 +6,7 @@ - test: tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.1]; command: pytest 'tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.1]' --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[13, 1.0, 37.90024197979272, 17.203233993860785, -7.116649974567649, -0.021575216626792508, 2.000335099899167, 0.4635526584222581, 0.349530376983387], [13, 1.0, 37.90024197979272, 17.203233993860785, -7.116649974567649, -0.02147987618263869, 2.0085919484035912, 0.466837758467451, 0.35096133223632]], "collections": [], "values": [], "patches": [], "images": [], "extents": []}, {"scales": ["linear", "linear"], "categories": [null, null], "lines": [[13, 1.0, 37.90024197979272, 17.203233993860785, -7.116649974567649, 9.53404441538172e-05, 0.008256848504424319, 0.0032851000451928547, 0.0014309552529329914], [2, 0.0, 1.0, 0.5, 0.5, 0.02000335099899167, 0.02000335099899167, 0.02000335099899167, 0.0]], "collections": [], "values": [], "patches": [], "images": [], "extents": []}]} + test: tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.1]; command: pytest 'tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.1]' --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[13, 1.0, 37.90024197979272, 17.203233993860785, -7.116649974567649, -0.021575216626792508, 2.000335099899167, 0.4635526584222581, 0.349530376983387], [13, 1.0, 37.90024197979272, 17.203233993860785, -7.116649974567649, -0.02147987618263869, 2.0085919484035912, 0.466837758467451, 0.35096133223632]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}, {"scales": ["linear", "linear"], "categories": [null, null], "lines": [[13, 1.0, 37.90024197979272, 17.203233993860785, -7.116649974567649, 9.53404441538172e-05, 0.008256848504424319, 0.0032851000451928547, 0.0014309552529329914], [2, 0.0, 1.0, 0.5, 0.5, 0.02000335099899167, 0.02000335099899167, 0.02000335099899167, 0.0]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} image/svg+xml From f02861b8c8cb331b58a36347b0162bef1b619e49 Mon Sep 17 00:00:00 2001 From: timlichtenberg Date: Sat, 24 Oct 2026 12:54:54 +0200 Subject: [PATCH 14/14] Pin proteus-mpl 1.5.0 and make the validation plots again proteus-mpl 1.5.0 compares the data summary of each validation plot with the committed pair in every test run, so the pair is made again with 1.5.0 and now carries the summary. --- .github/workflows/ci_tests.yml | 2 +- .github/workflows/nightly.yml | 2 +- .../core_layer_erfc_half_space_0p03myr.svg | 2 +- .../core_layer_erfc_half_space_0p03myr_dark.svg | 2 +- .../core_layer_erfc_half_space_0p1myr.svg | 2 +- .../core_layer_erfc_half_space_0p1myr_dark.svg | 2 +- pyproject.toml | 2 +- tests/conftest.py | 17 ----------------- 8 files changed, 7 insertions(+), 24 deletions(-) diff --git a/.github/workflows/ci_tests.yml b/.github/workflows/ci_tests.yml index dc325353..ebae3687 100644 --- a/.github/workflows/ci_tests.yml +++ b/.github/workflows/ci_tests.yml @@ -76,7 +76,7 @@ jobs: - name: Install pip-only test deps (scikits-odes-sundials builds against conda SUNDIALS) run: | - pip install pytest pytest-xdist pytest-dependency pytest-timeout hypothesis ruff "proteus-mpl>=1.4.0" + pip install pytest pytest-xdist pytest-dependency pytest-timeout hypothesis ruff "proteus-mpl>=1.5.0" # Install scikits-odes-sundials directly (skip the scikits-odes # metapackage). This avoids scikits-odes-daepack, which is a # transitive Fortran dep with f2py incompatibility with numpy diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 9fa22daa..b1513733 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -58,7 +58,7 @@ jobs: - name: Install pip-only test deps (scikits-odes-sundials builds against conda SUNDIALS) run: | - pip install pytest pytest-cov pytest-xdist pytest-dependency pytest-timeout hypothesis "proteus-mpl>=1.4.0" + pip install pytest pytest-cov pytest-xdist pytest-dependency pytest-timeout hypothesis "proteus-mpl>=1.5.0" # Direct install of scikits-odes-sundials only; aragog imports # CVODE directly from scikits_odes_sundials.cvode and skips the # daepack-pulling metapackage. See ci_tests.yml for details. diff --git a/docs/assets/validation/core_layer_erfc_half_space_0p03myr.svg b/docs/assets/validation/core_layer_erfc_half_space_0p03myr.svg index 86816beb..d8c78b43 100644 --- a/docs/assets/validation/core_layer_erfc_half_space_0p03myr.svg +++ b/docs/assets/validation/core_layer_erfc_half_space_0p03myr.svg @@ -6,7 +6,7 @@ - test: tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.03]; command: pytest 'tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.03]' --validation-plots + test: tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.03]; command: pytest 'tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.03]' --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[9, 1.0, 22.190359007197433, 10.769200446038507, -4.400822321479442, -0.006453914739208852, 0.9842613130845388, 0.21501919580761691, 0.17521861864578445], [9, 1.0, 22.190359007197433, 10.769200446038507, -4.400822321479442, -0.00638833208631695, 0.9892611229897739, 0.21809951937181418, 0.17633153424334422]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}, {"scales": ["linear", "linear"], "categories": [null, null], "lines": [[9, 1.0, 22.190359007197433, 10.769200446038507, -4.400822321479442, 6.55826528919018e-05, 0.005285845252757354, 0.0030803235641972656, 0.0011129155975597724], [2, 0.0, 1.0, 0.5, 0.5, 0.009842613130845388, 0.009842613130845388, 0.009842613130845388, 0.0]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} image/svg+xml diff --git a/docs/assets/validation/core_layer_erfc_half_space_0p03myr_dark.svg b/docs/assets/validation/core_layer_erfc_half_space_0p03myr_dark.svg index c4fd6768..b7c1dca1 100644 --- a/docs/assets/validation/core_layer_erfc_half_space_0p03myr_dark.svg +++ b/docs/assets/validation/core_layer_erfc_half_space_0p03myr_dark.svg @@ -6,7 +6,7 @@ - test: tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.03]; command: pytest 'tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.03]' --validation-plots + test: tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.03]; command: pytest 'tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.03]' --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[9, 1.0, 22.190359007197433, 10.769200446038507, -4.400822321479442, -0.006453914739208852, 0.9842613130845388, 0.21501919580761691, 0.17521861864578445], [9, 1.0, 22.190359007197433, 10.769200446038507, -4.400822321479442, -0.00638833208631695, 0.9892611229897739, 0.21809951937181418, 0.17633153424334422]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}, {"scales": ["linear", "linear"], "categories": [null, null], "lines": [[9, 1.0, 22.190359007197433, 10.769200446038507, -4.400822321479442, 6.55826528919018e-05, 0.005285845252757354, 0.0030803235641972656, 0.0011129155975597724], [2, 0.0, 1.0, 0.5, 0.5, 0.009842613130845388, 0.009842613130845388, 0.009842613130845388, 0.0]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} image/svg+xml diff --git a/docs/assets/validation/core_layer_erfc_half_space_0p1myr.svg b/docs/assets/validation/core_layer_erfc_half_space_0p1myr.svg index 2b5a67ab..fa0602bc 100644 --- a/docs/assets/validation/core_layer_erfc_half_space_0p1myr.svg +++ b/docs/assets/validation/core_layer_erfc_half_space_0p1myr.svg @@ -6,7 +6,7 @@ - test: tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.1]; command: pytest 'tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.1]' --validation-plots + test: tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.1]; command: pytest 'tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.1]' --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[13, 1.0, 37.90024197979272, 17.203233993860785, -7.116649974567649, -0.021575216626792508, 2.000335099899167, 0.4635526584222581, 0.349530376983387], [13, 1.0, 37.90024197979272, 17.203233993860785, -7.116649974567649, -0.02147987618263869, 2.0085919484035912, 0.466837758467451, 0.35096133223632]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}, {"scales": ["linear", "linear"], "categories": [null, null], "lines": [[13, 1.0, 37.90024197979272, 17.203233993860785, -7.116649974567649, 9.53404441538172e-05, 0.008256848504424319, 0.0032851000451928547, 0.0014309552529329914], [2, 0.0, 1.0, 0.5, 0.5, 0.02000335099899167, 0.02000335099899167, 0.02000335099899167, 0.0]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} image/svg+xml diff --git a/docs/assets/validation/core_layer_erfc_half_space_0p1myr_dark.svg b/docs/assets/validation/core_layer_erfc_half_space_0p1myr_dark.svg index 8714582f..fc9c2015 100644 --- a/docs/assets/validation/core_layer_erfc_half_space_0p1myr_dark.svg +++ b/docs/assets/validation/core_layer_erfc_half_space_0p1myr_dark.svg @@ -6,7 +6,7 @@ - test: tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.1]; command: pytest 'tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.1]' --validation-plots + test: tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.1]; command: pytest 'tests/test_core_layer.py::test_a_young_layer_follows_the_erfc_solution_of_a_half_space[0.1]' --validation-plots; summary: {"v": 1, "axes": [{"scales": ["linear", "linear"], "categories": [null, null], "lines": [[13, 1.0, 37.90024197979272, 17.203233993860785, -7.116649974567649, -0.021575216626792508, 2.000335099899167, 0.4635526584222581, 0.349530376983387], [13, 1.0, 37.90024197979272, 17.203233993860785, -7.116649974567649, -0.02147987618263869, 2.0085919484035912, 0.466837758467451, 0.35096133223632]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}, {"scales": ["linear", "linear"], "categories": [null, null], "lines": [[13, 1.0, 37.90024197979272, 17.203233993860785, -7.116649974567649, 9.53404441538172e-05, 0.008256848504424319, 0.0032851000451928547, 0.0014309552529329914], [2, 0.0, 1.0, 0.5, 0.5, 0.02000335099899167, 0.02000335099899167, 0.02000335099899167, 0.0]], "collections": [], "values": [], "value_scales": [], "patches": [], "images": [], "image_scales": [], "extents": []}]} image/svg+xml diff --git a/pyproject.toml b/pyproject.toml index 123c1126..d44c99d5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,7 @@ dependencies = [ [project.optional-dependencies] test = [ "hypothesis>=6.100.0", - "proteus-mpl>=1.4.0", + "proteus-mpl>=1.5.0", "pytest>=8.1", "pytest-cov>=1.0.0", "pytest-dependency>=0.6.0", diff --git a/tests/conftest.py b/tests/conftest.py index 597320e4..7be484ee 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -173,20 +173,3 @@ def get_reference_file(self, filename: str) -> AbstractContextManager[Path]: @pytest.fixture(scope='module') def helper(): return Helper() - - -@pytest.hookimpl(trylast=True) -def pytest_sessionstart(session): - """Print the data summary of every rendered validation plot, for the platform comparison.""" - import json - - from proteus_mpl import validation - - render = validation.render - - def logged(*args, **kwargs): - files, summary = render(*args, **kwargs) - warnings.warn(f'PILOT-SUMMARY {args[1]} {json.dumps(summary)}', stacklevel=2) - return files, summary - - validation.render = logged