Poisson Equation

To integrate a momentum field in time (specifically to enforce the incompressibility), we need to solve a Poisson equation:

\[\frac{1}{J} \dif{}{\gcs{1}} \left( \frac{J}{\sfact{1}} \frac{1}{\sfact{1}} \dif{p}{\gcs{1}} \right) + \frac{1}{J} \dif{}{\gcs{2}} \left( \frac{J}{\sfact{2}} \frac{1}{\sfact{2}} \dif{p}{\gcs{2}} \right) + \frac{1}{J} \dif{}{\gcs{3}} \left( \frac{J}{\sfact{3}} \frac{1}{\sfact{3}} \dif{p}{\gcs{3}} \right) = q,\]

where \(p\) and \(q\) are both defined at cell centers \(\left( \ccidx{i}, \ccidx{j}, \ccidx{k} \right)\).

Since directions except \(x\) are homogeneous (i.e., periodic boundary conditions are imposed and the grid sizes are equal), we consider the spectral representation (discrete backward Fourier transform) of \(p\):

\[\newcommand{ \wavy}{\exp \left( I \frac{2 \pi}{\ngp{2}} j m \right)} \newcommand{ \wavz}{\exp \left( I \frac{2 \pi}{\ngp{3}} k n \right)} \newcommand{\iwavy}{\exp \left( - I \frac{2 \pi}{\ngp{2}} j m^\prime \right)} \newcommand{\iwavz}{\exp \left( - I \frac{2 \pi}{\ngp{3}} k n^\prime \right)} \vat{p}{\ccidx{i}, \ccidx{j}, \ccidx{k}} = \sum_{n = 0}^{\ngp{3} - 1} \sum_{m = 0}^{\ngp{2} - 1} \vat{P}{\ccidx{i}, \ccidx{m}, \ccidx{n}} \wavy \wavz,\]

where \(I\) is the imaginary unit \(\sqrt{-1}\). Note that the discrete forward Fourier transform yields

\[ \begin{align}\begin{aligned}\sum_{k = 0}^{\ngp{3} - 1} \sum_{j = 0}^{\ngp{2} - 1} \vat{p}{\ccidx{i}, \ccidx{j}, \ccidx{k}} \iwavy \iwavz & = \sum_{k = 0}^{\ngp{3} - 1} \sum_{j = 0}^{\ngp{2} - 1} \sum_{n = 0}^{\ngp{3} - 1} \sum_{m = 0}^{\ngp{2} - 1} \vat{P}{\ccidx{i}, \ccidx{m}, \ccidx{n}} \wavy \wavz \iwavy \iwavz\\& = \sum_{k = 0}^{\ngp{3} - 1} \sum_{j = 0}^{\ngp{2} - 1} \sum_{n = 0}^{\ngp{3} - 1} \sum_{m = 0}^{\ngp{2} - 1} \vat{P}{\ccidx{i}, \ccidx{m}, \ccidx{n}} \delta_{m m^\prime} \delta_{n n^\prime}\\& = \ngp{2} \ngp{3} \sum_{n = 0}^{\ngp{3} - 1} \sum_{m = 0}^{\ngp{2} - 1} \vat{P}{\ccidx{i}, \ccidx{m}, \ccidx{n}} \delta_{m m^\prime} \delta_{n n^\prime}\\& = \ngp{2} \ngp{3} \vat{P}{\ccidx{i}, \ccidx{m^\prime}, \ccidx{n^\prime}}.\end{aligned}\end{align} \]

Since we have

\[ \begin{align}\begin{aligned}\vat{p}{\ccidx{i} \pm 1, \ccidx{j}, \ccidx{k}} & = \sum_{n = 0}^{\ngp{3} - 1} \sum_{m = 0}^{\ngp{2} - 1} \vat{P}{\ccidx{i} \pm 1, \ccidx{m}, \ccidx{n}} \wavy \wavz,\\\vat{p}{\ccidx{i}, \ccidx{j} \pm 1, \ccidx{k}} & = \sum_{n = 0}^{\ngp{3} - 1} \sum_{m = 0}^{\ngp{2} - 1} \vat{P}{\ccidx{i}, \ccidx{m}, \ccidx{n}} \exp \left( \pm I \frac{2 \pi}{\ngp{2}} m \right) \wavy \wavz,\\\vat{p}{\ccidx{i}, \ccidx{j}, \ccidx{k} \pm 1} & = \sum_{n = 0}^{\ngp{3} - 1} \sum_{m = 0}^{\ngp{2} - 1} \vat{P}{\ccidx{i}, \ccidx{m}, \ccidx{n}} \exp \left( \pm I \frac{2 \pi}{\ngp{3}} n \right) \wavy \wavz,\end{aligned}\end{align} \]

the second-order derivatives in the Poisson equation can be reformulated as

\[ \begin{align}\begin{aligned}\newcommand{\coefl}{C_l} \newcommand{\coefu}{C_u} \frac{1}{J} \dif{}{\gcs{1}} \left( \frac{J}{\sfact{1}} \frac{1}{\sfact{1}} \dif{p}{\gcs{1}} \right) & = \coefl \vat{p}{\ccidx{i} - 1, \ccidx{j}, \ccidx{k}} - \left( \coefl + \coefu \right) \vat{p}{\ccidx{i}, \ccidx{j}, \ccidx{k}} + \coefu \vat{p}{\ccidx{i} + 1, \ccidx{j}, \ccidx{k}}\\& = \coefl \sum_{n = 0}^{\ngp{3} - 1} \sum_{m = 0}^{\ngp{2} - 1} \vat{P}{\ccidx{i} - 1, \ccidx{m}, \ccidx{n}} \wavy \wavz - \left( \coefl + \coefu \right) \sum_{n = 0}^{\ngp{3} - 1} \sum_{m = 0}^{\ngp{2} - 1} \vat{P}{\ccidx{i}, \ccidx{m}, \ccidx{n}} \wavy \wavz + \coefu \sum_{n = 0}^{\ngp{3} - 1} \sum_{m = 0}^{\ngp{2} - 1} \vat{P}{\ccidx{i} + 1, \ccidx{m}, \ccidx{n}} \wavy \wavz,\\\frac{1}{J} \dif{}{\gcs{2}} \left( \frac{J}{\sfact{2}} \frac{1}{\sfact{2}} \dif{p}{\gcs{2}} \right) & = \frac{1}{\sfact{2}} \frac{1}{\sfact{2}} \left( \vat{p}{\ccidx{i}, \ccidx{j} - 1, \ccidx{k}} - 2 \vat{p}{\ccidx{i}, \ccidx{j} , \ccidx{k}} + \vat{p}{\ccidx{i}, \ccidx{j} + 1, \ccidx{k}} \right)\\& = \frac{1}{\sfact{2}} \frac{1}{\sfact{2}} \sum_{n = 0}^{\ngp{3} - 1} \sum_{m = 0}^{\ngp{2} - 1} \vat{P}{\ccidx{i}, \ccidx{m}, \ccidx{n}} \wavy \wavz \left\{ \exp \left( I \frac{2 \pi}{\ngp{2}} m \right) - 2 + \exp \left( - I \frac{2 \pi}{\ngp{2}} m \right) \right\}\\& = - \frac{1}{\sfact{2}} \frac{1}{\sfact{2}} \sum_{n = 0}^{\ngp{3} - 1} \sum_{m = 0}^{\ngp{2} - 1} 4 \vat{P}{\ccidx{i}, \ccidx{m}, \ccidx{n}} \wavy \wavz \sin^2 \left( \frac{\pi}{\ngp{2}} m \right),\\\frac{1}{J} \dif{}{\gcs{3}} \left( \frac{J}{\sfact{3}} \frac{1}{\sfact{3}} \dif{p}{\gcs{3}} \right) & = - \frac{1}{\sfact{3}} \frac{1}{\sfact{3}} \sum_{n = 0}^{\ngp{3} - 1} \sum_{m = 0}^{\ngp{2} - 1} 4 \vat{P}{\ccidx{i}, \ccidx{m}, \ccidx{n}} \wavy \wavz \sin^2 \left( \frac{\pi}{\ngp{3}} n \right),\end{aligned}\end{align} \]

where

\[ \begin{align}\begin{aligned}\coefl & \equiv \frac{ 1 }{ \vat{J}{\ccidx{i}} } \frac{ \vat{J}{\cmidx{i}} }{ \vat{\sfact{1}}{\cmidx{i}} } \frac{ 1 }{ \vat{\sfact{1}}{\cmidx{i}} },\\\coefu & \equiv \frac{ 1 }{ \vat{J}{\ccidx{i}} } \frac{ \vat{J}{\cpidx{i}} }{ \vat{\sfact{1}}{\cpidx{i}} } \frac{ 1 }{ \vat{\sfact{1}}{\cpidx{i}} },\end{aligned}\end{align} \]

which are computed here:

src/fluid/compute_potential.c
233double * tdm_l = NULL;
234double * tdm_u = NULL;
235tdm.get_l(*tdm_info, &tdm_l);
236tdm.get_u(*tdm_info, &tdm_u);
237const double * hxxf = domain->hxxf;
238const double * jdxf = domain->jdxf;
239const double * jdxc = domain->jdxc;
240for (int i = 1; i <= (int)tdm_sizes[0]; i++) {
241  // N.B. iterated from i = 1 to use macros
242  tdm_l[i-1] = 1. / JDXC(i  ) * JDXF(i  ) / HXXF(i  ) / HXXF(i  );
243  tdm_u[i-1] = 1. / JDXC(i  ) * JDXF(i+1) / HXXF(i+1) / HXXF(i+1);
244}

Consequently, we obtain

\[ \begin{align}\begin{aligned}& \sum_{n = 0}^{\ngp{3} - 1} \sum_{m = 0}^{\ngp{2} - 1} \coefl \vat{P}{\ccidx{i} - 1, \ccidx{m}, \ccidx{n}} \wavy \wavz\\+ & \sum_{n = 0}^{\ngp{3} - 1} \sum_{m = 0}^{\ngp{2} - 1} \left\{ - \left( \coefl + \coefu \right) - 4 \frac{1}{\sfact{2}} \frac{1}{\sfact{2}} \sin^2 \left( \frac{\pi}{\ngp{2}} m \right) - 4 \frac{1}{\sfact{3}} \frac{1}{\sfact{3}} \sin^2 \left( \frac{\pi}{\ngp{3}} n \right) \right\} \vat{P}{\ccidx{i}, \ccidx{m}, \ccidx{n}} \wavy \wavz\\+ & \sum_{n = 0}^{\ngp{3} - 1} \sum_{m = 0}^{\ngp{2} - 1} \coefu \vat{P}{\ccidx{i} + 1, \ccidx{m}, \ccidx{n}} \wavy \wavz\\= & \vat{q}{\ccidx{i}, \ccidx{j}, \ccidx{k}}.\end{aligned}\end{align} \]

By applying the discrete forward Fourier transform, we obtain

\[ \begin{align}\begin{aligned}& \coefl \vat{P}{\ccidx{i} - 1, \ccidx{m}, \ccidx{n}}\\+ & \left\{ - \left( \coefl + \coefu \right) - 4 \frac{1}{\sfact{2}} \frac{1}{\sfact{2}} \sin^2 \left( \frac{\pi}{\ngp{2}} m \right) - 4 \frac{1}{\sfact{3}} \frac{1}{\sfact{3}} \sin^2 \left( \frac{\pi}{\ngp{3}} n \right) \right\} \vat{P}{\ccidx{i}, \ccidx{m}, \ccidx{n}}\\+ & \coefu \vat{P}{\ccidx{i} + 1, \ccidx{m}, \ccidx{n}}\\= & \frac{1}{\ngp{2}} \frac{1}{\ngp{3}} \sum_{k = 0}^{\ngp{3} - 1} \sum_{j = 0}^{\ngp{2} - 1} \vat{q}{\ccidx{i}, \ccidx{j}, \ccidx{k}} \exp \left( - I \frac{2 \pi}{\ngp{2}} j m \right) \exp \left( - I \frac{2 \pi}{\ngp{3}} k n \right).\end{aligned}\end{align} \]

The resulting tri-diagonal matrix for each \(m\) and \(n\) is solved here:

src/fluid/compute_potential.c
546#if NDIMS == 2
547  for (size_t j = 0; j < jsize; j++) {
548    const double eval_y = eval_ys[j];
549    // set center diagonal components
550    for (size_t i = 0; i < isize; i++) {
551      tdm_c[i] =
552        - tdm_l[i]
553        - tdm_u[i]
554        + eval_y / hy / hy;
555    }
556    // boundary treatment (Neumann boundary condition)
557    tdm_c[        0] += tdm_l[        0];
558    tdm_c[isize - 1] += tdm_u[isize - 1];
559    // solve
560    tdm.solve(tdm_info, rhs + j * isize);
561  }
562#else
563  for (size_t k = 0; k < ksize; k++) {
564    const double eval_z = eval_zs[k];
565    for (size_t j = 0; j < jsize; j++) {
566      const double eval_y = eval_ys[j];
567      // set center diagonal components
568      for (size_t i = 0; i < isize; i++) {
569        tdm_c[i] =
570          - tdm_l[i]
571          - tdm_u[i]
572          + eval_y / hy / hy
573          + eval_z / hz / hz;
574      }
575      // boundary treatment (Neumann boundary condition)
576      tdm_c[        0] += tdm_l[        0];
577      tdm_c[isize - 1] += tdm_u[isize - 1];
578      // solve
579      tdm.solve(tdm_info, rhs + (k * jsize + j) * isize);
580    }
581  }
582#endif

The sinusoidal functions (wave numbers) are computed here:

src/fluid/compute_potential.c
377double * restrict * evals = &poisson_solver->eval_ys;
378size_t mysize = 0;
379size_t myoffs = 0;
380sdecomp.get_pencil_mysize(info, pencil, SDECOMP_YDIR, c_gl_sizes[SDECOMP_YDIR], &mysize);
381sdecomp.get_pencil_offset(info, pencil, SDECOMP_YDIR, c_gl_sizes[SDECOMP_YDIR], &myoffs);
382*evals = memory_calloc(mysize, sizeof(double));
383for (size_t local_n = 0; local_n < mysize; local_n++) {
384  const size_t global_n = local_n + myoffs;
385  (*evals)[local_n] =
386    - 4. * pow(
387      sin( g_pi * global_n / r_gl_sizes[SDECOMP_YDIR] ),
388      2.
389    );
390}
src/fluid/compute_potential.c
395double * restrict * evals = &poisson_solver->eval_zs;
396size_t mysize = 0;
397size_t myoffs = 0;
398sdecomp.get_pencil_mysize(info, pencil, SDECOMP_ZDIR, c_gl_sizes[SDECOMP_ZDIR], &mysize);
399sdecomp.get_pencil_offset(info, pencil, SDECOMP_ZDIR, c_gl_sizes[SDECOMP_ZDIR], &myoffs);
400*evals = memory_calloc(mysize, sizeof(double));
401for (size_t local_n = 0; local_n < mysize; local_n++) {
402  const size_t global_n = local_n + myoffs;
403  (*evals)[local_n] =
404    - 4. * pow(
405      sin( g_pi * global_n / r_gl_sizes[SDECOMP_ZDIR] ),
406      2.
407    );
408}

The overall procedure can be found here:

src/fluid/compute_potential.c
611int fluid_compute_potential (
612    const domain_t * domain,
613    const size_t rkstep,
614    const double dt,
615    fluid_t * fluid
616) {
617  static poisson_solver_t poisson_solver = {
618    .is_initialised = false,
619  };
620  // initialise Poisson solver
621  if (!poisson_solver.is_initialised) {
622    if (0 != init_poisson_solver(domain, &poisson_solver)) {
623      // failed to initialise Poisson solver
624      return 1;
625    }
626  }
627  // compute right-hand side of Poisson equation
628  // assigned to buf0
629  assign_input(domain, rkstep, dt, fluid, poisson_solver.buf0);
630  // transpose real x1pencil to y1pencil
631  // from buf0 to buf1
632  sdecomp.transpose.execute(
633      poisson_solver.r_transposer_x1_to_y1,
634      poisson_solver.buf0,
635      poisson_solver.buf1
636  );
637  // project y to wave space
638  // f(x, y)    -> f(x, k_y)
639  // f(x, y, z) -> f(x, k_y, z)
640  // from buf1 to buf0
641  fftw_execute(poisson_solver.fftw_plan_y[0]);
642#if NDIMS == 2
643  // transpose complex y1pencil to x1pencil
644  // from buf0 to buf1
645  sdecomp.transpose.execute(
646      poisson_solver.c_transposer_y1_to_x1,
647      poisson_solver.buf0,
648      poisson_solver.buf1
649  );
650#else
651  // transpose complex y1pencil to z1pencil
652  // from buf0 to buf1
653  sdecomp.transpose.execute(
654      poisson_solver.c_transposer_y1_to_z1,
655      poisson_solver.buf0,
656      poisson_solver.buf1
657  );
658  // project z to wave space
659  // f(x, k_y, z) -> f(x, k_y, k_z)
660  // from buf1 to buf0
661  fftw_execute(poisson_solver.fftw_plan_z[0]);
662  // transpose complex z1pencil to x2pencil
663  // from buf0 to buf1
664  sdecomp.transpose.execute(
665      poisson_solver.c_transposer_z1_to_x2,
666      poisson_solver.buf0,
667      poisson_solver.buf1
668  );
669#endif
670  // solve linear systems in x direction
671  solve_linear_systems(domain, &poisson_solver);
672#if NDIMS == 2
673  // transpose complex x1pencil to y1pencil
674  // from buf1 to buf0
675  sdecomp.transpose.execute(
676      poisson_solver.c_transposer_x1_to_y1,
677      poisson_solver.buf1,
678      poisson_solver.buf0
679  );
680#else
681  // transpose complex x2pencil to z1pencil
682  // from buf1 to buf0
683  sdecomp.transpose.execute(
684      poisson_solver.c_transposer_x2_to_z1,
685      poisson_solver.buf1,
686      poisson_solver.buf0
687  );
688  // project z to physical space
689  // f(x, k_y, k_z) -> f(x, k_y, z)
690  // from buf0 to buf1
691  fftw_execute(poisson_solver.fftw_plan_z[1]);
692  // transpose complex z1pencil to y1pencil
693  // from buf1 to buf0
694  sdecomp.transpose.execute(
695      poisson_solver.c_transposer_z1_to_y1,
696      poisson_solver.buf1,
697      poisson_solver.buf0
698  );
699#endif
700  // project y to physical space
701  // f(x, k_y)    -> f(x, y)
702  // f(x, k_y, z) -> f(x, y, z)
703  // from buf0 to buf1
704  fftw_execute(poisson_solver.fftw_plan_y[1]);
705  // transpose real y1pencil to x1pencil
706  // from buf1 to buf0
707  sdecomp.transpose.execute(
708      poisson_solver.r_transposer_y1_to_x1,
709      poisson_solver.buf1,
710      poisson_solver.buf0
711  );
712  // copy result to the original buffer
713  extract_output(domain, poisson_solver.buf0, fluid);
714  return 0;
715}