ERF
Energy Research and Forecasting: An Atmospheric Modeling Code
ERF_ComputeDiffusivityMRF.cpp File Reference
#include <algorithm>
#include "ERF_SurfaceLayer.H"
#include "ERF_DirectionSelector.H"
#include "ERF_Diffusion.H"
#include "ERF_Constants.H"
#include "ERF_TurbStruct.H"
#include "ERF_PBLModels.H"
#include "ERF_TileNoZ.H"
#include "ERF_MoistUtils.H"
#include "ERF_PBLScaleAwareBlending.H"
Include dependency graph for ERF_ComputeDiffusivityMRF.cpp:

Functions

void ComputeDiffusivityMRF (const MultiFab &xvel, const MultiFab &yvel, const MultiFab &cons_in, MultiFab &eddyViscosity, const Geometry &geom, const TurbChoice &turbChoice, std::unique_ptr< SurfaceLayer > &SurfLayer, bool use_terrain_fitted_coords, bool use_moisture, int level, const BCRec *bc_ptr, bool, const std::unique_ptr< MultiFab > &z_phys_nd, const std::unique_ptr< MultiFab > &z_phys_cc, const MoistureComponentIndices &moisture_indices, const MultiFab *terrain_blank)
 

Function Documentation

◆ ComputeDiffusivityMRF()

void ComputeDiffusivityMRF ( const MultiFab &  xvel,
const MultiFab &  yvel,
const MultiFab &  cons_in,
MultiFab &  eddyViscosity,
const Geometry &  geom,
const TurbChoice &  turbChoice,
std::unique_ptr< SurfaceLayer > &  SurfLayer,
bool  use_terrain_fitted_coords,
bool  use_moisture,
int  level,
const BCRec *  bc_ptr,
bool  ,
const std::unique_ptr< MultiFab > &  z_phys_nd,
const std::unique_ptr< MultiFab > &  z_phys_cc,
const MoistureComponentIndices &  moisture_indices,
const MultiFab *  terrain_blank 
)

Compute vertical diffusivity using the Medium-Range Forecast (MRF) boundary layer scheme.

Parameters
[in]xvelx-velocity field.
[in]yvely-velocity field.
[in]cons_inInput conserved variables.
[out]eddyViscosityEddy viscosity and diffusivity coefficients.
[in]geomGrid geometry.
[in]turbChoiceTurbulence model options.
[in]SurfLayerSurface layer data.
[in]use_terrain_fitted_coordsUse terrain-fitted coordinates.
[in]use_moistureEnable moisture components.
[in]levelCurrent AMR level.
[in]bc_ptrBoundary condition records.
[in]z_phys_ndNodal physical heights.
[in]z_phys_ccCell-centered physical heights.
[in]moisture_indicesIndices for moisture variables.
50 {
51  const StratType pbl_strat = turbChoice.pbl_strat_type;
52  /*
53  ============================================================================
54  Medium-Range Forecast (MRF) Boundary Layer Parameterization Scheme
55  ============================================================================
56 
57  Implementation of the MRF (Medium Range Forecast) boundary layer scheme
58  based on Hong and Pan (1996), with enhanced moisture handling and
59  cloud-aware stability corrections developed for ERF.
60 
61  PRIMARY REFERENCES:
62  -------------------
63  - Hong, S. Y., and H.-L. Pan, 1996: Nonlocal Boundary Layer Vertical
64  Diffusion in a Medium-Range Forecast Model. Monthly Weather Review, 124,
65  2322-2339. https://doi.org/10.1175/1520-0493(1996)124<2322:NBLVDI>2.0.CO;2
66 
67  - WRF Reference Implementation: module_bl_mrf.F
68  https://github.com/wrf-model/WRF/blob/master/phys/module_bl_mrf.F
69 
70  - Hong, S. Y., Y. Noh, and J. Dudhia, 2006: A new vertical diffusion
71  package with an explicit treatment of entrainment processes. Monthly
72  Weather Review, 134, 2318-2341. https://doi.org/10.1175/MWR3250.1
73  (YSU scheme, used for free atmosphere mixing)
74 
75  CORE ALGORITHM (Hong & Pan 1996):
76  ----------------------------------
77  1. Predictor-Corrector Bulk Richardson Number (Rib) PBL Height Diagnosis
78  - Rib = (g*z*(θ_v(z) - θ_v(0))) / (U²*θ_v(0))
79  - h = min(z where Rib > Rib_critical)
80 
81  2. Nonlocal Countergradient Flux Corrections
82  - HGAMT: thermal excess from sensible heat flux
83  - HGAMQ: moisture excess from latent heat flux
84  - VPERT: virtual potential temperature perturbation
85 
86  3. Stability-Dependent K-Profile in the PBL (used in EVERY stability regime)
87  - K_m = ρ * w_s * κ * z * (1 - (z - z_1)/(h - z_1))², z_1 = first level height
88  - K_t = K_m / Pr_t, and K_q = K_t (WRF diffuses moisture with the heat
89  coefficient XKZH; module_bl_mrf.F L1069, L1180)
90  - Stability enters only through w_s = u_* / φ_m (WRF's WSCALE); as in WRF there
91  is no regime test below the PBL top (module_bl_mrf.F L968-986). The
92  Ri-based mixing ERF used to apply inside stable/neutral PBLs is gone with
93  it; see KNOWN LIMITATION below for what that costs in stable layers.
94 
95  4. Free Atmosphere Mixing via Richardson Number
96  - Uses YSU stability functions (Hong et al. 2006)
97  - Ri-dependent diffusivity above the PBL top only (module_bl_mrf.F L988 on)
98 
99  ENHANCEMENTS IN ERF IMPLEMENTATION:
100  -----------------------------------
101 
102  A. Virtual Potential Temperature (θ_v) Treatment
103  - Proper handling of moisture effects on buoyancy
104  - Important for accurate PBL height and convective strength
105  - Not explicitly detailed in Hong & Pan (1996) but standard practice
106 
107  B. Cloud-Aware Stability Function Adjustments (OPTIONAL)
108  - Physically motivated extension beyond Hong & Pan (1996)
109  - Detects clouds via qc + qi > threshold (0.1 g/kg)
110  - In stable layers with clouds:
111  * Reduces stability damping (φ reduced by 10-20%)
112  * Represents suppressed turbulence due to stable stratification
113  - In unstable layers with clouds:
114  * Slightly increases instability enhancement
115  * Represents latent heat release effects
116  - Disabled by default for backward compatibility
117  - Reference: Conceptually similar to WRF's IMVDIF cloud handling
118 
119  C. Explicit Moisture Sign Convention Handling
120  - q_star uses WRF convention: negative for upward flux (evaporation)
121  - HGAMQ calculation: -const_b * u_* * q_* / w_*
122  - Produces positive HGAMQ for unstable (evaporating) conditions
123  - Corrected with MAX(HGAMQ, 0) to prevent negative values
124  - Land/water discrimination: HGAMQ zeroed over water surfaces
125 
126  D. Improved VPERT (Virtual Potential Temperature Perturbation)
127  Formulation
128  - VPERT = max(HGAMT + 0.61*θ*HGAMQ, 0)
129  - Enhancement vs WRF: Does not limit VPERT to GAMCRT
130  - WRF limits VPERT after adding moisture term (VPERT = min(VPERT, GAMCRT))
131  - This is physically incorrect: moisture contribution can exceed GAMCRT
132  - ERF only limits HGAMT (sensible heat), allowing larger VPERT when
133  both sensible and latent heat create strong surface heating
134  - This produces more realistic PBL heights in moist convection
135 
136  E. Prandtl Number with Stability Correction
137  - Pr_t = φ_t/φ_m + const_b * κ * sf
138  - Bounded: 0.5 ≤ Pr_t ≤ 4.0
139  - Consistency with WRF: const_b = 7.8, sf = 0.1
140  - Reference: Hong et al. (2006), Equation A17
141 
142  PARAMETER DEFAULTS (Matching WRF):
143  -----------------------------------
144  - const_b = 7.8 (heat flux weight in countergradient)
145  - sf = 0.1 (surface layer / PBL height ratio in stability functions)
146  - GAMCRT = 3.0 K (maximum heat countergradient)
147  - GAMCRQ = 2.0e-3 kg/kg (maximum moisture countergradient)
148  - Rib_crit = 0.5 (critical bulk Richardson number for PBL height)
149  - Pr_min = 0.5, Pr_max = 4.0 (Prandtl number bounds)
150  - Kmin = 0.1 m²/s, Kmax = 300 m²/s (diffusivity bounds, Hong & Pan 1996)
151 
152  STABILITY FUNCTIONS:
153  --------------------
154  Unstable (L < 0, HOL < 0):
155  phi_m = (1 - 8 * sf * h/L)^(-1/3)
156  phi_t = (1 - 16 * sf * h/L)^(-1/2)
157 
158  Stable (L > 0, HOL > 0):
159  Default: phi_m = phi_t = 1 + 5 * sf * h/L
160  QNSE (if enable_qnse_stable_functions=true):
161  phi_m = (1 + qnse_am * zeta) / (1 + qnse_bm * zeta)
162  phi_t = (1 + qnse_ah * zeta) / (1 + qnse_bh * zeta)
163  where zeta = sf * h/L, and default coefficients are from
164  Sukoriansky, Galperin & Perov (2005): qnse_am=2.5, qnse_bm=0.2,
165  qnse_ah=2.5, qnse_bh=0.2. QNSE functions remain bounded as zeta→∞,
166  avoiding over-mixing suppression in very stable conditions.
167 
168  MIXING ABOVE PBL (Free Atmosphere):
169  -----------------------------------
170  Uses YSU scheme (Hong et al. 2006, Appendix A) for Richardson number
171  dependent mixing. This avoids MRF oscillations in stable conditions.
172  It is applied above the PBL top only, as in WRF; below the top the
173  K-profile of item 3 above is used in every stability regime.
174 
175  Gradient Richardson number: Ri_g = (g/theta_v) * (dtheta_v/dz) / ((du/dz)^2 + (dv/dz)^2)
176 
177  For Ri_g > 0 (stable):
178  f_m = 1 / ((1 + 5*Ri_g)^2)
179  f_t = 1 / ((1 + 5*Ri_g)^2)
180  Pr_t = 1 + 2.1*Ri_g, bounded to [0.25, 4.0]
181 
182  For Ri_g < 0 (unstable):
183  f_m = 1 - 8*Ri_g / (1 + 1.746*sqrt(-Ri_g))
184  f_t = 1 - 8*Ri_g / (1 + 1.286*sqrt(-Ri_g))
185 
186  NUMERICAL CONSIDERATIONS:
187  -------------------------
188  - GPU-optimized with parallel_for loops
189  - Safe bounds on shear and vertical derivatives
190  - Gradient Richardson number limited: -100 <= Ri_g <= 100
191  - Avoids division by zero with minimum shear threshold (1e-10)
192  - theta_v_klo and theta_v guarded against zero/negative values
193 
194  KNOWN LIMITATION (stable layers):
195  ---------------------------------
196  With the K-profile applied in every regime, this scheme inherits WRF MRF's
197  over-mixing of stable boundary layers: on GABLS1 its peak K_m is roughly
198  2.1x YSUNew. That is the behaviour Hong et al. (2006) set out to fix in YSU,
199  so stable cases that need the weaker mixing should use the YSUNew scheme
200  (erf.pbl_type = YSUNew).
201 
202  TESTING & VALIDATION:
203  ---------------------
204  - Tested against WRF_ideal unstable/stable ABL cases
205  - Backward compatible when enhancements are disabled
206  - Cloud detection threshold: qc + qi > 1e-4 kg/kg (0.1 g/kg)
207  */
208 
209  // Domain extent in z-dir
210  int klo = geom.Domain().smallEnd(2);
211  int khi = geom.Domain().bigEnd(2);
212 
213 
214 
215 // Collect MRF-computed PBLH for writing back to SurfaceLayer
216 MultiFab pblh_mf(eddyViscosity.boxArray(), eddyViscosity.DistributionMap(), 1, 0);
217 pblh_mf.setVal(0.0);
218 
219 // PBLH smoothing reads one column per pass outside the cells it writes, so the
220 // planar work arrays have to carry that many columns of halo and the PBLH passes
221 // have to fill them: otherwise the stencil reads off the end of the tile and the
222 // answer depends on the decomposition. With smoothing off this is the single
223 // column of halo the diffusivity kernels already use, so nothing changes.
224 const int ng_pblh = (turbChoice.enable_pblh_smoothing)
225  ? std::max(1, turbChoice.pblh_smoothing_passes) : 1;
226 if (ng_pblh > 1) {
227  const int ng_avail = std::min({cons_in.nGrowVect()[0], cons_in.nGrowVect()[1],
228  xvel.nGrowVect()[0], xvel.nGrowVect()[1],
229  yvel.nGrowVect()[0], yvel.nGrowVect()[1],
230  SurfLayer->get_u_star(level)->nGrowVect()[0],
231  SurfLayer->get_u_star(level)->nGrowVect()[1],
232  SurfLayer->get_olen(level)->nGrowVect()[0],
233  SurfLayer->get_olen(level)->nGrowVect()[1],
234  (terrain_blank) ? terrain_blank->nGrowVect()[0] : 1000,
235  (terrain_blank) ? terrain_blank->nGrowVect()[1] : 1000});
236  if (ng_pblh > ng_avail) {
237  amrex::Abort("erf.pblh_smoothing_passes = " + std::to_string(turbChoice.pblh_smoothing_passes)
238  + " needs " + std::to_string(ng_pblh) + " halo columns, but the state and "
239  "surface-layer arrays carry only " + std::to_string(ng_avail)
240  + "; reduce erf.pblh_smoothing_passes to at most " + std::to_string(ng_avail));
241  }
242 }
243 
244 #ifdef _OPENMP
245 #pragma omp parallel if (Gpu::notInLaunchRegion())
246 #endif
247 
248  for (MFIter mfi(eddyViscosity, TileNoZ()); mfi.isValid(); ++mfi) {
249 
250  // Box operated on must span fill domain in z-dir
251  const Box& gbx = mfi.growntilebox(IntVect(1,1,0));
252  AMREX_ALWAYS_ASSERT( gbx.smallEnd(2) == klo &&
253  gbx.bigEnd(2) == khi );
254 
255  // create flattened boxes to store PBL height and related quantities
256  const GeometryData gdata = geom.data();
257  const Box xybx = PerpendicularBox<ZDir>(gbx, IntVect{0, 0, 0});
258 
259  // The PBLH passes run on the region the smoothing stencil will consume
260  // (see ng_pblh above). growntilebox() is no use here: it does not grow at
261  // an interior tile edge, so a tile in the middle of a box gets no halo at
262  // all -- which is exactly how the stencil came to read off the end of the
263  // array. Grow the tile box explicitly instead, which costs a little
264  // duplicated work in the overlaps. With smoothing off this is xybx and
265  // nothing is duplicated.
266  const Box gbx_work = (turbChoice.enable_pblh_smoothing)
267  ? amrex::grow(mfi.tilebox(), IntVect(ng_pblh,ng_pblh,0)) : gbx;
268  const Box xybx_work = PerpendicularBox<ZDir>(gbx_work, IntVect{0, 0, 0});
269  const Box xybx_tile = PerpendicularBox<ZDir>(mfi.tilebox(), IntVect{0, 0, 0});
270 
271  // Pass 1 (predictor): PBLH with base surface temperature, no VPERT
272  // Pass 2 (wstar/VPERT): compute wstar, HGAMT, HGAMQ, VPERT from predictor height
273  // Pass 3 (corrector): PBLH with VPERT-enhanced surface temperature (WRF-consistent)
274  // WRF reference (module_bl_mrf.F lines 813-964):
275  // https://github.com/wrf-model/WRF/blob/master/phys/module_bl_mrf.F#L813-L964
276  FArrayBox pbl_height_predictor(xybx_work, 1, The_Async_Arena()); // Pass 1: base t_layer_v
277  FArrayBox pbl_height_corrector(xybx_work, 1, The_Async_Arena()); // Pass 3: VPERT-enhanced
278  IArrayBox pbl_index(xybx_work, 1, The_Async_Arena());
279  IArrayBox pbl_index_zero_ri(xybx_work, 1, The_Async_Arena()); // Index for zero-Ri diagnostic pass
280  FArrayBox hgamt_fab(xybx_work, 1, The_Async_Arena()); // Store HGAMT/h (normalized countergradient)
281  FArrayBox hgamq_fab(xybx_work, 1, The_Async_Arena()); // Store HGAMQ/h (normalized countergradient)
282  FArrayBox wstar_fab(xybx_work, 1, The_Async_Arena()); // Convective velocity scale
283  FArrayBox vpert_fab(xybx_work, 1, The_Async_Arena()); // Virtual temperature perturbation VPERT
284  FArrayBox pblh_floor_fab(xybx_work, 1, The_Async_Arena()); // Floor of the corrected height per column, reapplied after the smoothing
285  const auto& pblh_pred_arr = pbl_height_predictor.array(); // predictor (base t_layer_v)
286  const auto& pblh_corr_arr = pbl_height_corrector.array(); // corrector (VPERT-enhanced)
287  const auto& pblh_floor_arr = pblh_floor_fab.array();
288  const auto& pbli_arr = pbl_index.array();
289  const auto& pbli_zero_arr = pbl_index_zero_ri.array(); // Zero-Ri diagnostic PBL index
290  const auto& hgamt_arr = hgamt_fab.array();
291  const auto& hgamq_arr = hgamq_fab.array();
292  const auto& wstar_arr = wstar_fab.array();
293  const auto& vpert_arr = vpert_fab.array();
294 
295  // Get some data in arrays
296  const auto& cell_data = cons_in.const_array(mfi);
297  const auto& uvel = xvel.const_array(mfi);
298  const auto& vvel = yvel.const_array(mfi);
299 
300  const Real Ribcr = turbChoice.pbl_mrf_Ribcr;
301  const auto& u_star_arr = SurfLayer->get_u_star(level)->const_array(mfi);
302  const auto& t_star_arr = SurfLayer->get_t_star(level)->const_array(mfi);
303  const auto& l_obuk_arr = SurfLayer->get_olen(level)->const_array(mfi);
304  const auto& t10av_arr = SurfLayer->get_mac_avg(level, 3)->const_array(mfi);
305  const auto& q10av_arr = SurfLayer->get_mac_avg(level, 4)->const_array(mfi);
306  const auto& lmask_arr = (SurfLayer->get_lmask(level)) ?
307  SurfLayer->get_lmask(level)->const_array(mfi) :
308  Array4<int>{};
309  // Only retrieve z_phys_nd array if terrain-fitted coordinates are in use
310  const Array4<Real const> z_nd_arr = use_terrain_fitted_coords ? z_phys_nd->array(mfi)
311  : Array4<Real const>{};
312  const PBLDerivativeDzInv_T pbl_derivative_dz_inv{z_phys_cc->const_array(mfi)};
313 
314  //
315  // PASS 1 (PREDICTOR): Compute PBL height using base surface virtual temperature.
316  // No thermal excess (VPERT = 0). Produces pblh_pred_arr and pbli_arr.
317  // WRF reference (module_bl_mrf.F lines 813-842):
318  // https://github.com/wrf-model/WRF/blob/master/phys/module_bl_mrf.F#L813-L842
319  //
320  // FIX: guard against null dereference of q_star when use_moisture is false (dry case)
321  // get_q_star can return null pointer when the field is not allocated
322  amrex::MultiFab* q_star_mf = SurfLayer->get_q_star(level);
323  Array4<Real const> q_star_arr = (q_star_mf != nullptr) ? q_star_mf->const_array(mfi)
324  : Array4<Real const>{};
325 
326  // --------------------------------------------------------------------
327  // Immersed-boundary awareness (turbChoice.pbl_ib_aware): per column,
328  // the first fluid cell above the immersed solid (ksurf) and its height
329  // above the domain bottom (zib). Every height below is measured from
330  // it, the diffusivities vanish inside the solid, and the surface
331  // scales of a column with solid cells are a neutral log law at its
332  // top instead of the ground surface layer's values (which come from
333  // cells inside the solid there). With the option off, or without
334  // immersed cells, ksurf = klo, zib = 0 and the scales are copies, so
335  // the arithmetic below is unchanged.
336  // --------------------------------------------------------------------
337  const bool l_ib = turbChoice.pbl_ib_aware && (terrain_blank != nullptr);
338  AMREX_ALWAYS_ASSERT_WITH_MESSAGE(!(l_ib && use_terrain_fitted_coords),
339  "erf.pbl_ib_aware is not supported with terrain-fitted coordinates");
340  const Array4<Real const> blank_arr = l_ib ? terrain_blank->const_array(mfi) : Array4<Real const>{};
341  IArrayBox ksurf_fab(xybx_work, 1, The_Async_Arena());
342  FArrayBox zib_fab(xybx_work, 1, The_Async_Arena());
343  FArrayBox us_eff_fab(xybx_work, 1, The_Async_Arena()), ts_eff_fab(xybx_work, 1, The_Async_Arena());
344  FArrayBox qs_eff_fab(xybx_work, 1, The_Async_Arena()), ol_eff_fab(xybx_work, 1, The_Async_Arena());
345  FArrayBox t10_eff_fab(xybx_work, 1, The_Async_Arena()), q10_eff_fab(xybx_work, 1, The_Async_Arena());
346  const auto& ksurf_arr = ksurf_fab.array();
347  const auto& zib_arr = zib_fab.array();
348  const auto& us_eff_arr = us_eff_fab.array(); const auto& ts_eff_arr = ts_eff_fab.array();
349  const auto& qs_eff_arr = qs_eff_fab.array(); const auto& ol_eff_arr = ol_eff_fab.array();
350  const auto& t10_eff_arr = t10_eff_fab.array(); const auto& q10_eff_arr = q10_eff_fab.array();
351  {
352  const Real dz_ib = geom.CellSize(2);
353  const Real z0_ib = turbChoice.pbl_ib_z0;
354  const Array4<Real const>& uv_ib = xvel.const_array(mfi);
355  const Array4<Real const>& vv_ib = yvel.const_array(mfi);
356  const bool have_qstar = (q_star_mf != nullptr);
357  ParallelFor(xybx_work, [=] AMREX_GPU_DEVICE(int i, int j, int) noexcept
358  {
359  int ks = klo;
360  if (l_ib) {
361  for (int kk = klo; kk <= khi; ++kk) { if (blank_arr(i, j, kk) >= Real(0.5)) { ks = kk + 1; } }
362  if (ks > khi) { ks = khi; }
363  }
364  ksurf_arr(i, j, 0) = ks;
365  zib_arr(i, j, 0) = (ks - klo) * dz_ib;
366  if (ks > klo) {
367  // Column with immersed solid: neutral log law at its top.
368  const Real u_top = myhalf * (uv_ib(i, j, ks) + uv_ib(i + 1, j, ks));
369  const Real v_top = myhalf * (vv_ib(i, j, ks) + vv_ib(i, j + 1, ks));
370  const Real ws_top = std::sqrt(u_top * u_top + v_top * v_top);
371  us_eff_arr(i, j, 0) = amrex::max(KAPPA * ws_top / std::log(myhalf * dz_ib / z0_ib), Real(1.0e-3));
372  ts_eff_arr(i, j, 0) = Real(0);
373  qs_eff_arr(i, j, 0) = Real(0);
374  ol_eff_arr(i, j, 0) = Real(1.0e10);
375  t10_eff_arr(i, j, 0) = cell_data(i, j, ks, RhoTheta_comp) / cell_data(i, j, ks, Rho_comp);
376  q10_eff_arr(i, j, 0) = (use_moisture && moisture_indices.qv >= 0)
377  ? cell_data(i, j, ks, moisture_indices.qv) / cell_data(i, j, ks, Rho_comp) : Real(0);
378  } else {
379  us_eff_arr(i, j, 0) = u_star_arr(i, j, 0);
380  ts_eff_arr(i, j, 0) = t_star_arr(i, j, 0);
381  qs_eff_arr(i, j, 0) = have_qstar ? q_star_arr(i, j, 0) : Real(0);
382  ol_eff_arr(i, j, 0) = l_obuk_arr(i, j, 0);
383  t10_eff_arr(i, j, 0) = t10av_arr(i, j, 0);
384  q10_eff_arr(i, j, 0) = use_moisture ? q10av_arr(i, j, 0) : Real(0);
385  }
386  });
387  }
388 
389  ParallelFor(xybx_work, [=] AMREX_GPU_DEVICE(int i, int j, int) noexcept
390  {
391  const int ksrf = ksurf_arr(i, j, 0); // local surface: first fluid cell of the column
392  const Real zib = zib_arr(i, j, 0); // its height, subtracted from every height below
393  amrex::ignore_unused(zib);
394  const Real t_layer = t10_eff_arr(i, j, 0);
395  const Real moisture_fraction = use_moisture ? q10_eff_arr(i, j, 0) : Real(0);
396  const Real t_layer_v = t_layer * (one + epsv * moisture_fraction);
397 
398  Real zval, Rib;
399  int kpbl = ksrf;
400 
401  {
402  zval = (use_terrain_fitted_coords)
403  ? Compute_Zrel_AtCellCenter(i, j, kpbl, z_nd_arr)
404  : ((kpbl + myhalf) * gdata.CellSize(2) - zib);
405  const Real theta_v = GetThetav(i, j, kpbl, cell_data, moisture_indices);
406  const Real theta_v_klo = amrex::max(GetThetav(i, j, ksrf, cell_data, moisture_indices), Real(1.0));
407  const Real ws2_raw = fourth * ( (uvel(i, j, kpbl) + uvel(i + 1, j, kpbl)) *
408  (uvel(i, j, kpbl) + uvel(i + 1, j, kpbl)) +
409  (vvel(i, j, kpbl) + vvel(i, j + 1, kpbl)) *
410  (vvel(i, j, kpbl) + vvel(i, j + 1, kpbl)) );
411  // Vogelezang & Holtslag (1996): Add shear correction term to denominator instead of ad-hoc floor
412  // to better represent shear associated with surface-layer turbulence at low wind speeds.
413  // Reference: Vogelezang, D.H.P., and A.A.M. Holtslag, 1996: Evaluation and model impacts of
414  // alternative boundary-layer height formulations. Boundary-Layer Meteorology, 81, 245–269.
415  const Real ws2 = (turbChoice.enable_vh96_shear_correction)
416  ? (ws2_raw + turbChoice.vh96_shear_const_b * us_eff_arr(i,j,0) * us_eff_arr(i,j,0))
417  : amrex::max(ws2_raw, Real(1.0));
418  Rib = CONST_GRAV * zval * (theta_v - t_layer_v) / (ws2 * theta_v_klo);
419  }
420 
421  Real zval0 = zval, Rib0 = Rib;
422  bool above_critical = false;
423  while (!above_critical && ((kpbl + 1) <= khi)) {
424  zval0 = zval;
425  Rib0 = Rib;
426  kpbl += 1;
427 
428  zval = (use_terrain_fitted_coords)
429  ? Compute_Zrel_AtCellCenter(i, j, kpbl, z_nd_arr)
430  : ((kpbl + myhalf) * gdata.CellSize(2) - zib);
431  const Real theta_v = GetThetav(i, j, kpbl, cell_data, moisture_indices);
432  const Real theta_v_klo = amrex::max(GetThetav(i, j, ksrf, cell_data, moisture_indices), Real(1.0));
433  const Real ws2_raw = fourth * ( (uvel(i, j, kpbl) + uvel(i + 1, j, kpbl)) *
434  (uvel(i, j, kpbl) + uvel(i + 1, j, kpbl)) +
435  (vvel(i, j, kpbl) + vvel(i, j + 1, kpbl)) *
436  (vvel(i, j, kpbl) + vvel(i, j + 1, kpbl)) );
437  // Vogelezang & Holtslag (1996): Add shear correction term to denominator instead of ad-hoc floor
438  // to better represent shear associated with surface-layer turbulence at low wind speeds.
439  const Real ws2 = (turbChoice.enable_vh96_shear_correction)
440  ? (ws2_raw + turbChoice.vh96_shear_const_b * us_eff_arr(i,j,0) * us_eff_arr(i,j,0))
441  : amrex::max(ws2_raw, Real(1.0));
442  Rib = CONST_GRAV * zval * (theta_v - t_layer_v) / (ws2 * theta_v_klo);
443  above_critical = (Rib >= Ribcr);
444  }
445 
446  const Real pblh_emp = (use_terrain_fitted_coords)
447  ? Compute_Zrel_AtCellCenter(i, j, ksrf, z_nd_arr)
448  : myhalf * gdata.CellSize(2);
449  const Real z_max = (use_terrain_fitted_coords)
450  ? Compute_Zrel_AtCellCenter(i, j, khi, z_nd_arr)
451  : ((khi + myhalf) * gdata.CellSize(2) - zib);
452  const Real pblh_max = Real(0.9) * z_max;
453  const Real pblh_min = amrex::max(pblh_emp, Real(10.0));
454 
455  if (above_critical) {
456  Real pblh_interp;
457  const Real rib_diff = Rib - Rib0;
458  if (std::abs(rib_diff) > Real(1.0e-10)) {
459  pblh_interp = zval0 + (zval - zval0) / rib_diff * (Ribcr - Rib0);
460  } else {
461  // If Rib not changing, use zval0 as PBL height
462  pblh_interp = zval0;
463  }
464  pblh_pred_arr(i, j, 0) = amrex::max(amrex::min(pblh_interp, pblh_max), pblh_min);
465  pbli_arr(i, j, 0) = kpbl;
466  } else {
467  pblh_pred_arr(i, j, 0) = pblh_min;
468  pbli_arr(i, j, 0) = ksrf + 1;
469  }
470  });
471 
472  const Real const_b = turbChoice.pbl_mrf_const_b;
473  const Real sf = turbChoice.pbl_mrf_sf;
474  constexpr Real prmin = Real(0.5);
475  constexpr Real prmax = Real(4.0);
476  constexpr Real GAMCRT = Real(3.0);
477  constexpr Real GAMCRQ = Real(2.e-3);
478  const bool enable_mrf_countergradient = turbChoice.enable_mrf_countergradient;
479  const bool enable_mrf_unbounded_vpert = turbChoice.enable_mrf_unbounded_vpert;
480 
481  //
482  // PASS 2 (WSTAR / VPERT): Compute wstar, HGAMT, HGAMQ, VPERT using the
483  // predictor PBL height (pblh_pred_arr). VPERT will be fed into the corrector
484  // Rib search in Pass 3 to raise the effective surface temperature.
485  // WRF reference (module_bl_mrf.F lines 857-880):
486  // https://github.com/wrf-model/WRF/blob/master/phys/module_bl_mrf.F#L857-L880
487  //
488  ParallelFor(xybx_work, [=,zero_d=zero] AMREX_GPU_DEVICE(int i, int j, int) noexcept
489  {
490  const int ksrf = ksurf_arr(i, j, 0); // local surface: first fluid cell of the column
491  const Real zib = zib_arr(i, j, 0); // its height, subtracted from every height below
492  amrex::ignore_unused(zib);
493  const Real t_layer = t10_eff_arr(i, j, 0);
494  Real obuk_val = ol_eff_arr(i, j, 0);
495  if (std::abs(obuk_val) < amrex::Real(1.0e-10)) {
496  obuk_val = (obuk_val >= zero_d) ? amrex::Real(1.0e-10) : amrex::Real(-1.0e-10);
497  }
498 
499  // HOL computed from predictor height (WRF uses predictor height for wstar/VPERT)
500  const Real HOL = sf * pblh_pred_arr(i, j, 0) / obuk_val;
501  const Real HOL_bounded = amrex::max(amrex::min(HOL, Real(100.0)), Real(-100.0));
502  // |HOL|, so the base 1 + 16*HOL_abs of the unstable arm below equals its
503  // 1 - 16*HOL there and is at least 1 for every HOL; a guard written inside the
504  // arm gets folded away by the optimiser and the pow hoisted (see sqrt_neg_Ri).
505  const Real HOL_abs = std::abs(HOL_bounded);
506  const Real one_quarter = Real(0.25);
507  const Real phiM = (obuk_val > 0)
508  ? (1 + 5 * HOL_bounded)
509  : std::pow(1 + 16 * HOL_abs, -one_quarter);
510  const Real phiM_safe = amrex::max(phiM, Real(0.01));
511 
512  // wstar = u* / phi_m
513  // Absolute bounds [0.01, 5.0] m/s: prevent division-by-zero (floor) and
514  // free-convection blow-up (ceiling), independent of u* for u*->0 safety.
515  // BOOTSTRAP NOTE: pblh_pred_arr must be finite (not sentinel) before this
516  // kernel runs. On cold start, pblh_pred_arr is computed in Pass 1 above;
517  // on regrid, SurfaceLayer PBLH may be sentinel — callers must re-run
518  // ComputeDiffusivityMRF before consuming wstar or Beljaars correction after regrid.
519  // Deardorff (1972) convective velocity scale — well-behaved as u_star -> 0.
520  // wstar_conv = (g/theta_v0 * zi * wthv0)^(1/3)
521  const Real theta_v0 = amrex::max(GetThetav(i, j, ksrf, cell_data, moisture_indices), Real(1.0));
522  const Real wthv0 = amrex::max(-us_eff_arr(i,j,0) * ts_eff_arr(i,j,0), Real(0.0)); // surface buoyancy flux, unstable only
523  const Real zi = amrex::max(pblh_pred_arr(i, j, 0), Real(10.0)); // predictor: Pass 3 has not run yet
524  const Real wstar_conv = std::cbrt(CONST_GRAV / theta_v0 * zi * wthv0);
525 
526  Real wstar_shear = us_eff_arr(i, j, 0) / phiM_safe;
527 
528  // Blend: standard convective/shear combination (WRF YSU-style),
529  // weighted by cube sum so either term can dominate smoothly
530  Real wstar = std::cbrt(wstar_shear*wstar_shear*wstar_shear +
531  wstar_conv*wstar_conv*wstar_conv);
532 
533  wstar = amrex::max(wstar, Real(0.01));
534  wstar = amrex::min(wstar, Real(5.0));
535 
536  bool SFCFLG = (obuk_val <= zero_d);
537  const Real HGAMT = (SFCFLG && enable_mrf_countergradient)
538  ? amrex::min(-const_b * us_eff_arr(i, j, 0) * ts_eff_arr(i, j, 0) / wstar, GAMCRT)
539  : zero_d;
540 
541  Real HGAMQ = Real(0);
542  if (SFCFLG && use_moisture && enable_mrf_countergradient) {
543  const Real q_star = qs_eff_arr(i, j, 0);
544  const Real HGAMQ_calc = -const_b * us_eff_arr(i, j, 0) * q_star / wstar;
545  HGAMQ = amrex::max(amrex::min(HGAMQ_calc, GAMCRQ), Real(0));
546 
547  if (lmask_arr) {
548  bool is_land = (lmask_arr(i,j,0) == 1);
549  if (!is_land) HGAMQ = zero_d;
550  }
551 
552  if (moisture_indices.qv >= 0) {
553  Real qv_klo = cell_data(i, j, ksrf, moisture_indices.qv) / cell_data(i, j, ksrf, Rho_comp);
554  Real T_klo = getTgivenRandRTh(cell_data(i, j, ksrf, Rho_comp),
555  cell_data(i, j, ksrf, RhoTheta_comp), qv_klo);
556  Real p_klo = getPgivenRTh(cell_data(i, j, ksrf, RhoTheta_comp), qv_klo) * Real(0.01);
557  Real qsat_klo = zero_d;
558  erf_qsatw(T_klo, p_klo, qsat_klo);
559  Real rh_klo = (qsat_klo > Real(1.0e-10)) ? (qv_klo / qsat_klo) : Real(0);
560  if (rh_klo > Real(0.95)) {
561  Real rh_scaling = amrex::max(zero_d, (Real(1) - rh_klo) / Real(0.05));
562  HGAMQ *= rh_scaling;
563  }
564  }
565  }
566 
567  // Compute VPERT = HGAMT + 0.61*theta*HGAMQ (virtual temperature perturbation)
568  // This will be added to the surface temperature in the corrector Rib search.
569  // WRF Reference: module_bl_mrf.F lines 879-880
570  if (pbli_arr(i, j, 0) <= ksrf + 1 || !enable_mrf_countergradient) {
571  vpert_arr(i, j, 0) = zero_d;
572  } else {
573  const Real VPERT_raw = HGAMT + epsv * t_layer * HGAMQ;
574  const Real VPERT_capped = enable_mrf_unbounded_vpert
575  ? VPERT_raw
576  : amrex::min(VPERT_raw, GAMCRT);
577  vpert_arr(i, j, 0) = amrex::max(VPERT_capped, zero_d);
578  }
579  });
580 
581  //
582  // PASS 3 (CORRECTOR): Recompute PBL height with VPERT-enhanced surface temperature.
583  // theta_s = theta_va + VPERT (Hong & Pan 1996, Eq. 4)
584  // This is the WRF-consistent corrector pass. pblh_corr_arr is used for the
585  // K-profile shape function K = rho*wstar*kappa*z*(1-z/h)^2.
586  // WRF reference (module_bl_mrf.F lines 932-964):
587  // https://github.com/wrf-model/WRF/blob/master/phys/module_bl_mrf.F#L932-L964
588  //
589  ParallelFor(xybx_work, [=,one_d=one]
590  AMREX_GPU_DEVICE(int i, int j, int) noexcept
591  {
592  const int ksrf = ksurf_arr(i, j, 0); // local surface: first fluid cell of the column
593  const Real zib = zib_arr(i, j, 0); // its height, subtracted from every height below
594  amrex::ignore_unused(zib);
595  const Real t_layer = t10_eff_arr(i, j, 0);
596  const Real moisture_fraction = use_moisture ? q10_eff_arr(i, j, 0) : Real(0);
597  // VPERT-enhanced surface virtual temperature (WRF THERMAL variable)
598  const Real t_layer_v = t_layer * (one + epsv * moisture_fraction)
599  + vpert_arr(i, j, 0);
600 
601  Real obuk_val = ol_eff_arr(i, j, 0);
602  if (std::abs(obuk_val) < amrex::Real(1.0e-10)) {
603  obuk_val = (obuk_val >= zero) ? amrex::Real(1.0e-10) : amrex::Real(-1.0e-10);
604  }
605 
606  int kpbl = ksrf;
607  Real zval, Rib;
608  {
609  zval = (use_terrain_fitted_coords)
610  ? Compute_Zrel_AtCellCenter(i, j, kpbl, z_nd_arr)
611  : ((kpbl + myhalf) * gdata.CellSize(2) - zib);
612  const Real theta_v = GetThetav(i, j, kpbl, cell_data, moisture_indices);
613  const Real theta_v_klo = amrex::max(GetThetav(i, j, ksrf, cell_data, moisture_indices), one_d);
614  const Real ws2_raw = fourth * ( (uvel(i, j, kpbl) + uvel(i + 1, j, kpbl)) *
615  (uvel(i, j, kpbl) + uvel(i + 1, j, kpbl)) +
616  (vvel(i, j, kpbl) + vvel(i, j + 1, kpbl)) *
617  (vvel(i, j, kpbl) + vvel(i, j + 1, kpbl)) );
618  // Vogelezang & Holtslag (1996): Add shear correction term to denominator instead of ad-hoc floor
619  const Real ws2 = (turbChoice.enable_vh96_shear_correction)
620  ? (ws2_raw + turbChoice.vh96_shear_const_b * us_eff_arr(i,j,0) * us_eff_arr(i,j,0))
621  : amrex::max(ws2_raw, one_d);
622  Rib = CONST_GRAV * zval * (theta_v - t_layer_v) / (ws2 * theta_v_klo);
623  }
624  Real zval0 = zval, Rib0 = Rib;
625 
626  bool above_critical = false;
627  while (!above_critical && ((kpbl + 1) <= khi)) {
628  zval0 = zval;
629  Rib0 = Rib;
630  kpbl += 1;
631 
632  zval = (use_terrain_fitted_coords)
633  ? Compute_Zrel_AtCellCenter(i, j, kpbl, z_nd_arr)
634  : ((kpbl + myhalf) * gdata.CellSize(2) - zib);
635  const Real theta_v = GetThetav(i, j, kpbl, cell_data, moisture_indices);
636  const Real theta_v_klo = amrex::max(GetThetav(i, j, ksrf, cell_data, moisture_indices), one_d);
637  const Real ws2_raw = fourth * ( (uvel(i, j, kpbl) + uvel(i + 1, j, kpbl)) *
638  (uvel(i, j, kpbl) + uvel(i + 1, j, kpbl)) +
639  (vvel(i, j, kpbl) + vvel(i, j + 1, kpbl)) *
640  (vvel(i, j, kpbl) + vvel(i, j + 1, kpbl)) );
641  // Vogelezang & Holtslag (1996): Add shear correction term to denominator instead of ad-hoc floor
642  const Real ws2 = (turbChoice.enable_vh96_shear_correction)
643  ? (ws2_raw + turbChoice.vh96_shear_const_b * us_eff_arr(i,j,0) * us_eff_arr(i,j,0))
644  : amrex::max(ws2_raw, one_d);
645  Rib = CONST_GRAV * zval * (theta_v - t_layer_v) / (ws2 * theta_v_klo);
646  above_critical = (Rib >= Ribcr);
647  }
648 
649  const Real pblh_emp = (use_terrain_fitted_coords)
650  ? Compute_Zrel_AtCellCenter(i, j, ksrf, z_nd_arr)
651  : myhalf * gdata.CellSize(2);
652  const Real z_max = (use_terrain_fitted_coords)
653  ? Compute_Zrel_AtCellCenter(i, j, khi, z_nd_arr)
654  : ((khi + myhalf) * gdata.CellSize(2) - zib);
655  const Real pblh_max = Real(0.9) * z_max;
656  const Real pblh_min = amrex::max(pblh_emp, Real(10.0));
657 
658  if (above_critical) {
659  Real pblh_interp;
660  const Real rib_diff = Rib - Rib0;
661  if (std::abs(rib_diff) > Real(1.0e-10)) {
662  pblh_interp = zval0 + (zval - zval0) / rib_diff * (Ribcr - Rib0);
663  } else {
664  // If Rib not changing, use zval0 as PBL height
665  pblh_interp = zval0;
666  }
667  pblh_corr_arr(i, j, 0) = amrex::max(amrex::min(pblh_interp, pblh_max), pblh_min);
668  pbli_arr(i, j, 0) = kpbl;
669  } else {
670  pblh_corr_arr(i, j, 0) = pblh_min;
671  pbli_arr(i, j, 0) = ksrf + 1;
672  }
673  pblh_floor_arr(i, j, 0) = pblh_min;
674  });
675 
676 
677  // Apply PBLH spatial smoothing if enabled (Seibert et al. 2000 methodology)
678  // Seibert et al. (2000): Review and intercomparison of operational methods
679  // for the determination of the mixing height. Atmospheric Environment, 34, 1001-1027.
680  // Spatial smoothing removes unphysical grid-to-grid noise from discrete Rib-crossing detection
681  if (turbChoice.enable_pblh_smoothing) {
682  // Smooth the tile's own columns, reading the halo the passes above
683  // filled. The result is the same however the domain is split.
684  // The stencil mixes neighbouring columns, so it runs on the
685  // absolute height (the stored convention): the column's surface
686  // height zib goes on over the work box first and comes off
687  // after, and the result is kept at or above the column's floor.
688  // zib is zero without erf.pbl_ib_aware.
689  ParallelFor(xybx_work, [=] AMREX_GPU_DEVICE(int i, int j, int) noexcept {
690  pblh_corr_arr(i, j, 0) += zib_arr(i, j, 0);
691  });
692  ApplyPBLHSmoothing(pbl_height_corrector, xybx_tile,
693  turbChoice.pblh_smoothing_weight,
694  turbChoice.pblh_smoothing_passes,
695  geom.Domain(), geom.periodicity());
696  ParallelFor(xybx_work, [=] AMREX_GPU_DEVICE(int i, int j, int) noexcept {
697  pblh_corr_arr(i, j, 0) = amrex::max(pblh_corr_arr(i, j, 0) - zib_arr(i, j, 0), pblh_floor_arr(i, j, 0));
698  });
699  }
700 
701  // Copy corrected PBL height into pblh_mf for SurfaceLayer storage.
702  // pbl_height_corrector only covers this tile (grown by one), so loop over
703  // the tile, not the valid box: with tiling in x/y the valid box reaches
704  // past it. Under TileNoZ the tile spans the full column, and pblh_mf has
705  // no ghost cells, so the tiles together still fill every cell.
706  {
707  auto pblh_out = pblh_mf.array(mfi);
708  const Box& tbx = mfi.tilebox();
709  // The scheme measures its height from the top of an immersed column
710  // (zib); the stored field is the absolute height above the domain
711  // bottom, the convention of YSUNew and of every consumer of
712  // SurfaceLayer::get_pblh(). zib is zero without erf.pbl_ib_aware.
713  ParallelFor(tbx, [=] AMREX_GPU_DEVICE(int i, int j, int k) noexcept {
714  pblh_out(i, j, k) = pblh_corr_arr(i, j, 0) + zib_arr(i, j, 0);
715  });
716  }
717  //
718  // PASS 4 (WSTAR RECOMPUTE): Recompute wstar, HGAMT, HGAMQ using the corrected
719  // PBL height (pblh_corr_arr) to ensure internal consistency between the K-profile
720  // amplitude and the countergradient fluxes. HOL = sf*h/L uses pblh_corr_arr.
721  // WRF reference (module_bl_mrf.F lines 857-880):
722  // https://github.com/wrf-model/WRF/blob/master/phys/module_bl_mrf.F#L857-L880
723  //
724  ParallelFor(xybx, [=] AMREX_GPU_DEVICE(int i, int j, int) noexcept
725  {
726  const int ksrf = ksurf_arr(i, j, 0); // local surface: first fluid cell of the column
727  const Real zib = zib_arr(i, j, 0); // its height, subtracted from every height below
728  amrex::ignore_unused(zib);
729  //const Real t_layer = t10_eff_arr(i, j, 0);
730  Real obuk_val = ol_eff_arr(i, j, 0);
731  if (std::abs(obuk_val) < amrex::Real(1.0e-10)) {
732  obuk_val = (obuk_val >= Real(0)) ? amrex::Real(1.0e-10) : amrex::Real(-1.0e-10);
733  }
734 
735  // HOL now uses corrected PBLH for full internal consistency
736  const Real HOL = sf * pblh_corr_arr(i, j, 0) / obuk_val;
737  const Real HOL_bounded = amrex::max(amrex::min(HOL, Real(100.0)), Real(-100.0));
738  // |HOL|, so the base 1 + 16*HOL_abs of the unstable arm below equals its
739  // 1 - 16*HOL there and is at least 1 for every HOL; a guard written inside the
740  // arm gets folded away by the optimiser and the pow hoisted (see sqrt_neg_Ri).
741  const Real HOL_abs = std::abs(HOL_bounded);
742  const Real one_quarter = Real(0.25);
743  // Enable QNSE stable functions if requested, otherwise use default linear form
744  const Real enable_qnse_d = (turbChoice.enable_qnse_stable_functions) ? Real(1.0) : Real(0.0);
745  const Real qnse_am_d = turbChoice.qnse_am;
746  const Real qnse_bm_d = turbChoice.qnse_bm;
747  const Real phiM = (obuk_val > 0)
748  ? (enable_qnse_d > Real(0.5)
749  ? (1 + qnse_am_d * HOL_bounded) / (1 + qnse_bm_d * HOL_bounded)
750  : (1 + 5 * HOL_bounded))
751  : std::pow(1 + 16 * HOL_abs, -one_quarter);
752  const Real phiM_safe = amrex::max(phiM, Real(0.01));
753 
754  // Absolute bounds [0.01, 5.0] m/s
755  // Deardorff (1972) convective velocity scale — well-behaved as u_star -> 0.
756  // wstar_conv = (g/theta_v0 * zi * wthv0)^(1/3)
757  const Real theta_v0 = amrex::max(GetThetav(i, j, ksrf, cell_data, moisture_indices), Real(1.0));
758  const Real wthv0 = amrex::max(-us_eff_arr(i,j,0) * ts_eff_arr(i,j,0), Real(0.0)); // surface buoyancy flux, unstable only
759  // Corrected height: this pass exists to make the K-profile amplitude and the
760  // countergradient fluxes consistent with the height Pass 3 produced, and HOL
761  // above already uses it. (Pass 2 has only the predictor height to work with.)
762  const Real zi = amrex::max(pblh_corr_arr(i, j, 0), Real(10.0));
763  const Real wstar_conv = std::cbrt(CONST_GRAV / theta_v0 * zi * wthv0);
764 
765  Real wstar_shear = us_eff_arr(i, j, 0) / phiM_safe;
766 
767  // Blend: standard convective/shear combination (WRF YSU-style),
768  // weighted by cube sum so either term can dominate smoothly
769  Real wstar = std::cbrt(wstar_shear*wstar_shear*wstar_shear +
770  wstar_conv*wstar_conv*wstar_conv);
771  //Real wstar = us_eff_arr(i, j, 0) / phiM_safe;
772  wstar = amrex::max(wstar, Real(0.01));
773  wstar = amrex::min(wstar, Real(5.0));
774  wstar_arr(i, j, 0) = wstar;
775  bool SFCFLG = (obuk_val <= Real(0));
776  // WRF caps the heat countergradient at GAMCRT and then floors it at zero:
777  // HGAMT(I)=MIN(GAMFAC*HFX(I)/CPM(I),GAMCRT) (module_bl_mrf.F L874)
778  // HGAMT(I)=MAX(HGAMT(I),0.0) (module_bl_mrf.F L880)
779  // The floor is not redundant here. The unstable branch is selected on
780  // sign(L), but the numerator is the sensible flux -u_* theta_* alone: over
781  // water with a small downward sensible flux and a large latent flux, L is
782  // negative while -u_* theta_* is not. Without the floor the term would then
783  // add to the down-gradient flux rather than oppose it. VPERT in Pass 2 is
784  // deliberately built from the unfloored value, as in WRF (L877 precedes L880).
785  const Real HGAMT = (SFCFLG && enable_mrf_countergradient)
786  ? amrex::max(amrex::min(-const_b * us_eff_arr(i, j, 0)
787  * ts_eff_arr(i, j, 0) / wstar, GAMCRT), Real(0))
788  : Real(0);
789 
790  Real HGAMQ = Real(0);
791  if (SFCFLG && use_moisture && enable_mrf_countergradient) {
792  const Real q_star = qs_eff_arr(i, j, 0);
793  const Real HGAMQ_calc = -const_b * us_eff_arr(i, j, 0) * q_star / wstar;
794  HGAMQ = amrex::max(amrex::min(HGAMQ_calc, GAMCRQ), Real(0));
795 
796  if (lmask_arr) {
797  bool is_land = (lmask_arr(i,j,0) == 1);
798  if (!is_land) HGAMQ = Real(0);
799  }
800 
801  if (moisture_indices.qv >= 0) {
802  Real qv_klo = cell_data(i, j, ksrf, moisture_indices.qv) / cell_data(i, j, ksrf, Rho_comp);
803  Real T_klo = getTgivenRandRTh(cell_data(i, j, ksrf, Rho_comp),
804  cell_data(i, j, ksrf, RhoTheta_comp), qv_klo);
805  Real p_klo = getPgivenRTh(cell_data(i, j, ksrf, RhoTheta_comp), qv_klo) * Real(0.01);
806  Real qsat_klo = Real(0);
807  erf_qsatw(T_klo, p_klo, qsat_klo);
808  Real rh_klo = (qsat_klo > Real(1.0e-10)) ? (qv_klo / qsat_klo) : Real(0);
809  if (rh_klo > Real(0.95)) {
810  Real rh_scaling = amrex::max(Real(0), (Real(1) - rh_klo) / Real(0.05));
811  HGAMQ *= rh_scaling;
812  }
813  }
814  }
815 
816  if (pbli_arr(i, j, 0) <= ksrf + 1) {
817  hgamt_arr(i, j, 0) = Real(0);
818  hgamq_arr(i, j, 0) = Real(0);
819  } else {
820  const Real pblh = pblh_corr_arr(i, j, 0);
821  if (pblh > Real(1.0e-10)) {
822  hgamt_arr(i, j, 0) = (enable_mrf_countergradient) ? HGAMT / pblh : Real(0);
823  hgamq_arr(i, j, 0) = (enable_mrf_countergradient && use_moisture) ? HGAMQ / pblh : Real(0);
824  } else {
825  hgamt_arr(i, j, 0) = Real(0);
826  hgamq_arr(i, j, 0) = Real(0);
827  }
828  }
829  });
830  //
831  // PASS 5 (ZERO-RI): Diagnostic PBL height with Ribcr=0, VPERT-enhanced surface temp.
832  // Used optionally (pbl_mrf_use_zero_ri_extent) to extend the nonlocal mixing region.
833  // WRF reference (module_bl_mrf.F lines 932-964):
834  // https://github.com/wrf-model/WRF/blob/master/phys/module_bl_mrf.F#L932-L964
835  //
836  constexpr Real Ribcr_zero = Real(0);
837  ParallelFor(xybx, [=,one_d=one]
838  AMREX_GPU_DEVICE(int i, int j, int) noexcept
839  {
840  const int ksrf = ksurf_arr(i, j, 0); // local surface: first fluid cell of the column
841  const Real zib = zib_arr(i, j, 0); // its height, subtracted from every height below
842  amrex::ignore_unused(zib);
843  const Real t_layer = t10_eff_arr(i, j, 0);
844  const Real moisture_fraction = use_moisture ? q10_eff_arr(i, j, 0) : zero;
845  const Real t_layer_v = t_layer * (one + epsv * moisture_fraction);
846  const Real t_layer_v_enhanced = t_layer_v + vpert_arr(i, j, 0);
847 
848  int kpbl_zero = ksrf;
849  Real zval_zero, Rib_zero;
850  {
851  zval_zero = (use_terrain_fitted_coords)
852  ? Compute_Zrel_AtCellCenter(i, j, kpbl_zero, z_nd_arr)
853  : ((kpbl_zero + myhalf) * gdata.CellSize(2) - zib);
854  const Real theta_v = GetThetav(i, j, kpbl_zero, cell_data, moisture_indices);
855  const Real theta_v_klo = amrex::max(GetThetav(i, j, ksrf, cell_data, moisture_indices), one_d);
856  const Real ws2_raw = fourth * ( (uvel(i, j, kpbl_zero) + uvel(i + 1, j, kpbl_zero)) *
857  (uvel(i, j, kpbl_zero) + uvel(i + 1, j, kpbl_zero)) +
858  (vvel(i, j, kpbl_zero) + vvel(i, j + 1, kpbl_zero)) *
859  (vvel(i, j, kpbl_zero) + vvel(i, j + 1, kpbl_zero)) );
860  // Vogelezang & Holtslag (1996): Add shear correction term to denominator instead of ad-hoc floor
861  const Real ws2 = (turbChoice.enable_vh96_shear_correction)
862  ? (ws2_raw + turbChoice.vh96_shear_const_b * us_eff_arr(i,j,0) * us_eff_arr(i,j,0))
863  : amrex::max(ws2_raw, one_d);
864  Rib_zero = CONST_GRAV * zval_zero * (theta_v - t_layer_v_enhanced) / (ws2 * theta_v_klo);
865  }
866 
867  bool above_critical_zero = false;
868  while (!above_critical_zero && ((kpbl_zero + 1) <= khi)) {
869  kpbl_zero += 1;
870  zval_zero = (use_terrain_fitted_coords)
871  ? Compute_Zrel_AtCellCenter(i, j, kpbl_zero, z_nd_arr)
872  : ((kpbl_zero + myhalf) * gdata.CellSize(2) - zib);
873  const Real theta_v = GetThetav(i, j, kpbl_zero, cell_data, moisture_indices);
874  const Real theta_v_klo = amrex::max(GetThetav(i, j, ksrf, cell_data, moisture_indices), one_d);
875  const Real ws2_raw = fourth * ( (uvel(i, j, kpbl_zero) + uvel(i + 1, j, kpbl_zero)) *
876  (uvel(i, j, kpbl_zero) + uvel(i + 1, j, kpbl_zero)) +
877  (vvel(i, j, kpbl_zero) + vvel(i, j + 1, kpbl_zero)) *
878  (vvel(i, j, kpbl_zero) + vvel(i, j + 1, kpbl_zero)) );
879  // Vogelezang & Holtslag (1996): Add shear correction term to denominator instead of ad-hoc floor
880  const Real ws2 = (turbChoice.enable_vh96_shear_correction)
881  ? (ws2_raw + turbChoice.vh96_shear_const_b * us_eff_arr(i,j,0) * us_eff_arr(i,j,0))
882  : amrex::max(ws2_raw, one_d);
883  Rib_zero = CONST_GRAV * zval_zero * (theta_v - t_layer_v_enhanced) / (ws2 * theta_v_klo);
884  above_critical_zero = (Rib_zero >= Ribcr_zero);
885  }
886 
887  if (above_critical_zero) {
888  pbli_zero_arr(i, j, 0) = kpbl_zero;
889  } else {
890  pbli_zero_arr(i, j, 0) = ksrf + 1;
891  }
892  });
893 
894  // -- Compute diffusion coefficients --
895 
896  const Array4<Real>& K_turb = eddyViscosity.array(mfi);
897 
898  bool c_ext_dir_on_zlo = ((bc_ptr[BCVars::cons_bc].lo(2) == ERFBCType::ext_dir));
899  bool c_ext_dir_on_zhi = ((bc_ptr[BCVars::cons_bc].hi(2) == ERFBCType::ext_dir));
900  bool u_ext_dir_on_zlo = ((bc_ptr[BCVars::xvel_bc].lo(2) == ERFBCType::ext_dir));
901  bool u_ext_dir_on_zhi = ((bc_ptr[BCVars::xvel_bc].hi(2) == ERFBCType::ext_dir));
902  bool v_ext_dir_on_zlo = ((bc_ptr[BCVars::yvel_bc].lo(2) == ERFBCType::ext_dir));
903  bool v_ext_dir_on_zhi = ((bc_ptr[BCVars::yvel_bc].hi(2) == ERFBCType::ext_dir));
904 
905  const auto& dxInv = geom.InvCellSizeArray();
906  const Real dz_inv = geom.InvCellSize(2);
907  const int izmin = geom.Domain().smallEnd(2);
908  const int izmax = geom.Domain().bigEnd(2);
909 
910  // Blending parameters captured as scalars for GPU lambda.
911  const amrex::Real l_blend_length = turbChoice.pbl_blend_length;
912  const amrex::Real l_blend_cs = turbChoice.pbl_blend_cs;
913  const amrex::Real l_blend_cmax = turbChoice.pbl_blend_c_max;
914  //const bool l_use_smag_ceil= turbChoice.pbl_blend_use_smag;
915  // dx: use horizontal spacing at this level (assume dx = dy for regular grids).
916  const amrex::Real l_dx = geom.CellSize(0);
917 
918  ParallelFor(gbx, [=] AMREX_GPU_DEVICE(int i, int j, int k) noexcept
919  {
920  const int ksrf = ksurf_arr(i, j, 0); // local surface: first fluid cell of the column
921  const Real zib = zib_arr(i, j, 0); // its height, subtracted from every height below
922  amrex::ignore_unused(zib);
923  Real obuk_val = ol_eff_arr(i, j, 0);
924  if (std::abs(obuk_val) < amrex::Real(1.0e-10)) {
925  obuk_val = (obuk_val >= Real(0)) ? amrex::Real(1.0e-10) : amrex::Real(-1.0e-10);
926  }
927 
928  const Real zval = (use_terrain_fitted_coords)
929  ? Compute_Zrel_AtCellCenter(i, j, k, z_nd_arr)
930  : ((k + myhalf) * gdata.CellSize(2) - zib);
931  const Real rho = cell_data(i, j, k, Rho_comp);
932  // Guard: skip lateral ghost cells that may have uninitialized density.
933  // Prevents division-by-zero inside GetThetav at lateral ghost cells of gbx.
934  // REGRID NOTE: On regrid, SurfaceLayer is reallocated and PBLH returns to sentinel.
935  // The driver must call ComputeDiffusivityMRF (or an equivalent bootstrap pass)
936  // before consuming PBLH in update_fluxes() or Beljaars correction after any regrid.
937  if (rho <= Real(0) || k < ksrf) { // no diffusivity inside an immersed solid
938  K_turb(i, j, k, EddyDiff::Mom_v) = Real(0);
939  K_turb(i, j, k, EddyDiff::Theta_v) = Real(0);
940  K_turb(i, j, k, EddyDiff::Q_v) = Real(0);
941  K_turb(i, j, k, EddyDiff::HGAMT_v) = Real(0);
942  K_turb(i, j, k, EddyDiff::HGAMQ_v) = Real(0);
943  K_turb(i, j, k, EddyDiff::Turb_lengthscale) = Real(0);
944  return;
945  }
946  const Real met_h_zeta = (use_terrain_fitted_coords)
947  ? Compute_h_zeta_AtCellCenter(i, j, k, dxInv, z_nd_arr) : Real(1);
948  const Real dz_terrain = met_h_zeta / dz_inv;
949 
950  constexpr Real qc_threshold = Real(1.0e-4); // Cloud water/ice threshold (kg/kg)
951  Real qc_mix = Real(0);
952  Real qi_mix = Real(0);
953  if (use_moisture) {
954  if (moisture_indices.qc >= 0) {
955  qc_mix = cell_data(i, j, k, moisture_indices.qc) / rho;
956  }
957  if (moisture_indices.qi >= 0) {
958  qi_mix = cell_data(i, j, k, moisture_indices.qi) / rho;
959  }
960  }
961  const Real total_qcloud = qc_mix + qi_mix;
962  const bool has_cloud = turbChoice.enable_mrf_cloud_adjustment && (total_qcloud > qc_threshold);
963 
964  const int pbli_extent = turbChoice.pbl_mrf_use_zero_ri_extent ? pbli_zero_arr(i, j, 0) : pbli_arr(i, j, 0);
965 
966  if (k < pbli_extent) {
967  const Real HOL = sf * pblh_corr_arr(i, j, 0) / obuk_val;
968  const Real HOL_bounded = amrex::max(amrex::min(HOL, Real(100.0)), Real(-100.0));
969  // |HOL|, so the base 1 + 16*HOL_abs of the unstable arm below equals its
970  // 1 - 16*HOL there and is at least 1 for every HOL; a guard written inside the
971  // arm gets folded away by the optimiser and the pow hoisted (see sqrt_neg_Ri).
972  const Real HOL_abs = std::abs(HOL_bounded);
973 
974  const Real one_quarter = Real(0.25);
975  // Enable QNSE stable functions if requested, otherwise use default linear form.
976  const Real enable_qnse_d = (turbChoice.enable_qnse_stable_functions) ? Real(1.0) : Real(0.0);
977  const Real qnse_am_d = turbChoice.qnse_am;
978  const Real qnse_bm_d = turbChoice.qnse_bm;
979  const Real qnse_ah_d = turbChoice.qnse_ah;
980  const Real qnse_bh_d = turbChoice.qnse_bh;
981  const Real phiM = (obuk_val > 0)
982  ? (enable_qnse_d > Real(0.5)
983  ? (1 + qnse_am_d * HOL_bounded) / (1 + qnse_bm_d * HOL_bounded)
984  : (1 + 5 * HOL_bounded))
985  : std::pow(1 + 16 * HOL_abs, -one_quarter);
986  const Real phit = (obuk_val > 0)
987  ? (enable_qnse_d > Real(0.5)
988  ? (1 + qnse_ah_d * HOL_bounded) / (1 + qnse_bh_d * HOL_bounded)
989  : (1 + 5 * HOL_bounded))
990  : std::pow(1 + 16 * HOL_abs, -Real(0.5));
991 
992  Real phit_cloud = phit;
993  Real phiM_cloud = phiM;
994  if (has_cloud && obuk_val > Real(0)) {
995  Real reduction_factor = Real(1) - Real(0.15) * amrex::min(total_qcloud / qc_threshold, Real(1));
996  // Apply cloud reduction on top of QNSE if enabled
997  if (enable_qnse_d > Real(0.5)) {
998  phiM_cloud = ((1 + qnse_am_d * HOL_bounded * reduction_factor) / (1 + qnse_bm_d * HOL_bounded));
999  phit_cloud = ((1 + qnse_ah_d * HOL_bounded * reduction_factor) / (1 + qnse_bh_d * HOL_bounded));
1000  } else {
1001  phiM_cloud = Real(1) + Real(5.0) * reduction_factor * sf * pblh_corr_arr(i, j, 0) / obuk_val;
1002  phit_cloud = Real(1) + Real(5.0) * reduction_factor * sf * pblh_corr_arr(i, j, 0) / obuk_val;
1003  }
1004  } else if (has_cloud && obuk_val <= Real(0)) {
1005  Real cloud_boost = Real(1.0) + Real(0.05) * amrex::min(total_qcloud / qc_threshold, Real(1));
1006  phiM_cloud = std::pow(Real(1) + Real(16.0) * HOL_abs / cloud_boost, -one_quarter);
1007  phit_cloud = std::pow(Real(1) + Real(16.0) * HOL_abs / cloud_boost, -Real(0.5));
1008  }
1009 
1010  const Real phiM_eff = phiM_cloud;
1011  const Real phit_eff = phit_cloud;
1012 
1013  Real Prt_base = phit_eff / phiM_eff;
1014  const Real Prt = amrex::min(amrex::max(Prt_base + const_b * KAPPA * sf, prmin), prmax);
1015 
1016  // wstar is WRF's WSCALE: u_*/phi_m in a stable or neutral column (the
1017  // convective part is floored at zero by the surface buoyancy flux), and
1018  // the cube-sum blend with w_* when the surface flux is upward.
1019  const Real wstar = wstar_arr(i, j, 0);
1020 
1021  // K-profile: K = rho * wstar * kappa * z * (1 - (z - zl1)/(h - zl1))^2
1022  //
1023  // The first-level offset ZL1 belongs to ZFAC alone: the linear factor
1024  // is the full height above the surface, ZQ(I,K). Subtracting zl1 from
1025  // both drives K_m to exactly zero in the first fluid cell.
1026  // ERF_ComputeDiffusivityYSUNew.cpp:1699-1720 splits them the same way.
1027  //
1028  // zval already measures height from the top of an immersed column (zib
1029  // was subtracted where it was formed), so it is that height directly;
1030  // zl1 is zval evaluated at the column's first fluid cell, so zval - zl1
1031  // is exactly zero there in both the terrain and the non-terrain path.
1032  const Real zl1 = (use_terrain_fitted_coords)
1033  ? Compute_Zrel_AtCellCenter(i, j, ksrf, z_nd_arr)
1034  : ((ksrf + myhalf) * gdata.CellSize(2) - zib);
1035  const Real pblh = pblh_corr_arr(i, j, 0);
1036  // The Rib passes floor pblh at max(zl1, 10), so pblh - zl1 is exactly
1037  // zero for any column whose height lands on that floor with a first
1038  // layer thicker than 20 m -- and zval - zl1 is zero too at k == ksrf,
1039  // making the quotient 0/0. Guard it, as YSUNew does (line 1701).
1040  const Real pblh_rel = amrex::max(pblh - zl1, Real(1.0e-4));
1041  const Real zfac = amrex::max(Real(1) - (zval - zl1) / pblh_rel, Real(1.0e-8));
1042 
1043  K_turb(i, j, k, EddyDiff::Mom_v) = rho * wstar * KAPPA * zval * zfac * zfac;
1044  K_turb(i, j, k, EddyDiff::Theta_v) = K_turb(i, j, k, EddyDiff::Mom_v) / Prt;
1045  K_turb(i, j, k, EddyDiff::Q_v) = K_turb(i, j, k, EddyDiff::Theta_v);
1046  } else if (k >= pbli_extent) {
1047  // Free atmosphere above the PBL top. This is the only branch left that
1048  // reads resolved vertical gradients, but they are not evaluated anywhere
1049  // near the surface.
1050  const Real lambda = Real(150.0);
1051  const Real lscale = (KAPPA * zval * lambda) / (KAPPA * zval + lambda);
1052  Real dthetadz, dudz, dvdz;
1053  ComputeVerticalDerivativesPBL(i, j, k, uvel, vvel, cell_data, izmin, izmax, pbl_derivative_dz_inv(i,j,k),
1054  c_ext_dir_on_zlo, c_ext_dir_on_zhi, u_ext_dir_on_zlo,
1055  u_ext_dir_on_zhi, v_ext_dir_on_zlo, v_ext_dir_on_zhi, dthetadz,
1056  dudz, dvdz, moisture_indices, pbl_strat);
1057 
1058  const Real dudz_safe = (k < izmax) ? dudz : Real(0);
1059  const Real dvdz_safe = (k < izmax) ? dvdz : Real(0);
1060  const Real wind_shear = dudz_safe * dudz_safe + dvdz_safe * dvdz_safe;
1061  const Real wind_shear_safe = std::max(wind_shear, Real(1.0e-8));
1062  const Real theta_v = amrex::max(GetThetav(i, j, k, cell_data, moisture_indices), Real(1.0));
1063  const Real dtheta_v_dz = dthetadz;
1064 
1065  Real grad_Ri = CONST_GRAV / theta_v * dtheta_v_dz / wind_shear_safe;
1066  grad_Ri = std::max(std::min(grad_Ri, Real(100.0)), -Real(100.0));
1067 
1068  const Real grad_Ri_safe = amrex::max(grad_Ri, -Real(100.0));
1069  // sqrt(|Ri|), which equals the sqrt(-Ri) of the unstable arm below and is valid
1070  // for every Ri. Written as max(-Ri, 0) inside the arm, the optimiser used the
1071  // arm's condition to drop the max and then hoisted the bare sqrt above the
1072  // selection, where Ri can be positive; written as min(Ri, 0) it folded the
1073  // sqrt into the arms of the min. Either way an invalid operation whose
1074  // result is discarded but whose flag kills the run under
1075  // amrex.fpe_trap_invalid. There is nothing to fold in the absolute value.
1076  const Real sqrt_neg_Ri = std::sqrt(std::abs(grad_Ri_safe));
1077  Real Pr = Real(1) + Real(2.1) * grad_Ri;
1078  const Real fm = (grad_Ri_safe > 0)
1079  ? Real(1) / ((Real(1) + Real(5.0) * grad_Ri_safe) * (Real(1) + Real(5.0) * grad_Ri_safe))
1080  : 1 - 8 * grad_Ri_safe / (1 + Real(1.746) * sqrt_neg_Ri);
1081  const Real ft = (grad_Ri_safe > 0)
1082  ? Real(1) / ((Real(1) + Real(5.0) * grad_Ri_safe) * (Real(1) + Real(5.0) * grad_Ri_safe))
1083  : 1 - 8 * grad_Ri_safe / (1 + Real(1.286) * sqrt_neg_Ri);
1084  const Real rl2wsp = rho * lscale * lscale * std::sqrt(wind_shear);
1085 
1086  Pr = std::max(amrex::Real(0.25), std::min(Pr, Real(4.0)));
1087 
1088  // In the stable regime, fm and ft are the same function, so the Prandtl
1089  // number has to be applied explicitly or momentum and heat mix
1090  // identically.
1091  //
1092  // In the unstable regime, fm and ft already differ (1.746 vs 1.286) and
1093  // WRF applies no Prandtl factor there.
1094  const Real Pr_mom = (grad_Ri_safe > 0) ? Pr : Real(1);
1095 
1096  K_turb(i, j, k, EddyDiff::Mom_v) = rl2wsp * fm * Pr_mom;
1097  K_turb(i, j, k, EddyDiff::Theta_v) = rl2wsp * ft;
1098  K_turb(i, j, k, EddyDiff::Q_v) = K_turb(i, j, k, EddyDiff::Theta_v);
1099  }
1100 
1101  // Scale-aware blending for grey-zone resolution (Boutle et al. 2014).
1102  // ERF_PBLScaleAwareBlending.H. Gated by pbl_blend_length > 0.
1103  // Applied to Theta_v and Q_v. Mom_v is not modified.
1104  //
1105  // K_turb holds rho*K [kg/m/s], but the ceiling applied inside
1106  // pbl_kh_blend_and_cap is a bare diffusivity [m^2/s]. Hand the helper the
1107  // kinematic value and restore the density weighting on the way out; passing
1108  // rho*K straight through would make the effective cap scale like 1/rho and
1109  // hence increase with height (issue #3580).
1110  if (l_blend_length > 0.0) {
1111  const Real rho_inv = Real(1.0) / rho;
1112 
1113  // For MRF, use power-law ceiling (SmnSmn not available)
1114  K_turb(i, j, k, EddyDiff::Theta_v) = rho * pbl_kh_blend_and_cap(
1115  K_turb(i, j, k, EddyDiff::Theta_v) * rho_inv,
1116  l_dx, l_blend_length, l_blend_cs, l_blend_cmax,
1117  amrex::Real(-1.0), false);
1118 
1119  K_turb(i, j, k, EddyDiff::Q_v) = rho * pbl_kh_blend_and_cap(
1120  K_turb(i, j, k, EddyDiff::Q_v) * rho_inv,
1121  l_dx, l_blend_length, l_blend_cs, l_blend_cmax,
1122  amrex::Real(-1.0), false);
1123  }
1124 
1125  // Limit diffusion coefficients to physical bounds
1126  // Hong & Pan (1996): Kmin=0.1, Kmax=300 m^2/s (module_bl_mrf.F lines 1014-1025)
1127  // Hong et al. (2006): Kmin=ckz*dz*rho, Kmax=1000 m^2/s (high-res option)
1128  Real rhoKmin, rhoKmax;
1129  if (turbChoice.pbl_mrf_highres_bounds) {
1130  constexpr Real ckz = Real(0.001);
1131  constexpr Real Kmax = Real(1000.0);
1132  rhoKmin = ckz * dz_terrain * rho;
1133  rhoKmax = rho * Kmax;
1134  } else {
1135  constexpr Real Kmin = Real(0.1);
1136  constexpr Real Kmax = Real(300.0);
1137  rhoKmin = rho * Kmin;
1138  rhoKmax = rho * Kmax;
1139  }
1140 
1141  K_turb(i, j, k, EddyDiff::Mom_v) = std::max(
1142  std::min(K_turb(i, j, k, EddyDiff::Mom_v), rhoKmax), rhoKmin);
1143  K_turb(i, j, k, EddyDiff::Theta_v) = std::max(
1144  std::min(K_turb(i, j, k, EddyDiff::Theta_v), rhoKmax), rhoKmin);
1145  K_turb(i, j, k, EddyDiff::Q_v) = std::max(
1146  std::min(K_turb(i, j, k, EddyDiff::Q_v), rhoKmax), rhoKmin);
1147  K_turb(i, j, k, EddyDiff::Turb_lengthscale) = pblh_corr_arr(i, j, 0) + zib; // absolute height, as stored
1148 
1149  // IMPORTANT — units convention for HGAMT_v / HGAMQ_v:
1150  // These are stored as (countergradient term / pblh), i.e. already
1151  // divided by the PBL height, NOT as a raw physical gradient needing
1152  // an extra 1/dz scaling downstream.
1153  //
1154  // The implicit solver (ERF_ImplicitDiff_T.cpp, ImplicitDiffForStateLU_T)
1155  // multiplies this stored value by `Fact = implicit_fac * dt * dz_inv`
1156  // and `rhoAlpha / met_h_zeta` ONLY — it must NOT apply an additional
1157  // dz_inv factor. `Fact` already carries the single grid-spacing
1158  // factor needed to convert the countergradient correction into a
1159  // flux-divergence contribution consistent with the K*d(phi)/dz terms
1160  // it is being added alongside. Adding a second dz_inv
1161  // double-counts the grid spacing and is
1162  // dimensionally incorrect — do NOT "fix" this by inserting dz_inv
1163  // into the RHS_a += Fact * gam_hi/lo terms in ERF_ImplicitDiff_T.cpp.
1164  if (k < pbli_extent) {
1165  K_turb(i, j, k, EddyDiff::HGAMT_v) = hgamt_arr(i, j, 0);
1166  K_turb(i, j, k, EddyDiff::HGAMQ_v) = hgamq_arr(i, j, 0);
1167  } else {
1168  K_turb(i, j, k, EddyDiff::HGAMT_v) = Real(0);
1169  K_turb(i, j, k, EddyDiff::HGAMQ_v) = Real(0);
1170  }
1171  });
1172 
1173  // FOEXTRAP top and bottom ghost cells
1174  ParallelFor(xybx, [=] AMREX_GPU_DEVICE(int i, int j, int ) noexcept
1175  {
1176  K_turb(i, j, klo-1, EddyDiff::Mom_v ) = K_turb(i, j, klo, EddyDiff::Mom_v );
1177  K_turb(i, j, klo-1, EddyDiff::Theta_v) = K_turb(i, j, klo, EddyDiff::Theta_v);
1178  K_turb(i, j, klo-1, EddyDiff::Q_v ) = K_turb(i, j, klo, EddyDiff::Q_v );
1179  K_turb(i, j, klo-1, EddyDiff::HGAMT_v) = K_turb(i, j, klo, EddyDiff::HGAMT_v);
1180  K_turb(i, j, klo-1, EddyDiff::HGAMQ_v) = K_turb(i, j, klo, EddyDiff::HGAMQ_v);
1181  K_turb(i, j, klo-1, EddyDiff::Turb_lengthscale) = K_turb(i, j, klo, EddyDiff::Turb_lengthscale);
1182  K_turb(i, j, khi+1, EddyDiff::Mom_v ) = K_turb(i, j, khi, EddyDiff::Mom_v );
1183  K_turb(i, j, khi+1, EddyDiff::Theta_v) = K_turb(i, j, khi, EddyDiff::Theta_v);
1184  K_turb(i, j, khi+1, EddyDiff::Q_v ) = K_turb(i, j, khi, EddyDiff::Q_v );
1185  K_turb(i, j, khi+1, EddyDiff::HGAMT_v) = K_turb(i, j, khi, EddyDiff::HGAMT_v);
1186  K_turb(i, j, khi+1, EddyDiff::HGAMQ_v) = K_turb(i, j, khi, EddyDiff::HGAMQ_v);
1187  K_turb(i, j, khi+1, EddyDiff::Turb_lengthscale) = K_turb(i, j, khi, EddyDiff::Turb_lengthscale);
1188  });
1189  }// mfi
1190  // Write MRF-computed PBLH back into SurfaceLayer so Beljaars correction
1191  // and diagnostics can use it, and update_pblh no longer aborts for MRF type.
1192  SurfLayer->set_pblh(level, pblh_mf);
1193 }
constexpr amrex::Real epsv
Definition: ERF_Constants.H:52
constexpr amrex::Real KAPPA
Definition: ERF_Constants.H:67
constexpr amrex::Real CONST_GRAV
Definition: ERF_Constants.H:68
AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE amrex::Real getTgivenRandRTh(const amrex::Real rho, const amrex::Real rhotheta, const amrex::Real qv=amrex::Real(0))
Definition: ERF_EOS.H:46
AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE amrex::Real getPgivenRTh(const amrex::Real rhotheta, const amrex::Real qv=amrex::Real(0))
Definition: ERF_EOS.H:81
#define Rho_comp
Definition: ERF_IndexDefines.H:42
#define RhoTheta_comp
Definition: ERF_IndexDefines.H:43
amrex::GpuArray< Real, AMREX_SPACEDIM > dxInv
Definition: ERF_InitCustomPertVels_ParticleTests.H:17
const int klo
Definition: ERF_InitCustomPert_ABL.H:75
const bool use_moisture
Definition: ERF_InitCustomPert_ABL.H:71
const int khi
Definition: ERF_InitCustomPert_Bubble.H:21
AMREX_ALWAYS_ASSERT(bx.length()[2]==khi+1)
AMREX_ALWAYS_ASSERT_WITH_MESSAGE(m_cloud_chamber_config.active, "Cloud Chamber: initializer reached without a parsed configuration")
AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE void erf_qsatw(amrex::Real t, amrex::Real p, amrex::Real &qsatw)
Definition: ERF_MicrophysicsUtils.H:264
AMREX_GPU_DEVICE AMREX_FORCE_INLINE amrex::Real GetThetav(const int &i, const int &j, const int &k, const amrex::Array4< amrex::Real const > &cell_data, const MoistureComponentIndices &moisture_indices)
Definition: ERF_MoistUtils.H:74
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 one
Definition: ERF_NumericalConstants.H:37
constexpr amrex::Real fourth
Definition: ERF_NumericalConstants.H:42
constexpr amrex::Real zero
Definition: ERF_NumericalConstants.H:36
constexpr amrex::Real myhalf
Definition: ERF_NumericalConstants.H:41
AMREX_GPU_DEVICE AMREX_FORCE_INLINE void ComputeVerticalDerivativesPBL(int i, int j, int k, const amrex::Array4< const amrex::Real > &uvel, const amrex::Array4< const amrex::Real > &vvel, const amrex::Array4< const amrex::Real > &cell_data, const int izmin, const int izmax, const PBLDerivativeDzInv &dz_inv, const bool c_ext_dir_on_zlo, const bool c_ext_dir_on_zhi, const bool u_ext_dir_on_zlo, const bool u_ext_dir_on_zhi, const bool v_ext_dir_on_zlo, const bool v_ext_dir_on_zhi, amrex::Real &dthetadz, amrex::Real &dudz, amrex::Real &dvdz, const MoistureComponentIndices &moisture_indices, const StratType strat=StratType::thetav)
Definition: ERF_PBLModels.H:304
void ApplyPBLHSmoothing(amrex::FArrayBox &pblh_fab, const amrex::Box &xybx_valid, const amrex::Real weight, const int passes, const amrex::Box &domain, const amrex::Periodicity &periodicity)
Apply spatial smoothing to PBLH field using 5-point stencil.
Definition: ERF_PBLModels.H:717
AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE amrex::Real pbl_kh_blend_and_cap(amrex::Real K_h, amrex::Real dx, amrex::Real L_blend, amrex::Real C_s, amrex::Real c_max, amrex::Real SmnSmn, bool use_smag) noexcept
Apply scale-aware blending and ceiling to a single K_h value.
Definition: ERF_PBLScaleAwareBlending.H:133
amrex::Real Real
Definition: ERF_ShocInterface.H:19
AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE amrex::Real Compute_Zrel_AtCellCenter(const int &i, const int &j, const int &k, const amrex::Array4< const amrex::Real > &z_nd)
Definition: ERF_TerrainMetrics.H:758
AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE amrex::Real Compute_h_zeta_AtCellCenter(const int &i, const int &j, const int &k, const amrex::GpuArray< amrex::Real, AMREX_SPACEDIM > &cellSizeInv, const amrex::Array4< const amrex::Real > &z_nd)
Definition: ERF_TerrainMetrics.H:197
AMREX_FORCE_INLINE amrex::IntVect TileNoZ()
Definition: ERF_TileNoZ.H:11
@ yvel_bc
Definition: ERF_IndexDefines.H:109
@ cons_bc
Definition: ERF_IndexDefines.H:92
@ xvel_bc
Definition: ERF_IndexDefines.H:108
@ ext_dir
Definition: ERF_IndexDefines.H:300
@ Theta_v
Definition: ERF_IndexDefines.H:253
@ Turb_lengthscale
Definition: ERF_IndexDefines.H:257
@ Q_v
Definition: ERF_IndexDefines.H:256
@ Mom_v
Definition: ERF_IndexDefines.H:252
@ HGAMQ_v
Definition: ERF_IndexDefines.H:264
@ HGAMT_v
Definition: ERF_IndexDefines.H:263
@ ks
Definition: ERF_SLM.H:74
@ rho
Definition: ERF_Kessler.H:25
@ xvel
Definition: ERF_IndexDefines.H:218
@ yvel
Definition: ERF_IndexDefines.H:219
@ zi
Definition: ERF_AdvanceWDM6.cpp:276
int qi
cloud ice
Definition: ERF_DataStruct.H:251
int qv
water vapor
Definition: ERF_DataStruct.H:249
int qc
cloud liquid water
Definition: ERF_DataStruct.H:250
Functor for inverse vertical spacings for terrain-following grids using cell-center heights.
Definition: ERF_PBLModels.H:663
bool enable_mrf_unbounded_vpert
Whether MRF leaves VPERT unlimited by GAMCRT.
Definition: ERF_TurbStruct.H:996
amrex::Real pbl_blend_length
Boutle blending length L [m]. 0 = off.
Definition: ERF_TurbStruct.H:987
bool pbl_mrf_use_zero_ri_extent
Whether MRF uses the Ri=0 K-profile extent.
Definition: ERF_TurbStruct.H:997
amrex::Real qnse_ah
Definition: ERF_TurbStruct.H:1007
StratType pbl_strat_type
Definition: ERF_TurbStruct.H:819
bool enable_vh96_shear_correction
Whether Vogelezang & Holtslag (1996) shear-correction term is enabled.
Definition: ERF_TurbStruct.H:974
bool enable_mrf_cloud_adjustment
Whether MRF cloud-aware stability adjustments are enabled.
Definition: ERF_TurbStruct.H:994
amrex::Real pbl_mrf_const_b
MRF constant used to compute PBL height.
Definition: ERF_TurbStruct.H:982
amrex::Real pblh_smoothing_weight
Center-cell weight in PBLH smoothing stencil (must be in [0,1]).
Definition: ERF_TurbStruct.H:978
amrex::Real pbl_blend_cs
Smagorinsky coeff for K_h ceiling.
Definition: ERF_TurbStruct.H:988
amrex::Real qnse_bm
Definition: ERF_TurbStruct.H:1006
amrex::Real pbl_blend_c_max
Power-law ceiling coeff [m^(2/3)/s].
Definition: ERF_TurbStruct.H:989
amrex::Real qnse_bh
Definition: ERF_TurbStruct.H:1008
bool enable_qnse_stable_functions
Definition: ERF_TurbStruct.H:1003
bool enable_mrf_countergradient
Whether MRF countergradient corrections are enabled.
Definition: ERF_TurbStruct.H:991
bool enable_pblh_smoothing
Whether spatial smoothing of diagnosed PBLH is enabled.
Definition: ERF_TurbStruct.H:976
int pblh_smoothing_passes
Number of PBLH smoothing iterations to apply.
Definition: ERF_TurbStruct.H:977
amrex::Real pbl_ib_z0
Definition: ERF_TurbStruct.H:993
bool pbl_ib_aware
Definition: ERF_TurbStruct.H:992
bool pbl_mrf_highres_bounds
Whether MRF applies high-resolution grid-dependent diffusivity bounds.
Definition: ERF_TurbStruct.H:995
amrex::Real vh96_shear_const_b
Vogelezang & Holtslag (1996) shear-correction constant b.
Definition: ERF_TurbStruct.H:975
amrex::Real pbl_mrf_Ribcr
Critical bulk Richardson number for the MRF PBL scheme.
Definition: ERF_TurbStruct.H:981
amrex::Real qnse_am
Definition: ERF_TurbStruct.H:1005
amrex::Real pbl_mrf_sf
MRF surface flux value used to compute PBL height.
Definition: ERF_TurbStruct.H:983

Referenced by ComputeTurbulentViscosity().

Here is the call graph for this function:
Here is the caller graph for this function: