sandbox/nlemoine/SWE/inflow-tilt.h

    Return to my homepage

    Toolbox for handling discharge inflows in a 2D domain without masks (MPI compatible)

    Structures

    The inflow procedure relies on the definition of an inlet segment (left bank to right bank), which is then “rasterized” on grid faces. We first define a couple of useful structures:

    • the BoundaryFace structure, which is the basic element constituting the rasterized inlet segment
    typedef struct {
     coord Center; 
     coord InwardNormal;
     double Delta;
    } BoundaryFace;
    • the PID_controller structure which implements a proportional-integral-derivative control loop. It will help regulate inflow discharge according to the difference between the desired setpoint and the “measured” discharge.
    typedef struct {
      double Kp;
      double Ti;
      double Td;
      double e; // current error, e = -log(Q/Qsetpoint)
      double E; // integral of error
      double de_dt; // derivative 
      double fQ; // control variable, such that eta = eta_b(fQ*Q,inlet)
    } PID_Controller;
    • the Inlet structure itself, basically composed of an array of BoundaryFace structures, and fitted with a PID controller.
    struct Inlet {
      coord Segment[2]; // LEFT bank to RIGHT bank
      coord vec_n; // inward normal
      coord vec_t;
      // Rasterization on grid faces ("staircase" version of the boundary):
      int nf;
      BoundaryFace * BFaces;
      PID_Controller Controller;
    //  char Name[100];
      int ID;
      bool with_velocity;
      int nQ;
      double * Time;
      double * Q;
    };
    Inlet geometry

    Inlet geometry

    Initialization functions

    Function to reset the PID controller

    int Reset_PID_Controller(PID_Controller * _C)
    {
     _C->e = 0.;
     _C->E = 0.;
     _C->de_dt = 0.;
     _C->fQ = 1.0;
    
     return(0);
    }

    Function to initialize the inlet structure

    Most importantly, this function performs the rasterization of the inlet segment on the grid. This rasterization is shared by all processes.

    int Init_Inlet(struct Inlet * _inlet, coord LeftBank, coord RightBank)
    {
      scalar pscaln[], pscalt[];
      double xA = LeftBank.x, yA = LeftBank.y, xB = RightBank.x, yB = RightBank.y;
      (_inlet->Segment)[0] = (coord){xA,yA};
      (_inlet->Segment)[1] = (coord){xB,yB};
    
      double norm = sqrt(sq(xB-xA) + sq(yB-yA));
      assert (norm > 0.);
      
      // tangent and normal unit vector to the inflow section 
      _inlet->vec_t = (coord){ (xB-xA)/norm , (yB-yA)/norm };
      _inlet->vec_n = (coord){ (yA-yB)/norm + 1.e-6 , (xB-xA)/norm - 1.5e-6};
    
      (void) Reset_PID_Controller(&(_inlet->Controller));
    
      /* Rasterize [AB] segment on grid faces */
    
      // Compute scalar products for each cell center
      foreach(){
        pscaln[] = (x-xA)*(_inlet->vec_n.x) + (y-yA)*(_inlet->vec_n.y);
        if(pscaln[]==0.) pscaln[] = -(1e-6); // "on the boundary" means "outside"
        pscalt[] = (x-xA)*(_inlet->vec_t.x) + (y-yA)*(_inlet->vec_t.y);
      }       
      boundary({pscaln,pscalt});    
    
      double xf,yf;
      int nf = 0.;
      int iig,jjg;
      BoundaryFace * sendFaces = NULL;
    
      foreach_face(x)
      {
        xf = x;
        yf = y;
        iig = pscaln[]>0. ? -ig : ig; // *inward* pointing normal
        double facesize = Delta;
        
        // check whether face-sharing cells are on either side of the segment
        if( (pscaln[]*pscaln[-1,0]<0.) && (pscalt[]>=0.) && (pscalt[-1,0]>=0) && (pscalt[]<=norm) && (pscalt[-1,0]<=norm) )
        {
          // locate "outer" cell
          Point point = locate(xf-iig*L0/N/2.,yf);
    
          if(point.level>0) // keep face only if "outer", pseudo-ghost cell is in the sub-domain (avoids duplicates when using MPI)
          {
            sendFaces = (BoundaryFace *) realloc(sendFaces,(nf+1)*sizeof(BoundaryFace));
    	sendFaces[nf].Center.x = xf;
    	sendFaces[nf].Center.y = yf;
    	sendFaces[nf].InwardNormal.x = (double) iig;
    	sendFaces[nf].InwardNormal.y = 0.;
    	sendFaces[nf].Delta = facesize;
            nf++;
          }
        } // end if
      } // end foreach_face(x)
    
      foreach_face(y)
      {
        xf = x;
        yf = y;
        jjg = pscaln[]>0. ? -jg : jg; // *inward* pointing normal
        double facesize = Delta;
    
        // check whether face-sharing cells are on either side of the segment
        if( (pscaln[]*pscaln[0,-1]<=0.) && (pscalt[]>=0.) && (pscalt[0,-1]>=0) && (pscalt[]<=norm) && (pscalt[0,-1]<=norm) )
        {
          // locate "outer" cell
          Point point = locate(xf,yf-jjg*L0/N/2.);
    
          if(point.level>0) // keep face only if "outer", pseudo-ghost cell is in the sub-domain (avoids duplicates when using MPI)
          {
            sendFaces = (BoundaryFace *) realloc(sendFaces,(nf+1)*sizeof(BoundaryFace));
    	sendFaces[nf].Center.x = xf;
    	sendFaces[nf].Center.y = yf;
    	sendFaces[nf].InwardNormal.x = 0.;
    	sendFaces[nf].InwardNormal.y = (double) jjg;
    	sendFaces[nf].Delta = facesize;
            nf++;
          }
        } // end if
      } // end foreach_face(y)
    
      // We want all the processes to share the full rasterized boundary, so we gather the results
      @if _MPI
        int nfa[npe()], nfat[npe()];
        int nft;
        nfat[0] = 0;
        // First count how many faces where found by each process, put the results in array nfa
        MPI_Allgather (&nf, 1, MPI_INT, &nfa[0], 1, MPI_INT, MPI_COMM_WORLD);
    
        // Compute displacements
        for(int j=1;j<npe();j++)
         nfat[j] = nfat[j-1]+nfa[j-1];
    
        // Compute total number of faces
        nft = nfat[npe()-1]+nfa[npe()-1];  
        // Allocate receiver buffer in the structure
        _inlet->BFaces = malloc(sizeof(BoundaryFace)*nft);
        // Scale size of counts (nfa) & displacements (nfat) to have them in bytes 
        for(int j=0;j<npe();j++){
          nfa[j]  *= sizeof(BoundaryFace);
          nfat[j] *= sizeof(BoundaryFace);
        }
        // Gather
        MPI_Allgatherv (&sendFaces[0], nfa[pid()], MPI_BYTE,
                        &(_inlet->BFaces[0]),nfa,nfat,MPI_BYTE,
    		    MPI_COMM_WORLD); 
        _inlet->nf = nft;
      @else
        _inlet->BFaces = malloc(sizeof(BoundaryFace)*nf);
        for(int j=0;j<nf;j++){
          _inlet->BFaces[j].Center.x = sendFaces[j].Center.x;
          _inlet->BFaces[j].Center.y = sendFaces[j].Center.y;
          _inlet->BFaces[j].InwardNormal.x = sendFaces[j].InwardNormal.x;
          _inlet->BFaces[j].InwardNormal.y = sendFaces[j].InwardNormal.y;
          _inlet->BFaces[j].Delta = sendFaces[j].Delta;
        }
        _inlet->nf = nf;
      @endif
    
      free(sendFaces);
    
      _inlet->nQ = 0;
      _inlet->Time = NULL;
      _inlet->Q = NULL;
    
      return(0);
    }
    
    int Deallocate_Inlet(struct Inlet * _inlet)
    {
       free(_inlet->BFaces);
       _inlet->nf = 0.;
       return(0);
    }

    Discharge functions

    The functions provided in this section are essentially a copy of the functions available in discharge.h: we seek the value \eta_b of the water surface elevation upstream of the inlet section, which yields the desired inflow discharge through the inlet section. The only difference is in the bflux function: because we do not have a true boundary condition on z_b (typically Neumann), we have to perform the hydrostatic reconstruction of interface elevation for each boundary face composing the inlet section, just as in the Saint-Venant solver.

    struct Eta_b {
      // compulsory arguments
      double Q_b;
      struct Inlet * ptr_inlet;
      // optional arguments
      double prec; // precision (default 0.1%)
    };
    
    static double bflux (struct Eta_b p, double eta_b)
    {
      double Q = 0.;
      char fluxfile[200];
      bool write_fluxes = false ; // (p.ptr_inlet->ID)>0;
      FILE * fp ;
      if(write_fluxes){
       sprintf(fluxfile,"out/bflux/inlet_%d_t_%.3f_pid_%d.csv",p.ptr_inlet->ID,t,pid());
       fp = fopen(fluxfile,"w"); 
       fprintf(fp,"%s\n","x,y,ig,jg,eta_b,zb[out],h[in],dQ");
      }
    
    // Center of inlet is taken as reference point for "lake-at-rest" correction with tilt
      double xcent = (p.ptr_inlet->Segment[0].x + p.ptr_inlet->Segment[1].x)/2.;
      double ycent = (p.ptr_inlet->Segment[0].y + p.ptr_inlet->Segment[1].y)/2.;
    
      for(int k = 0;k<(p.ptr_inlet->nf);k++)
      {
        int iig = (int) p.ptr_inlet->BFaces[k].InwardNormal.x;
        int jjg = (int) p.ptr_inlet->BFaces[k].InwardNormal.y;
        // locate "outer", pseudo-ghost cell (is in subdomain by construction)
        double xout = (p.ptr_inlet->BFaces[k].Center.x) - iig*L0/N/2;
        double yout = (p.ptr_inlet->BFaces[k].Center.y) - jjg*L0/N/2;
        Point point = locate(xout,yout);
    
        if(point.level>0){

    As the function can be used with topography detrending, we also correct \eta_b in the inlet area as a function of pseudo-ghost cell coordinates (x_\mathrm{out},y_\mathrm{out}) so that the water surface elevation truly corresponds to a lake-at-rest condition:

          double corr_eta = tilt.x*(xout-xcent)+tilt.y*(yout-ycent);

    Left / right states reconstruction (see here for details):

          double dx = p.ptr_inlet->BFaces[k].Delta ;
          
          double up = iig*u.x[iig,0]+jjg*u.y[0,jjg]; // projection of inner velocity on face inward normal
          double upp = iig*u.x[2*iig,0]+jjg*u.y[0,2*jjg];
          double un = p.ptr_inlet->with_velocity ? iig*u.x[]+jjg*u.y[] : 0.;
          double unn = p.ptr_inlet->with_velocity ? iig*u.x[-iig,0]+jjg*u.y[0,-jjg] : 0.;
    
          double etap = eta[iig,jjg]; // inner cell
          double etapp = eta[2*iig,2*jjg];
          double etan = eta_b + corr_eta; // outer, pseudo-ghost cell
          double etann = eta_b + corr_eta - iig*dx*tilt.x - jjg*dx*tilt.y;
    
          double hp = h[iig,jjg];
          double hpp = h[2*iig,2*jjg];
          double hn = etan - zb[];
          double hnn = etann - zb[-iig,-jjg];
    
          // Estimate gradients
    
          double gr_eta_p = eta.gradient(etan,etap,etapp)/dx;
          double gr_eta_n = eta.gradient(etann,etan,etap)/dx;
          double gr_h_p = h.gradient(hn,hp,hpp)/dx;
          double gr_h_n = h.gradient(hnn,hn,hp)/dx;
          double gr_u_p = u.x.gradient(un,up,upp)/dx;
          double gr_u_n = u.x.gradient(unn,un,up)/dx;
    
          // Left/Right state reconstruction
    
          double zl = (etap-hp) - (dx/2.)*(gr_eta_p - gr_h_p);
          double zr = (etan-hn) + (dx/2.)*(gr_eta_n - gr_h_n);
          double zlr = max(zl, zr);
    	
          double hl = hp - (dx/2.)*gr_h_p;
          double ul = up - (dx/2.)*gr_u_p;
          hl = max(0., hl + zl - zlr);
    	
          double hr = hn + (dx/2.)*gr_h_n;
          double ur = un + (dx/2.)*gr_u_n;
          hr = max(0., hr + zr - zlr);
    
          hp = hl;
          up = ul;
          hn = hr;
          un = ur;
    
          if (hp > dry || hn > dry) {
            double fh, fu, dtmax;
            kurganov (hn, hp, un, up, 0., &fh, &fu, &dtmax);
            double dQ = fh*(p.ptr_inlet->BFaces[k].Delta); 
            Q += dQ;
            if(write_fluxes)
             fprintf(fp,"%g,%g,%d,%d,%g,%g,%g,%g\n", (p.ptr_inlet->BFaces[k].Center.x), (p.ptr_inlet->BFaces[k].Center.y),iig,jjg,eta_b,zb[],hp,dQ);
          }
        } // end if point.level>0
      } // end for
    
    // Reduction
    
    if(write_fluxes)
      fclose(fp);
    
    @if _MPI
      MPI_Allreduce (MPI_IN_PLACE, &Q, 1, MPI_DOUBLE, MPI_SUM, MPI_COMM_WORLD);
    @endif
    
    //  printf("Q(eta=%g)=%g\n",eta_b,Q);
    
      return Q;
    
    }
    
    
    static double falsepos (struct Eta_b p,
    			double binf, double qinf,
    			double bsup, double qsup)
    {
      int n = 0;
      double newb, newq;
      qinf -= p.Q_b;
      qsup -= p.Q_b;
      do {
        newb = (binf*qsup - bsup*qinf)/(qsup - qinf);
        newq = bflux (p, newb) - p.Q_b;
        if (newq > 0.)
          bsup = newb, qsup = newq;
        else
          binf = newb, qinf = newq;
        n++;
      } while (fabs(newq/p.Q_b) > p.prec && n < 100);
    
      if (n >= 100)
        fprintf (stderr, "WARNING: eta_b(): convergence not reached\n");
      
      return newb;
    }
    
    double eta_b (struct Eta_b p)
    {
      double zmin = HUGE, etas = 0., hs = 0.;
    
      for(int k = 0;k<(p.ptr_inlet->nf);k++)
      {
        int iig = (int) p.ptr_inlet->BFaces[k].InwardNormal.x;
        int jjg = (int) p.ptr_inlet->BFaces[k].InwardNormal.y;
        // locate "outer" cell
        double xint = (p.ptr_inlet->BFaces[k].Center.x) - iig*L0/N/2;
        double yint = (p.ptr_inlet->BFaces[k].Center.y) - jjg*L0/N/2;
        Point point = locate(xint,yint);
        if(point.level>0){
          if (zb[] < zmin)
    	zmin = zb[];
          etas += (p.ptr_inlet->BFaces[k].Delta)*h[]*eta[];
          hs += (p.ptr_inlet->BFaces[k].Delta)*h[];
        }
      }
    
    // Reduction
    
      @if _MPI
      MPI_Allreduce (MPI_IN_PLACE, &zmin, 1, MPI_DOUBLE, MPI_MIN, MPI_COMM_WORLD);
      MPI_Allreduce (MPI_IN_PLACE, &etas, 1, MPI_DOUBLE, MPI_SUM, MPI_COMM_WORLD);
      MPI_Allreduce (MPI_IN_PLACE, &hs  , 1, MPI_DOUBLE, MPI_SUM, MPI_COMM_WORLD);
      @endif
    
      if (p.Q_b <= 0.)
        return zmin - 1.;
    
      // We try to find good bounds on the solution.
    
      double etasup = hs > 0. ? etas/hs : zmin;
      double Qsup = bflux (p, etasup), etainf = zmin, Qinf = 0.;
      double h0 = etasup - zmin;
      if (h0 < dry)
        h0 = 1.;
      int n = 0;
      while (Qsup < p.Q_b && n++ < 100) {
        etainf = etasup, Qinf = Qsup;
        etasup += h0;
        Qsup = bflux (p, etasup);
      }
      if (n >= 100)
        fprintf (stderr, "WARNING: eta_b() not converged\n");
        
      if (!p.prec) p.prec = 0.001; // 0.1% by default
      return falsepos (p, etainf, Qinf, etasup, Qsup);
    }
    
    double segment_flux (coord segment[2], scalar h, vector u)  // vient de "layered/hydro.h"
    {
      coord m = {segment[0].y - segment[1].y, segment[1].x - segment[0].x};
      normalize (&m);
      double tot = 0.;
    
      foreach_segment (segment, p) {
        double dl = 0.;
        foreach_dimension() {
          double dp = (p[1].x - p[0].x)*Delta/Delta_x*(fm.y[] + fm.y[0,1])/2.;
          dl += sq(dp);
        }
        dl = sqrt (dl);    
        for (int i = 0; i < 2; i++) {
          coord a = p[i];
          tot += dl/2.*
    	interpolate_linear (point, (struct _interpolate)
    			    {h, a.x, a.y, 0.})*
    	(m.x*interpolate_linear (point, (struct _interpolate)
    				 {u.x, a.x, a.y, 0.}) +
    	 m.y*interpolate_linear (point, (struct _interpolate)
    				 {u.y, a.x, a.y, 0.}));
        }
      }
      // reduction
    #if _MPI
      MPI_Allreduce (MPI_IN_PLACE, &tot, 1, MPI_DOUBLE, MPI_SUM, MPI_COMM_WORLD);
    #endif
      return tot;
    }
    
    int set_inlet_fields(struct Inlet * _inlet, double etain)
    {
        double xcent = (_inlet->Segment[0].x + _inlet->Segment[1].x)/2.;
        double ycent = (_inlet->Segment[0].y + _inlet->Segment[1].y)/2.;
    
        foreach(){  // set eta to etain for all points in alcove
          double xrel = x-(_inlet->Segment)[0].x , yrel = y-(_inlet->Segment)[0].y;
          double norm = sqrt(sq(_inlet->Segment[0].x-_inlet->Segment[1].x)+sq(_inlet->Segment[0].y-_inlet->Segment[1].y)); 
          double psn = xrel*(_inlet->vec_n.x) + yrel*(_inlet->vec_n.y);
          double pst = xrel*(_inlet->vec_t.x) + yrel*(_inlet->vec_t.y);
          if(psn<0. && (psn>=-500.) && pst>=0 && pst<=norm)
          {
            double corr_eta = tilt.x*(x-xcent)+tilt.y*(y-ycent);
            h[] = fmax(0.,etain+corr_eta-zb[]);
            if( !(_inlet->with_velocity) | h[]<dry){ 
              u.x[] = 0.;
              u.y[] = 0.;
            }
          }
        } // end foreach()
    
        // Set pseudo-Neumann velocity on segment outer cells
    
        for(int k = 0;k<(_inlet->nf);k++)
        {
          int iig = (int) _inlet->BFaces[k].InwardNormal.x;
          int jjg = (int) _inlet->BFaces[k].InwardNormal.y;
          // locate "outer" cell (is in subdomain by construction)
          double xint = (_inlet->BFaces[k].Center.x) - iig*L0/N/2;
          double yint = (_inlet->BFaces[k].Center.y) - jjg*L0/N/2;
          Point point = locate(xint,yint);
          if(point.level>0){
            double corr_eta = tilt.x*(xint-xcent)+tilt.y*(yint-ycent);
            h[] = fmax(0.,etain+corr_eta-zb[]);
             if( !(_inlet->with_velocity) | h[]<dry){
              u.x[] = 0.;
              u.y[] = 0.;
             }
          }
        } // end for(int k...
    
        // end
    
        return(0);
    }
    
    int update_PID_error(double Qm, double Qsp, PID_Controller * _C)
    {
       double newe = -log(Qm/Qsp); // setpoint-measurement, in log
       double de_dt = (newe-(_C->e))/dt; 
       double rho = exp(-dt/(_C->Ti));
       // Integral error with exp(t-t0) weight function (~Gauss-Laguerre quadrature)   
       _C->E = rho*(_C->E) + de_dt*dt + ((_C->e)-de_dt*(_C->Ti))*(1.-rho);
    
       double newcontrol =  (_C->Kp)*(newe
                                      + (_C->E)
                                      + de_dt*(_C->Td)
                                     );
       _C->e = newe;
       (_C->fQ) *= exp(newcontrol);
       
       return(0);
    }

    Managing inflow hydrographs

    Read hydrograph from file

    This function has 3 arguments: a pointer to an Inlet structure, the path to the text file containing (time,Q) pairs, and an offset expressed in fractional days. In this implementation, times in the file are assumed to be expressed in fractional days as returned by the function datenum() in Matlab, Scilab, or Octave (e.g. 2022/01/01 14:30:00 is 738522.604167). As the file is parsed, times are converted to seconds elapsed from the offset datenum0.

    int load_hydrograph(struct Inlet * _inlet, char * hydrofile, double datenum0)
    {
      char buffer[200]; 
      int nQ;
      double time,Q;
      FILE * fp;
    
      if(!(fp = fopen (hydrofile, "r")))
      {
        printf("Failed to open hydrograph file!\n");
        return -1;
      }
      
      nQ = 0;
    
      // Read (time,Q) pairs
      while( fgets(buffer, 200, fp)!= NULL ) // parse until end-of-file
      {
      	sscanf(buffer,"%lf %lf",&time, &Q);
            double t_sec = (time-datenum0)*86400.;
            if(t_sec>=0)
    	{
    	  nQ++;
    	  _inlet->Time = (double *) realloc( _inlet->Time, nQ*sizeof(double));
    	  _inlet->Q    = (double *) realloc( _inlet->Q   , nQ*sizeof(double));
    	  _inlet->Time[nQ-1] = t_sec;
    	  _inlet->Q[nQ-1] = Q;
    	}
      }
    
      _inlet->nQ = nQ;
      fclose(fp);
      printf("%d records loaded from %s\n",nQ,hydrofile);
    
      return(0);   
    }

    Linear interpolation in hydrograph

    double interpolate_discharge(struct Inlet * _inlet, double time)
    {
      if(time <= _inlet->Time[0])
        return(_inlet->Q[0]);
    
      if(time >= _inlet->Time[(_inlet->nQ)-1])
        return(_inlet->Q[(_inlet->nQ)-1]);
    
      int pos = 0;
      while(time > (_inlet->Time[pos]) )
       pos++;
    
      double t0 = _inlet->Time[pos-1];
      double t1 = _inlet->Time[pos];
      double Q0 = _inlet->Q[pos-1];
      double Q1 = _inlet->Q[pos];
    
      return (Q0 + (time-t0)*(Q1-Q0)/(t1-t0));
    }