@@ -3399,7 +3399,7 @@ def delta_t(self, value):
33993399 self ._delta_t .sym = value
34003400
34013401 @timing .routine_timer_decorator
3402- def estimate_dt (self , direction_aware : bool = False ):
3402+ def estimate_dt (self , direction_aware : bool = False , percentile : float = 0.0 ):
34033403 r"""
34043404 Estimate an appropriate timestep for the advection-diffusion solver.
34053405
@@ -3509,12 +3509,28 @@ def estimate_dt(self, direction_aware: bool = False):
35093509 ## dt_adv_i = h_i / |v_i| for advection
35103510 ## dt_diff_i = h_i^2 / κ for diffusion (using global κ for now)
35113511
3512+ # Reduce per-element dt to one global value. Default (percentile=0) =
3513+ # strict global MINIMUM — one cell sets the limit. percentile>0 takes the
3514+ # Nth global percentile (50 = median) of the per-element dt instead, so a
3515+ # few anisotropic SLIVER cells (velocity ACROSS a thin cell) don't collapse
3516+ # dt. SLCN is unconditionally stable, and ``direction_aware`` already
3517+ # credits cells stretched ALONG the flow — together they give the
3518+ # orientation-aware + sliver-robust timestep.
3519+ def _reduce_dt (per_elem ):
3520+ fin = per_elem [np .isfinite (per_elem )] if len (per_elem ) else per_elem
3521+ if percentile and percentile > 0 :
3522+ gathered = comm .allgather (np .ascontiguousarray (fin , dtype = float ))
3523+ allv = (np .concatenate ([a for a in gathered if a .size ])
3524+ if any (a .size for a in gathered ) else np .empty (0 ))
3525+ return float (np .percentile (allv , percentile )) if allv .size else np .inf
3526+ loc = float (np .min (fin )) if len (fin ) else np .inf
3527+ return comm .allreduce (loc , op = MPI .MIN )
3528+
35123529 # Per-element diffusive timestep (all elements use same diffusivity)
35133530 if diffusivity_glob > 0 :
35143531 dt_diff_per_element = (element_radii ** 2 ) / diffusivity_glob
3515- min_dt_diff_local = np .min (dt_diff_per_element ) if len (dt_diff_per_element ) > 0 else np .inf
35163532 else :
3517- min_dt_diff_local = np .inf
3533+ dt_diff_per_element = np .array ([ np . inf ])
35183534
35193535 # Per-element advective timestep — either isotropic
35203536 # (mesh._radii / |v|) or direction-aware (v-aligned cell
@@ -3552,11 +3568,9 @@ def estimate_dt(self, direction_aware: bool = False):
35523568 h_per_element / vel_magnitudes ,
35533569 np .inf
35543570 )
3555- min_dt_adv_local = np .min (dt_adv_per_element ) if len (dt_adv_per_element ) > 0 else np .inf
3556-
3557- # Get global minimum timesteps (parallel-safe)
3558- min_dt_diff_glob = comm .allreduce (min_dt_diff_local , op = MPI .MIN )
3559- min_dt_adv_glob = comm .allreduce (min_dt_adv_local , op = MPI .MIN )
3571+ # Global reduction — strict min (percentile=0) or Nth percentile (median).
3572+ min_dt_diff_glob = _reduce_dt (dt_diff_per_element )
3573+ min_dt_adv_glob = _reduce_dt (dt_adv_per_element )
35603574
35613575 # Store for user inspection
35623576 self .dt_adv = min_dt_adv_glob if not np .isinf (min_dt_adv_glob ) else 0.0
0 commit comments