/* +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
   Copyright (c) 2016-2020 The plumed team
   (see the PEOPLE file at the root of the distribution for a list of names)

   See http://www.plumed.org for more information.

   This file is part of plumed, version 2.

   plumed is free software: you can redistribute it and/or modify
   it under the terms of the GNU Lesser General Public License as published by
   the Free Software Foundation, either version 3 of the License, or
   (at your option) any later version.

   plumed is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU Lesser General Public License for more details.

   You should have received a copy of the GNU Lesser General Public License
   along with plumed.  If not, see <http://www.gnu.org/licenses/>.
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ */
#include "core/ActionRegister.h"
#include "ContourFindingObject.h"
#include "gridtools/ActionWithGrid.h"
#include "gridtools/EvaluateGridFunction.h"
#include "core/ParallelTaskManager.h"
#include "tools/Random.h"

//+PLUMEDOC GRIDANALYSIS FIND_SPHERICAL_CONTOUR
/*
Find an isocontour in a three dimensional grid by searching over a Fibonacci sphere.

As discussed in the documentation for the [gridtools](module_gridtools.md), PLUMED contains a number of tools that allow you to calculate
a function on a grid.  The function on this grid might be a [HISTOGRAM](HISTOGRAM.md)  or it might be one of the phase fields that are
discussed [here](module_contour.md).  If this function has one or two input
arguments it is relatively straightforward to plot the function.  If by contrast the data has a three dimensions it can be
difficult to visualize.

This action provides one tool for visualizing these functions.  It can be used to search for a set of points on a contour
where the function takes a particular value.  In other words, for the function $f(x,y,z)$ this action would find a set
of points $\{x_c,y_c,z_c\}$ that have:

$$
f(x_c,y_c,z_c) - c = 0
$$

where $c$ is some constant value that is specified by the user.  The points on this contour are find by searching along a
set of equally spaced radii of a sphere that centered at on particular, user-specified atom or virtual atom.  To ensure that
these search radii are equally spaced on the surface of the sphere the search directions are generated by using a Fibonacci
spiral projected on a sphere.  In other words, the search directions are given by:

$$
\mathbf{r}_i = \left(
\begin{matrix}
\sqrt{1 - y^2} \cos(\phi) \\
\frac{2i}{n} - 1 + \frac{1}{n}  \\
\sqrt{1 - y^2} \sin(\phi)
\end{matrix}
\right)
$$

where $y$ is the second component of the vector defined above, $n$ is the number of directions to look in and $\phi$ is

$$
\phi = \mod(i + R, n) \pi ( 3 - \sqrt{5} )
$$

where $R$ is a random variable between 0 and $n-1$ that is generated during the read in of the input file and that is fixed during
the whole calculation.

It is important to note that this action can only be used to detect contours in three dimensional functions.  In addition, this action will fail to
find the full set of contour  points if the contour does not have the same topology as a sphere.  If you are uncertain that the isocontours in your
function have a spherical topology you should use [FIND_CONTOUR](FIND_CONTOUR.md) instead.

## Examples

The following input demonstrates how this action can be used.  The input here is used to study the shape of a droplet that has been formed during the
condensation of Lennard Jones from the vapor.  The input below achieves this by calculating the coordination numbers, $c_i$, of all the atoms within the gas.
Obviously, those atoms within the droplet will have a large value for the coordination number while the isolated atoms in the gas will have a low value.

We can detect the sizes of the droplets by constructing a matrix whose $i,j$ element tells us whether atom $i$ and atom $j$ are within 6 nm of each other and
both have coordination numbers that are greater that two.  The atoms within the various droplets within the system can then be found by performing a
[DFSCLUSTERING](DFSCLUSTERING.md) on this matrix to detect the connected components.  We can take the largest of these connected components and find the center of the droplet
by exploiting the functionality within [CENTER](CENTER.md). We can then construct a phase field based on the positions of the atoms in the largest
cluster and the values of the coordination numbers of these atoms as follows:

$$
\rho'(x,y,z) = \frac{ \sum_{i=1}^N f(s_i) K\left(\frac{x-x_i}{\lambda}, \frac{y-y_i}{\lambda}, \frac{z-z_i}{\lambda}\right) }{ \sum_{i=1}^N K\left(\frac{x-x_i}{\lambda}, \frac{y-y_i}{\lambda}, \frac{z-z_i}{\lambda}\right) }
$$

where $f$ is a switching function.  The final line in the input then finds the a set of points on the dividing surface that separates
the droplet from the surrounding gas.  The value of the phase field on this isocontour is equal to 0.75.

```plumed
# Calculate coordination numbers
c1: COORDINATIONNUMBER SPECIES=1-512 SWITCH={EXP D_0=4.0 R_0=0.5 D_MAX=6.0}
# Select coordination numbers that are more than 2.0
cf: MORE_THAN ARG=c1 SWITCH={RATIONAL D_0=2.0 R_0=0.1}
# Build a contact matrix
c1_mat2: CONTACT_MATRIX GROUP=1-512 SWITCH={EXP D_0=4.0 R_0=0.5 D_MAX=6.0}
dp_mat: OUTER_PRODUCT ARG=cf,cf
# Build the final matrix
mat: CUSTOM ARG=c1_mat2,dp_mat FUNC=x*y PERIODIC=NO
# Find largest cluster
dfs: DFSCLUSTERING ARG=mat
clust1: CLUSTER_WEIGHTS CLUSTERS=dfs CLUSTER=1
# Find center of largest cluster
trans1: CUSTOM ARG=cf,clust1 FUNC=x*x*y PERIODIC=NO
cent: CENTER ATOMS=1-512 WEIGHTS=trans1 PHASES
# Calculate the phase field of the coordination
dens_dist: DISTANCES ORIGIN=cent ATOMS=c1 COMPONENTS
dens_numer: KDE ...
   VOLUMES=trans1 ARG=dens_dist.x,dens_dist.y,dens_dist.z
   GRID_BIN=30,30,30 BANDWIDTH=2.0,2.0,2.0
...
dens_denom: KDE ...
   VOLUMES=clust1 ARG=dens_dist.x,dens_dist.y,dens_dist.z
   GRID_BIN=30,30,30 BANDWIDTH=2.0,2.0,2.0
...
dens: CUSTOM ARG=dens_numer,dens_denom FUNC=x/y PERIODIC=NO
# Find the isocontour around the nucleus
sc: FIND_SPHERICAL_CONTOUR ARG=dens CONTOUR=0.85 INNER_RADIUS=10.0 OUTER_RADIUS=40.0 NPOINTS=100
# And print the grid to a file
DUMPGRID ARG=sc PRINT_XYZ FILE=mysurface.xyz STRIDE=1
```

*/
//+ENDPLUMEDOC

namespace PLMD {
namespace contour {

class FindSphericalContourObject {
public:
  unsigned nbins;
  ContourFindingObject<gridtools::EvaluateGridFunction> cf;
  static void registerKeywords( Keywords& keys ) {
    ContourFindingObject<gridtools::EvaluateGridFunction>::registerKeywords( keys );
    keys.add("compulsory","NBINS","1","the number of discrete sections in which to divide the distance between the inner and outer radius when searching for a contour");
  }
  static void read( FindSphericalContourObject& func, ActionWithArguments* action, function::FunctionOptions& options ) {
    action->parse("NBINS",func.nbins);
    ContourFindingObject<gridtools::EvaluateGridFunction>::read( func.cf, action, options );
  }
};

class FindSphericalContour : public gridtools::ActionWithGrid {
public:
  using input_type = FindSphericalContourObject;
  using PTM = ParallelTaskManager<FindSphericalContour>;
private:
/// The parallel task manager
  PTM taskmanager;
  unsigned npoints;
  double min, max;
  gridtools::GridCoordinatesObject gridcoords;
public:
  static void registerKeywords( Keywords& keys );
  explicit FindSphericalContour(const ActionOptions&ao);
  unsigned getNumberOfDerivatives() override ;
  std::vector<std::string> getGridCoordinateNames() const override ;
  const gridtools::GridCoordinatesObject& getGridCoordinatesObject() const override ;
  void calculate() override ;
  void getInputData( std::vector<double>& inputdata ) const override;
  static void performTask( std::size_t task_index,
                           const FindSphericalContourObject& actiondata,
                           ParallelActionsInput& input,
                           ParallelActionsOutput& output );
};

PLUMED_REGISTER_ACTION(FindSphericalContour,"FIND_SPHERICAL_CONTOUR")

void FindSphericalContour::registerKeywords( Keywords& keys ) {
  ActionWithGrid::registerKeywords( keys );
  keys.addInputKeyword("compulsory","ARG","grid","the labels of the grid in which the contour will be found");
  keys.add("compulsory","NPOINTS","the number of points for which we are looking for the contour");
  keys.add("compulsory","INNER_RADIUS","the minimum radius on which to look for the contour");
  keys.add("compulsory","OUTER_RADIUS","the outer radius on which to look for the contour");
  FindSphericalContourObject::registerKeywords( keys );
  keys.setValueDescription("grid","a grid on a Fibonacci sphere that describes the radial distance from the origin for the points on the Willard-Chandler surface");
  keys.addDOI("10.1063/1.5134461");
  PTM::registerKeywords( keys );
}

FindSphericalContour::FindSphericalContour(const ActionOptions&ao):
  Action(ao),
  ActionWithGrid(ao),
  taskmanager(this) {
  if( getPntrToArgument(0)->getRank()!=3 ) {
    error("input grid must be three dimensional");
  }

  parse("NPOINTS",npoints);
  log.printf("  searching for %u points on dividing surface \n",npoints);
  parse("INNER_RADIUS",min);
  parse("OUTER_RADIUS",max);
  log.printf("  expecting to find dividing surface at radii between %f and %f \n",min,max);
  // Set this here so the same set of grid points are used on every turn
  std::vector<bool> ipbc( 3, false );
  gridcoords.setup( "fibonacci", ipbc, npoints, 0.0 );
  // Now create a value
  std::vector<std::size_t> shape( 3 );
  shape[0]=npoints;
  shape[1]=shape[2]=1;
  addValueWithDerivatives( shape );
  setNotPeriodic();
  function::FunctionOptions options;
  FindSphericalContourObject::read( taskmanager.getActionInput(), this, options );
  log.printf("  looking for contour in windows of length %f \n", (max-min)/taskmanager.getActionInput().nbins);
  log.printf("  calculating dividing surface along which function equals %f \n", taskmanager.getActionInput().cf.contour);
  taskmanager.setupParallelTaskManager( 0, 0 );
}

unsigned FindSphericalContour::getNumberOfDerivatives() {
  return gridcoords.getDimension();
}

std::vector<std::string> FindSphericalContour::getGridCoordinateNames() const {
  gridtools::ActionWithGrid* ag=dynamic_cast<gridtools::ActionWithGrid*>( getPntrToArgument(0)->getPntrToAction() );
  plumed_assert( ag );
  return ag->getGridCoordinateNames();
}

const gridtools::GridCoordinatesObject& FindSphericalContour::getGridCoordinatesObject() const {
  return gridcoords;
}

void FindSphericalContour::calculate() {
  taskmanager.runAllTasks();
}

void FindSphericalContour::getInputData( std::vector<double>& inputdata ) const {
  std::size_t ndata = gridcoords.getNumberOfPoints();
  if( inputdata.size()!=6*ndata ) {
    inputdata.resize( 6*ndata );
  }
  std::vector<double> direction(3);
  for(unsigned i=0; i<ndata; ++i) {
    gridcoords.getGridPointCoordinates( i, direction );
    for(unsigned j=0; j<3; ++j) {
      inputdata[6*i+j] = min*direction[j];
      inputdata[6*i+3+j] = (max-min)*direction[j] / static_cast<double>(taskmanager.getActionInput().nbins);
    }
  }
}

void FindSphericalContour::performTask( std::size_t task_index,
                                        const FindSphericalContourObject& actiondata,
                                        ParallelActionsInput& input,
                                        ParallelActionsOutput& output ) {
  bool found=false;
  View<const double> cp( input.inputdata + 6*task_index, 3 );
  std::vector<double> direction(3), contour_point(3), der(3), tmp(3);
  contour_point[0] = cp[0];
  contour_point[1] = cp[1];
  contour_point[2] = cp[2];
  View<const double> d( input.inputdata + 6*task_index+3, 3);
  direction[0] = d[0];
  direction[1] = d[1];
  direction[2] = d[2];
  for(unsigned k=0; k<actiondata.nbins; ++k) {
    for(unsigned j=0; j<3; ++j) {
      tmp[j] = contour_point[j] + direction[j];
    }
    double val1 = actiondata.cf.getDifferenceFromContour( contour_point, der );
    double val2 = actiondata.cf.getDifferenceFromContour( tmp, der );
    if( val1*val2<0 ) {
      ContourFindingObject<gridtools::EvaluateGridFunction>::findContour( actiondata.cf, direction, contour_point );
      double norm=0;
      for(unsigned j=0; j<3; ++j) {
        norm += contour_point[j]*contour_point[j];
      }
      output.values[0] = sqrt(norm);
      found=true;
      break;
    }
    for(unsigned j=0; j<3; ++j) {
      contour_point[j] = tmp[j];
    }
  }
  if( !found ) {
    plumed_merror("range does not bracket the dividing surface");
  }
}

}
}
