From 473a6ecc8c98c8158fa83dabd931e1ec56c701de Mon Sep 17 00:00:00 2001 From: Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> Date: Wed, 22 Feb 2023 10:40:48 +0100 Subject: [PATCH] Free surface mass advection benchmark --- apps/benchmarks/CMakeLists.txt | 1 + .../FreeSurfaceAdvection/CMakeLists.txt | 13 + .../FreeSurfaceAdvection/DeformationField.cpp | 351 ++++++++++++++++++ .../FreeSurfaceAdvection/DeformationField.prm | 96 +++++ .../FreeSurfaceAdvection/SingleVortex.cpp | 351 ++++++++++++++++++ .../FreeSurfaceAdvection/SingleVortex.prm | 96 +++++ .../FreeSurfaceAdvection}/ZalesakDisk.cpp | 185 +++++---- .../FreeSurfaceAdvection}/ZalesakDisk.prm | 32 +- .../functionality/AdvectSweep.h | 152 ++++++++ .../functionality/AdvectionDynamicsHandler.h | 254 +++++++++++++ .../functionality/GeometricalErrorEvaluator.h | 139 +++++++ .../FreeSurface/BubblyPoiseuille.cpp | 19 +- .../FreeSurface/BubblyPoiseuille.prm | 1 + apps/showcases/FreeSurface/CMakeLists.txt | 8 +- apps/showcases/FreeSurface/CapillaryWave.cpp | 15 +- .../FreeSurface/DamBreakCylindrical.cpp | 20 +- .../FreeSurface/DamBreakRectangular.cpp | 19 +- apps/showcases/FreeSurface/DropImpact.cpp | 19 +- apps/showcases/FreeSurface/DropImpact.prm | 1 + apps/showcases/FreeSurface/DropWetting.cpp | 12 +- apps/showcases/FreeSurface/GravityWave.cpp | 17 +- .../FreeSurface/GravityWaveCodegen.cpp | 17 +- apps/showcases/FreeSurface/MovingDrop.cpp | 18 +- apps/showcases/FreeSurface/MovingDrop.prm | 20 +- apps/showcases/FreeSurface/RisingBubble.cpp | 16 +- apps/showcases/FreeSurface/TaylorBubble.cpp | 21 +- src/lbm/free_surface/TotalMassComputer.h | 26 +- src/lbm/free_surface/VtkWriter.h | 35 +- .../dynamics/ExcessMassDistributionModel.h | 68 ++-- .../dynamics/ExcessMassDistributionSweep.h | 7 +- .../ExcessMassDistributionSweep.impl.h | 89 +++-- .../dynamics/SurfaceDynamicsHandler.h | 6 +- .../ExcessMassDistributionParallelTest.cpp | 128 ++++++- .../ExcessMassDistributionParallelTest.ods | Bin 16650 -> 19371 bytes 34 files changed, 1994 insertions(+), 258 deletions(-) create mode 100644 apps/benchmarks/FreeSurfaceAdvection/CMakeLists.txt create mode 100644 apps/benchmarks/FreeSurfaceAdvection/DeformationField.cpp create mode 100644 apps/benchmarks/FreeSurfaceAdvection/DeformationField.prm create mode 100644 apps/benchmarks/FreeSurfaceAdvection/SingleVortex.cpp create mode 100644 apps/benchmarks/FreeSurfaceAdvection/SingleVortex.prm rename apps/{showcases/FreeSurface => benchmarks/FreeSurfaceAdvection}/ZalesakDisk.cpp (65%) rename apps/{showcases/FreeSurface => benchmarks/FreeSurfaceAdvection}/ZalesakDisk.prm (63%) create mode 100644 apps/benchmarks/FreeSurfaceAdvection/functionality/AdvectSweep.h create mode 100644 apps/benchmarks/FreeSurfaceAdvection/functionality/AdvectionDynamicsHandler.h create mode 100644 apps/benchmarks/FreeSurfaceAdvection/functionality/GeometricalErrorEvaluator.h diff --git a/apps/benchmarks/CMakeLists.txt b/apps/benchmarks/CMakeLists.txt index 2adab4750..3f5e6a95a 100644 --- a/apps/benchmarks/CMakeLists.txt +++ b/apps/benchmarks/CMakeLists.txt @@ -4,6 +4,7 @@ add_subdirectory( ComplexGeometry ) add_subdirectory( DEM ) add_subdirectory( MeshDistance ) add_subdirectory( CouetteFlow ) +add_subdirectory( FreeSurfaceAdvection ) add_subdirectory( FluidParticleCoupling ) add_subdirectory( FluidParticleCouplingWithLoadBalancing ) add_subdirectory( ForcesOnSphereNearPlaneInShearFlow ) diff --git a/apps/benchmarks/FreeSurfaceAdvection/CMakeLists.txt b/apps/benchmarks/FreeSurfaceAdvection/CMakeLists.txt new file mode 100644 index 000000000..e14b5fe37 --- /dev/null +++ b/apps/benchmarks/FreeSurfaceAdvection/CMakeLists.txt @@ -0,0 +1,13 @@ +waLBerla_link_files_to_builddir( *.prm ) + +waLBerla_add_executable(NAME DeformationField + FILES DeformationField.cpp + DEPENDS blockforest boundary core domain_decomposition field lbm postprocessing timeloop vtk) + +waLBerla_add_executable(NAME SingleVortex + FILES SingleVortex.cpp + DEPENDS blockforest boundary core domain_decomposition field lbm postprocessing timeloop vtk) + +waLBerla_add_executable(NAME ZalesakDisk + FILES ZalesakDisk.cpp + DEPENDS blockforest boundary core domain_decomposition field lbm postprocessing timeloop vtk) \ No newline at end of file diff --git a/apps/benchmarks/FreeSurfaceAdvection/DeformationField.cpp b/apps/benchmarks/FreeSurfaceAdvection/DeformationField.cpp new file mode 100644 index 000000000..fcbc4ca54 --- /dev/null +++ b/apps/benchmarks/FreeSurfaceAdvection/DeformationField.cpp @@ -0,0 +1,351 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla is free software: you can +// redistribute it and/or modify it under the terms of the GNU General Public +// License as published by the Free Software Foundation, either version 3 of +// the License, or (at your option) any later version. +// +// waLBerla is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +// for more details. +// +// You should have received a copy of the GNU General Public License along +// with waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file DeformationField.cpp +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +// +// This benchmark simulates the advection of a spherical bubble in a vortex field with very high deformation. The vortex +// field changes periodically so that the bubble returns to its initial position, where it should take its initial form. +// The relative geometrical error of the bubble's shape after one period is evaluated. There is no LBM flow simulation +// performed here. It is a test case for the FSLBM's mass advection only. This benchmark is based on Viktor Haag's +// master thesis (https://www10.cs.fau.de/publications/theses/2017/Haag_MT_2017.pdf). +//====================================================================================================================== + +#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/surface_geometry/SurfaceGeometryHandler.h" +#include "lbm/free_surface/surface_geometry/Utility.h" +#include "lbm/lattice_model/D3Q19.h" + +#include "functionality/AdvectionDynamicsHandler.h" +#include "functionality/GeometricalErrorEvaluator.h" + +namespace walberla +{ +namespace free_surface +{ +namespace DeformationField +{ +using ScalarField_T = GhostLayerField< real_t, 1 >; +using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >; + +// Lattice model is only created for dummy purposes; no LBM simulation is performed +using CollisionModel_T = lbm::collision_model::SRT; +using LatticeModel_T = lbm::D3Q19< CollisionModel_T, true >; +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 >; + +// function describing the initialization velocity profile (in global cell coordinates) +inline Vector3< real_t > velocityProfile(Cell globalCell, real_t timePeriod, uint_t timestep, + const Vector3< real_t >& domainSize) +{ + // add 0.5 to get the cell's center + const real_t x = (real_c(globalCell.x()) + real_c(0.5)) / domainSize[0]; + const real_t y = (real_c(globalCell.y()) + real_c(0.5)) / domainSize[1]; + const real_t z = (real_c(globalCell.z()) + real_c(0.5)) / domainSize[2]; + + const real_t timeTerm = real_c(std::cos(math::pi * real_t(timestep) / timePeriod)); + + const real_t sinpix = real_c(std::sin(math::pi * x)); + const real_t sinpiy = real_c(std::sin(math::pi * y)); + const real_t sinpiz = real_c(std::sin(math::pi * z)); + + const real_t sin2pix = real_c(std::sin(real_c(2) * math::pi * x)); + const real_t sin2piy = real_c(std::sin(real_c(2) * math::pi * y)); + const real_t sin2piz = real_c(std::sin(real_c(2) * math::pi * z)); + + const real_t velocityX = real_c(2) * sinpix * sinpix * sin2piy * sin2piz * timeTerm; + const real_t velocityY = -sin2pix * sinpiy * sinpiy * sin2piz * timeTerm; + const real_t velocityZ = -sin2pix * sin2piy * sinpiz * sinpiz * timeTerm; + + return Vector3< real_t >(velocityX, velocityY, velocityZ); +} + +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"); + + // get domain parameters from parameter file + auto domainParameters = walberlaEnv.config()->getOneBlock("DomainParameters"); + const uint_t domainWidth = domainParameters.getParameter< uint_t >("domainWidth"); + + const real_t bubbleDiameter = real_c(domainWidth) * real_c(0.3); + const Vector3< real_t > bubbleCenter = domainWidth * Vector3< real_t >(real_c(0.35), real_c(0.35), real_c(0.35)); + + // define domain size + Vector3< uint_t > domainSize; + domainSize[0] = domainWidth; + domainSize[1] = domainWidth; + domainSize[2] = domainWidth; + + // 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 + const 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(periodicity); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(bubbleDiameter); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(bubbleCenter); + + // get physics parameters from parameter file + auto physicsParameters = walberlaEnv.config()->getOneBlock("PhysicsParameters"); + const uint_t timesteps = physicsParameters.getParameter< uint_t >("timesteps"); + const uint_t timestepsToInitialPosition = physicsParameters.getParameter< uint_t >("timestepsToInitialPosition"); + + // compute CFL number + const real_t dx_SI = real_c(1) / real_c(domainWidth); + const real_t dt_SI = real_c(3) / real_c(timestepsToInitialPosition); + const real_t CFL = dt_SI / dx_SI; // with velocity_SI = 1 + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(CFL); + + // dummy collision model (LBM not simulated in this benchmark) + const CollisionModel_T collisionModel = CollisionModel_T(real_c(1)); + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(timestepsToInitialPosition); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(timesteps); + + // 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 excessMassDistributionModel = + modelParameters.getParameter< std::string >("excessMassDistributionModel"); + const std::string curvatureModel = modelParameters.getParameter< std::string >("curvatureModel"); + 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"); + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(pdfReconstructionModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(excessMassDistributionModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(curvatureModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(useSimpleMassExchange); + 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"); + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(performanceLogFrequency); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(evaluationFrequency); + + // create non-uniform block forest (non-uniformity required for load balancing) + const std::shared_ptr< StructuredBlockForest > blockForest = + createNonUniformBlockForest(domainSize, cellsPerBlock, numBlocks, periodicity); + + // create lattice model + const LatticeModel_T latticeModel = LatticeModel_T(collisionModel); + + // add pdf field + const BlockDataID pdfFieldID = lbm::addPdfFieldToStorage(blockForest, "PDF field", latticeModel, field::fzyx); + + // add fill level field (initialized with 1, i.e., liquid 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 the velocity profile + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + PdfField_T* const pdfField = blockIt->getData< PdfField_T >(pdfFieldID); + FlagField_T* const flagField = blockIt->getData< FlagField_T >(flagFieldID); + + WALBERLA_FOR_ALL_CELLS(pdfFieldIt, pdfField, flagFieldIt, flagField, { + // cell in block-local coordinates + const Cell localCell = pdfFieldIt.cell(); + + // get cell in global coordinates + Cell globalCell = pdfFieldIt.cell(); + blockForest->transformBlockLocalToGlobalCell(globalCell, *blockIt, localCell); + + // set velocity profile (CFL_SI = CFL_LBM = velocity_LBM * dt_LBM / dx_LBM = velocity_LBM) + const Vector3< real_t > initialVelocity = + CFL * velocityProfile(globalCell, real_c(timestepsToInitialPosition), uint_c(0), domainSize); + pdfField->setDensityAndVelocity(localCell, initialVelocity, real_c(1)); + }) // WALBERLA_FOR_ALL_CELLS + } + + // create the spherical bubble + const geometry::Sphere sphereBubble(real_c(domainWidth) * Vector3< real_t >(real_c(0.5), real_c(0.75), real_c(0.25)), + real_c(domainWidth) * real_c(0.15)); + bubble_model::addBodyToFillLevelField< geometry::Sphere >(*blockForest, fillFieldID, sphereBubble, true); + + // 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); + communication(); + + PdfCommunication_T pdfCommunication(blockForest, pdfFieldID); + pdfCommunication(); + + const ConstBlockDataID initialFillFieldID = + field::addCloneToStorage< ScalarField_T >(blockForest, fillFieldID, "Initial fill level field"); + + // add bubble model + const std::shared_ptr< bubble_model::BubbleModelBase > bubbleModel = + std::make_shared< bubble_model::BubbleModelConstantPressure >(real_c(1)); + + // create timeloop + SweepTimeloop timeloop(blockForest, timesteps); + + // add surface geometry handler + const SurfaceGeometryHandler< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > geometryHandler( + blockForest, freeSurfaceBoundaryHandling, fillFieldID, curvatureModel, false, false, real_c(0)); + + geometryHandler.addSweeps(timeloop); + + // get fields created by surface geometry handler + const ConstBlockDataID normalFieldID = geometryHandler.getConstNormalFieldID(); + + // add boundary handling for standard boundaries and free surface boundaries + const AdvectionDynamicsHandler< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > dynamicsHandler( + blockForest, pdfFieldID, flagFieldID, fillFieldID, normalFieldID, freeSurfaceBoundaryHandling, bubbleModel, + pdfReconstructionModel, excessMassDistributionModel, useSimpleMassExchange, cellConversionThreshold, + cellConversionForceThreshold); + + dynamicsHandler.addSweeps(timeloop); + + // add evaluator for geometrical + const std::shared_ptr< real_t > geometricalError = std::make_shared< real_t >(real_c(0)); + const GeometricalErrorEvaluator< FreeSurfaceBoundaryHandling_T, FlagField_T, ScalarField_T > + geometricalErrorEvaluator(blockForest, freeSurfaceBoundaryHandling, initialFillFieldID, fillFieldID, + evaluationFrequency, geometricalError); + timeloop.addFuncAfterTimeStep(geometricalErrorEvaluator, "Evaluator: geometrical error"); + + // add evaluator for total and excessive mass (mass that is currently undistributed) + const std::shared_ptr< real_t > totalMass = std::make_shared< real_t >(real_c(0)); + const std::shared_ptr< real_t > excessMass = std::make_shared< real_t >(real_c(0)); + const TotalMassComputer< FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T > totalMassComputer( + blockForest, freeSurfaceBoundaryHandling, pdfFieldID, fillFieldID, dynamicsHandler.getConstExcessMassFieldID(), + evaluationFrequency, totalMass, excessMass); + 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, BlockDataID(), + 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); + + if (t % evaluationFrequency == uint_c(0)) + { + WALBERLA_LOG_DEVEL_ON_ROOT("time step = " << t << "\n\t\ttotal mass = " << *totalMass << "\n\t\texcess mass = " + << *excessMass << "\n\t\tgeometrical error = " << *geometricalError); + } + + // set the constant velocity profile + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + PdfField_T* const pdfField = blockIt->getData< PdfField_T >(pdfFieldID); + FlagField_T* const flagField = blockIt->getData< FlagField_T >(flagFieldID); + + WALBERLA_FOR_ALL_CELLS(pdfFieldIt, pdfField, flagFieldIt, flagField, { + const Cell localCell = pdfFieldIt.cell(); + + // get cell in global coordinates + Cell globalCell = pdfFieldIt.cell(); + blockForest->transformBlockLocalToGlobalCell(globalCell, *blockIt, localCell); + + // set velocity profile (CFL_SI = CFL_LBM = velocity_LBM * dt_LBM / dx_LBM = velocity_LBM) + const Vector3< real_t > velocity = + CFL * velocityProfile(globalCell, real_c(timestepsToInitialPosition), t, domainSize); + pdfField->setDensityAndVelocity(localCell, velocity, real_c(1)); + }) // WALBERLA_FOR_ALL_CELLS + } + + pdfCommunication(); + + if (t % performanceLogFrequency == uint_c(0) && t > uint_c(0)) { timingPool.logResultOnRoot(); } + } + + return EXIT_SUCCESS; +} + +} // namespace DeformationField +} // namespace free_surface +} // namespace walberla + +int main(int argc, char** argv) { return walberla::free_surface::DeformationField::main(argc, argv); } \ No newline at end of file diff --git a/apps/benchmarks/FreeSurfaceAdvection/DeformationField.prm b/apps/benchmarks/FreeSurfaceAdvection/DeformationField.prm new file mode 100644 index 000000000..359fd4a98 --- /dev/null +++ b/apps/benchmarks/FreeSurfaceAdvection/DeformationField.prm @@ -0,0 +1,96 @@ +BlockForestParameters +{ + cellsPerBlock < 25, 25, 25 >; + periodicity < 1, 1, 1 >; +} + +DomainParameters +{ + domainWidth 50; +} + +PhysicsParameters +{ + timestepsToInitialPosition 3000; + timesteps 3001; + +} + +ModelParameters +{ + pdfReconstructionModel OnlyMissing; + excessMassDistributionModel EvenlyAllInterface; + curvatureModel FiniteDifferenceMethod; + useSimpleMassExchange false; + cellConversionThreshold 1e-2; + cellConversionForceThreshold 1e-1; +} + +EvaluationParameters +{ + evaluationFrequency 300; + performanceLogFrequency 10000; +} + +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 300; + baseFolder mesh-out; +} + +VTK +{ + fluid_field + { + writeFrequency 300; + ghostLayers 0; + baseFolder vtk-out; + samplingResolution 1; + + writers + { + fill_level; + mapped_flag; + velocity; + density; + //curvature; + //normal; + //obstacle_normal; + //pdf; + //flag; + } + + inclusion_filters + { + //liquidInterfaceFilter; // only include liquid and interface cells in VTK output + } + + before_functions + { + //ghost_layer_synchronization; // only needed if writing the ghost layer + gas_cell_zero_setter; // sets velocity=0 and density=1 all gas cells before writing VTK output + } + } + + domain_decomposition + { + writeFrequency 0; + baseFolder vtk-out; + outputDomainDecomposition true; + } +} \ No newline at end of file diff --git a/apps/benchmarks/FreeSurfaceAdvection/SingleVortex.cpp b/apps/benchmarks/FreeSurfaceAdvection/SingleVortex.cpp new file mode 100644 index 000000000..d3c8e01ed --- /dev/null +++ b/apps/benchmarks/FreeSurfaceAdvection/SingleVortex.cpp @@ -0,0 +1,351 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla is free software: you can +// redistribute it and/or modify it under the terms of the GNU General Public +// License as published by the Free Software Foundation, either version 3 of +// the License, or (at your option) any later version. +// +// waLBerla is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +// for more details. +// +// You should have received a copy of the GNU General Public License along +// with waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file SingleVortex.cpp +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +// +// This benchmark simulates the advection of a spherical bubble in a vortex field. +// The vortex field changes periodically so that the bubble returns to its initial position, where it should take its +// initial form. The relative geometrical error of the bubble's shape after one period is evaluated. There is no LBM +// flow simulation performed here, it is a test case for the FSLBM's mass advection. This benchmark is based on the +// two-dimensional test case from Rider and Kothe (doi: 10.1006/jcph.1998.5906). The extension to three-dimensional +// space is based on the Viktor Haag's master thesis +// (https://www10.cs.fau.de/publications/theses/2017/Haag_MT_2017.pdf). +//====================================================================================================================== + +#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/surface_geometry/SurfaceGeometryHandler.h" +#include "lbm/free_surface/surface_geometry/Utility.h" +#include "lbm/lattice_model/D3Q19.h" + +#include "functionality/AdvectionDynamicsHandler.h" +#include "functionality/GeometricalErrorEvaluator.h" + +namespace walberla +{ +namespace free_surface +{ +namespace SingleVortex +{ +using ScalarField_T = GhostLayerField< real_t, 1 >; +using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >; + +// Lattice model is only created for dummy purposes; no LBM simulation is performed +using CollisionModel_T = lbm::collision_model::SRT; +using LatticeModel_T = lbm::D3Q19< CollisionModel_T, true >; +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 >; + +// function describing the initialization velocity profile (in global cell coordinates) +inline Vector3< real_t > velocityProfile(Cell globalCell, real_t timePeriod, uint_t timestep, + const Vector3< real_t >& domainSize) +{ + // add 0.5 to get the cell's center + const real_t x = (real_c(globalCell.x()) + real_c(0.5)) / domainSize[0]; + const real_t y = (real_c(globalCell.y()) + real_c(0.5)) / domainSize[1]; + + const real_t xToDomainCenter = x - real_c(0.5); + const real_t yToDomainCenter = y - real_c(0.5); + const real_t r = real_c(std::sqrt(xToDomainCenter * xToDomainCenter + yToDomainCenter * yToDomainCenter)); + const real_t rTerm = (real_c(1) - real_c(2) * r) * (real_c(1) - real_c(2) * r); + + const real_t timeTerm = real_c(std::cos(math::pi * real_t(timestep) / timePeriod)); + + const real_t velocityX = real_c(std::sin(real_c(2) * math::pi * y)) * real_c(std::sin(math::pi * x)) * + real_c(std::sin(math::pi * x)) * timeTerm; + const real_t velocityY = -real_c(std::sin(real_c(2) * math::pi * x)) * real_c(std::sin(math::pi * y)) * + real_c(std::sin(math::pi * y)) * timeTerm; + const real_t velocityZ = rTerm * timeTerm; + + return Vector3< real_t >(velocityX, velocityY, velocityZ); +} + +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"); + + // get domain parameters from parameter file + auto domainParameters = walberlaEnv.config()->getOneBlock("DomainParameters"); + const uint_t domainWidth = domainParameters.getParameter< uint_t >("domainWidth"); + + const real_t bubbleDiameter = real_c(domainWidth) * real_c(0.075); + const Vector3< real_t > bubbleCenter = domainWidth * Vector3< real_t >(real_c(0.5), real_c(0.75), real_c(0.25)); + + // define domain size + Vector3< uint_t > domainSize; + domainSize[0] = domainWidth; + domainSize[1] = domainWidth; + domainSize[2] = domainWidth; + + // 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 + const 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(periodicity); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(bubbleDiameter); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(bubbleCenter); + + // get physics parameters from parameter file + auto physicsParameters = walberlaEnv.config()->getOneBlock("PhysicsParameters"); + const uint_t timesteps = physicsParameters.getParameter< uint_t >("timesteps"); + const uint_t timestepsToInitialPosition = physicsParameters.getParameter< uint_t >("timestepsToInitialPosition"); + + // compute CFL number + const real_t dx_SI = real_c(1) / real_c(domainWidth); + const real_t dt_SI = real_c(3) / real_c(timestepsToInitialPosition); + const real_t CFL = dt_SI / dx_SI; // with velocity_SI = 1 + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(CFL); + + // dummy collision model (LBM not simulated in this benchmark) + const CollisionModel_T collisionModel = CollisionModel_T(real_c(1)); + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(timestepsToInitialPosition); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(timesteps); + + // 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 excessMassDistributionModel = + modelParameters.getParameter< std::string >("excessMassDistributionModel"); + const std::string curvatureModel = modelParameters.getParameter< std::string >("curvatureModel"); + 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"); + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(pdfReconstructionModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(excessMassDistributionModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(curvatureModel); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(useSimpleMassExchange); + 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"); + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(performanceLogFrequency); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(evaluationFrequency); + + // create non-uniform block forest (non-uniformity required for load balancing) + const std::shared_ptr< StructuredBlockForest > blockForest = + createNonUniformBlockForest(domainSize, cellsPerBlock, numBlocks, periodicity); + + // create lattice model + const LatticeModel_T latticeModel = LatticeModel_T(collisionModel); + + // add pdf field + const BlockDataID pdfFieldID = lbm::addPdfFieldToStorage(blockForest, "PDF field", latticeModel, field::fzyx); + + // add fill level field (initialized with 1, i.e., liquid 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 the velocity profile + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + PdfField_T* const pdfField = blockIt->getData< PdfField_T >(pdfFieldID); + FlagField_T* const flagField = blockIt->getData< FlagField_T >(flagFieldID); + + WALBERLA_FOR_ALL_CELLS(pdfFieldIt, pdfField, flagFieldIt, flagField, { + // cell in block-local coordinates + const Cell localCell = pdfFieldIt.cell(); + + // get cell in global coordinates + Cell globalCell = pdfFieldIt.cell(); + blockForest->transformBlockLocalToGlobalCell(globalCell, *blockIt, localCell); + + // set velocity profile (CFL_SI = CFL_LBM = velocity_LBM * dt_LBM / dx_LBM = velocity_LBM) + const Vector3< real_t > initialVelocity = + CFL * velocityProfile(globalCell, real_c(timestepsToInitialPosition), uint_c(0), domainSize); + pdfField->setDensityAndVelocity(localCell, initialVelocity, real_c(1)); + }) // WALBERLA_FOR_ALL_CELLS + } + + // create the spherical bubble + const geometry::Sphere sphereBubble(real_c(domainWidth) * Vector3< real_t >(real_c(0.5), real_c(0.75), real_c(0.25)), + real_c(domainWidth) * real_c(0.15)); + bubble_model::addBodyToFillLevelField< geometry::Sphere >(*blockForest, fillFieldID, sphereBubble, true); + + // 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); + communication(); + + PdfCommunication_T pdfCommunication(blockForest, pdfFieldID); + pdfCommunication(); + + const ConstBlockDataID initialFillFieldID = + field::addCloneToStorage< ScalarField_T >(blockForest, fillFieldID, "Initial fill level field"); + + // add bubble model + const std::shared_ptr< bubble_model::BubbleModelBase > bubbleModel = + std::make_shared< bubble_model::BubbleModelConstantPressure >(real_c(1)); + + // create timeloop + SweepTimeloop timeloop(blockForest, timesteps); + + // add surface geometry handler + const SurfaceGeometryHandler< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > geometryHandler( + blockForest, freeSurfaceBoundaryHandling, fillFieldID, curvatureModel, false, false, real_c(0)); + + geometryHandler.addSweeps(timeloop); + + // get fields created by surface geometry handler + const ConstBlockDataID normalFieldID = geometryHandler.getConstNormalFieldID(); + + // add boundary handling for standard boundaries and free surface boundaries + const AdvectionDynamicsHandler< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > dynamicsHandler( + blockForest, pdfFieldID, flagFieldID, fillFieldID, normalFieldID, freeSurfaceBoundaryHandling, bubbleModel, + pdfReconstructionModel, excessMassDistributionModel, useSimpleMassExchange, cellConversionThreshold, + cellConversionForceThreshold); + + dynamicsHandler.addSweeps(timeloop); + + // add evaluator for geometrical + const std::shared_ptr< real_t > geometricalError = std::make_shared< real_t >(real_c(0)); + const GeometricalErrorEvaluator< FreeSurfaceBoundaryHandling_T, FlagField_T, ScalarField_T > + geometricalErrorEvaluator(blockForest, freeSurfaceBoundaryHandling, initialFillFieldID, fillFieldID, + evaluationFrequency, geometricalError); + timeloop.addFuncAfterTimeStep(geometricalErrorEvaluator, "Evaluator: geometrical error"); + + // add evaluator for total and excessive mass (mass that is currently undistributed) + const std::shared_ptr< real_t > totalMass = std::make_shared< real_t >(real_c(0)); + const std::shared_ptr< real_t > excessMass = std::make_shared< real_t >(real_c(0)); + const TotalMassComputer< FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T > totalMassComputer( + blockForest, freeSurfaceBoundaryHandling, pdfFieldID, fillFieldID, dynamicsHandler.getConstExcessMassFieldID(), + evaluationFrequency, totalMass, excessMass); + 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, BlockDataID(), + 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); + + if (t % evaluationFrequency == uint_c(0)) + { + WALBERLA_LOG_DEVEL_ON_ROOT("time step = " << t << "\n\t\ttotal mass = " << *totalMass << "\n\t\texcess mass = " + << *excessMass << "\n\t\tgeometrical error = " << *geometricalError); + } + + // set the constant velocity profile + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + PdfField_T* const pdfField = blockIt->getData< PdfField_T >(pdfFieldID); + FlagField_T* const flagField = blockIt->getData< FlagField_T >(flagFieldID); + + WALBERLA_FOR_ALL_CELLS(pdfFieldIt, pdfField, flagFieldIt, flagField, { + const Cell localCell = pdfFieldIt.cell(); + + // get cell in global coordinates + Cell globalCell = pdfFieldIt.cell(); + blockForest->transformBlockLocalToGlobalCell(globalCell, *blockIt, localCell); + + // set velocity profile (CFL_SI = CFL_LBM = velocity_LBM * dt_LBM / dx_LBM = velocity_LBM) + const Vector3< real_t > velocity = + CFL * velocityProfile(globalCell, real_c(timestepsToInitialPosition), t, domainSize); + pdfField->setDensityAndVelocity(localCell, velocity, real_c(1)); + }) // WALBERLA_FOR_ALL_CELLS + } + + pdfCommunication(); + + if (t % performanceLogFrequency == uint_c(0) && t > uint_c(0)) { timingPool.logResultOnRoot(); } + } + + return EXIT_SUCCESS; +} + +} // namespace SingleVortex +} // namespace free_surface +} // namespace walberla + +int main(int argc, char** argv) { return walberla::free_surface::SingleVortex::main(argc, argv); } \ No newline at end of file diff --git a/apps/benchmarks/FreeSurfaceAdvection/SingleVortex.prm b/apps/benchmarks/FreeSurfaceAdvection/SingleVortex.prm new file mode 100644 index 000000000..359fd4a98 --- /dev/null +++ b/apps/benchmarks/FreeSurfaceAdvection/SingleVortex.prm @@ -0,0 +1,96 @@ +BlockForestParameters +{ + cellsPerBlock < 25, 25, 25 >; + periodicity < 1, 1, 1 >; +} + +DomainParameters +{ + domainWidth 50; +} + +PhysicsParameters +{ + timestepsToInitialPosition 3000; + timesteps 3001; + +} + +ModelParameters +{ + pdfReconstructionModel OnlyMissing; + excessMassDistributionModel EvenlyAllInterface; + curvatureModel FiniteDifferenceMethod; + useSimpleMassExchange false; + cellConversionThreshold 1e-2; + cellConversionForceThreshold 1e-1; +} + +EvaluationParameters +{ + evaluationFrequency 300; + performanceLogFrequency 10000; +} + +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 300; + baseFolder mesh-out; +} + +VTK +{ + fluid_field + { + writeFrequency 300; + ghostLayers 0; + baseFolder vtk-out; + samplingResolution 1; + + writers + { + fill_level; + mapped_flag; + velocity; + density; + //curvature; + //normal; + //obstacle_normal; + //pdf; + //flag; + } + + inclusion_filters + { + //liquidInterfaceFilter; // only include liquid and interface cells in VTK output + } + + before_functions + { + //ghost_layer_synchronization; // only needed if writing the ghost layer + gas_cell_zero_setter; // sets velocity=0 and density=1 all gas cells before writing VTK output + } + } + + domain_decomposition + { + writeFrequency 0; + baseFolder vtk-out; + outputDomainDecomposition true; + } +} \ No newline at end of file diff --git a/apps/showcases/FreeSurface/ZalesakDisk.cpp b/apps/benchmarks/FreeSurfaceAdvection/ZalesakDisk.cpp similarity index 65% rename from apps/showcases/FreeSurface/ZalesakDisk.cpp rename to apps/benchmarks/FreeSurfaceAdvection/ZalesakDisk.cpp index edbaab294..f2177d559 100644 --- a/apps/showcases/FreeSurface/ZalesakDisk.cpp +++ b/apps/benchmarks/FreeSurfaceAdvection/ZalesakDisk.cpp @@ -16,12 +16,14 @@ //! \file ZalesakDisk.cpp //! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> // -// This showcase simulates a slotted disk of gas in a constant rotating velocity field. This benchmark is commonly -// referred to as Zalesak's rotating disk (see doi: 10.1016/0021-9991(79)90051-2). +// This benchmark simulates the advection of a slotted disk of gas in a constant rotating velocity field. The disk +// returns to its initial position, where it should take its initial form. The relative geometrical error of the +// bubble's shape after one rotation is evaluated. There is no LBM flow simulation performed here, it is a test case for +// the FSLBM's mass advection. This benchmark is commonly referred to as Zalesak's rotating disk (see +// doi: 10.1016/0021-9991(79)90051-2). The setup chosen here is identical to the one used by Janssen (see +// doi: 10.1016/j.camwa.2009.08.064). //====================================================================================================================== -#include "blockforest/Initialization.h" - #include "core/Environment.h" #include "field/Gather.h" @@ -33,11 +35,13 @@ #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 "functionality/AdvectionDynamicsHandler.h" +#include "functionality/GeometricalErrorEvaluator.h" + namespace walberla { namespace free_surface @@ -47,9 +51,9 @@ namespace ZalesakDisk using ScalarField_T = GhostLayerField< real_t, 1 >; using VectorField_T = GhostLayerField< Vector3< real_t >, 1 >; +// Lattice model is only created for dummy purposes; no LBM simulation is performed 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 LatticeModel_T = lbm::D2Q9< CollisionModel_T, true >; using LatticeModelStencil_T = LatticeModel_T::Stencil; using PdfField_T = lbm::PdfField< LatticeModel_T >; using PdfCommunication_T = blockforest::SimpleCommunication< LatticeModelStencil_T >; @@ -65,7 +69,7 @@ using FlagField_T = FlagField< flag_t >; using FreeSurfaceBoundaryHandling_T = FreeSurfaceBoundaryHandling< LatticeModel_T, FlagField_T, ScalarField_T >; // function describing the initialization velocity profile (in global cell coordinates) -inline Vector3< real_t > velocityProfile(real_t angularVelocity, Cell globalCell, Vector3< real_t > domainCenter) +inline Vector3< real_t > velocityProfile(real_t angularVelocity, Cell globalCell, const Vector3< real_t >& domainCenter) { // add 0.5 to get Cell's center const real_t velocityX = -angularVelocity * ((real_c(globalCell.y()) + real_c(0.5)) - domainCenter[0]); @@ -84,21 +88,19 @@ int main(int argc, char** argv) 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"); + 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"); // get domain parameters from parameter file auto domainParameters = walberlaEnv.config()->getOneBlock("DomainParameters"); const uint_t domainWidth = domainParameters.getParameter< uint_t >("domainWidth"); - const real_t diskRadius = real_c(domainWidth) * real_c(0.15); + const real_t diskRadius = real_c(domainWidth) * real_c(0.125); const Vector3< real_t > diskCenter = Vector3< real_t >(real_c(domainWidth) * real_c(0.5), real_c(domainWidth) * real_c(0.75), real_c(0.5)); - const real_t diskSlotLength = real_c(0.25) * real_c(domainWidth); - const real_t diskSlotWidth = real_c(0.05) * real_c(domainWidth); + const real_t diskSlotLength = real_c(2) * diskRadius - real_c(0.1) * real_c(domainWidth); + const real_t diskSlotWidth = real_c(0.06) * real_c(domainWidth); // define domain size Vector3< uint_t > domainSize; @@ -115,7 +117,7 @@ int main(int argc, char** argv) 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()); + const 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 " @@ -127,79 +129,68 @@ int main(int argc, char** argv) WALBERLA_LOG_DEVEL_VAR_ON_ROOT(numBlocks); WALBERLA_LOG_DEVEL_VAR_ON_ROOT(domainWidth); WALBERLA_LOG_DEVEL_VAR_ON_ROOT(periodicity); - WALBERLA_LOG_DEVEL_VAR_ON_ROOT(loadBalancingFrequency); - WALBERLA_LOG_DEVEL_VAR_ON_ROOT(printLoadBalancingStatistics); + + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(diskRadius); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(diskCenter); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(diskSlotLength); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(diskSlotWidth); // get physics parameters from parameter file - auto physicsParameters = walberlaEnv.config()->getOneBlock("PhysicsParameters"); - const uint_t timesteps = physicsParameters.getParameter< uint_t >("timesteps"); + auto physicsParameters = walberlaEnv.config()->getOneBlock("PhysicsParameters"); + const uint_t timesteps = physicsParameters.getParameter< uint_t >("timesteps"); + const uint_t timestepsFullRotation = physicsParameters.getParameter< uint_t >("timestepsFullRotation"); - const real_t relaxationRate = physicsParameters.getParameter< real_t >("relaxationRate"); - const CollisionModel_T collisionModel = CollisionModel_T(relaxationRate); - const real_t viscosity = collisionModel.viscosity(); + // compute CFL number + const real_t dx_SI = real_c(4) / real_c(domainWidth); + const real_t dt_SI = real_c(12.59652) / real_c(timestepsFullRotation); + const real_t CFL = dt_SI / dx_SI; // with velocity_SI = 1 + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(CFL); - const real_t reynoldsNumber = physicsParameters.getParameter< real_t >("reynoldsNumber"); - const real_t angularVelocity = - reynoldsNumber * viscosity / (real_c(0.5) * real_c(domainWidth) * real_c(domainWidth)); - const Vector3< real_t > force(real_c(0), real_c(0), real_c(0)); + // dummy collision model (LBM not simulated in this benchmark) + const CollisionModel_T collisionModel = CollisionModel_T(real_c(1)); - const bool enableWetting = physicsParameters.getParameter< bool >("enableWetting"); - const real_t contactAngle = physicsParameters.getParameter< real_t >("contactAngle"); + const real_t angularVelocity = real_c(2) * math::pi / real_c(timestepsFullRotation); - 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(timestepsFullRotation); 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(angularVelocity); - WALBERLA_LOG_DEVEL_ON_ROOT("Timesteps for full rotation " << real_c(2) * math::pi / angularVelocity); // 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 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(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"); WALBERLA_LOG_DEVEL_VAR_ON_ROOT(performanceLogFrequency); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(evaluationFrequency); // 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 forceDensityFieldID = - 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(forceDensityFieldID)); + const LatticeModel_T latticeModel = LatticeModel_T(collisionModel); // 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) + // add fill level field (initialized with 1, i.e., liquid everywhere) const BlockDataID fillFieldID = field::addToStorage< ScalarField_T >(blockForest, "Fill level field", real_c(1.0), field::fzyx, uint_c(2)); @@ -212,10 +203,9 @@ int main(int argc, char** argv) // initialize the velocity profile for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) { - PdfField_T* const pdfField = blockIt->getData< PdfField_T >(pdfFieldID); - FlagField_T* const flagField = blockIt->getData< FlagField_T >(flagFieldID); + PdfField_T* const pdfField = blockIt->getData< PdfField_T >(pdfFieldID); - WALBERLA_FOR_ALL_CELLS(pdfFieldIt, pdfField, flagFieldIt, flagField, { + WALBERLA_FOR_ALL_CELLS(pdfFieldIt, pdfField, { // cell in block-local coordinates const Cell localCell = pdfFieldIt.cell(); @@ -252,66 +242,57 @@ int main(int argc, char** argv) freeSurfaceBoundaryHandling->initFlagsFromFillLevel(); // communication after initialization - Communication_T communication(blockForest, flagFieldID, fillFieldID, forceDensityFieldID); + Communication_T communication(blockForest, flagFieldID, fillFieldID); 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< LatticeModelStencil_T > > bubbleModelDerived = - std::make_shared< bubble_model::BubbleModel< LatticeModelStencil_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)); } + const ConstBlockDataID initialFillFieldID = + field::addCloneToStorage< ScalarField_T >(blockForest, fillFieldID, "Initial fill level field"); - // 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); + // add bubble model + const std::shared_ptr< bubble_model::BubbleModelBase > bubbleModel = + std::make_shared< bubble_model::BubbleModelConstantPressure >(real_c(1)); // 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); + blockForest, freeSurfaceBoundaryHandling, fillFieldID, curvatureModel, false, false, real_c(0)); geometryHandler.addSweeps(timeloop); // get fields created by surface geometry handler - const ConstBlockDataID curvatureFieldID = geometryHandler.getConstCurvatureFieldID(); - const ConstBlockDataID normalFieldID = geometryHandler.getConstNormalFieldID(); + 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, forceDensityFieldID, normalFieldID, curvatureFieldID, - freeSurfaceBoundaryHandling, bubbleModel, pdfReconstructionModel, pdfRefillingModel, excessMassDistributionModel, - relaxationRate, force, surfaceTension, useSimpleMassExchange, cellConversionThreshold, + const AdvectionDynamicsHandler< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > dynamicsHandler( + blockForest, pdfFieldID, flagFieldID, fillFieldID, normalFieldID, freeSurfaceBoundaryHandling, bubbleModel, + pdfReconstructionModel, excessMassDistributionModel, 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 evaluator for geometrical + const std::shared_ptr< real_t > geometricalError = std::make_shared< real_t >(real_c(0)); + const GeometricalErrorEvaluator< FreeSurfaceBoundaryHandling_T, FlagField_T, ScalarField_T > + geometricalErrorEvaluator(blockForest, freeSurfaceBoundaryHandling, initialFillFieldID, fillFieldID, + evaluationFrequency, geometricalError); + timeloop.addFuncAfterTimeStep(geometricalErrorEvaluator, "Evaluator: geometrical errors"); + + // add evaluator for total and excessive mass (mass that is currently undistributed) + const std::shared_ptr< real_t > totalMass = std::make_shared< real_t >(real_c(0)); + const std::shared_ptr< real_t > excessMass = std::make_shared< real_t >(real_c(0)); + const TotalMassComputer< FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T > totalMassComputer( + blockForest, freeSurfaceBoundaryHandling, pdfFieldID, fillFieldID, dynamicsHandler.getConstExcessMassFieldID(), + evaluationFrequency, totalMass, excessMass); + 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, forceDensityFieldID, + blockForest, timeloop, walberlaEnv.config(), flagInfo, pdfFieldID, flagFieldID, fillFieldID, BlockDataID(), geometryHandler.getCurvatureFieldID(), geometryHandler.getNormalFieldID(), geometryHandler.getObstNormalFieldID()); @@ -332,6 +313,12 @@ int main(int argc, char** argv) { timeloop.singleStep(timingPool, true); + if (t % evaluationFrequency == uint_c(0)) + { + WALBERLA_LOG_DEVEL_ON_ROOT("time step = " << t << "\n\t\ttotal mass = " << *totalMass << "\n\t\texcess mass = " + << *excessMass << "\n\t\tgeometrical error = " << *geometricalError); + } + // set the constant velocity profile for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) { @@ -339,22 +326,20 @@ int main(int argc, char** argv) FlagField_T* const flagField = blockIt->getData< FlagField_T >(flagFieldID); WALBERLA_FOR_ALL_CELLS(pdfFieldIt, pdfField, flagFieldIt, flagField, { - if (flagInfo.isInterface(flagFieldIt) || flagInfo.isLiquid(flagFieldIt)) - { - // cell in block-local coordinates - const Cell localCell = pdfFieldIt.cell(); - - // get cell in global coordinates - Cell globalCell = pdfFieldIt.cell(); - blockForest->transformBlockLocalToGlobalCell(globalCell, *blockIt, localCell); - - // set velocity profile - const Vector3< real_t > initialVelocity = velocityProfile(angularVelocity, globalCell, domainCenter); - pdfField->setDensityAndVelocity(localCell, initialVelocity, real_c(1)); - } + const Cell localCell = pdfFieldIt.cell(); + + // get cell in global coordinates + Cell globalCell = pdfFieldIt.cell(); + blockForest->transformBlockLocalToGlobalCell(globalCell, *blockIt, localCell); + + // set velocity profile + const Vector3< real_t > initialVelocity = velocityProfile(angularVelocity, globalCell, domainCenter); + pdfField->setDensityAndVelocity(localCell, initialVelocity, real_c(1)); }) // WALBERLA_FOR_ALL_CELLS } + pdfCommunication(); + if (t % performanceLogFrequency == uint_c(0) && t > uint_c(0)) { timingPool.logResultOnRoot(); } } diff --git a/apps/showcases/FreeSurface/ZalesakDisk.prm b/apps/benchmarks/FreeSurfaceAdvection/ZalesakDisk.prm similarity index 63% rename from apps/showcases/FreeSurface/ZalesakDisk.prm rename to apps/benchmarks/FreeSurfaceAdvection/ZalesakDisk.prm index b9af1280c..857f1510b 100644 --- a/apps/showcases/FreeSurface/ZalesakDisk.prm +++ b/apps/benchmarks/FreeSurfaceAdvection/ZalesakDisk.prm @@ -1,53 +1,46 @@ BlockForestParameters { - cellsPerBlock < 50, 50, 1 >; - periodicity < 0, 0, 1 >; - loadBalancingFrequency 0; - printLoadBalancingStatistics false; + cellsPerBlock < 100, 100, 1 >; + periodicity < 0, 0, 0 >; } DomainParameters { - domainWidth 100; + domainWidth 200; } PhysicsParameters { - reynoldsNumber 100; // Re = angularVelocity * domainWidth * 0.5 * domainWidth / kin. viscosity - relaxationRate 1.8; - enableWetting false; - contactAngle 0; // only used if enableWetting=true - timesteps 1000000; + timestepsFullRotation 12570; // angularVelocity = 2 * pi / timestepsFullRotation + timesteps 12571; + } ModelParameters { pdfReconstructionModel OnlyMissing; - pdfRefillingModel EquilibriumRefilling; excessMassDistributionModel EvenlyAllInterface; curvatureModel FiniteDifferenceMethod; useSimpleMassExchange false; cellConversionThreshold 1e-2; cellConversionForceThreshold 1e-1; - - enableBubbleModel True; - enableBubbleSplits false; // only used if enableBubbleModel=true } EvaluationParameters { + evaluationFrequency 12570; performanceLogFrequency 10000; } BoundaryParameters { // X - Border { direction W; walldistance -1; FreeSlip{} } - Border { direction E; walldistance -1; FreeSlip{} } + //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{} } + //Border { direction N; walldistance -1; FreeSlip{} } + //Border { direction S; walldistance -1; FreeSlip{} } // Z //Border { direction T; walldistance -1; FreeSlip{} } @@ -64,7 +57,7 @@ VTK { fluid_field { - writeFrequency 4241; + writeFrequency 12570; ghostLayers 0; baseFolder vtk-out; samplingResolution 1; @@ -80,7 +73,6 @@ VTK //obstacle_normal; //pdf; //flag; - //force_density; } inclusion_filters diff --git a/apps/benchmarks/FreeSurfaceAdvection/functionality/AdvectSweep.h b/apps/benchmarks/FreeSurfaceAdvection/functionality/AdvectSweep.h new file mode 100644 index 000000000..80db158e6 --- /dev/null +++ b/apps/benchmarks/FreeSurfaceAdvection/functionality/AdvectSweep.h @@ -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 AdvectSweep.h +//! \ingroup free_surface +//! \author Martin Bauer +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Sweep for mass advection and interface cell conversion marking (simplified StreamReconstructAdvectSweep). +// +//====================================================================================================================== + +#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/free_surface/dynamics/PdfReconstructionModel.h" +#include "lbm/free_surface/dynamics/functionality/AdvectMass.h" +#include "lbm/free_surface/dynamics/functionality/FindInterfaceCellConversion.h" +#include "lbm/free_surface/dynamics/functionality/GetOredNeighborhood.h" +#include "lbm/free_surface/dynamics/functionality/ReconstructInterfaceCellABB.h" +#include "lbm/sweeps/StreamPull.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 > +class AdvectSweep +{ + public: + using flag_t = typename FlagInfo_T::flag_t; + using PdfField_T = lbm::PdfField< LatticeModel_T >; + + AdvectSweep(BlockDataID handlingID, BlockDataID fillFieldID, BlockDataID flagFieldID, BlockDataID pdfField, + const FlagInfo_T& flagInfo, const PdfReconstructionModel& pdfReconstructionModel, + bool useSimpleMassExchange, real_t cellConversionThreshold, real_t cellConversionForceThreshold) + : handlingID_(handlingID), fillFieldID_(fillFieldID), flagFieldID_(flagFieldID), pdfFieldID_(pdfField), + flagInfo_(flagInfo), neighborhoodFlagFieldClone_(flagFieldID), fillFieldClone_(fillFieldID), + pdfFieldClone_(pdfField), pdfReconstructionModel_(pdfReconstructionModel), + useSimpleMassExchange_(useSimpleMassExchange), cellConversionThreshold_(cellConversionThreshold), + cellConversionForceThreshold_(cellConversionForceThreshold) + {} + + void operator()(IBlock* const block); + + protected: + BlockDataID handlingID_; + BlockDataID fillFieldID_; + BlockDataID flagFieldID_; + BlockDataID pdfFieldID_; + + FlagInfo_T flagInfo_; + + // 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 AdvectSweep + +template< typename LatticeModel_T, typename BoundaryHandling_T, typename FlagField_T, typename FlagInfo_T, + typename ScalarField_T, typename VectorField_T > +void AdvectSweep< LatticeModel_T, BoundaryHandling_T, FlagField_T, FlagInfo_T, ScalarField_T, + VectorField_T >::operator()(IBlock* const block) +{ + // fetch data + ScalarField_T* const fillSrcField = block->getData< ScalarField_T >(fillFieldID_); + PdfField_T* const pdfSrcField = block->getData< PdfField_T >(pdfFieldID_); + + FlagField_T* const flagField = block->getData< FlagField_T >(flagFieldID_); + + // temporary fields that act as destination fields + 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( + pdfSrcFieldIt, pdfSrcField, fillSrcFieldIt, fillSrcField, fillDstFieldIt, fillDstField, flagFieldIt, flagField, + neighborhoodFlagFieldIt, neighborhoodFlagField, omp critical, { + if (flagInfo_.isInterface(flagFieldIt)) + { + const real_t rho = lbm::getDensity(pdfSrcField->latticeModel(), pdfSrcFieldIt); + + // 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 + deltaMass / rho; + } + 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)) { *fillDstFieldIt = real_c(1); } + else // flag is e.g. obstacle or outflow + { + *fillDstFieldIt = *fillSrcFieldIt; + } + } + } + }) // WALBERLA_FOR_ALL_CELLS_XYZ_OMP + + 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/apps/benchmarks/FreeSurfaceAdvection/functionality/AdvectionDynamicsHandler.h b/apps/benchmarks/FreeSurfaceAdvection/functionality/AdvectionDynamicsHandler.h new file mode 100644 index 000000000..1e48a1683 --- /dev/null +++ b/apps/benchmarks/FreeSurfaceAdvection/functionality/AdvectionDynamicsHandler.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 AdvectionDynamicsHandler.h +//! \ingroup surface_dynamics +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Handles free surface advection (without LBM flow simulation; this is a simplified SurfaceDynamicsHandler). +// +//====================================================================================================================== + +#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/free_surface/dynamics/CellConversionSweep.h" +#include "lbm/free_surface/dynamics/ConversionFlagsResetSweep.h" +#include "lbm/free_surface/dynamics/ExcessMassDistributionModel.h" +#include "lbm/free_surface/dynamics/ExcessMassDistributionSweep.h" +#include "lbm/free_surface/dynamics/PdfReconstructionModel.h" +#include "lbm/sweeps/CellwiseSweep.h" +#include "lbm/sweeps/SweepWrappers.h" + +#include "stencil/D3Q27.h" + +#include "timeloop/SweepTimeloop.h" + +#include "AdvectSweep.h" + +namespace walberla +{ +namespace free_surface +{ +template< typename LatticeModel_T, typename FlagField_T, typename ScalarField_T, typename VectorField_T > +class AdvectionDynamicsHandler +{ + 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: + AdvectionDynamicsHandler(const std::shared_ptr< StructuredBlockForest >& blockForest, BlockDataID pdfFieldID, + BlockDataID flagFieldID, BlockDataID fillFieldID, ConstBlockDataID normalFieldID, + const std::shared_ptr< FreeSurfaceBoundaryHandling_T >& freeSurfaceBoundaryHandling, + const std::shared_ptr< BubbleModelBase >& bubbleModel, + const std::string& pdfReconstructionModel, const std::string& excessMassDistributionModel, + bool useSimpleMassExchange, real_t cellConversionThreshold, + real_t cellConversionForceThreshold) + : blockForest_(blockForest), pdfFieldID_(pdfFieldID), flagFieldID_(flagFieldID), fillFieldID_(fillFieldID), + normalFieldID_(normalFieldID), bubbleModel_(bubbleModel), + freeSurfaceBoundaryHandling_(freeSurfaceBoundaryHandling), pdfReconstructionModel_(pdfReconstructionModel), + excessMassDistributionModel_({ excessMassDistributionModel }), useSimpleMassExchange_(useSimpleMassExchange), + cellConversionThreshold_(cellConversionThreshold), cellConversionForceThreshold_(cellConversionForceThreshold) + { + WALBERLA_CHECK(LatticeModel_T::compressible, + "The free surface lattice Boltzmann extension works only with compressible LBM models."); + + if (excessMassDistributionModel_.isEvenlyAllInterfaceFallbackLiquidType()) + { + // 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)); + } + } + + 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*) {}; + + // 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 AdvectSweep< LatticeModel_T, typename FreeSurfaceBoundaryHandling_T::BoundaryHandling_T, FlagField_T, + typename FreeSurfaceBoundaryHandling_T::FlagInfo_T, ScalarField_T, VectorField_T > + advectSweep(freeSurfaceBoundaryHandling_->getHandlingID(), fillFieldID_, flagFieldID_, pdfFieldID_, flagInfo, + pdfReconstructionModel_, useSimpleMassExchange_, cellConversionThreshold_, + cellConversionForceThreshold_); + // sweep acts only on blocks with at least one interface cell (due to StateSweep::fullFreeSurface) + timeloop.add() + << Sweep(advectSweep, "Sweep: Advect", StateSweep::fullFreeSurface) + << Sweep(emptySweep, "Empty sweep: Advect") + // 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 Advect sweep") + << AfterFunction(blockforest::UpdateSecondGhostLayer< ScalarField_T >(blockForest_, fillFieldID_), + "Second ghost layer update: after Advect sweep (fill level field)") + << AfterFunction(blockforest::UpdateSecondGhostLayer< FlagField_T >(blockForest_, flagFieldID_), + "Second ghost layer update: after Advect sweep (flag field)"); + + // 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)"); + + // distribute excess mass: + // - excess mass: mass that is free after conversion from interface to gas/liquid cells + // - update the bubble model + 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_.isEvenlyAllInterfaceFallbackLiquidType()) + { + const ExcessMassDistributionSweepInterfaceAndLiquid< LatticeModel_T, FlagField_T, ScalarField_T, + VectorField_T > + distributeMassSweep(excessMassDistributionModel_, fillFieldID_, flagFieldID_, pdfFieldID_, flagInfo, + excessMassFieldID_); + timeloop.add() + // perform this sweep also on "onlyLBM" blocks because liquid cells also exchange excess mass here + << Sweep(distributeMassSweep, "Sweep: excess mass distribution", StateSweep::fullFreeSurface) + << Sweep(distributeMassSweep, "Sweep: excess mass distribution", StateSweep::onlyLBM) + << 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_; + + ConstBlockDataID normalFieldID_; + + std::shared_ptr< BubbleModelBase > bubbleModel_; + std::shared_ptr< FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling_; + + PdfReconstructionModel pdfReconstructionModel_; + ExcessMassDistributionModel excessMassDistributionModel_; + + bool useSimpleMassExchange_; + real_t cellConversionThreshold_; + real_t cellConversionForceThreshold_; + + BlockDataID excessMassFieldID_ = BlockDataID(); +}; // class AdvectionDynamicsHandler + +} // namespace free_surface +} // namespace walberla \ No newline at end of file diff --git a/apps/benchmarks/FreeSurfaceAdvection/functionality/GeometricalErrorEvaluator.h b/apps/benchmarks/FreeSurfaceAdvection/functionality/GeometricalErrorEvaluator.h new file mode 100644 index 000000000..b0a5d7bba --- /dev/null +++ b/apps/benchmarks/FreeSurfaceAdvection/functionality/GeometricalErrorEvaluator.h @@ -0,0 +1,139 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla is free software: you can +// redistribute it and/or modify it under the terms of the GNU General Public +// License as published by the Free Software Foundation, either version 3 of +// the License, or (at your option) any later version. +// +// waLBerla is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +// for more details. +// +// You should have received a copy of the GNU General Public License along +// with waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file GeometricalErrorEvaluator.h +//! \author Christoph Schwarzmeier <christoph.schwarzmeier@fau.de> +//! \brief Compute the geometrical error in free-surface advection test cases. +//====================================================================================================================== + +#include "blockforest/StructuredBlockForest.h" + +#include "core/DataTypes.h" + +#include "domain_decomposition/BlockDataID.h" + +#include "field/iterators/IteratorMacros.h" + +namespace walberla +{ +namespace free_surface +{ +template< typename FreeSurfaceBoundaryHandling_T, typename FlagField_T, typename ScalarField_T > +class GeometricalErrorEvaluator +{ + public: + GeometricalErrorEvaluator(const std::weak_ptr< StructuredBlockForest >& blockForest, + const std::weak_ptr< const FreeSurfaceBoundaryHandling_T >& freeSurfaceBoundaryHandling, + const ConstBlockDataID& initialfillFieldID, const ConstBlockDataID& fillFieldID, + uint_t frequency, const std::shared_ptr< real_t >& geometricalError) + : blockForest_(blockForest), freeSurfaceBoundaryHandling_(freeSurfaceBoundaryHandling), + initialFillFieldID_(initialfillFieldID), fillFieldID_(fillFieldID), frequency_(frequency), + geometricalError_(geometricalError), 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)) + { + computeInitialFillLevelSum(blockForest, freeSurfaceBoundaryHandling); + computeError(blockForest, freeSurfaceBoundaryHandling); + } + else + { + // only evaluate in given frequencies + if (executionCounter_ % frequency_ == uint_c(0)) { computeError(blockForest, freeSurfaceBoundaryHandling); } + } + + ++executionCounter_; + } + + void computeInitialFillLevelSum( + 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(); + + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + const ScalarField_T* const initialfillField = blockIt->getData< const ScalarField_T >(initialFillFieldID_); + const FlagField_T* const flagField = blockIt->getData< const FlagField_T >(flagFieldID); + + // avoid OpenMP here because initialFillLevelSum_ is a class member and not a regular variable + WALBERLA_FOR_ALL_CELLS_OMP(initialfillFieldIt, initialfillField, flagFieldIt, flagField, omp critical, { + if (flagInfo.isInterface(flagFieldIt) || flagInfo.isLiquid(flagFieldIt)) + { + initialFillLevelSum_ += *initialfillFieldIt; + } + }) // WALBERLA_FOR_ALL_CELLS + } + + mpi::allReduceInplace< real_t >(initialFillLevelSum_, mpi::SUM); + } + + void computeError(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 geometricalError = real_c(0); + real_t fillLevelSum = real_c(0); + + for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) + { + const ScalarField_T* const initialfillField = blockIt->getData< const ScalarField_T >(initialFillFieldID_); + const ScalarField_T* const fillField = blockIt->getData< const ScalarField_T >(fillFieldID_); + const FlagField_T* const flagField = blockIt->getData< const FlagField_T >(flagFieldID); + + WALBERLA_FOR_ALL_CELLS_OMP(initialfillFieldIt, initialfillField, fillFieldIt, fillField, flagFieldIt, + flagField, omp parallel for schedule(static) reduction(+:geometricalError) + reduction(+:fillLevelSum), { + if (flagInfo.isInterface(flagFieldIt) || flagInfo.isLiquid(flagFieldIt)) + { + geometricalError += real_c(std::abs(*initialfillFieldIt - *fillFieldIt)); + fillLevelSum += *fillFieldIt; + } + }) // WALBERLA_FOR_ALL_CELLS + } + + mpi::allReduceInplace< real_t >(geometricalError, mpi::SUM); + + // compute L1 norms + *geometricalError_ = geometricalError / initialFillLevelSum_; + } + + private: + std::weak_ptr< StructuredBlockForest > blockForest_; + std::weak_ptr< const FreeSurfaceBoundaryHandling_T > freeSurfaceBoundaryHandling_; + ConstBlockDataID initialFillFieldID_; + ConstBlockDataID fillFieldID_; + uint_t frequency_; + std::shared_ptr< real_t > geometricalError_; + + uint_t executionCounter_; + real_t initialFillLevelSum_ = real_c(0); +}; // class GeometricalErrorEvaluator + +} // namespace free_surface +} // namespace walberla \ No newline at end of file diff --git a/apps/showcases/FreeSurface/BubblyPoiseuille.cpp b/apps/showcases/FreeSurface/BubblyPoiseuille.cpp index e27edf893..e54117c15 100644 --- a/apps/showcases/FreeSurface/BubblyPoiseuille.cpp +++ b/apps/showcases/FreeSurface/BubblyPoiseuille.cpp @@ -28,6 +28,7 @@ #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" @@ -195,8 +196,10 @@ int main(int argc, char** argv) // 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"); WALBERLA_LOG_DEVEL_VAR_ON_ROOT(performanceLogFrequency); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(evaluationFrequency); // create non-uniform block forest (non-uniformity required for load balancing) const std::shared_ptr< StructuredBlockForest > blockForest = @@ -327,6 +330,14 @@ int main(int argc, char** argv) loadBalancingFrequency, printLoadBalancingStatistics); timeloop.addFuncAfterTimeStep(loadBalancer, "Sweep: load balancing"); + // add evaluator for total and excessive mass (mass that is currently undistributed) + const std::shared_ptr< real_t > totalMass = std::make_shared< real_t >(real_c(0)); + const std::shared_ptr< real_t > excessMass = std::make_shared< real_t >(real_c(0)); + const TotalMassComputer< FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T > totalMassComputer( + blockForest, freeSurfaceBoundaryHandling, pdfFieldID, fillFieldID, dynamicsHandler.getConstExcessMassFieldID(), + evaluationFrequency, totalMass, excessMass); + 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, forceDensityFieldID, @@ -348,11 +359,13 @@ int main(int argc, char** argv) for (uint_t t = uint_c(0); t != timesteps; ++t) { - if (t % uint_c(real_c(timesteps / 100)) == uint_c(0)) + timeloop.singleStep(timingPool, true); + + if (t % evaluationFrequency == uint_c(0)) { - WALBERLA_LOG_DEVEL_ON_ROOT("Performing timestep = " << t); + WALBERLA_LOG_DEVEL_ON_ROOT("time step = " << t << "\n\t\ttotal mass = " << *totalMass + << "\n\t\texcess mass = " << *excessMass); } - timeloop.singleStep(timingPool, true); if (t % performanceLogFrequency == uint_c(0) && t > uint_c(0)) { timingPool.logResultOnRoot(); } } diff --git a/apps/showcases/FreeSurface/BubblyPoiseuille.prm b/apps/showcases/FreeSurface/BubblyPoiseuille.prm index e4fb01b3d..5b3c7728f 100644 --- a/apps/showcases/FreeSurface/BubblyPoiseuille.prm +++ b/apps/showcases/FreeSurface/BubblyPoiseuille.prm @@ -39,6 +39,7 @@ ModelParameters EvaluationParameters { + evaluationFrequency 5000; performanceLogFrequency 5000; } diff --git a/apps/showcases/FreeSurface/CMakeLists.txt b/apps/showcases/FreeSurface/CMakeLists.txt index b0dd58608..34bed9ecd 100644 --- a/apps/showcases/FreeSurface/CMakeLists.txt +++ b/apps/showcases/FreeSurface/CMakeLists.txt @@ -39,14 +39,14 @@ if( WALBERLA_BUILD_WITH_CODEGEN ) GravityWaveLatticeModelGeneration) endif() +waLBerla_add_executable(NAME MovingDrop + FILES MovingDrop.cpp + DEPENDS blockforest boundary core domain_decomposition field lbm postprocessing timeloop vtk) + 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) - -waLBerla_add_executable(NAME ZalesakDisk - FILES ZalesakDisk.cpp DEPENDS blockforest boundary core domain_decomposition field lbm postprocessing timeloop vtk) \ No newline at end of file diff --git a/apps/showcases/FreeSurface/CapillaryWave.cpp b/apps/showcases/FreeSurface/CapillaryWave.cpp index 64148c5b3..376a8d9cb 100644 --- a/apps/showcases/FreeSurface/CapillaryWave.cpp +++ b/apps/showcases/FreeSurface/CapillaryWave.cpp @@ -391,6 +391,14 @@ int main(int argc, char** argv) dynamicsHandler.addSweeps(timeloop); + // add evaluator for total and excessive mass (mass that is currently undistributed) + const std::shared_ptr< real_t > totalMass = std::make_shared< real_t >(real_c(0)); + const std::shared_ptr< real_t > excessMass = std::make_shared< real_t >(real_c(0)); + const TotalMassComputer< FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T > totalMassComputer( + blockForest, freeSurfaceBoundaryHandling, pdfFieldID, fillFieldID, dynamicsHandler.getConstExcessMassFieldID(), + evaluationFrequency, totalMass, excessMass); + timeloop.addFuncAfterTimeStep(totalMassComputer, "Evaluator: total mass"); + // add load balancing LoadBalancer< FlagField_T, CommunicationStencil_T, LatticeModelStencil_T > loadBalancer( blockForest, communication, pdfCommunication, bubbleModel, uint_c(50), uint_c(10), uint_c(5), @@ -436,9 +444,10 @@ int main(int argc, char** argv) 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); + WALBERLA_LOG_DEVEL("time step = " << t << "\n\t\ttNonDimensional = " << tNonDimensional + << "\n\t\tpositionNonDimensional = " << positionNonDimensional + << "\n\t\ttotal mass = " << *totalMass + << "\n\t\texcess mass = " << *excessMass); writeVectorToFile(resultVector, filename); } } diff --git a/apps/showcases/FreeSurface/DamBreakCylindrical.cpp b/apps/showcases/FreeSurface/DamBreakCylindrical.cpp index f48e23232..0cf759466 100644 --- a/apps/showcases/FreeSurface/DamBreakCylindrical.cpp +++ b/apps/showcases/FreeSurface/DamBreakCylindrical.cpp @@ -32,6 +32,7 @@ #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" @@ -527,6 +528,14 @@ int main(int argc, char** argv) evaluationFrequency, columnRadiusSample); timeloop.addFuncAfterTimeStep(columnRadiusEvaluator, "Evaluator: radius"); + // add evaluator for total and excessive mass (mass that is currently undistributed) + const std::shared_ptr< real_t > totalMass = std::make_shared< real_t >(real_c(0)); + const std::shared_ptr< real_t > excessMass = std::make_shared< real_t >(real_c(0)); + const TotalMassComputer< FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T > totalMassComputer( + blockForest, freeSurfaceBoundaryHandling, pdfFieldID, fillFieldID, dynamicsHandler.getConstExcessMassFieldID(), + evaluationFrequency, totalMass, excessMass); + 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, forceDensityFieldID, @@ -571,13 +580,14 @@ int main(int argc, char** argv) 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() { + WALBERLA_LOG_DEVEL("time step =" << t); + WALBERLA_LOG_DEVEL("\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 << "\n\t\ttotal mass = " << *totalMass + << "\n\t\texcess mass = " << *excessMass); + const std::vector< real_t > resultVector{ T, Z_mean, Z_max, Z_min, Z_stdDeviation, H }; writeNumberVector(resultVector, t, filename); } diff --git a/apps/showcases/FreeSurface/DamBreakRectangular.cpp b/apps/showcases/FreeSurface/DamBreakRectangular.cpp index e7abd62e6..fba4e9579 100644 --- a/apps/showcases/FreeSurface/DamBreakRectangular.cpp +++ b/apps/showcases/FreeSurface/DamBreakRectangular.cpp @@ -512,6 +512,14 @@ int main(int argc, char** argv) blockForest, freeSurfaceBoundaryHandling, domainSize, evaluationFrequency, currentColumnWidth); timeloop.addFuncAfterTimeStep(widthEvaluator, "Evaluator: column width"); + // add evaluator for total and excessive mass (mass that is currently undistributed) + const std::shared_ptr< real_t > totalMass = std::make_shared< real_t >(real_c(0)); + const std::shared_ptr< real_t > excessMass = std::make_shared< real_t >(real_c(0)); + const TotalMassComputer< FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T > totalMassComputer( + blockForest, freeSurfaceBoundaryHandling, pdfFieldID, fillFieldID, dynamicsHandler.getConstExcessMassFieldID(), + evaluationFrequency, totalMass, excessMass); + 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, forceDensityFieldID, @@ -543,9 +551,14 @@ int main(int argc, char** argv) 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); } + WALBERLA_ROOT_SECTION() + { + WALBERLA_LOG_DEVEL("time step =" << t); + WALBERLA_LOG_DEVEL("\t\tT = " << T << "\n\t\tZ = " << Z << "\n\t\tH = " << H << "\n\t\ttotal mass = " + << *totalMass << "\n\t\texcess mass = " << *excessMass); + + writeNumberVector(resultVector, t, filename); + } // simulation is considered converged if (Z >= real_c(domainSize[0]) / columnWidth - real_c(0.5)) diff --git a/apps/showcases/FreeSurface/DropImpact.cpp b/apps/showcases/FreeSurface/DropImpact.cpp index 896c5b098..61da9474b 100644 --- a/apps/showcases/FreeSurface/DropImpact.cpp +++ b/apps/showcases/FreeSurface/DropImpact.cpp @@ -29,6 +29,7 @@ #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" @@ -202,8 +203,10 @@ int main(int argc, char** argv) // 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"); WALBERLA_LOG_DEVEL_VAR_ON_ROOT(performanceLogFrequency); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(evaluationFrequency); // create non-uniform block forest (non-uniformity required for load balancing) const std::shared_ptr< StructuredBlockForest > blockForest = @@ -325,6 +328,14 @@ int main(int argc, char** argv) loadBalancingFrequency, printLoadBalancingStatistics); timeloop.addFuncAfterTimeStep(loadBalancer, "Sweep: load balancing"); + // add evaluator for total and excessive mass (mass that is currently undistributed) + const std::shared_ptr< real_t > totalMass = std::make_shared< real_t >(real_c(0)); + const std::shared_ptr< real_t > excessMass = std::make_shared< real_t >(real_c(0)); + const TotalMassComputer< FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T > totalMassComputer( + blockForest, freeSurfaceBoundaryHandling, pdfFieldID, fillFieldID, dynamicsHandler.getConstExcessMassFieldID(), + evaluationFrequency, totalMass, excessMass); + 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, forceDensityFieldID, @@ -346,11 +357,13 @@ int main(int argc, char** argv) for (uint_t t = uint_c(0); t != timesteps; ++t) { - if (t % uint_c(real_c(timesteps / 100)) == uint_c(0)) + timeloop.singleStep(timingPool, true); + + if (t % evaluationFrequency == uint_c(0)) { - WALBERLA_LOG_DEVEL_ON_ROOT("Performing timestep = " << t); + WALBERLA_LOG_DEVEL_ON_ROOT("time step = " << t << "\n\t\ttotal mass = " << *totalMass + << "\n\t\texcess mass = " << *excessMass); } - timeloop.singleStep(timingPool, true); if (t % performanceLogFrequency == uint_c(0) && t > uint_c(0)) { timingPool.logResultOnRoot(); } } diff --git a/apps/showcases/FreeSurface/DropImpact.prm b/apps/showcases/FreeSurface/DropImpact.prm index 8437db98e..1790dffb4 100644 --- a/apps/showcases/FreeSurface/DropImpact.prm +++ b/apps/showcases/FreeSurface/DropImpact.prm @@ -41,6 +41,7 @@ ModelParameters EvaluationParameters { + evaluationFrequency 1000; performanceLogFrequency 3000; } diff --git a/apps/showcases/FreeSurface/DropWetting.cpp b/apps/showcases/FreeSurface/DropWetting.cpp index bdba59086..a55ad8808 100644 --- a/apps/showcases/FreeSurface/DropWetting.cpp +++ b/apps/showcases/FreeSurface/DropWetting.cpp @@ -29,6 +29,7 @@ #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" @@ -368,6 +369,14 @@ int main(int argc, char** argv) blockForest, freeSurfaceBoundaryHandling, fillFieldID, dropHeight, evaluationFrequency); timeloop.addFuncAfterTimeStep(dropHeightEvaluator, "Evaluator: drop height"); + // add evaluator for total and excessive mass (mass that is currently undistributed) + const std::shared_ptr< real_t > totalMass = std::make_shared< real_t >(real_c(0)); + const std::shared_ptr< real_t > excessMass = std::make_shared< real_t >(real_c(0)); + const TotalMassComputer< FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T > totalMassComputer( + blockForest, freeSurfaceBoundaryHandling, pdfFieldID, fillFieldID, dynamicsHandler.getConstExcessMassFieldID(), + evaluationFrequency, totalMass, excessMass); + 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, forceDensityFieldID, @@ -395,8 +404,7 @@ int main(int argc, char** argv) // 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) + WALBERLA_LOG_DEVEL_ON_ROOT("time step = " << t << "\n\t\tdrop height = " << *dropHeight) if (std::abs(formerDropHeight - *dropHeight) / *dropHeight < convergenceThreshold) { WALBERLA_LOG_DEVEL_ON_ROOT("Final converged drop height=" << *dropHeight); diff --git a/apps/showcases/FreeSurface/GravityWave.cpp b/apps/showcases/FreeSurface/GravityWave.cpp index acca70e45..702682bcf 100644 --- a/apps/showcases/FreeSurface/GravityWave.cpp +++ b/apps/showcases/FreeSurface/GravityWave.cpp @@ -554,6 +554,14 @@ int main(int argc, char** argv) evaluationFrequency, symmetryNorm); timeloop.addFuncAfterTimeStep(symmetryEvaluator, "Evaluator: symmetry norm"); + // add evaluator for total and excessive mass (mass that is currently undistributed) + const std::shared_ptr< real_t > totalMass = std::make_shared< real_t >(real_c(0)); + const std::shared_ptr< real_t > excessMass = std::make_shared< real_t >(real_c(0)); + const TotalMassComputer< FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T > totalMassComputer( + blockForest, freeSurfaceBoundaryHandling, pdfFieldID, fillFieldID, dynamicsHandler.getConstExcessMassFieldID(), + evaluationFrequency, totalMass, excessMass); + 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, forceDensityFieldID, @@ -586,10 +594,11 @@ int main(int argc, char** argv) 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); + WALBERLA_LOG_DEVEL("time step = " << t << "\n\t\ttNonDimensional = " << tNonDimensional + << "\n\t\tpositionNonDimensional = " << positionNonDimensional + << "\n\t\tsymmetryNorm = " << *symmetryNorm << "\n\t\ttotal mass = " + << *totalMass << "\n\t\texcess mass = " << *excessMass); + writeVectorToFile(resultVector, filename); } } diff --git a/apps/showcases/FreeSurface/GravityWaveCodegen.cpp b/apps/showcases/FreeSurface/GravityWaveCodegen.cpp index dd603d62c..65d18594b 100644 --- a/apps/showcases/FreeSurface/GravityWaveCodegen.cpp +++ b/apps/showcases/FreeSurface/GravityWaveCodegen.cpp @@ -516,6 +516,14 @@ int main(int argc, char** argv) evaluationFrequency, symmetryNorm); timeloop.addFuncAfterTimeStep(symmetryEvaluator, "Evaluator: symmetry norm"); + // add evaluator for total and excessive mass (mass that is currently undistributed) + const std::shared_ptr< real_t > totalMass = std::make_shared< real_t >(real_c(0)); + const std::shared_ptr< real_t > excessMass = std::make_shared< real_t >(real_c(0)); + const TotalMassComputer< FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T > totalMassComputer( + blockForest, freeSurfaceBoundaryHandling, pdfFieldID, fillFieldID, dynamicsHandler.getConstExcessMassFieldID(), + evaluationFrequency, totalMass, excessMass); + timeloop.addFuncAfterTimeStep(totalMassComputer, "Evaluator: total mass"); + // add VTK output addVTKOutput< LatticeModel_T, FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T, VectorField_T, true, VectorFieldFlattened_T >( @@ -549,10 +557,11 @@ int main(int argc, char** argv) 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); + WALBERLA_LOG_DEVEL("time step = " << t << "\n\t\ttNonDimensional = " << tNonDimensional + << "\n\t\tpositionNonDimensional = " << positionNonDimensional + << "\n\t\tsymmetryNorm = " << *symmetryNorm << "\n\t\ttotal mass = " + << *totalMass << "\n\t\texcess mass = " << *excessMass); + writeVectorToFile(resultVector, filename); } } diff --git a/apps/showcases/FreeSurface/MovingDrop.cpp b/apps/showcases/FreeSurface/MovingDrop.cpp index 352a85c72..4fb4c49d3 100644 --- a/apps/showcases/FreeSurface/MovingDrop.cpp +++ b/apps/showcases/FreeSurface/MovingDrop.cpp @@ -27,6 +27,7 @@ #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" @@ -189,8 +190,10 @@ int main(int argc, char** argv) // 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"); WALBERLA_LOG_DEVEL_VAR_ON_ROOT(performanceLogFrequency); + WALBERLA_LOG_DEVEL_VAR_ON_ROOT(evaluationFrequency); // create non-uniform block forest (non-uniformity required for load balancing) const std::shared_ptr< StructuredBlockForest > blockForest = @@ -286,6 +289,14 @@ int main(int argc, char** argv) loadBalancingFrequency, printLoadBalancingStatistics); timeloop.addFuncAfterTimeStep(loadBalancer, "Sweep: load balancing"); + // add evaluator for total and excessive mass (mass that is currently undistributed) + const std::shared_ptr< real_t > totalMass = std::make_shared< real_t >(real_c(0)); + const std::shared_ptr< real_t > excessMass = std::make_shared< real_t >(real_c(0)); + const TotalMassComputer< FreeSurfaceBoundaryHandling_T, PdfField_T, FlagField_T, ScalarField_T > totalMassComputer( + blockForest, freeSurfaceBoundaryHandling, pdfFieldID, fillFieldID, dynamicsHandler.getConstExcessMassFieldID(), + evaluationFrequency, totalMass, excessMass); + 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, forceDensityFieldID, @@ -307,9 +318,14 @@ int main(int argc, char** argv) 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 % evaluationFrequency == uint_c(0)) + { + WALBERLA_LOG_DEVEL_ON_ROOT("time step = " << t << "\n\t\ttotal mass = " << *totalMass + << "\n\t\texcess mass = " << *excessMass); + } + if (t % performanceLogFrequency == uint_c(0) && t > uint_c(0)) { timingPool.logResultOnRoot(); } } diff --git a/apps/showcases/FreeSurface/MovingDrop.prm b/apps/showcases/FreeSurface/MovingDrop.prm index dceed84e7..79718c8c0 100644 --- a/apps/showcases/FreeSurface/MovingDrop.prm +++ b/apps/showcases/FreeSurface/MovingDrop.prm @@ -1,17 +1,17 @@ BlockForestParameters { - cellsPerBlock < 10, 10, 10 >; + cellsPerBlock < 20, 20, 20 >; periodicity < 1, 1, 1 >; loadBalancingFrequency 0; - printLoadBalancingStatistics true; + printLoadBalancingStatistics false; } DomainParameters { - dropDiameter 50; + dropDiameter 20; dropCenterFactor < 1, 1, 1 >; // values multiplied with dropDiameter - poolHeightFactor 0; // value multiplied with dropDiameter - domainSizeFactor < 2, 2, 4 >; // values multiplied with dropDiameter + poolHeightFactor 0; // value multiplied with dropDiameter + domainSizeFactor < 2, 2, 4 >; // values multiplied with dropDiameter } PhysicsParameters @@ -31,14 +31,16 @@ ModelParameters excessMassDistributionModel EvenlyAllInterface; curvatureModel FiniteDifferenceMethod; useSimpleMassExchange false; - enableBubbleModel false; - enableBubbleSplits false; // only used if enableBubbleModel=true cellConversionThreshold 1e-2; cellConversionForceThreshold 1e-1; + + enableBubbleModel false; + enableBubbleSplits false; // only used if enableBubbleModel=true } EvaluationParameters { + evaluationFrequency 100; performanceLogFrequency 10000; } @@ -59,7 +61,7 @@ BoundaryParameters MeshOutputParameters { - writeFrequency 10000; + writeFrequency 1000; baseFolder mesh-out; } @@ -67,7 +69,7 @@ VTK { fluid_field { - writeFrequency 10000; + writeFrequency 1000; ghostLayers 0; baseFolder vtk-out; samplingResolution 1; diff --git a/apps/showcases/FreeSurface/RisingBubble.cpp b/apps/showcases/FreeSurface/RisingBubble.cpp index 8fc773876..6699fdf1e 100644 --- a/apps/showcases/FreeSurface/RisingBubble.cpp +++ b/apps/showcases/FreeSurface/RisingBubble.cpp @@ -376,10 +376,12 @@ int main(int argc, char** argv) 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)); + // add evaluator for total and excessive mass (mass that is currently undistributed) + const std::shared_ptr< real_t > totalMass = std::make_shared< real_t >(real_c(0)); + const std::shared_ptr< real_t > excessMass = 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); + blockForest, freeSurfaceBoundaryHandling, pdfFieldID, fillFieldID, dynamicsHandler.getConstExcessMassFieldID(), + evaluationFrequency, totalMass, excessMass); timeloop.addFuncAfterTimeStep(totalMassComputer, "Evaluator: total mass"); // add VTK output @@ -419,10 +421,10 @@ int main(int argc, char** argv) 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); + WALBERLA_LOG_DEVEL("time step = " << t << "\n\t\tcenterOfMass = " << *centerOfMass + << "\n\t\triseVelocity = " << riseVelocity + << "\n\t\tdragForce = " << dragForce << "\n\t\ttotalMass = " << *totalMass + << "\n\t\texcessMass = " << *excessMass); const std::vector< real_t > resultVector{ (*centerOfMass)[2], riseVelocity, dragForce }; diff --git a/apps/showcases/FreeSurface/TaylorBubble.cpp b/apps/showcases/FreeSurface/TaylorBubble.cpp index a045cd6ac..ce2a45302 100644 --- a/apps/showcases/FreeSurface/TaylorBubble.cpp +++ b/apps/showcases/FreeSurface/TaylorBubble.cpp @@ -405,9 +405,12 @@ int main(int argc, char** argv) 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)); + // add evaluator for total and excessive mass (mass that is currently undistributed) + const std::shared_ptr< real_t > totalMass = std::make_shared< real_t >(real_c(0)); + const std::shared_ptr< real_t > excessMass = 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); + blockForest, freeSurfaceBoundaryHandling, pdfFieldID, fillFieldID, dynamicsHandler.getConstExcessMassFieldID(), + evaluationFrequency, totalMass, excessMass); timeloop.addFuncAfterTimeStep(totalMassComputer, "Evaluator: total mass"); // add VTK output @@ -447,20 +450,20 @@ int main(int argc, char** argv) 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); + WALBERLA_LOG_DEVEL("time step = " << t << "\n\t\tcenterOfMass = " << *centerOfMass + << "\n\t\triseVelocity = " << riseVelocity + << "\n\t\tdragForce = " << dragForce << "\n\t\ttotalMass = " << *totalMass + << "\n\t\texcessMass = " << *excessMass); const std::vector< real_t > resultVector{ (*centerOfMass)[2], riseVelocity, dragForce }; writeVectorToFile(resultVector, t, filename); } - - timestepOld = t; - centerOfMassOld = *centerOfMass; } + timestepOld = t; + centerOfMassOld = *centerOfMass; + // stop simulation before bubble hits the top wall if ((*centerOfMass)[2] > stoppingHeight) { break; } diff --git a/src/lbm/free_surface/TotalMassComputer.h b/src/lbm/free_surface/TotalMassComputer.h index 192ec8755..7b3fda3e8 100644 --- a/src/lbm/free_surface/TotalMassComputer.h +++ b/src/lbm/free_surface/TotalMassComputer.h @@ -50,10 +50,10 @@ class TotalMassComputer 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) + const std::shared_ptr< real_t >& totalMass, const std::shared_ptr< real_t >& excessMass) : blockForest_(blockForest), freeSurfaceBoundaryHandling_(freeSurfaceBoundaryHandling), pdfFieldID_(pdfFieldID), - fillFieldID_(fillFieldID), excessMassFieldID_(excessMassFieldID), totalMass_(totalMass), frequency_(frequency), - executionCounter_(uint_c(0)) + fillFieldID_(fillFieldID), excessMassFieldID_(excessMassFieldID), totalMass_(totalMass), + excessMass_(excessMass), frequency_(frequency), executionCounter_(uint_c(0)) {} void operator()() @@ -66,11 +66,10 @@ class TotalMassComputer 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) || executionCounter_ == uint_c(0)) { - // only evaluate in given frequencies - if (executionCounter_ % frequency_ == uint_c(0)) { computeMass(blockForest, freeSurfaceBoundaryHandling); } + computeMass(blockForest, freeSurfaceBoundaryHandling); } ++executionCounter_; @@ -82,7 +81,8 @@ class TotalMassComputer const BlockDataID flagFieldID = freeSurfaceBoundaryHandling->getFlagFieldID(); const typename FreeSurfaceBoundaryHandling_T::FlagInfo_T& flagInfo = freeSurfaceBoundaryHandling->getFlagInfo(); - real_t mass = real_c(0); + real_t mass = real_c(0); + real_t excessMass = real_c(0); for (auto blockIt = blockForest->begin(); blockIt != blockForest->end(); ++blockIt) { @@ -104,6 +104,8 @@ class TotalMassComputer { const real_t density = pdfField->getDensity(pdfFieldIt.cell()); mass += *fillFieldIt * density + *excessMassFieldIt; + + if (excessMass_ != nullptr) { excessMass += *excessMassFieldIt; } } }) // WALBERLA_FOR_ALL_CELLS_OMP } @@ -122,8 +124,13 @@ class TotalMassComputer } mpi::allReduceInplace< real_t >(mass, mpi::SUM); - *totalMass_ = mass; + + if (excessMass_ != nullptr) + { + mpi::allReduceInplace< real_t >(excessMass, mpi::SUM); + *excessMass_ = excessMass; + } }; private: @@ -135,6 +142,7 @@ class TotalMassComputer const ConstBlockDataID excessMassFieldID_ = ConstBlockDataID(); std::shared_ptr< real_t > totalMass_; + std::shared_ptr< real_t > excessMass_ = nullptr; // mass stored in the excessMassField uint_t frequency_; uint_t executionCounter_; diff --git a/src/lbm/free_surface/VtkWriter.h b/src/lbm/free_surface/VtkWriter.h index a27103510..726e0ac73 100644 --- a/src/lbm/free_surface/VtkWriter.h +++ b/src/lbm/free_surface/VtkWriter.h @@ -56,7 +56,7 @@ void addVTKOutput(const std::weak_ptr< StructuredBlockForest >& blockForestPtr, const BlockDataID& forceDensityFieldID, const BlockDataID& curvatureFieldID, const BlockDataID& normalFieldID, const BlockDataID& obstacleNormalFieldID) { - using value_type = typename FlagField_T::value_type; + using value_type = typename FlagField_T::value_type; const auto blockForest = blockForestPtr.lock(); WALBERLA_CHECK_NOT_NULLPTR(blockForest); @@ -89,27 +89,34 @@ void addVTKOutput(const std::weak_ptr< StructuredBlockForest >& blockForestPtr, std::make_shared< VTKWriter< VectorField_T, float > >(obstacleNormalFieldID, "obstacle_normal")); if constexpr (useCodegen) { - writers.push_back( - std::make_shared< VTKWriter< VectorFieldFlattened_T, float > >(forceDensityFieldID, "force_density")); + if (forceDensityFieldID != BlockDataID()) + { + writers.push_back( + std::make_shared< VTKWriter< VectorFieldFlattened_T, float > >(forceDensityFieldID, "force_density")); + } } else { - writers.push_back(std::make_shared< VTKWriter< VectorField_T, float > >(forceDensityFieldID, "force_density")); + if (forceDensityFieldID != BlockDataID()) + { + writers.push_back( + std::make_shared< VTKWriter< VectorField_T, float > >(forceDensityFieldID, "force_density")); + } } // map flagIDs to integer values const auto flagMapper = std::make_shared< field::FlagFieldMapping< FlagField_T, value_type > >(flagFieldID, "mapped_flag"); - flagMapper->addMapping(flagIDs::liquidFlagID, numeric_cast<value_type>(1)); - flagMapper->addMapping(flagIDs::interfaceFlagID, numeric_cast<value_type>(2)); - flagMapper->addMapping(flagIDs::gasFlagID, numeric_cast<value_type>(3)); - flagMapper->addMapping(FreeSurfaceBoundaryHandling_T::noSlipFlagID, numeric_cast<value_type>(4)); - flagMapper->addMapping(FreeSurfaceBoundaryHandling_T::freeSlipFlagID, numeric_cast<value_type>(6)); - flagMapper->addMapping(FreeSurfaceBoundaryHandling_T::ubbFlagID, numeric_cast<value_type>(6)); - flagMapper->addMapping(FreeSurfaceBoundaryHandling_T::ubbInflowFlagID, numeric_cast<value_type>(7)); - flagMapper->addMapping(FreeSurfaceBoundaryHandling_T::pressureFlagID, numeric_cast<value_type>(8)); - flagMapper->addMapping(FreeSurfaceBoundaryHandling_T::pressureOutflowFlagID, numeric_cast<value_type>(9)); - flagMapper->addMapping(FreeSurfaceBoundaryHandling_T::outletFlagID, numeric_cast<value_type>(10)); + flagMapper->addMapping(flagIDs::liquidFlagID, numeric_cast< value_type >(1)); + flagMapper->addMapping(flagIDs::interfaceFlagID, numeric_cast< value_type >(2)); + flagMapper->addMapping(flagIDs::gasFlagID, numeric_cast< value_type >(3)); + flagMapper->addMapping(FreeSurfaceBoundaryHandling_T::noSlipFlagID, numeric_cast< value_type >(4)); + flagMapper->addMapping(FreeSurfaceBoundaryHandling_T::freeSlipFlagID, numeric_cast< value_type >(6)); + flagMapper->addMapping(FreeSurfaceBoundaryHandling_T::ubbFlagID, numeric_cast< value_type >(6)); + flagMapper->addMapping(FreeSurfaceBoundaryHandling_T::ubbInflowFlagID, numeric_cast< value_type >(7)); + flagMapper->addMapping(FreeSurfaceBoundaryHandling_T::pressureFlagID, numeric_cast< value_type >(8)); + flagMapper->addMapping(FreeSurfaceBoundaryHandling_T::pressureOutflowFlagID, numeric_cast< value_type >(9)); + flagMapper->addMapping(FreeSurfaceBoundaryHandling_T::outletFlagID, numeric_cast< value_type >(10)); writers.push_back(flagMapper); diff --git a/src/lbm/free_surface/dynamics/ExcessMassDistributionModel.h b/src/lbm/free_surface/dynamics/ExcessMassDistributionModel.h index 2e4d0b798..a4b86c43c 100644 --- a/src/lbm/free_surface/dynamics/ExcessMassDistributionModel.h +++ b/src/lbm/free_surface/dynamics/ExcessMassDistributionModel.h @@ -63,15 +63,21 @@ namespace free_surface * cells, i.e., cells that are non-newly converted to interface. Falls back to WeightedAllInterface if not * applicable. * - * - EvenlyLiquidAndAllInterface: + * - EvenlyAllInterfaceAndLiquid: * 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 + * - EvenlyAllInterfaceFallbackLiquid: + * Similar to EvenlyAllInterfaceAndLiquid, however, excess mass is preferably distributed to interface cells. It is * distributed to liquid cells only if there are no neighboring interface cells available. + * + * - EvenlyNewInterfaceFallbackLiquid: + * Similar to EvenlyAllInterfaceFallbackLiquid, however, excess mass is preferably distributed to newly + * converted interface cells. If there are no newly converted interface cells, the excess mass is distributed to + * old interface cells. The excess mass is distributed to neighboring liquid cells only if there are no neighboring + * interface cells available. * ********************************************************************************************************************/ class ExcessMassDistributionModel { @@ -83,8 +89,9 @@ class ExcessMassDistributionModel WeightedAllInterface, WeightedNewInterface, WeightedOldInterface, - EvenlyLiquidAndAllInterface, - EvenlyLiquidAndAllInterfacePreferInterface + EvenlyAllInterfaceAndLiquid, + EvenlyAllInterfaceFallbackLiquid, + EvenlyNewInterfaceFallbackLiquid }; ExcessMassDistributionModel(const std::string& modelName) : modelName_(modelName), modelType_(chooseType(modelName)) @@ -107,9 +114,11 @@ class ExcessMassDistributionModel break; case ExcessMassModel::WeightedOldInterface: break; - case ExcessMassModel::EvenlyLiquidAndAllInterface: + case ExcessMassModel::EvenlyAllInterfaceAndLiquid: + break; + case ExcessMassModel::EvenlyAllInterfaceFallbackLiquid: break; - case ExcessMassModel::EvenlyLiquidAndAllInterfacePreferInterface: + case ExcessMassModel::EvenlyNewInterfaceFallbackLiquid: break; } } @@ -130,10 +139,12 @@ class ExcessMassDistributionModel modelType_ == ExcessMassModel::WeightedNewInterface || modelType_ == ExcessMassModel::WeightedOldInterface; } - inline bool isEvenlyLiquidAndAllInterfacePreferInterfaceType() const + inline bool isEvenlyAllInterfaceFallbackLiquidType() const { - return modelType_ == ExcessMassModel::EvenlyLiquidAndAllInterface || - modelType_ == ExcessMassModel::EvenlyLiquidAndAllInterfacePreferInterface; + return modelType_ == ExcessMassModel::EvenlyAllInterfaceAndLiquid || + modelType_ == ExcessMassModel::EvenlyAllInterfaceFallbackLiquid || + modelType_ == ExcessMassModel::EvenlyNewInterfaceFallbackLiquid; + ; } static inline std::initializer_list< const ExcessMassModel > getTypeIterator() { return listOfAllEnums; } @@ -153,14 +164,19 @@ class ExcessMassDistributionModel if (!string_icompare(modelName, "WeightedOldInterface")) { return ExcessMassModel::WeightedOldInterface; } - if (!string_icompare(modelName, "EvenlyLiquidAndAllInterface")) + if (!string_icompare(modelName, "EvenlyAllInterfaceAndLiquid")) + { + return ExcessMassModel::EvenlyAllInterfaceAndLiquid; + } + + if (!string_icompare(modelName, "EvenlyAllInterfaceFallbackLiquid")) { - return ExcessMassModel::EvenlyLiquidAndAllInterface; + return ExcessMassModel::EvenlyAllInterfaceFallbackLiquid; } - if (!string_icompare(modelName, "EvenlyLiquidAndAllInterfacePreferInterface")) + if (!string_icompare(modelName, "EvenlyNewInterfaceFallbackLiquid")) { - return ExcessMassModel::EvenlyLiquidAndAllInterfacePreferInterface; + return ExcessMassModel::EvenlyNewInterfaceFallbackLiquid; } WALBERLA_ABORT("The specified PDF reinitialization model " << modelName << " is not available."); @@ -190,11 +206,14 @@ class ExcessMassDistributionModel modelName = "WeightedOldInterface"; break; - case ExcessMassModel::EvenlyLiquidAndAllInterface: - modelName = "EvenlyLiquidAndAllInterface"; + case ExcessMassModel::EvenlyAllInterfaceAndLiquid: + modelName = "EvenlyAllInterfaceAndLiquid"; + break; + case ExcessMassModel::EvenlyAllInterfaceFallbackLiquid: + modelName = "EvenlyAllInterfaceFallbackLiquid"; break; - case ExcessMassModel::EvenlyLiquidAndAllInterfacePreferInterface: - modelName = "EvenlyLiquidAndAllInterfacePreferInterface"; + case ExcessMassModel::EvenlyNewInterfaceFallbackLiquid: + modelName = "EvenlyNewInterfaceFallbackLiquid"; break; } return modelName; @@ -203,10 +222,15 @@ class ExcessMassDistributionModel 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 + ExcessMassModel::EvenlyAllInterface, + ExcessMassModel::EvenlyNewInterface, + ExcessMassModel::EvenlyOldInterface, + ExcessMassModel::WeightedAllInterface, + ExcessMassModel::WeightedNewInterface, + ExcessMassModel::WeightedOldInterface, + ExcessMassModel::EvenlyAllInterfaceAndLiquid, + ExcessMassModel::EvenlyAllInterfaceFallbackLiquid, + ExcessMassModel::EvenlyNewInterfaceFallbackLiquid }; }; // class ExcessMassDistributionModel diff --git a/src/lbm/free_surface/dynamics/ExcessMassDistributionSweep.h b/src/lbm/free_surface/dynamics/ExcessMassDistributionSweep.h index ab9efe397..34ebe1b20 100644 --- a/src/lbm/free_surface/dynamics/ExcessMassDistributionSweep.h +++ b/src/lbm/free_surface/dynamics/ExcessMassDistributionSweep.h @@ -71,9 +71,8 @@ class ExcessMassDistributionSweepBase /******************************************************************************************************************** * 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); + void getNumberOfLiquidAndInterfaceNeighbors(const FlagField_T* flagField, const Cell& cell, uint_t& liquidNeighbors, + uint_t& interfaceNeighbors, uint_t& newInterfaceNeighbors); ExcessMassDistributionModel excessMassDistributionModel_; BlockDataID fillFieldID_; @@ -171,6 +170,8 @@ 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 + * - new neighboring interface cells, if not available to old 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. diff --git a/src/lbm/free_surface/dynamics/ExcessMassDistributionSweep.impl.h b/src/lbm/free_surface/dynamics/ExcessMassDistributionSweep.impl.h index ba30c8445..12ffe0317 100644 --- a/src/lbm/free_surface/dynamics/ExcessMassDistributionSweep.impl.h +++ b/src/lbm/free_surface/dynamics/ExcessMassDistributionSweep.impl.h @@ -79,18 +79,23 @@ void ExcessMassDistributionSweepInterfaceEvenly< LatticeModel_T, FlagField_T, Sc 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) + getNumberOfLiquidAndInterfaceNeighbors(const FlagField_T* flagField, const Cell& cell, uint_t& liquidNeighbors, + uint_t& interfaceNeighbors, uint_t& newInterfaceNeighbors) { - interfaceNeighbors = uint_c(0); - liquidNeighbors = uint_c(0); + newInterfaceNeighbors = uint_c(0); + 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; } + if (isFlagSet(neighborFlags, flagInfo_.interfaceFlag)) + { + ++interfaceNeighbors; + if (isFlagSet(neighborFlags, flagInfo_.convertedFlag)) { ++newInterfaceNeighbors; } + } else { if (isFlagSet(neighborFlags, flagInfo_.liquidFlag)) { ++liquidNeighbors; } @@ -147,7 +152,8 @@ void ExcessMassDistributionSweepInterfaceEvenly< LatticeModel_T, FlagField_T, Sc if (interfaceNeighbors == uint_c(0)) { - WALBERLA_LOG_WARNING("No interface cell is in the neighborhood to distribute excess mass to. Mass is lost."); + WALBERLA_LOG_WARNING( + "No interface cell is in the neighborhood to distribute excess mass to. Mass is lost/gained."); return; } @@ -285,7 +291,8 @@ void ExcessMassDistributionSweepInterfaceWeighted< LatticeModel_T, FlagField_T, if (interfaceNeighbors == uint_c(0)) { - WALBERLA_LOG_WARNING("No interface cell is in the neighborhood to distribute excess mass to. Mass is lost."); + WALBERLA_LOG_WARNING( + "No interface cell is in the neighborhood to distribute excess mass to. Mass is lost/gained."); return; } @@ -505,7 +512,9 @@ void ExcessMassDistributionSweepInterfaceAndLiquid< LatticeModel_T, FlagField_T, // 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 + // store excess mass such that it can be distributed below (no += here because cell was an interface cell + // that can not have an excess mass stored in the field; any excess mass is added to the interface cell's + // fill level) srcExcessMassField->get(cell) = excessFill * pdfField->getDensity(cell); if (newGas) { fillField->get(cell) = real_c(0.0); } @@ -533,28 +542,41 @@ void ExcessMassDistributionSweepInterfaceAndLiquid< LatticeModel_T, FlagField_T, 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; + uint_t liquidNeighbors = uint_c(0); + uint_t interfaceNeighbors = uint_c(0); + uint_t newInterfaceNeighbors = uint_c(0); + Base_T::getNumberOfLiquidAndInterfaceNeighbors(flagField, cell, liquidNeighbors, interfaceNeighbors, + newInterfaceNeighbors); + const uint_t liquidAndInterfaceNeighbors = liquidNeighbors + interfaceNeighbors; - if (EvenlyLiquidAndAllInterfacePreferInterfaceNeighbors == uint_c(0)) + if (liquidAndInterfaceNeighbors == uint_c(0)) { WALBERLA_LOG_WARNING( - "No liquid or interface cell is in the neighborhood to distribute excess mass to. Mass is lost."); + "No liquid or interface cell is in the neighborhood to distribute excess mass to. Mass is lost/gained."); return; } - const bool preferInterface = - Base_T::excessMassDistributionModel_.getModelType() == - ExcessMassDistributionModel::ExcessMassModel::EvenlyLiquidAndAllInterfacePreferInterface && - interfaceNeighbors > uint_c(0); + // check if there are neighboring new interface cells + const bool preferNewInterface = Base_T::excessMassDistributionModel_.getModelType() == + ExcessMassDistributionModel::ExcessMassModel::EvenlyNewInterfaceFallbackLiquid && + newInterfaceNeighbors > uint_c(0); + + // check if there are neighboring interface cells + const bool preferInterface = (Base_T::excessMassDistributionModel_.getModelType() == + ExcessMassDistributionModel::ExcessMassModel::EvenlyAllInterfaceFallbackLiquid || + !preferNewInterface) && + interfaceNeighbors > uint_c(0) && + Base_T::excessMassDistributionModel_.getModelType() != + ExcessMassDistributionModel::ExcessMassModel::EvenlyAllInterfaceAndLiquid; // compute mass to be distributed to neighboring cells real_t deltaMass; - if (preferInterface) { deltaMass = excessMass / real_c(interfaceNeighbors); } - else { deltaMass = excessMass / real_c(EvenlyLiquidAndAllInterfacePreferInterfaceNeighbors); } + if (preferNewInterface) { deltaMass = excessMass / real_c(newInterfaceNeighbors); } + else + { + if (preferInterface) { deltaMass = excessMass / real_c(interfaceNeighbors); } + else { deltaMass = excessMass / real_c(liquidAndInterfaceNeighbors); } + } // distribute the excess mass for (auto pushDir = LatticeModel_T::Stencil::beginNoCenter(); pushDir != LatticeModel_T::Stencil::end(); ++pushDir) @@ -571,20 +593,35 @@ void ExcessMassDistributionSweepInterfaceAndLiquid< LatticeModel_T, FlagField_T, continue; } - if (flagField->isFlagSet(neighborCell, Base_T::flagInfo_.interfaceFlag)) + // distribute excess mass to newly converted interface cell + if (flagField->isMaskSet(neighborCell, Base_T::flagInfo_.convertedFlag | 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 + // add excess mass directly to fill level for newly converted neighboring interface cells fillField->get(neighborCell) += deltaMass / neighborDensity; } else { - if (flagField->isFlagSet(neighborCell, Base_T::flagInfo_.liquidFlag) && !preferInterface) + // distribute excess mass to old interface cell + if (flagField->isFlagSet(neighborCell, Base_T::flagInfo_.interfaceFlag) && !preferNewInterface) { - // add excess mass to excessMassField for neighboring liquid cells - dstExcessMassField->get(neighborCell) += deltaMass; + // 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 + { + // distribute excess mass to liquid cell + if (flagField->isFlagSet(neighborCell, Base_T::flagInfo_.liquidFlag) && !preferInterface && + !preferNewInterface) + { + // add excess mass to excessMassField for neighboring liquid cells + dstExcessMassField->get(neighborCell) += deltaMass; + } } } } diff --git a/src/lbm/free_surface/dynamics/SurfaceDynamicsHandler.h b/src/lbm/free_surface/dynamics/SurfaceDynamicsHandler.h index d65469940..7180ec57c 100644 --- a/src/lbm/free_surface/dynamics/SurfaceDynamicsHandler.h +++ b/src/lbm/free_surface/dynamics/SurfaceDynamicsHandler.h @@ -103,7 +103,7 @@ class SurfaceDynamicsHandler "generate the Smagorinsky model directly into the kernel."); } - if (excessMassDistributionModel_.isEvenlyLiquidAndAllInterfacePreferInterfaceType()) + if (excessMassDistributionModel_.isEvenlyAllInterfaceFallbackLiquidType()) { // add additional field for storing excess mass in liquid cells excessMassFieldID_ = @@ -389,14 +389,16 @@ class SurfaceDynamicsHandler } else { - if (excessMassDistributionModel_.isEvenlyLiquidAndAllInterfacePreferInterfaceType()) + if (excessMassDistributionModel_.isEvenlyAllInterfaceFallbackLiquidType()) { const ExcessMassDistributionSweepInterfaceAndLiquid< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > distributeMassSweep(excessMassDistributionModel_, fillFieldID_, flagFieldID_, pdfFieldID_, flagInfo, excessMassFieldID_); timeloop.add() + // perform this sweep also on "onlyLBM" blocks because liquid cells also exchange excess mass here << Sweep(distributeMassSweep, "Sweep: excess mass distribution", StateSweep::fullFreeSurface) + << Sweep(distributeMassSweep, "Sweep: excess mass distribution", StateSweep::onlyLBM) << Sweep(emptySweep, "Empty sweep: distribute excess mass") << AfterFunction(CommunicationCorner_T(blockForest_, fillFieldID_, excessMassFieldID_), "Communication: after excess mass distribution sweep") diff --git a/tests/lbm/free_surface/dynamics/ExcessMassDistributionParallelTest.cpp b/tests/lbm/free_surface/dynamics/ExcessMassDistributionParallelTest.cpp index 8c99cf872..3bda9fd57 100644 --- a/tests/lbm/free_surface/dynamics/ExcessMassDistributionParallelTest.cpp +++ b/tests/lbm/free_surface/dynamics/ExcessMassDistributionParallelTest.cpp @@ -275,7 +275,7 @@ void runSimulation(const ExcessMassDistributionModel& excessMassDistributionMode } else { - if (excessMassDistributionModel.isEvenlyLiquidAndAllInterfacePreferInterfaceType()) + if (excessMassDistributionModel.isEvenlyAllInterfaceFallbackLiquidType()) { const ExcessMassDistributionSweepInterfaceAndLiquid< LatticeModel_T, FlagField_T, ScalarField_T, VectorField_T > @@ -876,7 +876,7 @@ void runSimulation(const ExcessMassDistributionModel& excessMassDistributionMode } if (excessMassDistributionModel.getModelType() == - ExcessMassDistributionModel::ExcessMassModel::EvenlyLiquidAndAllInterface) + ExcessMassDistributionModel::ExcessMassModel::EvenlyAllInterfaceAndLiquid) { // left block if (globalCell == Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0))) @@ -990,7 +990,7 @@ void runSimulation(const ExcessMassDistributionModel& excessMassDistributionMode } if (excessMassDistributionModel.getModelType() == - ExcessMassDistributionModel::ExcessMassModel::EvenlyLiquidAndAllInterfacePreferInterface) + ExcessMassDistributionModel::ExcessMassModel::EvenlyAllInterfaceFallbackLiquid) { // left block if (globalCell == Cell(cell_idx_c(0), cell_idx_c(0), cell_idx_c(0))) @@ -1102,6 +1102,120 @@ void runSimulation(const ExcessMassDistributionModel& excessMassDistributionMode WALBERLA_CHECK_FLOAT_EQUAL_EPSILON(*excessMassFieldIt, real_c(0), real_c(1e-4)); } } + + if (excessMassDistributionModel.getModelType() == + ExcessMassDistributionModel::ExcessMassModel::EvenlyNewInterfaceFallbackLiquid) + { + // 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.7), 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.5), 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.5), 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.590909090909091), 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.5), 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.595), 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.615277777777778), 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.605952380952381), 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.5), 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.573958333333333), 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 } @@ -1137,11 +1251,15 @@ int main(int argc, char** argv) WALBERLA_LOG_INFO_ON_ROOT("Testing model " << model.getFullModelSpecification()); runSimulation(model); - model = ExcessMassDistributionModel("EvenlyLiquidAndAllInterface"); + model = ExcessMassDistributionModel("EvenlyAllInterfaceAndLiquid"); + WALBERLA_LOG_INFO_ON_ROOT("Testing model " << model.getFullModelSpecification()); + runSimulation(model); + + model = ExcessMassDistributionModel("EvenlyAllInterfaceFallbackLiquid"); WALBERLA_LOG_INFO_ON_ROOT("Testing model " << model.getFullModelSpecification()); runSimulation(model); - model = ExcessMassDistributionModel("EvenlyLiquidAndAllInterfacePreferInterface"); + model = ExcessMassDistributionModel("EvenlyNewInterfaceFallbackLiquid"); WALBERLA_LOG_INFO_ON_ROOT("Testing model " << model.getFullModelSpecification()); runSimulation(model); diff --git a/tests/lbm/free_surface/dynamics/ExcessMassDistributionParallelTest.ods b/tests/lbm/free_surface/dynamics/ExcessMassDistributionParallelTest.ods index fa159c7b1b01c41933c16c886af01b6344a26bf9..8f0210d1c68a269eb7401a58947b64ebbc5bab05 100644 GIT binary patch delta 17171 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 FgaGwjF?av~ delta 14467 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 GNE86&#CL4~ -- GitLab