ERF
Energy Research and Forecasting: An Atmospheric Modeling Code
TwoStreamRadiation Class Reference

The two-stream radiation model (erf.radiation_model = TwoStream). More...

#include <ERF_TwoStreamRadiation.H>

Collaboration diagram for TwoStreamRadiation:

Classes

struct  FluxDiag
 
struct  OrbitalCache
 

Public Member Functions

 TwoStreamRadiation ()=default
 
void resize (int nlevs_max)
 Size the per-level containers; no allocation happens here. More...
 
bool active () const
 True once a level has been defined with the model selected. More...
 
const amrex::MultiFab * prognostic_surface_temperature_state (int lev) const noexcept
 Read-only view of the per-level prognostic surface-temperature state. More...
 
void define_level (int lev, const RadChoice &rad_choice, amrex::Real rdOcp, const amrex::BoxArray &ba2d, const amrex::DistributionMapping &dm, const amrex::BoxArray &ba, const amrex::Box &domain, bool supply_land_forcing=false)
 Allocate the 2D surface fields of one level on the given horizontal BoxArray and fill them with the scalar defaults. A no-op unless rad_choice.rad_type == TwoStream. More...
 
void advance (int lev, int nstep, amrex::Real time, amrex::Real dt_step, const std::string &call_site, const amrex::MultiFab &cons_old, const amrex::MultiFab *z_phys_nd, const amrex::Geometry &geom, LandSurface &lsm, const amrex::Vector< const amrex::MultiFab * > &radiation_inputs, bool noahmp_active, amrex::MultiFab *qheating, amrex::MultiFab *rad_fluxes, const amrex::MultiFab *t_surf, const amrex::MultiFab *sfc_sens_flux, const amrex::MultiFab *sfc_laten_flux, const amrex::MultiFab *lat_m, const amrex::MultiFab *lon_m, double epoch_time, bool have_datetime)
 One radiation call at one level. More...
 
amrex::MultiFab * seb_t_sfc (int lev)
 The prognostic surface state of one level, or nullptr when the SEB is off or the level has not been built. More...
 
amrex::MultiFab * seb_q_sfc (int lev)
 
const amrex::MultiFab * seb_hfx (int lev) const
 The sensible and latent heat fluxes [W/m^2] the surface energy balance of one level last used, or nullptr when the SEB is off or the level has not been built. Read-only: for output. More...
 
const amrex::MultiFab * seb_lh (int lev) const
 
const amrex::MultiFab * seb_surface_moisture (int lev)
 What level lev's surface hands the surface layer for its surface mixing ratio (erf.radiation.seb_surface_layer_uses_moisture), per column, in the components of erf_surface_moisture::Component: the soil-water factor (seb_moisture_availability of the current q_s); with erf.radiation.seb_soil_type, the bare-soil resistance and the relative humidity of its pore air at the skin temperature; and with erf.radiation.seb_vegetation_type as well, the vegetated fraction and the canopy resistance without its vapour-deficit factor (from the net shortwave the balance holds divided by (1 - surface albedo) – the sweep's with seb_use_radiation_fluxes, seb_sw_flux_default otherwise – and the skin temperature). Absent pieces are a fraction of 0 and resistances of 1e6 s/m. nullptr when the option is off or the level has not been built. Recomputed on every call, on the valid cells; the halo is not filled. More...
 
bool seb_prognostic_active () const
 True when the force-restore state is being evolved and so needs transferring. More...
 
bool supplies_land_forcing (int lev) const noexcept
 True when define_level allocated the land-model forcing fields on lev. More...
 
const amrex::MultiFab * land_forcing_sw_dn (int lev) const noexcept
 The surface forcing of the last sweep on lev (2D: i,j), or nullptr when the level does not supply it: total downwelling shortwave [W/m^2], downwelling longwave [W/m^2] and the cosine of the solar zenith angle floored at zero [1]. lsm_undefined until a sweep has run on the level, so a copy made before one is caught by the land model's missing-input check instead of reading as zero. More...
 
const amrex::MultiFab * land_forcing_lw_dn (int lev) const noexcept
 
const amrex::MultiFab * land_forcing_cos_zenith (int lev) const noexcept
 
void write_land_forcing (int lev, amrex::MultiFab *sw_flux_dn, amrex::MultiFab *lw_flux_dn, amrex::MultiFab *cos_zenith_angle) const
 Copy the surface forcing of the last sweep on lev into a land-surface model's radiation inputs (SurfaceModel's "sw_flux_dn", "lw_flux_dn" and "cos_zenith_angle" outputs). A null destination is skipped. Each destination must lie on this level's horizontal grids; see copy_surface_plane. Before the level's first sweep this copies the lsm_undefined sentinel, not zero. More...
 
void supply_canopy_forcing (int lev, int m_top)
 Have every later sweep on lev write what the building faces need besides the interface fluxes (erf.ibseb.radiation = two_stream; ERF_TwoStreamCanopyForcing.H): the direct beam on a horizontal surface at the interfaces 0 .. m_top of each column, counted from the surface, and the cosine of the zenith. Allocates one field on the level's horizontal grids given those interfaces in z and one 2D field. Call after define_level(); a later define_level() of the level (a regrid) drops the request. More...
 
bool supplies_canopy_forcing (int lev) const noexcept
 True when supply_canopy_forcing() was called for lev since its last define_level(). More...
 
TwoStreamCanopyView canopy_forcing (int lev, int nstep) const noexcept
 The beam and the cosine of the zenith of lev as the sweep of step nstep wrote them (the view's fluxes are left for the caller, who owns rad_fluxes), or an empty view when the level does not supply them or has not swept at that step: the faces must not take a previous step's radiation, nor the zeros of a level whose radiation came from its parent. More...
 
void write_checkpoint (int lev, const std::string &checkpointname) const
 Write the prognostic surface state of one level (SEB on). More...
 
void read_checkpoint (int lev, const std::string &restart_chkfile)
 Read it back when the checkpoint carries it; else keep defaults. More...
 

Private Attributes

const RadChoice * m_rad = nullptr
 
amrex::Real m_rdOcp = RdoCp
 
OrbitalCache m_orbit
 
amrex::Vector< std::unique_ptr< RadiationDiagnostics > > m_diag
 
amrex::Vector< std::unique_ptr< amrex::MultiFab > > m_alb_sw
 
amrex::Vector< std::unique_ptr< amrex::MultiFab > > m_emiss_lw
 
amrex::Vector< std::unique_ptr< amrex::MultiFab > > m_t_sfc
 
amrex::Vector< std::unique_ptr< amrex::MultiFab > > m_sw_flux_sfc
 
amrex::Vector< std::unique_ptr< amrex::MultiFab > > m_lw_flux_sfc
 
amrex::Vector< std::unique_ptr< amrex::MultiFab > > m_hfx_sfc
 
amrex::Vector< std::unique_ptr< amrex::MultiFab > > m_lh_sfc
 
amrex::Vector< std::unique_ptr< amrex::MultiFab > > m_grdflx_sfc
 
amrex::Vector< std::unique_ptr< amrex::MultiFab > > m_q_sfc
 
amrex::Vector< std::unique_ptr< amrex::MultiFab > > m_t_deep
 
amrex::Vector< std::unique_ptr< amrex::MultiFab > > m_q_deep
 
amrex::Vector< std::unique_ptr< amrex::MultiFab > > m_moisture
 
amrex::Vector< std::unique_ptr< amrex::MultiFab > > m_sw_dn_sfc
 
amrex::Vector< std::unique_ptr< amrex::MultiFab > > m_lw_dn_sfc
 
amrex::Vector< std::unique_ptr< amrex::MultiFab > > m_cos_zenith
 
amrex::Vector< std::unique_ptr< amrex::MultiFab > > m_canopy
 
amrex::Vector< std::unique_ptr< amrex::MultiFab > > m_canopy_cosz
 
amrex::Vector< int > m_canopy_m
 
amrex::Vector< int > m_canopy_step
 
amrex::Vector< FluxDiag > m_flux_diag
 

Detailed Description

The two-stream radiation model (erf.radiation_model = TwoStream).

Owns everything the model keeps between steps: the per-level surface property fields it falls back on when no land-surface model supplies them, the surface-energy-balance (SEB) fields and force-restore state, and the domain-mean flux diagnostics of the last column sweep. ERF holds one instance and calls it only after testing solverChoice.rad_type == RadiationType::TwoStream; every method here checks the same predicate and returns without acting otherwise.

Lifecycle:

  • resize(nlevs) once at construction (before the inputs are read);
  • define_level(lev, radChoice, rdOcp, ba2d, dm, ba, domain, supply_land_forcing) whenever a level is (re)built;
  • advance(...) twice per step: "pre_dycore" runs the column sweep on the old state and writes the heating rates, "post_dycore" reuses that sweep's flux diagnostics and advances the SEB state by dt_step;
  • write_land_forcing(...) after a sweep, when a land-surface model integrates on the radiation this model computes (see supply_land_forcing);
  • supply_canopy_forcing(...) once, when the building faces of the immersed-boundary balance take their radiation from the sweep (erf.ibseb.radiation = two_stream), and canopy_forcing(...) after each sweep;
  • write_checkpoint / read_checkpoint for the prognostic surface state.

The heating-rate MultiFab itself stays in ERF, since RRTMGP writes the same 2-component (SW, LW) array and the RhoTheta source term and the plotfile read it without caring which model filled it.

Constructor & Destructor Documentation

◆ TwoStreamRadiation()

TwoStreamRadiation::TwoStreamRadiation ( )
default

Member Function Documentation

◆ active()

bool TwoStreamRadiation::active ( ) const
inline

True once a level has been defined with the model selected.

57  {
58  return (m_rad != nullptr) && m_rad->enabled;
59  }
const RadChoice * m_rad
Definition: ERF_TwoStreamRadiation.H:320
bool enabled
True when erf.radiation_model = TwoStream. Set by init_params from the model SolverChoice read; the e...
Definition: ERF_RadStruct.H:92

Referenced by seb_hfx(), seb_lh(), seb_prognostic_active(), seb_q_sfc(), and seb_t_sfc().

Here is the caller graph for this function:

◆ advance()

void TwoStreamRadiation::advance ( int  lev,
int  nstep,
amrex::Real  time,
amrex::Real  dt_step,
const std::string &  call_site,
const amrex::MultiFab &  cons_old,
const amrex::MultiFab *  z_phys_nd,
const amrex::Geometry &  geom,
LandSurface &  lsm,
const amrex::Vector< const amrex::MultiFab * > &  radiation_inputs,
bool  noahmp_active,
amrex::MultiFab *  qheating,
amrex::MultiFab *  rad_fluxes,
const amrex::MultiFab *  t_surf,
const amrex::MultiFab *  sfc_sens_flux,
const amrex::MultiFab *  sfc_laten_flux,
const amrex::MultiFab *  lat_m,
const amrex::MultiFab *  lon_m,
double  epoch_time,
bool  have_datetime 
)

One radiation call at one level.

Parameters
levlevel
nstepstep number, used as the diagnostics row index
timetime logged with the diagnostics row [s]
dt_stepstep size, used by the SEB Euler update [s]
call_site"pre_dycore" (column sweep + diagnostics) or "post_dycore" (cached diagnostics + SEB update)
cons_oldold-state conserved variables of this level
z_phys_ndnodal heights (layer interfaces), or nullptr on a uniform grid
geomgeometry of this level
lsmland-surface model (queried for per-column fields)
radiation_inputscanonical surface-property inputs, in fixed order
noahmp_activewhether Noah-MP diagnostics may be queried
qheating2-component (SW, LW) heating-rate MultiFab of this level, written at the pre-dycore call; may be nullptr
rad_fluxes4-component (SW up, SW down, LW up, LW down) interface fluxes of this level in RRTMGP's level layout (index k the lower interface of layer k, the top of the atmosphere in the z-ghost cell above the top layer), written at the pre-dycore call; may be nullptr
t_surfthe surface layer's surface temperature, or nullptr; used as the longwave boundary condition where no land-surface field exists, as RRTMGP does
sfc_sens_fluxthe conservative heat flux (rho w'theta', z faces) the zlo surface layer applies to this level, or nullptr when it applies none; the surface energy balance's H where no land-surface model supplies one (see ERF_SEBTurbulentFlux.H)
sfc_laten_fluxthe moisture flux (rho w'qv', z faces) likewise, for LE; nullptr without a moisture model
lat_mper-column latitude [degrees], or nullptr (then erf.rad_cons_lat); only the WRF/metgrid inits fill it
lon_mper-column longitude [degrees], likewise
epoch_timeseconds since 1970-01-01 00:00 UTC of this call (start_time + time), the RRTMGP time base; double, as RRTMGP keeps it, since a float cannot resolve a time of day at ~1.7e9 s
have_datetimetrue when start_datetime (or a WRF/metgrid start date) fixed the calendar; without it a dynamic sun cannot be placed and the call aborts
556 {
557  BL_PROFILE("TwoStreamRadiation::advance()");
558 
559  // Only proceed if TwoStream radiation is enabled
560  if (!active()) { return; }
561  const RadChoice& rad_choice = *m_rad;
562 
563  // A level on which no box holds a whole column has nothing for the sweep or the surface
564  // state to work on; ERF::advance_radiation interpolates its fields from the parent
565  // instead. Returning here covers the post-dycore call too, which comes from ERF::Advance
566  // and does not test for it. Per box, and identical to level_needs_interpolation() there:
567  // if the two tests disagreed, one of them would try to sweep a level the other had
568  // already interpolated.
569  if (lev > 0) {
570  const BoxArray& lev_ba = cons_old.boxArray();
571  const Box& dom = geom.Domain();
572  for (int ibox = 0; ibox < lev_ba.size(); ++ibox) {
573  const Box& b = lev_ba[ibox];
574  if (b.smallEnd(2) != dom.smallEnd(2) || b.bigEnd(2) != dom.bigEnd(2)) { return; }
575  }
576  }
577 
578  // ---- Contract checks. Each of these would otherwise surface as a wrong
579  // heating rate or an out-of-bounds read several routines away.
580  AMREX_ALWAYS_ASSERT_WITH_MESSAGE(call_site == "pre_dycore" || call_site == "post_dycore",
581  "TwoStreamRadiation::advance: call_site must be pre_dycore or post_dycore");
582  AMREX_ALWAYS_ASSERT_WITH_MESSAGE(lev < m_alb_sw.size() && m_alb_sw[lev] != nullptr,
583  "TwoStreamRadiation::advance called on a level define_level has not built");
584  // The sweep indexes the 2D surface fields with the MFIter of the 3D state,
585  // so both must have the same number of boxes on the same ranks.
587  m_alb_sw[lev]->boxArray().size() == cons_old.boxArray().size() &&
588  m_alb_sw[lev]->DistributionMap() == cons_old.DistributionMap(),
589  "TwoStreamRadiation: the 2D surface fields and the 3D state are laid out differently");
590  if (qheating != nullptr) {
591  AMREX_ALWAYS_ASSERT_WITH_MESSAGE(qheating->nComp() == 2 &&
592  qheating->boxArray() == cons_old.boxArray() &&
593  qheating->DistributionMap() == cons_old.DistributionMap(),
594  "TwoStreamRadiation: qheating must be the 2-component (SW, LW) field on the state's grids");
595  }
596  if (rad_choice.seb_enable) {
598  m_lh_sfc[lev] && m_grdflx_sfc[lev] && m_q_sfc[lev] &&
599  m_t_deep[lev] && m_q_deep[lev],
600  "TwoStreamRadiation: seb_enable is set but the SEB fields were not allocated");
601  }
602  // The land-model forcing is read off the interface fluxes the sweep writes. Reached
603  // only on a level that will sweep: the per-box interpolation return above comes first,
604  // and a level whose radiation comes from its parent has no sweep and no rad_fluxes to
605  // read here. Keep that order -- TwoStreamRadiationDriver.LandForcingIsUndefinedUntilASweep
606  // advances such a level and expects it to return untouched.
607  AMREX_ALWAYS_ASSERT_WITH_MESSAGE(!supplies_land_forcing(lev) || rad_fluxes != nullptr ||
608  call_site == "post_dycore",
609  "TwoStreamRadiation: supplying a land-surface model's forcing needs the rad_fluxes array");
610  if (call_site == "post_dycore" && rad_choice.seb_prognostic_enable) {
611  AMREX_ALWAYS_ASSERT_WITH_MESSAGE(dt_step > 0.0 && std::isfinite(dt_step),
612  "TwoStreamRadiation: the force-restore update needs a positive, finite dt_step");
613  }
614 
615  // The surface energy balance runs on every level that sweeps, each evolving its
616  // own force-restore state.
617  //
618  // These fields are per level (see define_level), so two levels covering the same
619  // column are not advancing one shared state twice -- they are forming two
620  // independent estimates of one surface. Left alone those estimates drift apart,
621  // which would give the longwave boundary condition two different values for the
622  // same ground. ERF averages t_sfc and q_sfc down after the finer levels have
623  // advanced, so a coarse cell carries the mean of its fine children and the two
624  // agree where they overlap.
625  //
626  // A level that does not sweep has no fluxes of its own and returns earlier.
627  //
628  // NOTE for anyone resolving a merge here: the development side of this line still
629  // carries `&& (lev == 0)`, the single-level restriction. Taking that side back --
630  // which a "resolve by accepting theirs" does -- silently turns this whole branch
631  // into a no-op: the surface energy balance returns to level 0 only while the tests,
632  // docs and PR all still claim otherwise. Keep this form.
633  const bool seb_active = rad_choice.seb_enable;
634  // The force-restore state is only the longwave boundary when no external
635  // surface-temperature provider owns that boundary on this level. The
636  // canonical radiation input covers SurfaceModel providers such as SLM;
637  // Noah-MP can also supply t_sfc directly when no canonical input is set.
638  // Orthogonal to the level question above: this asks WHO owns the boundary,
639  // that one asks WHETHER this level sweeps at all.
640  const bool has_external_surface_temperature =
641  (radiation_inputs.size() > 0 && radiation_inputs[0] != nullptr) ||
642  (noahmp_active && lsm_has_field(lsm, lev, "t_sfc"));
643  // The column kernel would substitute placeholders (rho = 1, rho*theta of
644  // 288 K) for a non-finite or non-positive density or rho*theta and carry
645  // on, hiding a corrupt state behind plausible heating rates. Refuse such
646  // a state here instead.
647  //
648  // This runs every step, so it is one pass over the state and one
649  // collective, not the three that contains_nan() plus two MultiFab::min()
650  // calls would cost. A non-finite value is mapped to -infinity so that the
651  // same minimum answers both questions: -infinity means non-finite, and any
652  // other value <= 0 means non-positive. The reduction is collective, so
653  // every rank takes the same branch.
654  if (call_site == "pre_dycore") {
655  ReduceOps<ReduceOpMin, ReduceOpMin> state_ops;
656  ReduceData<amrex::Real, amrex::Real> state_data(state_ops);
657  using StateTuple = typename decltype(state_data)::Type;
658  for (MFIter mfi(cons_old, TilingIfNotGPU()); mfi.isValid(); ++mfi) {
659  const auto& arr = cons_old.const_array(mfi);
660  state_ops.eval(mfi.tilebox(), state_data,
661  [=] AMREX_GPU_DEVICE (int i, int j, int k) -> StateTuple {
662  constexpr amrex::Real neg_inf = -std::numeric_limits<amrex::Real>::infinity();
663  const amrex::Real rho = arr(i,j,k,Rho_comp);
664  const amrex::Real rth = arr(i,j,k,RhoTheta_comp);
665  return {amrex::Math::isfinite(rho) ? rho : neg_inf,
666  amrex::Math::isfinite(rth) ? rth : neg_inf};
667  });
668  }
669  auto state_tuple = state_data.value(state_ops);
670  amrex::Real mins[2] = {amrex::get<0>(state_tuple), amrex::get<1>(state_tuple)};
671  ParallelDescriptor::ReduceRealMin(mins, 2);
672  const amrex::Real rho_min = mins[0];
673  const amrex::Real rth_min = mins[1];
674  if (!std::isfinite(rho_min) || !std::isfinite(rth_min)) {
675  amrex::Abort("TwoStreamRadiation: the state handed to the column sweep at level " +
676  std::to_string(lev) + ", step " + std::to_string(nstep) +
677  " has a non-finite density or rho*theta");
678  }
679  if (!(rho_min > 0.0) || !(rth_min > 0.0)) {
680  amrex::Abort("TwoStreamRadiation: the state handed to the column sweep at level " +
681  std::to_string(lev) + ", step " + std::to_string(nstep) +
682  " has a non-positive density (min " + std::to_string(rho_min) +
683  ") or rho*theta (min " + std::to_string(rth_min) + ")");
684  }
685  }
686 
687  // The column sweep runs once per step, at the pre-dycore call. The
688  // post-dycore call would sweep the same old state (vars_old is not
689  // swapped until the next step), so it reuses the cached flux diagnostics
690  // and only advances the surface-energy-balance state.
691  const bool do_sweep = (call_site != "post_dycore");
692 
693  // One diagnostics writer per level for the life of the run, so the
694  // header-written flag and the (step, call_site, time) duplicate guard carry
695  // over between calls. Per level rather than one for the run: the levels of a
696  // hierarchy report the same step, call_site and time, so a single shared
697  // guard would drop every level but the first. All of them append to the one
698  // CSV, whose `level` column tells the rows apart.
699  if (!m_diag[lev]) {
700  m_diag[lev] = std::make_unique<RadiationDiagnostics>(
701  rad_choice.verbosity, rad_choice.diag_file, lev,
702  rad_choice.diag_enable, rad_choice.diag_stdout_enable,
703  rad_choice.diag_tagged_enable, rad_choice.diag_regtest_line_enable,
704  rad_choice.diag_csv_enable, rad_choice.diag_callsite_mode,
705  rad_choice.diag_dedup_tol);
706  }
707  RadiationDiagnostics& rad_diag = *m_diag[lev];
708 
709  // ========================================
710  // GPU-Safe ParallelFor Implementation with Cloud Fraction
711  // Blending, Diffuse (Scattering) SW Flux, and Per-Level Heating Rate
712  // Output (qheating_rates MultiFab)
713  // ========================================
714 
715  // Initialize global diagnostics
716  amrex::Real SW_surface = 0.0;
717  amrex::Real SW_TOA = 0.0;
718  amrex::Real SW_up_TOA = 0.0;
719  amrex::Real LW_net_surface = 0.0;
720  amrex::Real LW_up_TOA = 0.0;
721  amrex::Real heating_rate_max = 0.0;
722  amrex::Real seb_residual_mean = std::numeric_limits<amrex::Real>::quiet_NaN();
723  amrex::Real seb_residual_max = std::numeric_limits<amrex::Real>::quiet_NaN();
724 
725  // Prognostic SEB surface temperature and moisture diagnostics
726  amrex::Real t_s_mean = std::numeric_limits<amrex::Real>::quiet_NaN();
727  amrex::Real t_s_max = std::numeric_limits<amrex::Real>::quiet_NaN();
728  amrex::Real q_s_mean = std::numeric_limits<amrex::Real>::quiet_NaN();
729  amrex::Real q_s_max = std::numeric_limits<amrex::Real>::quiet_NaN();
730 
731  // Get state at this level (conservative variables: density, RhoTheta, etc.)
732  const MultiFab& state_cons = cons_old;
733 
734  // Only compute radiation if we have valid state data
735  if (state_cons.nComp() > 0 ) {
736 
737  // Trivially copyable parameter set for the device lambdas below
738  // (RadChoice itself holds std::string members and cannot be captured),
739  // with the sun of this call from the inputs shared with RRTMGP. The
740  // incident SW at the top (SW_TOA) is a domain mean formed by the sweep,
741  // since with a calendar sun it varies across the columns.
742  TwoStreamParams ts_params = make_two_stream_params(rad_choice, m_rdOcp);
743  if (do_sweep) { set_solar_state(ts_params, rad_choice, m_orbit, epoch_time, have_datetime, lev, nstep); }
744 
745  // Host-side storage for reduction results (will be set by device-side reduction)
746  amrex::Real max_heating_global = 0.0;
747  amrex::Real sw_surface_sum = 0.0;
748  amrex::Real sw_up_toa_sum = 0.0;
749  amrex::Real lw_net_sum = 0.0;
750  amrex::Real lw_up_toa_sum = 0.0;
751  amrex::Real sw_toa_sum = 0.0;
752  amrex::Long n_columns_total = 0;
753 
754  // SEB residual diagnostics
755  amrex::Real seb_residual_sum = 0.0;
756  amrex::Long n_seb_columns = 0;
757 
758  // cloud fraction used to blend clear-sky and cloudy-column results.
759  // cloud_fraction == 0.0 (default) means only the clear-sky column is
760  // ever evaluated, and the blend below reduces to F = F_clear exactly.
761  amrex::Real cloud_fraction = rad_choice.cloud_fraction;
762 
763  // qheating is ERF's 2-component (SW, LW) heating-rate MultiFab of this
764  // level. If it is not allocated yet, the sweep still runs for the
765  // diagnostics and skips the per-level heating write.
766  MultiFab* qheating_mf = qheating;
767 
768  // Surface radiative fluxes for the SEB. Precedence: an LSM field when
769  // the LSM exposes one; otherwise, with seb_use_radiation_fluxes, the
770  // fluxes the column sweep below computes at the surface (written per
771  // column by the sweep and left in place for the post-dycore call);
772  // otherwise the scalar defaults.
773  const bool sw_flux_from_rad = seb_active &&
774  rad_choice.seb_use_radiation_fluxes &&
775  !(noahmp_active && lsm_has_field(lsm, lev, "sav"));
776  const bool lw_flux_from_rad = seb_active &&
777  rad_choice.seb_use_radiation_fluxes &&
778  !(noahmp_active && lsm_has_field(lsm, lev, "fira"));
779 
780  if (seb_active) {
781  fill_or_copy_seb_field(m_alb_sw[lev].get(), lsm, lev, lsm_broadband_albedo_name, rad_choice.surface_albedo_sw);
782  fill_or_copy_seb_field(m_emiss_lw[lev].get(), lsm, lev, "sfc_emis", rad_choice.surface_emissivity_lw);
783 
784  // In prognostic mode, do not seed this state from an LSM field.
785  // The column sweep applies external-provider precedence directly,
786  // and the post-dycore update advances this state only when
787  // TwoStream owns the surface-temperature boundary.
788  if (!rad_choice.seb_prognostic_enable) {
789  fill_or_copy_seb_field(m_t_sfc[lev].get(), lsm, lev, "t_sfc", rad_choice.rad_t_sfc);
790  }
791 
792  // Net absorbed shortwave: Noah-MP splits it into the canopy (sav)
793  // and ground (sag) parts. Net longwave: Noah-MP's fira is the
794  // net flux to the atmosphere (positive up); the SEB wants the
795  // absorbed flux, so the sign flips.
796  if (!sw_flux_from_rad) {
797  fill_or_copy_seb_field(m_sw_flux_sfc[lev].get(), lsm, lev, "sav",
798  rad_choice.seb_sw_flux_default, 1.0, "sag");
799  }
800  if (!lw_flux_from_rad) {
801  fill_or_copy_seb_field(m_lw_flux_sfc[lev].get(), lsm, lev, "fira",
802  rad_choice.seb_lw_flux_default, -1.0);
803  }
804  // H and LE: a land-surface field of that name, else the flux the
805  // surface layer applies to the air (so the ground loses what the air
806  // receives), else the scalar defaults; see ERF_SEBTurbulentFlux.H.
807  // No land model exposes "hfx" or "lh" today, so without one the
808  // surface layer is the source. G is Noah-MP's grdflx when present.
809  //
810  // At the post-dycore call, the one the force-restore update uses, the
811  // surface layer's flux is the one it applied during this step. At the
812  // pre-dycore call it is the previous step's, which only the residual
813  // diagnostic reads.
814  const int surface_k = geom.Domain().smallEnd(2);
816  lsm_field(lsm, lev, "hfx"), sfc_sens_flux, surface_k,
817  rad_choice.seb_turbulent_flux_source,
818  rad_choice.seb_hfx_default);
820  lsm_field(lsm, lev, "lh"), sfc_laten_flux, surface_k,
821  rad_choice.seb_turbulent_flux_source,
822  rad_choice.seb_lh_default);
823  fill_or_copy_seb_field(m_grdflx_sfc[lev].get(), lsm, lev, "grdflx", rad_choice.seb_grdflx_default);
824 
825  // Gate q_sfc fill on prognostic mode: same reasoning as t_sfc.
826  if (!rad_choice.seb_prognostic_enable) {
827  fill_or_copy_seb_field(m_q_sfc[lev].get(), lsm, lev, "noahmp_water_vapor_mixing_ratio_2m_vegetated", rad_choice.seb_q_sfc_default);
828  }
829  // No LSM exposes a deep-soil temperature or moisture in kg/kg by
830  // name (Noah-MP's smstav / smstot are soil-moisture availability
831  // and total column water), so the reservoir values are the
832  // scalar defaults.
833  m_t_deep[lev]->setVal(rad_choice.seb_t_deep_default);
834  m_q_deep[lev]->setVal(rad_choice.seb_q_deep_default);
835  }
836 
837  // The column sweep integrates the whole atmospheric column in one
838  // kernel, so it needs every k of a box in a single pass. MFIter tiling
839  // would hand it partial columns (the default CPU tile size splits z),
840  // so this loop is deliberately untiled and works on valid boxes; the
841  // horizontal ParallelFor below still provides the parallelism.
842  const Box& rad_domain = geom.Domain();
843  if (do_sweep) {
844  for (MFIter mfi(state_cons, false); mfi.isValid(); ++mfi)
845  {
846  const Box& bx = mfi.validbox();
847 
848  // A box that does not span the domain vertically would give this
849  // column solver a truncated atmosphere: no beam from above, no
850  // cooling to space. ERF only decomposes in z when max_grid_size_z
851  // is smaller than the domain, so refuse that configuration rather
852  // than return heating rates that look plausible and are wrong.
854  bx.smallEnd(2) == rad_domain.smallEnd(2) &&
855  bx.bigEnd(2) == rad_domain.bigEnd(2),
856  "TwoStream radiation requires grids that span the domain in z; "
857  "set amr.max_grid_size_z to at least amr.n_cell in z");
858  const auto& state_arr = state_cons.const_array(mfi);
859  // Geometry::CellSize() is host-only; read it here and hand the
860  // value to the device sweep.
861  const amrex::Real dz_uniform_lev = geom.CellSize(2);
862 
863  // Nodal heights for the layer thicknesses on a stretched or
864  // terrain-following grid (nullptr on a uniform grid).
865  Array4<const amrex::Real> z_phys_nd_arr;
866  if (z_phys_nd != nullptr) {
867  z_phys_nd_arr = z_phys_nd->const_array(mfi);
868  }
869 
870  // Per-column scratch of the sweep (interfaces need nlev + 1 entries),
871  // shared by the clear and cloudy evaluations of the same column.
872  FArrayBox scratch_fab(two_stream_scratch_box(bx), TwoStreamScratch::NCOMP);
873  Array4<amrex::Real> scratch_arr = scratch_fab.array();
874 
875  // Wire LSM surface property fields or use standalone fallback MultiFabs
876  // Priority:
877  // 1. If LSM is active (lsm.Get_DataIdx() returns >=0), use real LSM fields
878  // 2. Otherwise, use standalone fallback MultiFabs (allocated and constant-filled from RadChoice scalars)
879  // The resolve_surface_*() helpers implement the full precedence chain with finite guards
880 
881  // SW albedo. With Noah-MP, its broadband albedo (see lsm_broadband_albedo_name)
882  // comes first: the canonical SurfaceModel input radiation_inputs[2] is the
883  // visible direct-beam band of RRTMGP's four (Noah-MP's sfc_alb_dir_vis), not a
884  // broadband value. Otherwise that canonical input (SLM), then the standalone
885  // field. resolve_surface_albedo_sw takes the value per column where it is in
886  // [0, 1] and otherwise the erf.radiation.surface_albedo_sw default, so the
887  // lsm_undefined placeholder over water, before the first land step and at
888  // night (Noah-MP has no albedo without sunlight) falls back to the default.
889  bool has_hetero_alb_sw = false;
890  Array4<const amrex::Real> hetero_alb_sw_arr;
891  {
892  const MultiFab* lsm_albedo = noahmp_active
893  ? lsm_field(lsm, lev, lsm_broadband_albedo_name)
894  : nullptr;
895  if (lsm_albedo != nullptr) {
896  hetero_alb_sw_arr = lsm_albedo->const_array(mfi);
897  has_hetero_alb_sw = true;
898  } else if (radiation_inputs.size() > 2 && radiation_inputs[2]) {
899  hetero_alb_sw_arr = radiation_inputs[2]->const_array(mfi);
900  has_hetero_alb_sw = true;
901  } else if (m_alb_sw[lev]) {
902  hetero_alb_sw_arr = m_alb_sw[lev]->const_array(mfi);
903  has_hetero_alb_sw = true;
904  }
905  }
906 
907  // LW emissivity: Try LSM field "sfc_emis"
908  bool has_hetero_emiss_lw = false;
909  Array4<const amrex::Real> hetero_emiss_lw_arr;
910  {
911  std::string varname_emiss = "sfc_emis";
912  int lsm_idx = noahmp_active ? lsm.Get_DataIdx(lev, varname_emiss) : -1;
913  if (radiation_inputs.size() > 1 && radiation_inputs[1]) {
914  hetero_emiss_lw_arr = radiation_inputs[1]->const_array(mfi);
915  has_hetero_emiss_lw = true;
916  } else if (lsm_idx >= 0) {
917  auto lsm_ptr = lsm.Get_Data_Ptr(lev, lsm_idx);
918  if (lsm_ptr) {
919  hetero_emiss_lw_arr = lsm_ptr->const_array(mfi);
920  has_hetero_emiss_lw = true;
921  }
922  } else if (m_emiss_lw[lev]) {
923  hetero_emiss_lw_arr = m_emiss_lw[lev]->const_array(mfi);
924  has_hetero_emiss_lw = true;
925  }
926  }
927 
928  // Keep the surface-temperature sources separate until the column
929  // kernel resolves them per cell. An LSM field can exist globally
930  // while carrying an undefined sentinel in an individual column;
931  // that column must still fall through to SurfaceLayer theta.
932  // The kernel's precedence is valid LSM absolute temperature,
933  // valid prognostic SEB absolute temperature, SurfaceLayer theta,
934  // then the scalar absolute-temperature fallback.
935  bool has_lsm_t_sfc = false;
936  Array4<const amrex::Real> lsm_t_sfc_arr;
937  bool has_seb_t_sfc = false;
938  Array4<const amrex::Real> seb_t_sfc_arr;
939  bool has_surface_layer = false;
940  Array4<const amrex::Real> surface_layer_theta_arr;
941  {
942  std::string varname_t_sfc = "t_sfc";
943  int lsm_idx = noahmp_active ? lsm.Get_DataIdx(lev, varname_t_sfc) : -1;
944  if (radiation_inputs.size() > 0 && radiation_inputs[0]) {
945  lsm_t_sfc_arr = radiation_inputs[0]->const_array(mfi);
946  has_lsm_t_sfc = true;
947  } else if (lsm_idx >= 0) {
948  auto lsm_ptr = lsm.Get_Data_Ptr(lev, lsm_idx);
949  if (lsm_ptr) {
950  lsm_t_sfc_arr = lsm_ptr->const_array(mfi);
951  has_lsm_t_sfc = true;
952  }
953  }
954  // Prognostic SEB is a level-wide alternative to an authoritative
955  // external surface-temperature provider. When one owns this
956  // level's boundary, m_t_sfc is not advanced and is not offered
957  // as a per-cell fallback.
958  if (!has_lsm_t_sfc && rad_choice.seb_prognostic_enable &&
959  seb_active && m_t_sfc[lev]) {
960  seb_t_sfc_arr = m_t_sfc[lev]->const_array(mfi);
961  has_seb_t_sfc = true;
962  }
963  if (t_surf != nullptr) {
964  surface_layer_theta_arr = t_surf->const_array(mfi);
965  has_surface_layer = true;
966  }
967  }
968 
969  // Per-column latitude and longitude for the calendar sun (only
970  // the WRF and metgrid initialisations fill them; otherwise the
971  // erf.rad_cons_lat/lon constants apply).
972  const bool has_latlon = (lat_m != nullptr) && (lon_m != nullptr);
973  Array4<const amrex::Real> lat_arr, lon_arr;
974  if (has_latlon) {
975  lat_arr = lat_m->const_array(mfi);
976  lon_arr = lon_m->const_array(mfi);
977  }
978 
979  // Interface fluxes for ERF's rad_fluxes (SW up, SW down, LW up,
980  // LW down at each layer's lower interface, plus the top of the
981  // atmosphere in the z-ghost cell above the column), the level
982  // layout RRTMGP writes. The cloudy evaluation goes to a scratch
983  // FArrayBox on the same grown box and is blended like the
984  // heating rates.
985  const bool write_fluxes = (rad_fluxes != nullptr);
986  Array4<amrex::Real> rad_flux_clear_arr;
987  FArrayBox rad_flux_cloudy_fab;
988  Array4<amrex::Real> rad_flux_cloudy_arr;
989  if (write_fluxes) {
990  rad_flux_clear_arr = rad_fluxes->array(mfi);
991  if (cloud_fraction > 0.0) {
992  rad_flux_cloudy_fab.resize(two_stream_scratch_box(bx), 4);
993  rad_flux_cloudy_arr = rad_flux_cloudy_fab.array();
994  }
995  }
996 
997 
998  // Surface flux arrays the sweep fills for the SEB when asked to.
999  Array4<amrex::Real> sw_sfc_out;
1000  Array4<amrex::Real> lw_sfc_out;
1001  if (sw_flux_from_rad) sw_sfc_out = m_sw_flux_sfc[lev]->array(mfi);
1002  if (lw_flux_from_rad) lw_sfc_out = m_lw_flux_sfc[lev]->array(mfi);
1003 
1004  // What a land-surface model integrates on, written by the same kernel: the
1005  // surface-interface values of the (blended) fluxes, and the sun the sweep used.
1006  const bool supply_land = supplies_land_forcing(lev);
1007  Array4<amrex::Real> sw_dn_out, lw_dn_out, coszen_out;
1008  if (supply_land) {
1009  sw_dn_out = m_sw_dn_sfc[lev]->array(mfi);
1010  lw_dn_out = m_lw_dn_sfc[lev]->array(mfi);
1011  coszen_out = m_cos_zenith[lev]->array(mfi);
1012  }
1013 
1014  // What the building faces take (erf.ibseb.radiation = two_stream) besides the
1015  // interface fluxes, written by the same kernel: the direct beam at the canopy's
1016  // interfaces from the scratch of each evaluation, weighted as the blend weights
1017  // them, and the cosine of the zenith the sweep used.
1018  const bool supply_canopy = supplies_canopy_forcing(lev);
1019  const int m_canopy_top = supply_canopy ? m_canopy_m[lev] : 0;
1020  const bool sw_on = rad_choice.sw_enabled;
1021  Array4<amrex::Real> canopy_out, canopy_cosz_out;
1022  if (supply_canopy) {
1023  AMREX_ALWAYS_ASSERT_WITH_MESSAGE(write_fluxes,
1024  "TwoStreamRadiation: the canopy forcing of the building faces needs the rad_fluxes array");
1025  AMREX_ALWAYS_ASSERT_WITH_MESSAGE(m_canopy_top <= bx.length(2),
1026  "TwoStreamRadiation: the canopy reaches above the top of the column");
1027  canopy_out = m_canopy[lev]->array(mfi);
1028  canopy_cosz_out = m_canopy_cosz[lev]->array(mfi);
1029  }
1030 
1031  // Create a 2D box for (i,j) iteration over the horizontal extent
1032  // One GPU thread per (i,j) column; k-loop is sequential within each thread
1033  const auto& lo = bx.loVect();
1034  const auto& hi = bx.hiVect();
1035  Box xy_box(IntVect(lo[0], lo[1], 0), IntVect(hi[0], hi[1], 0));
1036 
1037  // Count columns in this box for later averaging
1038  amrex::Long n_cols = static_cast<amrex::Long>(bx.length(0)) *
1039  static_cast<amrex::Long>(bx.length(1));
1040  n_columns_total += n_cols;
1041 
1042  // Clear-sky-column heating rates are written directly
1043  // into the real qheating_rates MultiFab when available.
1044  // Fall back to a throwaway local FArrayBox otherwise (keeps the
1045  // kernel call GPU-safe even if qheating_rates isn't allocated).
1046  FArrayBox qheating_fallback_fab;
1047  Array4<amrex::Real> qheating_clear_arr;
1048  if (qheating_mf != nullptr) {
1049  qheating_clear_arr = qheating_mf->array(mfi);
1050  } else {
1051  qheating_fallback_fab.resize(bx, 2);
1052  qheating_clear_arr = qheating_fallback_fab.array();
1053  }
1054 
1055  // Cloudy-column heating rates always go into a scratch
1056  // FArrayBox; only used/blended when cloud_fraction > 0.
1057  FArrayBox qheating_cloudy_fab;
1058  Array4<amrex::Real> qheating_cloudy_arr;
1059  if (cloud_fraction > 0.0) {
1060  qheating_cloudy_fab.resize(bx, 2);
1061  qheating_cloudy_arr = qheating_cloudy_fab.array();
1062  }
1063 
1064  // GPU-safe reduction using ReduceOps (per-column results aggregated on device)
1065  amrex::Real max_heating_box = 0.0;
1066  amrex::Real sw_sum_box = 0.0;
1067  amrex::Real sw_up_sum_box = 0.0;
1068  amrex::Real lw_sum_box = 0.0;
1069  amrex::Real lw_up_sum_box = 0.0;
1070  amrex::Real sw_toa_sum_box = 0.0;
1071 
1072  // Device-side reduction: max heating, sums of the surface and
1073  // top-of-atmosphere fluxes
1074  ReduceOps<ReduceOpMax, ReduceOpSum, ReduceOpSum, ReduceOpSum, ReduceOpSum, ReduceOpSum> reduce_ops;
1075  ReduceData<amrex::Real, amrex::Real, amrex::Real, amrex::Real, amrex::Real, amrex::Real> reduce_data(reduce_ops);
1076 
1077  using ReduceTuple = typename decltype(reduce_data)::Type;
1078 
1079  // Launch parallel kernel over (i,j) columns
1080  reduce_ops.eval(xy_box, reduce_data,
1081  [=] AMREX_GPU_DEVICE (int i, int j, int /*k_unused*/) -> ReduceTuple
1082  {
1083  // Clear-sky column (always evaluated; this is the sole
1084  // contributor when cloud_fraction == 0.0, matching earlier behavior)
1085  amrex::Real max_heating_clear = 0.0;
1086  amrex::Real sw_flux_clear = 0.0;
1087  amrex::Real sw_up_clear = 0.0;
1088  amrex::Real lw_net_clear = 0.0;
1089  amrex::Real lw_up_clear = 0.0;
1090  amrex::Real sw_toa_clear = 0.0;
1091  amrex::Real cos_zenith_col = 0.0;
1093  i, j, bx, dz_uniform_lev, state_arr, ts_params, /*cloudy=*/false,
1094  qheating_clear_arr,
1095  max_heating_clear, sw_flux_clear, sw_up_clear, lw_net_clear, lw_up_clear,
1096  sw_toa_clear,
1097  z_phys_nd_arr, scratch_arr,
1098  has_hetero_alb_sw, &hetero_alb_sw_arr,
1099  has_hetero_emiss_lw, &hetero_emiss_lw_arr,
1100  has_lsm_t_sfc, &lsm_t_sfc_arr,
1101  has_seb_t_sfc, &seb_t_sfc_arr,
1102  has_surface_layer, &surface_layer_theta_arr,
1103  has_latlon, &lat_arr, &lon_arr,
1104  write_fluxes ? &rad_flux_clear_arr : nullptr,
1105  &cos_zenith_col);
1106 
1107  // The scratch holds this evaluation's beam until the cloudy one overwrites it.
1108  if (supply_canopy) {
1109  two_stream_canopy_beam(i, j, bx.smallEnd(2), m_canopy_top, scratch_arr,
1110  TwoStreamScratch::F_DIR, sw_on, cos_zenith_col,
1111  cloud_fraction, false, canopy_out);
1112  }
1113 
1114  amrex::Real max_heating_col = max_heating_clear;
1115  amrex::Real sw_flux_col = sw_flux_clear;
1116  amrex::Real sw_up_col = sw_up_clear;
1117  amrex::Real lw_net_col = lw_net_clear;
1118  amrex::Real lw_up_col = lw_up_clear;
1119 
1120  // Cloudy column only needs to be evaluated when there is a
1121  // nonzero cloud fraction; this keeps the cloud_fraction==0
1122  // path numerically and computationally identical.
1123  if (cloud_fraction > 0.0) {
1124  amrex::Real max_heating_cloudy = 0.0;
1125  amrex::Real sw_flux_cloudy = 0.0;
1126  amrex::Real sw_up_cloudy = 0.0;
1127  amrex::Real lw_net_cloudy = 0.0;
1128  amrex::Real lw_up_cloudy = 0.0;
1129  amrex::Real sw_toa_cloudy = 0.0;
1131  i, j, bx, dz_uniform_lev, state_arr, ts_params, /*cloudy=*/true,
1132  qheating_cloudy_arr,
1133  max_heating_cloudy, sw_flux_cloudy, sw_up_cloudy, lw_net_cloudy, lw_up_cloudy,
1134  sw_toa_cloudy,
1135  z_phys_nd_arr, scratch_arr,
1136  has_hetero_alb_sw, &hetero_alb_sw_arr,
1137  has_hetero_emiss_lw, &hetero_emiss_lw_arr,
1138  has_lsm_t_sfc, &lsm_t_sfc_arr,
1139  has_seb_t_sfc, &seb_t_sfc_arr,
1140  has_surface_layer, &surface_layer_theta_arr,
1141  has_latlon, &lat_arr, &lon_arr,
1142  write_fluxes ? &rad_flux_cloudy_arr : nullptr);
1143 
1144  // Blend clear-sky and cloudy-column results
1145  sw_flux_col = (1.0 - cloud_fraction) * sw_flux_clear +
1146  cloud_fraction * sw_flux_cloudy;
1147  sw_up_col = (1.0 - cloud_fraction) * sw_up_clear +
1148  cloud_fraction * sw_up_cloudy;
1149  lw_net_col = (1.0 - cloud_fraction) * lw_net_clear +
1150  cloud_fraction * lw_net_cloudy;
1151  lw_up_col = (1.0 - cloud_fraction) * lw_up_clear +
1152  cloud_fraction * lw_up_cloudy;
1153  max_heating_col = std::max(max_heating_clear, max_heating_cloudy);
1154  if (supply_canopy) {
1155  two_stream_canopy_beam(i, j, bx.smallEnd(2), m_canopy_top, scratch_arr,
1156  TwoStreamScratch::F_DIR, sw_on, cos_zenith_col,
1157  cloud_fraction, true, canopy_out);
1158  }
1159 
1160  // Blend per-level heating rates in place
1161  // into qheating_clear_arr (which is the real output
1162  // MultiFab when qheating_mf != nullptr).
1163  int kmin = bx.smallEnd(2);
1164  int kmax = bx.bigEnd(2);
1165  for (int k = kmin; k <= kmax; ++k) {
1166  for (int comp = 0; comp < 2; ++comp) {
1167  amrex::Real q_clear_val = qheating_clear_arr(i, j, k, comp);
1168  amrex::Real q_cloudy_val = qheating_cloudy_arr(i, j, k, comp);
1169  qheating_clear_arr(i, j, k, comp) =
1170  (1.0 - cloud_fraction) * q_clear_val +
1171  cloud_fraction * q_cloudy_val;
1172  }
1173  }
1174  if (write_fluxes) {
1175  // nlev + 1 levels: the top interface sits at kmax + 1
1176  for (int k = kmin; k <= kmax + 1; ++k) {
1177  for (int comp = 0; comp < 4; ++comp) {
1178  rad_flux_clear_arr(i, j, k, comp) =
1179  (1.0 - cloud_fraction) * rad_flux_clear_arr(i, j, k, comp) +
1180  cloud_fraction * rad_flux_cloudy_arr(i, j, k, comp);
1181  }
1182  }
1183  }
1184  }
1185 
1186  // Surface fluxes for the SEB: absorbed shortwave, and the
1187  // absorbed longwave, which is minus the net (up - down).
1188  if (sw_flux_from_rad) sw_sfc_out(i, j, 0) = sw_flux_col;
1189  if (lw_flux_from_rad) lw_sfc_out(i, j, 0) = -lw_net_col;
1190 
1191  // The land model's forcing, after the clear/cloudy blend above.
1192  if (supply_land) {
1193  two_stream_land_forcing(i, j, bx.smallEnd(2), rad_flux_clear_arr,
1194  cos_zenith_col, sw_dn_out(i, j, 0),
1195  lw_dn_out(i, j, 0), coszen_out(i, j, 0));
1196  }
1197 
1198  // The sun the building faces must share (their beam is in canopy_out).
1199  if (supply_canopy) {
1200  canopy_cosz_out(i, j, 0) = (cos_zenith_col > amrex::Real(0.0)) ? cos_zenith_col : amrex::Real(0.0);
1201  }
1202 
1203  // The incident TOA flux is the same for both evaluations.
1204  // Return tuple for reduction
1205  return {max_heating_col, sw_flux_col, sw_up_col, lw_net_col, lw_up_col, sw_toa_clear};
1206  }
1207  );
1208 
1209  // Copy results from device to host
1210  amrex::Gpu::synchronize();
1211  auto reduce_tuple = reduce_data.value(reduce_ops);
1212  max_heating_box = amrex::get<0>(reduce_tuple);
1213  sw_sum_box = amrex::get<1>(reduce_tuple);
1214  sw_up_sum_box = amrex::get<2>(reduce_tuple);
1215  lw_sum_box = amrex::get<3>(reduce_tuple);
1216  lw_up_sum_box = amrex::get<4>(reduce_tuple);
1217  sw_toa_sum_box = amrex::get<5>(reduce_tuple);
1218 
1219  // Accumulate box results into global results
1220  max_heating_global = std::max(max_heating_global, max_heating_box);
1221  sw_surface_sum += sw_sum_box;
1222  sw_up_toa_sum += sw_up_sum_box;
1223  lw_net_sum += lw_sum_box;
1224  lw_up_toa_sum += lw_up_sum_box;
1225  sw_toa_sum += sw_toa_sum_box;
1226 
1227  }
1228  // Every accumulator above is rank-local. Reduce before forming means
1229  // and maxima, so the diagnostics describe the whole domain and do
1230  // not change with the decomposition. These are collective calls; the
1231  // conditions around them are input-driven and identical on all ranks.
1232  if (do_sweep) {
1233  amrex::Real sums[5] = {sw_surface_sum, sw_up_toa_sum, lw_net_sum, lw_up_toa_sum, sw_toa_sum};
1234  ParallelDescriptor::ReduceRealSum(sums, 5);
1235  sw_surface_sum = sums[0]; sw_up_toa_sum = sums[1];
1236  lw_net_sum = sums[2]; lw_up_toa_sum = sums[3];
1237  sw_toa_sum = sums[4];
1238  ParallelDescriptor::ReduceLongSum(n_columns_total);
1239  ParallelDescriptor::ReduceRealMax(max_heating_global);
1240  // The faces may take this sweep's canopy forcing in this step, and only in it.
1241  if (supplies_canopy_forcing(lev)) { m_canopy_step[lev] = nstep; }
1242  }
1243  } // do_sweep
1244 
1245  // The heating rates go straight into the RhoTheta source term, so a
1246  // non-finite value here becomes a non-finite state one step later.
1247  if (do_sweep && qheating_mf != nullptr &&
1248  (qheating_mf->contains_nan(0, 2, 0) || qheating_mf->contains_inf(0, 2, 0))) {
1249  amrex::Abort("TwoStreamRadiation: non-finite heating rate after the column sweep at level " +
1250  std::to_string(lev) + ", step " + std::to_string(nstep));
1251  }
1252 
1253  // Warn if diagnostic is requested but SEB infrastructure isn't enabled
1254  if (rad_choice.seb_diagnostic_enable && !rad_choice.seb_enable && lev == 0) {
1255  static bool warned_seb_misconfig = false;
1256  if (!warned_seb_misconfig && ParallelDescriptor::IOProcessor()) {
1257  Print() << "WARNING: erf.radiation.seb_diagnostic_enable=true but "
1258  "seb_enable=false; SEB residual diagnostics will report NaN. "
1259  "Set erf.radiation.seb_enable=true to enable SEB field "
1260  "population.\n";
1261  warned_seb_misconfig = true;
1262  }
1263  }
1264  // Compute SEB residual diagnostics if enabled
1265  if (rad_choice.seb_diagnostic_enable && seb_active) {
1266  seb_residual_max = 0.0;
1267  // Second loop over boxes to compute SEB residual from populated SEB
1268  // MultiFabs. Untiled: the work below is per surface column, and a
1269  // tiled iteration would visit each (i,j) once per z tile.
1270  for (MFIter mfi(state_cons, false); mfi.isValid(); ++mfi) {
1271  const Box& bx = mfi.validbox();
1272  const auto& lo = bx.loVect();
1273  const auto& hi = bx.hiVect();
1274  Box xy_box(IntVect(lo[0], lo[1], 0), IntVect(hi[0], hi[1], 0));
1275 
1276  // Get SEB field arrays
1277  Array4<const amrex::Real> sw_flux_arr = m_sw_flux_sfc[lev]->const_array(mfi);
1278  Array4<const amrex::Real> lw_flux_arr = m_lw_flux_sfc[lev]->const_array(mfi);
1279  Array4<const amrex::Real> hfx_arr = m_hfx_sfc[lev]->const_array(mfi);
1280  Array4<const amrex::Real> lh_arr = m_lh_sfc[lev]->const_array(mfi);
1281  Array4<const amrex::Real> grdflx_arr = m_grdflx_sfc[lev]->const_array(mfi);
1282 
1283  // Count columns and compute residuals
1284  amrex::Long n_cols_box = static_cast<amrex::Long>(bx.length(0)) *
1285  static_cast<amrex::Long>(bx.length(1));
1286  amrex::Real residual_sum_box = 0.0;
1287  amrex::Real residual_max_box = 0.0;
1288 
1289  // GPU-safe reduction for SEB residuals
1290  ReduceOps<ReduceOpSum, ReduceOpMax> seb_reduce_ops;
1291  ReduceData<amrex::Real, amrex::Real> seb_reduce_data(seb_reduce_ops);
1292 
1293  using SEBReduceTuple = typename decltype(seb_reduce_data)::Type;
1294 
1295  seb_reduce_ops.eval(xy_box, seb_reduce_data,
1296  [=] AMREX_GPU_DEVICE (int i, int j, int /*k_unused*/) -> SEBReduceTuple {
1297  amrex::Real sw_net = sw_flux_arr(i, j, 0);
1298  amrex::Real lw_net = lw_flux_arr(i, j, 0);
1299  amrex::Real hfx = hfx_arr(i, j, 0);
1300  amrex::Real lh = lh_arr(i, j, 0);
1301  amrex::Real grdflx = grdflx_arr(i, j, 0);
1302 
1303  // Compute residual using helper function
1304  amrex::Real residual = diagnose_seb_residual(sw_net, lw_net, hfx, lh, grdflx);
1305 
1306  // Return sum and abs(max) of residual
1307  return {residual, std::abs(residual)};
1308  }
1309  );
1310 
1311  // Copy results from device to host
1312  amrex::Gpu::synchronize();
1313  auto seb_reduce_tuple = seb_reduce_data.value(seb_reduce_ops);
1314  residual_sum_box = amrex::get<0>(seb_reduce_tuple);
1315  residual_max_box = amrex::get<1>(seb_reduce_tuple);
1316 
1317  // Accumulate into global results
1318  seb_residual_sum += residual_sum_box;
1319  seb_residual_max = std::max(seb_residual_max, residual_max_box);
1320  n_seb_columns += n_cols_box;
1321  }
1322  ParallelDescriptor::ReduceRealSum(seb_residual_sum);
1323  ParallelDescriptor::ReduceRealMax(seb_residual_max);
1324  ParallelDescriptor::ReduceLongSum(n_seb_columns);
1325  }
1326 
1327  // Prognostic SEB surface temperature and moisture evolution
1328  // Only advance the force-restore state when TwoStream owns the
1329  // surface-temperature boundary at this level.
1330  //
1331  // With the surface radiation taken from the sweep, also wait for this level's
1332  // first sweep. Until then m_sw_flux_sfc / m_lw_flux_sfc hold what define_level
1333  // left, and advancing on them would apply the scalar defaults for that step. That
1334  // is the first step of a level built by interp_atmos_from_coarse, whose pre-dycore
1335  // sweep ERF::advance_radiation skips (the level's state is not yet consistent),
1336  // while this post-dycore call still arrives.
1337  const bool seb_radiation_ready = m_flux_diag[lev].valid ||
1338  !(sw_flux_from_rad || lw_flux_from_rad);
1339  if (rad_choice.seb_prognostic_enable && seb_active &&
1340  call_site == "post_dycore" && seb_radiation_ready) {
1341  if (!has_external_surface_temperature) {
1342  // No external provider owns the boundary; advance TwoStream's state.
1343 
1344  // Initialize diagnostics for T_s and q_s
1345  amrex::Real t_s_sum = 0.0;
1346  amrex::Real t_s_max_val = -std::numeric_limits<amrex::Real>::max();
1347  amrex::Real q_s_sum = 0.0;
1348  amrex::Real q_s_max_val = -std::numeric_limits<amrex::Real>::max();
1349  amrex::Long n_prog_columns = 0;
1350 
1351  // Third loop over boxes for prognostic SEB update. Untiled for
1352  // the same reason as above, and here it also matters for
1353  // correctness: the force-restore update is applied in place, so
1354  // visiting a column twice would advance it twice in one step.
1355  for (MFIter mfi(state_cons, false); mfi.isValid(); ++mfi) {
1356  const Box& bx = mfi.validbox();
1357  const auto& lo = bx.loVect();
1358  const auto& hi = bx.hiVect();
1359  Box xy_box(IntVect(lo[0], lo[1], 0), IntVect(hi[0], hi[1], 0));
1360 
1361  // Get SEB field arrays (read-only)
1362  Array4<const amrex::Real> sw_flux_arr = m_sw_flux_sfc[lev]->const_array(mfi);
1363  Array4<const amrex::Real> lw_flux_arr = m_lw_flux_sfc[lev]->const_array(mfi);
1364  Array4<const amrex::Real> hfx_arr = m_hfx_sfc[lev]->const_array(mfi);
1365  Array4<const amrex::Real> lh_arr = m_lh_sfc[lev]->const_array(mfi);
1366  Array4<const amrex::Real> grdflx_arr = m_grdflx_sfc[lev]->const_array(mfi);
1367  Array4<const amrex::Real> t_deep_arr = m_t_deep[lev]->const_array(mfi);
1368  Array4<const amrex::Real> q_deep_arr = m_q_deep[lev]->const_array(mfi);
1369 
1370  // Get SEB state arrays (read-write for prognostic update)
1371  Array4<amrex::Real> t_s_arr = m_t_sfc[lev]->array(mfi);
1372  Array4<amrex::Real> q_s_arr = m_q_sfc[lev]->array(mfi);
1373  // Count columns and prepare for reductions
1374  amrex::Long n_cols_box = static_cast<amrex::Long>(bx.length(0)) *
1375  static_cast<amrex::Long>(bx.length(1));
1376  amrex::Real t_s_sum_box = 0.0;
1377  amrex::Real t_s_max_box = -std::numeric_limits<amrex::Real>::max();
1378  amrex::Real q_s_sum_box = 0.0;
1379  amrex::Real q_s_max_box = -std::numeric_limits<amrex::Real>::max();
1380 
1381  // GPU-safe update for prognostic T_s and q_s with reductions
1382  ReduceOps<ReduceOpSum, ReduceOpMax, ReduceOpSum, ReduceOpMax> prog_reduce_ops;
1383  ReduceData<amrex::Real, amrex::Real, amrex::Real, amrex::Real> prog_reduce_data(prog_reduce_ops);
1384 
1385  using ProgReduceTuple = typename decltype(prog_reduce_data)::Type;
1386 
1387  prog_reduce_ops.eval(xy_box, prog_reduce_data,
1388  [=, C_s=rad_choice.seb_surface_heat_capacity,
1389  tau=rad_choice.seb_restore_timescale_s,
1390  d_s=rad_choice.seb_moisture_layer_depth_m,
1391  tau_q=rad_choice.seb_moisture_restore_timescale_s,
1392  t_min=rad_choice.seb_prognostic_t_min_k,
1393  t_max=rad_choice.seb_prognostic_t_max_k,
1394  q_min=rad_choice.seb_prognostic_q_min,
1395  q_max=rad_choice.seb_prognostic_q_max]
1396  AMREX_GPU_DEVICE (int i, int j, int /*k_unused*/) -> ProgReduceTuple {
1397  amrex::Real t_s_old = t_s_arr(i, j, 0);
1398  amrex::Real q_s_old = q_s_arr(i, j, 0);
1399 
1400  // Read forcing data
1401  amrex::Real sw_net = sw_flux_arr(i, j, 0);
1402  amrex::Real lw_net = lw_flux_arr(i, j, 0);
1403  amrex::Real hfx = hfx_arr(i, j, 0);
1404  amrex::Real lh = lh_arr(i, j, 0);
1405  amrex::Real grdflx = grdflx_arr(i, j, 0);
1406  amrex::Real t_deep_val = t_deep_arr(i, j, 0);
1407  amrex::Real q_deep_val = q_deep_arr(i, j, 0);
1408 
1409  // Compute SEB residual
1410  amrex::Real seb_res = diagnose_seb_residual(sw_net, lw_net, hfx, lh, grdflx);
1411 
1412  // Compute tendencies
1413  amrex::Real dT_s_dt = prognostic_dTs_dt(seb_res, t_s_old, t_deep_val,
1414  C_s, tau);
1415  amrex::Real dq_s_dt = prognostic_dqs_dt(lh, q_s_old, q_deep_val,
1416  d_s, tau_q);
1417 
1418  // Explicit Euler update over the step size
1419  amrex::Real t_s_new = t_s_old + dt_step * dT_s_dt;
1420  amrex::Real q_s_new = q_s_old + dt_step * dq_s_dt;
1421 
1422  // Clamp to valid ranges
1423  t_s_new = amrex::max(t_min, amrex::min(t_max, t_s_new));
1424  q_s_new = amrex::max(q_min, amrex::min(q_max, q_s_new));
1425 
1426  // Write back updated values (this modifies the device array)
1427  t_s_arr(i, j, 0) = t_s_new;
1428  q_s_arr(i, j, 0) = q_s_new;
1429 
1430  // Return for reduction: sum T_s, max T_s, sum q_s, max q_s
1431  return {t_s_new, std::abs(t_s_new), q_s_new, std::abs(q_s_new)};
1432  });
1433 
1434  // Copy results from device to host
1435  amrex::Gpu::synchronize();
1436  auto prog_reduce_tuple = prog_reduce_data.value(prog_reduce_ops);
1437  t_s_sum_box = amrex::get<0>(prog_reduce_tuple);
1438  t_s_max_box = amrex::get<1>(prog_reduce_tuple);
1439  q_s_sum_box = amrex::get<2>(prog_reduce_tuple);
1440  q_s_max_box = amrex::get<3>(prog_reduce_tuple);
1441 
1442  // Accumulate into global results
1443  t_s_sum += t_s_sum_box;
1444  t_s_max_val = std::max(t_s_max_val, t_s_max_box);
1445  q_s_sum += q_s_sum_box;
1446  q_s_max_val = std::max(q_s_max_val, q_s_max_box);
1447  n_prog_columns += n_cols_box;
1448  }
1449 
1450  // The force-restore kernels return a zero tendency on any
1451  // non-finite input, so a NaN here means the state itself was
1452  // corrupted (a bad restart file, or an overwrite elsewhere).
1453  if (m_t_sfc[lev]->contains_nan(0, 1, 0) || m_q_sfc[lev]->contains_nan(0, 1, 0)) {
1454  amrex::Abort("TwoStreamRadiation: non-finite surface temperature or moisture "
1455  "after the force-restore update at level " + std::to_string(lev) +
1456  ", step " + std::to_string(nstep));
1457  }
1458 
1459  {
1460  amrex::Real sums[2] = {t_s_sum, q_s_sum};
1461  ParallelDescriptor::ReduceRealSum(sums, 2);
1462  t_s_sum = sums[0]; q_s_sum = sums[1];
1463  ParallelDescriptor::ReduceRealMax(t_s_max_val);
1464  ParallelDescriptor::ReduceRealMax(q_s_max_val);
1465  ParallelDescriptor::ReduceLongSum(n_prog_columns);
1466  }
1467 
1468  // Compute mean values from sums
1469  if (n_prog_columns > 0) {
1470  t_s_mean = t_s_sum / static_cast<amrex::Real>(n_prog_columns);
1471  t_s_max = t_s_max_val;
1472  q_s_mean = q_s_sum / static_cast<amrex::Real>(n_prog_columns);
1473  q_s_max = q_s_max_val;
1474  } else {
1475  t_s_mean = std::numeric_limits<amrex::Real>::quiet_NaN();
1476  t_s_max = std::numeric_limits<amrex::Real>::quiet_NaN();
1477  q_s_mean = std::numeric_limits<amrex::Real>::quiet_NaN();
1478  q_s_max = std::numeric_limits<amrex::Real>::quiet_NaN();
1479  }
1480  } else {
1481  // An external provider owns the boundary; leave the unused
1482  // TwoStream state untouched and do not report it as active.
1483  t_s_mean = std::numeric_limits<amrex::Real>::quiet_NaN();
1484  t_s_max = std::numeric_limits<amrex::Real>::quiet_NaN();
1485  q_s_mean = std::numeric_limits<amrex::Real>::quiet_NaN();
1486  q_s_max = std::numeric_limits<amrex::Real>::quiet_NaN();
1487  }
1488  }
1489  // equivalent to a single-column value for spatially UNIFORM atmospheres
1490  // (as in the current SW_ClearSky_Analytical / LW_Isothermal RegTests).
1491  // Cloud layer and scattering tests are ALSO spatially uniform (identical
1492  // cloud/tau/scattering parameters applied to every column), so the
1493  // domain-averaged value still equals the true single-column flux there.
1494  // True horizontal heterogeneity (e.g., patchy clouds varying by column)
1495  // remains deferred to future work.
1496  if (do_sweep) {
1497  if (n_columns_total > 0) {
1498  const amrex::Real inv_n = 1.0 / static_cast<amrex::Real>(n_columns_total);
1499  SW_surface = sw_surface_sum * inv_n;
1500  SW_TOA = sw_toa_sum * inv_n;
1501  SW_up_TOA = sw_up_toa_sum * inv_n;
1502  LW_net_surface = lw_net_sum * inv_n;
1503  LW_up_TOA = lw_up_toa_sum * inv_n;
1504  }
1505  heating_rate_max = max_heating_global;
1506  m_flux_diag[lev] = FluxDiag{true, SW_surface, SW_TOA, SW_up_TOA,
1507  LW_net_surface, LW_up_TOA,
1508  heating_rate_max};
1509  } else {
1510  const FluxDiag& cached = m_flux_diag[lev];
1511  if (!cached.valid) {
1512  // No sweep has run on this level yet, so there is nothing to report. This
1513  // happens on the first step of a level built by interp_atmos_from_coarse:
1514  // ERF::advance_radiation interpolates that level's fields and returns
1515  // before the pre-dycore sweep, while this post-dycore call still arrives
1516  // from ERF::Advance. Emitting here would write a row of zeros that reads
1517  // exactly like a real zero-flux result. A nested patch already contributes
1518  // no rows for the same reason; this is the same rule for a level that
1519  // simply has not swept yet.
1520  return;
1521  }
1522  SW_surface = cached.SW_surface;
1523  SW_TOA = cached.SW_TOA;
1524  SW_up_TOA = cached.SW_up_TOA;
1525  LW_net_surface = cached.LW_net_surface;
1526  LW_up_TOA = cached.LW_up_TOA;
1527  heating_rate_max = cached.heating_rate_max;
1528  }
1529 
1530  // Compute SEB residual mean from sum
1531  if (rad_choice.seb_diagnostic_enable && seb_active && n_seb_columns > 0) {
1532  seb_residual_mean = seb_residual_sum / static_cast<amrex::Real>(n_seb_columns);
1533  } else {
1534  // When feature is disabled, use NaN for backward compatibility
1535  seb_residual_mean = std::numeric_limits<amrex::Real>::quiet_NaN();
1536  seb_residual_max = std::numeric_limits<amrex::Real>::quiet_NaN();
1537  }
1538 
1539  }
1540 
1541  // Logging output
1542  if (rad_choice.verbosity >= 1 && ParallelDescriptor::IOProcessor()) {
1543  Print() << "Radiation diagnostics at step " << nstep << ":\n"
1544  << " SW TOA = " << SW_TOA << " W/m^2\n"
1545  << " SW surface = " << SW_surface << " W/m^2\n"
1546  << " SW up (TOA) = " << SW_up_TOA << " W/m^2\n"
1547  << " LW net (surface) = " << LW_net_surface << " W/m^2\n"
1548  << " LW up (TOA) = " << LW_up_TOA << " W/m^2\n"
1549  << " Max heating rate = " << heating_rate_max << " K/s\n";
1550  if (rad_choice.seb_diagnostic_enable && std::isfinite(seb_residual_mean)) {
1551  Print() << " SEB residual (mean) = " << seb_residual_mean << " W/m^2\n"
1552  << " SEB residual (max) = " << seb_residual_max << " W/m^2\n";
1553  }
1554  if (rad_choice.seb_prognostic_enable && std::isfinite(t_s_mean)) {
1555  Print() << " Surface temperature (mean) = " << t_s_mean << " K\n"
1556  << " Surface temperature (max) = " << t_s_max << " K\n"
1557  << " Surface moisture (mean) = " << q_s_mean << " kg/kg\n"
1558  << " Surface moisture (max) = " << q_s_max << " kg/kg\n";
1559  }
1560  }
1561 
1562  rad_diag.append(nstep, time, call_site, SW_surface, SW_TOA,
1563  SW_up_TOA, LW_net_surface, LW_up_TOA, heating_rate_max,
1564  seb_residual_mean, seb_residual_max,
1565  t_s_mean, t_s_max, q_s_mean, q_s_max);
1566 }
#define Rho_comp
Definition: ERF_IndexDefines.H:42
#define RhoTheta_comp
Definition: ERF_IndexDefines.H:43
AMREX_ALWAYS_ASSERT_WITH_MESSAGE(m_cloud_chamber_config.active, "Cloud Chamber: initializer reached without a parsed configuration")
pp get("wavelength", wavelength)
SEBFluxOrigin fill_seb_turbulent_flux(MultiFab &seb_flux, SEBTurbulentFlux kind, const MultiFab *land_surface_field, const MultiFab *surface_layer_flux, int surface_k, SEBTurbulentFluxSource source, Real fallback)
Definition: ERF_SEBTurbulentFlux.cpp:63
amrex::Real Real
Definition: ERF_ShocInterface.H:19
AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE amrex::Real diagnose_seb_residual(amrex::Real sw_net, amrex::Real lw_net, amrex::Real hfx, amrex::Real lh, amrex::Real grdflx)
Diagnose the surface energy balance residual.
Definition: ERF_SimplifiedSEB.H:68
AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE amrex::Real prognostic_dqs_dt(amrex::Real le, amrex::Real q_s, amrex::Real q_deep, amrex::Real d_s, amrex::Real tau_q)
Compute prognostic surface moisture tendency using force-restore formulation.
Definition: ERF_SimplifiedSEB.H:169
AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE amrex::Real prognostic_dTs_dt(amrex::Real seb_residual, amrex::Real t_s, amrex::Real t_deep, amrex::Real c_s, amrex::Real tau)
Compute prognostic surface temperature tendency using force-restore formulation.
Definition: ERF_SimplifiedSEB.H:114
AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE void two_stream_canopy_beam(int i, int j, int kmin, int m_top, const amrex::Array4< const amrex::Real > &scratch, int f_dir_comp, bool sw_on, amrex::Real cos_zenith, amrex::Real cloud_fraction, bool blend, const amrex::Array4< amrex::Real > &beam)
Write the direct beam on a horizontal surface at the interfaces 0 .. m_top of column (i,...
Definition: ERF_TwoStreamCanopyForcing.H:77
TwoStreamParams make_two_stream_params(const RadChoice &rc, const amrex::Real rdOcp)
Copy the column-kernel parameters out of a RadChoice.
Definition: ERF_TwoStreamColumn.H:127
AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE void two_stream_land_forcing(int i, int j, int kmin, const amrex::Array4< const amrex::Real > &rad_flux, amrex::Real cos_zenith, amrex::Real &sw_dn, amrex::Real &lw_dn, amrex::Real &coszen)
The radiative forcing a land-surface model integrates on, for column (i,j).
Definition: ERF_TwoStreamColumn.H:858
amrex::Box two_stream_scratch_box(const amrex::Box &bx)
Box of the scratch FArrayBox a sweep over box bx needs.
Definition: ERF_TwoStreamColumn.H:612
AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE void vertical_two_stream_sweep(int i, int j, const amrex::Box &bx, amrex::Real dz_uniform, const amrex::Array4< const amrex::Real > &state_arr, const TwoStreamParams &rad_choice, bool cloudy, const amrex::Array4< amrex::Real > &qheating_arr, amrex::Real &max_heating_rate, amrex::Real &sw_surface_flux, amrex::Real &sw_up_toa, amrex::Real &lw_net_surface, amrex::Real &lw_up_toa, amrex::Real &sw_down_toa, const amrex::Array4< const amrex::Real > &z_phys_nd, const amrex::Array4< amrex::Real > &scratch, bool has_hetero_alb_sw=false, const amrex::Array4< const amrex::Real > *hetero_alb_sw=nullptr, bool has_hetero_emiss_lw=false, const amrex::Array4< const amrex::Real > *hetero_emiss_lw=nullptr, bool has_lsm_t_sfc=false, const amrex::Array4< const amrex::Real > *lsm_t_sfc=nullptr, bool has_seb_t_sfc=false, const amrex::Array4< const amrex::Real > *seb_t_sfc=nullptr, bool has_surface_layer=false, const amrex::Array4< const amrex::Real > *surface_layer_theta=nullptr, bool has_latlon=false, const amrex::Array4< const amrex::Real > *lat_arr=nullptr, const amrex::Array4< const amrex::Real > *lon_arr=nullptr, const amrex::Array4< amrex::Real > *rad_flux_out=nullptr, amrex::Real *cos_zenith_out=nullptr)
GPU-safe per-column vertical integration kernel for two-stream radiation, computing either the clear-...
Definition: ERF_TwoStreamColumn.H:945
int Get_DataIdx(const int &lev, const std::string &varname)
Definition: ERF_LandSurface.H:186
amrex::MultiFab * Get_Data_Ptr(const int &lev, const int &varIdx)
Definition: ERF_LandSurface.H:142
CSV logger and debug-print manager for radiation module.
Definition: ERF_RadiationDiagnostics.H:75
void append(int step, amrex::Real time, std::string const &call_site, amrex::Real sw_surface, amrex::Real sw_toa, amrex::Real sw_up_toa, amrex::Real lw_net_surface, amrex::Real lw_up_toa, amrex::Real heating_rate_max, amrex::Real seb_residual_mean=std::numeric_limits< amrex::Real >::quiet_NaN(), amrex::Real seb_residual_max=std::numeric_limits< amrex::Real >::quiet_NaN(), amrex::Real t_s_mean=std::numeric_limits< amrex::Real >::quiet_NaN(), amrex::Real t_s_max=std::numeric_limits< amrex::Real >::quiet_NaN(), amrex::Real q_s_mean=std::numeric_limits< amrex::Real >::quiet_NaN(), amrex::Real q_s_max=std::numeric_limits< amrex::Real >::quiet_NaN())
Append a diagnostic row to the CSV file.
Definition: ERF_RadiationDiagnostics.cpp:79
bool supplies_canopy_forcing(int lev) const noexcept
True when supply_canopy_forcing() was called for lev since its last define_level().
Definition: ERF_TwoStreamRadiation.H:263
amrex::Vector< std::unique_ptr< amrex::MultiFab > > m_canopy_cosz
Definition: ERF_TwoStreamRadiation.H:359
bool supplies_land_forcing(int lev) const noexcept
True when define_level allocated the land-model forcing fields on lev.
Definition: ERF_TwoStreamRadiation.H:213
amrex::Vector< std::unique_ptr< amrex::MultiFab > > m_q_sfc
Definition: ERF_TwoStreamRadiation.H:342
amrex::Vector< std::unique_ptr< amrex::MultiFab > > m_canopy
Definition: ERF_TwoStreamRadiation.H:358
amrex::Vector< std::unique_ptr< amrex::MultiFab > > m_grdflx_sfc
Definition: ERF_TwoStreamRadiation.H:341
amrex::Vector< std::unique_ptr< amrex::MultiFab > > m_lw_flux_sfc
Definition: ERF_TwoStreamRadiation.H:338
amrex::Vector< std::unique_ptr< amrex::MultiFab > > m_q_deep
Definition: ERF_TwoStreamRadiation.H:344
amrex::Real m_rdOcp
Definition: ERF_TwoStreamRadiation.H:321
amrex::Vector< FluxDiag > m_flux_diag
Definition: ERF_TwoStreamRadiation.H:363
bool active() const
True once a level has been defined with the model selected.
Definition: ERF_TwoStreamRadiation.H:57
amrex::Vector< std::unique_ptr< amrex::MultiFab > > m_emiss_lw
Definition: ERF_TwoStreamRadiation.H:333
amrex::Vector< std::unique_ptr< amrex::MultiFab > > m_t_sfc
Definition: ERF_TwoStreamRadiation.H:334
amrex::Vector< std::unique_ptr< amrex::MultiFab > > m_lw_dn_sfc
Definition: ERF_TwoStreamRadiation.H:352
amrex::Vector< std::unique_ptr< amrex::MultiFab > > m_lh_sfc
Definition: ERF_TwoStreamRadiation.H:340
amrex::Vector< std::unique_ptr< amrex::MultiFab > > m_alb_sw
Definition: ERF_TwoStreamRadiation.H:332
amrex::Vector< std::unique_ptr< amrex::MultiFab > > m_sw_flux_sfc
Definition: ERF_TwoStreamRadiation.H:337
OrbitalCache m_orbit
Definition: ERF_TwoStreamRadiation.H:322
amrex::Vector< std::unique_ptr< amrex::MultiFab > > m_t_deep
Definition: ERF_TwoStreamRadiation.H:343
amrex::Vector< std::unique_ptr< RadiationDiagnostics > > m_diag
Definition: ERF_TwoStreamRadiation.H:329
amrex::Vector< std::unique_ptr< amrex::MultiFab > > m_sw_dn_sfc
Definition: ERF_TwoStreamRadiation.H:351
amrex::Vector< int > m_canopy_step
Definition: ERF_TwoStreamRadiation.H:361
amrex::Vector< std::unique_ptr< amrex::MultiFab > > m_hfx_sfc
Definition: ERF_TwoStreamRadiation.H:339
amrex::Vector< int > m_canopy_m
Definition: ERF_TwoStreamRadiation.H:360
amrex::Vector< std::unique_ptr< amrex::MultiFab > > m_cos_zenith
Definition: ERF_TwoStreamRadiation.H:353
@ rho
Definition: ERF_Kessler.H:25
constexpr int F_DIR
Definition: ERF_TwoStreamColumn.H:602
constexpr int NCOMP
Definition: ERF_TwoStreamColumn.H:608
Container holding radiation-related choices and parameters.
Definition: ERF_RadStruct.H:86
bool sw_enabled
Enable shortwave (solar) radiation computation. Only used by the two-stream model.
Definition: ERF_RadStruct.H:98
std::string diag_file
Output file path for radiation diagnostics CSV. Paths are relative to the simulation working director...
Definition: ERF_RadStruct.H:222
amrex::Real surface_albedo_sw
Shortwave surface albedo fallback [0, 1] for per-column heterogeneous-surface-property resolution....
Definition: ERF_RadStruct.H:356
amrex::Real surface_emissivity_lw
Longwave surface emissivity fallback [0, 1] for per-column heterogeneous-surface-property resolution....
Definition: ERF_RadStruct.H:371
amrex::Real seb_grdflx_default
Fallback SEB ground heat flux [W/m^2]. The force-restore restoring term already represents the flux i...
Definition: ERF_RadStruct.H:512
amrex::Real diag_dedup_tol
Tolerance for time equality in duplicate guard [s]. Times within this tolerance are considered equal....
Definition: ERF_RadStruct.H:273
amrex::Real seb_restore_timescale_s
Force-restore timescale for surface temperature [s]. Relaxation time constant toward T_deep in progno...
Definition: ERF_RadStruct.H:569
std::string diag_callsite_mode
Call-site filtering mode: "both" (pre+post rows), "pre_only", or "post_only".
Definition: ERF_RadStruct.H:266
bool diag_regtest_line_enable
Enable RADIATION_DIAG: line emission. Controls regtest-parsing diagnostics line. Default true.
Definition: ERF_RadStruct.H:253
amrex::Real seb_lh_default
Fallback SEB latent heat flux [W/m^2].
Definition: ERF_RadStruct.H:505
amrex::Real seb_moisture_restore_timescale_s
Force-restore timescale for surface moisture [s]. Relaxation time constant toward q_deep in prognosti...
Definition: ERF_RadStruct.H:585
amrex::Real seb_lw_flux_default
Fallback SEB surface net longwave flux [W/m^2].
Definition: ERF_RadStruct.H:397
amrex::Real seb_q_sfc_default
Fallback SEB surface moisture, and the initial value of the prognostic one. With seb_prognostic_enabl...
Definition: ERF_RadStruct.H:520
bool diag_csv_enable
Enable CSV file append behavior. When false, no file writes; other enabled streams may still print....
Definition: ERF_RadStruct.H:260
amrex::Real seb_t_deep_default
Fallback SEB deep soil temperature [K].
Definition: ERF_RadStruct.H:525
amrex::Real seb_prognostic_t_max_k
Maximum clamping bound for prognostic surface temperature [K]. After each Euler update,...
Definition: ERF_RadStruct.H:602
amrex::Real rad_t_sfc
erf.rad_t_sfc: surface temperature [K] used as the longwave boundary condition wherever no land-surfa...
Definition: ERF_RadStruct.H:151
bool seb_prognostic_enable
Enable prognostic SEB surface temperature and moisture evolution. When true, integrates SEB residual ...
Definition: ERF_RadStruct.H:550
bool seb_use_radiation_fluxes
Take the SEB's net surface shortwave and longwave fluxes from the two-stream sweep itself (per column...
Definition: ERF_RadStruct.H:387
amrex::Real seb_prognostic_t_min_k
Minimum clamping bound for prognostic surface temperature [K]. After each Euler update,...
Definition: ERF_RadStruct.H:594
amrex::Real cloud_fraction
Cloud fraction in [0, 1] used to blend clear-sky and cloudy-column fluxes: F = (1 - cloud_fraction) *...
Definition: ERF_RadStruct.H:308
amrex::Real seb_prognostic_q_max
Maximum clamping bound for prognostic surface moisture [kg/kg]. After each Euler update,...
Definition: ERF_RadStruct.H:618
amrex::Real seb_moisture_layer_depth_m
Effective surface moisture layer depth [m]. Used in force-restore prognostic q_s equation: dq_s/dt = ...
Definition: ERF_RadStruct.H:577
SEBTurbulentFluxSource seb_turbulent_flux_source
Source of the SEB's sensible and latent heat fluxes when no land-surface model supplies them: "surfac...
Definition: ERF_RadStruct.H:404
amrex::Real seb_hfx_default
Fallback SEB sensible heat flux [W/m^2].
Definition: ERF_RadStruct.H:500
bool diag_tagged_enable
Enable tagged [RAD][...] debug lines. Controls emission of bracketed-tag debug output....
Definition: ERF_RadStruct.H:246
amrex::Real seb_q_deep_default
Deep soil moisture the prognostic q_s restores to, in q_s's units (see seb_q_sfc_default).
Definition: ERF_RadStruct.H:531
amrex::Real seb_sw_flux_default
Fallback SEB surface net shortwave flux [W/m^2].
Definition: ERF_RadStruct.H:392
bool diag_enable
Master switch for radiation diagnostics emission. When false, no diagnostics output (stdout/tagged/re...
Definition: ERF_RadStruct.H:232
amrex::Real seb_prognostic_q_min
Minimum clamping bound for prognostic surface moisture [kg/kg]. After each Euler update,...
Definition: ERF_RadStruct.H:610
bool seb_enable
Master switch for simplified SEB infrastructure. When false (default), no SEB MultiFabs are allocated...
Definition: ERF_RadStruct.H:377
bool diag_stdout_enable
Enable human-readable stdout diagnostics block. Controls tagged [RAD][...] debug line emission (when ...
Definition: ERF_RadStruct.H:239
int verbosity
Debug verbosity level (0=off, 1+=debug output). Gated debug prints only emit when this is >= 1.
Definition: ERF_RadStruct.H:216
amrex::Real seb_surface_heat_capacity
Effective surface heat capacity [J/(m^2*K)]. Used in force-restore prognostic T_s equation: dT_s/dt =...
Definition: ERF_RadStruct.H:561
bool seb_diagnostic_enable
Enable diagnostic SEB residual computation and reporting. When true, computes and reports SEB residua...
Definition: ERF_RadStruct.H:541
Trivially copyable subset of RadChoice used by the column kernels.
Definition: ERF_TwoStreamColumn.H:57
Here is the call graph for this function:

◆ canopy_forcing()

TwoStreamCanopyView TwoStreamRadiation::canopy_forcing ( int  lev,
int  nstep 
) const
inlinenoexcept

The beam and the cosine of the zenith of lev as the sweep of step nstep wrote them (the view's fluxes are left for the caller, who owns rad_fluxes), or an empty view when the level does not supply them or has not swept at that step: the faces must not take a previous step's radiation, nor the zeros of a level whose radiation came from its parent.

274  {
276  if (supplies_canopy_forcing(lev) && m_canopy_step[lev] == nstep) {
277  v.beam = m_canopy[lev].get();
278  v.cos_zenith = m_canopy_cosz[lev].get();
279  }
280  return v;
281  }
The fields a level's faces read for one step: the beam and the cosine of the zenith the sweep of that...
Definition: ERF_TwoStreamCanopyForcing.H:45
const amrex::MultiFab * beam
Direct beam on a horizontal surface, interfaces 0 .. m_top [W/m^2].
Definition: ERF_TwoStreamCanopyForcing.H:46
const amrex::MultiFab * cos_zenith
Cosine of the zenith of the sweep, floored at zero (2D) [1].
Definition: ERF_TwoStreamCanopyForcing.H:47
Here is the call graph for this function:

◆ define_level()

void TwoStreamRadiation::define_level ( int  lev,
const RadChoice &  rad_choice,
amrex::Real  rdOcp,
const amrex::BoxArray &  ba2d,
const amrex::DistributionMapping &  dm,
const amrex::BoxArray &  ba,
const amrex::Box &  domain,
bool  supply_land_forcing = false 
)

Allocate the 2D surface fields of one level on the given horizontal BoxArray and fill them with the scalar defaults. A no-op unless rad_choice.rad_type == TwoStream.

Also enforces the model's one grid requirement: every box of ba must span domain in z, because the column sweep applies the top-of-atmosphere and surface boundary conditions at the ends of a box. Aborting here means a bad refinement patch is rejected when the level is built (including on regrid), not at the first radiation call.

Parameters
levlevel
rad_choicethe model's inputs
ba2dhorizontal BoxArray the 2D surface fields live on
dmdistribution mapping shared with the level's state
bathe level's 3D BoxArray, checked for the z span
domainthe level's domain Box
supply_land_forcingallocate the downwelling SW, downwelling LW and cos(zenith) fields the sweep fills for a land-surface model (SolverChoice::rad_feeds_lsm()); off, the sweep does no extra work and the fields do not exist
266 {
267  if (!rad_choice.enabled) { return; }
268  m_rad = &rad_choice;
269  m_rdOcp = rdOcp;
270 
271  // The sweep integrates a whole column in one pass inside a single box, applying the
272  // top-of-atmosphere boundary condition above the top layer and the surface condition
273  // below the bottom one. So the requirement is per box: every box must span the domain
274  // in z. Levels that do not meet it come in several shapes -- a shallow nest, a level
275  // tagged at different heights in different horizontal regions, grids decomposed in z --
276  // and they all have the same consequence, that no box holds a column to sweep.
277  //
278  // Above level 0 that is not an error: ERF::advance_radiation interpolates the level's
279  // heating rates and fluxes from its parent, the route RRTMGP takes for a nested patch,
280  // and level_needs_interpolation() there applies this same per-box test so the two agree.
281  //
282  // On level 0 there is no parent to interpolate from, so it is fatal. Level 0 always
283  // covers the domain, which makes a box that does not span z there exactly the
284  // decomposed-in-z case -- and the remedies below are the ones that apply to it.
285  if (lev == 0) {
286  for (int ibox = 0; ibox < ba.size(); ++ibox) {
287  const Box& b = ba[ibox];
288  if (b.smallEnd(2) != domain.smallEnd(2) || b.bigEnd(2) != domain.bigEnd(2)) {
289  amrex::Abort("erf.radiation_model = TwoStream cannot run on a level 0 whose "
290  "grids are decomposed in z: a box there holds only part of a "
291  "column, and level 0 has no parent to interpolate from. Set "
292  "amr.max_grid_size_z to at least the number of cells in z, or "
293  "leave amr.no_box_split_dir at its default of 2 so grids are "
294  "never split in z.");
295  }
296  }
297  }
298 
299  // 2D surface fields on the horizontal BoxArray, one ghost cell in x and y
300  const IntVect ng_sfc{1,1,0};
301  m_alb_sw[lev] = std::make_unique<MultiFab>(ba2d, dm, 1, ng_sfc);
302  m_emiss_lw[lev] = std::make_unique<MultiFab>(ba2d, dm, 1, ng_sfc);
303  m_t_sfc[lev] = std::make_unique<MultiFab>(ba2d, dm, 1, ng_sfc);
304  m_alb_sw[lev]->setVal(rad_choice.surface_albedo_sw);
305  m_emiss_lw[lev]->setVal(rad_choice.surface_emissivity_lw);
306  m_t_sfc[lev]->setVal(rad_choice.rad_t_sfc);
307 
308  if (rad_choice.seb_enable) {
309  m_sw_flux_sfc[lev] = std::make_unique<MultiFab>(ba2d, dm, 1, ng_sfc);
310  m_lw_flux_sfc[lev] = std::make_unique<MultiFab>(ba2d, dm, 1, ng_sfc);
311  m_hfx_sfc[lev] = std::make_unique<MultiFab>(ba2d, dm, 1, ng_sfc);
312  m_lh_sfc[lev] = std::make_unique<MultiFab>(ba2d, dm, 1, ng_sfc);
313  m_grdflx_sfc[lev] = std::make_unique<MultiFab>(ba2d, dm, 1, ng_sfc);
314  m_q_sfc[lev] = std::make_unique<MultiFab>(ba2d, dm, 1, ng_sfc);
315  m_t_deep[lev] = std::make_unique<MultiFab>(ba2d, dm, 1, ng_sfc);
316  m_q_deep[lev] = std::make_unique<MultiFab>(ba2d, dm, 1, ng_sfc);
317  m_sw_flux_sfc[lev]->setVal(rad_choice.seb_sw_flux_default);
318  m_lw_flux_sfc[lev]->setVal(rad_choice.seb_lw_flux_default);
319  m_hfx_sfc[lev]->setVal(rad_choice.seb_hfx_default);
320  m_lh_sfc[lev]->setVal(rad_choice.seb_lh_default);
321  m_grdflx_sfc[lev]->setVal(rad_choice.seb_grdflx_default);
322  m_q_sfc[lev]->setVal(rad_choice.seb_q_sfc_default);
323  m_t_deep[lev]->setVal(rad_choice.seb_t_deep_default);
324  m_q_deep[lev]->setVal(rad_choice.seb_q_deep_default);
325  if (rad_choice.seb_surface_layer_uses_moisture) {
326  m_moisture[lev] = std::make_unique<MultiFab>(ba2d, dm,
328  m_moisture[lev]->setVal(0.0);
329  } else {
330  m_moisture[lev].reset();
331  }
332  }
333 
334  // The land-model forcing (see the members). It holds the lsm_undefined sentinel until a
335  // sweep on this level fills it. Zero would be a valid forcing (night, no sky), so a copy
336  // made before any sweep -- a level whose advance() returned early, or a caller that
337  // drifts out of step with the sweep -- would hand the land model a 0 K sky that its
338  // first-land-step check (NOAHMP::Advance_With_State) accepts. The sentinel is what
339  // that check looks for.
340  if (supply_land_forcing) {
341  m_sw_dn_sfc[lev] = std::make_unique<MultiFab>(ba2d, dm, 1, 0);
342  m_lw_dn_sfc[lev] = std::make_unique<MultiFab>(ba2d, dm, 1, 0);
343  m_cos_zenith[lev] = std::make_unique<MultiFab>(ba2d, dm, 1, 0);
344  m_sw_dn_sfc[lev]->setVal(lsm_undefined);
345  m_lw_dn_sfc[lev]->setVal(lsm_undefined);
346  m_cos_zenith[lev]->setVal(lsm_undefined);
347  } else {
348  m_sw_dn_sfc[lev].reset();
349  m_lw_dn_sfc[lev].reset();
350  m_cos_zenith[lev].reset();
351  }
352  // A rebuilt level has new grids: the canopy forcing, if the faces asked for it,
353  // must be asked for again on them (the faces do not regrid; ERF::init_ibseb()).
354  m_canopy[lev].reset();
355  m_canopy_cosz[lev].reset();
356  m_canopy_m[lev] = -1;
357  m_canopy_step[lev] = -1;
358  m_flux_diag[lev] = FluxDiag{};
359 }
constexpr amrex::Real lsm_undefined
Definition: ERF_Constants.H:28
const Real rdOcp
Definition: ERF_InitCustomPert_ABL.H:72
amrex::Vector< std::unique_ptr< amrex::MultiFab > > m_moisture
Definition: ERF_TwoStreamRadiation.H:345
@ NumComponents
Definition: ERF_SurfaceMoisture.H:37
bool seb_surface_layer_uses_moisture
Two-way coupling of moisture as well: the surface layer takes its land surface mixing ratio from the ...
Definition: ERF_RadStruct.H:430

◆ land_forcing_cos_zenith()

const amrex::MultiFab* TwoStreamRadiation::land_forcing_cos_zenith ( int  lev) const
inlinenoexcept
230  {
231  return supplies_land_forcing(lev) ? m_cos_zenith[lev].get() : nullptr;
232  }
Here is the call graph for this function:

◆ land_forcing_lw_dn()

const amrex::MultiFab* TwoStreamRadiation::land_forcing_lw_dn ( int  lev) const
inlinenoexcept
227  {
228  return supplies_land_forcing(lev) ? m_lw_dn_sfc[lev].get() : nullptr;
229  }
Here is the call graph for this function:

◆ land_forcing_sw_dn()

const amrex::MultiFab* TwoStreamRadiation::land_forcing_sw_dn ( int  lev) const
inlinenoexcept

The surface forcing of the last sweep on lev (2D: i,j), or nullptr when the level does not supply it: total downwelling shortwave [W/m^2], downwelling longwave [W/m^2] and the cosine of the solar zenith angle floored at zero [1]. lsm_undefined until a sweep has run on the level, so a copy made before one is caught by the land model's missing-input check instead of reading as zero.

224  {
225  return supplies_land_forcing(lev) ? m_sw_dn_sfc[lev].get() : nullptr;
226  }
Here is the call graph for this function:

◆ prognostic_surface_temperature_state()

const amrex::MultiFab* TwoStreamRadiation::prognostic_surface_temperature_state ( int  lev) const
inlinenoexcept

Read-only view of the per-level prognostic surface-temperature state.

62  {
63  return (lev >= 0 && lev < static_cast<int>(m_t_sfc.size()))
64  ? m_t_sfc[lev].get() : nullptr;
65  }

◆ read_checkpoint()

void TwoStreamRadiation::read_checkpoint ( int  lev,
const std::string &  restart_chkfile 
)

Read it back when the checkpoint carries it; else keep defaults.

517 {
518  // Older checkpoints do not carry the state; then the defaults set by
519  // define_level stand. Every level, matching write_checkpoint. A checkpoint
520  // written before the SEB ran on fine levels simply has no file for them, which
521  // the FileExists test below already handles.
522  if (!active() || !m_rad->seb_enable) { return; }
523  const std::string tsfc_name =
524  MultiFabFileFullPrefix(lev, restart_chkfile, "Level_", "TwoStream_TSfc");
525  if (m_t_sfc[lev] && amrex::FileExists(tsfc_name + "_H")) {
526  VisMF::Read(*m_t_sfc[lev], tsfc_name);
527  }
528  const std::string qsfc_name =
529  MultiFabFileFullPrefix(lev, restart_chkfile, "Level_", "TwoStream_QSfc");
530  if (m_q_sfc[lev] && amrex::FileExists(qsfc_name + "_H")) {
531  VisMF::Read(*m_q_sfc[lev], qsfc_name);
532  }
533 }

◆ resize()

void TwoStreamRadiation::resize ( int  nlevs_max)

Size the per-level containers; no allocation happens here.

233 {
234  m_alb_sw.resize(nlevs_max);
235  m_emiss_lw.resize(nlevs_max);
236  m_t_sfc.resize(nlevs_max);
237  m_sw_flux_sfc.resize(nlevs_max);
238  m_lw_flux_sfc.resize(nlevs_max);
239  m_hfx_sfc.resize(nlevs_max);
240  m_lh_sfc.resize(nlevs_max);
241  m_grdflx_sfc.resize(nlevs_max);
242  m_sw_dn_sfc.resize(nlevs_max);
243  m_lw_dn_sfc.resize(nlevs_max);
244  m_cos_zenith.resize(nlevs_max);
245  m_q_sfc.resize(nlevs_max);
246  m_t_deep.resize(nlevs_max);
247  m_q_deep.resize(nlevs_max);
248  m_moisture.resize(nlevs_max);
249  m_canopy.resize(nlevs_max);
250  m_canopy_cosz.resize(nlevs_max);
251  m_canopy_m.resize(nlevs_max, -1);
252  m_canopy_step.resize(nlevs_max, -1);
253  m_flux_diag.resize(nlevs_max);
254  m_diag.resize(nlevs_max);
255 }

Referenced by ERF::ERF_shared().

Here is the caller graph for this function:

◆ seb_hfx()

const amrex::MultiFab* TwoStreamRadiation::seb_hfx ( int  lev) const
inline

The sensible and latent heat fluxes [W/m^2] the surface energy balance of one level last used, or nullptr when the SEB is off or the level has not been built. Read-only: for output.

184  {
185  return (active() && m_rad->seb_enable && lev < m_hfx_sfc.size()) ? m_hfx_sfc[lev].get() : nullptr;
186  }
Here is the call graph for this function:

◆ seb_lh()

const amrex::MultiFab* TwoStreamRadiation::seb_lh ( int  lev) const
inline
187  {
188  return (active() && m_rad->seb_enable && lev < m_lh_sfc.size()) ? m_lh_sfc[lev].get() : nullptr;
189  }
Here is the call graph for this function:

◆ seb_prognostic_active()

bool TwoStreamRadiation::seb_prognostic_active ( ) const
inline

True when the force-restore state is being evolved and so needs transferring.

208  {
210  }
Here is the call graph for this function:

◆ seb_q_sfc()

amrex::MultiFab* TwoStreamRadiation::seb_q_sfc ( int  lev)
inline
175  {
176  return (active() && m_rad->seb_enable && lev < m_q_sfc.size()) ? m_q_sfc[lev].get() : nullptr;
177  }
Here is the call graph for this function:

◆ seb_surface_moisture()

const MultiFab * TwoStreamRadiation::seb_surface_moisture ( int  lev)

What level lev's surface hands the surface layer for its surface mixing ratio (erf.radiation.seb_surface_layer_uses_moisture), per column, in the components of erf_surface_moisture::Component: the soil-water factor (seb_moisture_availability of the current q_s); with erf.radiation.seb_soil_type, the bare-soil resistance and the relative humidity of its pore air at the skin temperature; and with erf.radiation.seb_vegetation_type as well, the vegetated fraction and the canopy resistance without its vapour-deficit factor (from the net shortwave the balance holds divided by (1 - surface albedo) – the sweep's with seb_use_radiation_fluxes, seb_sw_flux_default otherwise – and the skin temperature). Absent pieces are a fraction of 0 and resistances of 1e6 s/m. nullptr when the option is off or the level has not been built. Recomputed on every call, on the valid cells; the halo is not filled.

406 {
408  lev >= static_cast<int>(m_moisture.size()) || !m_moisture[lev] || !m_q_sfc[lev]) {
409  return nullptr;
410  }
411  using namespace erf_surface_moisture;
412  const Real q_wilt = m_rad->seb_soil_moisture_wilt;
413  const Real q_fc = m_rad->seb_soil_moisture_fc;
414 
415  // With a soil type: the soil's parameters for the bare-soil resistance of the top
416  // seb_moisture_layer_depth_m, vegetation or not. With a vegetation type as well (which
417  // needs a soil type): Noah-MP's canopy parameters. RadChoice::init_params checked both
418  // categories.
421  const NoahMPSoilParams* soil = (m_rad->seb_soil_type != 0)
422  ? noahmp_soil_params(m_rad->seb_soil_type) : nullptr;
423  const bool has_soil = (soil != nullptr);
424  const bool vegetated = (veg != nullptr && has_soil);
425  const Real f_veg = vegetated ? m_rad->seb_vegetation_fraction : Real(0.0);
427  const Real rs_min = vegetated ? veg->rs_min : Real(0.0);
428  const Real rs_max = vegetated ? veg->rs_max : Real(0.0);
429  const Real rgl = vegetated ? veg->rgl : Real(0.0);
430  const Real t_opt = vegetated ? veg->t_opt : Real(0.0);
431  const Real smc_max = has_soil ? soil->smc_max : Real(0.0);
432  const Real bb = has_soil ? soil->bb : Real(0.0);
433  const Real psi_sat = has_soil ? soil->psi_sat : Real(0.0);
435  const Real resistance_exponent = noahmp_soil_resistance_exponent;
436 
437  MultiFab& out = *m_moisture[lev];
438  const MultiFab& q_s = *m_q_sfc[lev];
439  const MultiFab& t_s = *m_t_sfc[lev];
440  // The shortwave down at the surface, from the net the balance holds and the albedo.
441  const MultiFab* sw_net = m_sw_flux_sfc[lev].get();
442  const MultiFab* albedo = m_alb_sw[lev].get();
443 #ifdef _OPENMP
444 #pragma omp parallel if (Gpu::notInLaunchRegion())
445 #endif
446  for (MFIter mfi(out, TilingIfNotGPU()); mfi.isValid(); ++mfi) {
447  const Box& bx = mfi.tilebox();
448  const auto out_arr = out.array(mfi);
449  const auto q_arr = q_s.const_array(mfi);
450  const auto t_arr = t_s.const_array(mfi);
451  const auto sw_arr = (vegetated && sw_net) ? sw_net->const_array(mfi) : Array4<const Real>{};
452  const auto alb_arr = (vegetated && albedo) ? albedo->const_array(mfi) : Array4<const Real>{};
453  ParallelFor(bx, [=] AMREX_GPU_DEVICE (int i, int j, int k) noexcept
454  {
455  const Real soil_factor = seb_moisture_availability(q_arr(i, j, k), q_wilt, q_fc);
456  out_arr(i, j, k, SoilFactor) = soil_factor;
457  out_arr(i, j, k, VegetationFraction) = f_veg;
459  Real r_soil = no_flux_resistance;
460  if (vegetated) {
461  const Real a = (alb_arr) ? alb_arr(i, j, k) : Real(0.0);
462  const Real sw_down = (sw_arr && a < Real(1.0)) ? sw_arr(i, j, k) / (Real(1.0) - a) : Real(0.0);
463  r_c = seb_canopy_resistance_without_vpd(rs_min, rs_max, rgl, t_opt, lai, sw_down,
464  t_arr(i, j, k), soil_factor);
465  }
466  Real rh_ground = Real(1.0);
467  if (has_soil) {
468  r_soil = seb_soil_evaporation_resistance(q_arr(i, j, k), smc_max, q_wilt, bb, d1,
469  resistance_exponent);
470  rh_ground = seb_soil_surface_relative_humidity(q_arr(i, j, k), smc_max, psi_sat,
471  bb, t_arr(i, j, k));
472  }
473  out_arr(i, j, k, CanopyResistanceWithoutVPD) = r_c;
474  out_arr(i, j, k, SoilResistance) = r_soil;
475  out_arr(i, j, k, GroundRelativeHumidity) = rh_ground;
476  });
477  }
478  return m_moisture[lev].get();
479 }
Real r_c
Definition: ERF_InitCustomPert_SquallLine.H:49
ParallelFor(fab_box, [=] AMREX_GPU_DEVICE(int i, int j, int k) { qrcuten_arr(i, j, k)=Real(0);qscuten_arr(i, j, k)=Real(0);qicuten_arr(i, j, k)=Real(0);})
constexpr amrex::Real noahmp_soil_resistance_exponent
Definition: ERF_NoahMPSoilTable.H:39
const NoahMPSoilParams * noahmp_soil_params(int category) noexcept
The parameters of STAS soil category category (1-19), as Noah-MP reads them.
Definition: ERF_NoahMPSoilTable.H:49
const NoahMPVegetationParams * noahmp_vegetation_params(int category) noexcept
The parameters of MODIS land-use category category (1-20), as Noah-MP reads them.
Definition: ERF_NoahMPVegetationTable.H:41
AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE amrex::Real seb_soil_evaporation_resistance(amrex::Real theta, amrex::Real theta_sat, amrex::Real theta_wilt, amrex::Real b, amrex::Real d1, amrex::Real p)
Resistance of bare soil to evaporation [s/m], Noah-MP's ground-evaporation option 1 (Sakaguchi and Ze...
Definition: ERF_SimplifiedSEB.H:288
AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE amrex::Real seb_moisture_availability(amrex::Real q_s, amrex::Real q_wilt, amrex::Real q_fc)
Moisture availability beta of the force-restore surface, from its water content.
Definition: ERF_SimplifiedSEB.H:218
AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE amrex::Real seb_soil_surface_relative_humidity(amrex::Real theta, amrex::Real theta_sat, amrex::Real psi_sat, amrex::Real b, amrex::Real t_ground)
Relative humidity of the air in the top soil's pores, Noah-MP's for bare ground (ResistanceGroundEvap...
Definition: ERF_SimplifiedSEB.H:318
AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE amrex::Real seb_canopy_resistance_without_vpd(amrex::Real rs_min, amrex::Real rs_max, amrex::Real rgl, amrex::Real t_opt, amrex::Real lai, amrex::Real sw_down, amrex::Real t_canopy, amrex::Real soil_factor)
Canopy resistance to transpiration [s/m] without its vapour-deficit factor.
Definition: ERF_SimplifiedSEB.H:254
@ lai
Definition: ERF_SLM.H:76
Definition: ERF_SurfaceMoisture.H:21
constexpr amrex::Real no_flux_resistance
Definition: ERF_SurfaceMoisture.H:79
@ GroundRelativeHumidity
Definition: ERF_SurfaceMoisture.H:36
@ SoilResistance
Definition: ERF_SurfaceMoisture.H:35
@ VegetationFraction
Definition: ERF_SurfaceMoisture.H:33
@ CanopyResistanceWithoutVPD
Definition: ERF_SurfaceMoisture.H:34
@ SoilFactor
Definition: ERF_SurfaceMoisture.H:32
Definition: ERF_NoahMPSoilTable.H:20
amrex::Real bb
BB Clapp-Hornberger b [1].
Definition: ERF_NoahMPSoilTable.H:21
amrex::Real psi_sat
SATPSI saturated matric potential [m].
Definition: ERF_NoahMPSoilTable.H:25
amrex::Real smc_max
MAXSMC saturated soil moisture (porosity) [m^3/m^3].
Definition: ERF_NoahMPSoilTable.H:23
Definition: ERF_NoahMPVegetationTable.H:23
amrex::Real t_opt
TOPT optimum transpiration air temperature [K].
Definition: ERF_NoahMPVegetationTable.H:27
amrex::Real rs_min
RS minimum stomatal resistance [s/m].
Definition: ERF_NoahMPVegetationTable.H:24
amrex::Real rgl
RGL radiation stress parameter [W/m^2].
Definition: ERF_NoahMPVegetationTable.H:25
amrex::Real rs_max
RSMAX maximum stomatal resistance [s/m].
Definition: ERF_NoahMPVegetationTable.H:28
int seb_vegetation_type
Noah-MP land-use category (modified IGBP MODIS, 1-20 except the unvegetated 15, 16 and 17,...
Definition: ERF_RadStruct.H:471
amrex::Real seb_soil_moisture_wilt
Wilting point [m^3/m^3]: at or below it the soil-water factor is 0, so the vegetation does not transp...
Definition: ERF_RadStruct.H:439
amrex::Real seb_leaf_area_index
Leaf area index [m^2/m^2] of the vegetated fraction (seb_vegetation_type; giving it otherwise stops a...
Definition: ERF_RadStruct.H:486
int seb_soil_type
Noah-MP soil category (STAS, 1-19, not 14 (water), as in NoahmpTable.TBL; see ERF_NoahMPSoilTable....
Definition: ERF_RadStruct.H:459
amrex::Real seb_soil_moisture_fc
Field capacity [m^3/m^3]: at or above it the soil-water factor is 1, so without seb_soil_type the sur...
Definition: ERF_RadStruct.H:447
amrex::Real seb_vegetation_fraction
Vegetated fraction of the surface [0, 1] (seb_vegetation_type; giving it otherwise stops at start-up)...
Definition: ERF_RadStruct.H:477
Here is the call graph for this function:

◆ seb_t_sfc()

amrex::MultiFab* TwoStreamRadiation::seb_t_sfc ( int  lev)
inline

The prognostic surface state of one level, or nullptr when the SEB is off or the level has not been built.

Exposed so ERF can average t_sfc and q_sfc down after the finer levels have advanced. Every level evolves its own copy, and without that transfer the two estimates of one surface drift apart – which would give the longwave boundary condition two different values for the same ground. ERF owns refRatio and the level geometries; this class does not, and should not grow them for one call.

172  {
173  return (active() && m_rad->seb_enable && lev < m_t_sfc.size()) ? m_t_sfc[lev].get() : nullptr;
174  }
Here is the call graph for this function:

◆ supplies_canopy_forcing()

bool TwoStreamRadiation::supplies_canopy_forcing ( int  lev) const
inlinenoexcept

True when supply_canopy_forcing() was called for lev since its last define_level().

263  {
264  return lev >= 0 && lev < static_cast<int>(m_canopy.size()) && m_canopy[lev] != nullptr;
265  }

Referenced by canopy_forcing().

Here is the caller graph for this function:

◆ supplies_land_forcing()

bool TwoStreamRadiation::supplies_land_forcing ( int  lev) const
inlinenoexcept

True when define_level allocated the land-model forcing fields on lev.

213  {
214  return lev >= 0 && lev < static_cast<int>(m_sw_dn_sfc.size()) && m_sw_dn_sfc[lev] != nullptr;
215  }

Referenced by land_forcing_cos_zenith(), land_forcing_lw_dn(), and land_forcing_sw_dn().

Here is the caller graph for this function:

◆ supply_canopy_forcing()

void TwoStreamRadiation::supply_canopy_forcing ( int  lev,
int  m_top 
)

Have every later sweep on lev write what the building faces need besides the interface fluxes (erf.ibseb.radiation = two_stream; ERF_TwoStreamCanopyForcing.H): the direct beam on a horizontal surface at the interfaces 0 .. m_top of each column, counted from the surface, and the cosine of the zenith. Allocates one field on the level's horizontal grids given those interfaces in z and one 2D field. Call after define_level(); a later define_level() of the level (a regrid) drops the request.

Parameters
levlevel, which must have been defined; its sweep asserts that the columns reach m_top
m_tophighest interface the faces read (IBFaceSet::top_sample_interface()), in [0, number of cells in z]
363 {
365  "TwoStreamRadiation::supply_canopy_forcing: erf.radiation_model is not TwoStream");
366  AMREX_ALWAYS_ASSERT_WITH_MESSAGE(lev >= 0 && lev < static_cast<int>(m_alb_sw.size()) && m_alb_sw[lev],
367  "TwoStreamRadiation::supply_canopy_forcing: level " + std::to_string(lev) + " has not been defined");
369  "TwoStreamRadiation::supply_canopy_forcing: the top of the canopy must be at or above the surface");
370  // The level's grids flattened (the 2D fields' boxes, on their distribution), given the
371  // interfaces 0 .. m_top in z; the sweep asserts that the column reaches m_top.
372  BoxList canopy_boxes = m_alb_sw[lev]->boxArray().boxList();
373  for (Box& b : canopy_boxes) { b.setRange(2, 0, m_top + 1); }
374  // The beam alone: the diffuse light, the longwave and the ground's fluxes are the
375  // sweep's interface fluxes, which ERF keeps (rad_fluxes); only the beam and the
376  // cosine of the zenith are lost after the sweep.
377  m_canopy[lev] = std::make_unique<MultiFab>(BoxArray(std::move(canopy_boxes)), m_alb_sw[lev]->DistributionMap(), 1, 0);
378  m_canopy_cosz[lev] = std::make_unique<MultiFab>(m_alb_sw[lev]->boxArray(), m_alb_sw[lev]->DistributionMap(), 1, 0);
379  // Never read before a sweep has written them: canopy_forcing() returns null until then.
380  m_canopy[lev]->setVal(0.0);
381  m_canopy_cosz[lev]->setVal(0.0);
382  m_canopy_m[lev] = m_top;
383  m_canopy_step[lev] = -1;
384 }
Here is the call graph for this function:

◆ write_checkpoint()

void TwoStreamRadiation::write_checkpoint ( int  lev,
const std::string &  checkpointname 
) const

Write the prognostic surface state of one level (SEB on).

497 {
498  // The force-restore state is the only part of this model a restart must
499  // carry; without it T_s and q_s restart from the scalar defaults.
500  //
501  // Every level: each one evolves its own force-restore state now, so each has
502  // state worth restoring. A coarse level's values where a finer level covers it
503  // are the average of that finer level's, restored on both sides.
504  if (!active() || !m_rad->seb_enable) { return; }
505  if (m_t_sfc[lev]) {
506  VisMF::Write(*m_t_sfc[lev],
507  MultiFabFileFullPrefix(lev, checkpointname, "Level_", "TwoStream_TSfc"));
508  }
509  if (m_q_sfc[lev]) {
510  VisMF::Write(*m_q_sfc[lev],
511  MultiFabFileFullPrefix(lev, checkpointname, "Level_", "TwoStream_QSfc"));
512  }
513 }

◆ write_land_forcing()

void TwoStreamRadiation::write_land_forcing ( int  lev,
amrex::MultiFab *  sw_flux_dn,
amrex::MultiFab *  lw_flux_dn,
amrex::MultiFab *  cos_zenith_angle 
) const

Copy the surface forcing of the last sweep on lev into a land-surface model's radiation inputs (SurfaceModel's "sw_flux_dn", "lw_flux_dn" and "cos_zenith_angle" outputs). A null destination is skipped. Each destination must lie on this level's horizontal grids; see copy_surface_plane. Before the level's first sweep this copies the lsm_undefined sentinel, not zero.

486 {
488  "TwoStreamRadiation::write_land_forcing: level " + std::to_string(lev) +
489  " was not defined to supply a land-surface model's forcing");
490  if (sw_flux_dn != nullptr) { copy_surface_plane(*m_sw_dn_sfc[lev], *sw_flux_dn); }
491  if (lw_flux_dn != nullptr) { copy_surface_plane(*m_lw_dn_sfc[lev], *lw_flux_dn); }
492  if (cos_zenith_angle != nullptr) { copy_surface_plane(*m_cos_zenith[lev], *cos_zenith_angle); }
493 }
void copy_surface_plane(const MultiFab &src2d, MultiFab &dst)
Definition: ERF_TwoStreamRadiation.cpp:387
Here is the call graph for this function:

Member Data Documentation

◆ m_alb_sw

amrex::Vector<std::unique_ptr<amrex::MultiFab> > TwoStreamRadiation::m_alb_sw
private

◆ m_canopy

amrex::Vector<std::unique_ptr<amrex::MultiFab> > TwoStreamRadiation::m_canopy
private

◆ m_canopy_cosz

amrex::Vector<std::unique_ptr<amrex::MultiFab> > TwoStreamRadiation::m_canopy_cosz
private

Referenced by canopy_forcing().

◆ m_canopy_m

amrex::Vector<int> TwoStreamRadiation::m_canopy_m
private

◆ m_canopy_step

amrex::Vector<int> TwoStreamRadiation::m_canopy_step
private

Referenced by canopy_forcing().

◆ m_cos_zenith

amrex::Vector<std::unique_ptr<amrex::MultiFab> > TwoStreamRadiation::m_cos_zenith
private

Referenced by land_forcing_cos_zenith().

◆ m_diag

amrex::Vector<std::unique_ptr<RadiationDiagnostics> > TwoStreamRadiation::m_diag
private

◆ m_emiss_lw

amrex::Vector<std::unique_ptr<amrex::MultiFab> > TwoStreamRadiation::m_emiss_lw
private

◆ m_flux_diag

amrex::Vector<FluxDiag> TwoStreamRadiation::m_flux_diag
private

◆ m_grdflx_sfc

amrex::Vector<std::unique_ptr<amrex::MultiFab> > TwoStreamRadiation::m_grdflx_sfc
private

◆ m_hfx_sfc

amrex::Vector<std::unique_ptr<amrex::MultiFab> > TwoStreamRadiation::m_hfx_sfc
private

Referenced by seb_hfx().

◆ m_lh_sfc

amrex::Vector<std::unique_ptr<amrex::MultiFab> > TwoStreamRadiation::m_lh_sfc
private

Referenced by seb_lh().

◆ m_lw_dn_sfc

amrex::Vector<std::unique_ptr<amrex::MultiFab> > TwoStreamRadiation::m_lw_dn_sfc
private

Referenced by land_forcing_lw_dn().

◆ m_lw_flux_sfc

amrex::Vector<std::unique_ptr<amrex::MultiFab> > TwoStreamRadiation::m_lw_flux_sfc
private

◆ m_moisture

amrex::Vector<std::unique_ptr<amrex::MultiFab> > TwoStreamRadiation::m_moisture
private

◆ m_orbit

OrbitalCache TwoStreamRadiation::m_orbit
private

◆ m_q_deep

amrex::Vector<std::unique_ptr<amrex::MultiFab> > TwoStreamRadiation::m_q_deep
private

◆ m_q_sfc

amrex::Vector<std::unique_ptr<amrex::MultiFab> > TwoStreamRadiation::m_q_sfc
private

Referenced by seb_q_sfc().

◆ m_rad

const RadChoice* TwoStreamRadiation::m_rad = nullptr
private

◆ m_rdOcp

amrex::Real TwoStreamRadiation::m_rdOcp = RdoCp
private

◆ m_sw_dn_sfc

amrex::Vector<std::unique_ptr<amrex::MultiFab> > TwoStreamRadiation::m_sw_dn_sfc
private

◆ m_sw_flux_sfc

amrex::Vector<std::unique_ptr<amrex::MultiFab> > TwoStreamRadiation::m_sw_flux_sfc
private

◆ m_t_deep

amrex::Vector<std::unique_ptr<amrex::MultiFab> > TwoStreamRadiation::m_t_deep
private

◆ m_t_sfc

amrex::Vector<std::unique_ptr<amrex::MultiFab> > TwoStreamRadiation::m_t_sfc
private

The documentation for this class was generated from the following files: