/* ----------------------------------------------------------------------
   LAMMPS - Large-scale Atomic/Molecular Massively Parallel Simulator
   https://www.lammps.org/ Sandia National Laboratories
   LAMMPS development team: developers@lammps.org

   Copyright (2003) Sandia Corporation.  Under the terms of Contract
   DE-AC04-94AL85000 with Sandia Corporation, the U.S. Government retains
   certain rights in this software.  This software is distributed under
   the GNU General Public License.

   See the README file in the top-level LAMMPS directory.
------------------------------------------------------------------------- */

/* ----------------------------------------------------------------------
   Contributing authors:
     Trung Nguyen and Monica Olvera de la Cruz (Northwestern)
     based on the original implementation by Honghao Li (Northwestern)
   Reference:
     Jadhao, Solis, Olvera de la Cruz, J. Chem. Phys. 138, 054119, 2013

   Solve the following eq. for induced charges on fixed sharp interfaces:
     (Rww + Rww^T) w = q Rwq
   at every time step, the vector (q Rwq) is computed, and so
       w = [Rww + Rww^T)^(-1)] (q Rwq)
   NOTE: Oct 7, 2019: switch to using a conjugate gradient solver
------------------------------------------------------------------------- */

#include "fix_polarize_functional.h"

#include "atom.h"
#include "atom_vec_dielectric.h"
#include "comm.h"
#include "domain.h"
#include "error.h"
#include "force.h"
#include "kspace.h"
#include "math_const.h"
#include "math_extra.h"
#include "math_special.h"
#include "memory.h"
#include "msm_dielectric.h"
#include "neigh_list.h"
#include "neighbor.h"
#include "pair_coul_cut_dielectric.h"
#include "pair_coul_long_dielectric.h"
#include "pair_lj_cut_coul_cut_dielectric.h"
#include "pair_lj_cut_coul_debye_dielectric.h"
#include "pair_lj_cut_coul_long_dielectric.h"
#include "pair_lj_cut_coul_msm_dielectric.h"
#include "pppm_dielectric.h"
#include "update.h"

#include <cmath>
#include <cstring>

using namespace LAMMPS_NS;
using namespace FixConst;
using MathConst::MY_PI;
using MathConst::MY_PIS;
using MathSpecial::square;

//#define _POLARIZE_DEBUG

enum { REAL2SCALED = 0, SCALED2REAL = 1 };

static constexpr double EPSILON = 1.0e-6;

/* ---------------------------------------------------------------------- */

FixPolarizeFunctional::FixPolarizeFunctional(LAMMPS *_lmp, int narg, char **arg) :
    Fix(_lmp, narg, arg)
{
  if (narg < 4) error->all(FLERR, "Illegal fix polarize/functional command");

  avec = dynamic_cast<AtomVecDielectric *>(atom->style_match("dielectric"));
  if (!avec) error->all(FLERR, "Fix polarize/functional requires atom style dielectric");

  nevery = utils::inumeric(FLERR, arg[3], false, lmp);
  if (nevery < 0) error->all(FLERR, "Illegal fix polarize/functional command");

  tolerance = EPSILON;
  if (narg == 5) tolerance = utils::numeric(FLERR, arg[4], false, lmp);

  comm_forward = 1;
  nmax = 0;
  allocated = 0;

  induced_charge_idx = nullptr;
  induced_charges = nullptr;
  tag2mat = nullptr;
  mat2tag = nullptr;
  tag2mat_ions = nullptr;
  mat2tag_ions = nullptr;
  ion_idx = nullptr;

  rhs1 = nullptr;
  rhs2 = nullptr;
  buffer1 = nullptr;
  buffer2 = nullptr;

  // set flags for arrays to clear in force_clear()

  torqueflag = extraflag = 0;
  if (atom->torque_flag) torqueflag = 1;
  if (atom->avec->forceclearflag) extraflag = 1;

  Rww = nullptr;
  inverse_matrix = nullptr;
  G1ww = nullptr;
  G2ww = nullptr;
  G3ww = nullptr;
  ndotGww = nullptr;

  qiRqwVector = nullptr;
  G1qw_real = nullptr;
  sum2G2wq = nullptr;

  sum1G2qw = nullptr;
  sum1G1qw_epsilon = nullptr;
  sum2ndotGwq_epsilon = nullptr;

  efield_pair = nullptr;
  efield_kspace = nullptr;

  includingG3ww = 1;

  cg_r = cg_p = cg_Ap = nullptr;
  cg_A = nullptr;

  FixPolarizeFunctional::grow_arrays(atom->nmax);
  atom->add_callback(0);    // to ensure to work with atom->sort()
}

/* ---------------------------------------------------------------------- */

FixPolarizeFunctional::~FixPolarizeFunctional()
{
  memory->destroy(mat2tag);
  memory->destroy(tag2mat);
  memory->destroy(tag2mat_ions);
  memory->destroy(mat2tag_ions);
  memory->destroy(ion_idx);
  memory->destroy(induced_charge_idx);
  memory->destroy(induced_charges);
  memory->destroy(rhs1);
  memory->destroy(rhs2);
  memory->destroy(buffer1);
  memory->destroy(buffer2);

  if (allocated) deallocate();
  atom->delete_callback(id, 0);
}

/* ---------------------------------------------------------------------- */

int FixPolarizeFunctional::setmask()
{
  int mask = 0;
  mask |= PRE_FORCE;
  return mask;
}

/* ---------------------------------------------------------------------- */

void FixPolarizeFunctional::init()
{
  // mapping induced charge matrix/vector to atom tags and vice versa

  int i, maxtag;
  int *mask = atom->mask;
  tagint *tag = atom->tag;
  int nlocal = atom->nlocal;
  tagint max_tag;
  tagint itmp;
  int *ncount = nullptr;

  // induced charge arrays setup

  max_tag = -1;
  for (i = 0; i < nlocal; i++)
    if (mask[i] & groupbit) max_tag = MAX(max_tag, tag[i]);

  MPI_Allreduce(&max_tag, &itmp, 1, MPI_LMP_TAGINT, MPI_MAX, world);
  maxtag = (int) itmp;

  memory->create(ncount, maxtag + 1, "polarize:ncount");
  for (i = 0; i <= maxtag; i++) ncount[i] = 0;

  for (i = 0; i < nlocal; i++)
    if (mask[i] & groupbit) ncount[tag[i]]++;

  memory->create(tag2mat, maxtag + 1, "polarize:tag2mat");
  MPI_Allreduce(ncount, tag2mat, maxtag + 1, MPI_INT, MPI_SUM, world);

  num_induced_charges = 0;
  for (i = 0; i <= maxtag; i++)
    if (tag2mat[i])
      tag2mat[i] = num_induced_charges++;
    else
      tag2mat[i] = -1;

  memory->create(mat2tag, num_induced_charges, "polarize:mat2tag");

  num_induced_charges = 0;
  for (i = 0; i <= maxtag; i++)
    if (tag2mat[i] >= 0) mat2tag[num_induced_charges++] = i;

  for (i = 0; i < nlocal; i++) {
    induced_charge_idx[i] = -1;
    if (mask[i] & groupbit) induced_charge_idx[i] = tag2mat[tag[i]];
  }

  memory->destroy(ncount);

  // ion arrays setup

  max_tag = -1;
  for (i = 0; i < nlocal; i++)
    if (!(mask[i] & groupbit)) max_tag = MAX(max_tag, tag[i]);

  MPI_Allreduce(&max_tag, &itmp, 1, MPI_LMP_TAGINT, MPI_MAX, world);
  maxtag = (int) itmp;

  memory->create(ncount, maxtag + 1, "polarize:ncount");
  for (i = 0; i <= maxtag; i++) ncount[i] = 0;

  for (i = 0; i < nlocal; i++)
    if (!(mask[i] & groupbit)) ncount[tag[i]]++;

  memory->create(tag2mat_ions, maxtag + 1, "polarize:tag2mat_ions");
  MPI_Allreduce(ncount, tag2mat_ions, maxtag + 1, MPI_INT, MPI_SUM, world);

  num_ions = 0;
  for (i = 0; i <= maxtag; i++)
    if (tag2mat_ions[i])
      tag2mat_ions[i] = num_ions++;
    else
      tag2mat_ions[i] = -1;

  memory->create(mat2tag_ions, num_ions, "polarize:mat2tag_ions");
  memory->create(rhs1, num_induced_charges, "polarize:rhs1");
  memory->create(rhs2, num_induced_charges, "polarize:rhs2");
  int buffer_size = (num_induced_charges > num_ions) ? num_induced_charges : num_ions;
  memory->create(buffer1, buffer_size, num_induced_charges, "polarize:buffer1");
  memory->create(buffer2, num_induced_charges, num_induced_charges, "polarize:buffer2");
  memory->create(induced_charges, num_induced_charges, "polarize:induced_charges");

  num_ions = 0;
  for (i = 0; i <= maxtag; i++)
    if (tag2mat_ions[i] >= 0) mat2tag_ions[num_ions++] = i;

  for (i = 0; i < nlocal; i++) {
    ion_idx[i] = -1;
    if (!(mask[i] & groupbit)) ion_idx[i] = tag2mat_ions[tag[i]];
  }

  memory->destroy(ncount);

  if (allocated == 0) {
    nmax = atom->nmax;
    allocate();
    allocated = 1;
  }

  // need a full neighbor list w/ Newton off and ghost neighbors
  // built whenever re-neighboring occurs

  neighbor->add_request(this, NeighConst::REQ_FULL);

  if (force->kspace)
    g_ewald = force->kspace->g_ewald;
  else
    g_ewald = 0.01;

  if (comm->me == 0)
    utils::logmesg(lmp, "Direct solver using a variational approach for {} induced charges\n",
                   num_induced_charges);
}

/* ---------------------------------------------------------------------- */

void FixPolarizeFunctional::init_list(int /*id*/, NeighList *ptr)
{
  list = ptr;
}

/* ---------------------------------------------------------------------- */

void FixPolarizeFunctional::setup(int /*vflag*/)
{
  // check if the pair styles in use are compatible

  if (strcmp(force->pair_style, "lj/cut/coul/long/dielectric") == 0)
    efield_pair = (dynamic_cast<PairLJCutCoulLongDielectric *>(force->pair))->efield;
  else if (strcmp(force->pair_style, "lj/cut/coul/long/dielectric/omp") == 0)
    efield_pair = (dynamic_cast<PairLJCutCoulLongDielectric *>(force->pair))->efield;
  else if (strcmp(force->pair_style, "lj/cut/coul/msm/dielectric") == 0)
    efield_pair = (dynamic_cast<PairLJCutCoulMSMDielectric *>(force->pair))->efield;
  else if (strcmp(force->pair_style, "lj/cut/coul/cut/dielectric") == 0)
    efield_pair = (dynamic_cast<PairLJCutCoulCutDielectric *>(force->pair))->efield;
  else if (strcmp(force->pair_style, "lj/cut/coul/cut/dielectric/omp") == 0)
    efield_pair = (dynamic_cast<PairLJCutCoulCutDielectric *>(force->pair))->efield;
  else if (strcmp(force->pair_style, "lj/cut/coul/debye/dielectric") == 0)
    efield_pair = (dynamic_cast<PairLJCutCoulDebyeDielectric *>(force->pair))->efield;
  else if (strcmp(force->pair_style, "lj/cut/coul/debye/dielectric/omp") == 0)
    efield_pair = (dynamic_cast<PairLJCutCoulDebyeDielectric *>(force->pair))->efield;
  else if (strcmp(force->pair_style, "coul/long/dielectric") == 0)
    efield_pair = (dynamic_cast<PairCoulLongDielectric *>(force->pair))->efield;
  else if (strcmp(force->pair_style, "coul/cut/dielectric") == 0)
    efield_pair = (dynamic_cast<PairCoulCutDielectric *>(force->pair))->efield;
  else
    error->all(FLERR, "Pair style not compatible with fix polarize/functional");

  if (force->kspace) {

    kspaceflag = 1;
    if (strcmp(force->kspace_style, "pppm/dielectric") == 0)
      efield_kspace = (dynamic_cast<PPPMDielectric *>(force->kspace))->efield;
    else if (strcmp(force->kspace_style, "msm/dielectric") == 0)
      efield_kspace = (dynamic_cast<MSMDielectric *>(force->kspace))->efield;
    else
      error->all(FLERR, "Kspace style not compatible with fix polarize/functional");

  } else {

    if (kspaceflag == 1) {    // users specified kspace yes
      error->warning(FLERR, "No Kspace style available for fix polarize/functional");
      kspaceflag = 0;
    }
  }

  update_induced_charges();
}

/* ---------------------------------------------------------------------- */

void FixPolarizeFunctional::setup_pre_force(int /*vflag*/)
{
  // calculate Rww before the run (assuming that the interface is fixed for now)
  // otherwise this should be done every time step in pre_force()

  calculate_Rww_cutoff();
}

/* ---------------------------------------------------------------------- */

void FixPolarizeFunctional::pre_force(int)
{
  if (nevery == 0) return;
  if (update->ntimestep % nevery) return;

  // solve for the induced charges

  update_induced_charges();
}

/* ---------------------------------------------------------------------- */

void FixPolarizeFunctional::update_induced_charges()
{
  // convert all ion charges from scaled (q_scaled) to real q by multiplying with epsilon

  charge_rescaled(SCALED2REAL);

  // compute the right hand side vector qiRwVector

  calculate_qiRqw_cutoff();

  // conjugate gradient solver for w from Rww * w = -qRqw

  for (int i = 0; i < num_induced_charges; i++)
    for (int j = 0; j < num_induced_charges; j++) cg_A[i][j] = Rww[i][j] + Rww[j][i];

  for (int i = 0; i < num_induced_charges; i++) induced_charges[i] = 0;

  cg_solver(cg_A, qiRqwVector, induced_charges, num_induced_charges);

  // assign charges to the particles in the group

  double *q_scaled = atom->q_scaled;
  int nlocal = atom->nlocal;

  for (int i = 0; i < nlocal; i++) {
    if (induced_charge_idx[i] < 0) continue;
    int idx = induced_charge_idx[i];
    q_scaled[i] = -induced_charges[idx] / (4 * MY_PI);
  }

  // revert to scaled charges to calculate forces

  charge_rescaled(REAL2SCALED);
}

/* ----------------------------------------------------------------------
  scaled2real = 1: convert ion charges from scaled values (divided by epsilon) to real values
              = 0:                          real values to scaled values
------------------------------------------------------------------------- */

void FixPolarizeFunctional::charge_rescaled(int scaled2real)
{
  double *q_scaled = atom->q_scaled;
  double *q = atom->q;
  double *epsilon = atom->epsilon;
  int nlocal = atom->nlocal;

  if (scaled2real == SCALED2REAL) {
    for (int i = 0; i < nlocal; i++)
      if (induced_charge_idx[i] < 0) q_scaled[i] = q[i];
  } else {
    for (int i = 0; i < nlocal; i++)
      if (induced_charge_idx[i] < 0) q_scaled[i] = q[i] / epsilon[i];
  }

  // communicate q_scaled to neighboring procs

  comm->forward_comm(this);
}

/* ---------------------------------------------------------------------- */

void FixPolarizeFunctional::allocate()
{
  // initialize all data
  // interface terms, all matrix of M*M
  memory->create(inverse_matrix, num_induced_charges, num_induced_charges, "fix:inverse_matrix");
  memory->create(Rww, num_induced_charges, num_induced_charges, "fix:Rww");
  memory->create(G1ww, num_induced_charges, num_induced_charges, "fix:G1ww");
  memory->create(ndotGww, num_induced_charges, num_induced_charges, "fix:ndotGww");
  memory->create(G2ww, num_induced_charges, num_induced_charges, "fix:G2ww");
  memory->create(G3ww, num_induced_charges, num_induced_charges, "fix:G3ww");

  // each step, qw, qq terms, temp data

  memory->create(qiRqwVector, num_induced_charges, "fix:qiRqwVector");
  memory->create(sum2G2wq, num_induced_charges, "fix:sum2G2wq");
  memory->create(G1qw_real, num_ions, num_induced_charges, "fix:G1qw_real");

  // arrays of M
  memory->create(sum1G2qw, num_induced_charges, "fix:sum1G2qw");
  memory->create(sum1G1qw_epsilon, num_induced_charges, "fix:sum1G1qw_epsilon");
  memory->create(sum2ndotGwq_epsilon, num_induced_charges, "fix:sum2ndotGwq_epsilon");

  memory->create(cg_r, num_induced_charges, "polarize:cg_r");
  memory->create(cg_p, num_induced_charges, "polarize:cg_p");
  memory->create(cg_Ap, num_induced_charges, "polarize:cg_Ap");
  memory->create(cg_A, num_induced_charges, num_induced_charges, "polarize:cg_A");
}

/* ---------------------------------------------------------------------- */

void FixPolarizeFunctional::deallocate()
{
  memory->destroy(inverse_matrix);
  memory->destroy(Rww);
  memory->destroy(G1ww);
  memory->destroy(G2ww);
  memory->destroy(G3ww);
  memory->destroy(ndotGww);

  memory->destroy(qiRqwVector);
  memory->destroy(sum2G2wq);
  memory->destroy(G1qw_real);

  memory->destroy(sum1G2qw);
  memory->destroy(sum1G1qw_epsilon);
  memory->destroy(sum2ndotGwq_epsilon);

  memory->destroy(cg_r);
  memory->destroy(cg_p);
  memory->destroy(cg_Ap);
  memory->destroy(cg_A);
}

/* ---------------------------------------------------------------------- */

int FixPolarizeFunctional::modify_param(int narg, char **arg)
{
  int iarg = 0;
  while (iarg < narg) {
    if (strcmp(arg[iarg], "kspace") == 0) {
      if (iarg + 2 > narg) error->all(FLERR, "Illegal fix_modify command");
      kspaceflag = utils::logical(FLERR, arg[iarg + 1], false, lmp);
      iarg += 2;
    } else if (strcmp(arg[iarg], "dielectrics") == 0) {
      if (iarg + 6 > narg) error->all(FLERR, "Illegal fix_modify command");
      double epsiloni = -1, areai = -1;
      double q_unscaled = 0;
      int set_charge = 0;
      double ediff = utils::numeric(FLERR, arg[iarg + 1], false, lmp);
      double emean = utils::numeric(FLERR, arg[iarg + 2], false, lmp);
      if (strcmp(arg[iarg + 3], "NULL") != 0)
        epsiloni = utils::numeric(FLERR, arg[iarg + 3], false, lmp);
      if (strcmp(arg[iarg + 4], "NULL") != 0)
        areai = utils::numeric(FLERR, arg[iarg + 4], false, lmp);
      if (strcmp(arg[iarg + 5], "NULL") != 0) {
        q_unscaled = utils::numeric(FLERR, arg[iarg + 5], false, lmp);
        set_charge = 1;
      }
      set_dielectric_params(ediff, emean, epsiloni, areai, set_charge, q_unscaled);

      iarg += 6;
    } else
      error->all(FLERR, "Illegal fix_modify command");
  }

  return iarg;
}

/* ----------------------------------------------------------------------
   pack values in local atom-based arrays for exchange with another proc
------------------------------------------------------------------------- */

int FixPolarizeFunctional::pack_exchange(int i, double *buf)
{
  buf[0] = ubuf(induced_charge_idx[i]).d;
  buf[1] = ubuf(ion_idx[i]).d;
  return 2;
}

/* ----------------------------------------------------------------------
   unpack values in local atom-based arrays from exchange with another proc
------------------------------------------------------------------------- */

int FixPolarizeFunctional::unpack_exchange(int nlocal, double *buf)
{
  induced_charge_idx[nlocal] = (int) ubuf(buf[0]).i;
  ion_idx[nlocal] = (int) ubuf(buf[1]).i;
  return 2;
}

/* ---------------------------------------------------------------------- */

int FixPolarizeFunctional::pack_forward_comm(int n, int *list, double *buf, int /*pbc_flag*/,
                                             int * /*pbc*/)
{
  int m;
  for (m = 0; m < n; m++) buf[m] = atom->q_scaled[list[m]];
  return n;
}

/* ---------------------------------------------------------------------- */

void FixPolarizeFunctional::unpack_forward_comm(int n, int first, double *buf)
{
  int i, m;
  for (m = 0, i = first; m < n; m++, i++) atom->q_scaled[i] = buf[m];
}

/* ----------------------------------------------------------------------
   allocate local atom-based arrays
------------------------------------------------------------------------- */

void FixPolarizeFunctional::grow_arrays(int n)
{
  if (n > nmax) nmax = n;
  memory->grow(induced_charge_idx, nmax, "fix:induced_charge_idx");
  memory->grow(ion_idx, nmax, "fix:ion_idx");
}

/* ----------------------------------------------------------------------
   copy values within local atom-based arrays
------------------------------------------------------------------------- */

void FixPolarizeFunctional::copy_arrays(int i, int j, int /*delflag*/)
{
  induced_charge_idx[j] = induced_charge_idx[i];
  ion_idx[j] = ion_idx[i];
}

/* ----------------------------------------------------------------------
   initialize one atom's array values, called when atom is created
------------------------------------------------------------------------- */

void FixPolarizeFunctional::set_arrays(int i)
{
  induced_charge_idx[i] = -1;
  ion_idx[i] = 0;
}

/* ----------------------------------------------------------------------
   return # of bytes of allocated memory
------------------------------------------------------------------------- */

double FixPolarizeFunctional::memory_usage()
{
  double bytes = 0;
  bytes += square(num_induced_charges) * sizeof(double);                // inverse_matrix
  bytes += square(num_induced_charges) * sizeof(double);                // Rww
  bytes += square(num_induced_charges) * sizeof(double);                // G1ww
  bytes += square(num_induced_charges) * sizeof(double);                // ndotGww
  bytes += square(num_induced_charges) * sizeof(double);                // G2ww
  bytes += square(num_induced_charges) * sizeof(double);                // G3ww
  bytes += num_induced_charges * sizeof(double);                        // qiRqwVector
  bytes += num_induced_charges * sizeof(double);                        // sum2G2wq
  bytes += num_induced_charges * sizeof(double);                        // sum1G2qw
  bytes += num_induced_charges * sizeof(double);                        // sum1G1qw_epsilon
  bytes += num_induced_charges * sizeof(double);                        // sum2ndotGwq_epsilon
  bytes += (double) num_ions * num_induced_charges * sizeof(double);    // G1qw_real
  bytes += nmax * sizeof(int);                                          // induced_charge_idx
  bytes += nmax * sizeof(int);                                          // ion_idx
  bytes += num_induced_charges * sizeof(double);                        // induced_charges
  return bytes;
}

/* ---------------------------------------------------------------------- */

void FixPolarizeFunctional::calculate_Rww_cutoff()
{
  int *mask = atom->mask;
  tagint *tag = atom->tag;
  double **x = atom->x;
  double *area = atom->area;
  double *curvature = atom->curvature;
  double **norm = atom->mu;
  double *ed = atom->ed;
  double *em = atom->em;

  // invoke full neighbor list

  int inum, jnum, *ilist, *jlist, *numneigh, **firstneigh;
  inum = list->inum;
  ilist = list->ilist;
  numneigh = list->numneigh;
  firstneigh = list->firstneigh;

  // calculate G1ww, gradG1ww, ndotG1ww
  // fill up buffer1 with local G1ww and buffer2 with local ndotGww
  // seperate into two loops to let the compiler optimize/or later vectorization

  for (int i = 0; i < num_induced_charges; i++)
    for (int j = 0; j < num_induced_charges; j++) buffer1[i][j] = 0;

  for (int i = 0; i < num_induced_charges; i++)
    for (int j = 0; j < num_induced_charges; j++) buffer2[i][j] = 0;

  for (int ii = 0; ii < inum; ii++) {
    int i = ilist[ii];
    if (mask[i] & groupbit) {
      // interface particles
      int mi = induced_charge_idx[i];
      double xtmp = x[i][0];
      double ytmp = x[i][1];
      double ztmp = x[i][2];
      jlist = firstneigh[i];
      jnum = numneigh[i];

      for (int kk = 0; kk < jnum; kk++) {
        int k = jlist[kk] & NEIGHMASK;
        if (mask[k] & groupbit) {

          // interface particles: k can be ghost atoms
          double delx = xtmp - x[k][0];
          double dely = ytmp - x[k][1];
          double delz = ztmp - x[k][2];
          domain->minimum_image(delx, dely, delz);
          int mk = tag2mat[tag[k]];

          // G1ww[mi][mk] = calculate_greens_ewald(delx, dely, delz);
          buffer1[mi][mk] = calculate_greens_ewald(delx, dely, delz);

          // gradG1ww is vector, directly change it in the function
          double gradG1ww[3];
          calculate_grad_greens_ewald(gradG1ww, delx, dely, delz);

          // use mu to store the normal vector of interface vertex
          buffer2[mi][mk] = MathExtra::dot3(norm[i], gradG1ww) / (4 * MY_PI);
        }
      }

      // special treatment for the diagonal terms,
      // even though in the above loop there is mk == mi

      buffer1[mi][mi] = calculate_greens_ewald_self_vertex(area[i]);
      buffer2[mi][mi] = calculate_ndotgreens_ewald_self_vertex(area[i], curvature[i]) / (4 * MY_PI);
    }
  }

  MPI_Allreduce(buffer1[0], G1ww[0], num_induced_charges * num_induced_charges, MPI_DOUBLE, MPI_SUM,
                world);
  MPI_Allreduce(buffer2[0], ndotGww[0], num_induced_charges * num_induced_charges, MPI_DOUBLE,
                MPI_SUM, world);

  // calculate G2ww
  // fill up buffer1 with local G2ww

  for (int i = 0; i < num_induced_charges; i++)
    for (int j = 0; j < num_induced_charges; j++) buffer1[i][j] = 0;

  for (int ii = 0; ii < inum; ii++) {
    int i = ilist[ii];
    if (mask[i] & groupbit) {
      // interface particles
      int mi = induced_charge_idx[i];
      jlist = firstneigh[i];
      jnum = numneigh[i];

      for (int kk = 0; kk < jnum; kk++) {
        int k = jlist[kk] & NEIGHMASK;

        if (mask[k] & groupbit) {
          // interface particles: k can be ghost atoms
          int mk = tag2mat[tag[k]];
          double temp = 0;
          for (int ll = 0; ll < jnum; ll++) {
            int l = jlist[ll] & NEIGHMASK;
            if (mask[l] & groupbit) {
              // interface particles: l can be ghost atoms
              int ml = tag2mat[tag[l]];
              temp += G1ww[mi][ml] * ndotGww[ml][mk] * area[l] * ed[l];
            }
          }
          //G2ww[mi][mk] = temp;
          buffer1[mi][mk] = temp;
        }
      }

      double temp = 0;
      for (int kk = 0; kk < jnum; kk++) {
        int k = jlist[kk] & NEIGHMASK;
        if (mask[k] & groupbit) {
          // interface particles: k can be ghost atoms
          int mk = tag2mat[tag[k]];
          temp += G1ww[mi][mk] * ndotGww[mk][mi] * area[k] * ed[k];
        }
      }
      //G2ww[mi][mi] = temp;
      buffer1[mi][mi] = temp;
    }
  }

  MPI_Allreduce(buffer1[0], G2ww[0], num_induced_charges * num_induced_charges, MPI_DOUBLE, MPI_SUM,
                world);

  // calculate G3ww and Rww
  // G3ww is implemented as in _exact(), but can be optionally excluded
  // due to its minor contribution
  // fill up buffer1 with local G3ww

  for (int i = 0; i < num_induced_charges; i++)
    for (int j = 0; j < num_induced_charges; j++) buffer1[i][j] = 0;

  for (int ii = 0; ii < inum; ii++) {
    int i = ilist[ii];
    if (mask[i] & groupbit) {
      // interface particles
      int mi = induced_charge_idx[i];
      jlist = firstneigh[i];
      jnum = numneigh[i];

      for (int kk = 0; kk < jnum; kk++) {
        int k = jlist[kk] & NEIGHMASK;

        if (mask[k] & groupbit) {

          // interface particles: k can be ghost atoms

          int mk = tag2mat[tag[k]];

          double a1 = em[i] * (em[k] - 1.0);
          double a2 = 1.0 - em[i] - em[k];

          // The first term (w/ G1ww) contributes the most to Rww
          // the second term (w/ G2ww) includes certain correction

          //Rww[mi][mk] = a1 * G1ww[mi][mk] + a2 * G2ww[mi][mk];
          buffer1[mi][mk] = a1 * G1ww[mi][mk] + a2 * G2ww[mi][mk];

          if (includingG3ww) {
            double temp = 0;
            for (int ll = 0; ll < jnum; ll++) {
              int l = jlist[ll] & NEIGHMASK;
              if (mask[l] & groupbit) {
                // interface particles: l can be ghost atoms
                int ml = tag2mat[tag[l]];
                temp += (ndotGww[ml][mi]) * G2ww[ml][mk] * area[l] * ed[l];
              }
            }
            G3ww[mi][mk] = temp;
            //Rww[mi][mk] += G3ww[mi][mk];
            buffer1[mi][mk] += G3ww[mi][mk];
          }
        }
      }

      if (includingG3ww) {
        double temp = 0;
        for (int ll = 0; ll < jnum; ll++) {
          int l = jlist[ll] & NEIGHMASK;
          if (mask[l] & groupbit) {
            // interface particles: l can be ghost atoms
            int ml = tag2mat[tag[l]];
            temp += (ndotGww[ml][mi]) * G2ww[ml][mi] * area[l] * ed[l];
          }
        }
        G3ww[mi][mi] = temp;
        //Rww[mi][mi] += G3ww[mi][mi];
        buffer1[mi][mi] += G3ww[mi][mi];
      }

      // including the diagonal term
      double a1 = em[i] * (em[i] - 1.0);
      double a2 = 1.0 - em[i] - em[i];

      // The first term (w/ G1ww) contributes the most to Rww
      // the second term (w/ G2ww) includes certain correction
      //Rww[mi][mi] = a1 * G1ww[mi][mi] + a2 * G2ww[mi][mi];

      buffer1[mi][mi] = a1 * G1ww[mi][mi] + a2 * G2ww[mi][mi];
    }
  }

  MPI_Allreduce(buffer1[0], Rww[0], num_induced_charges * num_induced_charges, MPI_DOUBLE, MPI_SUM,
                world);
}

/* ---------------------------------------------------------------------- */

void FixPolarizeFunctional::calculate_qiRqw_cutoff()
{
  int ii, i, k, kk, jnum;
  double xtmp, ytmp, ztmp, delx, dely, delz, r;
  int *mask = atom->mask;
  tagint *tag = atom->tag;
  double **x = atom->x;
  double *q = atom->q_scaled;
  double *epsilon = atom->epsilon;
  double *area = atom->area;
  double **norm = atom->mu;
  double *ed = atom->ed;
  double *em = atom->em;

  // invoke full neighbor list

  int inum, *ilist, *jlist, *numneigh, **firstneigh;
  inum = list->inum;
  ilist = list->ilist;
  numneigh = list->numneigh;
  firstneigh = list->firstneigh;

  // calculate G1qw_real
  // fill up buffer1 with local G1qw_real

  for (int i = 0; i < num_ions; i++)
    for (int j = 0; j < num_induced_charges; j++) buffer1[i][j] = 0;

  for (ii = 0; ii < inum; ii++) {
    i = ilist[ii];
    if (!(mask[i] & groupbit)) {
      // ion particles
      int mi = ion_idx[i];
      xtmp = x[i][0];
      ytmp = x[i][1];
      ztmp = x[i][2];
      jlist = firstneigh[i];
      jnum = numneigh[i];

      for (kk = 0; kk < jnum; kk++) {
        k = jlist[kk] & NEIGHMASK;
        if (mask[k] & groupbit) {
          // interface particles: k can be ghost atoms
          delx = xtmp - x[k][0];
          dely = ytmp - x[k][1];
          delz = ztmp - x[k][2];
          domain->minimum_image(delx, dely, delz);
          r = sqrt(delx * delx + dely * dely + delz * delz);

          int mk = tag2mat[tag[k]];
          //G1qw_real[mi][mk] = greens_real(r);
          buffer1[mi][mk] = greens_real(r);
        }
      }
    }
  }

  MPI_Allreduce(&buffer1[0][0], &G1qw_real[0][0], num_ions * num_induced_charges, MPI_DOUBLE,
                MPI_SUM, world);

  // the following loop need the above results: gradG1wq_real
  // calculate sum1G1qw_epsilon and sum2ndotGwq_epsilon
  // fill up rhs1 with local sum1G1qw_epsilon and rhs2 with local sum2ndotGwq_epsilon

  memset(rhs1, 0, num_induced_charges * sizeof(double));
  memset(rhs2, 0, num_induced_charges * sizeof(double));

  for (kk = 0; kk < inum; kk++) {
    k = ilist[kk];    // k is local index
    if (mask[k] & groupbit) {
      // interface particles
      int mk = induced_charge_idx[k];
      xtmp = x[k][0];
      ytmp = x[k][1];
      ztmp = x[k][2];
      jlist = firstneigh[k];
      jnum = numneigh[k];

      double tempndotG[3] = {0.0, 0.0, 0.0};
      double temp_sum1 = 0;
      for (ii = 0; ii < jnum; ii++) {
        i = jlist[ii] & NEIGHMASK;
        if (!(mask[i] & groupbit)) {
          // ions particles: i can be ghost atoms
          delx = x[i][0] - xtmp;
          dely = x[i][1] - ytmp;
          delz = x[i][2] - ztmp;
          domain->minimum_image(delx, dely, delz);

          int mi = tag2mat_ions[tag[i]];    //ion_idx[i];

          //calculate_grad_greens_real(gradG1wq_real[mk][mi], delx, dely, delz);
          double gradG1wq[3];
          calculate_grad_greens_real(gradG1wq, delx, dely, delz);
          MathExtra::scale3(-1.0, gradG1wq);

          tempndotG[0] += gradG1wq[0] * (q[i] / epsilon[i]);
          tempndotG[1] += gradG1wq[1] * (q[i] / epsilon[i]);
          tempndotG[2] += gradG1wq[2] * (q[i] / epsilon[i]);
          temp_sum1 += G1qw_real[mi][mk] * q[i] / epsilon[i];
        }
      }

      //sum1G1qw_epsilon[mk] = temp_sum1;// + ewaldDielectric->sum1G1qw_k_epsilon[mk];
      rhs1[mk] = temp_sum1;

      //sum2ndotGwq_epsilon[mk] = MathExtra::dot3(norm[k], tempndotG);
      rhs2[mk] = MathExtra::dot3(norm[k], tempndotG);
    }
  }

  MPI_Allreduce(rhs1, sum1G1qw_epsilon, num_induced_charges, MPI_DOUBLE, MPI_SUM, world);
  MPI_Allreduce(rhs2, sum2ndotGwq_epsilon, num_induced_charges, MPI_DOUBLE, MPI_SUM, world);

  // calculate G2, gradient G2
  // sum2G2wq and sum1G2qw

  //  for (int i = 0; i < num_induced_charges; i++) rhs1[i] = rhs2[i] = 0;
  memset(rhs1, 0, num_induced_charges * sizeof(double));
  memset(rhs2, 0, num_induced_charges * sizeof(double));

  for (kk = 0; kk < inum; kk++) {
    k = ilist[kk];    // k is local index
    if (mask[k] & groupbit) {
      // interface particles
      int mk = induced_charge_idx[k];
      jlist = firstneigh[k];
      jnum = numneigh[k];

      double tempwq = 0;
      double temp = 0;
      for (ii = 0; ii < jnum; ii++) {
        i = jlist[ii] & NEIGHMASK;
        if (mask[i] & groupbit) {
          // interface particles: i can be ghost atoms
          int mi = tag2mat[tag[i]];
          tempwq += G1ww[mk][mi] * (sum2ndotGwq_epsilon[mi]) * area[i] * ed[i];
          temp += sum1G1qw_epsilon[mi] * (ndotGww[mi][mk]) * area[i] * ed[i];
        }
      }

      // add the corresponding self terms
      tempwq += G1ww[mk][mk] * (sum2ndotGwq_epsilon[mk]) * area[k] * ed[k];
      temp += sum1G1qw_epsilon[mk] * (ndotGww[mk][mk]) * area[k] * ed[k];

      //sum2G2wq[mk] = tempwq;
      rhs1[mk] = tempwq;
      //sum1G2qw[mk] = temp;
      rhs2[mk] = temp;
    }
  }

  MPI_Allreduce(rhs1, sum2G2wq, num_induced_charges, MPI_DOUBLE, MPI_SUM, world);
  MPI_Allreduce(rhs2, sum1G2qw, num_induced_charges, MPI_DOUBLE, MPI_SUM, world);

  // calculate G3, gradient G3
  // fill up rhs1 with local qiRqwVector

  memset(rhs1, 0, num_induced_charges * sizeof(double));

  for (kk = 0; kk < inum; kk++) {
    k = ilist[kk];    // k is local index
    if (mask[k] & groupbit) {
      // interface particles
      int mk = induced_charge_idx[k];
      jlist = firstneigh[k];
      jnum = numneigh[k];

      double sum1G3qw = 0;
      double qiRwwVectorTemp1 = 0;
      for (ii = 0; ii < jnum; ii++) {
        i = jlist[ii] & NEIGHMASK;
        if (mask[i] & groupbit) {
          // interface particles: i can be ghost atoms
          int mi = tag2mat[tag[i]];
          sum1G3qw += sum2ndotGwq_epsilon[mi] * G2ww[mi][mk] * area[i] * ed[i];
        } else {
          // ions particles: i can be ghost atoms
          int mi = tag2mat_ions[tag[i]];    //ion_idx[i];
          qiRwwVectorTemp1 += q[i] * (1.0 - em[k] / epsilon[i]) * G1qw_real[mi][mk];
        }
      }

      // add the diagonal term

      sum1G3qw += sum2ndotGwq_epsilon[mk] * G2ww[mk][mk] * area[k] * ed[k];

      // qiRwwVectorTemp2 is a significant contribution, of which sum2G2wq is significant
      double qiRwwVectorTemp2 = (1.0 - 2.0 * em[k]) * sum2G2wq[mk] + sum1G2qw[mk] + 2.0 * sum1G3qw;

      // qiRqwVector[mk] = qiRwwVectorTemp1 + qiRwwVectorTemp2;
      rhs1[mk] = qiRwwVectorTemp1 + qiRwwVectorTemp2;
    }
  }

  MPI_Allreduce(rhs1, qiRqwVector, num_induced_charges, MPI_DOUBLE, MPI_SUM, world);

#ifdef _POLARIZE_DEBUG
  if (comm->me == 0) {
    FILE *fp = fopen("qRqw-functional.txt", "w");
    for (int i = 0; i < num_induced_charges; i++) fprintf(fp, "%d %g\n", i, qiRqwVector[i]);
    fclose(fp);
  }
#endif
}

/* ----------------------------------------------------------------------
   set dielectric params for the atom in the group
------------------------------------------------------------------------- */

void FixPolarizeFunctional::set_dielectric_params(double ediff, double emean, double epsiloni,
                                                  double areai, int set_charge, double qvalue)
{
  double *area = atom->area;
  double *ed = atom->ed;
  double *em = atom->em;
  double *q = atom->q;
  double *epsilon = atom->epsilon;
  int *mask = atom->mask;
  int nlocal = atom->nlocal;

  for (int i = 0; i < nlocal; i++) {
    if (mask[i] & groupbit) {
      ed[i] = ediff;
      em[i] = emean;
      if (areai > 0) area[i] = areai;
      if (epsiloni > 0) epsilon[i] = epsiloni;
      if (set_charge) q[i] = qvalue;
    }
  }
}

/* ----------------------------------------------------------------------
   real Green's function
------------------------------------------------------------------------ */

double FixPolarizeFunctional::greens_real(double r)
{
  return erfc(g_ewald * r) / r;
}

/* ---------------------------------------------------------------------- */

double FixPolarizeFunctional::grad_greens_real_factor(double r)
{
  double alpharij = g_ewald * r;
  double factor = erfc(alpharij) + 2.0 * alpharij / MY_PIS * exp(-(alpharij * alpharij));
  double r3 = r * r * r;
  return (factor * (-1.0 / r3));
}

/* ---------------------------------------------------------------------- */

void FixPolarizeFunctional::calculate_grad_greens_real(double *vec, double dx, double dy, double dz)
{
  double r = sqrt(dx * dx + dy * dy + dz * dz);
  double real = grad_greens_real_factor(r);
  vec[0] = real * dx;
  vec[1] = real * dy;
  vec[2] = real * dz;
}

/* ---------------------------------------------------------------------- */

double FixPolarizeFunctional::calculate_greens_ewald(double dx, double dy, double dz)
{
  // excluding the reciprocal term
  double r = sqrt(dx * dx + dy * dy + dz * dz);
  return greens_real(r);
}

/* ---------------------------------------------------------------------- */

void FixPolarizeFunctional::calculate_grad_greens_ewald(double *vec, double dx, double dy,
                                                        double dz)
{
  // real part of grad greens, excluding the reciprocal term
  calculate_grad_greens_real(vec, dx, dy, dz);
}

/* ---------------------------------------------------------------------- */

void FixPolarizeFunctional::calculate_matrix_multiply_vector(double **matrix, double *in_vec,
                                                             double *out_vec, int M)
{
  for (int k = 0; k < M; ++k) {
    double temp = 0.0;
    for (int l = 0; l < M; ++l) { temp += matrix[k][l] * in_vec[l]; }
    out_vec[k] = temp;
  }
}

/* ---------------------------------------------------------------------- */

double FixPolarizeFunctional::calculate_greens_ewald_self_vertex(double area)
{
  // excluding the reciprocal term
  double corr = 2.0 * MY_PIS / sqrt(area);
  double self_energy = -2.0 * g_ewald / MY_PIS;
  return corr + self_energy;
}

/* ---------------------------------------------------------------------- */

double FixPolarizeFunctional::calculate_ndotgreens_ewald_self_vertex(double area, double curvature)
{
  // this term is important, cannot be set to zero
  // curvature = 1 / R, minus if norm is inverse of R to center.

  return curvature * MY_PIS / sqrt(area);
}

/* ----------------------------------------------------------------------
  compute the inner product between two vectors x and y: x^t * y
    where ^t is the transpose operator
-- ---------------------------------------------------------------------- */

double FixPolarizeFunctional::inner_product(double *x, double *y, int N)
{
  double t = 0;
  for (int i = 0; i < N; i++) t += x[i] * y[i];
  return t;
}

/* ---------------------------------------------------------------------- */

void FixPolarizeFunctional::cg_solver(double **A, double *b, double *x, int N)
{
  calculate_matrix_multiply_vector(A, x, cg_p, N);
  for (int i = 0; i < N; i++) {
    cg_r[i] = b[i] - cg_p[i];
    cg_p[i] = cg_r[i];
  }
  double rsq = inner_product(cg_r, cg_r, N);

  // maximum number of iterations do not exceed N
  for (int k = 0; k < N; k++) {

    // Ap = A * p
    calculate_matrix_multiply_vector(A, cg_p, cg_Ap, N);

    // pAp = p^t * Ap
    double pAp = inner_product(cg_p, cg_Ap, N);

    // alpha = r^t * r / pAp
    double alpha = rsq / pAp;

    // x = x + alpha * p
    // r = r - alpha * Ap
    for (int i = 0; i < N; i++) {
      x[i] = x[i] + alpha * cg_p[i];
      cg_r[i] = cg_r[i] - alpha * cg_Ap[i];
    }

    double rsq_new = inner_product(cg_r, cg_r, N);
    if (rsq_new < tolerance) break;

    // beta = rsq_new / rsq
    double beta = rsq_new / rsq;
    for (int i = 0; i < N; i++) cg_p[i] = cg_r[i] + beta * cg_p[i];
    rsq = rsq_new;
  }
}
