Modal (Legendre spectral-decay) refinement indicator for 3-D spectral element solutions.
Provides the trigger mechanism for adaptive mesh refinement (AMR): a per-element scalar that measures how much of a solution field's energy sits in its highest polynomial modes. A well-resolved (smooth) field has energy that decays rapidly with mode number, so almost all of its energy lives in the low modes; an under-resolved field (a steep front, a discontinuity, or simply too-coarse an element) leaves significant energy in the top modes. Comparing the top-mode energy fraction against thresholds flags each element for refinement, coarsening, or no change.
Following Persson & Peraire (2006), "Sub-cell shock capturing for discontinuous Galerkin methods", AIAA 2006-112, and the robustified variant of Hennemann, Rueda-Ramirez, Hindenlang & Gassner (2021), J. Comput. Phys. 426, 109935 (the shock indicator used in Trixi.jl), the nodal solution on an element is expanded in a tensor-product Legendre modal basis
u(xi,eta,zeta) = sum_{p=0}^{N} sum_{q=0}^{N} sum_{r=0}^{N} uhat(p,q,r) Ltilde_p(xi) Ltilde_q(eta) Ltilde_r(zeta),
where Ltilde_p are the L2-normalized Legendre polynomials on [-1,1] (int_{-1}^{1} Ltilde_p Ltilde_q dxi = delta_pq). With that normalization the total modal energy equals the exact L2 energy of the field on the reference element,
E_tot = sum_{p,q,r} uhat(p,q,r)^2 = ||u||_{L2([-1,1]^3)}^2 .
Two "clipped" energies are formed by dropping the highest one and two modes in each direction,
E_clip1 = sum_{p,q,r <= N-1} uhat(p,q,r)^2 , E_clip2 = sum_{p,q,r <= N-2} uhat(p,q,r)^2 ,
and the smoothness ratio is the larger of the top-shell and next-shell energy fractions,
S_e = max( (E_tot - E_clip1)/E_tot , (E_clip1 - E_clip2)/E_clip1 ) .
The second term (present only for N >= 2) guards against the odd/even parity dropouts that a single-mode measure can suffer for symmetric data - this is the Hennemann-Gassner refinement of the original single-mode Persson-Peraire estimate. The indicator returned per element is the base-10 logarithm sigma_e = log10(S_e); a near-machine-zero floor keeps it finite for a perfectly resolved (or identically zero) field.
S_e is a ratio and therefore scale-free: it says nothing about how much energy an element carries, only how that energy is distributed across modes. An element holding a negligible share of the field's energy - low-amplitude grid-scale residue in the wake of a passing wave, say - still shows a flat modal spectrum and so reads as under-resolved forever. Guarding the ratio with an absolute floor at machine epsilon does not help: E_tot carries the SQUARE of the field amplitude, so a wake at 1e-5 of the peak amplitude sits ~1e10 above epsilon and still takes the ratio branch. Such elements are never flagged for coarsening and the mesh behind a front is never released.
The fix is a second, RELATIVE floor. A per-element gate energy
g_e = sum_v w_v E_tot,e,v
is formed from the modal energies of the driving variables with non-negative weights w_v. Since E_tot,e,v is the exact L2 energy of variable v on the reference element, g_e with w taken from a quadratic entropy function is the discrete entropy integral over the element: a convex function of the state, and therefore an amplitude measure that is meaningful across variables of very different magnitude. Comparing it against a field-wide energy scale,
effective floor = max( epsilon, relativeEnergyFloor * energyScale ) g_e <= effective floor -> S_e := 0 (element is quiescent, hence resolved)
gives an amplitude gate that is independent of the modal shape. Because energy goes as amplitude squared, relativeEnergyFloor is 10**(dB/10) in amplitude terms: the default 1e-12 treats amplitudes below 1e-6 (-120 dB) of the field scale as quiescent.
Raising the floor is NOT free, and the useful range is narrower than it looks. The gate cannot distinguish residue from the low-amplitude flank of a feature that is genuinely under-resolved, so an aggressive floor also suppresses refinement DEPTH: on the ultrasound benchmark a floor of 1e-8 stopped the source pulse from reaching the level cap, which doubled the level-based time step and produced a mesh roughly twice as large by the end of the run. Measure before raising it; see the note on SELF_AMR_DEFAULT_RELFLOOR.
The gate is a single hard cut by default, which reintroduces on the energy axis exactly the thrashing the two sigma thresholds exist to prevent: an element whose energy drifts across that one value flips COARSEN <-> REFINE on successive epochs. Supplying significantEnergyFloor opens a hysteresis band instead,
g_e <= quiescent floor -> COARSEN, whatever the spectrum says quiescent < g_e <= significant -> SELF_AMR_KEEP (hold the mesh) g_e > significant floor -> the spectrum decides, as before
the middle zone reading "too weak to be worth spending levels on, too strong to declare resolved". Inside the band sigma_e still reports the true spectrum; only the flag is held.
energyScale defaults to the largest gate energy over the elements (globally reduced when Estimate is given an MPI communicator), which makes the gate track a decaying front. It can be pinned with SetEnergyScale for callers that need the flag decision to be independent of a floating-point reduction, and the gate itself can be supplied outright through the optional gate argument to Estimate (for example an exactly integrated nodal entropy).
Note that raising coarsenThreshold is NOT a substitute: in a low-amplitude wake sigma_e is near 0, so any threshold high enough to coarsen the wake also stops a genuine front from refining. The two decisions cannot be separated by thresholds alone.
With user-supplied thresholds sigma_refine > sigma_coarsen the per-element flag is
sigma_e > sigma_refine -> SELF_AMR_REFINE (+1) top modes carry too much energy sigma_e < sigma_coarsen -> SELF_AMR_COARSEN (-1) field is over-resolved on this element otherwise -> SELF_AMR_KEEP (0)
Larger (closer to zero) sigma_e means a less smooth / less resolved field. Recommended starting values for double precision are sigma_refine ~ -3.0 and sigma_coarsen ~ -8.0; they are problem dependent and are deliberately left as required arguments rather than hidden defaults.
This module contains the portable (CPU) implementation using do concurrent over elements. The backend extension modules (SELF_RefinementIndicator_3D) add device storage and a GPU kernel while preserving the mathematics bit-for-bit in structure.
| Type | Visibility | Attributes | Name | Initial | |||
|---|---|---|---|---|---|---|---|
| integer, | public, | parameter | :: | SELF_AMR_ALLVARS | = | 0 | |
| integer, | public, | parameter | :: | SELF_AMR_COARSEN | = | -1 | |
| real(kind=prec), | public, | parameter | :: | SELF_AMR_DEFAULT_RELFLOOR | = | 1.0e-12_prec | |
| integer, | public, | parameter | :: | SELF_AMR_KEEP | = | 0 | |
| integer, | public, | parameter | :: | SELF_AMR_REFINE | = | 1 |
| Type | Visibility | Attributes | Name | Initial | |||
|---|---|---|---|---|---|---|---|
| integer, | public | :: | N | = | 0 | Polynomial degree of the interpolant the indicator is built for. |
|
| real(kind=prec), | public, | pointer, contiguous, dimension(:,:) | :: | Pmodal | => | null() | Nodal-to-modal transform. Pmodal(ii,p) is the (p,ii) entry of the inverse Legendre Vandermonde in the L2-normalized basis, so the 1-D modal coefficients are uhat(p) = sum_ii Pmodal(ii,p) * u(ii) (first index summed, SELF matrix convention). |
| real(kind=prec), | public | :: | coarsenThreshold | = | 0.0_prec | Elements with sigma_e below this value are flagged SELF_AMR_COARSEN. |
|
| real(kind=prec), | public | :: | energyScale | = | 0.0_prec | Squared field scale the relative floor is measured against, in the units of the gate energy. Meaningful only when energyScaleIsSet is true; otherwise the scale is recomputed from the current field on every Estimate. |
|
| logical, | public | :: | energyScaleIsSet | = | .false. | Whether energyScale was pinned by SetEnergyScale (true) or is computed automatically as the largest gate energy over the elements (false, the default). |
|
| real(kind=prec), | public, | pointer, contiguous, dimension(:) | :: | energyWeight | => | null() | Non-negative weights w_v of the gate energy g_e = sum_v w_v E_tot,e,v, indexed by solution variable. Resolved lazily, because the variable count is a property of the solution field and is not known at Init. |
| logical, | public | :: | energyWeightsSet | = | .false. | Whether energyWeight was supplied by SetEnergyWeights (true) or is regenerated from the driving-variable index on every Estimate (false, the default). |
|
| integer, | public, | pointer, contiguous, dimension(:) | :: | flag | => | null() | Per-element refinement flag: SELF_AMR_REFINE / SELF_AMR_KEEP / SELF_AMR_COARSEN. |
| real(kind=prec), | public, | pointer, contiguous, dimension(:) | :: | gate | => | null() | Per-element gate energy g_e from the most recent Estimate. Retained as a diagnostic: it is the quantity the amplitude gate actually compared against the effective floor. |
| real(kind=prec), | public, | pointer, contiguous, dimension(:) | :: | indicator | => | null() | Per-element indicator value sigma_e = log10(S_e). |
| integer, | public | :: | nElem | = | 0 | Number of (rank-local) elements the indicator arrays are sized for. |
|
| integer, | public | :: | nVarWeights | = | 0 | Allocated length of energyWeight (the solution variable count it was resolved for). |
|
| real(kind=prec), | public | :: | refineThreshold | = | 0.0_prec | Elements with sigma_e above this value are flagged SELF_AMR_REFINE. |
|
| real(kind=prec), | public | :: | relativeEnergyFloor | = | SELF_AMR_DEFAULT_RELFLOOR | Elements whose gate energy is at or below relativeEnergyFloorenergyScale are treated as quiescent (hence perfectly resolved) regardless of their modal shape. Energy goes as amplitude squared, so this is 10*(dB/10) in amplitude terms: 1e-8 gates amplitudes below 1e-4 (-80 dB) of the field scale. Set to 0 to recover the pure absolute (machine-epsilon) floor. |
|
| real(kind=prec), | public | :: | significantEnergyFloor | = | SELF_AMR_DEFAULT_RELFLOOR | Upper edge of the hysteresis band on the energy axis. Elements between relativeEnergyFloor and this fraction of the energy scale are flagged SELF_AMR_KEEP whatever their spectrum: too weak to justify spending levels on, too strong to declare resolved. Equal to relativeEnergyFloor by default, which collapses the band to a single hard cut. See SetRelativeEnergyFloor. |
| procedure, public :: ClearEnergyScale => ClearEnergyScale_RefinementIndicator3D_t | |
| procedure, public :: CountFlagged => CountFlagged_RefinementIndicator3D_t | |
| procedure, public :: Estimate => Estimate_RefinementIndicator3D_t | |
| procedure, public :: Free => Free_RefinementIndicator3D_t | |
| procedure, public :: Init => Init_RefinementIndicator3D_t | |
| procedure, public :: SetEnergyScale => SetEnergyScale_RefinementIndicator3D_t | |
| procedure, public :: SetEnergyWeights => SetEnergyWeights_RefinementIndicator3D_t | |
| procedure, public :: SetRelativeEnergyFloor => SetRelativeEnergyFloor_RefinementIndicator3D_t | |
| procedure, public :: SetThresholds => SetThresholds_RefinementIndicator3D_t | |
| procedure, public :: UpdateDevice => UpdateDevice_RefinementIndicator3D_t | |
| procedure, public :: UpdateHost => UpdateHost_RefinementIndicator3D_t |
Count the rank-local elements currently carrying the requested flag value (SELF_AMR_REFINE / SELF_AMR_KEEP / SELF_AMR_COARSEN). Provided as a convenience for drivers and tests; a global count across MPI ranks is the caller's responsibility.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(RefinementIndicator3D_t), | intent(in) | :: | this | |||
| integer, | intent(in) | :: | flagValue |
L2-normalized Legendre polynomial Ltilde_p(x) = L_p(x)*sqrt((2p+1)/2) on [-1,1], evaluated with the standard three-term recurrence. The normalization gives int_{-1}^{1} Ltilde_p Ltilde_q dx = delta_pq.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| integer, | intent(in) | :: | p | |||
| real(kind=real64), | intent(in) | :: | x |
Build the nodal->modal transform Pmodal for a 1-D degree-N interpolant whose nodes are controlPoints(1:N+1). The transform is the exact inverse of the L2-normalized Legendre Vandermonde V(i,p) = Ltilde_{p-1}(x_i); Pmodal(ii,p) stores V^{-1}(p,ii) so that uhat(p) = sum_ii Pmodal(ii,p) * u(ii). The Vandermonde inverse is formed in double precision for conditioning and cast back to the working precision.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| real(kind=prec), | intent(in) | :: | controlPoints(1:N+1) | |||
| integer, | intent(in) | :: | N | |||
| real(kind=prec), | intent(out) | :: | Pmodal(1:N+1,1:N+1) |
Argument validation shared by the portable and backend Estimate implementations.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(RefinementIndicator3D_t), | intent(in) | :: | this | |||
| class(Scalar3D), | intent(in) | :: | solution | |||
| integer, | intent(in) | :: | ivar | |||
| real(kind=prec), | intent(in), | optional | :: | gate(:) |
Return to the automatic energy scale (the largest gate energy over the elements).
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(RefinementIndicator3D_t), | intent(inout) | :: | this |
Compute the per-element modal-energy indicator sigma_e and refine/keep/coarsen flag from the nodal solution field. ivar selects the driving variable in [1,solution%nVar]; passing SELF_AMR_ALLVARS (=0) reduces the indicator over all variables by taking, per element, the largest (least smooth) smoothness ratio S_e before the log10.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(RefinementIndicator3D_t), | intent(inout) | :: | this | |||
| class(Scalar3D), | intent(in) | :: | solution | |||
| integer, | intent(in) | :: | ivar | |||
| integer, | intent(in), | optional | :: | comm | ||
| real(kind=prec), | intent(in), | optional | :: | gate(:) |
Second phase of an estimate, shared by every backend so that the flags are identical whichever computed the spectra: apply the amplitude gate, take the log10 and set the refine/keep/coarsen flags.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(RefinementIndicator3D_t), | intent(inout) | :: | this | |||
| real(kind=prec), | intent(in) | :: | energyScale |
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(RefinementIndicator3D_t), | intent(inout) | :: | this |
Allocate the indicator for an interpolant of degree interp%N and nElem elements and precompute the nodal->modal transform matrix from the interpolant control points.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(RefinementIndicator3D_t), | intent(out) | :: | this | |||
| type(Lagrange), | intent(in), | target | :: | interp | ||
| integer, | intent(in) | :: | nElem | |||
| real(kind=prec), | intent(in) | :: | refineThreshold | |||
| real(kind=prec), | intent(in) | :: | coarsenThreshold |
Invert the n-by-n matrix A by Gauss-Jordan elimination with partial pivoting, in double precision. Used once at initialization for the small (N+1) Legendre Vandermonde; A is a well-conditioned Vandermonde in an orthonormal basis, so a direct solve is appropriate.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| real(kind=real64), | intent(in) | :: | A(1:n,1:n) | |||
| real(kind=real64), | intent(out) | :: | Ainv(1:n,1:n) | |||
| integer, | intent(in) | :: | n |
Determine the energy scale the relative floor is measured against: the pinned value when SetEnergyScale was used, otherwise the largest gate energy over the (rank-local) elements. When comm is present the maximum is taken over the communicator, so every rank applies the same floor and the flags - hence the adapted mesh - do not depend on the decomposition. One small collective per indicator evaluation, i.e. per adaptation epoch, never inside the time-stepping loop.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(RefinementIndicator3D_t), | intent(in) | :: | this | |||
| real(kind=prec), | intent(out) | :: | energyScale | |||
| integer, | intent(in), | optional | :: | comm |
Ensure energyWeight is associated and sized to nVar. Weights supplied by SetEnergyWeights must match nVar; otherwise the default weights are regenerated: unit weight on the driving variable, or on every variable when ivar is SELF_AMR_ALLVARS. Resolved here rather than at Init because the variable count belongs to the solution field, not to the indicator.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(RefinementIndicator3D_t), | intent(inout) | :: | this | |||
| integer, | intent(in) | :: | nVar | |||
| integer, | intent(in) | :: | ivar |
Pin the energy scale that normalizes the relative floor, in the units of the gate energy (squared field amplitude times the reference-element volume). Two reasons to use this rather than the automatic maximum over elements:
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(RefinementIndicator3D_t), | intent(inout) | :: | this | |||
| real(kind=prec), | intent(in) | :: | energyScale |
Set the per-variable weights w_v >= 0 of the gate energy g_e = sum_v w_v E_tot,e,v, where E_tot,e,v is the exact L2 energy of variable v on the reference element.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(RefinementIndicator3D_t), | intent(inout) | :: | this | |||
| real(kind=prec), | intent(in) | :: | w(:) |
Set the relative energy floor of the amplitude gate. An element whose gate energy g satisfies
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(RefinementIndicator3D_t), | intent(inout) | :: | this | |||
| real(kind=prec), | intent(in) | :: | relativeEnergyFloor | |||
| real(kind=prec), | intent(in), | optional | :: | significantEnergyFloor |
Update the refine/coarsen thresholds without rebuilding the transform.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(RefinementIndicator3D_t), | intent(inout) | :: | this | |||
| real(kind=prec), | intent(in) | :: | refineThreshold | |||
| real(kind=prec), | intent(in) | :: | coarsenThreshold |
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(RefinementIndicator3D_t), | intent(inout) | :: | this |
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(RefinementIndicator3D_t), | intent(inout) | :: | this |