C++ Solver for Stiff Nonlinear System

C++ Solver for Stiff Nonlinear System#

Here’s an implementation of a first-order backward-Euler solver for the same nonlinear stiff system.

This uses a two-dimensional multidimensional array class, Array defined in the header array.H. This allows us to do things like:

Array A{{{1, 1, 1},
         {-1, 2, 0},
         {2, 0, 1}}};

to initialize a 2-d array. We can then get the number of rows and columns via A.nrows() and A.ncols(). We can also index the array using A(i,j) to get the i-th row and j-th column.

Here’s the class header array.H:

#ifndef ARRAY_H
#define ARRAY_H

#include <vector>
#include <iostream>
#include <cassert>

// a contiguous 2-d array
// here the data is stored in row-major order in a 1-d memory space
// managed as a vector.  We overload () to allow us to index this as
// a(irow, icol)

struct Array {

    int _rows;
    int _cols;
    std::vector<double> _data;

    Array (int rows, int cols, double val=0.0)
        : _rows{rows},
          _cols{cols},
          _data(rows * cols, val)
    {
        // we do the asserts here after the initialization of _data
        // in the initialization list, but if the size is zero, we'll
        // abort here anyway.

        assert (rows > 0);
        assert (cols > 0);

    }

    // constructor that allows us to initialize array
    // via an initializer list of rows

    Array (std::vector<std::vector<double>> v)
        : _rows{static_cast<int>(v.size())},
          _cols{static_cast<int>(v[0].size())},
          _data(_rows * _cols, 0.0)
    {
        int idx = 0;
        for (int i = 0; i < _rows; ++i) {
            assert (static_cast<int>(v[i].size()) == _cols);
            for (int j = 0; j < _cols; ++j) {
                _data[idx] = v[i][j];
                ++idx;
            }
        }
    }

    // note the "const" after the argument list here -- this means
    // that this can be called on a const Array

    inline int ncols() const { return _cols;}
    inline int nrows() const { return _rows;}

    inline double& operator()(int row, int col) {
        assert (row >= 0 && row < _rows);
        assert (col >= 0 && col < _cols);
        return _data[row*_cols + col];
    }

    inline const double& operator()(int row, int col) const {
        assert (row >= 0 && row < _rows);
        assert (col >= 0 && col < _cols);
        return _data[row*_cols + col];
    }

};

// the << operator is not part of the of the class, so it is not a member

std::ostream& operator<< (std::ostream& os, const Array& a) {

    for (int row = 0; row < a.nrows(); ++row) {
        for (int col = 0; col < a.ncols(); ++col) {
            os << a(row, col) << " ";
        }
        os << std::endl;
    }

    return os;
}

#endif

The Gaussian elimination function is in the header gauss.H:

#ifndef GAUSS_H
#define GAUSS_H

#include <array.H>
#include <vector>
#include <limits>
#include <iostream>
#include <iomanip>


inline
std::vector<double> gauss_elim(Array& A, std::vector<double>& b) {

    // perform gaussian elimination with pivoting, solving A x = b.
    //
    //  A is an NxN matrix, x and b are an N-element vectors.  Note: A and b are
    //  changed upon exit to be in upper triangular (row echelon) form

    const int N = b.size();

    // A is square, with each dimension of length N

    assert(A.nrows() == N && A.ncols() == N);

    // allocate the solution array

    std::vector<double> x(N, 0.0);

    // find the scale factors for each row -- this is used when pivoting

    std::vector<double> scales(N, 0.0);

    for (int irow = 0; irow < N; ++irow) {
        double maxval = std::numeric_limits<double>::lowest();
        for (int jcol = 0; jcol < N; ++jcol) {
            maxval = std::max(maxval, std::abs(A(irow, jcol)));
        }
        scales[irow] = maxval;
    }

    // keep track of the number of times we swapped rows

    int num_row_swap{};

    // main loop over rows

    for (int krow = 0; krow < N; ++krow) {

        // find the pivot row based on the size of column k -- only consider the
        // rows beyond the current row

        int row_max{krow};
        double col_max = std::numeric_limits<double>::lowest();
        for (int kk = krow; kk < N; ++kk) {
            double scaled_col_val = std::abs(A(kk, krow)) / scales[kk];
            if (scaled_col_val > col_max) {
                col_max = scaled_col_val;
                row_max = kk;
            }
        }

        // swap the row with the largest scaled element in the current column
        // with the current row (pivot) -- do this with b too!

        if (row_max != krow) {

            // swap row krow with row row_max

            for (int jcol = 0; jcol < N; ++jcol) {
                double tmp_val = A(krow, jcol);
                A(krow, jcol) = A(row_max, jcol);
                A(row_max, jcol) = tmp_val;
            }

            // now swap the same elements in b

            double tmp_b = b[krow];
            b[krow] = b[row_max];
            b[row_max] = tmp_b;

            // finally swap the scales

            double tmp_scale = scales[krow];
            scales[krow] = scales[row_max];
            scales[row_max] = tmp_scale;

            ++num_row_swap;
        }

        // do the forward-elimination for all rows below the current

        for (int irow = krow+1; irow < N; ++irow) {
            double coeff = A(irow, krow) / A(krow, krow);

            for (int jcol = krow+1; jcol < N; ++jcol) {
                A(irow, jcol) += -A(krow, jcol) * coeff;
            }

            A(irow, krow) = 0.0;
            b[irow] += -coeff * b[krow];
        }

    }

    // back-substitution

    // last solution is easy

    x[N-1] = b[N-1] / A(N-1, N-1);

    for (int irow = N-2; irow >= 0; --irow) {
        double bsum = b[irow];
        for (int jcol = irow+1; jcol < N; ++jcol) {
            bsum += -A(irow, jcol) * x[jcol];
        }
        x[irow] = bsum / A(irow, irow);
    }

    return x;
}

#endif

and the full solver is in stiff_system.cpp:

#include <cmath>
#include <iostream>
#include <limits>

#include <array.H>
#include <gauss.H>

///
/// given the state Y, return the RHS of our ODE system
///
std::vector<double> rhs(const std::vector<double>& Y) {

    std::vector<double> dYdt(3, 0.0);

    dYdt[0] = -0.04 * Y[0] + 1.e4 * Y[1] * Y[2];
    dYdt[1] =  0.04 * Y[0] - 1.e4 * Y[1] * Y[2] - 3.e7 * Y[1] * Y[1];
    dYdt[2] =                                     3.e7 * Y[1] * Y[1];

    return dYdt;
}

///
/// given the state Y, return the Jacobian of the ODE system
///
Array jacobian(const std::vector<double>& Y) {

    Array jac(3, 3);

    jac(0, 0) = -0.04;
    jac(0, 1) = 1.e4 * Y[2];
    jac(0, 2) = 1.e4 * Y[1];

    jac(1, 0) = 0.04;
    jac(1, 1) = -1.e4 * Y[2] - 6.e7 * Y[1];
    jac(1, 2) = -1.e4 * Y[1];

    jac(2, 0) = 0.0;
    jac(2, 1) = 6.e7 * Y[1];
    jac(2, 2) = 0.0;

    return jac;
}


std::vector<double>
backward_euler(const double t_start, const double t_end, const double dt_init,
               const std::vector<double> y_init) {

    const double tol{1.e-6};
    const int max_iter{100};

    int neq = static_cast<int>(y_init.size());

    double time{t_start};
    double dt{dt_init};

    // starting point of the integration for each step
    std::vector<double> y_n{y_init};
    std::vector<double> y_new{0.0};

    while (time < t_end) {

        for (int n = 0; n < static_cast<int>(y_new.size()); ++n) {
            y_new[n] = y_n[n];
        }

        bool converged{false};

        int niter{0};
        while (!converged && niter < max_iter) {

            // construct the matrix A = I - dt J
            Array A = jacobian(y_n);

            for (int irow = 0; irow < neq; ++irow) {
                for (int jcol = 0; jcol < neq; ++jcol) {
                    A(irow, jcol) *= -dt;
                    if (irow == jcol) {
                        A(irow, jcol) += 1.0;
                    }
                }
            }

            // construct the HS
            std::vector<double> b = rhs(y_new);
            for (int irow = 0; irow < neq; ++irow) {
                b[irow] = y_n[irow] - y_new[irow] + dt * b[irow];
            }

            // solve the linear system A dy = b
            std::vector<double> dy = gauss_elim(A, b);

            // correct our guess
            for (int irow = 0; irow < neq; ++irow) {
                y_new[irow] += dy[irow];
            }

            // check for convergence by computing the norms
            double dy_norm{0.0};
            double y_new_norm(0.0);
            for (int irow = 0; irow < neq; ++irow) {
                dy_norm += dy[irow] * dy[irow];
                y_new_norm += y_new[irow] * y_new[irow];
            }
            double error = std::sqrt(dy_norm / y_new_norm);
            if (error < tol) {
                converged = true;
            }

            niter++;
        }

        if (!converged) {
            std::cout << "convergence failure" << std::endl;
            std::exit(EXIT_FAILURE);
        }

        if (time + dt > t_end) {
                dt = t_end - time;
        }

        // reset the solution for the next step
        for (int irow = 0; irow < neq; ++irow) {
            y_n[irow] = y_new[irow];
        }

        time += dt;
    }

    return y_n;
}

int main() {

    // setup the initial conditions
    std::vector<double> y_init{1.0, 0.0, 0.0};

    // we are going to do a bunch of short integrations to different
    // end points -- this holds the end times for each interval
    std::vector<double> t_ends{-6.0, -5.0, -4.0, -3.0, -2.0, -1.0, 0.0,
                               1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0};

    double time{0.0};
    std::vector<double> y_old{y_init};

    for (auto log_t_max: t_ends) {

        double tmax = std::pow(10.0, log_t_max);

        auto y_new = backward_euler(time, tmax, tmax/10.0, y_old);
        time = tmax;

        std::cout << tmax << " " << y_new[0] << " " << y_new[1] << " " << y_new[2] << std::endl;

        for (int irow = 0; irow < 3; ++irow) {
            y_old[irow] = y_new[irow];
        }
    }

}

This can be compiled as:

g++ -std=c++17 -I. -o stiff_system stiff_system.cpp