Immersed Boundary Method
The Immersed Boundary Method (IBM) [Peskin2002] is a mathematical framework for simulation of fluid-structure interaction. In the IBM the forces acting on a certain marker (particle) are distributed (spread) to the nearby fluid grid points via some smeared delta function (see figure below). Furthermore, we can apply the inverse operation, called interpolation, that gathers the surrounding fluid velocity field into the marker positions. The IBM offers us a way to discretize the spreading and interpolation operators via some sophisticated kernels (smeared delta functions of typical width
) often referred to as Peskin kernels.
Furthermore, the spreading and interpolation algorithms will also be applicable to other situations outside its intended purpose. For instance, to spread charges and interpolate electric fields when solving the Poisson equation (see Electrostatics). In general, we can use the spreading and interpolation algorithms here for transforming between a Lagrangian (particles) and an Eulerian (grid) description.
The spreading operator, , transforms a quantity,
, defined at the markers (particles) positions into a field,
, defined on a grid
The interpolation operator () averages a field,
, defined on a grid at the position of a marker,
. We discretize the interpolation operator as
Where the sum goes over all the cells, , in the grid, with centers (or nodes) at
. The function
are the quadrature weights (i.e the volume of the cell). For a regular grid with cell size
, the quadrature weights are simply
.
A representation of the Immersed Boundary. The blue circle represents a particle (with the green cross marking its center). Some quantity (i.e. the force) acting on it can be spread to the grid points inside its radius of action (red crosses). Alternatively, some quantity defined at the green crosses can be interpolated to the particle (via the interpolation operator).
A thorough description of the whole IBM can be found at [Peskin2002], including the equations of motion for an arbitrary structure submerged in an incompressible fluid. However, we will only make use of the properties devised for the kernels.
In particular, Peskin kernels must abide by a series of postulates that intend to maximize computational efficiency (which translates to closer support) while minimizing the discretization effect of the grid (such as translational invariance).
For the sake of simplicity, the first postulate consists in assuming the kernel can be separated as
This allows to state the postulates regarding the one-dimensional function, . Additionally, this form yields
as
.
The second postulate is that
must be continuous for
, avoiding jumps in the quantities spread to, or interpolated from, the grid. The close support postulate says that
, being
a cut off radius. This is our main means for seeking computational efficiency, since reducing the support of the kernel by one cell reduces dramatically the required operations. In particular, if the kernel has a support of
cells in each direction, spreading or interpolating requires visiting
nearby cells, so a support of
requires
cells while a support of
requires just
. Note that the support,
must be large enough to include all cells within
of the point to spread. In the case of a regular grid, this can be achieved by choosing
.
The last basic postulate is required for the kernel to conserve the communicated quantities and it is simply a discrete expression of the fact that the kernel must integrate to unity.
Where are the centers or the cells inside the support.
The next postulate intends to enforce the translational invariance of the distributed quantities as much as possible.
Where is some constant to be determined. This is a weaker version of the condition for exact grid translational invariance
Which states that the coupling between any two points must be a function of their distance. However, it can be shown that satisfying this condition is incompatible with a compact support [Peskin2002]. The above equation attempts to guarantee some degree of translational invariance by imposing a condition on the point with maximum coupling, .
Finally, we can impose conditions on the conservation of the first moments to get increasingly higher order accuracy interpolants (at the expense of wider support)
Where are some constants.
By solving the system of equations given by these conditions, different kernels can be found.
Already defined kernels
UAMMD exposes several kernels at src/misc/IBM_kernels.cuh.
3-point Peskin kernel
In particular, enforcing only the condition for the first moment (with ) we arrive at the so-called 3-point Peskin kernel.
Where the argument represents the fact that the above expression must be evaluated for the absolute value of the separation (since the kernel is symmetrical). The distance is in units of the cell size,
.
4-point Peskin kernel
We can add a more restrictive condition on the integration to unity postulate
Which smooths the contributions of the kernel when using a central difference discretization for the gradient operator.
Solving for with this extra condition yields the classic 4-point Peskin kernel
The main advantage of this kernel is that it interpolates linear functions exactly, and smooth functions are interpolated to second order accuracy. The distance, is in units of the cell size,
.
6-point Peskin kernel
Recently, a new 6-point kernel has been developed that satisfies the moment conditions up to for a special choice of
[Bao2016]. Additionally, it also satisfies the even-odd condition, it is three times differentiable and offers a really good translational invariance compared to similarly supported kernels.
This kernel sets and
.
Solving for using these conditions, and defining the following
We get the expression for the 6-point kernel
The distance, is in units of the cell size,
.
Given its complexity it is advisable to tabulate
. Other Peskin-like kernels can be found by enforcing other conditions, see for example [Yang2009].
Barnett-Magland (BM) kernel
A new kernel, called “exponential of the semicircle”(ES) and here referred to as BM, has been recently developed to improve the efficiency of non-uniform FFT methods [Barnett2019]. This kernel has a simple mathematical expression
Where and
are parameters related to the shape and support (width) of the kernel. The parameter
is the necessary normalization to ensure that the BM kernel integrates to unity. Since it does not have an analytic integral, this factor must be computed numerically. One advantage of BM kernel is that it decays faster than a Gaussian in Fourier space, which is beneficial in spectral methods.
One disadvantage of the kernels above is that we do not know their analytical Fourier transform (in the case of the BM kernel this stems from it not having an analytical integral).
-
class IBM_kernels::BarnettMagland
Note
Contrary to the Peskin kernels, the BM kernel does not provide a support distance by default. The kernel class must be inherited in order to define the getSupport and getMaxSupport methods.
Gaussian kernel
Finally, we can include here for completeness the Gaussian kernel, which can be defined as
Where is the width of the Gaussian.
-
class IBM_kernels::Gaussian
Note
Contrary to the Peskin kernels, the Gaussian kernel does not provide a support distance by default. The kernel class must be inherited in order to define the getSupport and getMaxSupport methods.
Defining a new kernel
Any kernel must adhere to the following interface
-
class IBMKernel
A conceptual interface class for IBM spread/interpolation kernels. The return type of the phi functions:
KernelReturnTypewill be auto deduced and can be anything, as long as theWeightComputeargument passed tospreadandgathercan handle it. Note that the return type will be, more often than not, a simplerealnumber. But there are situations where a more complex return type might be needed, for instance, to spread the value:we will need the return type to be a
real2storingrandphiin each direction.-
__device__ int3 getSupport(real3 pos, int3 cell);
Returns the number of support cells for a marker with position
poslying inside cellcell. Note that this function might just return the same number regardless of the position.
-
int3 getMaxSupport();
Return the maximum support required by the kernel.
-
__device__ KernelReturnType phiX(real r, real3 pos);
Computes the kernel at a distance
rin the X direction. The value of the kernel can depend on the position of the marker, given atpos.
-
__device__ KernelReturnType phiY(real r, real3 pos);
Computes the kernel at a distance
rin the Y direction. The value of the kernel can depend on the position of the marker, given atpos.
-
__device__ int3 getSupport(real3 pos, int3 cell);
Example
//A simple Gaussian kernel compatible with the IBM module.
class Gaussian{
const real prefactor;
const real tau;
const int support;
public:
Gaussian(real width, int support):
prefactor(pow(2.0*M_PI*width*width, -0.5)),
tau(-0.5/(width*width)),
support(support){}
int3 getMaxSupport(){
return {support, support, support};
}
__device__ int3 getSupport(real3 pos, int3 cell){
return getMaxSupport();
}
__device__ real phi(real r, real3 pos) const{
return prefactor*exp(tau*r*r);
}
};
Usage in UAMMD
The IBM class is used to communicate between marker (particle) and grid data.
-
template<class Kernel, class Grid, class Index3D>
class IBM The IBM class is templated to be generic for any kernel, grid type (for instance regular vs others), and layout of the grid data (for instance row vs column major). By default, IBM will use UAMMD’s
Gridtype (a regular grid) and expect a linear indexing of the grid data (so that the data for a cell with coordinates(i,j,k)is expected to be stored ingridData[(j+k*n.y)*n.x + i]). See Advanced functionality on how to modify these behaviors.The basic constructor of the IBM module requires an instance of a
Kernel(see Already defined kernels) and aGridinstance (which can be the regular grid provided by UAMMD or any other class adhering to the Grid concept). TheGridobject will provide information like the dimensions of the grid and domain, or the distance between an arbitrary point and the center of a certain grid point.
-
template<class PosIterator, class QuantityIterator, class GridDataIterator>
void spread(PosIterator q, QuantityIterator f, GridDataIterator gridData, int numberParticles, cudaStream_t st = 0); The basic overload of the spread function takes the position of the markers, the values defined at each marker’s positions and the grid data (stored according to
Index3D). This function will add to each cell,, in
gridDatathe result of. Here
defaults to multiplication, see Advanced functionality. The types of the different quantities are irrelevant as long as the required arithmetics are defined for them (for instance, the weight compute must be able to process the type of the marker data and return a type for which the
GridDataIterator::value_type::operator+=()exists.
-
template<class PosIterator, class QuantityIterator, class GridDataIterator>
void gather(PosIterator q, QuantityIterator v, GridDataIterator gridData, int numberParticles, cudaStream_t st = 0); The basic overload of the interpolation function takes the position of the markers, the values defined at each marker’s positions and the grid data (stored according to
Index3D). This function will add to each particles value,, the result of
, where
is the value stored for cell
. Here
defaults to multiplication. The
of each cell defaults to
, the volume of a grid cell provided by the
Gridobject. See Advanced functionality for more information. The types of the different quantities are irrelevant as long as the required arithmetics are defined for them (for instance, the weight compute must be able to process the type of the grid data and return a type for which theGridDataIterator::value_type::operator+=()exists.
Example
Spreading and interpolating using most of the default behavior in the IBM module.
#include<uammd.cuh>
#include<misc/IBM.cuh>
#include<misc/IBM_kernels.cuh>
using namespace uammd;
int main(){
using Kernel = IBM_kernels::Peskin::threePoint;
//using Kernel = IBM_kernels::Peskin::fourPoint;
//using Kernel = IBM_kernels::GaussianFlexible::sixPoint;
real L = 128;
Grid grid(Box({L,L,L}), make_int3(L, L, L));
int3 n = grid.cellDim;
//Initialize some arbitrary per particle data
thrust::device_vector<real> markerData(numberParticles);
thrust::fill(markerData.begin(), markerData.end(), 1.0);
thrust::device_vector<real3> markerPositions(numberParticles);
{ //Initialize some arbitrary positions
std::mt19937 gen(sys->rng().next());
std::uniform_real_distribution<real> dist(-0.5, 0.5);
auto rng = [&](){return dist(gen);};
std::generate(markerPositions.begin(), markerPositions.end(), [&](){ return make_real3(rng(), rng(), rng())*L;});
}
//Allocate the output grid data
thrust::device_vector<real> gridData(n.x*n.y*n.z);
thrust::fill(gridData.begin(), gridData.end(), 0);
auto kernel = std::make_shared<Kernel>(grid.cellSize.x);
IBM<Kernel> ibm(kernel, grid);
auto pos_ptr = thrust::raw_pointer_cast(markerPositions.data());
auto markerData_ptr = thrust::raw_pointer_cast(markerData.data());
auto gridData_ptr = thrust::raw_pointer_cast(gridData.data());
ibm.spread(pos_ptr, markerData_ptr, gridData_ptr, numberParticles);
//We can now go back to the particles, performing the inverse operation (interpolation).
thrust::fill(markerData.begin(), markerData.end(), 0);
ibm.gather(pos_ptr, markerData_ptr, gridData_ptr, numberParticles);
return 0;
}
Note
The value types of the marker and grid data are irrelevant, as long as the arithmetics of the types are well defined. We could, for instance, have both the grid and marker data be of type real3 to spread/interp 3 values at once.
Note
By default, the IBM module expects the grid data to be stored in a row-major format. The data for a cell with coordinates (i,j,k) is expected to be stored in gridData[(j+k*n.y)*n.x + i]. This default indexing can be modified, see Advanced functionality below.
Advanced functionality
- The IBM module offers a deep level of customization, besides the functionality described above (mainly the selection of an arbitrary kernel and grid/marker data types) we can:
Modify the memory layout of the grid data.
Modify the
Gridtype (a regular grid by default).Modify how the multiplication of the kernel and a marker value is performed when spreading (or the kernel and a grid value when interpolating).
Modify the quadrature weights of each grid point in the gather operation.
Changing the default indexing of the grid data
By default, the IBM module looks for the data of cell (i,j,k) at gridData[(j+k*n.y)*n.x + i]. The IBM class exposes an optional template parameter that allows to modify this assumption.
A constructor that allows to specialize the module with a different indexing for the grid data. The default type for
Index3DisIBM_ns::LinearIndex3D.
-
class Index3D
A conceptual functor that provides the index in the grid data arrays in
IBMgiven its coordinates.-
__device__ int operator()(int i, int j, int k);
The parenthesis operator must take the 3D coordinates of a cell and return the index of the value in the grid data arrays. The default
Index3Dtype is constructed with the grid dimensions,nx,ny,nz, and its parenthesis operator returns(j+k*n.y)*n.x + i.
-
__device__ int operator()(int i, int j, int k);
Example
Using the IBM module with a non-default indexing of the grid data arrays.
struct LinearIndex3D{
LinearIndex3D(int nx, int ny, int nz):nx(nx), ny(ny), nz(nz){}
__device__ int operator()(int i, int j, int k) const{
return i + nx*(j+ny*k);
}
private:
const int nx, ny, nz;
};
int main(){
//Grid grid; Assume a grid instance is available
using Kernel = IBM_kernels::Peskin::threePoint;
auto kernel = std::make_shared<Kernel>(grid.cellSize.x);
int3 n = grid.cellDim;
LinearIndex3D cell2index(n.x, n.y, n.z);
IBM<Kernel, Grid, LinearIndex3D> ibm(kernel, grid, cell2index);
//Now ibm.spread() and ibm.gather() will look for the data of cell (i,j,k) at cell2index(i,j,k).
return 0;
}
Using a non-default Grid
Any Grid type adhering to the Grid concept can be used to specialize IBM.
#include<uammd.cuh>
#include<misc/IBM.cuh>
#include<misc/IBM_kernels.cuh>
#include<utils/Grid.cuh>
using namespace uammd;
//A grid that simply inherits everything from UAMMD's default regular grid
struct MyGrid : public Grid{
MyGrid(Box box, int3 in_cellDim): Grid(box, in_cellDim){}
};
int main(){
//MyGrid grid; Assume a grid instance is available
using Kernel = IBM_kernels::Peskin::threePoint;
auto kernel = std::make_shared<Kernel>(grid.cellSize.x);
int3 n = grid.cellDim;
IBM<Kernel, MyGrid> ibm(kernel, grid);
//Now ibm.spread() and ibm.gather() will use the rules in MyGrid.
return 0;
}
Note
The Doubly Periodic modules make use of the IBM module with a Chebyshev grid, defined at src/mis/ChebyshevUtils.cuh.
Modify how the multiplication of the kernel and a marker value is performed when spreading (or the kernel and a grid value when interpolating).
-
template<class PosIterator, class ResultIterator, class GridQuantityIterator, class WeightCompute>
void spread(PosIterator pos, ResultIterator Jq, GridQuantityIterator gridData, WeightCompute wc, int numberParticles, cudaStream_t st = 0) This overload of the
spread()function allows to specialize the operation with a non-defaultWeightComputation
-
class WeightComputation
-
template<class T, class T2>
__device__ auto operator()(T value, thrust::tuple<T2, T2, T2> kernel); Takes a value (defined at a markers position in the case of spreading or at a grid point in the case of interpolation) and a kernel evaluation. Returns the value that will be added to the other description (a value at a grid point in the case of spreading and at a markers position in the case of interpolation). The default weight computation will return
value*thrust::get<0>(kernel)*thrust::get<1>(kernel)*thrust::get<2>(kernel);. The typeT2will be determined by the return type of the kernel.
-
template<class T, class T2>
Example
#include<uammd.cuh>
#include<misc/IBM.cuh>
#include<misc/IBM_kernels.cuh>
using namespace uammd;
struct MyWeightCompute{
inline __device__ real operator()(real value, thrust::tuple<real, real, real> kernel) const{
return value*thrust::get<0>(kernel)*thrust::get<1>(kernel)*thrust::get<2>(kernel);
}
};
int main(){
using Kernel = IBM_kernels::Peskin::threePoint;
real L = 128;
Grid grid(Box({L,L,L}), make_int3(L, L, L));
int3 n = grid.cellDim;
//Initialize some arbitrary per particle data
thrust::device_vector<real> markerData(numberParticles);
thrust::fill(markerData.begin(), markerData.end(), 1.0);
thrust::device_vector<real3> markerPositions(numberParticles);
{ //Initialize some arbitrary positions
std::mt19937 gen(sys->rng().next());
std::uniform_real_distribution<real> dist(-0.5, 0.5);
auto rng = [&](){return dist(gen);};
std::generate(markerPositions.begin(), markerPositions.end(), [&](){ return make_real3(rng(), rng(), rng())*L;});
}
//Allocate the output grid data
thrust::device_vector<real> gridData(n.x*n.y*n.z);
thrust::fill(gridData.begin(), gridData.end(), 0);
auto kernel = std::make_shared<Kernel>(grid.cellSize.x);
IBM<Kernel> ibm(kernel, grid);
auto pos_ptr = thrust::raw_pointer_cast(markerPositions.data());
auto markerData_ptr = thrust::raw_pointer_cast(markerData.data());
auto gridData_ptr = thrust::raw_pointer_cast(gridData.data());
MyWeightCompute weightCompute;
ibm.spread(pos_ptr, markerData_ptr, gridData_ptr, weightCompute, numberParticles);
return 0;
}
Modifying the quadrature weights of each grid point in the gather operation
-
template<class PosIterator, class ResultIterator, class GridQuantityIterator, class QuadratureWeights, class WeightCompute>
void gather(PosIterator pos, ResultIterator Jq, GridQuantityIterator gridData, QuadratureWeights qw, WeightCompute wc, int numberParticles, cudaStream_t st = 0) This overload of the
gather()function allows to specialize the operation with a non-defaultQuadratureWeight. Note that if this functionality is required, aWeightComputationmust also be provided, which can just be the default one:IBM_ns::DefaultWeightComputation.
-
class QuadratureWeight
Example
#include<uammd.cuh>
#include<misc/IBM.cuh>
#include<misc/IBM_kernels.cuh>
using namespace uammd;
struct MyQuadratureWeights{
__host__ __device__ real operator()(int3 cellj, const Grid &grid){
return grid.getCellVolume(cellj);
}
};
int main(){
using Kernel = IBM_kernels::Peskin::threePoint;
real L = 128;
Grid grid(Box({L,L,L}), make_int3(L, L, L));
int3 n = grid.cellDim;
//Initialize some arbitrary per particle data
thrust::device_vector<real> markerData(numberParticles);
thrust::fill(markerData.begin(), markerData.end(), 0.0);
thrust::device_vector<real3> markerPositions(numberParticles);
{ //Initialize some arbitrary positions
std::mt19937 gen(sys->rng().next());
std::uniform_real_distribution<real> dist(-0.5, 0.5);
auto rng = [&](){return dist(gen);};
std::generate(markerPositions.begin(), markerPositions.end(), [&](){ return make_real3(rng(), rng(), rng())*L;});
}
//Allocate the output grid data
thrust::device_vector<real> gridData(n.x*n.y*n.z);
thrust::fill(gridData.begin(), gridData.end(), 1.0);
auto kernel = std::make_shared<Kernel>(grid.cellSize.x);
IBM<Kernel> ibm(kernel, grid);
auto pos_ptr = thrust::raw_pointer_cast(markerPositions.data());
auto markerData_ptr = thrust::raw_pointer_cast(markerData.data());
auto gridData_ptr = thrust::raw_pointer_cast(gridData.data());
IBM_ns::DefaultWeightCompute weightCompute;
MyQuadratureWeights quadratureWeights;
ibm.gather(pos_ptr, markerData_ptr, gridData_ptr,
quadratureWeights, weightCompute,
numberParticles);
return 0;
}
References:
The immersed boundary method. Peskin, Charles S. 2002. Acta Numerica 11. https://doi.org/10.1017/S0962492902000077
A Gaussian-like immersed-boundary kernel with three continuous derivatives and improved translational invariance. Yuanxun Bao and Jason Kaye and Charles S. Peskin 2016. Journal of Computational Physics 316. https://www.sciencedirect.com/science/article/pii/S0021999116300663
A Parallel Nonuniform Fast Fourier Transform Library Based on an “Exponential of Semicircle” Kernel. Barnett, Alexander H. and Magland, Jeremy and af Klinteberg, Ludvig 2019. SIAM Journal on Scientific Computing 41. https://doi.org/10.1137/18M120885X
A smoothing technique for discrete delta functions with application to immersed boundary method in moving boundary simulations. Xiaolei Yang et. al. 2009. Journal of Computational Physics 228. https://doi.org/10.1016/j.jcp.2009.07.023