Menu

simulationCases/hyphal-flow.c

hyphal-flow.c

Canonical single-branch hypha simulation. The outer phase is an incompressible finite-strain neo-Hookean Kelvin–Voigt-type solid, while the carrier liquid and drop are finite-relaxation Oldroyd-B fluids. Separate VoF fields prevent the two interfaces from becoming one tracer; the overlap diagnostic still has to demonstrate that the carrier film remains resolved.

Author

Vatsal Sanjay

Version

  • August 11, 2024: allow stress relaxation in both drop and cytoplasm.

Numerical Tolerances

#define fErr (1e-3) // error tolerance in VOF
#define KErr (1e-4) // error tolerance in KAPPA
#define VelErr (1e-2) // error tolerances in velocity
#define AErr (1e-3) // error tolerance in Conformation tensor
#define MINlevel 4 // minimum level
int MAXlevel;
int initial_refine_level;
double tmax;
double snapshot_interval;
double log_interval;
double initial_refine_band;
double initial_volume_tolerance;

Material Parameters

The Newtonian Oh_* contribution is the solvent viscosity. For the two liquids, Ec_* is the elastic modulus and De_* the relaxation time, so the implied Oldroyd-B polymer viscosity is Ec_* De_*. The solid uses the same finite-strain conformation stress in the practical no-relaxation limit; its Newtonian contribution is the Kelvin–Voigt dashpot.

double Oh_drop, rho_drop, Ec_drop, De_drop;
double Oh_solid, rho_solid, Ec_solid, De_solid;
double Oh_liquid, rho_liquid, Ec_liquid, De_liquid;

Geometry Helper

#define gap(x,y,radius,width,x0,c0) (y - ((c0+1.) + 0.5*(radius - (c0+1.))*(1. + tanh(sq(x-x0)/width))))59

double channel_radius;
double Bond;
double Ldomain;

static void simulation_abort (int status)
{
#if _MPI
  MPI_Abort (MPI_COMM_WORLD, status);
#else
  exit (status);
#endif
}

static int validate_phase (const char * name, double viscosity,
                           double modulus, double relaxation_time)
{
  if (!isfinite (viscosity) || !isfinite (modulus) ||
      !isfinite (relaxation_time) || viscosity <= 0. || modulus < 0. ||
      relaxation_time < 0.) {
    fprintf (stderr, "ERROR: %s viscosity must be positive and Ec/De non-negative\n",
             name);
    return -1;
  }
  if ((modulus > 0.) != (relaxation_time > 0.)) {
    fprintf (stderr,
             "ERROR: %s needs both Ec > 0 and De > 0, or Ec = De =\n",
             name);
    return -1;
  }
  return 0;
}

static bool output_due (double current_time, double * next_time,
                        double interval)
{
  if (current_time + 1e-12 < *next_time)
    return false;
  do
    *next_time += interval;
  while (*next_time <= current_time + 1e-12);
  return true;
}

validate_initial_geometry()

Fail before the first timestep when the freshly constructed VoF geometry is not the intended resolved axisymmetric state. The direct drop metric volume has the analytic value

\[ \int_{-1.5}^{1.5}\int_0^{\sqrt{1-(x/1.5)^2}} y\,dy\,dx = 1. \]

This deliberately uses f1*dv() rather than normalised phase weights, so drop–solid overlap cannot hide a bad initial condition.

static void validate_initial_geometry (void)
{
  const double f_eps = 1e-6;
  double drop_volume = 0., overlap = 0.;
  double drop_xmin = HUGE, drop_xmax = -HUGE, drop_ymax = -HUGE;
  long drop_mixed = 0, solid_mixed = 0;
  int drop_min_level = initial_refine_level + 1;
  int solid_min_level = initial_refine_level + 1;

  foreach (reduction(+:drop_volume) reduction(max:overlap)
           reduction(min:drop_xmin) reduction(max:drop_xmax)
           reduction(max:drop_ymax) reduction(+:drop_mixed)
           reduction(+:solid_mixed) reduction(min:drop_min_level)
           reduction(min:solid_min_level)) {
    drop_volume += f1[]*dv();
    overlap = max (overlap, min (f1[], f2[]));
    if (f1[] > f_eps && f1[] < 1. - f_eps) {
      drop_mixed++;
      drop_min_level = min (drop_min_level, level);
      drop_xmin = min (drop_xmin, x);
      drop_xmax = max (drop_xmax, x);
      drop_ymax = max (drop_ymax, y);
    }
    if (f2[] > f_eps && f2[] < 1. - f_eps) {
      solid_mixed++;
      solid_min_level = min (solid_min_level, level);
    }
  }

  const double geometry_tolerance = 2.*L0/(1 << initial_refine_level);
  const bool valid =
    fabs (drop_volume - 1.) <= initial_volume_tolerance &&
    overlap <= 1e-6 &&
    drop_mixed > 0 && solid_mixed > 0 &&
    drop_min_level == initial_refine_level &&
    solid_min_level == initial_refine_level &&
    fabs (drop_xmin + 1.5) <= geometry_tolerance &&
    fabs (drop_xmax - 1.5) <= geometry_tolerance &&
    fabs (drop_ymax - 1.) <= geometry_tolerance;

  if (pid() == 0)
    fprintf (ferr,
             "INIT_GEOMETRY valid=%d Vd=%.16g overlap=%.16g "
             "drop_mixed=%ld solid_mixed=%ld drop_min_level=%d "
             "solid_min_level=%d xmin=%.16g xmax=%.16g ymax=%.16g\n",
             valid, drop_volume, overlap, drop_mixed, solid_mixed,
             drop_min_level, solid_min_level,
             drop_xmin, drop_xmax, drop_ymax);
  if (!valid) {
    if (pid() == 0)
      fprintf (ferr,
               "ERROR: unresolved or inconsistent initial VoF geometry\n");
    simulation_abort (3);
  }
}

main()

Initialize properties and forcing, then enter the Basilisk event loop.

int main (int argc, char const * argv[])
{
  // The command is a fixed literal; no parameter text reaches a shell.
  if (system ("mkdir -p intermediate"))
    return 2;

  params_init_from_argv (argc, argv);

  MAXlevel = param_int ("MAXlevel", 12);
  initial_refine_level = param_int ("initial_refine_level", MAXlevel);
  tmax = param_double ("tmax", 200.);
  snapshot_interval = param_double ("snapshot_interval", 0.1);
  log_interval = param_double ("log_interval", 0.01);
  initial_refine_band = param_double ("initial_refine_band", 1.25);
  initial_volume_tolerance = param_double ("initial_volume_tolerance", 0.01);

  Oh_drop = param_double ("Oh_drop", param_double ("Ohd", 1.));
  rho_drop = param_double ("rho_drop", param_double ("RhoR_dc", 1.2));
  Ec_drop = param_double ("Ec_drop", param_double ("Ec_d", 0.1));
  De_drop = param_double ("De_drop", param_double ("De_d", 1.));

  Oh_solid = param_double ("Oh_solid", param_double ("Ohf", 1.));
  rho_solid = param_double ("rho_solid", param_double ("RhoR_hc", 1.));
  Ec_solid = param_double ("Ec_solid", param_double ("Ec_h", 0.1));
  De_solid = param_double ("De_solid", param_double ("De_h", 1e30));

  Oh_liquid = param_double ("Oh_liquid",
                            param_double ("Oh_c", param_double ("Ohc", 0.01)));
  rho_liquid = param_double ("rho_liquid", 1.);
  Ec_liquid = param_double ("Ec_liquid", param_double ("Ec_c", 0.1));
  De_liquid = param_double ("De_liquid", param_double ("De_c", 1.));

  channel_radius = param_double ("channel_radius",
                                 param_double ("hr", param_double ("hf", 0.9)));
  Bond = param_double ("Bond", 1.);
  Ldomain = param_double ("Ldomain", 16.);

  if (MAXlevel < MINlevel || MAXlevel > 20 ||
      initial_refine_level < MINlevel || initial_refine_level > MAXlevel ||
      !isfinite (tmax) || tmax <= 0. ||
      !isfinite (snapshot_interval) || snapshot_interval <= 0. ||
      !isfinite (log_interval) || log_interval <= 0. ||
      !isfinite (initial_refine_band) || initial_refine_band <= 1. ||
      !isfinite (initial_volume_tolerance) ||
      initial_volume_tolerance <= 0. || initial_volume_tolerance >= 0.1 ||
      !isfinite (rho_drop) || !isfinite (rho_solid) ||
      !isfinite (rho_liquid) || rho_drop <= 0. || rho_solid <= 0. ||
      rho_liquid <= 0. || !isfinite (channel_radius) ||
      !isfinite (Ldomain) || !isfinite (Bond) || channel_radius <= 0. ||
      Ldomain < 8. ||
      validate_phase ("drop", Oh_drop, Ec_drop, De_drop) ||
      validate_phase ("outer solid", Oh_solid, Ec_solid, De_solid) ||
      validate_phase ("carrier liquid", Oh_liquid, Ec_liquid, De_liquid))
    return 2;

  if (Ec_solid > 0. && De_solid < 1e6*max (tmax, 1.))
    fprintf (stderr,
             "WARNING: De_solid is finite on the run timescale; the outer "
             "phase is relaxing viscoelastic rather than Kelvin--Voigt-type\n");

  fprintf (ferr,
           "level=%d tmax=%g | drop: Oh=%g Ec=%g De=%g mu_p=%g | "
           "liquid: Oh=%g Ec=%g De=%g mu_p=%g | solid: Oh=%g Ec=%g "
           "De=%g | radius=%g Bond=%g | Ldomain=%g initial_level=%d "
           "initial_band=%g\n",
           MAXlevel, tmax,
           Oh_drop, Ec_drop, De_drop, Ec_drop*De_drop,
           Oh_liquid, Ec_liquid, De_liquid, Ec_liquid*De_liquid,
           Oh_solid, Ec_solid, De_solid, channel_radius, Bond,
           Ldomain, initial_refine_level, initial_refine_band);

  L0 = Ldomain;
  X0 = -4.;
  Y0 = 0.;
  init_grid (1 << MINlevel);
  periodic (right);

  // Phase 1: drop (finite-relaxation Oldroyd-B).
  rho1 = rho_drop; mu1 = Oh_drop; G1 = Ec_drop; lambda1 = De_drop;

  // Phase 2: outer finite-strain Kelvin--Voigt-type solid.
  rho2 = rho_solid; mu2 = Oh_solid; G2 = Ec_solid; lambda2 = De_solid;

  // Phase 3: carrier liquid (finite-relaxation Oldroyd-B).
  rho3 = rho_liquid; mu3 = Oh_liquid; G3 = Ec_liquid; lambda3 = De_liquid;

  Bf1.x = Bond;
  Bf2.x = Bond;

  f1.sigma = 1.; // drop--carrier-liquid interfacial tension
  f2.sigma = 1.; // solid--carrier-liquid interfacial tension

  run ();
  return 0;
}

init()

Initialize interfaces unless a restart snapshot is available.

event init(t = 0){
  if (!restore (file = "restart")) {
    double width = 2e0; // width of the tanh function
    // Historical wall parameter; the current formula gives a 0.05 minimum gap.
    double clearance = 0.20;
    double x0tanh = 0.0; // midpoint of the tanh function

    // Resolve both interfaces before sampling either implicit geometry.
    refine (y < initial_refine_band && level < initial_refine_level);

    fraction(f1, sq(1e0) - sq(y) - sq(x/1.5));
    fraction(f2, gap(x,y,channel_radius,width,x0tanh,clearance));
    validate_initial_geometry();
  }
}

adapt()

Adaptive mesh refinement driven by interfaces, curvature, velocity, and conformation fields.

event adapt(i++){
  scalar KAPPA1[], KAPPA2[], trA[];
  curvature(f1, KAPPA1);
  curvature(f2, KAPPA2);
  foreach(){
    trA[] = (conform_p.x.x[] + conform_p.y.y[] + conform_qq[])/3.0;
  }

  adapt_wavelet ((scalar *){f1, f2, KAPPA1, KAPPA2, u.x, u.y, trA},
  (double[]){fErr, fErr, KErr, KErr, VelErr, VelErr, AErr},
  MAXlevel, MINlevel);

  unrefine(y > 1e1);
}

stop_when_drop_exits()

Stop the run when the leading drop edge approaches the domain outlet.

event stop_when_drop_exits (i++) {

  static double next_check = 0.;
  if (!output_due (t, &next_check, log_interval))
    return 0;

  scalar xpos[];
  coord ex = {1., 0.};
  coord z0 = {0., 0.};
  position (f1, xpos, ex, z0, add = false);

  stats sx = statsf (xpos);
  double xmax = sx.volume > 0. ? sx.max : -HUGE;

  // buffer
  double finest = L0/(1 << MAXlevel);
  double buffer = 2.*finest;

  double x_end = X0 + L0;

  if (pid() == 0)
    fprintf(ferr, "drop front xmax = %.6f, x_end = %.6f\n", xmax, x_end);

  if (xmax > x_end - buffer) {
    if (pid() == 0) {
      fprintf(ferr,
        "\n*** Drop leading edge reached end of domain ***\n"
        "xmax = %.6f, domain end = %.6f\n"
        "Stopping simulation at t = %.6g\n\n",
        xmax, x_end, t);
    }
    dump (file = "final");
    return 1;
  }
}

writingFiles()

Write periodic restart and snapshot files.

event writingFiles (i++) {
  static double next_snapshot = 0.;
  if (!output_due (t, &next_snapshot, snapshot_interval))
    return 0;
  dump (file = "restart");
  char nameOut[80];
  snprintf (nameOut, sizeof nameOut, "intermediate/snapshot-%5.4f",
            next_snapshot - snapshot_interval);
  dump (file = nameOut);
}

stop_at_tmax()

Write a terminal snapshot and stop the otherwise open-ended event loop at the configured physical time. This makes reduced runs genuinely bounded.

event stop_at_tmax (i++; t <= HUGE) {
  if (t + 1e-12 < tmax)
    return 0;
  dump (file = "final");
  return 1;
}

logWriting()

Log kinetic energy and droplet center-of-mass velocity.

event logWriting (i++) {
  static double next_log = 0.;
  if (!output_due (t, &next_log, log_interval))
    return 0;
  double ke = 0., vcm = 0., wt = 0., overlap = 0.;
  double volume_drop = 0., volume_solid = 0., volume_liquid = 0.;
  double stress_drop = 0., stress_solid = 0., stress_liquid = 0.;
  foreach (reduction(+:ke) reduction(+:vcm) reduction(+:wt)
           reduction(+:volume_drop) reduction(+:volume_solid)
           reduction(+:volume_liquid) reduction(max:overlap)
           reduction(max:stress_drop) reduction(max:stress_solid)
           reduction(max:stress_liquid)) {
    double volume = dv();
    double wd, ws, wl;
    phase_weights (f1[], f2[], &wd, &ws, &wl);
    double stress_norm = sqrt (sq(tau_p.x.x[]) + sq(tau_p.y.y[]) +
                               2.*sq(tau_p.x.y[]) + sq(tau_qq[]));
    ke += 0.5*rho(f1[], f2[])*(sq(u.x[]) + sq(u.y[]))*volume;
    vcm += f1[]*u.x[]*volume;
    wt += f1[]*volume;
    volume_drop += wd*volume;
    volume_solid += ws*volume;
    volume_liquid += wl*volume;
    overlap = max (overlap, min (f1[], f2[]));
    stress_drop = max (stress_drop, wd*stress_norm);
    stress_solid = max (stress_solid, ws*stress_norm);
    stress_liquid = max (stress_liquid, wl*stress_norm);
  }
  if (wt > 0.0) vcm /= wt;
  static FILE * fp;

  if (pid() == 0){
    if (i == 0) {
      fprintf (ferr, "i dt t ke vcm overlap Vd Vs Vl Td Ts Tl\n");
      fp = fopen ("log", "w");
      if (!fp) {
        perror ("log");
        simulation_abort (4);
      }
      fprintf (fp, "i dt t ke vcm overlap Vd Vs Vl Td Ts Tl\n");
    } else {
      fp = fopen ("log", "a");
      if (!fp) {
        perror ("log");
        simulation_abort (4);
      }
    }
    fprintf (fp, "%d %g %g %g %5.4e %g %g %g %g %g %g %g\n",
             i, dt, t, ke, vcm, overlap,
             volume_drop, volume_solid, volume_liquid,
             stress_drop, stress_solid, stress_liquid);
    fclose(fp);
    fprintf (ferr, "%d %g %g %g %5.4e %g %g %g %g %g %g %g\n",
             i, dt, t, ke, vcm, overlap,
             volume_drop, volume_solid, volume_liquid,
             stress_drop, stress_solid, stress_liquid);
    if (overlap > 1e-3)
      fprintf (ferr,
               "WARNING: VoF overlap=%g; carrier-film separation is under-resolved\n",
               overlap);
  }
    assert(ke > -1e-10);
  // assert(ke < 1e2);
  // dump(file = "dumpTest");
}

log_hypha_deformation()

Track maximum hypha interface height using a sub-cell estimate of the f2 = 0.5 contour.

event log_hypha_deformation (i++) {

  static double next_deformation_log = 0.;
  if (!output_due (t, &next_deformation_log, log_interval))
    return 0;

  const double f_eps = 1e-6;
  const double dy_eps = 1e-12;
  double y_if_max = -1e9;

  foreach (reduction(max:y_if_max)) {
    double f = f2[];
    if (f <= f_eps || f >= 1.0 - f_eps)
      continue; // not in interfacial band

    const double y_top = y + 0.5*Delta;
    if (y_top <= y_if_max)
      continue; // even clamped estimate cannot beat current local max

    // Default fallback is cell-center estimate.
    double y_if = y;
    double dfdy = (f2[0,1] - f2[0,-1])/(2.*Delta);

    if (fabs(dfdy) > dy_eps) {
      // Solve linearized f2(xc,y) = 0.5 and clamp to current cell bounds.
      const double y_bot = y - 0.5*Delta;
      y_if = y + (0.5 - f)/dfdy;
      if (y_if > y_top) y_if = y_top;
      else if (y_if < y_bot) y_if = y_bot;
    }

    if (y_if > y_if_max)
      y_if_max = y_if;
  }

  if (y_if_max < -1e8)
    y_if_max = NAN;

  if (pid() == 0) {
    static FILE *fh = NULL;
    static int fh_open_failed = 0;
    if (!fh && !fh_open_failed) {
      const bool fresh_run = t <= 1e-12;
      fh = fopen("hypha-def-log", fresh_run ? "w" : "a");
      if (!fh) {
        perror("hypha-def-log");
        fh_open_failed = 1;
      } else if (fresh_run)
        fprintf(fh, "t y_if_max\n");
    }

    if (fh) {
      fprintf(fh, "%g %g\n", t, y_if_max);
      fflush(fh);
    }
  }
}