sandbox/huet/src/lagrangian_caps/curvature-ft.h
Compute the curvature of the mesh in 2D and 3D
#if dimension < 3
Two-dimensional curvature computation
In 2D, we compute the curvature by fitting a 4th-degree polynomial to the mesh using two neighbours on each side of the node of interest
void compute_2d_curvature(lagMesh*) {
bool up; // decide if we switch the x and y axes
* cn; // current node
lagNodefor(int i=0; i<mesh->nln; i++) {
= &(mesh->nodes[i]);
cn = (fabs(cn->normal.y) > fabs(cn->normal.x)) ? true : false;
up [5]; // store the coordinates of the current node and of its
coord p// neighbors'
for(int j=0; j<5; j++) {
int index = (mesh->nln + i - 2 + j)%mesh->nln;
foreach_dimension() p[j].x = up ? mesh->nodes[index].pos.x :
->nodes[index].pos.y;
mesh}
If one of the neighboring nodes is across a periodic boundary, we correct its position
for(int j=0; j<5; j++) {
if (j!=2) {
foreach_dimension() {
if (ACROSS_PERIODIC(p[j].x,p[2].x)) {
[j].x += (ACROSS_PERIODIC(p[j].x + L0, p[2].x)) ? -L0 : L0;
p}
}
}
}
Since the bending force will involve taking the laplacian of the curvature, we seek a cuvrature to fourth order accuracy, so we need to interpolate the membrane with a fourth-degree polynomial, which we need to differentiate twice to get the curvature: P_4(x) = \sum_{j=i-2}^{i+2} y_j \prod_{k \neq j} \frac{x - x_j}{x_k - x_j}
P'_4(x) = \sum_{j=i-2}^{i+2} y_j \left( \prod_{k \neq j} \frac{1}{x_k - x_j} \right) \sum_{l \neq j}\prod_{m \neq j, m \neq l} x - x_m
P''_4(x) = \sum_{j=i-2}^{i+2} y_j \left( \prod_{k \neq j} \frac{1}{x_k - x_j} \right) \sum_{l \neq j}\sum_{m \neq j, m \neq l}\sum_{n \neq j, n \neq l, n \neq m}x - x_n
double dy = 0.;
double ddy = 0.;
for(int j=0; j<5; j++) {
double b1 = 0.; double b2 = 0.;
for(int l=0; l<5; l++) {
if (l!=j) {
double c1 = 1.; double c2 = 0.;
for(int m=0; m<5; m++) {
if (m!=j && m!=l) {
double d2 = 1.;
for(int n=0; n<5; n++) {
if (n!=j && n!=l && n!= m) {
*= p[2].x - p[n].x;
d2 }
}
*= p[2].x - p[m].x;
c1 += d2;
c2 }
}
+= c1;
b1 += c2;
b2 }
}
for(int k=0; k<5; k++) {
if (k!=j) {
/= (p[k].x - p[j].x);
b1 /= (p[k].x - p[j].x);
b2 }
}
+= b1*p[j].y;
dy += b2*p[j].y;
ddy }
The formula for the signed curvature of a function y(x) is \kappa = \frac{y''}{(1 + y'^2)^{\frac{3}{2}}}. The sign is dertemined from a parametrization of the curve: walking anticlockwise along the curve, if we turn left the curvature is positive. This statement can be easily written as a dot product between edge i’s normal vector and edge (i+1)’s direction vector.
, b;
coord aforeach_dimension() {
.x = mesh->edges[mesh->nodes[i].edge_ids[0]].normal.x;
a.x = mesh->edges[mesh->nodes[i].edge_ids[1]].normal.x;
b}
int s = (a.x*b.x + a.y*b.y > 0) ? 1 : -1;
->curv = s*fabs(ddy)/cube(sqrt(1 + sq(dy)));
cn}
}
Three-dimensional curvature computation
In 3D things are a little more complicated. Fitting a 4-th degree polynomial to the two-ring neighbors is too cumbersome and instead we fit a second-degree paraboloid to the first-ring neighbors, following the approach of Yazdani and Bagchi.
In most cases the node of interest has six neighbors and the system is overconstrained: we use the least-square method which we perform several times using a new normal vector computed from the paraboloid equation.
Since this method involves inverting small 5 \times 5 matrices, we include the header file matrix_toolbox.h containing helper functions to perform this operation using the LU-decomposition.
#else // dimension == 3
#include "matrix-toolbox.h"
The function below fits a second-degree paraboloid to the one-ring neighbors of a node of the membrane, using the ordinary least-squares method. In the rare (exactly 12) cases where the node has only 5 neighbours, the least squares method reduces to a simple 5x5 matrix inversion.
Input:
- XX is the matrix of regressors, of size 5 \times 6
- \beta is the 5 \times 1 vector of unknowns parametes
- yy is the 6 \times 1 vector of response variables
- perform_least_squares is a boolean indicating if the system is over-determined
Output:
The vector \beta is populated with the parameters minimizing the sum of the square of the error
void fit_paraboloid(double** XX, double* beta, double* yy, bool perform_least_squares) {
if (!perform_least_squares) {
int* P = malloc(6*sizeof(int));
LUPDecompose(XX, 5, 1.e-10, P);
LUPSolve(XX, P, yy, 5, beta);
free(P);
}
else {
int* P = malloc(7*sizeof(int));
We compute \bm{X^T} \bm{X} and its inverse
double** AA = malloc(5*sizeof(double*));
for(int i=0; i<5; i++) AA[i] = malloc(5*sizeof(double));
double** IAA = malloc(5*sizeof(double*));
for(int i=0; i<5; i++) IAA[i] = malloc(5*sizeof(double));
for(int i=0; i<5; i++) {
for(int j=0; j<5; j++) {
[i][j] = 0.;
AAfor(int k=0; k<6; k++) AA[i][j] += XX[k][j]*XX[k][i];
}
}
LUPDecompose(AA, 5, 1.e-10, P);
LUPInvert(AA, P, 5, IAA);
And then we apply the ordinary linear least squares differences: \bm{\beta} = \left( \bm{X^T} \bm{X} \right)^{-1} \bm{X^T}\bm{y}
for(int i=0; i<5; i++) {
[i] = 0.;
betafor(int j=0; j<5; j++) {
double Xty = 0.;
for(int k=0; k<6; k++) Xty += XX[k][j]*yy[k];
[i] += IAA[i][j]*Xty;
beta}
}
free(P);
for(int i=0; i<5; i++) {
free(AA[i]);
free(IAA[i]);
}
free(AA);
free(IAA);
}
}
The function below computes the surface Laplacian (also knows a Laplace-Beltrami operator) at node i of either the membrane (diff_curv = false) or the curvature of the membrane (diff_curv = true).
If diff_curv is set to false, this function populates the curv and gcurv attribute of node i with the mean curvature \kappa = (c_1 + c_2)/2 and the Gaussian curvature \kappa_g = c_1 c_2, with c_1 and c_2 the principal curvatures at node i. In this case the return value is not relevant and set to 0.
If diff_curv is set to true, this function instead returns the surface Laplacian of the (previously computed) curvature.
double laplace_beltrami(lagMesh* mesh, int i, bool diff_curv) {
If we wish to compute \Delta_{LB} \kappa, the result is stored in the variable lbcurv and returned by the function. Otherwise the lbcurv is not used.
double lbcurv = 0.; // lbcurv for "Laplace-Beltrami of the curvature"
* cn = &(mesh->nodes[i]); // cn for "current node"
lagNodeint* ngb = cn->neighbor_ids;
The following three variables are the data structures that will be fed to the least squares method
double** XX = malloc(6*sizeof(double*));
for(int j=0; j<6; j++) XX[j] = malloc(5*sizeof(double));
double* yy = malloc(6*sizeof(double));
double* beta = malloc(5*sizeof(double));
We successively fit a paraboloid to the one-ring neighbors until the vector normal to the paraboloid at the center node is converged.
double normal_change = 1.;
int nb_fit_iterations = 0;
int max_iterations = diff_curv ? 1 : 10;
while (normal_change > 1.e-10 && nb_fit_iterations < max_iterations) {
++; nb_fit_iterations
Create local frame of reference and the linear system to solve
, ey, ez;
coord exforeach_dimension() ez.x = cn->normal.x;
foreach_dimension() ey.x = GENERAL_1DIST(mesh->nodes[ngb[0]].pos.x,
->pos.x);
cndouble eydez, ney, nex;
= 0.; ney = 0.; nex = 0.;
eydez foreach_dimension() eydez += ey.x*ez.x; // eydez for "ey dot ez"
foreach_dimension() ey.x -= eydez*ez.x;
= cnorm(ey);
ney foreach_dimension() ey.x /= ney;
foreach_dimension() ex.x = ey.y*ez.z - ey.z*ez.y;
= cnorm(ex);
nex foreach_dimension() ex.x /= nex;
double M[3][3];
[0][0] = ex.x; M[1][0] = ex.y; M[2][0] = ex.z;
M[0][1] = ey.x; M[1][1] = ey.y; M[2][1] = ey.z;
M[0][2] = ez.x; M[1][2] = ez.y; M[2][2] = ez.z;
M
double ipngb[6][3]; // ipngb for "initial position of neighbors"
double rpngb[6][3]; // rpngb for "rotated position of neighbors"
for(int j=0; j<cn->nb_neighbors; j++) {
[j][0] = GENERAL_1DIST(mesh->nodes[ngb[j]].pos.x, cn->pos.x);
ipngb[j][1] = GENERAL_1DIST(mesh->nodes[ngb[j]].pos.y, cn->pos.y);
ipngb[j][2] = GENERAL_1DIST(mesh->nodes[ngb[j]].pos.z, cn->pos.z);
ipngbfor(int k=0; k<3; k++) {
[j][k] = 0;
rpngbfor(int l=0; l<3; l++) {
[j][k] += M[l][k]*ipngb[j][l];
rpngb}
}
}
Store the coordinates of the neighboring nodes in the appropriate data structure before using the ordinary least squares method.
for(int j=0; j<cn->nb_neighbors; j++) {
[j] = (diff_curv) ? (mesh->nodes[ngb[j]].curv -
yy->nodes[ngb[j]].ref_curv - cn->curv + cn->ref_curv) : rpngb[j][2];
mesh[j][0] = sq(rpngb[j][0]);
XX[j][1] = rpngb[j][0]*rpngb[j][1];
XX[j][2] = sq(rpngb[j][1]);
XX[j][3] = rpngb[j][0];
XX[j][4] = rpngb[j][1];
XX}
Solve the linear system directly (in case of 5 neighbors) or with the least-squares method (in case of 6 neighbors)
bool perform_least_squares = (cn->nb_neighbors > 5);
fit_paraboloid(XX, beta, yy, perform_least_squares);
if (!diff_curv) {
Compute the mean and Gaussian curvatures, as well as a refined normal vector, from the fitted paraboloid.
->curv = -(beta[0] + beta[2] + beta[0]*sq(beta[4])
cn+ beta[2]*sq(beta[3]) - beta[1]*beta[3]*beta[4])
/sqrt(cube(1 + sq(beta[3]) + sq(beta[4])));
->gcurv = (4*beta[0]*beta[2] - sq(beta[1]))
cn/sq(1 + sq(beta[3]) + sq(beta[4]));
, prev_n;
coord buffforeach_dimension() prev_n.x = cn->normal.x;
.x = -beta[3]; buff.y = -beta[4]; buff.z = 1;
buffdouble nn = cnorm(buff);
foreach_dimension() buff.x /= nn;
->normal.x = M[0][0]*buff.x + M[0][1]*buff.y + M[0][2]*buff.z;
cn->normal.y = M[1][0]*buff.x + M[1][1]*buff.y + M[1][2]*buff.z;
cn->normal.z = M[2][0]*buff.x + M[2][1]*buff.y + M[2][2]*buff.z;
cn= 0.;
normal_change foreach_dimension() {
.x = cn->normal.x - prev_n.x;
buffif (fabs(buff.x) > normal_change) normal_change = fabs(buff.x);
}
->nb_fit_iterations = nb_fit_iterations;
cn}
else {
= 2*(beta[0] + beta[2]);
lbcurv }
}
for(int j=0; j<6; j++) free(XX[j]);
free(XX);
free(yy);
free(beta);
return lbcurv;
}
#endif
General computation of the nodal curvatures
The function below computes the signed curvature of the Lagrangian mesh at each node.
void comp_curvature(lagMesh* mesh) {
if (!mesh->updated_curvatures) {
(mesh);
comp_normals#if dimension < 3
compute_2d_curvature(mesh);
#else
for(int i=0; i<mesh->nln; i++) {
laplace_beltrami(mesh, i, false);
}
#endif
->updated_curvatures = true;
mesh}
}
void initialize_refcurv_onecaps(lagMesh* mesh) {
#if REF_CURV
comp_curvature(mesh);
#endif
for(int j=0; j<mesh->nln; j++) {
#if (REF_CURV)
#if GLOBAL_REF_CURV
->nodes[j].ref_curv = C0;
mesh#else
->nodes[j].ref_curv = mesh->nodes[j].curv;
mesh#endif
#else
->nodes[j].ref_curv = 0.;
mesh#endif
}
}
void initialize_refcurv() {
for(int i=0; i<NCAPS; i++) {
if (CAPS(i).isactive)
initialize_refcurv_onecaps(&CAPS(i));
}
}
event init (i = 0) {
#if (RESTART_CASE == 0)
initialize_refcurv();
#endif
}