// -*- c++ -*-

// This file is part of the Collective Variables module (Colvars).
// The original version of Colvars and its updates are located at:
// https://github.com/Colvars/colvars
// Please update all Colvars source files before making any changes.
// If you wish to distribute your changes, please submit them to the
// Colvars repository at GitHub.

#ifndef COLVARTYPES_H
#define COLVARTYPES_H

#include "colvar_gpu_support.h"
#include <sstream> // TODO specialize templates and replace this with iosfwd
#include <vector>
#include <array>
#include <unordered_map>

#ifdef COLVARS_LAMMPS
// Use open-source Jacobi implementation
#include "math_eigen_impl.h"
#endif

#include "colvarmodule.h"

// ----------------------------------------------------------------------
/// Linear algebra functions and data types used in the collective
/// variables implemented so far
// ----------------------------------------------------------------------


/// \brief Arbitrary size array (one dimensions) suitable for linear
/// algebra operations (i.e. for floating point numbers it can be used
/// with library functions)
template <class T> class colvarmodule::vector1d
{
protected:

  std::vector<T> data;

public:

  /// Default constructor
  inline vector1d(size_t const n = 0)
  {
    data.resize(n);
    reset();
  }

  /// Constructor from C array
  inline vector1d(size_t const n, T const *t)
  {
    data.resize(n);
    reset();
    size_t i;
    for (i = 0; i < size(); i++) {
      data[i] = t[i];
    }
  }

  /// Explicit Copy constructor
  inline vector1d(const vector1d&) = default;

  /// Explicit Copy assignement
  inline vector1d& operator=(const vector1d&) = default;

  /// Return a pointer to the data location
  inline T * c_array()
  {
    if (data.size() > 0) {
      return &(data[0]);
    } else {
      return NULL;
    }
  }

  /// Return a reference to the data
  inline std::vector<T> &data_array()
  {
    return data;
  }

  /// Return a reference to the data
  inline std::vector<T> const &data_array() const
  {
    return data;
  }

  inline ~vector1d()
  {
    data.clear();
  }

  /// Set all elements to zero
  inline void reset()
  {
    data.assign(data.size(), T(0.0));
  }

  inline size_t size() const
  {
    return data.size();
  }

  inline void resize(size_t const n)
  {
    data.resize(n);
  }

  inline void clear()
  {
    data.clear();
  }

  inline T & operator [] (size_t const i) {
    return data[i];
  }

  inline T const & operator [] (size_t const i) const {
    return data[i];
  }

  inline static void check_sizes(vector1d<T> const &v1, vector1d<T> const &v2)
  {
    if (v1.size() != v2.size()) {
      cvm::error_static("Error: trying to perform an operation between vectors of different sizes, "+
                 cvm::to_str(v1.size())+" and "+cvm::to_str(v2.size())+".\n");
    }
  }

  inline void operator += (vector1d<T> const &v)
  {
    check_sizes(*this, v);
    size_t i;
    for (i = 0; i < this->size(); i++) {
      (*this)[i] += v[i];
    }
  }

  inline void operator -= (vector1d<T> const &v)
  {
    check_sizes(*this, v);
    size_t i;
    for (i = 0; i < this->size(); i++) {
      (*this)[i] -= v[i];
    }
  }

  inline void operator *= (cvm::real a)
  {
    size_t i;
    for (i = 0; i < this->size(); i++) {
      (*this)[i] *= a;
    }
  }

  inline void operator /= (cvm::real a)
  {
    size_t i;
    for (i = 0; i < this->size(); i++) {
      (*this)[i] /= a;
    }
  }

  inline friend vector1d<T> operator + (vector1d<T> const &v1,
                                        vector1d<T> const &v2)
  {
    check_sizes(v1.size(), v2.size());
    vector1d<T> result(v1.size());
    size_t i;
    for (i = 0; i < v1.size(); i++) {
      result[i] = v1[i] + v2[i];
    }
    return result;
  }

  inline friend vector1d<T> operator - (vector1d<T> const &v1,
                                        vector1d<T> const &v2)
  {
    check_sizes(v1.size(), v2.size());
    vector1d<T> result(v1.size());
    size_t i;
    for (i = 0; i < v1.size(); i++) {
      result[i] = v1[i] - v2[i];
    }
    return result;
  }

  inline friend vector1d<T> operator * (vector1d<T> const &v, cvm::real a)
  {
    vector1d<T> result(v.size());
    size_t i;
    for (i = 0; i < v.size(); i++) {
      result[i] = v[i] * a;
    }
    return result;
  }

  inline friend vector1d<T> operator * (cvm::real a, vector1d<T> const &v)
  {
    return v * a;
  }

  inline friend vector1d<T> operator / (vector1d<T> const &v, cvm::real a)
  {
    vector1d<T> result(v.size());
    size_t i;
    for (i = 0; i < v.size(); i++) {
      result[i] = v[i] / a;
    }
    return result;
  }

  /// Inner product
  inline friend T operator * (vector1d<T> const &v1, vector1d<T> const &v2)
  {
    check_sizes(v1.size(), v2.size());
    T prod(0.0);
    size_t i;
    for (i = 0; i < v1.size(); i++) {
      prod += v1[i] * v2[i];
    }
    return prod;
  }

  /// Squared norm
  inline cvm::real norm2() const
  {
    cvm::real result = 0.0;
    size_t i;
    for (i = 0; i < this->size(); i++) {
      result += (*this)[i] * (*this)[i];
    }
    return result;
  }

  inline cvm::real norm() const
  {
    return cvm::sqrt(this->norm2());
  }

  inline cvm::real sum() const
  {
    cvm::real result = 0.0;
    size_t i;
    for (i = 0; i < this->size(); i++) {
      result += (*this)[i];
    }
    return result;
  }

  /// Slicing
  inline vector1d<T> const slice(size_t const i1, size_t const i2) const
  {
    if ((i2 < i1) || (i2 >= this->size())) {
      cvm::error_static("Error: trying to slice a vector using incorrect boundaries.\n");
    }
    vector1d<T> result(i2 - i1);
    size_t i;
    for (i = 0; i < (i2 - i1); i++) {
      result[i] = (*this)[i1+i];
    }
    return result;
  }

  /// Assign a vector to a slice of this vector
  inline void sliceassign(size_t const i1, size_t const i2,
                          vector1d<T> const &v)
  {
    if ((i2 < i1) || (i2 >= this->size())) {
      cvm::error_static("Error: trying to slice a vector using incorrect boundaries.\n");
    }
    size_t i;
    for (i = 0; i < (i2 - i1); i++) {
      (*this)[i1+i] = v[i];
    }
  }

  /// Formatted output

  inline size_t output_width(size_t real_width) const
  {
    return real_width*(this->size()) + 3*(this->size()-1) + 4;
  }

  inline friend std::istream & operator >> (std::istream &is,
                                            cvm::vector1d<T> &v)
  {
    if (v.size() == 0) return is;
    std::streampos const start_pos = is.tellg();
    char sep;
    if ( !(is >> sep) || !(sep == '(') ) {
      is.clear();
      is.seekg(start_pos, std::ios::beg);
      is.setstate(std::ios::failbit);
      return is;
    }
    size_t count = 0;
    while ( (is >> v[count]) &&
            (count < (v.size()-1) ? ((is >> sep) && (sep == ',')) : true) ) {
      if (++count == v.size()) break;
    }
    if (count < v.size()) {
      is.clear();
      is.seekg(start_pos, std::ios::beg);
      is.setstate(std::ios::failbit);
    }
    return is;
  }

  inline friend std::ostream & operator << (std::ostream &os,
                                            cvm::vector1d<T> const &v)
  {
    std::streamsize const w = os.width();
    std::streamsize const p = os.precision();

    os.width(2);
    os << "( ";
    size_t i;
    for (i = 0; i < v.size()-1; i++) {
      os.width(w); os.precision(p);
      os << v[i] << " , ";
    }
    os.width(w); os.precision(p);
    os << v[v.size()-1] << " )";
    return os;
  }

  inline std::string to_simple_string() const
  {
    if (this->size() == 0) return std::string("");
    std::ostringstream os;
    os.setf(std::ios::scientific, std::ios::floatfield);
    // os.precision(cvmodule->cv_prec);
    os.precision(14);
    os << (*this)[0];
    size_t i;
    for (i = 1; i < this->size(); i++) {
      os << " " << (*this)[i];
    }
    return os.str();
  }

  inline int from_simple_string(std::string const &s)
  {
    std::stringstream stream(s);
    size_t i = 0;
    if (this->size()) {
      while ((i < this->size()) && (stream >> (*this)[i])) {
        i++;
      }
      if (i < this->size()) {
        return COLVARS_ERROR;
      }
    } else {
      T input;
      while (stream >> input) {
        if ((i % 100) == 0) {
          data.reserve(data.size()+100);
        }
        data.resize(data.size()+1);
        data[i] = input;
        i++;
      }
    }
    return COLVARS_OK;
  }

};


/// \brief Arbitrary size array (two dimensions) suitable for linear
/// algebra operations (i.e. for floating point numbers it can be used
/// with library functions)
template <class T> class colvarmodule::matrix2d
{
public:

  friend class row;
  size_t outer_length;
  size_t inner_length;

protected:

  class row {
  public:
    T * data;
    size_t length;
    inline row(T * const row_data, size_t const inner_length)
      : data(row_data), length(inner_length)
    {}
    inline T & operator [] (size_t const j) {
      return *(data+j);
    }
    inline T const & operator [] (size_t const j) const {
      return *(data+j);
    }
    inline operator vector1d<T>() const
    {
      return vector1d<T>(length, data);
    }
    inline int set(cvm::vector1d<T> const &v) const
    {
      if (v.size() != length) {
        return cvm::error_static("Error: setting a matrix row from a vector of "
                          "incompatible size.\n", COLVARS_BUG_ERROR);
      }
      for (size_t i = 0; i < length; i++) data[i] = v[i];
      return COLVARS_OK;
    }
  };

  std::vector<T> data;
  std::vector<row> rows;
  std::vector<T *> pointers;

public:

  /// Allocation routine, used by all constructors
  inline void resize(size_t const ol, size_t const il)
  {
    if ((ol > 0) && (il > 0)) {

      if (data.size() > 0) {
        // copy previous data
        size_t i, j;
        std::vector<T> new_data(ol * il);
        for (i = 0; i < outer_length; i++) {
          for (j = 0; j < inner_length; j++) {
            new_data[il*i+j] = data[inner_length*i+j];
          }
        }
        data.resize(ol * il);
        // copy them back
        data = new_data;
      } else {
        data.resize(ol * il);
      }

      outer_length = ol;
      inner_length = il;

      if (data.size() > 0) {
        // rebuild rows
        size_t i;
        rows.clear();
        rows.reserve(outer_length);
        pointers.clear();
        pointers.reserve(outer_length);
        for (i = 0; i < outer_length; i++) {
          rows.push_back(row(&(data[0])+inner_length*i, inner_length));
          pointers.push_back(&(data[0])+inner_length*i);
        }
     }
    } else {
      // zero size
      data.clear();
      rows.clear();
    }
  }

  /// Deallocation routine
  inline void clear() {
    rows.clear();
    data.clear();
  }

  /// Set all elements to zero
  inline void reset()
  {
    data.assign(data.size(), T(0.0));
  }

  inline size_t size() const
  {
    return data.size();
  }

  /// Default constructor
  inline matrix2d()
    : outer_length(0), inner_length(0)
  {
    this->resize(0, 0);
  }

  inline matrix2d(size_t const ol, size_t const il)
    : outer_length(ol), inner_length(il)
  {
    this->resize(outer_length, inner_length);
    reset();
  }

  /// Copy constructor
  inline matrix2d(matrix2d<T> const &m)
    : outer_length(m.outer_length), inner_length(m.inner_length)
  {
    // reinitialize data and rows arrays
    this->resize(outer_length, inner_length);
    // copy data
    data = m.data;
  }

  /// Destructor
  inline ~matrix2d() {
    this->clear();
  }

  /// Return a reference to the data
  inline std::vector<T> &data_array()
  {
    return data;
  }

  /// Return a reference to the data
  inline std::vector<T> const &data_array() const
  {
    return data;
  }

  inline row & operator [] (size_t const i)
  {
    return rows[i];
  }
  inline row const & operator [] (size_t const i) const
  {
    return rows[i];
  }

  /// Assignment
  inline matrix2d<T> & operator = (matrix2d<T> const &m)
  {
    if ((outer_length != m.outer_length) || (inner_length != m.inner_length)){
      this->clear();
      outer_length = m.outer_length;
      inner_length = m.inner_length;
      this->resize(outer_length, inner_length);
    }
    data = m.data;
    return *this;
  }

  /// Return the 2-d C array
  inline T ** c_array() {
    if (rows.size() > 0) {
      return &(pointers[0]);
    } else {
      return NULL;
    }
  }

  inline static void check_sizes(matrix2d<T> const &m1, matrix2d<T> const &m2)
  {
    if ((m1.outer_length != m2.outer_length) ||
        (m1.inner_length != m2.inner_length)) {
      cvm::error_static("Error: trying to perform an operation between "
                 "matrices of different sizes, "+
                 cvm::to_str(m1.outer_length)+"x"+
                 cvm::to_str(m1.inner_length)+" and "+
                 cvm::to_str(m2.outer_length)+"x"+
                 cvm::to_str(m2.inner_length)+".\n");
    }
  }

  inline void operator += (matrix2d<T> const &m)
  {
    check_sizes(*this, m);
    size_t i;
    for (i = 0; i < data.size(); i++) {
      data[i] += m.data[i];
    }
  }

  inline void operator -= (matrix2d<T> const &m)
  {
    check_sizes(*this, m);
    size_t i;
    for (i = 0; i < data.size(); i++) {
      data[i] -= m.data[i];
    }
  }

  inline void operator *= (cvm::real a)
  {
    size_t i;
    for (i = 0; i < data.size(); i++) {
      data[i] *= a;
    }
  }

  inline void operator /= (cvm::real a)
  {
    size_t i;
    for (i = 0; i < data.size(); i++) {
      data[i] /= a;
    }
  }

  inline friend matrix2d<T> operator + (matrix2d<T> const &m1,
                                        matrix2d<T> const &m2)
  {
    check_sizes(m1, m2);
    matrix2d<T> result(m1.outer_length, m1.inner_length);
    size_t i;
    for (i = 0; i < m1.data.size(); i++) {
      result.data[i] = m1.data[i] + m2.data[i];
    }
    return result;
  }

  inline friend matrix2d<T> operator - (matrix2d<T> const &m1,
                                        matrix2d<T> const &m2)
  {
    check_sizes(m1, m2);
    matrix2d<T> result(m1.outer_length, m1.inner_length);
    size_t i;
    for (i = 0; i < m1.data.size(); i++) {
      result.data[i] = m1.data[i] - m1.data[i];
    }
    return result;
  }

  inline friend matrix2d<T> operator * (matrix2d<T> const &m, cvm::real a)
  {
    matrix2d<T> result(m.outer_length, m.inner_length);
    size_t i;
    for (i = 0; i < m.data.size(); i++) {
      result.data[i] = m.data[i] * a;
    }
    return result;
  }

  inline friend matrix2d<T> operator * (cvm::real a, matrix2d<T> const &m)
  {
    return m * a;
  }

  inline friend matrix2d<T> operator / (matrix2d<T> const &m, cvm::real a)
  {
    matrix2d<T> result(m.outer_length, m.inner_length);
    size_t i;
    for (i = 0; i < m.data.size(); i++) {
      result.data[i] = m.data[i] * a;
    }
    return result;
  }

  /// vector-matrix multiplication
  inline friend vector1d<T> operator * (vector1d<T> const &v,
                                        matrix2d<T> const &m)
  {
    vector1d<T> result(m.inner_length);
    if (m.outer_length != v.size()) {
      cvm::error_static("Error: trying to multiply a vector and a matrix "
                 "of incompatible sizes, "+
                  cvm::to_str(v.size()) + " and " +
                 cvm::to_str(m.outer_length)+"x"+cvm::to_str(m.inner_length) +
                 ".\n");
    } else {
      size_t i, k;
      for (i = 0; i < m.inner_length; i++) {
        for (k = 0; k < m.outer_length; k++) {
          result[i] += m[k][i] * v[k];
        }
      }
    }
    return result;
  }

  /// Formatted output
  friend std::ostream & operator << (std::ostream &os,
                                     matrix2d<T> const &m)
  {
    std::streamsize const w = os.width();
    std::streamsize const p = os.precision();

    os.width(2);
    os << "( ";
    size_t i;
    for (i = 0; i < m.outer_length; i++) {
      os << " ( ";
      size_t j;
      for (j = 0; j < m.inner_length-1; j++) {
        os.width(w);
        os.precision(p);
        os << m[i][j] << " , ";
      }
      os.width(w);
      os.precision(p);
      os << m[i][m.inner_length-1] << " )";
    }

    os << " )";
    return os;
  }

  inline std::string to_simple_string() const
  {
    if (this->size() == 0) return std::string("");
    std::ostringstream os;
    os.setf(std::ios::scientific, std::ios::floatfield);
    // os.precision(cvmodule->cv_prec);
    os.precision(14);
    os << (*this)[0];
    size_t i;
    for (i = 1; i < data.size(); i++) {
      os << " " << data[i];
    }
    return os.str();
  }

  inline int from_simple_string(std::string const &s)
  {
    std::stringstream stream(s);
    size_t i = 0;
    while ((i < data.size()) && (stream >> data[i])) {
      i++;
    }
    if (i < data.size()) {
      return COLVARS_ERROR;
    }
    return COLVARS_OK;
  }

};


/// vector of real numbers with three components
class colvarmodule::rvector {

public:

  cvm::real x, y, z;

  inline COLVARS_HOST_DEVICE rvector()
  {
    reset();
  }

  /// \brief Set all components to zero
  inline COLVARS_HOST_DEVICE void reset()
  {
    set(0.0);
  }

  inline COLVARS_HOST_DEVICE rvector(cvm::real x_i, cvm::real y_i, cvm::real z_i)
  {
    set(x_i, y_i, z_i);
  }

  inline rvector(cvm::vector1d<cvm::real> const &v)
  {
    set(v[0], v[1], v[2]);
  }

  inline COLVARS_HOST_DEVICE rvector(cvm::real t)
  {
    set(t);
  }

  /// \brief Set all components to a scalar
  inline COLVARS_HOST_DEVICE void set(cvm::real value)
  {
    x = y = z = value;
  }

  /// \brief Assign all components
  inline COLVARS_HOST_DEVICE void set(cvm::real x_i, cvm::real y_i, cvm::real z_i)
  {
    x = x_i;
    y = y_i;
    z = z_i;
  }

  /// \brief Access cartesian components by index
  inline COLVARS_HOST_DEVICE cvm::real & operator [] (int i) {
    return (i == 0) ? x : (i == 1) ? y : (i == 2) ? z : x;
  }

  /// \brief Access cartesian components by index
  inline COLVARS_HOST_DEVICE cvm::real  operator [] (int i) const {
    return (i == 0) ? x : (i == 1) ? y : (i == 2) ? z : x;
  }

  inline cvm::vector1d<cvm::real> const as_vector() const
  {
    cvm::vector1d<cvm::real> result(3);
    result[0] = this->x;
    result[1] = this->y;
    result[2] = this->z;
    return result;
  }

  inline COLVARS_HOST_DEVICE void operator += (cvm::rvector const &v)
  {
    x += v.x;
    y += v.y;
    z += v.z;
  }

  inline COLVARS_HOST_DEVICE void operator -= (cvm::rvector const &v)
  {
    x -= v.x;
    y -= v.y;
    z -= v.z;
  }

  inline COLVARS_HOST_DEVICE void operator *= (cvm::real v)
  {
    x *= v;
    y *= v;
    z *= v;
  }

  inline COLVARS_HOST_DEVICE void operator /= (cvm::real const& v)
  {
    x /= v;
    y /= v;
    z /= v;
  }

  inline COLVARS_HOST_DEVICE cvm::real norm2() const
  {
    return (x*x + y*y + z*z);
  }

  inline COLVARS_HOST_DEVICE cvm::real norm() const
  {
#if (defined(__HIP_DEVICE_COMPILE__)) || (defined (__CUDA_ARCH__))
#define COLVARS_MATH_SQRT ::sqrt
#else
#define COLVARS_MATH_SQRT cvm::sqrt
#endif
    return COLVARS_MATH_SQRT(this->norm2());
#undef COLVARS_MATH_SQRT
  }

  inline COLVARS_HOST_DEVICE cvm::rvector unit() const
  {
    const cvm::real n = this->norm();
    return (n > 0. ? cvm::rvector(x, y, z)/n : cvm::rvector(1., 0., 0.));
  }

  static inline size_t output_width(size_t real_width)
  {
    return 3*real_width + 10;
  }


  static inline COLVARS_HOST_DEVICE
  cvm::rvector outer(cvm::rvector const &v1,
                     cvm::rvector const &v2)
  {
    return cvm::rvector( v1.y*v2.z - v2.y*v1.z,
                         -v1.x*v2.z + v2.x*v1.z,
                         v1.x*v2.y - v2.x*v1.y);
  }

  friend inline COLVARS_HOST_DEVICE cvm::rvector operator - (cvm::rvector const &v)
  {
    return cvm::rvector(-v.x, -v.y, -v.z);
  }

  friend inline COLVARS_HOST_DEVICE cvm::rvector operator + (cvm::rvector const &v1,
                                         cvm::rvector const &v2)
  {
    return cvm::rvector(v1.x + v2.x, v1.y + v2.y, v1.z + v2.z);
  }
  friend inline COLVARS_HOST_DEVICE cvm::rvector operator - (cvm::rvector const &v1,
                                         cvm::rvector const &v2)
  {
    return cvm::rvector(v1.x - v2.x, v1.y - v2.y, v1.z - v2.z);
  }

  /// Inner (dot) product
  friend inline COLVARS_HOST_DEVICE cvm::real operator * (cvm::rvector const &v1,
                                      cvm::rvector const &v2)
  {
    return v1.x * v2.x + v1.y * v2.y + v1.z * v2.z;
  }

  friend inline COLVARS_HOST_DEVICE cvm::rvector operator * (cvm::real a, cvm::rvector const &v)
  {
    return cvm::rvector(a*v.x, a*v.y, a*v.z);
  }

  friend inline COLVARS_HOST_DEVICE cvm::rvector operator * (cvm::rvector const &v, cvm::real a)
  {
    return cvm::rvector(a*v.x, a*v.y, a*v.z);
  }

  friend inline COLVARS_HOST_DEVICE cvm::rvector operator / (cvm::rvector const &v, cvm::real a)
  {
    return cvm::rvector(v.x/a, v.y/a, v.z/a);
  }

  std::string to_simple_string() const;
  int from_simple_string(std::string const &s);
};


/// \brief 2-dimensional array of real numbers with three components
/// along each dimension (works with colvarmodule::rvector)
class colvarmodule::rmatrix {

public:

  cvm::real xx, xy, xz, yx, yy, yz, zx, zy, zz;

  /// Default constructor
  inline COLVARS_HOST_DEVICE rmatrix()
  {
    reset();
  }

  /// Constructor component by component
  inline COLVARS_HOST_DEVICE
  rmatrix(cvm::real xxi, cvm::real xyi, cvm::real xzi,
          cvm::real yxi, cvm::real yyi, cvm::real yzi,
          cvm::real zxi, cvm::real zyi, cvm::real zzi)
  {
    xx = xxi;
    xy = xyi;
    xz = xzi;
    yx = yxi;
    yy = yyi;
    yz = yzi;
    zx = zxi;
    zy = zyi;
    zz = zzi;
  }


  inline COLVARS_HOST_DEVICE void reset()
  {
    xx = xy = xz = yx = yy = yz = zx = zy = zz = 0.0;
  }

  /// Return the determinant
  inline COLVARS_HOST_DEVICE cvm::real determinant() const
  {
    return
      (  xx * (yy*zz - zy*yz))
      - (yx * (xy*zz - zy*xz))
      + (zx * (xy*yz - yy*xz));
  }

  inline COLVARS_HOST_DEVICE cvm::rmatrix transpose() const
  {
    return cvm::rmatrix(xx, yx, zx,
                        xy, yy, zy,
                        xz, yz, zz);
  }

  inline COLVARS_HOST_DEVICE friend cvm::rvector operator * (cvm::rmatrix const &m,
                                         cvm::rvector const &r)
  {
    return cvm::rvector(m.xx*r.x + m.xy*r.y + m.xz*r.z,
                        m.yx*r.x + m.yy*r.y + m.yz*r.z,
                        m.zx*r.x + m.zy*r.y + m.zz*r.z);
  }

  inline COLVARS_HOST_DEVICE rmatrix& operator+=(const rmatrix& rhs) {
    this->xx += rhs.xx;
    this->xy += rhs.xy;
    this->xz += rhs.xz;
    this->yx += rhs.yx;
    this->yy += rhs.yy;
    this->yz += rhs.yz;
    this->zx += rhs.zx;
    this->zy += rhs.zy;
    this->zz += rhs.zz;
    return *this;
  }
};



/// \brief 1-dimensional vector of real numbers with four components and
/// a quaternion algebra
class colvarmodule::quaternion {

public:

  cvm::real q0, q1, q2, q3;

  /// Constructor component by component
  inline COLVARS_HOST_DEVICE quaternion(cvm::real const qv[4])
    : q0(qv[0]), q1(qv[1]), q2(qv[2]), q3(qv[3])
  {}

  /// Constructor component by component
  inline COLVARS_HOST_DEVICE quaternion(cvm::real q0i,
                    cvm::real q1i,
                    cvm::real q2i,
                    cvm::real q3i)
    : q0(q0i), q1(q1i), q2(q2i), q3(q3i)
  {}

  inline quaternion(cvm::vector1d<cvm::real> const &v)
    : q0(v[0]), q1(v[1]), q2(v[2]), q3(v[3])
  {}

  /// \brief Default constructor
  inline COLVARS_HOST_DEVICE quaternion()
  {
    reset();
  }

  /// \brief Set all components to zero (null quaternion)
  inline COLVARS_HOST_DEVICE void reset()
  {
    q0 = q1 = q2 = q3 = 0.0;
  }

  /// Tell the number of characters required to print a quaternion, given that of a real number
  static inline size_t output_width(size_t real_width)
  {
    return 4*real_width + 13;
  }

  std::string to_simple_string() const;
  int from_simple_string(std::string const &s);

  /// \brief Formatted output operator
  friend std::ostream & operator << (std::ostream &os, cvm::quaternion const &q);
  /// \brief Formatted input operator
  friend std::istream & operator >> (std::istream &is, cvm::quaternion &q);

  /// Access the quaternion as a 4-d array (return a reference)
  inline COLVARS_HOST_DEVICE cvm::real & operator [] (int i) {
    switch (i) {
    case 0:
      return this->q0;
    case 1:
      return this->q1;
    case 2:
      return this->q2;
    case 3:
      return this->q3;
    default:
#if !(defined(__NVCC__) || defined(__HIPCC__))
      cvm::error_static("Error: incorrect quaternion component.\n");
#endif
      return q0;
    }
  }

  /// Access the quaternion as a 4-d array (return a value)
  inline COLVARS_HOST_DEVICE cvm::real operator [] (int i) const {
    switch (i) {
    case 0:
      return this->q0;
    case 1:
      return this->q1;
    case 2:
      return this->q2;
    case 3:
      return this->q3;
    default:
#if !(defined(__NVCC__) || defined(__HIPCC__))
      cvm::error_static("Error: trying to access a quaternion "
                 "component which is not between 0 and 3.\n");
#endif
      return 0.0;
    }
  }

  inline cvm::vector1d<cvm::real> const as_vector() const
  {
    cvm::vector1d<cvm::real> result(4);
    result[0] = q0;
    result[1] = q1;
    result[2] = q2;
    result[3] = q3;
    return result;
  }

  /// Square norm of the quaternion
  inline COLVARS_HOST_DEVICE cvm::real norm2() const
  {
    return q0*q0 + q1*q1 + q2*q2 + q3*q3;
  }

  /// Norm of the quaternion
  inline COLVARS_HOST_DEVICE cvm::real norm() const
  {
#if (defined(__HIP_DEVICE_COMPILE__)) || (defined (__CUDA_ARCH__))
#define COLVARS_MATH_SQRT ::sqrt
#else
#define COLVARS_MATH_SQRT cvm::sqrt
#endif
    return COLVARS_MATH_SQRT(this->norm2());
#undef COLVARS_MATH_SQRT
  }

  /// Return the conjugate quaternion
  inline COLVARS_HOST_DEVICE cvm::quaternion conjugate() const
  {
    return cvm::quaternion(q0, -q1, -q2, -q3);
  }

  inline COLVARS_HOST_DEVICE void operator *= (cvm::real a)
  {
    q0 *= a; q1 *= a; q2 *= a; q3 *= a;
  }

  inline COLVARS_HOST_DEVICE void operator /= (cvm::real a)
  {
    q0 /= a; q1 /= a; q2 /= a; q3 /= a;
  }

  inline COLVARS_HOST_DEVICE void set_positive()
  {
    if (q0 > 0.0) return;
    q0 = -q0;
    q1 = -q1;
    q2 = -q2;
    q3 = -q3;
  }

  inline COLVARS_HOST_DEVICE void operator += (cvm::quaternion const &h)
  {
    q0+=h.q0; q1+=h.q1; q2+=h.q2; q3+=h.q3;
  }
  inline COLVARS_HOST_DEVICE void operator -= (cvm::quaternion const &h)
  {
    q0-=h.q0; q1-=h.q1; q2-=h.q2; q3-=h.q3;
  }

  /// Return the vector component
  inline COLVARS_HOST_DEVICE cvm::rvector get_vector() const
  {
    return cvm::rvector(q1, q2, q3);
  }


  friend inline COLVARS_HOST_DEVICE cvm::quaternion operator + (cvm::quaternion const &h,
                                            cvm::quaternion const &q)
  {
    return cvm::quaternion(h.q0+q.q0, h.q1+q.q1, h.q2+q.q2, h.q3+q.q3);
  }

  friend inline COLVARS_HOST_DEVICE cvm::quaternion operator - (cvm::quaternion const &h,
                                            cvm::quaternion const &q)
  {
    return cvm::quaternion(h.q0-q.q0, h.q1-q.q1, h.q2-q.q2, h.q3-q.q3);
  }

  /// \brief Provides the quaternion product.  \b NOTE: for the inner
  /// product use: `h.inner (q);`
  friend inline COLVARS_HOST_DEVICE cvm::quaternion operator * (cvm::quaternion const &h,
                                            cvm::quaternion const &q)
  {
    return cvm::quaternion(h.q0*q.q0 - h.q1*q.q1 - h.q2*q.q2 - h.q3*q.q3,
                           h.q0*q.q1 + h.q1*q.q0 + h.q2*q.q3 - h.q3*q.q2,
                           h.q0*q.q2 + h.q2*q.q0 + h.q3*q.q1 - h.q1*q.q3,
                           h.q0*q.q3 + h.q3*q.q0 + h.q1*q.q2 - h.q2*q.q1);
  }

  friend inline COLVARS_HOST_DEVICE cvm::quaternion operator * (cvm::real c,
                                            cvm::quaternion const &q)
  {
    return cvm::quaternion(c*q.q0, c*q.q1, c*q.q2, c*q.q3);
  }
  friend inline COLVARS_HOST_DEVICE cvm::quaternion operator * (cvm::quaternion const &q,
                                            cvm::real c)
  {
    return cvm::quaternion(q.q0*c, q.q1*c, q.q2*c, q.q3*c);
  }
  friend inline COLVARS_HOST_DEVICE cvm::quaternion operator / (cvm::quaternion const &q,
                                            cvm::real c)
  {
    return cvm::quaternion(q.q0/c, q.q1/c, q.q2/c, q.q3/c);
  }


  /// \brief Rotate v through this quaternion (put it in the rotated
  /// reference frame)
  inline COLVARS_HOST_DEVICE cvm::rvector rotate(cvm::rvector const &v) const
  {
    return ( (*this) * cvm::quaternion(0.0, v.x, v.y, v.z) *
             this->conjugate() ).get_vector();
  }

  /// \brief Rotate Q2 through this quaternion (put it in the rotated
  /// reference frame)
  inline COLVARS_HOST_DEVICE cvm::quaternion rotate(cvm::quaternion const &Q2) const
  {
    cvm::rvector const vq_rot = this->rotate(Q2.get_vector());
    return cvm::quaternion(Q2.q0, vq_rot.x, vq_rot.y, vq_rot.z);
  }

  /// Return the 3x3 matrix associated to this quaternion
  inline COLVARS_HOST_DEVICE cvm::rmatrix rotation_matrix() const
  {
    cvm::rmatrix R;

    R.xx = q0*q0 + q1*q1 - q2*q2 - q3*q3;
    R.yy = q0*q0 - q1*q1 + q2*q2 - q3*q3;
    R.zz = q0*q0 - q1*q1 - q2*q2 + q3*q3;

    R.xy = 2.0 * (q1*q2 - q0*q3);
    R.xz = 2.0 * (q0*q2 + q1*q3);

    R.yx = 2.0 * (q0*q3 + q1*q2);
    R.yz = 2.0 * (q2*q3 - q0*q1);

    R.zx = 2.0 * (q1*q3 - q0*q2);
    R.zy = 2.0 * (q0*q1 + q2*q3);

    return R;
  }

  /** \brief Calculate the sums of element-wise products of a given matrix with respect to dR/dq0, dR/dq1, dR/dq2 and dR/dq3
   *
   *  \tparam T The type of returning array (could be std::array or cuda::std::array)
   *  \param C A 3x3 matrix
   *
   *  \return A 4-element tuple (see the detailed documentation below).
   *
   *  This function is mainly used for projecting the gradients or forces on
   *  a rotation matrix to the gradients or forces on the quaternion of the
   *  same rotation matrix. Mathematically, let \f$C\f$ be the matrix
   *  \f[
   *  \begin{bmatrix}
   *  \frac{\partial f}{\partial R_{00}} & \frac{\partial f}{\partial R_{01}} & \frac{\partial f}{\partial R_{02}} \\
   *  \frac{\partial f}{\partial R_{10}} & \frac{\partial f}{\partial R_{11}} & \frac{\partial f}{\partial R_{12}} \\
   *  \frac{\partial f}{\partial R_{20}} & \frac{\partial f}{\partial R_{21}} & \frac{\partial f}{\partial R_{22}}
   *  \end{bmatrix}
   *  \f]
   *  and \f$\frac{{\rm d}R}{{\rm d}q_{i}}\f$ be
   *  \f[
   *  \begin{bmatrix}
   *  \frac{\partial R_{00}}{\partial q_i} & \frac{\partial R_{01}}{\partial q_i} & \frac{\partial R_{02}}{\partial q_i} \\
   *  \frac{\partial R_{10}}{\partial q_i} & \frac{\partial R_{11}}{\partial q_i} & \frac{\partial R_{12}}{\partial q_i} \\
   *  \frac{\partial R_{20}}{\partial q_i} & \frac{\partial R_{21}}{\partial q_i} & \frac{\partial R_{22}}{\partial q_i}
   *  \end{bmatrix}
   *  \f]
   *  This function returns
   *  \f[
   *  \left[\mathbf{e}^T\left(C \odot \frac{{\rm d}R}{{\rm d}q_0}\right)\mathbf{e},
   *        \mathbf{e}^T\left(C \odot \frac{{\rm d}R}{{\rm d}q_1}\right)\mathbf{e},
   *        \mathbf{e}^T\left(C \odot \frac{{\rm d}R}{{\rm d}q_2}\right)\mathbf{e},
   *        \mathbf{e}^T\left(C \odot \frac{{\rm d}R}{{\rm d}q_3}\right)\mathbf{e}\right]
   *  \f]
   *  where \f$\mathbf{e}\f$ is \f$[1, 1, 1]\f$ and \f$\odot\f$ is the element-wise product (Hadamard product).
   */
  template <typename T>
  inline COLVARS_HOST_DEVICE T derivative_element_wise_product_sum(const cvm::real (&C)[3][3]) const {
    return T{{
      2.0 * ( q0 * C[0][0] - q3 * C[0][1] + q2 * C[0][2] +
              q3 * C[1][0] + q0 * C[1][1] - q1 * C[1][2] +
             -q2 * C[2][0] + q1 * C[2][1] + q0 * C[2][2]),
      2.0 * ( q1 * C[0][0] + q2 * C[0][1] + q3 * C[0][2] +
              q2 * C[1][0] - q1 * C[1][1] - q0 * C[1][2] +
              q3 * C[2][0] + q0 * C[2][1] - q1 * C[2][2]),
      2.0 * (-q2 * C[0][0] + q1 * C[0][1] + q0 * C[0][2] +
              q1 * C[1][0] + q2 * C[1][1] + q3 * C[1][2] +
             -q0 * C[2][0] + q3 * C[2][1] - q2 * C[2][2]),
      2.0 * (-q3 * C[0][0] - q0 * C[0][1] + q1 * C[0][2] +
              q0 * C[1][0] - q3 * C[1][1] + q2 * C[1][2] +
              q1 * C[2][0] + q2 * C[2][1] + q3 * C[2][2])
    }};
  }

  /// \brief Return the cosine between the orientation frame
  /// associated to this quaternion and another
  inline COLVARS_HOST_DEVICE cvm::real cosine(cvm::quaternion const &q) const
  {
    cvm::real const iprod = this->inner(q);
    return 2.0*iprod*iprod - 1.0;
  }

  /// \brief Square distance from another quaternion on the
  /// 4-dimensional unit sphere: returns the square of the angle along
  /// the shorter of the two geodesics
  inline COLVARS_HOST_DEVICE cvm::real dist2(cvm::quaternion const &Q2) const
  {
    cvm::real const cos_omega = this->q0*Q2.q0 + this->q1*Q2.q1 +
      this->q2*Q2.q2 + this->q3*Q2.q3;
#if (defined(__HIP_DEVICE_COMPILE__)) || (defined (__CUDA_ARCH__))
#define COLVARS_MATH_ACOS ::acos
#else
#define COLVARS_MATH_ACOS cvm::acos
#endif
    cvm::real const omega = COLVARS_MATH_ACOS( (cos_omega > 1.0) ? 1.0 :
                                    ( (cos_omega < -1.0) ? -1.0 : cos_omega) );
#undef COLVARS_MATH_ACOS

    // get the minimum distance: x and -x are the same quaternion
    if (cos_omega > 0.0)
      return omega * omega;
    else
      return (PI-omega) * (PI-omega);
  }

  /// Gradient of the square distance: returns a 4-vector equivalent
  /// to that provided by slerp
  inline COLVARS_HOST_DEVICE cvm::quaternion dist2_grad(cvm::quaternion const &Q2) const
  {
#if (defined(__HIP_DEVICE_COMPILE__)) || (defined (__CUDA_ARCH__))
#define COLVARS_MATH_ACOS ::acos
#define COLVARS_MATH_SIN ::sin
#define COLVARS_MATH_FABS ::fabs
#else
#define COLVARS_MATH_ACOS cvm::acos
#define COLVARS_MATH_SIN cvm::sin
#define COLVARS_MATH_FABS cvm::fabs
#endif
    cvm::real const cos_omega = this->q0*Q2.q0 + this->q1*Q2.q1 + this->q2*Q2.q2 + this->q3*Q2.q3;
    cvm::real const omega = COLVARS_MATH_ACOS( (cos_omega > 1.0) ? 1.0 :
                                       ( (cos_omega < -1.0) ? -1.0 : cos_omega) );
    cvm::real const sin_omega = COLVARS_MATH_SIN(omega);

    if (COLVARS_MATH_FABS(sin_omega) < 1.0E-14) {
      // return a null 4d vector
      return cvm::quaternion(0.0, 0.0, 0.0, 0.0);
    }
#undef COLVARS_MATH_ACOS
#undef COLVARS_MATH_SIN
#undef COLVARS_MATH_FABS
    cvm::quaternion const
      grad1((-1.0)*sin_omega*Q2.q0 + cos_omega*(this->q0-cos_omega*Q2.q0)/sin_omega,
            (-1.0)*sin_omega*Q2.q1 + cos_omega*(this->q1-cos_omega*Q2.q1)/sin_omega,
            (-1.0)*sin_omega*Q2.q2 + cos_omega*(this->q2-cos_omega*Q2.q2)/sin_omega,
            (-1.0)*sin_omega*Q2.q3 + cos_omega*(this->q3-cos_omega*Q2.q3)/sin_omega);

    if (cos_omega > 0.0) {
      return 2.0*omega*grad1;
    } else {
      return -2.0*(PI-omega)*grad1;
    }
  }

  /// \brief Choose the closest between Q2 and -Q2 and save it back.
  /// Not required for dist2() and dist2_grad()
  inline COLVARS_HOST_DEVICE void match(cvm::quaternion &Q2) const
  {
    cvm::real const cos_omega = this->q0*Q2.q0 + this->q1*Q2.q1 +
      this->q2*Q2.q2 + this->q3*Q2.q3;
    if (cos_omega < 0.0) Q2 *= -1.0;
  }

  /// \brief Inner product (as a 4-d vector) with Q2; requires match()
  /// if the largest overlap is looked for
  inline COLVARS_HOST_DEVICE cvm::real inner(cvm::quaternion const &Q2) const
  {
    cvm::real const prod = this->q0*Q2.q0 + this->q1*Q2.q1 +
      this->q2*Q2.q2 + this->q3*Q2.q3;
    return prod;
  }


};

#ifndef COLVARS_LAMMPS
namespace NR {
int diagonalize_matrix(cvm::real m[4][4],
                        cvm::real eigval[4],
                        cvm::real eigvec[4][4]);
}
#endif


/// \brief A rotation between two sets of coordinates (for the moment
/// a wrapper for colvarmodule::quaternion)
class colvarmodule::rotation
{
private:
  /// Correlation matrix C (3, 3)
  cvm::rmatrix C;

  /// Overlap matrix S (4, 4)
  cvm::real S[4][4];

  /// Eigenvalues of S
  cvm::real S_eigval[4];

  /// Eigenvectors of S
  cvm::real S_eigvec[4][4];

  /// Used for debugging gradients
  cvm::real S_backup[4][4];

public:
  /// \brief Perform gradient tests
  bool b_debug_gradients;

  /// \brief The rotation itself (implemented as a quaternion)
  cvm::quaternion q{1.0, 0.0, 0.0, 0.0};

  friend struct rotation_derivative;
  friend class cvm::atom_group;

  cvm::real* get_S() {return (cvm::real*)S;}
  cvm::real* get_eigenvectors() {return (cvm::real*)S_eigvec;}
  cvm::real* get_eigenvalues() {return S_eigval;}
  cvm::quaternion* get_q() {return &q;}
  cvm::real* get_S_backup() {return (cvm::real*)S_backup;}
  cvm::rmatrix* get_C() {return &C;}

  /*! @brief  Function for debugging gradients
   *  @param[in]  pos1  Atom positions of group 1 in SOA (in xxxyyyzzz order)
   *  @param[in]  pos2  Atom positions of group 2 in SOA (in xxxyyyzzz order)
   *  @param[in]  num_atoms_pos1 Number of atoms of group 1
   *  @param[in]  num_atoms_pos2 Number of atoms of group 2
   */
  void debug_gradients(
    cvm::rotation &rot,
    const cvm::ag_vector_real_t &pos1,
    const cvm::ag_vector_real_t &pos2,
    const size_t num_atoms_pos1,
    const size_t num_atoms_pos2);

  /// \brief Calculate the optimal rotation and store the
  /// corresponding eigenvalue and eigenvector in the arguments l0 and
  /// q0; if the gradients have been previously requested, calculate
  /// them as well
  ///
  /// The method to derive the optimal rotation is defined in:
  /// Coutsias EA, Seok C, Dill KA.
  /// Using quaternions to calculate RMSD.
  /// J Comput Chem. 25(15):1849-57 (2004)
  /// DOI: 10.1002/jcc.20110  PubMed: 15376254
  void calc_optimal_rotation(std::vector<atom_pos> const &pos1,
                             std::vector<atom_pos> const &pos2);
  void calc_optimal_rotation_soa(
    cvm::ag_vector_real_t const &pos1,
    cvm::ag_vector_real_t const &pos2,
    const size_t num_atoms_pos1,
    const size_t num_atoms_pos2);

  /// Initialize member data
  int init();

  /// Default constructor
  rotation();

  /// Constructor after a quaternion
  rotation(cvm::quaternion const &qi);

  /// Constructor after an axis of rotation and an angle (in radians)
  rotation(cvm::real angle, cvm::rvector const &axis);

  /// Destructor
  ~rotation();

  /// Return the rotated vector
  inline cvm::rvector rotate(cvm::rvector const &v) const
  {
    return q.rotate(v);
  }

  /// Return the inverse of this rotation
  inline cvm::rotation inverse() const
  {
    return cvm::rotation(this->q.conjugate());
  }

  /// Return the associated 3x3 matrix
  inline cvm::rmatrix matrix() const
  {
    return q.rotation_matrix();
  }

  /// \brief Return the spin angle (in degrees) with respect to the
  /// provided axis (which MUST be normalized)
  inline cvm::real spin_angle(cvm::rvector const &axis) const
  {
    cvm::rvector const q_vec = q.get_vector();
    cvm::real alpha = (180.0/PI) * 2.0 * cvm::atan2(axis * q_vec, q.q0);
    while (alpha >  180.0) alpha -= 360;
    while (alpha < -180.0) alpha += 360;
    return alpha;
  }

  /// \brief Return the derivative of the spin angle with respect to
  /// the quaternion
  inline cvm::quaternion dspin_angle_dq(cvm::rvector const &axis) const
  {
    cvm::rvector const q_vec = q.get_vector();
    cvm::real const iprod = axis * q_vec;

    if (q.q0 != 0.0) {

      cvm::real const dspindx =
        (180.0/PI) * 2.0 * (1.0 / (1.0 + (iprod*iprod)/(q.q0*q.q0)));

      return cvm::quaternion( dspindx * (iprod * (-1.0) / (q.q0*q.q0)),
                              dspindx * ((1.0/q.q0) * axis.x),
                              dspindx * ((1.0/q.q0) * axis.y),
                              dspindx * ((1.0/q.q0) * axis.z));
    } else {
      // (1/(1+x^2)) ~ (1/x)^2
      // The documentation of spinAngle discourages its use when q_vec and
      // axis are not close
      return cvm::quaternion((180.0/PI) * 2.0 * ((-1.0)/iprod), 0.0, 0.0, 0.0);
    }
  }

  /// \brief Return the projection of the orientation vector onto a
  /// predefined axis
  inline cvm::real cos_theta(cvm::rvector const &axis) const
  {
    cvm::rvector const q_vec = q.get_vector();
    cvm::real const alpha =
      (180.0/PI) * 2.0 * cvm::atan2(axis * q_vec, q.q0);

    cvm::real const cos_spin_2 = cvm::cos(alpha * (PI/180.0) * 0.5);
    cvm::real const cos_theta_2 = ( (cos_spin_2 != 0.0) ?
                                    (q.q0 / cos_spin_2) :
                                    (0.0) );
    // cos(2t) = 2*cos(t)^2 - 1
    return 2.0 * (cos_theta_2*cos_theta_2) - 1.0;
  }

  /// Return the derivative of the tilt wrt the quaternion
  inline cvm::quaternion dcos_theta_dq(cvm::rvector const &axis) const
  {
    cvm::rvector const q_vec = q.get_vector();
    cvm::real const iprod = axis * q_vec;

    cvm::real const cos_spin_2 = cvm::cos(cvm::atan2(iprod, q.q0));

    if (q.q0 != 0.0)  {

      cvm::real const d_cos_theta_dq0 =
        (4.0 * q.q0 / (cos_spin_2*cos_spin_2)) *
        (1.0 - (iprod*iprod)/(q.q0*q.q0) / (1.0 + (iprod*iprod)/(q.q0*q.q0)));

      cvm::real const d_cos_theta_dqn =
        (4.0 * q.q0 / (cos_spin_2*cos_spin_2) *
         (iprod/q.q0) / (1.0 + (iprod*iprod)/(q.q0*q.q0)));

      return cvm::quaternion(d_cos_theta_dq0,
                             d_cos_theta_dqn * axis.x,
                             d_cos_theta_dqn * axis.y,
                             d_cos_theta_dqn * axis.z);
    } else {

      cvm::real const d_cos_theta_dqn =
        (4.0 / (cos_spin_2*cos_spin_2 * iprod));

      return cvm::quaternion(0.0,
                             d_cos_theta_dqn * axis.x,
                             d_cos_theta_dqn * axis.y,
                             d_cos_theta_dqn * axis.z);
    }
  }

  /// \brief Whether to test for eigenvalue crossing
  static bool monitor_crossings;
  /// \brief Threshold for the eigenvalue crossing test
  static cvm::real crossing_threshold;

protected:

  /// \brief Previous value of the rotation (used to warn the user
  /// when the structure changes too much, and there may be an
  /// eigenvalue crossing)
  cvm::quaternion q_old;

  /// Build the correlation matrix C (used by calc_optimal_rotation())
  void build_correlation_matrix(std::vector<cvm::atom_pos> const &pos1,
                                std::vector<cvm::atom_pos> const &pos2);

  /// \brief Actual implementation of `calc_optimal_rotation` (and called by it)
  void calc_optimal_rotation_impl();

  /// Compute the overlap matrix S (used by calc_optimal_rotation())
  void compute_overlap_matrix();

  /// Pointer to instance of Jacobi solver
  void *jacobi;
};

#if defined (COLVARS_CUDA) || defined (COLVARS_HIP)
namespace colvars_gpu {
class jacobi_gpu;
class rotation_gpu {
private:
  // cudaStream_t stream;
  /// Correlation matrix C (3, 3)
  // cvm::rmatrix* d_C;
  /// Overlap matrix S (4, 4)
  cvm::real* d_S;
  /// Eigenvalues of S
  cvm::real* d_S_eigval;
  /**
   * \brief Eigenvectors of S
   * @note d_S_eigvec is in column-major order.
   *   To get j-th element of the i-th eigenvector,
   *   use d_S_eigvec[i*4+j]
   */
  cvm::real* d_S_eigvec;
  /// Used for debugging gradients
  // cvm::real* d_S_backup;
  /// Eigensolver
  // jacobi_gpu* jacobi;
  /// Thread block atomic counter
  unsigned int* tbcount;
  /// \brief The rotation itself (implemented as a quaternion)
  cvm::quaternion* d_q;
  /// \brief Crossing monitor
  cvm::quaternion* d_q_old;
  int* discontinuous_rotation;
  /// \brief Flag for checking if eigendecomposition is failed
  int* max_iteration_reached;
  /// \brief Flag for checking if initialized
  bool b_initialized;
  /// \brief Host data for compatibility with CPU buffers
  cvm::rmatrix* h_C;
  cvm::real* h_S;
  cvm::real* h_S_eigval;
  cvm::real* h_S_eigvec;
public:
  /// Constructor
  rotation_gpu();
  /// Destructor
  ~rotation_gpu();
  /// Check if the object is initialized
  bool initialized() const {return b_initialized;}
  /// Initialize member data
  int init(/*const cudaStream_t& stream_in*/);
  /// \brief Calculate the optimal rotation and store the
  /// corresponding eigenvalue and eigenvector in the arguments l0 and
  /// q0; if the gradients have been previously requested, calculate
  /// them as well
  ///
  /// The method to derive the optimal rotation is defined in:
  /// Coutsias EA, Seok C, Dill KA.
  /// Using quaternions to calculate RMSD.
  /// J Comput Chem. 25(15):1849-57 (2004)
  /// DOI: 10.1002/jcc.20110  PubMed: 15376254
  int add_optimal_rotation_nodes(
    cvm::real* const d_pos1,
    cvm::real* const d_pos2,
    const size_t num_atoms_pos1,
    const size_t num_atoms_pos2,
    cudaGraph_t& graph,
    std::unordered_map<std::string, cudaGraphNode_t>& nodes_map);
  /// \brief Checking after synchronization (
  /// eigendecomposition iterations and max crossing)
  void after_sync_check() const;

  friend struct rotation_derivative_gpu;
  cvm::real* get_S() const {return d_S;}
  cvm::real* get_eigenvectors() const {return d_S_eigvec;}
  cvm::real* get_eigenvalues() const {return d_S_eigval;}
  cvm::quaternion* get_q() const {return d_q;}

  void to_cpu(cvm::rotation& rot) const;
};
}
#endif // defined(COLVARS_CUDA) || defined(COLVARS_HIP)

#endif
