shoc_intr.F90) vs EAMxx (eamxx_shoc_process_interface)New companion page: SHOC call-flow map — the Fortran flow as one annotated diagram, with the C++ home of each piece identified alongside.
Systematic, formula-by-formula comparison of the two SHOC host interfaces, on branch
maint-3.0-scm-mchinita-shoc_in_and_out_cpp. Method: one full extraction pass over both
sources, followed by an independent adversarial verification pass that re-checked every claim
against the code (two claims were corrected by it; noted below). Line-number shorthand: :NNN = components/eam/src/physics/cam/shoc_intr.F90 (EAM); cpp:NNN / hpp:NNN = components/eamxx/src/physics/shoc/eamxx_shoc_process_interface.cpp / .hpp (EAMxx; the .hpp header holds the SHOCPreprocess/SHOCPostprocess structs where most input/output formulas live — see the file map below). Full paths:
components/eam/src/physics/cam/shoc_intr.F90 (EAM) and
components/eamxx/src/physics/shoc/eamxx_shoc_process_interface.cpp/.hpp (EAMxx).
Companion pages: C++ vs Fortran shoc_main validation,
exp1 vs exp2 equivalence.
2026-07-07, Maria Chinita + Claude Fable 5.
The two codes split the same two layers (host interface / SHOC physics) across files differently.
In EAM each layer is one Fortran file. In EAMxx the interface is one module split across a header
(.hpp, which holds the SHOCPreprocess/SHOCPostprocess structs with the input/output
formulas — C++ template/Kokkos code must live in headers) and an implementation file
(.cpp: initialize/run orchestration, TMS, checks); the physics is spread over
shoc_functions.hpp plus 54 files in impl/, one per Fortran subroutine
(e.g. the nadv loop is impl/shoc_main_impl.hpp). The interface pair contains no
physics; the impl/ headers contain no interface code.
All shared point-wise formulas are identical or trivially equivalent:
inv_exner (EAM :879 / hpp:92), θ (:897 / hpp:112), θl (:898 / hpp:113),
thv = θ(1+zvirqv−qc) (:899 / hpp:114), qw=qv+qc
(:891-893 / hpp:108), wm_zt=−ω/(ρg) (:926 / hpp:120), rrho (:925 / hpp:119), rrho_i interpolation
(:938 / hpp:149), surface-flux conversions (Exner factor on the heat flux only, surface-interface
density, no sign flips; :943-951 / hpp:152-158; field map shf↔surf_sens_flux, cflx(:,1)↔surf_evap,
wsx/wsy↔surf_mom_flux(:,0/1)), TKE floor at 0.0004 m²/s² (:901 / hpp:97), qv=qw−qc
recovery (:1119 / hpp:287), cldfrac clamp to 1 (:1092 / hpp:289), relvar ↔ inv_qc_relvar
(same qc²/qc²̄ with [0.001,10] clamp; :1220 / hpp:293-300),
dx from the same WGS84 ellipsoid formula (:1415-1430 / cpp:452-467).
All shared tunings have identical defaults
(namelist_defaults_eam.xml:831-844 vs namelist_defaults_scream.xml:223-234):
thl2/qw2/qwthl2/w2tune=1.0, length_fac=0.5, c_diag_3rd_mom=7.0, lambda_low/high/slope/thresh=
0.001/0.04/2.65/0.02, Ckh=Ckm=0.1. The stable-regime Ckh_s/Ckm_s are namelist parameters in EAM (0.1)
and hardcoded to the same 0.1 in the C++ kernel (shoc_eddy_diffusivities_impl.hpp:39-40)
— numerically identical, only non-configurable in EAMxx.
Non-issue confirmed by the verification pass: there is no wet/dry mixing-ratio
mismatch. EAM's Q is specific humidity (wet) used without conversion; P3's condensates are registered
with the default mixtype='wet' (constituents.F90:157); EAMxx uses wet kg/kg throughout.
| # | Item | EAM | EAMxx |
|---|---|---|---|
| 1 | Heights / dz (largest structural difference) | Uses host dycore geopotential heights directly: dz = zi(k)−zi(k+1) (:916), zt/zi grids from state%zm/zi minus surface zi (:924, :933) | Reconstructs dz hydrostatically: dz=(Rd/g)·pseudo_density·Tv/p (hpp:117, Tv from qv only), then integrates z_int bottom-up with z_surf=0 (hpp:128-130). The discrepancy propagates into rrho, wm_zt and flux conversions. |
| 2 | Dry static energy source | Passes host state%s directly (:927) | Recomputes dse=cpT+gz+φs from its own z_mid (hpp:140); T recovered from the same dse/z on output (hpp:306) so it round-trips internally |
| 3 | Tracers diffused by SHOC | All constituents except modal-aerosol species/numbers and NUMLIQ when prog_modal_aero (shoc_init_e3sm:415-438). qv/ql/tke are in the bundle too but their diffused values are discarded at tendency time (:1133) in favour of the thermodynamic channel | The whole bundled tracers group, no exclusions (cpp:272,347); tke/qc protected by copies
(hpp:99-106) and group-diffused qv overwritten by qw−qc (hpp:287) — same discard
logic as EAM, different mechanism. No aerosol exclusion (EAMxx typically runs prescribed aerosol) |
| 4 | Where subcycling lives corrected by the verification pass | Three nested levels: physics step (e.g. 1800 s) → macmic loop
(cld_macmic_num_steps=6 with SHOC+P3 → 300 s per call, tendencies weighted 1/6) →
optional SHOC-internal nadv via shoc_timestep namelist (default off) |
Two levels: atmosphere step → driver-level subcycling of the mac_aero_mic process
group (number_of_subcycles: 24 ne4, 12 ne30, 6 ne120/256, …;
namelist_defaults_scream.xml:439-446), each sub-iteration calling SHOC once with nadv=1
forced and a hard dt≤300 s assert (cpp:473). Both models subcycle; EAMxx just has no
SHOC-internal nadv lever |
| 5 | Update style | Builds ptend (u,v,s,q) applied via physics_update (:1114-1147), plus a second detrainment ptend (:1175-1212) | Updates fields in place in SHOCPostprocess (hpp:282-308) |
Both models run SHOC at a few-minute effective timestep; they just nest the loops differently.
EAM: physics step → macmic loop (turbulence↔microphysics alternation) → optional
SHOC-internal nadv. EAMxx: atmosphere step → driver-level subcycling of the whole
mac_aero_mic group, nadv pinned to 1. The in-and-out runs exploit EAM's innermost lever
(macmic=1, shoc_timestep=60 → nadv=360), which the standalone C++ driver replicates
by looping shoc_main itself.
apply_turbulent_mountain_stress() cpp:560-577, gated by
apply_tms (default false, cpp:67; toggle at cpp:488): subtracts
drag·ubot/ρi,bot from upwp/vpwp_sfc before shoc_main. In EAM, TMS is applied
in the vertical_diffusion/CLUBB paths, never in the SHOC branch (compute_tms imported at :590 but unused).cldFraction process.One vertical pass through shoc_tend_e3sm (called once per macmic iteration), with the
raw SHOC physics collapsed into a single box (step 6). Colors: teal = interface operations
(shoc_intr.F90); purple = shoc.F90 operations outside the nadv loop (shown bit-for-bit neutral for
SHOC's prognostics on the exp1 vs exp2 page); amber =
in-and-out mode only; gray = state handling / raw SHOC. Steps 11-13 are the EAM-only machinery that
EAMxx delegates to separate processes (section 4).
Where each step lives (EAM diagram above):
| Step | Location |
|---|---|
| 1 state copy | shoc_intr.F90:791 |
| 2 input construction | shoc_intr.F90:~860-966 (dx :861-870, thermo/heights :879-933, TKE floor :901) |
| 3 surface-flux conversion | shoc_intr.F90:940-951 |
| 4 in-and-out ASCII reader | call at shoc_intr.F90:986; reader :1448-1655 |
| 5 energy integrals (before) | shoc.F90:502-505 |
| 6 nadv loop (raw SHOC) | shoc.F90:507-706 |
| 7 update_host_dse + integrals (after) | shoc.F90:712-720 |
| 8 energy fixer | call shoc.F90:722-729; subroutines :4036-4308 |
| 9 final diagnostics (qv, ustar, obklen, pblh) | shoc.F90:739-754 |
| in-and-out txt write | shoc.F90:690-703 (exp1) / shoc_intr.F90:1062-1077 (exp2) |
| 10 ptend build + local update | shoc_intr.F90:1114-1147 |
| 11 cloud-fraction macrophysics | shoc_intr.F90:1232-1339 |
| 12 deep-convection detrainment | shoc_intr.F90:1158-1213 |
| 13 pbuf interface fields (kvm/kvh/tke_zi) | shoc_intr.F90:1102-1111 |
| 14 history (outfld) | shoc_intr.F90:1359-1390 |
The twin diagram for EAMxx, same visual grammar (raw SHOC collapsed, same colors). Structural contrasts with the EAM diagram above: no state copy (step 1 — single field-manager copy, which is why the state/state1 class of inconsistency cannot exist here), the amber extras EAM does not have (TMS hook, flux check, property checks), in-place updates instead of ptend (step 13), and the final gray box lists what moved out of SHOC entirely.
Where each step lives (EAMxx diagram above; cpp/hpp = eamxx_shoc_process_interface, impl = impl/shoc_main_impl.hpp; line numbers for this branch):
| Step | Location |
|---|---|
| 1 fields from the field manager | declared in SHOCMacrophysics::set_grids (a fourth standard entry point, run before initialize: the process requests its fields from the field manager), eamxx_shoc_process_interface.cpp:22-123; views aliased in initialize_impl, cpp:264-303 |
| 2 SHOCPreprocess (input formulas) | struct hpp:71-265; configured cpp:318-322; launched cpp:482-485 |
| 3 TMS hook | cpp:560-577 (gate cpp:488) |
| 4 flux-state consistency check | cpp:580-627 (gate cpp:492) |
| 5 preparation (nadv=1, dt≤300, workspace) | cpp:473-502 |
| 6 energy integrals (before) | impl:~162 → impl/shoc_energy_integrals_impl.hpp |
| 7 nadv loop (raw SHOC) | impl:~165-290 → kernels in impl/shoc_*_impl.hpp (54 files) |
| 8 update_host_dse + integrals (after) | impl:~295-305 → impl/shoc_update_host_dse_impl.hpp |
| 9 energy fixer | impl:~305-310 → impl/shoc_energy_fixer_impl.hpp |
| 10 final diagnostics | impl:~315-335 (compute_shoc_vapor, diag_obklen, pblintd) |
| 11 SHOCPostprocess (output formulas) | struct hpp:271-380; launched cpp:513-516 |
| 12 property checks | cpp:411-422 (registered at init; run by the driver after each step) |
| 13 write to field manager / extra diagnostics | in-place via the views; extra diags copied cpp:519-552 |
state1%pmid/pint (updated state) to shoc_main where upstream passed state%pmid/pint
(original line kept commented, :1039-1041). Worth confirming with the team that this is intended and documented.hdtime is declared Int (hpp:499) but assigned a
double dt (cpp:499). Harmless today (nadv is always 1) but lossy if the internal-subcycling TODO
(cpp:496-498) is ever implemented. Candidate upstream fix.For coupled single-column comparisons (E3SM-SCM vs ERF with other parameterizations off), the interface-level divergence sources reduce to rows 1 and 2 of the table (source of heights/dz and of dse — ERF will supply its own grid heights, as EAMxx does) plus the dt≤300 s per-call cap. Row 3 is inert for DYCOMS (no active tracers), TMS stays off, property checks rarely trigger, and ptend-vs-in-place is mathematically equivalent. The EAM-only cloud-fraction/detrainment machinery (section 4) is inert when microphysics and convection are off.
For readers coming from Fortran. Two file types exist: a .cpp is like a
.F90 (compiled once, cheap to change); a .hpp with code in it is like an
.inc (text pasted into every file that includes it — recompiled everywhere, and changing
it recompiles all includers). C++ templates ("code with blanks") let one source text serve
several concrete variants — double vs single precision, vector pack 16 vs 1, different consumers
(the model, the BFB tests, the standalone driver, ERF) — and the compiler must see the full definition
wherever it creates a new filled-in variant. That leaves two options: keep templated code in headers
(so any consumer can instantiate any variant — the impl/*.hpp choice, right when the set of
variants is open-ended), or pre-instantiate a fixed list of variants in a .cpp (exactly what
eti/ does for the standard variant, as a compile-time optimization). Templates are also
contagious: everything shoc_main calls inherits its blanks, so the whole physics call
tree lives in impl/*.hpp. Everything concrete (run_impl, the TMS hook, the checks) goes
to .cpp — and still runs on GPU, since CPU-vs-GPU is decided per build for all files;
that was never what the blanks are for. The governing rule is economic: .cpp whenever possible
(fast builds), .hpp only when required (templates).
Directory map of components/eamxx/src/physics/shoc/: top level = the interface
(eamxx_shoc_process_interface.*), the physics declarations (shoc_functions.hpp),
the Fortran↔C++ bridges (shoc_iso_c/f.f90, shoc_functions_f90.*) and the
test harness (shoc_main_wrap.*, shoc_f90.*, shoc_ic_cases.*);
impl/ = the physics itself, one templated header per shoc.F90 subroutine (54);
eti/ = explicit template instantiations of the standard variant, a compile-time
optimization (42); disp/ = per-subroutine kernel launchers for the optional
SCREAM_SHOC_SMALL_KERNELS build mode (15); tests/ = per-subroutine BFB unit tests,
the shoc_run_and_cmp regression test, and the standalone shoc_in_and_out driver with its README (73).