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

Referenced by ComputeTurbulentViscosity().

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