diff --git a/apps/benchmarks/CMakeLists.txt b/apps/benchmarks/CMakeLists.txt index a59d3bc1a5d23a3f52b827507d756c5b2117a877..66e99fb276c27e6294b172e3abc8f60c699555a7 100644 --- a/apps/benchmarks/CMakeLists.txt +++ b/apps/benchmarks/CMakeLists.txt @@ -4,6 +4,7 @@ add_subdirectory( DEM ) add_subdirectory( FieldCommunication ) add_subdirectory( MeshDistance ) add_subdirectory( CouetteFlow ) +add_subdirectory( FluidParticleCoupling ) add_subdirectory( ForcesOnSphereNearPlaneInShearFlow ) add_subdirectory( GranularGas ) add_subdirectory( LennardJones ) diff --git a/apps/benchmarks/FluidParticleCoupling/CMakeLists.txt b/apps/benchmarks/FluidParticleCoupling/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..a22871bb3eb9db47c10b47ca3a08f7812d5d5068 --- /dev/null +++ b/apps/benchmarks/FluidParticleCoupling/CMakeLists.txt @@ -0,0 +1,33 @@ +waLBerla_link_files_to_builddir( "*.dat" ) + +waLBerla_python_file_generates(GeneratedLBM.py + GeneratedLBM.cpp GeneratedLBM.h + ) + +waLBerla_python_file_generates(GeneratedLBMWithForce.py + GeneratedLBMWithForce.cpp GeneratedLBMWithForce.h + ) + +waLBerla_add_executable ( NAME SphereWallCollision FILES SphereWallCollision.cpp GeneratedLBM.py + DEPENDS blockforest boundary core domain_decomposition field lbm lbm_mesapd_coupling mesa_pd postprocessing timeloop vtk ) + +waLBerla_add_executable ( NAME SettlingSphereTenCate FILES SettlingSphereInBox.cpp GeneratedLBM.py + DEPENDS blockforest boundary core domain_decomposition field lbm lbm_mesapd_coupling mesa_pd postprocessing timeloop vtk ) + +waLBerla_add_executable ( NAME SphereMovingWithPrescribedVelocity FILES SphereMovingWithPrescribedVelocity.cpp GeneratedLBM.py + DEPENDS blockforest boundary core domain_decomposition field lbm mesa_pd lbm_mesapd_coupling postprocessing timeloop vtk ) + +waLBerla_add_executable ( NAME LubricationForceEvaluation FILES LubricationForceEvaluation.cpp GeneratedLBM.py + DEPENDS blockforest boundary core domain_decomposition field lbm lbm_mesapd_coupling mesa_pd postprocessing timeloop vtk ) + +waLBerla_add_executable ( NAME DragForceSphere FILES DragForceSphere.cpp GeneratedLBMWithForce.py + DEPENDS blockforest boundary core domain_decomposition field lbm lbm_mesapd_coupling mesa_pd postprocessing timeloop vtk ) + +waLBerla_add_executable ( NAME ForcesOnSphereNearPlane FILES ForcesOnSphereNearPlane.cpp GeneratedLBM.py + DEPENDS blockforest boundary core domain_decomposition field lbm lbm_mesapd_coupling mesa_pd postprocessing timeloop vtk ) + +waLBerla_add_executable ( NAME ObliqueDryCollision FILES ObliqueDryCollision.cpp + DEPENDS blockforest core mesa_pd postprocessing ) + +waLBerla_add_executable ( NAME ObliqueWetCollision FILES ObliqueWetCollision.cpp GeneratedLBM.py + DEPENDS blockforest boundary core domain_decomposition field lbm lbm_mesapd_coupling mesa_pd postprocessing timeloop vtk ) \ No newline at end of file diff --git a/apps/benchmarks/FluidParticleCoupling/DragForceSphere.cpp b/apps/benchmarks/FluidParticleCoupling/DragForceSphere.cpp new file mode 100644 index 0000000000000000000000000000000000000000..2f269c5d944d021820fe9f63af86ff5c3671798d --- /dev/null +++ b/apps/benchmarks/FluidParticleCoupling/DragForceSphere.cpp @@ -0,0 +1,731 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla is free software: you can +// redistribute it and/or modify it under the terms of the GNU General Public +// License as published by the Free Software Foundation, either version 3 of +// the License, or (at your option) any later version. +// +// waLBerla is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +// for more details. +// +// You should have received a copy of the GNU General Public License along +// with waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file DragForceSphere.cpp +//! \ingroup lbm_mesapd_coupling +//! \author Christoph Rettinger <christoph.rettinger@fau.de> +// +//====================================================================================================================== + +#include "blockforest/Initialization.h" +#include "blockforest/communication/UniformBufferedScheme.h" + +#include "boundary/all.h" + +#include "core/DataTypes.h" +#include "core/Environment.h" +#include "core/debug/Debug.h" +#include "core/debug/TestSubsystem.h" +#include "core/logging/Logging.h" +#include "core/math/all.h" +#include "core/SharedFunctor.h" +#include "core/timing/RemainingTimeLogger.h" +#include "core/mpi/MPIManager.h" +#include "core/mpi/Reduce.h" + +#include "domain_decomposition/SharedSweep.h" + +#include "field/AddToStorage.h" +#include "field/communication/PackInfo.h" + +#include "lbm/boundary/all.h" +#include "lbm/communication/PdfFieldPackInfo.h" +#include "lbm/field/AddToStorage.h" +#include "lbm/field/MacroscopicValueCalculation.h" +#include "lbm/field/PdfField.h" +#include "lbm/lattice_model/D3Q19.h" +#include "lbm/lattice_model/ForceModel.h" +#include "lbm/sweeps/CellwiseSweep.h" +#include "lbm/sweeps/SweepWrappers.h" + +#include "lbm_mesapd_coupling/momentum_exchange_method/MovingParticleMapping.h" +#include "lbm_mesapd_coupling/momentum_exchange_method/boundary/SimpleBB.h" +#include "lbm_mesapd_coupling/momentum_exchange_method/boundary/CurvedLinear.h" +#include "lbm_mesapd_coupling/utility/ResetHydrodynamicForceTorqueKernel.h" +#include "lbm_mesapd_coupling/utility/ParticleSelector.h" +#include "lbm_mesapd_coupling/DataTypes.h" +#include "lbm_mesapd_coupling/utility/OmegaBulkAdaption.h" + +#include "mesa_pd/data/DataTypes.h" +#include "mesa_pd/data/ParticleAccessorWithShape.h" +#include "mesa_pd/data/ParticleStorage.h" +#include "mesa_pd/data/ShapeStorage.h" +#include "mesa_pd/domain/BlockForestDomain.h" +#include "mesa_pd/kernel/ParticleSelector.h" +#include "mesa_pd/vtk/ParticleVtkOutput.h" + +#include "stencil/D3Q27.h" + +#include "timeloop/SweepTimeloop.h" + +#include "vtk/all.h" +#include "field/vtk/all.h" +#include "lbm/vtk/all.h" + +#include <iomanip> +#include <iostream> + +#include "GeneratedLBMWithForce.h" + +#define USE_TRT_LIKE_LATTICE_MODEL + +namespace drag_force_sphere_mem +{ + +/////////// +// USING // +/////////// + +using namespace walberla; +using walberla::uint_t; + +using LatticeModel_T = lbm::GeneratedLBMWithForce; + +using Stencil_T = LatticeModel_T::Stencil; +using PdfField_T = lbm::PdfField<LatticeModel_T>; + +using flag_t = walberla::uint8_t; +using FlagField_T = FlagField<flag_t>; + +using ScalarField_T = GhostLayerField< real_t, 1>; + +const uint_t FieldGhostLayers = 1; + +/////////// +// FLAGS // +/////////// + +const FlagUID Fluid_Flag ( "fluid" ); +const FlagUID MO_BB_Flag ( "moving obstacle BB" ); +const FlagUID MO_CLI_Flag ( "moving obstacle CLI" ); + +//////////////// +// PARAMETERS // +//////////////// + +struct Setup{ + uint_t checkFrequency; + real_t visc; + real_t tau; + real_t radius; + uint_t length; + real_t chi; + real_t extForce; + real_t analyticalDrag; +}; + +enum MEMVariant { BB, CLI }; + +MEMVariant to_MEMVariant( const std::string& s ) +{ + if( s == "BB" ) return MEMVariant::BB; + if( s == "CLI" ) return MEMVariant::CLI; + throw std::runtime_error("invalid conversion from text to MEMVariant"); +} + +std::string MEMVariant_to_string ( const MEMVariant& m ) +{ + if( m == MEMVariant::BB ) return "BB"; + if( m == MEMVariant::CLI ) return "CLI"; + throw std::runtime_error("invalid conversion from MEMVariant to string"); +} + +///////////////////////////////////// +// BOUNDARY HANDLING CUSTOMIZATION // +///////////////////////////////////// + +template <typename ParticleAccessor_T> +class MyBoundaryHandling +{ +public: + + using SBB_T = lbm_mesapd_coupling::SimpleBB< LatticeModel_T, FlagField_T, ParticleAccessor_T >; + using CLI_T = lbm_mesapd_coupling::CurvedLinear< LatticeModel_T, FlagField_T, ParticleAccessor_T >; + using Type = BoundaryHandling< FlagField_T, Stencil_T, SBB_T, CLI_T >; + + MyBoundaryHandling( const BlockDataID & flagFieldID, const BlockDataID & pdfFieldID, + const BlockDataID & particleFieldID, const shared_ptr<ParticleAccessor_T>& ac) : + flagFieldID_( flagFieldID ), pdfFieldID_( pdfFieldID ), particleFieldID_( particleFieldID ), ac_( ac ) {} + + Type * operator()( IBlock* const block, const StructuredBlockStorage* const storage ) const + { + WALBERLA_ASSERT_NOT_NULLPTR( block ); + WALBERLA_ASSERT_NOT_NULLPTR( storage ); + + auto * flagField = block->getData< FlagField_T >( flagFieldID_ ); + auto * pdfField = block->getData< PdfField_T > ( pdfFieldID_ ); + auto * particleField = block->getData< lbm_mesapd_coupling::ParticleField_T > ( particleFieldID_ ); + + const auto fluid = flagField->flagExists( Fluid_Flag ) ? flagField->getFlag( Fluid_Flag ) : flagField->registerFlag( Fluid_Flag ); + + Type * handling = new Type( "moving obstacle boundary handling", flagField, fluid, + SBB_T("SBB_BB", MO_BB_Flag, pdfField, flagField, particleField, ac_, fluid, *storage, *block ), + CLI_T("CLI_BB", MO_CLI_Flag, pdfField, flagField, particleField, ac_, fluid, *storage, *block ) ); + + handling->fillWithDomain( FieldGhostLayers ); + + return handling; + } + +private: + + const BlockDataID flagFieldID_; + const BlockDataID pdfFieldID_; + const BlockDataID particleFieldID_; + + shared_ptr<ParticleAccessor_T> ac_; +}; + + +template< typename ParticleAccessor_T> +class DragForceEvaluator +{ +public: + DragForceEvaluator( SweepTimeloop* timeloop, Setup* setup, const shared_ptr< StructuredBlockStorage > & blocks, + const BlockDataID & flagFieldID, const BlockDataID & pdfFieldID, + const shared_ptr<ParticleAccessor_T>& ac, walberla::id_t sphereID ) +: timeloop_( timeloop ), setup_( setup ), blocks_( blocks ), + flagFieldID_( flagFieldID ), pdfFieldID_( pdfFieldID ), + ac_( ac ), sphereID_( sphereID ), + normalizedDragOld_( 0.0 ), normalizedDragNew_( 0.0 ) + { + // calculate the analytical drag force value based on the series expansion of chi + // see also Sangani - Slow flow through a periodic array of spheres, IJMF 1982. Eq. 60 and Table 1 + real_t analyticalDrag = real_c(0); + real_t tempChiPowS = real_c(1); + + // coefficients to calculate the drag in a series expansion + real_t dragCoefficients[31] = { real_c(1.000000), real_c(1.418649), real_c(2.012564), real_c(2.331523), real_c(2.564809), real_c(2.584787), + real_c(2.873609), real_c(3.340163), real_c(3.536763), real_c(3.504092), real_c(3.253622), real_c(2.689757), + real_c(2.037769), real_c(1.809341), real_c(1.877347), real_c(1.534685), real_c(0.9034708), real_c(0.2857896), + real_c(-0.5512626), real_c(-1.278724), real_c(1.013350), real_c(5.492491), real_c(4.615388), real_c(-0.5736023), + real_c(-2.865924), real_c(-4.709215), real_c(-6.870076), real_c(0.1455304), real_c(12.51891), real_c(9.742811), + real_c(-5.566269)}; + + for(uint_t s = 0; s <= uint_t(30); ++s){ + analyticalDrag += dragCoefficients[s] * tempChiPowS; + tempChiPowS *= setup->chi; + } + setup_->analyticalDrag = analyticalDrag; + } + + // evaluate the acting drag force + void operator()() + { + const uint_t timestep (timeloop_->getCurrentTimeStep()+1); + + if( timestep % setup_->checkFrequency != 0) return; + + // get force in x-direction acting on the sphere + real_t forceX = computeDragForce(); + // get average volumetric flowrate in the domain + real_t uBar = computeAverageVel(); + + averageVel_ = uBar; + + // f_total = f_drag + f_buoyancy + real_t totalForce = forceX + real_c(4.0/3.0) * math::pi * setup_->radius * setup_->radius * setup_->radius * setup_->extForce ; + + real_t normalizedDragForce = totalForce / real_c( 6.0 * math::pi * setup_->visc * setup_->radius * uBar ); + + // update drag force values + normalizedDragOld_ = normalizedDragNew_; + normalizedDragNew_ = normalizedDragForce; + } + + // return the relative temporal change in the normalized drag + real_t getDragForceDiff() const + { + return std::fabs( ( normalizedDragNew_ - normalizedDragOld_ ) / normalizedDragNew_ ); + } + + // return the drag force + real_t getDragForce() const + { + return normalizedDragNew_; + } + + real_t getAverageVel() + { + return averageVel_; + } + + void logResultToFile( const std::string & filename ) const + { + // write to file if desired + // format: length tau viscosity simulatedDrag analyticalDrag\n + WALBERLA_ROOT_SECTION() + { + std::ofstream file; + file.open( filename.c_str(), std::ofstream::app ); + file.precision(8); + file << setup_->length << " " << setup_->tau << " " << setup_->visc << " " << normalizedDragNew_ << " " << setup_->analyticalDrag << "\n"; + file.close(); + } + } + +private: + + // obtain the drag force acting on the sphere by summing up all the process local parts of fX + real_t computeDragForce() + { + size_t idx = ac_->uidToIdx(sphereID_); + real_t force = real_t(0); + if( idx!= ac_->getInvalidIdx()) + { + force = ac_->getHydrodynamicForce(idx)[0]; + } + + WALBERLA_MPI_SECTION() + { + mpi::allReduceInplace( force, mpi::SUM ); + } + + return force; + } + + // calculate the average velocity in forcing direction (here: x) inside the domain (assuming dx=1) + real_t computeAverageVel() + { + auto velocity_sum = real_t(0); + // iterate all blocks stored locally on this process + for( auto blockIt = blocks_->begin(); blockIt != blocks_->end(); ++blockIt ) + { + // retrieve the pdf field and the flag field from the block + PdfField_T * pdfField = blockIt->getData< PdfField_T >( pdfFieldID_ ); + FlagField_T * flagField = blockIt->getData< FlagField_T >( flagFieldID_ ); + + // get the flag that marks a cell as being fluid + auto fluid = flagField->getFlag( Fluid_Flag ); + + auto xyzField = pdfField->xyzSize(); + for (auto cell : xyzField) { + if( flagField->isFlagSet( cell.x(), cell.y(), cell.z(), fluid ) ) + { + velocity_sum += pdfField->getVelocity(cell)[0]; + } + } + } + + WALBERLA_MPI_SECTION() + { + mpi::allReduceInplace( velocity_sum, mpi::SUM ); + } + + return velocity_sum / real_c( setup_->length * setup_->length * setup_->length ); + } + + SweepTimeloop* timeloop_; + + Setup* setup_; + + shared_ptr< StructuredBlockStorage > blocks_; + const BlockDataID flagFieldID_; + const BlockDataID pdfFieldID_; + + shared_ptr<ParticleAccessor_T> ac_; + const walberla::id_t sphereID_; + + // drag coefficient + real_t normalizedDragOld_; + real_t normalizedDragNew_; + + real_t averageVel_; + +}; + +////////// +// MAIN // +////////// + + +//******************************************************************************************************************* +/*!\brief Testcase that checks the drag force acting on a fixed sphere in the center of a cubic domain in Stokes flow + * + * The drag force for this problem (often denoted as Simple Cubic setup) is given by a semi-analytical series expansion. + * The cubic domain is periodic in all directions, making it a physically infinite periodic array of spheres. + * _______________ + * ->| |-> + * ->| ___ |-> + * W ->| / \ |-> E + * E ->| | x | |-> A + * S ->| \___/ |-> S + * T ->| |-> T + * ->|_______________|-> + * + * The collision model used for the LBM is TRT with a relaxation parameter tau=1.5 and the magic parameter 3/16. + * The Stokes approximation of the equilibrium PDFs is used. + * The flow is driven by a constant body force of 1e-5. + * The domain is length x length x length, and the sphere has a diameter of chi * length cells + * The simulation is run until the relative change in the dragforce between 100 time steps is less than 1e-5. + * The RPD is not used since the sphere is kept fixed and the force is explicitly reset after each time step. + * To avoid periodicity constrain problems, the sphere is set as global. + * + */ +//******************************************************************************************************************* + + +int main( int argc, char **argv ) +{ + debug::enterTestMode(); + + mpi::Environment env( argc, argv ); + + auto processes = MPIManager::instance()->numProcesses(); + + if( processes != 1 && processes != 2 && processes != 4 && processes != 8) + { + std::cerr << "Number of processes must be equal to either 1, 2, 4, or 8!" << std::endl; + return EXIT_FAILURE; + } + + /////////////////// + // Customization // + /////////////////// + + bool shortrun = false; + bool funcTest = false; + bool logging = true; + uint_t vtkIOFreq = 0; + std::string baseFolder = "vtk_out_DragForceSphere"; + + real_t tau = real_c( 1.5 ); + real_t externalForcing = real_t(1e-8); + uint_t length = uint_c( 40 ); + + MEMVariant method = MEMVariant::CLI; + real_t bulkViscRateFactor = real_t(1); // ratio between bulk and shear viscosity related relaxation factors, 1 = TRT, > 1 for stability with MRT + // see Khirevich - Coarse- and fine-grid numerical behavior of MRT/TRT lattice-Boltzmann schemes in regular and random sphere packings + bool useSRT = false; + real_t magicNumber = real_t(3) / real_t(16); + bool useOmegaBulkAdaption = false; + real_t adaptionLayerSize = real_t(2); + + + for( int i = 1; i < argc; ++i ) + { + if( std::strcmp( argv[i], "--shortrun" ) == 0 ) { shortrun = true; continue; } + if( std::strcmp( argv[i], "--funcTest" ) == 0 ) { funcTest = true; continue; } + if( std::strcmp( argv[i], "--noLogging" ) == 0 ) { logging = false; continue; } + if( std::strcmp( argv[i], "--vtkIOFreq" ) == 0 ) { vtkIOFreq = uint_c( std::atof( argv[++i] ) ); continue; } + if( std::strcmp( argv[i], "--MEMVariant" ) == 0 ) { method = to_MEMVariant( argv[++i] ); continue; } + if( std::strcmp( argv[i], "--tau" ) == 0 ) { tau = real_c( std::atof( argv[++i] ) ); continue; } + if( std::strcmp( argv[i], "--extForce" ) == 0 ) { externalForcing = real_c( std::atof( argv[++i] ) ); continue; } + if( std::strcmp( argv[i], "--length" ) == 0 ) { length = uint_c( std::atof( argv[++i] ) ); continue; } + if( std::strcmp( argv[i], "--bulkViscRateFactor" ) == 0 ) { bulkViscRateFactor = real_c( std::atof( argv[++i] ) ); continue; } + if( std::strcmp( argv[i], "--useSRT" ) == 0 ) { useSRT = true; continue; } + if( std::strcmp( argv[i], "--magicNumber" ) == 0 ) { magicNumber = real_c( std::atof( argv[++i] ) ); continue; } + if( std::strcmp( argv[i], "--useOmegaBulkAdaption" ) == 0 ) { useOmegaBulkAdaption = true; continue; } + if( std::strcmp( argv[i], "--adaptionLayerSize" ) == 0 ) { adaptionLayerSize = real_c(std::atof( argv[++i] )); continue; } + WALBERLA_ABORT("Unrecognized command line argument found: " << argv[i]); + } + + + /////////////////////////// + // SIMULATION PROPERTIES // + /////////////////////////// + + Setup setup; + + setup.length = length; // length of the cubic domain in lattice cells + setup.chi = real_c( 0.5 ); // porosity parameter: diameter / length + setup.tau = tau; // relaxation time + setup.extForce = externalForcing; // constant body force in lattice units + setup.checkFrequency = uint_t( 100 ); // evaluate the drag force only every checkFrequency time steps + setup.radius = real_c(0.5) * setup.chi * real_c( setup.length ); // sphere radius + setup.visc = ( setup.tau - real_c(0.5) ) / real_c(3); // viscosity in lattice units + const real_t omega = real_c(1) / setup.tau; // relaxation rate + const real_t dx = real_c(1); // lattice dx + const real_t convergenceLimit = real_t(0.1) * setup.extForce; // tolerance for relative change in drag force + const uint_t timesteps = funcTest ? 1 : ( shortrun ? uint_c(150) : uint_c( 100000 ) ); // maximum number of time steps for the whole simulation + + WALBERLA_LOG_INFO_ON_ROOT("tau = " << tau); + WALBERLA_LOG_INFO_ON_ROOT("diameter = " << real_t(2) * setup.radius); + WALBERLA_LOG_INFO_ON_ROOT("viscosity = " << setup.visc); + WALBERLA_LOG_INFO_ON_ROOT("MEM variant = " << MEMVariant_to_string(method)); + WALBERLA_LOG_INFO_ON_ROOT("external forcing = " << setup.extForce); + + /////////////////////////// + // BLOCK STRUCTURE SETUP // + /////////////////////////// + + const uint_t XBlocks = (processes >= 2) ? uint_t( 2 ) : uint_t( 1 ); + const uint_t YBlocks = (processes >= 4) ? uint_t( 2 ) : uint_t( 1 ); + const uint_t ZBlocks = (processes == 8) ? uint_t( 2 ) : uint_t( 1 ); + const uint_t XCells = setup.length / XBlocks; + const uint_t YCells = setup.length / YBlocks; + const uint_t ZCells = setup.length / ZBlocks; + + // create fully periodic domain + auto blocks = blockforest::createUniformBlockGrid( XBlocks, YBlocks, ZBlocks, XCells, YCells, ZCells, dx, true, + true, true, true ); + + + ///////// + // RPD // + ///////// + + mesa_pd::domain::BlockForestDomain domain(blocks->getBlockForestPointer()); + + //init data structures + auto ps = std::make_shared<mesa_pd::data::ParticleStorage>(1); + auto ss = std::make_shared<mesa_pd::data::ShapeStorage>(); + using ParticleAccessor_T = mesa_pd::data::ParticleAccessorWithShape; + auto accessor = make_shared<ParticleAccessor_T >(ps, ss); + auto sphereShape = ss->create<mesa_pd::data::Sphere>( setup.radius ); + + ////////////////// + // RPD COUPLING // + ////////////////// + + // connect to pe + const real_t overlap = real_t( 1.5 ) * dx; + + if( setup.radius > real_c( setup.length ) * real_t(0.5) - overlap ) + { + std::cerr << "Periodic sphere is too large and would lead to incorrect mapping!" << std::endl; + // solution: create the periodic copies explicitly + return EXIT_FAILURE; + } + + // create the sphere in the middle of the domain + // it is global and thus present on all processes + + Vector3<real_t> position (real_c(setup.length) * real_c(0.5)); + walberla::id_t sphereID; + { + mesa_pd::data::Particle&& p = *ps->create(true); + p.setPosition(position); + p.setInteractionRadius(setup.radius); + p.setOwner(mpi::MPIManager::instance()->rank()); + p.setShapeID(sphereShape); + sphereID = p.getUid(); + } + + /////////////////////// + // ADD DATA TO BLOCKS // + //////////////////////// + + + real_t lambda_e = lbm::collision_model::TRT::lambda_e( omega ); + real_t lambda_d = (useSRT) ? lambda_e : lbm::collision_model::TRT::lambda_d( omega, magicNumber ); + real_t omegaBulk = (useSRT) ? lambda_e : lbm_mesapd_coupling::omegaBulkFromOmega(omega, bulkViscRateFactor); + + // add omega bulk field + BlockDataID omegaBulkFieldID = field::addToStorage<ScalarField_T>( blocks, "omega bulk field", omegaBulk, field::fzyx, uint_t(0) ); + + // create the lattice model + + // generated MRT +#ifdef USE_TRT_LIKE_LATTICE_MODEL + WALBERLA_LOG_INFO_ON_ROOT("Using TRT-like lattice model!"); + WALBERLA_LOG_INFO_ON_ROOT(" - magic number " << magicNumber); + WALBERLA_LOG_INFO_ON_ROOT(" - omegaBulk = " << omegaBulk << ", bulk visc. = " << lbm_mesapd_coupling::bulkViscosityFromOmegaBulk(omegaBulk) << " (bvrf " << bulkViscRateFactor << ")"); + WALBERLA_LOG_INFO_ON_ROOT(" - lambda_e " << lambda_e << ", lambda_d " << lambda_d << ", omegaBulk " << omegaBulk ); + WALBERLA_LOG_INFO_ON_ROOT(" - use omega bulk adaption = " << useOmegaBulkAdaption << " (adaption layer size = " << adaptionLayerSize << ")"); + + LatticeModel_T latticeModel = LatticeModel_T(omegaBulkFieldID, setup.extForce, lambda_d, lambda_e); + //LatticeModel_T latticeModel = LatticeModel_T(setup.extForce, omegaBulk, lambda_d, lambda_e); + //LatticeModel_T latticeModel = LatticeModel_T(setup.extForce, lambda_e, lambda_d); +#else + WALBERLA_LOG_INFO_ON_ROOT("Using other lattice model!"); + // generated KBC + LatticeModel_T latticeModel = LatticeModel_T(setup.extForce, omega); +#endif + + // set s_e and s_eps to omegaBulk + // s_e allows to set a specific bulk viscosity that is different from the SRT/TRT default: nu_b = 2/3 nu_shear + // Khirevich et al propose to set s_eps in the same way to avoid huge differences in s_e and s_eps (would be omega in SRT/TRT) that could lead to stability problems + // to not break TRT properties, it is proposed to set Lambda_bulk = conste * Lambda_shear (Khirevich), where conste sets the ratio between bulk and shear viscosity-terms (1 in SRT/TRT -> nu_b = 2/3 nu_shear) + // all in all, this allows to set the bulk viscosity but to retain the "TRT" properties, i.e. independent drag force of tau + //LatticeModel_T latticeModel = LatticeModel_T(lbm::collision_model::D3Q19MRT( omegaBulk, omegaBulk, lambda_d, lambda_e, lambda_e, lambda_d ), ForceModel_T( Vector3<real_t> ( setup.extForce, 0, 0 ) )); + + // add PDF field + BlockDataID pdfFieldID = lbm::addPdfFieldToStorage< LatticeModel_T >( blocks, "pdf field (fzyx)", latticeModel, + Vector3< real_t >( real_t(0) ), real_t(1), + uint_t(1), field::fzyx ); + + // add flag field + BlockDataID flagFieldID = field::addFlagFieldToStorage<FlagField_T>( blocks, "flag field" ); + + // add particle field + BlockDataID particleFieldID = field::addToStorage<lbm_mesapd_coupling::ParticleField_T>( blocks, "particle field", accessor->getInvalidUid(), field::fzyx, FieldGhostLayers ); + + // add boundary handling + using BoundaryHandling_T = MyBoundaryHandling<ParticleAccessor_T>::Type; + BlockDataID boundaryHandlingID = blocks->addStructuredBlockData< BoundaryHandling_T >(MyBoundaryHandling<ParticleAccessor_T>( flagFieldID, pdfFieldID, particleFieldID, accessor), "boundary handling" ); + + /////////////// + // TIME LOOP // + /////////////// + + // create the timeloop + SweepTimeloop timeloop( blocks->getBlockStorage(), timesteps ); + + // setup of the LBM communication for synchronizing the pdf field between neighboring blocks + blockforest::communication::UniformBufferedScheme< Stencil_T > optimizedPDFCommunicationScheme( blocks ); + optimizedPDFCommunicationScheme.addPackInfo( make_shared< lbm::PdfFieldPackInfo< LatticeModel_T > >( pdfFieldID ) ); // optimized sync + + //blockforest::communication::UniformBufferedScheme< stencil::D3Q27 > optimizedPDFCommunicationScheme( blocks ); + //optimizedPDFCommunicationScheme.addPackInfo( make_shared< field::communication::PackInfo< PdfField_T > >( pdfFieldID ) ); + + // initially map particles into the LBM simulation + lbm_mesapd_coupling::MovingParticleMappingKernel<BoundaryHandling_T> movingParticleMappingKernel(blocks, boundaryHandlingID, particleFieldID); + if( method == MEMVariant::CLI ) + { + // uses a higher order boundary condition (CLI) + ps->forEachParticle(false, mesa_pd::kernel::SelectAll(), *accessor, movingParticleMappingKernel, *accessor, MO_CLI_Flag); + }else{ + // uses standard bounce back boundary conditions + ps->forEachParticle(false, mesa_pd::kernel::SelectAll(), *accessor, movingParticleMappingKernel, *accessor, MO_BB_Flag); + } + + // update bulk omega in all cells to adapt to changed particle position + if(useOmegaBulkAdaption) + { + using OmegaBulkAdapter_T = lbm_mesapd_coupling::OmegaBulkAdapter<ParticleAccessor_T, mesa_pd::kernel::SelectAll>; + real_t defaultOmegaBulk = lbm_mesapd_coupling::omegaBulkFromOmega(omega, real_t(1)); + shared_ptr<OmegaBulkAdapter_T> omegaBulkAdapter = make_shared<OmegaBulkAdapter_T>(blocks, omegaBulkFieldID, accessor, defaultOmegaBulk, omegaBulk, adaptionLayerSize, mesa_pd::kernel::SelectAll()); + for (auto blockIt = blocks->begin(); blockIt != blocks->end(); ++blockIt) { + (*omegaBulkAdapter)(blockIt.get()); + } + } + + if( vtkIOFreq != uint_t(0) ) + { + + // spheres + auto particleVtkOutput = make_shared<mesa_pd::vtk::ParticleVtkOutput>(ps); + particleVtkOutput->addOutput<mesa_pd::data::SelectParticleLinearVelocity>("velocity"); + auto particleVtkWriter = vtk::createVTKOutput_PointData(particleVtkOutput, "Particles", vtkIOFreq, baseFolder, "simulation_step"); + timeloop.addFuncBeforeTimeStep( vtk::writeFiles( particleVtkWriter ), "VTK (sphere data)" ); + + // pdf field + auto pdfFieldVTK = vtk::createVTKOutput_BlockData( blocks, "fluid_field", vtkIOFreq, 0, false, baseFolder ); + + field::FlagFieldCellFilter< FlagField_T > fluidFilter( flagFieldID ); + fluidFilter.addFlag( Fluid_Flag ); + pdfFieldVTK->addCellInclusionFilter( fluidFilter ); + + pdfFieldVTK->addCellDataWriter( make_shared< lbm::VelocityVTKWriter< LatticeModel_T, float > >( pdfFieldID, "VelocityFromPDF" ) ); + pdfFieldVTK->addCellDataWriter( make_shared< lbm::DensityVTKWriter < LatticeModel_T, float > >( pdfFieldID, "DensityFromPDF" ) ); + + timeloop.addFuncBeforeTimeStep( vtk::writeFiles( pdfFieldVTK ), "VTK (fluid field data)" ); + + // omega bulk field + timeloop.addFuncBeforeTimeStep( field::createVTKOutput<ScalarField_T, float>( omegaBulkFieldID, *blocks, "omega_bulk_field", vtkIOFreq, uint_t(0), false, baseFolder ), "VTK (omega bulk field)" ); + } + + + // since external forcing is applied, the evaluation of the velocity has to be carried out directly after the streaming step + // however, the default sweep is a stream - collide step, i.e. after the sweep, the velocity evaluation is not correct + // solution: split the sweep explicitly into collide and stream + + using DragForceEval_T = DragForceEvaluator<ParticleAccessor_T>; + auto forceEval = make_shared< DragForceEval_T >( &timeloop, &setup, blocks, flagFieldID, pdfFieldID, accessor, sphereID ); + + auto lbmSweep = LatticeModel_T::Sweep( pdfFieldID ); + + + // splitting stream and collide is currently buggy in lbmpy with vectorization -> build without optimize for local host and vectorization + // collide LBM step + timeloop.add() << Sweep([&lbmSweep](IBlock * const block){lbmSweep.collide(block);}, "collide LB sweep" ); + + // add LBM communication function and boundary handling sweep (does the hydro force calculations and the no-slip treatment) + timeloop.add() << BeforeFunction( optimizedPDFCommunicationScheme, "LBM Communication" ) + << Sweep( BoundaryHandling_T::getBlockSweep( boundaryHandlingID ), "Boundary Handling" ); + + // stream LBM step + timeloop.add() << Sweep([&lbmSweep](IBlock * const block){lbmSweep.stream(block);}, "stream LB sweep" ) + << AfterFunction( SharedFunctor< DragForceEval_T >( forceEval ), "drag force evaluation" ); + +/* + // add LBM communication function and boundary handling sweep (does the hydro force calculations and the no-slip treatment) + timeloop.add() << BeforeFunction( optimizedPDFCommunicationScheme, "LBM Communication" ) + << Sweep( BoundaryHandling_T::getBlockSweep( boundaryHandlingID ), "Boundary Handling" ); + + // stream + collide LBM step + timeloop.add() << Sweep( lbmSweep, "LB sweep" ) + << AfterFunction( SharedFunctor< DragForceEval_T >( forceEval ), "drag force evaluation" ); +*/ + // resetting force + timeloop.addFuncAfterTimeStep( [ps,accessor](){ps->forEachParticle(false, mesa_pd::kernel::SelectAll(), *accessor, lbm_mesapd_coupling::ResetHydrodynamicForceTorqueKernel(),*accessor);}, "reset force on sphere"); + + timeloop.addFuncAfterTimeStep( RemainingTimeLogger( timeloop.getNrOfTimeSteps() ), "Remaining Time Logger" ); + + //////////////////////// + // EXECUTE SIMULATION // + //////////////////////// + + WcTimingPool timeloopTiming; + + // time loop + for (uint_t i = 0; i < timesteps; ++i ) + { + // perform a single simulation step + timeloop.singleStep( timeloopTiming ); + + // check if the relative change in the normalized drag force is below the specified convergence criterion + if ( i > setup.checkFrequency && forceEval->getDragForceDiff() < convergenceLimit ) + { + // if simulation has converged, terminate simulation + break; + } + + if( std::isnan(forceEval->getDragForce()) ) WALBERLA_ABORT("Nan found!"); + + if(i%1000 == 0) + { + WALBERLA_LOG_INFO_ON_ROOT("Current drag force: " << forceEval->getDragForce()); + } + + } + + WALBERLA_LOG_INFO_ON_ROOT("Final drag force: " << forceEval->getDragForce()); + WALBERLA_LOG_INFO_ON_ROOT("Re = " << forceEval->getAverageVel() * setup.radius * real_t(2) / setup.visc); + + timeloopTiming.logResultOnRoot(); + + if ( !funcTest && !shortrun ){ + // check the result + real_t relErr = std::fabs( ( setup.analyticalDrag - forceEval->getDragForce() ) / setup.analyticalDrag ); + if ( logging ) + { + WALBERLA_ROOT_SECTION() + { + std::cout << "Analytical drag: " << setup.analyticalDrag << "\n" + << "Simulated drag: " << forceEval->getDragForce() << "\n" + << "Relative error: " << relErr << "\n"; + } + + std::string fileName( MEMVariant_to_string(method) ); + fileName += "_bvrf" + std::to_string(uint_c(bulkViscRateFactor)); + fileName += "_mn" + std::to_string(float(magicNumber)); + if(useOmegaBulkAdaption ) fileName += "_uOBA" + std::to_string(uint_c(adaptionLayerSize)); + if(useSRT) fileName += "_SRT"; + + forceEval->logResultToFile( "log_DragForceSphereMEM_Generated_"+fileName+".txt" ); + } + } + + return 0; + +} + +} //namespace drag_force_sphere_mem + +int main( int argc, char **argv ){ + drag_force_sphere_mem::main(argc, argv); +} diff --git a/apps/benchmarks/FluidParticleCoupling/ForcesOnSphereNearPlane.cpp b/apps/benchmarks/FluidParticleCoupling/ForcesOnSphereNearPlane.cpp new file mode 100644 index 0000000000000000000000000000000000000000..eae674e9aa141bcee24ae4057b9da15a057a35f7 --- /dev/null +++ b/apps/benchmarks/FluidParticleCoupling/ForcesOnSphereNearPlane.cpp @@ -0,0 +1,812 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla is free software: you can +// redistribute it and/or modify it under the terms of the GNU General Public +// License as published by the Free Software Foundation, either version 3 of +// the License, or (at your option) any later version. +// +// waLBerla is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +// for more details. +// +// You should have received a copy of the GNU General Public License along +// with waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file ForcesOnSphereNearPlane.cpp +//! \author Christoph Rettinger <christoph.rettinger@fau.de> +// +//====================================================================================================================== + +#include "blockforest/Initialization.h" +#include "blockforest/communication/UniformBufferedScheme.h" +#include <blockforest/loadbalancing/StaticCurve.h> +#include <blockforest/SetupBlockForest.h> + +#include "boundary/all.h" + +#include "core/DataTypes.h" +#include "core/Environment.h" +#include "core/SharedFunctor.h" +#include "core/debug/Debug.h" +#include "core/debug/TestSubsystem.h" +#include "core/math/all.h" +#include "core/timing/RemainingTimeLogger.h" + +#include "domain_decomposition/SharedSweep.h" + +#include "field/AddToStorage.h" +#include "field/StabilityChecker.h" +#include "field/communication/PackInfo.h" + +#include "lbm/boundary/FreeSlip.h" +#include "lbm/communication/PdfFieldPackInfo.h" +#include "lbm/field/AddToStorage.h" +#include "lbm/field/PdfField.h" +#include "lbm/lattice_model/D3Q19.h" +#include "lbm/sweeps/CellwiseSweep.h" +#include "lbm/sweeps/SweepWrappers.h" + +#include "lbm_mesapd_coupling/mapping/ParticleMapping.h" +#include "lbm_mesapd_coupling/momentum_exchange_method/MovingParticleMapping.h" +#include "lbm_mesapd_coupling/momentum_exchange_method/boundary/SimpleBB.h" +#include "lbm_mesapd_coupling/momentum_exchange_method/boundary/CurvedLinear.h" +#include "lbm_mesapd_coupling/utility/AddForceOnParticlesKernel.h" +#include "lbm_mesapd_coupling/utility/ParticleSelector.h" +#include "lbm_mesapd_coupling/DataTypes.h" +#include "lbm_mesapd_coupling/utility/AverageHydrodynamicForceTorqueKernel.h" +#include "lbm_mesapd_coupling/utility/ResetHydrodynamicForceTorqueKernel.h" +#include "lbm_mesapd_coupling/utility/OmegaBulkAdaption.h" + +#include "mesa_pd/data/ParticleAccessorWithShape.h" +#include "mesa_pd/data/ParticleStorage.h" +#include "mesa_pd/data/ShapeStorage.h" +#include "mesa_pd/data/DataTypes.h" +#include "mesa_pd/data/shape/HalfSpace.h" +#include "mesa_pd/data/shape/Sphere.h" +#include "mesa_pd/domain/BlockForestDomain.h" +#include "mesa_pd/kernel/DoubleCast.h" +#include "mesa_pd/kernel/ParticleSelector.h" +#include "mesa_pd/mpi/SyncNextNeighbors.h" +#include "mesa_pd/mpi/ReduceProperty.h" +#include "mesa_pd/mpi/notifications/ForceTorqueNotification.h" +#include "mesa_pd/vtk/ParticleVtkOutput.h" + +#include "timeloop/SweepTimeloop.h" + +#include "vtk/all.h" +#include "field/vtk/all.h" +#include "lbm/vtk/all.h" + +#include "GeneratedLBM.h" +#include <functional> + +#define USE_TRT_LIKE_LATTICE_MODEL + +namespace forces_on_sphere_near_plane +{ + +/////////// +// USING // +/////////// + +using namespace walberla; +using walberla::uint_t; + +////////////// +// TYPEDEFS // +////////////// + +using LatticeModel_T = lbm::GeneratedLBM; +//using LatticeModel_T = lbm::D3Q19< lbm::collision_model::D3Q19MRT>; + +using Stencil_T = LatticeModel_T::Stencil; +using PdfField_T = lbm::PdfField<LatticeModel_T>; + +using flag_t = walberla::uint8_t; +using FlagField_T = FlagField<flag_t>; + +using ScalarField_T = GhostLayerField< real_t, 1>; + +const uint_t FieldGhostLayers = 1; + +/////////// +// FLAGS // +/////////// + +const FlagUID Fluid_Flag( "fluid" ); +const FlagUID CLI_Flag( "cli boundary" ); +const FlagUID SBB_Flag( "sbb boundary" ); + +///////////////////// +// BLOCK STRUCTURE // +///////////////////// + +///////////////////////////////////// +// BOUNDARY HANDLING CUSTOMIZATION // +///////////////////////////////////// +template <typename ParticleAccessor_T> +class MyBoundaryHandling +{ +public: + + using CLI_T = lbm_mesapd_coupling::CurvedLinear< LatticeModel_T, FlagField_T, ParticleAccessor_T >; + using SBB_T = lbm_mesapd_coupling::SimpleBB< LatticeModel_T, FlagField_T, ParticleAccessor_T >; + using Type = BoundaryHandling< FlagField_T, Stencil_T, CLI_T, SBB_T >; + + MyBoundaryHandling( const BlockDataID & flagFieldID, const BlockDataID & pdfFieldID, + const BlockDataID & particleFieldID, const shared_ptr<ParticleAccessor_T>& ac) : + flagFieldID_( flagFieldID ), pdfFieldID_( pdfFieldID ), particleFieldID_( particleFieldID ), ac_( ac ) {} + + Type * operator()( IBlock* const block, const StructuredBlockStorage* const storage ) const + { + WALBERLA_ASSERT_NOT_NULLPTR( block ); + WALBERLA_ASSERT_NOT_NULLPTR( storage ); + + auto * flagField = block->getData< FlagField_T >( flagFieldID_ ); + auto * pdfField = block->getData< PdfField_T > ( pdfFieldID_ ); + auto * particleField = block->getData< lbm_mesapd_coupling::ParticleField_T > ( particleFieldID_ ); + + const auto fluid = flagField->flagExists( Fluid_Flag ) ? flagField->getFlag( Fluid_Flag ) : flagField->registerFlag( Fluid_Flag ); + + Type * handling = new Type( "moving obstacle boundary handling", flagField, fluid, + CLI_T( "MO CLI", CLI_Flag, pdfField, flagField, particleField, ac_, fluid, *storage, *block ), + SBB_T( "MO SBB", SBB_Flag, pdfField, flagField, particleField, ac_, fluid, *storage, *block ) ); + + handling->fillWithDomain( FieldGhostLayers ); + + return handling; + } + +private: + + const BlockDataID flagFieldID_; + const BlockDataID pdfFieldID_; + const BlockDataID particleFieldID_; + + shared_ptr<ParticleAccessor_T> ac_; +}; +//******************************************************************************************************************* + + +template< typename ParticleAccessor_T> +class SpherePropertyLogger +{ +public: + SpherePropertyLogger( const shared_ptr< ParticleAccessor_T > & ac, walberla::id_t sphereUid, + const std::string & fileName, bool fileIO, + real_t dragNormalizationFactor, real_t liftNormalizationFactor, real_t physicalTimeScale ) : + ac_( ac ), sphereUid_( sphereUid ), + fileName_( fileName ), fileIO_(fileIO), + dragNormalizationFactor_( dragNormalizationFactor ), liftNormalizationFactor_( liftNormalizationFactor ), + physicalTimeScale_( physicalTimeScale ) + { + if ( fileIO_ ) + { + WALBERLA_ROOT_SECTION() + { + std::ofstream file; + file.open( fileName_.c_str() ); + file << "#\t t\t Cd\t Cl\t fX\t fY\t fZ\t tX\t tY\t tZ\n"; + file.close(); + } + } + } + + void operator()(const uint_t timestep) + { + + Vector3<real_t> force(real_t(0)); + Vector3<real_t> torque(real_t(0)); + + size_t idx = ac_->uidToIdx(sphereUid_); + if( idx != ac_->getInvalidIdx()) + { + force = ac_->getHydrodynamicForce(idx); + torque = ac_->getHydrodynamicTorque(idx); + } + + WALBERLA_MPI_SECTION() + { + mpi::allReduceInplace( force[0], mpi::SUM ); + mpi::allReduceInplace( force[1], mpi::SUM ); + mpi::allReduceInplace( force[2], mpi::SUM ); + + mpi::allReduceInplace( torque[0], mpi::SUM ); + mpi::allReduceInplace( torque[1], mpi::SUM ); + mpi::allReduceInplace( torque[2], mpi::SUM ); + } + + if( fileIO_ ) + writeToFile( timestep, force, torque); + + dragForce_ = force[0]; + liftForce_ = force[2]; + } + + real_t getDragForce() + { + return dragForce_; + } + + real_t getLiftForce() + { + return liftForce_; + } + + real_t getDragCoefficient() + { + return dragForce_ / dragNormalizationFactor_; + } + + real_t getLiftCoefficient() + { + return liftForce_ / liftNormalizationFactor_; + } + + +private: + void writeToFile( uint_t timestep, const Vector3<real_t> & force, const Vector3<real_t> & torque ) + { + WALBERLA_ROOT_SECTION() + { + std::ofstream file; + file.open( fileName_.c_str(), std::ofstream::app ); + + file << timestep << "\t" << real_c(timestep) / physicalTimeScale_ + << "\t" << force[0] / dragNormalizationFactor_ << "\t" << force[2] / liftNormalizationFactor_ + << "\t" << force[0] << "\t" << force[1] << "\t" << force[2] + << "\t" << torque[0] << "\t" << torque[1] << "\t" << torque[2] + << "\n"; + file.close(); + } + } + + shared_ptr< ParticleAccessor_T > ac_; + const walberla::id_t sphereUid_; + std::string fileName_; + bool fileIO_; + real_t dragForce_, liftForce_; + real_t dragNormalizationFactor_, liftNormalizationFactor_; + real_t physicalTimeScale_; +}; + +template <typename BoundaryHandling_T> +void initializeCouetteProfile( const shared_ptr< StructuredBlockStorage > & blocks, const BlockDataID & pdfFieldID, const BlockDataID & boundaryHandlingID, + const real_t & domainHeight, const real_t wallVelocity ) +{ + const real_t rho = real_c(1); + + for( auto blockIt = blocks->begin(); blockIt != blocks->end(); ++blockIt ) + { + auto pdfField = blockIt->getData< PdfField_T > ( pdfFieldID ); + auto boundaryHandling = blockIt->getData< BoundaryHandling_T >( boundaryHandlingID ); + + WALBERLA_FOR_ALL_CELLS_XYZ(pdfField, + if( !boundaryHandling->isDomain(x,y,z) ) continue; + + const Vector3< real_t > coord = blocks->getBlockLocalCellCenter( *blockIt, Cell(x,y,z) ); + + Vector3< real_t > velocity( real_c(0) ); + + velocity[0] = wallVelocity * coord[2] / domainHeight; + + pdfField->setToEquilibrium( x, y, z, velocity, rho ); + ) + } +} + +void createPlaneSetup(const shared_ptr<mesa_pd::data::ParticleStorage> & ps, const shared_ptr<mesa_pd::data::ShapeStorage> & ss, const math::AABB & simulationDomain, const real_t wallVelocity ) +{ + // create bounding planes + mesa_pd::data::Particle p0 = *ps->create(true); + p0.setPosition(simulationDomain.minCorner()); + p0.setShapeID(ss->create<mesa_pd::data::HalfSpace>( Vector3<real_t>(0,0,1) )); + p0.setOwner(mpi::MPIManager::instance()->rank()); + p0.setType(0); + mesa_pd::data::particle_flags::set(p0.getFlagsRef(), mesa_pd::data::particle_flags::INFINITE); + mesa_pd::data::particle_flags::set(p0.getFlagsRef(), mesa_pd::data::particle_flags::FIXED); + + mesa_pd::data::Particle p1 = *ps->create(true); + p1.setPosition(simulationDomain.maxCorner()); + p1.setShapeID(ss->create<mesa_pd::data::HalfSpace>( Vector3<real_t>(0,0,-1) )); + p1.setOwner(mpi::MPIManager::instance()->rank()); + p1.setType(0); + mesa_pd::data::particle_flags::set(p1.getFlagsRef(), mesa_pd::data::particle_flags::INFINITE); + mesa_pd::data::particle_flags::set(p1.getFlagsRef(), mesa_pd::data::particle_flags::FIXED); + p1.setLinearVelocity(Vector3<real_t>(wallVelocity, real_t(0), real_t(0))); //moving wall + +} + +void logFinalResult( const std::string & fileName, real_t Re, real_t wallDistance, real_t diameter, + uint_t domainLength, uint_t domainWidth, uint_t domainHeight, + real_t dragCoeff, real_t dragCoeffRef, + real_t liftCoeff, real_t liftCoeffRef, + uint_t timestep, real_t nonDimTimestep ) +{ + WALBERLA_ROOT_SECTION() + { + std::ofstream file; + file.open( fileName.c_str(), std::ofstream::app ); + + file << Re << "\t " << wallDistance << "\t " << diameter << "\t " + << domainLength << "\t " << domainWidth << "\t " << domainHeight << "\t " + << dragCoeff << "\t " << dragCoeffRef << "\t "<< std::abs(dragCoeff-dragCoeffRef) / dragCoeffRef << "\t " + << liftCoeff << "\t " << liftCoeffRef << "\t "<< std::abs(liftCoeff-liftCoeffRef) / liftCoeffRef << "\t " + << timestep << "\t " << nonDimTimestep + << "\n"; + file.close(); + } +} + +////////// +// MAIN // +////////// + +//******************************************************************************************************************* +/*!\brief Testcase that evaluates the drag and lift force on a sphere that is close to the bottom plane in shear flow + * + * see overview paper: + * Zeng, Najjar, Balachandar, Fischer - "Forces on a finite-sized particle located close to a wall in a linear shear flow", 2009 + * + * contains references to: + * Leighton, Acrivos - "The lift on a small sphere touching a plane in the presence of a simple shear flow", 1985 + * Zeng, Balachandar, Fischer - "Wall-induced forces on a rigid sphere at finite Reynolds number", 2005 + * + * CFD-IBM simulations in: + * Lee, Balachandar - "Drag and lift forces on a spherical particle moving on a wall in a shear flow at finite Re", 2010 + * + * Description: + * - Domain size [x, y, z] = [48 x 16 x 8 ] * diameter = [L(ength), W(idth), H(eight)] + * - horizontally periodic, bounded by two planes in z-direction + * - top plane is moving with constant wall velocity -> shear flow + * - sphere is placed in the vicinity of the bottom plane at [ L/2 + xOffset, W/2 + yOffset, wallDistance * diameter] + * - distance of sphere center to the bottom plane is crucial parameter + * - viscosity is adjusted to match specified Reynolds number = shearRate * diameter * wallDistance / viscosity + * - dimensionless drag and lift forces are evaluated and written to logging file + */ +//******************************************************************************************************************* + +int main( int argc, char **argv ) +{ + debug::enterTestMode(); + + mpi::Environment env( argc, argv ); + + /////////////////// + // Customization // + /////////////////// + + // simulation control + bool fileIO = true; + uint_t vtkIOFreq = 0; + std::string baseFolderVTK = "vtk_out_ForcesNearPlane"; + std::string baseFolderLogging = "."; + + // physical setup + real_t diameter = real_t(20); // cells per diameter -> determines overall resolution + real_t normalizedWallDistance = real_t(1); // distance of the sphere center to the bottom wall, normalized by the diameter + real_t ReynoldsNumberShear = real_t(1); // = shearRate * wallDistance * diameter / viscosity + + //numerical parameters + real_t maximumNonDimTimesteps = real_t(100); // maximum number of non-dimensional time steps + real_t xOffsetOfSpherePosition = real_t(0); // offset in x-direction of sphere position + real_t yOffsetOfSpherePosition = real_t(0); // offset in y-direction of sphere position + real_t bulkViscRateFactor = real_t(1); + real_t magicNumber = real_t(3)/real_t(16); + real_t wallVelocity = real_t(0.1); + + bool initializeVelocityProfile = false; + bool useOmegaBulkAdaption = false; + real_t adaptionLayerSize = real_t(2); + + real_t relativeChangeConvergenceEps = real_t(1e-5); + real_t physicalCheckingFrequency = real_t(0.1); + + // command line arguments + for( int i = 1; i < argc; ++i ) + { + if( std::strcmp( argv[i], "--noLogging" ) == 0 ) { fileIO = false; continue; } + if( std::strcmp( argv[i], "--vtkIOFreq" ) == 0 ) { vtkIOFreq = uint_c( std::atof( argv[++i] ) ); continue; } + if( std::strcmp( argv[i], "--diameter" ) == 0 ) { diameter = real_c( std::atof( argv[++i] ) ); continue; } + if( std::strcmp( argv[i], "--timesteps" ) == 0 ) { maximumNonDimTimesteps = real_c( std::atof( argv[++i] ) ); continue; } + if( std::strcmp( argv[i], "--wallDistance" ) == 0 ) { normalizedWallDistance = real_c( std::atof( argv[++i] ) ); continue; } + if( std::strcmp( argv[i], "--Re" ) == 0 ) { ReynoldsNumberShear = real_c( std::atof( argv[++i] ) ); continue; } + if( std::strcmp( argv[i], "--velocity" ) == 0 ) { wallVelocity = real_c( std::atof( argv[++i] ) ); continue; } + if( std::strcmp( argv[i], "--xOffset" ) == 0 ) { xOffsetOfSpherePosition = real_c( std::atof( argv[++i] ) ); continue; } + if( std::strcmp( argv[i], "--yOffset" ) == 0 ) { yOffsetOfSpherePosition = real_c( std::atof( argv[++i] ) ); continue; } + if( std::strcmp( argv[i], "--bvrf" ) == 0 ) { bulkViscRateFactor = real_c( std::atof( argv[++i] ) ); continue; } + if( std::strcmp( argv[i], "--baseFolderVTK" ) == 0 ) { baseFolderVTK = argv[++i]; continue; } + if( std::strcmp( argv[i], "--baseFolderLogging" ) == 0 ) { baseFolderLogging = argv[++i]; continue; } + if( std::strcmp( argv[i], "--initializeVelocityProfile" ) == 0 ) { initializeVelocityProfile = true; continue; } + if( std::strcmp( argv[i], "--magicNumber" ) == 0 ) { magicNumber = real_c(std::atof( argv[++i] ) ); continue; } + if( std::strcmp( argv[i], "--useOmegaBulkAdaption" ) == 0 ) { useOmegaBulkAdaption = true; continue; } + if( std::strcmp( argv[i], "--adaptionLayerSize" ) == 0 ) { adaptionLayerSize = real_c(std::atof( argv[++i] )); continue; } + if( std::strcmp( argv[i], "--convergenceLimit" ) == 0 ) { relativeChangeConvergenceEps = real_c(std::atof( argv[++i] )); continue; } + if( std::strcmp( argv[i], "--checkingFrequency" ) == 0 ) { physicalCheckingFrequency = real_c(std::atof( argv[++i] )); continue; } + WALBERLA_ABORT("Unrecognized command line argument found: " << argv[i]); + } + + WALBERLA_CHECK_GREATER_EQUAL(normalizedWallDistance, real_t(0.5)); + WALBERLA_CHECK_GREATER_EQUAL(ReynoldsNumberShear, real_t(0)); + WALBERLA_CHECK_GREATER_EQUAL(diameter, real_t(0)); + + ////////////////////////// + // NUMERICAL PARAMETERS // + ////////////////////////// + + const real_t domainLength = real_t(48) * diameter; //x + const real_t domainWidth = real_t(16) * diameter; //y + const real_t domainHeight = real_t( 8) * diameter; //z + + //const real_t domainLength = real_t(3) * diameter; //x + //const real_t domainWidth = real_t(3) * diameter; //y + //const real_t domainHeight = real_t(8) * diameter; //z + + Vector3<uint_t> domainSize( uint_c( std::ceil(domainLength)), uint_c( std::ceil(domainWidth)), uint_c( std::ceil(domainHeight)) ); + + + const real_t wallDistance = diameter * normalizedWallDistance; + const real_t shearRate = wallVelocity / domainHeight; + const real_t velAtSpherePosition = shearRate * wallDistance; + const real_t viscosity = velAtSpherePosition * diameter / ReynoldsNumberShear; + + const real_t relaxationTime = real_t(1) / lbm::collision_model::omegaFromViscosity(viscosity); + + const real_t densityFluid = real_t(1); + + const real_t dx = real_t(1); + + const real_t physicalTimeScale = diameter / velAtSpherePosition; + const uint_t timesteps = uint_c(maximumNonDimTimesteps * physicalTimeScale); + + const real_t omega = real_t(1) / relaxationTime; + const real_t omegaBulk = lbm_mesapd_coupling::omegaBulkFromOmega(omega, bulkViscRateFactor); + + Vector3<real_t> initialPosition( domainLength * real_t(0.5) + xOffsetOfSpherePosition, + domainWidth * real_t(0.5) + yOffsetOfSpherePosition, + wallDistance ); + + WALBERLA_LOG_INFO_ON_ROOT("Setup:"); + WALBERLA_LOG_INFO_ON_ROOT(" - domain size = " << domainSize ); + WALBERLA_LOG_INFO_ON_ROOT(" - normalized wall distance = " << normalizedWallDistance ); + WALBERLA_LOG_INFO_ON_ROOT(" - shear rate = " << shearRate ); + WALBERLA_LOG_INFO_ON_ROOT(" - wall velocity = " << wallVelocity ); + WALBERLA_LOG_INFO_ON_ROOT(" - Reynolds number (shear rate based) = " << ReynoldsNumberShear << ", vel at sphere pos = " << velAtSpherePosition); + WALBERLA_LOG_INFO_ON_ROOT(" - density = " << densityFluid ); + WALBERLA_LOG_INFO_ON_ROOT(" - viscosity = " << viscosity << " -> omega = " << omega << " , tau = " << relaxationTime); + WALBERLA_LOG_INFO_ON_ROOT(" - magic number = " << magicNumber); + WALBERLA_LOG_INFO_ON_ROOT(" - omegaBulk = " << omegaBulk << ", bulk visc. = " << lbm_mesapd_coupling::bulkViscosityFromOmegaBulk(omegaBulk) << " (bvrf " << bulkViscRateFactor << ")"); + WALBERLA_LOG_INFO_ON_ROOT(" - use omega bulk adaption = " << useOmegaBulkAdaption << " (adaption layer size = " << adaptionLayerSize << ")"); + WALBERLA_LOG_INFO_ON_ROOT(" - sphere diameter = " << diameter << ", position = " << initialPosition << " ( xOffset = " << xOffsetOfSpherePosition << ", yOffset = " << yOffsetOfSpherePosition << " )"); + WALBERLA_LOG_INFO_ON_ROOT(" - base folder VTK = " << baseFolderVTK << ", base folder logging = " << baseFolderLogging ); + + /////////////////////////// + // BLOCK STRUCTURE SETUP // + /////////////////////////// + + Vector3<uint_t> blocksPerDirection( 24, 5, 2 ); + //Vector3<uint_t> blocksPerDirection( 3, 3, 1 ); + + WALBERLA_CHECK( domainSize[0] % blocksPerDirection[0] == 0 && + domainSize[1] % blocksPerDirection[1] == 0 && + domainSize[2] % blocksPerDirection[2] == 0 ); + Vector3<uint_t> blockSizeInCells(domainSize[0] / ( blocksPerDirection[0] ), + domainSize[1] / ( blocksPerDirection[1] ), + domainSize[2] / ( blocksPerDirection[2] ) ); + + AABB simulationDomain( real_t(0), real_t(0), real_t(0), real_c(domainSize[0]), real_c(domainSize[1]), real_c(domainSize[2]) ); + auto blocks = blockforest::createUniformBlockGrid( blocksPerDirection[0], blocksPerDirection[1], blocksPerDirection[2], + blockSizeInCells[0], blockSizeInCells[1], blockSizeInCells[2], dx, + 0, false, false, + true, true, false, //periodicity + false ); + + WALBERLA_LOG_INFO_ON_ROOT(" - blocks = " << blocksPerDirection << ", block size = " << blockSizeInCells ); + + + //write domain decomposition to file + if( vtkIOFreq > 0 ) + { + vtk::writeDomainDecomposition( blocks, "initial_domain_decomposition", baseFolderVTK ); + } + + + ///////////////// + // PE COUPLING // + ///////////////// + + auto rpdDomain = std::make_shared<mesa_pd::domain::BlockForestDomain>(blocks->getBlockForestPointer()); + + //init data structures + auto ps = walberla::make_shared<mesa_pd::data::ParticleStorage>(1); + auto ss = walberla::make_shared<mesa_pd::data::ShapeStorage>(); + using ParticleAccessor_T = mesa_pd::data::ParticleAccessorWithShape; + auto accessor = walberla::make_shared<ParticleAccessor_T >(ps, ss); + + // bounding planes + createPlaneSetup(ps,ss,blocks->getDomain(), wallVelocity); + + // create sphere and store Uid + auto sphereShape = ss->create<mesa_pd::data::Sphere>( diameter * real_t(0.5) ); + + walberla::id_t sphereUid = 0; + if (rpdDomain->isContainedInProcessSubdomain( uint_c(mpi::MPIManager::instance()->rank()), initialPosition )) + { + mesa_pd::data::Particle&& p = *ps->create(); + p.setPosition(initialPosition); + p.setInteractionRadius(diameter * real_t(0.5)); + p.setOwner(mpi::MPIManager::instance()->rank()); + p.setShapeID(sphereShape); + sphereUid = p.getUid(); + } + mpi::allReduceInplace(sphereUid, mpi::SUM); + + // set up RPD functionality + std::function<void(void)> syncCall = [ps,rpdDomain](){ + const real_t overlap = real_t( 1.5 ); + mesa_pd::mpi::SyncNextNeighbors syncNextNeighborFunc; + syncNextNeighborFunc(*ps, *rpdDomain, overlap); + }; + + syncCall(); + + /////////////////////// + // ADD DATA TO BLOCKS // + //////////////////////// + + // add omega bulk field + BlockDataID omegaBulkFieldID = field::addToStorage<ScalarField_T>( blocks, "omega bulk field", omegaBulk, field::fzyx, uint_t(0) ); + + // create the lattice model +#ifdef USE_TRT_LIKE_LATTICE_MODEL + real_t lambda_e = lbm::collision_model::TRT::lambda_e( omega ); + real_t lambda_d = lbm::collision_model::TRT::lambda_d( omega, magicNumber ); + //LatticeModel_T latticeModel = LatticeModel_T(omegaBulk, lambda_d, lambda_e); + LatticeModel_T latticeModel = LatticeModel_T(omegaBulkFieldID, lambda_d, lambda_e); + //LatticeModel_T latticeModel = LatticeModel_T(lbm::collision_model::D3Q19MRT( omegaBulk, omegaBulk, lambda_d, lambda_e, lambda_e, lambda_d )); +#else + // generated KBC + LatticeModel_T latticeModel = LatticeModel_T(omega); +#endif + + // add PDF field + BlockDataID pdfFieldID = lbm::addPdfFieldToStorage< LatticeModel_T >( blocks, "pdf field (fzyx)", latticeModel, + Vector3< real_t >( real_t(0) ), real_t(1), + FieldGhostLayers, field::fzyx ); + // add flag field + BlockDataID flagFieldID = field::addFlagFieldToStorage<FlagField_T>( blocks, "flag field", FieldGhostLayers ); + + // add particle field + BlockDataID particleFieldID = field::addToStorage<lbm_mesapd_coupling::ParticleField_T>( blocks, "particle field", accessor->getInvalidUid(), field::fzyx, FieldGhostLayers ); + + // add boundary handling + using BoundaryHandling_T = MyBoundaryHandling<ParticleAccessor_T>::Type; + BlockDataID boundaryHandlingID = blocks->addStructuredBlockData< BoundaryHandling_T >(MyBoundaryHandling<ParticleAccessor_T>( flagFieldID, pdfFieldID, particleFieldID, accessor), "boundary handling" ); + + // set up coupling functionality + lbm_mesapd_coupling::ResetHydrodynamicForceTorqueKernel resetHydrodynamicForceTorque; + lbm_mesapd_coupling::MovingParticleMappingKernel<BoundaryHandling_T> movingParticleMappingKernel(blocks, boundaryHandlingID, particleFieldID); + lbm_mesapd_coupling::AverageHydrodynamicForceTorqueKernel averageHydrodynamicForceTorque; + + // map sphere into the LBM simulation + ps->forEachParticle(false, lbm_mesapd_coupling::RegularParticlesSelector(), *accessor, movingParticleMappingKernel, *accessor, CLI_Flag); + + // map planes + ps->forEachParticle(false, lbm_mesapd_coupling::GlobalParticlesSelector(), *accessor, movingParticleMappingKernel, *accessor, CLI_Flag); //TODO: was SBB_Flag? + + // initialize Couette velocity profile in whole domain + if(initializeVelocityProfile) + { + WALBERLA_LOG_INFO_ON_ROOT("Initializing Couette velocity profile."); + initializeCouetteProfile<BoundaryHandling_T>(blocks, pdfFieldID, boundaryHandlingID, domainHeight, wallVelocity); + } + + // update bulk omega in all cells to adapt to changed particle position + if(useOmegaBulkAdaption) + { + using OmegaBulkAdapter_T = lbm_mesapd_coupling::OmegaBulkAdapter<ParticleAccessor_T, lbm_mesapd_coupling::RegularParticlesSelector>; + real_t defaultOmegaBulk = lbm_mesapd_coupling::omegaBulkFromOmega(omega, real_t(1)); + shared_ptr<OmegaBulkAdapter_T> omegaBulkAdapter = make_shared<OmegaBulkAdapter_T>(blocks, omegaBulkFieldID, accessor, defaultOmegaBulk, omegaBulk, adaptionLayerSize, lbm_mesapd_coupling::RegularParticlesSelector()); + for (auto blockIt = blocks->begin(); blockIt != blocks->end(); ++blockIt) { + (*omegaBulkAdapter)(blockIt.get()); + } + } + + /////////////// + // TIME LOOP // + /////////////// + + // setup of the LBM communication for synchronizing the pdf field between neighboring blocks + blockforest::communication::UniformBufferedScheme< Stencil_T > optimizedPDFCommunicationScheme( blocks ); + optimizedPDFCommunicationScheme.addPackInfo( make_shared< lbm::PdfFieldPackInfo< LatticeModel_T > >( pdfFieldID ) ); // optimized sync + + // create the timeloop + SweepTimeloop timeloop( blocks->getBlockStorage(), timesteps ); + + timeloop.addFuncBeforeTimeStep( RemainingTimeLogger( timeloop.getNrOfTimeSteps() ), "Remaining Time Logger" ); + + if( vtkIOFreq != uint_t(0) ) + { + // flag field (written only once in the first time step, ghost layers are also written) + //auto flagFieldVTK = vtk::createVTKOutput_BlockData( blocks, "flag_field", vtkIOFreq, uint_t(0), false, baseFolderVTK ); + //flagFieldVTK->addCellDataWriter( make_shared< field::VTKWriter< FlagField_T > >( flagFieldID, "FlagField" ) ); + //timeloop.addFuncBeforeTimeStep( vtk::writeFiles( flagFieldVTK ), "VTK (flag field data)" ); + + auto pdfFieldVTK = vtk::createVTKOutput_BlockData( blocks, "fluid_field", vtkIOFreq, uint_t(0), false, baseFolderVTK ); + + field::FlagFieldCellFilter< FlagField_T > fluidFilter( flagFieldID ); + fluidFilter.addFlag( Fluid_Flag ); + pdfFieldVTK->addCellInclusionFilter( fluidFilter ); + + //AABB sliceAABB( real_t(0), real_c(domainSize[1])*real_t(0.5)-real_t(1), real_t(0), + // real_c(domainSize[0]), real_c(domainSize[1])*real_t(0.5)+real_t(1), real_c(domainSize[2]) ); + //vtk::AABBCellFilter aabbSliceFilter( sliceAABB ); + //pdfFieldVTK->addCellInclusionFilter( aabbSliceFilter ); + + //vtk::ChainedFilter combinedSliceFilter; + //combinedSliceFilter.addFilter( fluidFilter ); + //combinedSliceFilter.addFilter( aabbSliceFilter ); + //pdfFieldVTK->addCellInclusionFilter( combinedSliceFilter ); + + pdfFieldVTK->addCellDataWriter( make_shared< lbm::VelocityVTKWriter< LatticeModel_T, float > >( pdfFieldID, "VelocityFromPDF" ) ); + pdfFieldVTK->addCellDataWriter( make_shared< lbm::DensityVTKWriter < LatticeModel_T, float > >( pdfFieldID, "DensityFromPDF" ) ); + + timeloop.addFuncBeforeTimeStep( vtk::writeFiles( pdfFieldVTK ), "VTK (fluid field data)" ); + } + + auto bhSweep = BoundaryHandling_T::getBlockSweep( boundaryHandlingID ); + auto lbmSweep = LatticeModel_T::Sweep( pdfFieldID ); + + // add LBM communication function and boundary handling sweep (does the hydro force calculations and the no-slip treatment) + timeloop.add() << BeforeFunction( optimizedPDFCommunicationScheme, "LBM Communication" ) + << Sweep(bhSweep, "Boundary Handling" ); + + // stream + collide LBM step + timeloop.add() << Sweep( lbmSweep, "LB sweep" ); + + + // add force evaluation and logging + real_t normalizationFactor = math::pi / real_t(8) * densityFluid * shearRate * shearRate * wallDistance * wallDistance * diameter * diameter ; + std::string loggingFileName( baseFolderLogging + "/LoggingForcesNearPlane"); + loggingFileName += "_D" + std::to_string(uint_c(diameter)); + loggingFileName += "_Re" + std::to_string(uint_c(ReynoldsNumberShear)); + loggingFileName += "_WD" + std::to_string(uint_c(normalizedWallDistance * real_t(1000))); + loggingFileName += "_bvrf" + std::to_string(uint_c(bulkViscRateFactor)); + if(useOmegaBulkAdaption) loggingFileName += "_uOBA" + std::to_string(uint_c(adaptionLayerSize)); + loggingFileName += "_mn" + std::to_string(magicNumber); + loggingFileName += ".txt"; + WALBERLA_LOG_INFO_ON_ROOT(" - writing logging file " << loggingFileName); + SpherePropertyLogger<ParticleAccessor_T >logger( accessor, sphereUid, loggingFileName, fileIO, normalizationFactor, normalizationFactor, physicalTimeScale); + + // compute reference values from literature + + const real_t normalizedGapSize = normalizedWallDistance - real_t(0.5); + + // drag correlation for the drag coefficient + const real_t standardDragCorrelation = real_t(24) / ReynoldsNumberShear * (real_t(1) + real_t(0.15) * std::pow(ReynoldsNumberShear, real_t(0.687))); // Schiller-Naumann correlation + const real_t dragCorrelationWithGapSizeStokes = real_t(24) / ReynoldsNumberShear * (real_t(1) + real_t(0.138) * std::exp(real_t(-2) * normalizedGapSize) + real_t(9)/( real_t(16) * (real_t(1) + real_t(2) * normalizedGapSize) ) ); // Goldman et al. (1967) + const real_t alphaDragS = real_t(0.15) - real_t(0.046) * ( real_t(1) - real_t(0.16) * normalizedGapSize * normalizedGapSize ) * std::exp( -real_t(0.7) * normalizedGapSize); + const real_t betaDragS = real_t(0.687) + real_t(0.066)*(real_t(1) - real_t(0.76) * normalizedGapSize * normalizedGapSize) * std::exp( - std::pow( normalizedGapSize, real_t(0.9) ) ); + const real_t dragCorrelationZeng = dragCorrelationWithGapSizeStokes * ( real_t(1) + alphaDragS * std::pow( ReynoldsNumberShear, betaDragS ) ); // Zeng et al. (2009) - Eqs. (13) and (14) + + // lift correlations for the lift coefficient + const real_t liftCorrelationZeroGapStokes = real_t(5.87); // Leighton, Acrivos (1985) + const real_t liftCorrelationZeroGap = real_t(3.663) / std::pow( ReynoldsNumberShear * ReynoldsNumberShear + real_t(0.1173), real_t(0.22) ); // Zeng et al. (2009) - Eq. (19) + const real_t alphaLiftS = - std::exp( -real_t(0.3) + real_t(0.025) * ReynoldsNumberShear); + const real_t betaLiftS = real_t(0.8) + real_t(0.01) * ReynoldsNumberShear; + const real_t lambdaLiftS = ( real_t(1) - std::exp(-normalizedGapSize)) * std::pow( ReynoldsNumberShear / real_t(250), real_t(5) / real_t(2) ); + const real_t liftCorrelationZeng = liftCorrelationZeroGap * std::exp( - real_t(0.5) * normalizedGapSize * std::pow( ReynoldsNumberShear / real_t(250), real_t(4)/real_t(3))) * + ( std::exp( alphaLiftS * std::pow( normalizedGapSize, betaLiftS ) ) - lambdaLiftS ); // Zeng et al. (2009) - Eqs. (28) and (29) + + //////////////////////// + // EXECUTE SIMULATION // + //////////////////////// + + WcTimingPool timeloopTiming; + + + const uint_t checkingFrequency = uint_c( physicalCheckingFrequency * physicalTimeScale ); + + WALBERLA_LOG_INFO_ON_ROOT("Starting simulation with at maximum of " << timesteps << " time steps"); + WALBERLA_LOG_INFO_ON_ROOT("Convergence checking frequency = " << checkingFrequency << " (physically = " << physicalCheckingFrequency << ") until relative difference of " << relativeChangeConvergenceEps << " is reached."); + + real_t maxDragCurrentCheckingPeriod = -math::Limits<real_t>::inf(); + real_t minDragCurrentCheckingPeriod = math::Limits<real_t>::inf(); + real_t maxLiftCurrentCheckingPeriod = -math::Limits<real_t>::inf(); + real_t minLiftCurrentCheckingPeriod = math::Limits<real_t>::inf(); + uint_t timestep = 0; + + // time loop + while( true ) + { + // perform a single simulation step + timeloop.singleStep( timeloopTiming ); + + // average force + ps->forEachParticle(false, mesa_pd::kernel::SelectAll(), *accessor, averageHydrodynamicForceTorque, *accessor ); + + // evaluation + timeloopTiming["Logging"].start(); + logger(timestep); + timeloopTiming["Logging"].end(); + + // reset after logging here + ps->forEachParticle(false, mesa_pd::kernel::SelectAll(), *accessor, resetHydrodynamicForceTorque, *accessor ); + + // check for termination + real_t curDrag = logger.getDragCoefficient(); + real_t curLift = logger.getLiftCoefficient(); + + if(std::isinf(curDrag) || std::isnan(curDrag)) WALBERLA_ABORT("Found invalid drag value " << curDrag << " in time step " << timestep); + + maxDragCurrentCheckingPeriod = std::max( maxDragCurrentCheckingPeriod, curDrag); + minDragCurrentCheckingPeriod = std::min( minDragCurrentCheckingPeriod, curDrag); + maxLiftCurrentCheckingPeriod = std::max( maxLiftCurrentCheckingPeriod, curLift); + minLiftCurrentCheckingPeriod = std::min( minLiftCurrentCheckingPeriod, curLift); + + real_t dragDiffCurrentCheckingPeriod = std::fabs(maxDragCurrentCheckingPeriod - minDragCurrentCheckingPeriod)/std::fabs(maxDragCurrentCheckingPeriod); + real_t liftDiffCurrentCheckingPeriod = std::fabs(maxLiftCurrentCheckingPeriod - minLiftCurrentCheckingPeriod)/std::fabs(maxLiftCurrentCheckingPeriod); + + // continuous output during simulation + if( timestep % (checkingFrequency * uint_t(10)) == 0) + { + WALBERLA_LOG_INFO_ON_ROOT("Drag: current C_D = " << curDrag ); + WALBERLA_LOG_INFO_ON_ROOT(" - standard C_D = " << standardDragCorrelation ); + WALBERLA_LOG_INFO_ON_ROOT(" - C_D ( Stokes fit ) = " << dragCorrelationWithGapSizeStokes ); + WALBERLA_LOG_INFO_ON_ROOT(" - C_D ( Zeng ) = " << dragCorrelationZeng ); + + WALBERLA_LOG_INFO_ON_ROOT("Lift: current C_L = " << curLift ); + WALBERLA_LOG_INFO_ON_ROOT(" - C_L ( Stokes, zero gap ) = " << liftCorrelationZeroGapStokes); + WALBERLA_LOG_INFO_ON_ROOT(" - C_L ( zero gap ) = " << liftCorrelationZeroGap); + WALBERLA_LOG_INFO_ON_ROOT(" - C_L ( Zeng ) = " << liftCorrelationZeng); + + WALBERLA_LOG_INFO_ON_ROOT( "Drag difference [(max-min)/max] = " << dragDiffCurrentCheckingPeriod << ", lift difference = " << liftDiffCurrentCheckingPeriod); + } + + // check for convergence ( = difference between min and max values in current checking period is below limit) + if( timestep % checkingFrequency == 0 && timestep > 0 ) + { + if( dragDiffCurrentCheckingPeriod < relativeChangeConvergenceEps && + liftDiffCurrentCheckingPeriod < relativeChangeConvergenceEps ) + { + WALBERLA_LOG_INFO_ON_ROOT("Forces converged with an eps of " << relativeChangeConvergenceEps ); + WALBERLA_LOG_INFO_ON_ROOT(" - drag min = " << minDragCurrentCheckingPeriod << " , max = " << maxDragCurrentCheckingPeriod); + WALBERLA_LOG_INFO_ON_ROOT(" - lift min = " << minLiftCurrentCheckingPeriod << " , max = " << maxLiftCurrentCheckingPeriod); + break; + } + + //reset min and max values for new checking period + maxDragCurrentCheckingPeriod = -math::Limits<real_t>::inf(); + minDragCurrentCheckingPeriod = math::Limits<real_t>::inf(); + maxLiftCurrentCheckingPeriod = -math::Limits<real_t>::inf(); + minLiftCurrentCheckingPeriod = math::Limits<real_t>::inf(); + } + ++timestep; + + } + + timeloopTiming.logResultOnRoot(); + + std::string resultFileName( baseFolderLogging + "/ResultForcesNearPlane"); + resultFileName += "_D" + std::to_string(uint_c(diameter)); + resultFileName += "_Re" + std::to_string(uint_c(ReynoldsNumberShear)); + resultFileName += "_WD" + std::to_string(uint_c(normalizedWallDistance * real_t(1000))); + resultFileName += "_bvrf" + std::to_string(uint_c(bulkViscRateFactor)); + if(useOmegaBulkAdaption) resultFileName += "_uOBA" + std::to_string(uint_c(adaptionLayerSize)); + resultFileName += "_mn" + std::to_string(magicNumber); + resultFileName += ".txt"; + + WALBERLA_LOG_INFO_ON_ROOT(" - writing final result to file " << resultFileName); + logFinalResult(resultFileName, ReynoldsNumberShear, normalizedWallDistance, diameter, domainSize[0], domainSize[1], domainSize[2], + logger.getDragCoefficient(), dragCorrelationZeng, logger.getLiftCoefficient(), liftCorrelationZeng, timestep, real_c(timestep) / physicalTimeScale ); + + + return EXIT_SUCCESS; +} + +} // namespace forces_on_sphere_near_plane + +int main( int argc, char **argv ){ + forces_on_sphere_near_plane::main(argc, argv); +} diff --git a/apps/benchmarks/FluidParticleCoupling/GeneratedLBM.py b/apps/benchmarks/FluidParticleCoupling/GeneratedLBM.py new file mode 100644 index 0000000000000000000000000000000000000000..37d1a35ee5e3c18f79764ec4308cd4f2a93bbbbc --- /dev/null +++ b/apps/benchmarks/FluidParticleCoupling/GeneratedLBM.py @@ -0,0 +1,188 @@ +import sympy as sp +import pystencils as ps +from lbmpy.creationfunctions import create_lb_method_from_existing, create_lb_ast, create_lb_method +from lbmpy_walberla import generate_lattice_model +from pystencils_walberla import CodeGeneration + +from lbmpy.creationfunctions import create_lb_collision_rule +from lbmpy.moments import MOMENT_SYMBOLS, is_even, get_order +from lbmpy.stencils import get_stencil +from lbmpy.maxwellian_equilibrium import get_moments_of_discrete_maxwellian_equilibrium +from lbmpy.forcemodels import * + +from collections import OrderedDict +from lbmpy.methods import create_with_discrete_maxwellian_eq_moments + +with CodeGeneration() as ctx: + + # methods: TRTlike, KBC, SRT, cumulant + generatedMethod = "TRTlike" + + print("Generating " + generatedMethod + " LBM method") + + # + # x,y,z = MOMENT_SYMBOLS + # + # e_sq = x**2 + y**2 + z**2 + # + # moments = [0] * 19 + # + # moments[0] = sp.sympify(1) + # moments[1] = 19 * e_sq - 30 + # moments[2] = (21 * e_sq**2 - 53 * e_sq + 24) / 2 + # + # moments[3] = x + # moments[5] = y + # moments[7] = z + # + # moments[4] = (5*e_sq - 9) * x + # moments[6] = (5*e_sq - 9) * y + # moments[8] = (5*e_sq - 9) * z + # + # moments[9] = 3 * x**2 - e_sq + # moments[11] = y**2 - z**2 + # + # moments[13] = x * y + # moments[14] = y * z + # moments[15] = x * z + # + # moments[10] = (3* e_sq - 5)*(3*x**2 - e_sq) + # moments[12] = (3* e_sq - 5)*(y**2 - z**2) + # moments[16] = (y**2 - z**2) * x + # moments[17] = (z**2 - x**2) * y + # moments[18] = (x**2 - y**2) * z + # + # m_eq = get_moments_of_discrete_maxwellian_equilibrium(stencil, moments, order=2, c_s_sq=sp.Rational(1,3)) + # + # rr_dict = OrderedDict(zip(moments, omega)) + # + # forcing = sp.symbols("forcing_:%d" % 3) + # forcing=(sp.symbols("fx"),0,0) + # forcemodel=Luo(forcing) #None + # method = create_with_discrete_maxwellian_eq_moments(stencil, rr_dict, compressible=False, force_model=forcemodel) + # at this stage, we have the MRT model from the LBM book! + + #this is the schiller / walberla MRT + + if generatedMethod == "TRTlike": + stencil = get_stencil("D3Q19", 'walberla') + omega = sp.symbols("omega_:%d" % len(stencil)) + method = create_lb_method(stencil=stencil, method='mrt', maxwellian_moments=False) + + def modification_func(moment, eq, rate): + omegaVisc = sp.Symbol("omega_visc") + omegaBulk = ps.fields("omega_bulk: [3D]", layout='fzyx')# sp.Symbol("omega_bulk") + omegaMagic = sp.Symbol("omega_magic") + if get_order(moment) <= 1: + return moment, eq, 0 + elif rate == omega[1]: + return moment, eq, omegaBulk.center_vector + elif rate == omega[2]: + return moment, eq, omegaBulk.center_vector + elif is_even(moment): + return moment, eq, omegaVisc + else: + return moment, eq, omegaMagic + + my_method = create_lb_method_from_existing(method, modification_func) + + # optimizations + + collision_rule = create_lb_collision_rule(lb_method=my_method, + optimization={"cse_global": True, + "cse_pdfs": False, + #"split": True + } ) + + generate_lattice_model(ctx, 'GeneratedLBM', collision_rule) + + elif generatedMethod == "KBC": + methodName = 'trt-kbc-n4' + #omega_field = ps.fields("omegaField(1): [3D]", layout='fzyx') omega_output_field=omega_field.center, + collision_rule = create_lb_collision_rule(method=methodName,entropic=True, stencil=get_stencil("D3Q27", 'walberla'), compressible=True, + optimization={"cse_global": True, + "cse_pdfs": False, + "split": True}) + generate_lattice_model(ctx, 'GeneratedLBM', collision_rule) + + elif generatedMethod == "SRT": + methodName = 'srt' + collision_rule = create_lb_collision_rule(method=methodName,stencil=get_stencil("D3Q19", 'walberla'), + optimization={"cse_global": True, + "cse_pdfs": False, + "split": True}) + generate_lattice_model(ctx, 'GeneratedLBM', collision_rule) + + elif generatedMethod == "cumulant": + + x,y,z = MOMENT_SYMBOLS + + cumulants = [0] * 27 + + cumulants[0] = sp.sympify(1) #000 + + cumulants[1] = x #100 + cumulants[2] = y #010 + cumulants[3] = z #001 + + cumulants[4] = x*y #110 + cumulants[5] = x*z #101 + cumulants[6] = y*z #011 + + cumulants[7] = x**2 - y**2 #200 - 020 + cumulants[8] = x**2 - z**2 #200 - 002 + cumulants[9] = x**2 + y**2 + z**2 #200 + 020 + 002 + + cumulants[10] = x*y**2 + x*z**2 #120 + 102 + cumulants[11] = x**2*y + y*z**2 #210 + 012 + cumulants[12] = x**2*z + y**2*z #201 + 021 + cumulants[13] = x*y**2 - x*z**2 #120 - 102 + cumulants[14] = x**2*y - y*z**2 #210 - 012 + cumulants[15] = x**2*z - y**2*z #201 - 021 + + cumulants[16] = x*y*z #111 + + cumulants[17] = x**2*y**2 - 2*x**2*z**2 + y**2*z**2 # 220- 2*202 +022 + cumulants[18] = x**2*y**2 + x**2*z**2 - 2*y**2*z**2 # 220 + 202 - 2*002 + cumulants[19] = x**2*y**2 + x**2*z**2 + y**2*z**2 # 220 + 202 + 022 + + cumulants[20] = x**2*y*z # 211 + cumulants[21] = x*y**2*z # 121 + cumulants[22] = x*y*z**2 # 112 + + cumulants[23] = x**2*y**2*z # 221 + cumulants[24] = x**2*y*z**2 # 212 + cumulants[25] = x*y**2*z**2 # 122 + + cumulants[26] = x**2*y**2*z**2 # 222 + + + from lbmpy.moments import get_order + def get_relaxation_rate(cumulant, omega): + if get_order(cumulant) <= 1: + return 0 + elif get_order(cumulant) == 2 and cumulant != x**2 + y**2 + z**2: + return omega + else: + return 1 + + stencil = get_stencil("D3Q27", 'walberla') + + omega = sp.Symbol("omega") + rr_dict = OrderedDict((c, get_relaxation_rate(c, omega)) + for c in cumulants) + + from lbmpy.methods import create_with_continuous_maxwellian_eq_moments + my_method = create_with_continuous_maxwellian_eq_moments(stencil, rr_dict, cumulant=True, compressible=True) + + collision_rule = create_lb_collision_rule(lb_method=my_method, + optimization={"cse_global": True, + "cse_pdfs": False}) + + generate_lattice_model(ctx, 'GeneratedLBM', collision_rule) + + else: + print("Invalid generated method string! " + generatedMethod) + + + diff --git a/apps/benchmarks/FluidParticleCoupling/GeneratedLBMWithForce.py b/apps/benchmarks/FluidParticleCoupling/GeneratedLBMWithForce.py new file mode 100644 index 0000000000000000000000000000000000000000..00200a679512ecfcce9dd57113cac2fcf116cbd0 --- /dev/null +++ b/apps/benchmarks/FluidParticleCoupling/GeneratedLBMWithForce.py @@ -0,0 +1,81 @@ +import sympy as sp +import pystencils as ps +from lbmpy.creationfunctions import create_lb_method_from_existing, create_lb_ast, create_lb_method +from lbmpy_walberla import generate_lattice_model +from pystencils_walberla import CodeGeneration + +from lbmpy.creationfunctions import create_lb_collision_rule +from lbmpy.moments import MOMENT_SYMBOLS, is_even, get_order +from lbmpy.stencils import get_stencil +from lbmpy.maxwellian_equilibrium import get_moments_of_discrete_maxwellian_equilibrium +from lbmpy.forcemodels import * + +from collections import OrderedDict +from lbmpy.methods import create_with_discrete_maxwellian_eq_moments + +with CodeGeneration() as ctx: + + forcing=(sp.symbols("fx"),0,0) + forcemodel=Luo(forcing) #None + + # methods: TRTlike, KBC, SRT + generatedMethod = "TRTlike" + + if generatedMethod == "TRTlike": + stencil = get_stencil("D3Q19", 'walberla') + omega = sp.symbols("omega_:%d" % len(stencil)) + + methodWithForce = create_lb_method(stencil=stencil, method='mrt', maxwellian_moments=False,force_model=forcemodel) + + def modification_func(moment, eq, rate): + omegaVisc = sp.Symbol("omega_visc") + omegaBulk = ps.fields("omega_bulk: [3D]", layout='fzyx')# = sp.Symbol("omega_bulk") + #omegaBulk = sp.Symbol("omega_bulk") + omegaMagic = sp.Symbol("omega_magic") + if get_order(moment) <= 1: + return moment, eq, 0 + elif rate == omega[1]: + return moment, eq, omegaBulk.center_vector + elif rate == omega[2]: + return moment, eq, omegaBulk.center_vector + elif is_even(moment): + return moment, eq, omegaVisc + else: + return moment, eq, omegaMagic + + my_methodWithForce = create_lb_method_from_existing(methodWithForce, modification_func) + + collision_rule = create_lb_collision_rule(lb_method=my_methodWithForce) + # , + # optimization={"cse_global": True, + # "cse_pdfs": False, + # "split": True, + # "vectorization":{'instruction_set': 'avx', + # 'nontemporal': True, + # 'assume_inner_stride_one': True}}) + + generate_lattice_model(ctx, 'GeneratedLBMWithForce', collision_rule) + + elif generatedMethod == "SRT": + collision_rule = create_lb_collision_rule(method='srt',stencil=get_stencil("D3Q19", 'walberla'), force_model=forcemodel, + optimization={"cse_global": True, + "cse_pdfs": False, + #"split": True + }) + generate_lattice_model(ctx, 'GeneratedLBMWithForce', collision_rule) + + elif generatedMethod == "TRT": + collision_rule = create_lb_collision_rule(method='trt',stencil=get_stencil("D3Q19", 'walberla'), force_model=forcemodel, maxwellian_moments=False) + generate_lattice_model(ctx, 'GeneratedLBMWithForce', collision_rule) + + + elif generatedMethod == "KBC": + collision_rule = create_lb_collision_rule(method='trt-kbc-n4',entropic=True, stencil=get_stencil("D3Q27", 'walberla'), compressible=True, force_model=forcemodel, + optimization={"cse_global": True, + "cse_pdfs": False, + #"split": True + }) + generate_lattice_model(ctx, 'GeneratedLBMWithForce', collision_rule) + + else: + print("Invalid generated method string! " + generatedMethod) diff --git a/apps/benchmarks/FluidParticleCoupling/LubricationForceEvaluation.cpp b/apps/benchmarks/FluidParticleCoupling/LubricationForceEvaluation.cpp new file mode 100644 index 0000000000000000000000000000000000000000..6182af2dcd78f41e8b5da294644560b7502e50e7 --- /dev/null +++ b/apps/benchmarks/FluidParticleCoupling/LubricationForceEvaluation.cpp @@ -0,0 +1,781 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla is free software: you can +// redistribute it and/or modify it under the terms of the GNU General Public +// License as published by the Free Software Foundation, either version 3 of +// the License, or (at your option) any later version. +// +// waLBerla is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +// for more details. +// +// You should have received a copy of the GNU General Public License along +// with waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file LubricationForceEvaluation.cpp +//! \ingroup lbm_mesapd_coupling +//! \author Christoph Rettinger <christoph.rettinger@fau.de> +// +//====================================================================================================================== + +#include "blockforest/Initialization.h" +#include "blockforest/communication/UniformBufferedScheme.h" + +#include "boundary/all.h" + +#include "core/DataTypes.h" +#include "core/Environment.h" +#include "core/SharedFunctor.h" +#include "core/debug/Debug.h" +#include "core/debug/TestSubsystem.h" +#include "core/math/all.h" +#include "core/mpi/Broadcast.h" +#include "core/timing/RemainingTimeLogger.h" +#include "core/logging/all.h" + +#include "domain_decomposition/SharedSweep.h" + +#include "field/AddToStorage.h" +#include "field/StabilityChecker.h" +#include "field/communication/PackInfo.h" + +#include "lbm/boundary/all.h" +#include "lbm/communication/PdfFieldPackInfo.h" +#include "lbm/field/AddToStorage.h" +#include "lbm/field/PdfField.h" +#include "lbm/lattice_model/D3Q19.h" +#include "lbm/sweeps/CellwiseSweep.h" +#include "lbm/sweeps/SweepWrappers.h" + +#include "lbm_mesapd_coupling/DataTypes.h" +#include "lbm_mesapd_coupling/mapping/ParticleMapping.h" +#include "lbm_mesapd_coupling/momentum_exchange_method/MovingParticleMapping.h" +#include "lbm_mesapd_coupling/momentum_exchange_method/boundary/CurvedLinear.h" +#include "lbm_mesapd_coupling/momentum_exchange_method/reconstruction/Reconstructor.h" +#include "lbm_mesapd_coupling/momentum_exchange_method/reconstruction/ExtrapolationDirectionFinder.h" +#include "lbm_mesapd_coupling/momentum_exchange_method/reconstruction/PdfReconstructionManager.h" +#include "lbm_mesapd_coupling/utility/ParticleSelector.h" +#include "lbm_mesapd_coupling/utility/ResetHydrodynamicForceTorqueKernel.h" +#include "lbm_mesapd_coupling/utility/LubricationCorrectionKernel.h" +#include "lbm_mesapd_coupling/utility/OmegaBulkAdaption.h" + +#include "mesa_pd/collision_detection/AnalyticContactDetection.h" +#include "mesa_pd/data/ParticleAccessorWithShape.h" +#include "mesa_pd/data/ParticleStorage.h" +#include "mesa_pd/data/ShapeStorage.h" +#include "mesa_pd/data/DataTypes.h" +#include "mesa_pd/data/shape/HalfSpace.h" +#include "mesa_pd/data/shape/Sphere.h" +#include "mesa_pd/domain/BlockForestDomain.h" +#include "mesa_pd/kernel/DoubleCast.h" +#include "mesa_pd/kernel/ExplicitEulerWithShape.h" +#include "mesa_pd/kernel/SpringDashpot.h" +#include "mesa_pd/kernel/ParticleSelector.h" +#include "mesa_pd/mpi/SyncNextNeighbors.h" +#include "mesa_pd/mpi/ReduceProperty.h" +#include "mesa_pd/mpi/ContactFilter.h" +#include "mesa_pd/mpi/notifications/ForceTorqueNotification.h" +#include "mesa_pd/vtk/ParticleVtkOutput.h" + +#include "timeloop/SweepTimeloop.h" + +#include "vtk/all.h" +#include "field/vtk/all.h" +#include "lbm/vtk/all.h" + +#include "GeneratedLBM.h" + +#include <functional> + +#define USE_TRT_LIKE_LATTICE_MODEL + +namespace lubrication_force_evaluation +{ + +/////////// +// USING // +/////////// + +using namespace walberla; +using walberla::uint_t; + +using LatticeModel_T = lbm::GeneratedLBM; + +using Stencil_T = LatticeModel_T::Stencil; +using PdfField_T = lbm::PdfField<LatticeModel_T>; + +using flag_t = walberla::uint8_t; +using FlagField_T = FlagField<flag_t>; + +using ScalarField_T = GhostLayerField< real_t, 1>; + +const uint_t FieldGhostLayers = 1; + +/////////// +// FLAGS // +/////////// + +const FlagUID Fluid_Flag( "fluid" ); +const FlagUID FreeSlip_Flag( "free slip" ); +const FlagUID MO_Flag( "moving obstacle" ); +const FlagUID FormerMO_Flag( "former moving obstacle" ); + +///////////////////////////////////// +// BOUNDARY HANDLING CUSTOMIZATION // +///////////////////////////////////// +template <typename ParticleAccessor_T> +class MyBoundaryHandling +{ +public: + + using FreeSlip_T = lbm::FreeSlip< LatticeModel_T, FlagField_T>; + using MO_T = lbm_mesapd_coupling::CurvedLinear< LatticeModel_T, FlagField_T, ParticleAccessor_T >; + using Type = BoundaryHandling< FlagField_T, Stencil_T, FreeSlip_T, MO_T >; + + MyBoundaryHandling( const BlockDataID & flagFieldID, const BlockDataID & pdfFieldID, + const BlockDataID & particleFieldID, const shared_ptr<ParticleAccessor_T>& ac) : + flagFieldID_( flagFieldID ), pdfFieldID_( pdfFieldID ), particleFieldID_( particleFieldID ), ac_( ac ) {} + + Type * operator()( IBlock* const block, const StructuredBlockStorage* const storage ) const + { + WALBERLA_ASSERT_NOT_NULLPTR( block ); + WALBERLA_ASSERT_NOT_NULLPTR( storage ); + + auto * flagField = block->getData< FlagField_T >( flagFieldID_ ); + auto * pdfField = block->getData< PdfField_T > ( pdfFieldID_ ); + auto * particleField = block->getData< lbm_mesapd_coupling::ParticleField_T > ( particleFieldID_ ); + + const auto fluid = flagField->flagExists( Fluid_Flag ) ? flagField->getFlag( Fluid_Flag ) : flagField->registerFlag( Fluid_Flag ); + + Type * handling = new Type( "moving obstacle boundary handling", flagField, fluid, + FreeSlip_T( "FreeSlip", FreeSlip_Flag, pdfField, flagField, fluid ), + MO_T( "MO", MO_Flag, pdfField, flagField, particleField, ac_, fluid, *storage, *block ) ); + + const auto freeslip = flagField->getFlag( FreeSlip_Flag ); + + CellInterval domainBB = storage->getDomainCellBB(); + domainBB.xMin() -= cell_idx_c( FieldGhostLayers ); + domainBB.xMax() += cell_idx_c( FieldGhostLayers ); + + domainBB.yMin() -= cell_idx_c( FieldGhostLayers ); + domainBB.yMax() += cell_idx_c( FieldGhostLayers ); + + // SOUTH + CellInterval south( domainBB.xMin(), domainBB.yMin(), domainBB.zMin(), domainBB.xMax(), domainBB.yMin(), domainBB.zMax() ); + storage->transformGlobalToBlockLocalCellInterval( south, *block ); + handling->forceBoundary( freeslip, south ); + + // NORTH + CellInterval north( domainBB.xMin(), domainBB.yMax(), domainBB.zMin(), domainBB.xMax(), domainBB.yMax(), domainBB.zMax() ); + storage->transformGlobalToBlockLocalCellInterval( north, *block ); + handling->forceBoundary( freeslip, north ); + + domainBB.zMin() -= cell_idx_c( FieldGhostLayers ); + domainBB.zMax() += cell_idx_c( FieldGhostLayers ); + + // BOTTOM + CellInterval bottom( domainBB.xMin(), domainBB.yMin(), domainBB.zMin(), domainBB.xMax(), domainBB.yMax(), domainBB.zMin() ); + storage->transformGlobalToBlockLocalCellInterval( bottom, *block ); + handling->forceBoundary( freeslip, bottom ); + + // TOP + CellInterval top( domainBB.xMin(), domainBB.yMin(), domainBB.zMax(), domainBB.xMax(), domainBB.yMax(), domainBB.zMax() ); + storage->transformGlobalToBlockLocalCellInterval( top, *block ); + handling->forceBoundary( freeslip, top ); + + handling->fillWithDomain( FieldGhostLayers ); + + + return handling; + } + +private: + + const BlockDataID flagFieldID_; + const BlockDataID pdfFieldID_; + const BlockDataID particleFieldID_; + + shared_ptr<ParticleAccessor_T> ac_; +}; +//******************************************************************************************************************* + +////////// +// MAIN // +////////// + +//******************************************************************************************************************* +/*!\brief Evaluates the hydrodynamic force for the lubrication case for sphere-sphere and sphere-wall case. + * + * 4 different setups are available that change the relative velocity to investigate the different components individually. + * All particles are fixed but have a small velocity which is a valid assumption in Stokes flow. + * The simulations are run until steady state is reached. + * + * The positions are always shifted by a random offset to account for the dependence of the resulting force/torque + * on the exact location w.r.t. the underlying grid. + * This can be eliminated by averaging over several realizations of the same physical setup. + * + * see also Rettinger, Ruede 2020 for the details + */ +//******************************************************************************************************************* + +int main( int argc, char **argv ) +{ + + debug::enterTestMode(); + + mpi::Environment env( argc, argv ); + + bool sphSphTest = true; + bool fileIO = true; + uint_t vtkIOFreq = 0; + std::string fileNameEnding = ""; + std::string baseFolder = "vtk_out_Lubrication"; + + real_t radius = real_t(5); + real_t ReynoldsNumber = real_t(1e-2); + real_t tau = real_t(1); + real_t gapSize = real_t(0); + real_t bulkViscRateFactor = real_t(1); + real_t magicNumber = real_t(3)/real_t(16); + bool useOmegaBulkAdaption = false; + real_t adaptionLayerSize = real_t(2); + + // 1: translation in normal direction -> normal Lubrication force + // 2: translation in tangential direction -> tangential Lubrication force and torque + // 3: rotation around tangential direction -> force & torque + // 4: rotation around normal direction -> torque + uint_t setup = 1; + + for( int i = 1; i < argc; ++i ) + { + if( std::strcmp( argv[i], "--sphWallTest" ) == 0 ) { sphSphTest = false; continue; } + if( std::strcmp( argv[i], "--noLogging" ) == 0 ) { fileIO = false; continue; } + if( std::strcmp( argv[i], "--vtkIOFreq" ) == 0 ) { vtkIOFreq = uint_c( std::atof( argv[++i] ) ); continue; } + if( std::strcmp( argv[i], "--setup" ) == 0 ) { setup = uint_c( std::atof( argv[++i] ) ); continue; } + if( std::strcmp( argv[i], "--baseFolder" ) == 0 ) { baseFolder = argv[++i]; continue; } + if( std::strcmp( argv[i], "--diameter" ) == 0 ) { radius = real_t(0.5)*real_c(std::atof(argv[++i])); continue; } + if( std::strcmp( argv[i], "--gapSize" ) == 0 ) { gapSize = real_c(std::atof(argv[++i])); continue; } + if( std::strcmp( argv[i], "--bulkViscRateFactor" ) == 0 ) { bulkViscRateFactor = real_c(std::atof( argv[++i] ) ); continue; } + if( std::strcmp( argv[i], "--tau" ) == 0 ) { tau = real_c(std::atof( argv[++i] ) ); continue; } + if( std::strcmp( argv[i], "--fileName" ) == 0 ) { fileNameEnding = argv[++i]; continue; } + if( std::strcmp( argv[i], "--magicNumber" ) == 0 ) { magicNumber = real_c(std::atof(argv[++i])); continue; } + if( std::strcmp( argv[i], "--useOmegaBulkAdaption" ) == 0 ) { useOmegaBulkAdaption = true; continue; } + if( std::strcmp( argv[i], "--adaptionLayerSize" ) == 0 ) { adaptionLayerSize = real_c(std::atof( argv[++i] )); continue; } + WALBERLA_ABORT("Unrecognized command line argument found: " << argv[i]); + } + + /////////////////////////// + // SIMULATION PROPERTIES // + /////////////////////////// + + uint_t xSize = uint_c(real_t(24) * radius); + uint_t ySize = uint_c(real_t(24) * radius); + uint_t zSize = uint_c(real_t(24) * radius); + + uint_t xBlocks = uint_c(4); // number of blocks in x-direction + uint_t yBlocks = uint_c(1); // number of blocks in y-direction + uint_t zBlocks = uint_c(1); // number of blocks in z-direction + + uint_t xCells = xSize / xBlocks; // number of cells in x-direction on each block + uint_t yCells = ySize / yBlocks; // number of cells in y-direction on each block + uint_t zCells = zSize / zBlocks; // number of cells in z-direction on each block + + // Perform missing variable calculations + real_t omega = real_t(1) / tau; + real_t nu = walberla::lbm::collision_model::viscosityFromOmega(omega); + real_t velocity = ReynoldsNumber * nu / (real_t(2) * radius); + real_t omegaBulk = lbm_mesapd_coupling::omegaBulkFromOmega(omega, bulkViscRateFactor); + + uint_t timesteps = uint_c( 10000 ); + + real_t fStokes = real_t(6) * math::pi * nu * radius * velocity; + real_t tStokes = real_t(8) * math::pi * nu * radius * radius * velocity; + + WALBERLA_LOG_INFO_ON_ROOT_SECTION() + { + std::stringstream ss; + + if ( sphSphTest ) + { + ss << "-------------------------------------------------------\n" + << " Parameters for the sphere-sphere lubrication test \n" + << "-------------------------------------------------------\n"; + } + else + { + ss << "-------------------------------------------------------\n" + << " Parameters for the sphere-wall lubrication test \n" + << "-------------------------------------------------------\n"; + } + ss << " omega = " << omega << "\n" + << " omegaBulk = " << omegaBulk << " (bvrf " << bulkViscRateFactor << ")\n" + << " use omega bulk adaption = " << useOmegaBulkAdaption << " (adaption layer size = " << adaptionLayerSize << ")\n" + << " radius = " << radius << "\n" + << " velocity = " << velocity << "\n" + << " Re = " << ReynoldsNumber << "\n" + << " gap size = " << gapSize << "\n" + << " time steps = " << timesteps << "\n" + << " fStokes = " << fStokes << "\n" + << " setup = " << setup << "\n" + << "-------------------------------------------------------\n" + << " domainSize = " << xSize << " x " << ySize << " x " << zSize << "\n" + << " blocks = " << xBlocks << " x " << yBlocks << " x " << zBlocks << "\n" + << " blockSize = " << xCells << " x " << yCells << " x " << zCells << "\n" + << "-------------------------------------------------------\n"; + WALBERLA_LOG_INFO( ss.str() ); + } + + + auto blocks = blockforest::createUniformBlockGrid( xBlocks, yBlocks, zBlocks, xCells, yCells, zCells, real_t(1), + 0, false, false, + sphSphTest, true, true, //periodicity + false ); + + ////////////////// + // RPD COUPLING // + ////////////////// + + auto rpdDomain = std::make_shared<mesa_pd::domain::BlockForestDomain>(blocks->getBlockForestPointer()); + + //init data structures + auto ps = walberla::make_shared<mesa_pd::data::ParticleStorage>(1); + auto ss = walberla::make_shared<mesa_pd::data::ShapeStorage>(); + using ParticleAccessor_T = mesa_pd::data::ParticleAccessorWithShape; + auto accessor = walberla::make_shared<ParticleAccessor_T >(ps, ss); + + auto sphereShape = ss->create<mesa_pd::data::Sphere>( radius ); + + uint_t id1 (0); + uint_t id2 (0); + + uint_t randomSeed = uint_c(std::chrono::system_clock::now().time_since_epoch().count()); + mpi::broadcastObject(randomSeed); // root process chooses seed and broadcasts it + std::mt19937 randomNumberGenerator(randomSeed); + + Vector3<real_t> domainCenter( real_c(xSize) * real_t(0.5), real_c(ySize) * real_t(0.5), real_c(zSize) * real_t(0.5) ); + Vector3<real_t> offsetVector(math::realRandom<real_t>(real_t(0), real_t(1), randomNumberGenerator), + math::realRandom<real_t>(real_t(0), real_t(1), randomNumberGenerator), + math::realRandom<real_t>(real_t(0), real_t(1), randomNumberGenerator)); + + if ( sphSphTest ) + { + + Vector3<real_t> pos1 = domainCenter + offsetVector; + if (rpdDomain->isContainedInProcessSubdomain( uint_c(mpi::MPIManager::instance()->rank()), pos1 )) + { + mesa_pd::data::Particle&& p = *ps->create(); + p.setPosition(pos1); + p.setInteractionRadius(radius); + p.setOwner(mpi::MPIManager::instance()->rank()); + p.setShapeID(sphereShape); + if(setup == 1) + { + // only normal translational vel + p.setLinearVelocity(Vector3<real_t>( velocity, real_t(0), real_t(0))); + p.setAngularVelocity(Vector3<real_t>( real_t(0), real_t(0), real_t(0))); + } else if (setup == 2) + { + // only tangential translational velocity + p.setLinearVelocity(Vector3<real_t>( real_t(0), velocity, real_t(0))); + p.setAngularVelocity(Vector3<real_t>( real_t(0), real_t(0), real_t(0))); + } else if (setup == 3) + { + // only rotation around axis perpendicular to center line + p.setLinearVelocity(Vector3<real_t>( real_t(0), real_t(0), real_t(0))); + p.setAngularVelocity(Vector3<real_t>( real_t(0), velocity / radius, real_t(0))); + } else if (setup == 4) + { + // only rotation around center line + p.setLinearVelocity(Vector3<real_t>( real_t(0), real_t(0), real_t(0))); + p.setAngularVelocity(Vector3<real_t>( velocity / radius, real_t(0) , real_t(0))); + } + id1 = p.getUid(); + } + + Vector3<real_t> pos2 = pos1 + Vector3<real_t>(real_t(2) * radius + gapSize, real_t(0), real_t(0)); + if (rpdDomain->isContainedInProcessSubdomain( uint_c(mpi::MPIManager::instance()->rank()), pos2 )) + { + mesa_pd::data::Particle&& p = *ps->create(); + p.setPosition(pos2); + p.setInteractionRadius(radius); + p.setOwner(mpi::MPIManager::instance()->rank()); + p.setShapeID(sphereShape); + if(setup == 1) + { + // only normal translational vel + p.setLinearVelocity(Vector3<real_t>( -velocity, real_t(0), real_t(0))); + p.setAngularVelocity(Vector3<real_t>( real_t(0), real_t(0), real_t(0))); + } else if (setup == 2) + { + // only tangential translational velocity + p.setLinearVelocity(Vector3<real_t>( real_t(0), -velocity, real_t(0))); + p.setAngularVelocity(Vector3<real_t>( real_t(0), real_t(0), real_t(0))); + } else if (setup == 3) + { + // only rotation around axis perpendicular to center line + p.setLinearVelocity(Vector3<real_t>( real_t(0), real_t(0), real_t(0))); + p.setAngularVelocity(Vector3<real_t>( real_t(0), velocity / radius, real_t(0))); + } else if (setup == 4) + { + // only rotation around center line + p.setLinearVelocity(Vector3<real_t>( real_t(0), real_t(0), real_t(0))); + p.setAngularVelocity(Vector3<real_t>( -velocity / radius, real_t(0) , real_t(0))); + } + id2 = p.getUid(); + } + + mpi::allReduceInplace(id1, mpi::SUM); + mpi::allReduceInplace(id2, mpi::SUM); + + WALBERLA_LOG_INFO_ON_ROOT("pos sphere 1 = " << pos1); + WALBERLA_LOG_INFO_ON_ROOT("pos sphere 2 = " << pos2); + } else + { + // sphere-wall test + + Vector3<real_t> referenceVector(offsetVector[0], domainCenter[1], domainCenter[2]); + + // create two planes + mesa_pd::data::Particle&& p0 = *ps->create(true); + p0.setPosition(referenceVector); + p0.setShapeID(ss->create<mesa_pd::data::HalfSpace>( Vector3<real_t>(1,0,0) )); + p0.setOwner(mpi::MPIManager::instance()->rank()); + mesa_pd::data::particle_flags::set(p0.getFlagsRef(), mesa_pd::data::particle_flags::INFINITE); + mesa_pd::data::particle_flags::set(p0.getFlagsRef(), mesa_pd::data::particle_flags::FIXED); + id2 = p0.getUid(); + + mesa_pd::data::Particle&& p1 = *ps->create(true); + p1.setPosition(Vector3<real_t>(real_c(xSize),0,0)); + p1.setShapeID(ss->create<mesa_pd::data::HalfSpace>( Vector3<real_t>(-1,0,0) )); + p1.setOwner(mpi::MPIManager::instance()->rank()); + mesa_pd::data::particle_flags::set(p1.getFlagsRef(), mesa_pd::data::particle_flags::INFINITE); + mesa_pd::data::particle_flags::set(p1.getFlagsRef(), mesa_pd::data::particle_flags::FIXED); + + Vector3<real_t> pos1 = referenceVector + Vector3<real_t>(radius + gapSize, real_t(0), real_t(0)); + if (rpdDomain->isContainedInProcessSubdomain( uint_c(mpi::MPIManager::instance()->rank()), pos1 )) + { + mesa_pd::data::Particle&& p = *ps->create(); + p.setPosition(pos1); + p.setInteractionRadius(radius); + p.setOwner(mpi::MPIManager::instance()->rank()); + p.setShapeID(sphereShape); + if(setup == 1) + { + // only normal translational vel + p.setLinearVelocity(Vector3<real_t>( -velocity, real_t(0), real_t(0))); + p.setAngularVelocity(Vector3<real_t>( real_t(0), real_t(0), real_t(0))); + } else if (setup == 2) + { + // only tangential translational velocity + p.setLinearVelocity(Vector3<real_t>( real_t(0), velocity, real_t(0))); + p.setAngularVelocity(Vector3<real_t>( real_t(0), real_t(0), real_t(0))); + } else if (setup == 3) + { + // only rotation around axis perpendicular to center line + p.setLinearVelocity(Vector3<real_t>( real_t(0), real_t(0), real_t(0))); + p.setAngularVelocity(Vector3<real_t>( real_t(0), velocity / radius, real_t(0))); + } else if (setup == 4) + { + // only rotation around center line + p.setLinearVelocity(Vector3<real_t>( real_t(0), real_t(0), real_t(0))); + p.setAngularVelocity(Vector3<real_t>( velocity / radius, real_t(0) , real_t(0))); + } + id1 = p.getUid(); + } + + mpi::allReduceInplace(id1, mpi::SUM); + // id2 is globally known + + WALBERLA_LOG_INFO_ON_ROOT("pos plane = " << referenceVector); + WALBERLA_LOG_INFO_ON_ROOT("pos sphere = " << pos1); + } + + /////////////////////// + // ADD DATA TO BLOCKS // + //////////////////////// + + // add omega bulk field + BlockDataID omegaBulkFieldID = field::addToStorage<ScalarField_T>( blocks, "omega bulk field", omegaBulk, field::fzyx, uint_t(0) ); + + // create the lattice model +#ifdef USE_TRT_LIKE_LATTICE_MODEL + WALBERLA_LOG_INFO_ON_ROOT("Using TRTlike model!"); + real_t lambda_e = lbm::collision_model::TRT::lambda_e( omega ); + real_t lambda_d = lbm::collision_model::TRT::lambda_d( omega, magicNumber ); + LatticeModel_T latticeModel = LatticeModel_T(omegaBulkFieldID, lambda_d, lambda_e); + //LatticeModel_T latticeModel = LatticeModel_T(omegaBulk, lambda_d, lambda_e); + //LatticeModel_T latticeModel = LatticeModel_T(lbm::collision_model::D3Q19MRT( omegaBulk, omegaBulk, lambda_d, lambda_e, lambda_e, lambda_d )); +#else + WALBERLA_LOG_INFO_ON_ROOT("Using KBC model!"); + // generated KBC + LatticeModel_T latticeModel = LatticeModel_T(omega); +#endif + + // add PDF field + BlockDataID pdfFieldID = lbm::addPdfFieldToStorage< LatticeModel_T >( blocks, "pdf field", latticeModel, + Vector3< real_t >( real_t(0) ), real_t(1), + uint_t(1), field::fzyx ); + // add flag field + BlockDataID flagFieldID = field::addFlagFieldToStorage<FlagField_T>( blocks, "flag field" ); + + // add body field + BlockDataID particleFieldID = field::addToStorage<lbm_mesapd_coupling::ParticleField_T>( blocks, "particle field", accessor->getInvalidUid(), field::fzyx, FieldGhostLayers ); + + // add boundary handling + using BoundaryHandling_T = MyBoundaryHandling<ParticleAccessor_T>::Type; + BlockDataID boundaryHandlingID = blocks->addStructuredBlockData< BoundaryHandling_T >(MyBoundaryHandling<ParticleAccessor_T>( flagFieldID, pdfFieldID, particleFieldID, accessor), "boundary handling" ); + + // set up RPD functionality + std::function<void(void)> syncCall = [&ps,&rpdDomain](){ + const real_t overlap = real_t( 1.5 ); + mesa_pd::mpi::SyncNextNeighbors syncNextNeighborFunc; + syncNextNeighborFunc(*ps, *rpdDomain, overlap); + }; + + syncCall(); + + mesa_pd::mpi::ReduceProperty reduceProperty; + + lbm_mesapd_coupling::ResetHydrodynamicForceTorqueKernel resetHydrodynamicForceTorque; + + + real_t lubricationCutOffDistanceNormal = real_t(2) / real_t(3); + real_t lubricationCutOffDistanceTangentialTranslational = real_t(0.5); + real_t lubricationCutOffDistanceTangentialRotational = real_t(0.5); + lbm_mesapd_coupling::LubricationCorrectionKernel lubricationCorrectionKernel(nu, [](real_t){return real_t(0);}, lubricationCutOffDistanceNormal, lubricationCutOffDistanceTangentialTranslational, lubricationCutOffDistanceTangentialRotational ); + real_t maximalCutOffDistance = std::max(lubricationCutOffDistanceNormal, std::max(lubricationCutOffDistanceTangentialTranslational,lubricationCutOffDistanceTangentialRotational )); + + lbm_mesapd_coupling::RegularParticlesSelector sphereSelector; + + lbm_mesapd_coupling::MovingParticleMappingKernel<BoundaryHandling_T> movingParticleMappingKernel(blocks, boundaryHandlingID, particleFieldID); + + /////////////// + // TIME LOOP // + /////////////// + + // map particles into the LBM simulation + ps->forEachParticle(false, mesa_pd::kernel::SelectAll(), *accessor, movingParticleMappingKernel, *accessor, MO_Flag); + + // create the timeloop + SweepTimeloop timeloop( blocks->getBlockStorage(), timesteps ); + + // setup of the LBM communication for synchronizing the pdf field between neighboring blocks + blockforest::communication::UniformBufferedScheme< Stencil_T > optimizedPDFCommunicationScheme( blocks ); + optimizedPDFCommunicationScheme.addPackInfo( make_shared< lbm::PdfFieldPackInfo< LatticeModel_T > >( pdfFieldID ) ); // optimized sync + + blockforest::communication::UniformBufferedScheme< Stencil_T > fullPDFCommunicationScheme( blocks ); + fullPDFCommunicationScheme.addPackInfo( make_shared< field::communication::PackInfo< PdfField_T > >( pdfFieldID ) ); // full sync + + timeloop.addFuncBeforeTimeStep( RemainingTimeLogger( timeloop.getNrOfTimeSteps() ), "Remaining Time Logger" ); + + + if( vtkIOFreq != uint_t(0) ) + { + // spheres + auto particleVtkOutput = make_shared<mesa_pd::vtk::ParticleVtkOutput>(ps); + particleVtkOutput->addOutput<mesa_pd::data::SelectParticleOwner>("owner"); + particleVtkOutput->addOutput<mesa_pd::data::SelectParticleLinearVelocity>("velocity"); + auto particleVtkWriter = vtk::createVTKOutput_PointData(particleVtkOutput, "Particles", vtkIOFreq, baseFolder, "simulation_step"); + timeloop.addFuncBeforeTimeStep( vtk::writeFiles( particleVtkWriter ), "VTK (sphere data)" ); + + // flag field (written only once in the first time step, ghost layers are also written) + auto flagFieldVTK = vtk::createVTKOutput_BlockData( blocks, "flag_field", vtkIOFreq, FieldGhostLayers, false, baseFolder ); + flagFieldVTK->addCellDataWriter( make_shared< field::VTKWriter< FlagField_T > >( flagFieldID, "FlagField" ) ); + vtk::writeFiles( flagFieldVTK )(); + + // pdf field + auto pdfFieldVTK = vtk::createVTKOutput_BlockData( blocks, "fluid_field", vtkIOFreq, 0, false, baseFolder ); + + pdfFieldVTK->addBeforeFunction( fullPDFCommunicationScheme ); + + field::FlagFieldCellFilter< FlagField_T > fluidFilter( flagFieldID ); + fluidFilter.addFlag( Fluid_Flag ); + pdfFieldVTK->addCellInclusionFilter( fluidFilter ); + + pdfFieldVTK->addCellDataWriter( make_shared< lbm::VelocityVTKWriter< LatticeModel_T, float > >( pdfFieldID, "VelocityFromPDF" ) ); + pdfFieldVTK->addCellDataWriter( make_shared< lbm::DensityVTKWriter < LatticeModel_T, float > >( pdfFieldID, "DensityFromPDF" ) ); + + timeloop.addFuncBeforeTimeStep( vtk::writeFiles( pdfFieldVTK ), "VTK (fluid field data)" ); + + // omega bulk field + timeloop.addFuncBeforeTimeStep( field::createVTKOutput<ScalarField_T, float>( omegaBulkFieldID, *blocks, "omega_bulk_field", vtkIOFreq, uint_t(0), false, baseFolder ), "VTK (omega bulk field)" ); + + } + + // update bulk omega in all cells to adapt to changed particle position + if(useOmegaBulkAdaption) + { + using OmegaBulkAdapter_T = lbm_mesapd_coupling::OmegaBulkAdapter<ParticleAccessor_T, decltype(sphereSelector)>; + real_t defaultOmegaBulk = lbm_mesapd_coupling::omegaBulkFromOmega(omega, real_t(1)); + shared_ptr<OmegaBulkAdapter_T> omegaBulkAdapter = make_shared<OmegaBulkAdapter_T>(blocks, omegaBulkFieldID, accessor, defaultOmegaBulk, omegaBulk, adaptionLayerSize, sphereSelector); + for( auto blockIt = blocks->begin(); blockIt != blocks->end(); ++blockIt) + { + (*omegaBulkAdapter)(&(*blockIt)); + } + } + + // add LBM communication function and boundary handling sweep (does the hydro force calculations and the no-slip treatment) + + timeloop.add() << BeforeFunction( optimizedPDFCommunicationScheme, "LBM Communication" ) + << Sweep( BoundaryHandling_T::getBlockSweep( boundaryHandlingID ), "Boundary Handling" ); + + // stream + collide LBM step + timeloop.add() << Sweep( LatticeModel_T::Sweep( pdfFieldID ), "LB stream & collide" ); + + + //////////////////////// + // EXECUTE SIMULATION // + //////////////////////// + + WcTimingPool timeloopTiming; + + Vector3<real_t> hydForce(0.); + Vector3<real_t> lubForce(0.); + Vector3<real_t> hydTorque(0.); + Vector3<real_t> lubTorque(0.); + + real_t curForceNorm = real_t(0); + real_t oldForceNorm = real_t(0); + real_t curTorqueNorm = real_t(0); + real_t oldTorqueNorm = real_t(0); + + real_t convergenceLimit = real_t(1e-4); + + // time loop + for (uint_t i = 1; i <= timesteps; ++i ) + { + // perform a single simulation step -> this contains LBM and setting of the hydrodynamic interactions + timeloop.singleStep( timeloopTiming ); + + // lubrication correction + mesa_pd::collision_detection::AnalyticContactDetection acd; + acd.getContactThreshold() = maximalCutOffDistance; + + { + auto idx1 = accessor->uidToIdx(id1); + if( idx1 != accessor->getInvalidIdx() ) + { + auto idx2 = accessor->uidToIdx(id2); + if( idx2 != accessor->getInvalidIdx() ) + { + mesa_pd::kernel::DoubleCast double_cast; + mesa_pd::mpi::ContactFilter contact_filter; + if (double_cast(idx1, idx2, *accessor, acd, *accessor )) + { + if (contact_filter(acd.getIdx1(), acd.getIdx2(), *accessor, acd.getContactPoint(), *rpdDomain)) + { + //WALBERLA_LOG_INFO(acd.getIdx1() << " " << acd.getIdx2() << " " << acd.getContactNormal() << " " << acd.getPenetrationDepth()); + double_cast(acd.getIdx1(), acd.getIdx2(), *accessor, lubricationCorrectionKernel, *accessor, acd.getContactNormal(), acd.getPenetrationDepth()); + } + } + } + } + } + + if( i% 100 == 0 && i > 1 ) + { + oldForceNorm = curForceNorm; + oldTorqueNorm = curTorqueNorm; + + hydForce.reset(); + lubForce.reset(); + hydTorque.reset(); + lubTorque.reset(); + + auto idx1 = accessor->uidToIdx(id1); + if( idx1 != accessor->getInvalidIdx() ) + { + hydForce = accessor->getHydrodynamicForce(idx1); + lubForce = accessor->getForce(idx1); + hydTorque = accessor->getHydrodynamicTorque(idx1); + lubTorque= accessor->getTorque(idx1); + } + + WALBERLA_MPI_SECTION() + { + mpi::allReduceInplace( hydForce[0], mpi::SUM ); + mpi::allReduceInplace( hydForce[1], mpi::SUM ); + mpi::allReduceInplace( hydForce[2], mpi::SUM ); + mpi::reduceInplace( lubForce[0], mpi::SUM ); + mpi::reduceInplace( lubForce[1], mpi::SUM ); + mpi::reduceInplace( lubForce[2], mpi::SUM ); + mpi::allReduceInplace( hydTorque[0], mpi::SUM ); + mpi::allReduceInplace( hydTorque[1], mpi::SUM ); + mpi::allReduceInplace( hydTorque[2], mpi::SUM ); + mpi::reduceInplace( lubTorque[0], mpi::SUM ); + mpi::reduceInplace( lubTorque[1], mpi::SUM ); + mpi::reduceInplace( lubTorque[2], mpi::SUM ); + } + + curForceNorm = hydForce.length(); + curTorqueNorm = hydTorque.length(); + + real_t forceDiff = std::fabs((curForceNorm - oldForceNorm) / oldForceNorm); + real_t torqueDiff = std::fabs((curTorqueNorm - oldTorqueNorm) / oldTorqueNorm); + + WALBERLA_LOG_INFO_ON_ROOT( "F/Fs = " << hydForce/fStokes << " ( " << forceDiff << " ), T/Ts = " << hydTorque/tStokes << " ( " << torqueDiff << " )"); + + if( i == 100 ) { + WALBERLA_LOG_INFO_ON_ROOT("Flub = " << lubForce << ", Tlub = " << lubTorque); + WALBERLA_LOG_INFO_ON_ROOT("Flub/Fs = " << lubForce/fStokes << ", Tlub/Ts = " << lubTorque/tStokes); + } + + if( forceDiff < convergenceLimit && torqueDiff < convergenceLimit ) + { + WALBERLA_LOG_INFO_ON_ROOT("Force and torque norms converged - terminating simulation"); + break; + } + + } + + // reset forces + ps->forEachParticle(false, mesa_pd::kernel::SelectAll(), *accessor, + [](const size_t idx, auto& ac){ + ac.getForceRef(idx) = Vector3<real_t>(real_t(0)); + ac.getTorqueRef(idx) = Vector3<real_t>(real_t(0)); + }, *accessor ); + ps->forEachParticle(false, mesa_pd::kernel::SelectAll(), *accessor, resetHydrodynamicForceTorque, *accessor ); + + + } + + if( fileIO ) + { + std::string loggingFileName( baseFolder + "/Logging" ); + if( sphSphTest ) loggingFileName += "_SphSph"; + else loggingFileName += "_SphPla"; + loggingFileName += "_Setup" + std::to_string(setup); + loggingFileName += "_gapSize" + std::to_string(uint_c(gapSize*real_t(100))); + loggingFileName += "_radius" + std::to_string(uint_c(radius)); + loggingFileName += "_bvrf" + std::to_string(uint_c(bulkViscRateFactor)); + loggingFileName += "_mn" + std::to_string(float(magicNumber)); + if( useOmegaBulkAdaption ) loggingFileName += "_uOBA" + std::to_string(uint_c(adaptionLayerSize)); + if( !fileNameEnding.empty()) loggingFileName += "_" + fileNameEnding; + loggingFileName += ".txt"; + + WALBERLA_ROOT_SECTION() + { + std::ofstream file1; + file1.open( loggingFileName.c_str(), std::ofstream::app ); + file1.setf( std::ios::unitbuf ); + file1.precision(15); + file1 << radius << " " << gapSize << " " << fStokes << " " + << hydForce[0] << " " << hydForce[1] << " " << hydForce[2] << " " + << lubForce[0] << " " << lubForce[1] << " " << lubForce[2] << " " + << hydTorque[0] << " " << hydTorque[1] << " " << hydTorque[2] << " " + << lubTorque[0] << " " << lubTorque[1] << " " << lubTorque[2] << std::endl; + file1.close(); + } + } + + timeloopTiming.logResultOnRoot(); + + return EXIT_SUCCESS; +} + +} // namespace lubrication_force_evaluation + +int main( int argc, char **argv ){ + lubrication_force_evaluation::main(argc, argv); +} diff --git a/apps/benchmarks/FluidParticleCoupling/ObliqueDryCollision.cpp b/apps/benchmarks/FluidParticleCoupling/ObliqueDryCollision.cpp new file mode 100644 index 0000000000000000000000000000000000000000..fb1e42957ef2de7b349259947d59897fa9d7efee --- /dev/null +++ b/apps/benchmarks/FluidParticleCoupling/ObliqueDryCollision.cpp @@ -0,0 +1,216 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla is free software: you can +// redistribute it and/or modify it under the terms of the GNU General Public +// License as published by the Free Software Foundation, either version 3 of +// the License, or (at your option) any later version. +// +// waLBerla is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +// for more details. +// +// You should have received a copy of the GNU General Public License along +// with waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file ObliqueDryCollision.cpp +//! \author Christoph Rettinger <christoph.rettinger@fau.de> +// +//====================================================================================================================== + +#include "mesa_pd/collision_detection/AnalyticContactDetection.h" +#include "mesa_pd/common/ParticleFunctions.h" + +#include "mesa_pd/data/ParticleAccessorWithShape.h" +#include "mesa_pd/data/ParticleStorage.h" +#include "mesa_pd/data/ShapeStorage.h" + +#include "mesa_pd/kernel/DoubleCast.h" +#include "mesa_pd/kernel/ExplicitEulerWithShape.h" +#include "mesa_pd/kernel/VelocityVerletWithShape.h" +#include "mesa_pd/kernel/LinearSpringDashpot.h" +#include "mesa_pd/mpi/ReduceContactHistory.h" + +#include "core/Environment.h" +#include "core/logging/Logging.h" + +#include <iostream> + +namespace walberla { +namespace mesa_pd { + +/* + * Simulates oblique sphere-wall collision and checks rebound angle, i.e. the tangential part of the collision model. + * + */ +int main( int argc, char ** argv ) +{ + walberla::mpi::Environment env(argc, argv); + WALBERLA_UNUSED(env); + walberla::mpi::MPIManager::instance()->useWorldComm(); + + real_t uNin_SI = real_t(1.7); // m/s + real_t diameter_SI = real_t(0.00318); // m + //real_t density_SI = real_t(2500); // kg/m**3, not used + + real_t uNin = real_t(0.02); + real_t diameter = real_t(20); + real_t radius = real_t(0.5) * diameter; + real_t density = real_c(2.5); + + // these values have actually no influence here and are just computed for completeness + real_t dx_SI = diameter_SI / diameter; + real_t dt_SI = uNin / uNin_SI * dx_SI; + + real_t impactAngle = real_t(0); + real_t dt = real_t(0.1); // = (1 / #sub steps) + real_t frictionCoeff_s = real_t(0.8); // no influence + real_t frictionCoeff_d = real_t(0.12); // paper: 0.125+-0.007 + std::string filename = "TangentialCollision.txt"; + real_t collisionTime = real_t(80); + real_t nu = real_t(0.22); //Poissons ratio + bool useVelocityVerlet = false; + real_t restitutionCoeff = real_t(0.83); + + for( int i = 1; i < argc; ++i ) + { + if( std::strcmp( argv[i], "--impactAngle" ) == 0 ) { impactAngle = real_c( std::atof( argv[++i] ) ); continue; } + if( std::strcmp( argv[i], "--dt" ) == 0 ) { dt = real_c( std::atof( argv[++i] ) ); continue; } + if( std::strcmp( argv[i], "--staticFriction" ) == 0 ) { frictionCoeff_s = real_c( std::atof( argv[++i] ) ); continue; } + if( std::strcmp( argv[i], "--dynamicFriction" ) == 0 ) { frictionCoeff_d = real_c( std::atof( argv[++i] ) ); continue; } + if( std::strcmp( argv[i], "--collisionTime" ) == 0 ) { collisionTime = real_c( std::atof( argv[++i] ) ); continue; } + if( std::strcmp( argv[i], "--nu" ) == 0 ) { nu = real_c( std::atof( argv[++i] ) ); continue; } + if( std::strcmp( argv[i], "--filename" ) == 0 ) { filename = argv[++i]; continue; } + if( std::strcmp( argv[i], "--useVV" ) == 0 ) { useVelocityVerlet = true; continue; } + if( std::strcmp( argv[i], "--coefficientOfRestitution" ) == 0 ) { restitutionCoeff = real_c( std::atof( argv[++i] ) ); continue; } + } + + WALBERLA_LOG_INFO_ON_ROOT("******************************************************"); + WALBERLA_LOG_INFO_ON_ROOT("** NEW SIMULATION **"); + WALBERLA_LOG_INFO_ON_ROOT("******************************************************"); + WALBERLA_LOG_INFO_ON_ROOT("dt_SI = " << dt_SI); + WALBERLA_LOG_INFO_ON_ROOT("dx_SI = " << dx_SI); + WALBERLA_LOG_INFO_ON_ROOT("impactAngle = " << impactAngle); + WALBERLA_LOG_INFO_ON_ROOT("dt = " << dt); + WALBERLA_LOG_INFO_ON_ROOT("frictionCoeff_s = " << frictionCoeff_s); + WALBERLA_LOG_INFO_ON_ROOT("frictionCoeff_d = " << frictionCoeff_d); + WALBERLA_LOG_INFO_ON_ROOT("collisionTime = " << collisionTime); + WALBERLA_LOG_INFO_ON_ROOT("nu = " << nu); + WALBERLA_LOG_INFO_ON_ROOT("integrator = " << (useVelocityVerlet ? "Velocity Verlet" : "Explicit Euler")); + WALBERLA_LOG_INFO_ON_ROOT("restitutionCoeff = " << restitutionCoeff); + + + //init data structures + auto ps = walberla::make_shared<data::ParticleStorage>(2); + auto ss = walberla::make_shared<data::ShapeStorage>(); + using ParticleAccessor_T = mesa_pd::data::ParticleAccessorWithShape; + auto accessor = walberla::make_shared<ParticleAccessor_T >(ps, ss); + + auto sphereShape = ss->create<data::Sphere>( radius ); + ss->shapes[sphereShape]->updateMassAndInertia(density); + + const real_t particleMass = real_t(1) / ss->shapes[sphereShape]->getInvMass(); + const real_t Mij = particleMass; // * particleMass / ( real_t(2) * particleMass ); // Mij = M for sphere-wall collision + const real_t lnDryResCoeff = std::log(restitutionCoeff); + + // normal material parameters + const real_t stiffnessN = Mij * ( math::pi * math::pi + lnDryResCoeff * lnDryResCoeff ) / (collisionTime * collisionTime); // Costa et al., Eq. 18 + const real_t dampingN = - real_t(2) * Mij * lnDryResCoeff / collisionTime; // Costa et al., Eq. 18 + + WALBERLA_LOG_INFO_ON_ROOT("normal: stiffness = " << stiffnessN << ", damping = " << dampingN); + + // from Thornton et al + const real_t kappa = real_t(2) * ( real_t(1) - nu ) / ( real_t(2) - nu ) ; + const real_t stiffnessT = kappa * stiffnessN; + const real_t dampingT = std::sqrt(kappa) * dampingN; + + WALBERLA_LOG_INFO_ON_ROOT("tangential: kappa = " << kappa << ", stiffness T = " << stiffnessT << ", damping T = " << dampingT); + + real_t uTin = uNin * impactAngle; + + // create sphere + data::Particle&& p = *ps->create(); + p.setPosition(Vec3(0,0,2*radius)); + p.setLinearVelocity(Vec3(uTin, 0., -uNin)); + p.setType(0); + + // create plane + data::Particle&& p0 = *ps->create(true); + p0.setPosition(Vec3(0,0,0)); + p0.setShapeID(ss->create<data::HalfSpace>(Vector3<real_t>(0,0,1))); + p0.setType(0); + data::particle_flags::set(p0.getFlagsRef(), data::particle_flags::INFINITE); + data::particle_flags::set(p0.getFlagsRef(), data::particle_flags::FIXED); + + // velocity verlet + kernel::VelocityVerletWithShapePreForceUpdate vvPreForce( dt ); + kernel::VelocityVerletWithShapePostForceUpdate vvPostForce( dt ); + + // explicit euler + kernel::ExplicitEulerWithShape explEuler( dt ); + + // collision response + collision_detection::AnalyticContactDetection acd; + kernel::DoubleCast double_cast; + kernel::LinearSpringDashpot dem(1); + mpi::ReduceContactHistory rch; + dem.setStiffnessN(0,0,stiffnessN); + dem.setStiffnessT(0,0,stiffnessT); + dem.setDampingN(0,0,dampingN); + dem.setDampingT(0,0,dampingT); + dem.setFrictionCoefficientStatic(0,0,frictionCoeff_s); + dem.setFrictionCoefficientDynamic(0,0,frictionCoeff_d); + + WALBERLA_LOG_DEVEL("begin: vel = " << p.getLinearVelocity() << ", contact vel: " << getVelocityAtWFPoint(0,*accessor,p.getPosition() + Vec3(0,0,-radius)) ); + + uint_t steps = 0; + real_t maxPenetration = real_t(0); + do + { + if(useVelocityVerlet) vvPreForce(0,*accessor); + + real_t penetration; + + if (double_cast(0, 1, *accessor, acd, *accessor )) + { + penetration = acd.getPenetrationDepth(); + maxPenetration = std::max( maxPenetration, std::abs(penetration)); + + dem(acd.getIdx1(), acd.getIdx2(), *accessor, acd.getContactPoint(), acd.getContactNormal(), acd.getPenetrationDepth(), dt); + auto force = accessor->getForce(0); + auto torque = accessor->getTorque(0); + WALBERLA_LOG_INFO(steps << ": penetration = " << penetration << " || vel = " << accessor->getLinearVelocity(0) << " || force = " << force << ", torque = " << torque); + } + rch(*ps); + + if(useVelocityVerlet) vvPostForce(0,*accessor); + else explEuler(0, *accessor); + + ++steps; + } while (double_cast(0, 1, *accessor, acd, *accessor ) || p.getLinearVelocity()[2] < 0); + + real_t uTout = p.getLinearVelocity()[0] - radius * p.getAngularVelocity()[1]; + WALBERLA_LOG_DEVEL("end: linear vel = " << p.getLinearVelocity() << ", angular vel = " << p.getAngularVelocity()); + + real_t reboundAngle = uTout / uNin; + WALBERLA_LOG_INFO_ON_ROOT("gamma_in = " << impactAngle); + WALBERLA_LOG_INFO_ON_ROOT("gamma_out = " << reboundAngle); + + WALBERLA_LOG_INFO_ON_ROOT("Thornton: sliding should occur if " << real_t(2) * impactAngle / ( frictionCoeff_d * ( real_t(1) + restitutionCoeff)) << " >= " << real_t(7) - real_t(1) / kappa ); + WALBERLA_LOG_INFO_ON_ROOT("Max penetration = " << maxPenetration << " -> " << maxPenetration / radius * 100. << "% of radius"); + + std::ofstream file; + file.open( filename.c_str(), std::ios::out | std::ios::app ); + file << impactAngle << " " << reboundAngle << "\n"; + file.close(); + + return EXIT_SUCCESS; +} + +} //namespace mesa_pd +} //namespace walberla + +int main( int argc, char ** argv ) +{ + return walberla::mesa_pd::main(argc, argv); +} diff --git a/apps/benchmarks/FluidParticleCoupling/ObliqueWetCollision.cpp b/apps/benchmarks/FluidParticleCoupling/ObliqueWetCollision.cpp new file mode 100644 index 0000000000000000000000000000000000000000..c8601416f4e27d421b913ea52dd56dde3abee7e4 --- /dev/null +++ b/apps/benchmarks/FluidParticleCoupling/ObliqueWetCollision.cpp @@ -0,0 +1,1322 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla is free software: you can +// redistribute it and/or modify it under the terms of the GNU General Public +// License as published by the Free Software Foundation, either version 3 of +// the License, or (at your option) any later version. +// +// waLBerla is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +// for more details. +// +// You should have received a copy of the GNU General Public License along +// with waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file ObliqueWetCollision.cpp +//! \ingroup lbm_mesapd_coupling +//! \author Christoph Rettinger <christoph.rettinger@fau.de> +// +//====================================================================================================================== + +#include "blockforest/Initialization.h" +#include "blockforest/communication/UniformBufferedScheme.h" + +#include "boundary/all.h" + +#include "core/waLBerlaBuildInfo.h" +#include "core/DataTypes.h" +#include "core/Environment.h" +#include "core/SharedFunctor.h" +#include "core/debug/Debug.h" +#include "core/debug/TestSubsystem.h" +#include "core/math/all.h" +#include "core/timing/RemainingTimeLogger.h" +#include "core/logging/all.h" +#include "core/mpi/Broadcast.h" +#include "core/mpi/Reduce.h" + +#include "domain_decomposition/SharedSweep.h" + +#include "field/AddToStorage.h" +#include "field/adaptors/AdaptorCreators.h" +#include "field/communication/PackInfo.h" +#include "field/interpolators/FieldInterpolatorCreators.h" +#include "field/interpolators/NearestNeighborFieldInterpolator.h" + +#include "lbm/boundary/all.h" +#include "lbm/communication/PdfFieldPackInfo.h" +#include "lbm/field/AddToStorage.h" +#include "lbm/field/Adaptors.h" +#include "lbm/field/PdfField.h" +#include "lbm/lattice_model/D3Q19.h" +#include "lbm/sweeps/CellwiseSweep.h" +#include "lbm/sweeps/SweepWrappers.h" + +#include "lbm_mesapd_coupling/mapping/ParticleMapping.h" +#include "lbm_mesapd_coupling/momentum_exchange_method/MovingParticleMapping.h" +#include "lbm_mesapd_coupling/momentum_exchange_method/boundary/SimpleBB.h" +#include "lbm_mesapd_coupling/momentum_exchange_method/boundary/CurvedLinear.h" +#include "lbm_mesapd_coupling/momentum_exchange_method/reconstruction/Reconstructor.h" +#include "lbm_mesapd_coupling/momentum_exchange_method/reconstruction/ExtrapolationDirectionFinder.h" +#include "lbm_mesapd_coupling/momentum_exchange_method/reconstruction/PdfReconstructionManager.h" +#include "lbm_mesapd_coupling/utility/AddForceOnParticlesKernel.h" +#include "lbm_mesapd_coupling/utility/ParticleSelector.h" +#include "lbm_mesapd_coupling/DataTypes.h" +#include "lbm_mesapd_coupling/utility/AverageHydrodynamicForceTorqueKernel.h" +#include "lbm_mesapd_coupling/utility/AddHydrodynamicInteractionKernel.h" +#include "lbm_mesapd_coupling/utility/ResetHydrodynamicForceTorqueKernel.h" +#include "lbm_mesapd_coupling/utility/LubricationCorrectionKernel.h" +#include "lbm_mesapd_coupling/utility/OmegaBulkAdaption.h" + +#include "mesa_pd/collision_detection/AnalyticContactDetection.h" +#include "mesa_pd/data/ParticleAccessorWithShape.h" +#include "mesa_pd/data/ParticleStorage.h" +#include "mesa_pd/data/ShapeStorage.h" +#include "mesa_pd/data/DataTypes.h" +#include "mesa_pd/data/shape/HalfSpace.h" +#include "mesa_pd/data/shape/Sphere.h" +#include "mesa_pd/domain/BlockForestDomain.h" +#include "mesa_pd/domain/BlockForestDataHandling.h" +#include "mesa_pd/kernel/DoubleCast.h" +#include "mesa_pd/kernel/ExplicitEulerWithShape.h" +#include "mesa_pd/kernel/LinearSpringDashpot.h" +#include "mesa_pd/kernel/NonLinearSpringDashpot.h" +#include "mesa_pd/kernel/ParticleSelector.h" +#include "mesa_pd/kernel/VelocityVerletWithShape.h" +#include "mesa_pd/mpi/SyncNextNeighbors.h" +#include "mesa_pd/mpi/ReduceProperty.h" +#include "mesa_pd/mpi/ReduceContactHistory.h" +#include "mesa_pd/mpi/ContactFilter.h" +#include "mesa_pd/mpi/notifications/ForceTorqueNotification.h" +#include "mesa_pd/vtk/ParticleVtkOutput.h" + +#include "timeloop/SweepTimeloop.h" + +#include "vtk/all.h" +#include "field/vtk/all.h" +#include "lbm/vtk/all.h" + +#include "GeneratedLBM.h" + +#include <functional> + +#define USE_TRT_LIKE_LATTICE_MODEL + +namespace oblique_wet_collision +{ + +/////////// +// USING // +/////////// + +using namespace walberla; +using walberla::uint_t; + +using LatticeModel_T = lbm::GeneratedLBM; + +using Stencil_T = LatticeModel_T::Stencil; +using PdfField_T = lbm::PdfField<LatticeModel_T>; + +using flag_t = walberla::uint8_t; +using FlagField_T = FlagField<flag_t>; + +using ScalarField_T = GhostLayerField< real_t, 1>; + +const uint_t FieldGhostLayers = 1; + +/////////// +// FLAGS // +/////////// + +const FlagUID Fluid_Flag( "fluid" ); +const FlagUID NoSlip_Flag( "no slip" ); +const FlagUID MO_Flag( "moving obstacle" ); +const FlagUID FormerMO_Flag( "former moving obstacle" ); +const FlagUID SimplePressure_Flag("simple pressure"); + +///////////////////////////////////// +// BOUNDARY HANDLING CUSTOMIZATION // +///////////////////////////////////// +template <typename ParticleAccessor_T> +class MyBoundaryHandling +{ +public: + + using NoSlip_T = lbm::NoSlip< LatticeModel_T, flag_t >; + using MO_T = lbm_mesapd_coupling::CurvedLinear< LatticeModel_T, FlagField_T, ParticleAccessor_T >; + using SimplePressure_T = lbm::SimplePressure< LatticeModel_T, flag_t >; + using Type = BoundaryHandling< FlagField_T, Stencil_T, NoSlip_T, MO_T, SimplePressure_T >; + + MyBoundaryHandling( const BlockDataID & flagFieldID, const BlockDataID & pdfFieldID, + const BlockDataID & particleFieldID, const shared_ptr<ParticleAccessor_T>& ac, + bool applyOutflowBCAtTop) : + flagFieldID_( flagFieldID ), pdfFieldID_( pdfFieldID ), particleFieldID_( particleFieldID ), ac_( ac ), applyOutflowBCAtTop_(applyOutflowBCAtTop) {} + + Type * operator()( IBlock* const block, const StructuredBlockStorage* const storage ) const + { + WALBERLA_ASSERT_NOT_NULLPTR( block ); + WALBERLA_ASSERT_NOT_NULLPTR( storage ); + + auto * flagField = block->getData< FlagField_T >( flagFieldID_ ); + auto * pdfField = block->getData< PdfField_T > ( pdfFieldID_ ); + auto * particleField = block->getData< lbm_mesapd_coupling::ParticleField_T > ( particleFieldID_ ); + + const auto fluid = flagField->flagExists( Fluid_Flag ) ? flagField->getFlag( Fluid_Flag ) : flagField->registerFlag( Fluid_Flag ); + + Type * handling = new Type( "moving obstacle boundary handling", flagField, fluid, + NoSlip_T( "NoSlip", NoSlip_Flag, pdfField ), + MO_T( "MO", MO_Flag, pdfField, flagField, particleField, ac_, fluid, *storage, *block ), + SimplePressure_T( "SimplePressure", SimplePressure_Flag, pdfField, real_t(1) ) ); + + if(applyOutflowBCAtTop_) + { + const auto simplePressure = flagField->getFlag( SimplePressure_Flag ); + + CellInterval domainBB = storage->getDomainCellBB(); + domainBB.xMin() -= cell_idx_c( FieldGhostLayers ); + domainBB.xMax() += cell_idx_c( FieldGhostLayers ); + + domainBB.yMin() -= cell_idx_c( FieldGhostLayers ); + domainBB.yMax() += cell_idx_c( FieldGhostLayers ); + + domainBB.zMin() -= cell_idx_c( FieldGhostLayers ); + domainBB.zMax() += cell_idx_c( FieldGhostLayers ); + + // TOP + CellInterval top( domainBB.xMin(), domainBB.yMin(), domainBB.zMax(), domainBB.xMax(), domainBB.yMax(), domainBB.zMax() ); + storage->transformGlobalToBlockLocalCellInterval( top, *block ); + handling->forceBoundary( simplePressure, top ); + } + + handling->fillWithDomain( FieldGhostLayers ); + + return handling; + } + +private: + + const BlockDataID flagFieldID_; + const BlockDataID pdfFieldID_; + const BlockDataID particleFieldID_; + + shared_ptr<ParticleAccessor_T> ac_; + + bool applyOutflowBCAtTop_; +}; + + +//******************************************************************************************************************* +/*!\brief Evaluating the position and velocity of the sphere + * + */ +//******************************************************************************************************************* +template< typename ParticleAccessor_T> +class SpherePropertyLogger +{ +public: + SpherePropertyLogger( const shared_ptr< ParticleAccessor_T > & ac, walberla::id_t sphereUid, + const std::string fileNameLogging, const std::string fileNameForceLogging, bool fileIO, + real_t diameter, real_t uIn, real_t impactRatio, uint_t numberOfSubSteps, real_t fGravX, real_t fGravZ) : + ac_( ac ), sphereUid_( sphereUid ), fileNameLogging_( fileNameLogging ), fileNameForceLogging_( fileNameForceLogging ), + fileIO_(fileIO), + diameter_( diameter ), uIn_( uIn ), impactRatio_(impactRatio), numberOfSubSteps_(numberOfSubSteps), fGravX_(fGravX), fGravZ_(fGravZ), + gap_( real_t(0) ), settlingVelocity_( real_t(0) ), tangentialVelocity_(real_t(0)) + { + if ( fileIO_ ) + { + WALBERLA_ROOT_SECTION() + { + std::ofstream file; + file.open( fileNameLogging_.c_str() ); + file << "#\t D\t uIn\t impactRatio\t positionX\t positionY\t positionZ\t velX\t velY\t velZ\t angX\t angY\t angZ\n"; + file.close(); + } + WALBERLA_ROOT_SECTION() + { + std::ofstream file; + file.open( fileNameForceLogging_.c_str() ); + file << "#\t fGravX\t fGravZ\t fHydX\t fHydZ\t fLubX\t fLubZ\t fColX\t fColZ\t tHydY\t tLubY\t tColY\n"; + file.close(); + } + } + } + + void operator()(const uint_t timeStep, const uint_t subStep, Vector3<real_t> fHyd, Vector3<real_t> fLub, Vector3<real_t> fCol, Vector3<real_t> tHyd, Vector3<real_t> tLub, Vector3<real_t> tCol ) + { + real_t curTimestep = real_c(timeStep) + real_c(subStep) / real_c(numberOfSubSteps_); + + Vector3<real_t> pos(real_t(0)); + Vector3<real_t> transVel(real_t(0)); + Vector3<real_t> angVel(real_t(0)); + + size_t idx = ac_->uidToIdx(sphereUid_); + if( idx != std::numeric_limits<size_t>::max()) + { + if(!mesa_pd::data::particle_flags::isSet( ac_->getFlags(idx), mesa_pd::data::particle_flags::GHOST)) + { + pos = ac_->getPosition(idx); + transVel = ac_->getLinearVelocity(idx); + angVel = ac_->getAngularVelocity(idx); + } + } + + WALBERLA_MPI_SECTION() + { + mpi::allReduceInplace( pos[0], mpi::SUM ); + mpi::allReduceInplace( pos[1], mpi::SUM ); + mpi::allReduceInplace( pos[2], mpi::SUM ); + mpi::allReduceInplace( transVel[0], mpi::SUM ); + mpi::allReduceInplace( transVel[1], mpi::SUM ); + mpi::allReduceInplace( transVel[2], mpi::SUM ); + mpi::allReduceInplace( angVel[0], mpi::SUM ); + mpi::allReduceInplace( angVel[1], mpi::SUM ); + mpi::allReduceInplace( angVel[2], mpi::SUM ); + } + + position_ = pos; + gap_ = pos[2] - real_t(0.5) * diameter_; + settlingVelocity_ = transVel[2]; + tangentialVelocity_ = transVel[0] - diameter_ * real_t(0.5) * angVel[1]; + + + if( fileIO_ /* && gap_ < diameter_*/) + { + writeToLoggingFile( curTimestep, pos, transVel, angVel); + writeToForceLoggingFile( curTimestep, fHyd, fLub, fCol, tHyd, tLub, tCol); + } + } + + real_t getGapSize() const + { + return gap_; + } + + real_t getSettlingVelocity() const + { + return settlingVelocity_; + } + + real_t getTangentialVelocity() const + { + return tangentialVelocity_; + } + + Vector3<real_t> getPosition() const + { + return position_; + } + + +private: + void writeToLoggingFile( real_t timestep, Vector3<real_t> position, Vector3<real_t> transVel, Vector3<real_t> angVel ) + { + WALBERLA_ROOT_SECTION() + { + std::ofstream file; + file.open( fileNameLogging_.c_str(), std::ofstream::app ); + + file << timestep << "\t" << diameter_ << "\t" << uIn_ << "\t" << impactRatio_ + << "\t" << position[0] << "\t" << position[1] << "\t" << position[2] + << "\t" << transVel[0] << "\t" << transVel[1] << "\t" << transVel[2] + << "\t" << angVel[0] << "\t" << angVel[1] << "\t" << angVel[2] + << "\n"; + file.close(); + } + } + + void writeToForceLoggingFile( real_t timestep, Vector3<real_t> fHyd, Vector3<real_t> fLub, Vector3<real_t> fCol, Vector3<real_t> tHyd, Vector3<real_t> tLub, Vector3<real_t> tCol ) + { + WALBERLA_ROOT_SECTION() + { + std::ofstream file; + file.open( fileNameForceLogging_.c_str(), std::ofstream::app ); + + file << timestep + << "\t" << fGravX_ << "\t" << fGravZ_ + << "\t" << fHyd[0] << "\t" << fHyd[2] + << "\t" << fLub[0] << "\t" << fLub[2] + << "\t" << fCol[0] << "\t" << fCol[2] + << "\t" << tHyd[1] + << "\t" << tLub[1] + << "\t" << tCol[1] + << "\n"; + file.close(); + } + } + + + shared_ptr< ParticleAccessor_T > ac_; + const walberla::id_t sphereUid_; + std::string fileNameLogging_, fileNameForceLogging_; + bool fileIO_; + real_t diameter_, uIn_, impactRatio_; + uint_t numberOfSubSteps_; + real_t fGravX_, fGravZ_; + + real_t gap_, settlingVelocity_, tangentialVelocity_; + Vector3<real_t> position_; + +}; + + +void createPlaneSetup(const shared_ptr<mesa_pd::data::ParticleStorage> & ps, const shared_ptr<mesa_pd::data::ShapeStorage> & ss, const math::AABB & simulationDomain, bool applyOutflowBCAtTop ) +{ + // create bounding planes + mesa_pd::data::Particle&& p0 = *ps->create(true); + p0.setPosition(simulationDomain.minCorner()); + p0.setShapeID(ss->create<mesa_pd::data::HalfSpace>( Vector3<real_t>(0,0,1) )); + p0.setOwner(mpi::MPIManager::instance()->rank()); + p0.setType(0); + mesa_pd::data::particle_flags::set(p0.getFlagsRef(), mesa_pd::data::particle_flags::INFINITE); + mesa_pd::data::particle_flags::set(p0.getFlagsRef(), mesa_pd::data::particle_flags::FIXED); + + if(!applyOutflowBCAtTop) + { + //only create top plane when no outflow BC should be set there + mesa_pd::data::Particle&& p1 = *ps->create(true); + p1.setPosition(simulationDomain.maxCorner()); + p1.setShapeID(ss->create<mesa_pd::data::HalfSpace>( Vector3<real_t>(0,0,-1) )); + p1.setOwner(mpi::MPIManager::instance()->rank()); + p1.setType(0); + mesa_pd::data::particle_flags::set(p1.getFlagsRef(), mesa_pd::data::particle_flags::INFINITE); + mesa_pd::data::particle_flags::set(p1.getFlagsRef(), mesa_pd::data::particle_flags::FIXED); + } + +} + + + +class ForcesOnSphereLogger +{ +public: + ForcesOnSphereLogger( const std::string & fileName, bool fileIO, real_t weightForce, uint_t numberOfSubSteps ) : + fileName_( fileName ), fileIO_( fileIO ), weightForce_( weightForce ), numberOfSubSteps_( numberOfSubSteps ) + { + if ( fileIO_ ) + { + WALBERLA_ROOT_SECTION() + { + std::ofstream file; + file.open( fileName_.c_str() ); + file << "#t\t BuoyAndGravF\t HydInteractionF\t LubricationF\t CollisionForce\n"; + file.close(); + } + } + } + + void operator()(const uint_t timeStep, const uint_t subStep, real_t hydForce, real_t lubForce, real_t collisionForce ) + { + real_t curTimestep = real_c(timeStep) + real_c(subStep) / real_c(numberOfSubSteps_); + + if ( fileIO_ ) + { + WALBERLA_ROOT_SECTION() + { + std::ofstream file; + file.open( fileName_.c_str(), std::ofstream::app ); + + file << std::setprecision(10) << curTimestep << "\t" << weightForce_ << "\t" << hydForce << "\t" << lubForce << "\t" << collisionForce << "\n"; + file.close(); + } + } + + } + + +private: + + std::string fileName_; + bool fileIO_; + + real_t weightForce_; + uint_t numberOfSubSteps_; +}; + +template<typename ParticleAccessor_T> +Vector3<real_t> getForce(walberla::id_t uid, ParticleAccessor_T & ac) +{ + auto idx = ac.uidToIdx(uid); + Vector3<real_t> force(0); + if(idx != ac.getInvalidIdx()) + { + force = ac.getForce(idx); + } + WALBERLA_MPI_SECTION() + { + mpi::allReduceInplace( force[0], mpi::SUM ); + mpi::allReduceInplace( force[1], mpi::SUM ); + mpi::allReduceInplace( force[2], mpi::SUM ); + } + return force; +} + +template<typename ParticleAccessor_T> +Vector3<real_t> getTorque(walberla::id_t uid, ParticleAccessor_T & ac) +{ + auto idx = ac.uidToIdx(uid); + Vector3<real_t> torque(0); + if(idx != ac.getInvalidIdx()) + { + torque = ac.getTorque(idx); + } + WALBERLA_MPI_SECTION() + { + mpi::allReduceInplace( torque[0], mpi::SUM ); + mpi::allReduceInplace( torque[1], mpi::SUM ); + mpi::allReduceInplace( torque[2], mpi::SUM ); + } + return torque; +} + +////////// +// MAIN // +////////// + +//******************************************************************************************************************* +/*!\brief PHYSICAL Test case of a sphere settling and colliding with a wall submerged in a viscous fluid. + * + * The collision is oblique, i.e. features normal and tangential contributions. + * There are in principle two ways to carry out this simulation: + * 1) Use some (artificial) gravitational force such that the x(tangential) and z(normal)- components + * have the same ratio as the desired impact ratio of the velocities. + * To have similar normal collision velocities, the option 'useFullGravityInNormalDirection' always applies the same + * gravitational acceleration in normal direction. + * The main problem, however, is that the sphere begins to rotate for increasing impact angles and will thus generate + * a reposing force in normal direction, that will alter the intended impact ratio to larger values. + * Additionally, a reasonable value for the relaxation time, tau, has to be given to avoid too large settling velocities. + * + * 2) Specify a (normal or magnitude-wise) Stokes number and artificially accelerate the sphere to this value. + * Thus, the settling velocity is prescribed and no rotational velocity is allowed. + * This is stopped before the collision to allow for a 'natural' collision. + * To avoid a too large damping by the fluid forces, since no gravitational forces are present to balance them, + * an artificial gravity is assumed and taken as the opposite fluid force acting when the artificial + * acceleration is stopped. + * + * For details see Rettinger, Ruede 2020 + */ +//******************************************************************************************************************* + + +int main( int argc, char **argv ) +{ + Environment env( argc, argv ); + + if( !env.config() ) + { + WALBERLA_ABORT("Usage: " << argv[0] << " path-to-configuration-file \n"); + } + + Config::BlockHandle simulationInput = env.config()->getBlock( "Setup" ); + + + const int caseNumber = simulationInput.getParameter<int>("case"); + Config::BlockHandle caseDescription = env.config()->getBlock( "Case"+std::to_string(caseNumber) ); + + const std::string material = caseDescription.getParameter<std::string>("material"); + Config::BlockHandle materialDescription = env.config()->getBlock( "Mat_"+material ); + const real_t densitySphere_SI = materialDescription.getParameter<real_t>("densitySphere_SI"); + const real_t diameter_SI = materialDescription.getParameter<real_t>("diameter_SI"); + const real_t restitutionCoeff = materialDescription.getParameter<real_t>("restitutionCoeff"); + const real_t frictionCoeff = materialDescription.getParameter<real_t>("frictionCoeff"); + const real_t poissonsRatio = materialDescription.getParameter<real_t>("poissonsRatio"); + + const std::string fluid = caseDescription.getParameter<std::string>("fluid"); + Config::BlockHandle fluidDescription = env.config()->getBlock( "Fluid_"+fluid ); + const real_t densityFluid_SI = fluidDescription.getParameter<real_t>("densityFluid_SI"); + const real_t dynamicViscosityFluid_SI = fluidDescription.getParameter<real_t>("dynamicViscosityFluid_SI"); + + const std::string simulationVariant = simulationInput.getParameter<std::string>("variant"); + Config::BlockHandle variantDescription = env.config()->getBlock( "Variant_"+simulationVariant ); + + const real_t impactRatio = simulationInput.getParameter<real_t>("impactRatio"); + const bool applyOutflowBCAtTop = simulationInput.getParameter<bool>("applyOutflowBCAtTop"); + + // variant dependent parameters + const Vector3<real_t> domainSizeNonDim = variantDescription.getParameter<Vector3<real_t> >("domainSize"); + const Vector3<uint_t> numberOfBlocksPerDirection = variantDescription.getParameter<Vector3<uint_t> >("numberOfBlocksPerDirection"); + const real_t initialSphereHeight = variantDescription.getParameter<real_t>("initialSphereHeight"); + + // LBM + const real_t diameter = simulationInput.getParameter<real_t>("diameter"); + const real_t magicNumber = simulationInput.getParameter<real_t>("magicNumber"); + const real_t bulkViscRateFactor = simulationInput.getParameter<real_t>("bulkViscRateFactor"); + const bool useOmegaBulkAdaption = simulationInput.getParameter<bool>("useOmegaBulkAdaption"); + + const uint_t numRPDSubCycles = simulationInput.getParameter<uint_t>("numRPDSubCycles"); + const bool useLubricationCorrection = simulationInput.getParameter<bool>("useLubricationCorrection"); + const real_t lubricationCutOffDistanceNormal = simulationInput.getParameter<real_t>("lubricationCutOffDistanceNormal"); + const real_t lubricationCutOffDistanceTangentialTranslational = simulationInput.getParameter<real_t>("lubricationCutOffDistanceTangentialTranslational"); + const real_t lubricationCutOffDistanceTangentialRotational = simulationInput.getParameter<real_t>("lubricationCutOffDistanceTangentialRotational"); + const real_t lubricationMinimalGapSizeNonDim = simulationInput.getParameter<real_t>("lubricationMinimalGapSizeNonDim"); + + // Collision Response + const bool useVelocityVerlet = simulationInput.getParameter<bool>("useVelocityVerlet"); + const real_t collisionTime = simulationInput.getParameter<real_t>("collisionTime"); + + const bool averageForceTorqueOverTwoTimeSteps = simulationInput.getParameter<bool>("averageForceTorqueOverTwoTimeSteps"); + const bool conserveMomentum = simulationInput.getParameter<bool>("conserveMomentum"); + const std::string reconstructorType = simulationInput.getParameter<std::string>("reconstructorType"); + + const bool fileIO = simulationInput.getParameter<bool>("fileIO"); + const uint_t vtkIOFreq = simulationInput.getParameter<uint_t>("vtkIOFreq"); + const std::string baseFolder = simulationInput.getParameter<std::string>("baseFolder"); + + WALBERLA_ROOT_SECTION() + { + if( fileIO ) + { + // create base directory if it does not yet exist + filesystem::path tpath( baseFolder ); + if( !filesystem::exists( tpath ) ) + filesystem::create_directory( tpath ); + } + } + + ////////////////////////////////////// + // SIMULATION PROPERTIES in SI units// + ////////////////////////////////////// + + const real_t impactAngle = std::atan(impactRatio); + const real_t densityRatio = densitySphere_SI / densityFluid_SI; + const real_t kinematicViscosityFluid_SI = dynamicViscosityFluid_SI / densityFluid_SI; + + WALBERLA_LOG_INFO_ON_ROOT("SETUP OF CASE " << caseNumber); + WALBERLA_LOG_INFO_ON_ROOT("Setup (in SI units):"); + WALBERLA_LOG_INFO_ON_ROOT(" - impact ratio = " << impactRatio ); + WALBERLA_LOG_INFO_ON_ROOT(" - impact angle = " << impactAngle << " (" << impactAngle * real_t(180) / math::pi << " degrees) "); + WALBERLA_LOG_INFO_ON_ROOT(" - sphere: diameter = " << diameter_SI << ", densityRatio = " << densityRatio); + WALBERLA_LOG_INFO_ON_ROOT(" - fluid: density = " << densityFluid_SI << ", dyn. visc = " << dynamicViscosityFluid_SI << ", kin. visc = " << kinematicViscosityFluid_SI ); + WALBERLA_LOG_INFO_ON_ROOT(" - domain size = <" << real_c(domainSizeNonDim[0]) * diameter_SI << "," << real_c(domainSizeNonDim[1]) * diameter_SI << ","<< real_c(domainSizeNonDim[2]) * diameter_SI << ">"); + + + ////////////////////////// + // NUMERICAL PARAMETERS // + ////////////////////////// + + WALBERLA_LOG_INFO_ON_ROOT("Setup (in simulation, i.e. lattice, units):"); + + const real_t dx_SI = diameter_SI / diameter; + const real_t sphereVolume = math::pi / real_t(6) * diameter * diameter * diameter; + + real_t dt_SI, viscosity, omega; + real_t uIn = real_t(1); + real_t accelerationFactor = real_t(0); + bool applyArtificialGravityAfterAccelerating = false; + bool useFullGravityInNormalDirection = false; + bool artificiallyAccelerateSphere = false; + Vector3<real_t> gravitationalAccelerationVec(real_t(0)); + + if(simulationVariant=="Acceleration") + { + WALBERLA_LOG_INFO_ON_ROOT("USING MODE OF ARTIFICIALLY ACCELERATED SPHERE"); + + artificiallyAccelerateSphere = true; + + real_t StTarget = variantDescription.getParameter<real_t>("StTarget"); + const bool useStTargetInNormalDirection = variantDescription.getParameter<bool>("useStTargetInNormalDirection"); + applyArtificialGravityAfterAccelerating = variantDescription.getParameter<bool>("applyArtificialGravityAfterAccelerating"); + bool applyUInNormalDirection = variantDescription.getParameter<bool>("applyUInNormalDirection"); + uIn = variantDescription.getParameter<real_t>("uIn"); + accelerationFactor = variantDescription.getParameter<real_t>("accelerationFactor"); + + if(applyUInNormalDirection) + { + // the value for uIn is defined as the normal impact velocity, i.e. uNIn + uIn = uIn / std::cos(impactAngle); + } + + real_t StTargetN; + if(useStTargetInNormalDirection) + { + StTargetN = StTarget; + StTarget = StTarget / std::cos(impactAngle); + + } else + { + StTargetN = StTarget * std::cos(impactAngle); + } + WALBERLA_LOG_INFO_ON_ROOT(" - target St in settling direction = " << StTarget ); + WALBERLA_LOG_INFO_ON_ROOT(" - target St in normal direction = " << StTargetN ); + + const real_t uIn_SI = StTarget * real_t(9) / densityRatio * kinematicViscosityFluid_SI / diameter_SI; + dt_SI = uIn / uIn_SI * dx_SI; + viscosity = kinematicViscosityFluid_SI * dt_SI / ( dx_SI * dx_SI ); + + //note: no gravity when accelerating artificially + omega = lbm::collision_model::omegaFromViscosity(viscosity); + + const real_t uNIn = uIn * std::cos(impactAngle); + const real_t Re_p = diameter * uIn / viscosity; + + WALBERLA_LOG_INFO_ON_ROOT(" - target Reynolds number = " << Re_p ); + WALBERLA_LOG_INFO_ON_ROOT(" - expected normal impact velocity = " << uNIn ); + WALBERLA_LOG_INFO_ON_ROOT(" - expected tangential impact velocity = " << uIn * std::sin(impactAngle) ); + + } else{ + + WALBERLA_LOG_INFO_ON_ROOT("USING MODE OF SPHERE SETTLING UNDER GRAVITY"); + + const real_t gravitationalAcceleration_SI = variantDescription.getParameter<real_t>("gravitationalAcceleration_SI"); + useFullGravityInNormalDirection = variantDescription.getParameter<bool>("useFullGravityInNormalDirection"); + const real_t tau = variantDescription.getParameter<real_t>("tau"); + + const real_t ug_SI = std::sqrt((densityRatio-real_t(1)) * gravitationalAcceleration_SI * diameter_SI); + const real_t Ga = ug_SI * diameter_SI / kinematicViscosityFluid_SI; + + viscosity = lbm::collision_model::viscosityFromOmega(real_t(1) / tau); + dt_SI = viscosity * dx_SI * dx_SI / kinematicViscosityFluid_SI; + real_t gravitationalAcceleration = gravitationalAcceleration_SI * dt_SI * dt_SI / dx_SI; + omega = real_t(1) / tau; + + WALBERLA_LOG_INFO_ON_ROOT(" - ug = " << ug_SI ); + WALBERLA_LOG_INFO_ON_ROOT(" - Galileo number = " << Ga ); + + gravitationalAccelerationVec = useFullGravityInNormalDirection ? Vector3<real_t>( impactRatio, real_t(0), -real_t(1) ) * gravitationalAcceleration + : Vector3<real_t>( std::sin(impactAngle), real_t(0), -std::cos(impactAngle) ) * gravitationalAcceleration; + WALBERLA_LOG_INFO_ON_ROOT(" - g " << gravitationalAcceleration); + } + + const real_t omegaBulk = lbm_mesapd_coupling::omegaBulkFromOmega(omega, bulkViscRateFactor); + + const real_t densityFluid = real_t(1); + const real_t densitySphere = densityRatio; + + const real_t dx = real_t(1); + + const real_t responseTime = densityRatio * diameter * diameter / ( real_t(18) * viscosity ); + + const real_t particleMass = densitySphere * sphereVolume; + const real_t Mij = particleMass; // * particleMass / ( real_t(2) * particleMass ); // Mij = M for sphere-wall collision + const real_t lnDryResCoeff = std::log(restitutionCoeff); + + const real_t stiffnessN = Mij * ( math::pi * math::pi + lnDryResCoeff * lnDryResCoeff ) / (collisionTime * collisionTime); // Costa et al., Eq. 18 + const real_t dampingN = - real_t(2) * Mij * lnDryResCoeff / collisionTime; // Costa et al., Eq. 18 + + // from Biegert et al + const real_t kappa = real_t(2) * ( real_t(1) - poissonsRatio ) / ( real_t(2) - poissonsRatio ) ; + const real_t stiffnessT = kappa * stiffnessN; + const real_t dampingT = std::sqrt(kappa) * dampingN; + + Vector3<uint_t> domainSize( uint_c(domainSizeNonDim[0] * diameter ), uint_c(domainSizeNonDim[1] * diameter ), uint_c(domainSizeNonDim[2] * diameter ) ); + + real_t initialSpherePosition = initialSphereHeight * diameter; + + WALBERLA_LOG_INFO_ON_ROOT(" - dt_SI = " << dt_SI << " s, dx_SI = " << dx_SI << " m"); + WALBERLA_LOG_INFO_ON_ROOT(" - domain size = " << domainSize); + if(applyOutflowBCAtTop) WALBERLA_LOG_INFO_ON_ROOT(" - outflow BC at top"); + WALBERLA_LOG_INFO_ON_ROOT(" - sphere: diameter = " << diameter << ", density = " << densitySphere ); + WALBERLA_LOG_INFO_ON_ROOT(" - initial sphere position = " << initialSpherePosition ); + WALBERLA_LOG_INFO_ON_ROOT(" - fluid: density = " << densityFluid << ", relaxation time (tau) = " << real_t(1) / omega << ", omega = " << omega << ", kin. visc = " << viscosity ); + WALBERLA_LOG_INFO_ON_ROOT(" - magic number = " << magicNumber); + WALBERLA_LOG_INFO_ON_ROOT(" - omega bulk = " << omegaBulk << ", bulk visc = " << lbm_mesapd_coupling::bulkViscosityFromOmegaBulk(omegaBulk) << " ( bulk visc rate factor (conste) = " << bulkViscRateFactor << ")"); + if(useOmegaBulkAdaption) WALBERLA_LOG_INFO_ON_ROOT(" - using omega bulk adaption in vicinity of sphere"); + WALBERLA_LOG_INFO_ON_ROOT(" - gravitational acceleration = " << gravitationalAccelerationVec ); + WALBERLA_LOG_INFO_ON_ROOT(" - Stokes response time = " << responseTime ); + if( artificiallyAccelerateSphere ) + { + WALBERLA_LOG_INFO_ON_ROOT(" - artificially accelerating sphere with factor " << accelerationFactor) + if(applyArtificialGravityAfterAccelerating) + { + WALBERLA_LOG_INFO_ON_ROOT(" - applying artificial gravity after accelerating" ); + } + } + WALBERLA_LOG_INFO_ON_ROOT(" - integrator = " << (useVelocityVerlet ? "Velocity Verlet" : "Explicit Euler") ); + if( vtkIOFreq > 0 ) + { + WALBERLA_LOG_INFO_ON_ROOT(" - writing vtk files to folder \"" << baseFolder << "\" with frequency " << vtkIOFreq); + } + + WALBERLA_LOG_INFO_ON_ROOT("Collision Response properties:" ); + WALBERLA_LOG_INFO_ON_ROOT(" - collision time = " << collisionTime ); + WALBERLA_LOG_INFO_ON_ROOT(" - damping coeff n = " << dampingN ); + WALBERLA_LOG_INFO_ON_ROOT(" - stiffness coeff n = " << stiffnessN ); + WALBERLA_LOG_INFO_ON_ROOT(" - damping coeff t = " << dampingT ); + WALBERLA_LOG_INFO_ON_ROOT(" - stiffness coeff t= " << stiffnessT ); + WALBERLA_LOG_INFO_ON_ROOT(" - coeff of restitution = " << restitutionCoeff ); + WALBERLA_LOG_INFO_ON_ROOT(" - coeff of friction = " << frictionCoeff ); + + WALBERLA_LOG_INFO_ON_ROOT("Coupling properties:" ); + WALBERLA_LOG_INFO_ON_ROOT(" - number of RPD sub cycles = " << numRPDSubCycles ); + WALBERLA_LOG_INFO_ON_ROOT(" - lubrication correction = " << (useLubricationCorrection ? "yes" : "no") ); + if( useLubricationCorrection ) + { + WALBERLA_LOG_INFO_ON_ROOT(" - lubrication correction cut off normal = " << lubricationCutOffDistanceNormal ); + WALBERLA_LOG_INFO_ON_ROOT(" - lubrication correction cut off tangential translational = " << lubricationCutOffDistanceTangentialTranslational ); + WALBERLA_LOG_INFO_ON_ROOT(" - lubrication correction cut off tangential rotational = " << lubricationCutOffDistanceTangentialRotational ); + WALBERLA_LOG_INFO_ON_ROOT(" - lubrication correction minimal gap size non dim = " << lubricationMinimalGapSizeNonDim << " ( = " << lubricationMinimalGapSizeNonDim * diameter * real_t(0.5) << " cells )"); + } + WALBERLA_LOG_INFO_ON_ROOT(" - average forces over two LBM time steps = " << (averageForceTorqueOverTwoTimeSteps ? "yes" : "no") ); + WALBERLA_LOG_INFO_ON_ROOT(" - conserve momentum = " << (conserveMomentum ? "yes" : "no") ); + WALBERLA_LOG_INFO_ON_ROOT(" - reconstructor = " << reconstructorType ); + + /////////////////////////// + // BLOCK STRUCTURE SETUP // + /////////////////////////// + + + Vector3<uint_t> cellsPerBlockPerDirection( domainSize[0] / numberOfBlocksPerDirection[0], + domainSize[1] / numberOfBlocksPerDirection[1], + domainSize[2] / numberOfBlocksPerDirection[2] ); + for( uint_t i = 0; i < 3; ++i ) { + WALBERLA_CHECK_EQUAL(cellsPerBlockPerDirection[i] * numberOfBlocksPerDirection[i], domainSize[i], + "Unmatching domain decomposition in direction " << i << "!"); + } + + auto blocks = blockforest::createUniformBlockGrid( numberOfBlocksPerDirection[0], numberOfBlocksPerDirection[1], numberOfBlocksPerDirection[2], + cellsPerBlockPerDirection[0], cellsPerBlockPerDirection[1], cellsPerBlockPerDirection[2], dx, + 0, false, false, + true, true, false, //periodicity + false ); + + WALBERLA_LOG_INFO_ON_ROOT("Domain decomposition:"); + WALBERLA_LOG_INFO_ON_ROOT(" - blocks per direction = " << numberOfBlocksPerDirection ); + WALBERLA_LOG_INFO_ON_ROOT(" - cells per block = " << cellsPerBlockPerDirection ); + + //write domain decomposition to file + if( vtkIOFreq > 0 ) + { + vtk::writeDomainDecomposition( blocks, "initial_domain_decomposition", baseFolder ); + } + + + ////////////////// + // RPD COUPLING // + ////////////////// + + auto rpdDomain = std::make_shared<mesa_pd::domain::BlockForestDomain>(blocks->getBlockForestPointer()); + + //init data structures + auto ps = walberla::make_shared<mesa_pd::data::ParticleStorage>(1); + auto ss = walberla::make_shared<mesa_pd::data::ShapeStorage>(); + using ParticleAccessor_T = mesa_pd::data::ParticleAccessorWithShape; + auto accessor = walberla::make_shared<ParticleAccessor_T >(ps, ss); + + // bounding planes + createPlaneSetup(ps,ss,blocks->getDomain(), applyOutflowBCAtTop); + + // create sphere and store Uid + Vector3<real_t> initialPosition( real_t(0.5) * real_c(domainSize[0]), real_t(0.5) * real_c(domainSize[1]), initialSpherePosition ); + auto sphereShape = ss->create<mesa_pd::data::Sphere>( diameter * real_t(0.5) ); + ss->shapes[sphereShape]->updateMassAndInertia(densitySphere); + + walberla::id_t sphereUid = 0; + // create sphere + if (rpdDomain->isContainedInProcessSubdomain( uint_c(mpi::MPIManager::instance()->rank()), initialPosition )) + { + mesa_pd::data::Particle&& p = *ps->create(); + p.setPosition(initialPosition); + p.setInteractionRadius(diameter * real_t(0.5)); + p.setOwner(mpi::MPIManager::instance()->rank()); + p.setShapeID(sphereShape); + p.setType(0); + sphereUid = p.getUid(); + } + mpi::allReduceInplace(sphereUid, mpi::SUM); + + + /////////////////////// + // ADD DATA TO BLOCKS // + //////////////////////// + + // add omega bulk field + BlockDataID omegaBulkFieldID = field::addToStorage<ScalarField_T>( blocks, "omega bulk field", omegaBulk, field::fzyx, uint_t(0) ); + + // create the lattice model + + // TRT + //LatticeModel_T latticeModel = LatticeModel_T( lbm::collision_model::TRT::constructWithMagicNumber( real_t(1) / relaxationTime ) ); + + // generated MRT +#ifdef USE_TRT_LIKE_LATTICE_MODEL + WALBERLA_LOG_INFO_ON_ROOT("Using TRT-like lattice model!"); + real_t lambda_e = lbm::collision_model::TRT::lambda_e( omega ); + real_t lambda_d = lbm::collision_model::TRT::lambda_d( omega, magicNumber ); + //LatticeModel_T latticeModel = LatticeModel_T(omegaBulk, lambda_d, lambda_e); + LatticeModel_T latticeModel = LatticeModel_T(omegaBulkFieldID, lambda_d, lambda_e); + //LatticeModel_T latticeModel = LatticeModel_T(lbm::collision_model::D3Q19MRT( omegaBulk, omegaBulk, lambda_d, lambda_e, lambda_e, lambda_d )); +#else + WALBERLA_LOG_INFO_ON_ROOT("Using different lattice model!"); + // generated model with a single omega + LatticeModel_T latticeModel = LatticeModel_T(omega); +#endif + + // add PDF field + BlockDataID pdfFieldID = lbm::addPdfFieldToStorage< LatticeModel_T >( blocks, "pdf field", latticeModel, + Vector3< real_t >( real_t(0) ), real_t(1), + uint_t(1), field::fzyx ); + + // add flag field + BlockDataID flagFieldID = field::addFlagFieldToStorage<FlagField_T>( blocks, "flag field" ); + + // add particle field + BlockDataID particleFieldID = field::addToStorage<lbm_mesapd_coupling::ParticleField_T>( blocks, "particle field", accessor->getInvalidUid(), field::fzyx, FieldGhostLayers ); + + // add boundary handling + using BoundaryHandling_T = MyBoundaryHandling<ParticleAccessor_T>::Type; + BlockDataID boundaryHandlingID = blocks->addStructuredBlockData< BoundaryHandling_T >(MyBoundaryHandling<ParticleAccessor_T>( flagFieldID, pdfFieldID, particleFieldID, accessor, applyOutflowBCAtTop), "boundary handling" ); + + // set up RPD functionality + std::function<void(void)> syncCall = [&ps,&rpdDomain](){ + const real_t overlap = real_t( 1.5 ); + mesa_pd::mpi::SyncNextNeighbors syncNextNeighborFunc; + syncNextNeighborFunc(*ps, *rpdDomain, overlap); + }; + + syncCall(); + + real_t timeStepSizeRPD = real_t(1)/real_t(numRPDSubCycles); + mesa_pd::kernel::ExplicitEulerWithShape explEulerIntegrator(timeStepSizeRPD); + mesa_pd::kernel::VelocityVerletWithShapePreForceUpdate vvIntegratorPreForce(timeStepSizeRPD); + mesa_pd::kernel::VelocityVerletWithShapePostForceUpdate vvIntegratorPostForce(timeStepSizeRPD); + + // linear model + mesa_pd::kernel::LinearSpringDashpot linearCollisionResponse(1); + linearCollisionResponse.setStiffnessN(0,0,stiffnessN); + linearCollisionResponse.setDampingN(0,0,dampingN); + linearCollisionResponse.setStiffnessT(0,0,stiffnessT); + linearCollisionResponse.setDampingT(0,0,dampingT); + //linearCollisionResponse.setFrictionCoefficientStatic(0,0,frictionCoeff); // not used in this test case + linearCollisionResponse.setFrictionCoefficientDynamic(0,0,frictionCoeff); + + // nonlinear model for ACTM + + + mesa_pd::mpi::ReduceProperty reduceProperty; + + mesa_pd::mpi::ReduceContactHistory reduceAndSwapContactHistory; + + // set up coupling functionality + Vector3<real_t> gravitationalForce = (densitySphere - densityFluid) * sphereVolume * gravitationalAccelerationVec; + lbm_mesapd_coupling::AddForceOnParticlesKernel addGravitationalForce(gravitationalForce); + lbm_mesapd_coupling::ResetHydrodynamicForceTorqueKernel resetHydrodynamicForceTorque; + lbm_mesapd_coupling::AverageHydrodynamicForceTorqueKernel averageHydrodynamicForceTorque; + lbm_mesapd_coupling::LubricationCorrectionKernel lubricationCorrectionKernel(viscosity, [lubricationMinimalGapSizeNonDim](real_t r){ return lubricationMinimalGapSizeNonDim * r;}, lubricationCutOffDistanceNormal, + lubricationCutOffDistanceTangentialTranslational, lubricationCutOffDistanceTangentialRotational ); + lbm_mesapd_coupling::ParticleMappingKernel<BoundaryHandling_T> particleMappingKernel(blocks, boundaryHandlingID); + lbm_mesapd_coupling::MovingParticleMappingKernel<BoundaryHandling_T> movingParticleMappingKernel(blocks, boundaryHandlingID, particleFieldID); + + /////////////// + // TIME LOOP // + /////////////// + + // map planes into the LBM simulation -> act as no-slip boundaries + ps->forEachParticle(false, lbm_mesapd_coupling::GlobalParticlesSelector(), *accessor, particleMappingKernel, *accessor, NoSlip_Flag); + + // map particles into the LBM simulation + ps->forEachParticle(false, lbm_mesapd_coupling::RegularParticlesSelector(), *accessor, movingParticleMappingKernel, *accessor, MO_Flag); + + // setup of the LBM communication for synchronizing the pdf field between neighboring blocks + blockforest::communication::UniformBufferedScheme< Stencil_T > optimizedPDFCommunicationScheme( blocks ); + optimizedPDFCommunicationScheme.addPackInfo( make_shared< lbm::PdfFieldPackInfo< LatticeModel_T > >( pdfFieldID ) ); // optimized sync + + blockforest::communication::UniformBufferedScheme< Stencil_T > fullPDFCommunicationScheme( blocks ); + fullPDFCommunicationScheme.addPackInfo( make_shared< field::communication::PackInfo< PdfField_T > >( pdfFieldID ) ); // full sync + + // create the timeloop + const uint_t timesteps = uint_c( 1000000000 ); // just some large value + + SweepTimeloop timeloop( blocks->getBlockStorage(), timesteps ); + + timeloop.addFuncBeforeTimeStep( RemainingTimeLogger( timeloop.getNrOfTimeSteps() ), "Remaining Time Logger" ); + + // vtk output + if( vtkIOFreq != uint_t(0) ) + { + // sphere + auto particleVtkOutput = make_shared<mesa_pd::vtk::ParticleVtkOutput>(ps); + particleVtkOutput->addOutput<mesa_pd::data::SelectParticleLinearVelocity>("velocity"); + particleVtkOutput->addOutput<mesa_pd::data::SelectParticleAngularVelocity>("angular velocity"); + particleVtkOutput->setParticleSelector( [sphereShape](const mesa_pd::data::ParticleStorage::iterator& pIt) {return pIt->getShapeID() == sphereShape;} ); //limit output to sphere + auto particleVtkWriter = vtk::createVTKOutput_PointData(particleVtkOutput, "Particles", vtkIOFreq, baseFolder, "simulation_step"); + timeloop.addFuncBeforeTimeStep( vtk::writeFiles( particleVtkWriter ), "VTK (sphere data)" ); + + // flag field (written only once in the first time step, ghost layers are also written) + /* + auto flagFieldVTK = vtk::createVTKOutput_BlockData( blocks, "flag_field", timesteps, FieldGhostLayers, false, baseFolder ); + flagFieldVTK->addCellDataWriter( make_shared< field::VTKWriter< FlagField_T > >( flagFieldID, "FlagField" ) ); + timeloop.addFuncBeforeTimeStep( vtk::writeFiles( flagFieldVTK ), "VTK (flag field data)" ); + */ + + // pdf field, as slice + auto pdfFieldVTK = vtk::createVTKOutput_BlockData( blocks, "fluid_field", vtkIOFreq, 0, false, baseFolder ); + + pdfFieldVTK->addBeforeFunction( fullPDFCommunicationScheme ); + + AABB sliceAABB( real_t(0), real_c(domainSize[1])*real_t(0.5)-real_t(1), real_t(0), + real_c(domainSize[0]), real_c(domainSize[1])*real_t(0.5)+real_t(1), real_c(domainSize[2]) ); + vtk::AABBCellFilter aabbSliceFilter( sliceAABB ); + + field::FlagFieldCellFilter< FlagField_T > fluidFilter( flagFieldID ); + fluidFilter.addFlag( Fluid_Flag ); + + vtk::ChainedFilter combinedSliceFilter; + combinedSliceFilter.addFilter( fluidFilter ); + combinedSliceFilter.addFilter( aabbSliceFilter ); + + pdfFieldVTK->addCellInclusionFilter( combinedSliceFilter ); + + pdfFieldVTK->addCellDataWriter( make_shared< lbm::VelocityVTKWriter< LatticeModel_T, float > >( pdfFieldID, "VelocityFromPDF" ) ); + pdfFieldVTK->addCellDataWriter( make_shared< lbm::DensityVTKWriter < LatticeModel_T, float > >( pdfFieldID, "DensityFromPDF" ) ); + + timeloop.addFuncBeforeTimeStep( vtk::writeFiles( pdfFieldVTK ), "VTK (fluid field data)" ); + + // omega bulk field + //timeloop.addFuncBeforeTimeStep( field::createVTKOutput<ScalarField_T, float>( omegaBulkFieldID, *blocks, "omega_bulk_field", vtkIOFreq, uint_t(0), false, baseFolder ), "VTK (omega bulk field)" ); + } + + // sweep for updating the particle mapping into the LBM simulation + timeloop.add() << Sweep( lbm_mesapd_coupling::makeMovingParticleMapping<PdfField_T, BoundaryHandling_T>(blocks, pdfFieldID, boundaryHandlingID, particleFieldID, accessor, MO_Flag, FormerMO_Flag, lbm_mesapd_coupling::RegularParticlesSelector(), conserveMomentum), "Particle Mapping" ); + + // sweep for restoring PDFs in cells previously occupied by particles + if( reconstructorType == "EAN" ) + { + auto sphereNormalExtrapolationDirectionFinder = make_shared<lbm_mesapd_coupling::SphereNormalExtrapolationDirectionFinder>(blocks); + auto equilibriumAndNonEquilibriumSphereNormalReconstructor = lbm_mesapd_coupling::makeEquilibriumAndNonEquilibriumReconstructor<BoundaryHandling_T>(blocks, boundaryHandlingID, sphereNormalExtrapolationDirectionFinder, uint_t(3), true); + timeloop.add() << BeforeFunction( fullPDFCommunicationScheme, "LBM Communication" ) + << Sweep( makeSharedSweep(lbm_mesapd_coupling::makePdfReconstructionManager<PdfField_T,BoundaryHandling_T>(blocks, pdfFieldID, boundaryHandlingID, particleFieldID, accessor, FormerMO_Flag, Fluid_Flag, equilibriumAndNonEquilibriumSphereNormalReconstructor, conserveMomentum)), "PDF Restore" ); + }else if( reconstructorType == "Grad" ) + { + bool recomputeTargetDensity = false; + auto gradReconstructor = lbm_mesapd_coupling::makeGradsMomentApproximationReconstructor<BoundaryHandling_T>(blocks, boundaryHandlingID, omega, recomputeTargetDensity, true, true); + + timeloop.add() << BeforeFunction( fullPDFCommunicationScheme, "PDF Communication" ) + << Sweep( makeSharedSweep(lbm_mesapd_coupling::makePdfReconstructionManager<PdfField_T,BoundaryHandling_T>(blocks, pdfFieldID, boundaryHandlingID, particleFieldID, accessor, FormerMO_Flag, Fluid_Flag, gradReconstructor, conserveMomentum) ), "PDF Restore" ); + }else if( reconstructorType == "Eq" ) + { + timeloop.add() << Sweep( makeSharedSweep(lbm_mesapd_coupling::makePdfReconstructionManager<PdfField_T,BoundaryHandling_T>(blocks, pdfFieldID, boundaryHandlingID, particleFieldID, accessor, FormerMO_Flag, Fluid_Flag, conserveMomentum)), "PDF Restore" ); + }else if( reconstructorType == "Ext") + { + auto sphereNormalExtrapolationDirectionFinder = make_shared<lbm_mesapd_coupling::SphereNormalExtrapolationDirectionFinder>(blocks); +#ifdef USE_TRT_LIKE_LATTICE_MODEL + auto extrapolationReconstructor = lbm_mesapd_coupling::makeExtrapolationReconstructor<BoundaryHandling_T, lbm_mesapd_coupling::SphereNormalExtrapolationDirectionFinder, true>(blocks, boundaryHandlingID, sphereNormalExtrapolationDirectionFinder, uint_t(3), true); +#else + auto extrapolationReconstructor = lbm_mesapd_coupling::makeExtrapolationReconstructor<BoundaryHandling_T, lbm_mesapd_coupling::SphereNormalExtrapolationDirectionFinder, false>(blocks, boundaryHandlingID, sphereNormalExtrapolationDirectionFinder, uint_t(3), true); +#endif + timeloop.add() << BeforeFunction( fullPDFCommunicationScheme, "LBM Communication" ) + << Sweep( makeSharedSweep(lbm_mesapd_coupling::makePdfReconstructionManager<PdfField_T,BoundaryHandling_T>(blocks, pdfFieldID, boundaryHandlingID, particleFieldID, accessor, FormerMO_Flag, Fluid_Flag, extrapolationReconstructor, conserveMomentum)), "PDF Restore" ); + } else + { + WALBERLA_ABORT("Reconstructor Type " << reconstructorType << " not implemented!"); + } + + // update bulk omega in all cells to adapt to changed particle position + if(useOmegaBulkAdaption) + { + using OmegaBulkAdapter_T = lbm_mesapd_coupling::OmegaBulkAdapter<ParticleAccessor_T, lbm_mesapd_coupling::RegularParticlesSelector>; + real_t defaultOmegaBulk = lbm_mesapd_coupling::omegaBulkFromOmega(omega, real_t(1)); + real_t adaptionLayerSize = real_t(2); + shared_ptr<OmegaBulkAdapter_T> omegaBulkAdapter = make_shared<OmegaBulkAdapter_T>(blocks, omegaBulkFieldID, accessor, defaultOmegaBulk, omegaBulk, adaptionLayerSize, lbm_mesapd_coupling::RegularParticlesSelector()); + + // initial adaption + for (auto blockIt = blocks->begin(); blockIt != blocks->end(); ++blockIt) { + (*omegaBulkAdapter)(blockIt.get()); + } + + timeloop.add() << Sweep( makeSharedSweep(omegaBulkAdapter), "Omega Bulk Adapter"); + } + + + // add LBM communication function and boundary handling sweep (does the hydro force calculations and the no-slip treatment) + timeloop.add() << BeforeFunction( optimizedPDFCommunicationScheme, "LBM Communication" ) + << Sweep( BoundaryHandling_T::getBlockSweep( boundaryHandlingID ), "Boundary Handling" ); + + // stream + collide LBM step + // generated LBM sweep + timeloop.add() << Sweep( LatticeModel_T::Sweep( pdfFieldID ), "LB stream & collide" ); + // walberla sweeps: + //timeloop.add() << Sweep( makeSharedSweep( lbm::makeCellwiseSweep< LatticeModel_T, FlagField_T >( pdfFieldID, flagFieldID, Fluid_Flag ) ), "cell-wise LB sweep" ); + + // evaluation functionality + std::string loggingFileName( baseFolder + "/LoggingObliqueWetCollision.txt"); + std::string forceLoggingFileName( baseFolder + "/ForceLoggingObliqueWetCollision.txt"); + if( fileIO ) + { + WALBERLA_LOG_INFO_ON_ROOT(" - writing logging output to file \"" << loggingFileName << "\""); + WALBERLA_LOG_INFO_ON_ROOT(" - writing force logging output to file \"" << forceLoggingFileName << "\""); + } + SpherePropertyLogger<ParticleAccessor_T> logger( accessor, sphereUid, loggingFileName, forceLoggingFileName, fileIO, diameter, uIn, impactRatio, numRPDSubCycles, gravitationalForce[0], gravitationalForce[2] ); + + + //////////////////////// + // EXECUTE SIMULATION // + //////////////////////// + + WcTimingPool timeloopTiming; + + // evaluation quantities + uint_t numBounces = uint_t(0); + uint_t tImpact = uint_t(0); + + real_t curVel(real_t(0)), oldVel(real_t(0)); + real_t maxSettlingVel = real_t(0); + + real_t minGapSize(real_t(0)); + + real_t actualSt(real_t(0)); + real_t actualRe(real_t(0)); + + WALBERLA_LOG_INFO_ON_ROOT("Running for maximum of " << timesteps << " timesteps!"); + + const bool useOpenMP = false; + + uint_t averagingSampleSize = uint_c(real_t(1) / uIn); + std::vector<Vector3<real_t>> forceValues(averagingSampleSize, Vector3<real_t>(real_t(0))); + + // generally: z: normal direction, x: tangential direction + + uint_t endTimestep = timesteps; // will be adapted after bounce + // time loop + for (uint_t i = 0; i < endTimestep; ++i ) + { + // perform a single simulation step -> this contains LBM and setting of the hydrodynamic interactions + timeloop.singleStep( timeloopTiming ); + + + if( averageForceTorqueOverTwoTimeSteps && i!= 0) + { + ps->forEachParticle(useOpenMP, mesa_pd::kernel::SelectAll(), *accessor, averageHydrodynamicForceTorque, *accessor ); + } + + Vector3<real_t> hydForce(real_t(0)); + Vector3<real_t> lubForce(real_t(0)); + Vector3<real_t> collisionForce(real_t(0)); + + Vector3<real_t> hydTorque(real_t(0)); + Vector3<real_t> lubTorque(real_t(0)); + Vector3<real_t> collisionTorque(real_t(0)); + + for(auto subCycle = uint_t(0); subCycle < numRPDSubCycles; ++subCycle ) + { + + timeloopTiming["RPD"].start(); + + if( useVelocityVerlet ) + { + + Vector3<real_t> oldForce; + Vector3<real_t> oldTorque; + + if(artificiallyAccelerateSphere) + { + // since the pre-force step of VV updates the position based on velocity and old force, we set oldForce to zero here for this step while artificially accelerating + // to not perturb the step after which artificial acceleration is switched off (which requires valid oldForce values then) we store the old force and then re-apply it + auto idx = accessor->uidToIdx(sphereUid); + if(idx != accessor->getInvalidIdx()) + { + oldForce = accessor->getOldForce(idx); + accessor->setOldForce(idx, Vector3<real_t>(real_t(0))); + + oldTorque = accessor->getOldTorque(idx); + accessor->setOldTorque(idx, Vector3<real_t>(real_t(0))); + } + } + + ps->forEachParticle(useOpenMP, mesa_pd::kernel::SelectLocal(), *accessor, vvIntegratorPreForce, *accessor); + syncCall(); + + if(artificiallyAccelerateSphere) + { + // re-apply old force + auto idx = accessor->uidToIdx(sphereUid); + if(idx != accessor->getInvalidIdx()) + { + accessor->setOldForce(idx, oldForce); + accessor->setOldTorque(idx, oldTorque); + } + } + } + + // lubrication correction + ps->forEachParticlePairHalf(useOpenMP, mesa_pd::kernel::ExcludeInfiniteInfinite(), *accessor, + [&lubricationCorrectionKernel,&rpdDomain](const size_t idx1, const size_t idx2, auto& ac) + { + mesa_pd::collision_detection::AnalyticContactDetection acd; + acd.getContactThreshold() = lubricationCorrectionKernel.getNormalCutOffDistance(); + mesa_pd::kernel::DoubleCast double_cast; + mesa_pd::mpi::ContactFilter contact_filter; + if (double_cast(idx1, idx2, ac, acd, ac )) + { + if (contact_filter(acd.getIdx1(), acd.getIdx2(), ac, acd.getContactPoint(), *rpdDomain)) + { + double_cast(acd.getIdx1(), acd.getIdx2(), ac, lubricationCorrectionKernel, ac, acd.getContactNormal(), acd.getPenetrationDepth()); + } + } + }, + *accessor ); + + lubForce = getForce(sphereUid,*accessor); + lubTorque = getTorque(sphereUid,*accessor); + + // one could add linked cells here + + // collision response + ps->forEachParticlePairHalf(useOpenMP, mesa_pd::kernel::ExcludeInfiniteInfinite(), *accessor, + [&linearCollisionResponse, &rpdDomain, timeStepSizeRPD] + (const size_t idx1, const size_t idx2, auto& ac) + { + mesa_pd::collision_detection::AnalyticContactDetection acd; + mesa_pd::kernel::DoubleCast double_cast; + mesa_pd::mpi::ContactFilter contact_filter; + if (double_cast(idx1, idx2, ac, acd, ac )) + { + if (contact_filter(acd.getIdx1(), acd.getIdx2(), ac, acd.getContactPoint(), *rpdDomain)) + { + linearCollisionResponse(acd.getIdx1(), acd.getIdx2(), ac, acd.getContactPoint(), acd.getContactNormal(), acd.getPenetrationDepth(), timeStepSizeRPD); + } + } + }, + *accessor ); + + collisionForce = getForce(sphereUid,*accessor) - lubForce; + collisionTorque = getTorque(sphereUid,*accessor) - lubTorque; + + reduceAndSwapContactHistory(*ps); + + // add hydrodynamic force + lbm_mesapd_coupling::AddHydrodynamicInteractionKernel addHydrodynamicInteraction; + ps->forEachParticle(useOpenMP, lbm_mesapd_coupling::RegularParticlesSelector(), *accessor, addHydrodynamicInteraction, *accessor ); + + hydForce = getForce(sphereUid,*accessor) - lubForce - collisionForce; + WALBERLA_ASSERT(!std::isnan(hydForce[0]) && !std::isnan(hydForce[1]) && !std::isnan(hydForce[2]), "Found nan value in hydrodynamic force = " << hydForce); + hydTorque = getTorque(sphereUid,*accessor) - lubTorque - collisionTorque; + + ps->forEachParticle( useOpenMP, mesa_pd::kernel::SelectLocal(), *accessor, addGravitationalForce, *accessor ); + + reduceProperty.operator()<mesa_pd::ForceTorqueNotification>(*ps); + + // integration + if( useVelocityVerlet ) ps->forEachParticle( useOpenMP, mesa_pd::kernel::SelectLocal(), *accessor, vvIntegratorPostForce, *accessor); + else ps->forEachParticle( useOpenMP, mesa_pd::kernel::SelectLocal(), *accessor, explEulerIntegrator, *accessor); + + syncCall(); + + if( artificiallyAccelerateSphere ) + { + lbm_mesapd_coupling::RegularParticlesSelector sphereSelector; + real_t newSphereVel = uIn * (exp(-accelerationFactor * real_t(i) / responseTime ) - real_t(1)); + ps->forEachParticle(useOpenMP, sphereSelector, *accessor, + [newSphereVel,impactAngle](const size_t idx, ParticleAccessor_T& ac){ + ac.setLinearVelocity(idx, Vector3<real_t>( -std::sin(impactAngle), real_t(0), std::cos(impactAngle) ) * newSphereVel); + ac.setAngularVelocity(idx, Vector3<real_t>(real_t(0)));}, + *accessor); + } + + timeloopTiming["RPD"].end(); + + // logging + timeloopTiming["Logging"].start(); + logger(i, subCycle, hydForce, lubForce, collisionForce, hydTorque, lubTorque, collisionTorque); + timeloopTiming["Logging"].end(); + + } + + // store hyd force to average + forceValues[i % averagingSampleSize] = hydForce; + + ps->forEachParticle( useOpenMP, mesa_pd::kernel::SelectAll(), *accessor, resetHydrodynamicForceTorque, *accessor ); + + + // check for termination + oldVel = curVel; + curVel = logger.getSettlingVelocity(); + + maxSettlingVel = std::min(maxSettlingVel, curVel); + minGapSize = std::min(minGapSize, logger.getGapSize()); + + // detect the bounce + if( oldVel < real_t(0) && curVel > real_t(0) && logger.getGapSize() < real_t(1)) + { + + ++numBounces; + + actualSt = densityRatio * std::abs(maxSettlingVel) * diameter / ( real_t(9) * viscosity ); + actualRe = std::abs(maxSettlingVel) * diameter / viscosity; + + WALBERLA_LOG_INFO_ON_ROOT("Detected bounce with max settling velocity " << maxSettlingVel << " -> St = " << actualSt ); + + // end simulation after one non-dim timestep + uint_t remainingTimeSteps = uint_t(real_t(1) * diameter / std::abs(maxSettlingVel)); + endTimestep = i + remainingTimeSteps; + WALBERLA_LOG_INFO_ON_ROOT("Will terminate simulation after " << remainingTimeSteps << " time steps, i.e. at time step " << endTimestep); + } + + // impact times are measured when the contact between sphere and wall is broken up again + if( tImpact == uint_t(0) && numBounces == uint_t(1) && logger.getGapSize() > real_t(0) ) + { + tImpact = i; + WALBERLA_LOG_INFO_ON_ROOT("Detected impact time at time step " << tImpact); + } + + // check if sphere is close to bottom plane + if( logger.getGapSize() < real_t(1) * diameter && artificiallyAccelerateSphere) { + WALBERLA_LOG_INFO_ON_ROOT("Switching off acceleration!"); + artificiallyAccelerateSphere = false; + + if(applyArtificialGravityAfterAccelerating) + { + // to avoid a too large deceleration due to the missing gravitational force, we apply the averaged hydrodynamic force + // (that would have been balanced by the gravitational force) as a kind of artificial gravity + Vector3<real_t> artificialGravitationalForce = -std::accumulate(forceValues.begin(), forceValues.end(), Vector3<real_t>(real_t(0))) / real_t(averagingSampleSize); + WALBERLA_LOG_INFO_ON_ROOT("Applying artificial gravitational and buoyancy force of " << artificialGravitationalForce); + real_t actualGravitationalAcceleration = -artificialGravitationalForce[2] / ( (densitySphere - densityFluid) * sphereVolume ); + WALBERLA_LOG_INFO_ON_ROOT("This would correspond to a gravitational acceleration of g = " << actualGravitationalAcceleration << ", g_SI = " << actualGravitationalAcceleration * dx_SI / (dt_SI * dt_SI) ); + addGravitationalForce = lbm_mesapd_coupling::AddForceOnParticlesKernel(artificialGravitationalForce); + } + } + } + + WALBERLA_LOG_INFO_ON_ROOT("Terminating simulation"); + WALBERLA_LOG_INFO_ON_ROOT("Maximum settling velocities: " << maxSettlingVel ); + + std::string summaryFile(baseFolder + "/Summary.txt"); + WALBERLA_LOG_INFO_ON_ROOT("Writing summary file " << summaryFile); + WALBERLA_ROOT_SECTION() + { + std::ofstream file; + file.open( summaryFile.c_str()); + + file << "waLBerla Revision = " << std::string(WALBERLA_GIT_SHA1).substr(0,8) << "\n"; + file << "\nInput parameters:\n"; + file << "case: " << caseNumber << "\n"; + file << "fluid: " << fluid << "\n"; + file << "material: " << material << "\n"; + file << "variant: " << simulationVariant << "\n"; + file << "LBM parameters:\n"; + file << " - magic number = " << magicNumber << "\n"; + file << " - bulk viscosity rate factor = " << omegaBulk << "\n"; + file << " - use omega bulk adaption = " << useOmegaBulkAdaption << " (layer size = 2)\n"; + file << "Collision parameters:\n"; + file << " - subCycles = " << numRPDSubCycles << "\n"; + file << " - collision time (Tc) = " << collisionTime << "\n"; + file << "use lubrication correction = " << useLubricationCorrection << "\n"; + file << " - minimum gap size non dim = " << lubricationMinimalGapSizeNonDim << "\n"; + file << " - lubrication correction cut off normal = " << lubricationCutOffDistanceNormal << "\n"; + file << " - lubrication correction cut off tangential translational = " << lubricationCutOffDistanceTangentialTranslational << "\n"; + file << " - lubrication correction cut off tangential rotational = " << lubricationCutOffDistanceTangentialRotational << "\n"; + file << "reconstructor type " << reconstructorType << "\n"; + file << "apply outflow BC at top = " << applyOutflowBCAtTop << "\n"; + + file << "\nOutput quantities:\n"; + file << "actual St = " << actualSt << "\n"; + file << "actual Re = " << actualRe << "\n"; + file << "impact times = " << tImpact << "\n"; + file << "settling velocity = " << maxSettlingVel << "\n"; + file << "maximal overlap = " << std::abs(minGapSize) << " (" << std::abs(minGapSize)/diameter << "%)\n"; + + file.close(); + } + + + + + timeloopTiming.logResultOnRoot(); + + return EXIT_SUCCESS; +} + +} // namespace oblique_wet_collision + +int main( int argc, char **argv ){ + oblique_wet_collision::main(argc, argv); +} diff --git a/apps/benchmarks/FluidParticleCoupling/SettlingSphereInBox.cpp b/apps/benchmarks/FluidParticleCoupling/SettlingSphereInBox.cpp new file mode 100644 index 0000000000000000000000000000000000000000..6fa7822310fd770d223ec3beeefd3279b7aca029 --- /dev/null +++ b/apps/benchmarks/FluidParticleCoupling/SettlingSphereInBox.cpp @@ -0,0 +1,911 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla is free software: you can +// redistribute it and/or modify it under the terms of the GNU General Public +// License as published by the Free Software Foundation, either version 3 of +// the License, or (at your option) any later version. +// +// waLBerla is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +// for more details. +// +// You should have received a copy of the GNU General Public License along +// with waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file SettlingSphereInBox.cpp +//! \ingroup lbm_mesapd_coupling +//! \author Christoph Rettinger <christoph.rettinger@fau.de> +// +//====================================================================================================================== + +#include "blockforest/Initialization.h" +#include "blockforest/communication/UniformBufferedScheme.h" + +#include "boundary/all.h" + +#include "core/DataTypes.h" +#include "core/Environment.h" +#include "core/SharedFunctor.h" +#include "core/debug/Debug.h" +#include "core/debug/TestSubsystem.h" +#include "core/math/all.h" +#include "core/timing/RemainingTimeLogger.h" +#include "core/logging/all.h" + +#include "domain_decomposition/SharedSweep.h" + +#include "field/AddToStorage.h" +#include "field/StabilityChecker.h" +#include "field/communication/PackInfo.h" + +#include "lbm/boundary/all.h" +#include "lbm/communication/PdfFieldPackInfo.h" +#include "lbm/field/AddToStorage.h" +#include "lbm/field/PdfField.h" +#include "lbm/lattice_model/D3Q19.h" +#include "lbm/sweeps/CellwiseSweep.h" +#include "lbm/sweeps/SweepWrappers.h" + +#include "lbm_mesapd_coupling/mapping/ParticleMapping.h" +#include "lbm_mesapd_coupling/momentum_exchange_method/MovingParticleMapping.h" +#include "lbm_mesapd_coupling/momentum_exchange_method/boundary/CurvedLinear.h" +#include "lbm_mesapd_coupling/momentum_exchange_method/reconstruction/Reconstructor.h" +#include "lbm_mesapd_coupling/momentum_exchange_method/reconstruction/ExtrapolationDirectionFinder.h" +#include "lbm_mesapd_coupling/momentum_exchange_method/reconstruction/PdfReconstructionManager.h" +#include "lbm_mesapd_coupling/utility/AddForceOnParticlesKernel.h" +#include "lbm_mesapd_coupling/utility/ParticleSelector.h" +#include "lbm_mesapd_coupling/DataTypes.h" +#include "lbm_mesapd_coupling/utility/AverageHydrodynamicForceTorqueKernel.h" +#include "lbm_mesapd_coupling/utility/AddHydrodynamicInteractionKernel.h" +#include "lbm_mesapd_coupling/utility/ResetHydrodynamicForceTorqueKernel.h" +#include "lbm_mesapd_coupling/utility/LubricationCorrectionKernel.h" +#include "lbm_mesapd_coupling/utility/OmegaBulkAdaption.h" + +#include "mesa_pd/collision_detection/AnalyticContactDetection.h" +#include "mesa_pd/data/ParticleAccessorWithShape.h" +#include "mesa_pd/data/ParticleStorage.h" +#include "mesa_pd/data/ShapeStorage.h" +#include "mesa_pd/data/DataTypes.h" +#include "mesa_pd/data/shape/HalfSpace.h" +#include "mesa_pd/data/shape/Sphere.h" +#include "mesa_pd/domain/BlockForestDomain.h" +#include "mesa_pd/kernel/DoubleCast.h" +#include "mesa_pd/kernel/ExplicitEulerWithShape.h" +#include "mesa_pd/kernel/ParticleSelector.h" +#include "mesa_pd/kernel/SpringDashpot.h" +#include "mesa_pd/kernel/VelocityVerletWithShape.h" +#include "mesa_pd/mpi/SyncNextNeighbors.h" +#include "mesa_pd/mpi/ReduceProperty.h" +#include "mesa_pd/mpi/ContactFilter.h" +#include "mesa_pd/mpi/notifications/ForceTorqueNotification.h" +#include "mesa_pd/vtk/ParticleVtkOutput.h" + +#include "timeloop/SweepTimeloop.h" + +#include "vtk/all.h" +#include "field/vtk/all.h" +#include "lbm/vtk/all.h" + +#include "GeneratedLBM.h" + +#include <functional> + +#define USE_TRT_LIKE_LATTICE_MODEL + +namespace settling_sphere_in_box +{ + +/////////// +// USING // +/////////// + +using namespace walberla; +using walberla::uint_t; + +using LatticeModel_T = lbm::GeneratedLBM; +//using LatticeModel_T = lbm::D3Q19< lbm::collision_model::D3Q19MRT>; + +using Stencil_T = LatticeModel_T::Stencil; +using PdfField_T = lbm::PdfField<LatticeModel_T>; + +using flag_t = walberla::uint8_t; +using FlagField_T = FlagField<flag_t>; + +using ScalarField_T = GhostLayerField< real_t, 1>; + +const uint_t FieldGhostLayers = 1; + +/////////// +// FLAGS // +/////////// + +const FlagUID Fluid_Flag( "fluid" ); +const FlagUID NoSlip_Flag( "no slip" ); +const FlagUID MO_Flag( "moving obstacle" ); +const FlagUID FormerMO_Flag( "former moving obstacle" ); + +///////////////////////////////////// +// BOUNDARY HANDLING CUSTOMIZATION // +///////////////////////////////////// +template <typename ParticleAccessor_T> +class MyBoundaryHandling +{ +public: + + using NoSlip_T = lbm::NoSlip< LatticeModel_T, flag_t >; + using MO_T = lbm_mesapd_coupling::CurvedLinear< LatticeModel_T, FlagField_T, ParticleAccessor_T >; + using Type = BoundaryHandling< FlagField_T, Stencil_T, NoSlip_T, MO_T >; + + MyBoundaryHandling( const BlockDataID & flagFieldID, const BlockDataID & pdfFieldID, + const BlockDataID & particleFieldID, const shared_ptr<ParticleAccessor_T>& ac) : + flagFieldID_( flagFieldID ), pdfFieldID_( pdfFieldID ), particleFieldID_( particleFieldID ), ac_( ac ) {} + + Type * operator()( IBlock* const block, const StructuredBlockStorage* const storage ) const + { + WALBERLA_ASSERT_NOT_NULLPTR( block ); + WALBERLA_ASSERT_NOT_NULLPTR( storage ); + + auto * flagField = block->getData< FlagField_T >( flagFieldID_ ); + auto * pdfField = block->getData< PdfField_T > ( pdfFieldID_ ); + auto * particleField = block->getData< lbm_mesapd_coupling::ParticleField_T > ( particleFieldID_ ); + + const auto fluid = flagField->flagExists( Fluid_Flag ) ? flagField->getFlag( Fluid_Flag ) : flagField->registerFlag( Fluid_Flag ); + + Type * handling = new Type( "moving obstacle boundary handling", flagField, fluid, + NoSlip_T( "NoSlip", NoSlip_Flag, pdfField ), + MO_T( "MO", MO_Flag, pdfField, flagField, particleField, ac_, fluid, *storage, *block ) ); + + handling->fillWithDomain( FieldGhostLayers ); + + return handling; + } + +private: + + const BlockDataID flagFieldID_; + const BlockDataID pdfFieldID_; + const BlockDataID particleFieldID_; + + shared_ptr<ParticleAccessor_T> ac_; +}; +//******************************************************************************************************************* + +//******************************************************************************************************************* +/*!\brief Evaluating the position and velocity of the sphere + * + */ +//******************************************************************************************************************* +template< typename ParticleAccessor_T> +class SpherePropertyLogger +{ +public: + SpherePropertyLogger( const shared_ptr< ParticleAccessor_T > & ac, walberla::id_t sphereUid, + const std::string & fileName, bool fileIO, + real_t dx_SI, real_t dt_SI, real_t diameter, real_t gravitationalForceMag) : + ac_( ac ), sphereUid_( sphereUid ), fileName_( fileName ), fileIO_(fileIO), + dx_SI_( dx_SI ), dt_SI_( dt_SI ), diameter_( diameter ), gravitationalForceMag_( gravitationalForceMag ), + position_( real_t(0) ), maxVelocity_( real_t(0) ) + { + if ( fileIO_ ) + { + WALBERLA_ROOT_SECTION() + { + std::ofstream file; + file.open( fileName_.c_str() ); + file << "#\t t\t posZ\t gapZ/D\t velZ\t velZ_SI\t fZ\t fZ/fGravi\n"; + file.close(); + } + } + } + + void operator()(const uint_t timestep) + { + Vector3<real_t> pos(real_t(0)); + Vector3<real_t> transVel(real_t(0)); + Vector3<real_t> hydForce(real_t(0)); + + size_t idx = ac_->uidToIdx(sphereUid_); + if( idx != ac_->getInvalidIdx()) + { + if(!mesa_pd::data::particle_flags::isSet( ac_->getFlags(idx), mesa_pd::data::particle_flags::GHOST)) + { + pos = ac_->getPosition(idx); + transVel = ac_->getLinearVelocity(idx); + } + hydForce = ac_->getHydrodynamicForce(idx); + } + + WALBERLA_MPI_SECTION() + { + mpi::allReduceInplace( pos[0], mpi::SUM ); + mpi::allReduceInplace( pos[1], mpi::SUM ); + mpi::allReduceInplace( pos[2], mpi::SUM ); + + mpi::allReduceInplace( transVel[0], mpi::SUM ); + mpi::allReduceInplace( transVel[1], mpi::SUM ); + mpi::allReduceInplace( transVel[2], mpi::SUM ); + + mpi::allReduceInplace( hydForce[0], mpi::SUM ); + mpi::allReduceInplace( hydForce[1], mpi::SUM ); + mpi::allReduceInplace( hydForce[2], mpi::SUM ); + } + + position_ = pos[2]; + maxVelocity_ = std::max(maxVelocity_, -transVel[2]); + + if( fileIO_ ) + writeToFile( timestep, pos, transVel, hydForce); + } + + real_t getPosition() const + { + return position_; + } + + real_t getMaxVelocity() const + { + return maxVelocity_; + } + +private: + void writeToFile( const uint_t timestep, const Vector3<real_t> & position, const Vector3<real_t> & velocity, const Vector3<real_t> & hydForce ) + { + WALBERLA_ROOT_SECTION() + { + std::ofstream file; + file.open( fileName_.c_str(), std::ofstream::app ); + + auto scaledPosition = position / diameter_; + auto velocity_SI = velocity * dx_SI_ / dt_SI_; + auto normalizedHydForce = hydForce / gravitationalForceMag_; + + + file << timestep << "\t" << real_c(timestep) * dt_SI_ << "\t" + << "\t" << position[2] << "\t" << scaledPosition[2] - real_t(0.5) + << "\t" << velocity[2] << "\t" << velocity_SI[2] + << "\t" << hydForce[2] << "\t" << normalizedHydForce[2] + << "\n"; + file.close(); + } + } + + shared_ptr< ParticleAccessor_T > ac_; + const walberla::id_t sphereUid_; + std::string fileName_; + bool fileIO_; + real_t dx_SI_, dt_SI_, diameter_, gravitationalForceMag_; + + real_t position_; + real_t maxVelocity_; +}; + + +void createPlaneSetup(const shared_ptr<mesa_pd::data::ParticleStorage> & ps, const shared_ptr<mesa_pd::data::ShapeStorage> & ss, const math::AABB & simulationDomain) +{ + // create bounding planes + mesa_pd::data::Particle p0 = *ps->create(true); + p0.setPosition(simulationDomain.minCorner()); + p0.setShapeID(ss->create<mesa_pd::data::HalfSpace>( Vector3<real_t>(0,0,1) )); + p0.setOwner(mpi::MPIManager::instance()->rank()); + p0.setType(0); + mesa_pd::data::particle_flags::set(p0.getFlagsRef(), mesa_pd::data::particle_flags::INFINITE); + mesa_pd::data::particle_flags::set(p0.getFlagsRef(), mesa_pd::data::particle_flags::FIXED); + + mesa_pd::data::Particle p1 = *ps->create(true); + p1.setPosition(simulationDomain.maxCorner()); + p1.setShapeID(ss->create<mesa_pd::data::HalfSpace>( Vector3<real_t>(0,0,-1) )); + p1.setOwner(mpi::MPIManager::instance()->rank()); + p1.setType(0); + mesa_pd::data::particle_flags::set(p1.getFlagsRef(), mesa_pd::data::particle_flags::INFINITE); + mesa_pd::data::particle_flags::set(p1.getFlagsRef(), mesa_pd::data::particle_flags::FIXED); + + mesa_pd::data::Particle p2 = *ps->create(true); + p2.setPosition(simulationDomain.minCorner()); + p2.setShapeID(ss->create<mesa_pd::data::HalfSpace>( Vector3<real_t>(1,0,0) )); + p2.setOwner(mpi::MPIManager::instance()->rank()); + p2.setType(0); + mesa_pd::data::particle_flags::set(p2.getFlagsRef(), mesa_pd::data::particle_flags::INFINITE); + mesa_pd::data::particle_flags::set(p2.getFlagsRef(), mesa_pd::data::particle_flags::FIXED); + + mesa_pd::data::Particle p3 = *ps->create(true); + p3.setPosition(simulationDomain.maxCorner()); + p3.setShapeID(ss->create<mesa_pd::data::HalfSpace>( Vector3<real_t>(-1,0,0) )); + p3.setOwner(mpi::MPIManager::instance()->rank()); + p3.setType(0); + mesa_pd::data::particle_flags::set(p3.getFlagsRef(), mesa_pd::data::particle_flags::INFINITE); + mesa_pd::data::particle_flags::set(p3.getFlagsRef(), mesa_pd::data::particle_flags::FIXED); + + mesa_pd::data::Particle p4 = *ps->create(true); + p4.setPosition(simulationDomain.minCorner()); + p4.setShapeID(ss->create<mesa_pd::data::HalfSpace>( Vector3<real_t>(0,1,0) )); + p4.setOwner(mpi::MPIManager::instance()->rank()); + p4.setType(0); + mesa_pd::data::particle_flags::set(p4.getFlagsRef(), mesa_pd::data::particle_flags::INFINITE); + mesa_pd::data::particle_flags::set(p4.getFlagsRef(), mesa_pd::data::particle_flags::FIXED); + + mesa_pd::data::Particle p5 = *ps->create(true); + p5.setPosition(simulationDomain.maxCorner()); + p5.setShapeID(ss->create<mesa_pd::data::HalfSpace>( Vector3<real_t>(0,-1,0) )); + p5.setOwner(mpi::MPIManager::instance()->rank()); + p5.setType(0); + mesa_pd::data::particle_flags::set(p5.getFlagsRef(), mesa_pd::data::particle_flags::INFINITE); + mesa_pd::data::particle_flags::set(p5.getFlagsRef(), mesa_pd::data::particle_flags::FIXED); + +} + +////////// +// MAIN // +////////// + +//******************************************************************************************************************* +/*!\brief Testcase that simulates the settling of a sphere inside a rectangular column filled with viscous fluid + * + * see: ten Cate, Nieuwstad, Derksen, Van den Akker - "Particle imaging velocimetry experiments and lattice-Boltzmann + * simulations on a single sphere settling under gravity" (2002), Physics of Fluids, doi: 10.1063/1.1512918 + */ +//******************************************************************************************************************* + +int main( int argc, char **argv ) +{ + debug::enterTestMode(); + + mpi::Environment env( argc, argv ); + + /////////////////// + // Customization // + /////////////////// + + // simulation control + bool shortrun = false; + bool funcTest = false; + bool fileIO = true; + uint_t vtkIOFreq = 0; + std::string baseFolder = "vtk_out_SettlingSphere"; + std::string fileNameEnding = ""; + + // physical setup + uint_t fluidType = 1; + + //numerical parameters + uint_t numberOfCellsInHorizontalDirection = uint_t(135); + bool averageForceTorqueOverTwoTimSteps = true; + bool conserveMomentum = false; + uint_t numRPDSubCycles = uint_t(1); + bool useVelocityVerlet = true; + std::string reconstructorType = "Grad"; // Eq, EAN, Ext, Grad + real_t bulkViscRateFactor = real_t(1); + real_t magicNumber = real_t(3)/real_t(16); + real_t characteristicVelocity = real_t(0.02); + bool useOmegaBulkAdaption = false; + real_t adaptionLayerSize = real_t(2); + bool useLubricationCorrection = true; + + for( int i = 1; i < argc; ++i ) + { + if( std::strcmp( argv[i], "--shortrun" ) == 0 ) { shortrun = true; continue; } + if( std::strcmp( argv[i], "--funcTest" ) == 0 ) { funcTest = true; continue; } + if( std::strcmp( argv[i], "--noLogging" ) == 0 ) { fileIO = false; continue; } + if( std::strcmp( argv[i], "--vtkIOFreq" ) == 0 ) { vtkIOFreq = uint_c( std::atof( argv[++i] ) ); continue; } + if( std::strcmp( argv[i], "--fluidType" ) == 0 ) { fluidType = uint_c( std::atof( argv[++i] ) ); continue; } + if( std::strcmp( argv[i], "--numRPDSubCycles" ) == 0 ) { numRPDSubCycles = uint_c( std::atof( argv[++i] ) ); continue; } + if( std::strcmp( argv[i], "--resolution" ) == 0 ) { numberOfCellsInHorizontalDirection = uint_c( std::atof( argv[++i] ) ); continue; } + if( std::strcmp( argv[i], "--noForceAveraging" ) == 0 ) { averageForceTorqueOverTwoTimSteps = false; continue; } + if( std::strcmp( argv[i], "--conserveMomentum" ) == 0 ) { conserveMomentum = true; continue; } + if( std::strcmp( argv[i], "--baseFolder" ) == 0 ) { baseFolder = argv[++i]; continue; } + if( std::strcmp( argv[i], "--useEulerIntegrator" ) == 0 ) { useVelocityVerlet = false; continue; } + if( std::strcmp( argv[i], "--reconstructorType" ) == 0 ) { reconstructorType = argv[++i]; continue; } + if( std::strcmp( argv[i], "--bulkViscRateFactor" ) == 0 ) { bulkViscRateFactor = real_c(std::atof( argv[++i] ) ); continue; } + if( std::strcmp( argv[i], "--magicNumber" ) == 0 ) { magicNumber = real_c(std::atof( argv[++i] ) ); continue; } + if( std::strcmp( argv[i], "--velocity" ) == 0 ) { characteristicVelocity = real_c(std::atof( argv[++i] )); continue; } + if( std::strcmp( argv[i], "--fileName" ) == 0 ) { fileNameEnding = argv[++i]; continue; } + if( std::strcmp( argv[i], "--useOmegaBulkAdaption" ) == 0 ) { useOmegaBulkAdaption = true; continue; } + if( std::strcmp( argv[i], "--adaptionLayerSize" ) == 0 ) { adaptionLayerSize = real_c(std::atof( argv[++i] )); continue; } + if( std::strcmp( argv[i], "--noLubricationCorrection" ) == 0 ) { useLubricationCorrection = false; continue; } + WALBERLA_ABORT("Unrecognized command line argument found: " << argv[i]); + } + + if( funcTest ) + { + walberla::logging::Logging::instance()->setLogLevel(logging::Logging::LogLevel::WARNING); + } + + if( fileIO ) + { + // create base directory if it does not yet exist + filesystem::path tpath( baseFolder ); + if( !filesystem::exists( tpath ) ) + filesystem::create_directory( tpath ); + } + + ////////////////////////////////////// + // SIMULATION PROPERTIES in SI units// + ////////////////////////////////////// + + // values are mainly taken from the reference paper + const real_t diameter_SI = real_t(15e-3); + const real_t densitySphere_SI = real_t(1120); + + real_t densityFluid_SI, dynamicViscosityFluid_SI; + real_t expectedSettlingVelocity_SI; + switch( fluidType ) + { + case 1: + // Re_p around 1.5 + densityFluid_SI = real_t(970); + dynamicViscosityFluid_SI = real_t(373e-3); + expectedSettlingVelocity_SI = real_t(0.035986); + break; + case 2: + // Re_p around 4.1 + densityFluid_SI = real_t(965); + dynamicViscosityFluid_SI = real_t(212e-3); + expectedSettlingVelocity_SI = real_t(0.05718); + break; + case 3: + // Re_p around 11.6 + densityFluid_SI = real_t(962); + dynamicViscosityFluid_SI = real_t(113e-3); + expectedSettlingVelocity_SI = real_t(0.087269); + break; + case 4: + // Re_p around 31.9 + densityFluid_SI = real_t(960); + dynamicViscosityFluid_SI = real_t(58e-3); + expectedSettlingVelocity_SI = real_t(0.12224); + break; + default: + WALBERLA_ABORT("Only four different fluids are supported! Choose type between 1 and 4."); + } + const real_t kinematicViscosityFluid_SI = dynamicViscosityFluid_SI / densityFluid_SI; + + const real_t gravitationalAcceleration_SI = real_t(9.81); + + const real_t ug_SI = std::sqrt((densitySphere_SI/densityFluid_SI-real_t(1))*diameter_SI*gravitationalAcceleration_SI); + const real_t GalileoNumber = ug_SI * diameter_SI / (dynamicViscosityFluid_SI/densityFluid_SI); + + Vector3<real_t> domainSize_SI(real_t(100e-3), real_t(100e-3), real_t(160e-3)); + //shift starting gap a bit upwards to match the reported (plotted) values + const real_t startingGapSize_SI = real_t(120e-3) + real_t(0.25) * diameter_SI; + + WALBERLA_LOG_INFO_ON_ROOT("Setup (in SI units):"); + WALBERLA_LOG_INFO_ON_ROOT(" - fluid type = " << fluidType ); + WALBERLA_LOG_INFO_ON_ROOT(" - domain size = " << domainSize_SI ); + WALBERLA_LOG_INFO_ON_ROOT(" - sphere: diameter = " << diameter_SI << ", density = " << densitySphere_SI << ", starting gap size = " << startingGapSize_SI ); + WALBERLA_LOG_INFO_ON_ROOT(" - fluid: density = " << densityFluid_SI << ", dyn. visc = " << dynamicViscosityFluid_SI << ", kin. visc = " << kinematicViscosityFluid_SI ); + WALBERLA_LOG_INFO_ON_ROOT(" - expected settling velocity = " << expectedSettlingVelocity_SI << " --> Re_p = " << expectedSettlingVelocity_SI * diameter_SI / kinematicViscosityFluid_SI ); + WALBERLA_LOG_INFO_ON_ROOT(" - gravitational velocity = " << ug_SI << " --> Ga = " << GalileoNumber ); + + + ////////////////////////// + // NUMERICAL PARAMETERS // + ////////////////////////// + + + const real_t dx_SI = domainSize_SI[0] / real_c(numberOfCellsInHorizontalDirection); + const Vector3<uint_t> domainSize( uint_c(floor(domainSize_SI[0] / dx_SI + real_t(0.5)) ), + uint_c(floor(domainSize_SI[1] / dx_SI + real_t(0.5)) ), + uint_c(floor(domainSize_SI[2] / dx_SI + real_t(0.5)) ) ); + const real_t diameter = diameter_SI / dx_SI; + const real_t sphereVolume = math::pi / real_t(6) * diameter * diameter * diameter; + + //const real_t dt_SI = characteristicVelocity / ug_SI * dx_SI; // this uses Ga for parameterization + const real_t dt_SI = characteristicVelocity / expectedSettlingVelocity_SI * dx_SI; // this uses Re for parameterization (only possible since settling velocity is known) + + const real_t viscosity = kinematicViscosityFluid_SI * dt_SI / ( dx_SI * dx_SI ); + const real_t omega = lbm::collision_model::omegaFromViscosity(viscosity); + const real_t relaxationTime = real_t(1) / omega; + const real_t omegaBulk = lbm_mesapd_coupling::omegaBulkFromOmega(omega, bulkViscRateFactor); + + const real_t gravitationalAcceleration = gravitationalAcceleration_SI * dt_SI * dt_SI / dx_SI; + + const real_t densityFluid = real_t(1); + const real_t densitySphere = densityFluid * densitySphere_SI / densityFluid_SI; + + const real_t expectedSettlingVelocity = expectedSettlingVelocity_SI * dt_SI / dx_SI; + const real_t ug = std::sqrt((densitySphere/densityFluid-real_t(1))*diameter*gravitationalAcceleration); + const real_t GalileoNumber_LBM = ug * diameter / viscosity; + + const real_t dx = real_t(1); + + const uint_t timesteps = funcTest ? 1 : ( shortrun ? uint_t(200) : uint_t( 250000 ) ); + + WALBERLA_LOG_INFO_ON_ROOT(" - dx_SI = " << dx_SI << ", dt_SI = " << dt_SI); + WALBERLA_LOG_INFO_ON_ROOT("Setup (in simulation, i.e. lattice, units):"); + WALBERLA_LOG_INFO_ON_ROOT(" - domain size = " << domainSize); + WALBERLA_LOG_INFO_ON_ROOT(" - sphere: diameter = " << diameter << ", density = " << densitySphere ); + WALBERLA_LOG_INFO_ON_ROOT(" - fluid: density = " << densityFluid << ", relaxation time (tau) = " << relaxationTime << ", omega = " << omega << " kin. visc = " << viscosity ); + WALBERLA_LOG_INFO_ON_ROOT(" - magic number = " << magicNumber); + WALBERLA_LOG_INFO_ON_ROOT(" - omegaBulk = " << omegaBulk << ", bulk visc. = " << lbm_mesapd_coupling::bulkViscosityFromOmegaBulk(omegaBulk) << " (bvrf " << bulkViscRateFactor << ")"); + WALBERLA_LOG_INFO_ON_ROOT(" - use omega bulk adaption = " << useOmegaBulkAdaption << " (adaption layer size = " << adaptionLayerSize << ")"); + WALBERLA_LOG_INFO_ON_ROOT(" - gravitational acceleration = " << gravitationalAcceleration ); + WALBERLA_LOG_INFO_ON_ROOT(" - expected settling velocity = " << expectedSettlingVelocity << " --> Re_p = " << expectedSettlingVelocity * diameter / viscosity ); + WALBERLA_LOG_INFO_ON_ROOT(" - gravitational velocity = " << ug << " --> Ga = " << GalileoNumber_LBM ); + WALBERLA_LOG_INFO_ON_ROOT(" - integrator = " << (useVelocityVerlet ? "Velocity Verlet" : "Explicit Euler") ); + WALBERLA_LOG_INFO_ON_ROOT(" - conserve momentum = " << (conserveMomentum ? "yes" : "no") ); + WALBERLA_LOG_INFO_ON_ROOT(" - reconstructor type = " << reconstructorType ); + WALBERLA_LOG_INFO_ON_ROOT(" - lubrication correction = " << useLubricationCorrection ); + + if( vtkIOFreq > 0 ) + { + WALBERLA_LOG_INFO_ON_ROOT(" - writing vtk files to folder \"" << baseFolder << "\" with frequency " << vtkIOFreq); + } + + /////////////////////////// + // BLOCK STRUCTURE SETUP // + /////////////////////////// + + Vector3<uint_t> numberOfBlocksPerDirection( uint_t(1), uint_t(1), uint_t(4) ); + Vector3<uint_t> cellsPerBlockPerDirection( domainSize[0] / numberOfBlocksPerDirection[0], + domainSize[1] / numberOfBlocksPerDirection[1], + domainSize[2] / numberOfBlocksPerDirection[2] ); + for( uint_t i = 0; i < 3; ++i ) { + WALBERLA_CHECK_EQUAL(cellsPerBlockPerDirection[i] * numberOfBlocksPerDirection[i], domainSize[i], + "Unmatching domain decomposition in direction " << i << "!"); + } + + auto blocks = blockforest::createUniformBlockGrid( numberOfBlocksPerDirection[0], numberOfBlocksPerDirection[1], numberOfBlocksPerDirection[2], + cellsPerBlockPerDirection[0], cellsPerBlockPerDirection[1], cellsPerBlockPerDirection[2], dx, + 0, false, false, + false, false, false, //periodicity + false ); + + WALBERLA_LOG_INFO_ON_ROOT("Domain decomposition:"); + WALBERLA_LOG_INFO_ON_ROOT(" - blocks per direction = " << numberOfBlocksPerDirection ); + WALBERLA_LOG_INFO_ON_ROOT(" - cells per block = " << cellsPerBlockPerDirection ); + + //write domain decomposition to file + if( vtkIOFreq > 0 ) + { + vtk::writeDomainDecomposition( blocks, "initial_domain_decomposition", baseFolder ); + } + + ////////////////// + // RPD COUPLING // + ////////////////// + + auto rpdDomain = std::make_shared<mesa_pd::domain::BlockForestDomain>(blocks->getBlockForestPointer()); + + //init data structures + auto ps = walberla::make_shared<mesa_pd::data::ParticleStorage>(1); + auto ss = walberla::make_shared<mesa_pd::data::ShapeStorage>(); + using ParticleAccessor_T = mesa_pd::data::ParticleAccessorWithShape; + auto accessor = walberla::make_shared<ParticleAccessor_T >(ps, ss); + + // bounding planes + createPlaneSetup(ps,ss,blocks->getDomain()); + + // create sphere and store Uid + Vector3<real_t> initialPosition( real_t(0.5) * real_c(domainSize[0]), real_t(0.5) * real_c(domainSize[1]), startingGapSize_SI / dx_SI + real_t(0.5) * diameter); + auto sphereShape = ss->create<mesa_pd::data::Sphere>( diameter * real_t(0.5) ); + ss->shapes[sphereShape]->updateMassAndInertia(densitySphere); + + walberla::id_t sphereUid = 0; + if (rpdDomain->isContainedInProcessSubdomain( uint_c(mpi::MPIManager::instance()->rank()), initialPosition )) + { + mesa_pd::data::Particle&& p = *ps->create(); + p.setPosition(initialPosition); + p.setInteractionRadius(diameter * real_t(0.5)); + p.setOwner(mpi::MPIManager::instance()->rank()); + p.setShapeID(sphereShape); + sphereUid = p.getUid(); + } + mpi::allReduceInplace(sphereUid, mpi::SUM); + + /////////////////////// + // ADD DATA TO BLOCKS // + //////////////////////// + + // add omega bulk field + BlockDataID omegaBulkFieldID = field::addToStorage<ScalarField_T>( blocks, "omega bulk field", omegaBulk, field::fzyx, uint_t(0) ); + + // create the lattice model +#ifdef USE_TRT_LIKE_LATTICE_MODEL + WALBERLA_LOG_INFO_ON_ROOT("Using TRTlike model!"); + real_t lambda_e = lbm::collision_model::TRT::lambda_e( omega ); + real_t lambda_d = lbm::collision_model::TRT::lambda_d( omega, magicNumber ); + LatticeModel_T latticeModel = LatticeModel_T(omegaBulkFieldID, lambda_d, lambda_e); + //LatticeModel_T latticeModel = LatticeModel_T(omegaBulk, lambda_d, lambda_e); + //LatticeModel_T latticeModel = LatticeModel_T(lbm::collision_model::D3Q19MRT( omegaBulk, omegaBulk, lambda_d, lambda_e, lambda_e, lambda_d )); +#else + WALBERLA_LOG_INFO_ON_ROOT("Using KBC model!"); + // generated KBC + LatticeModel_T latticeModel = LatticeModel_T(omega); +#endif + + + // add PDF field + BlockDataID pdfFieldID = lbm::addPdfFieldToStorage< LatticeModel_T >( blocks, "pdf field (fzyx)", latticeModel, + Vector3< real_t >( real_t(0) ), real_t(1), + uint_t(1), field::fzyx ); + // add flag field + BlockDataID flagFieldID = field::addFlagFieldToStorage<FlagField_T>( blocks, "flag field" ); + + // add particle field + BlockDataID particleFieldID = field::addToStorage<lbm_mesapd_coupling::ParticleField_T>( blocks, "particle field", accessor->getInvalidUid(), field::fzyx, FieldGhostLayers ); + + // add boundary handling + using BoundaryHandling_T = MyBoundaryHandling<ParticleAccessor_T>::Type; + BlockDataID boundaryHandlingID = blocks->addStructuredBlockData< BoundaryHandling_T >(MyBoundaryHandling<ParticleAccessor_T>( flagFieldID, pdfFieldID, particleFieldID, accessor), "boundary handling" ); + + // set up RPD functionality + std::function<void(void)> syncCall = [ps,rpdDomain](){ + const real_t overlap = real_t( 1.5 ); + mesa_pd::mpi::SyncNextNeighbors syncNextNeighborFunc; + syncNextNeighborFunc(*ps, *rpdDomain, overlap); + }; + + syncCall(); + + mesa_pd::kernel::ExplicitEulerWithShape explEulerIntegrator(real_t(1)/real_t(numRPDSubCycles)); + mesa_pd::kernel::VelocityVerletWithShapePreForceUpdate vvIntegratorPreForce(real_t(1)/real_t(numRPDSubCycles)); + mesa_pd::kernel::VelocityVerletWithShapePostForceUpdate vvIntegratorPostForce(real_t(1)/real_t(numRPDSubCycles)); + + mesa_pd::kernel::SpringDashpot collisionResponse(1); + mesa_pd::mpi::ReduceProperty reduceProperty; + + // set up coupling functionality + Vector3<real_t> gravitationalForce( real_t(0), real_t(0), -(densitySphere - densityFluid) * gravitationalAcceleration * sphereVolume ); + lbm_mesapd_coupling::AddForceOnParticlesKernel addGravitationalForce(gravitationalForce); + lbm_mesapd_coupling::AddHydrodynamicInteractionKernel addHydrodynamicInteraction; + lbm_mesapd_coupling::ResetHydrodynamicForceTorqueKernel resetHydrodynamicForceTorque; + lbm_mesapd_coupling::AverageHydrodynamicForceTorqueKernel averageHydrodynamicForceTorque; + lbm_mesapd_coupling::LubricationCorrectionKernel lubricationCorrectionKernel(viscosity, [](real_t r){ return real_t(0.0016) * r;}); + + lbm_mesapd_coupling::RegularParticlesSelector sphereSelector; + + lbm_mesapd_coupling::ParticleMappingKernel<BoundaryHandling_T> particleMappingKernel(blocks, boundaryHandlingID); + lbm_mesapd_coupling::MovingParticleMappingKernel<BoundaryHandling_T> movingParticleMappingKernel(blocks, boundaryHandlingID, particleFieldID); + + /////////////// + // TIME LOOP // + /////////////// + + // map planes into the LBM simulation -> act as no-slip boundaries + ps->forEachParticle(false, lbm_mesapd_coupling::GlobalParticlesSelector(), *accessor, particleMappingKernel, *accessor, NoSlip_Flag); + + // map particles into the LBM simulation + ps->forEachParticle(false, sphereSelector, *accessor, movingParticleMappingKernel, *accessor, MO_Flag); + + + // setup of the LBM communication for synchronizing the pdf field between neighboring blocks + + blockforest::communication::UniformBufferedScheme< Stencil_T > optimizedPDFCommunicationScheme( blocks ); + optimizedPDFCommunicationScheme.addPackInfo( make_shared< lbm::PdfFieldPackInfo< LatticeModel_T > >( pdfFieldID ) ); // optimized sync + + blockforest::communication::UniformBufferedScheme< Stencil_T > fullPDFCommunicationScheme( blocks ); + fullPDFCommunicationScheme.addPackInfo( make_shared< field::communication::PackInfo< PdfField_T > >( pdfFieldID ) ); // full sync + + // create the timeloop + SweepTimeloop timeloop( blocks->getBlockStorage(), timesteps ); + + + timeloop.addFuncBeforeTimeStep( RemainingTimeLogger( timeloop.getNrOfTimeSteps() ), "Remaining Time Logger" ); + + // vtk output + if( vtkIOFreq != uint_t(0) ) + { + // spheres + auto particleVtkOutput = make_shared<mesa_pd::vtk::ParticleVtkOutput>(ps); + particleVtkOutput->addOutput<mesa_pd::data::SelectParticleOwner>("owner"); + particleVtkOutput->addOutput<mesa_pd::data::SelectParticleLinearVelocity>("velocity"); + auto particleVtkWriter = vtk::createVTKOutput_PointData(particleVtkOutput, "Particles", vtkIOFreq, baseFolder, "simulation_step"); + timeloop.addFuncBeforeTimeStep( vtk::writeFiles( particleVtkWriter ), "VTK (sphere data)" ); + + // flag field (written only once in the first time step, ghost layers are also written) + //auto flagFieldVTK = vtk::createVTKOutput_BlockData( blocks, "flag_field", timesteps, FieldGhostLayers, false, baseFolder ); + //flagFieldVTK->addCellDataWriter( make_shared< field::VTKWriter< FlagField_T > >( flagFieldID, "FlagField" ) ); + //timeloop.addFuncBeforeTimeStep( vtk::writeFiles( flagFieldVTK ), "VTK (flag field data)" ); + + // pdf field + auto pdfFieldVTK = vtk::createVTKOutput_BlockData( blocks, "fluid_field", vtkIOFreq, 0, false, baseFolder ); + + pdfFieldVTK->addBeforeFunction( fullPDFCommunicationScheme ); + + field::FlagFieldCellFilter< FlagField_T > fluidFilter( flagFieldID ); + fluidFilter.addFlag( Fluid_Flag ); + pdfFieldVTK->addCellInclusionFilter( fluidFilter ); + + pdfFieldVTK->addCellDataWriter( make_shared< lbm::VelocityVTKWriter< LatticeModel_T, float > >( pdfFieldID, "VelocityFromPDF" ) ); + pdfFieldVTK->addCellDataWriter( make_shared< lbm::DensityVTKWriter < LatticeModel_T, float > >( pdfFieldID, "DensityFromPDF" ) ); + + timeloop.addFuncBeforeTimeStep( vtk::writeFiles( pdfFieldVTK ), "VTK (fluid field data)" ); + + // omega bulk field + timeloop.addFuncBeforeTimeStep( field::createVTKOutput<ScalarField_T, float>( omegaBulkFieldID, *blocks, "omega_bulk_field", vtkIOFreq, uint_t(0), false, baseFolder ), "VTK (omega bulk field)" ); + + } + + // sweep for updating the particle mapping into the LBM simulation + timeloop.add() << Sweep( lbm_mesapd_coupling::makeMovingParticleMapping<PdfField_T, BoundaryHandling_T>(blocks, pdfFieldID, boundaryHandlingID, particleFieldID, accessor, MO_Flag, FormerMO_Flag, sphereSelector, conserveMomentum), "Particle Mapping" ); + + // sweep for restoring PDFs in cells previously occupied by particles + if( reconstructorType == "EAN") + { + + auto sphereNormalExtrapolationDirectionFinder = make_shared<lbm_mesapd_coupling::SphereNormalExtrapolationDirectionFinder>(blocks); + auto equilibriumAndNonEquilibriumSphereNormalReconstructor = lbm_mesapd_coupling::makeEquilibriumAndNonEquilibriumReconstructor<BoundaryHandling_T>(blocks, boundaryHandlingID, sphereNormalExtrapolationDirectionFinder, uint_t(3), true); + auto reconstructionManager = lbm_mesapd_coupling::makePdfReconstructionManager<PdfField_T,BoundaryHandling_T>(blocks, pdfFieldID, boundaryHandlingID, particleFieldID, accessor, FormerMO_Flag, Fluid_Flag, equilibriumAndNonEquilibriumSphereNormalReconstructor, conserveMomentum); + + timeloop.add() << BeforeFunction( fullPDFCommunicationScheme, "PDF Communication" ) + << Sweep( makeSharedSweep(reconstructionManager), "PDF Restore" ); + + } else if( reconstructorType == "Ext" ) + { + auto sphereNormalExtrapolationDirectionFinder = make_shared<lbm_mesapd_coupling::SphereNormalExtrapolationDirectionFinder>(blocks); + auto extrapolationSphereNormalReconstructor = lbm_mesapd_coupling::makeExtrapolationReconstructor<BoundaryHandling_T,lbm_mesapd_coupling::SphereNormalExtrapolationDirectionFinder,true>(blocks, boundaryHandlingID, sphereNormalExtrapolationDirectionFinder, uint_t(3), true); + + timeloop.add() << BeforeFunction( fullPDFCommunicationScheme, "PDF Communication" ) + << Sweep( makeSharedSweep(lbm_mesapd_coupling::makePdfReconstructionManager<PdfField_T,BoundaryHandling_T>(blocks, pdfFieldID, boundaryHandlingID, particleFieldID, accessor, FormerMO_Flag, Fluid_Flag, extrapolationSphereNormalReconstructor, conserveMomentum) ), "PDF Restore" ); + } else if( reconstructorType == "Grad") + { + auto gradReconstructor = lbm_mesapd_coupling::makeGradsMomentApproximationReconstructor<BoundaryHandling_T>(blocks, boundaryHandlingID, omega, false, true, true); + + timeloop.add() << BeforeFunction( fullPDFCommunicationScheme, "PDF Communication" ) + << Sweep( makeSharedSweep(lbm_mesapd_coupling::makePdfReconstructionManager<PdfField_T,BoundaryHandling_T>(blocks, pdfFieldID, boundaryHandlingID, particleFieldID, accessor, FormerMO_Flag, Fluid_Flag, gradReconstructor, conserveMomentum) ), "PDF Restore" ); + } else if( reconstructorType == "Eq") + { + timeloop.add() << Sweep( makeSharedSweep(lbm_mesapd_coupling::makePdfReconstructionManager<PdfField_T,BoundaryHandling_T>(blocks, pdfFieldID, boundaryHandlingID, particleFieldID, accessor, FormerMO_Flag, Fluid_Flag, conserveMomentum) ), "PDF Restore" ); + } else { + WALBERLA_ABORT("Unknown reconstructor type " << reconstructorType); + } + + // update bulk omega in all cells to adapt to changed particle position + if( useOmegaBulkAdaption ) + { + using OmegaBulkAdapter_T = lbm_mesapd_coupling::OmegaBulkAdapter<ParticleAccessor_T, decltype(sphereSelector)>; + real_t defaultOmegaBulk = lbm_mesapd_coupling::omegaBulkFromOmega(omega, real_t(1)); + shared_ptr<OmegaBulkAdapter_T> omegaBulkAdapter = make_shared<OmegaBulkAdapter_T>(blocks, omegaBulkFieldID, accessor, defaultOmegaBulk, omegaBulk, adaptionLayerSize, sphereSelector); + timeloop.add() << Sweep( makeSharedSweep(omegaBulkAdapter), "Omega Bulk Adapter"); + } + + bool useStreamCollide = true; + auto bhSweep = BoundaryHandling_T::getBlockSweep( boundaryHandlingID ); + + // generated sweeps + auto lbmSweep = LatticeModel_T::Sweep( pdfFieldID ); + if( useStreamCollide ) + { + // add LBM communication function and boundary handling sweep (does the hydro force calculations and the no-slip treatment) + timeloop.add() << BeforeFunction( optimizedPDFCommunicationScheme, "LBM Communication" ) + << Sweep(bhSweep, "Boundary Handling" ); + + // stream + collide LBM step + timeloop.add() << Sweep( lbmSweep, "LB sweep" ); + } + else + { + // collide LBM step + timeloop.add() << Sweep([&lbmSweep](IBlock * const block){lbmSweep.collide(block);}, "cell-wise collide LB sweep" ); + + // add LBM communication function and boundary handling sweep (does the hydro force calculations and the no-slip treatment) + timeloop.add() << BeforeFunction( optimizedPDFCommunicationScheme, "LBM Communication" ) + << Sweep( BoundaryHandling_T::getBlockSweep( boundaryHandlingID ), "Boundary Handling" ); + + // stream LBM step + timeloop.add() << Sweep([&lbmSweep](IBlock * const block){lbmSweep.stream(block);}, "cell-wise stream LB sweep" ); + } + + // evaluation functionality + std::string loggingFileName( baseFolder + "/LoggingSettlingSphere_"); + loggingFileName += std::to_string(fluidType); + loggingFileName += "_recon" + reconstructorType; + loggingFileName += "_bvrf" + std::to_string(uint_c(bulkViscRateFactor)); + loggingFileName += "_mn" + std::to_string(float(magicNumber)); + if( useOmegaBulkAdaption ) loggingFileName += "_uOBA" + std::to_string(uint_c(adaptionLayerSize)); + if( !fileNameEnding.empty()) loggingFileName += "_" + fileNameEnding; + loggingFileName += ".txt"; + if( fileIO ) + { + WALBERLA_LOG_INFO_ON_ROOT(" - writing logging output to file \"" << loggingFileName << "\""); + } + SpherePropertyLogger<ParticleAccessor_T> logger( accessor, sphereUid, loggingFileName, fileIO, dx_SI, dt_SI, diameter, -gravitationalForce[2] ); + + + //////////////////////// + // EXECUTE SIMULATION // + //////////////////////// + + WcTimingPool timeloopTiming; + + real_t terminationPosition = diameter * real_t(0.501); // right before sphere touches the bottom wall + real_t oldPos = initialPosition[2]; + + const bool useOpenMP = false; + + // time loop + for (uint_t i = 0; i < timesteps; ++i ) + { + // perform a single simulation step -> this contains LBM and setting of the hydrodynamic interactions + timeloop.singleStep( timeloopTiming ); + + timeloopTiming["RPD"].start(); + + if( averageForceTorqueOverTwoTimSteps && i!= 0) + { + ps->forEachParticle(useOpenMP, mesa_pd::kernel::SelectAll(), *accessor, averageHydrodynamicForceTorque, *accessor ); + } + + for(auto subCycle = uint_t(0); subCycle < numRPDSubCycles; ++subCycle ) + { + + if( useVelocityVerlet ) + { + ps->forEachParticle(useOpenMP, mesa_pd::kernel::SelectLocal(), *accessor, vvIntegratorPreForce, *accessor); + syncCall(); + } + + ps->forEachParticle(useOpenMP, sphereSelector, *accessor, addHydrodynamicInteraction, *accessor ); + ps->forEachParticle(useOpenMP, mesa_pd::kernel::SelectLocal(), *accessor, addGravitationalForce, *accessor ); + + // lubrication correction + if(useLubricationCorrection) + { + ps->forEachParticlePairHalf(useOpenMP, mesa_pd::kernel::ExcludeInfiniteInfinite(), *accessor, + [&lubricationCorrectionKernel,rpdDomain](const size_t idx1, const size_t idx2, auto& ac) + { + //TODO change this to storing copy, not reference + mesa_pd::collision_detection::AnalyticContactDetection acd; + acd.getContactThreshold() = lubricationCorrectionKernel.getNormalCutOffDistance(); + mesa_pd::kernel::DoubleCast double_cast; + mesa_pd::mpi::ContactFilter contact_filter; + if (double_cast(idx1, idx2, ac, acd, ac )) + { + if (contact_filter(acd.getIdx1(), acd.getIdx2(), ac, acd.getContactPoint(), *rpdDomain)) + { + double_cast(idx1, idx2, ac, lubricationCorrectionKernel, ac, acd.getContactNormal(), acd.getPenetrationDepth()); + } + } + }, + *accessor ); + } + + reduceProperty.operator()<mesa_pd::ForceTorqueNotification>(*ps); + + if( useVelocityVerlet ) ps->forEachParticle(useOpenMP, mesa_pd::kernel::SelectLocal(), *accessor, vvIntegratorPostForce, *accessor); + else ps->forEachParticle(useOpenMP, mesa_pd::kernel::SelectLocal(), *accessor, explEulerIntegrator, *accessor); + + syncCall(); + } + + timeloopTiming["RPD"].end(); + + // evaluation + timeloopTiming["Logging"].start(); + logger(i); + timeloopTiming["Logging"].end(); + + // reset after logging here + ps->forEachParticle(useOpenMP, mesa_pd::kernel::SelectAll(), *accessor, resetHydrodynamicForceTorque, *accessor ); + + // check for termination + if ( logger.getPosition() < terminationPosition || oldPos < logger.getPosition() ) + { + WALBERLA_LOG_INFO_ON_ROOT("Sphere reached terminal position " << logger.getPosition() << " after " << i << " timesteps!"); + break; + } + oldPos = logger.getPosition(); + } + + timeloopTiming.logResultOnRoot(); + + // check the result + if ( !funcTest && !shortrun ) + { + real_t relErr = std::fabs( expectedSettlingVelocity - logger.getMaxVelocity()) / expectedSettlingVelocity; + WALBERLA_LOG_INFO_ON_ROOT( "Expected maximum settling velocity: " << expectedSettlingVelocity ); + WALBERLA_LOG_INFO_ON_ROOT( "Simulated maximum settling velocity: " << logger.getMaxVelocity() ); + WALBERLA_LOG_INFO_ON_ROOT( "Relative error: " << relErr ); + + // the relative error has to be below 10% + WALBERLA_CHECK_LESS( relErr, real_t(0.1) ); + } + + return EXIT_SUCCESS; +} + +} // namespace settling_sphere_in_box + +int main( int argc, char **argv ){ + settling_sphere_in_box::main(argc, argv); +} diff --git a/apps/benchmarks/FluidParticleCoupling/SphereMovingWithPrescribedVelocity.cpp b/apps/benchmarks/FluidParticleCoupling/SphereMovingWithPrescribedVelocity.cpp new file mode 100644 index 0000000000000000000000000000000000000000..9e1b866792a5e5088c5e6940f8b89aa9074ede81 --- /dev/null +++ b/apps/benchmarks/FluidParticleCoupling/SphereMovingWithPrescribedVelocity.cpp @@ -0,0 +1,874 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla is free software: you can +// redistribute it and/or modify it under the terms of the GNU General Public +// License as published by the Free Software Foundation, either version 3 of +// the License, or (at your option) any later version. +// +// waLBerla is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +// for more details. +// +// You should have received a copy of the GNU General Public License along +// with waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file SphereMovingWithPrescribedVelocity.cpp +//! \ingroup lbm_mesapd_coupling +//! \author Christoph Rettinger <christoph.rettinger@fau.de> +// +//====================================================================================================================== + +#include "blockforest/Initialization.h" +#include "blockforest/communication/UniformBufferedScheme.h" + +#include "boundary/all.h" + +#include "core/DataTypes.h" +#include "core/Environment.h" +#include "core/SharedFunctor.h" +#include "core/debug/Debug.h" +#include "core/debug/TestSubsystem.h" +#include "core/math/all.h" +#include "core/timing/RemainingTimeLogger.h" +#include "core/logging/all.h" + +#include "domain_decomposition/SharedSweep.h" + +#include "field/AddToStorage.h" +#include "field/interpolators/FieldInterpolatorCreators.h" +#include "field/interpolators/TrilinearFieldInterpolator.h" +#include "field/StabilityChecker.h" +#include "field/communication/PackInfo.h" +#include "field/adaptors/AdaptorCreators.h" + +#include "lbm/boundary/all.h" +#include "lbm/communication/PdfFieldPackInfo.h" +#include "lbm/field/AddToStorage.h" +#include "lbm/field/Adaptors.h" +#include "lbm/field/PdfField.h" +#include "lbm/lattice_model/D3Q19.h" +#include "lbm/sweeps/CellwiseSweep.h" +#include "lbm/sweeps/SweepWrappers.h" + +#include "lbm_mesapd_coupling/mapping/ParticleMapping.h" +#include "lbm_mesapd_coupling/momentum_exchange_method/MovingParticleMapping.h" +#include "lbm_mesapd_coupling/momentum_exchange_method/boundary/SimpleBB.h" +#include "lbm_mesapd_coupling/momentum_exchange_method/boundary/CurvedLinear.h" +#include "lbm_mesapd_coupling/momentum_exchange_method/reconstruction/Reconstructor.h" +#include "lbm_mesapd_coupling/momentum_exchange_method/reconstruction/ExtrapolationDirectionFinder.h" +#include "lbm_mesapd_coupling/momentum_exchange_method/reconstruction/PdfReconstructionManager.h" +#include "lbm_mesapd_coupling/utility/AddForceOnParticlesKernel.h" +#include "lbm_mesapd_coupling/utility/ParticleSelector.h" +#include "lbm_mesapd_coupling/DataTypes.h" +#include "lbm_mesapd_coupling/utility/AverageHydrodynamicForceTorqueKernel.h" +#include "lbm_mesapd_coupling/utility/AddHydrodynamicInteractionKernel.h" +#include "lbm_mesapd_coupling/utility/ResetHydrodynamicForceTorqueKernel.h" +#include "lbm_mesapd_coupling/utility/OmegaBulkAdaption.h" + +#include "mesa_pd/collision_detection/AnalyticContactDetection.h" +#include "mesa_pd/data/ParticleAccessorWithShape.h" +#include "mesa_pd/data/ParticleStorage.h" +#include "mesa_pd/data/ShapeStorage.h" +#include "mesa_pd/data/DataTypes.h" +#include "mesa_pd/data/shape/HalfSpace.h" +#include "mesa_pd/data/shape/Sphere.h" +#include "mesa_pd/domain/BlockForestDomain.h" +#include "mesa_pd/kernel/DoubleCast.h" +#include "mesa_pd/kernel/ExplicitEulerWithShape.h" +#include "mesa_pd/kernel/ParticleSelector.h" +#include "mesa_pd/kernel/SpringDashpot.h" +#include "mesa_pd/kernel/VelocityVerlet.h" +#include "mesa_pd/mpi/SyncNextNeighbors.h" +#include "mesa_pd/mpi/ReduceProperty.h" +#include "mesa_pd/mpi/ContactFilter.h" +#include "mesa_pd/mpi/notifications/ForceTorqueNotification.h" +#include "mesa_pd/vtk/ParticleVtkOutput.h" + +#include "timeloop/SweepTimeloop.h" + +#include "vtk/all.h" +#include "field/vtk/all.h" +#include "lbm/vtk/all.h" + +#include "GeneratedLBM.h" + +#include <functional> + +#define USE_TRT_LIKE_LATTICE_MODEL + +namespace sphere_moving_with_prescribed_velocity +{ + +/////////// +// USING // +/////////// + +using namespace walberla; +using walberla::uint_t; + +using LatticeModel_T = lbm::GeneratedLBM; +//using LatticeModel_T = lbm::D3Q19< lbm::collision_model::D3Q19MRT>; + +using Stencil_T = LatticeModel_T::Stencil; +using PdfField_T = lbm::PdfField<LatticeModel_T>; + +using flag_t = walberla::uint8_t; +using FlagField_T = FlagField<flag_t>; + +using ScalarField_T = GhostLayerField< real_t, 1>; + +const uint_t FieldGhostLayers = 1; + +/////////// +// FLAGS // +/////////// + +const FlagUID Fluid_Flag( "fluid" ); +const FlagUID NoSlip_Flag( "no slip" ); +const FlagUID MO_Flag( "moving obstacle" ); +const FlagUID FormerMO_Flag( "former moving obstacle" ); + +///////////////////////////////////// +// BOUNDARY HANDLING CUSTOMIZATION // +///////////////////////////////////// +template <typename ParticleAccessor_T> +class MyBoundaryHandling +{ +public: + + using NoSlip_T = lbm::NoSlip< LatticeModel_T, flag_t >; + using MO_T = lbm_mesapd_coupling::CurvedLinear< LatticeModel_T, FlagField_T, ParticleAccessor_T >; + using Type = BoundaryHandling< FlagField_T, Stencil_T, NoSlip_T, MO_T >; + + MyBoundaryHandling( const BlockDataID & flagFieldID, const BlockDataID & pdfFieldID, + const BlockDataID & particleFieldID, const shared_ptr<ParticleAccessor_T>& ac) : + flagFieldID_( flagFieldID ), pdfFieldID_( pdfFieldID ), particleFieldID_( particleFieldID ), ac_( ac ) {} + + Type * operator()( IBlock* const block, const StructuredBlockStorage* const storage ) const + { + WALBERLA_ASSERT_NOT_NULLPTR( block ); + WALBERLA_ASSERT_NOT_NULLPTR( storage ); + + auto * flagField = block->getData< FlagField_T >( flagFieldID_ ); + auto * pdfField = block->getData< PdfField_T > ( pdfFieldID_ ); + auto * particleField = block->getData< lbm_mesapd_coupling::ParticleField_T > ( particleFieldID_ ); + + const auto fluid = flagField->flagExists( Fluid_Flag ) ? flagField->getFlag( Fluid_Flag ) : flagField->registerFlag( Fluid_Flag ); + + Type * handling = new Type( "moving obstacle boundary handling", flagField, fluid, + NoSlip_T( "NoSlip", NoSlip_Flag, pdfField ), + MO_T( "MO", MO_Flag, pdfField, flagField, particleField, ac_, fluid, *storage, *block ) ); + + handling->fillWithDomain( FieldGhostLayers ); + + return handling; + } + +private: + + const BlockDataID flagFieldID_; + const BlockDataID pdfFieldID_; + const BlockDataID particleFieldID_; + + shared_ptr<ParticleAccessor_T> ac_; +}; +//******************************************************************************************************************* + + +//******************************************************************************************************************* +/*!\brief Evaluating the position and velocity of the sphere + * + */ +//******************************************************************************************************************* +template< typename ParticleAccessor_T> +class SpherePropertyLogger +{ +public: + SpherePropertyLogger( const shared_ptr< ParticleAccessor_T > & ac, walberla::id_t sphereUid, + const std::string & fileName, bool fileIO, + real_t diameter, real_t velocity, real_t forceMag) : + ac_( ac ), sphereUid_( sphereUid ), fileName_( fileName ), fileIO_(fileIO), + diameter_( diameter ), velocity_(velocity), forceMag_( forceMag ), + position_( real_t(0) ) + { + if ( fileIO_ ) + { + WALBERLA_ROOT_SECTION() + { + std::ofstream file; + file.open( fileName_.c_str() ); + file << "#\t t\t posZ\t posZ/D\t fZ\t fZ/fSt\n"; + file.close(); + } + } + } + + void operator()() + { + real_t pos(real_t(0)); + real_t hydForce(real_t(0)); + + size_t idx = ac_->uidToIdx(sphereUid_); + if( idx != ac_->getInvalidIdx()) + { + if(!mesa_pd::data::particle_flags::isSet( ac_->getFlags(idx), mesa_pd::data::particle_flags::GHOST)) + { + pos = ac_->getPosition(idx)[2]; + } + hydForce = ac_->getHydrodynamicForce(idx)[2]; + } + + WALBERLA_MPI_SECTION() + { + mpi::allReduceInplace( pos, mpi::SUM ); + + mpi::allReduceInplace( hydForce, mpi::SUM ); + } + + position_ = pos; + hydForce_ = hydForce; + + } + + real_t getPosition() const + { + return position_; + } + + void writeToFile( const uint_t timestep, real_t density1, real_t density2, real_t totalMass ) + { + WALBERLA_ROOT_SECTION() + { + std::ofstream file; + file.open( fileName_.c_str(), std::ofstream::app ); + + auto scaledPosition = position_ / diameter_; + auto normalizedHydForce = hydForce_ / std::abs(forceMag_); + + file << std::setprecision(10) + << timestep << "\t" << real_c(timestep) / (diameter_ / velocity_) << "\t" + << "\t" << position_ << "\t" << scaledPosition + << "\t" << hydForce_ << "\t" << normalizedHydForce + << "\t" << density1 << "\t" << density2 << "\t" << totalMass + << "\n"; + file.close(); + } + } + +private: + shared_ptr< ParticleAccessor_T > ac_; + const walberla::id_t sphereUid_; + std::string fileName_; + bool fileIO_; + real_t diameter_, velocity_, forceMag_; + + real_t position_, hydForce_; +}; + + +void createPlaneSetup(const shared_ptr<mesa_pd::data::ParticleStorage> & ps, const shared_ptr<mesa_pd::data::ShapeStorage> & ss, const math::AABB & simulationDomain) +{ + mesa_pd::data::Particle p2 = *ps->create(true); + p2.setPosition(simulationDomain.minCorner()); + p2.setShapeID(ss->create<mesa_pd::data::HalfSpace>( Vector3<real_t>(1,0,0) )); + p2.setOwner(mpi::MPIManager::instance()->rank()); + p2.setType(0); + mesa_pd::data::particle_flags::set(p2.getFlagsRef(), mesa_pd::data::particle_flags::INFINITE); + mesa_pd::data::particle_flags::set(p2.getFlagsRef(), mesa_pd::data::particle_flags::FIXED); + + mesa_pd::data::Particle p3 = *ps->create(true); + p3.setPosition(simulationDomain.maxCorner()); + p3.setShapeID(ss->create<mesa_pd::data::HalfSpace>( Vector3<real_t>(-1,0,0) )); + p3.setOwner(mpi::MPIManager::instance()->rank()); + p3.setType(0); + mesa_pd::data::particle_flags::set(p3.getFlagsRef(), mesa_pd::data::particle_flags::INFINITE); + mesa_pd::data::particle_flags::set(p3.getFlagsRef(), mesa_pd::data::particle_flags::FIXED); + + mesa_pd::data::Particle p4 = *ps->create(true); + p4.setPosition(simulationDomain.minCorner()); + p4.setShapeID(ss->create<mesa_pd::data::HalfSpace>( Vector3<real_t>(0,1,0) )); + p4.setOwner(mpi::MPIManager::instance()->rank()); + p4.setType(0); + mesa_pd::data::particle_flags::set(p4.getFlagsRef(), mesa_pd::data::particle_flags::INFINITE); + mesa_pd::data::particle_flags::set(p4.getFlagsRef(), mesa_pd::data::particle_flags::FIXED); + + mesa_pd::data::Particle p5 = *ps->create(true); + p5.setPosition(simulationDomain.maxCorner()); + p5.setShapeID(ss->create<mesa_pd::data::HalfSpace>( Vector3<real_t>(0,-1,0) )); + p5.setOwner(mpi::MPIManager::instance()->rank()); + p5.setType(0); + mesa_pd::data::particle_flags::set(p5.getFlagsRef(), mesa_pd::data::particle_flags::INFINITE); + mesa_pd::data::particle_flags::set(p5.getFlagsRef(), mesa_pd::data::particle_flags::FIXED); + +} + +template< typename DensityInterpolator_T> +real_t getDensityAtPosition(const shared_ptr<StructuredBlockStorage> & blocks, BlockDataID densityInterpolatorID, Vector3<real_t> position) +{ + + real_t density = real_t(0); + for( auto & block: *blocks) + { + if(block.getAABB().contains(position)) + { + auto densityInterpolator = block.getData<DensityInterpolator_T>(densityInterpolatorID); + densityInterpolator->get(position, &density); + } + + } + mpi::reduceInplace(density, mpi::SUM); + + return density; +} + +template< typename BoundaryHandling_T> +real_t getAverageDensityInSystem(const shared_ptr<StructuredBlockStorage> & blocks, BlockDataID pdfFieldID, BlockDataID boundaryHandlingID) +{ + real_t totalMass = real_t(0); + uint_t count = uint_t(0); + for( auto & block: *blocks) + { + auto pdfField = block.getData<PdfField_T>(pdfFieldID); + auto boundaryHandling = block.getData<BoundaryHandling_T>(boundaryHandlingID); + WALBERLA_FOR_ALL_CELLS_XYZ(pdfField, + if(boundaryHandling->isDomain(x,y,z)) + { + totalMass += pdfField->getDensity(x,y,z); + ++count; + } + ); + } + + mpi::reduceInplace(totalMass, mpi::SUM); + mpi::reduceInplace(count, mpi::SUM); + + return totalMass/real_c(count); +} + +////////// +// MAIN // +////////// + +//******************************************************************************************************************* +/*!\brief Test of a sphere that is moving with a prescribed velocity. + * + * It is used to investigate fluctuations in the density field around the particle and how the bulk viscosity + * or others (PDF reconstructor, resolution, velocity) influence this behavior. + * + * With the option artificiallyAccelerateSphere, the sphere is gradually accelerated until the final velocity is reached. + * This gets rid of the strong oscillations that would appear in an abrupt start. + * + * Results of force plot: + * - mn=0.125 (2/16) reduces oscillations compared to 0.1875 (3/16) a bit, especially the peaks + * - ug=0.005 leads to higher oscillations than ug=0.01 -> since relaxation time is smaller + * - bvrf=100 reduces oscillations, especially below the mean. Peaks remain almost the same + * - Grad vs Ext reconstructor -> almost same behavior + * - D=25 decreases oscillations -> since relaxation time is bigger + * + * NOTE: Run at least with 3 processes due to periodicity! + */ +//******************************************************************************************************************* + +int main( int argc, char **argv ) +{ + debug::enterTestMode(); + + mpi::Environment env( argc, argv ); + + /////////////////// + // Customization // + /////////////////// + + // simulation control + bool funcTest = false; + bool fileIO = true; + uint_t vtkIOFreq = 0; + std::string baseFolder = "vtk_out_MovingWithPrescribedVelocity"; + std::string fileNameEnding = ""; + bool logDensity = true; + bool vtkOutputAtEnd = true; + + //numerical parameters + bool averageForceTorqueOverTwoTimSteps = true; + bool conserveMomentum = false; + uint_t numRPDSubCycles = uint_t(1); + std::string reconstructorType = "Grad"; // Eq, EAN, Ext, Grad + real_t bulkViscRateFactor = real_t(1); + real_t magicNumber = real_t(0.1875); + bool useOmegaBulkAdaption = false; + real_t adaptionLayerSize = real_t(2); + uint_t blocksInX = uint_t(1); + uint_t blocksInY = uint_t(1); + uint_t blocksInZ = uint_t(4); + real_t initialSpherePosition = real_t(0.5); // in ratio to domain height + + // simulation setup + real_t velocity = real_t(0.02); + real_t Re = real_t(164); + real_t diameter = real_t(20); + real_t numberOfPasses = real_t(3); + real_t domainWidthNonDim = real_t(4); + real_t domainHeightNonDim = real_t(8); + bool usePeriodicSetup = false; + bool artificiallyAccelerateSphere = false; + + for( int i = 1; i < argc; ++i ) + { + if( std::strcmp( argv[i], "--funcTest" ) == 0 ) { funcTest = true; continue; } + if( std::strcmp( argv[i], "--noLogging" ) == 0 ) { fileIO = false; continue; } + if( std::strcmp( argv[i], "--noVtkOutputAtEnd" ) == 0 ) { vtkOutputAtEnd = false; continue; } + if( std::strcmp( argv[i], "--logDensity" ) == 0 ) { logDensity = true; continue; } + if( std::strcmp( argv[i], "--vtkIOFreq" ) == 0 ) { vtkIOFreq = uint_c( std::atof( argv[++i] ) ); continue; } + if( std::strcmp( argv[i], "--numRPDSubCycles" ) == 0 ) { numRPDSubCycles = uint_c( std::atof( argv[++i] ) ); continue; } + if( std::strcmp( argv[i], "--noForceAveraging" ) == 0 ) { averageForceTorqueOverTwoTimSteps = false; continue; } + if( std::strcmp( argv[i], "--conserveMomentum" ) == 0 ) { conserveMomentum = true; continue; } + if( std::strcmp( argv[i], "--baseFolder" ) == 0 ) { baseFolder = argv[++i]; continue; } + if( std::strcmp( argv[i], "--reconstructorType" ) == 0 ) { reconstructorType = argv[++i]; continue; } + if( std::strcmp( argv[i], "--bulkViscRateFactor" ) == 0 ) { bulkViscRateFactor = real_c(std::atof( argv[++i] ) ); continue; } + if( std::strcmp( argv[i], "--magicNumber" ) == 0 ) { magicNumber = real_c(std::atof( argv[++i] ) ); continue; } + if( std::strcmp( argv[i], "--velocity" ) == 0 ) { velocity = real_c(std::atof( argv[++i] )); continue; } + if( std::strcmp( argv[i], "--Re" ) == 0 ) { Re = real_c(std::atof( argv[++i] )); continue; } + if( std::strcmp( argv[i], "--diameter" ) == 0 ) { diameter = real_c(std::atof( argv[++i] )); continue; } + if( std::strcmp( argv[i], "--domainWidth" ) == 0 ) { domainWidthNonDim = real_c(std::atof( argv[++i] )); continue; } + if( std::strcmp( argv[i], "--domainHeight" ) == 0 ) { domainHeightNonDim = real_c(std::atof( argv[++i] )); continue; } + if( std::strcmp( argv[i], "--fileName" ) == 0 ) { fileNameEnding = argv[++i]; continue; } + if( std::strcmp( argv[i], "--useOmegaBulkAdaption" ) == 0 ) { useOmegaBulkAdaption = true; continue; } + if( std::strcmp( argv[i], "--adaptionLayerSize" ) == 0 ) { adaptionLayerSize = real_c(std::atof( argv[++i] )); continue; } + if( std::strcmp( argv[i], "--numberOfPasses" ) == 0 ) { numberOfPasses = real_c(std::atof( argv[++i] )); continue; } + if( std::strcmp( argv[i], "--initialSpherePosition" )== 0 ) { initialSpherePosition = real_c(std::atof( argv[++i] )); continue; } + if( std::strcmp( argv[i], "--usePeriodicSetup" ) == 0 ) { usePeriodicSetup = true; continue; } + if( std::strcmp( argv[i], "--artificiallyAccelerateSphere" ) == 0 ) { artificiallyAccelerateSphere = true; continue; } + if( std::strcmp( argv[i], "--blocksInX" ) == 0 ) { blocksInX = uint_c( std::atof( argv[++i] ) ); continue; } + if( std::strcmp( argv[i], "--blocksInY" ) == 0 ) { blocksInY = uint_c( std::atof( argv[++i] ) ); continue; } + if( std::strcmp( argv[i], "--blocksInZ" ) == 0 ) { blocksInZ = uint_c( std::atof( argv[++i] ) ); continue; } + WALBERLA_ABORT("Unrecognized command line argument found: " << argv[i]); + } + + if( funcTest ) + { + walberla::logging::Logging::instance()->setLogLevel(logging::Logging::LogLevel::WARNING); + } + + if( fileIO ) + { + // create base directory if it does not yet exist + filesystem::path tpath( baseFolder ); + if( !filesystem::exists( tpath ) ) + filesystem::create_directory( tpath ); + } + + //if(adaptionLayerSize > real_t(2)) WALBERLA_LOG_WARNING_ON_ROOT("Adaption layer size is larger than permitted by parallel setup!"); + + ////////////////////////// + // NUMERICAL PARAMETERS // + ////////////////////////// + + real_t viscosity = velocity * diameter / Re; + const real_t omega = lbm::collision_model::omegaFromViscosity(viscosity); + const real_t relaxationTime = real_t(1) / omega; + const real_t omegaBulk = lbm_mesapd_coupling::omegaBulkFromOmega(omega, bulkViscRateFactor); + + const Vector3<uint_t> domainSize( uint_c(domainWidthNonDim * diameter), uint_c(domainWidthNonDim * diameter), uint_c(domainHeightNonDim * diameter) ); + + WALBERLA_LOG_INFO_ON_ROOT("Setup (in simulation, i.e. lattice, units):"); + WALBERLA_LOG_INFO_ON_ROOT(" - domain size = " << domainSize); + WALBERLA_LOG_INFO_ON_ROOT(" - Re = " << Re); + WALBERLA_LOG_INFO_ON_ROOT(" - sphere: diameter = " << diameter << ", constant velocity = " << velocity ); + WALBERLA_LOG_INFO_ON_ROOT(" - relaxation time (tau) = " << relaxationTime << ", omega = " << omega << " kin. visc = " << viscosity ); + WALBERLA_LOG_INFO_ON_ROOT(" - magic number " << magicNumber); + WALBERLA_LOG_INFO_ON_ROOT(" - omegaBulk = " << omegaBulk << ", bulk visc. = " << lbm_mesapd_coupling::bulkViscosityFromOmegaBulk(omegaBulk) << " (bvrf " << bulkViscRateFactor << ")"); + WALBERLA_LOG_INFO_ON_ROOT(" - use omega bulk adaption = " << useOmegaBulkAdaption << " (adaption layer size = " << adaptionLayerSize << ")"); + WALBERLA_LOG_INFO_ON_ROOT(" - reconstructor type = " << reconstructorType ); + + if( vtkIOFreq > 0 ) + { + WALBERLA_LOG_INFO_ON_ROOT(" - writing vtk files to folder \"" << baseFolder << "\" with frequency " << vtkIOFreq); + } + + /////////////////////////// + // BLOCK STRUCTURE SETUP // + /////////////////////////// + + Vector3<uint_t> numberOfBlocksPerDirection( blocksInX, blocksInY, blocksInZ ); + Vector3<uint_t> cellsPerBlockPerDirection( domainSize[0] / numberOfBlocksPerDirection[0], + domainSize[1] / numberOfBlocksPerDirection[1], + domainSize[2] / numberOfBlocksPerDirection[2] ); + for( uint_t i = 0; i < 3; ++i ) { + WALBERLA_CHECK_EQUAL(cellsPerBlockPerDirection[i] * numberOfBlocksPerDirection[i], domainSize[i], + "Unmatching domain decomposition in direction " << i << "!"); + } + + real_t dx = real_t(1); + auto blocks = blockforest::createUniformBlockGrid( numberOfBlocksPerDirection[0], numberOfBlocksPerDirection[1], numberOfBlocksPerDirection[2], + cellsPerBlockPerDirection[0], cellsPerBlockPerDirection[1], cellsPerBlockPerDirection[2], dx, + 0, false, false, + usePeriodicSetup, usePeriodicSetup, true, //periodicity + false ); + + WALBERLA_LOG_INFO_ON_ROOT("Domain decomposition:"); + WALBERLA_LOG_INFO_ON_ROOT(" - blocks per direction = " << numberOfBlocksPerDirection ); + WALBERLA_LOG_INFO_ON_ROOT(" - cells per block = " << cellsPerBlockPerDirection ); + + //write domain decomposition to file + if( vtkIOFreq > 0 ) + { + vtk::writeDomainDecomposition( blocks, "initial_domain_decomposition", baseFolder ); + } + + ////////////////// + // RPD COUPLING // + ////////////////// + + auto rpdDomain = std::make_shared<mesa_pd::domain::BlockForestDomain>(blocks->getBlockForestPointer()); + + //init data structures + auto ps = walberla::make_shared<mesa_pd::data::ParticleStorage>(1); + auto ss = walberla::make_shared<mesa_pd::data::ShapeStorage>(); + using ParticleAccessor_T = mesa_pd::data::ParticleAccessorWithShape; + auto accessor = walberla::make_shared<ParticleAccessor_T >(ps, ss); + + // bounding planes + if(!usePeriodicSetup) createPlaneSetup(ps,ss,blocks->getDomain()); + + // create sphere and store Uid + Vector3<real_t> initialPosition( real_t(0.5) * real_c(domainSize[0]), real_t(0.5) * real_c(domainSize[1]), initialSpherePosition * real_c(domainSize[2])); + auto sphereShape = ss->create<mesa_pd::data::Sphere>( diameter * real_t(0.5) ); + //ss->shapes[sphereShape]->updateMassAndInertia(densityRatio); + + walberla::id_t sphereUid = 0; + if (rpdDomain->isContainedInProcessSubdomain( uint_c(mpi::MPIManager::instance()->rank()), initialPosition )) + { + mesa_pd::data::Particle&& p = *ps->create(); + p.setPosition(initialPosition); + p.setInteractionRadius(diameter * real_t(0.5)); + p.setOwner(mpi::MPIManager::instance()->rank()); + p.setShapeID(sphereShape); + p.setLinearVelocity(Vector3<real_t>(0.0,0.0,velocity)); + sphereUid = p.getUid(); + } + mpi::allReduceInplace(sphereUid, mpi::SUM); + + /////////////////////// + // ADD DATA TO BLOCKS // + //////////////////////// + + // add omega bulk field + BlockDataID omegaBulkFieldID = field::addToStorage<ScalarField_T>( blocks, "omega bulk field", omegaBulk, field::fzyx, uint_t(0) ); + + // create the lattice model +#ifdef USE_TRT_LIKE_LATTICE_MODEL + real_t lambda_e = lbm::collision_model::TRT::lambda_e( omega ); + real_t lambda_d = lbm::collision_model::TRT::lambda_d( omega, magicNumber ); + //LatticeModel_T latticeModel = LatticeModel_T(omegaBulk, lambda_d, lambda_e); + LatticeModel_T latticeModel = LatticeModel_T(omegaBulkFieldID, lambda_d, lambda_e); + //LatticeModel_T latticeModel = LatticeModel_T(lbm::collision_model::D3Q19MRT( omegaBulk, omegaBulk, lambda_d, lambda_e, lambda_e, lambda_d )); +#else + // generated KBC + LatticeModel_T latticeModel = LatticeModel_T(omega); +#endif + + // add PDF field + BlockDataID pdfFieldID = lbm::addPdfFieldToStorage< LatticeModel_T >( blocks, "pdf field (fzyx)", latticeModel, + Vector3< real_t >( real_t(0) ), real_t(1), + uint_t(1), field::fzyx ); + // add flag field + BlockDataID flagFieldID = field::addFlagFieldToStorage<FlagField_T>( blocks, "flag field" ); + + // add particle field + BlockDataID particleFieldID = field::addToStorage<lbm_mesapd_coupling::ParticleField_T>( blocks, "particle field", accessor->getInvalidUid(), field::fzyx, FieldGhostLayers ); + + // add boundary handling + using BoundaryHandling_T = MyBoundaryHandling<ParticleAccessor_T>::Type; + BlockDataID boundaryHandlingID = blocks->addStructuredBlockData< BoundaryHandling_T >(MyBoundaryHandling<ParticleAccessor_T>( flagFieldID, pdfFieldID, particleFieldID, accessor), "boundary handling" ); + + // interpolation functionality + using DensityAdaptor_T = typename lbm::Adaptor< LatticeModel_T >::Density; + BlockDataID densityAdaptorID = field::addFieldAdaptor< DensityAdaptor_T >( blocks, pdfFieldID, "density adaptor" ); + + using DensityInterpolator_T = typename field::TrilinearFieldInterpolator<DensityAdaptor_T, FlagField_T>; + BlockDataID densityInterpolatorID = field::addFieldInterpolator< DensityInterpolator_T, FlagField_T >( blocks, densityAdaptorID, flagFieldID, Fluid_Flag ); + + // set up RPD functionality + std::function<void(void)> syncCall = [ps,rpdDomain, adaptionLayerSize ](){ + const real_t overlap = real_t( 1.5 ) + std::max(real_t(0), adaptionLayerSize - real_t(2)); + mesa_pd::mpi::SyncNextNeighbors syncNextNeighborFunc; + syncNextNeighborFunc(*ps, *rpdDomain, overlap); + }; + + syncCall(); + + mesa_pd::kernel::ExplicitEulerWithShape explEulerIntegrator(real_t(1)/real_t(numRPDSubCycles)); + + mesa_pd::kernel::SpringDashpot collisionResponse(1); + mesa_pd::mpi::ReduceProperty reduceProperty; + + // set up coupling functionality + //Vector3<real_t> gravitationalForce( real_t(0), real_t(0), -(densityRatio - real_t(1)) * gravitationalAcceleration * sphereVolume ); + //lbm_mesapd_coupling::AddForceOnParticlesKernel addGravitationalForce(gravitationalForce); + lbm_mesapd_coupling::AddHydrodynamicInteractionKernel addHydrodynamicInteraction; + lbm_mesapd_coupling::ResetHydrodynamicForceTorqueKernel resetHydrodynamicForceTorque; + lbm_mesapd_coupling::AverageHydrodynamicForceTorqueKernel averageHydrodynamicForceTorque; + + lbm_mesapd_coupling::RegularParticlesSelector sphereSelector; + + lbm_mesapd_coupling::ParticleMappingKernel<BoundaryHandling_T> particleMappingKernel(blocks, boundaryHandlingID); + lbm_mesapd_coupling::MovingParticleMappingKernel<BoundaryHandling_T> movingParticleMappingKernel(blocks, boundaryHandlingID, particleFieldID); + + /////////////// + // TIME LOOP // + /////////////// + + // map planes into the LBM simulation -> act as no-slip boundaries + ps->forEachParticle(false, lbm_mesapd_coupling::GlobalParticlesSelector(), *accessor, particleMappingKernel, *accessor, NoSlip_Flag); + + // map particles into the LBM simulation + ps->forEachParticle(false, sphereSelector, *accessor, movingParticleMappingKernel, *accessor, MO_Flag); + + + // setup of the LBM communication for synchronizing the pdf field between neighboring blocks + + blockforest::communication::UniformBufferedScheme< Stencil_T > optimizedPDFCommunicationScheme( blocks ); + optimizedPDFCommunicationScheme.addPackInfo( make_shared< lbm::PdfFieldPackInfo< LatticeModel_T > >( pdfFieldID ) ); // optimized sync + + blockforest::communication::UniformBufferedScheme< Stencil_T > fullPDFCommunicationScheme( blocks ); + fullPDFCommunicationScheme.addPackInfo( make_shared< field::communication::PackInfo< PdfField_T > >( pdfFieldID ) ); // full sync + + // create the timeloop + + const uint_t timesteps = uint_c(numberOfPasses * real_c(domainSize[2]) / velocity); + WALBERLA_LOG_INFO_ON_ROOT("Running for " << timesteps << " time steps"); + SweepTimeloop timeloop( blocks->getBlockStorage(), timesteps ); + + + timeloop.addFuncBeforeTimeStep( RemainingTimeLogger( timeloop.getNrOfTimeSteps() ), "Remaining Time Logger" ); + + // vtk output + if( vtkIOFreq != uint_t(0) ) + { + // spheres + auto particleVtkOutput = make_shared<mesa_pd::vtk::ParticleVtkOutput>(ps); + particleVtkOutput->addOutput<mesa_pd::data::SelectParticleOwner>("owner"); + particleVtkOutput->addOutput<mesa_pd::data::SelectParticleLinearVelocity>("velocity"); + auto particleVtkWriter = vtk::createVTKOutput_PointData(particleVtkOutput, "Particles", vtkIOFreq, baseFolder, "simulation_step"); + timeloop.addFuncBeforeTimeStep( vtk::writeFiles( particleVtkWriter ), "VTK (sphere data)" ); + + // flag field (written only once in the first time step, ghost layers are also written) + //auto flagFieldVTK = vtk::createVTKOutput_BlockData( blocks, "flag_field", timesteps, FieldGhostLayers, false, baseFolder ); + //flagFieldVTK->addCellDataWriter( make_shared< field::VTKWriter< FlagField_T > >( flagFieldID, "FlagField" ) ); + //timeloop.addFuncBeforeTimeStep( vtk::writeFiles( flagFieldVTK ), "VTK (flag field data)" ); + + // pdf field + auto pdfFieldVTK = vtk::createVTKOutput_BlockData( blocks, "fluid_field", vtkIOFreq, 0, false, baseFolder ); + + pdfFieldVTK->addBeforeFunction( fullPDFCommunicationScheme ); + + field::FlagFieldCellFilter< FlagField_T > fluidFilter( flagFieldID ); + fluidFilter.addFlag( Fluid_Flag ); + pdfFieldVTK->addCellInclusionFilter( fluidFilter ); + + pdfFieldVTK->addCellDataWriter( make_shared< lbm::VelocityVTKWriter< LatticeModel_T, float > >( pdfFieldID, "VelocityFromPDF" ) ); + pdfFieldVTK->addCellDataWriter( make_shared< lbm::DensityVTKWriter < LatticeModel_T, float > >( pdfFieldID, "DensityFromPDF" ) ); + + timeloop.addFuncBeforeTimeStep( vtk::writeFiles( pdfFieldVTK ), "VTK (fluid field data)" ); + + // omega bulk field + timeloop.addFuncBeforeTimeStep( field::createVTKOutput<ScalarField_T, float>( omegaBulkFieldID, *blocks, "omega_bulk_field", vtkIOFreq, uint_t(0), false, baseFolder ), "VTK (omega bulk field)" ); + + } + + // sweep for updating the particle mapping into the LBM simulation + timeloop.add() << Sweep( lbm_mesapd_coupling::makeMovingParticleMapping<PdfField_T, BoundaryHandling_T>(blocks, pdfFieldID, boundaryHandlingID, particleFieldID, accessor, MO_Flag, FormerMO_Flag, sphereSelector, conserveMomentum), "Particle Mapping" ); + + // sweep for restoring PDFs in cells previously occupied by particles + if( reconstructorType == "EAN") + { + + auto sphereNormalExtrapolationDirectionFinder = make_shared<lbm_mesapd_coupling::SphereNormalExtrapolationDirectionFinder>(blocks); + auto equilibriumAndNonEquilibriumSphereNormalReconstructor = lbm_mesapd_coupling::makeEquilibriumAndNonEquilibriumReconstructor<BoundaryHandling_T>(blocks, boundaryHandlingID, sphereNormalExtrapolationDirectionFinder, uint_t(3), true); + auto reconstructionManager = lbm_mesapd_coupling::makePdfReconstructionManager<PdfField_T,BoundaryHandling_T>(blocks, pdfFieldID, boundaryHandlingID, particleFieldID, accessor, FormerMO_Flag, Fluid_Flag, equilibriumAndNonEquilibriumSphereNormalReconstructor, conserveMomentum); + + timeloop.add() << BeforeFunction( fullPDFCommunicationScheme, "PDF Communication" ) + << Sweep( makeSharedSweep(reconstructionManager), "PDF Restore" ); + + } else if( reconstructorType == "Grad") + { + bool recomputeDensity = false; + auto gradReconstructor = lbm_mesapd_coupling::makeGradsMomentApproximationReconstructor<BoundaryHandling_T>(blocks, boundaryHandlingID, omega, recomputeDensity, true, true); + + timeloop.add() << BeforeFunction( fullPDFCommunicationScheme, "PDF Communication" ) + << Sweep( makeSharedSweep(lbm_mesapd_coupling::makePdfReconstructionManager<PdfField_T,BoundaryHandling_T>(blocks, pdfFieldID, boundaryHandlingID, particleFieldID, accessor, FormerMO_Flag, Fluid_Flag, gradReconstructor, conserveMomentum) ), "PDF Restore" ); + } else if( reconstructorType == "Eq") + { + timeloop.add() << Sweep( makeSharedSweep(lbm_mesapd_coupling::makePdfReconstructionManager<PdfField_T,BoundaryHandling_T>(blocks, pdfFieldID, boundaryHandlingID, particleFieldID, accessor, FormerMO_Flag, Fluid_Flag, conserveMomentum) ), "PDF Restore" ); + } else if( reconstructorType == "Ext") + { + auto sphereNormalExtrapolationDirectionFinder = make_shared<lbm_mesapd_coupling::SphereNormalExtrapolationDirectionFinder>(blocks); + auto extrapolationReconstructor = lbm_mesapd_coupling::makeExtrapolationReconstructor<BoundaryHandling_T, lbm_mesapd_coupling::SphereNormalExtrapolationDirectionFinder, true>(blocks, boundaryHandlingID, sphereNormalExtrapolationDirectionFinder, uint_t(3), true); + timeloop.add() << BeforeFunction( fullPDFCommunicationScheme, "LBM Communication" ) + << Sweep( makeSharedSweep(lbm_mesapd_coupling::makePdfReconstructionManager<PdfField_T,BoundaryHandling_T>(blocks, pdfFieldID, boundaryHandlingID, particleFieldID, accessor, FormerMO_Flag, Fluid_Flag, extrapolationReconstructor, conserveMomentum)), "PDF Restore" ); + } else { + WALBERLA_ABORT("Unknown reconstructor type " << reconstructorType); + } + + // update bulk omega in all cells to adapt to changed particle position + if(useOmegaBulkAdaption) + { + using OmegaBulkAdapter_T = lbm_mesapd_coupling::OmegaBulkAdapter<ParticleAccessor_T, decltype(sphereSelector)>; + real_t defaultOmegaBulk = lbm_mesapd_coupling::omegaBulkFromOmega(omega, real_t(1)); + shared_ptr<OmegaBulkAdapter_T> omegaBulkAdapter = make_shared<OmegaBulkAdapter_T>(blocks, omegaBulkFieldID, accessor, defaultOmegaBulk, omegaBulk, adaptionLayerSize, sphereSelector); + timeloop.add() << Sweep( makeSharedSweep(omegaBulkAdapter), "Omega Bulk Adapter"); + } + + + bool useStreamCollide = true; + auto bhSweep = BoundaryHandling_T::getBlockSweep( boundaryHandlingID ); + + // generated sweeps + auto lbmSweep = LatticeModel_T::Sweep( pdfFieldID ); + if( useStreamCollide ) + { + // add LBM communication function and boundary handling sweep (does the hydro force calculations and the no-slip treatment) + timeloop.add() << BeforeFunction( optimizedPDFCommunicationScheme, "LBM Communication" ) + << Sweep(bhSweep, "Boundary Handling" ); + + // stream + collide LBM step + timeloop.add() << Sweep( lbmSweep, "LB sweep" ); + } + else + { + // collide LBM step + timeloop.add() << Sweep([&lbmSweep](IBlock * const block){lbmSweep.collide(block);}, "cell-wise collide LB sweep" ); + + // add LBM communication function and boundary handling sweep (does the hydro force calculations and the no-slip treatment) + timeloop.add() << BeforeFunction( optimizedPDFCommunicationScheme, "LBM Communication" ) + << Sweep( BoundaryHandling_T::getBlockSweep( boundaryHandlingID ), "Boundary Handling" ); + + // stream LBM step + timeloop.add() << Sweep([&lbmSweep](IBlock * const block){lbmSweep.stream(block);}, "cell-wise stream LB sweep" ); + } + + // evaluation functionality + std::string loggingFileName( baseFolder + "/LoggingPrescribedMovingSphere"); + loggingFileName += "_Re" + std::to_string(uint_c(Re)); + loggingFileName += "_D" + std::to_string(uint_c(diameter)); + loggingFileName += "_vel" + std::to_string(float(velocity)); + loggingFileName += "_recon" + reconstructorType; + loggingFileName += "_bvrf" + std::to_string(uint_c(bulkViscRateFactor)); + loggingFileName += "_mn" + std::to_string(float(magicNumber)); + if( useOmegaBulkAdaption ) loggingFileName += "_uOBA" + std::to_string(uint_c(adaptionLayerSize)); + if( conserveMomentum ) loggingFileName += "_conserveMomentum"; + if( artificiallyAccelerateSphere ) loggingFileName += "_acc"; + if( !fileNameEnding.empty()) loggingFileName += "_" + fileNameEnding; + //loggingFileName += "_SBB"; + loggingFileName += ".txt"; + + if( fileIO ) + { + WALBERLA_LOG_INFO_ON_ROOT(" - writing logging output to file \"" << loggingFileName << "\""); + } + SpherePropertyLogger<ParticleAccessor_T > logger( accessor, sphereUid, loggingFileName, fileIO, diameter, velocity, real_t(3) * math::pi * diameter * viscosity * velocity); + + + //////////////////////// + // EXECUTE SIMULATION // + //////////////////////// + + WcTimingPool timeloopTiming; + + const bool useOpenMP = false; + + const real_t densityRatio = real_t(7800) / real_t(935); + const real_t responseTime = densityRatio * diameter * diameter / ( real_t(18) * viscosity ); + WALBERLA_LOG_INFO_ON_ROOT(" - response time " << responseTime); + const real_t accelerationFactor = real_t(1) / (real_t(0.1) * responseTime); + + // time loop + for (uint_t i = 0; i < timesteps; ++i ) + { + // perform a single simulation step -> this contains LBM and setting of the hydrodynamic interactions + timeloop.singleStep( timeloopTiming ); + + timeloopTiming["RPD"].start(); + + if( averageForceTorqueOverTwoTimSteps && i!= 0) + { + ps->forEachParticle(useOpenMP, mesa_pd::kernel::SelectAll(), *accessor, averageHydrodynamicForceTorque, *accessor ); + } + + reduceProperty.operator()<mesa_pd::ForceTorqueNotification>(*ps); + + ps->forEachParticle(useOpenMP, mesa_pd::kernel::SelectLocal(), *accessor, explEulerIntegrator, *accessor); + syncCall(); + + timeloopTiming["RPD"].end(); + + if( artificiallyAccelerateSphere ) + { + // accelerating after reading from a checkpoint file does not make sense, as current actual runtime is not known + real_t newSphereVel = velocity * (exp(-accelerationFactor * real_t(i) ) - real_t(1)); + ps->forEachParticle(useOpenMP, sphereSelector, *accessor, [newSphereVel](const size_t idx, ParticleAccessor_T& ac){ ac.setLinearVelocity(idx, Vector3<real_t>(real_t(0), real_t(0), newSphereVel));}, *accessor); + } + + // evaluation + timeloopTiming["Logging"].start(); + + logger(); + + real_t density1 = real_t(0); + real_t density2 = real_t(0); + real_t averageDensity = real_t(0); + if(logDensity) + { + density1 = getDensityAtPosition<DensityInterpolator_T >(blocks, densityInterpolatorID, Vector3<real_t>(real_c(domainSize[0])*real_t(0.25), real_c(domainSize[1])*real_t(0.5), real_c(domainSize[2])*real_t(0.5))); + density2 = getDensityAtPosition<DensityInterpolator_T >(blocks, densityInterpolatorID, Vector3<real_t>(real_c(domainSize[0])*real_t(0.75), real_c(domainSize[1])*real_t(0.5), logger.getPosition())); + averageDensity = getAverageDensityInSystem<BoundaryHandling_T>(blocks, pdfFieldID, boundaryHandlingID); + } + logger.writeToFile(i, density1, density2, averageDensity); + timeloopTiming["Logging"].end(); + + // reset after logging here + ps->forEachParticle(useOpenMP, mesa_pd::kernel::SelectAll(), *accessor, resetHydrodynamicForceTorque, *accessor ); + + } + + timeloopTiming.logResultOnRoot(); + + if(vtkOutputAtEnd) + { + std::string vtkFileName = "fluid_field"; + vtkFileName += "_bvrf" + std::to_string(uint_c(bulkViscRateFactor)); + if( useOmegaBulkAdaption ) vtkFileName += "_uOBA" + std::to_string(uint_c(adaptionLayerSize)); + + auto pdfFieldVTK = vtk::createVTKOutput_BlockData( blocks, vtkFileName, uint_t(1), 0, false, baseFolder ); + + pdfFieldVTK->addBeforeFunction( fullPDFCommunicationScheme ); + + AABB sliceAABB( real_t(0), real_c(domainSize[1])*real_t(0.5)-real_t(1), real_t(0), + real_c(domainSize[0]), real_c(domainSize[1])*real_t(0.5)+real_t(1), real_c(domainSize[2]) ); + vtk::AABBCellFilter aabbSliceFilter( sliceAABB ); + + field::FlagFieldCellFilter< FlagField_T > fluidFilter( flagFieldID ); + fluidFilter.addFlag( Fluid_Flag ); + + vtk::ChainedFilter combinedSliceFilter; + combinedSliceFilter.addFilter( fluidFilter ); + combinedSliceFilter.addFilter( aabbSliceFilter ); + + pdfFieldVTK->addCellInclusionFilter( combinedSliceFilter ); + + pdfFieldVTK->addCellDataWriter( make_shared< lbm::VelocityVTKWriter< LatticeModel_T, float > >( pdfFieldID, "VelocityFromPDF" ) ); + pdfFieldVTK->addCellDataWriter( make_shared< lbm::DensityVTKWriter < LatticeModel_T, float > >( pdfFieldID, "DensityFromPDF" ) ); + + vtk::writeFiles(pdfFieldVTK)(); + } + + + return EXIT_SUCCESS; +} + +} // namespace sphere_moving_with_prescribed_velocity + +int main( int argc, char **argv ){ + sphere_moving_with_prescribed_velocity::main(argc, argv); +} diff --git a/apps/benchmarks/FluidParticleCoupling/SphereWallCollision.cpp b/apps/benchmarks/FluidParticleCoupling/SphereWallCollision.cpp new file mode 100644 index 0000000000000000000000000000000000000000..61a3844f14a27fc0c6bfc89e953a419f6653ed11 --- /dev/null +++ b/apps/benchmarks/FluidParticleCoupling/SphereWallCollision.cpp @@ -0,0 +1,1538 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla is free software: you can +// redistribute it and/or modify it under the terms of the GNU General Public +// License as published by the Free Software Foundation, either version 3 of +// the License, or (at your option) any later version. +// +// waLBerla is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +// for more details. +// +// You should have received a copy of the GNU General Public License along +// with waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file SphereWallCollision.cpp +//! \ingroup lbm_mesapd_coupling +//! \author Christoph Rettinger <christoph.rettinger@fau.de> +// +//====================================================================================================================== + +#include "blockforest/Initialization.h" +#include "blockforest/communication/UniformBufferedScheme.h" + +#include "boundary/all.h" + +#include "core/waLBerlaBuildInfo.h" +#include "core/DataTypes.h" +#include "core/Environment.h" +#include "core/SharedFunctor.h" +#include "core/debug/Debug.h" +#include "core/debug/TestSubsystem.h" +#include "core/math/all.h" +#include "core/timing/RemainingTimeLogger.h" +#include "core/logging/all.h" +#include "core/mpi/Broadcast.h" +#include "core/mpi/Reduce.h" + +#include "domain_decomposition/SharedSweep.h" + +#include "field/AddToStorage.h" +#include "field/adaptors/AdaptorCreators.h" +#include "field/communication/PackInfo.h" +#include "field/interpolators/FieldInterpolatorCreators.h" +#include "field/interpolators/NearestNeighborFieldInterpolator.h" + +#include "lbm/boundary/all.h" +#include "lbm/communication/PdfFieldPackInfo.h" +#include "lbm/field/AddToStorage.h" +#include "lbm/field/Adaptors.h" +#include "lbm/field/PdfField.h" +#include "lbm/lattice_model/D3Q19.h" +#include "lbm/sweeps/CellwiseSweep.h" +#include "lbm/sweeps/SweepWrappers.h" + +#include "lbm_mesapd_coupling/mapping/ParticleMapping.h" +#include "lbm_mesapd_coupling/momentum_exchange_method/MovingParticleMapping.h" +#include "lbm_mesapd_coupling/momentum_exchange_method/boundary/SimpleBB.h" +#include "lbm_mesapd_coupling/momentum_exchange_method/boundary/CurvedLinear.h" +#include "lbm_mesapd_coupling/momentum_exchange_method/reconstruction/Reconstructor.h" +#include "lbm_mesapd_coupling/momentum_exchange_method/reconstruction/ExtrapolationDirectionFinder.h" +#include "lbm_mesapd_coupling/momentum_exchange_method/reconstruction/PdfReconstructionManager.h" +#include "lbm_mesapd_coupling/utility/AddForceOnParticlesKernel.h" +#include "lbm_mesapd_coupling/utility/ParticleSelector.h" +#include "lbm_mesapd_coupling/DataTypes.h" +#include "lbm_mesapd_coupling/utility/AverageHydrodynamicForceTorqueKernel.h" +#include "lbm_mesapd_coupling/utility/AddHydrodynamicInteractionKernel.h" +#include "lbm_mesapd_coupling/utility/ResetHydrodynamicForceTorqueKernel.h" +#include "lbm_mesapd_coupling/utility/LubricationCorrectionKernel.h" +#include "lbm_mesapd_coupling/utility/OmegaBulkAdaption.h" + +#include "mesa_pd/collision_detection/AnalyticContactDetection.h" +#include "mesa_pd/data/ParticleAccessorWithShape.h" +#include "mesa_pd/data/ParticleStorage.h" +#include "mesa_pd/data/ShapeStorage.h" +#include "mesa_pd/data/DataTypes.h" +#include "mesa_pd/data/shape/HalfSpace.h" +#include "mesa_pd/data/shape/Sphere.h" +#include "mesa_pd/domain/BlockForestDomain.h" +#include "mesa_pd/domain/BlockForestDataHandling.h" +#include "mesa_pd/kernel/DoubleCast.h" +#include "mesa_pd/kernel/ExplicitEulerWithShape.h" +#include "mesa_pd/kernel/LinearSpringDashpot.h" +#include "mesa_pd/kernel/NonLinearSpringDashpot.h" +#include "mesa_pd/kernel/ParticleSelector.h" +#include "mesa_pd/kernel/VelocityVerletWithShape.h" +#include "mesa_pd/mpi/SyncNextNeighbors.h" +#include "mesa_pd/mpi/ReduceProperty.h" +#include "mesa_pd/mpi/ReduceContactHistory.h" +#include "mesa_pd/mpi/ContactFilter.h" +#include "mesa_pd/mpi/notifications/ForceTorqueNotification.h" +#include "mesa_pd/vtk/ParticleVtkOutput.h" + +#include "timeloop/SweepTimeloop.h" + +#include "vtk/all.h" +#include "field/vtk/all.h" +#include "lbm/vtk/all.h" + +#include "GeneratedLBM.h" + +#include <functional> + +#define USE_TRT_LIKE_LATTICE_MODEL + +namespace sphere_wall_collision +{ + +/////////// +// USING // +/////////// + +using namespace walberla; +using walberla::uint_t; + +using LatticeModel_T = lbm::GeneratedLBM; + +using Stencil_T = LatticeModel_T::Stencil; +using PdfField_T = lbm::PdfField<LatticeModel_T>; + +using flag_t = walberla::uint8_t; +using FlagField_T = FlagField<flag_t>; + +using ScalarField_T = GhostLayerField< real_t, 1>; + +const uint_t FieldGhostLayers = 1; + +/////////// +// FLAGS // +/////////// + +const FlagUID Fluid_Flag( "fluid" ); +const FlagUID NoSlip_Flag( "no slip" ); +const FlagUID MO_Flag( "moving obstacle" ); +const FlagUID FormerMO_Flag( "former moving obstacle" ); +const FlagUID SimplePressure_Flag("simple pressure"); + +///////////////////////////////////// +// BOUNDARY HANDLING CUSTOMIZATION // +///////////////////////////////////// +template <typename ParticleAccessor_T> +class MyBoundaryHandling +{ +public: + + using NoSlip_T = lbm::NoSlip< LatticeModel_T, flag_t >; + using MO_T = lbm_mesapd_coupling::CurvedLinear< LatticeModel_T, FlagField_T, ParticleAccessor_T >; + using SimplePressure_T = lbm::SimplePressure< LatticeModel_T, flag_t >; + using Type = BoundaryHandling< FlagField_T, Stencil_T, NoSlip_T, MO_T, SimplePressure_T >; + + MyBoundaryHandling( const BlockDataID & flagFieldID, const BlockDataID & pdfFieldID, + const BlockDataID & particleFieldID, const shared_ptr<ParticleAccessor_T>& ac, + bool applyOutflowBCAtTop) : + flagFieldID_( flagFieldID ), pdfFieldID_( pdfFieldID ), particleFieldID_( particleFieldID ), ac_( ac ), applyOutflowBCAtTop_(applyOutflowBCAtTop) {} + + Type * operator()( IBlock* const block, const StructuredBlockStorage* const storage ) const + { + WALBERLA_ASSERT_NOT_NULLPTR( block ); + WALBERLA_ASSERT_NOT_NULLPTR( storage ); + + auto * flagField = block->getData< FlagField_T >( flagFieldID_ ); + auto * pdfField = block->getData< PdfField_T > ( pdfFieldID_ ); + auto * particleField = block->getData< lbm_mesapd_coupling::ParticleField_T > ( particleFieldID_ ); + + const auto fluid = flagField->flagExists( Fluid_Flag ) ? flagField->getFlag( Fluid_Flag ) : flagField->registerFlag( Fluid_Flag ); + + Type * handling = new Type( "moving obstacle boundary handling", flagField, fluid, + NoSlip_T( "NoSlip", NoSlip_Flag, pdfField ), + MO_T( "MO", MO_Flag, pdfField, flagField, particleField, ac_, fluid, *storage, *block ), + SimplePressure_T( "SimplePressure", SimplePressure_Flag, pdfField, real_t(1) ) ); + + if(applyOutflowBCAtTop_) + { + const auto simplePressure = flagField->getFlag( SimplePressure_Flag ); + + CellInterval domainBB = storage->getDomainCellBB(); + domainBB.xMin() -= cell_idx_c( FieldGhostLayers ); + domainBB.xMax() += cell_idx_c( FieldGhostLayers ); + + domainBB.yMin() -= cell_idx_c( FieldGhostLayers ); + domainBB.yMax() += cell_idx_c( FieldGhostLayers ); + + domainBB.zMin() -= cell_idx_c( FieldGhostLayers ); + domainBB.zMax() += cell_idx_c( FieldGhostLayers ); + + // TOP + CellInterval top( domainBB.xMin(), domainBB.yMin(), domainBB.zMax(), domainBB.xMax(), domainBB.yMax(), domainBB.zMax() ); + storage->transformGlobalToBlockLocalCellInterval( top, *block ); + handling->forceBoundary( simplePressure, top ); + } + + handling->fillWithDomain( FieldGhostLayers ); + + return handling; + } + +private: + + const BlockDataID flagFieldID_; + const BlockDataID pdfFieldID_; + const BlockDataID particleFieldID_; + + shared_ptr<ParticleAccessor_T> ac_; + + bool applyOutflowBCAtTop_; +}; +//******************************************************************************************************************* + +//******************************************************************************************************************* +/*!\brief Evaluating the position and velocity of the sphere + * + */ +//******************************************************************************************************************* +template< typename ParticleAccessor_T> +class SpherePropertyLogger +{ +public: + SpherePropertyLogger( const shared_ptr< ParticleAccessor_T > & ac, walberla::id_t sphereUid, + const std::string & fileName, bool fileIO, + real_t diameter, real_t uIn, uint_t numberOfSubSteps) : + ac_( ac ), sphereUid_( sphereUid ), fileName_( fileName ), fileIO_(fileIO), + diameter_( diameter ), uIn_( uIn ), numberOfSubSteps_(numberOfSubSteps), + gap_( real_t(0) ), settlingVelocity_( real_t(0) ) + { + if ( fileIO_ ) + { + WALBERLA_ROOT_SECTION() + { + std::ofstream file; + file.open( fileName_.c_str() ); + file << "#\t t/tref\t gapSize\t gapSize/D\t velZ\t velZ/uIn\n"; + file.close(); + } + } + } + + void operator()(const uint_t timeStep, const uint_t subStep ) + { + real_t curTimestep = real_c(timeStep) + real_c(subStep) / real_c(numberOfSubSteps_); + + Vector3<real_t> pos(real_t(0)); + Vector3<real_t> transVel(real_t(0)); + + size_t idx = ac_->uidToIdx(sphereUid_); + if( idx != std::numeric_limits<size_t>::max()) + { + if(!mesa_pd::data::particle_flags::isSet( ac_->getFlags(idx), mesa_pd::data::particle_flags::GHOST)) + { + pos = ac_->getPosition(idx); + transVel = ac_->getLinearVelocity(idx); + } + } + + WALBERLA_MPI_SECTION() + { + mpi::allReduceInplace( pos[0], mpi::SUM ); + mpi::allReduceInplace( pos[1], mpi::SUM ); + mpi::allReduceInplace( pos[2], mpi::SUM ); + mpi::allReduceInplace( transVel[2], mpi::SUM ); + } + + position_ = pos; + gap_ = pos[2] - real_t(0.5) * diameter_; + settlingVelocity_ = transVel[2]; + + WALBERLA_ROOT_SECTION() + { + // coarsen the output to the result file + if(subStep == 0) + { + gapLogging_.push_back(gap_); + velocityLogging_.push_back(settlingVelocity_); + } + } + + if( fileIO_ ) + writeToFile( curTimestep, gap_, settlingVelocity_); + } + + real_t getGapSize() const + { + return gap_; + } + + real_t getSettlingVelocity() const + { + return settlingVelocity_; + } + + Vector3<real_t> getPosition() const + { + return position_; + } + + void writeResult(std::string filename, uint_t tImpact) + { + WALBERLA_ROOT_SECTION() + { + WALBERLA_CHECK_EQUAL(gapLogging_.size(), velocityLogging_.size()); + std::ofstream file; + file.open( filename.c_str()); + + file << "#\t t\t t/tref\t gapSize\t gapSize/D\t velZ\t velZ/uIn\n"; + real_t tref = diameter_ / uIn_; + + for( uint_t i = uint_t(0); i < gapLogging_.size(); ++i) + { + int timestep = int(i) - int(tImpact); + file << timestep << "\t" << real_c(timestep) / tref<< "\t" + << "\t" << gapLogging_[i] << "\t" << gapLogging_[i] / diameter_ + << "\t" << velocityLogging_[i] << "\t" << velocityLogging_[i] / uIn_ + << "\n"; + } + + file.close(); + } + } + + +private: + void writeToFile( real_t timestep, real_t gap, real_t velocity ) + { + WALBERLA_ROOT_SECTION() + { + std::ofstream file; + file.open( fileName_.c_str(), std::ofstream::app ); + + real_t tref = diameter_ / uIn_; + + file << timestep << "\t" << timestep / tref<< "\t" + << "\t" << gap << "\t" << gap / diameter_ + << "\t" << velocity << "\t" << velocity / uIn_ + << "\n"; + file.close(); + } + } + + + shared_ptr< ParticleAccessor_T > ac_; + const walberla::id_t sphereUid_; + std::string fileName_; + bool fileIO_; + real_t diameter_, uIn_; + uint_t numberOfSubSteps_; + + real_t gap_, settlingVelocity_; + Vector3<real_t> position_; + + std::vector<real_t> gapLogging_, velocityLogging_; + +}; + + +void createPlaneSetup(const shared_ptr<mesa_pd::data::ParticleStorage> & ps, const shared_ptr<mesa_pd::data::ShapeStorage> & ss, const math::AABB & simulationDomain, bool applyOutflowBCAtTop ) +{ + // create bounding planes + mesa_pd::data::Particle&& p0 = *ps->create(true); + p0.setPosition(simulationDomain.minCorner()); + p0.setShapeID(ss->create<mesa_pd::data::HalfSpace>( Vector3<real_t>(0,0,1) )); + p0.setOwner(mpi::MPIManager::instance()->rank()); + p0.setType(0); + mesa_pd::data::particle_flags::set(p0.getFlagsRef(), mesa_pd::data::particle_flags::INFINITE); + mesa_pd::data::particle_flags::set(p0.getFlagsRef(), mesa_pd::data::particle_flags::FIXED); + + if(!applyOutflowBCAtTop) + { + //only create top plane when no outflow BC should be set there + mesa_pd::data::Particle&& p1 = *ps->create(true); + p1.setPosition(simulationDomain.maxCorner()); + p1.setShapeID(ss->create<mesa_pd::data::HalfSpace>( Vector3<real_t>(0,0,-1) )); + p1.setOwner(mpi::MPIManager::instance()->rank()); + p1.setType(0); + mesa_pd::data::particle_flags::set(p1.getFlagsRef(), mesa_pd::data::particle_flags::INFINITE); + mesa_pd::data::particle_flags::set(p1.getFlagsRef(), mesa_pd::data::particle_flags::FIXED); + } + +} + +struct NonCollidingSphereSelector +{ + explicit NonCollidingSphereSelector( real_t radius ) : radius_(radius) {} + + template< typename ParticleAccessor_T > + bool inline operator()(const size_t particleIdx, const ParticleAccessor_T & ac) const + { + static_assert(std::is_base_of<mesa_pd::data::IAccessor, ParticleAccessor_T>::value, "Provide a valid accessor as template"); + return !mesa_pd::data::particle_flags::isSet( ac.getFlags(particleIdx), mesa_pd::data::particle_flags::FIXED) && + !mesa_pd::data::particle_flags::isSet( ac.getFlags(particleIdx), mesa_pd::data::particle_flags::GLOBAL) && + ac.getPosition(particleIdx)[2] > radius_; + } +private: + real_t radius_; +}; + + +class ForcesOnSphereLogger +{ +public: + ForcesOnSphereLogger( const std::string & fileName, bool fileIO, real_t weightForce, uint_t numberOfSubSteps ) : + fileName_( fileName ), fileIO_( fileIO ), weightForce_( weightForce ), numberOfSubSteps_( numberOfSubSteps ) + { + if ( fileIO_ ) + { + WALBERLA_ROOT_SECTION() + { + std::ofstream file; + file.open( fileName_.c_str() ); + file << "#t\t BuoyAndGravF\t HydInteractionF\t LubricationF\t CollisionForce\n"; + file.close(); + } + } + } + + void operator()(const uint_t timeStep, const uint_t subStep, real_t hydForce, real_t lubForce, real_t collisionForce ) + { + real_t curTimestep = real_c(timeStep) + real_c(subStep) / real_c(numberOfSubSteps_); + + if ( fileIO_ ) + { + WALBERLA_ROOT_SECTION() + { + std::ofstream file; + file.open( fileName_.c_str(), std::ofstream::app ); + + file << std::setprecision(10) << curTimestep << "\t" << weightForce_ << "\t" << hydForce << "\t" << lubForce << "\t" << collisionForce << "\n"; + file.close(); + } + } + + } + + +private: + + std::string fileName_; + bool fileIO_; + + real_t weightForce_; + uint_t numberOfSubSteps_; +}; + +template<typename ParticleAccessor_T> +real_t getForce(walberla::id_t uid, ParticleAccessor_T & ac) +{ + auto idx = ac.uidToIdx(uid); + real_t force = real_t(0); + if(idx != ac.getInvalidIdx()) + { + force = ac.getForce(idx)[2]; + } + WALBERLA_MPI_SECTION() + { + mpi::allReduceInplace( force, mpi::SUM ); + } + return force; +} + + +real_t getAcceleratedSphereVelocity(real_t accelerationRate, uint_t timestep, real_t tref, real_t finalVelocity) +{ + return finalVelocity * (std::exp(-real_c(timestep)/(tref*accelerationRate))-real_t(1)); +} + +template< typename DensityInterpolator_T> +void logDensityToFile(const shared_ptr<StructuredBlockStorage> & blocks, BlockDataID densityInterpolatorID, real_t surfaceDistance, + Vector3<real_t> spherePosition, real_t diameter, std::string fileName, uint_t timestep, real_t averageDensityInSystem ) +{ + real_t evalRadius = real_t(0.5) * diameter + surfaceDistance; + std::vector< Vector3<real_t> > evaluationPositions; + evaluationPositions.push_back(spherePosition + Vector3<real_t>(real_t(0), real_t(0), evalRadius)); + evaluationPositions.push_back(spherePosition + Vector3<real_t>(evalRadius / sqrt(real_t(2)), real_t(0), evalRadius / sqrt(real_t(2)))); + evaluationPositions.push_back(spherePosition + Vector3<real_t>(evalRadius, real_t(0), real_t(0))); + evaluationPositions.push_back(spherePosition + Vector3<real_t>(evalRadius / sqrt(real_t(2)), real_t(0), -evalRadius / sqrt(real_t(2)))); + evaluationPositions.push_back(spherePosition + Vector3<real_t>(real_t(0), real_t(0), -evalRadius)); + + std::vector<real_t> densityAtPos(evaluationPositions.size(), real_t(0)); + + for( auto & block: *blocks) + { + auto densityInterpolator = block.getData<DensityInterpolator_T>(densityInterpolatorID); + for(auto i = uint_t(0); i < evaluationPositions.size(); ++i) + { + auto pos = evaluationPositions[i]; + if(block.getAABB().contains(pos)) + { + densityInterpolator->get(pos, &densityAtPos[i]); + } + } + } + + for(auto i = uint_t(0); i < densityAtPos.size(); ++i) + { + // reduce to root + mpi::reduceInplace(densityAtPos[i], mpi::SUM); + } + + WALBERLA_ROOT_SECTION() + { + std::ofstream file; + file.open( fileName.c_str(), std::ofstream::app ); + + file << std::setprecision(8) << timestep << "\t" << averageDensityInSystem; + for(auto density: densityAtPos) + { + file << std::setprecision(8) << "\t" << density; + } + file << "\n"; + file.close(); + } +} + +template< typename BoundaryHandling_T> +real_t getAverageDensityInSystem(const shared_ptr<StructuredBlockStorage> & blocks, BlockDataID pdfFieldID, BlockDataID boundaryHandlingID) +{ + real_t totalMass = real_t(0); + uint_t count = uint_t(0); + for( auto & block: *blocks) + { + auto pdfField = block.getData<PdfField_T>(pdfFieldID); + auto boundaryHandling = block.getData<BoundaryHandling_T>(boundaryHandlingID); + WALBERLA_FOR_ALL_CELLS_XYZ(pdfField, + if(boundaryHandling->isDomain(x,y,z)) + { + totalMass += pdfField->getDensity(x,y,z); + ++count; + } + ); + } + + // reduce to root + mpi::reduceInplace(totalMass, mpi::SUM); + mpi::reduceInplace(count, mpi::SUM); + + return totalMass/real_c(count); +} + + +////////// +// MAIN // +////////// + +//******************************************************************************************************************* +/*!\brief PHYSICAL Test case of a sphere settling and colliding with a wall submerged in a viscous fluid. + * + * The trajectory of the bouncing sphere is logged and compared to reference experiments. + * + * for experiments see: Gondret, Lance, Petit - "Bouncing motion of spherical particles in fluids" 2002 + * for simulations see e.g.: Biegert, Vowinckel, Meiburg - "A collision model for grain-resolving simulations of flows over + * dense, mobile, polydisperse granular sediment beds" 2017 + */ +//******************************************************************************************************************* + + +int main( int argc, char **argv ) +{ + Environment env( argc, argv ); + + if( !env.config() ) + { + WALBERLA_ABORT("Usage: " << argv[0] << " path-to-configuration-file \n"); + } + + Config::BlockHandle simulationInput = env.config()->getBlock( "BouncingSphere" ); + + // setup description from separate block + const std::string setup = simulationInput.getParameter<std::string>("setup"); + Config::BlockHandle setupDescription = env.config()->getBlock( setup ); + + const real_t Re = setupDescription.getParameter<real_t>("Re"); + const real_t densityFluid_SI = setupDescription.getParameter<real_t>("densityFluid_SI"); + const real_t dynamicViscosityFluid_SI = setupDescription.getParameter<real_t>("dynamicViscosityFluid_SI"); + const real_t densitySphere_SI = setupDescription.getParameter<real_t>("densitySphere_SI"); + const real_t diameter_SI = setupDescription.getParameter<real_t>("diameter_SI"); + const real_t gravitationalAcceleration_SI = setupDescription.getParameter<real_t>("gravitationalAcceleration_SI"); + const real_t restitutionCoeff = setupDescription.getParameter<real_t>("restitutionCoeff"); + const uint_t numberOfBouncesUntilTermination = setupDescription.getParameter<uint_t>("numberOfBouncesUntilTermination"); + const Vector3<real_t> domainSizeNonDim = setupDescription.getParameter<Vector3<real_t> >("domainSize"); + const Vector3<uint_t> numberOfBlocksPerDirection = setupDescription.getParameter<Vector3<uint_t> >("numberOfBlocksPerDirection"); + + // numerical parameters + + // control + const bool randomizeInitialSpherePosition = simulationInput.getParameter<bool>("randomizeInitialSpherePosition"); + const bool initializeSphereVelocity = simulationInput.getParameter<bool>("initializeSphereVelocity"); + const bool applyOutflowBCAtTop = simulationInput.getParameter<bool>("applyOutflowBCAtTop"); + bool artificiallyAccelerateSphere = simulationInput.getParameter<bool>("artificiallyAccelerateSphere"); + + // LBM + const real_t uIn = simulationInput.getParameter<real_t>("uIn"); + const real_t diameter = simulationInput.getParameter<real_t>("diameter"); + const real_t magicNumber = simulationInput.getParameter<real_t>("magicNumber"); + const real_t bulkViscRateFactor = simulationInput.getParameter<real_t>("bulkViscRateFactor"); + const bool useOmegaBulkAdaption = simulationInput.getParameter<bool>("useOmegaBulkAdaption"); + + const uint_t numRPDSubCycles = simulationInput.getParameter<uint_t>("numRPDSubCycles"); + const bool useLubricationCorrection = simulationInput.getParameter<bool>("useLubricationCorrection"); + const real_t lubricationCutOffDistance = simulationInput.getParameter<real_t>("lubricationCutOffDistance"); + const real_t lubricationMinimalGapSizeNonDim = simulationInput.getParameter<real_t>("lubricationMinimalGapSizeNonDim"); + const bool useVelocityVerlet = simulationInput.getParameter<bool>("useVelocityVerlet"); + + // Collision Response + const real_t collisionTime = simulationInput.getParameter<real_t>("collisionTime"); + const real_t StCrit = simulationInput.getParameter<real_t>("StCrit"); + const bool useACTM = simulationInput.getParameter<bool>("useACTM"); + + const bool averageForceTorqueOverTwoTimeSteps = simulationInput.getParameter<bool>("averageForceTorqueOverTwoTimeSteps"); + const bool conserveMomentum = simulationInput.getParameter<bool>("conserveMomentum"); + const bool disableFluidForceDuringContact = simulationInput.getParameter<bool>("disableFluidForceDuringContact"); + const std::string reconstructorType = simulationInput.getParameter<std::string>("reconstructorType"); + + const bool fileIO = simulationInput.getParameter<bool>("fileIO"); + const uint_t vtkIOFreq = simulationInput.getParameter<uint_t>("vtkIOFreq"); + const std::string baseFolder = simulationInput.getParameter<std::string>("baseFolder"); + bool vtkOutputOnCollision = simulationInput.getParameter<bool>("vtkOutputOnCollision"); + + bool writeCheckPointFile = simulationInput.getParameter<bool>("writeCheckPointFile", false); + bool readFromCheckPointFile = simulationInput.getParameter<bool>("readFromCheckPointFile", false); + + WALBERLA_ROOT_SECTION() + { + if( fileIO ) + { + // create base directory if it does not yet exist + filesystem::path tpath( baseFolder ); + if( !filesystem::exists( tpath ) ) + filesystem::create_directory( tpath ); + } + } + + + ////////////////////////////////////// + // SIMULATION PROPERTIES in SI units// + ////////////////////////////////////// + + const real_t densityRatio = densitySphere_SI / densityFluid_SI; + const real_t kinematicViscosityFluid_SI = dynamicViscosityFluid_SI / densityFluid_SI; + // here, we do the analogy with experiments via the Galileo number which is a-priori well-defined in contrast to the Stokes number which is a result of the simulation + // -> no, this is not a good idea as the sphere in the experiments has in some cases not yet reached the terminal settling velocity + // thus, it is better to pose the reported resulting Re as an input parameter + const real_t uIn_SI = Re * kinematicViscosityFluid_SI / diameter_SI; + const real_t ug_SI = std::sqrt((densityRatio-real_t(1)) * gravitationalAcceleration_SI * diameter_SI); + const real_t Ga = ug_SI * diameter_SI / kinematicViscosityFluid_SI; + + ////////////////////////// + // NUMERICAL PARAMETERS // + ////////////////////////// + + const real_t dx_SI = diameter_SI / diameter; + const real_t sphereVolume = math::pi / real_t(6) * diameter * diameter * diameter; + const real_t dt_SI = uIn / uIn_SI * dx_SI; + const real_t viscosity = kinematicViscosityFluid_SI * dt_SI / ( dx_SI * dx_SI ); + const real_t gravitationalAcceleration = gravitationalAcceleration_SI * dt_SI * dt_SI / dx_SI; + const real_t omega = lbm::collision_model::omegaFromViscosity(viscosity); + + const real_t Re_p = diameter * uIn / viscosity; + const real_t St = densityRatio * uIn * diameter / ( real_t(9) * viscosity ); + + const real_t omegaBulk = lbm_mesapd_coupling::omegaBulkFromOmega(omega, bulkViscRateFactor); + + const real_t densityFluid = real_t(1); + const real_t densitySphere = densityRatio; + + const real_t dx = real_t(1); + + const real_t responseTime = densityRatio * diameter * diameter / ( real_t(18) * viscosity ); + const real_t accelerationFactor = real_t(1) / (real_t(0.1) * responseTime); + const real_t tref = diameter / uIn; + + const real_t particleMass = densityRatio * sphereVolume; + const real_t Mij = particleMass; // * particleMass / ( real_t(2) * particleMass ); // Mij = M for sphere-wall collision + const real_t lnDryResCoeff = std::log(restitutionCoeff); + const real_t stiffnessCoeff = Mij * ( math::pi * math::pi + lnDryResCoeff * lnDryResCoeff ) / (collisionTime * collisionTime); // Costa et al., Eq. 18 + const real_t dampingCoeff = - real_t(2) * Mij * lnDryResCoeff / collisionTime; // Costa et al., Eq. 18 + //const real_t contactDuration = real_t(2) * math::pi * Mij / ( std::sqrt( real_t(4) * Mij * stiffnessCoeff - dampingCoeff * dampingCoeff )); //formula from Uhlman + + const real_t var_a = std::sqrt( math::pi * math::pi + (std::log(restitutionCoeff) * std::log(restitutionCoeff) ) ); + const real_t TnLimit1 = var_a * diameter / uIn * std::exp(-std::asin(math::pi / var_a ) / math::pi); // Costa et al, Eq. 20 + const real_t TnLimit2 = std::sqrt(diameter / gravitationalAcceleration * var_a * var_a / std::abs(real_t(1) - densityFluid/densitySphere)); // Costa et al, Eq. 21 + + const real_t overlapDueToWeight = std::abs(densitySphere - densityFluid) * gravitationalAcceleration * sphereVolume / stiffnessCoeff; // Costa et al., given in text after Eq. 34 + + const real_t uInCrit = real_t(9) * StCrit * viscosity / ( densityRatio * diameter); + + Vector3<uint_t> domainSize( uint_c(domainSizeNonDim[0] * diameter ), uint_c(domainSizeNonDim[1] * diameter ), uint_c(domainSizeNonDim[2] * diameter ) ); + + real_t initialSpherePosition = real_c(domainSize[2]) - real_t(1.5) * diameter; + + if( randomizeInitialSpherePosition ) + { + uint_t seed1 = uint_c(std::chrono::system_clock::now().time_since_epoch().count()); + mpi::broadcastObject(seed1); // root process chooses seed and broadcasts it + std::mt19937 g1 (seed1); + + real_t initialPositionOffset = real_t(0.5)*math::realRandom<real_t>(real_t(-1), real_t(1), g1); + initialSpherePosition += initialPositionOffset; + } + + + WALBERLA_LOG_INFO_ON_ROOT("Setup (in SI units):"); + WALBERLA_LOG_INFO_ON_ROOT(" - sphere: diameter = " << diameter_SI << ", densityRatio = " << densityRatio); + WALBERLA_LOG_INFO_ON_ROOT(" - fluid: density = " << densityFluid_SI << ", dyn. visc = " << dynamicViscosityFluid_SI << ", kin. visc = " << kinematicViscosityFluid_SI ); + WALBERLA_LOG_INFO_ON_ROOT(" - ug = " << ug_SI ); + WALBERLA_LOG_INFO_ON_ROOT(" - Galileo number = " << Ga ); + WALBERLA_LOG_INFO_ON_ROOT(" - target Reynolds number = " << Re ); + WALBERLA_LOG_INFO_ON_ROOT(" - domain size = <" << real_c(domainSizeNonDim[0]) * diameter_SI << "," << real_c(domainSizeNonDim[1]) * diameter_SI << ","<< real_c(domainSizeNonDim[2]) * diameter_SI << ">"); + WALBERLA_LOG_INFO_ON_ROOT(" - dx = " << dx_SI); + WALBERLA_LOG_INFO_ON_ROOT(" - dt = " << dt_SI); + + WALBERLA_LOG_INFO_ON_ROOT("Setup (in simulation, i.e. lattice, units):"); + WALBERLA_LOG_INFO_ON_ROOT(" - domain size = " << domainSize); + if(applyOutflowBCAtTop) WALBERLA_LOG_INFO_ON_ROOT(" - outflow BC at top"); + WALBERLA_LOG_INFO_ON_ROOT(" - sphere: diameter = " << diameter << ", density = " << densitySphere ); + WALBERLA_LOG_INFO_ON_ROOT(" - initial sphere position = " << initialSpherePosition ); + WALBERLA_LOG_INFO_ON_ROOT(" - fluid: density = " << densityFluid << ", relaxation time (tau) = " << real_t(1) / omega << ", omega = " << omega << ", kin. visc = " << viscosity ); + WALBERLA_LOG_INFO_ON_ROOT(" - magic number = " << magicNumber); + WALBERLA_LOG_INFO_ON_ROOT(" - omega bulk = " << omegaBulk << ", bulk visc = " << lbm_mesapd_coupling::bulkViscosityFromOmegaBulk(omegaBulk) << " ( bulk visc rate factor (conste) = " << bulkViscRateFactor << ")"); + if(useOmegaBulkAdaption) WALBERLA_LOG_INFO_ON_ROOT(" - using omega bulk adaption in vicinity of sphere"); + WALBERLA_LOG_INFO_ON_ROOT(" - gravitational acceleration = " << gravitationalAcceleration ); + WALBERLA_LOG_INFO_ON_ROOT(" - expected settling velocity = " << uIn ); + WALBERLA_LOG_INFO_ON_ROOT(" - target Reynolds number = " << Re_p ); + WALBERLA_LOG_INFO_ON_ROOT(" - target Stokes number = " << St ); + WALBERLA_LOG_INFO_ON_ROOT(" - tref = " << tref ); + WALBERLA_LOG_INFO_ON_ROOT(" - Stokes response time = " << responseTime ); + if( artificiallyAccelerateSphere ) + { + WALBERLA_LOG_INFO_ON_ROOT(" - artificially accelerating sphere with factor " << accelerationFactor) + } + WALBERLA_LOG_INFO_ON_ROOT(" - integrator = " << (useVelocityVerlet ? "Velocity Verlet" : "Explicit Euler") ); + if( vtkIOFreq > 0 ) + { + WALBERLA_LOG_INFO_ON_ROOT(" - writing vtk files to folder \"" << baseFolder << "\" with frequency " << vtkIOFreq); + } + if(vtkOutputOnCollision) + { + WALBERLA_LOG_INFO_ON_ROOT(" - writing vtk files during collision events"); + } + + WALBERLA_LOG_INFO_ON_ROOT("Collision Response properties:" ); + WALBERLA_LOG_INFO_ON_ROOT(" - collision time = " << collisionTime ); + if(useACTM) + { + WALBERLA_LOG_INFO_ON_ROOT(" - using nonlinear collision model with ACTM" ); + } + else + { + WALBERLA_LOG_INFO_ON_ROOT(" - using linear collision model with fixed parameters:" ); + WALBERLA_LOG_INFO_ON_ROOT(" - damping coeff = " << dampingCoeff ); + WALBERLA_LOG_INFO_ON_ROOT(" - stiffness coeff = " << stiffnessCoeff ); + WALBERLA_LOG_INFO_ON_ROOT(" - overlap due to particle weight = " << overlapDueToWeight); + } + WALBERLA_LOG_INFO_ON_ROOT(" - TnCrit1 = " << TnLimit1 << ", TnCrit2 = " << TnLimit2 ); + WALBERLA_LOG_INFO_ON_ROOT(" - coeff of restitution = " << restitutionCoeff ); + WALBERLA_LOG_INFO_ON_ROOT(" - number of RPD sub cycles = " << numRPDSubCycles ); + WALBERLA_LOG_INFO_ON_ROOT(" - lubrication correction = " << (useLubricationCorrection ? "yes" : "no") ); + if( useLubricationCorrection ) + { + WALBERLA_LOG_INFO_ON_ROOT(" - lubrication correction cut off = " << lubricationCutOffDistance ); + WALBERLA_LOG_INFO_ON_ROOT(" - lubrication correction minimal gap size non dim = " << lubricationMinimalGapSizeNonDim << " ( = " << lubricationMinimalGapSizeNonDim * diameter * real_t(0.5) << " cells )"); + } + WALBERLA_LOG_INFO_ON_ROOT("Coupling properties:" ); + WALBERLA_LOG_INFO_ON_ROOT(" - disable hydrodynamic forces during contact = " << (disableFluidForceDuringContact ? "yes" : "no") ); + if(disableFluidForceDuringContact) WALBERLA_LOG_INFO_ON_ROOT(" - StCrit = " << StCrit << ", uInCrit = " << uInCrit ); + WALBERLA_LOG_INFO_ON_ROOT(" - average forces over two LBM time steps = " << (averageForceTorqueOverTwoTimeSteps ? "yes" : "no") ); + WALBERLA_LOG_INFO_ON_ROOT(" - conserve momentum = " << (conserveMomentum ? "yes" : "no") ); + WALBERLA_LOG_INFO_ON_ROOT(" - reconstructor = " << reconstructorType ); + + std::string checkPointFileName = "checkPointingFile_" + setup + + "_uIn" + std::to_string(uIn) + + "_d" + std::to_string(diameter) + + "_bulkViscFactor" + std::to_string(bulkViscRateFactor) + + "_mn" + std::to_string(magicNumber); + if(useOmegaBulkAdaption) checkPointFileName +="_omegaBulkAdaption"; + if(applyOutflowBCAtTop) checkPointFileName +="_outflowBC"; +#ifdef USE_TRT_LIKE_LATTICE_MODEL + checkPointFileName +="_TRTlike"; +#else + checkPointFileName += "_other"; +#endif + + WALBERLA_LOG_INFO_ON_ROOT("Checkpointing:"); + WALBERLA_LOG_INFO_ON_ROOT(" - read from checkpoint file = " << (readFromCheckPointFile ? "yes" : "no")); + WALBERLA_LOG_INFO_ON_ROOT(" - write checkpoint file = " << (writeCheckPointFile ? "yes" : "no")); + if( readFromCheckPointFile || writeCheckPointFile) WALBERLA_LOG_INFO_ON_ROOT(" - checkPointingFileName = " << checkPointFileName ); + + if(readFromCheckPointFile && writeCheckPointFile ) + { + // decide which option to choose + if(filesystem::exists( checkPointFileName+"_lbm.txt")) + { + WALBERLA_LOG_INFO_ON_ROOT("Checkpoint file already exists! Will skip writing check point file and start from this check point!"); + writeCheckPointFile = false; + } else + { + WALBERLA_LOG_INFO_ON_ROOT("Checkpoint file does not exists yet! Will skip reading check point file and just regularly start the simulation from the beginning!"); + readFromCheckPointFile = false; + } + } + + if(readFromCheckPointFile && artificiallyAccelerateSphere) + { + // accelerating after reading from a checkpoint file does not make sense, as current actual runtime is not known + WALBERLA_LOG_INFO_ON_ROOT("NOTE: switching off artificial acceleration of sphere due to reading from check point file"); + artificiallyAccelerateSphere = false; + } + + + /////////////////////////// + // BLOCK STRUCTURE SETUP // + /////////////////////////// + + + Vector3<uint_t> cellsPerBlockPerDirection( domainSize[0] / numberOfBlocksPerDirection[0], + domainSize[1] / numberOfBlocksPerDirection[1], + domainSize[2] / numberOfBlocksPerDirection[2] ); + for( uint_t i = 0; i < 3; ++i ) { + WALBERLA_CHECK_EQUAL(cellsPerBlockPerDirection[i] * numberOfBlocksPerDirection[i], domainSize[i], + "Unmatching domain decomposition in direction " << i << "!"); + } + + auto blocks = blockforest::createUniformBlockGrid( numberOfBlocksPerDirection[0], numberOfBlocksPerDirection[1], numberOfBlocksPerDirection[2], + cellsPerBlockPerDirection[0], cellsPerBlockPerDirection[1], cellsPerBlockPerDirection[2], dx, + 0, false, false, + true, true, false, //periodicity + false ); + + WALBERLA_LOG_INFO_ON_ROOT("Domain decomposition:"); + WALBERLA_LOG_INFO_ON_ROOT(" - blocks per direction = " << numberOfBlocksPerDirection ); + WALBERLA_LOG_INFO_ON_ROOT(" - cells per block = " << cellsPerBlockPerDirection ); + + //write domain decomposition to file + if( vtkIOFreq > 0 ) + { + vtk::writeDomainDecomposition( blocks, "initial_domain_decomposition", baseFolder ); + } + + + ////////////////// + // RPD COUPLING // + ////////////////// + + auto rpdDomain = std::make_shared<mesa_pd::domain::BlockForestDomain>(blocks->getBlockForestPointer()); + + //init data structures + auto ps = walberla::make_shared<mesa_pd::data::ParticleStorage>(1); + BlockDataID particleStorageID; + if(readFromCheckPointFile) + { + WALBERLA_LOG_INFO_ON_ROOT( "Initializing particles from checkpointing file!" ); + particleStorageID = blocks->loadBlockData( checkPointFileName + "_mesa.txt", mesa_pd::domain::createBlockForestDataHandling(ps), "Particle Storage" ); + } else { + particleStorageID = blocks->addBlockData(mesa_pd::domain::createBlockForestDataHandling(ps), "Particle Storage"); + } + + auto ss = walberla::make_shared<mesa_pd::data::ShapeStorage>(); + using ParticleAccessor_T = mesa_pd::data::ParticleAccessorWithShape; + auto accessor = walberla::make_shared<ParticleAccessor_T >(ps, ss); + + // bounding planes + createPlaneSetup(ps,ss,blocks->getDomain(), applyOutflowBCAtTop); + + // create sphere and store Uid + Vector3<real_t> initialPosition( real_t(0.5) * real_c(domainSize[0]), real_t(0.5) * real_c(domainSize[1]), initialSpherePosition ); + auto sphereShape = ss->create<mesa_pd::data::Sphere>( diameter * real_t(0.5) ); + ss->shapes[sphereShape]->updateMassAndInertia(densitySphere); + + walberla::id_t sphereUid = 0; + if( readFromCheckPointFile ) + { + for( auto pIt = ps->begin(); pIt != ps->end(); ++pIt ) + { + // find sphere in loaded data structure and store uid for later reference + if( pIt->getShapeID() == sphereShape ) + { + sphereUid = pIt->getUid(); + } + } + } else + { + // create sphere + if (rpdDomain->isContainedInProcessSubdomain( uint_c(mpi::MPIManager::instance()->rank()), initialPosition )) + { + mesa_pd::data::Particle&& p = *ps->create(); + p.setPosition(initialPosition); + p.setInteractionRadius(diameter * real_t(0.5)); + p.setOwner(mpi::MPIManager::instance()->rank()); + p.setShapeID(sphereShape); + p.setType(0); + if( initializeSphereVelocity ) p.setLinearVelocity(Vector3<real_t>(real_t(0), real_t(0), -real_t(0.1) * uIn)); + sphereUid = p.getUid(); + } + } + mpi::allReduceInplace(sphereUid, mpi::SUM); + + if(sphereUid == 0) WALBERLA_ABORT("No sphere present - aborting!"); //something went wrong in the checkpointing probably + + /////////////////////// + // ADD DATA TO BLOCKS // + //////////////////////// + + // add omega bulk field + BlockDataID omegaBulkFieldID = field::addToStorage<ScalarField_T>( blocks, "omega bulk field", omegaBulk, field::fzyx, uint_t(0) ); + + // create the lattice model + + // TRT + //LatticeModel_T latticeModel = LatticeModel_T( lbm::collision_model::TRT::constructWithMagicNumber( real_t(1) / relaxationTime ) ); + + // generated MRT +#ifdef USE_TRT_LIKE_LATTICE_MODEL + WALBERLA_LOG_INFO_ON_ROOT("Using TRT-like lattice model!"); + real_t lambda_e = lbm::collision_model::TRT::lambda_e( omega ); + real_t lambda_d = lbm::collision_model::TRT::lambda_d( omega, magicNumber ); + //LatticeModel_T latticeModel = LatticeModel_T(omegaBulk, lambda_d, lambda_e); + LatticeModel_T latticeModel = LatticeModel_T(omegaBulkFieldID, lambda_d, lambda_e); + //LatticeModel_T latticeModel = LatticeModel_T(lbm::collision_model::D3Q19MRT( omegaBulk, omegaBulk, lambda_d, lambda_e, lambda_e, lambda_d )); +#else + WALBERLA_LOG_INFO_ON_ROOT("Using different lattice model!"); + // generated model with a single omega + LatticeModel_T latticeModel = LatticeModel_T(omega); +#endif + + // add PDF field + BlockDataID pdfFieldID; + if( readFromCheckPointFile ) + { + // add PDF field + WALBERLA_LOG_INFO_ON_ROOT( "Initializing PDF Field from checkpointing file!" ); + shared_ptr< lbm::internal::PdfFieldHandling< LatticeModel_T > > dataHandling = + make_shared< lbm::internal::PdfFieldHandling< LatticeModel_T > >( blocks, latticeModel, false, + Vector3< real_t >( real_t(0) ), real_t(1), + uint_t(1), field::fzyx ); + + pdfFieldID = blocks->loadBlockData( checkPointFileName+"_lbm.txt", dataHandling, "pdf field" ); + + } else { + // add PDF field + pdfFieldID = lbm::addPdfFieldToStorage< LatticeModel_T >( blocks, "pdf field", latticeModel, + Vector3< real_t >( real_t(0) ), real_t(1), + uint_t(1), field::fzyx ); + } + + + // add flag field + BlockDataID flagFieldID = field::addFlagFieldToStorage<FlagField_T>( blocks, "flag field" ); + + // add particle field + BlockDataID particleFieldID = field::addToStorage<lbm_mesapd_coupling::ParticleField_T>( blocks, "particle field", accessor->getInvalidUid(), field::fzyx, FieldGhostLayers ); + + // add boundary handling + using BoundaryHandling_T = MyBoundaryHandling<ParticleAccessor_T>::Type; + BlockDataID boundaryHandlingID = blocks->addStructuredBlockData< BoundaryHandling_T >(MyBoundaryHandling<ParticleAccessor_T>( flagFieldID, pdfFieldID, particleFieldID, accessor, applyOutflowBCAtTop), "boundary handling" ); + + // interpolation functionality + using DensityAdaptor_T = typename lbm::Adaptor< LatticeModel_T >::Density; + BlockDataID densityAdaptorID = field::addFieldAdaptor< DensityAdaptor_T >( blocks, pdfFieldID, "density adaptor" ); + + using DensityInterpolator_T = typename field::NearestNeighborFieldInterpolator<DensityAdaptor_T, FlagField_T>; + BlockDataID densityInterpolatorID = field::addFieldInterpolator< DensityInterpolator_T, FlagField_T >( blocks, densityAdaptorID, flagFieldID, Fluid_Flag ); + + // set up RPD functionality + std::function<void(void)> syncCall = [&ps,&rpdDomain](){ + const real_t overlap = real_t( 1.5 ); + mesa_pd::mpi::SyncNextNeighbors syncNextNeighborFunc; + syncNextNeighborFunc(*ps, *rpdDomain, overlap); + }; + + syncCall(); + + real_t timeStepSizeRPD = real_t(1)/real_t(numRPDSubCycles); + mesa_pd::kernel::ExplicitEulerWithShape explEulerIntegrator(timeStepSizeRPD); + mesa_pd::kernel::VelocityVerletWithShapePreForceUpdate vvIntegratorPreForce(timeStepSizeRPD); + mesa_pd::kernel::VelocityVerletWithShapePostForceUpdate vvIntegratorPostForce(timeStepSizeRPD); + + // linear model + mesa_pd::kernel::LinearSpringDashpot linearCollisionResponse(1); + linearCollisionResponse.setStiffnessN(0,0,stiffnessCoeff); + linearCollisionResponse.setDampingN(0,0,dampingCoeff); + + // nonlinear model for ACTM + mesa_pd::kernel::NonLinearSpringDashpot nonLinearCollisionResponse(1, collisionTime); + nonLinearCollisionResponse.setLnCORsqr(0,0,lnDryResCoeff * lnDryResCoeff); + nonLinearCollisionResponse.setMeff(0,0,Mij); + + mesa_pd::mpi::ReduceProperty reduceProperty; + + mesa_pd::mpi::ReduceContactHistory reduceAndSwapContactHistory; + + // set up coupling functionality + Vector3<real_t> gravitationalForce( real_t(0), real_t(0), -(densitySphere - densityFluid) * gravitationalAcceleration * sphereVolume ); + lbm_mesapd_coupling::AddForceOnParticlesKernel addGravitationalForce(gravitationalForce); + lbm_mesapd_coupling::ResetHydrodynamicForceTorqueKernel resetHydrodynamicForceTorque; + lbm_mesapd_coupling::AverageHydrodynamicForceTorqueKernel averageHydrodynamicForceTorque; + lbm_mesapd_coupling::LubricationCorrectionKernel lubricationCorrectionKernel(viscosity, [lubricationMinimalGapSizeNonDim](real_t r){ return lubricationMinimalGapSizeNonDim * r;}, lubricationCutOffDistance, real_t(0), real_t(0) ); // no tangential components needed + lbm_mesapd_coupling::ParticleMappingKernel<BoundaryHandling_T> particleMappingKernel(blocks, boundaryHandlingID); + lbm_mesapd_coupling::MovingParticleMappingKernel<BoundaryHandling_T> movingParticleMappingKernel(blocks, boundaryHandlingID, particleFieldID); + + /////////////// + // TIME LOOP // + /////////////// + + // map planes into the LBM simulation -> act as no-slip boundaries + ps->forEachParticle(false, lbm_mesapd_coupling::GlobalParticlesSelector(), *accessor, particleMappingKernel, *accessor, NoSlip_Flag); + + // map particles into the LBM simulation + ps->forEachParticle(false, lbm_mesapd_coupling::RegularParticlesSelector(), *accessor, movingParticleMappingKernel, *accessor, MO_Flag); + + // setup of the LBM communication for synchronizing the pdf field between neighboring blocks + blockforest::communication::UniformBufferedScheme< Stencil_T > optimizedPDFCommunicationScheme( blocks ); + optimizedPDFCommunicationScheme.addPackInfo( make_shared< lbm::PdfFieldPackInfo< LatticeModel_T > >( pdfFieldID ) ); // optimized sync + + blockforest::communication::UniformBufferedScheme< Stencil_T > fullPDFCommunicationScheme( blocks ); + fullPDFCommunicationScheme.addPackInfo( make_shared< field::communication::PackInfo< PdfField_T > >( pdfFieldID ) ); // full sync + + // create the timeloop + const uint_t timesteps = uint_c( real_t(3) * initialPosition[2] / uIn); + + SweepTimeloop timeloop( blocks->getBlockStorage(), timesteps ); + + timeloop.addFuncBeforeTimeStep( RemainingTimeLogger( timeloop.getNrOfTimeSteps() ), "Remaining Time Logger" ); + + // vtk output + if( vtkIOFreq != uint_t(0) ) + { + // sphere + auto particleVtkOutput = make_shared<mesa_pd::vtk::ParticleVtkOutput>(ps); + particleVtkOutput->addOutput<mesa_pd::data::SelectParticleOwner>("owner"); + particleVtkOutput->addOutput<mesa_pd::data::SelectParticleUid>("uid"); + particleVtkOutput->addOutput<mesa_pd::data::SelectParticleShapeID>("shapeID"); + particleVtkOutput->addOutput<mesa_pd::data::SelectParticleLinearVelocity>("velocity"); + particleVtkOutput->setParticleSelector( [sphereShape](const mesa_pd::data::ParticleStorage::iterator& pIt) {return pIt->getShapeID() == sphereShape;} ); //limit output to sphere + auto particleVtkWriter = vtk::createVTKOutput_PointData(particleVtkOutput, "Particles", vtkIOFreq, baseFolder, "simulation_step"); + timeloop.addFuncBeforeTimeStep( vtk::writeFiles( particleVtkWriter ), "VTK (sphere data)" ); + + // flag field (written only once in the first time step, ghost layers are also written) + + auto flagFieldVTK = vtk::createVTKOutput_BlockData( blocks, "flag_field", timesteps, FieldGhostLayers, false, baseFolder ); + flagFieldVTK->addCellDataWriter( make_shared< field::VTKWriter< FlagField_T > >( flagFieldID, "FlagField" ) ); + timeloop.addFuncBeforeTimeStep( vtk::writeFiles( flagFieldVTK ), "VTK (flag field data)" ); + + + // pdf field, as slice + auto pdfFieldVTK = vtk::createVTKOutput_BlockData( blocks, "fluid_field", vtkIOFreq, 0, false, baseFolder ); + + pdfFieldVTK->addBeforeFunction( fullPDFCommunicationScheme ); + + AABB sliceAABB( real_t(0), real_c(domainSize[1])*real_t(0.5)-real_t(1), real_t(0), + real_c(domainSize[0]), real_c(domainSize[1])*real_t(0.5)+real_t(1), real_c(domainSize[2]) ); + vtk::AABBCellFilter aabbSliceFilter( sliceAABB ); + + field::FlagFieldCellFilter< FlagField_T > fluidFilter( flagFieldID ); + fluidFilter.addFlag( Fluid_Flag ); + + vtk::ChainedFilter combinedSliceFilter; + combinedSliceFilter.addFilter( fluidFilter ); + combinedSliceFilter.addFilter( aabbSliceFilter ); + + pdfFieldVTK->addCellInclusionFilter( combinedSliceFilter ); + + pdfFieldVTK->addCellDataWriter( make_shared< lbm::VelocityVTKWriter< LatticeModel_T, float > >( pdfFieldID, "VelocityFromPDF" ) ); + pdfFieldVTK->addCellDataWriter( make_shared< lbm::DensityVTKWriter < LatticeModel_T, float > >( pdfFieldID, "DensityFromPDF" ) ); + + timeloop.addFuncBeforeTimeStep( vtk::writeFiles( pdfFieldVTK ), "VTK (fluid field data)" ); + + // omega bulk field + //timeloop.addFuncBeforeTimeStep( field::createVTKOutput<ScalarField_T, float>( omegaBulkFieldID, *blocks, "omega_bulk_field", vtkIOFreq, uint_t(0), false, baseFolder ), "VTK (omega bulk field)" ); + } + + + + // update bulk omega in all cells to adapt to changed particle position + if(useOmegaBulkAdaption) + { + using OmegaBulkAdapter_T = lbm_mesapd_coupling::OmegaBulkAdapter<ParticleAccessor_T, lbm_mesapd_coupling::RegularParticlesSelector>; + real_t defaultOmegaBulk = lbm_mesapd_coupling::omegaBulkFromOmega(omega, real_t(1)); + real_t adaptionLayerSize = real_t(2); + shared_ptr<OmegaBulkAdapter_T> omegaBulkAdapter = make_shared<OmegaBulkAdapter_T>(blocks, omegaBulkFieldID, accessor, defaultOmegaBulk, omegaBulk, adaptionLayerSize, lbm_mesapd_coupling::RegularParticlesSelector()); + + // initial adaption + for (auto blockIt = blocks->begin(); blockIt != blocks->end(); ++blockIt) { + (*omegaBulkAdapter)(blockIt.get()); + } + + timeloop.add() << Sweep( makeSharedSweep(omegaBulkAdapter), "Omega Bulk Adapter"); + } + + + // add LBM communication function and boundary handling sweep (does the hydro force calculations and the no-slip treatment) + timeloop.add() << BeforeFunction( optimizedPDFCommunicationScheme, "LBM Communication" ) + << Sweep( BoundaryHandling_T::getBlockSweep( boundaryHandlingID ), "Boundary Handling" ); + + // stream + collide LBM step + // generated LBM sweep + timeloop.add() << Sweep( LatticeModel_T::Sweep( pdfFieldID ), "LB stream & collide" ); + // walberla sweeps: + //timeloop.add() << Sweep( makeSharedSweep( lbm::makeCellwiseSweep< LatticeModel_T, FlagField_T >( pdfFieldID, flagFieldID, Fluid_Flag ) ), "cell-wise LB sweep" ); + + // this is carried out after the particle integration, it corrects the flag field and restores missing PDF information + // then, the checkpointing file can be written, as otherwise some cells are invalid and can not be recovered + SweepTimeloop timeloopAfterParticles( blocks->getBlockStorage(), timesteps ); + + // sweep for updating the particle mapping into the LBM simulation + timeloopAfterParticles.add() << Sweep( lbm_mesapd_coupling::makeMovingParticleMapping<PdfField_T,BoundaryHandling_T>(blocks, pdfFieldID,boundaryHandlingID, particleFieldID, accessor, MO_Flag, FormerMO_Flag, lbm_mesapd_coupling::RegularParticlesSelector(), conserveMomentum), "Particle Mapping" ); + + // sweep for restoring PDFs in cells previously occupied by particles + if( reconstructorType == "EAN" ) + { + auto sphereNormalExtrapolationDirectionFinder = make_shared<lbm_mesapd_coupling::SphereNormalExtrapolationDirectionFinder>(blocks); + auto equilibriumAndNonEquilibriumSphereNormalReconstructor = lbm_mesapd_coupling::makeEquilibriumAndNonEquilibriumReconstructor<BoundaryHandling_T>(blocks, boundaryHandlingID, sphereNormalExtrapolationDirectionFinder, uint_t(3), true); + timeloopAfterParticles.add() << BeforeFunction( fullPDFCommunicationScheme, "LBM Communication" ) + << Sweep( makeSharedSweep(lbm_mesapd_coupling::makePdfReconstructionManager<PdfField_T,BoundaryHandling_T>(blocks, pdfFieldID, boundaryHandlingID, particleFieldID, accessor, FormerMO_Flag, Fluid_Flag, equilibriumAndNonEquilibriumSphereNormalReconstructor, conserveMomentum)), "PDF Restore" ); + }else if( reconstructorType == "Grad" ) + { + bool recomputeTargetDensity = false; + auto gradReconstructor = lbm_mesapd_coupling::makeGradsMomentApproximationReconstructor<BoundaryHandling_T>(blocks, boundaryHandlingID, omega, recomputeTargetDensity,true); + + timeloopAfterParticles.add() << BeforeFunction( fullPDFCommunicationScheme, "PDF Communication" ) + << Sweep( makeSharedSweep(lbm_mesapd_coupling::makePdfReconstructionManager<PdfField_T,BoundaryHandling_T>(blocks, pdfFieldID, boundaryHandlingID, particleFieldID, accessor, FormerMO_Flag, Fluid_Flag, gradReconstructor, conserveMomentum) ), "PDF Restore" ); + }else if( reconstructorType == "Eq" ) + { + timeloopAfterParticles.add() << Sweep( makeSharedSweep(lbm_mesapd_coupling::makePdfReconstructionManager<PdfField_T,BoundaryHandling_T>(blocks, pdfFieldID, boundaryHandlingID, particleFieldID, accessor, FormerMO_Flag, Fluid_Flag, conserveMomentum)), "PDF Restore" ); + }else if( reconstructorType == "Ext") + { + auto sphereNormalExtrapolationDirectionFinder = make_shared<lbm_mesapd_coupling::SphereNormalExtrapolationDirectionFinder>(blocks); +#ifdef USE_TRT_LIKE_LATTICE_MODEL + auto extrapolationReconstructor = lbm_mesapd_coupling::makeExtrapolationReconstructor<BoundaryHandling_T, lbm_mesapd_coupling::SphereNormalExtrapolationDirectionFinder, true>(blocks, boundaryHandlingID, sphereNormalExtrapolationDirectionFinder, uint_t(3), true); +#else + auto extrapolationReconstructor = lbm_mesapd_coupling::makeExtrapolationReconstructor<BoundaryHandling_T, lbm_mesapd_coupling::SphereNormalExtrapolationDirectionFinder, false>(blocks, boundaryHandlingID, sphereNormalExtrapolationDirectionFinder, uint_t(3), true); +#endif + timeloopAfterParticles.add() << BeforeFunction( fullPDFCommunicationScheme, "LBM Communication" ) + << Sweep( makeSharedSweep(lbm_mesapd_coupling::makePdfReconstructionManager<PdfField_T,BoundaryHandling_T>(blocks, pdfFieldID, boundaryHandlingID, particleFieldID, accessor, FormerMO_Flag, Fluid_Flag, extrapolationReconstructor, conserveMomentum)), "PDF Restore" ); + } else + { + WALBERLA_ABORT("Reconstructor Type " << reconstructorType << " not implemented!"); + } + + + + + // evaluation functionality + std::string loggingFileName( baseFolder + "/LoggingSphereWallCollision.txt"); + if( fileIO ) + { + WALBERLA_LOG_INFO_ON_ROOT(" - writing logging output to file \"" << loggingFileName << "\""); + } + SpherePropertyLogger<ParticleAccessor_T> logger( accessor, sphereUid, loggingFileName, fileIO, diameter, uIn, numRPDSubCycles ); + + std::string forceLoggingFileName( baseFolder + "/ForceLoggingSphereWallCollision.txt"); + if( fileIO ) + { + WALBERLA_LOG_INFO_ON_ROOT(" - writing force logging output to file \"" << forceLoggingFileName << "\""); + } + ForcesOnSphereLogger sphereForceLogger(forceLoggingFileName, fileIO, gravitationalForce[2], numRPDSubCycles); + + //////////////////////// + // EXECUTE SIMULATION // + //////////////////////// + + WcTimingPool timeloopTiming; + + // evaluation quantities + uint_t numBounces = uint_t(0); + uint_t tImpact = uint_t(0); + std::vector<uint_t> impactTimes; + + real_t curVel(real_t(0)), oldVel(real_t(0)); + real_t maxSettlingVel = real_t(0); + std::vector<real_t> maxSettlingVelBetweenBounces; + + real_t minGapSize(real_t(0)); + real_t maxGapSize(real_t(0)); + std::vector<real_t> maxGapSizeBetweenBounces; + + real_t actualSt(real_t(0)); + real_t actualRe(real_t(0)); + + WALBERLA_LOG_INFO_ON_ROOT("Running for maximum of " << timesteps << " timesteps!"); + + const bool useOpenMP = false; + + // special vtk output + auto pdfFieldVTKCollision = vtk::createVTKOutput_BlockData( blocks, "collision_fluid_field", 1, 1, false, baseFolder ); + + blockforest::communication::UniformBufferedScheme< stencil::D3Q27 > pdfGhostLayerSync( blocks ); + pdfGhostLayerSync.addPackInfo( make_shared< field::communication::PackInfo< PdfField_T > >( pdfFieldID ) ); + pdfFieldVTKCollision->addBeforeFunction( pdfGhostLayerSync ); + + AABB boxAABB( initialPosition[0] - diameter, initialPosition[1] - diameter, -real_t(1), + initialPosition[0] + diameter, initialPosition[1] + diameter, real_t(2) * diameter); + vtk::AABBCellFilter aabbBoxFilter( boxAABB ); + + field::FlagFieldCellFilter< FlagField_T > fluidFilter( flagFieldID ); + fluidFilter.addFlag( Fluid_Flag ); + + vtk::ChainedFilter combinedSliceFilter; + combinedSliceFilter.addFilter( fluidFilter ); + combinedSliceFilter.addFilter( aabbBoxFilter ); + + pdfFieldVTKCollision->addCellInclusionFilter( combinedSliceFilter ); + + pdfFieldVTKCollision->addCellDataWriter( make_shared< lbm::VelocityVTKWriter< LatticeModel_T, float > >( pdfFieldID, "VelocityFromPDF" ) ); + pdfFieldVTKCollision->addCellDataWriter( make_shared< lbm::DensityVTKWriter < LatticeModel_T, float > >( pdfFieldID, "DensityFromPDF" ) ); + + + // time loop + for (uint_t i = 0; i < timesteps; ++i ) + { + // perform a single simulation step -> this contains LBM and setting of the hydrodynamic interactions + timeloop.singleStep( timeloopTiming ); + + + if( averageForceTorqueOverTwoTimeSteps && i!= 0) + { + ps->forEachParticle(useOpenMP, mesa_pd::kernel::SelectAll(), *accessor, averageHydrodynamicForceTorque, *accessor ); + } + + real_t hydForce(real_t(0)); + real_t lubForce(real_t(0)); + real_t collisionForce(real_t(0)); + + for(auto subCycle = uint_t(0); subCycle < numRPDSubCycles; ++subCycle ) + { + + timeloopTiming["RPD"].start(); + + if( useVelocityVerlet ) + { + + Vector3<real_t> oldForce; + + if(artificiallyAccelerateSphere) + { + // since the pre-force step of VV updates the position based on velocity and old force, we set oldForce to zero here for this step while artificially accelerating + // to not perturb the step after which artificial acceleration is switched off (which requires valid oldForce values then) we store the old force and then re-apply it + auto idx = accessor->uidToIdx(sphereUid); + if(idx != accessor->getInvalidIdx()) + { + oldForce = accessor->getOldForce(idx); + accessor->setOldForce(idx, Vector3<real_t>(real_t(0))); + } + } + + ps->forEachParticle(useOpenMP, mesa_pd::kernel::SelectLocal(), *accessor, vvIntegratorPreForce, *accessor); + syncCall(); + + if(artificiallyAccelerateSphere) + { + // re-apply old force + auto idx = accessor->uidToIdx(sphereUid); + if(idx != accessor->getInvalidIdx()) + { + accessor->setOldForce(idx, oldForce); + } + } + } + + // lubrication correction + ps->forEachParticlePairHalf(useOpenMP, mesa_pd::kernel::ExcludeInfiniteInfinite(), *accessor, + [&lubricationCorrectionKernel,&rpdDomain](const size_t idx1, const size_t idx2, auto& ac) + { + mesa_pd::collision_detection::AnalyticContactDetection acd; + acd.getContactThreshold() = lubricationCorrectionKernel.getNormalCutOffDistance(); + mesa_pd::kernel::DoubleCast double_cast; + mesa_pd::mpi::ContactFilter contact_filter; + if (double_cast(idx1, idx2, ac, acd, ac )) + { + if (contact_filter(acd.getIdx1(), acd.getIdx2(), ac, acd.getContactPoint(), *rpdDomain)) + { + double_cast(acd.getIdx1(), acd.getIdx2(), ac, lubricationCorrectionKernel, ac, acd.getContactNormal(), acd.getPenetrationDepth()); + } + } + }, + *accessor ); + + lubForce = getForce(sphereUid,*accessor); + + // one could add linked cells here + + // collision response + ps->forEachParticlePairHalf(useOpenMP, mesa_pd::kernel::ExcludeInfiniteInfinite(), *accessor, + [&linearCollisionResponse, &nonLinearCollisionResponse, &rpdDomain, timeStepSizeRPD, useACTM] + (const size_t idx1, const size_t idx2, auto& ac) + { + mesa_pd::collision_detection::AnalyticContactDetection acd; + mesa_pd::kernel::DoubleCast double_cast; + mesa_pd::mpi::ContactFilter contact_filter; + if (double_cast(idx1, idx2, ac, acd, ac )) + { + if (contact_filter(acd.getIdx1(), acd.getIdx2(), ac, acd.getContactPoint(), *rpdDomain)) + { + if(useACTM) nonLinearCollisionResponse(acd.getIdx1(), acd.getIdx2(), ac, acd.getContactPoint(), acd.getContactNormal(), acd.getPenetrationDepth(), timeStepSizeRPD); + else linearCollisionResponse(acd.getIdx1(), acd.getIdx2(), ac, acd.getContactPoint(), acd.getContactNormal(), acd.getPenetrationDepth(), timeStepSizeRPD); + } + } + }, + *accessor ); + + collisionForce = getForce(sphereUid,*accessor) - lubForce; + + reduceAndSwapContactHistory(*ps); + + // add hydrodynamic force + if( disableFluidForceDuringContact ) + { + lbm_mesapd_coupling::StokesNumberBasedSphereSelector sphereSelector(StCrit, densityFluid, densitySphere, viscosity); + lbm_mesapd_coupling::AddHydrodynamicInteractionKernel addHydrodynamicInteraction; + ps->forEachParticle(useOpenMP, sphereSelector, *accessor, addHydrodynamicInteraction, *accessor ); + } + else + { + lbm_mesapd_coupling::RegularParticlesSelector sphereSelector; + lbm_mesapd_coupling::AddHydrodynamicInteractionKernel addHydrodynamicInteraction; + ps->forEachParticle(useOpenMP, sphereSelector, *accessor, addHydrodynamicInteraction, *accessor ); + } + + hydForce = getForce(sphereUid,*accessor) - lubForce - collisionForce; + + ps->forEachParticle( useOpenMP, mesa_pd::kernel::SelectLocal(), *accessor, addGravitationalForce, *accessor ); + + reduceProperty.operator()<mesa_pd::ForceTorqueNotification>(*ps); + + // integration + if( useVelocityVerlet ) ps->forEachParticle( useOpenMP, mesa_pd::kernel::SelectLocal(), *accessor, vvIntegratorPostForce, *accessor); + else ps->forEachParticle( useOpenMP, mesa_pd::kernel::SelectLocal(), *accessor, explEulerIntegrator, *accessor); + + syncCall(); + + if( artificiallyAccelerateSphere) + { + // overwrite velocity of particle with prescribed one + lbm_mesapd_coupling::RegularParticlesSelector sphereSelector; + real_t newSphereVel = uIn * (exp(-accelerationFactor * real_t(i) ) - real_t(1)); + ps->forEachParticle(useOpenMP, sphereSelector, *accessor, [newSphereVel](const size_t idx, ParticleAccessor_T& ac){ ac.setLinearVelocity(idx, Vector3<real_t>(real_t(0), real_t(0), newSphereVel));}, *accessor); + } + + timeloopTiming["RPD"].end(); + + // logging + timeloopTiming["Logging"].start(); + logger(i, subCycle); + sphereForceLogger(i, subCycle, hydForce, lubForce, collisionForce); + timeloopTiming["Logging"].end(); + + } + + ps->forEachParticle( useOpenMP, mesa_pd::kernel::SelectAll(), *accessor, resetHydrodynamicForceTorque, *accessor ); + + + // update particle mapping + timeloopAfterParticles.singleStep(timeloopTiming); + + // check for termination + oldVel = curVel; + curVel = logger.getSettlingVelocity(); + + maxSettlingVel = std::min(maxSettlingVel, curVel); + minGapSize = std::min(minGapSize, logger.getGapSize()); + + if(numBounces >= uint_t(1)) + { + maxGapSize = std::max(maxGapSize, logger.getGapSize()); + } + + // detect a bounce + if( oldVel < real_t(0) && curVel > real_t(0) && logger.getGapSize() < real_t(1)) + { + + ++numBounces; + WALBERLA_LOG_INFO_ON_ROOT("Detected bounce number " << numBounces); + WALBERLA_LOG_INFO_ON_ROOT("Max settling velocity " << maxSettlingVel << " -> St = " << densityRatio * std::abs(maxSettlingVel) * diameter / ( real_t(9) * viscosity ) ); + + if(numBounces == uint_t(1)) + { + actualSt = densityRatio * std::abs(maxSettlingVel) * diameter / ( real_t(9) * viscosity ); + actualRe = std::abs(maxSettlingVel) * diameter / viscosity; + } + + // reset and store quantities + maxSettlingVelBetweenBounces.push_back(maxSettlingVel); + maxSettlingVel = real_t(0); + + if(numBounces > uint_t(1)) + { + maxGapSizeBetweenBounces.push_back(maxGapSize); + maxGapSize = real_t(0); + } + + if( numBounces >= numberOfBouncesUntilTermination ) + { + break; + } + } + + // impact times are measured when the contact between sphere and wall is broken up again + if( tImpact == uint_t(0) && numBounces == uint_t(1) && logger.getGapSize() > real_t(0) ) + { + tImpact = i; + WALBERLA_LOG_INFO_ON_ROOT("Detected impact time at time step " << tImpact); + + // switch off special vtk output after first bounce + if(vtkOutputOnCollision) vtkOutputOnCollision = false; + } + + if( numBounces > impactTimes.size() && logger.getGapSize() > real_t(0)) + { + impactTimes.push_back(i); + } + + // evaluate density around sphere + if(fileIO) + { + std::string densityLoggingFileName(baseFolder + "/DensityLogging.txt"); + if(i == uint_t(0)) + { + WALBERLA_ROOT_SECTION() + { + std::ofstream file; + file.open( densityLoggingFileName.c_str()); + file << "# t \t avgDensity \t densitiesAtDifferentPositions \n"; + file.close(); + } + } + + real_t surfaceDistance = real_t(2); + real_t avgDensity = getAverageDensityInSystem<BoundaryHandling_T>(blocks, pdfFieldID, boundaryHandlingID); + logDensityToFile<DensityInterpolator_T>(blocks, densityInterpolatorID, surfaceDistance, logger.getPosition(), diameter, densityLoggingFileName, i, avgDensity); + } + + + // check if sphere is close to bottom plane + if( logger.getGapSize() < real_t(1) * diameter ) { + + // write a single checkpointing file before any collision relevant parameters take effect + if( writeCheckPointFile ) + { + WALBERLA_LOG_INFO_ON_ROOT("Writing checkpointing file in time step " << timeloop.getCurrentTimeStep()); + + blocks->saveBlockData( checkPointFileName + "_lbm.txt", pdfFieldID ); + blocks->saveBlockData( checkPointFileName + "_mesa.txt", particleStorageID ); + + writeCheckPointFile = false; + } + + // switch off acceleration after writing of check point file + artificiallyAccelerateSphere = false; + + } + + if(vtkOutputOnCollision && logger.getGapSize() < real_t(0)) + { + pdfFieldVTKCollision->write(); + } + + } + + WALBERLA_LOG_INFO_ON_ROOT("Detected " << numBounces << " bounces, terminating simulation."); + WALBERLA_LOG_INFO_ON_ROOT("Maximum settling velocities: " ); + for( auto vel : maxSettlingVelBetweenBounces) + { + WALBERLA_LOG_INFO_ON_ROOT(" - vel = " << vel << " -> St = " << densityRatio * std::abs(vel) * diameter / ( real_t(9) * viscosity ) << ", Re = " << std::abs(vel) * diameter / viscosity ); + } + WALBERLA_LOG_INFO_ON_ROOT("Maximum gap sizes between bounces:") + for( auto gapSize : maxGapSizeBetweenBounces) + { + WALBERLA_LOG_INFO_ON_ROOT(" - gap size = " << gapSize << ", gap size / diameter = " << gapSize / diameter ); + } + + + std::string resultFile(baseFolder + "/ResultBouncingSphere.txt"); + WALBERLA_LOG_INFO_ON_ROOT("Writing logging file " << resultFile); + logger.writeResult(resultFile, tImpact); + + std::string summaryFile(baseFolder + "/Summary.txt"); + WALBERLA_LOG_INFO_ON_ROOT("Writing summary file " << summaryFile); + WALBERLA_ROOT_SECTION() + { + std::ofstream file; + file.open( summaryFile.c_str()); + + file << "waLBerla Revision = " << std::string(WALBERLA_GIT_SHA1).substr(0,8) << "\n"; + file << "\nInput parameters:\n"; + file << "Ga = " << Ga << "\n"; + file << "uIn = " << uIn << "\n"; + file << "LBM parameters:\n"; + file << " - magic number = " << magicNumber << "\n"; + file << " - bulk viscosity rate factor = " << bulkViscRateFactor << "\n"; + file << " - use omega bulk adaption = " << useOmegaBulkAdaption << " (layer size = 2)\n"; + file << "Collision parameters:\n"; + file << " - subCycles = " << numRPDSubCycles << "\n"; + file << " - collision time (Tc) = " << collisionTime << "\n"; + file << " - use ACTM = " << useACTM << "\n"; + file << "use lubrication correction = " << useLubricationCorrection << "\n"; + file << " - minimum gap size non dim = " << lubricationMinimalGapSizeNonDim << "\n"; + file << " - cut off distance = " << lubricationCutOffDistance << "\n"; + file << "switch off fluid interaction force during contact = " << disableFluidForceDuringContact << "\n"; + file << " - St_Crit = " << StCrit << "\n"; + file << "reconstructor type " << reconstructorType << "\n"; + file << "apply outflow BC at top = " << applyOutflowBCAtTop << "\n"; + file << "started from checkpoint file = " << readFromCheckPointFile << " ( " << checkPointFileName << ")\n"; + + file << "\nOutput quantities:\n"; + file << "actual St = " << actualSt << "\n"; + file << "actual Re = " << actualRe << "\n"; + file << "impact times:\n"; + uint_t bounce = uint_t(0); + for( auto impact : impactTimes) + { + file << " - " << bounce++ << ": impact time = " << impact << "\n"; + } + file << "settling velocities:\n"; + bounce = uint_t(0); + for( auto vel : maxSettlingVelBetweenBounces) + { + file << " - " << bounce++ << ": vel = " << vel << " -> St = " << densityRatio * std::abs(vel) * diameter / ( real_t(9) * viscosity ) << ", Re = " << std::abs(vel) * diameter / viscosity << "\n"; + } + file << "maximal overlap = " << std::abs(minGapSize) << " (" << std::abs(minGapSize)/diameter * real_t(100) << "%)\n"; + file << "maximum gap sizes:\n"; + bounce = uint_t(1); + for( auto gapSize : maxGapSizeBetweenBounces) + { + file << " - " << bounce++ << ": gap size = " << gapSize << ", gap size / diameter = " << gapSize / diameter << "\n"; + } + + file.close(); + } + + + + + timeloopTiming.logResultOnRoot(); + + return EXIT_SUCCESS; +} + +} // namespace sphere_wall_collision + +int main( int argc, char **argv ){ + sphere_wall_collision::main(argc, argv); +} diff --git a/apps/benchmarks/FluidParticleCoupling/inputObliqueWetCollision.dat b/apps/benchmarks/FluidParticleCoupling/inputObliqueWetCollision.dat new file mode 100644 index 0000000000000000000000000000000000000000..e4a1d7fc4c611b5796d9b80fcb0678ab5da1ce22 --- /dev/null +++ b/apps/benchmarks/FluidParticleCoupling/inputObliqueWetCollision.dat @@ -0,0 +1,137 @@ +Setup +{ + + case 3; + + impactRatio 0; + + variant Acceleration; // Gravity or Acceleration + + // control + applyOutflowBCAtTop true; + + // LBM + diameter 30; + magicNumber 0.1875; + bulkViscRateFactor 100.; + useOmegaBulkAdaption true; + + // RPD + numRPDSubCycles 10; + useLubricationCorrection true; + lubricationCutOffDistanceNormal 0.666667; + lubricationCutOffDistanceTangentialTranslational 0.5; + lubricationCutOffDistanceTangentialRotational 0.5; + lubricationMinimalGapSizeNonDim 0.002; + + useVelocityVerlet true; + + collisionTime 120; + + // coupling + averageForceTorqueOverTwoTimeSteps true; + conserveMomentum false; + reconstructorType Grad; // Eq EAN Grad Ext + + // IO properties + baseFolder vtk_out_ObliqueWetCollision; + fileIO true; + vtkIOFreq 0; + +} + +Variant_Gravity +{ + gravitationalAcceleration_SI 0.25; + tau 0.502; + useFullGravityInNormalDirection false; + domainSize <20,10,10>; // multiples of diameter + numberOfBlocksPerDirection <8,5,5>; + initialSphereHeight 7; // multiple of diameter +} + +Variant_Acceleration +{ + uIn 0.02; + StTarget 100; + useStTargetInNormalDirection true; + domainSize <12,12,24>; // multiples of diameter + numberOfBlocksPerDirection <5,5,8>; + initialSphereHeight 22.5; // multiple of diameter + applyArtificialGravityAfterAccelerating true; + applyUInNormalDirection true; + accelerationFactor 25; +} + +Case1 +{ + material Steel; + fluid Glycerol78; +} + +Case2 +{ + material Glass; + fluid Glycerol45; +} + +Case3 +{ + material Glass; + fluid Glycerol33; +} + +Case4 +{ + material Steel; + fluid Glycerol37; +} + +Mat_Steel +{ + densitySphere_SI 7780; // kg/m**3 + diameter_SI 12.7e-3; // m + restitutionCoeff 0.97; + frictionCoeff 0.02; + poissonsRatio 0.27; //Joseph 2001 +} + +Mat_Glass +{ + densitySphere_SI 2540; // kg/m**3 + diameter_SI 12.7e-3; // m + restitutionCoeff 0.97; + frictionCoeff 0.15; + poissonsRatio 0.23; //Joseph 2001 +} + +// from https://www.internetchemie.info/chemie-lexikon/stoffe/g/glycerol.php +Fluid_Water +{ + densityFluid_SI 998; // kg/m**3 + dynamicViscosityFluid_SI 1e-3; // Pa s +} + +Fluid_Glycerol33 +{ + densityFluid_SI 1081; // kg/m**3 + dynamicViscosityFluid_SI 2.5e-3; // Pa s +} + +Fluid_Glycerol37 +{ + densityFluid_SI 1091; // kg/m**3 + dynamicViscosityFluid_SI 3e-3; // Pa s +} + +Fluid_Glycerol45 +{ + densityFluid_SI 1113; // kg/m**3 + dynamicViscosityFluid_SI 4.5e-3; // Pa s +} + +Fluid_Glycerol78 +{ + densityFluid_SI 1203; // kg/m**3 + dynamicViscosityFluid_SI 48e-3; // Pa s +} diff --git a/apps/benchmarks/FluidParticleCoupling/inputSphereWallCollision.dat b/apps/benchmarks/FluidParticleCoupling/inputSphereWallCollision.dat new file mode 100644 index 0000000000000000000000000000000000000000..a93ce9c09034e40173a53d47c0d1e9026c37329e --- /dev/null +++ b/apps/benchmarks/FluidParticleCoupling/inputSphereWallCollision.dat @@ -0,0 +1,122 @@ +BouncingSphere +{ + setup Case_St152; + + // simulation quantities + + // control + randomizeInitialSpherePosition false; + initializeSphereVelocity false; // with 10% of final velocity + applyOutflowBCAtTop true; + artificiallyAccelerateSphere true; + + // LBM + uIn 0.02; + diameter 20; + magicNumber 0.1875; + bulkViscRateFactor 100.; + useOmegaBulkAdaption true; + + // RPD + numRPDSubCycles 100; + useLubricationCorrection true; + lubricationCutOffDistance 0.666667; + lubricationMinimalGapSizeNonDim 0.0005; + + useVelocityVerlet true; + + collisionTime 60; + StCrit 5; + useACTM false; + + // coupling + averageForceTorqueOverTwoTimeSteps true; + conserveMomentum false; + disableFluidForceDuringContact false; + reconstructorType Grad; // Eq EAN Grad Ext + + // IO properties + baseFolder vtk_out_SphereWallCollision; + fileIO true; + vtkIOFreq 100; + vtkOutputOnCollision false; + + // checkpointing + writeCheckPointFile true; + readFromCheckPointFile true; +} + +// Fig. 10d +Case_St27 +{ + Re 30; + densityFluid_SI 965; // kg/m**3 + dynamicViscosityFluid_SI 100e-3; // Pa s + densitySphere_SI 7800; // kg/m**3 + diameter_SI 6e-3; // m + gravitationalAcceleration_SI 9.81; // m/s**2 + restitutionCoeff 0.97; + numberOfBouncesUntilTermination 2; + domainSize <12,12,48>; // multiples of diameter + numberOfBlocksPerDirection <5,5,8>; +} + +// Fig. 10c +Case_St100 +{ + Re 110; + densityFluid_SI 953; // kg/m**3 + dynamicViscosityFluid_SI 20e-3; // Pa s + densitySphere_SI 7800; // kg/m**3 + diameter_SI 4e-3; // m + gravitationalAcceleration_SI 9.81; // m/s**2 + restitutionCoeff 0.97; + numberOfBouncesUntilTermination 2; + domainSize <12,12,48>; // multiples of diameter + numberOfBlocksPerDirection <5,5,8>; +} + +// Fig. 2 a,b +Case_St152 +{ + Re 164; + densityFluid_SI 935; // kg/m**3 + dynamicViscosityFluid_SI 10e-3; // Pa s + densitySphere_SI 7800; // kg/m**3 + diameter_SI 0.003; // m + gravitationalAcceleration_SI 9.81; // m/s**2 + restitutionCoeff 0.97; + numberOfBouncesUntilTermination 4; + domainSize <12,12,64>; // multiples of diameter + numberOfBlocksPerDirection <5,5,8>; +} + +// Fig. 10b +Case_St192 +{ + Re 212; + densityFluid_SI 953; // kg/m**3 + dynamicViscosityFluid_SI 20e-3; // Pa s + densitySphere_SI 7800; // kg/m**3 + diameter_SI 6e-3; // m + gravitationalAcceleration_SI 9.81; // m/s**2 + restitutionCoeff 0.97; + numberOfBouncesUntilTermination 2; + domainSize <12,12,64>; // multiples of diameter + numberOfBlocksPerDirection <5,5,8>; +} + +// Fig. 10a +Case_St742 +{ + Re 788; + densityFluid_SI 920; // kg/m**3 + dynamicViscosityFluid_SI 5e-3; // Pa s + densitySphere_SI 7800; // kg/m**3 + diameter_SI 0.005; // m + gravitationalAcceleration_SI 9.81; // m/s**2 + restitutionCoeff 0.97; + numberOfBouncesUntilTermination 2; + domainSize <12,12,64>; // multiples of diameter + numberOfBlocksPerDirection <5,5,8>; +} diff --git a/python/mesa_pd.py b/python/mesa_pd.py index 94cd212a7d618e9381898cb0c3068883052c2bff..c4ba4e61ae2faeb121ef953b493d28bb7c73b2cf 100755 --- a/python/mesa_pd.py +++ b/python/mesa_pd.py @@ -66,6 +66,13 @@ if __name__ == '__main__': ps.addProperty("dv", "walberla::mesa_pd::Vec3", defValue="real_t(0)", syncMode="NEVER") ps.addProperty("dw", "walberla::mesa_pd::Vec3", defValue="real_t(0)", syncMode="NEVER") + # Properties for lbm_mesapd_coupling: + ps.addProperty("hydrodynamicForce", "walberla::mesa_pd::Vec3", defValue="real_t(0)", syncMode="NEVER") + ps.addProperty("hydrodynamicTorque", "walberla::mesa_pd::Vec3", defValue="real_t(0)", syncMode="NEVER") + ps.addProperty("oldHydrodynamicForce", "walberla::mesa_pd::Vec3", defValue="real_t(0)", syncMode="NEVER") + ps.addProperty("oldHydrodynamicTorque", "walberla::mesa_pd::Vec3", defValue="real_t(0)", syncMode="NEVER") + + ch.addProperty("tangentialSpringDisplacement", "walberla::mesa_pd::Vec3", defValue="real_t(0)") ch.addProperty("isSticking", "bool", defValue="false") ch.addProperty("impactVelocityMagnitude", "real_t", defValue="real_t(0)") diff --git a/src/lbm_mesapd_coupling/CMakeLists.txt b/src/lbm_mesapd_coupling/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..1f7af49cecad9842836ab3c0e31ee65219412ea1 --- /dev/null +++ b/src/lbm_mesapd_coupling/CMakeLists.txt @@ -0,0 +1,8 @@ + +################################################################################################### +# +# Module lbm - mesapd - coupling +# +################################################################################################### + +waLBerla_add_module( DEPENDS boundary blockforest core domain_decomposition field lbm mesa_pd stencil ) diff --git a/src/lbm_mesapd_coupling/DataTypes.h b/src/lbm_mesapd_coupling/DataTypes.h new file mode 100644 index 0000000000000000000000000000000000000000..a71680613d72db044f8dca16dcec91eb8f8bedfb --- /dev/null +++ b/src/lbm_mesapd_coupling/DataTypes.h @@ -0,0 +1,37 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla is free software: you can +// redistribute it and/or modify it under the terms of the GNU General Public +// License as published by the Free Software Foundation, either version 3 of +// the License, or (at your option) any later version. +// +// waLBerla is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +// for more details. +// +// You should have received a copy of the GNU General Public License along +// with waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file DataTypes.h +//! \ingroup lbm_mesapd_coupling +//! \author Christoph Rettinger <christoph.rettinger@fau.de> +// +//====================================================================================================================== + +#pragma once + +#include "field/GhostLayerField.h" +#include "core/DataTypes.h" + +namespace walberla { +namespace lbm_mesapd_coupling { + +/* + * Typedefs specific to the lbm - mesa_pd coupling + */ +using ParticleField_T = walberla::GhostLayerField< walberla::id_t, 1 >; + + +} // namespace lbm_mesapd_coupling +} // namespace walberla diff --git a/src/lbm_mesapd_coupling/mapping/ParticleBoundingBox.h b/src/lbm_mesapd_coupling/mapping/ParticleBoundingBox.h new file mode 100644 index 0000000000000000000000000000000000000000..aa32158a99094b283016cace0a35632aae300dff --- /dev/null +++ b/src/lbm_mesapd_coupling/mapping/ParticleBoundingBox.h @@ -0,0 +1,101 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla is free software: you can +// redistribute it and/or modify it under the terms of the GNU General Public +// License as published by the Free Software Foundation, either version 3 of +// the License, or (at your option) any later version. +// +// waLBerla is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +// for more details. +// +// You should have received a copy of the GNU General Public License along +// with waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file ParticleBoundingBox.h +//! \ingroup lbm_mesapd_coupling +//! \author Christoph Rettinger <christoph.rettinger@fau.de> +// +//====================================================================================================================== + +#pragma once + +#include "core/debug/Debug.h" + +#include "domain_decomposition/StructuredBlockStorage.h" + +#include "mesa_pd/common/AABBConversion.h" +#include "mesa_pd/data/Flags.h" +#include "mesa_pd/data/IAccessor.h" + +namespace walberla { +namespace lbm_mesapd_coupling { + +/* + * Obtain a block-local cell bounding box from a given AABB (e.g. the particle's AABB) + * If the given AABB is (partly) infinite, AABBIsInfinite should be set to true (e.g. for infinite particles) + */ +CellInterval getCellBBFromAABB( const math::AABB & aabb, bool AABBIsInfinite, + const IBlock & block, StructuredBlockStorage & blockStorage, + const uint_t numberOfGhostLayersToInclude) +{ + + CellInterval cellBB; + + if( AABBIsInfinite ) + { + auto level = blockStorage.getLevel(block); + const real_t dx = blockStorage.dx(level); + const real_t dy = blockStorage.dy(level); + const real_t dz = blockStorage.dz(level); + Vector3<real_t> aabbExtensionByGhostLayers(real_c(numberOfGhostLayersToInclude) * dx, + real_c(numberOfGhostLayersToInclude) * dy, + real_c(numberOfGhostLayersToInclude) * dz); + auto extendedBlockAABB = blockStorage.getAABB(block.getId()).getExtended( aabbExtensionByGhostLayers ); + + // intersect the (partly) infinite aabb with the block AABB, extended by its ghost layers + // then determine the cell bounding box of the intersection + blockStorage.getCellBBFromAABB( cellBB, aabb.getIntersection( extendedBlockAABB ), level ); + + // if (partly) infinite aabb does not intersect with the extended block AABB, return an empty interval + if( cellBB.empty() ) return CellInterval(); + } + else + { + blockStorage.getCellBBFromAABB( cellBB, aabb, blockStorage.getLevel(block) ); + } + + WALBERLA_ASSERT( !cellBB.empty() ); + + cellBB.xMin() -= cell_idx_t(1); cellBB.yMin() -= cell_idx_t(1); cellBB.zMin() -= cell_idx_t(1); + cellBB.xMax() += cell_idx_t(1); cellBB.yMax() += cell_idx_t(1); cellBB.zMax() += cell_idx_t(1); + + CellInterval blockBB = blockStorage.getBlockCellBB( block ); + + cell_idx_t layers = cell_idx_c( numberOfGhostLayersToInclude ); + + blockBB.xMin() -= layers; blockBB.yMin() -= layers; blockBB.zMin() -= layers; + blockBB.xMax() += layers; blockBB.yMax() += layers; blockBB.zMax() += layers; + + cellBB.intersect( blockBB ); + + blockStorage.transformGlobalToBlockLocalCellInterval( cellBB, block ); + + return cellBB; + +} + +template< typename ParticleAccessor_T > +CellInterval getParticleCellBB(const size_t particleIdx, const ParticleAccessor_T& ac, + const IBlock & block, StructuredBlockStorage & blockStorage, + const uint_t numberOfGhostLayersToInclude) +{ + return getCellBBFromAABB(mesa_pd::getParticleAABB(particleIdx, ac), + mesa_pd::data::particle_flags::isSet( ac.getFlags(particleIdx), mesa_pd::data::particle_flags::INFINITE), + block, blockStorage, numberOfGhostLayersToInclude ); +} + + +} // namespace lbm_mesapd_coupling +} // namespace walberla diff --git a/src/lbm_mesapd_coupling/mapping/ParticleMapping.h b/src/lbm_mesapd_coupling/mapping/ParticleMapping.h new file mode 100644 index 0000000000000000000000000000000000000000..507156382398311804affce080bc800df8abc1ad --- /dev/null +++ b/src/lbm_mesapd_coupling/mapping/ParticleMapping.h @@ -0,0 +1,122 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla is free software: you can +// redistribute it and/or modify it under the terms of the GNU General Public +// License as published by the Free Software Foundation, either version 3 of +// the License, or (at your option) any later version. +// +// waLBerla is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +// for more details. +// +// You should have received a copy of the GNU General Public License along +// with waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file ParticleMapping.h +//! \ingroup lbm_mesapd_coupling +//! \author Christoph Rettinger <christoph.rettinger@fau.de> +// +//====================================================================================================================== + +#pragma once + +#include "ParticleBoundingBox.h" + +#include "core/debug/Debug.h" +#include "core/math/Vector3.h" +#include "domain_decomposition/StructuredBlockStorage.h" +#include "field/FlagField.h" + +#include "mesa_pd/common/Contains.h" +#include "mesa_pd/data/IAccessor.h" +#include "mesa_pd/kernel/SingleCast.h" + +#include <functional> + +namespace walberla { +namespace lbm_mesapd_coupling { + +/* + * Kernel that can be used to map MESA_PD particles into the domain with a given LBM boundary flag. + * In coupled simulations, usually carried out for walls or fixed particles. + * + */ +template< typename BoundaryHandling_T> +class ParticleMappingKernel +{ +public: + ParticleMappingKernel(const shared_ptr<StructuredBlockStorage> & blockStorage, const BlockDataID & boundaryHandlingID) + : blockStorage_(blockStorage), boundaryHandlingID_(boundaryHandlingID){} + + template<typename ParticleAccessor_T> + void operator()(const size_t particleIdx, const ParticleAccessor_T& ac, const FlagUID & obstacle ) + { + static_assert(std::is_base_of<mesa_pd::data::IAccessor, ParticleAccessor_T>::value, "Provide a valid accessor as template"); + + for( auto blockIt = blockStorage_->begin(); blockIt != blockStorage_->end(); ++blockIt ) + { + mapParticleOnBlock( particleIdx, ac, *blockIt, obstacle ); + } + } + +private: + + template< typename ParticleAccessor_T > + void mapParticleOnBlock( const size_t particleIdx, const ParticleAccessor_T& ac, + IBlock & block, const FlagUID & obstacle ) + { + static_assert(std::is_base_of<mesa_pd::data::IAccessor, ParticleAccessor_T>::value, "Provide a valid accessor as template"); + + WALBERLA_ASSERT_EQUAL( &block.getBlockStorage(), &(blockStorage_->getBlockStorage()) ); + + BoundaryHandling_T * boundaryHandling = block.getData< BoundaryHandling_T >( boundaryHandlingID_); + WALBERLA_ASSERT_NOT_NULLPTR( boundaryHandling ); + + auto * flagField = boundaryHandling->getFlagField(); + WALBERLA_ASSERT_NOT_NULLPTR( flagField ); + WALBERLA_ASSERT( flagField->flagExists( obstacle ) ); + + const auto obstacleFlag = flagField->getFlag( obstacle ); + + auto cellBB = getParticleCellBB(particleIdx, ac, block, *blockStorage_, flagField->nrOfGhostLayers() ); + + if( cellBB.empty() ) return; + + mesa_pd::kernel::SingleCast singleCast; + mesa_pd::ContainsPointFunctor containsPointFctr; + + Vector3<real_t> startCellCenter = blockStorage_->getBlockLocalCellCenter( block, cellBB.min() ); + auto blockLevel = blockStorage_->getLevel(block); + const real_t dx = blockStorage_->dx( blockLevel ); + const real_t dy = blockStorage_->dy( blockLevel ); + const real_t dz = blockStorage_->dz( blockLevel ); + + real_t cz = startCellCenter[2]; + for( cell_idx_t z = cellBB.zMin(); z <= cellBB.zMax(); ++z ) + { + real_t cy = startCellCenter[1]; + for( cell_idx_t y = cellBB.yMin(); y <= cellBB.yMax(); ++y ) + { + real_t cx = startCellCenter[0]; + for( cell_idx_t x = cellBB.xMin(); x <= cellBB.xMax(); ++x ) + { + if( singleCast(particleIdx, ac, containsPointFctr, ac, Vector3<real_t>(cx,cy,cz)) ) + { + boundaryHandling->forceBoundary(obstacleFlag, x, y, z); + } + cx += dx; + } + cy += dy; + } + cz += dz; + } + } + + shared_ptr<StructuredBlockStorage> blockStorage_; + BlockDataID boundaryHandlingID_; +}; + + +} // namespace lbm_mesapd_coupling +} // namespace walberla diff --git a/src/lbm_mesapd_coupling/momentum_exchange_method/MovingParticleMapping.h b/src/lbm_mesapd_coupling/momentum_exchange_method/MovingParticleMapping.h new file mode 100644 index 0000000000000000000000000000000000000000..29c161d4f0378d60e416e4dac2a3e887d2185ccd --- /dev/null +++ b/src/lbm_mesapd_coupling/momentum_exchange_method/MovingParticleMapping.h @@ -0,0 +1,354 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla is free software: you can +// redistribute it and/or modify it under the terms of the GNU General Public +// License as published by the Free Software Foundation, either version 3 of +// the License, or (at your option) any later version. +// +// waLBerla is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +// for more details. +// +// You should have received a copy of the GNU General Public License along +// with waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file MovingParticleMapping.h +//! \ingroup lbm_mesapd_coupling +//! \author Christoph Rettinger <christoph.rettinger@fau.de> +// +//====================================================================================================================== + +#pragma once + +#include "core/debug/Debug.h" +#include "core/cell/Cell.h" +#include "domain_decomposition/StructuredBlockStorage.h" +#include "field/FlagField.h" + +#include "lbm_mesapd_coupling/DataTypes.h" +#include "lbm_mesapd_coupling/mapping/ParticleBoundingBox.h" +#include "lbm_mesapd_coupling/utility/ParticleSelector.h" +#include "lbm_mesapd_coupling/utility/ParticleFunctions.h" + +#include "mesa_pd/common/Contains.h" +#include "mesa_pd/data/IAccessor.h" +#include "mesa_pd/kernel/SingleCast.h" +#include "mesa_pd/data/Flags.h" + +#include <functional> + +namespace walberla { +namespace lbm_mesapd_coupling { + +/*!\brief Maps the moving particles into the simulation domain and updates the mapping + * + * Cells that are inside a particle, will be marked with the 'obstacle' flag. + * 'Inside' means that the cell center is contained in the particle. + * Thus, a containsPoint function has to be available for all particles/shapes. + * + * Cells that in the last time step were inside the particle but are now outside of it, i.e. the particle has moved, + * will be marked with the 'formerObstacle' flag. + * The 'formerObstacle' flag is used in a second step by the PDFReconstruction class (see PDFReconstruction.h) to + * re-initialize the missing PDFs. Afterwards, the 'formerObstacle' flag is removed and the 'fluid' flag is set. + * + * It is not strictly required that the mapping has been initialized with the mapping kernel from below. + * + * The 'mappingParticleSelector' can be used to include/exclude certain particles from the mapping. + */ +template< typename PdfField_T, typename BoundaryHandling_T, typename ParticleAccessor_T, typename ParticleSelector_T> +class MovingParticleMapping +{ + static_assert(std::is_base_of<mesa_pd::data::IAccessor, ParticleAccessor_T>::value, "Provide a valid accessor as template"); + +public: + + using FlagField_T = typename BoundaryHandling_T::FlagField; + using flag_t = typename BoundaryHandling_T::flag_t; + + MovingParticleMapping( const shared_ptr<StructuredBlockStorage> & blockStorage, + const BlockDataID & pdfFieldID, + const BlockDataID & boundaryHandlingID, + const BlockDataID & particleFieldID, + const shared_ptr<ParticleAccessor_T>& ac, + const FlagUID & obstacle, const FlagUID & formerObstacle, + const ParticleSelector_T& mappingParticleSelector, + bool conserveMomentumWhenMapping) + : blockStorage_( blockStorage ), pdfFieldID_( pdfFieldID ), boundaryHandlingID_( boundaryHandlingID ), + particleFieldID_( particleFieldID ), ac_( ac ), + obstacle_( obstacle ), formerObstacle_( formerObstacle ), mappingParticleSelector_( mappingParticleSelector ), + conserveMomentumWhenMapping_( conserveMomentumWhenMapping ) + {} + + void inline operator()( IBlock * const block ) + { + WALBERLA_ASSERT_NOT_NULLPTR( block ); + + PdfField_T * pdfField = block->getData< PdfField_T >( pdfFieldID_ ); + BoundaryHandling_T * boundaryHandling = block->getData< BoundaryHandling_T >( boundaryHandlingID_ ); + auto * flagField = boundaryHandling->getFlagField(); + ParticleField_T * particleField = block->getData< ParticleField_T >( particleFieldID_ ); + + WALBERLA_ASSERT_NOT_NULLPTR( boundaryHandling ); + WALBERLA_ASSERT_NOT_NULLPTR( flagField ); + WALBERLA_ASSERT_NOT_NULLPTR( particleField ); + WALBERLA_ASSERT_EQUAL( flagField->xyzSize(), particleField->xyzSize() ); + WALBERLA_ASSERT( flagField->flagExists( obstacle_ ) ); + + const flag_t obstacle = flagField->getFlag( obstacle_ ); + const flag_t formerObstacle = flagField->flagExists( formerObstacle_ ) ? flagField->getFlag( formerObstacle_ ) : + flagField->registerFlag( formerObstacle_ ); + + const real_t dx = blockStorage_->dx( blockStorage_->getLevel(*block) ); + const real_t dy = blockStorage_->dy( blockStorage_->getLevel(*block) ); + const real_t dz = blockStorage_->dz( blockStorage_->getLevel(*block) ); + + for( size_t idx = 0; idx < ac_->size(); ++idx ) + { + if (mappingParticleSelector_(idx, *ac_)) + { + mapParticleAndUpdateMapping( idx, block, pdfField, boundaryHandling, flagField, particleField, obstacle, formerObstacle, dx, dy, dz ); + } + } + } + +private: + + void inline mapParticleAndUpdateMapping(const size_t particleIdx, IBlock * const block, + PdfField_T * pdfField, BoundaryHandling_T * boundaryHandling, FlagField_T * flagField, ParticleField_T * particleField, + const flag_t & obstacle, const flag_t & formerObstacle, + real_t dx, real_t dy, real_t dz) + { + // policy: every particle manages only its own flags + + auto cellBB = getParticleCellBB( particleIdx, *ac_, *block, *blockStorage_, flagField->nrOfGhostLayers() ); + + Vector3<real_t> startCellCenter = blockStorage_->getBlockLocalCellCenter( *block, cellBB.min() ); + + mesa_pd::kernel::SingleCast singleCast; + mesa_pd::ContainsPointFunctor containsPointFctr; + + auto particleUid = ac_->getUid(particleIdx); + + Vector3<real_t> currentCellCenter = startCellCenter; + for( cell_idx_t z = cellBB.zMin(); z <= cellBB.zMax(); ++z ) + { + currentCellCenter[1] = startCellCenter[1]; + for( cell_idx_t y = cellBB.yMin(); y <= cellBB.yMax(); ++y ) + { + currentCellCenter[0] = startCellCenter[0]; + for( cell_idx_t x = cellBB.xMin(); x <= cellBB.xMax(); ++x ) + { + + flag_t & cellFlagPtr = flagField->get(x,y,z); + + if( singleCast(particleIdx, *ac_, containsPointFctr, *ac_, currentCellCenter) ) + { + // cell is inside particle, now check the flag of the cell + // and let this cell refer to the present particle (via Uid) + + // cell is currently fluid and now newly occupied by the particle + if( boundaryHandling->isDomain(x,y,z)) + { + // set obstacle flag + boundaryHandling->forceBoundary( obstacle, x, y, z ); + (*particleField)(x, y, z) = particleUid; + + if( conserveMomentumWhenMapping_ && pdfField->isInInnerPart(Cell(x,y,z)) ) { + // this removes the fluid from the simulation, together with the containing momentum + // to ensure momentum conservation, the momentum of this fluid cell has to be added to the particle + // see Aidun, C. K., Lu, Y., & Ding, E. J. (1998). Direct analysis of particulate suspensions with inertia using the discrete Boltzmann equation. Journal of Fluid Mechanics, 373, 287-311. + + // force = momentum / dt, with dt = 1 + Vector3<real_t> force = pdfField->getMomentumDensity(x, y, z); + lbm_mesapd_coupling::addHydrodynamicForceAtWFPosAtomic( particleIdx, *ac_, force, currentCellCenter ); + } + + } + + // cell is already an obstacle (maybe from another particle) + if( isFlagSet( cellFlagPtr, obstacle ) ) { + auto formerParticleUid = (*particleField)(x, y, z); + auto formerParticleIdx = ac_->uidToIdx(formerParticleUid); + if(!isSet(ac_->getFlags(formerParticleIdx), mesa_pd::data::particle_flags::FIXED) ) + { + // only claim this cell if it not from a fixed particle, i.e. the fixed particle keeps the cell in all cases + // this is IMPORTANT to not mess up the mapping of cells that are contained inside fixed cells but are sporadically + // occupied by a moving particle as well (during collision and the there present overlap), + // since the fixed particle usually do not get remapped in every timestep + (*particleField)(x, y, z) = particleUid; + } + } + + // cell is a former obstacle cell (maybe from another particle that has moved away) + if( isFlagSet( cellFlagPtr, formerObstacle ) ) + { + boundaryHandling->setBoundary( obstacle, x, y, z ); + removeFlag( cellFlagPtr, formerObstacle ); + (*particleField)(x, y, z) = particleUid; + } + + // else: IMPORTANT in all other cases, another boundary flag is already present in the cell + // this could e.g. be an inflow boundary condition + // we do NOT want to overwrite this boundary condition and thus skip it, even though the particle overlaps + + } + else + { + // cell is outside particle + if( isFlagSet( cellFlagPtr, obstacle ) && ((*particleField)(x,y,z) == particleUid) ) + { + // cell was previously occupied by this particle + boundaryHandling->removeBoundary( obstacle, x, y, z ); + addFlag( cellFlagPtr, formerObstacle ); + // entry at (*particleField)(x,y,z) should still point to the previous particle. + // If during initialization the overlap between neighboring blocks + // was chosen correctly/large enough, the particle should still be on this block. + // The particle information is needed in the PDF reconstruction step. + // There, the flag will be removed and replaced by a domain flag after reconstruction. + } + } + currentCellCenter[0] += dx; + } + currentCellCenter[1] += dy; + } + currentCellCenter[2] += dz; + } + } + + shared_ptr<StructuredBlockStorage> blockStorage_; + + const BlockDataID pdfFieldID_; + const BlockDataID boundaryHandlingID_; + const BlockDataID particleFieldID_; + + shared_ptr<ParticleAccessor_T> ac_; + + const FlagUID obstacle_; + const FlagUID formerObstacle_; + + ParticleSelector_T mappingParticleSelector_; + + bool conserveMomentumWhenMapping_; + +}; + + +template< typename PdfField_T, typename BoundaryHandling_T, typename ParticleAccessor_T, typename ParticleSelector_T> +MovingParticleMapping< PdfField_T, BoundaryHandling_T, ParticleAccessor_T, ParticleSelector_T> +makeMovingParticleMapping( const shared_ptr<StructuredBlockStorage> & blockStorage, + const BlockDataID & pdfFieldID, + const BlockDataID & boundaryHandlingID, + const BlockDataID & particleFieldID, + const shared_ptr<ParticleAccessor_T>& ac, + const FlagUID & obstacle, const FlagUID & formerObstacle, + const ParticleSelector_T& mappingParticleSelector, + bool conserveMomentumWhenMapping) +{ + return MovingParticleMapping<PdfField_T, BoundaryHandling_T, ParticleAccessor_T, ParticleSelector_T> + (blockStorage, pdfFieldID, boundaryHandlingID, particleFieldID, ac, obstacle, formerObstacle, mappingParticleSelector, conserveMomentumWhenMapping); +} + +//////////////////// +// MAPPING KERNEL // +//////////////////// + + +/*!\brief Kernel that maps all particles onto all blocks to the "moving obstacle" boundary condition, that requires the additional particleField + * + * Cells that are inside the particles are set to 'obstacle' and the UID of the particle is stored in the particleField for later reference. + * + * Example: + * lbm_mesapd_coupling::MovingParticleMappingKernel<BoundaryHandling_T> movingParticleMappingKernel(blocks, boundaryHandlingID, particleFieldID); + * ps->forEachParticle(false, lbm_mesapd_coupling::RegularParticlesSelector(), *accessor, movingParticleMappingKernel, *accessor, MO_Flag); + * + * This kernel is usually applied before the start of the simulation, after the particle and fluid data structures have been created. + * As a result, a consistent mapping of the particles into the domain is obtained. + * For the update of the mapping during time stepping, use the class MovingParticleMapping (above). + * + * This mapping can also be used for stationary particle if the force and torque on this particle are to be evaluated. + */ +template< typename BoundaryHandling_T> +class MovingParticleMappingKernel +{ +public: + MovingParticleMappingKernel(const shared_ptr<StructuredBlockStorage> & blockStorage, const BlockDataID & boundaryHandlingID, const BlockDataID & particleFieldID) + : blockStorage_(blockStorage), boundaryHandlingID_(boundaryHandlingID), particleFieldID_(particleFieldID){} + + template<typename ParticleAccessor_T> + void operator()(const size_t particleIdx, const ParticleAccessor_T& ac, const FlagUID & obstacle ) + { + static_assert(std::is_base_of<mesa_pd::data::IAccessor, ParticleAccessor_T>::value, "Provide a valid accessor as template"); + + for( auto blockIt = blockStorage_->begin(); blockIt != blockStorage_->end(); ++blockIt ) + { + mapMovingParticleOnBlock( particleIdx, ac, *blockIt, obstacle ); + } + } + +private: + + template< typename ParticleAccessor_T > + void mapMovingParticleOnBlock( const size_t particleIdx, const ParticleAccessor_T& ac, + IBlock & block, const FlagUID & obstacle ) + { + static_assert(std::is_base_of<mesa_pd::data::IAccessor, ParticleAccessor_T>::value, "Provide a valid accessor as template"); + + WALBERLA_ASSERT_EQUAL( &block.getBlockStorage(), &(blockStorage_->getBlockStorage()) ); + + BoundaryHandling_T * boundaryHandling = block.getData< BoundaryHandling_T >( boundaryHandlingID_); + WALBERLA_ASSERT_NOT_NULLPTR( boundaryHandling ); + + auto * flagField = boundaryHandling->getFlagField(); + WALBERLA_ASSERT_NOT_NULLPTR( flagField ); + WALBERLA_ASSERT( flagField->flagExists( obstacle ) ); + + const auto obstacleFlag = flagField->getFlag( obstacle ); + + auto * particleField = block.getData< ParticleField_T >( particleFieldID_ ); + WALBERLA_ASSERT_NOT_NULLPTR( particleField ); + WALBERLA_ASSERT_EQUAL( flagField->xyzSize(), particleField->xyzSize() ); + + auto cellBB = getParticleCellBB(particleIdx, ac, block, *blockStorage_, flagField->nrOfGhostLayers() ); + + if( cellBB.empty() ) return; + + mesa_pd::kernel::SingleCast singleCast; + mesa_pd::ContainsPointFunctor containsPointFctr; + + Vector3<real_t> startCellCenter = blockStorage_->getBlockLocalCellCenter( block, cellBB.min() ); + auto blockLevel = blockStorage_->getLevel(block); + const real_t dx = blockStorage_->dx( blockLevel ); + const real_t dy = blockStorage_->dy( blockLevel ); + const real_t dz = blockStorage_->dz( blockLevel ); + + real_t cz = startCellCenter[2]; + for( cell_idx_t z = cellBB.zMin(); z <= cellBB.zMax(); ++z ) + { + real_t cy = startCellCenter[1]; + for( cell_idx_t y = cellBB.yMin(); y <= cellBB.yMax(); ++y ) + { + real_t cx = startCellCenter[0]; + for( cell_idx_t x = cellBB.xMin(); x <= cellBB.xMax(); ++x ) + { + if( singleCast(particleIdx, ac, containsPointFctr, ac, Vector3<real_t>(cx,cy,cz)) ) + { + boundaryHandling->forceBoundary(obstacleFlag, x, y, z); + (*particleField)(x,y,z) = ac.getUid(particleIdx); + } + cx += dx; + } + cy += dy; + } + cz += dz; + } + } + + shared_ptr<StructuredBlockStorage> blockStorage_; + BlockDataID boundaryHandlingID_; + BlockDataID particleFieldID_; +}; + + +} // namespace lbm_rpd_coupling +} // namespace walberla diff --git a/src/lbm_mesapd_coupling/momentum_exchange_method/boundary/CurvedLinear.h b/src/lbm_mesapd_coupling/momentum_exchange_method/boundary/CurvedLinear.h new file mode 100644 index 0000000000000000000000000000000000000000..eeba79e77a31c0989ea397c859babb05c6eca8a0 --- /dev/null +++ b/src/lbm_mesapd_coupling/momentum_exchange_method/boundary/CurvedLinear.h @@ -0,0 +1,294 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla is free software: you can +// redistribute it and/or modify it under the terms of the GNU General Public +// License as published by the Free Software Foundation, either version 3 of +// the License, or (at your option) any later version. +// +// waLBerla is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +// for more details. +// +// You should have received a copy of the GNU General Public License along +// with waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file CurvedLinear.h +//! \ingroup lbm_mesapd_coupling +//! \author Christoph Rettinger <christoph.rettinger@fau.de> +// +//====================================================================================================================== + +#pragma once + +#include "boundary/Boundary.h" + +#include "core/DataTypes.h" +#include "core/cell/CellInterval.h" +#include "core/config/Config.h" +#include "core/debug/Debug.h" +#include "core/logging/all.h" + +#include "field/FlagField.h" + +#include "lbm/field/PdfField.h" + +#include "lbm_mesapd_coupling/DataTypes.h" +#include "lbm_mesapd_coupling/utility/ParticleFunctions.h" + +#include "mesa_pd/common/ParticleFunctions.h" +#include "mesa_pd/common/RayParticleIntersection.h" +#include "mesa_pd/data/IAccessor.h" +#include "mesa_pd/kernel/SingleCast.h" + +#include "stencil/Directions.h" + +namespace walberla { +namespace lbm_mesapd_coupling { + +//************************************************************************************************************************************** +/*! +* \brief Linear boundary handling for moving particles +* +* This boundary condition implements the CLI scheme from +* Ginzburg et al. - "Two-Relaxation-Time Lattice Boltzmann Scheme: About Parametrization, Velocity, Pressure and Mixed Boundary Conditions" (2008) +* References to equations and tables in the code documentation are contained in this paper. +* +* It uses one additional cell in inverse direction of the boundary handling for a linear interpolation. +* +* In case not enough cells are available, one fall back solution is available: +* If no additional cell is available, the scheme is replaced by the bounce back scheme (see SimpleBB.h). +* +*/ +//************************************************************************************************************************************** +template< typename LatticeModel_T, typename FlagField_T, typename ParticleAccessor_T > +class CurvedLinear : public Boundary< typename FlagField_T::flag_t > +{ + using PDFField_T = lbm::PdfField< LatticeModel_T >; + using Stencil_T = typename LatticeModel_T::Stencil; + using flag_t = typename FlagField_T::flag_t; + + static_assert(std::is_base_of<mesa_pd::data::IAccessor, ParticleAccessor_T>::value, "Provide a valid accessor as template"); + +public: + + static shared_ptr<BoundaryConfiguration> createConfiguration( const Config::BlockHandle& ) + { + WALBERLA_ABORT( "A CurvedLinear boundary cannot be created from a config file" ); + return make_shared<BoundaryConfiguration>(); + } + + inline CurvedLinear( const BoundaryUID & boundaryUID, const FlagUID & uid, PDFField_T * const pdfField, const FlagField_T * const flagField, + ParticleField_T * const particleField, const shared_ptr<ParticleAccessor_T>& ac, + const flag_t domain, const StructuredBlockStorage & blockStorage, const IBlock & block ); + + void pushFlags( std::vector< FlagUID >& uids ) const { uids.push_back( uid_ ); } + + void beforeBoundaryTreatment() const {} + void afterBoundaryTreatment() const {} + + template< typename Buffer_T > + void packCell( Buffer_T &, const cell_idx_t, const cell_idx_t, const cell_idx_t ) const {} + + template< typename Buffer_T > + void registerCell( Buffer_T &, const flag_t, const cell_idx_t, const cell_idx_t, const cell_idx_t ) {} + + void registerCell( const flag_t, const cell_idx_t, const cell_idx_t, const cell_idx_t, const BoundaryConfiguration& ) {} + void registerCells( const flag_t, const CellInterval&, const BoundaryConfiguration& ) {} + template< typename CellIterator > + void registerCells( const flag_t, const CellIterator&, const CellIterator&, const BoundaryConfiguration& ) {} + + void unregisterCell( const flag_t, const cell_idx_t, const cell_idx_t, const cell_idx_t ) const {} + + inline void treatDirection( const cell_idx_t x, const cell_idx_t y, const cell_idx_t z, const stencil::Direction dir, + const cell_idx_t nx, const cell_idx_t ny, const cell_idx_t nz, const flag_t mask ); + +private: + + const FlagUID uid_; + + + PDFField_T * const pdfField_; + const FlagField_T * const flagField_; + ParticleField_T * const particleField_; + + shared_ptr<ParticleAccessor_T> ac_; + + flag_t domainMask_; + + const StructuredBlockStorage & blockStorage_; + const IBlock & block_; + + real_t lengthScalingFactor_; + real_t forceScalingFactor_; + +}; // class CurvedLinear + + +template< typename LatticeModel_T, typename FlagField_T, typename ParticleAccessor_T > +inline CurvedLinear< LatticeModel_T, FlagField_T, ParticleAccessor_T >::CurvedLinear( const BoundaryUID & boundaryUID, const FlagUID & uid, PDFField_T * const pdfField, const FlagField_T * const flagField, + ParticleField_T * const particleField, const shared_ptr<ParticleAccessor_T>& ac, + const flag_t domain, const StructuredBlockStorage & blockStorage, const IBlock & block ): +Boundary<flag_t>( boundaryUID ), uid_( uid ), pdfField_( pdfField ), flagField_( flagField ), particleField_( particleField ), ac_( ac ), domainMask_(domain), blockStorage_( blockStorage ), block_( block ) +{ + WALBERLA_ASSERT_NOT_NULLPTR( pdfField_ ); + WALBERLA_ASSERT_NOT_NULLPTR( flagField_ ); + WALBERLA_ASSERT_NOT_NULLPTR( particleField_ ); + WALBERLA_ASSERT( flagField_->isRegistered( domainMask_ ) ); + + // force scaling factor to account for different dx on this block + const real_t dxCurrentLevel = blockStorage_.dx( blockStorage_.getLevel(block) ); + lengthScalingFactor_ = dxCurrentLevel; + forceScalingFactor_ = lengthScalingFactor_ * lengthScalingFactor_; +} + + +// requires: getVelocityAtPosition (was velFromWF), addForceAtPosition (was addForceAtPos), intersectionRatio? +template< typename LatticeModel_T, typename FlagField_T, typename ParticleAccessor_T > +#ifndef NDEBUG +inline void CurvedLinear< LatticeModel_T, FlagField_T, ParticleAccessor_T >::treatDirection( const cell_idx_t x, const cell_idx_t y, const cell_idx_t z, const stencil::Direction dir, + const cell_idx_t nx, const cell_idx_t ny, const cell_idx_t nz, const flag_t mask ) +#else +inline void CurvedLinear< LatticeModel_T, FlagField_T, ParticleAccessor_T >::treatDirection( const cell_idx_t x, const cell_idx_t y, const cell_idx_t z, const stencil::Direction dir, + const cell_idx_t nx, const cell_idx_t ny, const cell_idx_t nz, const flag_t /*mask*/ ) +#endif +{ + WALBERLA_ASSERT_EQUAL ( nx, x + cell_idx_t( stencil::cx[ dir ] ) ); + WALBERLA_ASSERT_EQUAL ( ny, y + cell_idx_t( stencil::cy[ dir ] ) ); + WALBERLA_ASSERT_EQUAL ( nz, z + cell_idx_t( stencil::cz[ dir ] ) ); + WALBERLA_ASSERT_UNEQUAL( mask & this->mask_, numeric_cast<flag_t>(0) ); + WALBERLA_ASSERT_EQUAL ( mask & this->mask_, this->mask_ ); // only true if "this->mask_" only contains one single flag, which is the case for the + // current implementation of this boundary condition + WALBERLA_ASSERT_UNEQUAL( particleField_->get(nx,ny,nz), ac_->getInvalidUid() ); + + // determine distance to real boundary, i.e. delta value + // cell center of the near-boundary fluid cell + Cell nearBoundaryCell(x,y,z); + Vector3< real_t > cellCenter = blockStorage_.getBlockLocalCellCenter(block_, nearBoundaryCell); + + // direction of the ray (from the fluid cell center to the boundary cell) + Vector3< real_t > direction( lengthScalingFactor_ * real_c( stencil::cx[ dir ] ), + lengthScalingFactor_ * real_c( stencil::cy[ dir ] ), + lengthScalingFactor_ * real_c( stencil::cz[ dir ] ) ); + + //get particle index + auto particleIdx = ac_->uidToIdx(particleField_->get( nx, ny, nz )); + WALBERLA_ASSERT_UNEQUAL( particleIdx, ac_->getInvalidIdx(), "Index of particle is invalid!" ); + + real_t delta = real_t(0); + real_t pdf_new = real_t(0); + real_t alpha = real_t(0); + + // get the cell indices of the cell further away from the obstacle + const cell_idx_t xff = x - cell_idx_c( stencil::cx[ dir ] ); + const cell_idx_t yff = y - cell_idx_c( stencil::cy[ dir ] ); + const cell_idx_t zff = z - cell_idx_c( stencil::cz[ dir ] ); + + // check if CLI can be applied, i.e. the further-away-cell has to be a valid (i.e. fluid) cell + if( flagField_->isPartOfMaskSet( xff, yff, zff, domainMask_ ) ) + { + // apply CLI scheme + + // delta value obtained by ray - particle intersection + // depending on the implementation for the specific particle, either an analytical formula (e.g. for the sphere) or a line search algorithm is used + + // (if applicable) line search accuracy + const real_t tolerance = real_t( 1e-4 ) * lengthScalingFactor_; + + mesa_pd::kernel::SingleCast singleCast; + mesa_pd::RayParticleIntersectionRatioFunctor intersectionRatioFctr; + delta = singleCast(particleIdx, *ac_, intersectionRatioFctr, *ac_, cellCenter, direction, tolerance ); + + WALBERLA_ASSERT_LESS_EQUAL( delta, real_t(1)); + WALBERLA_ASSERT_GREATER_EQUAL( delta, real_t(0)); + + // coefficients from Table 3 + const real_t kappa0 = ( real_t(1) - real_t(2) * delta ) / ( real_t(1) + real_t(2) * delta ); + + // Eq. (4.1) + pdf_new = /*kappa1*/ pdfField_->get( x , y , z , Stencil_T::idx[dir] ) + + kappa0 * pdfField_->get( xff, yff, zff, Stencil_T::idx[dir] ) + - kappa0 * pdfField_->get( x , y , z , Stencil_T::invDirIdx(dir) ); + + // Table 4 + alpha = real_t(4) / ( real_t(1) + real_t(2) * delta); + + } + else + { + // fall back to Simple Bounce back + + delta = real_t(0.5); + pdf_new = pdfField_->get( x, y, z, Stencil_T::idx[dir] ); + alpha = real_t(2); + } + + // assumed boundary position + const Vector3<real_t> boundaryPosition( cellCenter + delta * direction ); + + // obtain the velocity at the obstacle boundary + auto boundaryVelocity = mesa_pd::getVelocityAtWFPoint(particleIdx, *ac_, boundaryPosition ); + + // include effect of boundary velocity + if( LatticeModel_T::compressible ) + { + const auto density = pdfField_->getDensity(x,y,z); + pdf_new -= real_t(3.0) * alpha * density * LatticeModel_T::w[ Stencil_T::idx[dir] ] * + ( real_c( stencil::cx[ dir ] ) * boundaryVelocity[0] + + real_c( stencil::cy[ dir ] ) * boundaryVelocity[1] + + real_c( stencil::cz[ dir ] ) * boundaryVelocity[2] ); + } + else + { + pdf_new -= real_t(3.0) * alpha * LatticeModel_T::w[ Stencil_T::idx[dir] ] * + ( real_c( stencil::cx[ dir ] ) * boundaryVelocity[0] + + real_c( stencil::cy[ dir ] ) * boundaryVelocity[1] + + real_c( stencil::cz[ dir ] ) * boundaryVelocity[2] ); + } + + // carry out the boundary handling + pdfField_->get( nx, ny, nz, Stencil_T::invDirIdx(dir) ) = pdf_new; + + // check if fluid cell is not inside ghost layer ( = is in inner part), since then no force is allowed to be added + if( !pdfField_->isInInnerPart( nearBoundaryCell ) ) + { + return; + } + + // calculate the force on the obstacle + // original work (MEM): Ladd - Numerical simulations of particulate suspensions via a discretized Boltzmann equation. Part 1. Theoretical foundation (1994) + // improved version (GIMEM): Wen et al. - Galilean invariant fluid-solid interfacial dynamics in lattice Boltzmann simulations (2014) + + real_t pdf_old = pdfField_->get( x, y, z, Stencil_T::idx[dir] ); + + if(LatticeModel_T::compressible) + { + // this artificially introduces the zero-centering of the PDF values here (see also /lbm/field/PdfField.h) + // it is important to have correct force values when two or more particles are so close such that there is no fluid cell between + // as a consequence, some (non-zero) PDF contributions would be missing after summing up the force contributions + // those would need to be added artificially, see e.g. Ernst, Dietzel, Sommerfeld - A lattice Boltzmann method for simulating transport and agglomeration of resolved particles, Acta Mech, 2013 + // instead, we use the trick there that we just require the deviations from the equilibrium to get the correct force as it is already used for the incompressible case + pdf_old -= LatticeModel_T::w[ Stencil_T::idx[dir] ]; + pdf_new -= LatticeModel_T::w[ Stencil_T::idx[dir] ]; + } + + // MEM: F = pdf_old + pdf_new - common + const real_t forceMEM = pdf_old + pdf_new; + + // correction from Wen + const real_t correction = pdf_old - pdf_new; + + // force consists of the MEM part and the galilean invariance correction including the boundary velocity + Vector3<real_t> force( real_c( stencil::cx[dir] ) * forceMEM - correction * boundaryVelocity[0], + real_c( stencil::cy[dir] ) * forceMEM - correction * boundaryVelocity[1], + real_c( stencil::cz[dir] ) * forceMEM - correction * boundaryVelocity[2] ); + + force *= forceScalingFactor_; + + // add the force onto the particle at the obstacle boundary + lbm_mesapd_coupling::addHydrodynamicForceAtWFPosAtomic( particleIdx, *ac_, force, boundaryPosition ); + +} + +} // namespace lbm_mesapd_coupling +} // namespace walberla diff --git a/src/lbm_mesapd_coupling/momentum_exchange_method/boundary/SimpleBB.h b/src/lbm_mesapd_coupling/momentum_exchange_method/boundary/SimpleBB.h new file mode 100644 index 0000000000000000000000000000000000000000..aeafb2b255fd965b248c71ee71c56b3c27972348 --- /dev/null +++ b/src/lbm_mesapd_coupling/momentum_exchange_method/boundary/SimpleBB.h @@ -0,0 +1,241 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla is free software: you can +// redistribute it and/or modify it under the terms of the GNU General Public +// License as published by the Free Software Foundation, either version 3 of +// the License, or (at your option) any later version. +// +// waLBerla is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +// for more details. +// +// You should have received a copy of the GNU General Public License along +// with waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file SimpleBB.h +//! \ingroup lbm_mesapd_coupling +//! \author Christoph Rettinger <christoph.rettinger@fau.de> +// +//====================================================================================================================== + +#pragma once + +#include "boundary/Boundary.h" + +#include "core/DataTypes.h" +#include "core/cell/CellInterval.h" +#include "core/config/Config.h" +#include "core/debug/Debug.h" +#include "core/logging/all.h" + +#include "field/FlagField.h" + +#include "lbm/field/PdfField.h" + +#include "lbm_mesapd_coupling/DataTypes.h" +#include "lbm_mesapd_coupling/utility/ParticleFunctions.h" + +#include "mesa_pd/common/ParticleFunctions.h" +#include "mesa_pd/data/IAccessor.h" + +#include "stencil/Directions.h" + +namespace walberla { +namespace lbm_mesapd_coupling { + +//************************************************************************************************************************************** +/*! +* \brief Bounce back boundary handling for moving particles +* +* This boundary condition implements the bounce back scheme to model a no-slip boundary condition for moving particles +* +*/ +//************************************************************************************************************************************** + +template< typename LatticeModel_T, typename FlagField_T, typename ParticleAccessor_T > +class SimpleBB : public Boundary< typename FlagField_T::flag_t > +{ + using PDFField_T = lbm::PdfField< LatticeModel_T >; + using Stencil_T = typename LatticeModel_T::Stencil; + using flag_t = typename FlagField_T::flag_t; + + static_assert(std::is_base_of<mesa_pd::data::IAccessor, ParticleAccessor_T>::value, "Provide a valid accessor as template"); + +public: + + static shared_ptr<BoundaryConfiguration> createConfiguration( const Config::BlockHandle& ) + { + WALBERLA_ABORT( "A SimpleBB boundary cannot be created from a config file" ); + return make_shared<BoundaryConfiguration>(); + } + + inline SimpleBB( const BoundaryUID & boundaryUID, const FlagUID & uid, PDFField_T * const pdfField, const FlagField_T * const flagField, + ParticleField_T * const particleField, const shared_ptr<ParticleAccessor_T>& ac, + const flag_t domain, const StructuredBlockStorage & blockStorage, const IBlock & block ); + + void pushFlags( std::vector< FlagUID >& uids ) const { uids.push_back( uid_ ); } + + void beforeBoundaryTreatment() const {} + void afterBoundaryTreatment() const {} + + template< typename Buffer_T > + void packCell( Buffer_T &, const cell_idx_t, const cell_idx_t, const cell_idx_t ) const {} + + template< typename Buffer_T > + void registerCell( Buffer_T &, const flag_t, const cell_idx_t, const cell_idx_t, const cell_idx_t ) {} + + void registerCell( const flag_t, const cell_idx_t, const cell_idx_t, const cell_idx_t, const BoundaryConfiguration& ) {} + void registerCells( const flag_t, const CellInterval&, const BoundaryConfiguration& ) {} + template< typename CellIterator > + void registerCells( const flag_t, const CellIterator&, const CellIterator&, const BoundaryConfiguration& ) {} + + void unregisterCell( const flag_t, const cell_idx_t, const cell_idx_t, const cell_idx_t ) const {} + + inline void treatDirection( const cell_idx_t x, const cell_idx_t y, const cell_idx_t z, const stencil::Direction dir, + const cell_idx_t nx, const cell_idx_t ny, const cell_idx_t nz, const flag_t mask ); + +private: + + const FlagUID uid_; + + + PDFField_T * const pdfField_; + const FlagField_T * const flagField_; + ParticleField_T * const particleField_; + shared_ptr<ParticleAccessor_T> ac_; + + flag_t domainMask_; + + const StructuredBlockStorage & blockStorage_; + const IBlock & block_; + + real_t lengthScalingFactor_; + real_t forceScalingFactor_; + +}; // class SimpleBB + + +template< typename LatticeModel_T, typename FlagField_T, typename ParticleAccessor_T > +inline SimpleBB< LatticeModel_T, FlagField_T, ParticleAccessor_T >::SimpleBB( const BoundaryUID & boundaryUID, const FlagUID & uid, PDFField_T * const pdfField, const FlagField_T * const flagField, + ParticleField_T * const particleField, const shared_ptr<ParticleAccessor_T>& ac, + const flag_t domain, const StructuredBlockStorage & blockStorage, const IBlock & block ): +Boundary<flag_t>( boundaryUID ), uid_( uid ), pdfField_( pdfField ), flagField_( flagField ), particleField_( particleField ), ac_( ac ), domainMask_(domain), blockStorage_( blockStorage ), block_( block ) +{ + WALBERLA_ASSERT_NOT_NULLPTR( pdfField_ ); + WALBERLA_ASSERT_NOT_NULLPTR( flagField_ ); + WALBERLA_ASSERT_NOT_NULLPTR( particleField_ ); + WALBERLA_ASSERT( flagField_->isRegistered( domainMask_ ) ); + + // force scaling factor to account for different dx on this block + const real_t dxCurrentLevel = blockStorage_.dx( blockStorage_.getLevel(block) ); + lengthScalingFactor_ = dxCurrentLevel; + forceScalingFactor_ = lengthScalingFactor_ * lengthScalingFactor_; +} + +// requires: getVelocityAtPosition (was velFromWF), addForceAtPosition (was addForceAtPos) +template< typename LatticeModel_T, typename FlagField_T, typename ParticleAccessor_T > +#ifndef NDEBUG +inline void SimpleBB< LatticeModel_T, FlagField_T, ParticleAccessor_T >::treatDirection( const cell_idx_t x, const cell_idx_t y, const cell_idx_t z, const stencil::Direction dir, + const cell_idx_t nx, const cell_idx_t ny, const cell_idx_t nz, const flag_t mask ) +#else +inline void SimpleBB< LatticeModel_T, FlagField_T, ParticleAccessor_T >::treatDirection( const cell_idx_t x, const cell_idx_t y, const cell_idx_t z, const stencil::Direction dir, + const cell_idx_t nx, const cell_idx_t ny, const cell_idx_t nz, const flag_t /*mask*/ ) +#endif +{ + WALBERLA_ASSERT_EQUAL ( nx, x + cell_idx_t( stencil::cx[ dir ] ) ); + WALBERLA_ASSERT_EQUAL ( ny, y + cell_idx_t( stencil::cy[ dir ] ) ); + WALBERLA_ASSERT_EQUAL ( nz, z + cell_idx_t( stencil::cz[ dir ] ) ); + WALBERLA_ASSERT_UNEQUAL( mask & this->mask_, numeric_cast<flag_t>(0) ); + WALBERLA_ASSERT_EQUAL ( mask & this->mask_, this->mask_ ); // only true if "this->mask_" only contains one single flag, which is the case for the + // current implementation of this boundary condition + WALBERLA_ASSERT_UNEQUAL( particleField_->get(nx,ny,nz), ac_->getInvalidUid(), "UID of particle is invalid!" ); + + real_t pdf_old = pdfField_->get( x, y, z, Stencil_T::idx[dir] ); + + // apply bounce back scheme + const real_t delta = real_c( 0.5 ); + real_t pdf_new = pdf_old; + const real_t alpha = real_c(2); + + // get coordinates of fluid cell center + Cell nearBoundaryCell(x,y,z); + Vector3< real_t > cellCenter = blockStorage_.getBlockLocalCellCenter(block_, nearBoundaryCell ); + + // get vector from fluid cell center to boundary cell center + Vector3< real_t > direction( lengthScalingFactor_ * real_c( stencil::cx[ dir ] ), + lengthScalingFactor_ * real_c( stencil::cy[ dir ] ), + lengthScalingFactor_ * real_c( stencil::cz[ dir ] ) ); + + //get particle index + auto particleIdx = ac_->uidToIdx(particleField_->get( nx, ny, nz )); + WALBERLA_ASSERT_UNEQUAL( particleIdx, ac_->getInvalidIdx(), "Index of particle is invalid!" ); + + // assumed boundary position + const Vector3<real_t> boundaryPosition( cellCenter + delta * direction ); + + auto boundaryVelocity = mesa_pd::getVelocityAtWFPoint(particleIdx, *ac_, boundaryPosition ); + + // include effect of boundary velocity + if( LatticeModel_T::compressible ) + { + const auto density = pdfField_->getDensity(x,y,z); + pdf_new -= real_c(3.0) * alpha * density * LatticeModel_T::w[ Stencil_T::idx[dir] ] * + ( real_c( stencil::cx[ dir ] ) * boundaryVelocity[0] + + real_c( stencil::cy[ dir ] ) * boundaryVelocity[1] + + real_c( stencil::cz[ dir ] ) * boundaryVelocity[2] ); + } + else + { + pdf_new -= real_c(3.0) * alpha * LatticeModel_T::w[ Stencil_T::idx[dir] ] * + ( real_c( stencil::cx[ dir ] ) * boundaryVelocity[0] + + real_c( stencil::cy[ dir ] ) * boundaryVelocity[1] + + real_c( stencil::cz[ dir ] ) * boundaryVelocity[2] ); + } + + // carry out the boundary handling + pdfField_->get( nx, ny, nz, Stencil_T::invDirIdx(dir) ) = pdf_new; + + // check if fluid cell is not inside ghost layer ( = is in inner part), since then no force is allowed to be added + if( !pdfField_->isInInnerPart( nearBoundaryCell ) ) + { + return; + } + + // calculate the force on the obstacle + // original work (MEM): Ladd - Numerical simulations of particulate suspensions via a discretized Boltzmann equation. Part 1. Theoretical foundation (1994) + // improved version (GIMEM): Wen et al. - Galilean invariant fluid-solid interfacial dynamics in lattice Boltzmann simulations (2014) + + if(LatticeModel_T::compressible) + { + // this artificially introduces the zero-centering of the PDF values here (see also /lbm/field/PdfField.h) + // it is important to have correct force values when two or more particles are so close such that there is no fluid cell between + // as a consequence, some (non-zero) PDF contributions would be missing after summing up the force contributions + // those would need to be added artificially, see e.g. Ernst, Dietzel, Sommerfeld - A lattice Boltzmann method for simulating transport and agglomeration of resolved particles, Acta Mech, 2013 + // instead, we use the trick there that we just require the deviations from the equilibrium to get the correct force as it is already used for the incompressible case + pdf_old -= LatticeModel_T::w[ Stencil_T::idx[dir] ]; + pdf_new -= LatticeModel_T::w[ Stencil_T::idx[dir] ]; + } + + // MEM: F = pdf_old + pdf_new - common + const real_t forceMEM = pdf_old + pdf_new; + + // correction from Wen + const real_t correction = pdf_old - pdf_new; + + + // force consists of the MEM part and the galilean invariance correction including the boundary velocity + Vector3<real_t> force( real_c( stencil::cx[dir] ) * forceMEM - correction * boundaryVelocity[0], + real_c( stencil::cy[dir] ) * forceMEM - correction * boundaryVelocity[1], + real_c( stencil::cz[dir] ) * forceMEM - correction * boundaryVelocity[2] ); + + + force *= forceScalingFactor_; + + // add the force onto the particle at the obstacle boundary + lbm_mesapd_coupling::addHydrodynamicForceAtWFPosAtomic( particleIdx, *ac_, force, boundaryPosition ); + +} + +} // namespace lbm_mesapd_coupling +} // namespace walberla diff --git a/src/lbm_mesapd_coupling/momentum_exchange_method/reconstruction/ExtrapolationDirectionFinder.h b/src/lbm_mesapd_coupling/momentum_exchange_method/reconstruction/ExtrapolationDirectionFinder.h new file mode 100644 index 0000000000000000000000000000000000000000..22ebdfb3b72b2e98a43e2696d45cd316ae3c50f8 --- /dev/null +++ b/src/lbm_mesapd_coupling/momentum_exchange_method/reconstruction/ExtrapolationDirectionFinder.h @@ -0,0 +1,158 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla is free software: you can +// redistribute it and/or modify it under the terms of the GNU General Public +// License as published by the Free Software Foundation, either version 3 of +// the License, or (at your option) any later version. +// +// waLBerla is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +// for more details. +// +// You should have received a copy of the GNU General Public License along +// with waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file ExtrapolationDirectionFinder.h +//! \ingroup lbm_mesapd_coupling +//! \author Christoph Rettinger <christoph.rettinger@fau.de> +// +//====================================================================================================================== + + +#pragma once + +#include "domain_decomposition/StructuredBlockStorage.h" + +#include "field/Field.h" + +#include "mesa_pd/data/IAccessor.h" + +#include "stencil/Directions.h" +#include "stencil/D3Q6.h" +#include "stencil/D3Q27.h" + + + +namespace walberla { +namespace lbm_mesapd_coupling { + + +//************************************************************************************************************************************** +/*! + * \brief Classes to be used with some Reconstructor variants to find an extrapolation direction + * + * The goal is to find a suitable extrapolation direction which is a three dimensional vector with cell index offsets to the current cell. + * Each ExtrapolationDirectionFinder class must exactly implement the member function + * template< typename ParticleAccessor_T > + * Vector3<cell_idx_t> operator()( IBlock * const block, const cell_idx_t & x, const cell_idx_t & y, const cell_idx_t & z, + * const size_t particleIdx, const ParticleAccessor_T & ac) const + * that returns an appropriate extrapolation direction (i.e. a certain lattice direction). + * + * Different variants are available: + * + * - FlagFieldNormalExtrapolationDirectionFinder: + * Uses information from the direct (N,S,E,W,T,B) neighbors' flags to find the extrapolation direction + * by checking for the domain (here: fluid) flag. + * This can lead to incorrect directions in close packing scenarios or in the vicinity of other boundaries. + * + * - SphereNormalExtrapolationDirectionFinder: + * Calculates the local normal of the particle which is in its current form only correct for spherical shapes. + * The corresponding direction with cell offsets is then determined based on this normal. + * + */ +//************************************************************************************************************************************** + +// finds lattice direction in given stencil that corresponds best to the provided direction +template < typename Stencil_T > +Vector3<cell_idx_t> findCorrespondingLatticeDirection( const Vector3<real_t> & direction ) +{ + stencil::Direction correspondingDirection = stencil::C; + real_t innerProduct = real_t(0); + for( auto d = Stencil_T::beginNoCenter(); d != Stencil_T::end(); ++d ) + { + // compute inner product <dir,c_i> + real_t temporaryInnerProduct = direction[0] * stencil::cNorm[0][*d] + direction[1] * stencil::cNorm[1][*d] + direction[2] * stencil::cNorm[2][*d]; + if( temporaryInnerProduct > innerProduct ) + { + // largest inner product is the most fitting lattice direction + innerProduct = temporaryInnerProduct; + correspondingDirection = *d; + } + } + return Vector3<cell_idx_t>(stencil::cx[correspondingDirection], + stencil::cy[correspondingDirection], + stencil::cz[correspondingDirection]); +} + + +template< typename BoundaryHandling_T > +class FlagFieldNormalExtrapolationDirectionFinder +{ +public: + + FlagFieldNormalExtrapolationDirectionFinder( const shared_ptr<StructuredBlockStorage> & blockStorage, const BlockDataID & boundaryHandlingID ) + : blockStorage_( blockStorage ), boundaryHandlingID_( boundaryHandlingID ) + {} + + template< typename ParticleAccessor_T > + Vector3<cell_idx_t> operator()( IBlock * const block, const cell_idx_t & x, const cell_idx_t & y, const cell_idx_t & z, + const size_t /*particleIdx*/, const ParticleAccessor_T & /*ac*/) const + { + BoundaryHandling_T * boundaryHandling = block->getData< BoundaryHandling_T >( boundaryHandlingID_ ); + + WALBERLA_ASSERT_NOT_NULLPTR( boundaryHandling ); + + Vector3<cell_idx_t> extrapolationDirection; + + for( auto directNeighborDir = stencil::D3Q6::begin(); directNeighborDir != stencil::D3Q6::end(); ++directNeighborDir) + { + if( boundaryHandling->isDomain( Cell( x + directNeighborDir.cx(), y + directNeighborDir.cy(), z + directNeighborDir.cz() ) ) ) + { + extrapolationDirection[0] += directNeighborDir.cx(); + extrapolationDirection[1] += directNeighborDir.cy(); + extrapolationDirection[2] += directNeighborDir.cz(); + } + } + + return extrapolationDirection; + } + +private: + shared_ptr<StructuredBlockStorage> blockStorage_; + const BlockDataID boundaryHandlingID_; +}; + + +class SphereNormalExtrapolationDirectionFinder +{ +public: + + explicit SphereNormalExtrapolationDirectionFinder( const shared_ptr<StructuredBlockStorage> & blockStorage ) + : blockStorage_( blockStorage ) + {} + + template< typename ParticleAccessor_T > + Vector3<cell_idx_t> operator()( IBlock * const block, const cell_idx_t & x, const cell_idx_t & y, const cell_idx_t & z, + const size_t particleIdx, const ParticleAccessor_T & ac) const + { + static_assert(std::is_base_of<mesa_pd::data::IAccessor, ParticleAccessor_T>::value, "Provide a valid accessor as template"); + + real_t cx, cy, cz; + blockStorage_->getBlockLocalCellCenter( *block, Cell(x,y,z), cx, cy, cz ); + + Vector3<real_t> particleCenterPosition = ac.getPosition(particleIdx); + WALBERLA_ASSERT( !math::isnan(particleCenterPosition) ); + + Vector3<real_t> particleNormal( cx - particleCenterPosition[0], cy - particleCenterPosition[1], cz - particleCenterPosition[2] ); + + return findCorrespondingLatticeDirection< stencil::D3Q27 >( particleNormal ); + } + +private: + shared_ptr<StructuredBlockStorage> blockStorage_; + +}; + +} // namespace lbm_mesapd_coupling +} // namespace walberla diff --git a/src/lbm_mesapd_coupling/momentum_exchange_method/reconstruction/PdfReconstructionManager.h b/src/lbm_mesapd_coupling/momentum_exchange_method/reconstruction/PdfReconstructionManager.h new file mode 100644 index 0000000000000000000000000000000000000000..dcf4c750ece66a25a5ee24019d41ac43a10877bb --- /dev/null +++ b/src/lbm_mesapd_coupling/momentum_exchange_method/reconstruction/PdfReconstructionManager.h @@ -0,0 +1,245 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla is free software: you can +// redistribute it and/or modify it under the terms of the GNU General Public +// License as published by the Free Software Foundation, either version 3 of +// the License, or (at your option) any later version. +// +// waLBerla is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +// for more details. +// +// You should have received a copy of the GNU General Public License along +// with waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file PdfReconstructionManager.h +//! \ingroup lbm_mesapd_coupling +//! \author Christoph Rettinger <christoph.rettinger@fau.de> +// +//====================================================================================================================== + + +#pragma once + +#include "core/debug/Debug.h" +#include "domain_decomposition/StructuredBlockStorage.h" +#include "field/Field.h" +#include "field/FlagField.h" +#include "field/iterators/IteratorMacros.h" + +#include "lbm_mesapd_coupling/DataTypes.h" +#include "lbm_mesapd_coupling/momentum_exchange_method/reconstruction/Reconstructor.h" +#include "lbm_mesapd_coupling/utility/ParticleFunctions.h" + +#include "mesa_pd/data/IAccessor.h" + +#include <functional> + +namespace walberla { +namespace lbm_mesapd_coupling { + + +//************************************************************************************************************************************** +/*!\brief Class to manage the reconstruction of PDFs that is needed when cells are becoming uncovered by moving obstacles. + * + * Due to the explicit mapping of particles into the fluid domain via flags, the PDFs of cells that turned from obstacle to fluid + * are missing and must be reconstructed in order to continue with the simulation. + * This class is to be used as a sweep in a LBM simulation with moving obstacles and calls for each cell that is tagged as + * 'formerObstacle' the specified reconstructor (see Reconstructor.h for the available variants). + * After the successful reconstruction of all PDFs, the flags are updated to 'fluid'. + * For small obstacle fractions, an optimized variant is available that only looks for 'formerObstacle' cells in the vicinity + * of available particles. It is activated via the 'optimizeForSmallObstacleFraction' argument in the constructor. + * + * + * Note: + * A potential problem arises when the cell that is to be reconstructed, i.e. filled with fluid, is surrounded (given by the LBM stencil) + * by only boundary cells. Consequently, the successive calls to LBM will simulate a local system containing a single fluid cell + * surrounded by boundary cells with different velocity boundary conditions. This system is not divergence free (as required by the mass + * conservation equation) and will thus lead to mass loss/gain depending on the boundary conditions. + * This is a very rare case but if it happens, it will almost always lead to malicious oscillations in the density. + * Note that this can also happen if several cells are locally "trapped" by surrounding boundary cells. Such a case can not be detected + * easily and also not be curred easily. + * It has to be inspected whether the behavior arising from this issue leads to problems in real simulations. + */ +//************************************************************************************************************************************** + +template< typename PdfField_T, typename BoundaryHandling_T, typename ParticleAccessor_T, typename Reconstructor_T> +class PdfReconstructionManager +{ + static_assert(std::is_base_of<mesa_pd::data::IAccessor, ParticleAccessor_T>::value, "Provide a valid accessor as template"); + +public: + + using FlagField_T = typename BoundaryHandling_T::FlagField; + using flag_t = typename BoundaryHandling_T::flag_t; + + inline PdfReconstructionManager( const shared_ptr<StructuredBlockStorage> & blockStorage, + const BlockDataID & pdfFieldID, + const BlockDataID & boundaryHandlingID, + const BlockDataID & particleFieldID, + const shared_ptr<ParticleAccessor_T> & ac, + const FlagUID & formerObstacle, const FlagUID & fluid, + const shared_ptr<Reconstructor_T> & reconstructor, + bool conserveMomentumWhenRestoring, + const bool optimizeForSmallObstacleFraction = false ) : + blockStorage_( blockStorage ), pdfFieldID_( pdfFieldID), boundaryHandlingID_( boundaryHandlingID ), + particleFieldID_( particleFieldID ), ac_( ac ), + reconstructor_ ( reconstructor ), formerObstacle_( formerObstacle ), fluid_( fluid ), + conserveMomentumWhenRestoring_( conserveMomentumWhenRestoring ), + optimizeForSmallObstacleFraction_( optimizeForSmallObstacleFraction ) + {} + + void inline operator()( IBlock * const block ); + +private: + + + + shared_ptr<StructuredBlockStorage> blockStorage_; + + const BlockDataID pdfFieldID_; + const BlockDataID boundaryHandlingID_; + const BlockDataID particleFieldID_; + + shared_ptr<ParticleAccessor_T> ac_; + + shared_ptr<Reconstructor_T> reconstructor_; + + const FlagUID formerObstacle_; + const FlagUID fluid_; + + bool conserveMomentumWhenRestoring_; + const bool optimizeForSmallObstacleFraction_; + +}; + + +template< typename PdfField_T, typename BoundaryHandling_T, typename ParticleAccessor_T, typename Reconstructor_T> +void PdfReconstructionManager< PdfField_T, BoundaryHandling_T, ParticleAccessor_T, Reconstructor_T > +::operator()( IBlock * const block ) +{ + WALBERLA_ASSERT_NOT_NULLPTR( block ); + + auto * pdfField = block->getData< PdfField_T >( pdfFieldID_ ); + auto * boundaryHandling = block->getData< BoundaryHandling_T >( boundaryHandlingID_ ); + auto * flagField = boundaryHandling->getFlagField(); + auto * particleField = block->getData< ParticleField_T >( particleFieldID_ ); + + WALBERLA_ASSERT_NOT_NULLPTR( pdfField ); + WALBERLA_ASSERT_NOT_NULLPTR( boundaryHandling ); + WALBERLA_ASSERT_NOT_NULLPTR( flagField ); + WALBERLA_ASSERT_NOT_NULLPTR( particleField ); + + WALBERLA_ASSERT_EQUAL( particleField->xyzSize(), flagField->xyzSize() ); + + WALBERLA_ASSERT( flagField->flagExists( formerObstacle_ ) ); + WALBERLA_ASSERT( flagField->flagExists( fluid_ ) ); + + const flag_t formerObstacle = flagField->getFlag( formerObstacle_ ); + const flag_t fluid = flagField->getFlag( fluid_ ); + + // reconstruct all missing PDFs (only inside the domain, ghost layer values get communicated) + + if( optimizeForSmallObstacleFraction_ ) + { + const uint_t numberOfGhostLayersToInclude = uint_t(0); + + for( size_t idx = 0; idx < ac_->size(); ++idx ) + { + auto cellIntervalForReconstruction = getParticleCellBB( idx, *ac_, *block, *blockStorage_, numberOfGhostLayersToInclude ); + + WALBERLA_FOR_ALL_CELLS_IN_INTERVAL_XYZ(cellIntervalForReconstruction, + size_t particleIdx = ac_->uidToIdx(particleField->get(x,y,z)); // to only reconstruct cells that belonged to this particle + if (isFlagSet(flagField->get(x,y,z), formerObstacle) && particleIdx == idx) { + (*reconstructor_)(block, x, y, z, pdfField, idx, *ac_); + if( conserveMomentumWhenRestoring_ && pdfField->isInInnerPart(Cell(x,y,z)) ) { + // the (artificially) added momentum in the restored fluid cell has to be subtracted from the former particle to ensure momentum conservation + // force = momentum / dt, with dt = 1 + Vector3<real_t> force = pdfField->getMomentumDensity(x, y, z); + Vector3< real_t > cellCenter = blockStorage_->getBlockLocalCellCenter(*block, Cell(x,y,z) ); + lbm_mesapd_coupling::addHydrodynamicForceAtWFPosAtomic( particleIdx, *ac_, -force, cellCenter ); + } + } + + ) + } + } + else + { + WALBERLA_FOR_ALL_CELLS_IN_INTERVAL_XYZ(flagField->xyzSize(), + if (isFlagSet(flagField->get(x,y,z), formerObstacle)) { + size_t particleIdx = ac_->uidToIdx(particleField->get(x,y,z)); + WALBERLA_ASSERT_UNEQUAL( particleIdx, ac_->getInvalidIdx(), "Index of particle is invalid!" ); + (*reconstructor_)(block, x, y, z, pdfField, particleIdx, *ac_); + if( conserveMomentumWhenRestoring_ && pdfField->isInInnerPart(Cell(x,y,z)) ) { + // the (artificially) added momentum in the restored fluid cell has to be subtracted from the former particle to ensure momentum conservation + // force = momentum / dt, with dt = 1 + Vector3<real_t> force = pdfField->getMomentumDensity(x, y, z); + Vector3< real_t > cellCenter = blockStorage_->getBlockLocalCellCenter(*block, Cell(x,y,z) ); + lbm_mesapd_coupling::addHydrodynamicForceAtWFPosAtomic( particleIdx, *ac_, -force, cellCenter ); + } + }; + ) + } + + // update the flags from formerObstacle to fluid (inside domain & in ghost layers), invalidate entry in particle field + if( optimizeForSmallObstacleFraction_ ) + { + const uint_t numberOfGhostLayersToInclude = flagField->nrOfGhostLayers(); + + for( size_t idx = 0; idx < ac_->size(); ++idx ) + { + auto cellIntervalToCleanUp = getParticleCellBB( idx, *ac_, *block, *blockStorage_, numberOfGhostLayersToInclude ); + WALBERLA_FOR_ALL_CELLS_IN_INTERVAL_XYZ(cellIntervalToCleanUp, + if (isFlagSet(flagField->get(x,y,z), formerObstacle)) { + boundaryHandling->setDomain( fluid, x, y, z ); + removeFlag( flagField->get(x,y,z), formerObstacle ); + particleField->get(x,y,z) = ac_->getInvalidUid(); + } + ) + } + } + else + { + WALBERLA_FOR_ALL_CELLS_IN_INTERVAL_XYZ(flagField->xyzSizeWithGhostLayer(), + if (isFlagSet(flagField->get(x,y,z), formerObstacle)) { + boundaryHandling->setDomain( fluid, x, y, z ); + removeFlag( flagField->get(x,y,z), formerObstacle ); + particleField->get(x,y,z) = ac_->getInvalidUid(); + } + ) + } +} + + +template< typename PdfField_T, typename BoundaryHandling_T, typename ParticleAccessor_T, typename Reconstructor_T > +shared_ptr< PdfReconstructionManager<PdfField_T,BoundaryHandling_T,ParticleAccessor_T,Reconstructor_T> > +makePdfReconstructionManager( const shared_ptr<StructuredBlockStorage> & blockStorage, const BlockDataID & pdfFieldID, + const BlockDataID & boundaryHandlingID, const BlockDataID & particleFieldID, const shared_ptr<ParticleAccessor_T> & ac, + const FlagUID & formerObstacle, const FlagUID & fluid, + const shared_ptr<Reconstructor_T> & reconstructor, + bool conserveMomentumWhenRestoring, + const bool optimizeForSmallObstacleFraction = false) +{ + using RM_T = PdfReconstructionManager<PdfField_T,BoundaryHandling_T,ParticleAccessor_T,Reconstructor_T>; + return make_shared<RM_T>( RM_T(blockStorage, pdfFieldID, boundaryHandlingID, particleFieldID, ac,formerObstacle, fluid, reconstructor, conserveMomentumWhenRestoring, optimizeForSmallObstacleFraction) ); +} + + +template< typename PdfField_T, typename BoundaryHandling_T, typename ParticleAccessor_T > +shared_ptr< PdfReconstructionManager<PdfField_T,BoundaryHandling_T,ParticleAccessor_T,EquilibriumReconstructor<BoundaryHandling_T> > > +makePdfReconstructionManager( const shared_ptr<StructuredBlockStorage> & blockStorage, const BlockDataID & pdfFieldID, + const BlockDataID & boundaryHandlingID, const BlockDataID & particleFieldID, const shared_ptr<ParticleAccessor_T> & ac, + const FlagUID & formerObstacle, const FlagUID & fluid, + bool conserveMomentumWhenRestoring, + const bool optimizeForSmallObstacleFraction = false) +{ + using RM_T = PdfReconstructionManager<PdfField_T,BoundaryHandling_T,ParticleAccessor_T,EquilibriumReconstructor<BoundaryHandling_T> >; + return make_shared<RM_T>( RM_T(blockStorage, pdfFieldID, boundaryHandlingID, particleFieldID, ac,formerObstacle, fluid, + makeEquilibriumReconstructor<BoundaryHandling_T>(blockStorage,boundaryHandlingID), conserveMomentumWhenRestoring, optimizeForSmallObstacleFraction) ); +} + + +} // namespace lbm_mesapd_coupling +} // namespace walberla diff --git a/src/lbm_mesapd_coupling/momentum_exchange_method/reconstruction/Reconstructor.h b/src/lbm_mesapd_coupling/momentum_exchange_method/reconstruction/Reconstructor.h new file mode 100644 index 0000000000000000000000000000000000000000..acb4fd95b1455af0973cde6b5ab80df1f0b99002 --- /dev/null +++ b/src/lbm_mesapd_coupling/momentum_exchange_method/reconstruction/Reconstructor.h @@ -0,0 +1,919 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla is free software: you can +// redistribute it and/or modify it under the terms of the GNU General Public +// License as published by the Free Software Foundation, either version 3 of +// the License, or (at your option) any later version. +// +// waLBerla is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +// for more details. +// +// You should have received a copy of the GNU General Public License along +// with waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file Reconstructor.h +//! \ingroup lbm_mesapd_coupling +//! \author Christoph Rettinger <christoph.rettinger@fau.de> +// +//====================================================================================================================== + + +#pragma once + +#include "domain_decomposition/StructuredBlockStorage.h" + +#include "mesa_pd/data/IAccessor.h" +#include "mesa_pd/common/ParticleFunctions.h" + +#include "lbm/lattice_model/EquilibriumDistribution.h" + +#include "stencil/D3Q19.h" +#include "stencil/D3Q27.h" + +namespace walberla { +namespace lbm_mesapd_coupling { + + +//************************************************************************************************************************************** +/*! + * \brief Classes to be used together with the PDFReconstruction class to reconstruct PDFs + * + * Each reconstructor must exactly implement the member function + * template< typename PdfField_T, typename ParticleAccessor_T > + * void operator()( IBlock * const block, const cell_idx_t & x, const cell_idx_t & y, const cell_idx_t & z, PdfField_T * const pdfField, + * const size_t particleIdx, const ParticleAccessor_T & ac); + * that reconstructs all PDFs in a specific cell with cell indices x,y,z on a given block. + * Additionally, a pointer to the pdfField and information about the formerly present particle is provided (via idx and accessor). + * + * Different variants are available: + * + * - EquilibriumReconstructor: + * Determines a local average density and sets the PDFs to their equilibrium values based on this density and the particle's velocity + * + * - EquilibriumAndNonEquilibriumReconstructor: + * First reconstructs the equilibrium values with the help of the EquilibriumReconstructor. + * Then extrapolates the non-equilibrium part from neighboring cells and adds it to the reconstructed PDFs for better accuracy. + * Defaults to EquilibriumReconstructor if no suitable cell for extrapolation is available. + * + * - ExtrapolationReconstructor: + * Extrapolates the PDFs of three or two neighboring cells that lie in extrapolation direction. + * Optionally, a special treatment after the extrapolation can be used that sets certain moments directly (only D3Q19). + * Defaults to EquilibriumAndNonEquilibriumReconstructor if not enough cells in this direction are available. + * + * - GradsMomentReconstructor: + * Uses Grad's moment approximation, i.e. equilibrium distribution function extended by pressure gradient information. + * + * EquilibriumAndNonEquilibriumReconstructor and ExtrapolationReconstructor need an extrapolation direction + * which is provided by the ExtrapolationDirectionFinder that is to be chosen (see ExtrapolationDirectionFinder.h). + * + * For convenient construction, use the provided makeXReconstructor functions. + * + */ +//************************************************************************************************************************************** + + +/* + * function that determines the number of valid (=fluid) cells for extrapolation in the given extrapolation direction + * If useDataFromGhostLayers = true, also cells inside the ghostlayer are considered valid so they have to contain valid data by communicating the PDF values beforehand + * Depending on the scheme, this can be an optimized PDF communication or has to be a full PDF communication. + */ +template< typename BoundaryHandling_T> +uint_t getNumberOfExtrapolationCells( IBlock * const block, const BlockDataID & boundaryHandlingID, + const cell_idx_t & x, const cell_idx_t & y, const cell_idx_t & z, + const Vector3<cell_idx_t> & extrapolationDirection, const uint_t maximumNumberOfNeededExtrapolationCells, + bool useDataFromGhostLayers ) +{ + WALBERLA_ASSERT_NOT_NULLPTR( block ); + + BoundaryHandling_T * boundaryHandling = block->getData< BoundaryHandling_T >( boundaryHandlingID ); + WALBERLA_ASSERT_NOT_NULLPTR( boundaryHandling ); + + if( extrapolationDirection == cell_idx_t(0) ) return uint_t(0); + + CellInterval localDomain = (useDataFromGhostLayers) ? boundaryHandling->getFlagField()->xyzSizeWithGhostLayer() : boundaryHandling->getFlagField()->xyzSize(); + + for( auto numCells = uint_t(1); numCells <= maximumNumberOfNeededExtrapolationCells; ++numCells ) + { + Cell checkCell( x + cell_idx_c(numCells) * extrapolationDirection[0], y + cell_idx_c(numCells) * extrapolationDirection[1], z + cell_idx_c(numCells) * extrapolationDirection[2] ); + + // check if cell is inside domain & fluid + if( !localDomain.contains( checkCell ) ) + return numCells - uint_t(1); + + if( !boundaryHandling->isDomain( checkCell ) ) + return numCells - uint_t(1); + } + return maximumNumberOfNeededExtrapolationCells; +} + + +template< typename BoundaryHandling_T > +class EquilibriumReconstructor +{ +public: + + EquilibriumReconstructor( const shared_ptr<StructuredBlockStorage> & blockStorage, const BlockDataID & boundaryHandlingID, + bool useDataFromGhostLayers = false) + : blockStorage_( blockStorage ), boundaryHandlingID_( boundaryHandlingID ), useDataFromGhostLayers_( useDataFromGhostLayers ) + {} + + + template< typename PdfField_T, typename ParticleAccessor_T > + void operator()( IBlock * const block, const cell_idx_t & x, const cell_idx_t & y, const cell_idx_t & z, PdfField_T * const pdfField, + const size_t particleIdx, const ParticleAccessor_T & ac) + { + static_assert(std::is_base_of<mesa_pd::data::IAccessor, ParticleAccessor_T>::value, "Provide a valid accessor as template"); + + WALBERLA_ASSERT_NOT_NULLPTR( block ); + WALBERLA_ASSERT_NOT_NULLPTR( pdfField ); + WALBERLA_ASSERT_UNEQUAL(particleIdx, ac.getInvalidIdx()); + + setLocalEquilibrium(block, x, y, z, pdfField, particleIdx, ac); + } + +private: + + template< typename PdfField_T, typename ParticleAccessor_T > + void setLocalEquilibrium( IBlock * const block, const cell_idx_t & x, const cell_idx_t & y, const cell_idx_t & z, PdfField_T * const pdfField, + const size_t particleIdx, const ParticleAccessor_T & ac ) + { + const real_t averageDensity = getLocalAverageDensity( block, x, y, z, pdfField ); + + real_t cx, cy, cz; + blockStorage_->getBlockLocalCellCenter( *block, Cell(x,y,z), cx, cy, cz ); + + const auto velocity = mesa_pd::getVelocityAtWFPoint(particleIdx, ac, Vector3<real_t>(cx,cy,cz)); + + pdfField->setToEquilibrium( x, y, z, velocity, averageDensity ); + } + + template< typename PdfField_T> + real_t getLocalAverageDensity( IBlock * const block, const cell_idx_t & x, const cell_idx_t & y, const cell_idx_t & z, PdfField_T * const pdfField ) const + { + BoundaryHandling_T * boundaryHandling = block->getData< BoundaryHandling_T >( boundaryHandlingID_ ); + + WALBERLA_ASSERT_NOT_NULLPTR( boundaryHandling ); + + CellInterval localDomain = useDataFromGhostLayers_ ? pdfField->xyzSizeWithGhostLayer() : pdfField->xyzSize(); + + auto nAverage = uint_t(0); + auto averageDensity = real_t(0); + for( auto neighborDir = stencil::D3Q27::beginNoCenter(); neighborDir != stencil::D3Q27::end(); ++neighborDir ) + { + Cell neighbor( x + neighborDir.cx(), y + neighborDir.cy(), z + neighborDir.cz() ); + + // check if neighbor cell is inside domain + if( !( localDomain.contains( neighbor ) ) ) + continue; + + // check if neighbor cell is a fluid cell + if( boundaryHandling->isDomain( neighbor ) ) + { + // obtain density and add it up + averageDensity += pdfField->getDensity( neighbor ); + ++nAverage; + } + } + + return ( nAverage > uint_t( 0 ) ) ? averageDensity / real_c( nAverage ) : real_t(1.0); + } + + + shared_ptr<StructuredBlockStorage> blockStorage_; + const BlockDataID boundaryHandlingID_; + const bool useDataFromGhostLayers_; + +}; + + +template< typename BoundaryHandling_T, typename ExtrapolationDirectionFinder_T > +class EquilibriumAndNonEquilibriumReconstructor +{ +public: + + EquilibriumAndNonEquilibriumReconstructor( const shared_ptr<StructuredBlockStorage> & blockStorage, const BlockDataID & boundaryHandlingID, + const shared_ptr<ExtrapolationDirectionFinder_T> & extrapolationDirectionFinder, + uint_t maximumNumberOfExtrapolationCells = uint_t(3), + bool useDataFromGhostLayers = false) + : blockStorage_( blockStorage ), boundaryHandlingID_( boundaryHandlingID ), + extrapolationDirectionFinder_( extrapolationDirectionFinder ), + maximumNumberOfExtrapolationCells_( maximumNumberOfExtrapolationCells ), useDataFromGhostLayers_( useDataFromGhostLayers ), + equilibriumReconstructor_( EquilibriumReconstructor< BoundaryHandling_T >( blockStorage, boundaryHandlingID, useDataFromGhostLayers ) ) + { + WALBERLA_ASSERT_LESS_EQUAL(maximumNumberOfExtrapolationCells, uint_t(3), "Only supports up to quadratic extrapolation!"); + } + + template< typename PdfField_T, typename ParticleAccessor_T > + void operator()( IBlock * const block, const cell_idx_t & x, const cell_idx_t & y, const cell_idx_t & z, PdfField_T * const pdfField, + const size_t particleIdx, const ParticleAccessor_T & ac) + { + static_assert(std::is_base_of<mesa_pd::data::IAccessor, ParticleAccessor_T>::value, "Provide a valid accessor as template"); + + WALBERLA_ASSERT_NOT_NULLPTR( block ); + WALBERLA_ASSERT_NOT_NULLPTR( pdfField ); + WALBERLA_ASSERT_UNEQUAL(particleIdx, ac.getInvalidIdx()); + + Vector3<cell_idx_t> extrapolationDirection = (*extrapolationDirectionFinder_)( block, x, y, z, particleIdx, ac ); + + (*this)(block, x, y, z, pdfField, particleIdx, ac, extrapolationDirection); + } + + template< typename PdfField_T, typename ParticleAccessor_T > + void operator()( IBlock * const block, const cell_idx_t & x, const cell_idx_t & y, const cell_idx_t & z, PdfField_T * const pdfField, + const size_t particleIdx, const ParticleAccessor_T & ac, const Vector3<cell_idx_t> & extrapolationDirection ) + { + static_assert(std::is_base_of<mesa_pd::data::IAccessor, ParticleAccessor_T>::value, "Provide a valid accessor as template"); + + WALBERLA_ASSERT_NOT_NULLPTR( block ); + WALBERLA_ASSERT_NOT_NULLPTR( pdfField ); + WALBERLA_ASSERT_UNEQUAL(particleIdx, ac.getInvalidIdx()); + + equilibriumReconstructor_( block, x, y, z, pdfField, particleIdx, ac ); + + uint_t numberOfCellsForExtrapolation = getNumberOfExtrapolationCells<BoundaryHandling_T>( block, boundaryHandlingID_, x, y, z, extrapolationDirection, maximumNumberOfExtrapolationCells_, useDataFromGhostLayers_ ); + + if( enoughCellsForExtrapolation( numberOfCellsForExtrapolation ) ) + { + extrapolateNonEquilibrium( x, y, z, pdfField, extrapolationDirection, numberOfCellsForExtrapolation ); + } + + } +private: + + template< typename PdfField_T > + void extrapolateNonEquilibrium( const cell_idx_t & x, const cell_idx_t & y, const cell_idx_t & z, PdfField_T * const pdfField, + const Vector3<cell_idx_t> & extrapolationDirection, const uint_t & numberOfCellsForExtrapolation) + { + WALBERLA_ASSERT_NOT_NULLPTR( pdfField ); + + WALBERLA_ASSERT_LESS_EQUAL(numberOfCellsForExtrapolation, maximumNumberOfExtrapolationCells_); + WALBERLA_ASSERT_GREATER(numberOfCellsForExtrapolation, uint_t(0), "Requires at least one point for extrapolation!"); + + if( enoughCellsForQuadraticExtrapolation( numberOfCellsForExtrapolation ) ) + { + applyQuadraticExtrapolation(x, y, z, pdfField, extrapolationDirection); + } + else if ( enoughCellsForLinearExtrapolation( numberOfCellsForExtrapolation ) ) + { + applyLinearExtrapolation(x, y, z, pdfField, extrapolationDirection); + } + else + { + applyConstantExtrapolation(x, y, z, pdfField, extrapolationDirection); + } + } + + bool enoughCellsForQuadraticExtrapolation(uint_t numberOfCellsForExtrapolation ) + { + return numberOfCellsForExtrapolation >= uint_t(3); + } + + bool enoughCellsForLinearExtrapolation(uint_t numberOfCellsForExtrapolation ) + { + return numberOfCellsForExtrapolation >= uint_t(2); + } + + bool enoughCellsForExtrapolation(uint_t numberOfCellsForExtrapolation ) + { + return numberOfCellsForExtrapolation >= uint_t(1); + } + + template< typename PdfField_T > + void applyQuadraticExtrapolation( const cell_idx_t & x, const cell_idx_t & y, const cell_idx_t & z, PdfField_T * const pdfField, + const Vector3<cell_idx_t> & extrapolationDirection) + { + WALBERLA_ASSERT_NOT_NULLPTR(pdfField); + + using LatticeModel_T = typename PdfField_T::LatticeModel; + + auto pdfsXf = getNonEquilibriumPdfsInCell(x + extrapolationDirection[0], y + extrapolationDirection[1], z + extrapolationDirection[2], pdfField); + auto pdfsXff = getNonEquilibriumPdfsInCell(x + 2*extrapolationDirection[0], y + 2*extrapolationDirection[1], z + 2*extrapolationDirection[2], pdfField); + auto pdfsXfff = getNonEquilibriumPdfsInCell(x + 3*extrapolationDirection[0], y + 3*extrapolationDirection[1], z + 3*extrapolationDirection[2], pdfField); + + for( auto d = LatticeModel_T::Stencil::begin(); d != LatticeModel_T::Stencil::end(); ++d ) + { + pdfField->get( x, y, z, d.toIdx() ) += real_t(3) * pdfsXf[d.toIdx()] - real_t(3) * pdfsXff[d.toIdx()] + pdfsXfff[d.toIdx()]; + } + } + + template< typename PdfField_T > + void applyLinearExtrapolation( const cell_idx_t & x, const cell_idx_t & y, const cell_idx_t & z, PdfField_T * const pdfField, + const Vector3<cell_idx_t> & extrapolationDirection) + { + WALBERLA_ASSERT_NOT_NULLPTR(pdfField); + + using LatticeModel_T = typename PdfField_T::LatticeModel; + + auto pdfsXf = getNonEquilibriumPdfsInCell(x + extrapolationDirection[0], y + extrapolationDirection[1], z + extrapolationDirection[2], pdfField); + auto pdfsXff = getNonEquilibriumPdfsInCell(x + 2*extrapolationDirection[0], y + 2*extrapolationDirection[1], z + 2*extrapolationDirection[2], pdfField); + + for( auto d = LatticeModel_T::Stencil::begin(); d != LatticeModel_T::Stencil::end(); ++d ) + { + pdfField->get( x, y, z, d.toIdx() ) += real_t(2) * pdfsXf[d.toIdx()] - pdfsXff[d.toIdx()]; + } + } + + template< typename PdfField_T > + void applyConstantExtrapolation( const cell_idx_t & x, const cell_idx_t & y, const cell_idx_t & z, PdfField_T * const pdfField, + const Vector3<cell_idx_t> & extrapolationDirection) + { + WALBERLA_ASSERT_NOT_NULLPTR(pdfField); + + using LatticeModel_T = typename PdfField_T::LatticeModel; + + // = copy from neighbor cell + auto pdfsXf = getNonEquilibriumPdfsInCell(x + extrapolationDirection[0], y + extrapolationDirection[1], z + extrapolationDirection[2], pdfField); + + for( auto d = LatticeModel_T::Stencil::begin(); d != LatticeModel_T::Stencil::end(); ++d ) + { + pdfField->get( x, y, z, d.toIdx() ) += pdfsXf[d.toIdx()]; + } + } + + template< typename PdfField_T > + std::vector<real_t> getNonEquilibriumPdfsInCell( const cell_idx_t & x, const cell_idx_t & y, const cell_idx_t & z, PdfField_T * const pdfField ) const + { + using LatticeModel_T = typename PdfField_T::LatticeModel; + + std::vector< real_t > nonEquilibriumPartOfPdfs(LatticeModel_T::Stencil::Size); + + Vector3< real_t > velocity; + real_t density = pdfField->getDensityAndVelocity( velocity, x,y,z ); + + for( auto d = LatticeModel_T::Stencil::begin(); d != LatticeModel_T::Stencil::end(); ++d ) + { + nonEquilibriumPartOfPdfs[d.toIdx()] = pdfField->get( x, y, z, d.toIdx() ) - lbm::EquilibriumDistribution< LatticeModel_T >::get( *d, velocity, density ); + } + return nonEquilibriumPartOfPdfs; + } + + shared_ptr<StructuredBlockStorage> blockStorage_; + const BlockDataID boundaryHandlingID_; + shared_ptr<ExtrapolationDirectionFinder_T> extrapolationDirectionFinder_; + const uint_t maximumNumberOfExtrapolationCells_; + const bool useDataFromGhostLayers_; + EquilibriumReconstructor< BoundaryHandling_T > equilibriumReconstructor_; + +}; + +template< typename BoundaryHandling_T, typename ExtrapolationDirectionFinder_T, bool EnforceNoSlipConstraintAfterExtrapolation = false > +class ExtrapolationReconstructor +{ +public: + + ExtrapolationReconstructor( const shared_ptr<StructuredBlockStorage> & blockStorage, const BlockDataID & boundaryHandlingID, + const shared_ptr<ExtrapolationDirectionFinder_T> & extrapolationDirectionFinder, + uint_t maximumNumberOfExtrapolationCells = uint_t(3), + bool useDataFromGhostLayers = false) + : blockStorage_( blockStorage ), boundaryHandlingID_( boundaryHandlingID ), + extrapolationDirectionFinder_( extrapolationDirectionFinder ), + maximumNumberOfExtrapolationCells_(maximumNumberOfExtrapolationCells), useDataFromGhostLayers_(useDataFromGhostLayers), + alternativeReconstructor_( EquilibriumAndNonEquilibriumReconstructor< BoundaryHandling_T, ExtrapolationDirectionFinder_T > + ( blockStorage, boundaryHandlingID, extrapolationDirectionFinder, uint_t(1), useDataFromGhostLayers ) ) + { + WALBERLA_ASSERT_LESS_EQUAL(maximumNumberOfExtrapolationCells, uint_t(3), "Only supports up to quadratic extrapolation!"); + } + + template< typename PdfField_T, typename ParticleAccessor_T > + void operator()( IBlock * const block, const cell_idx_t & x, const cell_idx_t & y, const cell_idx_t & z, PdfField_T * const pdfField, + const size_t particleIdx, const ParticleAccessor_T & ac) + { + static_assert(std::is_base_of<mesa_pd::data::IAccessor, ParticleAccessor_T>::value, "Provide a valid accessor as template"); + + WALBERLA_ASSERT_NOT_NULLPTR(block); + WALBERLA_ASSERT_NOT_NULLPTR(pdfField); + WALBERLA_ASSERT_UNEQUAL(particleIdx, ac.getInvalidIdx()); + + Vector3<cell_idx_t> extrapolationDirection = (*extrapolationDirectionFinder_)(block, x, y, z, particleIdx, ac); + + uint_t numberOfCellsForExtrapolation = getNumberOfExtrapolationCells<BoundaryHandling_T>( block, boundaryHandlingID_, x, y, z, extrapolationDirection, + maximumNumberOfExtrapolationCells_, useDataFromGhostLayers_ ); + + if( enoughCellsForExtrapolation(numberOfCellsForExtrapolation) ) + { + extrapolatePDFs( x, y, z, pdfField, extrapolationDirection, numberOfCellsForExtrapolation ); + if( EnforceNoSlipConstraintAfterExtrapolation ) + { + real_t cx, cy, cz; + blockStorage_->getBlockLocalCellCenter( *block, Cell(x,y,z), cx, cy, cz ); + const auto localParticleVelocity = mesa_pd::getVelocityAtWFPoint(particleIdx, ac, Vector3<real_t>(cx,cy,cz)); + + enforceNoSlipConstraint( x, y, z, pdfField, localParticleVelocity ); + } + } + else + { + alternativeReconstructor_( block, x, y, z, pdfField, particleIdx, ac, extrapolationDirection ); + } + + } + +private: + + bool enoughCellsForExtrapolation( uint_t numberOfCellsForExtrapolation ) + { + return numberOfCellsForExtrapolation >= uint_t(2); + } + + template< typename PdfField_T > + void extrapolatePDFs( const cell_idx_t & x, const cell_idx_t & y, const cell_idx_t & z, PdfField_T * const pdfField, + const Vector3<cell_idx_t> & extrapolationDirection, const uint_t & numberOfCellsForExtrapolation) + { + WALBERLA_ASSERT_NOT_NULLPTR( pdfField ); + + using LatticeModel_T = typename PdfField_T::LatticeModel; + + if( numberOfCellsForExtrapolation == uint_t(3) ) + { + // quadratic normal extrapolation + for( auto d = LatticeModel_T::Stencil::begin(); d != LatticeModel_T::Stencil::end(); ++d ) + { + pdfField->get( x, y, z, d.toIdx() ) = real_t(3) * pdfField->get( x + extrapolationDirection[0], y + extrapolationDirection[1], z + extrapolationDirection[2], d.toIdx() ) + - real_t(3) * pdfField->get( x + 2*extrapolationDirection[0], y + 2*extrapolationDirection[1], z + 2*extrapolationDirection[2], d.toIdx() ) + + pdfField->get( x + 3*extrapolationDirection[0], y + 3*extrapolationDirection[1], z + 3*extrapolationDirection[2], d.toIdx() ); + } + } else { // numberOfCellsForExtrapolation == 2 + // linear normal extrapolation + for( auto d = LatticeModel_T::Stencil::begin(); d != LatticeModel_T::Stencil::end(); ++d ) + { + pdfField->get( x, y, z, d.toIdx() ) = real_t(2) * pdfField->get( x + extrapolationDirection[0], y + extrapolationDirection[1], z + extrapolationDirection[2], d.toIdx() ) + - pdfField->get( x + 2*extrapolationDirection[0], y + 2*extrapolationDirection[1], z + 2*extrapolationDirection[2], d.toIdx() ); + } + } + } + + template< typename PdfField_T > + void enforceNoSlipConstraint( const cell_idx_t & x, const cell_idx_t & y, const cell_idx_t & z, PdfField_T * const pdfField, const Vector3<real_t> & localParticleVelocity) + { + using LatticeModel_T = typename PdfField_T::LatticeModel; + + static_assert(std::is_same<typename LatticeModel_T::Stencil, stencil::D3Q19>::value || !EnforceNoSlipConstraintAfterExtrapolation, "Enforcing no-slip constraint after extrapolation currently only works with D3Q19 stencil!"); + + WALBERLA_ASSERT_NOT_NULLPTR( pdfField ); + WALBERLA_ASSERT( !math::isnan(localParticleVelocity) ); + + // transforms to moment space (see MRT collision model) to set the particle's velocity in cell without affecting other moments + const real_t _1_2 = real_t(1) / real_t(2); + const real_t _1_3 = real_t(1) / real_t(3); + const real_t _1_4 = real_t(1) / real_t(4); + const real_t _1_6 = real_t(1) / real_t(6); + const real_t _1_8 = real_t(1) / real_t(8); + const real_t _1_12 = real_t(1) / real_t(12); + const real_t _1_16 = real_t(1) / real_t(16); + const real_t _1_18 = real_t(1) / real_t(18); + const real_t _1_24 = real_t(1) / real_t(24); + const real_t _1_36 = real_t(1) / real_t(36); + const real_t _1_48 = real_t(1) / real_t(48); + const real_t _1_72 = real_t(1) / real_t(72); + + // restriction to D3Q19! + const real_t vC = pdfField->get( x, y, z, LatticeModel_T::Stencil::idx[stencil::C] ); + const real_t vN = pdfField->get( x, y, z, LatticeModel_T::Stencil::idx[stencil::N] ); + const real_t vS = pdfField->get( x, y, z, LatticeModel_T::Stencil::idx[stencil::S] ); + const real_t vW = pdfField->get( x, y, z, LatticeModel_T::Stencil::idx[stencil::W] ); + const real_t vE = pdfField->get( x, y, z, LatticeModel_T::Stencil::idx[stencil::E] ); + const real_t vT = pdfField->get( x, y, z, LatticeModel_T::Stencil::idx[stencil::T] ); + const real_t vB = pdfField->get( x, y, z, LatticeModel_T::Stencil::idx[stencil::B] ); + const real_t vNW = pdfField->get( x, y, z, LatticeModel_T::Stencil::idx[stencil::NW] ); + const real_t vNE = pdfField->get( x, y, z, LatticeModel_T::Stencil::idx[stencil::NE] ); + const real_t vSW = pdfField->get( x, y, z, LatticeModel_T::Stencil::idx[stencil::SW] ); + const real_t vSE = pdfField->get( x, y, z, LatticeModel_T::Stencil::idx[stencil::SE] ); + const real_t vTN = pdfField->get( x, y, z, LatticeModel_T::Stencil::idx[stencil::TN] ); + const real_t vTS = pdfField->get( x, y, z, LatticeModel_T::Stencil::idx[stencil::TS] ); + const real_t vTW = pdfField->get( x, y, z, LatticeModel_T::Stencil::idx[stencil::TW] ); + const real_t vTE = pdfField->get( x, y, z, LatticeModel_T::Stencil::idx[stencil::TE] ); + const real_t vBN = pdfField->get( x, y, z, LatticeModel_T::Stencil::idx[stencil::BN] ); + const real_t vBS = pdfField->get( x, y, z, LatticeModel_T::Stencil::idx[stencil::BS] ); + const real_t vBW = pdfField->get( x, y, z, LatticeModel_T::Stencil::idx[stencil::BW] ); + const real_t vBE = pdfField->get( x, y, z, LatticeModel_T::Stencil::idx[stencil::BE] ); + + // transform to moment space and change momentum to particle's velocity ( * rho_0 ) + const real_t m0 = vC + vN + vS + vW + vE + vT + vB + vNW + vNE + vSW + vSE + vTN + vTS + vTW + vTE + vBN + vBS + vBW + vBE; + const real_t m1 = -vC + vNW + vNE + vSW + vSE + vTN + vTS + vTW + vTE + vBN + vBS + vBW + vBE; + const real_t m2 = vC - real_t(2) * ( vN + vS + vW + vE + vT + vB ) + vNW + vNE + vSW + vSE + vTN + vTS + vTW + vTE + vBN + vBS + vBW + vBE; + const real_t m3 = localParticleVelocity[0]; + const real_t m4 = real_t(2) * vW - real_t(2) * vE - vNW + vNE - vSW + vSE - vTW + vTE - vBW + vBE; + const real_t m5 = localParticleVelocity[1]; + const real_t m6 = real_t(-2) * vN + real_t(2) * vS + vNW + vNE - vSW - vSE + vTN - vTS + vBN - vBS; + const real_t m7 = localParticleVelocity[2]; + const real_t m8 = real_t(-2) * vT + real_t(2) * vB + vTN + vTS + vTW + vTE - vBN - vBS - vBW - vBE; + const real_t m9 = -vN - vS + real_t(2) * vW + real_t(2) * vE - vT - vB + vNW + vNE + vSW + vSE - real_t(2) * vTN + - real_t(2) * vTS + vTW + vTE - real_t(2) * vBN - real_t(2) * vBS + vBW + vBE; + const real_t m10 = vN + vS - real_t(2) * vW - real_t(2) * vE + vT + vB + vNW + vNE + vSW + vSE - real_t(2) * vTN + - real_t(2) * vTS + vTW + vTE - real_t(2) * vBN - real_t(2) * vBS + vBW + vBE; + const real_t m11 = vN + vS - vT - vB + vNW + vNE + vSW + vSE - vTW - vTE - vBW - vBE; + const real_t m12 = -vN - vS + vT + vB + vNW + vNE + vSW + vSE - vTW - vTE - vBW - vBE; + const real_t m13 = -vNW + vNE + vSW - vSE; + const real_t m14 = vTN - vTS - vBN + vBS; + const real_t m15 = -vTW + vTE + vBW - vBE; + const real_t m16 = -vNW + vNE - vSW + vSE + vTW - vTE + vBW - vBE; + const real_t m17 = -vNW - vNE + vSW + vSE + vTN - vTS + vBN - vBS; + const real_t m18 = -vTN - vTS + vTW + vTE + vBN + vBS - vBW - vBE; + + // transform back to PDF space + pdfField->get( x, y, z, LatticeModel_T::Stencil::idx[stencil::C] ) = _1_3 * m0 - _1_2 * m1 + _1_6 * m2; + pdfField->get( x, y, z, LatticeModel_T::Stencil::idx[stencil::N] ) = _1_18 * m0 - _1_18 * m2 + _1_6 * m5 - _1_6 * m6 - _1_24 * m9 + + _1_24 * m10 + _1_8 * m11 - _1_8 * m12; + pdfField->get( x, y, z, LatticeModel_T::Stencil::idx[stencil::S] ) = _1_18 * m0 - _1_18 * m2 - _1_6 * m5 + _1_6 * m6 - _1_24 * m9 + + _1_24 * m10 + _1_8 * m11 - _1_8 * m12; + pdfField->get( x, y, z, LatticeModel_T::Stencil::idx[stencil::W] ) = _1_18 * m0 - _1_18 * m2 - _1_6 * m3 + _1_6 * m4 + _1_12 * m9 - _1_12 * m10; + pdfField->get( x, y, z, LatticeModel_T::Stencil::idx[stencil::E] ) = _1_18 * m0 - _1_18 * m2 + _1_6 * m3 - _1_6 * m4 + _1_12 * m9 - _1_12 * m10; + pdfField->get( x, y, z, LatticeModel_T::Stencil::idx[stencil::T] ) = _1_18 * m0 - _1_18 * m2 + _1_6 * m7 - _1_6 * m8 - _1_24 * m9 + + _1_24 * m10 - _1_8 * m11 + _1_8 * m12; + pdfField->get( x, y, z, LatticeModel_T::Stencil::idx[stencil::B] ) = _1_18 * m0 - _1_18 * m2 - _1_6 * m7 + _1_6 * m8 - _1_24 * m9 + + _1_24 * m10 - _1_8 * m11 + _1_8 * m12; + pdfField->get( x, y, z, LatticeModel_T::Stencil::idx[stencil::NW] ) = _1_36 * m0 + _1_24 * m1 + _1_72 * m2 - _1_12 * m3 - _1_24 * m4 + + _1_12 * m5 + _1_24 * m6 + _1_48 * m9 + _1_48 * m10 + _1_16 * m11 + + _1_16 * m12 - _1_4 * m13 - _1_8 * m16 - _1_8 * m17; + pdfField->get( x, y, z, LatticeModel_T::Stencil::idx[stencil::NE] ) = _1_36 * m0 + _1_24 * m1 + _1_72 * m2 + _1_12 * m3 + _1_24 * m4 + + _1_12 * m5 + _1_24 * m6 + _1_48 * m9 + _1_48 * m10 + _1_16 * m11 + + _1_16 * m12 + _1_4 * m13 + _1_8 * m16 - _1_8 * m17; + pdfField->get( x, y, z, LatticeModel_T::Stencil::idx[stencil::SW] ) = _1_36 * m0 + _1_24 * m1 + _1_72 * m2 - _1_12 * m3 - _1_24 * m4 - + _1_12 * m5 - _1_24 * m6 + _1_48 * m9 + _1_48 * m10 + _1_16 * m11 + + _1_16 * m12 + _1_4 * m13 - _1_8 * m16 + _1_8 * m17; + pdfField->get( x, y, z, LatticeModel_T::Stencil::idx[stencil::SE] ) = _1_36 * m0 + _1_24 * m1 + _1_72 * m2 + _1_12 * m3 + _1_24 * m4 - + _1_12 * m5 - _1_24 * m6 + _1_48 * m9 + _1_48 * m10 + _1_16 * m11 + + _1_16 * m12 - _1_4 * m13 + _1_8 * m16 + _1_8 * m17; + pdfField->get( x, y, z, LatticeModel_T::Stencil::idx[stencil::TN] ) = _1_36 * m0 + _1_24 * m1 + _1_72 * m2 + _1_12 * m5 + _1_24 * m6 + + _1_12 * m7 + _1_24 * m8 - _1_24 * m9 - _1_24 * m10 + _1_4 * m14 + + _1_8 * m17 - _1_8 * m18; + pdfField->get( x, y, z, LatticeModel_T::Stencil::idx[stencil::TS] ) = _1_36 * m0 + _1_24 * m1 + _1_72 * m2 - _1_12 * m5 - _1_24 * m6 + + _1_12 * m7 + _1_24 * m8 - _1_24 * m9 - _1_24 * m10 - _1_4 * m14 - + _1_8 * m17 - _1_8 * m18; + pdfField->get( x, y, z, LatticeModel_T::Stencil::idx[stencil::TW] ) = _1_36 * m0 + _1_24 * m1 + _1_72 * m2 - _1_12 * m3 - _1_24 * m4 + + _1_12 * m7 + _1_24 * m8 + _1_48 * m9 + _1_48 * m10 - _1_16 * m11 - + _1_16 * m12 - _1_4 * m15 + _1_8 * m16 + _1_8 * m18; + pdfField->get( x, y, z, LatticeModel_T::Stencil::idx[stencil::TE] ) = _1_36 * m0 + _1_24 * m1 + _1_72 * m2 + _1_12 * m3 + _1_24 * m4 + + _1_12 * m7 + _1_24 * m8 + _1_48 * m9 + _1_48 * m10 - _1_16 * m11 - + _1_16 * m12 + _1_4 * m15 - _1_8 * m16 + _1_8 * m18; + pdfField->get( x, y, z, LatticeModel_T::Stencil::idx[stencil::BN] ) = _1_36 * m0 + _1_24 * m1 + _1_72 * m2 + _1_12 * m5 + _1_24 * m6 - + _1_12 * m7 - _1_24 * m8 - _1_24 * m9 - _1_24 * m10 - _1_4 * m14 + + _1_8 * m17 + _1_8 * m18; + pdfField->get( x, y, z, LatticeModel_T::Stencil::idx[stencil::BS] ) = _1_36 * m0 + _1_24 * m1 + _1_72 * m2 - _1_12 * m5 - _1_24 * m6 - + _1_12 * m7 - _1_24 * m8 - _1_24 * m9 - _1_24 * m10 + _1_4 * m14 - + _1_8 * m17 + _1_8 * m18; + pdfField->get( x, y, z, LatticeModel_T::Stencil::idx[stencil::BW] ) = _1_36 * m0 + _1_24 * m1 + _1_72 * m2 - _1_12 * m3 - _1_24 * m4 - + _1_12 * m7 - _1_24 * m8 + _1_48 * m9 + _1_48 * m10 - _1_16 * m11 - + _1_16 * m12 + _1_4 * m15 + _1_8 * m16 - _1_8 * m18; + pdfField->get( x, y, z, LatticeModel_T::Stencil::idx[stencil::BE] ) = _1_36 * m0 + _1_24 * m1 + _1_72 * m2 + _1_12 * m3 + _1_24 * m4 - + _1_12 * m7 - _1_24 * m8 + _1_48 * m9 + _1_48 * m10 - _1_16 * m11 - + _1_16 * m12 - _1_4 * m15 - _1_8 * m16 - _1_8 * m18; + } + + shared_ptr<StructuredBlockStorage> blockStorage_; + const BlockDataID boundaryHandlingID_; + shared_ptr<ExtrapolationDirectionFinder_T> extrapolationDirectionFinder_; + const uint_t maximumNumberOfExtrapolationCells_; + const bool useDataFromGhostLayers_; + + EquilibriumAndNonEquilibriumReconstructor< BoundaryHandling_T, ExtrapolationDirectionFinder_T > alternativeReconstructor_; +}; + + +/* + * similar to Chikatamarla, Ansumali and Karlin - Grad's approximation for missing data in lattice Boltzmann simulations, EPL (2006) + * also in Dorschner, Chikatamarla, Boesch, Karlin - Grad's approximation for moving and stationary walls in entropic lattice Boltzmann simulations, Journal of Computational Physics, 2015 + * omegaShear: relaxation RATE that determines the kinematic viscosity + * + * To obtain the pressure gradient information, finite differences with central differences (useCentralDifferences) are used if enouhg information available, else upwinding differences are applied. + * When useDataFromGhostLayers = true, a full ghost layer sync is required just before the reconstruction step. This is required to avoid inconsistencies when using parallelization as else the behavior close to block boarders is altered. + */ +template< typename BoundaryHandling_T > +class GradsMomentApproximationReconstructor +{ +public: + + GradsMomentApproximationReconstructor( const shared_ptr<StructuredBlockStorage> & blockStorage, const BlockDataID & boundaryHandlingID, real_t omegaShear, + bool recomputeTargetDensity = false, bool useCentralDifferences = true, bool useDataFromGhostLayers = false ) + : blockStorage_( blockStorage ), boundaryHandlingID_( boundaryHandlingID ), omegaShear_(omegaShear), + recomputeTargetDensity_(recomputeTargetDensity), useCentralDifferences_(useCentralDifferences), useDataFromGhostLayers_(useDataFromGhostLayers) + {} + + + template< typename PdfField_T, typename ParticleAccessor_T > + void operator()( IBlock * const block, const cell_idx_t & x, const cell_idx_t & y, const cell_idx_t & z, PdfField_T * const pdfField, + const size_t particleIdx, const ParticleAccessor_T & ac) + { + static_assert(std::is_base_of<mesa_pd::data::IAccessor, ParticleAccessor_T>::value, "Provide a valid accessor as template"); + + WALBERLA_ASSERT_NOT_NULLPTR( block ); + WALBERLA_ASSERT_NOT_NULLPTR( pdfField ); + WALBERLA_ASSERT_UNEQUAL(particleIdx, ac.getInvalidIdx()); + + using LatticeModel = typename PdfField_T::LatticeModel; + using Stencil = typename LatticeModel::Stencil; + + BoundaryHandling_T * boundaryHandling = block->getData< BoundaryHandling_T >( boundaryHandlingID_ ); + WALBERLA_ASSERT_NOT_NULLPTR( boundaryHandling ); + + CellInterval localDomain = (useDataFromGhostLayers_) ? pdfField->xyzSizeWithGhostLayer() : pdfField->xyzSize(); + + std::vector<bool> availableStencilIndices(Stencil::Size, false); + + auto nAverage = uint_t(0); + auto averageDensity = real_t(0); + + // density and velocity used in the reconstruction + auto targetDensity = real_t(0); + Vector3<real_t> targetVelocity; + + real_t cx, cy, cz; + blockStorage_->getBlockLocalCellCenter( *block, Cell(x,y,z), cx, cy, cz ); + + // 1. evaluate local average density and find available (=fluid) stencil directions + for( auto neighborDir = Stencil::beginNoCenter(); neighborDir != Stencil::end(); ++neighborDir ) + { + Cell neighbor( x + neighborDir.cx(), y + neighborDir.cy(), z + neighborDir.cz() ); + + // check if neighbor cell is inside domain + if( !( localDomain.contains( neighbor ) ) ) + continue; + + // check if neighbor cell is a fluid cell + if( boundaryHandling->isDomain( neighbor ) ) + { + // obtain density and add it up + averageDensity += pdfField->getDensity( neighbor ); + ++nAverage; + availableStencilIndices[neighborDir.toIdx()] = true; + } + } + averageDensity = ( nAverage > uint_t( 0 ) ) ? averageDensity / real_c( nAverage ) : real_t(1.0); + + // 2. evaluate target velocity + // we simply use the body velocity in the cell center here since the cell center is probably not far from the particle surface so this is a valid approximation + // alternatively, some interpolated velocity might be used here as well (see Chikatamarla et al) + targetVelocity = mesa_pd::getVelocityAtWFPoint(particleIdx, ac, Vector3<real_t>(cx,cy,cz)); + + // 3. compute target density (and adapt to compressible/incompressible) + + // note: for compressible lattice models, the targetDensity is centered around 1 + // for incompressible, the targetDensity is just the deviation from the average (=1) + + if(recomputeTargetDensity_) + { + // in this variant the target density gets explicitly computed by considering the surrounding PDFs and using "stream" and "velocity bounce back" considerations like in Chikatamarla et al + for( auto q = Stencil::begin(); q != Stencil::end(); ++q ) + { + if( availableStencilIndices[q.toInvIdx()]) + { + // "stream" + auto invDir = q.inverseDir(); + Cell neighbor( x + stencil::cx[invDir], y + stencil::cy[invDir], z + stencil::cz[invDir] ); + targetDensity += pdfField->get(neighbor, *q); + } + else if(availableStencilIndices[q.toIdx()]) + { + // "velocity bounce back" + Cell neighbor( x + stencil::cx[*q], y + stencil::cy[*q], z + stencil::cz[*q] ); + targetDensity += pdfField->get(neighbor, q.inverseDir()); // "bounce back part" + if(LatticeModel::compressible) + { + targetDensity += real_t(6) * averageDensity * LatticeModel::w[ Stencil::idx[*q] ] * ( real_c( stencil::cx[ *q ] ) * targetVelocity[0] + + real_c( stencil::cy[ *q ] ) * targetVelocity[1] + + real_c( stencil::cz[ *q ] ) * targetVelocity[2] ); //TODO use wall velocity here? + } else { + targetDensity += real_t(6) * LatticeModel::w[ Stencil::idx[*q] ] * ( real_c( stencil::cx[ *q ] ) * targetVelocity[0] + + real_c( stencil::cy[ *q ] ) * targetVelocity[1] + + real_c( stencil::cz[ *q ] ) * targetVelocity[2] ); //TODO use wall velocity here? + } + + } else{ + // no neighboring information available (e.g. for center), so we use feq based on target velocity and average density + targetDensity += lbm::EquilibriumDistribution< LatticeModel >::get( *q, targetVelocity, averageDensity ); + } + } + } else { + // alternatively, we can simply use the average density from the surrounding fluid cells + // only minor differences have been seen in comparison to recomputing + targetDensity = (LatticeModel::compressible) ? averageDensity : averageDensity - real_t(1); + } + + // 4. compute pressure tensor from eq and neq parts + // 4.1 evaluate Peq -> will be done directly lateron + + // 4.2 evaluate Pneq -> needs velocity gradient tensor + // grad(u) = + // | du1/dx1 du2/dx1 du3/dx1 | | 0 1 2 | | 0,0 0,1 0,2 | + // | du1/dx2 du2/dx2 du3/dx2 | = | 3 4 5 | = | 1,0 1,1 1,2 | + // | du1/dx3 du2/dx3 du3/dx3 | | 6 7 8 | | 2,0 2,1 2,2 | + // evaluation of velocity gradients done via central finite differences if two neighbors available + // else first-order FD upwinding is carried out if upwinding neighbor available + // else first-order finite differences if available if other neighbor is available + // else we assume gradient of 0 (if no fluid neighbors available in respective direction) + + Matrix3<real_t> velocityGradient(real_t(0)); + // check if both neighbors are available for central differences + if(useCentralDifferences_ && availableStencilIndices[Stencil::idx[stencil::E]] && availableStencilIndices[Stencil::idx[stencil::W]]) + { + auto neighborVelocity1 = pdfField->getVelocity(Cell(x,y,z)+stencil::E); + auto neighborVelocity2 = pdfField->getVelocity(Cell(x,y,z)+stencil::W); + velocityGradient[0] = real_t(0.5) * ( neighborVelocity1[0] - neighborVelocity2[0]); // assuming dx = 1 + velocityGradient[1] = real_t(0.5) * ( neighborVelocity1[1] - neighborVelocity2[1]); // assuming dx = 1 + velocityGradient[2] = real_t(0.5) * ( neighborVelocity1[2] - neighborVelocity2[2]); // assuming dx = 1 + + } else { + //upwinding + stencil::Direction upwindingXDirection = (targetVelocity[0] > real_t(0) ) ? stencil::W : stencil::E; + stencil::Direction sourceXDirection = (availableStencilIndices[Stencil::idx[upwindingXDirection]]) ? upwindingXDirection + : ((availableStencilIndices[Stencil::idx[stencil::inverseDir[upwindingXDirection]]]) ? stencil::inverseDir[upwindingXDirection] + : stencil::C ); + if(sourceXDirection == stencil::E) + { + auto neighborVelocity = pdfField->getVelocity(Cell(x,y,z)+sourceXDirection); + velocityGradient[0] = neighborVelocity[0] - targetVelocity[0]; // assuming dx = 1 + velocityGradient[1] = neighborVelocity[1] - targetVelocity[1]; // assuming dx = 1 + velocityGradient[2] = neighborVelocity[2] - targetVelocity[2]; // assuming dx = 1 + } + if(sourceXDirection == stencil::W) + { + auto neighborVelocity = pdfField->getVelocity(Cell(x,y,z)+sourceXDirection); + velocityGradient[0] = targetVelocity[0] - neighborVelocity[0]; // assuming dx = 1 + velocityGradient[1] = targetVelocity[1] - neighborVelocity[1]; // assuming dx = 1 + velocityGradient[2] = targetVelocity[2] - neighborVelocity[2]; // assuming dx = 1 + } + // else: 0 + } + + // check if both neighbors are available for central differences + if(useCentralDifferences_ && availableStencilIndices[Stencil::idx[stencil::N]] && availableStencilIndices[Stencil::idx[stencil::S]]) { + auto neighborVelocity1 = pdfField->getVelocity(Cell(x, y, z) + stencil::N); + auto neighborVelocity2 = pdfField->getVelocity(Cell(x, y, z) + stencil::S); + velocityGradient[3] = real_t(0.5) * (neighborVelocity1[0] - neighborVelocity2[0]); // assuming dx = 1 + velocityGradient[4] = real_t(0.5) * (neighborVelocity1[1] - neighborVelocity2[1]); // assuming dx = 1 + velocityGradient[5] = real_t(0.5) * (neighborVelocity1[2] - neighborVelocity2[2]); // assuming dx = 1 + } else { + //upwinding + stencil::Direction upwindingYDirection = (targetVelocity[1] > real_t(0) ) ? stencil::S : stencil::N; + stencil::Direction sourceYDirection = (availableStencilIndices[Stencil::idx[upwindingYDirection]]) ? upwindingYDirection + : ((availableStencilIndices[Stencil::idx[stencil::inverseDir[upwindingYDirection]]]) ? stencil::inverseDir[upwindingYDirection] + : stencil::C ); + if(sourceYDirection == stencil::N) + { + auto neighborVelocity = pdfField->getVelocity(Cell(x,y,z)+sourceYDirection); + velocityGradient[3] = neighborVelocity[0] - targetVelocity[0]; // assuming dx = 1 + velocityGradient[4] = neighborVelocity[1] - targetVelocity[1]; // assuming dx = 1 + velocityGradient[5] = neighborVelocity[2] - targetVelocity[2]; // assuming dx = 1 + } + if(sourceYDirection == stencil::S) + { + auto neighborVelocity = pdfField->getVelocity(Cell(x,y,z)+sourceYDirection); + velocityGradient[3] = targetVelocity[0] - neighborVelocity[0]; // assuming dx = 1 + velocityGradient[4] = targetVelocity[1] - neighborVelocity[1]; // assuming dx = 1 + velocityGradient[5] = targetVelocity[2] - neighborVelocity[2]; // assuming dx = 1 + } + // else: 0 + } + + if(Stencil::D == 3) + { + // only in 3D + // check if both neighbors are available for central differences + if(useCentralDifferences_ && availableStencilIndices[Stencil::idx[stencil::T]] && availableStencilIndices[Stencil::idx[stencil::B]]) { + auto neighborVelocity1 = pdfField->getVelocity(Cell(x, y, z) + stencil::T); + auto neighborVelocity2 = pdfField->getVelocity(Cell(x, y, z) + stencil::B); + velocityGradient[6] = real_t(0.5) * (neighborVelocity1[0] - neighborVelocity2[0]); // assuming dx = 1 + velocityGradient[7] = real_t(0.5) * (neighborVelocity1[1] - neighborVelocity2[1]); // assuming dx = 1 + velocityGradient[8] = real_t(0.5) * (neighborVelocity1[2] - neighborVelocity2[2]); // assuming dx = 1 + } else { + //upwinding + stencil::Direction upwindingZDirection = (targetVelocity[2] > real_t(0)) ? stencil::B : stencil::T; + stencil::Direction sourceZDirection = (availableStencilIndices[Stencil::idx[upwindingZDirection]]) ? upwindingZDirection + : ((availableStencilIndices[Stencil::idx[stencil::inverseDir[upwindingZDirection]]]) ? stencil::inverseDir[upwindingZDirection] + : stencil::C); + if (sourceZDirection == stencil::T) { + auto neighborVelocity = pdfField->getVelocity(Cell(x, y, z) + sourceZDirection); + velocityGradient[6] = neighborVelocity[0] - targetVelocity[0]; // assuming dx = 1 + velocityGradient[7] = neighborVelocity[1] - targetVelocity[1]; // assuming dx = 1 + velocityGradient[8] = neighborVelocity[2] - targetVelocity[2]; // assuming dx = 1 + } + if (sourceZDirection == stencil::B) { + auto neighborVelocity = pdfField->getVelocity(Cell(x, y, z) + sourceZDirection); + velocityGradient[6] = targetVelocity[0] - neighborVelocity[0]; // assuming dx = 1 + velocityGradient[7] = targetVelocity[1] - neighborVelocity[1]; // assuming dx = 1 + velocityGradient[8] = targetVelocity[2] - neighborVelocity[2]; // assuming dx = 1 + } + // else: 0 + } + } + + + Matrix3<real_t> pressureTensorNeq(real_t(0)); // without prefactor of rho, added later + const real_t preFac = - real_t(1) / (real_t(3) * omegaShear_); // 2 * beta (in Chikatamarla et al) = omega related to kinematic viscosity + for(auto j = uint_t(0); j <= uint_t(2); ++j) + { + for(auto i = uint_t(0); i <= uint_t(2); ++i) + { + pressureTensorNeq(i,j) += preFac * (velocityGradient(i,j)+velocityGradient(j,i)); + } + } + + // 5. set PDFs to approximated equilibrium by Grad + // this is just the regular feq with additional Pneq part + if(LatticeModel::compressible) + { + for( auto q = Stencil::begin(); q != Stencil::end(); ++q ) + { + const real_t velci = lbm::internal::multiplyVelocityDirection( *q, targetVelocity ); + + auto contributionFromPneq = real_t(0); + for(auto j = uint_t(0); j <= uint_t(2); ++j) { + for (auto i = uint_t(0); i <= uint_t(2); ++i) { + //Pneq_a,b * c_q,a * c_q,b + contributionFromPneq += pressureTensorNeq(i,j) * real_c(stencil::c[i][*q]) * real_c(stencil::c[j][*q]); + } + } + //- Pneq_a,b * cs**2 * delta_a,b + contributionFromPneq -= (pressureTensorNeq(0,0) + pressureTensorNeq(1,1) + pressureTensorNeq(2,2)) / real_t(3); + + // all terms are multiplied with the density + real_t fGrad = LatticeModel::w[ q.toIdx() ] * targetDensity * (real_t(1) + real_t(3) * velci - real_t(1.5) * targetVelocity.sqrLength() + real_t(4.5) * velci * velci + real_t(4.5) * contributionFromPneq); // standard comp. feq + comp. Pneq + pdfField->get(x,y,z,*q) = fGrad; + } + }else{ + // assume rho = 1 almost everywhere, targetDensity is just the deviation from 1 + for( auto q = Stencil::begin(); q != Stencil::end(); ++q ) + { + const real_t velci = lbm::internal::multiplyVelocityDirection( *q, targetVelocity ); + + auto contributionFromPneq = real_t(0); + for(auto j = uint_t(0); j <= uint_t(2); ++j) { + for (auto i = uint_t(0); i <= uint_t(2); ++i) { + //Pneq_a,b * c_q,a * c_q,b + contributionFromPneq += pressureTensorNeq(i,j) * real_c(stencil::c[i][*q]) * real_c(stencil::c[j][*q]); + } + } + //- Pneq_a,b * cs**2 * delta_a,b + contributionFromPneq -= (pressureTensorNeq(0,0) + pressureTensorNeq(1,1) + pressureTensorNeq(2,2)) / real_t(3); + + // density deviation just appears as the leading order term + real_t fGrad = LatticeModel::w[ q.toIdx() ] * ( targetDensity + real_t(3) * velci - real_t(1.5) * targetVelocity.sqrLength() + real_t(4.5) * velci * velci + real_t(4.5) * contributionFromPneq); // standard incomp. feq + incomp Pneq + pdfField->get(x,y,z,*q) = fGrad; + } + } + } + + void setOmegaShear(real_t omegaShear) + { + omegaShear_ = omegaShear; + } + + real_t getOmegaShear() + { + return omegaShear_; + } + +private: + + shared_ptr<StructuredBlockStorage> blockStorage_; + const BlockDataID boundaryHandlingID_; + real_t omegaShear_; + const bool recomputeTargetDensity_; + const bool useCentralDifferences_; + const bool useDataFromGhostLayers_; +}; + + + +// make functionality + +template< typename BoundaryHandling_T> +shared_ptr<EquilibriumReconstructor<BoundaryHandling_T> > +makeEquilibriumReconstructor(const shared_ptr<StructuredBlockStorage> & blockStorage, const BlockDataID & boundaryHandlingID, bool useDataFromGhostLayers = false) +{ + using Rec_T = EquilibriumReconstructor<BoundaryHandling_T>; + return make_shared<Rec_T>(blockStorage, boundaryHandlingID, useDataFromGhostLayers); +} + +template< typename BoundaryHandling_T, typename ExtrapolationDirectionFinder_T > +shared_ptr<EquilibriumAndNonEquilibriumReconstructor<BoundaryHandling_T,ExtrapolationDirectionFinder_T> > +makeEquilibriumAndNonEquilibriumReconstructor(const shared_ptr<StructuredBlockStorage> & blockStorage, const BlockDataID & boundaryHandlingID, const shared_ptr<ExtrapolationDirectionFinder_T> & extrapolationDirectionFinder, + uint_t maximumNumberOfExtrapolationCells = uint_t(3), bool useDataFromGhostLayers = false) +{ + using Rec_T = EquilibriumAndNonEquilibriumReconstructor<BoundaryHandling_T,ExtrapolationDirectionFinder_T>; + return make_shared<Rec_T>(blockStorage, boundaryHandlingID, extrapolationDirectionFinder, maximumNumberOfExtrapolationCells, useDataFromGhostLayers); +} + +template< typename BoundaryHandling_T, typename ExtrapolationDirectionFinder_T, bool EnforceNoSlipConstraintAfterExtrapolation = false > +shared_ptr<ExtrapolationReconstructor<BoundaryHandling_T,ExtrapolationDirectionFinder_T, EnforceNoSlipConstraintAfterExtrapolation> > +makeExtrapolationReconstructor(const shared_ptr<StructuredBlockStorage> & blockStorage, const BlockDataID & boundaryHandlingID, const shared_ptr<ExtrapolationDirectionFinder_T> & extrapolationDirectionFinder, + uint_t maximumNumberOfExtrapolationCells = uint_t(3), bool useDataFromGhostLayers = false) +{ + using Rec_T = ExtrapolationReconstructor<BoundaryHandling_T,ExtrapolationDirectionFinder_T, EnforceNoSlipConstraintAfterExtrapolation>; + return make_shared<Rec_T>(blockStorage, boundaryHandlingID, extrapolationDirectionFinder, maximumNumberOfExtrapolationCells, useDataFromGhostLayers); +} + +template< typename BoundaryHandling_T> +shared_ptr<GradsMomentApproximationReconstructor<BoundaryHandling_T> > +makeGradsMomentApproximationReconstructor(const shared_ptr<StructuredBlockStorage> & blockStorage, const BlockDataID & boundaryHandlingID, real_t omegaShear, + bool recomputeTargetDensity = false, bool useCentralDifferences = true, bool useDataFromGhostLayers = false) +{ + using Rec_T = GradsMomentApproximationReconstructor<BoundaryHandling_T>; + return make_shared<Rec_T>(blockStorage, boundaryHandlingID, omegaShear, recomputeTargetDensity, useCentralDifferences, useDataFromGhostLayers); +} + + +} // namespace lbm_mesapd_coupling +} // namespace walberla diff --git a/src/lbm_mesapd_coupling/utility/AddAccelerationOnParticlesKernel.h b/src/lbm_mesapd_coupling/utility/AddAccelerationOnParticlesKernel.h new file mode 100644 index 0000000000000000000000000000000000000000..b27ba1842a627415cd2d154b9bf1ef222ed37199 --- /dev/null +++ b/src/lbm_mesapd_coupling/utility/AddAccelerationOnParticlesKernel.h @@ -0,0 +1,77 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla is free software: you can +// redistribute it and/or modify it under the terms of the GNU General Public +// License as published by the Free Software Foundation, either version 3 of +// the License, or (at your option) any later version. +// +// waLBerla is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +// for more details. +// +// You should have received a copy of the GNU General Public License along +// with waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file AddAccelerationOnParticlesKernel.h +//! \ingroup lbm_mesapd_coupling +//! \author Christoph Rettinger <christoph.rettinger@fau.de> +// +//====================================================================================================================== + +#pragma once + +#include "core/math/Vector3.h" + +#include "mesa_pd/common/ParticleFunctions.h" +#include "mesa_pd/data/IAccessor.h" + +#include <functional> + +namespace walberla { +namespace lbm_mesapd_coupling { + +/* + * Kernel that adds a constant acceleration via F = m * a on particles + * + * Note that setting forces on ghost particles results in duplicated forces on these particles. + * + * If the force is constant for all particles, it is better to use AddForceOnParticlesKernel. + * + */ +class AddAccelerationOnParticlesKernel +{ + +public: + + explicit AddAccelerationOnParticlesKernel( const Vector3<real_t> & acceleration ) + : acceleration_( acceleration ) + { } + + + template< typename ParticleAccessor_T > + void operator()(const size_t idx, ParticleAccessor_T& ac) const + { + static_assert(std::is_base_of<mesa_pd::data::IAccessor, ParticleAccessor_T>::value, "Provide a valid accessor as template"); + + auto force = acceleration_ / ac.getInvMass(idx); + + mesa_pd::addForceAtomic(idx, ac, force); + } + + void setAcceleration( const Vector3<real_t> & newAcceleration ) + { + acceleration_ = newAcceleration; + } + + Vector3<real_t> getAcceleration() const + { + return acceleration_; + } + +private: + Vector3<real_t> acceleration_; +}; + +} // namespace lbm_mesapd_coupling +} // namespace walberla diff --git a/src/lbm_mesapd_coupling/utility/AddForceOnParticlesKernel.h b/src/lbm_mesapd_coupling/utility/AddForceOnParticlesKernel.h new file mode 100644 index 0000000000000000000000000000000000000000..703550f2e75a598e162765faafaa59db40498096 --- /dev/null +++ b/src/lbm_mesapd_coupling/utility/AddForceOnParticlesKernel.h @@ -0,0 +1,72 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla is free software: you can +// redistribute it and/or modify it under the terms of the GNU General Public +// License as published by the Free Software Foundation, either version 3 of +// the License, or (at your option) any later version. +// +// waLBerla is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +// for more details. +// +// You should have received a copy of the GNU General Public License along +// with waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file AddForceOnParticlesKernel.h +//! \ingroup lbm_mesapd_coupling +//! \author Christoph Rettinger <christoph.rettinger@fau.de> +// +//====================================================================================================================== + +#pragma once + +#include "core/math/Vector3.h" + +#include "mesa_pd/common/ParticleFunctions.h" +#include "mesa_pd/data/IAccessor.h" + +#include <functional> + +namespace walberla { +namespace lbm_mesapd_coupling { + +/* + * Kernel that adds a constant force on particles + * + * Note that setting forces on ghost particles results in duplicated forces on these particles. + * + */ +class AddForceOnParticlesKernel +{ + +public: + + explicit AddForceOnParticlesKernel( const Vector3<real_t> & force ) + : force_( force ) + { } + + template< typename ParticleAccessor_T > + void operator()(const size_t idx, ParticleAccessor_T& ac) const + { + static_assert(std::is_base_of<mesa_pd::data::IAccessor, ParticleAccessor_T>::value, "Provide a valid accessor as template"); + + mesa_pd::addForceAtomic(idx, ac, force_); + } + + void setForce( const Vector3<real_t> & newForce ) + { + force_ = newForce; + } + + Vector3<real_t> getForce() const + { + return force_; + } + +private: + Vector3<real_t> force_; +}; + +} // namespace lbm_mesapd_coupling +} // namespace walberla diff --git a/src/lbm_mesapd_coupling/utility/AddHydrodynamicInteractionKernel.h b/src/lbm_mesapd_coupling/utility/AddHydrodynamicInteractionKernel.h new file mode 100644 index 0000000000000000000000000000000000000000..4ca474cc0a24d59e9798bbfe4a387ac390a5da70 --- /dev/null +++ b/src/lbm_mesapd_coupling/utility/AddHydrodynamicInteractionKernel.h @@ -0,0 +1,53 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla is free software: you can +// redistribute it and/or modify it under the terms of the GNU General Public +// License as published by the Free Software Foundation, either version 3 of +// the License, or (at your option) any later version. +// +// waLBerla is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +// for more details. +// +// You should have received a copy of the GNU General Public License along +// with waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file AddHydrodynamicInteractionKernel.h +//! \ingroup lbm_mesapd_coupling +//! \author Christoph Rettinger <christoph.rettinger@fau.de> +// +//====================================================================================================================== + +#pragma once + +#include "mesa_pd/common/ParticleFunctions.h" +#include "mesa_pd/data/IAccessor.h" + +namespace walberla { +namespace lbm_mesapd_coupling { + +/* + * Kernel that adds the current hydrodynamic forces and torques onto the particles as forces and torques + * + * Should usually be carried out on local and ghost particles. + */ +class AddHydrodynamicInteractionKernel +{ + +public: + + AddHydrodynamicInteractionKernel() = default; + + template< typename ParticleAccessor_T > + void operator()(const size_t idx, ParticleAccessor_T& ac) const + { + static_assert(std::is_base_of<mesa_pd::data::IAccessor, ParticleAccessor_T>::value, "Provide a valid accessor as template"); + + mesa_pd::addForceAtomic(idx, ac, ac.getHydrodynamicForce(idx)); + mesa_pd::addTorqueAtomic(idx, ac, ac.getHydrodynamicTorque(idx)); + } +}; + +} // namespace lbm_mesapd_coupling +} // namespace walberla diff --git a/src/lbm_mesapd_coupling/utility/AverageHydrodynamicForceTorqueKernel.h b/src/lbm_mesapd_coupling/utility/AverageHydrodynamicForceTorqueKernel.h new file mode 100644 index 0000000000000000000000000000000000000000..31c828e0d1c574d6b372048ff007ccaeefe931fc --- /dev/null +++ b/src/lbm_mesapd_coupling/utility/AverageHydrodynamicForceTorqueKernel.h @@ -0,0 +1,65 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla is free software: you can +// redistribute it and/or modify it under the terms of the GNU General Public +// License as published by the Free Software Foundation, either version 3 of +// the License, or (at your option) any later version. +// +// waLBerla is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +// for more details. +// +// You should have received a copy of the GNU General Public License along +// with waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file AverageHydrodynamicForceTorqueKernel.h +//! \ingroup lbm_mesapd_coupling +//! \author Christoph Rettinger <christoph.rettinger@fau.de> +// +//====================================================================================================================== + +#pragma once + +#include "mesa_pd/data/IAccessor.h" + +namespace walberla { +namespace lbm_mesapd_coupling { + +/* + * Kernel that averages the force/torque over two time steps in a rolling average fashion. + * This is said to damp oscillations of the interaction force/torque. + * See Ladd - " Numerical simulations of particulate suspensions via a discretized Boltzmann equation. Part 1. Theoretical foundation", 1994, p. + * + * Should usually be carried out on local and ghost particles. + */ +class AverageHydrodynamicForceTorqueKernel +{ + +public: + + AverageHydrodynamicForceTorqueKernel( ) = default; + + template< typename ParticleAccessor_T > + void operator()(const size_t idx, ParticleAccessor_T& ac) const + { + static_assert(std::is_base_of<mesa_pd::data::IAccessor, ParticleAccessor_T>::value, "Provide a valid accessor as template"); + + auto hydForce = ac.getHydrodynamicForce(idx); + auto hydTorque = ac.getHydrodynamicTorque(idx); + + auto averageForce = real_t(0.5) * ( hydForce + ac.getOldHydrodynamicForce(idx) ); + auto averageTorque = real_t(0.5) * ( hydTorque + ac.getOldHydrodynamicTorque(idx) ); + + // swap old values with new ones + ac.setOldHydrodynamicForce( idx, hydForce ); + ac.setOldHydrodynamicTorque( idx, hydTorque ); + + // set values to averaged ones + ac.setHydrodynamicForce ( idx, averageForce ); + ac.setHydrodynamicTorque ( idx, averageTorque ); + } +}; + +} // namespace lbm_mesapd_coupling +} // namespace walberla diff --git a/src/lbm_mesapd_coupling/utility/InspectionProbe.h b/src/lbm_mesapd_coupling/utility/InspectionProbe.h new file mode 100644 index 0000000000000000000000000000000000000000..4513cdfaf616982c2d11aa1d5253ce2d49459eab --- /dev/null +++ b/src/lbm_mesapd_coupling/utility/InspectionProbe.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 InspectionProbe.h +//! \ingroup lbm_mesapd_coupling +//! \author Christoph Rettinger <christoph.rettinger@fau.de> +// +//====================================================================================================================== + +#pragma once + +#include "core/math/Vector3.h" +#include "domain_decomposition/StructuredBlockStorage.h" +#include "mesa_pd/data/Flags.h" + +namespace walberla { +namespace lbm_mesapd_coupling { + + +/* + * Functionality that allows to monitor the state of a specific cell in the fluid-particle simulation. + * Options to output the state (fluid with density and velocity, or particle) on screen or to file are available. + * Also, the state of surrounding cells can be printed to screen. + * Can generally be used to track down the source of instabilities in a specific cell which often has its origin locally in the direct neighborhood of the cell. + * + */ +template< typename PdfField_T, typename BoundaryHandling_T, typename ParticleAccessor_T > +class InspectionProbe +{ + +public: + InspectionProbe(Vector3<real_t> probeLocation, + const shared_ptr< StructuredBlockStorage > & blocks, const BlockDataID & pdfFieldID, const BlockDataID & boundaryHandlingID, + const BlockDataID & particleFieldID, const shared_ptr< ParticleAccessor_T > & ac, + bool printToScreen, bool printSurroundingState, std::string outputFileName) + : probeLocation_(probeLocation), blocks_(blocks), pdfFieldID_(pdfFieldID), boundaryHandlingID_(boundaryHandlingID), particleFieldID_(particleFieldID), + ac_(ac), printToScreen_(printToScreen), printSurroundingState_(printSurroundingState), outputFileName_(outputFileName) + {} + + void operator()(real_t & rho, Vector3<real_t> & velocity) + { + + Cell probeCellGlobal = blocks_->getCell(probeLocation_); + auto block = blocks_->getBlock(probeLocation_); + if(block != nullptr) + { + auto pdfField = block->template getData<PdfField_T>(pdfFieldID_); + auto boundaryHandling = block->template getData<BoundaryHandling_T>(boundaryHandlingID_); + + Cell probeCell; + blocks_->transformGlobalToBlockLocalCell( probeCell, *block, probeCellGlobal ); + + if(printSurroundingState_) printStatusOfCellSurrounding(probeCell, *block); + + rho = pdfField->getDensityAndVelocity(velocity,probeCell); + + bool isFluid = boundaryHandling->isDomain(probeCell); + + printToScreen(isFluid, rho, velocity); + writeToFile(isFluid, rho, velocity); + + } + } + +private: + + void printStatusOfCellSurrounding(Cell centerCell, const IBlock& block) + { + auto pdfField = block.getData<PdfField_T>(pdfFieldID_); + auto boundaryHandling = block.getData<BoundaryHandling_T>(boundaryHandlingID_); + auto particleField = block.getData<lbm_mesapd_coupling::ParticleField_T>(particleFieldID_); + + std::stringstream outputString; + + for(cell_idx_t z = cell_idx_t(-1); z <= cell_idx_t(1); ++z) + { + for(cell_idx_t y = cell_idx_t(1); y >= cell_idx_t(-1); --y) + { + outputString << "*------------------------------------------------------------------------------------------------------------------------------------------------------------------------*\n"; + for(cell_idx_t x = cell_idx_t(-1); x <= cell_idx_t(1); ++x) + { + auto cell = centerCell + Cell(x,y,z); + auto cellCenter = blocks_->getBlockLocalCellCenter(block, cell); + real_t rho; + Vector3<real_t> velocity; + bool isFluid=false; + bool isFixed=false; + bool isGlobal=false; + if(boundaryHandling->isDomain(cell)) + { + isFluid = true; + rho = pdfField->getDensityAndVelocity(velocity,cell); + } else{ + isFluid = false; + rho = real_t(0); + auto particleIdx = ac_->uidToIdx(particleField->get( cell )); + velocity = mesa_pd::getVelocityAtWFPoint(particleIdx, *ac_, cellCenter ); + if(isSet(ac_->getFlags(particleIdx), mesa_pd::data::particle_flags::FIXED)) isFixed = true; + if(isSet(ac_->getFlags(particleIdx), mesa_pd::data::particle_flags::GLOBAL)) isGlobal = true; + } + outputString << std::setprecision(5) << "| " << cellCenter << " (" << (isFluid?"F":(std::string("P")+(isFixed?"+F":"")+(isGlobal?"+G":""))) << ") " << rho << " " << velocity << " "; + } + outputString << "|\n"; + } + outputString << "*------------------------------------------------------------------------------------------------------------------------------------------------------------------------*\n\n"; + } + WALBERLA_LOG_INFO(outputString.str()); + } + + void printToScreen(bool isFluid, real_t rho, Vector3<real_t> velocity) + { + if(printToScreen_) WALBERLA_LOG_INFO("Values in probe at position " << probeLocation_ << ": " << (isFluid?"F":"P") << ", rho = " << rho << ", velocity = " << velocity); + } + + void writeToFile(bool isFluid, real_t rho, Vector3<real_t> velocity) + { + if(!outputFileName_.empty()) + { + std::ofstream file; + file.open( outputFileName_.c_str(), std::ofstream::app); + + file << isFluid << " " << rho << " " << velocity[0] << " " << velocity[1] << " " << velocity[2] << std::endl; + file.close(); + } + } + + Vector3<real_t> probeLocation_; + shared_ptr< StructuredBlockStorage > blocks_; + const ConstBlockDataID pdfFieldID_; + const ConstBlockDataID boundaryHandlingID_; + const ConstBlockDataID particleFieldID_; + shared_ptr< ParticleAccessor_T > ac_; + bool printToScreen_; + bool printSurroundingState_; + std::string outputFileName_; + +}; + +} // namespace lbm_mesapd_coupling +} // namespace walberla diff --git a/src/lbm_mesapd_coupling/utility/LubricationCorrectionKernel.h b/src/lbm_mesapd_coupling/utility/LubricationCorrectionKernel.h new file mode 100644 index 0000000000000000000000000000000000000000..39ac7597b5106fdbb8d7922335c13c3d7c47ef8b --- /dev/null +++ b/src/lbm_mesapd_coupling/utility/LubricationCorrectionKernel.h @@ -0,0 +1,359 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla is free software: you can +// redistribute it and/or modify it under the terms of the GNU General Public +// License as published by the Free Software Foundation, either version 3 of +// the License, or (at your option) any later version. +// +// waLBerla is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +// for more details. +// +// You should have received a copy of the GNU General Public License along +// with waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file LubricationCorrectionKernel.h +//! \author Christoph Rettinger <christoph.rettinger@fau.de> +// +//====================================================================================================================== + +#pragma once + +#include "core/math/Vector3.h" + +#include "mesa_pd/common/ParticleFunctions.h" +#include "mesa_pd/data/DataTypes.h" +#include "mesa_pd/data/IAccessor.h" +#include "mesa_pd/data/shape/Sphere.h" +#include "mesa_pd/data/shape/HalfSpace.h" + +namespace walberla { +namespace lbm_mesapd_coupling { + + +/*! \brief Computes lubrication corrections between two spheres 1 and 2 acting on sphere 1. + * + * The formulas for the lubrication corrections for two spheres with different radii are taken from + * Simeonov, Calantoni - Modeling mechanical contact and lubrication in Direct Numerical Simulations of colliding particles, IJMF, 2012 + * + * interactionNormal12 is sphere2Center - sphere1Center, normalized + * + * radius1, radius2: radius of sphere 1 and 2 + * u1, u2: linear/translational velocity of sphere 1 and 2 + * omega1, omega2: angular velocity of sphere 1 and 2 + * + * Returns force and torque via lubricationForce1 and lubricationTorque1 + * + */ +inline +void computeLubricationCorrectionSphereSphere( const Vector3<real_t> & interactionNormal12, const real_t gapSize, const real_t dynamicFluidViscosity, + const real_t radius1, const real_t radius2, + const Vector3<real_t> & u1, const Vector3<real_t> & u2, + const Vector3<real_t> & omega1, const Vector3<real_t> & omega2, + const real_t cutOffDistanceNormal, const real_t cutOffDistanceTangentialTranslational, + const real_t cutOffDistanceTangentialRotational, + Vector3<real_t> & lubricationForce1, Vector3<real_t> & lubricationTorque1 ) +{ + WALBERLA_ASSERT_FLOAT_EQUAL(interactionNormal12.length(), real_t(1), "InteractionNormal has to be normalized!"); + + real_t kappa = radius2 / radius1; + real_t epsilon = gapSize / radius1; + + Vector3<real_t> e12 = -interactionNormal12; + Vector3<real_t> u12 = u2 - u1; + Vector3<real_t> omega12 = omega1 + omega2; + + lubricationForce1 = Vector3<real_t>(real_t(0)); + lubricationTorque1 = Vector3<real_t>(real_t(0)); + + if( gapSize < cutOffDistanceNormal ) + { + // add lubrication force due to normal translation + real_t epsres = cutOffDistanceNormal / radius1; + Vector3<real_t> Fn1 = real_t(6) * walberla::math::pi * radius1 * dynamicFluidViscosity * + ( kappa * kappa / ( ( real_t(1) + kappa ) * ( real_t(1) + kappa ) ) ) * ( real_t(1) / epsilon - real_t(1) / epsres) * + math::dot(u12, e12) * e12; + + lubricationForce1 += Fn1; + } + + if( gapSize < cutOffDistanceTangentialTranslational ) + { + // add lubrication force and torque due to tangential translation + real_t epsres = cutOffDistanceTangentialTranslational / radius1; + real_t logEpsDivEpsres = std::log(epsilon/epsres); + Vector3<real_t> Ftt1 = real_t(6) * walberla::math::pi * radius1 * dynamicFluidViscosity * + ( - real_t(4) * kappa * (real_t(2) + kappa + real_t(2) * kappa * kappa ) / ( real_t(15) * (real_t(1) + kappa) * (real_t(1) + kappa) * (real_t(1) + kappa) ) ) * logEpsDivEpsres * + (u12 - math::dot(u12,e12) * e12); + + lubricationForce1 += Ftt1; + + Vector3<real_t> Ttt1 = real_t(8) * walberla::math::pi * radius1 * radius1 * dynamicFluidViscosity * + ( - kappa * (real_t(4) + kappa) / (real_t(10) * (real_t(1)+kappa) * (real_t(1)+kappa) ) ) * logEpsDivEpsres * + math::cross(e12, u12); + + lubricationTorque1 += Ttt1; + } + + if( gapSize < cutOffDistanceTangentialRotational ) + { + // add lubrication force and torque due to tangential rotation + real_t epsres = cutOffDistanceTangentialRotational / radius1; + real_t logEpsDivEpsres = std::log(epsilon/epsres); + Vector3<real_t> Ftr1 = real_t(6) * walberla::math::pi * radius1 * radius1 * dynamicFluidViscosity * + (real_t(2) * kappa * kappa / (real_t(15) * (real_t(1) + kappa) * (real_t(1) + kappa) ) ) * logEpsDivEpsres * + math::cross(omega12 + real_t(4) / kappa * omega1 + real_t(4) * kappa * omega2, e12); + + lubricationForce1 += Ftr1; + + Vector3<real_t> tempOmega = omega1 + kappa * omega2 / real_t(4); + + Vector3<real_t> Ttr1 = real_t(8) * walberla::math::pi * radius1 * radius1 * radius1 * dynamicFluidViscosity * + (real_t(2) * kappa / ( real_t(5) * (real_t(1) + kappa))) * logEpsDivEpsres * + (tempOmega - math::dot(tempOmega, e12) * e12); + + lubricationTorque1 += Ttr1; + + } + + // note: lubrication due to normal rotation is dropped here because of too low influence, see also Simeonov + +} + + +/*! \brief Computes lubrication corrections between a sphere and a half space acting on the sphere. + * + * The formulas for the lubrication corrections are taken from + * Simeonov, Calantoni - Modeling mechanical contact and lubrication in Direct Numerical Simulations of colliding particles, IJMF, 2012 + * with kappa -> infinity + * * + * radius1: radius of sphere + * u1, u2: linear/translational velocity of sphere and half space + * omega1: angular velocity of sphere 1, half space is assumed to have zero angular velocity + * + * Returns force and torque via lubricationForce1 and lubricationTorque1 + */ +inline +void computeLubricationCorrectionSphereHalfSpace( const Vector3<real_t> & interactionNormal12, const real_t gapSize, const real_t dynamicFluidViscosity, + const real_t radius1, + const Vector3<real_t> & u1, const Vector3<real_t> & u2, + const Vector3<real_t> & omega1, + const real_t cutOffDistanceNormal, const real_t cutOffDistanceTangentialTranslational, + const real_t cutOffDistanceTangentialRotational, + Vector3<real_t> & lubricationForce1, Vector3<real_t> & lubricationTorque1 ) +{ + WALBERLA_ASSERT_FLOAT_EQUAL(interactionNormal12.length(), real_t(1), "InteractionNormal has to be normalized!"); + + real_t epsilon = gapSize / radius1; + + Vector3<real_t> e12 = -interactionNormal12; + Vector3<real_t> u12 = u2 - u1; + Vector3<real_t> omega12 = omega1; + + lubricationForce1 = Vector3<real_t>(real_t(0)); + lubricationTorque1 = Vector3<real_t>(real_t(0)); + + if( gapSize < cutOffDistanceNormal ) + { + // add lubrication force due to normal translation + real_t epsres = cutOffDistanceNormal / radius1; + Vector3<real_t> Fn1 = real_t(6) * walberla::math::pi * radius1 * dynamicFluidViscosity * + ( real_t(1) / epsilon - real_t(1) / epsres) * + math::dot(u12, e12) * e12; + + lubricationForce1 += Fn1; + + } + + if( gapSize < cutOffDistanceTangentialTranslational ) + { + // add lubrication force and torque due to tangential translation + real_t epsres = cutOffDistanceTangentialTranslational / radius1; + real_t logEpsDivEpsres = std::log(epsilon/epsres); + Vector3<real_t> Ftt1 = real_t(6) * walberla::math::pi * radius1 * dynamicFluidViscosity * + ( - real_t(8) / real_t(15) ) * logEpsDivEpsres * + (u12 - math::dot(u12,e12) * e12); + + lubricationForce1 += Ftt1; + + Vector3<real_t> Ttt1 = real_t(8) * walberla::math::pi * radius1 * radius1 * dynamicFluidViscosity * + ( - real_t(1) / real_t(10) ) * logEpsDivEpsres * + math::cross(e12, u12); + + lubricationTorque1 += Ttt1; + } + + if( gapSize < cutOffDistanceTangentialRotational ) + { + // add lubrication force and torque due to tangential rotation + real_t epsres = cutOffDistanceTangentialRotational / radius1; + real_t logEpsDivEpsres = std::log(epsilon/epsres); + Vector3<real_t> Ftr1 = real_t(6) * walberla::math::pi * radius1 * radius1 * dynamicFluidViscosity * + ( real_t(2) / real_t(15) ) * logEpsDivEpsres * + math::cross(omega12, e12); + + lubricationForce1 += Ftr1; + + Vector3<real_t> Ttr1 = real_t(8) * walberla::math::pi * radius1 * radius1 * radius1 * dynamicFluidViscosity * + ( real_t(2) / real_t(5) ) * logEpsDivEpsres * + (omega1 - math::dot(omega1, e12) * e12); + + lubricationTorque1 += Ttr1; + + } + + // note: lubrication due to normal rotation is dropped here because of too low influence, see also Simeonov + +} + + +/** + * Applies a correction for the unresolved lubrication forces and torques on the the interacting particles + * + * For gap sizes (in cells) below minimalGapSizeFunction(radius), no longer the actual gap size is used in the formulas. + * This value/function has to be found by calibration with collision experiments. + * For negative gap sizes, i.e. there is an overlap of the surfaces, no lubrication corrections are applied at any time. + * + * Three different cut off distance have to be specified: normal, tangential translational and tangential rotational. + * These distances have to be determined by extra simulations to evaluate up to which resolution the forces can still be reliably predicted by the simulation itself. + * Note that these cutoff distances are in cell size "units" and not non-dimensionalized as they are not physically motivated but instead numerical parameters. + * + * The formulas for the lubrication corrections for two spheres with different radii are taken from + * Simeonov, Calantoni - Modeling mechanical contact and lubrication in Direct Numerical Simulations of colliding particles, IJMF, 2012 + * + * By choosing the respective cutoff distances equal to 0, these contributions can be switched off. + * + * Should be used in combination with mesa_pd's contact detection algorithm to determine the interaction partners and the gap size. + * + */ +class LubricationCorrectionKernel +{ +public: + + explicit LubricationCorrectionKernel( real_t dynamicFluidViscosity, + std::function<real_t(real_t)> minimalGapSizeFunction, + real_t cutOffDistanceNormal = real_t(2) / real_t(3), + real_t cutOffDistanceTangentialTranslational = real_t(0.5), + real_t cutOffDistanceTangentialRotational = real_t(0.5) ) + : dynamicFluidViscosity_( dynamicFluidViscosity ), minimalGapSizeFunction_( minimalGapSizeFunction ), + cutOffDistanceNormal_( cutOffDistanceNormal ), cutOffDistanceTangentialTranslational_( cutOffDistanceTangentialTranslational ), + cutOffDistanceTangentialRotational_( cutOffDistanceTangentialRotational ) + {} + + template <typename Shape1_T, typename Shape2_T, typename ParticleAccessor_T> + inline void operator()( const size_t /*idx1*/, const size_t /*idx2*/, + const Shape1_T& /*shape1*/, const Shape2_T& /*shape2*/, + ParticleAccessor_T& /*ac*/, const Vector3<real_t>& /*interactionNormal*/, const real_t& /*gapSize*/) + { + WALBERLA_ABORT("Lubrication correction not implemented!") + } + + template <typename ParticleAccessor_T> + inline void operator()( const size_t idx1, const size_t idx2, + const mesa_pd::data::Sphere& sphere1, const mesa_pd::data::Sphere& sphere2, + ParticleAccessor_T& ac, const Vector3<real_t>& interactionNormal, const real_t& gapSize) + { + WALBERLA_ASSERT_UNEQUAL(idx1, idx2, "interacting with itself!"); + + // interaction normal is from sphere 2 to sphere 1 + + if( gapSize > real_t(0)) + { + real_t radius1 = sphere1.getRadius(); + real_t radius2 = sphere2.getRadius(); + Vector3<real_t> u1 = ac.getLinearVelocity(idx1); + Vector3<real_t> u2 = ac.getLinearVelocity(idx2); + Vector3<real_t> omega1 = ac.getAngularVelocity(idx1); + Vector3<real_t> omega2 = ac.getAngularVelocity(idx2); + + // compute and add lubrication corrections on sphere 1 + auto gap1 = std::max(gapSize, minimalGapSizeFunction_(radius1) ); // TODO check this, maybe one should use an average radius here to assert symmetry of forces? + Vector3<real_t> lubricationForce1(real_t(0)); + Vector3<real_t> lubricationTorque1(real_t(0)); + computeLubricationCorrectionSphereSphere(interactionNormal, gap1, dynamicFluidViscosity_, radius1, radius2, u1, u2, omega1, omega2, + cutOffDistanceNormal_, cutOffDistanceTangentialTranslational_, cutOffDistanceTangentialRotational_, + lubricationForce1, lubricationTorque1 ); + + mesa_pd::addForceAtomic(idx1, ac, lubricationForce1); + mesa_pd::addTorqueAtomic(idx1, ac, lubricationTorque1); + + // compute and add lubrication corrections on sphere 2 + auto gap2 = std::max(gapSize, minimalGapSizeFunction_(radius2) ); + Vector3<real_t> lubricationForce2(real_t(0)); + Vector3<real_t> lubricationTorque2(real_t(0)); + computeLubricationCorrectionSphereSphere( - interactionNormal, gap2, dynamicFluidViscosity_, radius2, radius1, u2, u1, omega2, omega1, + cutOffDistanceNormal_, cutOffDistanceTangentialTranslational_, cutOffDistanceTangentialRotational_, + lubricationForce2, lubricationTorque2 ); + + mesa_pd::addForceAtomic(idx2, ac, lubricationForce2); + mesa_pd::addTorqueAtomic(idx2, ac, lubricationTorque2); + + } + // else: no lubrication correction + + } + + template <typename ParticleAccessor_T> + inline void operator()( const size_t idx1, const size_t idx2, + const mesa_pd::data::Sphere& sphere, const mesa_pd::data::HalfSpace& /*halfSpace*/, + ParticleAccessor_T& ac, const Vector3<real_t>& interactionNormal, const real_t& gapSize) + { + // interaction normal is the normal of the half space + + if( gapSize > real_t(0)) + { + real_t radius1 = sphere.getRadius(); + Vector3<real_t> u1 = ac.getLinearVelocity(idx1); + Vector3<real_t> u2 = ac.getLinearVelocity(idx2); + Vector3<real_t> omega1 = ac.getAngularVelocity(idx1); + // angular velocity of half space is zero! + + // compute and add lubrication corrections on sphere 1 + auto gap1 = std::max(gapSize, minimalGapSizeFunction_(radius1) ); + Vector3<real_t> lubricationForce1(real_t(0)); + Vector3<real_t> lubricationTorque1(real_t(0)); + computeLubricationCorrectionSphereHalfSpace(interactionNormal, gap1, dynamicFluidViscosity_, radius1, u1, u2, omega1, + cutOffDistanceNormal_, cutOffDistanceTangentialTranslational_, cutOffDistanceTangentialRotational_, + lubricationForce1, lubricationTorque1 ); + + mesa_pd::addForceAtomic(idx1, ac, lubricationForce1); + mesa_pd::addTorqueAtomic(idx1, ac, lubricationTorque1); + + // NOTE: no lubrication corrections added to the half space! + + } + // else: no lubrication correction + } + + template <typename ParticleAccessor_T> + inline void operator()( const size_t idx1, const size_t idx2, + const mesa_pd::data::HalfSpace& halfSpace, const mesa_pd::data::Sphere& sphere, + ParticleAccessor_T& ac, const Vector3<real_t>& interactionNormal, const real_t& gapSize) + { + operator()(idx2, idx1, sphere, halfSpace, ac, -interactionNormal, gapSize); + } + + void setDynamicFluidViscosity( const real_t dynamicFluidViscosity){ dynamicFluidViscosity_ = dynamicFluidViscosity; } + void setMinimalGapSizeFunction( std::function<real_t(real_t)> minimalGapSizeFunction ){ minimalGapSizeFunction_ = minimalGapSizeFunction; } + void setNormalCutOffDistance( const real_t cutOffDistanceNormal){ cutOffDistanceNormal_ = cutOffDistanceNormal; } + void setTangentialTranslationalCutOffDistance( const real_t cutOffDistanceTangentialTranslational){ cutOffDistanceTangentialTranslational_ = cutOffDistanceTangentialTranslational; } + void setTangentialRotationalCutOffDistance( const real_t cutOffDistanceTangentialRotational){ cutOffDistanceTangentialRotational_ = cutOffDistanceTangentialRotational; } + + real_t getDynamicFluidViscosity() const { return dynamicFluidViscosity_; } + real_t getMinimalGapSize(real_t radius) const { return minimalGapSizeFunction_(radius); } + real_t getNormalCutOffDistance() const { return cutOffDistanceNormal_; } + real_t getTangentialTranslationalCutOffDistance() const { return cutOffDistanceTangentialTranslational_; } + real_t getTangentialRotationalCutOffDistance() const { return cutOffDistanceTangentialRotational_; } + +private: + + real_t dynamicFluidViscosity_; + std::function<real_t(real_t)> minimalGapSizeFunction_; + real_t cutOffDistanceNormal_; + real_t cutOffDistanceTangentialTranslational_; + real_t cutOffDistanceTangentialRotational_; +}; + +} //namespace lbm_mesapd_coupling +} //namespace walberla \ No newline at end of file diff --git a/src/lbm_mesapd_coupling/utility/OmegaBulkAdaption.h b/src/lbm_mesapd_coupling/utility/OmegaBulkAdaption.h new file mode 100644 index 0000000000000000000000000000000000000000..226c03c2f16a2a8ba1b399545a5f38057ed29340 --- /dev/null +++ b/src/lbm_mesapd_coupling/utility/OmegaBulkAdaption.h @@ -0,0 +1,176 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla is free software: you can +// redistribute it and/or modify it under the terms of the GNU General Public +// License as published by the Free Software Foundation, either version 3 of +// the License, or (at your option) any later version. +// +// waLBerla is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +// for more details. +// +// You should have received a copy of the GNU General Public License along +// with waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file OmegaBulkAdaption.h +//! \ingroup lbm_mesapd_coupling +//! \author Christoph Rettinger <christoph.rettinger@fau.de> +// +//====================================================================================================================== + +#pragma once + +#include "mesa_pd/common/AABBConversion.h" +#include "domain_decomposition/StructuredBlockStorage.h" + +namespace walberla { +namespace lbm_mesapd_coupling { + +// utility functions + +real_t bulkViscosityFromOmegaBulk(real_t omegaBulk) +{ + return real_t(2) / real_t(9) * ( real_t(1) / omegaBulk - real_t(0.5) ); +} + + +real_t omegaBulkFromBulkViscosity(real_t bulkViscosity) +{ + return real_t(2) / ( real_t(9) * bulkViscosity + real_t(1) ); +} + +// see Khirevich et al. - Coarse- and fine-grid numerical behavior of MRT/TRT lattice-Boltzmann schemes in regular and random sphere packings +// LambdaBulk is the "magic parameter" here, i.e. the ratio between Lambda_e and Lambda_nu, Eq. 19 +real_t omegaBulkFromOmega(real_t omega, real_t LambdaBulk = real_t(1)) +{ + return real_t(1) / (LambdaBulk * ( real_t(1) / omega - real_t(1)/ real_t(2) ) + real_t(1)/ real_t(2) ); +} + +/* + * Adapts all cells that are inside a sphere of radius interactionRadius + adaptionLayerSize around the particle position (center) + */ +template< typename ParticleAccessor_T, typename ParticleSelector_T > +class OmegaBulkAdapter +{ + static_assert(std::is_base_of<mesa_pd::data::IAccessor, ParticleAccessor_T>::value, "Provide a valid accessor as template"); + +public: + OmegaBulkAdapter(const shared_ptr<StructuredBlockStorage> & blockStorage, + const BlockDataID & omegaBulkFieldID, + const shared_ptr<ParticleAccessor_T>& ac, + const real_t defaultOmegaBulk, + const real_t adaptedOmegaBulk, + const real_t adaptionLayerSize, + ParticleSelector_T particleSelector) + : blockStorage_( blockStorage ), omegaBulkFieldID_( omegaBulkFieldID ), ac_( ac ), + defaultOmegaBulk_(defaultOmegaBulk), + adaptedOmegaBulk_(adaptedOmegaBulk), + adaptionLayerSize_(adaptionLayerSize), + particleSelector_( particleSelector ) + {} + + + void inline operator()( IBlock * const block ) + { + + using namespace lbm_mesapd_coupling; + + WALBERLA_ASSERT_NOT_NULLPTR( block ); + + auto * omegaBulkField = block->getData< GhostLayerField< real_t, 1> >( omegaBulkFieldID_ ); + + WALBERLA_ASSERT_NOT_NULLPTR( omegaBulkField ); + + // reset whole field to default + WALBERLA_FOR_ALL_CELLS_XYZ(omegaBulkField, omegaBulkField->get(x,y,z) = defaultOmegaBulk_; ) + + const auto blockLevel = blockStorage_->getLevel(*block); + const real_t dx = blockStorage_->dx( blockLevel ); + const real_t dy = blockStorage_->dy( blockLevel ); + const real_t dz = blockStorage_->dz( blockLevel ); + + for( size_t particleIdx = 0; particleIdx < ac_->size(); ++particleIdx ) + { + // currently only works for finite particles + if (particleSelector_(particleIdx, *ac_) && !mesa_pd::data::particle_flags::isSet( ac_->getFlags(particleIdx), mesa_pd::data::particle_flags::INFINITE)) + { + + auto particlePosition = ac_->getPosition(particleIdx); + auto particleInteractionRadius = ac_->getInteractionRadius(particleIdx); + auto extendedParticleAABB = mesa_pd::getAABBFromInteractionRadius(particlePosition, particleInteractionRadius).getExtended(adaptionLayerSize_); + auto cellBB = getCellBBFromAABB( extendedParticleAABB, false, *block, *blockStorage_, uint_t(0)); + // no ghost layers are included -> omega field has to be communicated afterwards if ghost layer values are needed in LBM sweep + + auto adaptionRadius = particleInteractionRadius + adaptionLayerSize_; + + Vector3<real_t> startCellCenter = blockStorage_->getBlockLocalCellCenter( *block, cellBB.min() ); + + real_t cz = startCellCenter[2]; + for( cell_idx_t z = cellBB.zMin(); z <= cellBB.zMax(); ++z ) + { + real_t cy = startCellCenter[1]; + for( cell_idx_t y = cellBB.yMin(); y <= cellBB.yMax(); ++y ) + { + real_t cx = startCellCenter[0]; + for( cell_idx_t x = cellBB.xMin(); x <= cellBB.xMax(); ++x ) + { + auto cellCenter = Vector3<real_t>(cx,cy,cz); + auto sqrDistanceToParticleCenter = (cellCenter-particlePosition).sqrLength(); + + if(sqrDistanceToParticleCenter < adaptionRadius * adaptionRadius) + { + // change omega to given adaption value + omegaBulkField->get(x,y,z) = adaptedOmegaBulk_; + } + cx += dx; + } + cy += dy; + } + cz += dz; + } + } + } + } + + void setDefaultOmegaBulk(real_t defaultOmegaBulk) + { + defaultOmegaBulk_ = defaultOmegaBulk; + } + real_t getDefaultOmegaBulk() + { + return defaultOmegaBulk_; + } + + void setAdaptedOmegaBulk(real_t adaptedOmegaBulk) + { + adaptedOmegaBulk_ = adaptedOmegaBulk; + } + real_t getAdaptedOmegaBulk() + { + return adaptedOmegaBulk_; + } + + void setAdaptionLayerSize( real_t adaptionLayerSize ) + { + adaptionLayerSize_ = adaptionLayerSize; + } + real_t getAdaptionLayerSize() + { + return adaptionLayerSize_; + } + +private: + + shared_ptr<StructuredBlockStorage> blockStorage_; + BlockDataID omegaBulkFieldID_; + shared_ptr<ParticleAccessor_T> ac_; + real_t defaultOmegaBulk_; + real_t adaptedOmegaBulk_; + real_t adaptionLayerSize_; + ParticleSelector_T particleSelector_; + +}; + +} // namespace lbm_mesapd_coupling +} // namespace walberla diff --git a/src/lbm_mesapd_coupling/utility/ParticleFunctions.h b/src/lbm_mesapd_coupling/utility/ParticleFunctions.h new file mode 100644 index 0000000000000000000000000000000000000000..ab72a6a84b37c9b94a13ceaa2f1c057e2fcd27c3 --- /dev/null +++ b/src/lbm_mesapd_coupling/utility/ParticleFunctions.h @@ -0,0 +1,93 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla is free software: you can +// redistribute it and/or modify it under the terms of the GNU General Public +// License as published by the Free Software Foundation, either version 3 of +// the License, or (at your option) any later version. +// +// waLBerla is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +// for more details. +// +// You should have received a copy of the GNU General Public License along +// with waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file ParticleFunctions.h +//! \ingroup lbm_mesapd_coupling +//! \author Christoph Rettinger <christoph.rettinger@fau.de> +// +//====================================================================================================================== + +#pragma once + +#include "core/math/AABB.h" +#include "core/math/Limits.h" + +#include "mesa_pd/data/Flags.h" +#include "mesa_pd/data/IAccessor.h" +#include "mesa_pd/data/shape/HalfSpace.h" +#include "mesa_pd/data/shape/Sphere.h" +#include "mesa_pd/kernel/SingleCast.h" + +namespace walberla { +namespace lbm_mesapd_coupling { + + +/** + * Force is applied at the center of mass. + */ +template <typename ParticleAccessor_T> +inline void addHydrodynamicForceAtomic(const size_t p_idx, ParticleAccessor_T& ac, const Vector3<real_t>& f) +{ + // Increasing the force and torque on this particle +#ifdef _OPENMP +#pragma omp atomic +#endif + ac.getHydrodynamicForceRef(p_idx)[0] += f[0]; +#ifdef _OPENMP +#pragma omp atomic +#endif + ac.getHydrodynamicForceRef(p_idx)[1] += f[1]; +#ifdef _OPENMP +#pragma omp atomic +#endif + ac.getHydrodynamicForceRef(p_idx)[2] += f[2]; +} + +template <typename ParticleAccessor_T> +inline void addHydrodynamicForceAtWFPosAtomic(const size_t p_idx, ParticleAccessor_T& ac, const Vector3<real_t>& f, const Vector3<real_t>& wf_pt) +{ + // Increasing the force and torque on this particle +#ifdef _OPENMP +#pragma omp atomic +#endif + ac.getHydrodynamicForceRef(p_idx)[0] += f[0]; +#ifdef _OPENMP +#pragma omp atomic +#endif + ac.getHydrodynamicForceRef(p_idx)[1] += f[1]; +#ifdef _OPENMP +#pragma omp atomic +#endif + ac.getHydrodynamicForceRef(p_idx)[2] += f[2]; + + const auto t = cross(( wf_pt - ac.getPosition(p_idx) ), f); + +#ifdef _OPENMP +#pragma omp atomic +#endif + ac.getHydrodynamicTorqueRef(p_idx)[0] += t[0]; +#ifdef _OPENMP +#pragma omp atomic +#endif + ac.getHydrodynamicTorqueRef(p_idx)[1] += t[1]; +#ifdef _OPENMP +#pragma omp atomic +#endif + ac.getHydrodynamicTorqueRef(p_idx)[2] += t[2]; +} + + +} // namespace lbm_mesapd_coupling +} // namespace walberla diff --git a/src/lbm_mesapd_coupling/utility/ParticleSelector.h b/src/lbm_mesapd_coupling/utility/ParticleSelector.h new file mode 100644 index 0000000000000000000000000000000000000000..9baf0a4a1bb36d3938a6fcaefeeb0b10f480a1dd --- /dev/null +++ b/src/lbm_mesapd_coupling/utility/ParticleSelector.h @@ -0,0 +1,116 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla is free software: you can +// redistribute it and/or modify it under the terms of the GNU General Public +// License as published by the Free Software Foundation, either version 3 of +// the License, or (at your option) any later version. +// +// waLBerla is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +// for more details. +// +// You should have received a copy of the GNU General Public License along +// with waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file ParticleSelector.h +//! \ingroup lbm_mesapd_coupling +//! \author Christoph Rettinger <christoph.rettinger@fau.de> +// +//====================================================================================================================== + +#pragma once + +#include "mesa_pd/data/Flags.h" +#include "mesa_pd/data/IAccessor.h" +#include "mesa_pd/data/shape/Sphere.h" + +namespace walberla { +namespace lbm_mesapd_coupling { + +struct RegularParticlesSelector +{ + template< typename ParticleAccessor_T > + bool inline operator()(const size_t particleIdx, const ParticleAccessor_T & ac) const + { + static_assert(std::is_base_of<mesa_pd::data::IAccessor, ParticleAccessor_T>::value, "Provide a valid accessor as template"); + return !mesa_pd::data::particle_flags::isSet( ac.getFlags(particleIdx), mesa_pd::data::particle_flags::FIXED) && + !mesa_pd::data::particle_flags::isSet( ac.getFlags(particleIdx), mesa_pd::data::particle_flags::GLOBAL); + } +}; + +struct FixedParticlesSelector +{ + template< typename ParticleAccessor_T > + bool inline operator()(const size_t particleIdx, const ParticleAccessor_T & ac) const + { + static_assert(std::is_base_of<mesa_pd::data::IAccessor, ParticleAccessor_T>::value, "Provide a valid accessor as template"); + return mesa_pd::data::particle_flags::isSet( ac.getFlags(particleIdx), mesa_pd::data::particle_flags::FIXED) && + !mesa_pd::data::particle_flags::isSet( ac.getFlags(particleIdx), mesa_pd::data::particle_flags::GLOBAL); + } +}; + +struct GlobalParticlesSelector +{ + template< typename ParticleAccessor_T > + bool inline operator()(const size_t particleIdx, const ParticleAccessor_T & ac) const + { + static_assert(std::is_base_of<mesa_pd::data::IAccessor, ParticleAccessor_T>::value, "Provide a valid accessor as template"); + return mesa_pd::data::particle_flags::isSet( ac.getFlags(particleIdx), mesa_pd::data::particle_flags::GLOBAL); + } +}; + +// Only select spheres with a Stokes number above a critical Stokes number +// note: the density here is hardcoded and thus assumes a constant density throughout all particles +// this is due to the fact that otherwise the density would have to be re-computed from the inverse mass and the volume in the shape storage +// it also currently only works with spheres +// it accesses the impact velocity stored in the old contact history and thus has to be used AFTER the reduce contact history call +struct StokesNumberBasedSphereSelector +{ + explicit StokesNumberBasedSphereSelector( real_t StCrit, real_t densityFluid, real_t densityParticle, real_t kinematicViscosity ) : + StCrit_(StCrit), densityFluid_(densityFluid), densityParticle_(densityParticle), kinematicViscosity_(kinematicViscosity) {} + + template< typename ParticleAccessor_T > + bool inline operator()(const size_t particleIdx, const ParticleAccessor_T & ac) const + { + static_assert(std::is_base_of<mesa_pd::data::IAccessor, ParticleAccessor_T>::value, "Provide a valid accessor as template"); + + lbm_mesapd_coupling::RegularParticlesSelector regularParticlesSelector; + + if(regularParticlesSelector(particleIdx, ac)) + { + // check for non-fixed spheres + if( ac.getShape(particleIdx)->getShapeType() == mesa_pd::data::Sphere::SHAPE_TYPE ) + { + auto contactHistories = ac.getOldContactHistory(particleIdx); + + if(contactHistories.empty()) return true; // no active contacts + + // find maximum impact velocity among all active contacts + real_t maximumImpactVelocity = real_t(0); + for( auto & history : contactHistories ) + { + maximumImpactVelocity = std::max(maximumImpactVelocity, history.second.getImpactVelocityMagnitude()); + } + + auto sphereShape = *static_cast< mesa_pd::data::Sphere*>(ac.getShape(particleIdx)); + real_t diameter = real_t(2) * sphereShape.getRadius(); + + real_t St = (densityParticle_ / densityFluid_) * (maximumImpactVelocity * diameter / kinematicViscosity_) / real_t(9); + + if( St < StCrit_) return true; + } + } + + return false; + } + +private: + real_t StCrit_; + real_t densityFluid_; + real_t densityParticle_; + real_t kinematicViscosity_; +}; + +} // namespace lbm_mesapd_coupling +} // namespace walberla diff --git a/src/lbm_mesapd_coupling/utility/ResetHydrodynamicForceTorqueKernel.h b/src/lbm_mesapd_coupling/utility/ResetHydrodynamicForceTorqueKernel.h new file mode 100644 index 0000000000000000000000000000000000000000..1e7dd22c42e296f9f98e694def5a457480764683 --- /dev/null +++ b/src/lbm_mesapd_coupling/utility/ResetHydrodynamicForceTorqueKernel.h @@ -0,0 +1,54 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla is free software: you can +// redistribute it and/or modify it under the terms of the GNU General Public +// License as published by the Free Software Foundation, either version 3 of +// the License, or (at your option) any later version. +// +// waLBerla is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +// for more details. +// +// You should have received a copy of the GNU General Public License along +// with waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file ResetHydrodynamicForceTorqueKernel.h +//! \ingroup lbm_mesapd_coupling +//! \author Christoph Rettinger <christoph.rettinger@fau.de> +// +//====================================================================================================================== + +#pragma once + +#include "core/math/Vector3.h" + +#include "mesa_pd/data/IAccessor.h" + +namespace walberla { +namespace lbm_mesapd_coupling { + +/* + * Kernel that resets the values of hydrodynamicForce and hydrodynamicTorque currently stored to zero + * + * Should usually be carried out on local and ghost particles. + */ +class ResetHydrodynamicForceTorqueKernel +{ + +public: + + ResetHydrodynamicForceTorqueKernel() = default; + + template< typename ParticleAccessor_T> + void operator()(const size_t idx, ParticleAccessor_T& ac) const + { + static_assert(std::is_base_of<mesa_pd::data::IAccessor, ParticleAccessor_T>::value, "Provide a valid accessor as template"); + + ac.setHydrodynamicForce(idx, Vector3<real_t>(real_t(0))); + ac.setHydrodynamicTorque(idx, Vector3<real_t>(real_t(0))); + } +}; + +} // namespace lbm_mesapd_coupling +} // namespace walberla diff --git a/src/mesa_pd/common/ParticleFunctions.h b/src/mesa_pd/common/ParticleFunctions.h index 195dec2c7718535a3d71fd50e5ef2928830b2e13..d19045ab05c4cce40858672b5572188a337c8a5c 100644 --- a/src/mesa_pd/common/ParticleFunctions.h +++ b/src/mesa_pd/common/ParticleFunctions.h @@ -92,5 +92,27 @@ inline void addForceAtWFPosAtomic(const size_t p_idx, Accessor& ac, const Vec3& ac.getTorqueRef(p_idx)[2] += t[2]; } +/** + * Torque is directly applied on the particle. + */ +template <typename Accessor> +inline void addTorqueAtomic(const size_t p_idx, Accessor& ac, const Vec3& t) +{ + // Increasing the torque on this particle +#ifdef _OPENMP +#pragma omp atomic +#endif + ac.getTorqueRef(p_idx)[0] += t[0]; +#ifdef _OPENMP +#pragma omp atomic +#endif + ac.getTorqueRef(p_idx)[1] += t[1]; +#ifdef _OPENMP +#pragma omp atomic +#endif + ac.getTorqueRef(p_idx)[2] += t[2]; +} + + } //namespace mesa_pd } //namespace walberla diff --git a/src/mesa_pd/data/ParticleAccessor.h b/src/mesa_pd/data/ParticleAccessor.h index f71f39af2a0fa421d4965f3ec0e5f14f0d07ebfd..ebb81768e28487d7c32bdddcba4a8863a6c79902 100644 --- a/src/mesa_pd/data/ParticleAccessor.h +++ b/src/mesa_pd/data/ParticleAccessor.h @@ -144,6 +144,22 @@ public: walberla::mesa_pd::Vec3& getDwRef(const size_t p_idx) {return ps_->getDwRef(p_idx);} void setDw(const size_t p_idx, walberla::mesa_pd::Vec3 const & v) { ps_->setDw(p_idx, v);} + walberla::mesa_pd::Vec3 const & getHydrodynamicForce(const size_t p_idx) const {return ps_->getHydrodynamicForce(p_idx);} + walberla::mesa_pd::Vec3& getHydrodynamicForceRef(const size_t p_idx) {return ps_->getHydrodynamicForceRef(p_idx);} + void setHydrodynamicForce(const size_t p_idx, walberla::mesa_pd::Vec3 const & v) { ps_->setHydrodynamicForce(p_idx, v);} + + walberla::mesa_pd::Vec3 const & getHydrodynamicTorque(const size_t p_idx) const {return ps_->getHydrodynamicTorque(p_idx);} + walberla::mesa_pd::Vec3& getHydrodynamicTorqueRef(const size_t p_idx) {return ps_->getHydrodynamicTorqueRef(p_idx);} + void setHydrodynamicTorque(const size_t p_idx, walberla::mesa_pd::Vec3 const & v) { ps_->setHydrodynamicTorque(p_idx, v);} + + walberla::mesa_pd::Vec3 const & getOldHydrodynamicForce(const size_t p_idx) const {return ps_->getOldHydrodynamicForce(p_idx);} + walberla::mesa_pd::Vec3& getOldHydrodynamicForceRef(const size_t p_idx) {return ps_->getOldHydrodynamicForceRef(p_idx);} + void setOldHydrodynamicForce(const size_t p_idx, walberla::mesa_pd::Vec3 const & v) { ps_->setOldHydrodynamicForce(p_idx, v);} + + walberla::mesa_pd::Vec3 const & getOldHydrodynamicTorque(const size_t p_idx) const {return ps_->getOldHydrodynamicTorque(p_idx);} + walberla::mesa_pd::Vec3& getOldHydrodynamicTorqueRef(const size_t p_idx) {return ps_->getOldHydrodynamicTorqueRef(p_idx);} + void setOldHydrodynamicTorque(const size_t p_idx, walberla::mesa_pd::Vec3 const & v) { ps_->setOldHydrodynamicTorque(p_idx, v);} + std::unordered_set<walberla::mpi::MPIRank> const & getNeighborState(const size_t p_idx) const {return ps_->getNeighborState(p_idx);} std::unordered_set<walberla::mpi::MPIRank>& getNeighborStateRef(const size_t p_idx) {return ps_->getNeighborStateRef(p_idx);} void setNeighborState(const size_t p_idx, std::unordered_set<walberla::mpi::MPIRank> const & v) { ps_->setNeighborState(p_idx, v);} @@ -289,6 +305,22 @@ public: void setDw(const size_t /*p_idx*/, walberla::mesa_pd::Vec3 const & v) { dw_ = v;} walberla::mesa_pd::Vec3& getDwRef(const size_t /*p_idx*/) {return dw_;} + walberla::mesa_pd::Vec3 const & getHydrodynamicForce(const size_t /*p_idx*/) const {return hydrodynamicForce_;} + void setHydrodynamicForce(const size_t /*p_idx*/, walberla::mesa_pd::Vec3 const & v) { hydrodynamicForce_ = v;} + walberla::mesa_pd::Vec3& getHydrodynamicForceRef(const size_t /*p_idx*/) {return hydrodynamicForce_;} + + walberla::mesa_pd::Vec3 const & getHydrodynamicTorque(const size_t /*p_idx*/) const {return hydrodynamicTorque_;} + void setHydrodynamicTorque(const size_t /*p_idx*/, walberla::mesa_pd::Vec3 const & v) { hydrodynamicTorque_ = v;} + walberla::mesa_pd::Vec3& getHydrodynamicTorqueRef(const size_t /*p_idx*/) {return hydrodynamicTorque_;} + + walberla::mesa_pd::Vec3 const & getOldHydrodynamicForce(const size_t /*p_idx*/) const {return oldHydrodynamicForce_;} + void setOldHydrodynamicForce(const size_t /*p_idx*/, walberla::mesa_pd::Vec3 const & v) { oldHydrodynamicForce_ = v;} + walberla::mesa_pd::Vec3& getOldHydrodynamicForceRef(const size_t /*p_idx*/) {return oldHydrodynamicForce_;} + + walberla::mesa_pd::Vec3 const & getOldHydrodynamicTorque(const size_t /*p_idx*/) const {return oldHydrodynamicTorque_;} + void setOldHydrodynamicTorque(const size_t /*p_idx*/, walberla::mesa_pd::Vec3 const & v) { oldHydrodynamicTorque_ = v;} + walberla::mesa_pd::Vec3& getOldHydrodynamicTorqueRef(const size_t /*p_idx*/) {return oldHydrodynamicTorque_;} + std::unordered_set<walberla::mpi::MPIRank> const & getNeighborState(const size_t /*p_idx*/) const {return neighborState_;} void setNeighborState(const size_t /*p_idx*/, std::unordered_set<walberla::mpi::MPIRank> const & v) { neighborState_ = v;} std::unordered_set<walberla::mpi::MPIRank>& getNeighborStateRef(const size_t /*p_idx*/) {return neighborState_;} @@ -328,6 +360,10 @@ private: walberla::real_t heatFlux_; walberla::mesa_pd::Vec3 dv_; walberla::mesa_pd::Vec3 dw_; + walberla::mesa_pd::Vec3 hydrodynamicForce_; + walberla::mesa_pd::Vec3 hydrodynamicTorque_; + walberla::mesa_pd::Vec3 oldHydrodynamicForce_; + walberla::mesa_pd::Vec3 oldHydrodynamicTorque_; std::unordered_set<walberla::mpi::MPIRank> neighborState_; }; diff --git a/src/mesa_pd/data/ParticleStorage.h b/src/mesa_pd/data/ParticleStorage.h index 7ff16c341846c3169f16fdcee9a75b96a704b7fc..53c157277713dcb3ba503cb85bd9cf893617508f 100644 --- a/src/mesa_pd/data/ParticleStorage.h +++ b/src/mesa_pd/data/ParticleStorage.h @@ -95,6 +95,10 @@ public: using heatFlux_type = walberla::real_t; using dv_type = walberla::mesa_pd::Vec3; using dw_type = walberla::mesa_pd::Vec3; + using hydrodynamicForce_type = walberla::mesa_pd::Vec3; + using hydrodynamicTorque_type = walberla::mesa_pd::Vec3; + using oldHydrodynamicForce_type = walberla::mesa_pd::Vec3; + using oldHydrodynamicTorque_type = walberla::mesa_pd::Vec3; using neighborState_type = std::unordered_set<walberla::mpi::MPIRank>; @@ -194,6 +198,22 @@ public: dw_type& getDwRef() {return storage_.getDwRef(i_);} void setDw(dw_type const & v) { storage_.setDw(i_, v);} + hydrodynamicForce_type const & getHydrodynamicForce() const {return storage_.getHydrodynamicForce(i_);} + hydrodynamicForce_type& getHydrodynamicForceRef() {return storage_.getHydrodynamicForceRef(i_);} + void setHydrodynamicForce(hydrodynamicForce_type const & v) { storage_.setHydrodynamicForce(i_, v);} + + hydrodynamicTorque_type const & getHydrodynamicTorque() const {return storage_.getHydrodynamicTorque(i_);} + hydrodynamicTorque_type& getHydrodynamicTorqueRef() {return storage_.getHydrodynamicTorqueRef(i_);} + void setHydrodynamicTorque(hydrodynamicTorque_type const & v) { storage_.setHydrodynamicTorque(i_, v);} + + oldHydrodynamicForce_type const & getOldHydrodynamicForce() const {return storage_.getOldHydrodynamicForce(i_);} + oldHydrodynamicForce_type& getOldHydrodynamicForceRef() {return storage_.getOldHydrodynamicForceRef(i_);} + void setOldHydrodynamicForce(oldHydrodynamicForce_type const & v) { storage_.setOldHydrodynamicForce(i_, v);} + + oldHydrodynamicTorque_type const & getOldHydrodynamicTorque() const {return storage_.getOldHydrodynamicTorque(i_);} + oldHydrodynamicTorque_type& getOldHydrodynamicTorqueRef() {return storage_.getOldHydrodynamicTorqueRef(i_);} + void setOldHydrodynamicTorque(oldHydrodynamicTorque_type const & v) { storage_.setOldHydrodynamicTorque(i_, v);} + neighborState_type const & getNeighborState() const {return storage_.getNeighborState(i_);} neighborState_type& getNeighborStateRef() {return storage_.getNeighborStateRef(i_);} void setNeighborState(neighborState_type const & v) { storage_.setNeighborState(i_, v);} @@ -282,6 +302,10 @@ public: using heatFlux_type = walberla::real_t; using dv_type = walberla::mesa_pd::Vec3; using dw_type = walberla::mesa_pd::Vec3; + using hydrodynamicForce_type = walberla::mesa_pd::Vec3; + using hydrodynamicTorque_type = walberla::mesa_pd::Vec3; + using oldHydrodynamicForce_type = walberla::mesa_pd::Vec3; + using oldHydrodynamicTorque_type = walberla::mesa_pd::Vec3; using neighborState_type = std::unordered_set<walberla::mpi::MPIRank>; @@ -381,6 +405,22 @@ public: dw_type& getDwRef(const size_t idx) {return dw_[idx];} void setDw(const size_t idx, dw_type const & v) { dw_[idx] = v; } + hydrodynamicForce_type const & getHydrodynamicForce(const size_t idx) const {return hydrodynamicForce_[idx];} + hydrodynamicForce_type& getHydrodynamicForceRef(const size_t idx) {return hydrodynamicForce_[idx];} + void setHydrodynamicForce(const size_t idx, hydrodynamicForce_type const & v) { hydrodynamicForce_[idx] = v; } + + hydrodynamicTorque_type const & getHydrodynamicTorque(const size_t idx) const {return hydrodynamicTorque_[idx];} + hydrodynamicTorque_type& getHydrodynamicTorqueRef(const size_t idx) {return hydrodynamicTorque_[idx];} + void setHydrodynamicTorque(const size_t idx, hydrodynamicTorque_type const & v) { hydrodynamicTorque_[idx] = v; } + + oldHydrodynamicForce_type const & getOldHydrodynamicForce(const size_t idx) const {return oldHydrodynamicForce_[idx];} + oldHydrodynamicForce_type& getOldHydrodynamicForceRef(const size_t idx) {return oldHydrodynamicForce_[idx];} + void setOldHydrodynamicForce(const size_t idx, oldHydrodynamicForce_type const & v) { oldHydrodynamicForce_[idx] = v; } + + oldHydrodynamicTorque_type const & getOldHydrodynamicTorque(const size_t idx) const {return oldHydrodynamicTorque_[idx];} + oldHydrodynamicTorque_type& getOldHydrodynamicTorqueRef(const size_t idx) {return oldHydrodynamicTorque_[idx];} + void setOldHydrodynamicTorque(const size_t idx, oldHydrodynamicTorque_type const & v) { oldHydrodynamicTorque_[idx] = v; } + neighborState_type const & getNeighborState(const size_t idx) const {return neighborState_[idx];} neighborState_type& getNeighborStateRef(const size_t idx) {return neighborState_[idx];} void setNeighborState(const size_t idx, neighborState_type const & v) { neighborState_[idx] = v; } @@ -500,6 +540,10 @@ public: std::vector<heatFlux_type> heatFlux_ {}; std::vector<dv_type> dv_ {}; std::vector<dw_type> dw_ {}; + std::vector<hydrodynamicForce_type> hydrodynamicForce_ {}; + std::vector<hydrodynamicTorque_type> hydrodynamicTorque_ {}; + std::vector<oldHydrodynamicForce_type> oldHydrodynamicForce_ {}; + std::vector<oldHydrodynamicTorque_type> oldHydrodynamicTorque_ {}; std::vector<neighborState_type> neighborState_ {}; std::unordered_map<uid_type, size_t> uidToIdx_; static_assert(std::is_same<uid_type, id_t>::value, @@ -534,6 +578,10 @@ ParticleStorage::Particle& ParticleStorage::Particle::operator=(const ParticleSt getHeatFluxRef() = rhs.getHeatFlux(); getDvRef() = rhs.getDv(); getDwRef() = rhs.getDw(); + getHydrodynamicForceRef() = rhs.getHydrodynamicForce(); + getHydrodynamicTorqueRef() = rhs.getHydrodynamicTorque(); + getOldHydrodynamicForceRef() = rhs.getOldHydrodynamicForce(); + getOldHydrodynamicTorqueRef() = rhs.getOldHydrodynamicTorque(); getNeighborStateRef() = rhs.getNeighborState(); return *this; } @@ -565,6 +613,10 @@ ParticleStorage::Particle& ParticleStorage::Particle::operator=(ParticleStorage: getHeatFluxRef() = std::move(rhs.getHeatFluxRef()); getDvRef() = std::move(rhs.getDvRef()); getDwRef() = std::move(rhs.getDwRef()); + getHydrodynamicForceRef() = std::move(rhs.getHydrodynamicForceRef()); + getHydrodynamicTorqueRef() = std::move(rhs.getHydrodynamicTorqueRef()); + getOldHydrodynamicForceRef() = std::move(rhs.getOldHydrodynamicForceRef()); + getOldHydrodynamicTorqueRef() = std::move(rhs.getOldHydrodynamicTorqueRef()); getNeighborStateRef() = std::move(rhs.getNeighborStateRef()); return *this; } @@ -597,6 +649,10 @@ void swap(ParticleStorage::Particle lhs, ParticleStorage::Particle rhs) std::swap(lhs.getHeatFluxRef(), rhs.getHeatFluxRef()); std::swap(lhs.getDvRef(), rhs.getDvRef()); std::swap(lhs.getDwRef(), rhs.getDwRef()); + std::swap(lhs.getHydrodynamicForceRef(), rhs.getHydrodynamicForceRef()); + std::swap(lhs.getHydrodynamicTorqueRef(), rhs.getHydrodynamicTorqueRef()); + std::swap(lhs.getOldHydrodynamicForceRef(), rhs.getOldHydrodynamicForceRef()); + std::swap(lhs.getOldHydrodynamicTorqueRef(), rhs.getOldHydrodynamicTorqueRef()); std::swap(lhs.getNeighborStateRef(), rhs.getNeighborStateRef()); } @@ -629,6 +685,10 @@ std::ostream& operator<<( std::ostream& os, const ParticleStorage::Particle& p ) "heatFlux : " << p.getHeatFlux() << "\n" << "dv : " << p.getDv() << "\n" << "dw : " << p.getDw() << "\n" << + "hydrodynamicForce : " << p.getHydrodynamicForce() << "\n" << + "hydrodynamicTorque : " << p.getHydrodynamicTorque() << "\n" << + "oldHydrodynamicForce: " << p.getOldHydrodynamicForce() << "\n" << + "oldHydrodynamicTorque: " << p.getOldHydrodynamicTorque() << "\n" << "neighborState : " << p.getNeighborState() << "\n" << "================================" << std::endl; return os; @@ -731,6 +791,10 @@ inline ParticleStorage::iterator ParticleStorage::create(const id_t& uid) heatFlux_.emplace_back(real_t(0)); dv_.emplace_back(real_t(0)); dw_.emplace_back(real_t(0)); + hydrodynamicForce_.emplace_back(real_t(0)); + hydrodynamicTorque_.emplace_back(real_t(0)); + oldHydrodynamicForce_.emplace_back(real_t(0)); + oldHydrodynamicTorque_.emplace_back(real_t(0)); neighborState_.emplace_back(); uid_.back() = uid; uidToIdx_[uid] = uid_.size() - 1; @@ -788,6 +852,10 @@ inline ParticleStorage::iterator ParticleStorage::erase(iterator& it) heatFlux_.pop_back(); dv_.pop_back(); dw_.pop_back(); + hydrodynamicForce_.pop_back(); + hydrodynamicTorque_.pop_back(); + oldHydrodynamicForce_.pop_back(); + oldHydrodynamicTorque_.pop_back(); neighborState_.pop_back(); return it; } @@ -832,6 +900,10 @@ inline void ParticleStorage::reserve(const size_t size) heatFlux_.reserve(size); dv_.reserve(size); dw_.reserve(size); + hydrodynamicForce_.reserve(size); + hydrodynamicTorque_.reserve(size); + oldHydrodynamicForce_.reserve(size); + oldHydrodynamicTorque_.reserve(size); neighborState_.reserve(size); } @@ -861,6 +933,10 @@ inline void ParticleStorage::clear() heatFlux_.clear(); dv_.clear(); dw_.clear(); + hydrodynamicForce_.clear(); + hydrodynamicTorque_.clear(); + oldHydrodynamicForce_.clear(); + oldHydrodynamicTorque_.clear(); neighborState_.clear(); uidToIdx_.clear(); } @@ -891,6 +967,10 @@ inline size_t ParticleStorage::size() const //WALBERLA_ASSERT_EQUAL( uid_.size(), heatFlux.size() ); //WALBERLA_ASSERT_EQUAL( uid_.size(), dv.size() ); //WALBERLA_ASSERT_EQUAL( uid_.size(), dw.size() ); + //WALBERLA_ASSERT_EQUAL( uid_.size(), hydrodynamicForce.size() ); + //WALBERLA_ASSERT_EQUAL( uid_.size(), hydrodynamicTorque.size() ); + //WALBERLA_ASSERT_EQUAL( uid_.size(), oldHydrodynamicForce.size() ); + //WALBERLA_ASSERT_EQUAL( uid_.size(), oldHydrodynamicTorque.size() ); //WALBERLA_ASSERT_EQUAL( uid_.size(), neighborState.size() ); return uid_.size(); } @@ -1292,6 +1372,42 @@ public: walberla::mesa_pd::Vec3 const & operator()(const data::Particle& p) const {return p.getDw();} }; ///Predicate that selects a certain property from a Particle +class SelectParticleHydrodynamicForce +{ +public: + using return_type = walberla::mesa_pd::Vec3; + walberla::mesa_pd::Vec3& operator()(data::Particle& p) const {return p.getHydrodynamicForceRef();} + walberla::mesa_pd::Vec3& operator()(data::Particle&& p) const {return p.getHydrodynamicForceRef();} + walberla::mesa_pd::Vec3 const & operator()(const data::Particle& p) const {return p.getHydrodynamicForce();} +}; +///Predicate that selects a certain property from a Particle +class SelectParticleHydrodynamicTorque +{ +public: + using return_type = walberla::mesa_pd::Vec3; + walberla::mesa_pd::Vec3& operator()(data::Particle& p) const {return p.getHydrodynamicTorqueRef();} + walberla::mesa_pd::Vec3& operator()(data::Particle&& p) const {return p.getHydrodynamicTorqueRef();} + walberla::mesa_pd::Vec3 const & operator()(const data::Particle& p) const {return p.getHydrodynamicTorque();} +}; +///Predicate that selects a certain property from a Particle +class SelectParticleOldHydrodynamicForce +{ +public: + using return_type = walberla::mesa_pd::Vec3; + walberla::mesa_pd::Vec3& operator()(data::Particle& p) const {return p.getOldHydrodynamicForceRef();} + walberla::mesa_pd::Vec3& operator()(data::Particle&& p) const {return p.getOldHydrodynamicForceRef();} + walberla::mesa_pd::Vec3 const & operator()(const data::Particle& p) const {return p.getOldHydrodynamicForce();} +}; +///Predicate that selects a certain property from a Particle +class SelectParticleOldHydrodynamicTorque +{ +public: + using return_type = walberla::mesa_pd::Vec3; + walberla::mesa_pd::Vec3& operator()(data::Particle& p) const {return p.getOldHydrodynamicTorqueRef();} + walberla::mesa_pd::Vec3& operator()(data::Particle&& p) const {return p.getOldHydrodynamicTorqueRef();} + walberla::mesa_pd::Vec3 const & operator()(const data::Particle& p) const {return p.getOldHydrodynamicTorque();} +}; +///Predicate that selects a certain property from a Particle class SelectParticleNeighborState { public: diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ef1f0177a9c58db2092d9b712df077323d9a030b..b7032214d314cae434db0a84da4eace9bdf30c52 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -14,6 +14,7 @@ add_subdirectory( gather ) add_subdirectory( geometry ) add_subdirectory( gui ) add_subdirectory( lbm ) +add_subdirectory( lbm_mesapd_coupling ) add_subdirectory( mesa_pd ) add_subdirectory( mesh ) add_subdirectory( pde ) diff --git a/tests/lbm_mesapd_coupling/CMakeLists.txt b/tests/lbm_mesapd_coupling/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..40a818addf0e4ae264fcbb1d1dd1ebc4bb5bf150 --- /dev/null +++ b/tests/lbm_mesapd_coupling/CMakeLists.txt @@ -0,0 +1,40 @@ +################################################################################################### +# +# Tests for LBM_MESAPD_COUPLING functionality +# +################################################################################################### + +waLBerla_compile_test( NAME LBM_MESAPD_COUPLING_Mapping FILES mapping/ParticleMapping.cpp DEPENDS core mesa_pd lbm lbm_mesapd_coupling domain_decomposition field ) +waLBerla_execute_test( NAME LBM_MESAPD_COUPLING_Mapping PROCESSES 3) + +waLBerla_compile_test( NAME LBM_MESAPD_COUPLING_MEM_MovingMapping FILES momentum_exchange_method/MovingParticleMapping.cpp DEPENDS core mesa_pd lbm lbm_mesapd_coupling domain_decomposition field vtk) +waLBerla_execute_test( NAME LBM_MESAPD_COUPLING_MEM_MovingMapping PROCESSES 3) + +waLBerla_compile_test( NAME LBM_MESAPD_COUPLING_MEM_DragForceSphere FILES momentum_exchange_method/DragForceSphere.cpp DEPENDS core mesa_pd lbm lbm_mesapd_coupling domain_decomposition field vtk ) +waLBerla_execute_test( NAME LBM_MESAPD_COUPLING_MEM_DragForceSphere COMMAND $<TARGET_FILE:LBM_MESAPD_COUPLING_MEM_DragForceSphere> --funcTest PROCESSES 2) + +waLBerla_compile_test( NAME LBM_MESAPD_COUPLING_MEM_ForceBetweenTwoStationaryObjects FILES momentum_exchange_method/ForceBetweenTwoStationaryObjects.cpp DEPENDS core mesa_pd lbm lbm_mesapd_coupling domain_decomposition field vtk ) +waLBerla_execute_test( NAME LBM_MESAPD_COUPLING_MEM_ForceBetweenTwoStationaryObjectsSS1 COMMAND $<TARGET_FILE:LBM_MESAPD_COUPLING_MEM_ForceBetweenTwoStationaryObjects> PROCESSES 1) +waLBerla_execute_test( NAME LBM_MESAPD_COUPLING_MEM_ForceBetweenTwoStationaryObjectsSS2 COMMAND $<TARGET_FILE:LBM_MESAPD_COUPLING_MEM_ForceBetweenTwoStationaryObjects> --useSBB PROCESSES 1) +waLBerla_execute_test( NAME LBM_MESAPD_COUPLING_MEM_ForceBetweenTwoStationaryObjectsSS3 COMMAND $<TARGET_FILE:LBM_MESAPD_COUPLING_MEM_ForceBetweenTwoStationaryObjects> --useCompressible PROCESSES 1) +waLBerla_execute_test( NAME LBM_MESAPD_COUPLING_MEM_ForceBetweenTwoStationaryObjectsSS4 COMMAND $<TARGET_FILE:LBM_MESAPD_COUPLING_MEM_ForceBetweenTwoStationaryObjects> --systemVelocity 0.1 PROCESSES 1) +waLBerla_execute_test( NAME LBM_MESAPD_COUPLING_MEM_ForceBetweenTwoStationaryObjectsSW1 COMMAND $<TARGET_FILE:LBM_MESAPD_COUPLING_MEM_ForceBetweenTwoStationaryObjects> --useSphereWallSetup PROCESSES 1) +waLBerla_execute_test( NAME LBM_MESAPD_COUPLING_MEM_ForceBetweenTwoStationaryObjectsSW2 COMMAND $<TARGET_FILE:LBM_MESAPD_COUPLING_MEM_ForceBetweenTwoStationaryObjects> --useSphereWallSetup --useSBB PROCESSES 1) +waLBerla_execute_test( NAME LBM_MESAPD_COUPLING_MEM_ForceBetweenTwoStationaryObjectsSW3 COMMAND $<TARGET_FILE:LBM_MESAPD_COUPLING_MEM_ForceBetweenTwoStationaryObjects> --useSphereWallSetup --useCompressible PROCESSES 1) +waLBerla_execute_test( NAME LBM_MESAPD_COUPLING_MEM_ForceBetweenTwoStationaryObjectsSW4 COMMAND $<TARGET_FILE:LBM_MESAPD_COUPLING_MEM_ForceBetweenTwoStationaryObjects> --useSphereWallSetup --systemVelocity 0.1 PROCESSES 1) + +waLBerla_compile_test( NAME LBM_MESAPD_COUPLING_MEM_SettlingSphere FILES momentum_exchange_method/SettlingSphere.cpp DEPENDS core mesa_pd lbm lbm_mesapd_coupling domain_decomposition field vtk ) +waLBerla_execute_test( NAME LBM_MESAPD_COUPLING_MEM_SettlingSphere COMMAND $<TARGET_FILE:LBM_MESAPD_COUPLING_MEM_SettlingSphere> --funcTest PROCESSES 4) + +waLBerla_compile_test( NAME LBM_MESAPD_COUPLING_MEM_PdfReconstruction FILES momentum_exchange_method/PdfReconstruction.cpp DEPENDS core mesa_pd lbm lbm_mesapd_coupling domain_decomposition field vtk) +waLBerla_execute_test( NAME LBM_MESAPD_COUPLING_MEM_PdfReconstruction PROCESSES 2) + +waLBerla_compile_test( NAME LBM_MESAPD_COUPLING_MEM_UpdateParticleMapping FILES momentum_exchange_method/UpdateParticleMapping.cpp DEPENDS core mesa_pd lbm lbm_mesapd_coupling domain_decomposition field vtk) +waLBerla_execute_test( NAME LBM_MESAPD_COUPLING_MEM_UpdateParticleMapping PROCESSES 1) + +waLBerla_compile_test( NAME LBM_MESAPD_COUPLING_UTILITY_LubricationCorrection FILES utility/LubricationCorrection.cpp DEPENDS mesa_pd lbm_mesapd_coupling ) +waLBerla_execute_test( NAME LBM_MESAPD_COUPLING_UTILITY_LubricationCorrection PROCESSES 1 ) + +waLBerla_compile_test( NAME LBM_MESAPD_COUPLING_UTILITY_InspectionProbe FILES utility/InspectionProbe.cpp DEPENDS core mesa_pd lbm lbm_mesapd_coupling domain_decomposition field ) +waLBerla_execute_test( NAME LBM_MESAPD_COUPLING_UTILITY_InspectionProbe PROCESSES 1 ) + diff --git a/tests/lbm_mesapd_coupling/mapping/ParticleMapping.cpp b/tests/lbm_mesapd_coupling/mapping/ParticleMapping.cpp new file mode 100644 index 0000000000000000000000000000000000000000..b879418b6c7a914890fd49b7a99cf8134b992041 --- /dev/null +++ b/tests/lbm_mesapd_coupling/mapping/ParticleMapping.cpp @@ -0,0 +1,849 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla is free software: you can +// redistribute it and/or modify it under the terms of the GNU General Public +// License as published by the Free Software Foundation, either version 3 of +// the License, or (at your option) any later version. +// +// waLBerla is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +// for more details. +// +// You should have received a copy of the GNU General Public License along +// with waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file ParticleMapping.cpp +//! \ingroup lbm_mesapd_coupling +//! \author Christoph Rettinger <christoph.rettinger@fau.de> +// +//====================================================================================================================== + +#include "blockforest/Initialization.h" +#include "blockforest/communication/UniformBufferedScheme.h" + +#include "boundary/all.h" + +#include "core/DataTypes.h" +#include "core/Environment.h" +#include "core/debug/Debug.h" +#include "core/debug/TestSubsystem.h" +#include "core/math/all.h" +#include "core/mpi/MPIManager.h" +#include "core/mpi/Reduce.h" + +#include "field/AddToStorage.h" + +#include "lbm/boundary/all.h" +#include "lbm/field/AddToStorage.h" +#include "lbm/field/PdfField.h" +#include "lbm/lattice_model/D3Q19.h" + +#include "lbm_mesapd_coupling/mapping/ParticleMapping.h" +#include "lbm_mesapd_coupling/utility/ParticleSelector.h" + +#include "mesa_pd/data/DataTypes.h" +#include "mesa_pd/data/ParticleAccessorWithShape.h" +#include "mesa_pd/data/ParticleStorage.h" +#include "mesa_pd/data/ShapeStorage.h" +#include "mesa_pd/domain/BlockForestDomain.h" +#include "mesa_pd/kernel/ParticleSelector.h" +#include "mesa_pd/kernel/SingleCast.h" +#include "mesa_pd/mpi/SyncNextNeighbors.h" + +#include "vtk/all.h" +#include "field/vtk/all.h" + +namespace particle_mapping +{ + +/////////// +// USING // +/////////// + +using namespace walberla; +using walberla::uint_t; + +using LatticeModel_T = lbm::D3Q19<lbm::collision_model::SRT>; + +using Stencil_T = LatticeModel_T::Stencil; +using PdfField_T = lbm::PdfField<LatticeModel_T>; + +const uint_t FieldGhostLayers = 1; + +using flag_t = walberla::uint8_t; +using FlagField_T = FlagField<flag_t>; + +// boundary handling +using NoSlip_T = lbm::NoSlip< LatticeModel_T, flag_t >; +using BoundaryHandling_T = BoundaryHandling< FlagField_T, Stencil_T, NoSlip_T >; + +/////////// +// FLAGS // +/////////// + +const FlagUID Fluid_Flag ( "fluid" ); +const FlagUID NoSlip_Flag ( "no slip" ); + + +///////////////////////////////////// +// BOUNDARY HANDLING CUSTOMIZATION // +///////////////////////////////////// + +class MyBoundaryHandling +{ +public: + + MyBoundaryHandling( const BlockDataID & flagFieldID, const BlockDataID & pdfFieldID) : + flagFieldID_( flagFieldID ), pdfFieldID_( pdfFieldID ) {} + + BoundaryHandling_T * operator()( IBlock* const block, const StructuredBlockStorage* const storage ) const; + +private: + + const BlockDataID flagFieldID_; + const BlockDataID pdfFieldID_; + +}; // class MyBoundaryHandling + +BoundaryHandling_T * MyBoundaryHandling::operator()( IBlock * const block, const StructuredBlockStorage * const /*storage*/ ) const +{ + WALBERLA_ASSERT_NOT_NULLPTR( block ); + //WALBERLA_ASSERT_NOT_NULLPTR( storage ); + + FlagField_T * flagField = block->getData< FlagField_T >( flagFieldID_ ); + PdfField_T * pdfField = block->getData< PdfField_T > ( pdfFieldID_ ); + + const auto fluid = flagField->flagExists( Fluid_Flag ) ? flagField->getFlag( Fluid_Flag ) : flagField->registerFlag( Fluid_Flag ); + + BoundaryHandling_T * handling = new BoundaryHandling_T( "fixed obstacle boundary handling", flagField, fluid, + NoSlip_T( "NoSlip", NoSlip_Flag, pdfField ) ); + + handling->fillWithDomain( FieldGhostLayers ); + + return handling; +} + + + +class SphereMappingChecker +{ +public: + SphereMappingChecker(const shared_ptr< StructuredBlockStorage > & blocks, + const BlockDataID & boundaryHandlingID, real_t sphereRadius) : + blocks_( blocks ), boundaryHandlingID_( boundaryHandlingID ), + sphereRadius_( sphereRadius ), sphereVolume_( math::pi * real_t(4) / real_t(3) * sphereRadius * sphereRadius * sphereRadius ) + { + WALBERLA_ASSERT(blocks->isXPeriodic()); + } + + // check the mapping in the inner domain of the block and check mapped volume against real sphere volume + void operator()(std::string & testIdentifier, const Vector3<real_t> & pos, bool periodic ) + { + uint_t cellCounter( uint_t(0) ); + for( auto blockIt = blocks_->begin(); blockIt != blocks_->end(); ++blockIt ) + { + BoundaryHandling_T * boundaryHandling = blockIt->getData< BoundaryHandling_T >( boundaryHandlingID_ ); + FlagField_T * flagField = boundaryHandling->getFlagField(); + + auto xyzSize = flagField->xyzSize(); + + for (auto cellIt : xyzSize) { + Vector3<real_t> cellCenter = blocks_->getBlockLocalCellCenter(*blockIt, cellIt); + real_t distance = (cellCenter - pos).length(); + if( periodic ) + { + Vector3<real_t> periodicOffset(blocks_->getDomain().xSize(),0,0); + // check if other (periodic) copies are closer + distance = std::min(distance, (cellCenter - (pos-periodicOffset)).length()); + distance = std::min(distance, (cellCenter - (pos+periodicOffset)).length()); + } + + if (distance < sphereRadius_) { + WALBERLA_CHECK(boundaryHandling->isBoundary(cellIt), + testIdentifier << "Invalid mapping in cell " << cellIt + << " with center at " << cellCenter + << " - expected boundary cell. Distance cell center - particle center = " + << distance << "."); + ++cellCounter; + } else { + WALBERLA_CHECK(boundaryHandling->isDomain(cellIt), + testIdentifier << "Invalid mapping in cell " << cellIt + << " with center at " << cellCenter + << " - expected domain cell. Distance cell center - particle center = " + << distance << "."); + } + } + } + + mpi::allReduceInplace(cellCounter, mpi::SUM); + + // mapped volume has to be - approximately - the same as the real volume + real_t mappedVolume = real_c(cellCounter); // dx=1 + WALBERLA_CHECK(std::fabs( mappedVolume - sphereVolume_ ) / sphereVolume_ <= real_t(0.1), + "Mapped volume " << mappedVolume << " does not fit to real sphere volume " << sphereVolume_ << "."); + } + + // checks only the mapping in the ghost layers + void checkGhostLayer(std::string & testIdentifier, const Vector3<real_t> & pos, bool periodic ) + { + for( auto blockIt = blocks_->begin(); blockIt != blocks_->end(); ++blockIt ) + { + BoundaryHandling_T * boundaryHandling = blockIt->getData< BoundaryHandling_T >( boundaryHandlingID_ ); + FlagField_T * flagField = boundaryHandling->getFlagField(); + + auto xyzSizeWGl = flagField->xyzSizeWithGhostLayer(); + + for (auto cellIt : xyzSizeWGl) { + if( flagField->isInInnerPart(cellIt) ) continue; + + Vector3<real_t> cellCenter = blocks_->getBlockLocalCellCenter( *blockIt, cellIt); + real_t distance = (cellCenter - pos).length(); + if( periodic ) + { + Vector3<real_t> periodicOffset(blocks_->getDomain().xSize(),0,0); + // check if other (periodic) copies are closer + distance = std::min(distance, (cellCenter - (pos-periodicOffset)).length()); + distance = std::min(distance, (cellCenter - (pos+periodicOffset)).length()); + } + + if( distance < sphereRadius_ ) + { + WALBERLA_CHECK( boundaryHandling->isBoundary(cellIt), + testIdentifier << ": Invalid mapping in ghost layer cell " << cellIt + << " with center at " << cellCenter + << " - expected boundary cell. Distance cell center - particle center = " + << distance << "."); + } + else + { + WALBERLA_CHECK( boundaryHandling->isDomain(cellIt), + testIdentifier << ": Invalid mapping in ghost layer cell " << cellIt + << " with center at " << cellCenter + << " - expected domain cell. Distance cell center - particle center = " + << distance << "." ); + } + } + } + } + + +private: + shared_ptr< StructuredBlockStorage > blocks_; + const BlockDataID boundaryHandlingID_; + real_t sphereRadius_, sphereVolume_; + +}; + + +class HalfSpaceMappingChecker { +public: + HalfSpaceMappingChecker(const shared_ptr<StructuredBlockStorage> &blocks, + const BlockDataID &boundaryHandlingID) : + blocks_(blocks), boundaryHandlingID_(boundaryHandlingID) { + WALBERLA_ASSERT(blocks->isXPeriodic()); + } + + // check the mapping in the inner domain of the block + void operator()(std::string & testIdentifier, const Vector3<real_t> & pos, const Vector3<real_t> & normal ) + { + for( auto blockIt = blocks_->begin(); blockIt != blocks_->end(); ++blockIt ) + { + BoundaryHandling_T * boundaryHandling = blockIt->getData< BoundaryHandling_T >( boundaryHandlingID_ ); + FlagField_T * flagField = boundaryHandling->getFlagField(); + + auto xyzSize = flagField->xyzSize(); + + for (auto cellIt : xyzSize) { + Vector3<real_t> cellCenter = blocks_->getBlockLocalCellCenter(*blockIt, cellIt); + real_t distance = (cellCenter - pos) * normal; + + if (distance <= real_t(0)) { + WALBERLA_CHECK(boundaryHandling->isBoundary(cellIt), + testIdentifier << "Invalid mapping in cell " << cellIt + << " with center at " << cellCenter + << " - expected boundary cell. Distance cell center - particle center = " + << distance << "."); + } else { + WALBERLA_CHECK(boundaryHandling->isDomain(cellIt), + testIdentifier << "Invalid mapping in cell " << cellIt + << " with center at " << cellCenter + << " - expected domain cell. Distance cell center - particle center = " + << distance << "."); + } + } + } + } + + // checks only the mapping in the ghost layers + void checkGhostLayer(std::string & testIdentifier, const Vector3<real_t> & pos, const Vector3<real_t> & normal ) + { + for( auto blockIt = blocks_->begin(); blockIt != blocks_->end(); ++blockIt ) + { + BoundaryHandling_T * boundaryHandling = blockIt->getData< BoundaryHandling_T >( boundaryHandlingID_ ); + FlagField_T * flagField = boundaryHandling->getFlagField(); + + auto xyzSizeWGl = flagField->xyzSizeWithGhostLayer(); + + for (auto cellIt : xyzSizeWGl) { + if( flagField->isInInnerPart(cellIt) ) continue; + + Vector3<real_t> cellCenter = blocks_->getBlockLocalCellCenter(*blockIt, cellIt); + real_t distance = (cellCenter - pos) * normal; + + if (distance <= real_t(0)) + { + WALBERLA_CHECK( boundaryHandling->isBoundary(cellIt), + testIdentifier << ": Invalid mapping in ghost layer cell " << cellIt + << " with center at " << cellCenter + << " - expected boundary cell. Distance cell center - particle center = " + << distance << "."); + } + else + { + WALBERLA_CHECK( boundaryHandling->isDomain(cellIt), + testIdentifier << ": Invalid mapping in ghost layer cell " << cellIt + << " with center at " << cellCenter + << " - expected domain cell. Distance cell center - particle center = " + << distance << "." ); + } + } + } + } + + +private: + shared_ptr< StructuredBlockStorage > blocks_; + const BlockDataID boundaryHandlingID_; +}; + +class MappingResetter +{ +public: + MappingResetter(const shared_ptr< StructuredBlockStorage > & blocks, const BlockDataID & boundaryHandlingID) : + blocks_( blocks ), boundaryHandlingID_( boundaryHandlingID ) + { } + + void operator()() + { + for( auto blockIt = blocks_->begin(); blockIt != blocks_->end(); ++blockIt ) + { + BoundaryHandling_T * boundaryHandling = blockIt->getData< BoundaryHandling_T >( boundaryHandlingID_ ); + FlagField_T * flagField = boundaryHandling->getFlagField(); + + auto xyzSizeWGl = flagField->xyzSizeWithGhostLayer(); + // reset to domain (fluid) + boundaryHandling->forceDomain(xyzSizeWGl); + } + } + +private: + shared_ptr< StructuredBlockStorage > blocks_; + const BlockDataID boundaryHandlingID_; +}; + + + +/*!\brief Test case for the particle mapping functionality + * + * The following mapping functions are tested for spheres: + * - RegularParticlesSelector + * - GlobalParticlesSelector + * - FixedParticlesSelector + * The mapping inside a block, at a regular block boarder and at a periodic block boarder is tested. + * Regular, global and infinite-mass spheres are tested. + * After each test, the sphere is destroyed and the mapping is erased from the datastructures. + * + * The setup is as follows (x marks the different sphere positions, # is a periodic border) + * ---------------------------------------------------------------------- + * # | | # + * # | | # + * # | | # + * # | | # + * #X X X| | # + * # | | # + * # | | # + * # | | # + * # | | # + * ---------------------------------------------------------------------- + * + * Furthermore, the mapping of a plane (halfspace) is tested which resides at the bottom of the domain. + * It is by definition global and thus uses the GlobalParticlesSelector. + * + */ +////////// +// MAIN // +////////// +int main( int argc, char **argv ) +{ + debug::enterTestMode(); + + mpi::Environment env( argc, argv ); + + WALBERLA_CHECK(mpi::MPIManager::instance()->numProcesses()==3, "Due to periodicity, three processes have to be used!"); + + /////////////////////////// + // SIMULATION PROPERTIES // + /////////////////////////// + + bool writeVTK = false; + const real_t omega = real_t(1); + const real_t dx = real_t(1); + const real_t radius = real_t(5); + + /////////////////////////// + // DATA STRUCTURES SETUP // + /////////////////////////// + + Vector3<uint_t> blocksPerDirection(uint_t(3), uint_t(1), uint_t(1)); + Vector3<uint_t> cellsPerBlock(uint_t(20), uint_t(20), uint_t(20)); + Vector3<bool> periodicity(true, false, false); + + auto blocks = blockforest::createUniformBlockGrid( blocksPerDirection[0], blocksPerDirection[1], blocksPerDirection[2], + cellsPerBlock[0], cellsPerBlock[1], cellsPerBlock[2], + dx, + 1, false, false, + periodicity[0], periodicity[1], periodicity[2], + false ); + + // create the lattice model + LatticeModel_T latticeModel = LatticeModel_T( omega ); + + // add PDF field + BlockDataID pdfFieldID = lbm::addPdfFieldToStorage< LatticeModel_T >( blocks, "pdf field (zyxf)", latticeModel, + Vector3<real_t>(real_t(0)), real_t(1), + FieldGhostLayers, field::zyxf ); + + // add flag field + BlockDataID flagFieldID = field::addFlagFieldToStorage<FlagField_T>( blocks, "flag field", FieldGhostLayers ); + + // add boundary handling + BlockDataID boundaryHandlingID = blocks->addStructuredBlockData< BoundaryHandling_T >(MyBoundaryHandling( flagFieldID, pdfFieldID), "boundary handling" ); + + // rpd setup + mesa_pd::domain::BlockForestDomain domain(blocks->getBlockForestPointer()); + + //init data structures + auto ps = std::make_shared<mesa_pd::data::ParticleStorage>(1); + auto ss = std::make_shared<mesa_pd::data::ShapeStorage>(); + using ParticleAccessor = mesa_pd::data::ParticleAccessorWithShape; + ParticleAccessor accessor(ps, ss); + auto sphereShape = ss->create<mesa_pd::data::Sphere>( radius ); + mesa_pd::mpi::SyncNextNeighbors syncNextNeighborFunc; + + // coupling + const real_t overlap = real_t( 1.5 ) * dx; + lbm_mesapd_coupling::ParticleMappingKernel<BoundaryHandling_T> particleMappingKernel(blocks, boundaryHandlingID); + + // vtk + auto flagFieldVTK = vtk::createVTKOutput_BlockData( blocks, "flag_field" ); + flagFieldVTK->addCellDataWriter( make_shared< field::VTKWriter< FlagField_T > >( flagFieldID, "FlagField" ) ); + + // testing utilities + SphereMappingChecker sphereMappingChecker(blocks, boundaryHandlingID, radius); + MappingResetter mappingResetter(blocks, boundaryHandlingID); + + // sphere positions for test scenarios + Vector3<real_t> positionInsideBlock(real_t(10), real_t(10), real_t(10)); + Vector3<real_t> positionAtBlockBoarder(real_t(19), real_t(10), real_t(10)); + Vector3<real_t> positionAtPeriodicBoarder(real_t(1), real_t(10), real_t(10)); + + ///////////////////// + // NO SLIP MAPPING // + ///////////////////// + + ////////////////////////////////// + // TEST SPHERE INSIDE ONE BLOCK // + ////////////////////////////////// + + //////////////////// + // REGULAR SPHERE // + //////////////////// + { + std::string testIdentifier("Test: regular sphere inside block with no slip mapping "); + WALBERLA_LOG_DEVEL_ON_ROOT(testIdentifier << " - started"); + + // create sphere + if (domain.isContainedInProcessSubdomain( uint_c(mpi::MPIManager::instance()->rank()), positionInsideBlock )) + { + mesa_pd::data::Particle&& p = *ps->create(); + p.setPosition(positionInsideBlock); + p.setInteractionRadius(radius); + p.setOwner(mpi::MPIManager::instance()->rank()); + p.setShapeID(sphereShape); + } + syncNextNeighborFunc(*ps, domain, overlap); + + // map + ps->forEachParticle(false, lbm_mesapd_coupling::RegularParticlesSelector(), accessor, particleMappingKernel, accessor, NoSlip_Flag ); + + if( writeVTK ) flagFieldVTK->write(); + + // check mapping + sphereMappingChecker(testIdentifier,positionInsideBlock,false); + sphereMappingChecker.checkGhostLayer(testIdentifier,positionInsideBlock,false); + mappingResetter(); + + WALBERLA_LOG_DEVEL_ON_ROOT(testIdentifier << " - ended successfully"); + + // clean up + ps->clear(); + syncNextNeighborFunc(*ps, domain, overlap); + } + + /////////////////// + // GLOBAL SPHERE // + /////////////////// + { + std::string testIdentifier("Test: global sphere inside block with no slip mapping"); + WALBERLA_LOG_DEVEL_ON_ROOT(testIdentifier << " - started"); + + // create sphere + mesa_pd::data::Particle&& p = *ps->create(true); + p.setPosition(positionInsideBlock); + p.setInteractionRadius(radius); + p.setOwner(mpi::MPIManager::instance()->rank()); + p.setShapeID(sphereShape); + + syncNextNeighborFunc(*ps, domain, overlap); + + // map + ps->forEachParticle(false, lbm_mesapd_coupling::GlobalParticlesSelector(), accessor, particleMappingKernel, accessor, NoSlip_Flag ); + + if( writeVTK ) flagFieldVTK->write(); + + // check mapping + sphereMappingChecker(testIdentifier,positionInsideBlock,false); + sphereMappingChecker.checkGhostLayer(testIdentifier,positionInsideBlock,false); + mappingResetter(); + + WALBERLA_LOG_DEVEL_ON_ROOT(testIdentifier << " - ended successfully"); + + // clean up + ps->clear(); + syncNextNeighborFunc(*ps, domain, overlap); + } + + ////////////////// + // FIXED SPHERE // + ////////////////// + { + std::string testIdentifier("Test: fixed sphere inside block with no slip mapping "); + WALBERLA_LOG_DEVEL_ON_ROOT(testIdentifier << " - started"); + + // create sphere + if (domain.isContainedInProcessSubdomain( uint_c(mpi::MPIManager::instance()->rank()), positionInsideBlock )) + { + mesa_pd::data::Particle&& p = *ps->create(); + p.setPosition(positionInsideBlock); + p.setInteractionRadius(radius); + p.setOwner(mpi::MPIManager::instance()->rank()); + p.setShapeID(sphereShape); + mesa_pd::data::particle_flags::set(p.getFlagsRef(), mesa_pd::data::particle_flags::FIXED); + } + syncNextNeighborFunc(*ps, domain, overlap); + + // map + ps->forEachParticle(false, lbm_mesapd_coupling::FixedParticlesSelector(), accessor, particleMappingKernel, accessor, NoSlip_Flag ); + + if( writeVTK ) flagFieldVTK->write(); + + // check mapping + sphereMappingChecker(testIdentifier,positionInsideBlock,false); + sphereMappingChecker.checkGhostLayer(testIdentifier,positionInsideBlock,false); + mappingResetter(); + + WALBERLA_LOG_DEVEL_ON_ROOT(testIdentifier << " - ended successfully"); + + // clean up + ps->clear(); + syncNextNeighborFunc(*ps, domain, overlap); + } + + ////////////////////////////////// + // TEST SPHERE AT BLOCK BOARDER // + ////////////////////////////////// + + //////////////////// + // REGULAR SPHERE // + //////////////////// + { + std::string testIdentifier("Test: regular sphere at block boarder with no slip mapping "); + WALBERLA_LOG_DEVEL_ON_ROOT(testIdentifier << " - started"); + + // create sphere + if (domain.isContainedInProcessSubdomain( uint_c(mpi::MPIManager::instance()->rank()), positionAtBlockBoarder )) + { + mesa_pd::data::Particle&& p = *ps->create(); + p.setPosition(positionAtBlockBoarder); + p.setInteractionRadius(radius); + p.setOwner(mpi::MPIManager::instance()->rank()); + p.setShapeID(sphereShape); + } + syncNextNeighborFunc(*ps, domain, overlap); + + // map + ps->forEachParticle(false, lbm_mesapd_coupling::RegularParticlesSelector(), accessor, particleMappingKernel, accessor, NoSlip_Flag ); + + if( writeVTK ) flagFieldVTK->write(); + + // check mapping + sphereMappingChecker(testIdentifier,positionAtBlockBoarder,false); + sphereMappingChecker.checkGhostLayer(testIdentifier,positionAtBlockBoarder,false); + mappingResetter(); + + WALBERLA_LOG_DEVEL_ON_ROOT(testIdentifier << " - ended successfully"); + + // clean up + ps->clear(); + syncNextNeighborFunc(*ps, domain, overlap); + } + + /////////////////// + // GLOBAL SPHERE // + /////////////////// + + { + std::string testIdentifier("Test: global sphere at block boarder with no slip mapping "); + WALBERLA_LOG_DEVEL_ON_ROOT(testIdentifier << " - started"); + + // create sphere + mesa_pd::data::Particle&& p = *ps->create(true); + p.setPosition(positionAtBlockBoarder); + p.setInteractionRadius(radius); + p.setOwner(mpi::MPIManager::instance()->rank()); + p.setShapeID(sphereShape); + + syncNextNeighborFunc(*ps, domain, overlap); + + // map + ps->forEachParticle(false, lbm_mesapd_coupling::GlobalParticlesSelector(), accessor, particleMappingKernel, accessor, NoSlip_Flag ); + + if( writeVTK ) flagFieldVTK->write(); + + // check mapping + sphereMappingChecker(testIdentifier,positionAtBlockBoarder,false); + sphereMappingChecker.checkGhostLayer(testIdentifier,positionAtBlockBoarder,false); + mappingResetter(); + + WALBERLA_LOG_DEVEL_ON_ROOT(testIdentifier << " - ended successfully"); + + // clean up + ps->clear(); + syncNextNeighborFunc(*ps, domain, overlap); + } + + + ////////////////// + // FIXED SPHERE // + ////////////////// + { + std::string testIdentifier("Test: fixed sphere at block boarder with no slip mapping "); + WALBERLA_LOG_DEVEL_ON_ROOT(testIdentifier << " - started"); + + // create sphere + if (domain.isContainedInProcessSubdomain( uint_c(mpi::MPIManager::instance()->rank()), positionAtBlockBoarder )) + { + mesa_pd::data::Particle&& p = *ps->create(); + p.setPosition(positionAtBlockBoarder); + p.setInteractionRadius(radius); + p.setOwner(mpi::MPIManager::instance()->rank()); + p.setShapeID(sphereShape); + mesa_pd::data::particle_flags::set(p.getFlagsRef(), mesa_pd::data::particle_flags::FIXED); + } + syncNextNeighborFunc(*ps, domain, overlap); + + // map + ps->forEachParticle(false, lbm_mesapd_coupling::FixedParticlesSelector(), accessor, particleMappingKernel, accessor, NoSlip_Flag ); + + if( writeVTK ) flagFieldVTK->write(); + + // check mapping + sphereMappingChecker(testIdentifier,positionAtBlockBoarder,false); + sphereMappingChecker.checkGhostLayer(testIdentifier,positionAtBlockBoarder,false); + mappingResetter(); + + WALBERLA_LOG_DEVEL_ON_ROOT(testIdentifier << " - ended successfully"); + + // clean up + ps->clear(); + syncNextNeighborFunc(*ps, domain, overlap); + } + + + ///////////////////////////////////// + // TEST SPHERE AT PERIODIC BOARDER // + ///////////////////////////////////// + + //////////////////// + // REGULAR SPHERE // + //////////////////// + { + std::string testIdentifier("Test: regular sphere at periodic boarder with no slip mapping "); + WALBERLA_LOG_DEVEL_ON_ROOT(testIdentifier << " - started"); + + // create sphere + if (domain.isContainedInProcessSubdomain( uint_c(mpi::MPIManager::instance()->rank()), positionAtPeriodicBoarder )) + { + mesa_pd::data::Particle&& p = *ps->create(); + p.setPosition(positionAtPeriodicBoarder); + p.setInteractionRadius(radius); + p.setOwner(mpi::MPIManager::instance()->rank()); + p.setShapeID(sphereShape); + } + syncNextNeighborFunc(*ps, domain, overlap); + + // map + ps->forEachParticle(false, lbm_mesapd_coupling::RegularParticlesSelector(), accessor, particleMappingKernel, accessor, NoSlip_Flag ); + + if( writeVTK ) flagFieldVTK->write(); + + // check mapping + sphereMappingChecker(testIdentifier,positionAtPeriodicBoarder,true); + sphereMappingChecker.checkGhostLayer(testIdentifier,positionAtPeriodicBoarder,true); + mappingResetter(); + + WALBERLA_LOG_DEVEL_ON_ROOT(testIdentifier << " - ended successfully"); + + // clean up + ps->clear(); + syncNextNeighborFunc(*ps, domain, overlap); + } + + /////////////////// + // GLOBAL SPHERE // + /////////////////// + + // left out since periodicity does not affect global particles + + ////////////////// + // FIXED SPHERE // + ////////////////// + { + std::string testIdentifier("Test: fixed sphere at periodic boarder with no slip mapping "); + WALBERLA_LOG_DEVEL_ON_ROOT(testIdentifier << " - started"); + + // create sphere + if (domain.isContainedInProcessSubdomain( uint_c(mpi::MPIManager::instance()->rank()), positionAtPeriodicBoarder )) + { + mesa_pd::data::Particle&& p = *ps->create(); + p.setPosition(positionAtPeriodicBoarder); + p.setInteractionRadius(radius); + p.setOwner(mpi::MPIManager::instance()->rank()); + p.setShapeID(sphereShape); + mesa_pd::data::particle_flags::set(p.getFlagsRef(), mesa_pd::data::particle_flags::FIXED); + } + syncNextNeighborFunc(*ps, domain, overlap); + + // map + ps->forEachParticle(false, lbm_mesapd_coupling::FixedParticlesSelector(), accessor, particleMappingKernel, accessor, NoSlip_Flag ); + + if( writeVTK ) flagFieldVTK->write(); + + // check mapping + sphereMappingChecker(testIdentifier,positionAtPeriodicBoarder,true); + sphereMappingChecker.checkGhostLayer(testIdentifier,positionAtPeriodicBoarder,true); + mappingResetter(); + + WALBERLA_LOG_DEVEL_ON_ROOT(testIdentifier << " - ended successfully"); + + // clean up + ps->clear(); + syncNextNeighborFunc(*ps, domain, overlap); + } + + /////////////// + // HALFSPACE // + /////////////// + + auto halfSpacePosition = Vector3<real_t>(22.5,0.,3.5); + auto halfSpaceNormal = Vector3<real_t>(0.,0.,1.); + auto halfSpaceShape = ss->create<mesa_pd::data::HalfSpace>( halfSpaceNormal ); + + HalfSpaceMappingChecker halfSpaceMappingChecker(blocks, boundaryHandlingID); + { + std::string testIdentifier("Test: half space at bottom of domain with no slip mapping "); + WALBERLA_LOG_DEVEL_ON_ROOT(testIdentifier << " - started"); + + // create half space + { + mesa_pd::data::Particle&& p = *ps->create(true); + p.setPosition(halfSpacePosition); + p.setOwner(mpi::MPIManager::instance()->rank()); + p.setShapeID(halfSpaceShape); + mesa_pd::data::particle_flags::set(p.getFlagsRef(), mesa_pd::data::particle_flags::INFINITE); + mesa_pd::data::particle_flags::set(p.getFlagsRef(), mesa_pd::data::particle_flags::FIXED); + } + + syncNextNeighborFunc(*ps, domain, overlap); + + // map + ps->forEachParticle(false, lbm_mesapd_coupling::GlobalParticlesSelector(), accessor, particleMappingKernel, accessor, NoSlip_Flag ); + + if( writeVTK ) flagFieldVTK->write(); + + // check mapping + halfSpaceMappingChecker(testIdentifier,halfSpacePosition,halfSpaceNormal); + halfSpaceMappingChecker.checkGhostLayer(testIdentifier,halfSpacePosition,halfSpaceNormal); + mappingResetter(); + + WALBERLA_LOG_DEVEL_ON_ROOT(testIdentifier << " - ended successfully"); + + // clean up + ps->clear(); + syncNextNeighborFunc(*ps, domain, overlap); + } + + /////////////////////// + // ROTATED HALFSPACE // + /////////////////////// + + // rotate such that normal is <0,1,0> now + Vector3<real_t> rotationAngles( -math::pi / real_t(2), real_t(0), real_t(0)); + Quaternion<real_t> quat( rotationAngles ); + + { + std::string testIdentifier("Test: rotated half space with no slip mapping "); + WALBERLA_LOG_DEVEL_ON_ROOT(testIdentifier << " - started"); + + // create half space + { + mesa_pd::data::Particle&& p = *ps->create(true); + p.setPosition(halfSpacePosition); + p.setOwner(mpi::MPIManager::instance()->rank()); + p.setShapeID(halfSpaceShape); + p.setRotation(quat); + mesa_pd::data::particle_flags::set(p.getFlagsRef(), mesa_pd::data::particle_flags::INFINITE); + mesa_pd::data::particle_flags::set(p.getFlagsRef(), mesa_pd::data::particle_flags::FIXED); + } + syncNextNeighborFunc(*ps, domain, overlap); + + // map + ps->forEachParticle(false, lbm_mesapd_coupling::GlobalParticlesSelector(), accessor, particleMappingKernel, accessor, NoSlip_Flag ); + + if( writeVTK ) flagFieldVTK->write(); + + auto rotatedNormal = quat.toRotationMatrix() * halfSpaceNormal; + + // check mapping + halfSpaceMappingChecker(testIdentifier,halfSpacePosition,rotatedNormal); + halfSpaceMappingChecker.checkGhostLayer(testIdentifier,halfSpacePosition,rotatedNormal); + mappingResetter(); + + WALBERLA_LOG_DEVEL_ON_ROOT(testIdentifier << " - ended successfully"); + + // clean up + ps->clear(); + syncNextNeighborFunc(*ps, domain, overlap); + } + + + return 0; + +} + +} //namespace particle_mapping + +int main( int argc, char **argv ){ + particle_mapping::main(argc, argv); +} diff --git a/tests/lbm_mesapd_coupling/momentum_exchange_method/DragForceSphere.cpp b/tests/lbm_mesapd_coupling/momentum_exchange_method/DragForceSphere.cpp new file mode 100644 index 0000000000000000000000000000000000000000..18c91bd3ddbdbf45bc5bebc63782a999d9f9f450 --- /dev/null +++ b/tests/lbm_mesapd_coupling/momentum_exchange_method/DragForceSphere.cpp @@ -0,0 +1,586 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla is free software: you can +// redistribute it and/or modify it under the terms of the GNU General Public +// License as published by the Free Software Foundation, either version 3 of +// the License, or (at your option) any later version. +// +// waLBerla is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +// for more details. +// +// You should have received a copy of the GNU General Public License along +// with waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file DragForceSphere.cpp +//! \ingroup lbm_mesapd_coupling +//! \author Christoph Rettinger <christoph.rettinger@fau.de> +// +//====================================================================================================================== + +#include "blockforest/Initialization.h" +#include "blockforest/communication/UniformBufferedScheme.h" + +#include "boundary/all.h" + +#include "core/DataTypes.h" +#include "core/Environment.h" +#include "core/debug/Debug.h" +#include "core/debug/TestSubsystem.h" +#include "core/logging/Logging.h" +#include "core/math/all.h" +#include "core/SharedFunctor.h" +#include "core/timing/RemainingTimeLogger.h" +#include "core/mpi/MPIManager.h" +#include "core/mpi/Reduce.h" + +#include "domain_decomposition/SharedSweep.h" + +#include "field/AddToStorage.h" +#include "field/communication/PackInfo.h" + +#include "lbm/boundary/all.h" +#include "lbm/communication/PdfFieldPackInfo.h" +#include "lbm/field/AddToStorage.h" +#include "lbm/field/MacroscopicValueCalculation.h" +#include "lbm/field/PdfField.h" +#include "lbm/lattice_model/D3Q19.h" +#include "lbm/lattice_model/ForceModel.h" +#include "lbm/sweeps/CellwiseSweep.h" +#include "lbm/sweeps/SweepWrappers.h" + +#include "lbm_mesapd_coupling/momentum_exchange_method/MovingParticleMapping.h" +#include "lbm_mesapd_coupling/momentum_exchange_method/boundary/SimpleBB.h" +#include "lbm_mesapd_coupling/momentum_exchange_method/boundary/CurvedLinear.h" +#include "lbm_mesapd_coupling/utility/ResetHydrodynamicForceTorqueKernel.h" +#include "lbm_mesapd_coupling/utility/ParticleSelector.h" +#include "lbm_mesapd_coupling/DataTypes.h" + +#include "mesa_pd/data/DataTypes.h" +#include "mesa_pd/data/ParticleAccessorWithShape.h" +#include "mesa_pd/data/ParticleStorage.h" +#include "mesa_pd/data/ShapeStorage.h" +#include "mesa_pd/domain/BlockForestDomain.h" +#include "mesa_pd/kernel/ParticleSelector.h" + +#include "stencil/D3Q27.h" + +#include "timeloop/SweepTimeloop.h" + +#include <vector> +#include <iomanip> +#include <iostream> + +namespace drag_force_sphere_mem +{ + +/////////// +// USING // +/////////// + +using namespace walberla; +using walberla::uint_t; + +using ForceModel_T = lbm::force_model::LuoConstant; +using LatticeModel_T = lbm::D3Q19< lbm::collision_model::TRT, false, ForceModel_T >; + +using Stencil_T = LatticeModel_T::Stencil; +using PdfField_T = lbm::PdfField<LatticeModel_T>; + +using flag_t = walberla::uint8_t; +using FlagField_T = FlagField<flag_t>; + +const uint_t FieldGhostLayers = 1; + +/////////// +// FLAGS // +/////////// + +const FlagUID Fluid_Flag ( "fluid" ); +const FlagUID MO_BB_Flag ( "moving obstacle BB" ); +const FlagUID MO_CLI_Flag ( "moving obstacle CLI" ); + +//////////////// +// PARAMETERS // +//////////////// + +struct Setup{ + uint_t checkFrequency; + real_t visc; + real_t tau; + real_t radius; + uint_t length; + real_t chi; + real_t extForce; + real_t analyticalDrag; +}; + +enum MEMVariant { BB, CLI }; + +MEMVariant to_MEMVariant( const std::string& s ) +{ + if( s == "BB" ) return MEMVariant::BB; + if( s == "CLI" ) return MEMVariant::CLI; + throw std::runtime_error("invalid conversion from text to MEMVariant"); +} + +std::string MEMVariant_to_string ( const MEMVariant& m ) +{ + if( m == MEMVariant::BB ) return "BB"; + if( m == MEMVariant::CLI ) return "CLI"; + throw std::runtime_error("invalid conversion from MEMVariant to string"); +} + +///////////////////////////////////// +// BOUNDARY HANDLING CUSTOMIZATION // +///////////////////////////////////// + +template <typename ParticleAccessor_T> +class MyBoundaryHandling +{ +public: + + using SBB_T = lbm_mesapd_coupling::SimpleBB< LatticeModel_T, FlagField_T, ParticleAccessor_T >; + using CLI_T = lbm_mesapd_coupling::CurvedLinear< LatticeModel_T, FlagField_T, ParticleAccessor_T >; + using Type = BoundaryHandling< FlagField_T, Stencil_T, SBB_T, CLI_T >; + + MyBoundaryHandling( const BlockDataID & flagFieldID, const BlockDataID & pdfFieldID, + const BlockDataID & particleFieldID, const shared_ptr<ParticleAccessor_T>& ac) : + flagFieldID_( flagFieldID ), pdfFieldID_( pdfFieldID ), particleFieldID_( particleFieldID ), ac_( ac ) {} + + Type * operator()( IBlock* const block, const StructuredBlockStorage* const storage ) const + { + WALBERLA_ASSERT_NOT_NULLPTR( block ); + WALBERLA_ASSERT_NOT_NULLPTR( storage ); + + auto * flagField = block->getData< FlagField_T >( flagFieldID_ ); + auto * pdfField = block->getData< PdfField_T > ( pdfFieldID_ ); + auto * particleField = block->getData< lbm_mesapd_coupling::ParticleField_T > ( particleFieldID_ ); + + const auto fluid = flagField->flagExists( Fluid_Flag ) ? flagField->getFlag( Fluid_Flag ) : flagField->registerFlag( Fluid_Flag ); + + Type * handling = new Type( "moving obstacle boundary handling", flagField, fluid, + SBB_T("SBB_BB", MO_BB_Flag, pdfField, flagField, particleField, ac_, fluid, *storage, *block ), + CLI_T("CLI_BB", MO_CLI_Flag, pdfField, flagField, particleField, ac_, fluid, *storage, *block ) ); + + handling->fillWithDomain( FieldGhostLayers ); + + return handling; + } + +private: + + const BlockDataID flagFieldID_; + const BlockDataID pdfFieldID_; + const BlockDataID particleFieldID_; + + shared_ptr<ParticleAccessor_T> ac_; +}; + +template< typename ParticleAccessor_T> +class DragForceEvaluator +{ +public: + DragForceEvaluator( SweepTimeloop* timeloop, Setup* setup, const shared_ptr< StructuredBlockStorage > & blocks, + const BlockDataID & flagFieldID, const BlockDataID & pdfFieldID, + const shared_ptr<ParticleAccessor_T>& ac, walberla::id_t sphereID ) +: timeloop_( timeloop ), setup_( setup ), blocks_( blocks ), + flagFieldID_( flagFieldID ), pdfFieldID_( pdfFieldID ), + ac_( ac ), sphereID_( sphereID ), + normalizedDragOld_( 0.0 ), normalizedDragNew_( 0.0 ) + { + // calculate the analytical drag force value based on the series expansion of chi + // see also Sangani - Slow flow through a periodic array of spheres, IJMF 1982. Eq. 60 and Table 1 + real_t analyticalDrag = real_c(0); + real_t tempChiPowS = real_c(1); + + // coefficients to calculate the drag in a series expansion + real_t dragCoefficients[31] = { real_c(1.000000), real_c(1.418649), real_c(2.012564), real_c(2.331523), real_c(2.564809), real_c(2.584787), + real_c(2.873609), real_c(3.340163), real_c(3.536763), real_c(3.504092), real_c(3.253622), real_c(2.689757), + real_c(2.037769), real_c(1.809341), real_c(1.877347), real_c(1.534685), real_c(0.9034708), real_c(0.2857896), + real_c(-0.5512626), real_c(-1.278724), real_c(1.013350), real_c(5.492491), real_c(4.615388), real_c(-0.5736023), + real_c(-2.865924), real_c(-4.709215), real_c(-6.870076), real_c(0.1455304), real_c(12.51891), real_c(9.742811), + real_c(-5.566269)}; + + for(uint_t s = 0; s <= uint_t(30); ++s){ + analyticalDrag += dragCoefficients[s] * tempChiPowS; + tempChiPowS *= setup->chi; + } + setup_->analyticalDrag = analyticalDrag; + } + + // evaluate the acting drag force + void operator()() + { + const uint_t timestep (timeloop_->getCurrentTimeStep()+1); + + if( timestep % setup_->checkFrequency != 0) return; + + // get force in x-direction acting on the sphere + real_t forceX = computeDragForce(); + // get average volumetric flowrate in the domain + real_t uBar = computeAverageVel(); + + // f_total = f_drag + f_buoyancy + real_t totalForce = forceX + real_c(4.0/3.0) * math::pi * setup_->radius * setup_->radius * setup_->radius * setup_->extForce ; + + real_t normalizedDragForce = totalForce / real_c( 6.0 * math::pi * setup_->visc * setup_->radius * uBar ); + + // update drag force values + normalizedDragOld_ = normalizedDragNew_; + normalizedDragNew_ = normalizedDragForce; + } + + // return the relative temporal change in the normalized drag + real_t getDragForceDiff() const + { + return std::fabs( ( normalizedDragNew_ - normalizedDragOld_ ) / normalizedDragNew_ ); + } + + // return the drag force + real_t getDragForce() const + { + return normalizedDragNew_; + } + + void logResultToFile( const std::string & filename ) const + { + // write to file if desired + // format: length tau viscosity simulatedDrag analyticalDrag\n + WALBERLA_ROOT_SECTION() + { + std::ofstream file; + file.open( filename.c_str(), std::ofstream::app ); + file.precision(8); + file << setup_->length << " " << setup_->tau << " " << setup_->visc << " " << normalizedDragNew_ << " " << setup_->analyticalDrag << "\n"; + file.close(); + } + } + +private: + + // obtain the drag force acting on the sphere by summing up all the process local parts of fX + real_t computeDragForce() + { + size_t idx = ac_->uidToIdx(sphereID_); + real_t force = real_t(0); + if( idx!= ac_->getInvalidIdx()) + { + force = ac_->getHydrodynamicForce(idx)[0]; + } + + WALBERLA_MPI_SECTION() + { + mpi::allReduceInplace( force, mpi::SUM ); + } + + return force; + } + + // calculate the average velocity in forcing direction (here: x) inside the domain (assuming dx=1) + real_t computeAverageVel() + { + auto velocity_sum = real_t(0); + // iterate all blocks stored locally on this process + for( auto blockIt = blocks_->begin(); blockIt != blocks_->end(); ++blockIt ) + { + // retrieve the pdf field and the flag field from the block + PdfField_T * pdfField = blockIt->getData< PdfField_T >( pdfFieldID_ ); + FlagField_T * flagField = blockIt->getData< FlagField_T >( flagFieldID_ ); + + // get the flag that marks a cell as being fluid + auto fluid = flagField->getFlag( Fluid_Flag ); + + auto xyzField = pdfField->xyzSize(); + for (auto cell : xyzField) { + if( flagField->isFlagSet( cell.x(), cell.y(), cell.z(), fluid ) ) + { + velocity_sum += pdfField->getVelocity(cell)[0]; + } + } + } + + WALBERLA_MPI_SECTION() + { + mpi::allReduceInplace( velocity_sum, mpi::SUM ); + } + + return velocity_sum / real_c( setup_->length * setup_->length * setup_->length ); + } + + SweepTimeloop* timeloop_; + + Setup* setup_; + + shared_ptr< StructuredBlockStorage > blocks_; + const BlockDataID flagFieldID_; + const BlockDataID pdfFieldID_; + + shared_ptr<ParticleAccessor_T> ac_; + const walberla::id_t sphereID_; + + // drag coefficient + real_t normalizedDragOld_; + real_t normalizedDragNew_; + +}; + +////////// +// MAIN // +////////// + + +//******************************************************************************************************************* +/*!\brief Testcase that checks the drag force acting on a fixed sphere in the center of a cubic domain in Stokes flow + * + * The drag force for this problem (often denoted as Simple Cubic setup) is given by a semi-analytical series expansion. + * The cubic domain is periodic in all directions, making it a physically infinite periodic array of spheres. + * _______________ + * ->| |-> + * ->| ___ |-> + * W ->| / \ |-> E + * E ->| | x | |-> A + * S ->| \___/ |-> S + * T ->| |-> T + * ->|_______________|-> + * + * The collision model used for the LBM is TRT with a relaxation parameter tau=1.5 and the magic parameter 3/16. + * The Stokes approximation of the equilibrium PDFs is used. + * The flow is driven by a constant body force of 1e-5. + * The domain is 32x32x32, and the sphere has a diameter of 16 cells ( chi * domainlength ) + * The simulation is run until the relative change in the dragforce between 100 time steps is less than 1e-5. + * The RPD is not used since the sphere is kept fixed and the force is explicitly reset after each time step. + * To avoid periodicity constrain problems, the sphere is set as global. + * + */ +//******************************************************************************************************************* + + +int main( int argc, char **argv ) +{ + debug::enterTestMode(); + + mpi::Environment env( argc, argv ); + + auto processes = MPIManager::instance()->numProcesses(); + + if( processes != 1 && processes != 2 && processes != 4 && processes != 8) + { + std::cerr << "Number of processes must be equal to either 1, 2, 4, or 8!" << std::endl; + return EXIT_FAILURE; + } + + /////////////////// + // Customization // + /////////////////// + + bool shortrun = false; + bool funcTest = false; + bool logging = false; + MEMVariant method = MEMVariant::BB; + real_t tau = real_c( 1.5 ); + uint_t length = uint_c( 32 ); + + for( int i = 1; i < argc; ++i ) + { + if( std::strcmp( argv[i], "--shortrun" ) == 0 ) { shortrun = true; continue; } + if( std::strcmp( argv[i], "--funcTest" ) == 0 ) { funcTest = true; continue; } + if( std::strcmp( argv[i], "--logging" ) == 0 ) { logging = true; continue; } + if( std::strcmp( argv[i], "--MEMVariant") == 0 ) { method = to_MEMVariant( argv[++i] ); continue; } + if( std::strcmp( argv[i], "--tau" ) == 0 ) { tau = real_c( std::atof( argv[++i] ) ); continue; } + if( std::strcmp( argv[i], "--length" ) == 0 ) { length = uint_c( std::atof( argv[++i] ) ); continue; } + WALBERLA_ABORT("Unrecognized command line argument found: " << argv[i]); + } + + + /////////////////////////// + // SIMULATION PROPERTIES // + /////////////////////////// + + Setup setup; + + setup.length = length; // length of the cubic domain in lattice cells + setup.chi = real_c( 0.5 ); // porosity parameter: diameter / length + setup.tau = tau; // relaxation time + setup.extForce = real_c( 1e-7 ); // constant body force in lattice units + setup.checkFrequency = uint_t( 100 ); // evaluate the drag force only every checkFrequency time steps + setup.radius = real_c(0.5) * setup.chi * real_c( setup.length ); // sphere radius + setup.visc = ( setup.tau - real_c(0.5) ) / real_c(3); // viscosity in lattice units + const real_t omega = real_c(1) / setup.tau; // relaxation rate + const real_t dx = real_c(1); // lattice dx + const real_t convergenceLimit = real_c( 1e-7 ); // tolerance for relative change in drag force + const uint_t timesteps = funcTest ? 1 : ( shortrun ? uint_c(150) : uint_c( 50000 ) ); // maximum number of time steps for the whole simulation + + /////////////////////////// + // BLOCK STRUCTURE SETUP // + /////////////////////////// + + const uint_t XBlocks = (processes >= 2) ? uint_t( 2 ) : uint_t( 1 ); + const uint_t YBlocks = (processes >= 4) ? uint_t( 2 ) : uint_t( 1 ); + const uint_t ZBlocks = (processes == 8) ? uint_t( 2 ) : uint_t( 1 ); + const uint_t XCells = setup.length / XBlocks; + const uint_t YCells = setup.length / YBlocks; + const uint_t ZCells = setup.length / ZBlocks; + + // create fully periodic domain + auto blocks = blockforest::createUniformBlockGrid( XBlocks, YBlocks, ZBlocks, XCells, YCells, ZCells, dx, true, + true, true, true ); + + + ///////// + // RPD // + ///////// + + mesa_pd::domain::BlockForestDomain domain(blocks->getBlockForestPointer()); + + //init data structures + auto ps = std::make_shared<mesa_pd::data::ParticleStorage>(1); + auto ss = std::make_shared<mesa_pd::data::ShapeStorage>(); + using ParticleAccessor_T = mesa_pd::data::ParticleAccessorWithShape; + auto accessor = make_shared<ParticleAccessor_T >(ps, ss); + auto sphereShape = ss->create<mesa_pd::data::Sphere>( setup.radius ); + + ////////////////// + // RPD COUPLING // + ////////////////// + + // connect to pe + const real_t overlap = real_t( 1.5 ) * dx; + + if( setup.radius > real_c( setup.length ) * real_t(0.5) - overlap ) + { + std::cerr << "Periodic sphere is too large and would lead to incorrect mapping!" << std::endl; + // solution: create the periodic copies explicitly + return EXIT_FAILURE; + } + + // create the sphere in the middle of the domain + // it is global and thus present on all processes + Vector3<real_t> position (real_c(setup.length) * real_c(0.5)); + walberla::id_t sphereID; + { + mesa_pd::data::Particle&& p = *ps->create(true); + p.setPosition(position); + p.setInteractionRadius(setup.radius); + p.setOwner(mpi::MPIManager::instance()->rank()); + p.setShapeID(sphereShape); + sphereID = p.getUid(); + } + + /////////////////////// + // ADD DATA TO BLOCKS // + //////////////////////// + + // create the lattice model + LatticeModel_T latticeModel = LatticeModel_T( lbm::collision_model::TRT::constructWithMagicNumber( omega ), ForceModel_T( Vector3<real_t> ( setup.extForce, 0, 0 ) ) ); + + // add PDF field + BlockDataID pdfFieldID = lbm::addPdfFieldToStorage< LatticeModel_T >( blocks, "pdf field (zyxf)", latticeModel, + Vector3< real_t >( real_t(0) ), real_t(1), + uint_t(1), field::zyxf ); + + // add flag field + BlockDataID flagFieldID = field::addFlagFieldToStorage<FlagField_T>( blocks, "flag field" ); + + // add particle field + BlockDataID particleFieldID = field::addToStorage<lbm_mesapd_coupling::ParticleField_T>( blocks, "particle field", accessor->getInvalidUid(), field::zyxf, FieldGhostLayers ); + + // add boundary handling + using BoundaryHandling_T = MyBoundaryHandling<ParticleAccessor_T>::Type; + BlockDataID boundaryHandlingID = blocks->addStructuredBlockData< BoundaryHandling_T >(MyBoundaryHandling<ParticleAccessor_T>( flagFieldID, pdfFieldID, particleFieldID, accessor), "boundary handling" ); + + /////////////// + // TIME LOOP // + /////////////// + + // create the timeloop + SweepTimeloop timeloop( blocks->getBlockStorage(), timesteps ); + + // setup of the LBM communication for synchronizing the pdf field between neighboring blocks + blockforest::communication::UniformBufferedScheme< Stencil_T > optimizedPDFCommunicationScheme( blocks ); + optimizedPDFCommunicationScheme.addPackInfo( make_shared< lbm::PdfFieldPackInfo< LatticeModel_T > >( pdfFieldID ) ); // optimized sync + + // initially map particles into the LBM simulation + lbm_mesapd_coupling::MovingParticleMappingKernel<BoundaryHandling_T> movingParticleMappingKernel(blocks, boundaryHandlingID, particleFieldID); + if( method == MEMVariant::CLI ) + { + // uses a higher order boundary condition (CLI) + ps->forEachParticle(false, mesa_pd::kernel::SelectAll(), *accessor, movingParticleMappingKernel, *accessor, MO_CLI_Flag); + }else{ + // uses standard bounce back boundary conditions + ps->forEachParticle(false, mesa_pd::kernel::SelectAll(), *accessor, movingParticleMappingKernel, *accessor, MO_BB_Flag); + } + + // since external forcing is applied, the evaluation of the velocity has to be carried out directly after the streaming step + // however, the default sweep is a stream - collide step, i.e. after the sweep, the velocity evaluation is not correct + // solution: split the sweep explicitly into collide and stream + auto sweep = lbm::makeCellwiseSweep< LatticeModel_T, FlagField_T >( pdfFieldID, flagFieldID, Fluid_Flag ); + + // collision sweep + timeloop.add() << Sweep( lbm::makeCollideSweep( sweep ), "cell-wise LB sweep (collide)" ); + + // add LBM communication function and boundary handling sweep (does the hydro force calculations and the no-slip treatment) + timeloop.add() << BeforeFunction( optimizedPDFCommunicationScheme, "LBM Communication" ) + << Sweep( BoundaryHandling_T::getBlockSweep( boundaryHandlingID ), "Boundary Handling" ); + + // streaming & force evaluation + using DragForceEval_T = DragForceEvaluator<ParticleAccessor_T>; + auto forceEval = make_shared< DragForceEval_T >( &timeloop, &setup, blocks, flagFieldID, pdfFieldID, accessor, sphereID ); + timeloop.add() << Sweep( lbm::makeStreamSweep( sweep ), "cell-wise LB sweep (stream)" ) + << AfterFunction( SharedFunctor< DragForceEval_T >( forceEval ), "drag force evaluation" ); + + // resetting force + timeloop.addFuncAfterTimeStep( [ps,accessor](){ps->forEachParticle(false, mesa_pd::kernel::SelectAll(), *accessor, lbm_mesapd_coupling::ResetHydrodynamicForceTorqueKernel(), *accessor);}, "reset force on sphere"); + + timeloop.addFuncAfterTimeStep( RemainingTimeLogger( timeloop.getNrOfTimeSteps() ), "Remaining Time Logger" ); + + //////////////////////// + // EXECUTE SIMULATION // + //////////////////////// + + WcTimingPool timeloopTiming; + + // time loop + for (uint_t i = 0; i < timesteps; ++i ) + { + // perform a single simulation step + timeloop.singleStep( timeloopTiming ); + + // check if the relative change in the normalized drag force is below the specified convergence criterion + if ( i > setup.checkFrequency && forceEval->getDragForceDiff() < convergenceLimit ) + { + // if simulation has converged, terminate simulation + break; + } + + } + + timeloopTiming.logResultOnRoot(); + + if ( !funcTest && !shortrun ){ + // check the result + real_t relErr = std::fabs( ( setup.analyticalDrag - forceEval->getDragForce() ) / setup.analyticalDrag ); + if ( logging ) + { + WALBERLA_ROOT_SECTION() + { + std::cout << "Analytical drag: " << setup.analyticalDrag << "\n" + << "Simulated drag: " << forceEval->getDragForce() << "\n" + << "Relative error: " << relErr << "\n"; + } + forceEval->logResultToFile( "log_DragForceSphereMEM_"+MEMVariant_to_string(method)+".txt" ); + } + // the relative error has to be below 10% + WALBERLA_CHECK_LESS( relErr, real_c(0.1) ); + } + + return 0; + +} + +} //namespace drag_force_sphere_mem + +int main( int argc, char **argv ){ + drag_force_sphere_mem::main(argc, argv); +} diff --git a/tests/lbm_mesapd_coupling/momentum_exchange_method/ForceBetweenTwoStationaryObjects.cpp b/tests/lbm_mesapd_coupling/momentum_exchange_method/ForceBetweenTwoStationaryObjects.cpp new file mode 100644 index 0000000000000000000000000000000000000000..e244543af014d124057c8b187ef59e7395cb8116 --- /dev/null +++ b/tests/lbm_mesapd_coupling/momentum_exchange_method/ForceBetweenTwoStationaryObjects.cpp @@ -0,0 +1,431 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla is free software: you can +// redistribute it and/or modify it under the terms of the GNU General Public +// License as published by the Free Software Foundation, either version 3 of +// the License, or (at your option) any later version. +// +// waLBerla is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +// for more details. +// +// You should have received a copy of the GNU General Public License along +// with waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file ForceBetweenTwoStationaryObjects.cpp +//! \ingroup lbm_mesapd_coupling +//! \author Christoph Rettinger <christoph.rettinger@fau.de> +// +//====================================================================================================================== + +#include "blockforest/Initialization.h" +#include "blockforest/communication/UniformBufferedScheme.h" + +#include "boundary/all.h" + +#include "core/DataTypes.h" +#include "core/Environment.h" +#include "core/debug/Debug.h" +#include "core/debug/TestSubsystem.h" +#include "core/logging/Logging.h" +#include "core/math/all.h" +#include "core/SharedFunctor.h" +#include "core/timing/RemainingTimeLogger.h" +#include "core/mpi/MPIManager.h" +#include "core/mpi/Reduce.h" + +#include "domain_decomposition/SharedSweep.h" + +#include "field/AddToStorage.h" +#include "field/communication/PackInfo.h" + +#include "lbm/boundary/all.h" +#include "lbm/communication/PdfFieldPackInfo.h" +#include "lbm/field/AddToStorage.h" +#include "lbm/field/MacroscopicValueCalculation.h" +#include "lbm/field/PdfField.h" +#include "lbm/lattice_model/D3Q19.h" +#include "lbm/lattice_model/ForceModel.h" +#include "lbm/sweeps/CellwiseSweep.h" +#include "lbm/sweeps/SweepWrappers.h" + +#include "lbm_mesapd_coupling/momentum_exchange_method/MovingParticleMapping.h" +#include "lbm_mesapd_coupling/momentum_exchange_method/boundary/SimpleBB.h" +#include "lbm_mesapd_coupling/momentum_exchange_method/boundary/CurvedLinear.h" +#include "lbm_mesapd_coupling/utility/ResetHydrodynamicForceTorqueKernel.h" +#include "lbm_mesapd_coupling/utility/ParticleSelector.h" +#include "lbm_mesapd_coupling/DataTypes.h" + +#include "mesa_pd/data/DataTypes.h" +#include "mesa_pd/data/ParticleAccessorWithShape.h" +#include "mesa_pd/data/ParticleStorage.h" +#include "mesa_pd/data/ShapeStorage.h" +#include "mesa_pd/domain/BlockForestDomain.h" +#include "mesa_pd/kernel/ParticleSelector.h" + +#include "stencil/D3Q27.h" + +#include "timeloop/SweepTimeloop.h" + +#include <iomanip> +#include <iostream> + +namespace force_between_two_stationary_objects +{ + +/////////// +// USING // +/////////// + +using namespace walberla; +using walberla::uint_t; + +using LatticeModel_T = lbm::D3Q19< lbm::collision_model::TRT>; +using LatticeModelComp_T = lbm::D3Q19< lbm::collision_model::TRT, true>; + + +using flag_t = walberla::uint8_t; +using FlagField_T = FlagField<flag_t>; + +const uint_t FieldGhostLayers = 1; + +/////////// +// FLAGS // +/////////// + +const FlagUID Fluid_Flag ( "fluid" ); +const FlagUID BB_Flag ( "obstacle BB" ); +const FlagUID CLI_Flag ( "obstacle CLI" ); + +///////////////////////////////////// +// BOUNDARY HANDLING CUSTOMIZATION // +///////////////////////////////////// + +template <typename LM_T, typename ParticleAccessor_T> +class MyBoundaryHandling +{ +public: + + using PdfField_T = lbm::PdfField<LM_T>; + using SBB_T = lbm_mesapd_coupling::SimpleBB< LM_T, FlagField_T, ParticleAccessor_T >; + using CLI_T = lbm_mesapd_coupling::CurvedLinear< LM_T, FlagField_T, ParticleAccessor_T >; + using Type = BoundaryHandling< FlagField_T, typename LM_T::Stencil, SBB_T, CLI_T >; + + MyBoundaryHandling( const BlockDataID & flagFieldID, const BlockDataID & pdfFieldID, + const BlockDataID & particleFieldID, const shared_ptr<ParticleAccessor_T>& ac) : + flagFieldID_( flagFieldID ), pdfFieldID_( pdfFieldID ), particleFieldID_( particleFieldID ), ac_( ac ) {} + + Type * operator()( IBlock* const block, const StructuredBlockStorage* const storage ) const + { + WALBERLA_ASSERT_NOT_NULLPTR( block ); + WALBERLA_ASSERT_NOT_NULLPTR( storage ); + + auto * flagField = block->getData< FlagField_T >( flagFieldID_ ); + auto * pdfField = block->getData< PdfField_T > ( pdfFieldID_ ); + auto * particleField = block->getData< lbm_mesapd_coupling::ParticleField_T > ( particleFieldID_ ); + + const auto fluid = flagField->flagExists( Fluid_Flag ) ? flagField->getFlag( Fluid_Flag ) : flagField->registerFlag( Fluid_Flag ); + + Type * handling = new Type( "moving obstacle boundary handling", flagField, fluid, + SBB_T("SBB_BB", BB_Flag, pdfField, flagField, particleField, ac_, fluid, *storage, *block ), + CLI_T("CLI_BB", CLI_Flag, pdfField, flagField, particleField, ac_, fluid, *storage, *block ) ); + + handling->fillWithDomain( FieldGhostLayers ); + + return handling; + } + +private: + + const BlockDataID flagFieldID_; + const BlockDataID pdfFieldID_; + const BlockDataID particleFieldID_; + + shared_ptr<ParticleAccessor_T> ac_; +}; + +void createPlaneSetup(const shared_ptr<mesa_pd::data::ParticleStorage> & ps, const shared_ptr<mesa_pd::data::ShapeStorage> & ss, + const math::AABB & simulationDomain, Vector3<real_t> velocity) +{ + // create bounding planes + mesa_pd::data::Particle p0 = *ps->create(true); + p0.setPosition(simulationDomain.minCorner()); + p0.setShapeID(ss->create<mesa_pd::data::HalfSpace>( Vector3<real_t>(0,0,1) )); + p0.setOwner(mpi::MPIManager::instance()->rank()); + p0.setType(0); + p0.setLinearVelocity(velocity); + mesa_pd::data::particle_flags::set(p0.getFlagsRef(), mesa_pd::data::particle_flags::INFINITE); + mesa_pd::data::particle_flags::set(p0.getFlagsRef(), mesa_pd::data::particle_flags::FIXED); + + mesa_pd::data::Particle p1 = *ps->create(true); + p1.setPosition(simulationDomain.maxCorner()); + p1.setShapeID(ss->create<mesa_pd::data::HalfSpace>( Vector3<real_t>(0,0,-1) )); + p1.setOwner(mpi::MPIManager::instance()->rank()); + p1.setType(0); + p1.setLinearVelocity(velocity); + mesa_pd::data::particle_flags::set(p1.getFlagsRef(), mesa_pd::data::particle_flags::INFINITE); + mesa_pd::data::particle_flags::set(p1.getFlagsRef(), mesa_pd::data::particle_flags::FIXED); + + mesa_pd::data::Particle p2 = *ps->create(true); + p2.setPosition(simulationDomain.minCorner()); + p2.setShapeID(ss->create<mesa_pd::data::HalfSpace>( Vector3<real_t>(1,0,0) )); + p2.setOwner(mpi::MPIManager::instance()->rank()); + p2.setType(0); + p2.setLinearVelocity(velocity); + mesa_pd::data::particle_flags::set(p2.getFlagsRef(), mesa_pd::data::particle_flags::INFINITE); + mesa_pd::data::particle_flags::set(p2.getFlagsRef(), mesa_pd::data::particle_flags::FIXED); + + mesa_pd::data::Particle p3 = *ps->create(true); + p3.setPosition(simulationDomain.maxCorner()); + p3.setShapeID(ss->create<mesa_pd::data::HalfSpace>( Vector3<real_t>(-1,0,0) )); + p3.setOwner(mpi::MPIManager::instance()->rank()); + p3.setType(0); + p3.setLinearVelocity(velocity); + mesa_pd::data::particle_flags::set(p3.getFlagsRef(), mesa_pd::data::particle_flags::INFINITE); + mesa_pd::data::particle_flags::set(p3.getFlagsRef(), mesa_pd::data::particle_flags::FIXED); + + mesa_pd::data::Particle p4 = *ps->create(true); + p4.setPosition(simulationDomain.minCorner()); + p4.setShapeID(ss->create<mesa_pd::data::HalfSpace>( Vector3<real_t>(0,1,0) )); + p4.setOwner(mpi::MPIManager::instance()->rank()); + p4.setType(0); + p4.setLinearVelocity(velocity); + mesa_pd::data::particle_flags::set(p4.getFlagsRef(), mesa_pd::data::particle_flags::INFINITE); + mesa_pd::data::particle_flags::set(p4.getFlagsRef(), mesa_pd::data::particle_flags::FIXED); + + mesa_pd::data::Particle p5 = *ps->create(true); + p5.setPosition(simulationDomain.maxCorner()); + p5.setShapeID(ss->create<mesa_pd::data::HalfSpace>( Vector3<real_t>(0,-1,0) )); + p5.setOwner(mpi::MPIManager::instance()->rank()); + p5.setType(0); + p5.setLinearVelocity(velocity); + mesa_pd::data::particle_flags::set(p5.getFlagsRef(), mesa_pd::data::particle_flags::INFINITE); + mesa_pd::data::particle_flags::set(p5.getFlagsRef(), mesa_pd::data::particle_flags::FIXED); + +} + +template< typename LM_T, typename ParticleAccessor_T> +void createSimulationSetup( shared_ptr< StructuredBlockForest > blocks, shared_ptr<ParticleAccessor_T> accessor, + bool useSBB, shared_ptr<mesa_pd::data::ParticleStorage> ps, Vector3<real_t> velocity, + SweepTimeloop & timeloop ) +{ + real_t omega = real_t(1); + + // create the lattice model + LM_T latticeModel = LM_T( lbm::collision_model::TRT::constructWithMagicNumber( omega ) ); + + // add PDF field + BlockDataID pdfFieldID = lbm::addPdfFieldToStorage< LM_T >( blocks, "pdf field (zyxf)", latticeModel, velocity, real_t(1), uint_t(1), field::zyxf ); + + // add flag field + BlockDataID flagFieldID = field::addFlagFieldToStorage<FlagField_T>( blocks, "flag field" ); + + // add particle field + BlockDataID particleFieldID = field::addToStorage<lbm_mesapd_coupling::ParticleField_T>( blocks, "particle field", accessor->getInvalidUid(), field::zyxf, FieldGhostLayers ); + + // add boundary handling + using BoundaryHandling_T = typename MyBoundaryHandling<LM_T,ParticleAccessor_T>::Type; + BlockDataID boundaryHandlingID = blocks->addStructuredBlockData< BoundaryHandling_T >(MyBoundaryHandling<LM_T,ParticleAccessor_T>( flagFieldID, pdfFieldID, particleFieldID, accessor), "boundary handling" ); + + // boundary handling sweep (does the hydro force calculations and the no-slip treatment) + timeloop.add() << Sweep( BoundaryHandling_T::getBlockSweep( boundaryHandlingID ), "Boundary Handling" ); + + // LBM + auto sweep = lbm::makeCellwiseSweep< LM_T, FlagField_T >( pdfFieldID, flagFieldID, Fluid_Flag ); + timeloop.add() << Sweep( makeSharedSweep(sweep), "cell-wise LB sweep" ); + + // map particles + lbm_mesapd_coupling::MovingParticleMappingKernel<BoundaryHandling_T> movingParticleMappingKernel(blocks, boundaryHandlingID, particleFieldID); + if( useSBB ) + { + ps->forEachParticle(false, mesa_pd::kernel::SelectAll(), *accessor, movingParticleMappingKernel, *accessor, BB_Flag); + }else{ + ps->forEachParticle(false, mesa_pd::kernel::SelectAll(), *accessor, movingParticleMappingKernel, *accessor, CLI_Flag); + } +} + + +//******************************************************************************************************************* +/*!\brief Testcase that checks the force between two stationary objects (sphere-sphere and sphere-wall) in a quiescent fluid. + * + * Since the fluid has zero velocity, as well as the objects, the force has to be zero! + * + * The objects have only a small distance. + * Thus, not all cells around the sphere contain fluid and are thus skipped by the boundary handling and force computation. + * As a result, some force components are missing that balance the total force such that it adds up to zero. + * As such, one would need to reconstruct the PDF values inside the missing (from the perspective of one of the obstacles) + * fluid cells to then obtain the balance again. + * This is e.g. done in + * - Ernst, Dietzel, Sommerfeld - A lattice Boltzmann method for simulating transport and agglomeration of resolved particles + * doi: 10.1007/s00707-013-0923-1 (2013), Section 2.2.4 + * - in the work of Simon Bogner + * + * This is avoided here via the following: + * ( also see the docu in the beginning of lbm/PdfField.h) + * When using an incompressible LBM model in waLBerla, all stored PDF values are simply the offset from the default weights. + * I.e. in a quiescent fluid with density 1, all of them are zero. + * Consequently, the force contributions computed by the momentum exchange method are also all zero in that case. + * It thus does not matter if some of them are missing due to a nearby obstacle because only zero values would be missing. + * + * This changes when using a compressible model, as then the stored PDF values are the regular ones in waLBerla. + * As such, one would see the missing force contributions in this test. + * To compensate this, the PDF values are explicitly normalized like for the incompressible case in the MEM boundary conditions. + * Then, the same arguments as for the incompressible flow hold. + * + * This can also be seen as an implicit reconstruction of the PDFs in the missing cells with feq(0, vec(0)). + * As such, it can/will lead to errors if the velocity of the obstacle covering the missing cells is different from the + * surrounding flow. In that case, an explicit reconstruction like in Ernst et al. should be applied, which however will + * change the algorithm, as the computation of the force components can probbaly no longer be fused with the boundary handling. + * + * Tested variants: + * - boundary condition: SimpleBB, CLI + * - compressible and incompressible lattice model + * - different surface distances + * - sphere-sphere and sphere-wall + * - different system velocity, i.e. velocity of ALL things is constant but can be non-zero -> zero force expected + * + */ +//******************************************************************************************************************* +int main( int argc, char **argv ) +{ + debug::enterTestMode(); + + mpi::Environment env( argc, argv ); + + /////////////////// + // Customization // + /////////////////// + + bool useCompressible = false; + bool useSBB = false; + bool useSphereWallSetup = false; + real_t surfaceDistance = real_t(0.5); + real_t systemVelocity = real_t(0); + uint_t timesteps = uint_t(10); + + for( int i = 1; i < argc; ++i ) + { + if( std::strcmp( argv[i], "--useCompressible" ) == 0 ) { useCompressible = true; continue;} + if( std::strcmp( argv[i], "--useSBB" ) == 0 ) { useSBB = true; continue;} + if( std::strcmp( argv[i], "--useSphereWallSetup" ) == 0 ) { useSphereWallSetup = true; continue;} + if( std::strcmp( argv[i], "--surfaceDistance" ) == 0 ) { surfaceDistance = real_c(std::atof( argv[++i])); continue;} + if( std::strcmp( argv[i], "--systemVelocity" ) == 0 ) { systemVelocity = real_c(std::atof( argv[++i])); continue;} + if( std::strcmp( argv[i], "--timesteps" ) == 0 ) { timesteps = uint_c(std::atof( argv[++i])); continue;} + WALBERLA_ABORT("Unrecognized command line argument found: " << argv[i]); + } + + + /////////////////////////// + // SIMULATION PROPERTIES // + /////////////////////////// + + const uint_t length = uint_t(32); + const real_t radius = real_t(5); + const Vector3<real_t> velocity(systemVelocity, real_t(0), real_t(0)); + + /////////////////////////// + // BLOCK STRUCTURE SETUP // + /////////////////////////// + + const uint_t XBlocks = uint_t( 1 ); + const uint_t YBlocks = uint_t( 1 ); + const uint_t ZBlocks = uint_t( 1 ); + const uint_t XCells = length / XBlocks; + const uint_t YCells = length / YBlocks; + const uint_t ZCells = length / ZBlocks; + + auto blocks = blockforest::createUniformBlockGrid( XBlocks, YBlocks, ZBlocks, XCells, YCells, ZCells, uint_t(1), true, + false, false, false ); + + + mesa_pd::domain::BlockForestDomain domain(blocks->getBlockForestPointer()); + + auto ps = std::make_shared<mesa_pd::data::ParticleStorage>(1); + auto ss = std::make_shared<mesa_pd::data::ShapeStorage>(); + using ParticleAccessor_T = mesa_pd::data::ParticleAccessorWithShape; + auto accessor = make_shared<ParticleAccessor_T >(ps, ss); + auto sphereShape = ss->create<mesa_pd::data::Sphere>( radius ); + + createPlaneSetup(ps, ss, walberla::math::AABB(real_t(0), real_t(0), real_t(0), length, length, length), velocity); + + walberla::id_t sphereID; + if(useSphereWallSetup) + { + // create sphere close to right wall + Vector3<real_t> position1 (real_c(length) - radius - surfaceDistance, + real_c(length) * real_c(0.5), + real_c(length) * real_c(0.5)); + { + mesa_pd::data::Particle&& p = *ps->create(); + p.setPosition(position1); + p.setInteractionRadius(radius); + p.setOwner(mpi::MPIManager::instance()->rank()); + p.setShapeID(sphereShape); + p.setLinearVelocity(velocity); + sphereID = p.getUid(); + } + } else { + // create two spheres + Vector3<real_t> position1 (real_c(length) * real_c(0.5) - radius - surfaceDistance*real_t(0.5), + real_c(length) * real_c(0.5), + real_c(length) * real_c(0.5)); + { + mesa_pd::data::Particle&& p = *ps->create(); + p.setPosition(position1); + p.setInteractionRadius(radius); + p.setOwner(mpi::MPIManager::instance()->rank()); + p.setShapeID(sphereShape); + p.setLinearVelocity(velocity); + sphereID = p.getUid(); + } + + + Vector3<real_t> position2 (real_c(length) * real_c(0.5) + radius + surfaceDistance*real_t(0.5), + real_c(length) * real_c(0.5), + real_c(length) * real_c(0.5)); + { + mesa_pd::data::Particle&& p = *ps->create(); + p.setPosition(position2); + p.setInteractionRadius(radius); + p.setOwner(mpi::MPIManager::instance()->rank()); + p.setShapeID(sphereShape); + p.setLinearVelocity(velocity); + } + } + + // create the timeloop + SweepTimeloop timeloop( blocks->getBlockStorage(), timesteps ); + if(useCompressible) createSimulationSetup<LatticeModelComp_T>(blocks, accessor, useSBB, ps, velocity , timeloop); + else createSimulationSetup<LatticeModel_T>(blocks, accessor, useSBB, ps, velocity, timeloop); + + + for(uint_t t = 0; t < timeloop.getNrOfTimeSteps(); ++t) + { + // some timesteps + timeloop.singleStep(); + + // check force + size_t idx = accessor->uidToIdx(sphereID); + auto hydrodynamicForce = accessor->getHydrodynamicForce(idx); + + //WALBERLA_LOG_INFO(hydrodynamicForce); + for(uint_t comp = 0; comp < 3; ++comp) + { + WALBERLA_CHECK_FLOAT_EQUAL(hydrodynamicForce[comp], real_t(0), "Found non-zero force in component " << comp); + } + + lbm_mesapd_coupling::ResetHydrodynamicForceTorqueKernel resetHydrodynamicForceTorque; + ps->forEachParticle(false, mesa_pd::kernel::SelectAll(), *accessor, resetHydrodynamicForceTorque, *accessor ); + + } + + return 0; + +} + +} //namespace force_between_two_stationary_objects + +int main( int argc, char **argv ){ + force_between_two_stationary_objects::main(argc, argv); +} diff --git a/tests/lbm_mesapd_coupling/momentum_exchange_method/MovingParticleMapping.cpp b/tests/lbm_mesapd_coupling/momentum_exchange_method/MovingParticleMapping.cpp new file mode 100644 index 0000000000000000000000000000000000000000..a390f6feb62535af2d6edf140244649eecf9ba55 --- /dev/null +++ b/tests/lbm_mesapd_coupling/momentum_exchange_method/MovingParticleMapping.cpp @@ -0,0 +1,763 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla is free software: you can +// redistribute it and/or modify it under the terms of the GNU General Public +// License as published by the Free Software Foundation, either version 3 of +// the License, or (at your option) any later version. +// +// waLBerla is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +// for more details. +// +// You should have received a copy of the GNU General Public License along +// with waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file MovingParticleMapping.cpp +//! \ingroup lbm_mesapd_coupling +//! \author Christoph Rettinger <christoph.rettinger@fau.de> +// +//====================================================================================================================== + +#include "blockforest/Initialization.h" +#include "blockforest/communication/UniformBufferedScheme.h" + +#include "boundary/all.h" + +#include "core/DataTypes.h" +#include "core/Environment.h" +#include "core/debug/Debug.h" +#include "core/debug/TestSubsystem.h" +#include "core/math/all.h" +#include "core/mpi/MPIManager.h" +#include "core/mpi/Reduce.h" + +#include "field/AddToStorage.h" + +#include "lbm/boundary/all.h" +#include "lbm/field/AddToStorage.h" +#include "lbm/field/PdfField.h" +#include "lbm/lattice_model/D3Q19.h" + +#include "lbm_mesapd_coupling/momentum_exchange_method/MovingParticleMapping.h" +#include "lbm_mesapd_coupling/momentum_exchange_method/boundary/SimpleBB.h" +#include "lbm_mesapd_coupling/utility/ParticleSelector.h" +#include "lbm_mesapd_coupling/DataTypes.h" + +#include "mesa_pd/mpi/SyncNextNeighbors.h" +#include "mesa_pd/data/ParticleAccessorWithShape.h" +#include "mesa_pd/data/ParticleStorage.h" +#include "mesa_pd/data/ShapeStorage.h" +#include "mesa_pd/domain/BlockForestDomain.h" +#include "mesa_pd/data/DataTypes.h" +#include "mesa_pd/kernel/SingleCast.h" + +#include "vtk/all.h" +#include "field/vtk/all.h" + +namespace moving_particle_mapping +{ + +/////////// +// USING // +/////////// + +using namespace walberla; +using walberla::uint_t; + +using LatticeModel_T = lbm::D3Q19<lbm::collision_model::SRT>; + +using Stencil_T = LatticeModel_T::Stencil; +using PdfField_T = lbm::PdfField<LatticeModel_T>; + +const uint_t FieldGhostLayers = 1; + +using flag_t = walberla::uint8_t; +using FlagField_T = FlagField<flag_t>; + + +/////////// +// FLAGS // +/////////// + +const FlagUID Fluid_Flag ( "fluid" ); +const FlagUID MO_Flag ( "moving obstacle" ); +const FlagUID FormerMO_Flag ( "former moving obstacle" ); + + +///////////////////////////////////// +// BOUNDARY HANDLING CUSTOMIZATION // +///////////////////////////////////// + +template <typename ParticleAccessor> +class MyBoundaryHandling +{ +public: + + using MO_T = lbm_mesapd_coupling::SimpleBB< LatticeModel_T, FlagField_T, ParticleAccessor >; + using Type = BoundaryHandling< FlagField_T, Stencil_T, MO_T >; + + MyBoundaryHandling( const BlockDataID & flagFieldID, const BlockDataID & pdfFieldID, + const BlockDataID & particleFieldID, const shared_ptr<ParticleAccessor>& ac) : + flagFieldID_( flagFieldID ), pdfFieldID_( pdfFieldID ), particleFieldID_( particleFieldID ), ac_( ac ) {} + + Type * operator()( IBlock* const block, const StructuredBlockStorage* const storage ) const + { + WALBERLA_ASSERT_NOT_NULLPTR( block ); + WALBERLA_ASSERT_NOT_NULLPTR( storage ); + + auto * flagField = block->getData< FlagField_T >( flagFieldID_ ); + auto * pdfField = block->getData< PdfField_T > ( pdfFieldID_ ); + auto * particleField = block->getData< lbm_mesapd_coupling::ParticleField_T > ( particleFieldID_ ); + + const auto fluid = flagField->flagExists( Fluid_Flag ) ? flagField->getFlag( Fluid_Flag ) : flagField->registerFlag( Fluid_Flag ); + + Type * handling = new Type( "moving obstacle boundary handling", flagField, fluid, + MO_T("MO_BB", MO_Flag, pdfField, flagField, particleField, ac_, fluid, *storage, *block ) ); + + handling->fillWithDomain( FieldGhostLayers ); + + return handling; + } + +private: + + const BlockDataID flagFieldID_; + const BlockDataID pdfFieldID_; + const BlockDataID particleFieldID_; + + shared_ptr<ParticleAccessor> ac_; + +}; + +template <typename BoundaryHandling_T> +class MappingChecker +{ +public: + MappingChecker(const shared_ptr< StructuredBlockStorage > & blocks, + const BlockDataID & boundaryHandlingID, real_t sphereRadius) : + blocks_( blocks ), boundaryHandlingID_( boundaryHandlingID ), + sphereRadius_( sphereRadius ), sphereVolume_( math::pi * real_t(4) / real_t(3) * sphereRadius * sphereRadius * sphereRadius ) + { + WALBERLA_ASSERT(blocks->isXPeriodic()); + } + + // check the mapping in the inner domain of the block and check mapped volume against real sphere volume + void operator()(std::string & testIdentifier, const Vector3<real_t> & pos, bool periodic ) + { + uint_t cellCounter( uint_t(0) ); + for( auto blockIt = blocks_->begin(); blockIt != blocks_->end(); ++blockIt ) + { + auto * boundaryHandling = blockIt->getData< BoundaryHandling_T >( boundaryHandlingID_ ); + auto * flagField = boundaryHandling->getFlagField(); + + auto xyzSize = flagField->xyzSize(); + + for (auto cellIt : xyzSize) { + Vector3<real_t> cellCenter = blocks_->getBlockLocalCellCenter(*blockIt, cellIt); + real_t distance = (cellCenter - pos).length(); + if( periodic ) + { + Vector3<real_t> periodicOffset(blocks_->getDomain().xSize(),0,0); + // check if other (periodic) copies are closer + distance = std::min(distance, (cellCenter - (pos-periodicOffset)).length()); + distance = std::min(distance, (cellCenter - (pos+periodicOffset)).length()); + } + + if (distance < sphereRadius_) { + WALBERLA_CHECK(boundaryHandling->isBoundary(cellIt), + testIdentifier << "Invalid mapping in cell " << cellIt + << " with center at " << cellCenter + << " - expected boundary cell. Distance cell center - particle center = " + << distance << "."); + ++cellCounter; + } else { + WALBERLA_CHECK(boundaryHandling->isDomain(cellIt), + testIdentifier << "Invalid mapping in cell " << cellIt + << " with center at " << cellCenter + << " - expected domain cell. Distance cell center - particle center = " + << distance << "."); + } + } + } + + mpi::allReduceInplace(cellCounter, mpi::SUM); + + // mapped volume has to be - approximately - the same as the real volume + real_t mappedVolume = real_c(cellCounter); // dx=1 + WALBERLA_CHECK(std::fabs( mappedVolume - sphereVolume_ ) / sphereVolume_ <= real_t(0.1), + "Mapped volume " << mappedVolume << " does not fit to real sphere volume " << sphereVolume_ << "."); + } + + // checks only the mapping in the ghost layers + void checkGhostLayer(std::string & testIdentifier, const Vector3<real_t> & pos, bool periodic ) + { + for( auto blockIt = blocks_->begin(); blockIt != blocks_->end(); ++blockIt ) + { + auto * boundaryHandling = blockIt->getData< BoundaryHandling_T >( boundaryHandlingID_ ); + auto * flagField = boundaryHandling->getFlagField(); + + auto xyzSizeWGl = flagField->xyzSizeWithGhostLayer(); + + for (auto cellIt : xyzSizeWGl) { + if( flagField->isInInnerPart(cellIt) ) continue; + + Vector3<real_t> cellCenter = blocks_->getBlockLocalCellCenter( *blockIt, cellIt); + real_t distance = (cellCenter - pos).length(); + if( periodic ) + { + Vector3<real_t> periodicOffset(blocks_->getDomain().xSize(),0,0); + // check if other (periodic) copies are closer + distance = std::min(distance, (cellCenter - (pos-periodicOffset)).length()); + distance = std::min(distance, (cellCenter - (pos+periodicOffset)).length()); + } + + if( distance < sphereRadius_ ) + { + WALBERLA_CHECK( boundaryHandling->isBoundary(cellIt), + testIdentifier << ": Invalid mapping in ghost layer cell " << cellIt + << " with center at " << cellCenter + << " - expected boundary cell. Distance cell center - body center = " + << distance << "."); + } + else + { + WALBERLA_CHECK( boundaryHandling->isDomain(cellIt), + testIdentifier << ": Invalid mapping in ghost layer cell " << cellIt + << " with center at " << cellCenter + << " - expected domain cell. Distance cell center - body center = " + << distance << "." ); + } + } + } + } + + +private: + shared_ptr< StructuredBlockStorage > blocks_; + const BlockDataID boundaryHandlingID_; + real_t sphereRadius_, sphereVolume_; + +}; + +template< typename BoundaryHandling_T> +class MappingResetter +{ +public: + MappingResetter(const shared_ptr< StructuredBlockStorage > & blocks, const BlockDataID & boundaryHandlingID, const BlockDataID & particleFieldID, walberla::id_t invalidUID) : + blocks_( blocks ), boundaryHandlingID_( boundaryHandlingID ), particleFieldID_( particleFieldID ), invalidUID_( invalidUID ) + { } + + void operator()() + { + for( auto blockIt = blocks_->begin(); blockIt != blocks_->end(); ++blockIt ) + { + auto * boundaryHandling = blockIt->getData< BoundaryHandling_T >( boundaryHandlingID_ ); + auto * flagField = boundaryHandling->getFlagField(); + auto * particleField = blockIt->getData< lbm_mesapd_coupling::ParticleField_T >( particleFieldID_ ); + + auto xyzSizeWGl = flagField->xyzSizeWithGhostLayer(); + // reset to domain (fluid) + boundaryHandling->forceDomain(xyzSizeWGl); + + for( auto cellIt = xyzSizeWGl.begin(); cellIt != xyzSizeWGl.end(); ++cellIt ) + { + // reset body field + particleField->get(*cellIt) = invalidUID_; + } + } + } + +private: + shared_ptr< StructuredBlockStorage > blocks_; + const BlockDataID boundaryHandlingID_; + const BlockDataID particleFieldID_; + walberla::id_t invalidUID_; +}; + + + +/*!\brief Test case for the particle mapping functionality + * + * The following mapping functions are tested: + * - RegularParticlesSelector + * - GlobalParticlesSelector + * - FixedParticlesSelector + * The mapping inside a block, at a regular block boarder and at a periodic block boarder is tested. + * Regular, global and infinite-mass spheres are tested. + * After each test, the sphere is destroyed and the mapping is erased from the datastructures. + * + * The setup is as follows (x marks the different sphere positions, # is a periodic border) + * ---------------------------------------------------------------------- + * # | | # + * # | | # + * # | | # + * # | | # + * #X X X| | # + * # | | # + * # | | # + * # | | # + * # | | # + * ---------------------------------------------------------------------- + */ + + +////////// +// MAIN // +////////// +int main( int argc, char **argv ) +{ + debug::enterTestMode(); + + mpi::Environment env( argc, argv ); + + WALBERLA_CHECK(mpi::MPIManager::instance()->numProcesses()==3, "Due to periodicity, three processes have to be used!"); + + /////////////////////////// + // SIMULATION PROPERTIES // + /////////////////////////// + + bool writeVTK = false; + const real_t omega = real_t(1); + const real_t dx = real_t(1); + const real_t radius = real_t(5); + + /////////////////////////// + // DATA STRUCTURES SETUP // + /////////////////////////// + + Vector3<uint_t> blocksPerDirection(uint_t(3), uint_t(1), uint_t(1)); + Vector3<uint_t> cellsPerBlock(uint_t(20), uint_t(20), uint_t(20)); + Vector3<bool> periodicity(true, false, false); + + auto blocks = blockforest::createUniformBlockGrid( blocksPerDirection[0], blocksPerDirection[1], blocksPerDirection[2], + cellsPerBlock[0], cellsPerBlock[1], cellsPerBlock[2], + dx, + 1, false, false, + periodicity[0], periodicity[1], periodicity[2], + false ); + + // create the lattice model + LatticeModel_T latticeModel = LatticeModel_T( omega ); + + // add PDF field + BlockDataID pdfFieldID = lbm::addPdfFieldToStorage< LatticeModel_T >( blocks, "pdf field (zyxf)", latticeModel, + Vector3<real_t>(real_t(0)), real_t(1), + FieldGhostLayers, field::zyxf ); + + // add flag field + BlockDataID flagFieldID = field::addFlagFieldToStorage<FlagField_T>( blocks, "flag field", FieldGhostLayers ); + + // rpd setup + mesa_pd::domain::BlockForestDomain domain(blocks->getBlockForestPointer()); + + //init data structures + auto ps = std::make_shared<mesa_pd::data::ParticleStorage>(1); + auto ss = std::make_shared<mesa_pd::data::ShapeStorage>(); + using ParticleAccessor_T = mesa_pd::data::ParticleAccessorWithShape; + shared_ptr<ParticleAccessor_T> accessor = make_shared<ParticleAccessor_T>(ps, ss); + auto sphereShape = ss->create<mesa_pd::data::Sphere>( radius ); + mesa_pd::mpi::SyncNextNeighbors syncNextNeighborFunc; + + // coupling + const real_t overlap = real_t( 1.5 ) * dx; + + // add particle field + BlockDataID particleFieldID = field::addToStorage<lbm_mesapd_coupling::ParticleField_T>( blocks, "particle field", accessor->getInvalidUid(), field::zyxf, FieldGhostLayers ); + + // add boundary handling + using BoundaryHandling_T = MyBoundaryHandling<ParticleAccessor_T>::Type; + BlockDataID boundaryHandlingID = blocks->addStructuredBlockData< BoundaryHandling_T >(MyBoundaryHandling<ParticleAccessor_T>( flagFieldID, pdfFieldID, particleFieldID, accessor), "boundary handling" ); + + // vtk + auto flagFieldVTK = vtk::createVTKOutput_BlockData( blocks, "flag_field" ); + flagFieldVTK->addCellDataWriter( make_shared< field::VTKWriter< FlagField_T > >( flagFieldID, "FlagField" ) ); + + // testing utilities + MappingChecker<BoundaryHandling_T> mappingChecker(blocks, boundaryHandlingID, radius); + MappingResetter<BoundaryHandling_T> mappingResetter(blocks, boundaryHandlingID, particleFieldID, accessor->getInvalidUid()); + + // mapping functors + auto regularParticleMapper = lbm_mesapd_coupling::makeMovingParticleMapping<PdfField_T, BoundaryHandling_T>(blocks, pdfFieldID, boundaryHandlingID, particleFieldID, accessor, MO_Flag, FormerMO_Flag, lbm_mesapd_coupling::RegularParticlesSelector(), true ); + auto globalParticleMapper = lbm_mesapd_coupling::makeMovingParticleMapping<PdfField_T, BoundaryHandling_T>(blocks, pdfFieldID, boundaryHandlingID, particleFieldID, accessor, MO_Flag, FormerMO_Flag, lbm_mesapd_coupling::GlobalParticlesSelector(), true ); + + lbm_mesapd_coupling::MovingParticleMappingKernel<BoundaryHandling_T> movingParticleMappingKernel(blocks, boundaryHandlingID, particleFieldID); + + // sphere positions for test scenarios + Vector3<real_t> positionInsideBlock(real_t(10), real_t(10), real_t(10)); + Vector3<real_t> positionAtBlockBoarder(real_t(19), real_t(10), real_t(10)); + Vector3<real_t> positionAtPeriodicBoarder(real_t(1), real_t(10), real_t(10)); + + ////////////////////////// + // MOVING BODY MAPPING // + ///////////////////////// + + ////////////////////////////////// + // TEST SPHERE INSIDE ONE BLOCK // + ////////////////////////////////// + + ////////////////////// + // REGULAR SPHERE 1 // + ////////////////////// + { + std::string testIdentifier("Test: regular sphere inside block with moving particle mapping, case 1 "); + WALBERLA_LOG_DEVEL_ON_ROOT(testIdentifier << " - started"); + + // create sphere + if (domain.isContainedInProcessSubdomain( uint_c(mpi::MPIManager::instance()->rank()), positionInsideBlock )) + { + mesa_pd::data::Particle&& p = *ps->create(); + p.setPosition(positionInsideBlock); + p.setInteractionRadius(radius); + p.setOwner(mpi::MPIManager::instance()->rank()); + p.setShapeID(sphereShape); + } + syncNextNeighborFunc(*ps, domain, overlap); + + // map + ps->forEachParticle(false, lbm_mesapd_coupling::RegularParticlesSelector(), *accessor, movingParticleMappingKernel, *accessor, MO_Flag); + + if( writeVTK ) flagFieldVTK->write(); + + // check mapping + mappingChecker(testIdentifier,positionInsideBlock,false); + mappingChecker.checkGhostLayer(testIdentifier,positionInsideBlock,false); + mappingResetter(); + + WALBERLA_LOG_DEVEL_ON_ROOT(testIdentifier << " - ended successfully"); + + // clean up + ps->clear(); + syncNextNeighborFunc(*ps, domain, overlap); + } + + ////////////////////// + // REGULAR SPHERE 2 // + ////////////////////// + { + std::string testIdentifier("Test: regular sphere inside block with moving particle mapping, case 2 "); + WALBERLA_LOG_DEVEL_ON_ROOT(testIdentifier << " - started"); + + // create sphere + if (domain.isContainedInProcessSubdomain( uint_c(mpi::MPIManager::instance()->rank()), positionInsideBlock )) + { + mesa_pd::data::Particle&& p = *ps->create(); + p.setPosition(positionInsideBlock); + p.setInteractionRadius(radius); + p.setOwner(mpi::MPIManager::instance()->rank()); + p.setShapeID(sphereShape); + } + syncNextNeighborFunc(*ps, domain, overlap); + + // map + for( auto blockIt = blocks->begin(); blockIt != blocks->end(); ++blockIt ) regularParticleMapper(&(*blockIt)); + + if( writeVTK ) flagFieldVTK->write(); + + // check mapping + mappingChecker(testIdentifier,positionInsideBlock,false); + mappingChecker.checkGhostLayer(testIdentifier,positionInsideBlock,false); + mappingResetter(); + + WALBERLA_LOG_DEVEL_ON_ROOT(testIdentifier << " - ended successfully"); + + // clean up + ps->clear(); + syncNextNeighborFunc(*ps, domain, overlap); + } + + ///////////////////// + // GLOBAL SPHERE 1 // + ///////////////////// + { + std::string testIdentifier("Test: global sphere inside block with moving particle mapping, case 1 "); + WALBERLA_LOG_DEVEL_ON_ROOT(testIdentifier << " - started"); + + // create sphere + mesa_pd::data::Particle&& p = *ps->create(true); + p.setPosition(positionInsideBlock); + p.setInteractionRadius(radius); + p.setOwner(mpi::MPIManager::instance()->rank()); + p.setShapeID(sphereShape); + + syncNextNeighborFunc(*ps, domain, overlap); + + // map + ps->forEachParticle(false, lbm_mesapd_coupling::GlobalParticlesSelector(), *accessor, movingParticleMappingKernel, *accessor, MO_Flag); + + if( writeVTK ) flagFieldVTK->write(); + + // check mapping + mappingChecker(testIdentifier,positionInsideBlock,false); + mappingChecker.checkGhostLayer(testIdentifier,positionInsideBlock,false); + mappingResetter(); + + WALBERLA_LOG_DEVEL_ON_ROOT(testIdentifier << " - ended successfully"); + + // clean up + ps->clear(); + syncNextNeighborFunc(*ps, domain, overlap); + } + + ///////////////////// + // GLOBAL SPHERE 2 // + ///////////////////// + { + std::string testIdentifier("Test: global sphere inside block with moving particle mapping, case 2 "); + WALBERLA_LOG_DEVEL_ON_ROOT(testIdentifier << " - started"); + + // create sphere + mesa_pd::data::Particle&& p = *ps->create(true); + p.setPosition(positionInsideBlock); + p.setInteractionRadius(radius); + p.setOwner(mpi::MPIManager::instance()->rank()); + p.setShapeID(sphereShape); + + syncNextNeighborFunc(*ps, domain, overlap); + + // map + for( auto blockIt = blocks->begin(); blockIt != blocks->end(); ++blockIt ) globalParticleMapper(&(*blockIt)); + + if( writeVTK ) flagFieldVTK->write(); + + // check mapping + mappingChecker(testIdentifier,positionInsideBlock,false); + mappingChecker.checkGhostLayer(testIdentifier,positionInsideBlock,false); + mappingResetter(); + + WALBERLA_LOG_DEVEL_ON_ROOT(testIdentifier << " - ended successfully"); + + // clean up + ps->clear(); + syncNextNeighborFunc(*ps, domain, overlap); + } + + ////////////////////////////////// + // TEST SPHERE AT BLOCK BOARDER // + ////////////////////////////////// + + ////////////////////// + // REGULAR SPHERE 1 // + ////////////////////// + { + std::string testIdentifier("Test: regular sphere at block boarder with moving particle mapping, case 1 "); + WALBERLA_LOG_DEVEL_ON_ROOT(testIdentifier << " - started"); + + // create sphere + if (domain.isContainedInProcessSubdomain( uint_c(mpi::MPIManager::instance()->rank()), positionAtBlockBoarder )) + { + mesa_pd::data::Particle&& p = *ps->create(); + p.setPosition(positionAtBlockBoarder); + p.setInteractionRadius(radius); + p.setOwner(mpi::MPIManager::instance()->rank()); + p.setShapeID(sphereShape); + } + syncNextNeighborFunc(*ps, domain, overlap); + + // map + ps->forEachParticle(false, lbm_mesapd_coupling::RegularParticlesSelector(), *accessor, movingParticleMappingKernel, *accessor, MO_Flag); + + if( writeVTK ) flagFieldVTK->write(); + + // check mapping + mappingChecker(testIdentifier,positionAtBlockBoarder,false); + mappingChecker.checkGhostLayer(testIdentifier,positionAtBlockBoarder,false); + mappingResetter(); + + WALBERLA_LOG_DEVEL_ON_ROOT(testIdentifier << " - ended successfully"); + + // clean up + ps->clear(); + syncNextNeighborFunc(*ps, domain, overlap); + } + + ////////////////////// + // REGULAR SPHERE 2 // + ////////////////////// + { + std::string testIdentifier("Test: regular sphere at block boarder with moving particle mapping, case 2 "); + WALBERLA_LOG_DEVEL_ON_ROOT(testIdentifier << " - started"); + + // create sphere + if (domain.isContainedInProcessSubdomain( uint_c(mpi::MPIManager::instance()->rank()), positionAtBlockBoarder )) + { + mesa_pd::data::Particle&& p = *ps->create(); + p.setPosition(positionAtBlockBoarder); + p.setInteractionRadius(radius); + p.setOwner(mpi::MPIManager::instance()->rank()); + p.setShapeID(sphereShape); + } + syncNextNeighborFunc(*ps, domain, overlap); + + // map + for( auto blockIt = blocks->begin(); blockIt != blocks->end(); ++blockIt ) regularParticleMapper(&(*blockIt)); + + if( writeVTK ) flagFieldVTK->write(); + + // check mapping + mappingChecker(testIdentifier,positionAtBlockBoarder,false); + mappingChecker.checkGhostLayer(testIdentifier,positionAtBlockBoarder,false); + mappingResetter(); + + WALBERLA_LOG_DEVEL_ON_ROOT(testIdentifier << " - ended successfully"); + + // clean up + ps->clear(); + syncNextNeighborFunc(*ps, domain, overlap); + } + + ///////////////////// + // GLOBAL SPHERE 1 // + ///////////////////// + { + std::string testIdentifier("Test: global sphere at block boarder with moving particle mapping, case 1 "); + WALBERLA_LOG_DEVEL_ON_ROOT(testIdentifier << " - started"); + + // create sphere + mesa_pd::data::Particle&& p = *ps->create(true); + p.setPosition(positionAtBlockBoarder); + p.setInteractionRadius(radius); + p.setOwner(mpi::MPIManager::instance()->rank()); + p.setShapeID(sphereShape); + + syncNextNeighborFunc(*ps, domain, overlap); + + // map + ps->forEachParticle(false, lbm_mesapd_coupling::GlobalParticlesSelector(), *accessor, movingParticleMappingKernel, *accessor, MO_Flag); + + if( writeVTK ) flagFieldVTK->write(); + + // check mapping + mappingChecker(testIdentifier,positionAtBlockBoarder,false); + mappingChecker.checkGhostLayer(testIdentifier,positionAtBlockBoarder,false); + mappingResetter(); + + WALBERLA_LOG_DEVEL_ON_ROOT(testIdentifier << " - ended successfully"); + + // clean up + ps->clear(); + syncNextNeighborFunc(*ps, domain, overlap); + } + + ///////////////////// + // GLOBAL SPHERE 2 // + ///////////////////// + { + std::string testIdentifier("Test: global sphere at block boarder with moving particle mapping, case 2 "); + WALBERLA_LOG_DEVEL_ON_ROOT(testIdentifier << " - started"); + + // create sphere + mesa_pd::data::Particle&& p = *ps->create(true); + p.setPosition(positionAtBlockBoarder); + p.setInteractionRadius(radius); + p.setOwner(mpi::MPIManager::instance()->rank()); + p.setShapeID(sphereShape); + + syncNextNeighborFunc(*ps, domain, overlap); + + // map + for( auto blockIt = blocks->begin(); blockIt != blocks->end(); ++blockIt ) globalParticleMapper(&(*blockIt)); + + if( writeVTK ) flagFieldVTK->write(); + + // check mapping + mappingChecker(testIdentifier,positionAtBlockBoarder,false); + mappingChecker.checkGhostLayer(testIdentifier,positionAtBlockBoarder,false); + mappingResetter(); + + WALBERLA_LOG_DEVEL_ON_ROOT(testIdentifier << " - ended successfully"); + + // clean up + ps->clear(); + syncNextNeighborFunc(*ps, domain, overlap); + } + + ///////////////////////////////////// + // TEST SPHERE AT PERIODIC BOARDER // + ///////////////////////////////////// + + ////////////////////// + // REGULAR SPHERE 1 // + ////////////////////// + { + std::string testIdentifier("Test: regular sphere at periodic boarder with moving particle mapping, case 1 "); + WALBERLA_LOG_DEVEL_ON_ROOT(testIdentifier << " - started"); + + // create sphere + if (domain.isContainedInProcessSubdomain( uint_c(mpi::MPIManager::instance()->rank()), positionAtPeriodicBoarder )) + { + mesa_pd::data::Particle&& p = *ps->create(); + p.setPosition(positionAtPeriodicBoarder); + p.setInteractionRadius(radius); + p.setOwner(mpi::MPIManager::instance()->rank()); + p.setShapeID(sphereShape); + } + syncNextNeighborFunc(*ps, domain, overlap); + + // map + ps->forEachParticle(false, lbm_mesapd_coupling::RegularParticlesSelector(), *accessor, movingParticleMappingKernel, *accessor, MO_Flag); + + if( writeVTK ) flagFieldVTK->write(); + + // check mapping + mappingChecker(testIdentifier,positionAtPeriodicBoarder,true); + mappingChecker.checkGhostLayer(testIdentifier,positionAtPeriodicBoarder,true); + mappingResetter(); + + WALBERLA_LOG_DEVEL_ON_ROOT(testIdentifier << " - ended successfully"); + + // clean up + ps->clear(); + syncNextNeighborFunc(*ps, domain, overlap); + } + + ////////////////////// + // REGULAR SPHERE 2 // + ////////////////////// + { + std::string testIdentifier("Test: regular sphere at periodic boarder with moving particle mapping, case 2 "); + WALBERLA_LOG_DEVEL_ON_ROOT(testIdentifier << " - started"); + + // create sphere + if (domain.isContainedInProcessSubdomain( uint_c(mpi::MPIManager::instance()->rank()), positionAtPeriodicBoarder )) + { + mesa_pd::data::Particle&& p = *ps->create(); + p.setPosition(positionAtPeriodicBoarder); + p.setInteractionRadius(radius); + p.setOwner(mpi::MPIManager::instance()->rank()); + p.setShapeID(sphereShape); + } + syncNextNeighborFunc(*ps, domain, overlap); + + // map + for( auto blockIt = blocks->begin(); blockIt != blocks->end(); ++blockIt ) regularParticleMapper(&(*blockIt)); + + if( writeVTK ) flagFieldVTK->write(); + + // check mapping + mappingChecker(testIdentifier,positionAtPeriodicBoarder,true); + mappingChecker.checkGhostLayer(testIdentifier,positionAtPeriodicBoarder,true); + mappingResetter(); + + WALBERLA_LOG_DEVEL_ON_ROOT(testIdentifier << " - ended successfully"); + + // clean up + ps->clear(); + syncNextNeighborFunc(*ps, domain, overlap); + } + + ///////////////////// + // GLOBAL SPHERE 1 // + ///////////////////// + + // left out since periodicity does not affect global particles + + return 0; + +} + +} //namespace moving_particle_mapping + +int main( int argc, char **argv ){ + moving_particle_mapping::main(argc, argv); +} diff --git a/tests/lbm_mesapd_coupling/momentum_exchange_method/PdfReconstruction.cpp b/tests/lbm_mesapd_coupling/momentum_exchange_method/PdfReconstruction.cpp new file mode 100644 index 0000000000000000000000000000000000000000..c280a45f84cd5a7f8f79212381591ed732875c1a --- /dev/null +++ b/tests/lbm_mesapd_coupling/momentum_exchange_method/PdfReconstruction.cpp @@ -0,0 +1,922 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla is free software: you can +// redistribute it and/or modify it under the terms of the GNU General Public +// License as published by the Free Software Foundation, either version 3 of +// the License, or (at your option) any later version. +// +// waLBerla is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +// for more details. +// +// You should have received a copy of the GNU General Public License along +// with waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file PDFReconstruction.cpp +//! \ingroup lbm_mesapd_coupling +//! \author Christoph Rettinger <christoph.rettinger@fau.de> +// +//====================================================================================================================== + +#include "blockforest/Initialization.h" +#include "blockforest/communication/UniformBufferedScheme.h" + +#include "boundary/all.h" + +#include "core/DataTypes.h" +#include "core/Environment.h" +#include "core/debug/Debug.h" +#include "core/debug/TestSubsystem.h" +#include "core/math/all.h" +#include "core/mpi/MPIManager.h" +#include "core/mpi/Reduce.h" + +#include "field/AddToStorage.h" + +#include "lbm/boundary/all.h" +#include "lbm/field/AddToStorage.h" +#include "lbm/field/PdfField.h" +#include "lbm/lattice_model/D3Q19.h" + +#include "lbm_mesapd_coupling/momentum_exchange_method/MovingParticleMapping.h" +#include "lbm_mesapd_coupling/momentum_exchange_method/boundary/SimpleBB.h" +#include "lbm_mesapd_coupling/momentum_exchange_method/reconstruction/PdfReconstructionManager.h" +#include "lbm_mesapd_coupling/momentum_exchange_method/reconstruction/Reconstructor.h" +#include "lbm_mesapd_coupling/momentum_exchange_method/reconstruction/ExtrapolationDirectionFinder.h" +#include "lbm_mesapd_coupling/utility/ParticleSelector.h" +#include "lbm_mesapd_coupling/DataTypes.h" + +#include "mesa_pd/data/DataTypes.h" +#include "mesa_pd/data/ParticleAccessorWithShape.h" +#include "mesa_pd/data/ParticleStorage.h" +#include "mesa_pd/data/ShapeStorage.h" +#include "mesa_pd/domain/BlockForestDomain.h" +#include "mesa_pd/kernel/SingleCast.h" +#include "mesa_pd/kernel/ParticleSelector.h" +#include "mesa_pd/mpi/SyncNextNeighbors.h" + +#include "vtk/all.h" +#include "field/vtk/all.h" + +namespace pdf_reconstruction +{ + +/////////// +// USING // +/////////// + +using namespace walberla; +using walberla::uint_t; + +using LatticeModel_T = lbm::D3Q19<lbm::collision_model::SRT>; + +using Stencil_T = LatticeModel_T::Stencil; +using PdfField_T = lbm::PdfField<LatticeModel_T>; + +const uint_t FieldGhostLayers = 1; + +using flag_t = walberla::uint8_t; +using FlagField_T = FlagField<flag_t>; + + +/////////// +// FLAGS // +/////////// + +const FlagUID Fluid_Flag ( "fluid" ); +const FlagUID MO_Flag ( "moving obstacle" ); +const FlagUID FormerMO_Flag ( "former moving obstacle" ); + + +///////////////////////////////////// +// BOUNDARY HANDLING CUSTOMIZATION // +///////////////////////////////////// + +template <typename ParticleAccessor> +class MyBoundaryHandling +{ +public: + + using MO_T = lbm_mesapd_coupling::SimpleBB< LatticeModel_T, FlagField_T, ParticleAccessor >; + using Type = BoundaryHandling< FlagField_T, Stencil_T, MO_T>; + + MyBoundaryHandling( const BlockDataID & flagFieldID, const BlockDataID & pdfFieldID, + const BlockDataID & particleFieldID, const shared_ptr<ParticleAccessor>& ac) : + flagFieldID_( flagFieldID ), pdfFieldID_( pdfFieldID ), particleFieldID_( particleFieldID ), ac_( ac ) {} + + Type * operator()( IBlock* const block, const StructuredBlockStorage* const storage ) const + { + WALBERLA_ASSERT_NOT_NULLPTR( block ); + WALBERLA_ASSERT_NOT_NULLPTR( storage ); + + auto * flagField = block->getData< FlagField_T >( flagFieldID_ ); + auto * pdfField = block->getData< PdfField_T > ( pdfFieldID_ ); + auto * particleField = block->getData< lbm_mesapd_coupling::ParticleField_T > ( particleFieldID_ ); + + const auto fluid = flagField->flagExists( Fluid_Flag ) ? flagField->getFlag( Fluid_Flag ) : flagField->registerFlag( Fluid_Flag ); + + Type * handling = new Type( "moving obstacle boundary handling", flagField, fluid, + MO_T("MO_BB", MO_Flag, pdfField, flagField, particleField, ac_, fluid, *storage, *block ) ); + + handling->fillWithDomain( FieldGhostLayers ); + + return handling; + } + +private: + + const BlockDataID flagFieldID_; + const BlockDataID pdfFieldID_; + const BlockDataID particleFieldID_; + + shared_ptr<ParticleAccessor> ac_; + +}; + +template< typename PdfField_T, typename BoundaryHandling_T> +class ReconstructionChecker +{ +public: + ReconstructionChecker(const shared_ptr< StructuredBlockStorage > & blocks,const BlockDataID & pdfFieldID, + const BlockDataID & boundaryHandlingID, Vector3<real_t> expectedVelocity, real_t expectedDensity, + const FlagUID formerObstacle) : + blocks_( blocks ), pdfFieldID_( pdfFieldID ), boundaryHandlingID_ (boundaryHandlingID ), expectedVelocity_( expectedVelocity ), expectedDensity_( expectedDensity ), formerObstacle_(formerObstacle) + { } + + void operator()(std::string testIdentifier) + { + for( auto blockIt = blocks_->begin(); blockIt != blocks_->end(); ++blockIt ) { + auto *pdfField = blockIt->getData<PdfField_T>(pdfFieldID_); + auto *boundaryHandling = blockIt->getData<BoundaryHandling_T>(boundaryHandlingID_); + auto *flagField = boundaryHandling->getFlagField(); + + auto xyzSize = pdfField->xyzSize(); + + const flag_t formerObstacle = flagField->getFlag( formerObstacle_ ); + + for (auto cellIt : xyzSize) { + WALBERLA_CHECK(!isFlagSet(flagField->get(cellIt), formerObstacle),testIdentifier << "Found incorrect formerObstacle flag in cell " << cellIt << ".") + + if(boundaryHandling->isDomain(cellIt)) + { + auto velocity = pdfField->getVelocity(cellIt); + WALBERLA_CHECK_FLOAT_EQUAL(velocity[0], expectedVelocity_[0],testIdentifier << "Invalid velocity X in cell " << cellIt << "."); + WALBERLA_CHECK_FLOAT_EQUAL(velocity[1], expectedVelocity_[1],testIdentifier << "Invalid velocity Y in cell " << cellIt << "."); + WALBERLA_CHECK_FLOAT_EQUAL(velocity[2], expectedVelocity_[2],testIdentifier << "Invalid velocity Z in cell " << cellIt << "."); + + auto density = pdfField->getDensity(cellIt); + WALBERLA_CHECK_FLOAT_EQUAL(density, expectedDensity_,testIdentifier << "Invalid density in cell " << cellIt << "."); + } + } + } + } + +private: + shared_ptr< StructuredBlockStorage > blocks_; + const BlockDataID pdfFieldID_; + const BlockDataID boundaryHandlingID_; + + Vector3<real_t> expectedVelocity_; + real_t expectedDensity_; + + const FlagUID formerObstacle_; + +}; + + + +template <typename BoundaryHandling_T> +class MappingChecker +{ +public: + MappingChecker(const shared_ptr< StructuredBlockStorage > & blocks, + const BlockDataID & boundaryHandlingID, real_t sphereRadius) : + blocks_( blocks ), boundaryHandlingID_( boundaryHandlingID ), + sphereRadius_( sphereRadius ), sphereVolume_( math::pi * real_t(4) / real_t(3) * sphereRadius * sphereRadius * sphereRadius ) + { } + + // check the mapping in the inner domain of the block and check mapped volume against real sphere volume + void operator()(std::string testIdentifier, const Vector3<real_t> & pos, bool periodic ) + { + uint_t cellCounter( uint_t(0) ); + + for( auto blockIt = blocks_->begin(); blockIt != blocks_->end(); ++blockIt ) + { + + auto * boundaryHandling = blockIt->getData< BoundaryHandling_T >( boundaryHandlingID_ ); + auto * flagField = boundaryHandling->getFlagField(); + + auto xyzSize = flagField->xyzSize(); + + for (auto cellIt : xyzSize) { + Vector3<real_t> cellCenter = blocks_->getBlockLocalCellCenter(*blockIt, cellIt); + real_t distance = (cellCenter - pos).length(); + if( periodic ) + { + Vector3<real_t> periodicOffset(blocks_->getDomain().xSize(),0,0); + // check if other (periodic) copies are closer + distance = std::min(distance, (cellCenter - (pos-periodicOffset)).length()); + distance = std::min(distance, (cellCenter - (pos+periodicOffset)).length()); + } + + if (distance < sphereRadius_) { + WALBERLA_CHECK(boundaryHandling->isBoundary(cellIt), + testIdentifier << "Invalid mapping in cell " << cellIt + << " with center at " << cellCenter + << " - expected boundary cell. Distance cell center - particle center = " + << distance << "."); + ++cellCounter; + } else { + WALBERLA_CHECK(boundaryHandling->isDomain(cellIt), + testIdentifier << "Invalid mapping in cell " << cellIt + << " with center at " << cellCenter + << " - expected domain cell. Distance cell center - particle center = " + << distance << "."); + } + } + } + + mpi::allReduceInplace(cellCounter, mpi::SUM); + + // mapped volume has to be - approximately - the same as the real volume + real_t mappedVolume = real_c(cellCounter); // dx=1 + WALBERLA_CHECK(std::fabs( mappedVolume - sphereVolume_ ) / sphereVolume_ <= real_t(0.1), + "Mapped volume " << mappedVolume << " does not fit to real sphere volume " << sphereVolume_ << "."); + } + + // checks only the mapping in the ghost layers + void checkGhostLayer(std::string testIdentifier, const Vector3<real_t> & pos, bool periodic ) + { + for( auto blockIt = blocks_->begin(); blockIt != blocks_->end(); ++blockIt ) + { + auto * boundaryHandling = blockIt->getData< BoundaryHandling_T >( boundaryHandlingID_ ); + auto * flagField = boundaryHandling->getFlagField(); + + auto xyzSizeWGl = flagField->xyzSizeWithGhostLayer(); + + for (auto cellIt : xyzSizeWGl) { + if( flagField->isInInnerPart(cellIt) ) continue; + + Vector3<real_t> cellCenter = blocks_->getBlockLocalCellCenter( *blockIt, cellIt); + real_t distance = (cellCenter - pos).length(); + if( periodic ) + { + Vector3<real_t> periodicOffset(blocks_->getDomain().xSize(),0,0); + // check if other (periodic) copies are closer + distance = std::min(distance, (cellCenter - (pos-periodicOffset)).length()); + distance = std::min(distance, (cellCenter - (pos+periodicOffset)).length()); + } + + if( distance < sphereRadius_ ) + { + WALBERLA_CHECK( boundaryHandling->isBoundary(cellIt), + testIdentifier << ": Invalid mapping in ghost layer cell " << cellIt + << " with center at " << cellCenter + << " - expected boundary cell. Distance cell center - particle center = " + << distance << "."); + } + else + { + WALBERLA_CHECK( boundaryHandling->isDomain(cellIt), + testIdentifier << ": Invalid mapping in ghost layer cell " << cellIt + << " with center at " << cellCenter + << " - expected domain cell. Distance cell center - particle center = " + << distance << "." ); + } + } + } + } + + +private: + shared_ptr< StructuredBlockStorage > blocks_; + const BlockDataID boundaryHandlingID_; + real_t sphereRadius_, sphereVolume_; + +}; + +template< typename BoundaryHandling_T> +class MappingResetter +{ +public: + MappingResetter(const shared_ptr< StructuredBlockStorage > & blocks, const BlockDataID & boundaryHandlingID, const BlockDataID & particleFieldID, walberla::id_t invalidUID) : + blocks_( blocks ), boundaryHandlingID_( boundaryHandlingID ), particleFieldID_( particleFieldID ), invalidUID_( invalidUID ) + { } + + void operator()() + { + for( auto blockIt = blocks_->begin(); blockIt != blocks_->end(); ++blockIt ) + { + auto * boundaryHandling = blockIt->getData< BoundaryHandling_T >( boundaryHandlingID_ ); + auto * flagField = boundaryHandling->getFlagField(); + auto * particleField = blockIt->getData< lbm_mesapd_coupling::ParticleField_T >( particleFieldID_ ); + + auto xyzSizeWGl = flagField->xyzSizeWithGhostLayer(); + // reset to domain (fluid) + boundaryHandling->forceDomain(xyzSizeWGl); + + for( auto cellIt = xyzSizeWGl.begin(); cellIt != xyzSizeWGl.end(); ++cellIt ) + { + // reset particle field + particleField->get(*cellIt) = invalidUID_; + } + } + } + +private: + shared_ptr< StructuredBlockStorage > blocks_; + const BlockDataID boundaryHandlingID_; + const BlockDataID particleFieldID_; + walberla::id_t invalidUID_; +}; + + +template<typename BoundaryHandling_T, typename ParticleAccessor_T, typename ExtrapolationDirectionFinder_T> +void checkExtrapolationDirectionFinder( std::string identifier, + shared_ptr< StructuredBlockForest > & blocks, shared_ptr<mesa_pd::data::ParticleStorage> & ps, shared_ptr<mesa_pd::data::ShapeStorage> ss, + shared_ptr<ParticleAccessor_T> & accessor, BlockDataID pdfFieldID, BlockDataID boundaryHandlingID, BlockDataID particleFieldID, + ExtrapolationDirectionFinder_T directionFinder, real_t radius, bool conserveMomentum) +{ + mesa_pd::domain::BlockForestDomain domain(blocks->getBlockForestPointer()); + auto sphereShape = ss->create<mesa_pd::data::Sphere>( radius ); + mesa_pd::mpi::SyncNextNeighbors syncNextNeighborFunc; + const real_t overlap = real_t( 1.5 ); + + MappingResetter<BoundaryHandling_T> mappingResetter(blocks, boundaryHandlingID, particleFieldID, accessor->getInvalidUid()); + auto regularParticleMapper = lbm_mesapd_coupling::makeMovingParticleMapping<PdfField_T, BoundaryHandling_T>(blocks, pdfFieldID, boundaryHandlingID, particleFieldID, accessor, MO_Flag, FormerMO_Flag, mesa_pd::kernel::SelectAll(), conserveMomentum); + + std::string testIdentifier(identifier + " Test:"); + WALBERLA_LOG_DEVEL_ON_ROOT(testIdentifier << " - started"); + + Vector3<real_t> spherePosition(real_t(9.5), real_t(9.5), real_t(9.5)); + + std::array<Vector3<real_t>,3> referenceNormals = {{Vector3<real_t>(1,0,0), Vector3<real_t>(0,-1,0), Vector3<real_t>(-1,0,1)/sqrt(2)}}; + std::array<Vector3<real_t>,3> evaluationPoints = {{spherePosition + referenceNormals[0] * radius, spherePosition + referenceNormals[1] * radius, spherePosition + referenceNormals[2] * radius}}; + + // create sphere + walberla::id_t sphereUid = 0; + if (domain.isContainedInProcessSubdomain( uint_c(mpi::MPIManager::instance()->rank()), spherePosition )) + { + mesa_pd::data::Particle&& p = *ps->create(); + p.setPosition(spherePosition); + p.setInteractionRadius(radius); + p.setOwner(mpi::MPIManager::instance()->rank()); + p.setShapeID(sphereShape); + sphereUid = p.getUid(); + } + + mpi::allReduceInplace(sphereUid, mpi::SUM); + + syncNextNeighborFunc(*ps, domain, overlap); + + // map + for( auto blockIt = blocks->begin(); blockIt != blocks->end(); ++blockIt ) regularParticleMapper(&(*blockIt)); + + // check normal at evaluation points + // since the normal must be aligned with some lattice direction, the normal could be different from the real one + for( auto blockIt = blocks->begin(); blockIt != blocks->end(); ++blockIt ) + { + for( uint_t i = 0; i < evaluationPoints.size(); ++i) + { + auto evalPoint = evaluationPoints[i]; + if( blocks->getAABB((*blockIt).getId()).contains(evalPoint)) + { + auto cell = blocks->getBlockLocalCell(*blockIt,evalPoint); + + size_t idx = accessor->uidToIdx(sphereUid); + if( idx != accessor->getInvalidIdx()) + { + auto normal = directionFinder(&(*blockIt), cell.x(), cell.y(), cell.z(), idx, *accessor).getNormalized(); + for(uint_t comp = 0; comp < 3; ++comp) WALBERLA_CHECK_FLOAT_EQUAL(normal[comp], referenceNormals[i][comp], "Non-matching extrapolation direction for normal component " << comp << " in evaluation point " << evalPoint); + } + } + } + } + + WALBERLA_LOG_DEVEL_ON_ROOT(testIdentifier << " - ended successfully"); + + // clean up + mappingResetter(); + ps->clear(); + syncNextNeighborFunc(*ps, domain, overlap); + + +} + +// TEST SPHERE INSIDE ONE BLOCK +template<typename BoundaryHandling_T, typename ParticleAccessor_T, typename ReconstructionManager_T> +void checkSphereInsideBlock( std::string identifier, + shared_ptr< StructuredBlockForest > & blocks, shared_ptr<mesa_pd::data::ParticleStorage> & ps, shared_ptr<mesa_pd::data::ShapeStorage> ss, + shared_ptr<ParticleAccessor_T> & accessor, BlockDataID pdfFieldID, BlockDataID boundaryHandlingID, BlockDataID particleFieldID, + ReconstructionManager_T reconstructionManager, + real_t radius, Vector3<real_t> velocity, real_t density, bool conserveMomentum ) +{ + + + mesa_pd::domain::BlockForestDomain domain(blocks->getBlockForestPointer()); + auto sphereShape = ss->create<mesa_pd::data::Sphere>( radius ); + mesa_pd::mpi::SyncNextNeighbors syncNextNeighborFunc; + const real_t overlap = real_t( 1.5 ); + + MappingChecker<BoundaryHandling_T> mappingChecker(blocks, boundaryHandlingID, radius); + MappingResetter<BoundaryHandling_T> mappingResetter(blocks, boundaryHandlingID, particleFieldID, accessor->getInvalidUid()); + ReconstructionChecker<PdfField_T, BoundaryHandling_T> reconstructionChecker(blocks, pdfFieldID, boundaryHandlingID, velocity, density, FormerMO_Flag ); + + auto regularParticleMapper = lbm_mesapd_coupling::makeMovingParticleMapping<PdfField_T, BoundaryHandling_T>(blocks, pdfFieldID, boundaryHandlingID, particleFieldID, accessor, MO_Flag, FormerMO_Flag, mesa_pd::kernel::SelectAll(), conserveMomentum); + + std::string testIdentifier(identifier + " Test: sphere inside block"); + WALBERLA_LOG_DEVEL_ON_ROOT(testIdentifier << " - started"); + + Vector3<real_t> positionInsideBlock(real_t(10), real_t(10), real_t(10)); + + // create sphere + walberla::id_t sphereUid = 0; + if (domain.isContainedInProcessSubdomain( uint_c(mpi::MPIManager::instance()->rank()), positionInsideBlock )) + { + mesa_pd::data::Particle&& p = *ps->create(); + p.setPosition(positionInsideBlock); + p.setLinearVelocity(velocity); + p.setInteractionRadius(radius); + p.setOwner(mpi::MPIManager::instance()->rank()); + p.setShapeID(sphereShape); + sphereUid = p.getUid(); + } + + mpi::allReduceInplace(sphereUid, mpi::SUM); + + syncNextNeighborFunc(*ps, domain, overlap); + + // map + for( auto blockIt = blocks->begin(); blockIt != blocks->end(); ++blockIt ) regularParticleMapper(&(*blockIt)); + + auto currentSpherePosition = positionInsideBlock; + // run several "timesteps" and advance the sphere in total one cell + for( uint_t t = 0; t < 10; ++t ) + { + currentSpherePosition = currentSpherePosition + velocity; + + // advance sphere + size_t idx = accessor->uidToIdx(sphereUid); + if( idx != accessor->getInvalidIdx()) + { + accessor->setPosition(idx, currentSpherePosition); + } + syncNextNeighborFunc(*ps, domain, overlap); + + // update mapping + for( auto blockIt = blocks->begin(); blockIt != blocks->end(); ++blockIt ) regularParticleMapper(&(*blockIt)); + + // carry out restore step + for( auto blockIt = blocks->begin(); blockIt != blocks->end(); ++blockIt ) reconstructionManager(&(*blockIt)); + + + // check mapping + mappingChecker(testIdentifier + " (t=" + std::to_string(t)+") ",currentSpherePosition,false); + mappingChecker.checkGhostLayer(testIdentifier + " (t=" + std::to_string(t)+") ",currentSpherePosition,false); + + // check reconstruction + reconstructionChecker(testIdentifier + " (t=" + std::to_string(t)+") "); + } + + WALBERLA_LOG_DEVEL_ON_ROOT(testIdentifier << " - ended successfully"); + + // clean up + mappingResetter(); + ps->clear(); + syncNextNeighborFunc(*ps, domain, overlap); +} + +// TEST SPHERE ON BLOCK BOARDER +template<typename BoundaryHandling_T, typename ParticleAccessor_T, typename ReconstructionManager_T> +void checkSphereOnBlockBoarder( std::string identifier, + shared_ptr< StructuredBlockForest > & blocks, shared_ptr<mesa_pd::data::ParticleStorage> & ps, shared_ptr<mesa_pd::data::ShapeStorage> ss, + shared_ptr<ParticleAccessor_T> & accessor, BlockDataID pdfFieldID, BlockDataID boundaryHandlingID, BlockDataID particleFieldID, + ReconstructionManager_T reconstructionManager, + real_t radius, Vector3<real_t> velocity, real_t density, bool conserveMomentum ) +{ + + + mesa_pd::domain::BlockForestDomain domain(blocks->getBlockForestPointer()); + auto sphereShape = ss->create<mesa_pd::data::Sphere>( radius ); + mesa_pd::mpi::SyncNextNeighbors syncNextNeighborFunc; + const real_t overlap = real_t( 1.5 ); + + MappingChecker<BoundaryHandling_T> mappingChecker(blocks, boundaryHandlingID, radius); + MappingResetter<BoundaryHandling_T> mappingResetter(blocks, boundaryHandlingID, particleFieldID, accessor->getInvalidUid()); + ReconstructionChecker<PdfField_T, BoundaryHandling_T> reconstructionChecker(blocks, pdfFieldID, boundaryHandlingID, velocity, density, FormerMO_Flag ); + + auto regularParticleMapper = lbm_mesapd_coupling::makeMovingParticleMapping<PdfField_T, BoundaryHandling_T>(blocks, pdfFieldID, boundaryHandlingID, particleFieldID, accessor, MO_Flag, FormerMO_Flag, mesa_pd::kernel::SelectAll(), conserveMomentum); + + std::string testIdentifier(identifier + " Test: sphere on block boarder"); + WALBERLA_LOG_DEVEL_ON_ROOT(testIdentifier << " - started"); + + Vector3<real_t> positionAtBlockBoarder(real_t(19.5), real_t(10), real_t(10)); + + // create sphere + walberla::id_t sphereUid = 0; + if (domain.isContainedInProcessSubdomain( uint_c(mpi::MPIManager::instance()->rank()), positionAtBlockBoarder )) + { + mesa_pd::data::Particle&& p = *ps->create(); + p.setPosition(positionAtBlockBoarder); + p.setLinearVelocity(velocity); + p.setInteractionRadius(radius); + p.setOwner(mpi::MPIManager::instance()->rank()); + p.setShapeID(sphereShape); + sphereUid = p.getUid(); + } + syncNextNeighborFunc(*ps, domain, overlap); + + mpi::allReduceInplace(sphereUid, mpi::SUM); + + // map + for( auto blockIt = blocks->begin(); blockIt != blocks->end(); ++blockIt ) regularParticleMapper(&(*blockIt)); + + auto currentSpherePosition = positionAtBlockBoarder; + // run several "timesteps" and advance the sphere in total one cell + for( uint_t t = 0; t < 10; ++t ) + { + currentSpherePosition = currentSpherePosition + velocity; + + // advance sphere + size_t idx = accessor->uidToIdx(sphereUid); + if( idx != accessor->getInvalidIdx()) + { + accessor->setPosition(idx, currentSpherePosition); + } + syncNextNeighborFunc(*ps, domain, overlap); + + // update mapping + for( auto blockIt = blocks->begin(); blockIt != blocks->end(); ++blockIt ) regularParticleMapper(&(*blockIt)); + + // carry out restore step + for( auto blockIt = blocks->begin(); blockIt != blocks->end(); ++blockIt ) reconstructionManager(&(*blockIt)); + + + // check mapping + mappingChecker(testIdentifier + " (t=" + std::to_string(t)+") ",currentSpherePosition,false); + mappingChecker.checkGhostLayer(testIdentifier + " (t=" + std::to_string(t)+") ",currentSpherePosition,false); + + // check reconstruction + reconstructionChecker(testIdentifier + " (t=" + std::to_string(t)+") "); + } + + WALBERLA_LOG_DEVEL_ON_ROOT(testIdentifier << " - ended successfully"); + + // clean up + mappingResetter(); + ps->clear(); + syncNextNeighborFunc(*ps, domain, overlap); +} + +// TEST SPHERE ON BLOCK BOARDER 2 +template<typename BoundaryHandling_T, typename ParticleAccessor_T, typename ReconstructionManager_T> +void checkSphereOnBlockBoarder2( std::string identifier, + shared_ptr< StructuredBlockForest > & blocks, shared_ptr<mesa_pd::data::ParticleStorage> & ps, shared_ptr<mesa_pd::data::ShapeStorage> ss, + shared_ptr<ParticleAccessor_T> & accessor, BlockDataID pdfFieldID, BlockDataID boundaryHandlingID, BlockDataID particleFieldID, + ReconstructionManager_T reconstructionManager, + real_t radius, Vector3<real_t> velocity, real_t density, bool conserveMomentum ) +{ + + + mesa_pd::domain::BlockForestDomain domain(blocks->getBlockForestPointer()); + auto sphereShape = ss->create<mesa_pd::data::Sphere>( radius ); + mesa_pd::mpi::SyncNextNeighbors syncNextNeighborFunc; + const real_t overlap = real_t( 1.5 ); + + MappingChecker<BoundaryHandling_T> mappingChecker(blocks, boundaryHandlingID, radius); + MappingResetter<BoundaryHandling_T> mappingResetter(blocks, boundaryHandlingID, particleFieldID, accessor->getInvalidUid()); + ReconstructionChecker<PdfField_T, BoundaryHandling_T> reconstructionChecker(blocks, pdfFieldID, boundaryHandlingID, velocity, density, FormerMO_Flag ); + + auto regularParticleMapper = lbm_mesapd_coupling::makeMovingParticleMapping<PdfField_T,BoundaryHandling_T>(blocks, pdfFieldID, boundaryHandlingID, particleFieldID, accessor, MO_Flag, FormerMO_Flag, mesa_pd::kernel::SelectAll(), conserveMomentum); + + std::string testIdentifier(identifier + " Test: sphere on block boarder 2"); + WALBERLA_LOG_DEVEL_ON_ROOT(testIdentifier << " - started"); + + Vector3<real_t> positionAtBlockBoarder2(real_t(20)+radius, real_t(10), real_t(10)); + + // create sphere + walberla::id_t sphereUid = 0; + if (domain.isContainedInProcessSubdomain( uint_c(mpi::MPIManager::instance()->rank()), positionAtBlockBoarder2 )) + { + mesa_pd::data::Particle&& p = *ps->create(); + p.setPosition(positionAtBlockBoarder2); + p.setLinearVelocity(velocity); + p.setInteractionRadius(radius); + p.setOwner(mpi::MPIManager::instance()->rank()); + p.setShapeID(sphereShape); + sphereUid = p.getUid(); + } + syncNextNeighborFunc(*ps, domain, overlap); + + mpi::allReduceInplace(sphereUid, mpi::SUM); + + // map + for( auto blockIt = blocks->begin(); blockIt != blocks->end(); ++blockIt ) regularParticleMapper(&(*blockIt)); + + auto currentSpherePosition = positionAtBlockBoarder2; + // run several "timesteps" and advance the sphere in total one cell + for( uint_t t = 0; t < 10; ++t ) + { + currentSpherePosition = currentSpherePosition + velocity; + + // advance sphere + size_t idx = accessor->uidToIdx(sphereUid); + if( idx != accessor->getInvalidIdx()) + { + accessor->setPosition(idx, currentSpherePosition); + } + syncNextNeighborFunc(*ps, domain, overlap); + + // update mapping + for( auto blockIt = blocks->begin(); blockIt != blocks->end(); ++blockIt ) regularParticleMapper(&(*blockIt)); + + // carry out restore step + for( auto blockIt = blocks->begin(); blockIt != blocks->end(); ++blockIt ) reconstructionManager(&(*blockIt)); + + + // check mapping + mappingChecker(testIdentifier + " (t=" + std::to_string(t)+") ",currentSpherePosition,false); + mappingChecker.checkGhostLayer(testIdentifier + " (t=" + std::to_string(t)+") ",currentSpherePosition,false); + + // check reconstruction + reconstructionChecker(testIdentifier + " (t=" + std::to_string(t)+") "); + } + + WALBERLA_LOG_DEVEL_ON_ROOT(testIdentifier << " - ended successfully"); + + // clean up + mappingResetter(); + ps->clear(); + syncNextNeighborFunc(*ps, domain, overlap); +} + + +/*!\brief Test case for the Pdf reconstruction functionality + * + * It tests these different reconstructors: + * - EquilibriumReconstructor + * - EquilibriumAndNonEquilibriumReconstructor + * - ExtrapolationReconstructor + * - GradsMomentApproximationReconstructor + * + * The following features of the PdfReconstructionManager are tested as well: + * - reconstruction at block boarder + * - version for small obstacle fraction + * + * Additionally, the following extrapolation direction finders are tested: + * - SphereNormalExtrapolationFinder + * - FlagFieldExtrapolationDirectionFinder + * + * ----------------------------------------------- + * # | # + * # | # + * # | # + * # | # + * # X X| X # + * # | # + * # | # + * # | # + * # | # + * ----------------------------------------------- + */ +////////// +// MAIN // +////////// +int main( int argc, char **argv ) +{ + debug::enterTestMode(); + + mpi::Environment env( argc, argv ); + + /////////////////////////// + // SIMULATION PROPERTIES // + /////////////////////////// + + const real_t omega = real_t(1); + const real_t dx = real_t(1); + const real_t radius = real_t(5); + const Vector3<real_t> velocity( real_t(0.1), real_t(0), real_t(0) ); + const real_t density = real_t(1); + + bool conserveMomentum = true; + + /////////////////////////// + // DATA STRUCTURES SETUP // + /////////////////////////// + + Vector3<uint_t> blocksPerDirection(uint_t(2), uint_t(1), uint_t(1)); + Vector3<uint_t> cellsPerBlock(uint_t(20), uint_t(20), uint_t(20)); + Vector3<bool> periodicity(false, false, false); + + auto blocks = blockforest::createUniformBlockGrid( blocksPerDirection[0], blocksPerDirection[1], blocksPerDirection[2], + cellsPerBlock[0], cellsPerBlock[1], cellsPerBlock[2], + dx, + 0, false, false, + periodicity[0], periodicity[1], periodicity[2], + false ); + + // create the lattice model + LatticeModel_T latticeModel = LatticeModel_T( omega ); + + // add PDF field + BlockDataID pdfFieldID = lbm::addPdfFieldToStorage< LatticeModel_T >( blocks, "pdf field (zyxf)", latticeModel, + velocity, density, + FieldGhostLayers, field::zyxf ); + + // add flag field + BlockDataID flagFieldID = field::addFlagFieldToStorage<FlagField_T>( blocks, "flag field", FieldGhostLayers ); + + // rpd setup + + //init data structures + auto ps = std::make_shared<mesa_pd::data::ParticleStorage>(1); + auto ss = std::make_shared<mesa_pd::data::ShapeStorage>(); + using ParticleAccessor_T = mesa_pd::data::ParticleAccessorWithShape; + auto accessor = make_shared<ParticleAccessor_T>(ps, ss); + + // add particle field + BlockDataID particleFieldID = field::addToStorage<lbm_mesapd_coupling::ParticleField_T>( blocks, "particle field", accessor->getInvalidUid(), field::zyxf, FieldGhostLayers ); + + // add boundary handling + using BoundaryHandling_T = MyBoundaryHandling<ParticleAccessor_T>::Type; + BlockDataID boundaryHandlingID = blocks->addStructuredBlockData< BoundaryHandling_T >(MyBoundaryHandling<ParticleAccessor_T>( flagFieldID, pdfFieldID, particleFieldID, accessor), "boundary handling" ); + + // vtk + //auto flagFieldVTK = vtk::createVTKOutput_BlockData( blocks, "flag_field" ); + //flagFieldVTK->addCellDataWriter( make_shared< field::VTKWriter< FlagField_T > >( flagFieldID, "FlagField" ) ); + + + ///////////////////////////////////// + // EQUILIBRIUM RECONSTRUCTOR TESTS // + ///////////////////////////////////// + + auto equilibriumReconstructionManager = lbm_mesapd_coupling::makePdfReconstructionManager<PdfField_T,BoundaryHandling_T>(blocks, pdfFieldID, boundaryHandlingID, particleFieldID, accessor, FormerMO_Flag, Fluid_Flag, conserveMomentum); + + checkSphereInsideBlock<BoundaryHandling_T>( "Equilibrium Reconstructor", blocks, ps, ss, accessor, pdfFieldID, boundaryHandlingID, particleFieldID, + *equilibriumReconstructionManager, radius, velocity, density, conserveMomentum ); + + checkSphereOnBlockBoarder<BoundaryHandling_T>( "Equilibrium Reconstructor", blocks, ps, ss, accessor, pdfFieldID, boundaryHandlingID, particleFieldID, + *equilibriumReconstructionManager, radius, velocity, density, conserveMomentum ); + + checkSphereOnBlockBoarder2<BoundaryHandling_T>( "Equilibrium Reconstructor", blocks, ps, ss, accessor, pdfFieldID, boundaryHandlingID, particleFieldID, + *equilibriumReconstructionManager, radius, velocity, density, conserveMomentum ); + + + ///////////////////////////////////// + // EQUILIBRIUM RECONSTRUCTOR TESTS // + // for small obstacle fractions // + ///////////////////////////////////// + + auto equilibriumReconstructionSmallObstacleFractionManager = lbm_mesapd_coupling::makePdfReconstructionManager<PdfField_T,BoundaryHandling_T>(blocks, pdfFieldID, boundaryHandlingID, particleFieldID, accessor, FormerMO_Flag, Fluid_Flag, conserveMomentum, true); + + checkSphereInsideBlock<BoundaryHandling_T>( "Equilibrium Reconstructor Small Obstacle Fraction", blocks, ps, ss, accessor, pdfFieldID, boundaryHandlingID, particleFieldID, + *equilibriumReconstructionSmallObstacleFractionManager, radius, velocity, density, conserveMomentum ); + + checkSphereOnBlockBoarder<BoundaryHandling_T>( "Equilibrium Reconstructor Small Obstacle Fraction", blocks, ps, ss, accessor, pdfFieldID, boundaryHandlingID, particleFieldID, + *equilibriumReconstructionSmallObstacleFractionManager, radius, velocity, density, conserveMomentum ); + + checkSphereOnBlockBoarder2<BoundaryHandling_T>( "Equilibrium Reconstructor Small Obstacle Fraction", blocks, ps, ss, accessor, pdfFieldID, boundaryHandlingID, particleFieldID, + *equilibriumReconstructionSmallObstacleFractionManager, radius, velocity, density, conserveMomentum ); + + ///////////////////////////////////////////////////////// + // EQUILIBRIUM AND NON-EQUILIBRIUM RECONSTRUCTOR TESTS // + // with Sphere Normal Extrapolator // + ///////////////////////////////////////////////////////// + + auto sphereNormalExtrapolationDirectionFinder = make_shared<lbm_mesapd_coupling::SphereNormalExtrapolationDirectionFinder>(blocks); + + checkExtrapolationDirectionFinder<BoundaryHandling_T>( "Sphere Normal Extrapolation Direction Finder", blocks, ps, ss, accessor, pdfFieldID, boundaryHandlingID, particleFieldID, + *sphereNormalExtrapolationDirectionFinder, radius, conserveMomentum ); + + + auto equilibriumAndNonEquilibriumSphereNormalReconstructor = lbm_mesapd_coupling::makeEquilibriumAndNonEquilibriumReconstructor<BoundaryHandling_T>(blocks, boundaryHandlingID, sphereNormalExtrapolationDirectionFinder, 3); + auto equilibriumAndNonEquilibriumSphereNormalReconstructionManager = lbm_mesapd_coupling::makePdfReconstructionManager<PdfField_T,BoundaryHandling_T>(blocks, pdfFieldID, boundaryHandlingID, particleFieldID, accessor, FormerMO_Flag, Fluid_Flag, equilibriumAndNonEquilibriumSphereNormalReconstructor, conserveMomentum); + + checkSphereInsideBlock<BoundaryHandling_T>( "Equilibrium and NonEquilibrium Reconstructor with Sphere Normal", blocks, ps, ss, accessor, pdfFieldID, boundaryHandlingID, particleFieldID, + *equilibriumAndNonEquilibriumSphereNormalReconstructionManager, radius, velocity, density, conserveMomentum ); + + checkSphereOnBlockBoarder<BoundaryHandling_T>( "Equilibrium and NonEquilibrium Reconstructor with Sphere Normal", blocks, ps, ss, accessor, pdfFieldID, boundaryHandlingID, particleFieldID, + *equilibriumAndNonEquilibriumSphereNormalReconstructionManager, radius, velocity, density, conserveMomentum ); + + checkSphereOnBlockBoarder2<BoundaryHandling_T>( "Equilibrium and NonEquilibrium Reconstructor with Sphere Normal", blocks, ps, ss, accessor, pdfFieldID, boundaryHandlingID, particleFieldID, + *equilibriumAndNonEquilibriumSphereNormalReconstructionManager, radius, velocity, density, conserveMomentum ); + + + ///////////////////////////////////////////////////////// + // EQUILIBRIUM AND NON-EQUILIBRIUM RECONSTRUCTOR TESTS // + // with FlagField Normal Extrapolator // + ///////////////////////////////////////////////////////// + + auto flagFieldNormalExtrapolationDirectionFinder = make_shared<lbm_mesapd_coupling::FlagFieldNormalExtrapolationDirectionFinder<BoundaryHandling_T> >(blocks,boundaryHandlingID); + + checkExtrapolationDirectionFinder<BoundaryHandling_T>( "FlagField Normal Extrapolation Direction Finder", blocks, ps, ss, accessor, pdfFieldID, boundaryHandlingID, particleFieldID, + *flagFieldNormalExtrapolationDirectionFinder, radius, conserveMomentum ); + + auto equilibriumAndNonEquilibriumFlagFieldNormalReconstructor = lbm_mesapd_coupling::makeEquilibriumAndNonEquilibriumReconstructor<BoundaryHandling_T>(blocks, boundaryHandlingID, flagFieldNormalExtrapolationDirectionFinder, 2); + auto equilibriumAndNonEquilibriumFlagFieldNormalReconstructionManager = lbm_mesapd_coupling::makePdfReconstructionManager<PdfField_T,BoundaryHandling_T>(blocks, pdfFieldID, boundaryHandlingID, particleFieldID, accessor, FormerMO_Flag, Fluid_Flag, equilibriumAndNonEquilibriumFlagFieldNormalReconstructor, conserveMomentum); + + checkSphereInsideBlock<BoundaryHandling_T>( "Equilibrium and NonEquilibrium Reconstructor with FlagField Normal", blocks, ps, ss, accessor, pdfFieldID, boundaryHandlingID, particleFieldID, + *equilibriumAndNonEquilibriumFlagFieldNormalReconstructionManager, radius, velocity, density, conserveMomentum ); + + checkSphereOnBlockBoarder<BoundaryHandling_T>( "Equilibrium and NonEquilibrium Reconstructor with FlagField Normal", blocks, ps, ss, accessor, pdfFieldID, boundaryHandlingID, particleFieldID, + *equilibriumAndNonEquilibriumFlagFieldNormalReconstructionManager, radius, velocity, density, conserveMomentum ); + + checkSphereOnBlockBoarder2<BoundaryHandling_T>( "Equilibrium and NonEquilibrium Reconstructor with FlagField Normal", blocks, ps, ss, accessor, pdfFieldID, boundaryHandlingID, particleFieldID, + *equilibriumAndNonEquilibriumFlagFieldNormalReconstructionManager, radius, velocity, density, conserveMomentum ); + + + ///////////////////////////////////////////////////////// + // EQUILIBRIUM AND NON-EQUILIBRIUM RECONSTRUCTOR TESTS // + // with only one extrapolation cell // + ///////////////////////////////////////////////////////// + + auto equilibriumAndNonEquilibriumSphereNormal1Reconstructor = lbm_mesapd_coupling::makeEquilibriumAndNonEquilibriumReconstructor<BoundaryHandling_T>(blocks, boundaryHandlingID, sphereNormalExtrapolationDirectionFinder, 1); + auto equilibriumAndNonEquilibriumSphereNormal1ReconstructionManager = lbm_mesapd_coupling::makePdfReconstructionManager<PdfField_T,BoundaryHandling_T>(blocks, pdfFieldID, boundaryHandlingID, particleFieldID, accessor, FormerMO_Flag, Fluid_Flag, equilibriumAndNonEquilibriumSphereNormal1Reconstructor, conserveMomentum); + + checkSphereInsideBlock<BoundaryHandling_T>( "Equilibrium and NonEquilibrium Reconstructor with one ext cell", blocks, ps, ss, accessor, pdfFieldID, boundaryHandlingID, particleFieldID, + *equilibriumAndNonEquilibriumSphereNormal1ReconstructionManager, radius, velocity, density, conserveMomentum ); + + checkSphereOnBlockBoarder<BoundaryHandling_T>( "Equilibrium and NonEquilibrium Reconstructor with one ext cell", blocks, ps, ss, accessor, pdfFieldID, boundaryHandlingID, particleFieldID, + *equilibriumAndNonEquilibriumSphereNormal1ReconstructionManager, radius, velocity, density, conserveMomentum ); + + checkSphereOnBlockBoarder2<BoundaryHandling_T>( "Equilibrium and NonEquilibrium Reconstructor with one ext cell", blocks, ps, ss, accessor, pdfFieldID, boundaryHandlingID, particleFieldID, + *equilibriumAndNonEquilibriumSphereNormal1ReconstructionManager, radius, velocity, density, conserveMomentum ); + + + /////////////////////////////////////// + // EXTRAPOLATION RECONSTRUCTOR TESTS // + /////////////////////////////////////// + + auto extrapolationReconstructor = lbm_mesapd_coupling::makeExtrapolationReconstructor<BoundaryHandling_T>(blocks, boundaryHandlingID, sphereNormalExtrapolationDirectionFinder); + auto extrapolationReconstructionManager = lbm_mesapd_coupling::makePdfReconstructionManager<PdfField_T,BoundaryHandling_T>(blocks, pdfFieldID, boundaryHandlingID, particleFieldID, accessor, FormerMO_Flag, Fluid_Flag, extrapolationReconstructor, conserveMomentum); + + checkSphereInsideBlock<BoundaryHandling_T>( "Extrapolation Reconstructor", blocks, ps, ss, accessor, pdfFieldID, boundaryHandlingID, particleFieldID, + *extrapolationReconstructionManager, radius, velocity, density, conserveMomentum ); + + checkSphereOnBlockBoarder<BoundaryHandling_T>( "Extrapolation Reconstructor", blocks, ps, ss, accessor, pdfFieldID, boundaryHandlingID, particleFieldID, + *extrapolationReconstructionManager, radius, velocity, density, conserveMomentum ); + + checkSphereOnBlockBoarder2<BoundaryHandling_T>( "Extrapolation Reconstructor", blocks, ps, ss, accessor, pdfFieldID, boundaryHandlingID, particleFieldID, + *extrapolationReconstructionManager, radius, velocity, density, conserveMomentum ); + + + /////////////////////////////////////// + // EXTRAPOLATION RECONSTRUCTOR TESTS // + // with activated constrain // + /////////////////////////////////////// + + auto extrapolationConstrainReconstructor = lbm_mesapd_coupling::makeExtrapolationReconstructor<BoundaryHandling_T, lbm_mesapd_coupling::SphereNormalExtrapolationDirectionFinder, true>(blocks, boundaryHandlingID, sphereNormalExtrapolationDirectionFinder); + auto extrapolationConstrainReconstructionManager = lbm_mesapd_coupling::makePdfReconstructionManager<PdfField_T,BoundaryHandling_T>(blocks, pdfFieldID, boundaryHandlingID, particleFieldID, accessor, FormerMO_Flag, Fluid_Flag, extrapolationReconstructor, conserveMomentum); + + checkSphereInsideBlock<BoundaryHandling_T>( "Extrapolation Reconstructor with Constrain", blocks, ps, ss, accessor, pdfFieldID, boundaryHandlingID, particleFieldID, + *extrapolationConstrainReconstructionManager, radius, velocity, density, conserveMomentum ); + + checkSphereOnBlockBoarder<BoundaryHandling_T>( "Extrapolation Reconstructor with Constrain", blocks, ps, ss, accessor, pdfFieldID, boundaryHandlingID, particleFieldID, + *extrapolationConstrainReconstructionManager, radius, velocity, density, conserveMomentum ); + + checkSphereOnBlockBoarder2<BoundaryHandling_T>( "Extrapolation Reconstructor with Constrain", blocks, ps, ss, accessor, pdfFieldID, boundaryHandlingID, particleFieldID, + *extrapolationConstrainReconstructionManager, radius, velocity, density, conserveMomentum ); + + ////////////////////////////////////////////// + // GRADs MOMENT APPROXIMATION RECONSTRUCTOR // + ////////////////////////////////////////////// + + auto gradReconstructor = lbm_mesapd_coupling::makeGradsMomentApproximationReconstructor<BoundaryHandling_T>(blocks, boundaryHandlingID, omega, false); + auto gradReconstructorManager = lbm_mesapd_coupling::makePdfReconstructionManager<PdfField_T,BoundaryHandling_T>(blocks, pdfFieldID, boundaryHandlingID, particleFieldID, accessor, FormerMO_Flag, Fluid_Flag, gradReconstructor, conserveMomentum); + + checkSphereInsideBlock<BoundaryHandling_T>( "Grads Moment Approximation Reconstructor", blocks, ps, ss, accessor, pdfFieldID, boundaryHandlingID, particleFieldID, + *gradReconstructorManager, radius, velocity, density, conserveMomentum ); + + checkSphereOnBlockBoarder<BoundaryHandling_T>( "Grads Moment Approximation Reconstructor", blocks, ps, ss, accessor, pdfFieldID, boundaryHandlingID, particleFieldID, + *gradReconstructorManager, radius, velocity, density, conserveMomentum ); + + checkSphereOnBlockBoarder2<BoundaryHandling_T>( "Grads Moment Approximation Reconstructor", blocks, ps, ss, accessor, pdfFieldID, boundaryHandlingID, particleFieldID, + *gradReconstructorManager, radius, velocity, density, conserveMomentum ); + + ////////////////////////////////////////////// + // GRADs MOMENT APPROXIMATION RECONSTRUCTOR // + // with density recomputation // + ////////////////////////////////////////////// + + auto gradReconstructorWithRecomp = lbm_mesapd_coupling::makeGradsMomentApproximationReconstructor<BoundaryHandling_T>(blocks, boundaryHandlingID, omega, true); + auto gradReconstructorWithRecompManager = lbm_mesapd_coupling::makePdfReconstructionManager<PdfField_T,BoundaryHandling_T>(blocks, pdfFieldID, boundaryHandlingID, particleFieldID, accessor, FormerMO_Flag, Fluid_Flag, gradReconstructorWithRecomp, conserveMomentum); + + checkSphereInsideBlock<BoundaryHandling_T>( "Grads Moment Approximation Reconstructor with Density Recomputation", blocks, ps, ss, accessor, pdfFieldID, boundaryHandlingID, particleFieldID, + *gradReconstructorWithRecompManager, radius, velocity, density, conserveMomentum ); + + checkSphereOnBlockBoarder<BoundaryHandling_T>( "Grads Moment Approximation Reconstructor with Density Recomputation", blocks, ps, ss, accessor, pdfFieldID, boundaryHandlingID, particleFieldID, + *gradReconstructorWithRecompManager, radius, velocity, density, conserveMomentum ); + + checkSphereOnBlockBoarder2<BoundaryHandling_T>( "Grads Moment Approximation Reconstructor with Density Recomputation", blocks, ps, ss, accessor, pdfFieldID, boundaryHandlingID, particleFieldID, + *gradReconstructorWithRecompManager, radius, velocity, density, conserveMomentum ); + + return 0; + +} + +} //namespace pdf_reconstruction + +int main( int argc, char **argv ){ + pdf_reconstruction::main(argc, argv); +} diff --git a/tests/lbm_mesapd_coupling/momentum_exchange_method/SettlingSphere.cpp b/tests/lbm_mesapd_coupling/momentum_exchange_method/SettlingSphere.cpp new file mode 100644 index 0000000000000000000000000000000000000000..ef8db99fa2e19a58c8c14ac96973722e18cff824 --- /dev/null +++ b/tests/lbm_mesapd_coupling/momentum_exchange_method/SettlingSphere.cpp @@ -0,0 +1,849 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla is free software: you can +// redistribute it and/or modify it under the terms of the GNU General Public +// License as published by the Free Software Foundation, either version 3 of +// the License, or (at your option) any later version. +// +// waLBerla is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +// for more details. +// +// You should have received a copy of the GNU General Public License along +// with waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file SettlingSphere.cpp +//! \ingroup lbm_mesapd_coupling +//! \author Christoph Rettinger <christoph.rettinger@fau.de> +// +//====================================================================================================================== + +#include "blockforest/Initialization.h" +#include "blockforest/communication/UniformBufferedScheme.h" + +#include "boundary/all.h" + +#include "core/DataTypes.h" +#include "core/Environment.h" +#include "core/SharedFunctor.h" +#include "core/debug/Debug.h" +#include "core/debug/TestSubsystem.h" +#include "core/math/all.h" +#include "core/timing/RemainingTimeLogger.h" +#include "core/logging/all.h" + +#include "domain_decomposition/SharedSweep.h" + +#include "field/AddToStorage.h" +#include "field/communication/PackInfo.h" + +#include "lbm/boundary/all.h" +#include "lbm/communication/PdfFieldPackInfo.h" +#include "lbm/field/AddToStorage.h" +#include "lbm/field/PdfField.h" +#include "lbm/lattice_model/D3Q19.h" +#include "lbm/sweeps/CellwiseSweep.h" +#include "lbm/sweeps/SweepWrappers.h" + +#include "lbm_mesapd_coupling/mapping/ParticleMapping.h" +#include "lbm_mesapd_coupling/momentum_exchange_method/MovingParticleMapping.h" +#include "lbm_mesapd_coupling/momentum_exchange_method/boundary/SimpleBB.h" +#include "lbm_mesapd_coupling/momentum_exchange_method/boundary/CurvedLinear.h" +#include "lbm_mesapd_coupling/momentum_exchange_method/reconstruction/Reconstructor.h" +#include "lbm_mesapd_coupling/momentum_exchange_method/reconstruction/ExtrapolationDirectionFinder.h" +#include "lbm_mesapd_coupling/momentum_exchange_method/reconstruction/PdfReconstructionManager.h" +#include "lbm_mesapd_coupling/utility/AddForceOnParticlesKernel.h" +#include "lbm_mesapd_coupling/utility/ParticleSelector.h" +#include "lbm_mesapd_coupling/utility/AverageHydrodynamicForceTorqueKernel.h" +#include "lbm_mesapd_coupling/utility/AddHydrodynamicInteractionKernel.h" +#include "lbm_mesapd_coupling/utility/ResetHydrodynamicForceTorqueKernel.h" +#include "lbm_mesapd_coupling/utility/LubricationCorrectionKernel.h" +#include "lbm_mesapd_coupling/DataTypes.h" + +#include "mesa_pd/collision_detection/AnalyticContactDetection.h" +#include "mesa_pd/data/ParticleAccessorWithShape.h" +#include "mesa_pd/data/ParticleStorage.h" +#include "mesa_pd/data/ShapeStorage.h" +#include "mesa_pd/data/DataTypes.h" +#include "mesa_pd/data/shape/HalfSpace.h" +#include "mesa_pd/data/shape/Sphere.h" +#include "mesa_pd/domain/BlockForestDomain.h" +#include "mesa_pd/kernel/DoubleCast.h" +#include "mesa_pd/kernel/ExplicitEulerWithShape.h" +#include "mesa_pd/kernel/ParticleSelector.h" +#include "mesa_pd/kernel/SpringDashpot.h" +#include "mesa_pd/kernel/VelocityVerletWithShape.h" +#include "mesa_pd/mpi/ContactFilter.h" +#include "mesa_pd/mpi/SyncNextNeighbors.h" +#include "mesa_pd/mpi/ReduceProperty.h" +#include "mesa_pd/mpi/notifications/ForceTorqueNotification.h" +#include "mesa_pd/vtk/ParticleVtkOutput.h" + +#include "timeloop/SweepTimeloop.h" + +#include "vtk/all.h" +#include "field/vtk/all.h" +#include "lbm/vtk/all.h" + +#include <functional> + +namespace settling_sphere +{ + +/////////// +// USING // +/////////// + +using namespace walberla; +using walberla::uint_t; + +using LatticeModel_T = lbm::D3Q19< lbm::collision_model::TRT>; + +using Stencil_T = LatticeModel_T::Stencil; +using PdfField_T = lbm::PdfField<LatticeModel_T>; + +using flag_t = walberla::uint8_t; +using FlagField_T = FlagField<flag_t>; + +const uint_t FieldGhostLayers = 1; + +/////////// +// FLAGS // +/////////// + +const FlagUID Fluid_Flag( "fluid" ); +const FlagUID NoSlip_Flag( "no slip" ); +const FlagUID MO_Flag( "moving obstacle" ); +const FlagUID FormerMO_Flag( "former moving obstacle" ); + +///////////////////////////////////// +// BOUNDARY HANDLING CUSTOMIZATION // +///////////////////////////////////// +template <typename ParticleAccessor_T> +class MyBoundaryHandling +{ +public: + + using NoSlip_T = lbm::NoSlip< LatticeModel_T, flag_t >; + using MO_T = lbm_mesapd_coupling::CurvedLinear< LatticeModel_T, FlagField_T, ParticleAccessor_T >; + using Type = BoundaryHandling< FlagField_T, Stencil_T, NoSlip_T, MO_T >; + + MyBoundaryHandling( const BlockDataID & flagFieldID, const BlockDataID & pdfFieldID, + const BlockDataID & particleFieldID, const shared_ptr<ParticleAccessor_T>& ac) : + flagFieldID_( flagFieldID ), pdfFieldID_( pdfFieldID ), particleFieldID_( particleFieldID ), ac_( ac ) {} + + Type * operator()( IBlock* const block, const StructuredBlockStorage* const storage ) const + { + WALBERLA_ASSERT_NOT_NULLPTR( block ); + WALBERLA_ASSERT_NOT_NULLPTR( storage ); + + auto * flagField = block->getData< FlagField_T >( flagFieldID_ ); + auto * pdfField = block->getData< PdfField_T > ( pdfFieldID_ ); + auto * particleField = block->getData< lbm_mesapd_coupling::ParticleField_T > ( particleFieldID_ ); + + const auto fluid = flagField->flagExists( Fluid_Flag ) ? flagField->getFlag( Fluid_Flag ) : flagField->registerFlag( Fluid_Flag ); + + Type * handling = new Type( "moving obstacle boundary handling", flagField, fluid, + NoSlip_T( "NoSlip", NoSlip_Flag, pdfField ), + MO_T( "MO", MO_Flag, pdfField, flagField, particleField, ac_, fluid, *storage, *block ) ); + + handling->fillWithDomain( FieldGhostLayers ); + + return handling; + } + +private: + + const BlockDataID flagFieldID_; + const BlockDataID pdfFieldID_; + const BlockDataID particleFieldID_; + + shared_ptr<ParticleAccessor_T> ac_; +}; +//******************************************************************************************************************* + + +//******************************************************************************************************************* +/*!\brief Evaluating the position and velocity of the sphere + * + */ +//******************************************************************************************************************* +template< typename ParticleAccessor_T> +class SpherePropertyLogger +{ +public: + SpherePropertyLogger( const shared_ptr< ParticleAccessor_T > & ac, walberla::id_t sphereUid, + const std::string & fileName, bool fileIO, + real_t dx_SI, real_t dt_SI, real_t diameter, real_t gravitationalForceMag) : + ac_( ac ), sphereUid_( sphereUid ), fileName_( fileName ), fileIO_(fileIO), + dx_SI_( dx_SI ), dt_SI_( dt_SI ), diameter_( diameter ), gravitationalForceMag_( gravitationalForceMag ), + position_( real_t(0) ), maxVelocity_( real_t(0) ) + { + if ( fileIO_ ) + { + WALBERLA_ROOT_SECTION() + { + std::ofstream file; + file.open( fileName_.c_str() ); + file << "#\t t\t posX\t posY\t gapZ\t velX\t velY\t velZ\n"; + file.close(); + } + } + } + + void operator()(const uint_t timestep) + { + Vector3<real_t> pos(real_t(0)); + Vector3<real_t> transVel(real_t(0)); + Vector3<real_t> hydForce(real_t(0)); + + size_t idx = ac_->uidToIdx(sphereUid_); + if( idx != ac_->getInvalidIdx()) + { + if(!mesa_pd::data::particle_flags::isSet( ac_->getFlags(idx), mesa_pd::data::particle_flags::GHOST)) + { + pos = ac_->getPosition(idx); + transVel = ac_->getLinearVelocity(idx); + } + hydForce = ac_->getHydrodynamicForce(idx); + } + + WALBERLA_MPI_SECTION() + { + mpi::allReduceInplace( pos[0], mpi::SUM ); + mpi::allReduceInplace( pos[1], mpi::SUM ); + mpi::allReduceInplace( pos[2], mpi::SUM ); + + mpi::allReduceInplace( transVel[0], mpi::SUM ); + mpi::allReduceInplace( transVel[1], mpi::SUM ); + mpi::allReduceInplace( transVel[2], mpi::SUM ); + + mpi::allReduceInplace( hydForce[0], mpi::SUM ); + mpi::allReduceInplace( hydForce[1], mpi::SUM ); + mpi::allReduceInplace( hydForce[2], mpi::SUM ); + } + + position_ = pos[2]; + maxVelocity_ = std::max(maxVelocity_, -transVel[2]); + + if( fileIO_ ) + writeToFile( timestep, pos, transVel, hydForce); + } + + real_t getPosition() const + { + return position_; + } + + real_t getMaxVelocity() const + { + return maxVelocity_; + } + +private: + void writeToFile( const uint_t timestep, const Vector3<real_t> & position, const Vector3<real_t> & velocity, const Vector3<real_t> & hydForce ) + { + WALBERLA_ROOT_SECTION() + { + std::ofstream file; + file.open( fileName_.c_str(), std::ofstream::app ); + + auto scaledPosition = position / diameter_; + auto velocity_SI = velocity * dx_SI_ / dt_SI_; + auto normalizedHydForce = hydForce / gravitationalForceMag_; + + + file << timestep << "\t" << real_c(timestep) * dt_SI_ << "\t" + << "\t" << scaledPosition[0] << "\t" << scaledPosition[1] << "\t" << scaledPosition[2] - real_t(0.5) + << "\t" << velocity_SI[0] << "\t" << velocity_SI[1] << "\t" << velocity_SI[2] + << "\t" << normalizedHydForce[0] << "\t" << normalizedHydForce[1] << "\t" << normalizedHydForce[2] + << "\n"; + file.close(); + } + } + + shared_ptr< ParticleAccessor_T > ac_; + const walberla::id_t sphereUid_; + std::string fileName_; + bool fileIO_; + real_t dx_SI_, dt_SI_, diameter_, gravitationalForceMag_; + + real_t position_; + real_t maxVelocity_; +}; + + +void createPlaneSetup(const shared_ptr<mesa_pd::data::ParticleStorage> & ps, const shared_ptr<mesa_pd::data::ShapeStorage> & ss, const math::AABB & simulationDomain) +{ + // create bounding planes + mesa_pd::data::Particle p0 = *ps->create(true); + p0.setPosition(simulationDomain.minCorner()); + p0.setShapeID(ss->create<mesa_pd::data::HalfSpace>( Vector3<real_t>(0,0,1) )); + p0.setOwner(mpi::MPIManager::instance()->rank()); + p0.setType(0); + mesa_pd::data::particle_flags::set(p0.getFlagsRef(), mesa_pd::data::particle_flags::INFINITE); + mesa_pd::data::particle_flags::set(p0.getFlagsRef(), mesa_pd::data::particle_flags::FIXED); + + mesa_pd::data::Particle p1 = *ps->create(true); + p1.setPosition(simulationDomain.maxCorner()); + p1.setShapeID(ss->create<mesa_pd::data::HalfSpace>( Vector3<real_t>(0,0,-1) )); + p1.setOwner(mpi::MPIManager::instance()->rank()); + p1.setType(0); + mesa_pd::data::particle_flags::set(p1.getFlagsRef(), mesa_pd::data::particle_flags::INFINITE); + mesa_pd::data::particle_flags::set(p1.getFlagsRef(), mesa_pd::data::particle_flags::FIXED); + + mesa_pd::data::Particle p2 = *ps->create(true); + p2.setPosition(simulationDomain.minCorner()); + p2.setShapeID(ss->create<mesa_pd::data::HalfSpace>( Vector3<real_t>(1,0,0) )); + p2.setOwner(mpi::MPIManager::instance()->rank()); + p2.setType(0); + mesa_pd::data::particle_flags::set(p2.getFlagsRef(), mesa_pd::data::particle_flags::INFINITE); + mesa_pd::data::particle_flags::set(p2.getFlagsRef(), mesa_pd::data::particle_flags::FIXED); + + mesa_pd::data::Particle p3 = *ps->create(true); + p3.setPosition(simulationDomain.maxCorner()); + p3.setShapeID(ss->create<mesa_pd::data::HalfSpace>( Vector3<real_t>(-1,0,0) )); + p3.setOwner(mpi::MPIManager::instance()->rank()); + p3.setType(0); + mesa_pd::data::particle_flags::set(p3.getFlagsRef(), mesa_pd::data::particle_flags::INFINITE); + mesa_pd::data::particle_flags::set(p3.getFlagsRef(), mesa_pd::data::particle_flags::FIXED); + + mesa_pd::data::Particle p4 = *ps->create(true); + p4.setPosition(simulationDomain.minCorner()); + p4.setShapeID(ss->create<mesa_pd::data::HalfSpace>( Vector3<real_t>(0,1,0) )); + p4.setOwner(mpi::MPIManager::instance()->rank()); + p4.setType(0); + mesa_pd::data::particle_flags::set(p4.getFlagsRef(), mesa_pd::data::particle_flags::INFINITE); + mesa_pd::data::particle_flags::set(p4.getFlagsRef(), mesa_pd::data::particle_flags::FIXED); + + mesa_pd::data::Particle p5 = *ps->create(true); + p5.setPosition(simulationDomain.maxCorner()); + p5.setShapeID(ss->create<mesa_pd::data::HalfSpace>( Vector3<real_t>(0,-1,0) )); + p5.setOwner(mpi::MPIManager::instance()->rank()); + p5.setType(0); + mesa_pd::data::particle_flags::set(p5.getFlagsRef(), mesa_pd::data::particle_flags::INFINITE); + mesa_pd::data::particle_flags::set(p5.getFlagsRef(), mesa_pd::data::particle_flags::FIXED); + +} + +////////// +// MAIN // +////////// + +//******************************************************************************************************************* +/*!\brief Testcase that simulates the settling of a sphere inside a rectangular column filled with viscous fluid + * + * see: ten Cate, Nieuwstad, Derksen, Van den Akker - "Particle imaging velocimetry experiments and lattice-Boltzmann + * simulations on a single sphere settling under gravity" (2002), Physics of Fluids, doi: 10.1063/1.1512918 + */ +//******************************************************************************************************************* + +int main( int argc, char **argv ) +{ + debug::enterTestMode(); + + mpi::Environment env( argc, argv ); + + /////////////////// + // Customization // + /////////////////// + + // simulation control + bool shortrun = false; + bool funcTest = false; + bool fileIO = false; + uint_t vtkIOFreq = 0; + std::string baseFolder = "vtk_out_SettlingSphere"; + + // physical setup + uint_t fluidType = 1; + + //numerical parameters + uint_t numberOfCellsInHorizontalDirection = uint_t(135); + bool averageForceTorqueOverTwoTimSteps = true; + bool conserveMomentum = false; + uint_t numRPDSubCycles = uint_t(1); + bool useVelocityVerlet = false; + bool useEANReconstructor = false; + bool useExtReconstructor = false; + + for( int i = 1; i < argc; ++i ) + { + if( std::strcmp( argv[i], "--shortrun" ) == 0 ) { shortrun = true; continue; } + if( std::strcmp( argv[i], "--funcTest" ) == 0 ) { funcTest = true; continue; } + if( std::strcmp( argv[i], "--fileIO" ) == 0 ) { fileIO = true; continue; } + if( std::strcmp( argv[i], "--vtkIOFreq" ) == 0 ) { vtkIOFreq = uint_c( std::atof( argv[++i] ) ); continue; } + if( std::strcmp( argv[i], "--fluidType" ) == 0 ) { fluidType = uint_c( std::atof( argv[++i] ) ); continue; } + if( std::strcmp( argv[i], "--numRPDSubCycles" ) == 0 ) { numRPDSubCycles = uint_c( std::atof( argv[++i] ) ); continue; } + if( std::strcmp( argv[i], "--resolution" ) == 0 ) { numberOfCellsInHorizontalDirection = uint_c( std::atof( argv[++i] ) ); continue; } + if( std::strcmp( argv[i], "--noForceAveraging" ) == 0 ) { averageForceTorqueOverTwoTimSteps = false; continue; } + if( std::strcmp( argv[i], "--baseFolder" ) == 0 ) { baseFolder = argv[++i]; continue; } + if( std::strcmp( argv[i], "--useVV" ) == 0 ) { useVelocityVerlet = true; continue; } + if( std::strcmp( argv[i], "--useEANReconstructor" ) == 0 ) { useEANReconstructor = true; continue; } + if( std::strcmp( argv[i], "--useExtReconstructor" ) == 0 ) { useExtReconstructor = true; continue; } + WALBERLA_ABORT("Unrecognized command line argument found: " << argv[i]); + } + + WALBERLA_CHECK(!(useEANReconstructor && useExtReconstructor), "Can not use two reconstructors!"); + + if( funcTest ) + { + walberla::logging::Logging::instance()->setLogLevel(logging::Logging::LogLevel::WARNING); + } + + if( fileIO ) + { + // create base directory if it does not yet exist + filesystem::path tpath( baseFolder ); + if( !filesystem::exists( tpath ) ) + filesystem::create_directory( tpath ); + } + + ////////////////////////////////////// + // SIMULATION PROPERTIES in SI units// + ////////////////////////////////////// + + // values are mainly taken from the reference paper + const real_t diameter_SI = real_t(15e-3); + const real_t densitySphere_SI = real_t(1120); + + real_t densityFluid_SI, dynamicViscosityFluid_SI; + real_t expectedSettlingVelocity_SI; + switch( fluidType ) + { + case 1: + // Re_p around 1.5 + densityFluid_SI = real_t(970); + dynamicViscosityFluid_SI = real_t(373e-3); + expectedSettlingVelocity_SI = real_t(0.035986); + break; + case 2: + // Re_p around 4.1 + densityFluid_SI = real_t(965); + dynamicViscosityFluid_SI = real_t(212e-3); + expectedSettlingVelocity_SI = real_t(0.05718); + break; + case 3: + // Re_p around 11.6 + densityFluid_SI = real_t(962); + dynamicViscosityFluid_SI = real_t(113e-3); + expectedSettlingVelocity_SI = real_t(0.087269); + break; + case 4: + // Re_p around 31.9 + densityFluid_SI = real_t(960); + dynamicViscosityFluid_SI = real_t(58e-3); + expectedSettlingVelocity_SI = real_t(0.12224); + break; + default: + WALBERLA_ABORT("Only four different fluids are supported! Choose type between 1 and 4."); + } + const real_t kinematicViscosityFluid_SI = dynamicViscosityFluid_SI / densityFluid_SI; + + const real_t gravitationalAcceleration_SI = real_t(9.81); + Vector3<real_t> domainSize_SI(real_t(100e-3), real_t(100e-3), real_t(160e-3)); + //shift starting gap a bit upwards to match the reported (plotted) values + const real_t startingGapSize_SI = real_t(120e-3) + real_t(0.25) * diameter_SI; + + WALBERLA_LOG_INFO_ON_ROOT("Setup (in SI units):"); + WALBERLA_LOG_INFO_ON_ROOT(" - fluid type = " << fluidType ); + WALBERLA_LOG_INFO_ON_ROOT(" - domain size = " << domainSize_SI ); + WALBERLA_LOG_INFO_ON_ROOT(" - sphere: diameter = " << diameter_SI << ", density = " << densitySphere_SI << ", starting gap size = " << startingGapSize_SI ); + WALBERLA_LOG_INFO_ON_ROOT(" - fluid: density = " << densityFluid_SI << ", dyn. visc = " << dynamicViscosityFluid_SI << ", kin. visc = " << kinematicViscosityFluid_SI ); + WALBERLA_LOG_INFO_ON_ROOT(" - expected settling velocity = " << expectedSettlingVelocity_SI << " --> Re_p = " << expectedSettlingVelocity_SI * diameter_SI / kinematicViscosityFluid_SI ); + + + ////////////////////////// + // NUMERICAL PARAMETERS // + ////////////////////////// + + + const real_t dx_SI = domainSize_SI[0] / real_c(numberOfCellsInHorizontalDirection); + const Vector3<uint_t> domainSize( uint_c(floor(domainSize_SI[0] / dx_SI + real_t(0.5)) ), + uint_c(floor(domainSize_SI[1] / dx_SI + real_t(0.5)) ), + uint_c(floor(domainSize_SI[2] / dx_SI + real_t(0.5)) ) ); + const real_t diameter = diameter_SI / dx_SI; + const real_t sphereVolume = math::pi / real_t(6) * diameter * diameter * diameter; + + const real_t expectedSettlingVelocity = real_t(0.01); + const real_t dt_SI = expectedSettlingVelocity / expectedSettlingVelocity_SI * dx_SI; + + const real_t viscosity = kinematicViscosityFluid_SI * dt_SI / ( dx_SI * dx_SI ); + const real_t relaxationTime = real_t(1) / lbm::collision_model::omegaFromViscosity(viscosity); + + const real_t gravitationalAcceleration = gravitationalAcceleration_SI * dt_SI * dt_SI / dx_SI; + + const real_t densityFluid = real_t(1); + const real_t densitySphere = densityFluid * densitySphere_SI / densityFluid_SI; + + const real_t dx = real_t(1); + + const uint_t timesteps = funcTest ? 1 : ( shortrun ? uint_t(200) : uint_t( 250000 ) ); + + WALBERLA_LOG_INFO_ON_ROOT(" - dx_SI = " << dx_SI << ", dt_SI = " << dt_SI); + WALBERLA_LOG_INFO_ON_ROOT("Setup (in simulation, i.e. lattice, units):"); + WALBERLA_LOG_INFO_ON_ROOT(" - domain size = " << domainSize); + WALBERLA_LOG_INFO_ON_ROOT(" - sphere: diameter = " << diameter << ", density = " << densitySphere ); + WALBERLA_LOG_INFO_ON_ROOT(" - fluid: density = " << densityFluid << ", relaxation time (tau) = " << relaxationTime << ", kin. visc = " << viscosity ); + WALBERLA_LOG_INFO_ON_ROOT(" - gravitational acceleration = " << gravitationalAcceleration ); + WALBERLA_LOG_INFO_ON_ROOT(" - expected settling velocity = " << expectedSettlingVelocity << " --> Re_p = " << expectedSettlingVelocity * diameter / viscosity ); + WALBERLA_LOG_INFO_ON_ROOT(" - integrator = " << (useVelocityVerlet ? "Velocity Verlet" : "Explicit Euler") ); + + if( vtkIOFreq > 0 ) + { + WALBERLA_LOG_INFO_ON_ROOT(" - writing vtk files to folder \"" << baseFolder << "\" with frequency " << vtkIOFreq); + } + + /////////////////////////// + // BLOCK STRUCTURE SETUP // + /////////////////////////// + + Vector3<uint_t> numberOfBlocksPerDirection( uint_t(1), uint_t(1), uint_t(4) ); + Vector3<uint_t> cellsPerBlockPerDirection( domainSize[0] / numberOfBlocksPerDirection[0], + domainSize[1] / numberOfBlocksPerDirection[1], + domainSize[2] / numberOfBlocksPerDirection[2] ); + for( uint_t i = 0; i < 3; ++i ) { + WALBERLA_CHECK_EQUAL(cellsPerBlockPerDirection[i] * numberOfBlocksPerDirection[i], domainSize[i], + "Unmatching domain decomposition in direction " << i << "!"); + } + + auto blocks = blockforest::createUniformBlockGrid( numberOfBlocksPerDirection[0], numberOfBlocksPerDirection[1], numberOfBlocksPerDirection[2], + cellsPerBlockPerDirection[0], cellsPerBlockPerDirection[1], cellsPerBlockPerDirection[2], dx, + 0, false, false, + false, false, false, //periodicity + false ); + + WALBERLA_LOG_INFO_ON_ROOT("Domain decomposition:"); + WALBERLA_LOG_INFO_ON_ROOT(" - blocks per direction = " << numberOfBlocksPerDirection ); + WALBERLA_LOG_INFO_ON_ROOT(" - cells per block = " << cellsPerBlockPerDirection ); + + //write domain decomposition to file + if( vtkIOFreq > 0 ) + { + vtk::writeDomainDecomposition( blocks, "initial_domain_decomposition", baseFolder ); + } + + ////////////////// + // RPD COUPLING // + ////////////////// + + auto rpdDomain = std::make_shared<mesa_pd::domain::BlockForestDomain>(blocks->getBlockForestPointer()); + + //init data structures + auto ps = walberla::make_shared<mesa_pd::data::ParticleStorage>(1); + auto ss = walberla::make_shared<mesa_pd::data::ShapeStorage>(); + using ParticleAccessor_T = mesa_pd::data::ParticleAccessorWithShape; + auto accessor = walberla::make_shared<ParticleAccessor_T >(ps, ss); + + // bounding planes + createPlaneSetup(ps,ss,blocks->getDomain()); + + // create sphere and store Uid + Vector3<real_t> initialPosition( real_t(0.5) * real_c(domainSize[0]), real_t(0.5) * real_c(domainSize[1]), startingGapSize_SI / dx_SI + real_t(0.5) * diameter); + auto sphereShape = ss->create<mesa_pd::data::Sphere>( diameter * real_t(0.5) ); + ss->shapes[sphereShape]->updateMassAndInertia(densitySphere); + + walberla::id_t sphereUid = 0; + if (rpdDomain->isContainedInProcessSubdomain( uint_c(mpi::MPIManager::instance()->rank()), initialPosition )) + { + mesa_pd::data::Particle&& p = *ps->create(); + p.setPosition(initialPosition); + p.setInteractionRadius(diameter * real_t(0.5)); + p.setOwner(mpi::MPIManager::instance()->rank()); + p.setShapeID(sphereShape); + sphereUid = p.getUid(); + } + mpi::allReduceInplace(sphereUid, mpi::SUM); + + /////////////////////// + // ADD DATA TO BLOCKS // + //////////////////////// + + // create the lattice model + LatticeModel_T latticeModel = LatticeModel_T( lbm::collision_model::TRT::constructWithMagicNumber( real_t(1) / relaxationTime ) ); + + // add PDF field + BlockDataID pdfFieldID = lbm::addPdfFieldToStorage< LatticeModel_T >( blocks, "pdf field (zyxf)", latticeModel, + Vector3< real_t >( real_t(0) ), real_t(1), + uint_t(1), field::zyxf ); + // add flag field + BlockDataID flagFieldID = field::addFlagFieldToStorage<FlagField_T>( blocks, "flag field" ); + + // add particle field + BlockDataID particleFieldID = field::addToStorage<lbm_mesapd_coupling::ParticleField_T>( blocks, "particle field", accessor->getInvalidUid(), field::zyxf, FieldGhostLayers ); + + // add boundary handling + using BoundaryHandling_T = MyBoundaryHandling<ParticleAccessor_T>::Type; + BlockDataID boundaryHandlingID = blocks->addStructuredBlockData< BoundaryHandling_T >(MyBoundaryHandling<ParticleAccessor_T>( flagFieldID, pdfFieldID, particleFieldID, accessor), "boundary handling" ); + + // set up RPD functionality + std::function<void(void)> syncCall = [ps,rpdDomain](){ + const real_t overlap = real_t( 1.5 ); + mesa_pd::mpi::SyncNextNeighbors syncNextNeighborFunc; + syncNextNeighborFunc(*ps, *rpdDomain, overlap); + }; + + syncCall(); + + mesa_pd::kernel::ExplicitEulerWithShape explEulerIntegrator(real_t(1)/real_t(numRPDSubCycles)); + mesa_pd::kernel::VelocityVerletWithShapePreForceUpdate vvIntegratorPreForce(real_t(1)/real_t(numRPDSubCycles)); + mesa_pd::kernel::VelocityVerletWithShapePostForceUpdate vvIntegratorPostForce(real_t(1)/real_t(numRPDSubCycles)); + + mesa_pd::kernel::SpringDashpot collisionResponse(1); + mesa_pd::mpi::ReduceProperty reduceProperty; + + // set up coupling functionality + lbm_mesapd_coupling::RegularParticlesSelector sphereSelector; + Vector3<real_t> gravitationalForce( real_t(0), real_t(0), -(densitySphere - densityFluid) * gravitationalAcceleration * sphereVolume ); + lbm_mesapd_coupling::AddForceOnParticlesKernel addGravitationalForce(gravitationalForce); + lbm_mesapd_coupling::AddHydrodynamicInteractionKernel addHydrodynamicInteraction; + lbm_mesapd_coupling::ResetHydrodynamicForceTorqueKernel resetHydrodynamicForceTorque; + lbm_mesapd_coupling::AverageHydrodynamicForceTorqueKernel averageHydrodynamicForceTorque; + lbm_mesapd_coupling::LubricationCorrectionKernel lubricationCorrectionKernel(viscosity, [](real_t r){ return real_t(0.0016) * r;}); + lbm_mesapd_coupling::ParticleMappingKernel<BoundaryHandling_T> particleMappingKernel(blocks, boundaryHandlingID); + lbm_mesapd_coupling::MovingParticleMappingKernel<BoundaryHandling_T> movingParticleMappingKernel(blocks, boundaryHandlingID, particleFieldID); + + /////////////// + // TIME LOOP // + /////////////// + + // map planes into the LBM simulation -> act as no-slip boundaries + ps->forEachParticle(false, lbm_mesapd_coupling::GlobalParticlesSelector(), *accessor, particleMappingKernel, *accessor, NoSlip_Flag); + + // map particles into the LBM simulation + ps->forEachParticle(false, sphereSelector, *accessor, movingParticleMappingKernel, *accessor, MO_Flag); + + + // setup of the LBM communication for synchronizing the pdf field between neighboring blocks + std::function< void () > commFunction; + blockforest::communication::UniformBufferedScheme< Stencil_T > scheme( blocks ); + scheme.addPackInfo( make_shared< lbm::PdfFieldPackInfo< LatticeModel_T > >( pdfFieldID ) ); + commFunction = scheme; + + // create the timeloop + SweepTimeloop timeloop( blocks->getBlockStorage(), timesteps ); + + auto bhSweep = BoundaryHandling_T::getBlockSweep( boundaryHandlingID ); + auto lbmSweep = lbm::makeCellwiseSweep< LatticeModel_T, FlagField_T >( pdfFieldID, flagFieldID, Fluid_Flag ); + + timeloop.addFuncBeforeTimeStep( RemainingTimeLogger( timeloop.getNrOfTimeSteps() ), "Remaining Time Logger" ); + + // vtk output + if( vtkIOFreq != uint_t(0) ) + { + // spheres + auto particleVtkOutput = make_shared<mesa_pd::vtk::ParticleVtkOutput>(ps); + particleVtkOutput->addOutput<mesa_pd::data::SelectParticleOwner>("owner"); + particleVtkOutput->addOutput<mesa_pd::data::SelectParticleLinearVelocity>("velocity"); + auto particleVtkWriter = vtk::createVTKOutput_PointData(particleVtkOutput, "Particles", vtkIOFreq, baseFolder, "simulation_step"); + timeloop.addFuncBeforeTimeStep( vtk::writeFiles( particleVtkWriter ), "VTK (sphere data)" ); + + // flag field (written only once in the first time step, ghost layers are also written) + //auto flagFieldVTK = vtk::createVTKOutput_BlockData( blocks, "flag_field", timesteps, FieldGhostLayers, false, baseFolder ); + //flagFieldVTK->addCellDataWriter( make_shared< field::VTKWriter< FlagField_T > >( flagFieldID, "FlagField" ) ); + //timeloop.addFuncBeforeTimeStep( vtk::writeFiles( flagFieldVTK ), "VTK (flag field data)" ); + + // pdf field + auto pdfFieldVTK = vtk::createVTKOutput_BlockData( blocks, "fluid_field", vtkIOFreq, 0, false, baseFolder ); + + blockforest::communication::UniformBufferedScheme< stencil::D3Q27 > pdfGhostLayerSync( blocks ); + pdfGhostLayerSync.addPackInfo( make_shared< field::communication::PackInfo< PdfField_T > >( pdfFieldID ) ); + pdfFieldVTK->addBeforeFunction( pdfGhostLayerSync ); + + field::FlagFieldCellFilter< FlagField_T > fluidFilter( flagFieldID ); + fluidFilter.addFlag( Fluid_Flag ); + pdfFieldVTK->addCellInclusionFilter( fluidFilter ); + + pdfFieldVTK->addCellDataWriter( make_shared< lbm::VelocityVTKWriter< LatticeModel_T, float > >( pdfFieldID, "VelocityFromPDF" ) ); + pdfFieldVTK->addCellDataWriter( make_shared< lbm::DensityVTKWriter < LatticeModel_T, float > >( pdfFieldID, "DensityFromPDF" ) ); + + timeloop.addFuncBeforeTimeStep( vtk::writeFiles( pdfFieldVTK ), "VTK (fluid field data)" ); + } + + // sweep for updating the particle mapping into the LBM simulation + timeloop.add() << Sweep( lbm_mesapd_coupling::makeMovingParticleMapping<PdfField_T,BoundaryHandling_T>(blocks, pdfFieldID, boundaryHandlingID, particleFieldID, accessor, MO_Flag, FormerMO_Flag, sphereSelector, conserveMomentum), "Particle Mapping" ); + + + + // sweep for restoring PDFs in cells previously occupied by particles + if( useEANReconstructor ) + { + + auto sphereNormalExtrapolationDirectionFinder = make_shared<lbm_mesapd_coupling::SphereNormalExtrapolationDirectionFinder>(blocks); + auto equilibriumAndNonEquilibriumSphereNormalReconstructor = lbm_mesapd_coupling::makeEquilibriumAndNonEquilibriumReconstructor<BoundaryHandling_T>(blocks, boundaryHandlingID, sphereNormalExtrapolationDirectionFinder); + auto reconstructionManager = lbm_mesapd_coupling::makePdfReconstructionManager<PdfField_T,BoundaryHandling_T>(blocks, pdfFieldID, boundaryHandlingID, particleFieldID, accessor, FormerMO_Flag, Fluid_Flag, equilibriumAndNonEquilibriumSphereNormalReconstructor, conserveMomentum); + + timeloop.add() << Sweep( makeSharedSweep(reconstructionManager), "PDF Restore" ); + + } else if( useExtReconstructor ) + { + auto sphereNormalExtrapolationDirectionFinder = make_shared<lbm_mesapd_coupling::SphereNormalExtrapolationDirectionFinder>(blocks); + auto extrapolationSphereNormalReconstructor = lbm_mesapd_coupling::makeExtrapolationReconstructor<BoundaryHandling_T>(blocks, boundaryHandlingID, sphereNormalExtrapolationDirectionFinder, true); + timeloop.add() << Sweep( makeSharedSweep(lbm_mesapd_coupling::makePdfReconstructionManager<PdfField_T,BoundaryHandling_T>(blocks, pdfFieldID, boundaryHandlingID, particleFieldID, accessor, FormerMO_Flag, Fluid_Flag, extrapolationSphereNormalReconstructor, conserveMomentum) ), "PDF Restore" ); + } else + { + timeloop.add() << Sweep( makeSharedSweep(lbm_mesapd_coupling::makePdfReconstructionManager<PdfField_T,BoundaryHandling_T>(blocks, pdfFieldID, boundaryHandlingID, particleFieldID, accessor, FormerMO_Flag, Fluid_Flag, conserveMomentum) ), "PDF Restore" ); + } + + + bool useStreamCollide = true; + + + if( useStreamCollide ) + { + // add LBM communication function and boundary handling sweep (does the hydro force calculations and the no-slip treatment) + timeloop.add() << BeforeFunction( commFunction, "LBM Communication" ) + << Sweep(bhSweep, "Boundary Handling" ); + + // stream + collide LBM step + timeloop.add() << Sweep( makeSharedSweep( lbmSweep ), "cell-wise LB sweep" ); + } + else + { + // collide LBM step + timeloop.add() << Sweep( makeCollideSweep( lbmSweep ), "cell-wise collide LB sweep" ); + + // add LBM communication function and boundary handling sweep (does the hydro force calculations and the no-slip treatment) + timeloop.add() << BeforeFunction( commFunction, "LBM Communication" ) + << Sweep( BoundaryHandling_T::getBlockSweep( boundaryHandlingID ), "Boundary Handling" ); + + // stream LBM step + timeloop.add() << Sweep( makeStreamSweep( lbmSweep ), "cell-wise stream LB sweep" ); + } + + + // evaluation functionality + std::string loggingFileName( baseFolder + "/LoggingSettlingSphere_"); + loggingFileName += std::to_string(fluidType); + loggingFileName += ".txt"; + if( fileIO ) + { + WALBERLA_LOG_INFO_ON_ROOT(" - writing logging output to file \"" << loggingFileName << "\""); + } + SpherePropertyLogger<ParticleAccessor_T> logger( accessor, sphereUid, loggingFileName, fileIO, dx_SI, dt_SI, diameter, -gravitationalForce[2] ); + + + //////////////////////// + // EXECUTE SIMULATION // + //////////////////////// + + WcTimingPool timeloopTiming; + + real_t terminationPosition = real_t(0.51) * diameter; // right before sphere touches the bottom wall + + const bool useOpenMP = false; + + // time loop + for (uint_t i = 0; i < timesteps; ++i ) + { + // perform a single simulation step -> this contains LBM and setting of the hydrodynamic interactions + timeloop.singleStep( timeloopTiming ); + + timeloopTiming["RPD"].start(); + + if( averageForceTorqueOverTwoTimSteps && i!= 0) + { + ps->forEachParticle(useOpenMP, sphereSelector, *accessor, averageHydrodynamicForceTorque, *accessor ); + } + + for(auto subCycle = uint_t(0); subCycle < numRPDSubCycles; ++subCycle ) + { + + if( useVelocityVerlet ) + { + ps->forEachParticle(useOpenMP, mesa_pd::kernel::SelectLocal(), *accessor, vvIntegratorPreForce, *accessor); + syncCall(); + } + + ps->forEachParticle(useOpenMP, sphereSelector, *accessor, addHydrodynamicInteraction, *accessor ); + ps->forEachParticle(useOpenMP, mesa_pd::kernel::SelectLocal(), *accessor, addGravitationalForce, *accessor ); + + // lubrication correction + ps->forEachParticlePairHalf(useOpenMP, mesa_pd::kernel::ExcludeInfiniteInfinite(), *accessor, + [&lubricationCorrectionKernel,rpdDomain](const size_t idx1, const size_t idx2, auto& ac) + { + //TODO change this to storing copy, not reference + mesa_pd::collision_detection::AnalyticContactDetection acd; + acd.getContactThreshold() = lubricationCorrectionKernel.getNormalCutOffDistance(); + mesa_pd::kernel::DoubleCast double_cast; + mesa_pd::mpi::ContactFilter contact_filter; + if (double_cast(idx1, idx2, ac, acd, ac )) + { + if (contact_filter(acd.getIdx1(), acd.getIdx2(), ac, acd.getContactPoint(), *rpdDomain)) + { + double_cast(idx1, idx2, ac, lubricationCorrectionKernel, ac, acd.getContactNormal(), acd.getPenetrationDepth()); + } + } + }, + *accessor ); + + // one could add linked cells here + + // collision response + ps->forEachParticlePairHalf(useOpenMP, mesa_pd::kernel::ExcludeInfiniteInfinite(), *accessor, + [collisionResponse,rpdDomain](const size_t idx1, const size_t idx2, auto& ac) + { + mesa_pd::collision_detection::AnalyticContactDetection acd; + mesa_pd::kernel::DoubleCast double_cast; + mesa_pd::mpi::ContactFilter contact_filter; + if (double_cast(idx1, idx2, ac, acd, ac )) + { + if (contact_filter(acd.getIdx1(), acd.getIdx2(), ac, acd.getContactPoint(), *rpdDomain)) + { + collisionResponse(acd.getIdx1(), acd.getIdx2(), ac, acd.getContactPoint(), acd.getContactNormal(), acd.getPenetrationDepth()); + } + } + }, + *accessor ); + + + reduceProperty.operator()<mesa_pd::ForceTorqueNotification>(*ps); + + + if( useVelocityVerlet ) ps->forEachParticle(useOpenMP, mesa_pd::kernel::SelectLocal(), *accessor, vvIntegratorPostForce, *accessor); + else ps->forEachParticle(useOpenMP, mesa_pd::kernel::SelectLocal(), *accessor, explEulerIntegrator, *accessor); + + syncCall(); + } + + timeloopTiming["RPD"].end(); + + // evaluation + timeloopTiming["Logging"].start(); + logger(i); + timeloopTiming["Logging"].end(); + + // reset after logging here + ps->forEachParticle(useOpenMP, mesa_pd::kernel::SelectAll(), *accessor, resetHydrodynamicForceTorque, *accessor ); + + // check for termination + if ( logger.getPosition() < terminationPosition ) + { + WALBERLA_LOG_INFO_ON_ROOT("Sphere reached terminal position " << logger.getPosition() << " after " << i << " timesteps!"); + break; + } + } + + timeloopTiming.logResultOnRoot(); + + // check the result + if ( !funcTest && !shortrun ) + { + real_t relErr = std::fabs( expectedSettlingVelocity - logger.getMaxVelocity()) / expectedSettlingVelocity; + WALBERLA_LOG_INFO_ON_ROOT( "Expected maximum settling velocity: " << expectedSettlingVelocity ); + WALBERLA_LOG_INFO_ON_ROOT( "Simulated maximum settling velocity: " << logger.getMaxVelocity() ); + WALBERLA_LOG_INFO_ON_ROOT( "Relative error: " << relErr ); + + // the relative error has to be below 10% + WALBERLA_CHECK_LESS( relErr, real_t(0.1) ); + } + + return EXIT_SUCCESS; +} + +} // namespace settling_sphere_mem + +int main( int argc, char **argv ){ + settling_sphere::main(argc, argv); +} diff --git a/tests/lbm_mesapd_coupling/momentum_exchange_method/UpdateParticleMapping.cpp b/tests/lbm_mesapd_coupling/momentum_exchange_method/UpdateParticleMapping.cpp new file mode 100644 index 0000000000000000000000000000000000000000..6ac2b87c83547c946730b5c929809fb2af4f8b38 --- /dev/null +++ b/tests/lbm_mesapd_coupling/momentum_exchange_method/UpdateParticleMapping.cpp @@ -0,0 +1,625 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla is free software: you can +// redistribute it and/or modify it under the terms of the GNU General Public +// License as published by the Free Software Foundation, either version 3 of +// the License, or (at your option) any later version. +// +// waLBerla is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +// for more details. +// +// You should have received a copy of the GNU General Public License along +// with waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file UpdateParticleMapping.cpp +//! \ingroup lbm_mesapd_coupling +//! \author Christoph Rettinger <christoph.rettinger@fau.de> +// +//====================================================================================================================== + +#include "blockforest/Initialization.h" +#include "blockforest/communication/UniformBufferedScheme.h" + +#include "boundary/all.h" + +#include "core/DataTypes.h" +#include "core/Environment.h" +#include "core/debug/Debug.h" +#include "core/debug/TestSubsystem.h" +#include "core/math/all.h" + +#include "field/AddToStorage.h" + +#include "lbm/boundary/all.h" +#include "lbm/field/AddToStorage.h" +#include "lbm/field/PdfField.h" +#include "lbm/lattice_model/D3Q19.h" + +#include "lbm_mesapd_coupling/mapping/ParticleMapping.h" +#include "lbm_mesapd_coupling/momentum_exchange_method/MovingParticleMapping.h" +#include "lbm_mesapd_coupling/momentum_exchange_method/boundary/SimpleBB.h" +#include "lbm_mesapd_coupling/momentum_exchange_method/reconstruction/PdfReconstructionManager.h" +#include "lbm_mesapd_coupling/utility/ParticleSelector.h" +#include "lbm_mesapd_coupling/DataTypes.h" + +#include "mesa_pd/mpi/SyncNextNeighbors.h" +#include "mesa_pd/data/ParticleAccessorWithShape.h" +#include "mesa_pd/data/ParticleStorage.h" +#include "mesa_pd/data/ShapeStorage.h" +#include "mesa_pd/domain/BlockForestDomain.h" +#include "mesa_pd/data/DataTypes.h" +#include "mesa_pd/kernel/SingleCast.h" + +#include "vtk/all.h" +#include "field/vtk/all.h" + +namespace update_particle_mapping +{ + +/////////// +// USING // +/////////// + +using namespace walberla; +using walberla::uint_t; + +using LatticeModel_T = lbm::D3Q19<lbm::collision_model::SRT>; + +using Stencil_T = LatticeModel_T::Stencil; +using PdfField_T = lbm::PdfField<LatticeModel_T>; + +const uint_t FieldGhostLayers = 1; + +using flag_t = walberla::uint8_t; +using FlagField_T = FlagField<flag_t>; + + +/////////// +// FLAGS // +/////////// + +const FlagUID Fluid_Flag ( "fluid" ); +const FlagUID MO_Flag ( "moving obstacle" ); +const FlagUID FormerMO_Flag ( "former moving obstacle" ); +const FlagUID NoSlip_Flag( "no slip" ); + + +///////////////////////////////////// +// BOUNDARY HANDLING CUSTOMIZATION // +///////////////////////////////////// + +template <typename ParticleAccessor> +class MyBoundaryHandling +{ +public: + + using NoSlip_T = lbm::NoSlip< LatticeModel_T, flag_t >; + using MO_T = lbm_mesapd_coupling::SimpleBB< LatticeModel_T, FlagField_T, ParticleAccessor >; + using Type = BoundaryHandling< FlagField_T, Stencil_T, NoSlip_T, MO_T >; + + MyBoundaryHandling( const BlockDataID & flagFieldID, const BlockDataID & pdfFieldID, + const BlockDataID & particleFieldID, const shared_ptr<ParticleAccessor>& ac) : + flagFieldID_( flagFieldID ), pdfFieldID_( pdfFieldID ), particleFieldID_( particleFieldID ), ac_( ac ) {} + + Type * operator()( IBlock* const block, const StructuredBlockStorage* const storage ) const + { + WALBERLA_ASSERT_NOT_NULLPTR( block ); + WALBERLA_ASSERT_NOT_NULLPTR( storage ); + + auto * flagField = block->getData< FlagField_T >( flagFieldID_ ); + auto * pdfField = block->getData< PdfField_T > ( pdfFieldID_ ); + auto * particleField = block->getData< lbm_mesapd_coupling::ParticleField_T > ( particleFieldID_ ); + + const auto fluid = flagField->flagExists( Fluid_Flag ) ? flagField->getFlag( Fluid_Flag ) : flagField->registerFlag( Fluid_Flag ); + + Type * handling = new Type( "moving obstacle boundary handling", flagField, fluid, + NoSlip_T( "NoSlip", NoSlip_Flag, pdfField ), + MO_T("MO_BB", MO_Flag, pdfField, flagField, particleField, ac_, fluid, *storage, *block ) ); + + handling->fillWithDomain( FieldGhostLayers ); + + return handling; + } + +private: + + const BlockDataID flagFieldID_; + const BlockDataID pdfFieldID_; + const BlockDataID particleFieldID_; + + shared_ptr<ParticleAccessor> ac_; + +}; + +template <typename BoundaryHandling_T> +class MappingChecker +{ +public: + MappingChecker(const shared_ptr< StructuredBlockStorage > & blocks, + const BlockDataID & boundaryHandlingID) : + blocks_( blocks ), boundaryHandlingID_( boundaryHandlingID ) + { } + + // check the mapping and check mapped volume against real sphere volume located at position pos + void operator()(std::string testIdentifier, const Vector3<real_t> & pos, real_t sphereRadius ) + { + real_t sphereVolume = math::pi * real_t(4) / real_t(3) * sphereRadius * sphereRadius * sphereRadius; + uint_t cellCounter( uint_t(0) ); + for( auto blockIt = blocks_->begin(); blockIt != blocks_->end(); ++blockIt ) + { + auto * boundaryHandling = blockIt->getData< BoundaryHandling_T >( boundaryHandlingID_ ); + auto * flagField = boundaryHandling->getFlagField(); + + auto xyzSize = flagField->xyzSize(); + + for (auto cellIt : xyzSize) { + Vector3<real_t> cellCenter = blocks_->getBlockLocalCellCenter(*blockIt, cellIt); + real_t distance = (cellCenter - pos).length(); + + if (distance < sphereRadius) { + WALBERLA_CHECK(boundaryHandling->isBoundary(cellIt), + testIdentifier << " Invalid mapping in cell " << cellIt + << " with center at " << cellCenter + << " - expected boundary cell. Distance cell center - particle center = " + << distance << "."); + ++cellCounter; + } + } + } + // mapped volume has to be - approximately - the same as the real volume + real_t mappedVolume = real_c(cellCounter); // dx=1 + WALBERLA_CHECK(std::fabs( mappedVolume - sphereVolume ) / sphereVolume <= real_t(0.1), + testIdentifier << " Mapped volume " << mappedVolume << " does not fit to real sphere volume " << sphereVolume << "."); + } + + // check the mapping of a plane for a given AABB + void operator()(std::string testIdentifier, const math::AABB & planeAABB ) + { + uint_t cellCounter( uint_t(0) ); + for( auto blockIt = blocks_->begin(); blockIt != blocks_->end(); ++blockIt ) + { + auto * boundaryHandling = blockIt->getData< BoundaryHandling_T >( boundaryHandlingID_ ); + auto * flagField = boundaryHandling->getFlagField(); + + auto xyzSize = flagField->xyzSize(); + + for (auto cellIt : xyzSize) { + Vector3<real_t> cellCenter = blocks_->getBlockLocalCellCenter(*blockIt, cellIt); + + if (planeAABB.contains(cellCenter)) { + WALBERLA_CHECK(boundaryHandling->isBoundary(cellIt), + testIdentifier << " Invalid mapping in cell " << cellIt + << " with center at " << cellCenter + << " - expected boundary cell since inside AABB " << planeAABB); + ++cellCounter; + } + } + } + } + +private: + shared_ptr< StructuredBlockStorage > blocks_; + const BlockDataID boundaryHandlingID_; + +}; + +template< typename BoundaryHandling_T> +class MappingResetter +{ +public: + MappingResetter(const shared_ptr< StructuredBlockStorage > & blocks, const BlockDataID & boundaryHandlingID, const BlockDataID & particleFieldID, walberla::id_t invalidUID) : + blocks_( blocks ), boundaryHandlingID_( boundaryHandlingID ), particleFieldID_( particleFieldID ), invalidUID_( invalidUID ) + { } + + void operator()() + { + for( auto blockIt = blocks_->begin(); blockIt != blocks_->end(); ++blockIt ) + { + auto * boundaryHandling = blockIt->getData< BoundaryHandling_T >( boundaryHandlingID_ ); + auto * flagField = boundaryHandling->getFlagField(); + auto * particleField = blockIt->getData< lbm_mesapd_coupling::ParticleField_T >( particleFieldID_ ); + + auto xyzSizeWGl = flagField->xyzSizeWithGhostLayer(); + // reset to domain (fluid) + boundaryHandling->forceDomain(xyzSizeWGl); + + for( auto cellIt = xyzSizeWGl.begin(); cellIt != xyzSizeWGl.end(); ++cellIt ) + { + // reset body field + particleField->get(*cellIt) = invalidUID_; + } + } + } + +private: + shared_ptr< StructuredBlockStorage > blocks_; + const BlockDataID boundaryHandlingID_; + const BlockDataID particleFieldID_; + walberla::id_t invalidUID_; +}; + + + +/*!\brief Test case for the update particle mapping functionality + * + * The following scenarios are tested: + * - two spheres in contact (i.e. overlapping the same cell), then one moves away + * - sphere-plane in contact, then sphere moves away + * - sphere in contact with a no-slip boundary condition, then moves away + * + * The expected behavior in all cases is that the mapping afterwards is still valid and existing boundary conditions are not overwritten. + */ + + +////////// +// MAIN // +////////// +int main( int argc, char **argv ) +{ + debug::enterTestMode(); + + mpi::Environment env( argc, argv ); + + /////////////////////////// + // SIMULATION PROPERTIES // + /////////////////////////// + + bool writeVTK = true; + const real_t omega = real_t(1); + const real_t dx = real_t(1); + const real_t radius = real_t(5); + + /////////////////////////// + // DATA STRUCTURES SETUP // + /////////////////////////// + + Vector3<uint_t> blocksPerDirection(uint_t(1), uint_t(1), uint_t(1)); + Vector3<uint_t> cellsPerBlock(uint_t(30), uint_t(30), uint_t(30)); + Vector3<bool> periodicity(false, false, false); + + auto blocks = blockforest::createUniformBlockGrid( blocksPerDirection[0], blocksPerDirection[1], blocksPerDirection[2], + cellsPerBlock[0], cellsPerBlock[1], cellsPerBlock[2], + dx, + 1, false, false, + periodicity[0], periodicity[1], periodicity[2], + false ); + + // create the lattice model + LatticeModel_T latticeModel = LatticeModel_T( omega ); + + // add PDF field + BlockDataID pdfFieldID = lbm::addPdfFieldToStorage< LatticeModel_T >( blocks, "pdf field (zyxf)", latticeModel, + Vector3<real_t>(real_t(0)), real_t(1), + FieldGhostLayers, field::zyxf ); + + // add flag field + BlockDataID flagFieldID = field::addFlagFieldToStorage<FlagField_T>( blocks, "flag field", FieldGhostLayers ); + + //init data structures + auto ps = std::make_shared<mesa_pd::data::ParticleStorage>(1); + auto ss = std::make_shared<mesa_pd::data::ShapeStorage>(); + using ParticleAccessor_T = mesa_pd::data::ParticleAccessorWithShape; + shared_ptr<ParticleAccessor_T> accessor = make_shared<ParticleAccessor_T>(ps, ss); + auto sphereShape = ss->create<mesa_pd::data::Sphere>( radius ); + auto planeShape = ss->create<mesa_pd::data::HalfSpace>( Vector3<real_t>(real_t(1), real_t(0), real_t(0)) ); + + // add particle field + BlockDataID particleFieldID = field::addToStorage<lbm_mesapd_coupling::ParticleField_T>( blocks, "particle field", accessor->getInvalidUid(), field::zyxf, FieldGhostLayers ); + + // add boundary handling + using BoundaryHandling_T = MyBoundaryHandling<ParticleAccessor_T>::Type; + BlockDataID boundaryHandlingID = blocks->addStructuredBlockData< BoundaryHandling_T >(MyBoundaryHandling<ParticleAccessor_T>( flagFieldID, pdfFieldID, particleFieldID, accessor), "boundary handling" ); + auto bhBlockSweep = BoundaryHandling_T::getBlockSweep( boundaryHandlingID ); + + // reconstructor + auto reconstructionManager = lbm_mesapd_coupling::makePdfReconstructionManager<PdfField_T,BoundaryHandling_T>(blocks, pdfFieldID, boundaryHandlingID, particleFieldID, accessor, FormerMO_Flag, Fluid_Flag, false); + + // vtk + auto flagFieldVTK = vtk::createVTKOutput_BlockData( blocks, "flag_field" ); + flagFieldVTK->addCellDataWriter( make_shared< field::VTKWriter< FlagField_T > >( flagFieldID, "FlagField" ) ); + + // testing utilities + MappingChecker<BoundaryHandling_T> mappingChecker(blocks, boundaryHandlingID); + MappingResetter<BoundaryHandling_T> mappingResetter(blocks, boundaryHandlingID, particleFieldID, accessor->getInvalidUid()); + + // mapping functors + auto regularParticleMapper = lbm_mesapd_coupling::makeMovingParticleMapping<PdfField_T, BoundaryHandling_T>(blocks, pdfFieldID, boundaryHandlingID, particleFieldID, accessor, MO_Flag, FormerMO_Flag, lbm_mesapd_coupling::RegularParticlesSelector(), true ); + auto globalParticleMapper = lbm_mesapd_coupling::makeMovingParticleMapping<PdfField_T, BoundaryHandling_T>(blocks, pdfFieldID, boundaryHandlingID, particleFieldID, accessor, MO_Flag, FormerMO_Flag, lbm_mesapd_coupling::GlobalParticlesSelector(), true ); + + ///////////////////////////// + // SPHERE - SPHERE MAPPING // + ///////////////////////////// + { + std::string testIdentifier("Test: two spheres single moving"); + WALBERLA_LOG_DEVEL_ON_ROOT(testIdentifier << " - started"); + + Vector3<real_t> pos1(10, 10, 9.5); + Vector3<real_t> pos2(19, 10, 9.5); + + walberla::id_t sphere2Uid; + + // create sphere 1 + { + mesa_pd::data::Particle&& p = *ps->create(); + p.setPosition(pos1); + p.setInteractionRadius(radius); + p.setShapeID(sphereShape); + } + + // create sphere 2 + { + mesa_pd::data::Particle&& p = *ps->create(); + p.setPosition(pos2); + p.setInteractionRadius(radius); + p.setShapeID(sphereShape); + sphere2Uid = p.getUid(); + } + + // map + for( auto blockIt = blocks->begin(); blockIt != blocks->end(); ++blockIt ) (regularParticleMapper)(&(*blockIt)); + + if( writeVTK ) flagFieldVTK->write(); + + // check initial mapping + mappingChecker(testIdentifier + " mapping check 1, sphere 1", pos1, radius); + mappingChecker(testIdentifier + " mapping check 1, sphere 2", pos2, radius); + + // carry out boundary handling (sets forces) + for(auto blockIt = blocks->begin(); blockIt != blocks->end(); ++blockIt) bhBlockSweep(&(*blockIt)); + + // check force on particles (should be zero) + ps->forEachParticle(false, lbm_mesapd_coupling::RegularParticlesSelector(), *accessor, [](const size_t idx, const ParticleAccessor_T& ac){ WALBERLA_CHECK_EQUAL(ac.getHydrodynamicForce(idx), Vector3<real_t>(real_t(0)));}, *accessor); + + // update position + auto updatedPos2 = pos2 + Vector3<real_t>(real_t(1), real_t(0), real_t(0)); + accessor->setPosition(accessor->uidToIdx(sphere2Uid),updatedPos2 ); + + // update mapping + for( auto blockIt = blocks->begin(); blockIt != blocks->end(); ++blockIt ) (regularParticleMapper)(&(*blockIt)); + + // PDF reconstruction + for( auto blockIt = blocks->begin(); blockIt != blocks->end(); ++blockIt ) (*reconstructionManager)(&(*blockIt)); + + if( writeVTK ) flagFieldVTK->write(); + + // check updated mapping + mappingChecker(testIdentifier + " mapping check 2, sphere 1", pos1, radius); + mappingChecker(testIdentifier + " mapping check 2, sphere 2", updatedPos2, radius); + + mappingResetter(); + + WALBERLA_LOG_DEVEL_ON_ROOT(testIdentifier << " - ended successfully"); + + // clean up + ps->clear(); + } + + + ///////////////////////////// + // SPHERE - SPHERE MAPPING // + ///////////////////////////// + { + std::string testIdentifier("Test: two spheres both moving"); + WALBERLA_LOG_DEVEL_ON_ROOT(testIdentifier << " - started"); + + Vector3<real_t> pos1(10, 10, 9.5); + Vector3<real_t> pos2(19, 10, 9.5); + + walberla::id_t sphere1Uid; + walberla::id_t sphere2Uid; + + // create sphere 1 + { + mesa_pd::data::Particle&& p = *ps->create(); + p.setPosition(pos1); + p.setInteractionRadius(radius); + p.setShapeID(sphereShape); + sphere1Uid = p.getUid(); + } + + // create sphere 2 + { + mesa_pd::data::Particle&& p = *ps->create(); + p.setPosition(pos2); + p.setInteractionRadius(radius); + p.setShapeID(sphereShape); + sphere2Uid = p.getUid(); + } + + // map + for( auto blockIt = blocks->begin(); blockIt != blocks->end(); ++blockIt ) (regularParticleMapper)(&(*blockIt)); + + if( writeVTK ) flagFieldVTK->write(); + + // check initial mapping + mappingChecker(testIdentifier + " mapping check 1, sphere 1", pos1, radius); + mappingChecker(testIdentifier + " mapping check 1, sphere 2", pos2, radius); + + // carry out boundary handling (sets forces) + for(auto blockIt = blocks->begin(); blockIt != blocks->end(); ++blockIt) bhBlockSweep(&(*blockIt)); + + // check force on particles (should be zero) + ps->forEachParticle(false, lbm_mesapd_coupling::RegularParticlesSelector(), *accessor, [](const size_t idx, const ParticleAccessor_T& ac){ WALBERLA_CHECK_EQUAL(ac.getHydrodynamicForce(idx), Vector3<real_t>(real_t(0)));}, *accessor); + + // update position + auto updatedPos1 = pos1 + Vector3<real_t>(-real_t(1), real_t(0), real_t(0)); + accessor->setPosition(accessor->uidToIdx(sphere1Uid),updatedPos1 ); + auto updatedPos2 = pos2 + Vector3<real_t>(real_t(1), real_t(0), real_t(0)); + accessor->setPosition(accessor->uidToIdx(sphere2Uid),updatedPos2 ); + + // update mapping + for( auto blockIt = blocks->begin(); blockIt != blocks->end(); ++blockIt ) (regularParticleMapper)(&(*blockIt)); + + // PDF reconstruction + for( auto blockIt = blocks->begin(); blockIt != blocks->end(); ++blockIt ) (*reconstructionManager)(&(*blockIt)); + + if( writeVTK ) flagFieldVTK->write(); + + // check updated mapping + mappingChecker(testIdentifier + " mapping check 2, sphere 1", updatedPos1, radius); + mappingChecker(testIdentifier + " mapping check 2, sphere 2", updatedPos2, radius); + + mappingResetter(); + + WALBERLA_LOG_DEVEL_ON_ROOT(testIdentifier << " - ended successfully"); + + // clean up + ps->clear(); + } + + + ///////////////////////////// + // SPHERE - PLANE MAPPING // + ///////////////////////////// + { + std::string testIdentifier("Test: sphere and plane"); + WALBERLA_LOG_DEVEL_ON_ROOT(testIdentifier << " - started"); + + Vector3<real_t> pos1(1, 10, 9.5); + Vector3<real_t> pos2(5, 10, 9.5); + + walberla::id_t sphere2Uid; + + // create plane + { + mesa_pd::data::Particle&& p = *ps->create(true); + p.setPosition(pos1); + p.setShapeID(planeShape); + mesa_pd::data::particle_flags::set(p.getFlagsRef(), mesa_pd::data::particle_flags::INFINITE); + mesa_pd::data::particle_flags::set(p.getFlagsRef(), mesa_pd::data::particle_flags::FIXED); + } + + // create sphere 2 + { + mesa_pd::data::Particle&& p = *ps->create(); + p.setPosition(pos2); + p.setInteractionRadius(radius); + p.setShapeID(sphereShape); + sphere2Uid = p.getUid(); + } + + // map + for( auto blockIt = blocks->begin(); blockIt != blocks->end(); ++blockIt ) (regularParticleMapper)(&(*blockIt)); + for( auto blockIt = blocks->begin(); blockIt != blocks->end(); ++blockIt ) (globalParticleMapper)(&(*blockIt)); + + if( writeVTK ) flagFieldVTK->write(); + + // check initial mapping + math::AABB planeAABB(real_t(0), real_t(0), real_t(0), pos1[0], real_c(cellsPerBlock[1]), real_c(cellsPerBlock[2])); + mappingChecker(testIdentifier + " mapping check 1, plane", planeAABB); + mappingChecker(testIdentifier + " mapping check 1, sphere", pos2, radius); + + // carry out boundary handling (sets forces) + for(auto blockIt = blocks->begin(); blockIt != blocks->end(); ++blockIt) bhBlockSweep(&(*blockIt)); + + // check force on particles (should be zero) + ps->forEachParticle(false, lbm_mesapd_coupling::RegularParticlesSelector(), *accessor, [](const size_t idx, const ParticleAccessor_T& ac){ WALBERLA_CHECK_EQUAL(ac.getHydrodynamicForce(idx), Vector3<real_t>(real_t(0)));}, *accessor); + + // update position + auto updatedPos2 = pos2 + Vector3<real_t>(real_t(1), real_t(0), real_t(0)); + accessor->setPosition(accessor->uidToIdx(sphere2Uid),updatedPos2 ); + + // update mapping + for( auto blockIt = blocks->begin(); blockIt != blocks->end(); ++blockIt ) (regularParticleMapper)(&(*blockIt)); + + // PDF reconstruction + for( auto blockIt = blocks->begin(); blockIt != blocks->end(); ++blockIt ) (*reconstructionManager)(&(*blockIt)); + + if( writeVTK ) flagFieldVTK->write(); + + // check updated mapping + mappingChecker(testIdentifier + " mapping check 2, plane", planeAABB); + mappingChecker(testIdentifier + " mapping check 2, sphere", updatedPos2, radius); + + mappingResetter(); + + WALBERLA_LOG_DEVEL_ON_ROOT(testIdentifier << " - ended successfully"); + + // clean up + ps->clear(); + } + + /////////////////////////////// + // SPHERE - BOUNDARY MAPPING // + /////////////////////////////// + // here, a plane is used to map the no slip boundary condition into the domain + { + std::string testIdentifier("Test: sphere and boundary"); + WALBERLA_LOG_DEVEL_ON_ROOT(testIdentifier << " - started"); + + Vector3<real_t> pos1(1, 10, 9.5); + Vector3<real_t> pos2(5, 10, 9.5); + + walberla::id_t sphere2Uid; + + // create plane + { + mesa_pd::data::Particle&& p = *ps->create(true); + p.setPosition(pos1); + p.setShapeID(planeShape); + mesa_pd::data::particle_flags::set(p.getFlagsRef(), mesa_pd::data::particle_flags::INFINITE); + mesa_pd::data::particle_flags::set(p.getFlagsRef(), mesa_pd::data::particle_flags::FIXED); + } + + // create sphere 2 + { + mesa_pd::data::Particle&& p = *ps->create(); + p.setPosition(pos2); + p.setInteractionRadius(radius); + p.setShapeID(sphereShape); + sphere2Uid = p.getUid(); + } + + // map + lbm_mesapd_coupling::ParticleMappingKernel<BoundaryHandling_T> particleMappingKernel(blocks, boundaryHandlingID); + ps->forEachParticle(false, lbm_mesapd_coupling::GlobalParticlesSelector(), *accessor, particleMappingKernel, *accessor, NoSlip_Flag); + for( auto blockIt = blocks->begin(); blockIt != blocks->end(); ++blockIt ) (regularParticleMapper)(&(*blockIt)); + + if( writeVTK ) flagFieldVTK->write(); + + // check initial mapping + math::AABB planeAABB(real_t(0), real_t(0), real_t(0), pos1[0], real_c(cellsPerBlock[1]), real_c(cellsPerBlock[2])); + mappingChecker(testIdentifier + " mapping check 1, boundary", planeAABB); + mappingChecker(testIdentifier + " mapping check 1, sphere", pos2, radius); + + // carry out boundary handling (sets forces) + for(auto blockIt = blocks->begin(); blockIt != blocks->end(); ++blockIt) bhBlockSweep(&(*blockIt)); + + // check force on particles (should be zero) + ps->forEachParticle(false, lbm_mesapd_coupling::RegularParticlesSelector(), *accessor, [](const size_t idx, const ParticleAccessor_T& ac){ WALBERLA_CHECK_EQUAL(ac.getHydrodynamicForce(idx), Vector3<real_t>(real_t(0)));}, *accessor); + + // update position + auto updatedPos2 = pos2 + Vector3<real_t>(real_t(1), real_t(0), real_t(0)); + accessor->setPosition(accessor->uidToIdx(sphere2Uid),updatedPos2 ); + + // update mapping + for( auto blockIt = blocks->begin(); blockIt != blocks->end(); ++blockIt ) (regularParticleMapper)(&(*blockIt)); + + // PDF reconstruction + for( auto blockIt = blocks->begin(); blockIt != blocks->end(); ++blockIt ) (*reconstructionManager)(&(*blockIt)); + + if( writeVTK ) flagFieldVTK->write(); + + // check updated mapping + mappingChecker(testIdentifier + " mapping check 2, boundary", planeAABB); + mappingChecker(testIdentifier + " mapping check 2, sphere", updatedPos2, radius); + + mappingResetter(); + + WALBERLA_LOG_DEVEL_ON_ROOT(testIdentifier << " - ended successfully"); + + // clean up + ps->clear(); + } + + return 0; + +} + +} //namespace update_particle_mapping + +int main( int argc, char **argv ){ + update_particle_mapping::main(argc, argv); +} diff --git a/tests/lbm_mesapd_coupling/utility/InspectionProbe.cpp b/tests/lbm_mesapd_coupling/utility/InspectionProbe.cpp new file mode 100644 index 0000000000000000000000000000000000000000..c8c4d173e82be628dc3f79aadea30539f3fdf9bf --- /dev/null +++ b/tests/lbm_mesapd_coupling/utility/InspectionProbe.cpp @@ -0,0 +1,222 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla is free software: you can +// redistribute it and/or modify it under the terms of the GNU General Public +// License as published by the Free Software Foundation, either version 3 of +// the License, or (at your option) any later version. +// +// waLBerla is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +// for more details. +// +// You should have received a copy of the GNU General Public License along +// with waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file InspectionProbe.cpp +//! \ingroup lbm_mesapd_coupling +//! \author Christoph Rettinger <christoph.rettinger@fau.de> +// +//====================================================================================================================== + +#include "blockforest/Initialization.h" + +#include "boundary/all.h" + +#include "core/DataTypes.h" +#include "core/Environment.h" +#include "core/SharedFunctor.h" +#include "core/debug/Debug.h" +#include "core/debug/TestSubsystem.h" +#include "core/math/all.h" +#include "core/timing/RemainingTimeLogger.h" +#include "core/logging/all.h" + +#include "domain_decomposition/SharedSweep.h" + +#include "field/AddToStorage.h" + +#include "lbm/boundary/all.h" +#include "lbm/field/AddToStorage.h" +#include "lbm/field/PdfField.h" +#include "lbm/lattice_model/D3Q19.h" + +#include "lbm_mesapd_coupling/mapping/ParticleMapping.h" +#include "lbm_mesapd_coupling/momentum_exchange_method/MovingParticleMapping.h" +#include "lbm_mesapd_coupling/momentum_exchange_method/boundary/CurvedLinear.h" +#include "lbm_mesapd_coupling/utility/ParticleSelector.h" +#include "lbm_mesapd_coupling/utility/InspectionProbe.h" +#include "lbm_mesapd_coupling/DataTypes.h" + +#include "mesa_pd/data/ParticleAccessorWithShape.h" +#include "mesa_pd/data/ParticleStorage.h" +#include "mesa_pd/data/ShapeStorage.h" +#include "mesa_pd/data/DataTypes.h" +#include "mesa_pd/data/shape/Sphere.h" +#include "mesa_pd/domain/BlockForestDomain.h" +#include "mesa_pd/kernel/DoubleCast.h" +#include "mesa_pd/kernel/ParticleSelector.h" + +#include "timeloop/SweepTimeloop.h" + +namespace inspection_probe +{ + +/////////// +// USING // +/////////// + +using namespace walberla; +using walberla::uint_t; + +using LatticeModel_T = lbm::D3Q19< lbm::collision_model::TRT>; + +using Stencil_T = LatticeModel_T::Stencil; +using PdfField_T = lbm::PdfField<LatticeModel_T>; + +using flag_t = walberla::uint8_t; +using FlagField_T = FlagField<flag_t>; + +const uint_t FieldGhostLayers = 1; + +/////////// +// FLAGS // +/////////// + +const FlagUID Fluid_Flag( "fluid" ); +const FlagUID MO_Flag( "moving obstacle" ); + +///////////////////////////////////// +// BOUNDARY HANDLING CUSTOMIZATION // +///////////////////////////////////// +template <typename ParticleAccessor_T> +class MyBoundaryHandling +{ +public: + + using MO_T = lbm_mesapd_coupling::CurvedLinear< LatticeModel_T, FlagField_T, ParticleAccessor_T >; + using Type = BoundaryHandling< FlagField_T, Stencil_T, MO_T >; + + MyBoundaryHandling( const BlockDataID & flagFieldID, const BlockDataID & pdfFieldID, + const BlockDataID & particleFieldID, const shared_ptr<ParticleAccessor_T>& ac) : + flagFieldID_( flagFieldID ), pdfFieldID_( pdfFieldID ), particleFieldID_( particleFieldID ), ac_( ac ) {} + + Type * operator()( IBlock* const block, const StructuredBlockStorage* const storage ) const + { + WALBERLA_ASSERT_NOT_NULLPTR( block ); + WALBERLA_ASSERT_NOT_NULLPTR( storage ); + + auto * flagField = block->getData< FlagField_T >( flagFieldID_ ); + auto * pdfField = block->getData< PdfField_T > ( pdfFieldID_ ); + auto * particleField = block->getData< lbm_mesapd_coupling::ParticleField_T > ( particleFieldID_ ); + + const auto fluid = flagField->flagExists( Fluid_Flag ) ? flagField->getFlag( Fluid_Flag ) : flagField->registerFlag( Fluid_Flag ); + + Type * handling = new Type( "moving obstacle boundary handling", flagField, fluid, + MO_T( "MO", MO_Flag, pdfField, flagField, particleField, ac_, fluid, *storage, *block ) ); + + handling->fillWithDomain( FieldGhostLayers ); + + return handling; + } + +private: + + const BlockDataID flagFieldID_; + const BlockDataID pdfFieldID_; + const BlockDataID particleFieldID_; + + shared_ptr<ParticleAccessor_T> ac_; +}; + + +////////// +// MAIN // +////////// +int main( int argc, char **argv ) +{ + debug::enterTestMode(); + + mpi::Environment env( argc, argv ); + + bool printToScreen = false; + bool printSurroundingState = false; + std::string outputFileName = ""; + + for( int i = 1; i < argc; ++i ) + { + if( std::strcmp( argv[i], "--printToScreen" ) == 0 ) { printToScreen = true; continue; } + if( std::strcmp( argv[i], "--printSurroundingState" ) == 0 ) { printSurroundingState = true; continue; } + if( std::strcmp( argv[i], "--outputFileName" ) == 0 ) { outputFileName = argv[++i]; continue; } + WALBERLA_ABORT("Unrecognized command line argument found: " << argv[i]); + } + + const uint_t length = uint_t(32); + const real_t radius = real_t(5); + + const uint_t XBlocks = uint_t( 1 ); + const uint_t YBlocks = uint_t( 1 ); + const uint_t ZBlocks = uint_t( 1 ); + const uint_t XCells = length / XBlocks; + const uint_t YCells = length / YBlocks; + const uint_t ZCells = length / ZBlocks; + + auto blocks = blockforest::createUniformBlockGrid( XBlocks, YBlocks, ZBlocks, XCells, YCells, ZCells, uint_t(1), true, + true, true, true ); + + + mesa_pd::domain::BlockForestDomain domain(blocks->getBlockForestPointer()); + + auto ps = std::make_shared<mesa_pd::data::ParticleStorage>(1); + auto ss = std::make_shared<mesa_pd::data::ShapeStorage>(); + using ParticleAccessor_T = mesa_pd::data::ParticleAccessorWithShape; + auto accessor = make_shared<ParticleAccessor_T >(ps, ss); + auto sphereShape = ss->create<mesa_pd::data::Sphere>( radius ); + + Vector3<real_t> centerPosition (real_c(length) * real_c(0.5)); + { + mesa_pd::data::Particle&& p = *ps->create(); + p.setPosition(centerPosition); + p.setInteractionRadius(radius); + p.setOwner(mpi::MPIManager::instance()->rank()); + p.setShapeID(sphereShape); + + } + + LatticeModel_T latticeModel = LatticeModel_T( lbm::collision_model::TRT::constructWithMagicNumber( real_t(1) ) ); + + BlockDataID pdfFieldID = lbm::addPdfFieldToStorage< LatticeModel_T >( blocks, "pdf field (zyxf)", latticeModel, + Vector3< real_t >( real_t(0) ), real_t(1), + uint_t(1), field::zyxf ); + + BlockDataID flagFieldID = field::addFlagFieldToStorage<FlagField_T>( blocks, "flag field" ); + + BlockDataID particleFieldID = field::addToStorage<lbm_mesapd_coupling::ParticleField_T>( blocks, "particle field", accessor->getInvalidUid(), field::zyxf, FieldGhostLayers ); + + using BoundaryHandling_T = MyBoundaryHandling<ParticleAccessor_T>::Type; + BlockDataID boundaryHandlingID = blocks->addStructuredBlockData< BoundaryHandling_T >(MyBoundaryHandling<ParticleAccessor_T>( flagFieldID, pdfFieldID, particleFieldID, accessor), "boundary handling" ); + + // map sphere + lbm_mesapd_coupling::MovingParticleMappingKernel<BoundaryHandling_T> movingParticleMappingKernel(blocks, boundaryHandlingID, particleFieldID); + ps->forEachParticle(false, mesa_pd::kernel::SelectAll(), *accessor, movingParticleMappingKernel, *accessor, MO_Flag); + + Vector3<real_t> probeLocation = centerPosition - Vector3<real_t>(radius + real_t(1), real_t(0), real_t(0)); + lbm_mesapd_coupling::InspectionProbe<PdfField_T, BoundaryHandling_T, ParticleAccessor_T> probe(probeLocation, blocks, pdfFieldID, boundaryHandlingID, particleFieldID, accessor, printToScreen, printSurroundingState, outputFileName ); + + real_t rhoAtLocation; + Vector3<real_t> velocityAtLocation; + probe(rhoAtLocation, velocityAtLocation); + + WALBERLA_ASSERT_FLOAT_EQUAL(rhoAtLocation, real_t(1)); + WALBERLA_ASSERT_FLOAT_EQUAL(velocityAtLocation[0], real_t(0)); + WALBERLA_ASSERT_FLOAT_EQUAL(velocityAtLocation[1], real_t(0)); + WALBERLA_ASSERT_FLOAT_EQUAL(velocityAtLocation[2], real_t(0)); + + return EXIT_SUCCESS; +} + +} // namespace inspection_probe + +int main( int argc, char **argv ){ + inspection_probe::main(argc, argv); +} diff --git a/tests/lbm_mesapd_coupling/utility/LubricationCorrection.cpp b/tests/lbm_mesapd_coupling/utility/LubricationCorrection.cpp new file mode 100644 index 0000000000000000000000000000000000000000..ce4be90eb90c37e26e93ff86058a4a3f1784e391 --- /dev/null +++ b/tests/lbm_mesapd_coupling/utility/LubricationCorrection.cpp @@ -0,0 +1,427 @@ +//====================================================================================================================== +// +// This file is part of waLBerla. waLBerla is free software: you can +// redistribute it and/or modify it under the terms of the GNU General Public +// License as published by the Free Software Foundation, either version 3 of +// the License, or (at your option) any later version. +// +// waLBerla is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +// for more details. +// +// You should have received a copy of the GNU General Public License along +// with waLBerla (see COPYING.txt). If not, see <http://www.gnu.org/licenses/>. +// +//! \file LubricationCorrection.cpp +//! \ingroup lbm_mesapd_coupling +//! \author Christoph Rettinger <christoph.rettinger@fau.de> +// +//====================================================================================================================== + +#include "core/DataTypes.h" +#include "core/Environment.h" +#include "core/debug/TestSubsystem.h" +#include "core/math/all.h" + +#include "lbm_mesapd_coupling/utility/LubricationCorrectionKernel.h" + +#include "mesa_pd/collision_detection/AnalyticContactDetection.h" +#include "mesa_pd/data/DataTypes.h" +#include "mesa_pd/data/LinkedCells.h" +#include "mesa_pd/data/ParticleAccessorWithShape.h" +#include "mesa_pd/data/ParticleStorage.h" +#include "mesa_pd/data/ShapeStorage.h" +#include "mesa_pd/kernel/DoubleCast.h" +#include "mesa_pd/kernel/InsertParticleIntoLinkedCells.h" +#include "mesa_pd/kernel/ParticleSelector.h" + +namespace lubrication_correction_test +{ +using namespace walberla; + + +// from Brenner - "The slow motion of a sphere through a viscous fluid towards a plane surface" (1961), Gl. 3.13 +// from Cooley & O'Neill - "On the slow motion generated in a viscous fluid by the approach of a sphere to a plane wall or stationary sphere" (1969) -> Brenners work for free surface is identical to two spheres +//NOTE: Brenner uses a sphere with velocity U and thus calculates the force as F = 6 PI visc R U beta +// thus, the normalization to get beta is F / (..U..). However, we have two approaching spheres here with U and -U as velocities, thus relative velocity of 2U. Therefore, we have to divide this value by 2. +// see also Tab.2 of Cooley & O'Neill and discussion above. +real_t analyticalNonDimForceSphSph( real_t radius, real_t gap ) +{ + uint_t numberOfAdditions = 50; + + real_t h = radius + real_t(0.5)*gap; // half gap because the formula was derived for sph-free surface where the gap is half of the gap here + real_t alpha = std::acosh(h/radius); + + real_t temp = real_t(0); + + for( uint_t n = 1; n < numberOfAdditions; ++n ) + { + real_t nr = real_t(n); + real_t temp1 = real_t(4) * std::cosh( ( nr + real_t(0.5) ) * alpha ) * std::cosh( ( nr + real_t(0.5) ) * alpha ) + + ( real_t(2) * nr + real_t(1) ) * ( real_t(2) * nr + real_t(1) ) * std::sinh( alpha ) * std::sinh( alpha ); + real_t temp2 = real_t(2) * std::sinh( ( real_t(2) * nr + real_t(1) ) * alpha) - + (real_t(2) * nr + real_t(1) ) * std::sinh( real_t(2) * alpha ); + temp += nr * ( nr + real_t(1) ) / ( (real_t(2) * nr - real_t(1) ) * ( real_t(2) * nr + real_t(3) ) ) * ( temp1 / temp2 - real_t(1)); + } + + real_t beta = real_t(4)/real_t(3) * std::sinh(alpha) * temp; + + return beta * real_t(0.5); +} + +// from Brenner - "The slow motion of a sphere through a viscous fluid towards a plane surface" (1961), Gl. 2.19 +real_t analyticalNonDimForceSphWall( real_t radius, real_t gap ) +{ + uint_t numberOfAdditions = 50; + + real_t h = radius + gap; + real_t alpha = std::acosh(h/radius); + + real_t temp = real_t(0); + + for( uint_t n = 1; n < numberOfAdditions; ++n ) + { + real_t nr = real_t(n); + real_t temp1 = real_t(2) * std::sinh( ( real_t(2) * nr + real_t(1) ) * alpha ) + ( real_t(2) * nr + real_t(1) ) * std::sinh(real_t(2) * alpha ); + real_t temp2 = real_t(4) * std::sinh( ( nr + real_t(0.5) ) * alpha) * std::sinh( ( nr + real_t(0.5) ) * alpha) - (real_t(2) * nr + real_t(1)) * (real_t(2) * nr + real_t(1)) * std::sinh(alpha) * std::sinh(alpha); + temp += nr * ( nr + real_t(1) ) / ( (real_t(2) * nr - real_t(1) ) * ( real_t(2) * nr + real_t(3) ) ) * ( temp1 / temp2 - real_t(1)); + } + + real_t lambda = real_t(4)/real_t(3) * std::sinh(alpha) * temp; + + return lambda; + +} + +/*!\brief Checks the implementation of the lubrication correction + * + * the resulting forces of various scenarios are checked for plausibility (sign, fi = -fj, etc) and compared to the analytical function + * note that the lubrication formula of the correction is typically only a simple approximation of the analytical value (an infinite series) which is why differences will be present + * + * Currently, only the normal force is checked + */ + +////////// +// MAIN // +////////// +int main( int argc, char **argv ) +{ + debug::enterTestMode(); + + mpi::Environment env( argc, argv ); + + auto ps = std::make_shared<mesa_pd::data::ParticleStorage>(1); + auto shapeStorage = std::make_shared<mesa_pd::data::ShapeStorage>(); + using ParticleAccessor = mesa_pd::data::ParticleAccessorWithShape; + ParticleAccessor accessor(ps, shapeStorage); + + // note: these are just arbitrary test values and are not necessarily physically meaningful! + real_t cutOffDistance = real_t(0.8); + real_t minimalGapSize = real_t(0.1); + real_t dynamicViscosity = real_t(1.7); + + mesa_pd::kernel::DoubleCast doubleCast; + mesa_pd::collision_detection::AnalyticContactDetection acd; + acd.getContactThreshold() = cutOffDistance; + + + ///////////////////// + // SPHERE - SPHERE // + ///////////////////// + { + real_t sphereRadius = real_t(2); + auto sphereShape = shapeStorage->create<mesa_pd::data::Sphere>( sphereRadius ); + + real_t sphereGapSize = real_t(0.3); + real_t relativeVelocity = real_t(0.7); + + real_t normalizationFactor = real_t(6) * math::pi * dynamicViscosity * sphereRadius * relativeVelocity; + + real_t analyticalLubForce = (analyticalNonDimForceSphSph(sphereRadius, sphereGapSize) - analyticalNonDimForceSphSph(sphereRadius, cutOffDistance)) * normalizationFactor; + + Vector3<real_t> position1(real_t(0), real_t(0), real_t(0)); + Vector3<real_t> position2(real_t(2) * sphereRadius + sphereGapSize, real_t(0), real_t(0)); + Vector3<real_t> position3(real_t(0), real_t(2) * sphereRadius + sphereGapSize, real_t(0)); + Vector3<real_t> position4(real_t(0), real_t(0), real_t(2) * sphereRadius + sphereGapSize); + + mesa_pd::data::Particle&& p1 = *ps->create(); + p1.setPosition(position1); + p1.setShapeID(sphereShape); + p1.setLinearVelocity(Vector3<real_t>(relativeVelocity,-relativeVelocity,real_t(0.5)*relativeVelocity)); + auto idxSph1 = p1.getIdx(); + + mesa_pd::data::Particle&& p2 = *ps->create(); + p2.setPosition(position2); + p2.setShapeID(sphereShape); + p2.setLinearVelocity(Vector3<real_t>(real_t(0),real_t(2),real_t(0))); + auto idxSph2 = p2.getIdx(); + + mesa_pd::data::Particle&& p3 = *ps->create(); + p3.setPosition(position3); + p3.setShapeID(sphereShape); + p3.setLinearVelocity(Vector3<real_t>(real_t(2),real_t(0),real_t(2))); + auto idxSph3 = p3.getIdx(); + + mesa_pd::data::Particle&& p4 = *ps->create(); + p4.setPosition(position4); + p4.setShapeID(sphereShape); + p4.setLinearVelocity(Vector3<real_t>(real_t(0),real_t(0),real_t(-0.5)*relativeVelocity)); + auto idxSph4 = p4.getIdx(); + + lbm_mesapd_coupling::LubricationCorrectionKernel lubCorrFctr(dynamicViscosity, [minimalGapSize](real_t /*r*/){return minimalGapSize;}, cutOffDistance, real_t(0), real_t(0)); + + ps->forEachParticlePairHalf(false, mesa_pd::kernel::ExcludeInfiniteInfinite(), accessor, + [&](const size_t idx1, const size_t idx2) + { + if (doubleCast(idx1, idx2, accessor, acd, accessor )) + { + //note: in parallel simulations, first use the contact filter to assign the pairs to a single process + doubleCast(idx1, idx2, accessor, lubCorrFctr, accessor, acd.getContactNormal(), acd.getPenetrationDepth()); + } + } + ); + + auto force1 = accessor.getForce(idxSph1); + auto force2 = accessor.getForce(idxSph2); + auto force3 = accessor.getForce(idxSph3); + auto force4 = accessor.getForce(idxSph4); + + WALBERLA_CHECK_LESS(force1[0], real_t(0), "Sph1, dir 0, sign check"); + WALBERLA_CHECK_LESS((std::abs(force1[0]) - std::abs(analyticalLubForce))/ std::abs(analyticalLubForce), real_t(0.2), "Sph1, dir 0, rel error check"); + WALBERLA_CHECK_FLOAT_EQUAL(force1[0], -force2[0], "Sph1 - Sph2, dir 0"); + WALBERLA_CHECK_FLOAT_EQUAL(force2[1], real_t(0), "Sph2, dir 1"); + WALBERLA_CHECK_FLOAT_EQUAL(force2[2], real_t(0), "Sph2, dir 2"); + + WALBERLA_CHECK_GREATER(force1[1], real_t(0), "Sph1, dir 1, sign check"); + WALBERLA_CHECK_LESS((std::abs(force1[1]) - std::abs(analyticalLubForce))/ std::abs(analyticalLubForce), real_t(0.2), "Sph1, dir 1, rel error check"); + WALBERLA_CHECK_FLOAT_EQUAL(force1[1], -force3[1], "Sph1 - Sph3, dir 1"); + WALBERLA_CHECK_FLOAT_EQUAL(force3[0], real_t(0), "Sph3, dir 0"); + WALBERLA_CHECK_FLOAT_EQUAL(force3[2], real_t(0), "Sph3, dir 2"); + + WALBERLA_CHECK_LESS(force1[2], real_t(0), "Sph1, dir 2, sign check"); + WALBERLA_CHECK_LESS((std::abs(force1[2]) - std::abs(analyticalLubForce))/ std::abs(analyticalLubForce), real_t(0.2), "Sph1, dir 2, rel error check"); + WALBERLA_CHECK_FLOAT_EQUAL(force1[2], -force4[2], "Sph1 - Sph4, dir 2"); + WALBERLA_CHECK_FLOAT_EQUAL(force4[0], real_t(0), "Sph4, dir 0"); + WALBERLA_CHECK_FLOAT_EQUAL(force4[1], real_t(0), "Sph4, dir 1"); + + // clean up + ps->clear(); + + } + + /////////////////////////////////// + // SPHERE - SPHERE Special Cases // + /////////////////////////////////// + { + real_t sphereRadius = real_t(2); + auto sphereShape = shapeStorage->create<mesa_pd::data::Sphere>( sphereRadius ); + + real_t relativeVelocity = real_t(0.7); + + real_t normalizationFactor = real_t(6) * math::pi * dynamicViscosity * sphereRadius * relativeVelocity; + + Vector3<real_t> position1(real_t(0), real_t(0), real_t(0)); + Vector3<real_t> position2(real_t(2) * sphereRadius - real_t(0.1), real_t(0), real_t(0)); // with overlap + Vector3<real_t> position3(real_t(0), real_t(2) * sphereRadius + real_t(0.3) * minimalGapSize, real_t(0)); // below minimal gap size + Vector3<real_t> position4(real_t(0), real_t(0), real_t(2) * sphereRadius + real_t(1.2) * cutOffDistance); // larger than cutoff distance + + mesa_pd::data::Particle&& p1 = *ps->create(); + p1.setPosition(position1); + p1.setShapeID(sphereShape); + p1.setLinearVelocity(Vector3<real_t>(relativeVelocity,-relativeVelocity,real_t(0.5)*relativeVelocity)); + auto idxSph1 = p1.getIdx(); + + mesa_pd::data::Particle&& p2 = *ps->create(); + p2.setPosition(position2); + p2.setShapeID(sphereShape); + p2.setLinearVelocity(Vector3<real_t>(real_t(0),real_t(2),real_t(0))); + auto idxSph2 = p2.getIdx(); + + mesa_pd::data::Particle&& p3 = *ps->create(); + p3.setPosition(position3); + p3.setShapeID(sphereShape); + p3.setLinearVelocity(Vector3<real_t>(real_t(2),real_t(0),real_t(2))); + auto idxSph3 = p3.getIdx(); + + mesa_pd::data::Particle&& p4 = *ps->create(); + p4.setPosition(position4); + p4.setShapeID(sphereShape); + p4.setLinearVelocity(Vector3<real_t>(real_t(0),real_t(0),real_t(-0.5)*relativeVelocity)); + auto idxSph4 = p4.getIdx(); + + // variant with linked cells + // they can be used to break the O(N^2) complexity + + math::AABB domain(real_t(0), real_t(0), real_t(0), real_t(10), real_t(10), real_t(10)); + real_t spacing = real_t(1.1) * ( sphereRadius + cutOffDistance ); + mesa_pd::data::LinkedCells lc(domain.getExtended(spacing), spacing ); + mesa_pd::kernel::InsertParticleIntoLinkedCells ipilc; + + lc.clear(); + ps->forEachParticle( false, mesa_pd::kernel::SelectAll(), accessor, ipilc, accessor, lc); + + lbm_mesapd_coupling::LubricationCorrectionKernel lubCorrFctr(dynamicViscosity, [minimalGapSize](real_t /*r*/){return minimalGapSize;}, cutOffDistance, real_t(0), real_t(0)); + + lc.forEachParticlePairHalf( false, mesa_pd::kernel::ExcludeInfiniteInfinite(), accessor, + [&](const size_t idx1, const size_t idx2, auto& ac) + { + if (doubleCast(idx1, idx2, ac, acd, ac )) + { + //note: in parallel simulations, first use the contact filter to assign the pairs to a single process + doubleCast(idx1, idx2, ac, lubCorrFctr, ac, acd.getContactNormal(), acd.getPenetrationDepth()); + } + }, + accessor + ); + + auto force1 = accessor.getForce(idxSph1); + auto force2 = accessor.getForce(idxSph2); + auto force3 = accessor.getForce(idxSph3); + auto force4 = accessor.getForce(idxSph4); + + WALBERLA_CHECK_FLOAT_EQUAL(force1[0], real_t(0), "SpecialCase: Sph1, dir 0, sign check"); + WALBERLA_CHECK_FLOAT_EQUAL(force1[0], -force2[0], "SpecialCase: Sph1 - Sph2, dir 0"); + WALBERLA_CHECK_FLOAT_EQUAL(force2[1], real_t(0), "SpecialCase: Sph2, dir 1"); + WALBERLA_CHECK_FLOAT_EQUAL(force2[2], real_t(0), "SpecialCase: Sph2, dir 2"); + + real_t analyticalLubForce = (analyticalNonDimForceSphSph(sphereRadius, minimalGapSize) - analyticalNonDimForceSphSph(sphereRadius, cutOffDistance)) * normalizationFactor; + WALBERLA_CHECK_GREATER(force1[1], real_t(0), "SpecialCase: Sph1, dir 1, sign check"); + WALBERLA_CHECK_LESS((std::abs(force1[1]) - std::abs(analyticalLubForce))/ std::abs(analyticalLubForce), real_t(0.2), "SpecialCase: Sph1, dir 1, rel error check"); + WALBERLA_CHECK_FLOAT_EQUAL(force1[1], -force3[1], "SpecialCase: Sph1 - Sph3, dir 1"); + WALBERLA_CHECK_FLOAT_EQUAL(force3[0], real_t(0), "SpecialCase: Sph3, dir 0"); + WALBERLA_CHECK_FLOAT_EQUAL(force3[2], real_t(0), "SpecialCase: Sph3, dir 2"); + + WALBERLA_CHECK_FLOAT_EQUAL(force1[2], real_t(0), "SpecialCase: Sph1, dir 2, sign check"); + WALBERLA_CHECK_FLOAT_EQUAL(force1[2], -force4[2], "SpecialCase: Sph1 - Sph4, dir 2"); + WALBERLA_CHECK_FLOAT_EQUAL(force4[0], real_t(0), "SpecialCase: Sph4, dir 0"); + WALBERLA_CHECK_FLOAT_EQUAL(force4[1], real_t(0), "SpecialCase: Sph4, dir 1"); + + // clean up + ps->clear(); + + } + + /////////////////// + // SPHERE - WALL // + /////////////////// + + { + Vector3<real_t> wallPosition(real_t(0), real_t(0), real_t(0)); + Vector3<real_t> wallNormal(real_t(0), real_t(0), real_t(1)); + + auto planeShape = shapeStorage->create<mesa_pd::data::HalfSpace>( wallNormal.getNormalized() ); + + real_t sphereRadius = real_t(2); + auto sphereShape = shapeStorage->create<mesa_pd::data::Sphere>( sphereRadius ); + + real_t relativeVelocity = real_t(0.7); + + real_t gapSize = real_t(0.3); + real_t normalizationFactor = real_t(6) * math::pi * dynamicViscosity * sphereRadius * relativeVelocity; + + Vector3<real_t> position1(real_t(0), real_t(0), sphereRadius + gapSize); + Vector3<real_t> position2(real_t( 3) * sphereRadius, real_t(0), sphereRadius + gapSize); + Vector3<real_t> position3(real_t( 6) * sphereRadius, real_t(0), sphereRadius - real_t(0.1)); + Vector3<real_t> position4(real_t( 9) * sphereRadius, real_t(0), sphereRadius + real_t(0.3) * minimalGapSize); + Vector3<real_t> position5(real_t(12) * sphereRadius, real_t(0), sphereRadius + real_t(1.1) * cutOffDistance); + + mesa_pd::data::Particle&& p1 = *ps->create(); + p1.setPosition(position1); + p1.setShapeID(sphereShape); + p1.setLinearVelocity(Vector3<real_t>(real_t(0),real_t(0),relativeVelocity)); + auto idxSph1 = p1.getIdx(); + + // mix up order to test Sph - HSp and HSp - Sph variants + mesa_pd::data::Particle&& pW = *ps->create(true); + pW.setPosition(wallPosition); + pW.setShapeID(planeShape); + auto idxWall = pW.getIdx(); + + mesa_pd::data::Particle&& p2 = *ps->create(); + p2.setPosition(position2); + p2.setShapeID(sphereShape); + p2.setLinearVelocity(Vector3<real_t>(real_t(0),real_t(0),-relativeVelocity)); + auto idxSph2 = p2.getIdx(); + + mesa_pd::data::Particle&& p3 = *ps->create(); + p3.setPosition(position3); + p3.setShapeID(sphereShape); + p3.setLinearVelocity(Vector3<real_t>(real_t(0),real_t(0),relativeVelocity)); + auto idxSph3 = p3.getIdx(); + + mesa_pd::data::Particle&& p4 = *ps->create(); + p4.setPosition(position4); + p4.setShapeID(sphereShape); + p4.setLinearVelocity(Vector3<real_t>(real_t(0),real_t(0),relativeVelocity)); + auto idxSph4 = p4.getIdx(); + + mesa_pd::data::Particle&& p5 = *ps->create(); + p5.setPosition(position5); + p5.setShapeID(sphereShape); + p5.setLinearVelocity(Vector3<real_t>(real_t(0),real_t(0),relativeVelocity)); + auto idxSph5 = p5.getIdx(); + + lbm_mesapd_coupling::LubricationCorrectionKernel lubCorrFctr(dynamicViscosity, [minimalGapSize](real_t /*r*/){return minimalGapSize;}, cutOffDistance, real_t(0), real_t(0)); + + ps->forEachParticlePairHalf(false, mesa_pd::kernel::ExcludeInfiniteInfinite(), accessor, + [&](const size_t idx1, const size_t idx2) + { + if (doubleCast(idx1, idx2, accessor, acd, accessor )) + { + //note: in parallel simulations, first use the contact filter to assign the pairs to a single CPU + doubleCast(idx1, idx2, accessor, lubCorrFctr, accessor, acd.getContactNormal(), acd.getPenetrationDepth()); + } + } + ); + + auto force1 = accessor.getForce(idxSph1); + auto force2 = accessor.getForce(idxSph2); + auto force3 = accessor.getForce(idxSph3); + auto force4 = accessor.getForce(idxSph4); + auto force5 = accessor.getForce(idxSph5); + auto forceW = accessor.getForce(idxWall); + + WALBERLA_CHECK_FLOAT_EQUAL(forceW[0], real_t(0), "Wall, dir 0"); + WALBERLA_CHECK_FLOAT_EQUAL(forceW[1], real_t(0), "Wall, dir 1"); + WALBERLA_CHECK_FLOAT_EQUAL(forceW[2], real_t(0), "Wall, dir 2"); + + real_t analyticalLubForceRegular = (analyticalNonDimForceSphWall(sphereRadius, gapSize) - analyticalNonDimForceSphWall(sphereRadius, cutOffDistance)) * normalizationFactor; + WALBERLA_CHECK_FLOAT_EQUAL(force1[0], real_t(0), "Wall-Sph1, dir 0"); + WALBERLA_CHECK_FLOAT_EQUAL(force1[1], real_t(0), "Wall-Sph1, dir 1"); + WALBERLA_CHECK_LESS(force1[2], real_t(0), "Wall-Sph1, dir 2, sign check"); + WALBERLA_CHECK_LESS((std::abs(force1[2]) - std::abs(analyticalLubForceRegular))/ std::abs(analyticalLubForceRegular), real_t(0.2), "Wall-Sph1, dir 2, rel error check"); + + WALBERLA_CHECK_FLOAT_EQUAL(force2[0], real_t(0), "Wall-Sph2, dir 0"); + WALBERLA_CHECK_FLOAT_EQUAL(force2[1], real_t(0), "Wall-Sph2, dir 1"); + WALBERLA_CHECK_GREATER(force2[2], real_t(0), "Wall-Sph2, dir 2, sign check"); + WALBERLA_CHECK_LESS((std::abs(force2[2]) - std::abs(analyticalLubForceRegular))/ std::abs(analyticalLubForceRegular), real_t(0.2), "Wall-Sph2, dir 2, rel error check"); + + WALBERLA_CHECK_FLOAT_EQUAL(force3[0], real_t(0), "Wall-Sph3, dir 0"); + WALBERLA_CHECK_FLOAT_EQUAL(force3[1], real_t(0), "Wall-Sph3, dir 1"); + WALBERLA_CHECK_FLOAT_EQUAL(force3[2], real_t(0), "Wall-Sph3, dir 2"); + + real_t analyticalLubForceMin = (analyticalNonDimForceSphWall(sphereRadius, minimalGapSize) - analyticalNonDimForceSphWall(sphereRadius, cutOffDistance)) * normalizationFactor; + WALBERLA_CHECK_FLOAT_EQUAL(force4[0], real_t(0), "Wall-Sph4, dir 0"); + WALBERLA_CHECK_FLOAT_EQUAL(force4[1], real_t(0), "Wall-Sph4, dir 1"); + WALBERLA_CHECK_LESS(force4[2], real_t(0), "Wall-Sph4, dir 2, sign check"); + WALBERLA_CHECK_LESS((std::abs(force4[2]) - std::abs(analyticalLubForceMin))/ std::abs(analyticalLubForceMin), real_t(0.2), "Wall-Sph4, dir 2, rel error check"); + + WALBERLA_CHECK_FLOAT_EQUAL(force5[0], real_t(0), "Wall-Sph5, dir 0"); + WALBERLA_CHECK_FLOAT_EQUAL(force5[1], real_t(0), "Wall-Sph5, dir 1"); + WALBERLA_CHECK_FLOAT_EQUAL(force5[2], real_t(0), "Wall-Sph5, dir 2"); + + // clean up + ps->clear(); + + } + + return 0; + +} + +} //namespace lubrication_correction_test + +int main( int argc, char **argv ){ + lubrication_correction_test::main(argc, argv); +}