Conjugate Gradient Solution to Poisson Equation

Routine Name: solveLaplace

Author: Kyle Hovey

Language: C++

Description/Purpose:

This is the exact same code from the Poisson solver in last assignment, but instead of using LU factorization to find the solution, we use the conjugate gradient method

Input:

Same input (driving function, boundary conditions, etc…) as the Poisson solver from last assignment.

Output:

The solution to the specified Poisson equation as a 2D matrix. This also outputs an image file.

Usage/Example:

#include "../../matrix/src/matrix/matrix.h"
#include "../../image/src/image/image.h"
#include <functional>
#include <vector>
#include <iostream>
#include <limits>
#include <cmath>

using Mtx = Matrix::Matrix<double>;

template <typename T>
using planeToScalar = std::function<T(T, T)>;

template <typename T>
using coord = std::tuple<T, T>;

template <typename T>
using stencil = std::vector<std::tuple<T, coord<T>>>;

template <typename T>
using stencilGen = std::function<stencil<T>(
    const std::tuple<T, T>&,
    const T& h
)>;

int main() {
  // Define domain
  const coord<double> domain = { 0, 1 };

  // Define driving function
  const planeToScalar<double> driver =
    [](const double& x, const double& y) {
      return std::sin(x * y);
    };

  // G(x, y) along the boundaries
  const planeToScalar<double> boundary =
    [](const double& x, const double& y) -> double {
      (void) x;
      (void) y;

      return 0.0;
    };

  // Define size of mesh
  const uint size = 5;

  // Stencil generation
  const stencilGen<double> fivePoint = [](
      const coord<double>& center,
      const double& h
  ) {
    const auto [ x, y ] = center;
    const auto mult = 1.0 / (double) (h * h);

    return stencil<double>({
      { mult * -4,{ x + 0, y + 0 } },
      { mult * 1, { x + h, y + 0 } },
      { mult * 1, { x - h, y + 0 } },
      { mult * 1, { x + 0, y + h } },
      { mult * 1, { x + 0, y - h } }
    });
  };

  auto soln = solveLaplace<double>(size, domain, driver, fivePoint, boundary);

  std::cout << "Solution with 5-point stencil:" << std::endl;
  std::cout << soln << std::endl;

  // Output to file for funsies
  Image::ImageWriter::matrixHeatmap("soln.ppm", soln);

  return EXIT_SUCCESS;
}

Output:

-0.0970489 -0.162868 -0.1537
-0.162868 -0.276049 -0.265528
-0.1537 -0.265528 -0.266089

Implementation/Code:

/**
 * Solve ∇²u(x, y) = f(x, y)
 * @param size Mesh size
 * @param domain Lower-left and upper-right coordinates of domain
 * @param driver Driving function (f(x, y) on the r.h.s.)
 * @param stencil Function that takes tuple of evaluation point coordinates
 *  and returns a vector of tuples for the resulting stencil that also
 *  includes multipliers. Offsets should be integer multiples of h. Example:
 *    { (-4, (2, 2)), (1, (2, 3)), (1, (2, 1)) }
 * @param dirichlet Function u(x, y) along boundary
 * @return Matrix of values that resemble a solution over u's domain
 */
template <typename T>
Matrix::Matrix<T> solveLaplace(
    const uint& size,
    const coord<T>& domain,
    const planeToScalar<T>& driver,
    const stencilGen<T>& makeStencil,
    const planeToScalar<T>& dirichlet
) {
  // Find limits of domain
  const auto a = std::get<0>(domain);
  const auto b = std::get<1>(domain);
  const auto h = (b - a) / (size - 1);

  // Internal limits of sampling
  const auto _a = a + h;
  const auto _b = a + b - h;
  const auto _intSize = size - 2;

  // Instantiate output grid (interior of mesh)
  Matrix::Matrix<T> rhs(
      _intSize,
      _intSize,
      [&](const uint& row, const uint& col) {
        // Determine location in domain
        const auto x = a + h * (col + 1);
        const auto y = a + h * (row + 1);

        // Initialize an accumulator
        T acc = 0;

        // Add on driving function
        acc += driver(x, y);

        // Determine stencil
        const auto samples = makeStencil({ x, y }, h);

        for (const auto& sample : samples) {
          const auto [ mult, coords ] = sample;
          const auto [ _x, _y ] = coords;

          // If outside of interior
          if (_x < _a || _x > _b || _y < _a || _y > _b) {
            // Subtract off the boundary (since we are truncating it)
            acc -= dirichlet(_x, _y);
          }
        }

        return acc;
      }
  );

  // Turn rhs into a column vector
  rhs = rhs.flatten();

  // Create the laplacian operator
  Matrix::Matrix<T> lap(_intSize * _intSize, _intSize * _intSize);

  // For each of the laplacian entries in lexigraphical order
  for (uint col = 0; col < _intSize; ++col) {
    for (uint row = 0; row < _intSize; ++row) {
      // Create a temporary matrix to embed stencil in
      Matrix::Matrix<T> stencilOp(_intSize, _intSize);

      // Determine stencil w.r.t. index, not domain
      const auto samples = makeStencil({ col, row }, 1);

      for (const auto& sample : samples) {
        const auto [ mult, coords ] = sample;
        const auto [ _col, _row ] = coords;

        // If inside of interior
        if (_col >= 0 && _col < _intSize && _row >= 0 && _row < _intSize) {
          // Embed point inside of matrix
          stencilOp.setVal(_col, _row, mult);
        }
      }

      // Flatten into a column vector
      stencilOp = stencilOp.flatten();

      // Add values to the correct row in laplace operator
      for (uint j = 0; j < _intSize * _intSize; ++j) {
        lap.setVal(row + _intSize * col, j, stencilOp.getVal(j, 0));
      }
    }
  }

  // Solve system for solution
  auto u = Matrix::Matrix<T>::solve(lap, rhs, Matrix::Solve::ConjugateGradient);

  // Reform matrix
  u = u.squareUp(_intSize, _intSize);

  return u;
}