# Curvature of an interface

The curvature field is defined only in interfacial cells. In all the other cells it takes the value nodata.

On trees, we need to redefine the restriction function to take this into account i.e. the curvature of the parent cell is the average of the curvatures in the interfacial child cells.

#if TREE
static void curvature_restriction (Point point, scalar kappa)
{
double k = 0., s = 0.;
foreach_child()
if (kappa[] != nodata)
k += kappa[], s++;
kappa[] = s ? k/s : nodata;
}

The prolongation function performs a similar averaging, but using the same stencil as that used for bilinear interpolation, so that the symmetries of the volume fraction field and curvature field are preserved.

static void curvature_prolongation (Point point, scalar kappa)
{
foreach_child() {
double sk = 0., s = 0.;
for (int i = 0; i <= 1; i++)
#if dimension > 1
for (int j = 0; j <= 1; j++)
#endif
#if dimension > 2
for (int k = 0; k <= 1; k++)
#endif
if (coarse(kappa,child.x*i,child.y*j,child.z*k) != nodata)
sk += coarse(kappa,child.x*i,child.y*j,child.z*k), s++;
kappa[] = s ? sk/s : nodata;
}
}
#endif // TREE

## Height-function curvature and normal

To compute the curvature, we estimate the derivatives of the height functions in a given direction (x, y or z). We first check that all the heights are defined and that their orientations are the same. We then compute the curvature as \displaystyle \kappa = \frac{h_{xx}}{(1 + h_x^2)^{3/2}} in two dimensions, or \displaystyle \kappa = \frac{h_{xx}(1 + h_y^2) + h_{yy}(1 + h_x^2) - 2h_{xy}h_xh_y} {(1 + h_x^2 + h_y^2)^{3/2}} in three dimensions.

The normal is computed in a similar way, but also allowing for asymmetric 2-points stencils and taking into account the orientation.

#include "heights.h"

#if dimension == 2
foreach_dimension()
static double kappa_y (Point point, vector h)
{
int ori = orientation(h.y[]);
for (int i = -1; i <= 1; i++)
if (h.y[i] == nodata || orientation(h.y[i]) != ori)
return nodata;
double hx = (h.y - h.y[-1])/2.;
double hxx = (h.y + h.y[-1] - 2.*h.y[])/Delta;
return hxx/pow(1. + sq(hx), 3/2.);
}

foreach_dimension()
static coord normal_y (Point point, vector h)
{
coord n = {nodata, nodata, nodata};
if (h.y[] == nodata)
return n;
int ori = orientation(h.y[]);
if (h.y[-1] != nodata && orientation(h.y[-1]) == ori) {
if (h.y != nodata && orientation(h.y) == ori)
n.x = (h.y[-1] - h.y)/2.;
else
n.x = h.y[-1] - h.y[];
}
else if (h.y != nodata && orientation(h.y) == ori)
n.x = h.y[] - h.y;
else
return n;
double nn = (ori ? -1. : 1.)*sqrt(1. + sq(n.x));
n.x /= nn;
n.y = 1./nn;
return n;
}
#else // dimension == 3
foreach_dimension()
static double kappa_z (Point point, vector h)
{
int ori = orientation(h.z[]);
for (int i = -1; i <= 1; i++)
for (int j = -1; j <= 1; j++)
if (h.z[i,j] == nodata || orientation(h.z[i,j]) != ori)
return nodata;
double hx = (h.z - h.z[-1])/2.;
double hy = (h.z[0,1] - h.z[0,-1])/2.;

We “filter” the curvature using a weighted sum of the three second-derivatives in the x and y directions. This is necessary to avoid a numerical mode when the curvature is used to compute surface tension.

  double filter = 0.2;
double hxx = (filter*(h.z[1,1] + h.z[-1,1] - 2.*h.z[0,1]) +
(h.z + h.z[-1] - 2.*h.z[]) +
filter*(h.z[1,-1] + h.z[-1,-1] - 2.*h.z[0,-1]))/
((1. + 2.*filter)*Delta);
double hyy = (filter*(h.z[1,1] + h.z[1,-1] - 2.*h.z) +
(h.z[0,1] + h.z[0,-1] - 2.*h.z[]) +
filter*(h.z[-1,1] + h.z[-1,-1] - 2.*h.z[-1]))/
((1. + 2.*filter)*Delta);
double hxy = (h.z[1,1] + h.z[-1,-1] - h.z[1,-1] - h.z[-1,1])/(4.*Delta);
return (hxx*(1. + sq(hy)) + hyy*(1. + sq(hx)) - 2.*hxy*hx*hy)/
pow(1. + sq(hx) + sq(hy), 3/2.);
}

foreach_dimension()
static coord normal2_z (Point point, vector h)
{
scalar hz = h.z;
if (hz[] == nodata)
return (coord){nodata, nodata, nodata};
int ori = orientation(hz[]);
double a = ori ? -1. : 1.;
coord n;
n.z = a;
foreach_dimension(2) {
if (allocated(-1) && hz[-1] != nodata && orientation(hz[-1]) == ori) {
if (allocated(1) && hz != nodata && orientation(hz) == ori)
n.x = a*(hz[-1] - hz)/2.;
else
n.x = a*(hz[-1] - hz[]);
}
else if (allocated(1) && hz != nodata && orientation(hz) == ori)
n.x = a*(hz[] - hz);
else
n.x = nodata;
}
return n;
}

foreach_dimension()
static coord normal_z (Point point, vector h) {
coord n = normal2_z (point, h);
double nn = fabs(n.x) + fabs(n.y) + fabs(n.z);
if (nn < nodata) {
foreach_dimension()
n.x /= nn;
return n;
}
return (coord){nodata, nodata, nodata};
}
#endif

We now need to choose one of the x, y or z height functions to compute the curvature. This is done by the function below which returns the HF curvature given a volume fraction field c and a height function field h.

static double height_curvature (Point point, scalar c, vector h)
{

We first define pairs of normal coordinates n (computed by simple differencing of c) and corresponding HF curvature function kappa (defined above).

  typedef struct {
double n;
double (* kappa) (Point, vector);
} NormKappa;
struct { NormKappa x, y, z; } n;
foreach_dimension()
n.x.n = c - c[-1], n.x.kappa = kappa_x;
double (* kappaf) (Point, vector) = NULL; NOT_UNUSED (kappaf);

We sort these pairs in decreasing order of |n|.

  if (fabs(n.x.n) < fabs(n.y.n))
swap (NormKappa, n.x, n.y);
#if dimension == 3
if (fabs(n.x.n) < fabs(n.z.n))
swap (NormKappa, n.x, n.z);
if (fabs(n.y.n) < fabs(n.z.n))
swap (NormKappa, n.y, n.z);
#endif

We try each curvature function in turn.

  double kappa = nodata;
foreach_dimension()
if (kappa == nodata) {
kappa = n.x.kappa (point, h);
if (kappa != nodata) {
kappaf = n.x.kappa;
if (n.x.n < 0.)
kappa = - kappa;
}
}

if (kappa != nodata) {

We limit the maximum curvature to 1/\Delta.

    if (fabs(kappa) > 1./Delta)
kappa = sign(kappa)/Delta;

We add the axisymmetric curvature if necessary.

#if AXI
double nr, r = y, hx;
if (kappaf == kappa_x) {
hx = (height(h.x[0,1]) - height(h.x[0,-1]))/2.;
nr = hx*(orientation(h.x[]) ? 1 : -1);
}
else {
r += height(h.y[])*Delta;
hx = (height(h.y[1,0]) - height(h.y[-1,0]))/2.;
nr = orientation(h.y[]) ? -1 : 1;
}
/* limit the minimum radius to half the grid size */
kappa += nr/max (sqrt(1. + sq(hx))*r, Delta/2.);
#endif
}

return kappa;
}

The function below works in a similar manner to return the normal estimated using height-functions (or a nodata vector if this cannot be done).

coord height_normal (Point point, scalar c, vector h)
{

We first define pairs of normal coordinates n (computed by simple differencing of c) and corresponding normal function normal (defined above).

  typedef struct {
double n;
coord (* normal) (Point, vector);
} NormNormal;
struct { NormNormal x, y, z; } n;
foreach_dimension()
n.x.n = c - c[-1], n.x.normal = normal_x;

We sort these pairs in decreasing order of |n|.

  if (fabs(n.x.n) < fabs(n.y.n))
swap (NormNormal, n.x, n.y);
#if dimension == 3
if (fabs(n.x.n) < fabs(n.z.n))
swap (NormNormal, n.x, n.z);
if (fabs(n.y.n) < fabs(n.z.n))
swap (NormNormal, n.y, n.z);
#endif

We try each normal function in turn.

  coord normal = {nodata, nodata, nodata};
foreach_dimension()
if (normal.x == nodata)
normal = n.x.normal (point, h);

return normal;
}

In three dimensions, these functions return the (two) components of the normal projected onto the (x,y) plane (respectively).

#if dimension == 3
foreach_dimension()
coord height_normal_z (Point point, vector h)
{
coord nx = normal2_x (point, h);
coord ny = normal2_y (point, h);
if (fabs(nx.y) < fabs(ny.x)) {
normalize (&nx);
return nx;
}
else if (ny.x != nodata) {
normalize (&ny);
return ny;
}
return (coord){nodata, nodata, nodata};
}
#endif

## Parabolic fit of “mixed” height-functions

When the standard height function curvature calculation is not possible (for example because not enough heights are available in any given direction), one can try to combine all the available heights (thus using “mixed” directions) to obtain points on the interface. These point locations can then be fitted with a parabola (using least-mean-square optimisation) and the resulting curvature can be computed. The fitting functions are defined in the file included below.

#include "parabola.h"

Given n (interface) point coordinates, this function returns the number of “independent” points i.e. points which are more than half-a-cell distant from all the other points.

static int independents (coord * p, int n)
{
if (n < 2)
return n;
int ni = 1;
for (int j = 1; j < n; j++) {
bool depends = false;
for (int i = 0; i < j && !depends; i++) {
double d2 = 0.;
foreach_dimension()
d2 += sq(p[i].x - p[j].x);
depends = (d2 < sq(0.5));
}
ni += !depends;
}
return ni;
}

Given a volume fraction field c and a height function field h, this function returns the “mixed heights” parabola-fitted curvature (or nodata if the curvature cannot be computed).

static double height_curvature_fit (Point point, scalar c, vector h)
{

The coordinates of the interface points and the number of interface points.

  coord ip[dimension == 2 ? 6 : 27];
int n = 0;

We collect the points along all directions.

  foreach_dimension() {

We don’t want to mix heights with different orientations. We first find the “dominant” orientation ori.

    int n1 = 0, n2 = 0;
#if dimension == 2
for (int i = -1; i <= 1; i++)
if (h.y[i] != nodata) {
if (orientation(h.y[i])) n1++; else n2++;
}
#else // dimension == 3
for (int i = -1; i <= 1; i++)
for (int j = -1; j <= 1; j++)
if (h.z[i,j] != nodata) {
if (orientation(h.z[i,j])) n1++; else n2++;
}
#endif
int ori = (n1 > n2);

We look for height-functions with the dominant orientation and store the corresponding interface coordinates (relative to the center of the cell and normalised by the cell size).

#if dimension == 2
for (int i = -1; i <= 1; i++)
if (h.y[i] != nodata && orientation(h.y[i]) == ori)
ip[n].x = i, ip[n++].y = height(h.y[i]);
#else // dimension == 3
for (int i = -1; i <= 1; i++)
for (int j = -1; j <= 1; j++)
if (h.z[i,j] != nodata && orientation(h.z[i,j]) == ori)
ip[n].x = i, ip[n].y = j, ip[n++].z = height(h.z[i,j]);
#endif
}

If we don’t have enough independent points, we cannot do the parabolic fit.

  if (independents (ip, n) < (dimension == 2 ? 3 : 9))
return nodata;

We recover the interface normal and the centroid of the interface fragment and initialize the parabolic fit.

  coord m = mycs (point, c), fc;
double alpha = plane_alpha (c[], m);
double area = plane_area_center (m, alpha, &fc);
ParabolaFit fit;
parabola_fit_init (&fit, fc, m);
#if dimension == 2
NOT_UNUSED(area);
#else // dimension == 3
#endif

We add the collected interface positions and compute the curvature.

  for (int i = 0; i < n; i++)
parabola_fit_solve (&fit);
double kappa = parabola_fit_curvature (&fit, 2., NULL)/Delta;
#if AXI
parabola_fit_axi_curvature (&fit, y + fc.y*Delta, Delta, &kappa, NULL);
#endif
return kappa;
}

## Parabolic fit of centroids

If all else fails, we try a parabolic fit of interface centroids.

static double centroids_curvature_fit (Point point, scalar c)
{

We recover the interface normal and the centroid of the interface fragment and initialize the parabolic fit.

  coord m = mycs (point, c), fc;
double alpha = plane_alpha (c[], m);
plane_area_center (m, alpha, &fc);
ParabolaFit fit;
parabola_fit_init (&fit, fc, m);

We add the interface centroids in a 3^d neighborhood and compute the curvature.

  coord r = {x,y,z};
foreach_neighbor(1)
if (c[] > 0. && c[] < 1.) {
coord m = mycs (point, c), fc;
double alpha = plane_alpha (c[], m);
double area = plane_area_center (m, alpha, &fc);
coord rn = {x,y,z};
foreach_dimension()
fc.x += (rn.x - r.x)/Delta;
}
parabola_fit_solve (&fit);
double kappa = parabola_fit_curvature (&fit, 2., NULL)/Delta;
#if AXI
parabola_fit_axi_curvature (&fit, y + fc.y*Delta, Delta, &kappa, NULL);
#endif
return kappa;
}

## General curvature computation

We first need to define “interfacial cells” i.e. cells which contain an interface. A simple test would just be that the volume fraction is neither zero nor one. As usual things are more complicated because of round-off errors. They can cause the interface to be exactly aligned with cell boundaries, so that cells on either side of this interface have fractions exactly equal to zero or one. The function below takes this into account.

static inline bool interfacial (Point point, scalar c)
{
if (c[] >= 1.) {
for (int i = -1; i <= 1; i += 2)
foreach_dimension()
if (c[i] <= 0.)
return true;
}
else if (c[] <= 0.) {
for (int i = -1; i <= 1; i += 2)
foreach_dimension()
if (c[i] >= 1.)
return true;
}
else // c[] > 0. && c[] < 1.
return true;
return false;
}

The function below computes the mean curvature kappa of the interface defined by the volume fraction c. It uses a combination of the methods above: statistics on the number of curvatures computed which each method is returned in a cstats data structure.

If sigma is different from zero the curvature is multiplied by sigma.

If add is true, the curvature (optionally multiplied by sigma) is added to field kappa.

typedef struct {
int h; // number of standard HF curvatures
int f; // number of parabolic fit HF curvatures
int a; // number of averaged curvatures
int c; // number of centroids fit curvatures
} cstats;

struct Curvature {
scalar c, kappa;
double sigma;
};

trace
cstats curvature (struct Curvature p)
{
scalar c = p.c, kappa = p.kappa;
double sigma = p.sigma ? p.sigma : 1.;
int sh = 0, sf = 0, sa = 0, sc = 0;
vector ch = c.height, h = automatic (ch);
if (!ch.x.i)
heights (c, h);

On trees we set the prolongation and restriction functions for the curvature.

#if TREE
kappa.refine = kappa.prolongation = curvature_prolongation;
kappa.restriction = curvature_restriction;
#endif

We first compute a temporary curvature k: a “clone” of \kappa.

  scalar k[];
scalar_clone (k, kappa);

foreach(reduction(+:sh) reduction(+:sf)) {

If we are not in an interfacial cell, we set \kappa to nodata.

    if (!interfacial (point, c))
k[] = nodata;

Otherwise we try the standard HF curvature calculation first, and the “mixed heights” HF curvature second.

    else if ((k[] = height_curvature (point, c, h)) != nodata)
sh++;
else if ((k[] = height_curvature_fit (point, c, h)) != nodata)
sf++;
}
boundary ({k});

foreach (reduction(+:sa) reduction(+:sc)) {

We then construct the final curvature field using either the computed temporary curvature…

    double kf;
if (k[] < nodata)
kf = k[];
else if (interfacial (point, c)) {

…or the average of the curvatures in the 3^{d} neighborhood of interfacial cells.

      double sk = 0., a = 0.;
foreach_neighbor(1)
if (k[] < nodata)
sk += k[], a++;
if (a > 0.)
kf = sk/a, sa++;
else

Empty neighborhood: we try centroids as a last resort.

	kf = centroids_curvature_fit (point, c), sc++;
}
else
kf = nodata;

    if (kf == nodata)
kappa[] = nodata;
kappa[] += sigma*kf;
else
kappa[] = sigma*kf;
}
boundary ({kappa});

return (cstats){sh, sf, sa, sc};
}

# Position of an interface

This is similar to curvature but this time for the position of the interface, defined as \displaystyle pos = \mathbf{G}\cdot(\mathbf{x} - \mathbf{Z}) with \mathbf{G} and \mathbf{Z} two vectors and \mathbf{x} the coordinates of the interface.

This is defined only in interfacial cells. In all the other cells it takes the value nodata.

We first need a function to compute the position \mathbf{x} of an interface. For accuracy, we first try to use height functions.

foreach_dimension()
static double pos_x (Point point, vector h, coord * G, coord * Z)
{
if (fabs(height(h.x[])) > 1.)
return nodata;
coord o = {x, y, z};
o.x += height(h.x[])*Delta;
double pos = 0.;
foreach_dimension()
pos += (o.x - Z->x)*G->x;
return pos;
}

We now need to choose one of the x, y or z height functions to compute the position. This is done by the function below which returns the HF position given a volume fraction field f, a height function field h and vectors G and Z.

static double height_position (Point point, scalar f, vector h,
coord * G, coord * Z)
{

We first define pairs of normal coordinates n (computed by simple differencing of f) and corresponding HF position function pos (defined above).

  typedef struct {
double n;
double (* pos) (Point, vector, coord *, coord *);
} NormPos;
struct { NormPos x, y, z; } n;
foreach_dimension()
n.x.n = f - f[-1], n.x.pos = pos_x;

We sort these pairs in decreasing order of |n|.

  if (fabs(n.x.n) < fabs(n.y.n))
swap (NormPos, n.x, n.y);
#if dimension == 3
if (fabs(n.x.n) < fabs(n.z.n))
swap (NormPos, n.x, n.z);
if (fabs(n.y.n) < fabs(n.z.n))
swap (NormPos, n.y, n.z);
#endif

We try each position function in turn.

  double pos = nodata;
foreach_dimension()
if (pos == nodata)
pos = n.x.pos (point, h, G, Z);

return pos;
}

The position() function fills field pos with \displaystyle \mathbf{G}\cdot(\mathbf{x} - \mathbf{Z}) with \mathbf{x} the position of the interface defined by f.

struct Position {
scalar f, pos;
coord G, Z;
};

void position (struct Position p)
{
scalar f = p.f, pos = p.pos;
coord * G = &p.G, * Z = &p.Z;

On trees we set the prolongation and restriction functions for the position.

#if TREE
pos.refine = pos.prolongation = curvature_prolongation;
pos.restriction = curvature_restriction;
#endif

vector fh = f.height, h = automatic (fh);
if (!fh.x.i)
heights (f, h);
foreach() {
if (interfacial (point, f)) {
double hp = height_position (point, f, h, G, Z);
if (hp == nodata) {

If the height function is not defined, we use the centroid of the reconstructed VOF interface.

	coord n = mycs (point, f), o = {x,y,z}, c;
double alpha = plane_alpha (f[], n);
plane_area_center (n, alpha, &c);
hp = 0.;
foreach_dimension()
hp += (o.x + Delta*c.x - Z->x)*G->x;
}
}