diff --git a/apps/benchmarks/ComplexGeometry/ComplexGeometry.cpp b/apps/benchmarks/ComplexGeometry/ComplexGeometry.cpp index a9cca407c81d4ff61b905bb58abc754b880b51f5..0f436644770816f6d9fb1ae2b3897e81106c2555 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 6330a83bdde5027a2d0fd8e030862b7bf521dcd4..c6f0534cc6679f996777742823e5901aca8233f4 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 0000000000000000000000000000000000000000..00ae3e9f495e5ca7f465dcfff85100288eac5842 --- /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 0000000000000000000000000000000000000000..c6e7d413c5ef6b864f59f6c521542e6ff825a0e4 --- /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 0000000000000000000000000000000000000000..0e6edd5b2290c74048a0fcc2c34c8f4423c00178 --- /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 0000000000000000000000000000000000000000..d36003bc023db76f5b65c256c02857040452d278 --- /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 0000000000000000000000000000000000000000..8d172cadd60e0b0ed616fb4c724c814a2ca23dde --- /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 0000000000000000000000000000000000000000..129cda8de97d38b1c4d0b48614d0a09ced8f70ea --- /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 0000000000000000000000000000000000000000..de81d688e114b52e226f3c0b42b3f599b31bf33b --- /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 0000000000000000000000000000000000000000..e3a4a5e4e0ac257caedac82631069b64e6142e0d --- /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 0000000000000000000000000000000000000000..34fdca4b018ce9af4e4f80cf285ee545845246d2 --- /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 0000000000000000000000000000000000000000..143885a05538da4452ff2cff491e9a010fc39de2 --- /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 0000000000000000000000000000000000000000..07afacce7480bf9a0307180e4f5da17c1deebbbe --- /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 0000000000000000000000000000000000000000..20445deadacfbdda832dac862bf997006dd82066 --- /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 0000000000000000000000000000000000000000..bdfbe4e1ee632724634ad98b8ae2dfd5a8b1cd09 --- /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 0000000000000000000000000000000000000000..6faca3c70eb392845872bfb2b4e701519db7326d --- /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 0000000000000000000000000000000000000000..363a59c84abbc665dde520ebe4c1c1c89136874e --- /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 0000000000000000000000000000000000000000..a6b41aea0bc98dfd558dabe42f12a50675263df3 --- /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 0000000000000000000000000000000000000000..22f474fcfbc9011c21311c48a77b097f69f51410 --- /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 0000000000000000000000000000000000000000..478ee193c7ba3c70bb6f9eb8e3b91ae9ce2b307e --- /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 0000000000000000000000000000000000000000..184963db1f00e71bb9a3557987d5415a076de5b4 --- /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 0000000000000000000000000000000000000000..3dabdf5da79d90ede7a87400c858f8230b9ebe47 --- /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 0000000000000000000000000000000000000000..5d483426beb659bfe93db4ee94fe595e29c67255 --- /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 0000000000000000000000000000000000000000..3ec4eea3d58f64688d26d6a4d58c439eaeb962f9 --- /dev/null +++ b/apps/showcases/FreeSurface/TaylorBubble.cpp @@ -0,0 +1,493 @@ +//====================================================================================================================== +// +// 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 0000000000000000000000000000000000000000..a2b289aeeb75b6a9e2c57819602b595c755da687 --- /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 780cfb9de146f2eb82c9dcee247b6b47d34cd3e8..9ac7590fae38f92b276939cb2e839b4318daaa1d 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 730bf592867b6d2b7abbf134300431b3de25bfbf..02b5f3937216ea5345368505ef8664ec53b1eb34 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 63a650dbd0dd11ef5a9ef6f03564131c8efe0e0a..8f41297b78a1ff66d4cc7a9f39f98692d11d1b49 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 d031b53d309dbd5c3a848bb1ba26e9022b8606d5..10c897956d7fe466bcf109e49a8448e8f4b4d56d 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 511aafa84a04ca44385c22acba0cd02891640cc3..829e0505b5ff2d832c87aac94e39f93e1d3961b9 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 0000000000000000000000000000000000000000..a39e2f18741edaabe67ef1f946974f625f102bc6 --- /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 0000000000000000000000000000000000000000..42bffe263761d7c541a43ebff4b21e17eac168c1 --- /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 0000000000000000000000000000000000000000..1e9c69ed1c1bd38efd71f22775199ef5693713b2 --- /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/all.h b/src/lbm/boundary/all.h index 31709c60e0e538dddaaf537fe0d661fb2b4833c1..009c88888f8b30b7107d2a210a0f0df31ed74b0b 100644 --- a/src/lbm/boundary/all.h +++ b/src/lbm/boundary/all.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/free_surface/BlockStateDetectorSweep.h b/src/lbm/free_surface/BlockStateDetectorSweep.h new file mode 100644 index 0000000000000000000000000000000000000000..f2ef50bd14813126e512dc42a00ff225892af86c --- /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 0000000000000000000000000000000000000000..54b500251a5be6f3e48f70addb53a4e5fd090a69 --- /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 0000000000000000000000000000000000000000..4dd93303a0f79fc445ad09e5709cd57e89215b98 --- /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 0000000000000000000000000000000000000000..e332cf8dce916946fc1f36a18c3ce4aa0b8864d9 --- /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 0000000000000000000000000000000000000000..574001ea45642a76efc5dbb48d6a5d2b7cce6580 --- /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 0000000000000000000000000000000000000000..b98f55d54dbb411fa877e2a95690265fb6b4c80f --- /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 0000000000000000000000000000000000000000..ef113e327032695d73df64109f0581c3b03cc4bd --- /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 0000000000000000000000000000000000000000..c9df54478e5c4af223f0be17fe128f86427f21ed --- /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 0000000000000000000000000000000000000000..a3e2b0cb287e1a9ad427869047a0404eafd89ce1 --- /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 0000000000000000000000000000000000000000..334f9d714d2e3dc3f6eb4a465aa9f3606a775a22 --- /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 0000000000000000000000000000000000000000..192ec875594b98dc87bff0e1418c200544965590 --- /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 0000000000000000000000000000000000000000..f1ff2b93c6e3fdde748c1cf2b579837f37542cc1 --- /dev/null +++ b/src/lbm/free_surface/VtkWriter.h @@ -0,0 +1,172 @@ +//====================================================================================================================== +// +// 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)); + + 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 0000000000000000000000000000000000000000..9b3f1195a0381d0d6cbf3137539a305863dde5d1 --- /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 0000000000000000000000000000000000000000..0279bd4b9c445260180c8e61d9ae293db71acde9 --- /dev/null +++ b/src/lbm/free_surface/boundary/FreeSurfaceBoundaryHandling.h @@ -0,0 +1,184 @@ +//====================================================================================================================== +// +// 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 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, + 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 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 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 0000000000000000000000000000000000000000..38f62011908bc4c71049ed42377264509724df50 --- /dev/null +++ b/src/lbm/free_surface/boundary/FreeSurfaceBoundaryHandling.impl.h @@ -0,0 +1,540 @@ +//====================================================================================================================== +// +// 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::FreeSlip_T freeSlip(B::freeSlipBoundaryID, B::freeSlipFlagID, pdfField, flagField, domainMask); + + return new BoundaryHandling_T("Boundary Handling", flagField, domainMask, noslip, ubb, ubbInflow, pressure, + pressureOutflow, outlet, 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; + + // initialize outflowIDs + Set< FlagUID > outflowIDs; + outflowIDs += pressureOutflowFlagID; + outflowIDs += outletFlagID; + + // 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 >::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 >::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 0000000000000000000000000000000000000000..534d965f9ea15cb34554e70297b0133c45beed69 --- /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 0000000000000000000000000000000000000000..bb3b9a074f28256fd3693cee690be43f7134dfb4 --- /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 0000000000000000000000000000000000000000..b67397be121de63912a58f2c8787ae4895dc8e46 --- /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 0000000000000000000000000000000000000000..8f6a24a687f3950c76eda732c405ba6c0c80c118 --- /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 0000000000000000000000000000000000000000..05b0f0b23dc1c235c1cc72c3902722ac55441e88 --- /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 0000000000000000000000000000000000000000..a22d540eaac0904f62d7652c0f2c5e91c66a900c --- /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 0000000000000000000000000000000000000000..7ee915c2a5517379c1c13b605933740f9ff24127 --- /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 0000000000000000000000000000000000000000..dfd86821b9828fbd2a1791469f11a43643e0f506 --- /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 0000000000000000000000000000000000000000..4ffa219cb59d7a121cf568369f8e866571e35114 --- /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 0000000000000000000000000000000000000000..712930e3823899d801b4b1b60b16cbdf4cf9b478 --- /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 0000000000000000000000000000000000000000..278536feee3fb0df7213959528365980037b105b --- /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 0000000000000000000000000000000000000000..6f8e4221816332bb82e1d6c61230a1dedb343b33 --- /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 0000000000000000000000000000000000000000..db90cae4cda8d7dd3dad37b75338fb43696ccbd9 --- /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 0000000000000000000000000000000000000000..72efc7ca9c387c02b4d1f7479f183ef318f64dd4 --- /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 0000000000000000000000000000000000000000..b23d4d774e9376bf104198c64ea6f6394c41f77e --- /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 0000000000000000000000000000000000000000..96f1c0361466b96fcf71ac093acee0e14fa57415 --- /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 0000000000000000000000000000000000000000..7201a632eaea0df4d18a4e1884c02059ecf40796 --- /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 0000000000000000000000000000000000000000..9b0fd0dbaa1fef4ea6d969181d53e68f73f36d61 --- /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 0000000000000000000000000000000000000000..15d6f6bd8ba75e3fa9473551907b87cd20a79c8f --- /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 0000000000000000000000000000000000000000..d8ce84faaa5fdf2d34b09ef353536cf164b4ce3d --- /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 0000000000000000000000000000000000000000..f36c504a494c6abb8324aa999754339d82050e73 --- /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 0000000000000000000000000000000000000000..0f5e831239520354ed8f5adc45ea5c1a4b2f8207 --- /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 0000000000000000000000000000000000000000..c0f75536a9346c890729971d734383850bcc8fb5 --- /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 0000000000000000000000000000000000000000..458fabdfe2354a6879e1c826fa2577b0b7cbc866 --- /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 0000000000000000000000000000000000000000..4e913b657c1c2c9727ce2976a145db82e36dc68a --- /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 0000000000000000000000000000000000000000..4eb74cedef75e52794aa801080244a9e4c549fd6 --- /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 0000000000000000000000000000000000000000..2e4d0b798ca3fb8bf6e325f38abfaa30119291fc --- /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 0000000000000000000000000000000000000000..ab9efe397121b33391eceedefc407811aec16ac8 --- /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 0000000000000000000000000000000000000000..ba30c8445d19276136859641d97673fc8faff98c --- /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 0000000000000000000000000000000000000000..4a13fb3b41dd7cce966776acd4db6f79a10788c7 --- /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 0000000000000000000000000000000000000000..9468f06b33e271ee6e014722e333bb9e5125c363 --- /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 0000000000000000000000000000000000000000..ffd86279ceee8e81b9e6f51e80b4a04dc388ed9a --- /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 0000000000000000000000000000000000000000..8c1c745130aa0f37bb3fe353dc3251bc44e02801 --- /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 0000000000000000000000000000000000000000..cc5c382e5bd5f1fe91374d608f306bdbd54aed24 --- /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 0000000000000000000000000000000000000000..ec5ca220d3d9d3cfa5dcc20de3ba02b3dd0f4155 --- /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 0000000000000000000000000000000000000000..c0489961ab14243c568471d1c3d48621fb5dacc0 --- /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 0000000000000000000000000000000000000000..5e198e2db93fe707297fa8360e95a63a9005c665 --- /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 0000000000000000000000000000000000000000..357412052a8cdfc0d8edab2f9a71cd2e3144dba7 --- /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 0000000000000000000000000000000000000000..6445a61f6df00edc768aa8b4bc5e96cf33c2184c --- /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 0000000000000000000000000000000000000000..36c313ee2a0027498a3d758b6378603ed160a797 --- /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 0000000000000000000000000000000000000000..187528a6c08c3faf479a7e557e799fc031967f75 --- /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 0000000000000000000000000000000000000000..b0f45113c95155be79c6118406bcd911890b9ffa --- /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 0000000000000000000000000000000000000000..42fb177b77f77d2fa745866c7f5ef2cb677ea00c --- /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 0000000000000000000000000000000000000000..55f26d5ce82404d1e420440ce482d78178bcfd84 --- /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 0000000000000000000000000000000000000000..dd245dacfaeeb8ac5aaab3a07495c3e107d5d335 --- /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 0000000000000000000000000000000000000000..0aa62fc3c53e3f538607c7f8cfa64b28fde44699 --- /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 0000000000000000000000000000000000000000..dfe53545c609478d5543e6a86ba328137c49b369 --- /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 0000000000000000000000000000000000000000..f6d46d10334e86a23e20593a06de5af1b37e5b2b --- /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 0000000000000000000000000000000000000000..f4d69e0afc47fd98952f69344fd646f64e976e9a --- /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 0000000000000000000000000000000000000000..8d49e0c963fd6aaa96fc85d6abaa782a8600cf1e --- /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 0000000000000000000000000000000000000000..309bc79bb5c9868243c156e04f462f1e649cf790 --- /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 0000000000000000000000000000000000000000..75cc8b6d91ce5aa8a781fec2b83dae75a93231d9 --- /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 0000000000000000000000000000000000000000..a1ade27ec0021d29497258101212e29c06ad9395 --- /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 0000000000000000000000000000000000000000..c5bd8c858c0699580818d3388fea8291adf86f78 --- /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 0000000000000000000000000000000000000000..7d4182022d5fdb135f6c74619938a19a347be828 --- /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 0000000000000000000000000000000000000000..cb1c6b01dc1cec8f14a51e6a247074c1de5d450b --- /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 0000000000000000000000000000000000000000..6d3b722580e0dd573834754581d8b176c827374c --- /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 0000000000000000000000000000000000000000..9b76ffd20336cac435d7b2cb96d76fbcee5cb090 --- /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 0000000000000000000000000000000000000000..3b61d23d248562c5c27a7d5aab330142de8944b8 --- /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 0000000000000000000000000000000000000000..f0f21c2e6b162c19bba9756a33e6b5ed838eb681 --- /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 0000000000000000000000000000000000000000..9b1582c665e3173207918a27154afaedeb500dd9 --- /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 0000000000000000000000000000000000000000..7be227e8cc98ae75ad3b14d3044a37accd910c3b --- /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 d34cb0684b80ba7292dcf616c5c9a25cf2efd98f..bb8d6dd57faf11a0e0bf97523190bd2da8b39e46 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 0000000000000000000000000000000000000000..455dc2e17455b691ddbaa24a6fb4f4a069c08a93 --- /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 0000000000000000000000000000000000000000..f36124fcd685f00c2b9f13df7ad5fb8827807a7b --- /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 0000000000000000000000000000000000000000..f06d6ed6845e07173d04a497916975ea9d4ab659 --- /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 0000000000000000000000000000000000000000..08641c5b839a4e7f6e38ff3a71ce97c1966fd612 --- /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 0000000000000000000000000000000000000000..f70d7147696bed18eef2f98bb8361bdecbe4da0a --- /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 0000000000000000000000000000000000000000..230655c16e475d6dd624420fed9781eff54123a3 --- /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 0000000000000000000000000000000000000000..b9dab07637351533d04a823070ddbe659721d0be --- /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 Binary files /dev/null and b/tests/lbm/free_surface/bubble_model/MergeAndSplitTestConnected.png differ 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 Binary files /dev/null and b/tests/lbm/free_surface/bubble_model/MergeAndSplitTestUnconnected.png differ 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 0000000000000000000000000000000000000000..84e559b610e90ce9cc353453fc577cc77ba9a560 --- /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 0000000000000000000000000000000000000000..36f09079f29978b00fddbffad4b02ab883ea2340 --- /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 0000000000000000000000000000000000000000..6a41cd8e919c401c75c9b1379f4f84c97a58326c --- /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 0000000000000000000000000000000000000000..adfbce45ff63ba2bdf32bcb484e41fd87eb264be --- /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 0000000000000000000000000000000000000000..1c3acc672ca28f1d5ed59fbf33bee4644a276d06 --- /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 0000000000000000000000000000000000000000..6f7645c8437ae6de25eb5e4a51defa3385aac3e4 --- /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 0000000000000000000000000000000000000000..a141a0a3c8ea4f2032192383cb3ea27209f40a6f --- /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 0000000000000000000000000000000000000000..69c075f9eb27626d2708301b296e29b114c3b6e4 --- /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 0000000000000000000000000000000000000000..8c99cf8726ecd46fff29da30d9706edebb45ffcc --- /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 Binary files /dev/null and b/tests/lbm/free_surface/dynamics/ExcessMassDistributionParallelTest.ods differ diff --git a/tests/lbm/free_surface/dynamics/InflowTest.cpp b/tests/lbm/free_surface/dynamics/InflowTest.cpp new file mode 100644 index 0000000000000000000000000000000000000000..da8b5df18d1207957ca33bc9f14327c17caf3279 --- /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 0000000000000000000000000000000000000000..0ef55dce4bf764a8f60d310e24427593c1de7834 --- /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 0000000000000000000000000000000000000000..15ff0d96d078da1d3e33c958dc67f04b628c1795 --- /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 0000000000000000000000000000000000000000..2638379cd25f67ed3343f462e21fda35d1127c69 --- /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 0000000000000000000000000000000000000000..ae4ec83d12985f40aa95482eac3c2c6380d2884a --- /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 Binary files /dev/null and b/tests/lbm/free_surface/dynamics/PdfRefillingTest.ods differ diff --git a/tests/lbm/free_surface/dynamics/WettingConversionTest.cpp b/tests/lbm/free_surface/dynamics/WettingConversionTest.cpp new file mode 100644 index 0000000000000000000000000000000000000000..8a2590c6983bd65f0e640a7288c0a09ba4f30ab2 --- /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 0000000000000000000000000000000000000000..24fcc259bd67fd9ac1a1b736f67ab261e06d46fd --- /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 0000000000000000000000000000000000000000..9220f802244306482e9c796bb2946171a56f7a1a --- /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 0000000000000000000000000000000000000000..1a42213e6188b2db7e74a6dabdd9066bd4c00326 --- /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 0000000000000000000000000000000000000000..4c86d76ad67cdbc9a3db4544b9eac2e82808d1fc --- /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 0000000000000000000000000000000000000000..7123e9ba797fc84c708ac0f8f0ac2d82a54de755 --- /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 0000000000000000000000000000000000000000..c7b5fbd3f32333465b5fc5d91dfcd4ba4433982a --- /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 0000000000000000000000000000000000000000..8491557fee4e6d3587284ba73436ea4a1893ed29 --- /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 0000000000000000000000000000000000000000..3ca66b6f5a33ab6b71738da0e721ddc542643c06 --- /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 0000000000000000000000000000000000000000..ca3c6b4f1f802ac224d1ae94b64bcbd7d52a2c56 --- /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 0000000000000000000000000000000000000000..6ccbff11bc22d74015706c8406113da9206c4e73 --- /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 0000000000000000000000000000000000000000..0d45f6685679d9ff08a018049a4ddacfc56ae048 --- /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 0000000000000000000000000000000000000000..2bcff6248899965c14434f594187204c633c0a0e --- /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