Adaptive-mesh-refinement controller for 2-D DG models: the driver that closes the serial AMR loop around a live, time-stepping model. One call to Adapt performs one adaptation epoch between time steps:
The controller owns the forest, the indicator, and the meshes/geometries it emits. The mesh is rebuilt each epoch and the previous one freed after the model is rebound. Geometry instead lives in two long-lived buffers that alternate (AMR Stage 6c), so it is resized rather than reallocated and the previous epoch's geometry remains available while the new one is built. The base mesh and geometry the model was initialized with belong to the caller and are never freed here, but the base mesh must outlive the controller: it supplies the boundary-condition metadata and the communicator for every emitted mesh. After controller%Free the model's mesh/geometry pointers are dangling, so free or stop using the model first.
MPI (AMR Stage 5): the forest is rank-replicated. At Init the global base-mesh tables are allgathered so every rank builds an identical forest; each epoch the rank-local indicator flags are allgathered (one small collective) so every rank applies identical mutations and computes identical transfer plans and global connectivity. EmitMesh re-decomposes the new leaf list into contiguous (space-filling-curve) ranges, so repartitioning and load balance are implicit in every epoch.
Solution migration is point-to-point (Stage-5 v2). The old elements a rank's new element range reads through the plan form a contiguous WINDOW of the old element list, because both partitions are contiguous ranges of the same leaf order; and because the plan is replicated, every rank derives its own window and each peer's window locally - PlanWindows - with no communication at all. ExchangeOldWindow then moves exactly the runs that cross ranks with matched MPI_Isend/MPI_Irecv, so per-rank traffic and memory scale with what actually moves rather than with the global field. SELF_AMR_MIGRATE_GATHER=1 restores the v1 gather-then- slice migration (an allgathered global old field, sliced to the new rank-local range); the two are bit-identical, and SELF_AMR_MIGRATE_VERIFY=1 asserts that in-process.
All communication runs between time steps at the adaptation cadence; nothing is added to the time-stepping loop.
Because refinement halves the element scale per level, an explicit-stability time step chosen for the base mesh must shrink with the finest active level; RecommendedTimeStep(dtBase) = dtBase / 2**MaxLevel gives the level-based bound to pass to ForwardStep after each epoch.
Debug switches for the Stage 6c incremental geometry path, resolved once from the environment on first use. Present so that a suspected geometry problem can be split between the reuse copy and the compacted generation without rebuilding:
SELF_AMR_GEOM_NO_REUSE=1 regenerate every element (the reuse predicate never fires) SELF_AMR_GEOM_VERIFY=1 additionally generate the full geometry the old way and compare element by element, reporting the first quantity that differs
| Type | Visibility | Attributes | Name | Initial | |||
|---|---|---|---|---|---|---|---|
| logical, | public, | save | :: | geomDebugResolved | = | .false. | |
| logical, | public, | save | :: | geomFull | = | .false. | SELF_AMR_GEOM_FULL=1: bypass the incremental path Debug switches for the multi-rank solution migration, resolved in the same place: SELF_AMR_MIGRATE_GATHER=1 migrate through the Stage-5 v1 allgather instead of the point-to-point window exchange SELF_AMR_MIGRATE_VERIFY=1 run BOTH migrations and compare the received window against the allgathered global field, bit for bit SELF_AMR_TRANSFER_HOST=1 migrate and apply on the HOST even on a GPU build, i.e. the portable windowed path. Both an escape hatch and the way the device path is timed against it in one binary, which is what keeps the mesh trajectory identical on both sides of the comparison. SELF_AMR_TRANSFER_VERIFY=1 apply the plan BOTH ways from the same migrated window and compare, to a TOLERANCE. Deliberately a separate switch from MIGRATE_VERIFY and deliberately not exact: migration is byte movement and is checked bitwise, while the device apply contracts its multiply-accumulates into FMAs and agrees with the host only to round-off. Merging the two would force the strict one down to the loose bar. |
| logical, | public, | save | :: | geomNoReuse | = | .false. | |
| logical, | public, | save | :: | geomVerify | = | .false. | |
| logical, | public, | save | :: | migrateGather | = | .false. | |
| logical, | public, | save | :: | migrateVerify | = | .false. | |
| logical, | public, | save | :: | transferHost | = | .false. | |
| logical, | public, | save | :: | transferVerify | = | .false. |
| Type | Visibility | Attributes | Name | Initial | |||
|---|---|---|---|---|---|---|---|
| type(SEMQuad), | public, | pointer | :: | activeGeom | => | null() | geometry the model currently runs on |
| type(Mesh2D), | public, | pointer | :: | activeMesh | => | null() | mesh the model currently runs on |
| type(Mesh2D), | public, | pointer | :: | baseMesh | => | null() | caller-owned; metadata source for EmitMesh |
| real(kind=prec), | public | :: | coarsenThreshold | = | 0.0_prec | ||
| real(kind=prec), | public, | allocatable | :: | energyWeights(:) | |||
| type(QuadTreeMesh2D), | public | :: | forest | ||||
| type(SEMQuad), | public, | pointer | :: | genGeom | => | null() | Cumulative geometry-reuse accounting (AMR Stage 6c), so a run can report what fraction of elements avoided regeneration rather than leaving the payoff to be estimated. |
| type(SEMQuad), | public, | pointer | :: | geomA | => | null() | |
| type(SEMQuad), | public, | pointer | :: | geomB | => | null() | |
| integer, | public | :: | geomSlot | = | 0 | Scratch geometry holding just the elements an epoch actually changed, generated compacted and then scattered into place. Persistent and resized, so it allocates only when an epoch changes more elements than any epoch before it. |
|
| type(RefinementIndicator2D), | public | :: | indicator | ||||
| type(Lagrange), | public, | pointer | :: | interp | => | null() | the model's solution interpolant |
| integer, | public | :: | ivar | = | SELF_AMR_ALLVARS | driving variable for the indicator |
|
| integer, | public | :: | maxLevel | = | 1 | refinement-level cap |
|
| integer(kind=int64), | public | :: | nGeomGenerated | = | 0 | Point-to-point solution migration state. xferWin holds this rank's window of the OLD field - the contiguous run of old elements its new element range references - and is persistent and grow-only, so a settled adapting run performs no allocation in the migration path. It is flat because it is viewed through a rank-remapped pointer whose element lower bound is the window's first GLOBAL old element index, which is the numbering the transfer plan uses. winFirst/winLast hold that window for every rank. |
|
| integer(kind=int64), | public | :: | nGeomReused | = | 0 | ||
| integer, | public | :: | nHalo | = | 1 | refine-flag halo-expansion passes |
|
| integer(kind=int64), | public | :: | nMigrateBytesRecv | = | 0 | ||
| integer(kind=int64), | public | :: | nMigrateBytesSent | = | 0 | ||
| integer(kind=int64), | public | :: | nMigrateElemRemote | = | 0 | old elements received from other ranks |
|
| logical, | public | :: | ownsActive | = | .false. | whether activeMesh was emitted by us (vs the caller's) Two long-lived geometry buffers, alternated each epoch (AMR Stage 6c). Geometry is now resized in place rather than allocated and freed per epoch, which is what lets Stage 6b's amortization apply to it; alternating means the PREVIOUS epoch's geometry is still intact while the new one is filled, which the incremental reuse path needs. geomSlot records which buffer activeGeom currently is, or 0 while it is still the caller's geometry. |
|
| real(kind=prec), | public | :: | refineThreshold | = | 0.0_prec | ||
| real(kind=prec), | public | :: | relativeEnergyFloor | = | SELF_AMR_DEFAULT_RELFLOOR | ||
| real(kind=prec), | public | :: | significantEnergyFloor | = | SELF_AMR_DEFAULT_RELFLOOR | ||
| integer, | public, | allocatable | :: | winFirst(:) | |||
| integer, | public, | allocatable | :: | winLast(:) | Cumulative migration accounting, so the communication volume a repartition actually costs is measured rather than estimated. Both migration paths count, so the two are directly comparable in one binary. |
||
| real(kind=prec), | public, | allocatable | :: | xferWin(:) |
| procedure, public :: Adapt => Adapt_AMRController2D | |
| procedure, private :: ApplyIndicatorSettings => ApplyIndicatorSettings_AMRController2D | |
| procedure, private :: BuildGeometry => BuildGeometry_AMRController2D | |
| procedure, public :: Free => Free_AMRController2D | |
| procedure, public :: Init => Init_AMRController2D | |
| procedure, private :: NextGeomBuffer => NextGeomBuffer_AMRController2D | |
| procedure, public :: RecommendedTimeStep => RecommendedTimeStep_AMRController2D |
Level-based explicit-stability time step: refinement halves the element scale per level, so a time step dtBase that is stable on the base (level-0) mesh scales to dtBase / 2**MaxLevel on the current forest. Deterministic and exact for the quadtree (child elements are exact half-scale subdivisions); no geometry reduction is needed.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(AMRController2D), | intent(in) | :: | this | |||
| real(kind=prec), | intent(in) | :: | dtBase |
Perform one adaptation epoch on the model (see the module documentation). On return, adapted reports whether the mesh changed; when it did, the model is already rebound to the new mesh with the solution transferred (conservatively), and the caller should re-evaluate its time step (RecommendedTimeStep) before the next ForwardStep. When the leaf set is unchanged the model is untouched.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(AMRController2D), | intent(inout), | target | :: | this | ||
| class(DGModel2D_t), | intent(inout) | :: | model | |||
| logical, | intent(out) | :: | adapted |
Allgather an integer array with perElem entries per element from the decomposition's contiguous rank-local element ranges into the global element ordering.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| type(DomainDecomposition), | intent(in) | :: | decomp | |||
| integer, | intent(in) | :: | perElem | |||
| integer, | intent(in) | :: | localArr(*) | |||
| integer, | intent(out) | :: | globalArr(*) |
Allgather a real(prec) array with perElem entries per element from the decomposition's contiguous rank-local element ranges into the global element ordering.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| type(DomainDecomposition), | intent(in) | :: | decomp | |||
| integer, | intent(in) | :: | perElem | |||
| real(kind=prec), | intent(in) | :: | localArr(*) | |||
| real(kind=prec), | intent(out) | :: | globalArr(*) |
Push the controller-owned amplitude-gate settings onto the indicator. Called after every indicator Init, because Init is intent(out) and therefore resets them to the indicator's defaults. The setters carry the validation.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(AMRController2D), | intent(inout) | :: | this |
Fill newGeom for the emitted mesh, reusing the previous epoch's geometry for every element that did not change and generating only the rest (AMR Stage 6c). nReused reports how many elements were copied rather than computed.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(AMRController2D), | intent(inout) | :: | this | |||
| type(Mesh2D), | intent(in) | :: | newMesh | |||
| type(TransferPlan2D), | intent(in) | :: | plan | |||
| type(SEMQuad), | intent(inout) | :: | newGeom | |||
| integer, | intent(out) | :: | nReused |
Migrate the pre-regrid solution into this rank's old-element window with point-to-point messages: the Stage-5 v2 replacement for allgathering the whole old field onto every rank.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| type(DomainDecomposition), | intent(in) | :: | decomp | |||
| integer, | intent(in) | :: | Np | nodes per direction (N+1) |
||
| integer, | intent(in) | :: | nvar | |||
| integer, | intent(in) | :: | nLocalOld | elements this rank owned before the epoch |
||
| real(kind=prec), | intent(in) | :: | uLocal(1:Np,1:Np,1:nLocalOld,1:nvar) | |||
| integer, | intent(in) | :: | winFirst(1:decomp%nRanks) | |||
| integer, | intent(in) | :: | winLast(1:decomp%nRanks) | |||
| integer, | intent(in) | :: | wFirst | this rank's window, normalized (wFirst > wLast if empty) |
||
| integer, | intent(in) | :: | wLast | |||
| real(kind=prec), | intent(inout) | :: | uWin(1:Np,1:Np,wFirst:wLast,1:nvar) | intent(inout), not out: the receives write it through MPI rather than through the dummy, and an intent(out) dummy would license a compiler to treat it as undefined on entry. |
||
| integer(kind=int64), | intent(inout) | :: | nBytesRecv | |||
| integer(kind=int64), | intent(inout) | :: | nBytesSent | |||
| integer(kind=int64), | intent(inout) | :: | nElemRemote |
Release the forest, the indicator, and any controller-emitted mesh/geometry. The caller-owned base mesh/geometry are untouched. A model still pointing at a controller-emitted mesh must not be used after this call.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(AMRController2D), | intent(inout) | :: | this |
Build a rank-replicated forest over a decomposed base mesh: allgather the global node coordinates, side table, and material ids (by the decomposition's contiguous element ownership) and initialize the forest from the global tables. Collective over the mesh communicator; runs once, at controller initialization.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| type(QuadTreeMesh2D), | intent(out) | :: | forest | |||
| type(Mesh2D), | intent(in) | :: | mesh |
Attach the controller to an initialized model. The model's current mesh becomes the forest's base mesh (level 0); its geometry interpolant drives the indicator and the solution transfer. Thresholds are the sigma = log10 modal-energy-ratio cut-offs of the refinement indicator (refineThreshold > coarsenThreshold; see SELF_RefinementIndicator_2D). ivar is the driving solution variable (or SELF_AMR_ALLVARS). maxLevel >= 0 caps the refinement depth; nHalo >= 0 sets the refine-flag halo width in elements.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(AMRController2D), | intent(out) | :: | this | |||
| class(DGModel2D_t), | intent(in) | :: | model | |||
| real(kind=prec), | intent(in) | :: | refineThreshold | |||
| real(kind=prec), | intent(in) | :: | coarsenThreshold | |||
| integer, | intent(in) | :: | ivar | |||
| integer, | intent(in) | :: | maxLevel | |||
| integer, | intent(in) | :: | nHalo | |||
| real(kind=prec), | intent(in), | optional | :: | relativeEnergyFloor | ||
| real(kind=prec), | intent(in), | optional | :: | energyWeights(:) | ||
| real(kind=prec), | intent(in), | optional | :: | significantEnergyFloor |
Select the geometry buffer to fill this epoch: whichever of the two is not currently active, so the previous epoch's geometry stays intact and readable (AMR Stage 6c).
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(AMRController2D), | intent(inout) | :: | this | |||
| integer, | intent(in) | :: | nElem | |||
| type(SEMQuad), | intent(out), | pointer | :: | geom | ||
| integer, | intent(out) | :: | slot |
For every rank r, the contiguous window [winFirst(r),winLast(r)] of GLOBAL old element indices that rank r's new element range references through the transfer plan. The plan is rank-replicated, so this is a purely local computation: no communication is needed to learn what a peer wants, which is what makes the point-to-point migration cheap here.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| type(TransferPlan2D), | intent(in) | :: | plan | |||
| integer, | intent(in) | :: | nRanks | |||
| integer, | intent(in) | :: | newOffset(1:nRanks+1) | |||
| integer, | intent(out) | :: | winFirst(1:nRanks) | |||
| integer, | intent(out) | :: | winLast(1:nRanks) |
Read the Stage 6c geometry and the solution-migration debug switches once. Idempotent, so it is safe (and intended) to call from every path that consults a switch rather than relying on one caller having run first.
Generate the geometry the original way and report, per quantity, the largest discrepancy against the incrementally built one. Diagnostic only; gated by SELF_AMR_GEOM_VERIFY.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(AMRController2D), | intent(inout) | :: | this | |||
| type(Mesh2D), | intent(in) | :: | newMesh | |||
| type(SEMQuad), | intent(in) | :: | newGeom |
Cross-check a migrated window against the v1 allgathered global old field, bit for bit, over the whole window. Diagnostic only; gated by SELF_AMR_MIGRATE_VERIFY. Bit-identity is the design guarantee (the same numbers routed differently, then fed to the same operators in the same order), so any difference at all is a routing defect and stops the run.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| type(DomainDecomposition), | intent(in) | :: | decomp | |||
| integer, | intent(in) | :: | Np | |||
| integer, | intent(in) | :: | nvar | |||
| integer, | intent(in) | :: | nOld | |||
| integer, | intent(in) | :: | nLocalOld | |||
| real(kind=prec), | intent(in) | :: | uLocal(1:Np,1:Np,1:nLocalOld,1:nvar) | |||
| integer, | intent(in) | :: | wFirst | |||
| integer, | intent(in) | :: | wLast | |||
| real(kind=prec), | intent(in) | :: | uWin(1:Np,1:Np,wFirst:wLast,1:nvar) |
Apply the transfer plan a second time on the HOST, from the same migrated window the backend just used, and compare. SELF_AMR_TRANSFER_VERIFY=1.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(DGModel2D_t), | intent(inout) | :: | model | |||
| type(TransferPlan2D), | intent(in) | :: | plan | |||
| type(Lagrange), | intent(in) | :: | interp | |||
| integer, | intent(in) | :: | eFirst | |||
| integer, | intent(in) | :: | eLast | |||
| integer, | intent(in) | :: | wFirst | |||
| integer, | intent(in) | :: | wLast | |||
| real(kind=prec), | intent(in) | :: | uWin(1:interp%N+1,1:interp%N+1,wFirst:wLast,1:model%nvar) |