To get started, download the tarball of the most recent version of Palabos, for example palabos-1.0r0.tgz, and unpack it with the command tar xvfz palabos-1.0r0.tgz. At this point, the code of the library is not explicitly compiled and installed. Instead, the library is compiled on-demand when end-user applications are created. This approach reflects the fact that many Palabos users are also developers, and end-user development is coupled with development of the source core. Furthermore, the approach makes it easy to recompile the code on-demand with or without debug flags, or for sequential/parallel execution.
Change into the directory of the tutorial, palabos/examples/tutorial/section_1, and type make to compile the library and create the executable of the first tutorial code, tutorial_1_1.cpp. Finally, the application is executed by typing ./tutorial_1_1 at the command line.
This application simulates the time evolution of an initial-value problem, without boundary conditions. The boundaries are chosen to be periodic, i.e. flow particles leaving the domain on one boundary re-enter the domain on the opposite boundary. The initial condition has a zero velocity and constant density. On a squared sub-domain of the field, the density is initialized at a slightly higher value, to create a perturbation which then generates a flow pattern.
This setup is chosen mostly to illustrate programming concepts in Palabos, and not to propose an interesting hydrodynamic problem. It should in particular be pointed out that the initial condition does not represent the state of an incompressible fluid, because the zero velocity is incompatible with a non-zero density. After sufficient iterations, the flow automatically converges to a situation which is compatible with the physics of an incompressible, or slightly compressible, flow.
The code tutorial_1_1.cpp is listed below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 | /* Code 1.1 in the Palabos tutorial
*/
#include "palabos2D.h"
#include "palabos2D.hh"
#include <iostream>
#include <iomanip>
using namespace plb;
using namespace std;
typedef double T;
#define DESCRIPTOR plb::descriptors::D2Q9Descriptor
// Initialize the lattice at zero velocity and constant density, except
// for a slight density excess on a square sub-domain.
void defineInitialDensityAtCenter(MultiBlockLattice2D<T,DESCRIPTOR>& lattice)
{
// The lattice is of size nx-by-ny
const plint nx = lattice.getNx();
const plint ny = lattice.getNy();
// Create a Box2D which describes the location of cells with a slightly
// higher density.
plint centralSquareRadius = nx/6;
plint centerX = nx/3;
plint centerY = ny/4;
Box2D centralSquare (
centerX - centralSquareRadius, centerX + centralSquareRadius,
centerY - centralSquareRadius, centerY + centralSquareRadius );
// All cells have initially density rho ...
T rho0 = 1.;
// .. except for those in the box "centralSquare" which have density
// rho+deltaRho
T deltaRho = 1.e-4;
Array<T,2> u0(0,0);
// Initialize constant density everywhere.
initializeAtEquilibrium (
lattice, lattice.getBoundingBox(), rho0, u0 );
// And slightly higher density in the central box.
initializeAtEquilibrium (
lattice, centralSquare, rho0 + deltaRho, u0 );
lattice.initialize();
}
int main(int argc, char* argv[]) {
plbInit(&argc, &argv);
global::directories().setOutputDir("./tmp/");
const plint maxIter = 1000; // Iterate during 1000 steps.
const plint nx = 600; // Choice of lattice dimensions.
const plint ny = 600;
const T omega = 1.; // Choice of the relaxation parameter
MultiBlockLattice2D<T, DESCRIPTOR> lattice (
nx, ny, new BGKdynamics<T,DESCRIPTOR>(omega) );
lattice.periodicity().toggleAll(true); // Use periodic boundaries.
defineInitialDensityAtCenter(lattice);
// Main loop over time iterations.
for (plint iT=0; iT<maxIter; ++iT) {
if (iT%40==0) { // Write an image every 40th time step.
pcout << "Writing GIF file at iT=" << iT << endl;
// Instantiate an image writer with the color map "leeloo".
ImageWriter<T> imageWriter("leeloo");
// Write a GIF file with colors rescaled to the range of values
// in the matrix
imageWriter.writeScaledGif (
createFileName("u", iT, 6),
*computeVelocityNorm(lattice) );
}
// Execute lattice Boltzmann iteration.
lattice.collideAndStream();
}
}
|
This tutorial shows that the central structure in which the data of a simulation is stored, is the MultiBlockLatticeXD. It was chosen this way, because the interface of the MultiBlockLatticeXD mimicks regular data arrays with which researchers and engineers are usually familiar from the implementation of simpler problems in languages like Matlab or Fortran. Another advantage of the “regular-block interface” is the ease with which individual lattice cells are identified and treated, for example for the implementation of boundary conditions.
If you are unfamiliar with object-oriented programming, it is likely that at this point you start feeling nervous about this form of data encapsulation and the apparent loss of control over the details of the program workflow. If you stay focused, you will however progressively understand that the behind-the-scene action is simple enough and by no means hidden from the understanding of the programmer. Instead, object-oriented mechanisms make it easier to keep control over a structured and well understood course of action. The principles and mechanisms of the MultiBlockLatticeXD are presented and trained in the Tutorial 2. In particular, Tutorial 2.1 shows that the MultiBlockLatticeXD could in practice be replaced by an AtomicBlockLatticeXD, which stands for a simple regular array, and you should feel free to go ahead and do so. Be aware though that there are in practice only disadvantages in making this substitution, as one loses for example the possibility to parallelize the program. On the other hand, it has however been observed that the use of the simple AtomicBlockLatticeXD can help overcome psychological barriers by providing a sense of control in a first stage of gaining familiarity with Palabos.
In the previous lesson, the initial condition was created by assigning a constant value of density and velocity to domains of rectangular shape. A more detailed initialization of lattice cells is obtained by writing functions in which a different value of density and velocity is attributed to each cell. This is illustrated in the tutorial code 1.2, tutorial_1_2.cpp. To compile this code, edit the file Makefile, and replace tutorial_1_1 by tutorial_1_2 in the corresponding line.
The relevant parts of the code tutorial_1_2.cpp are listed below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | const plint maxIter = 1000; // Iterate during 1000 steps.
const plint nx = 600; // Choice of lattice dimensions.
const plint ny = 600;
const T omega = 1.; // Choice of the relaxation parameter
T rho0 = 1.; // All cells have initially density rho ...
// .. except for those inside the disk which have density
// rho+deltaRho
T deltaRho = 1.e-4;
Array<T,2> u0(0,0);
void initializeConstRho(plint iX, plint iY, T& rho, Array<T,2>& u) {
u = u0;
rho = rho0 + deltaRho;
}
void initializeRhoOnDisk(plint iX, plint iY, T& rho, Array<T,2>& u) {
plint radius = nx/6;
plint centerX = nx/3;
plint centerY = ny/4;
u = u0;
if( (iX-centerX)*(iX-centerX) + (iY-centerY)*(iY-centerY) < radius*radius) {
rho = rho0 + deltaRho;
}
else {
rho = rho0;
}
}
// Initialize the lattice at zero velocity and constant density, except
// for a slight density excess on a circular sub-domain.
void defineInitialDensityAtCenter(MultiBlockLattice2D<T,DESCRIPTOR>& lattice)
{
// Initialize constant density everywhere.
initializeAtEquilibrium (
lattice, lattice.getBoundingBox(), rho0, u0 );
// And slightly higher density in the central box.
initializeAtEquilibrium (
lattice, lattice.getBoundingBox(), initializeRhoOnDisk );
lattice.initialize();
}
|
So far, we have compiled the programs using the default compiler options. Edit the file Makefile to see other available options
Obsolete: the precompiled mode is not available any more in Version 0.7 or newer.
If your program crashes, you may want to recompile in debug mode, with the flag debug=true. In this mode, the program executes additional error checking, which can help in locating the source of the error. Furthermore, debug information is put into the object file, after which you can use a debugger such as gdb.
All codes developed in this tutorial work in serial and parallel. Be aware, though, that most codes execute massively output operations, like the generation of GIF images. They are therefore unlikely to run significantly faster in parallel, unless you comment out the output operations. To compile for parallel execution, choose your C++ compilation command which adds MPI functionality, and define it in the line parallelCXX=.... Then, set the flag MPIparallel=true. Finally, execute your program with the command mpirun or mpiexec or similar.
In this lesson, a few approaches to analyzing the results of a simulation are reviewed. The corresponding code is found in the file tutorial_1_4.cpp
Let us look at the function main in this code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | int main(int argc, char* argv[]) {
plbInit(&argc, &argv);
global::directories().setOutputDir("./tmp/");
MultiBlockLattice2D<T, DESCRIPTOR> lattice (
nx, ny, new BGKdynamics<T,DESCRIPTOR>(omega) );
lattice.periodicity().toggleAll(true); // Set periodic boundaries.
defineInitialDensityAtCenter(lattice);
// First part: loop over time iterations.
for (plint iT=0; iT<maxIter; ++iT) {
lattice.collideAndStream();
}
// Second part: Data analysis.
Array<T,2> velocity;
lattice.get(nx/2, ny/2).computeVelocity(velocity);
pcout << "Velocity in the middle of the lattice: ("
<< velocity[0] << "," << velocity[1] << ")" << endl;
pcout << "Velocity norm along a horizontal line: " << endl;
Box2D line(0, 100, ny/2, ny/2);
pcout << setprecision(3) << *computeVelocityNorm(*extractSubDomain(lattice, line)) << endl;
plb_ofstream ofile("profile.dat");
ofile << setprecision(3) << *computeVelocityNorm(*extractSubDomain(lattice, line)) << endl;
pcout << "Average density in the domain: " << computeAverageDensity(lattice) << endl;
pcout << "Average energy in the domain: " << computeAverageEnergy(lattice) << endl;
}
|
The data written to the file profile.dat is conveniently post-processed with a mathematics processing tool. For example, type the two following lines at the command prompt of the program Octave to plot a curve of the velocity profile:
load profile.dat
plot(profile)
For the first time, we implement a simulation which represents a well-known physical situation: a Poiseuille flow. This flow evolves in a channel with no-slip walls, and the flow velocity is everywhere parallel to the channel walls. The analytical solution of this flow is represented by a parabolic profile for the velocity component parallel to the channel walls. In the simulation, the analytical solution is used to implement Dirichlet boundary condition for the velocity on the domain inlet and outlet. As an initial condition, a zero velocity field is used, and the simulation is started to converge towards the Poiseuille solution in each point of the domain.
Note that the simulation has initially a discontinuity between the zero velocity in the initial condition and the finite velocity in the boundary condition. If the simulation becomes numerically unstable when you play with the parameters, try either increasing the lattice resolution or decreasing the velocity in lattice units.
This time, the full code is reprinted in the tutorial:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 | #include "palabos2D.h"
#include "palabos2D.hh"
#include <vector>
#include <iostream>
#include <iomanip>
/* Code 1.5 in the Palabos tutorial
*/
using namespace plb;
using namespace std;
typedef double T;
#define DESCRIPTOR plb::descriptors::D2Q9Descriptor
/// Velocity on the parabolic Poiseuille profile
T poiseuilleVelocity(plint iY, IncomprFlowParam<T> const& parameters) {
T y = (T)iY / parameters.getResolution();
return 4.*parameters.getLatticeU() * (y-y*y);
}
/// A functional, used to initialize the velocity for the boundary conditions
template<typename T>
class PoiseuilleVelocity {
public:
PoiseuilleVelocity(IncomprFlowParam<T> parameters_)
: parameters(parameters_)
{ }
/// This version of the operator returns the velocity only,
/// to instantiate the boundary condition.
void operator()(plint iX, plint iY, Array<T,2>& u) const {
u[0] = poiseuilleVelocity(iY, parameters);
u[1] = T();
}
/// This version of the operator returns also a constant value for
/// the density, to create the initial condition.
void operator()(plint iX, plint iY, T& rho, Array<T,2>& u) const {
u[0] = poiseuilleVelocity(iY, parameters);
u[1] = T();
rho = (T)1;
}
private:
IncomprFlowParam<T> parameters;
};
void channelSetup (
MultiBlockLattice2D<T,DESCRIPTOR>& lattice,
IncomprFlowParam<T> const& parameters,
OnLatticeBoundaryCondition2D<T,DESCRIPTOR>& boundaryCondition )
{
// Create Velocity boundary conditions.
boundaryCondition.setVelocityConditionOnBlockBoundaries(lattice);
// Specify the boundary velocity.
setBoundaryVelocity (
lattice, lattice.getBoundingBox(),
PoiseuilleVelocity<T>(parameters) );
// Create the initial condition.
initializeAtEquilibrium (
lattice, lattice.getBoundingBox(), PoiseuilleVelocity<T>(parameters) );
lattice.initialize();
}
void writeGifs(MultiBlockLattice2D<T,DESCRIPTOR>& lattice, plint iter)
{
const plint imSize = 600;
ImageWriter<T> imageWriter("leeloo");
imageWriter.writeScaledGif(createFileName("u", iter, 6),
*computeVelocityNorm(lattice),
imSize, imSize );
}
int main(int argc, char* argv[]) {
plbInit(&argc, &argv);
global::directories().setOutputDir("./tmp/");
// Use the class IncomprFlowParam to convert from
// dimensionless variables to lattice units, in the
// context of incompressible flows.
IncomprFlowParam<T> parameters(
(T) 1e-2, // Reference velocity (the maximum velocity
// in the Poiseuille profile) in lattice units.
(T) 100., // Reynolds number
100, // Resolution of the reference length (channel height).
2., // Channel length in dimensionless variables
1. // Channel height in dimensionless variables
);
const T imSave = (T)0.1; // Time intervals at which to save GIF
// images, in dimensionless time units.
const T maxT = (T)3.1; // Total simulation time, in dimensionless
// time units.
writeLogFile(parameters, "Poiseuille flow");
MultiBlockLattice2D<T, DESCRIPTOR> lattice (
parameters.getNx(), parameters.getNy(),
new BGKdynamics<T,DESCRIPTOR>(parameters.getOmega()) );
OnLatticeBoundaryCondition2D<T,DESCRIPTOR>*
boundaryCondition = createLocalBoundaryCondition2D<T,DESCRIPTOR>();
channelSetup(lattice, parameters, *boundaryCondition);
// Main loop over time iterations.
for (plint iT=0; iT*parameters.getDeltaT()<maxT; ++iT) {
if (iT%parameters.nStep(imSave)==0 && iT>0) {
pcout << "Saving Gif at time step " << iT << endl;
writeGifs(lattice, iT);
}
// Execute lattice Boltzmann iteration.
lattice.collideAndStream();
}
delete boundaryCondition;
}
|
To keep everything as simple as possible, all tutorials so far were based on 2D simulations. Turning to 3D is pretty easy, though. Let’s reconsider the Poiseuille flow from the previous tutorial and implement in 3D. The resulting code, shown below, can also be found in the file lesson_1_6.cpp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 | #include "palabos3D.h"
#include "palabos3D.hh"
#include <vector>
#include <iostream>
#include <iomanip>
/* Code 1.6 in the Palabos tutorial
*/
using namespace plb;
using namespace std;
typedef double T;
#define DESCRIPTOR plb::descriptors::D3Q19Descriptor
/// Velocity on the parabolic Poiseuille profile
T poiseuilleVelocity(plint iY, plint iZ, IncomprFlowParam<T> const& parameters) {
T y = (T)iY / parameters.getResolution();
T z = (T)iZ / parameters.getResolution();
return 4.*parameters.getLatticeU() * (y-y*y) * (z-z*z);
}
/// A functional, used to initialize the velocity for the boundary conditions
template<typename T>
class PoiseuilleVelocity {
public:
PoiseuilleVelocity(IncomprFlowParam<T> parameters_)
: parameters(parameters_)
{ }
void operator()(plint iX, plint iY, plint iZ, Array<T,3>& u) const {
u[0] = poiseuilleVelocity(iY, iZ, parameters);
u[1] = T();
u[2] = T();
}
private:
IncomprFlowParam<T> parameters;
};
void channelSetup( MultiBlockLattice3D<T,DESCRIPTOR>& lattice,
IncomprFlowParam<T> const& parameters,
OnLatticeBoundaryCondition3D<T,DESCRIPTOR>& boundaryCondition )
{
// Create Velocity boundary conditions
boundaryCondition.setVelocityConditionOnBlockBoundaries(lattice);
setBoundaryVelocity (
lattice, lattice.getBoundingBox(),
PoiseuilleVelocity<T>(parameters) );
lattice.initialize();
}
void writeGifs(MultiBlockLattice3D<T,DESCRIPTOR>& lattice, plint iter)
{
const plint imSize = 600;
const plint nx = lattice.getNx();
const plint ny = lattice.getNy();
const plint nz = lattice.getNz();
Box3D slice(0, nx-1, 0, ny-1, nz/2, nz/2);
ImageWriter<T> imageWriter("leeloo");
imageWriter.writeScaledGif(createFileName("u", iter, 6),
*computeVelocityNorm(lattice, slice),
imSize, imSize );
}
int main(int argc, char* argv[]) {
plbInit(&argc, &argv);
global::directories().setOutputDir("./tmp/");
// Use the class IncomprFlowParam to convert from
// dimensionless variables to lattice units, in the
// context of incompressible flows.
IncomprFlowParam<T> parameters(
(T) 1e-2, // Reference velocity (the maximum velocity
// in the Poiseuille profile) in lattice units.
(T) 100., // Reynolds number
30, // Resolution of the reference length (channel height).
3., // Channel length in dimensionless variables
1., // Channel height in dimensionless variables
1. // Channel depth in dimensionless variables
);
const T imSave = (T)0.02; // Time intervals at which to save GIF
// images, in dimensionless time units.
const T maxT = (T)2.5; // Total simulation time, in dimensionless
// time units.
writeLogFile(parameters, "3D Poiseuille flow");
MultiBlockLattice3D<T, DESCRIPTOR> lattice (
parameters.getNx(), parameters.getNy(), parameters.getNz(),
new BGKdynamics<T,DESCRIPTOR>(parameters.getOmega()) );
OnLatticeBoundaryCondition3D<T,DESCRIPTOR>*
//boundaryCondition = createInterpBoundaryCondition3D<T,DESCRIPTOR>();
boundaryCondition = createLocalBoundaryCondition3D<T,DESCRIPTOR>();
channelSetup(lattice, parameters, *boundaryCondition);
// Main loop over time iterations.
for (plint iT=0; iT*parameters.getDeltaT()<maxT; ++iT) {
if (iT%parameters.nStep(imSave)==0) {
pcout << "Saving Gif at time step " << iT << endl;
writeGifs(lattice, iT);
}
// Execute lattice Boltzmann iteration.
lattice.collideAndStream();
}
delete boundaryCondition;
}
|
The few lines which have changed from the 2D to the 3D code are the following:
In previous tutorials, two approaches to analyzing the computed numerical data have been used. The first consists of using the class ImageWriter to produce GIF snapshots of the velocity norm, or other scalar flow variables, during the simulation. This approach is particularly useful for obtaining a qualitative impression of the evolution of the flow, and for being able to interrupt the simulation when errors appear. Another, more quantitative approach, consists of writing the data into a file, using a plain ASCII format. In this case, the data stream is linearized, which means that structural information, such as the size of the domain in each space dimension, is lost. In the following code, based on the previous tutorial, the velocity is for example computed on a slice perpendicular to the z-axis, and written into an ASCII file:
// Attribute a value to nx, ny, and nz.
Box3D slice(0, nx-1, 0, ny-1, nz/2, nz/2);
plb_ofstream ofile("slice.dat");
ofile << setprecision(4) << *computeVelocityNorm(lattice, slice) << endl;
As you can see, the C++ stream operators have been overloaded for this task. This means that the output can be formated using I/O manipulators like setprecision(n) to chose the numerical precision, setw(n) to format each number in a cell of fixed width, or fixed respectively scientific to chose the representation of floating point variables. The flow field stored in the file slice.dat can for example be analyzed with help of the open-source program Octave. For this, you need to know the values of nx, ny and nz, because they are not stored in the file:
% Matlab/Octave script
% Assign a value to nx and ny
load slice.dat
slice = reshape(slice, nx, ny);
imagesc(slice)
In the present tutorial, an additional approach is shown in which the data is written into a file in a so-called VTK format, after which it can be analyzed with a general post-processing tool. The following function is used in the tutorial code tutorial_1_7 to write VTK data of the 3D Poiseuille flow presented in tutorial 1.6:
void writeVTK(MultiBlockLattice3D<T,DESCRIPTOR>& lattice,
IncomprFlowParam<T> const& parameters, plint iter)
{
T dx = parameters.getDeltaX();
T dt = parameters.getDeltaT();
VtkImageOutput3D<T> vtkOut(createFileName("vtk", iter, 6), dx);
vtkOut.writeData<float>(*computeDensity(lattice), "density", 1.);
vtkOut.writeData<3,float>(*computeVelocity(lattice), "velocity", dx/dt);
}
The data in the VTK file is represented in dimensionless variables. For this, the variable dx is given to the constructor of the VtkImageOutput3D object in order to indicate the size of a cell spacing. Furthermore, every variable which is written needs to be rescaled according to its units, namely 1. for the density and dx/dt for the velocity. A few comments on the VTK output are in order:
The open-source program Paraview provides a convenient interactive interface for analyzing the data. To end this tutorial, you are encouraged to run the program tutorial_1_7, install Paraview on your system and analyze the output. Among others, Paraview can easily produce an animation from the written image series.