Menu

simulationCases/dropMove.c

dropMove.c: onset of self-propulsion of a chemically active drop

A single planar drop emits a chemical species at its interface. The species is advected and diffuses in the outer phase. The implemented law sigma = 1/Ca + 4*cL increases surface tension with concentration. Above a critical Péclet number the isotropic state is unstable and the drop self-propels. The problem is integrated in the Stokes limit with the coupled level-set and volume-of-fluid (CLSVOF) interface method and the integral formulation of surface tension.

Non-dimensional groups

  • Oh: Ohnesorge number, entering only through the density \(\rho = 4/Oh^2\).
  • Pe: Péclet number; the species diffusivity is \(D = 1/Pe\).
  • Ca: the clean-interface surface tension is \(1/Ca\); the local coefficient is \(1/Ca + 4c\) with \(c\) the species concentration.
  • AcNum: constant chemical flux emitted at the interface.

Runtime parameters

All parameters are read from a key=value file passed as the first argument (default case.params) through src-local/params.h:

Key Default Meaning
CaseNo 1000 Case identifier used by the runner
Pe 1.6 Péclet number
MAXlevel, MINlevel 8, 0 Quadtree refinement bounds
Oh, Ca, AcNum 1, 0.1, 1 Fixed non-dimensional groups
L0 10 Square domain size in drop radii
tmax, tsnap 50, 0.1 Observation horizon and snapshot interval
threshold 1 Centroid displacement, in drop radii, classified as MOVED; <= 0 disables the early stop
FErr, VelErr, cErr, KErr 1e-3 each Wavelet adaptation tolerances
keLimit 1e3 Kinetic-energy limit classified as FAILED

Output and classification

Snapshots are written to intermediate/snapshot-<t> and the diagnostics to log.dat with columns i t ke dist xcm ycm volume in the working directory (the runner uses simulationCases/c<CaseNo>/). Exactly one STATUS {MOVED,NOT_MOVED,FAILED} line and one SUMMARY key=value line are printed on stdout when the run ends. MOVED is a finite-time, finite-displacement convention: the centroid moved by more than threshold before tmax. The centroid, drop volume and kinetic energy are integrated with the cell volume dv() so that the adaptive mesh does not bias the classifier; displacement is measured from the centroid recorded at the first step.

Author

Vatsal Sanjay Email: [email protected] Computational Multiphase Physics (CoMPhy) Lab, Durham University Last updated: Sep 9, 2026

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

#include "navier-stokes/centered.h"
#define FILTERED 161
#include "two-phase-clsvof.h"
#include "integral.h"
#include "activity.h"
#include "params.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. All walls are no-slip with zero concentration.

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

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

u.t[top] = dirichlet(0.);
u.t[right] = dirichlet(0.);
u.t[left] = dirichlet(0.);
u.t[bottom] = dirichlet(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 = 1.0;
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);
  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;
  }

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

  stokes = true;
  L0 = L0_param;
  origin (-0.5*L0, -0.5*L0);
  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);

  run();
  return exit_status;
}

Initialisation

Signed distance to a unit circle centred at the origin; the CLSVOF header derives the volume fraction from d.

event init (i = 0) {
  foreach() {
    d[] = 1. - sqrt (sq(x) + sq(y));
    u.x[] = 0.0;
    u.y[] = 0.0;
    cL[] = 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 ({f, u.x, u.y, cL, KAPPA},
                 (double[]){FErr, VelErr, VelErr, cErr, KErr},
                 MAXlevel, MINlevel);
}

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.;
  foreach (reduction(+:ke) reduction(+:drop_volume)
           reduction(+:x_moment) reduction(+:y_moment)) {
    double ff = clamp (f[], 0., 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;
  }
  dist_last = sqrt (sq(xcm_last - x0_cm) + sq(ycm_last - y0_cm));

  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 (!isfinite (ke) || (i > 10 && ke >= keLimit)) {
    if (pid() == 0)
      fprintf (stderr, "Kinetic energy %.8e at i=%d, t=%g: stopping.\n",
               ke, i, t);
    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);
}