Add two-stream radiation model with simplified surface energy balance (force-restore) - #3950
Open
hgopalan wants to merge 207 commits into
Open
Add two-stream radiation model with simplified surface energy balance (force-restore)#3950hgopalan wants to merge 207 commits into
hgopalan wants to merge 207 commits into
Conversation
…nd Solvers Co-authored-by: hgopalan <1108371+hgopalan@users.noreply.github.com>
…m-radiation Phase 1: Two-stream atmospheric radiation module—infrastructure and clear-sky solver
Co-authored-by: hgopalan <1108371+hgopalan@users.noreply.github.com>
- Add ERF_RadiationDiagnostics.cpp and ERF_AdvanceTwoStreamRadiation.cpp to CMake build - Add Source/Radiation include directory to CMake - Add ERF_AdvanceTwoStreamRadiation.cpp to GNUmake Make.package - Add compute_twostream_radiation_diagnostics() function declaration to ERF.H - Implement compute_twostream_radiation_diagnostics() for Phase 1 diagnostic output - Call compute_twostream_radiation_diagnostics() from ERF::Advance() at end of timestep - radChoice member and init_params() call already in place (Phase 1 PR) Co-authored-by: hgopalan <1108371+hgopalan@users.noreply.github.com>
…iation-module Wire Phase 1 Radiation Module into Build System and Timestep Loop
…n test folders Previously sounding_us_standard_atm lived at Exec/CanonicalTests/Radiation/ (outside both case folders) and was shared by reference across SW_ClearSky_Analytical and LW_Isothermal. Per repo convention (see Exec/CanonicalTests/ABL/*, e.g. input_sounding_GABLS1, mrf_sounding_unstable), sounding files must live inside their own case folder and be named after that case. This adds input_sounding_sw_clearsky and input_sounding_lw_isothermal inside their respective folders and updates the inputs files to reference them. The old shared file is removed.
Co-authored-by: hgopalan <1108371+hgopalan@users.noreply.github.com>
… kernel Co-authored-by: hgopalan <1108371+hgopalan@users.noreply.github.com>
…AD_MPI_SKILLS.md Co-authored-by: hgopalan <1108371+hgopalan@users.noreply.github.com>
…radiation Phase 2: Real per-column vertical integration for two-stream radiation
…plementation Co-authored-by: hgopalan <1108371+hgopalan@users.noreply.github.com>
Co-authored-by: hgopalan <1108371+hgopalan@users.noreply.github.com>
…olumn-kernel Wire per-column radiation kernel into diagnostics driver
… real dz Co-authored-by: hgopalan <1108371+hgopalan@users.noreply.github.com>
…nown issues Co-authored-by: hgopalan <1108371+hgopalan@users.noreply.github.com>
…completion Phase 2c: GPU-safe ParallelFor, real LW downward sweep, and documentation updates
…raction masking) - Add erf.radiation.tau_profile_type (constant|cloud_layer) to RadChoice - Add cloud_base_height_m, cloud_top_height_m, cloud_tau_per_layer params - Add erf.radiation.cloud_fraction (default 0.0, byte-identical when 0) - Extend vertical_two_stream_sweep() to compute clear-sky and cloudy-column SW/LW fluxes and blend via F = (1-cf)*F_clear + cf*F_cloudy - Add SW_Cloud_Layer RegTest - Update RAD_DEVELOPMENT.md and RAD_MPI_SKILLS.md with Phase 3 + Phase 2d sections
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: hgopalan <1108371+hgopalan@users.noreply.github.com>
Co-authored-by: hgopalan <1108371+hgopalan@users.noreply.github.com>
…IFICATION.md Co-authored-by: hgopalan <1108371+hgopalan@users.noreply.github.com>
…n-up-canonical-tests # Conflicts: # PHASE_12_MANUAL_VERIFICATION.md # PHASE_13_IMPLEMENTATION_SUMMARY.md # Source/Radiation/RAD_DEVELOPMENT.md # Source/Radiation/RAD_MPI_SKILLS.md Co-authored-by: hgopalan <1108371+hgopalan@users.noreply.github.com>
Professionalize Radiation test suite structure and documentation
…put, and canonical cases The two-stream column sweep treated the lowest vertical index as the top of the atmosphere and the highest as the surface, opposite to ERF's convention (and to the cloud-band helper in the same file). The sweep now uses k = kmin as the surface layer and k = kmax as the top layer: SW sweeps downward from kmax, and LW stores interface fluxes (m = 0 surface, m = nlev TOA) from a downward then an upward sweep. The per-layer optical depth assembly is factored into diagnose_layer_tau(). The LW heating-rate kernel also had the wrong sign for its stated convention and now returns -(dF_net/dz)/(rho cp). Layer temperature is now obtained from rho*theta through the equation of state (Exner function) instead of using theta directly, for LW emission and for the pressure/RH used by the prognostic cloud fraction. The plotfile capability gate only recognized the RRTMGP path, so qsrc_sw and qsrc_lw were dropped for TwoStream runs; it now matches the qheating_rates allocation. Canonical radiation cases: checkers parse the diagnostics CSV by header name (the call_site column broke positional parsing), SW checkers account for the surface albedo in the SW_surface diagnostic, the nonuniform-dz and SEB checkers accept pre/post-dycore rows and the extended column set, two soundings that carried water vapor without a moisture model are dried, the dynamic diurnal case is shortened to a 2-hour sunset window, a checker is added for SW_Cloud_Layer, README script names are corrected, and stale reference diagnostics are regenerated. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Fix two-stream radiation vertical orientation and Exner temperature; repair canonical cases
…flected LW surface term The TwoStream ctest entries were gated behind ERF_ENABLE_RRGMTP (a typo for RRTMGP), had no test_files inputs, and needed gold plotfiles that do not exist, so they never ran. Replace them with gold-free tests: TwoStream_ColumnHeating runs a short SW+LW column case and a new C++ checker (Tests/TwoStreamRadiationCheck.cpp) verifies from the plotfile that SW heating is strongest at the top layer and LW cooling to space is strongest at the top layer; Plotfile3D_TwoStreamHeatingSelection checks that qsrc_sw/qsrc_lw are written for the TwoStream path. Move the per-column two-stream helpers and vertical_two_stream_sweep() into Source/Radiation/ERF_TwoStreamColumn.H so unit tests can call them, and add Tests/Unit/Radiation with kernel tests (Beer-Lambert, diffuse source gating, gray-gas fixed points, LW heating sign) and column tests (Exner temperature, surface at k = 0, absorbed SW surface flux, cooling to space, reflected surface term, isothermal mode, night, disabled bands). The column test exposed that PR #325 was merged without its second commit, so the reflected (1 - eps) F_down(0) term in the LW surface boundary condition is re-applied here. Also fix Tests/Unit/IO/ERF_GTestPlotfileSelection.cpp, which called make_capabilities with a stale five-argument signature and prevented erf_unit_tests from compiling on this branch. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Wire two-stream radiation into unit and regression tests; re-apply reflected LW surface term
Remove dead code and unused options: - ERF_RadiationParams.H and ERF_RadiationCoupling.H were never included (a no-op stub and a duplicate of RadChoice); ERF_TwoStreamColumn.H is listed in Make.package in their place. - erf.radiation.cloud_fraction_smooth_enable / cloud_fraction_smooth_alpha were parsed and documented but never applied (the sweep has no persistent per-level state for an EMA). The inputs, the unused smooth_cloud_fraction_ema() helper and the docs entries are removed. Moisture handling in the column kernels: - The SW/LW dynamic-tau helpers read RhoQ1/RhoQ2 without checking the state carried them and contained a duplicated block after an always-true #if. They are replaced by get_qv_from_state()/get_qc_from_state() (guarded on nComp) and one diagnose_tau_dynamic() shared by both bands. - diagnose_cloud_fraction_prognostic() uses the same helpers and drops its unused Geometry argument. Cloud band on stretched grids: - The cloud band was located with the uniform geom.CellSize(2) even on stretched grids while the aerosol profile used z_phys_cc. Both now use one layer-center height above the surface, so is_cloud_level, tau_layer_value and select_scattering_props take a height instead of (k, kmin, dz). Identical on uniform grids. Constants: cp uses ERF's Cp_d and the Stefan-Boltzmann constant is defined once (stefan_boltzmann in ERF_TwoStreamLW.H). Unit tests cover the cloud band by height, the linear dynamic optical depth with clamping, and the moisture helpers on dry and moist states. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Clean up the two-stream radiation driver
…olumns Shortwave: the diffuse field is now a two-stream solution with upward and downward streams. compute_sw_layer_two_stream() replaces the downward-only single-scatter source with the Meador-Weaver (1980) layer solution (reflectance/transmittance for diffuse and direct incidence) using the practical-improved-flux-method gamma coefficients. The column sweep combines the layers with the surface by the adding method (A_0 = albedo, S_0 = albedo * F_dir(0)), so reflected sunlight is scattered and absorbed on its way up, and takes the heating rate from the divergence of F_dir + F_diff_down - F_diff_up. With omega = 0 the absorbed surface flux still reduces exactly to (1 - albedo) times Beer-Lambert. The theory doc and the SW_Scattering_Cloud Python replica describe/implement the same scheme. Diagnostics: the columns F_up_surface and F_down_toa held the net LW at the surface and a constant zero. They are renamed LW_net_surface and LW_up_TOA (now the outgoing longwave), and SW_up_TOA (reflected SW leaving the top) is added, giving step,time,call_site,SW_surface,SW_TOA,SW_up_TOA,LW_net_surface,LW_up_TOA,heating_rate_max The column sweep returns the two new quantities. All checkers, READMEs and reference .dat files are updated; the LW_Isothermal checker verifies LW_net_surface = 0 and LW_up_TOA = sigma T^4. Unit tests cover the layer solution (non-scattering, conservative and partly absorbing layers) and the column energy budget with surface reflection, including a conservative scattering column. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Two-stream SW adding method with surface albedo; honest diagnostics columns
…meter struct The RhoTheta source term adds rho * qheating to the rho-theta equation, so qheating_rates must hold dtheta/dt; the RRTMGP interface divides its dT/dt by the Exner function before storing, but the two-stream sweep stored the raw dT/dt, leaving its heating too weak by pi (about 3% at 1 km, 25% at 10 km). The sweep now multiplies the SW and LW flux- divergence tendencies by 1/pi from the equation of state before writing them, matching the RRTMGP convention. A unit test derives the top-layer dT/dt analytically and checks the stored value is dT/dt divided by pi. RadChoice carries std::string members and was captured by value in the driver's device reduction lambda and passed into the column kernels, which CUDA/HIP reject. TwoStreamParams is the trivially copyable subset the kernels need (static_assert-guarded); the driver builds it once per call with make_two_stream_params() and the column helpers and sweep take it instead of RadChoice. Reference diagnostics are regenerated (heating_rate_max changes by the Exner factor). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Two-stream: store theta tendencies (Exner) and use a device-safe parameter struct
…etire isothermal_test lw_mass_absorption_enable computes the clear-sky LW optical depth of a layer from its mass path, rho dz (k_dry + k_vapor qv + k_cloud qc), instead of the fixed tau_lw_per_layer. The column optical depth becomes independent of the vertical resolution, water vapor produces a greenhouse response, and the cloud term is the Stephens (1978) emissivity 1 - exp(-0.158 LWP[g/m^2]). Off by default. surface_albedo_sw_diffuse gives the surface a separate albedo for the diffuse stream (A_0 of the adding method; the direct-beam albedo still starts S_0). earth_sun_distance_enable scales S0 by Spencer's (1971) (d0/d)^2 for day_of_year. The isothermal_test / T_iso_K override, which forced F_up = F_down and zero heating instead of computing them, is removed from the inputs, the parameter struct, the sweep and the driver. LW_Isothermal is now a physically isothermal column: theta(z) = 300 exp(g z / (c_p 300)) gives T = 300 K everywhere over a black surface at 300 K, and the checker verifies LW_up_TOA = sigma T^4 (to 1e-5 in practice), a negligible surface net LW and a non-zero cooling rate. The benchmark-suite case directories were symlinks to the canonical cases; they are now plain copies so the tree carries no symlinks. Unit tests cover the resolution independence of the mass-based LW, the mass-path formula and Stephens emissivity, the diffuse albedo, and the Earth-Sun factor. Reference diagnostics are regenerated. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Two-stream: mass-based gray LW, diffuse albedo, Earth-Sun distance; retire isothermal_test
…rd compatible The default per_layer model assigns tau_per_layer / tau_lw_per_layer to every layer regardless of thickness, so the column optical depth scales with the number of vertical cells. The new opt-in tau_model = mass builds each layer's optical properties from its mass path rho dz: - Shortwave: dry absorption (sw_kabs_dry), Rayleigh scattering (sw_kscat_dry, omega = 1, g = 0), water vapor (sw_kabs_vapor) and cloud water (sw_kext_cloud with sw_cloud_omega / sw_cloud_g), mixed by extinction weighting into the layer tau, omega and g; the prescribed cloud band, moisture coefficients and aerosol are added on top. - Longwave: the mass path rho dz (lw_kabs_dry + lw_kabs_vapor qv + lw_kabs_cloud qc), also selected by tau_model = mass. diagnose_layer_optics() returns (tau, omega, g) per layer and reduces to the previous diagnose_layer_tau() + select_scattering_props() pair in the per-layer model, which is unchanged bit for bit. New canonical case TwoStream_MassOpticalDepth runs the same moist column on 32 and 64 layers and checks that the surface and top-of-atmosphere fluxes agree (they do to 3e-6). Unit tests cover the resolution independence, the extinction-weighted layer optics, a Rayleigh-only column, cloud-water brightening and the per-layer path. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Two-stream: mass-based optical depth model (tau_model = mass), backward compatible
…ep's box handling CI failed on every platform. Four build breaks, plus a correctness bug in how the sweep chooses its vertical extent that the regression test was configured to hide. Build breaks - M_PI is not declared by <cmath> on MSVC. Use ERF's own PI from ERF_Constants.H, which is amrex::Real rather than double. - amrex::min/max and std::min/max were called with a double literal and an amrex::Real. Template deduction fails whenever amrex::Real is float, so every SINGLE-precision job broke (GCC, Clang, HIP, SYCL), as did MSVC in both precisions. Five call sites. - nvcc rejected the AMREX_GPU_DEVICE-only helpers in ERF_SolarGeometry.H and ERF_AerosolOpticalDepth.H, which are called from the host driver and from AMREX_GPU_HOST_DEVICE helpers. Those headers, and the equivalents in ERF_SimplifiedSEB.H and ERF_TwoStreamColumn.H, are now AMREX_GPU_HOST_DEVICE, matching ERF_TwoStreamSW.H and ERF_TwoStreamLW.H. - Trailing whitespace in 18 files. Column sweep vertical_two_stream_sweep() took its surface and top-of-atmosphere indices from mfi.tilebox(). AMReX's default CPU tile size splits z, so each 8-cell tile restarted the sweep at its own bottom as if that were the ground: surface shortwave heating came out 35% high. All three MFIter loops in the driver are now untiled and use validbox(). That also stops the in-place force-restore surface update from advancing a column once per z tile. A box that does not span the domain in z gives this solver a truncated atmosphere, so it now aborts naming amr.max_grid_size_z instead of returning heating rates that look plausible and are wrong. Inputs and tests - 30 input files set amrex.max_grid_size_z, which ParmParse ignores. The option is amr.max_grid_size_z. - TwoStream_ColumnHeating no longer disables MFIter tiling, so it covers the sweep bug above. - RunTwoStreamRadiation.cmake echoes the simulation and checker logs when a step fails, and runs the executables directly in a build without MPI instead of invoking an empty launcher. - TwoStreamRadiationCheck names the cell holding a non-finite value, and no longer lets AMReX parse the plotfile directory as an inputs file. Verified: erf_unit_tests (485), ctest TwoStream regressions, and all 30 canonical radiation cases pass; results are unchanged on 1, 2 and 4 MPI ranks and with tiling on or off. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…red inputs
Review feedback on the comments, plus an audit for configurations that fail
without saying anything.
Comments and documentation
The comments carried internal development-phase numbering ("Phase 12",
"Step 4") that means nothing outside the branch it was written on. All of it
is gone from the radiation code, its canonical tests and its docs, along with
14 phase-numbered files and directories, three internal verification
documents, and some commented-out code. Only the pre-existing phase
references in ERF_ComputeDiffusivityYSUNew.cpp are left alone; they came from
upstream.
The specific inconsistency raised in review was a comment that referred to
both solverChoice.rad_type and solverChoice.radChoice.rad_type without saying
they are different things. They are: erf.radiation_model sets
SolverChoice::rad_type (a RadiationType) and selects RRTMGP, while
erf.radiation_type sets RadChoice::rad_type (a RadType) and selects
two-stream. Every comment that mentions them now says which input drives
which. ERF_RadiationDiagnostics.H also documented a "[RAD][Phase1]" log tag
that the code stopped emitting; the doc now matches what is printed.
Configurations that failed silently
Seven ways to misconfigure radiation produced no message at all:
- erf.radiation_type = RRTMGP parsed, then ran with no radiation, because
nothing reads that enum value.
- Setting erf.radiation_model and erf.radiation_type together silently
discarded the two-stream configuration: advance_radiation() takes the
RRTMGP branch first.
- A misspelled tau_model, tau_profile_type, aerosol_profile_type or
diag_callsite_mode fell through to the default, so a typo quietly changed
the physics or the diagnostics.
- aerosol_profile_type = table was accepted but contributes nothing, which
is indistinguishable from a typo.
- seb_prognostic_enable without seb_enable did nothing.
Each is now an abort naming the input and the valid values. The existing
seb_diagnostic_enable warning is the model for these.
One input file also set seb_grdflux_default where the code reads
seb_grdflx_default, so the SEB diagnostic baseline silently ran with a
different ground heat flux than the case it is compared against.
Test checkers
check_progcf.py and check_aero.py verified nothing. Both split a
comma-separated diagnostics file on whitespace, so every row became a single
token that float() rejected and "except ValueError: pass" swallowed, and both
returned success when the file was missing entirely. They now parse by column
name, fail on a missing or empty file, require the expected columns, and
check that the shortwave fluxes are physically ordered and the column heats.
Both were confirmed to fail on an injected NaN and on a missing file.
TwoStream_ProgCloudFraction ran with cloud_fraction_prog_enable = false while
its README said the feature was enabled, so the feature had no coverage. It
is enabled now, which moves surface shortwave from 160 to 319 W/m^2. The
README also promised temporal smoothing, whose inputs were removed earlier.
TwoStream_DynamicTau_MoistCloud is left feature-off: it is documented as the
backward-compatibility baseline.
Verified: erf_unit_tests (485), ctest TwoStream (37), and all 17 canonical
radiation cases with all 17 Python checkers pass. The new aborts were each
confirmed to fire with the intended message, and no existing case trips one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… config Drop the cross-solver check that was added to SolverChoice::init_params. It aborted when erf.radiation_model and erf.radiation_type both selected a solver, which is a gate on the RRTMGP path, and this work is meant to stay within the two-stream model. Setting both again resolves the way it always has: advance_radiation() takes the RRTMGP branch and the two-stream configuration is ignored. ERF_DataStruct.H is now unchanged by this branch. Every remaining validation sits in RadChoice::init_params, the two-stream configuration struct, and no RRTMGP source file is touched. Verified: erf_unit_tests (485), ctest TwoStream (37), and all 17 canonical radiation cases with their Python checkers pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The input validation added in the previous commit aborted. Existing inputs may still carry some of these settings, so nothing aborts any more: each case prints a message and the run continues with a stated fallback. - An unrecognized tau_model, tau_profile_type, aerosol_profile_type or diag_callsite_mode falls back to its documented default, and the warning names the input, the valid values and the fallback actually taken. - aerosol_profile_type = table keeps that selection rather than quietly substituting a different profile, and the warning says it contributes no aerosol optical depth. - erf.radiation_type = RRTMGP warns that the value is not wired to a solver, so no radiation is computed, and points at erf.radiation_model. Every message prints once, on the IO processor, so a multi-rank run does not repeat it. A correctly configured run prints none of them. The seb_prognostic_enable check from the previous commit is removed: it was dead code. init_params already auto-enables seb_enable and seb_diagnostic_enable when the prognostic mode is requested, above the point where the check sat, so its condition could never be true. That auto-enable now reports itself instead, since it switches on two inputs the user did not set. Verified against the executable: all seven paths print their message and exit 0, and a clean run prints nothing. erf_unit_tests (485), ctest TwoStream (37) and all 17 canonical radiation cases with their Python checkers pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Neither radiation path had a single BL_PROFILE region, so there was no way to
separate radiation cost from the dycore for either solver. Adds one at each
entry point and a canonical case that uses them to measure what a radiation
update costs and how that cost scales.
The case is a measurement, not a ranking. RRTMGP resolves 224 shortwave and
256 longwave g-points with gas optics from lookup tables; the two-stream
solver does one gray sweep per column. The gap is the price of spectral
detail, and the useful output is a cost model for choosing between them.
Measured on one CPU core, marginal cost per call:
grid cells two-stream RRTMGP
42x4x42 7056 1.08 ms 433 ms
84x4x42 14112 2.07 ms 881 ms
84x4x84 28224 4.27 ms 1812 ms
168x4x84 56448 8.55 ms 3581 ms
Cost per cell is flat for both across a factor of eight in problem size, so
either solver extrapolates predictably to a production grid.
Keeping the comparison honest
Both inputs pull in shared_settings through AMReX's FILE include, so the
shared configuration is the same bytes for both runs rather than two lists
that happen to agree. It pins the grid, timestep, sounding, moisture model and
surface temperature, calls radiation every slow step so the comparison is per
call rather than per schedule, and switches off plotfiles and both diagnostic
logs so file I/O stays outside the measured region.
The reported figure is the marginal cost, (T_long - T_short) divided by the
difference in call counts, measured at two step counts. That cancels one-time
work, which matters here: RRTMGP reads about 45 MB of lookup tables on its
first call, worth roughly 150 ms, and a naive average would have charged that
to its per-call cost. The two-stream solver is also called twice per step
against RRTMGP's once, so normalising by the profiler's own call count rather
than by step count keeps that from skewing the result.
check_timing_fairness.py enforces those invariants and fails if either input
shadows a shared setting, strays outside its solver's namespace, drops the
include, changes the surface temperature on one side only, stops calling
radiation every step, or re-enables plotfiles. It needs no RRTMGP build.
Caveats, stated in the README and on the figure: single CPU core, RRTMGP
targets GPUs, and the two solvers do not compute the same thing, so cost per
call is not cost per unit of accuracy.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CUDA
vertical_two_stream_sweep() read its uniform cell size from
amrex::Geometry::CellSize(), which is host-only, so nvcc rejected the call
once the sweep became AMREX_GPU_HOST_DEVICE:
error: calling a __host__ function from a __host__ __device__ function
is not allowed
The Geometry was only ever used for that one value, so it is gone from the
device signature and the cell size is passed in instead, read on the host at
the call site. That also stops a Geometry being copied into every kernel
launch. Three call sites updated, including the column unit test.
Warnings
The CI Report step exits with the number of warnings, so any warning fails the
job. Two came from this PR, both -Wshadow on HIP:
- the surface-energy-balance kernel declared locals t_deep and q_deep, which
shadow the ERF members of the same names; renamed to t_deep_val, q_deep_val;
- the surface-property allocation declared IntVect ng, shadowing an outer ng
in init_stuff(); renamed to ng_sfc.
One did not come from this PR but fails GCC, ubuntu, HIP and SYCL on its own:
an unused HOL_abs in ERF_ComputeDiffusivityMRF.cpp. Two other sites in that
file compute HOL_abs and use it as pow(1 + 16*HOL_abs, -1/4) for the unstable
arm, and the comment above this third site describes that idiom, but the site
itself writes pow(max(1 - 16*HOL_bounded, 0.01), -1/4) and never reads the
variable. The two forms agree where it matters: the branch is only reached
when obuk_val <= 0, so HOL_bounded <= 0, the max never binds, and the base
equals 1 + 16*|HOL|. The unused variable and its stale comment are removed and
the arithmetic is untouched.
That third site being written differently from its two siblings looks
unintentional and is worth a look by the author of the change that added it,
but it is not this PR's to decide.
Leftover internal markers
Seven "Phase N" labels survived the earlier comment cleanup, in canonical test
banners, two inputs files and one README. They are internal scheduling labels
with no meaning to a reader of the repository, so they are dropped. Only the
labels changed; no test logic, no input values.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TwoStream_SEB_Diagnostic is a pair: the feature case turns the SEB diagnostic
on, the baseline leaves it at its default off, and check_seb_diagnostic.py
compares the two diagnostic files to confirm the baseline is unchanged by the
feature. The pair only means something if both halves cover the same interval,
and they did not:
inputs_seb_diagnostic_enabled stop_time = 36.0 72 steps
inputs_seb_diagnostic_disabled stop_time = 3600.0 7200 steps
So the comparison could only ever reach the first 72 steps, and the baseline's
remaining 7128 steps were work nothing read - about 4000 s of a core, which is
most of the cost of running the canonical radiation cases at all.
The baseline is set to 36.0 to match, and a comment above it says the two have
to stay equal. Nothing else about either case changes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a two-stream atmospheric radiation model as an alternative to RRTMGP, together with an optional simplified surface energy balance (SEB) module that evolves surface temperature and moisture by a force-restore method where Noah-MP is not driving them. The model is selected with
erf.radiation_type = "TwoStream"and is independent of the RRTMGP path, which is still selected witherf.radiation_model.Everything new is off by default, so a run that does not ask for two-stream radiation is unaffected.
Shortwave
F_dir(z) = S0 cos(zenith) exp(-tau_cum(z) / cos(zenith)).surface_albedo_sw_diffuse).S0by day of year.Longwave
Optical depth
Two models, selected by
erf.radiation.tau_model:per_layer(default): a fixed optical depth per layer, so the column value scales with the number of vertical cells.mass: built from each layer's mass pathrho dz, so the column optical depth is set by the atmosphere rather than by the grid. Shortwave mixes dry absorption, Rayleigh scattering, water vapour and cloud water with extinction-weighted single-scattering albedo and asymmetry factor. Longwave uses the corresponding mass absorption coefficients, with the cloud term expressed as the Stephens (1978) emissivity.On top of either model: a prescribed cloud band, moisture- and cloud-aware dynamic optical depth diagnosed from
qvandqc, prognostic cloud fraction diagnosed from relative humidity and cloud water, and a prescribed bulk aerosol optical depth that is constant or exponential with height.Coupling
dtheta/dt = (dT/dt) / pi, matching the convention the RhoTheta source term and the RRTMGP path already use. They are written toqheating_ratesand exposed as theqsrc_swandqsrc_lwplot variables.Simplified surface energy balance
R_net - H - LE - G.Grid requirement
The column sweep integrates a whole atmospheric column in one pass, so each grid must span the domain in the vertical. ERF only decomposes in
zwhenamr.max_grid_size_zis smaller than the number of cells inz; setting it at least that large is sufficient. A vertically decomposed grid aborts with a message naming the input rather than returning heating rates that look plausible and are wrong. Results do not depend on the horizontal decomposition or on the MFIter tile size.Input validation
Several misconfigurations used to be accepted in silence. None of them aborts, so existing inputs keep running, but each now prints a warning naming the input, the valid values, and what the run will actually do instead:
tau_model,tau_profile_type,aerosol_profile_typeordiag_callsite_modefalls back to its default, which is stated in the warning;aerosol_profile_type = tableis not implemented and contributes no aerosol optical depth;seb_prognostic_enablereports that it also switches onseb_enableandseb_diagnostic_enable, which it depends on;erf.radiation_type = RRTMGPis not wired to a solver, so no radiation is computed. RRTMGP is selected witherf.radiation_model.Cost and scaling
Exec/CanonicalTests/Radiation/TwoStream_vs_RRTMGP_Timing/measures what each solver costs per radiation call and how that cost scales, with everything except the solver held fixed.Measured on one CPU core, marginal cost per call:
The two solvers compute different things: RRTMGP resolves 224 shortwave and 256 longwave g-points with gas optics from lookup tables, while the two-stream solver does a single gray sweep per column. The difference is the cost of spectral detail, so this is a guide to choosing a solver for a given run rather than a ranking. Cost per cell is flat for both across a factor of eight in problem size, which means either one extrapolates predictably to a production grid.
Both radiation paths previously had no
BL_PROFILEregion, so radiation cost could not be separated from the dycore for either solver. One is now added at each entry point, and the measurement reads exclusive time from AMReX's TinyProfiler.Keeping it fair: both inputs pull in one shared settings block through AMReX's
FILEinclude, pinning the grid, timestep, sounding, moisture model and surface temperature; radiation is called every slow step on both sides; plotfiles and both diagnostic logs are off so file I/O stays outside the measured region. The reported figure is the marginal cost across two step counts, which cancels one-time work, notably the roughly 150 ms RRTMGP spends reading its lookup tables on the first call.check_timing_fairness.pyenforces those invariants and needs no RRTMGP build.Caveats: a single CPU core, RRTMGP targets GPUs, and cost per call is not cost per unit of accuracy.
Testing
qsrc_swandqsrc_lware written.Exec/CanonicalTests/Radiation/, each with a Python checker, covering clear-sky and cloudy shortwave, scattering, longwave, non-uniform spacing, dynamic optical depth, prognostic cloud fraction, aerosol, diurnal solar geometry, surface heterogeneity, the mass optical-depth model, RhoTheta and YSUNew coupling, and all three SEB modes.Documentation
Docs/sphinx_doc/theory/Radiation.rstcovers the shortwave and longwave formulations, the optical-depth models, the potential-temperature convention, the SEB equations and the grid requirement.Docs/sphinx_doc/Inputs.rstdocuments every input.Exec/CanonicalTests/Radiation/inputs_twostream_masteris the reference input file and lists all 79erf.radiation.*parameters with defaults, units and descriptions.