sandbox/ghigo/src/myembed-moving.h

    Advection of a discrete rigid body

    We detail here the first-order in time coupling algorithm we use to advect the discrete rigid body \Gamma_{\Delta}, of boundary \delta \Gamma_{\Delta}, compatible only with the centered Navier-Stokes solver. We assume that there is only one rigid body and consider two advection scenarios:

    • imposed embedded boundary motion;

    • explicit weak fluid-solid coupling.

    Setup

    In each scenario, the discrete rigid body \Gamma_{\Delta} is characterized by: the position of the center of mass \mathbf{x}_{\Gamma} stored in p_p, the translation velocity of the center of mass \mathbf{u}_{\Gamma} stored in p_u, the angular velocity about the center of mass \mathbf{\omega}_{\Gamma} stored in p_w and their corresponding accelerations stored in p_au, p_aw.

    coord p_p;        // Position
    coord p_u, p_w;   // Velocity
    coord p_au, p_aw; // Acceleration

    The remaining unknown is the location and shape of the discrete rigid boundary \delta \Gamma_{\Delta}, which we describe using a user-defined distance function \Phi given by the function p_shape(). Note that this function must contain a call to the fractions_cleanup() function.

    extern void p_shape (scalar c, face vector f, coord p);

    Fluid-solid coupling

    The following no-slip Dirichlet boundary condition for velocity and Neumann boundary condition for pressure on the discrete rigid boundary \delta \Gamma_{\Delta} allow us to couple the motion of the fluid and the discrete rigid body \Gamma_{\Delta}:

    \displaystyle \left\{ \begin{aligned} & \mathbf{u} = \mathbf{u}_{\Gamma} = \mathbf{u_{\Gamma}} + \omega_{\Gamma} \times \left(\mathbf{x} - \mathbf{x}_{\Gamma}\right) \\ & {\nabla}_{\Gamma} p = -\rho \frac{\mathrm{D} \mathbf{u}}{\mathrm{D}t} \cdot \mathbf{n}_{\Gamma} + \left(\nabla \cdot \left(\mu \nabla \mathbf{u} \right)\right) \cdot \mathbf{n}_{\Gamma} = -\rho \left( \frac{\mathrm{d} \mathbf{u_{\Gamma}}}{\mathrm{d}t} + \frac{\mathrm{d} \mathbf{\omega_{\Gamma}}}{\mathrm{d}t} \times \left(\mathbf{x} - \mathbf{x}_{\Gamma}\right) + \mathbf{\omega_{\Gamma}} \times \mathbf{\omega_{\Gamma}} \times \left(\mathbf{x} - \mathbf{x}_{\Gamma}\right) \right) \cdot \mathbf{n}_{\Gamma} + \left(\nabla \cdot \left(\mu \nabla \mathbf{u} \right)\right) \cdot \mathbf{n}_{\Gamma}. \end{aligned} \right.

    In practice, we however use the following homogeneous Neumann boundary condition for pressure: \displaystyle {\nabla}_{\Gamma} p = 0 This condition is suitable for a fixed rigid body and we have found that using it with a moving rigid body does not significantly affect the computed solution.

    No-slip Dirichlet boundary condition for velocity

    The function velocity_noslip_x() computes the previously defined no-slip boundary condition for the x-component of the velocity \mathbf{u}.

    foreach_dimension()
    static inline double velocity_noslip_x (Point point,
    					coord up, coord wp, coord pp,
    					double xc, double yc, double zc)
    {
      assert (cs[] > 0. && cs[] < 1.);

    We first compute the relative position \mathbf{r} = \mathbf{x} - \mathbf{x}_{\Gamma}.

      // The coordinate x,y,z are not permuted with foreach_dimension()
      coord r = {xc, yc, zc};
      foreach_dimension() {
        r.x -= pp.x;
        if (Period.x) {
          if (fabs (r.x) > fabs (r.x + (L0)))
    	r.x += (L0);
          if (fabs (r.x) > fabs (r.x - (L0)))
    	r.x -= (L0);
        }
      }

    We then compute the veolcity (translation + rotation).

    #if dimension == 2
        coord sgn = {-1, 1};
        return (up.x) + sgn.x*wp.x*(r.y);
    #else // dimension == 3
        return (up.x) + wp.y*(r.z) - wp.z*(r.y);
    #endif // dimension
    }

    Neumann boundary condition for pressure

    The function pressure_gradient() returns in da the contribution of all the components of the particle acceleration and viscous stresses to the pressure gradient.

    static inline void pressure_acceleration (Point point,
    					  coord dup, coord dwp,
    					  coord wp, coord pp,
    					  double xc, double yc, double zc,
    					  coord * da)
    {
      assert (cs[] > 0. && cs[] < 1.);

    We first compute the relative position \mathbf{r} = \mathbf{x} - \mathbf{x}_{\Gamma}.

      // The coordinate x,y,z are not permuted with foreach_dimension()
      coord r = {xc,yc,zc};
      foreach_dimension() {
        r.x -= pp.x;
        if (Period.x) {
          if (fabs (r.x) > fabs (r.x + (L0)))
    	r.x += (L0);
          if (fabs (r.x) > fabs (r.x - (L0)))
    	r.x -= (L0);
        }
      }

    We first compute the acceleration of the particle gu.

      coord gu;
      foreach_dimension()
        gu.x = 0.;
      
      // Rotational acceleration
      coord dw1, dw2;
    #if dimension == 2
      coord sgn = {-1, 1};
      foreach_dimension() {
        dw1.x = sgn.x*dwp.x*(r.y);
        dw2.x = - sq(wp.x)*(r.x);
      } 
    #else // dimension = 3
      double w1 = 0., w2 = 0.;
      foreach_dimension() {
        w1 += wp.x*(r.x);
        w2 += sq(wp.x);
      }
      foreach_dimension() {
        dw1.x = dwp.y*(r.z) - dwp.z*(r.y);
        dw2.x = w1*wp.x - w2*(r.x);
      }
    #endif // dimension

    We include the translational and rotational accelerations.

      foreach_dimension()
        gu.x = dup.x + dw1.x + dw2.x;

    We then compute the viscous contribution.

      coord gmu;
      foreach_dimension()
        gmu.x = 0.;
      
      /* foreach_dimension() { */
      /*   scalar s = u.x; */
      /*   double a = 0.; */
      /*   foreach_dimension() */
      /* 	a += (mu.x[1]*face_gradient_x (s, 1) - mu.x[0]*face_gradient_x (s, 0))/Delta; */
      /*   double b, c = embed_flux (point, u.x, mu, &b); */
      /*   a += (b + c*u.x[]); */
      /*   gmu.x = -a; */
      /* } */

    Finally, we combine both contributions.

      foreach_dimension()
        da->x = (gu.x + gmu.x); 
    }
    
    foreach_dimension()
    static inline double pressure_acceleration_x (Point point,
    					      coord dup, coord dwp,
    					      coord wp, coord pp,
    					      double xc, double yc, double zc)
    {
      coord da;
      pressure_acceleration (point, dup, dwp, wp, pp, xc, yc, zc, &da);
      return da.x;
    }

    The following function finally computes the Neumann boundary condition for pressure, where the inward unit normal \mathbf{n}_{\Gamma} is pointing from fluid to solid. To switch to a homogenous Neumann boundary condition, the user can set the scalar attribute neumann_zero to true. The default is false.

    attribute {
      bool neumann_zero;
    }
    
    static inline double pressure_neumann (Point point,
    				       coord dup, coord dwp,
    				       coord wp, coord pp,
    				       double xc, double yc, double zc)
    {
      coord da = {0., 0., 0.};
      pressure_acceleration (point, dup, dwp, wp, pp, xc, yc, zc, &da);
    
      coord b, n;
      embed_geometry (point, &b, &n);
      
      double dpdn = 0.;
      foreach_dimension()
        dpdn += (-da.x)*n.x;
      dpdn *= rho[]/(cs[] + SEPS);
      return dpdn;
    }

    Dump and restore a particle

    typedef struct {
      coord c;      // Center of mass
      coord u, w;   // Velocity
      coord au, aw; // Acceleration
    } particle;
    
    struct p_Dump {
      char * file; // File name
      particle * list; // List of particles
      FILE * fp; // File pointer
      bool unbuffered;
    };
    
    void p_dump (struct p_Dump p)
    {
      FILE * fp = p.fp;
      char def[] = "p_dump", * file = p.file ? p.file : p.fp ? NULL : def;
    
      char * name = NULL;
      if (file) {
        name = (char *) malloc (strlen(file) + 2);
        strcpy (name, file);
        if (!p.unbuffered)
          strcat (name, "~");
        if ((fp = fopen (name, "w")) == NULL) {
          perror (name);
          exit (1);
        }
      }
      assert (fp);
    
      // Get particle data (only 1 particle for now)
      int p_n = 1;
      particle * p_list = p.list;
    
      // Dump particle data
      fwrite(p_list, sizeof(*p_list), p_n, fp);
      
      /* free (p_list); */
      if (file) {
        fclose (fp);
        if (!p.unbuffered)
          rename (name, file);
        free (name);
      }
    }
    
    bool p_restore (struct p_Dump p)
    {
      FILE * fp = p.fp;
      char * file = p.file;
      if (file && (fp = fopen (file, "r")) == NULL)
        return 0;
      assert (fp);
    
      // Read particle data (only 1 particle for now)
      int p_n = 1;
      particle * p_list = p.list;
    
      if (fread (p_list, sizeof(*p_list), (p_n), fp) < 1) {
        fprintf (ferr, "#p_restore(): error reading particle data\n");
        exit (1);
      }
    
      foreach_dimension() {
        p_p.x  = p_list->c.x;
        p_u.x  = p_list->u.x;
        p_w.x  = p_list->w.x;
        p_au.x = p_list->au.x;
        p_aw.x = p_list->aw.x;
      }
      
      /* free (p_list); */
      if (file)
        fclose (fp);
      
      return true;
    }

    Initialization

    event defaults (i = 0)
    {  
      foreach_dimension() {
        p_p.x  = 0.;
        p_u.x  = 0.;
        p_w.x  = 0.;
        p_au.x = 0.;
        p_aw.x = 0.;
      }
    }
    
    event init (i = 0)
    {

    We decrease the value of the CFL (same value as the one used with the VOF algorithm).

      CFL = 0.5;
    
      if (!restore (file = "restart")) { // No restart

    We initialize the embedded boundary in the test case file. We also initialize the velocity in the test case file.

      }
      else { // Restart

    We first restore the particle properties.

        particle pp_restore;
        bool p_restart = p_restore ("p_restart", &pp_restore);
        assert (p_restart == true);

    We then need to initialize the face fraction fs since it is not dumped.

        p_shape (cs, fs, p_p);
      }

    We then initialize the volume fraction at the previous timestep csm1. This needs to be done even when restarting the simulation as csm1 is not dumped.

      trash ({csm1});
      foreach()
        csm1[] = cs[];
      boundary    ({csm1});
      restriction ({csm1}); // Since restriction/prolongation depend on csm1

    Finally, we define the boundary conditions for the velocity, the pressure gradient g and the presssure p on the embedded boundaries.

      p[embed]  = neumann (p.neumann_zero ?  0. : pressure_neumann (point, (p_au), (p_aw), (p_w), (p_p), x, y, z));
      pf[embed] = neumann (pf.neumann_zero ? 0. : pressure_neumann (point, (p_au), (p_aw), (p_w), (p_p), x, y, z));
    
    #if dimension == 2
      u.n[embed]   = dirichlet (velocity_noslip_x (point, (p_u), (p_w), (p_p), x, y, z));
      u.t[embed]   = dirichlet (velocity_noslip_y (point, (p_u), (p_w), (p_p), x, y, z));
      uf.n[embed]  = dirichlet (velocity_noslip_x (point, (p_u), (p_w), (p_p), x, y, z));
      uf.t[embed]  = dirichlet (velocity_noslip_y (point, (p_u), (p_w), (p_p), x, y, z));
    
      g.n[embed] = dirichlet (p.neumann_zero ? 0. : pressure_acceleration_x (point, (p_au), (p_aw), (p_w), (p_p), x, y, z)); 
      g.t[embed] = dirichlet (p.neumann_zero ? 0. : pressure_acceleration_y (point, (p_au), (p_aw), (p_w), (p_p), x, y, z));
    #else // dimension == 3
      u.n[embed]   = dirichlet (velocity_noslip_x (point, (p_u), (p_w), (p_p), x, y, z));
      u.t[embed]   = dirichlet (velocity_noslip_y (point, (p_u), (p_w), (p_p), x, y, z));
      u.r[embed]   = dirichlet (velocity_noslip_z (point, (p_u), (p_w), (p_p), x, y, z));
      uf.n[embed]  = dirichlet (velocity_noslip_x (point, (p_u), (p_w), (p_p), x, y, z));
      uf.t[embed]  = dirichlet (velocity_noslip_y (point, (p_u), (p_w), (p_p), x, y, z));
      uf.r[embed]  = dirichlet (velocity_noslip_z (point, (p_u), (p_w), (p_p), x, y, z));
      
      g.n[embed] = dirichlet (p.neumann_zero ? 0. : pressure_acceleration_x (point, (p_au), (p_aw), (p_w), (p_p), x, y, z)); 
      g.t[embed] = dirichlet (p.neumann_zero ? 0. : pressure_acceleration_y (point, (p_au), (p_aw), (p_w), (p_p), x, y, z));
      g.r[embed] = dirichlet (p.neumann_zero ? 0. : pressure_acceleration_z (point, (p_au), (p_aw), (p_w), (p_p), x, y, z));
    #endif // dimension

    As rho is used in the Neumann boundary condition for pressure, we need rho on all levels of the grid.

      restriction ({rho});
    
      boundary ({p, u, g, pf, uf});
    }

    Timestep

    We modify the maximun timestep dtmax to account for the velocity of the discrete rigid body \Gamma_{\Delta}. Note that this event occurs before moving the embedded boundaries to their t^{n+1} position. We therefore use the position and boundary conditions at time t^n.

    event stability (i++)
    {
      foreach(reduction(min:dtmax)) {
        if (cs[] > 0. && cs[] < 1.) {
    
          // Barycenter and normal of the embedded fragment
          coord b, n;
          embed_geometry (point, &b, &n);
    
          // Local maximum velocity, in the direction of the normal
          double umax = 0.;
          foreach_dimension() {

    We use here the boundary condition on the embedded boundary.

    	bool dirichlet = true;
    	double ub = (u.x.boundary[embed] (point, point,
    					  u.x, &dirichlet));
    	assert (dirichlet);	
    	umax += (ub*n.x);
          }
          
          // Non-restrictive timestep (independent of *cs* and *fs*)
          double dte = Delta/(fabs (umax) + SEPS);
          if (dte < dtmax)
    	dtmax = dte;
        }
      }
    }

    Prediction

    event advection_term (i++)
    {

    In case of a periodic domain, we shift the coordinates of the center of mass p_p.

      coord p_o = {(X0), (Y0), (Z0)};
      foreach_dimension() {
        if (Period.x) {
          if (p_p.x < p_o.x)
    	p_p.x += (L0);
          if (p_p.x > p_o.x + (L0))
    	p_p.x -= (L0);
        }
      }

    Step 1

    We store the volume fraction defined at time t.

      trash ({csm1});
      foreach()
        csm1[] = cs[];
      boundary    ({csm1});
      restriction ({csm1}); // Since restriction/prolongation depend on csm1

    Step 2 (prediction)

    We advance the embedded boundary to time t+dt. This step requires the user to define the quantities p_p, p_u, p_w, p_au and p_aw at time t+dt.

      p_shape (cs, fs, p_p);

    We then make sure not to use any values in newly emerged cells by setting the flag emerged to false and update all boundaries as the boundary conditions depend on the embedded boundaries.

      emerged = false;
      boundary (all);

    We update the fluid properties to account for changes in the metric.

      event ("properties");

    Step 3 (emerged cells)

    Since the advection event is explict, we define here the values of the centered velocity u and centered pressure gradient g in emerged cells. We also define the pressures p and pf in emerged cells to provide an improved initial guess to the multigrid projection solver.

    In the solid cells, we set all variables to 0. This is necessary to avoid mesh adaptation inside the solid boundaries. This might however lead to inaccuracies when using the default restriction_average operator.

      for (scalar s in {p, u, g, pf})
        foreach()
          if (cs[] <= 0.)
    	s[] = 0.;

    In the emerged cells, we use an extrapolation along the normal, using the function embed_extrapolate, to update the velocity u and the pressure gradient g (both use Dirichlet boundary conditions).

      for (scalar s in {u, g})
        foreach() {
          if (csm1[] <= 0. && cs[] > 0.) {
    
      	// Normal emerged cell
      	if (cs[] < 1.) {
    
      	  // Cell centroid, barycenter and normal of the embedded fragment
    	  coord c, b, n;
    	  embed_geometry (point, &b, &n);
    	  double alpha = plane_alpha (cs[], n);
    	  plane_center (n, alpha, cs[], &c); // Different from line_area_center
    	
      	  // Dirichlet boundary condition on the embedded boundary
      	  bool dirichlet = true;
    	  double sb = (s.boundary[embed] (point, point, s, &dirichlet));
    	  assert (dirichlet);
    	  
      	  // Emerged cell value
      	  s[] = embed_extrapolate (point, s, cs, n, c, sb);
      	}
    	
      	// Pathological emerged cell (cs = 1)
      	else {
    	  int navg = 0;
    	  double savg = 0.;
    	  foreach_neighbor(1)
    	    if (cs[] > 0. && (emerged || csm1[] > 0.)) {
    	      navg += 1;
    	      savg += s[];
    	    }
    	  s[] = savg/(navg + SEPS);
      	}
          }
        }

    As the pressure uses a Neumann boundary condition, we update the value of the pressures p and pf in emerged cells using their average over neighboring cells. Note that these values of pressure are used only as the initial condition for the Poisson solver.

      for (scalar s in {p, pf})
        foreach() {
          if (csm1[] <= 0. && cs[] > 0.) {
    	int navg = 0;
    	double savg = 0.;
    	foreach_neighbor(1)
    	  if (cs[] > 0. && (emerged || csm1[] > 0.)) {
    	    navg += 1;
    	    savg += s[];
    	  }
    	s[] = savg/(navg + SEPS);
          }
        }

    Before using the boundary function, we set the emerged flag to true to indicate that all emerged cells have been updated and can now be used.

      emerged = true;
      boundary (all);

    Step 4 (boundary conditions)

    As rho is used in the Neumann boundary condition for pressure, we need rho on all levels of the grid.

      restriction ({rho});
    }

    TO DO

    • Multi-particles

    • Improve fluid-solid coupling algorithm (to remove added mass effect for density ratios close to 1)