diff --git a/README.md b/README.md index cbccb59c..9bfe36fa 100644 --- a/README.md +++ b/README.md @@ -38,3 +38,83 @@ To get started, you can visit the one and only [wiki page](https://github.com/ST - Everything compiles to your `$FOAM_USER_LIBBIN` with a recent ESI ([openfoam.com](https://openfoam.com)) version. - Our favorite refinement cell selector is the [`coded`](https://github.com/STFS-TUDa/blastAMR/blob/0e70469d718ee53a5ce892e0e24a4f940bfba369/tutorials/poly_freelyPropFlame2D_H2/constant/dynamicMeshDict#L25). Although a wide range of selectors is provided, they are not well tested. So, use the coded refinement whenever possible. + +## Overset meshes + +`adaptiveOversetFvMesh` is `dynamicOversetFvMesh` with AMR and load balancing on +top, so mesh motion, refinement and balancing are all driven from a single +`constant/dynamicMeshDict`: + +``` +dynamicFvMesh adaptiveOversetFvMesh; + +solvers +{} // or a motionSolver list, as for dynamicOversetFvMesh + +adaptiveOversetFvMeshCoeffs +{ + // the usual blastAMR settings +} +``` + +Zone book-keeping across topology changes and redistribution is handled by a +run-time selectable `oversetHandler`, picked up automatically for any mesh with +an `overset` patch. Set `oversetHandler none;` to switch it off, or register +your own type if you use custom `cellCellStencil` code: + +``` +class myHandler : public oversetHandler { ... }; +addToRunTimeSelectionTable(oversetHandler, myHandler, mesh); +``` + +Cells carried by a motion solver must be kept out of adaptation: + +``` + protectZones ( movingZone ); // cell zones excluded from AMR +``` + +OpenFOAM places points introduced by refinement in the reference configuration +by assuming the motion is a pure scaling (`points0MotionSolver::updateMesh`). +That is wrong under rotation, and the mesh tangles as the body turns. Refine the +background around the body instead, which is the useful case anyway. + +See `tutorials/oversetHeatTransfer` (static zones, AMR + load balancing) and +`tutorials/oversetRotatingSquare` (moving body, AMR). + +### Load balancing an overset mesh + +The default `cellCount` load policy weights every cell the same, which +misprices an overset mesh: a hole cell is masked out of the solution and +carries no physics, yet counts as a full cell, so a rank owning much of the +hole region reports as loaded while doing almost no work. `cellCountWithOverset` +prices cells by overset type instead, with user-configurable factors: + +``` + loadPolicy cellCountWithOverset; + + holeWeight 0.01; // masked out, no physics solved + interpolatedWeight 1.5; // extra matrix faces + donor comms + porousWeight 1.0; // no donors found + specialWeight 1.0; // hole acting as a donor +``` + +Only the hole discount is on by default; the rest default to 1, i.e. to plain +`cellCount`. Calculated cells always keep their weight, particle contributions +are never discounted, and a mesh with no overset patch falls back to +`cellCount` untouched. Both overset tutorials use this policy. + +Expect it to report *larger* imbalances than `cellCount`, because it can see +imbalance that cell counting cannot. On `tutorials/oversetRotatingSquare`, +where holes run 18-47% of the mesh, `cellCount` peaks at 2.8% imbalance while +`cellCountWithOverset` peaks at 20.9%. **A threshold tuned for `cellCount` is +therefore far too aggressive**: at `allowableImbalance 0.01` that case +rebalances 176 times instead of 12. Pick a threshold clear of the noise +floor: on that case the overset-aware metric sits at a 1-2.6% mean, so the +tutorial uses 0.05, which gives 24 balances. Raise `allowableImbalance` +whenever you switch policy. + +Known limitations: + +- Refinement driven by the `loadBalancedAMR` function object needs + `adaptiveOversetFvMesh`; the function object on a stock `dynamicOversetFvMesh` + covers load balancing only. diff --git a/src/dynamicFvMesh/Make/files b/src/dynamicFvMesh/Make/files index 69724fd1..e1f85bd4 100644 --- a/src/dynamicFvMesh/Make/files +++ b/src/dynamicFvMesh/Make/files @@ -1,3 +1,4 @@ adaptiveFvMesh/adaptiveFvMesh.C +adaptiveOversetFvMesh/adaptiveOversetFvMesh.C LIB = $(FOAM_USER_LIBBIN)/libamrDynamicFvMesh diff --git a/src/dynamicFvMesh/Make/options b/src/dynamicFvMesh/Make/options index 7147d6c4..d973d85c 100644 --- a/src/dynamicFvMesh/Make/options +++ b/src/dynamicFvMesh/Make/options @@ -3,6 +3,7 @@ EXE_INC = $(DEBUG_FLAGS) --std=c++17 \ -I$(LIB_SRC)/finiteVolume/lnInclude \ -I$(LIB_SRC)/dynamicMesh/lnInclude \ -I$(LIB_SRC)/dynamicFvMesh/lnInclude \ + -I$(LIB_SRC)/overset/lnInclude \ -I$(LIB_SRC)/lagrangian/basic/lnInclude \ -I$(LIB_SRC)/sampling/lnInclude \ -I$(LIB_SRC)/surfMesh/lnInclude \ @@ -19,6 +20,7 @@ LIB_LIBS = \ -lfiniteVolume \ -ldynamicMesh \ -ldynamicFvMesh \ + -loverset \ -llagrangian \ -lsampling \ -lsurfMesh \ diff --git a/src/dynamicFvMesh/adaptiveFvMesh/adaptiveFvMesh.C b/src/dynamicFvMesh/adaptiveFvMesh/adaptiveFvMesh.C index f9a870ef..0222e6cf 100644 --- a/src/dynamicFvMesh/adaptiveFvMesh/adaptiveFvMesh.C +++ b/src/dynamicFvMesh/adaptiveFvMesh/adaptiveFvMesh.C @@ -32,6 +32,7 @@ License #include "addToRunTimeSelectionTable.H" #include "cloudSupport.H" #include "sampledSurfaceWorkaround.H" +#include "dynMeshTools.H" // * * * * * * * * * * * * * * Static Data Members * * * * * * * * * * * * * // @@ -99,12 +100,14 @@ void Foam::adaptiveFvMesh::readDict() void Foam::adaptiveFvMesh::updateMesh(const mapPolyMesh& map) { + // Call parent updateMesh first: flux correction indexes the fluxes + // against the new face count and reads the new mesh geometry, so the + // fields have to be mapped to the new topology before amrCore runs + fvMesh::updateMesh(map); + // Delegate flux correction and refiner update to amrCore amrCore_.updateMesh(map); - // Call parent updateMesh - fvMesh::updateMesh(map); - // Remap clouds AFTER fvMesh::updateMesh completes. // This must happen after fvMesh::updateMesh because cloud.autoMap // triggers mesh_.V() which reconstructs volumes. If done before, @@ -167,11 +170,11 @@ void Foam::adaptiveFvMesh::mapFields(const mapPolyMesh& mpm) // Correct surface fields on introduced internal faces. These get // created out-of-nothing so get an interpolated value. - mapNewInternalFaces(mpm.faceMap()); - mapNewInternalFaces(mpm.faceMap()); - mapNewInternalFaces(mpm.faceMap()); - mapNewInternalFaces(mpm.faceMap()); - mapNewInternalFaces(mpm.faceMap()); + meshTools::mapNewInternalFaces(*this, mpm.faceMap()); + meshTools::mapNewInternalFaces(*this, mpm.faceMap()); + meshTools::mapNewInternalFaces(*this, mpm.faceMap()); + meshTools::mapNewInternalFaces(*this, mpm.faceMap()); + meshTools::mapNewInternalFaces(*this, mpm.faceMap()); } bool Foam::adaptiveFvMesh::firstUpdate() diff --git a/src/dynamicFvMesh/adaptiveFvMesh/adaptiveFvMesh.H b/src/dynamicFvMesh/adaptiveFvMesh/adaptiveFvMesh.H index 46d6c69d..0cf420c2 100644 --- a/src/dynamicFvMesh/adaptiveFvMesh/adaptiveFvMesh.H +++ b/src/dynamicFvMesh/adaptiveFvMesh/adaptiveFvMesh.H @@ -121,12 +121,6 @@ protected: //- Distribute additional information virtual void distribute(const mapDistributePolyMesh& map); - //- Map non-flux surfaceFields for new internal faces - // (from cell splitting) - template - void mapNewInternalFaces(const labelList& faceMap) - {} - bool firstUpdate(); public: diff --git a/src/dynamicFvMesh/adaptiveOversetFvMesh/adaptiveOversetFvMesh.C b/src/dynamicFvMesh/adaptiveOversetFvMesh/adaptiveOversetFvMesh.C new file mode 100644 index 00000000..446d0f88 --- /dev/null +++ b/src/dynamicFvMesh/adaptiveOversetFvMesh/adaptiveOversetFvMesh.C @@ -0,0 +1,206 @@ +/*---------------------------------------------------------------------------*\ + ========= | + \\ / F ield | OpenFOAM: The Open Source CFD Toolbox + \\ / O peration | + \\ / A nd | Copyright (C) 2025 + \\/ M anipulation | +------------------------------------------------------------------------------- +License + This file is a derivative work of OpenFOAM. + + OpenFOAM is free software: you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + OpenFOAM is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + for more details. + + You should have received a copy of the GNU General Public License + along with OpenFOAM. If not, see . + +\*---------------------------------------------------------------------------*/ + +#include "adaptiveOversetFvMesh.H" +#include "addToRunTimeSelectionTable.H" +#include "cloudSupport.H" +#include "sampledSurfaceWorkaround.H" +#include "dynMeshTools.H" + +// * * * * * * * * * * * * * * Static Data Members * * * * * * * * * * * * * // + +namespace Foam +{ + defineTypeNameAndDebug(adaptiveOversetFvMesh, 0); + addToRunTimeSelectionTable + ( + dynamicFvMesh, + adaptiveOversetFvMesh, + IOobject + ); +} + + +// * * * * * * * * * * * * Private Member Functions * * * * * * * * * * * * // + +Foam::IOobject Foam::adaptiveOversetFvMesh::dynamicMeshDictIOobject +( + const IOobject& io +) +{ + // Not registered: dynamicOversetFvMesh reads the same dictionary + return IOobject + ( + "dynamicMeshDict", + io.time().constant(), + (io.name() == polyMesh::defaultRegion ? "" : io.name()), + io.db(), + IOobject::MUST_READ_IF_MODIFIED, + IOobject::NO_WRITE, + false + ); +} + + +// * * * * * * * * * * * * Protected Member Functions * * * * * * * * * * * // + +void Foam::adaptiveOversetFvMesh::readDict() +{ + amrCore_.readDict + ( + dynamicMeshDict().optionalSubDict(typeName + "Coeffs") + ); +} + + +void Foam::adaptiveOversetFvMesh::updateMesh(const mapPolyMesh& mpm) +{ + // zoneID has to be right before anything rebuilds the stencil, and + // correcting the overset boundary conditions inside fvMesh::updateMesh + // does exactly that + if (oversetHandler* handler = amrCore_.oversetHandlerPtr()) + { + handler->updateMesh(mpm); + } + + // Motion solvers, mesh objects (including the overset stencil) and all + // registered fields. Runs before amrCore so that flux correction sees + // fields already resized to the new topology + dynamicOversetFvMesh::updateMesh(mpm); + + // Flux correction and refiner update + amrCore_.updateMesh(mpm); + + // Clouds have to be remapped after fvMesh::updateMesh, see adaptiveFvMesh + cloudSupport::autoMapClouds(*this, mpm); + + expireSampledSurfaces(time(), amrCore_.expireSampledSurfacesOnLB()); +} + + +// * * * * * * * * * * * * * * * * Constructors * * * * * * * * * * * * * * // + +Foam::adaptiveOversetFvMesh::adaptiveOversetFvMesh(const IOobject& io) +: + dynamicOversetFvMesh(io), + dynamicMeshDict_(dynamicMeshDictIOobject(io)), + amrCore_(*this), + currentTimeIndex_(-1) +{ + const dictionary refineDict + ( + dynamicMeshDict().optionalSubDict(typeName + "Coeffs") + ); + + amrCore_.initializeAMR(refineDict); + + if (refineDict.getOrDefault("balance", false)) + { + amrCore_.initializeLB(refineDict); + } +} + + +// * * * * * * * * * * * * * * * Member Functions * * * * * * * * * * * * * // + +void Foam::adaptiveOversetFvMesh::mapFields(const mapPolyMesh& mpm) +{ + dynamicOversetFvMesh::mapFields(mpm); + + // Correct surface fields on introduced internal faces. These get + // created out-of-nothing so get an interpolated value. + meshTools::mapNewInternalFaces(*this, mpm.faceMap()); + meshTools::mapNewInternalFaces(*this, mpm.faceMap()); + meshTools::mapNewInternalFaces(*this, mpm.faceMap()); + meshTools::mapNewInternalFaces(*this, mpm.faceMap()); + meshTools::mapNewInternalFaces(*this, mpm.faceMap()); +} + + +bool Foam::adaptiveOversetFvMesh::refine() +{ + readDict(); + + return amrCore_.refine(); +} + + +bool Foam::adaptiveOversetFvMesh::update() +{ + bool adapted = false; + + if (currentTimeIndex_ < time().timeIndex()) + { + currentTimeIndex_ = time().timeIndex(); + + adapted = + ( + amrCore_.refiner().canRefine(true) + || amrCore_.refiner().canUnrefine(true) + ) + && refine(); + + if (amrCore_.lbInitialized() && Pstream::parRun() && amrCore_.balance()) + { + adapted = true; + } + + reduce(adapted, orOp()); + } + + // Mesh motion runs after adaptation, not before: balancing resets the + // motion state, and only a subsequent movePoints re-establishes the mesh + // flux the solver asks for. This also refreshes the overset addressing + const bool moved = dynamicOversetFvMesh::update(); + + if (adapted && !moved) + { + // Topology changed but nothing moved, so rebuild the extended + // addressing and re-interpolate before the solver runs + oversetFvMeshBase::update(); + } + + return moved || adapted; +} + + +bool Foam::adaptiveOversetFvMesh::writeObject +( + IOstreamOption streamOpt, + const bool valid +) const +{ + // zoneID matching the adapted mesh, so that restarts do not fall back + // to the zoneID of the original mesh + if (amrCore_.oversetHandlerPtr()) + { + amrCore_.oversetHandlerPtr()->writeZoneID(); + } + + return dynamicOversetFvMesh::writeObject(streamOpt, valid); +} + + +// ************************************************************************* // diff --git a/src/dynamicFvMesh/adaptiveOversetFvMesh/adaptiveOversetFvMesh.H b/src/dynamicFvMesh/adaptiveOversetFvMesh/adaptiveOversetFvMesh.H new file mode 100644 index 00000000..42a8a90b --- /dev/null +++ b/src/dynamicFvMesh/adaptiveOversetFvMesh/adaptiveOversetFvMesh.H @@ -0,0 +1,169 @@ +/*---------------------------------------------------------------------------*\ + ========= | + \\ / F ield | OpenFOAM: The Open Source CFD Toolbox + \\ / O peration | + \\ / A nd | Copyright (C) 2025 + \\/ M anipulation | +------------------------------------------------------------------------------- +License + This file is a derivative work of OpenFOAM. + + OpenFOAM is free software: you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + OpenFOAM is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + for more details. + + You should have received a copy of the GNU General Public License + along with OpenFOAM. If not, see . + +Class + Foam::adaptiveOversetFvMesh + +Description + dynamicOversetFvMesh with adaptive mesh refinement and load balancing. + + Inherits the whole overset machinery (extended ldu addressing, implicit + and explicit interpolation, mesh motion through the motion solver list) + from dynamicOversetFvMesh and adds blastAMR's amrCore on top, so a + single dynamicMeshDict drives motion, refinement and balancing: + + \verbatim + dynamicFvMesh adaptiveOversetFvMesh; + + solver multiSolidBodyMotionSolver; + multiSolidBodyMotionSolverCoeffs { ... } + + adaptiveOversetFvMeshCoeffs + { + errorEstimator ...; + balance yes; + ... + } + \endverbatim + + Overset zone book-keeping across refinement and balancing is done by the + oversetHandler registered by amrCore. + +SourceFiles + adaptiveOversetFvMesh.C + +\*---------------------------------------------------------------------------*/ + +#ifndef adaptiveOversetFvMesh_H +#define adaptiveOversetFvMesh_H + +#include "dynamicOversetFvMesh.H" +#include "amrCore.H" + +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // + +namespace Foam +{ + +/*---------------------------------------------------------------------------*\ + Class adaptiveOversetFvMesh Declaration +\*---------------------------------------------------------------------------*/ + +class adaptiveOversetFvMesh +: + public dynamicOversetFvMesh +{ + //- Return the dynamicMeshDict IOobject + static IOobject dynamicMeshDictIOobject(const IOobject& io); + + +protected: + + //- The dynamicMeshDict + IOdictionary dynamicMeshDict_; + + //- Core AMR/LB functionality + amrCore amrCore_; + + //- Do mesh updates only once per timestep + label currentTimeIndex_; + + + // Protected Member Functions + + //- Read the AMR settings + void readDict(); + + //- Overload update mesh to include other methods + virtual void updateMesh(const mapPolyMesh& mpm); + + +public: + + //- Runtime type information + TypeName("adaptiveOversetFvMesh"); + + + // Constructors + + //- Construct from IOobject + explicit adaptiveOversetFvMesh(const IOobject& io); + + //- No copy construct + adaptiveOversetFvMesh(const adaptiveOversetFvMesh&) = delete; + + + //- Destructor + virtual ~adaptiveOversetFvMesh() = default; + + + // Member Functions + + //- Return the dynamicMeshDict + const dictionary& dynamicMeshDict() const + { + return dynamicMeshDict_; + } + + //- Direct access to the refinement engine + const fvMeshRefiner& refiner() const + { + return amrCore_.refiner(); + } + + //- Map all fields in time using given map + virtual void mapFields(const mapPolyMesh& mpm); + + //- Update the mesh for motion, topology change and balancing + virtual bool update(); + + //- Refine the mesh + bool refine(); + + + // Writing + + //- Write using given format, version and compression + virtual bool writeObject + ( + IOstreamOption streamOpt, + const bool valid + ) const; + + + // Member Operators + + //- No copy assignment + void operator=(const adaptiveOversetFvMesh&) = delete; +}; + + +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // + +} // End namespace Foam + +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // + +#endif + +// ************************************************************************* // diff --git a/src/dynamicMesh/Make/files b/src/dynamicMesh/Make/files index 83aa5b3e..1c34c287 100644 --- a/src/dynamicMesh/Make/files +++ b/src/dynamicMesh/Make/files @@ -32,6 +32,8 @@ fvMeshRefiner/fvMeshPolyRefiner/polyRefinementConstraint.C meshTools/dynMeshTools.C +oversetHandler/oversetHandler.C + amrCore/amrCore.C LIB = $(FOAM_USER_LIBBIN)/libamrDynamicMesh diff --git a/src/dynamicMesh/Make/options b/src/dynamicMesh/Make/options index 07849fed..424b55a5 100644 --- a/src/dynamicMesh/Make/options +++ b/src/dynamicMesh/Make/options @@ -3,6 +3,7 @@ EXE_INC = $(DEBUG_FLAGS) --std=c++17 \ -I$(LIB_SRC)/meshTools/lnInclude \ -I$(LIB_SRC)/dynamicMesh/lnInclude \ -I$(LIB_SRC)/dynamicFvMesh/lnInclude \ + -I$(LIB_SRC)/overset/lnInclude \ -I$(LIB_SRC)/parallel/decompose/decompositionMethods/lnInclude \ -I$(LIB_SRC)/lagrangian/basic/lnInclude \ -I$(LIB_SRC)/sampling/lnInclude \ @@ -18,6 +19,7 @@ LIB_LIBS = \ -lfiniteVolume \ -ldynamicMesh \ -ldynamicFvMesh \ + -loverset \ -ldecompositionMethods \ -llagrangian \ -lsampling \ diff --git a/src/dynamicMesh/amrCore/amrCore.C b/src/dynamicMesh/amrCore/amrCore.C index bb63f596..50118d1b 100644 --- a/src/dynamicMesh/amrCore/amrCore.C +++ b/src/dynamicMesh/amrCore/amrCore.C @@ -76,6 +76,9 @@ void Foam::amrCore::initializeAMR(const dictionary& dict) // Create refiner refiner_ = fvMeshRefiner::New(mesh_, dict, false, true); + // Overset meshes need their zone book-keeping mapped along with the mesh + oversetHandler::New(mesh_, dict); + // Read flux correction settings readCorrectFluxes(dict); @@ -122,6 +125,8 @@ void Foam::amrCore::initializeLB(const dictionary& dict) refiner_ = fvMeshRefiner::New(mesh_, minimalDict, false, true); } + oversetHandler::New(mesh_, dict); + // Configure balancer within refiner refiner_->balancer().read(dict); @@ -204,6 +209,14 @@ bool Foam::amrCore::refine() Info<< "Protecting " << returnReduce(nProtected, sumOp