View on GitLab
Inspired by [15].
Domain Setup and Initial State
The double shear-layer problem runs on a (pseudo-)two-dimensional fully periodic domain of $N \times N$ cells. The physical domain size is normalized to the unit square ($[0, 1]^2$), such that the cell spacing in x- and y-direction is $h = \frac{1}{N}$.
The density and velocity fields are initialized to the following state, depending to the initial velocity magnitude $u_0$, shear layer width $\kappa$ and vertical perturbation $\delta$:
\begin{align} \rho(x, y) &= 1 \\ u_x (x, y) &= \begin{cases} u_0 \tanh{ \left( \kappa (y - \frac{1}{4}) \right) } & \text{if } y \le \frac{1}{2} \\ u_0 \tanh{ \left( \kappa (\frac{3}{4} - y) \right) } & \text{if } y > \frac{1}{2} \end{cases} \\ u_y (x, y) &= \delta u_0 \sin{ \left( 2 \pi \left( x + \frac{1}{4} \right) \right) } \end{align}
This example showcases a portable parallel simulation app built with waLBerla V8, solving the transient evolution from the above initial conditions.
Code Generation
The numerical kernels for the double shear-layer application are generated by the script DoubleShearLayerSweeps.py.
Preamble
The preamble of the scripts imports all required functions and classes from the pycodegen packages and SweepGen:
22import pystencils
as ps
24from pystencilssfg
import SourceFileGenerator
26from sweepgen.build_config
import DEBUG
27from sweepgen.symbolic
import cell
28from sweepgen.prefabs
import LbmBulk
Next, we open the code generation manager, enable the walberla::v8 field classes, and set the namespace:
34with SourceFileGenerator()
as sfg:
35 sg.Sweep.use_v8core_fields()
36 sfg.namespace(
"DoubleShearLayer::gen")
Numerical Kernels
The application will comprise three numerical components:
- The lattice Boltzmann bulk dynamics, including PDF initialization and the stream-collide-sweep;
- Setup of the initial state of the density and velocity fields;
- Computation of the vorticity from the velocity field during post-processing.
LBM Bulk Dynamics
We start by defining the lattice Boltzmann method using lbmpy's configuration API. We pass this to the LbmBulk prefab to generate the sweeps for the bulk dynamics:
39 stencil = LBStencil(Stencil.D3Q19)
40 lbm_config = LBMConfig(
44 relaxation_rate=sp.Symbol(
"omega"),
47 lbm_bulk = LbmBulk(sfg,
"LBM", lbm_config)
48 sfg.generate(lbm_bulk)
Initial State
Then, we encode the above equations defining the initial state using SymPy and pystencils.flow, and produce an initialization sweep from them:
51 rho, u = lbm_bulk.rho, lbm_bulk.u
55 @sg.flow.generate_sweep(sfg)
56 def SetInitialState(_eq):
57 u_0, kappa, delta = sp.symbols(
"u_0, kappa, delta")
61 _eq.store[u(0)] = sp.Piecewise(
63 u_0 * sp.tanh(kappa * (cell.y() - sp.Rational(1, 4))),
64 cell.y() <= sp.Rational(1, 2),
67 u_0 * sp.tanh(kappa * (sp.Rational(3, 4) - cell.y())),
68 cell.y() > sp.Rational(1, 2),
73 delta * u_0 * sp.sin(2 * sp.pi * (cell.x() + sp.Rational(1, 4)))
Finite Differences for Vorticity
Finally, we set up a sweep computing the 2D vorticity from the velocity field using finite differences:
82 @sg.flow.generate_sweep(sfg)
83 def ComputeVorticity(_eq):
84 dvx, duy = sp.symbols(
"dvx, duy")
85 vorticity = ps.fields(f
"vorticity: double[{stencil.D}D]", layout=
"fzyx")
87 _eq.let[dvx] = (u[1, 0, 0](1) - u[-1, 0, 0](1)) / (2 * cell.dx())
88 _eq.let[duy] = (u[0, 1, 0](0) - u[0, -1, 0](0)) / (2 * cell.dy())
89 _eq.store[vorticity()] = (dvx - duy) / 2
Application Frame
The C++ simulation application is implemented in DoubleShearLayer.cpp.
Preamble
The application code begins with the inclusion of all required header files, and namespace declarations:
#include "blockforest/all.h"
#include "vtk/all.h"
#include "gen/DoubleShearLayerSweeps.hpp"
Collective header file for module core.
Definition DoubleShearLayer.cpp:39
Collective header file for module stencil.
Collective header file for module timeloop.
Next, we define a few type aliases. We automatically select the memory tag from the build configuration; We set up field types for the scalar and vector fields, as well as the LBM Stencil and PDF field type. These must match the stencil used in the code generator script (see above) in both dimensionality and size of the velocity set.
memtag::automatic MemoryTag
Definition DoubleShearLayer.cpp:45
memory::Field< real_t, 1, MemoryTag > ScalarField_T
Definition DoubleShearLayer.cpp:47
stencil::D3Q19 LbStencil
Definition DoubleShearLayer.cpp:50
Definition GridGeometry.hpp:32
Storage for detected contacts which can be used to perform actions for all contacts,...
Definition FreeSlip.hpp:42
lbm::PdfField< LatticeModel_T > PdfField_T
[typedefs]
Definition 02_LBMLatticeModelGeneration.cpp:60
field::GhostLayerField< real_t, Stencil_T::D > VectorField_T
Definition 03_AdvancedLBMCodegen.cpp:64
Domain and Fields Setup
Next comes the primary workhorse: The run method. It starts off by taking the C command line arguments and forwarding them to the waLBerla environment singleton. Then, options controlling the domain setup are read from the parameter file and the simulation domain is initialized.
void run(
int argc,
char** argv)
{
WALBERLA_CHECK_EQUAL(cellsPerBlock[0], cellsPerBlock[1],
"Number of cells in x- and y- direction must be the same");
AABB domainAabb{ 0., 0., 0., 1., 1.,
real_c(cellsPerBlock[2]) /
real_c(cellsPerBlock[0]) };
std::array< bool, 3 > periodic{ true, true, true };
domainAabb, numBlocks[0], numBlocks[1], numBlocks[2], cellsPerBlock[0], cellsPerBlock[1], cellsPerBlock[2],
true, periodic[0], periodic[1], periodic[2]);
#define WALBERLA_CHECK_EQUAL(...)
Definition CheckFunctions.h:192
Handle for a Block object.
Definition Config.h:263
Parameter< T > getParameter(const std::string &key) const
Returns an extracted parameter.
RAII Object to initialize waLBerla using command line parameters.
Definition Environment.h:39
shared_ptr< Config > config()
Returns configuration object, or null if no configuration object exists.
Definition Environment.h:72
Efficient, generic implementation of a 3-dimensional vector.
Definition Vector3.h:92
void run(int argc, char **argv)
[end aliases]
Definition DoubleShearLayer.cpp:54
shared_ptr< StructuredBlockForest > createUniformBlockGrid(const AABB &domainAABB, const uint_t numberOfXBlocks, const uint_t numberOfYBlocks, const uint_t numberOfZBlocks, const uint_t numberOfXCellsPerBlock, const uint_t numberOfYCellsPerBlock, const uint_t numberOfZCellsPerBlock, const uint_t numberOfXProcesses, const uint_t numberOfYProcesses, const uint_t numberOfZProcesses, const bool xPeriodic, const bool yPeriodic, const bool zPeriodic, const bool keepGlobalBlockInformation)
Function for creating a structured block forest that represents a uniform block grid.
Definition Initialization.cpp:418
GenericAABB< real_t > AABB
Definition AABBFwd.h:33
real_t real_c(T t)
cast to type real_t using "real_c(x)"
Definition DataTypes.h:241
We then add the required fields to the domain:
Parameters and Initial State
It is now time to set up the simulation's initial state, according to parameters provided by the user. These are the Reynolds number, the shear layer width and vertical perturbation, and the initial flow velocity. We read them from the parameter file and use them to set up and object of the initial-state sweep we generated above. We also set up the PDF initialization sweep from the LB bulk prefab. Then, we run both sweeps on all blocks of our simulation domain to prepare the fields:
gen::SetInitialState setInitialState{ blocks, rho, u, delta, kappa, u_0 };
gen::LBM::InitPdfs lbInit{ pdfs, rho, u };
for (auto& b : *blocks)
{
setInitialState(&b);
lbInit(&b);
}
float real_t
Definition DataTypes.h:197
Simulation Loop
The main simulation loop comprises three parts: The LB stream-collide sweep, the vorticity computation, and the ghost layer synchronization. We set up the generated stream-collide sweep after computing the relaxation rate from the Reynolds number:
const real_t nu{ (u_0 * N) / reynolds };
const real_t theta{ 1_r / 3 };
const real_t tau{ nu / theta };
const real_t omega{ 2_r / (2_r * tau + 1_r) };
auto streamCollide = std::make_shared< gen::LBM::StreamCollide >(pdfs, rho, u, omega);
- Note
- The stream-collide sweep uses the so-called pull streaming pattern, which requires a separate temporary array for memory safety. This array is managed internally by the sweep object. Due to this internal state, the stream-collide sweep is not copyable and must therefore be managed through a shared pointer.
Next, we prepare the communication scheme for ghost layer exchange. In addition to the pack info for the PDF field we also register a ghost-layer pack info for the velocity field. This is required by the finite-difference scheme of the vorticity sweep.
.sendDirectlyFromGPU(false)
.sync(u)
.makeShared();
static HaloExchangeBuilder< TCommStencil, TMemTag > create(const std::shared_ptr< StructuredBlockForest > &blocks)
Create a new halo-exchange object through its builder.
Definition HaloExchange.hpp:138
detail::_StreamPullSyncFactory< TStencil > streamPullSync
Definition PackInfoSelection.hpp:80
Finally, we prepare the timeloop and register both sweeps and the communication scheme with it. Also, a time logger is created to print updates on the estimated runtime during the simulation:
loop.add() <<
Sweep(gen::ComputeVorticity{ blocks, u, vorticity });
loop.addFuncAfterTimeStep(logger);
Definition SharedFunctor.h:32
Definition RemainingTimeLogger.h:47
shared_ptr< Loop > loop(const IFunctionNodePtr &body, uint_t iterations, bool logTimeStep=true)
Runs the child node for the given amount of iterations.
Definition ExecutionTree.impl.h:82
FuncCreator< void(IBlock *)> Sweep
Definition SelectableFunctionCreators.h:134
typename timeloop::SweepTimeloop< > SweepTimeloop
Definition SweepTimeloop.h:198
internal::SharedSweep< T > makeSharedSweep(const shared_ptr< T > &sweepPtr)
Definition SharedSweep.h:52
std::size_t uint_t
Definition DataTypes.h:161
Definition SelectableFunctionCreators.h:100
VTK Output for Visualization
One last thing remains to be done before we can run the simulation: We need to export its results in the VTK format for later visualization using ParaView. If VTK is enabled, we create the VTK output object; and register output functions for the density, velocity, and vorticity fields with it; and add it to the time loop:
if (vtkWriteFrequency > 0)
{
"simulation_step", false, true, true, false, 0);
auto densityWriter = make_shared< memory::FieldVtkWriter< ScalarField_T, float32 > >(rho, "density");
vtkOutput->addCellDataWriter(densityWriter);
auto velWriter = make_shared< memory::FieldVtkWriter< VectorField_T, float32 > >(u, "velocity");
vtkOutput->addCellDataWriter(velWriter);
auto vorticityWriter = make_shared< memory::FieldVtkWriter< ScalarField_T, float32 > >(vorticity, "vorticity");
vtkOutput->addCellDataWriter(vorticityWriter);
}
shared_ptr< VTKOutput > createVTKOutput_BlockData(const StructuredBlockStorage &sbs, const std::string &identifier=std::string("block_data"), const uint_t writeFrequency=1, const uint_t ghostLayers=0, const bool forcePVTU=false, const std::string &baseFolder=std::string("vtk_out"), const std::string &executionFolder=std::string("simulation_step"), const bool continuousNumbering=false, const bool binary=true, const bool littleEndian=true, const bool useMPIIO=true, const uint_t initialExecutionCount=0, const bool amrFileFormat=false, const bool oneFilePerProcess=false)
Definition VTKOutput.h:588
VTKOutput::Write writeFiles(const shared_ptr< VTKOutput > &vtk, const bool immediatelyWriteCollectors=true, const int simultaneousIOOperations=0, const Set< SUID > &requiredStates=Set< SUID >::emptySet(), const Set< SUID > &incompatibleStates=Set< SUID >::emptySet())
Definition VTKOutput.h:710
Invoke the Timeloop
Lastly, the timeloop is invoked and the run-function ends:
Of course, the entire application must also have a main function, which is placed at the bottom of the file and invokes run:
int main(
int argc,
char** argv)
{
return EXIT_SUCCESS;
}
int main(int argc, char **argv)
Definition 01_BlocksAndFields.cpp:58
CMake Target Definition
To build the app, we need to register it with CMake. In the beginning of our CMakeLists.txt, we first make sure to link any parameter files to the build directory:
walberla_link_files_to_builddir (*.prm)
We add an executable and register the application frame as a source file. For portability between CPU and GPU, we use walberla_set_gpu_language to set the correct GPU compilation language for our source files.
Then, we register the code generation script such that its output files will be linked against the application. We set the AUTO_LANGUAGE option such that the code generator selects the correct output language for the current GPU target:
walberla_generate_sources (
DoubleShearLayer SCRIPTS DoubleShearLayerSweeps.py AUTO_LANGUAGE)
Finally, we need to link the app against waLBerla's runtime libraries:
target_link_libraries (
)
Build and Run the App
Compilation
If not already done, generate the waLBerla build system with the V8 Core and SweepGen enabled by running this command in the waLBerla project root:
cmake -S . -B build -DWALBERLA_ENABLE_V8CORE=ON -DWALBERLA_ENABLE_SWEEPGEN=ON
Depending on your hardware, you may optionally enable OpenMP, CUDA or HIP for parallelization.
Navigate to build/apps/examples/DoubleShearLayer and build the app:
cd build/apps/examples/DoubleShearLayer
make -j
Parametrization and Execution
We use the following pre-defined parameter file (DoubleShearLayer.prm) to run the simulation:
Domain
{
blocks < 1, 1, 1 >;
cellsPerBlock < 256, 256, 8 >;
}
Parameters
{
Reynolds 30000;
kappa 80;
u_0 0.04;
delta 0.05;
timesteps 7001;
}
Output
{
vtkWriteFrequency 1000;
}
Start the simulation by calling ./DoubleShearLayer DoubleShearLayer.prm. Depending on your hardware, the simulation run can take several minutes. To review the results, open the file vtk_out/vtk.pvd in ParaView and investigate the velocity and vorticity arrays. Vorticity, when viewed from above, should look like this:
Plot of the Vorticity after 7000 time steps