
//
// WARNING! This file is automatically generated from the sources in the src directory.
// Do not modify this source code directly as any changes will be overwritten
//


// original file: ../src/helpme.h

// BEGINLICENSE
//
// This file is part of helPME, which is distributed under the BSD 3-clause license,
// as described in the LICENSE file in the top level directory of this project.
//
// Author: Andrew C. Simmonett
//
// ENDLICENSE
#ifndef _HELPME_STANDALONE_HELPME_H_
#define _HELPME_STANDALONE_HELPME_H_

#if __cplusplus || DOXYGEN

// C++ header

#include <algorithm>
#include <array>
#include <cmath>
#include <complex>
#include <functional>
#include <iostream>
#include <list>
#include <memory>
#ifdef _OPENMP
#include <omp.h>
#endif
#include <set>
#include <stdexcept>
#include <string>
#include <tuple>
#include <unistd.h>
#include <vector>

// original file: ../src/cartesiantransform.h

// BEGINLICENSE
//
// This file is part of helPME, which is distributed under the BSD 3-clause license,
// as described in the LICENSE file in the top level directory of this project.
//
// Author: Andrew C. Simmonett
//
// ENDLICENSE
#ifndef _HELPME_STANDALONE_CARTESIANTRANSFORM_H_
#define _HELPME_STANDALONE_CARTESIANTRANSFORM_H_

// original file: ../src/matrix.h

// BEGINLICENSE
//
// This file is part of helPME, which is distributed under the BSD 3-clause license,
// as described in the LICENSE file in the top level directory of this project.
//
// Author: Andrew C. Simmonett
//
// ENDLICENSE
#ifndef _HELPME_STANDALONE_MATRIX_H_
#define _HELPME_STANDALONE_MATRIX_H_

#include <functional>
#include <algorithm>
#include <complex>
#include <fstream>
#include <functional>
#include <initializer_list>
#include <iostream>
#include <iomanip>
#include <numeric>
#include <stdexcept>
#include <tuple>

// original file: ../src/lapack_wrapper.h

// BEGINLICENSE
//
// This file is part of helPME, which is distributed under the BSD 3-clause license,
// as described in the LICENSE file in the top level directory of this project.
//
// Author: Andrew C. Simmonett
//
// ENDLICENSE
//
// The code for Jacobi diagonalization is taken (with minimal modification) from
//
// http://www.mymathlib.com/c_source/matrices/eigen/jacobi_cyclic_method.c
//
#ifndef _HELPME_STANDALONE_LAPACK_WRAPPER_H_
#define _HELPME_STANDALONE_LAPACK_WRAPPER_H_

#include <cmath>
#include <limits>

namespace helpme {
////////////////////////////////////////////////////////////////////////////////
//  void Jacobi_Cyclic_Method                                                 //
//            (Real eigenvalues[], Real *eigenvectors, Real *A, int n)  //
//                                                                            //
//  Description:                                                              //
//     Find the eigenvalues and eigenvectors of a symmetric n x n matrix A    //
//     using the Jacobi method. Upon return, the input matrix A will have     //
//     been modified.                                                         //
//     The Jacobi procedure for finding the eigenvalues and eigenvectors of a //
//     symmetric matrix A is based on finding a similarity transformation     //
//     which diagonalizes A.  The similarity transformation is given by a     //
//     product of a sequence of orthogonal (rotation) matrices each of which  //
//     annihilates an off-diagonal element and its transpose.  The rotation   //
//     effects only the rows and columns containing the off-diagonal element  //
//     and its transpose, i.e. if a[i][j] is an off-diagonal element, then    //
//     the orthogonal transformation rotates rows a[i][] and a[j][], and      //
//     equivalently it rotates columns a[][i] and a[][j], so that a[i][j] = 0 //
//     and a[j][i] = 0.                                                       //
//     The cyclic Jacobi method considers the off-diagonal elements in the    //
//     following order: (0,1),(0,2),...,(0,n-1),(1,2),...,(n-2,n-1).  If the  //
//     the magnitude of the off-diagonal element is greater than a treshold,  //
//     then a rotation is performed to annihilate that off-diagnonal element. //
//     The process described above is called a sweep.  After a sweep has been //
//     completed, the threshold is lowered and another sweep is performed     //
//     with the new threshold. This process is completed until the final      //
//     sweep is performed with the final threshold.                           //
//     The orthogonal transformation which annihilates the matrix element     //
//     a[k][m], k != m, is Q = q[i][j], where q[i][j] = 0 if i != j, i,j != k //
//     i,j != m and q[i][j] = 1 if i = j, i,j != k, i,j != m, q[k][k] =       //
//     q[m][m] = cos(phi), q[k][m] = -sin(phi), and q[m][k] = sin(phi), where //
//     the angle phi is determined by requiring a[k][m] -> 0.  This condition //
//     on the angle phi is equivalent to                                      //
//               cot(2 phi) = 0.5 * (a[k][k] - a[m][m]) / a[k][m]             //
//     Since tan(2 phi) = 2 tan(phi) / (1 - tan(phi)^2),                      //
//               tan(phi)^2 + 2cot(2 phi) * tan(phi) - 1 = 0.                 //
//     Solving for tan(phi), choosing the solution with smallest magnitude,   //
//       tan(phi) = - cot(2 phi) + sgn(cot(2 phi)) sqrt(cot(2phi)^2 + 1).     //
//     Then cos(phi)^2 = 1 / (1 + tan(phi)^2) and sin(phi)^2 = 1 - cos(phi)^2 //
//     Finally by taking the sqrts and assigning the sign to the sin the same //
//     as that of the tan, the orthogonal transformation Q is determined.     //
//     Let A" be the matrix obtained from the matrix A by applying the        //
//     similarity transformation Q, since Q is orthogonal, A" = Q'AQ, where Q'//
//     is the transpose of Q (which is the same as the inverse of Q).  Then   //
//         a"[i][j] = Q'[i][p] a[p][q] Q[q][j] = Q[p][i] a[p][q] Q[q][j],     //
//     where repeated indices are summed over.                                //
//     If i is not equal to either k or m, then Q[i][j] is the Kronecker      //
//     delta.   So if both i and j are not equal to either k or m,            //
//                                a"[i][j] = a[i][j].                         //
//     If i = k, j = k,                                                       //
//        a"[k][k] =                                                          //
//           a[k][k]*cos(phi)^2 + a[k][m]*sin(2 phi) + a[m][m]*sin(phi)^2     //
//     If i = k, j = m,                                                       //
//        a"[k][m] = a"[m][k] = 0 =                                           //
//           a[k][m]*cos(2 phi) + 0.5 * (a[m][m] - a[k][k])*sin(2 phi)        //
//     If i = k, j != k or m,                                                 //
//        a"[k][j] = a"[j][k] = a[k][j] * cos(phi) + a[m][j] * sin(phi)       //
//     If i = m, j = k, a"[m][k] = 0                                          //
//     If i = m, j = m,                                                       //
//        a"[m][m] =                                                          //
//           a[m][m]*cos(phi)^2 - a[k][m]*sin(2 phi) + a[k][k]*sin(phi)^2     //
//     If i= m, j != k or m,                                                  //
//        a"[m][j] = a"[j][m] = a[m][j] * cos(phi) - a[k][j] * sin(phi)       //
//                                                                            //
//     If X is the matrix of normalized eigenvectors stored so that the ith   //
//     column corresponds to the ith eigenvalue, then AX = X Lamda, where     //
//     Lambda is the diagonal matrix with the ith eigenvalue stored at        //
//     Lambda[i][i], i.e. X'AX = Lambda and X is orthogonal, the eigenvectors //
//     are normalized and orthogonal.  So, X = Q1 Q2 ... Qs, where Qi is      //
//     the ith orthogonal matrix,  i.e. X can be recursively approximated by  //
//     the recursion relation X" = X Q, where Q is the orthogonal matrix and  //
//     the initial estimate for X is the identity matrix.                     //
//     If j = k, then x"[i][k] = x[i][k] * cos(phi) + x[i][m] * sin(phi),     //
//     if j = m, then x"[i][m] = x[i][m] * cos(phi) - x[i][k] * sin(phi), and //
//     if j != k and j != m, then x"[i][j] = x[i][j].                         //
//                                                                            //
//  Arguments:                                                                //
//     Real  eigenvalues                                                      //
//        Array of dimension n, which upon return contains the eigenvalues of //
//        the matrix A.                                                       //
//     Real* eigenvectors                                                     //
//        Matrix of eigenvectors, the ith column of which contains an         //
//        eigenvector corresponding to the ith eigenvalue in the array        //
//        eigenvalues.                                                        //
//     Real* A                                                                //
//        Pointer to the first element of the symmetric n x n matrix A. The   //
//        input matrix A is modified during the process.                      //
//     int     n                                                              //
//        The dimension of the array eigenvalues, number of columns and rows  //
//        of the matrices eigenvectors and A.                                 //
//                                                                            //
//  Return Values:                                                            //
//     Function is of type void.                                              //
//                                                                            //
//  Example:                                                                  //
//     #define N                                                              //
//     Real A[N][N], Real eigenvalues[N], Real eigenvectors[N][N]             //
//                                                                            //
//     (your code to initialize the matrix A )                                //
//                                                                            //
//     JacobiCyclicDiagonalization(eigenvalues, (Real*)eigenvectors,          //
//                                                          (Real *) A, N);   //
////////////////////////////////////////////////////////////////////////////////

template <typename Real>
void JacobiCyclicDiagonalization(Real *eigenvalues, Real *eigenvectors, const Real *A, int n) {
    int i, j, k, m;
    Real *pAk, *pAm, *p_r, *p_e;
    Real threshold_norm;
    Real threshold;
    Real tan_phi, sin_phi, cos_phi, tan2_phi, sin2_phi, cos2_phi;
    Real sin_2phi, cos_2phi, cot_2phi;
    Real dum1;
    Real dum2;
    Real dum3;
    Real max;

    // Take care of trivial cases

    if (n < 1) return;
    if (n == 1) {
        eigenvalues[0] = *A;
        *eigenvectors = 1;
        return;
    }

    // Initialize the eigenvalues to the identity matrix.

    for (p_e = eigenvectors, i = 0; i < n; i++)
        for (j = 0; j < n; p_e++, j++)
            if (i == j)
                *p_e = 1;
            else
                *p_e = 0;

    // Calculate the threshold and threshold_norm.

    for (threshold = 0, pAk = const_cast<Real *>(A), i = 0; i < (n - 1); pAk += n, i++)
        for (j = i + 1; j < n; j++) threshold += *(pAk + j) * *(pAk + j);
    threshold = sqrt(threshold + threshold);
    threshold_norm = threshold * std::numeric_limits<Real>::epsilon();
    max = threshold + 1;
    while (threshold > threshold_norm) {
        threshold /= 10;
        if (max < threshold) continue;
        max = 0;
        for (pAk = const_cast<Real *>(A), k = 0; k < (n - 1); pAk += n, k++) {
            for (pAm = pAk + n, m = k + 1; m < n; pAm += n, m++) {
                if (std::abs(*(pAk + m)) < threshold) continue;

                // Calculate the sin and cos of the rotation angle which
                // annihilates A[k][m].

                cot_2phi = 0.5f * (*(pAk + k) - *(pAm + m)) / *(pAk + m);
                dum1 = sqrt(cot_2phi * cot_2phi + 1);
                if (cot_2phi < 0) dum1 = -dum1;
                tan_phi = -cot_2phi + dum1;
                tan2_phi = tan_phi * tan_phi;
                sin2_phi = tan2_phi / (1 + tan2_phi);
                cos2_phi = 1 - sin2_phi;
                sin_phi = sqrt(sin2_phi);
                if (tan_phi < 0) sin_phi = -sin_phi;
                cos_phi = sqrt(cos2_phi);
                sin_2phi = 2 * sin_phi * cos_phi;
                cos_2phi = cos2_phi - sin2_phi;

                // Rotate columns k and m for both the matrix A
                //     and the matrix of eigenvectors.

                p_r = const_cast<Real *>(A);
                dum1 = *(pAk + k);
                dum2 = *(pAm + m);
                dum3 = *(pAk + m);
                *(pAk + k) = dum1 * cos2_phi + dum2 * sin2_phi + dum3 * sin_2phi;
                *(pAm + m) = dum1 * sin2_phi + dum2 * cos2_phi - dum3 * sin_2phi;
                *(pAk + m) = 0;
                *(pAm + k) = 0;
                for (i = 0; i < n; p_r += n, i++) {
                    if ((i == k) || (i == m)) continue;
                    if (i < k)
                        dum1 = *(p_r + k);
                    else
                        dum1 = *(pAk + i);
                    if (i < m)
                        dum2 = *(p_r + m);
                    else
                        dum2 = *(pAm + i);
                    dum3 = dum1 * cos_phi + dum2 * sin_phi;
                    if (i < k)
                        *(p_r + k) = dum3;
                    else
                        *(pAk + i) = dum3;
                    dum3 = -dum1 * sin_phi + dum2 * cos_phi;
                    if (i < m)
                        *(p_r + m) = dum3;
                    else
                        *(pAm + i) = dum3;
                }
                for (p_e = eigenvectors, i = 0; i < n; p_e += n, i++) {
                    dum1 = *(p_e + k);
                    dum2 = *(p_e + m);
                    *(p_e + k) = dum1 * cos_phi + dum2 * sin_phi;
                    *(p_e + m) = -dum1 * sin_phi + dum2 * cos_phi;
                }
            }
            for (i = 0; i < n; i++)
                if (i == k)
                    continue;
                else if (max < std::abs(*(pAk + i)))
                    max = std::abs(*(pAk + i));
        }
    }
    for (pAk = const_cast<Real *>(A), k = 0; k < n; pAk += n, k++) eigenvalues[k] = *(pAk + k);
}

}  // Namespace helpme
#endif  // Header guard
// original file: ../src/string_utils.h

// BEGINLICENSE
//
// This file is part of helPME, which is distributed under the BSD 3-clause license,
// as described in the LICENSE file in the top level directory of this project.
//
// Author: Andrew C. Simmonett
//
// ENDLICENSE
#ifndef _HELPME_STANDALONE_STRING_UTIL_H_
#define _HELPME_STANDALONE_STRING_UTIL_H_

#include <complex>
#include <iomanip>
#include <iostream>
#include <sstream>
#include <string>

namespace helpme {

/*!
 * \brief makes a string representation of a floating point number.
 * \param width the width used to display the number.
 * \param precision the precision used to display the number.
 * \return the string representation of the floating point number.
 */
template <typename T, typename std::enable_if<std::is_floating_point<T>::value, int>::type = 0>
std::string formatNumber(const T &number, int width, int precision) {
    std::stringstream stream;
    stream.setf(std::ios::fixed, std::ios::floatfield);
    stream << std::setw(width) << std::setprecision(precision) << number;
    return stream.str();
}

/*!
 * \brief makes a string representation of a complex number.
 * \param width the width used to display the real and the imaginary components.
 * \param precision the precision used to display the real and the imaginary components.
 * \return the string representation of the complex number.
 */
template <typename T, typename std::enable_if<!std::is_floating_point<T>::value, int>::type = 0>
std::string formatNumber(const T &number, int width, int precision) {
    std::stringstream stream;
    stream.setf(std::ios::fixed, std::ios::floatfield);
    stream << "(" << std::setw(width) << std::setprecision(precision) << number.real() << ", " << std::setw(width)
           << std::setprecision(precision) << number.imag() << ")";
    return stream.str();
}

/*!
 * \brief makes a string representation of a multdimensional tensor, stored in a flat array.
 * \param data pointer to the start of the array holding the tensor information.
 * \param size the length of the array holding the tensor information.
 * \param rowDim the dimension of the fastest running index.
 * \param width the width of each individual floating point number.
 * \param precision used to display each floating point number.
 * \return the string representation of the tensor.
 */
template <typename T>
std::string stringify(T *data, size_t size, size_t rowDim, int width = 14, int precision = 8) {
    std::stringstream stream;
    for (size_t ind = 0; ind < size; ++ind) {
        stream << formatNumber(data[ind], width, precision);
        if (ind % rowDim == rowDim - 1)
            stream << std::endl;
        else
            stream << "  ";
    }
    return stream.str();
}

}  // Namespace helpme

#endif  // Header guard
// original file: ../src/memory.h

// BEGINLICENSE
//
// This file is part of helPME, which is distributed under the BSD 3-clause license,
// as described in the LICENSE file in the top level directory of this project.
//
// Author: Andrew C. Simmonett
//
// ENDLICENSE
#ifndef _HELPME_STANDALONE_MEMORY_H_
#define _HELPME_STANDALONE_MEMORY_H_

#include <stdexcept>
#include <vector>

#include <fftw3.h>

namespace helpme {

/*!
 * \brief FFTWAllocator a class to handle aligned allocation of memory using the FFTW libraries.
 *        Code is adapted from http://www.josuttis.com/cppcode/myalloc.hpp.html.
 */
template <class T>
class FFTWAllocator {
   public:
    // type definitions
    typedef T value_type;
    typedef T* pointer;
    typedef const T* const_pointer;
    typedef T& reference;
    typedef const T& const_reference;
    typedef std::size_t size_type;
    typedef std::ptrdiff_t difference_type;

    // rebind allocator to type U
    template <class U>
    struct rebind {
        typedef FFTWAllocator<U> other;
    };

    // return address of values
    pointer address(reference value) const { return &value; }
    const_pointer address(const_reference value) const { return &value; }

    /* constructors and destructor
     * - nothing to do because the allocator has no state
     */
    FFTWAllocator() throw() {}
    FFTWAllocator(const FFTWAllocator&) throw() {}
    template <class U>
    FFTWAllocator(const FFTWAllocator<U>&) throw() {}
    ~FFTWAllocator() throw() {}
    FFTWAllocator& operator=(FFTWAllocator other) throw() {}
    template <class U>
    FFTWAllocator& operator=(FFTWAllocator<U> other) throw() {}

    // return maximum number of elements that can be allocated
    size_type max_size() const throw() { return std::numeric_limits<std::size_t>::max() / sizeof(T); }

    // allocate but don't initialize num elements of type T
    pointer allocate(size_type num, const void* = 0) { return static_cast<pointer>(fftw_malloc(num * sizeof(T))); }

    // initialize elements of allocated storage p with value value
    void construct(pointer p, const T& value) {
        // initialize memory with placement new
        new ((void*)p) T(value);
    }

    // destroy elements of initialized storage p
    void destroy(pointer p) {}

    // deallocate storage p of deleted elements
    void deallocate(pointer p, size_type num) { fftw_free(static_cast<void*>(p)); }
};

// return that all specializations of this allocator are interchangeable
template <class T1, class T2>
bool operator==(const FFTWAllocator<T1>&, const FFTWAllocator<T2>&) throw() {
    return true;
}
template <class T1, class T2>
bool operator!=(const FFTWAllocator<T1>&, const FFTWAllocator<T2>&) throw() {
    return false;
}

template <typename Real>
using vector = std::vector<Real, FFTWAllocator<Real>>;

}  // Namespace helpme

#endif  // Header guard

namespace helpme {

/*!
 * A helper function to transpose a dense matrix in place, gratuitously stolen from
 * https://stackoverflow.com/questions/9227747/in-place-transposition-of-a-matrix
 */
template <class RandomIterator>
void transposeMemoryInPlace(RandomIterator first, RandomIterator last, int m) {
    const int mn1 = (last - first - 1);
    const int n = (last - first) / m;
    std::vector<bool> visited(last - first);
    RandomIterator cycle = first;
    while (++cycle != last) {
        if (visited[cycle - first]) continue;
        int a = cycle - first;
        do {
            a = a == mn1 ? mn1 : (n * a) % mn1;
            std::swap(*(first + a), *cycle);
            visited[a] = true;
        } while ((first + a) != cycle);
    }
}

/*!
 * \brief The Matrix class is designed to serve as a convenient wrapper to simplify 2D matrix operations.
 *        It assumes dense matrices with contiguious data and the fast running index being the right
 *        (column) index.  The underlying memory may have already been allocated elsewhere by C, Fortran
 *        or Python, and is directly manipulated in place, saving an expensive copy operation.  To provide
 *        read-only access to such memory address, use a const template type.
 */
template <typename Real>
class Matrix {
   protected:
    /// The number of rows in the matrix.
    size_t nRows_;
    /// The number of columns in the matrix.
    size_t nCols_;
    /// A vector to conveniently allocate data, if we really need to.
    helpme::vector<Real> allocatedData_;
    /// Pointer to the raw data, whose allocation may not be controlled by us.
    Real* data_;

   public:
    enum class SortOrder { Ascending, Descending };

    inline const Real& operator()(int row, int col) const { return *(data_ + row * nCols_ + col); }
    inline const Real& operator()(const std::pair<int, int>& indices) const {
        return *(data_ + std::get<0>(indices) * nCols_ + std::get<1>(indices));
    }
    inline Real& operator()(int row, int col) { return *(data_ + row * nCols_ + col); }
    inline Real& operator()(const std::pair<int, int>& indices) {
        return *(data_ + std::get<0>(indices) * nCols_ + std::get<1>(indices));
    }
    inline const Real* operator[](int row) const { return data_ + row * nCols_; }
    inline Real* operator[](int row) { return data_ + row * nCols_; }

    Real* begin() const { return data_; }
    Real* end() const { return data_ + nRows_ * nCols_; }
    const Real* cbegin() const { return data_; }
    const Real* cend() const { return data_ + nRows_ * nCols_; }

    /*!
     * \brief The sliceIterator struct provides a read-only view of a sub-block of a matrix, with arbitrary size.
     */
    struct sliceIterator {
        Real *begin_, *end_, *ptr_;
        size_t stride_;
        sliceIterator(Real* start, Real* end, size_t stride) : begin_(start), end_(end), ptr_(start), stride_(stride) {}
        sliceIterator begin() const { return sliceIterator(begin_, end_, stride_); }
        sliceIterator end() const { return sliceIterator(end_, end_, 0); }
        sliceIterator cbegin() const { return sliceIterator(begin_, end_, stride_); }
        sliceIterator cend() const { return sliceIterator(end_, end_, 0); }
        bool operator!=(const sliceIterator& other) { return ptr_ != other.ptr_; }
        sliceIterator operator*=(Real val) {
            for (auto& element : *this) element *= val;
            return *this;
        }
        sliceIterator operator/=(Real val) {
            Real invVal = 1 / val;
            for (auto& element : *this) element *= invVal;
            return *this;
        }
        sliceIterator operator-=(Real val) {
            for (auto& element : *this) element -= val;
            return *this;
        }
        sliceIterator operator+=(Real val) {
            for (auto& element : *this) element += val;
            return *this;
        }
        sliceIterator operator++() {
            ptr_ += stride_;
            return *this;
        }
        const Real& operator[](size_t index) { return *(begin_ + index); }
        size_t size() const { return std::distance(begin_, end_) / stride_; }
        void assertSameSize(const sliceIterator& other) const {
            if (size() != other.size())
                throw std::runtime_error("Slice operations only supported for slices of the same size.");
        }
        void assertContiguous(const sliceIterator& iter) const {
            if (iter.stride_ != 1)
                throw std::runtime_error(
                    "Slice operations called on operation that is only allowed for contiguous data.");
        }
        Matrix<Real> operator-(const sliceIterator& other) const {
            assertSameSize(other);
            assertContiguous(*this);
            assertContiguous(other);
            Matrix ret(1, size());
            std::transform(begin_, end_, other.begin_, ret[0],
                           [](const Real& a, const Real& b) -> Real { return a - b; });
            return ret;
        }
        sliceIterator operator-=(const sliceIterator& other) const {
            assertSameSize(other);
            assertContiguous(*this);
            assertContiguous(other);
            std::transform(begin_, end_, other.begin_, begin_,
                           [](const Real& a, const Real& b) -> Real { return a - b; });
            return *this;
        }
        sliceIterator operator+=(const sliceIterator& other) const {
            assertSameSize(other);
            assertContiguous(*this);
            assertContiguous(other);
            std::transform(begin_, end_, other.begin_, begin_,
                           [](const Real& a, const Real& b) -> Real { return a + b; });
            return *this;
        }
        Real& operator*() { return *ptr_; }
    };

    /*!
     * \brief row returns a read-only iterator over a given row.
     * \param r the row to return.
     * \return the slice in memory corresponding to the rth row.
     */
    sliceIterator row(size_t r) const { return sliceIterator(data_ + r * nCols_, data_ + (r + 1) * nCols_, 1); }

    /*!
     * \brief col returns a read-only iterator over a given column.
     * \param c the column to return.
     * \return the slice in memory corresponding to the cth column.
     */
    sliceIterator col(size_t c) const { return sliceIterator(data_ + c, data_ + nRows_ * nCols_ + c, nCols_); }

    /*!
     * \return the number of rows in this matrix.
     */
    size_t nRows() const { return nRows_; }

    /*!
     * \return the number of columns in this matrix.
     */
    size_t nCols() const { return nCols_; }

    /*!
     * \brief Matrix Constructs an empty matrix.
     */
    Matrix() : nRows_(0), nCols_(0) {}

    /*!
     * \brief Matrix Constructs a new matrix, allocating memory.
     * \param nRows the number of rows in the matrix.
     * \param nCols the number of columns in the matrix.
     */
    Matrix(size_t nRows, size_t nCols)
        : nRows_(nRows), nCols_(nCols), allocatedData_(nRows * nCols, 0), data_(allocatedData_.data()) {}

    /*!
     * \brief Matrix Constructs a new matrix, allocating memory.
     * \param filename the ASCII file from which to read this matrix
     */
    Matrix(const std::string& filename) {
        Real tmp;
        std::ifstream inFile(filename);

        if (!inFile) {
            std::string msg("Unable to open file ");
            msg += filename;
            throw std::runtime_error(msg);
        }

        inFile >> nRows_;
        inFile >> nCols_;
        while (inFile >> tmp) allocatedData_.push_back(tmp);
        inFile.close();
        if (nRows_ * nCols_ != allocatedData_.size()) {
            allocatedData_.clear();
            std::string msg("Inconsistent dimensions in ");
            msg += filename;
            msg += ".  Amount of data inconsitent with declared size.";
            throw std::runtime_error(msg);
        }
        allocatedData_.shrink_to_fit();
        data_ = allocatedData_.data();
    }

    /*!
     * \brief Matrix Constructs a new matrix, allocating memory and initializing values using the braced initializer.
     * \param data a braced initializer list of braced initializer lists containing the values to be stored in the
     * matrix.
     */
    Matrix(std::initializer_list<std::initializer_list<Real>> data) {
        nRows_ = data.size();
        nCols_ = nRows_ ? data.begin()->size() : 0;
        allocatedData_.reserve(nRows_ * nCols_);
        for (auto& row : data) {
            if (row.size() != nCols_) throw std::runtime_error("Inconsistent row dimensions in matrix specification.");
            allocatedData_.insert(allocatedData_.end(), row.begin(), row.end());
        }
        data_ = allocatedData_.data();
    }

    /*!
     * \brief Matrix Constructs a new column vector, allocating memory and initializing values using the braced
     * initializer. \param data a braced initializer list of braced initializer lists containing the values to be stored
     * in the matrix.
     */
    Matrix(std::initializer_list<Real> data) : allocatedData_(data), data_(allocatedData_.data()) {
        nRows_ = data.size();
        nCols_ = 1;
    }

    /*!
     * \brief Matrix Constructs a new matrix using already allocated memory.
     * \param ptr the already-allocated memory underlying this matrix.
     * \param nRows the number of rows in the matrix.
     * \param nCols the number of columns in the matrix.
     */
    Matrix(Real* ptr, size_t nRows, size_t nCols) : nRows_(nRows), nCols_(nCols), data_(ptr) {}

    /*!
     * \brief cast make a copy of this matrix, with its elements cast as a different type.
     * \tparam NewReal the type to cast each element to.
     * \return the copy of the matrix with the new type.
     */
    template <typename NewReal>
    Matrix<NewReal> cast() const {
        Matrix<NewReal> newMat(nRows_, nCols_);
        NewReal* newPtr = newMat[0];
        const Real* dataPtr = data_;
        for (size_t addr = 0; addr < nRows_ * nCols_; ++addr) *newPtr++ = static_cast<NewReal>(*dataPtr++);
        return newMat;
    }

    /*!
     * \brief setConstant sets all elements of this matrix to a specified value.
     * \param value the value to set each element to.
     */
    void setConstant(Real value) { std::fill(begin(), end(), value); }

    /*!
     * \brief setZero sets each element of this matrix to zero.
     */
    void setZero() { setConstant(0); }

    /*!
     * \brief isNearZero checks that each element in this matrix has an absolute value below some threshold.
     * \param threshold the value below which an element is considered zero.
     * \return whether all values are near zero or not.
     */
    bool isNearZero(Real threshold = 1e-10f) const {
        return !std::any_of(cbegin(), cend(), [&](const Real& val) { return std::abs(val) > threshold; });
    }

    /*!
     * \brief inverse inverts this matrix, leaving the original matrix untouched.
     * \return the inverse of this matrix.
     */
    Matrix inverse() const {
        assertSquare();

        Matrix matrixInverse(nRows_, nRows_);

        if (nRows() == 3) {
            // 3x3 is a really common case, so treat it here as.
            Real determinant = data_[0] * (data_[4] * data_[8] - data_[7] * data_[5]) -
                               data_[1] * (data_[3] * data_[8] - data_[5] * data_[6]) +
                               data_[2] * (data_[3] * data_[7] - data_[4] * data_[6]);

            Real determinantInverse = 1 / determinant;

            matrixInverse.data_[0] = (data_[4] * data_[8] - data_[7] * data_[5]) * determinantInverse;
            matrixInverse.data_[1] = (data_[2] * data_[7] - data_[1] * data_[8]) * determinantInverse;
            matrixInverse.data_[2] = (data_[1] * data_[5] - data_[2] * data_[4]) * determinantInverse;
            matrixInverse.data_[3] = (data_[5] * data_[6] - data_[3] * data_[8]) * determinantInverse;
            matrixInverse.data_[4] = (data_[0] * data_[8] - data_[2] * data_[6]) * determinantInverse;
            matrixInverse.data_[5] = (data_[3] * data_[2] - data_[0] * data_[5]) * determinantInverse;
            matrixInverse.data_[6] = (data_[3] * data_[7] - data_[6] * data_[4]) * determinantInverse;
            matrixInverse.data_[7] = (data_[6] * data_[1] - data_[0] * data_[7]) * determinantInverse;
            matrixInverse.data_[8] = (data_[0] * data_[4] - data_[3] * data_[1]) * determinantInverse;
        } else {
            // Generic case; just use spectral decomposition, invert the eigenvalues, and stitch back together.
            // Note that this only works for symmetric matrices.  Need to hook into Lapack for a general
            // inversion routine if this becomes a limitation.
            return this->applyOperation([](Real& element) { element = 1 / element; });
        }
        return matrixInverse;
    }

    /*!
     * \brief assertSymmetric checks that this matrix is symmetric within some threshold.
     * \param threshold the value below which an pair's difference is considered zero.
     */
    void assertSymmetric(const Real& threshold = 1e-10f) const {
        assertSquare();
        for (int row = 0; row < nRows_; ++row) {
            for (int col = 0; col < row; ++col) {
                if (std::abs(data_[row * nCols_ + col] - data_[col * nCols_ + row]) > threshold)
                    throw std::runtime_error("Unexpected non-symmetric matrix found.");
            }
        }
    }

    /*!
     * \brief applyOperationToEachElement modifies every element in the matrix by applying an operation.
     * \param function a unary operator describing the operation to perform.
     */
    void applyOperationToEachElement(const std::function<void(Real&)>& fxn) { std::for_each(begin(), end(), fxn); }

    /*!
     * \brief applyOperation applies an operation to this matrix using the spectral decomposition,
     *        leaving the original untouched.  Only for symmetric matrices, as coded.
     * \param function a undary operator describing the operation to perform.
     * \return the matrix transformed by the operator.
     */
    Matrix applyOperation(const std::function<void(Real&)>& function) const {
        assertSymmetric();

        auto eigenPairs = this->diagonalize();
        Matrix evalsReal = std::get<0>(eigenPairs);
        Matrix evecs = std::get<1>(eigenPairs);
        evalsReal.applyOperationToEachElement(function);
        Matrix evecsT = evecs.transpose();
        for (int row = 0; row < nRows_; ++row) {
            Real transformedEigenvalue = evalsReal[row][0];
            std::for_each(evecsT.data_ + row * nCols_, evecsT.data_ + (row + 1) * nCols_,
                          [&](Real& val) { val *= transformedEigenvalue; });
        }
        return evecs * evecsT;
    }

    /*!
     * \brief assertSameSize make sure that this Matrix has the same dimensions as another Matrix.
     * \param other the matrix to compare to.
     */
    void assertSameSize(const Matrix& other) const {
        if (nRows_ != other.nRows_ || nCols_ != other.nCols_)
            throw std::runtime_error("Attepting to compare matrices of different sizes!");
    }

    /*!
     * \brief assertSquare make sure that this Matrix is square.
     */
    void assertSquare() const {
        if (nRows_ != nCols_)
            throw std::runtime_error("Attepting to perform a square matrix operation on a non-square matrix!");
    }

    /*!
     * \brief multiply this matrix with another, returning a new matrix containing the product.
     * \param other the right hand side of the matrix product.
     * \return the product of this matrix with the matrix other.
     */
    Matrix multiply(const Matrix& other) const {
        // TODO one fine day this should be replaced by GEMM calls, if matrix multiplies actually get used much.
        if (nCols_ != other.nRows_)
            throw std::runtime_error("Attempting to multiply matrices with incompatible dimensions.");
        Matrix product(nRows_, other.nCols_);
        Real* output = product.data_;
        for (int row = 0; row < nRows_; ++row) {
            const Real* rowPtr = data_ + row * nCols_;
            for (int col = 0; col < other.nCols_; ++col) {
                for (int link = 0; link < nCols_; ++link) {
                    *output += rowPtr[link] * other.data_[link * other.nCols_ + col];
                }
                ++output;
            }
        }
        return product;
    }

    /*!
     * \brief operator * a convenient wrapper for the multiply function.
     * \param other the right hand side of the matrix product.
     * \return the product of this matrix with the matrix other.
     */
    Matrix operator*(const Matrix& other) const { return this->multiply(other); }

    /*!
     * \brief increment this matrix with another, returning a new matrix containing the sum.
     * \param other the right hand side of the matrix sum.
     * \return the sum of this matrix and the matrix other.
     */
    Matrix incrementWith(const Matrix& other) {
        assertSameSize(other);
        std::transform(begin(), end(), other.begin(), begin(),
                       [](const Real& a, const Real& b) -> Real { return a + b; });
        return *this;
    }

    /*!
     * \brief a wrapper around the incrementWith() function.
     * \param other the right hand side of the matrix sum.
     * \return the sum of this matrix and the matrix other.
     */
    Matrix operator+=(const Matrix& other) { return this->incrementWith(other); }

    /*!
     * \brief almostEquals checks that two matrices have all elements the same, within some specificied tolerance.
     * \param other the matrix against which we're comparing.
     * \param tol the amount that each element is allowed to deviate by.
     * \return whether the two matrices are almost equal.
     */
    template <typename T = Real, typename std::enable_if<std::is_floating_point<T>::value, int>::type = 0>
    bool almostEquals(const Matrix& other, Real tolerance = 1e-6) const {
        // The floating point version
        assertSameSize(other);

        return std::equal(cbegin(), cend(), other.cbegin(), [&tolerance](Real a, Real b) -> bool {
            return (((a - b) < std::real(tolerance)) && ((a - b) > -std::real(tolerance)));
        });
    }
    template <typename T = Real, typename std::enable_if<!std::is_floating_point<T>::value, int>::type = 0>
    bool almostEquals(const Matrix& other, Real tolerance = 1e-6) const {
        // The complex version
        assertSameSize(other);

        auto tol = std::real(tolerance);
        // This is a little confusing, but the type "Real" is actually some king of std::complex<...>.
        return std::equal(cbegin(), cend(), other.cbegin(), [&tol](Real a, Real b) -> bool {
            return (((a.real() - b.real()) < tol) && ((a.real() - b.real()) > -tol) && ((a.imag() - b.imag()) < tol) &&
                    ((a.imag() - b.imag()) > -tol));
        });
    }

    /*!
     * \brief dot computes the inner product of this matrix with another.
     * \param other the other matrix in the inner product, which must have the same dimensions.
     * \return the inner product.
     */
    Real dot(const Matrix& other) const {
        assertSameSize(other);

        return std::inner_product(cbegin(), cend(), other.cbegin(), Real(0));
    }

    /*!
     * \brief writeToFile formats the matrix and writes to an ASCII file.
     * \param fileName the name of the file to save to.
     * \param width the width of each matrix element's formatted representation.
     * \param precision the precision of each matrix element's formatted representation.
     * \param printDimensions whether to print the dimensions at the top of the file.
     */
    void writeToFile(const std::string& filename, int width = 20, int precision = 14,
                     bool printDimensions = false) const {
        std::ofstream file;
        file.open(filename, std::ios::out);
        if (printDimensions) file << nRows_ << "  " << nCols_ << std::endl;
        file << stringify(data_, nRows_ * nCols_, nCols_, width, precision);
        file.close();
    }

    /*!
     * \brief write formatted matrix to a stream object.
     * \param os stream object to write to.
     * \return modified stream object.
     */
    std::ostream& write(std::ostream& os) const {
        for (int row = 0; row < nRows_; ++row) os << stringify(data_ + row * nCols_, nCols_, nCols_);
        os << std::endl;
        return os;
    }

    /*!
     * \brief transposeInPlace transposes this matrix in place.
     */
    void transposeInPlace() {
        transposeMemoryInPlace(begin(), end(), nCols_);
        std::swap(nCols_, nRows_);
    }

    /*!
     * \brief clone make a new copy of this matrix by deep copying the data.
     * \return the copy of this matrix.
     */
    Matrix clone() const {
        Matrix newMatrix = Matrix(nRows_, nCols_);
        std::copy(cbegin(), cend(), newMatrix.begin());
        return newMatrix;
    }

    /*!
     * \brief transpose this matrix, leaving the original untouched.
     * \return a transposed deep copy of this matrix.
     */
    Matrix transpose() const {
        Matrix copy = this->clone();
        copy.transposeInPlace();
        return copy;
    }

    /*!
     * \brief diagonalize diagonalize this matrix, leaving the original untouched.  Note that this assumes
     *        that this matrix is real and symmetric.
     * \param order how to order the (eigenvalue,eigenvector) pairs, where the sort key is the eigenvalue.
     * \return a pair of corresponding <eigenvalue , eigenvectors> sorted according to the order variable.
     *         The eigenvectors are stored by column.
     */
    std::pair<Matrix<Real>, Matrix<Real>> diagonalize(SortOrder order = SortOrder::Ascending) const {
        assertSymmetric();

        Matrix eigenValues(nRows_, 1);
        Matrix unsortedEigenVectors(nRows_, nRows_);
        Matrix sortedEigenVectors(nRows_, nRows_);

        JacobiCyclicDiagonalization<Real>(eigenValues[0], unsortedEigenVectors[0], cbegin(), nRows_);
        unsortedEigenVectors.transposeInPlace();

        std::vector<std::pair<Real, const Real*>> eigenPairs;
        for (int val = 0; val < nRows_; ++val) eigenPairs.push_back({eigenValues[val][0], unsortedEigenVectors[val]});
        std::sort(eigenPairs.begin(), eigenPairs.end());
        if (order == SortOrder::Descending) std::reverse(eigenPairs.begin(), eigenPairs.end());
        for (int val = 0; val < nRows_; ++val) {
            const auto& e = eigenPairs[val];
            eigenValues.data_[val] = std::get<0>(e);
            std::copy(std::get<1>(e), std::get<1>(e) + nCols_, sortedEigenVectors[val]);
        }
        sortedEigenVectors.transposeInPlace();
        return {std::move(eigenValues), std::move(sortedEigenVectors)};
    }
};

/*!
 * A helper function to allow printing of Matrix objects to a stream.
 */
template <typename Real>
std::ostream& operator<<(std::ostream& os, Matrix<Real> const& m) {
    return m.write(os);
}

}  // Namespace helpme
#endif  // Header guard

#include <vector>

namespace helpme {

static inline int cartesianAddress(int lx, int ly, int lz) {
    int l = lx + ly + lz;
    return lz * (2 * l - lz + 3) / 2 + ly;
}

/*!
 * \brief makeCartesianRotationMatrix builds a rotation matrix for unique Cartesian
 *        components with a given angular momentum.  The algorithm used here is the simple
 *        version (eq. 18) from D. M. Elking, J. Comp. Chem., 37 2067 (2016).  It's definitely
 *        not the fastest way to do it, but will be revisited if profiling shows it to be an issue.
 * \param angularMomentum the angular momentum of the rotation matrix desired.
 * \param transformer the matrix R to do the transform defined for a dipole as µ_new = R . µ_old.
 * \return the rotation matrix
 */
template <typename Real>
Matrix<Real> makeCartesianRotationMatrix(int angularMomentum, const Matrix<Real> &transformer) {
    Real R00 = transformer[0][0];
    Real R01 = transformer[0][1];
    Real R02 = transformer[0][2];
    Real R10 = transformer[1][0];
    Real R11 = transformer[1][1];
    Real R12 = transformer[1][2];
    Real R20 = transformer[2][0];
    Real R21 = transformer[2][1];
    Real R22 = transformer[2][2];
    int nComponents = (angularMomentum + 1) * (angularMomentum + 2) / 2;
    auto factorial = std::vector<int>(2 * angularMomentum + 1);
    factorial[0] = 1;
    for (int l = 1; l <= 2 * angularMomentum; ++l) factorial[l] = l * factorial[l - 1];
    Matrix<Real> R(nComponents, nComponents);
    for (int nz = 0; nz <= angularMomentum; ++nz) {
        for (int ny = 0; ny <= angularMomentum - nz; ++ny) {
            int nx = angularMomentum - ny - nz;
            for (int pz = 0; pz <= nx; ++pz) {
                for (int py = 0; py <= nx - pz; ++py) {
                    int px = nx - py - pz;
                    for (int qz = 0; qz <= ny; ++qz) {
                        for (int qy = 0; qy <= ny - qz; ++qy) {
                            int qx = ny - qy - qz;
                            for (int rz = 0; rz <= nz; ++rz) {
                                for (int ry = 0; ry <= nz - rz; ++ry) {
                                    int rx = nz - ry - rz;
                                    int mx = px + qx + rx;
                                    int my = py + qy + ry;
                                    int mz = pz + qz + rz;
                                    int m = mx + my + mz;
                                    if (m == angularMomentum) {
                                        Real normx = factorial[mx] / (factorial[px] * factorial[qx] * factorial[rx]);
                                        Real normy = factorial[my] / (factorial[py] * factorial[qy] * factorial[ry]);
                                        Real normz = factorial[mz] / (factorial[pz] * factorial[qz] * factorial[rz]);
                                        Real Rx = std::pow(R00, px) * std::pow(R10, py) * std::pow(R20, pz);
                                        Real Ry = std::pow(R01, qx) * std::pow(R11, qy) * std::pow(R21, qz);
                                        Real Rz = std::pow(R02, rx) * std::pow(R12, ry) * std::pow(R22, rz);
                                        Real term = normx * normy * normz * Rx * Ry * Rz;
                                        R[cartesianAddress(mx, my, mz)][cartesianAddress(nx, ny, nz)] += term;
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
    return R;
}

/*!
 * \brief matrixVectorProduct A naive implementation of matrix-vector products, avoiding BLAS requirements (for now).
 * \param transformer the transformation matrix.
 * \param inputVector the vector to be transformed.
 * \param outputVector the transformed vector.
 */
template <typename Real>
void matrixVectorProduct(const Matrix<Real> &transformer, const Real *inputVector, Real *outputVector) {
    int dimension = transformer.nRows();
    for (int row = 0; row < dimension; ++row) {
        outputVector[row] = std::inner_product(inputVector, inputVector + dimension, transformer[row], Real(0));
    }
}

/*!
 * \brief cartesianTransform transforms a list of a cartesian quantities to a different basis.
 *        Assumes a list of quantities are to be transformed (in place) and all angular momentum
 *        components up to and including the specified maximum are present in ascending A.M. order.
 * \param maxAngularMomentum the angular momentum of the incoming quantity.
 * \param transformOnlyThisShell if true, only the shell with angular momentum specified will be transformed
 * \param transformer the matrix R to do the transform defined for a dipole as µ_new = R . µ_old.
 * \param transformee the quantity to be transformed, stored as nAtoms X nComponents, with
 *        components being the fast running index.
 */
template <typename Real>
Matrix<Real> cartesianTransform(int maxAngularMomentum, bool transformOnlyThisShell, const Matrix<Real> &transformer,
                                const Matrix<Real> &transformee) {
    Matrix<Real> transformed = transformee.clone();
    int offset = transformOnlyThisShell ? 0 : 1;
    int nAtoms = transformee.nRows();
    int firstShell = transformOnlyThisShell ? maxAngularMomentum : 1;
    for (int angularMomentum = firstShell; angularMomentum <= maxAngularMomentum; ++angularMomentum) {
        auto rotationMatrix = makeCartesianRotationMatrix(angularMomentum, transformer);
        for (int atom = 0; atom < nAtoms; ++atom) {
            const Real *inputData = transformee[atom];
            Real *outputData = transformed[atom];
            matrixVectorProduct(rotationMatrix, inputData + offset, outputData + offset);
        }
        offset += (angularMomentum + 1) * (angularMomentum + 2) / 2;
    }
    return transformed;
}

}  // Namespace helpme
#endif  // Header guard
// original file: ../src/fftw_wrapper.h

// BEGINLICENSE
//
// This file is part of helPME, which is distributed under the BSD 3-clause license,
// as described in the LICENSE file in the top level directory of this project.
//
// Author: Andrew C. Simmonett
//
// ENDLICENSE
#ifndef _HELPME_STANDALONE_FFTW_WRAPPER_H_
#define _HELPME_STANDALONE_FFTW_WRAPPER_H_

#include <complex>
#include <iostream>
#include <limits>
#include <stdexcept>
#include <type_traits>

#include <fftw3.h>
// #include "memory.h"

namespace helpme {

/*!
 * \brief The FFTWTypes class is a placeholder to lookup function names and types in FFTW parlance by template.
 */
template <typename Real>
struct FFTWTypes {
    struct EmptyPlan {
        int unused;
    };
    using Plan = int;
    using Complex = std::complex<int>;
    static Plan makePlan4(size_t, void *, void *, int) { return 0; };
    static Plan makePlan5(size_t, void *, void *, int, int) { return 0; };
    static void execPlan1(Plan){};
    static void execPlan3(Plan, void *, void *){};
    static constexpr bool isImplemented = false;
    static constexpr decltype(&makePlan4) MakeRealToComplexPlan = &makePlan4;
    static constexpr decltype(&makePlan4) MakeComplexToRealPlan = &makePlan4;
    static constexpr decltype(&makePlan5) MakeComplexToComplexPlan = &makePlan5;
    static constexpr decltype(&execPlan3) ExecuteRealToComplexPlan = &execPlan3;
    static constexpr decltype(&execPlan3) ExecuteComplexToRealPlan = &execPlan3;
    static constexpr decltype(&execPlan3) ExecuteComplexToComplexPlan = &execPlan3;
    static constexpr decltype(&execPlan1) DestroyPlan = &execPlan1;
    static constexpr decltype(&execPlan1) CleanupFFTW = &execPlan1;
};

#if HAVE_FFTWF == 1
template <>
struct FFTWTypes<float> {
    using Plan = fftwf_plan;
    using Complex = fftwf_complex;
    static constexpr bool isImplemented = true;
    static constexpr decltype(&fftwf_plan_dft_r2c_1d) MakeRealToComplexPlan = &fftwf_plan_dft_r2c_1d;
    static constexpr decltype(&fftwf_plan_dft_c2r_1d) MakeComplexToRealPlan = &fftwf_plan_dft_c2r_1d;
    static constexpr decltype(&fftwf_plan_dft_1d) MakeComplexToComplexPlan = &fftwf_plan_dft_1d;
    static constexpr decltype(&fftwf_execute_dft_r2c) ExecuteRealToComplexPlan = &fftwf_execute_dft_r2c;
    static constexpr decltype(&fftwf_execute_dft_c2r) ExecuteComplexToRealPlan = &fftwf_execute_dft_c2r;
    static constexpr decltype(&fftwf_execute_dft) ExecuteComplexToComplexPlan = &fftwf_execute_dft;
    static constexpr decltype(&fftwf_destroy_plan) DestroyPlan = &fftwf_destroy_plan;
    static constexpr decltype(&fftwf_cleanup) CleanupFFTW = &fftwf_cleanup;
};
#endif  // HAVE_FFTWF

#if HAVE_FFTWD == 1
template <>
struct FFTWTypes<double> {
    using Plan = fftw_plan;
    using Complex = fftw_complex;
    static constexpr bool isImplemented = true;
    static constexpr decltype(&fftw_plan_dft_r2c_1d) MakeRealToComplexPlan = &fftw_plan_dft_r2c_1d;
    static constexpr decltype(&fftw_plan_dft_c2r_1d) MakeComplexToRealPlan = &fftw_plan_dft_c2r_1d;
    static constexpr decltype(&fftw_plan_dft_1d) MakeComplexToComplexPlan = &fftw_plan_dft_1d;
    static constexpr decltype(&fftw_execute_dft_r2c) ExecuteRealToComplexPlan = &fftw_execute_dft_r2c;
    static constexpr decltype(&fftw_execute_dft_c2r) ExecuteComplexToRealPlan = &fftw_execute_dft_c2r;
    static constexpr decltype(&fftw_execute_dft) ExecuteComplexToComplexPlan = &fftw_execute_dft;
    static constexpr decltype(&fftw_destroy_plan) DestroyPlan = &fftw_destroy_plan;
    static constexpr decltype(&fftw_cleanup) CleanupFFTW = &fftw_cleanup;
};
#endif  // HAVE_FFTWD

#if HAVE_FFTWL == 1
template <>
struct FFTWTypes<long double> {
    using Plan = fftwl_plan;
    using Complex = fftwl_complex;
    static constexpr bool isImplemented = true;
    static constexpr decltype(&fftwl_plan_dft_r2c_1d) MakeRealToComplexPlan = &fftwl_plan_dft_r2c_1d;
    static constexpr decltype(&fftwl_plan_dft_c2r_1d) MakeComplexToRealPlan = &fftwl_plan_dft_c2r_1d;
    static constexpr decltype(&fftwl_plan_dft_1d) MakeComplexToComplexPlan = &fftwl_plan_dft_1d;
    static constexpr decltype(&fftwl_execute_dft_r2c) ExecuteRealToComplexPlan = &fftwl_execute_dft_r2c;
    static constexpr decltype(&fftwl_execute_dft_c2r) ExecuteComplexToRealPlan = &fftwl_execute_dft_c2r;
    static constexpr decltype(&fftwl_execute_dft) ExecuteComplexToComplexPlan = &fftwl_execute_dft;
    static constexpr decltype(&fftwl_destroy_plan) DestroyPlan = &fftwl_destroy_plan;
    static constexpr decltype(&fftwl_cleanup) CleanupFFTW = &fftwl_cleanup;
};
#endif  // HAVE_FFTWL

/*!
 * \brief The FFTWWrapper class is a convenient wrapper to abstract away the details of different
 *        precision modes for FFTW, where the types and function names differ.
 */
template <typename Real>
class FFTWWrapper {
    using typeinfo = FFTWTypes<Real>;
    using Plan = typename typeinfo::Plan;
    using Complex = typename typeinfo::Complex;

   protected:
    /// An FFTW plan object, describing out of place complex to complex forward transforms.
    typename typeinfo::Plan forwardPlan_;
    /// An FFTW plan object, describing out of place complex to complex inverse transforms.
    typename typeinfo::Plan inversePlan_;
    /// An FFTW plan object, describing in place complex to complex forward transforms.
    typename typeinfo::Plan forwardInPlacePlan_;
    /// An FFTW plan object, describing in place complex to complex inverse transforms.
    typename typeinfo::Plan inverseInPlacePlan_;
    /// An FFTW plan object, describing out of place real to complex forward transforms.
    typename typeinfo::Plan realToComplexPlan_;
    /// An FFTW plan object, describing out of place complex to real inverse transforms.
    typename typeinfo::Plan complexToRealPlan_;
    /// The size of the real data.
    size_t fftDimension_;
    /// The flags to be passed to the FFTW plan creator, to determine startup cost.
    unsigned transformFlags_;

   public:
    FFTWWrapper() {}
    FFTWWrapper(size_t fftDimension) : fftDimension_(fftDimension), transformFlags_(FFTW_ESTIMATE) {
        if (!typeinfo::isImplemented) {
            throw std::runtime_error(
                "Attempting to call FFTW using a precision mode that has not been linked. "
                "Make sure that -DHAVE_FFTWF=1, -DHAVE_FFTWD=1 or -DHAVE_FFTWL=1 is added to the compiler flags"
                "for single, double and long double precision support, respectively.");
        }
        helpme::vector<Real> realTemp(fftDimension_);
        helpme::vector<std::complex<Real>> complexTemp1(fftDimension_);
        helpme::vector<std::complex<Real>> complexTemp2(fftDimension_);
        Real *realPtr = realTemp.data();
        Complex *complexPtr1 = reinterpret_cast<Complex *>(complexTemp1.data());
        Complex *complexPtr2 = reinterpret_cast<Complex *>(complexTemp2.data());
        forwardPlan_ =
            typeinfo::MakeComplexToComplexPlan(fftDimension_, complexPtr1, complexPtr2, FFTW_FORWARD, transformFlags_);
        inversePlan_ =
            typeinfo::MakeComplexToComplexPlan(fftDimension_, complexPtr1, complexPtr2, FFTW_BACKWARD, transformFlags_);
        forwardInPlacePlan_ =
            typeinfo::MakeComplexToComplexPlan(fftDimension_, complexPtr1, complexPtr1, FFTW_FORWARD, transformFlags_);
        inverseInPlacePlan_ =
            typeinfo::MakeComplexToComplexPlan(fftDimension_, complexPtr1, complexPtr1, FFTW_BACKWARD, transformFlags_);
        realToComplexPlan_ = typeinfo::MakeRealToComplexPlan(fftDimension_, realPtr, complexPtr1, transformFlags_);
        complexToRealPlan_ = typeinfo::MakeComplexToRealPlan(fftDimension_, complexPtr1, realPtr, transformFlags_);
    }

    /*!
     * \brief transform call FFTW to do an out of place complex to real FFT.
     * \param inBuffer the location of the input data.
     * \param outBuffer the location of the output data.
     */
    void transform(std::complex<Real> *inBuffer, Real *outBuffer) {
        typeinfo::ExecuteComplexToRealPlan(complexToRealPlan_, reinterpret_cast<Complex *>(inBuffer), outBuffer);
    }

    /*!
     * \brief transform call FFTW to do an out of place real to complex FFT.
     * \param inBuffer the location of the input data.
     * \param outBuffer the location of the output data.
     */
    void transform(Real *inBuffer, std::complex<Real> *outBuffer) {
        typeinfo::ExecuteRealToComplexPlan(realToComplexPlan_, inBuffer, reinterpret_cast<Complex *>(outBuffer));
    }

    /*!
     * \brief transform call FFTW to do an in place complex to complex FFT.
     * \param inPlaceBuffer the location of the input and output data.
     * \param direction either FFTW_FORWARD or FFTW_BACKWARD.
     */
    void transform(std::complex<Real> *inPlaceBuffer, int direction) {
        Complex *inPlacePtr = reinterpret_cast<Complex *>(inPlaceBuffer);
        switch (direction) {
            case FFTW_FORWARD:
                typeinfo::ExecuteComplexToComplexPlan(forwardInPlacePlan_, inPlacePtr, inPlacePtr);
                break;
            case FFTW_BACKWARD:
                typeinfo::ExecuteComplexToComplexPlan(inverseInPlacePlan_, inPlacePtr, inPlacePtr);
                break;
            default:
                throw std::runtime_error("Invalid FFTW transform passed to in place transform().");
        }
    }

    /*!
     * \brief transform call FFTW to do an out of place complex to complex FFT.
     * \param inBuffer the location of the input data.
     * \param outBuffer the location of the output data.
     * \param direction either FFTW_FORWARD or FFTW_BACKWARD.
     */
    void transform(std::complex<Real> *inBuffer, std::complex<Real> *outBuffer, int direction) {
        Complex *inPtr = reinterpret_cast<Complex *>(inBuffer);
        Complex *outPtr = reinterpret_cast<Complex *>(outBuffer);
        switch (direction) {
            case FFTW_FORWARD:
                typeinfo::ExecuteComplexToComplexPlan(forwardPlan_, inPtr, outPtr);
                break;
            case FFTW_BACKWARD:
                typeinfo::ExecuteComplexToComplexPlan(inversePlan_, inPtr, outPtr);
                break;
            default:
                throw std::runtime_error("Invalid FFTW transform passed to transform().");
        }
    }
};

}  // Namespace helpme
#endif  // Header guard
// original file: ../src/gamma.h

// BEGINLICENSE
//
// This file is part of helPME, which is distributed under the BSD 3-clause license,
// as described in the LICENSE file in the top level directory of this project.
//
// Author: Andrew C. Simmonett
//
// ENDLICENSE
#ifndef _HELPME_STANDALONE_GAMMA_H_
#define _HELPME_STANDALONE_GAMMA_H_

#include <cmath>
#include <limits>

/*!
 * \file gamma.h
 * \brief Contains C++ implementations of templated gamma and incomplete gamma functions, computed using recursion.
 */

namespace helpme {

#define SQRTTWO std::sqrt(static_cast<Real>(2))
#define SQRTPI static_cast<Real>(1.77245385090551602729816748334114518279754945612238712821381L)
#define PI static_cast<Real>(3.14159265358979323846264338327950288419716939937510582097494L)

/*!
 * Compute upper incomplete gamma functions for positive half-integral s values using the recursion
 * \f$ \Gamma[\frac{\mathrm{twoS}}{2},x] = \Gamma[\frac{\mathrm{twoS}-2}{2},x] + x^{\frac{\mathrm{twoS}-2}{2}}e^{-x}\f$
 */
template <typename Real, int twoS, bool isPositive>
struct incompleteGammaRecursion {
    static Real compute(Real x) {
        return (0.5f * twoS - 1) * incompleteGammaRecursion<Real, twoS - 2, isPositive>::compute(x) +
               pow(x, (0.5f * twoS - 1)) * exp(-x);
    }
};

/*!
 * Compute upper incomplete gamma functions for negative half-integral s values using the recursion
 * \f$ \Gamma[\frac{\mathrm{twoS}}{2},x] = \frac{2\Gamma[\frac{\mathrm{twoS}+2}{2},x] -
 * 2x^\frac{\mathrm{twoS}}{2}e^{-x}}{\mathrm{twoS}}\f$
 */
template <typename Real, int twoS>
struct incompleteGammaRecursion<Real, twoS, false> {
    static Real compute(Real x) {
        return (incompleteGammaRecursion<Real, twoS + 2, false>::compute(x) - pow(x, 0.5f * twoS) * exp(-x)) /
               (0.5f * twoS);
    }
};

/// Specific value of incomplete gamma function.
template <typename Real>
struct incompleteGammaRecursion<Real, 2, true> {
    static Real compute(Real x) { return exp(-x); }
};

/// Specific value of incomplete gamma function.
template <typename Real>
struct incompleteGammaRecursion<Real, 1, false> {
    static Real compute(Real x) { return SQRTPI * erfc(std::sqrt(x)); }
};

/// Specific value of incomplete gamma function.
template <typename Real>
struct incompleteGammaRecursion<Real, 1, true> {
    static Real compute(Real x) { return SQRTPI * erfc(std::sqrt(x)); }
};

/// Specific value of incomplete gamma function.
template <typename Real>
struct incompleteGammaRecursion<Real, 0, false> {
    static Real compute(Real x) {
        // Gamma(0,x) is (minus) the exponential integral of -x.  This implementation was stolen from
        // http://www.mymathlib.com/c_source/functions/exponential_integrals/exponential_integral_Ei.c
        x = -x;
        if (x < -5.0L) return -(Real)Continued_Fraction_Ei(x);
        if (x == 0.0L) return std::numeric_limits<Real>::max();
        if (x < 6.8L) return -(Real)Power_Series_Ei(x);
        if (x < 50.0L) return -(Real)Argument_Addition_Series_Ei(x);
        return -(Real)Continued_Fraction_Ei(x);
    }

   private:
    static constexpr long double epsilon = 10.0 * std::numeric_limits<long double>::epsilon();

    ////////////////////////////////////////////////////////////////////////////////
    // static long double Continued_Fraction_Ei( long double x )                  //
    //                                                                            //
    //  Description:                                                              //
    //     For x < -5 or x > 50, the continued fraction representation of Ei      //
    //     converges fairly rapidly.                                              //
    //                                                                            //
    //     The continued fraction expansion of Ei(x) is:                          //
    //        Ei(x) = -exp(x) { 1/(-x+1-) 1/(-x+3-) 4/(-x+5-) 9/(-x+7-) ... }.    //
    //                                                                            //
    //                                                                            //
    //  Arguments:                                                                //
    //     long double  x                                                         //
    //                The argument of the exponential integral Ei().              //
    //                                                                            //
    //  Return Value:                                                             //
    //     The value of the exponential integral Ei evaluated at x.               //
    ////////////////////////////////////////////////////////////////////////////////

    static long double Continued_Fraction_Ei(long double x) {
        long double Am1 = 1.0L;
        long double A0 = 0.0L;
        long double Bm1 = 0.0L;
        long double B0 = 1.0L;
        long double a = std::exp(x);
        long double b = -x + 1.0L;
        long double Ap1 = b * A0 + a * Am1;
        long double Bp1 = b * B0 + a * Bm1;
        int j = 1;

        a = 1.0L;
        while (std::fabs(Ap1 * B0 - A0 * Bp1) > epsilon * std::fabs(A0 * Bp1)) {
            if (std::fabs(Bp1) > 1.0L) {
                Am1 = A0 / Bp1;
                A0 = Ap1 / Bp1;
                Bm1 = B0 / Bp1;
                B0 = 1.0L;
            } else {
                Am1 = A0;
                A0 = Ap1;
                Bm1 = B0;
                B0 = Bp1;
            }
            a = -j * j;
            b += 2.0L;
            Ap1 = b * A0 + a * Am1;
            Bp1 = b * B0 + a * Bm1;
            j += 1;
        }
        return (-Ap1 / Bp1);
    }

    ////////////////////////////////////////////////////////////////////////////////
    // static long double Power_Series_Ei( long double x )                        //
    //                                                                            //
    //  Description:                                                              //
    //     For -5 < x < 6.8, the power series representation for (Ei(x) - gamma   //
    //     - ln|x|)/exp(x) is used, where gamma is Euler's gamma constant.        //
    //     Note that for x = 0.0, Ei is -inf.  In which case -DBL_MAX is          //
    //     returned.                                                              //
    //                                                                            //
    //     The power series expansion of (Ei(x) - gamma - ln|x|) / exp(x) is      //
    //        - Sum(1 + 1/2 + ... + 1/j) (-x)^j / j!, where the Sum extends       //
    //        from j = 1 to inf.                                                  //
    //                                                                            //
    //  Arguments:                                                                //
    //     long double  x                                                         //
    //                The argument of the exponential integral Ei().              //
    //                                                                            //
    //  Return Value:                                                             //
    //     The value of the exponential integral Ei evaluated at x.               //
    ////////////////////////////////////////////////////////////////////////////////

    static long double Power_Series_Ei(long double x) {
        long double xn = -x;
        long double Sn = -x;
        long double Sm1 = 0.0L;
        long double hsum = 1.0L;
        long double g = 0.5772156649015328606065121L;
        long double y = 1.0L;
        long double factorial = 1.0L;

        while (std::fabs(Sn - Sm1) > epsilon * std::fabs(Sm1)) {
            Sm1 = Sn;
            y += 1.0L;
            xn *= (-x);
            factorial *= y;
            hsum += (1.0 / y);
            Sn += hsum * xn / factorial;
        }
        return (g + std::log(std::fabs(x)) - std::exp(x) * Sn);
    }

    ////////////////////////////////////////////////////////////////////////////////
    // static long double Argument_Addition_Series_Ei(long double x)              //
    //                                                                            //
    //  Description:                                                              //
    //     For 6.8 < x < 50.0, the argument addition series is used to calculate  //
    //     Ei.                                                                    //
    //                                                                            //
    //     The argument addition series for Ei(x) is:                             //
    //      Ei(x+dx) = Ei(x) + exp(x) Sum j! [exp(j) expj(-dx) - 1] / x^(j+1),    //
    //     where the Sum extends from j = 0 to inf, |x| > |dx| and expj(y) is     //
    //     the exponential polynomial expj(y) = Sum y^k / k!,                     //
    //     the Sum extending from k = 0 to k = j.                                 //
    //                                                                            //
    //  Arguments:                                                                //
    //     long double  x                                                         //
    //                The argument of the exponential integral Ei().              //
    //                                                                            //
    //  Return Value:                                                             //
    //     The value of the exponential integral Ei evaluated at x.               //
    ////////////////////////////////////////////////////////////////////////////////
    static long double Argument_Addition_Series_Ei(long double x) {
        static long double ei[] = {
            1.915047433355013959531e2L,  4.403798995348382689974e2L,  1.037878290717089587658e3L,
            2.492228976241877759138e3L,  6.071406374098611507965e3L,  1.495953266639752885229e4L,
            3.719768849068903560439e4L,  9.319251363396537129882e4L,  2.349558524907683035782e5L,
            5.955609986708370018502e5L,  1.516637894042516884433e6L,  3.877904330597443502996e6L,
            9.950907251046844760026e6L,  2.561565266405658882048e7L,  6.612718635548492136250e7L,
            1.711446713003636684975e8L,  4.439663698302712208698e8L,  1.154115391849182948287e9L,
            3.005950906525548689841e9L,  7.842940991898186370453e9L,  2.049649711988081236484e10L,
            5.364511859231469415605e10L, 1.405991957584069047340e11L, 3.689732094072741970640e11L,
            9.694555759683939661662e11L, 2.550043566357786926147e12L, 6.714640184076497558707e12L,
            1.769803724411626854310e13L, 4.669055014466159544500e13L, 1.232852079912097685431e14L,
            3.257988998672263996790e14L, 8.616388199965786544948e14L, 2.280446200301902595341e15L,
            6.039718263611241578359e15L, 1.600664914324504111070e16L, 4.244796092136850759368e16L,
            1.126348290166966760275e17L, 2.990444718632336675058e17L, 7.943916035704453771510e17L,
            2.111342388647824195000e18L, 5.614329680810343111535e18L, 1.493630213112993142255e19L,
            3.975442747903744836007e19L, 1.058563689713169096306e20L};
        int k = (int)(x + 0.5f);
        int j = 0;
        long double xx = (long double)k;
        long double dx = x - xx;
        long double xxj = xx;
        long double edx = std::exp(dx);
        long double Sm = 1.0L;
        long double Sn = (edx - 1.0L) / xxj;
        long double term = std::numeric_limits<double>::max();
        long double factorial = 1.0L;
        long double dxj = 1.0L;

        while (std::fabs(term) > epsilon * std::fabs(Sn)) {
            j++;
            factorial *= (long double)j;
            xxj *= xx;
            dxj *= (-dx);
            Sm += (dxj / factorial);
            term = (factorial * (edx * Sm - 1.0L)) / xxj;
            Sn += term;
        }

        return ei[k - 7] + Sn * std::exp(xx);
    }
};

/*!
 * Compute gamma function for positive half-integral s values using the recursion.
 * \f$ \Gamma[\frac{\mathrm{twoS}}{2}] = \Gamma[\frac{\mathrm{twoS}-2}{2}]\frac{\mathrm{twoS}-2}{2} \f$
 */
template <typename Real, int twoS, bool isPositive>
struct gammaRecursion {
    static constexpr Real value = gammaRecursion<Real, twoS - 2, isPositive>::value * (0.5f * twoS - 1);
};

/*!
 * Compute gamma function for negative half-integral s values using the recursion.
 * \f$ \Gamma[\frac{\mathrm{twoS}}{2}] = \frac{2\Gamma[\frac{\mathrm{twoS}_2}{2}]}{\mathrm{twoS}} \f$
 * Returns infinity (expressed as the largest value representable by Real) for \f$twoS = 0, -2, -4, -6, \ldots\f$ .
 */
template <typename Real, int twoS>
struct gammaRecursion<Real, twoS, false> {
    static constexpr Real value = gammaRecursion<Real, twoS + 2, false>::value == std::numeric_limits<Real>::max()
                                      ? std::numeric_limits<Real>::max()
                                      : gammaRecursion<Real, twoS + 2, false>::value / (0.5f * twoS);
};

/// Specific value of the Gamma function.
template <typename Real>
struct gammaRecursion<Real, 0, false> {
    static constexpr Real value = std::numeric_limits<Real>::max();
};

/// Specific value of the Gamma function.
template <typename Real>
struct gammaRecursion<Real, 1, true> {
    static constexpr Real value = SQRTPI;
};

/// Specific value of the Gamma function.
template <typename Real>
struct gammaRecursion<Real, 1, false> {
    static constexpr Real value = SQRTPI;
};

/// Specific value of the Gamma function.
template <typename Real>
struct gammaRecursion<Real, 2, true> {
    static constexpr Real value = 1;
};

/// Specific value of the Gamma function.
template <typename Real>
struct gammaRecursion<Real, 2, false> {
    static constexpr Real value = 1;
};

/*!
 * \class incompleteGammaComputer
 * \brief Computes the upper incomplete Gamma function.
 * \f$ \Gamma[s,x] = \int_x^\infty t^{s-1} e^{-t} \mathrm{d}t \f$
 * In this code we only need half integral arguments for \f$s\f$, and only positive \f$x\f$ arguments.
 * \tparam Real the floating point type to use for arithmetic.
 * \tparam twoS twice the s value required.
 */
template <typename Real, int twoS>
struct incompleteGammaComputer {
    /*!
     * \brief Computes the incomplete gamma function.
     * \param x value required.
     * \return \f$\Gamma[\frac{\mathrm{twoS}}{2}, x^2]\f$.
     */
    static Real compute(Real x) { return incompleteGammaRecursion<Real, twoS, (twoS > 0)>::compute(x); }
};

/*!
 * Compute upper incomplete gamma functions for positive half-integral s values using the recursion
 * \f$ \Gamma[\frac{\mathrm{twoS}}{2},x] = \Gamma[\frac{\mathrm{twoS}-2}{2},x] + x^{\frac{\mathrm{twoS}-2}{2}}e^{-x}\f$
 */
template <typename Real, int twoS, bool isPositive>
struct incompleteVirialGammaRecursion {
    static std::pair<Real, Real> compute(Real x) {
        Real gamma = incompleteGammaComputer<Real, twoS>::compute(x);
        return {gamma, (0.5f * twoS) * gamma + pow(x, (0.5f * twoS)) * exp(-x)};
    }
};

/*!
 * Compute upper incomplete gamma functions for negative half-integral s values using the recursion
 * \f$ \Gamma[\frac{\mathrm{twoS}}{2},x] = \frac{2\Gamma[\frac{\mathrm{twoS}+2}{2},x] -
 * 2x^\frac{\mathrm{twoS}}{2}e^{-x}}{\mathrm{twoS}}\f$
 */
template <typename Real, int twoS>
struct incompleteVirialGammaRecursion<Real, twoS, false> {
    static std::pair<Real, Real> compute(Real x) {
        Real gamma = incompleteGammaComputer<Real, twoS + 2>::compute(x);
        return {(gamma - pow(x, 0.5f * twoS) * exp(-x)) / (0.5f * twoS), gamma};
    }
};

/*!
 * \class incompleteGammaVirialComputer
 * \brief Computes the upper incomplete Gamma function for two different values: s and s+1.
 * \f$ \Gamma[s,x] = \int_x^\infty t^{s-1} e^{-t} \mathrm{d}t \f$
 * In this code we only need half integral arguments for \f$s\f$, and only positive \f$x\f$ arguments.
 * \tparam Real the floating point type to use for arithmetic.
 * \tparam twoS twice the s value required.
 */
template <typename Real, int twoS>
struct incompleteGammaVirialComputer {
    /*!
     * \brief Computes the incomplete gamma function for argument twoS and twoS+2.
     * \param x value required.
     * \return \f$\Gamma[\frac{\mathrm{twoS}}{2}, x]\f$ and \f$\Gamma[\frac{\mathrm{twoS+2}}{2}, x]\f$.
     */
    static std::pair<Real, Real> compute(Real x) {
        return incompleteVirialGammaRecursion<Real, twoS, (twoS >= 0)>::compute(x);
    }
};

/*!
 * \class gammaComputer
 * \brief Computes the Gamma function.
 * \f$ \Gamma[s] = \int_0^\infty t^{s-1} e^{-t} \mathrm{d}t \f$
 * In this code we only need half integral values for the \f$s\f$ argument, so the input
 * argument \f$s\f$ will yield \f$\Gamma[\frac{s}{2}]\f$.
 * \tparam Real the floating point type to use for arithmetic.
 * \tparam twoS twice the s value required.
 */
template <typename Real, int twoS>
struct gammaComputer {
    /// The value of \f$\Gamma[\frac{\mathrm{twos}}{2}]\f$
    static constexpr Real value = gammaRecursion<Real, twoS, (twoS > 0)>::value;
};

/*!
 * \brief Computes the Gamma function using recursion instead of template metaprogramming.
 * \f$ \Gamma[s] = \int_0^\infty t^{s-1} e^{-t} \mathrm{d}t \f$
 * In this code we only need half integral values for the \f$s\f$ argument, so the input
 * argument \f$s\f$ will yield \f$\Gamma[\frac{s}{2}]\f$.
 * \tparam Real the floating point type to use for arithmetic.
 * \param twoS twice the s value required.
 */
template <typename Real>
Real nonTemplateGammaComputer(int twoS) {
    if (twoS == 1) {
        return SQRTPI;
    } else if (twoS == 2) {
        return 1;
    } else if (twoS <= 0 && twoS % 2 == 0) {
        return std::numeric_limits<Real>::max();
    } else if (twoS > 0) {
        return nonTemplateGammaComputer<Real>(twoS - 2) * (0.5f * twoS - 1);
    } else {
        return nonTemplateGammaComputer<Real>(twoS + 2) / (0.5f * twoS);
    }
}

}  // Namespace helpme
#endif  // Header guard
// original file: ../src/gridsize.h

// BEGINLICENSE
//
// This file is part of helPME, which is distributed under the BSD 3-clause license,
// as described in the LICENSE file in the top level directory of this project.
//
// Author: Andrew C. Simmonett
//
// ENDLICENSE
#ifndef _HELPME_STANDALONE_GRIDSIZE_H_
#define _HELPME_STANDALONE_GRIDSIZE_H_

#include <algorithm>
#include <cmath>
#include <initializer_list>
#include <vector>

namespace helpme {

// N.B. The templates here are just to avoid multiple definitions in the .so file.

/*!
 * \brief allDivisors checks that a list of values are divisors of a given input value.
 * \param gridSize the gridSize to check for divisors.
 * \param requiredDivisors the list of divisors.
 * \return whether all listed values are divisors of gridSize.
 */
template <typename T>
bool allDivisors(T gridSize, const std::initializer_list<T> &requiredDivisors) {
    for (const T &divisor : requiredDivisors)
        if (gridSize % divisor) return false;
    return true;
}

/*!
 * \brief findGridSize FFTW likes to have transformations with dimensions of the form
 *
 *       a  b  c  d   e   f
 *      2  3  5  7  11  13
 *
 * where a,b,c and d are general and e+f is either 0 or 1. MKL has similar demands:
 *
 *   https://software.intel.com/en-us/articles/fft-length-and-layout-advisor/
 *   http://www.fftw.org/fftw3_doc/Real_002ddata-DFTs.html
 *
 * This routine will compute the next largest grid size subject to the constraint that the
 * resulting size is a multiple of a given factor.
 * \param inputSize the minimum size of the grid.
 * \param requiredDivisors list of values that must be a factor of the output grid size.
 * \return the adjusted grid size.
 */
template <typename T>
int findGridSize(T inputSize, const std::initializer_list<T> &requiredDivisors) {
    std::vector<int> primeFactors{2, 3, 5, 7};
    T minDivisor = std::min(requiredDivisors);
    T currentSize = minDivisor * std::ceil(static_cast<float>(inputSize) / minDivisor);
    while (true) {
        // Now we know that the grid size is a multiple of requiredFactor, check
        // that it satisfies the prime factor requirements stated above.
        T remainder = currentSize;
        for (const int &factor : primeFactors)
            while (remainder > 1 && remainder % factor == 0) remainder /= factor;
        if ((remainder == 1 || remainder == 11 || remainder == 13) && allDivisors(currentSize, requiredDivisors))
            return currentSize;
        currentSize += minDivisor;
    }
}

}  // Namespace helpme

#endif  // Header guard
// #include "matrix.h"
#if HAVE_MKL == 1
#include "mkl.h"
#endif
// #include "memory.h"
#if HAVE_MPI == 1
// original file: ../src/mpi_wrapper.h

// BEGINLICENSE
//
// This file is part of helPME, which is distributed under the BSD 3-clause license,
// as described in the LICENSE file in the top level directory of this project.
//
// Author: Andrew C. Simmonett
//
// ENDLICENSE
#ifndef _HELPME_STANDALONE_MPI_WRAPPER_H_
#define _HELPME_STANDALONE_MPI_WRAPPER_H_

#include <mpi.h>

#include <complex>
#include <iomanip>
#include <iostream>
#include <stdexcept>

namespace helpme {

/*!
 * \brief The MPITypes struct abstracts away the MPI_Datatype types for different floating point modes
 *        using templates to hide the details from the caller.
 */
template <typename Real>
struct MPITypes {
    MPI_Datatype realType_;
    MPI_Datatype complexType_;
    MPITypes() {
        throw std::runtime_error("MPI wrapper has not been implemented for the requested floating point type.");
    }
};

template <>
MPITypes<float>::MPITypes() : realType_(MPI_FLOAT), complexType_(MPI_C_COMPLEX) {}

template <>
MPITypes<double>::MPITypes() : realType_(MPI_DOUBLE), complexType_(MPI_C_DOUBLE_COMPLEX) {}

template <>
MPITypes<long double>::MPITypes() : realType_(MPI_LONG_DOUBLE), complexType_(MPI_C_LONG_DOUBLE_COMPLEX) {}

/*!
 * \brief The MPIWrapper struct is a lightweight C++ wrapper around the C MPI functions.  Its main
 *        purpose is to provide RAII semantics, ensuring that memory is correctly freed.  It also
 *        conveniently abstracts away the different MPI type descriptors for each floating point type.
 */
template <typename Real>
struct MPIWrapper {
    MPITypes<Real> types_;
    /// The MPI communicator instance to use for all reciprocal space work.
    MPI_Comm mpiCommunicator_;
    /// The total number of MPI nodes involved in reciprocal space work.
    int numNodes_;
    /// The MPI rank of this node.
    int myRank_;
    /// The number of nodes in the X direction.
    int numNodesX_;
    /// The number of nodes in the Y direction.
    int numNodesY_;
    /// The number of nodes in the Z direction.
    int numNodesZ_;

    void assertNodePartitioningValid(int numNodes, int numNodesX, int numNodesY, int numNodesZ) const {
        if (numNodes != numNodesX * numNodesY * numNodesZ)
            throw std::runtime_error(
                "Communicator world size does not match the numNodesX, numNodesY, numNodesZ passed in.");
    }

    MPIWrapper() : mpiCommunicator_(0), numNodes_(0), myRank_(0) {}
    MPIWrapper(const MPI_Comm& communicator, int numNodesX, int numNodesY, int numNodesZ)
        : numNodesX_(numNodesX), numNodesY_(numNodesY), numNodesZ_(numNodesZ) {
        if (MPI_Comm_dup(communicator, &mpiCommunicator_) != MPI_SUCCESS)
            throw std::runtime_error("Problem calling MPI_Comm_dup in MPIWrapper constructor.");
        if (MPI_Comm_size(mpiCommunicator_, &numNodes_) != MPI_SUCCESS)
            throw std::runtime_error("Problem calling MPI_Comm_size in MPIWrapper constructor.");
        if (MPI_Comm_rank(mpiCommunicator_, &myRank_) != MPI_SUCCESS)
            throw std::runtime_error("Problem calling MPI_Comm_rank in MPIWrapper constructor.");

        assertNodePartitioningValid(numNodes_, numNodesX, numNodesY, numNodesZ);
    }
    ~MPIWrapper() {
        if (mpiCommunicator_) MPI_Comm_free(&mpiCommunicator_);
    }

    /*!
     * \brief barrier wait for all members of this communicator to reach this point.
     */
    void barrier() {
        if (MPI_Barrier(mpiCommunicator_) != MPI_SUCCESS) throw std::runtime_error("Problem in MPI Barrier call!");
    }

    /*!
     * \brief split split this communicator into subgroups.
     * \param color the number identifying the subgroup the new communicator belongs to.
     * \param key the rank of the new communicator within the subgroup.
     * \return the new communicator.
     */
    std::unique_ptr<MPIWrapper> split(int color, int key) {
        std::unique_ptr<MPIWrapper> newWrapper(new MPIWrapper);
        if (MPI_Comm_split(mpiCommunicator_, color, key, &newWrapper->mpiCommunicator_) != MPI_SUCCESS)
            throw std::runtime_error("Problem calling MPI_Comm_split in MPIWrapper split.");
        if (MPI_Comm_size(newWrapper->mpiCommunicator_, &newWrapper->numNodes_) != MPI_SUCCESS)
            throw std::runtime_error("Problem calling MPI_Comm_size in MPIWrapper split.");
        if (MPI_Comm_rank(newWrapper->mpiCommunicator_, &newWrapper->myRank_) != MPI_SUCCESS)
            throw std::runtime_error("Problem calling MPI_Comm_rank in MPIWrapper split.");
        return newWrapper;
    }

    /*!
     * \brief allToAll perform alltoall communication within this communicator.
     * \param inBuffer the buffer containing input data.
     * \param outBuffer the buffer to send results to.
     * \param dimension the number of elements to be communicated.
     */
    void allToAll(std::complex<Real>* inBuffer, std::complex<Real>* outBuffer, int dimension) {
        if (MPI_Alltoall(inBuffer, 2 * dimension, types_.realType_, outBuffer, 2 * dimension, types_.realType_,
                         mpiCommunicator_) != MPI_SUCCESS)
            throw std::runtime_error("Problem encountered calling MPI alltoall.");
    }
    /*!
     * \brief allToAll perform alltoall communication within this communicator.
     * \param inBuffer the buffer containing input data.
     * \param outBuffer the buffer to send results to.
     * \param dimension the number of elements to be communicated.
     */
    void allToAll(Real* inBuffer, Real* outBuffer, int dimension) {
        if (MPI_Alltoall(inBuffer, dimension, types_.realType_, outBuffer, dimension, types_.realType_,
                         mpiCommunicator_) != MPI_SUCCESS)
            throw std::runtime_error("Problem encountered calling MPI alltoall.");
    }
    /*!
     * \brief reduce performs a reduction, with summation as the operation.
     * \param inBuffer the buffer containing input data.
     * \param outBuffer the buffer to send results to.
     * \param dimension the number of elements to be reduced.
     * \param node the node to reduce the result to (defaulted to zero).
     */
    void reduce(Real* inBuffer, Real* outBuffer, int dimension, int node = 0) {
        if (MPI_Reduce(inBuffer, outBuffer, dimension, types_.realType_, MPI_SUM, node, mpiCommunicator_) !=
            MPI_SUCCESS)
            throw std::runtime_error("Problem encountered calling MPI reduce.");
    }
    /*!
     * \brief reduceScatterBlock performs a reduction, with summation as the operation, then scatters to all nodes.
     * \param inBuffer the buffer containing input data.
     * \param outBuffer the buffer to send results to.
     * \param dimension the number of elements to be reduced on each node (currently must be the same on all nodes).
     */
    void reduceScatterBlock(Real* inBuffer, Real* outBuffer, int dimension) {
        if (MPI_Reduce_scatter_block(inBuffer, outBuffer, dimension, types_.realType_, MPI_SUM, mpiCommunicator_) !=
            MPI_SUCCESS)
            throw std::runtime_error("Problem encountered calling MPI reducescatter.");
    }
    /*!
     * \brief allGather broadcasts a chunk of data from each node to every other node.
     * \param inBuffer the buffer containing input data.
     * \param dimension the number of elements to be broadcast.
     * \param outBuffer the buffer to send results to.
     */
    void allGather(Real* inBuffer, Real* outBuffer, int dimension) {
        if (MPI_Allgather(inBuffer, dimension, types_.realType_, outBuffer, dimension, types_.realType_,
                          mpiCommunicator_) != MPI_SUCCESS)
            throw std::runtime_error("Problem encountered calling MPI allgather.");
    }

    /*!
     * \brief operator << a convenience wrapper around ostream, to inject node info.
     */
    friend std::ostream& operator<<(std::ostream& os, const MPIWrapper& obj) {
        os << "Node " << obj.myRank_ << " of " << obj.numNodes_ << ":" << std::endl;
        return os;
    }
};

// Adapter to allow piping of streams into unique_ptr-held object
template <typename Real>
std::ostream& operator<<(std::ostream& os, const std::unique_ptr<MPIWrapper<Real>>& obj) {
    os << *obj;
    return os;
}

// A convenience macro to guarantee that each node prints in order.
#define PRINT(out)                                                                                           \
    if (mpiCommunicator_) {                                                                                  \
        for (int node = 0; node < mpiCommunicator_->numNodes_; ++node) {                                     \
            std::cout.setf(std::ios::fixed, std::ios::floatfield);                                           \
            if (node == mpiCommunicator_->myRank_)                                                           \
                std::cout << mpiCommunicator_ << std::setw(18) << std::setprecision(10) << out << std::endl; \
            mpiCommunicator_->barrier();                                                                     \
        };                                                                                                   \
    } else {                                                                                                 \
        std::cout << std::setw(18) << std::setprecision(10) << out << std::endl;                             \
    }

}  // Namespace helpme
#endif  // Header guard
#endif
// original file: ../src/powers.h

// BEGINLICENSE
//
// This file is part of helPME, which is distributed under the BSD 3-clause license,
// as described in the LICENSE file in the top level directory of this project.
//
// Author: Andrew C. Simmonett
//
// ENDLICENSE
#ifndef _HELPME_STANDALONE_POWERS_H_
#define _HELPME_STANDALONE_POWERS_H_

#include <cmath>

/*!
 * \file powers.h
 * \brief Contains template functions to compute various quantities raised to an integer power.
 */

namespace helpme {

template <typename Real, int n>
struct raiseToIntegerPower {
    static Real pow(Real val) { return val * raiseToIntegerPower<Real, n - 1>::pow(val); }
};

/// Base recursion for the power.
template <typename Real>
struct raiseToIntegerPower<Real, 0> {
    static Real pow(Real) { return 1; }
};

/// n is positive and even case
template <typename Real, int n, bool nIsPositive, bool nIsEven>
struct normIntegerPowerComputer {
    static Real compute(Real val) { return raiseToIntegerPower<Real, n / 2>::pow(val); }
};

/// n is positive and odd case
template <typename Real, int n>
struct normIntegerPowerComputer<Real, n, true, false> {
    static Real compute(Real val) { return raiseToIntegerPower<Real, n>::pow(std::sqrt(val)); }
};

/// n is negative and even case
template <typename Real, int n>
struct normIntegerPowerComputer<Real, n, false, true> {
    static Real compute(Real val) { return raiseToIntegerPower<Real, -n / 2>::pow(1 / val); }
};

/// n is negative and odd case
template <typename Real, int n>
struct normIntegerPowerComputer<Real, n, false, false> {
    static Real compute(Real val) { return raiseToIntegerPower<Real, -n>::pow(1 / sqrt(val)); }
};

/*!
 * \brief Compute a quantity exponentiated by an integer power, using multiplication,
 * at compile time.  The exponent is assumed to be positve.
 * \tparam Real the floating point type to use for arithmetic.
 * \tparam n the exponent to raise the value to.
 */
template <typename Real, int n>
struct raiseNormToIntegerPower {
    /*!
     * \brief pow compute the norm raised to the power n.
     * \param val the square of the norm to be exponentiated.
     * \return the norm raised to the integer power.
     */
    static Real compute(Real val) { return normIntegerPowerComputer<Real, n, (n >= 0), (n % 2 == 0)>::compute(val); }
};
}  // Namespace helpme

#endif  // Header guard
// original file: ../src/splines.h

// BEGINLICENSE
//
// This file is part of helPME, which is distributed under the BSD 3-clause license,
// as described in the LICENSE file in the top level directory of this project.
//
// Author: Andrew C. Simmonett
//
// ENDLICENSE
#ifndef _HELPME_STANDALONE_SPLINES_H_
#define _HELPME_STANDALONE_SPLINES_H_

// #include "matrix.h"

/*!
 * \file splines.h
 * \brief Contains the C++ implementation of a cardinal B-Splines.
 */

namespace helpme {

/*!
 * \class BSpline
 * \brief A class to compute cardinal B-splines. This code can compute arbitrary-order B-splines of
 *        arbitrary derivative level, subject to the usual constraint that an order m spline is
 *        differentiable m-2 times.
 * \tparam Real the floating point type to use for arithmetic.
 */
template <typename Real>
class BSpline {
   protected:
    /// The order of this B-spline.
    short order_;
    /// The maximum derivative level for this B-spline.
    short derivativeLevel_;
    /// B-Splines with rows corresponding to derivative level, and columns to spline component.
    Matrix<Real> splines_;
    /// The grid point at which to start interpolation.
    short startingGridPoint_;

    /// Makes B-Spline array.
    inline void makeSplineInPlace(Real *array, const Real &val, const short &n) const {
        Real denom = (Real)1 / (n - 1);
        array[n - 1] = denom * val * array[n - 2];
        for (short j = 1; j < n - 1; ++j)
            array[n - j - 1] = denom * ((val + j) * array[n - j - 2] + (n - j - val) * array[n - j - 1]);
        array[0] *= denom * (1 - val);
    }

    /// Takes BSpline derivative.
    inline void differentiateSpline(const Real *array, Real *dArray, const short &n) const {
        dArray[0] = -array[0];
        for (short j = 1; j < n - 1; ++j) dArray[j] = array[j - 1] - array[j];
        dArray[n - 1] = array[n - 2];
    }

    /*!
     * \brief assertSplineIsSufficient ensures that the spline is large enough to be differentiable.
     *        An mth order B-Spline is differentiable m-2 times.
     */
    void assertSplineIsSufficient(int splineOrder, int derivativeLevel) const {
        if (splineOrder - derivativeLevel < 2) {
            std::string msg(
                "The spline order used is not sufficient for the derivative level requested."
                "Set the spline order to at least ");
            msg += std::to_string(derivativeLevel + 2);
            msg += " to run this calculation.";
            throw std::runtime_error(msg);
        }
    }

   public:
    /// The B-splines and their derivatives.  See update() for argument details.
    BSpline(short start, Real value, short order, short derivativeLevel) : splines_(derivativeLevel + 1, order) {
        update(start, value, order, derivativeLevel);
    }

    /*!
     * \brief update computes information for BSpline, without reallocating memory unless needed.
     * \param start the grid point at which to start interpolation.
     * \param value the distance (in fractional coordinates) from the starting grid point.
     * \param order the order of the BSpline.
     * \param derivativeLevel the maximum level of derivative needed for this BSpline.
     */
    void update(short start, Real value, short order, short derivativeLevel) {
        assertSplineIsSufficient(order, derivativeLevel);
        startingGridPoint_ = start;
        order_ = order;
        derivativeLevel_ = derivativeLevel;

        // The +1 is to account for the fact that we need to store entries up to and including the max.
        if (splines_.nRows() < derivativeLevel + 1 || splines_.nCols() != order)
            splines_ = Matrix<Real>(derivativeLevel + 1, order);

        splines_.setZero();
        splines_(0, 0) = 1 - value;
        splines_(0, 1) = value;
        for (short m = 1; m < order_ - 1; ++m) {
            makeSplineInPlace(splines_[0], value, m + 2);
            if (m >= order_ - derivativeLevel_ - 2) {
                short currentDerivative = order_ - m - 2;
                for (short l = 0; l < currentDerivative; ++l)
                    differentiateSpline(splines_[l], splines_[l + 1], m + 2 + currentDerivative);
            }
        }
    }

    BSpline() {}

    /*!
     * \brief The modulus of the B-Spline in Fourier space.
     * \param gridDim the dimension of the grid in the dimension this spline is to be used.
     * \param mValues if provided, provides the ordering of the m values, if not they are
     *        ordered as 0, 1, 2, ..., Kmax, -Kmax+1, -Kmax+2, ..., -2, -1.
     * \return a gridDim long vector containing the inverse of the Fourier space spline moduli.
     */
    helpme::vector<Real> invSplineModuli(short gridDim, std::vector<int> mValues = {}) {
        int nKTerms = mValues.size() ? mValues.size() : gridDim;
        helpme::vector<Real> splineMods(nKTerms, 0);
        Real prefac = 2 * M_PI / gridDim;
        for (int m = 0; m < nKTerms; ++m) {
            Real real = 0;
            Real imag = 0;
            int mValue = mValues.size() ? mValues[m] : m;
            for (int n = 0; n < order_; ++n) {
                Real exparg = mValue * n * prefac;
                Real jSpline = splines_(0, n);
                real += jSpline * cos(exparg);
                imag += jSpline * sin(exparg);
            }
            splineMods[m] = real * real + imag * imag;
        }

        // Correct tiny values for conventional PME.
        if (!mValues.size()) {
            constexpr Real EPS = 1e-7f;
            if (splineMods[0] < EPS) splineMods[0] = splineMods[1] / 2;
            for (int i = 0; i < gridDim - 1; ++i)
                if (splineMods[i] < EPS) splineMods[i] = (splineMods[i - 1] + splineMods[i + 1]) / 2;
            if (splineMods[gridDim - 1] < EPS) splineMods[gridDim - 1] = splineMods[gridDim - 2] / 2;
        }

        // Invert, to avoid division later on.
        for (int i = 0; i < nKTerms; ++i) splineMods[i] = 1 / splineMods[i];

        return splineMods;
    }

    /*!
     * \brief Gets the grid point to start interpolating from.
     * \return the index of the first grid point this spline supports.
     */
    short startingGridPoint() const { return startingGridPoint_; }

    /*!
     * \brief Returns the B-Spline, or derivative thereof.
     * \param deriv the derivative level of the spline to be returned.
     */
    const Real *operator[](const int &deriv) const { return splines_[deriv]; }

    /*!
     * \brief Get read-only access to the full spline data.
     * \returns a const reference to the full spline data: row index is derivative, col index is spline component.
     */
    const Matrix<Real> &splineData() const { return splines_; }
};

}  // Namespace helpme
#endif  // Header guard
// #include "string_utils.h"
// original file: ../src/tensor_utils.h

// BEGINLICENSE
//
// This file is part of helPME, which is distributed under the BSD 3-clause license,
// as described in the LICENSE file in the top level directory of this project.
//
// Author: Andrew C. Simmonett
//
// ENDLICENSE
#ifndef _HELPME_STANDALONE_TENSOR_UTILS_H_
#define _HELPME_STANDALONE_TENSOR_UTILS_H_

#if HAVE_BLAS == 1
extern "C" {
extern void dgemm_(char *, char *, int *, int *, int *, double *, double *, int *, double *, int *, double *, double *,
                   int *);
extern void sgemm_(char *, char *, int *, int *, int *, float *, float *, int *, float *, int *, float *, float *,
                   int *);
}
#endif

namespace helpme {

/*!
 * \brief Sorts a 3D tensor stored contiguously as ABC into CBA order.
 * \param abcPtr the address of the incoming ABC ordered tensor.
 * \param aDimension the dimension of the A index.
 * \param bDimension the dimension of the B index.
 * \param cDimension the dimension of the C index.
 * \param cbaPtr the address of the outgoing CBA ordered tensor.
 * \param nThreads the number of parallel threads to use.
 */
template <typename Real>
void permuteABCtoCBA(Real const *__restrict__ abcPtr, int const aDimension, int const bDimension, int const cDimension,
                     Real *__restrict__ cbaPtr, size_t nThreads = 1) {
#pragma omp parallel for num_threads(nThreads)
    for (int C = 0; C <= -1 + cDimension; ++C)
        for (int B = 0; B <= -1 + bDimension; ++B)
            for (int A = 0; A <= -1 + aDimension; ++A)
                cbaPtr[aDimension * bDimension * C + aDimension * B + A] =
                    abcPtr[cDimension * bDimension * A + cDimension * B + C];
}

/*!
 * \brief Sorts a 3D tensor stored contiguously as ABC into ACB order.
 * \param abcPtr the address of the incoming ABC ordered tensor.
 * \param aDimension the dimension of the A index.
 * \param bDimension the dimension of the B index.
 * \param cDimension the dimension of the C index.
 * \param acbPtr the address of the outgoing ACB ordered tensor.
 * \param nThreads the number of parallel threads to use.
 */
template <typename Real>
void permuteABCtoACB(Real const *__restrict__ abcPtr, int const aDimension, int const bDimension, int const cDimension,
                     Real *__restrict__ acbPtr, size_t nThreads = 1) {
#pragma omp parallel for num_threads(nThreads)
    for (int A = 0; A <= -1 + aDimension; ++A)
        for (int C = 0; C <= -1 + cDimension; ++C)
            for (int B = 0; B <= -1 + bDimension; ++B)
                acbPtr[bDimension * cDimension * A + bDimension * C + B] =
                    abcPtr[cDimension * bDimension * A + cDimension * B + C];
}

/*!
 * \brief Contracts an ABxC tensor with a DxC tensor, to produce an ABxD quantity.
 * \param abcPtr the address of the incoming ABxC tensor.
 * \param dcPtr the address of the incoming DxC tensor.
 * \param abDimension the dimension of the AB index.
 * \param cDimension the dimension of the C index.
 * \param dDimension the dimension of the D index.
 * \param abdPtr the address of the outgoing ABD tensor.
 */
template <typename Real>
void contractABxCWithDxC(Real const *__restrict__ abcPtr, Real const *__restrict__ dcPtr, int const abDimension,
                         int const cDimension, int const dDimension, Real *__restrict__ abdPtr) {
    Real acc_C;
    for (int AB = 0; AB <= -1 + abDimension; ++AB) {
        for (int D = 0; D <= -1 + dDimension; ++D) {
            acc_C = 0;
            for (int C = 0; C <= -1 + cDimension; ++C)
                acc_C = acc_C + abcPtr[cDimension * AB + C] * dcPtr[cDimension * D + C];
            abdPtr[dDimension * AB + D] = acc_C;
        }
    }
}

#if HAVE_BLAS == 1
template <>
void contractABxCWithDxC<float>(float const *__restrict__ abcPtr, float const *__restrict__ dcPtr,
                                int const abDimension, int const cDimension, int const dDimension,
                                float *__restrict__ abdPtr) {
    if (abDimension == 0 || cDimension == 0 || dDimension == 0) return;

    char transB = 't';
    char transA = 'n';
    float alpha = 1;
    float beta = 0;
    sgemm_(&transB, &transA, const_cast<int *>(&dDimension), const_cast<int *>(&abDimension),
           const_cast<int *>(&cDimension), &alpha, const_cast<float *>(dcPtr), const_cast<int *>(&cDimension),
           const_cast<float *>(abcPtr), const_cast<int *>(&cDimension), &beta, abdPtr, const_cast<int *>(&dDimension));
}

template <>
void contractABxCWithDxC<double>(double const *__restrict__ abcPtr, double const *__restrict__ dcPtr,
                                 int const abDimension, int const cDimension, int const dDimension,
                                 double *__restrict__ abdPtr) {
    if (abDimension == 0 || cDimension == 0 || dDimension == 0) return;

    char transB = 't';
    char transA = 'n';
    double alpha = 1;
    double beta = 0;
    dgemm_(&transB, &transA, const_cast<int *>(&dDimension), const_cast<int *>(&abDimension),
           const_cast<int *>(&cDimension), &alpha, const_cast<double *>(dcPtr), const_cast<int *>(&cDimension),
           const_cast<double *>(abcPtr), const_cast<int *>(&cDimension), &beta, abdPtr, const_cast<int *>(&dDimension));
}
#endif

}  // Namespace helpme
#endif  // Header guard

/*!
 * \file helpme.h
 * \brief Contains the C++ implementation of a PME Instance, and related helper classes.
 */

namespace helpme {

/*!
 * \brief nCartesian computes the total number of Cartesian components of a given angular momentum.
 * \param L the angular momentum.
 * \return total number of components up to and including angular momentum L.
 */
static int nCartesian(int L) { return (L + 1) * (L + 2) * (L + 3) / 6; }

/*!
 * \brief cartAddress computes the address of a term with given quantum numbers in a Cartesian buffer.
 * \param lx the x quantum number.
 * \param ly the y quantum number.
 * \param lz the z quantum number.
 * \return the address of an {lx, ly, lz} quantity in a buffer that contains all lower angular momentum terms too.
 */
static int cartAddress(int lx, int ly, int lz) {
    int l = lx + ly + lz;
    return l * (l + 1) * (l + 2) / 6 + lz * (l * 2 - lz + 3) / 2 + ly;
}

// This is used to define function pointers in the constructor, and makes it easy to add new kernels.
#define ENABLE_KERNEL_WITH_INVERSE_R_EXPONENT_OF(n)                  \
    case n:                                                          \
        convolveEVFxn_ = &convolveEVImpl<n>;                         \
        convolveEVCompressedFxn_ = &convolveEVCompressedImpl<n>;     \
        cacheInfluenceFunctionFxn_ = &cacheInfluenceFunctionImpl<n>; \
        slfEFxn_ = &slfEImpl<n>;                                     \
        dirEFxn_ = &dirEImpl<n>;                                     \
        adjEFxn_ = &adjEImpl<n>;                                     \
        dirEFFxn_ = &dirEFImpl<n>;                                   \
        adjEFFxn_ = &adjEFImpl<n>;                                   \
        break;

/*!
 * \class splineCacheEntry
 * \brief A placeholder to encapsulate information about a given atom's splines
 */
template <typename Real>
struct SplineCacheEntry {
    BSpline<Real> aSpline, bSpline, cSpline;
    int absoluteAtomNumber;
    SplineCacheEntry(int order, int derivativeLevel)
        : aSpline(0, 0, order, derivativeLevel),
          bSpline(0, 0, order, derivativeLevel),
          cSpline(0, 0, order, derivativeLevel),
          absoluteAtomNumber(-1) {}
};

/*!
 * \class PMEInstance
 * \brief A class to encapsulate information related to a particle mesh Ewald calculation.
 *
 * By storing information related to a single PME calculation in this way, we allow multiple
 * instances to be created in calculations requiring multiple PMEs, e.g. for computing both
 * electrostatic and attractive dispersion terms using PME to handle long-range interactions.
 * \tparam Real the floating point type to use for arithmetic.
 */
template <typename Real, typename std::enable_if<std::is_floating_point<Real>::value, int>::type = 0>
class PMEInstance {
    using GridIterator = std::vector<std::vector<std::pair<short, short>>>;
    using Complex = std::complex<Real>;
    using Spline = BSpline<Real>;
    using RealMat = Matrix<Real>;
    using RealVec = helpme::vector<Real>;

   public:
    /*!
     * \brief The algorithm being used to solve for the reciprocal space quantities.
     */
    enum class AlgorithmType : int { Undefined = 0, PME = 1, CompressedPME = 2 };

    /*!
     * \brief The different conventions for orienting a lattice constructed from input parameters.
     */
    enum class LatticeType : int { Undefined = 0, XAligned = 1, ShapeMatrix = 2 };

    /*!
     * \brief The different conventions for numbering nodes.
     */
    enum class NodeOrder : int { Undefined = 0, ZYX = 1 };

   protected:
    /// The FFT grid dimensions in the {A,B,C} grid dimensions.
    int gridDimensionA_ = 0, gridDimensionB_ = 0, gridDimensionC_ = 0;
    /// The number of K vectors in the {A,B,C} dimensions.  Equal to dim{A,B,C} for PME, lower for cPME.
    int numKSumTermsA_ = 0, numKSumTermsB_ = 0, numKSumTermsC_ = 0;
    /// The number of K vectors in the {A,B,C} dimensions to be handled by this node in a parallel setup.
    int myNumKSumTermsA_ = 0, myNumKSumTermsB_ = 0, myNumKSumTermsC_ = 0;
    /// The full A dimension after real->complex transformation.
    int complexGridDimensionA_ = 0;
    /// The locally owned A dimension after real->complex transformation.
    int myComplexGridDimensionA_ = 0;
    /// The order of the cardinal B-Spline used for interpolation.
    int splineOrder_ = 0;
    /// The actual number of threads per MPI instance, and the number requested previously.
    int nThreads_ = -1, requestedNumberOfThreads_ = -1;
    /// The exponent of the (inverse) interatomic distance used in this kernel.
    int rPower_ = 0;
    /// The scale factor to apply to all energies and derivatives.
    Real scaleFactor_ = 0;
    /// The attenuation parameter, whose units should be the inverse of those used to specify coordinates.
    Real kappa_ = 0;
    /// The lattice vectors.
    RealMat boxVecs_ = RealMat(3, 3);
    /// The reciprocal lattice vectors.
    RealMat recVecs_ = RealMat(3, 3);
    /// The scaled reciprocal lattice vectors, for transforming forces from scaled fractional coordinates.
    RealMat scaledRecVecs_ = RealMat(3, 3);
    /// A list of the number of splines handle by each thread on this node.
    std::vector<size_t> numAtomsPerThread_;
    /// An iterator over angular momentum components.
    std::vector<std::array<short, 3>> angMomIterator_;
    /// From a given starting point on the {A,B,C} edge of the grid, lists all points to be handled, correctly wrapping
    /// around the end.
    GridIterator gridIteratorA_, gridIteratorB_, gridIteratorC_;
    /// The grid iterator for the C dimension, divided up by threads to avoid race conditions in parameter spreading.
    std::vector<GridIterator> threadedGridIteratorC_;
    /// The (inverse) bspline moduli to normalize the spreading / probing steps; these are folded into the convolution.
    RealVec splineModA_, splineModB_, splineModC_;
    /// The cached influence function involved in the convolution.
    RealVec cachedInfluenceFunction_;
    /// A function pointer to call the approprate function to implement convolution with virial for conventional PME,
    /// templated to the rPower value.
    std::function<Real(bool, int, int, int, int, int, int, int, Real, Complex *, const RealMat &, Real, Real,
                       const Real *, const Real *, const Real *, const int *, const int *, const int *, RealMat &, int)>
        convolveEVFxn_;
    /// A function pointer to call the approprate function to implement convolution with virial for comporessed PME,
    /// templated to the rPower value.
    std::function<Real(int, int, int, int, int, int, Real, const Real *, Real *, const RealMat &, Real, Real,
                       const Real *, const Real *, const Real *, const int *, const int *, const int *, RealMat &, int)>
        convolveEVCompressedFxn_;
    /// A function pointer to call the approprate function to implement cacheing of the influence function that appears
    //  in the convolution, templated to the rPower value.
    std::function<void(int, int, int, int, int, int, Real, RealVec &, const RealMat &, Real, Real, const Real *,
                       const Real *, const Real *, const int *, const int *, const int *, int)>
        cacheInfluenceFunctionFxn_;
    /// A function pointer to call the approprate function to compute self energy, templated to the rPower value.
    std::function<Real(int, const RealMat &, Real, Real)> slfEFxn_;
    /// A function pointer to call the approprate function to compute the direct energy, templated to the rPower value.
    std::function<Real(Real, Real)> dirEFxn_;
    /// A function pointer to call the approprate function to compute the adjusted energy, templated to the rPower
    /// value.
    std::function<Real(Real, Real)> adjEFxn_;
    /// A function pointer to call the approprate function to compute the direct energy and force, templated to the
    /// rPower value.
    std::function<std::tuple<Real, Real>(Real, Real, Real)> dirEFFxn_;
    /// A function pointer to call the approprate function to compute the adjusted energy and force, templated to the
    /// rPower value.
    std::function<std::tuple<Real, Real>(Real, Real, Real)> adjEFFxn_;
#if HAVE_MPI == 1
    /// The communicator object that handles interactions with MPI.
    std::unique_ptr<MPIWrapper<Real>> mpiCommunicator_;
    /// The communicator object that handles interactions with MPI along this nodes {A,B,C} pencils.
    std::unique_ptr<MPIWrapper<Real>> mpiCommunicatorA_, mpiCommunicatorB_, mpiCommunicatorC_;
#endif
    /// The number of nodes in the {A,B,C} dimensions.
    int numNodesA_ = 1, numNodesB_ = 1, numNodesC_ = 1;
    /// The rank of this node along the {A,B,C} dimensions.
    int myNodeRankA_ = 0, myNodeRankB_ = 0, myNodeRankC_ = 0;
    /// The first grid point that this node is responsible for in the {A,B,C} dimensions.
    int myFirstGridPointA_ = 0, myFirstGridPointB_ = 0, myFirstGridPointC_ = 0;
    /// The first K sum term that this node is responsible for.
    int firstKSumTermA_ = 0, firstKSumTermB_ = 0, firstKSumTermC_ = 0;
    /// The {X,Y,Z} dimensions of the locally owned chunk of the grid.
    int myGridDimensionA_ = 0, myGridDimensionB_ = 0, myGridDimensionC_ = 0;
    /// The subsets of a given dimension to be processed when doing a transform along another dimension.
    int subsetOfCAlongA_ = 0, subsetOfCAlongB_ = 0, subsetOfBAlongC_ = 0;
    /// The size of a cache line, in units of the size of the Real type, to allow better memory allocation policies.
    Real cacheLineSizeInReals_ = 0;
    /// The current unit cell parameters.
    Real cellA_ = 0, cellB_ = 0, cellC_ = 0, cellAlpha_ = 0, cellBeta_ = 0, cellGamma_ = 0;
    /// Whether the unit cell parameters have been changed, invalidating cached gF quantities.
    bool unitCellHasChanged_ = true;
    /// Whether the kappa has been changed, invalidating kappa-dependent quantities.
    bool kappaHasChanged_ = true;
    /// Whether any of the grid dimensions have changed.
    bool gridDimensionHasChanged_ = true;
    /// Whether any of the reciprocal sum dimensions have changed.
    bool reciprocalSumDimensionHasChanged_ = true;
    /// Whether the algorithm to be used has changed.
    bool algorithmHasChanged_ = true;
    /// Whether the spline order has changed.
    bool splineOrderHasChanged_ = true;
    /// Whether the scale factor has changed.
    bool scaleFactorHasChanged_ = true;
    /// Whether the power of R has changed.
    bool rPowerHasChanged_ = true;
    /// Whether the parallel node setup has changed in any way.
    bool numNodesHasChanged_ = true;
    /// The algorithm being used to solve for reciprocal space quantities.
    AlgorithmType algorithmType_ = AlgorithmType::Undefined;
    /// The type of alignment scheme used for the lattice vectors.
    LatticeType latticeType_ = LatticeType::Undefined;
    /// Communication buffers for MPI parallelism.
    helpme::vector<Complex> workSpace1_, workSpace2_;
    /// FFTW wrappers to help with transformations in the {A,B,C} dimensions.
    FFTWWrapper<Real> fftHelperA_, fftHelperB_, fftHelperC_;
    /// The cached list of splines.
    std::vector<SplineCacheEntry<Real>> splineCache_;
    /// A scratch array for each threads to use as storage when probing the grid.
    RealMat fractionalPhis_;
    /// A list of the splines that each thread should handle.
    std::vector<std::list<size_t>> splinesPerThread_;
    /// The transformation matrices for the compressed PME algorithms, in the {A,B,C} dimensions.
    RealMat compressionCoefficientsA_, compressionCoefficientsB_, compressionCoefficientsC_;
    /// Iterators that define the reciprocal lattice sums over each index, correctly defining -1/2 <= m{A,B,C} < 1/2.
    std::vector<int> mValsA_, mValsB_, mValsC_;
    /// A temporary list used in the assigning of atoms to threads and resorting by starting grid point.
    std::vector<std::set<std::pair<uint32_t, uint32_t>>> gridAtomList_;

    /*!
     * \brief makeGridIterator makes an iterator over the spline values that contribute to this node's grid
     *        in a given Cartesian dimension.  The iterator is of the form (grid point, spline index) and is
     *        sorted by increasing grid point, for cache efficiency.
     * \param dimension the dimension of the grid in the Cartesian dimension of interest.
     * \param first the first grid point in the Cartesian dimension to be handled by this node.
     * \param last the element past the last grid point in the Cartesian dimension to be handled by this node.
     * \param paddingSize the size of the "halo" region around this grid onto which the charge can be spread
     *        that really belongs to neighboring nodes.  For compressed PME we assume that each node handles
     *        only its own atoms and spreads onto an expanded grid to account for this.  In regular PME there
     *        is no padding because we assume that all halo atoms are present on this node before spreading.
     * \return the vector of spline iterators for each starting grid point.
     */
    GridIterator makeGridIterator(int dimension, int first, int last, int paddingSize) const {
        GridIterator gridIterator;
        if (paddingSize) {
            // This version assumes that every atom on this node is blindly place on the
            // grid, requiring that a padding area of size splineOrder-1 be present.
            for (int gridStart = 0; gridStart < dimension; ++gridStart) {
                std::vector<std::pair<short, short>> splineIterator(splineOrder_);
                splineIterator.clear();
                if (gridStart >= first && gridStart < last - paddingSize) {
                    for (int splineIndex = 0; splineIndex < splineOrder_; ++splineIndex) {
                        int gridPoint = (splineIndex + gridStart);
                        splineIterator.push_back(std::make_pair(gridPoint - first, splineIndex));
                    }
                }
                splineIterator.shrink_to_fit();
                gridIterator.push_back(splineIterator);
            }
        } else {
            // This version assumes that each node has its own atoms, plus "halo" atoms
            // from neighboring grids that can contribute to this node's grid.
            for (int gridStart = 0; gridStart < dimension; ++gridStart) {
                std::vector<std::pair<short, short>> splineIterator(splineOrder_);
                splineIterator.clear();
                for (int splineIndex = 0; splineIndex < splineOrder_; ++splineIndex) {
                    int gridPoint = (splineIndex + gridStart) % dimension;
                    if (gridPoint >= first && gridPoint < last)
                        splineIterator.push_back(std::make_pair(gridPoint - first, splineIndex));
                }
                splineIterator.shrink_to_fit();
                std::sort(splineIterator.begin(), splineIterator.end());
                gridIterator.push_back(splineIterator);
            }
        }
        gridIterator.shrink_to_fit();
        return gridIterator;
    }

    /*! Make sure that the iterator over AM components is up to date.
     * \param angMom the angular momentum required for the iterator over multipole components.
     */
    void updateAngMomIterator(int parameterAngMom) {
        auto L = parameterAngMom;
        size_t expectedNTerms = nCartesian(L);
        if (angMomIterator_.size() >= expectedNTerms) return;

        angMomIterator_.resize(expectedNTerms);
        for (short l = 0, count = 0; l <= L; ++l) {
            for (short lz = 0; lz <= l; ++lz) {
                for (short ly = 0; ly <= l - lz; ++ly) {
                    short lx = l - ly - lz;
                    angMomIterator_[count] = {{static_cast<short>(lx), static_cast<short>(ly), static_cast<short>(lz)}};
                    ++count;
                }
            }
        }
    }

    /*!
     * \brief updateInfluenceFunction builds the gF array cache, if the lattice vector has changed since the last
     *                                build of it.  If the cell is unchanged, this does nothing.  This is handled
     *                                separately from other initializations because we may skip the cacheing of
     *                                the influence function when the virial is requested; we assume it's an NPT
     *                                calculation in this case and therefore the influence function changes every time.
     */
    void updateInfluenceFunction() {
        if (unitCellHasChanged_ || kappaHasChanged_ || reciprocalSumDimensionHasChanged_ || splineOrderHasChanged_ ||
            scaleFactorHasChanged_ || numNodesHasChanged_ || algorithmHasChanged_) {
            cacheInfluenceFunctionFxn_(myNumKSumTermsA_, myNumKSumTermsB_, myNumKSumTermsC_, firstKSumTermA_,
                                       firstKSumTermB_, firstKSumTermC_, scaleFactor_, cachedInfluenceFunction_,
                                       recVecs_, cellVolume(), kappa_, &splineModA_[0], &splineModB_[0],
                                       &splineModC_[0], mValsA_.data(), mValsB_.data(), mValsC_.data(), nThreads_);
        }
    }

    /*!
     * \brief Spreads parameters onto the grid for a single atom
     * \param atom the absolute atom number.
     * \param realGrid pointer to the array containing the grid in CBA order
     * \param nComponents the number of angular momentum components in the parameters.
     * \param nForceComponents the number of angular momentum components in the parameters with one extra
     *        level of angular momentum to permit evaluation of forces.
     * \param splineA the BSpline object for the A direction.
     * \param splineB the BSpline object for the B direction.
     * \param splineC the BSpline object for the C direction.
     * \param parameters the list of parameters associated with each atom (charges, C6 coefficients, multipoles,
     * etc...). For a parameter with angular momentum L, a matrix of dimension nAtoms x nL is expected, where nL =
     * (L+1)*(L+2)*(L+3)/6 and the fast running index nL has the ordering
     *
     * 0 X Y Z XX XY YY XZ YZ ZZ XXX XXY XYY YYY XXZ XYZ YYZ XZZ YZZ ZZZ ...
     *
     * i.e. generated by the python loops
     * \code{.py}
     * for L in range(maxAM+1):
     *     for Lz in range(0,L+1):
     *         for Ly in range(0, L - Lz + 1):
     *              Lx  = L - Ly - Lz
     * \endcode
     * \param thread the ID of the thread handling this term.
     */
    void spreadParametersImpl(const int &atom, Real *realGrid, const int &nComponents, const Spline &splineA,
                              const Spline &splineB, const Spline &splineC, const RealMat &parameters, int thread) {
        const auto &aGridIterator = gridIteratorA_[splineA.startingGridPoint()];
        const auto &bGridIterator = gridIteratorB_[splineB.startingGridPoint()];
        const auto &cGridIterator = threadedGridIteratorC_[thread][splineC.startingGridPoint()];
        int numPointsA = static_cast<int>(aGridIterator.size());
        int numPointsB = static_cast<int>(bGridIterator.size());
        int numPointsC = static_cast<int>(cGridIterator.size());
        const auto *iteratorDataA = aGridIterator.data();
        const auto *iteratorDataB = bGridIterator.data();
        const auto *iteratorDataC = cGridIterator.data();
        for (int component = 0; component < nComponents; ++component) {
            const auto &quanta = angMomIterator_[component];
            Real param = parameters(atom, component);
            const Real *splineValsA = splineA[quanta[0]];
            const Real *splineValsB = splineB[quanta[1]];
            const Real *splineValsC = splineC[quanta[2]];
            for (int pointC = 0; pointC < numPointsC; ++pointC) {
                const auto &cPoint = iteratorDataC[pointC];
                Real cValP = param * splineValsC[cPoint.second];
                for (int pointB = 0; pointB < numPointsB; ++pointB) {
                    const auto &bPoint = iteratorDataB[pointB];
                    Real cbValP = cValP * splineValsB[bPoint.second];
                    Real *cbRow = realGrid + cPoint.first * myGridDimensionB_ * myGridDimensionA_ +
                                  bPoint.first * myGridDimensionA_;
                    for (int pointA = 0; pointA < numPointsA; ++pointA) {
                        const auto &aPoint = iteratorDataA[pointA];
                        cbRow[aPoint.first] += cbValP * splineValsA[aPoint.second];
                    }
                }
            }
        }
    }

    /*!
     * \brief Probes the grid and computes the force for a single atom, specialized for zero parameter angular momentum.
     * \param potentialGrid pointer to the array containing the potential, in ZYX order.
     * \param splineA the BSpline object for the A direction.
     * \param splineB the BSpline object for the B direction.
     * \param splineC the BSpline object for the C direction.
     * \param parameter the list of parameter associated with the given atom.
     * \param forces a 3 vector of the forces for this atom, ordered in memory as {Fx, Fy, Fz}.
     */
    void probeGridImpl(const Real *potentialGrid, const Spline &splineA, const Spline &splineB, const Spline &splineC,
                       const Real &parameter, Real *forces) const {
        const auto &aGridIterator = gridIteratorA_[splineA.startingGridPoint()];
        const auto &bGridIterator = gridIteratorB_[splineB.startingGridPoint()];
        const auto &cGridIterator = gridIteratorC_[splineC.startingGridPoint()];
        // We unpack the vector to raw pointers, as profiling shows that using range based for loops over vectors
        // causes a signficant penalty in the innermost loop, primarily due to checking the loop stop condition.
        int numPointsA = static_cast<int>(aGridIterator.size());
        int numPointsB = static_cast<int>(bGridIterator.size());
        int numPointsC = static_cast<int>(cGridIterator.size());
        const auto *iteratorDataA = aGridIterator.data();
        const auto *iteratorDataB = bGridIterator.data();
        const auto *iteratorDataC = cGridIterator.data();
        const Real *splineStartA0 = splineA[0];
        const Real *splineStartB0 = splineB[0];
        const Real *splineStartC0 = splineC[0];
        const Real *splineStartA1 = splineStartA0 + splineOrder_;
        const Real *splineStartB1 = splineStartB0 + splineOrder_;
        const Real *splineStartC1 = splineStartC0 + splineOrder_;
        Real Ex = 0, Ey = 0, Ez = 0;
        for (int pointC = 0; pointC < numPointsC; ++pointC) {
            const auto &cPoint = iteratorDataC[pointC];
            const Real &splineC0 = splineStartC0[cPoint.second];
            const Real &splineC1 = splineStartC1[cPoint.second];
            for (int pointB = 0; pointB < numPointsB; ++pointB) {
                const auto &bPoint = iteratorDataB[pointB];
                const Real &splineB0 = splineStartB0[bPoint.second];
                const Real &splineB1 = splineStartB1[bPoint.second];
                const Real *cbRow = potentialGrid + cPoint.first * myGridDimensionA_ * myGridDimensionB_ +
                                    bPoint.first * myGridDimensionA_;
                for (int pointA = 0; pointA < numPointsA; ++pointA) {
                    const auto &aPoint = iteratorDataA[pointA];
                    const Real &splineA0 = splineStartA0[aPoint.second];
                    const Real &splineA1 = splineStartA1[aPoint.second];
                    const Real &gridVal = cbRow[aPoint.first];
                    Ey += gridVal * splineA0 * splineB1 * splineC0;
                    Ez += gridVal * splineA0 * splineB0 * splineC1;
                    Ex += gridVal * splineA1 * splineB0 * splineC0;
                }
            }
        }

        forces[0] -= parameter * (scaledRecVecs_[0][0] * Ex + scaledRecVecs_[0][1] * Ey + scaledRecVecs_[0][2] * Ez);
        forces[1] -= parameter * (scaledRecVecs_[1][0] * Ex + scaledRecVecs_[1][1] * Ey + scaledRecVecs_[1][2] * Ez);
        forces[2] -= parameter * (scaledRecVecs_[2][0] * Ex + scaledRecVecs_[2][1] * Ey + scaledRecVecs_[2][2] * Ez);
    }

    /*!
     * \brief Probes the grid and computes the force for a single atom, for arbitrary parameter angular momentum.
     * \param potentialGrid pointer to the array containing the potential, in ZYX order.
     * \param nPotentialComponents the number of components in the potential and its derivatives with one extra
     *        level of angular momentum to permit evaluation of forces.
     * \param splineA the BSpline object for the A direction.
     * \param splineB the BSpline object for the B direction.
     * \param splineC the BSpline object for the C direction.
     * \param phiPtr a scratch array of length nPotentialComponents, to store the fractional potential.
     * N.B. Make sure that updateAngMomIterator() has been called first with the appropriate derivative
     * level for the requested potential derivatives.
     */
    void probeGridImpl(const Real *potentialGrid, const int &nPotentialComponents, const Spline &splineA,
                       const Spline &splineB, const Spline &splineC, Real *phiPtr) {
        const auto &aGridIterator = gridIteratorA_[splineA.startingGridPoint()];
        const auto &bGridIterator = gridIteratorB_[splineB.startingGridPoint()];
        const auto &cGridIterator = gridIteratorC_[splineC.startingGridPoint()];
        const Real *splineStartA = splineA[0];
        const Real *splineStartB = splineB[0];
        const Real *splineStartC = splineC[0];
        for (const auto &cPoint : cGridIterator) {
            for (const auto &bPoint : bGridIterator) {
                const Real *cbRow = potentialGrid + cPoint.first * myGridDimensionA_ * myGridDimensionB_ +
                                    bPoint.first * myGridDimensionA_;
                for (const auto &aPoint : aGridIterator) {
                    Real gridVal = cbRow[aPoint.first];
                    for (int component = 0; component < nPotentialComponents; ++component) {
                        const auto &quanta = angMomIterator_[component];
                        const Real *splineValsA = splineStartA + quanta[0] * splineOrder_;
                        const Real *splineValsB = splineStartB + quanta[1] * splineOrder_;
                        const Real *splineValsC = splineStartC + quanta[2] * splineOrder_;
                        phiPtr[component] += gridVal * splineValsA[aPoint.second] * splineValsB[bPoint.second] *
                                             splineValsC[cPoint.second];
                    }
                }
            }
        }
    }

    /*!
     * \brief Probes the grid and computes the force for a single atom, for arbitrary parameter angular momentum.
     * \param atom the absolute atom number.
     * \param potentialGrid pointer to the array containing the potential, in ZYX order.
     * \param nComponents the number of angular momentum components in the parameters.
     * \param nForceComponents the number of angular momentum components in the parameters with one extra
     *        level of angular momentum to permit evaluation of forces.
     * \param splineA the BSpline object for the A direction.
     * \param splineB the BSpline object for the B direction.
     * \param splineC the BSpline object for the C direction.
     * \param phiPtr a scratch array of length nForceComponents, to store the fractional potential.
     * \param parameters the list of parameters associated with each atom (charges, C6 coefficients, multipoles,
     * etc...). For a parameter with angular momentum L, a matrix of dimension nAtoms x nL is expected, where nL =
     * (L+1)*(L+2)*(L+3)/6 and the fast running index nL has the ordering
     *
     * 0 X Y Z XX XY YY XZ YZ ZZ XXX XXY XYY YYY XXZ XYZ YYZ XZZ YZZ ZZZ ...
     *
     * i.e. generated by the python loops
     * \code{.py}
     * for L in range(maxAM+1):
     *     for Lz in range(0,L+1):
     *         for Ly in range(0, L - Lz + 1):
     *              Lx  = L - Ly - Lz
     * \endcode
     * \param forces a Nx3 matrix of the forces, ordered in memory as {Fx1,Fy1,Fz1,Fx2,Fy2,Fz2,....FxN,FyN,FzN}.
     */
    void probeGridImpl(const int &atom, const Real *potentialGrid, const int &nComponents, const int &nForceComponents,
                       const Spline &splineA, const Spline &splineB, const Spline &splineC, Real *phiPtr,
                       const RealMat &parameters, Real *forces) {
        std::fill(phiPtr, phiPtr + nForceComponents, 0);
        probeGridImpl(potentialGrid, nForceComponents, splineA, splineB, splineC, phiPtr);

        Real fracForce[3] = {0, 0, 0};
        for (int component = 0; component < nComponents; ++component) {
            Real param = parameters(atom, component);
            const auto &quanta = angMomIterator_[component];
            short lx = quanta[0];
            short ly = quanta[1];
            short lz = quanta[2];
            fracForce[0] -= param * phiPtr[cartAddress(lx + 1, ly, lz)];
            fracForce[1] -= param * phiPtr[cartAddress(lx, ly + 1, lz)];
            fracForce[2] -= param * phiPtr[cartAddress(lx, ly, lz + 1)];
        }
        forces[0] += scaledRecVecs_[0][0] * fracForce[0] + scaledRecVecs_[0][1] * fracForce[1] +
                     scaledRecVecs_[0][2] * fracForce[2];
        forces[1] += scaledRecVecs_[1][0] * fracForce[0] + scaledRecVecs_[1][1] * fracForce[1] +
                     scaledRecVecs_[1][2] * fracForce[2];
        forces[2] += scaledRecVecs_[2][0] * fracForce[0] + scaledRecVecs_[2][1] * fracForce[1] +
                     scaledRecVecs_[2][2] * fracForce[2];
    }

    /*!
     * \brief assertInitialized makes sure that setup() has been called before running any calculations.
     */
    void assertInitialized() const {
        if (!rPower_)
            throw std::runtime_error(
                "Either setup(...) or setup_parallel(...) must be called before computing anything.");
    }

    /*!
     * \brief makeBSplines construct the {x,y,z} B-Splines.
     * \param atomCoords a 3-vector containing the atom's coordinates.
     * \param derivativeLevel level of derivative needed for the splines.
     * \return a 3-tuple containing the {x,y,z} B-splines.
     */
    std::tuple<Spline, Spline, Spline> makeBSplines(const Real *atomCoords, short derivativeLevel) const {
        // Subtract a tiny amount to make sure we're not exactly on the rightmost (excluded)
        // grid point. The calculation is translationally invariant, so this is valid.
        constexpr float EPS = 1e-6f;
        Real aCoord =
            atomCoords[0] * recVecs_(0, 0) + atomCoords[1] * recVecs_(1, 0) + atomCoords[2] * recVecs_(2, 0) - EPS;
        Real bCoord =
            atomCoords[0] * recVecs_(0, 1) + atomCoords[1] * recVecs_(1, 1) + atomCoords[2] * recVecs_(2, 1) - EPS;
        Real cCoord =
            atomCoords[0] * recVecs_(0, 2) + atomCoords[1] * recVecs_(1, 2) + atomCoords[2] * recVecs_(2, 2) - EPS;
        // Make sure the fractional coordinates fall in the range 0 <= s < 1
        aCoord -= floor(aCoord);
        bCoord -= floor(bCoord);
        cCoord -= floor(cCoord);
        short aStartingGridPoint = gridDimensionA_ * aCoord;
        short bStartingGridPoint = gridDimensionB_ * bCoord;
        short cStartingGridPoint = gridDimensionC_ * cCoord;
        Real aDistanceFromGridPoint = gridDimensionA_ * aCoord - aStartingGridPoint;
        Real bDistanceFromGridPoint = gridDimensionB_ * bCoord - bStartingGridPoint;
        Real cDistanceFromGridPoint = gridDimensionC_ * cCoord - cStartingGridPoint;
        return std::make_tuple(Spline(aStartingGridPoint, aDistanceFromGridPoint, splineOrder_, derivativeLevel),
                               Spline(bStartingGridPoint, bDistanceFromGridPoint, splineOrder_, derivativeLevel),
                               Spline(cStartingGridPoint, cDistanceFromGridPoint, splineOrder_, derivativeLevel));
    }

    /*!
     * \brief convolveEVImpl performs the reciprocal space convolution, returning the energy, for conventional PME.
     *       We opt to not cache this the same way as the non-virial version because it's safe to assume that if
     *       the virial is requested the box is likely to change, which renders the cache useless.
     * \tparam rPower the exponent of the (inverse) distance kernel (e.g. 1 for Coulomb, 6 for attractive dispersion).
     * \param useConjugateSymmetry whether to use the complex conjugate symmetry in the convolution or not.
     * \param fullNx full (complex) dimension of the reciprocal sum in the X direction.
     * \param myNx the subset of the reciprocal sum in the x direction to be handled by this node.
     * \param myNy the subset of the reciprocal sum in the y direction to be handled by this node.
     * \param myNz the subset of the reciprocal sum in the z direction to be handled by this node.
     * \param startX the starting reciprocal sum term handled by this node in the X direction.
     * \param startY the starting reciprocal sum term handled by this node in the Y direction.
     * \param startZ the starting reciprocal sum term handled by this node in the Z direction.
     * \param scaleFactor a scale factor to be applied to all computed energies and derivatives thereof (e.g. the
     *        1 / [4 pi epslion0] for Coulomb calculations).
     * \param gridPtr the Fourier space grid, with ordering YXZ.
     * \param boxInv the reciprocal lattice vectors.
     * \param volume the volume of the unit cell.
     * \param kappa the attenuation parameter in units inverse of those used to specify coordinates.
     * \param xMods the Fourier space norms of the x B-Splines.
     * \param yMods the Fourier space norms of the y B-Splines.
     * \param zMods the Fourier space norms of the z B-Splines.
     * \param xMVals the integer prefactors to iterate over reciprocal vectors in the x dimension.
     * \param yMVals the integer prefactors to iterate over reciprocal vectors in the y dimension.
     * \param zMVals the integer prefactors to iterate over reciprocal vectors in the z dimension.
     * \param virial a vector of length 6 containing the unique virial elements, in the order XX XY YY XZ YZ ZZ.
     *        This vector is incremented, not assigned.
     * \param nThreads the number of OpenMP threads to use.
     * \return the reciprocal space energy.
     */
    template <int rPower>
    static Real convolveEVImpl(bool useConjugateSymmetry, int fullNx, int myNx, int myNy, int myNz, int startX,
                               int startY, int startZ, Real scaleFactor, Complex *gridPtr, const RealMat &boxInv,
                               Real volume, Real kappa, const Real *xMods, const Real *yMods, const Real *zMods,
                               const int *xMVals, const int *yMVals, const int *zMVals, RealMat &virial, int nThreads) {
        Real energy = 0;

        bool nodeZero = startX == 0 && startY == 0 && startZ == 0;
        if (rPower > 3 && nodeZero) {
            // Kernels with rPower>3 are absolutely convergent and should have the m=0 term present.
            // To compute it we need sum_ij c(i)c(j), which can be obtained from the structure factor norm.
            Real prefac = 2 * scaleFactor * PI * SQRTPI * pow(kappa, rPower - 3) /
                          ((rPower - 3) * gammaComputer<Real, rPower>::value * volume);
            energy += prefac * (gridPtr[0].real() * gridPtr[0].real() + gridPtr[0].imag() * gridPtr[0].imag());
        }
        // Ensure the m=0 term convolution product is zeroed for the backtransform; it's been accounted for above.
        if (nodeZero) gridPtr[0] = Complex(0, 0);

        Real bPrefac = PI * PI / (kappa * kappa);
        Real volPrefac = scaleFactor * pow(PI, rPower - 1) / (SQRTPI * gammaComputer<Real, rPower>::value * volume);
        size_t nxz = (size_t)myNx * myNz;
        Real Vxx = 0, Vxy = 0, Vyy = 0, Vxz = 0, Vyz = 0, Vzz = 0;
        const Real *boxPtr = boxInv[0];
        size_t nyxz = myNy * nxz;
        // Exclude m=0 cell.
        int start = (nodeZero ? 1 : 0);
// Writing the three nested loops in one allows for better load balancing in parallel.
#pragma omp parallel for reduction(+ : energy, Vxx, Vxy, Vyy, Vxz, Vyz, Vzz) num_threads(nThreads)
        for (size_t yxz = start; yxz < nyxz; ++yxz) {
            size_t xz = yxz % nxz;
            short ky = yxz / nxz;
            short kx = xz / myNz;
            short kz = xz % myNz;
            // We only loop over the first nx/2+1 x values in the complex case;
            // this accounts for the "missing" complex conjugate values.
            Real permPrefac = (useConjugateSymmetry && (kx + startX != 0) && (kx + startX != fullNx - 1)) ? 2 : 1;
            const int &mx = xMVals[kx];
            const int &my = yMVals[ky];
            const int &mz = zMVals[kz];
            Real mVecX = boxPtr[0] * mx + boxPtr[1] * my + boxPtr[2] * mz;
            Real mVecY = boxPtr[3] * mx + boxPtr[4] * my + boxPtr[5] * mz;
            Real mVecZ = boxPtr[6] * mx + boxPtr[7] * my + boxPtr[8] * mz;
            Real mNormSq = mVecX * mVecX + mVecY * mVecY + mVecZ * mVecZ;
            Real mTerm = raiseNormToIntegerPower<Real, rPower - 3>::compute(mNormSq);
            Real bSquared = bPrefac * mNormSq;
            auto gammas = incompleteGammaVirialComputer<Real, 3 - rPower>::compute(bSquared);
            Real eGamma = std::get<0>(gammas);
            Real vGamma = std::get<1>(gammas);
            Complex &gridVal = gridPtr[yxz];
            Real structFacNorm = gridVal.real() * gridVal.real() + gridVal.imag() * gridVal.imag();
            Real totalPrefac = volPrefac * mTerm * yMods[ky] * xMods[kx] * zMods[kz];
            Real influenceFunction = totalPrefac * eGamma;
            gridVal *= influenceFunction;
            Real eTerm = permPrefac * influenceFunction * structFacNorm;
            Real vTerm = permPrefac * vGamma * totalPrefac / mNormSq * structFacNorm;
            energy += eTerm;
            Vxx += vTerm * mVecX * mVecX;
            Vxy += vTerm * mVecX * mVecY;
            Vyy += vTerm * mVecY * mVecY;
            Vxz += vTerm * mVecX * mVecZ;
            Vyz += vTerm * mVecY * mVecZ;
            Vzz += vTerm * mVecZ * mVecZ;
        }

        energy /= 2;

        virial[0][0] -= Vxx - energy;
        virial[0][1] -= Vxy;
        virial[0][2] -= Vyy - energy;
        virial[0][3] -= Vxz;
        virial[0][4] -= Vyz;
        virial[0][5] -= Vzz - energy;

        return energy;
    }

    /*!
     * \brief convolveEVCompressedImpl performs the reciprocal space convolution, returning the energy, for compressed
     * PME. We opt to not cache this the same way as the non-virial version because it's safe to assume that if the
     * virial is requested the box is likely to change, which renders the cache useless.
     * \tparam rPower the exponent of the (inverse) distance kernel (e.g. 1 for Coulomb, 6 for attractive dispersion).
     * \param myNx the subset of the reciprocal sum in the x direction to be handled by this node.
     * \param myNy the subset of the reciprocal sum in the y direction to be handled by this node.
     * \param myNz the subset of the reciprocal sum in the z direction to be handled by this node.
     * \param startX the starting reciprocal sum term handled by this node in the X direction.
     * \param startY the starting reciprocal sum term handled by this node in the Y direction.
     * \param startZ the starting reciprocal sum term handled by this node in the Z direction.
     * \param scaleFactor a scale factor to be applied to all computed energies and derivatives thereof
     *  (e.g. thee 1 / [4 pi epslion0] for Coulomb calculations).
     * \param gridPtrIn the Fourier space grid, with ordering YXZ.
     * \param gridPtrOut the convolved Fourier space grid, with ordering YXZ.
     * \param boxInv the reciprocal lattice vectors.
     * \param volume the volume of the unit cell.
     * \param kappa the attenuation parameter in units inverse of those used to specify coordinates.
     * \param xMods the Fourier space norms of the x B-Splines.
     * \param yMods the Fourier space norms of the y B-Splines.
     * \param zMods the Fourier space norms of the z B-Splines.
     * \param xMVals the integer prefactors to iterate over reciprocal vectors in the x dimension.
     * \param yMVals the integer prefactors to iterate over reciprocal vectors in the y dimension.
     * \param zMVals the integer prefactors to iterate over reciprocal vectors in the z dimension.
     * \param virial a vector of length 6 containing the unique virial elements, in the order XX XY YY XZ YZ ZZ.
     *        This vector is incremented, not assigned.
     * \param nThreads the number of OpenMP threads to use.
     * \return the reciprocal space energy.
     */
    template <int rPower>
    static Real convolveEVCompressedImpl(int myNx, int myNy, int myNz, int startX, int startY, int startZ,
                                         Real scaleFactor, const Real *__restrict__ gridPtrIn,
                                         Real *__restrict__ gridPtrOut, const RealMat &boxInv, Real volume, Real kappa,
                                         const Real *xMods, const Real *yMods, const Real *zMods, const int *xMVals,
                                         const int *yMVals, const int *zMVals, RealMat &virial, int nThreads) {
        Real energy = 0;

        bool nodeZero = startX == 0 && startY == 0 && startZ == 0;
        if (rPower > 3 && nodeZero) {
            // Kernels with rPower>3 are absolutely convergent and should have the m=0 term present.
            // To compute it we need sum_ij c(i)c(j), which can be obtained from the structure factor norm.
            Real prefac = 2 * scaleFactor * PI * SQRTPI * pow(kappa, rPower - 3) /
                          ((rPower - 3) * gammaComputer<Real, rPower>::value * volume);
            energy += prefac * gridPtrIn[0] * gridPtrIn[0];
        }
        // Ensure the m=0 term convolution product is zeroed for the backtransform; it's been accounted for above.
        if (nodeZero) gridPtrOut[0] = 0;

        Real bPrefac = PI * PI / (kappa * kappa);
        Real volPrefac = scaleFactor * pow(PI, rPower - 1) / (SQRTPI * gammaComputer<Real, rPower>::value * volume);
        size_t nxz = (size_t)myNx * myNz;
        size_t nyxz = myNy * nxz;
        Real Vxx = 0, Vxy = 0, Vyy = 0, Vxz = 0, Vyz = 0, Vzz = 0;
        const Real *boxPtr = boxInv[0];
        // Exclude m=0 cell.
        int start = (nodeZero ? 1 : 0);
// Writing the three nested loops in one allows for better load balancing in parallel.
#pragma omp parallel for reduction(+ : energy, Vxx, Vxy, Vyy, Vxz, Vyz, Vzz) num_threads(nThreads)
        for (size_t yxz = start; yxz < nyxz; ++yxz) {
            size_t xz = yxz % nxz;
            short ky = yxz / nxz;
            short kx = xz / myNz;
            short kz = xz % myNz;
            // We only loop over the first nx/2+1 x values in the complex case;
            // this accounts for the "missing" complex conjugate values.
            const int &mx = xMVals[kx];
            const int &my = yMVals[ky];
            const int &mz = zMVals[kz];
            Real mVecX = boxPtr[0] * mx + boxPtr[1] * my + boxPtr[2] * mz;
            Real mVecY = boxPtr[3] * mx + boxPtr[4] * my + boxPtr[5] * mz;
            Real mVecZ = boxPtr[6] * mx + boxPtr[7] * my + boxPtr[8] * mz;
            Real mNormSq = mVecX * mVecX + mVecY * mVecY + mVecZ * mVecZ;
            Real mTerm = raiseNormToIntegerPower<Real, rPower - 3>::compute(mNormSq);
            Real bSquared = bPrefac * mNormSq;
            auto gammas = incompleteGammaVirialComputer<Real, 3 - rPower>::compute(bSquared);
            Real eGamma = std::get<0>(gammas);
            Real vGamma = std::get<1>(gammas);
            const Real &gridVal = gridPtrIn[yxz];
            size_t minusKx = (mx == 0 ? 0 : (mx < 0 ? kx - 1 : kx + 1));
            size_t minusKy = (my == 0 ? 0 : (my < 0 ? ky - 1 : ky + 1));
            size_t minusKz = (mz == 0 ? 0 : (mz < 0 ? kz - 1 : kz + 1));
            size_t addressXY = minusKy * nxz + minusKx * myNz + kz;
            size_t addressXZ = ky * nxz + minusKx * myNz + minusKz;
            size_t addressYZ = minusKy * nxz + (size_t)kx * myNz + minusKz;
            Real totalPrefac = volPrefac * mTerm * yMods[ky] * xMods[kx] * zMods[kz];
            Real influenceFunction = totalPrefac * eGamma;
            gridPtrOut[yxz] = gridVal * influenceFunction;
            Real eTerm = influenceFunction * gridVal * gridVal;
            Real vPrefac = vGamma * totalPrefac / mNormSq * gridVal;
            Real vTerm = vPrefac * gridVal;
            Real vTermXY = vPrefac * gridPtrIn[addressXY];
            Real vTermXZ = vPrefac * gridPtrIn[addressXZ];
            Real vTermYZ = vPrefac * gridPtrIn[addressYZ];
            energy += eTerm;
            Vxx += vTerm * mVecX * mVecX;
            Vxy -= vTermXY * mVecX * mVecY;
            Vyy += vTerm * mVecY * mVecY;
            Vxz -= vTermXZ * mVecX * mVecZ;
            Vyz -= vTermYZ * mVecY * mVecZ;
            Vzz += vTerm * mVecZ * mVecZ;
        }

        energy /= 2;

        virial[0][0] -= Vxx - energy;
        virial[0][1] -= Vxy;
        virial[0][2] -= Vyy - energy;
        virial[0][3] -= Vxz;
        virial[0][4] -= Vyz;
        virial[0][5] -= Vzz - energy;

        return energy;
    }

    /*!
     * \brief sanityChecks just makes sure that inputs have consistent dimensions, and that prerequisites are
     * initialized.
     * \param parameterAngMom the angular momentum of the parameters (0 for charges, C6 coefficients, 2 for
     * quadrupoles, etc.).
     * \param parameters the input parameters.
     * \param coordinates the input coordinates.
     * \param cartesianOffset an offset to the start of the angular momentum shell for the parameters, in cases where
     * only a single angular momentum shell is to be processed (rather than all shells up to a given angular momentum).
     */
    void sanityChecks(int parameterAngMom, const RealMat &parameters, const RealMat &coordinates,
                      int cartesianOffset = 0) {
        assertInitialized();
        if (parameterAngMom < 0)
            throw std::runtime_error("Negative parameter angular momentum found where positive value was expected");
        if (boxVecs_.isNearZero())
            throw std::runtime_error(
                "Lattice vectors have not been set yet!  Call setLatticeVectors(...) before runPME(...);");
        if (coordinates.nRows() != parameters.nRows())
            throw std::runtime_error(
                "Inconsistent number of coordinates and parameters; there should be nAtoms of each.");
        if (parameters.nCols() != (nCartesian(parameterAngMom) - cartesianOffset))
            throw std::runtime_error(
                "Mismatch in the number of parameters provided and the parameter angular momentum");
    }

    /*!
     * \brief cacheInfluenceFunctionImpl computes the influence function used in convolution, for later use.
     * \tparam rPower the exponent of the (inverse) distance kernel (e.g. 1 for Coulomb, 6 for attractive dispersion).
     * \param myNx the subset of the grid in the x direction to be handled by this node.
     * \param myNy the subset of the grid in the y direction to be handled by this node.
     * \param myNz the subset of the grid in the z direction to be handled by this node.
     * \param startX the starting reciprocal space sum term handled by this node in the X direction.
     * \param startY the starting reciprocal space sum term handled by this node in the Y direction.
     * \param startZ the starting reciprocal space sum term handled by this node in the Z direction.
     * \param scaleFactor a scale factor to be applied to all computed energies and derivatives thereof (e.g. the
     *        1 / [4 pi epslion0] for Coulomb calculations).
     * \param gridPtr the Fourier space grid, with ordering YXZ.
     * \param boxInv the reciprocal lattice vectors.
     * \param volume the volume of the unit cell.
     * \param kappa the attenuation parameter in units inverse of those used to specify coordinates.
     * \param xMods the Fourier space norms of the x B-Splines.
     * \param yMods the Fourier space norms of the y B-Splines.
     * \param zMods the Fourier space norms of the z B-Splines.
     * \param xMVals the integer prefactors to iterate over reciprocal vectors in the x dimension.
     * \param yMVals the integer prefactors to iterate over reciprocal vectors in the y dimension.
     * \param zMVals the integer prefactors to iterate over reciprocal vectors in the z dimension.
     *        This vector is incremented, not assigned.
     * \param nThreads the number of OpenMP threads to use.
     * \return the energy for the m=0 term.
     */
    template <int rPower>
    static void cacheInfluenceFunctionImpl(int myNx, int myNy, int myNz, int startX, int startY, int startZ,
                                           Real scaleFactor, RealVec &influenceFunction, const RealMat &boxInv,
                                           Real volume, Real kappa, const Real *xMods, const Real *yMods,
                                           const Real *zMods, const int *xMVals, const int *yMVals, const int *zMVals,
                                           int nThreads) {
        bool nodeZero = startX == 0 && startY == 0 && startZ == 0;
        size_t nxz = (size_t)myNx * myNz;
        size_t nyxz = myNy * nxz;
        influenceFunction.resize(nyxz);
        Real *gridPtr = influenceFunction.data();
        if (nodeZero) gridPtr[0] = 0;

        Real bPrefac = PI * PI / (kappa * kappa);
        Real volPrefac = scaleFactor * pow(PI, rPower - 1) / (SQRTPI * gammaComputer<Real, rPower>::value * volume);
        const Real *boxPtr = boxInv[0];
        // Exclude m=0 cell.
        int start = (nodeZero ? 1 : 0);
// Writing the three nested loops in one allows for better load balancing in parallel.
#pragma omp parallel for num_threads(nThreads)
        for (size_t yxz = start; yxz < nyxz; ++yxz) {
            size_t xz = yxz % nxz;
            short ky = yxz / nxz;
            short kx = xz / myNz;
            short kz = xz % myNz;
            const Real mx = (Real)xMVals[kx];
            const Real my = (Real)yMVals[ky];
            const Real mz = (Real)zMVals[kz];
            Real mVecX = boxPtr[0] * mx + boxPtr[1] * my + boxPtr[2] * mz;
            Real mVecY = boxPtr[3] * mx + boxPtr[4] * my + boxPtr[5] * mz;
            Real mVecZ = boxPtr[6] * mx + boxPtr[7] * my + boxPtr[8] * mz;
            Real mNormSq = mVecX * mVecX + mVecY * mVecY + mVecZ * mVecZ;
            Real mTerm = raiseNormToIntegerPower<Real, rPower - 3>::compute(mNormSq);
            Real bSquared = bPrefac * mNormSq;
            Real incompleteGammaTerm = incompleteGammaComputer<Real, 3 - rPower>::compute(bSquared);
            gridPtr[yxz] = volPrefac * incompleteGammaTerm * mTerm * yMods[ky] * xMods[kx] * zMods[kz];
        }
    }

    /*!
     * \brief dirEImpl computes the kernel for the direct energy for a pair.
     * \tparam rPower the exponent of the (inverse) distance kernel (e.g. 1 for Coulomb, 6 for attractive dispersion).
     * \param rSquared the square of the internuclear distance
     * \param kappaSquared the square of attenuation parameter in units inverse of those used to specify coordinates.
     * \return the energy kernel.
     */
    template <int rPower>
    inline static Real dirEImpl(Real rSquared, Real kappaSquared) {
        Real denominator = raiseNormToIntegerPower<Real, rPower>::compute(rSquared);
        Real gammaTerm = incompleteGammaComputer<Real, rPower>::compute(rSquared * kappaSquared) /
                         gammaComputer<Real, rPower>::value;
        return gammaTerm / denominator;
    }

    /*!
     * \brief dirEFImpl computes the kernels for the direct energy and force for a pair.
     * \tparam rPower the exponent of the (inverse) distance kernel (e.g. 1 for Coulomb, 6 for attractive dispersion).
     * \param rSquared the square of the internuclear distance
     * \param kappa the attenuation parameter in units inverse of those used to specify coordinates.
     * \param kappaSquared the square of attenuation parameter in units inverse of those used to specify coordinates.
     * \return a tuple containing the energy and force kernels, respectively.
     */
    template <int rPower>
    inline static std::tuple<Real, Real> dirEFImpl(Real rSquared, Real kappa, Real kappaSquared) {
        Real rInv = 1 / rSquared;
        Real kappaToRPower = kappa;
        for (int i = 1; i < rPower; ++i) kappaToRPower *= kappa;
        Real denominator = raiseNormToIntegerPower<Real, rPower>::compute(rSquared);
        Real gammaTerm = incompleteGammaComputer<Real, rPower>::compute(rSquared * kappaSquared) /
                         gammaComputer<Real, rPower>::value;
        Real eKernel = gammaTerm / denominator;
        Real fKernel = -rPower * eKernel * rInv -
                       2 * rInv * exp(-kappaSquared * rSquared) * kappaToRPower / gammaComputer<Real, rPower>::value;
        return std::make_tuple(eKernel, fKernel);
    }

    /*!
     * \brief adjEImpl computes the kernel for the adjusted energy for a pair.
     * \tparam rPower the exponent of the (inverse) distance kernel (e.g. 1 for Coulomb, 6 for attractive dispersion).
     * \param rSquared the square of the internuclear distance
     * \param kappaSquared the square of attenuation parameter in units inverse of those used to specify coordinates.
     * \return the energy kernel.
     */
    template <int rPower>
    inline static Real adjEImpl(Real rSquared, Real kappaSquared) {
        Real denominator = raiseNormToIntegerPower<Real, rPower>::compute(rSquared);
        Real gammaTerm = incompleteGammaComputer<Real, rPower>::compute(rSquared * kappaSquared) /
                         gammaComputer<Real, rPower>::value;
        return (gammaTerm - 1) / denominator;
    }

    /*!
     * \brief adjEFImpl computes the kernels for the adjusted energy and force for a pair.
     * \tparam rPower the exponent of the (inverse) distance kernel (e.g. 1 for Coulomb, 6 for attractive dispersion).
     * \param rSquared the square of the internuclear distance
     * \param kappa the attenuation parameter in units inverse of those used to specify coordinates.
     * \param kappaSquared the square of attenuation parameter in units inverse of those used to specify coordinates.
     * \return a tuple containing the energy and force kernels, respectively.
     */
    template <int rPower>
    inline static std::tuple<Real, Real> adjEFImpl(Real rSquared, Real kappa, Real kappaSquared) {
        Real rInv = 1 / rSquared;
        Real kappaToRPower = kappa;
        for (int i = 1; i < rPower; ++i) kappaToRPower *= kappa;
        Real denominator = raiseNormToIntegerPower<Real, rPower>::compute(rSquared);
        Real gammaTerm = incompleteGammaComputer<Real, rPower>::compute(rSquared * kappaSquared) /
                         gammaComputer<Real, rPower>::value;
        Real eKernel = (gammaTerm - 1) / denominator;
        Real fKernel = -rPower * eKernel * rInv -
                       2 * rInv * exp(-kappaSquared * rSquared) * kappaToRPower / gammaComputer<Real, rPower>::value;
        return std::make_tuple(eKernel, fKernel);
    }

    /*!
     * \brief slfEImpl computes the self energy due to particles feeling their own potential.
     * \tparam rPower the exponent of the (inverse) distance kernel (e.g. 1 for Coulomb, 6 for attractive dispersion).
     * \param parameterAngMom the angular momentum of the parameters (0 for charges, C6 coefficients, 2 for quadrupoles,
     * etc.).
     * \param parameters the list of parameters associated with each atom (charges, C6 coefficients, multipoles,
     * etc...). For a parameter with angular momentum L, a matrix of dimension nAtoms x nL is expected, where nL =
     * (L+1)*(L+2)*(L+3)/6 and the fast running index nL has the ordering
     *
     * 0 X Y Z XX XY YY XZ YZ ZZ XXX XXY XYY YYY XXZ XYZ YYZ XZZ YZZ ZZZ ...
     *
     * i.e. generated by the python loops
     * \code{.py}
     * for L in range(maxAM+1):
     *     for Lz in range(0,L+1):
     *         for Ly in range(0, L - Lz + 1):
     *              Lx  = L - Ly - Lz
     * \endcode
     * \param kappa the attenuation parameter in units inverse of those used to specify coordinates.
     * \param scaleFactor a scale factor to be applied to all computed energies and derivatives thereof
     *        (e.g. the 1 / [4 pi epslion0] for Coulomb calculations).
     * \return the self energy.  N.B. there is no self force associated with this term.
     */
    template <int rPower>
    static Real slfEImpl(int parameterAngMom, const RealMat &parameters, Real kappa, Real scaleFactor) {
        if (parameterAngMom) throw std::runtime_error("Multipole self terms have not been coded yet.");

        size_t nAtoms = parameters.nRows();
        Real prefac = -scaleFactor * std::pow(kappa, rPower) / (rPower * gammaComputer<Real, rPower>::value);
        Real sumCoefs = 0;
        for (size_t atom = 0; atom < nAtoms; ++atom) {
            sumCoefs += parameters(atom, 0) * parameters(atom, 0);
        }
        return prefac * sumCoefs;
    }

    /*!
     * \brief common_init sets up information that is common to serial and parallel runs.
     */
    void setupCalculationMetadata(int rPower, Real kappa, int splineOrder, int dimA, int dimB, int dimC, int maxKA,
                                  int maxKB, int maxKC, Real scaleFactor, int nThreads, void *commPtrIn,
                                  NodeOrder nodeOrder, int numNodesA, int numNodesB, int numNodesC) {
        int numKSumTermsA = std::min(2 * maxKA + 1, dimA);
        int numKSumTermsB = std::min(2 * maxKB + 1, dimB);
        int numKSumTermsC = std::min(2 * maxKC + 1, dimC);
        AlgorithmType algorithm = numKSumTermsA < dimA && numKSumTermsB < dimB && numKSumTermsC < dimC
                                      ? AlgorithmType::CompressedPME
                                      : AlgorithmType::PME;
        kappaHasChanged_ = kappa != kappa_;
        numNodesHasChanged_ = numNodesA_ != numNodesA || numNodesB_ != numNodesB || numNodesC_ != numNodesC;
        rPowerHasChanged_ = rPower_ != rPower;
        gridDimensionHasChanged_ = gridDimensionA_ != dimA || gridDimensionB_ != dimB || gridDimensionC_ != dimC;
        reciprocalSumDimensionHasChanged_ =
            numKSumTermsA != numKSumTermsA_ || numKSumTermsB != numKSumTermsB_ || numKSumTermsC != numKSumTermsC_;
        algorithmHasChanged_ = algorithmType_ != algorithm;
        splineOrderHasChanged_ = splineOrder_ != splineOrder;
        scaleFactorHasChanged_ = scaleFactor_ != scaleFactor;
        if (kappaHasChanged_ || rPowerHasChanged_ || gridDimensionHasChanged_ || splineOrderHasChanged_ ||
            numNodesHasChanged_ || scaleFactorHasChanged_ || algorithmHasChanged_ ||
            requestedNumberOfThreads_ != nThreads) {
            numNodesA_ = numNodesA;
            numNodesB_ = numNodesB;
            numNodesC_ = numNodesC;
            myNodeRankA_ = myNodeRankB_ = myNodeRankC_ = 0;
#if HAVE_MPI == 1
            if (commPtrIn) {
                MPI_Comm const &communicator = *((MPI_Comm*)(commPtrIn));
                mpiCommunicator_ = std::unique_ptr<MPIWrapper<Real>>(
                    new MPIWrapper<Real>(communicator, numNodesA, numNodesB, numNodesC));
                switch (nodeOrder) {
                    case (NodeOrder::ZYX):
                        myNodeRankA_ = mpiCommunicator_->myRank_ % numNodesA;
                        myNodeRankB_ = (mpiCommunicator_->myRank_ % (numNodesB * numNodesA)) / numNodesA;
                        myNodeRankC_ = mpiCommunicator_->myRank_ / (numNodesB * numNodesA);
                        mpiCommunicatorA_ =
                            mpiCommunicator_->split(myNodeRankC_ * numNodesB + myNodeRankB_, myNodeRankA_);
                        mpiCommunicatorB_ =
                            mpiCommunicator_->split(myNodeRankC_ * numNodesA + myNodeRankA_, myNodeRankB_);
                        mpiCommunicatorC_ =
                            mpiCommunicator_->split(myNodeRankB_ * numNodesA + myNodeRankA_, myNodeRankC_);
                        break;
                    default:
                        throw std::runtime_error("Unknown NodeOrder in helpme::setupCalculationMetadata.");
                }
            }
#else   // Have MPI
            if (numNodesA * numNodesB * numNodesC > 1)
                throw std::runtime_error(
                    "a parallel calculation has been setup, but helpme was not compiled with MPI.  Make sure you "
                    "compile with -DHAVE_MPI=1 "
                    "in the list of compiler definitions.");
#endif  // Have MPI
            rPower_ = rPower;
            algorithmType_ = algorithm;
            splineOrder_ = splineOrder;
            cacheLineSizeInReals_ = static_cast<Real>(sysconf(_SC_PAGESIZE) / sizeof(Real));
            requestedNumberOfThreads_ = nThreads;
#ifdef _OPENMP
            nThreads_ = nThreads ? nThreads : omp_get_max_threads();
#else
            nThreads_ = 1;
#endif
            scaleFactor_ = scaleFactor;
            kappa_ = kappa;

            size_t scratchSize;
            int gridPaddingA = 0, gridPaddingB = 0, gridPaddingC = 0;
            if (algorithm == AlgorithmType::CompressedPME) {
                gridDimensionA_ = numNodesA * std::ceil(dimA / (float)numNodesA);
                gridDimensionB_ = numNodesB * std::ceil(dimB / (float)numNodesB);
                gridDimensionC_ = numNodesC * std::ceil(dimC / (float)numNodesC);
                gridPaddingA = (numNodesA > 1 ? splineOrder - 1 : 0);
                gridPaddingB = (numNodesB > 1 ? splineOrder - 1 : 0);
                gridPaddingC = (numNodesC > 1 ? splineOrder - 1 : 0);
                myGridDimensionA_ = gridDimensionA_ / numNodesA + gridPaddingA;
                myGridDimensionB_ = gridDimensionB_ / numNodesB + gridPaddingB;
                myGridDimensionC_ = gridDimensionC_ / numNodesC + gridPaddingC;
                myFirstGridPointA_ = myNodeRankA_ * (myGridDimensionA_ - gridPaddingA);
                myFirstGridPointB_ = myNodeRankB_ * (myGridDimensionB_ - gridPaddingB);
                myFirstGridPointC_ = myNodeRankC_ * (myGridDimensionC_ - gridPaddingC);
                myNumKSumTermsA_ = numNodesA == 1 ? numKSumTermsA : 2 * std::ceil((maxKA + 1.0) / numNodesA);
                myNumKSumTermsB_ = numNodesB == 1 ? numKSumTermsB : 2 * std::ceil((maxKB + 1.0) / numNodesB);
                myNumKSumTermsC_ = numNodesC == 1 ? numKSumTermsC : 2 * std::ceil((maxKC + 1.0) / numNodesC);
                numKSumTermsA_ = myNumKSumTermsA_ * numNodesA;
                numKSumTermsB_ = myNumKSumTermsB_ * numNodesB;
                numKSumTermsC_ = myNumKSumTermsC_ * numNodesC;
                firstKSumTermA_ = myNodeRankA_ * myNumKSumTermsA_;
                firstKSumTermB_ = myNodeRankB_ * myNumKSumTermsB_;
                firstKSumTermC_ = myNodeRankC_ * myNumKSumTermsC_;
                fftHelperA_ = FFTWWrapper<Real>();
                fftHelperB_ = FFTWWrapper<Real>();
                fftHelperC_ = FFTWWrapper<Real>();
                compressionCoefficientsA_ = RealMat(numKSumTermsA_, myGridDimensionA_);
                compressionCoefficientsB_ = RealMat(numKSumTermsB_, myGridDimensionB_);
                compressionCoefficientsC_ = RealMat(numKSumTermsC_, myGridDimensionC_);
                scratchSize = (size_t)std::max(myGridDimensionA_, numKSumTermsA) *
                              std::max(myGridDimensionB_, numKSumTermsB) * std::max(myGridDimensionC_, numKSumTermsC);
            } else {
                gridDimensionA_ = findGridSize(dimA, {numNodesA_});
                gridDimensionB_ = findGridSize(dimB, {numNodesB_ * numNodesC_});
                gridDimensionC_ = findGridSize(dimC, {numNodesA_ * numNodesC_, numNodesB_ * numNodesC_});
                gridPaddingA = gridPaddingB = gridPaddingC = 0;
                myGridDimensionA_ = gridDimensionA_ / numNodesA_;
                myGridDimensionB_ = gridDimensionB_ / numNodesB_;
                myGridDimensionC_ = gridDimensionC_ / numNodesC_;
                complexGridDimensionA_ = gridDimensionA_ / 2 + 1;
                myComplexGridDimensionA_ = myGridDimensionA_ / 2 + 1;
                numKSumTermsA_ = gridDimensionA_;
                numKSumTermsB_ = gridDimensionB_;
                numKSumTermsC_ = gridDimensionC_;
                myNumKSumTermsA_ = myComplexGridDimensionA_;
                myNumKSumTermsB_ = myGridDimensionB_ / numNodesC_;
                myNumKSumTermsC_ = gridDimensionC_;
                myFirstGridPointA_ = myNodeRankA_ * myGridDimensionA_;
                myFirstGridPointB_ = myNodeRankB_ * myGridDimensionB_;
                myFirstGridPointC_ = myNodeRankC_ * myGridDimensionC_;
                firstKSumTermA_ = myNodeRankA_ * myComplexGridDimensionA_;
                firstKSumTermB_ = myNodeRankB_ * myGridDimensionB_ + myNodeRankC_ * myGridDimensionB_ / numNodesC_;
                firstKSumTermC_ = 0;
                fftHelperA_ = FFTWWrapper<Real>(gridDimensionA_);
                fftHelperB_ = FFTWWrapper<Real>(gridDimensionB_);
                fftHelperC_ = FFTWWrapper<Real>(gridDimensionC_);
                compressionCoefficientsA_ = RealMat();
                compressionCoefficientsB_ = RealMat();
                compressionCoefficientsC_ = RealMat();
                scratchSize = (size_t)myGridDimensionC_ * myComplexGridDimensionA_ * myGridDimensionB_;
            }

            // Grid iterators to correctly wrap the grid when using splines.
            gridIteratorA_ = makeGridIterator(gridDimensionA_, myFirstGridPointA_,
                                              myFirstGridPointA_ + myGridDimensionA_, gridPaddingA);
            gridIteratorB_ = makeGridIterator(gridDimensionB_, myFirstGridPointB_,
                                              myFirstGridPointB_ + myGridDimensionB_, gridPaddingB);
            gridIteratorC_ = makeGridIterator(gridDimensionC_, myFirstGridPointC_,
                                              myFirstGridPointC_ + myGridDimensionC_, gridPaddingC);

            // Divide C grid points among threads to avoid race conditions.
            threadedGridIteratorC_.clear();
            for (int thread = 0; thread < nThreads_; ++thread) {
                GridIterator myIterator;
                for (int cGridPoint = 0; cGridPoint < gridDimensionC_; ++cGridPoint) {
                    std::vector<std::pair<short, short>> splineIterator;
                    for (const auto &fullIterator : gridIteratorC_[cGridPoint]) {
                        if (fullIterator.first % nThreads_ == thread) {
                            splineIterator.push_back(fullIterator);
                        }
                    }
                    splineIterator.shrink_to_fit();
                    myIterator.push_back(splineIterator);
                }
                myIterator.shrink_to_fit();
                threadedGridIteratorC_.push_back(myIterator);
            }
            threadedGridIteratorC_.shrink_to_fit();

            // Assign a large default so that uninitialized values end up generating zeros later on
            mValsA_.resize(myNumKSumTermsA_, 99);
            mValsB_.resize(myNumKSumTermsB_, 99);
            mValsC_.resize(myNumKSumTermsC_, 99);
            if (algorithm == AlgorithmType::CompressedPME) {
                // For compressed PME we order the m values as 0, 1, -1, 2, -2, ..., Kmax, -Kmax
                // because we need to guarantee that +/- m pairs live on the same node for the virial.
                mValsA_[0] = 0;
                int startA = myNodeRankA_ ? 0 : 1;
                for (int k = startA; k < (myNumKSumTermsA_ + (numNodesA_ == 1)) / 2; ++k) {
                    int m = myNodeRankA_ * myNumKSumTermsA_ / 2 + k;
                    mValsA_[startA + 2 * (k - startA)] = m;
                    mValsA_[startA + 2 * (k - startA) + 1] = -m;
                }
                mValsB_[0] = 0;
                int startB = myNodeRankB_ ? 0 : 1;
                for (int k = startB; k < (myNumKSumTermsB_ + (numNodesB_ == 1)) / 2; ++k) {
                    int m = myNodeRankB_ * myNumKSumTermsB_ / 2 + k;
                    mValsB_[startB + 2 * (k - startB)] = m;
                    mValsB_[startB + 2 * (k - startB) + 1] = -m;
                }
                mValsC_[0] = 0;
                int startC = myNodeRankC_ ? 0 : 1;
                for (int k = startC; k < (myNumKSumTermsC_ + (numNodesC_ == 1)) / 2; ++k) {
                    int m = myNodeRankC_ * myNumKSumTermsC_ / 2 + k;
                    mValsC_[startC + 2 * (k - startC)] = m;
                    mValsC_[startC + 2 * (k - startC) + 1] = -m;
                }

                std::fill(compressionCoefficientsA_[0], compressionCoefficientsA_[1], 1);
                for (int node = 0; node < numNodesA_; ++node) {
                    int offset = node ? 0 : 1;
                    for (int m = offset; m < (myNumKSumTermsA_ + (numNodesA_ == 1)) / 2; ++m) {
                        int fullM = m + node * myNumKSumTermsA_ / 2;
                        Real *rowPtr = compressionCoefficientsA_[offset + 2 * (fullM - offset)];
                        for (int n = 0; n < myGridDimensionA_; ++n) {
                            Real exponent = 2 * PI * fullM * (n + myFirstGridPointA_) / gridDimensionA_;
                            rowPtr[n] = std::sqrt(2) * std::cos(exponent);
                            rowPtr[n + myGridDimensionA_] = std::sqrt(2) * std::sin(exponent);
                        }
                    }
                }
                std::fill(compressionCoefficientsB_[0], compressionCoefficientsB_[1], 1);
                for (int node = 0; node < numNodesB_; ++node) {
                    int offset = node ? 0 : 1;
                    for (int m = offset; m < (myNumKSumTermsB_ + (numNodesB_ == 1)) / 2; ++m) {
                        int fullM = m + node * myNumKSumTermsB_ / 2;
                        Real *rowPtr = compressionCoefficientsB_[offset + 2 * (fullM - offset)];
                        for (int n = 0; n < myGridDimensionB_; ++n) {
                            Real exponent = 2 * PI * fullM * (n + myFirstGridPointB_) / gridDimensionB_;
                            rowPtr[n] = std::sqrt(2) * std::cos(exponent);
                            rowPtr[n + myGridDimensionB_] = std::sqrt(2) * std::sin(exponent);
                        }
                    }
                }
                std::fill(compressionCoefficientsC_[0], compressionCoefficientsC_[1], 1);
                for (int node = 0; node < numNodesC_; ++node) {
                    int offset = node ? 0 : 1;
                    for (int m = offset; m < (myNumKSumTermsC_ + (numNodesC_ == 1)) / 2; ++m) {
                        int fullM = m + node * myNumKSumTermsC_ / 2;
                        Real *rowPtr = compressionCoefficientsC_[offset + 2 * (fullM - offset)];
                        for (int n = 0; n < myGridDimensionC_; ++n) {
                            Real exponent = 2 * PI * fullM * (n + myFirstGridPointC_) / gridDimensionC_;
                            rowPtr[n] = std::sqrt(2) * std::cos(exponent);
                            rowPtr[n + myGridDimensionC_] = std::sqrt(2) * std::sin(exponent);
                        }
                    }
                }
                // Fourier space spline norms.
                Spline spline = Spline(0, 0, splineOrder_, 0);
                splineModA_ = spline.invSplineModuli(gridDimensionA_, mValsA_);
                splineModB_ = spline.invSplineModuli(gridDimensionB_, mValsB_);
                splineModC_ = spline.invSplineModuli(gridDimensionC_, mValsC_);
            } else {
                // For conventional PME we order the m values as 0, 1, 2, 3, .., Kmax, -Kmax, -Kmax+1, .., -2, -1
                // because this is consistent with the ordering of m values that emerge from the FFT.
                for (int ka = 0; ka < myNumKSumTermsA_; ++ka) {
                    mValsA_[ka] = firstKSumTermA_ +
                                  (ka + firstKSumTermA_ >= (gridDimensionA_ + 1) / 2 ? ka - gridDimensionA_ : ka);
                }
                for (int kb = 0; kb < myNumKSumTermsB_; ++kb) {
                    mValsB_[kb] = firstKSumTermB_ +
                                  (kb + firstKSumTermB_ >= (gridDimensionB_ + 1) / 2 ? kb - gridDimensionB_ : kb);
                }
                for (int kc = 0; kc < myNumKSumTermsC_; ++kc) {
                    mValsC_[kc] = firstKSumTermC_ +
                                  (kc + firstKSumTermC_ >= (gridDimensionC_ + 1) / 2 ? kc - gridDimensionC_ : kc);
                }
                // Fourier space spline norms.
                Spline spline = Spline(0, 0, splineOrder_, 0);
                auto fullSplineModA = spline.invSplineModuli(gridDimensionA_);
                auto fullSplineModB = spline.invSplineModuli(gridDimensionB_);
                auto fullSplineModC = spline.invSplineModuli(gridDimensionC_);

                scaledRecVecs_ = recVecs_.clone();
                scaledRecVecs_.row(0) *= gridDimensionA_;
                scaledRecVecs_.row(1) *= gridDimensionB_;
                scaledRecVecs_.row(2) *= gridDimensionC_;

                splineModA_.resize(myNumKSumTermsA_);
                splineModB_.resize(myNumKSumTermsB_);
                splineModC_.resize(myNumKSumTermsC_);
                std::copy(&fullSplineModA[firstKSumTermA_], &fullSplineModA[firstKSumTermA_ + myNumKSumTermsA_],
                          splineModA_.begin());
                std::copy(&fullSplineModB[firstKSumTermB_], &fullSplineModB[firstKSumTermB_ + myNumKSumTermsB_],
                          splineModB_.begin());
                std::copy(&fullSplineModC[firstKSumTermC_], &fullSplineModC[firstKSumTermC_ + myNumKSumTermsC_],
                          splineModC_.begin());
            }

            // Set up function pointers by instantiating the appropriate evaluation functions.  We could add many more
            // entries by default here, but don't right now to avoid code bloat.  To add an extra rPower kernel is a
            // trivial cut and paste exercise; just add a new line with the desired 1/R power as the macro's argument.
            switch (rPower) {
                ENABLE_KERNEL_WITH_INVERSE_R_EXPONENT_OF(1);
                ENABLE_KERNEL_WITH_INVERSE_R_EXPONENT_OF(6);
                default:
                    std::string msg("Bad rPower requested.  To fix this, add the appropriate entry in");
                    msg += __FILE__;
                    msg += ", line number ";
                    msg += std::to_string(__LINE__ - 5);
                    throw std::runtime_error(msg.c_str());
                    break;
            }

            subsetOfCAlongA_ = myGridDimensionC_ / numNodesA_;
            subsetOfCAlongB_ = myGridDimensionC_ / numNodesB_;
            subsetOfBAlongC_ = myGridDimensionB_ / numNodesC_;

            workSpace1_ = helpme::vector<Complex>(scratchSize);
            workSpace2_ = helpme::vector<Complex>(scratchSize);
#if HAVE_MKL
            mkl_set_num_threads(nThreads_);
#endif
        }
    }

   public:
    /*!
     * \brief Spread the parameters onto the charge grid.  Generally this shouldn't be called;
     *        use the various computeE() methods instead. This the more efficient version that filters
     *        the atom list and uses pre-computed splines.  Therefore, the splineCache_
     *        member must have been updated via a call to filterAtomsAndBuildSplineCache() first.
     * \param parameterAngMom the angular momentum of the parameters (0 for charges, C6 coefficients, 2 for
     * quadrupoles, etc.). \param parameters the list of parameters associated with each atom (charges, C6
     * coefficients, multipoles, etc...). For a parameter with angular momentum L, a matrix of dimension nAtoms x nL
     * is expected, where nL = (L+1)*(L+2)*(L+3)/6 and the fast running index nL has the ordering
     *
     * 0 X Y Z XX XY YY XZ YZ ZZ XXX XXY XYY YYY XXZ XYZ YYZ XZZ YZZ ZZZ ...
     *
     * i.e. generated by the python loops
     * \code{.py}
     * for L in range(maxAM+1):
     *     for Lz in range(0,L+1):
     *         for Ly in range(0, L - Lz + 1):
     *              Lx  = L - Ly - Lz
     * \endcode
     * \return realGrid the array of discretized parameters (stored in CBA order).
     */
    Real *spreadParameters(int parameterAngMom, const RealMat &parameters) {
        Real *realGrid = reinterpret_cast<Real *>(workSpace1_.data());
        updateAngMomIterator(parameterAngMom);

        int nComponents = nCartesian(parameterAngMom);
        size_t numBA = (size_t)myGridDimensionB_ * myGridDimensionA_;
#pragma omp parallel num_threads(nThreads_)
        {
#ifdef _OPENMP
            int threadID = omp_get_thread_num();
#else
            int threadID = 0;
#endif
            for (size_t row = threadID; row < myGridDimensionC_; row += nThreads_) {
                std::fill(&realGrid[row * numBA], &realGrid[(row + 1) * numBA], Real(0));
            }
            for (const auto &spline : splinesPerThread_[threadID]) {
                const auto &cacheEntry = splineCache_[spline];
                const int &atom = cacheEntry.absoluteAtomNumber;
                const auto &splineA = cacheEntry.aSpline;
                const auto &splineB = cacheEntry.bSpline;
                const auto &splineC = cacheEntry.cSpline;
                spreadParametersImpl(atom, realGrid, nComponents, splineA, splineB, splineC, parameters, threadID);
            }
        }
        return realGrid;
    }

    /*!
     * \brief filterAtomsAndBuildSplineCache builds a list of BSplines for only the atoms to be handled by this node.
     * \param splineDerivativeLevel the derivative level (parameter angular momentum + energy derivative level) of the
     * BSplines.
     * \param coordinates the cartesian coordinates, ordered in memory as {x1,y1,z1,x2,y2,z2,....xN,yN,zN}.
     */
    void filterAtomsAndBuildSplineCache(int splineDerivativeLevel, const RealMat &coords) {
        assertInitialized();
        constexpr float EPS = 1e-6;

        size_t nAtoms = coords.nRows();
        numAtomsPerThread_.resize(nThreads_);
        splinesPerThread_.resize(nThreads_);
        gridAtomList_.resize(gridDimensionC_);

// Classify atoms to their worker threads first, then construct splines for each thread
#pragma omp parallel num_threads(nThreads_)
        {
#ifdef _OPENMP
            int threadID = omp_get_thread_num();
#else
            int threadID = 0;
#endif
            for (size_t row = threadID; row < gridDimensionC_; row += nThreads_) {
                gridAtomList_[row].clear();
            }
            auto &mySplineList = splinesPerThread_[threadID];
            const auto &gridIteratorC = threadedGridIteratorC_[threadID];
            mySplineList.clear();
            size_t myNumAtoms = 0;
            for (int atom = 0; atom < nAtoms; ++atom) {
                const Real *atomCoords = coords[atom];
                Real cCoord = atomCoords[0] * recVecs_(0, 2) + atomCoords[1] * recVecs_(1, 2) +
                              atomCoords[2] * recVecs_(2, 2) - EPS;
                cCoord -= floor(cCoord);
                short cStartingGridPoint = gridDimensionC_ * cCoord;
                size_t thisAtomsThread = cStartingGridPoint % nThreads_;
                const auto &cGridIterator = gridIteratorC_[cStartingGridPoint];
                if (cGridIterator.size() && thisAtomsThread == threadID) {
                    Real aCoord = atomCoords[0] * recVecs_(0, 0) + atomCoords[1] * recVecs_(1, 0) +
                                  atomCoords[2] * recVecs_(2, 0) - EPS;
                    Real bCoord = atomCoords[0] * recVecs_(0, 1) + atomCoords[1] * recVecs_(1, 1) +
                                  atomCoords[2] * recVecs_(2, 1) - EPS;
                    // Make sure the fractional coordinates fall in the range 0 <= s < 1
                    aCoord -= floor(aCoord);
                    bCoord -= floor(bCoord);
                    short aStartingGridPoint = gridDimensionA_ * aCoord;
                    short bStartingGridPoint = gridDimensionB_ * bCoord;
                    const auto &aGridIterator = gridIteratorA_[aStartingGridPoint];
                    const auto &bGridIterator = gridIteratorB_[bStartingGridPoint];
                    uint32_t startingGridPoint = cStartingGridPoint * gridDimensionB_ * gridDimensionA_ +
                                                 bStartingGridPoint * gridDimensionA_ + aStartingGridPoint;
                    if (aGridIterator.size() && bGridIterator.size()) {
                        gridAtomList_[cStartingGridPoint].emplace(startingGridPoint, atom);
                        ++myNumAtoms;
                    }
                }
            }
            numAtomsPerThread_[threadID] = myNumAtoms;
        }

        // We could intervene here and do some load balancing by inspecting the list.  Currently
        // the lazy approach of just assuming that the atoms are evenly distributed along c is used.

        size_t numCacheEntries = std::accumulate(numAtomsPerThread_.begin(), numAtomsPerThread_.end(), 0);
        // Now we know how many atoms we loop over the dense list, redefining nAtoms accordingly.
        // The first stage above is to get the number of atoms, so we can avoid calling push_back
        // and thus avoid the many memory allocations.  If the cache is too small, grow it by a
        // certain scale factor to try and minimize allocations in a not-too-wasteful manner.
        if (splineCache_.size() < numCacheEntries) {
            size_t newSize = static_cast<size_t>(1.2 * numCacheEntries);
            for (int atom = splineCache_.size(); atom < newSize; ++atom)
                splineCache_.emplace_back(splineOrder_, splineDerivativeLevel);
        }
        std::vector<size_t> threadOffset(nThreads_, 0);
        for (int thread = 1; thread < nThreads_; ++thread) {
            threadOffset[thread] = threadOffset[thread - 1] + numAtomsPerThread_[thread - 1];
        }

#pragma omp parallel num_threads(nThreads_)
        {
#ifdef _OPENMP
            int threadID = omp_get_thread_num();
#else
            int threadID = 0;
#endif
            size_t entry = threadOffset[threadID];
            for (size_t cRow = threadID; cRow < gridDimensionC_; cRow += nThreads_) {
                for (const auto &gridPointAndAtom : gridAtomList_[cRow]) {
                    size_t atom = gridPointAndAtom.second;
                    const Real *atomCoords = coords[atom];
                    Real aCoord = atomCoords[0] * recVecs_(0, 0) + atomCoords[1] * recVecs_(1, 0) +
                                  atomCoords[2] * recVecs_(2, 0) - EPS;
                    Real bCoord = atomCoords[0] * recVecs_(0, 1) + atomCoords[1] * recVecs_(1, 1) +
                                  atomCoords[2] * recVecs_(2, 1) - EPS;
                    Real cCoord = atomCoords[0] * recVecs_(0, 2) + atomCoords[1] * recVecs_(1, 2) +
                                  atomCoords[2] * recVecs_(2, 2) - EPS;
                    // Make sure the fractional coordinates fall in the range 0 <= s < 1
                    aCoord -= floor(aCoord);
                    bCoord -= floor(bCoord);
                    cCoord -= floor(cCoord);
                    short aStartingGridPoint = gridDimensionA_ * aCoord;
                    short bStartingGridPoint = gridDimensionB_ * bCoord;
                    short cStartingGridPoint = gridDimensionC_ * cCoord;
                    auto &atomSplines = splineCache_[entry++];
                    atomSplines.absoluteAtomNumber = atom;
                    atomSplines.aSpline.update(aStartingGridPoint, gridDimensionA_ * aCoord - aStartingGridPoint,
                                               splineOrder_, splineDerivativeLevel);
                    atomSplines.bSpline.update(bStartingGridPoint, gridDimensionB_ * bCoord - bStartingGridPoint,
                                               splineOrder_, splineDerivativeLevel);
                    atomSplines.cSpline.update(cStartingGridPoint, gridDimensionC_ * cCoord - cStartingGridPoint,
                                               splineOrder_, splineDerivativeLevel);
                }
            }
        }

// Finally, find all of the splines that this thread will need to handle
#pragma omp parallel num_threads(nThreads_)
        {
#ifdef _OPENMP
            int threadID = omp_get_thread_num();
#else
            int threadID = 0;
#endif
            auto &mySplineList = splinesPerThread_[threadID];
            mySplineList.clear();
            const auto &gridIteratorC = threadedGridIteratorC_[threadID];
            size_t count = 0;
            for (size_t atom = 0; atom < numCacheEntries; ++atom) {
                if (gridIteratorC[splineCache_[atom].cSpline.startingGridPoint()].size()) {
                    mySplineList.emplace_back(count);
                }
                ++count;
            }
        }
    }

    /*!
     * \brief cellVolume Compute the volume of the unit cell.
     * \return volume in units consistent with those used to define the lattice vectors.
     */
    Real cellVolume() {
        return boxVecs_(0, 0) * boxVecs_(1, 1) * boxVecs_(2, 2) - boxVecs_(0, 0) * boxVecs_(1, 2) * boxVecs_(2, 1) +
               boxVecs_(0, 1) * boxVecs_(1, 2) * boxVecs_(2, 0) - boxVecs_(0, 1) * boxVecs_(1, 0) * boxVecs_(2, 2) +
               boxVecs_(0, 2) * boxVecs_(1, 0) * boxVecs_(2, 1) - boxVecs_(0, 2) * boxVecs_(1, 1) * boxVecs_(2, 0);
    }

    /*!
     * \brief Sets the unit cell lattice vectors, with units consistent with those used to specify coordinates.
     * \param A the A lattice parameter in units consistent with the coordinates.
     * \param B the B lattice parameter in units consistent with the coordinates.
     * \param C the C lattice parameter in units consistent with the coordinates.
     * \param alpha the alpha lattice parameter in degrees.
     * \param beta the beta lattice parameter in degrees.
     * \param gamma the gamma lattice parameter in degrees.
     * \param latticeType how to arrange the lattice vectors.  Options are
     * ShapeMatrix: enforce a symmetric representation of the lattice vectors [c.f. S. Nosé and M. L. Klein,
     *              Mol. Phys. 50 1055 (1983)] particularly appendix C.
     * XAligned: make the A vector coincide with the X axis, the B vector fall in the XY plane, and the C vector
     *           take the appropriate alignment to completely define the system.
     */
    void setLatticeVectors(Real A, Real B, Real C, Real alpha, Real beta, Real gamma, LatticeType latticeType) {
        if (A != cellA_ || B != cellB_ || C != cellC_ || alpha != cellAlpha_ || beta != cellBeta_ ||
            gamma != cellGamma_ || latticeType != latticeType_) {
            if (latticeType == LatticeType::ShapeMatrix) {
                RealMat HtH(3, 3);
                HtH(0, 0) = A * A;
                HtH(1, 1) = B * B;
                HtH(2, 2) = C * C;
                const float TOL = 1e-4f;
                // Check for angles very close to 90, to avoid noise from the eigensolver later on.
                HtH(0, 1) = HtH(1, 0) = std::abs(gamma - 90) < TOL ? 0 : A * B * std::cos(PI * gamma / 180);
                HtH(0, 2) = HtH(2, 0) = std::abs(beta - 90) < TOL ? 0 : A * C * std::cos(PI * beta / 180);
                HtH(1, 2) = HtH(2, 1) = std::abs(alpha - 90) < TOL ? 0 : B * C * std::cos(PI * alpha / 180);

                auto eigenTuple = HtH.diagonalize();
                RealMat evalsReal = std::get<0>(eigenTuple);
                RealMat evecs = std::get<1>(eigenTuple);
                for (int i = 0; i < 3; ++i) evalsReal(i, 0) = sqrt(evalsReal(i, 0));
                boxVecs_.setZero();
                for (int i = 0; i < 3; ++i) {
                    for (int j = 0; j < 3; ++j) {
                        for (int k = 0; k < 3; ++k) {
                            boxVecs_(i, j) += evecs(i, k) * evecs(j, k) * evalsReal(k, 0);
                        }
                    }
                }
                recVecs_ = boxVecs_.inverse();
            } else if (latticeType == LatticeType::XAligned) {
                boxVecs_(0, 0) = A;
                boxVecs_(0, 1) = 0;
                boxVecs_(0, 2) = 0;
                boxVecs_(1, 0) = B * std::cos(PI / 180 * gamma);
                boxVecs_(1, 1) = B * std::sin(PI / 180 * gamma);
                boxVecs_(1, 2) = 0;
                boxVecs_(2, 0) = C * std::cos(PI / 180 * beta);
                boxVecs_(2, 1) = (B * C * cos(PI / 180 * alpha) - boxVecs_(2, 0) * boxVecs_(1, 0)) / boxVecs_(1, 1);
                boxVecs_(2, 2) = std::sqrt(C * C - boxVecs_(2, 0) * boxVecs_(2, 0) - boxVecs_(2, 1) * boxVecs_(2, 1));
            } else {
                throw std::runtime_error("Unknown lattice type in setLatticeVectors");
            }
            recVecs_ = boxVecs_.inverse();
            scaledRecVecs_ = recVecs_.clone();
            scaledRecVecs_.row(0) *= gridDimensionA_;
            scaledRecVecs_.row(1) *= gridDimensionB_;
            scaledRecVecs_.row(2) *= gridDimensionC_;
            cellA_ = A;
            cellB_ = B;
            cellC_ = C;
            cellAlpha_ = alpha;
            cellBeta_ = beta;
            cellGamma_ = gamma;
            latticeType_ = latticeType;
            unitCellHasChanged_ = true;
        } else {
            unitCellHasChanged_ = false;
        }
    }

    /*!
     * \brief Performs the forward 3D FFT of the discretized parameter grid using the compressed PME algorithm.
     * \param realGrid the array of discretized parameters (stored in CBA order,
     *                 with A being the fast running index) to be transformed.
     * \return Pointer to the transformed grid, which is stored in one of the buffers in BAC order.
     */
    Real *compressedForwardTransform(Real *realGrid) {
        Real *__restrict__ buffer1, *__restrict__ buffer2;
        if (realGrid == reinterpret_cast<Real *>(workSpace1_.data())) {
            buffer1 = reinterpret_cast<Real *>(workSpace2_.data());
            buffer2 = reinterpret_cast<Real *>(workSpace1_.data());
        } else {
            buffer1 = reinterpret_cast<Real *>(workSpace1_.data());
            buffer2 = reinterpret_cast<Real *>(workSpace2_.data());
        }
        // Transform A index
        contractABxCWithDxC<Real>(realGrid, compressionCoefficientsA_[0], myGridDimensionC_ * myGridDimensionB_,
                                  myGridDimensionA_, numKSumTermsA_, buffer1);
        // Sort CBA->CAB
        permuteABCtoACB(buffer1, myGridDimensionC_, myGridDimensionB_, numKSumTermsA_, buffer2, nThreads_);
        // Transform B index
        contractABxCWithDxC<Real>(buffer2, compressionCoefficientsB_[0], myGridDimensionC_ * numKSumTermsA_,
                                  myGridDimensionB_, numKSumTermsB_, buffer1);
        // Sort CAB->BAC
        permuteABCtoCBA(buffer1, myGridDimensionC_, numKSumTermsA_, numKSumTermsB_, buffer2, nThreads_);
        // Transform C index
        contractABxCWithDxC<Real>(buffer2, compressionCoefficientsC_[0], numKSumTermsB_ * numKSumTermsA_,
                                  myGridDimensionC_, numKSumTermsC_, buffer1);

#if HAVE_MPI == 1
        int numNodes = numNodesA_ * numNodesB_ * numNodesC_;
        if (numNodes > 1) {
            // Resort the data to be grouped by node, for communication
            for (int node = 0; node < numNodes; ++node) {
                int nodeStartA = myNumKSumTermsA_ * (node % numNodesA_);
                int nodeStartB = myNumKSumTermsB_ * ((node % (numNodesB_ * numNodesA_)) / numNodesA_);
                int nodeStartC = myNumKSumTermsC_ * (node / (numNodesB_ * numNodesA_));
                Real *outPtr = buffer2 + node * myNumKSumTermsA_ * myNumKSumTermsB_ * myNumKSumTermsC_;
                for (int B = 0; B < myNumKSumTermsB_; ++B) {
                    const Real *inPtrB = buffer1 + (nodeStartB + B) * numKSumTermsA_ * numKSumTermsC_;
                    for (int A = 0; A < myNumKSumTermsA_; ++A) {
                        const Real *inPtrBA = inPtrB + (nodeStartA + A) * numKSumTermsC_;
                        const Real *inPtrBAC = inPtrBA + nodeStartC;
                        std::copy(inPtrBAC, inPtrBAC + myNumKSumTermsC_, outPtr);
                        outPtr += myNumKSumTermsC_;
                    }
                }
            }
            mpiCommunicator_->reduceScatterBlock(buffer2, buffer1,
                                                 myNumKSumTermsA_ * myNumKSumTermsB_ * myNumKSumTermsC_);
        }
#endif
        return buffer1;
    }

    /*!
     * \brief Performs the forward 3D FFT of the discretized parameter grid.
     * \param realGrid the array of discretized parameters (stored in CBA order,
     *                 with A being the fast running index) to be transformed.
     * \return Pointer to the transformed grid, which is stored in one of the buffers in BAC order.
     */
    Complex *forwardTransform(Real *realGrid) {
        Real *__restrict__ realCBA;
        Complex *__restrict__ buffer1, *__restrict__ buffer2;
        if (realGrid == reinterpret_cast<Real *>(workSpace1_.data())) {
            realCBA = reinterpret_cast<Real *>(workSpace2_.data());
            buffer1 = workSpace2_.data();
            buffer2 = workSpace1_.data();
        } else {
            realCBA = reinterpret_cast<Real *>(workSpace1_.data());
            buffer1 = workSpace1_.data();
            buffer2 = workSpace2_.data();
        }

#if HAVE_MPI == 1
        if (numNodesA_ > 1) {
            // Communicate A along columns
            mpiCommunicatorA_->allToAll(realGrid, realCBA, subsetOfCAlongA_ * myGridDimensionA_ * myGridDimensionB_);
            // Resort the data to end up with realGrid holding a full row of A data, for B pencil and C subset.
            for (int c = 0; c < subsetOfCAlongA_; ++c) {
                Real *outC = realGrid + c * myGridDimensionB_ * gridDimensionA_;
                for (int b = 0; b < myGridDimensionB_; ++b) {
                    for (int chunk = 0; chunk < numNodesA_; ++chunk) {
                        Real *inPtr = realCBA + (chunk * subsetOfCAlongA_ + c) * myGridDimensionB_ * myGridDimensionA_ +
                                      b * myGridDimensionA_;
                        std::copy(inPtr, inPtr + myGridDimensionA_,
                                  outC + b * gridDimensionA_ + chunk * myGridDimensionA_);
                    }
                }
            }
        }
#endif
        // Each parallel node allocates buffers of length dimA/(2 numNodesA)+1 for A, leading to a total of
        // dimA/2 + numNodesA = complexDimA+numNodesA-1 if dimA is even
        // and
        // numNodesA (dimA-1)/2 + numNodesA = complexDimA + numNodesA/2-1 if dimA is odd
        // We just allocate the larger size here, remembering that the final padding values on the last node
        // will all be allocated to zero and will not contribute to the final answer.
        const size_t scratchRowDim = complexGridDimensionA_ + numNodesA_ - 1;
        helpme::vector<Complex> buffer(nThreads_ * scratchRowDim);

// A transform, with instant sort to CAB ordering for each local block
#pragma omp parallel num_threads(nThreads_)
        {
#ifdef _OPENMP
            int threadID = omp_get_thread_num();
#else
            int threadID = 0;
#endif
            auto scratch = &buffer[threadID * scratchRowDim];
#pragma omp for
            for (int c = 0; c < subsetOfCAlongA_; ++c) {
                for (int b = 0; b < myGridDimensionB_; ++b) {
                    Real *gridPtr = realGrid + c * myGridDimensionB_ * gridDimensionA_ + b * gridDimensionA_;
                    fftHelperA_.transform(gridPtr, scratch);
                    for (int chunk = 0; chunk < numNodesA_; ++chunk) {
                        for (int a = 0; a < myComplexGridDimensionA_; ++a) {
                            buffer1[(chunk * subsetOfCAlongA_ + c) * myComplexGridDimensionA_ * myGridDimensionB_ +
                                    a * myGridDimensionB_ + b] = scratch[chunk * myComplexGridDimensionA_ + a];
                        }
                    }
                }
            }
        }

#if HAVE_MPI == 1
        // Communicate A back to blocks
        if (numNodesA_ > 1) {
            mpiCommunicatorA_->allToAll(buffer1, buffer2,
                                        subsetOfCAlongA_ * myComplexGridDimensionA_ * myGridDimensionB_);
            std::swap(buffer1, buffer2);
        }

        // Communicate B along rows
        if (numNodesB_ > 1) {
            mpiCommunicatorB_->allToAll(buffer1, buffer2,
                                        subsetOfCAlongB_ * myComplexGridDimensionA_ * myGridDimensionB_);
            // Resort the data to end up with the buffer holding a full row of B data, for A pencil and C subset.
            for (int c = 0; c < subsetOfCAlongB_; ++c) {
                Complex *cPtr = buffer1 + c * myComplexGridDimensionA_ * gridDimensionB_;
                for (int a = 0; a < myComplexGridDimensionA_; ++a) {
                    for (int chunk = 0; chunk < numNodesB_; ++chunk) {
                        Complex *inPtr = buffer2 +
                                         (chunk * subsetOfCAlongB_ + c) * myComplexGridDimensionA_ * myGridDimensionB_ +
                                         a * myGridDimensionB_;
                        std::copy(inPtr, inPtr + myGridDimensionB_,
                                  cPtr + a * gridDimensionB_ + chunk * myGridDimensionB_);
                    }
                }
            }
        }
#endif

        // B transform
        size_t numCA = (size_t)subsetOfCAlongB_ * myComplexGridDimensionA_;
#pragma omp parallel for num_threads(nThreads_)
        for (size_t ca = 0; ca < numCA; ++ca) {
            fftHelperB_.transform(buffer1 + ca * gridDimensionB_, FFTW_FORWARD);
        }

#if HAVE_MPI == 1
        if (numNodesB_ > 1) {
            for (int c = 0; c < subsetOfCAlongB_; ++c) {
                Complex *zPtr = buffer1 + c * myComplexGridDimensionA_ * gridDimensionB_;
                for (int a = 0; a < myComplexGridDimensionA_; ++a) {
                    for (int chunk = 0; chunk < numNodesB_; ++chunk) {
                        Complex *inPtr = zPtr + a * gridDimensionB_ + chunk * myGridDimensionB_;
                        Complex *outPtr =
                            buffer2 + (chunk * subsetOfCAlongB_ + c) * myComplexGridDimensionA_ * myGridDimensionB_ +
                            a * myGridDimensionB_;
                        std::copy(inPtr, inPtr + myGridDimensionB_, outPtr);
                    }
                }
            }
            // Communicate B back to blocks
            mpiCommunicatorB_->allToAll(buffer2, buffer1,
                                        subsetOfCAlongB_ * myComplexGridDimensionA_ * myGridDimensionB_);
        }
#endif
        // sort local blocks from CAB to BAC order
        permuteABCtoCBA(buffer1, myGridDimensionC_, myComplexGridDimensionA_, myGridDimensionB_, buffer2, nThreads_);

#if HAVE_MPI == 1
        if (numNodesC_ > 1) {
            // Communicate C along columns
            mpiCommunicatorC_->allToAll(buffer2, buffer1,
                                        subsetOfBAlongC_ * myComplexGridDimensionA_ * myGridDimensionC_);
            for (int b = 0; b < subsetOfBAlongC_; ++b) {
                Complex *outPtrB = buffer2 + b * myComplexGridDimensionA_ * gridDimensionC_;
                for (int a = 0; a < myComplexGridDimensionA_; ++a) {
                    Complex *outPtrBA = outPtrB + a * gridDimensionC_;
                    for (int chunk = 0; chunk < numNodesC_; ++chunk) {
                        Complex *inPtr = buffer1 +
                                         (chunk * subsetOfBAlongC_ + b) * myComplexGridDimensionA_ * myGridDimensionC_ +
                                         a * myGridDimensionC_;
                        std::copy(inPtr, inPtr + myGridDimensionC_, outPtrBA + chunk * myGridDimensionC_);
                    }
                }
            }
        }
#endif
        // C transform
        size_t numBA = (size_t)subsetOfBAlongC_ * myComplexGridDimensionA_;
#pragma omp parallel for num_threads(nThreads_)
        for (size_t ba = 0; ba < numBA; ++ba) {
            fftHelperC_.transform(buffer2 + ba * gridDimensionC_, FFTW_FORWARD);
        }

        return buffer2;
    }

    /*!
     * \brief Performs the inverse 3D FFT.
     * \param convolvedGrid the complex array of discretized parameters convolved with the influence function
     *                      (stored in BAC order, with C being the fast running index) to be transformed.
     * \return Pointer to the potential grid, which is stored in one of the buffers in CBA order.
     */
    Real *inverseTransform(Complex *convolvedGrid) {
        Complex *__restrict__ buffer1, *__restrict__ buffer2;
        // Setup scratch, taking care not to overwrite the convolved grid.
        if (convolvedGrid == workSpace1_.data()) {
            buffer1 = workSpace2_.data();
            buffer2 = workSpace1_.data();
        } else {
            buffer1 = workSpace1_.data();
            buffer2 = workSpace2_.data();
        }

        // C transform
        size_t numYX = (size_t)subsetOfBAlongC_ * myComplexGridDimensionA_;
#pragma omp parallel for num_threads(nThreads_)
        for (size_t yx = 0; yx < numYX; ++yx) {
            fftHelperC_.transform(convolvedGrid + yx * gridDimensionC_, FFTW_BACKWARD);
        }

#if HAVE_MPI == 1
        if (numNodesC_ > 1) {
            // Communicate C back to blocks
            for (int b = 0; b < subsetOfBAlongC_; ++b) {
                Complex *inPtrB = convolvedGrid + b * myComplexGridDimensionA_ * gridDimensionC_;
                for (int a = 0; a < myComplexGridDimensionA_; ++a) {
                    Complex *inPtrBA = inPtrB + a * gridDimensionC_;
                    for (int chunk = 0; chunk < numNodesC_; ++chunk) {
                        Complex *inPtrBAC = inPtrBA + chunk * myGridDimensionC_;
                        Complex *outPtr =
                            buffer1 + (chunk * subsetOfBAlongC_ + b) * myComplexGridDimensionA_ * myGridDimensionC_ +
                            a * myGridDimensionC_;
                        std::copy(inPtrBAC, inPtrBAC + myGridDimensionC_, outPtr);
                    }
                }
            }
            mpiCommunicatorC_->allToAll(buffer1, buffer2,
                                        subsetOfBAlongC_ * myComplexGridDimensionA_ * myGridDimensionC_);
        }
#endif

        // sort local blocks from BAC to CAB order
        permuteABCtoCBA(buffer2, myGridDimensionB_, myComplexGridDimensionA_, myGridDimensionC_, buffer1, nThreads_);

#if HAVE_MPI == 1
        // Communicate B along rows
        if (numNodesB_ > 1) {
            mpiCommunicatorB_->allToAll(buffer1, buffer2,
                                        subsetOfCAlongB_ * myComplexGridDimensionA_ * myGridDimensionB_);
            // Resort the data to end up with the buffer holding a full row of B data, for A pencil and C subset.
            for (int c = 0; c < subsetOfCAlongB_; ++c) {
                Complex *cPtr = buffer1 + c * myComplexGridDimensionA_ * gridDimensionB_;
                for (int a = 0; a < myComplexGridDimensionA_; ++a) {
                    for (int chunk = 0; chunk < numNodesB_; ++chunk) {
                        Complex *inPtr = buffer2 +
                                         (chunk * subsetOfCAlongB_ + c) * myComplexGridDimensionA_ * myGridDimensionB_ +
                                         a * myGridDimensionB_;
                        std::copy(inPtr, inPtr + myGridDimensionB_,
                                  cPtr + a * gridDimensionB_ + chunk * myGridDimensionB_);
                    }
                }
            }
        }
#endif

        // B transform with instant sort of local blocks from CAB -> CBA order
        size_t numCA = (size_t)subsetOfCAlongB_ * myComplexGridDimensionA_;
#pragma omp parallel for num_threads(nThreads_)
        for (size_t ca = 0; ca < numCA; ++ca) {
            fftHelperB_.transform(buffer1 + ca * gridDimensionB_, FFTW_BACKWARD);
        }
#pragma omp parallel for num_threads(nThreads_)
        for (int c = 0; c < subsetOfCAlongB_; ++c) {
            for (int a = 0; a < myComplexGridDimensionA_; ++a) {
                int cx = c * myComplexGridDimensionA_ * gridDimensionB_ + a * gridDimensionB_;
                for (int b = 0; b < myGridDimensionB_; ++b) {
                    for (int chunk = 0; chunk < numNodesB_; ++chunk) {
                        int cb = (chunk * subsetOfCAlongB_ + c) * myGridDimensionB_ * myComplexGridDimensionA_ +
                                 b * myComplexGridDimensionA_;
                        buffer2[cb + a] = buffer1[cx + chunk * myGridDimensionB_ + b];
                    }
                }
            }
        }

#if HAVE_MPI == 1
        // Communicate B back to blocks
        if (numNodesB_ > 1) {
            mpiCommunicatorB_->allToAll(buffer2, buffer1,
                                        subsetOfCAlongB_ * myComplexGridDimensionA_ * myGridDimensionB_);
        } else {
            std::swap(buffer1, buffer2);
        }

        // Communicate A along rows
        if (numNodesA_ > 1) {
            mpiCommunicatorA_->allToAll(buffer1, buffer2,
                                        subsetOfCAlongA_ * myComplexGridDimensionA_ * myGridDimensionB_);
            // Resort the data to end up with the buffer holding a full row of A data, for B pencil and C subset.
            for (int c = 0; c < subsetOfCAlongA_; ++c) {
                Complex *cPtr = buffer1 + c * myGridDimensionB_ * complexGridDimensionA_;
                for (int b = 0; b < myGridDimensionB_; ++b) {
                    for (int chunk = 0; chunk < numNodesA_; ++chunk) {
                        Complex *inPtr = buffer2 +
                                         (chunk * subsetOfCAlongA_ + c) * myComplexGridDimensionA_ * myGridDimensionB_ +
                                         b * myComplexGridDimensionA_;
                        std::copy(inPtr, inPtr + myComplexGridDimensionA_,
                                  cPtr + b * complexGridDimensionA_ + chunk * myComplexGridDimensionA_);
                    }
                }
            }
        }
#else
        std::swap(buffer1, buffer2);
#endif

        // A transform
        Real *realGrid = reinterpret_cast<Real *>(buffer2);
#pragma omp parallel for num_threads(nThreads_)
        for (int cb = 0; cb < subsetOfCAlongA_ * myGridDimensionB_; ++cb) {
            fftHelperA_.transform(buffer1 + cb * complexGridDimensionA_, realGrid + cb * gridDimensionA_);
        }

#if HAVE_MPI == 1
        // Communicate A back to blocks
        if (numNodesA_ > 1) {
            Real *realGrid2 = reinterpret_cast<Real *>(buffer1);
            for (int c = 0; c < subsetOfCAlongA_; ++c) {
                Real *cPtr = realGrid + c * myGridDimensionB_ * gridDimensionA_;
                for (int b = 0; b < myGridDimensionB_; ++b) {
                    for (int chunk = 0; chunk < numNodesA_; ++chunk) {
                        Real *outPtr = realGrid2 +
                                       (chunk * subsetOfCAlongA_ + c) * myGridDimensionB_ * myGridDimensionA_ +
                                       b * myGridDimensionA_;
                        Real *inPtr = cPtr + b * gridDimensionA_ + chunk * myGridDimensionA_;
                        std::copy(inPtr, inPtr + myGridDimensionA_, outPtr);
                    }
                }
            }
            mpiCommunicatorA_->allToAll(realGrid2, realGrid, subsetOfCAlongA_ * myGridDimensionB_ * myGridDimensionA_);
        }
#endif
        return realGrid;
    }

    /*!
     * \brief Performs the backward 3D FFT of the discretized parameter grid using the compressed PME algorithm.
     * \param reciprocalGrid the reciprocal space potential grid (stored in BAC order,
     *                 with C being the fast running index) to be transformed.
     * \return Pointer to the transformed grid, which is stored in one of the buffers in CBA order.
     */
    Real *compressedInverseTransform(Real *reciprocalGrid) {
        Real *__restrict__ buffer1, *__restrict__ buffer2;
        if (reciprocalGrid == reinterpret_cast<Real *>(workSpace1_.data())) {
            buffer1 = reinterpret_cast<Real *>(workSpace2_.data());
            buffer2 = reinterpret_cast<Real *>(workSpace1_.data());
        } else {
            buffer1 = reinterpret_cast<Real *>(workSpace1_.data());
            buffer2 = reinterpret_cast<Real *>(workSpace2_.data());
        }
        // Make the reciprocal dimensions the fast running indices
        compressionCoefficientsA_.transposeInPlace();
        compressionCoefficientsB_.transposeInPlace();
        compressionCoefficientsC_.transposeInPlace();

#if HAVE_MPI == 1
        int numNodes = numNodesA_ * numNodesB_ * numNodesC_;
        if (numNodes > 1) {
            mpiCommunicator_->allGather(buffer2, buffer1, myNumKSumTermsA_ * myNumKSumTermsB_ * myNumKSumTermsC_);
            // Resort the data to be grouped by node, for communication
            for (int node = 0; node < numNodes; ++node) {
                int nodeStartA = myNumKSumTermsA_ * (node % numNodesA_);
                int nodeStartB = myNumKSumTermsB_ * ((node % (numNodesB_ * numNodesA_)) / numNodesA_);
                int nodeStartC = myNumKSumTermsC_ * (node / (numNodesB_ * numNodesA_));
                Real *inPtr = buffer1 + node * myNumKSumTermsA_ * myNumKSumTermsB_ * myNumKSumTermsC_;
                for (int B = 0; B < myNumKSumTermsB_; ++B) {
                    Real *outPtrB = buffer2 + (nodeStartB + B) * numKSumTermsA_ * numKSumTermsC_;
                    for (int A = 0; A < myNumKSumTermsA_; ++A) {
                        Real *outPtrBA = outPtrB + (nodeStartA + A) * numKSumTermsC_;
                        Real *outPtrBAC = outPtrBA + nodeStartC;
                        std::copy(inPtr, inPtr + myNumKSumTermsC_, outPtrBAC);
                        inPtr += myNumKSumTermsC_;
                    }
                }
            }
        }
#endif

        // Transform C index
        contractABxCWithDxC<Real>(buffer2, compressionCoefficientsC_[0], numKSumTermsB_ * numKSumTermsA_,
                                  numKSumTermsC_, myGridDimensionC_, buffer1);
        // Sort BAC->CAB
        permuteABCtoCBA(buffer1, numKSumTermsB_, numKSumTermsA_, myGridDimensionC_, buffer2, nThreads_);
        // Transform B index
        contractABxCWithDxC<Real>(buffer2, compressionCoefficientsB_[0], myGridDimensionC_ * numKSumTermsA_,
                                  numKSumTermsB_, myGridDimensionB_, buffer1);
        // Sort CAB->CBA
        permuteABCtoACB(buffer1, myGridDimensionC_, numKSumTermsA_, myGridDimensionB_, buffer2, nThreads_);
        // Transform A index
        contractABxCWithDxC<Real>(buffer2, compressionCoefficientsA_[0], myGridDimensionC_ * myGridDimensionB_,
                                  numKSumTermsA_, myGridDimensionA_, buffer1);

        // Make the grid dimensions the fast running indices again
        compressionCoefficientsA_.transposeInPlace();
        compressionCoefficientsB_.transposeInPlace();
        compressionCoefficientsC_.transposeInPlace();

        return buffer1;
    }

    /*!
     * \brief convolveE performs the convolution on a compressed PME transformed Grid
     * \param transformedGrid the pointer to the complex array holding the transformed grid in YXZ ordering.
     * \return the reciprocal space energy.
     */
    Real convolveE(Real *transformedGrid) {
        updateInfluenceFunction();
        size_t nxz = (size_t)myNumKSumTermsA_ * myNumKSumTermsC_;
        size_t nyxz = myNumKSumTermsB_ * nxz;
        bool iAmNodeZero = (myNodeRankA_ == 0 && myNodeRankB_ == 0 && myNodeRankC_ == 0);
        Real *influenceFunction = cachedInfluenceFunction_.data();
        Real energy = 0;
        if (rPower_ > 3 && iAmNodeZero) {
            // Kernels with rPower>3 are absolutely convergent and should have the m=0 term present.
            // To compute it we need sum_ij c(i)c(j), which can be obtained from the structure factor norm.
            Real prefac = 2 * scaleFactor_ * PI * SQRTPI * pow(kappa_, rPower_ - 3) /
                          ((rPower_ - 3) * nonTemplateGammaComputer<Real>(rPower_) * cellVolume());
            energy += prefac * transformedGrid[0] * transformedGrid[0];
        }
        if (iAmNodeZero) transformedGrid[0] = 0;
// Writing the three nested loops in one allows for better load balancing in parallel.
#pragma omp parallel for reduction(+ : energy) num_threads(nThreads_)
        for (size_t yxz = 0; yxz < nyxz; ++yxz) {
            energy += transformedGrid[yxz] * transformedGrid[yxz] * influenceFunction[yxz];
            transformedGrid[yxz] *= influenceFunction[yxz];
        }
        return energy / 2;
    }

    /*!
     * \brief convolveE performs the convolution of a standard PME transformed grid
     * \param transformedGrid the pointer to the complex array holding the transformed grid in YXZ ordering.
     * \return the reciprocal space energy.
     */
    Real convolveE(Complex *transformedGrid) {
        updateInfluenceFunction();
        size_t nxz = (size_t)myNumKSumTermsA_ * myNumKSumTermsC_;
        size_t nyxz = myNumKSumTermsB_ * nxz;
        bool iAmNodeZero = (myNodeRankA_ == 0 && myNodeRankB_ == 0 && myNodeRankC_ == 0);
        Real *influenceFunction = cachedInfluenceFunction_.data();
        bool useConjugateSymmetry = algorithmType_ == AlgorithmType::PME;

        Real energy = 0;
        if (rPower_ > 3 && iAmNodeZero) {
            // Kernels with rPower>3 are absolutely convergent and should have the m=0 term present.
            // To compute it we need sum_ij c(i)c(j), which can be obtained from the structure factor norm.
            Real prefac = 2 * scaleFactor_ * PI * SQRTPI * pow(kappa_, rPower_ - 3) /
                          ((rPower_ - 3) * nonTemplateGammaComputer<Real>(rPower_) * cellVolume());
            energy += prefac * std::norm(transformedGrid[0]);
        }
        if (iAmNodeZero) transformedGrid[0] = Complex(0, 0);
        const size_t numCTerms(myNumKSumTermsC_);
#pragma omp parallel for reduction(+ : energy) num_threads(nThreads_)
        for (size_t yxz = 0; yxz < nyxz; ++yxz) {
            size_t xz = yxz % nxz;
            int kx = firstKSumTermA_ + xz / numCTerms;
            // We only loop over the first nx/2+1 x values; this
            // accounts for the "missing" complex conjugate values.
            Real permPrefac = useConjugateSymmetry && kx != 0 && kx != complexGridDimensionA_ - 1 ? 2 : 1;
            Real structFactorNorm = transformedGrid[yxz].real() * transformedGrid[yxz].real() +
                                    transformedGrid[yxz].imag() * transformedGrid[yxz].imag();
            energy += permPrefac * structFactorNorm * influenceFunction[yxz];
            transformedGrid[yxz] *= influenceFunction[yxz];
        }
        return energy / 2;
    }

    /*!
     * \brief convolveEV A wrapper to determine the correct convolution function to call, including virial, for
     *        the compressed PME algorithm.
     * \param transformedGrid the pointer to the Fourier space array holding the transformed grid in YXZ ordering.
     * \param convolvedGrid the (output) pointer to the Fourier space array holding the convolved grid in YXZ ordering.
     * \param virial a vector of length 6 containing the unique virial elements, in the order XX XY YY XZ YZ ZZ.
     *        This vector is incremented, not assigned.
     * \return the reciprocal space energy.
     */
    Real convolveEV(const Real *transformedGrid, Real *&convolvedGrid, RealMat &virial) {
        convolvedGrid = transformedGrid == reinterpret_cast<Real *>(workSpace1_.data())
                            ? reinterpret_cast<Real *>(workSpace2_.data())
                            : reinterpret_cast<Real *>(workSpace1_.data());
        return convolveEVCompressedFxn_(
            myNumKSumTermsA_, myNumKSumTermsB_, myNumKSumTermsC_, firstKSumTermA_, firstKSumTermB_, firstKSumTermC_,
            scaleFactor_, transformedGrid, convolvedGrid, recVecs_, cellVolume(), kappa_, &splineModA_[0],
            &splineModB_[0], &splineModC_[0], mValsA_.data(), mValsB_.data(), mValsC_.data(), virial, nThreads_);
    }

    /*!
     * \brief convolveEV A wrapper to determine the correct convolution function to call, including virial, for
     *        the conventional PME algorithm.
     * \param transformedGrid the pointer to the complex array holding the transformed grid in YXZ ordering.
     * \param virial a vector of length 6 containing the unique virial elements, in the order XX XY YY XZ YZ ZZ.
     *        This vector is incremented, not assigned.
     * \return the reciprocal space energy.
     */
    Real convolveEV(Complex *transformedGrid, RealMat &virial) {
        return convolveEVFxn_(true, complexGridDimensionA_, myNumKSumTermsA_, myNumKSumTermsB_, myNumKSumTermsC_,
                              firstKSumTermA_, firstKSumTermB_, firstKSumTermC_, scaleFactor_, transformedGrid,
                              recVecs_, cellVolume(), kappa_, &splineModA_[0], &splineModB_[0], &splineModC_[0],
                              mValsA_.data(), mValsB_.data(), mValsC_.data(), virial, nThreads_);
    }

    /*!
     * \brief Probes the potential grid to get the forces.  Generally this shouldn't be called;
     *        use the various computeE() methods instead.  This is the faster version that uses
     *        the filtered atom list and uses pre-computed splines.  Therefore, the splineCache_
     *        member must have been updated via a call to filterAtomsAndBuildSplineCache() first.
     *
     * \param potentialGrid pointer to the array containing the potential, in ZYX order.
     * \param parameterAngMom the angular momentum of the parameters (0 for charges, C6 coefficients, 2 for
     * quadrupoles, etc.). \param parameters the list of parameters associated with each atom (charges, C6
     * coefficients, multipoles, etc...). For a parameter with angular momentum L, a matrix of dimension nAtoms x nL
     * is expected, where nL = (L+1)*(L+2)*(L+3)/6 and the fast running index nL has the ordering
     *
     * 0 X Y Z XX XY YY XZ YZ ZZ XXX XXY XYY YYY XXZ XYZ YYZ XZZ YZZ ZZZ ...
     *
     * i.e. generated by the python loops
     * \code{.py}
     * for L in range(maxAM+1):
     *     for Lz in range(0,L+1):
     *         for Ly in range(0, L - Lz + 1):
     *              Lx  = L - Ly - Lz
     * \endcode
     * \param forces a Nx3 matrix of the forces, ordered in memory as {Fx1,Fy1,Fz1,Fx2,Fy2,Fz2,....FxN,FyN,FzN}.
     */
    void probeGrid(const Real *potentialGrid, int parameterAngMom, const RealMat &parameters, RealMat &forces) {
        updateAngMomIterator(parameterAngMom + 1);
        int nComponents = nCartesian(parameterAngMom);
        int nForceComponents = nCartesian(parameterAngMom + 1);
        const Real *paramPtr = parameters[0];
        // Find how many multiples of the cache line size are needed
        // to ensure that each thread hits a unique page.
        size_t rowSize = std::ceil(nForceComponents / cacheLineSizeInReals_) * cacheLineSizeInReals_;
        if (fractionalPhis_.nRows() != nThreads_ || fractionalPhis_.nCols() != rowSize) {
            fractionalPhis_ = RealMat(nThreads_, rowSize);
        }
        size_t nAtoms = std::accumulate(numAtomsPerThread_.begin(), numAtomsPerThread_.end(), 0);
#pragma omp parallel num_threads(nThreads_)
        {
#ifdef _OPENMP
            int threadID = omp_get_thread_num();
#else
            int threadID = 0;
#endif
#pragma omp for
            for (size_t atom = 0; atom < nAtoms; ++atom) {
                const auto &cacheEntry = splineCache_[atom];
                const auto &absAtom = cacheEntry.absoluteAtomNumber;
                const auto &splineA = cacheEntry.aSpline;
                const auto &splineB = cacheEntry.bSpline;
                const auto &splineC = cacheEntry.cSpline;
                if (parameterAngMom) {
                    Real *myScratch = fractionalPhis_[threadID % nThreads_];
                    probeGridImpl(absAtom, potentialGrid, nComponents, nForceComponents, splineA, splineB, splineC,
                                  myScratch, parameters, forces[absAtom]);
                } else {
                    probeGridImpl(potentialGrid, splineA, splineB, splineC, paramPtr[absAtom], forces[absAtom]);
                }
            }
        }
    }

    /*!
     * \brief computeESlf computes the Ewald self interaction energy.
     * \param parameterAngMom the angular momentum of the parameters (0 for charges, C6 coefficients, 2 for
     * quadrupoles, etc.). \param parameters the list of parameters associated with each atom (charges, C6
     * coefficients, multipoles, etc...). For a parameter with angular momentum L, a matrix of dimension nAtoms x nL
     * is expected, where nL = (L+1)*(L+2)*(L+3)/6 and the fast running index nL has the ordering
     *
     * 0 X Y Z XX XY YY XZ YZ ZZ XXX XXY XYY YYY XXZ XYZ YYZ XZZ YZZ ZZZ ...
     *
     * i.e. generated by the python loops
     * \code{.py}
     * for L in range(maxAM+1):
     *     for Lz in range(0,L+1):
     *         for Ly in range(0, L - Lz + 1):
     *              Lx  = L - Ly - Lz
     * \endcode
     * \return the self energy.
     */
    Real computeESlf(int parameterAngMom, const RealMat &parameters) {
        assertInitialized();
        return slfEFxn_(parameterAngMom, parameters, kappa_, scaleFactor_);
    }

    /*!
     * \brief computeEDir computes the direct space energy.  This is provided mostly for debugging and testing
     * purposes; generally the host program should provide the pairwise interactions. \param pairList dense list of
     * atom pairs, ordered like i1, j1, i2, j2, i3, j3, ... iN, jN. \param parameterAngMom the angular momentum of
     * the parameters (0 for charges, C6 coefficients, 2 for quadrupoles, etc.). \param parameters the list of
     * parameters associated with each atom (charges, C6 coefficients, multipoles, etc...). For a parameter with
     * angular momentum L, a matrix of dimension nAtoms x nL is expected, where nL = (L+1)*(L+2)*(L+3)/6 and the
     * fast running index nL has the ordering
     *
     * 0 X Y Z XX XY YY XZ YZ ZZ XXX XXY XYY YYY XXZ XYZ YYZ XZZ YZZ ZZZ ...
     *
     * i.e. generated by the python loops
     * \code{.py}
     * for L in range(maxAM+1):
     *     for Lz in range(0,L+1):
     *         for Ly in range(0, L - Lz + 1):
     *              Lx  = L - Ly - Lz
     * \endcode
     * \param coordinates the cartesian coordinates, ordered in memory as {x1,y1,z1,x2,y2,z2,....xN,yN,zN}.
     * \return the direct space energy.
     */
    Real computeEDir(const Matrix<short> &pairList, int parameterAngMom, const RealMat &parameters,
                     const RealMat &coordinates) {
        if (parameterAngMom) throw std::runtime_error("Multipole self terms have not been coded yet.");
        sanityChecks(parameterAngMom, parameters, coordinates);

        Real energy = 0;
        Real kappaSquared = kappa_ * kappa_;
        size_t nPair = pairList.nRows();
        for (int pair = 0; pair < nPair; ++pair) {
            short i = pairList(pair, 0);
            short j = pairList(pair, 1);
            auto deltaR = coordinates.row(j) - coordinates.row(i);
            // TODO: apply minimum image convention.
            Real rSquared = deltaR.dot(deltaR);
            energy += parameters(i, 0) * parameters(j, 0) * dirEFxn_(rSquared, kappaSquared);
        }
        return scaleFactor_ * energy;
    }

    /*!
     * \brief computeEFDir computes the direct space energy and force.  This is provided mostly for debugging and
     * testing purposes; generally the host program should provide the pairwise interactions.
     * \param pairList dense list of atom pairs, ordered like i1, j1, i2, j2, i3, j3, ... iN, jN.
     * \param parameterAngMom the angular momentum of the parameters (0 for charges, C6 coefficients, 2 for
     * quadrupoles, etc.). \param parameters the list of parameters associated with each atom (charges, C6
     * coefficients, multipoles, etc...). For a parameter with angular momentum L, a matrix of dimension nAtoms x nL
     * is expected, where nL = (L+1)*(L+2)*(L+3)/6 and the fast running index nL has the ordering
     *
     * 0 X Y Z XX XY YY XZ YZ ZZ XXX XXY XYY YYY XXZ XYZ YYZ XZZ YZZ ZZZ ...
     *
     * i.e. generated by the python loops
     * \code{.py}
     * for L in range(maxAM+1):
     *     for Lz in range(0,L+1):
     *         for Ly in range(0, L - Lz + 1):
     *              Lx  = L - Ly - Lz
     * \endcode
     * \param coordinates the cartesian coordinates, ordered in memory as {x1,y1,z1,x2,y2,z2,....xN,yN,zN}.
     * \param forces a Nx3 matrix of the forces, ordered in memory as {Fx1,Fy1,Fz1,Fx2,Fy2,Fz2,....FxN,FyN,FzN}.
     *        This matrix is incremented, not assigned.
     * \return the direct space energy.
     */
    Real computeEFDir(const Matrix<short> &pairList, int parameterAngMom, const RealMat &parameters,
                      const RealMat &coordinates, RealMat &forces) {
        if (parameterAngMom) throw std::runtime_error("Multipole self terms have not been coded yet.");
        sanityChecks(parameterAngMom, parameters, coordinates);

        Real energy = 0;
        Real kappaSquared = kappa_ * kappa_;
        size_t nPair = pairList.nRows();
        for (int pair = 0; pair < nPair; ++pair) {
            short i = pairList(pair, 0);
            short j = pairList(pair, 1);
            auto deltaR = coordinates.row(j) - coordinates.row(i);
            // TODO: apply minimum image convention.
            Real rSquared = deltaR.dot(deltaR);
            auto kernels = dirEFFxn_(rSquared, kappa_, kappaSquared);
            Real eKernel = std::get<0>(kernels);
            Real fKernel = std::get<1>(kernels);
            Real prefactor = scaleFactor_ * parameters(i, 0) * parameters(j, 0);
            energy += prefactor * eKernel;
            Real f = -prefactor * fKernel;
            auto force = deltaR.row(0);
            force *= f;
            forces.row(i) -= force;
            forces.row(j) += force;
        }
        return energy;
    }

    /*!
     * \brief computeEFVDir computes the direct space energy, force and virial.  This is provided mostly for
     * debugging and testing purposes; generally the host program should provide the pairwise interactions. \param
     * pairList dense list of atom pairs, ordered like i1, j1, i2, j2, i3, j3, ... iN, jN. \param parameterAngMom
     * the angular momentum of the parameters (0 for charges, C6 coefficients, 2 for quadrupoles, etc.). \param
     * parameters the list of parameters associated with each atom (charges, C6 coefficients, multipoles, etc...).
     * For a parameter with angular momentum L, a matrix of dimension nAtoms x nL is expected, where nL =
     * (L+1)*(L+2)*(L+3)/6 and the fast running index nL has the ordering
     *
     * 0 X Y Z XX XY YY XZ YZ ZZ XXX XXY XYY YYY XXZ XYZ YYZ XZZ YZZ ZZZ ...
     *
     * i.e. generated by the python loops
     * \code{.py}
     * for L in range(maxAM+1):
     *     for Lz in range(0,L+1):
     *         for Ly in range(0, L - Lz + 1):
     *              Lx  = L - Ly - Lz
     * \endcode
     * \param coordinates the cartesian coordinates, ordered in memory as {x1,y1,z1,x2,y2,z2,....xN,yN,zN}.
     * \param forces a Nx3 matrix of the forces, ordered in memory as {Fx1,Fy1,Fz1,Fx2,Fy2,Fz2,....FxN,FyN,FzN}.
     *        This matrix is incremented, not assigned.
     * \param virial a vector of length 6 containing the unique virial elements, in the order XX XY YY XZ YZ ZZ.
     *        This vector is incremented, not assigned.
     * \return the direct space energy.
     */
    Real computeEFVDir(const Matrix<short> &pairList, int parameterAngMom, const RealMat &parameters,
                       const RealMat &coordinates, RealMat &forces, RealMat &virial) {
        if (parameterAngMom) throw std::runtime_error("Multipole self terms have not been coded yet.");
        sanityChecks(parameterAngMom, parameters, coordinates);

        Real energy = 0;
        Real kappaSquared = kappa_ * kappa_;
        size_t nPair = pairList.nRows();
        for (int pair = 0; pair < nPair; ++pair) {
            short i = pairList(pair, 0);
            short j = pairList(pair, 1);
            auto deltaR = coordinates.row(j) - coordinates.row(i);
            // TODO: apply minimum image convention.
            Real rSquared = deltaR.dot(deltaR);
            auto kernels = dirEFFxn_(rSquared, kappa_, kappaSquared);
            Real eKernel = std::get<0>(kernels);
            Real fKernel = std::get<1>(kernels);
            Real prefactor = scaleFactor_ * parameters(i, 0) * parameters(j, 0);
            energy += prefactor * eKernel;
            Real f = -prefactor * fKernel;
            RealMat dRCopy = deltaR.clone();
            auto force = dRCopy.row(0);
            force *= f;
            forces.row(i) -= force;
            forces.row(j) += force;
            virial[0][0] += force[0] * deltaR[0][0];
            virial[0][1] += 0.5f * (force[0] * deltaR[0][1] + force[1] * deltaR[0][0]);
            virial[0][2] += force[1] * deltaR[0][1];
            virial[0][3] += 0.5f * (force[0] * deltaR[0][2] + force[2] * deltaR[0][0]);
            virial[0][4] += 0.5f * (force[1] * deltaR[0][2] + force[2] * deltaR[0][1]);
            virial[0][5] += force[2] * deltaR[0][2];
        }
        return energy;
    }

    /*!
     * \brief computeEAdj computes the adjusted real space energy which extracts the energy for excluded pairs that
     * is present in reciprocal space. This is provided mostly for debugging and testing purposes; generally the
     *        host program should provide the pairwise interactions.
     * \param pairList dense list of atom pairs, ordered like i1, j1, i2, j2, i3, j3, ... iN, jN.
     * \param parameterAngMom the angular momentum of the parameters (0 for charges, C6 coefficients, 2 for
     * quadrupoles, etc.). \param parameters the list of parameters associated with each atom (charges, C6
     * coefficients, multipoles, etc...). For a parameter with angular momentum L, a matrix of dimension nAtoms x nL
     * is expected, where nL = (L+1)*(L+2)*(L+3)/6 and the fast running index nL has the ordering
     *
     * 0 X Y Z XX XY YY XZ YZ ZZ XXX XXY XYY YYY XXZ XYZ YYZ XZZ YZZ ZZZ ...
     *
     * i.e. generated by the python loops
     * \code{.py}
     * for L in range(maxAM+1):
     *     for Lz in range(0,L+1):
     *         for Ly in range(0, L - Lz + 1):
     *              Lx  = L - Ly - Lz
     * \endcode
     * \param coordinates the cartesian coordinates, ordered in memory as {x1,y1,z1,x2,y2,z2,....xN,yN,zN}.
     * \return the adjusted energy.
     */
    Real computeEAdj(const Matrix<short> &pairList, int parameterAngMom, const RealMat &parameters,
                     const RealMat &coordinates) {
        if (parameterAngMom) throw std::runtime_error("Multipole self terms have not been coded yet.");
        sanityChecks(parameterAngMom, parameters, coordinates);

        Real energy = 0;
        Real kappaSquared = kappa_ * kappa_;
        size_t nPair = pairList.nRows();
        for (int pair = 0; pair < nPair; ++pair) {
            short i = pairList(pair, 0);
            short j = pairList(pair, 1);
            auto deltaR = coordinates.row(j) - coordinates.row(i);
            // TODO: apply minimum image convention.
            Real rSquared = deltaR.dot(deltaR);
            energy += parameters(i, 0) * parameters(j, 0) * adjEFxn_(rSquared, kappaSquared);
        }
        return scaleFactor_ * energy;
    }

    /*!
     * \brief computeEFAdj computes the adjusted energy and force.  This is provided mostly for debugging and
     * testing purposes; generally the host program should provide the pairwise interactions. \param pairList dense
     * list of atom pairs, ordered like i1, j1, i2, j2, i3, j3, ... iN, jN. \param parameterAngMom the angular
     * momentum of the parameters (0 for charges, C6 coefficients, 2 for quadrupoles, etc.). \param parameters the
     * list of parameters associated with each atom (charges, C6 coefficients, multipoles, etc...). For a parameter
     * with angular momentum L, a matrix of dimension nAtoms x nL is expected, where nL = (L+1)*(L+2)*(L+3)/6 and
     * the fast running index nL has the ordering
     *
     * 0 X Y Z XX XY YY XZ YZ ZZ XXX XXY XYY YYY XXZ XYZ YYZ XZZ YZZ ZZZ ...
     *
     * i.e. generated by the python loops
     * \code{.py}
     * for L in range(maxAM+1):
     *     for Lz in range(0,L+1):
     *         for Ly in range(0, L - Lz + 1):
     *              Lx  = L - Ly - Lz
     * \endcode
     * \param coordinates the cartesian coordinates, ordered in memory as {x1,y1,z1,x2,y2,z2,....xN,yN,zN}.
     * \param forces a Nx3 matrix of the forces, ordered in memory as {Fx1,Fy1,Fz1,Fx2,Fy2,Fz2,....FxN,FyN,FzN}.
     *        This matrix is incremented, not assigned.
     * \return the adjusted energy.
     */
    Real computeEFAdj(const Matrix<short> &pairList, int parameterAngMom, const RealMat &parameters,
                      const RealMat &coordinates, RealMat &forces) {
        if (parameterAngMom) throw std::runtime_error("Multipole self terms have not been coded yet.");
        sanityChecks(parameterAngMom, parameters, coordinates);

        Real energy = 0;
        Real kappaSquared = kappa_ * kappa_;
        size_t nPair = pairList.nRows();
        for (int pair = 0; pair < nPair; ++pair) {
            short i = pairList(pair, 0);
            short j = pairList(pair, 1);
            auto deltaR = coordinates.row(j) - coordinates.row(i);
            // TODO: apply minimum image convention.
            Real rSquared = deltaR.dot(deltaR);
            auto kernels = adjEFFxn_(rSquared, kappa_, kappaSquared);
            Real eKernel = std::get<0>(kernels);
            Real fKernel = std::get<1>(kernels);
            Real prefactor = scaleFactor_ * parameters(i, 0) * parameters(j, 0);
            energy += prefactor * eKernel;
            Real f = -prefactor * fKernel;
            auto force = deltaR.row(0);
            force *= f;
            forces.row(i) -= force;
            forces.row(j) += force;
        }
        return energy;
    }

    /*!
     * \brief computeEFVAdj computes the adjusted energy, forces and virial.  This is provided mostly for debugging
     *        and testing purposes; generally the host program should provide the pairwise interactions.
     * \param pairList dense list of atom pairs, ordered like i1, j1, i2, j2, i3, j3, ... iN, jN.
     * \param parameterAngMom the angular momentum of the parameters (0 for charges, C6 coefficients, 2 for
     * quadrupoles, etc.). \param parameters the list of parameters associated with each atom (charges, C6
     * coefficients, multipoles, etc...). For a parameter with angular momentum L, a matrix of dimension nAtoms x nL
     * is expected, where nL = (L+1)*(L+2)*(L+3)/6 and the fast running index nL has the ordering
     *
     * 0 X Y Z XX XY YY XZ YZ ZZ XXX XXY XYY YYY XXZ XYZ YYZ XZZ YZZ ZZZ ...
     *
     * i.e. generated by the python loops
     * \code{.py}
     * for L in range(maxAM+1):
     *     for Lz in range(0,L+1):
     *         for Ly in range(0, L - Lz + 1):
     *              Lx  = L - Ly - Lz
     * \endcode
     * \param coordinates the cartesian coordinates, ordered in memory as {x1,y1,z1,x2,y2,z2,....xN,yN,zN}.
     * \param forces a Nx3 matrix of the forces, ordered in memory as {Fx1,Fy1,Fz1,Fx2,Fy2,Fz2,....FxN,FyN,FzN}.
     *        This matrix is incremented, not assigned.
     * \param virial a vector of length 6 containing the unique virial elements, in the order XX XY YY XZ YZ ZZ.
     *        This vector is incremented, not assigned.
     * \return the adjusted energy.
     */
    Real computeEFVAdj(const Matrix<short> &pairList, int parameterAngMom, const RealMat &parameters,
                       const RealMat &coordinates, RealMat &forces, RealMat &virial) {
        if (parameterAngMom) throw std::runtime_error("Multipole self terms have not been coded yet.");
        sanityChecks(parameterAngMom, parameters, coordinates);

        Real energy = 0;
        Real kappaSquared = kappa_ * kappa_;
        size_t nPair = pairList.nRows();
        for (int pair = 0; pair < nPair; ++pair) {
            short i = pairList(pair, 0);
            short j = pairList(pair, 1);
            auto deltaR = coordinates.row(j) - coordinates.row(i);
            // TODO: apply minimum image convention.
            Real rSquared = deltaR.dot(deltaR);
            auto kernels = adjEFFxn_(rSquared, kappa_, kappaSquared);
            Real eKernel = std::get<0>(kernels);
            Real fKernel = std::get<1>(kernels);
            Real prefactor = scaleFactor_ * parameters(i, 0) * parameters(j, 0);
            energy += prefactor * eKernel;
            Real f = -prefactor * fKernel;
            RealMat dRCopy = deltaR.clone();
            auto force = dRCopy.row(0);
            force *= f;
            forces.row(i) -= force;
            forces.row(j) += force;
            virial[0][0] += force[0] * deltaR[0][0];
            virial[0][1] += 0.5f * (force[0] * deltaR[0][1] + force[1] * deltaR[0][0]);
            virial[0][2] += force[1] * deltaR[0][1];
            virial[0][3] += 0.5f * (force[0] * deltaR[0][2] + force[2] * deltaR[0][0]);
            virial[0][4] += 0.5f * (force[1] * deltaR[0][2] + force[2] * deltaR[0][1]);
            virial[0][5] += force[2] * deltaR[0][2];
        }
        return energy;
    }

    /*!
     * \brief Runs a PME reciprocal space calculation, computing the potential and, optionally, its derivatives.
     * \param parameterAngMom the angular momentum of the parameters (0 for charges, C6 coefficients, 2 for
     * quadrupoles, etc.).  A negative value indicates that only the shell with |parameterAngMom| is to be considered,
     * e.g. a value of -2 specifies that only quadrupoles (and not dipoles or charges) will be provided; the input
     * matrix should have dimensions corresponding only to the number of terms in this shell.
     * \param parameters the list of parameters associated with each atom (charges, C6
     * coefficients, multipoles, etc...). For a parameter with angular momentum L, a matrix of dimension nAtoms x nL
     * is expected, where nL = (L+1)*(L+2)*(L+3)/6 and the fast running index nL has the ordering
     *
     * 0 X Y Z XX XY YY XZ YZ ZZ XXX XXY XYY YYY XXZ XYZ YYZ XZZ YZZ ZZZ ...
     *
     * i.e. generated by the python loops
     * \code{.py}
     * for L in range(maxAM+1):
     *     for Lz in range(0,L+1):
     *         for Ly in range(0, L - Lz + 1):
     *              Lx  = L - Ly - Lz
     * \endcode
     * \param coordinates the cartesian coordinates, ordered in memory as {x1,y1,z1,x2,y2,z2,....xN,yN,zN}.
     * \param energy pointer to the variable holding the energy; this is incremented, not assigned.
     * \param gridPoints the list of grid points at which the potential is needed; can be the same as the
     * coordinates.
     * \param derivativeLevel the order of the potential derivatives required; 0 is the potential, 1 is
     * (minus) the field, etc.  A negative value indicates that only the derivative with order |parameterAngMom|
     * is to be generated, e.g. -2 specifies that only the second derivative (not the potential or its gradient)
     * will be returned as output.  The output matrix should have space for only these terms, accordingly.
     * \param potential the array holding the potential.  This is a matrix of dimensions
     * nAtoms x nD, where nD is the derivative level requested.  See the details fo the parameters argument for
     * information about ordering of derivative components. N.B. this array is incremented with the potential, not
     * assigned, so take care to zero it first if only the current results are desired.
     */

    void computePRec(int parameterAngMom, const RealMat &parameters, const RealMat &coordinates,
                     const RealMat &gridPoints, int derivativeLevel, RealMat &potential) {
        bool onlyOneShellForInput = parameterAngMom < 0;
        bool onlyOneShellForOutput = derivativeLevel < 0;
        parameterAngMom = std::abs(parameterAngMom);
        derivativeLevel = std::abs(derivativeLevel);
        int cartesianOffset = onlyOneShellForInput ? nCartesian(parameterAngMom - 1) : 0;
        sanityChecks(parameterAngMom, parameters, coordinates, cartesianOffset);
        updateAngMomIterator(std::max(parameterAngMom, derivativeLevel));
        // Note: we're calling the version of spread parameters that computes its own splines here.
        // This is quite inefficient, but allow the potential to be computed at arbitrary locations by
        // simply regenerating splines on demand in the probing stage.  If this becomes too slow, it's
        // easy to write some logic to check whether gridPoints and coordinates are the same, and
        // handle that special case using spline cacheing machinery for efficiency.
        Real *realGrid = reinterpret_cast<Real *>(workSpace1_.data());
        std::fill(workSpace1_.begin(), workSpace1_.end(), 0);
        updateAngMomIterator(parameterAngMom);
        auto fractionalParameters =
            cartesianTransform(parameterAngMom, onlyOneShellForInput, scaledRecVecs_, parameters);
        int nComponents = nCartesian(parameterAngMom) - cartesianOffset;
        size_t nAtoms = coordinates.nRows();
        for (size_t atom = 0; atom < nAtoms; ++atom) {
            // Blindly reconstruct splines for this atom, assuming nothing about the validity of the cache.
            // Note that this incurs a somewhat steep cost due to repeated memory allocations.
            auto bSplines = makeBSplines(coordinates[atom], parameterAngMom);
            const auto &splineA = std::get<0>(bSplines);
            const auto &splineB = std::get<1>(bSplines);
            const auto &splineC = std::get<2>(bSplines);
            const auto &aGridIterator = gridIteratorA_[splineA.startingGridPoint()];
            const auto &bGridIterator = gridIteratorB_[splineB.startingGridPoint()];
            const auto &cGridIterator = gridIteratorC_[splineC.startingGridPoint()];
            int numPointsA = static_cast<int>(aGridIterator.size());
            int numPointsB = static_cast<int>(bGridIterator.size());
            int numPointsC = static_cast<int>(cGridIterator.size());
            const auto *iteratorDataA = aGridIterator.data();
            const auto *iteratorDataB = bGridIterator.data();
            const auto *iteratorDataC = cGridIterator.data();
            for (int component = 0; component < nComponents; ++component) {
                const auto &quanta = angMomIterator_[component + cartesianOffset];
                Real param = fractionalParameters(atom, component);
                const Real *splineValsA = splineA[quanta[0]];
                const Real *splineValsB = splineB[quanta[1]];
                const Real *splineValsC = splineC[quanta[2]];
                for (int pointC = 0; pointC < numPointsC; ++pointC) {
                    const auto &cPoint = iteratorDataC[pointC];
                    Real cValP = param * splineValsC[cPoint.second];
                    for (int pointB = 0; pointB < numPointsB; ++pointB) {
                        const auto &bPoint = iteratorDataB[pointB];
                        Real cbValP = cValP * splineValsB[bPoint.second];
                        Real *cbRow = &realGrid[cPoint.first * myGridDimensionB_ * myGridDimensionA_ +
                                                bPoint.first * myGridDimensionA_];
                        for (int pointA = 0; pointA < numPointsA; ++pointA) {
                            const auto &aPoint = iteratorDataA[pointA];
                            cbRow[aPoint.first] += cbValP * splineValsA[aPoint.second];
                        }
                    }
                }
            }
        }

        Real *potentialGrid;
        if (algorithmType_ == AlgorithmType::PME) {
            auto gridAddress = forwardTransform(realGrid);
            convolveE(gridAddress);
            potentialGrid = inverseTransform(gridAddress);
        } else if (algorithmType_ == AlgorithmType::CompressedPME) {
            auto gridAddress = compressedForwardTransform(realGrid);
            convolveE(gridAddress);
            potentialGrid = compressedInverseTransform(gridAddress);
        } else {
            std::logic_error("Unknown algorithm in helpme::computePRec");
        }

        auto fracPotential = potential.clone();
        cartesianOffset = onlyOneShellForOutput ? nCartesian(derivativeLevel - 1) : 0;
        int nPotentialComponents = nCartesian(derivativeLevel) - cartesianOffset;
        size_t nPoints = gridPoints.nRows();
        for (size_t point = 0; point < nPoints; ++point) {
            Real *phiPtr = fracPotential[point];
            auto bSplines = makeBSplines(gridPoints[point], derivativeLevel);
            auto splineA = std::get<0>(bSplines);
            auto splineB = std::get<1>(bSplines);
            auto splineC = std::get<2>(bSplines);
            const auto &aGridIterator = gridIteratorA_[splineA.startingGridPoint()];
            const auto &bGridIterator = gridIteratorB_[splineB.startingGridPoint()];
            const auto &cGridIterator = gridIteratorC_[splineC.startingGridPoint()];
            const Real *splineStartA = splineA[0];
            const Real *splineStartB = splineB[0];
            const Real *splineStartC = splineC[0];
            for (const auto &cPoint : cGridIterator) {
                for (const auto &bPoint : bGridIterator) {
                    const Real *cbRow = potentialGrid + cPoint.first * myGridDimensionA_ * myGridDimensionB_ +
                                        bPoint.first * myGridDimensionA_;
                    for (const auto &aPoint : aGridIterator) {
                        Real gridVal = cbRow[aPoint.first];
                        for (int component = 0; component < nPotentialComponents; ++component) {
                            const auto &quanta = angMomIterator_[component + cartesianOffset];
                            const Real *splineValsA = splineStartA + quanta[0] * splineOrder_;
                            const Real *splineValsB = splineStartB + quanta[1] * splineOrder_;
                            const Real *splineValsC = splineStartC + quanta[2] * splineOrder_;
                            phiPtr[component] += gridVal * splineValsA[aPoint.second] * splineValsB[bPoint.second] *
                                                 splineValsC[cPoint.second];
                        }
                    }
                }
            }
        }
        potential += cartesianTransform(derivativeLevel, onlyOneShellForOutput, scaledRecVecs_, fracPotential);
    }

    /*!
     * \brief Runs a PME reciprocal space calculation, computing energies.
     * \param parameterAngMom the angular momentum of the parameters (0 for charges, C6 coefficients, 2 for
     * quadrupoles, etc.). \param parameters the list of parameters associated with each atom (charges, C6
     * coefficients, multipoles, etc...). For a parameter with angular momentum L, a matrix of dimension nAtoms x nL
     * is expected, where nL = (L+1)*(L+2)*(L+3)/6 and the fast running index nL has the ordering
     *
     * 0 X Y Z XX XY YY XZ YZ ZZ XXX XXY XYY YYY XXZ XYZ YYZ XZZ YZZ ZZZ ...
     *
     * i.e. generated by the python loops
     * \code{.py}
     * for L in range(maxAM+1):
     *     for Lz in range(0,L+1):
     *         for Ly in range(0, L - Lz + 1):
     *              Lx  = L - Ly - Lz
     * \endcode
     * \param coordinates the cartesian coordinates, ordered in memory as {x1,y1,z1,x2,y2,z2,....xN,yN,zN}.
     * \param energy pointer to the variable holding the energy; this is incremented, not assigned.
     * \return the reciprocal space energy.
     */
    Real computeERec(int parameterAngMom, const RealMat &parameters, const RealMat &coordinates) {
        sanityChecks(parameterAngMom, parameters, coordinates);
        filterAtomsAndBuildSplineCache(parameterAngMom, coordinates);
        auto realGrid = spreadParameters(parameterAngMom, parameters);
        Real energy;
        if (algorithmType_ == AlgorithmType::PME) {
            auto gridAddress = forwardTransform(realGrid);
            energy = convolveE(gridAddress);
        } else if (algorithmType_ == AlgorithmType::CompressedPME) {
            auto gridAddress = compressedForwardTransform(realGrid);
            energy = convolveE(gridAddress);
        } else {
            std::logic_error("Unknown algorithm in helpme::computeERec");
        }
        return energy;
    }

    /*!
     * \brief Runs a PME reciprocal space calculation, computing energies and forces.
     * \param parameterAngMom the angular momentum of the parameters (0 for charges, C6 coefficients, 2 for
     * quadrupoles, etc.). \param parameters the list of parameters associated with each atom (charges, C6
     * coefficients, multipoles, etc...). For a parameter with angular momentum L, a matrix of dimension nAtoms x nL
     * is expected, where nL = (L+1)*(L+2)*(L+3)/6 and the fast running index nL has the ordering
     *
     * 0 X Y Z XX XY YY XZ YZ ZZ XXX XXY XYY YYY XXZ XYZ YYZ XZZ YZZ ZZZ ...
     *
     * i.e. generated by the python loops
     * \code{.py}
     * for L in range(maxAM+1):
     *     for Lz in range(0,L+1):
     *         for Ly in range(0, L - Lz + 1):
     *              Lx  = L - Ly - Lz
     * \endcode
     * \param coordinates the cartesian coordinates, ordered in memory as {x1,y1,z1,x2,y2,z2,....xN,yN,zN}.
     * \param energy pointer to the variable holding the energy; this is incremented, not assigned.
     * \param forces a Nx3 matrix of the forces, ordered in memory as {Fx1,Fy1,Fz1,Fx2,Fy2,Fz2,....FxN,FyN,FzN}.
     *        This matrix is incremented, not assigned.
     * \return the reciprocal space energy.
     */
    Real computeEFRec(int parameterAngMom, const RealMat &parameters, const RealMat &coordinates, RealMat &forces) {
        sanityChecks(parameterAngMom, parameters, coordinates);
        // Spline derivative level bumped by 1, for energy gradients.
        filterAtomsAndBuildSplineCache(parameterAngMom + 1, coordinates);

        auto realGrid = spreadParameters(parameterAngMom, parameters);

        Real energy;
        if (algorithmType_ == AlgorithmType::PME) {
            auto gridAddress = forwardTransform(realGrid);
            energy = convolveE(gridAddress);
            auto potentialGrid = inverseTransform(gridAddress);
            probeGrid(potentialGrid, parameterAngMom, parameters, forces);
        } else if (algorithmType_ == AlgorithmType::CompressedPME) {
            auto gridAddress = compressedForwardTransform(realGrid);
            energy = convolveE(gridAddress);
            auto potentialGrid = compressedInverseTransform(gridAddress);
            probeGrid(potentialGrid, parameterAngMom, parameters, forces);
        } else {
            std::logic_error("Unknown algorithm in helpme::computeEFRec");
        }

        return energy;
    }

    /*!
     * \brief Runs a PME reciprocal space calculation, computing energies, forces and the virial.
     * \param parameterAngMom the angular momentum of the parameters (0 for charges, C6 coefficients, 2 for
     * quadrupoles, etc.). \param parameters the list of parameters associated with each atom (charges, C6
     * coefficients, multipoles, etc...). For a parameter with angular momentum L, a matrix of dimension nAtoms x nL
     * is expected, where nL = (L+1)*(L+2)*(L+3)/6 and the fast running index nL has the ordering
     *
     * 0 X Y Z XX XY YY XZ YZ ZZ XXX XXY XYY YYY XXZ XYZ YYZ XZZ YZZ ZZZ ...
     *
     * i.e. generated by the python loops
     * \code{.py}
     * for L in range(maxAM+1):
     *     for Lz in range(0,L+1):
     *         for Ly in range(0, L - Lz + 1):
     *              Lx  = L - Ly - Lz
     * \endcode
     * \param coordinates the cartesian coordinates, ordered in memory as {x1,y1,z1,x2,y2,z2,....xN,yN,zN}.
     * \param energy pointer to the variable holding the energy; this is incremented, not assigned.
     * \param forces a Nx3 matrix of the forces, ordered in memory as {Fx1,Fy1,Fz1,Fx2,Fy2,Fz2,....FxN,FyN,FzN}.
     *        This matrix is incremented, not assigned.
     * \param virial a vector of length 6 containing the unique virial elements, in the order XX XY YY XZ YZ ZZ.
     *        This vector is incremented, not assigned.
     * \return the reciprocal space energy.
     */
    Real computeEFVRec(int parameterAngMom, const RealMat &parameters, const RealMat &coordinates, RealMat &forces,
                       RealMat &virial) {
        sanityChecks(parameterAngMom, parameters, coordinates);

        // Spline derivative level bumped by 1, for energy gradients.
        filterAtomsAndBuildSplineCache(parameterAngMom + 1, coordinates);

        auto realGrid = spreadParameters(parameterAngMom, parameters);

        Real energy;
        if (algorithmType_ == AlgorithmType::PME) {
            auto gridAddress = forwardTransform(realGrid);
            energy = convolveEV(gridAddress, virial);
            auto potentialGrid = inverseTransform(gridAddress);
            probeGrid(potentialGrid, parameterAngMom, parameters, forces);
        } else if (algorithmType_ == AlgorithmType::CompressedPME) {
            auto gridAddress = compressedForwardTransform(realGrid);
            Real *convolvedGrid;
            energy = convolveEV(gridAddress, convolvedGrid, virial);
            auto potentialGrid = compressedInverseTransform(convolvedGrid);
            probeGrid(potentialGrid, parameterAngMom, parameters, forces);
        } else {
            std::logic_error("Unknown algorithm in helpme::computeEFRec");
        }

        return energy;
    }

    /*!
     * \brief Runs a full (direct and reciprocal space) PME calculation, computing the energy.  The direct space
     *        implementation here is not totally optimal, so this routine should primarily be used for testing and
     *        debugging.
     * \param includedList dense list of included atom pairs, ordered like i1, j1, i2, j2, i3, j3, ... iN,jN.
     * \param excludedList dense list of excluded atom pairs, ordered like i1, j1, i2, j2, i3, j3, ... iN, jN.
     * \param parameterAngMom the angular momentum of the parameters (0 for charges, C6 coefficients, 2 for
     * quadrupoles, etc.). \param parameters the list of parameters associated with each atom (charges, C6
     * coefficients, multipoles, etc...). For a parameter with angular momentum L, a matrix of dimension nAtoms x nL
     * is expected, where nL = (L+1)*(L+2)*(L+3)/6 and the fast running index nL has the ordering
     *
     * 0 X Y Z XX XY YY XZ YZ ZZ XXX XXY XYY YYY XXZ XYZ YYZ XZZ YZZ ZZZ ...
     *
     * i.e. generated by the python loops
     * \code{.py}
     * for L in range(maxAM+1):
     *     for Lz in range(0,L+1):
     *         for Ly in range(0, L - Lz + 1):
     *              Lx  = L - Ly - Lz
     * \endcode
     * \param coordinates the cartesian coordinates, ordered in memory as {x1,y1,z1,x2,y2,z2,....xN,yN,zN}.
     * \param energy pointer to the variable holding the energy; this is incremented, not assigned.
     * \param forces a Nx3 matrix of the forces, ordered in memory as {Fx1,Fy1,Fz1,Fx2,Fy2,Fz2,....FxN,FyN,FzN}.
     *        This matrix is incremented, not assigned.
     * \return the full PME energy.
     */
    Real computeEAll(const Matrix<short> &includedList, const Matrix<short> &excludedList, int parameterAngMom,
                     const RealMat &parameters, const RealMat &coordinates) {
        sanityChecks(parameterAngMom, parameters, coordinates);

        Real energy = computeERec(parameterAngMom, parameters, coordinates);
        energy += computeESlf(parameterAngMom, parameters);
        energy += computeEDir(includedList, parameterAngMom, parameters, coordinates);
        energy += computeEAdj(excludedList, parameterAngMom, parameters, coordinates);
        return energy;
    }

    /*!
     * \brief Runs a full (direct and reciprocal space) PME calculation, computing energies and forces.  The direct
     *        space implementation here is not totally optimal, so this routine should primarily be used for testing
     *        and debugging.
     * \param includedList dense list of included atom pairs, ordered like i1, j1, i2, j2, i3, j3, ... iN, jN.
     * \param excludedList dense list of excluded atom pairs, ordered like i1, j1, i2, j2, i3, j3, ... iN, jN.
     * \param parameterAngMom the angular momentum of the parameters (0 for charges, C6 coefficients, 2 for
     * quadrupoles, etc.). \param parameters the list of parameters associated with each atom (charges, C6
     * coefficients, multipoles, etc...). For a parameter with angular momentum L, a matrix of dimension nAtoms x nL
     * is expected, where nL = (L+1)*(L+2)*(L+3)/6 and the fast running index nL has the ordering
     *
     * 0 X Y Z XX XY YY XZ YZ ZZ XXX XXY XYY YYY XXZ XYZ YYZ XZZ YZZ ZZZ ...
     *
     * i.e. generated by the python loops
     * \code{.py}
     * for L in range(maxAM+1):
     *     for Lz in range(0,L+1):
     *         for Ly in range(0, L - Lz + 1):
     *              Lx  = L - Ly - Lz
     * \endcode
     * \param coordinates the cartesian coordinates, ordered in memory as {x1,y1,z1,x2,y2,z2,....xN,yN,zN}.
     * \param energy pointer to the variable holding the energy; this is incremented, not assigned.
     * \param forces a Nx3 matrix of the forces, ordered in memory as {Fx1,Fy1,Fz1,Fx2,Fy2,Fz2,....FxN,FyN,FzN}.
     *        This matrix is incremented, not assigned.
     * \return the full PME energy.
     */
    Real computeEFAll(const Matrix<short> &includedList, const Matrix<short> &excludedList, int parameterAngMom,
                      const RealMat &parameters, const RealMat &coordinates, RealMat &forces) {
        sanityChecks(parameterAngMom, parameters, coordinates);

        Real energy = computeEFRec(parameterAngMom, parameters, coordinates, forces);
        energy += computeESlf(parameterAngMom, parameters);
        energy += computeEFDir(includedList, parameterAngMom, parameters, coordinates, forces);
        energy += computeEFAdj(excludedList, parameterAngMom, parameters, coordinates, forces);
        return energy;
    }

    /*!
     * \brief Runs a full (direct and reciprocal space) PME calculation, computing energies, forces and virials.
     *        The direct space implementation here is not totally optimal, so this routine should primarily
     *        be used for testing and debugging.
     * \param includedList dense list of included atom pairs, ordered like i1, j1, i2, j2, i3, j3, ... iN, jN.
     * \param excludedList dense list of excluded atom pairs, ordered like i1, j1, i2, j2, i3, j3, ... iN, jN.
     * \param parameterAngMom the angular momentum of the parameters (0 for charges, C6 coefficients, 2 for
     * quadrupoles, etc.). \param parameters the list of parameters associated with each atom (charges, C6
     * coefficients, multipoles, etc...). For a parameter with angular momentum L, a matrix of dimension nAtoms x nL
     * is expected, where nL = (L+1)*(L+2)*(L+3)/6 and the fast running index nL has the ordering
     *
     * 0 X Y Z XX XY YY XZ YZ ZZ XXX XXY XYY YYY XXZ XYZ YYZ XZZ YZZ ZZZ ...
     *
     * i.e. generated by the python loops
     * \code{.py}
     * for L in range(maxAM+1):
     *     for Lz in range(0,L+1):
     *         for Ly in range(0, L - Lz + 1):
     *              Lx  = L - Ly - Lz
     * \endcode
     * \param coordinates the cartesian coordinates, ordered in memory as {x1,y1,z1,x2,y2,z2,....xN,yN,zN}.
     * \param energy pointer to the variable holding the energy; this is incremented, not assigned.
     * \param forces a Nx3 matrix of the forces, ordered in memory as {Fx1,Fy1,Fz1,Fx2,Fy2,Fz2,....FxN,FyN,FzN}.
     *        This matrix is incremented, not assigned.
     * \param virial a vector of length 6 containing the unique virial elements, in the order XX XY YY XZ YZ ZZ.
     *        This vector is incremented, not assigned.
     * \return the full PME energy.
     */
    Real computeEFVAll(const Matrix<short> &includedList, const Matrix<short> &excludedList, int parameterAngMom,
                       const RealMat &parameters, const RealMat &coordinates, RealMat &forces, RealMat &virial) {
        sanityChecks(parameterAngMom, parameters, coordinates);

        Real energy = computeEFVRec(parameterAngMom, parameters, coordinates, forces, virial);
        energy += computeESlf(parameterAngMom, parameters);
        energy += computeEFVDir(includedList, parameterAngMom, parameters, coordinates, forces, virial);
        energy += computeEFVAdj(excludedList, parameterAngMom, parameters, coordinates, forces, virial);
        return energy;
    }

    /*!
     * \brief setup initializes this object for a PME calculation using only threading.
     *        This may be called repeatedly without compromising performance.
     * \param rPower the exponent of the (inverse) distance kernel (e.g. 1 for Coulomb, 6 for attractive
     * dispersion). \param kappa the attenuation parameter in units inverse of those used to specify coordinates.
     * \param splineOrder the order of B-spline; must be at least (2 + max. multipole order + deriv. level needed).
     * \param dimA the dimension of the FFT grid along the A axis.
     * \param dimB the dimension of the FFT grid along the B axis.
     * \param dimC the dimension of the FFT grid along the C axis.
     * \param scaleFactor a scale factor to be applied to all computed energies and derivatives thereof (e.g. the
     *        1 / [4 pi epslion0] for Coulomb calculations).
     * \param nThreads the maximum number of threads to use for each MPI instance; if set to 0 all available threads
     * are used.
     */
    void setup(int rPower, Real kappa, int splineOrder, int dimA, int dimB, int dimC, Real scaleFactor, int nThreads) {
        setupCalculationMetadata(rPower, kappa, splineOrder, dimA, dimB, dimC, dimA, dimB, dimC, scaleFactor, nThreads,
                                 0, NodeOrder::ZYX, 1, 1, 1);
    }

    /*!
     * \brief setupCompressed initializes this object for a compressed PME calculation using only threading.
     *        This may be called repeatedly without compromising performance.
     * \param rPower the exponent of the (inverse) distance kernel (e.g. 1 for Coulomb, 6 for attractive
     *        dispersion).
     * \param kappa the attenuation parameter in units inverse of those used to specify coordinates.
     * \param splineOrder the order of B-spline; must be at least (2 + max. multipole order + deriv. level needed).
     * \param dimA the dimension of the FFT grid along the A axis.
     * \param dimB the dimension of the FFT grid along the B axis.
     * \param dimC the dimension of the FFT grid along the C axis.
     * \param maxKA the maximum K value in the reciprocal sum along the A axis.
     * \param maxKB the maximum K value in the reciprocal sum along the B axis.
     * \param maxKC the maximum K value in the reciprocal sum along the C axis.
     * \param scaleFactor a scale factor to be applied to all computed energies and derivatives thereof (e.g. the
     *        1 / [4 pi epslion0] for Coulomb calculations).
     * \param nThreads the maximum number of threads to use for each MPI instance; if set to 0 all available threads
     * are used.
     */
    void setupCompressed(int rPower, Real kappa, int splineOrder, int dimA, int dimB, int dimC, int maxKA, int maxKB,
                         int maxKC, Real scaleFactor, int nThreads) {
        setupCalculationMetadata(rPower, kappa, splineOrder, dimA, dimB, dimC, maxKA, maxKB, maxKC, scaleFactor,
                                 nThreads, 0, NodeOrder::ZYX, 1, 1, 1);
    }
#if HAVE_MPI == 1
    /*!
     * \brief setupParallel initializes this object for a conventional PME calculation using MPI parallism
     *        and threading.  This may be called repeatedly without compromising performance.
     * \param rPower the exponent of the (inverse) distance kernel (e.g. 1 for Coulomb, 6 for attractive
     *        dispersion).
     * \param kappa the attenuation parameter in units inverse of those used to specify coordinates.
     * \param splineOrder the order of B-spline; must be at least (2 + max. multipole order + deriv. level needed).
     * \param dimA the dimension of the FFT grid along the A axis.
     * \param dimB the dimension of the FFT grid along the B axis.
     * \param dimC the dimension of the FFT grid along the C axis.
     * \param scaleFactor a scale factor to be applied to all computed energies and derivatives thereof (e.g. the
     *        1 / [4 pi epslion0] for Coulomb calculations).
     * \param nThreads the maximum number of threads to use for each MPI instance; if set to 0 all available threads
     *        are used.
     * \param communicator the MPI communicator for the reciprocal space calcultion, which should already be
     *        initialized.
     * \param numNodesA the number of nodes to be used for the A dimension.
     * \param numNodesB the number of nodes to be used for the B dimension.
     * \param numNodesC the number of nodes to be used for the C dimension.
     */
    void setupParallel(int rPower, Real kappa, int splineOrder, int dimA, int dimB, int dimC, Real scaleFactor,
                       int nThreads, const MPI_Comm &communicator, NodeOrder nodeOrder, int numNodesA, int numNodesB,
                       int numNodesC) {
        setupCalculationMetadata(rPower, kappa, splineOrder, dimA, dimB, dimC, dimA, dimB, dimC, scaleFactor, nThreads,
                                 (void *)&communicator, nodeOrder, numNodesA, numNodesB, numNodesC);
    }

    /*!
     * \brief setupCompressedParallel initializes this object for a compressed PME calculation using MPI parallism
     *        and threading.  This may be called repeatedly without compromising performance.
     * \param rPower the exponent of the (inverse) distance kernel (e.g. 1 for Coulomb, 6 for attractive
     *        dispersion).
     * \param kappa the attenuation parameter in units inverse of those used to specify coordinates.
     * \param splineOrder the order of B-spline; must be at least (2 + max. multipole order + deriv. level needed).
     * \param dimA the dimension of the FFT grid along the A axis.
     * \param dimB the dimension of the FFT grid along the B axis.
     * \param dimC the dimension of the FFT grid along the C axis.
     * \param maxKA the maximum K value in the reciprocal sum along the A axis.
     * \param maxKB the maximum K value in the reciprocal sum along the B axis.
     * \param maxKC the maximum K value in the reciprocal sum along the C axis.
     * \param scaleFactor a scale factor to be applied to all computed energies and derivatives thereof (e.g. the
     *        1 / [4 pi epslion0] for Coulomb calculations).
     * \param nThreads the maximum number of threads to use for each MPI instance; if set to 0 all available threads
     *        are used.
     * \param communicator the MPI communicator for the reciprocal space calcultion, which should already be
     *        initialized.
     * \param numNodesA the number of nodes to be used for the A dimension.
     * \param numNodesB the number of nodes to be used for the B dimension.
     * \param numNodesC the number of nodes to be used for the C dimension.
     */
    void setupCompressedParallel(int rPower, Real kappa, int splineOrder, int dimA, int dimB, int dimC, int maxKA,
                                 int maxKB, int maxKC, Real scaleFactor, int nThreads, const MPI_Comm &communicator,
                                 NodeOrder nodeOrder, int numNodesA, int numNodesB, int numNodesC) {
        setupCalculationMetadata(rPower, kappa, splineOrder, dimA, dimB, dimC, maxKA, maxKB, maxKC, scaleFactor,
                                 nThreads, (void *)&communicator, nodeOrder, numNodesA, numNodesB, numNodesC);
    }
#endif
};
}  // Namespace helpme

using PMEInstanceD = helpme::PMEInstance<double>;
using PMEInstanceF = helpme::PMEInstance<float>;

#else

// C header
#include <stddef.h>
#if HAVE_MPI == 1
#include <mpi.h>
#endif

typedef enum { Undefined = 0, XAligned = 1, ShapeMatrix = 2 } LatticeType;
typedef enum { /* Undefined comes from the above scope */ ZYX = 1 } NodeOrder;

typedef struct PMEInstance PMEInstance;
extern struct PMEInstance *helpme_createD();
extern struct PMEInstance *helpme_createF();
extern void helpme_destroyD(struct PMEInstance *pme);
extern void helpme_destroyF(struct PMEInstance *pme);
extern void helpme_setupD(struct PMEInstance *pme, int rPower, double kappa, int splineOrder, int aDim, int bDim,
                          int cDim, double scaleFactor, int nThreads);
extern void helpme_setupF(struct PMEInstance *pme, int rPower, float kappa, int splineOrder, int aDim, int bDim,
                          int cDim, float scaleFactor, int nThreads);
extern void helpme_setup_compressedD(struct PMEInstance *pme, int rPower, double kappa, int splineOrder, int aDim,
                                     int bDim, int cDim, int maxKA, int maxKB, int maxKC, double scaleFactor,
                                     int nThreads);
extern void helpme_setup_compressedF(struct PMEInstance *pme, int rPower, float kappa, int splineOrder, int aDim,
                                     int bDim, int cDim, int maxKA, int maxKB, int maxKC, float scaleFactor,
                                     int nThreads);
#if HAVE_MPI == 1
extern void helpme_setup_parallelD(PMEInstance *pme, int rPower, double kappa, int splineOrder, int dimA, int dimB,
                                   int dimC, double scaleFactor, int nThreads, MPI_Comm communicator,
                                   NodeOrder nodeOrder, int numNodesA, int numNodesB, int numNodesC);
extern void helpme_setup_parallelF(PMEInstance *pme, int rPower, float kappa, int splineOrder, int dimA, int dimB,
                                   int dimC, float scaleFactor, int nThreads, MPI_Comm communicator,
                                   NodeOrder nodeOrder, int numNodesA, int numNodesB, int numNodesC);
extern void helpme_setup_compressed_parallelD(PMEInstance *pme, int rPower, double kappa, int splineOrder, int dimA,
                                              int dimB, int dimC, int maxKA, int maxKB, int maxKC, double scaleFactor,
                                              int nThreads, MPI_Comm communicator, NodeOrder nodeOrder, int numNodesA,
                                              int numNodesB, int numNodesC);
extern void helpme_setup_compressed_parallelF(PMEInstance *pme, int rPower, float kappa, int splineOrder, int dimA,
                                              int dimB, int dimC, int maxKA, int maxKB, int maxKC, float scaleFactor,
                                              int nThreads, MPI_Comm communicator, NodeOrder nodeOrder, int numNodesA,
                                              int numNodesB, int numNodesC);
#endif  // HAVE_MPI
extern void helpme_set_lattice_vectorsD(struct PMEInstance *pme, double A, double B, double C, double kappa,
                                        double beta, double gamma, LatticeType latticeType);
extern void helpme_set_lattice_vectorsF(struct PMEInstance *pme, float A, float B, float C, float kappa, float beta,
                                        float gamma, LatticeType latticeType);
extern double helpme_compute_E_recD(struct PMEInstance *pme, size_t nAtoms, int parameterAngMom, double *parameters,
                                    double *coordinates);
extern float helpme_compute_E_recF(struct PMEInstance *pme, size_t nAtoms, int parameterAngMom, float *parameters,
                                   float *coordinates);
extern double helpme_compute_EF_recD(struct PMEInstance *pme, size_t nAtoms, int parameterAngMom, double *parameters,
                                     double *coordinates, double *forces);
extern float helpme_compute_EF_recF(struct PMEInstance *pme, size_t nAtoms, int parameterAngMom, float *parameters,
                                    float *coordinates, float *forces);
extern double helpme_compute_EFV_recD(struct PMEInstance *pme, size_t nAtoms, int parameterAngMom, double *parameters,
                                      double *coordinates, double *forces, double *virial);
extern float helpme_compute_EFV_recF(struct PMEInstance *pme, size_t nAtoms, int parameterAngMom, float *parameters,
                                     float *coordinates, float *forces, float *virial);
extern void helpme_compute_P_recD(struct PMEInstance *pme, size_t nAtoms, int parameterAngMom, double *parameters,
                                  double *coordinates, size_t nGridPoints, double *gridPoints, int derivativeLevel,
                                  double *potential);
extern void helpme_compute_P_recF(struct PMEInstance *pme, size_t nAtoms, int parameterAngMom, float *parameters,
                                  float *coordinates, size_t nGridPoints, float *gridPoints, int derivativeLevel,
                                  float *potential);
#endif  // C++/C
#endif  // Header guard
