sandbox/huet/src/lagrangian_caps/capsule-ft.h
Triangulated mesh of a capsule
In this file, we implement a Lagrangian mesh for the front-tracking method, meant to track the position and compute the stresses of an elasitc membrane.
#ifndef dimension
#define dimension 3
#endif
Structure of the mesh
In the Lagrangian mesh, each node is assigned several attributes:
pos
, the node coordinateslagVel
, the node velocity vectornormal
, the vector normal to the membrane at the node coordinatescurv
, the (mean) curvature of the membrane at the node coordinatesref_curv
, the reference (mean) curvature of the membrane at the node coordinates (default is zero)lagForce
, an elastic and/or bending force exerted by the membrane on its surrounding fluidstencil
, a Cache structure used for averaging the neighboring velocities and spreading the Lagrangian force as a body force on the neighboring Eulerian nodes- in case of MPI simulations,
pid
, the rank of the processor owning the Eulerian cell which contains the lagNode edge_ids
, the IDs of its connecting edges: 2 in 2D, up to 6 in 3D (because every considered shape is derived by subdividing in icosahedron, leading to 5 or 6 neighbors only).
In case of 3D simulations, other attributes are introduced:
nb_neighbors
, the number of neighbors of the node, which should only be 5 or 6neighbor_ids
, the IDs of the node neighborsnb_triangles
, the number of triangles having the node as a vertextriangle_ids
, the ids of the above trianglesgcurv
, the Gaussian curvature of the membrane at the node coordinatesnb_fit_iterations
, the number of iterations needed to compute the membrane curvature and normal vector to the desired convergence threshold.
typedef struct lagNode {
coord pos;
coord lagVel;
coord normal;
double curv;
double ref_curv;
coord lagForce;
Cache stencil;
#if _MPI
int pid;
#endif
#if dimension < 3
int edge_ids[2];
#else
int nb_neighbors;
int neighbor_ids[6];
int edge_ids[6];
int nb_triangles;
int triangle_ids[6];
double gcurv;
int nb_fit_iterations;
#endif
} lagNode;
We specify the size of the 5x5(x5) stencil in 2D or 3D.
#if dimension < 3
#define STENCIL_SIZE 25
#else
#define STENCIL_SIZE 125
#endif
Similarly, the edges of the mesh are assigned:
node_ids
, the IDs of the two nodes they connect- In case of 3D simulations,
triangle_ids
, the IDs of the two triangles they separate l0
, the length of the edge in the initial stress-free configurationlength
, the current length of the edgenormal
, a vector normal to the membrane at the edge midpoint.
typedef struct Edge {
int node_ids[2];
#if dimension > 2
int triangle_ids[2];
#endif
double l0;
double length;
coord normal;
} Edge;
In case of 3D simulations, we define triangle faces. Each Triangle
structure has the following attributes:
node_ids
, the IDs of the triangle verticesedge_ids
, the IDs of the triangle edgesarea
, the current area of the trianglenormal
, the normal vector to the triangle pointing outside the membranecentroid
, the coordinates of the triangle centroidrefShape
, the coordinates of the vertices in the Common Plane in the stress-free configuration. By convention the first vertex is always placed at (0, 0, 0), so only the coordinates of the second and third vertex are stored inrefShape
sfc
, the shape function coefficients used in the finite elements method and computed in store_initial_configuration. There are two coefficients per vertex, hence the 3 \times 2 array structure ofsfc
.
#if dimension > 2
typedef struct Triangle {
int node_ids[3];
int edge_ids[3];
double area;
coord normal;
coord centroid;
coord refShape[2];
double sfc[3][2]; // sfc for "shape function coefficients"
double stretch[2];
double tension[2];
} Triangle;
#endif
The lagMesh
structure contains arrays of the previously introduced nodes, edges and triangles. It defines an unstructured mesh, the membrane of our capsule. Its attributes are:
nln
the number of Lagrangian nodesnodes
, the array of Lagrangian nodesnle
, the number of Lagrangian edgesedges
, the array of Lagrangian edges- In case of 3D simulations:
nlt
, the number of Lagrangian trianglestriangles
, the array of Lagrangian triangles
updated_stretches
, a boolean used to check if the current length of the edges has been updated since the last advection of the Lagrangian nodesupdated_normals
, a similar boolean telling if the nodal normal vectors should be recomputedupdated_curvatures
, a last boolean telling if the nodal curvatures should be recomputed.isactive
, a boolean indicating if the capsule exists in the flow (useful when capsules are introduced during a simulation)
typedef struct lagMesh {
int nln;
lagNode* nodes;
int nle;
Edge* edges;
#if dimension > 2
int nlt;
Triangle* triangles;
#endif
coord centroid;
double initial_volume;
double volume;
bool updated_stretches;
bool updated_normals;
bool updated_curvatures;
bool isactive;
} lagMesh;
We denote by NCAPS
the number of Lagrangian meshes, or capsules, in the simulation. It is one by default.
#ifndef NCAPS
#define NCAPS 1
#endif
#ifndef RESTART_CASE
#define RESTART_CASE 0
#endif
The Lagrangian mesh is accessible in the code thanks to the structure below, which is simply an array of Lagrangian meshes (useful when several of them are considered). The macro CAPS(k) can be used as a shortcut to access the k^{th} membrane.
typedef struct Capsules {
lagMesh caps[NCAPS];
int nbcaps;
} Capsules;
Capsules allCaps;
#define CAPS(i) (allCaps.caps[i])
Initialization, memory management and useful macros.
void initialize_empty_capsule(lagMesh* mesh) {
mesh->nln = 0;
mesh->nle = 0;
mesh->nodes = NULL;
mesh->edges = NULL;
#if dimension > 2
mesh->nlt = 0;
mesh->triangles = NULL;
#endif
mesh->updated_stretches = false;
mesh->updated_normals = false;
mesh->updated_curvatures = false;
mesh->isactive = false;
}
void free_one_caps(lagMesh* mesh) {
for(int i=0; i<mesh->nln; i++) free(mesh->nodes[i].stencil.p);
free(mesh->nodes);
free(mesh->edges);
#if dimension > 2
free(mesh->triangles);
#endif
}
void free_all_caps(Capsules* caps) {
for(int i=0; i<caps->nbcaps; i++)
if (CAPS(i).isactive)
free_one_caps(&(caps->caps[i]));
}
Adding capsules after restart
In case we add membranes to a simulation after restart, we need to call the function below.
void initialize_capsules() {
allCaps.nbcaps = NCAPS;
for(int i=0; i<NCAPS; i++) {
if (CAPS(i).isactive) initialize_empty_capsule(&CAPS(i));
}
if (is_constant(a.x)) {
a = new face vector;
foreach_face() a.x[] = 0.;
}
}
void initialize_capsule_stencils(lagMesh* mesh) {
for(int j=0; j<mesh->nln; j++) {
mesh->nodes[j].stencil.n = STENCIL_SIZE;
mesh->nodes[j].stencil.nm = STENCIL_SIZE;
mesh->nodes[j].stencil.p = (Index*) malloc(STENCIL_SIZE*sizeof(Index));
}
}
void initialize_all_capsules_stencils() {
for(int i=0; i<NCAPS; i++)
if (CAPS(i).isactive)
initialize_capsule_stencils(&CAPS(i));
}
void initialize_active_capsule(lagMesh* mesh) {
initialize_empty_capsule(mesh);
mesh->isactive = true;
initialize_capsule_stencils(mesh);
}
The next few macros are useful to compute signed distances and averages across periodic boundaries. We assume for this purpose that the length of the edges are less that half the domain size, which in practice should always be the case.
#define ACROSS_PERIODIC(a,b) (fabs(a - b) > L0/2.)
#define PERIODIC_1DIST(a,b) (fabs(a - L0 - b) > L0/2. ? a + L0 - b : a - L0 - b)
#define GENERAL_1DIST(a,b) (ACROSS_PERIODIC(a,b) ? PERIODIC_1DIST(a,b) : a - b)
#define PERIODIC_1DAVG(a,b) (fabs(a - L0 - b) > L0/2. ? a + L0 + b : a - L0 + b)
#define GENERAL_1DAVG(a,b) (ACROSS_PERIODIC(a,b) ? PERIODIC_1DAVG(a,b) : a + b)
#define GENERAL_SQNORM(a,b) (sq(GENERAL_1DIST(a.x, b.x)) + \
sq(GENERAL_1DIST(a.y, b.y)) + sq(GENERAL_1DIST(a.z, b.z)))
#if dimension < 3
#define cnorm(a) (sqrt(sq(a.x) + sq(a.y)))
#define cdot(a,b) (a.x*b.x + a.y*b.y)
#else
#define cnorm(a) (sqrt(sq(a.x) + sq(a.y) + sq(a.z)))
#define cdot(a,b) (a.x*b.x + a.y*b.y + a.z*b.z)
#endif
Advection of the mesh
The advection have to be compatible with MPI, so we start by including the MPI communication functions. We also include reg-dirac.h, which implements the interpolation of the Eulerian velocities onto the Lagrangian nodes.
By default, the mesh is advected using a second-order two-step Runge Kutta scheme. If the following macro is set to 0, a first-order forward Euler schme is used instead.
#ifndef ADVECT_LAG_RK2
#define ADVECT_LAG_RK2 1
#endif
#if _MPI
#include "capsule-ft-mpi.h"
#endif
#include "ibm-ft.h"
#ifndef CONSERVE_VOLUME
#define CONSERVE_VOLUME 1
#endif
A lot of triangulated mesh-related functions are defined in a separate header file.
#include "geometry-ft.h"
#if CONSERVE_VOLUME
#include "volume-conservation-ft.h"
#endif
The function below advects each Lagrangian node by interpolating the velocities around the node of interest. By default, a second-order Runge Kutta scheme is used. By setting the macro ADVECT_LAG_RK2
to 0, a simple forward Euler scheme is used.
trace
void advect_lagMesh(lagMesh* mesh) {
eul2lag(mesh);
#if !(ADVECT_LAG_RK2)
for(int i=0; i < mesh->nln; i++) {
foreach_dimension() {
mesh->nodes[i].pos.x += dt*mesh->nodes[i].lagVel.x;
}
}
#else
lagMesh buffer_mesh;
buffer_mesh.isactive = true;
buffer_mesh.nln = mesh->nln;
buffer_mesh.nodes = malloc(mesh->nln*sizeof(lagNode));
for(int i=0; i<mesh->nln; i++) {
// Step 1 of RK2
foreach_dimension()
buffer_mesh.nodes[i].pos.x = mesh->nodes[i].pos.x +
.5*dt*mesh->nodes[i].lagVel.x;
}
correct_lag_pos(&buffer_mesh);
for(int j=0; j<buffer_mesh.nln; j++) {
buffer_mesh.nodes[j].stencil.n = STENCIL_SIZE;
buffer_mesh.nodes[j].stencil.nm = STENCIL_SIZE;
buffer_mesh.nodes[j].stencil.p = malloc(STENCIL_SIZE*sizeof(Index));
}
generate_lag_stencils_one_caps(&buffer_mesh);
eul2lag(&buffer_mesh);
for(int i=0; i<mesh->nln; i++) {
// Step 2 of RK2
foreach_dimension()
mesh->nodes[i].pos.x += dt*buffer_mesh.nodes[i].lagVel.x;
}
for(int i=0; i<buffer_mesh.nln; i++) free(buffer_mesh.nodes[i].stencil.p);
free(buffer_mesh.nodes);
#endif
correct_lag_pos(mesh);
#if CONSERVE_VOLUME
enforce_optimal_volume_conservation(mesh);
#endif
comp_centroid(mesh);
comp_volume(mesh);
generate_lag_stencils_one_caps(mesh);
}
Putting the pieces together
Below, we call the above functions at the appropriate time using the Basilisk event syntax.
We start by creating empty Lagrangian meshes, and allocating an acceleration field in case it isn’t done yet by another Basilisk solver.
event defaults (i = 0) {
allCaps.nbcaps = NCAPS;
// #if (RESTART_CASE == 0)
for(int i=0; i<NCAPS; i++) {
initialize_empty_capsule(&CAPS(i));
}
// #endif
if (is_constant(a.x)) {
a = new face vector;
foreach_face() a.x[] = 0.;
}
}
Below, we advect each Lagrangian node using the interpolated Eulerian velocities. We also use this loop as an opportunity to re-initialize the Lagrangian forces to zero.
event tracer_advection(i++) {
for(int i=0; i<NCAPS; i++) {
if (CAPS(i).isactive) {
advect_lagMesh(&CAPS(i));
for(int j=0; j<CAPS(i).nln; j++)
foreach_dimension() CAPS(i).nodes[j].lagForce.x = 0.;
}
}
}
In the acceleration event, we transfer the Lagrangian forces to the fluid using a regularized Dirac function. The acceleration is stored on the cell faces, and will be fed as a source term to the Navier-Stokes solver.
vector forcing[];
event acceleration (i++) {
face vector ae = a;
foreach()
if (cm[] > 1.e-20) foreach_dimension() forcing.x[] = 0.;
for(int i=0; i<NCAPS; i++) {
if (CAPS(i).isactive) lag2eul(forcing, &CAPS(i));
}
foreach_face()
if (fm.x[] > 1.e-20) ae.x[] += .5*alpha.x[]*(forcing.x[] + forcing.x[-1]);
}
At the end of the simulation, we free the allocated memory.
event cleanup (t = end) {
free_all_caps(&allCaps);
}
Additional functionalities
#if dimension > 2
#include "dump-ft.h"
#include "post-processing-ft.h"
#endif
Tests
advect_caps.c: Tests the convergence of the advection scheme.
curvature.c: Tests the computation of the curvature at the Lagrangian nodes. Since the curvature depends on the normals, this case also validates the computation of the normal vectors.