sandbox/ghigo/src/test-stl/wind-turbine-flow.c

    Incompressible flow past a fixed windturbine

    #include "grid/octree.h"
    #include "../myembed.h"
    #include "../mycentered.h"
    #include "distance.h"
    #include "lambda2.h"
    #include "view.h"
    #include "../myperfs.h"

    Reference solution

    #define Re   (1000.)      // Reynolds number, u*l/nu
    #define l    (100.)       // Characteristic size of the windturbine
    #define uref (1.)         // Reference velocity
    #define tref ((l)/(uref)) // Reference time, tref=l/u

    Importing the geometry from an stl file

    This function computes the solid and face fractions given a pointer to an STL file, an adaptation criteria (maximum relative error on distance) and a minimum and maximum level.

    #define scmin (1.e-14) // Minimun volume and face fraction,
              
    void fraction_from_stl (scalar c, face vector f, FILE * fp, double eps,
    			int minlevel, int maxlevel)
    {

    We read the STL file and compute the bounding box of the model.

      coord * p = input_stl (fp);
      coord min, max;
      bounding_box (p, &min, &max);
      double maxl = -HUGE;
      foreach_dimension()
        if (max.x - min.x > maxl)
          maxl = max.x - min.x;

    We initialize the distance field on the coarse initial mesh and refine it adaptively until the threshold error (on distance) is reached.

      scalar d[];
      distance (d, p);
    #if TREE
      while (adapt_wavelet ({d}, (double[]){eps*maxl}, maxlevel, minlevel).nf);
    #endif // TREE

    We also compute the volume fraction from the distance field. We first construct a vertex field interpolated from the centered field and then call the appropriate VOF functions. We have to be carefull with the orientation of the normals (- sign here).

      vertex scalar phi[];
      foreach_vertex()
        phi[] = -(d[] + d[-1] + d[0,-1] + d[-1,-1] +
    	     d[0,0,-1] + d[-1,0,-1] + d[0,-1,-1] + d[-1,-1,-1])/8.;
      boundary ({phi});
      fractions (phi, c, f);
      fractions_cleanup (c, f,
      		     smin = (scmin), cmin = (scmin));
    }

    Setup

    We need a field for viscosity so that the embedded boundary metric can be taken into account.

    face vector muv[];

    We also define a reference velocity field.

    scalar un[];

    We define the mesh adaptation parameters.

    #define lmin (5) // Min mesh refinement level (l=5 is 3pt/l)
    #define lmax (9) // Max mesh refinement level (l=9 is 50pt/l)
    #define cmax (1.e-2*(uref)) // Absolute refinement criteria for the velocity field
    
    int main ()
    {

    The domain is 1024\times 1024\times 1024.

      L0 = 1024.;
      size (L0);
      origin (-(L0)/2., -(L0)/2., 0.); // Centered in x and y, z is the altitude

    We set the maximum timestep.

      DT = 1.e-2*(tref);
      NITERMAX = 200;

    We set the tolerance of the Poisson solver.

      TOLERANCE    = 1.e-4;
      TOLERANCE_MU = 1.e-4*(uref);

    We initialize the grid.

      N = 1 << (lmin);
      init_grid (N);
      
      run();
    }

    Boundary conditions

    We use inlet boundary conditions in the y-direction.

    u.n[bottom] = dirichlet ((uref));
    u.t[bottom] = dirichlet (0);
    u.r[bottom] = dirichlet (0);
    p[bottom]   = neumann (0);
    pf[bottom]  = neumann (0);
    
    u.n[top] = neumann (0);
    u.t[top] = neumann (0);
    u.r[top] = neumann (0);
    p[top]   = dirichlet (0);
    pf[top]  = dirichlet (0);

    The boundary z=0 is the ground with a no-slip boundary condition.

    u.n[back] = dirichlet (0);
    u.t[back] = dirichlet (0);
    u.r[back] = dirichlet (0);
    p[back]   = neumann (0);
    pf[back]  = neumann (0);

    We give boundary conditions for the face velocity to “potentially” improve the convergence of the multigrid Poisson solver.

    uf.n[left]   = 0;
    uf.n[right]  = 0;
    uf.n[bottom] = (uref);
    uf.n[back]   = 0;
    uf.n[front]  = 0;

    Properties

    event properties (i++)
    {
      foreach_face()
        muv.x[] = (uref)*(l)/(Re)*fm.x[];
      boundary ((scalar *) {muv});
    }

    Initial conditions

    event init (i = 0)
    {

    We set the viscosity field in the event properties.

      mu = muv;

    We use “third-order” face flux interpolation.

    #if ORDER2
      for (scalar s in {u, p, pf})
        s.third = false;
    #else
      for (scalar s in {u, p, pf})
        s.third = true;
    #endif // ORDER2

    We use a slope-limiter to reduce the errors made in small-cells.

    #if SLOPELIMITER
      for (scalar s in {u}) {
        s.gradient = minmod2;
      }
    #endif // SLOPELIMITER
      
    #if TREE

    When using TREE and in the presence of embedded boundaries, we should also define the gradient of u at the cell center of cut-cells.

    #endif // TREE

    We initialize the embedded boundary. We read the stl file that contains the network geometry.

      FILE * fp = fopen ("../data/wind-turbine/windTurbineGeom.stl", "r");
      fraction_from_stl (cs, fs, fp, 1.e-4, (lmin), max (10, (lmax)));
      fclose (fp);
      
    #if TREE

    When using TREE, we refine the mesh around the embedded boundary. Since we do not have an analytic expression for the level-set function, we perform this operation only once.

      adapt_wavelet ({cs}, (double[]) {1.e-30},
    		 maxlevel = (lmax), minlevel = (1));
      fractions_cleanup (cs, fs,
      		     smin = (scmin), cmin = (scmin));
    #endif // TREE

    We also define the volume fraction at the previous timestep csm1=cs.

      csm1 = cs;

    We define the no-slip boundary conditions for the velocity.

      u.n[embed] = dirichlet (0);
      u.t[embed] = dirichlet (0);
      u.r[embed] = dirichlet (0);
      p[embed]   = neumann (0);
    
      uf.n[embed] = dirichlet (0);
      uf.t[embed] = dirichlet (0);
      uf.r[embed] = dirichlet (0);
      pf[embed]   = neumann (0);

    We initialize the velocity.

      foreach()
        u.y[] = cs[]*(uref);
      boundary ((scalar *) {u});

    We finally initialize the reference velocity field.

      foreach()
        un[] = u.y[];
    }

    Embedded boundaries

    Adaptive mesh refinement

    #if TREE
    event adapt (i++)
    {
      adapt_wavelet ({cs,u}, (double[]) {1.e-2,(cmax),(cmax),(cmax)},
      		 maxlevel = (lmax), minlevel = (1));

    We do not reset the embedded fractions to avoid interpolation errors on the geometry as we do not have an analytic expression for the level-set function.

      fractions_cleanup (cs, fs,
      		     smin = (scmin), cmin = (scmin));
    }
    #endif // TREE

    Outputs

    event logfile (i++; t <= 100.*(tref))
    {

    We look for a stationary solution.

      double du = change (u.y, un);
    
      stats nx = statsf (u.x), ny = statsf(u.y), nz = statsf (u.z), np = statsf(p);
      fprintf (stderr, "%d %g %g %d %d %d %d %d %d %g %g %g %g %g %g %g %g %g %g %g %g %g %g %g %g %g %ld %g\n",
    	   i, t/(tref), dt/(tref),
    	   mgp.i, mgp.nrelax, mgp.minlevel,
    	   mgu.i, mgu.nrelax, mgu.minlevel,
    	   mgp.resb, mgp.resa,
    	   mgu.resb, mgu.resa,
    	   nx.sum/(nx.volume + SEPS), nx.min, nx.max,
    	   ny.sum/(ny.volume + SEPS), ny.min, ny.max,
    	   nz.sum/(nz.volume + SEPS), nz.min, nz.max,
    	   np.sum/(np.volume + SEPS), np.min, np.max,
    	   du,
    	   grid->tn, perf.t
    	   );
      fflush (stderr);
    }

    Snapshots

    event snapshots (t = end)
    {
      view (fov = 16,
      	quat = {0.575, -0.191, -0.261, 0.752},
      	ty = -0.2,
      	bg = {0.3,0.4,0.6},
      	width = 800, height = 800);
    
      clear();
      draw_vof ("cs", "fs", lw = 0.5);
      save ("vof.png");
      
      draw_vof ("cs", "fs", lw = 0.5);
      squares ("u.x", n= {1,0,0}, map = cool_warm);
      save ("ux.png");
    
      draw_vof ("cs", "fs", lw = 0.5);
      squares ("u.y", n= {1,0,0}, map = cool_warm);
      save ("uy.png");
    
      draw_vof ("cs", "fs", lw = 0.5);
      squares ("u.y", n= {0,1,0}, map = cool_warm);
      save ("uy-2.png");
    
      draw_vof ("cs", "fs", lw = 0.5);
      squares ("p", n= {1,0,0});
      save ("p.png");
    
      scalar l2[];
      lambda2 (u, l2);
      boundary ({l2});
    
      draw_vof ("cs", "fs", lw = 0.5);
      cells (n= {1,0,0});
      squares ("u.y", n= {1,0,0}, map = cool_warm);
      isosurface ("l2", -0.002);
      save ("l2.png");
    }
    
    event animations (i += 10)
    {
      view (fov = 16,
      	quat = {0.575, -0.191, -0.261, 0.752},
      	ty = -0.2,
      	bg = {0.3,0.4,0.6},
      	width = 800, height = 800);
    
      clear();
      cells (n= {1,0,0});
      save ("mesh.mp4");
      
      draw_vof ("cs", "fs", lw = 0.5);
      squares ("u.x", n= {1,0,0}, map = cool_warm);
      save ("ux.mp4");
    
      draw_vof ("cs", "fs", lw = 0.5);
      squares ("u.y", n= {1,0,0}, map = cool_warm);
      save ("uy.mp4");
    
      draw_vof ("cs", "fs", lw = 0.5);
      squares ("u.y", n= {0,1,0}, map = cool_warm);
      save ("uy-2.mp4");
    
      draw_vof ("cs", "fs", lw = 0.5);
      squares ("p", n= {1,0,0});
      save ("p.mp4");
    
      scalar l2[];
      lambda2 (u, l2);
      boundary ({l2});
    
      draw_vof ("cs", "fs", lw = 0.5);
      cells (n= {1,0,0});
      squares ("u.y", n= {1,0,0}, map = cool_warm);
      isosurface ("l2", -0.002);
      save ("l2.mp4");
    }

    Results

    \lambda_2 criteria