Menu

src-local/dropMove-embed.h

Embedded confinement driver for a chemically active drop

Shared implementation for dropMove-embed-pipe.c and dropMove-embed-channel.c. Uses the baseline CLSVOF/integral surface-tension model, including its convention \(\sigma = 1/Ca + 4c_L\) and source \(A|\nabla f|\). Positive AcNum produces species; it does not consume fuel.

The embedded sidewalls impose no slip and \(\partial_n c_L=0\). Activity acts only at the drop interface and adds no solid-wall chemistry. Axial endcaps impose no slip and cL=0; end-distance sensitivity is therefore part of any confinement study. No contact-angle or wetting model is supplied. The run stops when a liquid-containing cell enters a three-finest-cell wall/endcap band.

Parameters

The baseline parameter-file pathway and output convention are retained. Additional parameters (lengths in initial drop radii):

Key Default Meaning
wallHalfWidth 2.53 Pipe radius or planar channel half-width
dropOffset 0 pipe; 0.5 channel Initial transverse displacement; must be zero in the pipe
initialDipole 1e-3 Nonnegative axial concentration seed; zero preserves fore-aft symmetry
threshold 0 Displacement stop disabled unless supplied by the parameter file

In the pipe, volume and energy diagnostics use the cylindrical metric but omit the common factor \(2\pi\); ycm is the mean radius, not a transverse centre of mass. The motion classifier uses axial displacement only.

Author

Vatsal Sanjay ([email protected]) Computational Multiphase Physics (CoMPhy) Lab, Durham University

#include <sys/stat.h>
#include <errno.h>

#include "embed.h"
#if ACTIVE_DROP_PIPE
# include "axi.h"
#endif
#include "navier-stokes/centered.h"
#define FILTERED 145
#include "two-phase-clsvof.h"
#include "integral.h"
#include "activity.h"
#include "params.h"
#include "embed-channel-geometry.h"

Fields and boundary conditions

cL is the species concentration in the outer phase (cL.inverse = true), sigmaf the surface tension coefficient and KAPPA the distance-function curvature used for adaptation. The embedded sidewalls are inert and no-slip. The axial endcaps are no-slip concentration reservoirs with cL = 0. The pipe axis is a symmetry boundary, not a solid wall.

scalar cL[], * stracers = {cL};
scalar sigmaf[];
scalar KAPPA[];

cL[embed] = neumann(0.);
cL[top] = neumann(0.);
cL[right] = dirichlet(0.);
cL[left] = dirichlet(0.);
cL[bottom] = neumann(0.);

u.t[top] = dirichlet(0.);
u.t[right] = dirichlet(0.);
u.t[left] = dirichlet(0.);
#if !ACTIVE_DROP_PIPE
u.t[bottom] = dirichlet(0.);
#endif
u.n[embed] = dirichlet(0.);
u.t[embed] = dirichlet(0.);
d[embed] = neumann(0.);
f[embed] = neumann(0.);

Runtime parameters

int CaseNo = 1000;
double Pe = 1.6;
int MAXlevel = 8, MINlevel = 0;
double Oh = 1., Ca = 0.1, AcNum = 1.;
double tmax = 50., tsnap = 0.1;
double movement_threshold = 0.;
double wallHalfWidth = 2.53, initialDipole = 1e-3;
#if ACTIVE_DROP_PIPE
double dropOffset = 0.;
#else
double dropOffset = 0.5;
#endif
double FErr = 1e-3, VelErr = 1e-3, cErr = 1e-3, KErr = 1e-3;
double keLimit = 1e3;

Classifier state

static double x0_cm = 0., y0_cm = 0.;
static bool centroid_initialised = false;
static bool status_printed = false;
static int exit_status = 0;
static double dist_last = 0., xcm_last = 0., ycm_last = 0.;

Prints the single STATUS line and the SUMMARY line on rank zero, once.

static void print_status (const char * status, int i_now, double t_now)
{
  if (status_printed || pid() != 0)
    return;
  status_printed = true;
  fprintf (stdout, "STATUS %s\n", status);
  fprintf (stdout,
           "SUMMARY CaseNo=%d Pe=%g max_level=%d tmax=%g threshold=%g"
           " t_end=%g i_end=%d dist_end=%.8e xcm_end=%.8e ycm_end=%.8e"
           " status=%s\n",
           CaseNo, Pe, MAXlevel, tmax, movement_threshold,
           t_now, i_now, dist_last, xcm_last, ycm_last, status);
  fflush (stdout);
}

ensure_directory()

Creates path with mkdir(2); an existing directory is accepted.

static bool ensure_directory (const char * path)
{
  if (mkdir (path, 0775) != 0 && errno != EEXIST)
    return false;
  struct stat st;
  return stat (path, &st) == 0 && S_ISDIR (st.st_mode);
}

main()

Reads the parameter file, validates the values that must be positive, sets up the domain and material properties and runs the simulation.

int main (int argc, char const * argv[])
{
  params_init_from_argv (argc, argv);

  CaseNo = param_int ("CaseNo", CaseNo);
  Pe = param_double ("Pe", Pe);
  wallHalfWidth = param_double ("wallHalfWidth", wallHalfWidth);
  dropOffset = param_double ("dropOffset", dropOffset);
  initialDipole = param_double ("initialDipole", initialDipole);
  MAXlevel = param_int ("MAXlevel", MAXlevel);
  MINlevel = param_int ("MINlevel", MINlevel);
  Oh = param_double ("Oh", Oh);
  Ca = param_double ("Ca", Ca);
  AcNum = param_double ("AcNum", AcNum);
  double L0_param = param_double ("L0", 10.);
  tmax = param_double ("tmax", tmax);
  tsnap = param_double ("tsnap", tsnap);
  movement_threshold = param_double ("threshold", movement_threshold);
  FErr = param_double ("FErr", FErr);
  VelErr = param_double ("VelErr", VelErr);
  cErr = param_double ("cErr", cErr);
  KErr = param_double ("KErr", KErr);
  keLimit = param_double ("keLimit", keLimit);

  if (!(Pe > 0.) || !(Oh > 0.) || !(Ca > 0.) || !(L0_param > 0.) ||
      !(tmax > 0.) || !(tsnap > 0.) || !(keLimit > 0.) ||
      MAXlevel < 1 || MAXlevel > 20 || MINlevel < 0 || MINlevel > MAXlevel) {
    fprintf (stderr, "Invalid parameters: Pe, Oh, Ca, L0, tmax, tsnap and keLimit"
             " must be positive and 0 <= MINlevel <= MAXlevel <= 20.\n");
    return 2;
  }

  double dx = L0_param/(1 << MAXlevel);
  // A face-aligned wall has no cut-cell fragment for embedded no-slip flux.
#if ACTIVE_DROP_PIPE
  double wall_index = wallHalfWidth/dx;
#else
  double wall_index = (wallHalfWidth + L0_param/2.)/dx;
#endif
  if (isfinite(wall_index) && fabs(wall_index - round(wall_index)) < 1e-8) {
    fprintf(stderr, "wallHalfWidth places a wall on a finest-grid face. "
            "Choose a nonaligned value (default 2.53 for L0=10).\n");
    return 2;
  }
  if (!isfinite(wallHalfWidth) || !isfinite(dropOffset) ||
      !isfinite(initialDipole) || initialDipole < 0. ||
      !isfinite(AcNum) || AcNum < 0. || !isfinite(L0_param) ||
      !(wallHalfWidth > 1. + fabs(dropOffset) + 4.*dx) ||
      !(L0_param/2. > 1. + 4.*dx) ||
#if ACTIVE_DROP_PIPE
      dropOffset != 0. || !(wallHalfWidth < L0_param - 2.*dx) ||
#else
      !(wallHalfWidth < L0_param/2. - 2.*dx) ||
#endif
      !(FErr > 0.) || !(VelErr > 0.) || !(cErr > 0.) || !(KErr > 0.) ||
      !isfinite(Pe) || !isfinite(Oh) || !isfinite(Ca) ||
      !isfinite(tmax) || !isfinite(tsnap) || !isfinite(keLimit) ||
      !isfinite(movement_threshold) || !isfinite(FErr) ||
      !isfinite(VelErr) || !isfinite(cErr) || !isfinite(KErr)) {
    fprintf(stderr, "Invalid embedded geometry, dipole or adaptation tolerance. "
            "Keep the unit drop at least four finest cells from walls/endcaps; "
            "pipe dropOffset must be zero.\n");
    return 2;
  }

  if (pid() == 0 && !ensure_directory ("intermediate")) {
    fprintf (stderr, "Cannot create output directory 'intermediate'.\n");
    return 2;
  }

  stokes = true;
  L0 = L0_param;
#if ACTIVE_DROP_PIPE
  origin (-0.5*L0, 0.);
#else
  origin (-0.5*L0, -0.5*L0);
#endif
  init_grid (1 << MAXlevel);

  d.sigmaf = sigmaf;

  rho1 = 4./sq(Oh); rho2 = 4./sq(Oh);
  mu1 = 1.0; mu2 = 1.0;

  cL.inverse = true;
  cL.A = AcNum;
  cL.D = 1./Pe;

  if (pid() == 0)
    fprintf (stderr, "CaseNo=%d Pe=%g MAXlevel=%d MINlevel=%d Oh=%g Ca=%g AcNum=%g"
             " L0=%g tmax=%g tsnap=%g threshold=%g\n",
             CaseNo, Pe, MAXlevel, MINlevel, Oh, Ca, AcNum, L0, tmax, tsnap,
             movement_threshold);

  if (pid() == 0)
    fprintf(stderr, "geometry=%s wallHalfWidth=%g dropOffset=%g initialDipole=%g\n",
            ACTIVE_DROP_PIPE ? "pipe" : "channel", wallHalfWidth,
            dropOffset, initialDipole);
  run();
  return exit_status;
}

Initialisation

The unit drop is a sphere in the pipe and a circle in the planar channel. The optional nonnegative concentration dipole breaks fore-aft symmetry. Solid geometry and cylindrical metrics must precede the CLSVOF initialisation.

static void wall_geometry (void) {
  confined_geometry (wallHalfWidth);
}

event init (i = 0) {
  wall_geometry();
  foreach() {
    double radius = sqrt(sq(x) + sq(y - dropOffset));
    d[] = 1. - radius;
    u.x[] = 0.0;
    u.y[] = 0.0;
    cL[] = cs[] > 0. && radius > 1. ?
      initialDipole*(1. + x/radius)*exp(1. - radius) : 0.;
    sigmaf[] = 1./Ca + 4.*cL[];
  }
}

Surface tension coefficient from the local concentration

event properties (i++) {
  foreach()
    sigmaf[] = 1./Ca + 4.*cL[];
}

Adaptation

Wavelet adaptation on the volume fraction, velocity, concentration and the distance-function curvature.

event adapt (i++) {
  foreach()
    KAPPA[] = distance_curvature (point, d);
  adapt_wavelet ({cs, f, u.x, u.y, cL, KAPPA},
                 (double[]){1e-3, FErr, VelErr, VelErr, cErr, KErr},
                 MAXlevel, MINlevel);
  wall_geometry();
  foreach()
    if (cs[] <= 0.)
      f[] = cL[] = 0.;
}

Snapshots

event outputs (t = 0.; t += tsnap; t <= tmax) {
  char dumpFile[128];
  snprintf (dumpFile, sizeof(dumpFile), "intermediate/snapshot-%5.4f", t);
  dump (file = dumpFile);
}

Diagnostics and onset classifier

Kinetic energy, drop volume and the volume-weighted centroid are reduced with dv(). The run stops with STATUS FAILED if the drop volume vanishes or the kinetic energy is non-finite or exceeds keLimit, and with STATUS MOVED once the displacement from the initial centroid exceeds threshold (when threshold > 0).

event logWriting (i++) {
  double ke = 0., drop_volume = 0., x_moment = 0., y_moment = 0.;
  int invalid_field = 0;
  foreach (reduction(+:ke) reduction(+:drop_volume)
           reduction(+:x_moment) reduction(+:y_moment)
           reduction(max:invalid_field)) {
    double ff = clamp (f[], 0., 1.);
    if (cs[] > 0. && (!isfinite(cL[]) || !isfinite(sigmaf[]) || sigmaf[] <= 0.))
      invalid_field = 1;
    ke += 0.5*rho(ff)*(sq(u.x[]) + sq(u.y[]))*dv();
    drop_volume += ff*dv();
    x_moment += ff*x*dv();
    y_moment += ff*y*dv();
  }

  if (!(drop_volume > 0.)) {
    if (pid() == 0)
      fprintf (stderr, "Drop volume vanished at i=%d, t=%g.\n", i, t);
    print_status ("FAILED", i, t);
    exit_status = 3;
    return 1;
  }

  xcm_last = x_moment/drop_volume;
  ycm_last = y_moment/drop_volume;
  if (!centroid_initialised) {
    x0_cm = xcm_last, y0_cm = ycm_last;
    centroid_initialised = true;
  }
#if ACTIVE_DROP_PIPE
  // The radial moment is not a transverse centre-of-mass displacement.
  dist_last = fabs(xcm_last - x0_cm);
#else
  dist_last = sqrt (sq(xcm_last - x0_cm) + sq(ycm_last - y0_cm));
#endif

  if (pid() == 0) {
    static FILE * fp = NULL;
    static bool log_header_written = false;
    if (!log_header_written) {
      log_header_written = true;
      fp = fopen ("log.dat", "w");
      if (fp)
        fprintf (fp, "i t ke dist xcm ycm volume\n");
      else
        fprintf (stderr, "Cannot open log.dat for writing.\n");
      fprintf (ferr, "i t ke dist xcm ycm volume\n");
    }
    fprintf (ferr, "%d %g %.8e %.8e %.8e %.8e %.8e\n",
             i, t, ke, dist_last, xcm_last, ycm_last, drop_volume);
    if (fp) {
      fprintf (fp, "%d %g %.8e %.8e %.8e %.8e %.8e\n",
               i, t, ke, dist_last, xcm_last, ycm_last, drop_volume);
      fflush (fp);
    }
  }

  if (invalid_field || !isfinite (ke) || (i > 10 && ke >= keLimit)) {
    if (pid() == 0)
      fprintf (stderr, "Invalid species/tension or kinetic energy %.8e at i=%d, t=%g: stopping.\n",
               ke, i, t);
    print_status ("FAILED", i, t);
    exit_status = 3;
    return 1;
  }

  // The interfacial stencil must remain separated from solids/endcaps.
  int unresolved_gap = 0;
  double gap_limit = 3.*L0/(1 << MAXlevel);
  foreach (reduction(max:unresolved_gap))
    if (cs[] > 0. && f[] > 1e-6 &&
        (wallHalfWidth - fabs(y) - Delta/2. < gap_limit ||
         L0/2. - fabs(x) - Delta/2. < gap_limit))
      unresolved_gap = 1;
  if (unresolved_gap) {
    if (pid() == 0)
      fprintf(stderr, "Unresolved drop-wall/endcap gap; contact is not modelled.\n");
    print_status("FAILED", i, t);
    exit_status = 3;
    return 1;
  }

  if (movement_threshold > 0. && i > 10 && dist_last >= movement_threshold) {
    print_status ("MOVED", i, t);
    return 1;
  }
}

End of the observation window

event end (t = tmax) {
  print_status ("NOT_MOVED", i, t);
}