SHOC host-interface audit: EAM (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.

0. File map: where each layer lives

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.

EAM (Fortran) one file per layer EAMxx (C++) several files per layer shoc_intr.F90 host interface builds inputs, applies tendencies file layout: declarations + code together eamxx_shoc_process_interface .hpp — pre/post-processing formulas .cpp — orchestration, TMS, checks file layout: one module in two files shoc.F90 the physics: shoc_main + nadv loop all subroutines in one file shoc_functions.hpp + impl/ impl/shoc_main_impl.hpp (nadv loop) impl/shoc_*_impl.hpp (54 files, one per shoc.F90 subroutine) calls shoc_main calls shoc_main equivalent equivalent

1. Identical between the two interfaces

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.

2. Structural differences (the ones that matter)

#ItemEAMEAMxx
1Heights / 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.
2Dry 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
3Tracers 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)
4Where 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
5Update 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)

Subcycling side by side (row 4 illustrated)

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.

EAM physics step, e.g. 1800 s (ne30) macmic loop ×6 SHOC ↔ P3 alternated; 300 s per call tendencies weighted 1/6 shoc_main internal nadv loop: nadv = hdtime/shoc_timestep (namelist; default nadv = 1) in-and-out: macmic=1, shoc_timestep=60 → nadv=360 EAMxx atmosphere step mac_aero_mic group subcycling ×N driver repeats shoc→cldfrac→p3… N=12 (ne30), 24 (ne4); dt_sub = dt/N SHOC process nadv = 1 forced assert dt_sub ≤ 300 s no internal-subcycling lever (standalone driver loops shoc_main itself instead)

3. Only in EAMxx (locations)

4. Only in EAM (locations)

The EAM side at a glance: everything around SHOC that is not SHOC itself

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).

shoc_intr.F90 — shoc_tend_e3sm (one call per macmic iteration) 1. state copy: state1 = state state stays read-only; the routine works on state1 2. input construction theta_l, thv, qt, heights/dz, density, wm_zt from omega, dse; TKE floor; dx/dy; tracer list (excludes modal aerosols and NUMLIQ) 3. surface fluxes to kinematic units shf, cflx, wsx/wsy divided by density; Exner factor on heat only 4. in-and-out only: ASCII reader overwrites the state profiles, pressures (state1) and fluxes now come from the IC files shoc.F90 — call shoc_main (dtime, nadv) 5. column energy integrals (initial snapshot) dse, kinetic energy, vapor, liquid — saved for the fixer 6. nadv loop (x N): raw SHOC itself turbulence: mixing lengths, TKE, implicit diffusion, moments, cloud PDF — evolves thl, qt, u, v, TKE, ql, cldfrac 7. translate back: update_host_dse final theta_l and ql converted to T and dse = cp T + g z + phis; same energy snapshot again, now of the final state 8. energy fixer enforces: final energy = initial + surface input; the residual is subtracted from dse uniformly over the turbulent layer 9. final diagnostics qv, u-star, Obukhov length, PBL height (pblh) in-and-out only: per-substep txt write (inside or outside the loop) 10. tendencies: ptend = (new - old)/hdtime u, v, dse, qv, ql, TKE, tracers; applied to state1 (physics_update) 11. cloud-fraction macrophysics alst/qlst, aist (ice), ast, deep-convective fraction (dp1), concld 12. deep-convection detrainment dlf split liquid/ice by a T ramp; det_s/det_ice for the global energy budget 13. fields for other processes (pbuf) tk/tkh/tke/wthv interpolated to interfaces (kvm, kvh); relvar; pblh 14. history output (outfld) TKE, moments, fluxes in W/m2, cloud fractions, PBLH, LSCALE... state / raw SHOC interface (shoc_intr) shoc.F90 outside the loop in-and-out only

Where each step lives (EAM diagram above):

StepLocation
1 state copyshoc_intr.F90:791
2 input constructionshoc_intr.F90:~860-966 (dx :861-870, thermo/heights :879-933, TKE floor :901)
3 surface-flux conversionshoc_intr.F90:940-951
4 in-and-out ASCII readercall 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 fixercall shoc.F90:722-729; subroutines :4036-4308
9 final diagnostics (qv, ustar, obklen, pblh)shoc.F90:739-754
in-and-out txt writeshoc.F90:690-703 (exp1) / shoc_intr.F90:1062-1077 (exp2)
10 ptend build + local updateshoc_intr.F90:1114-1147
11 cloud-fraction macrophysicsshoc_intr.F90:1232-1339
12 deep-convection detrainmentshoc_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 EAMxx side at a glance: the same map for run_impl

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.

eamxx_shoc_process_interface — run_impl (one call per mac_aero_mic sub-iteration) 1. fields read from the field manager (single copy) no state1: there is no second copy of the state; pressures are read-only 2. SHOCPreprocess (formulas in the .hpp) theta_l, thv, qw = qv+qc, dse; hydrostatic dz and z integrated bottom-up (z_surf=0); densities; wm_zt from omega; TKE floor; tke/qc copies (tracer bundle); surface fluxes to kinematic units (Exner factor on heat only) 3. TMS hook (optional, default off) subtracts orographic drag from the surface momentum fluxes 4. flux-state consistency check (optional) if evaporation would exhaust bottom-level qv: redistributes and zeroes surf_evap 5. preparation nadv = 1 forced; assert dt <= 300 s; workspace reset SHF::shoc_main (impl/ — the same physics as shoc.F90) 6. column energy integrals (initial snapshot) dse, kinetic energy, vapor, liquid — saved for the fixer 7. nadv loop (x1 here): raw SHOC itself turbulence: mixing lengths, TKE, implicit diffusion, moments, cloud PDF — code equivalent to the Fortran (validated bit-for-bit in the in-and-out) 8. translate back: update_host_dse final theta_l and ql converted to dse = cp T + g z + phis; new energy snapshot of the final state 9. energy fixer final energy = initial + surface input; residual subtracted from dse over the turbulent layer 10. final diagnostics qv, u-star, Obukhov length, pblh (no txt hooks: no in-and-out path) 11. SHOCPostprocess (formulas in the .hpp) restores tke/qc into the tracer bundle; qv = qw - qc; T recovered from dse; cloud-fraction clamp; inv_qc_relvar (for P3) 12. property checks (always on) bounds on T, qc, winds, TKE, cldfrac (abort); negative qv is repaired to 0 13. write to the field manager (in place, no ptend) T, qv, qc, winds, TKE, cldfrac updated directly; pbl_height, ustar, obklen, eddy_diff_heat/mom, w_variance; extra diagnostics if requested absent here (vs. EAM): lives in other processes or does not exist cloud-fraction macrophysics (cldFraction process), deep-convection detrainment, pbuf interface interpolations, in-and-out path, state1 copy state / raw SHOC interface (.hpp/.cpp) shoc_main outside the loop optional / checks

Where each step lives (EAMxx diagram above; cpp/hpp = eamxx_shoc_process_interface, impl = impl/shoc_main_impl.hpp; line numbers for this branch):

StepLocation
1 fields from the field managerdeclared 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 hookcpp:560-577 (gate cpp:488)
4 flux-state consistency checkcpp:580-627 (gate cpp:492)
5 preparation (nadv=1, dt≤300, workspace)cpp:473-502
6 energy integrals (before)impl:~162impl/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-305impl/shoc_update_host_dse_impl.hpp
9 energy fixerimpl:~305-310impl/shoc_energy_fixer_impl.hpp
10 final diagnosticsimpl:~315-335 (compute_shoc_vapor, diag_obklen, pblintd)
11 SHOCPostprocess (output formulas)struct hpp:271-380; launched cpp:513-516
12 property checkscpp:411-422 (registered at init; run by the driver after each step)
13 write to field manager / extra diagnosticsin-place via the views; extra diags copied cpp:519-552

5. Flags and minor findings

6. Implications for the ERF integration (stage 2)

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.

Appendix: why the C++ code is organized this way (a primer)

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).

EAMxx goal (via Kokkos) one source: CPU+GPU, double+single, pack 16+1, several consumers physics = a library with blanks (templates) Functions<S,D>: same text, several filled-in variants template contagion everything shoc_main calls inherits the blanks has blanks → headers (.hpp) pasted text; mandatory for templates impl/*.hpp (the physics, 54 files) preprocess formulas in the interface .hpp concrete → .cpp compiled once, cheap to change run_impl, TMS hook, checks, setup (still runs on GPU!) the economic rule deciding between the two .cpp whenever possible; .hpp only when required (templates) and Fortran? modules = everything-together, always shoc_intr.F90 and shoc.F90: declarations + code in one file; changing a module recompiles every USEr — just like C++ headers

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).