From 6525cbb1c47afd72079a86eeb75fa7c63e02d46a Mon Sep 17 00:00:00 2001 From: Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> Date: Fri, 8 Jul 2022 16:09:02 +0200 Subject: [PATCH] Add free-surface module --- .../ComplexGeometry/ComplexGeometry.cpp | 2 +- apps/showcases/CMakeLists.txt | 2 + .../FreeSurface/BubblyPoiseuille.cpp | 369 ++++++ .../FreeSurface/BubblyPoiseuille.prm | 108 ++ apps/showcases/FreeSurface/CMakeLists.txt | 48 + apps/showcases/FreeSurface/CapillaryWave.cpp | 473 +++++++ apps/showcases/FreeSurface/CapillaryWave.prm | 107 ++ .../FreeSurface/DamBreakCylindrical.cpp | 606 +++++++++ .../FreeSurface/DamBreakCylindrical.prm | 111 ++ .../FreeSurface/DamBreakRectangular.cpp | 570 ++++++++ .../FreeSurface/DamBreakRectangular.prm | 111 ++ apps/showcases/FreeSurface/DropImpact.cpp | 367 ++++++ apps/showcases/FreeSurface/DropImpact.prm | 110 ++ apps/showcases/FreeSurface/DropWetting.cpp | 421 ++++++ apps/showcases/FreeSurface/DropWetting.prm | 108 ++ apps/showcases/FreeSurface/GravityWave.cpp | 579 +++++++++ apps/showcases/FreeSurface/GravityWave.prm | 107 ++ .../FreeSurface/GravityWaveCodegen.cpp | 580 +++++++++ .../GravityWaveLatticeModelGeneration.py | 34 + apps/showcases/FreeSurface/MovingDrop.cpp | 325 +++++ apps/showcases/FreeSurface/MovingDrop.prm | 108 ++ apps/showcases/FreeSurface/RisingBubble.cpp | 465 +++++++ apps/showcases/FreeSurface/RisingBubble.prm | 108 ++ apps/showcases/FreeSurface/TaylorBubble.cpp | 494 +++++++ apps/showcases/FreeSurface/TaylorBubble.prm | 110 ++ src/core/StringUtility.h | 40 +- src/core/StringUtility.impl.h | 115 +- src/core/cell/Cell.h | 16 +- src/core/stringToNum.h | 14 +- src/lbm/CMakeLists.txt | 4 +- .../blockforest/communication/CMakeLists.txt | 5 + .../communication/SimpleCommunication.h | 170 +++ .../communication/UpdateSecondGhostLayer.h | 144 ++ src/lbm/boundary/SimpleExtrapolationOutflow.h | 114 ++ src/lbm/boundary/all.h | 1 + .../free_surface/BlockStateDetectorSweep.h | 111 ++ src/lbm/free_surface/CMakeLists.txt | 19 + src/lbm/free_surface/FlagDefinitions.h | 51 + src/lbm/free_surface/FlagInfo.h | 215 +++ src/lbm/free_surface/FlagInfo.impl.h | 199 +++ src/lbm/free_surface/InitFunctions.h | 229 ++++ src/lbm/free_surface/InterfaceFromFillLevel.h | 78 ++ src/lbm/free_surface/LoadBalancing.h | 345 +++++ src/lbm/free_surface/MaxVelocityComputer.h | 110 ++ src/lbm/free_surface/SurfaceMeshWriter.h | 176 +++ src/lbm/free_surface/TotalMassComputer.h | 144 ++ src/lbm/free_surface/VtkWriter.h | 173 +++ src/lbm/free_surface/boundary/CMakeLists.txt | 6 + .../boundary/FreeSurfaceBoundaryHandling.h | 188 +++ .../FreeSurfaceBoundaryHandling.impl.h | 554 ++++++++ .../boundary/SimplePressureWithFreeSurface.h | 150 +++ src/lbm/free_surface/bubble_model/Bubble.h | 171 +++ .../bubble_model/BubbleDefinitions.h | 42 + .../bubble_model/BubbleDistanceAdaptor.h | 76 ++ .../bubble_model/BubbleIDFieldPackInfo.h | 147 +++ .../free_surface/bubble_model/BubbleModel.h | 240 ++++ .../bubble_model/BubbleModel.impl.h | 692 ++++++++++ .../bubble_model/BubbleModelFromConfig.h | 75 ++ .../bubble_model/BubbleModelFromConfig.impl.h | 91 ++ .../free_surface/bubble_model/CMakeLists.txt | 24 + .../DisjoiningPressureBubbleModel.h | 142 ++ .../DisjoiningPressureBubbleModel.impl.h | 83 ++ .../bubble_model/DistanceInfo.cpp | 112 ++ .../free_surface/bubble_model/DistanceInfo.h | 100 ++ src/lbm/free_surface/bubble_model/FloodFill.h | 104 ++ .../bubble_model/FloodFill.impl.h | 216 +++ src/lbm/free_surface/bubble_model/Geometry.h | 87 ++ .../free_surface/bubble_model/Geometry.impl.h | 219 ++++ .../bubble_model/MergeInformation.cpp | 337 +++++ .../bubble_model/MergeInformation.h | 102 ++ .../bubble_model/NewBubbleCommunication.cpp | 232 ++++ .../bubble_model/NewBubbleCommunication.h | 126 ++ .../bubble_model/RegionalFloodFill.h | 254 ++++ src/lbm/free_surface/dynamics/CMakeLists.txt | 17 + .../dynamics/CellConversionSweep.h | 315 +++++ .../dynamics/ConversionFlagsResetSweep.h | 70 + .../dynamics/ExcessMassDistributionModel.h | 214 +++ .../dynamics/ExcessMassDistributionSweep.h | 212 +++ .../ExcessMassDistributionSweep.impl.h | 594 +++++++++ .../dynamics/ForceWeightingSweep.h | 96 ++ .../dynamics/PdfReconstructionModel.h | 173 +++ .../free_surface/dynamics/PdfRefillingModel.h | 147 +++ .../free_surface/dynamics/PdfRefillingSweep.h | 447 +++++++ .../dynamics/PdfRefillingSweep.impl.h | 718 ++++++++++ .../dynamics/StreamReconstructAdvectSweep.h | 202 +++ .../dynamics/SurfaceDynamicsHandler.h | 441 +++++++ .../dynamics/functionality/AdvectMass.h | 305 +++++ .../dynamics/functionality/CMakeLists.txt | 8 + .../FindInterfaceCellConversion.h | 255 ++++ .../functionality/GetLaplacePressure.h | 61 + .../functionality/GetOredNeighborhood.h | 62 + .../ReconstructInterfaceCellABB.h | 442 +++++++ .../surface_geometry/CMakeLists.txt | 22 + .../surface_geometry/ContactAngle.h | 54 + .../surface_geometry/CurvatureModel.h | 90 ++ .../surface_geometry/CurvatureModel.impl.h | 269 ++++ .../surface_geometry/CurvatureSweep.h | 211 +++ .../surface_geometry/CurvatureSweep.impl.h | 518 ++++++++ .../surface_geometry/DetectWettingSweep.h | 334 +++++ .../ExtrapolateNormalsSweep.h | 71 + .../ExtrapolateNormalsSweep.impl.h | 70 + .../surface_geometry/NormalSweep.h | 109 ++ .../surface_geometry/NormalSweep.impl.h | 456 +++++++ .../surface_geometry/ObstacleFillLevelSweep.h | 91 ++ .../ObstacleFillLevelSweep.impl.h | 88 ++ .../surface_geometry/ObstacleNormalSweep.h | 98 ++ .../ObstacleNormalSweep.impl.h | 147 +++ .../surface_geometry/SmoothingSweep.h | 113 ++ .../surface_geometry/SmoothingSweep.impl.h | 159 +++ .../surface_geometry/SurfaceGeometryHandler.h | 168 +++ .../free_surface/surface_geometry/Utility.cpp | 547 ++++++++ .../free_surface/surface_geometry/Utility.h | 68 + tests/lbm/CMakeLists.txt | 144 +- tests/lbm/free_surface/LoadBalancingTest.cpp | 192 +++ .../bubble_model/BubbleBodyMover.h | 71 + .../bubble_model/BubbleBodyMover.impl.h | 99 ++ .../bubble_model/BubbleInitializationTest.cpp | 154 +++ .../bubble_model/BubbleModelTester.h | 96 ++ .../bubble_model/BubbleModelTester.impl.h | 160 +++ .../bubble_model/MergeAndSplitTest.cpp | 230 ++++ .../MergeAndSplitTestConnected.png | Bin 0 -> 1681 bytes .../MergeAndSplitTestUnconnected.png | Bin 0 -> 1649 bytes .../bubble_model/MergeInformationTest.cpp | 231 ++++ .../bubble_model/MovingSpheresTest.cpp | 173 +++ .../bubble_model/RegionalFloodFillTest.cpp | 88 ++ .../bubble_model/SplitDetectionTest.cpp | 152 +++ .../free_surface/dynamics/AdvectionTest.cpp | 220 ++++ .../dynamics/CellConversionTest.cpp | 280 ++++ .../lbm/free_surface/dynamics/CodegenTest.cpp | 227 ++++ .../ExcessMassDistributionFallbackTest.cpp | 360 +++++ .../ExcessMassDistributionParallelTest.cpp | 1154 +++++++++++++++++ .../ExcessMassDistributionParallelTest.ods | Bin 0 -> 16650 bytes .../lbm/free_surface/dynamics/InflowTest.cpp | 281 ++++ .../LatticeModelGenerationFreeSurface.py | 34 + .../PdfReconstructionFreeSlipTest.cpp | 201 +++ .../dynamics/PdfReconstructionTest.cpp | 606 +++++++++ .../dynamics/PdfRefillingTest.cpp | 1123 ++++++++++++++++ .../dynamics/PdfRefillingTest.ods | Bin 0 -> 37960 bytes .../dynamics/WettingConversionTest.cpp | 247 ++++ .../surface_geometry/CellFluidVolumeTest.cpp | 74 ++ .../surface_geometry/CurvatureOfSineTest.cpp | 545 ++++++++ .../CurvatureOfSphereTest.cpp | 373 ++++++ .../surface_geometry/DetectWettingTest.cpp | 294 +++++ .../GetInterfacePointTest.cpp | 76 ++ .../NormalsEquivalenceTest.cpp | 213 +++ .../surface_geometry/NormalsNearSolidTest.cpp | 178 +++ .../surface_geometry/NormalsOfSineTest.cpp | 470 +++++++ .../surface_geometry/NormalsOfSphereTest.cpp | 363 ++++++ .../ObstacleFillLevelTest.cpp | 283 ++++ .../surface_geometry/ObstacleNormalsTest.cpp | 186 +++ .../surface_geometry/WettingCurvatureTest.cpp | 341 +++++ 151 files changed, 31781 insertions(+), 66 deletions(-) create mode 100644 apps/showcases/FreeSurface/BubblyPoiseuille.cpp create mode 100644 apps/showcases/FreeSurface/BubblyPoiseuille.prm create mode 100644 apps/showcases/FreeSurface/CMakeLists.txt create mode 100644 apps/showcases/FreeSurface/CapillaryWave.cpp create mode 100644 apps/showcases/FreeSurface/CapillaryWave.prm create mode 100644 apps/showcases/FreeSurface/DamBreakCylindrical.cpp create mode 100644 apps/showcases/FreeSurface/DamBreakCylindrical.prm create mode 100644 apps/showcases/FreeSurface/DamBreakRectangular.cpp create mode 100644 apps/showcases/FreeSurface/DamBreakRectangular.prm create mode 100644 apps/showcases/FreeSurface/DropImpact.cpp create mode 100644 apps/showcases/FreeSurface/DropImpact.prm create mode 100644 apps/showcases/FreeSurface/DropWetting.cpp create mode 100644 apps/showcases/FreeSurface/DropWetting.prm create mode 100644 apps/showcases/FreeSurface/GravityWave.cpp create mode 100644 apps/showcases/FreeSurface/GravityWave.prm create mode 100644 apps/showcases/FreeSurface/GravityWaveCodegen.cpp create mode 100644 apps/showcases/FreeSurface/GravityWaveLatticeModelGeneration.py create mode 100644 apps/showcases/FreeSurface/MovingDrop.cpp create mode 100644 apps/showcases/FreeSurface/MovingDrop.prm create mode 100644 apps/showcases/FreeSurface/RisingBubble.cpp create mode 100644 apps/showcases/FreeSurface/RisingBubble.prm create mode 100644 apps/showcases/FreeSurface/TaylorBubble.cpp create mode 100644 apps/showcases/FreeSurface/TaylorBubble.prm create mode 100644 src/lbm/blockforest/communication/CMakeLists.txt create mode 100644 src/lbm/blockforest/communication/SimpleCommunication.h create mode 100644 src/lbm/blockforest/communication/UpdateSecondGhostLayer.h create mode 100644 src/lbm/boundary/SimpleExtrapolationOutflow.h create mode 100644 src/lbm/free_surface/BlockStateDetectorSweep.h create mode 100644 src/lbm/free_surface/CMakeLists.txt create mode 100644 src/lbm/free_surface/FlagDefinitions.h create mode 100644 src/lbm/free_surface/FlagInfo.h create mode 100644 src/lbm/free_surface/FlagInfo.impl.h create mode 100644 src/lbm/free_surface/InitFunctions.h create mode 100644 src/lbm/free_surface/InterfaceFromFillLevel.h create mode 100644 src/lbm/free_surface/LoadBalancing.h create mode 100644 src/lbm/free_surface/MaxVelocityComputer.h create mode 100644 src/lbm/free_surface/SurfaceMeshWriter.h create mode 100644 src/lbm/free_surface/TotalMassComputer.h create mode 100644 src/lbm/free_surface/VtkWriter.h create mode 100644 src/lbm/free_surface/boundary/CMakeLists.txt create mode 100644 src/lbm/free_surface/boundary/FreeSurfaceBoundaryHandling.h create mode 100644 src/lbm/free_surface/boundary/FreeSurfaceBoundaryHandling.impl.h create mode 100644 src/lbm/free_surface/boundary/SimplePressureWithFreeSurface.h create mode 100644 src/lbm/free_surface/bubble_model/Bubble.h create mode 100644 src/lbm/free_surface/bubble_model/BubbleDefinitions.h create mode 100644 src/lbm/free_surface/bubble_model/BubbleDistanceAdaptor.h create mode 100644 src/lbm/free_surface/bubble_model/BubbleIDFieldPackInfo.h create mode 100644 src/lbm/free_surface/bubble_model/BubbleModel.h create mode 100644 src/lbm/free_surface/bubble_model/BubbleModel.impl.h create mode 100644 src/lbm/free_surface/bubble_model/BubbleModelFromConfig.h create mode 100644 src/lbm/free_surface/bubble_model/BubbleModelFromConfig.impl.h create mode 100644 src/lbm/free_surface/bubble_model/CMakeLists.txt create mode 100644 src/lbm/free_surface/bubble_model/DisjoiningPressureBubbleModel.h create mode 100644 src/lbm/free_surface/bubble_model/DisjoiningPressureBubbleModel.impl.h create mode 100644 src/lbm/free_surface/bubble_model/DistanceInfo.cpp create mode 100644 src/lbm/free_surface/bubble_model/DistanceInfo.h create mode 100644 src/lbm/free_surface/bubble_model/FloodFill.h create mode 100644 src/lbm/free_surface/bubble_model/FloodFill.impl.h create mode 100644 src/lbm/free_surface/bubble_model/Geometry.h create mode 100644 src/lbm/free_surface/bubble_model/Geometry.impl.h create mode 100644 src/lbm/free_surface/bubble_model/MergeInformation.cpp create mode 100644 src/lbm/free_surface/bubble_model/MergeInformation.h create mode 100644 src/lbm/free_surface/bubble_model/NewBubbleCommunication.cpp create mode 100644 src/lbm/free_surface/bubble_model/NewBubbleCommunication.h create mode 100644 src/lbm/free_surface/bubble_model/RegionalFloodFill.h create mode 100644 src/lbm/free_surface/dynamics/CMakeLists.txt create mode 100644 src/lbm/free_surface/dynamics/CellConversionSweep.h create mode 100644 src/lbm/free_surface/dynamics/ConversionFlagsResetSweep.h create mode 100644 src/lbm/free_surface/dynamics/ExcessMassDistributionModel.h create mode 100644 src/lbm/free_surface/dynamics/ExcessMassDistributionSweep.h create mode 100644 src/lbm/free_surface/dynamics/ExcessMassDistributionSweep.impl.h create mode 100644 src/lbm/free_surface/dynamics/ForceWeightingSweep.h create mode 100644 src/lbm/free_surface/dynamics/PdfReconstructionModel.h create mode 100644 src/lbm/free_surface/dynamics/PdfRefillingModel.h create mode 100644 src/lbm/free_surface/dynamics/PdfRefillingSweep.h create mode 100644 src/lbm/free_surface/dynamics/PdfRefillingSweep.impl.h create mode 100644 src/lbm/free_surface/dynamics/StreamReconstructAdvectSweep.h create mode 100644 src/lbm/free_surface/dynamics/SurfaceDynamicsHandler.h create mode 100644 src/lbm/free_surface/dynamics/functionality/AdvectMass.h create mode 100644 src/lbm/free_surface/dynamics/functionality/CMakeLists.txt create mode 100644 src/lbm/free_surface/dynamics/functionality/FindInterfaceCellConversion.h create mode 100644 src/lbm/free_surface/dynamics/functionality/GetLaplacePressure.h create mode 100644 src/lbm/free_surface/dynamics/functionality/GetOredNeighborhood.h create mode 100644 src/lbm/free_surface/dynamics/functionality/ReconstructInterfaceCellABB.h create mode 100644 src/lbm/free_surface/surface_geometry/CMakeLists.txt create mode 100644 src/lbm/free_surface/surface_geometry/ContactAngle.h create mode 100644 src/lbm/free_surface/surface_geometry/CurvatureModel.h create mode 100644 src/lbm/free_surface/surface_geometry/CurvatureModel.impl.h create mode 100644 src/lbm/free_surface/surface_geometry/CurvatureSweep.h create mode 100644 src/lbm/free_surface/surface_geometry/CurvatureSweep.impl.h create mode 100644 src/lbm/free_surface/surface_geometry/DetectWettingSweep.h create mode 100644 src/lbm/free_surface/surface_geometry/ExtrapolateNormalsSweep.h create mode 100644 src/lbm/free_surface/surface_geometry/ExtrapolateNormalsSweep.impl.h create mode 100644 src/lbm/free_surface/surface_geometry/NormalSweep.h create mode 100644 src/lbm/free_surface/surface_geometry/NormalSweep.impl.h create mode 100644 src/lbm/free_surface/surface_geometry/ObstacleFillLevelSweep.h create mode 100644 src/lbm/free_surface/surface_geometry/ObstacleFillLevelSweep.impl.h create mode 100644 src/lbm/free_surface/surface_geometry/ObstacleNormalSweep.h create mode 100644 src/lbm/free_surface/surface_geometry/ObstacleNormalSweep.impl.h create mode 100644 src/lbm/free_surface/surface_geometry/SmoothingSweep.h create mode 100644 src/lbm/free_surface/surface_geometry/SmoothingSweep.impl.h create mode 100644 src/lbm/free_surface/surface_geometry/SurfaceGeometryHandler.h create mode 100644 src/lbm/free_surface/surface_geometry/Utility.cpp create mode 100644 src/lbm/free_surface/surface_geometry/Utility.h create mode 100644 tests/lbm/free_surface/LoadBalancingTest.cpp create mode 100644 tests/lbm/free_surface/bubble_model/BubbleBodyMover.h create mode 100644 tests/lbm/free_surface/bubble_model/BubbleBodyMover.impl.h create mode 100644 tests/lbm/free_surface/bubble_model/BubbleInitializationTest.cpp create mode 100644 tests/lbm/free_surface/bubble_model/BubbleModelTester.h create mode 100644 tests/lbm/free_surface/bubble_model/BubbleModelTester.impl.h create mode 100644 tests/lbm/free_surface/bubble_model/MergeAndSplitTest.cpp create mode 100644 tests/lbm/free_surface/bubble_model/MergeAndSplitTestConnected.png create mode 100644 tests/lbm/free_surface/bubble_model/MergeAndSplitTestUnconnected.png create mode 100644 tests/lbm/free_surface/bubble_model/MergeInformationTest.cpp create mode 100644 tests/lbm/free_surface/bubble_model/MovingSpheresTest.cpp create mode 100644 tests/lbm/free_surface/bubble_model/RegionalFloodFillTest.cpp create mode 100644 tests/lbm/free_surface/bubble_model/SplitDetectionTest.cpp create mode 100644 tests/lbm/free_surface/dynamics/AdvectionTest.cpp create mode 100644 tests/lbm/free_surface/dynamics/CellConversionTest.cpp create mode 100644 tests/lbm/free_surface/dynamics/CodegenTest.cpp create mode 100644 tests/lbm/free_surface/dynamics/ExcessMassDistributionFallbackTest.cpp create mode 100644 tests/lbm/free_surface/dynamics/ExcessMassDistributionParallelTest.cpp create mode 100644 tests/lbm/free_surface/dynamics/ExcessMassDistributionParallelTest.ods create mode 100644 tests/lbm/free_surface/dynamics/InflowTest.cpp create mode 100644 tests/lbm/free_surface/dynamics/LatticeModelGenerationFreeSurface.py create mode 100644 tests/lbm/free_surface/dynamics/PdfReconstructionFreeSlipTest.cpp create mode 100644 tests/lbm/free_surface/dynamics/PdfReconstructionTest.cpp create mode 100644 tests/lbm/free_surface/dynamics/PdfRefillingTest.cpp create mode 100644 tests/lbm/free_surface/dynamics/PdfRefillingTest.ods create mode 100644 tests/lbm/free_surface/dynamics/WettingConversionTest.cpp create mode 100644 tests/lbm/free_surface/surface_geometry/CellFluidVolumeTest.cpp create mode 100644 tests/lbm/free_surface/surface_geometry/CurvatureOfSineTest.cpp create mode 100644 tests/lbm/free_surface/surface_geometry/CurvatureOfSphereTest.cpp create mode 100644 tests/lbm/free_surface/surface_geometry/DetectWettingTest.cpp create mode 100644 tests/lbm/free_surface/surface_geometry/GetInterfacePointTest.cpp create mode 100644 tests/lbm/free_surface/surface_geometry/NormalsEquivalenceTest.cpp create mode 100644 tests/lbm/free_surface/surface_geometry/NormalsNearSolidTest.cpp create mode 100644 tests/lbm/free_surface/surface_geometry/NormalsOfSineTest.cpp create mode 100644 tests/lbm/free_surface/surface_geometry/NormalsOfSphereTest.cpp create mode 100644 tests/lbm/free_surface/surface_geometry/ObstacleFillLevelTest.cpp create mode 100644 tests/lbm/free_surface/surface_geometry/ObstacleNormalsTest.cpp create mode 100644 tests/lbm/free_surface/surface_geometry/WettingCurvatureTest.cpp diff --git a/apps/benchmarks/ComplexGeometry/ComplexGeometry.cpp b/apps/benchmarks/ComplexGeometry/ComplexGeometry.cpp index a9cca407c..0f4366447 100644 --- a/apps/benchmarks/ComplexGeometry/ComplexGeometry.cpp +++ b/apps/benchmarks/ComplexGeometry/ComplexGeometry.cpp @@ -239,7 +239,7 @@ int main( int argc, char * argv[] ) flagFieldId, fluidFlagUID ) ), "LBM stability check" ); - timeloop.addFuncAfterTimeStep( perfLogger, "PerformanceLogger" ); + timeloop.addFuncAfterTimeStep( perfLogger, "Evaluator: performance logging" ); // add VTK output to time loop diff --git a/apps/showcases/CMakeLists.txt b/apps/showcases/CMakeLists.txt index 6330a83bd..c6f0534cc 100644 --- a/apps/showcases/CMakeLists.txt +++ b/apps/showcases/CMakeLists.txt @@ -1,6 +1,7 @@ add_subdirectory( BidisperseFluidizedBed ) add_subdirectory( CombinedResolvedUnresolved ) add_subdirectory( FluidizedBed ) +add_subdirectory( FreeSurface ) add_subdirectory( HeatConduction ) add_subdirectory( LightRisingParticleInFluidAMR ) add_subdirectory( Mixer ) @@ -12,3 +13,4 @@ endif() if ( WALBERLA_BUILD_WITH_CODEGEN AND NOT WALBERLA_BUILD_WITH_OPENMP) add_subdirectory( PorousMedia ) endif() + diff --git a/apps/showcases/FreeSurface/BubblyPoiseuille.cpp b/apps/showcases/FreeSurface/BubblyPoiseuille.cpp new file mode 100644 index 000000000..00ae3e9f4 --- /dev/null +++ b/apps/showcases/FreeSurface/BubblyPoiseuille.cpp @@ -0,0 +1,369 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file BubblyPoiseuille.cpp +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +// +// This showcase simulates a plane Poiseuille flow with randomly distributed bubbles in the flow. +//====================================================================================================================== + +#include "blockforest/Initialization.h" + +#include "core/Environment.h" +#include "core/math/Random.h" + +#include "lbm/PerformanceLogger.h" +#include "lbm/field/AddToStorage.h" +#include "lbm/free_surface/LoadBalancing.h" +#include "lbm/free_surface/SurfaceMeshWriter.h" +#include "lbm/free_surface/VtkWriter.h" +#include "lbm/free_surface/bubble_model/Geometry.h" +#include "lbm/free_surface/dynamics/SurfaceDynamicsHandler.h" +#include "lbm/free_surface/surface_geometry/SurfaceGeometryHandler.h" +#include "lbm/free_surface/surface_geometry/Utility.h" +#include "lbm/lattice_model/D3Q19.h" + +namespace walberla +{ +namespace free_surface +{ +namespace DropInPool +{ +using ScalarField_T = GhostLayerField< real_t, 1 >; +using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >; + +using CollisionModel_T = lbm::collision_model::SRT; +using ForceModel_T = lbm::force_model::GuoField< VectorField_T >; +using LatticeModel_T = lbm::D3Q19< CollisionModel_T, true, ForceModel_T, 2 >; +using LatticeModelStencil_T = LatticeModel_T::Stencil; +using PdfField_T = lbm::PdfField< LatticeModel_T >; +using PdfCommunication_T = blockforest::SimpleCommunication< LatticeModelStencil_T >; + +// the geometry computations in SurfaceGeometryHandler require meaningful values in the ghost layers in corner +// directions (flag field and fill level field); this holds, even if the lattice model uses a D3Q19 stencil +using CommunicationStencil_T = + typename std::conditional< LatticeModel_T::Stencil::D == uint_t(2), stencil::D2Q9, stencil::D3Q27 >::type; +using Communication_T = blockforest::SimpleCommunication< CommunicationStencil_T >; + +using flag_t = uint32_t; +using FlagField_T = FlagField< flag_t >; +using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >; + +int main(int argc, char** argv) +{ + Environment walberlaEnv(argc, argv); + + if (argc < 2) { WALBERLA_ABORT("Please specify a parameter file as input argument.") } + + // print content of parameter file + WALBERLA_LOG_INFO_ON_ROOT(*walberlaEnv.config()); + + // get block forest parameters from parameter file + auto blockForestParameters = walberlaEnv.config()->getOneBlock("BlockForestParameters"); + const Vector3< uint_t > cellsPerBlock = blockForestParameters.getParameter< Vector3< uint_t > >("cellsPerBlock"); + const Vector3< bool > periodicity = blockForestParameters.getParameter< Vector3< bool > >("periodicity"); + const uint_t loadBalancingFrequency = blockForestParameters.getParameter< uint_t >("loadBalancingFrequency"); + const bool printLoadBalancingStatistics = blockForestParameters.getParameter< bool >("printLoadBalancingStatistics"); + + // read domain parameters from parameter file + const auto domainParameters = walberlaEnv.config()->getOneBlock("DomainParameters"); + const real_t channelWidth = domainParameters.getParameter< real_t >("channelWidth"); + const Vector3< real_t > domainSizeFactor = domainParameters.getParameter< Vector3< real_t > >("domainSizeFactor"); + const real_t bubbleDiameter = domainParameters.getParameter< real_t >("bubbleDiameterFactor") * channelWidth; + const real_t gasVolumeFraction = domainParameters.getParameter< real_t >("gasVolumeFraction"); + + // define domain size + Vector3< uint_t > domainSize = domainSizeFactor * channelWidth; + domainSize[0] = uint_c(domainSizeFactor[0] * channelWidth); + domainSize[1] = uint_c(domainSizeFactor[1] * channelWidth); + domainSize[2] = uint_c(domainSizeFactor[2] * channelWidth); + + // compute number of blocks as defined by domainSize and cellsPerBlock + Vector3< uint_t > numBlocks; + numBlocks[0] = uint_c(std::ceil(real_c(domainSize[0]) / real_c(cellsPerBlock[0]))); + numBlocks[1] = uint_c(std::ceil(real_c(domainSize[1]) / real_c(cellsPerBlock[1]))); + numBlocks[2] = uint_c(std::ceil(real_c(domainSize[2]) / real_c(cellsPerBlock[2]))); + + // get number of (MPI) processes + uint_t numProcesses = uint_c(MPIManager::instance()->numProcesses()); + WALBERLA_CHECK_LESS_EQUAL(numProcesses, numBlocks[0] * numBlocks[1] * numBlocks[2], + "The number of MPI processes is greater than the number of blocks as defined by " + "\"domainSize/cellsPerBlock\". This would result in unused MPI processes. Either decrease " + "the number of MPI processes or increase \"cellsPerBlock\".") + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(numProcesses); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellsPerBlock); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(numBlocks); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(channelWidth); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(domainSizeFactor); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(periodicity); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(domainSize); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(gasVolumeFraction); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(bubbleDiameter); + + Vector3< uint_t > realDomainSize; + realDomainSize[0] = cellsPerBlock[0] * numBlocks[0]; + realDomainSize[1] = cellsPerBlock[1] * numBlocks[1]; + realDomainSize[2] = cellsPerBlock[2] * numBlocks[2]; + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(realDomainSize); + + if (domainSize[0] != realDomainSize[0] && periodicity[0]) + { + WALBERLA_ABORT( + "The specified domain size in x-direction can not be obtained with the number of blocks you specified.") + } + if (domainSize[1] != realDomainSize[1] && periodicity[1]) + { + WALBERLA_ABORT( + "The specified domain size in y-direction can not be obtained with the number of blocks you specified.") + } + if (domainSize[2] != realDomainSize[2] && periodicity[2]) + { + WALBERLA_ABORT( + "The specified domain size in z-direction can not be obtained with the number of blocks you specified.") + } + + // read physics parameters from parameter file + const auto physicsParameters = walberlaEnv.config()->getOneBlock("PhysicsParameters"); + const real_t reynoldsNumber = physicsParameters.getParameter< real_t >("reynoldsNumber"); + const real_t mortonNumber = physicsParameters.getParameter< real_t >("mortonNumber"); + const real_t relaxationRate = physicsParameters.getParameter< real_t >("relaxationRate"); + + const CollisionModel_T collisionModel = CollisionModel_T(relaxationRate); + const real_t viscosity = collisionModel.viscosity(); + + const real_t forceX = real_c(8) * viscosity * viscosity * reynoldsNumber / real_c(std::pow(channelWidth, 3)); + const Vector3< real_t > force = Vector3< real_t >(forceX, real_c(0), real_c(0)); + + const real_t analMaxVelocity = viscosity * reynoldsNumber / channelWidth; + + const real_t surfaceTension = real_c(std::pow(forceX * real_c(std::pow(viscosity, 4)) / mortonNumber, + real_c(1) / real_c(3))); // formula only valid for rho=1 + + const bool enableWetting = physicsParameters.getParameter< bool >("enableWetting"); + const real_t contactAngle = physicsParameters.getParameter< real_t >("contactAngle"); + + const uint_t timesteps = physicsParameters.getParameter< uint_t >("timesteps"); + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(reynoldsNumber); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(mortonNumber); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(relaxationRate); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(surfaceTension); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(force); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableWetting); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(contactAngle); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(timesteps); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(viscosity); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(analMaxVelocity); + + // read model parameters from parameter file + const auto modelParameters = walberlaEnv.config()->getOneBlock("ModelParameters"); + const std::string pdfReconstructionModel = modelParameters.getParameter< std::string >("pdfReconstructionModel"); + const std::string pdfRefillingModel = modelParameters.getParameter< std::string >("pdfRefillingModel"); + const std::string excessMassDistributionModel = + modelParameters.getParameter< std::string >("excessMassDistributionModel"); + const std::string curvatureModel = modelParameters.getParameter< std::string >("curvatureModel"); + const bool enableForceWeighting = modelParameters.getParameter< bool >("enableForceWeighting"); + const bool useSimpleMassExchange = modelParameters.getParameter< bool >("useSimpleMassExchange"); + const bool enableBubbleModel = modelParameters.getParameter< bool >("enableBubbleModel"); + const bool enableBubbleSplits = modelParameters.getParameter< bool >("enableBubbleSplits"); + const real_t cellConversionThreshold = modelParameters.getParameter< real_t >("cellConversionThreshold"); + const real_t cellConversionForceThreshold = modelParameters.getParameter< real_t >("cellConversionForceThreshold"); + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(pdfReconstructionModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(pdfRefillingModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(excessMassDistributionModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(curvatureModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableForceWeighting); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(useSimpleMassExchange); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableBubbleModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableBubbleSplits); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellConversionThreshold); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellConversionForceThreshold); + + // read evaluation parameters from parameter file + const auto evaluationParameters = walberlaEnv.config()->getOneBlock("EvaluationParameters"); + const uint_t performanceLogFrequency = evaluationParameters.getParameter< uint_t >("performanceLogFrequency"); + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(performanceLogFrequency); + + // create non-uniform block forest (non-uniformity required for load balancing) + const std::shared_ptr< StructuredBlockForest > blockForest = + createNonUniformBlockForest(domainSize, cellsPerBlock, numBlocks, periodicity); + + // add force field + const BlockDataID forceFieldID = + field::addToStorage< VectorField_T >(blockForest, "Force field", force, field::fzyx, uint_c(1)); + + // create lattice model + const LatticeModel_T latticeModel = LatticeModel_T(collisionModel, ForceModel_T(forceFieldID)); + + // add pdf field + const BlockDataID pdfFieldID = lbm::addPdfFieldToStorage(blockForest, "PDF field", latticeModel, + Vector3< real_t >(real_c(0)), real_c(1), field::fzyx); + + // add fill level field (initialized with 0, i.e., gas everywhere) + const BlockDataID fillFieldID = + field::addToStorage< ScalarField_T >(blockForest, "Fill level field", real_c(1.0), field::fzyx, uint_c(2)); + + // add boundary handling + const std::shared_ptr< FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling = + std::make_shared< FreeSurfaceBoundaryHandling_T >(blockForest, pdfFieldID, fillFieldID); + const BlockDataID flagFieldID = freeSurfaceBoundaryHandling->getFlagFieldID(); + const typename FreeSurfaceBoundaryHandling_T::FlagInfo_T& flagInfo = freeSurfaceBoundaryHandling->getFlagInfo(); + + // initialize parabolic Poiseuille profile + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + PdfField_T* const pdfField = blockIt->getData< PdfField_T >(pdfFieldID); + + WALBERLA_FOR_ALL_CELLS(pdfFieldIt, pdfField, { + // get global coordinate (with respect to whole simulation domain) of the currently processed cell + Vector3< real_t > globalCellCoordinate = blockForest->getBlockLocalCellCenter(*blockIt, pdfFieldIt.cell()); + + const real_t height = real_c(realDomainSize[2]); + + const real_t velocityX = + forceX * real_c(0.5) / viscosity * globalCellCoordinate[2] * (height - globalCellCoordinate[2]); + + pdfField->setDensityAndVelocity(pdfFieldIt.cell(), Vector3< real_t >(velocityX, real_c(0), real_c(0)), + real_c(1)); + }); // WALBERLA_FOR_ALL_CELLS + } + + const real_t bubbleRadius = bubbleDiameter * real_c(0.5); + const real_t domainVolume = real_c(realDomainSize[0] * realDomainSize[1] * realDomainSize[2]); + const real_t gasVolume = gasVolumeFraction * domainVolume; + const real_t bubbleVolume = real_c(4) / real_c(3) * real_c(math::pi) * real_c(std::pow(bubbleRadius, 3)); + const uint_t numBubbles = uint_c(gasVolume / bubbleVolume); + const real_t realGasVolumeFraction = real_c(numBubbles) * real_c(bubbleVolume) / domainVolume; + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(numBubbles); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(realGasVolumeFraction); + + // randomly place bubbles + for (uint_t bubble = uint_c(0); bubble != numBubbles; ++bubble) + { + walberla::math::seedRandomGenerator(std::mt19937::result_type(bubble)); + const Vector3< real_t > bubbleCenter = + Vector3< real_t >(math::realRandom(bubbleRadius, real_c(realDomainSize[0]) - bubbleRadius), + math::realRandom(bubbleRadius, real_c(realDomainSize[1]) - bubbleRadius), + math::realRandom(bubbleRadius, real_c(realDomainSize[2]) - bubbleRadius)); + + const geometry::Sphere sphereBubble(bubbleCenter, bubbleRadius); + bubble_model::addBodyToFillLevelField< geometry::Sphere >(*blockForest, fillFieldID, sphereBubble, true); + } + + // initialize boundary conditions from config file + const auto boundaryParameters = walberlaEnv.config()->getOneBlock("BoundaryParameters"); + freeSurfaceBoundaryHandling->initFromConfig(boundaryParameters); + + // IMPORTANT REMARK: this must be only called after every solid flag has been set; otherwise, the boundary handling + // might not detect solid flags correctly + freeSurfaceBoundaryHandling->initFlagsFromFillLevel(); + + // communication after initialization + Communication_T communication(blockForest, flagFieldID, fillFieldID, forceFieldID); + communication(); + + PdfCommunication_T pdfCommunication(blockForest, pdfFieldID); + pdfCommunication(); + + // add bubble model + std::shared_ptr< bubble_model::BubbleModelBase > bubbleModel = nullptr; + if (enableBubbleModel) + { + const std::shared_ptr< bubble_model::BubbleModel< CommunicationStencil_T > > bubbleModelDerived = + std::make_shared< bubble_model::BubbleModel< CommunicationStencil_T > >(blockForest, enableBubbleSplits); + bubbleModelDerived->initFromFillLevelField(fillFieldID); + + bubbleModel = std::static_pointer_cast< bubble_model::BubbleModelBase >(bubbleModelDerived); + } + else { bubbleModel = std::make_shared< bubble_model::BubbleModelConstantPressure >(real_c(1)); } + + // set density in non-liquid or non-interface cells to 1 (after initializing with hydrostatic pressure) + setDensityInNonFluidCellsToOne< FlagField_T, PdfField_T >(blockForest, flagInfo, flagFieldID, pdfFieldID); + + // create timeloop + SweepTimeloop timeloop(blockForest, timesteps); + + // Laplace pressure = 2 * surface tension * curvature; curvature computation is not necessary with 0 surface tension + bool computeCurvature = false; + if (!realIsEqual(surfaceTension, real_c(0), real_c(1e-14))) { computeCurvature = true; } + + // add surface geometry handler + const SurfaceGeometryHandler< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > geometryHandler( + blockForest, freeSurfaceBoundaryHandling, fillFieldID, curvatureModel, computeCurvature, enableWetting, + contactAngle); + + geometryHandler.addSweeps(timeloop); + + const ConstBlockDataID curvatureFieldID = geometryHandler.getConstCurvatureFieldID(); + const ConstBlockDataID normalFieldID = geometryHandler.getConstNormalFieldID(); + + // add boundary handling for standard boundaries and free surface boundaries + const SurfaceDynamicsHandler< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > dynamicsHandler( + blockForest, pdfFieldID, flagFieldID, fillFieldID, forceFieldID, normalFieldID, curvatureFieldID, + freeSurfaceBoundaryHandling, bubbleModel, pdfReconstructionModel, pdfRefillingModel, excessMassDistributionModel, + relaxationRate, force, surfaceTension, enableForceWeighting, useSimpleMassExchange, cellConversionThreshold, + cellConversionForceThreshold); + + dynamicsHandler.addSweeps(timeloop); + + // add load balancing + const LoadBalancer< FlagField_T, CommunicationStencil_T, LatticeModelStencil_T > loadBalancer( + blockForest, communication, pdfCommunication, bubbleModel, uint_c(50), uint_c(10), uint_c(5), + loadBalancingFrequency, printLoadBalancingStatistics); + timeloop.addFuncAfterTimeStep(loadBalancer, "Sweep: load balancing"); + + // add VTK output + addVTKOutput< LatticeModel_T, FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T, VectorField_T >( + blockForest, timeloop, walberlaEnv.config(), flagInfo, pdfFieldID, flagFieldID, fillFieldID, forceFieldID, + geometryHandler.getCurvatureFieldID(), geometryHandler.getNormalFieldID(), + geometryHandler.getObstNormalFieldID()); + + // add triangle mesh output of free surface + SurfaceMeshWriter< ScalarField_T, FlagField_T > surfaceMeshWriter( + blockForest, fillFieldID, flagFieldID, flagIDs::liquidInterfaceGasFlagIDs, real_c(0), walberlaEnv.config()); + surfaceMeshWriter(); // write initial mesh + timeloop.addFuncAfterTimeStep(surfaceMeshWriter, "Writer: surface mesh"); + + // add logging for computational performance + const lbm::PerformanceLogger< FlagField_T > perfLogger(blockForest, flagFieldID, flagIDs::liquidInterfaceFlagIDs, + performanceLogFrequency); + timeloop.addFuncAfterTimeStep(perfLogger, "Evaluator: performance logging"); + + WcTimingPool timingPool; + + for (uint_t t = uint_c(0); t != timesteps; ++t) + { + if (t % uint_c(real_c(timesteps / 100)) == uint_c(0)) + { + WALBERLA_LOG_DEVEL_ON_ROOT("Performing timestep = " << t); + } + timeloop.singleStep(timingPool, true); + + if (t % performanceLogFrequency == uint_c(0) && t > uint_c(0)) { timingPool.logResultOnRoot(); } + } + + return EXIT_SUCCESS; +} + +} // namespace DropInPool +} // namespace free_surface +} // namespace walberla + +int main(int argc, char** argv) { return walberla::free_surface::DropInPool::main(argc, argv); } \ No newline at end of file diff --git a/apps/showcases/FreeSurface/BubblyPoiseuille.prm b/apps/showcases/FreeSurface/BubblyPoiseuille.prm new file mode 100644 index 000000000..c6e7d413c --- /dev/null +++ b/apps/showcases/FreeSurface/BubblyPoiseuille.prm @@ -0,0 +1,108 @@ +BlockForestParameters +{ + cellsPerBlock < 20, 20, 20 >; + periodicity < 1, 1, 0 >; + loadBalancingFrequency 0; + printLoadBalancingStatistics false; +} + +DomainParameters +{ + channelWidth 100; + domainSizeFactor < 2, 1, 1 >; // values multiplied with channelWidth + bubbleDiameterFactor 0.1; // value multiplied with channelWidth + gasVolumeFraction 0.1; +} + +PhysicsParameters +{ + reynoldsNumber 10000; + mortonNumber 1e-5; + relaxationRate 1.989; + enableWetting false; + contactAngle 0; // only used if enableWetting=true + timesteps 10000; +} + +ModelParameters +{ + pdfReconstructionModel OnlyMissing; + pdfRefillingModel EquilibriumRefilling; + excessMassDistributionModel EvenlyAllInterface; + curvatureModel FiniteDifferenceMethod; + enableForceWeighting false; + useSimpleMassExchange false; + enableBubbleModel true; + enableBubbleSplits false; // only used if enableBubbleModel=true + cellConversionThreshold 1e-2; + cellConversionForceThreshold 1e-1; +} + +EvaluationParameters +{ + performanceLogFrequency 5000; +} + +BoundaryParameters +{ + // X + //Border { direction W; walldistance -1; NoSlip{} } + //Border { direction E; walldistance -1; NoSlip{} } + + // Y + //Border { direction N; walldistance -1; NoSlip{} } + //Border { direction S; walldistance -1; NoSlip{} } + + // Z + Border { direction T; walldistance -1; NoSlip{} } + Border { direction B; walldistance -1; NoSlip{} } +} + +MeshOutputParameters +{ + writeFrequency 2170; + baseFolder mesh-out; +} + +VTK +{ + fluid_field + { + writeFrequency 2170; + ghostLayers 0; + baseFolder vtk-out; + samplingResolution 1; + + writers + { + velocity; + density; + //pdf; + flag; + fill_level; + force; + curvature; + normal; + obstacle_normal; + mapped_flag; + } + + inclusion_filters + { + // only include liquid and interface cells in VTK output + //liquidInterfaceFilter; + } + + before_functions + { + //ghost_layer_synchronization; // only needed if writing the ghost layer + } + + } + domain_decomposition + { + writeFrequency 0; + baseFolder vtk-out; + outputDomainDecomposition true; + } +} \ No newline at end of file diff --git a/apps/showcases/FreeSurface/CMakeLists.txt b/apps/showcases/FreeSurface/CMakeLists.txt new file mode 100644 index 000000000..0e6edd5b2 --- /dev/null +++ b/apps/showcases/FreeSurface/CMakeLists.txt @@ -0,0 +1,48 @@ +waLBerla_link_files_to_builddir( *.prm ) + +waLBerla_add_executable(NAME BubblyPoiseuille + FILES BubblyPoiseuille.cpp + DEPENDS blockforest boundary core domain_decomposition field lbm postprocessing timeloop vtk) + +waLBerla_add_executable(NAME CapillaryWave + FILES CapillaryWave.cpp + DEPENDS blockforest boundary core domain_decomposition field lbm postprocessing timeloop vtk) + +waLBerla_add_executable(NAME DamBreakCylindrical + FILES DamBreakCylindrical.cpp + DEPENDS blockforest boundary core domain_decomposition field lbm postprocessing timeloop vtk) + +waLBerla_add_executable(NAME DamBreakRectangular + FILES DamBreakRectangular.cpp + DEPENDS blockforest boundary core domain_decomposition field lbm postprocessing timeloop vtk) + +waLBerla_add_executable(NAME DropImpact + FILES DropImpact.cpp + DEPENDS blockforest boundary core domain_decomposition field lbm postprocessing timeloop vtk) + +waLBerla_add_executable(NAME DropWetting + FILES DropWetting.cpp + DEPENDS blockforest boundary core domain_decomposition field lbm postprocessing timeloop vtk) + +waLBerla_add_executable(NAME GravityWave + FILES GravityWave.cpp + DEPENDS blockforest boundary core domain_decomposition field lbm postprocessing timeloop vtk) + +if( WALBERLA_BUILD_WITH_CODEGEN ) + walberla_generate_target_from_python( NAME GravityWaveLatticeModelGeneration + FILE GravityWaveLatticeModelGeneration.py + OUT_FILES GravityWaveLatticeModel.cpp GravityWaveLatticeModel.h ) + + waLBerla_add_executable(NAME GravityWaveCodegen + FILES GravityWaveCodegen.cpp + DEPENDS blockforest boundary core domain_decomposition field lbm postprocessing timeloop vtk + GravityWaveLatticeModelGeneration) +endif() + +waLBerla_add_executable(NAME RisingBubble + FILES RisingBubble.cpp + DEPENDS blockforest boundary core domain_decomposition field lbm postprocessing timeloop vtk) + +waLBerla_add_executable(NAME TaylorBubble + FILES TaylorBubble.cpp + DEPENDS blockforest boundary core domain_decomposition field lbm postprocessing timeloop vtk) diff --git a/apps/showcases/FreeSurface/CapillaryWave.cpp b/apps/showcases/FreeSurface/CapillaryWave.cpp new file mode 100644 index 000000000..d36003bc0 --- /dev/null +++ b/apps/showcases/FreeSurface/CapillaryWave.cpp @@ -0,0 +1,473 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file CapillaryWave.cpp +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +// +// This showcase simulates a standing wave purely governed by surface tension forces, i.e., without any body forces +// such as gravity +//====================================================================================================================== + +#include "blockforest/Initialization.h" + +#include "core/Environment.h" + +#include "lbm/PerformanceLogger.h" +#include "lbm/field/AddToStorage.h" +#include "lbm/free_surface/LoadBalancing.h" +#include "lbm/free_surface/SurfaceMeshWriter.h" +#include "lbm/free_surface/TotalMassComputer.h" +#include "lbm/free_surface/VtkWriter.h" +#include "lbm/free_surface/dynamics/SurfaceDynamicsHandler.h" +#include "lbm/free_surface/surface_geometry/SurfaceGeometryHandler.h" +#include "lbm/free_surface/surface_geometry/Utility.h" +#include "lbm/lattice_model/D2Q9.h" + +namespace walberla +{ +namespace free_surface +{ +namespace CapillaryWave +{ +using ScalarField_T = GhostLayerField< real_t, 1 >; +using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >; + +using CollisionModel_T = lbm::collision_model::SRT; +using ForceModel_T = lbm::force_model::GuoField< VectorField_T >; +using LatticeModel_T = lbm::D2Q9< CollisionModel_T, true, ForceModel_T, 2 >; +using LatticeModelStencil_T = LatticeModel_T::Stencil; +using PdfField_T = lbm::PdfField< LatticeModel_T >; +using PdfCommunication_T = blockforest::SimpleCommunication< LatticeModelStencil_T >; + +// the geometry computations in SurfaceGeometryHandler require meaningful values in the ghost layers in corner +// directions (flag field and fill level field); this holds, even if the lattice model uses a D3Q19 stencil +using CommunicationStencil_T = + typename std::conditional< LatticeModel_T::Stencil::D == uint_t(2), stencil::D2Q9, stencil::D3Q27 >::type; +using Communication_T = blockforest::SimpleCommunication< CommunicationStencil_T >; + +using flag_t = uint32_t; +using FlagField_T = FlagField< flag_t >; +using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >; + +// write each entry in "vector" to line in a file; columns are separated by tabs +template< typename T > +void writeVectorToFile(const std::vector< T >& vector, const std::string& filename); + +// function describing the global initialization profile +inline real_t initializationProfile(real_t x, real_t amplitude, real_t offset, real_t wavelength) +{ + return amplitude * std::cos(x / wavelength * real_c(2) * math::pi + math::pi) + offset; +} + +// get interface position in y-direction at the specified (global) x-coordinate +template< typename FreeSurfaceBoundaryHandling_T > +class SurfaceYPositionEvaluator +{ + public: + SurfaceYPositionEvaluator(const std::weak_ptr< const StructuredBlockForest >& blockForest, + const std::weak_ptr< const FreeSurfaceBoundaryHandling_T >& freeSurfaceBoundaryHandling, + const ConstBlockDataID& fillFieldID, const Vector3< uint_t >& domainSize, + cell_idx_t globalXCoordinate, uint_t frequency, + const std::shared_ptr< real_t >& surfaceYPosition) + : blockForest_(blockForest), freeSurfaceBoundaryHandling_(freeSurfaceBoundaryHandling), fillFieldID_(fillFieldID), + domainSize_(domainSize), globalXCoordinate_(globalXCoordinate), surfaceYPosition_(surfaceYPosition), + frequency_(frequency), executionCounter_(uint_c(0)) + {} + + void operator()() + { + auto blockForest = blockForest_.lock(); + WALBERLA_CHECK_NOT_NULLPTR(blockForest); + + auto freeSurfaceBoundaryHandling = freeSurfaceBoundaryHandling_.lock(); + WALBERLA_CHECK_NOT_NULLPTR(freeSurfaceBoundaryHandling); + + ++executionCounter_; + + // only evaluate in given frequencies + if (executionCounter_ % frequency_ != uint_c(0) && executionCounter_ != uint_c(1)) { return; } + + const BlockDataID flagFieldID = freeSurfaceBoundaryHandling->getFlagFieldID(); + const typename FreeSurfaceBoundaryHandling_T::FlagInfo_T& flagInfo = freeSurfaceBoundaryHandling->getFlagInfo(); + + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + *surfaceYPosition_ = real_c(0); + + CellInterval globalSearchInterval(globalXCoordinate_, cell_idx_c(0), cell_idx_c(0), globalXCoordinate_, + cell_idx_c(domainSize_[1]), cell_idx_c(0)); + + if (blockForest->getBlockCellBB(*blockIt).overlaps(globalSearchInterval)) + { + // transform specified global x-coordinate into block local coordinate + Cell localEvalCell = Cell(globalXCoordinate_, cell_idx_c(0), cell_idx_c(0)); + blockForest->transformGlobalToBlockLocalCell(localEvalCell, *blockIt); + + const FlagField_T* const flagField = blockIt->template getData< const FlagField_T >(flagFieldID); + const ScalarField_T* const fillField = blockIt->template getData< const ScalarField_T >(fillFieldID_); + + // searching from top ensures that the interface cell with the greatest y-coordinate is found first + for (cell_idx_t y = cell_idx_c((flagField)->ySize() - uint_c(1)); y >= cell_idx_t(0); --y) + { + if (flagInfo.isInterface(flagField->get(localEvalCell[0], y, cell_idx_c(0)))) + { + const real_t fillLevel = fillField->get(localEvalCell[0], y, cell_idx_c(0)); + + // transform local y-coordinate to global coordinate + Cell localResultCell = localEvalCell; + localResultCell[1] = y; + blockForest->transformBlockLocalToGlobalCell(localResultCell, *blockIt); + *surfaceYPosition_ = real_c(localResultCell[1]) + fillLevel; + + break; + } + } + } + } + // communicate result among all processes + mpi::allReduceInplace< real_t >(*surfaceYPosition_, mpi::MAX); + } + + private: + std::weak_ptr< const StructuredBlockForest > blockForest_; + std::weak_ptr< const FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling_; + ConstBlockDataID fillFieldID_; + Vector3< uint_t > domainSize_; + cell_idx_t globalXCoordinate_; + std::shared_ptr< real_t > surfaceYPosition_; + + uint_t frequency_; + uint_t executionCounter_; +}; // class SurfaceYPositionEvaluator + +int main(int argc, char** argv) +{ + Environment walberlaEnv(argc, argv); + + if (argc < 2) { WALBERLA_ABORT("Please specify a parameter file as input argument.") } + + // print content of parameter file + WALBERLA_LOG_INFO_ON_ROOT(*walberlaEnv.config()); + + // get block forest parameters from parameter file + auto blockForestParameters = walberlaEnv.config()->getOneBlock("BlockForestParameters"); + const Vector3< uint_t > cellsPerBlock = blockForestParameters.getParameter< Vector3< uint_t > >("cellsPerBlock"); + const Vector3< bool > periodicity = blockForestParameters.getParameter< Vector3< bool > >("periodicity"); + const uint_t loadBalancingFrequency = blockForestParameters.getParameter< uint_t >("loadBalancingFrequency"); + const bool printLoadBalancingStatistics = blockForestParameters.getParameter< bool >("printLoadBalancingStatistics"); + + // get domain parameters from parameter file + auto domainParameters = walberlaEnv.config()->getOneBlock("DomainParameters"); + const uint_t domainWidth = domainParameters.getParameter< uint_t >("domainWidth"); + const real_t liquidDepth = domainParameters.getParameter< real_t >("liquidDepth"); + const real_t initialAmplitude = domainParameters.getParameter< real_t >("initialAmplitude"); + + // define domain size + Vector3< uint_t > domainSize; + domainSize[0] = domainWidth; + domainSize[1] = uint_c(liquidDepth * real_c(2)); + domainSize[2] = uint_c(1); + + // compute number of blocks as defined by domainSize and cellsPerBlock + Vector3< uint_t > numBlocks; + numBlocks[0] = uint_c(std::ceil(real_c(domainSize[0]) / real_c(cellsPerBlock[0]))); + numBlocks[1] = uint_c(std::ceil(real_c(domainSize[1]) / real_c(cellsPerBlock[1]))); + numBlocks[2] = uint_c(std::ceil(real_c(domainSize[2]) / real_c(cellsPerBlock[2]))); + + // get number of (MPI) processes + uint_t numProcesses = uint_c(MPIManager::instance()->numProcesses()); + WALBERLA_CHECK_LESS_EQUAL(numProcesses, numBlocks[0] * numBlocks[1] * numBlocks[2], + "The number of MPI processes is greater than the number of blocks as defined by " + "\"domainSize/cellsPerBlock\". This would result in unused MPI processes. Either decrease " + "the number of MPI processes or increase \"cellsPerBlock\".") + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(numProcesses); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellsPerBlock); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(domainSize); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(numBlocks); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(domainWidth); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(liquidDepth); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(initialAmplitude); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(periodicity); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(loadBalancingFrequency); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(printLoadBalancingStatistics); + + // get physics parameters from parameter file + auto physicsParameters = walberlaEnv.config()->getOneBlock("PhysicsParameters"); + const uint_t timesteps = physicsParameters.getParameter< uint_t >("timesteps"); + + const real_t relaxationRate = physicsParameters.getParameter< real_t >("relaxationRate"); + const CollisionModel_T collisionModel = CollisionModel_T(relaxationRate); + const real_t viscosity = collisionModel.viscosity(); + + const real_t reynoldsNumber = physicsParameters.getParameter< real_t >("reynoldsNumber"); + const real_t waveNumber = real_c(2) * math::pi / real_c(domainSize[0]); + const real_t waveFrequency = reynoldsNumber * viscosity / real_c(domainSize[0]) / initialAmplitude; + + // sum of both phases' densities is implicitly neglected here (would be factor of 1) + const real_t surfaceTension = waveFrequency * waveFrequency / std::pow(waveNumber, real_c(3)); + + const bool enableWetting = physicsParameters.getParameter< bool >("enableWetting"); + const real_t contactAngle = physicsParameters.getParameter< real_t >("contactAngle"); + + const Vector3< real_t > force = Vector3< real_t >(real_c(0)); + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(reynoldsNumber); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(relaxationRate); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableWetting); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(contactAngle); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(timesteps); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(viscosity); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(force); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(surfaceTension); + + // read model parameters from parameter file + const auto modelParameters = walberlaEnv.config()->getOneBlock("ModelParameters"); + const std::string pdfReconstructionModel = modelParameters.getParameter< std::string >("pdfReconstructionModel"); + const std::string pdfRefillingModel = modelParameters.getParameter< std::string >("pdfRefillingModel"); + const std::string excessMassDistributionModel = + modelParameters.getParameter< std::string >("excessMassDistributionModel"); + const std::string curvatureModel = modelParameters.getParameter< std::string >("curvatureModel"); + const bool enableForceWeighting = modelParameters.getParameter< bool >("enableForceWeighting"); + const bool useSimpleMassExchange = modelParameters.getParameter< bool >("useSimpleMassExchange"); + const real_t cellConversionThreshold = modelParameters.getParameter< real_t >("cellConversionThreshold"); + const real_t cellConversionForceThreshold = modelParameters.getParameter< real_t >("cellConversionForceThreshold"); + const bool enableBubbleModel = modelParameters.getParameter< bool >("enableBubbleModel"); + const bool enableBubbleSplits = modelParameters.getParameter< bool >("enableBubbleSplits"); + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(pdfReconstructionModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(pdfRefillingModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(excessMassDistributionModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(curvatureModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableForceWeighting); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(useSimpleMassExchange); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellConversionThreshold); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellConversionForceThreshold); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableBubbleModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableBubbleSplits); + + // read evaluation parameters from parameter file + const auto evaluationParameters = walberlaEnv.config()->getOneBlock("EvaluationParameters"); + const uint_t performanceLogFrequency = evaluationParameters.getParameter< uint_t >("performanceLogFrequency"); + const uint_t evaluationFrequency = evaluationParameters.getParameter< uint_t >("evaluationFrequency"); + const std::string filename = evaluationParameters.getParameter< std::string >("filename"); + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(performanceLogFrequency); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(evaluationFrequency); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(filename); + + // create non-uniform block forest (non-uniformity required for load balancing) + const std::shared_ptr< StructuredBlockForest > blockForest = + createNonUniformBlockForest(domainSize, cellsPerBlock, numBlocks, periodicity); + + // add force field + const BlockDataID forceFieldID = + field::addToStorage< VectorField_T >(blockForest, "Force field", force, field::fzyx, uint_c(1)); + + // create lattice model + const LatticeModel_T latticeModel = LatticeModel_T(collisionModel, ForceModel_T(forceFieldID)); + + // add pdf field + const BlockDataID pdfFieldID = lbm::addPdfFieldToStorage(blockForest, "PDF field", latticeModel, field::fzyx); + + // add fill level field (initialized with 0, i.e., gas everywhere) + const BlockDataID fillFieldID = + field::addToStorage< ScalarField_T >(blockForest, "Fill level field", real_c(0.0), field::fzyx, uint_c(2)); + + // add boundary handling + const std::shared_ptr< FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling = + std::make_shared< FreeSurfaceBoundaryHandling_T >(blockForest, pdfFieldID, fillFieldID); + const BlockDataID flagFieldID = freeSurfaceBoundaryHandling->getFlagFieldID(); + const typename FreeSurfaceBoundaryHandling_T::FlagInfo_T& flagInfo = freeSurfaceBoundaryHandling->getFlagInfo(); + + // samples used in the Monte-Carlo like estimation of the fill level + const uint_t fillLevelInitSamples = uint_c(100); // actually there will be 101 since 0 is also included + + const uint_t numTotalPoints = (fillLevelInitSamples + uint_c(1)) * (fillLevelInitSamples + uint_c(1)); + const real_t stepsize = real_c(1) / real_c(fillLevelInitSamples); + + // initialize sine profile such that there is exactly one period; every length is normalized with domainSize[0] + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + ScalarField_T* const fillField = blockIt->getData< ScalarField_T >(fillFieldID); + + WALBERLA_FOR_ALL_CELLS(fillFieldIt, fillField, { + // cell in block-local coordinates + const Cell localCell = fillFieldIt.cell(); + + // get cell in global coordinates + Cell globalCell = fillFieldIt.cell(); + blockForest->transformBlockLocalToGlobalCell(globalCell, *blockIt, localCell); + + // Monte-Carlo like estimation of the fill level: + // create uniformly-distributed sample points in each cell and count the number of points below the sine + // profile; this fraction of points is used as the fill level to initialize the profile + uint_t numPointsBelow = uint_c(0); + + for (uint_t xSample = uint_c(0); xSample <= fillLevelInitSamples; ++xSample) + { + // value of the sine-function + const real_t functionValue = initializationProfile(real_c(globalCell[0]) + real_c(xSample) * stepsize, + initialAmplitude, liquidDepth, real_c(domainSize[0])); + + for (uint_t ySample = uint_c(0); ySample <= fillLevelInitSamples; ++ySample) + { + const real_t yPoint = real_c(globalCell[1]) + real_c(ySample) * stepsize; + // with operator <, a fill level of 1 can not be reached when the line is equal to the cell's top border; + // with operator <=, a fill level of 0 can not be reached when the line is equal to the cell's bottom + // border + if (yPoint < functionValue) { ++numPointsBelow; } + } + } + + // fill level is fraction of points below sine profile + fillField->get(localCell) = real_c(numPointsBelow) / real_c(numTotalPoints); + }) // WALBERLA_FOR_ALL_CELLS + } + + // initialize domain boundary conditions from config file + const auto boundaryParameters = walberlaEnv.config()->getOneBlock("BoundaryParameters"); + freeSurfaceBoundaryHandling->initFromConfig(boundaryParameters); + + // IMPORTANT REMARK: this must be only called after every solid flag has been set; otherwise, the boundary handling + // might not detect solid flags correctly + freeSurfaceBoundaryHandling->initFlagsFromFillLevel(); + + // communication after initialization + Communication_T communication(blockForest, flagFieldID, fillFieldID, forceFieldID); + communication(); + + PdfCommunication_T pdfCommunication(blockForest, pdfFieldID); + pdfCommunication(); + + // add bubble model + std::shared_ptr< bubble_model::BubbleModelBase > bubbleModel = nullptr; + if (enableBubbleModel) + { + const std::shared_ptr< bubble_model::BubbleModel< CommunicationStencil_T > > bubbleModelDerived = + std::make_shared< bubble_model::BubbleModel< CommunicationStencil_T > >(blockForest, enableBubbleSplits); + bubbleModelDerived->initFromFillLevelField(fillFieldID); + bubbleModelDerived->setAtmosphere(Cell(domainSize[0] - uint_c(1), domainSize[1] - uint_c(1), uint_c(0)), + real_c(1)); + + bubbleModel = std::static_pointer_cast< bubble_model::BubbleModelBase >(bubbleModelDerived); + } + else { bubbleModel = std::make_shared< bubble_model::BubbleModelConstantPressure >(real_c(1)); } + + // create timeloop + SweepTimeloop timeloop(blockForest, timesteps); + + // Laplace pressure = 2 * surface tension * curvature; curvature computation is not necessary with 0 surface tension + bool computeCurvature = false; + if (!realIsEqual(surfaceTension, real_c(0), real_c(1e-14))) { computeCurvature = true; } + + // add surface geometry handler + const SurfaceGeometryHandler< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > geometryHandler( + blockForest, freeSurfaceBoundaryHandling, fillFieldID, curvatureModel, computeCurvature, enableWetting, + contactAngle); + + geometryHandler.addSweeps(timeloop); + + // get fields created by surface geometry handler + const ConstBlockDataID curvatureFieldID = geometryHandler.getConstCurvatureFieldID(); + const ConstBlockDataID normalFieldID = geometryHandler.getConstNormalFieldID(); + + // add boundary handling for standard boundaries and free surface boundaries + const SurfaceDynamicsHandler< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > dynamicsHandler( + blockForest, pdfFieldID, flagFieldID, fillFieldID, forceFieldID, normalFieldID, curvatureFieldID, + freeSurfaceBoundaryHandling, bubbleModel, pdfReconstructionModel, pdfRefillingModel, excessMassDistributionModel, + relaxationRate, force, surfaceTension, enableForceWeighting, useSimpleMassExchange, cellConversionThreshold, + cellConversionForceThreshold); + + dynamicsHandler.addSweeps(timeloop); + + // add load balancing + LoadBalancer< FlagField_T, CommunicationStencil_T, LatticeModelStencil_T > loadBalancer( + blockForest, communication, pdfCommunication, bubbleModel, uint_c(50), uint_c(10), uint_c(5), + loadBalancingFrequency, printLoadBalancingStatistics); + timeloop.addFuncAfterTimeStep(loadBalancer, "Sweep: load balancing"); + + // add sweep for evaluating the surface position in y-direction + const std::shared_ptr< real_t > surfaceYPosition = std::make_shared< real_t >(real_c(0)); + const SurfaceYPositionEvaluator< FreeSurfaceBoundaryHandling_T > positionEvaluator( + blockForest, freeSurfaceBoundaryHandling, fillFieldID, domainSize, + cell_idx_c(real_c(0.5) * real_c(domainSize[0])), evaluationFrequency, surfaceYPosition); + timeloop.addFuncAfterTimeStep(positionEvaluator, "Evaluator: surface position"); + + // add VTK output + addVTKOutput< LatticeModel_T, FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T, VectorField_T >( + blockForest, timeloop, walberlaEnv.config(), flagInfo, pdfFieldID, flagFieldID, fillFieldID, forceFieldID, + geometryHandler.getCurvatureFieldID(), geometryHandler.getNormalFieldID(), + geometryHandler.getObstNormalFieldID()); + + // add triangle mesh output of free surface + SurfaceMeshWriter< ScalarField_T, FlagField_T > surfaceMeshWriter( + blockForest, fillFieldID, flagFieldID, flagIDs::liquidInterfaceGasFlagIDs, real_c(0), walberlaEnv.config()); + surfaceMeshWriter(); // write initial mesh + timeloop.addFuncAfterTimeStep(surfaceMeshWriter, "Writer: surface mesh"); + + // add logging for computational performance + const lbm::PerformanceLogger< FlagField_T > perfLogger(blockForest, flagFieldID, flagIDs::liquidInterfaceFlagIDs, + performanceLogFrequency); + timeloop.addFuncAfterTimeStep(perfLogger, "Evaluator: performance logging"); + + WcTimingPool timingPool; + + for (uint_t t = uint_c(0); t != timesteps; ++t) + { + timeloop.singleStep(timingPool, true); + + WALBERLA_ROOT_SECTION() + { + // non-dimensionalize time and surface position + const real_t tNonDimensional = real_c(t) * waveFrequency; + const real_t positionNonDimensional = (*surfaceYPosition - liquidDepth) / initialAmplitude; + + const std::vector< real_t > resultVector{ tNonDimensional, positionNonDimensional }; + if (t % evaluationFrequency == uint_c(0)) + { + WALBERLA_LOG_DEVEL("time step = " << t); + WALBERLA_LOG_DEVEL("\t\ttNonDimensional = " << tNonDimensional + << "\n\t\tpositionNonDimensional = " << positionNonDimensional); + writeVectorToFile(resultVector, filename); + } + } + + if (t % performanceLogFrequency == uint_c(0) && t > uint_c(0)) { timingPool.logResultOnRoot(); } + } + + return EXIT_SUCCESS; +} + +template< typename T > +void writeVectorToFile(const std::vector< T >& vector, const std::string& filename) +{ + std::fstream file; + file.open(filename, std::fstream::app); + + for (const auto i : vector) + { + file << "\t" << i; + } + + file << "\n"; + file.close(); +} + +} // namespace CapillaryWave +} // namespace free_surface +} // namespace walberla + +int main(int argc, char** argv) { return walberla::free_surface::CapillaryWave::main(argc, argv); } \ No newline at end of file diff --git a/apps/showcases/FreeSurface/CapillaryWave.prm b/apps/showcases/FreeSurface/CapillaryWave.prm new file mode 100644 index 000000000..8d172cadd --- /dev/null +++ b/apps/showcases/FreeSurface/CapillaryWave.prm @@ -0,0 +1,107 @@ +BlockForestParameters +{ + cellsPerBlock < 25, 25, 1 >; + periodicity < 1, 0, 1 >; + loadBalancingFrequency 50; + printLoadBalancingStatistics true; +} + +DomainParameters +{ + domainWidth 50; // equivalent to wavelength + liquidDepth 25; + initialAmplitude 0.5; +} + +PhysicsParameters +{ + reynoldsNumber 10; + relaxationRate 1.8; + enableWetting false; + contactAngle 0; // only used if enableWetting=true + timesteps 10000; +} + +ModelParameters +{ + pdfReconstructionModel OnlyMissing; + pdfRefillingModel EquilibriumRefilling; + excessMassDistributionModel EvenlyAllInterface; + curvatureModel FiniteDifferenceMethod; + enableForceWeighting false; + useSimpleMassExchange false; + cellConversionThreshold 1e-2; + cellConversionForceThreshold 1e-1; + + enableBubbleModel false; + enableBubbleSplits false; // only used if enableBubbleModel=true +} + +EvaluationParameters +{ + performanceLogFrequency 5000; + evaluationFrequency 100; + filename capillary-wave.txt; +} + +BoundaryParameters +{ + // X + //Border { direction W; walldistance -1; NoSlip{} } + //Border { direction E; walldistance -1; NoSlip{} } + + // Y + Border { direction N; walldistance -1; NoSlip{} } + Border { direction S; walldistance -1; NoSlip{} } + + // Z + //Border { direction T; walldistance -1; NoSlip{} } + //Border { direction B; walldistance -1; NoSlip{} } +} + +MeshOutputParameters +{ + writeFrequency 100; + baseFolder mesh-out; +} + +VTK +{ + fluid_field + { + writeFrequency 100; + ghostLayers 0; + baseFolder vtk-out; + samplingResolution 1; + + writers + { + fill_level; + mapped_flag; + velocity; + density; + //curvature; + //normal; + //obstacle_normal; + //pdf; + //flag; + //force; + } + + inclusion_filters + { + //liquidInterfaceFilter; // only include liquid and interface cells in VTK output + } + + before_functions + { + //ghost_layer_synchronization; // only needed if writing the ghost layer + } + } + domain_decomposition + { + writeFrequency 100; + baseFolder vtk-out; + outputDomainDecomposition true; + } +} \ No newline at end of file diff --git a/apps/showcases/FreeSurface/DamBreakCylindrical.cpp b/apps/showcases/FreeSurface/DamBreakCylindrical.cpp new file mode 100644 index 000000000..129cda8de --- /dev/null +++ b/apps/showcases/FreeSurface/DamBreakCylindrical.cpp @@ -0,0 +1,606 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file DamBreakCylindrical.cpp +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +// +// This showcase simulates the collapse of a cylindrical liquid column in 3D. Reference experiments are available from +// Martin, Moyce (1952), doi:10.1098/rsta.1952.0006 +//====================================================================================================================== + +#include "blockforest/Initialization.h" + +#include "core/Environment.h" +#include "core/math/DistributedSample.h" +#include "core/math/Sample.h" + +#include "field/Gather.h" + +#include "lbm/PerformanceLogger.h" +#include "lbm/field/AddToStorage.h" +#include "lbm/free_surface/LoadBalancing.h" +#include "lbm/free_surface/SurfaceMeshWriter.h" +#include "lbm/free_surface/VtkWriter.h" +#include "lbm/free_surface/bubble_model/Geometry.h" +#include "lbm/free_surface/dynamics/SurfaceDynamicsHandler.h" +#include "lbm/free_surface/surface_geometry/SurfaceGeometryHandler.h" +#include "lbm/free_surface/surface_geometry/Utility.h" +#include "lbm/lattice_model/D3Q19.h" + +#include "stencil/D3Q27.h" + +namespace walberla +{ +namespace free_surface +{ +namespace DamBreakCylindrical +{ +using ScalarField_T = GhostLayerField< real_t, 1 >; +using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >; + +using CollisionModel_T = lbm::collision_model::SRTField< ScalarField_T >; +using ForceModel_T = lbm::force_model::GuoField< VectorField_T >; +using LatticeModel_T = lbm::D3Q19< CollisionModel_T, true, ForceModel_T, 2 >; +using LatticeModelStencil_T = LatticeModel_T::Stencil; +using PdfField_T = lbm::PdfField< LatticeModel_T >; +using PdfCommunication_T = blockforest::SimpleCommunication< LatticeModelStencil_T >; + +// the geometry computations in SurfaceGeometryHandler require meaningful values in the ghost layers in corner +// directions (flag field and fill level field); this holds, even if the lattice model uses a D3Q19 stencil +using CommunicationStencil_T = + typename std::conditional< LatticeModel_T::Stencil::D == uint_t(2), stencil::D2Q9, stencil::D3Q27 >::type; +using Communication_T = blockforest::SimpleCommunication< CommunicationStencil_T >; + +using flag_t = uint32_t; +using FlagField_T = FlagField< flag_t >; +using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >; + +template< typename T > +void writeNumberVector(const std::vector< T >& numberVector, const uint_t& timestep, const std::string& filename) +{ + std::fstream file; + file.open(filename, std::fstream::app); + + file << timestep; + for (const auto number : numberVector) + { + file << "\t" << number; + } + + file << "\n"; + file.close(); +} + +// get statistical Sample of column width, i.e., radius of the liquid front at the bottom layer (y=0); the center point +// of the cylindrical column is assumed to be constant throughout the simulations +// IMPORTANT REMARK: This implementation is not very efficient, as it gathers a slice of the whole flag field on a +// single process to perform the evaluation. +template< typename FreeSurfaceBoundaryHandling_T > +class columnRadiusEvaluator +{ + public: + columnRadiusEvaluator(const std::weak_ptr< StructuredBlockForest >& blockForest, + const std::weak_ptr< const FreeSurfaceBoundaryHandling_T >& freeSurfaceBoundaryHandling, + const Vector3< uint_t >& domainSize, const Vector3< real_t >& initialOrigin, uint_t interval, + const std::shared_ptr< math::Sample >& columnRadiusSample) + : blockForest_(blockForest), freeSurfaceBoundaryHandling_(freeSurfaceBoundaryHandling), domainSize_(domainSize), + initialOrigin_(initialOrigin), columnRadiusSample_(columnRadiusSample), interval_(interval), + executionCounter_(uint_c(0)) + {} + + void operator()() + { + auto blockForest = blockForest_.lock(); + WALBERLA_CHECK_NOT_NULLPTR(blockForest); + + auto freeSurfaceBoundaryHandling = freeSurfaceBoundaryHandling_.lock(); + WALBERLA_CHECK_NOT_NULLPTR(freeSurfaceBoundaryHandling); + + ++executionCounter_; + + // only evaluate in given intervals + if (executionCounter_ % interval_ != uint_c(0)) { return; } + + // gather a slice of the flag field on rank 0 (WARNING: simple, but inefficient) + std::shared_ptr< FlagField_T > flagFieldGathered = nullptr; + WALBERLA_ROOT_SECTION() + { + flagFieldGathered = std::make_shared< FlagField_T >(domainSize_[0], uint_c(1), domainSize_[2], uint_c(0)); + } + field::gatherSlice< FlagField_T, FlagField_T >( + *flagFieldGathered, blockForest, freeSurfaceBoundaryHandling->getFlagFieldID(), 1, cell_idx_c(0), 0); + + WALBERLA_ROOT_SECTION() + { + columnRadiusSample_->clear(); + + const Cell startCell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0)); + + const typename FreeSurfaceBoundaryHandling_T::FlagInfo_T& flagInfo = + freeSurfaceBoundaryHandling->getFlagInfo(); + WALBERLA_CHECK(flagInfo.isGas(flagFieldGathered->get(startCell)), + "The \"startCell\" in columnRadiusEvaluator's flood fill algorithm must be a gas cell."); + + getRadiusFromFloodFill(flagFieldGathered, startCell, *columnRadiusSample_); + } + } + + void getRadiusFromFloodFill(const std::shared_ptr< FlagField_T >& flagFieldGathered, const Cell& startCell, + math::Sample& columnRadiusSample) + { + WALBERLA_CHECK_EQUAL(startCell.y(), cell_idx_c(0), + "columnRadiusEvaluator is meant to be search at the domain's bottom layer only (at y=0)."); + + auto freeSurfaceBoundaryHandling = freeSurfaceBoundaryHandling_.lock(); + WALBERLA_CHECK_NOT_NULLPTR(freeSurfaceBoundaryHandling); + + const typename FreeSurfaceBoundaryHandling_T::FlagInfo_T& flagInfo = freeSurfaceBoundaryHandling->getFlagInfo(); + + std::queue< Cell > cellQueue; + cellQueue.push(startCell); + + while (!cellQueue.empty()) + { + Cell cell = cellQueue.front(); + cellQueue.pop(); + + if (flagInfo.isGas(flagFieldGathered->get(cell))) + { + // remove flag such that cell is not detected as gas when searching from neighboring cells + flagFieldGathered->get(cell) = flag_t(0); + + for (auto dir = stencil::D3Q27::beginNoCenter(); dir != stencil::D3Q27::end(); ++dir) + { + // only search at y=0 + if (dir.cy() != 0) { continue; } + + const Cell neighborCell = Cell(cell.x() + dir.cx(), cell.y() + dir.cy(), cell.z() + dir.cz()); + + // make sure that the algorithm stays within the field dimensions + if (neighborCell.x() >= cell_idx_c(0) && neighborCell.x() < cell_idx_c(flagFieldGathered->xSize()) && + neighborCell.y() >= cell_idx_c(0) && neighborCell.y() < cell_idx_c(flagFieldGathered->ySize()) && + neighborCell.z() >= cell_idx_c(0) && neighborCell.z() < cell_idx_c(flagFieldGathered->zSize())) + { + // add neighboring cell to queue + cellQueue.push(neighborCell); + } + } + } + else + { + if (flagInfo.isInterface(flagFieldGathered->get(cell))) + { + // get center point of this cell; directly use y=0 here + const Vector3< real_t > cellCenter(real_c(cell.x()) + real_c(0.5), real_c(0), + real_c(cell.z()) + real_c(0.5)); + + const real_t radius = (initialOrigin_ - cellCenter).length(); + + columnRadiusSample.castToRealAndInsert(radius); + } // else: cell is neither gas, nor interface => do nothing + } + } + } + + private: + std::weak_ptr< StructuredBlockForest > blockForest_; + std::weak_ptr< const FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling_; + Vector3< uint_t > domainSize_; + Vector3< real_t > initialOrigin_; + std::shared_ptr< math::Sample > columnRadiusSample_; + + uint_t interval_; + uint_t executionCounter_; +}; // class columnRadiusEvaluator + +// get height of residual liquid column, i.e., height of liquid at initial center +template< typename FreeSurfaceBoundaryHandling_T > +class ColumnHeightEvaluator +{ + public: + ColumnHeightEvaluator(const std::weak_ptr< const StructuredBlockForest >& blockForest, + const std::weak_ptr< const FreeSurfaceBoundaryHandling_T >& freeSurfaceBoundaryHandling, + const Vector3< uint_t >& domainSize, const Vector3< real_t >& initialOrigin, uint_t interval, + const std::shared_ptr< cell_idx_t >& currentColumnHeight) + : blockForest_(blockForest), freeSurfaceBoundaryHandling_(freeSurfaceBoundaryHandling), domainSize_(domainSize), + initialOrigin_(initialOrigin), currentColumnHeight_(currentColumnHeight), interval_(interval), + executionCounter_(uint_c(0)) + {} + + void operator()() + { + auto blockForest = blockForest_.lock(); + WALBERLA_CHECK_NOT_NULLPTR(blockForest); + + auto freeSurfaceBoundaryHandling = freeSurfaceBoundaryHandling_.lock(); + WALBERLA_CHECK_NOT_NULLPTR(freeSurfaceBoundaryHandling); + + ++executionCounter_; + + // only evaluate in given intervals + if (executionCounter_ % interval_ != uint_c(0)) { return; } + + const BlockDataID flagFieldID = freeSurfaceBoundaryHandling->getFlagFieldID(); + const typename FreeSurfaceBoundaryHandling_T::FlagInfo_T& flagInfo = freeSurfaceBoundaryHandling->getFlagInfo(); + + *currentColumnHeight_ = cell_idx_c(0); + + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + cell_idx_t maxColumnHeight = cell_idx_c(0); + bool isInterfaceFound = false; + + const CellInterval globalSearchInterval(cell_idx_c(initialOrigin_[0]), cell_idx_c(0), + cell_idx_c(initialOrigin_[2]), cell_idx_c(initialOrigin_[0]), + cell_idx_c(domainSize_[1]), cell_idx_c(initialOrigin_[2])); + + if (blockForest->getBlockCellBB(*blockIt).overlaps(globalSearchInterval)) + { + CellInterval localSearchInterval = globalSearchInterval; + + // get intersection of globalSearchInterval and this block's bounding box (both in global coordinates) + localSearchInterval.intersect(blockForest->getBlockCellBB(*blockIt)); + + blockForest->transformGlobalToBlockLocalCellInterval(localSearchInterval, *blockIt); + + const FlagField_T* const flagField = blockIt->template getData< const FlagField_T >(flagFieldID); + + for (auto c = localSearchInterval.begin(); c != localSearchInterval.end(); ++c) + { + if (flagInfo.isInterface(flagField->get(*c))) + { + if (c->y() >= maxColumnHeight) + { + maxColumnHeight = c->y(); + isInterfaceFound = true; + } + } + } + + if (isInterfaceFound) + { + // transform local y-coordinate to global coordinate + Cell localResultCell = Cell(cell_idx_c(0), maxColumnHeight, cell_idx_c(0)); + blockForest->transformBlockLocalToGlobalCell(localResultCell, *blockIt); + if (localResultCell[1] > *currentColumnHeight_) { *currentColumnHeight_ = localResultCell[1]; } + } + } + } + mpi::allReduceInplace< cell_idx_t >(*currentColumnHeight_, mpi::MAX); + } + + private: + std::weak_ptr< const StructuredBlockForest > blockForest_; + std::weak_ptr< const FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling_; + Vector3< uint_t > domainSize_; + Vector3< real_t > initialOrigin_; + std::shared_ptr< cell_idx_t > currentColumnHeight_; + + uint_t interval_; + uint_t executionCounter_; +}; // class ColumnHeightEvaluator + +int main(int argc, char** argv) +{ + Environment walberlaEnv(argc, argv); + + if (argc < 2) { WALBERLA_ABORT("Please specify a parameter file as input argument.") } + + // print content of parameter file + WALBERLA_LOG_INFO_ON_ROOT(*walberlaEnv.config()); + + // get block forest parameters from parameter file + auto blockForestParameters = walberlaEnv.config()->getOneBlock("BlockForestParameters"); + const Vector3< uint_t > cellsPerBlock = blockForestParameters.getParameter< Vector3< uint_t > >("cellsPerBlock"); + const Vector3< bool > periodicity = blockForestParameters.getParameter< Vector3< bool > >("periodicity"); + const uint_t loadBalancingFrequency = blockForestParameters.getParameter< uint_t >("loadBalancingFrequency"); + const bool printLoadBalancingStatistics = blockForestParameters.getParameter< bool >("printLoadBalancingStatistics"); + + // read domain parameters from parameter file + auto domainParameters = walberlaEnv.config()->getOneBlock("DomainParameters"); + const real_t columnRadius = domainParameters.getParameter< real_t >("columnRadius"); + const real_t columnRatio = domainParameters.getParameter< real_t >("columnRatio"); + const real_t columnHeight = columnRadius * columnRatio; + + // define domain size + Vector3< uint_t > domainSize; + domainSize[0] = uint_c(real_c(12) * columnRadius); + domainSize[1] = uint_c(real_c(2) * columnHeight); + domainSize[2] = domainSize[0]; + + // compute number of blocks as defined by domainSize and cellsPerBlock + Vector3< uint_t > numBlocks; + numBlocks[0] = uint_c(std::ceil(real_c(domainSize[0]) / real_c(cellsPerBlock[0]))); + numBlocks[1] = uint_c(std::ceil(real_c(domainSize[1]) / real_c(cellsPerBlock[1]))); + numBlocks[2] = uint_c(std::ceil(real_c(domainSize[2]) / real_c(cellsPerBlock[2]))); + + uint_t numProcesses = uint_c(MPIManager::instance()->numProcesses()); + WALBERLA_CHECK_LESS_EQUAL(numProcesses, numBlocks[0] * numBlocks[1] * numBlocks[2], + "The number of MPI processes is greater than the number of blocks as defined by " + "\"domainSize/cellsPerBlock\". This would result in unused MPI processes. Either decrease " + "the number of MPI processes or increase \"cellsPerBlock\".") + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(numProcesses); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellsPerBlock); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(domainSize); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(numBlocks); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(columnRadius); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(columnHeight); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(columnRatio); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(periodicity); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(loadBalancingFrequency); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(printLoadBalancingStatistics); + + // read physics parameters from parameter file + auto physicsParameters = walberlaEnv.config()->getOneBlock("PhysicsParameters"); + const uint_t timesteps = physicsParameters.getParameter< uint_t >("timesteps"); + + // Galilei number: Ga = density * force * L^3 / kinematicViscosity^2 + const real_t galileiNumber = physicsParameters.getParameter< real_t >("galileiNumber"); + + // Bond (Eötvös) number: Bo = (density_liquid - density_gas) * force * L^2 / surfaceTension + const real_t bondNumber = physicsParameters.getParameter< real_t >("bondNumber"); + + const real_t relaxationRate = physicsParameters.getParameter< real_t >("relaxationRate"); + const real_t viscosity = (real_c(1) / relaxationRate - real_c(0.5)) / real_c(3); + const real_t forceY = galileiNumber * viscosity * viscosity / real_c(std::pow(columnRadius, real_c(3))); + const Vector3< real_t > force(real_c(0), -forceY, real_c(0)); + const real_t surfaceTension = std::abs(forceY) * columnRadius * columnRadius / bondNumber; + + const bool enableWetting = physicsParameters.getParameter< bool >("enableWetting"); + const real_t contactAngle = physicsParameters.getParameter< real_t >("contactAngle"); + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(galileiNumber); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(bondNumber); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(relaxationRate); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableWetting); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(contactAngle); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(timesteps); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(viscosity); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(force); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(surfaceTension); + + // read model parameters from parameter file + const auto modelParameters = walberlaEnv.config()->getOneBlock("ModelParameters"); + const std::string pdfReconstructionModel = modelParameters.getParameter< std::string >("pdfReconstructionModel"); + const std::string pdfRefillingModel = modelParameters.getParameter< std::string >("pdfRefillingModel"); + const std::string excessMassDistributionModel = + modelParameters.getParameter< std::string >("excessMassDistributionModel"); + const std::string curvatureModel = modelParameters.getParameter< std::string >("curvatureModel"); + const bool enableForceWeighting = modelParameters.getParameter< bool >("enableForceWeighting"); + const bool useSimpleMassExchange = modelParameters.getParameter< bool >("useSimpleMassExchange"); + const real_t cellConversionThreshold = modelParameters.getParameter< real_t >("cellConversionThreshold"); + const real_t cellConversionForceThreshold = modelParameters.getParameter< real_t >("cellConversionForceThreshold"); + const bool enableBubbleModel = modelParameters.getParameter< bool >("enableBubbleModel"); + const bool enableBubbleSplits = modelParameters.getParameter< bool >("enableBubbleSplits"); + const real_t smagorinskyConstant = modelParameters.getParameter< real_t >("smagorinskyConstant"); + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(pdfReconstructionModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(pdfRefillingModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(excessMassDistributionModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(curvatureModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableForceWeighting); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(useSimpleMassExchange); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellConversionThreshold); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellConversionForceThreshold); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableBubbleModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableBubbleSplits); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(smagorinskyConstant); + + // read evaluation parameters from parameter file + const auto evaluationParameters = walberlaEnv.config()->getOneBlock("EvaluationParameters"); + const uint_t performanceLogFrequency = evaluationParameters.getParameter< uint_t >("performanceLogFrequency"); + const uint_t evaluationFrequency = evaluationParameters.getParameter< uint_t >("evaluationFrequency"); + const std::string filename = evaluationParameters.getParameter< std::string >("filename"); + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(performanceLogFrequency); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(evaluationFrequency); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(filename); + + // create non-uniform block forest (non-uniformity required for load balancing) + const std::shared_ptr< StructuredBlockForest > blockForest = + createNonUniformBlockForest(domainSize, cellsPerBlock, numBlocks, periodicity); + + // add relaxationRate field (initialized with relaxationRate from parameter file) + const BlockDataID relaxationRateFieldID = field::addToStorage< ScalarField_T >( + blockForest, "Relaxation rate field", relaxationRate, field::fzyx, uint_c(1)); + + const CollisionModel_T collisionModel = lbm::collision_model::SRTField< ScalarField_T >(relaxationRateFieldID); + + // add force field + const BlockDataID forceFieldID = + field::addToStorage< VectorField_T >(blockForest, "Force field", force, field::fzyx, uint_c(1)); + + // create lattice model + const LatticeModel_T latticeModel = + LatticeModel_T(collisionModel, lbm::force_model::GuoField< VectorField_T >(forceFieldID)); + + // add pdf field + const BlockDataID pdfFieldID = lbm::addPdfFieldToStorage(blockForest, "PDF field", latticeModel, field::fzyx); + + // add fill level field (initialized with 0, i.e., gas everywhere) + const BlockDataID fillFieldID = + field::addToStorage< ScalarField_T >(blockForest, "Fill level field", real_c(0.0), field::fzyx, uint_c(2)); + + // add boundary handling + const std::shared_ptr< FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling = + std::make_shared< FreeSurfaceBoundaryHandling_T >(blockForest, pdfFieldID, fillFieldID); + const BlockDataID flagFieldID = freeSurfaceBoundaryHandling->getFlagFieldID(); + const typename FreeSurfaceBoundaryHandling_T::FlagInfo_T& flagInfo = freeSurfaceBoundaryHandling->getFlagInfo(); + + const geometry::Cylinder cylinderColumn( + Vector3< real_t >(real_c(0.5) * real_c(domainSize[0]), real_c(-1), real_c(0.5) * real_c(domainSize[2])), + Vector3< real_t >(real_c(0.5) * real_c(domainSize[0]), columnHeight, real_c(0.5) * real_c(domainSize[2])), + columnRadius); + bubble_model::addBodyToFillLevelField< geometry::Cylinder >(*blockForest, fillFieldID, cylinderColumn, false); + + // initialize boundary conditions from config file + const auto boundaryParameters = walberlaEnv.config()->getOneBlock("BoundaryParameters"); + freeSurfaceBoundaryHandling->initFromConfig(boundaryParameters); + + // IMPORTANT REMARK: this must be only called after every solid flag has been set; otherwise, the boundary handling + // might not detect solid flags correctly + freeSurfaceBoundaryHandling->initFlagsFromFillLevel(); + + // communication after initialization + Communication_T communication(blockForest, flagFieldID, fillFieldID, forceFieldID); + communication(); + + PdfCommunication_T pdfCommunication(blockForest, pdfFieldID); + pdfCommunication(); + + // add bubble model + std::shared_ptr< bubble_model::BubbleModelBase > bubbleModel = nullptr; + if (enableBubbleModel) + { + const std::shared_ptr< bubble_model::BubbleModel< CommunicationStencil_T > > bubbleModelDerived = + std::make_shared< bubble_model::BubbleModel< CommunicationStencil_T > >(blockForest, enableBubbleSplits); + bubbleModelDerived->initFromFillLevelField(fillFieldID); + bubbleModelDerived->setAtmosphere( + Cell(domainSize[0] - uint_c(1), domainSize[1] - uint_c(1), domainSize[2] - uint_c(1)), real_c(1)); + + bubbleModel = std::static_pointer_cast< bubble_model::BubbleModelBase >(bubbleModelDerived); + } + else { bubbleModel = std::make_shared< bubble_model::BubbleModelConstantPressure >(real_c(1)); } + + // initialize hydrostatic pressure + initHydrostaticPressure< PdfField_T >(blockForest, pdfFieldID, force, columnHeight); + + // set density in non-liquid or non-interface cells to 1 (after initializing with hydrostatic pressure) + setDensityInNonFluidCellsToOne< FlagField_T, PdfField_T >(blockForest, flagInfo, flagFieldID, pdfFieldID); + + // create timeloop + SweepTimeloop timeloop(blockForest, timesteps); + + // Laplace pressure = 2 * surface tension * curvature; curvature computation is not necessary with 0 surface tension + bool computeCurvature = false; + if (!realIsEqual(surfaceTension, real_c(0), real_c(1e-14))) { computeCurvature = true; } + + // add surface geometry handler + const SurfaceGeometryHandler< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > geometryHandler( + blockForest, freeSurfaceBoundaryHandling, fillFieldID, curvatureModel, computeCurvature, enableWetting, + contactAngle); + + geometryHandler.addSweeps(timeloop); + + const ConstBlockDataID curvatureFieldID = geometryHandler.getConstCurvatureFieldID(); + const ConstBlockDataID normalFieldID = geometryHandler.getConstNormalFieldID(); + + // add boundary handling for standard boundaries and free surface boundaries + const SurfaceDynamicsHandler< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > dynamicsHandler( + blockForest, pdfFieldID, flagFieldID, fillFieldID, forceFieldID, normalFieldID, curvatureFieldID, + freeSurfaceBoundaryHandling, bubbleModel, pdfReconstructionModel, pdfRefillingModel, excessMassDistributionModel, + relaxationRate, force, surfaceTension, enableForceWeighting, useSimpleMassExchange, cellConversionThreshold, + cellConversionForceThreshold, relaxationRateFieldID, smagorinskyConstant); + + dynamicsHandler.addSweeps(timeloop); + + // add load balancing + LoadBalancer< FlagField_T, CommunicationStencil_T, LatticeModelStencil_T > loadBalancer( + blockForest, communication, pdfCommunication, bubbleModel, uint_c(50), uint_c(10), uint_c(5), + loadBalancingFrequency, printLoadBalancingStatistics); + timeloop.addFuncAfterTimeStep(loadBalancer, "Sweep: load balancing"); + + // add sweep for evaluating the column height at the origin + const std::shared_ptr< cell_idx_t > currentColumnHeight = std::make_shared< cell_idx_t >(columnHeight); + const ColumnHeightEvaluator< FreeSurfaceBoundaryHandling_T > heightEvaluator( + blockForest, freeSurfaceBoundaryHandling, domainSize, + Vector3< real_t >(real_c(0.5) * real_c(domainSize[0]), real_c(0), real_c(0.5) * real_c(domainSize[2])), + evaluationFrequency, currentColumnHeight); + timeloop.addFuncAfterTimeStep(heightEvaluator, "Evaluator: column height"); + + // add sweep for evaluating the column width (distance of front to origin) + const std::shared_ptr< math::Sample > columnRadiusSample = std::make_shared< math::Sample >(); + const columnRadiusEvaluator< FreeSurfaceBoundaryHandling_T > columnRadiusEvaluator( + blockForest, freeSurfaceBoundaryHandling, domainSize, + Vector3< real_t >(real_c(0.5) * real_c(domainSize[0]), real_c(0), real_c(0.5) * real_c(domainSize[2])), + evaluationFrequency, columnRadiusSample); + timeloop.addFuncAfterTimeStep(columnRadiusEvaluator, "Evaluator: radius"); + + // add VTK output + addVTKOutput< LatticeModel_T, FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T, VectorField_T >( + blockForest, timeloop, walberlaEnv.config(), flagInfo, pdfFieldID, flagFieldID, fillFieldID, forceFieldID, + geometryHandler.getCurvatureFieldID(), geometryHandler.getNormalFieldID(), + geometryHandler.getObstNormalFieldID()); + + // add triangle mesh output of free surface + SurfaceMeshWriter< ScalarField_T, FlagField_T > surfaceMeshWriter( + blockForest, fillFieldID, flagFieldID, flagIDs::liquidInterfaceGasFlagIDs, real_c(0), walberlaEnv.config()); + surfaceMeshWriter(); // write initial mesh + timeloop.addFuncAfterTimeStep(surfaceMeshWriter, "Writer: surface mesh"); + + // add logging for computational performance + const lbm::PerformanceLogger< FlagField_T > perfLogger(blockForest, flagFieldID, flagIDs::liquidInterfaceFlagIDs, + performanceLogFrequency); + timeloop.addFuncAfterTimeStep(perfLogger, "Evaluator: performance logging"); + + WcTimingPool timingPool; + + for (uint_t t = uint_c(0); t != timesteps; ++t) + { + timeloop.singleStep(timingPool, true); + + if (t % evaluationFrequency == uint_c(0)) + { + // set initial values + real_t T = real_c(0); + real_t Z_mean = real_c(1); + real_t Z_max = real_c(1); + real_t Z_min = real_c(1); + real_t Z_stdDeviation = real_c(0); + real_t H = real_c(1); + + if (!columnRadiusSample->empty()) + { + // compute dimensionless quantities as defined in paper from Martin and Moyce (1952) + T = real_c(t) * std::sqrt(columnRatio * std::abs(force[1]) / columnRadius); + Z_mean = columnRadiusSample->mean() / columnRadius; + Z_max = columnRadiusSample->max() / columnRadius; + Z_min = columnRadiusSample->min() / columnRadius; + Z_stdDeviation = columnRadiusSample->stdDeviation() / columnRadius; + H = real_c(*currentColumnHeight) / columnHeight; + } + + WALBERLA_LOG_DEVEL_ON_ROOT("time step =" << t); + WALBERLA_LOG_DEVEL_ON_ROOT("\t\tT = " << T << "\n\t\tZ_mean = " << Z_mean << "\n\t\tZ_max = " << Z_max + << "\n\t\tZ_min = " << Z_min + << "\n\t\tZ_stdDeviation = " << Z_stdDeviation << "\n\t\tH = " << H); + + WALBERLA_ROOT_SECTION() + { + const std::vector< real_t > resultVector{ T, Z_mean, Z_max, Z_min, Z_stdDeviation, H }; + writeNumberVector(resultVector, t, filename); + } + + mpi::broadcastObject(Z_max, 0); + + // simulation is considered converged + if (Z_max >= real_c(domainSize[0]) * real_c(0.5) / columnRadius - real_c(0.5)) + { + WALBERLA_LOG_DEVEL_ON_ROOT("Liquid has reached domain borders."); + break; + } + } + + if (t % performanceLogFrequency == uint_c(0) && t > uint_c(0)) { timingPool.logResultOnRoot(); } + } + + return EXIT_SUCCESS; +} +} // namespace DamBreakCylindrical +} // namespace free_surface +} // namespace walberla + +int main(int argc, char** argv) { return walberla::free_surface::DamBreakCylindrical::main(argc, argv); } \ No newline at end of file diff --git a/apps/showcases/FreeSurface/DamBreakCylindrical.prm b/apps/showcases/FreeSurface/DamBreakCylindrical.prm new file mode 100644 index 000000000..de81d688e --- /dev/null +++ b/apps/showcases/FreeSurface/DamBreakCylindrical.prm @@ -0,0 +1,111 @@ +BlockForestParameters +{ + cellsPerBlock < 10, 5, 10 >; + periodicity < 1, 0, 1 >; + loadBalancingFrequency 248; + printLoadBalancingStatistics true; +} + +DomainParameters +{ + columnRadius 25; // initial radius of the liquid column; "a" in Martin & Moyce's paper (10.1098/rsta.1952.0006) + columnRatio 1; // ratio between columnHeight and columnRadius; "n^2" in Martin & Moyce's paper +} + +PhysicsParameters +{ + galileiNumber 1831123817; + bondNumber 445; + relaxationRate 1.9995; + enableWetting false; + contactAngle 0; // only used if enableWetting=true + timesteps 2480; +} + +ModelParameters +{ + pdfReconstructionModel OnlyMissing; + pdfRefillingModel EquilibriumRefilling; + excessMassDistributionModel EvenlyAllInterface; + curvatureModel FiniteDifferenceMethod; + enableForceWeighting false; + useSimpleMassExchange false; + cellConversionThreshold 1e-2; + cellConversionForceThreshold 1e-1; + + enableBubbleModel false; + enableBubbleSplits false; // only used if enableBubbleModel=true + + smagorinskyConstant 0.1; +} + +EvaluationParameters +{ + performanceLogFrequency 2480; + evaluationFrequency 24; + filename rot-breaking-dam.txt; +} + +BoundaryParameters +{ + // X + //Border { direction W; walldistance -1; FreeSlip{} } + //Border { direction E; walldistance -1; FreeSlip{} } + + // Y + Border { direction N; walldistance -1; FreeSlip{} } + Border { direction S; walldistance -1; FreeSlip{} } + + // Z + //Border { direction T; walldistance -1; FreeSlip{} } + //Border { direction B; walldistance -1; FreeSlip{} } +} + +MeshOutputParameters +{ + writeFrequency 248; + baseFolder mesh-out; +} + +VTK +{ + fluid_field + { + writeFrequency 248; + ghostLayers 0; + baseFolder vtk-out; + samplingResolution 1; + + writers + { + velocity; + density; + //pdf; + flag; + fill_level; + force; + curvature; + normal; + obstacle_normal; + mapped_flag; + } + + inclusion_filters + { + // only include liquid and interface cells in VTK output + //liquidInterfaceFilter; + } + + before_functions + { + //ghost_layer_synchronization; // only needed if writing the ghost layer + } + + } + domain_decomposition + { + writeFrequency 248; + baseFolder vtk-out; + outputDomainDecomposition true; + } +} \ No newline at end of file diff --git a/apps/showcases/FreeSurface/DamBreakRectangular.cpp b/apps/showcases/FreeSurface/DamBreakRectangular.cpp new file mode 100644 index 000000000..e3a4a5e4e --- /dev/null +++ b/apps/showcases/FreeSurface/DamBreakRectangular.cpp @@ -0,0 +1,570 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file DamBreakRectangular.cpp +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +// +// This showcase simulates the collapse of a rectangular liquid column in 2D. Reference experiments are available from +// Martin, Moyce (1952), doi:10.1098/rsta.1952.0006 +//====================================================================================================================== + +#include "blockforest/Initialization.h" + +#include "core/Environment.h" + +#include "lbm/PerformanceLogger.h" +#include "lbm/field/AddToStorage.h" +#include "lbm/free_surface/LoadBalancing.h" +#include "lbm/free_surface/SurfaceMeshWriter.h" +#include "lbm/free_surface/TotalMassComputer.h" +#include "lbm/free_surface/VtkWriter.h" +#include "lbm/free_surface/bubble_model/Geometry.h" +#include "lbm/free_surface/dynamics/SurfaceDynamicsHandler.h" +#include "lbm/free_surface/surface_geometry/SurfaceGeometryHandler.h" +#include "lbm/free_surface/surface_geometry/Utility.h" +#include "lbm/lattice_model/D2Q9.h" +#include "lbm/lattice_model/D3Q19.h" + +namespace walberla +{ +namespace free_surface +{ +namespace DamBreakRectangular +{ +using ScalarField_T = GhostLayerField< real_t, 1 >; +using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >; + +using CollisionModel_T = lbm::collision_model::SRTField< ScalarField_T >; +using ForceModel_T = lbm::force_model::GuoField< VectorField_T >; +using LatticeModel_T = lbm::D2Q9< CollisionModel_T, true, ForceModel_T, 2 >; +using LatticeModelStencil_T = LatticeModel_T::Stencil; +using PdfField_T = lbm::PdfField< LatticeModel_T >; +using PdfCommunication_T = blockforest::SimpleCommunication< LatticeModelStencil_T >; + +// the geometry computations in SurfaceGeometryHandler require meaningful values in the ghost layers in corner +// directions (flag field and fill level field); this holds, even if the lattice model uses a D3Q19 stencil +using CommunicationStencil_T = + typename std::conditional< LatticeModel_T::Stencil::D == uint_t(2), stencil::D2Q9, stencil::D3Q27 >::type; +using Communication_T = blockforest::SimpleCommunication< CommunicationStencil_T >; + +using flag_t = uint32_t; +using FlagField_T = FlagField< flag_t >; +using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >; + +template< typename T > +void writeNumberVector(const std::vector< T >& numberVector, const uint_t& timestep, const std::string& filename) +{ + std::fstream file; + file.open(filename, std::fstream::app); + + file << timestep; + for (const auto number : numberVector) + { + file << "\t" << number; + } + + file << "\n"; + file.close(); +} + +// get height of residual liquid column, i.e., height of liquid at x=0 +template< typename FreeSurfaceBoundaryHandling_T > +class ColumnHeightEvaluator +{ + public: + ColumnHeightEvaluator(const std::weak_ptr< const StructuredBlockForest >& blockForest, + const std::weak_ptr< const FreeSurfaceBoundaryHandling_T >& freeSurfaceBoundaryHandling, + const Vector3< uint_t >& domainSize, uint_t interval, + const std::shared_ptr< cell_idx_t >& currentColumnHeight) + : blockForest_(blockForest), freeSurfaceBoundaryHandling_(freeSurfaceBoundaryHandling), domainSize_(domainSize), + currentColumnHeight_(currentColumnHeight), interval_(interval), executionCounter_(uint_c(0)) + {} + + void operator()() + { + auto blockForest = blockForest_.lock(); + WALBERLA_CHECK_NOT_NULLPTR(blockForest); + + auto freeSurfaceBoundaryHandling = freeSurfaceBoundaryHandling_.lock(); + WALBERLA_CHECK_NOT_NULLPTR(freeSurfaceBoundaryHandling); + + ++executionCounter_; + + // only evaluate in given intervals + if (executionCounter_ % interval_ != uint_c(0)) { return; } + + const BlockDataID flagFieldID = freeSurfaceBoundaryHandling->getFlagFieldID(); + const typename FreeSurfaceBoundaryHandling_T::FlagInfo_T& flagInfo = freeSurfaceBoundaryHandling->getFlagInfo(); + + *currentColumnHeight_ = cell_idx_c(0); + + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + cell_idx_t maxColumnHeight = cell_idx_c(0); + bool isInterfaceFound = false; + + const CellInterval globalSearchInterval(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0), cell_idx_c(0), + cell_idx_c(domainSize_[1]), cell_idx_c(0)); + + if (blockForest->getBlockCellBB(*blockIt).overlaps(globalSearchInterval)) + { + CellInterval localSearchInterval = globalSearchInterval; + + // get intersection of globalSearchInterval and this block's bounding box (both in global coordinates) + localSearchInterval.intersect(blockForest->getBlockCellBB(*blockIt)); + + blockForest->transformGlobalToBlockLocalCellInterval(localSearchInterval, *blockIt); + + const FlagField_T* const flagField = blockIt->template getData< const FlagField_T >(flagFieldID); + + for (auto c = localSearchInterval.begin(); c != localSearchInterval.end(); ++c) + { + if (flagInfo.isInterface(flagField->get(*c))) + { + if (c->y() >= maxColumnHeight) + { + maxColumnHeight = c->y(); + isInterfaceFound = true; + } + } + } + + if (isInterfaceFound) + { + // transform local y-coordinate to global coordinate + Cell localResultCell = Cell(cell_idx_c(0), maxColumnHeight, cell_idx_c(0)); + blockForest->transformBlockLocalToGlobalCell(localResultCell, *blockIt); + if (localResultCell[1] > *currentColumnHeight_) { *currentColumnHeight_ = localResultCell[1]; } + } + } + } + mpi::allReduceInplace< cell_idx_t >(*currentColumnHeight_, mpi::MAX); + } + + private: + std::weak_ptr< const StructuredBlockForest > blockForest_; + std::weak_ptr< const FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling_; + Vector3< uint_t > domainSize_; + std::shared_ptr< cell_idx_t > currentColumnHeight_; + + uint_t interval_; + uint_t executionCounter_; +}; // class ColumnHeightEvaluator + +// get width of liquid column (distance of wave front to origin, at bottom of the domain) +template< typename FreeSurfaceBoundaryHandling_T > +class ColumnWidthEvaluator +{ + public: + ColumnWidthEvaluator(const std::weak_ptr< const StructuredBlockForest >& blockForest, + const std::weak_ptr< const FreeSurfaceBoundaryHandling_T >& freeSurfaceBoundaryHandling, + const Vector3< uint_t >& domainSize, uint_t interval, + const std::shared_ptr< cell_idx_t >& currentColumnWidth) + : blockForest_(blockForest), freeSurfaceBoundaryHandling_(freeSurfaceBoundaryHandling), domainSize_(domainSize), + currentColumnWidth_(currentColumnWidth), interval_(interval), executionCounter_(uint_c(0)) + {} + + void operator()() + { + auto blockForest = blockForest_.lock(); + WALBERLA_CHECK_NOT_NULLPTR(blockForest); + + auto freeSurfaceBoundaryHandling = freeSurfaceBoundaryHandling_.lock(); + WALBERLA_CHECK_NOT_NULLPTR(freeSurfaceBoundaryHandling); + + ++executionCounter_; + + // only evaluate in given intervals + if (executionCounter_ % interval_ != uint_c(0)) { return; } + + const BlockDataID flagFieldID = freeSurfaceBoundaryHandling->getFlagFieldID(); + const typename FreeSurfaceBoundaryHandling_T::FlagInfo_T& flagInfo = freeSurfaceBoundaryHandling->getFlagInfo(); + + *currentColumnWidth_ = cell_idx_c(0); + + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + cell_idx_t maxColumnWidth = cell_idx_c(0); + bool isInterfaceFound = false; + + // only search in an interval with a height of 10 cells to avoid detecting droplets + const CellInterval globalSearchInterval(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0), + cell_idx_c(domainSize_[0]), cell_idx_c(0), cell_idx_c(0)); + + if (blockForest->getBlockCellBB(*blockIt).overlaps(globalSearchInterval)) + { + CellInterval localSearchInterval = globalSearchInterval; + + // get intersection of globalSearchInterval and this block's bounding box (both in global coordinates) + localSearchInterval.intersect(blockForest->getBlockCellBB(*blockIt)); + + blockForest->transformGlobalToBlockLocalCellInterval(localSearchInterval, *blockIt); + + const FlagField_T* const flagField = blockIt->template getData< const FlagField_T >(flagFieldID); + + for (auto c = localSearchInterval.begin(); c != localSearchInterval.end(); ++c) + { + if (flagInfo.isInterface(flagField->get(*c))) + { + if (c->x() >= maxColumnWidth) + { + maxColumnWidth = c->x(); + isInterfaceFound = true; + } + } + } + + if (isInterfaceFound) + { + // transform local x-coordinate to global coordinate + Cell localResultCell = Cell(maxColumnWidth, cell_idx_c(0), cell_idx_c(0)); + blockForest->transformBlockLocalToGlobalCell(localResultCell, *blockIt); + if (localResultCell[0] > *currentColumnWidth_) { *currentColumnWidth_ = localResultCell[0]; } + } + } + } + mpi::allReduceInplace< cell_idx_t >(*currentColumnWidth_, mpi::MAX); + } + + private: + std::weak_ptr< const StructuredBlockForest > blockForest_; + std::weak_ptr< const FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling_; + Vector3< uint_t > domainSize_; + std::shared_ptr< cell_idx_t > currentColumnWidth_; + + uint_t interval_; + uint_t executionCounter_; +}; // class ColumnWidthEvaluator + +int main(int argc, char** argv) +{ + Environment walberlaEnv(argc, argv); + + if (argc < 2) { WALBERLA_ABORT("Please specify a parameter file as input argument.") } + + // print content of parameter file + WALBERLA_LOG_INFO_ON_ROOT(*walberlaEnv.config()); + + // get block forest parameters from parameter file + auto blockForestParameters = walberlaEnv.config()->getOneBlock("BlockForestParameters"); + const Vector3< uint_t > cellsPerBlock = blockForestParameters.getParameter< Vector3< uint_t > >("cellsPerBlock"); + const Vector3< bool > periodicity = blockForestParameters.getParameter< Vector3< bool > >("periodicity"); + const uint_t loadBalancingFrequency = blockForestParameters.getParameter< uint_t >("loadBalancingFrequency"); + const bool printLoadBalancingStatistics = blockForestParameters.getParameter< bool >("printLoadBalancingStatistics"); + + // read domain parameters from parameter file + auto domainParameters = walberlaEnv.config()->getOneBlock("DomainParameters"); + const real_t columnWidth = domainParameters.getParameter< real_t >("columnWidth"); + const real_t columnRatio = domainParameters.getParameter< real_t >("columnRatio"); + const real_t columnHeight = columnWidth * columnRatio; + + // define domain size + Vector3< uint_t > domainSize; + domainSize[0] = uint_c(real_c(15) * columnWidth); + domainSize[1] = uint_c(real_c(2) * columnHeight); + domainSize[2] = uint_c(1); + + // compute number of blocks as defined by domainSize and cellsPerBlock + Vector3< uint_t > numBlocks; + numBlocks[0] = uint_c(std::ceil(real_c(domainSize[0]) / real_c(cellsPerBlock[0]))); + numBlocks[1] = uint_c(std::ceil(real_c(domainSize[1]) / real_c(cellsPerBlock[1]))); + numBlocks[2] = uint_c(std::ceil(real_c(domainSize[2]) / real_c(cellsPerBlock[2]))); + + // get number of (MPI) processes + uint_t numProcesses = uint_c(MPIManager::instance()->numProcesses()); + WALBERLA_CHECK_LESS_EQUAL(numProcesses, numBlocks[0] * numBlocks[1] * numBlocks[2], + "The number of MPI processes is greater than the number of blocks as defined by " + "\"domainSize/cellsPerBlock\". This would result in unused MPI processes. Either decrease " + "the number of MPI processes or increase \"cellsPerBlock\".") + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(numProcesses); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellsPerBlock); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(domainSize); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(numBlocks); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(columnWidth); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(columnHeight); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(columnRatio); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(periodicity); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(loadBalancingFrequency); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(printLoadBalancingStatistics); + + // read physics parameters from parameter file + auto physicsParameters = walberlaEnv.config()->getOneBlock("PhysicsParameters"); + const uint_t timesteps = physicsParameters.getParameter< uint_t >("timesteps"); + + // Galilei number: Ga = density * force * L^3 / kinematicViscosity^2 + const real_t galileiNumber = physicsParameters.getParameter< real_t >("galileiNumber"); + + // Bond (Eötvös) number: Bo = (density_liquid - density_gas) * force * L^2 / surfaceTension + const real_t bondNumber = physicsParameters.getParameter< real_t >("bondNumber"); + + const real_t relaxationRate = physicsParameters.getParameter< real_t >("relaxationRate"); + const real_t viscosity = (real_c(1) / relaxationRate - real_c(0.5)) / real_c(3); + const real_t forceY = galileiNumber * viscosity * viscosity / real_c(std::pow(columnWidth, real_c(3))); + const Vector3< real_t > force(real_c(0), -forceY, real_c(0)); + const real_t surfaceTension = std::abs(forceY) * columnWidth * columnWidth / bondNumber; + + const bool enableWetting = physicsParameters.getParameter< bool >("enableWetting"); + const real_t contactAngle = physicsParameters.getParameter< real_t >("contactAngle"); + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(galileiNumber); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(bondNumber); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(relaxationRate); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableWetting); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(contactAngle); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(timesteps); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(viscosity); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(force); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(surfaceTension); + + // read model parameters from parameter file + const auto modelParameters = walberlaEnv.config()->getOneBlock("ModelParameters"); + const std::string pdfReconstructionModel = modelParameters.getParameter< std::string >("pdfReconstructionModel"); + const std::string pdfRefillingModel = modelParameters.getParameter< std::string >("pdfRefillingModel"); + const std::string excessMassDistributionModel = + modelParameters.getParameter< std::string >("excessMassDistributionModel"); + const std::string curvatureModel = modelParameters.getParameter< std::string >("curvatureModel"); + const bool enableForceWeighting = modelParameters.getParameter< bool >("enableForceWeighting"); + const bool useSimpleMassExchange = modelParameters.getParameter< bool >("useSimpleMassExchange"); + const real_t cellConversionThreshold = modelParameters.getParameter< real_t >("cellConversionThreshold"); + const real_t cellConversionForceThreshold = modelParameters.getParameter< real_t >("cellConversionForceThreshold"); + const bool enableBubbleModel = modelParameters.getParameter< bool >("enableBubbleModel"); + const bool enableBubbleSplits = modelParameters.getParameter< bool >("enableBubbleSplits"); + const real_t smagorinskyConstant = modelParameters.getParameter< real_t >("smagorinskyConstant"); + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(pdfReconstructionModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(pdfRefillingModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(excessMassDistributionModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(curvatureModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableForceWeighting); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(useSimpleMassExchange); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellConversionThreshold); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellConversionForceThreshold); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableBubbleModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableBubbleSplits); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(smagorinskyConstant); + + // read evaluation parameters from parameter file + const auto evaluationParameters = walberlaEnv.config()->getOneBlock("EvaluationParameters"); + const uint_t performanceLogFrequency = evaluationParameters.getParameter< uint_t >("performanceLogFrequency"); + const uint_t evaluationFrequency = evaluationParameters.getParameter< uint_t >("evaluationFrequency"); + const std::string filename = evaluationParameters.getParameter< std::string >("filename"); + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(performanceLogFrequency); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(evaluationFrequency); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(filename); + + // create non-uniform block forest (non-uniformity required for load balancing) + const std::shared_ptr< StructuredBlockForest > blockForest = + createNonUniformBlockForest(domainSize, cellsPerBlock, numBlocks, periodicity); + + // add relaxationRate field (initialized with relaxationRate from parameter file) + const BlockDataID relaxationRateFieldID = field::addToStorage< ScalarField_T >( + blockForest, "Relaxation rate field", relaxationRate, field::fzyx, uint_c(1)); + + const CollisionModel_T collisionModel = lbm::collision_model::SRTField< ScalarField_T >(relaxationRateFieldID); + + // add force field + const BlockDataID forceFieldID = + field::addToStorage< VectorField_T >(blockForest, "Force field", force, field::fzyx, uint_c(1)); + + // create lattice model + const LatticeModel_T latticeModel = LatticeModel_T(collisionModel, ForceModel_T(forceFieldID)); + + // add pdf field + const BlockDataID pdfFieldID = lbm::addPdfFieldToStorage(blockForest, "PDF field", latticeModel, field::fzyx); + + // add fill level field (initialized with 0, i.e., gas everywhere) + const BlockDataID fillFieldID = + field::addToStorage< ScalarField_T >(blockForest, "Fill level field", real_c(0.0), field::fzyx, uint_c(2)); + + // add boundary handling + const std::shared_ptr< FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling = + std::make_shared< FreeSurfaceBoundaryHandling_T >(blockForest, pdfFieldID, fillFieldID); + const BlockDataID flagFieldID = freeSurfaceBoundaryHandling->getFlagFieldID(); + const typename FreeSurfaceBoundaryHandling_T::FlagInfo_T& flagInfo = freeSurfaceBoundaryHandling->getFlagInfo(); + + // initialize rectangular column of liquid + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + ScalarField_T* const fillField = blockIt->getData< ScalarField_T >(fillFieldID); + + // cell of the liquid column's outermost corner in block-local coordinates + Cell localColumnCornerCell = + Cell(cell_idx_c(std::floor(columnWidth)), cell_idx_c(std::floor(columnHeight)), cell_idx_c(0)); + + blockForest->transformGlobalToBlockLocalCell(localColumnCornerCell, *blockIt); + + WALBERLA_FOR_ALL_CELLS(fillFieldIt, fillField, { + // liquid cells + if (fillFieldIt.x() < localColumnCornerCell[0] && fillFieldIt.y() < localColumnCornerCell[1]) + { + *fillFieldIt = real_c(1); + } + + // interface cells at side + if (fillFieldIt.x() == localColumnCornerCell[0] && fillFieldIt.y() < localColumnCornerCell[1]) + { + *fillFieldIt = columnWidth - std::floor(columnWidth); + } + + // interface cells at top + if (fillFieldIt.y() == localColumnCornerCell[1] && fillFieldIt.x() < localColumnCornerCell[0]) + { + *fillFieldIt = columnHeight - std::floor(columnHeight); + } + + // interface cell in corner + if (fillFieldIt.x() == localColumnCornerCell[0] && fillFieldIt.y() == localColumnCornerCell[1]) + { + *fillFieldIt = + real_c(0.5) * ((columnWidth - std::floor(columnWidth)) + (columnHeight - std::floor(columnHeight))); + } + }) // WALBERLA_FOR_ALL_CELLS + } + + // initialize boundary conditions from config file + const auto boundaryParameters = walberlaEnv.config()->getOneBlock("BoundaryParameters"); + freeSurfaceBoundaryHandling->initFromConfig(boundaryParameters); + + // IMPORTANT REMARK: this must be only called after every solid flag has been set; otherwise, the boundary handling + // might not detect solid flags correctly + freeSurfaceBoundaryHandling->initFlagsFromFillLevel(); + + // communication after initialization + Communication_T communication(blockForest, flagFieldID, fillFieldID, forceFieldID); + communication(); + + PdfCommunication_T pdfCommunication(blockForest, pdfFieldID); + pdfCommunication(); + + // add bubble model + std::shared_ptr< bubble_model::BubbleModelBase > bubbleModel = nullptr; + if (enableBubbleModel) + { + const std::shared_ptr< bubble_model::BubbleModel< CommunicationStencil_T > > bubbleModelDerived = + std::make_shared< bubble_model::BubbleModel< CommunicationStencil_T > >(blockForest, enableBubbleSplits); + bubbleModelDerived->initFromFillLevelField(fillFieldID); + bubbleModelDerived->setAtmosphere(Cell(domainSize[0] - uint_c(1), domainSize[1] - uint_c(1), uint_c(0)), + real_c(1)); + + bubbleModel = std::static_pointer_cast< bubble_model::BubbleModelBase >(bubbleModelDerived); + } + else { bubbleModel = std::make_shared< bubble_model::BubbleModelConstantPressure >(real_c(1)); } + + // initialize hydrostatic pressure + initHydrostaticPressure< PdfField_T >(blockForest, pdfFieldID, force, columnHeight); + + // set density in non-liquid or non-interface cells to 1 (after initializing with hydrostatic pressure) + setDensityInNonFluidCellsToOne< FlagField_T, PdfField_T >(blockForest, flagInfo, flagFieldID, pdfFieldID); + + // create timeloop + SweepTimeloop timeloop(blockForest, timesteps); + + // Laplace pressure = 2 * surface tension * curvature; curvature computation is not necessary with 0 surface tension + bool computeCurvature = false; + if (!realIsEqual(surfaceTension, real_c(0), real_c(1e-14))) { computeCurvature = true; } + + // add surface geometry handler + const SurfaceGeometryHandler< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > geometryHandler( + blockForest, freeSurfaceBoundaryHandling, fillFieldID, curvatureModel, computeCurvature, enableWetting, + contactAngle); + + geometryHandler.addSweeps(timeloop); + + const ConstBlockDataID curvatureFieldID = geometryHandler.getConstCurvatureFieldID(); + const ConstBlockDataID normalFieldID = geometryHandler.getConstNormalFieldID(); + + // add boundary handling for standard boundaries and free surface boundaries + const SurfaceDynamicsHandler< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > dynamicsHandler( + blockForest, pdfFieldID, flagFieldID, fillFieldID, forceFieldID, normalFieldID, curvatureFieldID, + freeSurfaceBoundaryHandling, bubbleModel, pdfReconstructionModel, pdfRefillingModel, excessMassDistributionModel, + relaxationRate, force, surfaceTension, enableForceWeighting, useSimpleMassExchange, cellConversionThreshold, + cellConversionForceThreshold, relaxationRateFieldID, smagorinskyConstant); + + dynamicsHandler.addSweeps(timeloop); + + // add load balancing + const LoadBalancer< FlagField_T, CommunicationStencil_T, LatticeModelStencil_T > loadBalancer( + blockForest, communication, pdfCommunication, bubbleModel, uint_c(50), uint_c(10), uint_c(5), + loadBalancingFrequency, printLoadBalancingStatistics); + timeloop.addFuncAfterTimeStep(loadBalancer, "Sweep: load balancing"); + + // add sweep for evaluating the column height at the origin + const std::shared_ptr< cell_idx_t > currentColumnHeight = std::make_shared< cell_idx_t >(columnHeight); + const ColumnHeightEvaluator< FreeSurfaceBoundaryHandling_T > heightEvaluator( + blockForest, freeSurfaceBoundaryHandling, domainSize, evaluationFrequency, currentColumnHeight); + timeloop.addFuncAfterTimeStep(heightEvaluator, "Evaluator: column height"); + + // add sweep for evaluating the column width (distance of front to origin) + const std::shared_ptr< cell_idx_t > currentColumnWidth = std::make_shared< cell_idx_t >(columnWidth); + const ColumnWidthEvaluator< FreeSurfaceBoundaryHandling_T > widthEvaluator( + blockForest, freeSurfaceBoundaryHandling, domainSize, evaluationFrequency, currentColumnWidth); + timeloop.addFuncAfterTimeStep(widthEvaluator, "Evaluator: column width"); + + // add VTK output + addVTKOutput< LatticeModel_T, FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T, VectorField_T >( + blockForest, timeloop, walberlaEnv.config(), flagInfo, pdfFieldID, flagFieldID, fillFieldID, forceFieldID, + geometryHandler.getCurvatureFieldID(), geometryHandler.getNormalFieldID(), + geometryHandler.getObstNormalFieldID()); + + // add triangle mesh output of free surface + SurfaceMeshWriter< ScalarField_T, FlagField_T > surfaceMeshWriter( + blockForest, fillFieldID, flagFieldID, flagIDs::liquidInterfaceGasFlagIDs, real_c(0), walberlaEnv.config()); + surfaceMeshWriter(); // write initial mesh + timeloop.addFuncAfterTimeStep(surfaceMeshWriter, "Writer: surface mesh"); + + // add logging for computational performance + const lbm::PerformanceLogger< FlagField_T > perfLogger(blockForest, flagFieldID, flagIDs::liquidInterfaceFlagIDs, + performanceLogFrequency); + timeloop.addFuncAfterTimeStep(perfLogger, "Evaluator: performance logging"); + + WcTimingPool timingPool; + + for (uint_t t = uint_c(0); t != timesteps; ++t) + { + timeloop.singleStep(timingPool, true); + + if (t % evaluationFrequency == uint_c(0)) + { + // compute dimensionless quantities as defined in paper from Martin and Moyce (1952) + const real_t T = real_c(t) * std::sqrt(columnRatio * std::abs(force[1]) / columnWidth); + const real_t Z = real_c(*currentColumnWidth) / columnWidth; + const real_t H = real_c(*currentColumnHeight) / columnHeight; + const std::vector< real_t > resultVector{ T, Z, H }; + + WALBERLA_LOG_DEVEL_ON_ROOT("time step =" << t); + WALBERLA_LOG_DEVEL_ON_ROOT("\t\tT = " << T << "\n\t\tZ = " << Z << "\n\t\tH = " << H); + WALBERLA_ROOT_SECTION() { writeNumberVector(resultVector, t, filename); } + + // simulation is considered converged + if (Z >= real_c(domainSize[0]) / columnWidth - real_c(0.5)) + { + WALBERLA_LOG_DEVEL_ON_ROOT("Liquid has reached opposite wall."); + break; + } + } + + if (t % performanceLogFrequency == uint_c(0) && t > uint_c(0)) { timingPool.logResultOnRoot(); } + } + + return EXIT_SUCCESS; +} + +} // namespace DamBreakRectangular +} // namespace free_surface +} // namespace walberla + +int main(int argc, char** argv) { return walberla::free_surface::DamBreakRectangular::main(argc, argv); } \ No newline at end of file diff --git a/apps/showcases/FreeSurface/DamBreakRectangular.prm b/apps/showcases/FreeSurface/DamBreakRectangular.prm new file mode 100644 index 000000000..34fdca4b0 --- /dev/null +++ b/apps/showcases/FreeSurface/DamBreakRectangular.prm @@ -0,0 +1,111 @@ +BlockForestParameters +{ + cellsPerBlock < 50, 50, 1 >; + periodicity < 0, 0, 1 >; + loadBalancingFrequency 991; + printLoadBalancingStatistics true; +} + +DomainParameters +{ + columnWidth 50; // initial width of the liquid column; "a" in Martin & Moyce's paper (10.1098/rsta.1952.0006) + columnRatio 2; // ratio between columnHeight and columnWidth; "n^2" in Martin & Moyce's paper +} + +PhysicsParameters +{ + galileiNumber 1831123817; + bondNumber 445; + relaxationRate 1.9995; + enableWetting false; + contactAngle 0; // only used if enableWetting=true + timesteps 9910; +} + +ModelParameters +{ + pdfReconstructionModel OnlyMissing; + pdfRefillingModel EquilibriumRefilling; + excessMassDistributionModel EvenlyAllInterface; + curvatureModel FiniteDifferenceMethod; + enableForceWeighting false; + useSimpleMassExchange false; + cellConversionThreshold 1e-2; + cellConversionForceThreshold 1e-1; + + enableBubbleModel false; + enableBubbleSplits false; // only used if enableBubbleModel=true + + smagorinskyConstant 0.1; +} + +EvaluationParameters +{ + performanceLogFrequency 25000; + evaluationFrequency 99; + filename breaking-dam.txt; +} + +BoundaryParameters +{ + // X + Border { direction W; walldistance -1; FreeSlip{} } + Border { direction E; walldistance -1; FreeSlip{} } + + // Y + Border { direction N; walldistance -1; FreeSlip{} } + Border { direction S; walldistance -1; FreeSlip{} } + + // Z + //Border { direction T; walldistance -1; FreeSlip{} } + //Border { direction B; walldistance -1; FreeSlip{} } +} + +MeshOutputParameters +{ + writeFrequency 0; + baseFolder mesh-out; +} + +VTK +{ + fluid_field + { + writeFrequency 991; + ghostLayers 0; + baseFolder vtk-out; + samplingResolution 1; + + writers + { + velocity; + density; + //pdf; + flag; + fill_level; + force; + curvature; + normal; + obstacle_normal; + mapped_flag; + } + + inclusion_filters + { + // only include liquid and interface cells in VTK output + //liquidInterfaceFilter; + } + + before_functions + { + //ghost_layer_synchronization; // only needed if writing the ghost layer + } + + } + domain_decomposition + { + writeFrequency 991; + baseFolder vtk-out; + outputDomainDecomposition true; + } +} \ No newline at end of file diff --git a/apps/showcases/FreeSurface/DropImpact.cpp b/apps/showcases/FreeSurface/DropImpact.cpp new file mode 100644 index 000000000..143885a05 --- /dev/null +++ b/apps/showcases/FreeSurface/DropImpact.cpp @@ -0,0 +1,367 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file DropImpact.cpp +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +// +// This showcase simulates the impact of a droplet into a pool of liquid. Reference experiments are available from +// - Wang, Chen (2000), doi:10.1063/1.1287511 (vertical impact) +// - Reijers, Liu, Lohse, Gelderblom (2019), url: http://arxiv.org/abs/1903.08978 (oblique impact) +//====================================================================================================================== + +#include "blockforest/Initialization.h" + +#include "core/Environment.h" + +#include "lbm/PerformanceLogger.h" +#include "lbm/field/AddToStorage.h" +#include "lbm/free_surface/LoadBalancing.h" +#include "lbm/free_surface/SurfaceMeshWriter.h" +#include "lbm/free_surface/VtkWriter.h" +#include "lbm/free_surface/bubble_model/Geometry.h" +#include "lbm/free_surface/dynamics/SurfaceDynamicsHandler.h" +#include "lbm/free_surface/surface_geometry/SurfaceGeometryHandler.h" +#include "lbm/free_surface/surface_geometry/Utility.h" +#include "lbm/lattice_model/D3Q19.h" + +namespace walberla +{ +namespace free_surface +{ +namespace DropImpact +{ +using ScalarField_T = GhostLayerField< real_t, 1 >; +using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >; + +using CollisionModel_T = lbm::collision_model::SRT; +using ForceModel_T = lbm::force_model::GuoField< VectorField_T >; +using LatticeModel_T = lbm::D3Q19< CollisionModel_T, true, ForceModel_T, 2 >; +using LatticeModelStencil_T = LatticeModel_T::Stencil; +using PdfField_T = lbm::PdfField< LatticeModel_T >; +using PdfCommunication_T = blockforest::SimpleCommunication< LatticeModelStencil_T >; + +// the geometry computations in SurfaceGeometryHandler require meaningful values in the ghost layers in corner +// directions (flag field and fill level field); this holds, even if the lattice model uses a D3Q19 stencil +using CommunicationStencil_T = + typename std::conditional< LatticeModel_T::Stencil::D == uint_t(2), stencil::D2Q9, stencil::D3Q27 >::type; +using Communication_T = blockforest::SimpleCommunication< CommunicationStencil_T >; + +using flag_t = uint32_t; +using FlagField_T = FlagField< flag_t >; +using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >; + +int main(int argc, char** argv) +{ + Environment walberlaEnv(argc, argv); + + if (argc < 2) { WALBERLA_ABORT("Please specify a parameter file as input argument.") } + + // print content of parameter file + WALBERLA_LOG_INFO_ON_ROOT(*walberlaEnv.config()); + + // get block forest parameters from parameter file + auto blockForestParameters = walberlaEnv.config()->getOneBlock("BlockForestParameters"); + const Vector3< uint_t > cellsPerBlock = blockForestParameters.getParameter< Vector3< uint_t > >("cellsPerBlock"); + const Vector3< bool > periodicity = blockForestParameters.getParameter< Vector3< bool > >("periodicity"); + const uint_t loadBalancingFrequency = blockForestParameters.getParameter< uint_t >("loadBalancingFrequency"); + const bool printLoadBalancingStatistics = blockForestParameters.getParameter< bool >("printLoadBalancingStatistics"); + + // read domain parameters from parameter file + const auto domainParameters = walberlaEnv.config()->getOneBlock("DomainParameters"); + const real_t dropDiameter = domainParameters.getParameter< real_t >("dropDiameter"); + const Vector3< real_t > dropCenterFactor = domainParameters.getParameter< Vector3< real_t > >("dropCenterFactor"); + const real_t poolHeightFactor = domainParameters.getParameter< real_t >("poolHeightFactor"); + const Vector3< real_t > domainSizeFactor = domainParameters.getParameter< Vector3< real_t > >("domainSizeFactor"); + + // define domain size + Vector3< uint_t > domainSize = domainSizeFactor * dropDiameter; + domainSize[0] = uint_c(domainSizeFactor[0] * dropDiameter); + domainSize[1] = uint_c(domainSizeFactor[1] * dropDiameter); + domainSize[2] = uint_c(domainSizeFactor[2] * dropDiameter); + + // compute number of blocks as defined by domainSize and cellsPerBlock + Vector3< uint_t > numBlocks; + numBlocks[0] = uint_c(std::ceil(real_c(domainSize[0]) / real_c(cellsPerBlock[0]))); + numBlocks[1] = uint_c(std::ceil(real_c(domainSize[1]) / real_c(cellsPerBlock[1]))); + numBlocks[2] = uint_c(std::ceil(real_c(domainSize[2]) / real_c(cellsPerBlock[2]))); + + // get number of (MPI) processes + uint_t numProcesses = uint_c(MPIManager::instance()->numProcesses()); + WALBERLA_CHECK_LESS_EQUAL(numProcesses, numBlocks[0] * numBlocks[1] * numBlocks[2], + "The number of MPI processes is greater than the number of blocks as defined by " + "\"domainSize/cellsPerBlock\". This would result in unused MPI processes. Either decrease " + "the number of MPI processes or increase \"cellsPerBlock\".") + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(numProcesses); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellsPerBlock); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(numBlocks); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(dropDiameter); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(dropCenterFactor); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(poolHeightFactor); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(domainSizeFactor); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(periodicity); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(domainSize); + + Vector3< uint_t > realDomainSize; + realDomainSize[0] = cellsPerBlock[0] * numBlocks[0]; + realDomainSize[1] = cellsPerBlock[1] * numBlocks[1]; + realDomainSize[2] = cellsPerBlock[2] * numBlocks[2]; + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(realDomainSize); + + if (domainSize[0] != realDomainSize[0] && periodicity[0]) + { + WALBERLA_ABORT( + "The specified domain size in x-direction can not be obtained with the number of blocks you specified.") + } + if (domainSize[1] != realDomainSize[1] && periodicity[1]) + { + WALBERLA_ABORT( + "The specified domain size in y-direction can not be obtained with the number of blocks you specified.") + } + // the z-direction must be no slip in this setup and is simply extended (see below) to obtain the specified size + int boundaryThicknessZ = int_c(realDomainSize[2]) - int_c(domainSize[2]); + if (boundaryThicknessZ < 0) + { + WALBERLA_ABORT("Something went wrong: the resulting domain size in z-direction is less than specified.") + } + + // read physics parameters from parameter file + const auto physicsParameters = walberlaEnv.config()->getOneBlock("PhysicsParameters"); + const real_t bondNumber = physicsParameters.getParameter< real_t >("bondNumber"); + const real_t weberNumber = physicsParameters.getParameter< real_t >("weberNumber"); + const real_t ohnesorgeNumber = physicsParameters.getParameter< real_t >("ohnesorgeNumber"); + const real_t relaxationRate = physicsParameters.getParameter< real_t >("relaxationRate"); + const real_t impactAngleDegree = physicsParameters.getParameter< real_t >("impactAngleDegree"); + + const CollisionModel_T collisionModel = CollisionModel_T(relaxationRate); + const real_t viscosity = collisionModel.viscosity(); + + const real_t surfaceTension = real_c(std::pow(viscosity / ohnesorgeNumber, 2)) / dropDiameter; + const real_t gravitationalAccelerationZ = bondNumber * surfaceTension / (dropDiameter * dropDiameter); + const real_t impactVelocityMagnitude = real_c(std::pow(weberNumber * surfaceTension / dropDiameter, 0.5)); + + const Vector3< real_t > force(real_c(0), real_c(0), -gravitationalAccelerationZ); + + const real_t impactVelocityY = + impactVelocityMagnitude * real_c(std::sin(impactAngleDegree * math::pi / real_c(180))); + const real_t impactVelocityZ = + impactVelocityMagnitude * real_c(std::cos(impactAngleDegree * math::pi / real_c(180))); + + const Vector3< real_t > impactVelocity(real_c(0), impactVelocityY, -impactVelocityZ); + + const bool enableWetting = physicsParameters.getParameter< bool >("enableWetting"); + const real_t contactAngle = physicsParameters.getParameter< real_t >("contactAngle"); + + const uint_t timesteps = physicsParameters.getParameter< uint_t >("timesteps"); + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(relaxationRate); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(surfaceTension); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(force); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(impactVelocity); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableWetting); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(contactAngle); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(timesteps); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(viscosity); + + // read model parameters from parameter file + const auto modelParameters = walberlaEnv.config()->getOneBlock("ModelParameters"); + const std::string pdfReconstructionModel = modelParameters.getParameter< std::string >("pdfReconstructionModel"); + const std::string pdfRefillingModel = modelParameters.getParameter< std::string >("pdfRefillingModel"); + const std::string excessMassDistributionModel = + modelParameters.getParameter< std::string >("excessMassDistributionModel"); + const std::string curvatureModel = modelParameters.getParameter< std::string >("curvatureModel"); + const bool enableForceWeighting = modelParameters.getParameter< bool >("enableForceWeighting"); + const bool useSimpleMassExchange = modelParameters.getParameter< bool >("useSimpleMassExchange"); + const bool enableBubbleModel = modelParameters.getParameter< bool >("enableBubbleModel"); + const bool enableBubbleSplits = modelParameters.getParameter< bool >("enableBubbleSplits"); + const real_t cellConversionThreshold = modelParameters.getParameter< real_t >("cellConversionThreshold"); + const real_t cellConversionForceThreshold = modelParameters.getParameter< real_t >("cellConversionForceThreshold"); + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(pdfReconstructionModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(pdfRefillingModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(excessMassDistributionModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(curvatureModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableForceWeighting); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(useSimpleMassExchange); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableBubbleModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableBubbleSplits); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellConversionThreshold); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellConversionForceThreshold); + + // read evaluation parameters from parameter file + const auto evaluationParameters = walberlaEnv.config()->getOneBlock("EvaluationParameters"); + const uint_t performanceLogFrequency = evaluationParameters.getParameter< uint_t >("performanceLogFrequency"); + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(performanceLogFrequency); + + // create non-uniform block forest (non-uniformity required for load balancing) + const std::shared_ptr< StructuredBlockForest > blockForest = + createNonUniformBlockForest(domainSize, cellsPerBlock, numBlocks, periodicity); + + // add force field + const BlockDataID forceFieldID = + field::addToStorage< VectorField_T >(blockForest, "Force field", force, field::fzyx, uint_c(1)); + + // create lattice model + const LatticeModel_T latticeModel = LatticeModel_T(collisionModel, ForceModel_T(forceFieldID)); + + // add pdf field + const BlockDataID pdfFieldID = + lbm::addPdfFieldToStorage(blockForest, "PDF field", latticeModel, impactVelocity, real_c(1), field::fzyx); + + // add fill level field (initialized with 0, i.e., gas everywhere) + const BlockDataID fillFieldID = + field::addToStorage< ScalarField_T >(blockForest, "Fill level field", real_c(0.0), field::fzyx, uint_c(2)); + + // add boundary handling + const std::shared_ptr< FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling = + std::make_shared< FreeSurfaceBoundaryHandling_T >(blockForest, pdfFieldID, fillFieldID); + const BlockDataID flagFieldID = freeSurfaceBoundaryHandling->getFlagFieldID(); + const typename FreeSurfaceBoundaryHandling_T::FlagInfo_T& flagInfo = freeSurfaceBoundaryHandling->getFlagInfo(); + + const real_t poolHeight = poolHeightFactor * dropDiameter; + + // initialize a pool of liquid at the bottom of the domain in z-direction + const AABB poolAABB(real_c(0), real_c(0), real_c(0), real_c(domainSize[0]), real_c(domainSize[1]), poolHeight); + + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + ScalarField_T* const fillField = blockIt->getData< ScalarField_T >(fillFieldID); + PdfField_T* const pdfField = blockIt->getData< PdfField_T >(pdfFieldID); + + // determine the cells that are relevant for a block (still in global coordinates) + const CellInterval relevantCellBB = blockForest->getCellBBFromAABB(poolAABB.getIntersection(blockIt->getAABB())); + + // transform the global coordinates of relevant cells to block local coordinates + CellInterval blockLocalCellBB; + blockForest->transformGlobalToBlockLocalCellInterval(blockLocalCellBB, *blockIt, relevantCellBB); + + WALBERLA_FOR_ALL_CELLS_IN_INTERVAL_XYZ(blockLocalCellBB, { + fillField->get(x, y, z) = real_c(1); + pdfField->setDensityAndVelocity(x, y, z, Vector3< real_t >(real_c(0)), real_c(1)); + }) // WALBERLA_FOR_ALL_CELLS_IN_INTERVAL_XYZ + } + + const Vector3< real_t > dropCenter = dropCenterFactor * dropDiameter; + + const geometry::Sphere sphereDrop(dropCenter, dropDiameter * real_c(0.5)); + bubble_model::addBodyToFillLevelField< geometry::Sphere >(*blockForest, fillFieldID, sphereDrop, false); + + // initialize boundary conditions from config file + const auto boundaryParameters = walberlaEnv.config()->getOneBlock("BoundaryParameters"); + freeSurfaceBoundaryHandling->initFromConfig(boundaryParameters); + + // IMPORTANT REMARK: this must be only called after every solid flag has been set; otherwise, the boundary handling + // might not detect solid flags correctly + freeSurfaceBoundaryHandling->initFlagsFromFillLevel(); + + // communication after initialization + Communication_T communication(blockForest, flagFieldID, fillFieldID, forceFieldID); + communication(); + + PdfCommunication_T pdfCommunication(blockForest, pdfFieldID); + pdfCommunication(); + + // add bubble model + std::shared_ptr< bubble_model::BubbleModelBase > bubbleModel = nullptr; + if (enableBubbleModel) + { + const std::shared_ptr< bubble_model::BubbleModel< CommunicationStencil_T > > bubbleModelDerived = + std::make_shared< bubble_model::BubbleModel< CommunicationStencil_T > >(blockForest, enableBubbleSplits); + bubbleModelDerived->initFromFillLevelField(fillFieldID); + bubbleModelDerived->setAtmosphere( + Cell(domainSize[0] - uint_c(1), domainSize[1] - uint_c(1), domainSize[2] - uint_c(1)), real_c(1)); + + bubbleModel = std::static_pointer_cast< bubble_model::BubbleModelBase >(bubbleModelDerived); + } + else { bubbleModel = std::make_shared< bubble_model::BubbleModelConstantPressure >(real_c(1)); } + + // initialize hydrostatic pressure + initHydrostaticPressure< PdfField_T >(blockForest, pdfFieldID, force, poolHeight); + + // set density in non-liquid or non-interface cells to 1 (after initializing with hydrostatic pressure) + setDensityInNonFluidCellsToOne< FlagField_T, PdfField_T >(blockForest, flagInfo, flagFieldID, pdfFieldID); + + // create timeloop + SweepTimeloop timeloop(blockForest, timesteps); + + // Laplace pressure = 2 * surface tension * curvature; curvature computation is not necessary with 0 surface tension + bool computeCurvature = false; + if (!realIsEqual(surfaceTension, real_c(0), real_c(1e-14))) { computeCurvature = true; } + + // add surface geometry handler + const SurfaceGeometryHandler< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > geometryHandler( + blockForest, freeSurfaceBoundaryHandling, fillFieldID, curvatureModel, computeCurvature, enableWetting, + contactAngle); + + geometryHandler.addSweeps(timeloop); + + const ConstBlockDataID curvatureFieldID = geometryHandler.getConstCurvatureFieldID(); + const ConstBlockDataID normalFieldID = geometryHandler.getConstNormalFieldID(); + + // add boundary handling for standard boundaries and free surface boundaries + const SurfaceDynamicsHandler< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > dynamicsHandler( + blockForest, pdfFieldID, flagFieldID, fillFieldID, forceFieldID, normalFieldID, curvatureFieldID, + freeSurfaceBoundaryHandling, bubbleModel, pdfReconstructionModel, pdfRefillingModel, excessMassDistributionModel, + relaxationRate, force, surfaceTension, enableForceWeighting, useSimpleMassExchange, cellConversionThreshold, + cellConversionForceThreshold); + + dynamicsHandler.addSweeps(timeloop); + + // add load balancing + const LoadBalancer< FlagField_T, CommunicationStencil_T, LatticeModelStencil_T > loadBalancer( + blockForest, communication, pdfCommunication, bubbleModel, uint_c(50), uint_c(10), uint_c(5), + loadBalancingFrequency, printLoadBalancingStatistics); + timeloop.addFuncAfterTimeStep(loadBalancer, "Sweep: load balancing"); + + // add VTK output + addVTKOutput< LatticeModel_T, FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T, VectorField_T >( + blockForest, timeloop, walberlaEnv.config(), flagInfo, pdfFieldID, flagFieldID, fillFieldID, forceFieldID, + geometryHandler.getCurvatureFieldID(), geometryHandler.getNormalFieldID(), + geometryHandler.getObstNormalFieldID()); + + // add triangle mesh output of free surface + SurfaceMeshWriter< ScalarField_T, FlagField_T > surfaceMeshWriter( + blockForest, fillFieldID, flagFieldID, flagIDs::liquidInterfaceGasFlagIDs, real_c(0), walberlaEnv.config()); + surfaceMeshWriter(); // write initial mesh + timeloop.addFuncAfterTimeStep(surfaceMeshWriter, "Writer: surface mesh"); + + // add logging for computational performance + const lbm::PerformanceLogger< FlagField_T > perfLogger(blockForest, flagFieldID, flagIDs::liquidInterfaceFlagIDs, + performanceLogFrequency); + timeloop.addFuncAfterTimeStep(perfLogger, "Evaluator: performance logging"); + + WcTimingPool timingPool; + + for (uint_t t = uint_c(0); t != timesteps; ++t) + { + if (t % uint_c(real_c(timesteps / 100)) == uint_c(0)) + { + WALBERLA_LOG_DEVEL_ON_ROOT("Performing timestep = " << t); + } + timeloop.singleStep(timingPool, true); + + if (t % performanceLogFrequency == uint_c(0) && t > uint_c(0)) { timingPool.logResultOnRoot(); } + } + + return EXIT_SUCCESS; +} + +} // namespace DropImpact +} // namespace free_surface +} // namespace walberla + +int main(int argc, char** argv) { return walberla::free_surface::DropImpact::main(argc, argv); } \ No newline at end of file diff --git a/apps/showcases/FreeSurface/DropImpact.prm b/apps/showcases/FreeSurface/DropImpact.prm new file mode 100644 index 000000000..07afacce7 --- /dev/null +++ b/apps/showcases/FreeSurface/DropImpact.prm @@ -0,0 +1,110 @@ +BlockForestParameters +{ + cellsPerBlock < 10, 10, 10 >; + periodicity < 1, 1, 0 >; + loadBalancingFrequency 372; + printLoadBalancingStatistics true; +} + +DomainParameters +{ + dropDiameter 20; + dropCenterFactor < 2, 2, 1 >; // values multiplied with dropDiameter + poolHeightFactor 0.5; // value multiplied with dropDiameter + domainSizeFactor < 10, 10, 5 >; // values multiplied with dropDiameter +} + +PhysicsParameters +{ + bondNumber 3.18; + weberNumber 2010; + ohnesorgeNumber 0.0384; + relaxationRate 1.989; + impactAngleDegree 0; + enableWetting false; + contactAngle 0; // only used if enableWetting=true + timesteps 6000; +} + +ModelParameters +{ + pdfReconstructionModel OnlyMissing; + pdfRefillingModel EquilibriumRefilling; + excessMassDistributionModel EvenlyAllInterface; + curvatureModel FiniteDifferenceMethod; + enableForceWeighting false; + useSimpleMassExchange false; + enableBubbleModel false; + enableBubbleSplits false; // only used if enableBubbleModel=true + cellConversionThreshold 1e-2; + cellConversionForceThreshold 1e-1; +} + +EvaluationParameters +{ + performanceLogFrequency 3000; +} + +BoundaryParameters +{ + // X + //Border { direction W; walldistance -1; NoSlip{} } + //Border { direction E; walldistance -1; NoSlip{} } + + // Y + //Border { direction N; walldistance -1; NoSlip{} } + //Border { direction S; walldistance -1; NoSlip{} } + + // Z + Border { direction T; walldistance -1; NoSlip{} } + Border { direction B; walldistance -1; NoSlip{} } +} + +MeshOutputParameters +{ + writeFrequency 50; + baseFolder mesh-out; +} + +VTK +{ + fluid_field + { + writeFrequency 1000; + ghostLayers 0; + baseFolder vtk-out; + samplingResolution 1; + + writers + { + velocity; + density; + //pdf; + flag; + fill_level; + force; + curvature; + normal; + obstacle_normal; + mapped_flag; + } + + inclusion_filters + { + // only include liquid and interface cells in VTK output + //liquidInterfaceFilter; + } + + before_functions + { + //ghost_layer_synchronization; // only needed if writing the ghost layer + } + + } + domain_decomposition + { + writeFrequency 372; + baseFolder vtk-out; + outputDomainDecomposition true; + } +} \ No newline at end of file diff --git a/apps/showcases/FreeSurface/DropWetting.cpp b/apps/showcases/FreeSurface/DropWetting.cpp new file mode 100644 index 000000000..20445dead --- /dev/null +++ b/apps/showcases/FreeSurface/DropWetting.cpp @@ -0,0 +1,421 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file DropWetting.cpp +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +// +// This showcase simulates a droplet on a plane with specified target contact angle. The resulting drop height can be +// compared to an analytical model, as done in e.g.: +// dissertation of S. Bogner (2017), section 4.4.3.2 , urn:nbn:de:bvb:29-opus4-87191 +//====================================================================================================================== + +#include "blockforest/Initialization.h" + +#include "core/Environment.h" + +#include "lbm/PerformanceLogger.h" +#include "lbm/field/AddToStorage.h" +#include "lbm/free_surface/LoadBalancing.h" +#include "lbm/free_surface/SurfaceMeshWriter.h" +#include "lbm/free_surface/VtkWriter.h" +#include "lbm/free_surface/bubble_model/Geometry.h" +#include "lbm/free_surface/dynamics/SurfaceDynamicsHandler.h" +#include "lbm/free_surface/surface_geometry/SurfaceGeometryHandler.h" +#include "lbm/free_surface/surface_geometry/Utility.h" +#include "lbm/lattice_model/D3Q19.h" + +namespace walberla +{ +namespace free_surface +{ +namespace DropWetting +{ +using ScalarField_T = GhostLayerField< real_t, 1 >; +using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >; + +using CollisionModel_T = lbm::collision_model::SRT; +using ForceModel_T = lbm::force_model::GuoField< VectorField_T >; +using LatticeModel_T = lbm::D3Q19< CollisionModel_T, true, ForceModel_T, 2 >; +using LatticeModelStencil_T = LatticeModel_T::Stencil; +using PdfField_T = lbm::PdfField< LatticeModel_T >; +using PdfCommunication_T = blockforest::SimpleCommunication< LatticeModelStencil_T >; + +// the geometry computations in SurfaceGeometryHandler require meaningful values in the ghost layers in corner +// directions (flag field and fill level field); this holds, even if the lattice model uses a D3Q19 stencil +using CommunicationStencil_T = + typename std::conditional< LatticeModel_T::Stencil::D == uint_t(2), stencil::D2Q9, stencil::D3Q27 >::type; +using Communication_T = blockforest::SimpleCommunication< CommunicationStencil_T >; + +using flag_t = uint32_t; +using FlagField_T = FlagField< flag_t >; +using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >; + +// compute the L2 norm of the relative difference between the current and the previous time-averaged quantities +template< typename FreeSurfaceBoundaryHandling_T > +class DropHeightEvaluator +{ + public: + DropHeightEvaluator(const std::weak_ptr< const StructuredBlockForest >& blockForest, + const std::weak_ptr< const FreeSurfaceBoundaryHandling_T >& boundaryHandlingID, + const ConstBlockDataID& fillFieldID, const std::shared_ptr< real_t >& dropHeight, + uint_t interval) + : blockForest_(blockForest), freeSurfaceBoundaryHandling_(boundaryHandlingID), fillFieldID_(fillFieldID), + dropHeight_(dropHeight), interval_(interval), executionCounter_(uint_c(0)) + {} + + void operator()() + { + ++executionCounter_; + + // only evaluate in given intervals + if (executionCounter_ % interval_ != uint_c(0)) { return; } + + // compute the height of the spherical drop cap + computeDropHeight(); + } + + private: + std::weak_ptr< const StructuredBlockForest > blockForest_; + std::weak_ptr< const FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling_; + ConstBlockDataID fillFieldID_; + + std::shared_ptr< real_t > dropHeight_; + + uint_t interval_; + + uint_t executionCounter_; // number of times operator() has been called + + void computeDropHeight() + { + auto blockForest = blockForest_.lock(); + WALBERLA_CHECK_NOT_NULLPTR(blockForest); + + auto freeSurfaceBoundaryHandling = freeSurfaceBoundaryHandling_.lock(); + WALBERLA_CHECK_NOT_NULLPTR(freeSurfaceBoundaryHandling); + + const BlockDataID flagFieldID = freeSurfaceBoundaryHandling->getFlagFieldID(); + const typename FreeSurfaceBoundaryHandling_T::FlagInfo_T& flagInfo = freeSurfaceBoundaryHandling->getFlagInfo(); + + *dropHeight_ = real_c(0); + + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + real_t maxDropHeight = real_c(0); + + const FlagField_T* const flagField = blockIt->template getData< const FlagField_T >(flagFieldID); + const ScalarField_T* const fillField = blockIt->template getData< const ScalarField_T >(fillFieldID_); + + WALBERLA_FOR_ALL_CELLS( + flagFieldIt, flagField, fillFieldIt, fillField, + + if (flagInfo.isInterface(flagFieldIt)) { + Cell globalCell = flagFieldIt.cell(); + blockForest->transformBlockLocalToGlobalCell(globalCell, *blockIt); + + real_t currentDropHeight = real_c(globalCell.z()) + *fillFieldIt; + maxDropHeight = currentDropHeight > maxDropHeight ? currentDropHeight : maxDropHeight; + }) // WALBERLA_FOR_ALL_CELLS + + if (maxDropHeight > *dropHeight_) { *dropHeight_ = maxDropHeight; } + } + // get maximum dropHeight among all processes + mpi::allReduceInplace< real_t >(*dropHeight_, mpi::MAX); + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(*dropHeight_); + } +}; // class DropHeightEvaluator + +int main(int argc, char** argv) +{ + Environment walberlaEnv(argc, argv); + + if (argc < 2) { WALBERLA_ABORT("Please specify a parameter file as input argument.") } + + // print content of parameter file + WALBERLA_LOG_INFO_ON_ROOT(*walberlaEnv.config()); + + // get block forest parameters from parameter file + auto blockForestParameters = walberlaEnv.config()->getOneBlock("BlockForestParameters"); + const Vector3< uint_t > cellsPerBlock = blockForestParameters.getParameter< Vector3< uint_t > >("cellsPerBlock"); + const Vector3< bool > periodicity = blockForestParameters.getParameter< Vector3< bool > >("periodicity"); + const uint_t loadBalancingFrequency = blockForestParameters.getParameter< uint_t >("loadBalancingFrequency"); + const bool printLoadBalancingStatistics = blockForestParameters.getParameter< bool >("printLoadBalancingStatistics"); + + // read domain parameters from parameter file + const auto domainParameters = walberlaEnv.config()->getOneBlock("DomainParameters"); + const real_t dropDiameter = domainParameters.getParameter< real_t >("dropDiameter"); + const Vector3< real_t > domainSizeFactor = domainParameters.getParameter< Vector3< real_t > >("domainSizeFactor"); + + // define domain size + Vector3< uint_t > domainSize = domainSizeFactor * dropDiameter; + domainSize[0] = uint_c(domainSizeFactor[0] * dropDiameter); + domainSize[1] = uint_c(domainSizeFactor[1] * dropDiameter); + domainSize[2] = uint_c(domainSizeFactor[2] * dropDiameter); + + // compute number of blocks as defined by domainSize and cellsPerBlock + Vector3< uint_t > numBlocks; + numBlocks[0] = uint_c(std::ceil(real_c(domainSize[0]) / real_c(cellsPerBlock[0]))); + numBlocks[1] = uint_c(std::ceil(real_c(domainSize[1]) / real_c(cellsPerBlock[1]))); + numBlocks[2] = uint_c(std::ceil(real_c(domainSize[2]) / real_c(cellsPerBlock[2]))); + + // get number of (MPI) processes + uint_t numProcesses = uint_c(MPIManager::instance()->numProcesses()); + WALBERLA_CHECK_LESS_EQUAL(numProcesses, numBlocks[0] * numBlocks[1] * numBlocks[2], + "The number of MPI processes is greater than the number of blocks as defined by " + "\"domainSize/cellsPerBlock\". This would result in unused MPI processes. Either decrease " + "the number of MPI processes or increase \"cellsPerBlock\".") + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(numProcesses); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellsPerBlock); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(numBlocks); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(dropDiameter); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(domainSizeFactor); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(periodicity); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(domainSize); + + Vector3< uint_t > realDomainSize; + realDomainSize[0] = cellsPerBlock[0] * numBlocks[0]; + realDomainSize[1] = cellsPerBlock[1] * numBlocks[1]; + realDomainSize[2] = cellsPerBlock[2] * numBlocks[2]; + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(realDomainSize); + + if (domainSize[0] != realDomainSize[0] && periodicity[0]) + { + WALBERLA_ABORT( + "The specified domain size in x-direction can not be obtained with the number of blocks you specified.") + } + if (domainSize[1] != realDomainSize[1] && periodicity[1]) + { + WALBERLA_ABORT( + "The specified domain size in y-direction can not be obtained with the number of blocks you specified.") + } + // the z-direction must be no slip in this setup and is simply extended (see below) to obtain the specified size + int boundaryThicknessZ = int_c(realDomainSize[2]) - int_c(domainSize[2]); + if (boundaryThicknessZ < 0) + { + WALBERLA_ABORT("Something went wrong: the resulting domain size in z-direction is less than specified.") + } + + // read physics parameters from parameter file + const auto physicsParameters = walberlaEnv.config()->getOneBlock("PhysicsParameters"); + const real_t relaxationRate = physicsParameters.getParameter< real_t >("relaxationRate"); + const real_t surfaceTension = physicsParameters.getParameter< real_t >("surfaceTension"); + const Vector3< real_t > force = physicsParameters.getParameter< Vector3< real_t > >("force"); + + const bool enableWetting = physicsParameters.getParameter< bool >("enableWetting"); + const real_t contactAngle = physicsParameters.getParameter< real_t >("contactAngle"); + + const CollisionModel_T collisionModel = CollisionModel_T(relaxationRate); + const real_t viscosity = collisionModel.viscosity(); + + const uint_t timesteps = physicsParameters.getParameter< uint_t >("timesteps"); + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(relaxationRate); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(surfaceTension); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(force); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableWetting); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(contactAngle); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(timesteps); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(viscosity); + + // read model parameters from parameter file + const auto modelParameters = walberlaEnv.config()->getOneBlock("ModelParameters"); + const std::string pdfReconstructionModel = modelParameters.getParameter< std::string >("pdfReconstructionModel"); + const std::string pdfRefillingModel = modelParameters.getParameter< std::string >("pdfRefillingModel"); + const std::string excessMassDistributionModel = + modelParameters.getParameter< std::string >("excessMassDistributionModel"); + const std::string curvatureModel = modelParameters.getParameter< std::string >("curvatureModel"); + const bool enableForceWeighting = modelParameters.getParameter< bool >("enableForceWeighting"); + const bool useSimpleMassExchange = modelParameters.getParameter< bool >("useSimpleMassExchange"); + const bool enableBubbleModel = modelParameters.getParameter< bool >("enableBubbleModel"); + const bool enableBubbleSplits = modelParameters.getParameter< bool >("enableBubbleSplits"); + const real_t cellConversionThreshold = modelParameters.getParameter< real_t >("cellConversionThreshold"); + const real_t cellConversionForceThreshold = modelParameters.getParameter< real_t >("cellConversionForceThreshold"); + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(pdfReconstructionModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(pdfRefillingModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(excessMassDistributionModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(curvatureModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableForceWeighting); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(useSimpleMassExchange); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableBubbleModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableBubbleSplits); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellConversionThreshold); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellConversionForceThreshold); + + // read evaluation parameters from parameter file + const auto evaluationParameters = walberlaEnv.config()->getOneBlock("EvaluationParameters"); + const uint_t performanceLogFrequency = evaluationParameters.getParameter< uint_t >("performanceLogFrequency"); + const uint_t evaluationFrequency = evaluationParameters.getParameter< uint_t >("evaluationFrequency"); + const real_t convergenceThreshold = evaluationParameters.getParameter< real_t >("convergenceThreshold"); + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(performanceLogFrequency); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(evaluationFrequency); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(convergenceThreshold); + + // create non-uniform block forest (non-uniformity required for load balancing) + const std::shared_ptr< StructuredBlockForest > blockForest = + createNonUniformBlockForest(domainSize, cellsPerBlock, numBlocks, periodicity); + + // add force field + const BlockDataID forceFieldID = + field::addToStorage< VectorField_T >(blockForest, "Force field", force, field::fzyx, uint_c(1)); + + // create lattice model + const LatticeModel_T latticeModel = LatticeModel_T(collisionModel, ForceModel_T(forceFieldID)); + + // add pdf field + const BlockDataID pdfFieldID = lbm::addPdfFieldToStorage(blockForest, "PDF field", latticeModel, field::fzyx); + + // add fill level field (initialized with 0, i.e., gas everywhere) + const BlockDataID fillFieldID = + field::addToStorage< ScalarField_T >(blockForest, "Fill level field", real_c(0.0), field::fzyx, uint_c(2)); + + // add boundary handling + const std::shared_ptr< FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling = + std::make_shared< FreeSurfaceBoundaryHandling_T >(blockForest, pdfFieldID, fillFieldID); + const BlockDataID flagFieldID = freeSurfaceBoundaryHandling->getFlagFieldID(); + const typename FreeSurfaceBoundaryHandling_T::FlagInfo_T& flagInfo = freeSurfaceBoundaryHandling->getFlagInfo(); + + // add spherical drop to fill level field + const geometry::Sphere sphereDrop(Vector3< real_t >(real_c(0.5) * real_c(domainSize[0]), + real_c(0.5) * real_c(domainSize[1]), + real_c(0.5) * real_c(dropDiameter)), + real_c(dropDiameter) * real_c(0.5)); + bubble_model::addBodyToFillLevelField< geometry::Sphere >(*blockForest, fillFieldID, sphereDrop, false); + + // initialize boundary conditions from config file + const auto boundaryParameters = walberlaEnv.config()->getOneBlock("BoundaryParameters"); + freeSurfaceBoundaryHandling->initFromConfig(boundaryParameters); + + // IMPORTANT REMARK: this must be only called after every solid flag has been set; otherwise, the boundary handling + // might not detect solid flags correctly + freeSurfaceBoundaryHandling->initFlagsFromFillLevel(); + + // communication after initialization + Communication_T communication(blockForest, flagFieldID, fillFieldID, forceFieldID); + communication(); + + PdfCommunication_T pdfCommunication(blockForest, pdfFieldID); + pdfCommunication(); + + // add bubble model + std::shared_ptr< bubble_model::BubbleModelBase > bubbleModel = nullptr; + if (enableBubbleModel) + { + const std::shared_ptr< bubble_model::BubbleModel< CommunicationStencil_T > > bubbleModelDerived = + std::make_shared< bubble_model::BubbleModel< CommunicationStencil_T > >(blockForest, enableBubbleSplits); + bubbleModelDerived->initFromFillLevelField(fillFieldID); + bubbleModelDerived->setAtmosphere( + Cell(domainSize[0] - uint_c(1), domainSize[1] - uint_c(1), domainSize[2] - uint_c(1)), real_c(1)); + + bubbleModel = std::static_pointer_cast< bubble_model::BubbleModelBase >(bubbleModelDerived); + } + else { bubbleModel = std::make_shared< bubble_model::BubbleModelConstantPressure >(real_c(1)); } + + // initialize hydrostatic pressure + initHydrostaticPressure< PdfField_T >(blockForest, pdfFieldID, force, real_c(0.5) * dropDiameter); + + // set density in non-liquid or non-interface cells to 1 (after initializing with hydrostatic pressure) + setDensityInNonFluidCellsToOne< FlagField_T, PdfField_T >(blockForest, flagInfo, flagFieldID, pdfFieldID); + + // create timeloop + SweepTimeloop timeloop(blockForest, timesteps); + + // Laplace pressure = 2 * surface tension * curvature; curvature computation is not necessary with 0 surface tension + bool computeCurvature = false; + if (!realIsEqual(surfaceTension, real_c(0), real_c(1e-14))) { computeCurvature = true; } + + // add surface geometry handler + const SurfaceGeometryHandler< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > geometryHandler( + blockForest, freeSurfaceBoundaryHandling, fillFieldID, curvatureModel, computeCurvature, enableWetting, + contactAngle); + + geometryHandler.addSweeps(timeloop); + + const ConstBlockDataID curvatureFieldID = geometryHandler.getConstCurvatureFieldID(); + const ConstBlockDataID normalFieldID = geometryHandler.getConstNormalFieldID(); + + // add boundary handling for standard boundaries and free surface boundaries + const SurfaceDynamicsHandler< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > dynamicsHandler( + blockForest, pdfFieldID, flagFieldID, fillFieldID, forceFieldID, normalFieldID, curvatureFieldID, + freeSurfaceBoundaryHandling, bubbleModel, pdfReconstructionModel, pdfRefillingModel, excessMassDistributionModel, + relaxationRate, force, surfaceTension, enableForceWeighting, useSimpleMassExchange, cellConversionThreshold, + cellConversionForceThreshold); + + dynamicsHandler.addSweeps(timeloop); + + // add load balancing + LoadBalancer< FlagField_T, CommunicationStencil_T, LatticeModelStencil_T > loadBalancer( + blockForest, communication, pdfCommunication, bubbleModel, uint_c(50), uint_c(10), uint_c(5), + loadBalancingFrequency, printLoadBalancingStatistics); + timeloop.addFuncAfterTimeStep(loadBalancer, "Sweep: load balancing"); + + // evaluate height of the drop + const std::shared_ptr< real_t > dropHeight = std::make_shared< real_t >(real_c(0)); + const DropHeightEvaluator< FreeSurfaceBoundaryHandling_T > dropHeightEvaluator( + blockForest, freeSurfaceBoundaryHandling, fillFieldID, dropHeight, evaluationFrequency); + timeloop.addFuncAfterTimeStep(dropHeightEvaluator, "Evaluator: drop height"); + + // add VTK output + addVTKOutput< LatticeModel_T, FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T, VectorField_T >( + blockForest, timeloop, walberlaEnv.config(), flagInfo, pdfFieldID, flagFieldID, fillFieldID, forceFieldID, + geometryHandler.getCurvatureFieldID(), geometryHandler.getNormalFieldID(), + geometryHandler.getObstNormalFieldID()); + + // add triangle mesh output of free surface + SurfaceMeshWriter< ScalarField_T, FlagField_T > surfaceMeshWriter( + blockForest, fillFieldID, flagFieldID, flagIDs::liquidInterfaceGasFlagIDs, real_c(1), walberlaEnv.config()); + surfaceMeshWriter(); // write initial mesh + timeloop.addFuncAfterTimeStep(surfaceMeshWriter, "Writer: surface mesh"); + + // add logging for computational performance + const lbm::PerformanceLogger< FlagField_T > perfLogger(blockForest, flagFieldID, flagIDs::liquidInterfaceFlagIDs, + performanceLogFrequency); + timeloop.addFuncAfterTimeStep(perfLogger, "Evaluator: performance logging"); + + WcTimingPool timingPool; + + real_t formerDropHeight = real_c(0); + for (uint_t t = uint_c(0); t != timesteps; ++t) + { + timeloop.singleStep(timingPool, true); + + // check convergence + if (t % evaluationFrequency == uint_c(0)) + { + WALBERLA_LOG_DEVEL_ON_ROOT("time step = " << t) + WALBERLA_LOG_DEVEL_ON_ROOT("\t\tdrop height = " << *dropHeight) + if (std::abs(formerDropHeight - *dropHeight) / *dropHeight < convergenceThreshold) + { + WALBERLA_LOG_DEVEL_ON_ROOT("Final converged drop height=" << *dropHeight); + return EXIT_SUCCESS; + } + formerDropHeight = *dropHeight; + } + + if (t % performanceLogFrequency == uint_c(0) && t > uint_c(0)) { timingPool.logResultOnRoot(); } + } + + WALBERLA_LOG_DEVEL_ON_ROOT("Final non-converged drop height=" << *dropHeight); + + return EXIT_SUCCESS; +} +} // namespace DropWetting +} // namespace free_surface +} // namespace walberla + +int main(int argc, char** argv) { return walberla::free_surface::DropWetting::main(argc, argv); } \ No newline at end of file diff --git a/apps/showcases/FreeSurface/DropWetting.prm b/apps/showcases/FreeSurface/DropWetting.prm new file mode 100644 index 000000000..bdfbe4e1e --- /dev/null +++ b/apps/showcases/FreeSurface/DropWetting.prm @@ -0,0 +1,108 @@ +BlockForestParameters +{ + cellsPerBlock < 10, 10, 10 >; + periodicity < 1, 1, 0 >; + loadBalancingFrequency 100; + printLoadBalancingStatistics true; +} + +DomainParameters +{ + dropDiameter 20; + domainSizeFactor < 3, 3, 2 >; // values multiplied with dropDiameter +} + +PhysicsParameters +{ + relaxationRate 1.96; + surfaceTension 8.37e-3; + force <0, 0, -1.14e-7>; + enableWetting true; + contactAngle 45; // only used if enableWetting=true + timesteps 10001; +} + +ModelParameters +{ + pdfReconstructionModel OnlyMissing; + pdfRefillingModel EquilibriumRefilling; + excessMassDistributionModel EvenlyAllInterface; + curvatureModel FiniteDifferenceMethod; + enableForceWeighting false; + useSimpleMassExchange false; + enableBubbleModel false; + enableBubbleSplits false; // only used if enableBubbleModel=true + cellConversionThreshold 1e-2; + cellConversionForceThreshold 1e-1; +} + +EvaluationParameters +{ + performanceLogFrequency 1000; + evaluationFrequency 200; + convergenceThreshold 1e-5; +} + +BoundaryParameters +{ + // X + //Border { direction W; walldistance -1; NoSlip{} } + //Border { direction E; walldistance -1; NoSlip{} } + + // Y + //Border { direction N; walldistance -1; NoSlip{} } + //Border { direction S; walldistance -1; NoSlip{} } + + // Z + Border { direction T; walldistance -1; NoSlip{} } + Border { direction B; walldistance -1; NoSlip{} } +} + +MeshOutputParameters +{ + writeFrequency 100; + baseFolder mesh-out; +} + +VTK +{ + fluid_field + { + writeFrequency 100; + ghostLayers 0; + baseFolder vtk-out; + samplingResolution 1; + + writers + { + velocity; + density; + //pdf; + flag; + fill_level; + force; + curvature; + normal; + obstacle_normal; + mapped_flag; + } + + inclusion_filters + { + // only include liquid and interface cells in VTK output + //liquidInterfaceFilter; + } + + before_functions + { + //ghost_layer_synchronization; // only needed if writing the ghost layer + } + + } + domain_decomposition + { + writeFrequency 100; + baseFolder vtk-out; + outputDomainDecomposition true; + } +} \ No newline at end of file diff --git a/apps/showcases/FreeSurface/GravityWave.cpp b/apps/showcases/FreeSurface/GravityWave.cpp new file mode 100644 index 000000000..6faca3c70 --- /dev/null +++ b/apps/showcases/FreeSurface/GravityWave.cpp @@ -0,0 +1,579 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file GravityWave.cpp +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +// +// This showcase simulates a standing wave purely governed by gravity, i.e., without surface tension forces. +//====================================================================================================================== + +#include "blockforest/Initialization.h" + +#include "core/Environment.h" + +#include "field/Gather.h" + +#include "lbm/PerformanceLogger.h" +#include "lbm/field/AddToStorage.h" +#include "lbm/free_surface/LoadBalancing.h" +#include "lbm/free_surface/SurfaceMeshWriter.h" +#include "lbm/free_surface/TotalMassComputer.h" +#include "lbm/free_surface/VtkWriter.h" +#include "lbm/free_surface/dynamics/SurfaceDynamicsHandler.h" +#include "lbm/free_surface/surface_geometry/SurfaceGeometryHandler.h" +#include "lbm/free_surface/surface_geometry/Utility.h" +#include "lbm/lattice_model/D2Q9.h" + +namespace walberla +{ +namespace free_surface +{ +namespace GravityWave +{ +using ScalarField_T = GhostLayerField< real_t, 1 >; +using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >; + +using CollisionModel_T = lbm::collision_model::SRT; +using ForceModel_T = lbm::force_model::GuoField< VectorField_T >; +using LatticeModel_T = lbm::D2Q9< CollisionModel_T, true, ForceModel_T, 2 >; +using LatticeModelStencil_T = LatticeModel_T::Stencil; +using PdfField_T = lbm::PdfField< LatticeModel_T >; +using PdfCommunication_T = blockforest::SimpleCommunication< LatticeModelStencil_T >; + +// the geometry computations in SurfaceGeometryHandler require meaningful values in the ghost layers in corner +// directions (flag field and fill level field); this holds, even if the lattice model uses a D3Q19 stencil +using CommunicationStencil_T = + typename std::conditional< LatticeModel_T::Stencil::D == uint_t(2), stencil::D2Q9, stencil::D3Q27 >::type; +using Communication_T = blockforest::SimpleCommunication< CommunicationStencil_T >; + +using flag_t = uint32_t; +using FlagField_T = FlagField< flag_t >; +using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >; + +// write each entry in "vector" to line in a file; columns are separated by tabs +template< typename T > +void writeVectorToFile(const std::vector< T >& vector, const std::string& filename); + +// function describing the initialization profile (in global coordinates) +inline real_t initializationProfile(real_t x, real_t amplitude, real_t offset, real_t wavelength) +{ + return amplitude * std::cos(x / wavelength * real_c(2) * math::pi + math::pi) + offset; +} + +// evaluate the symmetry of the fill level field along the y-axis located at the center in x-direction +// IMPORTANT REMARK: This implementation is very inefficient, as it gathers the field on a single process to perform the +// evaluation. +template< typename ScalarField_T > +class SymmetryXEvaluator +{ + public: + SymmetryXEvaluator(const std::weak_ptr< StructuredBlockForest >& blockForest, const ConstBlockDataID& fillFieldID, + const Vector3< uint_t >& domainSize, uint_t interval, + const std::shared_ptr< real_t >& symmetryNorm) + : blockForest_(blockForest), fillFieldID_(fillFieldID), domainSize_(domainSize), interval_(interval), + symmetryNorm_(symmetryNorm), executionCounter_(uint_c(0)) + { + auto blockForestPtr = blockForest_.lock(); + WALBERLA_CHECK_NOT_NULLPTR(blockForestPtr); + } + + void operator()() + { + auto blockForest = blockForest_.lock(); + WALBERLA_CHECK_NOT_NULLPTR(blockForest); + + ++executionCounter_; + + // only evaluate in given intervals + if (executionCounter_ % interval_ != uint_c(0) && executionCounter_ != uint_c(1)) { return; } + + real_t fillLevelSum2 = real_c(0); // sum of each cell's squared fill level + real_t deltaFillLevelSum2 = real_c(0); // sum of each cell's fill level difference to its symmetrical counterpart + + // gather the fill level field on rank 0 (WARNING: simple, but very inefficient) + std::shared_ptr< ScalarField_T > fillFieldGathered = nullptr; + WALBERLA_ROOT_SECTION() + { + fillFieldGathered = + std::make_shared< ScalarField_T >(domainSize_[0], domainSize_[1], domainSize_[2], uint_c(0)); + } + field::gather< ScalarField_T, ScalarField_T >(*fillFieldGathered, blockForest, fillFieldID_); + + WALBERLA_ROOT_SECTION() + { + // get field's center-coordinate in x-direction + uint_t fieldXCenter = fillFieldGathered->xSize() / uint_c(2); + + WALBERLA_FOR_ALL_CELLS_XYZ(fillFieldGathered, { + // skip cells in the right half of the field in x-direction, as they are treated + // as mirrored cells later + if (x >= cell_idx_c(fieldXCenter)) { continue; } + + // get this cell's x-distance to the field's center + uint_t cellDistXCenter = uint_c(std::abs(int_c(fieldXCenter) - int_c(x))); + + // get x-coordinate of (mirrored) cell in the right half of the field + cell_idx_t fieldRightX = cell_idx_c(fieldXCenter + cellDistXCenter); + if (fillFieldGathered->xSize() % 2 == uint_c(0)) + { + fieldRightX -= cell_idx_c(1); // if xSize is even, the blocks on the right must be shifted by -1 + } + + // get fill level + const real_t fillLevel = fillFieldGathered->get(x, y, z); + real_t fillLevelMirrored = real_c(0); + fillLevelMirrored = fillFieldGathered->get(fieldRightX, y, z); + + fillLevelSum2 += fillLevel * fillLevel; + + const real_t deltaFill = fillLevel - fillLevelMirrored; + deltaFillLevelSum2 += deltaFill * deltaFill; + }) // WALBERLA_FOR_ALL_CELLS_XYZ + } + + // communicate values among all processes + mpi::allReduceInplace< real_t >(fillLevelSum2, mpi::SUM); + mpi::allReduceInplace< real_t >(deltaFillLevelSum2, mpi::SUM); + + // compute L2 norm evaluate symmetry + *symmetryNorm_ = real_c(std::pow(deltaFillLevelSum2 / fillLevelSum2, real_c(0.5))); + } + + private: + std::weak_ptr< StructuredBlockForest > blockForest_; + ConstBlockDataID fillFieldID_; + Vector3< uint_t > domainSize_; + uint_t interval_; + std::shared_ptr< real_t > symmetryNorm_; + uint_t executionCounter_; +}; // class SymmetryXEvaluator + +// get interface position in y-direction at the specified (global) x-coordinate +template< typename FreeSurfaceBoundaryHandling_T > +class SurfaceYPositionEvaluator +{ + public: + SurfaceYPositionEvaluator(const std::weak_ptr< const StructuredBlockForest >& blockForest, + const std::weak_ptr< const FreeSurfaceBoundaryHandling_T >& freeSurfaceBoundaryHandling, + const ConstBlockDataID& fillFieldID, const Vector3< uint_t >& domainSize, + cell_idx_t globalXCoordinate, uint_t frequency, + const std::shared_ptr< real_t >& surfaceYPosition) + : blockForest_(blockForest), freeSurfaceBoundaryHandling_(freeSurfaceBoundaryHandling), fillFieldID_(fillFieldID), + domainSize_(domainSize), globalXCoordinate_(globalXCoordinate), surfaceYPosition_(surfaceYPosition), + frequency_(frequency), executionCounter_(uint_c(0)) + {} + + void operator()() + { + auto blockForest = blockForest_.lock(); + WALBERLA_CHECK_NOT_NULLPTR(blockForest); + + auto freeSurfaceBoundaryHandling = freeSurfaceBoundaryHandling_.lock(); + WALBERLA_CHECK_NOT_NULLPTR(freeSurfaceBoundaryHandling); + + ++executionCounter_; + + // only evaluate in given frequencies + if (executionCounter_ % frequency_ != uint_c(0) && executionCounter_ != uint_c(1)) { return; } + + const BlockDataID flagFieldID = freeSurfaceBoundaryHandling->getFlagFieldID(); + const typename FreeSurfaceBoundaryHandling_T::FlagInfo_T& flagInfo = freeSurfaceBoundaryHandling->getFlagInfo(); + + *surfaceYPosition_ = real_c(0); + + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + real_t maxSurfaceYPosition = real_c(0); + + CellInterval globalSearchInterval(globalXCoordinate_, cell_idx_c(0), cell_idx_c(0), globalXCoordinate_, + cell_idx_c(domainSize_[1]), cell_idx_c(0)); + + if (blockForest->getBlockCellBB(*blockIt).overlaps(globalSearchInterval)) + { + // transform specified global x-coordinate into block local coordinate + Cell localEvalCell = Cell(globalXCoordinate_, cell_idx_c(0), cell_idx_c(0)); + blockForest->transformGlobalToBlockLocalCell(localEvalCell, *blockIt); + + const FlagField_T* const flagField = blockIt->template getData< const FlagField_T >(flagFieldID); + const ScalarField_T* const fillField = blockIt->template getData< const ScalarField_T >(fillFieldID_); + + // searching from top ensures that the interface cell with the greatest y-coordinate is found first + for (cell_idx_t y = cell_idx_c((flagField)->ySize() - uint_c(1)); y >= cell_idx_t(0); --y) + { + if (flagInfo.isInterface(flagField->get(localEvalCell[0], y, cell_idx_c(0)))) + { + const real_t fillLevel = fillField->get(localEvalCell[0], y, cell_idx_c(0)); + + // transform local y-coordinate to global coordinate + Cell localResultCell = localEvalCell; + localResultCell[1] = y; + blockForest->transformBlockLocalToGlobalCell(localResultCell, *blockIt); + maxSurfaceYPosition = real_c(localResultCell[1]) + fillLevel; + + break; + } + } + } + + if (maxSurfaceYPosition > *surfaceYPosition_) { *surfaceYPosition_ = maxSurfaceYPosition; } + } + // communicate result among all processes + mpi::allReduceInplace< real_t >(*surfaceYPosition_, mpi::MAX); + } + + private: + std::weak_ptr< const StructuredBlockForest > blockForest_; + std::weak_ptr< const FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling_; + ConstBlockDataID fillFieldID_; + Vector3< uint_t > domainSize_; + cell_idx_t globalXCoordinate_; + std::shared_ptr< real_t > surfaceYPosition_; + + uint_t frequency_; + uint_t executionCounter_; +}; // class SurfaceYPositionEvaluator + +int main(int argc, char** argv) +{ + Environment walberlaEnv(argc, argv); + + if (argc < 2) { WALBERLA_ABORT("Please specify a parameter file as input argument.") } + + // print content of parameter file + WALBERLA_LOG_INFO_ON_ROOT(*walberlaEnv.config()); + + // get block forest parameters from parameter file + auto blockForestParameters = walberlaEnv.config()->getOneBlock("BlockForestParameters"); + const Vector3< uint_t > cellsPerBlock = blockForestParameters.getParameter< Vector3< uint_t > >("cellsPerBlock"); + const Vector3< bool > periodicity = blockForestParameters.getParameter< Vector3< bool > >("periodicity"); + const uint_t loadBalancingFrequency = blockForestParameters.getParameter< uint_t >("loadBalancingFrequency"); + const bool printLoadBalancingStatistics = blockForestParameters.getParameter< bool >("printLoadBalancingStatistics"); + + // get domain parameters from parameter file + auto domainParameters = walberlaEnv.config()->getOneBlock("DomainParameters"); + const uint_t domainWidth = domainParameters.getParameter< uint_t >("domainWidth"); + const real_t liquidDepth = domainParameters.getParameter< real_t >("liquidDepth"); + const real_t initialAmplitude = domainParameters.getParameter< real_t >("initialAmplitude"); + + // define domain size + Vector3< uint_t > domainSize; + domainSize[0] = domainWidth; + domainSize[1] = uint_c(liquidDepth * real_c(2)); + domainSize[2] = uint_c(1); + + // compute number of blocks as defined by domainSize and cellsPerBlock + Vector3< uint_t > numBlocks; + numBlocks[0] = uint_c(std::ceil(real_c(domainSize[0]) / real_c(cellsPerBlock[0]))); + numBlocks[1] = uint_c(std::ceil(real_c(domainSize[1]) / real_c(cellsPerBlock[1]))); + numBlocks[2] = uint_c(std::ceil(real_c(domainSize[2]) / real_c(cellsPerBlock[2]))); + + // get number of (MPI) processes + uint_t numProcesses = uint_c(MPIManager::instance()->numProcesses()); + WALBERLA_CHECK_LESS_EQUAL(numProcesses, numBlocks[0] * numBlocks[1] * numBlocks[2], + "The number of MPI processes is greater than the number of blocks as defined by " + "\"domainSize/cellsPerBlock\". This would result in unused MPI processes. Either decrease " + "the number of MPI processes or increase \"cellsPerBlock\".") + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(numProcesses); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellsPerBlock); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(domainSize); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(numBlocks); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(domainWidth); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(liquidDepth); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(initialAmplitude); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(periodicity); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(loadBalancingFrequency); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(printLoadBalancingStatistics); + + // get physics parameters from parameter file + auto physicsParameters = walberlaEnv.config()->getOneBlock("PhysicsParameters"); + const uint_t timesteps = physicsParameters.getParameter< uint_t >("timesteps"); + + const real_t relaxationRate = physicsParameters.getParameter< real_t >("relaxationRate"); + const CollisionModel_T collisionModel = CollisionModel_T(relaxationRate); + const real_t viscosity = collisionModel.viscosity(); + + const real_t reynoldsNumber = physicsParameters.getParameter< real_t >("reynoldsNumber"); + const real_t waveNumber = real_c(2) * math::pi / real_c(domainSize[0]); + const real_t waveFrequency = reynoldsNumber * viscosity / real_c(domainSize[0]) / initialAmplitude; + const real_t forceY = -(waveFrequency * waveFrequency) / waveNumber / std::tanh(waveNumber * liquidDepth); + const Vector3< real_t > force(real_c(0), forceY, real_c(0)); + + const bool enableWetting = physicsParameters.getParameter< bool >("enableWetting"); + const real_t contactAngle = physicsParameters.getParameter< real_t >("contactAngle"); + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(reynoldsNumber); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(relaxationRate); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableWetting); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(contactAngle); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(timesteps); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(viscosity); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(force); + + // read model parameters from parameter file + const auto modelParameters = walberlaEnv.config()->getOneBlock("ModelParameters"); + const std::string pdfReconstructionModel = modelParameters.getParameter< std::string >("pdfReconstructionModel"); + const std::string pdfRefillingModel = modelParameters.getParameter< std::string >("pdfRefillingModel"); + const std::string excessMassDistributionModel = + modelParameters.getParameter< std::string >("excessMassDistributionModel"); + const std::string curvatureModel = modelParameters.getParameter< std::string >("curvatureModel"); + const bool enableForceWeighting = modelParameters.getParameter< bool >("enableForceWeighting"); + const bool useSimpleMassExchange = modelParameters.getParameter< bool >("useSimpleMassExchange"); + const real_t cellConversionThreshold = modelParameters.getParameter< real_t >("cellConversionThreshold"); + const real_t cellConversionForceThreshold = modelParameters.getParameter< real_t >("cellConversionForceThreshold"); + const bool enableBubbleModel = modelParameters.getParameter< bool >("enableBubbleModel"); + const bool enableBubbleSplits = modelParameters.getParameter< bool >("enableBubbleSplits"); + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(pdfReconstructionModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(pdfRefillingModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(excessMassDistributionModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(curvatureModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableForceWeighting); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(useSimpleMassExchange); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellConversionThreshold); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellConversionForceThreshold); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableBubbleModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableBubbleSplits); + + // read evaluation parameters from parameter file + const auto evaluationParameters = walberlaEnv.config()->getOneBlock("EvaluationParameters"); + const uint_t performanceLogFrequency = evaluationParameters.getParameter< uint_t >("performanceLogFrequency"); + const uint_t evaluationFrequency = evaluationParameters.getParameter< uint_t >("evaluationFrequency"); + const std::string filename = evaluationParameters.getParameter< std::string >("filename"); + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(performanceLogFrequency); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(evaluationFrequency); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(filename); + + // create non-uniform block forest (non-uniformity required for load balancing) + const std::shared_ptr< StructuredBlockForest > blockForest = + createNonUniformBlockForest(domainSize, cellsPerBlock, numBlocks, periodicity); + + // add force field + const BlockDataID forceFieldID = + field::addToStorage< VectorField_T >(blockForest, "Force field", force, field::fzyx, uint_c(1)); + + // create lattice model + const LatticeModel_T latticeModel = LatticeModel_T(collisionModel, ForceModel_T(forceFieldID)); + + // add pdf field + const BlockDataID pdfFieldID = lbm::addPdfFieldToStorage(blockForest, "PDF field", latticeModel, field::fzyx); + + // add fill level field (initialized with 0, i.e., gas everywhere) + const BlockDataID fillFieldID = + field::addToStorage< ScalarField_T >(blockForest, "Fill level field", real_c(0.0), field::fzyx, uint_c(2)); + + // add boundary handling + const std::shared_ptr< FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling = + std::make_shared< FreeSurfaceBoundaryHandling_T >(blockForest, pdfFieldID, fillFieldID); + const BlockDataID flagFieldID = freeSurfaceBoundaryHandling->getFlagFieldID(); + const typename FreeSurfaceBoundaryHandling_T::FlagInfo_T& flagInfo = freeSurfaceBoundaryHandling->getFlagInfo(); + + // samples used in the Monte-Carlo-like estimation of the fill level + const uint_t fillLevelInitSamples = uint_c(100); // actually there will be 101 since 0 is also included + + const uint_t numTotalPoints = (fillLevelInitSamples + uint_c(1)) * (fillLevelInitSamples + uint_c(1)); + const real_t stepsize = real_c(1) / real_c(fillLevelInitSamples); + + // initialize sine profile such that there is exactly one period in the domain, i.e., with wavelength=domainSize[0]; + // every length is normalized with domainSize[0] + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + ScalarField_T* const fillField = blockIt->getData< ScalarField_T >(fillFieldID); + + WALBERLA_FOR_ALL_CELLS(fillFieldIt, fillField, { + // cell in block-local coordinates + const Cell localCell = fillFieldIt.cell(); + + // get cell in global coordinates + Cell globalCell = fillFieldIt.cell(); + blockForest->transformBlockLocalToGlobalCell(globalCell, *blockIt, localCell); + + // Monte-Carlo like estimation of the fill level: + // create uniformly-distributed sample points in each cell and count the number of points below the sine + // profile; this fraction of points is used as the fill level to initialize the profile + uint_t numPointsBelow = uint_c(0); + + for (uint_t xSample = uint_c(0); xSample <= fillLevelInitSamples; ++xSample) + { + // value of the sine-function + const real_t functionValue = initializationProfile(real_c(globalCell[0]) + real_c(xSample) * stepsize, + initialAmplitude, liquidDepth, real_c(domainSize[0])); + + for (uint_t ySample = uint_c(0); ySample <= fillLevelInitSamples; ++ySample) + { + const real_t yPoint = real_c(globalCell[1]) + real_c(ySample) * stepsize; + // with operator <, a fill level of 1 can not be reached when the line is equal to the cell's top border; + // with operator <=, a fill level of 0 can not be reached when the line is equal to the cell's bottom + // border + if (yPoint < functionValue) { ++numPointsBelow; } + } + } + + // fill level is fraction of points below sine profile + fillField->get(localCell) = real_c(numPointsBelow) / real_c(numTotalPoints); + }) // WALBERLA_FOR_ALL_CELLS + } + + // initialize domain boundary conditions from config file + const auto boundaryParameters = walberlaEnv.config()->getOneBlock("BoundaryParameters"); + freeSurfaceBoundaryHandling->initFromConfig(boundaryParameters); + + // IMPORTANT REMARK: this must be called only after every solid flag has been set; otherwise, the boundary handling + // might not detect solid flags correctly + freeSurfaceBoundaryHandling->initFlagsFromFillLevel(); + + // communication after initialization + Communication_T communication(blockForest, flagFieldID, fillFieldID, forceFieldID); + communication(); + + PdfCommunication_T pdfCommunication(blockForest, pdfFieldID); + pdfCommunication(); + + // add bubble model + std::shared_ptr< bubble_model::BubbleModelBase > bubbleModel = nullptr; + if (enableBubbleModel) + { + const std::shared_ptr< bubble_model::BubbleModel< CommunicationStencil_T > > bubbleModelDerived = + std::make_shared< bubble_model::BubbleModel< CommunicationStencil_T > >(blockForest, enableBubbleSplits); + bubbleModelDerived->initFromFillLevelField(fillFieldID); + bubbleModelDerived->setAtmosphere(Cell(domainSize[0] - uint_c(1), domainSize[1] - uint_c(1), uint_c(0)), + real_c(1)); + + bubbleModel = std::static_pointer_cast< bubble_model::BubbleModelBase >(bubbleModelDerived); + } + else { bubbleModel = std::make_shared< bubble_model::BubbleModelConstantPressure >(real_c(1)); } + + // initialize hydrostatic pressure + initHydrostaticPressure< PdfField_T >(blockForest, pdfFieldID, force, liquidDepth); + + // set density in non-liquid or non-interface cells to one (after initializing with hydrostatic pressure) + setDensityInNonFluidCellsToOne< FlagField_T, PdfField_T >(blockForest, flagInfo, flagFieldID, pdfFieldID); + + // create timeloop + SweepTimeloop timeloop(blockForest, timesteps); + + const real_t surfaceTension = real_c(0); + + // Laplace pressure = 2 * surface tension * curvature; curvature computation is not necessary with zero surface + // tension + bool computeCurvature = false; + if (!realIsEqual(surfaceTension, real_c(0), real_c(1e-14))) { computeCurvature = true; } + + // add surface geometry handler + const SurfaceGeometryHandler< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > geometryHandler( + blockForest, freeSurfaceBoundaryHandling, fillFieldID, curvatureModel, computeCurvature, enableWetting, + contactAngle); + + geometryHandler.addSweeps(timeloop); + + // get fields created by surface geometry handler + const ConstBlockDataID curvatureFieldID = geometryHandler.getConstCurvatureFieldID(); + const ConstBlockDataID normalFieldID = geometryHandler.getConstNormalFieldID(); + + // add boundary handling for standard boundaries and free surface boundaries + const SurfaceDynamicsHandler< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > dynamicsHandler( + blockForest, pdfFieldID, flagFieldID, fillFieldID, forceFieldID, normalFieldID, curvatureFieldID, + freeSurfaceBoundaryHandling, bubbleModel, pdfReconstructionModel, pdfRefillingModel, excessMassDistributionModel, + relaxationRate, force, surfaceTension, enableForceWeighting, useSimpleMassExchange, cellConversionThreshold, + cellConversionForceThreshold); + + dynamicsHandler.addSweeps(timeloop); + + // add load balancing + const LoadBalancer< FlagField_T, CommunicationStencil_T, LatticeModelStencil_T > loadBalancer( + blockForest, communication, pdfCommunication, bubbleModel, uint_c(50), uint_c(10), uint_c(5), + loadBalancingFrequency, printLoadBalancingStatistics); + timeloop.addFuncAfterTimeStep(loadBalancer, "Sweep: load balancing"); + + // add sweep for evaluating the surface position in y-direction + const std::shared_ptr< real_t > surfaceYPosition = std::make_shared< real_t >(real_c(0)); + const SurfaceYPositionEvaluator< FreeSurfaceBoundaryHandling_T > positionEvaluator( + blockForest, freeSurfaceBoundaryHandling, fillFieldID, domainSize, cell_idx_c(real_c(domainWidth) * real_c(0.5)), + evaluationFrequency, surfaceYPosition); + timeloop.addFuncAfterTimeStep(positionEvaluator, "Evaluator: surface position"); + + // add sweep for evaluating the symmetry of the fill level field in x-direction + const std::shared_ptr< real_t > symmetryNorm = std::make_shared< real_t >(real_c(0)); + const SymmetryXEvaluator< ScalarField_T > symmetryEvaluator(blockForest, fillFieldID, domainSize, + evaluationFrequency, symmetryNorm); + timeloop.addFuncAfterTimeStep(symmetryEvaluator, "Evaluator: symmetry norm"); + + // add VTK output + addVTKOutput< LatticeModel_T, FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T, VectorField_T >( + blockForest, timeloop, walberlaEnv.config(), flagInfo, pdfFieldID, flagFieldID, fillFieldID, forceFieldID, + geometryHandler.getCurvatureFieldID(), geometryHandler.getNormalFieldID(), + geometryHandler.getObstNormalFieldID()); + + // add triangle mesh output of free surface + SurfaceMeshWriter< ScalarField_T, FlagField_T > surfaceMeshWriter( + blockForest, fillFieldID, flagFieldID, flagIDs::liquidInterfaceGasFlagIDs, real_c(0), walberlaEnv.config()); + surfaceMeshWriter(); // write initial mesh + timeloop.addFuncAfterTimeStep(surfaceMeshWriter, "Writer: surface mesh"); + + // add logging for computational performance + const lbm::PerformanceLogger< FlagField_T > performanceLogger( + blockForest, flagFieldID, flagIDs::liquidInterfaceFlagIDs, performanceLogFrequency); + timeloop.addFuncAfterTimeStep(performanceLogger, "Evaluator: performance logging"); + + WcTimingPool timingPool; + + for (uint_t t = uint_c(0); t != timesteps; ++t) + { + timeloop.singleStep(timingPool, true); + + WALBERLA_ROOT_SECTION() + { + // non-dimensionalize time and surface position + const real_t tNonDimensional = real_c(t) * waveFrequency; + const real_t positionNonDimensional = (*surfaceYPosition - liquidDepth) / initialAmplitude; + + const std::vector< real_t > resultVector{ tNonDimensional, positionNonDimensional, *symmetryNorm }; + if (t % evaluationFrequency == uint_c(0)) + { + WALBERLA_LOG_DEVEL("time step = " << t); + WALBERLA_LOG_DEVEL("\t\ttNonDimensional = " << tNonDimensional + << "\n\t\tpositionNonDimensional = " << positionNonDimensional + << "\n\t\tsymmetryNorm = " << *symmetryNorm); + writeVectorToFile(resultVector, filename); + } + } + + if (t % performanceLogFrequency == uint_c(0) && t > uint_c(0)) { timingPool.logResultOnRoot(); } + } + + return EXIT_SUCCESS; +} + +template< typename T > +void writeVectorToFile(const std::vector< T >& vector, const std::string& filename) +{ + std::fstream file; + file.open(filename, std::fstream::app); + + for (const auto i : vector) + { + file << "\t" << i; + } + + file << "\n"; + file.close(); +} + +} // namespace GravityWave +} // namespace free_surface +} // namespace walberla + +int main(int argc, char** argv) { return walberla::free_surface::GravityWave::main(argc, argv); } \ No newline at end of file diff --git a/apps/showcases/FreeSurface/GravityWave.prm b/apps/showcases/FreeSurface/GravityWave.prm new file mode 100644 index 000000000..363a59c84 --- /dev/null +++ b/apps/showcases/FreeSurface/GravityWave.prm @@ -0,0 +1,107 @@ +BlockForestParameters +{ + cellsPerBlock < 25, 25, 1 >; + periodicity < 1, 0, 1 >; + loadBalancingFrequency 0; + printLoadBalancingStatistics false; +} + +DomainParameters +{ + domainWidth 200; // equivalent to wavelength + liquidDepth 100; + initialAmplitude 2; +} + +PhysicsParameters +{ + reynoldsNumber 10; + relaxationRate 1.8; + enableWetting false; + contactAngle 0; // only used if enableWetting=true + timesteps 86400; +} + +ModelParameters +{ + pdfReconstructionModel OnlyMissing; + pdfRefillingModel EquilibriumRefilling; + excessMassDistributionModel EvenlyAllInterface; + curvatureModel FiniteDifferenceMethod; + enableForceWeighting false; + useSimpleMassExchange false; + cellConversionThreshold 1e-2; + cellConversionForceThreshold 1e-1; + + enableBubbleModel false; + enableBubbleSplits false; // only used if enableBubbleModel=true +} + +EvaluationParameters +{ + performanceLogFrequency 43200; + evaluationFrequency 864; + filename gravity-wave.txt; +} + +BoundaryParameters +{ + // X + //Border { direction W; walldistance -1; NoSlip{} } + //Border { direction E; walldistance -1; NoSlip{} } + + // Y + Border { direction N; walldistance -1; NoSlip{} } + Border { direction S; walldistance -1; NoSlip{} } + + // Z + //Border { direction T; walldistance -1; NoSlip{} } + //Border { direction B; walldistance -1; NoSlip{} } +} + +MeshOutputParameters +{ + writeFrequency 864; + baseFolder mesh-out; +} + +VTK +{ + fluid_field + { + writeFrequency 864; + ghostLayers 0; + baseFolder vtk-out; + samplingResolution 1; + + writers + { + fill_level; + mapped_flag; + velocity; + density; + //curvature; + //normal; + //obstacle_normal; + //pdf; + //flag; + //force; + } + + inclusion_filters + { + //liquidInterfaceFilter; // only include liquid and interface cells in VTK output + } + + before_functions + { + //ghost_layer_synchronization; // only needed if writing the ghost layer + } + } + domain_decomposition + { + writeFrequency 0; + baseFolder vtk-out; + outputDomainDecomposition true; + } +} \ No newline at end of file diff --git a/apps/showcases/FreeSurface/GravityWaveCodegen.cpp b/apps/showcases/FreeSurface/GravityWaveCodegen.cpp new file mode 100644 index 000000000..a6b41aea0 --- /dev/null +++ b/apps/showcases/FreeSurface/GravityWaveCodegen.cpp @@ -0,0 +1,580 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file GravityWaveCodegen.cpp +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +// +// This showcase simulates a standing wave purely governed by gravity, i.e., without surface tension forces. The +// implementation uses an LBM kernel generated with lbmpy. +//====================================================================================================================== + +#include "blockforest/Initialization.h" + +#include "core/Environment.h" + +#include "field/Gather.h" + +#include "lbm/PerformanceLogger.h" +#include "lbm/field/AddToStorage.h" +#include "lbm/free_surface/LoadBalancing.h" +#include "lbm/free_surface/SurfaceMeshWriter.h" +#include "lbm/free_surface/TotalMassComputer.h" +#include "lbm/free_surface/VtkWriter.h" +#include "lbm/free_surface/bubble_model/Geometry.h" +#include "lbm/free_surface/dynamics/SurfaceDynamicsHandler.h" +#include "lbm/free_surface/surface_geometry/SurfaceGeometryHandler.h" +#include "lbm/free_surface/surface_geometry/Utility.h" +#include "lbm/lattice_model/D3Q19.h" + +#include "GravityWaveLatticeModel.h" + +namespace walberla +{ +namespace free_surface +{ +namespace GravityWaveCodegen +{ +using ScalarField_T = GhostLayerField< real_t, 1 >; +using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >; + +using LatticeModel_T = lbm::GravityWaveLatticeModel; +using LatticeModelStencil_T = LatticeModel_T::Stencil; +using PdfField_T = lbm::PdfField< LatticeModel_T >; +using PdfCommunication_T = blockforest::SimpleCommunication< LatticeModelStencil_T >; + +// the geometry computations in SurfaceGeometryHandler require meaningful values in the ghost layers in corner +// directions (flag field and fill level field); this holds, even if the lattice model uses a D3Q19 stencil +using CommunicationStencil_T = + typename std::conditional< LatticeModel_T::Stencil::D == uint_t(2), stencil::D2Q9, stencil::D3Q27 >::type; +using Communication_T = blockforest::SimpleCommunication< CommunicationStencil_T >; + +using flag_t = uint32_t; +using FlagField_T = FlagField< flag_t >; +using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >; + +// write each entry in "vector" to line in a file; columns are separated by tabs +template< typename T > +void writeVectorToFile(const std::vector< T >& vector, const std::string& filename); + +// function describing the global initialization profile +inline real_t initializationProfile(real_t x, real_t amplitude, real_t offset, real_t wavelength) +{ + return amplitude * std::cos(x / wavelength * real_c(2) * math::pi + math::pi) + offset; +} + +// evaluate the symmetry of the fill level field along the y-axis located at the center in x-direction +// IMPORTANT REMARK: This implementation is very inefficient, as it gathers the field on a single process to perform the +// evaluation. +template< typename ScalarField_T > +class SymmetryXEvaluator +{ + public: + SymmetryXEvaluator(const std::weak_ptr< StructuredBlockForest >& blockForest, const ConstBlockDataID& fillFieldID, + const Vector3< uint_t >& domainSize, uint_t interval, + const std::shared_ptr< real_t >& symmetryNorm) + : blockForest_(blockForest), fillFieldID_(fillFieldID), domainSize_(domainSize), interval_(interval), + symmetryNorm_(symmetryNorm), executionCounter_(uint_c(0)) + { + auto blockForestPtr = blockForest_.lock(); + WALBERLA_CHECK_NOT_NULLPTR(blockForestPtr); + } + + void operator()() + { + auto blockForest = blockForest_.lock(); + WALBERLA_CHECK_NOT_NULLPTR(blockForest); + + ++executionCounter_; + + // only evaluate in given intervals + if (executionCounter_ % interval_ != uint_c(0) && executionCounter_ != uint_c(1)) { return; } + + real_t fillLevelSum2 = real_c(0); // sum of each cell's squared fill level + real_t deltaFillLevelSum2 = real_c(0); // sum of each cell's fill level difference to its symmetrical counterpart + + // gather the fill level field on rank 0 (WARNING: simple, but very inefficient) + std::shared_ptr< ScalarField_T > fillFieldGathered = nullptr; + WALBERLA_ROOT_SECTION() + { + fillFieldGathered = + std::make_shared< ScalarField_T >(domainSize_[0], domainSize_[1], domainSize_[2], uint_c(0)); + } + field::gather< ScalarField_T, ScalarField_T >(*fillFieldGathered, blockForest, fillFieldID_); + + WALBERLA_ROOT_SECTION() + { + // get field's center-coordinate in x-direction + uint_t fieldXCenter = fillFieldGathered->xSize() / uint_c(2); + + WALBERLA_FOR_ALL_CELLS_XYZ(fillFieldGathered, { + // skip cells in the right half of the field in x-direction, as they are treated + // as mirrored cells later + if (x >= cell_idx_c(fieldXCenter)) { continue; } + + // get this cell's x-distance to the field's center + uint_t cellDistXCenter = uint_c(std::abs(int_c(fieldXCenter) - int_c(x))); + + // get x-coordinate of (mirrored) cell in the right half of the field + cell_idx_t fieldRightX = cell_idx_c(fieldXCenter + cellDistXCenter); + if (fillFieldGathered->xSize() % 2 == uint_c(0)) + { + fieldRightX -= cell_idx_c(1); // if xSize is even, the blocks on the right must be shifted by -1 + } + + // get fill level + const real_t fillLevel = fillFieldGathered->get(x, y, z); + real_t fillLevelMirrored = real_c(0); + fillLevelMirrored = fillFieldGathered->get(fieldRightX, y, z); + + fillLevelSum2 += fillLevel * fillLevel; + + const real_t deltaFill = fillLevel - fillLevelMirrored; + deltaFillLevelSum2 += deltaFill * deltaFill; + }) // WALBERLA_FOR_ALL_CELLS_XYZ + } + + // communicate values among all processes + mpi::allReduceInplace< real_t >(fillLevelSum2, mpi::SUM); + mpi::allReduceInplace< real_t >(deltaFillLevelSum2, mpi::SUM); + + // compute L2 norm evaluate symmetry + *symmetryNorm_ = real_c(std::pow(deltaFillLevelSum2 / fillLevelSum2, real_c(0.5))); + } + + private: + std::weak_ptr< StructuredBlockForest > blockForest_; + ConstBlockDataID fillFieldID_; + Vector3< uint_t > domainSize_; + uint_t interval_; + std::shared_ptr< real_t > symmetryNorm_; + uint_t executionCounter_; +}; // class SymmetryXEvaluator + +// get interface position in y-direction at the specified (global) x-coordinate +template< typename FreeSurfaceBoundaryHandling_T > +class SurfaceYPositionEvaluator +{ + public: + SurfaceYPositionEvaluator(const std::weak_ptr< const StructuredBlockForest >& blockForest, + const std::weak_ptr< const FreeSurfaceBoundaryHandling_T >& freeSurfaceBoundaryHandling, + const ConstBlockDataID& fillFieldID, const Vector3< uint_t >& domainSize, + cell_idx_t globalXCoordinate, uint_t frequency, + const std::shared_ptr< real_t >& surfaceYPosition) + : blockForest_(blockForest), freeSurfaceBoundaryHandling_(freeSurfaceBoundaryHandling), fillFieldID_(fillFieldID), + domainSize_(domainSize), globalXCoordinate_(globalXCoordinate), surfaceYPosition_(surfaceYPosition), + frequency_(frequency), executionCounter_(uint_c(0)) + {} + + void operator()() + { + auto blockForest = blockForest_.lock(); + WALBERLA_CHECK_NOT_NULLPTR(blockForest); + + auto freeSurfaceBoundaryHandling = freeSurfaceBoundaryHandling_.lock(); + WALBERLA_CHECK_NOT_NULLPTR(freeSurfaceBoundaryHandling); + + ++executionCounter_; + + // only evaluate in given frequencies + if (executionCounter_ % frequency_ != uint_c(0) && executionCounter_ != uint_c(1)) { return; } + + const BlockDataID flagFieldID = freeSurfaceBoundaryHandling->getFlagFieldID(); + const typename FreeSurfaceBoundaryHandling_T::FlagInfo_T& flagInfo = freeSurfaceBoundaryHandling->getFlagInfo(); + + *surfaceYPosition_ = real_c(0); + + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + real_t maxSurfaceYPosition = real_c(0); + + CellInterval globalSearchInterval(globalXCoordinate_, cell_idx_c(0), cell_idx_c(0), globalXCoordinate_, + cell_idx_c(domainSize_[1]), cell_idx_c(0)); + + if (blockForest->getBlockCellBB(*blockIt).overlaps(globalSearchInterval)) + { + // transform specified global x-coordinate into block local coordinate + Cell localEvalCell = Cell(globalXCoordinate_, cell_idx_c(0), cell_idx_c(0)); + blockForest->transformGlobalToBlockLocalCell(localEvalCell, *blockIt); + + const FlagField_T* const flagField = blockIt->template getData< const FlagField_T >(flagFieldID); + const ScalarField_T* const fillField = blockIt->template getData< const ScalarField_T >(fillFieldID_); + + // searching from top ensures that the interface cell with the greatest y-coordinate is found first + for (cell_idx_t y = cell_idx_c((flagField)->ySize() - uint_c(1)); y >= cell_idx_t(0); --y) + { + if (flagInfo.isInterface(flagField->get(localEvalCell[0], y, cell_idx_c(0)))) + { + const real_t fillLevel = fillField->get(localEvalCell[0], y, cell_idx_c(0)); + + // transform local y-coordinate to global coordinate + Cell localResultCell = localEvalCell; + localResultCell[1] = y; + blockForest->transformBlockLocalToGlobalCell(localResultCell, *blockIt); + maxSurfaceYPosition = real_c(localResultCell[1]) + fillLevel; + + break; + } + } + } + + if (maxSurfaceYPosition > *surfaceYPosition_) { *surfaceYPosition_ = maxSurfaceYPosition; } + } + // communicate result among all processes + mpi::allReduceInplace< real_t >(*surfaceYPosition_, mpi::MAX); + } + + private: + std::weak_ptr< const StructuredBlockForest > blockForest_; + std::weak_ptr< const FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling_; + ConstBlockDataID fillFieldID_; + Vector3< uint_t > domainSize_; + cell_idx_t globalXCoordinate_; + std::shared_ptr< real_t > surfaceYPosition_; + + uint_t frequency_; + uint_t executionCounter_; +}; // class SurfaceYPositionEvaluator + +int main(int argc, char** argv) +{ + Environment walberlaEnv(argc, argv); + + if (argc < 2) { WALBERLA_ABORT("Please specify a parameter file as input argument.") } + + // print content of parameter file + WALBERLA_LOG_INFO_ON_ROOT(*walberlaEnv.config()); + + // get block forest parameters from parameter file + auto blockForestParameters = walberlaEnv.config()->getOneBlock("BlockForestParameters"); + const Vector3< uint_t > cellsPerBlock = blockForestParameters.getParameter< Vector3< uint_t > >("cellsPerBlock"); + const Vector3< bool > periodicity = blockForestParameters.getParameter< Vector3< bool > >("periodicity"); + const uint_t loadBalancingFrequency = blockForestParameters.getParameter< uint_t >("loadBalancingFrequency"); + const bool printLoadBalancingStatistics = blockForestParameters.getParameter< bool >("printLoadBalancingStatistics"); + + // get domain parameters from parameter file + auto domainParameters = walberlaEnv.config()->getOneBlock("DomainParameters"); + const uint_t domainWidth = domainParameters.getParameter< uint_t >("domainWidth"); + const real_t liquidDepth = domainParameters.getParameter< real_t >("liquidDepth"); + const real_t initialAmplitude = domainParameters.getParameter< real_t >("initialAmplitude"); + + // define domain size + Vector3< uint_t > domainSize; + domainSize[0] = domainWidth; + domainSize[1] = uint_c(liquidDepth * real_c(2)); + domainSize[2] = uint_c(1); + + // compute number of blocks as defined by domainSize and cellsPerBlock + Vector3< uint_t > numBlocks; + numBlocks[0] = uint_c(std::ceil(real_c(domainSize[0]) / real_c(cellsPerBlock[0]))); + numBlocks[1] = uint_c(std::ceil(real_c(domainSize[1]) / real_c(cellsPerBlock[1]))); + numBlocks[2] = uint_c(std::ceil(real_c(domainSize[2]) / real_c(cellsPerBlock[2]))); + + // get number of (MPI) processes + uint_t numProcesses = uint_c(MPIManager::instance()->numProcesses()); + WALBERLA_CHECK_LESS_EQUAL(numProcesses, numBlocks[0] * numBlocks[1] * numBlocks[2], + "The number of MPI processes is greater than the number of blocks as defined by " + "\"domainSize/cellsPerBlock\". This would result in unused MPI processes. Either decrease " + "the number of MPI processes or increase \"cellsPerBlock\".") + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(numProcesses); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellsPerBlock); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(domainSize); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(numBlocks); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(domainWidth); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(liquidDepth); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(initialAmplitude); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(periodicity); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(loadBalancingFrequency); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(printLoadBalancingStatistics); + + // get physics parameters from parameter file + auto physicsParameters = walberlaEnv.config()->getOneBlock("PhysicsParameters"); + const uint_t timesteps = physicsParameters.getParameter< uint_t >("timesteps"); + + const real_t relaxationRate = physicsParameters.getParameter< real_t >("relaxationRate"); + const real_t viscosity = real_c(1) / real_c(3) * (real_c(1) / relaxationRate - real_c(0.5)); + + const real_t reynoldsNumber = physicsParameters.getParameter< real_t >("reynoldsNumber"); + const real_t waveNumber = real_c(2) * math::pi / real_c(domainSize[0]); + const real_t waveFrequency = reynoldsNumber * viscosity / real_c(domainSize[0]) / initialAmplitude; + const real_t forceY = -(waveFrequency * waveFrequency) / waveNumber / std::tanh(waveNumber * liquidDepth); + const Vector3< real_t > force(real_c(0), forceY, real_c(0)); + + const bool enableWetting = physicsParameters.getParameter< bool >("enableWetting"); + const real_t contactAngle = physicsParameters.getParameter< real_t >("contactAngle"); + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(reynoldsNumber); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(relaxationRate); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableWetting); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(contactAngle); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(timesteps); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(viscosity); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(force); + + // read model parameters from parameter file + const auto modelParameters = walberlaEnv.config()->getOneBlock("ModelParameters"); + const std::string pdfReconstructionModel = modelParameters.getParameter< std::string >("pdfReconstructionModel"); + const std::string pdfRefillingModel = modelParameters.getParameter< std::string >("pdfRefillingModel"); + const std::string excessMassDistributionModel = + modelParameters.getParameter< std::string >("excessMassDistributionModel"); + const std::string curvatureModel = modelParameters.getParameter< std::string >("curvatureModel"); + const bool enableForceWeighting = modelParameters.getParameter< bool >("enableForceWeighting"); + const bool useSimpleMassExchange = modelParameters.getParameter< bool >("useSimpleMassExchange"); + const real_t cellConversionThreshold = modelParameters.getParameter< real_t >("cellConversionThreshold"); + const real_t cellConversionForceThreshold = modelParameters.getParameter< real_t >("cellConversionForceThreshold"); + const bool enableBubbleModel = modelParameters.getParameter< bool >("enableBubbleModel"); + const bool enableBubbleSplits = modelParameters.getParameter< bool >("enableBubbleSplits"); + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(pdfReconstructionModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(pdfRefillingModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(excessMassDistributionModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(curvatureModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableForceWeighting); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(useSimpleMassExchange); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellConversionThreshold); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellConversionForceThreshold); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableBubbleModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableBubbleSplits); + + // read evaluation parameters from parameter file + const auto evaluationParameters = walberlaEnv.config()->getOneBlock("EvaluationParameters"); + const uint_t performanceLogFrequency = evaluationParameters.getParameter< uint_t >("performanceLogFrequency"); + const uint_t evaluationFrequency = evaluationParameters.getParameter< uint_t >("evaluationFrequency"); + const std::string filename = evaluationParameters.getParameter< std::string >("filename"); + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(performanceLogFrequency); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(evaluationFrequency); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(filename); + + // create non-uniform block forest (non-uniformity required for load balancing) + const std::shared_ptr< StructuredBlockForest > blockForest = + createNonUniformBlockForest(domainSize, cellsPerBlock, numBlocks, periodicity); + + // add force field + const BlockDataID forceFieldID = + field::addToStorage< VectorField_T >(blockForest, "Force field", force, field::fzyx, uint_c(1)); + + // create lattice model + LatticeModel_T latticeModel = LatticeModel_T(force[0], force[1], force[2], relaxationRate); + + // add pdf field + const BlockDataID pdfFieldID = lbm::addPdfFieldToStorage(blockForest, "PDF field", latticeModel, field::fzyx); + + // add fill level field (initialized with 0, i.e., gas everywhere) + const BlockDataID fillFieldID = + field::addToStorage< ScalarField_T >(blockForest, "Fill level field", real_c(0.0), field::fzyx, uint_c(2)); + + // add boundary handling + const std::shared_ptr< FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling = + std::make_shared< FreeSurfaceBoundaryHandling_T >(blockForest, pdfFieldID, fillFieldID); + const BlockDataID flagFieldID = freeSurfaceBoundaryHandling->getFlagFieldID(); + const typename FreeSurfaceBoundaryHandling_T::FlagInfo_T& flagInfo = freeSurfaceBoundaryHandling->getFlagInfo(); + + // samples used in the Monte-Carlo like estimation of the fill level + const uint_t fillLevelInitSamples = uint_c(100); // actually there will be 101 since 0 is also included + + const uint_t numTotalPoints = (fillLevelInitSamples + uint_c(1)) * (fillLevelInitSamples + uint_c(1)); + const real_t stepsize = real_c(1) / real_c(fillLevelInitSamples); + + // initialize sine profile such that there is exactly one period in the domain, i.e., with wavelength=domainSize[0]; + // every length is normalized with domainSize[0] + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + ScalarField_T* const fillField = blockIt->getData< ScalarField_T >(fillFieldID); + + WALBERLA_FOR_ALL_CELLS(fillFieldIt, fillField, { + // cell in block-local coordinates + const Cell localCell = fillFieldIt.cell(); + + // get cell in global coordinates + Cell globalCell = fillFieldIt.cell(); + blockForest->transformBlockLocalToGlobalCell(globalCell, *blockIt, localCell); + + // Monte-Carlo like estimation of the fill level: + // create uniformly-distributed sample points in each cell and count the number of points below the sine + // profile; this fraction of points is used as the fill level to initialize the profile + uint_t numPointsBelow = uint_c(0); + + for (uint_t xSample = uint_c(0); xSample <= fillLevelInitSamples; ++xSample) + { + // value of the sine-function + const real_t functionValue = initializationProfile(real_c(globalCell[0]) + real_c(xSample) * stepsize, + initialAmplitude, liquidDepth, real_c(domainSize[0])); + + for (uint_t ySample = uint_c(0); ySample <= fillLevelInitSamples; ++ySample) + { + const real_t yPoint = real_c(globalCell[1]) + real_c(ySample) * stepsize; + // with operator <, a fill level of 1 can not be reached when the line is equal to the cell's top border; + // with operator <=, a fill level of 0 can not be reached when the line is equal to the cell's bottom + // border + if (yPoint < functionValue) { ++numPointsBelow; } + } + } + + // fill level is fraction of points below sine profile + fillField->get(localCell) = real_c(numPointsBelow) / real_c(numTotalPoints); + }) // WALBERLA_FOR_ALL_CELLS + } + + // initialize domain boundary conditions from config file + const auto boundaryParameters = walberlaEnv.config()->getOneBlock("BoundaryParameters"); + freeSurfaceBoundaryHandling->initFromConfig(boundaryParameters); + + // IMPORTANT REMARK: this must be only called after every solid flag has been set; otherwise, the boundary handling + // might not detect solid flags correctly + freeSurfaceBoundaryHandling->initFlagsFromFillLevel(); + + // communication after initialization + Communication_T communication(blockForest, flagFieldID, fillFieldID, forceFieldID); + communication(); + + PdfCommunication_T pdfCommunication(blockForest, pdfFieldID); + pdfCommunication(); + + // add bubble model + std::shared_ptr< bubble_model::BubbleModelBase > bubbleModel = nullptr; + if (enableBubbleModel) + { + const std::shared_ptr< bubble_model::BubbleModel< CommunicationStencil_T > > bubbleModelDerived = + std::make_shared< bubble_model::BubbleModel< CommunicationStencil_T > >(blockForest, enableBubbleSplits); + bubbleModelDerived->initFromFillLevelField(fillFieldID); + bubbleModelDerived->setAtmosphere(Cell(domainSize[0] - uint_c(1), domainSize[1] - uint_c(1), uint_c(0)), + real_c(1)); + + bubbleModel = std::static_pointer_cast< bubble_model::BubbleModelBase >(bubbleModelDerived); + } + else { bubbleModel = std::make_shared< bubble_model::BubbleModelConstantPressure >(real_c(1)); } + + // initialize hydrostatic pressure + initHydrostaticPressure< PdfField_T >(blockForest, pdfFieldID, force, liquidDepth); + + // set density in non-liquid or non-interface cells to 1 (after initializing with hydrostatic pressure) + setDensityInNonFluidCellsToOne< FlagField_T, PdfField_T >(blockForest, flagInfo, flagFieldID, pdfFieldID); + + // create timeloop + SweepTimeloop timeloop(blockForest, timesteps); + + const real_t surfaceTension = real_c(0); + + // Laplace pressure = 2 * surface tension * curvature; curvature computation is not necessary with no surface + // tension + bool computeCurvature = false; + if (!realIsEqual(surfaceTension, real_c(0), real_c(1e-14))) { computeCurvature = true; } + + // add surface geometry handler + const SurfaceGeometryHandler< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > geometryHandler( + blockForest, freeSurfaceBoundaryHandling, fillFieldID, curvatureModel, computeCurvature, enableWetting, + contactAngle); + + geometryHandler.addSweeps(timeloop); + + // get fields created by surface geometry handler + const ConstBlockDataID curvatureFieldID = geometryHandler.getConstCurvatureFieldID(); + const ConstBlockDataID normalFieldID = geometryHandler.getConstNormalFieldID(); + + // add boundary handling for standard boundaries and free surface boundaries + const SurfaceDynamicsHandler< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T, true > dynamicsHandler( + blockForest, pdfFieldID, flagFieldID, fillFieldID, forceFieldID, normalFieldID, curvatureFieldID, + freeSurfaceBoundaryHandling, bubbleModel, pdfReconstructionModel, pdfRefillingModel, excessMassDistributionModel, + relaxationRate, force, surfaceTension, enableForceWeighting, useSimpleMassExchange, cellConversionThreshold, + cellConversionForceThreshold); + + dynamicsHandler.addSweeps(timeloop); + + // add load balancing + const LoadBalancer< FlagField_T, CommunicationStencil_T, LatticeModelStencil_T > loadBalancer( + blockForest, communication, pdfCommunication, bubbleModel, uint_c(50), uint_c(10), uint_c(5), + loadBalancingFrequency, printLoadBalancingStatistics); + timeloop.addFuncAfterTimeStep(loadBalancer, "Sweep: load balancing"); + + // add sweep for evaluating the surface position in y-direction + const std::shared_ptr< real_t > surfaceYPosition = std::make_shared< real_t >(real_c(0)); + const SurfaceYPositionEvaluator< FreeSurfaceBoundaryHandling_T > positionEvaluator( + blockForest, freeSurfaceBoundaryHandling, fillFieldID, domainSize, cell_idx_c(real_c(domainWidth) * real_c(0.5)), + evaluationFrequency, surfaceYPosition); + timeloop.addFuncAfterTimeStep(positionEvaluator, "Evaluator: surface position"); + + // add sweep for evaluating the symmetry of the fill level field in x-direction + const std::shared_ptr< real_t > symmetryNorm = std::make_shared< real_t >(real_c(0)); + const SymmetryXEvaluator< ScalarField_T > symmetryEvaluator(blockForest, fillFieldID, domainSize, + evaluationFrequency, symmetryNorm); + timeloop.addFuncAfterTimeStep(symmetryEvaluator, "Evaluator: symmetry norm"); + + // add VTK output + addVTKOutput< LatticeModel_T, FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T, VectorField_T >( + blockForest, timeloop, walberlaEnv.config(), flagInfo, pdfFieldID, flagFieldID, fillFieldID, forceFieldID, + geometryHandler.getCurvatureFieldID(), geometryHandler.getNormalFieldID(), + geometryHandler.getObstNormalFieldID()); + + // add triangle mesh output of free surface + SurfaceMeshWriter< ScalarField_T, FlagField_T > surfaceMeshWriter( + blockForest, fillFieldID, flagFieldID, flagIDs::liquidInterfaceGasFlagIDs, real_c(0), walberlaEnv.config()); + surfaceMeshWriter(); // write initial mesh + timeloop.addFuncAfterTimeStep(surfaceMeshWriter, "Writer: surface mesh"); + + // add logging for computational performance + const lbm::PerformanceLogger< FlagField_T > performanceLogger( + blockForest, flagFieldID, flagIDs::liquidInterfaceFlagIDs, performanceLogFrequency); + timeloop.addFuncAfterTimeStep(performanceLogger, "Evaluator: performance logging"); + + WcTimingPool timingPool; + + for (uint_t t = uint_c(0); t != timesteps; ++t) + { + timeloop.singleStep(timingPool, true); + + WALBERLA_ROOT_SECTION() + { + // non-dimensionalize time and surface position + const real_t tNonDimensional = real_c(t) * waveFrequency; + const real_t positionNonDimensional = (*surfaceYPosition - liquidDepth) / initialAmplitude; + + const std::vector< real_t > resultVector{ tNonDimensional, positionNonDimensional, *symmetryNorm }; + if (t % evaluationFrequency == uint_c(0)) + { + WALBERLA_LOG_DEVEL("time step = " << t); + WALBERLA_LOG_DEVEL("\t\ttNonDimensional = " << tNonDimensional + << "\n\t\tpositionNonDimensional = " << positionNonDimensional + << "\n\t\tsymmetryNorm = " << *symmetryNorm); + writeVectorToFile(resultVector, filename); + } + } + + if (t % performanceLogFrequency == uint_c(0) && t > uint_c(0)) { timingPool.logResultOnRoot(); } + } + + return EXIT_SUCCESS; +} + +template< typename T > +void writeVectorToFile(const std::vector< T >& vector, const std::string& filename) +{ + std::fstream file; + file.open(filename, std::fstream::app); + + for (const auto i : vector) + { + file << "\t" << i; + } + + file << "\n"; + file.close(); +} + +} // namespace GravityWaveCodegen +} // namespace free_surface +} // namespace walberla + +int main(int argc, char** argv) { return walberla::free_surface::GravityWaveCodegen::main(argc, argv); } \ No newline at end of file diff --git a/apps/showcases/FreeSurface/GravityWaveLatticeModelGeneration.py b/apps/showcases/FreeSurface/GravityWaveLatticeModelGeneration.py new file mode 100644 index 000000000..22f474fcf --- /dev/null +++ b/apps/showcases/FreeSurface/GravityWaveLatticeModelGeneration.py @@ -0,0 +1,34 @@ +import sympy as sp + +from lbmpy.creationfunctions import LBMConfig, LBMOptimisation, create_lb_collision_rule +from lbmpy.enums import ForceModel, Method, Stencil +from lbmpy.stencils import LBStencil + +from pystencils_walberla import CodeGeneration +from lbmpy_walberla import generate_lattice_model + +# general parameters +stencil = LBStencil(Stencil.D3Q19) +omega = sp.Symbol('omega') +force = sp.symbols('force_:3') +layout = 'fzyx' + +# method definition +lbm_config = LBMConfig(stencil=stencil, + method=Method.SRT, + relaxation_rate=omega, + compressible=True, + force=force, + force_model=ForceModel.GUO, + zero_centered=False, + streaming_pattern='pull') # free surface implementation only works with pull pattern + +# optimizations to be used by the code generator +lbm_opt = LBMOptimisation(cse_global=True, + field_layout=layout) + +collision_rule = create_lb_collision_rule(lbm_config=lbm_config, + lbm_optimisation=lbm_opt) + +with CodeGeneration() as ctx: + generate_lattice_model(ctx, "GravityWaveLatticeModel", collision_rule, field_layout=layout) diff --git a/apps/showcases/FreeSurface/MovingDrop.cpp b/apps/showcases/FreeSurface/MovingDrop.cpp new file mode 100644 index 000000000..478ee193c --- /dev/null +++ b/apps/showcases/FreeSurface/MovingDrop.cpp @@ -0,0 +1,325 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file MovingDrop.cpp +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief This showcase simulates a moving drop through a periodic domain. +// +//====================================================================================================================== + +#include "blockforest/Initialization.h" + +#include "core/Environment.h" + +#include "lbm/PerformanceLogger.h" +#include "lbm/field/AddToStorage.h" +#include "lbm/free_surface/LoadBalancing.h" +#include "lbm/free_surface/SurfaceMeshWriter.h" +#include "lbm/free_surface/VtkWriter.h" +#include "lbm/free_surface/bubble_model/Geometry.h" +#include "lbm/free_surface/dynamics/SurfaceDynamicsHandler.h" +#include "lbm/free_surface/surface_geometry/SurfaceGeometryHandler.h" +#include "lbm/free_surface/surface_geometry/Utility.h" +#include "lbm/lattice_model/D3Q19.h" + +namespace walberla +{ +namespace free_surface +{ +namespace DropInPool +{ +using ScalarField_T = GhostLayerField< real_t, 1 >; +using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >; + +using CollisionModel_T = lbm::collision_model::SRT; +using ForceModel_T = lbm::force_model::GuoField< VectorField_T >; +using LatticeModel_T = lbm::D3Q19< CollisionModel_T, true, ForceModel_T, 2 >; +using LatticeModelStencil_T = LatticeModel_T::Stencil; +using PdfField_T = lbm::PdfField< LatticeModel_T >; +using PdfCommunication_T = blockforest::SimpleCommunication< LatticeModelStencil_T >; + +// the geometry computations in SurfaceGeometryHandler require meaningful values in the ghost layers in corner +// directions (flag field and fill level field); this holds, even if the lattice model uses a D3Q19 stencil +using CommunicationStencil_T = + typename std::conditional< LatticeModel_T::Stencil::D == uint_t(2), stencil::D2Q9, stencil::D3Q27 >::type; +using Communication_T = blockforest::SimpleCommunication< CommunicationStencil_T >; + +using flag_t = uint32_t; +using FlagField_T = FlagField< flag_t >; +using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >; + +int main(int argc, char** argv) +{ + Environment walberlaEnv(argc, argv); + + if (argc < 2) { WALBERLA_ABORT("Please specify a parameter file as input argument.") } + + // print content of parameter file + WALBERLA_LOG_INFO_ON_ROOT(*walberlaEnv.config()); + + // get block forest parameters from parameter file + auto blockForestParameters = walberlaEnv.config()->getOneBlock("BlockForestParameters"); + const Vector3< uint_t > cellsPerBlock = blockForestParameters.getParameter< Vector3< uint_t > >("cellsPerBlock"); + const Vector3< bool > periodicity = blockForestParameters.getParameter< Vector3< bool > >("periodicity"); + const uint_t loadBalancingFrequency = blockForestParameters.getParameter< uint_t >("loadBalancingFrequency"); + const bool printLoadBalancingStatistics = blockForestParameters.getParameter< bool >("printLoadBalancingStatistics"); + + // read domain parameters from parameter file + const auto domainParameters = walberlaEnv.config()->getOneBlock("DomainParameters"); + const real_t dropDiameter = domainParameters.getParameter< real_t >("dropDiameter"); + const Vector3< real_t > dropCenterFactor = domainParameters.getParameter< Vector3< real_t > >("dropCenterFactor"); + const Vector3< real_t > domainSizeFactor = domainParameters.getParameter< Vector3< real_t > >("domainSizeFactor"); + + // define domain size + Vector3< uint_t > domainSize = domainSizeFactor * dropDiameter; + domainSize[0] = uint_c(domainSizeFactor[0] * dropDiameter); + domainSize[1] = uint_c(domainSizeFactor[1] * dropDiameter); + domainSize[2] = uint_c(domainSizeFactor[2] * dropDiameter); + + // compute number of blocks as defined by domainSize and cellsPerBlock + Vector3< uint_t > numBlocks; + numBlocks[0] = uint_c(std::ceil(real_c(domainSize[0]) / real_c(cellsPerBlock[0]))); + numBlocks[1] = uint_c(std::ceil(real_c(domainSize[1]) / real_c(cellsPerBlock[1]))); + numBlocks[2] = uint_c(std::ceil(real_c(domainSize[2]) / real_c(cellsPerBlock[2]))); + + // get number of (MPI) processes + uint_t numProcesses = uint_c(MPIManager::instance()->numProcesses()); + WALBERLA_CHECK_LESS_EQUAL(numProcesses, numBlocks[0] * numBlocks[1] * numBlocks[2], + "The number of MPI processes is greater than the number of blocks as defined by " + "\"domainSize/cellsPerBlock\". This would result in unused MPI processes. Either decrease " + "the number of MPI processes or increase \"cellsPerBlock\".") + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(numProcesses); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellsPerBlock); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(numBlocks); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(dropDiameter); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(dropCenterFactor); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(domainSizeFactor); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(periodicity); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(domainSize); + + Vector3< uint_t > realDomainSize; + realDomainSize[0] = cellsPerBlock[0] * numBlocks[0]; + realDomainSize[1] = cellsPerBlock[1] * numBlocks[1]; + realDomainSize[2] = cellsPerBlock[2] * numBlocks[2]; + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(realDomainSize); + + if (domainSize[0] != realDomainSize[0] && periodicity[0]) + { + WALBERLA_ABORT( + "The specified domain size in x-direction can not be obtained with the number of blocks you specified.") + } + if (domainSize[1] != realDomainSize[1] && periodicity[1]) + { + WALBERLA_ABORT( + "The specified domain size in y-direction can not be obtained with the number of blocks you specified.") + } + if (domainSize[2] != realDomainSize[2] && periodicity[2]) + { + WALBERLA_ABORT( + "The specified domain size in z-direction can not be obtained with the number of blocks you specified.") + } + + // read physics parameters from parameter file + const auto physicsParameters = walberlaEnv.config()->getOneBlock("PhysicsParameters"); + const real_t weberNumber = physicsParameters.getParameter< real_t >("weberNumber"); + const real_t ohnesorgeNumber = physicsParameters.getParameter< real_t >("ohnesorgeNumber"); + const real_t relaxationRate = physicsParameters.getParameter< real_t >("relaxationRate"); + + const CollisionModel_T collisionModel = CollisionModel_T(relaxationRate); + const real_t viscosity = collisionModel.viscosity(); + + const real_t surfaceTension = real_c(std::pow(viscosity / ohnesorgeNumber, 2)) / dropDiameter; + const real_t initialVelocityX = real_c(std::pow(weberNumber * surfaceTension / dropDiameter, 0.5)); + + const Vector3< real_t > force(real_c(0)); + + const Vector3< real_t > initialVelocity(initialVelocityX, real_c(0), real_c(0)); + + const bool enableWetting = physicsParameters.getParameter< bool >("enableWetting"); + const real_t contactAngle = physicsParameters.getParameter< real_t >("contactAngle"); + + const uint_t timesteps = physicsParameters.getParameter< uint_t >("timesteps"); + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(relaxationRate); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(surfaceTension); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(force); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(initialVelocity); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableWetting); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(contactAngle); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(timesteps); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(viscosity); + + // read model parameters from parameter file + const auto modelParameters = walberlaEnv.config()->getOneBlock("ModelParameters"); + const std::string pdfReconstructionModel = modelParameters.getParameter< std::string >("pdfReconstructionModel"); + const std::string pdfRefillingModel = modelParameters.getParameter< std::string >("pdfRefillingModel"); + const std::string excessMassDistributionModel = + modelParameters.getParameter< std::string >("excessMassDistributionModel"); + const std::string curvatureModel = modelParameters.getParameter< std::string >("curvatureModel"); + const bool enableForceWeighting = modelParameters.getParameter< bool >("enableForceWeighting"); + const bool useSimpleMassExchange = modelParameters.getParameter< bool >("useSimpleMassExchange"); + const bool enableBubbleModel = modelParameters.getParameter< bool >("enableBubbleModel"); + const bool enableBubbleSplits = modelParameters.getParameter< bool >("enableBubbleSplits"); + const real_t cellConversionThreshold = modelParameters.getParameter< real_t >("cellConversionThreshold"); + const real_t cellConversionForceThreshold = modelParameters.getParameter< real_t >("cellConversionForceThreshold"); + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(pdfReconstructionModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(pdfRefillingModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(excessMassDistributionModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(curvatureModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableForceWeighting); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(useSimpleMassExchange); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableBubbleModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableBubbleSplits); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellConversionThreshold); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellConversionForceThreshold); + + // read evaluation parameters from parameter file + const auto evaluationParameters = walberlaEnv.config()->getOneBlock("EvaluationParameters"); + const uint_t performanceLogFrequency = evaluationParameters.getParameter< uint_t >("performanceLogFrequency"); + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(performanceLogFrequency); + + // create non-uniform block forest (non-uniformity required for load balancing) + const std::shared_ptr< StructuredBlockForest > blockForest = + createNonUniformBlockForest(domainSize, cellsPerBlock, numBlocks, periodicity); + + // add force field + const BlockDataID forceFieldID = + field::addToStorage< VectorField_T >(blockForest, "Force field", force, field::fzyx, uint_c(1)); + + // create lattice model + const LatticeModel_T latticeModel = LatticeModel_T(collisionModel, ForceModel_T(forceFieldID)); + + // add pdf field + const BlockDataID pdfFieldID = + lbm::addPdfFieldToStorage(blockForest, "PDF field", latticeModel, initialVelocity, real_c(1), field::fzyx); + + // add fill level field (initialized with 0, i.e., gas everywhere) + const BlockDataID fillFieldID = + field::addToStorage< ScalarField_T >(blockForest, "Fill level field", real_c(0.0), field::fzyx, uint_c(2)); + + // add boundary handling + const std::shared_ptr< FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling = + std::make_shared< FreeSurfaceBoundaryHandling_T >(blockForest, pdfFieldID, fillFieldID); + const BlockDataID flagFieldID = freeSurfaceBoundaryHandling->getFlagFieldID(); + const typename FreeSurfaceBoundaryHandling_T::FlagInfo_T& flagInfo = freeSurfaceBoundaryHandling->getFlagInfo(); + + const Vector3< real_t > dropCenter = dropCenterFactor * dropDiameter; + + const geometry::Sphere sphereDrop(dropCenter, dropDiameter * real_c(0.5)); + bubble_model::addBodyToFillLevelField< geometry::Sphere >(*blockForest, fillFieldID, sphereDrop, false); + + // initialize boundary conditions from config file + const auto boundaryParameters = walberlaEnv.config()->getOneBlock("BoundaryParameters"); + freeSurfaceBoundaryHandling->initFromConfig(boundaryParameters); + + // IMPORTANT REMARK: this must be only called after every solid flag has been set; otherwise, the boundary handling + // might not detect solid flags correctly + freeSurfaceBoundaryHandling->initFlagsFromFillLevel(); + + // communication after initialization + Communication_T communication(blockForest, flagFieldID, fillFieldID, forceFieldID); + communication(); + + PdfCommunication_T pdfCommunication(blockForest, pdfFieldID); + pdfCommunication(); + + // add bubble model + std::shared_ptr< bubble_model::BubbleModelBase > bubbleModel = nullptr; + if (enableBubbleModel) + { + const std::shared_ptr< bubble_model::BubbleModel< CommunicationStencil_T > > bubbleModelDerived = + std::make_shared< bubble_model::BubbleModel< CommunicationStencil_T > >(blockForest, enableBubbleSplits); + bubbleModelDerived->initFromFillLevelField(fillFieldID); + bubbleModelDerived->setAtmosphere( + Cell(domainSize[0] - uint_c(1), domainSize[1] - uint_c(1), domainSize[2] - uint_c(1)), real_c(1)); + + bubbleModel = std::static_pointer_cast< bubble_model::BubbleModelBase >(bubbleModelDerived); + } + else { bubbleModel = std::make_shared< bubble_model::BubbleModelConstantPressure >(real_c(1)); } + + // set density in non-liquid or non-interface cells to 1 (after initializing with hydrostatic pressure) + setDensityInNonFluidCellsToOne< FlagField_T, PdfField_T >(blockForest, flagInfo, flagFieldID, pdfFieldID); + + // create timeloop + SweepTimeloop timeloop(blockForest, timesteps); + + // Laplace pressure = 2 * surface tension * curvature; curvature computation is not necessary with 0 surface tension + bool computeCurvature = false; + if (!realIsEqual(surfaceTension, real_c(0), real_c(1e-14))) { computeCurvature = true; } + + // add surface geometry handler + const SurfaceGeometryHandler< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > geometryHandler( + blockForest, freeSurfaceBoundaryHandling, fillFieldID, curvatureModel, computeCurvature, enableWetting, + contactAngle); + + const ConstBlockDataID curvatureFieldID = geometryHandler.getConstCurvatureFieldID(); + const ConstBlockDataID normalFieldID = geometryHandler.getConstNormalFieldID(); + + geometryHandler.addSweeps(timeloop); + + // add boundary handling for standard boundaries and free surface boundaries + const SurfaceDynamicsHandler< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > dynamicsHandler( + blockForest, pdfFieldID, flagFieldID, fillFieldID, forceFieldID, normalFieldID, curvatureFieldID, + freeSurfaceBoundaryHandling, bubbleModel, pdfReconstructionModel, pdfRefillingModel, excessMassDistributionModel, + relaxationRate, force, surfaceTension, enableForceWeighting, useSimpleMassExchange, cellConversionThreshold, + cellConversionForceThreshold); + + dynamicsHandler.addSweeps(timeloop); + + // add load balancing + const LoadBalancer< FlagField_T, CommunicationStencil_T, LatticeModelStencil_T > loadBalancer( + blockForest, communication, pdfCommunication, bubbleModel, uint_c(50), uint_c(10), uint_c(5), + loadBalancingFrequency, printLoadBalancingStatistics); + timeloop.addFuncAfterTimeStep(loadBalancer, "Sweep: load balancing"); + + // add VTK output + addVTKOutput< LatticeModel_T, FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T, VectorField_T >( + blockForest, timeloop, walberlaEnv.config(), flagInfo, pdfFieldID, flagFieldID, fillFieldID, forceFieldID, + geometryHandler.getCurvatureFieldID(), geometryHandler.getNormalFieldID(), + geometryHandler.getObstNormalFieldID()); + + // add triangle mesh output of free surface + SurfaceMeshWriter< ScalarField_T, FlagField_T > surfaceMeshWriter( + blockForest, fillFieldID, flagFieldID, flagIDs::liquidInterfaceGasFlagIDs, real_c(0), walberlaEnv.config()); + surfaceMeshWriter(); // write initial mesh + timeloop.addFuncAfterTimeStep(surfaceMeshWriter, "Writer: surface mesh"); + + // add logging for computational performance + const lbm::PerformanceLogger< FlagField_T > perfLogger(blockForest, flagFieldID, flagIDs::liquidInterfaceFlagIDs, + performanceLogFrequency); + timeloop.addFuncAfterTimeStep(perfLogger, "Evaluator: performance logging"); + + WcTimingPool timingPool; + + for (uint_t t = uint_c(0); t != timesteps; ++t) + { + if (t % uint_c(100) == uint_c(0)) { WALBERLA_LOG_DEVEL_ON_ROOT("Performing timestep=" << t); } + timeloop.singleStep(timingPool, true); + + if (t % performanceLogFrequency == uint_c(0) && t > uint_c(0)) { timingPool.logResultOnRoot(); } + } + + return EXIT_SUCCESS; +} + +} // namespace DropInPool +} // namespace free_surface +} // namespace walberla + +int main(int argc, char** argv) { return walberla::free_surface::DropInPool::main(argc, argv); } \ No newline at end of file diff --git a/apps/showcases/FreeSurface/MovingDrop.prm b/apps/showcases/FreeSurface/MovingDrop.prm new file mode 100644 index 000000000..184963db1 --- /dev/null +++ b/apps/showcases/FreeSurface/MovingDrop.prm @@ -0,0 +1,108 @@ +BlockForestParameters +{ + cellsPerBlock < 10, 10, 10 >; + periodicity < 1, 1, 1 >; + loadBalancingFrequency 0; + printLoadBalancingStatistics true; +} + +DomainParameters +{ + dropDiameter 50; + dropCenterFactor < 1, 1, 1 >; // values multiplied with dropDiameter + poolHeightFactor 0; // value multiplied with dropDiameter + domainSizeFactor < 2, 2, 4 >; // values multiplied with dropDiameter +} + +PhysicsParameters +{ + weberNumber 2010; + ohnesorgeNumber 0.0384; + relaxationRate 1.989; + enableWetting false; + contactAngle 0; // only used if enableWetting=true + timesteps 1000000; +} + +ModelParameters +{ + pdfReconstructionModel OnlyMissing; + pdfRefillingModel EquilibriumRefilling; + excessMassDistributionModel EvenlyAllInterface; + curvatureModel FiniteDifferenceMethod; + enableForceWeighting false; + useSimpleMassExchange false; + enableBubbleModel false; + enableBubbleSplits false; // only used if enableBubbleModel=true + cellConversionThreshold 1e-2; + cellConversionForceThreshold 1e-1; +} + +EvaluationParameters +{ + performanceLogFrequency 10000; +} + +BoundaryParameters +{ + // X + //Border { direction W; walldistance -1; NoSlip{} } + //Border { direction E; walldistance -1; NoSlip{} } + + // Y + //Border { direction N; walldistance -1; NoSlip{} } + //Border { direction S; walldistance -1; NoSlip{} } + + // Z + //Border { direction T; walldistance -1; NoSlip{} } + //Border { direction B; walldistance -1; NoSlip{} } +} + +MeshOutputParameters +{ + writeFrequency 10000; + baseFolder mesh-out; +} + +VTK +{ + fluid_field + { + writeFrequency 10000; + ghostLayers 0; + baseFolder vtk-out; + samplingResolution 1; + + writers + { + velocity; + density; + //pdf; + flag; + fill_level; + force; + curvature; + normal; + obstacle_normal; + mapped_flag; + } + + inclusion_filters + { + // only include liquid and interface cells in VTK output + //liquidInterfaceFilter; + } + + before_functions + { + //ghost_layer_synchronization; // only needed if writing the ghost layer + } + + } + domain_decomposition + { + writeFrequency 0; + baseFolder vtk-out; + outputDomainDecomposition true; + } +} \ No newline at end of file diff --git a/apps/showcases/FreeSurface/RisingBubble.cpp b/apps/showcases/FreeSurface/RisingBubble.cpp new file mode 100644 index 000000000..3dabdf5da --- /dev/null +++ b/apps/showcases/FreeSurface/RisingBubble.cpp @@ -0,0 +1,465 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file RisingBubble.cpp +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +// +// This showcase simulates a rising gas bubble in a stationary liquid. Reference experiments are available from +// Bhaga, Weber (1981), doi:10.1017/S002211208100311X +//====================================================================================================================== + +#include "blockforest/Initialization.h" + +#include "core/Environment.h" +#include "core/StringUtility.h" + +#include "lbm/PerformanceLogger.h" +#include "lbm/field/AddToStorage.h" +#include "lbm/free_surface/LoadBalancing.h" +#include "lbm/free_surface/SurfaceMeshWriter.h" +#include "lbm/free_surface/TotalMassComputer.h" +#include "lbm/free_surface/VtkWriter.h" +#include "lbm/free_surface/bubble_model/Geometry.h" +#include "lbm/free_surface/dynamics/SurfaceDynamicsHandler.h" +#include "lbm/free_surface/surface_geometry/SurfaceGeometryHandler.h" +#include "lbm/free_surface/surface_geometry/Utility.h" +#include "lbm/lattice_model/D3Q19.h" + +namespace walberla +{ +namespace free_surface +{ +namespace RisingBubble +{ +using ScalarField_T = GhostLayerField< real_t, 1 >; +using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >; + +using CollisionModel_T = lbm::collision_model::SRT; +using ForceModel_T = lbm::force_model::GuoField< VectorField_T >; +using LatticeModel_T = lbm::D3Q19< CollisionModel_T, true, ForceModel_T, 2 >; +using LatticeModelStencil_T = LatticeModel_T::Stencil; +using PdfField_T = lbm::PdfField< LatticeModel_T >; +using PdfCommunication_T = blockforest::SimpleCommunication< LatticeModelStencil_T >; + +// the geometry computations in SurfaceGeometryHandler require meaningful values in the ghost layers in corner +// directions (flag field and fill level field); this holds, even if the lattice model uses a D3Q19 stencil +using CommunicationStencil_T = + typename std::conditional< LatticeModel_T::Stencil::D == uint_t(2), stencil::D2Q9, stencil::D3Q27 >::type; +using Communication_T = blockforest::SimpleCommunication< CommunicationStencil_T >; + +using flag_t = uint32_t; +using FlagField_T = FlagField< flag_t >; +using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >; + +// write each entry in "vector" to line in a file; the first column contains the current timestep; all values are +// separated by tabs +template< typename T > +void writeVectorToFile(const std::vector< T >& vector, uint_t timestep, const std::string& filename); + +// IMPORTANT REMARK: this does not work when multiple bubbles are in the system (e.g. after splitting) +template< typename FreeSurfaceBoundaryHandling_T > +class CenterOfMassComputer +{ + public: + CenterOfMassComputer(const std::weak_ptr< const StructuredBlockForest >& blockForest, + const std::weak_ptr< const FreeSurfaceBoundaryHandling_T >& freeSurfaceBoundaryHandling, + uint_t frequency, const std::shared_ptr< Vector3< real_t > >& centerOfMass) + : blockForest_(blockForest), freeSurfaceBoundaryHandling_(freeSurfaceBoundaryHandling), + centerOfMass_(centerOfMass), frequency_(frequency), executionCounter_(uint_c(0)) + {} + + void operator()() + { + auto blockForest = blockForest_.lock(); + WALBERLA_CHECK_NOT_NULLPTR(blockForest); + + auto freeSurfaceBoundaryHandling = freeSurfaceBoundaryHandling_.lock(); + WALBERLA_CHECK_NOT_NULLPTR(freeSurfaceBoundaryHandling); + + ++executionCounter_; + + // only evaluate in given frequencies + if (executionCounter_ % frequency_ != uint_c(0)) { return; } + + const BlockDataID flagFieldID = freeSurfaceBoundaryHandling->getFlagFieldID(); + const typename FreeSurfaceBoundaryHandling_T::FlagInfo_T& flagInfo = freeSurfaceBoundaryHandling->getFlagInfo(); + + *centerOfMass_ = Vector3< real_t >(real_c(0)); + Cell cellSum = Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0)); + uint_t cellCount = uint_c(0); + + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + const FlagField_T* const flagField = blockIt->template getData< const FlagField_T >(flagFieldID); + + WALBERLA_FOR_ALL_CELLS(flagFieldIt, flagField, { + if (flagInfo.isGas(flagFieldIt)) + { + Cell globalCell = flagFieldIt.cell(); + // transform local cell to global coordinates + blockForest->transformBlockLocalToGlobalCell(globalCell, *blockIt); + cellSum += globalCell; + ++cellCount; + } + }) // WALBERLA_FOR_ALL_CELLS + } + + mpi::allReduceInplace< uint_t >(cellCount, mpi::SUM); + mpi::allReduceInplace< cell_idx_t >(cellSum[0], mpi::SUM); + mpi::allReduceInplace< cell_idx_t >(cellSum[1], mpi::SUM); + mpi::allReduceInplace< cell_idx_t >(cellSum[2], mpi::SUM); + + (*centerOfMass_)[0] = real_c(cellSum[0]) / real_c(cellCount); + (*centerOfMass_)[1] = real_c(cellSum[1]) / real_c(cellCount); + (*centerOfMass_)[2] = real_c(cellSum[2]) / real_c(cellCount); + } + + private: + std::weak_ptr< const StructuredBlockForest > blockForest_; + std::weak_ptr< const FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling_; + std::shared_ptr< Vector3< real_t > > centerOfMass_; + + uint_t frequency_; + uint_t executionCounter_; +}; // class CenterOfMassComputer + +int main(int argc, char** argv) +{ + Environment walberlaEnv(argc, argv); + + if (argc < 2) { WALBERLA_ABORT("Please specify a parameter file as input argument.") } + + // print content of parameter file + WALBERLA_LOG_INFO_ON_ROOT(*walberlaEnv.config()); + + // get block forest parameters from parameter file + auto blockForestParameters = walberlaEnv.config()->getOneBlock("BlockForestParameters"); + const Vector3< uint_t > cellsPerBlock = blockForestParameters.getParameter< Vector3< uint_t > >("cellsPerBlock"); + const Vector3< bool > periodicity = blockForestParameters.getParameter< Vector3< bool > >("periodicity"); + const uint_t loadBalancingFrequency = blockForestParameters.getParameter< uint_t >("loadBalancingFrequency"); + const bool printLoadBalancingStatistics = blockForestParameters.getParameter< bool >("printLoadBalancingStatistics"); + + // get domain parameters from parameter file + const auto domainParameters = walberlaEnv.config()->getOneBlock("DomainParameters"); + const uint_t bubbleDiameter = domainParameters.getParameter< uint_t >("bubbleDiameter"); + const Vector3< real_t > bubblePosition = domainParameters.getParameter< Vector3< real_t > >("bubblePosition"); + const Vector3< real_t > domainSizeFactor = domainParameters.getParameter< Vector3< real_t > >("domainSizeFactor"); + + // define domain size + Vector3< uint_t > domainSize; + domainSize[0] = uint_c(domainSizeFactor[0] * real_c(bubbleDiameter)); + domainSize[1] = uint_c(domainSizeFactor[1] * real_c(bubbleDiameter)); + domainSize[2] = uint_c(domainSizeFactor[2] * real_c(bubbleDiameter)); + + // compute number of blocks as defined by domainSize and cellsPerBlock + Vector3< uint_t > numBlocks; + numBlocks[0] = uint_c(std::ceil(real_c(domainSize[0]) / real_c(cellsPerBlock[0]))); + numBlocks[1] = uint_c(std::ceil(real_c(domainSize[1]) / real_c(cellsPerBlock[1]))); + numBlocks[2] = uint_c(std::ceil(real_c(domainSize[2]) / real_c(cellsPerBlock[2]))); + + // get number of (MPI) processes + uint_t numProcesses = uint_c(MPIManager::instance()->numProcesses()); + WALBERLA_CHECK_LESS_EQUAL(numProcesses, numBlocks[0] * numBlocks[1] * numBlocks[2], + "The number of MPI processes is greater than the number of blocks as defined by " + "\"domainSize/cellsPerBlock\". This would result in unused MPI processes. Either decrease " + "the number of MPI processes or increase \"cellsPerBlock\".") + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(numProcesses); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellsPerBlock); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(numBlocks); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(bubbleDiameter); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(bubblePosition); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(domainSizeFactor); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(periodicity); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(domainSize); + + Vector3< uint_t > realDomainSize; + realDomainSize[0] = cellsPerBlock[0] * numBlocks[0]; + realDomainSize[1] = cellsPerBlock[1] * numBlocks[1]; + realDomainSize[2] = cellsPerBlock[2] * numBlocks[2]; + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(realDomainSize); + + if (domainSize[0] != realDomainSize[0] && periodicity[0]) + { + WALBERLA_ABORT( + "The specified domain size in x-direction can not be obtained with the numer of blocks you specified.") + } + if (domainSize[1] != realDomainSize[1] && periodicity[1]) + { + WALBERLA_ABORT( + "The specified domain size in y-direction can not be obtained with the numer of blocks you specified.") + } + // the z-direction must be no slip in this setup and is simply extended (see below) to obtain the specified size + int boundaryThicknessZ = int_c(realDomainSize[2]) - int_c(domainSize[2]); + if (boundaryThicknessZ < 0) + { + WALBERLA_ABORT("Something went wrong: the resulting domain size in z-direction is less than specified.") + } + + // read physics parameters from parameter file + const auto physicsParameters = walberlaEnv.config()->getOneBlock("PhysicsParameters"); + const real_t bondNumber = physicsParameters.getParameter< real_t >("bondNumber"); + const real_t mortonNumber = physicsParameters.getParameter< real_t >("mortonNumber"); + const real_t relaxationRate = physicsParameters.getParameter< real_t >("relaxationRate"); + const bool enableWetting = physicsParameters.getParameter< bool >("enableWetting"); + const real_t contactAngle = physicsParameters.getParameter< real_t >("contactAngle"); + const uint_t timesteps = physicsParameters.getParameter< uint_t >("timesteps"); + + const CollisionModel_T collisionModel = CollisionModel_T(relaxationRate); + const real_t viscosity = collisionModel.viscosity(); + + const real_t surfaceTension = real_c( + std::pow(bondNumber * std::pow(viscosity, 4) / mortonNumber / real_c(bubbleDiameter * bubbleDiameter), 0.5)); + + const real_t gravitationalAccelerationZ = + mortonNumber * real_c(std::pow(surfaceTension, 3)) / real_c(std::pow(viscosity, 4)); + + const Vector3< real_t > force(real_c(0), real_c(0), -gravitationalAccelerationZ); + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(relaxationRate); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(mortonNumber); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(bondNumber); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableWetting); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(contactAngle); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(timesteps); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(viscosity); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(force); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(surfaceTension); + + // read model parameters from parameter file + const auto modelParameters = walberlaEnv.config()->getOneBlock("ModelParameters"); + const std::string pdfReconstructionModel = modelParameters.getParameter< std::string >("pdfReconstructionModel"); + const std::string pdfRefillingModel = modelParameters.getParameter< std::string >("pdfRefillingModel"); + const std::string excessMassDistributionModel = + modelParameters.getParameter< std::string >("excessMassDistributionModel"); + const std::string curvatureModel = modelParameters.getParameter< std::string >("curvatureModel"); + const bool enableForceWeighting = modelParameters.getParameter< bool >("enableForceWeighting"); + const bool useSimpleMassExchange = modelParameters.getParameter< bool >("useSimpleMassExchange"); + const real_t cellConversionThreshold = modelParameters.getParameter< real_t >("cellConversionThreshold"); + const real_t cellConversionForceThreshold = modelParameters.getParameter< real_t >("cellConversionForceThreshold"); + const bool enableBubbleModel = modelParameters.getParameter< bool >("enableBubbleModel"); + const bool enableBubbleSplits = modelParameters.getParameter< bool >("enableBubbleSplits"); + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(pdfReconstructionModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(pdfRefillingModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(excessMassDistributionModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(curvatureModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableForceWeighting); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(useSimpleMassExchange); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellConversionThreshold); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellConversionForceThreshold); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableBubbleModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableBubbleSplits); + + // read evaluation parameters from parameter file + const auto evaluationParameters = walberlaEnv.config()->getOneBlock("EvaluationParameters"); + const uint_t performanceLogFrequency = evaluationParameters.getParameter< uint_t >("performanceLogFrequency"); + const uint_t evaluationFrequency = evaluationParameters.getParameter< uint_t >("evaluationFrequency"); + const std::string filename = evaluationParameters.getParameter< std::string >("filename"); + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(performanceLogFrequency); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(evaluationFrequency); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(filename); + + // create non-uniform block forest (non-uniformity required for load balancing) + const std::shared_ptr< StructuredBlockForest > blockForest = + createNonUniformBlockForest(domainSize, cellsPerBlock, numBlocks, periodicity); + + // add force field + const BlockDataID forceFieldID = + field::addToStorage< VectorField_T >(blockForest, "Force field", force, field::fzyx, uint_c(1)); + + // create lattice model + const LatticeModel_T latticeModel = LatticeModel_T(collisionModel, ForceModel_T(forceFieldID)); + + // add various fields + const BlockDataID pdfFieldID = lbm::addPdfFieldToStorage(blockForest, "PDF field", latticeModel, field::fzyx); + const BlockDataID fillFieldID = + field::addToStorage< ScalarField_T >(blockForest, "Fill level field", real_c(1.0), field::fzyx, uint_c(2)); + + const std::shared_ptr< FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling = + std::make_shared< FreeSurfaceBoundaryHandling_T >(blockForest, pdfFieldID, fillFieldID); + const BlockDataID flagFieldID = freeSurfaceBoundaryHandling->getFlagFieldID(); + const typename FreeSurfaceBoundaryHandling_T::FlagInfo_T& flagInfo = freeSurfaceBoundaryHandling->getFlagInfo(); + + const geometry::Sphere sphereBubble(Vector3< real_t >(bubblePosition[0] * real_c(bubbleDiameter), + bubblePosition[1] * real_c(bubbleDiameter), + bubblePosition[2] * real_c(bubbleDiameter)), + real_c(bubbleDiameter) * real_c(0.5)); + bubble_model::addBodyToFillLevelField< geometry::Sphere >(*blockForest, fillFieldID, sphereBubble, true); + + // add boundary conditions from config file + const auto boundaryParameters = walberlaEnv.config()->getOneBlock("BoundaryParameters"); + freeSurfaceBoundaryHandling->initFromConfig(boundaryParameters); + for (int i = -1; i != boundaryThicknessZ; ++i) + { + freeSurfaceBoundaryHandling->setNoSlipAtBorder(stencil::T, cell_idx_c(i)); + } + + // IMPORTANT REMARK: this must be only called after every solid flag has been set; otherwise, the boundary handling + // might not detect solid flags correctly + freeSurfaceBoundaryHandling->initFlagsFromFillLevel(); + + // communication after initialization + Communication_T communication(blockForest, flagFieldID, fillFieldID, forceFieldID); + communication(); + + PdfCommunication_T pdfCommunication(blockForest, pdfFieldID); + pdfCommunication(); + + // add bubble model + std::shared_ptr< bubble_model::BubbleModelBase > bubbleModel = nullptr; + if (enableBubbleModel) + { + const std::shared_ptr< bubble_model::BubbleModel< CommunicationStencil_T > > bubbleModelDerived = + std::make_shared< bubble_model::BubbleModel< CommunicationStencil_T > >(blockForest, enableBubbleSplits); + bubbleModelDerived->initFromFillLevelField(fillFieldID); + + bubbleModel = std::static_pointer_cast< bubble_model::BubbleModelBase >(bubbleModelDerived); + } + else { bubbleModel = std::make_shared< bubble_model::BubbleModelConstantPressure >(real_c(1)); } + + // initialize hydrostatic pressure + initHydrostaticPressure< PdfField_T >(blockForest, pdfFieldID, force, real_c(domainSize[2]) * real_c(0.5)); + + // set density in non-liquid or non-interface cells to 1 (to be done after initializing with hydrostatic pressure) + setDensityInNonFluidCellsToOne< FlagField_T, PdfField_T >(blockForest, flagInfo, flagFieldID, pdfFieldID); + + // create timeloop + SweepTimeloop timeloop(blockForest, timesteps); + + // Laplace pressure = 2 * surface tension * curvature; curvature computation is not necessary with 0 surface tension + bool computeCurvature = false; + if (!realIsEqual(surfaceTension, real_c(0), real_c(1e-14))) { computeCurvature = true; } + + // add surface geometry handler + const SurfaceGeometryHandler< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > geometryHandler( + blockForest, freeSurfaceBoundaryHandling, fillFieldID, curvatureModel, computeCurvature, enableWetting, + contactAngle); + + geometryHandler.addSweeps(timeloop); + + const ConstBlockDataID curvatureFieldID = geometryHandler.getConstCurvatureFieldID(); + const ConstBlockDataID normalFieldID = geometryHandler.getConstNormalFieldID(); + + // add boundary handling for standard boundaries and free surface boundaries + const SurfaceDynamicsHandler< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > dynamicsHandler( + blockForest, pdfFieldID, flagFieldID, fillFieldID, forceFieldID, normalFieldID, curvatureFieldID, + freeSurfaceBoundaryHandling, bubbleModel, pdfReconstructionModel, pdfRefillingModel, excessMassDistributionModel, + relaxationRate, force, surfaceTension, enableForceWeighting, useSimpleMassExchange, cellConversionThreshold, + cellConversionForceThreshold); + + dynamicsHandler.addSweeps(timeloop); + + // add load balancing + const LoadBalancer< FlagField_T, CommunicationStencil_T, LatticeModelStencil_T > loadBalancer( + blockForest, communication, pdfCommunication, bubbleModel, uint_c(50), uint_c(10), uint_c(5), + loadBalancingFrequency, printLoadBalancingStatistics); + timeloop.addFuncAfterTimeStep(loadBalancer, "Sweep: load balancing"); + + // get bubble's center of mass position + const std::shared_ptr< Vector3< real_t > > centerOfMass = + std::make_shared< Vector3< real_t > >(Vector3< real_t >(real_c(0))); + const CenterOfMassComputer< FreeSurfaceBoundaryHandling_T > centerOfMassComputer( + blockForest, freeSurfaceBoundaryHandling, evaluationFrequency, centerOfMass); + timeloop.addFuncAfterTimeStep(centerOfMassComputer, "Evaluator: center of mass"); + + // add computation of total mass + const std::shared_ptr< real_t > totalMass = std::make_shared< real_t >(real_c(0)); + const TotalMassComputer< FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T > totalMassComputer( + blockForest, freeSurfaceBoundaryHandling, pdfFieldID, fillFieldID, evaluationFrequency, totalMass); + timeloop.addFuncAfterTimeStep(totalMassComputer, "Evaluator: total mass"); + + // add VTK output + addVTKOutput< LatticeModel_T, FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T, VectorField_T >( + blockForest, timeloop, walberlaEnv.config(), flagInfo, pdfFieldID, flagFieldID, fillFieldID, forceFieldID, + geometryHandler.getCurvatureFieldID(), geometryHandler.getNormalFieldID(), + geometryHandler.getObstNormalFieldID()); + + // add triangle mesh output of free surface + SurfaceMeshWriter< ScalarField_T, FlagField_T > surfaceMeshWriter( + blockForest, fillFieldID, flagFieldID, flagIDs::liquidInterfaceGasFlagIDs, real_c(1), walberlaEnv.config()); + surfaceMeshWriter(); // write initial mesh + timeloop.addFuncAfterTimeStep(surfaceMeshWriter, "Writer: surface mesh"); + + // add performance logging + const lbm::PerformanceLogger< FlagField_T > perfLogger(blockForest, flagFieldID, flagIDs::liquidInterfaceFlagIDs, + performanceLogFrequency); + timeloop.addFuncAfterTimeStep(perfLogger, "Evaluator: performance logging"); + + WcTimingPool timingPool; + + const real_t stoppingHeight = real_c(domainSize[2] - bubbleDiameter); + + uint_t timestepOld = uint_c(0); + Vector3< real_t > centerOfMassOld = Vector3< real_t >(real_c(0)); + + for (uint_t t = uint_c(0); t != timesteps; ++t) + { + timeloop.singleStep(timingPool, true); + + // only evaluate if center of mass has moved "significantly" + if ((*centerOfMass)[2] > (centerOfMassOld[2] + real_c(1)) && t > uint_c(0)) + { + WALBERLA_ROOT_SECTION() + { + real_t riseVelocity = ((*centerOfMass)[2] - centerOfMassOld[2]) / real_c(t - timestepOld); + const real_t dragForce = real_c(4) / real_c(3) * gravitationalAccelerationZ * real_c(bubbleDiameter) / + (riseVelocity * riseVelocity); + + WALBERLA_LOG_DEVEL("time step = " << t); + WALBERLA_LOG_DEVEL("\t\tcenterOfMass = " << *centerOfMass << "\n\t\triseVelocity = " << riseVelocity + << "\n\t\tdragForce = " << dragForce); + WALBERLA_LOG_DEVEL("\t\ttotalMass = " << *totalMass); + + const std::vector< real_t > resultVector{ (*centerOfMass)[2], riseVelocity, dragForce }; + + writeVectorToFile(resultVector, t, filename); + } + + timestepOld = t; + centerOfMassOld = *centerOfMass; + } + + // stop simulation before bubble hits the top wall + if ((*centerOfMass)[2] > stoppingHeight) { break; } + + if (t % performanceLogFrequency == uint_c(0) && t > uint_c(0)) { timingPool.logResultOnRoot(); } + } + return EXIT_SUCCESS; +} + +template< typename T > +void writeVectorToFile(const std::vector< T >& vector, uint_t timestep, const std::string& filename) +{ + std::fstream file; + file.open(filename, std::fstream::app); + + file << timestep; + for (const auto i : vector) + { + file << "\t" << i; + } + + file << "\n"; + file.close(); +} + +} // namespace RisingBubble +} // namespace free_surface +} // namespace walberla + +int main(int argc, char** argv) { return walberla::free_surface::RisingBubble::main(argc, argv); } \ No newline at end of file diff --git a/apps/showcases/FreeSurface/RisingBubble.prm b/apps/showcases/FreeSurface/RisingBubble.prm new file mode 100644 index 000000000..5d483426b --- /dev/null +++ b/apps/showcases/FreeSurface/RisingBubble.prm @@ -0,0 +1,108 @@ +BlockForestParameters +{ + cellsPerBlock < 16, 16, 16 >; + periodicity < 1, 1, 0 >; + loadBalancingFrequency 445; + printLoadBalancingStatistics true; +} + +DomainParameters +{ + bubbleDiameter 16; + bubblePosition < 4, 4, 1 >; // initial bubble position (values multiplied with bubbleDiameter) + domainSizeFactor < 8, 8, 20 >; // values multiplied with bubbleDiameter +} + +PhysicsParameters +{ + bondNumber 115; + mortonNumber 4.63e-3; + relaxationRate 1.95; + enableWetting false; + contactAngle 0; // only used if enableWetting=true + timesteps 8900; +} + +ModelParameters +{ + pdfReconstructionModel OnlyMissing; + pdfRefillingModel EquilibriumRefilling; + excessMassDistributionModel EvenlyAllInterface; + curvatureModel FiniteDifferenceMethod; + enableForceWeighting false; + useSimpleMassExchange false; + cellConversionThreshold 1e-2; + cellConversionForceThreshold 1e-1; + + enableBubbleModel true; + enableBubbleSplits false; // only used if enableBubbleModel=true +} + +EvaluationParameters +{ + performanceLogFrequency 4450; + evaluationFrequency 44; + filename rising-bubble.txt; +} + +BoundaryParameters +{ + // X + //Border { direction W; walldistance -1; NoSlip{} } + //Border { direction E; walldistance -1; NoSlip{} } + + // Y + //Border { direction N; walldistance -1; NoSlip{} } + //Border { direction S; walldistance -1; NoSlip{} } + + // Z + Border { direction T; walldistance -1; NoSlip{} } + Border { direction B; walldistance -1; NoSlip{} } +} + +MeshOutputParameters +{ + writeFrequency 445; + baseFolder mesh-out; +} + +VTK +{ + fluid_field + { + writeFrequency 445; + ghostLayers 0; + baseFolder vtk-out; + samplingResolution 1; + + writers + { + fill_level; + mapped_flag; + velocity; + density; + //curvature; + //normal; + //obstacle_normal; + //pdf; + //flag; + //force; + } + + inclusion_filters + { + //liquidInterfaceFilter; // only include liquid and interface cells in VTK output + } + + before_functions + { + //ghost_layer_synchronization; // only needed if writing the ghost layer + } + } + domain_decomposition + { + writeFrequency 445; + baseFolder vtk-out; + outputDomainDecomposition true; + } +} \ No newline at end of file diff --git a/apps/showcases/FreeSurface/TaylorBubble.cpp b/apps/showcases/FreeSurface/TaylorBubble.cpp new file mode 100644 index 000000000..f32cfb982 --- /dev/null +++ b/apps/showcases/FreeSurface/TaylorBubble.cpp @@ -0,0 +1,494 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file TaylorBubble.cpp +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief This showcase simulates a rising gas bubble in a cylindrical tube. +// +// This showcase simulates an elongated gas bubble in a cylindrical tube. Reference experiments are available from +// Bugg, Saad (2002), doi:10.1016/S0301-9322(02)00002-2 +//====================================================================================================================== + +#include "blockforest/Initialization.h" + +#include "core/Environment.h" +#include "core/StringUtility.h" + +#include "lbm/PerformanceLogger.h" +#include "lbm/field/AddToStorage.h" +#include "lbm/free_surface/LoadBalancing.h" +#include "lbm/free_surface/SurfaceMeshWriter.h" +#include "lbm/free_surface/TotalMassComputer.h" +#include "lbm/free_surface/VtkWriter.h" +#include "lbm/free_surface/bubble_model/Geometry.h" +#include "lbm/free_surface/dynamics/SurfaceDynamicsHandler.h" +#include "lbm/free_surface/surface_geometry/SurfaceGeometryHandler.h" +#include "lbm/free_surface/surface_geometry/Utility.h" +#include "lbm/lattice_model/D3Q19.h" + +namespace walberla +{ +namespace free_surface +{ +namespace TaylorBubble +{ +using ScalarField_T = GhostLayerField< real_t, 1 >; +using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >; + +using CollisionModel_T = lbm::collision_model::SRT; +using ForceModel_T = lbm::force_model::GuoField< VectorField_T >; +using LatticeModel_T = lbm::D3Q19< CollisionModel_T, true, ForceModel_T, 2 >; +using LatticeModelStencil_T = LatticeModel_T::Stencil; +using PdfField_T = lbm::PdfField< LatticeModel_T >; +using PdfCommunication_T = blockforest::SimpleCommunication< LatticeModelStencil_T >; + +// the geometry computations in SurfaceGeometryHandler require meaningful values in the ghost layers in corner +// directions (flag field and fill level field); this holds, even if the lattice model uses a D3Q19 stencil +using CommunicationStencil_T = + typename std::conditional< LatticeModel_T::Stencil::D == uint_t(2), stencil::D2Q9, stencil::D3Q27 >::type; +using Communication_T = blockforest::SimpleCommunication< CommunicationStencil_T >; + +using flag_t = uint32_t; +using FlagField_T = FlagField< flag_t >; +using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >; + +// write each entry in "vector" to line in a file; the first column contains the current timestep; all values are +// separated by tabs +template< typename T > +void writeVectorToFile(const std::vector< T >& vector, uint_t timestep, const std::string& filename); + +// IMPORTANT REMARK: this does not work when multiple bubbles are in the system (e.g. after splitting) +template< typename FreeSurfaceBoundaryHandling_T > +class CenterOfMassComputer +{ + public: + CenterOfMassComputer(const std::weak_ptr< const StructuredBlockForest >& blockForest, + const std::weak_ptr< const FreeSurfaceBoundaryHandling_T >& freeSurfaceBoundaryHandling, + uint_t interval, const std::shared_ptr< Vector3< real_t > >& centerOfMass) + : blockForest_(blockForest), freeSurfaceBoundaryHandling_(freeSurfaceBoundaryHandling), + centerOfMass_(centerOfMass), interval_(interval), executionCounter_(uint_c(0)) + {} + + void operator()() + { + auto blockForest = blockForest_.lock(); + WALBERLA_CHECK_NOT_NULLPTR(blockForest); + + auto freeSurfaceBoundaryHandling = freeSurfaceBoundaryHandling_.lock(); + WALBERLA_CHECK_NOT_NULLPTR(freeSurfaceBoundaryHandling); + + ++executionCounter_; + + // only evaluate in given intervals + if (executionCounter_ % interval_ != uint_c(0)) { return; } + + const BlockDataID flagFieldID = freeSurfaceBoundaryHandling->getFlagFieldID(); + const typename FreeSurfaceBoundaryHandling_T::FlagInfo_T& flagInfo = freeSurfaceBoundaryHandling->getFlagInfo(); + + *centerOfMass_ = Vector3< real_t >(real_c(0)); + Cell cellSum = Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0)); + uint_t cellCount = uint_c(0); + + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + const FlagField_T* const flagField = blockIt->template getData< const FlagField_T >(flagFieldID); + + WALBERLA_FOR_ALL_CELLS(flagFieldIt, flagField, { + if (flagInfo.isGas(flagFieldIt)) + { + Cell globalCell = flagFieldIt.cell(); + // transform local cell to global coordinates + blockForest->transformBlockLocalToGlobalCell(globalCell, *blockIt); + cellSum += globalCell; + ++cellCount; + } + }) // WALBERLA_FOR_ALL_CELLS + } + + mpi::allReduceInplace< uint_t >(cellCount, mpi::SUM); + mpi::allReduceInplace< cell_idx_t >(cellSum[0], mpi::SUM); + mpi::allReduceInplace< cell_idx_t >(cellSum[1], mpi::SUM); + mpi::allReduceInplace< cell_idx_t >(cellSum[2], mpi::SUM); + + (*centerOfMass_)[0] = real_c(cellSum[0]) / real_c(cellCount); + (*centerOfMass_)[1] = real_c(cellSum[1]) / real_c(cellCount); + (*centerOfMass_)[2] = real_c(cellSum[2]) / real_c(cellCount); + } + + private: + std::weak_ptr< const StructuredBlockForest > blockForest_; + std::weak_ptr< const FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling_; + std::shared_ptr< Vector3< real_t > > centerOfMass_; + + uint_t interval_; + uint_t executionCounter_; +}; // class CenterOfMassComputer + +int main(int argc, char** argv) +{ + Environment walberlaEnv(argc, argv); + + if (argc < 2) { WALBERLA_ABORT("Please specify a parameter file as input argument.") } + + // print content of parameter file + WALBERLA_LOG_INFO_ON_ROOT(*walberlaEnv.config()); + + // get block forest parameters from parameter file + auto blockForestParameters = walberlaEnv.config()->getOneBlock("BlockForestParameters"); + const Vector3< uint_t > cellsPerBlock = blockForestParameters.getParameter< Vector3< uint_t > >("cellsPerBlock"); + const Vector3< bool > periodicity = blockForestParameters.getParameter< Vector3< bool > >("periodicity"); + const uint_t loadBalancingFrequency = blockForestParameters.getParameter< uint_t >("loadBalancingFrequency"); + const bool printLoadBalancingStatistics = blockForestParameters.getParameter< bool >("printLoadBalancingStatistics"); + + // read domain parameters from parameter file + const auto domainParameters = walberlaEnv.config()->getOneBlock("DomainParameters"); + const real_t tubeDiameter = domainParameters.getParameter< real_t >("tubeDiameter"); + const real_t bubbleDiameter = domainParameters.getParameter< real_t >("bubbleDiameter"); + const real_t bubbleHeight = domainParameters.getParameter< real_t >("bubbleHeight"); + const Vector3< real_t > bubbleBottomEnd = domainParameters.getParameter< Vector3< real_t > >("bubbleBottomEnd"); + const Vector3< real_t > domainSizeFactor = domainParameters.getParameter< Vector3< real_t > >("domainSizeFactor"); + + // define domain size + Vector3< uint_t > domainSize; + domainSize[0] = uint_c(domainSizeFactor[0] * tubeDiameter); + domainSize[1] = uint_c(domainSizeFactor[1] * tubeDiameter); + domainSize[2] = uint_c(domainSizeFactor[2] * tubeDiameter); + + // compute number of blocks as defined by domainSize and cellsPerBlock + Vector3< uint_t > numBlocks; + numBlocks[0] = uint_c(std::ceil(real_c(domainSize[0]) / real_c(cellsPerBlock[0]))); + numBlocks[1] = uint_c(std::ceil(real_c(domainSize[1]) / real_c(cellsPerBlock[1]))); + numBlocks[2] = uint_c(std::ceil(real_c(domainSize[2]) / real_c(cellsPerBlock[2]))); + + // get number of (MPI) processes + uint_t numProcesses = uint_c(MPIManager::instance()->numProcesses()); + WALBERLA_CHECK_LESS_EQUAL(numProcesses, numBlocks[0] * numBlocks[1] * numBlocks[2], + "The number of MPI processes is greater than the number of blocks as defined by " + "\"domainSize/cellsPerBlock\". This would result in unused MPI processes. Either decrease " + "the number of MPI processes or increase \"cellsPerBlock\".") + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(numProcesses); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellsPerBlock); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(numBlocks); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(tubeDiameter); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(bubbleDiameter); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(bubbleHeight); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(bubbleBottomEnd); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(domainSizeFactor); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(periodicity); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(domainSize); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellsPerBlock); + + Vector3< uint_t > realDomainSize; + realDomainSize[0] = cellsPerBlock[0] * numBlocks[0]; + realDomainSize[1] = cellsPerBlock[1] * numBlocks[1]; + realDomainSize[2] = cellsPerBlock[2] * numBlocks[2]; + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(realDomainSize); + + if (domainSize[0] != realDomainSize[0] && periodicity[0]) + { + WALBERLA_ABORT( + "The specified domain size in x-direction can not be obtained with the numer of blocks you specified.") + } + if (domainSize[1] != realDomainSize[1] && periodicity[1]) + { + WALBERLA_ABORT( + "The specified domain size in y-direction can not be obtained with the numer of blocks you specified.") + } + // the z-direction must be no slip in this setup and is simply extended (see below) to obtain the specified size + int boundaryThicknessZ = int_c(realDomainSize[2]) - int_c(domainSize[2]); + if (boundaryThicknessZ < 0) + { + WALBERLA_ABORT("Something went wrong: the resulting domain size in z-direction is less than specified.") + } + + // read physics parameters from parameter file + const auto physicsParameters = walberlaEnv.config()->getOneBlock("PhysicsParameters"); + const real_t bondNumber = physicsParameters.getParameter< real_t >("bondNumber"); + const real_t mortonNumber = physicsParameters.getParameter< real_t >("mortonNumber"); + const real_t relaxationRate = physicsParameters.getParameter< real_t >("relaxationRate"); + const bool enableWetting = physicsParameters.getParameter< bool >("enableWetting"); + const real_t contactAngle = physicsParameters.getParameter< real_t >("contactAngle"); + const uint_t timesteps = physicsParameters.getParameter< uint_t >("timesteps"); + + const CollisionModel_T collisionModel = CollisionModel_T(relaxationRate); + const real_t viscosity = collisionModel.viscosity(); + + const real_t surfaceTension = + real_c(std::pow(bondNumber * std::pow(viscosity, 4) / mortonNumber / real_c(tubeDiameter * tubeDiameter), 0.5)); + + const real_t gravitationalAccelerationZ = + mortonNumber * real_c(std::pow(surfaceTension, 3)) / real_c(std::pow(viscosity, 4)); + + const Vector3< real_t > force = Vector3< real_t >(real_c(0), real_c(0), -gravitationalAccelerationZ); + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(relaxationRate); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(mortonNumber); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(bondNumber); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableWetting); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(contactAngle); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(timesteps); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(viscosity); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(force); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(surfaceTension); + + // read model parameters from parameter file + const auto modelParameters = walberlaEnv.config()->getOneBlock("ModelParameters"); + const std::string pdfReconstructionModel = modelParameters.getParameter< std::string >("pdfReconstructionModel"); + const std::string pdfRefillingModel = modelParameters.getParameter< std::string >("pdfRefillingModel"); + const std::string excessMassDistributionModel = + modelParameters.getParameter< std::string >("excessMassDistributionModel"); + const std::string curvatureModel = modelParameters.getParameter< std::string >("curvatureModel"); + const bool enableForceWeighting = modelParameters.getParameter< bool >("enableForceWeighting"); + const bool useSimpleMassExchange = modelParameters.getParameter< bool >("useSimpleMassExchange"); + const real_t cellConversionThreshold = modelParameters.getParameter< real_t >("cellConversionThreshold"); + const real_t cellConversionForceThreshold = modelParameters.getParameter< real_t >("cellConversionForceThreshold"); + const bool enableBubbleModel = modelParameters.getParameter< bool >("enableBubbleModel"); + const bool enableBubbleSplits = modelParameters.getParameter< bool >("enableBubbleSplits"); + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(pdfReconstructionModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(pdfRefillingModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(excessMassDistributionModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(curvatureModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableForceWeighting); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(useSimpleMassExchange); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellConversionThreshold); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(cellConversionForceThreshold); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableBubbleModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(enableBubbleSplits); + + // read evaluation parameters from parameter file + const auto evaluationParameters = walberlaEnv.config()->getOneBlock("EvaluationParameters"); + const uint_t performanceLogFrequency = evaluationParameters.getParameter< uint_t >("performanceLogFrequency"); + const uint_t evaluationFrequency = evaluationParameters.getParameter< uint_t >("evaluationFrequency"); + const std::string filename = evaluationParameters.getParameter< std::string >("filename"); + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(performanceLogFrequency); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(evaluationFrequency); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(filename); + + // create non-uniform block forest (non-uniformity required for load balancing) + const std::shared_ptr< StructuredBlockForest > blockForest = + createNonUniformBlockForest(domainSize, cellsPerBlock, numBlocks, periodicity); + + // add force field + const BlockDataID forceFieldID = + field::addToStorage< VectorField_T >(blockForest, "Force field", force, field::fzyx, uint_c(1)); + + // create lattice model + const LatticeModel_T latticeModel = LatticeModel_T(collisionModel, ForceModel_T(forceFieldID)); + + // add various fields + const BlockDataID pdfFieldID = lbm::addPdfFieldToStorage(blockForest, "PDF field", latticeModel, field::fzyx); + const BlockDataID fillFieldID = + field::addToStorage< ScalarField_T >(blockForest, "Fill level field", real_c(1.0), field::fzyx, uint_c(2)); + + const std::shared_ptr< FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling = + std::make_shared< FreeSurfaceBoundaryHandling_T >(blockForest, pdfFieldID, fillFieldID); + const BlockDataID flagFieldID = freeSurfaceBoundaryHandling->getFlagFieldID(); + const typename FreeSurfaceBoundaryHandling_T::FlagInfo_T& flagInfo = freeSurfaceBoundaryHandling->getFlagInfo(); + + const Vector3< real_t > bubbleTopEnd = + Vector3< real_t >(bubbleBottomEnd[0], bubbleBottomEnd[1], bubbleBottomEnd[2] + bubbleHeight); + const geometry::Cylinder cylinderBubble(bubbleBottomEnd * tubeDiameter, bubbleTopEnd * tubeDiameter, + bubbleDiameter * tubeDiameter * real_c(0.5)); + + bubble_model::addBodyToFillLevelField< geometry::Cylinder >(*blockForest, fillFieldID, cylinderBubble, true); + + // add boundary conditions from config file + const auto boundaryParameters = walberlaEnv.config()->getOneBlock("BoundaryParameters"); + freeSurfaceBoundaryHandling->initFromConfig(boundaryParameters); + for (int i = -1; i != boundaryThicknessZ; ++i) + { + freeSurfaceBoundaryHandling->setNoSlipAtBorder(stencil::T, cell_idx_c(i)); + } + + // initialize cylindrical domain walls + const Vector3< real_t > domainCylinderBottomEnd = + Vector3< real_t >(real_c(domainSize[0]) * real_c(0.5), real_c(domainSize[1]) * real_c(0.5), real_c(0)); + const Vector3< real_t > domainCylinderTopEnd = Vector3< real_t >( + real_c(domainSize[0]) * real_c(0.5), real_c(domainSize[1]) * real_c(0.5), real_c(domainSize[2])); + const geometry::Cylinder cylinderTube(domainCylinderBottomEnd, domainCylinderTopEnd, tubeDiameter * real_c(0.5)); + + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + const FlagField_T* const flagField = blockIt->template getData< const FlagField_T >(flagFieldID); + + WALBERLA_FOR_ALL_CELLS(flagFieldIt, flagField, { + Cell globalCell = flagFieldIt.cell(); + blockForest->transformBlockLocalToGlobalCell(globalCell, *blockIt); + const Vector3< real_t > globalPoint = blockForest->getCellCenter(globalCell); + + if (!geometry::contains(cylinderTube, globalPoint)) + { + freeSurfaceBoundaryHandling->setNoSlipInCell(globalCell); + } + }) // WALBERLA_FOR_ALL_CELLS + } + + // IMPORTANT REMARK: this must be only called after every solid flag has been set; otherwise, the boundary handling + // might not detect solid flags correctly + freeSurfaceBoundaryHandling->initFlagsFromFillLevel(); + + // communication after initialization + Communication_T communication(blockForest, flagFieldID, fillFieldID, forceFieldID); + communication(); + + PdfCommunication_T pdfCommunication(blockForest, pdfFieldID); + pdfCommunication(); + + // add bubble model + std::shared_ptr< bubble_model::BubbleModelBase > bubbleModel = nullptr; + if (enableBubbleModel) + { + const std::shared_ptr< bubble_model::BubbleModel< CommunicationStencil_T > > bubbleModelDerived = + std::make_shared< bubble_model::BubbleModel< CommunicationStencil_T > >(blockForest, enableBubbleSplits); + bubbleModelDerived->initFromFillLevelField(fillFieldID); + + bubbleModel = std::static_pointer_cast< bubble_model::BubbleModelBase >(bubbleModelDerived); + } + else { bubbleModel = std::make_shared< bubble_model::BubbleModelConstantPressure >(real_c(1)); } + + // initialize hydrostatic pressure + initHydrostaticPressure< PdfField_T >(blockForest, pdfFieldID, force, real_c(domainSize[2]) * real_c(0.5)); + + // set density in non-liquid or non-interface cells to 1 (after initializing with hydrostatic pressure) + setDensityInNonFluidCellsToOne< FlagField_T, PdfField_T >(blockForest, flagInfo, flagFieldID, pdfFieldID); + + // create timeloop + SweepTimeloop timeloop(blockForest, timesteps); + + // Laplace pressure = 2 * surface tension * curvature; curvature computation is not necessary with 0 surface tension + bool computeCurvature = false; + if (!realIsEqual(surfaceTension, real_c(0), real_c(1e-14))) { computeCurvature = true; } + + // add surface geometry handler + const SurfaceGeometryHandler< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > geometryHandler( + blockForest, freeSurfaceBoundaryHandling, fillFieldID, curvatureModel, computeCurvature, enableWetting, + contactAngle); + + geometryHandler.addSweeps(timeloop); + + const ConstBlockDataID curvatureFieldID = geometryHandler.getConstCurvatureFieldID(); + const ConstBlockDataID normalFieldID = geometryHandler.getConstNormalFieldID(); + + // add boundary handling for standard boundaries and free surface boundaries + const SurfaceDynamicsHandler< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > dynamicsHandler( + blockForest, pdfFieldID, flagFieldID, fillFieldID, forceFieldID, normalFieldID, curvatureFieldID, + freeSurfaceBoundaryHandling, bubbleModel, pdfReconstructionModel, pdfRefillingModel, excessMassDistributionModel, + relaxationRate, force, surfaceTension, enableForceWeighting, useSimpleMassExchange, cellConversionThreshold, + cellConversionForceThreshold); + + dynamicsHandler.addSweeps(timeloop); + + // add load balancing + const LoadBalancer< FlagField_T, CommunicationStencil_T, LatticeModelStencil_T > loadBalancer( + blockForest, communication, pdfCommunication, bubbleModel, uint_c(50), uint_c(10), uint_c(5), + loadBalancingFrequency, printLoadBalancingStatistics); + timeloop.addFuncAfterTimeStep(loadBalancer, "Sweep: load balancing"); + + const std::shared_ptr< Vector3< real_t > > centerOfMass = + std::make_shared< Vector3< real_t > >(Vector3< real_t >(real_c(0))); + const CenterOfMassComputer< FreeSurfaceBoundaryHandling_T > centerOfMassComputer( + blockForest, freeSurfaceBoundaryHandling, evaluationFrequency, centerOfMass); + timeloop.addFuncAfterTimeStep(centerOfMassComputer, "Evaluator: center of mass"); + + const std::shared_ptr< real_t > totalMass = std::make_shared< real_t >(real_c(0)); + const TotalMassComputer< FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T > totalMassComputer( + blockForest, freeSurfaceBoundaryHandling, pdfFieldID, fillFieldID, evaluationFrequency, totalMass); + timeloop.addFuncAfterTimeStep(totalMassComputer, "Evaluator: total mass"); + + // add VTK output + addVTKOutput< LatticeModel_T, FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T, VectorField_T >( + blockForest, timeloop, walberlaEnv.config(), flagInfo, pdfFieldID, flagFieldID, fillFieldID, forceFieldID, + geometryHandler.getCurvatureFieldID(), geometryHandler.getNormalFieldID(), + geometryHandler.getObstNormalFieldID()); + + // add triangle mesh output of free surface + SurfaceMeshWriter< ScalarField_T, FlagField_T > surfaceMeshWriter( + blockForest, fillFieldID, flagFieldID, flagIDs::liquidInterfaceGasFlagIDs, real_c(1), walberlaEnv.config()); + surfaceMeshWriter(); // write initial mesh + timeloop.addFuncAfterTimeStep(surfaceMeshWriter, "Writer: surface mesh"); + + // add performance logging + const lbm::PerformanceLogger< FlagField_T > perfLogger(blockForest, flagFieldID, flagIDs::liquidInterfaceFlagIDs, + performanceLogFrequency); + timeloop.addFuncAfterTimeStep(perfLogger, "Evaluator: performance logging"); + + WcTimingPool timingPool; + + const real_t stoppingHeight = real_c(domainSize[2]) - bubbleHeight * tubeDiameter; + + uint_t timestepOld = uint_c(0); + Vector3< real_t > centerOfMassOld = Vector3< real_t >(real_c(0)); + + for (uint_t t = uint_c(0); t != timesteps; ++t) + { + timeloop.singleStep(timingPool, true); + + // only evaluate if center of mass has moved "significantly" + if ((*centerOfMass)[2] > (centerOfMassOld[2] + real_c(1)) && t > uint_c(0)) + { + WALBERLA_ROOT_SECTION() + { + real_t riseVelocity = ((*centerOfMass)[2] - centerOfMassOld[2]) / real_c(t - timestepOld); + const real_t dragForce = real_c(4) / real_c(3) * gravitationalAccelerationZ * real_c(bubbleDiameter) / + (riseVelocity * riseVelocity); + + WALBERLA_LOG_DEVEL("time step = " << t); + WALBERLA_LOG_DEVEL("\t\tcenterOfMass = " << *centerOfMass << "\n\t\triseVelocity = " << riseVelocity + << "\n\t\tdragForce = " << dragForce); + WALBERLA_LOG_DEVEL("\t\ttotalMass = " << *totalMass); + + const std::vector< real_t > resultVector{ (*centerOfMass)[2], riseVelocity, dragForce }; + + writeVectorToFile(resultVector, t, filename); + } + + timestepOld = t; + centerOfMassOld = *centerOfMass; + } + + + // stop simulation before bubble hits the top wall + if ((*centerOfMass)[2] > stoppingHeight) { break; } + + if (t % performanceLogFrequency == uint_c(0) && t > uint_c(0)) { timingPool.logResultOnRoot(); } + } + return EXIT_SUCCESS; +} + +template< typename T > +void writeVectorToFile(const std::vector< T >& vector, uint_t timestep, const std::string& filename) +{ + std::fstream file; + file.open(filename, std::fstream::app); + + file << timestep; + for (const auto i : vector) + { + file << "\t" << i; + } + + file << "\n"; + file.close(); +} + +} // namespace TaylorBubble +} // namespace free_surface +} // namespace walberla + +int main(int argc, char** argv) { return walberla::free_surface::TaylorBubble::main(argc, argv); } \ No newline at end of file diff --git a/apps/showcases/FreeSurface/TaylorBubble.prm b/apps/showcases/FreeSurface/TaylorBubble.prm new file mode 100644 index 000000000..a2b289aee --- /dev/null +++ b/apps/showcases/FreeSurface/TaylorBubble.prm @@ -0,0 +1,110 @@ +BlockForestParameters +{ + cellsPerBlock < 16, 16, 20 >; + periodicity < 0, 0, 0 >; + loadBalancingFrequency 0; + printLoadBalancingStatistics false; +} + +DomainParameters +{ + tubeDiameter 32; + bubbleDiameter 0.75; // value multiplied with tubeDiameter + bubbleHeight 3; // value multiplied with tubeDiameter + bubbleBottomEnd < 0.5, 0.5, 1 >; // initial cylindrical bubble's bottom end center (values multiplied with tubeDiameter) + domainSizeFactor < 1, 1, 10 >; // values multiplied with tubeDiameter +} + +PhysicsParameters +{ + bondNumber 100; + mortonNumber 0.015; + relaxationRate 1.8; + enableWetting false; + contactAngle 60; // only used if enableWetting=true + timesteps 12240; +} + +ModelParameters +{ + pdfReconstructionModel OnlyMissing; + pdfRefillingModel EquilibriumRefilling; + excessMassDistributionModel EvenlyAllInterface; + curvatureModel FiniteDifferenceMethod; + enableForceWeighting false; + useSimpleMassExchange false; + cellConversionThreshold 1e-2; + cellConversionForceThreshold 1e-1; + + enableBubbleModel true; + enableBubbleSplits false; // only used if enableBubbleModel=true +} + +EvaluationParameters +{ + performanceLogFrequency 1224; + evaluationFrequency 61; + filename taylor-bubble.txt; +} + +BoundaryParameters +{ + // X + Border { direction W; walldistance -1; NoSlip{} } + Border { direction E; walldistance -1; NoSlip{} } + + // Y + Border { direction N; walldistance -1; NoSlip{} } + Border { direction S; walldistance -1; NoSlip{} } + + // Z + Border { direction T; walldistance -1; NoSlip{} } + Border { direction B; walldistance -1; NoSlip{} } +} + +MeshOutputParameters +{ + writeFrequency 612; + baseFolder mesh-out; +} + +VTK +{ + fluid_field + { + writeFrequency 612; + ghostLayers 0; + baseFolder vtk-out; + samplingResolution 1; + + writers + { + fill_level; + mapped_flag; + velocity; + density; + curvature; + normal; + obstacle_normal; + //pdf; + //flag; + //force; + } + + inclusion_filters + { + //liquidInterfaceFilter; // only include liquid and interface cells in VTK output + } + + before_functions + { + //ghost_layer_synchronization; // only needed if writing the ghost layer + } + } + domain_decomposition + { + writeFrequency 0; + baseFolder vtk-out; + outputDomainDecomposition true; + } +} \ No newline at end of file diff --git a/src/core/StringUtility.h b/src/core/StringUtility.h index 780cfb9de..9ac7590fa 100644 --- a/src/core/StringUtility.h +++ b/src/core/StringUtility.h @@ -30,50 +30,60 @@ namespace walberla { // Convert (in place) every character in string to uppercase. -inline void string_to_upper(std::string &s); +inline void string_to_upper(std::string& s); // Convert (copy) every character in string to uppercase. -inline std::string string_to_upper_copy(const std::string &s); +inline std::string string_to_upper_copy(const std::string& s); // Convert (in place) every character in string to lowercase. -inline void string_to_lower(std::string &s); +inline void string_to_lower(std::string& s); // Convert (in place) every character in string to lowercase. -inline std::string string_to_lower_copy(const std::string &s); +inline std::string string_to_lower_copy(const std::string& s); // Remove (in place) all whitespaces at the beginning of a string. -inline void string_trim_left(std::string &s); +inline void string_trim_left(std::string& s); // Remove (in place) all whitespaces at the end of a string. -inline void string_trim_right(std::string &s); +inline void string_trim_right(std::string& s); // Remove (in place) all whitespaces at the beginning and at the end of a string. -inline void string_trim(std::string &s); +inline void string_trim(std::string& s); // Remove (copy) all whitespaces at the beginning of a string. -inline std::string string_trim_left_copy(const std::string &s); +inline std::string string_trim_left_copy(const std::string& s); // Remove (copy) all whitespaces at the end of a string. -inline std::string string_trim_right_copy(const std::string &s); +inline std::string string_trim_right_copy(const std::string& s); // Remove (copy) all whitespaces at the beginning and at the end of a string. -inline std::string string_trim_copy(const std::string &s); +inline std::string string_trim_copy(const std::string& s); // Split a string at the given delimiters into a vector of substrings. // E.g. specify std::string(" |,") in order to split at characters ' ' and ','. -inline std::vector<std::string> string_split(std::string s, const std::string &delimiters); +inline std::vector< std::string > string_split(std::string s, const std::string& delimiters); // Replace (in place) all occurrences of substring "old" with substring "new". -inline void string_replace_all(std::string &s, const std::string &oldSubstr, const std::string &newSubstr); +inline void string_replace_all(std::string& s, const std::string& oldSubstr, const std::string& newSubstr); // Replace (copy) all occurrences of substring "old" with substring "new". -inline std::string string_replace_all_copy(const std::string &s, const std::string &oldSubstr, const std::string &newSubstr); +inline std::string string_replace_all_copy(const std::string& s, const std::string& oldSubstr, + const std::string& newSubstr); // Check whether a string ends with a certain substring. -inline bool string_ends_with(const std::string &s, const std::string &substr); +inline bool string_ends_with(const std::string& s, const std::string& substr); // Case-insensitive std::string::compare. -inline int string_icompare(const std::string &s1, const std::string &s2); +inline int string_icompare(const std::string& s1, const std::string& s2); + +// Convert a floating point number to string with a specified number of decimal places. +template< typename T > +inline std::string to_string_with_precision(T number, uint_t decimalPlaces); + +// Convert a floating point number to string with only the relevant number of decimal places, i.e., zeros at the end are +// cut off. +template< typename T > +inline std::string to_string_only_relevant_digits(T number); } // namespace walberla diff --git a/src/core/StringUtility.impl.h b/src/core/StringUtility.impl.h index 730bf5928..02b5f3937 100644 --- a/src/core/StringUtility.impl.h +++ b/src/core/StringUtility.impl.h @@ -31,61 +31,76 @@ namespace walberla { // Convert (in place) every character in string to uppercase. -inline void string_to_upper(std::string &s) { - std::transform(s.begin(), s.end(), s.begin(), [](char c){ return static_cast<char>(std::toupper(static_cast<unsigned char>(c))); }); +inline void string_to_upper(std::string& s) +{ + std::transform(s.begin(), s.end(), s.begin(), + [](char c) { return static_cast< char >(std::toupper(static_cast< unsigned char >(c))); }); } // Convert (copy) every character in string to uppercase. -inline std::string string_to_upper_copy(const std::string &s) { +inline std::string string_to_upper_copy(const std::string& s) +{ std::string result = s; string_to_upper(result); return result; } // Convert (in place) every character in string to lowercase. -inline void string_to_lower(std::string &s) { - std::transform(s.begin(), s.end(), s.begin(), [](char c){ return static_cast<char>(std::tolower(static_cast<unsigned char>(c))); }); +inline void string_to_lower(std::string& s) +{ + std::transform(s.begin(), s.end(), s.begin(), + [](char c) { return static_cast< char >(std::tolower(static_cast< unsigned char >(c))); }); } // Convert (copy) every character in string to lowercase. -inline std::string string_to_lower_copy(const std::string &s) { +inline std::string string_to_lower_copy(const std::string& s) +{ std::string result = s; string_to_lower(result); return result; } // Remove (in place) all whitespaces at the beginning of a string. -inline void string_trim_left(std::string &s) { - s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](char c) { return ! std::isspace(static_cast<unsigned char>(c)); })); +inline void string_trim_left(std::string& s) +{ + s.erase(s.begin(), + std::find_if(s.begin(), s.end(), [](char c) { return !std::isspace(static_cast< unsigned char >(c)); })); } // Remove (in place) all whitespaces at the end of a string. -inline void string_trim_right(std::string &s) { - s.erase(std::find_if(s.rbegin(), s.rend(), [](char c) { return ! std::isspace(static_cast<unsigned char>(c)); }).base(), s.end()); +inline void string_trim_right(std::string& s) +{ + s.erase( + std::find_if(s.rbegin(), s.rend(), [](char c) { return !std::isspace(static_cast< unsigned char >(c)); }).base(), + s.end()); } // Remove (in place) all whitespaces at the beginning and at the end of a string. -inline void string_trim(std::string &s) { +inline void string_trim(std::string& s) +{ string_trim_left(s); string_trim_right(s); } // Remove (copy) all whitespaces at the beginning of a string. -inline std::string string_trim_left_copy(const std::string &s) { +inline std::string string_trim_left_copy(const std::string& s) +{ std::string result = s; string_trim_left(result); return result; } // Remove (copy) all whitespaces at the end of a string. -inline std::string string_trim_right_copy(const std::string &s) { +inline std::string string_trim_right_copy(const std::string& s) +{ std::string result = s; string_trim_left(result); return result; } // Remove (copy) all whitespaces at the beginning and at the end of a string. -inline std::string string_trim_copy(const std::string &s) { +inline std::string string_trim_copy(const std::string& s) +{ std::string result = s; string_trim_left(result); return result; @@ -93,17 +108,22 @@ inline std::string string_trim_copy(const std::string &s) { // Split a string at the given delimiters into a vector of substrings. // E.g. specify std::string(" ,") in order to split at characters ' ' and ','. -inline std::vector<std::string> string_split(std::string s, const std::string &delimiters) { - std::vector<std::string> substrings; +inline std::vector< std::string > string_split(std::string s, const std::string& delimiters) +{ + std::vector< std::string > substrings; - auto sub_begin = s.begin(); // iterator to the begin and end of a substring - auto sub_end = sub_begin; + auto sub_begin = s.begin(); // iterator to the begin and end of a substring + auto sub_end = sub_begin; - for (auto it = s.begin(); it != s.end(); ++it) { - for (auto d : delimiters) { - if (*it == d) { // current character in s is a delimiter + for (auto it = s.begin(); it != s.end(); ++it) + { + for (auto d : delimiters) + { + if (*it == d) + { // current character in s is a delimiter sub_end = it; - if (sub_begin < sub_end) { // make sure that the substring is not empty + if (sub_begin < sub_end) + { // make sure that the substring is not empty substrings.emplace_back(sub_begin, sub_end); } sub_begin = ++sub_end; @@ -113,38 +133,73 @@ inline std::vector<std::string> string_split(std::string s, const std::string &d } // add substring from last delimiter to the end of s - if (sub_begin < s.end()) { - substrings.emplace_back(sub_begin, s.end()); - } + if (sub_begin < s.end()) { substrings.emplace_back(sub_begin, s.end()); } return substrings; } // Replace (in place) all occurrences of substring "old" with substring "new". -inline void string_replace_all(std::string &s, const std::string &oldSubstr, const std::string &newSubstr) { +inline void string_replace_all(std::string& s, const std::string& oldSubstr, const std::string& newSubstr) +{ // loop written to avoid infinite-loops when newSubstr contains oldSubstr - for (size_t pos = s.find(oldSubstr); pos != std::string::npos;) { + for (size_t pos = s.find(oldSubstr); pos != std::string::npos;) + { s.replace(pos, oldSubstr.length(), newSubstr); pos = s.find(oldSubstr, pos + newSubstr.length()); } } // Replace (copy) all occurrences of substring "old" with substring "new". -inline std::string string_replace_all_copy(const std::string &s, const std::string &oldSubstr, const std::string &newSubstr) { +inline std::string string_replace_all_copy(const std::string& s, const std::string& oldSubstr, + const std::string& newSubstr) +{ std::string result = s; string_replace_all(result, oldSubstr, newSubstr); return result; } // Check whether a string ends with a certain substring. -inline bool string_ends_with(const std::string &s, const std::string &substr) { +inline bool string_ends_with(const std::string& s, const std::string& substr) +{ return s.rfind(substr) == (s.length() - substr.length()); } // Case-insensitive wrapper for std::string::compare (return values as for std::string::compare). -inline int string_icompare(const std::string &s1, const std::string &s2) { +inline int string_icompare(const std::string& s1, const std::string& s2) +{ // function std::string::compare returns 0 in case of equality => invert result to obtain expected bool behavior return string_to_lower_copy(s1).compare(string_to_lower_copy(s2)); } +// Convert a floating point number to string with a specified number of decimal places. +template< typename T > +inline std::string to_string_with_precision(T number, uint_t decimalPlaces) +{ + return std::to_string(number).substr(0, std::to_string(number).find(".") + decimalPlaces + 1); +} + +// Convert a floating point number to string with only the relevant number of decimal places, i.e., zeros at the end are +// cut off. +template< typename T > +inline std::string to_string_only_relevant_digits(T number) +{ + if (std::numeric_limits< T >::is_integer) { return std::to_string(number); } + + // get integer part of number + std::string integerPart = std::to_string(number).substr(0, std::to_string(number).find(".") + 1); + + // get fractional part of number + std::string fractionalPart = std::to_string(number).substr(std::to_string(number).find(".") + 1); + + // remove any 0 at the end of the number's fractional part + fractionalPart = fractionalPart.substr(0, fractionalPart.find_last_not_of('0') + 1); + + // remove "." of integerPart if fractionalPart is empty + if (fractionalPart.empty()) { + integerPart.pop_back(); + } + + return integerPart + fractionalPart; +} + } // namespace walberla diff --git a/src/core/cell/Cell.h b/src/core/cell/Cell.h index 63a650dbd..8f41297b7 100644 --- a/src/core/cell/Cell.h +++ b/src/core/cell/Cell.h @@ -1,15 +1,15 @@ //====================================================================================================================== // -// This file is part of waLBerla. waLBerla is free software: you can +// This file is part of waLBerla. waLBerla 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 +// License as published by the Free Software Foundation, either version 3 of // the License, or (at your option) any later version. -// -// waLBerla 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 +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. // @@ -24,6 +24,7 @@ #include "core/DataTypes.h" #include "core/debug/Debug.h" +#include "core/math/Vector3.h" #include "core/mpi/BufferSizeTrait.h" #include "core/mpi/RecvBuffer.h" #include "core/mpi/SendBuffer.h" @@ -51,6 +52,7 @@ public: inline Cell( const cell_idx_t _x, const cell_idx_t _y, const cell_idx_t _z ) { cell[0] = _x; cell[1] = _y; cell[2] = _z; } //inline Cell( const int _x, const int _y, const int _z ); inline Cell( const uint_t _x, const uint_t _y, const uint_t _z ); + inline Cell( const Vector3<cell_idx_t>& vec ){ cell[0] = vec[0]; cell[1] = vec[1]; cell[2] = vec[2]; }; //@} /*! \name Arithmetic operators */ diff --git a/src/core/stringToNum.h b/src/core/stringToNum.h index d031b53d3..10c897956 100644 --- a/src/core/stringToNum.h +++ b/src/core/stringToNum.h @@ -1,15 +1,15 @@ //====================================================================================================================== // -// This file is part of waLBerla. waLBerla is free software: you can +// This file is part of waLBerla. waLBerla 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 +// License as published by the Free Software Foundation, either version 3 of // the License, or (at your option) any later version. -// -// waLBerla 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 +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. // diff --git a/src/lbm/CMakeLists.txt b/src/lbm/CMakeLists.txt index 511aafa84..829e0505b 100644 --- a/src/lbm/CMakeLists.txt +++ b/src/lbm/CMakeLists.txt @@ -1,6 +1,6 @@ ################################################################################################### # -# Module lbm +# Module lbm # ################################################################################################### @@ -33,6 +33,7 @@ add_subdirectory( blockforest ) add_subdirectory( sweeps ) add_subdirectory( communication ) add_subdirectory( field ) +add_subdirectory( free_surface ) add_subdirectory( refinement ) add_subdirectory( gui ) add_subdirectory( boundary ) @@ -47,4 +48,3 @@ add_subdirectory( inplace_streaming ) ################################################################################################### - \ No newline at end of file diff --git a/src/lbm/blockforest/communication/CMakeLists.txt b/src/lbm/blockforest/communication/CMakeLists.txt new file mode 100644 index 000000000..a39e2f187 --- /dev/null +++ b/src/lbm/blockforest/communication/CMakeLists.txt @@ -0,0 +1,5 @@ +target_sources( lbm + PRIVATE + SimpleCommunication.h + UpdateSecondGhostLayer.h + ) diff --git a/src/lbm/blockforest/communication/SimpleCommunication.h b/src/lbm/blockforest/communication/SimpleCommunication.h new file mode 100644 index 000000000..42bffe263 --- /dev/null +++ b/src/lbm/blockforest/communication/SimpleCommunication.h @@ -0,0 +1,170 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file SimpleCommunication.h +//! \ingroup blockforest +//! \author Martin Bauer +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +// +//====================================================================================================================== + +#pragma once + +#include "blockforest/communication/UniformBufferedScheme.h" + +#include "core/Abort.h" +#include "core/math/Vector3.h" +#include "core/mpi/BufferDataTypeExtensions.h" + +#include "field/FlagField.h" +#include "field/communication/PackInfo.h" + +#include "lbm/communication/PdfFieldPackInfo.h" + +namespace walberla +{ +namespace blockforest +{ +using communication::UniformBufferedScheme; + +template< typename Stencil_T > +class SimpleCommunication : public communication::UniformBufferedScheme< Stencil_T > +{ + using RealScalarField_T = GhostLayerField< real_t, 1 >; + using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >; + using PdfField_T = GhostLayerField< real_t, Stencil_T::Size >; + using UintScalarField_T = GhostLayerField< uint_t, 1 >; + + using FlagField16_T = FlagField< uint16_t >; + using FlagField32_T = FlagField< uint32_t >; + using FlagField64_T = FlagField< uint64_t >; + + public: + SimpleCommunication(const std::weak_ptr< StructuredBlockForest >& blockForest, BlockDataID f1) + : UniformBufferedScheme< Stencil_T >(blockForest), blockForest_(blockForest) + { + (*this) << f1; + } + SimpleCommunication(const std::weak_ptr< StructuredBlockForest >& blockForest, BlockDataID f1, BlockDataID f2) + : UniformBufferedScheme< Stencil_T >(blockForest), blockForest_(blockForest) + { + (*this) << f1 << f2; + } + SimpleCommunication(const std::weak_ptr< StructuredBlockForest >& blockForest, BlockDataID f1, BlockDataID f2, + BlockDataID f3) + : UniformBufferedScheme< Stencil_T >(blockForest), blockForest_(blockForest) + { + (*this) << f1 << f2 << f3; + } + + SimpleCommunication(const std::weak_ptr< StructuredBlockForest >& blockForest, BlockDataID f1, BlockDataID f2, + BlockDataID f3, BlockDataID f4) + : UniformBufferedScheme< Stencil_T >(blockForest), blockForest_(blockForest) + { + (*this) << f1 << f2 << f3 << f4; + } + SimpleCommunication(const std::weak_ptr< StructuredBlockForest >& blockForest, BlockDataID f1, BlockDataID f2, + BlockDataID f3, BlockDataID f4, BlockDataID f5) + : UniformBufferedScheme< Stencil_T >(blockForest), blockForest_(blockForest) + { + (*this) << f1 << f2 << f3 << f4 << f5; + } + SimpleCommunication(const std::weak_ptr< StructuredBlockForest >& blockForest, BlockDataID f1, BlockDataID f2, + BlockDataID f3, BlockDataID f4, BlockDataID f5, BlockDataID f6) + : UniformBufferedScheme< Stencil_T >(blockForest), blockForest_(blockForest) + { + (*this) << f1 << f2 << f3 << f4 << f5 << f6; + } + SimpleCommunication(const std::weak_ptr< StructuredBlockForest >& blockForest, BlockDataID f1, BlockDataID f2, + BlockDataID f3, BlockDataID f4, BlockDataID f5, BlockDataID f6, BlockDataID f7) + : UniformBufferedScheme< Stencil_T >(blockForest), blockForest_(blockForest) + { + (*this) << f1 << f2 << f3 << f4 << f5 << f6 << f7; + } + SimpleCommunication(const std::weak_ptr< StructuredBlockForest >& blockForest, BlockDataID f1, BlockDataID f2, + BlockDataID f3, BlockDataID f4, BlockDataID f5, BlockDataID f6, BlockDataID f7, BlockDataID f8) + : UniformBufferedScheme< Stencil_T >(blockForest), blockForest_(blockForest) + { + (*this) << f1 << f2 << f3 << f4 << f5 << f6 << f7 << f8; + } + + SimpleCommunication& operator<<(BlockDataID fieldId) + { + auto blockForest = blockForest_.lock(); + WALBERLA_CHECK_NOT_NULLPTR(blockForest); + + if (blockForest->begin() == blockForest->end()) { return *this; } + + IBlock& firstBlock = *(blockForest->begin()); + + using field::communication::PackInfo; + + if (firstBlock.isDataClassOrSubclassOf< PdfField_T >(fieldId)) + { + this->addPackInfo(make_shared< PackInfo< PdfField_T > >(fieldId)); + } + else + { + if (firstBlock.isDataClassOrSubclassOf< RealScalarField_T >(fieldId)) + { + this->addPackInfo(make_shared< PackInfo< RealScalarField_T > >(fieldId)); + } + else + { + if (firstBlock.isDataClassOrSubclassOf< VectorField_T >(fieldId)) + { + this->addPackInfo(make_shared< PackInfo< VectorField_T > >(fieldId)); + } + else + { + if (firstBlock.isDataClassOrSubclassOf< FlagField16_T >(fieldId)) + { + this->addPackInfo(make_shared< PackInfo< FlagField16_T > >(fieldId)); + } + else + { + if (firstBlock.isDataClassOrSubclassOf< FlagField32_T >(fieldId)) + { + this->addPackInfo(make_shared< PackInfo< FlagField32_T > >(fieldId)); + } + else + { + if (firstBlock.isDataClassOrSubclassOf< FlagField64_T >(fieldId)) + { + this->addPackInfo(make_shared< PackInfo< FlagField64_T > >(fieldId)); + } + else + { + if (firstBlock.isDataClassOrSubclassOf< UintScalarField_T >(fieldId)) + { + this->addPackInfo(make_shared< PackInfo< UintScalarField_T > >(fieldId)); + } + else { WALBERLA_ABORT("Problem with UID"); } + } + } + } + } + } + } + + return *this; + } + + protected: + std::weak_ptr< StructuredBlockForest > blockForest_; +}; // class SimpleCommunication + +} // namespace blockforest +} // namespace walberla diff --git a/src/lbm/blockforest/communication/UpdateSecondGhostLayer.h b/src/lbm/blockforest/communication/UpdateSecondGhostLayer.h new file mode 100644 index 000000000..1e9c69ed1 --- /dev/null +++ b/src/lbm/blockforest/communication/UpdateSecondGhostLayer.h @@ -0,0 +1,144 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file UpdateSecondGhostLayer.h +//! \ingroup blockforest +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +// +//====================================================================================================================== + +#pragma once + +#include "blockforest/StructuredBlockForest.h" + +namespace walberla +{ +namespace blockforest +{ +/******************************************************************************************************************** + * Manual update of a field's second and further ghost layers in setups with domain size = 1 and periodicity in at + * least one direction. + * + * If a field has two ghost layers and if the inner field has only a size of one (in one or more directions), + * regular communication does not update the second (and further) ghost layers correctly. Here, the content of the + * first ghost layers is manually copied to the second (and further) ghost layers when the dimension of the field is + * one in this direction. + *******************************************************************************************************************/ +template< typename Field_T > +class UpdateSecondGhostLayer +{ + public: + UpdateSecondGhostLayer(const std::weak_ptr< StructuredBlockForest >& blockForestPtr, BlockDataID fieldID) + : blockForest_(blockForestPtr), fieldID_(fieldID) + { + const auto blockForest = blockForest_.lock(); + WALBERLA_CHECK_NOT_NULLPTR(blockForest); + + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + Field_T* const field = blockIt->template getData< Field_T >(fieldID_); + + // this function is only necessary if the flag field has at least two ghost layers + if (field->nrOfGhostLayers() < uint_c(2)) { isNecessary_ = false; } + else + { + // running this function is only necessary when the field is of size 1 in at least one direction + isNecessary_ = (field->xSize() == uint_c(1) && blockForest->isXPeriodic()) || + (field->ySize() == uint_c(1) && blockForest->isYPeriodic()) || + (field->zSize() == uint_c(1) && blockForest->isZPeriodic()); + } + } + } + + void operator()() + { + if (!isNecessary_) { return; } + + const auto blockForest = blockForest_.lock(); + WALBERLA_CHECK_NOT_NULLPTR(blockForest); + + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + Field_T* const field = blockIt->template getData< Field_T >(fieldID_); + + if (field->xSize() == uint_c(1) && blockForest->isXPeriodic()) + { + // iterate ghost layer at x == -1 + for (auto fieldIt = field->beginGhostLayerOnly(uint_c(1), stencil::W, true); fieldIt != field->end(); + ++fieldIt) + { + // copy data from ghost layer at x == -1 to x == -2 + fieldIt.neighbor(stencil::W) = *fieldIt; + } + + // iterate ghost layer at x == xSize()+1 + for (auto fieldIt = field->beginGhostLayerOnly(uint_c(1), stencil::E, true); fieldIt != field->end(); + ++fieldIt) + { + // copy data from ghost layer at x == xSize()+1 to x == xSize()+2 + fieldIt.neighbor(stencil::E) = *fieldIt; + } + } + + if (field->ySize() == uint_c(1) && blockForest->isYPeriodic()) + { + // iterate ghost layer at y == -1 + for (auto fieldIt = field->beginGhostLayerOnly(uint_c(1), stencil::S, true); fieldIt != field->end(); + ++fieldIt) + { + // copy data from ghost layer at y == -1 to y == -2 + fieldIt.neighbor(stencil::S) = *fieldIt; + } + + // iterate ghost layer at y == ySize()+1 + for (auto fieldIt = field->beginGhostLayerOnly(uint_c(1), stencil::N, true); fieldIt != field->end(); + ++fieldIt) + { + // copy data from ghost layer at y == ySize()+1 to y == ySize()+2 + fieldIt.neighbor(stencil::N) = *fieldIt; + } + } + + if (field->zSize() == uint_c(1) && blockForest->isZPeriodic()) + { + // iterate ghost layer at z == -1 + for (auto fieldIt = field->beginGhostLayerOnly(uint_c(1), stencil::B, true); fieldIt != field->end(); + ++fieldIt) + { + // copy data from ghost layer at z == -1 to z == -2 + fieldIt.neighbor(stencil::B) = *fieldIt; + } + + // iterate ghost layer at y == zSize()+1 + for (auto fieldIt = field->beginGhostLayerOnly(uint_c(1), stencil::T, true); fieldIt != field->end(); + ++fieldIt) + { + // copy data from ghost layer at z == zSize()+1 to z == zSize()+2 + fieldIt.neighbor(stencil::T) = *fieldIt; + } + } + } + } + + private: + std::weak_ptr< StructuredBlockForest > blockForest_; + + BlockDataID fieldID_; + + bool isNecessary_; +}; // class UpdateSecondGhostLayer + +} // namespace blockforest +} // namespace walberla diff --git a/src/lbm/boundary/SimpleExtrapolationOutflow.h b/src/lbm/boundary/SimpleExtrapolationOutflow.h new file mode 100644 index 000000000..46c43aa70 --- /dev/null +++ b/src/lbm/boundary/SimpleExtrapolationOutflow.h @@ -0,0 +1,114 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file SimpleExtrapolationOutflow.h +//! \ingroup lbm +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Outflow boundary condition based on extrapolation. See equation F.1 in Geier et al., 2005, +//! doi: 10.1016/j.camwa.2015.05.001 +// +//====================================================================================================================== + +#pragma once + +#include "boundary/Boundary.h" + +#include "core/DataTypes.h" +#include "core/cell/CellInterval.h" +#include "core/config/Config.h" +#include "core/debug/Debug.h" + +#include "lbm/field/PdfField.h" + +#include "stencil/Directions.h" + +#include <vector> + +namespace walberla +{ +namespace lbm +{ +template< typename LatticeModel_T, typename FlagField_T > +class SimpleExtrapolationOutflow : public Boundary< typename FlagField_T::flag_t > +{ + protected: + using PDFField = PdfField< LatticeModel_T >; + using Stencil = typename LatticeModel_T::Stencil; + using flag_t = typename FlagField_T::flag_t; + + public: + static const bool threadsafe = true; + + static shared_ptr< BoundaryConfiguration > createConfiguration(const Config::BlockHandle& /*config*/) + { + return make_shared< BoundaryConfiguration >(); + } + + SimpleExtrapolationOutflow(const BoundaryUID& boundaryUID, const FlagUID& uid, PDFField* const pdfField) + : Boundary< flag_t >(boundaryUID), uid_(uid), pdfField_(pdfField) + { + WALBERLA_ASSERT_NOT_NULLPTR(pdfField_); + } + + void pushFlags(std::vector< FlagUID >& uids) const { uids.push_back(uid_); } + + void beforeBoundaryTreatment() const {} + void afterBoundaryTreatment() const {} + + template< typename Buffer_T > + void packCell(Buffer_T&, const cell_idx_t, const cell_idx_t, const cell_idx_t) const + {} + + template< typename Buffer_T > + void registerCell(Buffer_T&, const flag_t, const cell_idx_t, const cell_idx_t, const cell_idx_t) + {} + void registerCell(const flag_t, const cell_idx_t, const cell_idx_t, const cell_idx_t, const BoundaryConfiguration&) + {} + + void registerCells(const flag_t, const CellInterval&, const BoundaryConfiguration&) {} + template< typename CellIterator > + void registerCells(const flag_t, const CellIterator&, const CellIterator&, const BoundaryConfiguration&) + {} + + void unregisterCell(const flag_t, const cell_idx_t, const cell_idx_t, const cell_idx_t) const {} + +#ifndef NDEBUG + inline void treatDirection(const cell_idx_t x, const cell_idx_t y, const cell_idx_t z, const stencil::Direction dir, + const cell_idx_t nx, const cell_idx_t ny, const cell_idx_t nz, const flag_t mask) +#else + inline void treatDirection(const cell_idx_t x, const cell_idx_t y, const cell_idx_t z, const stencil::Direction dir, + const cell_idx_t nx, const cell_idx_t ny, const cell_idx_t nz, const flag_t /*mask*/) +#endif + { + WALBERLA_ASSERT_EQUAL(nx, x + cell_idx_c(stencil::cx[dir])); + WALBERLA_ASSERT_EQUAL(ny, y + cell_idx_c(stencil::cy[dir])); + WALBERLA_ASSERT_EQUAL(nz, z + cell_idx_c(stencil::cz[dir])); + WALBERLA_ASSERT_UNEQUAL(mask & this->mask_, numeric_cast< flag_t >(0)); + + // only true if "this->mask_" only contains one single flag, which is the case for the current implementation of + // this boundary condition (Outlet) + WALBERLA_ASSERT_EQUAL(mask & this->mask_, this->mask_); + + // equation F.1 in Geier et al. 2015 + pdfField_->get(nx, ny, nz, Stencil::invDirIdx(dir)) = pdfField_->get(x, y, z, Stencil::invDirIdx(dir)); + } + + private: + const FlagUID uid_; + PDFField* const pdfField_; +}; // class Outlet + +} // namespace lbm +} // namespace walberla \ No newline at end of file diff --git a/src/lbm/boundary/all.h b/src/lbm/boundary/all.h index 31709c60e..11f7698b3 100644 --- a/src/lbm/boundary/all.h +++ b/src/lbm/boundary/all.h @@ -34,6 +34,7 @@ #include "ParserUBB.h" #include "Pressure.h" #include "SimpleDiffusionDirichlet.h" +#include "SimpleExtrapolationOutflow.h" #include "SimplePAB.h" #include "SimplePressure.h" #include "SimpleUBB.h" diff --git a/src/lbm/free_surface/BlockStateDetectorSweep.h b/src/lbm/free_surface/BlockStateDetectorSweep.h new file mode 100644 index 000000000..f2ef50bd1 --- /dev/null +++ b/src/lbm/free_surface/BlockStateDetectorSweep.h @@ -0,0 +1,111 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file BlockStateDetectorSweep.h +//! \ingroup free_surface +//! \author Martin Bauer +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Set block states according to their content, i.e., free surface flags. +// +//====================================================================================================================== + +#pragma once + +#include "core/uid/SUID.h" + +#include "FlagInfo.h" + +namespace walberla +{ +namespace free_surface +{ +/*********************************************************************************************************************** + * Set block states according to free surface flag field: + * - blocks that contain at least one interface cell are marked as "fullFreeSurface" (computationally most expensive + * type of blocks) + * - cells without interface cell and with at least one liquid cell are marked "onlyLBM" + * - all other blocks are marked as "onlyGasAndBoundary" + **********************************************************************************************************************/ +template< typename FlagField_T > +class BlockStateDetectorSweep +{ + public: + static const SUID fullFreeSurface; + static const SUID onlyGasAndBoundary; + static const SUID onlyLBM; + + BlockStateDetectorSweep(const std::weak_ptr< StructuredBlockForest >& blockForestPtr, + FlagInfo< FlagField_T > flagInfo, ConstBlockDataID flagFieldID) + : flagFieldID_(flagFieldID), flagInfo_(flagInfo) + { + const auto blockForest = blockForestPtr.lock(); + WALBERLA_CHECK_NOT_NULLPTR(blockForest); + + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + (*this)(&(*blockIt)); // initialize block states + } + } + + void operator()(IBlock* const block) + { + bool liquidFound = false; + bool interfaceFound = false; + + const FlagField_T* const flagField = block->getData< const FlagField_T >(flagFieldID_); + + // search the flag field for interface and liquid cells + WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ(flagField, uint_c(1), { + const typename FlagField_T::ConstPtr flagFieldPtr(*flagField, x, y, z); + + // at inflow boundaries, interface cells are generated; therefore, the block must be fullFreeSurface even if it + // does not yet contain an interface cell + if (flagInfo_.isInterface(flagFieldPtr) || flagInfo_.isInflow(flagFieldPtr)) + { + interfaceFound = true; + break; // stop search, as this block belongs already to the computationally most expensive block type + } + + if (flagInfo_.isLiquid(flagFieldPtr)) { liquidFound = true; } + }) // WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ + + block->clearState(); + if (interfaceFound) + { + block->addState(fullFreeSurface); + return; + } + if (liquidFound) + { + block->addState(onlyLBM); + return; + } + block->addState(onlyGasAndBoundary); + } + + protected: + ConstBlockDataID flagFieldID_; + FlagInfo< FlagField_T > flagInfo_; +}; // class BlockStateDetectorSweep + +template< typename FlagField_T > +const SUID BlockStateDetectorSweep< FlagField_T >::fullFreeSurface = SUID("fullFreeSurface"); +template< typename FlagField_T > +const SUID BlockStateDetectorSweep< FlagField_T >::onlyGasAndBoundary = SUID("onlyGasAndBoundary"); +template< typename FlagField_T > +const SUID BlockStateDetectorSweep< FlagField_T >::onlyLBM = SUID("onlyLBM"); + +} // namespace free_surface +} // namespace walberla diff --git a/src/lbm/free_surface/CMakeLists.txt b/src/lbm/free_surface/CMakeLists.txt new file mode 100644 index 000000000..54b500251 --- /dev/null +++ b/src/lbm/free_surface/CMakeLists.txt @@ -0,0 +1,19 @@ +target_sources( lbm + PRIVATE + BlockStateDetectorSweep.h + FlagDefinitions.h + FlagInfo.h + FlagInfo.impl.h + InitFunctions.h + InterfaceFromFillLevel.h + LoadBalancing.h + MaxVelocityComputer.h + SurfaceMeshWriter.h + TotalMassComputer.h + VtkWriter.h + ) + +add_subdirectory( boundary ) +add_subdirectory( bubble_model ) +add_subdirectory( dynamics ) +add_subdirectory( surface_geometry ) diff --git a/src/lbm/free_surface/FlagDefinitions.h b/src/lbm/free_surface/FlagDefinitions.h new file mode 100644 index 000000000..4dd93303a --- /dev/null +++ b/src/lbm/free_surface/FlagDefinitions.h @@ -0,0 +1,51 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file FlagInfo.h +//! \ingroup free_surface +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Define free surface flags (e.g. conversion flags). +// +//====================================================================================================================== + +#include "field/FlagUID.h" + +namespace walberla +{ +namespace free_surface +{ +namespace flagIDs +{ +/*********************************************************************************************************************** + * Definition of free surface flag IDs. + **********************************************************************************************************************/ +const field::FlagUID interfaceFlagID = field::FlagUID("interface"); +const field::FlagUID liquidFlagID = field::FlagUID("liquid"); +const field::FlagUID gasFlagID = field::FlagUID("gas"); +const field::FlagUID convertedFlagID = field::FlagUID("converted"); +const field::FlagUID convertToGasFlagID = field::FlagUID("convert to gas"); +const field::FlagUID convertToLiquidFlagID = field::FlagUID("convert to liquid"); +const field::FlagUID convertedFromGasToInterfaceFlagID = field::FlagUID("convert from gas to interface"); +const field::FlagUID keepInterfaceForWettingFlagID = field::FlagUID("convert to and keep interface for wetting"); +const field::FlagUID convertToInterfaceForInflowFlagID = field::FlagUID("convert to interface for inflow"); + +const Set< field::FlagUID > liquidInterfaceFlagIDs = setUnion< field::FlagUID >(liquidFlagID, interfaceFlagID); + +const Set< field::FlagUID > liquidInterfaceGasFlagIDs = + setUnion(liquidInterfaceFlagIDs, Set< field::FlagUID >(gasFlagID)); + +} // namespace flagIDs +} // namespace free_surface +} // namespace walberla \ No newline at end of file diff --git a/src/lbm/free_surface/FlagInfo.h b/src/lbm/free_surface/FlagInfo.h new file mode 100644 index 000000000..e332cf8dc --- /dev/null +++ b/src/lbm/free_surface/FlagInfo.h @@ -0,0 +1,215 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file FlagInfo.h +//! \ingroup free_surface +//! \author Martin Bauer +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Manage free surface flags (e.g. conversion flags). +// +//====================================================================================================================== + +#pragma once + +#include "core/mpi/Broadcast.h" + +#include "domain_decomposition/StructuredBlockStorage.h" + +#include "field/FlagField.h" + +#include "FlagDefinitions.h" + +namespace walberla +{ +namespace free_surface +{ +/*********************************************************************************************************************** + * Manage free surface flags (e.g. conversion flags). + **********************************************************************************************************************/ +template< typename FlagField_T > +class FlagInfo +{ + public: + using flag_t = typename FlagField_T::flag_t; + + FlagInfo(); + FlagInfo(const Set< field::FlagUID >& obstacleIDs, + const Set< field::FlagUID >& outflowIDs = Set< field::FlagUID >::emptySet(), + const Set< field::FlagUID >& inflowIDs = Set< field::FlagUID >::emptySet(), + const Set< field::FlagUID >& freeSlipIDs = Set< field::FlagUID >::emptySet()); + + bool operator==(const FlagInfo& o) const; + bool operator!=(const FlagInfo& o) const; + + flag_t interfaceFlag; + flag_t liquidFlag; + flag_t gasFlag; + + flag_t convertedFlag; // interface cell that is already converted + flag_t convertToGasFlag; // interface cell with too low fill level, should be converted + flag_t convertToLiquidFlag; // interface cell with too high fill level, should be converted + flag_t convertFromGasToInterfaceFlag; // interface cell that was a gas cell and needs refilling of its pdfs + flag_t keepInterfaceForWettingFlag; // gas/liquid cell that needs to be converted to interface cell for a smooth + // continuation of the wetting surface (see dissertation of S. Donath, 2011, + // section 6.3.5.3) + flag_t convertToInterfaceForInflowFlag; // gas cell that needs to be converted to interface cell to enable inflow + + flag_t obstacleFlagMask; + flag_t outflowFlagMask; + flag_t inflowFlagMask; + flag_t freeSlipFlagMask; // free slip obstacle cell (needs to be treated separately since PDFs going from gas into + // boundary are not available and must be reconstructed) + + template< typename FieldItOrPtr_T > + inline bool isInterface(const FieldItOrPtr_T& flagItOrPtr) const + { + return isFlagSet(flagItOrPtr, interfaceFlag); + } + template< typename FieldItOrPtr_T > + inline bool isLiquid(const FieldItOrPtr_T& flagItOrPtr) const + { + return isFlagSet(flagItOrPtr, liquidFlag); + } + template< typename FieldItOrPtr_T > + inline bool isGas(const FieldItOrPtr_T& flagItOrPtr) const + { + return isFlagSet(flagItOrPtr, gasFlag); + } + template< typename FieldItOrPtr_T > + inline bool isObstacle(const FieldItOrPtr_T& flagItOrPtr) const + { + return isPartOfMaskSet(flagItOrPtr, obstacleFlagMask); + } + template< typename FieldItOrPtr_T > + inline bool isOutflow(const FieldItOrPtr_T& flagItOrPtr) const + { + return isPartOfMaskSet(flagItOrPtr, outflowFlagMask); + } + template< typename FieldItOrPtr_T > + inline bool isInflow(const FieldItOrPtr_T& flagItOrPtr) const + { + return isPartOfMaskSet(flagItOrPtr, inflowFlagMask); + } + template< typename FieldItOrPtr_T > + inline bool isFreeSlip(const FieldItOrPtr_T& flagItOrPtr) const + { + return isPartOfMaskSet(flagItOrPtr, freeSlipFlagMask); + } + template< typename FieldItOrPtr_T > + inline bool hasConverted(const FieldItOrPtr_T& flagItOrPtr) const + { + return isFlagSet(flagItOrPtr, convertedFlag); + } + template< typename FieldItOrPtr_T > + inline bool hasConvertedToGas(const FieldItOrPtr_T& flagItOrPtr) const + { + return isFlagSet(flagItOrPtr, convertToGasFlag); + } + template< typename FieldItOrPtr_T > + inline bool hasConvertedToLiquid(const FieldItOrPtr_T& flagItOrPtr) const + { + return isFlagSet(flagItOrPtr, convertToLiquidFlag); + } + template< typename FieldItOrPtr_T > + inline bool hasConvertedFromGasToInterface(const FieldItOrPtr_T& flagItOrPtr) const + { + return isFlagSet(flagItOrPtr, convertFromGasToInterfaceFlag); + } + template< typename FieldItOrPtr_T > + inline bool isKeepInterfaceForWetting(const FieldItOrPtr_T& flagItOrPtr) const + { + return isFlagSet(flagItOrPtr, keepInterfaceForWettingFlag); + } + template< typename FieldItOrPtr_T > + inline bool isConvertToInterfaceForInflow(const FieldItOrPtr_T& flagItOrPtr) const + { + return isFlagSet(flagItOrPtr, convertToInterfaceForInflowFlag); + } + + inline bool isInterface(const flag_t val) const { return isFlagSet(val, interfaceFlag); } + inline bool isLiquid(const flag_t val) const { return isFlagSet(val, liquidFlag); } + inline bool isGas(const flag_t val) const { return isFlagSet(val, gasFlag); } + inline bool isObstacle(const flag_t val) const { return isPartOfMaskSet(val, obstacleFlagMask); } + inline bool isOutflow(const flag_t val) const { return isPartOfMaskSet(val, outflowFlagMask); } + inline bool isInflow(const flag_t val) const { return isPartOfMaskSet(val, inflowFlagMask); } + inline bool isFreeSlip(const flag_t val) const { return isPartOfMaskSet(val, freeSlipFlagMask); } + inline bool isKeepInterfaceForWetting(const flag_t val) const { return isFlagSet(val, keepInterfaceForWettingFlag); } + inline bool hasConvertedToGas(const flag_t val) const { return isFlagSet(val, convertToGasFlag); } + inline bool hasConvertedToLiquid(const flag_t val) const { return isFlagSet(val, convertToLiquidFlag); } + inline bool hasConvertedFromGasToInterface(const flag_t val) const + { + return isFlagSet(val, convertFromGasToInterfaceFlag); + } + inline bool isConvertToInterfaceForInflow(const flag_t val) const + { + return isFlagSet(val, convertToInterfaceForInflowFlag); + } + + // check whether FlagInfo is identical on all blocks and all processes + bool isConsistentAcrossBlocksAndProcesses(const std::weak_ptr< StructuredBlockStorage >& blockStorage, + ConstBlockDataID flagFieldID) const; + + // register flags in flag field + static void registerFlags(FlagField_T* field, const Set< field::FlagUID >& obstacleIDs, + const Set< field::FlagUID >& outflowIDs = Set< field::FlagUID >::emptySet(), + const Set< field::FlagUID >& inflowIDs = Set< field::FlagUID >::emptySet(), + const Set< field::FlagUID >& freeSlipIDs = Set< field::FlagUID >::emptySet()); + + void registerFlags(FlagField_T* field) const; + + Set< field::FlagUID > getObstacleIDSet() const { return obstacleIDs_; } + Set< field::FlagUID > getOutflowIDs() const { return outflowIDs_; } + Set< field::FlagUID > getInflowIDs() const { return inflowIDs_; } + Set< field::FlagUID > getFreeSlipIDs() const { return freeSlipIDs_; } + + protected: + FlagInfo(const FlagField_T* field, const Set< field::FlagUID >& obstacleIDs, const Set< field::FlagUID >& outflowIDs, + const Set< field::FlagUID >& inflowIDs, const Set< field::FlagUID >& freeSlipIDs); + + // create sets of flag IDs with flags from free_surface/boundary/FreeSurfaceBoundaryHandling.impl.h + Set< field::FlagUID > obstacleIDs_; + Set< field::FlagUID > outflowIDs_; + Set< field::FlagUID > inflowIDs_; + Set< field::FlagUID > freeSlipIDs_; +}; + +template< typename FlagField_T > +mpi::SendBuffer& operator<<(mpi::SendBuffer& buf, const FlagInfo< FlagField_T >& flagInfo) +{ + buf << flagInfo.interfaceFlag << flagInfo.liquidFlag << flagInfo.gasFlag << flagInfo.convertedFlag + << flagInfo.convertToGasFlag << flagInfo.convertToLiquidFlag << flagInfo.convertFromGasToInterfaceFlag + << flagInfo.keepInterfaceForWettingFlag << flagInfo.keepInterfaceForWettingFlag + << flagInfo.convertToInterfaceForInflowFlag << flagInfo.obstacleFlagMask << flagInfo.outflowFlagMask + << flagInfo.inflowFlagMask << flagInfo.freeSlipFlagMask; + + return buf; +} + +template< typename FlagField_T > +mpi::RecvBuffer& operator>>(mpi::RecvBuffer& buf, FlagInfo< FlagField_T >& flagInfo) +{ + buf >> flagInfo.interfaceFlag >> flagInfo.liquidFlag >> flagInfo.gasFlag >> flagInfo.convertedFlag >> + flagInfo.convertToGasFlag >> flagInfo.convertToLiquidFlag >> flagInfo.convertFromGasToInterfaceFlag >> + flagInfo.keepInterfaceForWettingFlag >> flagInfo.keepInterfaceForWettingFlag >> + flagInfo.convertToInterfaceForInflowFlag >> flagInfo.obstacleFlagMask >> flagInfo.outflowFlagMask >> + flagInfo.inflowFlagMask >> flagInfo.freeSlipFlagMask; + + return buf; +} + +} // namespace free_surface +} // namespace walberla + +#include "FlagInfo.impl.h" \ No newline at end of file diff --git a/src/lbm/free_surface/FlagInfo.impl.h b/src/lbm/free_surface/FlagInfo.impl.h new file mode 100644 index 000000000..574001ea4 --- /dev/null +++ b/src/lbm/free_surface/FlagInfo.impl.h @@ -0,0 +1,199 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file FlagInfo.impl.h +//! \ingroup free_surface +//! \author Martin Bauer +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Define and manage free surface flags (e.g. conversion flags). +// +//====================================================================================================================== + +#include "core/DataTypes.h" +#include "core/mpi/Broadcast.h" + +#include "field/FlagField.h" + +#include "FlagInfo.h" + +namespace walberla +{ +namespace free_surface +{ +template< typename FlagField_T > +FlagInfo< FlagField_T >::FlagInfo() + : interfaceFlag(0), liquidFlag(0), gasFlag(0), convertedFlag(0), convertToGasFlag(0), convertToLiquidFlag(0), + convertFromGasToInterfaceFlag(0), keepInterfaceForWettingFlag(0), convertToInterfaceForInflowFlag(0), + obstacleFlagMask(0), outflowFlagMask(0), inflowFlagMask(0), freeSlipFlagMask(0) +{} + +template< typename FlagField_T > +FlagInfo< FlagField_T >::FlagInfo(const FlagField_T* field, const Set< field::FlagUID >& obstacleIDs, + const Set< field::FlagUID >& outflowIDs, const Set< field::FlagUID >& inflowIDs, + const Set< field::FlagUID >& freeSlipIDs) + : interfaceFlag(field->getFlag(flagIDs::interfaceFlagID)), liquidFlag(field->getFlag(flagIDs::liquidFlagID)), + gasFlag(field->getFlag(flagIDs::gasFlagID)), convertedFlag(field->getFlag(flagIDs::convertedFlagID)), + convertToGasFlag(field->getFlag(flagIDs::convertToGasFlagID)), + convertToLiquidFlag(field->getFlag(flagIDs::convertToLiquidFlagID)), + convertFromGasToInterfaceFlag(field->getFlag(flagIDs::convertedFromGasToInterfaceFlagID)), + keepInterfaceForWettingFlag(field->getFlag(flagIDs::keepInterfaceForWettingFlagID)), + convertToInterfaceForInflowFlag(field->getFlag(flagIDs::convertToInterfaceForInflowFlagID)), + obstacleIDs_(obstacleIDs), outflowIDs_(outflowIDs), inflowIDs_(inflowIDs), freeSlipIDs_(freeSlipIDs) +{ + // create obstacleFlagMask from obstacleIDs using bitwise OR + obstacleFlagMask = 0; + for (auto obstacleID = obstacleIDs.begin(); obstacleID != obstacleIDs.end(); ++obstacleID) + { + obstacleFlagMask = flag_t(obstacleFlagMask | field->getFlag(*obstacleID)); + } + + // create outflowFlagMask from outflowIDs using bitwise OR + outflowFlagMask = 0; + for (auto outflowID = outflowIDs.begin(); outflowID != outflowIDs.end(); ++outflowID) + { + outflowFlagMask = flag_t(outflowFlagMask | field->getFlag(*outflowID)); + } + + // create inflowFlagMask from inflowIDs using bitwise OR + inflowFlagMask = 0; + for (auto inflowID = inflowIDs.begin(); inflowID != inflowIDs.end(); ++inflowID) + { + inflowFlagMask = flag_t(inflowFlagMask | field->getFlag(*inflowID)); + } + + // create freeSlipFlagMask from freeSlipIDs using bitwise OR + freeSlipFlagMask = 0; + for (auto freeSlipID = freeSlipIDs.begin(); freeSlipID != freeSlipIDs.end(); ++freeSlipID) + { + freeSlipFlagMask = flag_t(freeSlipFlagMask | field->getFlag(*freeSlipID)); + } +} + +template< typename FlagField_T > +FlagInfo< FlagField_T >::FlagInfo(const Set< field::FlagUID >& obstacleIDs, const Set< field::FlagUID >& outflowIDs, + const Set< field::FlagUID >& inflowIDs, const Set< field::FlagUID >& freeSlipIDs) + : obstacleIDs_(obstacleIDs), outflowIDs_(outflowIDs), inflowIDs_(inflowIDs), freeSlipIDs_(freeSlipIDs) +{ + // define flags + flag_t nextFreeBit = flag_t(0); + interfaceFlag = flag_t(flag_t(1) << nextFreeBit++); // outermost flag_t is necessary to avoid warning C4334 on MSVC + liquidFlag = flag_t(flag_t(1) << nextFreeBit++); + gasFlag = flag_t(flag_t(1) << nextFreeBit++); + convertedFlag = flag_t(flag_t(1) << nextFreeBit++); + convertToGasFlag = flag_t(flag_t(1) << nextFreeBit++); + convertToLiquidFlag = flag_t(flag_t(1) << nextFreeBit++); + convertFromGasToInterfaceFlag = flag_t(flag_t(1) << nextFreeBit++); + keepInterfaceForWettingFlag = flag_t(flag_t(1) << nextFreeBit++); + convertToInterfaceForInflowFlag = flag_t(flag_t(1) << nextFreeBit++); + + obstacleFlagMask = flag_t(0); + outflowFlagMask = flag_t(0); + inflowFlagMask = flag_t(0); + freeSlipFlagMask = flag_t(0); + + // define flags for obstacles, outflow, inflow and freeSlip + auto setUnion = obstacleIDs + outflowIDs + inflowIDs + freeSlipIDs; + for (auto id = setUnion.begin(); id != setUnion.end(); ++id) + { + if (obstacleIDs.contains(*id)) { obstacleFlagMask = obstacleFlagMask | flag_t((flag_t(1) << nextFreeBit)); } + + if (outflowIDs.contains(*id)) { outflowFlagMask = outflowFlagMask | flag_t((flag_t(1) << nextFreeBit)); } + + if (inflowIDs.contains(*id)) { inflowFlagMask = inflowFlagMask | flag_t((flag_t(1) << nextFreeBit)); } + + if (freeSlipIDs.contains(*id)) { freeSlipFlagMask = freeSlipFlagMask | flag_t((flag_t(1) << nextFreeBit)); } + + nextFreeBit++; + } +} + +template< typename FlagField_T > +void FlagInfo< FlagField_T >::registerFlags(FlagField_T* field, const Set< field::FlagUID >& obstacleIDs, + const Set< field::FlagUID >& outflowIDs, + const Set< field::FlagUID >& inflowIDs, + const Set< field::FlagUID >& freeSlipIDs) +{ + flag_t nextFreeBit = flag_t(0); + field->registerFlag(flagIDs::interfaceFlagID, nextFreeBit++); + field->registerFlag(flagIDs::liquidFlagID, nextFreeBit++); + field->registerFlag(flagIDs::gasFlagID, nextFreeBit++); + field->registerFlag(flagIDs::convertedFlagID, nextFreeBit++); + field->registerFlag(flagIDs::convertToGasFlagID, nextFreeBit++); + field->registerFlag(flagIDs::convertToLiquidFlagID, nextFreeBit++); + field->registerFlag(flagIDs::convertedFromGasToInterfaceFlagID, nextFreeBit++); + field->registerFlag(flagIDs::keepInterfaceForWettingFlagID, nextFreeBit++); + field->registerFlag(flagIDs::convertToInterfaceForInflowFlagID, nextFreeBit++); + + // extract flags + auto setUnion = obstacleIDs + outflowIDs + inflowIDs + freeSlipIDs; + for (auto id = setUnion.begin(); id != setUnion.end(); ++id) + { + field->registerFlag(*id, nextFreeBit++); + } +} + +template< typename FlagField_T > +void FlagInfo< FlagField_T >::registerFlags(FlagField_T* field) const +{ + registerFlags(field, obstacleIDs_, outflowIDs_, inflowIDs_, freeSlipIDs_); +} + +template< typename FlagField_T > +bool FlagInfo< FlagField_T >::isConsistentAcrossBlocksAndProcesses( + const std::weak_ptr< StructuredBlockStorage >& blockStoragePtr, ConstBlockDataID flagFieldID) const +{ + // check consistency across processes + FlagInfo rootFlagInfo = *this; + + // root broadcasts its FlagInfo to all other processes + mpi::broadcastObject(rootFlagInfo); + + // this process' FlagInfo is not identical to the one of root + if (rootFlagInfo != *this) { return false; } + + auto blockStorage = blockStoragePtr.lock(); + WALBERLA_CHECK_NOT_NULLPTR(blockStorage); + + // check consistency across blocks + for (auto blockIt = blockStorage->begin(); blockIt != blockStorage->end(); ++blockIt) + { + const FlagField_T* const flagField = blockIt->getData< const FlagField_T >(flagFieldID); + FlagInfo fi(flagField, obstacleIDs_, outflowIDs_, inflowIDs_, freeSlipIDs_); + if (fi != *this) { return false; } + } + + return true; +} + +template< typename FlagField_T > +bool FlagInfo< FlagField_T >::operator==(const FlagInfo& o) const +{ + return interfaceFlag == o.interfaceFlag && gasFlag == o.gasFlag && liquidFlag == o.liquidFlag && + convertToGasFlag == o.convertToGasFlag && convertToLiquidFlag == o.convertToLiquidFlag && + convertFromGasToInterfaceFlag == o.convertFromGasToInterfaceFlag && + keepInterfaceForWettingFlag == o.keepInterfaceForWettingFlag && + convertToInterfaceForInflowFlag == o.convertToInterfaceForInflowFlag && + obstacleFlagMask == o.obstacleFlagMask && outflowFlagMask == o.outflowFlagMask && + inflowFlagMask == o.inflowFlagMask && freeSlipFlagMask == o.freeSlipFlagMask; +} + +template< typename FlagField_T > +bool FlagInfo< FlagField_T >::operator!=(const FlagInfo& o) const +{ + return !(*this == o); +} + +} // namespace free_surface +} // namespace walberla \ No newline at end of file diff --git a/src/lbm/free_surface/InitFunctions.h b/src/lbm/free_surface/InitFunctions.h new file mode 100644 index 000000000..b98f55d54 --- /dev/null +++ b/src/lbm/free_surface/InitFunctions.h @@ -0,0 +1,229 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file InitFunctions.h +//! \ingroup free_surface +//! \author Matthias Markl <matthias.markl@fau.de> +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Initialization functions. +// +//====================================================================================================================== + +#pragma once + +#include "blockforest/StructuredBlockForest.h" + +#include "domain_decomposition/BlockDataID.h" + +#include <functional> + +#include "FlagInfo.h" +#include "InterfaceFromFillLevel.h" + +namespace walberla +{ +namespace free_surface +{ +/*********************************************************************************************************************** + * Initialize fill level with "value" in cells belonging to boundary and obstacles such that the bubble model does not + * detect obstacles as gas cells. + **********************************************************************************************************************/ +template< typename BoundaryHandling_T, typename Stencil_T, typename ScalarField_T > +void initFillLevelsInBoundaries(const std::weak_ptr< StructuredBlockForest >& blockForestPtr, + const ConstBlockDataID& handlingID, const BlockDataID& fillFieldID, + real_t value = real_c(1)) +{ + const auto blockForest = blockForestPtr.lock(); + WALBERLA_CHECK_NOT_NULLPTR(blockForest); + + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + ScalarField_T* const fillField = blockIt->getData< ScalarField_T >(fillFieldID); + const BoundaryHandling_T* const handling = blockIt->getData< const BoundaryHandling_T >(handlingID); + + // set fill level to "value" in every cell belonging to boundary + WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ(fillField, { + if (handling->isBoundary(x, y, z)) { fillField->get(x, y, z) = value; } + }) // WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ + } +} + +/*********************************************************************************************************************** + * Clear and initialize flags in every cell according to the fill level. + **********************************************************************************************************************/ +template< typename BoundaryHandling_T, typename Stencil_T, typename FlagField_T, typename ScalarField_T > +void initFlagsFromFillLevels(const std::weak_ptr< StructuredBlockForest >& blockForestPtr, + const FlagInfo< FlagField_T >& flagInfo, const BlockDataID& handlingID, + const ConstBlockDataID& fillFieldID) +{ + const auto blockForest = blockForestPtr.lock(); + WALBERLA_CHECK_NOT_NULLPTR(blockForest); + + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + const ScalarField_T* const fillField = blockIt->getData< const ScalarField_T >(fillFieldID); + BoundaryHandling_T* const handling = blockIt->getData< BoundaryHandling_T >(handlingID); + + // clear all flags in the boundary handling + handling->removeFlag(flagInfo.gasFlag); + handling->removeFlag(flagInfo.liquidFlag); + handling->removeFlag(flagInfo.interfaceFlag); + + WALBERLA_FOR_ALL_CELLS(fillFieldIt, fillField, { + // set flags only in non-boundary and non-obstacle cells + if (!handling->isBoundary(fillFieldIt.x(), fillFieldIt.y(), fillFieldIt.z())) + { + if (*fillFieldIt <= real_c(0)) + { + // set gas flag + handling->forceFlag(flagInfo.gasFlag, fillFieldIt.x(), fillFieldIt.y(), fillFieldIt.z()); + } + else + { + if (*fillFieldIt < real_c(1.0)) + { + // set interface flag + handling->forceFlag(flagInfo.interfaceFlag, fillFieldIt.x(), fillFieldIt.y(), fillFieldIt.z()); + } + else + { + // check if the cell is an interface cell due to direct neighboring gas cells + if (isInterfaceFromFillLevel< Stencil_T >(fillFieldIt)) + { + // set interface flag + handling->forceFlag(flagInfo.interfaceFlag, fillFieldIt.x(), fillFieldIt.y(), fillFieldIt.z()); + } + else + { + // set liquid flag + handling->forceFlag(flagInfo.liquidFlag, fillFieldIt.x(), fillFieldIt.y(), fillFieldIt.z()); + } + } + } + } + }) // WALBERLA_FOR_ALL_CELLS + } +} + +/*********************************************************************************************************************** + * Initialize the hydrostatic pressure in the direction in which a force is acting in ALL cells (regardless of a cell's + * flag). The velocity remains unchanged. + * + * The force vector must have only one component, i.e., the direction of the force can only be in x-, y- or z-axis. + * The variable fluidHeight determines the height at which the density is equal to reference density (=1). + **********************************************************************************************************************/ +template< typename PdfField_T > +void initHydrostaticPressure(const std::weak_ptr< StructuredBlockForest >& blockForestPtr, + const BlockDataID& pdfFieldID, const Vector3< real_t >& force, real_t fluidHeight) +{ + // count number of non-zero components of the force vector + uint_t numForceComponents = uint_c(0); + if (!realIsEqual(force[0], real_c(0), real_c(1e-14))) { ++numForceComponents; } + if (!realIsEqual(force[1], real_c(0), real_c(1e-14))) { ++numForceComponents; } + if (!realIsEqual(force[2], real_c(0), real_c(1e-14))) { ++numForceComponents; } + + WALBERLA_CHECK_EQUAL(numForceComponents, uint_c(1), + "The current implementation of the hydrostatic pressure initialization does not support " + "forces that have none or multiple components, i. e., a force that points in a direction other " + "than the x-, y- or z-axis."); + + const auto blockForest = blockForestPtr.lock(); + WALBERLA_CHECK_NOT_NULLPTR(blockForest); + + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + PdfField_T* const pdfField = blockIt->getData< PdfField_T >(pdfFieldID); + + CellInterval local = pdfField->xyzSizeWithGhostLayer(); // block-, i.e., process-local cell interval + + for (auto cellIt = local.begin(); cellIt != local.end(); ++cellIt) + { + // transform the block-local coordinate to global coordinates + Cell global; + blockForest->transformBlockLocalToGlobalCell(global, *blockIt, *cellIt); + + // get the current global coordinate, i.e., height of the fluid in the relevant direction + cell_idx_t coordinate = cell_idx_c(0); + real_t forceComponent = real_c(0); + if (!realIsEqual(force[0], real_c(0), real_c(1e-14))) + { + coordinate = global.x(); + forceComponent = force[0]; + } + else + { + if (!realIsEqual(force[1], real_c(0), real_c(1e-14))) + { + coordinate = global.y(); + forceComponent = force[1]; + } + else + { + if (!realIsEqual(force[2], real_c(0), real_c(1e-14))) + { + coordinate = global.z(); + forceComponent = force[2]; + } + else + { + WALBERLA_ABORT( + "The current implementation of the hydrostatic pressure initialization does not support " + "forces that have none or multiple components, i. e., a force that points in a direction other " + "than the x-, y- or z-axis.") + } + } + } + + // initialize the (hydrostatic) pressure, i.e., LBM density + // Bernoulli: p = p0 + density * gravity * height + // => LBM (density=1): rho = rho0 + gravity * height = 1 + 1/cs^2 * g * h = 1 + 3 * g * h + // shift global cell by 0.5 since density is set for cell center + const real_t rho = + real_c(1) + real_c(3) * forceComponent * (real_c(coordinate) + real_c(0.5) - std::ceil(fluidHeight)); + + const Vector3< real_t > velocity = pdfField->getVelocity(*cellIt); + + pdfField->setDensityAndVelocity(*cellIt, velocity, rho); + } + } +} + +/*********************************************************************************************************************** + * Set density in non-liquid and non-interface cells to 1. + **********************************************************************************************************************/ +template< typename FlagField_T, typename PdfField_T > +void setDensityInNonFluidCellsToOne(const std::weak_ptr< StructuredBlockForest >& blockForestPtr, + const FlagInfo< FlagField_T >& flagInfo, const ConstBlockDataID& flagFieldID, + const BlockDataID& pdfFieldID) +{ + const auto blockForest = blockForestPtr.lock(); + WALBERLA_CHECK_NOT_NULLPTR(blockForest); + + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + PdfField_T* const pdfField = blockIt->getData< PdfField_T >(pdfFieldID); + const FlagField_T* const flagField = blockIt->getData< const FlagField_T >(flagFieldID); + + WALBERLA_FOR_ALL_CELLS(pdfFieldIt, pdfField, flagFieldIt, flagField, { + if (!flagInfo.isLiquid(*flagFieldIt) && !flagInfo.isInterface(*flagFieldIt)) + { + // set density in gas cells to 1 + pdfField->setDensityAndVelocity(pdfFieldIt.cell(), Vector3< real_t >(real_c(0)), real_c(1)); + } + }) // WALBERLA_FOR_ALL_CELLS + } +} +} // namespace free_surface +} // namespace walberla diff --git a/src/lbm/free_surface/InterfaceFromFillLevel.h b/src/lbm/free_surface/InterfaceFromFillLevel.h new file mode 100644 index 000000000..ef113e327 --- /dev/null +++ b/src/lbm/free_surface/InterfaceFromFillLevel.h @@ -0,0 +1,78 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file InterfaceFromFillLevel.h +//! \ingroup free_surface +//! \author Matthias Markl <matthias.markl@fau.de> +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Check whether a cell should be an interface cell according to its properties. +// +//====================================================================================================================== + +#pragma once + +#include "core/cell/Cell.h" + +#include <type_traits> + +namespace walberla +{ +namespace free_surface +{ +/*********************************************************************************************************************** + * Check whether a cell is an interface cell with respect to its fill level and its direct neighborhood (liquid cells + * can not have gas cell in their direct neighborhood; therefore, the liquid cell is forced to be an interface cell). + **********************************************************************************************************************/ +template< typename Stencil_T, typename ScalarField_T > +inline bool isInterfaceFromFillLevel(const ScalarField_T& fillField, cell_idx_t x, cell_idx_t y, cell_idx_t z) +{ + WALBERLA_ASSERT(std::is_floating_point< typename ScalarField_T::value_type >::value, + "Fill level field must contain floating point values.") + + real_t fillLevel = fillField.get(x, y, z); + + // this cell is regular gas cell + if (fillLevel <= real_c(0.0)) { return false; } + + // this cell is regular interface cell + if (fillLevel < real_c(1.0)) { return true; } + + // check this cell's direct neighborhood for gas cells (a liquid cell can not be direct neighbor to a gas cell) + for (auto d = Stencil_T::beginNoCenter(); d != Stencil_T::end(); ++d) + { + // this cell has a gas cell in its direct neighborhood; it therefore must not be a liquid cell but an interface + // cell although its fill level is 1.0 + if (fillField.get(x + d.cx(), y + d.cy(), z + d.cz()) <= real_c(0.0)) { return true; } + } + + // this cell is a regular fluid cell + return false; +} + +template< typename Stencil_T, typename ScalarFieldIt_T > +inline bool isInterfaceFromFillLevel(const ScalarFieldIt_T& fillFieldIt) +{ + return isInterfaceFromFillLevel< Stencil_T, typename ScalarFieldIt_T::FieldType >( + *(fillFieldIt.getField()), fillFieldIt.x(), fillFieldIt.y(), fillFieldIt.z()); +} + +template< typename Stencil_T, typename ScalarField > +inline bool isInterfaceFromFillLevel(const ScalarField& fillField, const Cell& cell) +{ + return isInterfaceFromFillLevel< Stencil_T >(fillField, cell.x(), cell.y(), cell.z()); +} + +} // namespace free_surface +} // namespace walberla diff --git a/src/lbm/free_surface/LoadBalancing.h b/src/lbm/free_surface/LoadBalancing.h new file mode 100644 index 000000000..c9df54478 --- /dev/null +++ b/src/lbm/free_surface/LoadBalancing.h @@ -0,0 +1,345 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file LoadBalancing.h +//! \ingroup free_surface +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Free surface-specific functionality for load balancing. +// +//====================================================================================================================== + +#include "blockforest/BlockForest.h" +#include "blockforest/SetupBlockForest.h" +#include "blockforest/StructuredBlockForest.h" +#include "blockforest/loadbalancing/DynamicCurve.h" +#include "blockforest/loadbalancing/StaticCurve.h" + +#include "core/logging/Logging.h" +#include "core/math/AABB.h" +#include "core/math/DistributedSample.h" +#include "core/math/Vector3.h" +#include "core/mpi/MPIManager.h" + +#include "lbm/blockforest/communication/SimpleCommunication.h" +#include "lbm/free_surface/BlockStateDetectorSweep.h" +#include "lbm/free_surface/bubble_model/BubbleModel.h" + +#include <algorithm> +#include <numeric> + +namespace walberla +{ +namespace free_surface +{ + +template< typename FlagField_T > +class ProcessLoadEvaluator; + +/*********************************************************************************************************************** + * Create non-uniform block forest to be used for load balancing. + **********************************************************************************************************************/ +std::shared_ptr< StructuredBlockForest > createNonUniformBlockForest(const Vector3< uint_t >& domainSize, + const Vector3< uint_t >& cellsPerBlock, + const Vector3< uint_t >& numBlocks, + const Vector3< bool >& periodicity) +{ + WALBERLA_CHECK_EQUAL(domainSize[0], cellsPerBlock[0] * numBlocks[0], + "The domain size is not divisible by the specified \"cellsPerBlock\" in x-direction."); + WALBERLA_CHECK_EQUAL(domainSize[1], cellsPerBlock[1] * numBlocks[1], + "The domain size is not divisible by the specified \"cellsPerBlock\" in y-direction."); + WALBERLA_CHECK_EQUAL(domainSize[2], cellsPerBlock[2] * numBlocks[2], + "The domain size is not divisible by the specified \"cellsPerBlock\" in z-direction."); + + // create SetupBlockForest for allowing load balancing + SetupBlockForest setupBlockForest; + + AABB domainAABB(real_c(0), real_c(0), real_c(0), real_c(domainSize[0]), real_c(domainSize[1]), + real_c(domainSize[2])); + + setupBlockForest.init(domainAABB, numBlocks[0], numBlocks[1], numBlocks[2], periodicity[0], periodicity[1], + periodicity[2]); + + // compute initial process distribution + setupBlockForest.balanceLoad(blockforest::StaticLevelwiseCurveBalance(true), + uint_c(MPIManager::instance()->numProcesses())); + + WALBERLA_LOG_INFO_ON_ROOT(setupBlockForest); + + // define MPI communicator + if (!MPIManager::instance()->rankValid()) { MPIManager::instance()->useWorldComm(); } + + // create BlockForest (will be encapsulated in StructuredBlockForest) + const std::shared_ptr< BlockForest > blockForest = + std::make_shared< BlockForest >(uint_c(MPIManager::instance()->rank()), setupBlockForest, false); + + // create StructuredBlockForest + std::shared_ptr< StructuredBlockForest > structuredBlockForest = + std::make_shared< StructuredBlockForest >(blockForest, cellsPerBlock[0], cellsPerBlock[1], cellsPerBlock[2]); + structuredBlockForest->createCellBoundingBoxes(); + + return structuredBlockForest; +} + +/*********************************************************************************************************************** + * Example for a load balancing implementation. + * + * IMPORTANT REMARK: The following implementation should be considered a demonstrator based on best-practices. That is, + * it was not thoroughly benchmarked and does not guarantee a performance-optimal load balancing. + **********************************************************************************************************************/ +template< typename FlagField_T, typename CommunicationStencil_T, typename LatticeModelStencil_T > +class LoadBalancer +{ + public: + LoadBalancer(const std::shared_ptr< StructuredBlockForest >& blockForestPtr, + const blockforest::SimpleCommunication< CommunicationStencil_T >& communication, + const blockforest::SimpleCommunication< LatticeModelStencil_T >& pdfCommunication, + const std::shared_ptr< bubble_model::BubbleModelBase >& bubbleModel, uint_t blockWeightFullFreeSurface, + uint_t blockWeightOnlyLBM, uint_t blockWeightOnlyGasAndBoundary, uint_t frequency, + bool printStatistics = false) + : blockForest_(blockForestPtr), communication_(communication), pdfCommunication_(pdfCommunication), + bubbleModel_(bubbleModel), blockWeightFullFreeSurface_(blockWeightFullFreeSurface), + blockWeightOnlyLBM_(blockWeightOnlyLBM), blockWeightOnlyGasAndBoundary_(blockWeightOnlyGasAndBoundary), + frequency_(frequency), printStatistics_(printStatistics), executionCounter_(uint_c(0)), + evaluator_(ProcessLoadEvaluator< FlagField_T >(blockForest_, blockWeightFullFreeSurface_, blockWeightOnlyLBM_, + blockWeightOnlyGasAndBoundary_, uint_c(1))) + { + BlockForest& blockForest = blockForest_->getBlockForest(); + + // refinement is not implemented in FSLBM such that this can be set to false + blockForest.recalculateBlockLevelsInRefresh(false); + + // rebalancing of blocks must be forced here, as it would normally be done when refinement levels change + blockForest.alwaysRebalanceInRefresh(true); + + // depth of levels is not changing and must therefore not be communicated + blockForest.allowRefreshChangingDepth(false); + + // refinement is not implemented in FSLBM such that this can be set to false + blockForest.allowMultipleRefreshCycles(false); + + // leave algorithm when load balancing has been performed + blockForest.checkForEarlyOutAfterLoadBalancing(true); + + // use PhantomWeight as defined below + blockForest.setRefreshPhantomBlockDataAssignmentFunction(blockWeightAssignment); + blockForest.setRefreshPhantomBlockDataPackFunction(phantomWeightsPack); + blockForest.setRefreshPhantomBlockDataUnpackFunction(phantomWeightsUnpack); + + // assign load balancing function + blockForest.setRefreshPhantomBlockMigrationPreparationFunction( + blockforest::DynamicCurveBalance< PhantomWeight >(true, true)); + } + + void operator()() + { + if (frequency_ == uint_c(0)) { return; } + + // only balance load in given frequencies + if (executionCounter_ % frequency_ == uint_c(0)) + { + BlockForest& blockForest = blockForest_->getBlockForest(); + + // balance load by updating the blockForest + const uint_t modificationStamp = blockForest.getModificationStamp(); + blockForest.refresh(); + + const uint_t newModificationStamp = blockForest.getModificationStamp(); + + if (newModificationStamp != modificationStamp) + { + // communicate all fields + communication_(); + pdfCommunication_(); + bubbleModel_->update(); + + if (printStatistics_) { evaluator_(); } + + if (blockForest.getNumberOfBlocks() == uint_c(0)) + { + WALBERLA_ABORT( + "Load balancing lead to a situation where there is a process with no blocks. This is " + "not supported yet. This can be avoided by either using smaller blocks or, equivalently, more blocks " + "per process."); + } + } + } + ++executionCounter_; + } + + private: + std::shared_ptr< StructuredBlockForest > blockForest_; + blockforest::SimpleCommunication< CommunicationStencil_T > communication_; + blockforest::SimpleCommunication< LatticeModelStencil_T > pdfCommunication_; + std::shared_ptr< bubble_model::BubbleModelBase > bubbleModel_; + + uint_t blockWeightFullFreeSurface_; // empirical choice, not thoroughly benchmarked: 50 + uint_t blockWeightOnlyLBM_; // empirical choice, not thoroughly benchmarked: 10 + uint_t blockWeightOnlyGasAndBoundary_; // empirical choice, not thoroughly benchmarked: 5 + + uint_t frequency_; + bool printStatistics_; + + uint_t executionCounter_; + + ProcessLoadEvaluator< FlagField_T > evaluator_; + + class PhantomWeight // used as a 'PhantomBlockForest::PhantomBlockDataAssignmentFunction' + { + public: + using weight_t = uint_t; + PhantomWeight(const weight_t _weight) : weight_(_weight) {} + weight_t weight() const { return weight_; } + + private: + weight_t weight_; + }; // class PhantomWeight + + std::function< void(mpi::SendBuffer& buffer, const PhantomBlock& block) > phantomWeightsPack = + [](mpi::SendBuffer& buffer, const PhantomBlock& block) { buffer << block.getData< PhantomWeight >().weight(); }; + + std::function< void(mpi::RecvBuffer& buffer, const PhantomBlock&, walberla::any& data) > phantomWeightsUnpack = + [](mpi::RecvBuffer& buffer, const PhantomBlock&, walberla::any& data) { + typename PhantomWeight::weight_t w; + buffer >> w; + data = PhantomWeight(w); + }; + + std::function< void(std::vector< std::pair< const PhantomBlock*, walberla::any > >& blockData, + const PhantomBlockForest&) > + blockWeightAssignment = + [this](std::vector< std::pair< const PhantomBlock*, walberla::any > >& blockData, const PhantomBlockForest&) { + for (auto it = blockData.begin(); it != blockData.end(); ++it) + { + if (it->first->getState().contains(BlockStateDetectorSweep< FlagField_T >::fullFreeSurface)) + { + it->second = PhantomWeight(blockWeightFullFreeSurface_); + } + else + { + if (it->first->getState().contains(BlockStateDetectorSweep< FlagField_T >::onlyLBM)) + { + it->second = PhantomWeight(blockWeightOnlyLBM_); + } + else + { + if (it->first->getState().contains(BlockStateDetectorSweep< FlagField_T >::onlyGasAndBoundary)) + { + it->second = PhantomWeight(blockWeightOnlyGasAndBoundary_); + } + else { WALBERLA_ABORT("Unknown block state"); } + } + } + } + }; + +}; // class LoadBalancer + +/*********************************************************************************************************************** + * Evaluates and prints statistics about the current load distribution situation: + * - Average weight per process + * - Maximum weight per process + * - Minimum weight per process + **********************************************************************************************************************/ +template< typename FlagField_T > +class ProcessLoadEvaluator +{ + public: + ProcessLoadEvaluator(const std::weak_ptr< const StructuredBlockForest >& blockForest, + uint_t blockWeightFullFreeSurface, uint_t blockWeightOnlyLBM, + uint_t blockWeightOnlyGasAndBoundary, uint_t frequency) + : blockForest_(blockForest), blockWeightFullFreeSurface_(blockWeightFullFreeSurface), + blockWeightOnlyLBM_(blockWeightOnlyLBM), blockWeightOnlyGasAndBoundary_(blockWeightOnlyGasAndBoundary), + frequency_(frequency), executionCounter_(uint_c(0)) + {} + + void operator()() + { + if (frequency_ == uint_c(0)) { return; } + + ++executionCounter_; + + // only evaluate in given intervals + if (executionCounter_ % frequency_ != uint_c(0) && executionCounter_ != uint_c(1)) { return; } + + std::vector< real_t > weightSum = computeWeightSumPerProcess(); + + print(weightSum); + } + + std::vector< real_t > computeWeightSumPerProcess() + { + auto blockForest = blockForest_.lock(); + WALBERLA_CHECK_NOT_NULLPTR(blockForest); + + std::vector< real_t > weightSum(uint_c(MPIManager::instance()->numProcesses()), real_c(0)); + + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + if (blockForest->blockExistsLocally(blockIt->getId())) + { + if (blockIt->getState().contains(BlockStateDetectorSweep< FlagField_T >::fullFreeSurface)) + { + weightSum[blockForest->getProcessRank(blockIt->getId())] += real_c(blockWeightFullFreeSurface_); + } + else + { + if (blockIt->getState().contains(BlockStateDetectorSweep< FlagField_T >::onlyLBM)) + { + weightSum[blockForest->getProcessRank(blockIt->getId())] += real_c(blockWeightOnlyLBM_); + } + else + { + if (blockIt->getState().contains(BlockStateDetectorSweep< FlagField_T >::onlyGasAndBoundary)) + { + weightSum[blockForest->getProcessRank(blockIt->getId())] += real_c(blockWeightOnlyGasAndBoundary_); + } + } + } + } + } + + mpi::reduceInplace< real_t >(weightSum, mpi::SUM, 0); + + return weightSum; + } + + void print(const std::vector< real_t >& weightSum) + { + WALBERLA_ROOT_SECTION() + { + const std::vector< real_t >::const_iterator max = std::max_element(weightSum.cbegin(), weightSum.end()); + const std::vector< real_t >::const_iterator min = std::min_element(weightSum.cbegin(), weightSum.end()); + const real_t sum = std::accumulate(weightSum.cbegin(), weightSum.end(), real_c(0)); + const real_t avg = sum / real_c(MPIManager::instance()->numProcesses()); + + WALBERLA_LOG_INFO("Load balancing:"); + WALBERLA_LOG_INFO("\t Average weight per process " << avg); + WALBERLA_LOG_INFO("\t Maximum weight per process " << *max); + WALBERLA_LOG_INFO("\t Minimum weight per process " << *min); + } + } + + private: + std::weak_ptr< const StructuredBlockForest > blockForest_; + uint_t blockWeightFullFreeSurface_; + uint_t blockWeightOnlyLBM_; + uint_t blockWeightOnlyGasAndBoundary_; + + uint_t frequency_; + uint_t executionCounter_; +}; // class ProcessLoadEvaluator + +} // namespace free_surface +} // namespace walberla diff --git a/src/lbm/free_surface/MaxVelocityComputer.h b/src/lbm/free_surface/MaxVelocityComputer.h new file mode 100644 index 000000000..a3e2b0cb2 --- /dev/null +++ b/src/lbm/free_surface/MaxVelocityComputer.h @@ -0,0 +1,110 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file MaxVelocityComputer.h +//! \ingroup free_surface +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Compute the maximum velocity in the system. +// +//====================================================================================================================== + +#include "blockforest/Initialization.h" + +#include "core/Environment.h" + +namespace walberla +{ +namespace free_surface +{ +template< typename FreeSurfaceBoundaryHandling_T, typename PdfField_T, typename FlagField_T > +class MaxVelocityComputer +{ + public: + MaxVelocityComputer(const std::weak_ptr< const StructuredBlockForest >& blockForest, + const std::weak_ptr< const FreeSurfaceBoundaryHandling_T >& freeSurfaceBoundaryHandling, + const ConstBlockDataID& pdfFieldID, uint_t frequency, + const std::shared_ptr< Vector3< real_t > >& maxVelocity) + : blockForest_(blockForest), freeSurfaceBoundaryHandling_(freeSurfaceBoundaryHandling), pdfFieldID_(pdfFieldID), + maxVelocity_(maxVelocity), frequency_(frequency), executionCounter_(uint_c(0)) + {} + + void operator()() + { + if (frequency_ == uint_c(0)) { return; } + + auto blockForest = blockForest_.lock(); + WALBERLA_CHECK_NOT_NULLPTR(blockForest); + + auto freeSurfaceBoundaryHandling = freeSurfaceBoundaryHandling_.lock(); + WALBERLA_CHECK_NOT_NULLPTR(freeSurfaceBoundaryHandling); + + if (executionCounter_ == uint_c(0)) { getMaxVelocity(blockForest, freeSurfaceBoundaryHandling); } + else + { + // only evaluate in given frequencies + if (executionCounter_ % frequency_ == uint_c(0)) { getMaxVelocity(blockForest, freeSurfaceBoundaryHandling); } + } + + ++executionCounter_; + } + + void getMaxVelocity(const std::shared_ptr< const StructuredBlockForest >& blockForest, + const std::shared_ptr< const FreeSurfaceBoundaryHandling_T >& freeSurfaceBoundaryHandling) + { + const BlockDataID flagFieldID = freeSurfaceBoundaryHandling->getFlagFieldID(); + const typename FreeSurfaceBoundaryHandling_T::FlagInfo_T& flagInfo = freeSurfaceBoundaryHandling->getFlagInfo(); + + Vector3< real_t > maxVelocity = Vector3< real_t >(real_c(0)); + + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + const FlagField_T* const flagField = blockIt->template getData< const FlagField_T >(flagFieldID); + const PdfField_T* const pdfField = blockIt->template getData< const PdfField_T >(pdfFieldID_); + + WALBERLA_FOR_ALL_CELLS_OMP(flagFieldIt, flagField, pdfFieldIt, pdfField, + omp parallel for schedule(static) reduction(max:maxVelocity[0]) reduction(max:maxVelocity[1]) reduction(max:maxVelocity[2]), + { + if (flagInfo.isLiquid(flagFieldIt) || flagInfo.isInterface(flagFieldIt)) + { + const Vector3< real_t > velocity = pdfField->getVelocity(pdfFieldIt.cell()); + + if (velocity[0] > maxVelocity[0]) { maxVelocity[0] = velocity[0]; } + if (velocity[1] > maxVelocity[1]) { maxVelocity[1] = velocity[1]; } + if (velocity[2] > maxVelocity[2]) { maxVelocity[2] = velocity[2]; } + } + }) // WALBERLA_FOR_ALL_CELLS_OMP + } + + mpi::allReduceInplace< real_t >(maxVelocity[0], mpi::MAX); + mpi::allReduceInplace< real_t >(maxVelocity[1], mpi::MAX); + mpi::allReduceInplace< real_t >(maxVelocity[2], mpi::MAX); + + *maxVelocity_ = maxVelocity; + }; + + private: + std::weak_ptr< const StructuredBlockForest > blockForest_; + std::weak_ptr< const FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling_; + + const ConstBlockDataID pdfFieldID_; + + std::shared_ptr< Vector3< real_t > > maxVelocity_; + + uint_t frequency_; + uint_t executionCounter_; +}; // class MaxVelocityComputer + +} // namespace free_surface +} // namespace walberla \ No newline at end of file diff --git a/src/lbm/free_surface/SurfaceMeshWriter.h b/src/lbm/free_surface/SurfaceMeshWriter.h new file mode 100644 index 000000000..334f9d714 --- /dev/null +++ b/src/lbm/free_surface/SurfaceMeshWriter.h @@ -0,0 +1,176 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file SurfaceMeshWriter.h +//! \ingroup free_surface +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Free surface-specific class for writing the free surface as triangle mesh. +// +//====================================================================================================================== + +#include "blockforest/StructuredBlockForest.h" + +#include "core/Filesystem.h" + +#include "field/AddToStorage.h" + +#include "geometry/mesh/TriangleMeshIO.h" + +#include "postprocessing/FieldToSurfaceMesh.h" + +#include "timeloop/SweepTimeloop.h" + +namespace walberla +{ +namespace free_surface +{ +namespace abortIfNullptr +{ + +// helper function to check validity of std::weak_ptr in constructors' initializer list; WALBERLA_CHECK_NOT_NULLPTR() +// does not work there, because the macro terminates with ";" +template< typename T > +void abortIfNullptr(const std::weak_ptr< T >& weakPointer) +{ + if (weakPointer.lock() == nullptr) { WALBERLA_ABORT("Weak pointer has expired."); } +} +} // namespace abortIfNullptr + +/*********************************************************************************************************************** + * Write free surface as triangle mesh. + * + * Internally, a clone of the fill level field is stored and all cells not marked as liquidInterfaceGasFlagIDSet are set + * to "obstacleFillLevel" in the cloned field. This is done to avoid writing e.g. obstacle cells that were possibly + * assigned a fill level of 1 to not make them detect as gas cells in the bubble model. + **********************************************************************************************************************/ +template< typename ScalarField_T, typename FlagField_T > +class SurfaceMeshWriter +{ + public: + SurfaceMeshWriter(const std::weak_ptr< StructuredBlockForest >& blockForest, const ConstBlockDataID& fillFieldID, + const ConstBlockDataID& flagFieldID, const Set< FlagUID >& liquidInterfaceGasFlagIDSet, + real_t obstacleFillLevel, uint_t writeFrequency, const std::string& baseFolder) + : blockForest_(blockForest), fillFieldID_(fillFieldID), flagFieldID_(flagFieldID), + liquidInterfaceGasFlagIDSet_(liquidInterfaceGasFlagIDSet), obstacleFillLevel_(obstacleFillLevel), + writeFrequency_(writeFrequency), baseFolder_(baseFolder), executionCounter_(uint_c(0)), + fillFieldCloneID_( + (abortIfNullptr::abortIfNullptr(blockForest), + field::addCloneToStorage< ScalarField_T >(blockForest_.lock(), fillFieldID_, "Fill level field clone"))) + {} + + // config block must be named "MeshOutputParameters" + SurfaceMeshWriter(const std::weak_ptr< StructuredBlockForest >& blockForest, const ConstBlockDataID& fillFieldID, + const ConstBlockDataID& flagFieldID, const Set< FlagUID >& liquidInterfaceGasFlagIDSet, + real_t obstacleFillLevel, const std::weak_ptr< Config >& config) + : SurfaceMeshWriter( + blockForest, fillFieldID, flagFieldID, liquidInterfaceGasFlagIDSet, obstacleFillLevel, + (abortIfNullptr::abortIfNullptr(config), + config.lock()->getOneBlock("MeshOutputParameters").getParameter< uint_t >("writeFrequency")), + (abortIfNullptr::abortIfNullptr(config), + config.lock()->getOneBlock("MeshOutputParameters").getParameter< std::string >("baseFolder"))) + {} + + void operator()() + { + if (writeFrequency_ == uint_c(0)) { return; } + + if (executionCounter_ == uint_c(0)) + { + createBaseFolder(); + writeMesh(); + } + else { writeMesh(); } + + ++executionCounter_; + } + + private: + void createBaseFolder() const + { + WALBERLA_ROOT_SECTION() + { + const filesystem::path basePath(baseFolder_); + if (filesystem::exists(basePath)) { filesystem::remove_all(basePath); } + filesystem::create_directories(basePath); + } + WALBERLA_MPI_BARRIER(); + } + + void writeMesh() + { + // only write mesh in given frequency + if (executionCounter_ % writeFrequency_ != uint_c(0)) { return; } + + // rank=0 is just an arbitrary choice here + const int targetRank = 0; + + auto blockForest = blockForest_.lock(); + WALBERLA_CHECK_NOT_NULLPTR(blockForest); + + // update clone of fill level field and set fill level of all non-liquid, -interface, or -gas cells to zero + updateFillFieldClone(blockForest); + + const auto surfaceMesh = postprocessing::realFieldToSurfaceMesh< ScalarField_T >( + blockForest, fillFieldCloneID_, real_c(0.5), uint_c(0), true, targetRank, MPI_COMM_WORLD); + + WALBERLA_EXCLUSIVE_WORLD_SECTION(targetRank) + { + geometry::writeMesh(baseFolder_ + "/" + "simulation_step_" + std::to_string(executionCounter_) + ".obj", + *surfaceMesh); + } + } + + // update clone of fill level field and set fill level of all non-liquid, -interface, or -gas cells to zero; + // explicitly use shared_ptr instead of weak_ptr to avoid checking the latter's validity (is done in writeMesh() + // already) + void updateFillFieldClone(const shared_ptr< StructuredBlockForest >& blockForest) + { + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + ScalarField_T* const fillFieldClone = blockIt->template getData< ScalarField_T >(fillFieldCloneID_); + const ScalarField_T* const fillField = blockIt->template getData< const ScalarField_T >(fillFieldID_); + const FlagField_T* const flagField = blockIt->template getData< const FlagField_T >(flagFieldID_); + + const auto liquidInterfaceGasFlagMask = flagField->getMask(liquidInterfaceGasFlagIDSet_); + + WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ(fillFieldClone, fillField->nrOfGhostLayers(), { + const typename ScalarField_T::Ptr fillFieldClonePtr(*fillFieldClone, x, y, z); + const typename ScalarField_T::ConstPtr fillFieldPtr(*fillField, x, y, z); + const typename FlagField_T::ConstPtr flagFieldPtr(*flagField, x, y, z); + + // set fill level to zero in every non-liquid, -interface, or -gas cell + if (!isPartOfMaskSet(flagFieldPtr, liquidInterfaceGasFlagMask)) { *fillFieldClonePtr = obstacleFillLevel_; } + else + { + // copy fill level from fill level field + *fillFieldClonePtr = *fillFieldPtr; + } + }) // WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ + } + } + + std::weak_ptr< StructuredBlockForest > blockForest_; + ConstBlockDataID fillFieldID_; + ConstBlockDataID flagFieldID_; + Set< FlagUID > liquidInterfaceGasFlagIDSet_; + real_t obstacleFillLevel_; + uint_t writeFrequency_; + std::string baseFolder_; + uint_t executionCounter_; + + BlockDataID fillFieldCloneID_; +}; // class SurfaceMeshWriter +} // namespace free_surface +} // namespace walberla \ No newline at end of file diff --git a/src/lbm/free_surface/TotalMassComputer.h b/src/lbm/free_surface/TotalMassComputer.h new file mode 100644 index 000000000..192ec8755 --- /dev/null +++ b/src/lbm/free_surface/TotalMassComputer.h @@ -0,0 +1,144 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file TotalMassComputer.h +//! \ingroup free_surface +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Compute the total mass of the system (including mass from the excessMassField). +// +//====================================================================================================================== + +#include "blockforest/Initialization.h" + +#include "core/Environment.h" +#include "core/StringUtility.h" + +#include "lbm/free_surface/dynamics/SurfaceDynamicsHandler.h" +#include "lbm/free_surface/surface_geometry/SurfaceGeometryHandler.h" +#include "lbm/free_surface/surface_geometry/Utility.h" +#include "lbm/lattice_model/D3Q19.h" + +namespace walberla +{ +namespace free_surface +{ +template< typename FreeSurfaceBoundaryHandling_T, typename PdfField_T, typename FlagField_T, typename ScalarField_T > +class TotalMassComputer +{ + public: + TotalMassComputer(const std::weak_ptr< const StructuredBlockForest >& blockForest, + const std::weak_ptr< const FreeSurfaceBoundaryHandling_T >& freeSurfaceBoundaryHandling, + const ConstBlockDataID& pdfFieldID, const ConstBlockDataID& fillFieldID, uint_t frequency, + const std::shared_ptr< real_t >& totalMass) + : blockForest_(blockForest), freeSurfaceBoundaryHandling_(freeSurfaceBoundaryHandling), pdfFieldID_(pdfFieldID), + fillFieldID_(fillFieldID), totalMass_(totalMass), frequency_(frequency), executionCounter_(uint_c(0)) + {} + + TotalMassComputer(const std::weak_ptr< const StructuredBlockForest >& blockForest, + const std::weak_ptr< const FreeSurfaceBoundaryHandling_T >& freeSurfaceBoundaryHandling, + const ConstBlockDataID& pdfFieldID, const ConstBlockDataID& fillFieldID, + const ConstBlockDataID& excessMassFieldID, uint_t frequency, + const std::shared_ptr< real_t >& totalMass) + : blockForest_(blockForest), freeSurfaceBoundaryHandling_(freeSurfaceBoundaryHandling), pdfFieldID_(pdfFieldID), + fillFieldID_(fillFieldID), excessMassFieldID_(excessMassFieldID), totalMass_(totalMass), frequency_(frequency), + executionCounter_(uint_c(0)) + {} + + void operator()() + { + if (frequency_ == uint_c(0)) { return; } + + auto blockForest = blockForest_.lock(); + WALBERLA_CHECK_NOT_NULLPTR(blockForest); + + auto freeSurfaceBoundaryHandling = freeSurfaceBoundaryHandling_.lock(); + WALBERLA_CHECK_NOT_NULLPTR(freeSurfaceBoundaryHandling); + + if (executionCounter_ == uint_c(0)) { computeMass(blockForest, freeSurfaceBoundaryHandling); } + else + { + // only evaluate in given frequencies + if (executionCounter_ % frequency_ == uint_c(0)) { computeMass(blockForest, freeSurfaceBoundaryHandling); } + } + + ++executionCounter_; + } + + void computeMass(const std::shared_ptr< const StructuredBlockForest >& blockForest, + const std::shared_ptr< const FreeSurfaceBoundaryHandling_T >& freeSurfaceBoundaryHandling) + { + const BlockDataID flagFieldID = freeSurfaceBoundaryHandling->getFlagFieldID(); + const typename FreeSurfaceBoundaryHandling_T::FlagInfo_T& flagInfo = freeSurfaceBoundaryHandling->getFlagInfo(); + + real_t mass = real_c(0); + + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + const FlagField_T* const flagField = blockIt->template getData< const FlagField_T >(flagFieldID); + const PdfField_T* const pdfField = blockIt->template getData< const PdfField_T >(pdfFieldID_); + const ScalarField_T* const fillField = blockIt->template getData< const ScalarField_T >(fillFieldID_); + + // if provided, also consider mass stored in excessMassField + if (excessMassFieldID_ != ConstBlockDataID()) + { + const ScalarField_T* const excessMassField = + blockIt->template getData< const ScalarField_T >(excessMassFieldID_); + + WALBERLA_FOR_ALL_CELLS_OMP(flagFieldIt, flagField, pdfFieldIt, pdfField, fillFieldIt, fillField, + excessMassFieldIt, excessMassField, + omp parallel for schedule(static) reduction(+:mass), + { + if (flagInfo.isLiquid(flagFieldIt) || flagInfo.isInterface(flagFieldIt)) + { + const real_t density = pdfField->getDensity(pdfFieldIt.cell()); + mass += *fillFieldIt * density + *excessMassFieldIt; + } + }) // WALBERLA_FOR_ALL_CELLS_OMP + } + else + { + WALBERLA_FOR_ALL_CELLS_OMP(flagFieldIt, flagField, pdfFieldIt, pdfField, fillFieldIt, fillField, + omp parallel for schedule(static) reduction(+:mass), + { + if (flagInfo.isLiquid(flagFieldIt) || flagInfo.isInterface(flagFieldIt)) + { + const real_t density = pdfField->getDensity(pdfFieldIt.cell()); + mass += *fillFieldIt * density; + } + }) // WALBERLA_FOR_ALL_CELLS_OMP + } + } + + mpi::allReduceInplace< real_t >(mass, mpi::SUM); + + *totalMass_ = mass; + }; + + private: + std::weak_ptr< const StructuredBlockForest > blockForest_; + std::weak_ptr< const FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling_; + + const ConstBlockDataID pdfFieldID_; + const ConstBlockDataID fillFieldID_; + const ConstBlockDataID excessMassFieldID_ = ConstBlockDataID(); + + std::shared_ptr< real_t > totalMass_; + + uint_t frequency_; + uint_t executionCounter_; +}; // class TotalMassComputer + +} // namespace free_surface +} // namespace walberla \ No newline at end of file diff --git a/src/lbm/free_surface/VtkWriter.h b/src/lbm/free_surface/VtkWriter.h new file mode 100644 index 000000000..36f056740 --- /dev/null +++ b/src/lbm/free_surface/VtkWriter.h @@ -0,0 +1,173 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file VtkWriter.h +//! \ingroup free_surface +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Free surface-specific VTK writer function. +// +//====================================================================================================================== + +#include "blockforest/communication/UniformBufferedScheme.h" + +#include "field/adaptors/AdaptorCreators.h" +#include "field/communication/PackInfo.h" +#include "field/vtk/FlagFieldCellFilter.h" +#include "field/vtk/FlagFieldMapping.h" +#include "field/vtk/VTKWriter.h" + +#include "lbm/field/Adaptors.h" + +#include "stencil/D3Q27.h" + +#include "timeloop/SweepTimeloop.h" + +#include "vtk/Initialization.h" + +#include "FlagInfo.h" + +namespace walberla +{ +namespace free_surface +{ +/*********************************************************************************************************************** + * Add VTK output to time loop that includes all relevant free surface information. It must be configured via + * config-file. + **********************************************************************************************************************/ +template< typename LatticeModel_T, typename FreeSurfaceBoundaryHandling_T, typename PdfField_T, typename FlagField_T, + typename ScalarField_T, typename VectorField_T > +void addVTKOutput(const std::weak_ptr< StructuredBlockForest >& blockForestPtr, SweepTimeloop& timeloop, + const std::weak_ptr< Config >& configPtr, + const typename FreeSurfaceBoundaryHandling_T::FlagInfo_T& flagInfo, const BlockDataID& pdfFieldID, + const BlockDataID& flagFieldID, const BlockDataID& fillFieldID, const BlockDataID& forceFieldID, + const BlockDataID& curvatureFieldID, const BlockDataID& normalFieldID, + const BlockDataID& obstacleNormalFieldID) +{ + const auto blockForest = blockForestPtr.lock(); + WALBERLA_CHECK_NOT_NULLPTR(blockForest); + + const auto config = configPtr.lock(); + WALBERLA_CHECK_NOT_NULLPTR(config); + + // add various adaptors (for simplified access to macroscopic quantities) + const BlockDataID densityAdaptorID = field::addFieldAdaptor< typename lbm::Adaptor< LatticeModel_T >::Density >( + blockForest, pdfFieldID, "DensityAdaptor"); + const BlockDataID velocityAdaptorID = + field::addFieldAdaptor< typename lbm::Adaptor< LatticeModel_T >::VelocityVector >(blockForest, pdfFieldID, + "VelocityVectorAdaptor"); + // define VTK output (see src/vtk/Initialization.cpp, line 574 for usage) + const auto vtkConfigFunc = [&](std::vector< std::shared_ptr< vtk::BlockCellDataWriterInterface > >& writers, + std::map< std::string, vtk::VTKOutput::CellFilter >& filters, + std::map< std::string, vtk::VTKOutput::BeforeFunction >& beforeFuncs) { + using field::VTKWriter; + + // add fields to VTK output + writers.push_back(std::make_shared< VTKWriter< typename lbm::Adaptor< LatticeModel_T >::VelocityVector > >( + velocityAdaptorID, "velocity")); + writers.push_back(std::make_shared< VTKWriter< typename lbm::Adaptor< LatticeModel_T >::Density > >( + densityAdaptorID, "density")); + writers.push_back(std::make_shared< VTKWriter< PdfField_T, float > >(pdfFieldID, "pdf")); + writers.push_back(std::make_shared< VTKWriter< FlagField_T, float > >(flagFieldID, "flag")); + writers.push_back(std::make_shared< VTKWriter< ScalarField_T, float > >(fillFieldID, "fill_level")); + writers.push_back(std::make_shared< VTKWriter< ScalarField_T, float > >(curvatureFieldID, "curvature")); + writers.push_back(std::make_shared< VTKWriter< VectorField_T, float > >(normalFieldID, "normal")); + writers.push_back( + std::make_shared< VTKWriter< VectorField_T, float > >(obstacleNormalFieldID, "obstacle_normal")); + writers.push_back(std::make_shared< VTKWriter< VectorField_T, float > >(forceFieldID, "force")); + + // map flagIDs to integer values + const auto flagMapper = + std::make_shared< field::FlagFieldMapping< FlagField_T, walberla::uint_t > >(flagFieldID, "mapped_flag"); + flagMapper->addMapping(flagIDs::liquidFlagID, uint_c(1)); + flagMapper->addMapping(flagIDs::interfaceFlagID, uint_c(2)); + flagMapper->addMapping(flagIDs::gasFlagID, uint_c(3)); + flagMapper->addMapping(FreeSurfaceBoundaryHandling_T::noSlipFlagID, uint_c(4)); + flagMapper->addMapping(FreeSurfaceBoundaryHandling_T::freeSlipFlagID, uint_c(6)); + flagMapper->addMapping(FreeSurfaceBoundaryHandling_T::ubbFlagID, uint_c(6)); + flagMapper->addMapping(FreeSurfaceBoundaryHandling_T::ubbInflowFlagID, uint_c(7)); + flagMapper->addMapping(FreeSurfaceBoundaryHandling_T::pressureFlagID, uint_c(8)); + flagMapper->addMapping(FreeSurfaceBoundaryHandling_T::pressureOutflowFlagID, uint_c(9)); + flagMapper->addMapping(FreeSurfaceBoundaryHandling_T::outletFlagID, uint_c(10)); + flagMapper->addMapping(FreeSurfaceBoundaryHandling_T::simpleExtrapolationOutflowFlagID, uint_c(11)); + + writers.push_back(flagMapper); + + // filter for writing only liquid and interface cells to VTK + auto liquidInterfaceFilter = field::FlagFieldCellFilter< FlagField_T >(flagFieldID); + liquidInterfaceFilter.addFlag(flagIDs::liquidFlagID); + liquidInterfaceFilter.addFlag(flagIDs::interfaceFlagID); + filters["liquidInterfaceFilter"] = liquidInterfaceFilter; + + // communicate fields to update the ghost layer + auto preVTKComm = blockforest::communication::UniformBufferedScheme< stencil::D3Q27 >(blockForest); + preVTKComm.addPackInfo(std::make_shared< field::communication::PackInfo< PdfField_T > >(pdfFieldID)); + preVTKComm.addPackInfo(std::make_shared< field::communication::PackInfo< FlagField_T > >(flagFieldID)); + preVTKComm.addPackInfo(std::make_shared< field::communication::PackInfo< ScalarField_T > >(fillFieldID)); + preVTKComm.addPackInfo(std::make_shared< field::communication::PackInfo< ScalarField_T > >(curvatureFieldID)); + preVTKComm.addPackInfo(std::make_shared< field::communication::PackInfo< VectorField_T > >(normalFieldID)); + preVTKComm.addPackInfo( + std::make_shared< field::communication::PackInfo< VectorField_T > >(obstacleNormalFieldID)); + + beforeFuncs["ghost_layer_synchronization"] = preVTKComm; + }; + + // add VTK output to timeloop + std::map< std::string, vtk::SelectableOutputFunction > vtkOutputFunctions; + vtk::initializeVTKOutput(vtkOutputFunctions, vtkConfigFunc, blockForest, config); + for (auto output = vtkOutputFunctions.begin(); output != vtkOutputFunctions.end(); ++output) + { + timeloop.addFuncBeforeTimeStep(output->second.outputFunction, std::string("VTK: ") + output->first, + output->second.requiredGlobalStates, output->second.incompatibleGlobalStates); + } + + // only enable the zerosetter (see below) if the non-liquid and non-interface cells are not excluded anyway + bool enableZeroSetter = true; + const auto vtkConfigBlock = config->getOneBlock("VTK"); + const auto fluidFieldConfigBlock = vtkConfigBlock.getBlock("fluid_field"); + if (fluidFieldConfigBlock) + { + auto inclusionFiltersConfigBlock = fluidFieldConfigBlock.getBlock("inclusion_filters"); + + if (inclusionFiltersConfigBlock.isDefined("liquidInterfaceFilter")) + { + // liquidInterfaceFilter is defined which limits VTK-output to only liquid and interface cells + enableZeroSetter = false; + } + } + + // set velocity and density to zero in obstacle and gas cells (only for visualization purposes); the PDF values in + // these cells are not important and thus not set during the simulation + if (enableZeroSetter) + { + const auto function = [&](IBlock* block) { + using namespace free_surface; + PdfField_T* const pdfField = block->getData< PdfField_T >(pdfFieldID); + const FlagField_T* const flagField = block->getData< const FlagField_T >(flagFieldID); + WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ(pdfField, uint_c(1), { + const typename PdfField_T::Ptr pdfFieldPtr(*pdfField, x, y, z); + const typename FlagField_T::ConstPtr flagFieldPtr(*flagField, x, y, z); + + if (flagInfo.isGas(*flagFieldPtr) || flagInfo.isObstacle(*flagFieldPtr)) + { + pdfField->setDensityAndVelocity(pdfFieldPtr.cell(), Vector3< real_t >(0), real_c(1.0)); + } + }) // WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ + }; + timeloop.add() << Sweep(function, "VTK: zero-setting"); + } +} + +} // namespace free_surface +} // namespace walberla \ No newline at end of file diff --git a/src/lbm/free_surface/boundary/CMakeLists.txt b/src/lbm/free_surface/boundary/CMakeLists.txt new file mode 100644 index 000000000..9b3f1195a --- /dev/null +++ b/src/lbm/free_surface/boundary/CMakeLists.txt @@ -0,0 +1,6 @@ +target_sources( lbm + PRIVATE + FreeSurfaceBoundaryHandling.h + FreeSurfaceBoundaryHandling.impl.h + SimplePressureWithFreeSurface.h + ) diff --git a/src/lbm/free_surface/boundary/FreeSurfaceBoundaryHandling.h b/src/lbm/free_surface/boundary/FreeSurfaceBoundaryHandling.h new file mode 100644 index 000000000..f21e971ce --- /dev/null +++ b/src/lbm/free_surface/boundary/FreeSurfaceBoundaryHandling.h @@ -0,0 +1,188 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file Boundary.h +//! \ingroup free_surface +//! \author Martin Bauer +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Boundary handling for the free surface LBM module. +// +//====================================================================================================================== + +#pragma once + +#include "blockforest/StructuredBlockForest.h" +#include "blockforest/communication/UniformBufferedScheme.h" + +#include "boundary/BoundaryHandling.h" + +#include "field/GhostLayerField.h" + +#include "geometry/initializer/InitializationManager.h" +#include "geometry/initializer/OverlapFieldFromBody.h" + +#include "lbm/boundary/all.h" +#include "lbm/field/PdfField.h" +#include "lbm/free_surface/FlagInfo.h" +#include "lbm/free_surface/InitFunctions.h" +#include "lbm/free_surface/InterfaceFromFillLevel.h" +#include "lbm/free_surface/boundary/SimplePressureWithFreeSurface.h" + +namespace walberla +{ +namespace free_surface +{ +/*********************************************************************************************************************** + * Boundary handling for the free surface LBM extension. + **********************************************************************************************************************/ +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T > +class FreeSurfaceBoundaryHandling +{ + public: + using flag_t = typename FlagField_T::value_type; + using Stencil_T = typename LatticeModel_T::Stencil; + using CommunicationStencil_T = + typename std::conditional< LatticeModel_T::Stencil::D == uint_t(2), stencil::D2Q9, stencil::D3Q27 >::type; + using PdfField_T = lbm::PdfField< LatticeModel_T >; + + // boundary + using NoSlip_T = lbm::NoSlip< LatticeModel_T, flag_t >; + using FreeSlip_T = lbm::FreeSlip< LatticeModel_T, FlagField_T >; + using UBB_T = lbm::UBB< LatticeModel_T, flag_t >; + using Pressure_T = SimplePressureWithFreeSurface< LatticeModel_T, FlagField_T >; + using Outlet_T = lbm::Outlet< LatticeModel_T, FlagField_T, 4, 3 >; + using SimpleExtrapolationOutflow_T = lbm::SimpleExtrapolationOutflow< LatticeModel_T, FlagField_T >; + using UBB_Inflow_T = + lbm::UBB< LatticeModel_T, flag_t >; // creates interface cells in the direction of the prescribed velocity, i.e., + // represents an inflow boundary condition + + // handling type + using BoundaryHandling_T = + BoundaryHandling< FlagField_T, Stencil_T, NoSlip_T, UBB_T, UBB_Inflow_T, Pressure_T, Pressure_T, Outlet_T, + SimpleExtrapolationOutflow_T, + FreeSlip_T >; // 2 pressure boundaries with different densities, e.g., inflow-outflow + + using FlagInfo_T = FlagInfo< FlagField_T >; + + // constructor + FreeSurfaceBoundaryHandling(const std::shared_ptr< StructuredBlockForest >& blockForest, BlockDataID pdfFieldID, + BlockDataID fillLevelID); + + // initialize fluid field from config file using (quotes indicate the string to be used in the file): + // - "CellInterval" (see src/geometry/initializer/BoundaryFromCellInterval.h) + // - "Border" (see src/geometry/initializer/BoundaryFromDomainBorder.h) + // - "Image" (see src/geometry/initializer/BoundaryFromImage.h) + // - "Body" (see src/geometry/initializer/OverlapFieldFromBody.h) + inline void initFromConfig(const Config::BlockHandle& block); + + // initialize free surface object from geometric body (see src/geometry/initializer/OverlapFieldFromBody.h) + template< typename Body_T > + inline void addFreeSurfaceObject(const Body_T& body, bool addOrSubtract = false); + + // clear and initialize flags in every cell according to the fill level and initialize fill level in boundaries (with + // value 1) and obstacles such that the bubble model does not detect obstacles as gas cells + void initFlagsFromFillLevel(); + + inline void setNoSlipAtBorder(stencil::Direction d, cell_idx_t wallDistance = cell_idx_c(0)); + inline void setNoSlipAtAllBorders(cell_idx_t wallDistance = cell_idx_c(0)); + void setNoSlipInCell(const Cell& globalCell); + + inline void setFreeSlipAtBorder(stencil::Direction d, cell_idx_t wallDistance = cell_idx_c(0)); + inline void setFreeSlipAtAllBorders(cell_idx_t wallDistance = cell_idx_c(0)); + void setFreeSlipInCell(const Cell& globalCell); + + void setUBBInCell(const Cell& globalCell, const Vector3< real_t >& velocity); + + // UBB that generates interface cells to resemble an inflow boundary + void setInflowInCell(const Cell& globalCell, const Vector3< real_t >& velocity); + + inline void setPressure(real_t density); + void setPressureOutflow(real_t density); + void setBodyForce(const Vector3< real_t >& bodyForce); + + void enableBubbleOutflow(BubbleModelBase* bubbleModel); + + // checks if an obstacle cell is located in an outermost ghost layer (corners are explicitly ignored, as they do not + // influence periodic communication) + Vector3< bool > isObstacleInGlobalGhostLayer(); + + // flag management + const FlagInfo< FlagField_T >& getFlagInfo() const { return flagInfo_; } + + // flag IDs + static const field::FlagUID noSlipFlagID; + static const field::FlagUID ubbFlagID; + static const field::FlagUID ubbInflowFlagID; + static const field::FlagUID pressureFlagID; + static const field::FlagUID pressureOutflowFlagID; + static const field::FlagUID outletFlagID; + static const field::FlagUID simpleExtrapolationOutflowFlagID; + static const field::FlagUID freeSlipFlagID; + + // boundary IDs + static const BoundaryUID noSlipBoundaryID; + static const BoundaryUID ubbBoundaryID; + static const BoundaryUID ubbInflowBoundaryID; + static const BoundaryUID pressureBoundaryID; + static const BoundaryUID pressureOutflowBoundaryID; + static const BoundaryUID outletBoundaryID; + static const BoundaryUID simpleExtrapolationOutflowBoundaryID; + static const BoundaryUID freeSlipBoundaryID; + + inline BlockDataID getHandlingID() const { return handlingID_; } + inline BlockDataID getPdfFieldID() const { return pdfFieldID_; } + inline BlockDataID getFillFieldID() const { return fillFieldID_; } + inline BlockDataID getFlagFieldID() const { return flagFieldID_; } + + // executes standard waLBerla boundary handling + class ExecuteBoundaryHandling + { + public: + ExecuteBoundaryHandling(const BlockDataID& collection) : handlingID_(collection) {} + void operator()(IBlock* const block) const + { + BoundaryHandling_T* const handling = block->getData< BoundaryHandling_T >(handlingID_); + // reset "near boundary" flags + handling->refresh(); + (*handling)(); + } + + protected: + BlockDataID handlingID_; + }; // class ExecuteBoundaryHandling + + ExecuteBoundaryHandling getBoundarySweep() const { return ExecuteBoundaryHandling(getHandlingID()); } + + private: + FlagInfo< FlagField_T > flagInfo_; + + // register standard waLBerla initializers + geometry::initializer::InitializationManager getInitManager(); + + std::shared_ptr< StructuredBlockForest > blockForest_; + + BlockDataID flagFieldID_; + BlockDataID pdfFieldID_; + BlockDataID fillFieldID_; + + BlockDataID handlingID_; + + blockforest::communication::UniformBufferedScheme< CommunicationStencil_T > comm_; +}; // class FreeSurfaceBoundaryHandling + +} // namespace free_surface +} // namespace walberla + +#include "FreeSurfaceBoundaryHandling.impl.h" diff --git a/src/lbm/free_surface/boundary/FreeSurfaceBoundaryHandling.impl.h b/src/lbm/free_surface/boundary/FreeSurfaceBoundaryHandling.impl.h new file mode 100644 index 000000000..e5fde552f --- /dev/null +++ b/src/lbm/free_surface/boundary/FreeSurfaceBoundaryHandling.impl.h @@ -0,0 +1,554 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file FreeSurfaceBoundaryHandling.impl.h +//! \ingroup free_surface +//! \author Martin Bauer +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Boundary handling for the free surface LBM module. +// +//====================================================================================================================== + +#pragma once + +#include "field/AddToStorage.h" +#include "field/FlagField.h" +#include "field/communication/PackInfo.h" + +#include "geometry/initializer/BoundaryFromCellInterval.h" +#include "geometry/initializer/BoundaryFromDomainBorder.h" +#include "geometry/initializer/BoundaryFromImage.h" +#include "geometry/structured/GrayScaleImage.h" + +#include "lbm/free_surface/FlagInfo.h" +#include "lbm/free_surface/InterfaceFromFillLevel.h" +#include "lbm/lattice_model/CollisionModel.h" + +#include "FreeSurfaceBoundaryHandling.h" + +namespace walberla +{ +namespace free_surface +{ +namespace internal +{ +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T > +class BoundaryBlockDataHandling + : public domain_decomposition::BlockDataHandling< + typename FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::BoundaryHandling_T > +{ + public: + using BoundaryHandling_T = + typename FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, + ScalarField_T >::BoundaryHandling_T; // handling as defined in + // FreeSurfaceBoundaryHandling.h + + BoundaryBlockDataHandling(const FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >* boundary) + : boundary_(boundary) + {} + + // initialize standard waLBerla boundary handling + BoundaryHandling_T* initialize(IBlock* const block) + { + using B = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >; + using flag_t = typename B::flag_t; + + // get fields + FlagField_T* const flagField = block->getData< FlagField_T >(boundary_->getFlagFieldID()); + typename B::PdfField_T* const pdfField = block->getData< typename B::PdfField_T >(boundary_->getPdfFieldID()); + + auto interfaceFlag = flag_t(flagField->getFlag(flagIDs::interfaceFlagID)); + auto liquidFlag = flag_t(flagField->getFlag(flagIDs::liquidFlagID)); + + // domainMask is used to identify liquid and interface cells + auto domainMask = flag_t(liquidFlag | interfaceFlag); + WALBERLA_ASSERT(domainMask != 0); + + // initialize boundary conditions + typename B::UBB_T ubb(B::ubbBoundaryID, B::ubbFlagID, pdfField, flagField); + typename B::UBB_Inflow_T ubbInflow(B::ubbInflowBoundaryID, B::ubbInflowFlagID, pdfField, flagField); + typename B::NoSlip_T noslip(B::noSlipBoundaryID, B::noSlipFlagID, pdfField); + typename B::Pressure_T pressure(B::pressureBoundaryID, B::pressureFlagID, block, pdfField, flagField, + interfaceFlag, real_c(1.0)); + typename B::Pressure_T pressureOutflow(B::pressureOutflowBoundaryID, B::pressureOutflowFlagID, block, pdfField, + flagField, interfaceFlag, real_c(1.0)); + typename B::Outlet_T outlet(B::outletBoundaryID, B::outletFlagID, pdfField, flagField, domainMask); + typename B::SimpleExtrapolationOutflow_T simpleExtrapolationOutflow( + B::simpleExtrapolationOutflowBoundaryID, B::simpleExtrapolationOutflowFlagID, pdfField); + typename B::FreeSlip_T freeSlip(B::freeSlipBoundaryID, B::freeSlipFlagID, pdfField, flagField, domainMask); + + return new BoundaryHandling_T("Boundary Handling", flagField, domainMask, noslip, ubb, ubbInflow, pressure, + pressureOutflow, outlet, simpleExtrapolationOutflow, freeSlip); + } + + void serialize(IBlock* const block, const BlockDataID& id, mpi::SendBuffer& buffer) + { + BoundaryHandling_T* const boundaryHandlingPtr = block->getData< BoundaryHandling_T >(id); + CellInterval everyCell = boundaryHandlingPtr->getFlagField()->xyzSizeWithGhostLayer(); + boundaryHandlingPtr->pack(buffer, everyCell, true); + } + + BoundaryHandling_T* deserialize(IBlock* const block) { return initialize(block); } + + void deserialize(IBlock* const block, const BlockDataID& id, mpi::RecvBuffer& buffer) + { + BoundaryHandling_T* const boundaryHandlingPtr = block->getData< BoundaryHandling_T >(id); + CellInterval everyCell = boundaryHandlingPtr->getFlagField()->xyzSizeWithGhostLayer(); + boundaryHandlingPtr->unpack(buffer, everyCell, true); + } + + private: + const FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >* boundary_; +}; // class BoundaryBlockDataHandling + +// helper function wrapper for adding the flag field to the block storage; since the input parameter for an +// initialization function in field::addFlagFieldToStorage() is a std::function<void(FlagField_T*,IBlock* const)>, we +// need a function wrapper that has both these input parameters; as FlagInfo< FlagField_T >::registerFlags() does not +// have both of these input parameters, a function wrapper with both input parameters is created and the second input +// parameter is simply ignored inside the function wrapper +template< typename FlagField_T > +void flagFieldInitFunction(FlagField_T* flagField, IBlock* const, const Set< field::FlagUID >& obstacleIDs, + const Set< field::FlagUID >& outflowIDs, const Set< field::FlagUID >& inflowIDs, + const Set< field::FlagUID >& freeSlipIDs) +{ + // register flags in the flag field + FlagInfo< FlagField_T >::registerFlags(flagField, obstacleIDs, outflowIDs, inflowIDs, freeSlipIDs); +} + +} // namespace internal + +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T > +FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::FreeSurfaceBoundaryHandling( + const std::shared_ptr< StructuredBlockForest >& blockForest, BlockDataID pdfFieldID, BlockDataID fillLevelID) + : blockForest_(blockForest), pdfFieldID_(pdfFieldID), fillFieldID_(fillLevelID), comm_(blockForest) +{ + // initialize obstacleIDs + Set< FlagUID > obstacleIDs; + obstacleIDs += noSlipFlagID; + obstacleIDs += ubbFlagID; + obstacleIDs += ubbInflowFlagID; + obstacleIDs += pressureFlagID; + obstacleIDs += pressureOutflowFlagID; + obstacleIDs += freeSlipFlagID; + obstacleIDs += outletFlagID; + obstacleIDs += simpleExtrapolationOutflowFlagID; + + // initialize outflowIDs + Set< FlagUID > outflowIDs; + outflowIDs += pressureOutflowFlagID; + outflowIDs += outletFlagID; + outflowIDs += simpleExtrapolationOutflowFlagID; + + // initialize outflowIDs + Set< FlagUID > inflowIDs; + inflowIDs += ubbInflowFlagID; + + // initialize freeSlipIDs + Set< FlagUID > freeSlipIDs; + freeSlipIDs += freeSlipFlagID; + + // create callable function wrapper with input arguments 1 and 2 unset, whereas arguments 3, 4 and 5 are set to be + // obstacleIDs, outflowIDs, and inflowIDs, respectively; this is necessary for field::addFlagFieldToStorage() + auto ffInitFunc = std::bind(internal::flagFieldInitFunction< FlagField_T >, std::placeholders::_1, + std::placeholders::_2, obstacleIDs, outflowIDs, inflowIDs, freeSlipIDs); + + // IMPORTANT REMARK: The flag field needs two ghost layers because of function advectMass(). There, the flags of all + // D3Q* neighbors are determined for each cell, including cells in the first ghost layer. Therefore, all D3Q* + // neighbors of the first ghost layer must be accessible. This requires a second ghost layer. + flagFieldID_ = field::addFlagFieldToStorage< FlagField_T >(blockForest, "Flags", uint_c(2), true, ffInitFunc); + + // create FlagInfo + flagInfo_ = FlagInfo< FlagField_T >(obstacleIDs, outflowIDs, inflowIDs, freeSlipIDs); + WALBERLA_ASSERT(flagInfo_.isConsistentAcrossBlocksAndProcesses(blockForest, flagFieldID_)); + + // add boundary handling to blockForest + handlingID_ = blockForest_->addBlockData( + std::make_shared< internal::BoundaryBlockDataHandling< LatticeModel_T, FlagField_T, ScalarField_T > >(this), + "Boundary Handling"); + + // create communication object with fill level field, since fill levels determine the flags during the simulation + comm_.addPackInfo(std::make_shared< field::communication::PackInfo< ScalarField_T > >(fillFieldID_)); +} + +// define IDs (static const variables) +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T > +const field::FlagUID + FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::noSlipFlagID = field::FlagUID("NoSlip"); + +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T > +const field::FlagUID + FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::ubbFlagID = field::FlagUID("UBB"); + +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T > +const field::FlagUID FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::ubbInflowFlagID = + field::FlagUID("UBB_Inflow"); + +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T > +const field::FlagUID FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::pressureFlagID = + field::FlagUID("Pressure"); + +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T > +const field::FlagUID FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::pressureOutflowFlagID = + field::FlagUID("PressureOutflow"); + +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T > +const field::FlagUID + FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::outletFlagID = field::FlagUID("Outlet"); + +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T > +const field::FlagUID + FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::simpleExtrapolationOutflowFlagID = + field::FlagUID("SimpleExtrapolationOutflow"); + +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T > +const field::FlagUID FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::freeSlipFlagID = + field::FlagUID("FreeSlip"); + +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T > +const BoundaryUID + FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::noSlipBoundaryID = BoundaryUID("NoSlip"); + +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T > +const BoundaryUID + FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::ubbBoundaryID = BoundaryUID("UBB"); + +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T > +const BoundaryUID FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::ubbInflowBoundaryID = + BoundaryUID("UBB_Inflow"); + +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T > +const BoundaryUID FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::pressureBoundaryID = + BoundaryUID("Pressure"); + +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T > +const BoundaryUID FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::pressureOutflowBoundaryID = + BoundaryUID("PressureOutflow"); + +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T > +const BoundaryUID + FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::outletBoundaryID = BoundaryUID("Outlet"); + +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T > +const BoundaryUID + FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::simpleExtrapolationOutflowBoundaryID = + BoundaryUID("SimpleExtrapolationOutflow"); + +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T > +const BoundaryUID FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::freeSlipBoundaryID = + BoundaryUID("FreeSlip"); + +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T > +geometry::initializer::InitializationManager + FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::getInitManager() +{ + using namespace geometry::initializer; + + InitializationManager initManager(blockForest_->getBlockStorage()); + + // define initializers + auto cellIntvInit = std::make_shared< BoundaryFromCellInterval< BoundaryHandling_T > >(*blockForest_, handlingID_); + auto borderInit = std::make_shared< BoundaryFromDomainBorder< BoundaryHandling_T > >(*blockForest_, handlingID_); + auto imgInit = + std::make_shared< BoundaryFromImage< BoundaryHandling_T, geometry::GrayScaleImage > >(*blockForest_, handlingID_); + auto bodyInit = std::make_shared< OverlapFieldFromBody >(*blockForest_, fillFieldID_); + + // register initializers + initManager.registerInitializer("CellInterval", cellIntvInit); + initManager.registerInitializer("Border", borderInit); + initManager.registerInitializer("Image", imgInit); + initManager.registerInitializer("Body", bodyInit); + + return initManager; +} + +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T > +void FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::initFromConfig( + const Config::BlockHandle& configBlock) +{ + // initialize from config file + getInitManager().init(configBlock); +} + +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T > +template< typename Body_T > +void FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::addFreeSurfaceObject(const Body_T& body, + bool addOrSubtract) +{ + geometry::initializer::OverlapFieldFromBody(*blockForest_, fillFieldID_).init(body, addOrSubtract); +} + +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T > +void FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::setNoSlipAtBorder( + stencil::Direction d, cell_idx_t wallDistance) +{ + geometry::initializer::BoundaryFromDomainBorder< BoundaryHandling_T > init(*blockForest_, handlingID_); + init.init(noSlipFlagID, d, wallDistance); +} + +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T > +void FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::setNoSlipAtAllBorders( + cell_idx_t wallDistance) +{ + geometry::initializer::BoundaryFromDomainBorder< BoundaryHandling_T > init(*blockForest_, handlingID_); + init.initAllBorders(noSlipFlagID, wallDistance); +} + +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T > +void FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::setNoSlipInCell(const Cell& globalCell) +{ + for (auto blockIt = blockForest_->begin(); blockIt != blockForest_->end(); ++blockIt) + { + BoundaryHandling_T* const handling = blockIt->template getData< BoundaryHandling_T >(handlingID_); + + // transform cell in global coordinates to cell in block local coordinates + Cell blockLocalCell; + blockForest_->transformGlobalToBlockLocalCell(blockLocalCell, *blockIt, globalCell); + + handling->forceBoundary(noSlipFlagID, blockLocalCell[0], blockLocalCell[1], blockLocalCell[2]); + } +} + +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T > +void FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::setFreeSlipAtBorder( + stencil::Direction d, cell_idx_t wallDistance) +{ + geometry::initializer::BoundaryFromDomainBorder< BoundaryHandling_T > init(*blockForest_, handlingID_); + init.init(freeSlipFlagID, d, wallDistance); +} + +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T > +void FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::setFreeSlipAtAllBorders( + cell_idx_t wallDistance) +{ + geometry::initializer::BoundaryFromDomainBorder< BoundaryHandling_T > init(*blockForest_, handlingID_); + init.initAllBorders(freeSlipFlagID, wallDistance); +} + +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T > +void FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::setFreeSlipInCell( + const Cell& globalCell) +{ + for (auto blockIt = blockForest_->begin(); blockIt != blockForest_->end(); ++blockIt) + { + BoundaryHandling_T* const handling = blockIt->template getData< BoundaryHandling_T >(handlingID_); + + // transform cell in global coordinates to cell in block local coordinates + Cell blockLocalCell; + blockForest_->transformGlobalToBlockLocalCell(blockLocalCell, *blockIt, globalCell); + + handling->forceBoundary(freeSlipFlagID, blockLocalCell[0], blockLocalCell[1], blockLocalCell[2]); + } +} + +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T > +void FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::setPressure(real_t density) +{ + for (auto blockIt = blockForest_->begin(); blockIt != blockForest_->end(); ++blockIt) + { + BoundaryHandling_T* const handling = blockIt->template getData< BoundaryHandling_T >(handlingID_); + Pressure_T& pressure = + handling->template getBoundaryCondition< Pressure_T >(handling->getBoundaryUID(pressureFlagID)); + pressure.setLatticeDensity(density); + } +} + +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T > +void FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::setUBBInCell( + const Cell& globalCell, const Vector3< real_t >& velocity) +{ + for (auto blockIt = blockForest_->begin(); blockIt != blockForest_->end(); ++blockIt) + { + BoundaryHandling_T* const handling = blockIt->template getData< BoundaryHandling_T >(handlingID_); + + typename UBB_Inflow_T::Velocity ubbVel(velocity); + + // transform cell in global coordinates to cell in block-local coordinates + Cell blockLocalCell; + blockForest_->transformGlobalToBlockLocalCell(blockLocalCell, *blockIt, globalCell); + + // get block cell bounding box to check if cell is contained in block + CellInterval blockCellBB = blockForest_->getBlockCellBB(*blockIt); + + // flag field has two ghost layers so blockCellBB is actually larger than returned; this is relevant for setups + // where the UBB is set in a ghost layer cell + blockCellBB.expand(cell_idx_c(2)); + + if (blockCellBB.contains(globalCell)) + { + handling->forceBoundary(ubbFlagID, blockLocalCell[0], blockLocalCell[1], blockLocalCell[2], ubbVel); + } + } +} + +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T > +void FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::setInflowInCell( + const Cell& globalCell, const Vector3< real_t >& velocity) +{ + for (auto blockIt = blockForest_->begin(); blockIt != blockForest_->end(); ++blockIt) + { + BoundaryHandling_T* const handling = blockIt->template getData< BoundaryHandling_T >(handlingID_); + + typename UBB_Inflow_T::Velocity ubbVel(velocity); + + // transform cell in global coordinates to cell in block-local coordinates + Cell blockLocalCell; + blockForest_->transformGlobalToBlockLocalCell(blockLocalCell, *blockIt, globalCell); + + // get block cell bounding box to check if cell is contained in block + CellInterval blockCellBB = blockForest_->getBlockCellBB(*blockIt); + + // flag field has two ghost layers so blockCellBB is actually larger than returned; this is relevant for setups + // where the UBB is set in a ghost layer cell + blockCellBB.expand(cell_idx_c(2)); + + if (blockCellBB.contains(globalCell)) + { + handling->forceBoundary(ubbInflowFlagID, blockLocalCell[0], blockLocalCell[1], blockLocalCell[2], ubbVel); + } + } +} + +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T > +void FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::setPressureOutflow(real_t density) +{ + for (auto blockIt = blockForest_->begin(); blockIt != blockForest_->end(); ++blockIt) + { + BoundaryHandling_T* const handling = blockIt->template getData< BoundaryHandling_T >(handlingID_); + Pressure_T& pressure = + handling->template getBoundaryCondition< Pressure_T >(handling->getBoundaryUID(pressureOutflowFlagID)); + pressure.setLatticeDensity(density); + } +} + +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T > +void FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::enableBubbleOutflow( + BubbleModelBase* bubbleModel) +{ + for (auto blockIt = blockForest_->begin(); blockIt != blockForest_->end(); ++blockIt) + { + BoundaryHandling_T* const handling = blockIt->template getData< BoundaryHandling_T >(handlingID_); + + // get pressure from boundary handling + Pressure_T& pressure = + handling->template getBoundaryCondition< Pressure_T >(handling->getBoundaryUID(pressureFlagID)); + Pressure_T& pressureOutflow = + handling->template getBoundaryCondition< Pressure_T >(handling->getBoundaryUID(pressureOutflowFlagID)); + + // set pressure in bubble model + pressure.setBubbleModel(bubbleModel); + pressureOutflow.setBubbleModel(bubbleModel); + } +} + +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T > +Vector3< bool > + FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::isObstacleInGlobalGhostLayer() +{ + Vector3< bool > isObstacleInGlobalGhostLayer(false, false, false); + + for (auto blockIt = blockForest_->begin(); blockIt != blockForest_->end(); ++blockIt) + { + const FlagField_T* const flagField = blockIt->template getData< const FlagField_T >(flagFieldID_); + + const CellInterval domainCellBB = blockForest_->getDomainCellBB(); + + // disable OpenMP such that loop termination works correctly + WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ_OMP(flagField, uint_c(1), omp critical, { + // get cell in global coordinates + Cell globalCell = Cell(x, y, z); + blockForest_->transformBlockLocalToGlobalCell(globalCell, *blockIt); + + // check if the current cell is located in a global ghost layer + const bool isCellInGlobalGhostLayerX = + globalCell[0] < domainCellBB.xMin() || globalCell[0] > domainCellBB.xMax(); + + const bool isCellInGlobalGhostLayerY = + globalCell[1] < domainCellBB.yMin() || globalCell[1] > domainCellBB.yMax(); + + const bool isCellInGlobalGhostLayerZ = + globalCell[2] < domainCellBB.zMin() || globalCell[2] > domainCellBB.zMax(); + + // skip corners, as they do not influence periodic communication + if ((isCellInGlobalGhostLayerX && (isCellInGlobalGhostLayerY || isCellInGlobalGhostLayerZ)) || + (isCellInGlobalGhostLayerY && isCellInGlobalGhostLayerZ)) + { + continue; + } + + if (!isObstacleInGlobalGhostLayer[0] && isCellInGlobalGhostLayerX && + isPartOfMaskSet(flagField->get(x, y, z), flagField->getMask(flagInfo_.getObstacleIDSet()))) + { + isObstacleInGlobalGhostLayer[0] = true; + } + + if (!isObstacleInGlobalGhostLayer[1] && isCellInGlobalGhostLayerY && + isPartOfMaskSet(flagField->get(x, y, z), flagField->getMask(flagInfo_.getObstacleIDSet()))) + { + isObstacleInGlobalGhostLayer[1] = true; + } + + if (!isObstacleInGlobalGhostLayer[2] && isCellInGlobalGhostLayerZ && + isPartOfMaskSet(flagField->get(x, y, z), flagField->getMask(flagInfo_.getObstacleIDSet()))) + { + isObstacleInGlobalGhostLayer[2] = true; + } + + if (isObstacleInGlobalGhostLayer[0] && isObstacleInGlobalGhostLayer[1] && isObstacleInGlobalGhostLayer[2]) + { + break; // there is no need to check other cells on this block + } + }) // WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ_OMP + } + + mpi::allReduceInplace(isObstacleInGlobalGhostLayer[0], mpi::LOGICAL_OR); + mpi::allReduceInplace(isObstacleInGlobalGhostLayer[1], mpi::LOGICAL_OR); + mpi::allReduceInplace(isObstacleInGlobalGhostLayer[2], mpi::LOGICAL_OR); + + return isObstacleInGlobalGhostLayer; +} + +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T > +void FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::initFlagsFromFillLevel() +{ + const Vector3< bool > isObstacleInGlobalGhostLayer = this->isObstacleInGlobalGhostLayer(); + + WALBERLA_ROOT_SECTION() + { + if ((blockForest_->isXPeriodic() && isObstacleInGlobalGhostLayer[0]) || + (blockForest_->isYPeriodic() && isObstacleInGlobalGhostLayer[1]) || + (blockForest_->isZPeriodic() && isObstacleInGlobalGhostLayer[2])) + { + WALBERLA_LOG_WARNING_ON_ROOT( + "WARNING: An obstacle cell is located in a global outermost ghost layer in a periodic " + "direction. Be aware that due to periodicity, this obstacle cell will be " + "overwritten during communication."); + } + } + + // communicate fill level (neighborhood is used in initialization) + comm_(); + + // initialize fill level in boundaries (with value 1), i.e., obstacles such that the bubble model does not detect + // obstacles as gas cells + free_surface::initFillLevelsInBoundaries< BoundaryHandling_T, typename LatticeModel_T::Stencil, ScalarField_T >( + blockForest_, handlingID_, fillFieldID_); + + // clear and initialize flags in every cell according to the fill level + free_surface::initFlagsFromFillLevels< BoundaryHandling_T, typename LatticeModel_T::Stencil, FlagField_T, + const ScalarField_T >(blockForest_, flagInfo_, handlingID_, fillFieldID_); +} + +} // namespace free_surface +} // namespace walberla diff --git a/src/lbm/free_surface/boundary/SimplePressureWithFreeSurface.h b/src/lbm/free_surface/boundary/SimplePressureWithFreeSurface.h new file mode 100644 index 000000000..534d965f9 --- /dev/null +++ b/src/lbm/free_surface/boundary/SimplePressureWithFreeSurface.h @@ -0,0 +1,150 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file SimplePressureWithFreeSurface.h +//! \ingroup free_surface +//! \author Martin Bauer +//! \author Christian Godenschwager +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief SimplePressure boundary condition for the free surface LBM. +// +//====================================================================================================================== + +#pragma once + +#include "boundary/Boundary.h" + +#include "core/DataTypes.h" +#include "core/cell/CellInterval.h" +#include "core/config/Config.h" +#include "core/debug/Debug.h" +#include "core/math/Vector3.h" + +#include "field/FlagField.h" + +#include "lbm/field/PdfField.h" +#include "lbm/free_surface/bubble_model/BubbleModel.h" +#include "lbm/lattice_model/EquilibriumDistribution.h" +#include "lbm/lattice_model/ForceModel.h" + +#include "stencil/Directions.h" + +#include <vector> + +namespace walberla +{ +namespace free_surface +{ +/*********************************************************************************************************************** + * SimplePressure boundary condition for the free surface LBM. The implementation is almost identical to the general + * lbm/boundary/SimplePressure.h boundary condition, however, the boundary pressure (density) is also set in the bubble + * model. + **********************************************************************************************************************/ +template< typename LatticeModel_T, typename FlagField_T > +class SimplePressureWithFreeSurface : public boundary::Boundary< typename FlagField_T::flag_t > +{ + using PdfField_T = lbm::PdfField< LatticeModel_T >; + using Stencil_T = typename LatticeModel_T::Stencil; + using flag_t = typename FlagField_T::flag_t; + + public: + static const bool threadsafe = true; + + static std::shared_ptr< BoundaryConfiguration > createConfiguration(const Config::BlockHandle& /*config*/) + { + return std::make_shared< BoundaryConfiguration >(); + } + + SimplePressureWithFreeSurface(const BoundaryUID& boundaryUID, const FlagUID& uid, IBlock* block, + PdfField_T* const pdfField, FlagField_T* flagField, flag_t interfaceFlag, + const real_t latticeDensity) + : Boundary< flag_t >(boundaryUID), uid_(uid), block_(block), pdfs_(pdfField), flagField_(flagField), + interfaceFlag_(interfaceFlag), bubbleModel_(nullptr), latticeDensity_(latticeDensity) + { + WALBERLA_ASSERT_NOT_NULLPTR(pdfs_); + WALBERLA_ASSERT_NOT_NULLPTR(flagField); + } + + void pushFlags(std::vector< FlagUID >& uids) const { uids.push_back(uid_); } + + void beforeBoundaryTreatment() const {} + void afterBoundaryTreatment() const {} + + template< typename Buffer_T > + void packCell(Buffer_T&, const cell_idx_t, const cell_idx_t, const cell_idx_t) const + {} + + template< typename Buffer_T > + void registerCell(Buffer_T&, const flag_t, const cell_idx_t, const cell_idx_t, const cell_idx_t) + {} + + void registerCell(const flag_t, const cell_idx_t, const cell_idx_t, const cell_idx_t, const BoundaryConfiguration&) + {} + void registerCells(const flag_t, const CellInterval&, const BoundaryConfiguration&) const {} + template< typename CellIterator > + void registerCells(const flag_t, const CellIterator&, const CellIterator&, const BoundaryConfiguration&) const + {} + + void unregisterCell(const flag_t, const cell_idx_t, const cell_idx_t, const cell_idx_t) const {} + + void setLatticeDensity(real_t newLatticeDensity) { latticeDensity_ = newLatticeDensity; } + + void setBubbleModel(BubbleModelBase* bubbleModel) { bubbleModel_ = bubbleModel; } + +#ifndef NDEBUG + inline void treatDirection(const cell_idx_t x, const cell_idx_t y, const cell_idx_t z, const stencil::Direction dir, + const cell_idx_t nx, const cell_idx_t ny, const cell_idx_t nz, const flag_t mask) +#else + inline void treatDirection(const cell_idx_t x, const cell_idx_t y, const cell_idx_t z, const stencil::Direction dir, + const cell_idx_t nx, const cell_idx_t ny, const cell_idx_t nz, const flag_t /*mask*/) +#endif + { + WALBERLA_ASSERT_EQUAL(nx, x + cell_idx_c(stencil::cx[dir])); + WALBERLA_ASSERT_EQUAL(ny, y + cell_idx_c(stencil::cy[dir])); + WALBERLA_ASSERT_EQUAL(nz, z + cell_idx_c(stencil::cz[dir])); + + WALBERLA_ASSERT_UNEQUAL((mask & this->mask_), numeric_cast< flag_t >(0)); + WALBERLA_ASSERT_EQUAL((mask & this->mask_), + this->mask_); // only true if "this->mask_" only contains one single flag, which is the case + // for the current implementation of this boundary condition (SimplePressure) + Vector3< real_t > u = pdfs_->getVelocity(x, y, z); + + // set density in bubble model according to pressure boundary condition + if (bubbleModel_ && flagField_->isFlagSet(x, y, z, interfaceFlag_)) + { + bubbleModel_->setDensity(block_, Cell(x, y, z), latticeDensity_); + } + + // result will be streamed to (x,y,z, stencil::inverseDir[d]) during sweep + pdfs_->get(nx, ny, nz, Stencil_T::invDirIdx(dir)) = + -pdfs_->get(x, y, z, Stencil_T::idx[dir]) // anti-bounce-back + + real_c(2) * lbm::EquilibriumDistribution< LatticeModel_T >::getSymmetricPart( + dir, u, latticeDensity_); // pressure term + } + + protected: + FlagUID uid_; + + IBlock* block_; + PdfField_T* pdfs_; + FlagField_T* flagField_; + flag_t interfaceFlag_; + BubbleModelBase* bubbleModel_; + + real_t latticeDensity_; +}; + +} // namespace free_surface +} // namespace walberla diff --git a/src/lbm/free_surface/bubble_model/Bubble.h b/src/lbm/free_surface/bubble_model/Bubble.h new file mode 100644 index 000000000..bb3b9a074 --- /dev/null +++ b/src/lbm/free_surface/bubble_model/Bubble.h @@ -0,0 +1,171 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file Bubble.h +//! \ingroup bubble_model +//! \author Martin Bauer +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Describes a bubble as gas volume via volume and density. +// +//====================================================================================================================== + +#pragma once + +#include "core/DataTypes.h" +#include "core/debug/Debug.h" +#include "core/mpi/MPIWrapper.h" +#include "core/mpi/RecvBuffer.h" +#include "core/mpi/SendBuffer.h" + +#include <iostream> + +namespace walberla +{ +namespace free_surface +{ +namespace bubble_model +{ +// forward declarations of friend classes +template< typename Stencil_T > +class BubbleModel; + +class MergeInformation; + +/*********************************************************************************************************************** + * Describes a bubble as gas volume via volume and density. A bubble can be located on multiple blocks, i.e., processes. + **********************************************************************************************************************/ +class Bubble +{ + public: + explicit Bubble(real_t initVolume) + : initVolume_(initVolume), currentVolume_(initVolume), volumeDiff_(real_c(0)), rho_(real_c(1.0)) + {} + + Bubble(real_t initVolume, real_t density) + : initVolume_(initVolume), currentVolume_(density * initVolume), volumeDiff_(real_c(0)), rho_(density) + {} + + // dummy constructor with meaningless default values + Bubble() : initVolume_(real_c(-1.0)), currentVolume_(real_c(-1.0)), volumeDiff_(real_c(0)), rho_(real_c(-1.0)) {} + + real_t getInitVolume() const { return initVolume_; } + real_t getCurrentVolume() const { return currentVolume_; } + real_t getDensity() const { return rho_; } + + bool hasConstantDensity() const { return hasConstantDensity_; } + + void setConstantDensity(real_t density = real_c(1.0)) + { + hasConstantDensity_ = true; + rho_ = density; + } + + // update the bubble volume change + void updateVolumeDiff(real_t diff) { volumeDiff_ += diff; } + + void setDensity(real_t rho) + { + initVolume_ = rho * currentVolume_; + updateDensity(); + } + + private: + template< typename Stencil_T > + friend class BubbleModel; + + friend class MergeInformation; + + // merge bubbles by adding volumes, and update density (see dissertation of S. Bogner, 2017, section 4.3) + void merge(const Bubble& other) + { + initVolume_ += other.initVolume_; + currentVolume_ += other.currentVolume_; + updateDensity(); + } + + // update bubble volume and density using the change in the bubble's volume (see dissertation of S. Bogner, 2017, + // section 4.3) + void applyVolumeDiff(real_t diff) + { + WALBERLA_ASSERT(volumeDiff_ <= real_c(0.0)); + currentVolume_ += diff; + updateDensity(); + } + + // return and reset the bubble's volume change + real_t getAndResetVolumeDiff() + { + real_t ret = volumeDiff_; + volumeDiff_ = real_c(0); + return ret; + } + + // update bubble density (see dissertation of S. Bogner, 2017, section 4.3) + void updateDensity() + { + if (hasConstantDensity_) return; + rho_ = initVolume_ / currentVolume_; + } + + real_t initVolume_; + real_t currentVolume_; + real_t volumeDiff_; // bubble's volume change (caused by interface fill level or movement) + real_t rho_; + + bool hasConstantDensity_{ false }; + + // function for packing a bubble into SendBuffer + friend mpi::SendBuffer& operator<<(mpi::SendBuffer& buf, const Bubble& b); + + // function for unpacking a bubble from RecvBuffer + friend mpi::RecvBuffer& operator>>(mpi::RecvBuffer& buf, Bubble& b); +}; // class Bubble + +inline mpi::SendBuffer& operator<<(mpi::SendBuffer& buf, const Bubble& b) +{ + return buf << b.initVolume_ << b.currentVolume_; +} + +inline mpi::RecvBuffer& operator>>(mpi::RecvBuffer& buf, Bubble& b) +{ + buf >> b.initVolume_ >> b.currentVolume_; + b.updateDensity(); + return buf; +} + +inline std::ostream& operator<<(std::ostream& os, const Bubble& b) +{ + os << "Bubble (" << b.getInitVolume() << "," << b.getCurrentVolume() << "," << b.getDensity() << ")"; + + return os; +} + +} // namespace bubble_model +} // namespace free_surface +} // namespace walberla + +namespace walberla +{ +namespace mpi +{ +template<> // value type +struct BufferSizeTrait< free_surface::bubble_model::Bubble > +{ + static const bool constantSize = true; + // 2 * real_t since the buffers above are filled with initVolume_ and currentVolume_ + static const uint_t size = 2 * sizeof(real_t) + mpi::BUFFER_DEBUG_OVERHEAD; +}; +} // namespace mpi +} // namespace walberla diff --git a/src/lbm/free_surface/bubble_model/BubbleDefinitions.h b/src/lbm/free_surface/bubble_model/BubbleDefinitions.h new file mode 100644 index 000000000..b67397be1 --- /dev/null +++ b/src/lbm/free_surface/bubble_model/BubbleDefinitions.h @@ -0,0 +1,42 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file BubbleDefinitions.h +//! \ingroup bubble_model +//! \author Martin Bauer +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Type definitions for the bubble_model. +// +//====================================================================================================================== + +#pragma once + +#include "field/GhostLayerField.h" + +namespace walberla +{ +namespace free_surface +{ +namespace bubble_model +{ +using BubbleID = uint32_t; +const uint32_t INVALID_BUBBLE_ID = uint32_t(-1); + +using BubbleField_T = GhostLayerField< BubbleID, 1 >; +using ScalarField_T = GhostLayerField< real_t, 1 >; + +} // namespace bubble_model +} // namespace free_surface +} // namespace walberla diff --git a/src/lbm/free_surface/bubble_model/BubbleDistanceAdaptor.h b/src/lbm/free_surface/bubble_model/BubbleDistanceAdaptor.h new file mode 100644 index 000000000..8f6a24a68 --- /dev/null +++ b/src/lbm/free_surface/bubble_model/BubbleDistanceAdaptor.h @@ -0,0 +1,76 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file BubbleDistanceAdaptor.h +//! \ingroup bubble_model +//! \author Martin Bauer +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Get the distance to a certain bubble ID. +// +//====================================================================================================================== + +#pragma once + +#include "field/adaptors/GhostLayerFieldAdaptor.h" + +#include "DisjoiningPressureBubbleModel.h" + +namespace walberla +{ +namespace free_surface +{ +namespace bubble_model +{ +/*********************************************************************************************************************** + * Get the distance to a certain bubble ID. The search region is limited by maxDistance. + **********************************************************************************************************************/ +class BubbleDistanceAdaptionFunction +{ + public: + using basefield_t = DisjoiningPressureBubbleModel::DistanceField; + using basefield_iterator = basefield_t::const_base_iterator; + using value_type = real_t; + + static const uint_t F_SIZE = 1u; + + BubbleDistanceAdaptionFunction(BubbleID ownBubbleID, real_t maxDistance) + : bubbleID_(ownBubbleID), maxDistance_(maxDistance) + { + WALBERLA_ASSERT_GREATER(maxDistance_, real_c(1.0)); + } + + value_type operator()(const basefield_t& baseField, cell_idx_t x, cell_idx_t y, cell_idx_t z, + cell_idx_t /*f*/ = 0) const + { + WALBERLA_ASSERT_GREATER(maxDistance_, real_c(1.0)); + return baseField.get(x, y, z).getDistanceToNearestBubble(bubbleID_, maxDistance_); + } + + value_type operator()(const basefield_iterator& it) const + { + WALBERLA_ASSERT_GREATER(maxDistance_, real_c(1.0)); + return it->getDistanceToNearestBubble(bubbleID_, maxDistance_); + } + + private: + BubbleID bubbleID_; + real_t maxDistance_; +}; // class BubbleDistanceAdaptionFunction + +using BubbleDistanceAdaptor = field::GhostLayerFieldAdaptor< BubbleDistanceAdaptionFunction, 0 >; + +} // namespace bubble_model +} // namespace free_surface +} // namespace walberla diff --git a/src/lbm/free_surface/bubble_model/BubbleIDFieldPackInfo.h b/src/lbm/free_surface/bubble_model/BubbleIDFieldPackInfo.h new file mode 100644 index 000000000..05b0f0b23 --- /dev/null +++ b/src/lbm/free_surface/bubble_model/BubbleIDFieldPackInfo.h @@ -0,0 +1,147 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file BubbleIDFieldPackInfo.h +//! \ingroup bubble_model +//! \author Martin Bauer +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Pack/unpack information for a field containing bubble IDs. +// +//====================================================================================================================== + +#pragma once + +#include "field/communication/PackInfo.h" + +#include "stencil/D3Q27.h" + +namespace walberla +{ +namespace free_surface +{ +namespace bubble_model +{ +/*********************************************************************************************************************** + * Pack/unpack information for a field containing bubble IDs. + * + * The bubble ID field requires a special pack info, since whenever the ghost layers are updated, the bubble model has + * to look for possible bubble merges. + * + * This could also be implemented with a regular FieldPackInfo and an immediate loop over the ghost layer only. However, + * it is more efficient by directly looking for bubble merges while unpacking the ghost layer, since the elements + * do not have to be loaded twice. + ***********************************************************************************************************************/ +template< typename Stencil_T > +class BubbleIDFieldPackInfo : public field::communication::PackInfo< GhostLayerField< BubbleID, 1 > > +{ + public: + using CommunicationStencil_T = + typename std::conditional< Stencil_T::D == uint_t(2), stencil::D2Q9, stencil::D3Q27 >::type; + + using field_t = GhostLayerField< BubbleID, 1 >; + + BubbleIDFieldPackInfo(const BlockDataID& bdId, MergeInformation* mergeInfo) + : field::communication::PackInfo< field_t >(bdId), mergeInfo_(mergeInfo) + {} + + bool threadsafeReceiving() const override { return false; } + + void unpackData(IBlock* receiver, stencil::Direction dir, mpi::RecvBuffer& buffer) override + { + field_t* const field = receiver->getData< field_t >(this->bdId_); + WALBERLA_ASSERT_NOT_NULLPTR(field); + +#ifndef NDEBUG + uint_t xSize; + uint_t ySize; + uint_t zSize; + buffer >> xSize >> ySize >> zSize; + WALBERLA_ASSERT_EQUAL(xSize, field->xSize()); + WALBERLA_ASSERT_EQUAL(ySize, field->ySize()); + WALBERLA_ASSERT_EQUAL(zSize, field->zSize()); +#endif + + for (auto fieldIt = field->beginGhostLayerOnly(dir); fieldIt != field->end(); ++fieldIt) + { + // update ghost layer with received values + buffer >> *fieldIt; + + // look for bubble merges with bubbles from other blocks, i.e., analyze the just received ghost layer + lookForMerges(fieldIt, dir, field); + } + } + + // communicate bubble IDs locally (between blocks on the same process) and immediately check for bubble merges + void communicateLocal(const IBlock* sender, IBlock* receiver, stencil::Direction dir) override + { + // get sender and receiver fields + const field_t* const senderField = sender->getData< const field_t >(this->bdId_); + field_t* const receiverField = receiver->getData< field_t >(this->bdId_); + + WALBERLA_ASSERT_EQUAL(senderField->xSize(), receiverField->xSize()); + WALBERLA_ASSERT_EQUAL(senderField->ySize(), receiverField->ySize()); + WALBERLA_ASSERT_EQUAL(senderField->zSize(), receiverField->zSize()); + + auto srcIt = senderField->beginSliceBeforeGhostLayer(dir); // iterates only over last slice before ghost layer + auto dstIt = receiverField->beginGhostLayerOnly(stencil::inverseDir[dir]); // iterates only over ghost layer + + while (srcIt != senderField->end()) + { + // fill receiver's ghost layer with values from the sender's outermost inner layer + *dstIt = *srcIt; + + // look for bubble merges with bubbles from other blocks, i.e., analyze the just received ghost layer + lookForMerges(dstIt, stencil::inverseDir[dir], receiverField); + + ++srcIt; + ++dstIt; + } + + WALBERLA_ASSERT(srcIt == senderField->end() && dstIt == receiverField->end()); + } + + protected: + // looks for bubble merges with bubbles from other blocks from the view of a ghost layer; argument "iterator i" + // should iterate ghost layer only + void lookForMerges(const field_t::iterator& i, stencil::Direction dir, const field_t* field) + { + using namespace stencil; + + // only iterate the relevant neighborhood, for example: + // in ghost layer at "W", check all neighbors in directions containing "E" + // in ghost layer at "NE", check all neighbors in directions containing "SW" (=> SW, TSW and BSW) + // in ghost layer at "TNE", only check the neighbor in direction "BSW" + for (uint_t d = uint_c(0); d < uint_c(CommunicationStencil_T::d_per_d_length[inverseDir[dir]]); ++d) + { + const Direction neighborDirection = CommunicationStencil_T::d_per_d[inverseDir[dir]][d]; + auto neighborVal = i.neighbor(neighborDirection); + + // merge only occurs if bubble IDs from both blocks are valid and different + if (neighborVal != INVALID_BUBBLE_ID && *i != INVALID_BUBBLE_ID && neighborVal != *i) + { + Cell neighborCell = i.cell() + Cell(cx[neighborDirection], cy[neighborDirection], cz[neighborDirection]); + + // make sure that the neighboring cell is not in a different ghost layer but part of the domain + if (field->isInInnerPart(neighborCell)) { mergeInfo_->registerMerge(*i, neighborVal); } + } + } + } + + MergeInformation* mergeInfo_; +}; // class BubbleIDFieldPackInfo + +} // namespace bubble_model +} // namespace free_surface +} // namespace walberla diff --git a/src/lbm/free_surface/bubble_model/BubbleModel.h b/src/lbm/free_surface/bubble_model/BubbleModel.h new file mode 100644 index 000000000..a22d540ea --- /dev/null +++ b/src/lbm/free_surface/bubble_model/BubbleModel.h @@ -0,0 +1,240 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file BubbleModel.h +//! \ingroup bubble_model +//! \author Martin Bauer +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief System for tracking pressure/density in gas volumes. +// +//====================================================================================================================== + +#pragma once + +#include "blockforest/StructuredBlockForest.h" +#include "blockforest/communication/UniformBufferedScheme.h" + +#include "core/math/Vector3.h" + +#include "field/GhostLayerField.h" + +#include "stencil/D2Q9.h" +#include "stencil/D3Q27.h" + +#include "Bubble.h" +#include "BubbleDefinitions.h" +#include "FloodFill.h" +#include "MergeInformation.h" +#include "NewBubbleCommunication.h" + +namespace walberla +{ +namespace free_surface +{ +namespace bubble_model +{ +class BubbleModelBase +{ + public: + virtual ~BubbleModelBase() = default; + + virtual real_t getDensity(IBlock* block, const Cell& cell) const = 0; + virtual void setDensity(IBlock* block, const Cell& cell, real_t value) = 0; + virtual void setDensityOfAllBubbles(real_t val) = 0; + + // call updateVolumeDiff() with fillLevelDifference + virtual void reportFillLevelChange(IBlock* block, const Cell& cell, real_t fillLevelDifference) = 0; + virtual void reportLiquidToInterfaceConversion(IBlock* block, const Cell& cell) = 0; + virtual void reportInterfaceToLiquidConversion(IBlock* block, const Cell& cell) = 0; + + virtual void update() = 0; +}; // class BubbleModelBase + +/*********************************************************************************************************************** + * Implementation for setups in which no bubble model is required. Reports always a constant pressure + **********************************************************************************************************************/ +class BubbleModelConstantPressure : public BubbleModelBase +{ + public: + BubbleModelConstantPressure(real_t constantLatticeDensity) : constantLatticeDensity_(constantLatticeDensity) {} + ~BubbleModelConstantPressure() override = default; + + real_t getDensity(IBlock*, const Cell&) const override { return constantLatticeDensity_; } + void setDensity(IBlock*, const Cell&, real_t) override {} + void setDensityOfAllBubbles(real_t val) override { constantLatticeDensity_ = val; } + + void reportFillLevelChange(IBlock*, const Cell&, real_t) override{}; + void reportLiquidToInterfaceConversion(IBlock*, const Cell&) override{}; + void reportInterfaceToLiquidConversion(IBlock*, const Cell&) override{}; + + void update() override {} + + private: + real_t constantLatticeDensity_; +}; // class BubbleModelConstantPressure + +/*********************************************************************************************************************** + * System for tracking pressure/density in gas volumes. + * + * The pure volume of fluid code calculates how mass/fluid moves across the domain. As input it needs the gas density or + * pressure. The density is equal in the same bubble. To track the pressure, individual gas volumes have to be tracked. + * The bubbles can split or merge, can possibly range across multiple blocks and across multiple processes. The handling + * of this is implemented in this class. + **********************************************************************************************************************/ +template< typename Stencil_T > +class BubbleModel : public BubbleModelBase +{ + public: + BubbleModel(const std::shared_ptr< StructuredBlockForest >& blockStorage, bool enableBubbleSplits); + ~BubbleModel() override = default; + + // initialize bubble model from fill level field; bubble model is cleared and bubbles are created in cells with fill + // level less than 1 + void initFromFillLevelField(const ConstBlockDataID& fillField); + + void setDensityOfAllBubbles(real_t rho) override; + + // mark the specified (gas) cell for belonging to the atmosphere bubble with constant pressure; the atmosphere's + // bubble ID is set to the highest ID that was found on any of the processes at cells that belong to the atmosphere; + // WARNING: This function must be called on all processes for the same cells, even if the cells are not located on + // the current block. + void setAtmosphere(const Cell& cellInGlobalCoordinates, real_t constantRho = real_c(1.0)); + + // accessing + real_t getDensity(IBlock* block, const Cell& cell) const override + { + // get the bubble containing cell + const Bubble* bubble = getBubble(block, cell); + WALBERLA_ASSERT_NOT_NULLPTR(bubble, "Cell " << cell << " does not belong to a bubble."); + + return bubble->getDensity(); + } + + void setDensity(IBlock* block, const Cell& cell, real_t value) override + { + // get the bubble containing cell + Bubble* bubble = getBubble(block, cell); + WALBERLA_ASSERT_NOT_NULLPTR(bubble, "Cell " << cell << " does not belong to a bubble."); + + bubble->setDensity(value); + } + + const BubbleID& getBubbleID(IBlock* block, const Cell& cell) const; + BubbleID& getBubbleID(IBlock* block, const Cell& cell); + + // cell and fill level update + void reportFillLevelChange(IBlock* block, const Cell& cell, real_t fillLevelDifference) override; + + // assign a bubble ID (from the first found neighboring cell) to the newly created interface cell; if multiple bubble + // IDs are found in neighborhood, register a merge + void reportLiquidToInterfaceConversion(IBlock* block, const Cell& cell) override; + + // invalidate the bubble ID of the converted liquid cell and check for bubble splits + void reportInterfaceToLiquidConversion(IBlock* block, const Cell& cell) override; + + ConstBlockDataID getBubbleFieldID() const { return bubbleFieldID_; } + + // combine information about a bubble + struct BubbleInfo + { + BubbleInfo() : nrOfCells(uint_c(0)) {} + Vector3< real_t > centerOfMass; + uint_t nrOfCells; + Bubble* bubble; + }; + + // compute bubbleInfo for each bubble and (MPI) reduce it on root + std::vector< BubbleInfo > computeBubbleStats(); + + // write bubbleInfo to terminal on root process + void logBubbleStatsOnRoot(); + + // update the bubble model: + // - communicate bubble ID field + // - merge and split bubbles (involves global MPI communications) + void update() override; + + protected: + const Bubble* getBubble(IBlock* block, const Cell& cell) const; + Bubble* getBubble(IBlock* block, const Cell& cell); + + const std::vector< Bubble >& getBubbles() const { return bubbles_; }; + + // check a 3x3x3 neighborhood whether a bubble could have split; calling is function is relatively inexpensive and + // can be used as indicator whether the more expensive extendedSplitCheck() makes sense + static bool checkForSplit(BubbleField_T* bf, const Cell& cell, BubbleID prevBubbleID); + + // check "neighborhood" cells in each direction around "cell" to ensure that a bubble has really split + static bool extendedSplitCheck(BubbleField_T* bf, const Cell& cell, BubbleID oldBubbleID, + cell_idx_t neighborhood = 2); + + using StencilForSplit_T = + typename std::conditional< Stencil_T::D == uint_t(2), stencil::D2Q9, stencil::D3Q27 >::type; + + // split bubbles, assign new global bubble IDs, and handle potential bubble merges resulting from splitting (involves + // global MPI communication) + void handleSplits(); + + // delete potentially splitting bubbles and create new ones from them; new bubbles are added to newBubbleCom + void markAndCreateSplittedBubbles(NewBubbleCommunication& newBubbleComm, const std::vector< bool >& splitIndicator); + + // in a 3x3x3 neighborhood, find all directions that are connected to startDir (by having same bubbleID) using a + // flood fill algorithm; flood fill is more efficient than simply iterating over all neighbors since the latter would + // require lots of extra logic + static uint32_t mapNeighborhood(BubbleField_T* bf, stencil::Direction startDir, const Cell& cell, BubbleID bubbleID); + + // block storage to access bubbleField and fillField + std::shared_ptr< StructuredBlockStorage > blockStorage_; + + // field that stores every cell's bubble ID; if a cell does not belong to any bubble, its ID is set to + // INVALID_BUBBLE_ID; this field is managed by the BubbleModel and should not be passed outside + BlockDataID bubbleFieldID_; + + // vector that stores all bubbles; it is kept synchronized across all processes + std::vector< Bubble > bubbles_; + + // helper class to manage bubble merges + MergeInformation mergeInformation_; + + // communication scheme for the bubble field + blockforest::communication::UniformBufferedScheme< Stencil_T > bubbleFieldCommunication_; + + // store split information, i.e., hints for splitting; store only hints since merges have to be processed first + struct SplitHint + { + SplitHint(IBlock* _block, const Cell& _cell) : block(_block), cell(_cell) {} + IBlock* block; + Cell cell; + }; + + // vector with outstanding splits that (still) need to be processed + std::vector< SplitHint > splitsToProcess_; + + std::shared_ptr< FloodFillInterface > floodFill_; + + // disable splits to decrease computational costs + bool enableBubbleSplits_; + + inline BubbleField_T* getBubbleField(IBlock* block) const { return block->getData< BubbleField_T >(bubbleFieldID_); } + +}; // class BubbleModel + +} // namespace bubble_model +} // namespace free_surface +} // namespace walberla + +using walberla::free_surface::bubble_model::BubbleModelBase; + +#include "BubbleModel.impl.h" diff --git a/src/lbm/free_surface/bubble_model/BubbleModel.impl.h b/src/lbm/free_surface/bubble_model/BubbleModel.impl.h new file mode 100644 index 000000000..7ee915c2a --- /dev/null +++ b/src/lbm/free_surface/bubble_model/BubbleModel.impl.h @@ -0,0 +1,692 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file BubbleModel.impl.h +//! \ingroup bubble_model +//! \author Martin Bauer +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief System for tracking pressure/density in gas volumes. +// +//====================================================================================================================== + +#include "field/AddToStorage.h" + +#include "lbm/free_surface/InterfaceFromFillLevel.h" + +#include "stencil/D2Q9.h" +#include "stencil/D3Q27.h" + +#include "BubbleIDFieldPackInfo.h" +#include "BubbleModel.h" +#include "RegionalFloodFill.h" + +namespace walberla +{ +namespace free_surface +{ +namespace bubble_model +{ +template< typename Stencil_T > +BubbleModel< Stencil_T >::BubbleModel(const std::shared_ptr< StructuredBlockForest >& blockStorage, + bool enableBubbleSplits) + : blockStorage_(blockStorage), bubbleFieldID_(field::addToStorage< BubbleField_T >( + blockStorage, "BubbleIDs", BubbleID(INVALID_BUBBLE_ID), field::fzyx, uint_c(1))), + bubbleFieldCommunication_(blockStorage), enableBubbleSplits_(enableBubbleSplits) +{ + bubbleFieldCommunication_.addPackInfo( + std::make_shared< BubbleIDFieldPackInfo< Stencil_T > >(bubbleFieldID_, &mergeInformation_)); +} + +template< typename Stencil_T > +void BubbleModel< Stencil_T >::initFromFillLevelField(const ConstBlockDataID& fillFieldID) +{ + // mark regions belonging to the same bubble + floodFill_ = std::make_shared< FloodFillUsingFillLevel< Stencil_T > >(fillFieldID); + + bubbles_.clear(); + + NewBubbleCommunication newBubbleComm; + + // start numbering the bubbles from 0 + BubbleID firstNewBubbleID = 0; + BubbleID nextID = firstNewBubbleID; + + for (auto blockIt = blockStorage_->begin(); blockIt != blockStorage_->end(); ++blockIt) + { + // get fields + const ScalarField_T* const fillField = blockIt->getData< const ScalarField_T >(fillFieldID); + BubbleField_T* const bubbleField = blockIt->getData< BubbleField_T >(bubbleFieldID_); + + // initialize bubbleField with invalid IDs + bubbleField->set(INVALID_BUBBLE_ID); + + auto bubbleIt = bubbleField->begin(); + auto fillFieldIt = fillField->begin(); + + while (bubbleIt != bubbleField->end()) + { + // only consider cells + // - that are either gas or interface + // - for which no bubble ID is set yet (each call to floodFill_->run() sets new bubbleIDs to cells) + if ((*fillFieldIt < real_c(1) || isInterfaceFromFillLevel< Stencil_T >(*fillField, bubbleIt.cell())) && + *bubbleIt == INVALID_BUBBLE_ID) + { + real_t volume; + uint_t nrOfCells; + + // set (block local, preliminary) bubble IDs in the bubble field + floodFill_->run(*blockIt, bubbleFieldID_, bubbleIt.cell(), nextID++, volume, nrOfCells); + + // create new bubble with preliminary bubbleIDs; the final global bubbleIDs are set in communicateAndApply() + newBubbleComm.createBubble(Bubble(volume)); + } + + ++bubbleIt; + ++fillFieldIt; + } + } + + // set global bubble IDs + newBubbleComm.communicateAndApply(bubbles_, *blockStorage_, bubbleFieldID_); + + // clear merge information, i.e., make sure that no merge is already registered + mergeInformation_.resizeAndClear(bubbles_.size()); + + // communicate bubble field IDs + bubbleFieldCommunication_(); + + // communicate bubble merges + mergeInformation_.communicateMerges(); + + if (mergeInformation_.hasMerges()) + { + // merge bubbles (add bubble volumes and delete/rename bubble IDs) + mergeInformation_.mergeAndReorderBubbleVector(bubbles_); + + // rename bubble IDs on all blocks + for (auto blockIt = blockStorage_->begin(); blockIt != blockStorage_->end(); ++blockIt) + mergeInformation_.renameOnBubbleField(blockIt->getData< BubbleField_T >(bubbleFieldID_)); + } + + // clear merge information after bubbles have been merged + mergeInformation_.resizeAndClear(bubbles_.size()); +} + +template< typename Stencil_T > +void BubbleModel< Stencil_T >::setDensityOfAllBubbles(real_t rho) +{ + for (auto it = bubbles_.begin(); it != bubbles_.end(); ++it) + { + if (!it->hasConstantDensity()) { it->setDensity(rho); } + } +} + +template< typename Stencil_T > +void BubbleModel< Stencil_T >::setAtmosphere(const Cell& cellInGlobalCoordinates, real_t rho) +{ + // temporarily set the atmosphere's bubble ID to invalid + BubbleID atmosphereBubbbleID = INVALID_BUBBLE_ID; + + // get the cell's block (or nullptr if the block is not on this process) + IBlock* blockWithAtmosphereBubble = blockStorage_->getBlock(cellInGlobalCoordinates); + + // set atmosphere bubble ID to this cell's bubble ID + if (blockWithAtmosphereBubble) // else: block does not exist locally + { + Cell localCell; + blockStorage_->transformGlobalToBlockLocalCell(localCell, *blockWithAtmosphereBubble, cellInGlobalCoordinates); + + const BubbleField_T* const bf = blockWithAtmosphereBubble->getData< const BubbleField_T >(bubbleFieldID_); + + // get this cell's bubble ID + atmosphereBubbbleID = bf->get(localCell); + + // cell must be a gas cell; therefore, a valid bubble ID must be set + WALBERLA_ASSERT_UNEQUAL(atmosphereBubbbleID, INVALID_BUBBLE_ID); + } + + // variable for (MPI) reducing the bubble ID; value of -1 is set in order to ignore this bubble in maximum reduction + int reducedBubbleID = atmosphereBubbbleID != INVALID_BUBBLE_ID ? int_c(atmosphereBubbbleID) : -1; + + // get the highest of all processes' bubble IDs + WALBERLA_MPI_SECTION() + { + MPI_Allreduce(MPI_IN_PLACE, &reducedBubbleID, 1, MPITrait< int >::type(), MPI_MAX, MPI_COMM_WORLD); + } + + if (reducedBubbleID < 0) + { + WALBERLA_LOG_WARNING_ON_ROOT("Could not set atmosphere in the non-gas cell " << cellInGlobalCoordinates); + } + else + { + // set atmosphere bubble ID to highest of all processes' bubble IDs + atmosphereBubbbleID = BubbleID(reducedBubbleID); + bubbles_[atmosphereBubbbleID].setConstantDensity(rho); + } +} + +template< typename Stencil_T > +const Bubble* BubbleModel< Stencil_T >::getBubble(IBlock* blockIt, const Cell& cell) const +{ + const BubbleField_T* bf = getBubbleField(blockIt); + const BubbleID id = bf->get(cell); + + return (id == INVALID_BUBBLE_ID) ? nullptr : &bubbles_[id]; +} + +template< typename Stencil_T > +Bubble* BubbleModel< Stencil_T >::getBubble(IBlock* blockIt, const Cell& cell) +{ + const BubbleField_T* bf = getBubbleField(blockIt); + const BubbleID id = bf->get(cell); + + return (id == INVALID_BUBBLE_ID) ? nullptr : &bubbles_[id]; +} + +template< typename Stencil_T > +const BubbleID& BubbleModel< Stencil_T >::getBubbleID(IBlock* blockIt, const Cell& cell) const +{ + const BubbleField_T* bf = getBubbleField(blockIt); + return bf->get(cell); +} + +template< typename Stencil_T > +BubbleID& BubbleModel< Stencil_T >::getBubbleID(IBlock* blockIt, const Cell& cell) +{ + BubbleField_T* bf = getBubbleField(blockIt); + return bf->get(cell); +} + +template< typename Stencil_T > +void BubbleModel< Stencil_T >::reportFillLevelChange(IBlock* blockIt, const Cell& cell, real_t fillLevelDifference) +{ + Bubble* b = getBubble(blockIt, cell); + WALBERLA_ASSERT_NOT_NULLPTR(b, + "Reporting fill level change in cell " << cell << " where no bubble ID is registered."); + + // update the bubble volume change; fillLevelDifference is negated because variable is: + // - positive if fill level increased => bubble volume has to decrease + // - negative if fill level decreased => bubble volume has to increase + if (b) { b->updateVolumeDiff(-fillLevelDifference); } +} + +template< typename Stencil_T > +void BubbleModel< Stencil_T >::reportLiquidToInterfaceConversion(IBlock* blockIt, const Cell& cell) +{ + // get bubble field + BubbleField_T* bf = getBubbleField(blockIt); + + // get this cell's bubble ID + BubbleID& thisCellID = bf->get(cell); + + // this cell is converted from liquid to interface; liquid cells have no bubble ID such that this cell can not have + // a bubble ID, yet + WALBERLA_ASSERT_EQUAL(thisCellID, INVALID_BUBBLE_ID); + + // iterate neighborhood and assign the first found bubble ID to this new interface cell + using SearchStencil_T = typename std::conditional< Stencil_T::D == uint_t(2), stencil::D2Q9, stencil::D3Q27 >::type; + for (auto d = SearchStencil_T::beginNoCenter(); d != SearchStencil_T::end(); ++d) + { + // get bubble ID of neighboring cell + BubbleID neighborID = bf->get(cell[0] + d.cx(), cell[1] + d.cy(), cell[2] + d.cz()); + if (neighborID != INVALID_BUBBLE_ID) + { + // assign the first found neighbor's bubble ID to this cell + if (thisCellID == INVALID_BUBBLE_ID) { thisCellID = neighborID; } + else + { + // if multiple different bubble IDs are in neighborhood, trigger merging + if (thisCellID != neighborID) { mergeInformation_.registerMerge(thisCellID, neighborID); } + } + } + } +} + +template< typename Stencil_T > +void BubbleModel< Stencil_T >::reportInterfaceToLiquidConversion(IBlock* blockIt, const Cell& cell) +{ + // get bubble field + BubbleField_T* bf = getBubbleField(blockIt); + + // get this cell's bubble ID + BubbleID oldBubbleID = bf->get(cell); + + // this cell is converted from interface to liquid; the interface cell must already have a valid bubble ID + WALBERLA_ASSERT_UNEQUAL(oldBubbleID, INVALID_BUBBLE_ID); + + // invalidate the converted cell's bubble ID (liquid cells must not have a bubble ID) + bf->get(cell) = INVALID_BUBBLE_ID; + + if (enableBubbleSplits_) + { + // check a 3x3x3 neighborhood whether a bubble could have split + if (checkForSplit(bf, cell, oldBubbleID)) + { + WALBERLA_LOG_INFO("Possible bubble split detected due to conversion in cell " << cell << "."); + + // check a larger neighborhood to ensure that the bubble has really split + if (extendedSplitCheck(bf, cell, oldBubbleID, cell_idx_c(3))) + { + WALBERLA_LOG_INFO("Extended split check confirmed split."); + // register this bubble split + splitsToProcess_.emplace_back(blockIt, cell); + } + else { WALBERLA_LOG_INFO("Extended split check ruled out split."); } + } + } +} + +template< typename Stencil_T > +void BubbleModel< Stencil_T >::update() +{ + // communicate field with bubble IDs + bubbleFieldCommunication_(); + + // vector for (MPI) reducing each bubble's volume change and indicators for whether merges and splits occurred + static std::vector< real_t > reduceVector; + + // indicators that are (MPI) reduced to identify merges and splits + real_t mergeIndicator = mergeInformation_.hasMerges() ? real_c(1) : real_c(0); + real_t splitIndicator = splitsToProcess_.empty() ? real_c(0) : real_c(1); + + // extend the vector for storing the merge and split indicator + reduceVector.resize(bubbles_.size() + 2); + + uint_t i = uint_c(0); + for (; i < bubbles_.size(); ++i) + { + // get each bubble's volume change + reduceVector[i] = bubbles_[i].getAndResetVolumeDiff(); + } + + // append the indicators at the end of reduceVector + reduceVector[i++] = mergeIndicator; + reduceVector[i++] = splitIndicator; + WALBERLA_ASSERT_EQUAL(i, reduceVector.size()); // make sure that indexing is correct + + WALBERLA_MPI_SECTION() + { + // globally (MPI) reduce each bubble's volume change, the number of merges, and the number of splits + MPI_Allreduce(MPI_IN_PLACE, &reduceVector[0], int_c(reduceVector.size()), MPITrait< real_t >::type(), MPI_SUM, + MPI_COMM_WORLD); + } + + uint_t j = uint_c(0); + for (; j < bubbles_.size(); ++j) + { + // update each bubble's volume and density + bubbles_[j].applyVolumeDiff(reduceVector[j]); + } + + // check for merges and splits + bool mergeHappened = (reduceVector[j++] > real_c(0)); + bool splitHappened = (reduceVector[j++] > real_c(0)); + WALBERLA_ASSERT_EQUAL(j, reduceVector.size()); // make sure that indexing is correct + + // treat bubble merges + if (mergeHappened) + { + WALBERLA_ROOT_SECTION() + { + // std::stringstream ss; + // mergeInformation_.print(ss); + // WALBERLA_LOG_INFO("Merge detected, " << ss.str()); + WALBERLA_LOG_INFO("Merge detected"); + } + + // globally communicate bubble merges and rename bubble IDs accordingly + mergeInformation_.communicateMerges(); + + // merge bubbles + mergeInformation_.mergeAndReorderBubbleVector(bubbles_); + + // update, i.e., rename bubble IDs in the bubble ID field + for (auto blockIt = blockStorage_->begin(); blockIt != blockStorage_->end(); ++blockIt) + { + mergeInformation_.renameOnBubbleField(getBubbleField(&(*blockIt))); + } + } + + // treat bubble splits + if (splitHappened) { handleSplits(); } + + mergeInformation_.resizeAndClear(bubbles_.size()); + splitsToProcess_.clear(); +} + +template< typename Stencil_T > +bool BubbleModel< Stencil_T >::checkForSplit(BubbleField_T* bf, const Cell& cell, BubbleID previousBubbleID) +{ + using namespace stencil; + + // Variable neighborHoodInfo has bits set in each connected neighboring direction where the cell belongs to bubble + // previousBubbleID (=ID of the bubble that got "lost" by converting cell from interface to liquid). The + // neighborHoodInfo is built via flood fill exactly once from a starting direction (e.g., first direction in which + // previousBubbleID was found). Thus, the connected neighborhood is built for only one direction and cells that + // are not connected to this starting direction are not part of the connected neighborhood. A split occurs, i.e., is + // detected when previousBubbleID is found in an unconnected region. + // Example: + // previousBubbleID is 1, the cells in the center were converted from bubble ID=1 to liquid (ID=f) + // 1 1 1 + // f f f + // 1 1 1 + // The first direction in which previousBubbleID is set shall be N (north). The connected neighborhood is then built + // (c=connected, n=not connected): + // c c c + // n n n + // n n n + // At neighbor N, neighbors NW and NE are found: no split is detected since we are in the connected neighborhood. + // Then, neighbor S is found. Since S is not in the (first) connected neighborhood, a split is detected. + uint32_t neighborHoodInfo = uint32_c(0); + + for (auto d = StencilForSplit_T::beginNoCenter(); d != StencilForSplit_T::end(); ++d) + { + // get the bubble ID of the neighboring cell + BubbleID neighborID = bf->getNeighbor(cell, *d); + + // neighboring bubble is a different one (or no bubble) than this cell's bubble + if (neighborID != previousBubbleID) { continue; } + // => from here: bubbles are the same, i.e., neighborID == previousBubbleID + + if (neighborHoodInfo > uint32_c(0)) // the neighborhood map has already been created + { + // "connected" bit is set in this direction, i.e., the neighbor is connected + if (neighborHoodInfo & dirToBinary[*d]) { continue; } + else // "connected" bit is not set in this direction, i.e., the neighbor is not connected + { + // since neighborID == previousBubbleID but neighbor is not connected, a split had to occur + return true; + } + } + else // the neighborhood map has not been created, yet + { + // create connected neighborhood starting from direction d + neighborHoodInfo = mapNeighborhood(bf, *d, cell, neighborID); + } + } + return false; +} + +template< typename Stencil_T > +bool BubbleModel< Stencil_T >::extendedSplitCheck(BubbleField_T* bf, const Cell& cell, BubbleID previousBubbleID, + cell_idx_t neighborhood) +{ + // RegionalFloodFill is used to find connected regions in a larger (>3x3x3) neighborhood + RegionalFloodFill< BubbleID, StencilForSplit_T >* neighborHoodInfo = nullptr; + + for (auto d = StencilForSplit_T::beginNoCenter(); d != StencilForSplit_T::end(); ++d) + { + // get the bubble ID of the neighboring cell + BubbleID neighborID = bf->getNeighbor(cell, *d); + + // neighboring bubble is a different one (or no bubble) than this cell's bubble + if (neighborID != previousBubbleID) { continue; } + // => from here: bubbles are the same, i.e., neighborID == previousBubbleID + + if (neighborHoodInfo) // the neighborhood map has already been created + { + if (neighborHoodInfo->connected(*d)) { continue; } + else // bubble is not connected in direction d and a split occurred + { + delete neighborHoodInfo; + return true; + } + } + else // the neighborhood map has not been created, yet + { + // create connected neighborhood starting from direction d + neighborHoodInfo = + new RegionalFloodFill< BubbleID, StencilForSplit_T >(bf, cell, *d, neighborID, neighborhood); + } + } + + delete neighborHoodInfo; + return false; +} + +template< typename Stencil_T > +uint32_t BubbleModel< Stencil_T >::mapNeighborhood(BubbleField_T* bf, stencil::Direction startDir, const Cell& cell, + BubbleID bubbleID) +{ + using namespace stencil; + + uint32_t result = uint32_c(0); + + // use stack to store directions that still need to be searched + std::vector< Direction > stack; + stack.push_back(startDir); + + while (!stack.empty()) + { + // next search direction is the last entry in stack + Direction d = stack.back(); + + // remove the current search direction from stack + stack.pop_back(); + + WALBERLA_ASSERT(d != C); // do not search in center direction + WALBERLA_ASSERT(bf->get(cell[0] + cx[d], cell[1] + cy[d], cell[2] + cz[d]) == + bubbleID); // cell must belong to the same bubble + + // add this direction to result, i.e., to the "connected neighborhood" using bitwise OR + result |= dirToBinary[d]; + + // in direction d, iterate over d's neighboring directions i, i.e., iterate over a (at maximum) 3x3x3 neighborhood + // from the viewpoint of cell + for (uint_t i = uint_c(1); i < StencilForSplit_T::dir_neighbors_length[d]; ++i) + { + // transform direction d's neighbor in direction i to a direction from the viewpoint of cell, e.g., d=N, i=W => + // nDir=NW + Direction nDir = StencilForSplit_T::dir_neighbors[d][i]; + + // cell in direction nDir belongs to the bubble and is not already in result + if (bf->get(cell[0] + cx[nDir], cell[1] + cy[nDir], cell[2] + cz[nDir]) == bubbleID && + (result & dirToBinary[nDir]) == 0) + { + // add nDir to stack such that it gets added to result and used as start cell in the next iteration; + // the connected bit will never be set for directions that are not connected to startDir + stack.push_back(nDir); + } + } + } + + return result; +} + +template< typename Stencil_T > +void BubbleModel< Stencil_T >::handleSplits() +{ + // splitIndicator is set to true for bubbles for which a split was registered; this can not be done earlier since + // bubble IDs may have changed during merging + std::vector< bool > splitIndicator(bubbles_.size()); + + // process all registered splits + for (auto i = splitsToProcess_.begin(); i != splitsToProcess_.end(); ++i) + { + BubbleField_T* bubbleField = getBubbleField(i->block); + + const Cell& c = i->cell; + + // cell c was transformed from interface to liquid and has no valid bubble ID anymore; mark remaining gas cells + // with valid bubble IDs for being split + for (auto d = StencilForSplit_T::begin(); d != StencilForSplit_T::end(); ++d) + { + BubbleID id = bubbleField->get(c.x() + d.cx(), c.y() + d.cy(), c.z() + d.cz()); + + // mark bubble's cell for splitting in splitIndicator + if (id != INVALID_BUBBLE_ID) { splitIndicator[id] = true; } + } + } + + // communicate (MPI reduce) splitIndicator among all processes + allReduceInplace(splitIndicator, mpi::BITWISE_OR); + + // create communication for new bubbles + NewBubbleCommunication newBubbleComm(bubbles_.size()); + + // treat split and create new (splitted) bubbles with new IDs + markAndCreateSplittedBubbles(newBubbleComm, splitIndicator); + + // communicate all bubbles and assign new global bubble IDs + newBubbleComm.communicateAndApply(bubbles_, splitIndicator, *blockStorage_, bubbleFieldID_); + + // clear merge information + mergeInformation_.resizeAndClear(bubbles_.size()); + + // communicate bubble field + bubbleFieldCommunication_(); + + // communicate merges + mergeInformation_.communicateMerges(); + + // merge new splitted bubbles (merges can occur at the block border after the bubbleField was communicated; + // bubbleFieldCommunication is linked to mergeInformation_ to report the merges there) + if (mergeInformation_.hasMerges()) + { + // merge bubbles + mergeInformation_.mergeAndReorderBubbleVector(bubbles_); + for (auto blockIt = blockStorage_->begin(); blockIt != blockStorage_->end(); ++blockIt) + { + // assign new bubble IDs + mergeInformation_.renameOnBubbleField(blockIt->getData< BubbleField_T >(bubbleFieldID_)); + } + } + + // clear merge information + mergeInformation_.resizeAndClear(bubbles_.size()); +} + +template< typename Stencil_T > +void BubbleModel< Stencil_T >::markAndCreateSplittedBubbles(NewBubbleCommunication& newBubbleComm, + const std::vector< bool >& splitIndicator) +{ + for (auto blockIt = blockStorage_->begin(); blockIt != blockStorage_->end(); ++blockIt) + { + BubbleField_T* bubbleField = getBubbleField(&(*blockIt)); + + // loop over the whole domain and start a flood fill at positions where bubbles are marked in splitIndicator + for (auto bubbleIt = bubbleField->begin(); bubbleIt != bubbleField->end(); ++bubbleIt) + { + // skip cells that + // - do not belong to a bubble + // - belong to a bubble that might have been created during this function call (i.e., this bubble ID is not yet + // known to the vector spliIndicator) + // - belong to a bubble for which no split was detected + if (*bubbleIt == INVALID_BUBBLE_ID || *bubbleIt >= splitIndicator.size() || !splitIndicator[*bubbleIt]) + { + continue; + } + + const Cell& curCell = bubbleIt.cell(); + real_t densityOfOldBubble = getDensity(&(*blockIt), curCell); + real_t volume; + uint_t nrOfCells; + + // mark the whole region of the bubble (to which this cells belongs) in bubbleField + floodFill_->run(*blockIt, bubbleFieldID_, curCell, newBubbleComm.nextFreeBubbleID(), volume, nrOfCells); + + // create new bubble + newBubbleComm.createBubble(Bubble(volume, densityOfOldBubble)); + } + } +} + +template< typename Stencil_T > +std::vector< typename BubbleModel< Stencil_T >::BubbleInfo > BubbleModel< Stencil_T >::computeBubbleStats() +{ + std::vector< BubbleInfo > bubbleStats; + bubbleStats.assign(bubbles_.size(), BubbleInfo()); + + // iterate all bubbles on each block + for (auto blockIt = blockStorage_->begin(); blockIt != blockStorage_->end(); ++blockIt) + { + const BubbleField_T* bubbleField = getBubbleField(&(*blockIt)); + WALBERLA_FOR_ALL_CELLS(bubbleFieldIt, bubbleField, { + if (*bubbleFieldIt == INVALID_BUBBLE_ID) { continue; } + + Vector3< real_t > cellCenter; + Cell globalCell; + blockStorage_->transformBlockLocalToGlobalCell(globalCell, *blockIt, bubbleFieldIt.cell()); + blockStorage_->getCellCenter(cellCenter[0], cellCenter[1], cellCenter[2], globalCell); + + // center of mass of this bubble on this block + bubbleStats[*bubbleFieldIt].centerOfMass += cellCenter; + + // bubble's number of cells + bubbleStats[*bubbleFieldIt].nrOfCells++; + }) // WALBERLA_FOR_ALL_CELLS + } + + // store bubble information in reduceVec; reducing a vector of real_t is significantly easier than reducing + // bubbleStats (vector of bubbleInfo) + std::vector< real_t > reduceVec; + for (auto statsIter = bubbleStats.begin(); statsIter != bubbleStats.end(); ++statsIter) + { + reduceVec.push_back(statsIter->centerOfMass[0]); + reduceVec.push_back(statsIter->centerOfMass[1]); + reduceVec.push_back(statsIter->centerOfMass[2]); + reduceVec.push_back(real_c(statsIter->nrOfCells)); + } + + // (MPI) reduce bubble information on root + mpi::reduceInplace(reduceVec, mpi::SUM, 0, MPI_COMM_WORLD); + + WALBERLA_ROOT_SECTION() + { + uint_t idx = uint_c(0); + uint_t i = uint_c(0); + for (auto statsIter = bubbleStats.begin(); statsIter != bubbleStats.end(); ++statsIter) + { + statsIter->centerOfMass[0] = reduceVec[idx++]; + statsIter->centerOfMass[1] = reduceVec[idx++]; + statsIter->centerOfMass[2] = reduceVec[idx++]; + statsIter->nrOfCells = uint_c(reduceVec[idx++]); + + // compute the bubble's global center of mass + statsIter->centerOfMass /= real_c(statsIter->nrOfCells); + + // store the bubble ID + statsIter->bubble = &(bubbles_[i]); + ++i; + } + WALBERLA_ASSERT_EQUAL(idx, reduceVec.size()); + + return bubbleStats; + } + else // else belongs to macro WALBERLA_ROOT_SECTION() + { + return std::vector< BubbleInfo >(); + } +} + +template< typename Stencil_T > +void BubbleModel< Stencil_T >::logBubbleStatsOnRoot() +{ + std::vector< BubbleInfo > bubbleStats = computeBubbleStats(); + + WALBERLA_ROOT_SECTION() + { + WALBERLA_LOG_RESULT("Bubble Status:"); + for (auto it = bubbleStats.begin(); it != bubbleStats.end(); ++it) + { + WALBERLA_LOG_RESULT("\tPosition:" << it->centerOfMass << " #Cells: " << it->nrOfCells); + } + } +} + +} // namespace bubble_model +} // namespace free_surface +} // namespace walberla diff --git a/src/lbm/free_surface/bubble_model/BubbleModelFromConfig.h b/src/lbm/free_surface/bubble_model/BubbleModelFromConfig.h new file mode 100644 index 000000000..dfd86821b --- /dev/null +++ b/src/lbm/free_surface/bubble_model/BubbleModelFromConfig.h @@ -0,0 +1,75 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file BubbleModelFromConfig.h +//! \ingroup bubble_model +//! \author Martin Bauer +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Create and initialize BubbleModel with information from a config object. +// +//====================================================================================================================== + +#pragma once + +#include "core/config/Config.h" + +#include "BubbleModel.h" + +namespace walberla +{ +namespace free_surface +{ +namespace bubble_model +{ +/*********************************************************************************************************************** +* Create and initialize BubbleModel with information from a config object. +* +* Example configuration block: +* \verbatim + { + // For scenarios where no splits can occur - expensive split detection can be switched off - defaults to False + enableBubbleSplits False; + + // optional atmosphere block(s) : Atmosphere bubbles are bubbles with constant pressure + atmosphere { + position < 1.5, 175.5, 1 >; // a point inside the bubble that should becomes atmosphere + density 1.1; // the value of constant pressure. Default value: 1.0 + } + + // optional disjoining pressure + // Disjoining pressure model holds bubbles apart that are close to each other + DisjoiningPressure + { + maxDisjoiningPressure 0.1; // maximum value of disjoining pressure - defaults to 0.2 + maxDistance 4; // for bubbles with distance greater 'maxDistance' cells + // there is no disjoining pressure - defaults to 10 cells + } + } + \endverbatim +* +* +* The fill level field must be fully initialized. +* If the handle of the config block returns nullptr, the bubble model is created with default values. +***********************************************************************************************************************/ +template< typename Stencil_T > +std::shared_ptr< BubbleModelBase > createFromConfig(const std::shared_ptr< StructuredBlockForest >& blockStorage, + ConstBlockDataID fillFieldID, + const Config::BlockHandle& configBlock = Config::BlockHandle()); + +} // namespace bubble_model +} // namespace free_surface +} // namespace walberla + +#include "BubbleModelFromConfig.impl.h" \ No newline at end of file diff --git a/src/lbm/free_surface/bubble_model/BubbleModelFromConfig.impl.h b/src/lbm/free_surface/bubble_model/BubbleModelFromConfig.impl.h new file mode 100644 index 000000000..4ffa219cb --- /dev/null +++ b/src/lbm/free_surface/bubble_model/BubbleModelFromConfig.impl.h @@ -0,0 +1,91 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file BubbleModelFromConfig.impl.h +//! \ingroup bubble_model +//! \author Martin Bauer +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Create and initialize BubbleModel with information from a config object. +// +//====================================================================================================================== + +#include "BubbleModelFromConfig.h" +#include "DisjoiningPressureBubbleModel.h" + +namespace walberla +{ +namespace free_surface +{ +namespace bubble_model +{ +template< typename Stencil_T > +std::shared_ptr< BubbleModelBase > createFromConfig(const std::shared_ptr< StructuredBlockForest >& blockForest, + ConstBlockDataID fillFieldID, + const Config::BlockHandle& configBlock) +{ + if (!configBlock) + { + auto bubbleModel = std::make_shared< BubbleModel< Stencil_T > >(blockForest); + bubbleModel->initFromFillLevelField(fillFieldID); + return bubbleModel; + } + + bool enableBubbleSplits = configBlock.getParameter< bool >("enableBubbleSplits", false); + + std::shared_ptr< BubbleModel< Stencil_T > > bubbleModel; + + real_t constantLatticeDensity = configBlock.getParameter< real_t >("constantLatticeDensity", real_c(-1)); + if (constantLatticeDensity > 0) { return std::make_shared< BubbleModelConstantPressure >(constantLatticeDensity); } + + auto disjoiningPressureCfg = configBlock.getBlock("DisjoiningPressure"); + if (disjoiningPressureCfg) + { + const real_t maxDistance = disjoiningPressureCfg.getParameter< real_t >( + "maxBubbleDistance"); // d_range in the paper from Koerner et al., 2005 + const real_t disjoiningPressureConstant = disjoiningPressureCfg.getParameter< real_t >( + "disjoiningPressureConstant"); // c_pi in the paper from Koerner et al., 2005 + const uint_t distFieldUpdateInter = disjoiningPressureCfg.getParameter< uint_t >("distanceFieldUpdateInterval"); + + bubbleModel = std::make_shared< DisjoiningPressureBubbleModel< Stencil_T > >( + blockForest, maxDistance, disjoiningPressureConstant, enableBubbleSplits, distFieldUpdateInter); + + WALBERLA_LOG_PROGRESS_ON_ROOT("Using Disjoining Pressure BubbleModel"); + } + else + { + bubbleModel = std::make_shared< bubble_model::BubbleModel< Stencil_T > >(blockForest, enableBubbleSplits); + WALBERLA_LOG_PROGRESS_ON_ROOT("Using Standard BubbleModel"); + } + + bubbleModel->initFromFillLevelField(fillFieldID); + + // get all atmosphere blocks + std::vector< Config::BlockHandle > atmosphereBlocks; + configBlock.getBlocks("atmosphere", atmosphereBlocks); + for (auto it = atmosphereBlocks.begin(); it != atmosphereBlocks.end(); ++it) + { + Vector3< real_t > position = it->getParameter< Vector3< real_t > >("position"); + real_t density = it->getParameter< real_t >("density", 1.0); + Cell cell; + blockForest->getCell(cell, position[0], position[1], position[2]); + bubbleModel->setAtmosphere(cell, density); + } + + return bubbleModel; +} + +} // namespace bubble_model +} // namespace free_surface +} // namespace walberla diff --git a/src/lbm/free_surface/bubble_model/CMakeLists.txt b/src/lbm/free_surface/bubble_model/CMakeLists.txt new file mode 100644 index 000000000..712930e38 --- /dev/null +++ b/src/lbm/free_surface/bubble_model/CMakeLists.txt @@ -0,0 +1,24 @@ +target_sources( lbm + PRIVATE + Bubble.h + BubbleDefinitions.h + BubbleDistanceAdaptor.h + BubbleIDFieldPackInfo.h + BubbleModel.h + BubbleModel.impl.h + BubbleModelFromConfig.h + BubbleModelFromConfig.impl.h + DisjoiningPressureBubbleModel.h + DisjoiningPressureBubbleModel.impl.h + DistanceInfo.cpp + DistanceInfo.h + FloodFill.h + FloodFill.impl.h + Geometry.h + Geometry.impl.h + MergeInformation.cpp + MergeInformation.h + NewBubbleCommunication.cpp + NewBubbleCommunication.h + RegionalFloodFill.h + ) diff --git a/src/lbm/free_surface/bubble_model/DisjoiningPressureBubbleModel.h b/src/lbm/free_surface/bubble_model/DisjoiningPressureBubbleModel.h new file mode 100644 index 000000000..278536fee --- /dev/null +++ b/src/lbm/free_surface/bubble_model/DisjoiningPressureBubbleModel.h @@ -0,0 +1,142 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file DisjoiningPressureBubbleModel.h +//! \ingroup bubble_model +//! \author Daniela Anderl +//! \author Martin Bauer +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Bubble Model with additional pressure term (disjoining pressure). +// +//====================================================================================================================== + +#pragma once + +#include "field/GhostLayerField.h" + +#include <cmath> + +#include "BubbleModel.h" +#include "DistanceInfo.h" + +namespace walberla +{ +namespace free_surface +{ +namespace bubble_model +{ +/*********************************************************************************************************************** + * Bubble Model with additional pressure term (disjoining pressure) which depends on distance to the nearest bubble. The + * disjoining pressure is computed as in the paper from Koerner et al., 2005, where variable + * - "disjPressConst_" is called "c_pi" + * - "maxDistance_" is called "d_range" + * - "dist" is called "d_int". + * + * Be aware that the value of the first two variables are phenomenological, i.e., they have to be chosen according to + * comparisons with experiments. The current default values are more or less randomly chosen. + **********************************************************************************************************************/ +template< typename Stencil_T > +class DisjoiningPressureBubbleModel : public BubbleModel< Stencil_T > +{ + public: + using DistanceField_T = GhostLayerField< DistanceInfo, 1 >; + + explicit DisjoiningPressureBubbleModel(const std::shared_ptr< StructuredBlockForest >& blockStorage, + const real_t maxDistance = real_c(7), + const real_t disjPressConst = real_c(0.05), bool enableBubbleSplits = true, + uint_t distanceFieldUpdateInterval = uint_c(1)); + + ~DisjoiningPressureBubbleModel() override = default; + + // compute the gas density with disjoining pressure according to the paper from Koerner et al., 2005 + real_t getDensity(IBlock* block, const Cell& cell) const override + { + // get bubble field and bubble ID + const BubbleField_T* bf = BubbleModel< Stencil_T >::getBubbleField(block); + const BubbleID id = bf->get(cell); + + WALBERLA_ASSERT_UNEQUAL(id, INVALID_BUBBLE_ID, "Cell " << cell << " does not contain a bubble."); + + const real_t gasDensity = BubbleModel< Stencil_T >::bubbles_[id].getDensity(); + + // cache a pointer to the block and to the distance field since block->getData<DistanceField>() is expensive + static IBlock* blockCache = nullptr; + static DistanceField_T* distField = nullptr; + + // update cache + if (block != blockCache) + { + blockCache = block; + distField = block->getData< DistanceField_T >(distanceFieldSrcID_); + } + + const DistanceInfo& distanceInfo = distField->get(cell); + + // get the distance to the nearest neighboring interface cell from a different bubble + const real_t dist = distanceInfo.getDistanceToNearestBubble(id, maxDistance_); + + real_t disjoiningPressure = real_c(0.0); + + // computation of disjoining pressure according to the paper from Koerner et al., 2005 where variable + // - "disjPressConst_" is called "c_pi" + // - "maxDistance_" is called "d_range" + // - "dist" is called "d_int" + if (dist > maxDistance_) + { + // disjoining pressure is zero for bubbles outside of range "maxDistance" + disjoiningPressure = real_c(0); + } + else + { + // disjoining pressure as Koerner et al., 2005 + disjoiningPressure = disjPressConst_ * real_c(std::fabs(maxDistance_ - dist)); + } + + return gasDensity - disjoiningPressure; + } + + // update the bubble model + void update() override + { + static uint_t step = uint_c(0); + + // update the distance field + if (step % distanceFieldUpdateInterval_ == 0) updateDistanceField(); + + // update regular bubble model + BubbleModel< Stencil_T >::update(); + + ++step; + } + + ConstBlockDataID getDistanceFieldID() { return distanceFieldSrcID_; } + + protected: + void updateDistanceField(); + + real_t maxDistance_; // d_range in the paper from Koerner et al., 2005 + real_t disjPressConst_; // c_pi in the paper from Koerner et al., 2005 + + BlockDataID distanceFieldSrcID_; + BlockDataID distanceFieldDstID_; + + uint_t distanceFieldUpdateInterval_; +}; // class DisjoiningPressureBubbleModel + +} // namespace bubble_model +} // namespace free_surface +} // namespace walberla + +#include "DisjoiningPressureBubbleModel.impl.h" diff --git a/src/lbm/free_surface/bubble_model/DisjoiningPressureBubbleModel.impl.h b/src/lbm/free_surface/bubble_model/DisjoiningPressureBubbleModel.impl.h new file mode 100644 index 000000000..6f8e42218 --- /dev/null +++ b/src/lbm/free_surface/bubble_model/DisjoiningPressureBubbleModel.impl.h @@ -0,0 +1,83 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file DisjoiningPressureBubbleModel.impl.h +//! \ingroup bubble_model +//! \author Martin Bauer +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Bubble Model with additional pressure term (disjoining pressure). +// +//====================================================================================================================== + +#include "field/AddToStorage.h" +#include "field/communication/PackInfo.h" + +#include "DisjoiningPressureBubbleModel.h" + +namespace walberla +{ +namespace free_surface +{ +namespace bubble_model +{ +template< typename Stencil_T > +DisjoiningPressureBubbleModel< Stencil_T >::DisjoiningPressureBubbleModel( + const std::shared_ptr< StructuredBlockForest >& blockStorage, real_t maxDistance, real_t disjPressConst, + bool enableBubbleSplits, uint_t distanceFieldUpdateInterval) + : BubbleModel< Stencil_T >(blockStorage, enableBubbleSplits), maxDistance_(maxDistance), + disjPressConst_(disjPressConst), distanceFieldSrcID_(field::addToStorage< DistanceField_T >( + blockStorage, "BubbleDistance Src", DistanceInfo(), field::fzyx, uint_c(1))), + distanceFieldDstID_(field::addToStorage< DistanceField_T >(blockStorage, "BubbleDistance Dst", DistanceInfo(), + field::fzyx, uint_c(1))), + distanceFieldUpdateInterval_(distanceFieldUpdateInterval) +{ + BubbleModel< Stencil_T >::bubbleFieldCommunication_.addPackInfo( + std::make_shared< field::communication::PackInfo< DistanceField_T > >(distanceFieldSrcID_)); +} + +template< typename Stencil_T > +void DisjoiningPressureBubbleModel< Stencil_T >::updateDistanceField() +{ + for (auto blockIt = BubbleModel< Stencil_T >::blockStorage_->begin(); + blockIt != BubbleModel< Stencil_T >::blockStorage_->end(); ++blockIt) + { + // get fields + const BubbleField_T* const bubbleField = + blockIt->template getData< const BubbleField_T >(BubbleModel< Stencil_T >::getBubbleFieldID()); + DistanceField_T* const distSrcField = blockIt->template getData< DistanceField_T >(distanceFieldSrcID_); + DistanceField_T* const distDstField = blockIt->template getData< DistanceField_T >(distanceFieldDstID_); + + WALBERLA_FOR_ALL_CELLS(distDstIt, distDstField, distSrcIt, distSrcField, bubbleIt, bubbleField, { + distDstIt->clear(); + + // if current cell is part of a bubble, set the distance to zero + if (*bubbleIt != INVALID_BUBBLE_ID) { distDstIt->setToZero(*bubbleIt); } + + // loop over src field neighborhood and combine them into this cell + for (auto d = Stencil_T::beginNoCenter(); d != Stencil_T::end(); ++d) + { + // sum distance with already known distances from neighborhood + distDstIt->combine(distSrcIt.neighbor(*d), d.length(), maxDistance_); + } + }); // WALBERLA_FOR_ALL_CELLS + + // swap pointers to distance fields + distSrcField->swapDataPointers(*distDstField); + } +} + +} // namespace bubble_model +} // namespace free_surface +} // namespace walberla diff --git a/src/lbm/free_surface/bubble_model/DistanceInfo.cpp b/src/lbm/free_surface/bubble_model/DistanceInfo.cpp new file mode 100644 index 000000000..db90cae4c --- /dev/null +++ b/src/lbm/free_surface/bubble_model/DistanceInfo.cpp @@ -0,0 +1,112 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file DistanceInfo.cpp +//! \ingroup bubble_model +//! \author Martin Bauer +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Compute the distances between bubbles. +// +//====================================================================================================================== + +#include "DistanceInfo.h" + +#include "core/mpi/BufferDataTypeExtensions.h" + +namespace walberla +{ +namespace free_surface +{ +namespace bubble_model +{ +mpi::SendBuffer& operator<<(mpi::SendBuffer& buf, const DistanceInfo& di) +{ + buf << di.bubbleInfos_; + return buf; +} + +mpi::RecvBuffer& operator>>(mpi::RecvBuffer& buf, DistanceInfo& di) +{ + di.clear(); + buf >> di.bubbleInfos_; + return buf; +} + +real_t DistanceInfo::getDistanceToNearestBubble(const BubbleID& ownID, real_t maxDistance) const +{ + // limit search region to maxDistance + real_t minDistance = maxDistance; + + // find the minimum distance + for (auto it = bubbleInfos_.begin(); it != bubbleInfos_.end(); it++) + { + if (minDistance > it->second && it->first != ownID) { minDistance = it->second; } + } + + return minDistance; +} + +void DistanceInfo::combine(const DistanceInfo& other, real_t linkDistance, real_t maxDistance) +{ + // sum of another cell's distance to the nearest bubble and linkDistance + real_t sumDistance = real_c(0); + + // loop over the "bubbleInfos" (std::map) of another cell + for (auto it = other.bubbleInfos_.begin(); it != other.bubbleInfos_.end(); it++) + { + // the other cell's bubble is not in contained in this cell's bubbleInfos, yet + if (bubbleInfos_.find(it->first) == bubbleInfos_.end()) + { + sumDistance = it->second + linkDistance; + + // add an entry for the other cell's nearest bubble, if the summed distance is below maxDistance + if (sumDistance < maxDistance) { bubbleInfos_[it->first] = sumDistance; } + } + else // the other cell's bubble is contained in this cell's bubbleInfos + { + sumDistance = it->second + linkDistance; + real_t& entry = bubbleInfos_[it->first]; + + // update the distance for this bubble if it is less than the currently known distance + if (entry > sumDistance) { entry = sumDistance; } + } + } +} + +void DistanceInfo::setToZero(bubble_model::BubbleID id) +{ + // "bubbleInfos" is of type std::map, if no entry with key "id" exists, a new entry is created + bubbleInfos_[id] = real_c(0.0); +} + +void DistanceInfo::removeZeroDistanceFromLiquid() +{ + for (auto it = bubbleInfos_.begin(); it != bubbleInfos_.end(); it++) + if (it->second <= real_c(0.0)) { bubbleInfos_.erase(it); } +} + +bool DistanceInfo::operator==(DistanceInfo& other) { return (this->bubbleInfos_ == other.bubbleInfos_); } + +void DistanceInfo::print(std::ostream& os) const +{ + for (auto it = bubbleInfos_.begin(); it != bubbleInfos_.end(); it++) + { + os << "( " << it->first << "," << it->second << ")\n"; + } +} + +} // namespace bubble_model +} // namespace free_surface +} // namespace walberla diff --git a/src/lbm/free_surface/bubble_model/DistanceInfo.h b/src/lbm/free_surface/bubble_model/DistanceInfo.h new file mode 100644 index 000000000..72efc7ca9 --- /dev/null +++ b/src/lbm/free_surface/bubble_model/DistanceInfo.h @@ -0,0 +1,100 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file DistanceInfo.h +//! \ingroup bubble_model +//! \author Daniela Anderl +//! \author Martin Bauer +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Compute the distances between bubbles. +// +//====================================================================================================================== + +#pragma once + +#include "core/mpi/BufferDataTypeExtensions.h" + +#include "field/GhostLayerField.h" + +#include <map> + +#include "BubbleDefinitions.h" + +namespace walberla +{ +namespace free_surface +{ +namespace bubble_model +{ +/*********************************************************************************************************************** + * Compute the distances from other bubbles in a diffusive manner. + * + * Each bubble spreads its distance until a defined maximum distance is reached. Cells that are within this + * maximum distance from one ore more bubbles know the distance from itself to each of these bubbles (or the single + * bubble). + * + **********************************************************************************************************************/ +class DistanceInfo +{ + public: + DistanceInfo() = default; + + // get the distance to the bubble that is closest to bubble "ownID" within the range of maxDistance + real_t getDistanceToNearestBubble(const BubbleID& ownID, real_t maxDistance) const; + + // combine "bubbleInfos" (std::map) of the current cell with "bubbleInfos" of another cell; the distance between + // these two cells is linkDistance: + // - if the other cell's bubble is not yet registered, insert it and calculate the distance + // - if the other cell's bubble is already registered, update the distance + void combine(const DistanceInfo& other, real_t linkDistance, real_t maxDistance); + + // set the distance to bubble "id" to zero; create an entry for this bubble, if non yet exists + void setToZero(bubble_model::BubbleID id); + + // remove any entry in "bubbleInfos" with distance <= 0 + void removeZeroDistanceFromLiquid(); + + bool operator==(DistanceInfo& other); + + void clear() { bubbleInfos_.clear(); } + + // print the content of bubbleInfos + void print(std::ostream& os) const; + + protected: + friend mpi::SendBuffer& operator<<(mpi::SendBuffer&, const DistanceInfo&); + friend mpi::RecvBuffer& operator>>(mpi::RecvBuffer&, DistanceInfo&); + + std::map< bubble_model::BubbleID, real_t > bubbleInfos_; +}; // class DistanceInfo + +mpi::SendBuffer& operator<<(mpi::SendBuffer& buf, const DistanceInfo& di); +mpi::RecvBuffer& operator>>(mpi::RecvBuffer& buf, DistanceInfo& di); + +} // namespace bubble_model +} // namespace free_surface +} // namespace walberla + +namespace walberla +{ +namespace mpi +{ +template<> +struct BufferSizeTrait< free_surface::bubble_model::DistanceInfo > +{ + static const bool constantSize = false; +}; +} // namespace mpi +} // namespace walberla diff --git a/src/lbm/free_surface/bubble_model/FloodFill.h b/src/lbm/free_surface/bubble_model/FloodFill.h new file mode 100644 index 000000000..b23d4d774 --- /dev/null +++ b/src/lbm/free_surface/bubble_model/FloodFill.h @@ -0,0 +1,104 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file FloodFill.h +//! \ingroup bubble_model +//! \author Martin Bauer +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Flood fill algorithm to identify connected gas volumes as bubbles. +// +//====================================================================================================================== + +#pragma once + +#include "core/cell/Cell.h" + +#include <queue> + +#include "BubbleDefinitions.h" + +namespace walberla +{ +namespace free_surface +{ +namespace bubble_model +{ +/*********************************************************************************************************************** + * Use the flood fill (also called seed fill) algorithm to identify connected gas volumes as bubbles and mark the whole + * region belonging to the bubble with a common bubble ID in bubbleField. + **********************************************************************************************************************/ +class FloodFillInterface +{ + public: + virtual ~FloodFillInterface() = default; + + /******************************************************************************************************************** + * Marks the region of a bubble in the BubbleField + * + * bubbleField Field where the bubble is marked. BubbleField must not contain entries equal to newBubbleID. + * startCell Cell that belongs to bubble. It is used as starting point for flood fill algorithm. + * newBubbleID An integer, greater than zero. This value is used as the bubble marker (ID) in the bubble field. + * volume Volume of the gas phase, i.e., the sum of (1-fillLevel) for all cells inside the bubble. + * nrOfCells The number of cells that belong to this bubble. + ********************************************************************************************************************/ + virtual void run(IBlock& block, BlockDataID bubbleIDField, const Cell& startCell, BubbleID newBubbleID, + real_t& volume, uint_t& nrOfCells) = 0; +}; // class FloodFillInterface + +/*********************************************************************************************************************** + * Use only fill level to identify bubbles. + * Problem: isInterfaceFromFillLevel() has to be called to identify interface cells; this is expensive and requires + * information from neighboring cells. + **********************************************************************************************************************/ +template< typename Stencil_T > +class FloodFillUsingFillLevel : public FloodFillInterface +{ + public: + FloodFillUsingFillLevel(ConstBlockDataID fillFieldID) : fillFieldID_(fillFieldID) {} + ~FloodFillUsingFillLevel() override = default; + + void run(IBlock& block, BlockDataID bubbleFieldID, const Cell& startCell, BubbleID newBubbleID, real_t& volume, + uint_t& nrOfCells) override; + + private: + ConstBlockDataID fillFieldID_; +}; // FloodFillUsingFillLevel + +/*********************************************************************************************************************** + * Use flag field to identify interface cells which should be faster than using only the fill level. + **********************************************************************************************************************/ +template< typename FlagField_T > +class FloodFillUsingFlagField : public FloodFillInterface +{ + public: + FloodFillUsingFlagField(ConstBlockDataID fillFieldID, ConstBlockDataID flagFieldID) + : fillFieldID_(fillFieldID), flagFieldID_(flagFieldID) + {} + + ~FloodFillUsingFlagField() override = default; + + void run(IBlock& block, BlockDataID bubbleFieldID, const Cell& startCell, BubbleID newBubbleID, real_t& volume, + uint_t& nrOfCells) override; + + private: + ConstBlockDataID fillFieldID_; + ConstBlockDataID flagFieldID_; +}; // FloodFillUsingFlagField + +} // namespace bubble_model +} // namespace free_surface +} // namespace walberla + +#include "FloodFill.impl.h" \ No newline at end of file diff --git a/src/lbm/free_surface/bubble_model/FloodFill.impl.h b/src/lbm/free_surface/bubble_model/FloodFill.impl.h new file mode 100644 index 000000000..96f1c0361 --- /dev/null +++ b/src/lbm/free_surface/bubble_model/FloodFill.impl.h @@ -0,0 +1,216 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file FloodFill.impl.h +//! \ingroup bubble_model +//! \author Martin Bauer +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Flood fill algorithm to identify connected gas volumes as bubbles. +// +//====================================================================================================================== + +#include "lbm/free_surface/FlagInfo.h" +#include "lbm/free_surface/InterfaceFromFillLevel.h" + +#include "FloodFill.h" + +namespace walberla +{ +namespace free_surface +{ +namespace bubble_model +{ +// Sets newBubbleID in all cells that belong to the same bubble as startCell. Only uses the fillField but needs to +// call isInterfaceFromFillLevel() to identify interface cells. This is expensive and requires information from +// neighboring cells. +template< typename Stencil_T > +void FloodFillUsingFillLevel< Stencil_T >::run(IBlock& block, BlockDataID bubbleFieldID, const Cell& startCell, + BubbleID newBubbleID, real_t& volume, uint_t& nrOfCells) +{ + // get fields + BubbleField_T* const bubbleField = block.getData< BubbleField_T >(bubbleFieldID); + const ScalarField_T* const fillField = block.getData< const ScalarField_T >(fillFieldID_); + + WALBERLA_ASSERT_EQUAL(fillField->xyzSize(), bubbleField->xyzSize()); + + volume = real_c(0); + nrOfCells = uint_c(0); + + std::queue< Cell > cellQueue; + cellQueue.push(startCell); + + std::vector< bool > addedLast; + + using namespace stencil; + const int dirs[4] = { N, S, T, B }; + const uint_t numDirs = uint_c(4); + + CellInterval fieldSizeInterval = fillField->xyzSize(); + + while (!cellQueue.empty()) + { + // process first cell in queue + Cell cell = cellQueue.front(); + + // go to the beginning of the x-line, i.e., find the minimum x-coordinate that still belongs to this bubble + while (cell.x() > cell_idx_c(0) && + bubbleField->get(cell.x() - cell_idx_c(1), cell.y(), cell.z()) != newBubbleID && + (fillField->get(cell.x() - cell_idx_c(1), cell.y(), cell.z()) < real_c(1.0) || + isInterfaceFromFillLevel< Stencil_T >(*fillField, cell.x() - cell_idx_c(1), cell.y(), cell.z()))) + { + --cell.x(); + } + + // vector stores whether a cell in a direction from dirs was added already to make sure that for the whole x-line, + // only one neighboring cell per direction is added to the cellQueue + addedLast.assign(numDirs, false); + + // loop to the end of the x-line and mark any cell that belongs to this bubble + while (cell.x() < cell_idx_c(fillField->xSize()) && bubbleField->get(cell) != newBubbleID && + (fillField->get(cell) < real_c(1) || isInterfaceFromFillLevel< Stencil_T >(*fillField, cell))) + { + // set bubble ID to this cell + bubbleField->get(cell) = newBubbleID; + volume += real_c(1.0) - fillField->get(cell); + nrOfCells++; + + // iterate over all directions in dirs to check which cells in y- and z-direction should be processed next, + // i.e., which cells should be added to cellQueue + for (uint_t i = uint_c(0); i < numDirs; ++i) + { + Cell neighborCell(cell.x(), cell.y() + cy[dirs[i]], cell.z() + cz[dirs[i]]); + + // neighboring cell is not inside the field + if (!fieldSizeInterval.contains(neighborCell)) { continue; } + + // neighboring cell is part of the bubble + if ((fillField->get(neighborCell) < real_c(1) || + isInterfaceFromFillLevel< Stencil_T >(*fillField, neighborCell)) && + bubbleField->get(neighborCell) != newBubbleID) + { + // make sure that for the whole x-line, only one neighboring cell per direction is added to the cellQueue + if (!addedLast[i]) + { + addedLast[i] = true; + + // add neighboring cell to queue such that it serves as starting cell in one of the next iterations + cellQueue.push(neighborCell); + } + } + else { addedLast[i] = false; } + } + + ++cell.x(); + } + + // remove the processed cell from cellQueue + cellQueue.pop(); + } +} + +// Sets newBubbleID in all cells that belong to the same bubble as startCell. Uses the fillField and the flagField +// and is therefore less expensive than the approach that only uses the fillField. +template< typename FlagField_T > +void FloodFillUsingFlagField< FlagField_T >::run(IBlock& block, BlockDataID bubbleFieldID, const Cell& startCell, + BubbleID newBubbleID, real_t& volume, uint_t& nrOfCells) +{ + // get fields + BubbleField_T* const bubbleField = block.getData< BubbleField_T >(bubbleFieldID); + const ScalarField_T* const fillField = block.getData< const ScalarField_T >(fillFieldID_); + const FlagField_T* const flagField = block.getData< const FlagField_T >(flagFieldID_); + + WALBERLA_ASSERT_EQUAL(fillField->xyzSize(), bubbleField->xyzSize()); + WALBERLA_ASSERT_EQUAL(fillField->xyzSize(), flagField->xyzSize()); + + using flag_t = typename FlagField_T::flag_t; + // gasInterfaceFlagMask masks interface and gas cells (using bitwise OR) + flag_t gasInterfaceFlagMask = flag_t(0); + gasInterfaceFlagMask = flag_t(gasInterfaceFlagMask | flagField->getFlag(flagIDs::interfaceFlagID)); + gasInterfaceFlagMask = flag_t(gasInterfaceFlagMask | flagField->getFlag(flagIDs::gasFlagID)); + + volume = real_c(0); + nrOfCells = uint_c(0); + + std::queue< Cell > cellQueue; + cellQueue.push(startCell); + + std::vector< bool > addedLast; + + using namespace stencil; + const int dirs[4] = { N, S, T, B }; + const uint_t numDirs = uint_c(4); + + CellInterval fieldSizeInterval = flagField->xyzSize(); + + while (!cellQueue.empty()) + { + // process first cell in queue + Cell& cell = cellQueue.front(); + + // go to the beginning of the x-line, i.e., find the minimum x-coordinate that still belongs to this bubble + while (isPartOfMaskSet(flagField->get(cell.x() - cell_idx_c(1), cell.y(), cell.z()), gasInterfaceFlagMask) && + cell.x() > cell_idx_c(0) && bubbleField->get(cell.x() - cell_idx_c(1), cell.y(), cell.z()) != newBubbleID) + { + --cell.x(); + } + + // vector stores whether a cell in a direction from dirs was added already to make sure that for the whole x-line, + // only one neighboring cell per direction is added to the cellQueue + addedLast.assign(numDirs, false); + + // loop to the end of the x-line and mark any cell that belongs to this bubble + while (isPartOfMaskSet(flagField->get(cell), gasInterfaceFlagMask) && bubbleField->get(cell) != newBubbleID && + cell.x() < cell_idx_c(flagField->xSize())) + { + // set bubble ID to this cell + bubbleField->get(cell) = newBubbleID; + volume += real_c(1.0) - fillField->get(cell); + nrOfCells++; + + // iterate over all directions in dirs to check which cells in y- and z-direction should be processed next, + // i.e., which cells should be added to cellQueue + for (uint_t i = uint_c(0); i < numDirs; ++i) + { + Cell neighborCell(cell.x(), cell.y() + cy[dirs[i]], cell.z() + cz[dirs[i]]); + + // neighboring cell is not inside the field + if (!fieldSizeInterval.contains(neighborCell)) { continue; } + + // neighboring cell is part of the bubble + if (!isPartOfMaskSet(flagField->get(neighborCell), gasInterfaceFlagMask) && + bubbleField->get(neighborCell) != newBubbleID) + { + // make sure that for the whole x-line, only one neighboring cell per direction is added to the cellQueue + if (!addedLast[i]) + { + addedLast[i] = true; + + // add neighboring cell to queue such that it serves as starting cell in one of the next iterations + cellQueue.push(neighborCell); + } + } + else { addedLast[i] = false; } + } + ++cell.x(); + } + + // remove the processed cell from cellQueue + cellQueue.pop(); + } +} + +} // namespace bubble_model +} // namespace free_surface +} // namespace walberla diff --git a/src/lbm/free_surface/bubble_model/Geometry.h b/src/lbm/free_surface/bubble_model/Geometry.h new file mode 100644 index 000000000..7201a632e --- /dev/null +++ b/src/lbm/free_surface/bubble_model/Geometry.h @@ -0,0 +1,87 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file Geometry.h +//! \ingroup bubble_model +//! \author Martin Bauer +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Geometrical helper functions for the bubble model. +// +//====================================================================================================================== + +#pragma once + +#include "core/math/AABB.h" +#include "core/math/Vector3.h" + +#include "domain_decomposition/StructuredBlockStorage.h" + +#include "field/FlagField.h" + +#include "geometry/bodies/BodyOverlapFunctions.h" + +namespace walberla +{ +namespace free_surface +{ +namespace bubble_model +{ +/*********************************************************************************************************************** + * Set fill levels in a scalar ghost layer field (GhostLayerField<real_t,1>) using a geometric body object. Specifying + * isGas determines whether the body is initialized as gas bubble or liquid drop. + * + * The function overlapVolume(body, cellmidpoint, dx) has to be defined for the body object. + * + * The volume fraction of the body is _SUBTRACTED_ from the each cells' current fill level, including ghost layer cells. + * Therefore, the field should be initialized with 1 in each cell ( "everywhere fluid"). + * Then: + * - if a cell is inside a body, its fill level is 0 + * - if a cell is completely outside the body, its fill level is not changed and remains 1 + * - if a cell is partially inside the sphere, the amount of overlap is subtracted from the current fill level; it can + * not become negative and is limited to 0 + **********************************************************************************************************************/ +template< typename Body_T > +void addBodyToFillLevelField(StructuredBlockStorage& blockStorage, BlockDataID fillFieldID, const Body_T& body, + bool isGas); + +/*********************************************************************************************************************** + * Set flag field according to given fill level field. + * + * FillLevel <=0 : gas flag (and both other flags are removed if set) + * FillLevel >=1 : fluid flag (and both other flags are removed if set) + * otherwise : interface flag (and both other flags are removed if set) + * + * The three FlagUIDs for liquid, gas and interface must already be registered at the flag field. + **********************************************************************************************************************/ +template< typename flag_t > +void setFlagFieldFromFillLevels(FlagField< flag_t >* flagField, const GhostLayerField< real_t, 1 >* fillField, + const FlagUID& liquid, const FlagUID& gas, const FlagUID& interFace); + +/*********************************************************************************************************************** + * Check if interface layer is valid and correctly separates gas and fluid cells: + * - gas cell must not have a fluid cell in its (Stencil_T) neighborhood + * - fluid cell must not have a gas cell in its (Stencil_T) neighborhood + * + * The three FlagUIDs for liquid, gas and interface must already be registered at the flag field. + **********************************************************************************************************************/ +template< typename flag_t, typename Stencil_T > +bool checkForValidInterfaceLayer(const FlagField< flag_t >* flagField, const FlagUID& liquid, const FlagUID& gas, + const FlagUID& interFace, bool printWarnings); + +} // namespace bubble_model +} // namespace free_surface +} // namespace walberla + +#include "Geometry.impl.h" \ No newline at end of file diff --git a/src/lbm/free_surface/bubble_model/Geometry.impl.h b/src/lbm/free_surface/bubble_model/Geometry.impl.h new file mode 100644 index 000000000..9b0fd0dba --- /dev/null +++ b/src/lbm/free_surface/bubble_model/Geometry.impl.h @@ -0,0 +1,219 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file Geometry.impl.h +//! \ingroup bubble_model +//! \author Martin Bauer +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Geometrical helper functions for the bubble model. +// +//====================================================================================================================== + +#include "core/logging/Logging.h" + +#include "field/FlagField.h" +#include "field/GhostLayerField.h" + +#include <limits> + +#include "Geometry.h" + +namespace walberla +{ +namespace free_surface +{ +namespace bubble_model +{ +template< typename Body_T > +void addBodyToFillLevelField(StructuredBlockStorage& blockStorage, BlockDataID fillFieldID, const Body_T& body, + bool isGas) +{ + const real_t dx = blockStorage.dx(); + + for (auto blockIt = blockStorage.begin(); blockIt != blockStorage.end(); ++blockIt) + { + // get block + IBlock* block = &(*blockIt); + + // get fill level field + GhostLayerField< real_t, 1 >* const fillField = block->getData< GhostLayerField< real_t, 1 > >(fillFieldID); + + // get the block's bounding box + AABB blockBB = block->getAABB(); + + // extend the bounding box with the ghost layer + blockBB.extend(dx * real_c(fillField->nrOfGhostLayers())); + + // skip blocks that do not intersect the body + if (geometry::fastOverlapCheck(body, blockBB) == geometry::COMPLETELY_OUTSIDE) { continue; } + + // get the global coordinates (via the bounding box) of the block's first cell + AABB firstCellBB; + blockStorage.getBlockLocalCellAABB(*block, fillField->beginWithGhostLayer().cell(), firstCellBB); + + // get the global coordinates of the midpoint of the block's first cell + Vector3< real_t > firstCellMidpoint; + for (uint_t i = uint_c(0); i < uint_c(3); ++i) + { + firstCellMidpoint[i] = firstCellBB.min(i) + real_c(0.5) * firstCellBB.size(i); + } + + // get the number of ghost layers + const uint_t numGl = fillField->nrOfGhostLayers(); + cell_idx_t glCellIndex = cell_idx_c(numGl); + + // starting from a block's first cell, iterate over all cells and determine the overlap of the body with each cell + // to set the cell's fill level accordingly + Vector3< real_t > currentMidpoint; + currentMidpoint[2] = firstCellMidpoint[2]; + for (cell_idx_t z = -glCellIndex; z < cell_idx_c(fillField->zSize() + numGl); ++z, currentMidpoint[2] += dx) + { + currentMidpoint[1] = firstCellMidpoint[1]; + for (cell_idx_t y = -glCellIndex; y < cell_idx_c(fillField->ySize() + numGl); ++y, currentMidpoint[1] += dx) + { + currentMidpoint[0] = firstCellMidpoint[0]; + for (cell_idx_t x = -glCellIndex; x < cell_idx_c(fillField->xSize() + numGl); ++x, currentMidpoint[0] += dx) + { + // get the current cell's overlap with the body (full overlap=1, no overlap=0, else in between) + real_t overlapFraction = geometry::overlapFraction(body, currentMidpoint, dx); + + // get the current fill level + real_t& fillLevel = fillField->get(x, y, z); + WALBERLA_ASSERT(fillLevel >= 0 && fillLevel <= 1); + + // initialize a gas bubble (fill level=0) + if (isGas) + { + // subtract the fraction of the body's overlap from the fill level + fillLevel -= overlapFraction; + + // limit the fill level such that it does not become negative during initialization + fillLevel = std::max(fillLevel, real_c(0.0)); + } + else // initialize a liquid drop (fill level=1) + { + // add the fraction of the body's overlap from the fill level + fillLevel += overlapFraction; + + // limit the fill level such that it does not become too large during initialization + fillLevel = std::min(fillLevel, real_c(1.0)); + } + } + } + } + } +} + +template< typename flag_t > +void setFlagFieldFromFillLevels(FlagField< flag_t >* flagField, const GhostLayerField< real_t, 1 >* fillField, + const FlagUID& liquid, const FlagUID& gas, const FlagUID& interFace) +{ + WALBERLA_ASSERT(flagField->xyzSize() == fillField->xyzSize()); + + // get flags from flag field + flag_t liquidFlag = flagField->getFlag(liquid); + flag_t gasFlag = flagField->getFlag(gas); + flag_t interfaceFlag = flagField->getFlag(interFace); + + flag_t allMask = liquidFlag | gasFlag | interfaceFlag; + + using FillField_T = GhostLayerField< real_t, 1 >; + + // iterate over all cells (including ghost layer) in fill level field and flag field + WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ(fillField, { + const typename FillField_T::ConstPtr fillFieldPtr(*fillField, x, y, z); + const typename FlagField< flag_t >::Ptr flagFieldPtr(*flagField, x, y, z); + + // clear liquid, gas, and interface flags in flag field + removeMask(flagFieldPtr, allMask); + + if (*fillFieldPtr <= real_c(0)) { addFlag(flagFieldPtr, gasFlag); } + else + { + if (*fillFieldPtr >= real_c(1)) { addFlag(flagFieldPtr, liquidFlag); } + else + { + // add interface flag for fill levels between 0 and 1 + addFlag(flagFieldPtr, interfaceFlag); + } + } + }) // WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ +} + +template< typename flag_t, typename Stencil_T > +bool checkForValidInterfaceLayer(const FlagField< flag_t >* flagField, const FlagUID& liquid, const FlagUID& gas, + const FlagUID& interFace, bool printWarnings) +{ + // variable only used when printWarnings is true; avoids premature termination of the search such that any non-valid + // cell can be print + bool valid = true; + + // get flags + flag_t liquidFlag = flagField->getFlag(liquid); + flag_t gasFlag = flagField->getFlag(gas); + flag_t interfaceFlag = flagField->getFlag(interFace); + + // iterate flag field + for (auto flagFieldIt = flagField->begin(); flagFieldIt != flagField->end(); ++flagFieldIt) + { + // check that not more than one flag is set for a cell + if (isMaskSet(flagFieldIt, flag_t(liquidFlag | gasFlag)) || + isMaskSet(flagFieldIt, flag_t(liquidFlag | interfaceFlag)) || + isMaskSet(flagFieldIt, flag_t(gasFlag | interfaceFlag))) + { + if (!printWarnings) { return false; } + valid = false; + WALBERLA_LOG_WARNING("More than one free surface flag is set in cell " << flagFieldIt.cell() << "."); + } + + // if current cell is a gas cell it must not have a fluid cell in its neighborhood + if (isFlagSet(flagFieldIt, liquidFlag)) + { + for (auto dir = Stencil_T::beginNoCenter(); dir != Stencil_T::end(); ++dir) + { + if (isFlagSet(flagFieldIt.neighbor(*dir), gasFlag)) + { + if (!printWarnings) { return false; } + valid = false; + WALBERLA_LOG_WARNING("Fluid cell " << flagFieldIt.cell() + << " has a gas cell in its direct neighborhood."); + } + } + } + // if current cell is a fluid cell it must not have a gas cell in its neighborhood + else + { + if (isFlagSet(flagFieldIt, gasFlag)) + { + for (auto dir = Stencil_T::beginNoCenter(); dir != Stencil_T::end(); ++dir) + { + if (isFlagSet(flagFieldIt.neighbor(*dir), liquidFlag)) + { + if (!printWarnings) { return false; } + valid = false; + WALBERLA_LOG_WARNING("Gas cell " << flagFieldIt.cell() + << " has a fluid cell in its direct neighborhood."); + } + } + } + } + } + + return valid; +} + +} // namespace bubble_model +} // namespace free_surface +} // namespace walberla \ No newline at end of file diff --git a/src/lbm/free_surface/bubble_model/MergeInformation.cpp b/src/lbm/free_surface/bubble_model/MergeInformation.cpp new file mode 100644 index 000000000..15d6f6bd8 --- /dev/null +++ b/src/lbm/free_surface/bubble_model/MergeInformation.cpp @@ -0,0 +1,337 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file MergeInformation.cpp +//! \ingroup bubble_model +//! \author Martin Bauer +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Manage merging of bubbles. +// +//====================================================================================================================== + +#include "MergeInformation.h" + +#include "core/mpi/MPIManager.h" + +namespace walberla +{ +namespace free_surface +{ +namespace bubble_model +{ +MergeInformation::MergeInformation(uint_t numberOfBubbles) +{ + hasMerges_ = false; + resize(numberOfBubbles); +} + +/*********************************************************************************************************************** + * Implementation Note (see also MergeInformationTest.cpp): + * --------------------- + * renameVec_ = { 0, 1, 2, 3, 4, 5 } + * + * position 5 is renamed to the ID at position 3: + * (1) registerMerge( 5, 3 ); + * renameVec_ = { 0, 1, 2, 3, 4, 3 } + * + * position 3 is renamed to the ID at position 1: + * (2) registerMerge(3, 1); + * renameVec_ = { 0, 1, 2, 1, 4, 3 } + * + * since position 3 was already renamed to the ID at position 1, registerMerge(1, 0) is called internally and + * position 1 is renamed to the ID at position 0; position 3 is renamed to the ID at position 0 + * (3) registerMerge( 3, 0 ); + * -> leads to recursive registerMerge(1, 0); + * renameVec_ = { 0, 0, 2, 0, 4, 3 } + * since position 5 was already renamed to the ID of position 3, registerMerge(3, 2) is called internally; since + * position 3 was already renamed to the ID of position 0, registerMerge(2, 0) is called internally; position 2 is + * renamed to the ID at position 0, position 5 is renamed to the ID at position 0 + * (4) registerMerge( 5, 2) + * -> leads to recursive registerMerge( 3, 2) + * registerMerge( 0, 2) + * renameVec_ = { 0, 0, 0, 0, 4, 0 } + * + * Recursive call in Step(3) is necessary because otherwise the mapping from 3->1 would be "forgotten". + * However, not all transitive renames are resolved by this function as seen in step (3): 5->3->1 + * Thus, the additional function resolveTransitiveRenames() is required. + **********************************************************************************************************************/ +void MergeInformation::registerMerge(BubbleID b0, BubbleID b1) +{ + WALBERLA_ASSERT_LESS(b0, renameVec_.size()); + WALBERLA_ASSERT_LESS(b1, renameVec_.size()); + + // identical bubbles can not be merged + if (b0 == b1) { return; } + + // register the merge using hasMerges_ + hasMerges_ = true; + + // ensure that b0 < b1 (increasing ID ordering is required for some functions later on) + if (b1 < b0) { std::swap(b0, b1); } + + WALBERLA_ASSERT_LESS(b0, b1); + + // if bubble b1 is also marked for merging with another bubble, e.g., bubble b2 + if (isRenamed(b1)) + { + // mark bubble b2 for merging with b0 (b2 = renameVec_[b1]) + registerMerge(b0, renameVec_[b1]); + + // mark bubble b1 for merging with bubble b0 or bubble b2 (depending on the ID order found in + // registerMerge(b0,b2) above) + renameVec_[b1] = renameVec_[renameVec_[b1]]; + } + else + { + // mark bubble b1 for merging with bubble b0 + renameVec_[b1] = b0; + } +} + +void MergeInformation::mergeAndReorderBubbleVector(std::vector< Bubble >& bubbles) +{ + // no bubble merge possible with less than 2 bubbles + if (bubbles.size() < uint_c(2)) { return; } + + // rename merged bubbles (always keep smallest bubble ID) + resolveTransitiveRenames(); + +#ifndef NDEBUG + uint_t numberOfMerges = countNumberOfRenames(); +#endif + + WALBERLA_ASSERT_EQUAL(bubbles.size(), renameVec_.size()); + WALBERLA_ASSERT_EQUAL(renameVec_[0], 0); + + for (size_t i = uint_c(0); i < bubbles.size(); ++i) + { + // any entry that does not point to itself needs to be merged + if (renameVec_[i] != i) + { + WALBERLA_ASSERT_LESS(renameVec_[i], i); + + // merge bubbles + bubbles[renameVec_[i]].merge(bubbles[i]); + } + } + + // create temporary vector with "index = bubble ID" to store the exchanged bubble IDs + std::vector< BubbleID > exchangeVector(bubbles.size()); + for (size_t i = uint_c(0); i < exchangeVector.size(); ++i) + { + exchangeVector[i] = BubbleID(i); + } + + // gapPointer searches for the renamed bubbles in increasing order; vector entries found by gapPointer are "gaps" and + // can be overwritten + uint_t gapPointer = uint_c(1); + while (gapPointer < bubbles.size() && !isRenamed(gapPointer)) // find first renamed bubble + { + ++gapPointer; + } + + // lastPointer searches for not-renamed bubbles in decreasing order; vector entries found by lastPointer remain in + // the vector and will be copied to the gaps + uint_t lastPointer = uint_c(renameVec_.size() - 1); + while (isRenamed(lastPointer) && lastPointer > uint_c(0)) // find last non-renamed bubble + { + --lastPointer; + } + + while (lastPointer > gapPointer) + { + // exchange the last valid (non-renamed) bubble ID with the first non-valid (renamed) bubble ID; anything + // gapPointer points to will be deleted later; this reorders the bubble vector + std::swap(bubbles[gapPointer], bubbles[lastPointer]); + + // store the above exchange + exchangeVector[lastPointer] = BubbleID(gapPointer); + exchangeVector[gapPointer] = BubbleID(lastPointer); + + // update lastPointer, i.e., find next non-renamed bubble + do + { + --lastPointer; + // important condition: "lastPointer > gapPointer" since gapPointer is valid now + } while (isRenamed(lastPointer) && lastPointer > gapPointer); + + // update gapPointer, i.e., find next renamed bubble + do + { + ++gapPointer; + } while (gapPointer < bubbles.size() && !isRenamed(gapPointer)); + } + + // shrink bubble vector (any element after lastPointer is not valid and can be removed) + uint_t newSize = lastPointer + uint_c(1); + WALBERLA_ASSERT_EQUAL(newSize, bubbles.size() - numberOfMerges); + WALBERLA_ASSERT_LESS_EQUAL(newSize, bubbles.size()); + bubbles.resize(newSize); + + // update renameVec_ with exchanged bubble IDs with the highest unnecessary bubble IDs being dropped; this ensures + // that bubble IDs are always numbered continuously from 0 upwards + for (size_t i = 0; i < renameVec_.size(); ++i) + { + renameVec_[i] = exchangeVector[renameVec_[i]]; + WALBERLA_ASSERT_LESS(renameVec_[i], newSize); + } +} + +void MergeInformation::communicateMerges() +{ + // merges can only be communicated if they occurred and are registered in renameVec_ + if (renameVec_.empty()) { return; } + + // rename process local bubble IDs + resolveTransitiveRenames(); + + // globally combine all rename vectors + int numProcesses = MPIManager::instance()->numProcesses(); + std::vector< BubbleID > allRenameVectors(renameVec_.size() * uint_c(numProcesses)); + WALBERLA_MPI_SECTION() + { + MPI_Allgather(&renameVec_[0], int_c(renameVec_.size()), MPITrait< BubbleID >::type(), &allRenameVectors[0], + int_c(renameVec_.size()), MPITrait< BubbleID >::type(), MPI_COMM_WORLD); + } + + // check for inter-process bubble merges + for (size_t i = renameVec_.size() - 1; i > 0; --i) + for (int process = 0; process < numProcesses; ++process) + { + if (process == MPIManager::instance()->rank()) { continue; } // local merges have already been treated + + size_t idx = uint_c(process) * renameVec_.size() + i; + + // register inter-process bubble merge (this updated renameVec_) + if (allRenameVectors[idx] != i) { registerMerge(allRenameVectors[idx], BubbleID(i)); } + } + + // rename global bubble IDs + resolveTransitiveRenames(); +} + +void MergeInformation::renameOnBubbleField(BubbleField_T* bubbleField) const +{ + WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ(bubbleField, { + const typename BubbleField_T::Ptr bubblePtr(*bubbleField, x, y, z); + if (*bubblePtr != INVALID_BUBBLE_ID) { *bubblePtr = renameVec_[*bubblePtr]; } + }) // WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ +} + +void MergeInformation::resizeAndClear(uint_t numberOfBubbles) +{ + renameVec_.resize(numberOfBubbles); + for (uint_t i = uint_c(0); i < numberOfBubbles; ++i) + { + renameVec_[i] = BubbleID(i); + } + + hasMerges_ = false; +} + +void MergeInformation::resize(uint_t numberOfBubbles) +{ + if (numberOfBubbles > renameVec_.size()) + { + renameVec_.reserve(numberOfBubbles); + for (size_t i = renameVec_.size(); i < numberOfBubbles; ++i) + { + renameVec_.push_back(BubbleID(i)); + } + } +} + +void MergeInformation::resolveTransitiveRenames() +{ + WALBERLA_ASSERT_GREATER(renameVec_.size(), 0); + WALBERLA_ASSERT_EQUAL(renameVec_[0], 0); + + for (size_t i = renameVec_.size() - 1; i > 0; --i) + { + // create new bubble ID for each entry in renameVec_ + BubbleID& newBubbleID = renameVec_[i]; + + // example 1: "renameVec_[4] = 2" means that bubble 4 has merged with bubble 2 + // => bubble 4 is renamed to bubble 2 (always the smaller ID is kept) + // example 2: "renameVec_[4] = 2" and "renameVec_[2] = 1" means above and that bubble 2 has merged with bubble 1 + // => bubble 4 should finally be renamed to bubble 1 + + // this loop ensures that renaming is correct even if multiple bubbles have merged (as in example 2 from above) + while (renameVec_[newBubbleID] != newBubbleID) + { + newBubbleID = renameVec_[newBubbleID]; + } + } + + WALBERLA_ASSERT(transitiveRenamesResolved()); // ensure that renaming was resolved correctly +} + +bool MergeInformation::transitiveRenamesResolved() const +{ + for (size_t i = 0; i < renameVec_.size(); ++i) + { + // A = renameVec_[i] + // B = renameVec_[A] + // A must be equal to B after bubble renaming + if (renameVec_[i] != renameVec_[renameVec_[i]]) { return false; } + } + + // ensure that no entry points to an element that has been renamed + for (size_t i = 0; i < renameVec_.size(); ++i) + { + if (renameVec_[i] != i) // bubble at entry i has been renamed + { + for (size_t j = 0; j < renameVec_.size(); ++j) + { + // renameVec_[j] points to entry i although i has been renamed + if (i != j && renameVec_[j] == i) { return false; } + } + } + } + + return true; +} + +uint_t MergeInformation::countNumberOfRenames() const +{ + uint_t renames = uint_c(0); + for (size_t i = 0; i < renameVec_.size(); ++i) + { + // only bubbles with "bubble ID != index" have been renamed + if (renameVec_[i] != i) { ++renames; } + } + + return renames; +} + +void MergeInformation::print(std::ostream& os) const +{ + os << "Merge Information: "; + + for (size_t i = 0; i < renameVec_.size(); ++i) + os << "( " << i << " -> " << renameVec_[i] << " ), "; + + os << std::endl; +} + +std::ostream& operator<<(std::ostream& os, const MergeInformation& mi) +{ + mi.print(os); + return os; +} + +} // namespace bubble_model +} // namespace free_surface +} // namespace walberla diff --git a/src/lbm/free_surface/bubble_model/MergeInformation.h b/src/lbm/free_surface/bubble_model/MergeInformation.h new file mode 100644 index 000000000..d8ce84faa --- /dev/null +++ b/src/lbm/free_surface/bubble_model/MergeInformation.h @@ -0,0 +1,102 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file MergeInformation.h +//! \ingroup bubble_model +//! \author Martin Bauer +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Manage merging of bubbles. +// +//====================================================================================================================== + +#pragma once + +#include <iostream> + +#include "Bubble.h" +#include "BubbleDefinitions.h" + +namespace walberla +{ +namespace free_surface +{ +namespace bubble_model +{ +/*********************************************************************************************************************** + * Class for managing bubble merging. + * + * - assumption: each process holds a synchronized, global vector of bubbles + * - each process creates a new MergeInformation class, which internally holds a mapping from BubbleID -> BubbleID + * - then during the time step, several merges can be registered using registerMerge() member function + * - this information is then communicated between all processes using communicateMerges() + * - Then the merges are executed on each process locally. This means that bubble volumes have to be added, + * and bubbles have to be deleted. To still have a continuous array, the last elements are copied to the place where + * the deleted bubbles have been -> renaming of bubbles. + * - the renaming has to be done also in the bubble field using renameOnBubbleField() + **********************************************************************************************************************/ +class MergeInformation +{ + public: + MergeInformation(uint_t numberOfBubbles = uint_c(0)); + + void registerMerge(BubbleID b0, BubbleID b1); + + // globally communicate bubble merges and rename bubble IDs accordingly in renameVec_ + void communicateMerges(); + + // merge bubbles according to renameVec_, and reorder and shrink the bubble vector such that bubble IDs are always + // numbered continuously from 0 upwards + void mergeAndReorderBubbleVector(std::vector< Bubble >& bubbles); + + // rename all bubbles in the bubble field according to renameVec_ + void renameOnBubbleField(BubbleField_T* bf) const; + + void resizeAndClear(uint_t numberOfBubbles); + + bool hasMerges() const { return hasMerges_; } + + void print(std::ostream& os) const; + + // vector for tracking bubble renaming: + // - "renameVec_[4] = 2": bubble 4 has merged with bubble 2, i.e., any 4 can be replaced by 2 in the bubble field + // - "renameVec_[i] <= i" for all i: when two bubbles merge, the smaller bubble ID is chosen for the resulting bubble + std::vector< BubbleID > renameVec_; + + private: + // adapt size of permutation vector, and init new elements with "identity" + void resize(uint_t numberOfBubbles); + + inline bool isRenamed(size_t i) const { return renameVec_[i] != i; } + + // ensure that renaming of bubble IDs is correct even if multiple bubbles merge during the same time step + void resolveTransitiveRenames(); + + // check whether renaming has been done correctly, i.e., check the correctness of renameVec_ + bool transitiveRenamesResolved() const; + + uint_t countNumberOfRenames() const; + + // signals that merges have to be treated in this time step, true whenever renameVec_ is not the identity mapping + bool hasMerges_; + + // friend function used in unit tests (MergeInformationTest) + friend void checkRenameVec(const MergeInformation& mi, const std::vector< BubbleID >& vecCompare); +}; // MergeInformation + +std::ostream& operator<<(std::ostream& os, const MergeInformation& mi); + +} // namespace bubble_model +} // namespace free_surface +} // namespace walberla diff --git a/src/lbm/free_surface/bubble_model/NewBubbleCommunication.cpp b/src/lbm/free_surface/bubble_model/NewBubbleCommunication.cpp new file mode 100644 index 000000000..f36c504a4 --- /dev/null +++ b/src/lbm/free_surface/bubble_model/NewBubbleCommunication.cpp @@ -0,0 +1,232 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file NewBubbleCommunication.cpp +//! \ingroup bubble_model +//! \author Martin Bauer +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Communication for the creation of new bubbles. +// +//====================================================================================================================== + +#include "NewBubbleCommunication.h" + +namespace walberla +{ +namespace free_surface +{ +namespace bubble_model +{ +class CommunicatedNewBubbles +{ + public: + CommunicatedNewBubbles(size_t nrOfBubblesBefore, size_t locallyCreatedBubbles, size_t localOffset) + : nextBubbleCtr_(0) + { + temporalIDToNewIDMap_.resize(locallyCreatedBubbles); + nrOfBubblesBefore_ = numeric_cast< BubbleID >(nrOfBubblesBefore); + nrOfLocallyCreatedBubbles_ = numeric_cast< BubbleID >(locallyCreatedBubbles); + localOffset_ = numeric_cast< BubbleID >(localOffset); + } + + void storeNextBubble(BubbleID newBubbleID, Bubble& bubbleToStoreTo) + { + // store newBubbleID in map + if (wasNextBubbleCreatedOnThisProcess()) { temporalIDToNewIDMap_[nextBubbleCtr_ - localOffset_] = newBubbleID; } + + // store bubble + recvBuffer_ >> bubbleToStoreTo; + + ++nextBubbleCtr_; + } + + // return whether there are new bubbles that have to be processed + bool hasMoreBubbles() const { return !recvBuffer_.isEmpty(); } + + // map the preliminary bubble IDs to global new bubble IDs + void mapTemporalToNewBubbleID(BubbleID& id) const + { + if (id >= nrOfBubblesBefore_) { id = temporalIDToNewIDMap_[id - nrOfBubblesBefore_]; } + // else: bubble ID is already mapped correctly + } + + mpi::RecvBuffer& recvBuffer() { return recvBuffer_; } + + private: + // return whether the next bubble was created on this process + bool wasNextBubbleCreatedOnThisProcess() const + { + // return whether counter of current bubble is between offset and offset+numberOfLocallyCreatedBubbles + return nextBubbleCtr_ >= localOffset_ && nextBubbleCtr_ < localOffset_ + nrOfLocallyCreatedBubbles_; + } + + mpi::RecvBuffer recvBuffer_; + + BubbleID nrOfBubblesBefore_; // size of global bubble array without new/deleted bubbles + BubbleID nrOfLocallyCreatedBubbles_; // number of bubbles that were added on this process + BubbleID localOffset_; // the offset of the locally created bubbles in recvBuffer + BubbleID nextBubbleCtr_; // number of bubbles that have already been stored with storeNextBubble() + + std::vector< BubbleID > temporalIDToNewIDMap_; // maps the temporal bubble IDs to new global bubble IDs +}; // class CommunicatedNewBubbles + +std::shared_ptr< CommunicatedNewBubbles > NewBubbleCommunication::communicate(size_t nrOfBubblesBefore) +{ + std::shared_ptr< MPIManager > mpiManager = MPIManager::instance(); + + // cast number of every process' new bubbles to int (int is used in MPI_Allgather) + int localNewBubbles = int_c(bubblesToCreate_.size()); + + std::vector< int > numNewBubblesPerProcess(uint_t(mpiManager->numProcesses()), 0); + + // communicate, i.e., gather each process' number of new bubbles + WALBERLA_MPI_SECTION() + { + MPI_Allgather(&localNewBubbles, 1, MPITrait< int >::type(), &numNewBubblesPerProcess[0], 1, + MPITrait< int >::type(), MPI_COMM_WORLD); + } + WALBERLA_NON_MPI_SECTION() { numNewBubblesPerProcess[0] = localNewBubbles; } + + // offsetVector[i] is the number of new bubbles created on processes with rank smaller than i + std::vector< int > offsetVector; + offsetVector.push_back(0); + for (size_t i = 0; i < numNewBubblesPerProcess.size() - 1; ++i) + { + offsetVector.push_back(offsetVector.back() + numNewBubblesPerProcess[i]); + } + + WALBERLA_ASSERT_EQUAL(offsetVector.size(), numNewBubblesPerProcess.size()); + + // get each process' individual offset + size_t offset = uint_c(offsetVector[uint_c(mpiManager->worldRank())]); + + // get the total number of new bubbles + size_t numNewBubbles = uint_c(offsetVector.back() + numNewBubblesPerProcess.back()); + + mpi::SendBuffer sendBuffer; + + size_t bytesPerBubble = mpi::BufferSizeTrait< Bubble >::size; + + // reserve space for a bubble's size in bytes (clean way to create send buffer) + sendBuffer.reserve(bytesPerBubble); + + // pack local new bubble data into sendBuffer + for (auto i = bubblesToCreate_.begin(); i != bubblesToCreate_.end(); ++i) + { + sendBuffer << *i; + } + + // compute the byte size of numNewBubblesPerProcess[i] and offsetVector[i] + for (uint_t i = uint_c(0); i < uint_c(mpiManager->numProcesses()); ++i) + { + numNewBubblesPerProcess[i] *= int_c(bytesPerBubble); + offsetVector[i] *= int_c(bytesPerBubble); + } + + // create new CommunicatedNewBubbles object to store the gathered new bubbles (see below) + auto result = std::make_shared< CommunicatedNewBubbles >(nrOfBubblesBefore, bubblesToCreate_.size(), offset); + + // resize recvBuffer for storing the total number of new bubbles + result->recvBuffer().resize(bytesPerBubble * numNewBubbles); + + WALBERLA_MPI_SECTION() + { + // communicate, i.e., gather all locally added bubbles (and store them in the new CommunicatedNewBubbles object) + MPI_Allgatherv(sendBuffer.ptr(), int_c(sendBuffer.size()), MPI_UNSIGNED_CHAR, result->recvBuffer().ptr(), + &numNewBubblesPerProcess[0], &offsetVector[0], MPI_UNSIGNED_CHAR, MPI_COMM_WORLD); + } + WALBERLA_NON_MPI_SECTION() { result->recvBuffer() = sendBuffer; } + + // all bubbles have been "created" and vector can be cleared + bubblesToCreate_.clear(); + + return result; +} + +void NewBubbleCommunication::communicateAndApply(std::vector< Bubble >& vectorToAddBubbles, + StructuredBlockStorage& blockStorage, BlockDataID bubbleFieldID) +{ + // communicate + std::shared_ptr< CommunicatedNewBubbles > newBubbleContainer = communicate(vectorToAddBubbles.size()); + + // append new bubbles to vectorToAddBubbles + BubbleID nextBubbleID = numeric_cast< BubbleID >(vectorToAddBubbles.size()); + while (newBubbleContainer->hasMoreBubbles()) + { + // create and append a new empty bubble + vectorToAddBubbles.emplace_back(); + + // update the empty bubble with the received bubble + newBubbleContainer->storeNextBubble(nextBubbleID, vectorToAddBubbles.back()); + + ++nextBubbleID; + } + + // rename bubble IDs + for (auto blockIt = blockStorage.begin(); blockIt != blockStorage.end(); ++blockIt) + { + BubbleField_T* const bubbleField = blockIt->getData< BubbleField_T >(bubbleFieldID); + for (auto fieldIt = bubbleField->begin(); fieldIt != bubbleField->end(); ++fieldIt) + { + // rename an existing bubble ID in bubbleField + if (*fieldIt != INVALID_BUBBLE_ID) { newBubbleContainer->mapTemporalToNewBubbleID(*fieldIt); } + } + } +} + +void NewBubbleCommunication::communicateAndApply(std::vector< Bubble >& vectorToAddBubbles, + const std::vector< bool >& bubblesToOverwrite, + StructuredBlockStorage& blockStorage, BlockDataID bubbleFieldID) +{ + WALBERLA_ASSERT_EQUAL(bubblesToOverwrite.size(), vectorToAddBubbles.size()); + + // communicate + std::shared_ptr< CommunicatedNewBubbles > newBubbleContainer = communicate(vectorToAddBubbles.size()); + + // overwrite existing bubbles with new bubbles; there must be at least the same number of new bubbles than in + // bubblesToOverwrite + for (size_t i = 0; i < bubblesToOverwrite.size(); ++i) + { + if (bubblesToOverwrite[i]) { newBubbleContainer->storeNextBubble(BubbleID(i), vectorToAddBubbles[i]); } + } + + // append remaining new bubbles to vectorToAddBubbles + BubbleID nextBubbleID = numeric_cast< BubbleID >(vectorToAddBubbles.size()); + while (newBubbleContainer->hasMoreBubbles()) + { + // create and append a new empty bubble + vectorToAddBubbles.emplace_back(); + + // update the empty bubble with the received bubble + newBubbleContainer->storeNextBubble(nextBubbleID, vectorToAddBubbles.back()); + + ++nextBubbleID; + } + + // rename bubble IDs + for (auto blockIt = blockStorage.begin(); blockIt != blockStorage.end(); ++blockIt) + { + BubbleField_T* const bubbleField = blockIt->getData< BubbleField_T >(bubbleFieldID); + for (auto fieldIt = bubbleField->begin(); fieldIt != bubbleField->end(); ++fieldIt) + { + // rename an existing bubble ID in bubbleField + if (*fieldIt != INVALID_BUBBLE_ID) { newBubbleContainer->mapTemporalToNewBubbleID(*fieldIt); } + } + } +} + +} // namespace bubble_model +} // namespace free_surface +} // namespace walberla diff --git a/src/lbm/free_surface/bubble_model/NewBubbleCommunication.h b/src/lbm/free_surface/bubble_model/NewBubbleCommunication.h new file mode 100644 index 000000000..0f5e83123 --- /dev/null +++ b/src/lbm/free_surface/bubble_model/NewBubbleCommunication.h @@ -0,0 +1,126 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file NewBubbleCommunication.h +//! \ingroup bubble_model +//! \author Martin Bauer +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Communication for the creation of new bubbles. +// +//====================================================================================================================== + +#pragma once + +#include "core/logging/Logging.h" + +#include <vector> + +#include "Bubble.h" +#include "BubbleDefinitions.h" + +namespace walberla +{ +namespace free_surface +{ +namespace bubble_model +{ +// forward declaration of internal data structure +class CommunicatedNewBubbles; + +/*********************************************************************************************************************** + * Communication for the creation of new bubbles. + * + * - Manages the distribution of newly created bubbles. + * - Bubbles must have the same BubbleID everywhere, since BubbleID is an index in the global bubble array. Thus, a + * problem arises when two (or more) processes add a new bubble in the same time step. + * - Every bubble is first created via this class and assigned a temporary BubbleID that is not used, yet. + * - These data are then synchronized in a communication step. + * - New bubbles are sorted by process rank such that there is then a sorted array of new bubbles that is identical on + * each process. + * - This vector of new bubbles can then be appended/inserted into the global bubble array. + * - Finally, temporary bubble IDs in the bubble field are renamed to global IDs. + * + * Usage example: + * \code + NewBubbleCommunication newBubbleComm; + + //the following lines are usually different on different processes: + BubbleID newID = newBubbleComm.createBubble( Bubble ( ... ) ); + BubbleID anotherID = newBubbleComm.createBubble( Bubble ( ... ) ); + // use the returned temporary bubble IDs in the BubbleField + + // The following call, updates the global bubble vector and the bubble field. + // Here the new bubbles are appended to the vector and the temporary bubble IDs in the bubble field are + // mapped to global IDs. + newBubbleComm.communicateAndApply( globalBubbleVector, blockStorage, bubbleFieldID ); + + * \endcode + * + * + **********************************************************************************************************************/ +class NewBubbleCommunication +{ + public: + explicit NewBubbleCommunication(uint_t nrOfExistingBubbles = uint_c(0)) + { + // initialize the temporary bubble ID with the currently registered number of bubbles + nextFreeBubbleID_ = numeric_cast< BubbleID >(nrOfExistingBubbles); + } + + // add a new bubble which gets assigned a temporary bubble ID that should be stored in bubbleField; temporary IDs are + // converted to valid global IDs in communicateAndApply() later + BubbleID createBubble(const Bubble& newBubble) + { + bubblesToCreate_.push_back(newBubble); + + // get a temporary bubble ID + return nextFreeBubbleID_++; + } + + // get the temporary bubble ID that is returned at the next call to createBubble() + BubbleID nextFreeBubbleID() const { return nextFreeBubbleID_; } + + /******************************************************************************************************************** + * Communicate all locally added bubbles and append them to a global bubble array. Convert the preliminary bubble IDs + * in the bubble field to global IDs. + *******************************************************************************************************************/ + void communicateAndApply(std::vector< Bubble >& vectorToAddBubbles, StructuredBlockStorage& blockStorage, + BlockDataID bubbleFieldID); + + /******************************************************************************************************************** + * Communicate all locally added bubbles and delete bubbles marked inside a boolean array. Convert the preliminary + * bubble IDs in the bubble field to global IDs. Instead of appending all new bubbles, first all "holes" in the + * globalVector are filled + * + * \Important: - the bubblesToOverwrite vector must be the same on all processes, i.e., it has to be communicated/ + * reduced before calling this function + * - bubblesToCreate_.size() > number of "true"s in bubblesToOverwrite, i.e., more new bubbles have to be + * created than deleted + *******************************************************************************************************************/ + void communicateAndApply(std::vector< Bubble >& vectorToAddBubbles, const std::vector< bool >& bubblesToOverwrite, + StructuredBlockStorage& blockStorage, BlockDataID bubbleFieldID); + + private: + // communicate all new local bubbles and store the (MPI) gathered new bubbles in the returned object; + // bubblesToCreate_ is cleared + std::shared_ptr< CommunicatedNewBubbles > communicate(size_t nrOfBubblesBefore); + + BubbleID nextFreeBubbleID_; // temporary bubble ID + std::vector< Bubble > bubblesToCreate_; +}; // class NewBubbleCommunication + +} // namespace bubble_model +} // namespace free_surface +} // namespace walberla diff --git a/src/lbm/free_surface/bubble_model/RegionalFloodFill.h b/src/lbm/free_surface/bubble_model/RegionalFloodFill.h new file mode 100644 index 000000000..c0f75536a --- /dev/null +++ b/src/lbm/free_surface/bubble_model/RegionalFloodFill.h @@ -0,0 +1,254 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file RegionalFloodFill.h +//! \ingroup bubble_model +//! \author Martin Bauer +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Flood fill algorithm in a constrained neighborhood around a cell. +// +//====================================================================================================================== + +#pragma once + +#include "field/GhostLayerField.h" + +namespace walberla +{ +namespace free_surface +{ +namespace bubble_model +{ +/*********************************************************************************************************************** + * + * Flood fill algorithm in a constrained neighborhood around a cell. + * + * This algorithm is used to check if a bubble has split up. A split detection is triggered + * when an interface cell becomes a fluid cell. The normal case is to check in a 1-neighborhood if + * the bubble has split up: ("-" means no bubble, numbers indicate bubble IDs) + * Example: 0 0 - + * - x - + * 0 0 - + * In this case, the bubble has split up. + * + * This check in a 1-neighborhood is performed by the function BubbleModel::mapNeighborhood(). + * + * To look only in 1-neighborhoods has the drawback that we trigger splits when no split has occurred. This is OK, since + * later it is detected that the bubble has not really split. However, this process requires communication and is + * therefore expensive. + * + * The RegionalFloodFill has the task to rule out splits before they are triggered by looking at larger neighborhoods. + * + * Example: (same as above but with larger neighborhood) + * 0 0 0 - - + * 0 0 0 - - + * 0 - x - - + * 0 0 0 - - + * 0 0 0 - - + * By looking at the 2-neighborhood we see that the bubble has not really split up. + * The RegionalFloodFill needs as input the current cell (marked with x), a direction to start, e.g., N, and the size of + * the neighborhood to look at, e.g., 2. In this case, it creates a small 5x5x5 field where a flood fill is executed. + * The field might be smaller if the cell is near the boundary, as the algorithm is capable of detecting boundaries. + * + **********************************************************************************************************************/ +template< typename T, typename Stencil_T > +class RegionalFloodFill +{ + public: + RegionalFloodFill(const GhostLayerField< T, 1 >* externField, const Cell& startCell, + stencil::Direction startDirection, const T& searchValue, cell_idx_t neighborhood = 2); + + RegionalFloodFill(const GhostLayerField< T, 1 >* externField, const Cell& startCell, const T& emptyValue, + cell_idx_t neighborhood = 2); + + ~RegionalFloodFill() { delete workingField_; } + + inline bool connected(stencil::Direction d); + inline bool connected(cell_idx_t xOff, cell_idx_t yOff, cell_idx_t zOff); + + const Field< bool, 1 >& workingField() const { return *workingField_; } + + protected: + inline bool cellInsideAndNotMarked(const Cell& cell); + + void runFloodFill(const Cell& startCell, stencil::Direction startDirection, const T& searchValue, + cell_idx_t neighborhood); + + const GhostLayerField< T, 1 >* externField_; + Field< bool, 1 >* workingField_; + + // the size of the working field is 2*neighborhood+1 for storing startCell (+1) and neighborhood in each direction; + // boundaries in externField_ reduce the size of workingField_ accordingly + CellInterval workingFieldSize_; + + T searchValue_; + Cell offset_; // offset of workingField_, allows coordinate transformations: + // externFieldCoordinates = workingFieldCoordinates + offset + Cell startCellInWorkingField_; +}; // class RegionalFloodFill + +// iterate over all neighboring cells and run flood fill with starting direction of first neighboring cell whose value +// is not equal to emptyValue +template< typename T, typename Stencil_T > +RegionalFloodFill< T, Stencil_T >::RegionalFloodFill(const GhostLayerField< T, 1 >* externField, const Cell& startCell, + const T& emptyValue, cell_idx_t neighborhood) + : externField_(externField), workingField_(nullptr) +{ + for (auto d = Stencil_T::begin(); d != Stencil_T::end(); ++d) + { + const T& neighborValue = externField_->getNeighbor(startCell, *d); + if (neighborValue != emptyValue) + { + // run flood fill with starting direction of first non-empty neighboring cell + runFloodFill(startCell, *d, neighborValue, neighborhood); + return; + } + } + + WALBERLA_ASSERT(false); // should not happen, only empty values in neighborhood +} + +/*********************************************************************************************************************** + * Create a small overlay field in given neighborhood and execute a flood fill in this small field. + * + * The flood fill starts in startCell and startDirection. The neighboring cell of startCell in startDirection has to be + * set to searchValue. The size of the neighborhood is specified by neighborhood. In case of nearby boundary in a + * certain direction, the size of the neighborhood is automatically reduced in this direction. + **********************************************************************************************************************/ +template< typename T, typename Stencil_T > +RegionalFloodFill< T, Stencil_T >::RegionalFloodFill(const GhostLayerField< T, 1 >* externField, const Cell& startCell, + stencil::Direction startDirection, const T& searchValue, + cell_idx_t neighborhood) + : externField_(externField), workingField_(nullptr) +{ + runFloodFill(startCell, startDirection, searchValue, neighborhood); +} + +template< typename T, typename Stencil_T > +void RegionalFloodFill< T, Stencil_T >::runFloodFill(const Cell& startCell, stencil::Direction startDirection, + const T& searchValue, cell_idx_t neighborhood) +{ + searchValue_ = searchValue; + + const cell_idx_t externFieldGhostLayers = cell_idx_c(externField_->nrOfGhostLayers()); + + // size of workingField_ is defined by the number of cells around startCell + cell_idx_t extendLower[3]; // number of cells in negative x-, y-, and z-direction of startCell + cell_idx_t extendUpper[3]; // number of cells in negative x-, y-, and z-direction of startCell + + // determine the size of the working field with respect to boundaries of externField_ + for (uint_t i = uint_c(0); i < uint_c(3); ++i) + { + const cell_idx_t minCoord = -externFieldGhostLayers; + const cell_idx_t maxCoord = cell_idx_c(externField_->size(i)) - cell_idx_c(1) + externFieldGhostLayers; + + // in case of nearby boundaries in externField_, the size of workingField_ is adjusted such that these boundaries + // are respected + extendLower[i] = std::min(neighborhood, startCell[i] - minCoord); + extendUpper[i] = std::min(neighborhood, maxCoord - startCell[i]); + } + + // offset_ can be used to transform coordinates from externField_ to coordinates of workingField_ + offset_ = Cell(startCell[0] - extendLower[0], startCell[1] - extendLower[1], startCell[2] - extendLower[2]); + + startCellInWorkingField_ = startCell - offset_; + + // create workingField_ + workingField_ = new Field< bool, 1 >(uint_c(1) + uint_c(extendLower[0] + extendUpper[0]), + uint_c(1) + uint_c(extendLower[1] + extendUpper[1]), + uint_c(1) + uint_c(extendLower[2] + extendUpper[2]), false); + + workingFieldSize_ = workingField_->xyzSize(); + + // mark the startCell such that flood fill can not take a path across startCell + workingField_->get(startCellInWorkingField_) = true; + + // use stack to store cells that still need to be searched + std::vector< Cell > stack; + stack.reserve(Stencil_T::Size); + stack.emplace_back(startCellInWorkingField_[0] + stencil::cx[startDirection], + startCellInWorkingField_[1] + stencil::cy[startDirection], + startCellInWorkingField_[2] + stencil::cz[startDirection]); + + while (!stack.empty()) + { + // next search cell is the last entry in stack + Cell currentCell = stack.back(); + + // remove the searched cell from stack + stack.pop_back(); + + WALBERLA_ASSERT_EQUAL(externField_->get(currentCell + offset_), searchValue_); + + // mark the searched cell to be connected in workingField_ + workingField_->get(currentCell) = true; + + // iterate over all existing neighbors that are not yet marked and push them onto the stack + for (auto d = Stencil_T::beginNoCenter(); d != Stencil_T::end(); ++d) + { + Cell neighbor(currentCell[0] + d.cx(), currentCell[1] + d.cy(), currentCell[2] + d.cz()); + + // check if neighboring cell is: + // - inside workingField_ + // - not yet marked as connected + // - connected to this cell + if (cellInsideAndNotMarked(neighbor)) + { + // add neighbor to stack such that it gets marked as connected and used as start cell in the next iteration; + // cells that are not connected to startCell will never get marked as connected + stack.push_back(neighbor); + } + } + } +} + +// check if startCell is connected to the neighboring cell in direction d +template< typename T, typename Stencil_T > +bool RegionalFloodFill< T, Stencil_T >::connected(stencil::Direction d) +{ + using namespace stencil; + return workingField_->get(startCellInWorkingField_[0] + cx[d], startCellInWorkingField_[1] + cy[d], + startCellInWorkingField_[2] + cz[d]); +} + +// check if startCell is connected to the cell with some offset from startCell +template< typename T, typename Stencil_T > +bool RegionalFloodFill< T, Stencil_T >::connected(cell_idx_t xOff, cell_idx_t yOff, cell_idx_t zOff) +{ + return workingField_->get(startCellInWorkingField_[0] + xOff, startCellInWorkingField_[1] + yOff, + startCellInWorkingField_[2] + zOff); +} + +// check if cell is: +// - inside workingField_ +// - not yet marked as connected +// - connected to this cell +template< typename T, typename Stencil_T > +bool RegionalFloodFill< T, Stencil_T >::cellInsideAndNotMarked(const Cell& cell) +{ + if (!workingFieldSize_.contains(cell)) { return false; } + + // check if cell is already marked as connected + if (!workingField_->get(cell)) + { + // test if cell is connected + return (externField_->get(cell + offset_) == searchValue_); + } + else { return false; } +} + +} // namespace bubble_model +} // namespace free_surface +} // namespace walberla diff --git a/src/lbm/free_surface/dynamics/CMakeLists.txt b/src/lbm/free_surface/dynamics/CMakeLists.txt new file mode 100644 index 000000000..458fabdfe --- /dev/null +++ b/src/lbm/free_surface/dynamics/CMakeLists.txt @@ -0,0 +1,17 @@ +target_sources( lbm + PRIVATE + CellConversionSweep.h + ConversionFlagsResetSweep.h + ExcessMassDistributionModel.h + ExcessMassDistributionSweep.h + ExcessMassDistributionSweep.impl.h + ForceWeightingSweep.h + PdfReconstructionModel.h + PdfRefillingModel.h + PdfRefillingSweep.h + PdfRefillingSweep.impl.h + StreamReconstructAdvectSweep.h + SurfaceDynamicsHandler.h + ) + +add_subdirectory( functionality ) \ No newline at end of file diff --git a/src/lbm/free_surface/dynamics/CellConversionSweep.h b/src/lbm/free_surface/dynamics/CellConversionSweep.h new file mode 100644 index 000000000..4e913b657 --- /dev/null +++ b/src/lbm/free_surface/dynamics/CellConversionSweep.h @@ -0,0 +1,315 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file CellConversionSweep.h +//! \ingroup dynamics +//! \author Martin Bauer +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Convert cells to/from interface cells. +// +//====================================================================================================================== + +#pragma once + +#include "field/FlagField.h" + +#include "lbm/field/MacroscopicValueCalculation.h" +#include "lbm/field/PdfField.h" +#include "lbm/free_surface/FlagInfo.h" +#include "lbm/free_surface/boundary/FreeSurfaceBoundaryHandling.h" +#include "lbm/free_surface/bubble_model/BubbleModel.h" + +namespace walberla +{ +namespace free_surface +{ +/*********************************************************************************************************************** + * Convert cells to/from interface + * - expects that a previous sweep has set the convertToLiquidFlag and convertToGasFlag flags + * - notifies the bubble model that conversions occurred + * - all cells that have been converted, have the "converted" flag set + * - PDF field is required in order to initialize the velocity in new gas cells with information from surrounding cells + * + * A) Interface -> Liquid/Gas + * - always converts interface to liquid + * - converts interface to gas only if no newly created liquid cell is in neighborhood + * B) Liquid/Gas -> Interface (to obtain a closed interface layer) + * - "old" liquid cells in the neighborhood of newly converted cells in A) are converted to interface + * - "old" gas cells in the neighborhood of newly converted cells in A) are converted to interface + * The term "old" refers to cells that were not converted themselves in the same time step. + * C) GAS -> INTERFACE (due to inflow boundary condition) + * D) LIQUID/GAS -> INTERFACE (due to wetting; only when using local triangulation for curvature computation) + * + * For gas cells that were converted to interface in B), the flag "convertedFromGasToInterface" is set to signal another + * sweep (i.e. "PdfRefillingSweep.h") that the this cell's PDFs need to be reinitialized. + * + * For gas cells that were converted to interface in C), the cell's PDFs are reinitialized with equilibrium constructed + * with the inflow velocity. + * + * More information can be found in the dissertation of N. Thuerey, 2007, section 4.3. + * ********************************************************************************************************************/ +template< typename LatticeModel_T, typename BoundaryHandling_T, typename ScalarField_T > +class CellConversionSweep +{ + public: + using FlagField_T = typename BoundaryHandling_T::FlagField; + using flag_t = typename FlagField_T::flag_t; + using PdfField_T = lbm::PdfField< LatticeModel_T >; + using Stencil_T = typename LatticeModel_T::Stencil; + + CellConversionSweep(BlockDataID handlingID, BlockDataID pdfFieldID, const FlagInfo< FlagField_T >& flagInfo, + BubbleModelBase* bubbleModel) + : handlingID_(handlingID), pdfFieldID_(pdfFieldID), bubbleModel_(bubbleModel), flagInfo_(flagInfo) + {} + + void operator()(IBlock* const block) + { + BoundaryHandling_T* const handling = block->getData< BoundaryHandling_T >(handlingID_); + PdfField_T* const pdfField = block->getData< PdfField_T >(pdfFieldID_); + + FlagField_T* const flagField = handling->getFlagField(); + + // A) INTERFACE -> LIQUID/GAS + // convert interface cells that have filled/emptied to liquid/gas (cflagInfo_. dissertation of N. Thuerey, 2007, + // section 4.3) + // the conversion is also performed in the first ghost layer, since B requires an up-to-date first ghost layer; + // explicitly avoid OpenMP, as bubble IDs are set here + WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ_OMP(flagField, uint_c(1), omp critical, { + const typename FlagField_T::ConstPtr flagFieldPtr(*flagField, x, y, z); + + // 1) convert interface cells to liquid + if (isFlagSet(flagFieldPtr, flagInfo_.convertToLiquidFlag)) + { + handling->removeFlag(flagInfo_.interfaceFlag, x, y, z); + handling->setFlag(flagInfo_.liquidFlag, x, y, z); + handling->setFlag(flagInfo_.convertedFlag, x, y, z); + + if (flagField->isInInnerPart(flagFieldPtr.cell())) + { + // register and detect splitting of bubbles + bubbleModel_->reportInterfaceToLiquidConversion(block, flagFieldPtr.cell()); + } + } + + // 2) convert interface cells to gas only if no newly converted liquid cell from 1) is in neighborhood to + // ensure a closed interface + if (isFlagSet(flagFieldPtr, flagInfo_.convertToGasFlag) && + !isFlagInNeighborhood< Stencil_T >(flagFieldPtr, flagInfo_.convertToLiquidFlag)) + { + handling->removeFlag(flagInfo_.interfaceFlag, x, y, z); + handling->setFlag(flagInfo_.gasFlag, x, y, z); + handling->setFlag(flagInfo_.convertedFlag, x, y, z); + } + }) // WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ_OMP + + // B) LIQUID/GAS -> INTERFACE + // convert those liquid/gas cells to interface that are in the neighborhood of the newly created liquid/gas cells + // from A; this maintains a closed interface layer; + // explicitly avoid OpenMP, as bubble IDs are set here + WALBERLA_FOR_ALL_CELLS_XYZ_OMP(flagField, omp critical, { + const typename FlagField_T::ConstPtr flagFieldPtr(*flagField, x, y, z); + + // only consider "old" liquid cells, i.e., cells that have not been converted in this time step + if (flagInfo_.isLiquid(flagFieldPtr) && !flagInfo_.hasConverted(flagFieldPtr)) + { + const flag_t newGasFlagMask = flagInfo_.convertedFlag | flagInfo_.gasFlag; // flag newly converted gas cell + + // the state of ghost layer cells becomes relevant here + for (auto d = LatticeModel_T::Stencil::beginNoCenter(); d != LatticeModel_T::Stencil::end(); ++d) + if (isMaskSet(flagFieldPtr.neighbor(*d), newGasFlagMask)) // newly converted gas cell is in neighborhood + { + // convert the current cell to interface + handling->removeFlag(flagInfo_.liquidFlag, x, y, z); + handling->setFlag(flagInfo_.interfaceFlag, x, y, z); + handling->setFlag(flagInfo_.convertedFlag, x, y, z); + + if (flagField->isInInnerPart(flagFieldPtr.cell())) + { + // register and detect merging of bubbles + bubbleModel_->reportLiquidToInterfaceConversion(block, flagFieldPtr.cell()); + } + + // current cell was already converted to interface, flags of other neighbors are not relevant + break; + } + } + // only consider "old" gas cells, i.e., cells that have not been converted in this time step + else + { + if (flagInfo_.isGas(flagFieldPtr) && !flagInfo_.hasConverted(flagFieldPtr)) + { + const flag_t newLiquidFlagMask = + flagInfo_.convertedFlag | flagInfo_.liquidFlag; // flag of newly converted liquid cell + + // the state of ghost layer cells is relevant here + for (auto d = LatticeModel_T::Stencil::beginNoCenter(); d != LatticeModel_T::Stencil::end(); ++d) + + // newly converted liquid cell is in neighborhood + if (isMaskSet(flagFieldPtr.neighbor(*d), newLiquidFlagMask)) + { + // convert the current cell to interface + handling->removeFlag(flagInfo_.gasFlag, x, y, z); + handling->setFlag(flagInfo_.interfaceFlag, x, y, z); + handling->setFlag(flagInfo_.convertedFlag, x, y, z); + handling->setFlag(flagInfo_.convertFromGasToInterfaceFlag, x, y, z); + + // current cell was already converted to interface, flags of other neighbors are not relevant + break; + } + } + } + }) // WALBERLA_FOR_ALL_CELLS_XYZ_OMP + + // C) GAS -> INTERFACE (due to inflow boundary condition) + // convert gas cells to interface cells near inflow boundaries; + // explicitly avoid OpenMP, such that cell conversions are performed sequentially + convertedFromGasToInterfaceDueToInflow.clear(); + WALBERLA_FOR_ALL_CELLS_XYZ_OMP(flagField, omp critical, { + const typename FlagField_T::ConstPtr flagFieldPtr(*flagField, x, y, z); + + if (flagInfo_.isConvertToInterfaceForInflow(flagFieldPtr) && !flagInfo_.hasConverted(flagFieldPtr)) + { + // newly converted liquid cell is in neighborhood + handling->removeFlag(flagInfo_.convertToInterfaceForInflowFlag, x, y, z); + + // convert the current cell to interface + handling->removeFlag(flagInfo_.gasFlag, x, y, z); + handling->setFlag(flagInfo_.interfaceFlag, x, y, z); + handling->setFlag(flagInfo_.convertedFlag, x, y, z); + convertedFromGasToInterfaceDueToInflow.insert(flagFieldPtr.cell()); + } + }) // WALBERLA_FOR_ALL_CELLS_XYZ_OMP + + // D) LIQUID/GAS -> INTERFACE (due to wetting; only active when using local triangulation for curvature + // computation) + // convert liquid/gas to interface cells where the interface cell is required for a smooth + // continuation of the wetting surface (see dissertation of S. Donath, 2011, section 6.3.5.3); + // explicitly avoid OpenMP, as bubble IDs are set here + WALBERLA_FOR_ALL_CELLS_XYZ_OMP(flagField, omp critical, { + const typename FlagField_T::ConstPtr flagFieldPtr(*flagField, x, y, z); + + // only consider wetting and non-interface cells + if (flagInfo_.isKeepInterfaceForWetting(flagFieldPtr) && !flagInfo_.isInterface(flagFieldPtr)) + { + // convert liquid cell to interface + if (isFlagSet(flagFieldPtr, flagInfo_.liquidFlag)) + { + handling->removeFlag(flagInfo_.liquidFlag, x, y, z); + handling->setFlag(flagInfo_.interfaceFlag, x, y, z); + handling->setFlag(flagInfo_.convertedFlag, x, y, z); + handling->removeFlag(flagInfo_.keepInterfaceForWettingFlag, x, y, z); + if (flagField->isInInnerPart(flagFieldPtr.cell())) + { + // register and detect merging of bubbles + bubbleModel_->reportLiquidToInterfaceConversion(block, flagFieldPtr.cell()); + } + } + else + { + // convert gas cell to interface + if (isFlagSet(flagFieldPtr, flagInfo_.gasFlag)) + { + handling->removeFlag(flagInfo_.gasFlag, x, y, z); + handling->setFlag(flagInfo_.interfaceFlag, x, y, z); + handling->setFlag(flagInfo_.convertedFlag, x, y, z); + handling->removeFlag(flagInfo_.keepInterfaceForWettingFlag, x, y, z); + handling->setFlag(flagInfo_.convertFromGasToInterfaceFlag, x, y, z); + } + } + } + }) // WALBERLA_FOR_ALL_CELLS_XYZ_OMP + + // initialize PDFs of interface cells that were created due to an inflow boundary; the PDFs are set to equilibrium + // with density=1 and velocity of the inflow boundary + initializeFromInflow(convertedFromGasToInterfaceDueToInflow, flagField, pdfField, handling); + } + + protected: + /******************************************************************************************************************** + * Initializes PDFs in cells that are converted to interface due to a neighboring inflow boundary. + * + * The PDFs of these cells are set to equilibrium values using density=1 and the average velocity of neighboring + * inflow boundaries. An inflow cell is used for averaging only if the velocity actually flows towards the newly + * created interface cell. In other words, the velocity direction is compared to the converted cell's direction with + * respect to the inflow location. + * + * REMARK: The inflow boundary condition must implement function "getValue()" that returns the prescribed velocity + * (see e.g. UBB). + *******************************************************************************************************************/ + void initializeFromInflow(const std::set< Cell >& cells, FlagField_T* flagField, PdfField_T* pdfField, + BoundaryHandling_T* handling) + { + for (auto setIt = cells.begin(); setIt != cells.end(); ++setIt) + { + const Cell& cell = *setIt; + + Vector3< real_t > u(real_c(0.0)); + uint_t numNeighbors = uint_c(0); + + // get UBB inflow boundary + auto ubbInflow = handling->template getBoundaryCondition< + typename FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::UBB_Inflow_T >( + handling->getBoundaryUID( + FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::ubbInflowFlagID)); + + for (auto i = LatticeModel_T::Stencil::beginNoCenter(); i != LatticeModel_T::Stencil::end(); ++i) + { + using namespace stencil; + const Cell neighborCell(cell[0] + i.cx(), cell[1] + i.cy(), cell[2] + i.cz()); + + const flag_t neighborFlag = flagField->get(neighborCell); + + // neighboring cell is inflow + if (isPartOfMaskSet(neighborFlag, flagInfo_.inflowFlagMask)) + { + // get direction towards cell containing inflow boundary + const Vector3< int > dir = Vector3< int >(-i.cx(), -i.cy(), -i.cz()); + + // get velocity from UBB boundary + const Vector3< real_t > inflowVel = + ubbInflow.getValue(cell[0] + i.cx(), cell[1] + i.cy(), cell[2] + i.cz()); + + // skip directions in which the corresponding velocity component is zero + if (realIsEqual(inflowVel[0], real_c(0), real_c(1e-14)) && dir[0] != 0) { continue; } + if (realIsEqual(inflowVel[1], real_c(0), real_c(1e-14)) && dir[1] != 0) { continue; } + if (realIsEqual(inflowVel[2], real_c(0), real_c(1e-14)) && dir[2] != 0) { continue; } + + // skip directions in which the corresponding velocity component is in opposite direction + if (inflowVel[0] > real_c(0) && dir[0] < 0) { continue; } + if (inflowVel[1] > real_c(0) && dir[1] < 0) { continue; } + if (inflowVel[2] > real_c(0) && dir[2] < 0) { continue; } + + // use inflow velocity to get average velocity + u += inflowVel; + numNeighbors++; + } + } + if (numNeighbors > uint_c(0)) { u /= real_c(numNeighbors); } // else: velocity is zero + + pdfField->setDensityAndVelocity(cell, u, real_c(1)); // set density=1 + } + } + + BlockDataID handlingID_; + BlockDataID pdfFieldID_; + BubbleModelBase* bubbleModel_; + + FlagInfo< FlagField_T > flagInfo_; + + std::set< Cell > convertedFromGasToInterfaceDueToInflow; +}; // class CellConversionSweep + +} // namespace free_surface +} // namespace walberla diff --git a/src/lbm/free_surface/dynamics/ConversionFlagsResetSweep.h b/src/lbm/free_surface/dynamics/ConversionFlagsResetSweep.h new file mode 100644 index 000000000..4eb74cede --- /dev/null +++ b/src/lbm/free_surface/dynamics/ConversionFlagsResetSweep.h @@ -0,0 +1,70 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file ResetFlagSweep.h +//! \ingroup dynamics +//! \author Martin Bauer +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Reset all free surface flags that mark cell conversions. +// +//====================================================================================================================== + +#pragma once + +#include "core/logging/Logging.h" + +#include "domain_decomposition/BlockDataID.h" + +#include "field/FlagField.h" + +#include "lbm/free_surface/FlagInfo.h" + +namespace walberla +{ +namespace free_surface +{ +/*********************************************************************************************************************** + * Reset all free surface flags that signal cell conversions. The flag "keepInterfaceForWettingFlag" is explicitly not + * reset since this flag must persist in the next time step. + **********************************************************************************************************************/ +template< typename FlagField_T > +class ConversionFlagsResetSweep +{ + public: + ConversionFlagsResetSweep(BlockDataID flagFieldID, const FlagInfo< FlagField_T >& flagInfo) + : flagFieldID_(flagFieldID), flagInfo_(flagInfo) + {} + + void operator()(IBlock* const block) + { + FlagField_T* const flagField = block->getData< FlagField_T >(flagFieldID_); + + // reset all conversion flags (except flagInfo_.keepInterfaceForWettingFlag) + const flag_t allConversionFlags = flagInfo_.convertToGasFlag | flagInfo_.convertToLiquidFlag | + flagInfo_.convertedFlag | flagInfo_.convertFromGasToInterfaceFlag | + flagInfo_.convertToInterfaceForInflowFlag; + WALBERLA_FOR_ALL_CELLS(flagFieldIt, flagField, + { removeMask(flagFieldIt, allConversionFlags); }) // WALBERLA_FOR_ALL_CELLS + } + + private: + using flag_t = typename FlagField_T::flag_t; + + BlockDataID flagFieldID_; + FlagInfo< FlagField_T > flagInfo_; +}; // class ConversionFlagsResetSweep + +} // namespace free_surface +} // namespace walberla diff --git a/src/lbm/free_surface/dynamics/ExcessMassDistributionModel.h b/src/lbm/free_surface/dynamics/ExcessMassDistributionModel.h new file mode 100644 index 000000000..2e4d0b798 --- /dev/null +++ b/src/lbm/free_surface/dynamics/ExcessMassDistributionModel.h @@ -0,0 +1,214 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file ExcessMassDistributionModel.h +//! \ingroup dynamics +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Class that specifies how excessive mass is distributed. +// +//====================================================================================================================== + +#pragma once + +#include "core/StringUtility.h" +#include "core/stringToNum.h" + +#include <string> + +namespace walberla +{ +namespace free_surface +{ +/*********************************************************************************************************************** + * Class that specifies how excessive mass is distributed after cell conversions from interface to liquid or interface + * to gas. + * For example, when converting an interface cell with fill level 1.1 to liquid with fill level 1,0, an excessive mass + * corresponding to the fill level 0.1 must be distributed. + * + * Available models: + * - EvenlyAllInterface: + * Excess mass is distributed evenly among all neighboring interface cells (see dissertations of T. Pohl, S. + * Donath, S. Bogner). + * + * - EvenlyNewInterface: + * Excess mass is distributed evenly among newly converted neighboring interface cells (see Koerner et al., 2005). + * Falls back to EvenlyAllInterface if not applicable. + * + * - EvenlyOldInterface: + * Excess mass is distributed evenly among old neighboring interface cells, i.e., cells that are non-newly + * converted to interface. Falls back to EvenlyAllInterface if not applicable. + * + * - WeightedAllInterface: + * Excess mass is distributed weighted with the direction of the interface normal among all neighboring interface + * cells (see dissertation of N. Thuerey, 2007). Falls back to EvenlyAllInterface if not applicable. + * + * - WeightedNewInterface: + * Excess mass is distributed weighted with the direction of the interface normal among newly converted + * neighboring interface cells. Falls back to WeightedAllInterface if not applicable. + * + * - WeightedOldInterface: + * Excess mass is distributed weighted with the direction of the interface normal among old neighboring interface + * cells, i.e., cells that are non-newly converted to interface. Falls back to WeightedAllInterface if not + * applicable. + * + * - EvenlyLiquidAndAllInterface: + * Excess mass is distributed evenly among all neighboring interface and liquid cells (see p.47 in master thesis of + * M. Lehmann, 2019). The excess mass distributed to liquid cells does neither modify the cell's density nor fill + * level. Instead, it is stored in an additional excess mass field. Therefore, not only the converted interface + * cells' excess mass is distributed, but also the excess mass of liquid cells stored in this additional field. + * + * - EvenlyLiquidAndAllInterfacePreferInterface: + * Similar to EvenlyLiquidAndAllInterface, however, excess mass is preferably distributed to interface cells. It is + * distributed to liquid cells only if there are no neighboring interface cells available. + * ********************************************************************************************************************/ +class ExcessMassDistributionModel +{ + public: + enum class ExcessMassModel { + EvenlyAllInterface, + EvenlyNewInterface, + EvenlyOldInterface, + WeightedAllInterface, + WeightedNewInterface, + WeightedOldInterface, + EvenlyLiquidAndAllInterface, + EvenlyLiquidAndAllInterfacePreferInterface + }; + + ExcessMassDistributionModel(const std::string& modelName) : modelName_(modelName), modelType_(chooseType(modelName)) + {} + + ExcessMassDistributionModel(const ExcessMassModel& modelType) + : modelName_(chooseName(modelType)), modelType_(modelType) + { + switch (modelType_) + { + case ExcessMassModel::EvenlyAllInterface: + break; + case ExcessMassModel::EvenlyNewInterface: + break; + case ExcessMassModel::EvenlyOldInterface: + break; + case ExcessMassModel::WeightedAllInterface: + break; + case ExcessMassModel::WeightedNewInterface: + break; + case ExcessMassModel::WeightedOldInterface: + break; + case ExcessMassModel::EvenlyLiquidAndAllInterface: + break; + case ExcessMassModel::EvenlyLiquidAndAllInterfacePreferInterface: + break; + } + } + + inline ExcessMassModel getModelType() const { return modelType_; } + inline std::string getModelName() const { return modelName_; } + inline std::string getFullModelSpecification() const { return getModelName(); } + + inline bool isEvenlyType() const + { + return modelType_ == ExcessMassModel::EvenlyAllInterface || modelType_ == ExcessMassModel::EvenlyNewInterface || + modelType_ == ExcessMassModel::EvenlyOldInterface; + } + + inline bool isWeightedType() const + { + return modelType_ == ExcessMassModel::WeightedAllInterface || + modelType_ == ExcessMassModel::WeightedNewInterface || modelType_ == ExcessMassModel::WeightedOldInterface; + } + + inline bool isEvenlyLiquidAndAllInterfacePreferInterfaceType() const + { + return modelType_ == ExcessMassModel::EvenlyLiquidAndAllInterface || + modelType_ == ExcessMassModel::EvenlyLiquidAndAllInterfacePreferInterface; + } + + static inline std::initializer_list< const ExcessMassModel > getTypeIterator() { return listOfAllEnums; } + + private: + ExcessMassModel chooseType(const std::string& modelName) + { + if (!string_icompare(modelName, "EvenlyAllInterface")) { return ExcessMassModel::EvenlyAllInterface; } + + if (!string_icompare(modelName, "EvenlyNewInterface")) { return ExcessMassModel::EvenlyNewInterface; } + + if (!string_icompare(modelName, "EvenlyOldInterface")) { return ExcessMassModel::EvenlyOldInterface; } + + if (!string_icompare(modelName, "WeightedAllInterface")) { return ExcessMassModel::WeightedAllInterface; } + + if (!string_icompare(modelName, "WeightedNewInterface")) { return ExcessMassModel::WeightedNewInterface; } + + if (!string_icompare(modelName, "WeightedOldInterface")) { return ExcessMassModel::WeightedOldInterface; } + + if (!string_icompare(modelName, "EvenlyLiquidAndAllInterface")) + { + return ExcessMassModel::EvenlyLiquidAndAllInterface; + } + + if (!string_icompare(modelName, "EvenlyLiquidAndAllInterfacePreferInterface")) + { + return ExcessMassModel::EvenlyLiquidAndAllInterfacePreferInterface; + } + + WALBERLA_ABORT("The specified PDF reinitialization model " << modelName << " is not available."); + } + + std::string chooseName(ExcessMassModel const& modelType) const + { + std::string modelName; + switch (modelType) + { + case ExcessMassModel::EvenlyAllInterface: + modelName = "EvenlyAllInterface"; + break; + case ExcessMassModel::EvenlyNewInterface: + modelName = "EvenlyNewInterface"; + break; + case ExcessMassModel::EvenlyOldInterface: + modelName = "EvenlyOldInterface"; + break; + case ExcessMassModel::WeightedAllInterface: + modelName = "WeightedAllInterface"; + break; + case ExcessMassModel::WeightedNewInterface: + modelName = "WeightedNewInterface"; + break; + case ExcessMassModel::WeightedOldInterface: + modelName = "WeightedOldInterface"; + break; + + case ExcessMassModel::EvenlyLiquidAndAllInterface: + modelName = "EvenlyLiquidAndAllInterface"; + break; + case ExcessMassModel::EvenlyLiquidAndAllInterfacePreferInterface: + modelName = "EvenlyLiquidAndAllInterfacePreferInterface"; + break; + } + return modelName; + } + + std::string modelName_; + ExcessMassModel modelType_; + static constexpr std::initializer_list< const ExcessMassModel > listOfAllEnums = { + ExcessMassModel::EvenlyAllInterface, ExcessMassModel::EvenlyNewInterface, + ExcessMassModel::EvenlyOldInterface, ExcessMassModel::WeightedAllInterface, + ExcessMassModel::WeightedNewInterface, ExcessMassModel::WeightedOldInterface, + ExcessMassModel::EvenlyLiquidAndAllInterface, ExcessMassModel::EvenlyLiquidAndAllInterfacePreferInterface + }; + +}; // class ExcessMassDistributionModel +} // namespace free_surface +} // namespace walberla \ No newline at end of file diff --git a/src/lbm/free_surface/dynamics/ExcessMassDistributionSweep.h b/src/lbm/free_surface/dynamics/ExcessMassDistributionSweep.h new file mode 100644 index 000000000..ab9efe397 --- /dev/null +++ b/src/lbm/free_surface/dynamics/ExcessMassDistributionSweep.h @@ -0,0 +1,212 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file ExcessMassDistributionSweep.h +//! \ingroup dynamics +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Distribute excess mass, i.e., mass that is undistributed after conversions from interface to liquid or gas. +// +//====================================================================================================================== + +#pragma once + +#include "core/logging/Logging.h" + +#include "domain_decomposition/BlockDataID.h" + +#include "field/FieldClone.h" +#include "field/FlagField.h" +#include "field/GhostLayerField.h" + +#include "lbm/field/PdfField.h" +#include "lbm/free_surface/FlagInfo.h" + +#include "ExcessMassDistributionModel.h" + +namespace walberla +{ +namespace free_surface +{ +/*********************************************************************************************************************** + * Distribute excess mass, i.e., mass that is undistributed after cells have been converted from interface to + * gas/liquid. For example, when converting an interface cell with fill level 1.1 to liquid with fill level 1.0, an + * excessive mass corresponding to the fill level 0.1 must be distributed to conserve mass. + **********************************************************************************************************************/ +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T > +class ExcessMassDistributionSweepBase +{ + public: + ExcessMassDistributionSweepBase(const ExcessMassDistributionModel& excessMassDistributionModel, + BlockDataID fillFieldID, ConstBlockDataID flagFieldID, ConstBlockDataID pdfFieldID, + const FlagInfo< FlagField_T >& flagInfo) + : excessMassDistributionModel_(excessMassDistributionModel), fillFieldID_(fillFieldID), flagFieldID_(flagFieldID), + pdfFieldID_(pdfFieldID), flagInfo_(flagInfo) + {} + + virtual void operator()(IBlock* const block) = 0; + + virtual ~ExcessMassDistributionSweepBase() = default; + + protected: + /******************************************************************************************************************** + * Determines the number of a cell's + * - neighboring newly-converted interface cells + * - neighboring interface cells (regardless if newly converted or not) + *******************************************************************************************************************/ + void getNumberOfInterfaceNeighbors(const FlagField_T* flagField, const Cell& cell, uint_t& newInterfaceNeighbors, + uint_t& interfaceNeighbors); + + /******************************************************************************************************************** + * Determines the number of a cell's neighboring liquid and interface cells. + *******************************************************************************************************************/ + void getNumberOfEvenlyLiquidAndAllInterfacePreferInterfaceNeighbors(const FlagField_T* flagField, const Cell& cell, + uint_t& liquidNeighbors, + uint_t& interfaceNeighbors); + + ExcessMassDistributionModel excessMassDistributionModel_; + BlockDataID fillFieldID_; + ConstBlockDataID flagFieldID_; + ConstBlockDataID pdfFieldID_; + FlagInfo< FlagField_T > flagInfo_; +}; // class ExcessMassDistributionSweep + +/*********************************************************************************************************************** + * Distribute the excess mass evenly among either + * - all neighboring interface cells (see dissertations of T. Pohl, S. Donath, S. Bogner). + * - newly converted interface cells (see Koerner et al., 2005) + * - old, i.e., non-newly converted interface cells + * + * If either no newly converted interface cell or old interface cell is available in the neighborhood, the + * respective other approach is used as fallback. + **********************************************************************************************************************/ +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T > +class ExcessMassDistributionSweepInterfaceEvenly + : public ExcessMassDistributionSweepBase< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > +{ + public: + using ExcessMassDistributionSweepBase_T = + ExcessMassDistributionSweepBase< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T >; + + ExcessMassDistributionSweepInterfaceEvenly(const ExcessMassDistributionModel& excessMassDistributionModel, + BlockDataID fillFieldID, ConstBlockDataID flagFieldID, + ConstBlockDataID pdfFieldID, const FlagInfo< FlagField_T >& flagInfo) + : ExcessMassDistributionSweepBase_T(excessMassDistributionModel, fillFieldID, flagFieldID, pdfFieldID, flagInfo) + {} + + ~ExcessMassDistributionSweepInterfaceEvenly() override = default; + + void operator()(IBlock* const block) override; + + private: + template< typename PdfField_T > + void distributeMassEvenly(ScalarField_T* fillField, const FlagField_T* flagField, const PdfField_T* pdfField, + const Cell& cell, real_t excessFill); +}; // class ExcessMassDistributionSweepInterfaceEvenly + +/*********************************************************************************************************************** + * Distribute the excess mass weighted with the direction of the interface normal among either + * - all neighboring interface cells (see section 4.3 in dissertation of N. Thuerey, 2007) + * - newly converted interface cells + * - old, i.e., non-newly converted interface cells + * + * If either no newly converted interface cell or old interface cell is available in the neighborhood, the + * respective other approach is used as fallback. + **********************************************************************************************************************/ +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T > +class ExcessMassDistributionSweepInterfaceWeighted + : public ExcessMassDistributionSweepBase< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > +{ + public: + using ExcessMassDistributionSweepBase_T = + ExcessMassDistributionSweepBase< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T >; + + ExcessMassDistributionSweepInterfaceWeighted(const ExcessMassDistributionModel& excessMassDistributionModel, + BlockDataID fillFieldID, ConstBlockDataID flagFieldID, + ConstBlockDataID pdfFieldID, const FlagInfo< FlagField_T >& flagInfo, + ConstBlockDataID normalFieldID) + : ExcessMassDistributionSweepBase_T(excessMassDistributionModel, fillFieldID, flagFieldID, pdfFieldID, flagInfo), + normalFieldID_(normalFieldID) + {} + + ~ExcessMassDistributionSweepInterfaceWeighted() override = default; + + void operator()(IBlock* const block) override; + + private: + template< typename PdfField_T > + void distributeMassWeighted(ScalarField_T* fillField, const FlagField_T* flagField, const PdfField_T* pdfField, + const VectorField_T* normalField, const Cell& cell, bool isNewLiquid, real_t excessFill); + + /******************************************************************************************************************** + * Returns vector with weights for excess mass distribution among neighboring cells. + *******************************************************************************************************************/ + void getExcessMassWeights(const FlagField_T* flagField, const VectorField_T* normalField, const Cell& cell, + bool isNewLiquid, bool useWeightedOld, bool useWeightedAll, bool useWeightedNew, + std::vector< real_t >& weights); + + /******************************************************************************************************************** + * Computes the weights for distributing the excess mass based on the direction of the interface normal (see equation + * (4.9) in dissertation of N. Thuerey, 2007) + *******************************************************************************************************************/ + void computeWeightWithNormal(real_t n_dot_ci, bool isNewLiquid, typename LatticeModel_T::Stencil::iterator dir, + std::vector< real_t >& weights); + + ConstBlockDataID normalFieldID_; + +}; // class ExcessMassDistributionSweepInterfaceWeighted + +/*********************************************************************************************************************** + * Distribute the excess mass evenly among + * - all neighboring liquid and interface cells (see p. 47 in master thesis of M. Lehmann, 2019) + * - all neighboring interface cells and only to liquid cells if there exists no neighboring interface cell + * + * Neither the fill level, nor the density of liquid cells is modified. Instead, the excess mass is stored in an + * additional field. + **********************************************************************************************************************/ +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T > +class ExcessMassDistributionSweepInterfaceAndLiquid + : public ExcessMassDistributionSweepBase< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > +{ + public: + using ExcessMassDistributionSweepBase_T = + ExcessMassDistributionSweepBase< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T >; + + ExcessMassDistributionSweepInterfaceAndLiquid(const ExcessMassDistributionModel& excessMassDistributionModel, + BlockDataID fillFieldID, ConstBlockDataID flagFieldID, + ConstBlockDataID pdfFieldID, const FlagInfo< FlagField_T >& flagInfo, + BlockDataID excessMassFieldID) + : ExcessMassDistributionSweepBase_T(excessMassDistributionModel, fillFieldID, flagFieldID, pdfFieldID, flagInfo), + excessMassFieldID_(excessMassFieldID), excessMassFieldClone_(excessMassFieldID) + {} + + ~ExcessMassDistributionSweepInterfaceAndLiquid() override = default; + + void operator()(IBlock* const block) override; + + private: + template< typename PdfField_T > + void distributeMassInterfaceAndLiquid(ScalarField_T* fillField, ScalarField_T* dstExcessMassField, + const FlagField_T* flagField, const PdfField_T* pdfField, const Cell& cell, + real_t excessMass); + + BlockDataID excessMassFieldID_; + field::FieldClone< ScalarField_T, true > excessMassFieldClone_; + +}; // class ExcessMassDistributionSweepInterfaceAndLiquid + +} // namespace free_surface +} // namespace walberla + +#include "ExcessMassDistributionSweep.impl.h" \ No newline at end of file diff --git a/src/lbm/free_surface/dynamics/ExcessMassDistributionSweep.impl.h b/src/lbm/free_surface/dynamics/ExcessMassDistributionSweep.impl.h new file mode 100644 index 000000000..ba30c8445 --- /dev/null +++ b/src/lbm/free_surface/dynamics/ExcessMassDistributionSweep.impl.h @@ -0,0 +1,594 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file ExcessMassDistributionSweep.impl.h +//! \ingroup dynamics +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Distribute excess mass, i.e., mass that is undistributed after conversions from interface to liquid or gas. +// +//====================================================================================================================== + +#pragma once + +#include "core/logging/Logging.h" + +#include "domain_decomposition/BlockDataID.h" + +#include "field/FlagField.h" +#include "field/GhostLayerField.h" + +#include "lbm/field/PdfField.h" +#include "lbm/free_surface/FlagInfo.h" + +#include "ExcessMassDistributionSweep.h" + +namespace walberla +{ +namespace free_surface +{ +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T > +void ExcessMassDistributionSweepInterfaceEvenly< LatticeModel_T, FlagField_T, ScalarField_T, + VectorField_T >::operator()(IBlock* const block) +{ + using Base_T = ExcessMassDistributionSweepBase_T; + + const FlagField_T* const flagField = block->getData< const FlagField_T >(Base_T::flagFieldID_); + ScalarField_T* const fillField = block->getData< ScalarField_T >(Base_T::fillFieldID_); + const lbm::PdfField< LatticeModel_T >* const pdfField = + block->getData< const lbm::PdfField< LatticeModel_T > >(Base_T::pdfFieldID_); + + // disable OpenMP to avoid mass distribution to neighboring cells before they have distributed their excess mass + WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ_OMP(fillField, uint_c(1), omp critical, { + const Cell cell(x, y, z); + + if (flagField->isFlagSet(cell, Base_T::flagInfo_.convertedFlag)) + { + // identify cells that were converted to gas/liquid in this time step + const bool newGas = flagField->isMaskSet(cell, Base_T::flagInfo_.convertedFlag | Base_T::flagInfo_.gasFlag); + const bool newLiquid = + flagField->isMaskSet(cell, Base_T::flagInfo_.convertedFlag | Base_T::flagInfo_.liquidFlag); + + if (newGas || newLiquid) + { + // a cell can not be converted to both gas and liquid + WALBERLA_ASSERT(!(newGas && newLiquid)); + + // calculate excess fill level + const real_t excessFill = newGas ? fillField->get(cell) : (fillField->get(cell) - real_c(1.0)); + + distributeMassEvenly(fillField, flagField, pdfField, cell, excessFill); + + if (newGas) { fillField->get(cell) = real_c(0.0); } + else { fillField->get(cell) = real_c(1.0); } + } + } + }) // WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ +} + +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T > +void ExcessMassDistributionSweepBase< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T >:: + getNumberOfEvenlyLiquidAndAllInterfacePreferInterfaceNeighbors(const FlagField_T* flagField, const Cell& cell, + uint_t& liquidNeighbors, uint_t& interfaceNeighbors) +{ + interfaceNeighbors = uint_c(0); + liquidNeighbors = uint_c(0); + + for (auto d = LatticeModel_T::Stencil::beginNoCenter(); d != LatticeModel_T::Stencil::end(); ++d) + { + const Cell neighborCell = Cell(cell.x() + d.cx(), cell.y() + d.cy(), cell.z() + d.cz()); + auto neighborFlags = flagField->get(neighborCell); + + if (isFlagSet(neighborFlags, flagInfo_.interfaceFlag)) { ++interfaceNeighbors; } + else + { + if (isFlagSet(neighborFlags, flagInfo_.liquidFlag)) { ++liquidNeighbors; } + } + } +} + +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T > +void ExcessMassDistributionSweepBase< LatticeModel_T, FlagField_T, ScalarField_T, + VectorField_T >::getNumberOfInterfaceNeighbors(const FlagField_T* flagField, + const Cell& cell, + uint_t& newInterfaceNeighbors, + uint_t& interfaceNeighbors) +{ + interfaceNeighbors = uint_c(0); + newInterfaceNeighbors = uint_c(0); + + for (auto d = LatticeModel_T::Stencil::beginNoCenter(); d != LatticeModel_T::Stencil::end(); ++d) + { + const Cell neighborCell = Cell(cell.x() + d.cx(), cell.y() + d.cy(), cell.z() + d.cz()); + auto neighborFlags = flagField->get(neighborCell); + + if (isFlagSet(neighborFlags, flagInfo_.interfaceFlag)) + { + ++interfaceNeighbors; + if (isFlagSet(neighborFlags, flagInfo_.convertedFlag)) { ++newInterfaceNeighbors; } + } + } +} + +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T > +template< typename PdfField_T > +void ExcessMassDistributionSweepInterfaceEvenly< LatticeModel_T, FlagField_T, ScalarField_T, + VectorField_T >::distributeMassEvenly(ScalarField_T* fillField, + const FlagField_T* flagField, + const PdfField_T* pdfField, + const Cell& cell, + real_t excessFill) +{ + using Base_T = ExcessMassDistributionSweepBase_T; + + bool useEvenlyAll = Base_T::excessMassDistributionModel_.getModelType() == + ExcessMassDistributionModel::ExcessMassModel::EvenlyAllInterface; + bool useEvenlyNew = Base_T::excessMassDistributionModel_.getModelType() == + ExcessMassDistributionModel::ExcessMassModel::EvenlyNewInterface; + bool useEvenlyOld = Base_T::excessMassDistributionModel_.getModelType() == + ExcessMassDistributionModel::ExcessMassModel::EvenlyOldInterface; + + // get number of interface neighbors + uint_t newInterfaceNeighbors = uint_c(0); + uint_t interfaceNeighbors = uint_c(0); + Base_T::getNumberOfInterfaceNeighbors(flagField, cell, newInterfaceNeighbors, interfaceNeighbors); + const uint_t oldInterfaceNeighbors = interfaceNeighbors - newInterfaceNeighbors; + + if (interfaceNeighbors == uint_c(0)) + { + WALBERLA_LOG_WARNING("No interface cell is in the neighborhood to distribute excess mass to. Mass is lost."); + return; + } + + // get density of the current cell + const real_t density = pdfField->getDensity(cell); + + // compute mass to be distributed to neighboring cells + real_t deltaMass = real_c(0); + if ((useEvenlyOld && oldInterfaceNeighbors > uint_c(0)) || newInterfaceNeighbors == uint_c(0)) + { + useEvenlyOld = true; + useEvenlyAll = false; + useEvenlyNew = false; + + deltaMass = excessFill / real_c(oldInterfaceNeighbors) * density; + } + else + { + if (useEvenlyNew || oldInterfaceNeighbors == uint_c(0)) + { + useEvenlyOld = false; + useEvenlyAll = false; + useEvenlyNew = true; + + deltaMass = excessFill / real_c(newInterfaceNeighbors) * density; + } + else + { + useEvenlyOld = false; + useEvenlyAll = true; + useEvenlyNew = false; + + deltaMass = excessFill / real_c(interfaceNeighbors) * density; + } + } + + // distribute the excess mass + for (auto pushDir = LatticeModel_T::Stencil::beginNoCenter(); pushDir != LatticeModel_T::Stencil::end(); ++pushDir) + { + const Cell neighborCell = Cell(cell.x() + pushDir.cx(), cell.y() + pushDir.cy(), cell.z() + pushDir.cz()); + + // do not push mass in the direction of the second ghost layer + // - inner domain: 0 to *Size()-1 + // - inner domain incl. first ghost layer: -1 to *Size() + if (neighborCell.x() < cell_idx_c(-1) || neighborCell.y() < cell_idx_c(-1) || neighborCell.z() < cell_idx_c(-1) || + neighborCell.x() > cell_idx_c(fillField->xSize()) || neighborCell.y() > cell_idx_c(fillField->ySize()) || + neighborCell.z() > cell_idx_c(fillField->zSize())) + { + continue; + } + + // only push mass to neighboring interface cells + if (flagField->isFlagSet(neighborCell, Base_T::flagInfo_.interfaceFlag)) + { + // get density of neighboring interface cell + const real_t neighborDensity = pdfField->getDensity(neighborCell); + + if (flagField->isFlagSet(neighborCell, Base_T::flagInfo_.convertedFlag) && (useEvenlyAll || useEvenlyNew)) + { + // push mass to newly converted interface cell + fillField->get(neighborCell) += deltaMass / neighborDensity; + } + else + { + if (!flagField->isFlagSet(neighborCell, Base_T::flagInfo_.convertedFlag) && (useEvenlyOld || useEvenlyAll)) + { + // push mass to old, i.e., non-newly converted interface cells + fillField->getNeighbor(cell, *pushDir) += deltaMass / neighborDensity; + } + } + } + } +} + +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T > +void ExcessMassDistributionSweepInterfaceWeighted< LatticeModel_T, FlagField_T, ScalarField_T, + VectorField_T >::operator()(IBlock* const block) +{ + using Base_T = ExcessMassDistributionSweepBase_T; + + const FlagField_T* const flagField = block->getData< const FlagField_T >(Base_T::flagFieldID_); + ScalarField_T* const fillField = block->getData< ScalarField_T >(Base_T::fillFieldID_); + const lbm::PdfField< LatticeModel_T >* const pdfField = + block->getData< const lbm::PdfField< LatticeModel_T > >(Base_T::pdfFieldID_); + const VectorField_T* const normalField = block->getData< const VectorField_T >(normalFieldID_); + + // disable OpenMP to avoid mass distribution to neighboring cells before they have distributed their excess mass + WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ_OMP(fillField, uint_c(1), omp critical, { + const Cell cell(x, y, z); + + if (flagField->isFlagSet(cell, Base_T::flagInfo_.convertedFlag)) + { + // identify cells that were converted to gas/liquid in this time step + const bool newGas = flagField->isMaskSet(cell, Base_T::flagInfo_.convertedFlag | Base_T::flagInfo_.gasFlag); + const bool newLiquid = + flagField->isMaskSet(cell, Base_T::flagInfo_.convertedFlag | Base_T::flagInfo_.liquidFlag); + + if (newGas || newLiquid) + { + // a cell can not be converted to both gas and liquid + WALBERLA_ASSERT(!(newGas && newLiquid)); + + // calculate excess fill level + const real_t excessFill = newGas ? fillField->get(cell) : (fillField->get(cell) - real_c(1.0)); + + distributeMassWeighted(fillField, flagField, pdfField, normalField, cell, newLiquid, excessFill); + + if (newGas) { fillField->get(cell) = real_c(0.0); } + else { fillField->get(cell) = real_c(1.0); } + } + } + }) // WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ +} + +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T > +template< typename PdfField_T > +void ExcessMassDistributionSweepInterfaceWeighted< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T >:: + distributeMassWeighted(ScalarField_T* fillField, const FlagField_T* flagField, const PdfField_T* pdfField, + const VectorField_T* normalField, const Cell& cell, bool isNewLiquid, real_t excessFill) +{ + using Base_T = ExcessMassDistributionSweepBase_T; + + bool useWeightedAll = Base_T::excessMassDistributionModel_.getModelType() == + ExcessMassDistributionModel::ExcessMassModel::WeightedAllInterface; + bool useWeightedNew = Base_T::excessMassDistributionModel_.getModelType() == + ExcessMassDistributionModel::ExcessMassModel::WeightedNewInterface; + bool useWeightedOld = Base_T::excessMassDistributionModel_.getModelType() == + ExcessMassDistributionModel::ExcessMassModel::WeightedOldInterface; + + // get number of interface neighbors + uint_t newInterfaceNeighbors = uint_c(0); + uint_t interfaceNeighbors = uint_c(0); + Base_T::getNumberOfInterfaceNeighbors(flagField, cell, newInterfaceNeighbors, interfaceNeighbors); + const uint_t oldInterfaceNeighbors = interfaceNeighbors - newInterfaceNeighbors; + + if (interfaceNeighbors == uint_c(0)) + { + WALBERLA_LOG_WARNING("No interface cell is in the neighborhood to distribute excess mass to. Mass is lost."); + return; + } + + // check applicability of the chosen model + if ((useWeightedOld && oldInterfaceNeighbors > uint_c(0)) || newInterfaceNeighbors == uint_c(0)) + { + useWeightedOld = true; + useWeightedAll = false; + useWeightedNew = false; + } + else + { + if (useWeightedNew || oldInterfaceNeighbors == uint_c(0)) + { + useWeightedOld = false; + useWeightedAll = false; + useWeightedNew = true; + } + else + { + useWeightedOld = false; + useWeightedAll = true; + useWeightedNew = false; + } + } + + // get normal-direction-based weights of the excess mass + std::vector< real_t > weights(LatticeModel_T::Stencil::Size, real_c(0)); + getExcessMassWeights(flagField, normalField, cell, isNewLiquid, useWeightedOld, useWeightedAll, useWeightedNew, + weights); + + // get the sum of all weights + real_t weightSum = real_c(0); + for (const auto& w : weights) + { + weightSum += w; + } + + // if there are either no old or no newly converted interface cells in normal direction, distribute mass to whatever + // interface cell is available in normal direction + if (realIsEqual(weightSum, real_c(0), real_c(1e-14)) && (useWeightedOld || useWeightedNew)) + { + useWeightedOld = false; + useWeightedAll = true; + useWeightedNew = false; + + // recompute mass weights since other type of interface cells are now considered also + getExcessMassWeights(flagField, normalField, cell, isNewLiquid, useWeightedOld, useWeightedAll, useWeightedNew, + weights); + + // update sum of all weights + for (const auto& w : weights) + { + weightSum += w; + } + + // if no interface cell is available in normal direction, distribute mass evenly to all neighboring interface + // cells + if (realIsEqual(weightSum, real_c(0), real_c(1e-14))) + { + WALBERLA_LOG_WARNING_ON_ROOT( + "Excess mass can not be distributed with a weighted approach since no interface cell is available in " + "normal direction. Distributing excess mass evenly among all surrounding interface cells."); + + // manually set weights to 1 to get equal weight in any direction + for (auto& w : weights) + { + w = real_c(1); + } + + // weight sum is now the number of neighboring interface cells + weightSum = real_c(interfaceNeighbors); + } + } + + WALBERLA_ASSERT_GREATER( + weightSum, real_c(0), + "Sum of all weights is zero in ExcessMassDistribution. This means that no neighboring interface cell is " + "available for distributing the excess mass to. This error should have been caught earlier."); + + const real_t excessMass = excessFill * pdfField->getDensity(cell); + + // distribute the excess mass + for (auto pushDir = LatticeModel_T::Stencil::beginNoCenter(); pushDir != LatticeModel_T::Stencil::end(); ++pushDir) + { + const Cell neighborCell = Cell(cell.x() + pushDir.cx(), cell.y() + pushDir.cy(), cell.z() + pushDir.cz()); + + // do not push mass in the direction of the second ghost layer + // - inner domain: 0 to *Size()-1 + // - inner domain incl. first ghost layer: -1 to *Size() + if (neighborCell.x() < cell_idx_c(-1) || neighborCell.y() < cell_idx_c(-1) || neighborCell.z() < cell_idx_c(-1) || + neighborCell.x() > cell_idx_c(fillField->xSize()) || neighborCell.y() > cell_idx_c(fillField->ySize()) || + neighborCell.z() > cell_idx_c(fillField->zSize())) + { + continue; + } + + // only push mass to neighboring interface cells + if (flagField->isFlagSet(neighborCell, Base_T::flagInfo_.interfaceFlag)) + { + // get density of neighboring interface cell + const real_t neighborDensity = pdfField->getDensity(neighborCell); + + if (flagField->isFlagSet(neighborCell, Base_T::flagInfo_.convertedFlag) && (useWeightedAll || useWeightedNew)) + { + // push mass to newly converted interface cell + const real_t deltaMass = excessMass * weights[pushDir.toIdx()] / weightSum; + fillField->get(neighborCell) += deltaMass / neighborDensity; + } + else + { + if (!flagField->isFlagSet(neighborCell, Base_T::flagInfo_.convertedFlag) && + (useWeightedOld || useWeightedAll)) + { + // push mass to old, i.e., non-newly converted interface cells + const real_t deltaMass = excessMass * weights[pushDir.toIdx()] / weightSum; + fillField->getNeighbor(cell, *pushDir) += deltaMass / neighborDensity; + } + } + } + } +} + +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T > +void ExcessMassDistributionSweepInterfaceWeighted< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T >:: + getExcessMassWeights(const FlagField_T* flagField, const VectorField_T* normalField, const Cell& cell, + bool isNewLiquid, bool useWeightedOld, bool useWeightedAll, bool useWeightedNew, + std::vector< real_t >& weights) +{ + using Base_T = ExcessMassDistributionSweepBase_T; + + // iterate all neighboring cells + for (auto d = LatticeModel_T::Stencil::beginNoCenter(); d != LatticeModel_T::Stencil::end(); ++d) + { + const Cell neighborCell = Cell(cell.x() + d.cx(), cell.y() + d.cy(), cell.z() + d.cz()); + auto neighborFlags = flagField->get(neighborCell); + + if (isFlagSet(neighborFlags, Base_T::flagInfo_.interfaceFlag)) + { + // compute dot product of normal direction and lattice direction to neighboring cell + const real_t n_dot_ci = + normalField->get(cell) * Vector3< real_t >(real_c(d.cx()), real_c(d.cy()), real_c(d.cz())); + + if (useWeightedAll || (useWeightedOld && !isFlagSet(neighborFlags, Base_T::flagInfo_.convertedFlag))) + { + computeWeightWithNormal(n_dot_ci, isNewLiquid, d, weights); + } + else + { + if (useWeightedNew && isFlagSet(neighborFlags, Base_T::flagInfo_.convertedFlag)) + { + computeWeightWithNormal(n_dot_ci, isNewLiquid, d, weights); + } + else { weights[d.toIdx()] = real_c(0); } + } + } + else + { + // no interface cell in this direction, weight is zero + weights[d.toIdx()] = real_c(0); + } + } +} + +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T > +void ExcessMassDistributionSweepInterfaceWeighted< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T >:: + computeWeightWithNormal(real_t n_dot_ci, bool isNewLiquid, typename LatticeModel_T::Stencil::iterator dir, + std::vector< real_t >& weights) +{ + // dissertation of N. Thuerey, 2007, equation (4.9) + if (isNewLiquid) + { + if (n_dot_ci > real_c(0)) { weights[dir.toIdx()] = n_dot_ci; } + else { weights[dir.toIdx()] = real_c(0); } + } + else // cell was converted from interface to gas + { + if (n_dot_ci < real_c(0)) { weights[dir.toIdx()] = -n_dot_ci; } + else { weights[dir.toIdx()] = real_c(0); } + } +} + +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T > +void ExcessMassDistributionSweepInterfaceAndLiquid< LatticeModel_T, FlagField_T, ScalarField_T, + VectorField_T >::operator()(IBlock* const block) +{ + using Base_T = ExcessMassDistributionSweepBase_T; + + const FlagField_T* const flagField = block->getData< const FlagField_T >(Base_T::flagFieldID_); + ScalarField_T* const fillField = block->getData< ScalarField_T >(Base_T::fillFieldID_); + const lbm::PdfField< LatticeModel_T >* const pdfField = + block->getData< const lbm::PdfField< LatticeModel_T > >(Base_T::pdfFieldID_); + + ScalarField_T* const srcExcessMassField = block->getData< ScalarField_T >(excessMassFieldID_); + ScalarField_T* const dstExcessMassField = excessMassFieldClone_.get(block); + + WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ(dstExcessMassField, uint_c(1), { + dstExcessMassField->get(x, y, z) = real_c(0); + }) // WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ + + // disable OpenMP to avoid mass distribution to neighboring cells before they have distributed their excess mass + WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ_OMP(fillField, uint_c(1), omp critical, { + const Cell cell(x, y, z); + + if (flagField->isFlagSet(cell, Base_T::flagInfo_.convertedFlag)) + { + // identify cells that were converted to gas/liquid in this time step + const bool newGas = flagField->isMaskSet(cell, Base_T::flagInfo_.convertedFlag | Base_T::flagInfo_.gasFlag); + const bool newLiquid = + flagField->isMaskSet(cell, Base_T::flagInfo_.convertedFlag | Base_T::flagInfo_.liquidFlag); + + if (newGas || newLiquid) + { + // a cell can not be converted to both gas and liquid + WALBERLA_ASSERT(!(newGas && newLiquid)); + + // calculate excess fill level + const real_t excessFill = newGas ? fillField->get(cell) : (fillField->get(cell) - real_c(1.0)); + + // store excess mass such that it can be distributed below + srcExcessMassField->get(cell) = excessFill * pdfField->getDensity(cell); + + if (newGas) { fillField->get(cell) = real_c(0.0); } + else { fillField->get(cell) = real_c(1.0); } + } + } + + if (!realIsEqual(srcExcessMassField->get(cell), real_c(0), real_c(1e-14))) + { + distributeMassInterfaceAndLiquid(fillField, dstExcessMassField, flagField, pdfField, cell, + srcExcessMassField->get(cell)); + } + }) // WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ + + srcExcessMassField->swapDataPointers(dstExcessMassField); +} + +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T > +template< typename PdfField_T > +void ExcessMassDistributionSweepInterfaceAndLiquid< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T >:: + distributeMassInterfaceAndLiquid(ScalarField_T* fillField, ScalarField_T* dstExcessMassField, + const FlagField_T* flagField, const PdfField_T* pdfField, const Cell& cell, + real_t excessMass) +{ + using Base_T = ExcessMassDistributionSweepBase_T; + + // get number of liquid and interface neighbors + uint_t liquidNeighbors = uint_c(0); + uint_t interfaceNeighbors = uint_c(0); + Base_T::getNumberOfEvenlyLiquidAndAllInterfacePreferInterfaceNeighbors(flagField, cell, liquidNeighbors, + interfaceNeighbors); + const uint_t EvenlyLiquidAndAllInterfacePreferInterfaceNeighbors = liquidNeighbors + interfaceNeighbors; + + if (EvenlyLiquidAndAllInterfacePreferInterfaceNeighbors == uint_c(0)) + { + WALBERLA_LOG_WARNING( + "No liquid or interface cell is in the neighborhood to distribute excess mass to. Mass is lost."); + return; + } + + const bool preferInterface = + Base_T::excessMassDistributionModel_.getModelType() == + ExcessMassDistributionModel::ExcessMassModel::EvenlyLiquidAndAllInterfacePreferInterface && + interfaceNeighbors > uint_c(0); + + // compute mass to be distributed to neighboring cells + real_t deltaMass; + if (preferInterface) { deltaMass = excessMass / real_c(interfaceNeighbors); } + else { deltaMass = excessMass / real_c(EvenlyLiquidAndAllInterfacePreferInterfaceNeighbors); } + + // distribute the excess mass + for (auto pushDir = LatticeModel_T::Stencil::beginNoCenter(); pushDir != LatticeModel_T::Stencil::end(); ++pushDir) + { + const Cell neighborCell = Cell(cell.x() + pushDir.cx(), cell.y() + pushDir.cy(), cell.z() + pushDir.cz()); + + // do not push mass to cells in the ghost layer (done by the process from which the ghost layer is synchronized) + // - inner domain: 0 to *Size()-1 + // - inner domain incl. first ghost layer: -1 to *Size() + if (neighborCell.x() <= cell_idx_c(-1) || neighborCell.y() <= cell_idx_c(-1) || + neighborCell.z() <= cell_idx_c(-1) || neighborCell.x() >= cell_idx_c(fillField->xSize()) || + neighborCell.y() >= cell_idx_c(fillField->ySize()) || neighborCell.z() >= cell_idx_c(fillField->zSize())) + { + continue; + } + + if (flagField->isFlagSet(neighborCell, Base_T::flagInfo_.interfaceFlag)) + { + // get density of neighboring interface cell + const real_t neighborDensity = pdfField->getDensity(neighborCell); + + // add excess mass directly to fill level for neighboring interface cells + fillField->get(neighborCell) += deltaMass / neighborDensity; + } + else + { + if (flagField->isFlagSet(neighborCell, Base_T::flagInfo_.liquidFlag) && !preferInterface) + { + // add excess mass to excessMassField for neighboring liquid cells + dstExcessMassField->get(neighborCell) += deltaMass; + } + } + } +} + +} // namespace free_surface +} // namespace walberla diff --git a/src/lbm/free_surface/dynamics/ForceWeightingSweep.h b/src/lbm/free_surface/dynamics/ForceWeightingSweep.h new file mode 100644 index 000000000..4a13fb3b4 --- /dev/null +++ b/src/lbm/free_surface/dynamics/ForceWeightingSweep.h @@ -0,0 +1,96 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file ForceWeightingSweep.h +//! \ingroup dynamics +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Weight force in interface cells with fill level and density (equation 15 in Koerner et al., 2005). +// +//====================================================================================================================== + +#pragma once + +#include "core/logging/Logging.h" + +#include "domain_decomposition/BlockDataID.h" + +#include "field/FlagField.h" +#include "field/GhostLayerField.h" + +#include "lbm/field/PdfField.h" +#include "lbm/free_surface/FlagInfo.h" + +namespace walberla +{ +namespace free_surface +{ +/*********************************************************************************************************************** + * Weight the specified force in interface cells according to their density and fill level, as in equation 15 in Koerner + * et al., 2005. + * In liquid cells, the force is set to the specified constant global force. + **********************************************************************************************************************/ +template< typename LatticeModel_T, typename FlagField_T, typename VectorField_T, typename ScalarField_T > +class ForceWeightingSweep +{ + public: + ForceWeightingSweep(BlockDataID forceFieldID, ConstBlockDataID pdfFieldID, ConstBlockDataID flagFieldID, + ConstBlockDataID fillFieldID, const FlagInfo< FlagField_T >& flagInfo, + const Vector3< real_t >& globalForce) + : forceFieldID_(forceFieldID), pdfFieldID_(pdfFieldID), flagFieldID_(flagFieldID), fillFieldID_(fillFieldID), + flagInfo_(flagInfo), globalForce_(globalForce) + {} + + void operator()(IBlock* const block) + { + using PdfField_T = lbm::PdfField< LatticeModel_T >; + + VectorField_T* const forceField = block->getData< VectorField_T >(forceFieldID_); + const PdfField_T* const pdfField = block->getData< const PdfField_T >(pdfFieldID_); + const FlagField_T* const flagField = block->getData< const FlagField_T >(flagFieldID_); + const ScalarField_T* const fillField = block->getData< const ScalarField_T >(fillFieldID_); + + WALBERLA_FOR_ALL_CELLS(forceFieldIt, forceField, pdfFieldIt, pdfField, flagFieldIt, flagField, fillFieldIt, + fillField, { + flag_t flag = *flagFieldIt; + + // set force in interface cells to globalForce_ * density * fillLevel (see equation 15 + // in Koerner et al., 2005) + if (flagInfo_.isInterface(flag)) + { + const real_t density = pdfField->getDensity(pdfFieldIt.cell()); + const real_t fillLevel = *fillFieldIt; + *forceFieldIt = globalForce_ * fillLevel * density; + } + else + { + // set force to globalForce_ in all non-interface cells + *forceFieldIt = globalForce_; + } + }) // WALBERLA_FOR_ALL_CELLS + } + + private: + using flag_t = typename FlagField_T::flag_t; + + BlockDataID forceFieldID_; + ConstBlockDataID pdfFieldID_; + ConstBlockDataID flagFieldID_; + ConstBlockDataID fillFieldID_; + FlagInfo< FlagField_T > flagInfo_; + Vector3< real_t > globalForce_; +}; // class ForceWeightingSweep + +} // namespace free_surface +} // namespace walberla diff --git a/src/lbm/free_surface/dynamics/PdfReconstructionModel.h b/src/lbm/free_surface/dynamics/PdfReconstructionModel.h new file mode 100644 index 000000000..9468f06b3 --- /dev/null +++ b/src/lbm/free_surface/dynamics/PdfReconstructionModel.h @@ -0,0 +1,173 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file PdfReconstructionModel.h +//! \ingroup dynamics +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Class that specifies the number of reconstructed PDFs at the free surface interface. +// +//====================================================================================================================== + +#pragma once + +#include "core/StringUtility.h" +#include "core/stringToNum.h" + +namespace walberla +{ +namespace free_surface +{ +/*********************************************************************************************************************** + * Class that specifies the number of reconstructed PDFs at the free surface interface. PDFs need to be reconstructed + * as they might be missing, i.e., PDFs streaming from gas to interface are not available inherently. + * + * Available models: + * - NormalBasedKeepCenter: reconstruct all PDFs for which n * c_i >= 0 (approach by Koerner et al., 2005); some + * already available PDFs will be overwritten + * + * - NormalBasedReconstructCenter: reconstruct all PDFs for which n * c_i >= 0 (including the center PDF); some already + * available PDFs coming from liquid will be overwritten + * + * - OnlyMissing: reconstruct only missing PDFs (no already available PDF gets overwritten) + * + * - All: reconstruct all PDFs (any already available PDF is overwritten) + * + * - OnlyMissingMin-N-largest: Reconstruct only missing PDFs but at least N (and therefore potentially overwrite + * available PDFs); "smallest" or "largest" specifies whether PDFs with smallest or largest + * n * c_i get overwritten first. This model is motivated by the dissertation of Simon + * Bogner, 2017, section 4.2.1, where it is argued that at least 3 PDFs must be + * reconstructed, as otherwise the free surface boundary condition is claimed to be + * underdetermined. However, a mathematical proof for this statement is not given. + * + * - OnlyMissingMin-N-smallest: see comment at "OnlyMissingMin-N-largest" + * + * - OnlyMissingMin-N-normalBasedKeepCenter: see comment at "OnlyMissingMin-N-largest"; if less than N PDFs are unknown, + * reconstruct according to the model "NormalBasedKeepCenter" + * ********************************************************************************************************************/ +class PdfReconstructionModel +{ + public: + enum class ReconstructionModel { + NormalBasedReconstructCenter, + NormalBasedKeepCenter, + OnlyMissing, + All, + OnlyMissingMin, + }; + + enum class FallbackModel { + Largest, + Smallest, + NormalBasedKeepCenter, + }; + + PdfReconstructionModel(const std::string& modelName) : modelName_(modelName), modelType_(chooseType(modelName)) + { + if (modelType_ == ReconstructionModel::OnlyMissingMin) + { + const std::vector< std::string > substrings = string_split(modelName, "-"); + + modelName_ = substrings[0]; // "OnlyMissingMin" + numMinReconstruct_ = stringToNum< uint_t >(substrings[1]); // N + fallbackModelName_ = substrings[2]; // "smallest" or "largest" or "normalBasedKeepCenter" + fallbackModel_ = chooseFallbackModel(fallbackModelName_); + + if (fallbackModel_ != FallbackModel::Largest && fallbackModel_ != FallbackModel::Smallest && + fallbackModel_ != FallbackModel::NormalBasedKeepCenter) + { + WALBERLA_ABORT("The specified PDF reconstruction fallback-model " << modelName << " is not available."); + } + } + } + + inline ReconstructionModel getModelType() const { return modelType_; } + inline std::string getModelName() const { return modelName_; } + inline uint_t getNumMinReconstruct() const { return numMinReconstruct_; } + inline FallbackModel getFallbackModel() const { return fallbackModel_; } + inline std::string getFallbackModelName() const { return fallbackModelName_; } + inline std::string getFullModelSpecification() const + { + if (modelType_ == ReconstructionModel::OnlyMissingMin) + { + return modelName_ + "-" + std::to_string(numMinReconstruct_) + "-" + fallbackModelName_; + } + else { return modelName_; } + } + + private: + ReconstructionModel chooseType(const std::string& modelName) + { + if (!string_icompare(modelName, "NormalBasedReconstructCenter")) + { + return ReconstructionModel::NormalBasedReconstructCenter; + } + + else + { + if (!string_icompare(modelName, "NormalBasedKeepCenter")) + { + return ReconstructionModel::NormalBasedKeepCenter; + } + else + { + if (!string_icompare(modelName, "OnlyMissing")) { return ReconstructionModel::OnlyMissing; } + else + { + if (!string_icompare(modelName, "All")) { return ReconstructionModel::All; } + else + { + if (!string_icompare(string_split(modelName, "-")[0], "OnlyMissingMin")) + { + return ReconstructionModel::OnlyMissingMin; + } + else + { + WALBERLA_ABORT("The specified PDF reconstruction model " << modelName << " is not available."); + } + } + } + } + } + } + + FallbackModel chooseFallbackModel(const std::string& fallbackModelName) + { + if (!string_icompare(fallbackModelName, "largest")) { return FallbackModel::Largest; } + else + { + if (!string_icompare(fallbackModelName, "smallest")) { return FallbackModel::Smallest; } + else + { + if (!string_icompare(fallbackModelName, "normalBasedKeepCenter")) + { + return FallbackModel::NormalBasedKeepCenter; + } + else + { + WALBERLA_ABORT("The specified PDF reconstruction fallback-model " << fallbackModelName + << " is not available."); + } + } + } + } + + std::string modelName_; + ReconstructionModel modelType_; + uint_t numMinReconstruct_; + std::string fallbackModelName_; + FallbackModel fallbackModel_; +}; // class PdfReconstructionModel +} // namespace free_surface +} // namespace walberla \ No newline at end of file diff --git a/src/lbm/free_surface/dynamics/PdfRefillingModel.h b/src/lbm/free_surface/dynamics/PdfRefillingModel.h new file mode 100644 index 000000000..ffd86279c --- /dev/null +++ b/src/lbm/free_surface/dynamics/PdfRefillingModel.h @@ -0,0 +1,147 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file PdfRefillingModel.h +//! \ingroup dynamics +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \author Michael Zikeli +//! \brief Defines how cells are refilled (i.e. PDFs reinitialized) after the cell was converted from gas to interface. +// +//====================================================================================================================== + +#pragma once + +#include "core/StringUtility.h" + +namespace walberla +{ +namespace free_surface +{ +/*********************************************************************************************************************** + * Class that specifies how PDFs are reinitialized in cells that are converted from gas to interface. + * + * Available options are: + * - EquilibriumRefilling: + * initialize PDFs with equilibrium using average density and velocity from neighboring cells; default + * approach used in any known publication with free surface LBM + * + * - AverageRefilling: + * initialize PDFs with average PDFs (in the respective directions) of neighboring cells + * + * - EquilibriumAndNonEquilibriumRefilling: + * initialize PDFs with EquilibriumRefilling and add the non-equilibrium contribution of neighboring cells + * + * - ExtrapolationRefilling: + * initialize PDFs with PDFs extrapolated (in surface normal direction) from neighboring cells + * + * - GradsMomentsRefilling: + * initialize PDFs with EquilibriumRefilling and add the contribution of the non-equilibrium pressure + * tensor + * + * See src/lbm/free_surface/dynamics/PdfRefillingSweep.h for a detailed description of the models. + * + * The models and their implementation are inspired by the equivalent functionality of the lbm-particle coupling, see + * src/lbm_mesapd_coupling/momentum_exchange_method/reconstruction/Reconstructor.h. + **********************************************************************************************************************/ +class PdfRefillingModel +{ + public: + enum class RefillingModel { + EquilibriumRefilling, + AverageRefilling, + EquilibriumAndNonEquilibriumRefilling, + ExtrapolationRefilling, + GradsMomentsRefilling + }; + + PdfRefillingModel(const std::string& modelName) : modelName_(modelName), modelType_(chooseType(modelName)) {} + + PdfRefillingModel(const RefillingModel& modelType) : modelName_(chooseName(modelType)), modelType_(modelType) + { + switch (modelType_) + { + case RefillingModel::EquilibriumRefilling: + break; + case RefillingModel::AverageRefilling: + break; + case RefillingModel::EquilibriumAndNonEquilibriumRefilling: + break; + case RefillingModel::ExtrapolationRefilling: + break; + case RefillingModel::GradsMomentsRefilling: + break; + } + } + + inline RefillingModel getModelType() const { return modelType_; } + inline std::string getModelName() const { return modelName_; } + inline std::string getFullModelSpecification() const { return getModelName(); } + + static inline std::initializer_list< const RefillingModel > getTypeIterator() { return listOfAllEnums; } + + private: + RefillingModel chooseType(const std::string& modelName) + { + if (!string_icompare(modelName, "EquilibriumRefilling")) { return RefillingModel::EquilibriumRefilling; } + + if (!string_icompare(modelName, "AverageRefilling")) { return RefillingModel::AverageRefilling; } + + if (!string_icompare(modelName, "EquilibriumAndNonEquilibriumRefilling")) + { + return RefillingModel::EquilibriumAndNonEquilibriumRefilling; + } + + if (!string_icompare(modelName, "ExtrapolationRefilling")) { return RefillingModel::ExtrapolationRefilling; } + + if (!string_icompare(modelName, "GradsMomentsRefilling")) { return RefillingModel::GradsMomentsRefilling; } + + WALBERLA_ABORT("The specified PDF reinitialization model " << modelName << " is not available."); + } + + std::string chooseName(RefillingModel const& modelType) const + { + std::string modelName; + switch (modelType) + { + case RefillingModel::EquilibriumRefilling: + modelName = "EquilibriumRefilling"; + break; + case RefillingModel::AverageRefilling: + modelName = "AverageRefilling"; + break; + case RefillingModel::EquilibriumAndNonEquilibriumRefilling: + modelName = "EquilibriumAndNonEquilibriumRefilling"; + break; + case RefillingModel::ExtrapolationRefilling: + modelName = "ExtrapolationRefilling"; + break; + case RefillingModel::GradsMomentsRefilling: + modelName = "GradsMomentsRefilling"; + break; + } + return modelName; + } + + std::string modelName_; + RefillingModel modelType_; + static constexpr std::initializer_list< const RefillingModel > listOfAllEnums = { + RefillingModel::EquilibriumRefilling, RefillingModel::AverageRefilling, + RefillingModel::EquilibriumAndNonEquilibriumRefilling, RefillingModel::ExtrapolationRefilling, + RefillingModel::GradsMomentsRefilling + }; + +}; // class PdfRefillingModel +} // namespace free_surface +} // namespace walberla \ No newline at end of file diff --git a/src/lbm/free_surface/dynamics/PdfRefillingSweep.h b/src/lbm/free_surface/dynamics/PdfRefillingSweep.h new file mode 100644 index 000000000..8c1c74513 --- /dev/null +++ b/src/lbm/free_surface/dynamics/PdfRefillingSweep.h @@ -0,0 +1,447 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file PdfRefillingSweep.h +//! \ingroup dynamics +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \author Michael Zikeli +//! \brief Sweeps for refilling cells (i.e. reinitializing PDFs) after the cell was converted from gas to interface. +//====================================================================================================================== + +#pragma once + +#include "core/DataTypes.h" +#include "core/StringUtility.h" +#include "core/cell/Cell.h" +#include "core/debug/CheckFunctions.h" +#include "core/logging/Logging.h" +#include "core/math/Vector3.h" + +#include "domain_decomposition/IBlock.h" + +#include "field/GhostLayerField.h" + +#include "lbm/field/PdfField.h" +#include "lbm/free_surface/FlagInfo.h" +#include "lbm/free_surface/dynamics/PdfRefillingModel.h" +#include "lbm/free_surface/surface_geometry/NormalSweep.h" + +#include "stencil/D3Q6.h" + +#include <functional> +#include <vector> + +#include "PdfRefillingModel.h" + +namespace walberla +{ +namespace free_surface +{ +/*********************************************************************************************************************** + * Base class for all sweeps to reinitialize (refill) all PDFs in cells that were converted from gas to interface. + * This is required since gas cells do not have PDFs. The sweep expects that a previous sweep has set the + * "convertedFromGasToInterface" flag and reinitializes the PDFs in all cells with this flag according to the specified + * model. + **********************************************************************************************************************/ +template< typename LatticeModel_T, typename FlagField_T > +class RefillingSweepBase +{ + public: + using PdfField_T = lbm::PdfField< LatticeModel_T >; + using flag_t = typename FlagField_T::flag_t; + using Stencil_T = typename LatticeModel_T::Stencil; + + RefillingSweepBase(const BlockDataID& pdfFieldID, const ConstBlockDataID& flagFieldID, + const FlagInfo< FlagField_T >& flagInfo, bool useDataFromGhostLayers) + : pdfFieldID_(pdfFieldID), flagFieldID_(flagFieldID), flagInfo_(flagInfo), + useDataFromGhostLayers_(useDataFromGhostLayers) + {} + + virtual void operator()(IBlock* const block) = 0; + + virtual ~RefillingSweepBase() = default; + + real_t getAverageDensityAndVelocity(const Cell& cell, const PdfField_T& pdfField, const FlagField_T& flagField, + const FlagInfo< FlagField_T >& flagInfo, Vector3< real_t >& avgVelocity) + { + std::vector< bool > validStencilIndices(Stencil_T::Size, false); + return getAverageDensityAndVelocity(cell, pdfField, flagField, flagInfo, avgVelocity, validStencilIndices); + } + + // also stores stencil indices of valid neighboring cells (liquid/ non-newly converted interface) in a vector + real_t getAverageDensityAndVelocity(const Cell& cell, const PdfField_T& pdfField, const FlagField_T& flagField, + const FlagInfo< FlagField_T >& flagInfo, Vector3< real_t >& avgVelocity, + std::vector< bool >& validStencilIndices); + + // returns the averaged PDFs of valid neighboring cells (liquid/ non-newly converted interface) + std::vector< real_t > getAveragePdfs(const Cell& cell, const PdfField_T& pdfField, const FlagField_T& flagField, + const FlagInfo< FlagField_T >& flagInfo); + + protected: + BlockDataID pdfFieldID_; + ConstBlockDataID flagFieldID_; + FlagInfo< FlagField_T > flagInfo_; + bool useDataFromGhostLayers_; +}; // class RefillingSweepBase + +/*********************************************************************************************************************** + * Base class for refilling models that need to obtain information by extrapolation from neighboring cells. + * + * The parameter "useDataFromGhostLayers" is useful, when reducedCommunication is used, i.e., when not all PDFs are + * communicated in the PDF field but only those that are actually required in a certain direction. This is currently not + * used in the free surface part of waLBerla: In SurfaceDynamicsHandler, SimpleCommunication uses the default PackInfo + * of the GhostLayerField for PDF communication. The optimized version would be using the PdfFieldPackInfo (in + * src/lbm/communication). + **********************************************************************************************************************/ +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T > +class ExtrapolationRefillingSweepBase : public RefillingSweepBase< LatticeModel_T, FlagField_T > +{ + public: + using RefillingSweepBase_T = RefillingSweepBase< LatticeModel_T, FlagField_T >; + using PdfField_T = typename RefillingSweepBase_T::PdfField_T; + using flag_t = typename RefillingSweepBase_T::flag_t; + using Stencil_T = typename RefillingSweepBase_T::Stencil_T; + + ExtrapolationRefillingSweepBase(const BlockDataID& pdfFieldID, const ConstBlockDataID& flagFieldID, + const ConstBlockDataID& fillFieldID, const FlagInfo< FlagField_T >& flagInfo, + uint_t extrapolationOrder, bool useDataFromGhostLayers) + : RefillingSweepBase_T(pdfFieldID, flagFieldID, flagInfo, useDataFromGhostLayers), fillFieldID_(fillFieldID), + extrapolationOrder_(extrapolationOrder) + {} + + virtual ~ExtrapolationRefillingSweepBase() = default; + + virtual void operator()(IBlock* const block) = 0; + + /******************************************************************************************************************** + * Find the lattice direction in the given stencil that corresponds best to the provided direction. + * + * Mostly copied from src/lbm_mesapd_coupling/momentum_exchange_method/reconstruction/ExtrapolationDirectionFinder.h. + *******************************************************************************************************************/ + Vector3< cell_idx_t > findCorrespondingLatticeDirection(const Vector3< real_t >& direction); + + /******************************************************************************************************************** + * The extrapolation direction is chosen such that it most closely resembles the surface normal in the cell. + * + * The normal has to be recomputed here and MUST NOT be taken directly from the normal field because the fill levels + * will have changed since the last computation of the normal. As the normal is computed from the fill levels, it + * must be recomputed to have an up-to-date normal. + *******************************************************************************************************************/ + Vector3< cell_idx_t > findExtrapolationDirection(const Cell& cell, const FlagField_T& flagField, + const ScalarField_T& fillField); + + /******************************************************************************************************************** + * Determine the number of applicable (liquid or interface) cells for extrapolation in the given extrapolation + * direction. + *******************************************************************************************************************/ + uint_t getNumberOfExtrapolationCells(const Cell& cell, const FlagField_T& flagField, const PdfField_T& pdfField, + const Vector3< cell_idx_t >& extrapolationDirection); + + /******************************************************************************************************************** + * Get the non-equilibrium part of all PDFs in "cell" and store them in a std::vector<real_t>. + *******************************************************************************************************************/ + std::vector< real_t > getNonEquilibriumPdfsInCell(const Cell& cell, lbm::PdfField< LatticeModel_T >& pdfField); + + /******************************************************************************************************************** + * Get all PDFs in "cell" and store them in a std::vector<real_t>. + *******************************************************************************************************************/ + std::vector< real_t > getPdfsInCell(const Cell& cell, lbm::PdfField< LatticeModel_T >& pdfField); + + /******************************************************************************************************************** + * Set the PDFs in cell "x" according to the following linear combination: + * f(x,q) = f(x,q) + 3 * f^{get}(x+e,q) - 3 * f^{get}(x+2e,q) + 1 * f^{get}(x+3e,q) + * with x: cell position + * q: index of the respective PDF + * e: extrapolation direction + * f^{get}: PDF specified by getPdfFunc + * "includeThisCell" defines whether f(x,q) is included + * + * Note: this function does NOT assert out of bounds access, i.e., it may only be called for cells that have at least + * three neighboring cells in extrapolationDirection. + *******************************************************************************************************************/ + void applyQuadraticExtrapolation( + const Cell& cell, lbm::PdfField< LatticeModel_T >& pdfField, const Vector3< cell_idx_t >& extrapolationDirection, + bool includeThisCell, + const std::function< std::vector< real_t >(const Cell& cell, lbm::PdfField< LatticeModel_T >& pdfField) >& + getPdfFunc); + + /******************************************************************************************************************** + * Set the PDFs in cell (x) according to the following linear combination: + * f(x,q) = f(x,q) + 2 * f^{get}(x+1e,q) - 1 * f^{get}(x+2e,q) + * with x: cell position + * q: index of the respective PDF + * e: extrapolation direction + * f^{get}: PDF specified by getPdfFunc + * "includeThisCell" defines whether f(x,q) is included + * + * Note: this function does NOT assert out of bounds access, i.e., it may only be called for cells that have at least + * two neighboring cells in extrapolationDirection. + *******************************************************************************************************************/ + void applyLinearExtrapolation( + const Cell& cell, lbm::PdfField< LatticeModel_T >& pdfField, const Vector3< cell_idx_t >& extrapolationDirection, + bool includeThisCell, + const std::function< std::vector< real_t >(const Cell& cell, lbm::PdfField< LatticeModel_T >& pdfField) >& + getPdfFunc); + + /******************************************************************************************************************** + * Set the PDFs in cell (x) according to the following linear combination: + * f(x,q) = f(x,q) + 1 * f^{get}(x+1e,q) + * with x: cell position + * q: index of the respective PDF + * e: extrapolation direction + * f^{get}: PDF specified by getPdfFunc + * "includeThisCell" defines whether f(x,q) is included + * + * Note: this function does NOT assert out of bounds access, i.e., it may only be called for cells that have at least + * a neighboring cell in extrapolationDirection. + *******************************************************************************************************************/ + void applyConstantExtrapolation( + const Cell& cell, lbm::PdfField< LatticeModel_T >& pdfField, const Vector3< cell_idx_t >& extrapolationDirection, + bool includeThisCell, + const std::function< std::vector< real_t >(const Cell& cell, lbm::PdfField< LatticeModel_T >& pdfField) >& + getPdfFunc); + + protected: + ConstBlockDataID fillFieldID_; + uint_t extrapolationOrder_; +}; // class ExtrapolationRefillingSweepBase + +/*********************************************************************************************************************** + * EquilibriumRefillingSweep: + * PDFs are initialized with the equilibrium based on the average density and velocity from neighboring liquid and + * (non-newly converted) interface cells. + * + * Reference: dissertation of N. Thuerey, 2007, section 4.3 + * + * f(x,q) = f^{eq}(x+e,q) + * with x: cell position + * q: index of the respective PDF + * e: direction of a valid neighbor + **********************************************************************************************************************/ + +template< typename LatticeModel_T, typename FlagField_T > +class EquilibriumRefillingSweep : public RefillingSweepBase< LatticeModel_T, FlagField_T > +{ + public: + using RefillingSweepBase_T = RefillingSweepBase< LatticeModel_T, FlagField_T >; + using PdfField_T = typename RefillingSweepBase_T::PdfField_T; + using flag_t = typename RefillingSweepBase_T::flag_t; + using Stencil_T = typename RefillingSweepBase_T::Stencil_T; + + EquilibriumRefillingSweep(const BlockDataID& pdfFieldID, const ConstBlockDataID& flagFieldID, + const FlagInfo< FlagField_T >& flagInfo, bool useDataFromGhostLayers) + : RefillingSweepBase_T(pdfFieldID, flagFieldID, flagInfo, useDataFromGhostLayers) + {} + + ~EquilibriumRefillingSweep() override = default; + + void operator()(IBlock* const block) override; +}; // class EquilibriumRefillingSweep + +/*********************************************************************************************************************** + * AverageRefillingSweep: + * PDFs are initialized with the average of the PDFs in the same direction from applicable neighboring cells. + * + * f_i(x,q) = \sum_N( f_i(x+e,q) ) / N + * with x: cell position + * i: PDF index, i.e., direction + * q: index of the respective PDF + * e: direction of a valid neighbor + * N: number of applicable neighbors + * sum_N: sum over all applicable neighbors + * + * Reference: not available in literature (as of 06/2022). + **********************************************************************************************************************/ + +template< typename LatticeModel_T, typename FlagField_T > +class AverageRefillingSweep : public RefillingSweepBase< LatticeModel_T, FlagField_T > +{ + public: + using RefillingSweepBase_T = RefillingSweepBase< LatticeModel_T, FlagField_T >; + using PdfField_T = typename RefillingSweepBase_T::PdfField_T; + using flag_t = typename RefillingSweepBase_T::flag_t; + using Stencil_T = typename RefillingSweepBase_T::Stencil_T; + + AverageRefillingSweep(const BlockDataID& pdfFieldID, const ConstBlockDataID& flagFieldID, + const FlagInfo< FlagField_T >& flagInfo, bool useDataFromGhostLayers) + : RefillingSweepBase_T(pdfFieldID, flagFieldID, flagInfo, useDataFromGhostLayers) + {} + + ~AverageRefillingSweep() override = default; + + void operator()(IBlock* const block) override; +}; // class AverageRefillingSweep + +/*********************************************************************************************************************** + * EquilibriumAndNonEquilibriumRefilling: + * First reconstruct the equilibrium values according to the "EquilibriumRefilling". Then extrapolate the + * non-equilibrium part of the PDFs in the direction of the surface normal and add it to the (equilibrium-) + * reinitialized PDFs. + * + * Reference: equation (51) in Peng et al., "Implementation issues and benchmarking of lattice Boltzmann method for + * moving rigid particle simulations in a viscous flow", 2015, doi: 10.1016/j.camwa.2015.08.027) + * + * f_q(x,t+dt) = f_q^{eq}(x,t) + f_q^{neq}(x+e*dt,t+dt) + * with x: cell position + * q: index of the respective PDF + * e: direction of a valid neighbor/ extrapolation direction + * t: current time step + * dt: time step width + * f^{eq}: equilibrium PDF with velocity and density averaged from neighboring cells + * f^{neq}: non-equilibrium PDF (f - f^{eq}) + * + * Note: Analogously as in the "ExtrapolationRefilling", the expression "f_q^{neq}(x+e*dt,t+dt)" can also be obtained by + * extrapolation (in literature, only zeroth order extrapolation is used/documented): + * - zeroth order: f_q^{neq}(x+e*dt,t+dt) + * - first order: 2 * f_q^{neq}(x+e*dt,t+dt) - 1 * f_q^{neq}(x+2e*dt,t+dt) + * - second order: 3 * f_q^{neq}(x+e*dt,t+dt) - 3 * f_q^{neq}(x+2e*dt,t+dt) + f_q^{neq}(x+3e*dt,t+dt) + * If not enough cells are available for the chosen extrapolation order, the algorithm falls back to the corresponding + * lower order. If even zeroth order can not be applied, only f_q^{eq}(x,t) is considered which corresponds to + * "EquilibriumRefilling". + **********************************************************************************************************************/ +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T > +class EquilibriumAndNonEquilibriumRefillingSweep + : public ExtrapolationRefillingSweepBase< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > +{ + public: + using ExtrapolationRefillingSweepBase_T = + ExtrapolationRefillingSweepBase< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T >; + using RefillingSweepBase_T = typename ExtrapolationRefillingSweepBase_T::RefillingSweepBase_T; + using PdfField_T = typename ExtrapolationRefillingSweepBase_T::PdfField_T; + using flag_t = typename ExtrapolationRefillingSweepBase_T::flag_t; + using Stencil_T = typename ExtrapolationRefillingSweepBase_T::Stencil_T; + + EquilibriumAndNonEquilibriumRefillingSweep(const BlockDataID& pdfFieldID, const ConstBlockDataID& flagFieldID, + const ConstBlockDataID& fillFieldID, + const FlagInfo< FlagField_T >& flagInfo, uint_t extrapolationOrder, + bool useDataFromGhostLayers) + : ExtrapolationRefillingSweepBase_T(pdfFieldID, flagFieldID, fillFieldID, flagInfo, extrapolationOrder, + useDataFromGhostLayers) + {} + + ~EquilibriumAndNonEquilibriumRefillingSweep() override = default; + + void operator()(IBlock* const block) override; +}; // class EquilibriumAndNonEquilibriumRefillingSweep + +/*********************************************************************************************************************** + * ExtrapolationRefilling: + * Extrapolate the PDFs of one or more cells in the direction of the surface normal. + * + * Reference: equation (50) in Peng et al., "Implementation issues and benchmarking of lattice Boltzmann method for + * moving rigid particle simulations in a viscous flow", 2015, doi: 10.1016/j.camwa.2015.08.027) + * + * f_q(x,t+dt) = 3 * f_q(x+e*dt,t+dt) - 3 * f_q(x+2e*dt,t+dt) + f_q(x+3e*dt,t+dt) + * with x: cell position + * q: index of the respective PDF + * e: direction of a valid neighbor/ extrapolation direction + * t: current time step + * dt: time step width + * Note: The equation contains a second order extrapolation, however other options are also available. If not enough + * cells are available for second order extrapolation, the algorithm falls back to the next applicable + * lower order: + * - second order: 3 * f_q(x+e*dt,t+dt) - 3 * f_q(x+2e*dt,t+dt) + f_q(x+3e*dt,t+dt) + * - first order: 2 * f_q(x+e*dt,t+dt) - 1 * f_q(x+2e*dt,t+dt) + * - zeroth order: f_q(x+e*dt,t+dt) + * If even zeroth order can not be applied, only f_q^{eq}(x,t) is considered which corresponds to + * "EquilibriumRefilling". + **********************************************************************************************************************/ +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T > +class ExtrapolationRefillingSweep + : public ExtrapolationRefillingSweepBase< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > +{ + public: + using ExtrapolationRefillingSweepBase_T = + ExtrapolationRefillingSweepBase< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T >; + using RefillingSweepBase_T = typename ExtrapolationRefillingSweepBase_T::RefillingSweepBase_T; + using PdfField_T = typename ExtrapolationRefillingSweepBase_T::PdfField_T; + using flag_t = typename ExtrapolationRefillingSweepBase_T::flag_t; + using Stencil_T = typename ExtrapolationRefillingSweepBase_T::Stencil_T; + + ExtrapolationRefillingSweep(const BlockDataID& pdfFieldID, const ConstBlockDataID& flagFieldID, + const ConstBlockDataID& fillFieldID, const FlagInfo< FlagField_T >& flagInfo, + uint_t extrapolationOrder, bool useDataFromGhostLayers) + : ExtrapolationRefillingSweepBase_T(pdfFieldID, flagFieldID, fillFieldID, flagInfo, extrapolationOrder, + useDataFromGhostLayers) + {} + + ~ExtrapolationRefillingSweep() override = default; + + void operator()(IBlock* const block) override; +}; // class ExtrapolationRefillingSweep + +/*********************************************************************************************************************** + * GradsMomentsRefilling: + * Reconstruct missing PDFs based on Grad's moment closure. + * + * References: - equation (11) in Chikatamarla et al., "Grad’s approximation for missing data in lattice Boltzmann + * simulations", 2006, doi: 10.1209/epl/i2005-10535-x + * - equation (10) in Dorscher et al., "Grad’s approximation for moving and stationary walls in entropic + * lattice Boltzmann simulations", 2015, doi: 10.1016/j.jcp.2015.04.017 + * + * The following equation is a rewritten and easier version of the equation in the above references: + * f_q(x,t+dt) = f_q^{eq}(x,t) + + * w_q * rho / 2 / cs^2 / omega * (du_a / dx_b + du_b / dx_a)(cs^2 * delta_{ab} - c_{q,a}c_{q,b} ) + * with x: cell position + * q: index of the respective PDF + * t: current time step + * f^{eq}: equilibrium PDF with velocity and density averaged from neighboring cells + * w_q: lattice weight + * rho: density averaged from neighboring cells + * cs: lattice speed of sound + * omega: relaxation rate + * du_a / dx_b: gradient of the velocity (in index notation) + * delta_{ab}: Kronecker delta (in index notation) + * c_q{q,a}: lattice velocity (in index notation) + * + * The velocity gradient is computed using a first order central finite difference scheme if two neighboring cells + * are available. Otherwise, a first order upwind scheme is applied. + * IMPORTANT REMARK: The current implementation only works for dx=1 (this is assumed in the gradient computation). + **********************************************************************************************************************/ +template< typename LatticeModel_T, typename FlagField_T > +class GradsMomentsRefillingSweep : public RefillingSweepBase< LatticeModel_T, FlagField_T > +{ + public: + using RefillingSweepBase_T = RefillingSweepBase< LatticeModel_T, FlagField_T >; + using PdfField_T = typename RefillingSweepBase_T::PdfField_T; + using flag_t = typename RefillingSweepBase_T::flag_t; + using Stencil_T = typename RefillingSweepBase_T::Stencil_T; + + GradsMomentsRefillingSweep(const BlockDataID& pdfFieldID, const ConstBlockDataID& flagFieldID, + const FlagInfo< FlagField_T >& flagInfo, real_t relaxRate, bool useDataFromGhostLayers) + + : RefillingSweepBase_T(pdfFieldID, flagFieldID, flagInfo, useDataFromGhostLayers), relaxRate_(relaxRate) + {} + + ~GradsMomentsRefillingSweep() override = default; + + void operator()(IBlock* const block) override; + + // compute the gradient of the velocity in the specified direction + // - using a first order central finite difference scheme if two valid neighboring cells are available + // - using a first order upwind scheme if only one valid neighboring cell is available + // - assuming a gradient of zero if no valid neighboring cell is available + Vector3< real_t > getVelocityGradient(stencil::Direction direction, const Cell& cell, const PdfField_T* pdfField, + const Vector3< real_t >& avgVelocity, + const std::vector< bool >& validStencilIndices); + + private: + real_t relaxRate_; +}; // class GradsMomentApproximationRefilling + +} // namespace free_surface +} // namespace walberla + +#include "PdfRefillingSweep.impl.h" \ No newline at end of file diff --git a/src/lbm/free_surface/dynamics/PdfRefillingSweep.impl.h b/src/lbm/free_surface/dynamics/PdfRefillingSweep.impl.h new file mode 100644 index 000000000..cc5c382e5 --- /dev/null +++ b/src/lbm/free_surface/dynamics/PdfRefillingSweep.impl.h @@ -0,0 +1,718 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file PdfRefillingSweep.impl.h +//! \ingroup dynamics +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \author Michael Zikeli +//! \brief Sweeps for refilling cells (i.e. reinitializing PDFs) after the cell was converted from gas to interface. +// +//====================================================================================================================== + +#include "core/DataTypes.h" +#include "core/cell/Cell.h" +#include "core/debug/CheckFunctions.h" +#include "core/logging/Logging.h" +#include "core/math/Vector3.h" + +#include "domain_decomposition/IBlock.h" + +#include "field/GhostLayerField.h" + +#include "lbm/field/Equilibrium.h" +#include "lbm/field/PdfField.h" +#include "lbm/free_surface/FlagInfo.h" +#include "lbm/free_surface/dynamics/PdfRefillingModel.h" +#include "lbm/free_surface/surface_geometry/NormalSweep.h" + +#include "stencil/D3Q6.h" + +#include <set> +#include <vector> + +#include "PdfRefillingSweep.h" + +namespace walberla +{ +namespace free_surface +{ +template< typename LatticeModel_T, typename FlagField_T > +real_t RefillingSweepBase< LatticeModel_T, FlagField_T >::getAverageDensityAndVelocity( + const Cell& cell, const PdfField_T& pdfField, const FlagField_T& flagField, const FlagInfo< FlagField_T >& flagInfo, + Vector3< real_t >& avgVelocity, std::vector< bool >& validStencilIndices) +{ + real_t rho = real_c(0.0); + Vector3< real_t > u(real_c(0.0)); + uint_t numNeighbors = uint_c(0); + + // do not use data from ghost layer if optimized communication is used (see comment in PdfRefillingSweep.h at + // ExtrapolationRefillingSweepBase) + const CellInterval localDomain = useDataFromGhostLayers_ ? pdfField.xyzSizeWithGhostLayer() : pdfField.xyzSize(); + + for (auto i = Stencil_T::beginNoCenter(); i != Stencil_T::end(); ++i) + { + const Cell neighborCell(cell[0] + i.cx(), cell[1] + i.cy(), cell[2] + i.cz()); + + const flag_t neighborFlag = flagField.get(neighborCell); + const flag_t liquidInterfaceMask = flagInfo.interfaceFlag | flagInfo.liquidFlag; + + // only use neighboring cell if + // - neighboring cell is part of the block-local domain + // - neighboring cell is liquid or interface + // - not newly converted from G->I + const bool useNeighbor = isPartOfMaskSet(neighborFlag, liquidInterfaceMask) && + !flagInfo.hasConvertedFromGasToInterface(flagField.get(neighborCell)) && + localDomain.contains(neighborCell); + + // calculate the average of valid neighbor cells to calculate an average density and velocity. + if (useNeighbor) + { + numNeighbors++; + const typename PdfField_T::ConstPtr neighbor(pdfField, neighborCell[0], neighborCell[1], neighborCell[2]); + Vector3< real_t > neighborU; + real_t neighborRho; + neighborRho = lbm::getDensityAndMomentumDensity(neighborU, pdfField.latticeModel(), neighbor); + neighborU /= neighborRho; + u += neighborU; + rho += neighborRho; + + validStencilIndices[i.toIdx()] = true; + } + } + + // normalize the newly calculated velocity and density + if (numNeighbors != uint_c(0)) + { + u /= real_c(numNeighbors); + rho /= real_c(numNeighbors); + } + else + { + u = Vector3< real_t >(0.0); + rho = real_c(1.0); + WALBERLA_LOG_WARNING_ON_ROOT("There are no valid neighbors for the refilling of (block-local) cell: " << cell); + } + + avgVelocity = u; + return rho; +} + +template< typename LatticeModel_T, typename FlagField_T > +std::vector< real_t > RefillingSweepBase< LatticeModel_T, FlagField_T >::getAveragePdfs( + const Cell& cell, const PdfField_T& pdfField, const FlagField_T& flagField, const FlagInfo< FlagField_T >& flagInfo) +{ + uint_t numNeighbors = uint_c(0); + + // do not use data from ghost layer if optimized communication is used (see comment in PdfRefillingSweep.h at + // ExtrapolationRefillingSweepBase) + const CellInterval localDomain = useDataFromGhostLayers_ ? pdfField.xyzSizeWithGhostLayer() : pdfField.xyzSize(); + + std::vector< real_t > pdfSum(Stencil_T::Size, real_c(0)); + + for (auto i = Stencil_T::beginNoCenter(); i != Stencil_T::end(); ++i) + { + const Cell neighborCell(cell[0] + i.cx(), cell[1] + i.cy(), cell[2] + i.cz()); + + const flag_t neighborFlag = flagField.get(neighborCell); + const flag_t liquidInterfaceMask = flagInfo.interfaceFlag | flagInfo.liquidFlag; + + // only use neighboring cell if + // - neighboring cell is part of the block-local domain + // - neighboring cell is liquid or interface + // - not newly converted from G->I + const bool useNeighbor = isPartOfMaskSet(neighborFlag, liquidInterfaceMask) && + !flagInfo.hasConvertedFromGasToInterface(flagField.get(neighborCell)) && + localDomain.contains(neighborCell); + + // calculate the average of valid neighbor cells to calculate an average of PDFs in each direction + if (useNeighbor) + { + ++numNeighbors; + for (auto pdfDir = Stencil_T::begin(); pdfDir != Stencil_T::end(); ++pdfDir) + { + pdfSum[pdfDir.toIdx()] += pdfField.get(neighborCell, *pdfDir); + } + } + } + + // average the PDFs of all neighboring cells + if (numNeighbors != uint_c(0)) + { + for (auto& pdf : pdfSum) + { + pdf /= real_c(numNeighbors); + } + } + else + { + // fall back to EquilibriumRefilling by setting PDFs according to equilibrium with velocity=0 and density=1 + lbm::Equilibrium< LatticeModel_T >::set(pdfSum, Vector3< real_t >(real_c(0)), real_c(1)); + + WALBERLA_LOG_WARNING_ON_ROOT("There are no valid neighbors for the refilling of (block-local) cell: " << cell); + } + + return pdfSum; +} + +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T > +Vector3< cell_idx_t > ExtrapolationRefillingSweepBase< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T >:: + findCorrespondingLatticeDirection(const Vector3< real_t >& direction) +{ + if (direction == Vector3< real_t >(real_c(0))) { return direction; } + + stencil::Direction bestFittingDirection = stencil::C; // arbitrary default initialization + real_t scalarProduct = real_c(0); + + for (auto dir = Stencil_T::beginNoCenter(); dir != Stencil_T::end(); ++dir) + { + // compute inner product <dir,c_i> + const real_t scalarProductTmp = direction[0] * stencil::cNorm[0][*dir] + direction[1] * stencil::cNorm[1][*dir] + + direction[2] * stencil::cNorm[2][*dir]; + if (scalarProductTmp > scalarProduct) + { + // largest scalar product is the best fitting lattice direction, i.e., this direction has the smallest angle to + // the given direction + scalarProduct = scalarProductTmp; + bestFittingDirection = *dir; + } + } + + return Vector3< cell_idx_t >(stencil::cx[bestFittingDirection], stencil::cy[bestFittingDirection], + stencil::cz[bestFittingDirection]); +} + +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T > +Vector3< cell_idx_t > + ExtrapolationRefillingSweepBase< LatticeModel_T, FlagField_T, ScalarField_T, + VectorField_T >::findExtrapolationDirection(const Cell& cell, + const FlagField_T& flagField, + const ScalarField_T& fillField) +{ + Vector3< real_t > normal = Vector3< real_t >(real_c(0)); + + // get flag mask for obstacle boundaries + const flag_t obstacleFlagMask = flagField.getMask(RefillingSweepBase_T::flagInfo_.getObstacleIDSet()); + + // get flag mask for liquid, interface, and gas cells + const flag_t liquidInterfaceGasMask = flagField.getMask(flagIDs::liquidInterfaceGasFlagIDs); + + const typename ScalarField_T::ConstPtr fillPtr(fillField, cell.x(), cell.y(), cell.z()); + const typename FlagField_T::ConstPtr flagPtr(flagField, cell.x(), cell.y(), cell.z()); + + if (!isFlagInNeighborhood< Stencil_T >(flagPtr, obstacleFlagMask)) + { + normal_computation::computeNormal< + typename std::conditional< Stencil_T::D == uint_t(2), stencil::D2Q9, stencil::D3Q27 >::type >( + normal, fillPtr, flagPtr, liquidInterfaceGasMask); + } + else + { + normal_computation::computeNormalNearSolidBoundary< + typename std::conditional< Stencil_T::D == uint_t(2), stencil::D2Q9, stencil::D3Q27 >::type >( + normal, fillPtr, flagPtr, liquidInterfaceGasMask, obstacleFlagMask); + } + + // normalize normal (in contrast to the usual definition in FSLBM, it points from gas to fluid here) + normal = normal.getNormalizedOrZero(); + + // find the lattice direction that most closely resembles the normal direction + // Note: the normal must point from gas to fluid because the extrapolation direction is also defined to point from + // gas to fluid + return findCorrespondingLatticeDirection(normal); +} + +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T > +uint_t ExtrapolationRefillingSweepBase< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T >:: + getNumberOfExtrapolationCells(const Cell& cell, const FlagField_T& flagField, const PdfField_T& pdfField, + const Vector3< cell_idx_t >& extrapolationDirection) +{ + // skip center cell + if (extrapolationDirection == Vector3< cell_idx_t >(cell_idx_c(0))) { return uint_c(0); } + + // do not use data from ghost layer if optimized communication is used (see comment in PdfRefillingSweep.h at + // ExtrapolationRefillingSweepBase) + const CellInterval localDomain = + (RefillingSweepBase_T::useDataFromGhostLayers_) ? pdfField.xyzSizeWithGhostLayer() : pdfField.xyzSize(); + + // for extrapolation order n, n+1 applicable cells must be available in the desired direction + const uint_t maxExtrapolationCells = extrapolationOrder_ + uint_c(1); + + for (uint_t numCells = uint_c(1); numCells <= maxExtrapolationCells; ++numCells) + { + // potential cell used for extrapolation + const Cell checkCell = cell + Cell(cell_idx_c(numCells) * extrapolationDirection); + + const flag_t neighborFlag = flagField.get(checkCell); + const flag_t liquidInterfaceMask = + RefillingSweepBase_T::flagInfo_.interfaceFlag | RefillingSweepBase_T::flagInfo_.liquidFlag; + + // only use cell if the cell is + // - inside domain + // - liquid or interface + // - not a cell that has also just been converted from gas to interface + if (!localDomain.contains(checkCell) || !isPartOfMaskSet(neighborFlag, liquidInterfaceMask) || + RefillingSweepBase_T::flagInfo_.hasConvertedFromGasToInterface(flagField.get(checkCell))) + { + return numCells - uint_c(1); + } + } + + return maxExtrapolationCells; +} + +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T > +std::vector< real_t > ExtrapolationRefillingSweepBase< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T >:: + getNonEquilibriumPdfsInCell(const Cell& cell, lbm::PdfField< LatticeModel_T >& pdfField) +{ + std::vector< real_t > nonEquilibriumPartOfPdfs(Stencil_T::Size); + + Vector3< real_t > velocity; + const real_t density = pdfField.getDensityAndVelocity(velocity, cell); + + // compute non-equilibrium of each PDF + for (auto dir = Stencil_T::begin(); dir != Stencil_T::end(); ++dir) + { + nonEquilibriumPartOfPdfs[dir.toIdx()] = + pdfField.get(cell, dir.toIdx()) - lbm::EquilibriumDistribution< LatticeModel_T >::get(*dir, velocity, density); + } + return nonEquilibriumPartOfPdfs; +} + +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T > +std::vector< real_t > + ExtrapolationRefillingSweepBase< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T >::getPdfsInCell( + const Cell& cell, lbm::PdfField< LatticeModel_T >& pdfField) +{ + std::vector< real_t > Pdfs(Stencil_T::Size); + + for (auto dir = Stencil_T::begin(); dir != Stencil_T::end(); ++dir) + { + Pdfs[dir.toIdx()] = pdfField.get(cell, dir.toIdx()); + } + return Pdfs; +} + +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T > +void ExtrapolationRefillingSweepBase< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T >:: + applyQuadraticExtrapolation( + const Cell& cell, lbm::PdfField< LatticeModel_T >& pdfField, const Vector3< cell_idx_t >& extrapolationDirection, + bool includeThisCell, + const std::function< std::vector< real_t >(const Cell& cell, lbm::PdfField< LatticeModel_T >& pdfField) >& + getPdfFunc) +{ + // store the PDFs of the cells participating in the extrapolation + std::vector< real_t > pdfsXf(LatticeModel_T::Stencil::Size); // cell + 1 * extrapolationDirection + std::vector< real_t > pdfsXff(LatticeModel_T::Stencil::Size); // cell + 2 * extrapolationDirection + std::vector< real_t > pdfsXfff(LatticeModel_T::Stencil::Size); // cell + 3 * extrapolationDirection + + // determines if the PDFs of the current cell are also considered + const real_t centerFactor = includeThisCell ? real_c(1) : real_c(0); + + // get PDFs + pdfsXf = getPdfFunc(cell + Cell(cell_idx_c(1) * extrapolationDirection), pdfField); + pdfsXff = getPdfFunc(cell + Cell(cell_idx_c(2) * extrapolationDirection), pdfField); + pdfsXfff = getPdfFunc(cell + Cell(cell_idx_c(3) * extrapolationDirection), pdfField); + + // compute the resulting PDF values of "cell" according to second order extrapolation + for (auto dir = Stencil_T::begin(); dir != Stencil_T::end(); ++dir) + { + pdfField.get(cell, dir.toIdx()) = centerFactor * pdfField.get(cell, dir.toIdx()) + + real_c(3) * pdfsXf[dir.toIdx()] - real_c(3) * pdfsXff[dir.toIdx()] + + real_c(1) * pdfsXfff[dir.toIdx()]; + } +} + +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T > +void ExtrapolationRefillingSweepBase< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T >:: + applyLinearExtrapolation( + const Cell& cell, lbm::PdfField< LatticeModel_T >& pdfField, const Vector3< cell_idx_t >& extrapolationDirection, + bool includeThisCell, + const std::function< std::vector< real_t >(const Cell& cell, lbm::PdfField< LatticeModel_T >& pdfField) >& + getPdfFunc) +{ + // store the PDFs of the cells participating in the interpolation + std::vector< real_t > pdfsXf(Stencil_T::Size); // cell + 1 * extrapolationDirection + std::vector< real_t > pdfsXff(Stencil_T::Size); // cell + 2 * extrapolationDirection + + // determines if the PDFs of the current cell are also considered + const real_t centerFactor = includeThisCell ? real_c(1) : real_c(0); + + // get PDFs + pdfsXf = getPdfFunc(cell + Cell(cell_idx_c(1) * extrapolationDirection), pdfField); + pdfsXff = getPdfFunc(cell + Cell(cell_idx_c(2) * extrapolationDirection), pdfField); + + // compute the resulting PDF values of "cell" according to first order extrapolation + for (auto dir = Stencil_T::begin(); dir != Stencil_T::end(); ++dir) + { + pdfField.get(cell, dir.toIdx()) = centerFactor * pdfField.get(cell, dir.toIdx()) + + real_c(2) * pdfsXf[dir.toIdx()] - real_c(1) * pdfsXff[dir.toIdx()]; + } +} + +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T > +void ExtrapolationRefillingSweepBase< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T >:: + applyConstantExtrapolation( + const Cell& cell, lbm::PdfField< LatticeModel_T >& pdfField, const Vector3< cell_idx_t >& extrapolationDirection, + bool includeThisCell, + const std::function< std::vector< real_t >(const Cell& cell, lbm::PdfField< LatticeModel_T >& pdfField) >& + getPdfFunc) +{ + // store the PDFs of the cells participating in the interpolation + std::vector< real_t > pdfsXf(Stencil_T::Size); // cell + 1 * extrapolationDirection + + // determines if the PDFs of the current cell are also considered + const real_t centerFactor = includeThisCell ? real_c(1) : real_c(0); + + // get PDFs + pdfsXf = getPdfFunc(cell + Cell(cell_idx_c(1) * extrapolationDirection), pdfField); + + // compute the resulting PDF values of "cell" according to zeroth order extrapolation + for (auto dir = Stencil_T::begin(); dir != Stencil_T::end(); ++dir) + { + pdfField.get(cell, dir.toIdx()) = + centerFactor * pdfField.get(cell, dir.toIdx()) + real_c(1) * pdfsXf[dir.toIdx()]; + } +} + +template< typename LatticeModel_T, typename FlagField_T > +void EquilibriumRefillingSweep< LatticeModel_T, FlagField_T >::operator()(IBlock* const block) +{ + PdfField_T* const pdfField = block->getData< PdfField_T >(RefillingSweepBase_T::pdfFieldID_); + const FlagField_T* const flagField = block->getData< const FlagField_T >(RefillingSweepBase_T::flagFieldID_); + + WALBERLA_FOR_ALL_CELLS(pdfFieldIt, pdfField, flagFieldIt, flagField, { + if (RefillingSweepBase_T::flagInfo_.hasConvertedFromGasToInterface(flagFieldIt)) + { + const Cell cell = pdfFieldIt.cell(); + + Vector3< real_t > avgVelocity; + real_t avgDensity; + avgDensity = RefillingSweepBase_T::getAverageDensityAndVelocity(cell, *pdfField, *flagField, + RefillingSweepBase_T::flagInfo_, avgVelocity); + + pdfField->setDensityAndVelocity(cell, avgVelocity, avgDensity); + } + }) // WALBERLA_FOR_ALL_CELLS +} + +template< typename LatticeModel_T, typename FlagField_T > +void AverageRefillingSweep< LatticeModel_T, FlagField_T >::operator()(IBlock* const block) +{ + PdfField_T* const pdfField = block->getData< PdfField_T >(RefillingSweepBase_T::pdfFieldID_); + const FlagField_T* const flagField = block->getData< const FlagField_T >(RefillingSweepBase_T::flagFieldID_); + + WALBERLA_FOR_ALL_CELLS(pdfFieldIt, pdfField, flagFieldIt, flagField, { + if (RefillingSweepBase_T::flagInfo_.hasConvertedFromGasToInterface(flagFieldIt)) + { + const Cell cell = pdfFieldIt.cell(); + + // compute average PDFs (in each direction) from all applicable neighboring cells + const std::vector< real_t > pdfAverage = + RefillingSweepBase_T::getAveragePdfs(cell, *pdfField, *flagField, RefillingSweepBase_T::flagInfo_); + + for (auto pdfDir = Stencil_T::begin(); pdfDir != Stencil_T::end(); ++pdfDir) + { + pdfField->get(cell, *pdfDir) = pdfAverage[pdfDir.toIdx()]; + } + } + }) // WALBERLA_FOR_ALL_CELLS +} + +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T > +void EquilibriumAndNonEquilibriumRefillingSweep< LatticeModel_T, FlagField_T, ScalarField_T, + VectorField_T >::operator()(IBlock* const block) +{ + PdfField_T* const pdfField = + block->getData< PdfField_T >(ExtrapolationRefillingSweepBase_T::RefillingSweepBase_T::pdfFieldID_); + const FlagField_T* const flagField = + block->getData< const FlagField_T >(ExtrapolationRefillingSweepBase_T::RefillingSweepBase_T::flagFieldID_); + const ScalarField_T* const fillField = + block->getData< const ScalarField_T >(ExtrapolationRefillingSweepBase_T::fillFieldID_); + + // function to fetch relevant PDFs + auto getPdfFunc = std::bind(&ExtrapolationRefillingSweepBase_T::getNonEquilibriumPdfsInCell, this, + std::placeholders::_1, std::placeholders::_2); + + WALBERLA_FOR_ALL_CELLS(pdfFieldIt, pdfField, flagFieldIt, flagField, { + if (RefillingSweepBase_T::flagInfo_.hasConvertedFromGasToInterface(flagFieldIt)) + { + const Cell cell = pdfFieldIt.cell(); + + // apply EquilibriumRefilling first + Vector3< real_t > avgVelocity; + real_t avgDensity; + avgDensity = RefillingSweepBase_T::getAverageDensityAndVelocity(cell, *pdfField, *flagField, + RefillingSweepBase_T::flagInfo_, avgVelocity); + pdfField->setDensityAndVelocity(cell, avgVelocity, avgDensity); + + // find valid cells for extrapolation + const Vector3< cell_idx_t > extrapolationDirection = + ExtrapolationRefillingSweepBase_T::findExtrapolationDirection(cell, *flagField, *fillField); + const uint_t numberOfCellsForExtrapolation = ExtrapolationRefillingSweepBase_T::getNumberOfExtrapolationCells( + cell, *flagField, *pdfField, extrapolationDirection); + + // add non-equilibrium part of PDF (which might be obtained by extrapolation) + if (numberOfCellsForExtrapolation >= uint_c(3)) + { + ExtrapolationRefillingSweepBase_T::applyQuadraticExtrapolation(cell, *pdfField, extrapolationDirection, + true, getPdfFunc); + } + else + { + if (numberOfCellsForExtrapolation >= uint_c(2)) + { + ExtrapolationRefillingSweepBase_T::applyLinearExtrapolation(cell, *pdfField, extrapolationDirection, + true, getPdfFunc); + } + else + { + if (numberOfCellsForExtrapolation >= uint_c(1)) + { + ExtrapolationRefillingSweepBase_T::applyConstantExtrapolation(cell, *pdfField, extrapolationDirection, + true, getPdfFunc); + } + // else: do nothing here; this corresponds to using EquilibriumRefilling (done already at the beginning) + } + } + } + }) // WALBERLA_FOR_ALL_CELLS +} + +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T > +void ExtrapolationRefillingSweep< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T >::operator()( + IBlock* const block) +{ + PdfField_T* const pdfField = block->getData< PdfField_T >(ExtrapolationRefillingSweepBase_T::pdfFieldID_); + const FlagField_T* const flagField = + block->getData< const FlagField_T >(ExtrapolationRefillingSweepBase_T::flagFieldID_); + const ScalarField_T* const fillField = + block->getData< const ScalarField_T >(ExtrapolationRefillingSweepBase_T::fillFieldID_); + + WALBERLA_FOR_ALL_CELLS(pdfFieldIt, pdfField, flagFieldIt, flagField, { + if (RefillingSweepBase_T::flagInfo_.hasConvertedFromGasToInterface(flagFieldIt)) + { + const Cell cell = pdfFieldIt.cell(); + + // find valid cells for extrapolation + const Vector3< cell_idx_t > extrapolationDirection = + ExtrapolationRefillingSweepBase_T::findExtrapolationDirection(cell, *flagField, *fillField); + const uint_t numberOfCellsForExtrapolation = ExtrapolationRefillingSweepBase_T::getNumberOfExtrapolationCells( + cell, *flagField, *pdfField, extrapolationDirection); + + // function to fetch relevant PDFs + auto getPdfFunc = std::bind(&ExtrapolationRefillingSweepBase_T::getPdfsInCell, this, std::placeholders::_1, + std::placeholders::_2); + + if (numberOfCellsForExtrapolation >= uint_c(3)) + { + ExtrapolationRefillingSweepBase_T::applyQuadraticExtrapolation(cell, *pdfField, extrapolationDirection, + false, getPdfFunc); + } + else + { + if (numberOfCellsForExtrapolation >= uint_c(2)) + { + ExtrapolationRefillingSweepBase_T::applyLinearExtrapolation(cell, *pdfField, extrapolationDirection, + false, getPdfFunc); + } + else + { + if (numberOfCellsForExtrapolation >= uint_c(1)) + { + ExtrapolationRefillingSweepBase_T::applyConstantExtrapolation(cell, *pdfField, extrapolationDirection, + false, getPdfFunc); + } + else + { + // if not enough cells for extrapolation are available, use EquilibriumRefilling + Vector3< real_t > avgVelocity; + real_t avgDensity; + avgDensity = RefillingSweepBase_T::getAverageDensityAndVelocity( + cell, *pdfField, *flagField, RefillingSweepBase_T::flagInfo_, avgVelocity); + pdfField->setDensityAndVelocity(cell, avgVelocity, avgDensity); + } + } + } + } + }) // WALBERLA_FOR_ALL_CELLS +} + +template< typename LatticeModel_T, typename FlagField_T > +Vector3< real_t > GradsMomentsRefillingSweep< LatticeModel_T, FlagField_T >::getVelocityGradient( + stencil::Direction direction, const Cell& cell, const PdfField_T* pdfField, const Vector3< real_t >& avgVelocity, + const std::vector< bool >& validStencilIndices) +{ + stencil::Direction dir; + stencil::Direction invDir; + + switch (direction) + { + case stencil::E: + case stencil::W: + dir = stencil::E; + invDir = stencil::W; + break; + case stencil::N: + case stencil::S: + dir = stencil::N; + invDir = stencil::S; + break; + case stencil::T: + case stencil::B: + dir = stencil::T; + invDir = stencil::B; + break; + default: + WALBERLA_ABORT("Velocity gradient for GradsMomentsRefilling can not be computed in a direction other than in x-, " + "y-, or z-direction."); + } + + Vector3< real_t > velocityGradient(real_c(0)); + + // apply central finite differences if both neighboring cells are available + if (validStencilIndices[Stencil_T::idx[dir]] && validStencilIndices[Stencil_T::idx[invDir]]) + { + const Vector3< real_t > neighborVelocity1 = pdfField->getVelocity(cell + dir); + const Vector3< real_t > neighborVelocity2 = pdfField->getVelocity(cell + invDir); + velocityGradient[0] = real_c(0.5) * (neighborVelocity1[0] - neighborVelocity2[0]); // assuming dx = 1 + velocityGradient[1] = real_c(0.5) * (neighborVelocity1[1] - neighborVelocity2[1]); // assuming dx = 1 + velocityGradient[2] = real_c(0.5) * (neighborVelocity1[2] - neighborVelocity2[2]); // assuming dx = 1 + } + else + { + // apply first order upwind scheme + stencil::Direction upwindDirection = (avgVelocity[0] > real_c(0)) ? invDir : dir; + + stencil::Direction sourceDirection = stencil::C; // arbitrary default initialization + + if (validStencilIndices[Stencil_T::idx[upwindDirection]]) { sourceDirection = upwindDirection; } + else + { + if (validStencilIndices[Stencil_T::idx[stencil::inverseDir[upwindDirection]]]) + { + sourceDirection = stencil::inverseDir[upwindDirection]; + } + } + + if (sourceDirection == dir) + { + auto neighborVelocity = pdfField->getVelocity(cell + sourceDirection); + velocityGradient[0] = neighborVelocity[0] - avgVelocity[0]; // assuming dx = 1 + velocityGradient[1] = neighborVelocity[1] - avgVelocity[1]; // assuming dx = 1 + velocityGradient[2] = neighborVelocity[2] - avgVelocity[2]; // assuming dx = 1 + } + else + { + if (sourceDirection == invDir) + { + auto neighborVelocity = pdfField->getVelocity(cell + sourceDirection); + velocityGradient[0] = avgVelocity[0] - neighborVelocity[0]; // assuming dx = 1 + velocityGradient[1] = avgVelocity[1] - neighborVelocity[1]; // assuming dx = 1 + velocityGradient[2] = avgVelocity[2] - neighborVelocity[2]; // assuming dx = 1 + } + // else: no stencil direction is valid, velocityGradient is zero + } + } + + return velocityGradient; +} + +template< typename LatticeModel_T, typename FlagField_T > +void GradsMomentsRefillingSweep< LatticeModel_T, FlagField_T >::operator()(IBlock* const block) +{ + PdfField_T* pdfField = block->getData< PdfField_T >(RefillingSweepBase_T::pdfFieldID_); + const FlagField_T* const flagField = block->getData< const FlagField_T >(RefillingSweepBase_T::flagFieldID_); + + WALBERLA_FOR_ALL_CELLS(pdfFieldIt, pdfField, flagFieldIt, flagField, { + if (RefillingSweepBase_T::flagInfo_.hasConvertedFromGasToInterface(flagFieldIt)) + { + const Cell cell = pdfFieldIt.cell(); + + // get average density and velocity from valid neighboring cells and store the directions of valid neighbors + std::vector< bool > validStencilIndices(Stencil_T::Size, false); + Vector3< real_t > avgVelocity; + real_t avgDensity; + avgDensity = RefillingSweepBase_T::getAverageDensityAndVelocity( + cell, *pdfField, *flagField, RefillingSweepBase_T::flagInfo_, avgVelocity, validStencilIndices); + + // get velocity gradients + // - using a first order central finite differences (if two neighboring cells are available) + // - using a first order upwind scheme (if only one neighboring cell is available) + // - assuming a zero gradient if no valid neighboring cell is available + // velocityGradient(u) = + // | du1/dx1 du2/dx1 du3/dx1 | | 0 1 2 | | 0,0 0,1 0,2 | + // | du1/dx2 du2/dx2 du3/dx2 | = | 3 4 5 | = | 1,0 1,1 1,2 | + // | du1/dx3 du2/dx3 du3/dx3 | | 6 7 8 | | 2,0 2,1 2,2 | + const Vector3< real_t > gradientX = + getVelocityGradient(stencil::E, cell, pdfField, avgVelocity, validStencilIndices); + const Vector3< real_t > gradientY = + getVelocityGradient(stencil::N, cell, pdfField, avgVelocity, validStencilIndices); + Vector3< real_t > gradientZ = Vector3< real_t >(real_c(0)); + if (Stencil_T::D == 3) + { + gradientZ = getVelocityGradient(stencil::T, cell, pdfField, avgVelocity, validStencilIndices); + } + + Matrix3< real_t > velocityGradient(gradientX, gradientY, gradientZ); + + // compute non-equilibrium pressure tensor (equation (13) in Dorschner et al.); rho is not included in the + // pre-factor here, but will be considered later + Matrix3< real_t > pressureTensorNeq(real_c(0)); + + // in equation (13) in Dorschner et al., 2*beta is used in the pre-factor; note that 2*beta=omega (relaxation + // rate related to kinematic viscosity) + const real_t preFac = -real_c(1) / (real_c(3) * relaxRate_); + + for (uint_t j = uint_c(0); j != uint_c(3); ++j) + { + for (uint_t i = uint_c(0); i != uint_c(3); ++i) + { + pressureTensorNeq(i, j) += preFac * (velocityGradient(i, j) + velocityGradient(j, i)); + } + } + + // set PDFs according to equation (10) in Dorschner et al.; this is equivalent to setting the PDFs to + // equilibrium f^{eq} and adding a contribution of the non-equilibrium pressure tensor P^{neq} + for (auto q = Stencil_T::begin(); q != Stencil_T::end(); ++q) + { + const real_t velCi = lbm::internal::multiplyVelocityDirection(*q, avgVelocity); + + real_t contributionFromPneq = real_c(0); + for (uint_t j = uint_c(0); j != uint_c(3); ++j) + { + for (uint_t i = uint_c(0); i != uint_c(3); ++i) + { + // P^{neq}_{a,b} * c_{q,a} * c_{q,b} + contributionFromPneq += + pressureTensorNeq(i, j) * real_c(stencil::c[i][*q]) * real_c(stencil::c[j][*q]); + } + } + + // - P^{neq}_{a,b} * cs^2 * delta_{a,b} + contributionFromPneq -= + (pressureTensorNeq(0, 0) + pressureTensorNeq(1, 1) + pressureTensorNeq(2, 2)) / real_c(3); + + // compute f^{eq} and add contribution from P^{neq} + const real_t pdf = LatticeModel_T::w[q.toIdx()] * avgDensity * + (real_c(1) + real_c(3) * velCi - real_c(1.5) * avgVelocity.sqrLength() + + real_c(4.5) * velCi * velCi + real_c(4.5) * contributionFromPneq); + pdfField->get(cell, *q) = pdf; + } + } + }) // WALBERLA_FOR_ALL_CELLS +} + +} // namespace free_surface +} // namespace walberla \ No newline at end of file diff --git a/src/lbm/free_surface/dynamics/StreamReconstructAdvectSweep.h b/src/lbm/free_surface/dynamics/StreamReconstructAdvectSweep.h new file mode 100644 index 000000000..ec5ca220d --- /dev/null +++ b/src/lbm/free_surface/dynamics/StreamReconstructAdvectSweep.h @@ -0,0 +1,202 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file StreamReconstructAdvectSweep.h +//! \ingroup free_surface +//! \author Martin Bauer +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Sweep for reconstruction of PDFs, streaming of PDFs (only in interface cells), advection of mass, update of +//! bubble volumes and marking interface cells for conversion. +// +//====================================================================================================================== + +#pragma once + +#include "core/DataTypes.h" +#include "core/math/Vector3.h" +#include "core/mpi/Reduce.h" +#include "core/timing/TimingPool.h" + +#include "field/FieldClone.h" +#include "field/FlagField.h" + +#include "lbm/free_surface/FlagInfo.h" +#include "lbm/free_surface/bubble_model/BubbleModel.h" +#include "lbm/sweeps/StreamPull.h" + +#include "PdfReconstructionModel.h" +#include "functionality/AdvectMass.h" +#include "functionality/FindInterfaceCellConversion.h" +#include "functionality/GetOredNeighborhood.h" +#include "functionality/ReconstructInterfaceCellABB.h" + +namespace walberla +{ +namespace free_surface +{ +template< typename LatticeModel_T, typename BoundaryHandling_T, typename FlagField_T, typename FlagInfo_T, + typename ScalarField_T, typename VectorField_T, bool useCodegen > +class StreamReconstructAdvectSweep +{ + public: + using flag_t = typename FlagInfo_T::flag_t; + using PdfField_T = lbm::PdfField< LatticeModel_T >; + + StreamReconstructAdvectSweep(real_t sigma, BlockDataID handlingID, BlockDataID fillFieldID, BlockDataID flagFieldID, + BlockDataID pdfField, ConstBlockDataID normalFieldID, ConstBlockDataID curvatureFieldID, + const FlagInfo_T& flagInfo, BubbleModelBase* bubbleModel, + const PdfReconstructionModel& pdfReconstructionModel, bool useSimpleMassExchange, + real_t cellConversionThreshold, real_t cellConversionForceThreshold) + : sigma_(sigma), handlingID_(handlingID), fillFieldID_(fillFieldID), flagFieldID_(flagFieldID), + pdfFieldID_(pdfField), normalFieldID_(normalFieldID), curvatureFieldID_(curvatureFieldID), flagInfo_(flagInfo), + bubbleModel_(bubbleModel), neighborhoodFlagFieldClone_(flagFieldID), fillFieldClone_(fillFieldID), + pdfFieldClone_(pdfField), pdfReconstructionModel_(pdfReconstructionModel), + useSimpleMassExchange_(useSimpleMassExchange), cellConversionThreshold_(cellConversionThreshold), + cellConversionForceThreshold_(cellConversionForceThreshold) + {} + + void operator()(IBlock* const block); + + protected: + real_t sigma_; // surface tension + + BlockDataID handlingID_; + BlockDataID fillFieldID_; + BlockDataID flagFieldID_; + BlockDataID pdfFieldID_; + + ConstBlockDataID normalFieldID_; + ConstBlockDataID curvatureFieldID_; + + FlagInfo_T flagInfo_; + bubble_model::BubbleModelBase* const bubbleModel_; + + // efficient clones of fields to provide temporary fields (for writing to) + field::FieldClone< FlagField_T, true > neighborhoodFlagFieldClone_; + field::FieldClone< ScalarField_T, true > fillFieldClone_; + field::FieldClone< PdfField_T, true > pdfFieldClone_; + + PdfReconstructionModel pdfReconstructionModel_; + bool useSimpleMassExchange_; + real_t cellConversionThreshold_; + real_t cellConversionForceThreshold_; +}; // class StreamReconstructAdvectSweep + +template< typename LatticeModel_T, typename BoundaryHandling_T, typename FlagField_T, typename FlagInfo_T, + typename ScalarField_T, typename VectorField_T, bool useCodegen > +void StreamReconstructAdvectSweep< LatticeModel_T, BoundaryHandling_T, FlagField_T, FlagInfo_T, ScalarField_T, + VectorField_T, useCodegen >::operator()(IBlock* const block) +{ + // fetch data + ScalarField_T* const fillSrcField = block->getData< ScalarField_T >(fillFieldID_); + PdfField_T* const pdfSrcField = block->getData< PdfField_T >(pdfFieldID_); + + const ScalarField_T* const curvatureField = block->getData< const ScalarField_T >(curvatureFieldID_); + const VectorField_T* const normalField = block->getData< const VectorField_T >(normalFieldID_); + FlagField_T* const flagField = block->getData< FlagField_T >(flagFieldID_); + + // temporary fields that act as destination fields + PdfField_T* const pdfDstField = pdfFieldClone_.get(block); + FlagField_T* const neighborhoodFlagField = neighborhoodFlagFieldClone_.get(block); + ScalarField_T* const fillDstField = fillFieldClone_.get(block); + + // combine all neighbor flags using bitwise OR and write them to the neighborhood field + // this is simply a pre-computation of often required values + // IMPORTANT REMARK: the "OredNeighborhood" is also required for each cell in the first ghost layer; this requires + // access to all first ghost layer cell's neighbors (i.e., to the second ghost layer) + WALBERLA_CHECK_GREATER_EQUAL(flagField->nrOfGhostLayers(), uint_c(2)); + getOredNeighborhood< typename LatticeModel_T::Stencil >(flagField, neighborhoodFlagField); + + // explicitly avoid OpenMP due to bubble model update (reportFillLevelChange) + WALBERLA_FOR_ALL_CELLS_OMP( + pdfDstFieldIt, pdfDstField, pdfSrcFieldIt, pdfSrcField, fillSrcFieldIt, fillSrcField, fillDstFieldIt, + fillDstField, flagFieldIt, flagField, neighborhoodFlagFieldIt, neighborhoodFlagField, normalFieldIt, normalField, + curvatureFieldIt, curvatureField, omp critical, { + if (flagInfo_.isInterface(flagFieldIt)) + { + // get density (rhoGas) for interface PDF reconstruction + const real_t bubbleDensity = bubbleModel_->getDensity(block, pdfSrcFieldIt.cell()); + const real_t rhoGas = computeDeltaRhoLaplacePressure(sigma_, *curvatureFieldIt) + bubbleDensity; + + // reconstruct missing PDFs coming from gas neighbors according to the specified model (see dissertation of + // N. Thuerey, 2007, section 4.2); reconstruction includes streaming of PDFs to interface cells (no LBM + // stream required here) + (reconstructInterfaceCellLegacy< LatticeModel_T, FlagField_T >) (flagField, pdfSrcFieldIt, flagFieldIt, + normalFieldIt, flagInfo_, rhoGas, + pdfDstFieldIt, pdfReconstructionModel_); + + // density before LBM stream (post-collision) + const real_t oldRho = lbm::getDensity(pdfSrcField->latticeModel(), pdfSrcFieldIt); + + // density after LBM stream in reconstruction + const real_t newRho = lbm::getDensity(pdfDstField->latticeModel(), pdfDstFieldIt); + + // compute mass advection using post-collision PDFs (explicitly not PDFs updated by stream above) + const real_t deltaMass = + (advectMass< LatticeModel_T, FlagField_T, typename ScalarField_T::iterator, + typename PdfField_T::iterator, typename FlagField_T::iterator, + typename FlagField_T::iterator, FlagInfo_T >) (flagField, fillSrcFieldIt, pdfSrcFieldIt, + flagFieldIt, neighborhoodFlagFieldIt, + flagInfo_, useSimpleMassExchange_); + + // update fill level after LBM stream and mass exchange + *fillDstFieldIt = (*fillSrcFieldIt * oldRho + deltaMass) / newRho; + const real_t deltaFill = *fillDstFieldIt - *fillSrcFieldIt; + + // update the volume of bubbles + bubbleModel_->reportFillLevelChange(block, fillSrcFieldIt.cell(), deltaFill); + } + else // treat non-interface cells + { + // manually adjust the fill level to avoid outdated fill levels being copied from fillSrcField + if (flagInfo_.isGas(flagFieldIt)) { *fillDstFieldIt = real_c(0); } + else + { + if (flagInfo_.isLiquid(flagFieldIt)) + { + const Cell cell = pdfSrcFieldIt.cell(); + if constexpr (useCodegen) + { + auto lbmSweepGenerated = typename LatticeModel_T::Sweep(pdfFieldID_); + const CellInterval ci(cell, cell); + lbmSweepGenerated.streamInCellInterval(pdfSrcField, pdfDstField, ci); + } + else + { + lbm::StreamPull< LatticeModel_T >::execute(pdfSrcField, pdfDstField, cell[0], cell[1], cell[2]); + } + + *fillDstFieldIt = real_c(1); + } + else // flag is e.g. obstacle or outflow + { + *fillDstFieldIt = *fillSrcFieldIt; + } + } + } + }) // WALBERLA_FOR_ALL_CELLS_XYZ_OMP + + pdfSrcField->swapDataPointers(pdfDstField); + fillSrcField->swapDataPointers(fillDstField); + + BoundaryHandling_T* const handling = block->getData< BoundaryHandling_T >(handlingID_); + + // mark interface cell for conversion + findInterfaceCellConversions< LatticeModel_T >(handling, fillSrcField, flagField, neighborhoodFlagField, flagInfo_, + cellConversionThreshold_, cellConversionForceThreshold_); +} + +} // namespace free_surface +} // namespace walberla diff --git a/src/lbm/free_surface/dynamics/SurfaceDynamicsHandler.h b/src/lbm/free_surface/dynamics/SurfaceDynamicsHandler.h new file mode 100644 index 000000000..c0489961a --- /dev/null +++ b/src/lbm/free_surface/dynamics/SurfaceDynamicsHandler.h @@ -0,0 +1,441 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file SurfaceDynamicsHandler.h +//! \ingroup surface_dynamics +//! \author Martin Bauer +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Handles the free surface dynamics (mass advection, LBM, boundary condition, cell conversion etc.). +// +//====================================================================================================================== + +#pragma once + +#include "core/DataTypes.h" + +#include "domain_decomposition/StructuredBlockStorage.h" + +#include "field/AddToStorage.h" +#include "field/FlagField.h" + +#include "lbm/blockforest/communication/SimpleCommunication.h" +#include "lbm/blockforest/communication/UpdateSecondGhostLayer.h" +#include "lbm/free_surface/BlockStateDetectorSweep.h" +#include "lbm/free_surface/FlagInfo.h" +#include "lbm/free_surface/boundary/FreeSurfaceBoundaryHandling.h" +#include "lbm/free_surface/bubble_model/BubbleModel.h" +#include "lbm/lattice_model/SmagorinskyLES.h" +#include "lbm/sweeps/CellwiseSweep.h" +#include "lbm/sweeps/SweepWrappers.h" + +#include "stencil/D3Q27.h" + +#include "timeloop/SweepTimeloop.h" + +#include "CellConversionSweep.h" +#include "ConversionFlagsResetSweep.h" +#include "ExcessMassDistributionModel.h" +#include "ExcessMassDistributionSweep.h" +#include "ForceWeightingSweep.h" +#include "PdfReconstructionModel.h" +#include "PdfRefillingModel.h" +#include "PdfRefillingSweep.h" +#include "StreamReconstructAdvectSweep.h" + +namespace walberla +{ +namespace free_surface +{ +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T, + bool useCodegen = false > +class SurfaceDynamicsHandler +{ + protected: + using Communication_T = blockforest::SimpleCommunication< typename LatticeModel_T::Stencil >; + + // communication in corner directions (D2Q9/D3Q27) is required for all fields but the PDF field + using CommunicationStencil_T = + typename std::conditional< LatticeModel_T::Stencil::D == uint_t(2), stencil::D2Q9, stencil::D3Q27 >::type; + using CommunicationCorner_T = blockforest::SimpleCommunication< CommunicationStencil_T >; + + using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >; + + public: + SurfaceDynamicsHandler(const std::shared_ptr< StructuredBlockForest >& blockForest, BlockDataID pdfFieldID, + BlockDataID flagFieldID, BlockDataID fillFieldID, BlockDataID forceFieldID, + ConstBlockDataID normalFieldID, ConstBlockDataID curvatureFieldID, + const std::shared_ptr< FreeSurfaceBoundaryHandling_T >& freeSurfaceBoundaryHandling, + const std::shared_ptr< BubbleModelBase >& bubbleModel, + const std::string& pdfReconstructionModel, const std::string& pdfRefillingModel, + const std::string& excessMassDistributionModel, real_t relaxationRate, + const Vector3< real_t >& globalForce, real_t surfaceTension, bool enableForceWeighting, + bool useSimpleMassExchange, real_t cellConversionThreshold, + real_t cellConversionForceThreshold, BlockDataID relaxationRateFieldID = BlockDataID(), + real_t smagorinskyConstant = real_c(0)) + : blockForest_(blockForest), pdfFieldID_(pdfFieldID), flagFieldID_(flagFieldID), fillFieldID_(fillFieldID), + forceFieldID_(forceFieldID), normalFieldID_(normalFieldID), curvatureFieldID_(curvatureFieldID), + bubbleModel_(bubbleModel), freeSurfaceBoundaryHandling_(freeSurfaceBoundaryHandling), + pdfReconstructionModel_(pdfReconstructionModel), pdfRefillingModel_({ pdfRefillingModel }), + excessMassDistributionModel_({ excessMassDistributionModel }), relaxationRate_(relaxationRate), + globalForce_(globalForce), surfaceTension_(surfaceTension), enableForceWeighting_(enableForceWeighting), + useSimpleMassExchange_(useSimpleMassExchange), cellConversionThreshold_(cellConversionThreshold), + cellConversionForceThreshold_(cellConversionForceThreshold), relaxationRateFieldID_(relaxationRateFieldID), + smagorinskyConstant_(smagorinskyConstant) + { + WALBERLA_CHECK(LatticeModel_T::compressible, + "The free surface lattice Boltzmann extension works only with compressible LBM models."); + + if (useCodegen && !realIsEqual(smagorinskyConstant_, real_c(0))) + { + WALBERLA_ABORT("When using a generated LBM kernel from lbmpy, please use lbmpy's inbuilt-functionality to " + "generate the Smagorinsky model directly into the kernel."); + } + + if (excessMassDistributionModel_.isEvenlyLiquidAndAllInterfacePreferInterfaceType()) + { + // add additional field for storing excess mass in liquid cells + excessMassFieldID_ = + field::addToStorage< ScalarField_T >(blockForest_, "Excess mass", real_c(0), field::fzyx, uint_c(1)); + } + + if (LatticeModel_T::Stencil::D == uint_t(2)) + { + WALBERLA_LOG_INFO_ON_ROOT( + "IMPORTANT REMARK: You are using a D2Q9 stencil in SurfaceDynamicsHandler. In the FSLBM, a D2Q9 setup is " + "not identical to a D3Q19 setup with periodicity in the third direction but only identical to the same " + "D3Q27 setup. This comes from the distribution of excess mass, where the number of available neighbors " + "differs for D2Q9 and a periodic D3Q19 setup.") + } + } + + ConstBlockDataID getConstExcessMassFieldID() const { return excessMassFieldID_; } + + /******************************************************************************************************************** + * The order of these sweeps is similar to page 40 in the dissertation of T. Pohl, 2008. + *******************************************************************************************************************/ + void addSweeps(SweepTimeloop& timeloop) const + { + using StateSweep = BlockStateDetectorSweep< FlagField_T >; + + const auto& flagInfo = freeSurfaceBoundaryHandling_->getFlagInfo(); + + const auto blockStateUpdate = StateSweep(blockForest_, flagInfo, flagFieldID_); + + // empty sweeps required for using selectors (e.g. StateSweep::onlyGasAndBoundary) + const auto emptySweep = [](IBlock*) {}; + + // add standard waLBerla boundary handling + timeloop.add() << Sweep(freeSurfaceBoundaryHandling_->getBoundarySweep(), "Sweep: boundary handling", + Set< SUID >::emptySet(), StateSweep::onlyGasAndBoundary) + << Sweep(emptySweep, "Empty sweep: boundary handling", StateSweep::onlyGasAndBoundary); + + if (enableForceWeighting_) + { + // add sweep for weighting force in interface cells with fill level and density + const ForceWeightingSweep< LatticeModel_T, FlagField_T, VectorField_T, ScalarField_T > forceWeightingSweep( + forceFieldID_, pdfFieldID_, flagFieldID_, fillFieldID_, flagInfo, globalForce_); + timeloop.add() << Sweep(forceWeightingSweep, "Sweep: force weighting", Set< SUID >::emptySet(), + StateSweep::onlyGasAndBoundary) + << Sweep(emptySweep, "Empty sweep: force weighting", StateSweep::onlyGasAndBoundary) + << AfterFunction(CommunicationCorner_T(blockForest_, forceFieldID_), + "Communication: after force weighting sweep"); + } + + // sweep for + // - reconstruction of PDFs in interface cells + // - streaming of PDFs in interface cells (and liquid cells on the same block) + // - advection of mass + // - update bubble volumes + // - marking interface cells for conversion + const StreamReconstructAdvectSweep< LatticeModel_T, typename FreeSurfaceBoundaryHandling_T::BoundaryHandling_T, + FlagField_T, typename FreeSurfaceBoundaryHandling_T::FlagInfo_T, + ScalarField_T, VectorField_T, useCodegen > + streamReconstructAdvectSweep(surfaceTension_, freeSurfaceBoundaryHandling_->getHandlingID(), fillFieldID_, + flagFieldID_, pdfFieldID_, normalFieldID_, curvatureFieldID_, flagInfo, + bubbleModel_.get(), pdfReconstructionModel_, useSimpleMassExchange_, + cellConversionThreshold_, cellConversionForceThreshold_); + // sweep acts only on blocks with at least one interface cell (due to StateSweep::fullFreeSurface) + timeloop.add() + << Sweep(streamReconstructAdvectSweep, "Sweep: StreamReconstructAdvect", StateSweep::fullFreeSurface) + << Sweep(emptySweep, "Empty sweep: StreamReconstructAdvect") + // do not communicate PDFs here: + // - stream on blocks with "StateSweep::fullFreeSurface" was performed here using post-collision PDFs + // - stream on other blocks is performed below and should also use post-collision PDFs + // => if PDFs were communicated here, the ghost layer of other blocks would have post-stream PDFs + << AfterFunction(CommunicationCorner_T(blockForest_, fillFieldID_, flagFieldID_), + "Communication: after StreamReconstructAdvect sweep") + << AfterFunction(blockforest::UpdateSecondGhostLayer< ScalarField_T >(blockForest_, fillFieldID_), + "Second ghost layer update: after StreamReconstructAdvect sweep (fill level field)") + << AfterFunction(blockforest::UpdateSecondGhostLayer< FlagField_T >(blockForest_, flagFieldID_), + "Second ghost layer update: after StreamReconstructAdvect sweep (flag field)"); + + if constexpr (useCodegen) + { + auto lbmSweepGenerated = typename LatticeModel_T::Sweep(pdfFieldID_); + + // temporary class for being able to call the LBM collision with operator() + class CollideSweep + { + public: + CollideSweep(const typename LatticeModel_T::Sweep& sweep) : sweep_(sweep){}; + + void operator()(IBlock* const block, const uint_t numberOfGhostLayersToInclude = uint_t(1)) + { + sweep_.collide(block, numberOfGhostLayersToInclude); + } + + private: + typename LatticeModel_T::Sweep sweep_; + }; + + timeloop.add() << Sweep(CollideSweep(lbmSweepGenerated), "Sweep: collision (generated)", + StateSweep::fullFreeSurface) + << Sweep(lbmSweepGenerated, "Sweep: streamCollide (generated)", StateSweep::onlyLBM) + << Sweep(emptySweep, "Empty sweep: streamCollide (generated)") + << AfterFunction(Communication_T(blockForest_, pdfFieldID_), + "Communication: after streamCollide (generated)"); + } + else + { + // sweep for standard LBM stream and collision + const auto lbmSweep = lbm::makeCellwiseSweep< LatticeModel_T, FlagField_T >(pdfFieldID_, flagFieldID_, + flagIDs::liquidInterfaceFlagIDs); + + // LBM collision in interface cells; standard LBM stream and collision in liquid cells + if (!realIsEqual(smagorinskyConstant_, real_c(0), real_c(1e-14))) // using Smagorinsky turbulence model + { + const real_t kinematicViscosity = (real_c(1) / relaxationRate_ - real_c(0.5)) / real_c(3); + + // standard LBM stream in liquid cells that have not been streamed, yet + timeloop.add() << Sweep(lbm::makeStreamSweep(lbmSweep), "Stream sweep", StateSweep::onlyLBM) + << Sweep(emptySweep, "Deactivated Stream sweep") + << AfterFunction(Communication_T(blockForest_, pdfFieldID_), + "Communication after Stream sweep"); + + // sweep for turbulence modelling + const lbm::SmagorinskyLES< LatticeModel_T > smagorinskySweep( + blockForest_, pdfFieldID_, relaxationRateFieldID_, kinematicViscosity, real_c(0.12)); + + timeloop.add() + // Smagorinsky turbulence model + << BeforeFunction(smagorinskySweep, "Sweep: Smagorinsky turbulence model") + << BeforeFunction(CommunicationCorner_T(blockForest_, relaxationRateFieldID_), + "Communication: after Smagorinsky sweep") + // standard LBM collision + << Sweep(lbm::makeCollideSweep(lbmSweep), "Sweep: collision after Smagorinsky sweep") + << AfterFunction(Communication_T(blockForest_, pdfFieldID_), + "Communication: after collision sweep with preceding Smagorinsky sweep"); + } + else + { + // no turbulence model + timeloop.add() + // collision in interface cells and liquid cells that have already been streamed (in + // streamReconstructAdvectSweep due to StateSweep::fullFreeSurface) + << Sweep(lbm::makeCollideSweep(lbmSweep), "Sweep: collision", StateSweep::fullFreeSurface) + // standard LBM stream-collide in liquid cells that have not been streamed, yet + << Sweep(*lbmSweep, "Sweep: streamCollide", StateSweep::onlyLBM) + << Sweep(emptySweep, "Empty sweep: streamCollide") + << AfterFunction(Communication_T(blockForest_, pdfFieldID_), "Communication: after streamCollide sweep"); + } + } + + // convert cells + // - according to the flags from StreamReconstructAdvectSweep (interface -> gas/liquid) + // - to ensure a closed layer of interface cells (gas/liquid -> interface) + // - detect and register bubble merges/splits (bubble volumes are already updated in StreamReconstructAdvectSweep) + // - convert cells and initialize PDFs near inflow boundaries + const CellConversionSweep< LatticeModel_T, typename FreeSurfaceBoundaryHandling_T::BoundaryHandling_T, + ScalarField_T > + cellConvSweep(freeSurfaceBoundaryHandling_->getHandlingID(), pdfFieldID_, flagInfo, bubbleModel_.get()); + timeloop.add() << Sweep(cellConvSweep, "Sweep: cell conversion", StateSweep::fullFreeSurface) + << Sweep(emptySweep, "Empty sweep: cell conversion") + << AfterFunction(Communication_T(blockForest_, pdfFieldID_), + "Communication: after cell conversion sweep (PDF field)") + // communicate the flag field also in corner directions + << AfterFunction(CommunicationCorner_T(blockForest_, flagFieldID_), + "Communication: after cell conversion sweep (flag field)") + << AfterFunction(blockforest::UpdateSecondGhostLayer< FlagField_T >(blockForest_, flagFieldID_), + "Second ghost layer update: after cell conversion sweep (flag field)"); + + // reinitialize PDFs, i.e., refill cells that were converted from gas to interface + // - when the flag "convertedFromGasToInterface" has been set (by CellConversionSweep) + // - according to the method specified with pdfRefillingModel_ + switch (pdfRefillingModel_.getModelType()) + { // the scope for each "case" is required since variables are defined within "case" + case PdfRefillingModel::RefillingModel::EquilibriumRefilling: { + const EquilibriumRefillingSweep< LatticeModel_T, FlagField_T > equilibriumRefillingSweep( + pdfFieldID_, flagFieldID_, flagInfo, true); + timeloop.add() << Sweep(equilibriumRefillingSweep, "Sweep: EquilibriumRefilling", StateSweep::fullFreeSurface) + << Sweep(emptySweep, "Empty sweep: EquilibriumRefilling") + << AfterFunction(Communication_T(blockForest_, pdfFieldID_), + "Communication: after EquilibriumRefilling sweep"); + break; + } + case PdfRefillingModel::RefillingModel::AverageRefilling: { + const AverageRefillingSweep< LatticeModel_T, FlagField_T > averageRefillingSweep(pdfFieldID_, flagFieldID_, + flagInfo, true); + timeloop.add() << Sweep(averageRefillingSweep, "Sweep: AverageRefilling", StateSweep::fullFreeSurface) + << Sweep(emptySweep, "Empty sweep: AverageRefilling") + << AfterFunction(Communication_T(blockForest_, pdfFieldID_), + "Communication: after AverageRefilling sweep"); + break; + } + case PdfRefillingModel::RefillingModel::EquilibriumAndNonEquilibriumRefilling: { + // default: extrapolation order: 0 + const EquilibriumAndNonEquilibriumRefillingSweep< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > + equilibriumAndNonEquilibriumRefillingSweep(pdfFieldID_, flagFieldID_, fillFieldID_, flagInfo, uint_c(0), + true); + timeloop.add() << Sweep(equilibriumAndNonEquilibriumRefillingSweep, + "Sweep: EquilibriumAndNonEquilibriumRefilling sweep", StateSweep::fullFreeSurface) + << Sweep(emptySweep, "Empty sweep: EquilibriumAndNonEquilibriumRefilling") + << AfterFunction(Communication_T(blockForest_, pdfFieldID_), + "Communication: after EquilibriumAndNonEquilibriumRefilling sweep"); + break; + } + case PdfRefillingModel::RefillingModel::ExtrapolationRefilling: { + // default: extrapolation order: 2 + const ExtrapolationRefillingSweep< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > + extrapolationRefillingSweep(pdfFieldID_, flagFieldID_, fillFieldID_, flagInfo, uint_c(2), true); + timeloop.add() << Sweep(extrapolationRefillingSweep, "Sweep: ExtrapolationRefilling", + StateSweep::fullFreeSurface) + << Sweep(emptySweep, "Empty sweep: ExtrapolationRefilling") + << AfterFunction(Communication_T(blockForest_, pdfFieldID_), + "Communication: after ExtrapolationRefilling sweep"); + break; + } + case PdfRefillingModel::RefillingModel::GradsMomentsRefilling: { + const GradsMomentsRefillingSweep< LatticeModel_T, FlagField_T > gradsMomentRefillingSweep( + pdfFieldID_, flagFieldID_, flagInfo, relaxationRate_, true); + timeloop.add() << Sweep(gradsMomentRefillingSweep, "Sweep: GradsMomentRefilling", StateSweep::fullFreeSurface) + << Sweep(emptySweep, "Empty sweep: GradsMomentRefilling") + << AfterFunction(Communication_T(blockForest_, pdfFieldID_), + "Communication: after GradsMomentRefilling sweep"); + break; + } + default: + WALBERLA_ABORT("The specified pdf refilling model is not available."); + } + + // distribute excess mass: + // - excess mass: mass that is free after conversion from interface to gas/liquid cells + // - update the bubble model + // IMPORTANT REMARK: this sweep computes the mass via the density, i.e., the PDF field must be up-to-date and the + // PdfRefillingSweep must have been performed + if (excessMassDistributionModel_.isEvenlyType()) + { + const ExcessMassDistributionSweepInterfaceEvenly< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > + distributeMassSweep(excessMassDistributionModel_, fillFieldID_, flagFieldID_, pdfFieldID_, flagInfo); + timeloop.add() << Sweep(distributeMassSweep, "Sweep: excess mass distribution", StateSweep::fullFreeSurface) + << Sweep(emptySweep, "Empty sweep: distribute excess mass") + << AfterFunction(CommunicationCorner_T(blockForest_, fillFieldID_), + "Communication: after excess mass distribution sweep") + << AfterFunction( + blockforest::UpdateSecondGhostLayer< ScalarField_T >(blockForest_, fillFieldID_), + "Second ghost layer update: after excess mass distribution sweep (fill level field)") + // update bubble model, i.e., perform registered bubble merges/splits; bubble merges/splits are + // already detected and registered by CellConversionSweep + << AfterFunction(std::bind(&bubble_model::BubbleModelBase::update, bubbleModel_), + "Sweep: bubble model update"); + } + else + { + if (excessMassDistributionModel_.isWeightedType()) + { + const ExcessMassDistributionSweepInterfaceWeighted< LatticeModel_T, FlagField_T, ScalarField_T, + VectorField_T > + distributeMassSweep(excessMassDistributionModel_, fillFieldID_, flagFieldID_, pdfFieldID_, flagInfo, + normalFieldID_); + timeloop.add() << Sweep(distributeMassSweep, "Sweep: excess mass distribution", StateSweep::fullFreeSurface) + << Sweep(emptySweep, "Empty sweep: distribute excess mass") + << AfterFunction(CommunicationCorner_T(blockForest_, fillFieldID_), + "Communication: after excess mass distribution sweep") + << AfterFunction( + blockforest::UpdateSecondGhostLayer< ScalarField_T >(blockForest_, fillFieldID_), + "Second ghost layer update: after excess mass distribution sweep (fill level field)") + // update bubble model, i.e., perform registered bubble merges/splits; bubble merges/splits + // are already detected and registered by CellConversionSweep + << AfterFunction(std::bind(&bubble_model::BubbleModelBase::update, bubbleModel_), + "Sweep: bubble model update"); + } + else + { + if (excessMassDistributionModel_.isEvenlyLiquidAndAllInterfacePreferInterfaceType()) + { + const ExcessMassDistributionSweepInterfaceAndLiquid< LatticeModel_T, FlagField_T, ScalarField_T, + VectorField_T > + distributeMassSweep(excessMassDistributionModel_, fillFieldID_, flagFieldID_, pdfFieldID_, flagInfo, + excessMassFieldID_); + timeloop.add() + << Sweep(distributeMassSweep, "Sweep: excess mass distribution", StateSweep::fullFreeSurface) + << Sweep(emptySweep, "Empty sweep: distribute excess mass") + << AfterFunction(CommunicationCorner_T(blockForest_, fillFieldID_, excessMassFieldID_), + "Communication: after excess mass distribution sweep") + << AfterFunction(blockforest::UpdateSecondGhostLayer< ScalarField_T >(blockForest_, fillFieldID_), + "Second ghost layer update: after excess mass distribution sweep (fill level field)") + // update bubble model, i.e., perform registered bubble merges/splits; bubble + // merges/splits are already detected and registered by CellConversionSweep + << AfterFunction(std::bind(&bubble_model::BubbleModelBase::update, bubbleModel_), + "Sweep: bubble model update"); + } + } + } + + // reset all flags that signal cell conversions (except "keepInterfaceForWettingFlag") + ConversionFlagsResetSweep< FlagField_T > resetConversionFlagsSweep(flagFieldID_, flagInfo); + timeloop.add() << Sweep(resetConversionFlagsSweep, "Sweep: conversion flag reset", StateSweep::fullFreeSurface) + << Sweep(emptySweep, "Empty sweep: conversion flag reset") + << AfterFunction(CommunicationCorner_T(blockForest_, flagFieldID_), + "Communication: after excess mass distribution sweep") + << AfterFunction(blockforest::UpdateSecondGhostLayer< FlagField_T >(blockForest_, flagFieldID_), + "Second ghost layer update: after excess mass distribution sweep (flag field)"); + + // update block states + timeloop.add() << Sweep(blockStateUpdate, "Sweep: block state update"); + } + + private: + std::shared_ptr< StructuredBlockForest > blockForest_; + + BlockDataID pdfFieldID_; + BlockDataID flagFieldID_; + BlockDataID fillFieldID_; + BlockDataID forceFieldID_; + + ConstBlockDataID normalFieldID_; + ConstBlockDataID curvatureFieldID_; + + std::shared_ptr< BubbleModelBase > bubbleModel_; + std::shared_ptr< FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling_; + + PdfReconstructionModel pdfReconstructionModel_; + PdfRefillingModel pdfRefillingModel_; + ExcessMassDistributionModel excessMassDistributionModel_; + real_t relaxationRate_; + Vector3< real_t > globalForce_; + real_t surfaceTension_; + bool enableForceWeighting_; + bool useSimpleMassExchange_; + real_t cellConversionThreshold_; + real_t cellConversionForceThreshold_; + + BlockDataID relaxationRateFieldID_; + real_t smagorinskyConstant_; + + BlockDataID excessMassFieldID_ = BlockDataID(); +}; // class SurfaceDynamicsHandler + +} // namespace free_surface +} // namespace walberla \ No newline at end of file diff --git a/src/lbm/free_surface/dynamics/functionality/AdvectMass.h b/src/lbm/free_surface/dynamics/functionality/AdvectMass.h new file mode 100644 index 000000000..5e198e2db --- /dev/null +++ b/src/lbm/free_surface/dynamics/functionality/AdvectMass.h @@ -0,0 +1,305 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file AdvectMass.h +//! \ingroup dynamics +//! \author Martin Bauer +//! \author Matthias Markl <matthias.markl@fau.de> +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Calculate the mass advection for a single interface cell. +// +//====================================================================================================================== + +#pragma once + +#include "core/Abort.h" +#include "core/logging/Logging.h" +#include "core/timing/TimingPool.h" + +#include "domain_decomposition/IBlock.h" + +#include "lbm/field/MacroscopicValueCalculation.h" +#include "lbm/field/PdfField.h" +#include "lbm/free_surface/FlagInfo.h" +#include "lbm/free_surface/bubble_model/BubbleModel.h" + +#include <type_traits> + +namespace walberla +{ +namespace free_surface +{ +/*********************************************************************************************************************** + * Calculate the mass advection for a single interface cell and returns the mass delta for this cell. + * + * With "useSimpleMassExchange==true", mass exchange is performed according to the paper from Koerner et al., 2005, + * equation (9). That is, mass is exchanged regardless of the cells' neighborhood. + * Mass exchange of interface cell with + * - obstacle cell: no mass is exchanged + * - gas cell: no mass is exchanged + * - liquid cell: mass exchange is determined by the difference between incoming and outgoing PDFs + * - interface cell: The mass exchange is determined by the difference between incoming and outgoing PDFs weighted with + * the average fill level of both interface cells. This is basically a linear estimate of the wetted + * area between the two cells. + * - free slip cell: mass is exchanged with the cell where the mirrored PDFs are coming from + * - inflow cell: mass exchange as with liquid cell + * - outflow cell: mass exchange as with interface cell (outflow cell is assumed to have the same fill level) + * + * With "useSimpleMassExchange==false", mass is exchanged according to the dissertation of N. Thuerey, + * 2007, sections 4.1 and 4.4, and table 4.1. However, here, the fill level field and density are used for the + * computations instead of storing an additional mass field. + * To ensure a single interface layer, an interface cell is + * - forced to empty if it has no fluid neighbors. + * - forced to fill if it has no gas neighbors. + * This is done by modifying the exchanged PDFs accordingly. If an interface cell is + * - forced to empty, only outgoing PDFs but no incoming PDFs are allowed. + * - forced to fill, only incoming PDFs but no outgoing PDFs are allowed. + * A more detailed description is available in the dissertation of N. Thuerey, 2007, section 4.4 and table 4.1. + * + * neighIt is an iterator pointing to a pre-computed flag field that contains the bitwise OR'ed neighborhood flags of + * the current cell. See free_surface::getOredNeighborhood for more information. + **********************************************************************************************************************/ +template< typename LatticeModel_T, typename FlagField_T, typename ConstScalarIt_T, typename ConstPdfIt_T, + typename ConstFlagIt_T, typename ConstNeighIt_T, typename FlagInfo_T > +real_t advectMass(const FlagField_T* flagField, const ConstScalarIt_T& fillSrc, const ConstPdfIt_T& pdfFieldIt, + const ConstFlagIt_T& flagFieldIt, const ConstNeighIt_T& neighIt, const FlagInfo_T& flagInfo, + bool useSimpleMassExchange) +{ + using flag_c_t = typename std::remove_const< typename ConstFlagIt_T::value_type >::type; + using flag_n_t = typename std::remove_const< typename ConstNeighIt_T::value_type >::type; + using flag_i_t = typename std::remove_const< typename FlagField_T::value_type >::type; + + static_assert(std::is_same< flag_i_t, flag_c_t >::value && std::is_same< flag_i_t, flag_n_t >::value, + "Flag types have to be equal."); + + WALBERLA_ASSERT(flagInfo.isInterface(flagFieldIt), "Function advectMass() must only be called for interface cell."); + + // determine the type of the current interface cell (similar to section 4.4 in the dissertation of N. Thuerey, + // 2007) neighIt is the pre-computed bitwise OR-ed neighborhood of the cell + bool localNoGasNeig = !flagInfo.isGas(*neighIt); // this cell has no gas neighbor (should be converted to liquid) + bool localNoFluidNeig = !flagInfo.isLiquid(*neighIt); // this cell has no fluid neighbor (should be converted to gas) + bool localStandardCell = !(localNoGasNeig || localNoFluidNeig); // this cell has both gas and fluid neighbors + + // evaluate flag of this cell (flagFieldIt) and not the neighborhood flags (neighIt) + bool localWettingCell = flagInfo.isKeepInterfaceForWetting(*flagFieldIt); // this cell should be kept for wetting + + if (localNoFluidNeig && localNoGasNeig && + !localWettingCell) // this cell has only interface neighbors (interface layer of 3 cells width) + { + // WALBERLA_LOG_WARNING("Interface layer of 3 cells width at cell " << fillSrc.cell()); + // set this cell to standard for enabling regular mass exchange + localNoGasNeig = false; + localNoFluidNeig = false; + localStandardCell = true; + } + + real_t deltaMass = real_c(0); + for (auto dir = LatticeModel_T::Stencil::beginNoCenter(); dir != LatticeModel_T::Stencil::end(); ++dir) + { + flag_c_t neighFlag = flagFieldIt.neighbor(*dir); + + bool isFreeSlip = false; // indicates whether dir points to a free slip cell + + // from the viewpoint of the current cell, direction where the PDFs are actually coming from when there is a free + // slip cell in direction dir; explicitly not a Cell object to emphasize that this denotes a direction + Vector3< cell_idx_t > freeSlipDir; + + // determine type of cell where the mirrored PDFs at free slip boundary are coming from + if (flagInfo.isFreeSlip(neighFlag)) + { + // REMARK: the following implementation is based on lbm/boundary/FreeSlip.h + + // get components of inverse direction of dir + const int ix = stencil::cx[stencil::inverseDir[*dir]]; + const int iy = stencil::cy[stencil::inverseDir[*dir]]; + const int iz = stencil::cz[stencil::inverseDir[*dir]]; + + int wnx = 0; // compute "normal" vector of free slip wall + int wny = 0; + int wnz = 0; + + // from the current cell, go into neighboring cell in direction dir and from there determine the type of the + // neighboring cell in ix, iy and iz direction + const auto flagFieldFreeSlipPtrX = typename FlagField_T::ConstPtr( + *flagField, flagFieldIt.x() + dir.cx() + ix, flagFieldIt.y() + dir.cy(), flagFieldIt.z() + dir.cz()); + if (flagInfo.isLiquid(*flagFieldFreeSlipPtrX) || flagInfo.isInterface(*flagFieldFreeSlipPtrX)) { wnx = ix; } + + const auto flagFieldFreeSlipPtrY = typename FlagField_T::ConstPtr( + *flagField, flagFieldIt.x() + dir.cx(), flagFieldIt.y() + dir.cy() + iy, flagFieldIt.z() + dir.cz()); + if (flagInfo.isLiquid(*flagFieldFreeSlipPtrY) || flagInfo.isInterface(*flagFieldFreeSlipPtrY)) { wny = iy; } + + const auto flagFieldFreeSlipPtrZ = typename FlagField_T::ConstPtr( + *flagField, flagFieldIt.x() + dir.cx(), flagFieldIt.y() + dir.cy(), flagFieldIt.z() + dir.cz() + iz); + if (flagInfo.isLiquid(*flagFieldFreeSlipPtrZ) || flagInfo.isInterface(*flagFieldFreeSlipPtrZ)) { wnz = iz; } + + // flagFieldFreeSlipPtr denotes the cell from which the PDF is coming from + const auto flagFieldFreeSlipPtr = + typename FlagField_T::ConstPtr(*flagField, flagFieldIt.x() + dir.cx() + wnx, + flagFieldIt.y() + dir.cy() + wny, flagFieldIt.z() + dir.cz() + wnz); + + // no mass must be exchanged if: + // - PDFs are not coming from liquid or interface cells + // - PDFs are mirrored from this cell (deltaPdf=0) + if ((!flagInfo.isLiquid(*flagFieldFreeSlipPtr) && !flagInfo.isInterface(*flagFieldFreeSlipPtr)) || + flagFieldFreeSlipPtr.cell() == flagFieldIt.cell()) + { + continue; + } + else + { + // update neighFlag such that it does contain the flags of the cell that mirrored at the free slip boundary; + // PDFs from the boundary cell can be used because they were correctly updated by the boundary handling + neighFlag = *flagFieldFreeSlipPtr; + + // direction of the cell that is mirrored at the free slip boundary + freeSlipDir = Vector3< cell_idx_t >(cell_idx_c(dir.cx() + wnx), cell_idx_c(dir.cy() + wny), + cell_idx_c(dir.cz() + wnz)); + isFreeSlip = true; + } + } + + // no mass exchange with gas and obstacle cells that are neither inflow nor outflow + if (flagInfo.isGas(neighFlag) || + (flagInfo.isObstacle(neighFlag) && !flagInfo.isInflow(neighFlag) && !flagInfo.isOutflow(neighFlag))) + { + continue; + } + + // PDF pointing from neighbor to current cell + const real_t neighborPdf = pdfFieldIt.neighbor(*dir, dir.toInvIdx()); + + // PDF pointing to neighbor + const real_t localPdf = pdfFieldIt.getF(dir.toIdx()); + + // mass exchange with liquid cells (inflow cells are considered to be liquid) + if (flagInfo.isLiquid(neighFlag) || flagInfo.isInflow(neighFlag)) + { + // mass exchange is difference between incoming and outgoing PDFs (see equation (9) in Koerner et al., 2005) + deltaMass += neighborPdf - localPdf; + continue; + } + + // assert cells that are neither gas, obstacle nor interface + WALBERLA_ASSERT(flagInfo.isInterface(neighFlag) || flagInfo.isOutflow(neighFlag), + "In cell " << fillSrc.cell() << ", flag of neighboring cell " + << Cell(fillSrc.x() + dir.cx(), fillSrc.y() + dir.cy(), fillSrc.z() + dir.cz()) + << " is not plausible."); + + // direction of the cell from which the PDFs are coming from + const Vector3< cell_idx_t > relevantDir = + isFreeSlip ? freeSlipDir : + Vector3< cell_idx_t >(cell_idx_c(dir.cx()), cell_idx_c(dir.cy()), cell_idx_c(dir.cz())); + + // determine the type of the neighboring cell (similar to section 4.4 in the dissertation of N. Thuerey, 2007) + bool neighborNoGasNeig = !flagInfo.isGas(neighIt.neighbor(relevantDir[0], relevantDir[1], relevantDir[2])); + bool neighborNoFluidNeig = !flagInfo.isLiquid(neighIt.neighbor(relevantDir[0], relevantDir[1], relevantDir[2])); + bool neighborStandardCell = !(neighborNoGasNeig || neighborNoFluidNeig); + bool neighborWettingCell = flagInfo.isKeepInterfaceForWetting(flagFieldIt.neighbor( + relevantDir[0], relevantDir[1], + relevantDir[2])); // evaluate flag of this cell (flagFieldIt) and not the neighborhood flags (neighIt) + + if (neighborNoGasNeig && neighborNoFluidNeig && + !neighborWettingCell) // neighboring cell has only interface neighbors + { + // WALBERLA_LOG_WARNING("Interface layer of 3 cells width at cell " << fillSrc.cell()); + // set neighboring cell to standard for enabling regular mass exchange + neighborNoGasNeig = false; + neighborNoFluidNeig = false; + neighborStandardCell = true; + } + + const real_t localFill = *fillSrc; + const real_t neighborFill = fillSrc.neighbor(relevantDir[0], relevantDir[1], relevantDir[2]); + + real_t fillAvg = real_c(0); + real_t deltaPdf = real_c(0); // deltaMass = fillAvg * deltaPdf (see equation (9) in Koerner et al., 2005) + + // both cells are interface cells (standard mass exchange) + // see paper of C. Koerner et al., 2005, equation (9) + // see dissertation of N. Thuerey, 2007, table 4.1: (standard at x) <-> (standard at x_nb); + if (useSimpleMassExchange || (localStandardCell && (neighborStandardCell || neighborWettingCell)) || + (neighborStandardCell && (localStandardCell || localWettingCell)) || flagInfo.isOutflow(neighFlag)) + { + if (flagInfo.isOutflow(neighFlag)) + { + fillAvg = localFill; // use local fill level only, since outflow cells do not have a meaningful fill level + } + else { fillAvg = real_c(0.5) * (neighborFill + localFill); } + + deltaPdf = neighborPdf - localPdf; + } + else + { + // see dissertation of N. Thuerey, 2007, table 4.1: + // (standard at x) <-> (no empty neighbors at x_nb) + // (no fluid neighbors at x) <-> (standard cell at x_nb) + // (no fluid neighbors at x) <-> (no empty neighbors at x_nb) + // => push local, i.e., this cell empty (if it is not a cell needed for wetting) + if (((localStandardCell && neighborNoGasNeig) || (localNoFluidNeig && !neighborNoFluidNeig)) && + !localWettingCell) + { + fillAvg = real_c(0.5) * (neighborFill + localFill); + deltaPdf = -localPdf; + } + else + { + // see dissertation of N. Thuerey, 2007, table 4.1: + // (standard at x) <-> (no fluid neighbors at x_nb) + // (no empty neighbors at x) <-> (standard cell at x_nb) + // (no empty neighbors at x) <-> (no fluid neighbors at x_nb) + // => push neighboring cell empty (if it is not a cell needed for wetting) + if (((localStandardCell && neighborNoFluidNeig) || (localNoGasNeig && !neighborNoGasNeig)) && + !neighborWettingCell) + { + fillAvg = real_c(0.5) * (neighborFill + localFill); + deltaPdf = neighborPdf; + } + else + { + // see dissertation of N. Thuerey, 2007, table 4.1: + // (no fluid neighbors at x) <-> (no fluid neighbors at x_nb) + // (no empty neighbors at x) <-> (no empty neighbors at x_nb) + if ((localNoFluidNeig && neighborNoFluidNeig) || (localNoGasNeig && neighborNoGasNeig)) + { + fillAvg = real_c(0.5) * (neighborFill + localFill); + deltaPdf = (neighborPdf - localPdf); + } + else + { + // treat remaining cases that were not covered above and include wetting cells + if (localWettingCell || neighborWettingCell) + { + fillAvg = real_c(0.5) * (neighborFill + localFill); + deltaPdf = (neighborPdf - localPdf); + } + else + { + WALBERLA_ABORT("Unknown mass advection combination of flags (loc=" << *flagFieldIt << ", neig=" + << neighFlag << ")"); + } + } + } + } + } + // this cell's deltaMass is the sum over all stencil directions (see dissertation of N. Thuerey, 2007, equation + // (4.4)) + deltaMass += fillAvg * deltaPdf; + } + + return deltaMass; +} + +} // namespace free_surface +} // namespace walberla diff --git a/src/lbm/free_surface/dynamics/functionality/CMakeLists.txt b/src/lbm/free_surface/dynamics/functionality/CMakeLists.txt new file mode 100644 index 000000000..357412052 --- /dev/null +++ b/src/lbm/free_surface/dynamics/functionality/CMakeLists.txt @@ -0,0 +1,8 @@ +target_sources( lbm + PRIVATE + AdvectMass.h + FindInterfaceCellConversion.h + GetLaplacePressure.h + GetOredNeighborhood.h + ReconstructInterfaceCellABB.h + ) diff --git a/src/lbm/free_surface/dynamics/functionality/FindInterfaceCellConversion.h b/src/lbm/free_surface/dynamics/functionality/FindInterfaceCellConversion.h new file mode 100644 index 000000000..6445a61f6 --- /dev/null +++ b/src/lbm/free_surface/dynamics/functionality/FindInterfaceCellConversion.h @@ -0,0 +1,255 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file FindInterfaceCellConversion.h +//! \ingroup dynamics +//! \author Martin Bauer +//! \author Matthias Markl <matthias.markl@fau.de> +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Find and mark interface cells for conversion to gas/liquid. +// +//====================================================================================================================== + +#pragma once + +#include "core/debug/Debug.h" +#include "core/logging/Logging.h" +#include "core/timing/TimingPool.h" + +#include "lbm/free_surface/FlagInfo.h" + +#include <type_traits> + +namespace walberla +{ +namespace free_surface +{ +/*********************************************************************************************************************** + * Finds interface cell conversions to gas/liquid and sets corresponding conversion flag that marks this cell for + * conversion. + * + * This version uses the cached OR'ed flag neighborhood given in neighborFlags. See free_surface::getOredNeighborhood. + * + * This function decides by looking at the fill level which interface cells should be converted to gas or liquid cells. + * It does not change the state directly, it only sets "conversion suggestions" by setting flags called + * 'convertToGasFlag' and 'convertToLiquidFlag'. + * + * An interface is first classified as one of the following types (same classification as in free_surface::advectMass) + * - _to gas_ : if cell has no liquid cell in neighborhood, this cell should become gas + * - _to liquid_: if cell has no gas cell in neighborhood, this cell should become liquid + * - _pure interface_: if not '_to gas_' and not '_to liquid_' + * + * This classification up to now depends only on the neighborhood flags, not on the fill level. + * + * _Pure interface_ cells are marked for to-gas-conversion if their fill level is lower than + * "0 - cellConversionThreshold" and marked for to-liquid-conversion if the fill level is higher than + * "1 + cellConversionThreshold". The threshold is introduced to prevent oscillating cell conversions. + * The value of the offset is chosen heuristically (see dissertation of N. Thuerey, 2007: 1e-3, dissertation of + * T. Pohl, 2008: 1e-2). + * + * Additionally, interface cells without fluid neighbors are marked for conversion to gas if an: + * - interface cell is almost empty (fill < cellConversionForceThreshold) AND + * - interface cell has no neighboring interface cells + * OR + * - interface cell has neighboring cell with outflow boundary condition + * + * Similarly, interface cells without gas neighbors are marked for conversion to liquid if: + * - interface cell is almost full (fill > 1.0-cellConversionForceThreshold) AND + * - interface cell has no neighboring interface cells + * + * The value of cellConversionForceThreshold is chosen heuristically: 1e-1 (see dissertation of N. Thuerey, 2007, + * section 4.4). + **********************************************************************************************************************/ +template< typename LatticeModel_T, typename BoundaryHandling_T, typename ScalarField_T, typename FlagField_T, + typename ScalarIt_T, typename FlagIt_T > +void findInterfaceCellConversion(const BoundaryHandling_T& handling, const ScalarIt_T& fillFieldIt, + FlagIt_T& flagFieldIt, const typename FlagField_T::flag_t& neighborFlags, + const FlagInfo< FlagField_T >& flagInfo, real_t cellConversionThreshold, + real_t cellConversionForceThreshold) +{ + static_assert(std::is_same< typename FlagField_T::value_type, typename FlagIt_T::value_type >::value, + "The given flagFieldIt does not seem to be an iterator of the provided FlagField_T."); + + static_assert(std::is_floating_point< typename ScalarIt_T::value_type >::value, + "The given fillFieldIt has to be a floating point value."); + + cellConversionThreshold = std::abs(cellConversionThreshold); + cellConversionForceThreshold = std::abs(cellConversionForceThreshold); + + WALBERLA_ASSERT_LESS(cellConversionThreshold, cellConversionForceThreshold); + + // in the neighborhood of inflow boundaries, convert gas cells to interface cells depending on the direction of the + // prescribed inflow velocity + if (field::isFlagSet(neighborFlags, flagInfo.inflowFlagMask) && field::isFlagSet(flagFieldIt, flagInfo.gasFlag)) + { + // get UBB inflow boundary + auto ubbInflow = handling->template getBoundaryCondition< + typename FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::UBB_Inflow_T >( + handling->getBoundaryUID( + FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::ubbInflowFlagID)); + + for (auto d = LatticeModel_T::Stencil::beginNoCenter(); d != LatticeModel_T::Stencil::end(); ++d) + { + if (field::isMaskSet(flagFieldIt.neighbor(*d), flagInfo.inflowFlagMask)) + { + // get direction of cell containing inflow boundary + const Vector3< int > dir = Vector3< int >(-d.cx(), -d.cy(), -d.cz()); + + // get velocity from UBB inflow boundary + const Vector3< real_t > inflowVel = + ubbInflow.getValue(flagFieldIt.x() + d.cx(), flagFieldIt.y() + d.cy(), flagFieldIt.z() + d.cz()); + + // skip directions in which the corresponding velocity component is zero + if (realIsEqual(inflowVel[0], real_c(0), real_c(1e-14)) && dir[0] != 0) { continue; } + if (realIsEqual(inflowVel[1], real_c(0), real_c(1e-14)) && dir[1] != 0) { continue; } + if (realIsEqual(inflowVel[2], real_c(0), real_c(1e-14)) && dir[2] != 0) { continue; } + + // skip directions in which the corresponding velocity component is in opposite direction + if (inflowVel[0] > real_c(0) && dir[0] < 0) { continue; } + if (inflowVel[1] > real_c(0) && dir[1] < 0) { continue; } + if (inflowVel[2] > real_c(0) && dir[2] < 0) { continue; } + + // set conversion flag to remaining cells + field::addFlag(flagFieldIt, flagInfo.convertToInterfaceForInflowFlag); + } + } + return; + } + + // only interface cells are converted directly (except for cells near inflow boundaries, see above) + if (!field::isFlagSet(flagFieldIt, flagInfo.interfaceFlag)) { return; } + + // interface cell is empty and should be converted to gas + if (*fillFieldIt < -cellConversionThreshold) + { + if (field::isFlagSet(flagFieldIt, flagInfo.keepInterfaceForWettingFlag)) + { + field::removeFlag(flagFieldIt, flagInfo.keepInterfaceForWettingFlag); + } + + field::addFlag(flagFieldIt, flagInfo.convertToGasFlag); + return; + } + + // interface cell is full and should be converted to liquid + if (*fillFieldIt > real_c(1.0) + cellConversionThreshold) + { + if (field::isFlagSet(flagFieldIt, flagInfo.keepInterfaceForWettingFlag)) + { + field::removeFlag(flagFieldIt, flagInfo.keepInterfaceForWettingFlag); + } + + field::addFlag(flagFieldIt, flagInfo.convertToLiquidFlag); + return; + } + + // interface cell has no liquid neighbor and should be converted to gas (see dissertation of N. Thuerey, 2007 + // section 4.4) + if (!field::isFlagSet(neighborFlags, flagInfo.liquidFlag) && + !field::isFlagSet(flagFieldIt, flagInfo.keepInterfaceForWettingFlag) && + !field::isFlagSet(neighborFlags, flagInfo.inflowFlagMask)) + { + // interface cell is almost empty + if (*fillFieldIt < cellConversionForceThreshold && field::isFlagSet(neighborFlags, flagInfo.interfaceFlag)) + { + // mass is not necessarily lost as it can be distributed to a neighboring interface cell + field::addFlag(flagFieldIt, flagInfo.convertToGasFlag); + return; + } + + // interface cell has no other interface neighbors; conversion might lead to loss in mass (depending on the excess + // mass distribution model) + if (!field::isFlagSet(neighborFlags, flagInfo.interfaceFlag)) + { + field::addFlag(flagFieldIt, flagInfo.convertToGasFlag); + return; + } + } + + // interface cell has no gas neighbor and should be converted to liquid (see dissertation of N. Thuerey, 2007 + // section 4.4) + if (!field::isFlagSet(neighborFlags, flagInfo.gasFlag) && + !field::isFlagSet(flagFieldIt, flagInfo.keepInterfaceForWettingFlag)) + { + // interface cell is almost full + if (*fillFieldIt > real_c(1.0) - cellConversionForceThreshold && + field::isFlagSet(neighborFlags, flagInfo.interfaceFlag)) + { + // mass is not necessarily gained as it can be taken from a neighboring interface cell + field::addFlag(flagFieldIt, flagInfo.convertToLiquidFlag); + return; + } + + // interface cell has no other interface neighbors; conversion might lead to gain in mass (depending on the excess + // mass distribution model) + if (!field::isFlagSet(neighborFlags, flagInfo.interfaceFlag)) + { + field::addFlag(flagFieldIt, flagInfo.convertToLiquidFlag); + return; + } + } +} + +/*********************************************************************************************************************** + * Triggers findInterfaceCellConversion() for each cell of the given fields and recomputes the OR'ed flag neighborhood + * info. + **********************************************************************************************************************/ +template< typename LatticeModel_T, typename BoundaryHandling_T, typename ScalarField_T, typename FlagField_T > +void findInterfaceCellConversions(const BoundaryHandling_T& handling, const ScalarField_T* fillField, + FlagField_T* flagField, const FlagInfo< FlagField_T >& flagInfo, + real_t cellConversionThreshold, real_t cellConversionForceThreshold) +{ + WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ_OMP(flagField, uint_c(1), omp critical, { + const typename FlagField_T::Ptr flagFieldPtr(*flagField, x, y, z); + const typename ScalarField_T::ConstPtr fillFieldPtr(*fillField, x, y, z); + + if (field::isFlagSet(flagFieldPtr, flagInfo.interfaceFlag)) + { + const typename FlagField_T::value_type neighborFlags = + field::getOredNeighborhood< typename LatticeModel_T::Stencil >(flagFieldPtr); + + (findInterfaceCellConversion< LatticeModel_T, BoundaryHandling_T, ScalarField_T, + FlagField_T >) (handling, fillFieldPtr, flagFieldPtr, neighborFlags, flagInfo, + cellConversionThreshold, cellConversionForceThreshold); + } + }) // WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ +} + +/*********************************************************************************************************************** + * Triggers findInterfaceCellConversion() for each cell of the given fields using the cached OR'ed flag neighborhood + * given in neighField. + **********************************************************************************************************************/ +template< typename LatticeModel_T, typename BoundaryHandling_T, typename ScalarField_T, typename FlagField_T, + typename NeighField_T > +void findInterfaceCellConversions(const BoundaryHandling_T& handling, const ScalarField_T* fillField, + FlagField_T* flagField, const NeighField_T* neighField, + const FlagInfo< FlagField_T >& flagInfo, real_t cellConversionThreshold, + real_t cellConversionForceThreshold) +{ + WALBERLA_ASSERT_EQUAL_2(flagField->xyzSize(), fillField->xyzSize()); + WALBERLA_ASSERT_EQUAL_2(flagField->xyzSize(), neighField->xyzSize()); + + WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ_OMP(flagField, uint_c(1), omp critical, { + const typename FlagField_T::Ptr flagFieldPtr(*flagField, x, y, z); + const typename ScalarField_T::ConstPtr fillFieldPtr(*fillField, x, y, z); + + (findInterfaceCellConversion< LatticeModel_T, BoundaryHandling_T, ScalarField_T, + FlagField_T >) (handling, fillFieldPtr, flagFieldPtr, neighField->get(x, y, z), + flagInfo, cellConversionThreshold, cellConversionForceThreshold); + }) // WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ_OMP +} + +} // namespace free_surface +} // namespace walberla diff --git a/src/lbm/free_surface/dynamics/functionality/GetLaplacePressure.h b/src/lbm/free_surface/dynamics/functionality/GetLaplacePressure.h new file mode 100644 index 000000000..36c313ee2 --- /dev/null +++ b/src/lbm/free_surface/dynamics/functionality/GetLaplacePressure.h @@ -0,0 +1,61 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file GetLaplacePressure.h +//! \ingroup dynamics +//! \author Matthias Markl <matthias.markl@fau.de> +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Compute difference in density due to Laplace pressure. +// +//====================================================================================================================== + +#pragma once + +#include "core/DataTypes.h" +#include "core/logging/Logging.h" + +namespace walberla +{ +namespace free_surface +{ +/*********************************************************************************************************************** + * Compute difference in density due to Laplace pressure. + **********************************************************************************************************************/ +inline real_t computeDeltaRhoLaplacePressure(real_t sigma, real_t curvature, real_t maxDeltaRho = real_c(0.1)) +{ + static const real_t inv_cs2 = real_c(3); // 1.0 / (cs * cs) + const real_t laplacePressure = real_c(2) * sigma * curvature; + real_t deltaRho = inv_cs2 * laplacePressure; + + if (deltaRho > maxDeltaRho) + { + WALBERLA_LOG_WARNING("Too large density variation of " << deltaRho << " due to laplacePressure " + << laplacePressure << " with curvature " << curvature + << ". Will be limited to " << maxDeltaRho << ".\n"); + deltaRho = maxDeltaRho; + } + if (deltaRho < -maxDeltaRho) + { + WALBERLA_LOG_WARNING("Too large density variation of " << deltaRho << " due to laplacePressure " + << laplacePressure << " with curvature " << curvature + << ". Will be limited to " << -maxDeltaRho << ".\n"); + deltaRho = -maxDeltaRho; + } + + return deltaRho; +} + +} // namespace free_surface +} // namespace walberla diff --git a/src/lbm/free_surface/dynamics/functionality/GetOredNeighborhood.h b/src/lbm/free_surface/dynamics/functionality/GetOredNeighborhood.h new file mode 100644 index 000000000..187528a6c --- /dev/null +++ b/src/lbm/free_surface/dynamics/functionality/GetOredNeighborhood.h @@ -0,0 +1,62 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file GetOredNeighborhood.h +//! \ingroup dynamics +//! \author Martin Bauer +//! \author Matthias Markl <matthias.markl@fau.de> +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Combines the flags of all neighboring cells using bitwise OR. +// +//====================================================================================================================== + +#pragma once + +#include "core/debug/Debug.h" +#include "core/timing/TimingPool.h" + +#include "field/FlagField.h" + +#include <type_traits> + +namespace walberla +{ +namespace free_surface +{ +/*********************************************************************************************************************** + * Combines the flags of all neighboring cells using bitwise OR. + * Flags are read from flagField and stored in neighborhoodFlagField. Every cell contains the bitwise OR of the + * neighboring flags in flagField. + **********************************************************************************************************************/ +template< typename Stencil_T, typename FlagField_T > +void getOredNeighborhood(const FlagField_T* flagField, FlagField_T* neighborhoodFlagField) +{ + WALBERLA_ASSERT_GREATER_EQUAL(flagField->nrOfGhostLayers(), 2); + WALBERLA_ASSERT_EQUAL(neighborhoodFlagField->xyzSize(), flagField->xyzSize()); + WALBERLA_ASSERT_EQUAL(neighborhoodFlagField->xyzAllocSize(), flagField->xyzAllocSize()); + + // REMARK: here is the reason why the flag field MUST have two ghost layers; + // the "OredNeighborhood" of the first ghost layer is determined, such that the first ghost layer's neighbors (i.e., + // the second ghost layer) must be available + WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ(flagField, uint_c(1), { + const typename FlagField_T::ConstPtr flagFieldPtr(*flagField, x, y, z); + const typename FlagField_T::Ptr neighborhoodFlagFieldPtr(*neighborhoodFlagField, x, y, z); + + *neighborhoodFlagFieldPtr = field::getOredNeighborhood< Stencil_T >(flagFieldPtr); + }) // WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOSTLAYER_XYZ +} + +} // namespace free_surface +} // namespace walberla diff --git a/src/lbm/free_surface/dynamics/functionality/ReconstructInterfaceCellABB.h b/src/lbm/free_surface/dynamics/functionality/ReconstructInterfaceCellABB.h new file mode 100644 index 000000000..b0f45113c --- /dev/null +++ b/src/lbm/free_surface/dynamics/functionality/ReconstructInterfaceCellABB.h @@ -0,0 +1,442 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file ReconstructInterfaceCellABB.h +//! \ingroup dynamics +//! \author Martin Bauer +//! \author Matthias Markl <matthias.markl@fau.de> +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Free surface boundary condition as in Koerner et al., 2005. Similar to anti-bounce-back pressure condition. +// +//====================================================================================================================== + +#pragma once + +#include "lbm/field/MacroscopicValueCalculation.h" +#include "lbm/field/PdfField.h" +#include "lbm/free_surface/dynamics/PdfReconstructionModel.h" +#include "lbm/lattice_model/EquilibriumDistribution.h" + +#include "stencil/Directions.h" + +#include "GetLaplacePressure.h" + +namespace walberla +{ +namespace free_surface +{ +// get index of largest entry in n_dot_ci with isInterfaceOrLiquid==true && isPdfAvailable==false +uint_t getIndexOfMaximum(const std::vector< bool >& isInterfaceOrLiquid, const std::vector< bool >& isPdfAvailable, + const std::vector< real_t >& n_dot_ci); + +// get index of smallest entry in n_dot_ci with isInterfaceOrLiquid==true && isPdfAvailable==false +uint_t getIndexOfMinimum(const std::vector< bool >& isInterfaceOrLiquid, const std::vector< bool >& isPdfAvailable, + const std::vector< real_t >& n_dot_ci); + +// reconstruct PDFs according to pressure anti bounce back boundary condition (page 31, equation 4.5 in dissertation of +// N. Thuerey, 2007) +template< typename LatticeModel_T, typename ConstPdfIt_T > +inline real_t reconstructPressureAntiBounceBack(const stencil::Iterator< typename LatticeModel_T::Stencil >& dir, + const ConstPdfIt_T& pdfFieldIt, const Vector3< real_t >& u, + real_t rhoGas, real_t dir_independent) +{ + const real_t vel = real_c(dir.cx()) * u[0] + real_c(dir.cy()) * u[1] + real_c(dir.cz()) * u[2]; + + // compute f^{eq}_i + f^{eq}_{\overline{i}} using rhoGas (without linear terms as they cancel out) + const real_t tmp = + real_c(2.0) * LatticeModel_T::w[dir.toIdx()] * rhoGas * (dir_independent + real_c(4.5) * vel * vel); + + // reconstruct PDFs (page 31, equation 4.5 in dissertation of N. Thuerey, 2007) + return tmp - pdfFieldIt.getF(dir.toIdx()); +} + +/*********************************************************************************************************************** + * Free surface boundary condition as described in the publication of Koerner et al., 2005. Missing PDFs are + * reconstructed according to an anti-bounce-back pressure boundary condition at the free surface. + * + * Be aware that in Koerner et al., 2005, the PDFs are reconstructed in gas cells (neighboring to interface cells). + * These PDFs are then streamed into the interface cells in the LBM stream. Here, we directly reconstruct the PDFs in + * interface cells such that our implementation follows the notation in the dissertation of N. Thuerey, 2007, page 31. + * ********************************************************************************************************************/ +template< typename LatticeModel_T, typename FlagField_T, typename ConstPdfIt_T, typename ConstFlagIt_T, + typename ConstVectorIt_T, typename OutputArray_T > +void reconstructInterfaceCellLegacy(const FlagField_T* flagField, const ConstPdfIt_T& pdfFieldIt, + const ConstFlagIt_T& flagFieldIt, const ConstVectorIt_T& normalFieldIt, + const FlagInfo< FlagField_T >& flagInfo, const real_t rhoGas, OutputArray_T& f, + const PdfReconstructionModel& pdfReconstructionModel) +{ + using Stencil_T = typename LatticeModel_T::Stencil; + + // get velocity and density in interface cell + Vector3< real_t > u; + auto pdfField = dynamic_cast< const lbm::PdfField< LatticeModel_T >* >(pdfFieldIt.getField()); + WALBERLA_ASSERT_NOT_NULLPTR(pdfField); + const real_t rho = lbm::getDensityAndMomentumDensity(u, pdfField->latticeModel(), pdfFieldIt); + u /= rho; + + const real_t dir_independent = + real_c(1.0) - real_c(1.5) * u.sqrLength(); // direction independent value used for PDF reconstruction + + // get type of the model that determines the PDF reconstruction + const PdfReconstructionModel::ReconstructionModel reconstructionModel = pdfReconstructionModel.getModelType(); + + // vector that stores the dot product between interface normal and lattice direction for each lattice direction + std::vector< real_t > n_dot_ci; + + // vector that stores which index from loop "for (auto dir = Stencil_T::beginNoCenter(); dir != Stencil_T::end(); + // ++dir)" is currently available, i.e.: + // - reconstructed (or scheduled for reconstruction) + // - coming from boundary condition + // - available due to fluid or interface neighbor + std::vector< bool > isPdfAvailable; + + // vector that stores which index from loop "for (auto dir = Stencil_T::beginNoCenter(); dir != Stencil_T::end(); + // ++dir)" points to a neighboring interface or fluid cell + std::vector< bool > isInterfaceOrLiquid; + + // count number of reconstructed links + uint_t numReconstructed = uint_c(0); + + for (auto dir = Stencil_T::begin(); dir != Stencil_T::end(); ++dir) + { + const auto neighborFlag = flagFieldIt.neighbor(*dir); + + if (flagInfo.isObstacle(neighborFlag)) + { + // free slip boundaries need special treatment because PDFs traveling from gas cells into the free slip + // boundary must be reconstructed, for instance: + // [I][G] with I: interface cell; G: gas cell; f: free slip cell + // [f][f] + // During streaming, the interface cell's PDF with direction (-1, 1) is coming from the right free slip cell. + // For a free slip boundary, this PDF is identical to the PDF with direction (-1, -1) in the gas cell. Since + // gas-side PDFs are not available, such PDFs must be reconstructed. + // Non-gas cells do not need to be treated here as they are treated correctly by the boundary handling. + + if (flagInfo.isFreeSlip(neighborFlag)) + { + // REMARK: the following implementation is based on lbm/boundary/FreeSlip.h + + // get components of inverse direction of dir + const int ix = stencil::cx[stencil::inverseDir[*dir]]; + const int iy = stencil::cy[stencil::inverseDir[*dir]]; + const int iz = stencil::cz[stencil::inverseDir[*dir]]; + + int wnx = 0; // compute "normal" vector of free slip wall + int wny = 0; + int wnz = 0; + + // from the current cell, go into neighboring cell in direction dir and from there check whether the + // neighbors in ix, iy and iz are gas cells + const auto flagFieldFreeSlipPtrX = typename FlagField_T::ConstPtr( + *flagField, flagFieldIt.x() + dir.cx() + ix, flagFieldIt.y() + dir.cy(), flagFieldIt.z() + dir.cz()); + if (flagInfo.isGas(*flagFieldFreeSlipPtrX)) { wnx = ix; } + + const auto flagFieldFreeSlipPtrY = typename FlagField_T::ConstPtr( + *flagField, flagFieldIt.x() + dir.cx(), flagFieldIt.y() + dir.cy() + iy, flagFieldIt.z() + dir.cz()); + if (flagInfo.isGas(*flagFieldFreeSlipPtrY)) { wny = iy; } + + const auto flagFieldFreeSlipPtrZ = typename FlagField_T::ConstPtr( + *flagField, flagFieldIt.x() + dir.cx(), flagFieldIt.y() + dir.cy(), flagFieldIt.z() + dir.cz() + iz); + if (flagInfo.isGas(*flagFieldFreeSlipPtrZ)) { wnz = iz; } + + if (wnx != 0 || wny != 0 || wnz != 0) + { + // boundaryNeighbor denotes the cell from which the PDF is coming from + const auto flagFieldFreeSlipPtr = + typename FlagField_T::ConstPtr(*flagField, flagFieldIt.x() + dir.cx() + wnx, + flagFieldIt.y() + dir.cy() + wny, flagFieldIt.z() + dir.cz() + wnz); + + if (flagInfo.isGas(*flagFieldFreeSlipPtr)) + { + // reconstruct PDF + f[dir.toInvIdx()] = reconstructPressureAntiBounceBack< LatticeModel_T, ConstPdfIt_T >( + dir, pdfFieldIt, u, rhoGas, dir_independent); + isPdfAvailable.push_back(true); + isInterfaceOrLiquid.push_back(false); + n_dot_ci.push_back( + real_c(0)); // dummy entry for having index as in vectors isPDFAvailable and isInterfaceOrLiquid + ++numReconstructed; + continue; + } + // else: do nothing here, i.e., make usual obstacle boundary treatment below + } // else: concave corner, all surrounding PDFs are known, i.e., make usual obstacle boundary treatment + // below + } + + f[dir.toInvIdx()] = pdfFieldIt.neighbor(*dir, dir.toInvIdx()); // use PDFs defined by boundary handling + isPdfAvailable.push_back(true); + isInterfaceOrLiquid.push_back(false); + n_dot_ci.push_back( + real_c(0)); // dummy entry for having same indices as in vectors isPDFAvailable and isInterfaceOrLiquid + continue; + } + else + { + if (flagInfo.isGas(neighborFlag)) + { + f[dir.toInvIdx()] = reconstructPressureAntiBounceBack< LatticeModel_T, ConstPdfIt_T >( + dir, pdfFieldIt, u, rhoGas, dir_independent); + isPdfAvailable.push_back(true); + isInterfaceOrLiquid.push_back(false); + n_dot_ci.push_back( + real_c(0)); // dummy entry for having index as in vectors isPDFAvailable and isInterfaceOrLiquid + ++numReconstructed; + continue; + } + } + + // dot product between interface normal and lattice direction + real_t dotProduct = (*normalFieldIt)[0] * real_c(dir.cx()) + (*normalFieldIt)[1] * real_c(dir.cy()) + + (*normalFieldIt)[2] * real_c(dir.cz()); + + // avoid numerical inaccuracies in the computation of the scalar product n_dot_ci + if (realIsEqual(dotProduct, real_c(0), real_c(1e-14))) { dotProduct = real_c(0); } + + // approach from Koerner; reconstruct all PDFs in direction opposite to interface normal and center PDF (not + // stated in the paper explicitly but follows from n*e_i>=0 for i=0) + if (reconstructionModel == PdfReconstructionModel::ReconstructionModel::NormalBasedReconstructCenter) + { + if (dotProduct >= real_c(0)) + { + f[dir.toInvIdx()] = reconstructPressureAntiBounceBack< LatticeModel_T, ConstPdfIt_T >( + dir, pdfFieldIt, u, rhoGas, dir_independent); + } + else + { + // regular LBM stream with PDFs from neighbor + f[dir.toInvIdx()] = pdfFieldIt.neighbor(*dir, dir.toInvIdx()); + } + continue; + } + + if (reconstructionModel == PdfReconstructionModel::ReconstructionModel::NormalBasedKeepCenter) + { + if (*dir == stencil::C) + { + // use old center PDF + f[Stencil_T::idx[stencil::C]] = pdfFieldIt[Stencil_T::idx[stencil::C]]; + } + else + { + if (dotProduct >= real_c(0)) + { + f[dir.toInvIdx()] = reconstructPressureAntiBounceBack< LatticeModel_T, ConstPdfIt_T >( + dir, pdfFieldIt, u, rhoGas, dir_independent); + } + else + { + // regular LBM stream with PDFs from neighbor + f[dir.toInvIdx()] = pdfFieldIt.neighbor(*dir, dir.toInvIdx()); + } + } + continue; + } + + // reconstruct all non-obstacle PDFs, including those that come from liquid and are already known + if (reconstructionModel == PdfReconstructionModel::ReconstructionModel::All) + { + f[dir.toInvIdx()] = reconstructPressureAntiBounceBack< LatticeModel_T, ConstPdfIt_T >(dir, pdfFieldIt, u, + rhoGas, dir_independent); + continue; + } + + // reconstruct only those gas-side PDFs that are really missing + if (reconstructionModel == PdfReconstructionModel::ReconstructionModel::OnlyMissing) + { + // regular LBM stream with PDFs from neighboring interface or liquid cell + f[dir.toInvIdx()] = pdfFieldIt.neighbor(*dir, dir.toInvIdx()); + continue; + } + + // reconstruct only those gas-side PDFs that are really missing but make sure that at least a + // specified number of PDFs are reconstructed (even if available PDFs are overwritten) + if (reconstructionModel == PdfReconstructionModel::ReconstructionModel::OnlyMissingMin) + { + isPdfAvailable.push_back(false); // PDF has not yet been marked for reconstruction + isInterfaceOrLiquid.push_back(true); + n_dot_ci.push_back(dotProduct); + continue; + } + + WALBERLA_ABORT("Unknown pdfReconstructionModel.") + } + + if (reconstructionModel == PdfReconstructionModel::ReconstructionModel::OnlyMissingMin) + { + WALBERLA_ASSERT_EQUAL(Stencil_T::Size, uint_c(n_dot_ci.size())); + WALBERLA_ASSERT_EQUAL(Stencil_T::Size, uint_c(isInterfaceOrLiquid.size())); + WALBERLA_ASSERT_EQUAL(Stencil_T::Size, uint_c(isPdfAvailable.size())); + + const uint_t numMinReconstruct = pdfReconstructionModel.getNumMinReconstruct(); + + // number of remaining PDFs that need to be reconstructed according to the specified model (number can be negative + // => do not use uint_t) + int numRemainingReconstruct = int_c(numMinReconstruct) - int_c(numReconstructed); + + // count the number of neighboring cells that are interface or liquid (and not obstacle or gas) + const uint_t numLiquidNeighbors = + uint_c(std::count_if(isInterfaceOrLiquid.begin(), isInterfaceOrLiquid.end(), [](bool a) { return a; })); + + // // REMARK: this was commented because it regularly occurred in practical simulations (e.g. BreakingDam) + // if (numRemainingReconstruct > int_c(0) && numRemainingReconstruct < int_c(numLiquidNeighbors)) + // { + // // There are less neighboring liquid and interface cells than needed to reconstruct PDFs in the + // // free surface boundary condition. You have probably specified a large minimum number of PDFs to be + // // reconstructed. This happens e.g. near solid boundaries (especially corners). There, the number of + // // surrounding non-obstacle cells might be less than the number of PDFs that you want to have + // // reconstructed. + // WALBERLA_LOG_WARNING_ON_ROOT("Less PDFs reconstructed in cell " + // << pdfFieldIt.cell() + // << " than specified in the PDF reconstruction model. See comment in " + // "source code of ReconstructInterfaceCellABB.h for further information. " + // "Here, as many PDFs as possible are reconstructed now."); + // } + + // count additionally reconstructed PDFs (that come from interface or liquid) + uint_t numAdditionalReconstructed = uint_c(0); + + // define which PDFs to additionally reconstruct (overwrite known information) according to the specified model + while (numRemainingReconstruct > int_c(0) && numAdditionalReconstructed < numLiquidNeighbors) + { + if (pdfReconstructionModel.getFallbackModel() == PdfReconstructionModel::FallbackModel::Largest) + { + // get index of largest n_dot_ci with isInterfaceOrLiquid==true && isPdfAvailable==false + const uint_t maxIndex = getIndexOfMaximum(isInterfaceOrLiquid, isPdfAvailable, n_dot_ci); + isPdfAvailable[maxIndex] = true; // reconstruct this PDF later + ++numReconstructed; + ++numAdditionalReconstructed; + } + else + { + if (pdfReconstructionModel.getFallbackModel() == PdfReconstructionModel::FallbackModel::Smallest) + { + // get index of smallest n_dot_ci with isInterfaceOrLiquid==true && isPdfAvailable==false + const uint_t minIndex = getIndexOfMinimum(isInterfaceOrLiquid, isPdfAvailable, n_dot_ci); + isPdfAvailable[minIndex] = true; // reconstruct this PDF later + ++numReconstructed; + ++numAdditionalReconstructed; + } + else + { + // use approach from Koerner + if (pdfReconstructionModel.getFallbackModel() == + PdfReconstructionModel::FallbackModel::NormalBasedKeepCenter) + { + uint_t index = uint_c(0); + for (const real_t& value : n_dot_ci) + { + if (value >= real_c(0) && index > uint_c(1)) // skip center PDF with index=0 + { + isPdfAvailable[index] = true; // reconstruct this PDF later + } + + ++index; + } + break; // exit while loop + } + else { WALBERLA_ABORT("Unknown fallbackModel in pdfReconstructionModel.") } + } + } + + numRemainingReconstruct = int_c(numMinReconstruct) - int_c(numReconstructed); + } + + // reconstruct additional PDFs + uint_t index = uint_c(0); + for (auto dir = Stencil_T::begin(); dir != Stencil_T::end(); ++dir, ++index) + { + const auto neighborFlag = flagFieldIt.neighbor(*dir); + + // skip links pointing to obstacle and gas neighbors; they were treated above already + if (flagInfo.isObstacle(neighborFlag) || flagInfo.isGas(neighborFlag)) { continue; } + else + { + // reconstruct links that were marked for reconstruction + if (isPdfAvailable[index] && isInterfaceOrLiquid[index]) + { + f[dir.toInvIdx()] = reconstructPressureAntiBounceBack< LatticeModel_T, ConstPdfIt_T >( + dir, pdfFieldIt, u, rhoGas, dir_independent); + } + else + { + if (!isPdfAvailable[index] && isInterfaceOrLiquid[index]) + { + // regular LBM stream with PDFs from neighbor + f[dir.toInvIdx()] = pdfFieldIt.neighbor(*dir, dir.toInvIdx()); + continue; + } + WALBERLA_ABORT("Error in PDF reconstruction. This point should never be reached.") + } + } + } + } +} + +uint_t getIndexOfMaximum(const std::vector< bool >& isInterfaceOrLiquid, const std::vector< bool >& isPdfAvailable, + const std::vector< real_t >& n_dot_ci) +{ + real_t maximum = -std::numeric_limits< real_t >::max(); + uint_t index = std::numeric_limits< uint_t >::max(); + + for (uint_t i = uint_c(0); i != isInterfaceOrLiquid.size(); ++i) + { + if (isInterfaceOrLiquid[i] && !isPdfAvailable[i]) + { + const real_t absValue = std::abs(n_dot_ci[i]); + if (absValue > maximum) + { + maximum = absValue; + index = i; + } + } + } + + // less Pdfs available for being reconstructed than specified by the user; these assertions should never fail, as the + // conditionals in reconstructInterfaceCellLegacy() should avoid calling this function + WALBERLA_ASSERT(maximum > -real_c(std::numeric_limits< real_t >::min())); + WALBERLA_ASSERT(index != std::numeric_limits< uint_t >::max()); + + return index; +} + +uint_t getIndexOfMinimum(const std::vector< bool >& isInterfaceOrLiquid, const std::vector< bool >& isPdfAvailable, + const std::vector< real_t >& n_dot_ci) +{ + real_t minimum = std::numeric_limits< real_t >::max(); + uint_t index = std::numeric_limits< uint_t >::max(); + + for (uint_t i = uint_c(0); i != isInterfaceOrLiquid.size(); ++i) + { + if (isInterfaceOrLiquid[i] && !isPdfAvailable[i]) + { + const real_t absValue = std::abs(n_dot_ci[i]); + if (absValue < minimum) + { + minimum = absValue; + index = i; + } + } + } + + // fewer PDFs available for being reconstructed than specified by the user; these assertions should never fail, as + // the conditionals in reconstructInterfaceCellLegacy() should avoid calling this function + WALBERLA_ASSERT(minimum < real_c(std::numeric_limits< real_t >::max())); + WALBERLA_ASSERT(index != std::numeric_limits< uint_t >::max()); + + return index; +} + +} // namespace free_surface +} // namespace walberla diff --git a/src/lbm/free_surface/surface_geometry/CMakeLists.txt b/src/lbm/free_surface/surface_geometry/CMakeLists.txt new file mode 100644 index 000000000..42fb177b7 --- /dev/null +++ b/src/lbm/free_surface/surface_geometry/CMakeLists.txt @@ -0,0 +1,22 @@ +target_sources( lbm + PRIVATE + ContactAngle.h + CurvatureModel.h + CurvatureModel.impl.h + CurvatureSweep.h + CurvatureSweep.impl.h + DetectWettingSweep.h + ExtrapolateNormalsSweep.h + ExtrapolateNormalsSweep.impl.h + NormalSweep.h + NormalSweep.impl.h + ObstacleFillLevelSweep.h + ObstacleFillLevelSweep.impl.h + ObstacleNormalSweep.h + ObstacleNormalSweep.impl.h + SmoothingSweep.h + SmoothingSweep.impl.h + SurfaceGeometryHandler.h + Utility.cpp + Utility.h + ) diff --git a/src/lbm/free_surface/surface_geometry/ContactAngle.h b/src/lbm/free_surface/surface_geometry/ContactAngle.h new file mode 100644 index 000000000..55f26d5ce --- /dev/null +++ b/src/lbm/free_surface/surface_geometry/ContactAngle.h @@ -0,0 +1,54 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file ContactAngle.h +//! \ingroup surface_geometry +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Class to avoid re-computing sine and cosine of the contact angle. +// +//====================================================================================================================== + +#pragma once + +#include "core/math/Constants.h" + +namespace walberla +{ +namespace free_surface +{ +/*********************************************************************************************************************** + * Class to avoid re-computing sine and cosine of the contact angle. + **********************************************************************************************************************/ +class ContactAngle +{ + public: + ContactAngle(real_t angleInDegrees) + : angleDegrees_(angleInDegrees), angleRadians_(math::pi / real_c(180) * angleInDegrees), + sinAngle_(std::sin(angleRadians_)), cosAngle_(std::cos(angleRadians_)) + {} + + inline real_t getInDegrees() const { return angleDegrees_; } + inline real_t getInRadians() const { return angleRadians_; } + inline real_t getSin() const { return sinAngle_; } + inline real_t getCos() const { return cosAngle_; } + + private: + real_t angleDegrees_; + real_t angleRadians_; + real_t sinAngle_; + real_t cosAngle_; +}; // class ContactAngle +} // namespace free_surface +} // namespace walberla \ No newline at end of file diff --git a/src/lbm/free_surface/surface_geometry/CurvatureModel.h b/src/lbm/free_surface/surface_geometry/CurvatureModel.h new file mode 100644 index 000000000..dd245dacf --- /dev/null +++ b/src/lbm/free_surface/surface_geometry/CurvatureModel.h @@ -0,0 +1,90 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file CurvatureModel.h +//! \ingroup surface_geometry +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Collection of sweeps required for using a specific curvature model. +// +//====================================================================================================================== + +#pragma once + +namespace walberla +{ +namespace free_surface +{ +// forward declaration +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T > +class SurfaceGeometryHandler; + +namespace curvature_model +{ +/*********************************************************************************************************************** + * Collection of sweeps for computing the curvature using a finite difference-based (Parker-Youngs) approach according + * to: + * dissertation of S. Bogner, 2017 (section 4.4.2.1) + **********************************************************************************************************************/ +template< typename Stencil_T, typename LatticeModel_T, typename FlagField_T, typename ScalarField_T, + typename VectorField_T > +class FiniteDifferenceMethod +{ + private: + using SurfaceGeometryHandler_T = + walberla::free_surface::SurfaceGeometryHandler< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T >; + + public: + void addSweeps(SweepTimeloop& timeloop, const SurfaceGeometryHandler_T& geometryHandler); +}; // class FiniteDifferenceMethod + +/*********************************************************************************************************************** + * Collection of sweeps for computing the curvature using local triangulation according to: + * - dissertation of T. Pohl, 2008 (section 2.5) + * - dissertation of S. Donath, 2011 (wetting model, section 6.3.3) + **********************************************************************************************************************/ +template< typename Stencil_T, typename LatticeModel_T, typename FlagField_T, typename ScalarField_T, + typename VectorField_T > +class LocalTriangulation +{ + private: + using SurfaceGeometryHandler_T = + walberla::free_surface::SurfaceGeometryHandler< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T >; + + public: + void addSweeps(SweepTimeloop& timeloop, const SurfaceGeometryHandler_T& geometryHandler); +}; // class LocalTriangulation + +/*********************************************************************************************************************** + * Collection of sweeps for computing the curvature with a simplistic finite difference method. This approach is not + * documented in literature and neither thoroughly tested or validated. + * Use it with caution and preferably for testing purposes only. + **********************************************************************************************************************/ +template< typename Stencil_T, typename LatticeModel_T, typename FlagField_T, typename ScalarField_T, + typename VectorField_T > +class SimpleFiniteDifferenceMethod +{ + private: + using SurfaceGeometryHandler_T = + walberla::free_surface::SurfaceGeometryHandler< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T >; + + public: + void addSweeps(SweepTimeloop& timeloop, const SurfaceGeometryHandler_T& geometryHandler); +}; // class SimpleFiniteDifferenceMethod + +} // namespace curvature_model +} // namespace free_surface +} // namespace walberla + +#include "CurvatureModel.impl.h" \ No newline at end of file diff --git a/src/lbm/free_surface/surface_geometry/CurvatureModel.impl.h b/src/lbm/free_surface/surface_geometry/CurvatureModel.impl.h new file mode 100644 index 000000000..0aa62fc3c --- /dev/null +++ b/src/lbm/free_surface/surface_geometry/CurvatureModel.impl.h @@ -0,0 +1,269 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file CurvatureModel.impl.h +//! \ingroup surface_geometry +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Collection of sweeps required for using a specific curvature model. +// +//====================================================================================================================== + +#include "lbm/blockforest/communication/UpdateSecondGhostLayer.h" + +#include "CurvatureModel.h" +#include "CurvatureSweep.h" +#include "DetectWettingSweep.h" +#include "ExtrapolateNormalsSweep.h" +#include "NormalSweep.h" +#include "ObstacleFillLevelSweep.h" +#include "ObstacleNormalSweep.h" +#include "SmoothingSweep.h" +#include "SurfaceGeometryHandler.h" + +namespace walberla +{ +namespace free_surface +{ +namespace curvature_model +{ +// empty sweep required for using selectors (e.g. StateSweep::fullFreeSurface) +struct emptySweep +{ + void operator()(IBlock*) {} +}; + +template< typename Stencil_T, typename LatticeModel_T, typename FlagField_T, typename ScalarField_T, + typename VectorField_T > +void FiniteDifferenceMethod< Stencil_T, LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T >::addSweeps( + SweepTimeloop& timeloop, const FiniteDifferenceMethod::SurfaceGeometryHandler_T& geometryHandler) +{ + using Communication_T = typename SurfaceGeometryHandler_T::Communication_T; + using StateSweep = typename SurfaceGeometryHandler_T::StateSweep; + + // layout for allocating the smoothed fill level field + field::Layout fillFieldLayout = field::fzyx; + + // check if an obstacle cell is in a non-periodic outermost global ghost layer; used to check if two ghost layers are + // required for the fill level field + const Vector3< bool > isObstacleInGlobalGhostLayerXYZ = + geometryHandler.freeSurfaceBoundaryHandling_->isObstacleInGlobalGhostLayer(); + + bool isObstacleInGlobalGhostLayer = false; + if ((!geometryHandler.blockForest_->isXPeriodic() && isObstacleInGlobalGhostLayerXYZ[0]) || + (!geometryHandler.blockForest_->isYPeriodic() && isObstacleInGlobalGhostLayerXYZ[1]) || + (!geometryHandler.blockForest_->isZPeriodic() && isObstacleInGlobalGhostLayerXYZ[2])) + { + isObstacleInGlobalGhostLayer = true; + } + + for (auto blockIt = geometryHandler.blockForest_->begin(); blockIt != geometryHandler.blockForest_->end(); ++blockIt) + { + const ScalarField_T* const fillField = + blockIt->template getData< const ScalarField_T >(geometryHandler.fillFieldID_); + + // check if two ghost layers are required for the fill level field + if (isObstacleInGlobalGhostLayer && fillField->nrOfGhostLayers() < uint_c(2) && geometryHandler.enableWetting_) + { + WALBERLA_ABORT( + "With wetting enabled, the curvature computation with the finite difference method requires two ghost " + "layers in the fill level field whenever solid obstacles are located in a global outermost ghost layer. " + "For more information, see the remark in the description of ObstacleFillLevelSweep.h"); + } + + // get layout of fill level field (to be used in allocating the smoothed fill level field; cloning would + // waste memory, as the fill level field might have two ghost layers, whereas the smoothed fill level field needs + // only one ghost layer) + fillFieldLayout = fillField->layout(); + } + + // IMPORTANT REMARK: ObstacleNormalSweep and ObstacleFillLevelSweep must be executed on all blocks, because the + // SmoothingSweep requires meaningful values in the ghost layers. + + // add field for smoothed fill levels + BlockDataID smoothFillFieldID = field::addToStorage< ScalarField_T >( + geometryHandler.blockForest_, "Smooth fill level field", real_c(0), fillFieldLayout, uint_c(1)); + + if (geometryHandler.enableWetting_) + { + // compute obstacle normals + ObstacleNormalSweep< Stencil_T, FlagField_T, VectorField_T > obstacleNormalSweep( + geometryHandler.obstacleNormalFieldID_, geometryHandler.flagFieldID_, flagIDs::interfaceFlagID, + flagIDs::liquidInterfaceGasFlagIDs, geometryHandler.obstacleFlagIDSet_, false, true, true); + timeloop.add() << Sweep(obstacleNormalSweep, "Sweep: obstacle normal computation") + << AfterFunction( + Communication_T(geometryHandler.blockForest_, geometryHandler.obstacleNormalFieldID_), + "Communication: after obstacle normal sweep"); + + // reflect fill level into obstacle cells such that they can be used for smoothing the fill level field and for + // computing the interface normal; MUST be performed BEFORE SmoothingSweep + ObstacleFillLevelSweep< Stencil_T, FlagField_T, ScalarField_T, VectorField_T > obstacleFillLevelSweep( + smoothFillFieldID, geometryHandler.fillFieldID_, geometryHandler.flagFieldID_, + geometryHandler.obstacleNormalFieldID_, flagIDs::liquidInterfaceGasFlagIDs, + geometryHandler.obstacleFlagIDSet_); + timeloop.add() << Sweep(obstacleFillLevelSweep, "Sweep: obstacle fill level computation") + << AfterFunction(Communication_T(geometryHandler.blockForest_, smoothFillFieldID), + "Communication: after obstacle fill level sweep"); + } + + // smooth fill level field for decreasing error in finite difference normal and curvature computation (see + // dissertation of S. Bogner, 2017 (section 4.4.2.1)) + SmoothingSweep< Stencil_T, FlagField_T, ScalarField_T, VectorField_T > smoothingSweep( + smoothFillFieldID, geometryHandler.fillFieldID_, geometryHandler.flagFieldID_, flagIDs::liquidInterfaceGasFlagIDs, + geometryHandler.obstacleFlagIDSet_, geometryHandler.enableWetting_); + // IMPORTANT REMARK: SmoothingSweep must be executed on all blocks, because the algorithm works on all liquid, + // interface and gas cells. This is necessary since the normals are not only computed in interface cells, but also in + // the neighborhood of interface cells. Therefore, meaningful values for the fill levels of the second neighbors of + // interface cells are also required in NormalSweep. + timeloop.add() << Sweep(smoothingSweep, "Sweep: fill level smoothing") + << AfterFunction(Communication_T(geometryHandler.blockForest_, smoothFillFieldID), + "Communication: after smoothing sweep"); + + // compute interface normals (using smoothed fill level field) + NormalSweep< Stencil_T, FlagField_T, ScalarField_T, VectorField_T > normalSweep( + geometryHandler.normalFieldID_, smoothFillFieldID, geometryHandler.flagFieldID_, flagIDs::interfaceFlagID, + flagIDs::liquidInterfaceGasFlagIDs, geometryHandler.obstacleFlagIDSet_, true, geometryHandler.enableWetting_, + true, geometryHandler.enableWetting_); + timeloop.add() << Sweep(normalSweep, "Sweep: normal computation", StateSweep::fullFreeSurface) + << Sweep(emptySweep(), "Empty sweep: normal") + << AfterFunction(Communication_T(geometryHandler.blockForest_, geometryHandler.normalFieldID_), + "Communication: after normal sweep"); + + if (geometryHandler.computeCurvature_) + { + // compute interface curvature using finite differences according to Brackbill et al. + CurvatureSweepFiniteDifferences< Stencil_T, FlagField_T, ScalarField_T, VectorField_T > curvSweep( + geometryHandler.curvatureFieldID_, geometryHandler.normalFieldID_, geometryHandler.obstacleNormalFieldID_, + geometryHandler.flagFieldID_, flagIDs::interfaceFlagID, flagIDs::liquidInterfaceGasFlagIDs, + geometryHandler.obstacleFlagIDSet_, geometryHandler.enableWetting_, geometryHandler.contactAngle_); + timeloop.add() << Sweep(curvSweep, "Sweep: curvature computation (finite difference method)", + StateSweep::fullFreeSurface) + << Sweep(emptySweep(), "Empty sweep: curvature") + << AfterFunction(Communication_T(geometryHandler.blockForest_, geometryHandler.curvatureFieldID_), + "Communication: after curvature sweep"); + } +} + +template< typename Stencil_T, typename LatticeModel_T, typename FlagField_T, typename ScalarField_T, + typename VectorField_T > +void LocalTriangulation< Stencil_T, LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T >::addSweeps( + SweepTimeloop& timeloop, const LocalTriangulation::SurfaceGeometryHandler_T& geometryHandler) +{ + using Communication_T = typename SurfaceGeometryHandler_T::Communication_T; + using StateSweep = typename SurfaceGeometryHandler_T::StateSweep; + + // compute interface normals + NormalSweep< Stencil_T, FlagField_T, ScalarField_T, VectorField_T > normalSweep( + geometryHandler.normalFieldID_, geometryHandler.fillFieldID_, geometryHandler.flagFieldID_, + flagIDs::interfaceFlagID, flagIDs::liquidInterfaceGasFlagIDs, geometryHandler.obstacleFlagIDSet_, false, false, + true, false); + timeloop.add() << Sweep(normalSweep, "Sweep: normal computation", StateSweep::fullFreeSurface) + << Sweep(emptySweep(), "Empty sweep: normal") + << AfterFunction(Communication_T(geometryHandler.blockForest_, geometryHandler.normalFieldID_), + "Communication: after normal sweep"); + + // compute obstacle normals + ObstacleNormalSweep< Stencil_T, FlagField_T, VectorField_T > obstacleNormalSweep( + geometryHandler.obstacleNormalFieldID_, geometryHandler.flagFieldID_, flagIDs::interfaceFlagID, + flagIDs::liquidInterfaceGasFlagIDs, geometryHandler.obstacleFlagIDSet_, true, false, false); + timeloop.add() << Sweep(obstacleNormalSweep, "Sweep: obstacle normal computation", StateSweep::fullFreeSurface) + << Sweep(emptySweep(), "Empty sweep: obstacle normal") + << AfterFunction( + Communication_T(geometryHandler.blockForest_, geometryHandler.obstacleNormalFieldID_), + "Communication: after obstacle normal sweep"); + + if (geometryHandler.computeCurvature_) + { + // compute interface curvature using local triangulation according to dissertation of T. Pohl, 2008 + CurvatureSweepLocalTriangulation< Stencil_T, FlagField_T, ScalarField_T, VectorField_T > curvSweep( + geometryHandler.blockForest_, geometryHandler.curvatureFieldID_, geometryHandler.normalFieldID_, + geometryHandler.fillFieldID_, geometryHandler.flagFieldID_, geometryHandler.obstacleNormalFieldID_, + flagIDs::interfaceFlagID, geometryHandler.obstacleFlagIDSet_, geometryHandler.enableWetting_, + geometryHandler.contactAngle_); + timeloop.add() << Sweep(curvSweep, "Sweep: curvature computation (local triangulation)", + StateSweep::fullFreeSurface) + << Sweep(emptySweep(), "Empty sweep: curvature") + << AfterFunction(Communication_T(geometryHandler.blockForest_, geometryHandler.curvatureFieldID_), + "Communication: after curvature sweep"); + } + + // sweep for detecting cells that need to be converted to interface cells for continuing the wetting + // surface correctly + // IMPORTANT REMARK: this MUST NOT be performed when using finite differences for curvature computation and can + // otherwise lead to instabilities and errors + if (geometryHandler.enableWetting_) + { + const auto& flagInfo = geometryHandler.freeSurfaceBoundaryHandling_->getFlagInfo(); + + DetectWettingSweep< + Stencil_T, + typename FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::BoundaryHandling_T, + FlagField_T, ScalarField_T, VectorField_T > + detWetSweep(geometryHandler.freeSurfaceBoundaryHandling_->getHandlingID(), flagInfo, + geometryHandler.normalFieldID_, geometryHandler.fillFieldID_); + timeloop.add() << Sweep(detWetSweep, "Sweep: wetting detection", StateSweep::fullFreeSurface) + << Sweep(emptySweep(), "Empty sweep: wetting detection") + << AfterFunction(Communication_T(geometryHandler.blockForest_, geometryHandler.flagFieldID_), + "Communication: after wetting detection sweep") + << AfterFunction(blockforest::UpdateSecondGhostLayer< FlagField_T >(geometryHandler.blockForest_, + geometryHandler.flagFieldID_), + "Second ghost layer update: after wetting detection sweep (flag field)"); + } +} + +template< typename Stencil_T, typename LatticeModel_T, typename FlagField_T, typename ScalarField_T, + typename VectorField_T > +void SimpleFiniteDifferenceMethod< Stencil_T, LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T >::addSweeps( + SweepTimeloop& timeloop, const SimpleFiniteDifferenceMethod::SurfaceGeometryHandler_T& geometryHandler) +{ + using Communication_T = typename SurfaceGeometryHandler_T::Communication_T; + using StateSweep = typename SurfaceGeometryHandler_T::StateSweep; + + // compute interface normals + NormalSweep< Stencil_T, FlagField_T, ScalarField_T, VectorField_T > normalSweep( + geometryHandler.normalFieldID_, geometryHandler.fillFieldID_, geometryHandler.flagFieldID_, + flagIDs::interfaceFlagID, flagIDs::liquidInterfaceGasFlagIDs, geometryHandler.obstacleFlagIDSet_, false, false, + false, false); + timeloop.add() << Sweep(normalSweep, "Sweep: normal computation", StateSweep::fullFreeSurface) + << Sweep(emptySweep(), "Empty sweep: normal") + << AfterFunction(Communication_T(geometryHandler.blockForest_, geometryHandler.normalFieldID_), + "Communication: after normal sweep"); + + // extrapolation of normals to interface neighboring cells (required for computing the curvature with finite + // differences) + ExtrapolateNormalsSweep< Stencil_T, FlagField_T, VectorField_T > extNormalsSweep( + geometryHandler.normalFieldID_, geometryHandler.flagFieldID_, flagIDs::interfaceFlagID); + timeloop.add() << Sweep(extNormalsSweep, "Sweep: normal extrapolation", StateSweep::fullFreeSurface) + << Sweep(emptySweep(), "Empty sweep: normal extrapolation") + << AfterFunction(Communication_T(geometryHandler.blockForest_, geometryHandler.normalFieldID_), + "Communication: after normal extrapolation sweep"); + + if (geometryHandler.computeCurvature_) + { + // curvature computation using finite differences + CurvatureSweepSimpleFiniteDifferences< Stencil_T, FlagField_T, ScalarField_T, VectorField_T > curvSweep( + geometryHandler.curvatureFieldID_, geometryHandler.normalFieldID_, geometryHandler.flagFieldID_, + flagIDs::interfaceFlagID, geometryHandler.obstacleFlagIDSet_, geometryHandler.enableWetting_, + geometryHandler.contactAngle_); + timeloop.add() << Sweep(curvSweep, "Sweep: curvature computation (simple finite difference method)", + StateSweep::fullFreeSurface) + << Sweep(emptySweep(), "Empty sweep: curvature") + << AfterFunction(Communication_T(geometryHandler.blockForest_, geometryHandler.curvatureFieldID_), + "Communication: after curvature sweep "); + } +} + +} // namespace curvature_model +} // namespace free_surface +} // namespace walberla \ No newline at end of file diff --git a/src/lbm/free_surface/surface_geometry/CurvatureSweep.h b/src/lbm/free_surface/surface_geometry/CurvatureSweep.h new file mode 100644 index 000000000..dfe53545c --- /dev/null +++ b/src/lbm/free_surface/surface_geometry/CurvatureSweep.h @@ -0,0 +1,211 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file CurvatureSweep.h +//! \ingroup surface_geometry +//! \author Martin Bauer +//! \author Matthias Markl <matthias.markl@fau.de> +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Sweeps for computing the interface curvature. +// +//====================================================================================================================== + +#pragma once + +#include "blockforest/StructuredBlockForest.h" + +#include "core/logging/Logging.h" +#include "core/math/Constants.h" + +#include "domain_decomposition/BlockDataID.h" + +#include "field/FlagField.h" + +#include "stencil/D2Q9.h" +#include "stencil/D3Q27.h" +#include "stencil/Directions.h" + +#include <type_traits> +#include <vector> + +#include "ContactAngle.h" + +namespace walberla +{ +namespace free_surface +{ +/*********************************************************************************************************************** + * Compute the interface curvature using a finite difference scheme (Parker-Youngs approach) as described in + * - dissertation of S. Bogner, 2017 (section 4.4.2.1) + * which is based on + * - Brackbill, Kothe and Zemach, "A continuum method ...", 1992 + **********************************************************************************************************************/ +template< typename Stencil_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T > +class CurvatureSweepFiniteDifferences +{ + protected: + using vector_t = typename std::remove_const< typename VectorField_T::value_type >::type; + using flag_t = typename std::remove_const< typename FlagField_T::value_type >::type; + + public: + CurvatureSweepFiniteDifferences(const BlockDataID& curvatureFieldID, const ConstBlockDataID& normalFieldID, + const ConstBlockDataID& obstacleNormalFieldID, const ConstBlockDataID& flagFieldID, + const FlagUID& interfaceFlagID, const Set< FlagUID >& liquidInterfaceGasFlagIDSet, + const Set< FlagUID >& obstacleFlagIDSet, bool enableWetting, + const ContactAngle& contactAngle) + : curvatureFieldID_(curvatureFieldID), normalFieldID_(normalFieldID), + obstacleNormalFieldID_(obstacleNormalFieldID), flagFieldID_(flagFieldID), enableWetting_(enableWetting), + contactAngle_(contactAngle), interfaceFlagID_(interfaceFlagID), + liquidInterfaceGasFlagIDSet_(liquidInterfaceGasFlagIDSet), obstacleFlagIDSet_(obstacleFlagIDSet) + {} + + void operator()(IBlock* const block); + + /******************************************************************************************************************** + * Returns an adjusted interface normal according to the wetting model from the dissertation of S. Bogner, 2017 + * (section 4.4.2.1). + *******************************************************************************************************************/ + template< typename VectorIt_T > + Vector3< real_t > getNormalWithWetting(VectorIt_T normalFieldIt, VectorIt_T obstacleNormalFieldIt, + const stencil::Direction dir) + { + // get reversed interface normal + const Vector3< real_t > n = -*normalFieldIt; + + // get n_w, i.e., obstacle normal + const Vector3< real_t > nw = obstacleNormalFieldIt.neighbor(dir); + + // get n_t: vector tangent to the wall and normal to the contact line; obtained by subtracting the wall-normal + // component from the interface normal n + Vector3< real_t > nt = n - (nw * n) * nw; // "(nw * n) * nw" is orthogonal projection of nw on n + nt = nt.getNormalizedOrZero(); + + // compute interface normal at wall according to wetting model (equation 4.21 in dissertation of S. Bogner, + // 2017) + const Vector3< real_t > nWall = nw * contactAngle_.getCos() + nt * contactAngle_.getSin(); + + // extrapolate into obstacle cell to obtain boundary value; expression comes from 1D extrapolation formula + // IMPORTANT REMARK: corner and diagonal directions must use different formula, Bogner did not consider this in + // his implementation; here, nevertheless Bogner's approach is used + const Vector3< real_t > nWallExtrapolated = n + real_c(2) * (nWall - n); + + return -nWallExtrapolated; + } + + private: + BlockDataID curvatureFieldID_; + ConstBlockDataID normalFieldID_; + ConstBlockDataID obstacleNormalFieldID_; + ConstBlockDataID flagFieldID_; + + bool enableWetting_; + ContactAngle contactAngle_; + + FlagUID interfaceFlagID_; + Set< FlagUID > liquidInterfaceGasFlagIDSet_; + Set< FlagUID > obstacleFlagIDSet_; +}; // class CurvatureSweepFiniteDifferences + +/*********************************************************************************************************************** + * Compute the interface curvature using local triangulation as described in + * - dissertation of T. Pohl, 2008 (section 2.5) + * - dissertation of S. Donath, 2011 (wetting model, section 6.3.3) + **********************************************************************************************************************/ +template< typename Stencil_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T > +class CurvatureSweepLocalTriangulation +{ + protected: + using vector_t = typename std::remove_const< typename VectorField_T::value_type >::type; + + public: + CurvatureSweepLocalTriangulation(const std::weak_ptr< const StructuredBlockForest >& blockForest, + const BlockDataID& curvatureFieldID, const ConstBlockDataID& normalFieldID, + const ConstBlockDataID& fillFieldID, const ConstBlockDataID& flagFieldID, + const ConstBlockDataID& obstacleNormalFieldID, const FlagUID& interfaceFlagID, + const Set< FlagUID >& obstacleFlagIDSet, bool enableWetting, + const ContactAngle& contactAngle) + : blockForest_(blockForest), curvatureFieldID_(curvatureFieldID), normalFieldID_(normalFieldID), + fillFieldID_(fillFieldID), flagFieldID_(flagFieldID), obstacleNormalFieldID_(obstacleNormalFieldID), + enableWetting_(enableWetting), contactAngle_(contactAngle), interfaceFlagID_(interfaceFlagID), + obstacleFlagIDSet_(obstacleFlagIDSet) + { + if constexpr (std::is_same_v< Stencil_T, stencil::D2Q9 >) + { + WALBERLA_ABORT( + "Curvature computation with local triangulation using a D2Q9 stencil has not been thoroughly tested."); + } + } + + void operator()(IBlock* const block); + + private: + std::weak_ptr< const StructuredBlockForest > blockForest_; + BlockDataID curvatureFieldID_; + ConstBlockDataID normalFieldID_; + ConstBlockDataID fillFieldID_; + ConstBlockDataID flagFieldID_; + ConstBlockDataID obstacleNormalFieldID_; + + bool enableWetting_; + ContactAngle contactAngle_; + + FlagUID interfaceFlagID_; + Set< FlagUID > obstacleFlagIDSet_; +}; // class CurvatureSweepLocalTriangulation + +/*********************************************************************************************************************** + * Compute the interface curvature with a simplistic finite difference method. This approach is not documented in + * literature and neither thoroughly tested or validated. + * Use it with caution and preferably for testing purposes only. + **********************************************************************************************************************/ +template< typename Stencil_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T > +class CurvatureSweepSimpleFiniteDifferences +{ + protected: + using vector_t = typename std::remove_const< typename VectorField_T::value_type >::type; + + public: + CurvatureSweepSimpleFiniteDifferences(const BlockDataID& curvatureFieldID, const ConstBlockDataID& normalFieldID, + const ConstBlockDataID& flagFieldID, const FlagUID& interfaceFlagID, + const Set< FlagUID >& obstacleFlagIDSet, bool enableWetting, + const ContactAngle& contactAngle) + : curvatureFieldID_(curvatureFieldID), normalFieldID_(normalFieldID), flagFieldID_(flagFieldID), + enableWetting_(enableWetting), contactAngle_(contactAngle), interfaceFlagID_(interfaceFlagID), + obstacleFlagIDSet_(obstacleFlagIDSet) + { + WALBERLA_LOG_WARNING_ON_ROOT( + "You are using curvature computation based on a simplistic finite difference method. This " + "was implemented for testing purposes only and has not been thoroughly " + "validated and tested in the current state of the code. Use it with caution."); + } + + void operator()(IBlock* const block); + + private: + BlockDataID curvatureFieldID_; + ConstBlockDataID normalFieldID_; + ConstBlockDataID flagFieldID_; + + bool enableWetting_; + ContactAngle contactAngle_; + + FlagUID interfaceFlagID_; + Set< FlagUID > obstacleFlagIDSet_; +}; // class CurvatureSweepSimpleFiniteDifferences + +} // namespace free_surface +} // namespace walberla + +#include "CurvatureSweep.impl.h" \ No newline at end of file diff --git a/src/lbm/free_surface/surface_geometry/CurvatureSweep.impl.h b/src/lbm/free_surface/surface_geometry/CurvatureSweep.impl.h new file mode 100644 index 000000000..f6d46d103 --- /dev/null +++ b/src/lbm/free_surface/surface_geometry/CurvatureSweep.impl.h @@ -0,0 +1,518 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file CurvatureSweep.impl.h +//! \ingroup surface_geometry +//! \author Martin Bauer +//! \author Matthias Markl <matthias.markl@fau.de> +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Sweeps for computing the interface curvature. +// +//====================================================================================================================== + +#include "core/debug/CheckFunctions.h" +#include "core/logging/Logging.h" +#include "core/math/Matrix3.h" +#include "core/math/Utility.h" +#include "core/math/Vector3.h" + +#include "field/FlagField.h" + +#include "stencil/D3Q27.h" +#include "stencil/Directions.h" + +#include <algorithm> +#include <cmath> + +#include "ContactAngle.h" +#include "CurvatureSweep.h" +#include "Utility.h" + +namespace walberla +{ +namespace free_surface +{ +template< typename Stencil_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T > +void CurvatureSweepFiniteDifferences< Stencil_T, FlagField_T, ScalarField_T, VectorField_T >::operator()( + IBlock* const block) +{ + // get fields + ScalarField_T* const curvatureField = block->getData< ScalarField_T >(curvatureFieldID_); + const VectorField_T* const normalField = block->getData< const VectorField_T >(normalFieldID_); + const VectorField_T* const obstacleNormalField = block->getData< const VectorField_T >(obstacleNormalFieldID_); + const FlagField_T* const flagField = block->getData< const FlagField_T >(flagFieldID_); + + // get flags + const flag_t interfaceFlag = flagField->getFlag(interfaceFlagID_); + const flag_t liquidInterfaceGasFlagMask = flagField->getMask(liquidInterfaceGasFlagIDSet_); + const flag_t obstacleFlagMask = flagField->getMask(obstacleFlagIDSet_); + + WALBERLA_FOR_ALL_CELLS( + flagFieldIt, flagField, normalFieldIt, normalField, obstacleNormalFieldIt, obstacleNormalField, curvatureFieldIt, + curvatureField, { + real_t& curv = *curvatureFieldIt; + curv = real_c(0.0); + + if (isFlagSet(flagFieldIt, interfaceFlag)) // only treat interface cells + { + real_t weightSum = real_c(0); + + if (normalFieldIt->sqrLength() < real_c(1e-14)) + { + WALBERLA_LOG_WARNING("Invalid normal detected in CurvatureSweep.") + continue; + } + + // Parker-Youngs central finite difference approximation of curvature (see dissertation + // of S. Bogner, 2017, section 4.4.2.1) + for (auto dir = Stencil_T::beginNoCenter(); dir != Stencil_T::end(); ++dir) + { + const Vector3< real_t > dirVector = + Vector3< real_t >(real_c(dir.cx()), real_c(dir.cy()), real_c(dir.cz())); + + Vector3< real_t > neighborNormal; + + if (isPartOfMaskSet(flagFieldIt.neighbor(*dir), liquidInterfaceGasFlagMask | obstacleFlagMask)) + { + // get interface normal of neighbor in direction dir with respect to wetting model + if (enableWetting_ && isPartOfMaskSet(flagFieldIt.neighbor(*dir), obstacleFlagMask)) + { + neighborNormal = getNormalWithWetting(normalFieldIt, obstacleNormalFieldIt, *dir); + neighborNormal = neighborNormal.getNormalizedOrZero(); + } + else + { + if (isPartOfMaskSet(flagFieldIt.neighbor(*dir), liquidInterfaceGasFlagMask)) + { + neighborNormal = normalFieldIt.neighbor(*dir); + } + else + { + // skip remainder of this direction such that it is not considered in curvature computation + continue; + } + } + } + + // equation (35) in Brackbill et al. discretized with finite difference method + // according to Parker-Youngs + if constexpr (Stencil_T::D == uint_t(2)) + { + const real_t weight = + real_c(stencil::gaussianMultipliers[stencil::D3Q27::idx[stencil::map2Dto3D[2][*dir]]]); + weightSum += weight; + curv += weight * (dirVector * neighborNormal); + } + else + { + const real_t weight = real_c(stencil::gaussianMultipliers[dir.toIdx()]); + weightSum += weight; + curv += weight * (dirVector * neighborNormal); + } + } + + // divide by sum of weights in Parker-Youngs approximation; sum does not contain weights of directions in + // which there is no liquid, interface, gas, or obstacle cell (only when wetting is enabled, otherwise + // obstacle cell is also not considered); must be done like this because otherwise such non-valid + // directions would implicitly influence the finite difference scheme by assuming a normal of zero + curv /= weightSum; + } + }) // WALBERLA_FOR_ALL_CELLS +} + +template< typename Stencil_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T > +void CurvatureSweepLocalTriangulation< Stencil_T, FlagField_T, ScalarField_T, VectorField_T >::operator()( + IBlock* const block) +{ + const auto blockForest = blockForest_.lock(); + WALBERLA_CHECK_NOT_NULLPTR(blockForest); + + // struct for storing relevant information of each neighboring interface cell (POD type) + using Neighbor = struct + { + Vector3< real_t > diff; // difference (distance in coordinates) between this and neighboring interface point + Vector3< real_t > diffNorm; // normalized difference between this and neighboring interface point + Vector3< real_t > normal; // interface normal of neighboring interface point + real_t dist2; // square of the distance between this and neighboring interface point + real_t area; // sum of the area of the two triangles that this neighboring interface is part of + real_t sort; // angle that is used to sort the order neighboring points accordingly + bool valid; // validity, used to remove triangles with too narrow angles + bool wall; // used to include wetting effects near solid cells + }; + + // get fields + ScalarField_T* const curvatureField = block->getData< ScalarField_T >(curvatureFieldID_); + const VectorField_T* const normalField = block->getData< const VectorField_T >(normalFieldID_); + const ScalarField_T* const fillField = block->getData< const ScalarField_T >(fillFieldID_); + const FlagField_T* const flagField = block->getData< const FlagField_T >(flagFieldID_); + const VectorField_T* const obstacleNormalField = block->getData< const VectorField_T >(obstacleNormalFieldID_); + + // get flags + auto interfaceFlag = flagField->getFlag(interfaceFlagID_); + auto obstacleFlagMask = flagField->getMask(obstacleFlagIDSet_); + + WALBERLA_FOR_ALL_CELLS( + flagFieldIt, flagField, normalFieldIt, normalField, fillFieldIt, fillField, obstacleNormalFieldIt, + obstacleNormalField, curvatureFieldIt, curvatureField, { + real_t& curv = *curvatureFieldIt; + curv = real_c(0.0); + std::vector< Neighbor > neighbors; + Vector3< real_t > meanInterfaceNormal; + + // compute curvature only in interface points + if (!isFlagSet(flagFieldIt, interfaceFlag)) { continue; } + + // normal of this cell also contributes to mean normal + meanInterfaceNormal = *normalFieldIt; + + // iterate over all neighboring cells (Eq. 2.18 in dissertation of T. Pohl, 2008) + for (auto dir = Stencil_T::beginNoCenter(); dir != Stencil_T::end(); ++dir) + { + auto neighborFlags = flagFieldIt.neighbor(*dir); + + if (isFlagSet(neighborFlags, interfaceFlag) || // Eq. 2.19 in dissertation of T. Pohl, 2008 + (isPartOfMaskSet(neighborFlags, obstacleFlagMask) && + dir.toIdx() <= uint_c(18))) // obstacle in main direction or diagonal direction (not corner direction) + { + Vector3< real_t > neighborNormal; + const real_t neighborFillLevel = fillFieldIt.neighbor(*dir); + + // if loop was entered because of neighboring solid cell, normal of this solid cell points towards the + // currently processed interface cell + Vector3< real_t > wallNormal(real_c(-dir.cx()), real_c(-dir.cy()), real_c(-dir.cz())); + + if (isPartOfMaskSet(neighborFlags, obstacleFlagMask)) + { + neighborNormal = + Vector3< real_t >(real_c(1)); // temporarily guarantees "neighborNormal.sqrLength()>0" + wallNormal.getNormalized(); + } + else { neighborNormal = normalFieldIt.neighbor(*dir); } + + if (neighborNormal.sqrLength() > real_c(0)) + { + Neighbor n; + + // get global coordinate (with respect to whole simulation domain) of the currently processed cell + Vector3< real_t > globalCellCoordinate = + blockForest->getBlockLocalCellCenter(*block, flagFieldIt.cell()) - Vector3< real_t >(real_c(0.5)); + + for (auto dir2 = Stencil_T::beginNoCenter(); dir2 != Stencil_T::end(); ++dir2) + { + // stay in the close neighborhood of the currently processed interface cell + if ((dir.cx() != 0 && dir2.cx() != 0) || (dir.cy() != 0 && dir2.cy() != 0) || + (dir.cz() != 0 && dir2.cz() != 0)) + { + continue; + } + + if (isPartOfMaskSet(neighborFlags, obstacleFlagMask) && enableWetting_) + { + // get flags of neighboring cell in direction dir2 + auto neighborFlagsInDir2 = flagFieldIt.neighbor(*dir2); + + // the currently processed interface cell i has a neighboring solid cell j in direction dir; + // get the flags of j's neighboring cell in direction dir2 + // i.e., from the current cell, go to neighbor in dir; from there, go to next cell in dir2 + auto neighborNeighborFlags = + flagFieldIt.neighbor(dir.cx() + dir2.cx(), dir.cy() + dir2.cy(), dir.cz() + dir2.cz()); + + // true if the currently processed interface cell i has a neighboring interface cell j (in + // dir2), which (j) has a neighboring obstacle cell in the same direction as i does (in dir) + if (isFlagSet(neighborFlagsInDir2, interfaceFlag) && + isPartOfMaskSet(neighborNeighborFlags, obstacleFlagMask)) + { + // get the normal of the currently processed interface cell's neighboring interface cell + // (in direction 2) + Vector3< real_t > neighborNormalDir2 = normalFieldIt.neighbor(*dir2); + + Vector3< real_t > neighborGlobalCoordDir2 = globalCellCoordinate; + neighborGlobalCoordDir2[0] += real_c(dir2.cx()); + neighborGlobalCoordDir2[1] += real_c(dir2.cy()); + neighborGlobalCoordDir2[2] += real_c(dir2.cz()); + + // get neighboring interface point, i.e., location of interface within cell + Vector3< real_t > neighborGlobalInterfacePoint = + getInterfacePoint(normalFieldIt.neighbor(*dir2), fillFieldIt.neighbor(*dir2)); + + // transform to global coordinates, i.e., neighborInterfacePoint specifies the global + // location of the interface point in the currently processed interface cell's neighbor in + // direction dir2 + neighborGlobalInterfacePoint += neighborGlobalCoordDir2; + + // get the mean (averaged over multiple solid cells) wall normal of the neighbor in + // direction dir2 + Vector3< real_t > obstacleNormal = obstacleNormalFieldIt.neighbor(*dir2); + obstacleNormal *= real_c(-1); + if (obstacleNormal.sqrLength() < real_c(1e-10)) { obstacleNormal = wallNormal; } + + Vector3< real_t > neighborPoint; + + bool result = computeArtificalWallPoint( + neighborGlobalInterfacePoint, neighborGlobalCoordDir2, neighborNormalDir2, wallNormal, + obstacleNormal, contactAngle_, neighborPoint, neighborNormal); + if (!result) { continue; } + n.wall = true; + n.diff = neighborPoint - neighborGlobalInterfacePoint; + n.dist2 = n.diff.sqrLength(); + } + else { continue; } + } + else + // will be entered if: + // isFlagSet(neighborFlags, interfaceFlag) && !isPartOfMaskSet(neighborFlags, obstacleFlagMask) + { + n.wall = false; + + // get neighboring interface point, i.e., location of interface within cell + n.diff = getInterfacePoint(neighborNormal, neighborFillLevel); + + // get distance between this cell (0,0,0) and neighboring interface point + (dx,dy,dz) + n.diff += Vector3< real_t >(real_c(dir.cx()), real_c(dir.cy()), real_c(dir.cz())); + + // get distance between this and neighboring interface point + n.diff -= getInterfacePoint(*normalFieldIt, *fillFieldIt); + n.dist2 = n.diff.sqrLength(); + } + + // exclude neighboring interface points that are too close or too far away from this cell's + // interface point + if (n.dist2 >= real_c(0.64) && n.dist2 <= real_c(3.24)) // Eq. 2.20, 0.64 = 0.8^2; 3.24 = 1.8^2 + { + n.normal = neighborNormal; + n.diffNorm = n.diff.getNormalized(); + n.area = real_c(0); + n.sort = real_c(0); + n.valid = true; + + neighbors.push_back(n); + } + + // if there is no obstacle, loop should be interrupted immediately + if (!isPartOfMaskSet(neighborFlags, obstacleFlagMask)) + { + // interrupt loop + break; + } + } + } + } + } + + // remove degenerated triangles, see dissertation of T. Pohl, 2008, p. 27 + for (auto nIt1 = ++neighbors.begin(); !neighbors.empty() && nIt1 != neighbors.end(); ++nIt1) + { + if (!nIt1->valid) { continue; } + + for (auto nIt2 = neighbors.begin(); nIt2 != nIt1; ++nIt2) + { + if (!nIt2->valid) { continue; } + + // triangle is degenerated if angle between surface normals is less than 30° (heuristically chosen + // in dissertation of T. Pohl, 2008, p. 27); greater sign is correct here due to cos(29) > cos(30) + if (nIt1->diffNorm * nIt2->diffNorm > + real_c(0.866)) // cos(30°) = 0.866, as in dissertation of T. Pohl, 2008, p. 27 + { + const real_t diff = nIt1->dist2 - nIt2->dist2; + + if (diff < real_c(1e-4)) { nIt1->valid = nIt1->wall ? true : false; } + if (diff > real_c(-1e-4)) { nIt2->valid = nIt2->wall ? true : false; } + } + } + } + + // remove invalid neighbors + neighbors.erase(std::remove_if(neighbors.begin(), neighbors.end(), [](const Neighbor& a) { return !a.valid; }), + neighbors.end()); + + if (neighbors.size() < 4) + { + // WALBERLA_LOG_WARNING_ON_ROOT( + // "Not enough faces in curvature reconstruction, setting curvature in this cell to zero."); + curv = real_c(0); // not documented in literature but taken from S. Donath's code + continue; // process next cell in WALBERLA_FOR_ALL_CELLS + } + + // compute mean normal + for (auto const& n : neighbors) + { + meanInterfaceNormal += n.normal; + } + meanInterfaceNormal = meanInterfaceNormal.getNormalized(); + + // compute xAxis and yAxis that define a coordinate system on a tangent plane for sorting neighbors + // T_i' = (I - N * N^t) * (p_i - p); projection of neighbors.diff[0] onto tangent plane (Figure 2.14 in + // dissertation of T. Pohl, 2008) + Vector3< real_t > xAxis = + (Matrix3< real_t >::makeIdentityMatrix() - dyadicProduct(meanInterfaceNormal, meanInterfaceNormal)) * + neighbors[0].diff; + + // T_i = T_i' / ||T_i'|| + xAxis = xAxis.getNormalized(); + + // get vector that is orthogonal to xAxis and meanInterfaceNormal + const Vector3< real_t > yAxis = cross(xAxis, meanInterfaceNormal); + + for (auto& n : neighbors) + { + // get cosine of angles between n.diff and axes of the new coordinate system + const real_t cosAngX = xAxis * n.diff; + const real_t cosAngY = yAxis * n.diff; + + // sort the neighboring interface points using atan2 (which is not just atan(wy/wx), see Wikipedia) + n.sort = std::atan2(cosAngY, cosAngX); + } + + std::sort(neighbors.begin(), neighbors.end(), + [](const Neighbor& a, const Neighbor& b) { return a.sort < b.sort; }); + + Vector3< real_t > meanTriangleNormal(real_c(0)); + for (auto nIt1 = neighbors.begin(); neighbors.size() > uint_c(1) && nIt1 != neighbors.end(); ++nIt1) + { + // index of second neighbor starts over at 0: (k + 1) mod N_P + auto nIt2 = (nIt1 != (neighbors.end() - 1)) ? (nIt1 + 1) : neighbors.begin(); + + // N_f_k (with real length, i.e., not normalized yet) + const Vector3< real_t > triangleNormal = cross(nIt1->diff, nIt2->diff); + + // |f_k| (multiplication with 0.5, since triangleNormal.length() is area of parallelogram and not + // triangle) + const real_t area = real_c(0.5) * triangleNormal.length(); + + // lambda_i' = |f_{(i-1+N_p) mod N_p}| + |f_i| + nIt1->area += area; // from the view of na, this is |f_i| + nIt2->area += area; // from the view of nb, this is |f_{(i-1+N_p) mod N_p}|, i.e., area of the face + // from the neighbor with smaller index + + // N' = sum(|f_k| * N_f_k) + meanTriangleNormal += area * triangleNormal; + } + + if (meanTriangleNormal.length() < real_c(1e-10)) + { + // WALBERLA_LOG_WARNING_ON_ROOT("Invalid meanTriangleNormal, setting curvature in this cell to zero."); + curv = real_c(0); // not documented in literature but taken from S. Donath's code + continue; // process next cell in WALBERLA_FOR_ALL_CELLS + } + + // N = N' / ||N'|| + meanTriangleNormal = meanTriangleNormal.getNormalized(); + + // I - N * N^t; matrix for projection of vector on tangent plane + const Matrix3< real_t > projMatrix = + Matrix3< real_t >::makeIdentityMatrix() - dyadicProduct(meanTriangleNormal, meanTriangleNormal); + + // M + Matrix3< real_t > mMatrix(real_c(0)); + + // sum(lambda_i') + real_t neighborAreaSum = real_c(0); + + // M = sum(lambda_i' * kappa_i * T_i * T_i^t) + for (auto& n : neighbors) + { + if (n.area > real_c(0)) + { + // kappa_i = 2 * N^t * (p_i - p) / ||p_i - p||^2 + const real_t kappa = real_c(2) * (meanTriangleNormal * n.diff) / n.dist2; + + // T_i' = (I - N * N^t) * (p_i - p) + Vector3< real_t > tVector = projMatrix * n.diff; + + // T_i = T_i' / ||T_i'|| + tVector = tVector.getNormalized(); + + // T_i * T_i^t + const Matrix3< real_t > auxMat = dyadicProduct(tVector, tVector); + + // M += T_i * T_i^t * kappa_i * lambda_i' + mMatrix += auxMat * kappa * n.area; + + // sum(lambda_i') + neighborAreaSum += n.area; + } + } + + // M = M * 1 / sum(lambda_i') + mMatrix = mMatrix * (real_c(1) / neighborAreaSum); + + // kappa = tr(M) + curv = (mMatrix(0, 0) + mMatrix(1, 1) + mMatrix(2, 2)); + }) // WALBERLA_FOR_ALL_CELLS +} + +template< typename Stencil_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T > +void CurvatureSweepSimpleFiniteDifferences< Stencil_T, FlagField_T, ScalarField_T, VectorField_T >::operator()( + IBlock* const block) +{ + // get fields + ScalarField_T* const curvatureField = block->getData< ScalarField_T >(curvatureFieldID_); + const VectorField_T* const normalField = block->getData< const VectorField_T >(normalFieldID_); + const FlagField_T* const flagField = block->getData< const FlagField_T >(flagFieldID_); + + // get flags + auto interfaceFlag = flagField->getFlag(interfaceFlagID_); + auto obstacleFlagMask = flagField->getMask(obstacleFlagIDSet_); + + WALBERLA_FOR_ALL_CELLS(flagFieldIt, flagField, normalFieldIt, normalField, curvatureFieldIt, curvatureField, { + real_t& curv = *curvatureFieldIt; + curv = real_c(0.0); + + // interface cells + if (isFlagSet(flagFieldIt, interfaceFlag)) + { + // this cell is next to a wall/obstacle + if (enableWetting_ && isFlagInNeighborhood< Stencil_T >(flagFieldIt, obstacleFlagMask)) + { + // compute wall/obstacle curvature + vector_t obstacleNormal(real_c(0), real_c(0), real_c(0)); + for (auto it = Stencil_T::beginNoCenter(); it != Stencil_T::end(); ++it) + { + // calculate obstacle normal with central finite difference approximation of the surface's gradient (see + // dissertation of S. Donath, 2011, section 6.3.5.2) + if (isPartOfMaskSet(flagFieldIt.neighbor(*it), obstacleFlagMask)) + { + obstacleNormal[0] -= real_c(it.cx()); + obstacleNormal[1] -= real_c(it.cy()); + obstacleNormal[2] -= real_c(it.cz()); + } + } + + if (obstacleNormal.sqrLength() > real_c(0)) + { + obstacleNormal = obstacleNormal.getNormalized(); + + // IMPORTANT REMARK: + // the following wetting model is not documented in literature and not tested for correctness; use it + // with caution + curv = -real_c(0.25) * (contactAngle_.getCos() - (*normalFieldIt) * obstacleNormal); + } + } + else // no obstacle cell is in next neighborhood + { + // central finite difference approximation of curvature (see dissertation of S. Bogner, 2017, + // section 4.4.2.1) + curv = normalFieldIt.neighbor(1, 0, 0)[0] - normalFieldIt.neighbor(-1, 0, 0)[0] + + normalFieldIt.neighbor(0, 1, 0)[1] - normalFieldIt.neighbor(0, -1, 0)[1] + + normalFieldIt.neighbor(0, 0, 1)[2] - normalFieldIt.neighbor(0, 0, -1)[2]; + + curv *= real_c(0.25); + } + } + }) // WALBERLA_FOR_ALL_CELLS +} + +} // namespace free_surface +} // namespace walberla diff --git a/src/lbm/free_surface/surface_geometry/DetectWettingSweep.h b/src/lbm/free_surface/surface_geometry/DetectWettingSweep.h new file mode 100644 index 000000000..f4d69e0af --- /dev/null +++ b/src/lbm/free_surface/surface_geometry/DetectWettingSweep.h @@ -0,0 +1,334 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file DetectWettingSweep.h +//! \ingroup surface_geometry +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Sweep for detecting cells that need to be converted to interface to obtain a smooth wetting interface. +// +//====================================================================================================================== + +#pragma once + +#include "blockforest/StructuredBlockForest.h" + +#include "core/math/Constants.h" + +#include "domain_decomposition/BlockDataID.h" + +#include "field/FlagField.h" + +#include "stencil/D2Q4.h" +#include "stencil/D3Q19.h" + +#include <type_traits> +#include <vector> + +#include "ContactAngle.h" +#include "Utility.h" + +namespace walberla +{ +namespace free_surface +{ +/*********************************************************************************************************************** + * Sweep for detecting interface cells that need to be created in order to obtain a smooth interface continuation in + * case of wetting. + * + * See dissertation of S. Donath, 2011 section 6.3.5.3. + **********************************************************************************************************************/ +template< typename Stencil_T, typename BoundaryHandling_T, typename FlagField_T, typename ScalarField_T, + typename VectorField_T > +class DetectWettingSweep +{ + protected: + using FlagUIDSet = Set< FlagUID >; + + using vector_t = typename std::remove_const< typename VectorField_T::value_type >::type; + + // restrict stencil because surface continuation in corner directions is not meaningful + using WettingStencil_T = typename std::conditional< Stencil_T::D == uint_t(2), stencil::D2Q9, stencil::D3Q19 >::type; + + public: + DetectWettingSweep(BlockDataID boundaryHandling, const FlagInfo< FlagField_T >& flagInfo, + const ConstBlockDataID& normalFieldID, const ConstBlockDataID& fillFieldID) + : boundaryHandlingID_(boundaryHandling), flagInfo_(flagInfo), normalFieldID_(normalFieldID), + fillFieldID_(fillFieldID) + {} + + void operator()(IBlock* const block); + + private: + BlockDataID boundaryHandlingID_; + FlagInfo< FlagField_T > flagInfo_; + ConstBlockDataID normalFieldID_; + ConstBlockDataID fillFieldID_; + ConstBlockDataID flagFieldID_; + +}; // class DetectWettingSweep + +template< typename Stencil_T, typename BoundaryHandling_T, typename FlagField_T, typename ScalarField_T, + typename VectorField_T > +void DetectWettingSweep< Stencil_T, BoundaryHandling_T, FlagField_T, ScalarField_T, VectorField_T >::operator()( + IBlock* const block) +{ + // get free surface boundary handling + BoundaryHandling_T* const boundaryHandling = block->getData< BoundaryHandling_T >(boundaryHandlingID_); + + // get fields + const VectorField_T* const normalField = block->getData< const VectorField_T >(normalFieldID_); + const ScalarField_T* const fillField = block->getData< const ScalarField_T >(fillFieldID_); + const FlagField_T* const flagField = boundaryHandling->getFlagField(); + + // get flags + const FlagInfo< FlagField_T >& flagInfo = flagInfo_; + using flag_t = typename FlagField_T::flag_t; + + const flag_t liquidInterfaceGasFlagMask = flagInfo_.liquidFlag | flagInfo_.interfaceFlag | flagInfo_.gasFlag; + + WALBERLA_FOR_ALL_CELLS(flagFieldIt, flagField, normalFieldIt, normalField, fillFieldIt, fillField, { + // skip non-interface cells + if (!isFlagSet(flagFieldIt, flagInfo.interfaceFlag)) { continue; } + + // skip cells that have no solid cell in their neighborhood + if (!isFlagInNeighborhood< WettingStencil_T >(flagFieldIt, flagInfo.obstacleFlagMask)) { continue; } + + // restrict maximal and minimal angle such that the surface continuation does not become too flat + if (*fillFieldIt < real_c(0.005) || *fillFieldIt > real_c(0.995)) { continue; } + + const Vector3< real_t > interfacePointLocation = getInterfacePoint(*normalFieldIt, *fillFieldIt); + + for (auto dir = WettingStencil_T::beginNoCenter(); dir != WettingStencil_T::end(); ++dir) + { + const Cell neighborCell = + Cell(flagFieldIt.x() + dir.cx(), flagFieldIt.y() + dir.cy(), flagFieldIt.z() + dir.cz()); + const flag_t neighborFlag = flagField->get(neighborCell); + + // skip neighboring cells that + // - are not liquid, gas or interface + // - are already marked for conversion to interface due to wetting + // IMPORTANT REMARK: It is crucial that interface cells are NOT skipped here. Since the + // "keepInterfaceForWettingFlag" flag is cleared in all cells after performing the conversion, interface cells + // that were converted due to wetting must still get this flag to avoid being prematurely converted back. + if (!isPartOfMaskSet(neighborFlag, liquidInterfaceGasFlagMask) || + isFlagSet(neighborFlag, flagInfo.keepInterfaceForWettingFlag)) + { + continue; + } + + // skip neighboring cells that do not have solid cells in their neighborhood + bool hasObstacle = false; + for (auto dir2 = WettingStencil_T::beginNoCenter(); dir2 != WettingStencil_T::end(); ++dir2) + { + const Cell neighborNeighborCell = + Cell(flagFieldIt.x() + dir.cx() + dir2.cx(), flagFieldIt.y() + dir.cy() + dir2.cy(), + flagFieldIt.z() + dir.cz() + dir2.cz()); + const flag_t neighborNeighborFlag = flagField->get(neighborNeighborCell); + + if (isPartOfMaskSet(neighborNeighborFlag, flagInfo.obstacleFlagMask)) + { + hasObstacle = true; + break; // exit dir2 loop + } + } + if (!hasObstacle) { continue; } + + // check cell edges for intersection with the interface surface plane and mark the respective neighboring for + // conversion + // bottom south + if ((dir.cx() == 0 && dir.cy() == -1 && dir.cz() == -1) || + (dir.cx() == 0 && dir.cy() == 0 && dir.cz() == -1) || (dir.cx() == 0 && dir.cy() == -1 && dir.cz() == 0)) + { + const real_t intersection = getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(0), real_c(0)), + Vector3< real_t >(real_c(1), real_c(0), real_c(0)), + *normalFieldIt, interfacePointLocation); + if (intersection > real_c(0)) + { + boundaryHandling->setFlag(flagInfo.keepInterfaceForWettingFlag, flagFieldIt.x() + dir.cx(), + flagFieldIt.y() + dir.cy(), flagFieldIt.z() + dir.cz()); + continue; + } + } + + // bottom north + if ((dir.cx() == 0 && dir.cy() == 1 && dir.cz() == -1) || (dir.cx() == 0 && dir.cy() == 0 && dir.cz() == -1) || + (dir.cx() == 0 && dir.cy() == 1 && dir.cz() == 0)) + { + const real_t intersection = getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(1), real_c(0)), + Vector3< real_t >(real_c(1), real_c(0), real_c(0)), + *normalFieldIt, interfacePointLocation); + if (intersection > real_c(0)) + { + boundaryHandling->setFlag(flagInfo.keepInterfaceForWettingFlag, flagFieldIt.x() + dir.cx(), + flagFieldIt.y() + dir.cy(), flagFieldIt.z() + dir.cz()); + continue; + } + } + + // bottom west + if ((dir.cx() == -1 && dir.cy() == 0 && dir.cz() == -1) || + (dir.cx() == 0 && dir.cy() == 0 && dir.cz() == -1) || (dir.cx() == -1 && dir.cy() == 0 && dir.cz() == 0)) + { + const real_t intersection = getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(0), real_c(0)), + Vector3< real_t >(real_c(0), real_c(1), real_c(0)), + *normalFieldIt, interfacePointLocation); + if (intersection > real_c(0)) + { + boundaryHandling->setFlag(flagInfo.keepInterfaceForWettingFlag, flagFieldIt.x() + dir.cx(), + flagFieldIt.y() + dir.cy(), flagFieldIt.z() + dir.cz()); + continue; + } + } + + // bottom east + if ((dir.cx() == 1 && dir.cy() == 0 && dir.cz() == -1) || (dir.cx() == 0 && dir.cy() == 0 && dir.cz() == -1) || + (dir.cx() == 1 && dir.cy() == 0 && dir.cz() == 0)) + { + const real_t intersection = getCellEdgeIntersection(Vector3< real_t >(real_c(1), real_c(0), real_c(0)), + Vector3< real_t >(real_c(0), real_c(1), real_c(0)), + *normalFieldIt, interfacePointLocation); + if (intersection > real_c(0)) + { + boundaryHandling->setFlag(flagInfo.keepInterfaceForWettingFlag, flagFieldIt.x() + dir.cx(), + flagFieldIt.y() + dir.cy(), flagFieldIt.z() + dir.cz()); + continue; + } + } + + // top south + if ((dir.cx() == 0 && dir.cy() == -1 && dir.cz() == 1) || (dir.cx() == 0 && dir.cy() == 0 && dir.cz() == 1) || + (dir.cx() == 0 && dir.cy() == -1 && dir.cz() == 0)) + { + const real_t intersection = getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(0), real_c(1)), + Vector3< real_t >(real_c(1), real_c(0), real_c(0)), + *normalFieldIt, interfacePointLocation); + if (intersection > real_c(0)) + { + boundaryHandling->setFlag(flagInfo.keepInterfaceForWettingFlag, flagFieldIt.x() + dir.cx(), + flagFieldIt.y() + dir.cy(), flagFieldIt.z() + dir.cz()); + continue; + } + } + + // top north + if ((dir.cx() == 0 && dir.cy() == 1 && dir.cz() == 1) || (dir.cx() == 0 && dir.cy() == 0 && dir.cz() == 1) || + (dir.cx() == 0 && dir.cy() == 1 && dir.cz() == 0)) + { + const real_t intersection = getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(1), real_c(1)), + Vector3< real_t >(real_c(1), real_c(0), real_c(0)), + *normalFieldIt, interfacePointLocation); + if (intersection > real_c(0)) + { + boundaryHandling->setFlag(flagInfo.keepInterfaceForWettingFlag, flagFieldIt.x() + dir.cx(), + flagFieldIt.y() + dir.cy(), flagFieldIt.z() + dir.cz()); + continue; + } + } + + // top west + if ((dir.cx() == -1 && dir.cy() == 0 && dir.cz() == 1) || (dir.cx() == 0 && dir.cy() == 0 && dir.cz() == 1) || + (dir.cx() == -1 && dir.cy() == 0 && dir.cz() == 0)) + { + const real_t intersection = getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(0), real_c(1)), + Vector3< real_t >(real_c(0), real_c(1), real_c(0)), + *normalFieldIt, interfacePointLocation); + if (intersection > real_c(0)) + { + boundaryHandling->setFlag(flagInfo.keepInterfaceForWettingFlag, flagFieldIt.x() + dir.cx(), + flagFieldIt.y() + dir.cy(), flagFieldIt.z() + dir.cz()); + continue; + } + } + + // top east + if ((dir.cx() == 1 && dir.cy() == 0 && dir.cz() == 1) || (dir.cx() == 0 && dir.cy() == 0 && dir.cz() == 1) || + (dir.cx() == 1 && dir.cy() == 0 && dir.cz() == 0)) + { + const real_t intersection = getCellEdgeIntersection(Vector3< real_t >(real_c(1), real_c(0), real_c(1)), + Vector3< real_t >(real_c(0), real_c(1), real_c(0)), + *normalFieldIt, interfacePointLocation); + if (intersection > real_c(0)) + { + boundaryHandling->setFlag(flagInfo.keepInterfaceForWettingFlag, flagFieldIt.x() + dir.cx(), + flagFieldIt.y() + dir.cy(), flagFieldIt.z() + dir.cz()); + continue; + } + } + + // south-west + if ((dir.cx() == -1 && dir.cy() == -1 && dir.cz() == 0) || + (dir.cx() == 0 && dir.cy() == -1 && dir.cz() == 0) || (dir.cx() == -1 && dir.cy() == 0 && dir.cz() == 0)) + { + const real_t intersection = getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(0), real_c(0)), + Vector3< real_t >(real_c(0), real_c(0), real_c(1)), + *normalFieldIt, interfacePointLocation); + if (intersection > real_c(0)) + { + boundaryHandling->setFlag(flagInfo.keepInterfaceForWettingFlag, flagFieldIt.x() + dir.cx(), + flagFieldIt.y() + dir.cy(), flagFieldIt.z() + dir.cz()); + continue; + } + } + + // south-east + if ((dir.cx() == 1 && dir.cy() == -1 && dir.cz() == 0) || (dir.cx() == 0 && dir.cy() == -1 && dir.cz() == 0) || + (dir.cx() == 1 && dir.cy() == 0 && dir.cz() == 0)) + { + const real_t intersection = getCellEdgeIntersection(Vector3< real_t >(real_c(1), real_c(0), real_c(0)), + Vector3< real_t >(real_c(0), real_c(0), real_c(1)), + *normalFieldIt, interfacePointLocation); + if (intersection > real_c(0)) + { + boundaryHandling->setFlag(flagInfo.keepInterfaceForWettingFlag, flagFieldIt.x() + dir.cx(), + flagFieldIt.y() + dir.cy(), flagFieldIt.z() + dir.cz()); + continue; + } + } + + // north-west + if ((dir.cx() == -1 && dir.cy() == 1 && dir.cz() == 0) || (dir.cx() == 0 && dir.cy() == 1 && dir.cz() == 0) || + (dir.cx() == -1 && dir.cy() == 0 && dir.cz() == 0)) + { + const real_t intersection = getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(1), real_c(0)), + Vector3< real_t >(real_c(0), real_c(0), real_c(1)), + *normalFieldIt, interfacePointLocation); + if (intersection > real_c(0)) + { + boundaryHandling->setFlag(flagInfo.keepInterfaceForWettingFlag, flagFieldIt.x() + dir.cx(), + flagFieldIt.y() + dir.cy(), flagFieldIt.z() + dir.cz()); + continue; + } + } + + // north-east + if ((dir.cx() == 1 && dir.cy() == 1 && dir.cz() == 0) || (dir.cx() == 0 && dir.cy() == 1 && dir.cz() == 0) || + (dir.cx() == 1 && dir.cy() == 0 && dir.cz() == 0)) + { + const real_t intersection = getCellEdgeIntersection(Vector3< real_t >(real_c(1), real_c(1), real_c(0)), + Vector3< real_t >(real_c(0), real_c(0), real_c(1)), + *normalFieldIt, interfacePointLocation); + if (intersection > real_c(0)) + { + boundaryHandling->setFlag(flagInfo.keepInterfaceForWettingFlag, flagFieldIt.x() + dir.cx(), + flagFieldIt.y() + dir.cy(), flagFieldIt.z() + dir.cz()); + continue; + } + } + } + }) // WALBERLA_FOR_ALL_CELLS +} + +} // namespace free_surface +} // namespace walberla \ No newline at end of file diff --git a/src/lbm/free_surface/surface_geometry/ExtrapolateNormalsSweep.h b/src/lbm/free_surface/surface_geometry/ExtrapolateNormalsSweep.h new file mode 100644 index 000000000..8d49e0c96 --- /dev/null +++ b/src/lbm/free_surface/surface_geometry/ExtrapolateNormalsSweep.h @@ -0,0 +1,71 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file ExtrapolateNormalsSweep.h +//! \ingroup surface_geometry +//! \author Martin Bauer +//! \author Matthias Markl <matthias.markl@fau.de> +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Extrapolate interface normals to neighboring cells in D3Q27 direction. +// +//====================================================================================================================== + +#pragma once + +#include "domain_decomposition/BlockDataID.h" + +#include "field/FlagField.h" + +#include <type_traits> +#include <vector> + +namespace walberla +{ +namespace free_surface +{ +/*********************************************************************************************************************** + * Approximates the normals of non-interface cells using the normals of all neighboring interface cells in D3Q27 + * direction. + * The approximation is computed by summing the weighted normals of all neighboring interface cells. The weights are + * chosen as in the Parker-Youngs approximation. + **********************************************************************************************************************/ +template< typename Stencil_T, typename FlagField_T, typename VectorField_T > +class ExtrapolateNormalsSweep +{ + protected: + using FlagUIDSet = Set< FlagUID >; + + using vector_t = typename std::remove_const< typename VectorField_T::value_type >::type; + using flag_t = typename std::remove_const< typename FlagField_T::value_type >::type; + + public: + ExtrapolateNormalsSweep(const BlockDataID& normalFieldID, const ConstBlockDataID& flagFieldID, + const FlagUID& interfaceFlagID) + : normalFieldID_(normalFieldID), flagFieldID_(flagFieldID), interfaceFlagID_(interfaceFlagID) + {} + + void operator()(IBlock* const block); + + private: + BlockDataID normalFieldID_; + ConstBlockDataID flagFieldID_; + + FlagUID interfaceFlagID_; +}; // class ExtrapolateNormalsSweep + +} // namespace free_surface +} // namespace walberla + +#include "ExtrapolateNormalsSweep.impl.h" \ No newline at end of file diff --git a/src/lbm/free_surface/surface_geometry/ExtrapolateNormalsSweep.impl.h b/src/lbm/free_surface/surface_geometry/ExtrapolateNormalsSweep.impl.h new file mode 100644 index 000000000..309bc79bb --- /dev/null +++ b/src/lbm/free_surface/surface_geometry/ExtrapolateNormalsSweep.impl.h @@ -0,0 +1,70 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file ExtrapolateNormalsSweep.impl.h +//! \ingroup surface_geometry +//! \author Martin Bauer +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Extrapolate interface normals to neighboring cells in D3Q27 direction. +// +//====================================================================================================================== + +#include "core/math/Vector3.h" + +#include "field/GhostLayerField.h" + +#include "stencil/D3Q27.h" + +#include "ExtrapolateNormalsSweep.h" + +namespace walberla +{ +namespace free_surface +{ +template< typename Stencil_T, typename FlagField_T, typename VectorField_T > +void ExtrapolateNormalsSweep< Stencil_T, FlagField_T, VectorField_T >::operator()(IBlock* const block) +{ + VectorField_T* const normalField = block->getData< VectorField_T >(normalFieldID_); + const FlagField_T* const flagField = block->getData< const FlagField_T >(flagFieldID_); + + const auto interfaceFlag = flagField->getFlag(interfaceFlagID_); + + // compute normals in interface neighboring cells, i.e., in D3Q27 direction of interface cells + WALBERLA_FOR_ALL_CELLS(flagFieldIt, flagField, normalFieldIt, normalField, { + if (!isFlagSet(flagFieldIt, interfaceFlag) && isFlagInNeighborhood< stencil::D3Q27 >(flagFieldIt, interfaceFlag)) + { + uint_t count = uint_c(0); + vector_t& normal = *normalFieldIt; + normal.set(real_c(0), real_c(0), real_c(0)); + + // approximate the normal of non-interface cells with the normal of neighboring interface cells (weights as + // in Parker-Youngs approximation) + for (auto i = Stencil_T::beginNoCenter(); i != Stencil_T::end(); ++i) + { + if (isFlagSet(flagFieldIt.neighbor(*i), interfaceFlag)) + { + normal += real_c(stencil::gaussianMultipliers[i.toIdx()]) * normalFieldIt.neighbor(*i); + ++count; + } + } + + // normalize the normal + normal = normal.getNormalizedOrZero(); + } + }) // WALBERLA_FOR_ALL_CELLS +} + +} // namespace free_surface +} // namespace walberla diff --git a/src/lbm/free_surface/surface_geometry/NormalSweep.h b/src/lbm/free_surface/surface_geometry/NormalSweep.h new file mode 100644 index 000000000..75cc8b6d9 --- /dev/null +++ b/src/lbm/free_surface/surface_geometry/NormalSweep.h @@ -0,0 +1,109 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file NormalSweep.h +//! \ingroup surface_geometry +//! \author Martin Bauer <martin.bauer@fau.de> +//! \author Daniela Anderl +//! \author Stefan Donath +//! \author Matthias Markl <matthias.markl@fau.de> +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Compute interface normal. +// +//====================================================================================================================== + +#pragma once + +#include "domain_decomposition/BlockDataID.h" + +#include "field/FlagField.h" + +#include <type_traits> +#include <vector> + +namespace walberla +{ +namespace free_surface +{ +/*********************************************************************************************************************** + * Compute normals in interface cells by taking the derivative of the fill level field using the Parker-Youngs + * approximation. Near boundary cells, a modified Parker-Youngs approximation is applied with the cell being shifted by + * 0.5 away from the boundary. + * + * Details can be found in the Dissertation of S. Donath, page 21f. + * + * IMPORTANT REMARK: In this FSLBM implementation, the normal is defined to point from liquid to gas. + * + * More general: compute the gradient of a given scalar field on cells that are marked with a specific flag. + **********************************************************************************************************************/ + +template< typename Stencil_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T > +class NormalSweep +{ + protected: + using vector_t = typename std::remove_const< typename VectorField_T::value_type >::type; + using scalar_t = typename std::remove_const< typename ScalarField_T::value_type >::type; + using flag_t = typename std::remove_const< typename FlagField_T::value_type >::type; + + public: + NormalSweep(const BlockDataID& normalFieldID, const ConstBlockDataID& fillFieldID, + const ConstBlockDataID& flagFieldID, const FlagUID& interfaceFlagID, + const Set< FlagUID >& liquidInterfaceGasFlagIDSet, const Set< FlagUID >& obstacleFlagIDSet, + bool computeInInterfaceNeighbors, bool includeObstacleNeighbors, bool modifyNearObstacles, + bool computeInGhostLayer) + : normalFieldID_(normalFieldID), fillFieldID_(fillFieldID), flagFieldID_(flagFieldID), + interfaceFlagID_(interfaceFlagID), liquidInterfaceGasFlagIDSet_(liquidInterfaceGasFlagIDSet), + obstacleFlagIDSet_(obstacleFlagIDSet), computeInInterfaceNeighbors_(computeInInterfaceNeighbors), + includeObstacleNeighbors_(includeObstacleNeighbors), modifyNearObstacles_(modifyNearObstacles), + computeInGhostLayer_(computeInGhostLayer) + {} + + void operator()(IBlock* const block); + + private: + BlockDataID normalFieldID_; + ConstBlockDataID fillFieldID_; + ConstBlockDataID flagFieldID_; + + FlagUID interfaceFlagID_; + Set< FlagUID > liquidInterfaceGasFlagIDSet_; + Set< FlagUID > obstacleFlagIDSet_; + + bool computeInInterfaceNeighbors_; + bool includeObstacleNeighbors_; + bool modifyNearObstacles_; + bool computeInGhostLayer_; +}; // class NormalSweep + +// namespace to use these functions outside NormalSweep, e.g., in ReinitializationSweep +namespace normal_computation +{ +// compute the normal using Parker-Youngs approximation (see dissertation of S. Donath, 2011, section 2.3.3.1.1) +template< typename Stencil_T, typename vector_t, typename ScalarFieldIt_T, typename FlagFieldIt_T, typename flag_t > +void computeNormal(vector_t& normal, const ScalarFieldIt_T& fillFieldIt, const FlagFieldIt_T& flagFieldIt, + const flag_t& validNeighborFlagMask); + +// near solid boundary cells, compute a Parker-Youngs approximation around a virtual (constructed) midpoint that is +// displaced by a distance of 0.5 away from the boundary (see dissertation of S. Donath, 2011, section 6.3.5.1) +template< typename Stencil_T, typename vector_t, typename ScalarFieldIt_T, typename FlagFieldIt_T, typename flag_t > +void computeNormalNearSolidBoundary(vector_t& normal, const ScalarFieldIt_T& fillFieldIt, + const FlagFieldIt_T& flagFieldIt, const flag_t& validNeighborFlagMask, + const flag_t& obstacleFlagMask); +} // namespace normal_computation + +} // namespace free_surface +} // namespace walberla + +#include "NormalSweep.impl.h" \ No newline at end of file diff --git a/src/lbm/free_surface/surface_geometry/NormalSweep.impl.h b/src/lbm/free_surface/surface_geometry/NormalSweep.impl.h new file mode 100644 index 000000000..a1ade27ec --- /dev/null +++ b/src/lbm/free_surface/surface_geometry/NormalSweep.impl.h @@ -0,0 +1,456 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file NormalSweep.impl.h +//! \ingroup surface_geometry +//! \author Martin Bauer +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Compute interface normal. +// +//====================================================================================================================== + +#include "core/logging/Logging.h" +#include "core/math/Vector3.h" + +#include "field/iterators/IteratorMacros.h" + +#include "stencil/D2Q9.h" +#include "stencil/D3Q19.h" +#include "stencil/D3Q27.h" + +#include <type_traits> + +#include "NormalSweep.h" +namespace walberla +{ +namespace free_surface +{ + +template< typename Stencil_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T > +void NormalSweep< Stencil_T, FlagField_T, ScalarField_T, VectorField_T >::operator()(IBlock* const block) +{ + // fetch fields + VectorField_T* const normalField = block->getData< VectorField_T >(normalFieldID_); + const ScalarField_T* const fillField = block->getData< const ScalarField_T >(fillFieldID_); + const FlagField_T* const flagField = block->getData< const FlagField_T >(flagFieldID_); + + // two ghost layers are required in the flag field + WALBERLA_ASSERT_EQUAL(flagField->nrOfGhostLayers(), uint_c(2)); + + // get flags + const flag_t interfaceFlag = flagField->getFlag(interfaceFlagID_); + const flag_t liquidInterfaceGasFlagMask = flagField->getMask(liquidInterfaceGasFlagIDSet_); + const flag_t obstacleFlagMask = flagField->getMask(obstacleFlagIDSet_); + + // evaluate flags in D2Q19 neighborhood for 2D, and in D3Q27 neighborhood for 3D simulations + using NeighborhoodStencil_T = + typename std::conditional< Stencil_T::D == uint_t(2), stencil::D2Q9, stencil::D3Q27 >::type; + + // include ghost layer because solid cells might be located in the (outermost global) ghost layer + WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ(normalField, uint_c(1), { + if (!computeInGhostLayer_ && (!flagField->isInInnerPart(Cell(x, y, z)))) { continue; } + + const typename FlagField_T::ConstPtr flagFieldPtr(*flagField, x, y, z); + const typename ScalarField_T::ConstPtr fillFieldPtr(*fillField, x, y, z); + + const bool computeNormalInCell = + isFlagSet(flagFieldPtr, interfaceFlag) || + (computeInInterfaceNeighbors_ && isFlagInNeighborhood< NeighborhoodStencil_T >(flagFieldPtr, interfaceFlag)); + + vector_t& normal = normalField->get(x, y, z); + + if (computeNormalInCell) + { + if (includeObstacleNeighbors_) + { + // requires meaningful fill level values in obstacle cells, as set by ObstacleFillLevelSweep when using + // curvature computation via the finite difference method + normal_computation::computeNormal< Stencil_T >(normal, fillFieldPtr, flagFieldPtr, + liquidInterfaceGasFlagMask | obstacleFlagMask); + } + else + { + if (modifyNearObstacles_ && isFlagInNeighborhood< Stencil_T >(flagFieldPtr, obstacleFlagMask)) + { + // near solid boundary cells, compute a Parker-Youngs approximation around a virtual (constructed) + // midpoint that is displaced by a distance of 0.5 away from the boundary (see dissertation of S. Donath, + // 2011, section 6.3.5.1); use only for curvature computation based on local triangulation + normal_computation::computeNormalNearSolidBoundary< Stencil_T >( + normal, fillFieldPtr, flagFieldPtr, liquidInterfaceGasFlagMask, obstacleFlagMask); + } + else + { + normal_computation::computeNormal< Stencil_T >(normal, fillFieldPtr, flagFieldPtr, + liquidInterfaceGasFlagMask); + } + } + + // normalize and negate normal (to make it point from liquid to gas) + normal = real_c(-1) * normal.getNormalizedOrZero(); + } + else { normal.set(real_c(0), real_c(0), real_c(0)); } + }); // WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ +} + +namespace normal_computation +{ +template< typename Stencil_T, typename vector_t, typename ScalarFieldIt_T, typename FlagFieldIt_T, typename flag_t > +void computeNormal(vector_t& normal, const ScalarFieldIt_T& fillFieldIt, const FlagFieldIt_T& flagFieldIt, + const flag_t& validNeighborFlagMask) +{ + // All computations are performed in double precision here (and truncated later). This is done to avoid an issue + // observed with the Intel 19 compiler when built in "DebugOptimized" mode with single precision. There, the result + // is dependent on the order of the "tmp_*" variables' definitions. It is assumed that an Intel-specific optimization + // leads to floating point inaccuracies. + normal = vector_t(real_c(0)); + + // avoid accessing neighbors that are out-of-range, i.e., restrict neighbor access to first ghost layer + const bool useW = flagFieldIt.x() >= cell_idx_c(0); + const bool useE = flagFieldIt.x() < cell_idx_c(flagFieldIt.getField()->xSize()); + const bool useS = flagFieldIt.y() >= cell_idx_c(0); + const bool useN = flagFieldIt.y() < cell_idx_c(flagFieldIt.getField()->ySize()); + + // loops are unrolled for improved computational performance + // IMPORTANT REMARK: the non-unrolled implementation was observed to give different results at O(1e-15); this + // accumulated and lead to inaccuracies, e.g., a drop wetting a surface became asymmetrical and started to move + // sideways + if constexpr (std::is_same_v< Stencil_T, stencil::D2Q9 >) + { + // get fill level in neighboring cells + const double tmp_S = useS && isPartOfMaskSet(flagFieldIt.neighbor(0, -1, 0), validNeighborFlagMask) ? + static_cast< double >(fillFieldIt.neighbor(0, -1, 0)) : + static_cast< double >(0); + const double tmp_N = useN && isPartOfMaskSet(flagFieldIt.neighbor(0, 1, 0), validNeighborFlagMask) ? + static_cast< double >(fillFieldIt.neighbor(0, 1, 0)) : + static_cast< double >(0); + const double tmp_W = useW && isPartOfMaskSet(flagFieldIt.neighbor(-1, 0, 0), validNeighborFlagMask) ? + static_cast< double >(fillFieldIt.neighbor(-1, 0, 0)) : + static_cast< double >(0); + const double tmp_E = useE && isPartOfMaskSet(flagFieldIt.neighbor(1, 0, 0), validNeighborFlagMask) ? + static_cast< double >(fillFieldIt.neighbor(1, 0, 0)) : + static_cast< double >(0); + const double tmp_SW = useS && useW && isPartOfMaskSet(flagFieldIt.neighbor(-1, -1, 0), validNeighborFlagMask) ? + static_cast< double >(fillFieldIt.neighbor(-1, -1, 0)) : + static_cast< double >(0); + const double tmp_SE = useS && useE && isPartOfMaskSet(flagFieldIt.neighbor(1, -1, 0), validNeighborFlagMask) ? + static_cast< double >(fillFieldIt.neighbor(1, -1, 0)) : + static_cast< double >(0); + const double tmp_NW = useN && useW && isPartOfMaskSet(flagFieldIt.neighbor(-1, 1, 0), validNeighborFlagMask) ? + static_cast< double >(fillFieldIt.neighbor(-1, 1, 0)) : + static_cast< double >(0); + const double tmp_NE = useN && useE && isPartOfMaskSet(flagFieldIt.neighbor(1, 1, 0), validNeighborFlagMask) ? + static_cast< double >(fillFieldIt.neighbor(1, 1, 0)) : + static_cast< double >(0); + + // compute normal with Parker-Youngs approximation (PY) + const double weight_1 = real_c(4); + normal[0] = real_c(weight_1 * (tmp_E - tmp_W)); + normal[1] = real_c(weight_1 * (tmp_N - tmp_S)); + normal[2] = real_c(0); + + const double weight_2 = real_c(2); + normal[0] += real_c(weight_2 * ((tmp_NE + tmp_SE) - (tmp_NW + tmp_SW))); + normal[1] += real_c(weight_2 * ((tmp_NE + tmp_NW) - (tmp_SE + tmp_SW))); + } + else + { + const bool useB = flagFieldIt.z() >= cell_idx_c(0); + const bool useT = flagFieldIt.z() < cell_idx_c(flagFieldIt.getField()->zSize()); + + if constexpr (std::is_same_v< Stencil_T, stencil::D3Q19 >) + { + // get fill level in neighboring cells + const double tmp_S = useS && isPartOfMaskSet(flagFieldIt.neighbor(0, -1, 0), validNeighborFlagMask) ? + static_cast< double >(fillFieldIt.neighbor(0, -1, 0)) : + static_cast< double >(0); + const double tmp_N = useN && isPartOfMaskSet(flagFieldIt.neighbor(0, 1, 0), validNeighborFlagMask) ? + static_cast< double >(fillFieldIt.neighbor(0, 1, 0)) : + static_cast< double >(0); + const double tmp_W = useW && isPartOfMaskSet(flagFieldIt.neighbor(-1, 0, 0), validNeighborFlagMask) ? + static_cast< double >(fillFieldIt.neighbor(-1, 0, 0)) : + static_cast< double >(0); + const double tmp_E = useE && isPartOfMaskSet(flagFieldIt.neighbor(1, 0, 0), validNeighborFlagMask) ? + static_cast< double >(fillFieldIt.neighbor(1, 0, 0)) : + static_cast< double >(0); + const double tmp_SW = useS && useW && isPartOfMaskSet(flagFieldIt.neighbor(-1, -1, 0), validNeighborFlagMask) ? + static_cast< double >(fillFieldIt.neighbor(-1, -1, 0)) : + static_cast< double >(0); + const double tmp_SE = useS && useE && isPartOfMaskSet(flagFieldIt.neighbor(1, -1, 0), validNeighborFlagMask) ? + static_cast< double >(fillFieldIt.neighbor(1, -1, 0)) : + static_cast< double >(0); + const double tmp_NW = useN && useW && isPartOfMaskSet(flagFieldIt.neighbor(-1, 1, 0), validNeighborFlagMask) ? + static_cast< double >(fillFieldIt.neighbor(-1, 1, 0)) : + static_cast< double >(0); + const double tmp_NE = useN && useE && isPartOfMaskSet(flagFieldIt.neighbor(1, 1, 0), validNeighborFlagMask) ? + static_cast< double >(fillFieldIt.neighbor(1, 1, 0)) : + static_cast< double >(0); + const double tmp_B = useB && isPartOfMaskSet(flagFieldIt.neighbor(0, 0, -1), validNeighborFlagMask) ? + static_cast< double >(fillFieldIt.neighbor(0, 0, -1)) : + static_cast< double >(0); + const double tmp_T = useT && isPartOfMaskSet(flagFieldIt.neighbor(0, 0, 1), validNeighborFlagMask) ? + static_cast< double >(fillFieldIt.neighbor(0, 0, 1)) : + static_cast< double >(0); + const double tmp_BS = useB && useS && isPartOfMaskSet(flagFieldIt.neighbor(0, -1, -1), validNeighborFlagMask) ? + static_cast< double >(fillFieldIt.neighbor(0, -1, -1)) : + static_cast< double >(0); + const double tmp_BN = useB && useN && isPartOfMaskSet(flagFieldIt.neighbor(0, 1, -1), validNeighborFlagMask) ? + static_cast< double >(fillFieldIt.neighbor(0, 1, -1)) : + static_cast< double >(0); + const double tmp_BW = useB && useW && isPartOfMaskSet(flagFieldIt.neighbor(-1, 0, -1), validNeighborFlagMask) ? + static_cast< double >(fillFieldIt.neighbor(-1, 0, -1)) : + static_cast< double >(0); + const double tmp_BE = useB && useE && isPartOfMaskSet(flagFieldIt.neighbor(1, 0, -1), validNeighborFlagMask) ? + static_cast< double >(fillFieldIt.neighbor(1, 0, -1)) : + static_cast< double >(0); + const double tmp_TS = useT && useS && isPartOfMaskSet(flagFieldIt.neighbor(0, -1, 1), validNeighborFlagMask) ? + static_cast< double >(fillFieldIt.neighbor(0, -1, 1)) : + static_cast< double >(0); + const double tmp_TN = useT && useN && isPartOfMaskSet(flagFieldIt.neighbor(0, 1, 1), validNeighborFlagMask) ? + static_cast< double >(fillFieldIt.neighbor(0, 1, 1)) : + static_cast< double >(0); + const double tmp_TW = useT && useW && isPartOfMaskSet(flagFieldIt.neighbor(-1, 0, 1), validNeighborFlagMask) ? + static_cast< double >(fillFieldIt.neighbor(-1, 0, 1)) : + static_cast< double >(0); + const double tmp_TE = useT && useE && isPartOfMaskSet(flagFieldIt.neighbor(1, 0, 1), validNeighborFlagMask) ? + static_cast< double >(fillFieldIt.neighbor(1, 0, 1)) : + static_cast< double >(0); + + // compute normal with Parker-Youngs approximation (PY) + const double weight_1 = real_c(4); + normal[0] = real_c(weight_1 * (tmp_E - tmp_W)); + normal[1] = real_c(weight_1 * (tmp_N - tmp_S)); + normal[2] = real_c(weight_1 * (tmp_T - tmp_B)); + + const double weight_2 = real_c(2); + normal[0] += real_c(weight_2 * ((tmp_NE + tmp_SE + tmp_TE + tmp_BE) - (tmp_NW + tmp_SW + tmp_TW + tmp_BW))); + normal[1] += real_c(weight_2 * ((tmp_NE + tmp_NW + tmp_TN + tmp_BN) - (tmp_SE + tmp_SW + tmp_TS + tmp_BS))); + normal[2] += real_c(weight_2 * ((tmp_TN + tmp_TS + tmp_TE + tmp_TW) - (tmp_BN + tmp_BS + tmp_BE + tmp_BW))); + } + else + { + if constexpr (std::is_same_v< Stencil_T, stencil::D3Q27 >) + { + // get fill level in neighboring cells + const double tmp_S = useS && isPartOfMaskSet(flagFieldIt.neighbor(0, -1, 0), validNeighborFlagMask) ? + static_cast< double >(fillFieldIt.neighbor(0, -1, 0)) : + static_cast< double >(0); + const double tmp_N = useN && isPartOfMaskSet(flagFieldIt.neighbor(0, 1, 0), validNeighborFlagMask) ? + static_cast< double >(fillFieldIt.neighbor(0, 1, 0)) : + static_cast< double >(0); + const double tmp_W = useW && isPartOfMaskSet(flagFieldIt.neighbor(-1, 0, 0), validNeighborFlagMask) ? + static_cast< double >(fillFieldIt.neighbor(-1, 0, 0)) : + static_cast< double >(0); + const double tmp_E = useE && isPartOfMaskSet(flagFieldIt.neighbor(1, 0, 0), validNeighborFlagMask) ? + static_cast< double >(fillFieldIt.neighbor(1, 0, 0)) : + static_cast< double >(0); + const double tmp_SW = + useS && useW && isPartOfMaskSet(flagFieldIt.neighbor(-1, -1, 0), validNeighborFlagMask) ? + static_cast< double >(fillFieldIt.neighbor(-1, -1, 0)) : + static_cast< double >(0); + const double tmp_SE = + useS && useE && isPartOfMaskSet(flagFieldIt.neighbor(1, -1, 0), validNeighborFlagMask) ? + static_cast< double >(fillFieldIt.neighbor(1, -1, 0)) : + static_cast< double >(0); + const double tmp_NW = + useN && useW && isPartOfMaskSet(flagFieldIt.neighbor(-1, 1, 0), validNeighborFlagMask) ? + static_cast< double >(fillFieldIt.neighbor(-1, 1, 0)) : + static_cast< double >(0); + const double tmp_NE = + useN && useE && isPartOfMaskSet(flagFieldIt.neighbor(1, 1, 0), validNeighborFlagMask) ? + static_cast< double >(fillFieldIt.neighbor(1, 1, 0)) : + static_cast< double >(0); + const double tmp_B = useB && isPartOfMaskSet(flagFieldIt.neighbor(0, 0, -1), validNeighborFlagMask) ? + static_cast< double >(fillFieldIt.neighbor(0, 0, -1)) : + static_cast< double >(0); + const double tmp_T = useT && isPartOfMaskSet(flagFieldIt.neighbor(0, 0, 1), validNeighborFlagMask) ? + static_cast< double >(fillFieldIt.neighbor(0, 0, 1)) : + static_cast< double >(0); + const double tmp_BS = + useB && useS && isPartOfMaskSet(flagFieldIt.neighbor(0, -1, -1), validNeighborFlagMask) ? + static_cast< double >(fillFieldIt.neighbor(0, -1, -1)) : + static_cast< double >(0); + const double tmp_BN = + useB && useN && isPartOfMaskSet(flagFieldIt.neighbor(0, 1, -1), validNeighborFlagMask) ? + static_cast< double >(fillFieldIt.neighbor(0, 1, -1)) : + static_cast< double >(0); + const double tmp_BW = + useB && useW && isPartOfMaskSet(flagFieldIt.neighbor(-1, 0, -1), validNeighborFlagMask) ? + static_cast< double >(fillFieldIt.neighbor(-1, 0, -1)) : + static_cast< double >(0); + const double tmp_BE = + useB && useE && isPartOfMaskSet(flagFieldIt.neighbor(1, 0, -1), validNeighborFlagMask) ? + static_cast< double >(fillFieldIt.neighbor(1, 0, -1)) : + static_cast< double >(0); + const double tmp_TS = + useT && useS && isPartOfMaskSet(flagFieldIt.neighbor(0, -1, 1), validNeighborFlagMask) ? + static_cast< double >(fillFieldIt.neighbor(0, -1, 1)) : + static_cast< double >(0); + const double tmp_TN = + useT && useN && isPartOfMaskSet(flagFieldIt.neighbor(0, 1, 1), validNeighborFlagMask) ? + static_cast< double >(fillFieldIt.neighbor(0, 1, 1)) : + static_cast< double >(0); + const double tmp_TW = + useT && useW && isPartOfMaskSet(flagFieldIt.neighbor(-1, 0, 1), validNeighborFlagMask) ? + static_cast< double >(fillFieldIt.neighbor(-1, 0, 1)) : + static_cast< double >(0); + const double tmp_TE = + useT && useE && isPartOfMaskSet(flagFieldIt.neighbor(1, 0, 1), validNeighborFlagMask) ? + static_cast< double >(fillFieldIt.neighbor(1, 0, 1)) : + static_cast< double >(0); + const double tmp_BSW = + useB && useS && useW && isPartOfMaskSet(flagFieldIt.neighbor(-1, -1, -1), validNeighborFlagMask) ? + static_cast< double >(fillFieldIt.neighbor(-1, -1, -1)) : + static_cast< double >(0); + const double tmp_BNW = + useB && useN && useW && isPartOfMaskSet(flagFieldIt.neighbor(-1, 1, -1), validNeighborFlagMask) ? + static_cast< double >(fillFieldIt.neighbor(-1, 1, -1)) : + static_cast< double >(0); + const double tmp_BSE = + useB && useS && useE && isPartOfMaskSet(flagFieldIt.neighbor(1, -1, -1), validNeighborFlagMask) ? + static_cast< double >(fillFieldIt.neighbor(1, -1, -1)) : + static_cast< double >(0); + const double tmp_BNE = + useB && useN && useE && isPartOfMaskSet(flagFieldIt.neighbor(1, 1, -1), validNeighborFlagMask) ? + static_cast< double >(fillFieldIt.neighbor(1, 1, -1)) : + static_cast< double >(0); + const double tmp_TSW = + useT && useS && useW && isPartOfMaskSet(flagFieldIt.neighbor(-1, -1, 1), validNeighborFlagMask) ? + static_cast< double >(fillFieldIt.neighbor(-1, -1, 1)) : + static_cast< double >(0); + const double tmp_TNW = + useT && useN && useW && isPartOfMaskSet(flagFieldIt.neighbor(-1, 1, 1), validNeighborFlagMask) ? + static_cast< double >(fillFieldIt.neighbor(-1, 1, 1)) : + static_cast< double >(0); + const double tmp_TSE = + useT && useS && useE && isPartOfMaskSet(flagFieldIt.neighbor(1, -1, 1), validNeighborFlagMask) ? + static_cast< double >(fillFieldIt.neighbor(1, -1, 1)) : + static_cast< double >(0); + const double tmp_TNE = + useT && useN && useE && isPartOfMaskSet(flagFieldIt.neighbor(1, 1, 1), validNeighborFlagMask) ? + static_cast< double >(fillFieldIt.neighbor(1, 1, 1)) : + static_cast< double >(0); + + // compute normal with Parker-Youngs approximation (PY) + const double weight_1 = real_c(4); + normal[0] = real_c(weight_1 * (tmp_E - tmp_W)); + normal[1] = real_c(weight_1 * (tmp_N - tmp_S)); + normal[2] = real_c(weight_1 * (tmp_T - tmp_B)); + + const double weight_2 = real_c(2); + normal[0] += real_c(weight_2 * ((tmp_NE + tmp_SE + tmp_TE + tmp_BE) - (tmp_NW + tmp_SW + tmp_TW + tmp_BW))); + normal[1] += real_c(weight_2 * ((tmp_NE + tmp_NW + tmp_TN + tmp_BN) - (tmp_SE + tmp_SW + tmp_TS + tmp_BS))); + normal[2] += real_c(weight_2 * ((tmp_TN + tmp_TS + tmp_TE + tmp_TW) - (tmp_BN + tmp_BS + tmp_BE + tmp_BW))); + + // weight (=1) corresponding to Parker-Youngs approximation + normal[0] += real_c((tmp_TNE + tmp_TSE + tmp_BNE + tmp_BSE) - (tmp_TNW + tmp_TSW + tmp_BNW + tmp_BSW)); + normal[1] += real_c((tmp_TNE + tmp_TNW + tmp_BNE + tmp_BNW) - (tmp_TSE + tmp_TSW + tmp_BSE + tmp_BSW)); + normal[2] += real_c((tmp_TNE + tmp_TNW + tmp_TSE + tmp_TSW) - (tmp_BNE + tmp_BNW + tmp_BSE + tmp_BSW)); + } + else { WALBERLA_ABORT("The chosen stencil type is not implemented in computeNormal()."); } + } + } +} + +template< typename Stencil_T, typename vector_t, typename ScalarFieldIt_T, typename FlagFieldIt_T, typename flag_t > +void computeNormalNearSolidBoundary(vector_t& normal, const ScalarFieldIt_T& fillFieldIt, + const FlagFieldIt_T& flagFieldIt, const flag_t& validNeighborFlagMask, + const flag_t& obstacleFlagMask) +{ + Vector3< real_t > midPoint(real_c(0)); + + // construct the virtual midpoint + for (auto dir = Stencil_T::beginNoCenter(); dir != Stencil_T::end(); ++dir) + { + if (isPartOfMaskSet(flagFieldIt.neighbor(*dir), validNeighborFlagMask) && + isPartOfMaskSet(flagFieldIt.neighbor(dir.inverseDir()), obstacleFlagMask)) + { + if constexpr (Stencil_T::D == uint_t(2)) + { + midPoint[0] += real_c(dir.cx()) * + real_c(stencil::gaussianMultipliers[stencil::D3Q27::idx[stencil::map2Dto3D[2][*dir]]]); + midPoint[1] += real_c(dir.cy()) * + real_c(stencil::gaussianMultipliers[stencil::D3Q27::idx[stencil::map2Dto3D[2][*dir]]]); + midPoint[2] += real_c(0); + } + else + { + midPoint[0] += real_c(dir.cx()) * real_c(stencil::gaussianMultipliers[dir.toIdx()]); + midPoint[1] += real_c(dir.cy()) * real_c(stencil::gaussianMultipliers[dir.toIdx()]); + midPoint[2] += real_c(dir.cz()) * real_c(stencil::gaussianMultipliers[dir.toIdx()]); + } + } + } + + // restrict the displacement of the virtual midpoint to an absolute value of 0.5 + for (uint_t i = uint_c(0); i != uint_c(3); ++i) + { + if (midPoint[i] > real_c(0.0)) { midPoint[i] = real_c(0.5); } + else + { + if (midPoint[i] < real_c(0.0)) { midPoint[i] = real_c(-0.5); } + // else midPoint[i] == 0 + } + } + + normal.set(real_c(0), real_c(0), real_c(0)); + + // use standard Parker-Youngs approximation (PY) for all cells without solid boundary neighbors + // otherwise shift neighboring cell by virtual midpoint (also referred to as narrower PY) + for (auto dir = Stencil_T::beginNoCenter(); dir != Stencil_T::end(); ++dir) + { + // skip directions that have obstacleFlagMask set in this and the opposing direction; this is NOT documented in + // literature, however, it avoids that an undefined fill level is used from the wall cells + if (isPartOfMaskSet(flagFieldIt.neighbor(*dir), obstacleFlagMask) && + isPartOfMaskSet(flagFieldIt.neighbor(dir.inverseDir()), obstacleFlagMask)) + { + continue; + } + + cell_idx_t modCx = dir.cx(); + cell_idx_t modCy = dir.cy(); + cell_idx_t modCz = dir.cz(); + + // shift neighboring cells by midpoint if they are solid boundary cells + if (isPartOfMaskSet(flagFieldIt.neighbor(*dir), obstacleFlagMask) || + isPartOfMaskSet(flagFieldIt.neighbor(dir.inverseDir()), obstacleFlagMask)) + { + // truncate cells towards 0, i.e., make the access pattern narrower + modCx = cell_idx_c(real_c(modCx) + midPoint[0]); + modCy = cell_idx_c(real_c(modCy) + midPoint[1]); + modCz = cell_idx_c(real_c(modCz) + midPoint[2]); + } + + real_t fill; + + if (isPartOfMaskSet(flagFieldIt.neighbor(modCx, modCy, modCz), validNeighborFlagMask)) + { + // compute normal with formula from regular Parker-Youngs approximation + if constexpr (Stencil_T::D == uint_t(2)) + { + fill = fillFieldIt.neighbor(modCx, modCy, modCz) * + real_c(stencil::gaussianMultipliers[stencil::D3Q27::idx[stencil::map2Dto3D[2][*dir]]]); + } + else { fill = fillFieldIt.neighbor(modCx, modCy, modCz) * real_c(stencil::gaussianMultipliers[dir.toIdx()]); } + + normal[0] += real_c(dir.cx()) * fill; + normal[1] += real_c(dir.cy()) * fill; + normal[2] += real_c(dir.cz()) * fill; + } + else { normal = Vector3< real_t >(real_c(0)); } + } +} +} // namespace normal_computation +} // namespace free_surface +} // namespace walberla diff --git a/src/lbm/free_surface/surface_geometry/ObstacleFillLevelSweep.h b/src/lbm/free_surface/surface_geometry/ObstacleFillLevelSweep.h new file mode 100644 index 000000000..c5bd8c858 --- /dev/null +++ b/src/lbm/free_surface/surface_geometry/ObstacleFillLevelSweep.h @@ -0,0 +1,91 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file ObstacleFillLevelSweep.h +//! \ingroup surface_geometry +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Reflect fill levels into obstacle cells (for finite difference curvature computation). +// +//====================================================================================================================== + +#pragma once + +#include "blockforest/StructuredBlockForest.h" + +#include "domain_decomposition/BlockDataID.h" + +namespace walberla +{ +namespace free_surface +{ +/*********************************************************************************************************************** + * Reflect fill levels into obstacle cells by averaging the fill levels from fluid cells with weights according to the + * surface normal. + * + * See dissertation of S. Bogner, 2017 (section 4.4.2.1). + * + * IMPORTANT REMARK: If an obstacle is located in a non-periodic outermost ghost layer, the fill level field must have + * two ghost layers. That is, the ObstacleFillLevelSweep computes the fill level obstacle of cells located in the + * outermost global ghost layer. For this, the all neighboring cells' fill levels are required. + * A single ghost layer is not sufficient, because the computed values by ObstacleFillLevelSweep (located in an + * outermost global ghost layer) are not communicated themselves. In the example below, the values A, D, E, and H are + * located in a global outermost ghost layer. Only directions without # shall be communicated and * marks ghost layers + * in the directions to be communicated. In this example, only B, C, F, and G will be communicated as expected. In + * contrast, A, D, E, and H will not be communicated. + * + * Block 1 Block 2 Block 1 Block 2 + * ###### ###### ###### ###### + * # A |* *| E # # A |* *| E # + * # ---- -----# # ---- -----# + * # B |* *| F # ===> communication # B |F B| F # + * # C |* *| G # # C |G C| G # + * # ---- -----# # ---- -----# + * # D |* *| H # # D |* *| H # + * ###### ###### ###### ###### + **********************************************************************************************************************/ +template< typename Stencil_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T > +class ObstacleFillLevelSweep +{ + protected: + using FlagUIDSet = Set< FlagUID >; + + using vector_t = typename std::remove_const< typename VectorField_T::value_type >::type; + using flag_t = typename std::remove_const< typename FlagField_T::value_type >::type; + + public: + ObstacleFillLevelSweep(const BlockDataID& fillFieldDstID, const ConstBlockDataID& fillFieldSrcID, + const ConstBlockDataID& flagFieldID, const ConstBlockDataID& obstacleNormalFieldID, + const FlagUIDSet& liquidInterfaceGasFlagIDSet, const FlagUIDSet& obstacleFlagIDSet) + : fillFieldDstID_(fillFieldDstID), fillFieldSrcID_(fillFieldSrcID), flagFieldID_(flagFieldID), + obstacleNormalFieldID_(obstacleNormalFieldID), liquidInterfaceGasFlagIDSet_(liquidInterfaceGasFlagIDSet), + obstacleFlagIDSet_(obstacleFlagIDSet) + {} + + void operator()(IBlock* const block); + + private: + BlockDataID fillFieldDstID_; + ConstBlockDataID fillFieldSrcID_; + ConstBlockDataID flagFieldID_; + ConstBlockDataID obstacleNormalFieldID_; + + FlagUIDSet liquidInterfaceGasFlagIDSet_; + FlagUIDSet obstacleFlagIDSet_; +}; // class ObstacleFillLevelSweep + +} // namespace free_surface +} // namespace walberla + +#include "ObstacleFillLevelSweep.impl.h" \ No newline at end of file diff --git a/src/lbm/free_surface/surface_geometry/ObstacleFillLevelSweep.impl.h b/src/lbm/free_surface/surface_geometry/ObstacleFillLevelSweep.impl.h new file mode 100644 index 000000000..7d4182022 --- /dev/null +++ b/src/lbm/free_surface/surface_geometry/ObstacleFillLevelSweep.impl.h @@ -0,0 +1,88 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file ObstacleFillLevelSweep.h +//! \ingroup surface_geometry +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Reflect fill levels into obstacle cells (for finite difference curvature computation). +// +//====================================================================================================================== + +#include "core/debug/CheckFunctions.h" +#include "core/math/Utility.h" +#include "core/math/Vector3.h" + +#include "field/FlagField.h" + +#include <algorithm> +#include <cmath> + +namespace walberla +{ +namespace free_surface +{ +template< typename Stencil_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T > +void ObstacleFillLevelSweep< Stencil_T, FlagField_T, ScalarField_T, VectorField_T >::operator()(IBlock* const block) +{ + // get fields + const ScalarField_T* const fillFieldSrc = block->getData< const ScalarField_T >(fillFieldSrcID_); + ScalarField_T* const fillFieldDst = block->getData< ScalarField_T >(fillFieldDstID_); + const FlagField_T* const flagField = block->getData< const FlagField_T >(flagFieldID_); + const VectorField_T* const obstacleNormalField = block->getData< const VectorField_T >(obstacleNormalFieldID_); + + // get flags + const flag_t liquidInterfaceGasFlagMask = flagField->getMask(liquidInterfaceGasFlagIDSet_); + const flag_t obstacleFlagMask = flagField->getMask(obstacleFlagIDSet_); + + // equation (4.22) in dissertation of S. Bogner, 2017 (section 4.4.2.1); include ghost layer because solid cells + // might be located in the (outermost global) ghost layer + WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ(fillFieldDst, uint_c(1), { + // IMPORTANT REMARK: do not restrict this algorithm to obstacle cells that are direct neighbors of interface + // cells; the succeeding SmoothingSweep uses the values computed here and must be executed for an at least + // two-cell neighborhood of interface cells + const typename FlagField_T::ConstPtr flagFieldPtr(*flagField, x, y, z); + const typename ScalarField_T::ConstPtr fillFieldSrcPtr(*fillFieldSrc, x, y, z); + const typename ScalarField_T::Ptr fillFieldDstPtr(*fillFieldDst, x, y, z); + + if (isPartOfMaskSet(flagFieldPtr, obstacleFlagMask) && + isFlagInNeighborhood< Stencil_T >(flagFieldPtr, liquidInterfaceGasFlagMask)) + { + WALBERLA_CHECK_GREATER(obstacleNormalField->get(x, y, z).length(), real_c(0), + "An obstacleNormal of an obstacle cell was found to be zero in obstacleNormalSweep. " + "This is not plausible."); + + real_t sum = real_c(0); + real_t weightSum = real_c(0); + for (auto dir = Stencil_T::beginNoCenter(); dir != Stencil_T::end(); ++dir) + { + if (isPartOfMaskSet(flagFieldPtr.neighbor(*dir), liquidInterfaceGasFlagMask)) + { + const Vector3< real_t > dirVector = + Vector3< real_t >(real_c(dir.cx()), real_c(dir.cy()), real_c(dir.cz())).getNormalized(); + + const real_t weight = std::abs(obstacleNormalField->get(x, y, z) * dirVector); + + sum += weight * fillFieldSrcPtr.neighbor(*dir); + + weightSum += weight; + } + } + + *fillFieldDstPtr = weightSum > real_c(0) ? sum / weightSum : real_c(0); + } + }) // WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ +} +} // namespace free_surface +} // namespace walberla diff --git a/src/lbm/free_surface/surface_geometry/ObstacleNormalSweep.h b/src/lbm/free_surface/surface_geometry/ObstacleNormalSweep.h new file mode 100644 index 000000000..cb1c6b01d --- /dev/null +++ b/src/lbm/free_surface/surface_geometry/ObstacleNormalSweep.h @@ -0,0 +1,98 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file ObstacleNormalSweep.h +//! \ingroup surface_geometry +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Compute a mean obstacle normal in interface cells near solid boundary cells. +// +//====================================================================================================================== + +#pragma once + +#include "core/logging/Logging.h" + +#include "domain_decomposition/BlockDataID.h" + +#include "field/FlagField.h" + +namespace walberla +{ +namespace free_surface +{ +/*********************************************************************************************************************** + * Compute a mean obstacle normal in interface cells near obstacle cells, and/or in obstacle cells. This reduces the + * influence of a stair-case approximated wall in the wetting model. + * + * - computeInInterfaceCells: Compute the obstacle normal in interface cells. Required when using curvature computation + * based on local triangulation (not with finite difference method). + * - computeInObstacleCells: Compute the obstacle normal in obstacle cells. Required when using curvature computation + * based on the finite difference method (not with local triangulation). + * + * Details can be found in the dissertation of S. Donath, 2011 section 6.3.5.2. + **********************************************************************************************************************/ +template< typename Stencil_T, typename FlagField_T, typename VectorField_T > +class ObstacleNormalSweep +{ + protected: + using vector_t = typename std::remove_const< typename VectorField_T::value_type >::type; + using flag_t = typename std::remove_const< typename FlagField_T::value_type >::type; + + public: + ObstacleNormalSweep(const BlockDataID& obstacleNormalFieldID, const ConstBlockDataID& flagFieldID, + const FlagUID& interfaceFlagID, const Set< FlagUID >& liquidInterfaceGasFlagIDSet, + const Set< FlagUID >& obstacleFlagIDSet, bool computeInInterfaceCells, + bool computeInObstacleCells, bool computeInGhostLayer) + : obstacleNormalFieldID_(obstacleNormalFieldID), flagFieldID_(flagFieldID), interfaceFlagID_(interfaceFlagID), + liquidInterfaceGasFlagIDSet_(liquidInterfaceGasFlagIDSet), obstacleFlagIDSet_(obstacleFlagIDSet), + computeInInterfaceCells_(computeInInterfaceCells), computeInObstacleCells_(computeInObstacleCells), + computeInGhostLayer_(computeInGhostLayer) + { + if (!computeInInterfaceCells_ && !computeInObstacleCells_) + { + WALBERLA_LOG_WARNING_ON_ROOT( + "In ObstacleNormalSweep, you specified to neither compute the obstacle normal in interface cells, nor in " + "obstacle cells. That is, ObstacleNormalSweep will do nothing. Please check if this is what you really " + "want."); + } + } + + void operator()(IBlock* const block); + + private: + template< typename FlagFieldIt_T > + void computeObstacleNormalInInterfaceCell(vector_t& obstacleNormal, const FlagFieldIt_T& flagFieldIt, + const flag_t& validNeighborFlagMask); + + template< typename FlagFieldIt_T > + void computeObstacleNormalInObstacleCell(vector_t& obstacleNormal, const FlagFieldIt_T& flagFieldIt, + const flag_t& liquidInterfaceGasFlagMask); + + BlockDataID obstacleNormalFieldID_; + ConstBlockDataID flagFieldID_; + + FlagUID interfaceFlagID_; + Set< FlagUID > liquidInterfaceGasFlagIDSet_; + Set< FlagUID > obstacleFlagIDSet_; + + bool computeInInterfaceCells_; + bool computeInObstacleCells_; + bool computeInGhostLayer_; +}; // class ObstacleNormalSweep + +} // namespace free_surface +} // namespace walberla + +#include "ObstacleNormalSweep.impl.h" \ No newline at end of file diff --git a/src/lbm/free_surface/surface_geometry/ObstacleNormalSweep.impl.h b/src/lbm/free_surface/surface_geometry/ObstacleNormalSweep.impl.h new file mode 100644 index 000000000..6d3b72258 --- /dev/null +++ b/src/lbm/free_surface/surface_geometry/ObstacleNormalSweep.impl.h @@ -0,0 +1,147 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file ObstacleNormalSweep.impl.h +//! \ingroup surface_geometry +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Compute a mean obstacle normal in interface cells near solid boundary cells. +// +//====================================================================================================================== + +#include "core/math/Vector3.h" + +#include "field/iterators/IteratorMacros.h" + +#include "stencil/D3Q27.h" + +#include "ObstacleNormalSweep.h" + +namespace walberla +{ +namespace free_surface +{ +template< typename Stencil_T, typename FlagField_T, typename VectorField_T > +void ObstacleNormalSweep< Stencil_T, FlagField_T, VectorField_T >::operator()(IBlock* const block) +{ + // do nothing if obstacle normal must not be computed anywhere + if (!computeInInterfaceCells_ && !computeInObstacleCells_) { return; } + + // fetch fields + VectorField_T* const obstacleNormalField = block->getData< VectorField_T >(obstacleNormalFieldID_); + const FlagField_T* const flagField = block->getData< const FlagField_T >(flagFieldID_); + + // two ghost layers are required in the flag field + WALBERLA_ASSERT_EQUAL(flagField->nrOfGhostLayers(), uint_c(2)); + + // get flags + const flag_t interfaceFlag = flagField->getFlag(interfaceFlagID_); + const flag_t liquidInterfaceGasFlagMask = flagField->getMask(liquidInterfaceGasFlagIDSet_); + const flag_t obstacleFlagMask = flagField->getMask(obstacleFlagIDSet_); + + // include ghost layer because solid cells might be located in the (outermost global) ghost layer + WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ(obstacleNormalField, uint_c(1), { + if (!computeInGhostLayer_ && (!flagField->isInInnerPart(Cell(x, y, z)))) { continue; } + + const typename FlagField_T::ConstPtr flagFieldPtr(*flagField, x, y, z); + + const bool computeInInterfaceCell = computeInInterfaceCells_ && isPartOfMaskSet(flagFieldPtr, interfaceFlag) && + isFlagInNeighborhood< Stencil_T >(flagFieldPtr, obstacleFlagMask); + + const bool computeInObstacleCell = computeInObstacleCells_ && isPartOfMaskSet(flagFieldPtr, obstacleFlagMask) && + isFlagInNeighborhood< Stencil_T >(flagFieldPtr, liquidInterfaceGasFlagMask); + + // IMPORTANT REMARK: do not restrict this algorithm to obstacle cells that are direct neighbors of interface + // cells; the succeeding ObstacleFillLevelSweep and SmoothingSweep use the values computed here and the latter + // must work on an at least two-cell neighborhood of interface cells + + vector_t& obstacleNormal = obstacleNormalField->get(x, y, z); + + if (computeInInterfaceCell) + { + WALBERLA_ASSERT(!computeInObstacleCell); + + // compute mean obstacle, i.e., mean wall normal in interface cell (see dissertation of S. Donath, 2011, + // section 6.3.5.2) + computeObstacleNormalInInterfaceCell(obstacleNormal, flagFieldPtr, obstacleFlagMask); + } + else + { + if (computeInObstacleCell) + { + WALBERLA_ASSERT(!computeInInterfaceCell); + + // compute mean obstacle normal in obstacle cell + computeObstacleNormalInObstacleCell(obstacleNormal, flagFieldPtr, liquidInterfaceGasFlagMask); + } + else + { + // set obstacle normal of all other cells to zero + obstacleNormal.set(real_c(0), real_c(0), real_c(0)); + } + } + + // normalize mean obstacle normal + const real_t sqrObstNormal = obstacleNormal.sqrLength(); + if (sqrObstNormal > real_c(0)) + { + const real_t invlength = -real_c(1) / real_c(std::sqrt(sqrObstNormal)); + obstacleNormal *= invlength; + } + }); // WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ +} + +template< typename Stencil_T, typename FlagField_T, typename VectorField_T > +template< typename FlagFieldIt_T > +void ObstacleNormalSweep< Stencil_T, FlagField_T, VectorField_T >::computeObstacleNormalInInterfaceCell( + vector_t& obstacleNormal, const FlagFieldIt_T& flagFieldIt, const flag_t& obstacleFlagMask) +{ + uint_t obstCount = uint_c(0); + obstacleNormal = vector_t(real_c(0)); + + for (auto i = Stencil_T::beginNoCenter(); i != Stencil_T::end(); ++i) + { + // only consider directions in which there is an obstacle cell + if (isPartOfMaskSet(flagFieldIt.neighbor(*i), obstacleFlagMask)) + { + obstacleNormal += vector_t(real_c(-i.cx()), real_c(-i.cy()), real_c(-i.cz())); + ++obstCount; + } + } + obstacleNormal = obstCount > uint_c(0) ? obstacleNormal / real_c(obstCount) : vector_t(real_c(0)); +} + +template< typename Stencil_T, typename FlagField_T, typename VectorField_T > +template< typename FlagFieldIt_T > +void ObstacleNormalSweep< Stencil_T, FlagField_T, VectorField_T >::computeObstacleNormalInObstacleCell( + vector_t& obstacleNormal, const FlagFieldIt_T& flagFieldIt, const flag_t& liquidInterfaceGasFlagMask) +{ + uint_t obstCount = uint_c(0); + obstacleNormal = vector_t(real_c(0)); + + for (auto i = Stencil_T::beginNoCenter(); i != Stencil_T::end(); ++i) + { + // only consider directions in which there is a liquid, interface, or gas cell + if (isPartOfMaskSet(flagFieldIt.neighbor(*i), liquidInterfaceGasFlagMask)) + { + obstacleNormal += vector_t(real_c(i.cx()), real_c(i.cy()), real_c(i.cz())); + + ++obstCount; + } + } + obstacleNormal = obstCount > uint_c(0) ? obstacleNormal / real_c(obstCount) : vector_t(real_c(0)); +} + +} // namespace free_surface +} // namespace walberla diff --git a/src/lbm/free_surface/surface_geometry/SmoothingSweep.h b/src/lbm/free_surface/surface_geometry/SmoothingSweep.h new file mode 100644 index 000000000..9b76ffd20 --- /dev/null +++ b/src/lbm/free_surface/surface_geometry/SmoothingSweep.h @@ -0,0 +1,113 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file SmoothingSweep.h +//! \ingroup surface_geometry +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Smooth fill levels (used for finite difference curvature computation). +// +//====================================================================================================================== + +#pragma once + +#include "blockforest/StructuredBlockForest.h" + +#include "domain_decomposition/BlockDataID.h" + +namespace walberla +{ +namespace free_surface +{ +// forward declaration +template< typename Stencil_T > +class KernelK8; + +/*********************************************************************************************************************** + * Smooth fill levels such that interface-neighboring cells get assigned a new fill level. This is required for + * computing the interface curvature using the finite difference method. + * + * The same smoothing kernel is used as in the dissertation of S. Bogner, 2017, i.e., the K8 kernel with support + * radius 2 from + * Williams, Kothe and Puckett, "Accuracy and Convergence of Continuum Surface Tension Models", 1998. + **********************************************************************************************************************/ +template< typename Stencil_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T > +class SmoothingSweep +{ + protected: + using vector_t = typename std::remove_const< typename VectorField_T::value_type >::type; + using flag_t = typename std::remove_const< typename FlagField_T::value_type >::type; + + public: + SmoothingSweep(const BlockDataID& smoothFillFieldID, const ConstBlockDataID& fillFieldID, + const ConstBlockDataID& flagFieldID, const Set< FlagUID >& liquidInterfaceGasFlagIDSet, + const Set< FlagUID >& obstacleFlagIDSet, bool includeObstacleNeighbors) + : smoothFillFieldID_(smoothFillFieldID), fillFieldID_(fillFieldID), flagFieldID_(flagFieldID), + liquidInterfaceGasFlagIDSet_(liquidInterfaceGasFlagIDSet), obstacleFlagIDSet_(obstacleFlagIDSet), + includeObstacleNeighbors_(includeObstacleNeighbors), smoothingKernel_(KernelK8< Stencil_T >(real_c(2.0))) + {} + + void operator()(IBlock* const block); + + private: + BlockDataID smoothFillFieldID_; + ConstBlockDataID fillFieldID_; + ConstBlockDataID flagFieldID_; + + Set< FlagUID > liquidInterfaceGasFlagIDSet_; + Set< FlagUID > obstacleFlagIDSet_; + + bool includeObstacleNeighbors_; + + KernelK8< Stencil_T > smoothingKernel_; +}; // class SmoothingSweep + +/*********************************************************************************************************************** + * K8 kernel from Williams, Kothe and Puckett, "Accuracy and Convergence of Continuum Surface Tension Models", 1998. + **********************************************************************************************************************/ +template< typename Stencil_T > +class KernelK8 +{ + public: + KernelK8(real_t epsilon) : epsilon_(epsilon) { stencilSize_ = uint_c(std::ceil(epsilon_) - real_c(1)); } + + // equation (11) in Williams et al. (normalization constant A=1 here, result must be normalized outside the kernel) + inline real_t kernelFunction(const Vector3< real_t >& dirVec) const + { + const real_t r_sqr = dirVec.sqrLength(); + const real_t eps_sqr = epsilon_ * epsilon_; + + if (r_sqr < eps_sqr) + { + real_t result = real_c(1.0) - (r_sqr / (eps_sqr)); + result = result * result * result * result; + + return result; + } + else { return real_c(0.0); } + } + + inline real_t getSupportRadius() const { return epsilon_; } + inline uint_t getStencilSize() const { return static_cast< uint_t >(stencilSize_); } + + private: + real_t epsilon_; // support radius of the kernel + uint_t stencilSize_; // size of the stencil which defines included neighbors in smoothing + +}; // class KernelK8 + +} // namespace free_surface +} // namespace walberla + +#include "SmoothingSweep.impl.h" \ No newline at end of file diff --git a/src/lbm/free_surface/surface_geometry/SmoothingSweep.impl.h b/src/lbm/free_surface/surface_geometry/SmoothingSweep.impl.h new file mode 100644 index 000000000..3b61d23d2 --- /dev/null +++ b/src/lbm/free_surface/surface_geometry/SmoothingSweep.impl.h @@ -0,0 +1,159 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file SmoothingSweep.impl.h +//! \ingroup surface_geometry +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Smooth fill levels (used for finite difference curvature computation). +// +//====================================================================================================================== + +#include "core/debug/CheckFunctions.h" +#include "core/math/Utility.h" +#include "core/math/Vector3.h" + +#include "field/FlagField.h" + +#include <algorithm> +#include <cmath> + +#include "ContactAngle.h" +#include "SmoothingSweep.h" +#include "Utility.h" + +namespace walberla +{ +namespace free_surface +{ +template< typename Stencil_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T > +void SmoothingSweep< Stencil_T, FlagField_T, ScalarField_T, VectorField_T >::operator()(IBlock* const block) +{ + // get fields + ScalarField_T* const smoothFillField = block->getData< ScalarField_T >(smoothFillFieldID_); + const ScalarField_T* const fillField = block->getData< const ScalarField_T >(fillFieldID_); + const FlagField_T* const flagField = block->getData< const FlagField_T >(flagFieldID_); + + // get flags + const flag_t liquidInterfaceGasFlagMask = flagField->getMask(liquidInterfaceGasFlagIDSet_); + const flag_t obstacleFlagMask = flagField->getMask(obstacleFlagIDSet_); + + // const KernelK8< Stencil_T > smoothingKernel(real_c(2.0)); + + const uint_t kernelSize = smoothingKernel_.getStencilSize(); + + WALBERLA_CHECK_GREATER_EQUAL( + smoothFillField->nrOfGhostLayers(), kernelSize, + "Support radius of smoothing kernel results in a smoothing stencil size that exceeds the ghost layers."); + + // including ghost layers is not necessary, even if solid cells are located in the (outermost global) ghost layer, + // because fill level in obstacle cells is set by ObstacleFillLevelSweep + WALBERLA_FOR_ALL_CELLS(smoothFillFieldIt, smoothFillField, fillFieldIt, fillField, flagFieldIt, flagField, { + // IMPORTANT REMARK: do not restrict this algorithm to interface cells and their neighbors; when the normals are + // computed in the neighborhood of interface cells, the second neighbors of interface cells are also required + + // mollify fill level in interface, liquid, and gas according to equation (9) in Williams et al. + if (isPartOfMaskSet(flagFieldIt, liquidInterfaceGasFlagMask)) + { + real_t normalizationConstant = real_c(0); + real_t smoothedFillLevel = real_c(0); + if constexpr (Stencil_T::D == uint_t(2)) + { + for (int j = -int_c(kernelSize); j <= int_c(kernelSize); ++j) + { + for (int i = -int_c(kernelSize); i <= int_c(kernelSize); ++i) + { + const Vector3< real_t > dirVector(real_c(i), real_c(j), real_c(0)); + + if (isPartOfMaskSet(flagFieldIt.neighbor(cell_idx_c(i), cell_idx_c(j), cell_idx_c(0)), + obstacleFlagMask)) + { + if (includeObstacleNeighbors_) + { + // in solid cells, use values from smoothed fill field (instead of regular fill field) that have + // been set by ObstacleFillLevelSweep + smoothedFillLevel += smoothingKernel_.kernelFunction(dirVector) * + smoothFillFieldIt.neighbor(cell_idx_c(i), cell_idx_c(j), cell_idx_c(0)); + + if (dirVector.length() < smoothingKernel_.getSupportRadius()) + { + normalizationConstant += smoothingKernel_.kernelFunction(dirVector); + } + } // else: do not include this direction in smoothing + } + else + { + smoothedFillLevel += smoothingKernel_.kernelFunction(dirVector) * + fillFieldIt.neighbor(cell_idx_c(i), cell_idx_c(j), cell_idx_c(0)); + + if (dirVector.length() < smoothingKernel_.getSupportRadius()) + { + normalizationConstant += smoothingKernel_.kernelFunction(dirVector); + } + } + } + } + } + else + { + if constexpr (Stencil_T::D == uint_t(3)) + { + for (int k = -int_c(kernelSize); k <= int_c(kernelSize); ++k) + { + for (int j = -int_c(kernelSize); j <= int_c(kernelSize); ++j) + { + for (int i = -int_c(kernelSize); i <= int_c(kernelSize); ++i) + { + const Vector3< real_t > dirVector(real_c(i), real_c(j), real_c(k)); + + if (isPartOfMaskSet(flagFieldIt.neighbor(cell_idx_c(i), cell_idx_c(j), cell_idx_c(k)), + obstacleFlagMask)) + { + if (includeObstacleNeighbors_) + { + // in solid cells, use values from smoothed fill field (instead of regular fill field) + // that have been set by ObstacleFillLevelSweep + smoothedFillLevel += + smoothingKernel_.kernelFunction(dirVector) * + smoothFillFieldIt.neighbor(cell_idx_c(i), cell_idx_c(j), cell_idx_c(k)); + + if (dirVector.length() < smoothingKernel_.getSupportRadius()) + { + normalizationConstant += smoothingKernel_.kernelFunction(dirVector); + } + } // else: do not include this direction in smoothing + } + else + { + smoothedFillLevel += smoothingKernel_.kernelFunction(dirVector) * + fillFieldIt.neighbor(cell_idx_c(i), cell_idx_c(j), cell_idx_c(k)); + + if (dirVector.length() < smoothingKernel_.getSupportRadius()) + { + normalizationConstant += smoothingKernel_.kernelFunction(dirVector); + } + } + } + } + } + } + } + + smoothedFillLevel /= normalizationConstant; + *smoothFillFieldIt = smoothedFillLevel; + } + }) // WALBERLA_FOR_ALL_CELLS +} +} // namespace free_surface +} // namespace walberla diff --git a/src/lbm/free_surface/surface_geometry/SurfaceGeometryHandler.h b/src/lbm/free_surface/surface_geometry/SurfaceGeometryHandler.h new file mode 100644 index 000000000..f0f21c2e6 --- /dev/null +++ b/src/lbm/free_surface/surface_geometry/SurfaceGeometryHandler.h @@ -0,0 +1,168 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file SurfaceGeometryHandler.h +//! \ingroup surface_geometry +//! \author Matthias Markl <matthias.markl@fau.de> +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Handles the surface geometry (normal and curvature computation) by creating fields and adding sweeps. +// +//====================================================================================================================== + +#pragma once + +#include "core/StringUtility.h" + +#include "domain_decomposition/StructuredBlockStorage.h" + +#include "field/AddToStorage.h" +#include "field/FlagField.h" + +#include "lbm/blockforest/communication/SimpleCommunication.h" +#include "lbm/free_surface/BlockStateDetectorSweep.h" +#include "lbm/free_surface/FlagInfo.h" + +#include "stencil/D2Q9.h" +#include "stencil/D3Q27.h" + +#include "timeloop/SweepTimeloop.h" + +#include <type_traits> +#include <vector> + +#include "CurvatureModel.h" + +namespace walberla +{ +namespace free_surface +{ +/*********************************************************************************************************************** + * Handles the surface geometry (normal and curvature computation) by creating fields and adding sweeps. + **********************************************************************************************************************/ +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T > +class SurfaceGeometryHandler +{ + protected: + using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >; + using vector_t = typename std::remove_const< typename VectorField_T::value_type >::type; + + // explicitly use either D2Q9 or D3Q27 here, as the geometry operations require (or are most accurate with) the full + // neighborhood; + using Stencil_T = + typename std::conditional< LatticeModel_T::Stencil::D == uint_t(2), stencil::D2Q9, stencil::D3Q27 >::type; + using Communication_T = blockforest::SimpleCommunication< Stencil_T >; + using StateSweep = BlockStateDetectorSweep< FlagField_T >; // used in friend classes + + public: + SurfaceGeometryHandler(const std::shared_ptr< StructuredBlockForest >& blockForest, + const std::shared_ptr< FreeSurfaceBoundaryHandling_T >& freeSurfaceBoundaryHandling, + const BlockDataID& fillFieldID, const std::string& curvatureModel, bool computeCurvature, + bool enableWetting, real_t contactAngleInDegrees) + : blockForest_(blockForest), freeSurfaceBoundaryHandling_(freeSurfaceBoundaryHandling), fillFieldID_(fillFieldID), + curvatureModel_(curvatureModel), computeCurvature_(computeCurvature), enableWetting_(enableWetting), + contactAngle_(ContactAngle(contactAngleInDegrees)) + { + curvatureFieldID_ = + field::addToStorage< ScalarField_T >(blockForest_, "Curvature field", real_c(0), field::fzyx, uint_c(1)); + normalFieldID_ = field::addToStorage< VectorField_T >(blockForest_, "Normal field", vector_t(real_c(0)), + field::fzyx, uint_c(1)); + obstacleNormalFieldID_ = field::addToStorage< VectorField_T >(blockForest_, "Obstacle normal field", + vector_t(real_c(0)), field::fzyx, uint_c(1)); + + flagFieldID_ = freeSurfaceBoundaryHandling_->getFlagFieldID(); + + obstacleFlagIDSet_ = freeSurfaceBoundaryHandling_->getFlagInfo().getObstacleIDSet(); + + if (LatticeModel_T::Stencil::D == uint_t(2)) + { + WALBERLA_LOG_INFO_ON_ROOT( + "IMPORTANT REMARK: You are using a D2Q9 stencil in SurfaceGeometryHandler. Be aware that the " + "results might slightly differ when compared to a D3Q19 stencil and periodicity in the third direction. " + "This is caused by the smoothing of the fill level field, where the additional directions in the D3Q27 add " + "additional weights to the smoothing kernel. Therefore, the resulting smoothed fill level will be " + "different.") + } + } + + ConstBlockDataID getConstCurvatureFieldID() const { return curvatureFieldID_; } + ConstBlockDataID getConstNormalFieldID() const { return normalFieldID_; } + ConstBlockDataID getConstObstNormalFieldID() const { return obstacleNormalFieldID_; } + + BlockDataID getCurvatureFieldID() const { return curvatureFieldID_; } + BlockDataID getNormalFieldID() const { return normalFieldID_; } + BlockDataID getObstNormalFieldID() const { return obstacleNormalFieldID_; } + + void addSweeps(SweepTimeloop& timeloop) const + { + auto blockStateUpdate = StateSweep(blockForest_, freeSurfaceBoundaryHandling_->getFlagInfo(), flagFieldID_); + + if (!string_icompare(curvatureModel_, "FiniteDifferenceMethod")) + { + curvature_model::FiniteDifferenceMethod< Stencil_T, LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > + model; + model.addSweeps(timeloop, *this); + } + else + { + if (!string_icompare(curvatureModel_, "LocalTriangulation")) + { + curvature_model::LocalTriangulation< Stencil_T, LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > + model; + model.addSweeps(timeloop, *this); + } + else + { + if (!string_icompare(curvatureModel_, "SimpleFiniteDifferenceMethod")) + { + curvature_model::SimpleFiniteDifferenceMethod< Stencil_T, LatticeModel_T, FlagField_T, ScalarField_T, + VectorField_T > + model; + model.addSweeps(timeloop, *this); + } + else { WALBERLA_ABORT("The specified curvature model is unknown.") } + } + } + } + + protected: + std::shared_ptr< StructuredBlockForest > blockForest_; // used by friend classes + + std::shared_ptr< FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling_; + + BlockDataID flagFieldID_; + ConstBlockDataID fillFieldID_; + + BlockDataID curvatureFieldID_; + BlockDataID normalFieldID_; + BlockDataID obstacleNormalFieldID_; // mean normal in obstacle cells required for e.g. artificial curvature contact + // model (dissertation of S. Donath, 2011, section 6.5.3.2) + + Set< FlagUID > obstacleFlagIDSet_; // used by friend classes (see CurvatureModel.impl.h) + + std::string curvatureModel_; + bool computeCurvature_; // allows to not compute curvature (just normal) when e.g. the surface tension is 0 + bool enableWetting_; // used by friend classes + ContactAngle contactAngle_; // used by friend classes + + friend class curvature_model::FiniteDifferenceMethod< Stencil_T, LatticeModel_T, FlagField_T, ScalarField_T, + VectorField_T >; + friend class curvature_model::LocalTriangulation< Stencil_T, LatticeModel_T, FlagField_T, ScalarField_T, + VectorField_T >; + friend class curvature_model::SimpleFiniteDifferenceMethod< Stencil_T, LatticeModel_T, FlagField_T, ScalarField_T, + VectorField_T >; +}; // class SurfaceGeometryHandler + +} // namespace free_surface +} // namespace walberla diff --git a/src/lbm/free_surface/surface_geometry/Utility.cpp b/src/lbm/free_surface/surface_geometry/Utility.cpp new file mode 100644 index 000000000..9b1582c66 --- /dev/null +++ b/src/lbm/free_surface/surface_geometry/Utility.cpp @@ -0,0 +1,547 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file Utility.cpp +//! \ingroup surface_geometry +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Helper functions for surface geometry computations. +// +//====================================================================================================================== + +#include "Utility.h" + +#include "core/math/Constants.h" +#include "core/math/Matrix3.h" +#include "core/math/Vector3.h" + +#include <cmath> +#include <vector> + +#include "ContactAngle.h" + +namespace walberla +{ +namespace free_surface +{ +bool computeArtificalWallPoint(const Vector3< real_t >& globalInterfacePointLocation, + const Vector3< real_t >& globalCellCoordinate, const Vector3< real_t >& normal, + const Vector3< real_t >& wallNormal, const Vector3< real_t >& obstacleNormal, + const ContactAngle& contactAngle, Vector3< real_t >& artificialWallPointCoord, + Vector3< real_t >& artificialWallNormal) +{ + // get local interface point location (location of interface point inside cell with origin (0.5,0.5,0.5)) + const Vector3< real_t > interfacePointLocation = globalInterfacePointLocation - globalCellCoordinate; + + // check whether the interface plane intersects one of the cell's edges; exit this function if it does not intersect + // any edge in at least one direction (see dissertation of S. Donath, 2011, section 6.4.5.4) + if (wallNormal[0] < real_c(0) && + (getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(0), real_c(0)), + Vector3< real_t >(real_c(0), real_c(1), real_c(0)), normal, + interfacePointLocation) < real_c(0)) && + + (getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(0), real_c(0)), + Vector3< real_t >(real_c(0), real_c(0), real_c(1)), normal, + interfacePointLocation) < real_c(0)) && + + (getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(1), real_c(0)), + Vector3< real_t >(real_c(0), real_c(0), real_c(1)), normal, + interfacePointLocation) < real_c(0)) && + + (getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(0), real_c(1)), + Vector3< real_t >(real_c(0), real_c(1), real_c(0)), normal, + interfacePointLocation) < real_c(0))) + { + return false; + } + + if (wallNormal[0] > real_c(0) && + (getCellEdgeIntersection(Vector3< real_t >(real_c(1), real_c(0), real_c(0)), + Vector3< real_t >(real_c(0), real_c(1), real_c(0)), normal, + interfacePointLocation) < real_c(0)) && + + (getCellEdgeIntersection(Vector3< real_t >(real_c(1), real_c(0), real_c(0)), + Vector3< real_t >(real_c(0), real_c(0), real_c(1)), normal, + interfacePointLocation) < real_c(0)) && + + (getCellEdgeIntersection(Vector3< real_t >(real_c(1), real_c(1), real_c(0)), + Vector3< real_t >(real_c(0), real_c(0), real_c(1)), normal, + interfacePointLocation) < real_c(0)) && + + (getCellEdgeIntersection(Vector3< real_t >(real_c(1), real_c(0), real_c(1)), + Vector3< real_t >(real_c(0), real_c(1), real_c(0)), normal, + interfacePointLocation) < real_c(0))) + { + return false; + } + + if (wallNormal[1] < real_c(0) && + (getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(0), real_c(0)), + Vector3< real_t >(real_c(1), real_c(0), real_c(0)), normal, + interfacePointLocation) < real_c(0)) && + + (getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(0), real_c(0)), + Vector3< real_t >(real_c(0), real_c(0), real_c(1)), normal, + interfacePointLocation) < real_c(0)) && + + (getCellEdgeIntersection(Vector3< real_t >(real_c(1), real_c(0), real_c(0)), + Vector3< real_t >(real_c(0), real_c(0), real_c(1)), normal, + interfacePointLocation) < real_c(0)) && + + (getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(0), real_c(1)), + Vector3< real_t >(real_c(1), real_c(0), real_c(0)), normal, + interfacePointLocation) < real_c(0))) + { + return false; + } + + if (wallNormal[1] > real_c(0) && + (getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(1), real_c(0)), + Vector3< real_t >(real_c(1), real_c(0), real_c(0)), normal, + interfacePointLocation) < real_c(0)) && + + (getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(1), real_c(0)), + Vector3< real_t >(real_c(0), real_c(0), real_c(1)), normal, + interfacePointLocation) < real_c(0)) && + + (getCellEdgeIntersection(Vector3< real_t >(real_c(1), real_c(1), real_c(0)), + Vector3< real_t >(real_c(0), real_c(0), real_c(1)), normal, + interfacePointLocation) < real_c(0)) && + + (getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(1), real_c(1)), + Vector3< real_t >(real_c(1), real_c(0), real_c(0)), normal, + interfacePointLocation) < real_c(0))) + { + return false; + } + + if (wallNormal[2] < real_c(0) && + (getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(0), real_c(0)), + Vector3< real_t >(real_c(1), real_c(0), real_c(0)), normal, + interfacePointLocation) < real_c(0)) && + + (getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(0), real_c(0)), + Vector3< real_t >(real_c(0), real_c(1), real_c(0)), normal, + interfacePointLocation) < real_c(0)) && + + (getCellEdgeIntersection(Vector3< real_t >(real_c(1), real_c(0), real_c(0)), + Vector3< real_t >(real_c(0), real_c(1), real_c(0)), normal, + interfacePointLocation) < real_c(0)) && + + (getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(1), real_c(0)), + Vector3< real_t >(real_c(1), real_c(0), real_c(0)), normal, + interfacePointLocation) < real_c(0))) + { + return false; + } + + if (wallNormal[2] > real_c(0) && + (getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(0), real_c(1)), + Vector3< real_t >(real_c(1), real_c(0), real_c(0)), normal, + interfacePointLocation) < real_c(0)) && + + (getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(0), real_c(1)), + Vector3< real_t >(real_c(0), real_c(1), real_c(0)), normal, + interfacePointLocation) < real_c(0)) && + + (getCellEdgeIntersection(Vector3< real_t >(real_c(1), real_c(0), real_c(1)), + Vector3< real_t >(real_c(0), real_c(1), real_c(0)), normal, + interfacePointLocation) < real_c(0)) && + + (getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(1), real_c(1)), + Vector3< real_t >(real_c(1), real_c(0), real_c(0)), normal, + interfacePointLocation) < real_c(0))) + { + return false; + } + + // line 1 in Algorithm 6.2 in dissertation of S. Donath, 2011 + real_t cosAlpha = dot(normal, obstacleNormal); + + // determine sin(alpha) via orthogonal projections to compute alpha in the correct quadrant + const Vector3< real_t > projector = normal - cosAlpha * obstacleNormal; + const Vector3< real_t > baseVec = cross(obstacleNormal, normal); + const real_t sinAlpha = projector * cross(baseVec, obstacleNormal).getNormalized(); + + // compute alpha (angle between surface plane and wall) + real_t alpha; + if (sinAlpha >= real_c(0)) { alpha = real_c(std::acos(cosAlpha)); } + else { alpha = real_c(2) * math::pi - real_c(std::acos(cosAlpha)); } + + // line 2 in Algorithm 6.2 in dissertation of S. Donath, 2011 + const real_t theta = contactAngle.getInRadians(); + const real_t delta = theta - alpha; + + // determine distance from interface point to wall plane + const real_t wallDistance = dot(fabs(interfacePointLocation), wallNormal); + + // line 3-4 in Algorithm 6.2 in dissertation of S. Donath, 2011 + // correct contact angle is already reached + if (realIsEqual(delta, real_c(0), real_c(1e-14))) + { + // IMPORTANT: the following approach is in contrast to the dissertation of S. Donath, 2011 + // - Dissertation: only the intersection of the interface surface with the wall is computed; it is known that this + // could in rare cases lead to unstable configurations + // - Here: extrapolate (extend) the wall point such that the triangle is considered valid during curvature + // computation; + // This has been adapted from the old waLBerla source code. While delta is considered to be zero, there is + // still a division by delta below. This has not been found problematic when using double precision, however it + // leads to invalid values in single precision. Therefore, the following macro exits the function prematurely when + // using single precision and returns false such that the wall point will not be used. +#ifndef WALBERLA_DOUBLE_ACCURACY + return false; +#endif + + // determine the direction in which the artifical wall point is to be expected + // expression "dot(wallNormal,normal)*wallNormal" is orthogonal projection of normal on wallNormal + // (wallNormal has already been normalized => no division by vector length required) + const Vector3< real_t > targetDir = (normal - dot(wallNormal, normal) * wallNormal).getNormalized(); + + const real_t sinTheta = contactAngle.getSin(); + + // distance of interface point to wall in direction of wallNormal + real_t wallPointDistance = wallDistance / sinTheta; // d_WP in dissertation of S. Donath, 2011 + + // wall point must not be too close or far away from interface point too avoid degenerated triangles + real_t virtWallDistance; + if (wallPointDistance < real_c(0.8)) + { + // extend distance with a heuristically chosen tolerance such that the point is not thrown away when checking + // for degenerated triangles in the curvature computation + wallPointDistance = real_c(0.801); + virtWallDistance = wallPointDistance * sinTheta; // d_W' + } + else + { + if (wallPointDistance > real_c(1.8)) + { + // reduce distance with heuristically chosen tolerance + wallPointDistance = real_c(1.799); + virtWallDistance = wallPointDistance * sinTheta; // d_W' + } + else + { + virtWallDistance = wallDistance; // d_W' + } + } + + const real_t virtPointDistance = virtWallDistance / sinTheta; // d_WP' + + // compute point by shifting virtWallDistance along wallNormal starting from globalInterfacePointLocation + // virtWallProjection is given in global coordinates + const Vector3< real_t > virtWallProjection = globalInterfacePointLocation - virtWallDistance * wallNormal; + + const real_t cosTheta = contactAngle.getCos(); + + // compute artificial wall point by starting from virtWallProjection and shifting "virtPointDistance*cosTheta" in + // targetDir (vritualWallPointCoord is r_W in dissertation of S. Donath, 2011) + artificialWallPointCoord = virtWallProjection + virtPointDistance * cosTheta * targetDir; + + // radius r of the artificial circle "virtPointDistance*0.5/(sin(0.5)*delta)" + // midpoint M of the artificial circle "normal*r+globalInterfacePointLocation" + // normal of the virtual wall point "M-artificialWallPointCoord" + artificialWallNormal = normal * virtPointDistance * real_c(0.5) / (std::sin(real_c(0.5) * delta)) + + globalInterfacePointLocation - artificialWallPointCoord; + artificialWallNormal = artificialWallNormal.getNormalized(); + + return true; + } + else + { + // compute base angles of triangle; line 6 in Algorithm 6.2 in dissertation of S. Donath, 2011 + const real_t beta = real_c(0.5) * (math::pi - real_c(std::fabs(delta))); + + real_t gamma; + // line 7 in Algorithm 6.2 in dissertation of S. Donath, 2011 + if (theta < alpha) { gamma = beta - theta; } + else { gamma = beta + theta - math::pi; } + + const real_t wallPointDistance = + wallDistance / real_c(std::cos(std::fabs(gamma))); // d_WP in dissertation of S. Donath, 2011 + + // line 9 in Algorithm 6.2 in dissertation of S. Donath, 2011 + // division by zero not possible as delta==0 is caught above + real_t radius = real_c(0.5) * wallPointDistance / std::sin(real_c(0.5) * real_c(std::fabs(delta))); + + // check wallPointDistance for being in a valid range (to avoid degenerated triangles) + real_t artificialWallPointDistance = wallPointDistance; // d'_WP in dissertation of S. Donath, 2011 + + if (wallPointDistance < real_c(0.8)) + { + // extend distance with a heuristically chosen tolerance such that the point is not thrown away when checking + // for degenerated triangles in the curvature computation + artificialWallPointDistance = real_c(0.801); + + // if extended distance exceeds circle diameter, assume delta=90 degrees + if (artificialWallPointDistance > real_c(2) * radius) + { + radius = artificialWallPointDistance * math::one_div_root_two; + } + } + else + { + // reduce distance with heuristically chosen tolerance + if (wallPointDistance > real_c(1.8)) { artificialWallPointDistance = real_c(1.799); } + } + + // line 17 in Algorithm 6.2 in dissertation of S. Donath, 2011 + const real_t artificialDelta = + real_c(2) * real_c(std::asin(real_c(0.5) * artificialWallPointDistance / + real_c(radius))); // delta' in dissertation of S. Donath, 2011 + + // line 18 in Algorithm 6.2 in dissertation of S. Donath, 2011 + Vector3< real_t > rotVec = cross(normal, obstacleNormal); + + // change direction of rotation axis for delta>0; this is in contrast to Algorithm 6.2 in dissertation of Stefan + // Donath, 2011 but was found to be necessary as otherwise the artificialWallNormal points in the wrong direction + if (delta > real_c(0)) { rotVec *= real_c(-1); } + + // line 19 in Algorithm 6.2 in dissertation of S. Donath, 2011 + const Matrix3< real_t > rotMat(rotVec, artificialDelta); + + // line 20 in Algorithm 6.2 in dissertation of S. Donath, 2011 + artificialWallNormal = rotMat * normal; + + // line 21 in Algorithm 6.2 in dissertation of S. Donath, 2011 + if (theta < alpha) + { + artificialWallPointCoord = globalInterfacePointLocation + radius * (normal - artificialWallNormal); + } + else { artificialWallPointCoord = globalInterfacePointLocation - radius * (normal - artificialWallNormal); } + + return true; + } +} + +Vector3< real_t > getInterfacePoint(const Vector3< real_t >& normal, real_t fillLevel) +{ + // exploit symmetries of cubic cell to simplify the algorithm, i.e., restrict the fill level to the interval + // [0,0.5] (see dissertation of T. Pohl, 2008, p. 22f) + bool fillMirrored = false; + if (fillLevel >= real_c(0.5)) + { + fillMirrored = true; + fillLevel = real_c(1) - fillLevel; + } + + // sort normal components such that nx, ny, nz >= 0 and nx <= ny <= nz to simplify the algorithm + Vector3< real_t > normalSorted = fabs(normal); + if (normalSorted[0] > normalSorted[1]) + { + // swap nx and ny + real_t tmp = normalSorted[0]; + normalSorted[0] = normalSorted[1]; + normalSorted[1] = tmp; + } + + if (normalSorted[1] > normalSorted[2]) + { + // swap ny and nz + real_t tmp = normalSorted[1]; + normalSorted[1] = normalSorted[2]; + normalSorted[2] = tmp; + + if (normalSorted[0] > normalSorted[1]) + { + // swap nx and ny + tmp = normalSorted[0]; + normalSorted[0] = normalSorted[1]; + normalSorted[1] = tmp; + } + } + + // minimal and maximal plane offset chosen as in the dissertation of T. Pohl, 2008, p. 22 + real_t offsetMin = real_c(-0.866025); // sqrt(3/4), lowest possible value + real_t offsetMax = real_c(0); + + // find correct interface position by bisection (Algorithm 2.1, p. 22 in dissertation of T. Pohl, 2008) + const uint_t numBisections = + uint_c(10); // number of bisections, =10 in dissertation of T. Pohl, 2008 (heuristically chosen) + + for (uint_t i = uint_c(0); i <= numBisections; ++i) + { + const real_t offsetTmp = real_c(0.5) * (offsetMin + offsetMax); + const real_t newFillLevel = computeCellFluidVolume(normalSorted, offsetTmp); + + // volume is too small, reduce upper bound + if (newFillLevel > fillLevel) { offsetMax = offsetTmp; } + + // volume is too large, reduce lower bound + else { offsetMin = offsetTmp; } + } + real_t offset = real_c(0.5) * (offsetMin + offsetMax); + + if (fillMirrored) { offset *= real_c(-1); } + const Vector3< real_t > interfacePoint = Vector3< real_t >(real_c(0.5)) + offset * normal; + + return interfacePoint; +} + +real_t getCellEdgeIntersection(const Vector3< real_t >& edgePoint, const Vector3< real_t >& edgeDirection, + const Vector3< real_t >& normal, const Vector3< real_t >& surfacePoint) +{ + //#ifndef BELOW_CELL + //# define BELOW_CELL (-10) + //#endif + +#ifndef ABOVE_CELL +# define ABOVE_CELL (-20) +#endif + // mathematical description: + // surface plane in coordinate form: x * normal = surfacePoint * normal + // cell edge line: edgePoint + lambda * edgeDirection + // => point of intersection: lambda = (interfacePoint * normal - edgePoint * normal) / edgeDirection * normal + + // compute angle between normal and cell edge + real_t cosAngle = dot(edgeDirection, normal); + + real_t intersection = real_c(0); + + // intersection exists only if angle is not 90°, i.e., (intersection != 0) + if (std::fabs(cosAngle) >= real_c(1e-14)) + { + intersection = ((surfacePoint - edgePoint) * normal) / cosAngle; + + // // intersection is below cell + // if (intersection < real_c(0)) { intersection = real_c(BELOW_CELL); } + + // intersection is above cell + if (intersection > real_c(1)) { intersection = real_c(ABOVE_CELL); } + } + else // no intersection if angle is 90° (intersection == 0) + { + intersection = real_c(-1); + } + + return intersection; +} + +real_t computeCellFluidVolume(const Vector3< real_t >& normal, real_t offset) +{ + const Vector3< real_t > interfacePoint = Vector3< real_t >(real_c(0.5)) + offset * normal; + + real_t volume = real_c(0); + + // stores points of intersection with cell edges; points are shifted along normal such that the points lay + // on one plane and surface area can be calculated + std::vector< Vector3< real_t > > points; + + // SW: south west, EB: east bottom, etc. + real_t iSW = getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(0), real_c(0)), + Vector3< real_t >(real_c(0), real_c(0), real_c(1)), normal, interfacePoint); + + // if no intersection with edge SW, volume is zero + if (iSW > real_c(0) || realIsIdentical(iSW, ABOVE_CELL)) + { + real_t iSE = getCellEdgeIntersection(Vector3< real_t >(real_c(1), real_c(0), real_c(0)), + Vector3< real_t >(real_c(0), real_c(0), real_c(1)), normal, interfacePoint); + real_t iNW = getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(1), real_c(0)), + Vector3< real_t >(real_c(0), real_c(0), real_c(1)), normal, interfacePoint); + real_t iNE = getCellEdgeIntersection(Vector3< real_t >(real_c(1), real_c(1), real_c(0)), + Vector3< real_t >(real_c(0), real_c(0), real_c(1)), normal, interfacePoint); + + // simple case: all four vertices are included in fluid domain (see Figure 2.12, p. 24 in dissertation of Thomas + // Pohl, 2008) + if (iNE > real_c(0)) { volume = real_c(0.25) * (iSW + iSE + iNW + iNE); } + else + { + if (iSE >= real_c(0)) + { + real_t iEB = + getCellEdgeIntersection(Vector3< real_t >(real_c(1), real_c(0), real_c(0)), + Vector3< real_t >(real_c(0), real_c(1), real_c(0)), normal, interfacePoint); + + // shift intersection points along normal and store points + points.push_back(Vector3< real_t >(real_c(1), real_c(0), real_c(iSE)) - interfacePoint); + points.push_back(Vector3< real_t >(real_c(1), real_c(iEB), real_c(0)) - interfacePoint); + + volume += iSE * iEB; + } + else + { + real_t iSB = + getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(0), real_c(0)), + Vector3< real_t >(real_c(1), real_c(0), real_c(0)), normal, interfacePoint); + + points.push_back(Vector3< real_t >(real_c(iSB), real_c(0), real_c(0)) - interfacePoint); + } + + if (iNW >= real_c(0)) + { + real_t iNB = + getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(1), real_c(0)), + Vector3< real_t >(real_c(1), real_c(0), real_c(0)), normal, interfacePoint); + + points.push_back(Vector3< real_t >(real_c(iNB), real_c(1), real_c(0)) - interfacePoint); + points.push_back(Vector3< real_t >(real_c(0), real_c(1), real_c(iNW)) - interfacePoint); + + volume += iNB * iNW; + } + else + { + real_t iWB = + getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(0), real_c(0)), + Vector3< real_t >(real_c(0), real_c(1), real_c(0)), normal, interfacePoint); + + points.push_back(Vector3< real_t >(real_c(0), real_c(iWB), real_c(0)) - interfacePoint); + } + + real_t iWT = + getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(0), real_c(1)), + Vector3< real_t >(real_c(0), real_c(1), real_c(0)), normal, interfacePoint); + if (iWT >= real_c(0)) + { + real_t iST = + getCellEdgeIntersection(Vector3< real_t >(real_c(0), real_c(0), real_c(1)), + Vector3< real_t >(real_c(1), real_c(0), real_c(0)), normal, interfacePoint); + + points.push_back(Vector3< real_t >(real_c(0), real_c(iWT), real_c(1)) - interfacePoint); + points.push_back(Vector3< real_t >(real_c(iST), real_c(0), real_c(1)) - interfacePoint); + + volume += iWT * iST; + } + else { points.push_back(Vector3< real_t >(real_c(0), real_c(0), real_c(iSW)) - interfacePoint); } + + Vector3< real_t > vectorProduct(real_c(0)); + real_t area = real_c(0); + size_t j = points.size() - 1; + + // compute the vector product, the length of which gives the surface area of the corresponding + // parallelogram; + for (size_t i = 0; i != points.size(); ++i) + { + vectorProduct[0] = points[i][1] * points[j][2] - points[i][2] * points[j][1]; + vectorProduct[1] = points[i][2] * points[j][0] - points[i][0] * points[j][2]; + vectorProduct[2] = points[i][0] * points[j][1] - points[i][1] * points[j][0]; + + // area of the triangle is obtained through division by 2; this is done below in the volume + // calculation (where the division is then by 6 instead of by 3) + area += vectorProduct.length(); + j = i; + } + + // compute and sum the volumes of each resulting pyramid: V = area * height * 1/3 + volume += area * (normal * interfacePoint); + volume /= real_c(6.0); // division by 6 since the area was not divided by 2 above + } + } + else // no intersection with edge SW, volume is zero + { + volume = real_c(0); + } + + return volume; +} +} // namespace free_surface +} // namespace walberla diff --git a/src/lbm/free_surface/surface_geometry/Utility.h b/src/lbm/free_surface/surface_geometry/Utility.h new file mode 100644 index 000000000..7be227e8c --- /dev/null +++ b/src/lbm/free_surface/surface_geometry/Utility.h @@ -0,0 +1,68 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file Utility.h +//! \ingroup surface_geometry +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Helper functions for surface geometry computations. +// +//====================================================================================================================== + +#pragma once + +#include "core/math/Vector3.h" + +#include "ContactAngle.h" + +namespace walberla +{ +namespace free_surface +{ +/*********************************************************************************************************************** + * Compute the point p that lays on the plane of the interface surface (see Algorithm 2.1 in dissertation of Thomas + * Pohl, 2008). p = (0.5, 0.5, 0.5) + offset * normal. + **********************************************************************************************************************/ +Vector3< real_t > getInterfacePoint(const Vector3< real_t >& normal, real_t fillLevel); + +/*********************************************************************************************************************** + * Compute the intersection point of a surface (defined by normal and surfacePoint) with an edge of a cell. + **********************************************************************************************************************/ +real_t getCellEdgeIntersection(const Vector3< real_t >& edgePoint, const Vector3< real_t >& edgeDirection, + const Vector3< real_t >& normal, const Vector3< real_t >& surfacePoint); + +/*********************************************************************************************************************** + * Compute the fluid volume within an interface cell with respect to + * - the interface normal + * - the interface surface offset. + * + * see dissertation of T. Pohl, 2008, section 2.5.3, p. 23-26. + **********************************************************************************************************************/ +real_t computeCellFluidVolume(const Vector3< real_t >& normal, real_t offset); + +/*********************************************************************************************************************** + * Compute an artificial wall point according to the artifical curvature wetting model from the dissertation of Stefan + * Donath, 2011. The artificial wall point and the artificial normal can be used to alter the curvature computation with + * local triangulation. The interface curvature is changed such that the correct laplace pressure with respect to the + * contact angle is assumed near solid cells. + * + * see dissertation of T. Pohl, 2008, section 6.3.3 + **********************************************************************************************************************/ +bool computeArtificalWallPoint(const Vector3< real_t >& globalInterfacePointLocation, + const Vector3< real_t >& globalCellCoordinate, const Vector3< real_t >& normal, + const Vector3< real_t >& wallNormal, const Vector3< real_t >& obstacleNormal, + const ContactAngle& contactAngle, Vector3< real_t >& artificialWallPointCoord, + Vector3< real_t >& artificialWallNormal); +} // namespace free_surface +} // namespace walberla diff --git a/tests/lbm/CMakeLists.txt b/tests/lbm/CMakeLists.txt index d34cb0684..bb8d6dd57 100644 --- a/tests/lbm/CMakeLists.txt +++ b/tests/lbm/CMakeLists.txt @@ -130,10 +130,10 @@ waLBerla_compile_test( FILES codegen/FieldLayoutAndVectorizationTest.cpp DEPENDS waLBerla_execute_test( NAME FieldLayoutAndVectorizationTest ) waLBerla_generate_target_from_python(NAME LbmPackInfoGenerationTestCodegen FILE codegen/LbmPackInfoGenerationTest.py - OUT_FILES AccessorBasedPackInfoEven.cpp AccessorBasedPackInfoEven.h - AccessorBasedPackInfoOdd.cpp AccessorBasedPackInfoOdd.h - FromKernelPackInfoPull.cpp FromKernelPackInfoPull.h - FromKernelPackInfoPush.cpp FromKernelPackInfoPush.h) + OUT_FILES AccessorBasedPackInfoEven.cpp AccessorBasedPackInfoEven.h + AccessorBasedPackInfoOdd.cpp AccessorBasedPackInfoOdd.h + FromKernelPackInfoPull.cpp FromKernelPackInfoPull.h + FromKernelPackInfoPush.cpp FromKernelPackInfoPush.h) waLBerla_link_files_to_builddir( "diff_packinfos.sh" ) waLBerla_execute_test( NAME LbmPackInfoGenerationDiffTest COMMAND bash diff_packinfos.sh ) @@ -170,3 +170,139 @@ waLBerla_compile_test( FILES codegen/StreamInCellIntervalCodegenTest.cpp waLBerla_execute_test( NAME StreamInCellIntervalCodegenTest ) endif() + +# Free Surface +waLBerla_compile_test( FILES free_surface/bubble_model/BubbleInitializationTest.cpp + DEPENDS blockforest field geometry lbm timeloop ) +waLBerla_execute_test( NAME BubbleInitializationTest + PROCESSES 2 ) + +file( COPY free_surface/bubble_model/MergeAndSplitTestUnconnected.png + free_surface/bubble_model/MergeAndSplitTestConnected.png + DESTINATION ${CMAKE_CURRENT_BINARY_DIR} ) +waLBerla_compile_test( FILES free_surface/bubble_model/MergeAndSplitTest.cpp + free_surface/bubble_model/BubbleBodyMover.impl.h + free_surface/bubble_model/BubbleModelTester.impl.h + DEPENDS blockforest field lbm timeloop ) +waLBerla_execute_test( NAME MergeAndSplitTest + PROCESSES 10 ) + +waLBerla_compile_test( FILES free_surface/bubble_model/MergeInformationTest.cpp + DEPENDS blockforest field lbm timeloop ) +waLBerla_execute_test( NAME MergeInformationTest + PROCESSES 3 ) + +waLBerla_compile_test( FILES free_surface/bubble_model/MovingSpheresTest.cpp + free_surface/bubble_model/BubbleBodyMover.impl.h + free_surface/bubble_model/BubbleModelTester.impl.h + DEPENDS blockforest field geometry lbm timeloop ) +waLBerla_execute_test( NAME MovingSpheresTest + PROCESSES 2 ) + +waLBerla_compile_test( FILES free_surface/bubble_model/RegionalFloodFillTest.cpp + DEPENDS field lbm ) +waLBerla_execute_test( NAME RegionalFloodFillTest ) + +waLBerla_compile_test( FILES free_surface/bubble_model/SplitDetectionTest.cpp + DEPENDS field lbm) +waLBerla_execute_test( NAME SplitDetectionTest ) + +waLBerla_compile_test( FILES free_surface/dynamics/AdvectionTest.cpp + DEPENDS blockforest field lbm timeloop ) +waLBerla_execute_test( NAME AdvectionTest ) + +waLBerla_compile_test( FILES free_surface/dynamics/CellConversionTest.cpp + DEPENDS blockforest field lbm timeloop ) +waLBerla_execute_test( NAME CellConversionTest ) + +if( WALBERLA_BUILD_WITH_CODEGEN ) + walberla_generate_target_from_python( NAME LatticeModelGenerationFreeSurfacePython + FILE free_surface/dynamics/LatticeModelGenerationFreeSurface.py + OUT_FILES GeneratedLatticeModel_FreeSurface.cpp + GeneratedLatticeModel_FreeSurface.h ) + waLBerla_compile_test( FILES free_surface/dynamics/CodegenTest.cpp + DEPENDS blockforest field lbm timeloop LatticeModelGenerationFreeSurfacePython ) + waLBerla_execute_test( NAME CodegenTest ) +endif() + +waLBerla_compile_test( FILES free_surface/dynamics/ExcessMassDistributionFallbackTest.cpp + DEPENDS blockforest field lbm timeloop ) +waLBerla_execute_test( NAME ExcessMassDistributionFallbackTest ) + +waLBerla_compile_test( FILES free_surface/dynamics/ExcessMassDistributionParallelTest.cpp + DEPENDS blockforest field lbm timeloop ) +waLBerla_execute_test( NAME ExcessMassDistributionParallelTest + PROCESSES 2) + +waLBerla_compile_test( FILES free_surface/dynamics/InflowTest.cpp + DEPENDS blockforest field lbm timeloop ) +waLBerla_execute_test( NAME InflowTest ) + +waLBerla_compile_test( FILES free_surface/LoadBalancingTest.cpp + DEPENDS blockforest field lbm timeloop ) +waLBerla_execute_test( NAME LoadBalancingTest + PROCESSES 4) + +waLBerla_compile_test( FILES free_surface/dynamics/PdfReconstructionFreeSlipTest.cpp + DEPENDS blockforest field lbm timeloop ) +waLBerla_execute_test( NAME PdfReconstructionFreeSlipTest ) + +waLBerla_compile_test( FILES free_surface/dynamics/PdfReconstructionTest.cpp + DEPENDS blockforest field lbm timeloop ) +waLBerla_execute_test( NAME PdfReconstructionTest ) + +waLBerla_compile_test( FILES free_surface/dynamics/PdfRefillingTest.cpp + DEPENDS blockforest field lbm timeloop ) +waLBerla_execute_test( NAME PdfRefillingTest ) + +waLBerla_compile_test( FILES free_surface/dynamics/WettingConversionTest.cpp + DEPENDS blockforest field geometry lbm timeloop ) +waLBerla_execute_test( NAME WettingConversionTest ) + +waLBerla_compile_test( FILES free_surface/surface_geometry/CellFluidVolumeTest.cpp + DEPENDS lbm ) +waLBerla_execute_test( NAME CellFluidVolumeTest ) + +waLBerla_compile_test( FILES free_surface/surface_geometry/CurvatureOfSineTest.cpp + DEPENDS blockforest field lbm timeloop ) +waLBerla_execute_test( NAME CurvatureOfSineTest ) + +waLBerla_compile_test( FILES free_surface/surface_geometry/CurvatureOfSphereTest.cpp + DEPENDS blockforest field geometry lbm timeloop ) +waLBerla_execute_test( NAME CurvatureOfSphereTest ) + +waLBerla_compile_test( FILES free_surface/surface_geometry/DetectWettingTest.cpp + DEPENDS blockforest field lbm timeloop ) +waLBerla_execute_test( NAME DetectWettingTest ) + +waLBerla_compile_test( FILES free_surface/surface_geometry/GetInterfacePointTest.cpp + DEPENDS lbm ) +waLBerla_execute_test( NAME GetInterfacePointTest ) + +waLBerla_compile_test( FILES free_surface/surface_geometry/NormalsOfSineTest.cpp + DEPENDS blockforest field lbm timeloop ) +waLBerla_execute_test( NAME NormalsOfSineTest ) + +waLBerla_compile_test( FILES free_surface/surface_geometry/NormalsOfSphereTest.cpp + DEPENDS blockforest field geometry lbm timeloop ) +waLBerla_execute_test( NAME NormalsOfSphereTest ) + +waLBerla_compile_test( FILES free_surface/surface_geometry/NormalsEquivalenceTest.cpp + DEPENDS blockforest field lbm timeloop ) +waLBerla_execute_test( NAME NormalsEquivalenceTest ) + +waLBerla_compile_test( FILES free_surface/surface_geometry/NormalsNearSolidTest.cpp + DEPENDS blockforest field lbm timeloop ) +waLBerla_execute_test( NAME NormalsNearSolidTest ) + +waLBerla_compile_test( FILES free_surface/surface_geometry/ObstacleFillLevelTest.cpp + DEPENDS blockforest field lbm timeloop ) +waLBerla_execute_test( NAME ObstacleFillLevelTest ) + +waLBerla_compile_test( FILES free_surface/surface_geometry/ObstacleNormalsTest.cpp + DEPENDS blockforest field lbm timeloop ) +waLBerla_execute_test( NAME ObstacleNormalsTest ) + +waLBerla_compile_test( FILES free_surface/surface_geometry/WettingCurvatureTest.cpp + DEPENDS blockforest field lbm timeloop ) +waLBerla_execute_test( NAME WettingCurvatureTest ) \ No newline at end of file diff --git a/tests/lbm/free_surface/LoadBalancingTest.cpp b/tests/lbm/free_surface/LoadBalancingTest.cpp new file mode 100644 index 000000000..455dc2e17 --- /dev/null +++ b/tests/lbm/free_surface/LoadBalancingTest.cpp @@ -0,0 +1,192 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file LoadBalancingTest.cpp +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Test FSLBM load balancing in simplistic setup with 3x3x1 blocks on a 12x12x1 domain. +// +//! An initially less optimal weight distribution should be increased after performing load balancing. +//====================================================================================================================== + +#include "lbm/free_surface/LoadBalancing.h" + +#include "blockforest/Initialization.h" + +#include "core/Environment.h" +#include "core/debug/TestSubsystem.h" + +#include "lbm/blockforest/communication/SimpleCommunication.h" +#include "lbm/field/AddToStorage.h" +#include "lbm/free_surface/BlockStateDetectorSweep.h" +#include "lbm/free_surface/boundary/FreeSurfaceBoundaryHandling.h" +#include "lbm/lattice_model/D2Q9.h" + +#include "timeloop/SweepTimeloop.h" + +namespace walberla +{ +namespace free_surface +{ +namespace LoadBalancingTest +{ +using ScalarField_T = GhostLayerField< real_t, 1 >; +using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >; + +using LatticeModel_T = lbm::D2Q9< lbm::collision_model::SRT >; +using Stencil_T = LatticeModel_T::Stencil; + +using Communication_T = blockforest::SimpleCommunication< Stencil_T >; + +using FlagField_T = FlagField< uint32_t >; +using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >; + +int main(int argc, char** argv) +{ + debug::enterTestMode(); + Environment walberlaEnv(argc, argv); + + // define the domain size + const Vector3< uint_t > domainSize(uint_c(12), uint_c(12), uint_c(1)); + const Vector3< uint_t > cellsPerBlock(uint_c(3), uint_c(3), uint_c(1)); + const Vector3< uint_t > periodicity(uint_c(0), uint_c(0), uint_c(0)); + + Vector3< uint_t > numBlocks; + numBlocks[0] = uint_c(std::ceil(domainSize[0] / cellsPerBlock[0])); + numBlocks[1] = uint_c(std::ceil(domainSize[1] / cellsPerBlock[1])); + numBlocks[2] = uint_c(std::ceil(domainSize[2] / cellsPerBlock[2])); + + uint_t numProcesses = uint_c(MPIManager::instance()->numProcesses()); + + WALBERLA_CHECK_EQUAL(numProcesses, uint_c(4), "This test must be executed with four MPI processes.") + + WALBERLA_CHECK_LESS_EQUAL(numProcesses, numBlocks[0] * numBlocks[1] * numBlocks[2], + "The number of MPI processes is greater than the number of blocks as defined by " + "\"domainSize/cellsPerBlock\". This would result in unused MPI processes. Either decrease " + "the number of MPI processes or increase \"cellsPerBlock\".") + + // create non-uniform block forest (non-uniformity required for load balancing) + const std::shared_ptr< StructuredBlockForest > blockForest = + createNonUniformBlockForest(domainSize, cellsPerBlock, numBlocks, periodicity); + + // create (dummy) lattice model with dummy PDF field + LatticeModel_T latticeModel = LatticeModel_T(lbm::collision_model::SRT(real_c(1.0))); + const BlockDataID pdfFieldID = lbm::addPdfFieldToStorage(blockForest, "PDF field", latticeModel, field::fzyx); + + const BlockDataID fillFieldID = + field::addToStorage< ScalarField_T >(blockForest, "Fill level field", real_c(0.0), field::fzyx, uint_c(1)); + + // initialize fill level field + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + ScalarField_T* const fillField = blockIt->getData< ScalarField_T >(fillFieldID); + WALBERLA_FOR_ALL_CELLS(fillFieldIt, fillField, { + // cell in block-local coordinates + const Cell localCell = fillFieldIt.cell(); + + // get cell in global coordinates + Cell globalCell = fillFieldIt.cell(); + blockForest->transformBlockLocalToGlobalCell(globalCell, *blockIt, localCell); + + // set liquid cells + if (globalCell[1] < cell_idx_c(5)) { *fillFieldIt = real_c(1); } + + // set interface cells + if (globalCell[1] == cell_idx_c(5)) { *fillFieldIt = real_c(0.5); } + + // set gas cells + if (globalCell[1] > cell_idx_c(5)) { *fillFieldIt = real_c(0); } + }) // WALBERLA_FOR_ALL_CELLS + } + + // create boundary handling for initializing flag field + const std::shared_ptr< FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling = + std::make_shared< FreeSurfaceBoundaryHandling_T >(blockForest, pdfFieldID, fillFieldID); + const BlockDataID flagFieldID = freeSurfaceBoundaryHandling->getFlagFieldID(); + const typename FreeSurfaceBoundaryHandling_T::FlagInfo_T& flagInfo = freeSurfaceBoundaryHandling->getFlagInfo(); + freeSurfaceBoundaryHandling->setNoSlipAtAllBorders(cell_idx_c(-1)); + freeSurfaceBoundaryHandling->initFlagsFromFillLevel(); + + // communication after initialization + Communication_T communication(blockForest, flagFieldID, fillFieldID); + communication(); + + Communication_T pdfCommunication(blockForest, pdfFieldID); + pdfCommunication(); + + // create bubble model + std::shared_ptr< bubble_model::BubbleModelBase > bubbleModel = + std::make_shared< bubble_model::BubbleModelConstantPressure >(real_c(1)); + + // detect block states (detection performed during construction) + BlockStateDetectorSweep< FlagField_T > blockStateDetector(blockForest, flagInfo, flagFieldID); + + // the initialization as chosen above results in the following block states + // |G|G|G| with G: onlyGasAndBoundary + // |F|F|F| F: fullFreeSurface + // |F|F|F| L: onlyLBM + // |L|L|L| + // + // Note that the blocks in row 3 have also state F, although they only consist of gas cells. This is because there is + // an interface cell in the ghost layer that is synchronized from the blocks of row 2. The BlockStateDetectorSweep + // also checks the ghost layer, as e.g. during cell conversion, a block having an interface cell in its ghost layer + // must also perform a corresponding conversion on its inner cells. + + uint_t blockWeightFullFreeSurface = uint_c(50); + uint_t blockWeightOnlyLBM = uint_c(10); + uint_t blockWeightOnlyGasAndBoundary = uint_c(5); + + // evaluate process loads + ProcessLoadEvaluator< FlagField_T > loadEvaluator(blockForest, blockWeightFullFreeSurface, blockWeightOnlyLBM, + blockWeightOnlyGasAndBoundary, uint_c(1)); + + // evaluate weight distribution on processes + std::vector< real_t > weightSum = loadEvaluator.computeWeightSumPerProcess(); + + // initial weight distribution before load balancing + WALBERLA_ROOT_SECTION() + { + WALBERLA_LOG_DEVEL("Checking initial weight distribution") + WALBERLA_CHECK_FLOAT_EQUAL(weightSum[0], real_c(40), real_c(1e-14)); + WALBERLA_CHECK_FLOAT_EQUAL(weightSum[1], real_c(200), real_c(1e-14)); + WALBERLA_CHECK_FLOAT_EQUAL(weightSum[2], real_c(200), real_c(1e-14)); + WALBERLA_CHECK_FLOAT_EQUAL(weightSum[3], real_c(20), real_c(1e-14)); + } + + // perform load balancing + LoadBalancer< FlagField_T, Stencil_T, Stencil_T > loadBalancer( + blockForest, communication, pdfCommunication, bubbleModel, blockWeightFullFreeSurface, blockWeightOnlyLBM, + blockWeightOnlyGasAndBoundary, uint_c(1), false); + loadBalancer(); + + // evaluate weight distribution on processes + weightSum = loadEvaluator.computeWeightSumPerProcess(); + + // check weight distribution after load balancing + WALBERLA_ROOT_SECTION() + { + WALBERLA_LOG_DEVEL("Checking weight distribution after load balancing") + WALBERLA_CHECK_FLOAT_EQUAL(weightSum[0], real_c(140), real_c(1e-14)); + WALBERLA_CHECK_FLOAT_EQUAL(weightSum[1], real_c(100), real_c(1e-14)); + WALBERLA_CHECK_FLOAT_EQUAL(weightSum[2], real_c(100), real_c(1e-14)); + WALBERLA_CHECK_FLOAT_EQUAL(weightSum[3], real_c(120), real_c(1e-14)); + } + + return EXIT_SUCCESS; +} +} // namespace LoadBalancingTest +} // namespace free_surface +} // namespace walberla + +int main(int argc, char** argv) { return walberla::free_surface::LoadBalancingTest::main(argc, argv); } diff --git a/tests/lbm/free_surface/bubble_model/BubbleBodyMover.h b/tests/lbm/free_surface/bubble_model/BubbleBodyMover.h new file mode 100644 index 000000000..f36124fcd --- /dev/null +++ b/tests/lbm/free_surface/bubble_model/BubbleBodyMover.h @@ -0,0 +1,71 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file BubbleBodyMover.h +//! \ingroup lbm/free_surface/bubble_model +//! \author Martin Bauer +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Helper class for bubble model test cases. +//! +//! Add bubbles that are represented by geometric bodies. Move the bubbles by updating the fill level and flag field. +// +//====================================================================================================================== + +#pragma once + +#include "geometry/initializer/OverlapFieldFromBody.h" + +#include "BubbleModelTester.h" + +namespace walberla +{ +namespace free_surface +{ +namespace bubble_model +{ +template< typename Body_T, typename Stencil_T > +class BubbleBodyMover : public BubbleModelTester< Stencil_T > +{ + public: + BubbleBodyMover(const std::shared_ptr< StructuredBlockStorage >& blockStorage, + const std::shared_ptr< BubbleModel< Stencil_T > >& bubbleModel); + + inline const std::vector< Body_T >& getBodies() const { return bodies_; } + + void addBody(const Body_T& b, std::function< void(Body_T&) > moveFunction) + { + bodies_.push_back(b); + moveFunctions_.push_back(moveFunction); + } + + // initialize the added bodies in the fill level and flag fields, and in the bubble model + void initAddedBodies(); + + protected: + void updateDestinationFields() override; + + std::vector< Body_T > bodies_; + + std::vector< std::function< void(Body_T&) > > moveFunctions_; + + geometry::initializer::OverlapFieldFromBody srcFillInitializer_; + geometry::initializer::OverlapFieldFromBody dstFillInitializer_; +}; + +} // namespace bubble_model +} // namespace free_surface +} // namespace walberla + +#include "BubbleBodyMover.impl.h" \ No newline at end of file diff --git a/tests/lbm/free_surface/bubble_model/BubbleBodyMover.impl.h b/tests/lbm/free_surface/bubble_model/BubbleBodyMover.impl.h new file mode 100644 index 000000000..f06d6ed68 --- /dev/null +++ b/tests/lbm/free_surface/bubble_model/BubbleBodyMover.impl.h @@ -0,0 +1,99 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file BubbleBodyMover.impl.h +//! \ingroup lbm/free_surface/bubble_model +//! \author Martin Bauer +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +// +//====================================================================================================================== + +#include "geometry/bodies/AABBBody.h" +#include "geometry/bodies/Ellipsoid.h" +#include "geometry/bodies/Sphere.h" + +#include "lbm/free_surface/bubble_model/Geometry.h" + +#include "BubbleBodyMover.h" + +namespace walberla +{ +namespace free_surface +{ +namespace bubble_model +{ +template< typename Body_T, typename Stencil_T > +BubbleBodyMover< Body_T, Stencil_T >::BubbleBodyMover(const std::shared_ptr< StructuredBlockStorage >& blockStorage, + const std::shared_ptr< BubbleModel< Stencil_T > >& bubbleModel) + : BubbleModelTester< Stencil_T >(blockStorage, bubbleModel), + srcFillInitializer_(*blockStorage, BubbleModelTester< Stencil_T >::srcFillLevelFieldID_), + dstFillInitializer_(*blockStorage, BubbleModelTester< Stencil_T >::dstFillLevelFieldID_) +{} + +template< typename Body_T, typename Stencil_T > +void BubbleBodyMover< Body_T, Stencil_T >::initAddedBodies() +{ + // initialize fill levels + for (auto body = bodies_.begin(); body != bodies_.end(); ++body) + { + srcFillInitializer_.init(*body, false); + dstFillInitializer_.init(*body, false); + } + + // set flags + BubbleModelTester< Stencil_T >::srcFlagsFromSrcFills(); + BubbleModelTester< Stencil_T >::dstFlagsFromDstFills(); + + // initialize bubble model + BubbleModelTester< Stencil_T >::bubbleModel_->initFromFillLevelField( + BubbleModelTester< Stencil_T >::dstFillLevelFieldID_); +} + +template< typename Body_T, typename Stencil_T > +void BubbleBodyMover< Body_T, Stencil_T >::updateDestinationFields() +{ + using BubbleModelTester_T = BubbleModelTester< Stencil_T >; + + // move bodies according to moveFunctions_ + for (uint_t i = uint_c(0); i < bodies_.size(); ++i) + { + moveFunctions_[i](bodies_[i]); + } + + // clear destination field + for (auto blockIt = this->blockStorage_->begin(); blockIt != this->blockStorage_->end(); ++blockIt) + { + typename BubbleModelTester_T::FlagField_T* const flagField = + blockIt->template getData< typename BubbleModelTester_T::FlagField_T >(this->dstFlagFieldID_); + typename BubbleModelTester_T::ScalarField_T* const fillField = + blockIt->template getData< typename BubbleModelTester_T::ScalarField_T >(this->dstFillLevelFieldID_); + + flagField->setWithGhostLayer(typename BubbleModelTester_T::flag_t(0)); + fillField->setWithGhostLayer(real_c(1.0)); + } + + // update fill level field with new body position + for (auto body = bodies_.begin(); body != bodies_.end(); ++body) + { + dstFillInitializer_.init(*body, false); + } + + // update flags + this->dstFlagsFromDstFills(); +} + +} // namespace bubble_model +} // namespace free_surface +} // namespace walberla diff --git a/tests/lbm/free_surface/bubble_model/BubbleInitializationTest.cpp b/tests/lbm/free_surface/bubble_model/BubbleInitializationTest.cpp new file mode 100644 index 000000000..08641c5b8 --- /dev/null +++ b/tests/lbm/free_surface/bubble_model/BubbleInitializationTest.cpp @@ -0,0 +1,154 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file BubbleInitializationTest.cpp +//! \ingroup lbm/free_surface/bubble_model +//! \author Martin Bauer +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Test bubble initialization from fill level by evaluating initial bubble volumes (within and across blocks). +// +//====================================================================================================================== + +#include "blockforest/Initialization.h" + +#include "core/Environment.h" +#include "core/debug/TestSubsystem.h" +#include "core/math/Constants.h" +#include "core/mpi/MPIManager.h" + +#include "field/AddToStorage.h" +#include "field/Printers.h" + +#include "geometry/bodies/Sphere.h" + +#include "lbm/free_surface/bubble_model/BubbleModel.h" +#include "lbm/free_surface/bubble_model/Geometry.h" + +#include "stencil/D2Q9.h" +#include "stencil/D3Q19.h" +#include "stencil/D3Q27.h" + +namespace walberla +{ +namespace free_surface +{ +namespace BubbleInitializationTest +{ +// define field +using ScalarField_T = GhostLayerField< real_t, 1 >; + +// class derived from BubbleModel to access its protected members for testing purposes +template< typename Stencil_T > +class BubbleModelTest : public bubble_model::BubbleModel< Stencil_T > +{ + public: + BubbleModelTest(const std::shared_ptr< StructuredBlockForest >& blockForest) + : bubble_model::BubbleModel< Stencil_T >(blockForest, true) + {} + + real_t getBubbleInitVolume(bubble_model::BubbleID id) + { + WALBERLA_ASSERT_GREATER(bubble_model::BubbleModel< Stencil_T >::getBubbles().size(), id); + return bubble_model::BubbleModel< Stencil_T >::getBubbles()[id].getInitVolume(); + } +}; // class BubbleModelTest + +template< typename Stencil_T > +void testBubbleInitialization() +{ + // define the domain size + const Vector3< uint_t > numBlocks(uint_c(2), uint_c(1), uint_c(1)); + const Vector3< uint_t > domainSize(uint_c(56), uint_c(16), uint_c(1)); + const Vector3< uint_t > cellsPerBlock(domainSize[0] / numBlocks[0], domainSize[1] / numBlocks[1], + domainSize[2] / numBlocks[2]); + + // create block grid + const std::shared_ptr< StructuredBlockForest > blockForest = + blockforest::createUniformBlockGrid(numBlocks[0], numBlocks[1], numBlocks[2], // blocks + cellsPerBlock[0], cellsPerBlock[1], cellsPerBlock[2], // cells + real_c(1.0), // dx + true, // one block per process + true, false, false, // periodicity + true); // global info + + // add fill level field + BlockDataID fillFieldID = + field::addToStorage< ScalarField_T >(blockForest, "Fill levels", real_c(1.0), field::fzyx, uint_c(1)); + + const real_t xQuarter = real_c(domainSize[0]) / real_c(4); + const real_t yHalf = real_c(domainSize[1]) / real_c(2); + + // add sphere in the left half (in x-direction) of the domain + bubble_model::addBodyToFillLevelField( + *blockForest, fillFieldID, + geometry::Sphere(Vector3< real_t >(real_c(0.75) * xQuarter, yHalf, real_c(0)), xQuarter * real_c(0.25)), true); + + // add sphere in the center of the domain (across blockForest) + bubble_model::addBodyToFillLevelField( + *blockForest, fillFieldID, + geometry::Sphere(Vector3< real_t >(real_c(domainSize[0]) * real_c(0.5), yHalf, real_c(0)), yHalf), true); + + // add sphere in the right half (in x-direction) of the domain + bubble_model::addBodyToFillLevelField( + *blockForest, fillFieldID, + geometry::Sphere(Vector3< real_t >(real_c(3.25) * xQuarter, yHalf, real_c(0)), xQuarter * real_c(0.25)), true); + + // create bubble model + BubbleModelTest< Stencil_T > bubbleModel(blockForest); + bubbleModel.initFromFillLevelField(fillFieldID); + + // test correctness of bubble volumes (bubble IDs were determined empirically for this test) + // left bubble + WALBERLA_CHECK_LESS( + std::abs(bubbleModel.getBubbleInitVolume(2) - (xQuarter * real_c(0.25) * xQuarter * real_c(0.25) * math::pi)), + real_c(1.07)); + + // center bubble + WALBERLA_CHECK_LESS(std::abs(bubbleModel.getBubbleInitVolume(0) - (yHalf * yHalf * math::pi)), real_c(1.12)); + + // right bubble + WALBERLA_CHECK_LESS( + std::abs(bubbleModel.getBubbleInitVolume(1) - (xQuarter * real_c(0.25) * xQuarter * real_c(0.25) * math::pi)), + real_c(1.07)); + + MPIManager::instance()->resetMPI(); +} + +int main(int argc, char** argv) +{ + debug::enterTestMode(); + Environment env(argc, argv); + + auto mpiManager = MPIManager::instance(); + + WALBERLA_CHECK_EQUAL(mpiManager->numProcesses(), 2); + + WALBERLA_LOG_INFO_ON_ROOT("Testing with D2Q9 stencil."); + testBubbleInitialization< stencil::D2Q9 >(); + + WALBERLA_LOG_INFO_ON_ROOT("Testing with D3Q19 stencil."); + testBubbleInitialization< stencil::D3Q19 >(); + + WALBERLA_LOG_INFO_ON_ROOT("Testing with D3Q27 stencil."); + testBubbleInitialization< stencil::D3Q27 >(); + + return EXIT_SUCCESS; +} + +} // namespace BubbleInitializationTest +} // namespace free_surface +} // namespace walberla + +int main(int argc, char** argv) { return walberla::free_surface::BubbleInitializationTest::main(argc, argv); } \ No newline at end of file diff --git a/tests/lbm/free_surface/bubble_model/BubbleModelTester.h b/tests/lbm/free_surface/bubble_model/BubbleModelTester.h new file mode 100644 index 000000000..f70d71476 --- /dev/null +++ b/tests/lbm/free_surface/bubble_model/BubbleModelTester.h @@ -0,0 +1,96 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file BubbleModelTester.h +//! \ingroup lbm/free_surface/bubble_model +//! \author Martin Bauer +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Helper class for bubble model test cases. +//! +//! Provides a source and destination field for fill levels and flags. A derived class is supposed to work on the +//! destination fields. This (base) class then updates the bubble model and converts cells with respect to any changes +//! in the destination fields. +// +//====================================================================================================================== + +#pragma once + +#include "core/math/Vector3.h" + +#include "field/FlagField.h" +#include "field/GhostLayerField.h" + +#include "lbm/free_surface/bubble_model/BubbleModel.h" + +namespace walberla +{ +namespace free_surface +{ +namespace bubble_model +{ +template< typename Stencil_T > +class BubbleModelTester +{ + public: + // define fields + using flag_t = uint32_t; + using FlagField_T = FlagField< flag_t >; + using ScalarField_T = GhostLayerField< real_t, 1 >; + + BubbleModelTester(const std::shared_ptr< StructuredBlockStorage >& blockStorage, + const std::shared_ptr< BubbleModel< Stencil_T > >& bubbleModel); + virtual ~BubbleModelTester() = default; + + // swap source and destination fill level fields; + // swap source and destination flag fields; + // update bubble model; + // update destination fields; + virtual void operator()(); + + inline ConstBlockDataID getFlagFieldID() const { return srcFlagFieldID_; } + inline ConstBlockDataID getFillLevelFieldID() const { return srcFillLevelFieldID_; } + + private: + // report cell conversions from liquid to interface; + // report changes in the fill level (destination fill level - source fill level); + // report cell conversions from interface to liquid; + // check interface layer for correctness + void updateBubbleModel(); + + protected: + virtual void updateDestinationFields() = 0; + + // initialize flag field from fill level + void srcFlagsFromSrcFills(); + void dstFlagsFromDstFills(); + + std::shared_ptr< StructuredBlockStorage > blockStorage_; + std::shared_ptr< BubbleModel< Stencil_T > > bubbleModel_; + + uint32_t liquidFlag_; + uint32_t gasFlag_; + uint32_t interfaceFlag_; + + BlockDataID srcFlagFieldID_; + BlockDataID dstFlagFieldID_; + BlockDataID srcFillLevelFieldID_; + BlockDataID dstFillLevelFieldID_; +}; + +} // namespace bubble_model +} // namespace free_surface +} // namespace walberla + +#include "BubbleModelTester.impl.h" \ No newline at end of file diff --git a/tests/lbm/free_surface/bubble_model/BubbleModelTester.impl.h b/tests/lbm/free_surface/bubble_model/BubbleModelTester.impl.h new file mode 100644 index 000000000..230655c16 --- /dev/null +++ b/tests/lbm/free_surface/bubble_model/BubbleModelTester.impl.h @@ -0,0 +1,160 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file BubbleModelTester.impl.h +//! \ingroup lbm/free_surface/bubble_model +//! \author Martin Bauer +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +// +//====================================================================================================================== + +#include "field/AddToStorage.h" + +#include "lbm/free_surface/bubble_model/Geometry.h" + +#include "BubbleModelTester.h" + +namespace walberla +{ +namespace free_surface +{ +namespace bubble_model +{ +template< typename Stencil_T > +BubbleModelTester< Stencil_T >::BubbleModelTester(const std::shared_ptr< StructuredBlockStorage >& blockStorage, + const std::shared_ptr< BubbleModel< Stencil_T > >& bubbleModel) + : blockStorage_(blockStorage), bubbleModel_(bubbleModel) +{ + // add flag fields + srcFlagFieldID_ = field::addFlagFieldToStorage< FlagField_T >(blockStorage_, "FlagFieldSrc"); + dstFlagFieldID_ = field::addFlagFieldToStorage< FlagField_T >(blockStorage_, "FlagFieldDst"); + + // add fill level fields + srcFillLevelFieldID_ = + field::addToStorage< ScalarField_T >(blockStorage_, "FillLevelSrc", real_c(1.0), field::fzyx, uint_c(1)); + dstFillLevelFieldID_ = + field::addToStorage< ScalarField_T >(blockStorage_, "FillLevelDst", real_c(1.0), field::fzyx, uint_c(1)); + + // register flags + for (auto blockIt = blockStorage->begin(); blockIt != blockStorage->end(); ++blockIt) + { + FlagField_T* const srcFlagField = blockIt->getData< FlagField_T >(srcFlagFieldID_); + FlagField_T* const dstFlagField = blockIt->getData< FlagField_T >(dstFlagFieldID_); + + liquidFlag_ = srcFlagField->registerFlag("liquid", uint_c(1)); + gasFlag_ = srcFlagField->registerFlag("gas", uint_c(2)); + interfaceFlag_ = srcFlagField->registerFlag("interface", uint_c(3)); + + dstFlagField->registerFlag("liquid", uint_c(1)); + dstFlagField->registerFlag("gas", uint_c(2)); + dstFlagField->registerFlag("interface", uint_c(3)); + } +} + +template< typename Stencil_T > +void BubbleModelTester< Stencil_T >::srcFlagsFromSrcFills() +{ + // initialize flag field from fill level + for (auto blockIt = blockStorage_->begin(); blockIt != blockStorage_->end(); ++blockIt) + { + FlagField_T* const srcFlagField = blockIt->getData< FlagField_T >(srcFlagFieldID_); + const ScalarField_T* const srcFillLevel = blockIt->getData< const ScalarField_T >(srcFillLevelFieldID_); + + bubble_model::setFlagFieldFromFillLevels(srcFlagField, srcFillLevel, "liquid", "gas", "interface"); + } +} + +template< typename Stencil_T > +void BubbleModelTester< Stencil_T >::dstFlagsFromDstFills() +{ + // initialize flag field from fill level + for (auto blockIt = blockStorage_->begin(); blockIt != blockStorage_->end(); ++blockIt) + { + FlagField_T* const dstFlagField = blockIt->getData< FlagField_T >(dstFlagFieldID_); + const ScalarField_T* const dstFillLevel = blockIt->getData< const ScalarField_T >(dstFillLevelFieldID_); + + bubble_model::setFlagFieldFromFillLevels(dstFlagField, dstFillLevel, "liquid", "gas", "interface"); + } +} + +template< typename Stencil_T > +void BubbleModelTester< Stencil_T >::operator()() +{ + // swap source and destination fill level fields, and flag fields + for (auto blockIt = blockStorage_->begin(); blockIt != blockStorage_->end(); ++blockIt) + { + ScalarField_T* const srcFillLevel = blockIt->getData< ScalarField_T >(srcFillLevelFieldID_); + ScalarField_T* const dstFillLevel = blockIt->getData< ScalarField_T >(dstFillLevelFieldID_); + + FlagField_T* const srcFlagField = blockIt->getData< FlagField_T >(srcFlagFieldID_); + FlagField_T* const dstFlagField = blockIt->getData< FlagField_T >(dstFlagFieldID_); + + srcFlagField->swapDataPointers(*dstFlagField); + srcFillLevel->swapDataPointers(*dstFillLevel); + } + + updateDestinationFields(); + updateBubbleModel(); +} + +template< typename Stencil_T > +void BubbleModelTester< Stencil_T >::updateBubbleModel() +{ + for (auto blockIt = blockStorage_->begin(); blockIt != blockStorage_->end(); ++blockIt) + { + IBlock* thisBlock = &(*blockIt); + + ScalarField_T* const srcFillLevel = blockIt->getData< ScalarField_T >(srcFillLevelFieldID_); + ScalarField_T* const dstFillLevel = blockIt->getData< ScalarField_T >(dstFillLevelFieldID_); + WALBERLA_ASSERT(srcFillLevel->hasSameSize(*dstFillLevel)); + WALBERLA_ASSERT_EQUAL_2(srcFillLevel->xyzSize(), dstFillLevel->xyzSize()); + + FlagField_T* const srcFlagField = blockIt->getData< FlagField_T >(srcFlagFieldID_); + FlagField_T* const dstFlagField = blockIt->getData< FlagField_T >(dstFlagFieldID_); + WALBERLA_ASSERT_EQUAL_2(srcFlagField->xyzSize(), dstFlagField->xyzSize()); + + WALBERLA_ASSERT_EQUAL_2(srcFlagField->xyzSize(), srcFillLevel->xyzSize()); + + // report cell conversion from liquid to interface; explicitly avoid OpenMP when setting bubble IDs + WALBERLA_FOR_ALL_CELLS_OMP(srcFlagFieldIt, srcFlagField, dstFlagFieldIt, dstFlagField, omp critical, { + if (isFlagSet(srcFlagFieldIt, liquidFlag_) && isFlagSet(dstFlagFieldIt, interfaceFlag_)) + { + bubbleModel_->reportLiquidToInterfaceConversion(thisBlock, srcFlagFieldIt.cell()); + } + }) // WALBERLA_FOR_ALL_CELLS_OMP + + // report changes in the fill level + WALBERLA_FOR_ALL_CELLS_OMP( + srcFlagFieldIt, srcFlagField, dstFlagFieldIt, dstFlagField, srcFillIt, srcFillLevel, dstFillIt, dstFillLevel, + omp critical, { + if (isFlagSet(srcFlagFieldIt, interfaceFlag_) || isFlagSet(dstFlagFieldIt, interfaceFlag_)) + { + bubbleModel_->reportFillLevelChange(thisBlock, srcFillIt.cell(), *dstFillIt - *srcFillIt); + } + }) // WALBERLA_FOR_ALL_CELLS_OMP + + // report cell conversion from interface to liquid + WALBERLA_FOR_ALL_CELLS_OMP(srcFlagFieldIt, srcFlagField, dstFlagFieldIt, dstFlagField, omp critical, { + if (isFlagSet(srcFlagFieldIt, interfaceFlag_) && isFlagSet(dstFlagFieldIt, liquidFlag_)) + { + bubbleModel_->reportInterfaceToLiquidConversion(thisBlock, srcFlagFieldIt.cell()); + } + }) // WALBERLA_FOR_ALL_CELLS_OMP + } +} + +} // namespace bubble_model +} // namespace free_surface +} // namespace walberla diff --git a/tests/lbm/free_surface/bubble_model/MergeAndSplitTest.cpp b/tests/lbm/free_surface/bubble_model/MergeAndSplitTest.cpp new file mode 100644 index 000000000..b9dab0763 --- /dev/null +++ b/tests/lbm/free_surface/bubble_model/MergeAndSplitTest.cpp @@ -0,0 +1,230 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file MergeAndSplitTest.cpp +//! \ingroup lbm/free_surface/bubble_model +//! \author Martin Bauer +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Test bubble merging and splitting in a complex multi-process scenario. +//! +//! Initialize the fill levels, flags and bubble model from image MergeAndSplitTestUnconnected.png. This sets up a +//! complex scenario of 12 bubbles that are located on 10 processes. Bubble merging is tested by loading image +//! MergeAndSplitTestConnected.png. By loading the initial image in a second time step, bubble splitting is tested. +// +//====================================================================================================================== + +#include "blockforest/Initialization.h" + +#include "core/Environment.h" +#include "core/debug/TestSubsystem.h" +#include "core/mpi/MPIManager.h" + +#include "geometry/initializer/ScalarFieldFromGrayScaleImage.h" +#include "geometry/structured/GrayScaleImage.h" + +#include "lbm/free_surface/bubble_model/BubbleModel.h" + +#include "stencil/D2Q9.h" +#include "stencil/D3Q19.h" +#include "stencil/D3Q27.h" + +#include "timeloop/SweepTimeloop.h" + +#include <vector> + +#include "BubbleModelTester.h" + +namespace walberla +{ +namespace free_surface +{ +namespace MergeAndSplitTest +{ +// class derived from BubbleModel to access its protected members for testing purposes +template< typename Stencil_T > +class BubbleModelTest : public bubble_model::BubbleModel< Stencil_T > +{ + public: + BubbleModelTest(const std::shared_ptr< StructuredBlockForest >& blockStorage) + : bubble_model::BubbleModel< Stencil_T >(blockStorage, true) + {} + + static void testComplexMerge(); +}; // class BubbleModelTest + +// initialize and update a source and destination field for fill levels and flags from images; in an alternating manner, +// update the fields such that the bubble model must either handle bubble merging or splitting on every second call +template< typename Stencil_T > +class ImageMover : public bubble_model::BubbleModelTester< Stencil_T > +{ + public: + ImageMover(const std::shared_ptr< StructuredBlockStorage >& blockStorage, + const std::shared_ptr< bubble_model::BubbleModel< Stencil_T > >& bubbleModel) + : bubble_model::BubbleModelTester< Stencil_T >(blockStorage, bubbleModel), + imgInitializerSrc_(*blockStorage, bubble_model::BubbleModelTester< Stencil_T >::srcFillLevelFieldID_), + imgInitializerDst_(*blockStorage, bubble_model::BubbleModelTester< Stencil_T >::dstFillLevelFieldID_), + calls_(uint_c(0)) + { + // load image of test scenario with bubbles that are not connected + geometry::GrayScaleImage img("MergeAndSplitTestUnconnected.png"); + + // initialize fill level field from image + imgInitializerSrc_.init(img, 2, false); + imgInitializerDst_.init(img, 2, false); + + // initialize flag field + bubble_model::BubbleModelTester< Stencil_T >::srcFlagsFromSrcFills(); + bubble_model::BubbleModelTester< Stencil_T >::dstFlagsFromDstFills(); + + // initialize bubble model + bubble_model::BubbleModelTester< Stencil_T >::bubbleModel_->initFromFillLevelField( + bubble_model::BubbleModelTester< Stencil_T >::dstFillLevelFieldID_); + } + + // in an alternating manner, update fill levels such that the bubbles must either merge or split on every second call + void updateDestinationFields() override + { + // load image of test scenario with bubbles that are not connected + geometry::GrayScaleImage unconnected("MergeAndSplitTestUnconnected.png"); + + // load image of test scenario with bubbles that are connected + geometry::GrayScaleImage connected("MergeAndSplitTestConnected.png"); + + if (uint_c(calls_) % uint_c(2) == uint_c(0)) + { + // bubbles must merge (destination field is updated to be connected) + imgInitializerSrc_.init(unconnected, 2, false); + imgInitializerDst_.init(connected, 2, false); + } + else + { + // bubbles must split (destination field is updated to be unconnected) + imgInitializerSrc_.init(connected, 2, false); + imgInitializerDst_.init(unconnected, 2, false); + } + + // update flag field + bubble_model::BubbleModelTester< Stencil_T >::srcFlagsFromSrcFills(); + bubble_model::BubbleModelTester< Stencil_T >::dstFlagsFromDstFills(); + + ++calls_; + } + + private: + geometry::initializer::ScalarFieldFromGrayScaleImage imgInitializerSrc_; + geometry::initializer::ScalarFieldFromGrayScaleImage imgInitializerDst_; + uint_t calls_; +}; // class ImageMover + +template< typename Stencil_T > +void BubbleModelTest< Stencil_T >::testComplexMerge() +{ + auto mpiManager = MPIManager::instance(); + + WALBERLA_CHECK_EQUAL(mpiManager->numProcesses(), 10); + + // define the domain size + const Vector3< uint_t > numBlocks(uint_c(2), uint_c(5), uint_c(1)); + const Vector3< uint_t > domainSize(uint_c(100), uint_c(200), uint_c(1)); + const Vector3< uint_t > cellsPerBlock(domainSize[0] / numBlocks[0], domainSize[1] / numBlocks[1], + domainSize[2] / numBlocks[2]); + + // create block grid + const std::shared_ptr< StructuredBlockForest > blockForest = + blockforest::createUniformBlockGrid(numBlocks[0], numBlocks[1], numBlocks[2], // blocks + cellsPerBlock[0], cellsPerBlock[1], cellsPerBlock[2], // cells + real_c(1.0), // dx + true, // one block per process + true, false, false); // periodicity + + // create bubble model + std::shared_ptr< BubbleModelTest > bubbleModel = std::make_shared< BubbleModelTest >(blockForest); + + // create imageMover that initializes the fill level field, and bubble model; updates the fill level field in an + // alternating manner, such that bubbles must either merge or split on every second call + ImageMover< Stencil_T > imageMover(blockForest, bubbleModel); + + // create timeloop + SweepTimeloop timeloop(blockForest, uint_c(10)); + + // add imageMover to timeloop + timeloop.addFuncBeforeTimeStep(imageMover, "UpdateDomainFromImage"); + + // update bubble model in timeloop + timeloop.addFuncAfterTimeStep(std::bind(&bubble_model::BubbleModel< Stencil_T >::update, bubbleModel)); + + // ensure correctness of initialization (number of bubbles) + WALBERLA_CHECK_EQUAL(bubbleModel->getBubbles().size(), 12); + + // compute total volume of all bubbles + real_t volumeBefore = real_c(0); + for (auto b = bubbleModel->getBubbles().begin(); b != bubbleModel->getBubbles().end(); ++b) + { + volumeBefore += b->getCurrentVolume(); + } + + // ensure correctness of initialization (total volume of all bubbles) + WALBERLA_CHECK_LESS(std::abs(volumeBefore - real_c(8905.51)), 0.1); + + // merge bubbles + timeloop.singleStep(); + + // there must be a single bubble in the system + WALBERLA_CHECK_EQUAL(bubbleModel->getBubbles().size(), 1); + + // the total volume of this single bubble must be slightly larger than the initial volume of all bubbles + real_t volumeAfter = bubbleModel->getBubbles()[0].getCurrentVolume(); + WALBERLA_CHECK_LESS(std::abs(volumeAfter - real_c(8918.41)), 0.1); + + // split bubbles + timeloop.singleStep(); + + // the number of bubbles must be as before merging + WALBERLA_CHECK_EQUAL(bubbleModel->getBubbles().size(), 12); + + // compute total volume of all bubbles + real_t volumeAfterSplit = real_c(0); + for (auto b = bubbleModel->getBubbles().begin(); b != bubbleModel->getBubbles().end(); ++b) + { + volumeAfterSplit += b->getCurrentVolume(); + } + + // the total volume of all bubbles must be as before merging + WALBERLA_CHECK_LESS(std::abs(volumeAfterSplit - real_c(8905.51)), 0.1); + + MPIManager::instance()->resetMPI(); +} + +int main(int argc, char** argv) +{ + debug::enterTestMode(); + Environment env(argc, argv); + + WALBERLA_LOG_INFO_ON_ROOT("Testing with D2Q9 stencil."); + BubbleModelTest< stencil::D2Q9 >::testComplexMerge(); + + WALBERLA_LOG_INFO_ON_ROOT("Testing with D3Q19 stencil."); + BubbleModelTest< stencil::D3Q19 >::testComplexMerge(); + + WALBERLA_LOG_INFO_ON_ROOT("Testing with D3Q27 stencil."); + BubbleModelTest< stencil::D3Q27 >::testComplexMerge(); + + return EXIT_SUCCESS; +} +} // namespace MergeAndSplitTest +} // namespace free_surface +} // namespace walberla + +int main(int argc, char** argv) { return walberla::free_surface::MergeAndSplitTest::main(argc, argv); } \ No newline at end of file diff --git a/tests/lbm/free_surface/bubble_model/MergeAndSplitTestConnected.png b/tests/lbm/free_surface/bubble_model/MergeAndSplitTestConnected.png new file mode 100644 index 0000000000000000000000000000000000000000..08b183c1a3e0cef8d84530859474d78a135ef764 GIT binary patch literal 1681 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 bU(o#l%_AWiKn1LV00000NkvXXu0mjf{pt}> literal 0 HcmV?d00001 diff --git a/tests/lbm/free_surface/bubble_model/MergeAndSplitTestUnconnected.png b/tests/lbm/free_surface/bubble_model/MergeAndSplitTestUnconnected.png new file mode 100644 index 0000000000000000000000000000000000000000..0e6039cdcffe6832eb04643fd830b6c8fd3bb7a6 GIT binary patch literal 1649 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 literal 0 HcmV?d00001 diff --git a/tests/lbm/free_surface/bubble_model/MergeInformationTest.cpp b/tests/lbm/free_surface/bubble_model/MergeInformationTest.cpp new file mode 100644 index 000000000..84e559b61 --- /dev/null +++ b/tests/lbm/free_surface/bubble_model/MergeInformationTest.cpp @@ -0,0 +1,231 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file MergeInformationTest.cpp +//! \ingroup lbm/free_surface/bubble_model +//! \author Martin Bauer +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Test bubble merge and reordering, merge registering, and merge communication. +// +//====================================================================================================================== + +#include "lbm/free_surface/bubble_model/MergeInformation.h" + +#include "core/Environment.h" +#include "core/debug/TestSubsystem.h" +#include "core/logging/Logging.h" +#include "core/mpi/MPIManager.h" + +#include <iostream> +#include <vector> + +namespace std +{ +// overload std::cout for printing vector content +template< typename T > +std::ostream& operator<<(std::ostream& os, const std::vector< T >& vec) +{ + os << "Vector (" << vec.size() << "): "; + for (auto i = vec.begin(); i != vec.end(); ++i) + { + os << *i << " "; + } + os << std::endl; + + return os; +} +} // namespace std + +namespace walberla +{ +namespace free_surface +{ +namespace bubble_model +{ +// test if renameVec_ and vecCompare are equal +void checkRenameVec(const bubble_model::MergeInformation& mi, const std::vector< bubble_model::BubbleID >& vecCompare) +{ + // renameVec_ and vecCompare must have the same size + WALBERLA_ASSERT_EQUAL(mi.renameVec_.size(), vecCompare.size()); + + bool equal = true; + for (size_t i = 0; i < mi.renameVec_.size(); ++i) + if (mi.renameVec_[i] != vecCompare[i]) + { + equal = false; + break; + } + + WALBERLA_CRITICAL_SECTION_START + if (!equal) + { + WALBERLA_LOG_WARNING("Process " << MPIManager::instance()->rank() + << ": rename vector different than expected result.\n" + << "\tExpected:\n" + << "\t\t" << vecCompare << "\tResult:\n" + << "\t\t" << mi.renameVec_); + } + WALBERLA_CRITICAL_SECTION_END + + WALBERLA_CHECK(equal); +} +} // namespace bubble_model + +namespace MergeInformationTest +{ +void mergeAndReorderTest1() +{ + std::vector< bubble_model::Bubble > bubbles; + bubbles.emplace_back(real_c(10)); // BubbleID 0 + bubbles.emplace_back(bubble_model::Bubble(real_c(11))); // BubbleID 1 + bubbles.emplace_back(bubble_model::Bubble(real_c(12))); // BubbleID 2 + bubbles.emplace_back(bubble_model::Bubble(real_c(13))); // BubbleID 3 + + bubble_model::MergeInformation mi(bubbles.size()); + mi.registerMerge(0, 1); + + // merge bubbles with IDs 0 and 1 to ID 0: + // - ID 4 gets assigned ID 1 (highest ID, i.e., last position is copied to free space position 1) + // - highest ID is now 3 and bubbles vector must have reduced to size 2 + mi.mergeAndReorderBubbleVector(bubbles); + + WALBERLA_CHECK_EQUAL(bubbles.size(), 3); + WALBERLA_CHECK_FLOAT_EQUAL(bubbles[0].getInitVolume(), real_c(10 + 11)); + WALBERLA_CHECK_FLOAT_EQUAL(bubbles[1].getInitVolume(), real_c(13)); + WALBERLA_CHECK_FLOAT_EQUAL(bubbles[2].getInitVolume(), real_c(12)); +} + +void mergeAndReorderTest2() +{ + std::vector< bubble_model::Bubble > bubbles; + bubbles.emplace_back(bubble_model::Bubble(real_c(10))); // BubbleID 0 + bubbles.emplace_back(bubble_model::Bubble(real_c(11))); // BubbleID 1 + bubbles.emplace_back(bubble_model::Bubble(real_c(12))); // BubbleID 2 + bubbles.emplace_back(bubble_model::Bubble(real_c(13))); // BubbleID 3 + bubbles.emplace_back(bubble_model::Bubble(real_c(14))); // BubbleID 4 + bubbles.emplace_back(real_c(15)); // BubbleID 5 + + bubble_model::MergeInformation mi(bubbles.size()); + mi.registerMerge(4, 3); + mi.registerMerge(3, 2); + mi.registerMerge(0, 1); + + // merge bubbles with IDs 4, 3, 2 to ID 2, merge bubbles with IDs 0 and 1 to ID 0: + // - ID 5 gets assigned ID 1 (highest ID, i.e., last position is copied to free space position 1) + // - highest ID is now 3 and bubbles vector must have reduced to size 2 + mi.mergeAndReorderBubbleVector(bubbles); + + WALBERLA_CHECK_EQUAL(bubbles.size(), 3); + WALBERLA_CHECK_FLOAT_EQUAL(bubbles[0].getInitVolume(), real_c(10 + 11)); + WALBERLA_CHECK_FLOAT_EQUAL(bubbles[1].getInitVolume(), real_c(15)); + WALBERLA_CHECK_FLOAT_EQUAL(bubbles[2].getInitVolume(), real_c(12 + 13 + 14)); +} + +void mergeRegisterTest() +{ + // create new MergeInformation with 6 bubbles + bubble_model::MergeInformation mi(6); + + // initial ID distribution + std::vector< bubble_model::BubbleID > correctRenameVec = { 0, 1, 2, 3, 4, 5 }; + checkRenameVec(mi, correctRenameVec); + + // Function registerMerge() only registers the merge by (temporarily) renaming bubble IDs appropriately. Therefore, + // in below comments, it is more meaningful to write "position x (in renameVec_) is renamed to the ID at position + // y" than "ID x is renamed to ID y". + + // position 5 is renamed to the ID at position 3 + mi.registerMerge(5, 3); + correctRenameVec = { 0, 1, 2, 3, 4, 3 }; + checkRenameVec(mi, correctRenameVec); + + // position 3 is renamed to the ID at position 1 + mi.registerMerge(3, 1); + correctRenameVec = { 0, 1, 2, 1, 4, 3 }; + checkRenameVec(mi, correctRenameVec); + + // since position 3 was already renamed to the ID at position 1, registerMerge(1, 0) is called internally and + // position 1 is renamed to the ID at position 0; position 3 is renamed to the ID at position 0 + mi.registerMerge(3, 0); + correctRenameVec = { 0, 0, 2, 0, 4, 3 }; + checkRenameVec(mi, correctRenameVec); + + // since position 5 was already renamed to the ID of position 3, registerMerge(3, 2) is called internally; since + // position 3 was already renamed to the ID of position 0, registerMerge(2, 0) is called internally; position 2 is + // renamed to the ID at position 0, position 5 is renamed to the ID at position 0 + mi.registerMerge(5, 2); + correctRenameVec = { 0, 0, 0, 0, 4, 0 }; + checkRenameVec(mi, correctRenameVec); +} + +void mergeCommunicationTest() +{ + auto mpiManager = MPIManager::instance(); + + // this test is only meaningful with multiple processes + if (!mpiManager->isMPIInitialized() || mpiManager->numProcesses() < 3) { return; } + + // create new MergeInformation with 5 bubbles and renameVec_={ 0, 1, 2, 3, 4 } + bubble_model::MergeInformation mi(5); + + if (mpiManager->rank() == 0) { mi.registerMerge(1, 0); } + else + { + if (mpiManager->rank() == 1) + { + mi.registerMerge(3, 2); + mi.registerMerge(2, 1); + } + else + { + if (mpiManager->rank() == 2) { mi.registerMerge(4, 1); } + } + } + + // before communication: + // process 0: renameVec_={ 0, 0, 2, 3, 4 } + // process 1: renameVec_={ 0, 1, 1, 1, 4 } + // process 2: renameVec_={ 0, 1, 2, 3, 1 } + + mi.communicateMerges(); + + // after communication: + // process 0 to 2: renameVec_={ 0, 0, 0, 0, 0 } + std::vector< bubble_model::BubbleID > res = { 0, 0, 0, 0, 0 }; + checkRenameVec(mi, res); +} + +int main(int argc, char** argv) +{ + debug::enterTestMode(); + Environment env(argc, argv); + + // the MPI communicator is normally created with the block forest; we will not use a block forest in this test and + // thus choose the MPI communicator manually + WALBERLA_MPI_SECTION() { MPIManager::instance()->useWorldComm(); } + + mergeAndReorderTest1(); + mergeAndReorderTest2(); + + mergeRegisterTest(); + mergeCommunicationTest(); + + return EXIT_SUCCESS; +} +} // namespace MergeInformationTest +} // namespace free_surface +} // namespace walberla + +int main(int argc, char** argv) { return walberla::free_surface::MergeInformationTest::main(argc, argv); } diff --git a/tests/lbm/free_surface/bubble_model/MovingSpheresTest.cpp b/tests/lbm/free_surface/bubble_model/MovingSpheresTest.cpp new file mode 100644 index 000000000..36f09079f --- /dev/null +++ b/tests/lbm/free_surface/bubble_model/MovingSpheresTest.cpp @@ -0,0 +1,173 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file MovingSpheresTest.cpp +//! \ingroup lbm/free_surface/bubble_model +//! \author Martin Bauer +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! +//! \brief Test volume conservation of moving bubbles, bubble merging and bubble splitting. +//! +//! A spherical bubble is moved towards a static spherical bubble in the center of the domain. The bubbles merge and +//! split again, as the movement of the spherical volume is continued after merging. +// +//====================================================================================================================== + +#include "blockforest/Initialization.h" + +#include "core/Environment.h" +#include "core/debug/TestSubsystem.h" +#include "core/mpi/MPIManager.h" + +#include "geometry/bodies/Sphere.h" + +#include "lbm/free_surface/bubble_model/BubbleModel.h" + +#include "stencil/D2Q9.h" +#include "stencil/D3Q19.h" +#include "stencil/D3Q27.h" + +#include "timeloop/SweepTimeloop.h" + +#include "BubbleBodyMover.h" + +namespace walberla +{ +namespace free_surface +{ +namespace MovingSpheresTest +{ +// class derived from BubbleModel to access its protected members for testing purposes +template< typename Stencil_T > +class BubbleModelTest : public bubble_model::BubbleModel< Stencil_T > +{ + public: + BubbleModelTest(const std::shared_ptr< StructuredBlockForest >& blockForest) + : bubble_model::BubbleModel< Stencil_T >(blockForest, true) + {} + + static void testMovingSpheres(); +}; // class BubbleModelTest + +template< typename Stencil_T > +void BubbleModelTest< Stencil_T >::testMovingSpheres() +{ + auto mpiManager = MPIManager::instance(); + + WALBERLA_CHECK_EQUAL(mpiManager->numProcesses(), 2); + + // define the domain size + const Vector3< uint_t > numBlocks(uint_c(2), uint_c(1), uint_c(1)); + Vector3< uint_t > domainSize(uint_c(60), uint_c(16), uint_c(16)); + + if (Stencil_T::D == uint_c(2)) { domainSize[2] = uint_c(1); } + + const Vector3< uint_t > cellsPerBlock(domainSize[0] / numBlocks[0], domainSize[1] / numBlocks[1], + domainSize[2] / numBlocks[2]); + + // create block grid + const std::shared_ptr< StructuredBlockForest > blockForest = + blockforest::createUniformBlockGrid(numBlocks[0], numBlocks[1], numBlocks[2], // blocks + cellsPerBlock[0], cellsPerBlock[1], cellsPerBlock[2], // cells + real_c(1.0), // dx + true, // one block per process + true, false, false); // periodicity + + // create bubble model + std::shared_ptr< BubbleModelTest > bubbleModel = std::make_shared< BubbleModelTest >(blockForest); + + // create bubble body mover for moving bubbles + bubble_model::BubbleBodyMover< geometry::Sphere, Stencil_T > bubbleSphereMover(blockForest, bubbleModel); + + Vector3< real_t > domainCenter(real_c(domainSize[0]) * real_c(0.5), real_c(domainSize[1]) * real_c(0.5), + real_c(domainSize[2]) * real_c(0.5)); + + // create a static spherical bubble in the center of the domain + geometry::Sphere sphereCenter(domainCenter, real_c(5)); + auto doNotMoveSphere = [](geometry::Sphere&) {}; + bubbleSphereMover.addBody(sphereCenter, doNotMoveSphere); + + // create a moving spherical bubble in the left (in x-direction) half of the domain + geometry::Sphere sphereLeft(Vector3< real_t >(real_c(8.0), domainCenter[1], domainCenter[2]), real_c(5)); + auto moveSphere = [](geometry::Sphere& sphere) { + // midpoint of the sphere is shifted by 1 cell in positive x-direction at each call + sphere.setMidpoint(sphere.midpoint() + Vector3< real_t >(real_c(1), real_c(0), real_c(0))); + }; + bubbleSphereMover.addBody(sphereLeft, moveSphere); + + // initialize the just added bubbles + bubbleSphereMover.initAddedBodies(); + + // create timeloop + SweepTimeloop timeloop(blockForest, uint_c(40)); + timeloop.addFuncBeforeTimeStep(bubbleSphereMover, "Move bubbles"); + timeloop.addFuncAfterTimeStep(std::bind(&bubble_model::BubbleModel< Stencil_T >::update, bubbleModel)); + + real_t singleBubbleVolume = real_c(523.346); + + if (Stencil_T::D == uint_c(2)) { singleBubbleVolume = real_c(78.1987); } + + uint_t timestep = uint_c(0); + + // at time step 8, there should still be two bubbles + for (; timestep < uint_c(8); ++timestep) + { + timeloop.singleStep(); + } + WALBERLA_CHECK_EQUAL(bubbleModel->getBubbles().size(), 2); + WALBERLA_CHECK_LESS(std::abs(bubbleModel->getBubbles()[0].getInitVolume() - singleBubbleVolume), real_c(0.5)); + WALBERLA_CHECK_LESS(std::abs(bubbleModel->getBubbles()[1].getInitVolume() - singleBubbleVolume), real_c(0.5)); + + // at time step 12 the bubbles should have merged + for (; timestep < uint_c(12); ++timestep) + { + timeloop.singleStep(); + } + WALBERLA_CHECK_EQUAL(bubbleModel->getBubbles().size(), 1); + WALBERLA_CHECK_LESS(std::abs(bubbleModel->getBubbles()[0].getInitVolume() - 2 * singleBubbleVolume), real_c(0.5)); + + // at time step 35 the bubbles should have split again + for (; timestep < uint_c(35); ++timestep) + { + timeloop.singleStep(); + } + WALBERLA_CHECK_EQUAL(bubbleModel->getBubbles().size(), 2); + WALBERLA_CHECK_LESS(std::abs(bubbleModel->getBubbles()[0].getInitVolume() - singleBubbleVolume), real_c(0.5)); + WALBERLA_CHECK_LESS(std::abs(bubbleModel->getBubbles()[1].getInitVolume() - singleBubbleVolume), real_c(0.5)); + + MPIManager::instance()->resetMPI(); +} + +int main(int argc, char** argv) +{ + debug::enterTestMode(); + Environment env(argc, argv); + + WALBERLA_LOG_INFO_ON_ROOT("Testing with D2Q9 stencil.") + BubbleModelTest< stencil::D2Q9 >::testMovingSpheres(); + + WALBERLA_LOG_INFO_ON_ROOT("Testing with D3Q19 stencil.") + BubbleModelTest< stencil::D3Q19 >::testMovingSpheres(); + + WALBERLA_LOG_INFO_ON_ROOT("Testing with D3Q27 stencil.") + BubbleModelTest< stencil::D3Q27 >::testMovingSpheres(); + + return EXIT_SUCCESS; +} +} // namespace MovingSpheresTest +} // namespace free_surface +} // namespace walberla + +int main(int argc, char** argv) { return walberla::free_surface::MovingSpheresTest::main(argc, argv); } \ No newline at end of file diff --git a/tests/lbm/free_surface/bubble_model/RegionalFloodFillTest.cpp b/tests/lbm/free_surface/bubble_model/RegionalFloodFillTest.cpp new file mode 100644 index 000000000..6a41cd8e9 --- /dev/null +++ b/tests/lbm/free_surface/bubble_model/RegionalFloodFillTest.cpp @@ -0,0 +1,88 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file RegionalFloodFillTest.cpp +//! \ingroup lbm/free_surface/bubble_model +//! \author Martin Bauer +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Test flood fill algorithm with D3Q7 and D3Q19 stencil. +// +//====================================================================================================================== + +#include "lbm/free_surface/bubble_model/RegionalFloodFill.h" + +#include "core/Environment.h" +#include "core/debug/TestSubsystem.h" + +#include "field/Printers.h" + +#include "stencil/D3Q19.h" +#include "stencil/D3Q7.h" + +namespace walberla +{ +namespace free_surface +{ +namespace RegionalFloodFillTest +{ +int main(int argc, char** argv) +{ + debug::enterTestMode(); + Environment walberlaEnv(argc, argv); + + // create 3x3 field with 1 ghost layer (initialized with 0) + GhostLayerField< int, 1 > field(3, 3, 1, 1, 0); + + // initial values of the field; the flood fill starting cell is marked with /**/; + // attention: the y coordinate is upside down in this array + const int initValues[5][5] = { + { 1, 1, 1, 1, 1 }, { 0, 0 /**/, 0, 1, 0 }, { 1, 0, 0, 1, 0 }, { 1, 0, 0, 1, 0 }, { 1, 1, 1, 0, 0 }, + }; + + // test scenario: detect the connection from (1,0) and (0,2) with starting cell (0,0) + for (cell_idx_t y = cell_idx_c(-1); y < cell_idx_c(4); ++y) + { + for (cell_idx_t x = cell_idx_c(-1); x < cell_idx_c(4); ++x) + { + field(x, y, cell_idx_c(0)) = initValues[y + 1][x + 1]; + } + } + + // print the initialized field (for debugging purposes) + // std::cout << "Initialized field:" << std::endl; + // field::printSlice(std::cout, field, 2, 0); + + // connection should not be found since search neighborhood (2) is too small + bubble_model::RegionalFloodFill< int, stencil::D3Q19 > neigh2_D3Q19( + &field, Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0)), stencil::S, 1, cell_idx_c(2)); + WALBERLA_CHECK_EQUAL(neigh2_D3Q19.connected(stencil::NW), false); + + // connection should be found since search neighborhood (3) is large enough + bubble_model::RegionalFloodFill< int, stencil::D3Q19 > neigh3_D3Q19( + &field, Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0)), stencil::S, 1, cell_idx_c(3)); + WALBERLA_CHECK_EQUAL(neigh3_D3Q19.connected(stencil::NW), true); + + // connection should be found since search neighborhood (3) is large enough + bubble_model::RegionalFloodFill< int, stencil::D3Q7 > neigh3_D3Q7( + &field, Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0)), stencil::S, 1, cell_idx_c(3)); + WALBERLA_CHECK_EQUAL(neigh3_D3Q7.connected(stencil::NW), false); + + return EXIT_SUCCESS; +} +} // namespace RegionalFloodFillTest +} // namespace free_surface +} // namespace walberla + +int main(int argc, char** argv) { return walberla::free_surface::RegionalFloodFillTest::main(argc, argv); } \ No newline at end of file diff --git a/tests/lbm/free_surface/bubble_model/SplitDetectionTest.cpp b/tests/lbm/free_surface/bubble_model/SplitDetectionTest.cpp new file mode 100644 index 000000000..adfbce45f --- /dev/null +++ b/tests/lbm/free_surface/bubble_model/SplitDetectionTest.cpp @@ -0,0 +1,152 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file SplitDetectionTest.cpp +//! \ingroup lbm/free_surface/bubble_model +//! \author Martin Bauer +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Test bubble split detection with 2D and 3D bubbles. +// +//====================================================================================================================== + +#include "core/Environment.h" +#include "core/debug/TestSubsystem.h" + +#include "lbm/free_surface/bubble_model/BubbleModel.h" + +#include "stencil/D2Q9.h" +#include "stencil/D3Q19.h" +#include "stencil/D3Q27.h" + +namespace walberla +{ +namespace free_surface +{ +namespace SplitDetectionTest +{ +using namespace bubble_model; + +// class derived from BubbleModel to access its protected members for testing purposes +template< typename Stencil_T > +class BubbleModelTest : public BubbleModel< Stencil_T > +{ + public: + static bool checkForSplit(BubbleField_T* bf, const Cell& cell, BubbleID prevID) + { + return BubbleModel< Stencil_T >::checkForSplit(bf, cell, prevID); + } +}; // class BubbleModelTest + +void initSlice(BubbleField_T* bf, cell_idx_t z, const BubbleID array3x3[]) +{ + for (cell_idx_t y = cell_idx_c(0); y < cell_idx_c(3); ++y) + { + for (cell_idx_t x = cell_idx_c(0); x < cell_idx_c(3); ++x) + { + bf->get(x, y, z) = array3x3[3 * (2 - y) + x]; + } + } +} + +void test2D_notConnected() +{ + // create a 3x3x3 bubble field (without ghost layer) and initialize with invalid IDs + BubbleField_T bubbleField(uint_c(3), uint_c(3), uint_c(3), uint_c(0), INVALID_BUBBLE_ID); + + // initialize a 2D slice of the bubble field (disconnected bubble) + const BubbleID N = INVALID_BUBBLE_ID; + const BubbleID init[] = { 2, 2, 2, N, N, N, 2, 2, 2 }; + + initSlice(&bubbleField, cell_idx_c(1), init); + + WALBERLA_CHECK(BubbleModelTest< stencil::D2Q9 >::checkForSplit( + &bubbleField, Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(1)), 2) == true); +} + +void test2D_connected() +{ + // create a 3x3x3 bubble field (without ghost layer) and initialize with invalid IDs + BubbleField_T bubbleField(uint_c(3), uint_c(3), uint_c(3), uint_c(0), INVALID_BUBBLE_ID); + + // initialize a 2D slice of the bubble field (connected bubble) + const BubbleID N = INVALID_BUBBLE_ID; + const BubbleID init[] = { 2, 2, 2, 2, N, N, 2, 2, 2 }; + + initSlice(&bubbleField, cell_idx_c(1), init); + + WALBERLA_CHECK(BubbleModelTest< stencil::D2Q9 >::checkForSplit( + &bubbleField, Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(1)), 2) == false); +} + +void test3D_connected() +{ + // create a 3x3x3 bubble field (without ghost layer) and initialize with invalid IDs + BubbleField_T bubbleField(uint_c(3), uint_c(3), uint_c(3), uint_c(0), INVALID_BUBBLE_ID); + + // initialize whole bubble field (connected bubble) + const BubbleID N = INVALID_BUBBLE_ID; + const BubbleID z0[] = { 2, 2, 2, N, N, N, 2, 2, 2 }; + const BubbleID z1[] = { N, 2, N, N, N, N, N, N, N }; + const BubbleID z2[] = { N, 2, N, N, 2, N, N, 2, N }; + + initSlice(&bubbleField, cell_idx_c(0), z0); + initSlice(&bubbleField, cell_idx_c(0), z1); + initSlice(&bubbleField, cell_idx_c(0), z2); + + WALBERLA_CHECK(BubbleModelTest< stencil::D3Q19 >::checkForSplit( + &bubbleField, Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(1)), 2) == false); + WALBERLA_CHECK(BubbleModelTest< stencil::D3Q27 >::checkForSplit( + &bubbleField, Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(1)), 2) == false); +} + +void test3D_notConnected() +{ + // create a 3x3x3 bubble field (without ghost layer) and initialize with invalid IDs + BubbleField_T bubbleField(uint_c(3), uint_c(3), uint_c(3), uint_c(0), INVALID_BUBBLE_ID); + + // initialize whole bubble field (disconnected bubble) + const BubbleID N = INVALID_BUBBLE_ID; + const BubbleID z0[] = { 2, 2, 2, N, N, N, 2, 2, 2 }; + const BubbleID z1[] = { N, N, N, N, N, N, N, N, N }; + const BubbleID z2[] = { N, 2, N, N, 2, N, N, 2, N }; + + initSlice(&bubbleField, 0, z0); + initSlice(&bubbleField, 1, z1); + initSlice(&bubbleField, 2, z2); + + WALBERLA_CHECK(BubbleModelTest< stencil::D3Q19 >::checkForSplit( + &bubbleField, Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(1)), 2) == true); + WALBERLA_CHECK(BubbleModelTest< stencil::D3Q27 >::checkForSplit( + &bubbleField, Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(1)), 2) == true); +} + +int main(int argc, char** argv) +{ + debug::enterTestMode(); + Environment env(argc, argv); + + test2D_notConnected(); + test2D_connected(); + + test3D_connected(); + test3D_notConnected(); + + return EXIT_SUCCESS; +} +} // namespace SplitDetectionTest +} // namespace free_surface +} // namespace walberla + +int main(int argc, char** argv) { return walberla::free_surface::SplitDetectionTest::main(argc, argv); } \ No newline at end of file diff --git a/tests/lbm/free_surface/dynamics/AdvectionTest.cpp b/tests/lbm/free_surface/dynamics/AdvectionTest.cpp new file mode 100644 index 000000000..1c3acc672 --- /dev/null +++ b/tests/lbm/free_surface/dynamics/AdvectionTest.cpp @@ -0,0 +1,220 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file AdvectionTest.cpp +//! \ingroup lbm/free_surface/dynamics +//! \author Martin Bauer +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Test mass advection on a flat surface. +//! +//! Initialize a pool of liquid in half of the domain and a box-shaped atmosphere (density 1.01) bubble in the remaining +//! cells. Only the StreamReconstructAdvectSweep is performed. Due to the higher density in the gas, the interface cells +//! that separate liquid and gas are emptied and their fill level must become negative. These cells must be marked +//! for conversion and the fluid density must balance the atmosphere density. +// +//====================================================================================================================== + +#include "blockforest/Initialization.h" +#include "blockforest/communication/UniformBufferedScheme.h" + +#include "core/Environment.h" +#include "core/debug/TestSubsystem.h" + +#include "field/AddToStorage.h" +#include "field/adaptors/AdaptorCreators.h" +#include "field/communication/PackInfo.h" + +#include "lbm/field/Adaptors.h" +#include "lbm/field/AddToStorage.h" +#include "lbm/free_surface//FlagInfo.h" +#include "lbm/free_surface/boundary/FreeSurfaceBoundaryHandling.h" +#include "lbm/free_surface/dynamics/PdfReconstructionModel.h" +#include "lbm/free_surface/dynamics/StreamReconstructAdvectSweep.h" +#include "lbm/lattice_model/CollisionModel.h" +#include "lbm/lattice_model/D3Q19.h" +#include "lbm/lattice_model/ForceModel.h" +#include "lbm/sweeps/CellwiseSweep.h" +#include "lbm/sweeps/SweepWrappers.h" + +#include "timeloop/SweepTimeloop.h" + +namespace walberla +{ +namespace free_surface +{ +namespace AdvectionTest +{ +// define types +using Flag_T = uint32_t; +using ScalarField_T = GhostLayerField< real_t, 1 >; +using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >; +using FlagField_T = FlagField< Flag_T >; + +template< typename LatticeModel_T > +void testAdvection() +{ + // define types + using Stencil_T = typename LatticeModel_T::Stencil; + using PdfField_T = lbm::PdfField< LatticeModel_T >; + using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >; + + // define the domain size + const Vector3< uint_t > numBlocks(uint_c(1)); + const Vector3< uint_t > domainSize(uint_c(3), uint_c(10), uint_c(2)); + const Vector3< uint_t > cellsPerBlock(domainSize[0] / numBlocks[0], domainSize[1] / numBlocks[1], + domainSize[2] / numBlocks[2]); + + // create block grid + const std::shared_ptr< StructuredBlockForest > blockForest = + blockforest::createUniformBlockGrid(numBlocks[0], numBlocks[1], numBlocks[2], // blocks + cellsPerBlock[0], cellsPerBlock[1], cellsPerBlock[2], // cells + real_c(1.0), // dx + true, // one block per process + true, false, true); // periodicity + + // create lattice model with omega=0.51 + LatticeModel_T latticeModel(real_c(0.51)); + + // add fields + BlockDataID pdfFieldID = lbm::addPdfFieldToStorage(blockForest, "PDF field", latticeModel, field::fzyx); + BlockDataID fillFieldID = + field::addToStorage< ScalarField_T >(blockForest, "Fill levels", real_c(1.0), field::fzyx, uint_c(1)); + BlockDataID curvatureFieldID = + field::addToStorage< ScalarField_T >(blockForest, "Curvature", real_c(0.0), field::fzyx, uint_c(1)); + BlockDataID normalFieldID = field::addToStorage< VectorField_T >( + blockForest, "Normals", Vector3< real_t >(real_c(0), real_c(1), real_c(0)), field::fzyx, uint_c(1)); + + BlockDataID densityAdaptor = field::addFieldAdaptor< typename lbm::Adaptor< LatticeModel_T >::Density >( + blockForest, pdfFieldID, "DensityAdaptor"); + + // add boundary handling + const std::shared_ptr< FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling = + std::make_shared< FreeSurfaceBoundaryHandling_T >(blockForest, pdfFieldID, fillFieldID); + const typename FreeSurfaceBoundaryHandling_T::FlagInfo_T& flagInfo = freeSurfaceBoundaryHandling->getFlagInfo(); + + // add box-shaped gas bubble (occupies about half of the domain in y-direction) + AABB box = blockForest->getDomain(); + auto newMin = box.min() + Vector3< real_t >(real_c(0), real_c(0.5) * box.ySize() + real_c(0.01), real_c(0)); + box.initMinMaxCorner(newMin, box.max()); + freeSurfaceBoundaryHandling->addFreeSurfaceObject(box); + + // set no slip boundary conditions at the southern and northern domain borders + freeSurfaceBoundaryHandling->setNoSlipAtBorder(stencil::S); + freeSurfaceBoundaryHandling->setNoSlipAtBorder(stencil::N); + freeSurfaceBoundaryHandling->initFlagsFromFillLevel(); + + // add bubble model + bubble_model::BubbleModel< Stencil_T > bubbleModel(blockForest, false); + bubbleModel.initFromFillLevelField(fillFieldID); + + // get some cell located inside the bubble (used as representative cell to set bubble to atmosphere bubble type) + Cell cellInBubble; + cellInBubble.x() = cell_idx_c(box.xMin() + real_c(0.5) * box.xSize()); + cellInBubble.y() = cell_idx_c(box.yMin() + real_c(0.5) * box.ySize()); + cellInBubble.z() = cell_idx_c(box.zMin() + real_c(0.5) * box.zSize()); + + // set bubble to atmosphere with constant density higher than the initial fluid density + const real_t atmDensity = real_c(1.01); + bubbleModel.setAtmosphere(cellInBubble, atmDensity); + + // create timeloop; 300 time steps are required (for very low omega of 0.51) to ensure that fluid density has + // stabilized + SweepTimeloop timeloop(blockForest, uint_c(300)); + + // add communication + blockforest::communication::UniformBufferedScheme< typename LatticeModel_T::Stencil > comm(blockForest); + comm.addPackInfo(std::make_shared< field::communication::PackInfo< PdfField_T > >(pdfFieldID)); + comm.addPackInfo(std::make_shared< field::communication::PackInfo< ScalarField_T > >(fillFieldID)); + comm.addPackInfo( + std::make_shared< field::communication::PackInfo< FlagField_T > >(freeSurfaceBoundaryHandling->getFlagFieldID())); + + // communicate + comm(); + + const PdfReconstructionModel pdfRecModel = PdfReconstructionModel("NormalBasedKeepCenter"); + + // add free surface boundary sweep for + // - reconstruction of PDFs in interface cells + // - advection of mass + // - marking interface cells for conversion + // - update bubble volumes + StreamReconstructAdvectSweep< LatticeModel_T, typename FreeSurfaceBoundaryHandling_T::BoundaryHandling_T, + FlagField_T, typename FreeSurfaceBoundaryHandling_T::FlagInfo_T, ScalarField_T, + VectorField_T, false > + streamReconstructAdvectSweep(real_c(0), freeSurfaceBoundaryHandling->getHandlingID(), fillFieldID, + freeSurfaceBoundaryHandling->getFlagFieldID(), pdfFieldID, normalFieldID, + curvatureFieldID, flagInfo, &bubbleModel, pdfRecModel, false, real_c(1e-3), + real_c(1e-1)); + timeloop.add() << Sweep(streamReconstructAdvectSweep); + + // add boundary handling sweep + timeloop.add() << BeforeFunction(comm) << Sweep(freeSurfaceBoundaryHandling->getBoundarySweep()); + + // add LBM collision sweep + auto lbmSweep = lbm::makeCellwiseSweep< LatticeModel_T, FlagField_T >( + pdfFieldID, freeSurfaceBoundaryHandling->getFlagFieldID(), flagIDs::liquidInterfaceFlagIDs); + timeloop.add() << Sweep(lbm::makeCollideSweep(lbmSweep)); + + timeloop.run(); + + // evaluate + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + const ScalarField_T* const fillField = blockIt->getData< const ScalarField_T >(fillFieldID); + const typename lbm::Adaptor< LatticeModel_T >::Density* const densityField = + blockIt->getData< const typename lbm::Adaptor< LatticeModel_T >::Density >(densityAdaptor); + const FlagField_T* const flagField = + blockIt->getData< const FlagField_T >(freeSurfaceBoundaryHandling->getFlagFieldID()); + + WALBERLA_FOR_ALL_CELLS(fillFieldIt, fillField, densityFieldIt, densityField, flagFieldIt, flagField, { + if (flagInfo.isInterface(flagFieldIt)) + { + // fill level in interface cells must be negative + WALBERLA_CHECK_LESS(*fillFieldIt, real_c(0)); + + // due to negative fill level, these cells must be marked for conversion to gas + WALBERLA_CHECK(isFlagSet(flagFieldIt, flagInfo.convertToGasFlag)); + } + + // in the absence of forces, fluid density must balance atmosphere density + if (flagInfo.isLiquid(flagFieldIt)) { WALBERLA_CHECK_FLOAT_EQUAL(*densityFieldIt, atmDensity); } + }) // WALBERLA_FOR_ALL_CELLS + } + + MPIManager::instance()->resetMPI(); +} + +int main(int argc, char** argv) +{ + debug::enterTestMode(); + Environment env(argc, argv); + + WALBERLA_LOG_INFO("Testing with D2Q9 stencil."); + testAdvection< lbm::D2Q9< lbm::collision_model::SRT, true, lbm::force_model::None, 2 > >(); + + WALBERLA_LOG_INFO("Testing with D3Q19 stencil."); + testAdvection< lbm::D3Q19< lbm::collision_model::SRT, true, lbm::force_model::None, 2 > >(); + + WALBERLA_LOG_INFO("Testing with D3Q27 stencil."); + testAdvection< lbm::D3Q27< lbm::collision_model::SRT, true, lbm::force_model::None, 2 > >(); + + return EXIT_SUCCESS; +} + +} // namespace AdvectionTest +} // namespace free_surface +} // namespace walberla + +int main(int argc, char** argv) { return walberla::free_surface::AdvectionTest::main(argc, argv); } \ No newline at end of file diff --git a/tests/lbm/free_surface/dynamics/CellConversionTest.cpp b/tests/lbm/free_surface/dynamics/CellConversionTest.cpp new file mode 100644 index 000000000..6f7645c84 --- /dev/null +++ b/tests/lbm/free_surface/dynamics/CellConversionTest.cpp @@ -0,0 +1,280 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file CellConversionTest.cpp +//! \ingroup lbm/free_surface/dynamics +//! \author Martin Bauer +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Test cell conversions on a flat surface by checking mass conservation and density balance. +//! +//! Initialize a pool of liquid in half of the domain and a box-shaped atmosphere (density 1.1) bubble in the remaining +//! cells. All sweeps from SurfaceDynamicsHandler are performed. Due to the higher density in the gas, the interface +//! cells that separate liquid and gas are emptied and their fill level must become negative. These cells are converted +//! to liquid, while former fluid cells must become interface cells. After this process, mass must be conserved and the +//! fluid density must balance the atmosphere density. +// +//====================================================================================================================== + +#include "blockforest/Initialization.h" +#include "blockforest/communication/UniformBufferedScheme.h" + +#include "core/Environment.h" +#include "core/debug/TestSubsystem.h" + +#include "field/AddToStorage.h" +#include "field/communication/PackInfo.h" + +#include "lbm/field/Adaptors.h" +#include "lbm/field/AddToStorage.h" +#include "lbm/free_surface/FlagInfo.h" +#include "lbm/free_surface/boundary/FreeSurfaceBoundaryHandling.h" +#include "lbm/free_surface/dynamics/SurfaceDynamicsHandler.h" +#include "lbm/lattice_model/CollisionModel.h" +#include "lbm/lattice_model/D3Q19.h" +#include "lbm/lattice_model/ForceModel.h" + +#include "timeloop/SweepTimeloop.h" + +#include <limits> + +namespace walberla +{ +namespace free_surface +{ +namespace CellConversionTest +{ +// define types +using flag_t = uint32_t; + +// define fields +using ScalarField_T = GhostLayerField< real_t, 1 >; +using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >; +using FlagField_T = FlagField< flag_t >; + +// compute the total mass of the fluid (in all interface and liquid cells) +template< typename LatticeModel_T, typename FlagField_T > +real_t computeTotalMass( + const std::weak_ptr< StructuredBlockForest >& blockForest, ConstBlockDataID pdfField, ConstBlockDataID fillField, + ConstBlockDataID flagField, + const typename FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::FlagInfo_T& flags); + +// compute the minimum and maximum density in all interface and liquid cells +template< typename LatticeModel_T, typename FlagField_T > +void computeMinMaxDensity( + const std::weak_ptr< StructuredBlockForest >& blockForest, ConstBlockDataID pdfField, ConstBlockDataID flagField, + const typename FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::FlagInfo_T& flags, + real_t& minDensity, real_t& maxDensity); + +template< typename LatticeModel_T > +void testCellConversion() +{ + // define types + using Stencil_T = typename LatticeModel_T::Stencil; + using PdfField_T = lbm::PdfField< LatticeModel_T >; + + using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >; + + // define the domain size + const Vector3< uint_t > numBlocks(uint_c(1)); + const Vector3< uint_t > domainSize(uint_c(3), uint_c(10), uint_c(2)); + const Vector3< uint_t > cellsPerBlock(domainSize[0] / numBlocks[0], domainSize[1] / numBlocks[1], + domainSize[2] / numBlocks[2]); + + // create block grid + const std::shared_ptr< StructuredBlockForest > blockForest = + blockforest::createUniformBlockGrid(numBlocks[0], numBlocks[1], numBlocks[2], // blocks + cellsPerBlock[0], cellsPerBlock[1], cellsPerBlock[2], // cells + real_c(1.0), // dx + true, // one block per process + true, false, true); // periodicity + + real_t relaxRate = real_c(0.51); + + // create lattice model with omega=0.51 + LatticeModel_T latticeModel(relaxRate); + + // add fields + BlockDataID pdfFieldID = lbm::addPdfFieldToStorage(blockForest, "PDF field", latticeModel, field::fzyx); + BlockDataID fillFieldID = + field::addToStorage< ScalarField_T >(blockForest, "Fill levels", real_c(1.0), field::fzyx, uint_c(1)); + BlockDataID forceFieldID = field::addToStorage< VectorField_T >( + blockForest, "Force field", Vector3< real_t >(real_c(0)), field::fzyx, uint_c(1)); + BlockDataID curvatureFieldID = + field::addToStorage< ScalarField_T >(blockForest, "Curvature", real_c(0.0), field::fzyx, uint_c(1)); + BlockDataID normalFieldID = field::addToStorage< VectorField_T >( + blockForest, "Normals", Vector3< real_t >(real_c(0), real_c(1), real_c(0)), field::fzyx, uint_c(1)); + + // add boundary handling + const std::shared_ptr< FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling = + std::make_shared< FreeSurfaceBoundaryHandling_T >(blockForest, pdfFieldID, fillFieldID); + BlockDataID flagFieldID = freeSurfaceBoundaryHandling->getFlagFieldID(); + const typename FreeSurfaceBoundaryHandling_T::FlagInfo_T& flagInfo = freeSurfaceBoundaryHandling->getFlagInfo(); + + // add box-shaped gas bubble (occupies about half of the domain in y-direction) + AABB box = blockForest->getDomain(); + auto newMin = box.min() + Vector3< real_t >(real_c(0), real_c(0.5) * box.ySize() + real_c(1 - 0.02), real_c(0)); + box.initMinMaxCorner(newMin, box.max()); + freeSurfaceBoundaryHandling->addFreeSurfaceObject(box); + + // set no slip boundary conditions at the southern and northern domain borders + freeSurfaceBoundaryHandling->setNoSlipAtBorder(stencil::S); + freeSurfaceBoundaryHandling->setNoSlipAtBorder(stencil::N); + freeSurfaceBoundaryHandling->initFlagsFromFillLevel(); + + // add bubble model + auto bubbleModel = std::make_shared< bubble_model::BubbleModel< Stencil_T > >(blockForest, false); + bubbleModel->initFromFillLevelField(fillFieldID); + + // get some cell located inside the bubble (used as representative cell to set bubble to atmosphere bubble type) + Cell cellInBubble; + cellInBubble.x() = cell_idx_c(box.xMin() + real_c(0.5) * box.xSize()); + cellInBubble.y() = cell_idx_c(box.yMin() + real_c(0.5) * box.ySize()); + cellInBubble.z() = cell_idx_c(box.zMin() + real_c(0.5) * box.zSize()); + + // set bubble to atmosphere with constant density higher than the initial fluid density + const real_t atmDensity = real_c(1.1); + bubbleModel->setAtmosphere(cellInBubble, atmDensity); + + // create timeloop; 400 time steps are required (for very low omega of 0.51) to ensure that fluid density has + // stabilized + SweepTimeloop timeloop(blockForest, uint_c(400)); + + // add communication + blockforest::communication::UniformBufferedScheme< Stencil_T > comm(blockForest); + comm.addPackInfo(std::make_shared< field::communication::PackInfo< PdfField_T > >(pdfFieldID)); + comm.addPackInfo(std::make_shared< field::communication::PackInfo< ScalarField_T > >(fillFieldID)); + comm.addPackInfo(std::make_shared< field::communication::PackInfo< FlagField_T > >(flagFieldID)); + + // communicate + comm(); + + // add various sweeps for surface dynamics + SurfaceDynamicsHandler< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > dynamicsHandler( + blockForest, pdfFieldID, flagFieldID, fillFieldID, forceFieldID, normalFieldID, curvatureFieldID, + freeSurfaceBoundaryHandling, bubbleModel, "NormalBasedKeepCenter", "EquilibriumRefilling", "EvenlyNewInterface", + relaxRate, Vector3< real_t >(real_c(0)), real_c(0), false, false, real_c(1e-3), real_c(1e-1)); + dynamicsHandler.addSweeps(timeloop); + + real_t initialMass = + computeTotalMass< LatticeModel_T, FlagField_T >(blockForest, pdfFieldID, fillFieldID, flagFieldID, flagInfo); + + timeloop.run(); + + // in the absence of forces, fluid density must balance atmosphere density + real_t rhoMin = real_c(0); + real_t rhoMax = real_c(0); + computeMinMaxDensity< LatticeModel_T, FlagField_T >(blockForest, pdfFieldID, flagFieldID, flagInfo, rhoMin, rhoMax); + WALBERLA_CHECK_FLOAT_EQUAL(atmDensity, rhoMin); + WALBERLA_CHECK_FLOAT_EQUAL(atmDensity, rhoMax); + + // mass must be conserved + real_t finalMass = + computeTotalMass< LatticeModel_T, FlagField_T >(blockForest, pdfFieldID, fillFieldID, flagFieldID, flagInfo); + WALBERLA_CHECK_FLOAT_EQUAL(initialMass, finalMass); + + MPIManager::instance()->resetMPI(); +} + +// compute the total mass of the fluid (in all interface and liquid cells) +template< typename LatticeModel_T, typename FlagField_T > +real_t computeTotalMass( + const std::weak_ptr< StructuredBlockForest >& blockForestPtr, ConstBlockDataID pdfFieldID, + ConstBlockDataID fillFieldID, ConstBlockDataID flagFieldID, + const typename FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::FlagInfo_T& flagInfo) +{ + const auto blockForest = blockForestPtr.lock(); + WALBERLA_CHECK_NOT_NULLPTR(blockForest); + + real_t mass = real_c(0); + + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + const lbm::PdfField< LatticeModel_T >* const pdfField = + blockIt->getData< const lbm::PdfField< LatticeModel_T > >(pdfFieldID); + const ScalarField_T* const fillField = blockIt->getData< const ScalarField_T >(fillFieldID); + const FlagField_T* const flagField = blockIt->getData< const FlagField_T >(flagFieldID); + + // iterate over all interface and liquid cells and compute the total mass of fluid in the domain + WALBERLA_FOR_ALL_CELLS_OMP(fillFieldIt, fillField, pdfFieldIt, pdfField, flagFieldIt, flagField, + omp parallel for schedule(static) reduction(+:mass), + { + const real_t rho = lbm::getDensity< LatticeModel_T >(pdfField->latticeModel(), pdfFieldIt); + if (flagInfo.isInterface(flagFieldIt)) { mass += rho * (*fillFieldIt); } + else + { + if (flagInfo.isLiquid(flagFieldIt)) { mass += rho; } + } + }); // WALBERLA_FOR_ALL_CELLS_OMP + } + + WALBERLA_LOG_RESULT("Total current mass " << mass << "."); + + return mass; +} + +// compute the minimum and maximum density in all interface and liquid cells +template< typename LatticeModel_T, typename FlagField_T > +void computeMinMaxDensity( + const std::weak_ptr< StructuredBlockForest >& blockForestPtr, ConstBlockDataID pdfFieldID, + ConstBlockDataID flagFieldID, + const typename FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >::FlagInfo_T& flags, + real_t& minDensity, real_t& maxDensity) +{ + const auto blockForest = blockForestPtr.lock(); + WALBERLA_CHECK_NOT_NULLPTR(blockForest); + + minDensity = std::numeric_limits< real_t >::max(); + maxDensity = std::numeric_limits< real_t >::min(); + + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + const lbm::PdfField< LatticeModel_T >* const pdfField = + blockIt->getData< const lbm::PdfField< LatticeModel_T > >(pdfFieldID); + const FlagField_T* const flagField = blockIt->getData< const FlagField_T >(flagFieldID); + + // iterate over all interface and liquid cells and find the minimum and maximum density; explicitly avoid OpenMP + // (problematic to reduce max and min) + WALBERLA_FOR_ALL_CELLS_OMP(flagFieldIt, flagField, pdfFieldIt, pdfField, omp critical, { + if (flags.isInterface(flagFieldIt) || flags.isLiquid(flagFieldIt)) + { + const real_t rho = lbm::getDensity< LatticeModel_T >(pdfField->latticeModel(), pdfFieldIt); + minDensity = std::min(rho, minDensity); + maxDensity = std::max(rho, maxDensity); + } + }) // WALBERLA_FOR_ALL_CELLS + } +} + +int main(int argc, char** argv) +{ + debug::enterTestMode(); + Environment env(argc, argv); + + WALBERLA_LOG_INFO("Testing with D2Q9 stencil.") + testCellConversion< walberla::lbm::D2Q9< walberla::lbm::collision_model::SRT, true > >(); + + WALBERLA_LOG_INFO("Testing with D3Q19 stencil.") + testCellConversion< walberla::lbm::D3Q19< walberla::lbm::collision_model::SRT, true > >(); + + WALBERLA_LOG_INFO("Testing with D3Q27 stencil.") + testCellConversion< walberla::lbm::D3Q27< walberla::lbm::collision_model::SRT, true > >(); + + return EXIT_SUCCESS; +} +} // namespace CellConversionTest +} // namespace free_surface +} // namespace walberla + +int main(int argc, char** argv) { return walberla::free_surface::CellConversionTest::main(argc, argv); } \ No newline at end of file diff --git a/tests/lbm/free_surface/dynamics/CodegenTest.cpp b/tests/lbm/free_surface/dynamics/CodegenTest.cpp new file mode 100644 index 000000000..a141a0a3c --- /dev/null +++ b/tests/lbm/free_surface/dynamics/CodegenTest.cpp @@ -0,0 +1,227 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file CodegenTest.cpp +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Test equivalence of generated LBM kernels in the free surface implementation. +//! +//! Simulates 100 time steps of a moving drop with diameter 2 in a periodic 4x4x4 domain. The drop moves due to a +//! constant body force. +//====================================================================================================================== + +#include "blockforest/Initialization.h" + +#include "core/Environment.h" +#include "core/debug/TestSubsystem.h" + +#include "lbm/field/AddToStorage.h" +#include "lbm/free_surface/bubble_model/Geometry.h" +#include "lbm/free_surface/dynamics/SurfaceDynamicsHandler.h" +#include "lbm/free_surface/surface_geometry/SurfaceGeometryHandler.h" +#include "lbm/lattice_model/D3Q19.h" + +#include <type_traits> + +// include files generated by lbmpy +#include "GeneratedLatticeModel_FreeSurface.h" + +namespace walberla +{ +namespace free_surface +{ +namespace CodegenTest +{ + +using ScalarField_T = GhostLayerField< real_t, 1 >; +using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >; + +template< bool useCodegen > +void runSimulation() +{ + using CollisionModel_T = lbm::collision_model::SRT; + using ForceModel_T = lbm::force_model::GuoField< VectorField_T >; + using LatticeModel_T = typename std::conditional< useCodegen, lbm::GeneratedLatticeModel_FreeSurface, + lbm::D3Q19< CollisionModel_T, true, ForceModel_T, 2 > >::type; + + using Communication_T = blockforest::SimpleCommunication< typename LatticeModel_T::CommunicationStencil >; + + using flag_t = uint32_t; + using FlagField_T = FlagField< flag_t >; + using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >; + + // define the domain size + const Vector3< uint_t > numBlocks(uint_c(1)); + const Vector3< uint_t > domainSize(uint_c(4), uint_c(4), uint_c(4)); + const Vector3< uint_t > cellsPerBlock(domainSize[0] / numBlocks[0], domainSize[1] / numBlocks[1], + domainSize[2] / numBlocks[2]); + + // create block grid + const std::shared_ptr< StructuredBlockForest > blockForest = + blockforest::createUniformBlockGrid(numBlocks[0], numBlocks[1], numBlocks[2], // blocks + cellsPerBlock[0], cellsPerBlock[1], cellsPerBlock[2], // cells + real_c(1.0), // dx + true, // one block per process + true, true, true); // periodicity + + // physics parameters + const real_t dropDiameter = real_c(2); + const real_t relaxationRate = real_c(1.8); + const real_t surfaceTension = real_c(1e-5); + const bool enableWetting = false; + const real_t contactAngle = real_c(0); + const Vector3< real_t > force = Vector3< real_t >(real_c(1e-5), real_c(0), real_c(0)); + + // model parameters + const std::string pdfReconstructionModel = "NormalBasedKeepCenter"; + const std::string pdfRefillingModel = "EquilibriumRefilling"; + const std::string excessMassDistributionModel = "EvenlyAllInterface"; + const std::string curvatureModel = "FiniteDifferenceMethod"; + const bool enableForceWeighting = false; + const bool useSimpleMassExchange = false; + const real_t cellConversionThreshold = real_c(1e-2); + const real_t cellConversionForceThreshold = real_c(1e-1); + + // add force field + const BlockDataID forceFieldID = + field::addToStorage< VectorField_T >(blockForest, "Force field", force, field::fzyx, uint_c(1)); + + std::shared_ptr< LatticeModel_T > latticeModel; + + // create lattice model + if constexpr (useCodegen) + { + latticeModel = std::make_shared< LatticeModel_T >(force[0], force[1], force[2], relaxationRate); + } + else + { + latticeModel = std::make_shared< LatticeModel_T >(CollisionModel_T(relaxationRate), ForceModel_T(forceFieldID)); + } + + // add various fields + const BlockDataID pdfFieldID = lbm::addPdfFieldToStorage(blockForest, "PDF field", *latticeModel, field::fzyx); + const BlockDataID fillFieldID = + field::addToStorage< ScalarField_T >(blockForest, "Fill level field", real_c(0.0), field::fzyx, uint_c(1)); + + // add boundary handling + const std::shared_ptr< FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling = + std::make_shared< FreeSurfaceBoundaryHandling_T >(blockForest, pdfFieldID, fillFieldID); + const BlockDataID flagFieldID = freeSurfaceBoundaryHandling->getFlagFieldID(); + + // add drop to fill level field + const geometry::Sphere sphereDrop(Vector3< real_t >(real_c(0.5) * real_c(domainSize[0]), + real_c(0.5) * real_c(domainSize[1]), + real_c(0.5) * real_c(domainSize[2])), + real_c(dropDiameter) * real_c(0.5)); + bubble_model::addBodyToFillLevelField< geometry::Sphere >(*blockForest, fillFieldID, sphereDrop, false); + + // initialize flag field from fill level field + freeSurfaceBoundaryHandling->initFlagsFromFillLevel(); + + // initial Communication_Tunication + Communication_T(blockForest, pdfFieldID, fillFieldID, flagFieldID, forceFieldID)(); + + // add bubble model + const std::shared_ptr< bubble_model::BubbleModelConstantPressure > bubbleModel = + std::make_shared< bubble_model::BubbleModelConstantPressure >(real_c(1)); + + // create timeloop + SweepTimeloop timeloop(blockForest, uint_c(100)); + + // Laplace pressure = 2 * surface tension * curvature; curvature computation is not necessary with 0 surface tension + bool computeCurvature = false; + if (!realIsEqual(surfaceTension, real_c(0), real_c(1e-14))) { computeCurvature = true; } + + // add surface geometry handler + const SurfaceGeometryHandler< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > geometryHandler( + blockForest, freeSurfaceBoundaryHandling, fillFieldID, curvatureModel, computeCurvature, enableWetting, + contactAngle); + + const ConstBlockDataID curvatureFieldID = geometryHandler.getConstCurvatureFieldID(); + const ConstBlockDataID normalFieldID = geometryHandler.getConstNormalFieldID(); + + geometryHandler.addSweeps(timeloop); + + // add boundary handling for standard boundaries and free surface boundaries + SurfaceDynamicsHandler< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T, useCodegen > dynamicsHandler( + blockForest, pdfFieldID, flagFieldID, fillFieldID, forceFieldID, normalFieldID, curvatureFieldID, + freeSurfaceBoundaryHandling, bubbleModel, pdfReconstructionModel, pdfRefillingModel, excessMassDistributionModel, + relaxationRate, force, surfaceTension, enableForceWeighting, useSimpleMassExchange, cellConversionThreshold, + cellConversionForceThreshold); + + dynamicsHandler.addSweeps(timeloop); + + timeloop.run(); + + // check fill level (must be identical in lattice model from waLBerla and from lbmpy) + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + const ScalarField_T* const fillField = blockIt->template getData< const ScalarField_T >(fillFieldID); + + WALBERLA_FOR_ALL_CELLS(fillFieldIt, fillField, { + if (fillFieldIt.cell() == Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(1))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.504035), real_c(1e-4)); + } + if (fillFieldIt.cell() == Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(1))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.538017), real_c(1e-4)); + } + if (fillFieldIt.cell() == Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(1))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.504035), real_c(1e-4)); + } + if (fillFieldIt.cell() == Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(1))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.538017), real_c(1e-4)); + } + if (fillFieldIt.cell() == Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(2))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.504035), real_c(1e-4)); + } + if (fillFieldIt.cell() == Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(2))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.538017), real_c(1e-4)); + } + if (fillFieldIt.cell() == Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(2))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.504035), real_c(1e-4)); + } + if (fillFieldIt.cell() == Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(2))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.538017), real_c(1e-4)); + } + }); // WALBERLA_FOR_ALL_CELLS + } +} + +int main(int argc, char** argv) +{ + debug::enterTestMode(); + Environment walberlaEnv(argc, argv); + + WALBERLA_LOG_INFO_ON_ROOT("Testing with lattice model from waLBerla."); + runSimulation< false >(); + + WALBERLA_LOG_INFO_ON_ROOT("Testing with lattice model generated by lbmpy."); + runSimulation< true >(); + + return EXIT_SUCCESS; +} + +} // namespace CodegenTest +} // namespace free_surface +} // namespace walberla + +int main(int argc, char** argv) { return walberla::free_surface::CodegenTest::main(argc, argv); } diff --git a/tests/lbm/free_surface/dynamics/ExcessMassDistributionFallbackTest.cpp b/tests/lbm/free_surface/dynamics/ExcessMassDistributionFallbackTest.cpp new file mode 100644 index 000000000..69c075f9e --- /dev/null +++ b/tests/lbm/free_surface/dynamics/ExcessMassDistributionFallbackTest.cpp @@ -0,0 +1,360 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file ExcessMassDistributionFallbackTest.cpp +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Test excess mass distribution in cases where the chosen model is not applicable. +//! +//! Test if fall back to other models works correctly with a simple two-dimensional 3x3 grid, where the center cell at +//! (1,1) is assumed to have converted from interface to liquid with excess mass 0.1. +//====================================================================================================================== + +#include "blockforest/Initialization.h" + +#include "core/Environment.h" +#include "core/debug/TestSubsystem.h" + +#include "field/FieldClone.h" + +#include "lbm/blockforest/communication/SimpleCommunication.h" +#include "lbm/field/Adaptors.h" +#include "lbm/field/AddToStorage.h" +#include "lbm/free_surface/boundary/FreeSurfaceBoundaryHandling.h" +#include "lbm/free_surface/bubble_model/Geometry.h" +#include "lbm/free_surface/dynamics/ExcessMassDistributionModel.h" +#include "lbm/free_surface/dynamics/ExcessMassDistributionSweep.h" +#include "lbm/lattice_model/D2Q9.h" + +#include "timeloop/SweepTimeloop.h" + +namespace walberla +{ +namespace free_surface +{ +namespace ExcessMassDistributionFallbackTest +{ +using LatticeModel_T = lbm::D2Q9< lbm::collision_model::SRT, true, lbm::force_model::None, 2 >; +using Stencil = typename LatticeModel_T::Stencil; + +using Communication_T = blockforest::SimpleCommunication< LatticeModel_T::CommunicationStencil >; + +using PdfField_T = lbm::PdfField< LatticeModel_T >; +using ScalarField_T = GhostLayerField< real_t, 1 >; +using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >; + +using flag_t = uint32_t; +using FlagField_T = FlagField< flag_t >; +using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >; + +void runSimulation(const ExcessMassDistributionModel& excessMassDistributionModel, + const std::vector< Cell >& newInterfaceCells, const std::vector< Cell >& oldInterfaceCells, + const Vector3< real_t >& interfaceNormal) +{ + // define the domain size + const Vector3< uint_t > numBlocks(uint_c(1)); + const Vector3< uint_t > domainSize(uint_c(3), uint_c(3), uint_c(1)); + const Vector3< uint_t > cellsPerBlock(domainSize[0] / numBlocks[0], domainSize[1] / numBlocks[1], + domainSize[2] / numBlocks[2]); + + // create block grid + const std::shared_ptr< StructuredBlockForest > blockForest = + blockforest::createUniformBlockGrid(numBlocks[0], numBlocks[1], numBlocks[2], // blocks + cellsPerBlock[0], cellsPerBlock[1], cellsPerBlock[2], // cells + real_c(1.0), // dx + true, // one block per process + false, false, false); // periodicity + + // create (dummy) lattice model + LatticeModel_T latticeModel = LatticeModel_T(lbm::collision_model::SRT(real_c(1.8))); + + // add pdf field + const BlockDataID pdfFieldID = lbm::addPdfFieldToStorage(blockForest, "PDF field", latticeModel, + Vector3< real_t >(real_c(0)), real_c(1), field::fzyx); + + // add normal field + const BlockDataID normalFieldID = field::addToStorage< VectorField_T >( + blockForest, "Normal field", Vector3< real_t >(real_c(0)), field::fzyx, uint_c(1)); + + // add fill level field + const BlockDataID fillFieldID = + field::addToStorage< ScalarField_T >(blockForest, "Fill level field", real_c(1), field::fzyx, uint_c(1)); + + // initialize fill levels and interface normal + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + ScalarField_T* const fillField = blockIt->getData< ScalarField_T >(fillFieldID); + VectorField_T* const normalField = blockIt->getData< VectorField_T >(normalFieldID); + + WALBERLA_FOR_ALL_CELLS(fillFieldIt, fillField, normalFieldIt, normalField, { + // initialize interface cells + for (const Cell& cell : newInterfaceCells) + { + if (fillFieldIt.cell() == cell) { *fillFieldIt = real_c(0.5); } + } + + for (const Cell& cell : oldInterfaceCells) + { + if (fillFieldIt.cell() == cell) { *fillFieldIt = real_c(0.5); } + } + + // this cell is assigned a fill level of 1.1 leading to an excess mass equivalent to a fill level of 0.1 + if (fillFieldIt.cell() == Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0))) { *fillFieldIt = real_c(1.1); } + + if (normalFieldIt.cell() == Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0))) + { + *normalFieldIt = interfaceNormal; + } + }) // WALBERLA_FOR_ALL_CELLS + } + + // add boundary handling + const std::shared_ptr< FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling = + std::make_shared< FreeSurfaceBoundaryHandling_T >(blockForest, pdfFieldID, fillFieldID); + const BlockDataID flagFieldID = freeSurfaceBoundaryHandling->getFlagFieldID(); + const typename FreeSurfaceBoundaryHandling_T::FlagInfo_T& flagInfo = freeSurfaceBoundaryHandling->getFlagInfo(); + freeSurfaceBoundaryHandling->initFlagsFromFillLevel(); + + // initialize flags + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + FlagField_T* const flagField = blockIt->getData< FlagField_T >(flagFieldID); + + WALBERLA_FOR_ALL_CELLS(flagFieldIt, flagField, { + // flag the cell with excess mass as newly converted to liquid + if (flagFieldIt.cell() == Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0))) + { + field::addFlag(flagFieldIt, flagInfo.convertedFlag | flagInfo.convertToLiquidFlag); + } + + // consider these cells to be newly-converted to interface + for (const Cell& cell : newInterfaceCells) + { + if (flagFieldIt.cell() == cell) { field::addFlag(flagFieldIt, flagInfo.convertedFlag); } + } + }) // WALBERLA_FOR_ALL_CELLS + } + + // initial communication + Communication_T(blockForest, pdfFieldID, fillFieldID, flagFieldID, normalFieldID)(); + + // create timeloop + SweepTimeloop timeloop(blockForest, uint_c(1)); + + if (excessMassDistributionModel.isEvenlyType()) + { + const ExcessMassDistributionSweepInterfaceEvenly< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > + distributeMassSweep(excessMassDistributionModel, fillFieldID, flagFieldID, pdfFieldID, flagInfo); + timeloop.add() << Sweep(distributeMassSweep, "Distribute excess mass"); + } + else + { + if (excessMassDistributionModel.isWeightedType()) + { + const ExcessMassDistributionSweepInterfaceWeighted< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > + distributeMassSweep(excessMassDistributionModel, fillFieldID, flagFieldID, pdfFieldID, flagInfo, + normalFieldID); + timeloop.add() << Sweep(distributeMassSweep, "Distribute excess mass"); + } + } + + timeloop.singleStep(); + + // check if excess mass was distributed correctly; expected solutions were obtained manually + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + const ScalarField_T* const fillField = blockIt->getData< const ScalarField_T >(fillFieldID); + + WALBERLA_FOR_ALL_CELLS(fillFieldIt, fillField, { + if (excessMassDistributionModel.getModelType() == + ExcessMassDistributionModel::ExcessMassModel::EvenlyOldInterface) + { + if (fillFieldIt.cell() == Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + } + + if (fillFieldIt.cell() == Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.6), real_c(1e-4)); + } + } + + if (excessMassDistributionModel.getModelType() == + ExcessMassDistributionModel::ExcessMassModel::EvenlyNewInterface) + { + if (fillFieldIt.cell() == Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + } + + if (fillFieldIt.cell() == Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.6), real_c(1e-4)); + } + } + + if (excessMassDistributionModel.getModelType() == + ExcessMassDistributionModel::ExcessMassModel::WeightedOldInterface && + !oldInterfaceCells.empty()) + { + if (fillFieldIt.cell() == Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + } + + if (fillFieldIt.cell() == Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4)); + } + + if (fillFieldIt.cell() == Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.6), real_c(1e-4)); + } + } + + if (excessMassDistributionModel.getModelType() == + ExcessMassDistributionModel::ExcessMassModel::WeightedNewInterface && + !newInterfaceCells.empty()) + { + if (fillFieldIt.cell() == Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + } + + if (fillFieldIt.cell() == Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4)); + } + + if (fillFieldIt.cell() == Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.6), real_c(1e-4)); + } + } + + if (excessMassDistributionModel.getModelType() == + ExcessMassDistributionModel::ExcessMassModel::WeightedOldInterface && + oldInterfaceCells.empty()) + { + if (fillFieldIt.cell() == Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + } + + if (fillFieldIt.cell() == Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.55), real_c(1e-4)); + } + + if (fillFieldIt.cell() == Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.55), real_c(1e-4)); + } + } + + if (excessMassDistributionModel.getModelType() == + ExcessMassDistributionModel::ExcessMassModel::WeightedNewInterface && + newInterfaceCells.empty()) + { + if (fillFieldIt.cell() == Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + } + + if (fillFieldIt.cell() == Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.55), real_c(1e-4)); + } + + if (fillFieldIt.cell() == Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.55), real_c(1e-4)); + } + } + }) // WALBERLA_FOR_ALL_CELLS + } + + MPIManager::instance()->resetMPI(); +} + +int main(int argc, char** argv) +{ + debug::enterTestMode(); + Environment env(argc, argv); + + Vector3< real_t > interfaceNormal(real_c(0)); + + // (0,0) is the only interface cell (newly-converted) => EvenlyOldInterface must fall back to EvenlyNewInterface + ExcessMassDistributionModel model = ExcessMassDistributionModel("EvenlyOldInterface"); + std::vector< Cell > newInterfaceCells{ Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0)) }; + std::vector< Cell > oldInterfaceCells{}; + WALBERLA_LOG_INFO_ON_ROOT("Testing model " << model.getFullModelSpecification()); + runSimulation(model, newInterfaceCells, oldInterfaceCells, interfaceNormal); + + // (0,0) is the only interface cell (not newly-converted) => EvenlyNewInterface must fall back to EvenlyOldInterface + model = ExcessMassDistributionModel("EvenlyNewInterface"); + newInterfaceCells = std::vector< Cell >{}; + oldInterfaceCells = std::vector< Cell >{ Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0)) }; + WALBERLA_LOG_INFO_ON_ROOT("Testing model " << model.getFullModelSpecification()); + runSimulation(model, newInterfaceCells, oldInterfaceCells, interfaceNormal); + + interfaceNormal = Vector3< real_t >(real_c(0.71), real_c(0.71), real_c(0)); + + // (0,0) is old interface cell; (2,2) is newly-converted interface cell; interface normal points in direction (1,1) + // => WeightedOldInterface must fall back to WeightedNewInterface + model = ExcessMassDistributionModel("WeightedOldInterface"); + newInterfaceCells = std::vector< Cell >{ Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(0)) }; + oldInterfaceCells = std::vector< Cell >{ Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0)) }; + WALBERLA_LOG_INFO_ON_ROOT("Testing model " << model.getFullModelSpecification()); + runSimulation(model, newInterfaceCells, oldInterfaceCells, interfaceNormal); + + // (0,0) is newly-converted interface cell; (2,2) is old interface cell; interface normal points in direction (1,1) + // => WeightedNewInterface must fall back to WeightedOldInterface + model = ExcessMassDistributionModel("WeightedNewInterface"); + newInterfaceCells = std::vector< Cell >{ Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0)) }; + oldInterfaceCells = std::vector< Cell >{ Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(0)) }; + WALBERLA_LOG_INFO_ON_ROOT("Testing model " << model.getFullModelSpecification()); + runSimulation(model, newInterfaceCells, oldInterfaceCells, interfaceNormal); + + interfaceNormal = Vector3< real_t >(real_c(0), real_c(-1), real_c(0)); + + // (1,2) and (2,2) are newly-converted interface cells; interface normal points in direction (0,-1) + // => WeightedOldInterface must fall back to EvenlyAllInterface, as no interface cell is available in normal + // direction + model = ExcessMassDistributionModel("WeightedOldInterface"); + newInterfaceCells = std::vector< Cell >{ Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(0)), + Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(0)) }; + oldInterfaceCells = std::vector< Cell >{}; + WALBERLA_LOG_INFO_ON_ROOT("Testing model " << model.getFullModelSpecification()); + runSimulation(model, newInterfaceCells, oldInterfaceCells, interfaceNormal); + + // (1,2) and (2,2) are old interface cells; interface normal points in direction (0,-1) + // => WeightedNewInterface must fall back to EvenlyAllInterface, as no interface cell is available in normal + // direction + model = ExcessMassDistributionModel("WeightedNewInterface"); + newInterfaceCells = std::vector< Cell >{}; + oldInterfaceCells = std::vector< Cell >{ Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(0)), + Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(0)) }; + WALBERLA_LOG_INFO_ON_ROOT("Testing model " << model.getFullModelSpecification()); + runSimulation(model, newInterfaceCells, oldInterfaceCells, interfaceNormal); + + return EXIT_SUCCESS; +} +} // namespace ExcessMassDistributionFallbackTest +} // namespace free_surface +} // namespace walberla + +int main(int argc, char** argv) { return walberla::free_surface::ExcessMassDistributionFallbackTest::main(argc, argv); } \ No newline at end of file diff --git a/tests/lbm/free_surface/dynamics/ExcessMassDistributionParallelTest.cpp b/tests/lbm/free_surface/dynamics/ExcessMassDistributionParallelTest.cpp new file mode 100644 index 000000000..8c99cf872 --- /dev/null +++ b/tests/lbm/free_surface/dynamics/ExcessMassDistributionParallelTest.cpp @@ -0,0 +1,1154 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file ExcessMassDistributionParallelTest.cpp +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Test distribution of excess mass using two blocks (to verify also on a parallel environment). +//! +//! The tests and their expected solutions are also shown in the file +//! "/tests/lbm/free_surface/dynamics/ExcessMassDistributionParallelTest.odp". +//====================================================================================================================== + +#include "blockforest/Initialization.h" + +#include "core/Environment.h" +#include "core/debug/TestSubsystem.h" + +#include "field/FieldClone.h" + +#include "lbm/blockforest/communication/SimpleCommunication.h" +#include "lbm/field/Adaptors.h" +#include "lbm/field/AddToStorage.h" +#include "lbm/free_surface/boundary/FreeSurfaceBoundaryHandling.h" +#include "lbm/free_surface/bubble_model/Geometry.h" +#include "lbm/free_surface/dynamics/ExcessMassDistributionModel.h" +#include "lbm/free_surface/dynamics/ExcessMassDistributionSweep.h" +#include "lbm/lattice_model/D2Q9.h" + +#include "timeloop/SweepTimeloop.h" + +namespace walberla +{ +namespace free_surface +{ +namespace ExcessMassDistributionTest +{ +using LatticeModel_T = lbm::D2Q9< lbm::collision_model::SRT, true, lbm::force_model::None, 2 >; +using Stencil = typename LatticeModel_T::Stencil; + +using Communication_T = blockforest::SimpleCommunication< LatticeModel_T::CommunicationStencil >; + +using PdfField_T = lbm::PdfField< LatticeModel_T >; +using ScalarField_T = GhostLayerField< real_t, 1 >; +using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >; + +using flag_t = uint32_t; +using FlagField_T = FlagField< flag_t >; +using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >; + +void runSimulation(const ExcessMassDistributionModel& excessMassDistributionModel) +{ + // define the domain size + const Vector3< uint_t > numBlocks(uint_c(2), uint_c(1), uint_c(1)); + const Vector3< uint_t > domainSize(uint_c(6), uint_c(3), uint_c(1)); + const Vector3< uint_t > cellsPerBlock(domainSize[0] / numBlocks[0], domainSize[1] / numBlocks[1], + domainSize[2] / numBlocks[2]); + + // create block grid + const std::shared_ptr< StructuredBlockForest > blockForest = + blockforest::createUniformBlockGrid(numBlocks[0], numBlocks[1], numBlocks[2], // blocks + cellsPerBlock[0], cellsPerBlock[1], cellsPerBlock[2], // cells + real_c(1.0), // dx + true, // one block per process + false, false, false); // periodicity + + // create (dummy) lattice model + LatticeModel_T latticeModel = LatticeModel_T(lbm::collision_model::SRT(real_c(1.8))); + + // add pdf field + const BlockDataID pdfFieldID = lbm::addPdfFieldToStorage(blockForest, "PDF field", latticeModel, + Vector3< real_t >(real_c(0)), real_c(1), field::fzyx); + + // add normal field + const BlockDataID normalFieldID = field::addToStorage< VectorField_T >( + blockForest, "Normal field", Vector3< real_t >(real_c(0)), field::fzyx, uint_c(1)); + + // add fill level field + const BlockDataID fillFieldID = + field::addToStorage< ScalarField_T >(blockForest, "Fill level field", real_c(1), field::fzyx, uint_c(1)); + + // add field for excess mass + const BlockDataID excessMassFieldID = + field::addToStorage< ScalarField_T >(blockForest, "Excess mass field", real_c(0), field::fzyx, uint_c(1)); + + // add boundary handling + const std::shared_ptr< FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling = + std::make_shared< FreeSurfaceBoundaryHandling_T >(blockForest, pdfFieldID, fillFieldID); + const BlockDataID flagFieldID = freeSurfaceBoundaryHandling->getFlagFieldID(); + const typename FreeSurfaceBoundaryHandling_T::FlagInfo_T& flagInfo = freeSurfaceBoundaryHandling->getFlagInfo(); + + // initialize cells as in file "/tests/lbm/free_surface/dynamics/ExcessMassDistributionParallelTest.odp" + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + ScalarField_T* const fillField = blockIt->getData< ScalarField_T >(fillFieldID); + ScalarField_T* const excessMassField = blockIt->getData< ScalarField_T >(excessMassFieldID); + FlagField_T* const flagField = blockIt->getData< FlagField_T >(flagFieldID); + PdfField_T* const pdfField = blockIt->getData< PdfField_T >(pdfFieldID); + VectorField_T* const normalField = blockIt->getData< VectorField_T >(normalFieldID); + + WALBERLA_FOR_ALL_CELLS( + fillFieldIt, fillField, excessMassFieldIt, excessMassField, flagFieldIt, flagField, pdfFieldIt, pdfField, + normalFieldIt, normalField, { + const Cell localCell = fillFieldIt.cell(); + + // get global coordinate of this cell + Cell globalCell; + blockForest->transformBlockLocalToGlobalCell(globalCell, *blockIt, localCell); + + if (globalCell == Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0))) + { + *fillFieldIt = real_c(1); + *excessMassFieldIt = real_c(0.2); + pdfField->setDensityAndVelocity(localCell, Vector3< real_t >(real_c(0)), real_c(1)); + field::addFlag(flagFieldIt, flagInfo.liquidFlag); + } + + if (globalCell == Cell(cell_idx_c(1), cell_idx_c(0), cell_idx_c(0))) + { + *fillFieldIt = real_c(0.5); + pdfField->setDensityAndVelocity(localCell, Vector3< real_t >(real_c(0)), real_c(1.5)); + field::addFlag(flagFieldIt, flagInfo.interfaceFlag | flagInfo.convertedFlag); + } + + if (globalCell == Cell(cell_idx_c(2), cell_idx_c(0), cell_idx_c(0))) + { + *fillFieldIt = real_c(0.5); + pdfField->setDensityAndVelocity(localCell, Vector3< real_t >(real_c(0)), real_c(1.4)); + field::addFlag(flagFieldIt, flagInfo.interfaceFlag); + } + + if (globalCell == Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(0))) + { + *fillFieldIt = real_c(1); + *excessMassFieldIt = real_c(0.1); + pdfField->setDensityAndVelocity(localCell, Vector3< real_t >(real_c(0)), real_c(1)); + field::addFlag(flagFieldIt, flagInfo.liquidFlag); + } + + if (globalCell == Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0))) + { + *fillFieldIt = real_c(1.1); + pdfField->setDensityAndVelocity(localCell, Vector3< real_t >(real_c(0)), real_c(1)); + field::addFlag(flagFieldIt, flagInfo.liquidFlag | flagInfo.convertedFlag); + *normalFieldIt = Vector3< real_t >(real_c(0.71), real_c(0.71), real_c(0)); + } + + if (globalCell == Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(0))) + { + *fillFieldIt = real_c(0.5); + pdfField->setDensityAndVelocity(localCell, Vector3< real_t >(real_c(0)), real_c(1.3)); + field::addFlag(flagFieldIt, flagInfo.interfaceFlag); + } + + if (globalCell == Cell(cell_idx_c(0), cell_idx_c(2), cell_idx_c(0))) + { + *fillFieldIt = real_c(0); + pdfField->setDensityAndVelocity(localCell, Vector3< real_t >(real_c(0)), real_c(1)); + field::addFlag(flagFieldIt, flagInfo.gasFlag); + } + + if (globalCell == Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(0))) + { + *fillFieldIt = real_c(0.5); + pdfField->setDensityAndVelocity(localCell, Vector3< real_t >(real_c(0)), real_c(1.1)); + field::addFlag(flagFieldIt, flagInfo.interfaceFlag | flagInfo.convertedFlag); + } + + if (globalCell == Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(0))) + { + *fillFieldIt = real_c(0.5); + pdfField->setDensityAndVelocity(localCell, Vector3< real_t >(real_c(0)), real_c(1.2)); + field::addFlag(flagFieldIt, flagInfo.interfaceFlag); + } + + if (globalCell == Cell(cell_idx_c(3), cell_idx_c(0), cell_idx_c(0))) + { + *fillFieldIt = real_c(0.5); + pdfField->setDensityAndVelocity(localCell, Vector3< real_t >(real_c(0)), real_c(0.5)); + field::addFlag(flagFieldIt, flagInfo.interfaceFlag | flagInfo.convertedFlag); + } + + if (globalCell == Cell(cell_idx_c(4), cell_idx_c(0), cell_idx_c(0))) + { + *fillFieldIt = real_c(0.5); + pdfField->setDensityAndVelocity(localCell, Vector3< real_t >(real_c(0)), real_c(0.6)); + field::addFlag(flagFieldIt, flagInfo.interfaceFlag | flagInfo.convertedFlag); + } + + if (globalCell == Cell(cell_idx_c(5), cell_idx_c(0), cell_idx_c(0))) + { + *fillFieldIt = real_c(1); + *excessMassFieldIt = real_c(0.03); + pdfField->setDensityAndVelocity(localCell, Vector3< real_t >(real_c(0)), real_c(1)); + field::addFlag(flagFieldIt, flagInfo.liquidFlag); + } + + if (globalCell == Cell(cell_idx_c(3), cell_idx_c(1), cell_idx_c(0))) + { + *fillFieldIt = real_c(1.2); + pdfField->setDensityAndVelocity(localCell, Vector3< real_t >(real_c(0)), real_c(0.95)); + field::addFlag(flagFieldIt, flagInfo.liquidFlag | flagInfo.convertedFlag); + *normalFieldIt = Vector3< real_t >(real_c(0.71), real_c(0.71), real_c(0)); + } + + if (globalCell == Cell(cell_idx_c(4), cell_idx_c(1), cell_idx_c(0))) + { + *fillFieldIt = real_c(0.5); + pdfField->setDensityAndVelocity(localCell, Vector3< real_t >(real_c(0)), real_c(0.7)); + field::addFlag(flagFieldIt, flagInfo.interfaceFlag | flagInfo.convertedFlag); + } + + if (globalCell == Cell(cell_idx_c(5), cell_idx_c(1), cell_idx_c(0))) + { + *fillFieldIt = real_c(1); + *excessMassFieldIt = real_c(0.02); + pdfField->setDensityAndVelocity(localCell, Vector3< real_t >(real_c(0)), real_c(1)); + field::addFlag(flagFieldIt, flagInfo.liquidFlag); + } + + if (globalCell == Cell(cell_idx_c(3), cell_idx_c(2), cell_idx_c(0))) + { + *fillFieldIt = real_c(0.5); + pdfField->setDensityAndVelocity(localCell, Vector3< real_t >(real_c(0)), real_c(0.9)); + field::addFlag(flagFieldIt, flagInfo.interfaceFlag); + } + + if (globalCell == Cell(cell_idx_c(4), cell_idx_c(2), cell_idx_c(0))) + { + *fillFieldIt = real_c(0.5); + pdfField->setDensityAndVelocity(localCell, Vector3< real_t >(real_c(0)), real_c(0.8)); + field::addFlag(flagFieldIt, flagInfo.interfaceFlag | flagInfo.convertedFlag); + } + + if (globalCell == Cell(cell_idx_c(5), cell_idx_c(2), cell_idx_c(0))) + { + *fillFieldIt = real_c(1); + *excessMassFieldIt = real_c(0.01); + pdfField->setDensityAndVelocity(localCell, Vector3< real_t >(real_c(0)), real_c(1)); + field::addFlag(flagFieldIt, flagInfo.liquidFlag); + } + }) // WALBERLA_FOR_ALL_CELLS + } + + // initial communication + Communication_T(blockForest, pdfFieldID, fillFieldID, flagFieldID, normalFieldID, excessMassFieldID)(); + + // create timeloop + SweepTimeloop timeloop(blockForest, uint_c(1)); + + if (excessMassDistributionModel.isEvenlyType()) + { + const ExcessMassDistributionSweepInterfaceEvenly< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > + distributeMassSweep(excessMassDistributionModel, fillFieldID, flagFieldID, pdfFieldID, flagInfo); + timeloop.add() << Sweep(distributeMassSweep, "Distribute excess mass"); + } + else + { + if (excessMassDistributionModel.isWeightedType()) + { + const ExcessMassDistributionSweepInterfaceWeighted< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > + distributeMassSweep(excessMassDistributionModel, fillFieldID, flagFieldID, pdfFieldID, flagInfo, + normalFieldID); + timeloop.add() << Sweep(distributeMassSweep, "Distribute excess mass"); + } + else + { + if (excessMassDistributionModel.isEvenlyLiquidAndAllInterfacePreferInterfaceType()) + { + const ExcessMassDistributionSweepInterfaceAndLiquid< LatticeModel_T, FlagField_T, ScalarField_T, + VectorField_T > + distributeMassSweep(excessMassDistributionModel, fillFieldID, flagFieldID, pdfFieldID, flagInfo, + excessMassFieldID); + timeloop.add() << Sweep(distributeMassSweep, "Distribute excess mass"); + } + } + } + + timeloop.singleStep(); + + // check if excess mass was distributed correctly; expected solutions were obtained manually, see file + // "/tests/lbm/free_surface/dynamics/ExcessMassDistributionParallelTest.odp" + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + const ScalarField_T* const fillField = blockIt->getData< const ScalarField_T >(fillFieldID); + const ScalarField_T* const excessMassField = blockIt->getData< const ScalarField_T >(excessMassFieldID); + + WALBERLA_FOR_ALL_CELLS(fillFieldIt, fillField, excessMassFieldIt, excessMassField, { + Cell globalCell; + blockForest->transformBlockLocalToGlobalCell(globalCell, *blockIt, fillFieldIt.cell()); + + if (excessMassDistributionModel.getModelType() == + ExcessMassDistributionModel::ExcessMassModel::EvenlyAllInterface) + { + // left block + if (globalCell == Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(1), cell_idx_c(0), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.513333333333333), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(2), cell_idx_c(0), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.53125), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.533653846153846), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(0), cell_idx_c(2), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.518181818181818), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.536458333333333), real_c(1e-4)); + } + + // right block + if (globalCell == Cell(cell_idx_c(3), cell_idx_c(0), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5475), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(4), cell_idx_c(0), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.539583333333333), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(5), cell_idx_c(0), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(3), cell_idx_c(1), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(4), cell_idx_c(1), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.533928571428571), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(5), cell_idx_c(1), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(3), cell_idx_c(2), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.526388888888889), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(4), cell_idx_c(2), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5296875), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(5), cell_idx_c(2), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + } + } + + if (excessMassDistributionModel.getModelType() == + ExcessMassDistributionModel::ExcessMassModel::EvenlyOldInterface) + { + // left block + if (globalCell == Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(1), cell_idx_c(0), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(2), cell_idx_c(0), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.557738095238095), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.562179487179487), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(0), cell_idx_c(2), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.567361111111111), real_c(1e-4)); + } + + // right block + if (globalCell == Cell(cell_idx_c(3), cell_idx_c(0), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(4), cell_idx_c(0), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(5), cell_idx_c(0), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(3), cell_idx_c(1), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(4), cell_idx_c(1), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(5), cell_idx_c(1), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(3), cell_idx_c(2), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.552777777777778), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(4), cell_idx_c(2), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(5), cell_idx_c(2), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + } + } + + if (excessMassDistributionModel.getModelType() == + ExcessMassDistributionModel::ExcessMassModel::EvenlyNewInterface) + { + // left block + if (globalCell == Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(1), cell_idx_c(0), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.533333333333333), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(2), cell_idx_c(0), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(0), cell_idx_c(2), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.545454545454546), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4)); + } + + // right block + if (globalCell == Cell(cell_idx_c(3), cell_idx_c(0), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.595), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(4), cell_idx_c(0), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.579166666666667), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(5), cell_idx_c(0), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(3), cell_idx_c(1), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(4), cell_idx_c(1), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.567857142857143), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(5), cell_idx_c(1), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(3), cell_idx_c(2), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(4), cell_idx_c(2), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.559375), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(5), cell_idx_c(2), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + } + } + + if (excessMassDistributionModel.getModelType() == + ExcessMassDistributionModel::ExcessMassModel::WeightedAllInterface) + { + // left block + if (globalCell == Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(1), cell_idx_c(0), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(2), cell_idx_c(0), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.519230769230769), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(0), cell_idx_c(2), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.522727272727273), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.541666666666667), real_c(1e-4)); + } + + // right block + if (globalCell == Cell(cell_idx_c(3), cell_idx_c(0), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(4), cell_idx_c(0), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(5), cell_idx_c(0), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(3), cell_idx_c(1), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(4), cell_idx_c(1), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.567857142857143), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(5), cell_idx_c(1), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(3), cell_idx_c(2), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.552777777777778), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(4), cell_idx_c(2), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.61875), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(5), cell_idx_c(2), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + } + } + + if (excessMassDistributionModel.getModelType() == + ExcessMassDistributionModel::ExcessMassModel::WeightedOldInterface) + { + // left block + if (globalCell == Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(1), cell_idx_c(0), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(2), cell_idx_c(0), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.525641025641026), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(0), cell_idx_c(2), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.555555555555556), real_c(1e-4)); + } + + // right block + if (globalCell == Cell(cell_idx_c(3), cell_idx_c(0), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(4), cell_idx_c(0), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(5), cell_idx_c(0), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(3), cell_idx_c(1), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(4), cell_idx_c(1), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(5), cell_idx_c(1), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(3), cell_idx_c(2), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.711111111111111), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(4), cell_idx_c(2), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(5), cell_idx_c(2), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + } + } + + if (excessMassDistributionModel.getModelType() == + ExcessMassDistributionModel::ExcessMassModel::WeightedNewInterface) + { + // left block + if (globalCell == Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(1), cell_idx_c(0), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(2), cell_idx_c(0), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(0), cell_idx_c(2), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.590909090909091), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4)); + } + + // right block + if (globalCell == Cell(cell_idx_c(3), cell_idx_c(0), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(4), cell_idx_c(0), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(5), cell_idx_c(0), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(3), cell_idx_c(1), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(4), cell_idx_c(1), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.59047619047619), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(5), cell_idx_c(1), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(3), cell_idx_c(2), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(4), cell_idx_c(2), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.658333333333333), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(5), cell_idx_c(2), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + } + } + + if (excessMassDistributionModel.getModelType() == + ExcessMassDistributionModel::ExcessMassModel::EvenlyLiquidAndAllInterface) + { + // left block + if (globalCell == Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0.039285714285714), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(1), cell_idx_c(0), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.570634920634921), real_c(1e-4)); + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(2), cell_idx_c(0), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.527168367346939), real_c(1e-4)); + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0.080952380952381), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0.091666666666667), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.529258241758242), real_c(1e-4)); + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(0), cell_idx_c(2), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0), real_c(1e-4)); + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.535714285714286), real_c(1e-4)); + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.531696428571429), real_c(1e-4)); + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4)); + } + + // right block + if (globalCell == Cell(cell_idx_c(3), cell_idx_c(0), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5475), real_c(1e-4)); + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(4), cell_idx_c(0), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.562916666666667), real_c(1e-4)); + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(5), cell_idx_c(0), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0.004), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(3), cell_idx_c(1), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(4), cell_idx_c(1), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.558690476190476), real_c(1e-4)); + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(5), cell_idx_c(1), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0.013333333333333), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(3), cell_idx_c(2), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.526388888888889), real_c(1e-4)); + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(4), cell_idx_c(2), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.538854166666667), real_c(1e-4)); + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(5), cell_idx_c(2), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0.004), real_c(1e-4)); + } + } + + if (excessMassDistributionModel.getModelType() == + ExcessMassDistributionModel::ExcessMassModel::EvenlyLiquidAndAllInterfacePreferInterface) + { + // left block + if (globalCell == Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(1), cell_idx_c(0), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.68), real_c(1e-4)); + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(2), cell_idx_c(0), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.53125), real_c(1e-4)); + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.533653846153846), real_c(1e-4)); + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(0), cell_idx_c(2), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0), real_c(1e-4)); + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.563636363636364), real_c(1e-4)); + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.536458333333333), real_c(1e-4)); + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4)); + } + + // right block + if (globalCell == Cell(cell_idx_c(3), cell_idx_c(0), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.5475), real_c(1e-4)); + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(4), cell_idx_c(0), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.575694444444444), real_c(1e-4)); + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(5), cell_idx_c(0), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(3), cell_idx_c(1), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(4), cell_idx_c(1), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.57202380952381), real_c(1e-4)); + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(5), cell_idx_c(1), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(3), cell_idx_c(2), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.526388888888889), real_c(1e-4)); + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(4), cell_idx_c(2), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(0.544270833333333), real_c(1e-4)); + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4)); + } + + if (globalCell == Cell(cell_idx_c(5), cell_idx_c(2), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*fillFieldIt, real_c(1), real_c(1e-4)); + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4)); + } + } + }) // WALBERLA_FOR_ALL_CELLS + } + + MPIManager::instance()->resetMPI(); +} + +int main(int argc, char** argv) +{ + debug::enterTestMode(); + Environment env(argc, argv); + + ExcessMassDistributionModel model = ExcessMassDistributionModel("EvenlyAllInterface"); + WALBERLA_LOG_INFO_ON_ROOT("Testing model " << model.getFullModelSpecification()); + runSimulation(model); + + model = ExcessMassDistributionModel("EvenlyOldInterface"); + WALBERLA_LOG_INFO_ON_ROOT("Testing model " << model.getFullModelSpecification()); + runSimulation(model); + + model = ExcessMassDistributionModel("EvenlyNewInterface"); + WALBERLA_LOG_INFO_ON_ROOT("Testing model " << model.getFullModelSpecification()); + runSimulation(model); + + model = ExcessMassDistributionModel("WeightedAllInterface"); + WALBERLA_LOG_INFO_ON_ROOT("Testing model " << model.getFullModelSpecification()); + runSimulation(model); + + model = ExcessMassDistributionModel("WeightedOldInterface"); + WALBERLA_LOG_INFO_ON_ROOT("Testing model " << model.getFullModelSpecification()); + runSimulation(model); + + model = ExcessMassDistributionModel("WeightedNewInterface"); + WALBERLA_LOG_INFO_ON_ROOT("Testing model " << model.getFullModelSpecification()); + runSimulation(model); + + model = ExcessMassDistributionModel("EvenlyLiquidAndAllInterface"); + WALBERLA_LOG_INFO_ON_ROOT("Testing model " << model.getFullModelSpecification()); + runSimulation(model); + + model = ExcessMassDistributionModel("EvenlyLiquidAndAllInterfacePreferInterface"); + WALBERLA_LOG_INFO_ON_ROOT("Testing model " << model.getFullModelSpecification()); + runSimulation(model); + + return EXIT_SUCCESS; +} +} // namespace ExcessMassDistributionTest +} // namespace free_surface +} // namespace walberla + +int main(int argc, char** argv) { return walberla::free_surface::ExcessMassDistributionTest::main(argc, argv); } \ No newline at end of file diff --git a/tests/lbm/free_surface/dynamics/ExcessMassDistributionParallelTest.ods b/tests/lbm/free_surface/dynamics/ExcessMassDistributionParallelTest.ods new file mode 100644 index 0000000000000000000000000000000000000000..fa159c7b1b01c41933c16c886af01b6344a26bf9 GIT binary patch literal 16650 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 literal 0 HcmV?d00001 diff --git a/tests/lbm/free_surface/dynamics/InflowTest.cpp b/tests/lbm/free_surface/dynamics/InflowTest.cpp new file mode 100644 index 000000000..da8b5df18 --- /dev/null +++ b/tests/lbm/free_surface/dynamics/InflowTest.cpp @@ -0,0 +1,281 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file InflowTest.cpp +//! \ingroup lbm/free_surface/dynamics +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Test inflow boundary condition. +//! +//! Set inflow boundaries and initialize gas everywhere. After performing one time step, it is evaluated whether gas +//! cells have been converted to interface and initialized correctly according to the neighboring inflow cells. +//====================================================================================================================== + +#include "blockforest/Initialization.h" +#include "blockforest/communication/UniformBufferedScheme.h" + +#include "core/Environment.h" +#include "core/debug/TestSubsystem.h" + +#include "field/AddToStorage.h" +#include "field/adaptors/AdaptorCreators.h" +#include "field/communication/PackInfo.h" + +#include "lbm/field/Adaptors.h" +#include "lbm/field/AddToStorage.h" +#include "lbm/free_surface//FlagInfo.h" +#include "lbm/free_surface/boundary/FreeSurfaceBoundaryHandling.h" +#include "lbm/free_surface/dynamics/StreamReconstructAdvectSweep.h" +#include "lbm/free_surface/dynamics/SurfaceDynamicsHandler.h" +#include "lbm/free_surface/surface_geometry/SurfaceGeometryHandler.h" +#include "lbm/lattice_model/CollisionModel.h" +#include "lbm/lattice_model/D3Q19.h" +#include "lbm/lattice_model/ForceModel.h" + +#include "timeloop/SweepTimeloop.h" + +#include <algorithm> + +namespace walberla +{ +namespace free_surface +{ +namespace InflowTest +{ +// define types +using Flag_T = uint32_t; +using ScalarField_T = GhostLayerField< real_t, 1 >; +using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >; +using FlagField_T = FlagField< Flag_T >; +using LatticeModel_T = lbm::D3Q19< lbm::collision_model::SRT, true, lbm::force_model::None, 2 >; + +void testInflow() +{ + // define types + using Stencil_T = typename LatticeModel_T::Stencil; + using PdfField_T = lbm::PdfField< LatticeModel_T >; + using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >; + + // define the domain size + const Vector3< uint_t > numBlocks(uint_c(1)); + const Vector3< uint_t > domainSize(uint_c(10), uint_c(3), uint_c(1)); + const Vector3< uint_t > cellsPerBlock(domainSize[0] / numBlocks[0], domainSize[1] / numBlocks[1], + domainSize[2] / numBlocks[2]); + + // create block grid + const std::shared_ptr< StructuredBlockForest > blockForest = + blockforest::createUniformBlockGrid(numBlocks[0], numBlocks[1], numBlocks[2], // blocks + cellsPerBlock[0], cellsPerBlock[1], cellsPerBlock[2], // cells + real_c(1.0), // dx + true, // one block per process + false, false, true); // periodicity + + real_t relaxRate = real_c(0.51); + + // create lattice model with omega=0.51 + LatticeModel_T latticeModel(relaxRate); + + // add fields + BlockDataID pdfFieldID = lbm::addPdfFieldToStorage(blockForest, "PDF field", latticeModel, field::fzyx); + BlockDataID fillFieldID = + field::addToStorage< ScalarField_T >(blockForest, "Fill levels", real_c(0.0), field::fzyx, uint_c(2)); + BlockDataID forceFieldID = field::addToStorage< VectorField_T >( + blockForest, "Force field", Vector3< real_t >(real_c(0)), field::fzyx, uint_c(1)); + BlockDataID densityAdaptor = field::addFieldAdaptor< typename lbm::Adaptor< LatticeModel_T >::Density >( + blockForest, pdfFieldID, "DensityAdaptor"); + BlockDataID velocityAdaptor = field::addFieldAdaptor< typename lbm::Adaptor< LatticeModel_T >::VelocityVector >( + blockForest, pdfFieldID, "VelocityAdaptor"); + + // add boundary handling + const std::shared_ptr< FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling = + std::make_shared< FreeSurfaceBoundaryHandling_T >(blockForest, pdfFieldID, fillFieldID); + const BlockDataID flagFieldID = freeSurfaceBoundaryHandling->getFlagFieldID(); + const typename FreeSurfaceBoundaryHandling_T::FlagInfo_T& flagInfo = freeSurfaceBoundaryHandling->getFlagInfo(); + + // set inflow boundary conditions in some cells of the western domain border + freeSurfaceBoundaryHandling->setInflowInCell(Cell(cell_idx_c(0), cell_idx_c(-1), cell_idx_c(0)), + Vector3< real_t >(real_c(0), real_c(0.01), real_c(0))); + freeSurfaceBoundaryHandling->setInflowInCell(Cell(cell_idx_c(-1), cell_idx_c(0), cell_idx_c(0)), + Vector3< real_t >(real_c(0.01), real_c(0), real_c(0))); + freeSurfaceBoundaryHandling->setInflowInCell(Cell(cell_idx_c(-1), cell_idx_c(1), cell_idx_c(0)), + Vector3< real_t >(real_c(0.02), real_c(0), real_c(0))); + freeSurfaceBoundaryHandling->setInflowInCell(Cell(cell_idx_c(-1), cell_idx_c(2), cell_idx_c(0)), + Vector3< real_t >(real_c(0.03), real_c(0.01), real_c(0))); + freeSurfaceBoundaryHandling->setInflowInCell(Cell(cell_idx_c(-1), cell_idx_c(3), cell_idx_c(0)), + Vector3< real_t >(real_c(0.04), real_c(0), real_c(0))); + freeSurfaceBoundaryHandling->setInflowInCell(Cell(cell_idx_c(-1), cell_idx_c(4), cell_idx_c(0)), + Vector3< real_t >(real_c(0.05), real_c(0.02), real_c(0))); + + // these inflow cells should not have any influence as their velocity direction points away from the neighboring gas + // cells + freeSurfaceBoundaryHandling->setInflowInCell(Cell(cell_idx_c(-1), cell_idx_c(5), cell_idx_c(0)), + Vector3< real_t >(real_c(-0.06), real_c(0), real_c(0))); + freeSurfaceBoundaryHandling->setInflowInCell(Cell(cell_idx_c(-1), cell_idx_c(6), cell_idx_c(0)), + Vector3< real_t >(real_c(-0.07), real_c(0), real_c(0))); + + freeSurfaceBoundaryHandling->initFlagsFromFillLevel(); + + // add bubble model + auto bubbleModel = std::make_shared< bubble_model::BubbleModel< Stencil_T > >(blockForest, true); + bubbleModel->initFromFillLevelField(fillFieldID); + bubbleModel->setAtmosphere(Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0)), real_c(1)); + + // create timeloop + SweepTimeloop timeloop(blockForest, uint_c(1)); + + // add communication + blockforest::communication::UniformBufferedScheme< typename LatticeModel_T::Stencil > comm(blockForest); + comm.addPackInfo(std::make_shared< field::communication::PackInfo< PdfField_T > >(pdfFieldID)); + comm.addPackInfo(std::make_shared< field::communication::PackInfo< ScalarField_T > >(fillFieldID)); + comm.addPackInfo( + std::make_shared< field::communication::PackInfo< FlagField_T > >(freeSurfaceBoundaryHandling->getFlagFieldID())); + + // communicate + comm(); + + // add surface geometry handler + SurfaceGeometryHandler< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > geometryHandler( + blockForest, freeSurfaceBoundaryHandling, fillFieldID, "FiniteDifferenceMethod", false, false, real_c(0)); + + ConstBlockDataID curvatureFieldID = geometryHandler.getConstCurvatureFieldID(); + ConstBlockDataID normalFieldID = geometryHandler.getConstNormalFieldID(); + + geometryHandler.addSweeps(timeloop); + + // add boundary handling for standard boundaries and free surface boundaries + SurfaceDynamicsHandler< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > dynamicsHandler( + blockForest, pdfFieldID, flagFieldID, fillFieldID, forceFieldID, normalFieldID, curvatureFieldID, + freeSurfaceBoundaryHandling, bubbleModel, "NormalBasedKeepCenter", "EquilibriumRefilling", "EvenlyNewInterface", + relaxRate, Vector3< real_t >(real_c(0)), real_c(0), false, false, real_c(1e-3), real_c(1e-1)); + + dynamicsHandler.addSweeps(timeloop); + + timeloop.singleStep(); + + // evaluate if inflow boundary has generated the correct interface cells + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + const ScalarField_T* const fillField = blockIt->getData< const ScalarField_T >(fillFieldID); + const typename lbm::Adaptor< LatticeModel_T >::Density* const densityField = + blockIt->getData< const typename lbm::Adaptor< LatticeModel_T >::Density >(densityAdaptor); + const typename lbm::Adaptor< LatticeModel_T >::VelocityVector* const velocityField = + blockIt->getData< const typename lbm::Adaptor< LatticeModel_T >::VelocityVector >(velocityAdaptor); + const FlagField_T* const flagField = + blockIt->getData< const FlagField_T >(freeSurfaceBoundaryHandling->getFlagFieldID()); + + WALBERLA_FOR_ALL_CELLS(fillFieldIt, fillField, velocityFieldIt, velocityField, densityFieldIt, densityField, + flagFieldIt, flagField, { + if (flagFieldIt.cell() == Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0))) + { + WALBERLA_CHECK(flagInfo.isInterface(flagFieldIt)); + + // velocity must be the average from inflow cells (-1,0,0) and (1,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL((*velocityFieldIt)[0], real_c(0.005), real_c(1e-15)); + WALBERLA_CHECK_FLOAT_EQUAL((*velocityFieldIt)[1], real_c(0.005), real_c(1e-15)); + WALBERLA_CHECK_FLOAT_EQUAL((*velocityFieldIt)[2], real_c(0), real_c(1e-15)); + WALBERLA_CHECK_FLOAT_EQUAL(*densityFieldIt, real_c(1), real_c(1e-15)); + continue; + } + + if (flagFieldIt.cell() == Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(0))) + { + WALBERLA_CHECK(flagInfo.isInterface(flagFieldIt)); + + // velocity must be identical to the inflow cell (-1,1,0); no other inflow cell + // should influence this cell + WALBERLA_CHECK_FLOAT_EQUAL((*velocityFieldIt)[0], real_c(0.02), real_c(1e-15)); + WALBERLA_CHECK_FLOAT_EQUAL((*velocityFieldIt)[1], real_c(0), real_c(1e-15)); + WALBERLA_CHECK_FLOAT_EQUAL((*velocityFieldIt)[2], real_c(0), real_c(1e-15)); + WALBERLA_CHECK_FLOAT_EQUAL(*densityFieldIt, real_c(1), real_c(1e-15)); + continue; + } + + if (flagFieldIt.cell() == Cell(cell_idx_c(0), cell_idx_c(2), cell_idx_c(0))) + { + WALBERLA_CHECK(flagInfo.isInterface(flagFieldIt)); + + // velocity must be identical to the inflow cell (-1,2,0); no other inflow cell + // should influence this cell + WALBERLA_CHECK_FLOAT_EQUAL((*velocityFieldIt)[0], real_c(0.03), real_c(1e-15)); + WALBERLA_CHECK_FLOAT_EQUAL((*velocityFieldIt)[1], real_c(0.01), real_c(1e-15)); + WALBERLA_CHECK_FLOAT_EQUAL((*velocityFieldIt)[2], real_c(0), real_c(1e-15)); + WALBERLA_CHECK_FLOAT_EQUAL(*densityFieldIt, real_c(1), real_c(1e-15)); + continue; + } + + if (flagFieldIt.cell() == Cell(cell_idx_c(0), cell_idx_c(3), cell_idx_c(0))) + { + WALBERLA_CHECK(flagInfo.isInterface(flagFieldIt)); + + // velocity must be the average from inflow cells (-1,2,0) and (-1,3,0) + WALBERLA_CHECK_FLOAT_EQUAL((*velocityFieldIt)[0], real_c(0.035), real_c(1e-15)); + WALBERLA_CHECK_FLOAT_EQUAL((*velocityFieldIt)[1], real_c(0.005), real_c(1e-15)); + WALBERLA_CHECK_FLOAT_EQUAL((*velocityFieldIt)[2], real_c(0), real_c(1e-15)); + WALBERLA_CHECK_FLOAT_EQUAL(*densityFieldIt, real_c(1), real_c(1e-15)); + continue; + } + + if (flagFieldIt.cell() == Cell(cell_idx_c(0), cell_idx_c(4), cell_idx_c(0))) + { + WALBERLA_CHECK(flagInfo.isInterface(flagFieldIt)); + + // velocity must be identical to inflow cell (-1,4,0); no other inflow cell + // should influence this cell + WALBERLA_CHECK_FLOAT_EQUAL((*velocityFieldIt)[0], real_c(0.05), real_c(1e-15)); + WALBERLA_CHECK_FLOAT_EQUAL((*velocityFieldIt)[1], real_c(0.02), real_c(1e-15)); + WALBERLA_CHECK_FLOAT_EQUAL((*velocityFieldIt)[2], real_c(0), real_c(1e-15)); + WALBERLA_CHECK_FLOAT_EQUAL(*densityFieldIt, real_c(1), real_c(1e-15)); + WALBERLA_LOG_DEVEL_VAR(*velocityFieldIt); + WALBERLA_LOG_DEVEL_VAR(flagInfo.isInterface(flagFieldIt)); + continue; + } + + if (flagFieldIt.cell() == Cell(cell_idx_c(0), cell_idx_c(5), cell_idx_c(0))) + { + // cell must be converted due to velocity vector pointing from inflow + // cell(-1,4,0) to this cell + WALBERLA_CHECK(flagInfo.isInterface(flagFieldIt)); + + // velocity must be identical to inflow cell (-1,4,0); no other inflow cell + // should influence this cell + WALBERLA_CHECK_FLOAT_EQUAL((*velocityFieldIt)[0], real_c(0.05), real_c(1e-15)); + WALBERLA_CHECK_FLOAT_EQUAL((*velocityFieldIt)[1], real_c(0.02), real_c(1e-15)); + WALBERLA_CHECK_FLOAT_EQUAL((*velocityFieldIt)[2], real_c(0), real_c(1e-15)); + WALBERLA_CHECK_FLOAT_EQUAL(*densityFieldIt, real_c(1), real_c(1e-15)); + continue; + } + + // cell(0,6,0) + WALBERLA_CHECK(flagInfo.isGas(flagFieldIt)); + }) // WALBERLA_FOR_ALL_CELLS + } + + MPIManager::instance()->resetMPI(); +} + +int main(int argc, char** argv) +{ + debug::enterTestMode(); + Environment env(argc, argv); + + testInflow(); + + return EXIT_SUCCESS; +} + +} // namespace InflowTest +} // namespace free_surface +} // namespace walberla + +int main(int argc, char** argv) { return walberla::free_surface::InflowTest::main(argc, argv); } \ No newline at end of file diff --git a/tests/lbm/free_surface/dynamics/LatticeModelGenerationFreeSurface.py b/tests/lbm/free_surface/dynamics/LatticeModelGenerationFreeSurface.py new file mode 100644 index 000000000..0ef55dce4 --- /dev/null +++ b/tests/lbm/free_surface/dynamics/LatticeModelGenerationFreeSurface.py @@ -0,0 +1,34 @@ +import sympy as sp + +from lbmpy.creationfunctions import LBMConfig, LBMOptimisation, create_lb_collision_rule +from lbmpy.enums import ForceModel, Method, Stencil +from lbmpy.stencils import LBStencil + +from pystencils_walberla import CodeGeneration +from lbmpy_walberla import generate_lattice_model + +# general parameters +stencil = LBStencil(Stencil.D3Q19) +omega = sp.Symbol('omega') +force = sp.symbols('force_:3') +layout = 'fzyx' + +# method definition +lbm_config = LBMConfig(stencil=stencil, + method=Method.SRT, + relaxation_rate=omega, + compressible=True, + force=force, + force_model=ForceModel.GUO, + zero_centered=False, + streaming_pattern='pull') # free surface implementation only works with pull pattern + +# optimizations to be used by the code generator +lbm_opt = LBMOptimisation(cse_global=True, + field_layout=layout) + +collision_rule = create_lb_collision_rule(lbm_config=lbm_config, + lbm_optimisation=lbm_opt) + +with CodeGeneration() as ctx: + generate_lattice_model(ctx, "GeneratedLatticeModel_FreeSurface", collision_rule, field_layout=layout) diff --git a/tests/lbm/free_surface/dynamics/PdfReconstructionFreeSlipTest.cpp b/tests/lbm/free_surface/dynamics/PdfReconstructionFreeSlipTest.cpp new file mode 100644 index 000000000..15ff0d96d --- /dev/null +++ b/tests/lbm/free_surface/dynamics/PdfReconstructionFreeSlipTest.cpp @@ -0,0 +1,201 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file PdfReconstructionFreeSlipTest.cpp +//! \ingroup lbm/free_surface/dynamics +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Test PDF reconstruction due to a free-slip cell near the free surface boundary. +//! +//! Initialize a 3x3 grid and test reconstruction of PDFs due to a free slip boundary condition. +//! [L][L][L] with L: liquid cell; I: interface cell; G: gas cell; f: free-slip cell +//! [L][I][G] F: free-slip cell of interest (for the following explanation) +//! [f][f][F] +//! The PDF that streams from the free-slip cell in the lower right corner (F) into the interface cell (I) must be +//! reconstructed. This is because PDFs in the free-slip cells are specularly reflected. Therefore, this free-slip +//! cell's PDF in direction (-1,1) is the same as the the gas cell's PDF in direction (-1,-1). However, since PDFs in +//! gas cells are not available, this PDF must be reconstructed. +//====================================================================================================================== + +#include "blockforest/Initialization.h" + +#include "core/Environment.h" +#include "core/debug/TestSubsystem.h" + +#include "field/FieldClone.h" + +#include "lbm/blockforest/communication/SimpleCommunication.h" +#include "lbm/field/Adaptors.h" +#include "lbm/field/AddToStorage.h" +#include "lbm/free_surface/boundary/FreeSurfaceBoundaryHandling.h" +#include "lbm/free_surface/bubble_model/Geometry.h" +#include "lbm/free_surface/dynamics/PdfReconstructionModel.h" +#include "lbm/free_surface/dynamics/functionality/ReconstructInterfaceCellABB.h" +#include "lbm/lattice_model/D2Q9.h" + +#include "timeloop/SweepTimeloop.h" + +namespace walberla +{ +namespace free_surface +{ +namespace PdfReconstructionFreeSlipTest +{ +using LatticeModel_T = lbm::D2Q9< lbm::collision_model::SRT, true, lbm::force_model::None, 2 >; +using Stencil = typename LatticeModel_T::Stencil; + +using Communication_T = blockforest::SimpleCommunication< LatticeModel_T::CommunicationStencil >; + +using PdfField_T = lbm::PdfField< LatticeModel_T >; +using ScalarField_T = GhostLayerField< real_t, 1 >; +using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >; + +using flag_t = uint32_t; +using FlagField_T = FlagField< flag_t >; +using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >; + +void runSimulation() +{ + // define the domain size + const Vector3< uint_t > numBlocks(uint_c(1)); + const Vector3< uint_t > domainSize(uint_c(3), uint_c(3), uint_c(1)); + const Vector3< uint_t > cellsPerBlock(domainSize[0] / numBlocks[0], domainSize[1] / numBlocks[1], + domainSize[2] / numBlocks[2]); + + // create block grid + const std::shared_ptr< StructuredBlockForest > blockForest = + blockforest::createUniformBlockGrid(numBlocks[0], numBlocks[1], numBlocks[2], // blocks + cellsPerBlock[0], cellsPerBlock[1], cellsPerBlock[2], // cells + real_c(1.0), // dx + true, // one block per process + false, false, false); // periodicity + + // create (dummy) lattice model + LatticeModel_T latticeModel = LatticeModel_T(lbm::collision_model::SRT(real_c(1.8))); + + // add pdf source and destination fields + const BlockDataID pdfSrcFieldID = + lbm::addPdfFieldToStorage(blockForest, "PDF source field", latticeModel, uint_c(0), field::fzyx); + const BlockDataID pdfDstFieldID = + lbm::addPdfFieldToStorage(blockForest, "PDF destination field", latticeModel, uint_c(0), field::fzyx); + + // add (dummy) normal field + const BlockDataID normalFieldID = field::addToStorage< VectorField_T >( + blockForest, "Normals", Vector3< real_t >(real_c(0)), field::fzyx, uint_c(1)); + + // add fill level field (MUST be initialized with 1, i.e., fluid everywhere for this test; otherwise the fluid + // flag is not detected below by initFlagsFromFillLevel()) + const BlockDataID fillFieldID = + field::addToStorage< ScalarField_T >(blockForest, "Fill level field", real_c(1), field::fzyx, uint_c(1)); + + // central interface cell, in which the reconstruction and evaluation will be performed + const Cell centralCell = Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0)); + + // construct 3x3 grid with flags according to the description at the top of this file + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + ScalarField_T* const fillField = blockIt->getData< ScalarField_T >(fillFieldID); + VectorField_T* const normalField = blockIt->getData< VectorField_T >(normalFieldID); + + WALBERLA_FOR_ALL_CELLS( + fillFieldIt, fillField, normalFieldIt, normalField, + + // initialize gas cell + if (fillFieldIt.cell() == Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(0))) { *fillFieldIt = real_c(0); } + + // initialize interface cells + if (fillFieldIt.cell() == centralCell) { *fillFieldIt = real_c(0.5); } + + // initialize fluid cells + if (fillFieldIt.cell() == Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(0)) || + fillFieldIt.cell() == Cell(cell_idx_c(0), cell_idx_c(2), cell_idx_c(0)) || + fillFieldIt.cell() == Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(0)) || + fillFieldIt.cell() == Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(0))) { + *fillFieldIt = real_c(1); + }) // WALBERLA_FOR_ALL_CELLS + } + + // add boundary handling + const std::shared_ptr< FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling = + std::make_shared< FreeSurfaceBoundaryHandling_T >(blockForest, pdfSrcFieldID, fillFieldID); + const BlockDataID flagFieldID = freeSurfaceBoundaryHandling->getFlagFieldID(); + const typename FreeSurfaceBoundaryHandling_T::FlagInfo_T& flagInfo = freeSurfaceBoundaryHandling->getFlagInfo(); + freeSurfaceBoundaryHandling->initFlagsFromFillLevel(); + freeSurfaceBoundaryHandling->setFreeSlipAtBorder(stencil::S, cell_idx_c(0)); + + // initial communication + Communication_T(blockForest, pdfSrcFieldID, fillFieldID, flagFieldID)(); + + // create timeloop + SweepTimeloop timeloop(blockForest, uint_c(1)); + + // perform reconstruction + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + const PdfField_T* const pdfSrcField = blockIt->getData< const PdfField_T >(pdfSrcFieldID); + PdfField_T* const pdfDstField = blockIt->getData< PdfField_T >(pdfDstFieldID); + const VectorField_T* const normalField = blockIt->getData< const VectorField_T >(normalFieldID); + const FlagField_T* const flagField = blockIt->getData< const FlagField_T >(flagFieldID); + + WALBERLA_FOR_ALL_CELLS( + pdfSrcFieldIt, pdfSrcField, pdfDstFieldIt, pdfDstField, flagFieldIt, flagField, normalFieldIt, normalField, + if (pdfSrcFieldIt.cell() == centralCell) { + // reconstruct with rhoGas!=1 such that reconstructed values differ from those in pdfSrcField + reconstructInterfaceCellLegacy< LatticeModel_T >(flagField, pdfSrcFieldIt, flagFieldIt, normalFieldIt, + flagInfo, real_c(2), pdfDstFieldIt, + PdfReconstructionModel("OnlyMissing")); + }) // WALBERLA_FOR_ALL_CELLS + } + + // evaluate if the correct cells were reconstructed + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + const PdfField_T* const pdfSrcField = blockIt->getData< const PdfField_T >(pdfSrcFieldID); + const PdfField_T* const pdfDstField = blockIt->getData< const PdfField_T >(pdfDstFieldID); + + WALBERLA_FOR_ALL_CELLS( + pdfSrcFieldIt, pdfSrcField, pdfDstFieldIt, pdfDstField, if (pdfSrcFieldIt.cell() == centralCell) { + // the boundary handling is not executed so the only change must be the PDF coming from the free-slip + // boundary cell that reflects the PDF from the gas cell + WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[0], pdfDstFieldIt[0]); // C, (0,0,0) + WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[1], pdfDstFieldIt[1]); // N, (0,1,0) + WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[2], pdfDstFieldIt[2]); // S, (0,-1,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[3], pdfDstFieldIt[3]); // W, (-1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[4], pdfDstFieldIt[4]); // E, (1,0,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[5], pdfDstFieldIt[5]); // NW, (-1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[6], pdfDstFieldIt[6]); // NE, (1,1,0) + + // this is the PDF that must be reconstructed due to having + WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[7], pdfDstFieldIt[7]); // SW, (-1,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[8], pdfDstFieldIt[8]); // SE, (1,-1,0) + }) + } + + MPIManager::instance()->resetMPI(); +} + +int main(int argc, char** argv) +{ + debug::enterTestMode(); + Environment env(argc, argv); + + runSimulation(); + + return EXIT_SUCCESS; +} +} // namespace PdfReconstructionFreeSlipTest +} // namespace free_surface +} // namespace walberla + +int main(int argc, char** argv) { return walberla::free_surface::PdfReconstructionFreeSlipTest::main(argc, argv); } \ No newline at end of file diff --git a/tests/lbm/free_surface/dynamics/PdfReconstructionTest.cpp b/tests/lbm/free_surface/dynamics/PdfReconstructionTest.cpp new file mode 100644 index 000000000..2638379cd --- /dev/null +++ b/tests/lbm/free_surface/dynamics/PdfReconstructionTest.cpp @@ -0,0 +1,606 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file PdfReconstructionTest.cpp +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Test PDF reconstruction at free surface boundary. +//! +//! Initialize 3x3 grid similar to figure 3 in publication of Koerner et al., 2005 and test reconstruction of PDFs at +//! the free surface boundary with respect to the models specified in PdfReconstructionModel.h. +//====================================================================================================================== + +#include "blockforest/Initialization.h" + +#include "core/Environment.h" +#include "core/debug/TestSubsystem.h" + +#include "field/FieldClone.h" + +#include "lbm/blockforest/communication/SimpleCommunication.h" +#include "lbm/field/Adaptors.h" +#include "lbm/field/AddToStorage.h" +#include "lbm/free_surface/boundary/FreeSurfaceBoundaryHandling.h" +#include "lbm/free_surface/bubble_model/Geometry.h" +#include "lbm/free_surface/dynamics/PdfReconstructionModel.h" +#include "lbm/free_surface/dynamics/functionality/ReconstructInterfaceCellABB.h" +#include "lbm/lattice_model/D2Q9.h" + +#include "timeloop/SweepTimeloop.h" + +namespace walberla +{ +namespace free_surface +{ +namespace PdfReconstructionTest +{ +using LatticeModel_T = lbm::D2Q9< lbm::collision_model::SRT, true, lbm::force_model::None, 2 >; +using Stencil = typename LatticeModel_T::Stencil; + +using Communication_T = blockforest::SimpleCommunication< LatticeModel_T::CommunicationStencil >; + +using PdfField_T = lbm::PdfField< LatticeModel_T >; +using ScalarField_T = GhostLayerField< real_t, 1 >; +using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >; + +using flag_t = uint32_t; +using FlagField_T = FlagField< flag_t >; +using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >; + +void runSimulation(const PdfReconstructionModel& pdfReconstructionModel) +{ + // define the domain size + const Vector3< uint_t > numBlocks(uint_c(1)); + const Vector3< uint_t > domainSize(uint_c(3), uint_c(3), uint_c(1)); + const Vector3< uint_t > cellsPerBlock(domainSize[0] / numBlocks[0], domainSize[1] / numBlocks[1], + domainSize[2] / numBlocks[2]); + + // create block grid + const std::shared_ptr< StructuredBlockForest > blockForest = + blockforest::createUniformBlockGrid(numBlocks[0], numBlocks[1], numBlocks[2], // blocks + cellsPerBlock[0], cellsPerBlock[1], cellsPerBlock[2], // cells + real_c(1.0), // dx + true, // one block per process + false, false, false); // periodicity + + // create (dummy) lattice model + LatticeModel_T latticeModel = LatticeModel_T(lbm::collision_model::SRT(real_c(1.8))); + + // add pdf source and destination fields + const BlockDataID pdfSrcFieldID = + lbm::addPdfFieldToStorage(blockForest, "PDF source field", latticeModel, uint_c(0), field::fzyx); + const BlockDataID pdfDstFieldID = + lbm::addPdfFieldToStorage(blockForest, "PDF destination field", latticeModel, uint_c(0), field::fzyx); + + // add normal field + const BlockDataID normalFieldID = field::addToStorage< VectorField_T >( + blockForest, "Normal field", Vector3< real_t >(real_c(0)), field::fzyx, uint_c(1)); + + // add fill level field (MUST be initialized with 1, i.e., fluid everywhere for this test; otherwise the fluid + // flag is not detected below by initFlagsFromFillLevel()) + const BlockDataID fillFieldID = + field::addToStorage< ScalarField_T >(blockForest, "Fill level field", real_c(1), field::fzyx, uint_c(1)); + + // central interface cell, in which the reconstruction and evaluation will be performed + const Cell centralCell = Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0)); + + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + ScalarField_T* const fillField = blockIt->getData< ScalarField_T >(fillFieldID); + VectorField_T* const normalField = blockIt->getData< VectorField_T >(normalFieldID); + + WALBERLA_FOR_ALL_CELLS(fillFieldIt, fillField, normalFieldIt, normalField, { + // initialize gas cells as in figure 3 from Koerner et al., 2005 + if (fillFieldIt.cell() == Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0)) || + fillFieldIt.cell() == Cell(cell_idx_c(1), cell_idx_c(0), cell_idx_c(0)) || + fillFieldIt.cell() == Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(0))) + { + *fillFieldIt = real_c(0); + } + + // initialize interface cells as in figure 3 from Koerner et al., 2005 + if (fillFieldIt.cell() == Cell(cell_idx_c(2), cell_idx_c(0), cell_idx_c(0)) || + fillFieldIt.cell() == Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0)) || + fillFieldIt.cell() == Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(0)) || + fillFieldIt.cell() == Cell(cell_idx_c(0), cell_idx_c(2), cell_idx_c(0)) || + fillFieldIt.cell() == Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(0))) + { + *fillFieldIt = real_c(0.5); + } + + // initialize fluid cell as in figure 3 from Koerner et al., 2005 + if (fillFieldIt.cell() == Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(0))) { *fillFieldIt = real_c(1); } + + // initialize interface normal as in figure 3 from Koerner et al., 2005 (values estimated) + // IMPORTANT REMARK: In this waLBerla's free surface implementation, the normal is defined to point from fluid + // to gas, whereas in Koerner et al., the normal is defined to point from gas to fluid. Therefore, we + // initialize the normal in opposite direction than in figure 3. + if (normalFieldIt.cell() == centralCell) + { + *normalFieldIt = Vector3< real_t >(real_c(-0.83867), real_c(-0.54464), real_c(0)); + } + }) // WALBERLA_FOR_ALL_CELLS + } + + // add boundary handling + const std::shared_ptr< FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling = + std::make_shared< FreeSurfaceBoundaryHandling_T >(blockForest, pdfSrcFieldID, fillFieldID); + const BlockDataID flagFieldID = freeSurfaceBoundaryHandling->getFlagFieldID(); + const typename FreeSurfaceBoundaryHandling_T::FlagInfo_T& flagInfo = freeSurfaceBoundaryHandling->getFlagInfo(); + freeSurfaceBoundaryHandling->initFlagsFromFillLevel(); + + // initial communication + Communication_T(blockForest, pdfSrcFieldID, fillFieldID, flagFieldID)(); + + // create timeloop + SweepTimeloop timeloop(blockForest, uint_c(1)); + + // perform reconstruction + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + const PdfField_T* const pdfSrcField = blockIt->getData< const PdfField_T >(pdfSrcFieldID); + PdfField_T* const pdfDstField = blockIt->getData< PdfField_T >(pdfDstFieldID); + const VectorField_T* const normalField = blockIt->getData< const VectorField_T >(normalFieldID); + const FlagField_T* const flagField = blockIt->getData< const FlagField_T >(flagFieldID); + + WALBERLA_FOR_ALL_CELLS( + pdfSrcFieldIt, pdfSrcField, pdfDstFieldIt, pdfDstField, flagFieldIt, flagField, normalFieldIt, normalField, { + if (pdfSrcFieldIt.cell() == centralCell) + { + // reconstruct with rhoGas!=1 such that reconstructed values differ from those in pdfSrcField + reconstructInterfaceCellLegacy< LatticeModel_T >(flagField, pdfSrcFieldIt, flagFieldIt, normalFieldIt, + flagInfo, real_c(2), pdfDstFieldIt, + pdfReconstructionModel); + } + }) // WALBERLA_FOR_ALL_CELLS + } + + const PdfReconstructionModel::ReconstructionModel reconstructionModel = pdfReconstructionModel.getModelType(); + const uint_t minReconstruct = pdfReconstructionModel.getNumMinReconstruct(); + const PdfReconstructionModel::FallbackModel fallbackModel = pdfReconstructionModel.getFallbackModel(); + + // evaluate if the correct cells were reconstructed: + // 1. equality/ inequality between pdfSrc and pdfDst was verified manually, i.e., by hand + // 2. comparison with expected (reconstructed) values: + // - results only valid for rhoGas=2 (as specified above) + // - results obtained with a version that is believed to be correct + // - was included for detection of changes in PDF reconstruction boundary condition + // - makes 1. actually obsolete (1. was kept for as this is what the test was actually intended for, i.e., find + // out whether the correct PDFs are reconstructed) + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + const PdfField_T* const pdfSrcField = blockIt->getData< const PdfField_T >(pdfSrcFieldID); + const PdfField_T* const pdfDstField = blockIt->getData< const PdfField_T >(pdfDstFieldID); + + WALBERLA_FOR_ALL_CELLS(pdfSrcFieldIt, pdfSrcField, pdfDstFieldIt, pdfDstField, { + if (pdfSrcFieldIt.cell() == centralCell) + { + if (reconstructionModel == PdfReconstructionModel::ReconstructionModel::NormalBasedKeepCenter || + (reconstructionModel == PdfReconstructionModel::ReconstructionModel::OnlyMissingMin && + minReconstruct > uint_c(3) && + fallbackModel == PdfReconstructionModel::FallbackModel::NormalBasedKeepCenter)) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[0], real_c(0.444444), real_c(1e-6)); // C, (0,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[1], real_c(0.333333), real_c(1e-6)); // N, (0,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[2], real_c(0.111111), real_c(1e-6)); // S, (0,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[3], real_c(0.111111), real_c(1e-6)); // W, (-1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[4], real_c(0.333333), real_c(1e-6)); // E, (1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[5], real_c(0.0277778), real_c(1e-6)); // NW, (-1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[6], real_c(0.0833333), real_c(1e-6)); // NE, (1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[7], real_c(0.0277778), real_c(1e-6)); // SW, (-1,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[8], real_c(0.0833333), real_c(1e-6)); // SE, (1,-1,0) + + WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[0], pdfDstFieldIt[0]); // C, (0,0,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[1], pdfDstFieldIt[1]); // N, (0,1,0) + WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[2], pdfDstFieldIt[2]); // S, (0,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[3], pdfDstFieldIt[3]); // W, (-1,0,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[4], pdfDstFieldIt[4]); // E, (1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[5], pdfDstFieldIt[5]); // NW, (-1,1,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[6], pdfDstFieldIt[6]); // NE, (1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[7], pdfDstFieldIt[7]); // SW, (-1,-1,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[8], pdfDstFieldIt[8]); // SE, (1,-1,0) + } + + if (reconstructionModel == PdfReconstructionModel::ReconstructionModel::NormalBasedReconstructCenter) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[0], real_c(1.333333), real_c(1e-6)); // C, (0,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[1], real_c(0.333333), real_c(1e-6)); // N, (0,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[2], real_c(0.111111), real_c(1e-6)); // S, (0,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[3], real_c(0.111111), real_c(1e-6)); // W, (-1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[4], real_c(0.333333), real_c(1e-6)); // E, (1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[5], real_c(0.0277778), real_c(1e-6)); // NW, (-1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[6], real_c(0.0833333), real_c(1e-6)); // NE, (1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[7], real_c(0.0277778), real_c(1e-6)); // SW, (-1,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[8], real_c(0.0833333), real_c(1e-6)); // SE, (1,-1,0) + + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[0], pdfDstFieldIt[0]); // C, (0,0,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[1], pdfDstFieldIt[1]); // N, (0,1,0) + WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[2], pdfDstFieldIt[2]); // S, (0,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[3], pdfDstFieldIt[3]); // W, (-1,0,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[4], pdfDstFieldIt[4]); // E, (1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[5], pdfDstFieldIt[5]); // NW, (-1,1,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[6], pdfDstFieldIt[6]); // NE, (1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[7], pdfDstFieldIt[7]); // SW, (-1,-1,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[8], pdfDstFieldIt[8]); // SE, (1,-1,0) + } + + if (reconstructionModel == PdfReconstructionModel::ReconstructionModel::All) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[0], real_c(1.333333), real_c(1e-6)); // C, (0,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[1], real_c(0.333333), real_c(1e-6)); // N, (0,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[2], real_c(0.333333), real_c(1e-6)); // S, (0,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[3], real_c(0.333333), real_c(1e-6)); // W, (-1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[4], real_c(0.333333), real_c(1e-6)); // E, (1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[5], real_c(0.0833333), real_c(1e-6)); // NW, (-1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[6], real_c(0.0833333), real_c(1e-6)); // NE, (1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[7], real_c(0.0833333), real_c(1e-6)); // SW, (-1,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[8], real_c(0.0833333), real_c(1e-6)); // SE, (1,-1,0) + + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[0], pdfDstFieldIt[0]); // C, (0,0,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[1], pdfDstFieldIt[1]); // N, (0,1,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[2], pdfDstFieldIt[2]); // S, (0,-1,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[3], pdfDstFieldIt[3]); // W, (-1,0,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[4], pdfDstFieldIt[4]); // E, (1,0,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[5], pdfDstFieldIt[5]); // NW, (-1,1,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[6], pdfDstFieldIt[6]); // NE, (1,1,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[7], pdfDstFieldIt[7]); // SW, (-1,-1,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[8], pdfDstFieldIt[8]); // SE, (1,-1,0) + } + + // in the setup here, 3 PDFs will always be reconstructed as they are coming from the gas-side and are + // therefore missing + if (reconstructionModel == PdfReconstructionModel::ReconstructionModel::OnlyMissing || + (reconstructionModel == PdfReconstructionModel::ReconstructionModel::OnlyMissingMin && + (minReconstruct == uint_c(0) || minReconstruct == uint_c(1) || minReconstruct == uint_c(2) || + minReconstruct == uint_c(3)) && + (fallbackModel == PdfReconstructionModel::FallbackModel::Smallest || + fallbackModel == PdfReconstructionModel::FallbackModel::Largest || + fallbackModel == PdfReconstructionModel::FallbackModel::NormalBasedKeepCenter))) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[0], real_c(0.444444), real_c(1e-6)); // C, (0,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[1], real_c(0.333333), real_c(1e-6)); // N, (0,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[2], real_c(0.111111), real_c(1e-6)); // S, (0,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[3], real_c(0.111111), real_c(1e-6)); // W, (-1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[4], real_c(0.333333), real_c(1e-6)); // E, (1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[5], real_c(0.0277778), real_c(1e-6)); // NW, (-1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[6], real_c(0.0833333), real_c(1e-6)); // NE, (1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[7], real_c(0.0277778), real_c(1e-6)); // SW, (-1,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[8], real_c(0.0277778), real_c(1e-6)); // SE, (1,-1,0) + + WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[0], pdfDstFieldIt[0]); // C, (0,0,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[1], pdfDstFieldIt[1]); // N, (0,1,0) + WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[2], pdfDstFieldIt[2]); // S, (0,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[3], pdfDstFieldIt[3]); // W, (-1,0,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[4], pdfDstFieldIt[4]); // E, (1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[5], pdfDstFieldIt[5]); // NW, (-1,1,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[6], pdfDstFieldIt[6]); // NE, (1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[7], pdfDstFieldIt[7]); // SW, (-1,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[8], pdfDstFieldIt[8]); // SE, (1,-1,0) + } + + if (reconstructionModel == PdfReconstructionModel::ReconstructionModel::OnlyMissingMin && + minReconstruct == uint_c(4) && fallbackModel == PdfReconstructionModel::FallbackModel::Smallest) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[0], real_c(1.333333), real_c(1e-6)); // C, (0,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[1], real_c(0.333333), real_c(1e-6)); // N, (0,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[2], real_c(0.111111), real_c(1e-6)); // S, (0,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[3], real_c(0.111111), real_c(1e-6)); // W, (-1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[4], real_c(0.333333), real_c(1e-6)); // E, (1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[5], real_c(0.0277778), real_c(1e-6)); // NW, (-1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[6], real_c(0.0833333), real_c(1e-6)); // NE, (1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[7], real_c(0.0277778), real_c(1e-6)); // SW, (-1,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[8], real_c(0.0277778), real_c(1e-6)); // SE, (1,-1,0) + + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[0], pdfDstFieldIt[0]); // C, (0,0,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[1], pdfDstFieldIt[1]); // N, (0,1,0) + WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[2], pdfDstFieldIt[2]); // S, (0,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[3], pdfDstFieldIt[3]); // W, (-1,0,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[4], pdfDstFieldIt[4]); // E, (1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[5], pdfDstFieldIt[5]); // NW, (-1,1,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[6], pdfDstFieldIt[6]); // NE, (1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[7], pdfDstFieldIt[7]); // SW, (-1,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[8], pdfDstFieldIt[8]); // SE, (1,-1,0) + } + + if (reconstructionModel == PdfReconstructionModel::ReconstructionModel::OnlyMissingMin && + minReconstruct == uint_c(4) && fallbackModel == PdfReconstructionModel::FallbackModel::Largest) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[0], real_c(0.444444), real_c(1e-6)); // C, (0,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[1], real_c(0.333333), real_c(1e-6)); // N, (0,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[2], real_c(0.111111), real_c(1e-6)); // S, (0,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[3], real_c(0.111111), real_c(1e-6)); // W, (-1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[4], real_c(0.333333), real_c(1e-6)); // E, (1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[5], real_c(0.0277778), real_c(1e-6)); // NW, (-1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[6], real_c(0.0833333), real_c(1e-6)); // NE, (1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[7], real_c(0.0833333), real_c(1e-6)); // SW, (-1,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[8], real_c(0.0277778), real_c(1e-6)); // SE, (1,-1,0) + + WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[0], pdfDstFieldIt[0]); // C, (0,0,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[1], pdfDstFieldIt[1]); // N, (0,1,0) + WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[2], pdfDstFieldIt[2]); // S, (0,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[3], pdfDstFieldIt[3]); // W, (-1,0,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[4], pdfDstFieldIt[4]); // E, (1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[5], pdfDstFieldIt[5]); // NW, (-1,1,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[6], pdfDstFieldIt[6]); // NE, (1,1,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[7], pdfDstFieldIt[7]); // SW, (-1,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[8], pdfDstFieldIt[8]); // SE, (1,-1,0) + } + + if (reconstructionModel == PdfReconstructionModel::ReconstructionModel::OnlyMissingMin && + minReconstruct == uint_c(5) && fallbackModel == PdfReconstructionModel::FallbackModel::Smallest) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[0], real_c(1.333333), real_c(1e-6)); // C, (0,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[1], real_c(0.333333), real_c(1e-6)); // N, (0,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[2], real_c(0.111111), real_c(1e-6)); // S, (0,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[3], real_c(0.111111), real_c(1e-6)); // W, (-1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[4], real_c(0.333333), real_c(1e-6)); // E, (1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[5], real_c(0.0277778), real_c(1e-6)); // NW, (-1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[6], real_c(0.0833333), real_c(1e-6)); // NE, (1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[7], real_c(0.0277778), real_c(1e-6)); // SW, (-1,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[8], real_c(0.0833333), real_c(1e-6)); // SE, (1,-1,0) + + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[0], pdfDstFieldIt[0]); // C, (0,0,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[1], pdfDstFieldIt[1]); // N, (0,1,0) + WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[2], pdfDstFieldIt[2]); // S, (0,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[3], pdfDstFieldIt[3]); // W, (-1,0,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[4], pdfDstFieldIt[4]); // E, (1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[5], pdfDstFieldIt[5]); // NW, (-1,1,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[6], pdfDstFieldIt[6]); // NE, (1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[7], pdfDstFieldIt[7]); // SW, (-1,-1,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[8], pdfDstFieldIt[8]); // SE, (1,-1,0) + } + + if (reconstructionModel == PdfReconstructionModel::ReconstructionModel::OnlyMissingMin && + minReconstruct == uint_c(5) && fallbackModel == PdfReconstructionModel::FallbackModel::Largest) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[0], real_c(0.444444), real_c(1e-6)); // C, (0,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[1], real_c(0.333333), real_c(1e-6)); // N, (0,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[2], real_c(0.111111), real_c(1e-6)); // S, (0,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[3], real_c(0.333333), real_c(1e-6)); // W, (-1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[4], real_c(0.333333), real_c(1e-6)); // E, (1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[5], real_c(0.0277778), real_c(1e-6)); // NW, (-1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[6], real_c(0.0833333), real_c(1e-6)); // NE, (1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[7], real_c(0.0833333), real_c(1e-6)); // SW, (-1,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[8], real_c(0.0277778), real_c(1e-6)); // SE, (1,-1,0) + + WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[0], pdfDstFieldIt[0]); // C, (0,0,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[1], pdfDstFieldIt[1]); // N, (0,1,0) + WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[2], pdfDstFieldIt[2]); // S, (0,-1,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[3], pdfDstFieldIt[3]); // W, (-1,0,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[4], pdfDstFieldIt[4]); // E, (1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[5], pdfDstFieldIt[5]); // NW, (-1,1,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[6], pdfDstFieldIt[6]); // NE, (1,1,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[7], pdfDstFieldIt[7]); // SW, (-1,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[8], pdfDstFieldIt[8]); // SE, (1,-1,0) + } + + if (reconstructionModel == PdfReconstructionModel::ReconstructionModel::OnlyMissingMin && + minReconstruct == uint_c(6) && fallbackModel == PdfReconstructionModel::FallbackModel::Smallest) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[0], real_c(1.333333), real_c(1e-6)); // C, (0,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[1], real_c(0.333333), real_c(1e-6)); // N, (0,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[2], real_c(0.111111), real_c(1e-6)); // S, (0,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[3], real_c(0.111111), real_c(1e-6)); // W, (-1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[4], real_c(0.333333), real_c(1e-6)); // E, (1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[5], real_c(0.0833333), real_c(1e-6)); // NW, (-1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[6], real_c(0.0833333), real_c(1e-6)); // NE, (1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[7], real_c(0.0277778), real_c(1e-6)); // SW, (-1,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[8], real_c(0.0833333), real_c(1e-6)); // SE, (1,-1,0) + + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[0], pdfDstFieldIt[0]); // C, (0,0,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[1], pdfDstFieldIt[1]); // N, (0,1,0) + WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[2], pdfDstFieldIt[2]); // S, (0,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[3], pdfDstFieldIt[3]); // W, (-1,0,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[4], pdfDstFieldIt[4]); // E, (1,0,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[5], pdfDstFieldIt[5]); // NW, (-1,1,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[6], pdfDstFieldIt[6]); // NE, (1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[7], pdfDstFieldIt[7]); // SW, (-1,-1,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[8], pdfDstFieldIt[8]); // SE, (1,-1,0) + } + + if (reconstructionModel == PdfReconstructionModel::ReconstructionModel::OnlyMissingMin && + minReconstruct == uint_c(6) && fallbackModel == PdfReconstructionModel::FallbackModel::Largest) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[0], real_c(0.444444), real_c(1e-6)); // C, (0,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[1], real_c(0.333333), real_c(1e-6)); // N, (0,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[2], real_c(0.333333), real_c(1e-6)); // S, (0,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[3], real_c(0.333333), real_c(1e-6)); // W, (-1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[4], real_c(0.333333), real_c(1e-6)); // E, (1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[5], real_c(0.0277778), real_c(1e-6)); // NW, (-1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[6], real_c(0.0833333), real_c(1e-6)); // NE, (1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[7], real_c(0.0833333), real_c(1e-6)); // SW, (-1,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[8], real_c(0.0277778), real_c(1e-6)); // SE, (1,-1,0) + + WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[0], pdfDstFieldIt[0]); // C, (0,0,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[1], pdfDstFieldIt[1]); // N, (0,1,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[2], pdfDstFieldIt[2]); // S, (0,-1,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[3], pdfDstFieldIt[3]); // W, (-1,0,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[4], pdfDstFieldIt[4]); // E, (1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[5], pdfDstFieldIt[5]); // NW, (-1,1,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[6], pdfDstFieldIt[6]); // NE, (1,1,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[7], pdfDstFieldIt[7]); // SW, (-1,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[8], pdfDstFieldIt[8]); // SE, (1,-1,0) + } + + if (reconstructionModel == PdfReconstructionModel::ReconstructionModel::OnlyMissingMin && + minReconstruct == uint_c(7) && fallbackModel == PdfReconstructionModel::FallbackModel::Smallest) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[0], real_c(1.333333), real_c(1e-6)); // C, (0,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[1], real_c(0.333333), real_c(1e-6)); // N, (0,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[2], real_c(0.333333), real_c(1e-6)); // S, (0,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[3], real_c(0.111111), real_c(1e-6)); // W, (-1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[4], real_c(0.333333), real_c(1e-6)); // E, (1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[5], real_c(0.0833333), real_c(1e-6)); // NW, (-1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[6], real_c(0.0833333), real_c(1e-6)); // NE, (1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[7], real_c(0.0277778), real_c(1e-6)); // SW, (-1,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[8], real_c(0.0833333), real_c(1e-6)); // SE, (1,-1,0) + + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[0], pdfDstFieldIt[0]); // C, (0,0,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[1], pdfDstFieldIt[1]); // N, (0,1,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[2], pdfDstFieldIt[2]); // S, (0,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[3], pdfDstFieldIt[3]); // W, (-1,0,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[4], pdfDstFieldIt[4]); // E, (1,0,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[5], pdfDstFieldIt[5]); // NW, (-1,1,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[6], pdfDstFieldIt[6]); // NE, (1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[7], pdfDstFieldIt[7]); // SW, (-1,-1,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[8], pdfDstFieldIt[8]); // SE, (1,-1,0) + } + + if (reconstructionModel == PdfReconstructionModel::ReconstructionModel::OnlyMissingMin && + minReconstruct == uint_c(7) && fallbackModel == PdfReconstructionModel::FallbackModel::Largest) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[0], real_c(0.444444), real_c(1e-6)); // C, (0,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[1], real_c(0.333333), real_c(1e-6)); // N, (0,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[2], real_c(0.333333), real_c(1e-6)); // S, (0,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[3], real_c(0.333333), real_c(1e-6)); // W, (-1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[4], real_c(0.333333), real_c(1e-6)); // E, (1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[5], real_c(0.0277778), real_c(1e-6)); // NW, (-1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[6], real_c(0.0833333), real_c(1e-6)); // NE, (1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[7], real_c(0.0833333), real_c(1e-6)); // SW, (-1,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[8], real_c(0.0833333), real_c(1e-6)); // SE, (1,-1,0) + + WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[0], pdfDstFieldIt[0]); // C, (0,0,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[1], pdfDstFieldIt[1]); // N, (0,1,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[2], pdfDstFieldIt[2]); // S, (0,-1,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[3], pdfDstFieldIt[3]); // W, (-1,0,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[4], pdfDstFieldIt[4]); // E, (1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[5], pdfDstFieldIt[5]); // NW, (-1,1,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[6], pdfDstFieldIt[6]); // NE, (1,1,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[7], pdfDstFieldIt[7]); // SW, (-1,-1,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[8], pdfDstFieldIt[8]); // SE, (1,-1,0) + } + + if (reconstructionModel == PdfReconstructionModel::ReconstructionModel::OnlyMissingMin && + minReconstruct == uint_c(8) && fallbackModel == PdfReconstructionModel::FallbackModel::Smallest) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[0], real_c(1.333333), real_c(1e-6)); // C, (0,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[1], real_c(0.333333), real_c(1e-6)); // N, (0,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[2], real_c(0.333333), real_c(1e-6)); // S, (0,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[3], real_c(0.333333), real_c(1e-6)); // W, (-1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[4], real_c(0.333333), real_c(1e-6)); // E, (1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[5], real_c(0.0833333), real_c(1e-6)); // NW, (-1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[6], real_c(0.0833333), real_c(1e-6)); // NE, (1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[7], real_c(0.0277778), real_c(1e-6)); // SW, (-1,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[8], real_c(0.0833333), real_c(1e-6)); // SE, (1,-1,0) + + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[0], pdfDstFieldIt[0]); // C, (0,0,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[1], pdfDstFieldIt[1]); // N, (0,1,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[2], pdfDstFieldIt[2]); // S, (0,-1,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[3], pdfDstFieldIt[3]); // W, (-1,0,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[4], pdfDstFieldIt[4]); // E, (1,0,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[5], pdfDstFieldIt[5]); // NW, (-1,1,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[6], pdfDstFieldIt[6]); // NE, (1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[7], pdfDstFieldIt[7]); // SW, (-1,-1,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[8], pdfDstFieldIt[8]); // SE, (1,-1,0) + } + + if (reconstructionModel == PdfReconstructionModel::ReconstructionModel::OnlyMissingMin && + minReconstruct == uint_c(8) && fallbackModel == PdfReconstructionModel::FallbackModel::Largest) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[0], real_c(0.444444), real_c(1e-6)); // C, (0,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[1], real_c(0.333333), real_c(1e-6)); // N, (0,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[2], real_c(0.333333), real_c(1e-6)); // S, (0,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[3], real_c(0.333333), real_c(1e-6)); // W, (-1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[4], real_c(0.333333), real_c(1e-6)); // E, (1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[5], real_c(0.0833333), real_c(1e-6)); // NW, (-1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[6], real_c(0.0833333), real_c(1e-6)); // NE, (1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[7], real_c(0.0833333), real_c(1e-6)); // SW, (-1,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[8], real_c(0.0833333), real_c(1e-6)); // SE, (1,-1,0) + + WALBERLA_CHECK_FLOAT_EQUAL(pdfSrcFieldIt[0], pdfDstFieldIt[0]); // C, (0,0,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[1], pdfDstFieldIt[1]); // N, (0,1,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[2], pdfDstFieldIt[2]); // S, (0,-1,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[3], pdfDstFieldIt[3]); // W, (-1,0,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[4], pdfDstFieldIt[4]); // E, (1,0,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[5], pdfDstFieldIt[5]); // NW, (-1,1,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[6], pdfDstFieldIt[6]); // NE, (1,1,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[7], pdfDstFieldIt[7]); // SW, (-1,-1,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[8], pdfDstFieldIt[8]); // SE, (1,-1,0) + } + + if (reconstructionModel == PdfReconstructionModel::ReconstructionModel::OnlyMissingMin && + minReconstruct == uint_c(9) && + (fallbackModel == PdfReconstructionModel::FallbackModel::Smallest || + fallbackModel == PdfReconstructionModel::FallbackModel::Largest)) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[0], real_c(1.333333), real_c(1e-6)); // C, (0,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[1], real_c(0.333333), real_c(1e-6)); // N, (0,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[2], real_c(0.333333), real_c(1e-6)); // S, (0,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[3], real_c(0.333333), real_c(1e-6)); // W, (-1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[4], real_c(0.333333), real_c(1e-6)); // E, (1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[5], real_c(0.0833333), real_c(1e-6)); // NW, (-1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[6], real_c(0.0833333), real_c(1e-6)); // NE, (1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[7], real_c(0.0833333), real_c(1e-6)); // SW, (-1,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfDstFieldIt[8], real_c(0.0833333), real_c(1e-6)); // SE, (1,-1,0) + + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[0], pdfDstFieldIt[0]); // C, (0,0,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[1], pdfDstFieldIt[1]); // N, (0,1,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[2], pdfDstFieldIt[2]); // S, (0,-1,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[3], pdfDstFieldIt[3]); // W, (-1,0,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[4], pdfDstFieldIt[4]); // E, (1,0,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[5], pdfDstFieldIt[5]); // NW, (-1,1,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[6], pdfDstFieldIt[6]); // NE, (1,1,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[7], pdfDstFieldIt[7]); // SW, (-1,-1,0) + WALBERLA_CHECK_FLOAT_UNEQUAL(pdfSrcFieldIt[8], pdfDstFieldIt[8]); // SE, (1,-1,0) + } + } + }) // WALBERLA_FOR_ALL_CELLS + } + + MPIManager::instance()->resetMPI(); +} + +int main(int argc, char** argv) +{ + debug::enterTestMode(); + Environment env(argc, argv); + + PdfReconstructionModel model = PdfReconstructionModel("NormalBasedKeepCenter"); + WALBERLA_LOG_INFO_ON_ROOT("Testing model " << model.getFullModelSpecification()); + runSimulation(model); + + model = PdfReconstructionModel("NormalBasedReconstructCenter"); + WALBERLA_LOG_INFO_ON_ROOT("Testing model " << model.getFullModelSpecification()); + runSimulation(model); + + model = PdfReconstructionModel("All"); + WALBERLA_LOG_INFO_ON_ROOT("Testing model " << model.getFullModelSpecification()); + runSimulation(model); + + model = PdfReconstructionModel("OnlyMissing"); + WALBERLA_LOG_INFO_ON_ROOT("Testing model " << model.getFullModelSpecification()); + runSimulation(model); + + for (uint_t i = uint_c(0); i != uint_c(10); ++i) + { + model = PdfReconstructionModel("OnlyMissingMin-" + std::to_string(i) + "-smallest"); + WALBERLA_LOG_INFO_ON_ROOT("Testing model " << model.getFullModelSpecification()); + runSimulation(model); + + model = PdfReconstructionModel("OnlyMissingMin-" + std::to_string(i) + "-largest"); + WALBERLA_LOG_INFO_ON_ROOT("Testing model " << model.getFullModelSpecification()); + runSimulation(model); + + model = PdfReconstructionModel("OnlyMissingMin-" + std::to_string(i) + "-normalBasedKeepCenter"); + WALBERLA_LOG_INFO_ON_ROOT("Testing model " << model.getFullModelSpecification()); + runSimulation(model); + } + + return EXIT_SUCCESS; +} +} // namespace PdfReconstructionTest +} // namespace free_surface +} // namespace walberla + +int main(int argc, char** argv) { return walberla::free_surface::PdfReconstructionTest::main(argc, argv); } \ No newline at end of file diff --git a/tests/lbm/free_surface/dynamics/PdfRefillingTest.cpp b/tests/lbm/free_surface/dynamics/PdfRefillingTest.cpp new file mode 100644 index 000000000..ae4ec83d1 --- /dev/null +++ b/tests/lbm/free_surface/dynamics/PdfRefillingTest.cpp @@ -0,0 +1,1123 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file PdfRefillingTest.cpp +//! \ingroup lbm/free_surface/dynamics +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \author Michael Zikeli +//! \brief Test PDF refilling of gas cells that are converted to interface cells at the free surface boundary. +//! +//! For each test two helper functions are necessary, that are both passed in the main as callback functions +//! - The initialization function which fills four sets (gasCells, interfaceCells, liquidCells, conversionCells). +//! - The verification function, that checks whether the values are calculated correctly. +//! The domain is consists of 5x3x1 cells with periodic boundaries. Different scenarios are tested. The expected +//! solutions are obtained manually from the file "/tests/lbm/free_surface/dynamics/PdfRefillingTest.odp". +//====================================================================================================================== + +#include "blockforest/Initialization.h" + +#include "core/Environment.h" +#include "core/debug/TestSubsystem.h" + +#include "lbm/blockforest/communication/SimpleCommunication.h" +#include "lbm/field/Adaptors.h" +#include "lbm/field/AddToStorage.h" +#include "lbm/free_surface/FlagInfo.h" +#include "lbm/free_surface/boundary/FreeSurfaceBoundaryHandling.h" +#include "lbm/free_surface/dynamics/PdfRefillingModel.h" +#include "lbm/free_surface/dynamics/PdfRefillingSweep.h" +#include "lbm/free_surface/surface_geometry/SurfaceGeometryHandler.h" +#include "lbm/lattice_model/D2Q9.h" +#include "lbm/sweeps/CellwiseSweep.h" + +#include "timeloop/SweepTimeloop.h" + +#include <functional> +#include <iostream> + +namespace walberla +{ +namespace free_surface +{ +namespace PdfRefillingTest +{ +using ScalarField_T = GhostLayerField< real_t, 1 >; +using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >; + +using LatticeModel_T = lbm::D2Q9< lbm::collision_model::SRT, true, lbm::force_model::None, 2 >; +using Stencil_T = typename LatticeModel_T::Stencil; + +using Communication_T = blockforest::SimpleCommunication< LatticeModel_T::CommunicationStencil >; + +using flag_t = uint32_t; +using FlagField_T = field::FlagField< flag_t >; +using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >; +using FlagInfo_T = FlagInfo< FlagField_T >; + +using PdfField_T = lbm::PdfField< LatticeModel_T >; + +using RefillingModel_T = PdfRefillingModel::RefillingModel; + +using initCallback_T = + std::function< void(std::set< Cell >&, std::set< Cell >&, std::set< Cell >&, std::set< Cell >&) >; +using verifyCallback_T = std::function< void(const PdfRefillingModel&, const Cell&, const PdfField_T* const) >; + +void runSimulation(const PdfRefillingModel& pdfRefillingModel, const initCallback_T& fillInitializationSets, + const verifyCallback_T& verifyResults); + +/*********************************************************************************************************************** + * Test: noRefillingCells + * | G | G | G | G | G | | G | G | G | G | G | + * | G | G | G | G | G | ==> | G | G | I | G | G | + * | G | G | G | G | G | | G | G | G | G | G | + **********************************************************************************************************************/ +void init_noRefillingCells(std::set< Cell >& gasCells, std::set< Cell >& interfaceCells, std::set< Cell >& liquidCells, + std::set< Cell >& conversionCells); +void verify_noRefillingCells(const PdfRefillingModel& pdfRefillingModel, const Cell& cell, + const PdfField_T* const pdfField); + +/*********************************************************************************************************************** + * Test: fullRefillingCells + * | I | I | I | I | I | | I | I | I | I | I | + * | I | G | I | I | I | ==> | I | I | I | I | I | + * | I | I | I | I | I | | I | I | I | I | I | + **********************************************************************************************************************/ +void init_fullRefillingCells(std::set< Cell >& gasCells, std::set< Cell >& interfaceCells, + std::set< Cell >& liquidCells, std::set< Cell >& conversionCells); +void verify_fullRefillingCells(const PdfRefillingModel& pdfRefillingModel, const Cell& cell, + const PdfField_T* const pdfField); + +/*********************************************************************************************************************** + * Test: someRefillingCells + * | G | I | I | I | I | | I | I | I | I | I | + * | G | G | I | I | I | ==> | G | I | I | I | I | + * | G | G | G | I | I | | G | G | I | I | I | + **********************************************************************************************************************/ +void init_someRefillingCells(std::set< Cell >& gasCells, std::set< Cell >& interfaceCells, + std::set< Cell >& liquidCells, std::set< Cell >& conversionCells); +void verify_someRefillingCells(PdfRefillingModel const& pdfRefillingModel, Cell const& cell, + PdfField_T const* const pdfField); + +/*********************************************************************************************************************** + * Test: straightRefillingCells + * | G | G | I | I | I | | G | G | I | I | I | + * | G | G | I | I | I | ==> | G | I | I | I | I | + * | G | G | I | I | I | | G | G | I | I | I | + **********************************************************************************************************************/ +void init_straightRefillingCells(std::set< Cell >& gasCells, std::set< Cell >& interfaceCells, + std::set< Cell >& liquidCells, std::set< Cell >& conversionCells); +void verify_straightRefillingCells(const PdfRefillingModel& pdfRefillingModel, const Cell& cell, + const PdfField_T* const pdfField); + +int main(int argc, char** argv) +{ + debug::enterTestMode(); + Environment env(argc, argv); + + for (auto modelType : PdfRefillingModel::getTypeIterator()) + { + PdfRefillingModel model = PdfRefillingModel(modelType); + + WALBERLA_LOG_INFO_ON_ROOT("Testing model:" << model.getFullModelSpecification() + << " with setup \"noRefillingCells\":" + << "\n\t | G | G | G | G | G | | G | G | G | G | G | " + << "\n\t | G | G | G | G | G | ==> | G | G | I | G | G | " + << "\n\t | G | G | G | G | G | | G | G | G | G | G | \n"); + runSimulation(model, &init_noRefillingCells, &verify_noRefillingCells); + + WALBERLA_LOG_INFO_ON_ROOT("Testing model:" << model.getFullModelSpecification() + << " with setup \"fullRefillingCells\":" + << "\n\t | I | I | I | I | I | | I | I | I | I | I | " + << "\n\t | I | G | I | I | I | ==> | I | I | I | I | I | " + << "\n\t | I | I | I | I | I | | I | I | I | I | I | \n"); + runSimulation(model, init_fullRefillingCells, verify_fullRefillingCells); + + WALBERLA_LOG_INFO_ON_ROOT("Testing model:" << model.getFullModelSpecification() + << " with setup \"someRefillingCells\":" + << "\n\t | G | I | I | I | I | | I | I | I | I | I | " + << "\n\t | G | G | I | I | I | ==> | G | I | I | I | I | " + << "\n\t | G | G | G | I | I | | G | G | I | I | I | \n"); + runSimulation(model, &init_someRefillingCells, &verify_someRefillingCells); + + WALBERLA_LOG_INFO_ON_ROOT("Testing model:" << model.getFullModelSpecification() + << " with setup \"straightRefillingCells\":" + << "\n\t | G | G | I | I | I | | G | G | I | I | I | " + << "\n\t | G | G | I | I | I | ==> | G | I | I | I | I | " + << "\n\t | G | G | I | I | I | | G | G | I | I | I | \n"); + runSimulation(model, &init_straightRefillingCells, &verify_straightRefillingCells); + } + + return EXIT_SUCCESS; +} + +void init_noRefillingCells(std::set< Cell >& gasCells, std::set< Cell >& interfaceCells, std::set< Cell >& liquidCells, + std::set< Cell >& conversionCells) +{ + gasCells = std::set< Cell >( + { Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0)), Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(0)), + Cell(cell_idx_c(0), cell_idx_c(2), cell_idx_c(0)), Cell(cell_idx_c(1), cell_idx_c(0), cell_idx_c(0)), + Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0)), Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(0)), + Cell(cell_idx_c(2), cell_idx_c(0), cell_idx_c(0)), Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(0)), + Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(0)), Cell(cell_idx_c(3), cell_idx_c(0), cell_idx_c(0)), + Cell(cell_idx_c(3), cell_idx_c(1), cell_idx_c(0)), Cell(cell_idx_c(3), cell_idx_c(2), cell_idx_c(0)), + Cell(cell_idx_c(4), cell_idx_c(0), cell_idx_c(0)), Cell(cell_idx_c(4), cell_idx_c(1), cell_idx_c(0)), + Cell(cell_idx_c(4), cell_idx_c(2), cell_idx_c(0)) }); + + interfaceCells = std::set< Cell >({}); + + liquidCells = std::set< Cell >({}); + + conversionCells = std::set< Cell >({ Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(0)) }); +} + +void init_fullRefillingCells(std::set< Cell >& gasCells, std::set< Cell >& interfaceCells, + std::set< Cell >& liquidCells, std::set< Cell >& conversionCells) +{ + gasCells = std::set< Cell >({ Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0)) }); + + interfaceCells = std::set< Cell >( + { Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0)), Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(0)), + Cell(cell_idx_c(0), cell_idx_c(2), cell_idx_c(0)), Cell(cell_idx_c(1), cell_idx_c(0), cell_idx_c(0)), + Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(0)), Cell(cell_idx_c(2), cell_idx_c(0), cell_idx_c(0)), + Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(0)), Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(0)), + Cell(cell_idx_c(3), cell_idx_c(0), cell_idx_c(0)), Cell(cell_idx_c(3), cell_idx_c(1), cell_idx_c(0)), + Cell(cell_idx_c(3), cell_idx_c(2), cell_idx_c(0)), Cell(cell_idx_c(4), cell_idx_c(0), cell_idx_c(0)), + Cell(cell_idx_c(4), cell_idx_c(1), cell_idx_c(0)), Cell(cell_idx_c(4), cell_idx_c(2), cell_idx_c(0)) }); + + liquidCells = std::set< Cell >({}); + + conversionCells = std::set< Cell >({ Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0)) }); +} + +void init_someRefillingCells(std::set< Cell >& gasCells, std::set< Cell >& interfaceCells, + std::set< Cell >& liquidCells, std::set< Cell >& conversionCells) +{ + gasCells = std::set< Cell >( + { Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0)), Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(0)), + Cell(cell_idx_c(1), cell_idx_c(0), cell_idx_c(0)), Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0)), + Cell(cell_idx_c(0), cell_idx_c(2), cell_idx_c(0)), Cell(cell_idx_c(2), cell_idx_c(0), cell_idx_c(0)) }); + + interfaceCells = std::set< Cell >( + { Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(0)), Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(0)), + Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(0)), Cell(cell_idx_c(3), cell_idx_c(0), cell_idx_c(0)), + Cell(cell_idx_c(3), cell_idx_c(1), cell_idx_c(0)), Cell(cell_idx_c(3), cell_idx_c(2), cell_idx_c(0)), + Cell(cell_idx_c(4), cell_idx_c(0), cell_idx_c(0)), Cell(cell_idx_c(4), cell_idx_c(1), cell_idx_c(0)), + Cell(cell_idx_c(4), cell_idx_c(2), cell_idx_c(0)) }); + + liquidCells = std::set< Cell >({}); + + conversionCells = std::set< Cell >({ Cell(cell_idx_c(0), cell_idx_c(2), cell_idx_c(0)), + Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0)), + Cell(cell_idx_c(2), cell_idx_c(0), cell_idx_c(0)) }); +} + +void init_straightRefillingCells(std::set< Cell >& gasCells, std::set< Cell >& interfaceCells, + std::set< Cell >& liquidCells, std::set< Cell >& conversionCells) +{ + gasCells = std::set< Cell >( + { Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0)), Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(0)), + Cell(cell_idx_c(0), cell_idx_c(2), cell_idx_c(0)), Cell(cell_idx_c(1), cell_idx_c(0), cell_idx_c(0)), + Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0)), Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(0)) }); + + interfaceCells = std::set< Cell >( + { Cell(cell_idx_c(2), cell_idx_c(0), cell_idx_c(0)), Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(0)), + Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(0)), Cell(cell_idx_c(3), cell_idx_c(0), cell_idx_c(0)), + Cell(cell_idx_c(3), cell_idx_c(1), cell_idx_c(0)), Cell(cell_idx_c(3), cell_idx_c(2), cell_idx_c(0)), + Cell(cell_idx_c(4), cell_idx_c(0), cell_idx_c(0)), Cell(cell_idx_c(4), cell_idx_c(1), cell_idx_c(0)), + Cell(cell_idx_c(4), cell_idx_c(2), cell_idx_c(0)) }); + + liquidCells = std::set< Cell >({}); + + conversionCells = std::set< Cell >({ Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0)) }); +} + +void verify_noRefillingCells(const PdfRefillingModel& pdfRefillingModel, const Cell& cell, + const PdfField_T* const pdfField) +{ + switch (pdfRefillingModel.getModelType()) + { + case RefillingModel_T::EquilibriumRefilling: + case RefillingModel_T::AverageRefilling: + case RefillingModel_T::EquilibriumAndNonEquilibriumRefilling: + case RefillingModel_T::ExtrapolationRefilling: + case RefillingModel_T::GradsMomentsRefilling: + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 0), real_c(0.444444444444444), + real_c(1e-6)); // C, (0,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 1), real_c(0.111111111111111), + real_c(1e-6)); // N, (0,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 2), real_c(0.111111111111111), + real_c(1e-6)); // S, (0,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 3), real_c(0.111111111111111), + real_c(1e-6)); // W, (-1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 4), real_c(0.111111111111111), + real_c(1e-6)); // E, (1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 5), real_c(0.027777777777778), + real_c(1e-6)); // NW, (-1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 6), real_c(0.027777777777778), + real_c(1e-6)); // NE, (1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 7), real_c(0.027777777777778), + real_c(1e-6)); // SW, (-1,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 8), real_c(0.027777777777778), + real_c(1e-6)); // SE, (1,-1,0) + break; + default: + WALBERLA_ABORT("The specified PDF refilling model " << pdfRefillingModel.getModelName() + << " is not available.\nSomething went wrong!"); + } +} + +void verify_fullRefillingCells(const PdfRefillingModel& pdfRefillingModel, const Cell& cell, + const PdfField_T* const pdfField) +{ + switch (pdfRefillingModel.getModelType()) + { + case RefillingModel_T::EquilibriumRefilling: + case RefillingModel_T::EquilibriumAndNonEquilibriumRefilling: + case RefillingModel_T::ExtrapolationRefilling: + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(0)), real_c(0.443017361111111), + real_c(1e-6)); // C, (0,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(1)), real_c(0.101584288194444), + real_c(1e-6)); // N, (0,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(2)), real_c(0.120750954861111), + real_c(1e-6)); // S, (0,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(3)), real_c(0.099328038194445), + real_c(1e-6)); // W, (-1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(4)), real_c(0.123494704861111), + real_c(1e-6)); // E, (1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(5)), real_c(0.022800043402778), + real_c(1e-6)); // NW, (-1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(6)), real_c(0.028320616319445), + real_c(1e-6)); // NE, (1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(7)), real_c(0.027070616319445), + real_c(1e-6)); // SW, (-1,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(8)), real_c(0.033633376736111), + real_c(1e-6)); // SE, (1,-1,0) + break; + case RefillingModel_T::AverageRefilling: + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 0), real_c(0.440902777777778), + real_c(1e-6)); // C, (0,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 1), real_c(0.102829861111111), + real_c(1e-6)); // N, (0,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 2), real_c(0.119496527777777), + real_c(1e-6)); // S, (0,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 3), real_c(0.097361111111111), + real_c(1e-6)); // W, (-1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 4), real_c(0.122777777777778), + real_c(1e-6)); // E, (1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 5), real_c(0.022803819444445), + real_c(1e-6)); // NW, (-1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 6), real_c(0.029470486111111), + real_c(1e-6)); // NE, (1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 7), real_c(0.030095486111111), + real_c(1e-6)); // SW, (-1,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 8), real_c(0.034262152777778), + real_c(1e-6)); // SE, (1,-1,0) + break; + case RefillingModel_T::GradsMomentsRefilling: + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 0), real_c(0.449190200617283), + real_c(1e-6)); // C, (0,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 1), real_c(0.098497868441358), + real_c(1e-6)); // N, (0,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 2), real_c(0.117664535108025), + real_c(1e-6)); // S, (0,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 3), real_c(0.100871248070988), + real_c(1e-6)); // W, (-1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 4), real_c(0.125037914737654), + real_c(1e-6)); // E, (1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 5), real_c(0.024343253279321), + real_c(1e-6)); // NW, (-1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 6), real_c(0.025234196566358), + real_c(1e-6)); // NE, (1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 7), real_c(0.023984196566358), + real_c(1e-6)); // SW, (-1,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 8), real_c(0.035176586612654), + real_c(1e-6)); // SE, (1,-1,0) + break; + default: + WALBERLA_ABORT("The specified PDF refilling model " << pdfRefillingModel.getModelName() + << " is not available.\nSomething went wrong!"); + } +} + +void verify_someRefillingCells(const PdfRefillingModel& pdfRefillingModel, const Cell& cell, + const PdfField_T* const pdfField) +{ + switch (pdfRefillingModel.getModelType()) + { + case RefillingModel_T::EquilibriumRefilling: + if (cell.x() == cell_idx_c(1) && cell.y() == cell_idx_c(1) && cell.z() == cell_idx_c(0)) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(0)), real_c(0.443777777777778), + real_c(1e-6)); // C, (0,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(1)), real_c(0.107661111111111), + real_c(1e-6)); // N, (0,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(2)), real_c(0.114327777777778), + real_c(1e-6)); // S, (0,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(3)), real_c(0.101394444444444), + real_c(1e-6)); // W, (-1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(4)), real_c(0.121394444444444), + real_c(1e-6)); // E, (1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(5)), real_c(0.024602777777778), + real_c(1e-6)); // NW, (-1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(6)), real_c(0.029452777777778), + real_c(1e-6)); // NE, (1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(7)), real_c(0.026119444444445), + real_c(1e-6)); // SW, (-1,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(8)), real_c(0.031269444444445), + real_c(1e-6)); // SE, (1,-1,0) + } + else + { + if (cell.x() == cell_idx_c(0) && cell.y() == cell_idx_c(2) && cell.z() == cell_idx_c(0)) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(0)), real_c(0.443506944444444), + real_c(1e-6)); // C, (0,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(1)), real_c(0.103629861111111), + real_c(1e-6)); // N, (0,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(2)), real_c(0.118629861111111), + real_c(1e-6)); // S, (0,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(3)), real_c(0.101326736111111), + real_c(1e-6)); // W, (-1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(4)), real_c(0.121326736111111), + real_c(1e-6)); // E, (1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(5)), real_c(0.023688715277778), + real_c(1e-6)); // NW, (-1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(6)), real_c(0.028351215277778), + real_c(1e-6)); // NE, (1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(7)), real_c(0.027101215277778), + real_c(1e-6)); // SW, (-1,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(8)), real_c(0.032438715277778), + real_c(1e-6)); // SE, (1,-1,0) + } + else + { + if (cell.x() == cell_idx_c(2) && cell.y() == cell_idx_c(0) && cell.z() == cell_idx_c(0)) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(0)), real_c(0.44262037037037), + real_c(1e-6)); // C, (0,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(1)), real_c(0.104188425925926), + real_c(1e-6)); // N, (0,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(2)), real_c(0.117521759259259), + real_c(1e-6)); // S, (0,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(3)), real_c(0.095712037037037), + real_c(1e-6)); // W, (-1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(4)), real_c(0.127934259259259), + real_c(1e-6)); // E, (1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(5)), real_c(0.022553009259259), + real_c(1e-6)); // NW, (-1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(6)), real_c(0.030125231481482), + real_c(1e-6)); // NE, (1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(7)), real_c(0.025403009259259), + real_c(1e-6)); // SW, (-1,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(8)), real_c(0.033941898148148), + real_c(1e-6)); // SE, (1,-1,0) + } + else { WALBERLA_ABORT("The cells that need refilling are not set correctly."); } + } + } + break; + case RefillingModel_T::AverageRefilling: + if (cell.x() == cell_idx_c(1) && cell.y() == cell_idx_c(1) && cell.z() == cell_idx_c(0)) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 0), real_c(0.441666666666667), + real_c(1e-6)); // C, (0,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 1), real_c(0.110416666666666), + real_c(1e-6)); // N, (0,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 2), real_c(0.110416666666666), + real_c(1e-6)); // S, (0,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 3), real_c(0.095833333333333), + real_c(1e-6)); // W, (-1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 4), real_c(0.119166666666667), + real_c(1e-6)); // E, (1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 5), real_c(0.023958333333333), + real_c(1e-6)); // NW, (-1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 6), real_c(0.032291666666667), + real_c(1e-6)); // NE, (1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 7), real_c(0.033958333333333), + real_c(1e-6)); // SW, (-1,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 8), real_c(0.032291666666667), + real_c(1e-6)); // SE, (1,-1,0) + } + else if (cell.x() == cell_idx_c(0) && cell.y() == cell_idx_c(2) && cell.z() == cell_idx_c(0)) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 0), real_c(0.441111111111111), + real_c(1e-6)); // C, (0,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 1), real_c(0.099340277777778), + real_c(1e-6)); // N, (0,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 2), real_c(0.116840277777778), + real_c(1e-6)); // S, (0,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 3), real_c(0.098715277777778), + real_c(1e-6)); // W, (-1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 4), real_c(0.108715277777778), + real_c(1e-6)); // E, (1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 5), real_c(0.022256944444444), + real_c(1e-6)); // NW, (-1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 6), real_c(0.042881944444445), + real_c(1e-6)); // NE, (1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 7), real_c(0.035381944444445), + real_c(1e-6)); // SW, (-1,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 8), real_c(0.034756944444445), + real_c(1e-6)); // SE, (1,-1,0) + } + else if (cell.x() == cell_idx_c(2) && cell.y() == cell_idx_c(0) && cell.z() == cell_idx_c(0)) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 0), real_c(0.44), + real_c(1e-6)); // C, (0,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 1), real_c(0.102708333333333), + real_c(1e-6)); // N, (0,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 2), real_c(0.109375), + real_c(1e-6)); // S, (0,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 3), real_c(0.092847222222222), + real_c(1e-6)); // W, (-1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 4), real_c(0.126736111111111), + real_c(1e-6)); // E, (1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 5), real_c(0.021805555555556), + real_c(1e-6)); // NW, (-1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 6), real_c(0.035694444444445), + real_c(1e-6)); // NE, (1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 7), real_c(0.035138888888889), + real_c(1e-6)); // SW, (-1,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 8), real_c(0.035694444444445), + real_c(1e-6)); // SE, (1,-1,0) + } + else { WALBERLA_ABORT("The cells that need refilling are not set correctly!"); } + break; + case RefillingModel_T::EquilibriumAndNonEquilibriumRefilling: + if (cell.x() == cell_idx_c(1) && cell.y() == cell_idx_c(1) && cell.z() == cell_idx_c(0)) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(0)), real_c(0.452777777777777), + real_c(1e-6)); // C, (0,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(1)), real_c(0.144811111111111), + real_c(1e-6)); // N, (0,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(2)), real_c(0.101477777777778), + real_c(1e-6)); // S, (0,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(3)), real_c(0.051994444444444), + real_c(1e-6)); // W, (-1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(4)), real_c(0.111994444444444), + real_c(1e-6)); // E, (1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(5)), real_c(0.021427777777778), + real_c(1e-6)); // NW, (-1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(6)), real_c(0.020377777777778), + real_c(1e-6)); // NE, (1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(7)), real_c(0.062044444444445), + real_c(1e-6)); // SW, (-1,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(8)), real_c(0.033094444444445), + real_c(1e-6)); // SE, (1,-1,0) + } + else + { + if (cell.x() == cell_idx_c(0) && cell.y() == cell_idx_c(2) && cell.z() == cell_idx_c(0)) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(0)), real_c(0.443506944444444), + real_c(1e-6)); // C, (0,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(1)), real_c(0.103629861111111), + real_c(1e-6)); // N, (0,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(2)), real_c(0.118629861111111), + real_c(1e-6)); // S, (0,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(3)), real_c(0.101326736111111), + real_c(1e-6)); // W, (-1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(4)), real_c(0.121326736111111), + real_c(1e-6)); // E, (1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(5)), real_c(0.023688715277778), + real_c(1e-6)); // NW, (-1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(6)), real_c(0.028351215277778), + real_c(1e-6)); // NE, (1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(7)), real_c(0.027101215277778), + real_c(1e-6)); // SW, (-1,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(8)), real_c(0.032438715277778), + real_c(1e-6)); // SE, (1,-1,0) + } + else + { + if (cell.x() == cell_idx_c(2) && cell.y() == cell_idx_c(0) && cell.z() == cell_idx_c(0)) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(0)), real_c(0.43522037037037), + real_c(1e-6)); // C, (0,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(1)), real_c(0.112788425925926), + real_c(1e-6)); // N, (0,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(2)), real_c(0.046121759259259), + real_c(1e-6)); // S, (0,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(3)), real_c(0.078962037037037), + real_c(1e-6)); // W, (-1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(4)), real_c(0.181184259259259), + real_c(1e-6)); // E, (1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(5)), real_c(0.021353009259259), + real_c(1e-6)); // NW, (-1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(6)), real_c(0.008175231481481), + real_c(1e-6)); // NE, (1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(7)), real_c(0.078453009259259), + real_c(1e-6)); // SW, (-1,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(8)), real_c(0.037741898148148), + real_c(1e-6)); // SE, (1,-1,0) + } + else { WALBERLA_ABORT("The cells that need refilling are not set correctly!"); } + } + } + break; + case RefillingModel_T::ExtrapolationRefilling: + if (cell.x() == cell_idx_c(1) && cell.y() == cell_idx_c(1) && cell.z() == cell_idx_c(0)) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 0), real_c(0.452777777777777), + real_c(1e-6)); // C, (0,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 1), real_c(0.141527777777778), + real_c(1e-6)); // N, (0,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 2), real_c(0.104861111111111), + real_c(1e-6)); // S, (0,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 3), real_c(0.128611111111111), + real_c(1e-6)); // W, (-1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 4), real_c(0.035277777777778), + real_c(1e-6)); // E, (1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 5), real_c(0.037986111111111), + real_c(1e-6)); // NW, (-1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 6), real_c(0.002152777777778), + real_c(1e-6)); // NE, (1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 7), real_c(0.083819444444445), + real_c(1e-6)); // SW, (-1,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 8), real_c(0.012986111111111), + real_c(1e-6)); // SE, (1,-1,0) + } + else + { + if (cell.x() == cell_idx_c(0) && cell.y() == cell_idx_c(2) && cell.z() == cell_idx_c(0)) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(0)), real_c(0.443506944444444), + real_c(1e-6)); // C, (0,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(1)), real_c(0.103629861111111), + real_c(1e-6)); // N, (0,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(2)), real_c(0.118629861111111), + real_c(1e-6)); // S, (0,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(3)), real_c(0.101326736111111), + real_c(1e-6)); // W, (-1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(4)), real_c(0.121326736111111), + real_c(1e-6)); // E, (1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(5)), real_c(0.023688715277778), + real_c(1e-6)); // NW, (-1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(6)), real_c(0.028351215277778), + real_c(1e-6)); // NE, (1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(7)), real_c(0.027101215277778), + real_c(1e-6)); // SW, (-1,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(8)), real_c(0.032438715277778), + real_c(1e-6)); // SE, (1,-1,0) + } + else + { + if (cell.x() == cell_idx_c(2) && cell.y() == cell_idx_c(0) && cell.z() == cell_idx_c(0)) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(0)), real_c(0.429444444444444), + real_c(1e-6)); // C, (0,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(1)), real_c(0.076527777777778), + real_c(1e-6)); // N, (0,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(2)), real_c(0.083194444444444), + real_c(1e-6)); // S, (0,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(3)), real_c(0.066111111111111), + real_c(1e-6)); // W, (-1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(4)), real_c(0.196111111111111), + real_c(1e-6)); // E, (1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(5)), real_c(0.011319444444444), + real_c(1e-6)); // NW, (-1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(6)), real_c(0.001319444444444), + real_c(1e-6)); // NE, (1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(7)), real_c(0.082986111111111), + real_c(1e-6)); // SW, (-1,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(8)), real_c(0.052986111111111), + real_c(1e-6)); // SE, (1,-1,0) + } + else { WALBERLA_ABORT("The cells that need refilling are not set correctly!"); } + } + } + break; + case RefillingModel_T::GradsMomentsRefilling: + if (cell.x() == cell_idx_c(1) && cell.y() == cell_idx_c(1) && cell.z() == cell_idx_c(0)) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 0), real_c(0.46353086419753), + real_c(1e-6)); // C, (0,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 1), real_c(0.110747530864197), + real_c(1e-6)); // N, (0,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 2), real_c(0.117414197530864), + real_c(1e-6)); // S, (0,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 3), real_c(0.09336975308642), + real_c(1e-6)); // W, (-1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 4), real_c(0.11336975308642), + real_c(1e-6)); // E, (1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 5), real_c(0.023522530864198), + real_c(1e-6)); // NW, (-1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 6), real_c(0.02559475308642), + real_c(1e-6)); // NE, (1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 7), real_c(0.022261419753087), + real_c(1e-6)); // SW, (-1,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 8), real_c(0.030189197530864), + real_c(1e-6)); // SE, (1,-1,0) + } + else + { + if (cell.x() == cell_idx_c(0) && cell.y() == cell_idx_c(2) && cell.z() == cell_idx_c(0)) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(0)), real_c(0.443506944444444), + real_c(1e-6)); // C, (0,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(1)), real_c(0.103629861111111), + real_c(1e-6)); // N, (0,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(2)), real_c(0.118629861111111), + real_c(1e-6)); // S, (0,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(3)), real_c(0.101326736111111), + real_c(1e-6)); // W, (-1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(4)), real_c(0.121326736111111), + real_c(1e-6)); // E, (1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(5)), real_c(0.022994270833333), + real_c(1e-6)); // NW, (-1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(6)), real_c(0.029045659722222), + real_c(1e-6)); // NE, (1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(7)), real_c(0.027795659722222), + real_c(1e-6)); // SW, (-1,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(8)), real_c(0.031744270833333), + real_c(1e-6)); // SE, (1,-1,0) + } + else + { + if (cell.x() == cell_idx_c(2) && cell.y() == cell_idx_c(0) && cell.z() == cell_idx_c(0)) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(0)), real_c(0.454143004115226), + real_c(1e-6)); // C, (0,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(1)), real_c(0.104291306584362), + real_c(1e-6)); // N, (0,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(2)), real_c(0.117624639917695), + real_c(1e-6)); // S, (0,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(3)), real_c(0.092728497942387), + real_c(1e-6)); // W, (-1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(4)), real_c(0.124950720164609), + real_c(1e-6)); // E, (1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(5)), real_c(0.02389045781893), + real_c(1e-6)); // NW, (-1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(6)), real_c(0.025907124485597), + real_c(1e-6)); // NE, (1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(7)), real_c(0.021184902263375), + real_c(1e-6)); // SW, (-1,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(8)), real_c(0.035279346707819), + real_c(1e-6)); // SE, (1,-1,0) + } + else { WALBERLA_ABORT("The cells that need refilling are not set correctly!"); } + } + } + break; + default: + WALBERLA_ABORT("The specified PDF refilling model " << pdfRefillingModel.getModelName() + << " is not available.\nSomething went wrong!"); + } +} + +void verify_straightRefillingCells(const PdfRefillingModel& pdfRefillingModel, const Cell& cell, + const PdfField_T* const pdfField) +{ + switch (pdfRefillingModel.getModelType()) + { + case RefillingModel_T::EquilibriumRefilling: + if (cell.x() == cell_idx_c(1) && cell.y() == cell_idx_c(1) && cell.z() == cell_idx_c(0)) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(0)), real_c(0.444259259259259), + real_c(1e-6)); // C, (0,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(1)), real_c(0.107781481481481), + real_c(1e-6)); // N, (0,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(2)), real_c(0.114448148148148), + real_c(1e-6)); // S, (0,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(3)), real_c(0.106709259259259), + real_c(1e-6)); // W, (-1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(4)), real_c(0.115598148148148), + real_c(1e-6)); // E, (1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(5)), real_c(0.025889814814815), + real_c(1e-6)); // NW, (-1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(6)), real_c(0.02804537037037), + real_c(1e-6)); // NE, (1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(7)), real_c(0.027489814814815), + real_c(1e-6)); // SW, (-1,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(8)), real_c(0.029778703703704), + real_c(1e-6)); // SE, (1,-1,0) + } + else { WALBERLA_ABORT("The cells that need refilling are not set correctly!"); } + break; + case RefillingModel_T::AverageRefilling: + if (cell.x() == cell_idx_c(1) && cell.y() == cell_idx_c(1) && cell.z() == cell_idx_c(0)) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 0), real_c(0.442222222222222), + real_c(1e-6)); // C, (0,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 1), real_c(0.110555555555555), + real_c(1e-6)); // N, (0,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 2), real_c(0.110555555555555), + real_c(1e-6)); // S, (0,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 3), real_c(0.101111111111111), + real_c(1e-6)); // W, (-1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 4), real_c(0.113333333333333), + real_c(1e-6)); // E, (1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 5), real_c(0.025277777777778), + real_c(1e-6)); // NW, (-1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 6), real_c(0.030833333333333), + real_c(1e-6)); // NE, (1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 7), real_c(0.035277777777778), + real_c(1e-6)); // SW, (-1,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, 8), real_c(0.030833333333333), + real_c(1e-6)); // SE, (1,-1,0) + } + else { WALBERLA_ABORT("The cells that need refilling are not set correctly!"); } + break; + case RefillingModel_T::EquilibriumAndNonEquilibriumRefilling: + if (cell.x() == cell_idx_c(1) && cell.y() == cell_idx_c(1) && cell.z() == cell_idx_c(0)) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(0)), real_c(0.449659259259259), + real_c(1e-6)); // C, (0,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(1)), real_c(0.10168148148148), + real_c(1e-6)); // N, (0,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(2)), real_c(0.188348148148148), + real_c(1e-6)); // S, (0,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(3)), real_c(0.121459259259259), + real_c(1e-6)); // W, (-1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(4)), real_c(0.060348148148148), + real_c(1e-6)); // E, (1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(5)), real_c(0.027489814814815), + real_c(1e-6)); // NW, (-1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(6)), real_c(0.050095370370371), + real_c(1e-6)); // NE, (1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(7)), real_c(-0.025460185185185), + real_c(1e-6)); // SW, (-1,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(8)), real_c(0.026378703703704), + real_c(1e-6)); // SE, (1,-1,0) + } + else { WALBERLA_ABORT("The cells that need refilling are not set correctly!"); } + break; + case RefillingModel_T::ExtrapolationRefilling: + if (cell.x() == cell_idx_c(1) && cell.y() == cell_idx_c(1) && cell.z() == cell_idx_c(0)) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(0)), real_c(0.441111111111112), + real_c(1e-6)); // C, (0,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(1)), real_c(0.128194444444443), + real_c(1e-6)); // N, (0,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(2)), real_c(0.154861111111111), + real_c(1e-6)); // S, (0,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(3)), real_c(0.094861111111111), + real_c(1e-6)); // W, (-1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(4)), real_c(0.098194444444445), + real_c(1e-6)); // E, (1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(5)), real_c(0.025694444444445), + real_c(1e-6)); // NW, (-1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(6)), real_c(0.069027777777778), + real_c(1e-6)); // NE, (1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(7)), real_c(-0.037638888888889), + real_c(1e-6)); // SW, (-1,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(8)), real_c(0.025694444444444), + real_c(1e-6)); // SE, (1,-1,0) + } + else { WALBERLA_ABORT("The cells that need refilling are not set correctly!"); } + break; + case RefillingModel_T::GradsMomentsRefilling: + if (cell.x() == cell_idx_c(1) && cell.y() == cell_idx_c(1) && cell.z() == cell_idx_c(0)) + { + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(0)), real_c(0.465658436213991), + real_c(1e-6)); // C, (0,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(1)), real_c(0.113131275720165), + real_c(1e-6)); // N, (0,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(2)), real_c(0.119797942386831), + real_c(1e-6)); // S, (0,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(3)), real_c(0.096009670781893), + real_c(1e-6)); // W, (-1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(4)), real_c(0.104898559670782), + real_c(1e-6)); // E, (1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(5)), real_c(0.023677880658436), + real_c(1e-6)); // NW, (-1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(6)), real_c(0.024907510288066), + real_c(1e-6)); // NE, (1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(7)), real_c(0.02435195473251), + real_c(1e-6)); // SW, (-1,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(pdfField->get(cell, cell_idx_c(8)), real_c(0.027566769547325), + real_c(1e-6)); // SE, (1,-1,0) + } + else { WALBERLA_ABORT("The cells that need refilling are not set correctly!"); } + break; + default: + WALBERLA_ABORT("The specified PDF refilling model " << pdfRefillingModel.getModelName() + << " is not available.\nSomething went wrong!"); + } +} + +void runSimulation(const PdfRefillingModel& pdfRefillingModel, const initCallback_T& fillInitializationSets, + const verifyCallback_T& verifyResults) +{ + // define the domain size (5x3x1) + const Vector3< uint_t > numBlocks(uint_c(1)); + const Vector3< uint_t > domainSize(uint_c(5), uint_c(3), uint_c(1)); + const Vector3< uint_t > cellsPerBlock(domainSize[0] / numBlocks[0], domainSize[1] / numBlocks[1], + domainSize[2] / numBlocks[2]); + + // create block grid + const std::shared_ptr< StructuredBlockForest > blockForest = + blockforest::createUniformBlockGrid(numBlocks[0], numBlocks[1], numBlocks[2], // blocks + cellsPerBlock[0], cellsPerBlock[1], cellsPerBlock[2], // cells + real_c(1.0), // dx + true, // one block per process + true, true, false); // periodicity + + // relaxation rate (used by GradsMomentsRefilling) + real_t omega = real_c(1.8); + + // create lattice model + LatticeModel_T latticeModel = LatticeModel_T(lbm::collision_model::SRT(omega)); + + // add PDF field (and a copy that stores the original PDF field) + const BlockDataID pdfFieldID = + lbm::addPdfFieldToStorage(blockForest, "PDF source field", latticeModel, uint_c(1), field::fzyx); + + // field to store PDFs before refilling, i.e., to test if non-refilling cells are modified + const BlockDataID pdfOrgFieldID = + lbm::addPdfFieldToStorage(blockForest, "PDF destination field", latticeModel, uint_c(1), field::fzyx); + + // add fill level field (MUST be initialized with 1, i.e., fluid everywhere for this test; otherwise the fluid + // flag is not detected below by initFlagsFromFillLevel()) + BlockDataID fillFieldID = + field::addToStorage< ScalarField_T >(blockForest, "Fill level field", real_c(1), field::fzyx, uint_c(1)); + + // define the initial properties of the cells ( gas, liquid, interface ) + std::set< Cell > gasCells; + std::set< Cell > interfaceCells; + std::set< Cell > liquidCells; + std::set< Cell > conversionCells; + fillInitializationSets(gasCells, interfaceCells, liquidCells, conversionCells); + + real_t initDensity = real_c(1.0); + + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + ScalarField_T* const fillField = blockIt->getData< ScalarField_T >(fillFieldID); + PdfField_T* const pdfField = blockIt->getData< PdfField_T >(pdfFieldID); + PdfField_T* pdfOrgField = blockIt->getData< PdfField_T >(pdfOrgFieldID); + + std::vector< std::pair< Cell, Vector3< real_t > > > velocities = { + { Cell{ cell_idx_c(0), cell_idx_c(0), cell_idx_c(0) }, + Vector3< real_t >{ real_c(0.05), real_c(-0.10), real_c(0.00) } }, + { Cell{ cell_idx_c(1), cell_idx_c(0), cell_idx_c(0) }, + Vector3< real_t >{ real_c(0.00), real_c(-0.05), real_c(0.00) } }, + { Cell{ cell_idx_c(2), cell_idx_c(0), cell_idx_c(0) }, + Vector3< real_t >{ real_c(0.00), real_c(0.00), real_c(0.00) } }, + { Cell{ cell_idx_c(3), cell_idx_c(0), cell_idx_c(0) }, + Vector3< real_t >{ real_c(0.05), real_c(-0.10), real_c(0.00) } }, + { Cell{ cell_idx_c(4), cell_idx_c(0), cell_idx_c(0) }, + Vector3< real_t >{ real_c(0.00), real_c(-0.05), real_c(0.00) } }, + { Cell{ cell_idx_c(0), cell_idx_c(1), cell_idx_c(0) }, + Vector3< real_t >{ real_c(0.10), real_c(-0.05), real_c(0.00) } }, + { Cell{ cell_idx_c(1), cell_idx_c(1), cell_idx_c(0) }, + Vector3< real_t >{ real_c(0.05), real_c(-0.10), real_c(0.00) } }, + { Cell{ cell_idx_c(2), cell_idx_c(1), cell_idx_c(0) }, + Vector3< real_t >{ real_c(0.10), real_c(0.00), real_c(0.00) } }, + { Cell{ cell_idx_c(3), cell_idx_c(1), cell_idx_c(0) }, + Vector3< real_t >{ real_c(0.10), real_c(-0.05), real_c(0.00) } }, + { Cell{ cell_idx_c(4), cell_idx_c(1), cell_idx_c(0) }, + Vector3< real_t >{ real_c(0.05), real_c(-0.10), real_c(0.00) } }, + { Cell{ cell_idx_c(0), cell_idx_c(2), cell_idx_c(0) }, + Vector3< real_t >{ real_c(0.05), real_c(0.00), real_c(0.00) } }, + { Cell{ cell_idx_c(1), cell_idx_c(2), cell_idx_c(0) }, + Vector3< real_t >{ real_c(0.05), real_c(0.00), real_c(0.00) } }, + { Cell{ cell_idx_c(2), cell_idx_c(2), cell_idx_c(0) }, + Vector3< real_t >{ real_c(0.00), real_c(0.00), real_c(0.00) } }, + { Cell{ cell_idx_c(3), cell_idx_c(2), cell_idx_c(0) }, + Vector3< real_t >{ real_c(0.05), real_c(0.00), real_c(0.00) } }, + { Cell{ cell_idx_c(4), cell_idx_c(2), cell_idx_c(0) }, + Vector3< real_t >{ real_c(0.05), real_c(0.00), real_c(0.00) } } + }; + + for (auto velocity = velocities.begin(); velocity != velocities.end(); velocity++) + { + pdfField->setDensityAndVelocity(velocity->first, velocity->second, initDensity); + pdfOrgField->setDensityAndVelocity(velocity->first, velocity->second, initDensity); + } + + // modify the PDFs to achieve inequality for the equalAndNonEqualRefilling + for (auto d = Stencil_T::begin(); d != Stencil_T::end(); ++d) + { + using D = stencil::Direction; + D dir = d.direction(); + Cell cell1(cell_idx_c(2), cell_idx_c(2), cell_idx_c(0)); + Cell cell2(cell_idx_c(3), cell_idx_c(0), cell_idx_c(0)); + Cell cell3(cell_idx_c(4), cell_idx_c(1), cell_idx_c(0)); + Cell cell4(cell_idx_c(3), cell_idx_c(1), cell_idx_c(0)); + Cell cell5(cell_idx_c(4), cell_idx_c(2), cell_idx_c(0)); + real_t PDFOffset(real_c(0.03)); + + if (dir == D::SW) + { + pdfField->get(cell1, dir) += PDFOffset; + pdfOrgField->get(cell1, dir) += PDFOffset; + pdfField->get(cell3, dir) += PDFOffset; + pdfOrgField->get(cell3, dir) += PDFOffset; + pdfField->get(cell4, dir) += PDFOffset; + pdfOrgField->get(cell4, dir) += PDFOffset; + } + if (dir == D::E) + { + pdfField->get(cell1, dir) -= PDFOffset; + pdfOrgField->get(cell1, dir) -= PDFOffset; + pdfField->get(cell3, dir) -= PDFOffset; + pdfOrgField->get(cell3, dir) -= PDFOffset; + pdfField->get(cell5, dir) -= PDFOffset; + pdfOrgField->get(cell5, dir) -= PDFOffset; + } + if (dir == D::S) + { + pdfField->get(cell2, dir) -= PDFOffset; + pdfOrgField->get(cell2, dir) -= PDFOffset; + pdfField->get(cell3, dir) -= PDFOffset; + pdfOrgField->get(cell3, dir) -= PDFOffset; + pdfField->get(cell4, dir) -= PDFOffset; + pdfOrgField->get(cell4, dir) -= PDFOffset; + } + if (dir == D::NE) + { + pdfField->get(cell2, dir) += PDFOffset; + pdfOrgField->get(cell2, dir) += PDFOffset; + pdfField->get(cell3, dir) += PDFOffset; + pdfOrgField->get(cell3, dir) += PDFOffset; + pdfField->get(cell5, dir) += PDFOffset; + pdfOrgField->get(cell5, dir) += PDFOffset; + } + } + + pdfOrgField = pdfField->clone(); + + // initialize fill level + WALBERLA_FOR_ALL_CELLS(fillFieldIt, fillField, { + if (gasCells.find(fillFieldIt.cell()) != gasCells.end()) { *fillFieldIt = real_c(0.0); } + else + { + if (interfaceCells.find(fillFieldIt.cell()) != interfaceCells.end()) { *fillFieldIt = real_c(0.5); } + else + { + if (liquidCells.find(fillFieldIt.cell()) != liquidCells.end()) { *fillFieldIt = real_c(1.0); } + } + } + }) // WALBERLA_FOR_ALL_CELLS + } + + // add boundary handling + const std::shared_ptr< FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling = + std::make_shared< FreeSurfaceBoundaryHandling_T >(blockForest, pdfFieldID, fillFieldID); + const BlockDataID flagFieldID = freeSurfaceBoundaryHandling->getFlagFieldID(); + const FlagInfo< FlagField_T > flagInfo = freeSurfaceBoundaryHandling->getFlagInfo(); + freeSurfaceBoundaryHandling->initFlagsFromFillLevel(); + + // create timeloop + uint_t timesteps = uint_c(1); + SweepTimeloop timeloop(blockForest, timesteps); + + Communication_T(blockForest, pdfFieldID, pdfOrgFieldID, flagFieldID)(); + + using geometryHandler = SurfaceGeometryHandler< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T >; + auto geometryHandlerPtr = std::make_shared< geometryHandler >(blockForest, freeSurfaceBoundaryHandling, fillFieldID, + "FiniteDifferenceMethod", false, false, real_c(0.0)); + + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + ScalarField_T* const fillField = blockIt->getData< ScalarField_T >(fillFieldID); + FlagField_T* const flagField = blockIt->getData< FlagField_T >(flagFieldID); + + // convert a cell from gas to interface + WALBERLA_FOR_ALL_CELLS(fillFieldIt, fillField, flagFieldIt, flagField, { + if (conversionCells.find(flagFieldIt.cell()) != conversionCells.end()) + { + // fill one cell entirely and create an interface cell from it; the fill level must be 1.1 to obtain the + // normal as used in the manual computations of the expected solutions + (*fillFieldIt) = real_c(1.1); + + // mark cell to be reinitialized + field::removeFlag(flagFieldIt, flagInfo.gasFlag); + field::addFlag(flagFieldIt, flagInfo.interfaceFlag); + field::addFlag(flagFieldIt, flagInfo.convertedFlag); + field::addFlag(flagFieldIt, flagInfo.convertFromGasToInterfaceFlag); + } + }) // WALBERLA_FOR_ALL_CELLS + + // perform refilling + switch (pdfRefillingModel.getModelType()) + { // the scope for each "case" is required since variables are defined within "case" + case PdfRefillingModel::RefillingModel::EquilibriumRefilling: { + EquilibriumRefillingSweep< LatticeModel_T, FlagField_T > equilibriumRefillingSweep(pdfFieldID, flagFieldID, + flagInfo, true); + equilibriumRefillingSweep(blockIt.get()); + break; + } + + case PdfRefillingModel::RefillingModel::AverageRefilling: { + AverageRefillingSweep< LatticeModel_T, FlagField_T > averageRefillingSweep(pdfFieldID, flagFieldID, flagInfo, + true); + averageRefillingSweep(blockIt.get()); + break; + } + + case PdfRefillingModel::RefillingModel::EquilibriumAndNonEquilibriumRefilling: { + EquilibriumAndNonEquilibriumRefillingSweep< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > + equilibriumAndNonEquilibriumRefillingSweep(pdfFieldID, flagFieldID, fillFieldID, flagInfo, uint_c(3), true); + equilibriumAndNonEquilibriumRefillingSweep(blockIt.get()); + break; + } + + case PdfRefillingModel::RefillingModel::ExtrapolationRefilling: { + ExtrapolationRefillingSweep< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > + extrapolationRefillingSweep(pdfFieldID, flagFieldID, fillFieldID, flagInfo, uint_c(3), true); + extrapolationRefillingSweep(blockIt.get()); + break; + } + + case PdfRefillingModel::RefillingModel::GradsMomentsRefilling: { + GradsMomentsRefillingSweep< LatticeModel_T, FlagField_T > gradsMomentsRefillingSweep(pdfFieldID, flagFieldID, + flagInfo, omega, true); + gradsMomentsRefillingSweep(blockIt.get()); + break; + } + default: + WALBERLA_ABORT("The specified pdf refilling model is not available."); + } + } + + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + const PdfField_T* const pdfField = blockIt->getData< const PdfField_T >(pdfFieldID); + const PdfField_T* const pdfOrgField = blockIt->getData< const PdfField_T >(pdfOrgFieldID); + const FlagField_T* const flagField = blockIt->getData< const FlagField_T >(flagFieldID); + const ScalarField_T* const fillField = blockIt->getData< const ScalarField_T >(fillFieldID); + + WALBERLA_FOR_ALL_CELLS(pdfFieldIt, pdfField, pdfFieldOrgIt, pdfOrgField, flagFieldIt, flagField, fillFieldIt, + fillField, { + if (conversionCells.find(flagFieldIt.cell()) == conversionCells.end()) + { + // check cells that were not converted if their PDFs changed + WALBERLA_CHECK_FLOAT_EQUAL(pdfFieldIt[0], pdfFieldOrgIt[0]); // C, (0,0,0) + WALBERLA_CHECK_FLOAT_EQUAL(pdfFieldIt[1], pdfFieldOrgIt[1]); // N, (0,1,0) + WALBERLA_CHECK_FLOAT_EQUAL(pdfFieldIt[2], pdfFieldOrgIt[2]); // S, (0,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL(pdfFieldIt[3], pdfFieldOrgIt[3]); // W, (-1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL(pdfFieldIt[4], pdfFieldOrgIt[4]); // E, (1,0,0) + WALBERLA_CHECK_FLOAT_EQUAL(pdfFieldIt[5], pdfFieldOrgIt[5]); // NW, (-1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL(pdfFieldIt[6], pdfFieldOrgIt[6]); // NE, (1,1,0) + WALBERLA_CHECK_FLOAT_EQUAL(pdfFieldIt[7], pdfFieldOrgIt[7]); // SW, (-1,-1,0) + WALBERLA_CHECK_FLOAT_EQUAL(pdfFieldIt[8], pdfFieldOrgIt[8]); // SE, (1,-1,0) + } + else + { + // check cells converted from gas to interface + verifyResults(pdfRefillingModel, pdfFieldIt.cell(), pdfField); + } + }) // WALBERLA_FOR_ALL_CELLS + } + + MPIManager::instance()->resetMPI(); +} + +} // namespace PdfRefillingTest +} // namespace free_surface +} // namespace walberla + +int main(int argc, char** argv) { return walberla::free_surface::PdfRefillingTest::main(argc, argv); } \ No newline at end of file diff --git a/tests/lbm/free_surface/dynamics/PdfRefillingTest.ods b/tests/lbm/free_surface/dynamics/PdfRefillingTest.ods new file mode 100644 index 0000000000000000000000000000000000000000..90899e2de1bc6574ddbf680509e901c84f202018 GIT binary patch literal 37960 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 literal 0 HcmV?d00001 diff --git a/tests/lbm/free_surface/dynamics/WettingConversionTest.cpp b/tests/lbm/free_surface/dynamics/WettingConversionTest.cpp new file mode 100644 index 000000000..8a2590c69 --- /dev/null +++ b/tests/lbm/free_surface/dynamics/WettingConversionTest.cpp @@ -0,0 +1,247 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file WettingConversionTest.cpp +//! \ingroup lbm/free_surface/dynamics +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Test cell conversion initiated by wetting. +//! +//! Initialize drop as a cylinder section near a solid wall. Run a free surface LBM simulation with local triangulation +//! and wetting, and evaluate the converted interface cells for correctness. +//====================================================================================================================== + +#include "blockforest/Initialization.h" + +#include "core/Environment.h" +#include "core/debug/TestSubsystem.h" + +#include "lbm/field/Adaptors.h" +#include "lbm/field/AddToStorage.h" +#include "lbm/free_surface/bubble_model/Geometry.h" +#include "lbm/free_surface/dynamics/SurfaceDynamicsHandler.h" +#include "lbm/free_surface/surface_geometry/SurfaceGeometryHandler.h" +#include "lbm/lattice_model/D3Q19.h" + +#include <algorithm> +#include <vector> +namespace walberla +{ +namespace free_surface +{ +namespace WettingConversionTest +{ +// define types +using flag_t = uint32_t; +using FlagField_T = FlagField< flag_t >; + +using ScalarField_T = GhostLayerField< real_t, 1 >; +using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >; + +template< typename LatticeModel_T > +std::vector< Cell > runSimulation(uint_t timesteps, real_t contactAngle) +{ + using Stencil_T = typename LatticeModel_T::Stencil; + using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >; + using Communication_T = blockforest::SimpleCommunication< typename LatticeModel_T::CommunicationStencil >; + + // define the domain size + const Vector3< uint_t > numBlocks(uint_c(1)); + const Vector3< uint_t > domainSize(uint_c(14), uint_c(7), uint_c(1)); + const Vector3< uint_t > cellsPerBlock(domainSize[0] / numBlocks[0], domainSize[1] / numBlocks[1], + domainSize[2] / numBlocks[2]); + + // create block grid + const std::shared_ptr< StructuredBlockForest > blockForest = + blockforest::createUniformBlockGrid(numBlocks[0], numBlocks[1], numBlocks[2], // blocks + cellsPerBlock[0], cellsPerBlock[1], cellsPerBlock[2], // cells + real_c(1.0), // dx + true, // one block per process + false, false, true); // periodicity + + real_t relaxRate = real_c(1.8); + + // create lattice model + LatticeModel_T latticeModel = LatticeModel_T(lbm::collision_model::SRT(relaxRate)); + + // add pdf field + const BlockDataID pdfFieldID = lbm::addPdfFieldToStorage(blockForest, "PDF field", latticeModel, field::fzyx); + + // add fill level field (initialized with 0, i.e., gas everywhere) + BlockDataID fillFieldID = + field::addToStorage< ScalarField_T >(blockForest, "Fill level field", real_c(0.0), field::fzyx, uint_c(1)); + + // add dummy force field + BlockDataID forceFieldID = field::addToStorage< VectorField_T >( + blockForest, "Force field", Vector3< real_t >(real_c(0)), field::fzyx, uint_c(1)); + + // add boundary handling + const std::shared_ptr< FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling = + std::make_shared< FreeSurfaceBoundaryHandling_T >(blockForest, pdfFieldID, fillFieldID); + const BlockDataID flagFieldID = freeSurfaceBoundaryHandling->getFlagFieldID(); + const typename FreeSurfaceBoundaryHandling_T::FlagInfo_T& flagInfo = freeSurfaceBoundaryHandling->getFlagInfo(); + + // add liquid drop (as cylinder section) + Vector3< real_t > midpoint1(real_c(14) * real_c(0.5), real_c(1), real_c(0)); + Vector3< real_t > midpoint2(real_c(14) * real_c(0.5), real_c(1), real_c(5)); + geometry::Cylinder cylinder(midpoint1, midpoint2, real_c(14) * real_c(0.2)); + bubble_model::addBodyToFillLevelField< geometry::Cylinder >(*blockForest, fillFieldID, cylinder, false); + + // initialize bottom (in y-direction) of domain as no-slip boundary + freeSurfaceBoundaryHandling->setNoSlipAtBorder(stencil::S, cell_idx_c(0)); + freeSurfaceBoundaryHandling->initFlagsFromFillLevel(); + + // initial communication + Communication_T(blockForest, pdfFieldID, fillFieldID, flagFieldID, forceFieldID)(); + + // add (dummy) bubble model + const bool disableSplits = true; // necessary if a gas bubble could split + auto bubbleModel = std::make_shared< bubble_model::BubbleModel< Stencil_T > >(blockForest, disableSplits); + bubbleModel->initFromFillLevelField(fillFieldID); + bubbleModel->setAtmosphere(Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(0)), real_c(1)); + + // create timeloop + SweepTimeloop timeloop(blockForest, timesteps); + + // add surface geometry handler + SurfaceGeometryHandler< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > geometryHandler( + blockForest, freeSurfaceBoundaryHandling, fillFieldID, "LocalTriangulation", true, true, contactAngle); + geometryHandler.addSweeps(timeloop); + + const ConstBlockDataID curvatureFieldID = geometryHandler.getConstCurvatureFieldID(); + const ConstBlockDataID normalFieldID = geometryHandler.getConstNormalFieldID(); + + // add surface dynamics handler + SurfaceDynamicsHandler< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > dynamicsHandler( + blockForest, pdfFieldID, flagFieldID, fillFieldID, forceFieldID, normalFieldID, curvatureFieldID, + freeSurfaceBoundaryHandling, bubbleModel, "NormalBasedKeepCenter", "EquilibriumRefilling", "EvenlyNewInterface", + relaxRate, Vector3< real_t >(real_c(0)), real_c(1e-2), false, false, real_c(1e-3), real_c(1e-1)); + dynamicsHandler.addSweeps(timeloop); + + timeloop.run(); + + // get interface cells after performing simulation + std::vector< Cell > interfaceCells; + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + const FlagField_T* const flagField = blockIt->getData< const FlagField_T >(flagFieldID); + + WALBERLA_FOR_ALL_CELLS_OMP(flagFieldIt, flagField, omp critical, { + if (flagInfo.isInterface(flagFieldIt)) { interfaceCells.emplace_back(flagFieldIt.cell()); } + }) // WALBERLA_FOR_ALL_CELLS_OMP + } + + return interfaceCells; +} + +template< typename LatticeModel_T > +void testWettingConversion() +{ + uint_t timesteps; + real_t contactAngle; + std::vector< Cell > expectedInterfaceCells; + std::vector< Cell > computedInterfaceCells; + bool vectorsEqual; + + // test different contact angles; expected results have been determined using a version of the code that was assumed + // to be correct + timesteps = uint_c(200); + contactAngle = real_c(120); + expectedInterfaceCells = std::vector< Cell >{ + Cell(cell_idx_c(4), cell_idx_c(1), cell_idx_c(0)), Cell(cell_idx_c(9), cell_idx_c(1), cell_idx_c(0)), + Cell(cell_idx_c(4), cell_idx_c(2), cell_idx_c(0)), Cell(cell_idx_c(9), cell_idx_c(2), cell_idx_c(0)), + Cell(cell_idx_c(4), cell_idx_c(3), cell_idx_c(0)), Cell(cell_idx_c(5), cell_idx_c(3), cell_idx_c(0)), + Cell(cell_idx_c(6), cell_idx_c(3), cell_idx_c(0)), Cell(cell_idx_c(7), cell_idx_c(3), cell_idx_c(0)), + Cell(cell_idx_c(8), cell_idx_c(3), cell_idx_c(0)), Cell(cell_idx_c(9), cell_idx_c(3), cell_idx_c(0)) + }; + WALBERLA_LOG_INFO("Testing interface conversion with wetting cells, contact angle=" << contactAngle << " degrees"); + computedInterfaceCells = runSimulation< LatticeModel_T >(timesteps, contactAngle); + vectorsEqual = std::is_permutation(computedInterfaceCells.begin(), computedInterfaceCells.end(), + expectedInterfaceCells.begin(), expectedInterfaceCells.end()); + if (!vectorsEqual) { WALBERLA_ABORT("Wrong cells converted."); } + MPIManager::instance()->resetMPI(); + + timesteps = uint_c(200); + contactAngle = real_c(80); + expectedInterfaceCells = std::vector< Cell >{ + Cell(cell_idx_c(3), cell_idx_c(1), cell_idx_c(0)), Cell(cell_idx_c(4), cell_idx_c(1), cell_idx_c(0)), + Cell(cell_idx_c(9), cell_idx_c(1), cell_idx_c(0)), Cell(cell_idx_c(10), cell_idx_c(1), cell_idx_c(0)), + Cell(cell_idx_c(4), cell_idx_c(2), cell_idx_c(0)), Cell(cell_idx_c(5), cell_idx_c(2), cell_idx_c(0)), + Cell(cell_idx_c(8), cell_idx_c(2), cell_idx_c(0)), Cell(cell_idx_c(9), cell_idx_c(2), cell_idx_c(0)), + Cell(cell_idx_c(5), cell_idx_c(3), cell_idx_c(0)), Cell(cell_idx_c(6), cell_idx_c(3), cell_idx_c(0)), + Cell(cell_idx_c(7), cell_idx_c(3), cell_idx_c(0)), Cell(cell_idx_c(8), cell_idx_c(3), cell_idx_c(0)) + }; + WALBERLA_LOG_INFO("Testing interface conversion with wetting cells, contact angle=" << contactAngle << " degrees"); + computedInterfaceCells = runSimulation< LatticeModel_T >(timesteps, contactAngle); + vectorsEqual = std::is_permutation(computedInterfaceCells.begin(), computedInterfaceCells.end(), + expectedInterfaceCells.begin(), expectedInterfaceCells.end()); + if (!vectorsEqual) { WALBERLA_ABORT("Wrong cells converted."); } + MPIManager::instance()->resetMPI(); + + timesteps = uint_c(200); + contactAngle = real_c(45); + expectedInterfaceCells = std::vector< Cell >{ + Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(0)), Cell(cell_idx_c(3), cell_idx_c(1), cell_idx_c(0)), + Cell(cell_idx_c(10), cell_idx_c(1), cell_idx_c(0)), Cell(cell_idx_c(11), cell_idx_c(1), cell_idx_c(0)), + Cell(cell_idx_c(3), cell_idx_c(2), cell_idx_c(0)), Cell(cell_idx_c(4), cell_idx_c(2), cell_idx_c(0)), + Cell(cell_idx_c(5), cell_idx_c(2), cell_idx_c(0)), Cell(cell_idx_c(6), cell_idx_c(2), cell_idx_c(0)), + Cell(cell_idx_c(7), cell_idx_c(2), cell_idx_c(0)), Cell(cell_idx_c(8), cell_idx_c(2), cell_idx_c(0)), + Cell(cell_idx_c(9), cell_idx_c(2), cell_idx_c(0)), Cell(cell_idx_c(10), cell_idx_c(2), cell_idx_c(0)) + }; + WALBERLA_LOG_INFO("Testing interface conversion with wetting cells, contact angle=" << contactAngle << " degrees"); + computedInterfaceCells = runSimulation< LatticeModel_T >(timesteps, contactAngle); + vectorsEqual = std::is_permutation(computedInterfaceCells.begin(), computedInterfaceCells.end(), + expectedInterfaceCells.begin(), expectedInterfaceCells.end()); + if (!vectorsEqual) { WALBERLA_ABORT("Wrong cells converted."); } + MPIManager::instance()->resetMPI(); + + timesteps = uint_c(500); + contactAngle = real_c(1); + expectedInterfaceCells = std::vector< Cell >{ + Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0)), Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(0)), + Cell(cell_idx_c(3), cell_idx_c(1), cell_idx_c(0)), Cell(cell_idx_c(10), cell_idx_c(1), cell_idx_c(0)), + Cell(cell_idx_c(11), cell_idx_c(1), cell_idx_c(0)), Cell(cell_idx_c(12), cell_idx_c(1), cell_idx_c(0)), + Cell(cell_idx_c(3), cell_idx_c(2), cell_idx_c(0)), Cell(cell_idx_c(4), cell_idx_c(2), cell_idx_c(0)), + Cell(cell_idx_c(5), cell_idx_c(2), cell_idx_c(0)), Cell(cell_idx_c(6), cell_idx_c(2), cell_idx_c(0)), + Cell(cell_idx_c(7), cell_idx_c(2), cell_idx_c(0)), Cell(cell_idx_c(8), cell_idx_c(2), cell_idx_c(0)), + Cell(cell_idx_c(9), cell_idx_c(2), cell_idx_c(0)), Cell(cell_idx_c(10), cell_idx_c(2), cell_idx_c(0)) + }; + WALBERLA_LOG_INFO("Testing interface conversion with wetting cells, contact angle=" << contactAngle << " degrees"); + computedInterfaceCells = runSimulation< LatticeModel_T >(timesteps, contactAngle); + vectorsEqual = std::is_permutation(computedInterfaceCells.begin(), computedInterfaceCells.end(), + expectedInterfaceCells.begin(), expectedInterfaceCells.end()); + if (!vectorsEqual) { WALBERLA_ABORT("Wrong cells converted."); } + + MPIManager::instance()->resetMPI(); +} + +int main(int argc, char** argv) +{ + debug::enterTestMode(); + Environment env(argc, argv); + + WALBERLA_LOG_INFO("Testing with D3Q19 stencil."); + testWettingConversion< lbm::D3Q19< lbm::collision_model::SRT, true, lbm::force_model::None, 2 > >(); + + WALBERLA_LOG_INFO("Testing with D3Q27 stencil."); + testWettingConversion< lbm::D3Q27< lbm::collision_model::SRT, true, lbm::force_model::None, 2 > >(); + + return EXIT_SUCCESS; +} + +} // namespace WettingConversionTest +} // namespace free_surface +} // namespace walberla + +int main(int argc, char** argv) { return walberla::free_surface::WettingConversionTest::main(argc, argv); } \ No newline at end of file diff --git a/tests/lbm/free_surface/surface_geometry/CellFluidVolumeTest.cpp b/tests/lbm/free_surface/surface_geometry/CellFluidVolumeTest.cpp new file mode 100644 index 000000000..24fcc259b --- /dev/null +++ b/tests/lbm/free_surface/surface_geometry/CellFluidVolumeTest.cpp @@ -0,0 +1,74 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file CellFluidVolumeTest.cpp +//! \ingroup lbm/free_surface/surface_geometry +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Calculate fluid volume within a cell and compare with results obtained with ParaView. +// +//====================================================================================================================== + +#include "core/Environment.h" +#include "core/debug/Debug.h" +#include "core/debug/TestSubsystem.h" + +#include "lbm/free_surface/surface_geometry/CurvatureSweep.h" + +namespace walberla +{ +namespace free_surface +{ +namespace CellFluidVolumeTest +{ +inline void test(const Vector3< real_t >& normal, real_t offset, real_t expectedVolume, real_t tolerance) +{ + real_t volume = computeCellFluidVolume(normal, offset); + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(expectedVolume, volume, tolerance); +} + +int main(int argc, char** argv) +{ + debug::enterTestMode(); + Environment env(argc, argv); + + // allowed deviation from the expected result + real_t tolerance = real_c(1e-3); + + // test case where simple formula is applied (Figure 2.12, p. 24 in dissertation of Thomas Pohl) + const Vector3< real_t > normalSimpleCase(real_c(0), real_c(0.5), real_c(0.866)); + const real_t offsetSimpleCase = real_c(-0.1); + const real_t expectedVolumeSimpleCase = real_c(0.3845); // obtained with ParaView + test(normalSimpleCase, offsetSimpleCase, expectedVolumeSimpleCase, tolerance); + + // test cases as in dissertation of Thomas Pohl, page 25 + const Vector3< real_t > normal(real_c(0.37), real_c(0.61), real_c(0.7)); + const std::vector< real_t > offsetList{ real_c(-0.52), real_c(-0.3), real_c(-0.17), real_c(0) }; + const std::vector< real_t > volumeList{ real_c(0.0347), real_c(0.1612), real_c(0.2887), + real_c(0.5) }; // obtained with ParaView + + WALBERLA_ASSERT_EQUAL(offsetList.size(), volumeList.size()); + + for (size_t i = 0; i != offsetList.size(); ++i) + { + test(normal, offsetList[i], volumeList[i], tolerance); + } + + return EXIT_SUCCESS; +} +} // namespace CellFluidVolumeTest +} // namespace free_surface +} // namespace walberla + +int main(int argc, char** argv) { return walberla::free_surface::CellFluidVolumeTest::main(argc, argv); } diff --git a/tests/lbm/free_surface/surface_geometry/CurvatureOfSineTest.cpp b/tests/lbm/free_surface/surface_geometry/CurvatureOfSineTest.cpp new file mode 100644 index 000000000..9220f8022 --- /dev/null +++ b/tests/lbm/free_surface/surface_geometry/CurvatureOfSineTest.cpp @@ -0,0 +1,545 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file CurvatureOfSineTest.cpp +//! \ingroup lbm/free_surface/surface_geometry +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Initialize sine profile and compare computed curvature with analytical curvature. +// +//====================================================================================================================== + +#include "blockforest/Initialization.h" + +#include "core/Environment.h" +#include "core/debug/TestSubsystem.h" +#include "core/logging/Logging.h" +#include "core/math/Constants.h" +#include "core/math/DistributedSample.h" + +#include "field/AddToStorage.h" + +#include "lbm/blockforest/communication/SimpleCommunication.h" +#include "lbm/field/AddToStorage.h" +#include "lbm/free_surface/boundary/FreeSurfaceBoundaryHandling.h" +#include "lbm/free_surface/surface_geometry/CurvatureSweep.h" +#include "lbm/free_surface/surface_geometry/ExtrapolateNormalsSweep.h" +#include "lbm/free_surface/surface_geometry/NormalSweep.h" +#include "lbm/free_surface/surface_geometry/SmoothingSweep.h" +#include "lbm/lattice_model/CollisionModel.h" +#include "lbm/lattice_model/D2Q9.h" +#include "lbm/lattice_model/D3Q19.h" +#include "lbm/lattice_model/D3Q27.h" + +#include "stencil/D2Q9.h" +#include "stencil/D3Q19.h" +#include "stencil/D3Q27.h" + +#include "timeloop/SweepTimeloop.h" + +#include <cmath> + +namespace walberla +{ +namespace free_surface +{ +namespace CurvatureOfSineTest +{ +// define types +using flag_t = uint32_t; + +// define fields +using ScalarField_T = GhostLayerField< real_t, 1 >; +using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >; +using FlagField_T = FlagField< flag_t >; + +// function describing the global sine profile +inline real_t function(real_t x, real_t amplitude, real_t offset, uint_t domainWidth) +{ + return amplitude * std::sin(x / real_c(domainWidth) * real_c(2) * math::pi) + offset; +} + +// derivative of the function that is describing the sine profile +inline real_t derivative(real_t x, real_t amplitude, uint_t domainWidth) +{ + const real_t domainWidthInv = real_c(1) / real_c(domainWidth); + return amplitude * std::cos(x * domainWidthInv * real_c(2) * math::pi) * real_c(2) * math::pi * domainWidthInv; +} + +// second derivative of the function that is describing the sine profile +inline real_t secondDerivative(real_t x, real_t amplitude, uint_t domainWidth) +{ + const real_t domainWidthInv = real_c(1) / real_c(domainWidth); + + // the minus has been neglected on purpose: due to the definition of the normal (pointing from liquid to gas), the + // curvature also has a different sign + return amplitude * std::sin(x * domainWidthInv * real_c(2) * math::pi) * real_c(4) * math::pi * math::pi * + domainWidthInv * domainWidthInv; +} + +// compute the analytical normal vector and evaluate the error of the computed normal (absolute error in angle) +template< typename Stencil_T > +class ComputeAnalyticalNormal +{ + public: + ComputeAnalyticalNormal(const std::weak_ptr< const StructuredBlockForest >& blockForest, BlockDataID& normalFieldID, + const ConstBlockDataID& flagFieldID, const field::FlagUID& interfaceFlagID, + const Vector3< uint_t >& domainSize, real_t amplitude) + : blockForest_(blockForest), normalFieldID_(normalFieldID), flagFieldID_(flagFieldID), + interfaceFlagID_(interfaceFlagID), domainSize_(domainSize), amplitude_(amplitude) + {} + + void operator()(IBlock* const block) + { + const auto blockForest = blockForest_.lock(); + WALBERLA_CHECK_NOT_NULLPTR(blockForest); + + // compute analytical normals + const FlagField_T* const flagField = block->getData< const FlagField_T >(flagFieldID_); + VectorField_T* const normalField = block->getData< VectorField_T >(normalFieldID_); + + const flag_t interfaceFlag = flagField->getFlag(interfaceFlagID_); + + // explicitly use either D2Q9 or D3Q27 here, as the geometry operations require (or are most accurate with) the + // full neighborhood; + using NeighborhoodStencil_T = + typename std::conditional< Stencil_T::D == uint_t(2), stencil::D2Q9, stencil::D3Q27 >::type; + + WALBERLA_FOR_ALL_CELLS(flagFieldIt, flagField, normalFieldIt, normalField, { + // only treat interface cells + if (!isFlagSet(flagFieldIt, interfaceFlag) && + !field::isFlagInNeighborhood< NeighborhoodStencil_T >(flagFieldIt, interfaceFlag)) + { + continue; + } + + Cell globalCell = flagFieldIt.cell(); + blockForest->transformBlockLocalToGlobalCell(globalCell, *block, flagFieldIt.cell()); + + // global x-location of this cell's center + const real_t globalXCenter = real_c(globalCell[0]) + real_c(0.5); + + // get normal vector (slope of the negative inverse derivative gives direction of the normal) + Vector3< real_t > normalVec = + Vector3< real_t >(real_c(1), -real_c(1) / derivative(globalXCenter, amplitude_, domainSize_[0]), real_c(0)); + normalVec = normalVec.getNormalized(); + + // mirror vectors that are pointing downwards, as normal vector is defined to point from fluid to gas in FSLBM + if (normalVec[1] < real_c(0)) { normalVec *= -real_c(1); } + + *normalFieldIt = normalVec; + }) // WALBERLA_FOR_ALL_CELLS + } + + private: + std::weak_ptr< const StructuredBlockForest > blockForest_; + BlockDataID normalFieldID_; + ConstBlockDataID flagFieldID_; + + field::FlagUID interfaceFlagID_; + + Vector3< uint_t > domainSize_; + real_t amplitude_; +}; // class ComputeAnalyticalNormal + +// compute the analytical normal vector and evaluate the error of the computed normal (absolute error in angle) +template< typename Stencil_T > +class ComputeCurvatureError +{ + public: + ComputeCurvatureError(const std::weak_ptr< const StructuredBlockForest >& blockForest, + const ConstBlockDataID& curvatureFieldID, const ConstBlockDataID& flagFieldID, + const field::FlagUID& interfaceFlagID, const Vector3< uint_t >& domainSize, real_t amplitude, + const std::shared_ptr< real_t >& l2Error) + : blockForest_(blockForest), curvatureFieldID_(curvatureFieldID), flagFieldID_(flagFieldID), + interfaceFlagID_(interfaceFlagID), domainSize_(domainSize), amplitude_(amplitude), l2Error_(l2Error) + {} + + void operator()(IBlock* const block) + { + const auto blockForest = blockForest_.lock(); + WALBERLA_CHECK_NOT_NULLPTR(blockForest); + + // compute analytical normals and compare their directions with the computed normals + const FlagField_T* const flagField = block->getData< const FlagField_T >(flagFieldID_); + const ScalarField_T* const curvatureField = block->getData< const ScalarField_T >(curvatureFieldID_); + + const flag_t interfaceFlag = flagField->getFlag(interfaceFlagID_); + + real_t curvDiffSum2 = real_c(0); + real_t analCurvSum2 = real_c(0); + + WALBERLA_FOR_ALL_CELLS_OMP(flagFieldIt, flagField, curvatureFieldIt, curvatureField, + omp parallel for schedule(static) reduction(+:curvDiffSum2) reduction(+:analCurvSum2), + { + // only treat interface cells + if (isFlagSet(flagFieldIt, interfaceFlag)) + { + Cell globalCell = flagFieldIt.cell(); + blockForest->transformBlockLocalToGlobalCell(globalCell, *block, flagFieldIt.cell()); + + // normalized global x-location of this cell's center + const real_t globalXCenter = real_c(globalCell[0]) + real_c(0.5); + + // get analytical curvature + const real_t analyticalCurvature = secondDerivative(globalXCenter, amplitude_, domainSize_[0]); + + // calculate the relative error in curvature (dx=1/domainSize[0] for converting from LBM to physical units) + const real_t curvDiff = *curvatureFieldIt - analyticalCurvature; + curvDiffSum2 += curvDiff * curvDiff; + analCurvSum2 += analyticalCurvature * analyticalCurvature; + } + }) // WALBERLA_FOR_ALL_CELLS_OMP + + mpi::allReduceInplace< real_t >(curvDiffSum2, mpi::SUM); + mpi::allReduceInplace< real_t >(analCurvSum2, mpi::SUM); + + *l2Error_ = std::pow(curvDiffSum2 / analCurvSum2, real_c(0.5)); + + WALBERLA_LOG_RESULT("Relative error in curvature according to L2 norm = " << *l2Error_); + } + + private: + std::weak_ptr< const StructuredBlockForest > blockForest_; + ConstBlockDataID curvatureFieldID_; + ConstBlockDataID flagFieldID_; + + field::FlagUID interfaceFlagID_; + + Vector3< uint_t > domainSize_; + real_t amplitude_; + + std::shared_ptr< real_t > l2Error_; +}; // class ComputeCurvatureError + +template< typename LatticeModel_T > +real_t test(uint_t domainWidth, real_t amplitude, real_t offset, uint_t fillLevelInitSamples, bool useTriangulation, + bool useAnalyticalNormal) +{ + // define types + using Stencil_T = typename LatticeModel_T::Stencil; + using Communication_T = blockforest::SimpleCommunication< typename LatticeModel_T::CommunicationStencil >; + using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >; + + // define the domain size + const Vector3< uint_t > numBlocks(uint_c(1)); + Vector3< uint_t > domainSize(domainWidth); + domainSize[2] = uint_c(1); + const Vector3< uint_t > cellsPerBlock(domainSize[0] / numBlocks[0], domainSize[1] / numBlocks[1], + domainSize[2] / numBlocks[2]); + + // create block grid + const std::shared_ptr< StructuredBlockForest > blockForest = + blockforest::createUniformBlockGrid(numBlocks[0], numBlocks[1], numBlocks[2], // blocks + cellsPerBlock[0], cellsPerBlock[1], cellsPerBlock[2], // cells + real_c(1.0), // dx + true, // one block per process + true, false, true); // periodicity + + // create lattice model with omega=1 + LatticeModel_T latticeModel(real_c(1.0)); + + // add fields + BlockDataID pdfFieldID = + lbm::addPdfFieldToStorage< LatticeModel_T >(blockForest, "PDF field", latticeModel, field::fzyx); + BlockDataID fillFieldID = + field::addToStorage< ScalarField_T >(blockForest, "Fill levels", real_c(1), field::fzyx, uint_c(1)); + BlockDataID normalFieldID = field::addToStorage< VectorField_T >( + blockForest, "Normals", Vector3< real_t >(real_c(0)), field::fzyx, uint_c(1)); + BlockDataID curvatureFieldID = + field::addToStorage< ScalarField_T >(blockForest, "Curvature", real_c(0), field::fzyx, uint_c(1)); + BlockDataID smoothFillFieldID = + field::addToStorage< ScalarField_T >(blockForest, "Smooth fill levels", real_c(1.0), field::fzyx, uint_c(1)); + + // obstacle normal field is only a dummy field here (wetting effects are not considered in this test) + BlockDataID dummyObstNormalFieldID = field::addToStorage< VectorField_T >( + blockForest, "Dummy obstacle normal field", Vector3< real_t >(real_c(0)), field::fzyx, uint_c(1)); + + // add boundary handling + const std::shared_ptr< FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling = + std::make_shared< FreeSurfaceBoundaryHandling_T >(blockForest, pdfFieldID, fillFieldID); + BlockDataID flagFieldID = freeSurfaceBoundaryHandling->getFlagFieldID(); + + // initialize sine profile such that there is exactly one period; every length is normalized with domainSize[0] + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + const uint_t numTotalPoints = fillLevelInitSamples * fillLevelInitSamples; + const real_t stepsize = real_c(1) / real_c(fillLevelInitSamples); + + ScalarField_T* const fillField = blockIt->getData< ScalarField_T >(fillFieldID); + + WALBERLA_FOR_ALL_CELLS(fillFieldIt, fillField, { + // cell in block-local coordinates + const Cell localCell = fillFieldIt.cell(); + + // get cell in global coordinates + Cell globalCell = fillFieldIt.cell(); + blockForest->transformBlockLocalToGlobalCell(globalCell, *blockIt, localCell); + + // Monte-Carlo like estimation of the fill level: + // create uniformly-distributed sample points in each cell and count the number of points below the sine + // profile; this fraction of points is used as the fill level to initialize the profile + uint_t numPointsBelow = uint_c(0); + + for (uint_t xSample = uint_c(0); xSample < fillLevelInitSamples; ++xSample) + { + // value of the sine-function + const real_t functionValue = + function(real_c(globalCell[0]) + real_c(xSample) * stepsize, amplitude, offset, domainSize[0]); + + for (uint_t ySample = uint_c(0); ySample < fillLevelInitSamples; ++ySample) + { + const real_t yPoint = real_c(globalCell[1]) + real_c(ySample) * stepsize; + if (yPoint < functionValue) { ++numPointsBelow; } + } + } + + // fill level is fraction of points below sine profile + fillField->get(localCell) = real_c(numPointsBelow) / real_c(numTotalPoints); + }) // WALBERLA_FOR_ALL_CELLS + } + + // communicate fill level field to have meaningful values in ghost layer cells in periodic directions + Communication_T(blockForest, fillFieldID)(); + + // initialize fill level field + freeSurfaceBoundaryHandling->initFlagsFromFillLevel(); + + // communicate initialized flag field + Communication_T(blockForest, flagFieldID)(); + + // contact angle is only a dummy object here (wetting effects are not considered in this test) + ContactAngle dummyContactAngle(real_c(0)); + + // create timeloop + SweepTimeloop timeloop(blockForest, uint_c(50)); + + BlockDataID* relevantFillFieldID = &fillFieldID; + + if (useAnalyticalNormal) + { + // add sweep for getting analytical interface normal + ComputeAnalyticalNormal< Stencil_T > analyticalNormalSweep(blockForest, normalFieldID, flagFieldID, + flagIDs::interfaceFlagID, domainSize, amplitude); + timeloop.add() << Sweep(analyticalNormalSweep, "Analytical normal sweep") + << AfterFunction(Communication_T(blockForest, normalFieldID), + "Communication after analytical normal sweep"); + } + else + { + if (!useTriangulation) + { + smoothFillFieldID = field::addToStorage< ScalarField_T >(blockForest, "Smooth fill levels", real_c(1.0), + field::fzyx, uint_c(1)); + + // add sweep for smoothing the fill level field when not using local triangulation + SmoothingSweep< Stencil_T, FlagField_T, ScalarField_T, VectorField_T > smoothingSweep( + smoothFillFieldID, fillFieldID, flagFieldID, flagIDs::liquidInterfaceGasFlagIDs, + freeSurfaceBoundaryHandling->getFlagInfo().getObstacleIDSet(), false); + timeloop.add() << Sweep(smoothingSweep, "Smoothing sweep") + << AfterFunction(Communication_T(blockForest, smoothFillFieldID), + "Communication after smoothing sweep"); + + relevantFillFieldID = &smoothFillFieldID; + } + + NormalSweep< Stencil_T, FlagField_T, ScalarField_T, VectorField_T > normalsSweep( + normalFieldID, *relevantFillFieldID, flagFieldID, flagIDs::interfaceFlagID, flagIDs::liquidInterfaceGasFlagIDs, + freeSurfaceBoundaryHandling->getFlagInfo().getObstacleIDSet(), !useTriangulation, false, false, false); + timeloop.add() << Sweep(normalsSweep, "Normal sweep") + << AfterFunction(Communication_T(blockForest, normalFieldID), "Communication after normal sweep"); + } + + if (useTriangulation) // use local triangulation for curvature computation + { + CurvatureSweepLocalTriangulation< Stencil_T, FlagField_T, ScalarField_T, VectorField_T > curvatureSweep( + blockForest, curvatureFieldID, normalFieldID, fillFieldID, flagFieldID, dummyObstNormalFieldID, + flagIDs::interfaceFlagID, freeSurfaceBoundaryHandling->getFlagInfo().getObstacleIDSet(), false, + dummyContactAngle); + timeloop.add() << Sweep(curvatureSweep, "Curvature sweep"); + } + else // use finite differences for curvature computation + { + CurvatureSweepFiniteDifferences< Stencil_T, FlagField_T, ScalarField_T, VectorField_T > curvatureSweep( + curvatureFieldID, normalFieldID, dummyObstNormalFieldID, flagFieldID, flagIDs::interfaceFlagID, + flagIDs::liquidInterfaceGasFlagIDs, freeSurfaceBoundaryHandling->getFlagInfo().getObstacleIDSet(), false, + dummyContactAngle); + timeloop.add() << Sweep(curvatureSweep, "Curvature sweep"); + } + + // add sweep for computing the error (with respect to analytical solution) of the interface curvature + std::shared_ptr< real_t > l2Error = std::make_shared< real_t >(real_c(0)); + ComputeCurvatureError< Stencil_T > errorEvaluationSweep(blockForest, curvatureFieldID, flagFieldID, + flagIDs::interfaceFlagID, domainSize, amplitude, l2Error); + timeloop.add() << Sweep(errorEvaluationSweep, "Error evaluation sweep"); + + // perform a single time step + timeloop.singleStep(); + + MPIManager::instance()->resetMPI(); + + return *l2Error; +} + +template< typename LatticeModel_T > +void runAllTests() +{ + using Stencil_T = typename LatticeModel_T::Stencil; + + real_t l2Error; + + // used for initializing the fill level in a Monte-Carlo-like fashion; each cell is sampled by the specified value in + // x- and y-direction + const uint_t fillLevelInitSamples = uint_c(100); + + // test with various domain sizes, i.e., resolutions + for (uint_t domainWidth = uint_c(50); domainWidth <= uint_c(200); domainWidth += uint_c(50)) + { + const real_t amplitude = real_c(0.1) * real_c(domainWidth); // amplitude of the sine profile + const real_t offset = real_c(0.5) * real_c(domainWidth); // offset the sine profile in y-direction + + WALBERLA_LOG_RESULT("Domain width " << domainWidth << " cells; curvature with finite difference method;"); + l2Error = test< LatticeModel_T >(domainWidth, amplitude, offset, fillLevelInitSamples, false, false); + if constexpr (std::is_same_v< Stencil_T, stencil::D2Q9 > || std::is_same_v< Stencil_T, stencil::D3Q27 >) + { + WALBERLA_CHECK_LESS(l2Error, real_c(0.63)); + } + // computation is less accurate if corner directions are not included (as opposed to D2Q9/D3Q27) + if constexpr (std::is_same_v< Stencil_T, stencil::D3Q19 >) { WALBERLA_CHECK_LESS(l2Error, real_c(0.756)); } + + WALBERLA_LOG_RESULT("Domain width " + << domainWidth + << " cells; curvature with finite difference method; normal from analytical solution;"); + l2Error = test< LatticeModel_T >(domainWidth, amplitude, offset, fillLevelInitSamples, false, true); + if constexpr (std::is_same_v< Stencil_T, stencil::D2Q9 > || std::is_same_v< Stencil_T, stencil::D3Q27 >) + { + WALBERLA_CHECK_LESS(l2Error, real_c(0.52)); + } + // computation is less accurate if corner directions are not included (as opposed to D2Q9/D3Q27) + if constexpr (std::is_same_v< Stencil_T, stencil::D3Q19 >) { WALBERLA_CHECK_LESS(l2Error, real_c(0.58)); } + + if constexpr (!std::is_same_v< Stencil_T, stencil::D2Q9 >) + { + WALBERLA_LOG_RESULT("Domain width " << domainWidth << " cells; curvature with local triangulation;"); + l2Error = test< LatticeModel_T >(domainWidth, amplitude, offset, fillLevelInitSamples, true, false); + if constexpr (std::is_same_v< Stencil_T, stencil::D3Q27 >) { WALBERLA_CHECK_LESS(l2Error, real_c(0.79)); } + // computation is less accurate if corner directions are not included (as opposed to D2Q9/D3Q27) + if constexpr (std::is_same_v< Stencil_T, stencil::D3Q19 >) { WALBERLA_CHECK_LESS(l2Error, real_c(0.845)); } + + WALBERLA_LOG_RESULT("Domain width " + << domainWidth + << " cells; curvature with local triangulation; normal from analytical solution;"); + l2Error = test< LatticeModel_T >(domainWidth, amplitude, offset, fillLevelInitSamples, true, true); + WALBERLA_CHECK_LESS(l2Error, real_c(0.71)); + } + } + + // test with various amplitudes + for (real_t i = real_c(0.1); i <= real_c(0.3); i += real_c(0.05)) + { + const uint_t domainWidth = uint_c(100); // size of the domain in x- and y-direction + const real_t amplitude = i * real_c(domainWidth); // amplitude of the sine profile + const real_t offset = real_c(0.5) * real_c(domainWidth); // offset the sine profile in y-direction + + WALBERLA_LOG_RESULT("Amplitude " << amplitude << " cells; curvature with finite difference method;"); + l2Error = test< LatticeModel_T >(domainWidth, amplitude, offset, fillLevelInitSamples, false, false); + if constexpr (std::is_same_v< Stencil_T, stencil::D2Q9 > || std::is_same_v< Stencil_T, stencil::D3Q27 >) + { + WALBERLA_CHECK_LESS(l2Error, real_c(0.76)); + } + // computation is less accurate if corner directions are not included (as opposed to D2Q9/D3Q27) + if constexpr (std::is_same_v< Stencil_T, stencil::D3Q19 >) { WALBERLA_CHECK_LESS(l2Error, real_c(0.79)); } + + WALBERLA_LOG_RESULT( + "Amplitude " << amplitude + << " cells; curvature with finite difference method; normal from analytical solution;"); + l2Error = test< LatticeModel_T >(domainWidth, amplitude, offset, fillLevelInitSamples, false, true); + if constexpr (std::is_same_v< Stencil_T, stencil::D2Q9 > || std::is_same_v< Stencil_T, stencil::D3Q27 >) + { + WALBERLA_CHECK_LESS(l2Error, real_c(0.77)); + } + // computation is less accurate if corner directions are not included (as opposed to D2Q9/D3Q27) + if constexpr (std::is_same_v< Stencil_T, stencil::D3Q19 >) { WALBERLA_CHECK_LESS(l2Error, real_c(0.79)); } + + if constexpr (!std::is_same_v< Stencil_T, stencil::D2Q9 >) + { + WALBERLA_LOG_RESULT("Amplitude " << amplitude << " cells; curvature with local triangulation;"); + l2Error = test< LatticeModel_T >(domainWidth, amplitude, offset, fillLevelInitSamples, true, false); + WALBERLA_CHECK_LESS(l2Error, real_c(0.8)); + + WALBERLA_LOG_RESULT("Amplitude " + << amplitude + << " cells; curvature with local triangulation; normal from analytical solution;"); + l2Error = test< LatticeModel_T >(domainWidth, amplitude, offset, fillLevelInitSamples, true, true); + WALBERLA_CHECK_LESS(l2Error, real_c(0.8)); + } + } + + // test with various offsets + for (real_t i = real_c(0.4); i <= real_c(0.6); i += real_c(0.04)) + { + const uint_t domainWidth = uint_c(100); // size of the domain in x- and y-direction + const real_t amplitude = real_c(0.2) * real_c(domainWidth); // amplitude of the sine profile + const real_t offset = i * real_c(domainWidth); // offset the sine profile in y-direction + + WALBERLA_LOG_RESULT("Offset " << offset << " cells; curvature with finite difference method;"); + l2Error = test< LatticeModel_T >(domainWidth, amplitude, offset, fillLevelInitSamples, false, false); + WALBERLA_CHECK_LESS(l2Error, real_c(0.72)); + + WALBERLA_LOG_RESULT( + "Offset " << offset << " cells; curvature with finite difference method; normal from analytical solution;"); + l2Error = test< LatticeModel_T >(domainWidth, amplitude, offset, fillLevelInitSamples, false, true); + WALBERLA_CHECK_LESS(l2Error, real_c(0.73)); + + if constexpr (!std::is_same_v< Stencil_T, stencil::D2Q9 >) + { + WALBERLA_LOG_RESULT("Offset " << offset << " cells; curvature with local triangulation;"); + l2Error = test< LatticeModel_T >(domainWidth, amplitude, offset, fillLevelInitSamples, true, false); + WALBERLA_CHECK_LESS(l2Error, real_c(0.699)); + + WALBERLA_LOG_RESULT( + "Offset " << offset << " cells; curvature with local triangulation; normal from analytical solution;"); + l2Error = test< LatticeModel_T >(domainWidth, amplitude, offset, fillLevelInitSamples, true, true); + WALBERLA_CHECK_LESS(l2Error, real_c(0.706)); + } + } +} + +int main(int argc, char** argv) +{ + debug::enterTestMode(); + Environment env(argc, argv); + + WALBERLA_LOG_RESULT("##################################"); + WALBERLA_LOG_RESULT("### Testing with D2Q9 stencil. ###"); + WALBERLA_LOG_RESULT("##################################"); + runAllTests< lbm::D2Q9< lbm::collision_model::SRT > >(); + + WALBERLA_LOG_RESULT("###################################"); + WALBERLA_LOG_RESULT("### Testing with D3Q19 stencil. ###"); + WALBERLA_LOG_RESULT("###################################"); + runAllTests< lbm::D3Q19< lbm::collision_model::SRT > >(); + + WALBERLA_LOG_RESULT("###################################"); + WALBERLA_LOG_RESULT("### Testing with D3Q27 stencil. ###"); + WALBERLA_LOG_RESULT("###################################"); + runAllTests< lbm::D3Q27< lbm::collision_model::SRT > >(); + + return EXIT_SUCCESS; +} +} // namespace CurvatureOfSineTest +} // namespace free_surface +} // namespace walberla + +int main(int argc, char** argv) { return walberla::free_surface::CurvatureOfSineTest::main(argc, argv); } \ No newline at end of file diff --git a/tests/lbm/free_surface/surface_geometry/CurvatureOfSphereTest.cpp b/tests/lbm/free_surface/surface_geometry/CurvatureOfSphereTest.cpp new file mode 100644 index 000000000..1a42213e6 --- /dev/null +++ b/tests/lbm/free_surface/surface_geometry/CurvatureOfSphereTest.cpp @@ -0,0 +1,373 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file CurvatureOfSphereTest.cpp +//! \ingroup lbm/free_surface/surface_geometry +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Compare computed mean curvature of spherical gas bubbles with analytical curvature. +// +//====================================================================================================================== + +#include "blockforest/Initialization.h" + +#include "core/Environment.h" +#include "core/debug/TestSubsystem.h" +#include "core/logging/Logging.h" +#include "core/math/DistributedSample.h" + +#include "field/AddToStorage.h" + +#include "geometry/bodies/Sphere.h" + +#include "lbm/blockforest/communication/SimpleCommunication.h" +#include "lbm/field/AddToStorage.h" +#include "lbm/free_surface/boundary/FreeSurfaceBoundaryHandling.h" +#include "lbm/free_surface/surface_geometry/ContactAngle.h" +#include "lbm/free_surface/surface_geometry/CurvatureSweep.h" +#include "lbm/free_surface/surface_geometry/ExtrapolateNormalsSweep.h" +#include "lbm/free_surface/surface_geometry/NormalSweep.h" +#include "lbm/free_surface/surface_geometry/SmoothingSweep.h" +#include "lbm/lattice_model/CollisionModel.h" +#include "lbm/lattice_model/D3Q19.h" +#include "lbm/lattice_model/D3Q27.h" + +#include "timeloop/SweepTimeloop.h" + +namespace walberla +{ +namespace free_surface +{ +namespace CurvatureOfSphereTest +{ +// define types +using flag_t = uint32_t; + +// define fields +using ScalarField_T = GhostLayerField< real_t, 1 >; +using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >; +using FlagField_T = FlagField< flag_t >; + +template< typename Stencil_T > +class ComputeAnalyticalNormal +{ + public: + ComputeAnalyticalNormal(const std::weak_ptr< const StructuredBlockForest >& blockForest, + const BlockDataID& normalField, const ConstBlockDataID& flagField, + const field::FlagUID& interfaceFlag, const Vector3< real_t > sphereMidpoint) + : blockForest_(blockForest), normalFieldID_(normalField), flagFieldID_(flagField), + interfaceFlagID_(interfaceFlag), sphereMidpoint_(sphereMidpoint) + {} + + void operator()(IBlock* const block) + { + const auto blockForest = blockForest_.lock(); + WALBERLA_CHECK_NOT_NULLPTR(blockForest); + + // get fields + const FlagField_T* const flagField = block->getData< const FlagField_T >(flagFieldID_); + VectorField_T* const normalField = block->getData< VectorField_T >(normalFieldID_); + + flag_t interfaceFlag = flagField->getFlag(interfaceFlagID_); + + WALBERLA_FOR_ALL_CELLS(flagFieldIt, flagField, normalFieldIt, normalField, { + // evaluate normal only in interface cell (and possibly in an interface cell's neighborhood) + if (isFlagSet(flagFieldIt, interfaceFlag) || + field::isFlagInNeighborhood< Stencil_T >(flagFieldIt, interfaceFlag)) + { + // get a vector pointing from sphere's center to current cell's center + Vector3< real_t > r; + + // get the global coordinate of the current cell's center + blockForest->getBlockLocalCellCenter(*block, flagFieldIt.cell(), r); + r -= sphereMidpoint_; + + // invert normal direction: in this FSLBM implementation, the normal vector is defined to point from liquid + // to gas + r *= real_c(-1); + normalize(r); + + // store analytical normal + *normalFieldIt = r; + } + }) // WALBERLA_FOR_ALL_CELLS + } + + private: + std::weak_ptr< const StructuredBlockForest > blockForest_; + + BlockDataID normalFieldID_; + ConstBlockDataID flagFieldID_; + field::FlagUID interfaceFlagID_; + + Vector3< real_t > sphereMidpoint_; +}; // class ComputeAnalyticalNormal + +class ComputeCurvatureError +{ + public: + ComputeCurvatureError(const std::weak_ptr< const StructuredBlockForest >& blockForest, + const ConstBlockDataID& curvatureFieldID, const ConstBlockDataID& flagFieldID, + const field::FlagUID& interfaceFlagID, real_t sphereDiameter, + const Vector3< uint_t >& domainSize, const std::shared_ptr< real_t >& l2Error) + : blockForest_(blockForest), curvatureFieldID_(curvatureFieldID), flagFieldID_(flagFieldID), + interfaceFlagID_(interfaceFlagID), sphereDiameter_(sphereDiameter), domainSize_(domainSize), l2Error_(l2Error) + {} + + void operator()(IBlock* const block) + { + const auto blockForest = blockForest_.lock(); + WALBERLA_CHECK_NOT_NULLPTR(blockForest); + + // get fields + const ScalarField_T* const curvatureField = block->getData< const ScalarField_T >(curvatureFieldID_); + const FlagField_T* const flagField = block->getData< const FlagField_T >(flagFieldID_); + + const flag_t interfaceFlag = flagField->getFlag(interfaceFlagID_); + + const real_t analyticalCurvature = real_c(-1) / (real_c(0.5) * real_c(sphereDiameter_)); + + real_t curvDiffSum2 = real_c(0); + real_t analCurvSum2 = real_c(0); + + WALBERLA_FOR_ALL_CELLS_OMP(flagFieldIt, flagField, curvatureFieldIt, curvatureField, + omp parallel for schedule(static) reduction(+:curvDiffSum2) reduction(+:analCurvSum2), + { + // skip non-interface cells + if (isFlagSet(flagFieldIt, interfaceFlag)) + { + const real_t curvDiff = *curvatureFieldIt - analyticalCurvature; + curvDiffSum2 += curvDiff * curvDiff; + analCurvSum2 += analyticalCurvature * analyticalCurvature; + } + }) // WALBERLA_FOR_ALL_CELLS_OMP + + mpi::allReduceInplace< real_t >(curvDiffSum2, mpi::SUM); + mpi::allReduceInplace< real_t >(analCurvSum2, mpi::SUM); + + *l2Error_ = std::pow(curvDiffSum2 / analCurvSum2, real_c(0.5)); + + WALBERLA_LOG_RESULT("Relative error in curvature according to L2 norm = " << *l2Error_); + } + + private: + std::weak_ptr< const StructuredBlockForest > blockForest_; + + ConstBlockDataID curvatureFieldID_; + ConstBlockDataID flagFieldID_; + field::FlagUID interfaceFlagID_; + + real_t sphereDiameter_; + Vector3< uint_t > domainSize_; + + std::shared_ptr< real_t > l2Error_; +}; // class ComputeCurvatureError + +template< typename LatticeModel_T > +real_t test(uint_t sphereDiameter, Vector3< real_t > offset, bool useTriangulation, bool useAnalyticalNormal) +{ + // define types + using Stencil_T = typename LatticeModel_T::Stencil; + using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >; + using Communication_T = blockforest::SimpleCommunication< typename LatticeModel_T::CommunicationStencil >; + + // define the domain size + const Vector3< uint_t > numBlocks(uint_c(1)); + const Vector3< uint_t > domainSize(sphereDiameter + uint_c(6)); + const Vector3< uint_t > cellsPerBlock(domainSize[0] / numBlocks[0], domainSize[1] / numBlocks[1], + domainSize[2] / numBlocks[2]); + + // create block grid + const std::shared_ptr< StructuredBlockForest > blockForest = + blockforest::createUniformBlockGrid(numBlocks[0], numBlocks[1], numBlocks[2], // blocks + cellsPerBlock[0], cellsPerBlock[1], cellsPerBlock[2], // cells + real_c(1.0), // dx + true, // one block per process + false, false, false); // periodicity + + // create lattice model with omega=1 + LatticeModel_T latticeModel(real_c(1.0)); + + // add fields + BlockDataID pdfFieldID = + lbm::addPdfFieldToStorage< LatticeModel_T >(blockForest, "PDF field", latticeModel, field::fzyx); + BlockDataID fillFieldID = + field::addToStorage< ScalarField_T >(blockForest, "Fill levels", real_c(1.0), field::fzyx, uint_c(1)); + BlockDataID normalFieldID = field::addToStorage< VectorField_T >( + blockForest, "Normals", Vector3< real_t >(real_c(0)), field::fzyx, uint_c(1)); + BlockDataID curvatureFieldID = + field::addToStorage< ScalarField_T >(blockForest, "Curvature", real_c(0), field::fzyx, uint_c(1)); + BlockDataID smoothFillFieldID; + + // obstacle normal field is only a dummy field here (wetting effects are not considered in this test) + BlockDataID dummyObstNormalFieldID = field::addToStorage< VectorField_T >( + blockForest, "Dummy obstacle normal field", Vector3< real_t >(real_c(0)), field::fzyx, uint_c(1)); + + // add boundary handling + const std::shared_ptr< FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling = + std::make_shared< FreeSurfaceBoundaryHandling_T >(blockForest, pdfFieldID, fillFieldID); + BlockDataID flagFieldID = freeSurfaceBoundaryHandling->getFlagFieldID(); + + // add gas sphere, i.e., bubble + Vector3< real_t > midpoint(real_c(domainSize[0]) * real_c(0.5), real_c(domainSize[1]) * real_c(0.5), + real_c(domainSize[2]) * real_c(0.5)); + geometry::Sphere sphere(midpoint + offset, real_c(sphereDiameter) * real_c(0.5)); + freeSurfaceBoundaryHandling->addFreeSurfaceObject(sphere); + freeSurfaceBoundaryHandling->initFlagsFromFillLevel(); + + // contact angle is only a dummy object here (wetting effects are not considered in this test) + ContactAngle dummyContactAngle(real_c(0)); + + // create timeloop + SweepTimeloop timeloop(blockForest, uint_c(1)); + + BlockDataID* relevantFillFieldID = &fillFieldID; + + if (useAnalyticalNormal) + { + // add sweep for getting analytical interface normal + ComputeAnalyticalNormal< Stencil_T > analyticalNormalSweep(blockForest, normalFieldID, flagFieldID, + flagIDs::interfaceFlagID, midpoint); + timeloop.add() << Sweep(analyticalNormalSweep, "Analytical normal sweep") + << AfterFunction(Communication_T(blockForest, normalFieldID), + "Communication after analytical normal sweep"); + } + else + { + if (!useTriangulation) + { + smoothFillFieldID = field::addToStorage< ScalarField_T >(blockForest, "Smooth fill levels", real_c(1.0), + field::fzyx, uint_c(1)); + + // add sweep for smoothing the fill level field when not using local triangulation + SmoothingSweep< Stencil_T, FlagField_T, ScalarField_T, VectorField_T > smoothingSweep( + smoothFillFieldID, fillFieldID, flagFieldID, flagIDs::liquidInterfaceGasFlagIDs, + freeSurfaceBoundaryHandling->getFlagInfo().getObstacleIDSet(), false); + timeloop.add() << Sweep(smoothingSweep, "Smoothing sweep") + << AfterFunction(Communication_T(blockForest, smoothFillFieldID), + "Communication after smoothing sweep"); + + relevantFillFieldID = &smoothFillFieldID; + } + + NormalSweep< Stencil_T, FlagField_T, ScalarField_T, VectorField_T > normalsSweep( + normalFieldID, *relevantFillFieldID, flagFieldID, flagIDs::interfaceFlagID, flagIDs::liquidInterfaceGasFlagIDs, + freeSurfaceBoundaryHandling->getFlagInfo().getObstacleIDSet(), true, false, false, false); + timeloop.add() << Sweep(normalsSweep, "Normal sweep") + << AfterFunction(Communication_T(blockForest, normalFieldID), "Communication after normal sweep"); + } + + if (useTriangulation) // use local triangulation for curvature computation + { + CurvatureSweepLocalTriangulation< Stencil_T, FlagField_T, ScalarField_T, VectorField_T > curvatureSweep( + blockForest, curvatureFieldID, normalFieldID, fillFieldID, flagFieldID, dummyObstNormalFieldID, + flagIDs::interfaceFlagID, freeSurfaceBoundaryHandling->getFlagInfo().getObstacleIDSet(), false, + dummyContactAngle); + timeloop.add() << Sweep(curvatureSweep, "Curvature sweep"); + } + else // use finite differences for curvature computation + { + CurvatureSweepFiniteDifferences< Stencil_T, FlagField_T, ScalarField_T, VectorField_T > curvatureSweep( + curvatureFieldID, normalFieldID, dummyObstNormalFieldID, flagFieldID, flagIDs::interfaceFlagID, + flagIDs::liquidInterfaceGasFlagIDs, freeSurfaceBoundaryHandling->getFlagInfo().getObstacleIDSet(), false, + dummyContactAngle); + timeloop.add() << Sweep(curvatureSweep, "Curvature sweep"); + } + + const std::shared_ptr< real_t > l2Error = std::make_shared< real_t >(real_c(0)); + ComputeCurvatureError errorEvaluationSweep(blockForest, curvatureFieldID, flagFieldID, flagIDs::interfaceFlagID, + real_c(sphereDiameter), domainSize, l2Error); + timeloop.add() << Sweep(errorEvaluationSweep, "Error evaluation sweep"); + + // perform a single time step + timeloop.singleStep(); + + MPIManager::instance()->resetMPI(); + + return *l2Error; +} + +template< typename LatticeModel_T > +void runAllTests() +{ + real_t l2Error; + + // test with various bubble diameters + for (uint_t diameter = uint_c(10); diameter <= uint_c(50); diameter += uint_c(10)) + { + WALBERLA_LOG_RESULT("Bubble diameter " << diameter << " cells; curvature with finite difference method;") + l2Error = test< LatticeModel_T >(diameter, Vector3< real_t >(real_c(0), real_c(0), real_c(0)), false, false); + WALBERLA_CHECK_LESS(l2Error, real_c(0.2)); + + WALBERLA_LOG_RESULT("Bubble diameter " + << diameter + << " cells; curvature with finite difference method; normal from analytical solution;") + l2Error = test< LatticeModel_T >(diameter, Vector3< real_t >(real_c(0), real_c(0), real_c(0)), false, true); + WALBERLA_CHECK_LESS(l2Error, real_c(0.19)); + + WALBERLA_LOG_RESULT("Bubble diameter " << diameter << " cells; curvature with local triangulation;") + l2Error = test< LatticeModel_T >(diameter, Vector3< real_t >(real_c(0), real_c(0), real_c(0)), true, false); + WALBERLA_CHECK_LESS(l2Error, real_c(0.29)); + + WALBERLA_LOG_RESULT("Bubble diameter " + << diameter << " cells; curvature with local triangulation; normal from analytical solution;") + l2Error = test< LatticeModel_T >(diameter, Vector3< real_t >(real_c(0), real_c(0), real_c(0)), true, true); + WALBERLA_CHECK_LESS(l2Error, real_c(0.29)); + } + + // test with various offsets of sphere's center + for (real_t off = real_c(0.1); off < real_c(1.0); off += real_c(0.2)) + { + WALBERLA_LOG_RESULT("Sphere center offset " << off << " cells; curvature with finite difference method;") + l2Error = test< LatticeModel_T >(uint_c(20), Vector3< real_t >(off, off, real_c(0)), false, false); + WALBERLA_CHECK_LESS(l2Error, real_c(0.82)); + + WALBERLA_LOG_RESULT("Sphere center offset " + << off << " cells; curvature with finite difference method; normal from analytical solution;") + l2Error = test< LatticeModel_T >(uint_c(20), Vector3< real_t >(off, off, real_c(0)), false, true); + WALBERLA_CHECK_LESS(l2Error, real_c(0.83)); + + WALBERLA_LOG_RESULT("Sphere center offset " << off << " cells; curvature with local triangulation;") + l2Error = test< LatticeModel_T >(uint_c(20), Vector3< real_t >(off, off, real_c(0)), true, false); + WALBERLA_CHECK_LESS(l2Error, real_c(0.13)); + + WALBERLA_LOG_RESULT("Sphere center offset " + << off << " cells; curvature with local triangulation; normal from analytical solution;") + l2Error = test< LatticeModel_T >(uint_c(20), Vector3< real_t >(off, off, real_c(0)), true, true); + WALBERLA_CHECK_LESS(l2Error, real_c(0.16)); + } +} + +int main(int argc, char** argv) +{ + debug::enterTestMode(); + Environment env(argc, argv); + + WALBERLA_LOG_RESULT("###################################"); + WALBERLA_LOG_RESULT("### Testing with D3Q19 stencil. ###"); + WALBERLA_LOG_RESULT("###################################"); + runAllTests< lbm::D3Q19< lbm::collision_model::SRT > >(); + + WALBERLA_LOG_RESULT("###################################"); + WALBERLA_LOG_RESULT("### Testing with D3Q27 stencil. ###"); + WALBERLA_LOG_RESULT("###################################"); + runAllTests< lbm::D3Q27< lbm::collision_model::SRT > >(); + + return EXIT_SUCCESS; +} +} // namespace CurvatureOfSphereTest +} // namespace free_surface +} // namespace walberla + +int main(int argc, char** argv) { return walberla::free_surface::CurvatureOfSphereTest::main(argc, argv); } \ No newline at end of file diff --git a/tests/lbm/free_surface/surface_geometry/DetectWettingTest.cpp b/tests/lbm/free_surface/surface_geometry/DetectWettingTest.cpp new file mode 100644 index 000000000..4c86d76ad --- /dev/null +++ b/tests/lbm/free_surface/surface_geometry/DetectWettingTest.cpp @@ -0,0 +1,294 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file DetectWettingTest.cpp +//! \ingroup lbm/free_surface/surface_geometry +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Test the DetectWettingSweep using a single interface cell surrounded by gas cells and obstacle cells. +//! +//! 3x4x3 domain where the first and fourth layer in y-direction are solid cells. The cell at (1,1,1) is an interface +//! cell with given normal and fill level. All remaining cells are gas cells that might be marked for conversion to +//! wetting cells by DetectWettingSweep. +//====================================================================================================================== + +#include "blockforest/Initialization.h" + +#include "core/Environment.h" +#include "core/debug/TestSubsystem.h" + +#include "lbm/field/AddToStorage.h" +#include "lbm/free_surface/boundary/FreeSurfaceBoundaryHandling.h" +#include "lbm/free_surface/surface_geometry/DetectWettingSweep.h" +#include "lbm/lattice_model/D3Q19.h" + +#include "timeloop/SweepTimeloop.h" + +#include <algorithm> +#include <vector> + +namespace walberla +{ +namespace free_surface +{ +namespace DetectWettingTest +{ +using LatticeModel_T = lbm::D3Q19< lbm::collision_model::SRT >; +using PdfField_T = lbm::PdfField< LatticeModel_T >; +using Stencil_T = LatticeModel_T::Stencil; + +using ScalarField_T = GhostLayerField< real_t, 1 >; +using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >; + +using flag_t = uint32_t; +using FlagField_T = FlagField< flag_t >; +using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >; + +std::vector< Cell > detectWettingCells(const Vector3< real_t >& normal, const real_t fillLevel) +{ + // define the domain size + const Vector3< uint_t > numBlocks(uint_c(1)); + const Vector3< uint_t > domainSize(uint_c(3), uint_c(4), uint_c(3)); + const Vector3< uint_t > cellsPerBlock(domainSize[0] / numBlocks[0], domainSize[1] / numBlocks[1], + domainSize[2] / numBlocks[2]); + + // create block grid + const std::shared_ptr< StructuredBlockForest > blockForest = + blockforest::createUniformBlockGrid(numBlocks[0], numBlocks[1], numBlocks[2], // blocks + cellsPerBlock[0], cellsPerBlock[1], cellsPerBlock[2], // cells + real_c(1.0), // dx + true, // one block per process + false, false, false); // periodicity + + // create lattice model (dummy, not relevant for this test) + LatticeModel_T latticeModel = LatticeModel_T(lbm::collision_model::SRT(real_c(1))); + + // add fields + const BlockDataID pdfFieldID = lbm::addPdfFieldToStorage(blockForest, "PDF field", latticeModel, field::fzyx); + const BlockDataID fillFieldID = + field::addToStorage< ScalarField_T >(blockForest, "Fill level field", real_c(0.0), field::fzyx, uint_c(1)); + const BlockDataID normalFieldID = field::addToStorage< VectorField_T >( + blockForest, "Normal field", Vector3< real_t >(real_c(0.0)), field::fzyx, uint_c(1)); + + // add boundary handling + const std::shared_ptr< FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling = + std::make_shared< FreeSurfaceBoundaryHandling_T >(blockForest, pdfFieldID, fillFieldID); + const BlockDataID flagFieldID = freeSurfaceBoundaryHandling->getFlagFieldID(); + const BlockDataID boundaryHandlingID = freeSurfaceBoundaryHandling->getHandlingID(); + const typename FreeSurfaceBoundaryHandling_T::FlagInfo_T& flagInfo = freeSurfaceBoundaryHandling->getFlagInfo(); + + // initialize domain + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + ScalarField_T* const fillField = blockIt->getData< ScalarField_T >(fillFieldID); + VectorField_T* const normalField = blockIt->getData< VectorField_T >(normalFieldID); + FreeSurfaceBoundaryHandling_T::BoundaryHandling_T* const boundaryHandling = + blockIt->getData< FreeSurfaceBoundaryHandling_T::BoundaryHandling_T >(boundaryHandlingID); + + WALBERLA_FOR_ALL_CELLS_XYZ(fillField, { + // set boundary cells at bottom and top of domain + if (y == 0 || y == 3) { boundaryHandling->setBoundary(FreeSurfaceBoundaryHandling_T::noSlipFlagID, x, y, z); } + + // set interface cell in the center of the domain + if (x == 1 && y == 1 && z == 1) + { + boundaryHandling->setFlag(flagInfo.interfaceFlag, x, y, z); + fillField->get(x, y, z) = fillLevel; + normalField->get(x, y, z) = normal.getNormalized(); + } + + // set remaining domain to gas + if ((y == 1 || y == 2) && !(x == 1 && y == 1 && z == 1)) + { + boundaryHandling->setFlag(flagInfo.gasFlag, x, y, z); + } + }) // WALBERLA_FOR_ALL_CELLS_XYZ + } + + // create timeloop + SweepTimeloop timeloop(blockForest, 1); + + // add DetectWettingSweep + DetectWettingSweep< Stencil_T, FreeSurfaceBoundaryHandling_T::BoundaryHandling_T, FlagField_T, ScalarField_T, + VectorField_T > + detWetSweep(boundaryHandlingID, flagInfo, normalFieldID, fillFieldID); + timeloop.add() << Sweep(detWetSweep, "Detect wetting sweep"); + + timeloop.singleStep(); + + std::vector< Cell > markedCells; + + // get cells that were marked by DetectWettingSweep + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + const FlagField_T* const flagField = blockIt->getData< const FlagField_T >(flagFieldID); + + WALBERLA_FOR_ALL_CELLS_OMP( + flagFieldIt, flagField, omp critical, if (flagInfo.isKeepInterfaceForWetting(flagFieldIt)) { + markedCells.emplace_back(flagFieldIt.cell()); + }) // WALBERLA_FOR_ALL_CELLS_OMP + } + + return markedCells; +} + +int main(int argc, char** argv) +{ + debug::enterTestMode(); + Environment env(argc, argv); + + Vector3< real_t > normal; + real_t fillLevel; + std::vector< Cell > expectedWettingCells; + std::vector< Cell > computedWettingCells; + bool vectorsEqual; + + // test various different normals and fill levels; expected results have been determined using ParaView + normal = Vector3< real_t >(real_c(1), real_c(1), real_c(1)); + fillLevel = real_c(0.01); + expectedWettingCells = std::vector< Cell >{ Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(0)), + Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0)), + Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(1)) }; + WALBERLA_LOG_INFO("Testing wetting cells with normal=" << normal << " and fill level=" << fillLevel); + computedWettingCells = detectWettingCells(normal, fillLevel); + vectorsEqual = std::is_permutation(computedWettingCells.begin(), computedWettingCells.end(), + expectedWettingCells.begin(), expectedWettingCells.end()); + if (!vectorsEqual) { WALBERLA_ABORT("Wrong cells converted."); } + + normal = Vector3< real_t >(real_c(-1), real_c(-1), real_c(-1)); + fillLevel = real_c(0.01); + expectedWettingCells = std::vector< Cell >{ + Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(1)), Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(1)), + Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(1)), Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(2)), + Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(2)), Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(2)) + }; + WALBERLA_LOG_INFO("Testing wetting cells with normal=" << normal << " and fill level=" << fillLevel); + computedWettingCells = detectWettingCells(normal, fillLevel); + vectorsEqual = std::is_permutation(computedWettingCells.begin(), computedWettingCells.end(), + expectedWettingCells.begin(), expectedWettingCells.end()); + if (!vectorsEqual) { WALBERLA_ABORT("Wrong cells converted."); } + + normal = Vector3< real_t >(real_c(1), real_c(1), real_c(0)); + fillLevel = real_c(0.01); + expectedWettingCells = std::vector< Cell >{ Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(0)), + Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0)), + Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(1)), + Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(2)), + Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(2)) }; + WALBERLA_LOG_INFO("Testing wetting cells with normal=" << normal << " and fill level=" << fillLevel); + computedWettingCells = detectWettingCells(normal, fillLevel); + vectorsEqual = std::is_permutation(computedWettingCells.begin(), computedWettingCells.end(), + expectedWettingCells.begin(), expectedWettingCells.end()); + if (!vectorsEqual) { WALBERLA_ABORT("Wrong cells converted."); } + + normal = Vector3< real_t >(real_c(0), real_c(1), real_c(1)); + fillLevel = real_c(0.01); + expectedWettingCells = std::vector< Cell >{ Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(0)), + Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0)), + Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(0)), + Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(1)), + Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(1)) }; + WALBERLA_LOG_INFO("Testing wetting cells with normal=" << normal << " and fill level=" << fillLevel); + computedWettingCells = detectWettingCells(normal, fillLevel); + vectorsEqual = std::is_permutation(computedWettingCells.begin(), computedWettingCells.end(), + expectedWettingCells.begin(), expectedWettingCells.end()); + if (!vectorsEqual) { WALBERLA_ABORT("Wrong cells converted."); } + + normal = Vector3< real_t >(real_c(1), real_c(0), real_c(1)); + fillLevel = real_c(0.01); + expectedWettingCells = std::vector< Cell >{ Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0)), + Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(0)), + Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(1)), + Cell(cell_idx_c(0), cell_idx_c(2), cell_idx_c(1)), + Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(1)) }; + WALBERLA_LOG_INFO("Testing wetting cells with normal=" << normal << " and fill level=" << fillLevel); + computedWettingCells = detectWettingCells(normal, fillLevel); + vectorsEqual = std::is_permutation(computedWettingCells.begin(), computedWettingCells.end(), + expectedWettingCells.begin(), expectedWettingCells.end()); + if (!vectorsEqual) { WALBERLA_ABORT("Wrong cells converted."); } + + normal = Vector3< real_t >(real_c(-1), real_c(0), real_c(-1)); + fillLevel = real_c(0.01); + expectedWettingCells = std::vector< Cell >{ Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(1)), + Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(1)), + Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(1)), + Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(2)), + Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(2)) }; + WALBERLA_LOG_INFO("Testing wetting cells with normal=" << normal << " and fill level=" << fillLevel); + computedWettingCells = detectWettingCells(normal, fillLevel); + vectorsEqual = std::is_permutation(computedWettingCells.begin(), computedWettingCells.end(), + expectedWettingCells.begin(), expectedWettingCells.end()); + if (!vectorsEqual) { WALBERLA_ABORT("Wrong cells converted."); } + + normal = Vector3< real_t >(real_c(-1), real_c(0), real_c(-1)); + fillLevel = real_c(0.5); + expectedWettingCells = std::vector< Cell >{ Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0)), + Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(0)), + Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(1)), + Cell(cell_idx_c(0), cell_idx_c(2), cell_idx_c(1)), + Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(1)) }; + WALBERLA_LOG_INFO("Testing wetting cells with normal=" << normal << " and fill level=" << fillLevel); + computedWettingCells = detectWettingCells(normal, fillLevel); + vectorsEqual = std::is_permutation(computedWettingCells.begin(), computedWettingCells.end(), + expectedWettingCells.begin(), expectedWettingCells.end()); + if (!vectorsEqual) { WALBERLA_ABORT("Wrong cells converted."); } + + normal = Vector3< real_t >(real_c(1), real_c(1), real_c(1)); + fillLevel = real_c(0.9); + expectedWettingCells = std::vector< Cell >{ + Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(1)), Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(1)), + Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(1)), Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(2)), + Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(2)), Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(2)) + }; + WALBERLA_LOG_INFO("Testing wetting cells with normal=" << normal << " and fill level=" << fillLevel); + computedWettingCells = detectWettingCells(normal, fillLevel); + vectorsEqual = std::is_permutation(computedWettingCells.begin(), computedWettingCells.end(), + expectedWettingCells.begin(), expectedWettingCells.end()); + if (!vectorsEqual) { WALBERLA_ABORT("Wrong cells converted."); } + + normal = Vector3< real_t >(real_c(1), real_c(1), real_c(0)); + fillLevel = real_c(0.9); + expectedWettingCells = std::vector< Cell >{ + Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0)), Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(0)), + Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(0)), Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(1)), + Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(1)), Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(2)), + Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(2)), Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(2)) + }; + WALBERLA_LOG_INFO("Testing wetting cells with normal=" << normal << " and fill level=" << fillLevel); + computedWettingCells = detectWettingCells(normal, fillLevel); + vectorsEqual = std::is_permutation(computedWettingCells.begin(), computedWettingCells.end(), + expectedWettingCells.begin(), expectedWettingCells.end()); + if (!vectorsEqual) { WALBERLA_ABORT("Wrong cells converted."); } + + normal = Vector3< real_t >(real_c(0), real_c(1), real_c(0)); + fillLevel = real_c(0.9); + expectedWettingCells = std::vector< Cell >{ + Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(0)), Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0)), + Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(0)), Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(1)), + Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(1)), Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(2)), + Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(2)), Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(2)) + }; + WALBERLA_LOG_INFO("Testing wetting cells with normal=" << normal << " and fill level=" << fillLevel); + computedWettingCells = detectWettingCells(normal, fillLevel); + vectorsEqual = std::is_permutation(computedWettingCells.begin(), computedWettingCells.end(), + expectedWettingCells.begin(), expectedWettingCells.end()); + if (!vectorsEqual) { WALBERLA_ABORT("Wrong cells converted."); } + + return EXIT_SUCCESS; +} +} // namespace DetectWettingTest +} // namespace free_surface +} // namespace walberla + +int main(int argc, char** argv) { return walberla::free_surface::DetectWettingTest::main(argc, argv); } \ No newline at end of file diff --git a/tests/lbm/free_surface/surface_geometry/GetInterfacePointTest.cpp b/tests/lbm/free_surface/surface_geometry/GetInterfacePointTest.cpp new file mode 100644 index 000000000..7123e9ba7 --- /dev/null +++ b/tests/lbm/free_surface/surface_geometry/GetInterfacePointTest.cpp @@ -0,0 +1,76 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file GetInterfacePointTest.cpp +//! \ingroup lbm/free_surface/surface_geometry +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Compute the interface point from normal and fill level, and compare with results obtained with ParaView. +// +//====================================================================================================================== + +#include "core/Environment.h" +#include "core/debug/TestSubsystem.h" + +#include "lbm/free_surface/surface_geometry/CurvatureSweep.h" + +namespace walberla +{ +namespace free_surface +{ +namespace GetInterfacePointTest +{ +inline void test(const Vector3< real_t >& normal, real_t fillLevel, Vector3< real_t > expectedInterfacePoint, + real_t tolerance) +{ + const Vector3< real_t > interfacePoint = getInterfacePoint(normal, fillLevel); + + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(interfacePoint[0], expectedInterfacePoint[0], tolerance); + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(interfacePoint[1], expectedInterfacePoint[1], tolerance); + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(interfacePoint[2], expectedInterfacePoint[2], tolerance); +} + +int main(int argc, char** argv) +{ + debug::enterTestMode(); + Environment env(argc, argv); + + // allowed deviation from the expected result + real_t tolerance = real_c(1e-2); + + // tests identical to those in CellFluidVolumeTest.cpp, results obtained with ParaView + Vector3< real_t > normal(real_c(0), real_c(0.5), real_c(0.866)); + const real_t offset = real_c(-0.1); + const real_t fillLevel = real_c(0.3845); + Vector3< real_t > expectedInterfacePoint = Vector3< real_t >(real_c(0.5)) + offset * normal; + test(normal, fillLevel, expectedInterfacePoint, tolerance); + + normal = Vector3< real_t >(real_c(0.37), real_c(0.61), real_c(0.7)); + const std::vector< real_t > offsetList{ real_c(-0.52), real_c(-0.3), real_c(-0.17), real_c(0) }; + const std::vector< real_t > fillLevelList{ real_c(0.0347), real_c(0.1612), real_c(0.2887), real_c(0.5) }; + + WALBERLA_ASSERT_EQUAL(offsetList.size(), fillLevelList.size()); + for (size_t i = 0; i != offsetList.size(); ++i) + { + expectedInterfacePoint = Vector3< real_t >(real_c(0.5)) + offset * normal; + test(normal, fillLevel, expectedInterfacePoint, tolerance); + } + + return EXIT_SUCCESS; +} +} // namespace GetInterfacePointTest +} // namespace free_surface +} // namespace walberla + +int main(int argc, char** argv) { return walberla::free_surface::GetInterfacePointTest::main(argc, argv); } diff --git a/tests/lbm/free_surface/surface_geometry/NormalsEquivalenceTest.cpp b/tests/lbm/free_surface/surface_geometry/NormalsEquivalenceTest.cpp new file mode 100644 index 000000000..c7b5fbd3f --- /dev/null +++ b/tests/lbm/free_surface/surface_geometry/NormalsEquivalenceTest.cpp @@ -0,0 +1,213 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file NormalsEquivalenceTest.cpp +//! \ingroup lbm/free_surface/surface_geometry +//! \author Matthias Markl <matthias.markl@fau.de> +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Test if the NormalSweep (unrolled version) is equal to a loop-based computation of the interface normal. +//! +//! Test (with respect to a maximum error of 1e-13) if the explicit normal computation (implemented in NormalSweep) +//! gives the same result as the loop-based computation of the interface normal. The former method is faster and does +//! not loop over all D3Q27 directions but calculates the interface normal explicitly. See function computeNormal() in +//! src/lbm/free_surface/surface_geometry/NormalSweep.impl.h +// +//====================================================================================================================== + +#include "blockforest/Initialization.h" + +#include "core/Environment.h" +#include "core/debug/TestSubsystem.h" + +#include "field/AddToStorage.h" + +#include "lbm/blockforest/communication/SimpleCommunication.h" +#include "lbm/free_surface/FlagInfo.h" +#include "lbm/free_surface/surface_geometry/NormalSweep.h" + +#include "stencil/D3Q27.h" + +#include "timeloop/SweepTimeloop.h" + +#include <cstdlib> + +namespace walberla +{ +namespace free_surface +{ +namespace NormalsEquivalenceTest +{ +// define types +using Stencil_T = stencil::D3Q27; +using flag_t = uint32_t; + +// define fields +using ScalarField_T = GhostLayerField< real_t, 1 >; +using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >; +using FlagField_T = FlagField< flag_t >; + +int main(int argc, char** argv) +{ + debug::enterTestMode(); + Environment env(argc, argv); + + // define the domain size + const Vector3< uint_t > numBlocks(uint_c(1)); + const Vector3< uint_t > domainSize(uint_c(32), uint_c(32), uint_c(32)); + const Vector3< uint_t > cellsPerBlock(domainSize[0] / numBlocks[0], domainSize[1] / numBlocks[1], + domainSize[2] / numBlocks[2]); + + // create block grid + const std::shared_ptr< StructuredBlockForest > blockForest = + blockforest::createUniformBlockGrid(numBlocks[0], numBlocks[1], numBlocks[2], // blocks + cellsPerBlock[0], cellsPerBlock[1], cellsPerBlock[2], // cells + real_c(1.0), // dx + true, // one block per process + true, true, true); // periodicity + + // add fields + BlockDataID flagFieldID = field::addFlagFieldToStorage< FlagField_T >(blockForest, "Flags", uint_c(2)); + BlockDataID fillFieldID = + field::addToStorage< ScalarField_T >(blockForest, "Fill levels", real_c(0), field::fzyx, uint_c(1)); + BlockDataID normalFieldLoopID = field::addToStorage< VectorField_T >( + blockForest, "Normals loop", Vector3< real_t >(real_c(0)), field::fzyx, uint_c(1)); + BlockDataID normalFieldExplID = field::addToStorage< VectorField_T >( + blockForest, "Normals explicit", Vector3< real_t >(real_c(0)), field::fzyx, uint_c(1)); + + // add boundary handling + const auto flagInfo = FlagInfo< FlagField_T >(Set< FlagUID >(), Set< FlagUID >()); + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + flagInfo.registerFlags(blockIt->getData< FlagField_T >(flagFieldID)); + } + WALBERLA_ASSERT(flagInfo.isConsistentAcrossBlocksAndProcesses(blockForest, flagFieldID)); + + // add communication + blockforest::SimpleCommunication< stencil::D3Q27 > commFill(blockForest, fillFieldID); + blockforest::SimpleCommunication< stencil::D3Q27 > commFlag(blockForest, flagFieldID); + + // initialize flag field (only interface cells) and fill levels (random values) + std::srand(static_cast< unsigned int >(std::time(nullptr))); + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + ScalarField_T* const fillField = blockIt->getData< ScalarField_T >(fillFieldID); + FlagField_T* const flagField = blockIt->getData< FlagField_T >(flagFieldID); + + // set whole flag field to interface + flagField->set(flagInfo.interfaceFlag); + + // set random fill levels in fill field + WALBERLA_FOR_ALL_CELLS(fillFieldIt, fillField, { + *fillFieldIt = real_c(std::rand()) / real_c(std::numeric_limits< int >::max()); + }) // WALBERLA_FOR_ALL_CELLS + } + + // communicate fill level field, and flag field + commFill(); + commFlag(); + + // loop-based normal computation, i.e., old and slower version of NormalSweep operator() + auto normalsFunc = [&](IBlock* block) { + // get fields + VectorField_T* const normalField = block->getData< VectorField_T >(normalFieldLoopID); + const ScalarField_T* const fillField = block->getData< const ScalarField_T >(fillFieldID); + const FlagField_T* const flagField = block->getData< const FlagField_T >(flagFieldID); + + WALBERLA_FOR_ALL_CELLS(normalFieldIt, normalField, fillFieldIt, fillField, flagFieldIt, flagField, { + bool computeNormalInCell = flagInfo.isInterface(flagFieldIt); + + Vector3< real_t >& normal = *normalFieldIt; + + if (computeNormalInCell) + { + if (!isFlagInNeighborhood< stencil::D3Q27 >(flagFieldIt, flagInfo.obstacleFlagMask)) + { + normal.set(real_c(0), real_c(0), real_c(0)); + + // loop over all directions to compute interface normal; compare this loop with the explicit (non-loop) + // computation in function computeNormal() in src/lbm/free_surface/surface_geometry/NormalSweep.impl.h + for (auto d = stencil::D3Q27::beginNoCenter(); d != stencil::D3Q27::end(); ++d) + { + const real_t weightedFill = + real_c(stencil::gaussianMultipliers[d.toIdx()]) * fillFieldIt.neighbor(*d); + normal[0] += real_c(d.cx()) * weightedFill; + normal[1] += real_c(d.cy()) * weightedFill; + normal[2] += real_c(d.cz()) * weightedFill; + } + } + + // normalize and negate normal (to make it point from gas to liquid) + normal = real_c(-1) * normal.getNormalizedOrZero(); + } + else { normal.set(real_c(0), real_c(0), real_c(0)); } + }) // WALBERLA_FOR_ALL_CELLS + }; + + real_t err_max = real_c(0); + real_t err_mean = real_c(0); + +#ifdef WALBERLA_DOUBLE_ACCURACY + real_t tolerance = real_c(1e-13); +#else + real_t tolerance = real_c(1e-4); +#endif + + auto evaluateError = [&](IBlock* block) { + const VectorField_T* const loopField = block->getData< const VectorField_T >(normalFieldLoopID); + const VectorField_T* const explField = block->getData< const VectorField_T >(normalFieldExplID); + + err_mean = real_c(0); + err_max = real_c(0); + + WALBERLA_FOR_ALL_CELLS(loopFieldIt, loopField, explFieldIt, explField, { + const Vector3< real_t > diff = *loopFieldIt - *explFieldIt; + const real_t length = diff.length(); + + if (length > tolerance) + { + WALBERLA_ABORT("Unequal normals at " << loopFieldIt.cell() << " diff=" << diff << ", |diff|=" << length); + } + err_mean += length; + err_max = std::max(err_max, length); + }) // WALBERLA_FOR_ALL_CELLS + }; + + // create timeloop + SweepTimeloop timeloop(blockForest, uint_c(1)); + + // add regular sweep for computing interface normals + NormalSweep< Stencil_T, FlagField_T, ScalarField_T, VectorField_T > normalsSweep( + normalFieldExplID, fillFieldID, flagFieldID, flagIDs::interfaceFlagID, flagIDs::liquidInterfaceGasFlagIDs, + flagIDs::gasFlagID, false, false, false, false); + timeloop.add() << Sweep(normalsSweep, "Normal sweep (explicit)"); + + // add sweeps + timeloop.add() << Sweep(normalsFunc, "Normal loop-based"); + timeloop.add() << Sweep(evaluateError, "Error computation"); + + WcTimingPool timeloopTiming; + timeloop.run(timeloopTiming); + // timeloopTiming.logResultOnRoot(); + + WALBERLA_LOG_RESULT("Max Error: " << err_max); + WALBERLA_LOG_RESULT("Mean Error: " << err_mean / real_c(domainSize[0] * domainSize[1] * domainSize[2])); + + return EXIT_SUCCESS; +} +} // namespace NormalsEquivalenceTest +} // namespace free_surface +} // namespace walberla + +int main(int argc, char** argv) { return walberla::free_surface::NormalsEquivalenceTest::main(argc, argv); } diff --git a/tests/lbm/free_surface/surface_geometry/NormalsNearSolidTest.cpp b/tests/lbm/free_surface/surface_geometry/NormalsNearSolidTest.cpp new file mode 100644 index 000000000..8491557fe --- /dev/null +++ b/tests/lbm/free_surface/surface_geometry/NormalsNearSolidTest.cpp @@ -0,0 +1,178 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file NormalsNearSolidTest.cpp +//! \ingroup lbm/free_surface/surface_geometry +//! \author Martin Bauer +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Test interface normal computation as influenced by obstacle cells, i.e., test narrower Parker-Youngs scheme. +// +//! The setup is similar to Figure 6.11 in the dissertation of S. Donath, 2011. +//====================================================================================================================== + +#include "blockforest/Initialization.h" + +#include "core/Environment.h" +#include "core/debug/TestSubsystem.h" + +#include "field/AddToStorage.h" + +#include "lbm/blockforest/communication/SimpleCommunication.h" +#include "lbm/field/AddToStorage.h" +#include "lbm/field/PdfField.h" +#include "lbm/free_surface/boundary/FreeSurfaceBoundaryHandling.h" +#include "lbm/free_surface/surface_geometry/NormalSweep.h" +#include "lbm/lattice_model/CollisionModel.h" +#include "lbm/lattice_model/D3Q27.h" + +#include "timeloop/SweepTimeloop.h" + +namespace walberla +{ +namespace free_surface +{ +namespace NormalsNearSolidTest +{ +// define types +using LatticeModel_T = lbm::D3Q27< lbm::collision_model::SRT, true >; +using Stencil_T = LatticeModel_T::Stencil; +using flag_t = uint32_t; + +// define fields +using ScalarField_T = GhostLayerField< real_t, 1 >; +using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >; +using FlagField_T = FlagField< flag_t >; + +using Communication_T = blockforest::SimpleCommunication< LatticeModel_T::CommunicationStencil >; +using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >; + +int main(int argc, char** argv) +{ + debug::enterTestMode(); + Environment env(argc, argv); + + // define the domain size + const Vector3< uint_t > numBlocks(uint_c(1)); + const Vector3< uint_t > domainSize(uint_c(11), uint_c(3), uint_c(1)); + const Vector3< uint_t > cellsPerBlock(domainSize[0] / numBlocks[0], domainSize[1] / numBlocks[1], + domainSize[2] / numBlocks[2]); + + // create block grid + const std::shared_ptr< StructuredBlockForest > blockForest = + blockforest::createUniformBlockGrid(numBlocks[0], numBlocks[1], numBlocks[2], // blocks + cellsPerBlock[0], cellsPerBlock[1], cellsPerBlock[2], // cells + real_c(1.0), // dx + true, // one block per process + false, false, true); // periodicity + + // create lattice model with omega=1 + LatticeModel_T latticeModel(real_c(1.0)); + + // add fields + BlockDataID pdfFieldID = + lbm::addPdfFieldToStorage< LatticeModel_T >(blockForest, "PDF field", latticeModel, field::fzyx); + BlockDataID fillFieldID = + field::addToStorage< ScalarField_T >(blockForest, "Fill levels", real_c(1.0), field::fzyx, uint_c(1)); + BlockDataID normalFieldID = field::addToStorage< VectorField_T >( + blockForest, "Normals", Vector3< real_t >(real_c(0)), field::fzyx, uint_c(1)); + + // add boundary handling + const std::shared_ptr< FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling = + std::make_shared< FreeSurfaceBoundaryHandling_T >(blockForest, pdfFieldID, fillFieldID); + BlockDataID flagFieldID = freeSurfaceBoundaryHandling->getFlagFieldID(); + + // initialize domain as in Figure 6.11 in dissertation of S. Donath, 2011 + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + ScalarField_T* const fillField = blockIt->getData< ScalarField_T >(fillFieldID); + + WALBERLA_FOR_ALL_CELLS(fillFieldIt, fillField, { + if (fillFieldIt.y() == cell_idx_c(0)) { *fillFieldIt = real_c(1); } + if (fillFieldIt.y() == cell_idx_c(1)) { *fillFieldIt = real_c(fillFieldIt.x()) / real_c(25); } + if (fillFieldIt.y() == cell_idx_c(2)) { *fillFieldIt = real_c(0); } + }) // WALBERLA_FOR_ALL_CELLS + } + // set solid obstacle cells + freeSurfaceBoundaryHandling->setNoSlipAtBorder(stencil::E, cell_idx_c(0)); + freeSurfaceBoundaryHandling->setNoSlipAtBorder(stencil::W, cell_idx_c(0)); + freeSurfaceBoundaryHandling->setNoSlipInCell(Cell(cell_idx_c(9), cell_idx_c(2), cell_idx_c(0))); + + freeSurfaceBoundaryHandling->initFlagsFromFillLevel(); + + // initial communication (to initialize ghost layer in periodic z-direction) + Communication_T(blockForest, pdfFieldID, fillFieldID, flagFieldID)(); + + // create timeloop + SweepTimeloop timeloop(blockForest, uint_c(1)); + + // add sweep for computing interface normals + NormalSweep< Stencil_T, FlagField_T, ScalarField_T, VectorField_T > normalsSweep( + normalFieldID, fillFieldID, flagFieldID, flagIDs::interfaceFlagID, flagIDs::liquidInterfaceGasFlagIDs, + FreeSurfaceBoundaryHandling_T::noSlipFlagID, false, false, true, false); + timeloop.add() << Sweep(normalsSweep, "Normals sweep"); + + // perform a single time step + timeloop.singleStep(); + + // check correctness of computed interface normals; reference values have been obtained with a version of the code + // that is assumed to be correct; results are also qualitatively verified with Figure 6.11 in dissertation of S. + // Donath, 2011 + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + const FlagField_T* const flagField = blockIt->getData< const FlagField_T >(flagFieldID); + const VectorField_T* const normalField = blockIt->getData< const VectorField_T >(normalFieldID); + const flag_t interfaceFlag = flagField->getFlag(flagIDs::interfaceFlagID); + + WALBERLA_FOR_ALL_CELLS(flagFieldIt, flagField, normalFieldIt, normalField, { + if (isFlagSet(flagFieldIt, interfaceFlag)) + { + // regular Parker-Youngs normal computation + if (flagFieldIt.x() >= cell_idx_c(2) && flagFieldIt.x() <= cell_idx_c(7)) + { + WALBERLA_CHECK_FLOAT_EQUAL((*normalFieldIt)[0], real_c(-0.0399680383488715887), real_c(1e-6)); + WALBERLA_CHECK_FLOAT_EQUAL((*normalFieldIt)[1], real_c(0.999200958721789267), real_c(1e-6)); + WALBERLA_CHECK_FLOAT_EQUAL((*normalFieldIt)[2], real_c(0), real_c(1e-6)); + } + + // modified, i.e., narrower Parker-Youngs normal computation near solid boundaries + if (flagFieldIt.cell() == Cell(cell_idx_c(1), cell_idx_c(1), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL((*normalFieldIt)[0], real_c(-0.0199960011996001309), real_c(1e-6)); + WALBERLA_CHECK_FLOAT_EQUAL((*normalFieldIt)[1], real_c(0.999800059980007094), real_c(1e-6)); + WALBERLA_CHECK_FLOAT_EQUAL((*normalFieldIt)[2], real_c(0), real_c(1e-6)); + } + if (flagFieldIt.cell() == Cell(cell_idx_c(8), cell_idx_c(1), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL((*normalFieldIt)[0], real_c(-0.129339184067768065), real_c(1e-6)); + WALBERLA_CHECK_FLOAT_EQUAL((*normalFieldIt)[1], real_c(0.991600411186221775), real_c(1e-6)); + WALBERLA_CHECK_FLOAT_EQUAL((*normalFieldIt)[2], real_c(-2.99157e-17), real_c(1e-6)); + } + if (flagFieldIt.cell() == Cell(cell_idx_c(9), cell_idx_c(1), cell_idx_c(0))) + { + WALBERLA_CHECK_FLOAT_EQUAL((*normalFieldIt)[0], real_c(-0.0461047666084008420), real_c(1e-6)); + WALBERLA_CHECK_FLOAT_EQUAL((*normalFieldIt)[1], real_c(0.998936609848685486), real_c(1e-6)); + WALBERLA_CHECK_FLOAT_EQUAL((*normalFieldIt)[2], real_c(-8.5311e-17), real_c(1e-6)); + } + } + }) // WALBERLA_FOR_ALL_CELLS + } + + return EXIT_SUCCESS; +} +} // namespace NormalsNearSolidTest +} // namespace free_surface +} // namespace walberla + +int main(int argc, char** argv) { return walberla::free_surface::NormalsNearSolidTest::main(argc, argv); } \ No newline at end of file diff --git a/tests/lbm/free_surface/surface_geometry/NormalsOfSineTest.cpp b/tests/lbm/free_surface/surface_geometry/NormalsOfSineTest.cpp new file mode 100644 index 000000000..3ca66b6f5 --- /dev/null +++ b/tests/lbm/free_surface/surface_geometry/NormalsOfSineTest.cpp @@ -0,0 +1,470 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file NormalsOfSineTest.cpp +//! \ingroup lbm/free_surface/surface_geometry +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Initialize sine profile and compare calculated normals to analytical normals. +// +//====================================================================================================================== + +#include "blockforest/Initialization.h" + +#include "core/Environment.h" +#include "core/debug/TestSubsystem.h" +#include "core/logging/Logging.h" +#include "core/math/Constants.h" +#include "core/math/DistributedSample.h" + +#include "field/AddToStorage.h" + +#include "lbm/blockforest/communication/SimpleCommunication.h" +#include "lbm/field/AddToStorage.h" +#include "lbm/field/PdfField.h" +#include "lbm/free_surface/boundary/FreeSurfaceBoundaryHandling.h" +#include "lbm/free_surface/surface_geometry/NormalSweep.h" +#include "lbm/free_surface/surface_geometry/SmoothingSweep.h" +#include "lbm/lattice_model/CollisionModel.h" +#include "lbm/lattice_model/D2Q9.h" +#include "lbm/lattice_model/D3Q19.h" +#include "lbm/lattice_model/D3Q27.h" + +#include "stencil/D2Q9.h" +#include "stencil/D3Q19.h" +#include "stencil/D3Q27.h" + +#include "timeloop/SweepTimeloop.h" + +#include <cmath> + +namespace walberla +{ +namespace free_surface +{ +namespace NormalsOfSineTest +{ +// define types +using flag_t = uint32_t; + +// define fields +using ScalarField_T = GhostLayerField< real_t, 1 >; +using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >; +using FlagField_T = FlagField< flag_t >; + +// function describing the global sine profile +inline real_t function(real_t x, real_t amplitude, real_t offset, uint_t domainWidth) +{ + return amplitude * std::sin(x / real_c(domainWidth) * real_c(2) * math::pi) + offset; +} + +// derivative of the function that is describing the sine profile +inline real_t derivative(real_t x, real_t amplitude, uint_t domainWidth) +{ + const real_t domainWidthInv = real_c(1) / real_c(domainWidth); + return amplitude * std::cos(x * domainWidthInv * real_c(2) * math::pi) * real_c(2) * math::pi * domainWidthInv; +} + +// compute and evaluate the absolute error of the angle of the interface normal in each interface cell; +// reference: derivative of sine function, i.e., analytically computed normal +template< typename Stencil_T > +class EvaluateNormalError +{ + public: + EvaluateNormalError(const std::weak_ptr< const StructuredBlockForest >& blockForest, + const ConstBlockDataID& normalFieldID, const ConstBlockDataID& flagFieldID, + const field::FlagUID& interfaceFlagID, const Vector3< uint_t >& domainSize, real_t amplitude, + bool computeNormalsInInterfaceNeighbors, bool smoothFillLevel) + : blockForest_(blockForest), normalFieldID_(normalFieldID), flagFieldID_(flagFieldID), + interfaceFlagID_(interfaceFlagID), domainSize_(domainSize), amplitude_(amplitude), + computeNormalsInInterfaceNeighbors_(computeNormalsInInterfaceNeighbors), smoothFillLevel_(smoothFillLevel) + {} + + void operator()(IBlock* const block) + { + const auto blockForest = blockForest_.lock(); + WALBERLA_CHECK_NOT_NULLPTR(blockForest); + + // compute analytical normals and compare their directions with the computed normals + const FlagField_T* const flagField = block->getData< const FlagField_T >(flagFieldID_); + const VectorField_T* const normalField = block->getData< const VectorField_T >(normalFieldID_); + + const flag_t interfaceFlag = flagField->getFlag(interfaceFlagID_); + + math::DistributedSample errorSample; + + // avoid OpenMP parallelization as DistributedSample can not be reduced by OpenMP + WALBERLA_FOR_ALL_CELLS_OMP(flagFieldIt, flagField, normalFieldIt, normalField, omp critical, { + if (isFlagSet(flagFieldIt, interfaceFlag) || + (computeNormalsInInterfaceNeighbors_ && isFlagInNeighborhood< Stencil_T >(flagFieldIt, interfaceFlag))) + { + Cell globalCell = flagFieldIt.cell(); + blockForest->transformBlockLocalToGlobalCell(globalCell, *block, flagFieldIt.cell()); + + // global x-location of this cell's center + const real_t globalXCenter = real_c(globalCell[0]) + real_c(0.5); + + // get normal vector (slope of the negative inverse derivative gives direction of the normal) + Vector3< real_t > analyticalNormal = Vector3< real_t >( + real_c(1), -real_c(1) / derivative(globalXCenter, amplitude_, domainSize_[0]), real_c(0)); + analyticalNormal = analyticalNormal.getNormalized(); + + // mirror vectors that are pointing upwards, i.e., from gas to liquid; in this FSLBM implementation, the + // normal vector is defined to point from liquid to gas + if (analyticalNormal[1] < real_c(0)) { analyticalNormal *= -real_c(1); } + + // calculate the error in the angle of the interface normal + // the domain of arccosine is [-1,1]; due to numerical inaccuracies, the dot product + // "*normalFieldIt*analyticalNormal" might be slightly out of this range; these cases are ignored here + const real_t dotProduct = *normalFieldIt * analyticalNormal; + if (dotProduct >= real_c(-1) && dotProduct <= real_c(1)) + { + const real_t angleDiff = std::acos(dotProduct); + errorSample.insert(angleDiff); + } + } + }) // WALBERLA_FOR_ALL_CELLS_OMP + + errorSample.mpiAllGather(); + + const real_t maxError = errorSample.max(); + const real_t meanError = errorSample.mean(); + + WALBERLA_LOG_RESULT("Mean absolute error in angle of normal = " << meanError); + WALBERLA_LOG_RESULT("Maximum absolute error in angle of normal = " << maxError); + WALBERLA_LOG_RESULT("Minimum absolute error in angle of normal = " << errorSample.min()); + + // the following reference errors have been obtained with a version of the code that is believed to be correct + if constexpr (std::is_same_v< Stencil_T, stencil::D2Q9 > || std::is_same_v< Stencil_T, stencil::D3Q27 >) + { + if (computeNormalsInInterfaceNeighbors_ && smoothFillLevel_) + { + WALBERLA_CHECK_LESS(meanError, real_c(0.036)); + WALBERLA_CHECK_LESS(maxError, real_c(0.135)); + } + + if (!computeNormalsInInterfaceNeighbors_ && smoothFillLevel_) + { + WALBERLA_CHECK_LESS(meanError, real_c(0.019)); + WALBERLA_CHECK_LESS(maxError, real_c(0.044)); + } + + if (!computeNormalsInInterfaceNeighbors_ && !smoothFillLevel_) + { + WALBERLA_CHECK_LESS(meanError, real_c(0.027)); + WALBERLA_CHECK_LESS(maxError, real_c(0.07)); + } + + if (computeNormalsInInterfaceNeighbors_ && !smoothFillLevel_) + { + WALBERLA_CHECK_LESS(meanError, real_c(0.158)); + WALBERLA_CHECK_LESS(maxError, real_c(1.58)); + } + } + + // normal computation is less accurate if corner directions are not included (as opposed to D2Q9/D3Q27) + if constexpr (std::is_same_v< Stencil_T, stencil::D3Q19 >) + { + if (computeNormalsInInterfaceNeighbors_ && smoothFillLevel_) + { + WALBERLA_CHECK_LESS(meanError, real_c(0.036)); + WALBERLA_CHECK_LESS(maxError, real_c(0.135)); + } + + if (!computeNormalsInInterfaceNeighbors_ && smoothFillLevel_) + { + WALBERLA_CHECK_LESS(meanError, real_c(0.023)); + WALBERLA_CHECK_LESS(maxError, real_c(0.046)); + } + + if (!computeNormalsInInterfaceNeighbors_ && !smoothFillLevel_) + { + WALBERLA_CHECK_LESS(meanError, real_c(0.048)); + WALBERLA_CHECK_LESS(maxError, real_c(0.101)); + } + + if (computeNormalsInInterfaceNeighbors_ && !smoothFillLevel_) + { + WALBERLA_CHECK_LESS(meanError, real_c(0.158)); + WALBERLA_CHECK_LESS(maxError, real_c(1.58)); + } + } + } + + private: + std::weak_ptr< const StructuredBlockForest > blockForest_; + ConstBlockDataID normalFieldID_; + ConstBlockDataID flagFieldID_; + field::FlagUID interfaceFlagID_; + + Vector3< uint_t > domainSize_; + real_t amplitude_; + + bool computeNormalsInInterfaceNeighbors_; + bool smoothFillLevel_; +}; // class EvaluateNormalError + +template< typename LatticeModel_T > +void test(uint_t domainWidth, real_t amplitude, real_t offset, uint_t fillLevelInitSamples, + bool computeNormalsInInterfaceNeighbors, bool smoothFillLevel) +{ + // define types + using Stencil_T = typename LatticeModel_T::Stencil; + using Communication_T = blockforest::SimpleCommunication< typename LatticeModel_T::CommunicationStencil >; + using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >; + + // define the domain size + const Vector3< uint_t > numBlocks(uint_c(1)); + Vector3< uint_t > domainSize(domainWidth); + domainSize[2] = uint_c(1); + const Vector3< uint_t > cellsPerBlock(domainSize[0] / numBlocks[0], domainSize[1] / numBlocks[1], + domainSize[2] / numBlocks[2]); + + // create block grid + const std::shared_ptr< StructuredBlockForest > blockForest = + blockforest::createUniformBlockGrid(numBlocks[0], numBlocks[1], numBlocks[2], // blocks + cellsPerBlock[0], cellsPerBlock[1], cellsPerBlock[2], // cells + real_c(1.0), // dx + true, // one block per process + true, false, true); // periodicity + + // create (dummy) lattice model with omega=1 + LatticeModel_T latticeModel(real_c(1.0)); + + // add fields + BlockDataID pdfFieldID = + lbm::addPdfFieldToStorage< LatticeModel_T >(blockForest, "PDF field", latticeModel, field::fzyx); + BlockDataID fillFieldID = + field::addToStorage< ScalarField_T >(blockForest, "Fill levels", real_c(1.0), field::fzyx, uint_c(1)); + BlockDataID normalFieldID = field::addToStorage< VectorField_T >( + blockForest, "Normals", Vector3< real_t >(real_c(0)), field::fzyx, uint_c(1)); + BlockDataID smoothFillFieldID; + + // add boundary handling + const std::shared_ptr< FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling = + std::make_shared< FreeSurfaceBoundaryHandling_T >(blockForest, pdfFieldID, fillFieldID); + BlockDataID flagFieldID = freeSurfaceBoundaryHandling->getFlagFieldID(); + + // initialize sine profile such that there is exactly one period; every length is normalized with domainSize[0] + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + uint_t numTotalPoints = fillLevelInitSamples * fillLevelInitSamples; + const real_t stepsize = real_c(1) / real_c(fillLevelInitSamples); + + ScalarField_T* const fillField = blockIt->getData< ScalarField_T >(fillFieldID); + + WALBERLA_FOR_ALL_CELLS(fillFieldIt, fillField, { + // cell in block-local coordinates + const Cell localCell = fillFieldIt.cell(); + + // get cell in global coordinates + Cell globalCell = fillFieldIt.cell(); + blockForest->transformBlockLocalToGlobalCell(globalCell, *blockIt, localCell); + + // Monte-Carlo like estimation of the fill level: + // create uniformly-distributed sample points in each cell and count the number of points below the sine + // profile; this fraction of points is used as the fill level to initialize the profile + uint_t numPointsBelow = uint_c(0); + + for (uint_t xSample = uint_c(0); xSample < fillLevelInitSamples; ++xSample) + { + // value of the sine-function + const real_t functionValue = + function(real_c(globalCell[0]) + real_c(xSample) * stepsize, amplitude, offset, domainSize[0]); + + for (uint_t ySample = uint_c(0); ySample < fillLevelInitSamples; ++ySample) + { + const real_t yPoint = real_c(globalCell[1]) + real_c(ySample) * stepsize; + if (yPoint < functionValue) { ++numPointsBelow; } + } + } + + // fill level is fraction of points below sine profile + fillField->get(localCell) = real_c(numPointsBelow) / real_c(numTotalPoints); + }) // WALBERLA_FOR_ALL_CELLS + } + + // communicate fill level field to have meaningful values in ghost layer cells in periodic directions + Communication_T(blockForest, fillFieldID)(); + + // initialize fill level field + freeSurfaceBoundaryHandling->initFlagsFromFillLevel(); + + // communicate initialized flag field + Communication_T(blockForest, flagFieldID)(); + + // create timeloop + SweepTimeloop timeloop(blockForest, uint_c(50)); + + BlockDataID* relevantFillFieldID = &fillFieldID; + + if (smoothFillLevel) + { + smoothFillFieldID = + field::addToStorage< ScalarField_T >(blockForest, "Smooth fill levels", real_c(0.0), field::fzyx, uint_c(1)); + + // add sweep for smoothing the fill level field + SmoothingSweep< Stencil_T, FlagField_T, ScalarField_T, VectorField_T > smoothingSweep( + smoothFillFieldID, fillFieldID, flagFieldID, flagIDs::liquidInterfaceGasFlagIDs, + freeSurfaceBoundaryHandling->getFlagInfo().getObstacleIDSet(), false); + timeloop.add() << Sweep(smoothingSweep, "Smoothing sweep") + << AfterFunction(Communication_T(blockForest, smoothFillFieldID), + "Communication after smoothing sweep"); + relevantFillFieldID = &smoothFillFieldID; + } + + // add sweep for computing interface normals + NormalSweep< Stencil_T, FlagField_T, ScalarField_T, VectorField_T > normalsSweep( + normalFieldID, *relevantFillFieldID, flagFieldID, flagIDs::interfaceFlagID, flagIDs::liquidInterfaceGasFlagIDs, + freeSurfaceBoundaryHandling->getFlagInfo().getObstacleIDSet(), computeNormalsInInterfaceNeighbors, false, false, + false); + timeloop.add() << Sweep(normalsSweep, "Normals sweep") + << AfterFunction(Communication_T(blockForest, normalFieldID), "Communication after normal sweep"); + + // add sweep for evaluating the error of the interface normals (by comparing with analytical normal) + EvaluateNormalError< Stencil_T > errorEvaluationSweep(blockForest, normalFieldID, flagFieldID, + flagIDs::interfaceFlagID, domainSize, amplitude, + computeNormalsInInterfaceNeighbors, smoothFillLevel); + timeloop.add() << Sweep(errorEvaluationSweep, "Error evaluation sweep"); + + // perform a single time step + timeloop.singleStep(); + + MPIManager::instance()->resetMPI(); +} + +template< typename LatticeModel_T > +void runAllTests() +{ + // used for initializing the fill level in a Monte-Carlo-like fashion; each cell is sampled by the specified value in + // x- and y-direction + const uint_t fillLevelInitSamples = uint_c(100); + + // test with various domain sizes, i.e., resolutions + for (uint_t i = uint_c(50); i <= uint_c(200); i += uint_c(50)) + { + const real_t amplitude = real_c(0.1) * real_c(i); // amplitude of the sine profile + const real_t offset = real_c(0.5) * real_c(i); // offset the sine profile in y-direction + + WALBERLA_LOG_RESULT("Domain size " << i << " cells; normals computed only in interface cells;"); + test< LatticeModel_T >(i, amplitude, offset, fillLevelInitSamples, false, false); + + WALBERLA_LOG_RESULT( + "Domain size " << i << " cells; normals computed only in interface cells; fill level field smoothed;"); + test< LatticeModel_T >(i, amplitude, offset, fillLevelInitSamples, false, true); + + WALBERLA_LOG_RESULT("Domain size " << i + << " cells; normals computed in D2Q9/D3Q27 neighborhood of interface cells;"); + test< LatticeModel_T >(i, amplitude, offset, fillLevelInitSamples, true, false); + + WALBERLA_LOG_RESULT( + "Domain size " + << i << " cells; normals computed in D2Q9/D3Q27 neighborhood of interface cells; fill level field smoothed;"); + test< LatticeModel_T >(i, amplitude, offset, fillLevelInitSamples, true, true); + } + + // default values + const uint_t domainWidth = uint_c(100); // size of the domain in x- and y-direction + const real_t amplitude = real_c(0.1) * real_c(domainWidth); // amplitude of the sine profile + const real_t offset = real_c(0.5) * real_c(domainWidth); // offset the sine profile in y-direction + + // test with various amplitudes + for (real_t i = real_c(0.01); i < real_c(0.3); i += real_c(0.03)) + { + WALBERLA_LOG_RESULT("Amplitude " << i << " cells; normals computed only in interface cells;"); + test< LatticeModel_T >(domainWidth, i * real_c(domainWidth), offset, fillLevelInitSamples, false, false); + + WALBERLA_LOG_RESULT( + "Amplitude " << i << " cells; normals computed only in interface cells; fill level field smoothed;"); + test< LatticeModel_T >(domainWidth, i * real_c(domainWidth), offset, fillLevelInitSamples, false, true); + + WALBERLA_LOG_RESULT("Amplitude " << i + << " cells; normals computed in D2Q9/D3Q27 neighborhood of interface cells;"); + test< LatticeModel_T >(domainWidth, i * real_c(domainWidth), offset, fillLevelInitSamples, true, false); + + WALBERLA_LOG_RESULT( + "Amplitude " + << i << " cells; normals computed in D2Q9/D3Q27 neighborhood of interface cells; fill level field smoothed;"); + test< LatticeModel_T >(domainWidth, i * real_c(domainWidth), offset, fillLevelInitSamples, true, true); + } + + // test with various offsets, i.e., position of the sine-profile's zero-line + for (real_t i = real_c(0.4); i < real_c(0.6); i += real_c(0.03)) + { + WALBERLA_LOG_RESULT("Offset " << i << " cells; normals computed only in interface cells;"); + test< LatticeModel_T >(domainWidth, amplitude, i * real_c(domainWidth), fillLevelInitSamples, false, false); + + WALBERLA_LOG_RESULT("Offset " << i + << " cells; normals computed only in interface cells; fill level field smoothed;"); + test< LatticeModel_T >(domainWidth, amplitude, i * real_c(domainWidth), fillLevelInitSamples, false, true); + + WALBERLA_LOG_RESULT("Offset " << i << " cells; normals computed in D2Q9/D3Q27 neighborhood of interface cells;"); + test< LatticeModel_T >(domainWidth, amplitude, i * real_c(domainWidth), fillLevelInitSamples, true, false); + + WALBERLA_LOG_RESULT( + "Offset " + << i << " cells; normals computed in D2Q9/D3Q27 neighborhood of interface cells; fill level field smoothed;"); + test< LatticeModel_T >(domainWidth, amplitude, i * real_c(domainWidth), fillLevelInitSamples, true, true); + } + + // test with different fill level initialization samples (more samples => initialization of fill levels is + // closer to the real sine-profile) + for (uint_t i = uint_c(50); i <= uint_c(200); i += uint_c(50)) + { + WALBERLA_LOG_RESULT("Fill level initialization sample " << i + << " cells; normals computed only in interface cells;"); + test< LatticeModel_T >(domainWidth, amplitude, offset, i, false, false); + + WALBERLA_LOG_RESULT("Fill level initialization sample " + << i << " cells; normals computed only in interface cells; fill level field smoothed;"); + test< LatticeModel_T >(domainWidth, amplitude, offset, i, false, true); + + WALBERLA_LOG_RESULT("Fill level initialization sample " + << i << " cells; normals computed in D2Q9/D3Q27 neighborhood of interface cells;"); + test< LatticeModel_T >(domainWidth, amplitude, offset, i, true, false); + + WALBERLA_LOG_RESULT( + "Fill level initialization sample " + << i << " cells; normals computed in D2Q9/D3Q27 neighborhood of interface cells; fill level field smoothed;"); + test< LatticeModel_T >(domainWidth, amplitude, offset, i, true, true); + } +} + +int main(int argc, char** argv) +{ + debug::enterTestMode(); + Environment env(argc, argv); + + WALBERLA_LOG_RESULT("##################################"); + WALBERLA_LOG_RESULT("### Testing with D2Q9 stencil. ###"); + WALBERLA_LOG_RESULT("##################################"); + runAllTests< lbm::D2Q9< lbm::collision_model::SRT > >(); + + WALBERLA_LOG_RESULT("###################################"); + WALBERLA_LOG_RESULT("### Testing with D3Q19 stencil. ###"); + WALBERLA_LOG_RESULT("###################################"); + runAllTests< lbm::D3Q19< lbm::collision_model::SRT > >(); + + WALBERLA_LOG_RESULT("###################################"); + WALBERLA_LOG_RESULT("### Testing with D3Q27 stencil. ###"); + WALBERLA_LOG_RESULT("###################################"); + runAllTests< lbm::D3Q27< lbm::collision_model::SRT > >(); + + return EXIT_SUCCESS; +} +} // namespace NormalsOfSineTest +} // namespace free_surface +} // namespace walberla + +int main(int argc, char** argv) { return walberla::free_surface::NormalsOfSineTest::main(argc, argv); } \ No newline at end of file diff --git a/tests/lbm/free_surface/surface_geometry/NormalsOfSphereTest.cpp b/tests/lbm/free_surface/surface_geometry/NormalsOfSphereTest.cpp new file mode 100644 index 000000000..ca3c6b4f1 --- /dev/null +++ b/tests/lbm/free_surface/surface_geometry/NormalsOfSphereTest.cpp @@ -0,0 +1,363 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file NormalsOfSphereTest.cpp +//! \ingroup lbm/free_surface/surface_geometry +//! \author Martin Bauer +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Initialize spherical bubble and compare calculated normals to analytical (radial) normals. +// +//====================================================================================================================== + +#include "blockforest/Initialization.h" + +#include "core/Environment.h" +#include "core/debug/TestSubsystem.h" +#include "core/logging/Logging.h" +#include "core/math/DistributedSample.h" + +#include "field/AddToStorage.h" + +#include "geometry/bodies/Sphere.h" + +#include "lbm/blockforest/communication/SimpleCommunication.h" +#include "lbm/field/AddToStorage.h" +#include "lbm/field/PdfField.h" +#include "lbm/free_surface/boundary/FreeSurfaceBoundaryHandling.h" +#include "lbm/free_surface/surface_geometry/NormalSweep.h" +#include "lbm/free_surface/surface_geometry/SmoothingSweep.h" +#include "lbm/lattice_model/CollisionModel.h" +#include "lbm/lattice_model/D3Q19.h" +#include "lbm/lattice_model/D3Q27.h" + +#include "timeloop/SweepTimeloop.h" + +#include <cmath> + +namespace walberla +{ +namespace free_surface +{ +namespace NormalsOfSphereTest +{ +// define types +using flag_t = uint32_t; + +// define fields +using ScalarField_T = GhostLayerField< real_t, 1 >; +using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >; +using FlagField_T = FlagField< flag_t >; + +// compute and evaluate the absolute error of the angle of the interface normal in each interface cell; +// reference: vector that points from the gas bubble's, i.e., sphere's center to the current interface cell's center +template< typename Stencil_T > +class EvaluateNormalError +{ + public: + EvaluateNormalError(const std::weak_ptr< const StructuredBlockForest >& blockForest, const geometry::Sphere& sphere, + const ConstBlockDataID& normalFieldID, const ConstBlockDataID& flagFieldID, + const field::FlagUID& interfaceFlagID, bool computeNormalsInInterfaceNeighbors, + bool smoothFillLevel) + : blockForest_(blockForest), sphere_(sphere), normalFieldID_(normalFieldID), flagFieldID_(flagFieldID), + interfaceFlagID_(interfaceFlagID), computeNormalsInInterfaceNeighbors_(computeNormalsInInterfaceNeighbors), + smoothFillLevel_(smoothFillLevel) + {} + + void operator()(IBlock* const block) + { + const auto blockForest = blockForest_.lock(); + WALBERLA_CHECK_NOT_NULLPTR(blockForest); + + // get fields + const FlagField_T* const flagField = block->getData< const FlagField_T >(flagFieldID_); + const VectorField_T* const normalField = block->getData< const VectorField_T >(normalFieldID_); + + const flag_t interfaceFlag = flagField->getFlag(interfaceFlagID_); + + math::DistributedSample errorSample; + + WALBERLA_FOR_ALL_CELLS_OMP(flagFieldIt, flagField, normalFieldIt, normalField, omp critical, { + // evaluate normal only in interface cell (and possibly in an interface cell's neighborhood) + if (isFlagSet(flagFieldIt, interfaceFlag) || + (computeNormalsInInterfaceNeighbors_ && isFlagInNeighborhood< Stencil_T >(flagFieldIt, interfaceFlag))) + { + // get a vector pointing from sphere's center to current cell's center + Vector3< real_t > r; + + // get the global coordinate of the current cell's center + blockForest->getBlockLocalCellCenter(*block, flagFieldIt.cell(), r); + r -= sphere_.midpoint(); + + // invert normal direction: in this FSLBM implementation, the normal vector is defined to point from liquid + // to gas + r *= real_c(-1); + normalize(r); + + // calculate the error in the angle of the interface normal + // the domain of arccosine is [-1,1]; due to numerical inaccuracies, the dot product "*normalFieldIt*r" + // might be slightly out of this range; these cases are ignored here + const real_t dotProduct = *normalFieldIt * r; + if (dotProduct >= real_c(-1) && dotProduct <= real_c(1)) + { + const real_t angleDiff = std::acos(dotProduct); + errorSample.insert(angleDiff); + } + } + }) // WALBERLA_FOR_ALL_CELLS_OMP + + errorSample.mpiAllGather(); + + const real_t maxError = errorSample.max(); + const real_t meanError = errorSample.mean(); + + WALBERLA_LOG_RESULT("Mean absolute error in angle of normal = " << meanError); + WALBERLA_LOG_RESULT("Maximum absolute error in angle of normal = " << maxError); + WALBERLA_LOG_RESULT("Minimum absolute error in angle of normal = " << errorSample.min()); + + // the following reference errors have been obtained with a version of the code that is believed to be correct + if (computeNormalsInInterfaceNeighbors_ && smoothFillLevel_) + { + if constexpr (std::is_same_v< Stencil_T, stencil::D3Q27 >) + { + WALBERLA_CHECK_LESS(meanError, real_c(0.036)); + WALBERLA_CHECK_LESS(maxError, real_c(0.2)); + } + else + { + if constexpr (std::is_same_v< Stencil_T, stencil::D3Q19 >) + { + WALBERLA_CHECK_LESS(meanError, real_c(0.036)); + WALBERLA_CHECK_LESS(maxError, real_c(0.19)); + } + } + } + + if (!computeNormalsInInterfaceNeighbors_ && smoothFillLevel_) + { + if constexpr (std::is_same_v< Stencil_T, stencil::D3Q27 >) + { + WALBERLA_CHECK_LESS(meanError, real_c(0.016)); + WALBERLA_CHECK_LESS(maxError, real_c(0.045)); + } + else + { + if constexpr (std::is_same_v< Stencil_T, stencil::D3Q19 >) + { + WALBERLA_CHECK_LESS(meanError, real_c(0.0147)); + WALBERLA_CHECK_LESS(maxError, real_c(0.0457)); + } + } + } + + if (!computeNormalsInInterfaceNeighbors_ && !smoothFillLevel_) + { + if constexpr (std::is_same_v< Stencil_T, stencil::D3Q27 >) + { + WALBERLA_CHECK_LESS(meanError, real_c(0.021)); + WALBERLA_CHECK_LESS(maxError, real_c(0.08)); + } + else + { + if constexpr (std::is_same_v< Stencil_T, stencil::D3Q19 >) + { + WALBERLA_CHECK_LESS(meanError, real_c(0.037)); + WALBERLA_CHECK_LESS(maxError, real_c(0.096)); + } + } + } + + if (computeNormalsInInterfaceNeighbors_ && !smoothFillLevel_) + { + if constexpr (std::is_same_v< Stencil_T, stencil::D3Q27 >) + { + WALBERLA_CHECK_LESS(meanError, real_c(0.139)); + WALBERLA_CHECK_LESS(maxError, real_c(1.58)); + } + else + { + if constexpr (std::is_same_v< Stencil_T, stencil::D3Q19 >) + { + WALBERLA_CHECK_LESS(meanError, real_c(0.121)); + WALBERLA_CHECK_LESS(maxError, real_c(1.58)); + } + } + } + } + + private: + std::weak_ptr< const StructuredBlockForest > blockForest_; + geometry::Sphere sphere_; + + ConstBlockDataID normalFieldID_; + ConstBlockDataID flagFieldID_; + field::FlagUID interfaceFlagID_; + + bool computeNormalsInInterfaceNeighbors_; + bool smoothFillLevel_; +}; // class EvaluateNormalError + +template< typename LatticeModel_T > +void test(uint_t numCells, Vector3< real_t > offset, bool computeNormalsInInterfaceNeighbors, bool smoothFillLevel) +{ + // define types + using Stencil_T = typename LatticeModel_T::Stencil; + using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >; + using Communication_T = blockforest::SimpleCommunication< typename LatticeModel_T::CommunicationStencil >; + + // define the domain size + const Vector3< uint_t > numBlocks(uint_c(1)); + const Vector3< uint_t > domainSize(numCells + uint_c(6)); + const Vector3< uint_t > cellsPerBlock(domainSize[0] / numBlocks[0], domainSize[1] / numBlocks[1], + domainSize[2] / numBlocks[2]); + + // create block grid + const std::shared_ptr< StructuredBlockForest > blockForest = + blockforest::createUniformBlockGrid(numBlocks[0], numBlocks[1], numBlocks[2], // blocks + cellsPerBlock[0], cellsPerBlock[1], cellsPerBlock[2], // cells + real_c(1.0), // dx + true, // one block per process + false, false, false); // periodicity + + // create lattice model with omega=1 + LatticeModel_T latticeModel(real_c(1.0)); + + // add fields + BlockDataID pdfFieldID = + lbm::addPdfFieldToStorage< LatticeModel_T >(blockForest, "PDF field", latticeModel, field::fzyx); + BlockDataID fillFieldID = + field::addToStorage< ScalarField_T >(blockForest, "Fill levels", real_c(1.0), field::fzyx, uint_c(1)); + BlockDataID normalFieldID = field::addToStorage< VectorField_T >( + blockForest, "Normals", Vector3< real_t >(real_c(0)), field::fzyx, uint_c(1)); + BlockDataID smoothFillFieldID; + + // add boundary handling + const std::shared_ptr< FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling = + std::make_shared< FreeSurfaceBoundaryHandling_T >(blockForest, pdfFieldID, fillFieldID); + BlockDataID flagFieldID = freeSurfaceBoundaryHandling->getFlagFieldID(); + + // add gas sphere, i.e., bubble + Vector3< real_t > midpoint(real_c(domainSize[0]) * real_c(0.5), real_c(domainSize[1]) * real_c(0.5), + real_c(domainSize[2]) * real_c(0.5)); + geometry::Sphere sphere(midpoint + offset, real_c(numCells) * real_c(0.5)); + freeSurfaceBoundaryHandling->addFreeSurfaceObject(sphere); + freeSurfaceBoundaryHandling->initFlagsFromFillLevel(); + + // create timeloop + SweepTimeloop timeloop(blockForest, uint_c(1)); + + BlockDataID* relevantFillFieldID = &fillFieldID; + + if (smoothFillLevel) + { + smoothFillFieldID = + field::addToStorage< ScalarField_T >(blockForest, "Smooth fill levels", real_c(1.0), field::fzyx, uint_c(1)); + + // add sweep for smoothing the fill level field + SmoothingSweep< Stencil_T, FlagField_T, ScalarField_T, VectorField_T > smoothingSweep( + smoothFillFieldID, fillFieldID, flagFieldID, flagIDs::liquidInterfaceGasFlagIDs, + freeSurfaceBoundaryHandling->getFlagInfo().getObstacleIDSet(), false); + timeloop.add() << Sweep(smoothingSweep, "Smoothing sweep") + << AfterFunction(Communication_T(blockForest, smoothFillFieldID), + "Communication after smoothing sweep"); + + relevantFillFieldID = &smoothFillFieldID; + } + + // add sweep for computing interface normals + NormalSweep< Stencil_T, FlagField_T, ScalarField_T, VectorField_T > normalsSweep( + normalFieldID, *relevantFillFieldID, flagFieldID, flagIDs::interfaceFlagID, flagIDs::liquidInterfaceGasFlagIDs, + freeSurfaceBoundaryHandling->getFlagInfo().getObstacleIDSet(), computeNormalsInInterfaceNeighbors, false, false, + false); + timeloop.add() << Sweep(normalsSweep, "Normals sweep") + << AfterFunction(Communication_T(blockForest, normalFieldID), "Communication after normal sweep"); + + // add sweep for evaluating the error of the interface normals (by comparing with analytical normal) + EvaluateNormalError< Stencil_T > errorEvaluationSweep(blockForest, sphere, normalFieldID, flagFieldID, + flagIDs::interfaceFlagID, computeNormalsInInterfaceNeighbors, + smoothFillLevel); + timeloop.add() << Sweep(errorEvaluationSweep, "Error evaluation sweep"); + + // perform a single time step + timeloop.singleStep(); + + MPIManager::instance()->resetMPI(); +} + +template< typename LatticeModel_T > +void runAllTests() +{ + // test with various bubble diameters + for (uint_t i = uint_c(10); i <= uint_c(30); i += uint_c(10)) + { + WALBERLA_LOG_RESULT("Bubble diameter " << i << " cells; normals computed only in interface cells;"); + test< LatticeModel_T >(i, Vector3< real_t >(real_c(0.5), real_c(0), real_c(0)), false, false); + + WALBERLA_LOG_RESULT("Bubble diameter " + << i << " cells; normals computed only in interface cells; fill level field smoothed;"); + test< LatticeModel_T >(i, Vector3< real_t >(real_c(0.5), real_c(0), real_c(0)), false, true); + + WALBERLA_LOG_RESULT("Bubble diameter " << i + << " cells; normals computed in D3Q27 neighborhood of interface cells;"); + test< LatticeModel_T >(i, Vector3< real_t >(real_c(0.5), real_c(0), real_c(0)), true, false); + + WALBERLA_LOG_RESULT( + "Bubble diameter " + << i << " cells; normals computed in D3Q27 neighborhood of interface cells; fill level field smoothed;"); + test< LatticeModel_T >(i, Vector3< real_t >(real_c(0.5), real_c(0), real_c(0)), true, true); + } + + // test with various offsets of sphere's center + for (real_t off = real_c(0.0); off < real_c(1.0); off += real_c(0.2)) + { + WALBERLA_LOG_RESULT("Bubble offset " << off << " cells; normals computed only in interface cells;"); + test< LatticeModel_T >(uint_c(10), Vector3< real_t >(off, real_c(0), real_c(0)), false, false); + + WALBERLA_LOG_RESULT("Bubble offset " + << off << " cells; normals computed only in interface cells; fill level field smoothed;"); + test< LatticeModel_T >(uint_c(10), Vector3< real_t >(off, real_c(0), real_c(0)), false, true); + + WALBERLA_LOG_RESULT("Bubble offset " << off + << " cells; normals computed in D3Q27 neighborhood of interface cells;"); + test< LatticeModel_T >(uint_c(10), Vector3< real_t >(off, real_c(0), real_c(0)), true, false); + + WALBERLA_LOG_RESULT( + "Bubble offset " + << off << " cells; normals computed in D3Q27 neighborhood of interface cells; fill level field smoothed;"); + test< LatticeModel_T >(uint_c(10), Vector3< real_t >(off, real_c(0), real_c(0)), true, true); + } +} + +int main(int argc, char** argv) +{ + debug::enterTestMode(); + Environment env(argc, argv); + + WALBERLA_LOG_RESULT("###################################"); + WALBERLA_LOG_RESULT("### Testing with D3Q19 stencil. ###"); + WALBERLA_LOG_RESULT("###################################"); + runAllTests< lbm::D3Q19< lbm::collision_model::SRT > >(); + + WALBERLA_LOG_RESULT("###################################"); + WALBERLA_LOG_RESULT("### Testing with D3Q27 stencil. ###"); + WALBERLA_LOG_RESULT("###################################"); + runAllTests< lbm::D3Q27< lbm::collision_model::SRT > >(); + + return EXIT_SUCCESS; +} +} // namespace NormalsOfSphereTest +} // namespace free_surface +} // namespace walberla + +int main(int argc, char** argv) { return walberla::free_surface::NormalsOfSphereTest::main(argc, argv); } \ No newline at end of file diff --git a/tests/lbm/free_surface/surface_geometry/ObstacleFillLevelTest.cpp b/tests/lbm/free_surface/surface_geometry/ObstacleFillLevelTest.cpp new file mode 100644 index 000000000..6ccbff11b --- /dev/null +++ b/tests/lbm/free_surface/surface_geometry/ObstacleFillLevelTest.cpp @@ -0,0 +1,283 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file ObstacleFillLevelTest.cpp +//! \ingroup lbm/free_surface/surface_geometry +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Test the ObstacleFillLevelSweep with an obstacle cell that is surrounded by different other cells. +//! +//! 3x3x1 domain, with obstacle cell at (1,1,0) and given obstacle normal. +//====================================================================================================================== + +#include "blockforest/Initialization.h" + +#include "core/Environment.h" +#include "core/debug/TestSubsystem.h" + +#include "lbm/field/AddToStorage.h" +#include "lbm/free_surface/boundary/FreeSurfaceBoundaryHandling.h" +#include "lbm/free_surface/surface_geometry/ObstacleFillLevelSweep.h" +#include "lbm/lattice_model/D3Q19.h" + +#include "timeloop/SweepTimeloop.h" + +#include <unordered_map> +#include <unordered_set> + +namespace walberla +{ +namespace free_surface +{ +namespace ObstacleFillLevelTest +{ +using LatticeModel_T = lbm::D3Q19< lbm::collision_model::SRT >; +using PdfField_T = lbm::PdfField< LatticeModel_T >; +using Stencil_T = LatticeModel_T::Stencil; + +using ScalarField_T = GhostLayerField< real_t, 1 >; +using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >; + +using flag_t = uint32_t; +using FlagField_T = FlagField< flag_t >; +using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >; + +real_t computeObstacleFillLevel(const std::unordered_map< Cell, real_t >& interfaceCells, + const std::unordered_set< Cell >& liquidCells, + const std::unordered_set< Cell >& gasCells, + const std::unordered_set< Cell >& solidCells, + const Vector3< real_t >& centerObstacleNormal) +{ + // define the domain size + const Vector3< uint_t > numBlocks(uint_c(1)); + const Vector3< uint_t > domainSize(uint_c(3), uint_c(3), uint_c(3)); + const Vector3< uint_t > cellsPerBlock(domainSize[0] / numBlocks[0], domainSize[1] / numBlocks[1], + domainSize[2] / numBlocks[2]); + + // create block grid + const std::shared_ptr< StructuredBlockForest > blockForest = + blockforest::createUniformBlockGrid(numBlocks[0], numBlocks[1], numBlocks[2], // blocks + cellsPerBlock[0], cellsPerBlock[1], cellsPerBlock[2], // cells + real_c(1.0), // dx + true, // one block per process + true, true, true); // periodicity + + // create lattice model (dummy, not relevant for this test) + LatticeModel_T latticeModel = LatticeModel_T(lbm::collision_model::SRT(real_c(1))); + + // add fields + const BlockDataID pdfFieldID = lbm::addPdfFieldToStorage(blockForest, "PDF field", latticeModel, field::fzyx); + BlockDataID fillFieldID = + field::addToStorage< ScalarField_T >(blockForest, "Fill level field", real_c(0.0), field::fzyx, uint_c(1)); + BlockDataID obstaclefillFieldID = field::addToStorage< ScalarField_T >(blockForest, "Obstacle fill level field", + real_c(0.0), field::fzyx, uint_c(1)); + const BlockDataID obstacleNormalFieldID = field::addToStorage< VectorField_T >( + blockForest, "Obstacle normals", Vector3< real_t >(real_c(0)), field::fzyx, uint_c(1)); + + // add boundary handling + const std::shared_ptr< FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling = + std::make_shared< FreeSurfaceBoundaryHandling_T >(blockForest, pdfFieldID, fillFieldID); + const BlockDataID flagFieldID = freeSurfaceBoundaryHandling->getFlagFieldID(); + const BlockDataID boundaryHandlingID = freeSurfaceBoundaryHandling->getHandlingID(); + const typename FreeSurfaceBoundaryHandling_T::FlagInfo_T& flagInfo = freeSurfaceBoundaryHandling->getFlagInfo(); + + // initialize domain + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + ScalarField_T* const fillField = blockIt->getData< ScalarField_T >(fillFieldID); + VectorField_T* const obstacleNormalField = blockIt->getData< VectorField_T >(obstacleNormalFieldID); + FreeSurfaceBoundaryHandling_T::BoundaryHandling_T* const boundaryHandling = + blockIt->getData< FreeSurfaceBoundaryHandling_T::BoundaryHandling_T >(boundaryHandlingID); + + WALBERLA_FOR_ALL_CELLS_XYZ(fillField, { + // set no slip at domain boundaries in z-direction to reduce problem to 2D + if (z == cell_idx_c(0) || z == cell_idx_c(2)) + { + boundaryHandling->setBoundary(FreeSurfaceBoundaryHandling_T::noSlipFlagID, x, y, z); + + // obstacle normal must be normalized to not trigger the assertion in ObstacleFillLevelSweep + obstacleNormalField->get(x, y, z) = Vector3< real_t >(real_c(1), real_c(1), real_c(1)).getNormalized(); + continue; + } + + // obstacle cell (to be evaluated) in the domain center + if (x == cell_idx_c(1) && y == cell_idx_c(1) && z == cell_idx_c(1)) + { + boundaryHandling->setBoundary(FreeSurfaceBoundaryHandling_T::noSlipFlagID, x, y, z); + obstacleNormalField->get(x, y, z) = centerObstacleNormal.getNormalized(); + fillField->get(x, y, z) = real_c(-5); // dummy value to check that the value did not change + continue; + } + + if (interfaceCells.find(Cell(x, y, z)) != interfaceCells.end()) + { + boundaryHandling->setFlag(flagIDs::interfaceFlagID, x, y, z); + fillField->get(x, y, z) = interfaceCells.find(Cell(x, y, z))->second; + continue; + } + + if (liquidCells.find(Cell(x, y, z)) != liquidCells.end()) + { + boundaryHandling->setFlag(flagIDs::liquidFlagID, x, y, z); + fillField->get(x, y, z) = real_c(1); + continue; + } + + if (gasCells.find(Cell(x, y, z)) != gasCells.end()) + { + boundaryHandling->setFlag(flagIDs::gasFlagID, x, y, z); + fillField->get(x, y, z) = real_c(0); + continue; + } + + if (solidCells.find(Cell(x, y, z)) != solidCells.end()) + { + boundaryHandling->setFlag(FreeSurfaceBoundaryHandling_T::noSlipFlagID, x, y, z); + + // obstacle normal must be normalized to not trigger the assertion in ObstacleFillLevelSweep + obstacleNormalField->get(x, y, z) = Vector3< real_t >(real_c(1), real_c(1), real_c(1)).getNormalized(); + continue; + } + }) // WALBERLA_FOR_ALL_CELLS_XYZ + } + + // create timeloop + SweepTimeloop timeloop(blockForest, 1); + + // add ObstacleFillLevelSweep + ObstacleFillLevelSweep< Stencil_T, FlagField_T, ScalarField_T, VectorField_T > obstFillSweep( + obstaclefillFieldID, fillFieldID, flagFieldID, obstacleNormalFieldID, flagIDs::liquidInterfaceGasFlagIDs, + flagInfo.getObstacleIDSet()); + timeloop.add() << Sweep(obstFillSweep, "Obstacle fill level sweep"); + + timeloop.singleStep(); + + real_t obstacleFillLevel = real_c(0); + + // get fill level of (central) solid cell + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + const ScalarField_T* const obstaclefillField = blockIt->getData< const ScalarField_T >(obstaclefillFieldID); + + obstacleFillLevel = obstaclefillField->get(cell_idx_c(1), cell_idx_c(1), cell_idx_c(1)); + } + + return obstacleFillLevel; +} + +int main(int argc, char** argv) +{ + debug::enterTestMode(); + Environment env(argc, argv); + + Vector3< real_t > centerObstacleNormal; + std::unordered_map< Cell, real_t > interfaceCells; + std::unordered_set< Cell > liquidCells; + std::unordered_set< Cell > gasCells; + std::unordered_set< Cell > solidCells; + real_t expectedFillLevel; + real_t computedFillLevel; + + // IMPORTANT REMARK: + // the fill level of the obstacle center cell at (1, 1, 1) is going to be computed; therefore, Cell(1, 1, 1) must not + // be set here + + // test case 1: interface neighbors at the top, no liquid or gas neighbors, remaining cells are solid, obstacle + // normal points upwards + WALBERLA_LOG_RESULT("Performing test case 1") + centerObstacleNormal = Vector3< real_t >(real_c(0), real_c(1), real_c(0)); + solidCells.emplace(Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(1))); + solidCells.emplace(Cell(cell_idx_c(1), cell_idx_c(0), cell_idx_c(1))); + solidCells.emplace(Cell(cell_idx_c(2), cell_idx_c(0), cell_idx_c(1))); + solidCells.emplace(Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(1))); + solidCells.emplace(Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(1))); + interfaceCells.emplace(Cell(cell_idx_c(0), cell_idx_c(2), cell_idx_c(1)), real_c(0.5)); + interfaceCells.emplace(Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(1)), real_c(0.5)); + interfaceCells.emplace(Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(1)), real_c(0.5)); + expectedFillLevel = real_c(0.5); // verified by hand + computedFillLevel = + computeObstacleFillLevel(interfaceCells, liquidCells, gasCells, solidCells, centerObstacleNormal); + WALBERLA_CHECK_FLOAT_EQUAL(expectedFillLevel, computedFillLevel) + + interfaceCells.clear(); + liquidCells.clear(); + gasCells.clear(); + solidCells.clear(); + + // test case 2: interface neighbors at the top, left cell is liquid, right cell is gas, bottom cells are solid, + // obstacle normal points upwards + WALBERLA_LOG_RESULT("Performing test case 2") + centerObstacleNormal = Vector3< real_t >(real_c(0), real_c(1), real_c(0)); + solidCells.emplace(Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(1))); + solidCells.emplace(Cell(cell_idx_c(1), cell_idx_c(0), cell_idx_c(1))); + solidCells.emplace(Cell(cell_idx_c(2), cell_idx_c(0), cell_idx_c(1))); + interfaceCells.emplace(Cell(cell_idx_c(0), cell_idx_c(2), cell_idx_c(1)), real_c(0.75)); + interfaceCells.emplace(Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(1)), real_c(0.5)); + interfaceCells.emplace(Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(1)), real_c(0.5)); + liquidCells.emplace(Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(1))); + gasCells.emplace(Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(1))); + expectedFillLevel = real_c(0.5732233); // verified by hand + computedFillLevel = + computeObstacleFillLevel(interfaceCells, liquidCells, gasCells, solidCells, centerObstacleNormal); + WALBERLA_CHECK_FLOAT_EQUAL(expectedFillLevel, computedFillLevel) + + interfaceCells.clear(); + liquidCells.clear(); + gasCells.clear(); + solidCells.clear(); + + // test case 3: same as testcase 2 but obstacle normal points to the upper left corner + WALBERLA_LOG_RESULT("Performing test case 3") + centerObstacleNormal = Vector3< real_t >(real_c(-1), real_c(1), real_c(0)); + solidCells.emplace(Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(1))); + solidCells.emplace(Cell(cell_idx_c(1), cell_idx_c(0), cell_idx_c(1))); + solidCells.emplace(Cell(cell_idx_c(2), cell_idx_c(0), cell_idx_c(1))); + interfaceCells.emplace(Cell(cell_idx_c(0), cell_idx_c(2), cell_idx_c(1)), real_c(0.75)); + interfaceCells.emplace(Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(1)), real_c(0.5)); + interfaceCells.emplace(Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(1)), real_c(0.5)); + liquidCells.emplace(Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(1))); + gasCells.emplace(Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(1))); + expectedFillLevel = real_c(0.58009431); // verified by hand + computedFillLevel = + computeObstacleFillLevel(interfaceCells, liquidCells, gasCells, solidCells, centerObstacleNormal); + WALBERLA_CHECK_FLOAT_EQUAL(expectedFillLevel, computedFillLevel) + + interfaceCells.clear(); + liquidCells.clear(); + gasCells.clear(); + solidCells.clear(); + + // test case 4: only interface cells in neighborhood (all with same fill level) + WALBERLA_LOG_RESULT("Performing test case 4") + centerObstacleNormal = Vector3< real_t >(real_c(-1), real_c(1), real_c(0)); + interfaceCells.emplace(Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(1)), real_c(0.5)); + interfaceCells.emplace(Cell(cell_idx_c(1), cell_idx_c(0), cell_idx_c(1)), real_c(0.5)); + interfaceCells.emplace(Cell(cell_idx_c(2), cell_idx_c(0), cell_idx_c(1)), real_c(0.5)); + interfaceCells.emplace(Cell(cell_idx_c(0), cell_idx_c(1), cell_idx_c(1)), real_c(0.5)); + interfaceCells.emplace(Cell(cell_idx_c(2), cell_idx_c(1), cell_idx_c(1)), real_c(0.5)); + interfaceCells.emplace(Cell(cell_idx_c(0), cell_idx_c(2), cell_idx_c(1)), real_c(0.5)); + interfaceCells.emplace(Cell(cell_idx_c(1), cell_idx_c(2), cell_idx_c(1)), real_c(0.5)); + interfaceCells.emplace(Cell(cell_idx_c(2), cell_idx_c(2), cell_idx_c(1)), real_c(0.5)); + expectedFillLevel = real_c(0.5); // dummy value as initialized in code above + computedFillLevel = + computeObstacleFillLevel(interfaceCells, liquidCells, gasCells, solidCells, centerObstacleNormal); + WALBERLA_CHECK_FLOAT_EQUAL(expectedFillLevel, computedFillLevel) + + return EXIT_SUCCESS; +} +} // namespace ObstacleFillLevelTest +} // namespace free_surface +} // namespace walberla + +int main(int argc, char** argv) { return walberla::free_surface::ObstacleFillLevelTest::main(argc, argv); } \ No newline at end of file diff --git a/tests/lbm/free_surface/surface_geometry/ObstacleNormalsTest.cpp b/tests/lbm/free_surface/surface_geometry/ObstacleNormalsTest.cpp new file mode 100644 index 000000000..0d45f6685 --- /dev/null +++ b/tests/lbm/free_surface/surface_geometry/ObstacleNormalsTest.cpp @@ -0,0 +1,186 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file ObstacleNormalsTest.cpp +//! \ingroup lbm/free_surface/surface_geometry +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Test if mean obstacle normal computation is correct in setup with inclined plane and different inclination +//! angles. +// +//====================================================================================================================== + +#include "blockforest/Initialization.h" + +#include "core/Environment.h" +#include "core/debug/TestSubsystem.h" +#include "core/math/Constants.h" + +#include "field/AddToStorage.h" + +#include "lbm/free_surface/surface_geometry/ObstacleNormalSweep.h" + +#include "stencil/D3Q27.h" +#include "stencil/D3Q7.h" + +#include "timeloop/SweepTimeloop.h" + +#include <cmath> + +namespace walberla +{ +namespace free_surface +{ +namespace ObstacleNormalsTest +{ +// define types +using Stencil_T = stencil::D3Q27; +using flag_t = uint8_t; + +const FlagUID Fluid_Flag("fluid"); +const FlagUID FluidNearSolid_Flag("fluid near solid"); +const FlagUID Solid_Flag("no slip"); +const Set< FlagUID > All_Fluid_Flags = setUnion< FlagUID >(Fluid_Flag, FluidNearSolid_Flag); + +// define fields +using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >; +using FlagField_T = FlagField< flag_t >; + +void test(real_t degreeInclinationAngle) +{ + // define the domain size + const Vector3< uint_t > numBlocks(uint_c(1)); + const Vector3< uint_t > domainSize(uint_c(20), uint_c(3), uint_c(20)); + const Vector3< uint_t > cellsPerBlock(domainSize[0] / numBlocks[0], domainSize[1] / numBlocks[1], + domainSize[2] / numBlocks[2]); + + // create block grid + const std::shared_ptr< StructuredBlockForest > blockForest = + blockforest::createUniformBlockGrid(numBlocks[0], numBlocks[1], numBlocks[2], // blocks + cellsPerBlock[0], cellsPerBlock[1], cellsPerBlock[2], // cells + real_c(1.0), // dx + true, // one block per process + false, false, false); // periodicity + + // add fields + BlockDataID obstacleNormalFieldID = field::addToStorage< VectorField_T >( + blockForest, "Obstacle normals", Vector3< real_t >(real_c(0)), field::fzyx, uint_c(1)); + BlockDataID flagFieldID = field::addFlagFieldToStorage< FlagField_T >( + blockForest, "flags", uint_c(2)); // flag field must have two ghost layers as in regular FSLBM application code + + // initialize flag field as inclination, the lower part of the domain will be solid + const real_t tanAngle = real_c(std::tan(degreeInclinationAngle * math::pi / real_c(180))); + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + FlagField_T* const flagField = blockIt->getData< FlagField_T >(flagFieldID); + + const auto fluidFlag = flagField->registerFlag(Fluid_Flag); + const auto fluidNearSolidFlag = flagField->registerFlag(FluidNearSolid_Flag); + const auto solidFlag = flagField->registerFlag(Solid_Flag); + + // create inclination in flag field + WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ( + flagField, uint_c(2), + // all cells below the specified inclination angle are marked as solid + if (real_c(x) * tanAngle <= real_c(z)) { flagField->get(x, y, z) = solidFlag; } else { + flagField->get(x, y, z) = fluidFlag; + }); // WALBERLA_FOR_ALL_CELLS_INCLUDING_GHOST_LAYER_XYZ + + // mark fluid cells that have neighboring solid cells ( only in these cells, the obstacle normal will be computed) + WALBERLA_FOR_ALL_CELLS( + flagFieldIt, flagField, + if (isFlagSet(flagFieldIt, solidFlag) && isFlagInNeighborhood< stencil::D3Q7 >(flagFieldIt, fluidFlag)) { + *flagFieldIt = fluidNearSolidFlag; + }); // WALBERLA_FOR_ALL_CELLS + } + + // create timeloop + SweepTimeloop timeloop(blockForest, uint_c(1)); + + // add obstacle normals sweep + ObstacleNormalSweep< Stencil_T, FlagField_T, VectorField_T > obstNormalsSweep( + obstacleNormalFieldID, flagFieldID, FluidNearSolid_Flag, All_Fluid_Flags, Solid_Flag, true, false, false); + timeloop.add() << Sweep(obstNormalsSweep, "Obstacle normals sweep"); + + // perform a single time step + timeloop.singleStep(); + + // compute analytical normal + const real_t sinAngle = real_c(std::sin(degreeInclinationAngle * math::pi / real_c(180))); + const real_t cosAngle = real_c(std::cos(degreeInclinationAngle * math::pi / real_c(180))); + const Vector3< real_t > analyticalNormal(-sinAngle, real_c(0), cosAngle); + + // compare analytical normal with the average of all computed obstacle normals + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + const VectorField_T* const obstacleNormalField = blockIt->getData< const VectorField_T >(obstacleNormalFieldID); + const FlagField_T* const flagField = blockIt->getData< const FlagField_T >(flagFieldID); + + const auto fluidNearSolidFlag = flagField->getFlag(FluidNearSolid_Flag); + + real_t averageObstNormalX = real_c(0); + real_t averageObstNormalY = real_c(0); + real_t averageObstNormalZ = real_c(0); + uint_t cellCount = uint_c(0); + WALBERLA_FOR_ALL_CELLS_XYZ_OMP(obstacleNormalField, + omp parallel for schedule(static) reduction(+:averageObstNormalX) reduction(+:averageObstNormalY) + reduction(+:averageObstNormalZ) reduction(+:cellCount), { + // skip cells at the domain boundary; obstacle normal in these cells deviates further from analytical solution + // since neighborhood for obstacle computation is too small + if (x == cell_idx_c(0) || y == cell_idx_c(0) || z == cell_idx_c(0) || + x == cell_idx_c(domainSize[0] - uint_c(1)) || y == cell_idx_c(domainSize[1] - uint_c(1)) || + z == cell_idx_c(domainSize[2] - uint_c(1))) + { + continue; + } + + if (!isFlagSet(flagField->get(x, y, z), fluidNearSolidFlag)) + { + // obstacle normal must be zero in all cells that are not of type fluidNearSolid + WALBERLA_CHECK_FLOAT_EQUAL(obstacleNormalField->get(x, y, z), Vector3< real_t >(real_c(0))); + } + else + { + ++cellCount; + averageObstNormalX += obstacleNormalField->get(x, y, z)[0]; + averageObstNormalY += obstacleNormalField->get(x, y, z)[1]; + averageObstNormalZ += obstacleNormalField->get(x, y, z)[2]; + }}); // WALBERLA_FOR_ALL_CELLS_XYZ_OMP + + // compute average obstacle normal + Vector3< real_t > averageObstNormal(averageObstNormalX, averageObstNormalY, averageObstNormalZ); + averageObstNormal /= real_c(cellCount); + + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(averageObstNormal, analyticalNormal, real_c(0.1)); + } +} + +int main(int argc, char** argv) +{ + debug::enterTestMode(); + Environment env(argc, argv); + // test with various inclination angles (only up to 87 degree, as otherwise in this setup no more fluid cells remain) + for (uint_t angle = uint_c(1); angle <= uint_c(86); angle += uint_c(1)) + { + WALBERLA_LOG_INFO_ON_ROOT("Testing inclination angle " << real_c(angle) << " degree.") + test(real_c(angle)); + } + + return EXIT_SUCCESS; +} +} // namespace ObstacleNormalsTest +} // namespace free_surface +} // namespace walberla + +int main(int argc, char** argv) { return walberla::free_surface::ObstacleNormalsTest::main(argc, argv); } \ No newline at end of file diff --git a/tests/lbm/free_surface/surface_geometry/WettingCurvatureTest.cpp b/tests/lbm/free_surface/surface_geometry/WettingCurvatureTest.cpp new file mode 100644 index 000000000..2bcff6248 --- /dev/null +++ b/tests/lbm/free_surface/surface_geometry/WettingCurvatureTest.cpp @@ -0,0 +1,341 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla 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. +// +// waLBerla 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 waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file WettingCurvatureTest.cpp +//! \ingroup lbm/free_surface/surface_geometry +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Initialize a drop as a cylinder section near a solid wall and evaluate the resulting wetting curvature. +// +//====================================================================================================================== + +#include "blockforest/Initialization.h" + +#include "core/Environment.h" +#include "core/debug/TestSubsystem.h" +#include "core/logging/Logging.h" + +#include "field/AddToStorage.h" +#include "field/vtk/FlagFieldCellFilter.h" +#include "field/vtk/VTKWriter.h" + +#include "geometry/bodies/Cylinder.h" +#include "geometry/initializer/OverlapFieldFromBody.h" + +#include "lbm/free_surface/surface_geometry/ContactAngle.h" +#include "lbm/free_surface/surface_geometry/CurvatureSweep.h" +#include "lbm/free_surface/surface_geometry/NormalSweep.h" +#include "lbm/free_surface/surface_geometry/ObstacleFillLevelSweep.h" +#include "lbm/free_surface/surface_geometry/ObstacleNormalSweep.h" +#include "lbm/free_surface/surface_geometry/SmoothingSweep.h" + +#include "stencil/D3Q27.h" + +#include "timeloop/SweepTimeloop.h" + +#include "vtk/Initialization.h" +#include "vtk/VTKOutput.h" + +namespace walberla +{ +namespace free_surface +{ +namespace WettingCurvatureTest +{ +// define types +using Stencil_T = stencil::D3Q27; + +using flag_t = uint32_t; + +const FlagUID liquidFlagID("liquid"); +const FlagUID interfaceFlagID("interface"); +const FlagUID gasFlagID("gas"); +const FlagUID solidID("solid"); +const Set< FlagUID > liquidInterfaceGasFlagIDs = + setUnion< FlagUID >(setUnion< FlagUID >(liquidFlagID, interfaceFlagID), gasFlagID); + +// define fields +using ScalarField_T = GhostLayerField< real_t, 1 >; +using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >; +using FlagField_T = FlagField< flag_t >; + +real_t computeCurvature(real_t contactAngle, bool useTriangulation) +{ + // define the domain size + const Vector3< uint_t > numBlocks(uint_c(1)); + const Vector3< uint_t > domainSize(uint_c(6), uint_c(5), uint_c(5)); + const Vector3< uint_t > cellsPerBlock(domainSize[0] / numBlocks[0], domainSize[1] / numBlocks[1], + domainSize[2] / numBlocks[2]); + + // create block grid + const std::shared_ptr< StructuredBlockForest > blockForest = + blockforest::createUniformBlockGrid(numBlocks[0], numBlocks[1], numBlocks[2], // blocks + cellsPerBlock[0], cellsPerBlock[1], cellsPerBlock[2], // cells + real_c(1.0), // dx + true, // one block per process + false, false, false); // periodicity + + // add fields + BlockDataID fillFieldID = + field::addToStorage< ScalarField_T >(blockForest, "Fill levels", real_c(0.0), field::fzyx, uint_c(1)); + BlockDataID normalFieldID = field::addToStorage< VectorField_T >( + blockForest, "Normals", Vector3< real_t >(real_c(0)), field::fzyx, uint_c(1)); + BlockDataID curvatureFieldID = + field::addToStorage< ScalarField_T >(blockForest, "Curvature", real_c(0), field::fzyx, uint_c(1)); + BlockDataID obstacleNormalFieldID = field::addToStorage< VectorField_T >( + blockForest, "Obstacle normal field", Vector3< real_t >(real_c(0)), field::fzyx, uint_c(1)); + BlockDataID flagFieldID = field::addFlagFieldToStorage< FlagField_T >( + blockForest, "Flags", uint_c(2)); // flag field must have two ghost layers as in regular FSLBM application code + BlockDataID smoothFillFieldID = + field::addToStorage< ScalarField_T >(blockForest, "Smooth fill levels", real_c(1.0), field::fzyx, uint_c(1)); + + // add liquid drop + Vector3< real_t > midpoint1(real_c(5), real_c(1), real_c(0)); + Vector3< real_t > midpoint2(real_c(5), real_c(1), real_c(5)); + geometry::Cylinder cylinder(midpoint1, midpoint2, real_c(5) * real_c(0.5)); + geometry::initializer::OverlapFieldFromBody(*blockForest, fillFieldID).init(cylinder, true); + + // set flags + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + FlagField_T* const flagField = blockIt->getData< FlagField_T >(flagFieldID); + ScalarField_T* const fillField = blockIt->getData< ScalarField_T >(fillFieldID); + + const auto gas = flagField->registerFlag(gasFlagID); + const auto liquid = flagField->registerFlag(liquidFlagID); + const auto interface = flagField->registerFlag(interfaceFlagID); + const auto solid = flagField->registerFlag(solidID); + + // initialize whole flag field as gas + WALBERLA_FOR_ALL_CELLS(flagFieldIt, flagField, { *flagFieldIt = gas; }) // WALBERLA_FOR_ALL_CELLS + + // initialize flag field according to fill level + WALBERLA_FOR_ALL_CELLS_XYZ( + flagField, + if (y == 0) { + flagField->get(x, y, z) = solid; + fillField->get(x, y, z) = real_c(0); + } + + if (fillField->get(x, y, z) <= real_c(0) && flagField->get(x, y, z) != solid) { + flagField->get(x, y, z) = gas; + } + + if (fillField->get(x, y, z) >= real_c(1)) { flagField->get(x, y, z) = liquid; } + + // not using else here to avoid overwriting solid flags + if (fillField->get(x, y, z) < real_c(1) && fillField->get(x, y, z) > real_c(0)) { + flagField->get(x, y, z) = interface; + }) // WALBERLA_FOR_ALL_CELLS_XYZ + } + + ContactAngle contactAngleObj = ContactAngle(contactAngle); + + // create timeloop + SweepTimeloop timeloop(blockForest, uint_c(1)); + + if (useTriangulation) // use local triangulation for curvature computation + { + // add sweep for computing obstacle normals in interface cells near obstacle cells + ObstacleNormalSweep< Stencil_T, FlagField_T, VectorField_T > obstNormalsSweep( + obstacleNormalFieldID, flagFieldID, interfaceFlagID, liquidInterfaceGasFlagIDs, solidID, true, false, false); + timeloop.add() << Sweep(obstNormalsSweep, "Obstacle normals sweep"); + + // add sweep for computing interface normals + NormalSweep< Stencil_T, FlagField_T, ScalarField_T, VectorField_T > normalsSweep( + normalFieldID, fillFieldID, flagFieldID, interfaceFlagID, liquidInterfaceGasFlagIDs, solidID, false, false, + true, false); + timeloop.add() << Sweep(normalsSweep, "Normal sweep"); + + // add sweep for computing curvature (including wetting) + CurvatureSweepLocalTriangulation< Stencil_T, FlagField_T, ScalarField_T, VectorField_T > curvatureSweep( + blockForest, curvatureFieldID, normalFieldID, fillFieldID, flagFieldID, obstacleNormalFieldID, interfaceFlagID, + solidID, true, contactAngleObj); + timeloop.add() << Sweep(curvatureSweep, "Curvature sweep"); + } + else // use finite differences for curvature computation + { + // add sweep for computing obstacle normals in obstacle cells + ObstacleNormalSweep< Stencil_T, FlagField_T, VectorField_T > obstNormalsSweep( + obstacleNormalFieldID, flagFieldID, interfaceFlagID, liquidInterfaceGasFlagIDs, solidID, false, true, true); + timeloop.add() << Sweep(obstNormalsSweep, "Obstacle normals sweep"); + + // add sweep for reflecting fill level into obstacle cells + ObstacleFillLevelSweep< Stencil_T, FlagField_T, ScalarField_T, VectorField_T > obstacleFillLevelSweep( + smoothFillFieldID, fillFieldID, flagFieldID, obstacleNormalFieldID, liquidInterfaceGasFlagIDs, solidID); + timeloop.add() << Sweep(obstacleFillLevelSweep, "Obstacle fill level sweep"); + + // add sweep for smoothing the fill level field (uses fill level values from obstacle cells set by + // ObstacelFillLevelSweep) + SmoothingSweep< Stencil_T, FlagField_T, ScalarField_T, VectorField_T > smoothingSweep( + smoothFillFieldID, fillFieldID, flagFieldID, liquidInterfaceGasFlagIDs, solidID, true); + timeloop.add() << Sweep(smoothingSweep, "Smoothing sweep"); + + // add sweep for computing interface normals + NormalSweep< Stencil_T, FlagField_T, ScalarField_T, VectorField_T > normalsSweep( + normalFieldID, smoothFillFieldID, flagFieldID, interfaceFlagID, liquidInterfaceGasFlagIDs, solidID, true, true, + false, true); + timeloop.add() << Sweep(normalsSweep, "Normal sweep"); + + // add sweep for computing curvature (including wetting) + CurvatureSweepFiniteDifferences< Stencil_T, FlagField_T, ScalarField_T, VectorField_T > curvatureSweep( + curvatureFieldID, normalFieldID, obstacleNormalFieldID, flagFieldID, interfaceFlagID, + liquidInterfaceGasFlagIDs, solidID, true, contactAngleObj); + timeloop.add() << Sweep(curvatureSweep, "Curvature sweep"); + } + + // run one time step + timeloop.singleStep(); + + // get the curvature in cell (2, 1, 2) + real_t curvature = real_c(0); + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + const ScalarField_T* const curvatureField = blockIt->getData< const ScalarField_T >(curvatureFieldID); + curvature = curvatureField->get(2, 1, 2); + } + + // auto vtkFlagField = + // field::createVTKOutput< FlagField_T >(flagFieldID, *blockForest, "flag_field", uint_c(1), uint_c(0)); + // vtkFlagField(); + // auto vtkFillField = + // field::createVTKOutput< ScalarField_T >(fillFieldID, *blockForest, "fill_field", uint_c(1), uint_c(0)); + // vtkFillField(); + // auto vtkCurvatureField = + // field::createVTKOutput< ScalarField_T >(curvatureFieldID, *blockForest, "curvature_field", uint_c(1), + // uint_c(0)); + // vtkCurvatureField(); + // auto vtkNormalField = + // field::createVTKOutput< VectorField_T >(normalFieldID, *blockForest, "normal_field", uint_c(1), uint_c(0)); + // vtkNormalField(); + // auto vtkObstNormalField = field::createVTKOutput< VectorField_T >(obstacleNormalFieldID, *blockForest, + // "obst_normal_field", uint_c(1), uint_c(0)); + // vtkObstNormalField(); + // auto vtkSmoothFillField = field::createVTKOutput< ScalarField_T >(smoothFillFieldID, *blockForest, + // "smooth_fill_field", uint_c(1), uint_c(0)); + // vtkSmoothFillField(); + + return curvature; +} + +// the following values have been obtained with a version of the local triangulation curvature computation algorithm +// that is assumed to be correct (the angles computed in computeArtificalWallPoint() have been manually verified with +// ParaView for some of the values) +std::vector< std::pair< real_t, real_t > > expectedSolutionTriangulation() +{ + // pair contains: [0] contact angle, [1] expected curvature + std::vector< std::pair< real_t, real_t > > testcases; + + testcases.emplace_back(real_c(0), real_c(-0.208893)); + testcases.emplace_back(real_c(1), real_c(-0.208832)); + testcases.emplace_back(real_c(10), real_c(-0.202813)); + testcases.emplace_back(real_c(30), real_c(-0.15704)); + testcases.emplace_back(real_c(45), real_c(-0.0994945)); + testcases.emplace_back(real_c(65), real_c(-0.00311236)); + testcases.emplace_back(real_c(65.5), real_c(-0.000512801)); + + // IMPORTANT REMARK REGARDING THE CHANGE IN THE SIGN OF THE CURVATURE: + // A change in the sign of the curvature would only be expected when the specified contact angle is equivalent to the + // angle that is already present. However, the algorithm ensures that the constructed virtual wall point is valid + // during curvature computation (such that the computed triangle by local triangulation is not degenerated). + // Therefore, the algorithm internally changes the target contact angle (see lines 10 to 17 in algorithm 6.2 in + // dissertation of S. Donath). The specified contact angle will then slowly be approached in subsequent time steps. + + testcases.emplace_back(real_c(65.8), real_c(0.00105015)); + testcases.emplace_back(real_c(66), real_c(0.00209344)); + testcases.emplace_back(real_c(66.5), real_c(0.00470618)); + testcases.emplace_back(real_c(67), real_c(0.00732526)); + testcases.emplace_back(real_c(70), real_c(0.0231628)); + testcases.emplace_back(real_c(75), real_c(0.0499505)); + testcases.emplace_back(real_c(77), real_c(0.0607714)); + testcases.emplace_back(real_c(78), real_c(0.0661992)); + + // this contact angle is already reached by the initial setup + // => the algorithm should take the route with if(realIsEqual(...)) in CurvatureSweepTR + // => this route has been found to cause problems when using single precision (see comment in CurvatureSweepTR()) +#ifdef WALBERLA_DOUBLE_ACCURACY + testcases.emplace_back(real_c(78.40817854), real_c(0.0684177)); +#endif + + testcases.emplace_back(real_c(80), real_c(0.0770832)); + testcases.emplace_back(real_c(90), real_c(0.131739)); + testcases.emplace_back(real_c(120), real_c(0.287743)); + testcases.emplace_back(real_c(160), real_c(0.431406)); + testcases.emplace_back(real_c(180), real_c(0.312837)); + + return testcases; +} + +// the following values have been obtained with a version of the finite difference curvature computation algorithm +// that is assumed to be correct +std::vector< std::pair< real_t, real_t > > expectedSolutionFiniteDifferences() +{ + std::vector< std::pair< real_t, real_t > > testcases; + + testcases.emplace_back(real_c(0), real_c(-0.0454018)); + testcases.emplace_back(real_c(1), real_c(-0.0474911)); + testcases.emplace_back(real_c(10), real_c(-0.0641205)); + testcases.emplace_back(real_c(30), real_c(-0.0810346)); + testcases.emplace_back(real_c(45), real_c(-0.0622399)); + testcases.emplace_back(real_c(65), real_c(0.0397875)); + testcases.emplace_back(real_c(65.5), real_c(0.0436974)); + testcases.emplace_back(real_c(65.8), real_c(0.046073)); + testcases.emplace_back(real_c(66), real_c(0.0476689)); + testcases.emplace_back(real_c(66.5), real_c(0.0517007)); + testcases.emplace_back(real_c(67), real_c(0.0557916)); + testcases.emplace_back(real_c(70), real_c(0.0814937)); + testcases.emplace_back(real_c(75), real_c(0.127884)); + testcases.emplace_back(real_c(77), real_c(0.147265)); + testcases.emplace_back(real_c(78), real_c(0.157051)); + testcases.emplace_back(real_c(78.40817854), real_c(0.161057)); + testcases.emplace_back(real_c(80), real_c(0.176711)); + testcases.emplace_back(real_c(90), real_c(0.271672)); + testcases.emplace_back(real_c(120), real_c(0.448388)); + testcases.emplace_back(real_c(160), real_c(0.487812)); + testcases.emplace_back(real_c(180), real_c(0.467033)); + + return testcases; +} + +int main(int argc, char** argv) +{ + debug::enterTestMode(); + Environment env(argc, argv); + + // test with local triangulation curvature computation + std::vector< std::pair< real_t, real_t > > testcases = expectedSolutionTriangulation(); + for (const auto& i : testcases) + { + WALBERLA_LOG_INFO_ON_ROOT("Testing contact angle=" << i.first + << " with local triangulation curvature computation"); + const real_t curvature = computeCurvature(i.first, true); + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(curvature, i.second, real_c(1e-5)); + } + + // test with finite difference curvature computation + testcases = expectedSolutionFiniteDifferences(); + for (const auto& i : testcases) + { + WALBERLA_LOG_INFO_ON_ROOT("Testing contact angle=" << i.first << " with finite difference curvature computation"); + const real_t curvature = computeCurvature(i.first, false); + WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(curvature, i.second, real_c(1e-6)); + } + + return EXIT_SUCCESS; +} +} // namespace WettingCurvatureTest +} // namespace free_surface +} // namespace walberla + +int main(int argc, char** argv) { return walberla::free_surface::WettingCurvatureTest::main(argc, argv); } \ No newline at end of file -- GitLab