#!/usr/bin/env python3
# ----------------------------------------------------------------------
#   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.
# -------------------------------------------------------------------------

from tabulate import PairTabulate
import sys
import argparse
import numpy as np
from scipy.signal import savgol_filter
from scipy.optimize import curve_fit

"""
   This script gives an example on how to make tabulated forces from radial
   distribution function using tabulate.py.
   Required: python3, numpy, scipy.
   BI stands for Boltzmann Inversion.
   WARNING: Using BI does not garranty an out of the box working potential for
            your simulation. Check the relevamt literature.
"""
###############################################################################


class BI(PairTabulate):
    def __init__(self, units=None, comment=None):
        super(PairTabulate, self).__init__("pair", self.energy, units, comment)
        self.parser.add_argument(
            "--eshift",
            "-e",
            dest="eshift",
            default=False,
            action="store_true",
            help="Shift potential energy to be zero at outer cutoff",
        )
        self.parser.add_argument("--units", required=True, help="LAMMPS units to use")
        self.parser.add_argument("--rdffile", required=True, help="Rdf file to be read")
        self.parser.add_argument("--temperature", required=True, type=float,
                                 help="Temperature for Boltzman Inversion.")
        try:
            self.args = self.parser.parse_args()
        except argparse.ArgumentError:
            sys.exit()

        if self.args.temperature > 0:
            T = self.args.temperature
        else:
            sys.exit("Invalid temperature provided.")

        kb = 1.0
        # Add more kb units if you need
        self.units = self.args.units
        if self.units == "lj":
            kb = 1.0           # reduced
        elif self.units == "si":
            kb = 1.380649e-23  # J/K
        elif self.units == "metal":
            kb = 8.617333e-5   # eV/K
        elif self.units == "real":
            kb = 1.987204e-3   # kcal/mol/K
        else:
            sys.exit("Unsupported units setting " + self.units)
        self.kbT = kb * T
        self.r, self.e = self.read_rdf(self.args.rdffile)

    # This function assumes LAMMPS format for rdf with a single entry
    def read_rdf(self, rdffile):
        data = np.loadtxt(rdffile, skiprows=4)
        r = data[:, 1]
        g = data[:, 2]

        # savgol_filter is an example of smoothing.
        # Other filters/functions can be used.
        g = savgol_filter(g, 9, 5)
        return self.inversion(r, g)

    def inversion(self, r, g):
        e = -self.kbT * np.log(np.clip(g,1.0e-100,1.0e100))
        e = self.complete_exponential(r, e)
        return r, e,

    def complete_exponential(self, r, e):
        r_temp = r[e != np.inf]
        e_temp = e[e != np.inf]

        # Optimising the parameter for a function for derivation
        # to be continuous.
        # Here a gaussian function, can be anything relevant defined in func.
        popt, pcov = curve_fit(self.func, r_temp[:2], e_temp[:2])
        for i, _ in enumerate(e):
            if e[i] == np.inf:
                e[i] = self.func(r[i], *popt)
        return e

    def func(self, x, K, s):
        return K * np.exp(-0.5 * (x / s) ** 2) / (s * np.sqrt(2 * np.pi))

    def energy(self, x):
        e = self.e
        r = self.r
        # Force estimation at minimum distance.
        # Should not be that useful
        f0 = (e[1] - e[0]) / (r[1] - r[0])

        minr = min(r)
        maxr = max(r)
        # Note that you might want OOB to return an error.
        if x >= maxr:
            return 0
        if x < minr:
            dx = minr - x
            return -f0 * dx
        else:
            # Linear interpolation between points.
            for i, ri in enumerate(r):
                if r[i] < x:
                    r1, e1 = r[i], e[i]
                    r2, e2 = r[i + 1], e[i + 1]
            dr12 = r2 - r1
            dr = x - r1
            de = (e2 - e1) / (r2 - r1)
            return e1 + (de * dr / dr12)


###############################################################################


if __name__ == "__main__":
    ptable = BI()
    ptable.run("BI")
