Coupling to Noah-MP¶
Overview¶
The Noah-MP Land Surface Model (LSM) is integrated with ERF to facilitate interaction with the Noah-MP (Multi-Physics) land surface processes.
This documentation covers key components of this interface and its associated data structures and routines, which are implemented in C++ and Fortran, and provides details on initialization and management of data structures necessary for these processes.
Building and Running with Noah-MP¶
To build ERF with Noah-MP support, the NetCDF, NetCDF Fortran, and HDF5 libraries are
required. Furthermore, ERF_ENABLE_NOAHMP=ON must be specified with CMake builds or USE_NOAHMP=TRUE
and USE_NETCDF=TRUE must be specified with GNU Make. Once an executable has been generated, the
inputs file for the simulation must specify Noah-MP as the land surface model type:
erf.land_surface_model = "NOAHMP"
Currently, Noah-MP may only be utilized for simulations that are initialized from a WRF input file
(erf.init_type = "WRFInput"). Additionally, two files are required to be in the run directory
for Noah-MP initialization: namelist.erf and NoahmpTable.TBL. Sample files are provided for
the namelist.erf and NoahmpTable.TBL.
To improve computational efficiency, the Noah-MP timestep, specified via NOAH_TIMESTEP
in the namelist.erf file, may be set larger than the ERF timestep to allow subcycling
in time. For example, if an 4s timestep is utilized for ERF and a 40s timestep is utilized for
Noah-MP, then Noah-MP will be updated every 10 steps.
The latest completed exchange supplies the provider inventory used by 2D
output. The transfer layer converts native Noah-MP specific humidity to dry-air
mixing ratio once. HFX gates Noah-MP returned state, flux, native 2-m, and
soil result fields. It does not gate radiation-to-LSM forcing fields. Processed
return fields still retain field-specific validity checks; unprocessed return
values use the internal land-surface undefined sentinel before public
translation. MOST fallback applies only to unified 2-m diagnostics. It does
not populate raw provider fields or alter prognostic state or flux updates.
For the complete request inventory and canonical order, see Built-in diagnostic catalog. For configuration-level selection and runtime values, see Selection and runtime-value rules. For the numerical source-selection and MOST contracts, see Land-surface and 2-m diagnostics.
Files Overview¶
The ERF-side driver is split by concern across several files under Source/LandSurfaceModel/Noah-MP/:
ERF_NOAHMP.H: The
NOAHMPclass declaration (members, accessors, helpers); the class extendsNullSurf.ERF_NOAHMP_Fields.H: The coupling-field enums and the X-macro field registry.
ERF_NOAHMP_Init.cpp:
Initand the run lifecycle.ERF_NOAHMP_Advance.cpp:
Advance_With_Stateand the per-step state exchange between ERF and Noah-MP.ERF_NOAHMP_Precip.cpp: Precip source collection, snapshots, and guard reporting.
ERF_NOAHMP_IO.cpp: The land plotfile and checkpoint/restart.
Developer design specifications for these files live under
Source/LandSurfaceModel/Noah-MP/dev/ (start with dev/README.md); see
Working on the ERF driver with a coding agent.
The C++ ↔ Fortran coupling glue under Submodules/Noah-MP/drivers/erf
is no longer hand-written. Five files are generated at build time from a single
source of truth by tools/NoahmpMacro.py (see
Generating the C++–Fortran Coupling Glue). Only the tracked *-mc
templates are edited by hand; the generated targets are git-ignored and recreated
on every build:
Template (tracked) |
Generated target (git-ignored) |
Role |
|---|---|---|
|
|
C++ |
|
|
C++ method implementations that forward into Fortran |
|
|
Fortran |
|
|
Fortran storage type holding the coupled members |
|
|
Fortran |
Submodules/Noah-MP/drivers/erf/NoahmpIO.H-mc: Defines the C++
NoahmpIO_typethat is used to interface with Noah-MP implementations, following a similar structure as the underlying Fortran interface (https://dx.doi.org/10.5065/ew8g-yr95). The@NoahmpMacro:Source m_noahmpio { ... }block inside this template is the canonical, ABI-ordered list of all coupled members.Submodules/Noah-MP/drivers/erf/tools/NoahmpMacro.py: The stdlib-only code generator (any Python 3, no third-party libraries) that expands the
@NoahmpMacro:Sourceblock into the five generated targets above. The build runs it automatically;--checkmode drift-checks the generated files against the source block.Submodules/Noah-MP/drivers/erf/NoahmpReadNamelistMod.F90: Hand-written Fortran module that reads
namelist.erf; it also holds the sentinel guards that let a C++-API value override a namelist default for a scalar that is also read from the namelist.Submodules/Noah-MP/drivers/erf/NoahmpWriteLandMod.F90: Hand-written Fortran module that controls which fields appear in the per-timestep NetCDF land output.
Submodules/Noah-MP/drivers/erf/NoahmpWriteRestartMod.F90 and NoahmpReadRestartMod.F90: Fortran modules that serialize and restore the full Noah-MP prognostic state (soil, snow, canopy, aquifer, albedo history, …) to/from a NetCDF restart file. They are exposed to C++ through the
WriteRestart/ReadRestartmethods ofNoahmpIO_typeand are used by ERF’s checkpoint/restart capability (see Checkpoint and Restart).Submodules/Noah-MP/drivers/erf/dev/: Developer design specifications (markdown) for the coupling layer — architecture and file map (
spec-overview.md), the Fortran↔C ABI/API and the code generator (spec-fc-api.md), memory safety (spec-memory-safety.md), the land-output and restart I/O (spec-io-parallel.md,spec-io-restart.md), and the add-a-coupled-variable workflow (spec-add-coupled-variable.md). Start withdev/README.md.
NOAHMP Class¶
The NOAH class serves as the handler for initializing and managing the data structures required for NOAH-MP operations. It inherits from the NullSurf class. This class declares private variable NoahmpIO_type noahmpio, that is passed to NoahMP routines similar to the Fortran interface in the Noah-MP documentation (https://dx.doi.org/10.5065/ew8g-yr95)
NoahmpIO_type Structure¶
This structure is key for handling the input and output operations for NOAH-MP through C++ and Fortran interoperation. Contains various variables for domain, memory, and tile configuration. Also, contains arrays for geographic variables. At present this type exposes only a select set of variables. More variables should be exposed as needed by applications in ERF.
All coupled members are declared once, in ABI order, inside the
@NoahmpMacro:Source m_noahmpio { ... } block of
Submodules/Noah-MP/drivers/erf/NoahmpIO.H-mc. The member’s kind is inferred
from its C++ type; arrays carry a trailing [lo:hi, ...] bounds clause, in
Fortran order, on the member name:
int ids, ide, jds, jde; // dimensions/handles
noahmp_real DTBL; // scalar
noahmp_real ZLVL = -9999.0; // scalar with a default
NoahmpArray2D<noahmp_real> XLAT[xstart:xend, ystart:yend]; // 2-D array
NoahmpArray3D<noahmp_real> T_PHY[xstart:xend, kms:kme, ystart:yend]; // 3-D array
Every bound token must be a literal or another coupled member name (xstart,
xend, kms, kme, nsoil, numrad, …) so C++ and Fortran resolve
it to the same value, and the number of lo:hi pairs must match the array rank.
From this one ordered list the generator re-derives the mirrored fi struct,
both constructors, the move constructor, NOAHMP_IO_FI_NUM_MEMBERS, the Fortran
bind(C) type, the coupled members of the storage type, the
C_LOC/C_F_POINTER wiring, the Fortran allocate() of each array, and the
C++ NoahmpArray extents. Because the C++ struct and the Fortran bind(C)
type are emitted from the same list in one run, their member count, order, and
types are identical by construction.
To expose a new variable you therefore add a single line here and rebuild; the only remaining manual steps are the runtime wiring the generator does not own (the namelist guard for a namelist-read scalar, and optional NetCDF output). See Generating the C++–Fortran Coupling Glue for the full procedure.
Note
Always use noahmp_real (C++) / c_kind_noahmp (Fortran) for coupled
floating-point members, never a hardcoded double/C_DOUBLE. Both are
selected by the DOUBLE_PREC build macro so the boundary precision tracks
amrex::Real. ERF builds Noah-MP in double precision; a run-time guard
(NoahmpIO_AssertAbi) aborts if the two compilers disagree.
Fortran Interoperability¶
The connection between C++ and Fortran is managed through NoahmpIO_fi. This module contains a mirroring of the C++ structure for NOAH-MP input-output operations.
Operations are invoked as methods on NoahmpIO_type; each forwards to its
*_fi Fortran implementation through the generated bind(C) glue. The main
ones are:
void ScalarInitDefault()/void VarInitDefault(): Initialize default scalar and array members ofNoahmpIO_typeand create the C pointers into the Fortran-owned storage.void InitMain(): Main initialization routine for the NOAH-MP operations.void ReadNamelist()/void ReadTable(): Readnamelist.erfandNoahmpTable.TBL.void ReadLandHeader()/void ReadLandMain(): Read the land input data.void DriverMain(): Advance the Noah-MP physics one land-surface step.void WriteLand(int filenum): Write the per-timestep NetCDF land output.void WriteRestart(const std::string& dir)/void ReadRestart(const std::string& dir): Serialize and restore the full prognostic state for checkpoint/restart (see Checkpoint and Restart).
For applications that manage one NoahmpIO_type per domain tile, the
NoahmpIO_vector container mirrors a Fortran allocatable array and is sized
exactly once via resize(size, level).
Usage¶
To use the NOAH class and associated functions, ensure the correct initialization sequence is followed within the simulation setup. The interplay between C++ and Fortran necessitates careful memory and data handling, which is crucial for ensuring performance and correctness in simulations. The interface is designed to mimic the Fortran interface described in the Noah-MP documentation, therefore similar practices should be followed.
Checkpoint and Restart¶
Noah-MP participates in ERF’s standard checkpoint/restart capability (see
Checkpoint / Restart for the general erf.check_file, erf.check_int,
erf.check_per, and amr.restart controls). No additional input options
are required to checkpoint or restart a Noah-MP simulation; whenever a
checkpoint is written, the land-surface state is written alongside the
atmospheric state, and it is read back automatically on restart.
A Noah-MP restart is bitwise reproducible: restarting from a checkpoint reproduces the trajectory of an equivalent cold-start run exactly. Achieving this requires persisting two pieces of state in addition to the regular ERF MultiFab data:
The land-surface substep counter (Noah-MP’s
itimestep). When the Noah-MP timestep is larger than the ERF timestep (subcycling viaNOAH_TIMESTEP; see Building and Running with Noah-MP), the counter determines when Noah-MP fires relative to the ERF steps. It is written to<chkfile>/lsm_step(one value per AMR level) and restored on restart so that the firing schedule, and hence the LSM-to-atmosphere flux timing, is preserved.The full Noah-MP prognostic state. The complete land state — soil temperature and moisture, the snowpack (including the active snow layers and the layer count), canopy and vegetation variables, the aquifer, albedo history, phenology, and accumulators — is serialized at the model’s working precision to
<chkfile>/noahmp_restart/Level_<lev>.ncusingNoahmpWriteRestart/NoahmpReadRestart. Each local block writes its tile into the global-domain NetCDF file collectively.
On restart, ERF first cold-initializes the land state from the WRF input file
and tables (as it would for a fresh run) and then overwrites it with the
checkpointed state; Noah-MP’s per-step input transfer pulls the restored state
into the physics on the first Advance.
Note
These per-level files (lsm_step and noahmp_restart/) are written into
the checkpoint directory and are not AMReX MultiFabs. If you copy or archive a
checkpoint manually, make sure these are included.
Note
Legacy checkpoints. Checkpoints written before this capability was added
do not contain the lsm_step file or the noahmp_restart directory. ERF
detects their absence and falls back to the previous behavior — the substep
counter resets to zero and the Noah-MP state is cold-initialized from the WRF
input — printing a warning that the restarted land trajectory will differ from
a cold start. Such restarts are therefore not bitwise reproducible.
Generating the C++–Fortran Coupling Glue¶
The C++ ↔ Fortran ABI under Submodules/Noah-MP/drivers/erf is produced by a
deterministic, stdlib-only code generator,
tools/NoahmpMacro.py — not by an LLM. The generator reads the single
@NoahmpMacro:Source m_noahmpio { ... } block in NoahmpIO.H-mc and expands
every @NoahmpMacro:<region> marker in the five *-mc templates into the
corresponding generated target (see the table under Files Overview).
The generated targets (NoahmpIO.H, NoahmpIO.cpp, NoahmpIO_fi.F90,
NoahmpIOVarType.F90, NoahmpIOVarInitMod.F90) are git-ignored and recreated
by the build:
GNU Make runs the generator at parse time (before source discovery).
CMake runs it at configure time.
A fresh checkout therefore builds with no manual step. Two convenience targets are available for editing or CI:
# GNU Make (from Submodules/Noah-MP/drivers/erf)
make codegen # regenerate the five targets in place
make codegen-check # exit 1 if regeneration would change anything (drift check)
# CMake
cmake --build <dir> --target noahmp_codegen
cmake --build <dir> --target noahmp_codegen_check
codegen-check fails if any generated target — including the array
allocate() in NoahmpIOVarInitMod.F90 — is out of sync with the
@NoahmpMacro:Source block. Run it in CI.
Adding a coupled variable¶
The full, authoritative procedure lives in Submodules/Noah-MP/drivers/erf/dev/spec-add-coupled-variable.md. In short:
Declare it (boundary glue). Add ONE line to the
@NoahmpMacro:Source m_noahmpio { ... }block inNoahmpIO.H-mc, in the ABI position you want (order in the block = ABI order):noahmp_real RAINBL; // scalar, rainfall [mm] NoahmpArray2D<noahmp_real> RAINLSM[xstart:xend, ystart:yend]; // lsm rain NoahmpArray3D<noahmp_real> SMOIS[xstart:xend, nsoil:nsoil, ystart:yend]; // soil moisture
The generator infers the kind from the C++ type (
int/noahmp_real/NoahmpArray{2,3}D). Arrays carry a trailing[lo:hi, ...]bounds clause in Fortran order; every bound token must be a literal or another coupled member name (xstart,xend,kms,kme,nsoil,numrad, …) so C++ and Fortran resolve it to the same value, and the number of pairs must match the array rank. Scalars (noahmp_real) and ints need no bounds clause; an int may carry a shared C++ default (e.g.int numrad = 2;). A trailing//comment is free-form documentation.Regenerate.
make codegen(orcmake --build ... --target noahmp_codegen); a plain build does this automatically. Usecodegen-checkto verify.Runtime wiring (manual, not generated). The generator emits the member declarations, all ABI plumbing, and the Fortran
allocate()of each array (bounds taken from the[lo:hi, ...]clause), so a brand-new array needs no hand-written allocate. The only manual step is for a scalar that is also read from the Fortran namelist: add a sentinel guard inNoahmpReadNamelistMod.F90so a C++-API value wins over the namelist default.(Optional) NetCDF land output. To make the variable appear in the per-timestep land output, add it to
NoahmpWriteLandMod.F90following the existingTSK/SMOISpattern (seedev/spec-io-parallel.md).
To extend the generator itself — a new region or better diagnostics — see
dev/spec-fc-api.md.
Working on the ERF driver with a coding agent¶
The ERF-side C++ driver (the ERF_NOAHMP_* files under
Source/LandSurfaceModel/Noah-MP/) is not covered by the macroprocessor above
— it contains the GPU-aware, component-indexed state exchange rather than flat ABI
plumbing. It is instead maintained with the help of a coding agent (Claude Code or
similar) driven from its developer specifications.
Those specifications — the source layout and X-macro field registry, the
component-indexed field enums, the GPU-aware state exchange, the run lifecycle, and
the contract a change must respect — live as developer specs under
Source/LandSurfaceModel/Noah-MP/dev/ (start with dev/README.md). The
intended workflow is:
Load
dev/README.mdinto the agent first — it is the map from each file to its concern and to the spec that documents it.Then load the concern-scoped spec for the file being changed (
spec-noahmp-api.mdfor the API and lifecycle,spec-noahmp-gpu.mdfor the per-step data movement,spec-noahmp-io.mdfor I/O, orspec-noahmp-reorg.mdfor the source layout and field registry).Treat the invariants — and, for a new coupled variable, the “Adding a coupled variable” validation checklist in
spec-noahmp-api.md— as the acceptance criteria the change must satisfy.
Keeping the design specs in dev/ beside the code, so a human or an agent works
from the same contract, is part of ongoing AI-for-HPC research using
CodeScribe.
Note
The Submodules/Noah-MP/drivers/erf coupling glue (NoahmpIO.H,
NoahmpIO.cpp, NoahmpIO_fi.F90, NoahmpIOVarType.F90,
NoahmpIOVarInitMod.F90) is not produced this way. That glue is generated
deterministically by tools/NoahmpMacro.py from the @NoahmpMacro:Source
block; do not hand-edit the generated targets. Coupled reals use noahmp_real
/ c_kind_noahmp, whose precision is selected by the DOUBLE_PREC build
flag.