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 
227 // The surface-layer fields carry one column of halo on the bottom surface; with more
228 // smoothing passes than that, the PBLH passes read them from copies widened to ng_pblh
229 // (see WidenForPBLH). Otherwise the fields are read directly.
230 const auto widen_sfc = [&] (const MultiFab* mf) -> std::unique_ptr<MultiFab> {
231  if (mf == nullptr || std::min(mf->nGrowVect()[0], mf->nGrowVect()[1]) >= ng_pblh) {
232  return nullptr;
233  }
234  return WidenForPBLH(*mf, ng_pblh,
235  [&] (MultiFab& wide) { SurfLayer->fill_planar_boundary(level, wide); });
236 };
237 const std::unique_ptr<MultiFab> u_star_wide = widen_sfc(SurfLayer->get_u_star(level));
238 const std::unique_ptr<MultiFab> t_star_wide = widen_sfc(SurfLayer->get_t_star(level));
239 const std::unique_ptr<MultiFab> q_star_wide = widen_sfc(SurfLayer->get_q_star(level));
240 const std::unique_ptr<MultiFab> olen_wide = widen_sfc(SurfLayer->get_olen(level));
241 const MultiFab* u_star_src = (u_star_wide) ? u_star_wide.get() : SurfLayer->get_u_star(level);
242 const MultiFab* t_star_src = (t_star_wide) ? t_star_wide.get() : SurfLayer->get_t_star(level);
243 const MultiFab* q_star_src = (q_star_wide) ? q_star_wide.get() : SurfLayer->get_q_star(level);
244 const MultiFab* olen_src = (olen_wide) ? olen_wide.get() : SurfLayer->get_olen(level);
245 
246 if (ng_pblh > 1) {
247  int ng_avail = std::min({cons_in.nGrowVect()[0], cons_in.nGrowVect()[1],
248  xvel.nGrowVect()[0], xvel.nGrowVect()[1],
249  yvel.nGrowVect()[0], yvel.nGrowVect()[1],
250  u_star_src->nGrowVect()[0], u_star_src->nGrowVect()[1],
251  olen_src->nGrowVect()[0], olen_src->nGrowVect()[1],
252  (terrain_blank) ? terrain_blank->nGrowVect()[0] : 1000,
253  (terrain_blank) ? terrain_blank->nGrowVect()[1] : 1000});
254  // The PBLH passes also read these fields over the same widened region;
255  // a field that is not allocated (nullptr) is not read.
256  auto limit_by = [&ng_avail] (const auto* mf) {
257  if (mf) { ng_avail = std::min({ng_avail, mf->nGrowVect()[0], mf->nGrowVect()[1]}); }
258  };
259  limit_by(t_star_src);
260  limit_by(q_star_src);
261  limit_by(SurfLayer->get_mac_avg(level, 3));
262  limit_by(SurfLayer->get_mac_avg(level, 4));
263  limit_by(SurfLayer->get_lmask(level));
264  limit_by((use_terrain_fitted_coords) ? z_phys_nd.get() : nullptr);
265  if (ng_pblh > ng_avail) {
266  amrex::Abort("erf.pblh_smoothing_passes = " + std::to_string(turbChoice.pblh_smoothing_passes)
267  + " needs " + std::to_string(ng_pblh) + " halo columns, but the fields the "
268  "PBLH passes read carry only " + std::to_string(ng_avail)
269  + "; reduce erf.pblh_smoothing_passes to at most " + std::to_string(ng_avail));
270  }
271 }
272 
273 #ifdef _OPENMP
274 #pragma omp parallel if (Gpu::notInLaunchRegion())
275 #endif
276 
277  for (MFIter mfi(eddyViscosity, TileNoZ()); mfi.isValid(); ++mfi) {
278 
279  // Box operated on must span fill domain in z-dir
280  const Box& gbx = mfi.growntilebox(IntVect(1,1,0));
281  AMREX_ALWAYS_ASSERT( gbx.smallEnd(2) == klo &&
282  gbx.bigEnd(2) == khi );
283 
284  // create flattened boxes to store PBL height and related quantities
285  const GeometryData gdata = geom.data();
286  const Box xybx = PerpendicularBox<ZDir>(gbx, IntVect{0, 0, 0});
287 
288  // The PBLH passes run on the region the smoothing stencil will consume
289  // (see ng_pblh above). growntilebox() is no use here: it does not grow at
290  // an interior tile edge, so a tile in the middle of a box gets no halo at
291  // all -- which is exactly how the stencil came to read off the end of the
292  // array. Grow the tile box explicitly instead, which costs a little
293  // duplicated work in the overlaps. With smoothing off this is xybx and
294  // nothing is duplicated.
295  const Box gbx_work = (turbChoice.enable_pblh_smoothing)
296  ? amrex::grow(mfi.tilebox(), IntVect(ng_pblh,ng_pblh,0)) : gbx;
297  const Box xybx_work = PerpendicularBox<ZDir>(gbx_work, IntVect{0, 0, 0});
298  const Box xybx_tile = PerpendicularBox<ZDir>(mfi.tilebox(), IntVect{0, 0, 0});
299 
300  // Pass 1 (predictor): PBLH with base surface temperature, no VPERT
301  // Pass 2 (wstar/VPERT): compute wstar, HGAMT, HGAMQ, VPERT from predictor height
302  // Pass 3 (corrector): PBLH with VPERT-enhanced surface temperature (WRF-consistent)
303  // WRF reference (module_bl_mrf.F lines 813-964):
304  // https://github.com/wrf-model/WRF/blob/master/phys/module_bl_mrf.F#L813-L964
305  FArrayBox pbl_height_predictor(xybx_work, 1, The_Async_Arena()); // Pass 1: base t_layer_v
306  FArrayBox pbl_height_corrector(xybx_work, 1, The_Async_Arena()); // Pass 3: VPERT-enhanced
307  IArrayBox pbl_index(xybx_work, 1, The_Async_Arena());
308  IArrayBox pbl_index_zero_ri(xybx_work, 1, The_Async_Arena()); // Index for zero-Ri diagnostic pass
309  FArrayBox hgamt_fab(xybx_work, 1, The_Async_Arena()); // Store HGAMT/h (normalized countergradient)
310  FArrayBox hgamq_fab(xybx_work, 1, The_Async_Arena()); // Store HGAMQ/h (normalized countergradient)
311  FArrayBox wstar_fab(xybx_work, 1, The_Async_Arena()); // Convective velocity scale
312  FArrayBox vpert_fab(xybx_work, 1, The_Async_Arena()); // Virtual temperature perturbation VPERT
313  FArrayBox pblh_floor_fab(xybx_work, 1, The_Async_Arena()); // Floor of the corrected height per column, reapplied after the smoothing
314  const auto& pblh_pred_arr = pbl_height_predictor.array(); // predictor (base t_layer_v)
315  const auto& pblh_corr_arr = pbl_height_corrector.array(); // corrector (VPERT-enhanced)
316  const auto& pblh_floor_arr = pblh_floor_fab.array();
317  const auto& pbli_arr = pbl_index.array();
318  const auto& pbli_zero_arr = pbl_index_zero_ri.array(); // Zero-Ri diagnostic PBL index
319  const auto& hgamt_arr = hgamt_fab.array();
320  const auto& hgamq_arr = hgamq_fab.array();
321  const auto& wstar_arr = wstar_fab.array();
322  const auto& vpert_arr = vpert_fab.array();
323 
324  // Get some data in arrays
325  const auto& cell_data = cons_in.const_array(mfi);
326  const auto& uvel = xvel.const_array(mfi);
327  const auto& vvel = yvel.const_array(mfi);
328 
329  const Real Ribcr = turbChoice.pbl_mrf_Ribcr;
330  const auto& u_star_arr = u_star_src->const_array(mfi);
331  const auto& t_star_arr = t_star_src->const_array(mfi);
332  const auto& l_obuk_arr = olen_src->const_array(mfi);
333  const auto& t10av_arr = SurfLayer->get_mac_avg(level, 3)->const_array(mfi);
334  const auto& q10av_arr = SurfLayer->get_mac_avg(level, 4)->const_array(mfi);
335  const auto& lmask_arr = (SurfLayer->get_lmask(level)) ?
336  SurfLayer->get_lmask(level)->const_array(mfi) :
337  Array4<int>{};
338  // Only retrieve z_phys_nd array if terrain-fitted coordinates are in use
339  const Array4<Real const> z_nd_arr = use_terrain_fitted_coords ? z_phys_nd->array(mfi)
340  : Array4<Real const>{};
341  const PBLDerivativeDzInv_T pbl_derivative_dz_inv{z_phys_cc->const_array(mfi)};
342 
343  //
344  // PASS 1 (PREDICTOR): Compute PBL height using base surface virtual temperature.
345  // No thermal excess (VPERT = 0). Produces pblh_pred_arr and pbli_arr.
346  // WRF reference (module_bl_mrf.F lines 813-842):
347  // https://github.com/wrf-model/WRF/blob/master/phys/module_bl_mrf.F#L813-L842
348  //
349  // FIX: guard against null dereference of q_star when use_moisture is false (dry case)
350  // get_q_star can return null pointer when the field is not allocated
351  const amrex::MultiFab* q_star_mf = q_star_src;
352  Array4<Real const> q_star_arr = (q_star_mf != nullptr) ? q_star_mf->const_array(mfi)
353  : Array4<Real const>{};
354 
355  // --------------------------------------------------------------------
356  // Immersed-boundary awareness (turbChoice.pbl_ib_aware): per column,
357  // the first fluid cell above the immersed solid (ksurf) and its height
358  // above the domain bottom (zib). Every height below is measured from
359  // it, the diffusivities vanish inside the solid, and the surface
360  // scales of a column with solid cells are a neutral log law at its
361  // top instead of the ground surface layer's values (which come from
362  // cells inside the solid there). With the option off, or without
363  // immersed cells, ksurf = klo, zib = 0 and the scales are copies, so
364  // the arithmetic below is unchanged.
365  // --------------------------------------------------------------------
366  const bool l_ib = turbChoice.pbl_ib_aware && (terrain_blank != nullptr);
367  AMREX_ALWAYS_ASSERT_WITH_MESSAGE(!(l_ib && use_terrain_fitted_coords),
368  "erf.pbl_ib_aware is not supported with terrain-fitted coordinates");
369  const Array4<Real const> blank_arr = l_ib ? terrain_blank->const_array(mfi) : Array4<Real const>{};
370  IArrayBox ksurf_fab(xybx_work, 1, The_Async_Arena());
371  FArrayBox zib_fab(xybx_work, 1, The_Async_Arena());
372  FArrayBox us_eff_fab(xybx_work, 1, The_Async_Arena()), ts_eff_fab(xybx_work, 1, The_Async_Arena());
373  FArrayBox qs_eff_fab(xybx_work, 1, The_Async_Arena()), ol_eff_fab(xybx_work, 1, The_Async_Arena());
374  FArrayBox t10_eff_fab(xybx_work, 1, The_Async_Arena()), q10_eff_fab(xybx_work, 1, The_Async_Arena());
375  const auto& ksurf_arr = ksurf_fab.array();
376  const auto& zib_arr = zib_fab.array();
377  const auto& us_eff_arr = us_eff_fab.array(); const auto& ts_eff_arr = ts_eff_fab.array();
378  const auto& qs_eff_arr = qs_eff_fab.array(); const auto& ol_eff_arr = ol_eff_fab.array();
379  const auto& t10_eff_arr = t10_eff_fab.array(); const auto& q10_eff_arr = q10_eff_fab.array();
380  {
381  const Real dz_ib = geom.CellSize(2);
382  const Real z0_ib = turbChoice.pbl_ib_z0;
383  const Array4<Real const>& uv_ib = xvel.const_array(mfi);
384  const Array4<Real const>& vv_ib = yvel.const_array(mfi);
385  const bool have_qstar = (q_star_mf != nullptr);
386  ParallelFor(xybx_work, [=] AMREX_GPU_DEVICE(int i, int j, int) noexcept
387  {
388  int ks = klo;
389  if (l_ib) {
390  for (int kk = klo; kk <= khi; ++kk) { if (blank_arr(i, j, kk) >= Real(0.5)) { ks = kk + 1; } }
391  if (ks > khi) { ks = khi; }
392  }
393  ksurf_arr(i, j, 0) = ks;
394  zib_arr(i, j, 0) = (ks - klo) * dz_ib;
395  if (ks > klo) {
396  // Column with immersed solid: neutral log law at its top.
397  const Real u_top = myhalf * (uv_ib(i, j, ks) + uv_ib(i + 1, j, ks));
398  const Real v_top = myhalf * (vv_ib(i, j, ks) + vv_ib(i, j + 1, ks));
399  const Real ws_top = std::sqrt(u_top * u_top + v_top * v_top);
400  us_eff_arr(i, j, 0) = amrex::max(KAPPA * ws_top / std::log(myhalf * dz_ib / z0_ib), Real(1.0e-3));
401  ts_eff_arr(i, j, 0) = Real(0);
402  qs_eff_arr(i, j, 0) = Real(0);
403  ol_eff_arr(i, j, 0) = immersed_neutral_obukhov_length;
404  t10_eff_arr(i, j, 0) = cell_data(i, j, ks, RhoTheta_comp) / cell_data(i, j, ks, Rho_comp);
405  q10_eff_arr(i, j, 0) = (use_moisture && moisture_indices.qv >= 0)
406  ? cell_data(i, j, ks, moisture_indices.qv) / cell_data(i, j, ks, Rho_comp) : Real(0);
407  } else {
408  us_eff_arr(i, j, 0) = u_star_arr(i, j, 0);
409  ts_eff_arr(i, j, 0) = t_star_arr(i, j, 0);
410  qs_eff_arr(i, j, 0) = have_qstar ? q_star_arr(i, j, 0) : Real(0);
411  ol_eff_arr(i, j, 0) = l_obuk_arr(i, j, 0);
412  t10_eff_arr(i, j, 0) = t10av_arr(i, j, 0);
413  q10_eff_arr(i, j, 0) = use_moisture ? q10av_arr(i, j, 0) : Real(0);
414  }
415  });
416  }
417 
418  ParallelFor(xybx_work, [=] AMREX_GPU_DEVICE(int i, int j, int) noexcept
419  {
420  const int ksrf = ksurf_arr(i, j, 0); // local surface: first fluid cell of the column
421  const Real zib = zib_arr(i, j, 0); // its height, subtracted from every height below
422  amrex::ignore_unused(zib);
423  const Real t_layer = t10_eff_arr(i, j, 0);
424  const Real moisture_fraction = use_moisture ? q10_eff_arr(i, j, 0) : Real(0);
425  const Real t_layer_v = t_layer * (one + epsv * moisture_fraction);
426 
427  Real zval, Rib;
428  int kpbl = ksrf;
429 
430  {
431  zval = (use_terrain_fitted_coords)
432  ? Compute_Zrel_AtCellCenter(i, j, kpbl, z_nd_arr)
433  : ((kpbl + myhalf) * gdata.CellSize(2) - zib);
434  const Real theta_v = GetThetav(i, j, kpbl, cell_data, moisture_indices);
435  const Real theta_v_klo = amrex::max(GetThetav(i, j, ksrf, cell_data, moisture_indices), Real(1.0));
436  const Real ws2_raw = fourth * ( (uvel(i, j, kpbl) + uvel(i + 1, j, kpbl)) *
437  (uvel(i, j, kpbl) + uvel(i + 1, j, kpbl)) +
438  (vvel(i, j, kpbl) + vvel(i, j + 1, kpbl)) *
439  (vvel(i, j, kpbl) + vvel(i, j + 1, kpbl)) );
440  // Vogelezang & Holtslag (1996): Add shear correction term to denominator instead of ad-hoc floor
441  // to better represent shear associated with surface-layer turbulence at low wind speeds.
442  // Reference: Vogelezang, D.H.P., and A.A.M. Holtslag, 1996: Evaluation and model impacts of
443  // alternative boundary-layer height formulations. Boundary-Layer Meteorology, 81, 245–269.
444  const Real ws2 = (turbChoice.enable_vh96_shear_correction)
445  ? (ws2_raw + turbChoice.vh96_shear_const_b * us_eff_arr(i,j,0) * us_eff_arr(i,j,0))
446  : amrex::max(ws2_raw, Real(1.0));
447  Rib = CONST_GRAV * zval * (theta_v - t_layer_v) / (ws2 * theta_v_klo);
448  }
449 
450  Real zval0 = zval, Rib0 = Rib;
451  bool above_critical = false;
452  while (!above_critical && ((kpbl + 1) <= khi)) {
453  zval0 = zval;
454  Rib0 = Rib;
455  kpbl += 1;
456 
457  zval = (use_terrain_fitted_coords)
458  ? Compute_Zrel_AtCellCenter(i, j, kpbl, z_nd_arr)
459  : ((kpbl + myhalf) * gdata.CellSize(2) - zib);
460  const Real theta_v = GetThetav(i, j, kpbl, cell_data, moisture_indices);
461  const Real theta_v_klo = amrex::max(GetThetav(i, j, ksrf, cell_data, moisture_indices), Real(1.0));
462  const Real ws2_raw = fourth * ( (uvel(i, j, kpbl) + uvel(i + 1, j, kpbl)) *
463  (uvel(i, j, kpbl) + uvel(i + 1, j, kpbl)) +
464  (vvel(i, j, kpbl) + vvel(i, j + 1, kpbl)) *
465  (vvel(i, j, kpbl) + vvel(i, j + 1, kpbl)) );
466  // Vogelezang & Holtslag (1996): Add shear correction term to denominator instead of ad-hoc floor
467  // to better represent shear associated with surface-layer turbulence at low wind speeds.
468  const Real ws2 = (turbChoice.enable_vh96_shear_correction)
469  ? (ws2_raw + turbChoice.vh96_shear_const_b * us_eff_arr(i,j,0) * us_eff_arr(i,j,0))
470  : amrex::max(ws2_raw, Real(1.0));
471  Rib = CONST_GRAV * zval * (theta_v - t_layer_v) / (ws2 * theta_v_klo);
472  above_critical = (Rib >= Ribcr);
473  }
474 
475  const Real pblh_emp = (use_terrain_fitted_coords)
476  ? Compute_Zrel_AtCellCenter(i, j, ksrf, z_nd_arr)
477  : myhalf * gdata.CellSize(2);
478  const Real z_max = (use_terrain_fitted_coords)
479  ? Compute_Zrel_AtCellCenter(i, j, khi, z_nd_arr)
480  : ((khi + myhalf) * gdata.CellSize(2) - zib);
481  const Real pblh_max = Real(0.9) * z_max;
482  const Real pblh_min = amrex::max(pblh_emp, Real(10.0));
483 
484  if (above_critical) {
485  Real pblh_interp;
486  const Real rib_diff = Rib - Rib0;
487  if (Rib0 >= Ribcr) {
488  // The level below was at or above Ribcr too (only the surface cell can be):
489  // WRF brint = 0, the height of that level, not an extrapolation above it
490  pblh_interp = zval0;
491  } else if (std::abs(rib_diff) > Real(1.0e-10)) {
492  pblh_interp = zval0 + (zval - zval0) / rib_diff * (Ribcr - Rib0);
493  } else {
494  // If Rib not changing, use zval0 as PBL height
495  pblh_interp = zval0;
496  }
497  pblh_pred_arr(i, j, 0) = amrex::max(amrex::min(pblh_interp, pblh_max), pblh_min);
498  pbli_arr(i, j, 0) = kpbl;
499  } else {
500  // No crossing up to khi: the scan ended there with zval = z(khi), the top of
501  // the scan, capped by pblh_max, and kpbl = khi, so the K profile and the
502  // countergradient gates below cover the column (WRF module_bl_mrf.F, brint = 1).
503  pblh_pred_arr(i, j, 0) = amrex::max(amrex::min(zval, pblh_max), pblh_min);
504  pbli_arr(i, j, 0) = kpbl;
505  }
506  });
507 
508  const Real const_b = turbChoice.pbl_mrf_const_b;
509  const Real sf = turbChoice.pbl_mrf_sf;
510  constexpr Real prmin = Real(0.5);
511  constexpr Real prmax = Real(4.0);
512  constexpr Real GAMCRT = Real(3.0);
513  constexpr Real GAMCRQ = Real(2.e-3);
514  const bool enable_mrf_countergradient = turbChoice.enable_mrf_countergradient;
515  const bool enable_mrf_unbounded_vpert = turbChoice.enable_mrf_unbounded_vpert;
516 
517  //
518  // PASS 2 (WSTAR / VPERT): Compute wstar, HGAMT, HGAMQ, VPERT using the
519  // predictor PBL height (pblh_pred_arr). VPERT will be fed into the corrector
520  // Rib search in Pass 3 to raise the effective surface temperature.
521  // WRF reference (module_bl_mrf.F lines 857-880):
522  // https://github.com/wrf-model/WRF/blob/master/phys/module_bl_mrf.F#L857-L880
523  //
524  ParallelFor(xybx_work, [=,zero_d=zero] AMREX_GPU_DEVICE(int i, int j, int) noexcept
525  {
526  const int ksrf = ksurf_arr(i, j, 0); // local surface: first fluid cell of the column
527  const Real zib = zib_arr(i, j, 0); // its height, subtracted from every height below
528  amrex::ignore_unused(zib);
529  const Real t_layer = t10_eff_arr(i, j, 0);
530  Real obuk_val = ol_eff_arr(i, j, 0);
531  if (std::abs(obuk_val) < amrex::Real(1.0e-10)) {
532  obuk_val = (obuk_val >= zero_d) ? amrex::Real(1.0e-10) : amrex::Real(-1.0e-10);
533  }
534 
535  // HOL computed from predictor height (WRF uses predictor height for wstar/VPERT)
536  const Real HOL = sf * pblh_pred_arr(i, j, 0) / obuk_val;
537  const Real HOL_bounded = amrex::max(amrex::min(HOL, Real(100.0)), Real(-100.0));
538  // |HOL|, so the base 1 + 16*HOL_abs of the unstable arm below equals its
539  // 1 - 16*HOL there and is at least 1 for every HOL; a guard written inside the
540  // arm gets folded away by the optimiser and the pow hoisted (see sqrt_neg_Ri).
541  const Real HOL_abs = std::abs(HOL_bounded);
542  const Real one_quarter = Real(0.25);
543  const Real phiM = (obuk_val > 0)
544  ? (1 + 5 * HOL_bounded)
545  : std::pow(1 + 16 * HOL_abs, -one_quarter);
546  const Real phiM_safe = amrex::max(phiM, Real(0.01));
547 
548  // wstar = u* / phi_m
549  // Absolute bounds [0.01, 5.0] m/s: prevent division-by-zero (floor) and
550  // free-convection blow-up (ceiling), independent of u* for u*->0 safety.
551  // BOOTSTRAP NOTE: pblh_pred_arr must be finite (not sentinel) before this
552  // kernel runs. On cold start, pblh_pred_arr is computed in Pass 1 above;
553  // on regrid, SurfaceLayer PBLH may be sentinel — callers must re-run
554  // ComputeDiffusivityMRF before consuming wstar or Beljaars correction after regrid.
555  // Deardorff (1972) convective velocity scale — well-behaved as u_star -> 0.
556  // wstar_conv = (g/theta_v0 * zi * wthv0)^(1/3)
557  const Real theta_v0 = amrex::max(GetThetav(i, j, ksrf, cell_data, moisture_indices), Real(1.0));
558  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
559  const Real zi = amrex::max(pblh_pred_arr(i, j, 0), Real(10.0)); // predictor: Pass 3 has not run yet
560  const Real wstar_conv = std::cbrt(CONST_GRAV / theta_v0 * zi * wthv0);
561 
562  Real wstar_shear = us_eff_arr(i, j, 0) / phiM_safe;
563 
564  // Blend: standard convective/shear combination (WRF YSU-style),
565  // weighted by cube sum so either term can dominate smoothly
566  Real wstar = std::cbrt(wstar_shear*wstar_shear*wstar_shear +
567  wstar_conv*wstar_conv*wstar_conv);
568 
569  wstar = amrex::max(wstar, Real(0.01));
570  wstar = amrex::min(wstar, Real(5.0));
571 
572  bool SFCFLG = (obuk_val <= zero_d);
573  const Real HGAMT = (SFCFLG && enable_mrf_countergradient)
574  ? amrex::min(-const_b * us_eff_arr(i, j, 0) * ts_eff_arr(i, j, 0) / wstar, GAMCRT)
575  : zero_d;
576 
577  Real HGAMQ = Real(0);
578  if (SFCFLG && use_moisture && enable_mrf_countergradient) {
579  const Real q_star = qs_eff_arr(i, j, 0);
580  const Real HGAMQ_calc = -const_b * us_eff_arr(i, j, 0) * q_star / wstar;
581  HGAMQ = amrex::max(amrex::min(HGAMQ_calc, GAMCRQ), Real(0));
582 
583  if (lmask_arr) {
584  bool is_land = (lmask_arr(i,j,0) == 1);
585  if (!is_land) HGAMQ = zero_d;
586  }
587 
588  if (moisture_indices.qv >= 0) {
589  Real qv_klo = cell_data(i, j, ksrf, moisture_indices.qv) / cell_data(i, j, ksrf, Rho_comp);
590  Real T_klo = getTgivenRandRTh(cell_data(i, j, ksrf, Rho_comp),
591  cell_data(i, j, ksrf, RhoTheta_comp), qv_klo);
592  Real p_klo = getPgivenRTh(cell_data(i, j, ksrf, RhoTheta_comp), qv_klo) * Real(0.01);
593  Real qsat_klo = zero_d;
594  erf_qsatw(T_klo, p_klo, qsat_klo);
595  Real rh_klo = (qsat_klo > Real(1.0e-10)) ? (qv_klo / qsat_klo) : Real(0);
596  if (rh_klo > Real(0.95)) {
597  Real rh_scaling = amrex::max(zero_d, (Real(1) - rh_klo) / Real(0.05));
598  HGAMQ *= rh_scaling;
599  }
600  }
601  }
602 
603  // Compute VPERT = HGAMT + 0.61*theta*HGAMQ (virtual temperature perturbation)
604  // This will be added to the surface temperature in the corrector Rib search.
605  // WRF Reference: module_bl_mrf.F lines 879-880
606  if (pbli_arr(i, j, 0) <= ksrf + 1 || !enable_mrf_countergradient) {
607  vpert_arr(i, j, 0) = zero_d;
608  } else {
609  const Real VPERT_raw = HGAMT + epsv * t_layer * HGAMQ;
610  const Real VPERT_capped = enable_mrf_unbounded_vpert
611  ? VPERT_raw
612  : amrex::min(VPERT_raw, GAMCRT);
613  vpert_arr(i, j, 0) = amrex::max(VPERT_capped, zero_d);
614  }
615  });
616 
617  //
618  // PASS 3 (CORRECTOR): Recompute PBL height with VPERT-enhanced surface temperature.
619  // theta_s = theta_va + VPERT (Hong & Pan 1996, Eq. 4)
620  // This is the WRF-consistent corrector pass. pblh_corr_arr is used for the
621  // K-profile shape function K = rho*wstar*kappa*z*(1-z/h)^2.
622  // WRF reference (module_bl_mrf.F lines 932-964):
623  // https://github.com/wrf-model/WRF/blob/master/phys/module_bl_mrf.F#L932-L964
624  //
625  ParallelFor(xybx_work, [=,one_d=one]
626  AMREX_GPU_DEVICE(int i, int j, int) noexcept
627  {
628  const int ksrf = ksurf_arr(i, j, 0); // local surface: first fluid cell of the column
629  const Real zib = zib_arr(i, j, 0); // its height, subtracted from every height below
630  amrex::ignore_unused(zib);
631  const Real t_layer = t10_eff_arr(i, j, 0);
632  const Real moisture_fraction = use_moisture ? q10_eff_arr(i, j, 0) : Real(0);
633  // VPERT-enhanced surface virtual temperature (WRF THERMAL variable)
634  const Real t_layer_v = t_layer * (one + epsv * moisture_fraction)
635  + vpert_arr(i, j, 0);
636 
637  Real obuk_val = ol_eff_arr(i, j, 0);
638  if (std::abs(obuk_val) < amrex::Real(1.0e-10)) {
639  obuk_val = (obuk_val >= zero) ? amrex::Real(1.0e-10) : amrex::Real(-1.0e-10);
640  }
641 
642  int kpbl = ksrf;
643  Real zval, Rib;
644  {
645  zval = (use_terrain_fitted_coords)
646  ? Compute_Zrel_AtCellCenter(i, j, kpbl, z_nd_arr)
647  : ((kpbl + myhalf) * gdata.CellSize(2) - zib);
648  const Real theta_v = GetThetav(i, j, kpbl, cell_data, moisture_indices);
649  const Real theta_v_klo = amrex::max(GetThetav(i, j, ksrf, cell_data, moisture_indices), one_d);
650  const Real ws2_raw = fourth * ( (uvel(i, j, kpbl) + uvel(i + 1, j, kpbl)) *
651  (uvel(i, j, kpbl) + uvel(i + 1, j, kpbl)) +
652  (vvel(i, j, kpbl) + vvel(i, j + 1, kpbl)) *
653  (vvel(i, j, kpbl) + vvel(i, j + 1, kpbl)) );
654  // Vogelezang & Holtslag (1996): Add shear correction term to denominator instead of ad-hoc floor
655  const Real ws2 = (turbChoice.enable_vh96_shear_correction)
656  ? (ws2_raw + turbChoice.vh96_shear_const_b * us_eff_arr(i,j,0) * us_eff_arr(i,j,0))
657  : amrex::max(ws2_raw, one_d);
658  Rib = CONST_GRAV * zval * (theta_v - t_layer_v) / (ws2 * theta_v_klo);
659  }
660  Real zval0 = zval, Rib0 = Rib;
661 
662  bool above_critical = false;
663  while (!above_critical && ((kpbl + 1) <= khi)) {
664  zval0 = zval;
665  Rib0 = Rib;
666  kpbl += 1;
667 
668  zval = (use_terrain_fitted_coords)
669  ? Compute_Zrel_AtCellCenter(i, j, kpbl, z_nd_arr)
670  : ((kpbl + myhalf) * gdata.CellSize(2) - zib);
671  const Real theta_v = GetThetav(i, j, kpbl, cell_data, moisture_indices);
672  const Real theta_v_klo = amrex::max(GetThetav(i, j, ksrf, cell_data, moisture_indices), one_d);
673  const Real ws2_raw = fourth * ( (uvel(i, j, kpbl) + uvel(i + 1, j, kpbl)) *
674  (uvel(i, j, kpbl) + uvel(i + 1, j, kpbl)) +
675  (vvel(i, j, kpbl) + vvel(i, j + 1, kpbl)) *
676  (vvel(i, j, kpbl) + vvel(i, j + 1, kpbl)) );
677  // Vogelezang & Holtslag (1996): Add shear correction term to denominator instead of ad-hoc floor
678  const Real ws2 = (turbChoice.enable_vh96_shear_correction)
679  ? (ws2_raw + turbChoice.vh96_shear_const_b * us_eff_arr(i,j,0) * us_eff_arr(i,j,0))
680  : amrex::max(ws2_raw, one_d);
681  Rib = CONST_GRAV * zval * (theta_v - t_layer_v) / (ws2 * theta_v_klo);
682  above_critical = (Rib >= Ribcr);
683  }
684 
685  const Real pblh_emp = (use_terrain_fitted_coords)
686  ? Compute_Zrel_AtCellCenter(i, j, ksrf, z_nd_arr)
687  : myhalf * gdata.CellSize(2);
688  const Real z_max = (use_terrain_fitted_coords)
689  ? Compute_Zrel_AtCellCenter(i, j, khi, z_nd_arr)
690  : ((khi + myhalf) * gdata.CellSize(2) - zib);
691  const Real pblh_max = Real(0.9) * z_max;
692  const Real pblh_min = amrex::max(pblh_emp, Real(10.0));
693 
694  if (above_critical) {
695  Real pblh_interp;
696  const Real rib_diff = Rib - Rib0;
697  if (Rib0 >= Ribcr) {
698  // The level below was at or above Ribcr too (only the surface cell can be):
699  // WRF brint = 0, the height of that level, not an extrapolation above it
700  pblh_interp = zval0;
701  } else if (std::abs(rib_diff) > Real(1.0e-10)) {
702  pblh_interp = zval0 + (zval - zval0) / rib_diff * (Ribcr - Rib0);
703  } else {
704  // If Rib not changing, use zval0 as PBL height
705  pblh_interp = zval0;
706  }
707  pblh_corr_arr(i, j, 0) = amrex::max(amrex::min(pblh_interp, pblh_max), pblh_min);
708  pbli_arr(i, j, 0) = kpbl;
709  } else {
710  // No crossing up to khi: the scan ended there with zval = z(khi), the top of
711  // the scan, capped by pblh_max, and kpbl = khi, so the K profile and the
712  // countergradient gates below cover the column (WRF module_bl_mrf.F, brint = 1).
713  pblh_corr_arr(i, j, 0) = amrex::max(amrex::min(zval, pblh_max), pblh_min);
714  pbli_arr(i, j, 0) = kpbl;
715  }
716  pblh_floor_arr(i, j, 0) = pblh_min;
717  });
718 
719 
720  // Apply PBLH spatial smoothing if enabled (Seibert et al. 2000 methodology)
721  // Seibert et al. (2000): Review and intercomparison of operational methods
722  // for the determination of the mixing height. Atmospheric Environment, 34, 1001-1027.
723  // Spatial smoothing removes unphysical grid-to-grid noise from discrete Rib-crossing detection
724  if (turbChoice.enable_pblh_smoothing) {
725  // Smooth the tile's own columns, reading the halo the passes above
726  // filled. The result is the same however the domain is split.
727  // The stencil mixes neighbouring columns, so it runs on the
728  // absolute height (the stored convention): the column's surface
729  // height zib goes on over the work box first and comes off
730  // after, and the result is kept at or above the column's floor.
731  // zib is zero without erf.pbl_ib_aware.
732  ParallelFor(xybx_work, [=] AMREX_GPU_DEVICE(int i, int j, int) noexcept {
733  pblh_corr_arr(i, j, 0) += zib_arr(i, j, 0);
734  });
735  ApplyPBLHSmoothing(pbl_height_corrector, xybx_tile,
736  turbChoice.pblh_smoothing_weight,
737  turbChoice.pblh_smoothing_passes,
738  geom.Domain(), geom.periodicity());
739  ParallelFor(xybx_work, [=] AMREX_GPU_DEVICE(int i, int j, int) noexcept {
740  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));
741  });
742  }
743 
744  // Copy corrected PBL height into pblh_mf for SurfaceLayer storage.
745  // pbl_height_corrector only covers this tile (grown by one), so loop over
746  // the tile, not the valid box: with tiling in x/y the valid box reaches
747  // past it. Under TileNoZ the tile spans the full column, and pblh_mf has
748  // no ghost cells, so the tiles together still fill every cell.
749  {
750  auto pblh_out = pblh_mf.array(mfi);
751  const Box& tbx = mfi.tilebox();
752  // The scheme measures its height from the top of an immersed column
753  // (zib); the stored field is the absolute height above the domain
754  // bottom, the convention of YSUNew and of every consumer of
755  // SurfaceLayer::get_pblh(). zib is zero without erf.pbl_ib_aware.
756  ParallelFor(tbx, [=] AMREX_GPU_DEVICE(int i, int j, int k) noexcept {
757  pblh_out(i, j, k) = pblh_corr_arr(i, j, 0) + zib_arr(i, j, 0);
758  });
759  }
760  //
761  // PASS 4 (WSTAR RECOMPUTE): Recompute wstar, HGAMT, HGAMQ using the corrected
762  // PBL height (pblh_corr_arr) to ensure internal consistency between the K-profile
763  // amplitude and the countergradient fluxes. HOL = sf*h/L uses pblh_corr_arr.
764  // WRF reference (module_bl_mrf.F lines 857-880):
765  // https://github.com/wrf-model/WRF/blob/master/phys/module_bl_mrf.F#L857-L880
766  //
767  ParallelFor(xybx, [=] AMREX_GPU_DEVICE(int i, int j, int) noexcept
768  {
769  const int ksrf = ksurf_arr(i, j, 0); // local surface: first fluid cell of the column
770  const Real zib = zib_arr(i, j, 0); // its height, subtracted from every height below
771  amrex::ignore_unused(zib);
772  //const Real t_layer = t10_eff_arr(i, j, 0);
773  Real obuk_val = ol_eff_arr(i, j, 0);
774  if (std::abs(obuk_val) < amrex::Real(1.0e-10)) {
775  obuk_val = (obuk_val >= Real(0)) ? amrex::Real(1.0e-10) : amrex::Real(-1.0e-10);
776  }
777 
778  // HOL now uses corrected PBLH for full internal consistency
779  const Real HOL = sf * pblh_corr_arr(i, j, 0) / obuk_val;
780  const Real HOL_bounded = amrex::max(amrex::min(HOL, Real(100.0)), Real(-100.0));
781  // |HOL|, so the base 1 + 16*HOL_abs of the unstable arm below equals its
782  // 1 - 16*HOL there and is at least 1 for every HOL; a guard written inside the
783  // arm gets folded away by the optimiser and the pow hoisted (see sqrt_neg_Ri).
784  const Real HOL_abs = std::abs(HOL_bounded);
785  const Real one_quarter = Real(0.25);
786  // Enable QNSE stable functions if requested, otherwise use default linear form
787  const Real enable_qnse_d = (turbChoice.enable_qnse_stable_functions) ? Real(1.0) : Real(0.0);
788  const Real qnse_am_d = turbChoice.qnse_am;
789  const Real qnse_bm_d = turbChoice.qnse_bm;
790  const Real phiM = (obuk_val > 0)
791  ? (enable_qnse_d > Real(0.5)
792  ? (1 + qnse_am_d * HOL_bounded) / (1 + qnse_bm_d * HOL_bounded)
793  : (1 + 5 * HOL_bounded))
794  : std::pow(1 + 16 * HOL_abs, -one_quarter);
795  const Real phiM_safe = amrex::max(phiM, Real(0.01));
796 
797  // Absolute bounds [0.01, 5.0] m/s
798  // Deardorff (1972) convective velocity scale — well-behaved as u_star -> 0.
799  // wstar_conv = (g/theta_v0 * zi * wthv0)^(1/3)
800  const Real theta_v0 = amrex::max(GetThetav(i, j, ksrf, cell_data, moisture_indices), Real(1.0));
801  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
802  // Corrected height: this pass exists to make the K-profile amplitude and the
803  // countergradient fluxes consistent with the height Pass 3 produced, and HOL
804  // above already uses it. (Pass 2 has only the predictor height to work with.)
805  const Real zi = amrex::max(pblh_corr_arr(i, j, 0), Real(10.0));
806  const Real wstar_conv = std::cbrt(CONST_GRAV / theta_v0 * zi * wthv0);
807 
808  Real wstar_shear = us_eff_arr(i, j, 0) / phiM_safe;
809 
810  // Blend: standard convective/shear combination (WRF YSU-style),
811  // weighted by cube sum so either term can dominate smoothly
812  Real wstar = std::cbrt(wstar_shear*wstar_shear*wstar_shear +
813  wstar_conv*wstar_conv*wstar_conv);
814  //Real wstar = us_eff_arr(i, j, 0) / phiM_safe;
815  wstar = amrex::max(wstar, Real(0.01));
816  wstar = amrex::min(wstar, Real(5.0));
817  wstar_arr(i, j, 0) = wstar;
818  bool SFCFLG = (obuk_val <= Real(0));
819  // WRF caps the heat countergradient at GAMCRT and then floors it at zero:
820  // HGAMT(I)=MIN(GAMFAC*HFX(I)/CPM(I),GAMCRT) (module_bl_mrf.F L874)
821  // HGAMT(I)=MAX(HGAMT(I),0.0) (module_bl_mrf.F L880)
822  // The floor is not redundant here. The unstable branch is selected on
823  // sign(L), but the numerator is the sensible flux -u_* theta_* alone: over
824  // water with a small downward sensible flux and a large latent flux, L is
825  // negative while -u_* theta_* is not. Without the floor the term would then
826  // add to the down-gradient flux rather than oppose it. VPERT in Pass 2 is
827  // deliberately built from the unfloored value, as in WRF (L877 precedes L880).
828  const Real HGAMT = (SFCFLG && enable_mrf_countergradient)
829  ? amrex::max(amrex::min(-const_b * us_eff_arr(i, j, 0)
830  * ts_eff_arr(i, j, 0) / wstar, GAMCRT), Real(0))
831  : Real(0);
832 
833  Real HGAMQ = Real(0);
834  if (SFCFLG && use_moisture && enable_mrf_countergradient) {
835  const Real q_star = qs_eff_arr(i, j, 0);
836  const Real HGAMQ_calc = -const_b * us_eff_arr(i, j, 0) * q_star / wstar;
837  HGAMQ = amrex::max(amrex::min(HGAMQ_calc, GAMCRQ), Real(0));
838 
839  if (lmask_arr) {
840  bool is_land = (lmask_arr(i,j,0) == 1);
841  if (!is_land) HGAMQ = Real(0);
842  }
843 
844  if (moisture_indices.qv >= 0) {
845  Real qv_klo = cell_data(i, j, ksrf, moisture_indices.qv) / cell_data(i, j, ksrf, Rho_comp);
846  Real T_klo = getTgivenRandRTh(cell_data(i, j, ksrf, Rho_comp),
847  cell_data(i, j, ksrf, RhoTheta_comp), qv_klo);
848  Real p_klo = getPgivenRTh(cell_data(i, j, ksrf, RhoTheta_comp), qv_klo) * Real(0.01);
849  Real qsat_klo = Real(0);
850  erf_qsatw(T_klo, p_klo, qsat_klo);
851  Real rh_klo = (qsat_klo > Real(1.0e-10)) ? (qv_klo / qsat_klo) : Real(0);
852  if (rh_klo > Real(0.95)) {
853  Real rh_scaling = amrex::max(Real(0), (Real(1) - rh_klo) / Real(0.05));
854  HGAMQ *= rh_scaling;
855  }
856  }
857  }
858 
859  if (pbli_arr(i, j, 0) <= ksrf + 1) {
860  hgamt_arr(i, j, 0) = Real(0);
861  hgamq_arr(i, j, 0) = Real(0);
862  } else {
863  const Real pblh = pblh_corr_arr(i, j, 0);
864  if (pblh > Real(1.0e-10)) {
865  hgamt_arr(i, j, 0) = (enable_mrf_countergradient) ? HGAMT / pblh : Real(0);
866  hgamq_arr(i, j, 0) = (enable_mrf_countergradient && use_moisture) ? HGAMQ / pblh : Real(0);
867  } else {
868  hgamt_arr(i, j, 0) = Real(0);
869  hgamq_arr(i, j, 0) = Real(0);
870  }
871  }
872  });
873  //
874  // PASS 5 (ZERO-RI): Diagnostic PBL height with Ribcr=0, VPERT-enhanced surface temp.
875  // Used optionally (pbl_mrf_use_zero_ri_extent) to extend the nonlocal mixing region.
876  // WRF reference (module_bl_mrf.F lines 932-964):
877  // https://github.com/wrf-model/WRF/blob/master/phys/module_bl_mrf.F#L932-L964
878  //
879  constexpr Real Ribcr_zero = Real(0);
880  ParallelFor(xybx, [=,one_d=one]
881  AMREX_GPU_DEVICE(int i, int j, int) noexcept
882  {
883  const int ksrf = ksurf_arr(i, j, 0); // local surface: first fluid cell of the column
884  const Real zib = zib_arr(i, j, 0); // its height, subtracted from every height below
885  amrex::ignore_unused(zib);
886  const Real t_layer = t10_eff_arr(i, j, 0);
887  const Real moisture_fraction = use_moisture ? q10_eff_arr(i, j, 0) : zero;
888  const Real t_layer_v = t_layer * (one + epsv * moisture_fraction);
889  const Real t_layer_v_enhanced = t_layer_v + vpert_arr(i, j, 0);
890 
891  int kpbl_zero = ksrf;
892  Real zval_zero, Rib_zero;
893  {
894  zval_zero = (use_terrain_fitted_coords)
895  ? Compute_Zrel_AtCellCenter(i, j, kpbl_zero, z_nd_arr)
896  : ((kpbl_zero + myhalf) * gdata.CellSize(2) - zib);
897  const Real theta_v = GetThetav(i, j, kpbl_zero, cell_data, moisture_indices);
898  const Real theta_v_klo = amrex::max(GetThetav(i, j, ksrf, cell_data, moisture_indices), one_d);
899  const Real ws2_raw = fourth * ( (uvel(i, j, kpbl_zero) + uvel(i + 1, j, kpbl_zero)) *
900  (uvel(i, j, kpbl_zero) + uvel(i + 1, j, kpbl_zero)) +
901  (vvel(i, j, kpbl_zero) + vvel(i, j + 1, kpbl_zero)) *
902  (vvel(i, j, kpbl_zero) + vvel(i, j + 1, kpbl_zero)) );
903  // Vogelezang & Holtslag (1996): Add shear correction term to denominator instead of ad-hoc floor
904  const Real ws2 = (turbChoice.enable_vh96_shear_correction)
905  ? (ws2_raw + turbChoice.vh96_shear_const_b * us_eff_arr(i,j,0) * us_eff_arr(i,j,0))
906  : amrex::max(ws2_raw, one_d);
907  Rib_zero = CONST_GRAV * zval_zero * (theta_v - t_layer_v_enhanced) / (ws2 * theta_v_klo);
908  }
909 
910  bool above_critical_zero = false;
911  while (!above_critical_zero && ((kpbl_zero + 1) <= khi)) {
912  kpbl_zero += 1;
913  zval_zero = (use_terrain_fitted_coords)
914  ? Compute_Zrel_AtCellCenter(i, j, kpbl_zero, z_nd_arr)
915  : ((kpbl_zero + myhalf) * gdata.CellSize(2) - zib);
916  const Real theta_v = GetThetav(i, j, kpbl_zero, cell_data, moisture_indices);
917  const Real theta_v_klo = amrex::max(GetThetav(i, j, ksrf, cell_data, moisture_indices), one_d);
918  const Real ws2_raw = fourth * ( (uvel(i, j, kpbl_zero) + uvel(i + 1, j, kpbl_zero)) *
919  (uvel(i, j, kpbl_zero) + uvel(i + 1, j, kpbl_zero)) +
920  (vvel(i, j, kpbl_zero) + vvel(i, j + 1, kpbl_zero)) *
921  (vvel(i, j, kpbl_zero) + vvel(i, j + 1, kpbl_zero)) );
922  // Vogelezang & Holtslag (1996): Add shear correction term to denominator instead of ad-hoc floor
923  const Real ws2 = (turbChoice.enable_vh96_shear_correction)
924  ? (ws2_raw + turbChoice.vh96_shear_const_b * us_eff_arr(i,j,0) * us_eff_arr(i,j,0))
925  : amrex::max(ws2_raw, one_d);
926  Rib_zero = CONST_GRAV * zval_zero * (theta_v - t_layer_v_enhanced) / (ws2 * theta_v_klo);
927  above_critical_zero = (Rib_zero >= Ribcr_zero);
928  }
929 
930  if (above_critical_zero) {
931  pbli_zero_arr(i, j, 0) = kpbl_zero;
932  } else {
933  // No Ri = 0 crossing up to khi: the extent is the top of the scan, as for
934  // pbli_arr above (WRF module_bl_mrf.F, brint = 1)
935  pbli_zero_arr(i, j, 0) = kpbl_zero;
936  }
937  });
938 
939  // -- Compute diffusion coefficients --
940 
941  const Array4<Real>& K_turb = eddyViscosity.array(mfi);
942 
943  bool c_ext_dir_on_zlo = ((bc_ptr[BCVars::cons_bc].lo(2) == ERFBCType::ext_dir));
944  bool c_ext_dir_on_zhi = ((bc_ptr[BCVars::cons_bc].hi(2) == ERFBCType::ext_dir));
945  bool u_ext_dir_on_zlo = ((bc_ptr[BCVars::xvel_bc].lo(2) == ERFBCType::ext_dir));
946  bool u_ext_dir_on_zhi = ((bc_ptr[BCVars::xvel_bc].hi(2) == ERFBCType::ext_dir));
947  bool v_ext_dir_on_zlo = ((bc_ptr[BCVars::yvel_bc].lo(2) == ERFBCType::ext_dir));
948  bool v_ext_dir_on_zhi = ((bc_ptr[BCVars::yvel_bc].hi(2) == ERFBCType::ext_dir));
949 
950  const auto& dxInv = geom.InvCellSizeArray();
951  const Real dz_inv = geom.InvCellSize(2);
952  const int izmin = geom.Domain().smallEnd(2);
953  const int izmax = geom.Domain().bigEnd(2);
954 
955  // Blending parameters captured as scalars for GPU lambda.
956  const amrex::Real l_blend_length = turbChoice.pbl_blend_length;
957  const amrex::Real l_blend_cs = turbChoice.pbl_blend_cs;
958  const amrex::Real l_blend_cmax = turbChoice.pbl_blend_c_max;
959  //const bool l_use_smag_ceil= turbChoice.pbl_blend_use_smag;
960  // dx: use horizontal spacing at this level (assume dx = dy for regular grids).
961  const amrex::Real l_dx = geom.CellSize(0);
962 
963  ParallelFor(gbx, [=] AMREX_GPU_DEVICE(int i, int j, int k) noexcept
964  {
965  const int ksrf = ksurf_arr(i, j, 0); // local surface: first fluid cell of the column
966  const Real zib = zib_arr(i, j, 0); // its height, subtracted from every height below
967  amrex::ignore_unused(zib);
968  Real obuk_val = ol_eff_arr(i, j, 0);
969  if (std::abs(obuk_val) < amrex::Real(1.0e-10)) {
970  obuk_val = (obuk_val >= Real(0)) ? amrex::Real(1.0e-10) : amrex::Real(-1.0e-10);
971  }
972 
973  const Real zval = (use_terrain_fitted_coords)
974  ? Compute_Zrel_AtCellCenter(i, j, k, z_nd_arr)
975  : ((k + myhalf) * gdata.CellSize(2) - zib);
976  const Real rho = cell_data(i, j, k, Rho_comp);
977  // Guard: skip lateral ghost cells that may have uninitialized density.
978  // Prevents division-by-zero inside GetThetav at lateral ghost cells of gbx.
979  // REGRID NOTE: On regrid, SurfaceLayer is reallocated and PBLH returns to sentinel.
980  // The driver must call ComputeDiffusivityMRF (or an equivalent bootstrap pass)
981  // before consuming PBLH in update_fluxes() or Beljaars correction after any regrid.
982  if (rho <= Real(0) || k < ksrf) { // no diffusivity inside an immersed solid
983  K_turb(i, j, k, EddyDiff::Mom_v) = Real(0);
984  K_turb(i, j, k, EddyDiff::Theta_v) = Real(0);
985  K_turb(i, j, k, EddyDiff::Q_v) = Real(0);
986  K_turb(i, j, k, EddyDiff::HGAMT_v) = Real(0);
987  K_turb(i, j, k, EddyDiff::HGAMQ_v) = Real(0);
988  K_turb(i, j, k, EddyDiff::Turb_lengthscale) = Real(0);
989  return;
990  }
991  const Real met_h_zeta = (use_terrain_fitted_coords)
992  ? Compute_h_zeta_AtCellCenter(i, j, k, dxInv, z_nd_arr) : Real(1);
993  const Real dz_terrain = met_h_zeta / dz_inv;
994 
995  constexpr Real qc_threshold = Real(1.0e-4); // Cloud water/ice threshold (kg/kg)
996  Real qc_mix = Real(0);
997  Real qi_mix = Real(0);
998  if (use_moisture) {
999  if (moisture_indices.qc >= 0) {
1000  qc_mix = cell_data(i, j, k, moisture_indices.qc) / rho;
1001  }
1002  if (moisture_indices.qi >= 0) {
1003  qi_mix = cell_data(i, j, k, moisture_indices.qi) / rho;
1004  }
1005  }
1006  const Real total_qcloud = qc_mix + qi_mix;
1007  const bool has_cloud = turbChoice.enable_mrf_cloud_adjustment && (total_qcloud > qc_threshold);
1008 
1009  const int pbli_extent = turbChoice.pbl_mrf_use_zero_ri_extent ? pbli_zero_arr(i, j, 0) : pbli_arr(i, j, 0);
1010 
1011  if (k < pbli_extent) {
1012  const Real HOL = sf * pblh_corr_arr(i, j, 0) / obuk_val;
1013  const Real HOL_bounded = amrex::max(amrex::min(HOL, Real(100.0)), Real(-100.0));
1014  // |HOL|, so the base 1 + 16*HOL_abs of the unstable arm below equals its
1015  // 1 - 16*HOL there and is at least 1 for every HOL; a guard written inside the
1016  // arm gets folded away by the optimiser and the pow hoisted (see sqrt_neg_Ri).
1017  const Real HOL_abs = std::abs(HOL_bounded);
1018 
1019  const Real one_quarter = Real(0.25);
1020  // Enable QNSE stable functions if requested, otherwise use default linear form.
1021  const Real enable_qnse_d = (turbChoice.enable_qnse_stable_functions) ? Real(1.0) : Real(0.0);
1022  const Real qnse_am_d = turbChoice.qnse_am;
1023  const Real qnse_bm_d = turbChoice.qnse_bm;
1024  const Real qnse_ah_d = turbChoice.qnse_ah;
1025  const Real qnse_bh_d = turbChoice.qnse_bh;
1026  const Real phiM = (obuk_val > 0)
1027  ? (enable_qnse_d > Real(0.5)
1028  ? (1 + qnse_am_d * HOL_bounded) / (1 + qnse_bm_d * HOL_bounded)
1029  : (1 + 5 * HOL_bounded))
1030  : std::pow(1 + 16 * HOL_abs, -one_quarter);
1031  const Real phit = (obuk_val > 0)
1032  ? (enable_qnse_d > Real(0.5)
1033  ? (1 + qnse_ah_d * HOL_bounded) / (1 + qnse_bh_d * HOL_bounded)
1034  : (1 + 5 * HOL_bounded))
1035  : std::pow(1 + 16 * HOL_abs, -Real(0.5));
1036 
1037  Real phit_cloud = phit;
1038  Real phiM_cloud = phiM;
1039  if (has_cloud && obuk_val > Real(0)) {
1040  Real reduction_factor = Real(1) - Real(0.15) * amrex::min(total_qcloud / qc_threshold, Real(1));
1041  // Apply cloud reduction on top of QNSE if enabled
1042  if (enable_qnse_d > Real(0.5)) {
1043  phiM_cloud = ((1 + qnse_am_d * HOL_bounded * reduction_factor) / (1 + qnse_bm_d * HOL_bounded));
1044  phit_cloud = ((1 + qnse_ah_d * HOL_bounded * reduction_factor) / (1 + qnse_bh_d * HOL_bounded));
1045  } else {
1046  phiM_cloud = Real(1) + Real(5.0) * reduction_factor * sf * pblh_corr_arr(i, j, 0) / obuk_val;
1047  phit_cloud = Real(1) + Real(5.0) * reduction_factor * sf * pblh_corr_arr(i, j, 0) / obuk_val;
1048  }
1049  } else if (has_cloud && obuk_val <= Real(0)) {
1050  Real cloud_boost = Real(1.0) + Real(0.05) * amrex::min(total_qcloud / qc_threshold, Real(1));
1051  phiM_cloud = std::pow(Real(1) + Real(16.0) * HOL_abs / cloud_boost, -one_quarter);
1052  phit_cloud = std::pow(Real(1) + Real(16.0) * HOL_abs / cloud_boost, -Real(0.5));
1053  }
1054 
1055  const Real phiM_eff = phiM_cloud;
1056  const Real phit_eff = phit_cloud;
1057 
1058  Real Prt_base = phit_eff / phiM_eff;
1059  const Real Prt = amrex::min(amrex::max(Prt_base + const_b * KAPPA * sf, prmin), prmax);
1060 
1061  // wstar is WRF's WSCALE: u_*/phi_m in a stable or neutral column (the
1062  // convective part is floored at zero by the surface buoyancy flux), and
1063  // the cube-sum blend with w_* when the surface flux is upward.
1064  const Real wstar = wstar_arr(i, j, 0);
1065 
1066  // K-profile: K = rho * wstar * kappa * z * (1 - (z - zl1)/(h - zl1))^2
1067  //
1068  // The first-level offset ZL1 belongs to ZFAC alone: the linear factor
1069  // is the full height above the surface, ZQ(I,K). Subtracting zl1 from
1070  // both drives K_m to exactly zero in the first fluid cell.
1071  // ERF_ComputeDiffusivityYSUNew.cpp:1699-1720 splits them the same way.
1072  //
1073  // zval already measures height from the top of an immersed column (zib
1074  // was subtracted where it was formed), so it is that height directly;
1075  // zl1 is zval evaluated at the column's first fluid cell, so zval - zl1
1076  // is exactly zero there in both the terrain and the non-terrain path.
1077  const Real zl1 = (use_terrain_fitted_coords)
1078  ? Compute_Zrel_AtCellCenter(i, j, ksrf, z_nd_arr)
1079  : ((ksrf + myhalf) * gdata.CellSize(2) - zib);
1080  const Real pblh = pblh_corr_arr(i, j, 0);
1081  // The Rib passes floor pblh at max(zl1, 10), so pblh - zl1 is exactly
1082  // zero for any column whose height lands on that floor with a first
1083  // layer thicker than 20 m -- and zval - zl1 is zero too at k == ksrf,
1084  // making the quotient 0/0. Guard it, as YSUNew does (line 1701).
1085  const Real pblh_rel = amrex::max(pblh - zl1, Real(1.0e-4));
1086  const Real zfac = amrex::max(Real(1) - (zval - zl1) / pblh_rel, Real(1.0e-8));
1087 
1088  K_turb(i, j, k, EddyDiff::Mom_v) = rho * wstar * KAPPA * zval * zfac * zfac;
1089  K_turb(i, j, k, EddyDiff::Theta_v) = K_turb(i, j, k, EddyDiff::Mom_v) / Prt;
1090  K_turb(i, j, k, EddyDiff::Q_v) = K_turb(i, j, k, EddyDiff::Theta_v);
1091  } else if (k >= pbli_extent) {
1092  // Free atmosphere above the PBL top. This is the only branch left that
1093  // reads resolved vertical gradients, but they are not evaluated anywhere
1094  // near the surface.
1095  const Real lambda = Real(150.0);
1096  const Real lscale = (KAPPA * zval * lambda) / (KAPPA * zval + lambda);
1097  Real dthetadz, dudz, dvdz;
1098  ComputeVerticalDerivativesPBL(i, j, k, uvel, vvel, cell_data, izmin, izmax, pbl_derivative_dz_inv(i,j,k),
1099  c_ext_dir_on_zlo, c_ext_dir_on_zhi, u_ext_dir_on_zlo,
1100  u_ext_dir_on_zhi, v_ext_dir_on_zlo, v_ext_dir_on_zhi, dthetadz,
1101  dudz, dvdz, moisture_indices, pbl_strat);
1102 
1103  const Real dudz_safe = (k < izmax) ? dudz : Real(0);
1104  const Real dvdz_safe = (k < izmax) ? dvdz : Real(0);
1105  const Real wind_shear = dudz_safe * dudz_safe + dvdz_safe * dvdz_safe;
1106  const Real wind_shear_safe = std::max(wind_shear, Real(1.0e-8));
1107  const Real theta_v = amrex::max(GetThetav(i, j, k, cell_data, moisture_indices), Real(1.0));
1108  const Real dtheta_v_dz = dthetadz;
1109 
1110  Real grad_Ri = CONST_GRAV / theta_v * dtheta_v_dz / wind_shear_safe;
1111  grad_Ri = std::max(std::min(grad_Ri, Real(100.0)), -Real(100.0));
1112 
1113  const Real grad_Ri_safe = amrex::max(grad_Ri, -Real(100.0));
1114  // sqrt(|Ri|), which equals the sqrt(-Ri) of the unstable arm below and is valid
1115  // for every Ri. Written as max(-Ri, 0) inside the arm, the optimiser used the
1116  // arm's condition to drop the max and then hoisted the bare sqrt above the
1117  // selection, where Ri can be positive; written as min(Ri, 0) it folded the
1118  // sqrt into the arms of the min. Either way an invalid operation whose
1119  // result is discarded but whose flag kills the run under
1120  // amrex.fpe_trap_invalid. There is nothing to fold in the absolute value.
1121  const Real sqrt_neg_Ri = std::sqrt(std::abs(grad_Ri_safe));
1122  Real Pr = Real(1) + Real(2.1) * grad_Ri;
1123  const Real fm = (grad_Ri_safe > 0)
1124  ? Real(1) / ((Real(1) + Real(5.0) * grad_Ri_safe) * (Real(1) + Real(5.0) * grad_Ri_safe))
1125  : 1 - 8 * grad_Ri_safe / (1 + Real(1.746) * sqrt_neg_Ri);
1126  const Real ft = (grad_Ri_safe > 0)
1127  ? Real(1) / ((Real(1) + Real(5.0) * grad_Ri_safe) * (Real(1) + Real(5.0) * grad_Ri_safe))
1128  : 1 - 8 * grad_Ri_safe / (1 + Real(1.286) * sqrt_neg_Ri);
1129  const Real rl2wsp = rho * lscale * lscale * std::sqrt(wind_shear);
1130 
1131  Pr = std::max(amrex::Real(0.25), std::min(Pr, Real(4.0)));
1132 
1133  // In the stable regime, fm and ft are the same function, so the Prandtl
1134  // number has to be applied explicitly or momentum and heat mix
1135  // identically.
1136  //
1137  // In the unstable regime, fm and ft already differ (1.746 vs 1.286) and
1138  // WRF applies no Prandtl factor there.
1139  const Real Pr_mom = (grad_Ri_safe > 0) ? Pr : Real(1);
1140 
1141  K_turb(i, j, k, EddyDiff::Mom_v) = rl2wsp * fm * Pr_mom;
1142  K_turb(i, j, k, EddyDiff::Theta_v) = rl2wsp * ft;
1143  K_turb(i, j, k, EddyDiff::Q_v) = K_turb(i, j, k, EddyDiff::Theta_v);
1144  }
1145 
1146  // Scale-aware blending for grey-zone resolution (Boutle et al. 2014).
1147  // ERF_PBLScaleAwareBlending.H. Gated by pbl_blend_length > 0.
1148  // Applied to Theta_v and Q_v. Mom_v is not modified.
1149  //
1150  // K_turb holds rho*K [kg/m/s], but the ceiling applied inside
1151  // pbl_kh_blend_and_cap is a bare diffusivity [m^2/s]. Hand the helper the
1152  // kinematic value and restore the density weighting on the way out; passing
1153  // rho*K straight through would make the effective cap scale like 1/rho and
1154  // hence increase with height (issue #3580).
1155  if (l_blend_length > 0.0) {
1156  const Real rho_inv = Real(1.0) / rho;
1157 
1158  // For MRF, use power-law ceiling (SmnSmn not available)
1159  K_turb(i, j, k, EddyDiff::Theta_v) = rho * pbl_kh_blend_and_cap(
1160  K_turb(i, j, k, EddyDiff::Theta_v) * rho_inv,
1161  l_dx, l_blend_length, l_blend_cs, l_blend_cmax,
1162  amrex::Real(-1.0), false);
1163 
1164  K_turb(i, j, k, EddyDiff::Q_v) = rho * pbl_kh_blend_and_cap(
1165  K_turb(i, j, k, EddyDiff::Q_v) * rho_inv,
1166  l_dx, l_blend_length, l_blend_cs, l_blend_cmax,
1167  amrex::Real(-1.0), false);
1168  }
1169 
1170  // Limit diffusion coefficients to physical bounds
1171  // Hong & Pan (1996): Kmin=0.1, Kmax=300 m^2/s (module_bl_mrf.F lines 1014-1025)
1172  // Hong et al. (2006): Kmin=ckz*dz*rho, Kmax=1000 m^2/s (high-res option)
1173  Real rhoKmin, rhoKmax;
1174  if (turbChoice.pbl_mrf_highres_bounds) {
1175  constexpr Real ckz = Real(0.001);
1176  constexpr Real Kmax = Real(1000.0);
1177  rhoKmin = ckz * dz_terrain * rho;
1178  rhoKmax = rho * Kmax;
1179  } else {
1180  constexpr Real Kmin = Real(0.1);
1181  constexpr Real Kmax = Real(300.0);
1182  rhoKmin = rho * Kmin;
1183  rhoKmax = rho * Kmax;
1184  }
1185 
1186  K_turb(i, j, k, EddyDiff::Mom_v) = std::max(
1187  std::min(K_turb(i, j, k, EddyDiff::Mom_v), rhoKmax), rhoKmin);
1188  K_turb(i, j, k, EddyDiff::Theta_v) = std::max(
1189  std::min(K_turb(i, j, k, EddyDiff::Theta_v), rhoKmax), rhoKmin);
1190  K_turb(i, j, k, EddyDiff::Q_v) = std::max(
1191  std::min(K_turb(i, j, k, EddyDiff::Q_v), rhoKmax), rhoKmin);
1192  K_turb(i, j, k, EddyDiff::Turb_lengthscale) = pblh_corr_arr(i, j, 0) + zib; // absolute height, as stored
1193 
1194  // IMPORTANT — units convention for HGAMT_v / HGAMQ_v:
1195  // These are stored as (countergradient term / pblh), i.e. already
1196  // divided by the PBL height, NOT as a raw physical gradient needing
1197  // an extra 1/dz scaling downstream.
1198  //
1199  // The implicit solver (ERF_ImplicitDiff_T.cpp, ImplicitDiffForStateLU_T)
1200  // multiplies this stored value by `Fact = implicit_fac * dt * dz_inv`
1201  // and `rhoAlpha / met_h_zeta` ONLY — it must NOT apply an additional
1202  // dz_inv factor. `Fact` already carries the single grid-spacing
1203  // factor needed to convert the countergradient correction into a
1204  // flux-divergence contribution consistent with the K*d(phi)/dz terms
1205  // it is being added alongside. Adding a second dz_inv
1206  // double-counts the grid spacing and is
1207  // dimensionally incorrect — do NOT "fix" this by inserting dz_inv
1208  // into the RHS_a += Fact * gam_hi/lo terms in ERF_ImplicitDiff_T.cpp.
1209  if (k < pbli_extent) {
1210  K_turb(i, j, k, EddyDiff::HGAMT_v) = hgamt_arr(i, j, 0);
1211  K_turb(i, j, k, EddyDiff::HGAMQ_v) = hgamq_arr(i, j, 0);
1212  } else {
1213  K_turb(i, j, k, EddyDiff::HGAMT_v) = Real(0);
1214  K_turb(i, j, k, EddyDiff::HGAMQ_v) = Real(0);
1215  }
1216  });
1217 
1218  // FOEXTRAP top and bottom ghost cells
1219  ParallelFor(xybx, [=] AMREX_GPU_DEVICE(int i, int j, int ) noexcept
1220  {
1221  K_turb(i, j, klo-1, EddyDiff::Mom_v ) = K_turb(i, j, klo, EddyDiff::Mom_v );
1222  K_turb(i, j, klo-1, EddyDiff::Theta_v) = K_turb(i, j, klo, EddyDiff::Theta_v);
1223  K_turb(i, j, klo-1, EddyDiff::Q_v ) = K_turb(i, j, klo, EddyDiff::Q_v );
1224  K_turb(i, j, klo-1, EddyDiff::HGAMT_v) = K_turb(i, j, klo, EddyDiff::HGAMT_v);
1225  K_turb(i, j, klo-1, EddyDiff::HGAMQ_v) = K_turb(i, j, klo, EddyDiff::HGAMQ_v);
1226  K_turb(i, j, klo-1, EddyDiff::Turb_lengthscale) = K_turb(i, j, klo, EddyDiff::Turb_lengthscale);
1227  K_turb(i, j, khi+1, EddyDiff::Mom_v ) = K_turb(i, j, khi, EddyDiff::Mom_v );
1228  K_turb(i, j, khi+1, EddyDiff::Theta_v) = K_turb(i, j, khi, EddyDiff::Theta_v);
1229  K_turb(i, j, khi+1, EddyDiff::Q_v ) = K_turb(i, j, khi, EddyDiff::Q_v );
1230  K_turb(i, j, khi+1, EddyDiff::HGAMT_v) = K_turb(i, j, khi, EddyDiff::HGAMT_v);
1231  K_turb(i, j, khi+1, EddyDiff::HGAMQ_v) = K_turb(i, j, khi, EddyDiff::HGAMQ_v);
1232  K_turb(i, j, khi+1, EddyDiff::Turb_lengthscale) = K_turb(i, j, khi, EddyDiff::Turb_lengthscale);
1233  });
1234  }// mfi
1235  // Write MRF-computed PBLH back into SurfaceLayer so Beljaars correction
1236  // and diagnostics can use it, and update_pblh no longer aborts for MRF type.
1237  SurfLayer->set_pblh(level, pblh_mf);
1238 }
constexpr amrex::Real epsv
Definition: ERF_Constants.H:56
constexpr amrex::Real KAPPA
Definition: ERF_Constants.H:71
constexpr amrex::Real CONST_GRAV
Definition: ERF_Constants.H:72
constexpr amrex::Real immersed_neutral_obukhov_length
Definition: ERF_Constants.H:31
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:53
#define RhoTheta_comp
Definition: ERF_IndexDefines.H:54
amrex::GpuArray< Real, AMREX_SPACEDIM > dxInv
Definition: ERF_InitCustomPertVels_ParticleTests.H:17
const bool use_moisture
Definition: ERF_InitCustomPert_ABL.H:71
AMREX_ALWAYS_ASSERT_WITH_MESSAGE(axis >=0 &&axis< AMREX_SPACEDIM, "prob.axis must be 0, 1 or 2")
const int khi
Definition: ERF_InitCustomPert_Bubble.H:25
AMREX_ALWAYS_ASSERT(bx.length()[2]==khi+1)
const int klo
Definition: ERF_InitCustomPert_KE.H:9
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:93
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
std::unique_ptr< amrex::MultiFab > WidenForPBLH(const amrex::MultiFab &src, const int ng, const std::function< void(amrex::MultiFab &)> &fill)
Definition: ERF_PBLModels.H:805
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:309
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:722
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:762
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:201
AMREX_FORCE_INLINE amrex::IntVect TileNoZ()
Definition: ERF_TileNoZ.H:11
@ yvel_bc
Definition: ERF_IndexDefines.H:127
@ cons_bc
Definition: ERF_IndexDefines.H:110
@ xvel_bc
Definition: ERF_IndexDefines.H:126
@ ext_dir
Definition: ERF_IndexDefines.H:340
@ Theta_v
Definition: ERF_IndexDefines.H:290
@ Turb_lengthscale
Definition: ERF_IndexDefines.H:294
@ Q_v
Definition: ERF_IndexDefines.H:293
@ Mom_v
Definition: ERF_IndexDefines.H:289
@ HGAMQ_v
Definition: ERF_IndexDefines.H:301
@ HGAMT_v
Definition: ERF_IndexDefines.H:300
@ ks
Definition: ERF_SLM.H:74
@ rho
Definition: ERF_Kessler.H:25
@ xvel
Definition: ERF_IndexDefines.H:255
@ yvel
Definition: ERF_IndexDefines.H:256
@ zi
Definition: ERF_AdvanceWDM6.cpp:277
int qi
cloud ice
Definition: ERF_DataStruct.H:264
int qv
water vapor
Definition: ERF_DataStruct.H:262
int qc
cloud liquid water
Definition: ERF_DataStruct.H:263
Functor for inverse vertical spacings for terrain-following grids using cell-center heights.
Definition: ERF_PBLModels.H:668
bool enable_mrf_unbounded_vpert
Whether MRF leaves VPERT unlimited by GAMCRT.
Definition: ERF_TurbStruct.H:1050
amrex::Real pbl_blend_length
Boutle blending length L [m]. 0 = off.
Definition: ERF_TurbStruct.H:1041
bool pbl_mrf_use_zero_ri_extent
Whether MRF uses the Ri=0 K-profile extent.
Definition: ERF_TurbStruct.H:1051
amrex::Real qnse_ah
Definition: ERF_TurbStruct.H:1061
StratType pbl_strat_type
Definition: ERF_TurbStruct.H:868
bool enable_vh96_shear_correction
Whether Vogelezang & Holtslag (1996) shear-correction term is enabled.
Definition: ERF_TurbStruct.H:1028
bool enable_mrf_cloud_adjustment
Whether MRF cloud-aware stability adjustments are enabled.
Definition: ERF_TurbStruct.H:1048
amrex::Real pbl_mrf_const_b
MRF constant used to compute PBL height.
Definition: ERF_TurbStruct.H:1036
amrex::Real pblh_smoothing_weight
Center-cell weight in PBLH smoothing stencil (must be in [0,1]).
Definition: ERF_TurbStruct.H:1032
amrex::Real pbl_blend_cs
Smagorinsky coeff for K_h ceiling.
Definition: ERF_TurbStruct.H:1042
amrex::Real qnse_bm
Definition: ERF_TurbStruct.H:1060
amrex::Real pbl_blend_c_max
Power-law ceiling coeff [m^(2/3)/s].
Definition: ERF_TurbStruct.H:1043
amrex::Real qnse_bh
Definition: ERF_TurbStruct.H:1062
bool enable_qnse_stable_functions
Definition: ERF_TurbStruct.H:1057
bool enable_mrf_countergradient
Whether MRF countergradient corrections are enabled.
Definition: ERF_TurbStruct.H:1045
bool enable_pblh_smoothing
Whether spatial smoothing of diagnosed PBLH is enabled.
Definition: ERF_TurbStruct.H:1030
int pblh_smoothing_passes
Number of PBLH smoothing iterations to apply.
Definition: ERF_TurbStruct.H:1031
amrex::Real pbl_ib_z0
Definition: ERF_TurbStruct.H:1047
bool pbl_ib_aware
Definition: ERF_TurbStruct.H:1046
bool pbl_mrf_highres_bounds
Whether MRF applies high-resolution grid-dependent diffusivity bounds.
Definition: ERF_TurbStruct.H:1049
amrex::Real vh96_shear_const_b
Vogelezang & Holtslag (1996) shear-correction constant b.
Definition: ERF_TurbStruct.H:1029
amrex::Real pbl_mrf_Ribcr
Critical bulk Richardson number for the MRF PBL scheme.
Definition: ERF_TurbStruct.H:1035
amrex::Real qnse_am
Definition: ERF_TurbStruct.H:1059
amrex::Real pbl_mrf_sf
MRF surface flux value used to compute PBL height.
Definition: ERF_TurbStruct.H:1037

Referenced by ComputeTurbulentViscosity().

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