@@ -172,26 +172,48 @@ function lamb_smooth_minimum(l, smoothness_param, λ_floor)
172172 return numerator / max (eps (FT), denominator)
173173end
174174
175+ function blend_scales (
176+ method:: SmoothMinimumBlending ,
177+ l:: SA.SVector ,
178+ turbconv_params,
179+ )
180+ FT = eltype (l)
181+ smin_ub = CAP. smin_ub (turbconv_params)
182+ smin_rm = CAP. smin_rm (turbconv_params)
183+ l_final = lamb_smooth_minimum (l, smin_ub, smin_rm)
184+ return max (l_final, FT (0 ))
185+ end
186+
187+ function blend_scales (
188+ method:: HardMinimumBlending ,
189+ l:: SA.SVector ,
190+ turbconv_params,
191+ )
192+ FT = eltype (l)
193+ return max (minimum (l), FT (0 ))
194+ end
195+
175196"""
176- mixing_length(params, ustar, ᶜz, sfc_tke, ᶜlinear_buoygrad, ᶜtke, obukhov_length, ᶜstrain_rate_norm, ᶜPr, ᶜtke_exch)
197+ mixing_length(params, ustar, ᶜz, z_sfc, ᶜdz,
198+ sfc_tke, ᶜlinear_buoygrad, ᶜtke, obukhov_length,
199+ ᶜstrain_rate_norm, ᶜPr, ᶜtke_exch, scale_blending_method)
177200
178201where:
179- - `params`: set with model parameters
180- - `ustar`: friction velocity
181- - `ᶜz`: height
182- - `tke_sfc`: env kinetic energy at first cell center
183- - `ᶜlinear_buoygrad`: buoyancy gradient
184- - `ᶜtke`: env turbulent kinetic energy
185- - `obukhov_length`: surface Monin Obukhov length
186- - `ᶜstrain_rate_norm`: Frobenius norm of strain rate tensor
187- - `ᶜPr`: Prandtl number
188- - `ᶜtke_exch`: subdomain exchange term
189-
190- Returns mixing length as a smooth minimum between
191- wall-constrained length scale,
192- production-dissipation balanced length scale,
193- effective static stability length scale, and
194- Smagorinsky length scale.
202+ - `params`: Parameter set (e.g., CLIMAParameters.AbstractParameterSet).
203+ - `ustar`: Friction velocity [m/s].
204+ - `ᶜz`: Cell center height [m].
205+ - `z_sfc`: Surface elevation [m].
206+ - `ᶜdz`: Cell vertical thickness [m].
207+ - `sfc_tke`: TKE near the surface (e.g., first cell center) [m^2/s^2].
208+ - `ᶜlinear_buoygrad`: N^2, Brunt-Väisälä frequency squared [1/s^2].
209+ - `ᶜtke`: Turbulent kinetic energy at cell center [m^2/s^2].
210+ - `obukhov_length`: Surface Monin-Obukhov length [m].
211+ - `ᶜstrain_rate_norm`: Frobenius norm of strain rate tensor [1/s].
212+ - `ᶜPr`: Turbulent Prandtl number [-].
213+ - `ᶜtke_exch`: TKE exchange term [m^2/s^3].
214+ - `scale_blending_method`: The method to use for blending physical scales.
215+
216+ Calculates the turbulent mixing length, limited by physical constraints and grid resolution.
195217"""
196218function mixing_length (
197219 params,
@@ -206,78 +228,187 @@ function mixing_length(
206228 ᶜstrain_rate_norm,
207229 ᶜPr,
208230 ᶜtke_exch,
231+ scale_blending_method,
209232)
210233
211234 FT = eltype (params)
235+ eps_FT = eps (FT)
236+
237+
212238 turbconv_params = CAP. turbconv_params (params)
239+ sf_params = CAP. surface_fluxes_params (params) # Businger params
240+
213241 c_m = CAP. tke_ed_coeff (turbconv_params)
214242 c_d = CAP. tke_diss_coeff (turbconv_params)
215243 smin_ub = CAP. smin_ub (turbconv_params)
216244 smin_rm = CAP. smin_rm (turbconv_params)
217245 c_b = CAP. static_stab_coeff (turbconv_params)
218246 vkc = CAP. von_karman_const (params)
219247
220- # compute the maximum mixing length at height z
221- l_z = ᶜz - z_sfc
248+ # MOST stability function coefficients
249+ most_a_m = sf_params. ufp. a_m # Businger a_m
250+ most_b_m = sf_params. ufp. b_m # Businger b_m
251+ most_g_m = CAP. coefficient_b_m_gryanik (params) # Gryanik b_m
222252
223- # compute the l_W - the wall constraint mixing length
224- # which imposes an upper limit on the size of eddies near the surface
225- # kz scale (surface layer)
226- if obukhov_length < 0.0 # unstable
227- l_W =
228- vkc * (ᶜz - z_sfc) /
229- max (sqrt (sfc_tke / ustar / ustar) * c_m, eps (FT)) *
230- min ((1 - 100 * (ᶜz - z_sfc) / obukhov_length)^ FT (0.2 ), 1 / vkc)
231- else # neutral or stable
232- l_W =
233- vkc * (ᶜz - z_sfc) /
234- max (sqrt (sfc_tke / ustar / ustar) * c_m, eps (FT))
253+ # l_z: Geometric distance from the surface
254+ l_z = ᶜz - z_sfc
255+ # Ensure l_z is non-negative when ᶜz is numerically smaller than z_sfc.
256+ l_z = max (l_z, FT (0 ))
257+
258+ # l_W: Wall-constrained length scale (near-surface limit, to match
259+ # Monin-Obukhov Similarity Theory in the surface layer, with Businger-Dyer
260+ # type stability functions)
261+ tke_sfc_safe = max (sfc_tke, eps_FT)
262+ ustar_sq_safe = max (ustar * ustar, eps_FT) # ustar^2 may vanish in certain LES setups
263+
264+ # Denominator of the base length scale (always positive):
265+ # c_m * √(tke_sfc / u_*²) = c_m * √(e_sfc) / u_*
266+ # The value increases when u_* is small and decreases when e_sfc is small.
267+ l_W_denom_factor = sqrt (tke_sfc_safe / ustar_sq_safe)
268+ l_W_denom = max (c_m * l_W_denom_factor, eps_FT)
269+
270+ # Base length scale (neutral, but adjusted for TKE level)
271+ # l_W_base = κ * l_z / (c_m * sqrt(e_sfc) / u_star)
272+ # This can be Inf if l_W_denom is eps_FT and l_z is large.
273+ # This can be 0 if l_z is 0.
274+ # The expression approaches ∞ when l_W_denom ≈ eps_FT and l_z > eps_FT,
275+ # and approaches 0 when l_z → 0.
276+ l_W_base = vkc * l_z / l_W_denom
277+
278+ if obukhov_length < FT (0 ) # Unstable case
279+ obukhov_len_safe = min (obukhov_length, - eps_FT) # Ensure L < 0
280+ zeta = l_z / obukhov_len_safe # Stability parameter zeta = z/L (<0)
281+
282+ # Calculate MOST term (1 - b_m * zeta)
283+ # Since zeta is negative, this term is > 1
284+ inner_term = 1 - most_b_m * zeta
285+
286+ # Numerical safety check – by theory the value is ≥ 1.
287+ inner_term_safe = max (inner_term, eps_FT)
288+
289+ # Unstable-regime correction factor:
290+ # (1 − b_m ζ)^(1/4) = φ_m⁻¹,
291+ # where φ_m is the Businger stability function φ_m = (1 − b_m ζ)^(-1/4).
292+ stability_correction = sqrt (sqrt (inner_term_safe))
293+ l_W = l_W_base * stability_correction
294+
295+ else # Neutral or stable case
296+ # Ensure L > 0 for Monin-Obukhov length
297+ obukhov_len_safe_stable = max (obukhov_length, eps_FT)
298+ zeta = l_z / obukhov_len_safe_stable # zeta >= 0
299+
300+ # Stable/neutral-regime correction after Gryanik (2020):
301+ # φ_m = 1 + a_m ζ / (1 + g_m ζ)^(2/3),
302+ # a nonlinear refinement to the Businger formulation.
303+ phi_m_denom_term = (1 + most_g_m * zeta)
304+ # Guard against a negative base in the fractional power
305+ # (theoretically impossible for ζ ≥ 0 and g_m > 0, retained for robustness).
306+ phi_m_denom_cubed_sqrt = cbrt (phi_m_denom_term)
307+ phi_m_denom =
308+ max (phi_m_denom_cubed_sqrt * phi_m_denom_cubed_sqrt, eps_FT) # (val)^(2/3)
309+
310+ phi_m = 1 + (most_a_m * zeta) / phi_m_denom
311+
312+ # Stable-regime correction factor: 1 / φ_m.
313+ # phi_m should be >= 1 for stable/neutral
314+ stability_correction = 1 / max (phi_m, eps_FT)
315+
316+ # Apply the correction factor
317+ l_W = l_W_base * stability_correction
235318 end
236-
237- # compute l_TKE - the production-dissipation balanced length scale
238- a_pd = c_m * (2 * ᶜstrain_rate_norm - ᶜlinear_buoygrad / ᶜPr) * sqrt (ᶜtke)
239- # Dissipation term
240- c_neg = c_d * ᶜtke * sqrt (ᶜtke)
241- if abs (a_pd) > eps (FT) && 4 * a_pd * c_neg > - (ᶜtke_exch * ᶜtke_exch)
242- l_TKE = max (
243- - (ᶜtke_exch / 2 / a_pd) +
244- sqrt (ᶜtke_exch * ᶜtke_exch + 4 * a_pd * c_neg) / 2 / a_pd,
245- 0 ,
246- )
247- elseif abs (a_pd) < eps (FT) && abs (ᶜtke_exch) > eps (FT)
248- l_TKE = c_neg / ᶜtke_exch
249- else
250- l_TKE = FT (0 )
319+ l_W = max (l_W, FT (0 )) # Ensure non-negative
320+
321+ # --- l_TKE: TKE production-dissipation balance scale ---
322+ tke_pos = max (ᶜtke, FT (0 )) # Ensure TKE is not negative
323+ sqrt_tke_pos = sqrt (tke_pos)
324+
325+ # Net production of TKE from shear and buoyancy is approximated by
326+ # (S² − N²/Pr_t) · √TKE · l,
327+ # where S² denotes the gradient involved in shear production and
328+ # N²/Pr_t denotes the gradient involved in buoyancy production.
329+ # The factor below corresponds to that production term normalised by l.
330+ a_pd = c_m * (2 * ᶜstrain_rate_norm - ᶜlinear_buoygrad / ᶜPr) * sqrt_tke_pos
331+
332+ # Dissipation is modelled as c_d · k^{3/2} / l.
333+ # For the quadratic expression below, c_neg ≡ c_d · k^{3/2}.
334+ c_neg = c_d * tke_pos * sqrt_tke_pos
335+
336+ l_TKE = FT (0 )
337+ # Solve for l_TKE in
338+ # a_pd · l_TKE − c_neg / l_TKE + ᶜtke_exch = 0
339+ # ⇒ a_pd · l_TKE² + ᶜtke_exch · l_TKE − c_neg = 0
340+ # yielding
341+ # l_TKE = (−ᶜtke_exch ± √(ᶜtke_exch² + 4 a_pd c_neg)) / (2 a_pd).
342+ if abs (a_pd) > eps_FT # If net of shear and buoyancy production (a_pd) is non-zero
343+ discriminant = ᶜtke_exch * ᶜtke_exch + 4 * a_pd * c_neg
344+ if discriminant >= FT (0 ) # Ensure real solution exists
345+ # Select the physically admissible (positive) root for l_TKE.
346+ # When a_pd > 0 (production exceeds dissipation) the root
347+ # (−ᶜtke_exch + √D) / (2 a_pd)
348+ # is positive. For a_pd < 0 the opposite root is required.
349+ l_TKE_sol1 = (- (ᶜtke_exch) + sqrt (discriminant)) / (2 * a_pd)
350+ # For a_pd < 0 (local destruction exceeds production) use
351+ # (−ᶜtke_exch − √D) / (2 a_pd).
352+ if a_pd > FT (0 )
353+ l_TKE = l_TKE_sol1
354+ else # a_pd < FT(0)
355+ l_TKE = (- (ᶜtke_exch) - sqrt (discriminant)) / (2 * a_pd)
356+ end
357+ l_TKE = max (l_TKE, FT (0 )) # Ensure it's non-negative
358+ end
359+ elseif abs (ᶜtke_exch) > eps_FT # If a_pd is zero, balance is between exchange and dissipation
360+ # ᶜtke_exch = c_neg / l_TKE => l_TKE = c_neg / ᶜtke_exch
361+ # Ensure division is safe and result is positive
362+ if ᶜtke_exch > eps_FT # Assuming positive exchange means TKE sink from env perspective
363+ l_TKE = c_neg / ᶜtke_exch # if c_neg is positive, l_TKE is positive
364+ elseif ᶜtke_exch < - eps_FT # Negative exchange means TKE source for env
365+ # -|ᶜtke_exch| = c_neg / l_TKE. If c_neg > 0, this implies l_TKE < 0, which is unphysical.
366+ # This case (a_pd=0, tke_exch < 0, c_neg > 0) implies TKE source and dissipation, no production.
367+ # Dissipation = Source. So, c_d * k_sqrt_k / l = -tke_exch. l = c_d * k_sqrt_k / (-tke_exch)
368+ l_TKE = c_neg / (- (ᶜtke_exch))
369+ end
370+ l_TKE = max (l_TKE, FT (0 ))
251371 end
252-
253- # compute l_N - the effective static stability length scale.
254- N_eff = sqrt (max (ᶜlinear_buoygrad, 0 ))
255- if N_eff > 0.0
256- l_N = min (sqrt (max (c_b * ᶜtke, 0 )) / N_eff, l_z)
257- else
258- l_N = l_z
372+ # If a_pd = 0 and ᶜtke_exch = 0 (or c_neg = 0), l_TKE remains zero.
373+
374+ # --- l_N: Static-stability length scale (buoyancy limit), constrained by l_z ---
375+ N_eff_sq = max (ᶜlinear_buoygrad, FT (0 )) # Use N^2 only if stable (N^2 > 0)
376+ l_N = l_z # Default to wall distance if not stably stratified or TKE is zero
377+ if N_eff_sq > eps_FT && tke_pos > eps_FT
378+ N_eff = sqrt (N_eff_sq)
379+ # l_N ~ sqrt(c_b * TKE) / N_eff
380+ l_N_physical = sqrt (c_b * tke_pos) / N_eff
381+ # Limit by distance from wall
382+ l_N = min (l_N_physical, l_z)
259383 end
384+ l_N = max (l_N, FT (0 )) # Ensure non-negative
260385
261- # compute l_smag - smagorinsky length scale
262- l_smag = smagorinsky_lilly_length (
263- CAP. c_smag (params),
264- N_eff,
265- ᶜdz,
266- ᶜPr,
267- ᶜstrain_rate_norm,
268- )
269386
270- # add limiters
271- l = SA. SVector (
272- l_N > l_z ? l_z : l_N,
273- l_TKE > l_z ? l_z : l_TKE,
274- l_W > l_z ? l_z : l_W,
275- )
276- # get soft minimum
277- l_smin = lamb_smooth_minimum (l, smin_ub, smin_rm)
278- l_limited = max (l_smag, min (l_smin, l_z))
387+ # --- Combine Scales ---
388+
389+ # Vector of *physical* scales (wall, TKE, stability)
390+ # These scales (l_W, l_TKE, l_N) are already ensured to be non-negative.
391+ # l_N is already limited by l_z. l_W and l_TKE are not necessarily.
392+ l_physical_scales = SA. SVector (l_W, l_TKE, l_N)
393+
394+ l_smin =
395+ blend_scales (scale_blending_method, l_physical_scales, turbconv_params)
396+
397+ # 1. Limit the combined physical scale by the distance from the wall.
398+ # This step mitigates excessive values of l_W or l_TKE.
399+ l_limited_phys_wall = min (l_smin, l_z)
400+
401+ # 2. Impose the grid-scale limit
402+ l_final = min (l_limited_phys_wall, ᶜdz)
403+
404+ # Final check: guarantee that the mixing length is at least a small positive
405+ # value. This prevents division-by-zero in
406+ # ε_d = C_d · TKE^{3/2} / l_mix
407+ # when TKE > 0. When TKE = 0, l_mix is inconsequential, but eps_FT
408+ # provides a conservative lower bound.
409+ l_final = max (l_final, eps_FT)
279410
280- return MixingLength {FT} (l_limited , l_W, l_TKE, l_N)
411+ return MixingLength {FT} (l_final , l_W, l_TKE, l_N)
281412end
282413
283414"""
0 commit comments