Skip to content

Writing and inspecting NetCDF output

Aragog writes its own NetCDF4 snapshots in standalone use, and PROTEUS-coupled runs additionally produce a per-iteration interior snapshot via the framework wrapper. The two paths are independent on purpose: the standalone schema captures the full SolverOutput dataclass (every scalar diagnostic and every profile), while the PROTEUS schema is a curated subset of that, written under a shorter variable-name convention that the PROTEUS post-processors and read_last_Sfield resume helper expect.

This page covers both: how to write a standalone snapshot, and how to inspect either.

Standalone NetCDF write

SolverOutput.to_netcdf writes a self-contained file capturing every field on the dataclass. For convenience EntropySolver.write_netcdf calls self.get_state().to_netcdf(...) so a typical script reads:

from pathlib import Path
from aragog.solver import EntropySolver

solver = EntropySolver.from_file(
    filename="src/aragog/cfg/abe_solid.toml",
    eos_dir="/path/to/eos/tables",
)
solver.initialize()
solver.set_initial_dSdr_cmb(0.0)        # only when core_bc='energy_balance'
solver.set_initial_entropy(2900.0)
solver.solve()

# Single call: dumps the final state to a NetCDF4 file.
solver.write_netcdf(Path("output/snapshot.nc"), time=0.0,
                    description="Final state of abe_solid smoke run")

The writer mkdirs any missing parent directories, opens the file in mode='w' (overwriting any pre-existing file), and stamps the dataset with aragog_version, an ISO-8601 UTC created_utc, the user-supplied description, and the CF-1.8 conventions tag. Every variable carries a units and a long_name attribute so the file is interpretable without consulting the source.

To dump an intermediate state (rather than the final), call to_netcdf directly on a SolverOutput:

out = solver.get_state()
out.to_netcdf("output/snapshot.nc", time=t_yr)

When the time argument is omitted the writer falls back to dt_actual (the per-call integration duration); pass an explicit time whenever you have an absolute simulation clock.

Variables in the standalone schema

Variables are named verbatim after the SolverOutput field names, with their units in SI by default.

Staggered-node profiles (length \(N\))

Variable Source field Unit Description
r_stag r_stag m Staggered-node radii
P_stag P_stag Pa Pressure at staggered nodes
S_final S_final J kg⁻¹ K⁻¹ Specific entropy
T_stag T_stag K Temperature
phi_stag phi_stag -- Melt mass fraction
rho_stag rho_stag kg m⁻³ Density
visc_stag visc_stag Pa s Dynamic viscosity
vol vol Per-shell volume
mass_stag mass_stag kg Per-shell mass
heating heating W kg⁻¹ Internal heating (radiogenic + tidal)
cap_stag cap_stag kg K m⁻³ Capacitance \(\rho T\)

Basic-node profiles (length \(N+1\))

Variable Source field Unit Description
r_basic r_basic m Basic-node radii
heat_flux heat_flux W m⁻² Total radial heat flux
eddy_diff eddy_diff m² s⁻¹ Thermal eddy diffusivity (\(\kappa_h\))
jcond_b jcond_b W m⁻² Conductive flux
jconv_b jconv_b W m⁻² Convective (MLT) flux
jgrav_b jgrav_b W m⁻² Gravitational-separation flux
jmix_b jmix_b W m⁻² SPIDER-parity phase-mixing flux
dSdr_b dSdr_b J kg⁻¹ K⁻¹ m⁻¹ Radial entropy gradient
phi_basic phi_basic -- Melt fraction at basic nodes
T_basic T_basic K Temperature at basic nodes
cp_basic cp_basic J kg⁻¹ K⁻¹ Heat capacity at basic nodes
rho_basic rho_basic kg m⁻³ Density at basic nodes

Scalars

Time, surface and CMB temperatures, both averaging conventions of the global melt fraction, the energy-balance integrals, and the solver status code:

time, T_magma, T_core, Phi_global, Phi_global_vol, M_mantle, M_mantle_liquid, M_mantle_solid, RF_depth, E_th, E_state, E_state_cons, Cp_eff, F_heat_total, F_cmb, Q_radio_total, Q_tidal_total, 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, dt_actual, status (i4).

Inspecting a snapshot

Either via the netCDF4 CLI, the Python netCDF4 library, or xarray:

ncdump -h output/snapshot.nc                     # header + variable list
ncdump output/snapshot.nc | less                 # full contents
import xarray as xr

ds = xr.open_dataset("output/snapshot.nc")
print(ds)                                          # one-line summary
print(ds["Phi_global"].item())                     # scalar access
print(ds["S_final"].values.shape, ds["r_stag"].values.shape)

ncdump is bundled with most netCDF4 distributions; on macOS brew install netcdf provides it, on Linux apt install netcdf-bin or dnf install netcdf. xarray is the recommended tool for analysis-style work because it keeps coordinates and units alongside the data.

Plotting from a snapshot

A minimal plot of temperature, melt fraction, and total heat flux versus pressure:

import matplotlib.pyplot as plt
import xarray as xr

ds = xr.open_dataset("output/snapshot.nc")

fig, axes = plt.subplots(1, 3, figsize=(11, 4), sharey=True)
P_GPa = ds["P_stag"].values / 1e9

axes[0].plot(ds["T_stag"], P_GPa)
axes[0].set_xlabel("Temperature [K]")
axes[0].set_ylabel("Pressure [GPa]")
axes[0].invert_yaxis()

axes[1].plot(ds["phi_stag"], P_GPa)
axes[1].set_xlabel("Melt fraction")
axes[1].invert_yaxis()

axes[2].plot(ds["heat_flux"], ds["r_basic"].values / 1e3)
axes[2].set_xlabel(r"Total heat flux [W m$^{-2}$]")
axes[2].set_ylabel("Radius [km]")
axes[2].set_xscale("symlog", linthresh=1e-3)

fig.tight_layout()
fig.savefig("interior_snapshot.pdf")

The per-component flux decomposition lives in the same file as jcond_b, jconv_b, jgrav_b, jmix_b; overlay them on the heat-flux panel for the full Figure 2 of Heat transport.

PROTEUS-coupled snapshots

When Aragog is driven by PROTEUS, the framework wrapper (proteus.interior_energetics.aragog.AragogRunner._write_output_ncdf) writes a separate, more compact per-iteration snapshot at output/data/<iter>_int.nc (where <iter> is the integer time in years), using shorter *_s and *_b variable names that the resume helper recognises. This schema is independent of SolverOutput.to_netcdf so that PROTEUS post-processors and the resume path do not break when the standalone schema evolves.

PROTEUS variable name convention

Always written:

Variable Source field Unit Description
entropy_s S_final J kg⁻¹ K⁻¹ Specific entropy
temp_s T_stag K Temperature derived from \((P, S)\)
phi_s phi_stag -- Melt fraction
density_s rho_stag kg m⁻³ Density
log10visc_s \(\log_{10}(\texttt{visc\_stag})\) \(\log_{10}\) Pa s Base-10 dynamic viscosity
pres_s P_stag / 1e9 GPa Pressure (note: GPa, not Pa)
radius_s r_stag / 1e3 km Staggered-node radii (km)
mass_s mass_stag kg Cell mass
Htotal_s heating W kg⁻¹ Per-node internal heating
radius_b r_basic / 1e3 km Basic-node radii (km)
Ftotal_b heat_flux W m⁻² Total radial heat flux
time -- yr Snapshot time
phi_global Phi_global -- Mass-weighted mean melt fraction

Written only when interior_energetics.write_flux_diagnostics = true:

Variable Source field Unit Description
Jcond_b jcond_b W m⁻² Conductive flux
Jconv_b jconv_b W m⁻² Convective (MLT) flux
Jgrav_b jgrav_b W m⁻² Gravitational-separation flux
Jmix_b jmix_b W m⁻² Chemical-mixing flux
dSdr_b dSdr_b J kg⁻¹ K⁻¹ m⁻¹ Entropy gradient
eddy_diff_b eddy_diff m² s⁻¹ Thermal eddy diffusivity (\(\kappa_h\))
phi_basic_b phi_basic -- Melt fraction at basic nodes
T_basic_b T_basic K Temperature at basic nodes
cp_basic_b cp_basic J kg⁻¹ K⁻¹ Heat capacity at basic nodes
rho_basic_b rho_basic kg m⁻³ Density at basic nodes

Per-iteration scalar diagnostics (\(T_\mathrm{magma}\), \(T_\mathrm{core}\), \(\Phi_\mathrm{global}\), \(E_\mathrm{th}\), \(C_p^\mathrm{eff}\), \(F_\mathrm{heat}^\mathrm{total}\), the four step_dE_*_J integrals) live in output/runtime_helpfile.csv rather than the snapshot, since PROTEUS aggregates them across all interior steps.

Resuming from a PROTEUS snapshot

PROTEUS itself uses proteus.interior_energetics.aragog.read_last_Sfield to re-load the previous step's entropy field at the start of the next iteration. The same helper is the canonical way to resume a run from a saved snapshot:

from proteus.interior_energetics.aragog import read_last_Sfield

S_prev = read_last_Sfield(output_dir="output", time=t_prev)
solver.set_initial_entropy(S_prev)

The helper reads entropy_s (PROTEUS schema) from the snapshot, validates the array length against the live mesh, and returns the entropy in J/kg/K. For standalone snapshots written via to_netcdf, the equivalent field is S_final and you can re-load it with one line of xarray or netCDF4.