From 9c4f667227a0f46512bbdaa18d3b19455f167462 Mon Sep 17 00:00:00 2001 From: Tomislav Maric Date: Mon, 2 Nov 2026 12:33:15 +0100 Subject: [PATCH] fix: reset topoChanging in steps without a mesh change The refiners set topoChanging only in the steps in which they run (every refineInterval steps). After a step that changed the mesh, the flag therefore stays true until the next refinement step, so solvers see mesh.changing() in steps in which nothing changed. interIsoFoam, interFoam and the other dynamic-mesh solvers then redo their mesh-change work in every such step: gh/ghf, and correctPhi with a pcorr solve on the whole mesh. adaptiveFvMesh::update() now resets the flag before refining, as OpenFOAM's dynamicRefineFvMesh does, and sets it when the mesh changed, which also covers a redistribution in a step without refinement. test: topoChanging() must equal the return value of update() in every step, with refinement and balancing every third step. --- .../adaptiveFvMesh/adaptiveFvMesh.C | 12 +++ .../adaptiveFvMeshTests/adaptiveFvMeshTest.C | 92 +++++++++++++++++++ 2 files changed, 104 insertions(+) diff --git a/src/dynamicFvMesh/adaptiveFvMesh/adaptiveFvMesh.C b/src/dynamicFvMesh/adaptiveFvMesh/adaptiveFvMesh.C index 0222e6c..d908701 100644 --- a/src/dynamicFvMesh/adaptiveFvMesh/adaptiveFvMesh.C +++ b/src/dynamicFvMesh/adaptiveFvMesh/adaptiveFvMesh.C @@ -191,6 +191,12 @@ bool Foam::adaptiveFvMesh::update() { if (!firstUpdate()) return false; + // The refiner sets topoChanging only in the steps in which it runs, so + // reset it here; otherwise it stays true after a mesh change until the + // next refinement step, and solvers redo their mesh-change work (e.g. + // correctPhi) in every step in between + topoChanging(false); + bool changed = (amrCore_.refiner().canRefine(true) || amrCore_.refiner().canUnrefine(true)) && refine(); @@ -207,6 +213,12 @@ bool Foam::adaptiveFvMesh::update() reduce(changed, orOp()); + // A redistribution changes the topology as well + if (changed) + { + topoChanging(true); + } + return changed; } diff --git a/tests/adaptiveFvMeshTests/adaptiveFvMeshTest.C b/tests/adaptiveFvMeshTests/adaptiveFvMeshTest.C index dfa7dd3..46b698a 100644 --- a/tests/adaptiveFvMeshTests/adaptiveFvMeshTest.C +++ b/tests/adaptiveFvMeshTests/adaptiveFvMeshTest.C @@ -257,3 +257,95 @@ TEST_CASE("Check boundary protection for adaptiveFvMesh", // Reset time for good mesure runTime.setTime(0.0, 0); } + +TEST_CASE("Check that topoChanging is reset in steps without a mesh change", + "[hex2D][hex3D][poly2D][poly3D][serial][parallel][!throws]") { + FatalError.throwExceptions(); + Time &runTime = *timePtr; + + // Refinement box + const std::vector boxBounds = {0.02, 0.04, 0.025, 0.035}; + + // Tested variables' matrix + word refiner = GENERATE("polyRefiner", "hexRefiner"); + if (refiner == "hexRefiner" && + runTime.caseName().find("poly") != string::npos) { + REQUIRE(true); + return; + } + word balance = Pstream::parRun() ? GENERATE("no", "yes") : GENERATE("no"); + + // Adapt only every third step, so that steps without adaptation follow a + // mesh change + IStringStream is("dynamicFvMesh adaptiveFvMesh;" + "errorEstimator fieldValue;" + "fieldName test;" + "balance " + + balance + + ";" + "refiner " + + refiner + + ";" + "refineInterval 3;" + "unrefineInterval 3;" + "balanceInterval 3;" + "lowerRefineLevel 0.01;" + "unrefineLevel 0.01;" + "nBufferLayers 1;" + "maxRefinement 1;" + "dumpLevel false;" + "protectedPatches ();" + "nPatchesBuffers 1;"); + + // Reset time + runTime.setTime(0.0, 0); + + // Create mesh object + IOdictionary dynamicMeshDict(IOobject("dynamicMeshDict", runTime.constant(), + runTime, IOobject::NO_READ, + IOobject::NO_WRITE), + is); + dynamicMeshDict.regIOobject::write(); + adaptiveFvMesh mesh(IOobject("", runTime.constant(), runTime, + IOobject::MUST_READ, IOobject::NO_WRITE)); + + CAPTURE(Pstream::myProcNo(), Pstream::nProcs(), runTime.caseName(), + mesh.nCells(), balance, refiner); + + // Create AMR criterion field, 1 inside the refined box + volScalarField test(IOobject("test", runTime.timeName(), mesh, + IOobject::NO_READ, IOobject::NO_WRITE, true), + mesh, dimensionedScalar("zero", dimless, 0.0)); + forAll(mesh.C(), ci) { + const auto ¢er = mesh.C()[ci]; + if (center.x() >= boxBounds[0] && center.x() <= boxBounds[1] && + center.y() >= boxBounds[2] && center.y() <= boxBounds[3]) { + test[ci] = 1; + } + } + + // topoChanging() must report a change exactly in the steps in which the + // mesh changed; solvers use it (through changing()) to decide whether to + // redo their mesh-change work, e.g. correctPhi + bool changedBefore = false; + label nUnchangedAfterChange = 0; + for (label i = 0; i < 7; i++) { + runTime++; + bool changed = false; + tryAndCatchAbortingCode([&]() { changed = mesh.update(); }); + CAPTURE(runTime.timeIndex(), changed); + REQUIRE(mesh.topoChanging() == changed); + if (changedBefore && !changed) { + nUnchangedAfterChange++; + } + changedBefore = changedBefore || changed; + } + + // The first step always refines; the check above is only meaningful if + // steps without a change follow it + REQUIRE(changedBefore); + REQUIRE(nUnchangedAfterChange > 0); + + // Reset time for good mesure + runTime.setTime(0.0, 0); +}