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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 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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 O@jYGrT-G@yGywp$4)bFG 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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 F0|1E%*6{!U 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