Package metadynminer

Metadynminer is a package designed to help you analyse output HILLS files from PLUMED metadynamics simulations.

It is based on Metadynminer package for R programming language, but it is not just a port from R to Python, as it is updated and improved in many aspects. It supports HILLS files with one, two or three collective variables.

All built-in functions can be easily customized with many parameters. You can learn more about that in the documentation. There are also functions allowing you to enhance your presentation with animations of your 3D FES or remove a CV from existing FES.

Installation:

pip install metadynminer

or

conda install -c jan8be metadynminer

Sample code:

Load your HILLS file:

hillsfile = metadynminer.Hills(name="HILLS")

Compute the free energy surface using the fast Bias Sum Algorithm:

fes = metadynminer.Fes(hillsfile)

Alternativelly, you can use slower, exact algorithm to sum the hills and compute the free energy surface with the option original=True. This algorithm was checked and it gives the same result (to the machine level precision) as the PLUMED sum_hills function (for plumed v2.8.0).

fes2 = metadynminer.Fes(hillsfile, original=True)

Visualize the free energy surface and save the picture to a file:

fes.plot(png_name="fes.png")

Find local minima on the FES, print them and save FES with minima as a picture:

minima = metadynminer.Minima(fes)
print(minima.minima)
minima.plot(png_name="fes.png")

You can also plot free energy profile to see, how the differences between each minima were evolving during the simulation. Convergence in the free energy profile suggests, that the resulting free energy surface converged to correct values.

fep = metadynminer.FEProfile(minima, hillsfile)
fep.plot()
Expand source code
"""
Metadynminer is a package designed to help you analyse output HILLS files from PLUMED metadynamics simulations. 

It is based on Metadynminer package for R programming language, but it is not just a port from R to Python, as it is updated and improved in many aspects. It supports HILLS files with one, two or three collective variables. 

All built-in functions can be easily customized with many parameters. You can learn more about that in the documentation. There are also functions allowing you to enhance your presentation with animations of your 3D FES or remove a CV from existing FES. 
        
Installation:

```bash
pip install metadynminer
```
or
```bash
conda install -c jan8be metadynminer
```

Sample code:

Load your HILLS file: 
```python
hillsfile = metadynminer.Hills(name="HILLS")
```

Compute the free energy surface using the fast Bias Sum Algorithm:
```python
fes = metadynminer.Fes(hillsfile)
```

Alternativelly, you can use slower, exact algorithm to sum the hills and compute the free energy surface 
with the option original=True. This algorithm was checked and it gives the same result 
(to the machine level precision) as the PLUMED sum_hills function (for plumed v2.8.0).
```python
fes2 = metadynminer.Fes(hillsfile, original=True)
```

Visualize the free energy surface and save the picture to a file:
```python
fes.plot(png_name="fes.png")
```

Find local minima on the FES, print them and save FES with minima as a picture:
```python
minima = metadynminer.Minima(fes)
print(minima.minima)
minima.plot(png_name="fes.png")
```

You can also plot free energy profile to see, how the differences between each minima were evolving 
during the simulation. Convergence in the free energy profile suggests, that the resulting free energy surface converged to correct values.
```python
fep = metadynminer.FEProfile(minima, hillsfile)
fep.plot()
```
"""

name = "metadynminer"
__version__ = "0.8.3"
__author__ = 'Jan Beránek'

__pdoc__ = {}

#print(f"""Welcome to Metadynminer version {__version__}. """)

try:
    import numpy as np
except:
    print("Error while loading numpy")
    exit()
try:
    from matplotlib import pyplot as plt
except:
    print("Error while loading matplotlib pyplot")
    exit()
try:
    from matplotlib import colormaps as cm
except:
    print("Error while loading matplotlib colormaps")
    exit()
try:
    import pandas as pd
except:
    print("Error while loading pandas")
    exit()
try:
    import pyvista as pv
except:
    print("Error while loading pyvista")
    exit()
try:
    import imageio
except:
    print("Error while loading imageio")
    exit()
try:
    import os
except:
    print("Error while loading os")
    exit()
try:
    import copy
except:
    print("Error while loading copy")
    exit()



class TU:
    __pdoc__["TU"] = False
    def __init__(self, name="ps"):
        if name == "ps":
            self.name = name
            self.factor = 1.0
        elif name == "fs":
            self.name = name
            self.factor = 1e-3
        elif name == "ns":
            self.name = name
            self.factor = 1e3
        elif name == "us":
            self.name = "\u03bcs"
            self.factor = 1e6
        elif name == "\u03bcs":
            self.name = "\u03bcs"
            self.factor = 1e6
        elif name == "ms":
            self.name = name
            self.factor = 1e9
        elif name == "s":
            self.name = name
            self.factor = 1e12
        else:
            print(f"Error: invalid name of time unit ({name})")
            
    def inps(self, i):
        if type(i) == int:
            return int(i*self.factor)
        else:
            return i*self.factor

    def intu(self, i):
        if type(i) == int:
            return int(i/self.factor)
        else:
            return i/self.factor



class Hills:
    """
    Object of Hills class are created for loading HILLS files, and obtaining the necessary information from them. 

    Hills files are loaded with command:
    ```python
    hillsfile = metadynminer.Hills(name="HILLS")
    ```
    
    optional parameters:
    
    * name (default="HILLS") = string with name of HILLS file
    
    * ignoretime (default=True) = boolean, if set to False, it will save the time in the HILLS file;
                                if set to True, and timestep is not set, 
                                        each time value will be incremented by the same amount as the time of the first step.
                                        
    * timestep = numeric value of the time difference between hills, in picoseconds
    
    * periodic (default=[False, False]) = list of boolean values telling which CV is periodic.
    
    * cv1per, cv2per, cv3per (defaults = [-numpy.pi, numpy.pi]) = List of two numeric values defining the periodicity of given CV. 
                                        Has to be provided for each periodic CV.

    Hills attributes: 

    * Hills.hills = array of values from the HILLS file
    
    * Hills.cvs = number of CVs

    * Hills.cv1_name = name of CV 1

    * Hills.cv1per = list of two boundaries of the perodicity interval for CV 1

    * Hills.dt = time step between hills in ps

    * Hills.sigma1 = widths of hills along the CV 1 axis

    * Hills.cv1 = list of CV 1 values during simulation

    * Hills.biasf = biasfactors

    * and analogous attributes for other CVs
    """
    
    def __init__(self, name="HILLS", encoding="utf8", ignoretime=True, periodic=None, 
                 cv1per=[-np.pi, np.pi],cv2per=[-np.pi, np.pi],cv3per=[-np.pi, np.pi], timestep=None):
        self.read(name, encoding, ignoretime, periodic, 
                 cv1per=cv1per,cv2per=cv2per,cv3per=cv3per, timestep=timestep)
        self.hillsfilename = name
    
    def read(self, name="HILLS", encoding="utf8", ignoretime=True, periodic=None, 
                 cv1per=[-np.pi, np.pi],cv2per=[-np.pi, np.pi],cv3per=[-np.pi, np.pi], timestep=None):
        with open(name, 'r', encoding=encoding) as hillsfile:
            lines = hillsfile.readlines()
        columns = lines[0].split()
        number_of_columns_head = len(columns) - 2
        
        if number_of_columns_head == 5:
            self.cvs = 1
            self.cv1_name = lines[0].split()[3]
            self.cv1per = cv1per

        elif number_of_columns_head == 7:
            self.cvs = 2
            self.cv1_name = lines[0].split()[3]
            self.cv2_name = lines[0].split()[4]
            self.cv1per = cv1per
            self.cv2per = cv2per
        elif number_of_columns_head == 9:
            self.cvs = 3
            self.cv1_name = lines[0].split()[3]
            self.cv2_name = lines[0].split()[4]
            self.cv3_name = lines[0].split()[5]
            
            self.cv1per = cv1per
            self.cv2per = cv2per
            self.cv3per = cv3per
        else:
            print("Error: Unexpected number of columns in provided HILLS file.")
            return None
        
        self.ignoretime = ignoretime
        if not ignoretime:
            if timestep != None:
                dt = timestep
            else:
                for line in range(len(lines)):
                    if lines[line][0] != "#":
                        dt = round(float(lines[line+1].split()[0]),14) - round(float(lines[line].split()[0]),14)
                        break
        else:
            if timestep != None:
                dt = timestep
            else:
                dt = 1.0
        self.dt = dt
        t = 0
        for line in range(len(lines)):
            if lines[line][0] != "#":
                t += 1 
                if t == 1:
                    if self.cvs == 1:
                        self.sigma1 = float(lines[line].split()[2])
                        self.biasf = float(lines[line].split()[4])
                    elif self.cvs == 2:
                        self.sigma1 = float(lines[line].split()[3])
                        self.sigma2 = float(lines[line].split()[4])
                        self.biasf = float(lines[line].split()[6])
                    elif self.cvs == 3:
                        self.sigma1 = float(lines[line].split()[4])
                        self.sigma2 = float(lines[line].split()[5])
                        self.sigma3 = float(lines[line].split()[6])
                        self.biasf = float(lines[line].split()[8])
                    self.hills = [lines[line].split()]
                    if ignoretime:
                        self.hills[t-1][0] = t*dt
                else:
                    if self.cvs == 1 and len(lines[line].split()) == 5:
                        self.hills.append(lines[line].split())
                        if ignoretime:
                            self.hills[t-1][0] = t*dt
                    if self.cvs == 2 and len(lines[line].split()) == 7:
                        self.hills.append(lines[line].split())
                        if ignoretime:
                            self.hills[t-1][0] = t*dt
                    if self.cvs == 3 and len(lines[line].split()) == 9:
                        self.hills.append(lines[line].split())
                        if ignoretime:
                            self.hills[t-1][0] = t*dt
        
        self.hills = np.array(self.hills, dtype=np.double)
                
        if self.cvs == 1:
            self.cv1 = self.hills[:,1]
            self.sigma1 = self.hills[:,2]
            self.heights = self.hills[:,3]
            self.biasf = self.hills[:,4]
        elif self.cvs == 2:
            self.cv1 = self.hills[:,1]
            self.cv2 = self.hills[:,2]
            self.sigma1 = self.hills[:,3]
            self.sigma2 = self.hills[:,4]
            self.heights = self.hills[:,5]
            self.biasf = self.hills[:,6]
        elif self.cvs == 3:
            self.cv1 = self.hills[:,1]
            self.cv2 = self.hills[:,2]
            self.cv3 = self.hills[:,3]
            self.sigma1 = self.hills[:,4]
            self.sigma2 = self.hills[:,5]
            self.sigma3 = self.hills[:,6]
            self.heights = self.hills[:,7]
            self.biasf = self.hills[:,8]
        print(f"Loaded HILLS file named {name}. ")
        
        # detect periodicity
        if periodic == None:
            if self.cvs >= 1:
                if np.max(np.diff(self.cv1)) > 0.8*(np.max(self.cv1)-np.min(self.cv1)):
                    periodic = list([True])
                else:
                    periodic = list([False])
            if self.cvs >= 2:
                if np.max(np.diff(self.cv2)) > 0.8*(np.max(self.cv2)-np.min(self.cv2)):
                    periodic.append(True)
                else:
                    periodic.append(False)
            if self.cvs >= 3:
                if np.max(np.diff(self.cv3)) > 0.8*(np.max(self.cv3)-np.min(self.cv3)):
                    periodic.append(True)
                else:
                    periodic.append(False)
            print(f"Automatically detected which CVs are periodic: {periodic}. ")
            print("This detection can be overriden by specifying a list of boolean values to 'periodic' keyword. ")
        else:
            if self.cvs == 1:
                if len(periodic) != 1 or (type(periodic[0]) != type(True)):
                    print(f"Error: argument 'periodic' has wrong number of parameters({len(periodic)})")
            if self.cvs == 2:
                if len(periodic) != 2 or (type(periodic[0]) != type(True)) or (type(periodic[1]) != type(True)):
                    print(f"Error: argument 'periodic' has wrong number of parameters({len(periodic)})")
            if self.cvs == 3:
                if len(periodic) != 3 or (type(periodic[0]) != type(True)) or (type(periodic[1]) != type(True)) or (type(periodic[2]) != type(True)):
                    print(f"Error: argument 'periodic' has wrong number of parameters({len(periodic)})")
        
        self.periodic = periodic
        # check periodicity

        if self.cvs >= 1:
            if periodic[0] == False and np.max(np.diff(self.cv1))>0.8*(np.max(self.cv1)-np.min(self.cv1)):
                print(f"WARNING: It looks like CV 1 ({self.cv1_name}) is periodic, however you specified that it is not. This may be completely fine, however, if you forgot to specify the periodicity correctly, it can lead to errors later during FES calculation.")
            if periodic[0] == True and np.max(np.diff(self.cv1))<0.2*(np.max(self.cv1)-np.min(self.cv1)):
                print(f"WARNING: It looks like CV 1 ({self.cv1_name}) is not periodic, however you specified that it is. This may be completely fine, however, if you forgot to specify the periodicity correctly, it can lead to errors later during FES calculation.")
                
        if self.cvs >= 2:
            if periodic[1] == False and np.max(np.diff(self.cv2))>0.8*(np.max(self.cv2)-np.min(self.cv2)):
                print(f"WARNING: It looks like CV 2 ({self.cv2_name}) is periodic, however you specified that it is not. This may be completely fine, however, if you forgot to specify the periodicity correctly, it can lead to errors later during FES calculation.")
            if periodic[1] == True and np.max(np.diff(self.cv2))<0.2*(np.max(self.cv2)-np.min(self.cv2)):
                print(f"WARNING: It looks like CV 2 ({self.cv2_name}) is not periodic, however you specified that it is. This may be completely fine, however, if you forgot to specify the periodicity correctly, it can lead to errors later during FES calculation.")
        
        if self.cvs >= 3:
            if periodic[2] == False and np.max(np.diff(self.cv3))>0.8*(np.max(self.cv3)-np.min(self.cv3)):
                print(f"WARNING: It looks like CV 3 ({self.cv3_name}) is periodic, however you specified that it is not. This may be completely fine, however, if you forgot to specify the periodicity correctly, it can lead to errors later during FES calculation.")
            if periodic[2] == True and np.max(np.diff(self.cv3))<0.2*(np.max(self.cv3)-np.min(self.cv3)):
                print(f"WARNING: It looks like CV 3 ({self.cv3_name}) is not periodic, however you specified that it is. This may be completely fine, however, if you forgot to specify the periodicity correctly, it can lead to errors later during FES calculation.")
                
        return self
    
    
    def get_cv1(self):
        return self.cv1
    def get_cv2(self):
        return self.cv2
    def get_cv3(self):
        return self.cv3
    
    def get_cv1per(self):
        return self.cv1per
    def get_cv2per(self):
        return self.cv2per
    def get_cv3per(self):
        return self.cv3per
    
    def get_periodic(self):
        return self.periodic
    
    def get_cv1_name(self):
        return self.cv1_name
    def get_cv2_name(self):
        return self.cv2_name
    def get_cv3_name(self):
        return self.cv3_name
    
    def get_hills(self):
        return self.hills
    
    def get_number_of_cvs(self):
        return self.cvs
    
    def get_sigma1(self):
        return self.sigma1
    def get_sigma2(self):
        return self.sigma2
    def get_sigma3(self):
        return self.sigma3
    
    def get_heights(self):
        return(self.heights)
    
    __pdoc__["Hills.get_cv1"] = False
    __pdoc__["Hills.get_cv2"] = False
    __pdoc__["Hills.get_cv3"] = False
    __pdoc__["Hills.get_cv1per"] = False
    __pdoc__["Hills.get_cv2per"] = False
    __pdoc__["Hills.get_cv3per"] = False
    __pdoc__["Hills.get_cv1_name"] = False
    __pdoc__["Hills.get_cv2_name"] = False
    __pdoc__["Hills.get_cv3_name"] = False
    __pdoc__["Hills.get_periodic"] = False
    __pdoc__["Hills.get_hills"] = False
    __pdoc__["Hills.get_number_of_cvs"] = False
    __pdoc__["Hills.get_sigma1"] = False
    __pdoc__["Hills.get_sigma2"] = False
    __pdoc__["Hills.get_sigma3"] = False
    __pdoc__["Hills.get_heights"] = False
    __pdoc__["Hills.read"] = False

    def plot_heights(self, png_name=None, energy_unit="kJ/mol", xlabel=None, ylabel=None, label_size=12, image_size=None, image_size_unit="in", dpi=100, tu = "ps", time_min=None, time_max=None, xlim=[None, None], ylim=[None, None], title=None, return_fig=False):
        """
        Function used to visualize heights of the hills that were added during the simulation. 
        
        ```python
        hillsfile.plot_heights(png_name="picture.png")
        ```
        
        Parameters:
        
        * png_name = String. If this parameter is supplied, the picture of FES will be saved under this name to the current working directory.
        
        * energy_unit (default="kJ/mol") = String, used in description of the y axis

        * xlim, ylim (default = [None, None] for both) = list of two values specifying the range of x and y axes respectively. 
        None means that Matplotlib will choose appropriate range, so this keyword is useful when you need to overwrite it. 
        
        * xlabel, ylabel = Strings, if provided, they will be used as labels for the graphs
        
        * labelsize (default = 12) = size of text in labels
        
        * image_size (default = [9,6]) = list of the width and height of the picture

        * image_size_unit (default = "in") = Units for width and height of the picture, accepts "in" as inches, "cm", "mm" or "px" as pixels. 

        * dpi (default = 100) = DPI of the picture

        * tu (default = "ps") = string, time unit to be shown on x axis. Also aplies to time_min and time_max, if those are used. 
        Available options: "s", "ms", "us", "ns", "ps", "fs"

        * time_min, time_max = The time range for plot, closed interval, in the time unit specified by "tu"

        * title = optional, string that defines the title of the graph

        * return_fig (default=False) = whether the method should return the Matplotlib.Pyplot.figure object for further use
        """
        tu = TU(tu)
        
        if time_min == time_max == 0:
            print("Error: Values of start and end time are zero.")
            return None
        if time_min != None:
            #time_min = tu.inps(time_min)
            if tu.inps(time_min) < 0:
                print("Warning: Start time is lower than zero, it will be set to zero instead. ")
                time_min = 0
            if tu.inps(time_min) < int(self.hills[0,0]):
                print(f"Warning: Start time {tu.inps(time_min)} ps is lower than the first time from HILLS file {int(self.hills[0,0])} ps, which will be used instead. ")
                time_min = tu.intu(self.hills[0,0])
        else:
            time_min = tu.intu(self.hills[0,0])
        if time_max != None:
            #time_max = tu.inps(time_max)
            if time_max < time_min:
                print("Warning: End time is lower than start time. Values are flipped. ")
                time_value = time_max
                time_max = time_min
                time_min = time_value
            if tu.inps(time_max) > int(self.hills[-1,0]):
                print(f"Warning: End time {tu.inps(time_max)} ps is higher than number of lines in HILLS file {int(self.hills[-1,0])} ps, which will be used instead. ")
                time_max = tu.intu(self.hills[-1,0])
        else:
            time_max = tu.intu(self.hills[-1,0])
        #print(f"Berofe fes: min {time_min}, max {time_max}")
        
        if not self.ignoretime:
            time_max = int(round(((time_max - self.hills[0,0])/self.dt),0)) + 1
            time_min = int(round(((time_min - self.hills[0,0])/self.dt),0)) + 1

        if image_size == None:
            image_size = [9,6]
        
        if image_size_unit == "cm":
            image_size[0] /= 2.54
            image_size[1] /= 2.54
        elif image_size_unit == "mm":
            image_size[0] /= 25.4
            image_size[1] /= 25.4
        elif image_size_unit == "px":
            image_size[0] /= dpi
            image_size[1] /= dpi
        elif image_size_unit != "in":
            print(f"Warning: unknown image_size_unit value: {image_size_unit}. Using inches instead. ")
            
        fig = plt.figure(figsize=(image_size[0],image_size[1]), dpi=dpi)
        #plt.plot(tu.intu(np.array(range(len(self.heights))))[time_min-1:time_max], self.heights[time_min-1:time_max])
        plt.plot(tu.intu(np.arange(int(self.hills[0,0]), self.heights.shape[0]+1)[int(tu.inps(time_min)-1):int(tu.inps(time_max))]),
                 self.heights[int(tu.inps(time_min)-1):int(tu.inps(time_max))])
        if xlabel == None:
            plt.xlabel(f'time ({tu.name})', size=label_size)
        else:
            plt.xlabel(xlabel, size=label_size)
        if ylabel == None:
            plt.ylabel(f'free energy ({energy_unit})', size=label_size)
        else:
            plt.ylabel(ylabel, size=label_size)
        
        if title != None:
            plt.title(title, size=label_size)

        plt.xticks(fontsize=label_size)
        plt.yticks(fontsize=label_size)
        
        ax = plt.gca()
        ax.set_xlim(xlim)
        ax.set_ylim(ylim)
            
        if png_name != None:
            plt.savefig(png_name, bbox_inches = 'tight')
        
        if return_fig:
            return fig

    def plot_CV(self, png_name=None, CV=None, xlabel=None, ylabel=None, label_size=12, image_size=None, image_size_unit="in", dpi=100, tu = "ps", time_min=None, time_max=None, points = True, point_size=1, xlim=[None, None], ylim=[None, None], title=None, return_fig=False):
        """
        Function used to visualize CV values from the simulation. 
        
        ```python
        hillsfile.plot_heights(png_name="picture.png")
        ```
        
        Parameters:
        
        * png_name = String. If this parameter is supplied, the picture of FES will be saved under this name to the current working directory.
        
        * CV = Integer, number of the CV to plot (between 1-3)

        * xlim, ylim (default = [None, None] for both) = list of two values specifying the range of x and y axes respectively. 
        None means that Matplotlib will choose appropriate range, so this keyword is useful when you need to overwrite it. 
        
        * xlabel, ylabel = Strings, if provided, they will be used as labels for the graphs
        
        * labelsize (default = 12) = size of text in labels
        
        * image_size (default = [9,6]) = List of the width and height of the picture

        * image_size_unit (default = "in") = Units for width and height of the picture, accepts "in" as inches, "cm", "mm" or "px" as pixels. 

        * dpi (default = 100) = DPI of the resulting image. 
        
        * tu (default = "ps") = string, time unit to be shown on x axis. Also aplies to time_min and time_max, if those are used. Available options: "s", "ms", "us", "ns", "ps", "fs"

        * time_min, time_max = The time range for plot

        * points (default=True) = Boolean value; if True, plot type will be scatter plot, which is better for periodic CVs; if False, it will be line plot, which is sometimes more suitable for non-periodic CVs. 

        * point_size (default = 1) = The size of dots in the plot

        * title = optional, string that defines the title of the graph

        * return_fig (default=False) = whether the method should return the Matplotlib.Pyplot.figure object for further use
        """
        if CV==None:
            print("Error: CV was not chosen")
            return None
        if CV>self.cvs:
            print(f"Error: CV {CV} is not available. ")
            return None
        if CV==1.0:
            CV=1
        if CV==2.0:
            CV=2
        if CV==3.0:
            CV=3
        if CV!=1 and CV!=2 and CV!=3:
            print(f"Error: supplied value of CV {CV} is not correct value")
            return None
            
        tu = TU(tu)
        
        if time_min == time_max == 0:
            print("Error: Values of start and end time are zero.")
            return None
        if time_min != None:
            #time_min = tu.inps(time_min)
            if tu.inps(time_min) < 0:
                print("Warning: Start time is lower than zero, it will be set to zero instead. ")
                time_min = 0
            if tu.inps(time_min) < int(self.hills[0,0]):
                print(f"Warning: Start time {tu.inps(time_min)} ps is lower than the first time from HILLS file {int(self.hills[0,0])} ps, which will be used instead. ")
                time_min = tu.intu(self.hills[0,0])
        else:
            time_min = tu.intu(self.hills[0,0])
        if time_max != None:
            #time_max = tu.inps(time_max)
            if time_max < time_min:
                print("Warning: End time is lower than start time. Values are flipped. ")
                time_value = time_max
                time_max = time_min
                time_min = time_value
            if tu.inps(time_max) > int(self.hills[-1,0]):
                print(f"Warning: End time {tu.inps(time_max)} ps is higher than number of lines in HILLS file {int(self.hills[-1,0])} ps, which will be used instead. ")
                time_max = tu.intu(self.hills[-1,0])
        else:
            time_max = tu.intu(self.hills[-1,0])
        
        if not self.ignoretime:
            time_max = int(round(((time_max - self.hills[0,0])/self.dt),0)) + 1
            time_min = int(round(((time_min - self.hills[0,0])/self.dt),0)) + 1

        if image_size == None:
            image_size = [9,6]
        
        if image_size_unit == "cm":
            image_size[0] /= 2.54
            image_size[1] /= 2.54
        elif image_size_unit == "mm":
            image_size[0] /= 25.4
            image_size[1] /= 25.4
        elif image_size_unit == "px":
            image_size[0] /= dpi
            image_size[1] /= dpi
        elif image_size_unit != "in":
            print(f"Warning: unknown image_size_unit value: {image_size_unit}. Using inches instead. ")
        
        fig = plt.figure(figsize=(image_size[0],image_size[1]), dpi=dpi)
        if points:
            if CV==1:
                plt.scatter(tu.intu(np.array(range(int(round(tu.inps(time_min),0)),int(round(tu.inps(time_max)+1,0)),int(round(self.dt,0))))), 
                            self.cv1[int(tu.inps(time_min)-1):int(tu.inps(time_max))], s=point_size)
            if CV==2:
                plt.scatter(tu.intu(np.array(range(int(round(tu.inps(time_min),0)),int(round(tu.inps(time_max)+1,0)),int(round(self.dt,0))))), 
                            self.cv2[int(tu.inps(time_min)-1):int(tu.inps(time_max))], s=point_size)
            if CV==3:
                plt.scatter(tu.intu(np.array(range(int(round(tu.inps(time_min),0)),int(round(tu.inps(time_max)+1,0)),int(round(self.dt,0))))), 
                            self.cv3[int(tu.inps(time_min)-1):int(tu.inps(time_max))], s=point_size)
        else:
            if CV==1:
                plt.plot(tu.intu(np.array(range(int(round(tu.inps(time_min),0)),int(round(tu.inps(time_max)+1,0)),int(round(self.dt,0))))), 
                         self.cv1[int(tu.inps(time_min)-1):int(tu.inps(time_max))], lw=point_size)
            if CV==2:
                plt.plot(tu.intu(np.array(range(int(round(tu.inps(time_min),0)),int(round(tu.inps(time_max)+1,0)),int(round(self.dt,0))))), 
                         self.cv2[int(tu.inps(time_min)-1):int(tu.inps(time_max))], lw=point_size)
            if CV==3:
                plt.plot(tu.intu(np.array(range(int(round(tu.inps(time_min),0)),int(round(tu.inps(time_max)+1,0)),int(round(self.dt,0))))), 
                         self.cv3[int(tu.inps(time_min)-1):int(tu.inps(time_max))], lw=point_size)
            
        if xlabel == None:
            plt.xlabel(f'time ({tu.name})', size=label_size)
        else:
            plt.xlabel(xlabel, size=label_size)
        if ylabel == None:
            if CV==1:
                plt.ylabel(f'CV {CV} - {self.cv1_name}', size=label_size)
            if CV==2:
                plt.ylabel(f'CV {CV} - {self.cv2_name}', size=label_size)
            if CV==3:
                plt.ylabel(f'CV {CV} - {self.cv3_name}', size=label_size)
        else:
            plt.ylabel(ylabel, size=label_size)
        
        if title != None:
            plt.title(title, size=label_size)

        plt.xticks(fontsize=label_size)
        plt.yticks(fontsize=label_size)
                
        ax = plt.gca()
        ax.set_xlim(xlim)
        ax.set_ylim(ylim)
            
        if png_name != None:
            plt.savefig(png_name, bbox_inches = 'tight')
        
        if return_fig:
            return fig

class Fes: 
    """
    Object of this class is created to compute the free energy surface corresponding to the provided Hills object. 
    Command:
    ```python
    fes = metadynminer.Fes(hills=hillsfile)
    ```
    parameters:
    
    * hills = Hills object
    
    * resolution (default=256) = should be positive integer, controls the resolution of FES
    
    * original (default=False) = boolean, if False, FES will be calculated using very fast, but not
    'exact' Bias Sum Algorithm
                                        if True, FES will be calculated with slower algorithm, but it will be exactly the same as FES calculated 
                                        with PLUMED sum_hills function
                                        
    * cv1range, cv2range, cv3range = lists of two numbers, defining lower and upper bound of the respective CV (in the units of the CVs)
    
    * time_min, time_max = Limit points for closed interval of times from the HILLS file from which the FES will be constructed. Useful for making animations of flooding of the FES during simulation. The values here should have the same units as those in the HILLS file, especially if ignoretime = False. 

    * tu (default = "ps") = string, time unit for time_min and time_max parameters, if those are used. Available options: "s", "ms", "us", "ns", "ps", "fs"

    * print_output (default = True), tells whether the function should print messages like progress of calculation

    * subtract_min (deault = True), whether the global minimum value should be subtracted from the whole FES, so that global minimum has zero free energy and other states have higher values

    Fes atributes: 

    * Fes.fes = free energy surface
    
    * Fes.res = resolution of the FES

    * Fes.original = whether FES should be calculated by plumed's original precise algorithm

    * Fes.cvs = number of CVs

    * Fes.hills = array with values from the HILLS file

    * Fes.periodic = list of boolean values setting which CVs are periodic

    * Fes.cv1min, Fes.cv1max = minimum and maximum values on the FES, respectively

    * Fes.cv1_fes_range, Fes.cv2_fes_range, Fes.cv3_fes_range = FES range = maximum - minimum values on FES for given CV

    * Fes.cv1_name, Fes.cv2_name, Fes.cv3_name = CV names
    
    """
    
    __pdoc__["Fes.makefes"] = False
    __pdoc__["Fes.makefes2"] = False
    __pdoc__["Fes.set_fes"] = False
    
    def __init__(self, hills=None, resolution=256, original=False, \
                 calculate_new_fes=True, cv1range=None, cv2range=None, cv3range=None, \
                 time_min=None, time_max=None, tu="ps", print_output=True, subtract_min = True):
        self.res = resolution
        self.original = original
        self.cv1range = cv1range
        self.cv2range = cv2range
        self.cv3range = cv3range
        if hills != None:
            self.hills = hills
            self.cvs = hills.get_number_of_cvs()
            self.heights = hills.get_heights()
            self.periodic = hills.get_periodic()
            self.biasf = hills.biasf
            self.ignoretime = hills.ignoretime
            self.dt = hills.dt
            
            if cv1range!=None and len(cv1range) != 2:
                print("Error: You have to specify CV1 range as a list of two values. ")
            if cv2range!=None and len(cv2range) != 2:
                print("Error: You have to specify CV2 range as a list of two values. ")
            if cv3range!=None and len(cv3range) != 2:
                print("Error: You have to specify CV3 range as a list of two values. ")
            
            if self.cvs >= 1:
                self.cv1 = hills.get_cv1()
                self.s1 = hills.get_sigma1()

                self.cv1_name = hills.get_cv1_name()
                self.cv1per = hills.get_cv1per()

                self.fes = np.zeros((resolution))

                if self.periodic[0]:
                    cv1min = self.cv1per[0]
                    cv1max = self.cv1per[1]
                    if cv1range != None:
                        print("Warning: CV1 is specified as periodic, to change it's range you must specify the periodicity to cv1per parameter when loading the HILLS file. The cv1range parameter is ignored. ")
                else:
                    if cv1range == None:
                        cv1min = np.min(self.cv1) - 1e-8
                        cv1max = np.max(self.cv1) + 1e-8
                        cv1min -= (cv1max-cv1min)*0.15          
                        cv1max += (cv1max-cv1min)*0.15
                    else:
                        if cv1range[1] <= cv1range[0]:
                            print(f"Error: Wrong values of cv1range: {cv1range}. ")
                            return None
                        cv1min = cv1range[0]
                        cv1max = cv1range[1]
                self.cv1min = cv1min
                self.cv1max = cv1max
                self.cv1_fes_range = self.cv1max - self.cv1min
                
                
                if not original:
                    if ((np.max(self.s1)/np.min(self.s1))>1.00000001):
                        print("""Error: Bias sum algorithm only works for hills files 
                        in which all hills have the same width. 
                        For this file, you need the slower but exact, algorithm, to use it, 
                        specify the argument 'original=True'.""")
                        return None

            if self.cvs >= 2:
                self.cv2 = hills.get_cv2()
                self.s2 = hills.get_sigma2()
                
                self.cv2_name = hills.get_cv2_name()
                self.cv2per = hills.get_cv2per()

                self.fes = np.zeros((resolution, resolution))

                if self.periodic[1]:
                    cv2min = self.cv2per[0]
                    cv2max = self.cv2per[1]
                    if cv2range != None:
                        print("Warning: CV2 is specified as periodic, to change it's range you must specify the periodicity to cv2per parameter when loading the HILLS file. The cv2range parameter is ignored. ")
                else:
                    if cv2range == None:
                        cv2min = np.min(self.cv2) - 1e-8
                        cv2max = np.max(self.cv2) + 1e-8
                        cv2min -= (cv2max-cv2min)*0.15          
                        cv2max += (cv2max-cv2min)*0.15
                    else:
                        if cv2range[1] <= cv2range[0]:
                            print(f"Error: Wrong values of cv2range: {cv2range}. ")
                            return None
                        cv2min = cv2range[0]
                        cv2max = cv2range[1]
                self.cv2min = cv2min
                self.cv2max = cv2max
                self.cv2_fes_range = self.cv2max - self.cv2min


                
                if not original:
                    if ((np.max(self.s2)/np.min(self.s2))>1.00000001):
                        print("""Error: Bias sum algorithm only works for hills files 
                        in which all hills have the same width. 
                        For this file, you need the slower, but exact, algorithm, 
                        specify the argument 'original=True'.""")
                        return None

            if self.cvs == 3:
                self.cv3 = hills.get_cv3()
                self.s3 = hills.get_sigma3()

                self.cv3_name = hills.get_cv3_name()
                self.cv3per = hills.get_cv3per()

                self.fes = np.zeros((resolution, resolution, resolution))
                
                if self.periodic[2]:
                    cv3min = self.cv3per[0]
                    cv3max = self.cv3per[1]
                    if cv3range != None:
                        print("Warning: CV3 is specified as periodic, to change it's range you must specify the periodicity to cv3per parameter when loading the HILLS file. The cv3range is ignored. ")
                else:
                    if cv3range == None:
                        cv3min = np.min(self.cv3) - 1e-8
                        cv3max = np.max(self.cv3) + 1e-8
                        cv3min -= (cv3max-cv3min)*0.15          
                        cv3max += (cv3max-cv3min)*0.15
                    else:
                        if cv3range[1] <= cv3range[0]:
                            print(f"Error: Wrong values of cv3range: {cv3range}. ")
                            return None
                        cv3min = cv3range[0]
                        cv3max = cv3range[1]
                self.cv3min = cv3min
                self.cv3max = cv3max
                self.cv3_fes_range = self.cv3max - self.cv3min
                
                if not original:
                    if ((np.max(self.s3)/np.min(self.s3))>1.00000001):
                        print("""Error: Bias sum algorithm only works for hills files 
                        in which all hills have the same width of given CV. 
                        For this file, you need the exact algorithm, to do that, 
                        specify the argument 'original=True'.""")
                        return None
                        
            tu = TU(tu)
        
            if time_min == time_max == 0:
                print("Error: Values of start and end time are zero.")
                return None
            if time_min != None:
                #time_min = tu.inps(time_min)
                if time_min < 0:
                    print("Warning: Start time is lower than zero, it will be set to zero instead. ")
                    time_min = 0
                if tu.inps(time_min) < int(hills.hills[0,0]):
                    print(f"Warning: Start time {tu.inps(time_min)} is lower than the first time from HILLS file {int(hills.hills[0,0])}, which will be used instead. ")
                    time_min = tu.intu(int(hills.hills[0,0]))
            else:
                time_min = tu.intu(int(hills.hills[0,0]))
            if time_max != None:
                #time_max = int(tu.inps(time_max))
                if time_max < time_min:
                    print("Warning: End time is lower than start time. Values are flipped. ")
                    time_value = time_max
                    time_max = time_min
                    time_min = time_value
                if tu.inps(time_max) > int(hills.hills[-1,0]):
                    print(f"Warning: End time {tu.inps(time_max)} is higher than number of lines in HILLS file {int(hills.hills[-1,0])}, which will be used instead. ")
                    time_max = tu.intu(int(hills.hills[-1,0]))
            else:
                time_max = tu.intu(int(hills.hills[-1,0]))
            
            if not self.ignoretime:
                time_max = int(round(((time_max - hills.hills[0,0])/self.dt),0)) + 1
                time_min = int(round(((time_min - hills.hills[0,0])/self.dt),0)) + 1
                #print(f"Berofe fes: min {time_min}, max {time_max}")

            if calculate_new_fes:
                if original:
                    self.makefes2(resolution, int(tu.inps(time_min)), int(tu.inps(time_max)), 
                                  print_output=print_output, subtract_min = subtract_min)
                else:
                    self.makefes(resolution, int(tu.inps(time_min)), int(tu.inps(time_max)), 
                                 print_output=print_output, subtract_min = subtract_min)
        
    def makefes(self, resolution, time_min, time_max, print_output=True, subtract_min = True):
        """
        Function used internally for summing hills in Hills object with the fast Bias Sum Algorithm. 
        """
        #self.res = resolution
        #if self.res % 2 == 0:
        #    self.res += 1
        #print(f"min: {time_min}, max: {time_max}")
        
        if self.cvs == 1:
            cv1bin = np.ceil((self.cv1-self.cv1min)*self.res/(self.cv1_fes_range))
            cv1bin = cv1bin.astype(int)
            
            s1res = (self.s1[0]*self.res)/(self.cv1_fes_range)
            
            self.cv1bin = cv1bin
            
            gauss_res = 8*s1res
            gauss_res = int(gauss_res)
            if gauss_res%2 == 0:
                gauss_res += 1
            
            gauss_center = int((gauss_res-1)/2)+1
            gauss = np.zeros((gauss_res))
            for i in range(gauss_res):
                gauss[int(i)] = -np.exp(-((i+1)-gauss_center)**2/(2*s1res**2))
                
            fes = np.zeros((self.res))
            
            for line in range(time_min-1, time_max):
                if print_output and (((line) % 500 == 0) or (line == time_max-1)):
                    print(f"Constructing free energy surface: {((line+1-time_min+1)/(time_max-time_min+1)):.1%} finished", end="\r")
                
                gauss_center_to_end = int((gauss_res-1)/2)
                
                fes_to_edit_cv1 = [cv1bin[line]-1-gauss_center_to_end,
                                   cv1bin[line]-1+gauss_center_to_end]
                fes_crop_cv1 = [max(0,fes_to_edit_cv1[0]),min(self.res-1,fes_to_edit_cv1[1])]
                
                gauss_crop_cv1 = [max(0,gauss_center_to_end-(cv1bin[line]-1)),
                                      gauss_res-1-max(0,(cv1bin[line]-1)+gauss_center_to_end-self.res+1)]
                
                fes[fes_crop_cv1[0]:fes_crop_cv1[1]+1]\
                        += gauss[gauss_crop_cv1[0]:gauss_crop_cv1[1]+1]\
                        * self.heights[line]
                
                if self.periodic[0]:
                    if cv1bin[line] < gauss_center_to_end:
                        fes_crop_cv1_p = [self.res-1+(cv1bin[line]-gauss_center_to_end),self.res-1]
                        gauss_crop_cv1_p = [0,gauss_center_to_end-cv1bin[line]]
                        fes[fes_crop_cv1_p[0]:fes_crop_cv1_p[1]+1]\
                                    += gauss[gauss_crop_cv1_p[0]:gauss_crop_cv1_p[1]+1]\
                                    * self.heights[line]
                    
                    if cv1bin[line] > (self.res-gauss_center_to_end):
                        fes_crop_cv1_p = [0,gauss_center_to_end+cv1bin[line]-self.res-1]
                        gauss_crop_cv1_p = [gauss_res-(gauss_center_to_end+cv1bin[line]-self.res),gauss_res-1]
                        fes[fes_crop_cv1_p[0]:fes_crop_cv1_p[1]+1]\
                                    += gauss[gauss_crop_cv1_p[0]:gauss_crop_cv1_p[1]+1]\
                                    * self.heights[line]
            if print_output:
                print("\n")
            if subtract_min:
                fes = fes-np.min(fes)
            self.fes = np.array(fes)
            
        elif self.cvs == 2:
            cv1bin = np.ceil((self.cv1-self.cv1min)*self.res/(self.cv1_fes_range))
            cv2bin = np.ceil((self.cv2-self.cv2min)*self.res/(self.cv2_fes_range))
                        
            cv1bin = cv1bin.astype(int)
            cv2bin = cv2bin.astype(int)
            
            s1res = (self.s1[0]*self.res)/(self.cv1_fes_range)
            s2res = (self.s2[0]*self.res)/(self.cv2_fes_range)
            
            gauss_res = max(8*s1res, 8*s2res)
            gauss_res = int(gauss_res)
            if gauss_res%2 == 0:
                gauss_res += 1
            
            gauss_center = int((gauss_res-1)/2)+1
            gauss = np.zeros((gauss_res,gauss_res))
            for i in range(gauss_res):
                for j in range(gauss_res):
                    #dp2 = ((i+1)-gauss_center)**2/(2*s1res**2) + ((j+1)-gauss_center)**2/(2*s2res**2)
                    #if dp2 < 6.25:
                    #    gauss[int(i), int(j)] = -np.exp(-dp2) * 1.00193418799744762399 - 0.00193418799744762399
                    gauss[int(i), int(j)] = -np.exp(-(((i+1)-gauss_center)**2/(2*s1res**2) + ((j+1)-gauss_center)**2/(2*s2res**2)))
            
            fes = np.zeros((self.res,self.res))
            for line in range(time_min-1, time_max):
                if print_output and (((line) % 500 == 0) or (line == time_max-1)):
                    print(f"Constructing free energy surface: {((line+1-time_min+1)/(time_max-time_min+1)):.1%} finished.", end="\r")
                
                #fes_center = int((self.res-1)/2)
                gauss_center_to_end = int((gauss_res-1)/2)
                #print(f"\ng_res: {gauss_res}, gauss_center_to_end: {gauss_center_to_end}, cvib:{cv1bin[line]}, cv2b:{cv2bin[line]}")
                
                fes_to_edit_cv1 = [cv1bin[line]-1-gauss_center_to_end,
                                   cv1bin[line]-1+gauss_center_to_end]
                fes_to_edit_cv2 = [cv2bin[line]-1-gauss_center_to_end,
                                   cv2bin[line]-1+gauss_center_to_end]
                fes_crop_cv1 = [max(0,fes_to_edit_cv1[0]),min(self.res-1,fes_to_edit_cv1[1])]
                fes_crop_cv2 = [max(0,fes_to_edit_cv2[0]),min(self.res-1,fes_to_edit_cv2[1])]
                
                gauss_crop_cv1 = [max(0,gauss_center_to_end-(cv1bin[line]-1)),
                                      gauss_res-1-max(0,(cv1bin[line]-1)+gauss_center_to_end-self.res+1)]
                gauss_crop_cv2 = [max(0,gauss_center_to_end-(cv2bin[line]-1)),
                                      gauss_res-1-max(0,(cv2bin[line]-1)+gauss_center_to_end-self.res+1)]
                
                fes[fes_crop_cv1[0]:fes_crop_cv1[1]+1,fes_crop_cv2[0]:fes_crop_cv2[1]+1]\
                        += gauss[gauss_crop_cv1[0]:gauss_crop_cv1[1]+1,gauss_crop_cv2[0]:gauss_crop_cv2[1]+1]\
                        * self.heights[line]
                
                if self.periodic[0]:
                    if cv1bin[line] < gauss_center_to_end:
                        fes_crop_cv1_p = [self.res-1+(cv1bin[line]-gauss_center_to_end),self.res-1]
                        gauss_crop_cv1_p = [0,gauss_center_to_end-cv1bin[line]]
                        fes[fes_crop_cv1_p[0]:fes_crop_cv1_p[1]+1,fes_crop_cv2[0]:fes_crop_cv2[1]+1]\
                                    += gauss[gauss_crop_cv1_p[0]:gauss_crop_cv1_p[1]+1,gauss_crop_cv2[0]:gauss_crop_cv2[1]+1]\
                                    * self.heights[line]
                    
                    if cv1bin[line] > (self.res-gauss_center_to_end):
                        fes_crop_cv1_p = [0,gauss_center_to_end+cv1bin[line]-self.res-1]
                        gauss_crop_cv1_p = [gauss_res-(gauss_center_to_end+cv1bin[line]-self.res),gauss_res-1]
                        fes[fes_crop_cv1_p[0]:fes_crop_cv1_p[1]+1,fes_crop_cv2[0]:fes_crop_cv2[1]+1]\
                                    += gauss[gauss_crop_cv1_p[0]:gauss_crop_cv1_p[1]+1,gauss_crop_cv2[0]:gauss_crop_cv2[1]+1]\
                                    * self.heights[line]
                
                if self.periodic[1]:
                    if cv2bin[line] < gauss_center_to_end:
                        fes_crop_cv2_p = [self.res-1+(cv2bin[line]-gauss_center_to_end),self.res-1]
                        gauss_crop_cv2_p = [0,gauss_center_to_end-cv2bin[line]]
                        fes[fes_crop_cv1[0]:fes_crop_cv1[1]+1,fes_crop_cv2_p[0]:fes_crop_cv2_p[1]+1]\
                                    += gauss[gauss_crop_cv1[0]:gauss_crop_cv1[1]+1,gauss_crop_cv2_p[0]:gauss_crop_cv2_p[1]+1]\
                                    * self.heights[line]
                    
                    if cv2bin[line] > (self.res-gauss_center_to_end):
                        fes_crop_cv2_p = [0,gauss_center_to_end+cv2bin[line]-self.res-1]
                        gauss_crop_cv2_p = [gauss_res-(gauss_center_to_end+cv2bin[line]-self.res),gauss_res-1]
                        fes[fes_crop_cv1[0]:fes_crop_cv1[1]+1,fes_crop_cv2_p[0]:fes_crop_cv2_p[1]+1]\
                                    += gauss[gauss_crop_cv1[0]:gauss_crop_cv1[1]+1,gauss_crop_cv2_p[0]:gauss_crop_cv2_p[1]+1]\
                                    * self.heights[line]
                
                if self.periodic[0] and self.periodic[1]:
                    if ((cv1bin[line] < gauss_center_to_end) or (cv1bin[line] > (self.res-gauss_center_to_end))) \
                            and ((cv2bin[line] < gauss_center_to_end) or (cv2bin[line] > (self.res-gauss_center_to_end))):
                        fes[fes_crop_cv1_p[0]:fes_crop_cv1_p[1]+1,fes_crop_cv2_p[0]:fes_crop_cv2_p[1]+1]\
                                    += gauss[gauss_crop_cv1_p[0]:gauss_crop_cv1_p[1]+1,gauss_crop_cv2_p[0]:gauss_crop_cv2_p[1]+1]\
                                    * self.heights[line]
            if print_output:
                print("\n")
            if subtract_min:
                fes = fes-np.min(fes)
            self.fes = np.array(fes)
        elif self.cvs == 3:
            cv1bin = np.ceil((self.cv1-self.cv1min)*self.res/(self.cv1_fes_range))
            cv2bin = np.ceil((self.cv2-self.cv2min)*self.res/(self.cv2_fes_range))
            cv3bin = np.ceil((self.cv3-self.cv3min)*self.res/(self.cv3_fes_range))
                        
            cv1bin = cv1bin.astype(int)
            cv2bin = cv2bin.astype(int)
            cv3bin = cv3bin.astype(int)
            
            s1res = (self.s1[0]*self.res)/(self.cv1_fes_range)
            s2res = (self.s2[0]*self.res)/(self.cv2_fes_range)
            s3res = (self.s3[0]*self.res)/(self.cv3_fes_range)
            
            gauss_res = max(10*s1res, 10*s2res, 10*s3res)
            gauss_res = int(gauss_res)
            if gauss_res%2 == 0:
                gauss_res += 1
            gauss_center = int((gauss_res-1)/2)+1
            gauss = np.zeros((gauss_res,gauss_res,gauss_res))
            for i in range(gauss_res):
                for j in range(gauss_res):
                    for k in range(gauss_res):
                        #dp2 = ((i+1)-gauss_center)**2/(2*s1res**2) + ((j+1)-gauss_center)**2/(2*s2res**2) + ((k+1)-gauss_center)**2/(2*s3res**2)
                        #if dp2 < 6.25:
                        #    gauss[int(i), int(j), int(k)] = -np.exp(-dp2) * 1.00193418799744762399 - 0.00193418799744762399
                        gauss[int(i), int(j), int(k)] = -np.exp(-(((i+1)-gauss_center)**2/(2*s1res**2) + 
                                                                  ((j+1)-gauss_center)**2/(2*s2res**2) + 
                                                                  ((k+1)-gauss_center)**2/(2*s3res**2)))
            
            fes = np.zeros((self.res, self.res, self.res))
            
            for line in range(time_min-1, time_max):
                if print_output and (((line) % 500 == 0) or (line == time_max-1)):
                    print(f"Constructing free energy surface: {((line+1-time_min+1)/(time_max-time_min+1)):.1%} finished", end="\r")
                
                #fes_center = int((self.res-1)/2)
                gauss_center_to_end = int((gauss_res-1)/2)
                
                fes_to_edit_cv1 = [cv1bin[line]-1-gauss_center_to_end,
                                   cv1bin[line]-1+gauss_center_to_end]
                fes_to_edit_cv2 = [(cv2bin[line]-1)-gauss_center_to_end,
                                   (cv2bin[line]-1)+gauss_center_to_end]
                fes_to_edit_cv3 = [(cv3bin[line]-1)-gauss_center_to_end,
                                   (cv3bin[line]-1)+gauss_center_to_end]
                fes_crop_cv1 = [max(0,fes_to_edit_cv1[0]),min(self.res-1,fes_to_edit_cv1[1])]
                fes_crop_cv2 = [max(0,fes_to_edit_cv2[0]),min(self.res-1,fes_to_edit_cv2[1])]
                fes_crop_cv3 = [max(0,fes_to_edit_cv3[0]),min(self.res-1,fes_to_edit_cv3[1])]
                
                gauss_crop_cv1 = [max(0,gauss_center_to_end-(cv1bin[line]-1)),
                                      gauss_res-1-max(0,(cv1bin[line]-1)+gauss_center_to_end-self.res+1)]
                gauss_crop_cv2 = [max(0,gauss_center_to_end-(cv2bin[line]-1)),
                                      gauss_res-1-max(0,(cv2bin[line]-1)+gauss_center_to_end-self.res+1)]
                gauss_crop_cv3 = [max(0,gauss_center_to_end-(cv3bin[line]-1)),
                                      gauss_res-1-max(0,(cv3bin[line]-1)+gauss_center_to_end-self.res+1)]
                
                fes[fes_crop_cv1[0]:fes_crop_cv1[1]+1,\
                    fes_crop_cv2[0]:fes_crop_cv2[1]+1,\
                    fes_crop_cv3[0]:fes_crop_cv3[1]+1]\
                        += gauss[gauss_crop_cv1[0]:gauss_crop_cv1[1]+1,\
                                 gauss_crop_cv2[0]:gauss_crop_cv2[1]+1,\
                                 gauss_crop_cv3[0]:gauss_crop_cv3[1]+1]\
                        * self.heights[line]
                
                if self.periodic[0]:
                    if cv1bin[line] < gauss_center_to_end:
                        fes_crop_cv1_p = [self.res-1+(cv1bin[line]-gauss_center_to_end),self.res-1]
                        gauss_crop_cv1_p = [0,gauss_center_to_end-cv1bin[line]]
                        fes[fes_crop_cv1_p[0]:fes_crop_cv1_p[1]+1,\
                            fes_crop_cv2[0]:fes_crop_cv2[1]+1,\
                            fes_crop_cv3[0]:fes_crop_cv3[1]+1]\
                                    += gauss[gauss_crop_cv1_p[0]:gauss_crop_cv1_p[1]+1,\
                                             gauss_crop_cv2[0]:gauss_crop_cv2[1]+1,\
                                             gauss_crop_cv3[0]:gauss_crop_cv3[1]+1]\
                                    * self.heights[line]
                    
                    if cv1bin[line] > (self.res-gauss_center_to_end):
                        fes_crop_cv1_p = [0,gauss_center_to_end+cv1bin[line]-self.res-1]
                        gauss_crop_cv1_p = [gauss_res-(gauss_center_to_end+cv1bin[line]-self.res),gauss_res-1]
                        fes[fes_crop_cv1_p[0]:fes_crop_cv1_p[1]+1,\
                            fes_crop_cv2[0]:fes_crop_cv2[1]+1,\
                            fes_crop_cv3[0]:fes_crop_cv3[1]+1]\
                                    += gauss[gauss_crop_cv1_p[0]:gauss_crop_cv1_p[1]+1,\
                                             gauss_crop_cv2[0]:gauss_crop_cv2[1]+1,\
                                             gauss_crop_cv3[0]:gauss_crop_cv3[1]+1]\
                                    * self.heights[line]
                
                if self.periodic[1]:
                    if cv2bin[line] < gauss_center_to_end:
                        fes_crop_cv2_p = [self.res-1+(cv2bin[line]-gauss_center_to_end),self.res-1]
                        gauss_crop_cv2_p = [0,gauss_center_to_end-cv2bin[line]]
                        fes[fes_crop_cv1[0]:fes_crop_cv1[1]+1,\
                            fes_crop_cv2_p[0]:fes_crop_cv2_p[1]+1,\
                            fes_crop_cv3[0]:fes_crop_cv3[1]+1]\
                                    += gauss[gauss_crop_cv1[0]:gauss_crop_cv1[1]+1,\
                                             gauss_crop_cv2_p[0]:gauss_crop_cv2_p[1]+1,\
                                             gauss_crop_cv3[0]:gauss_crop_cv3[1]+1]\
                                    * self.heights[line]
                    
                    if cv2bin[line] > (self.res-gauss_center_to_end):
                        fes_crop_cv2_p = [0,gauss_center_to_end+cv2bin[line]-self.res-1]
                        gauss_crop_cv2_p = [gauss_res-(gauss_center_to_end+cv2bin[line]-self.res),gauss_res-1]
                        fes[fes_crop_cv1[0]:fes_crop_cv1[1]+1,\
                            fes_crop_cv2_p[0]:fes_crop_cv2_p[1]+1,\
                            fes_crop_cv3[0]:fes_crop_cv3[1]+1]\
                                    += gauss[gauss_crop_cv1[0]:gauss_crop_cv1[1]+1,\
                                             gauss_crop_cv2_p[0]:gauss_crop_cv2_p[1]+1,\
                                             gauss_crop_cv3[0]:gauss_crop_cv3[1]+1]\
                                    * self.heights[line]
                if self.periodic[2]:
                    if cv3bin[line] < gauss_center_to_end:
                        fes_crop_cv3_p = [self.res-1+(cv3bin[line]-gauss_center_to_end),self.res-1]
                        gauss_crop_cv3_p = [0,gauss_center_to_end-cv3bin[line]]
                        fes[fes_crop_cv1[0]:fes_crop_cv1[1]+1,\
                            fes_crop_cv2[0]:fes_crop_cv2[1]+1,\
                            fes_crop_cv3_p[0]:fes_crop_cv3_p[1]+1]\
                                    += gauss[gauss_crop_cv1[0]:gauss_crop_cv1[1]+1,\
                                             gauss_crop_cv2[0]:gauss_crop_cv2[1]+1,\
                                             gauss_crop_cv3_p[0]:gauss_crop_cv3_p[1]+1]\
                                    * self.heights[line]
                    
                    if cv3bin[line] > (self.res-gauss_center_to_end):
                        fes_crop_cv3_p = [0,gauss_center_to_end+cv3bin[line]-self.res-1]
                        gauss_crop_cv3_p = [gauss_res-(gauss_center_to_end+cv3bin[line]-self.res),gauss_res-1]
                        fes[fes_crop_cv1[0]:fes_crop_cv1[1]+1,\
                            fes_crop_cv2[0]:fes_crop_cv2[1]+1,\
                            fes_crop_cv3_p[0]:fes_crop_cv3_p[1]+1]\
                                    += gauss[gauss_crop_cv1[0]:gauss_crop_cv1[1]+1,\
                                             gauss_crop_cv2[0]:gauss_crop_cv2[1]+1,\
                                             gauss_crop_cv3_p[0]:gauss_crop_cv3_p[1]+1]\
                                    * self.heights[line]
                
                if self.periodic[0] and self.periodic[1]:
                    if ((cv1bin[line] < gauss_center_to_end) or (cv1bin[line] > (self.res-gauss_center_to_end))) \
                            and ((cv2bin[line] < gauss_center_to_end) or (cv2bin[line] > (self.res-gauss_center_to_end))):
                        fes[fes_crop_cv1_p[0]:fes_crop_cv1_p[1]+1,\
                            fes_crop_cv2_p[0]:fes_crop_cv2_p[1]+1,\
                            fes_crop_cv3[0]:fes_crop_cv3[1]+1]\
                                    += gauss[gauss_crop_cv1_p[0]:gauss_crop_cv1_p[1]+1,\
                                             gauss_crop_cv2_p[0]:gauss_crop_cv2_p[1]+1,\
                                             gauss_crop_cv3[0]:gauss_crop_cv3[1]+1]\
                                    * self.heights[line]
                
                if self.periodic[0] and self.periodic[2]:
                    if ((cv1bin[line] < gauss_center_to_end) or (cv1bin[line] > (self.res-gauss_center_to_end))) \
                            and ((cv3bin[line] < gauss_center_to_end) or (cv3bin[line] > (self.res-gauss_center_to_end))):
                        fes[fes_crop_cv1_p[0]:fes_crop_cv1_p[1]+1,\
                            fes_crop_cv2[0]:fes_crop_cv2[1]+1,\
                            fes_crop_cv3_p[0]:fes_crop_cv3_p[1]+1]\
                                    += gauss[gauss_crop_cv1_p[0]:gauss_crop_cv1_p[1]+1,\
                                             gauss_crop_cv2[0]:gauss_crop_cv2[1]+1,\
                                             gauss_crop_cv3_p[0]:gauss_crop_cv3_p[1]+1]\
                                    * self.heights[line]
                
                if self.periodic[1] and self.periodic[2]:
                    if ((cv2bin[line] < gauss_center_to_end) or (cv2bin[line] > (self.res-gauss_center_to_end))) \
                            and ((cv3bin[line] < gauss_center_to_end) or (cv3bin[line] > (self.res-gauss_center_to_end))):
                        fes[fes_crop_cv1[0]:fes_crop_cv1[1]+1,\
                            fes_crop_cv2_p[0]:fes_crop_cv2_p[1]+1,\
                            fes_crop_cv3_p[0]:fes_crop_cv3_p[1]+1]\
                                    += gauss[gauss_crop_cv1[0]:gauss_crop_cv1[1]+1,\
                                             gauss_crop_cv2_p[0]:gauss_crop_cv2_p[1]+1,\
                                             gauss_crop_cv3_p[0]:gauss_crop_cv3_p[1]+1]\
                                    * self.heights[line]
                
                if self.periodic[0] and self.periodic[1] and self.periodic[2]:
                    if ((cv1bin[line] < gauss_center_to_end) or (cv1bin[line] > (self.res-gauss_center_to_end)))\
                            and ((cv2bin[line] < gauss_center_to_end) or (cv2bin[line] > (self.res-gauss_center_to_end))) \
                            and ((cv3bin[line] < gauss_center_to_end) or (cv3bin[line] > (self.res-gauss_center_to_end))) :
                        fes[fes_crop_cv1_p[0]:fes_crop_cv1_p[1]+1,\
                            fes_crop_cv2_p[0]:fes_crop_cv2_p[1]+1,\
                            fes_crop_cv3_p[0]:fes_crop_cv3_p[1]+1]\
                                    += gauss[gauss_crop_cv1_p[0]:gauss_crop_cv1_p[1]+1,\
                                             gauss_crop_cv2_p[0]:gauss_crop_cv2_p[1]+1,\
                                             gauss_crop_cv3_p[0]:gauss_crop_cv3_p[1]+1]\
                                    * self.heights[line]
            
            if print_output:
                print("\n")
            if subtract_min:
                fes = fes-np.min(fes)
            self.fes = np.array(fes)
        else:
            print("Fes object doesn't have supported number of CVs.")
    
    def makefes2(self, resolution, time_min, time_max, print_output=True, subtract_min = True):
        """
        Function internally used to sum Hills in the same way as Plumed sum_hills. 
        """
        self.res = resolution
        
        if self.cvs == 1:
            fes = np.zeros((self.res))
            progress = 1
            max_progress = self.res ** self.cvs
            
            for x in range(self.res):
                progress += 1
                if print_output and (((progress) % 200 == 0) or (progress == max_progress)):
                    print(f"Constructing free energy surface: {(progress/max_progress):.2%} finished", end="\r")
                
                dist_cv1 = self.cv1[time_min-1:time_max]-(self.cv1min+(x)*self.cv1_fes_range/(self.res))
                if self.periodic[0]:
                    dist_cv1[dist_cv1<-0.5*self.cv1_fes_range] += self.cv1_fes_range
                    dist_cv1[dist_cv1>+0.5*self.cv1_fes_range] -= self.cv1_fes_range
                
                dp2 = dist_cv1**2/(2*self.s1[time_min-1:time_max]**2)
                tmp = np.zeros(time_max-time_min+1)
                heights = self.heights[time_min-1:time_max]
                tmp[dp2<6.25] = heights[dp2<6.25] * (np.exp(-dp2[dp2<6.25]) * 1.00193418799744762399 - 0.00193418799744762399)
                fes[x] = -tmp.sum()
                    
            if subtract_min:
                fes = fes - np.min(fes)
            self.fes = np.array(fes)
            if print_output:
                print("\n")
            
        elif self.cvs == 2:
            fes = np.zeros((self.res, self.res))
            progress = 0
            max_progress = self.res ** self.cvs
            
            for x in range(self.res):
                dist_cv1 = self.cv1[time_min-1:time_max]-(self.cv1min+(x)*self.cv1_fes_range/(self.res))
                if self.periodic[0]:
                    dist_cv1[dist_cv1<-0.5*self.cv1_fes_range] += self.cv1_fes_range
                    dist_cv1[dist_cv1>+0.5*self.cv1_fes_range] -= self.cv1_fes_range
                    
                for y in range(self.res):
                    progress += 1
                    if print_output and ((progress) % 200 == 0 or (progress == max_progress)):
                        print(f"Constructing free energy surface: {(progress/max_progress):.2%} finished", end="\r")
                    
                    dist_cv2 = self.cv2[time_min-1:time_max]-(self.cv2min+(y)*self.cv2_fes_range/(self.res))
                    if self.periodic[1]:
                        dist_cv2[dist_cv2<-0.5*self.cv2_fes_range] += self.cv2_fes_range
                        dist_cv2[dist_cv2>+0.5*self.cv2_fes_range] -= self.cv2_fes_range
                        
                    dp2 = dist_cv1**2/(2*self.s1[time_min-1:time_max]**2) + dist_cv2**2/(2*self.s2[time_min-1:time_max]**2)
                    tmp = np.zeros(time_max-time_min+1)
                    heights = self.heights[time_min-1:time_max]
                    tmp[dp2<6.25] = heights[dp2<6.25] * (np.exp(-dp2[dp2<6.25]) * 1.00193418799744762399 - 0.00193418799744762399)
                    fes[x,y] = -tmp.sum()
                    
            if subtract_min:
                fes = fes - np.min(fes)
            self.fes = np.array(fes)
            if print_output:
                print("\n")
            
        elif self.cvs == 3:
            fes = np.zeros((self.res, self.res, self.res))
            progress = 0
            max_progress = self.res ** self.cvs
            
            for x in range(self.res):
                dist_cv1 = self.cv1[time_min-1:time_max]-(self.cv1min+(x)*self.cv1_fes_range/(self.res))
                if self.periodic[0]:
                    dist_cv1[dist_cv1<-0.5*self.cv1_fes_range] += self.cv1_fes_range
                    dist_cv1[dist_cv1>+0.5*self.cv1_fes_range] -= self.cv1_fes_range
                    
                for y in range(self.res):
                    dist_cv2 = self.cv2[time_min-1:time_max]-(self.cv2min+(y)*self.cv2_fes_range/(self.res))
                    if self.periodic[1]:
                        dist_cv2[dist_cv2<-0.5*self.cv2_fes_range] += self.cv2_fes_range
                        dist_cv2[dist_cv2>+0.5*self.cv2_fes_range] -= self.cv2_fes_range
                        
                    for z in range(self.res):
                        progress += 1
                        if print_output and ((progress) % 200 == 0 or (progress == max_progress)):
                            print(f"Constructing free energy surface: {(progress/max_progress):.2%} finished", end="\r")
                        
                        dist_cv3 = self.cv3[time_min-1:time_max]-(self.cv3min+(z)*self.cv3_fes_range/(self.res))
                        if self.periodic[2]:
                            dist_cv3[dist_cv3<-0.5*self.cv3_fes_range] += self.cv3_fes_range
                            dist_cv3[dist_cv3>+0.5*self.cv3_fes_range] -= self.cv3_fes_range
                        
                        dp2 = dist_cv1**2/(2*self.s1[time_min-1:time_max]**2) + \
                              dist_cv2**2/(2*self.s2[time_min-1:time_max]**2) + \
                              dist_cv3**2/(2*self.s3[time_min-1:time_max]**2)
                        tmp = np.zeros(time_max-time_min+1)
                        heights = self.heights[time_min-1:time_max]
                        tmp[dp2<6.25] = heights[dp2<6.25] * (np.exp(-dp2[dp2<6.25]) * 1.00193418799744762399 - 0.00193418799744762399)
                        fes[x,y,z] = -tmp.sum()
                        
            if subtract_min:
                fes = fes - np.min(fes)
            self.fes = np.array(fes)
            if print_output:
                print("\n")
        else:
            print(f"Error: unsupported number of CVs: {self.cvs}.")
    
    def plot(self, png_name=None, contours=True, contours_spacing=0.0, aspect = 1.0, cmap = "RdYlBu_r", 
             energy_unit="kJ/mol", xlabel=None, ylabel=None, zlabel=None, label_size=12, clabel_size = 12,
             image_size=None, image_size_unit="in", dpi=100, vmin = 0, vmax = None, 
             opacity=0.2, levels=None, title = None, off_screen = False, 
             xlim=[None, None], ylim=[None, None], return_fig=False):
        """
        Function used to visualize FES, based on Matplotlib for 1D and 2D FES or PyVista for 3D FES. 
        
        ```python
        fes.plot(png_name="fes.png")
        ```
        
        Parameters:
        
        * png_name = String. If this parameter is supplied, the picture of FES will be saved under this name to the current working directory. 
        For 1D and 2D FES, the recommended format in '.png'. For 3D FES, the formats supported by PyVista are '.svg','.eps','.ps','.pdf' and '.tex'. 
        
        * contours (default=True) = whether contours should be shown on 2D FES
        
        * contours_spacing (default=0.0) = when a positive number is set, it will be used as spacing for contours on 2D FES. 
                Otherwise, if contours=True, there will be five equally spaced contour levels.
        
        * aspect (default = 1.0) = aspect ratio of the graph. Works with 1D and 2D FES. 
        
        * cmap (default = "RdYlBu_r") = Matplotlib colormap used to color 2D or 3D FES
        
        * energy_unit (default="kJ/mol") = String, used in description of colorbar

        * xlim, ylim (default = [None, None] for both) = list of two values specifying the range of x and y axes respectively. 
        None means that Matplotlib will choose appropriate range, so this keyword is useful when you need to overwrite it. 
        Does not work for 3D FES at the moment. 
        
        * xlabel, ylabel, zlabel = Strings, if provided, they will be used as labels for the graphs
        
        * labelsize (default = 12) = size of text in labels
        
        * image_size (default = [9,6]) = List of the width and height of the picture

        * image_size_unit (default = "in") = Units for width and height of the picture, accepts "in" as inches, "cm", "mm" or "px" as pixels. 

        * dpi (default = 100) = DPI of the resulting image (for 1D and 2D FES). 
        
        * vmin (default=0) = real number, lower bound for the colormap on 2D FES
        
        * vmax = real number, upper bound for the colormap on 2D FES
        
        * opacity (default=0.2) = number between 0 and 1, is the opacity of isosurfaces of 3D FES
        
        * levels = Here you can specify list of free energy values for isosurfaces on 3D FES. 
                        If not provided, default values from contours parameters will be used instead. 

        * title = optional, string that defines the title of the graph

        * offscreen (default = False) = for 3D FES only, grapf will not be shown after creation - used internally by metadynminer when making animations

        * return_fig (default=False) = whether the method should return the Matplotlib.Pyplot.figure object for further use. 
        In the case of plotting 3D FES, it returns Pyvista.plotter object instead. 
        """
        if vmax == None:
            vmax = np.max(self.fes)+0.01 # if the addition is smaller than 0.01, the 3d plot stops working. 
            
        if contours_spacing == 0.0:
            contours_spacing = (vmax-vmin)/5.0
        
        cmap = cm.get_cmap(cmap)
        
        cmap.set_over("white")
        cmap.set_under("white")
        
        if self.cvs >= 1:
            cv1min = self.cv1min
            cv1max = self.cv1max
        if self.cvs >=2:
            cv2min = self.cv2min
            cv2max = self.cv2max 
        if self.cvs == 3:
            cv3min = self.cv3min
            cv3max = self.cv3max
        
        if image_size == None:
            image_size = [9,6]    
        
        if image_size_unit == "cm":
            image_size[0] /= 2.54
            image_size[1] /= 2.54
        elif image_size_unit == "mm":
            image_size[0] /= 25.4
            image_size[1] /= 25.4
        elif image_size_unit == "px":
            image_size[0] /= dpi
            image_size[1] /= dpi
        elif image_size_unit != "in":
            print(f"Warning: unknown image_size_unit value: {image_size_unit}. Using inches instead. ")
            
        
        if self.cvs == 1:
            fig = plt.figure(figsize=(image_size[0],image_size[1]), dpi=dpi)
            X = np.linspace(cv1min, cv1max, self.res)
            plt.plot(X, self.fes)
            if xlabel == None:
                plt.xlabel(f'CV1 - {self.cv1_name}', size=label_size)
            else:
                plt.xlabel(xlabel, size=label_size)
            if ylabel == None:
                plt.ylabel(f'free energy ({energy_unit})', size=label_size)
            else:
                plt.ylabel(ylabel, size=label_size)
        
            if title != None:
                plt.title(title, size=label_size)
    
            plt.xticks(fontsize=label_size)
            plt.yticks(fontsize=label_size)
                
            ax = plt.gca()
            ax.set_xlim(xlim)
            ax.set_ylim(ylim)
            
            if png_name != None:
                plt.savefig(png_name, bbox_inches = 'tight')

            if return_fig:
                return fig
            
        if self.cvs == 2:
            fig = plt.figure(figsize=(image_size[0], image_size[1]), dpi=dpi)
            plt.imshow(np.rot90(self.fes, axes=(0,1)), cmap=cmap, interpolation='nearest', 
                       extent=[cv1min, cv1max, cv2min, cv2max], 
                       aspect = (((cv1max-cv1min)/(cv2max-cv2min))/(aspect)),
                       vmin = vmin, vmax = vmax)
            cbar = plt.colorbar()
            cbar.ax.tick_params(labelsize=label_size) 
            cbar.set_label(energy_unit, size=label_size)
            
            if contours:
                if levels != None:
                    cont = plt.contour(np.rot90(self.fes, axes=(0,1)), 
                             levels = levels, 
                             extent=[cv1min, cv1max, cv2max, cv2min], 
                             colors = "k", linestyles = "solid")
                    plt.clabel(cont, levels = levels, fontsize=clabel_size)
                else:
                    cont = plt.contour(np.rot90(self.fes, axes=(0,1)), 
                             levels = np.arange(vmin, (vmax - 0.01), contours_spacing), 
                             extent=[cv1min, cv1max, cv2max, cv2min], 
                             colors = "k", linestyles = "solid")
                    plt.clabel(cont, levels = np.arange(vmin, (vmax - 0.01), contours_spacing), fontsize=clabel_size)
            
            if xlabel == None:
                plt.xlabel(f'CV1 - {self.cv1_name}', size=label_size)
            else:
                plt.xlabel(xlabel, size=label_size)
                
            if ylabel == None:
                plt.ylabel(f'CV2 - {self.cv2_name}', size=label_size)
            else:
                plt.ylabel(ylabel, size=label_size)
            
            if title != None:
                plt.title(title, size=label_size)
    
            plt.xticks(fontsize=label_size)
            plt.yticks(fontsize=label_size)

            ax = plt.gca()
            ax.set_xlim(xlim)
            ax.set_ylim(ylim)

            ax.tick_params(axis='x', labelsize=label_size)
            ax.tick_params(axis='y', labelsize=label_size)
            
            if png_name != None:
                plt.savefig(png_name, bbox_inches = 'tight')
            
            if return_fig:
                return fig
        
        if self.cvs == 3:
            if xlabel == None:
                xlabel = "CV1 - " + self.cv1_name
            if ylabel == None:
                ylabel = "CV2 - " + self.cv2_name
            if zlabel == None:
                zlabel = "CV3 - " + self.cv3_name
            
            grid = pv.ImageData(
                dimensions=(self.res, self.res, self.res),
                spacing=((cv1max-cv1min)/self.res,(cv2max-cv2min)/self.res,(cv3max-cv3min)/self.res),
                origin=(cv1min, cv2min, cv3min)
            )
            grid["vol"] = self.fes.ravel(order="F")
            if levels == None:
                contours = grid.contour(np.arange(0, (vmax - 0.01), contours_spacing))
            else:
                contours = grid.contour(levels)
            fescolors = []
            for i in range(contours.points.shape[0]):
                fescolors.append(self.fes[int((contours.points[i,0]-cv1min)*self.res/(cv1max-cv1min)),
                                          int((contours.points[i,1]-cv2min)*self.res/(cv2max-cv2min)),
                                          int((contours.points[i,2]-cv3min)*self.res/(cv3max-cv3min))])
            #%% Visualization
            bounds = [cv1min, cv1max, cv2min, cv2max, cv3min, cv3max]
            pv.set_plot_theme('document')
            p = pv.Plotter()
            p.add_mesh(contours, scalars=fescolors, opacity=opacity, cmap=cmap, show_scalar_bar=False, interpolate_before_map=True)
            p.show_bounds(bounds=bounds, xtitle=xlabel, ytitle=ylabel, ztitle=zlabel, grid=True, location="outer")
            if title != None:
                p.add_title(title, font_size=label_size)
            if not off_screen:
                p.show()
            
            if png_name != None:
                p.save_graphic(png_name)
            
            if return_fig:
                return p
    
    def set_fes(self, fes):
        self.fes = fes
        
    def surface_plot(self, png_name=None, cmap = "RdYlBu_r", 
                     energy_unit="kJ/mol", xlabel=None, ylabel=None, zlabel=None, dpi=100, label_size=12, image_size=None, image_size_unit="in", rstride=1, cstride=1, vmin = 0, vmax = None, title=None, return_fig=False):
        """
        Function for visualization of 2D FES as 3D surface plot. For now, it is based on Matplotlib, but there are issues with interactivity. 
        
        It can be interacted with in jupyter notebook or jupyter lab in %matplotlib widget mode. Otherwise it is just static image of the 3D surface plot. 
        
        ```python
        %matplotlib widget
        fes.surface_plot()
        ```
        
        """
        if self.cvs == 2:
            cv1min = self.cv1min
            cv1max = self.cv1max
            cv2min = self.cv2min
            cv2max = self.cv2max 
            
            x = np.linspace(cv1min, cv1max, self.res)
            y = np.linspace(cv2min, cv2max, self.res)
            
            X, Y = np.meshgrid(x, y)
            Z = self.fes.T
    
            if image_size == None:
                image_size = [9,9]
            
            if image_size_unit == "cm":
                image_size[0] /= 2.54
                image_size[1] /= 2.54
            elif image_size_unit == "mm":
                image_size[0] /= 25.4
                image_size[1] /= 25.4
            elif image_size_unit == "px":
                image_size[0] /= dpi
                image_size[1] /= dpi
            elif image_size_unit != "in":
                print(f"Warning: unknown image_size_unit value: {image_size_unit}. Using inches instead. ")
            
            fig = plt.figure(figsize=(image_size[0],image_size[1]), dpi=dpi)
            ax = fig.add_subplot(projection="3d")
            ax.plot_surface(X,Y,Z, cmap=cmap, rstride=rstride, cstride=cstride)
            
            if xlabel == None:
                ax.set_xlabel(f'CV1 - {self.cv1_name}', size=label_size)
            else:
                ax.set_xlabel(xlabel, size=label_size)
            if ylabel == None:
                ax.set_ylabel(f'CV2 - {self.cv2_name}', size=label_size)
            else:
                ax.set_ylabel(ylabel, size=label_size)
            if zlabel == None:
                ax.set_zlabel(f'free energy ({energy_unit})', size=label_size)
            else:
                ax.set_zlabel(zlabel, size=label_size)
    
            if title != None:
                plt.title(title, size=label_size)
    
            plt.xticks(fontsize=label_size)
            plt.yticks(fontsize=label_size)
    
            if png_name != None:
                plt.savefig(png_name, bbox_inches = 'tight')
            
            if return_fig:
                return fig
                
        else:
            print(f"Error: Surface plot only works for FES with exactly two CVs, and this FES has {self.cvs} CV.")
    
    def remove_CV(self, CV=None, energy_unit="kJ/mol", temp=300.0):
        """
        This function is used to remove a CV from an existing FES. The function first recalculates the FES to an array of probabilities. The probabilities 
        are summed along the CV to be removed, and resulting probability distribution with 1 less dimension is converted back to FES. 

        ```python
        fes_cv1 = fes.remove_CV(CV=2)
        ```
        
        Parameters:
        
        * CV = integer, the CV to be removed
        
        * energy_unit (default="kJ/mol") = has to be either "kJ/mol" or "kcal/mol". Make sure to suply the correct energy unit, otherwise you will get wrong FES as a result. 
        
        * temp (default=300.0) = temperature of the simulation in Kelvins.
        """
        CV = int(float(CV))
        print(f"Removing CV {CV}.")
        if CV > self.cvs:
            print("Error: The CV to remove is not available in this FES object.")
            return None
        if self.cvs == 1:
            print("Error: You can not remove the only CV. ")
            return None
        elif self.cvs == 2:
            if energy_unit == "kJ/mol":
                probabilities = np.exp(-1000*self.fes/8.314/temp)
                if CV == 1:
                    new_prob = np.sum(probabilities, axis=0)
                    new_fes = Fes(hills=None)
                    new_fes.fes = -8.314*temp*np.log(new_prob)/1000
                    new_fes.fes = new_fes.fes - np.min(new_fes.fes)
                    new_fes.cvs = 1
                    new_fes.res = self.res
                    new_fes.periodic = [self.periodic[1]]
                    new_fes.cv1min = self.cv2min
                    new_fes.cv1max = self.cv2max
                    new_fes.cv1_name = self.cv2_name
                    new_fes.cv1per = self.cv2per
                    new_fes.cv1_fes_range = self.cv2_fes_range
                if CV == 2:
                    new_prob = np.sum(probabilities, axis=1)
                    new_fes = Fes(hills=None)
                    new_fes.fes = -8.314*temp*np.log(new_prob)/1000
                    new_fes.fes = new_fes.fes - np.min(new_fes.fes)
                    new_fes.cvs = 1
                    new_fes.res = self.res
                    new_fes.periodic = [self.periodic[0]]
                    new_fes.cv1min = self.cv1min
                    new_fes.cv1max = self.cv1max
                    new_fes.cv1_name = self.cv1_name
                    new_fes.cv1per = self.cv1per
                    new_fes.cv1_fes_range = self.cv1_fes_range
                return new_fes
            elif energy_unit == "kcal/mol":
                probabilities = np.exp(-1000*4.184*self.fes/8.314/temp)
                if CV == 1:
                    new_prob = np.sum(probabilities, axis=1)
                    new_fes = Fes(hills=None)
                    new_fes.fes = -8.314*temp*np.log(new_prob)/1000/4.184
                    new_fes.fes = new_fes.fes - np.min(new_fes.fes)
                    new_fes.cvs = 1
                    new_fes.res = self.res
                    new_fes.periodic = [self.periodic[1]]
                    new_fes.cv1min = self.cv2min
                    new_fes.cv1max = self.cv2max
                    new_fes.cv1_name = self.cv2_name
                    new_fes.cv1per = self.cv2per
                    new_fes.cv1_fes_range = self.cv2_fes_range
                if CV == 2:
                    new_prob = np.sum(probabilities, axis=0)
                    new_fes = Fes(hills=None)
                    new_fes.fes = -8.314*temp*np.log(new_prob)/1000/4.184
                    new_fes.fes = new_fes.fes - np.min(new_fes.fes)
                    new_fes.cvs = 1
                    new_fes.res = self.res
                    new_fes.periodic = [self.periodic[0]]
                    new_fes.cv1min = self.cv1min
                    new_fes.cv1max = self.cv1max
                    new_fes.cv1_name = self.cv1_name
                    new_fes.cv1per = self.cv1per
                    new_fes.cv1_fes_range = self.cv1_fes_range
                return new_fes
            else:
                print("Error: unknown energy unit")
                return None
        elif self.cvs == 3:
            if energy_unit == "kJ/mol":
                probabilities = np.exp(-1000*self.fes/8.314/temp)
                if CV == 1:
                    new_prob = np.sum(probabilities, axis=0)
                    new_fes = Fes(hills=None)
                    new_fes.fes = -8.314*temp*np.log(new_prob)/1000
                    new_fes.fes = new_fes.fes - np.min(new_fes.fes)
                    new_fes.cvs = 2
                    new_fes.res = self.res
                    new_fes.periodic = [self.periodic[1], self.periodic[2]]
                    new_fes.cv1min = self.cv2min
                    new_fes.cv1max = self.cv2max
                    new_fes.cv2min = self.cv3min
                    new_fes.cv2max = self.cv3max
                    new_fes.cv1_name = self.cv2_name
                    new_fes.cv2_name = self.cv3_name
                    new_fes.cv1per = self.cv2per
                    new_fes.cv2per = self.cv3per
                    new_fes.cv1_fes_range = self.cv2_fes_range
                    new_fes.cv2_fes_range = self.cv3_fes_range
                if CV == 2:
                    new_prob = np.sum(probabilities, axis=1)
                    new_fes = Fes(hills=None)
                    new_fes.fes = -8.314*temp*np.log(new_prob)/1000
                    new_fes.fes = new_fes.fes - np.min(new_fes.fes)
                    new_fes.cvs = 2
                    new_fes.res = self.res
                    new_fes.periodic = [self.periodic[0], self.periodic[2]]
                    new_fes.cv1min = self.cv1min
                    new_fes.cv1max = self.cv1max
                    new_fes.cv2min = self.cv3min
                    new_fes.cv2max = self.cv3max
                    new_fes.cv1_name = self.cv1_name
                    new_fes.cv2_name = self.cv3_name
                    new_fes.cv1per = self.cv1per
                    new_fes.cv2per = self.cv3per
                    new_fes.cv1_fes_range = self.cv1_fes_range
                    new_fes.cv2_fes_range = self.cv3_fes_range
                if CV == 3:
                    new_prob = np.sum(probabilities, axis=2)
                    new_fes = Fes(hills=None)
                    new_fes.fes = -8.314*temp*np.log(new_prob)/1000
                    new_fes.fes = new_fes.fes - np.min(new_fes.fes)
                    new_fes.cvs = 2
                    new_fes.res = self.res
                    new_fes.periodic = [self.periodic[0], self.periodic[1]]
                    new_fes.cv1min = self.cv1min
                    new_fes.cv1max = self.cv1max
                    new_fes.cv2min = self.cv2min
                    new_fes.cv2max = self.cv2max
                    new_fes.cv1_name = self.cv1_name
                    new_fes.cv2_name = self.cv2_name
                    new_fes.cv1per = self.cv1per
                    new_fes.cv2per = self.cv2per
                    new_fes.cv1_fes_range = self.cv1_fes_range
                    new_fes.cv2_fes_range = self.cv2_fes_range
                return new_fes
            elif energy_unit == "kcal/mol":
                probabilities = np.exp(-1000*4.184*self.fes/8.314/temp)
                if CV == 1:
                    new_prob = np.sum(probabilities, axis=0)
                    new_fes = Fes(hills=None)
                    new_fes.fes = -8.314*temp*np.log(new_prob)/1000/4.184
                    new_fes.fes = new_fes.fes - np.min(new_fes.fes)
                    new_fes.cvs = 2
                    new_fes.res = self.res
                    new_fes.periodic = [self.periodic[1], self.periodic[2]]
                    new_fes.cv1min = self.cv2min
                    new_fes.cv1max = self.cv2max
                    new_fes.cv2min = self.cv3min
                    new_fes.cv2max = self.cv3max
                    new_fes.cv1_name = self.cv2_name
                    new_fes.cv2_name = self.cv3_name
                    new_fes.cv1per = self.cv2per
                    new_fes.cv2per = self.cv3per
                    new_fes.cv1_fes_range = self.cv2_fes_range
                    new_fes.cv2_fes_range = self.cv3_fes_range
                if CV == 2:
                    new_prob = np.sum(probabilities, axis=1)
                    new_fes = Fes(hills=None)
                    new_fes.fes = -8.314*temp*np.log(new_prob)/1000/4.184
                    new_fes.fes = new_fes.fes - np.min(new_fes.fes)
                    new_fes.cvs = 2
                    new_fes.res = self.res
                    new_fes.periodic = [self.periodic[0], self.periodic[2]]
                    new_fes.cv1min = self.cv1min
                    new_fes.cv1max = self.cv1max
                    new_fes.cv2min = self.cv3min
                    new_fes.cv2max = self.cv3max
                    new_fes.cv1_name = self.cv1_name
                    new_fes.cv2_name = self.cv3_name
                    new_fes.cv1per = self.cv1per
                    new_fes.cv2per = self.cv3per
                    new_fes.cv1_fes_range = self.cv1_fes_range
                    new_fes.cv2_fes_range = self.cv3_fes_range
                if CV == 3:
                    new_prob = np.sum(probabilities, axis=2)
                    new_fes = Fes(hills=None)
                    new_fes.fes = -8.314*temp*np.log(new_prob)/1000/4.184
                    new_fes.fes = new_fes.fes - np.min(new_fes.fes)
                    new_fes.cvs = 2
                    new_fes.res = self.res
                    new_fes.periodic = [self.periodic[0], self.periodic[1]]
                    new_fes.cv1min = self.cv1min
                    new_fes.cv1max = self.cv1max
                    new_fes.cv2min = self.cv2min
                    new_fes.cv2max = self.cv2max
                    new_fes.cv1_name = self.cv1_name
                    new_fes.cv2_name = self.cv2_name
                    new_fes.cv1per = self.cv1per
                    new_fes.cv2per = self.cv2per
                    new_fes.cv1_fes_range = self.cv1_fes_range
                    new_fes.cv2_fes_range = self.cv2_fes_range
                return new_fes
            else:
                print("Error: unknown energy unit")
                return None

    def flooding_animation(self, gif_name = "flooding.gif", use_vmax_from_end = True, with_minima = True, use_minima_from_end=False, cmap="RdYlBu_r", xlabel=None, ylabel=None, zlabel=None, label_size=12, image_size=None, image_size_unit="in", dpi=100, tu = "ps", time_min=None, time_max=None, step=1000, contours_spacing = 20, levels=None, opacity = 0.2, vmin = 0, vmax = None, energy_unit="kJ/mol", clear_temporary_folder=True, temporary_folder_name="temporary_folder", time_unit="ps", fps=5, enable_loop = True, minima_precise = False, minima_nbins=8, temp=300.0, 
                           xlim=[None, None], ylim=[None, None]):
        """
        This method is used to make an animation that shows, how the FES was evolving during metadynamics simulation. It creates temporary folder and svaes plots of FES at different times during simulation, then it concatenates them to make a gif animation and removes the temporary files (remove can be switched off, if necessary). 

        Use:
        
        ```python
        fes.flooding_animation()
        ```
        Parameters:
        * gif_name (default="flooding.gif") = name for the animation file

        * optional parameters used when calling
        ```python
        fes.plot()
        ```
        or
        ```python
        minima.plot()
        ```
        can be used to adjust the graphs used to create animation

        * use_vmax_from_end (default=True), boolean value, if it is True and vmax is not specified, it will use the vmax value from the end point of the fes object for all frames of animation; then, for 2D FES, the colors will be comparable to each other

        * with_minima (default = True), boolean value, if True, graphs will be shown with letters at each local minima found at each frame of the animation

        * use _minima_from_end (default = True), boolean value, if True, the local minima from the end point of the fes object wil be depicted at each frame of animation

        * minima_precise (default=False) = whether the local minima should be calculated using the precise algorithm (see Minima for more info)

        * minima_nbins (default = 8) = the nbins keyword for minima localization

        * temp (default = 300.0), energy_unit (default = "kJ/mol") - keywords for minima localization in case minima_precise = True

        * step (default=1000), integer, frames for animation will be made at each n-th line in HILLS file

        * temporary_folder_name (default="temporary_folder"), name of the temporary folder where the individual graphs will be saved; directory with this name shouldn't be present in working irectory when calling the method, otherwise it will throw an error
        
        * clear_temporary_folder (default=True), if set to false, the graphs made for each frame of the simulation will not be removed afterwards, so they can be viewed later

        * fps (defalt=5) = how many frames per second the animation will have

        * enable_loop (default=True) = whether the animation should be running in loop
        """
        tu = TU(tu)
        
        current_directory = os.getcwd()
        final_directory = os.path.join(current_directory, temporary_folder_name)
        if os.path.exists(final_directory):
            print(f"Error: directory with the name {temporary_folder_name} already exists. Try using different name using temporary_folder_name keyword. ")
            return None
        else:
           os.makedirs(final_directory)
        flooding_fes = copy.deepcopy(self)
        step_fes = copy.deepcopy(self)
        
        if time_min == time_max == 0:
            print("Error: Values of start and end time are zero.")
            return None
        if time_min != None:
            time_min = tu.inps(time_min)
            if time_min < 0:
                print("Warning: Start time is lower than zero, it will be set to zero instead. ")
                time_min = 0
            if time_min < int(self.hills.hills[0,0]):
                print(f"Warning: Start time {tu.inps(time_min)} ps is lower than the first time from HILLS file {int(self.hills.hills[0,0])}, which will be used instead. ")
                time_min = int(self.hills.hills[0,0])
        else:
            time_min = int(self.hills.hills[0,0])
        if time_max != None:
            time_max = tu.inps(time_max)
            if time_max < time_min:
                print("Warning: End time is lower than start time. Values are flipped. ")
                time_value = time_max
                time_max = time_min
                time_min = time_value
            if time_max > int(self.hills.hills[-1,0]):
                print(f"Warning: End time {tu.inps(time_max)} ps is higher than number of lines in HILLS file {int(self.hills.hills[-1,0])}, which will be used instead. ")
                time_max = int(self.hills.hills[-1,0])
        else:
            time_max = int(self.hills.hills[-1,0])
        
        if not self.ignoretime:
            time_max = int(round(((time_max - self.hills.hills[0,0])/self.dt),0)) + 1
            time_min = int(round(((time_min - self.hills.hills[0,0])/self.dt),0)) + 1

        if time_min==None:
            time_min=1
        if time_max==None:
            time_max = int(self.hills.hills[-1,0])

        if (vmax == None) and use_vmax_from_end:
            vmax = np.max(self.fes)+0.1
        if self.cvs == 1 or self.cvs == 2:
            suffix="png"
        if self.cvs == 3:
            suffix="eps"
        
        times = np.array((range(time_min-1, time_max+1, step)))
        image_files = [f'{final_directory}/{times[i]}.{suffix}'.format(i) for i in range(1, len(times))]
        
        minima_final = Minima(self, precise=minima_precise, nbins=minima_nbins, energy_unit=energy_unit, temp=temp, print_output=False)
        
        for i in range(1,len(times)):
            print(f"Constructing flooding animation: {((i+1)/len(times)):.2%} finished", end="\r")
            if self.original==False:
                step_fes.makefes(flooding_fes.res, time_min=(times[i-1]+1), time_max=times[i], print_output=False)
            else:
                step_fes.makefes2(flooding_fes.res, time_min=(times[i-1]+1), time_max=times[i], print_output=False)
            if i == 1:
                flooding_fes.fes = step_fes.fes
            else:
                flooding_fes.fes += step_fes.fes
            flooding_fes.fes = flooding_fes.fes - np.min(flooding_fes.fes)
            try:
                if with_minima:
                    mf = Minima(flooding_fes, precise=minima_precise, print_output=False, nbins=minima_nbins, temp=temp, energy_unit=energy_unit)
                    if use_minima_from_end:
                        mf.minima = minima_final.minima
                    mf.plot(contours_spacing=contours_spacing, cmap = cmap, 
                            xlabel = xlabel, ylabel = ylabel, zlabel=zlabel, 
                            label_size=label_size, image_size=image_size, 
                            image_size_unit=image_size_unit, dpi=dpi, vmin = vmin, 
                            vmax = vmax, levels=levels, energy_unit=energy_unit, 
                            off_screen = True, opacity=opacity, 
                            png_name=f"{final_directory}/{times[i]}.{suffix}", title=f"{times[i]} {time_unit}", xlim=xlim, ylim=ylim)
                else:
                    flooding_fes.plot(contours_spacing=contours_spacing, cmap = cmap, xlabel = xlabel, ylabel = ylabel, zlabel=zlabel, label_size=label_size, image_size=image_size, image_size_unit=image_size_unit, dpi = dpi, vmin = vmin, vmax = vmax, levels=levels, energy_unit=energy_unit, off_screen = True, opacity=opacity, png_name=f"{final_directory}/{times[i]}.{suffix}", title=f"{times[i]} {time_unit}", xlim=xlim, ylim=ylim)
                plt.close()
            except ValueError:
                print("Warning: The first frame of animation would be blank with the current settings, but PyVista 3D plotter can not plot empty meshes. Try to increase the timestep between frames or decrease the spacing between isosurfaces.")
        print("\n")

        duration = 1000/fps
        if enable_loop:
            with imageio.get_writer(gif_name, format="GIF", duration=duration, loop=0) as writer:
                for image_file in image_files:
                    try:
                        image = imageio.imread(image_file)
                        writer.append_data(image)
                    except FileNotFoundError:
                        print("Warning: File for animation was not found.")
        else:
            with imageio.get_writer(gif_name, format="GIF", fps = fps) as writer:
                for image_file in image_files:
                    try:
                        image = imageio.imread(image_file)
                        writer.append_data(image)
                    except FileNotFoundError:
                        print("Warning: File for animation was not found.")

        if clear_temporary_folder:
            for image_file in image_files:
                try:
                    os.remove(image_file)
                except FileNotFoundError:
                        print("Warning: File for animation was not found.")
            try: 
                os.rmdir(final_directory)
            except OSError:
                print(f"Warning: directory{final_directory} is not empty and will not be removed. Metadynminer's temporary files inside were removed. ")
               

    
    def make_gif(self, gif_name=None, cmap = "RdYlBu_r", energy_unit="kJ/mol",
                 xlabel=None, ylabel=None, zlabel=None, label_size=12, image_size=[10,7], 
                  opacity=0.2, levels=None, frames=64):
        """
        Function that generates animation of 3D FES showing different isosurfaces.
        
        ```python
        fes.make_gif(gif_name="FES_animation.gif")
        ```
        
        Parameters:
        
        * gif_name (default="FES.gif") = String. Name of the gif of FES that will be saved in the current working directory.
        
        * cmap (default = "RdYlBu_r") = Matplotlib colormap used to color the 3D FES
        
        * xlabel, ylabel, zlabel = Strings, if provided, they will be used as labels for the graph
        
        * labelsize (default = 12) = size of text in labels
        
        * image_size (default = [10,7]) = List of the width and height of the picture
        
        * opacity (default = 0.2) = number between 0 and 1, is the opacity of isosurfaces of 3D FES
        
        * levels = Here you can specify list of free energy values for isosurfaces on 3D FES. 
                If not provided, default values from contours parameters will be used instead. 
        
        * frames (default = 64) = Number of frames the animation will be made of. 
        """
        if self.cvs == 3:
            cv1min = self.cv1min
            cv1max = self.cv1max
            cv2min = self.cv2min
            cv2max = self.cv2max
            cv3min = self.cv3min
            cv3max = self.cv3max
            
            values = np.linspace(np.min(self.fes)+0.1, np.max(self.fes), num=frames)
            grid = pv.ImageData(
                dimensions=(self.res, self.res, self.res),
                spacing=((cv1max-cv1min)/self.res,(cv2max-cv2min)/self.res,(cv3max-cv3min)/self.res),
                origin=(cv1min, cv2min, cv3min),
            )
            grid["vol"] = self.fes.ravel(order="F")
            surface = grid.contour(values[:1])
            surfaces = [grid.contour([v]) for v in values]
            surface = surfaces[0].copy()
            
            pv.set_plot_theme('document')
            plotter = pv.Plotter(off_screen=True)
            # Open a movie file
            if gif_name == None:
                gif_name = "FES_animation.gif"
            plotter.open_gif(gif_name)

            # Add initial mesh
            plotter.add_mesh(
                surface,
                opacity=opacity,
                clim=grid.get_data_range(),
                show_scalar_bar=False,
                cmap="RdYlBu_r"
            )
            plotter.add_mesh(grid.outline_corners(), color="k")
            text = plotter.add_text(f"{values[0]:.2f}+kJ/mol", position='lower_right', font_size=12)
            if xlabel == None and ylabel == None and zlabel == None:
                plotter.show_grid(xtitle=f"CV1 - {self.cv1_name}", ytitle=f"CV2 - {self.cv2_name}", ztitle=f"CV3 - {self.cv3_name}")
            else:
                plotter.show_grid(xtitle=xlabel, ytitle=ylabel, ztitle=zlabel)
            plotter.set_background('white')
            plotter.show(auto_close=False)

            # Run through each frame
            for surf in range(len(surfaces)):
                surface.copy_from(surfaces[surf])
                plotter.remove_actor(text)
                text = plotter.add_text(f"{values[surf]:.2f} {energy_unit}", position='lower_right', font_size=12)
                plotter.write_frame()  # Write this frame
            # Run through backwards
            for surf in range(len(surfaces)-1,0,-1):
                surface.copy_from(surfaces[surf])
                plotter.remove_actor(text)
                text = plotter.add_text(f"{values[surf]:.2f} {energy_unit}", position='lower_right', font_size=12)
                plotter.write_frame()  # Write this frame

            # Be sure to close the plotter when finished
            plotter.close()
        else:
            print("Error: this method is only available for FES with 3 CVs.")     
 
class Minima():
    """
    Object of Minima class is created to find local free energy minima on FES and calculates their free energy values. 
    
    The list of minima is stored as pandas dataframe. 
    
    Command:
    ```python
    minima = metadynminer.Minima(fes=f, nbins=8)
    ```
    
    List of minima can be later shown like this:
    
    ```python
    print(minima.minima)
    ```
    
    Parameters:
    
    * fes = Fes object to find the minima on
    
    * nbins (default = 8) = number of bins to divide the FES

    * precise (default=True) = if True, the local minima will use an algorithm which finds all local minima, even very shallow and probably unimportant minima, each point on the FES will be assigned to the minimum the system would most likely go to, if it only follows the gradient of free energy, and free energy value of minima will be calculated from each point on FES assigned to the respective minima. This results in more precise free energy values, as it accounts for the width of the minimum as well. For this calculation the unit of free energy and the thermodynamical temperature of the simulation must be supplied. This algorithm doesn't use the nbins keyword. 
    If you set precise = False, the method will use the original algorithm from the metadynminer package for R. In this algorithm the FES is 
    first divided to number of bins (can be set with option nbins, default is 8)
    and the absolute minima is found for each bin. Then the algorithm checks 
    if this point is really a local minimum by comparing to the surrounding points of FES.
    This algorithm only accounts for the depth of each minima, which is less precise, but usually sufficient. 
    In some cases, this algorithm is the prefered one, 
    because on some free energy landscapes the total number of local free energy minima can reach tens of thousands, 
    which makes the calculation using precise algorithm slow and impractical. 

    * temp (default = 300.0) = thermodynamical temperature of the simulation. Used only if precise=True. 

    * energy_unit (default = "kJ/mol") = energy unit of the free energy surface; must be either "kJ/mol" or "kcal/mol". Used only if precise=True. 

    * max_iteration (default=10000), the maximum number of iteration the algorithm will last when assigning FES points to their respective local minima

    Attributes: 

    Minima.fes = source free energy surface 

    Minima.minima = pandas dataframe with local minima and their properties

    Minima.m_fes = if you use precise=True, this attribute contains the FES, but each bin contains the index of the minima to which it belongs

    * and other attributes analogous to those of Fes objects
    """
    
    def __init__(self, fes, nbins = 8, precise=True, temp=300.0, energy_unit="kJ/mol", max_iteration=10000, print_output=True):
        self.fes = fes.fes
        self.periodic = fes.periodic
        self.cvs = fes.cvs
        self.res = fes.res

        if self.cvs >= 1:
            self.cv1_name = fes.cv1_name
            self.cv1min = fes.cv1min
            self.cv1max = fes.cv1max
            self.cv1per = fes.cv1per
            self.cv1_fes_range = fes.cv1_fes_range
        if self.cvs >= 2:
            self.cv2min = fes.cv2min
            self.cv2max = fes.cv2max
            self.cv2_name = fes.cv2_name
            self.cv2per = fes.cv2per
            self.cv2_fes_range = fes.cv2_fes_range
        if self.cvs == 3:
            self.cv3min = fes.cv3min
            self.cv3max = fes.cv3max
            self.cv3_name = fes.cv3_name
            self.cv3per = fes.cv3per
            self.cv3_fes_range = fes.cv3_fes_range

        if precise:
            if energy_unit in ["kJ/mol", "kcal/mol"]:
                self.findminima2(temp=temp, energy_unit=energy_unit, max_iteration=max_iteration, print_output=print_output)
            else:
                print("Error: energy_unit must be either 'kJ/mol' or 'kcal/mol'. ")
        else:
            self.findminima(nbins=nbins)

    def _get_indexes(self, i=0, j=0, k=0):
        # returns array of indexes of the surroundings points of FES, respecting periodicity
        periodic = self.periodic
        if self.cvs == 1:
            a_indexes = np.empty((3))
        
            a_indexes[0] = np.array((i-1)) 
            a_indexes[1] = np.array((i))
            a_indexes[2] = np.array((i+1))
            
            if periodic[0]:
                if i == 0:
                    a_indexes[0] = (self.fes.shape[0]-1)
                elif i == self.fes.shape[0]-1:
                    a_indexes[2] = (0)
            
            # remove uninitialized parts in a_indexes - edges not periodic
            if i == 0 and not periodic[0]:
                a_indexes = a_indexes[1:]
            if i == self.fes.shape[0]-1 and not periodic[0]:
                a_indexes = a_indexes[:-1]
        
            return a_indexes.astype(int)
            
        elif self.cvs == 2:
            a_indexes = np.empty((3,3,2))
        
            a_indexes[0,:,0] = np.array((i-1,i-1,i-1)) 
            a_indexes[1,:,0] = np.array((i,i,i))
            a_indexes[2,:,0] = np.array((i+1,i+1,i+1))
            if i == 0:
                if periodic[0]:
                    a_indexes[0,:,0] = np.array((self.fes.shape[0]-1,self.fes.shape[0]-1,self.fes.shape[0]-1))
            elif i == self.fes.shape[0]-1:
                if periodic[0]:
                    a_indexes[2,:,0] = np.array((0,0,0))
        
            a_indexes[:,0,1] = np.array((j-1,j-1,j-1))
            a_indexes[:,1,1] = np.array((j,j,j))
            a_indexes[:,2,1] = np.array((j+1,j+1,j+1))
            
            if j == 0:
                if periodic[1]:
                    a_indexes[:,0,1] = np.array((self.fes.shape[1]-1,self.fes.shape[1]-1,self.fes.shape[1]-1))
            elif j == self.fes.shape[1]-1:
                if periodic[1]:
                    a_indexes[:,2,1] = np.array((0,0,0))
            
            # remove uninitialized parts in a_indexes - edges not periodic
            if i == 0 and not periodic[0]:
                a_indexes = a_indexes[1:,:,:]
            if i == self.fes.shape[0]-1 and not periodic[0]:
                a_indexes = a_indexes[:-1,:,:]
            if j == 0 and not periodic[1]:
                a_indexes = a_indexes[:,1:,:]
            if j == self.fes.shape[0]-1 and not periodic[1]:
                a_indexes = a_indexes[:,:-1,:]
        
            return a_indexes.astype(int)
        
        elif self.cvs == 3:
            a_indexes = np.empty((3,3,3,3))
        
            a_indexes[0,:,:,0] = np.ones((3,3)) * i-1
            a_indexes[1,:,:,0] = np.ones((3,3)) * i
            a_indexes[2,:,:,0] = np.ones((3,3)) * i+1
            if periodic[0]:
                if i == 0:
                    a_indexes[0,:,:,0] = np.ones((3,3)) * (self.fes.shape[0]-1)
                elif i == self.fes.shape[0]-1:
                    a_indexes[2,:,:,0] = np.zeros((3,3))
        
            a_indexes[:,0,:,1] = np.ones((3,3)) * j-1
            a_indexes[:,1,:,1] = np.ones((3,3)) * j
            a_indexes[:,2,:,1] = np.ones((3,3)) * j+1
            
            if j == 0:
                if periodic[1]:
                    a_indexes[:,0,:,1] = np.ones((3,3)) * (self.fes.shape[1]-1)
            elif j == self.fes.shape[1]-1:
                if periodic[1]:
                    a_indexes[:,2,:,1] = np.zeros((3,3))

            a_indexes[:,:,0,2] = np.ones((3,3)) * k-1
            a_indexes[:,:,1,2] = np.ones((3,3)) * k
            a_indexes[:,:,2,2] = np.ones((3,3)) * k+1
            
            if k == 0:
                if periodic[2]:
                    a_indexes[:,:,0,2] = np.ones((3,3)) * (self.fes.shape[2]-1)
            elif k == self.fes.shape[2]-1:
                if periodic[2]:
                    a_indexes[:,:,2,2] = np.zeros((3,3))
            
            # remove uninitialized parts in a_indexes - edges not periodic
            if i == 0 and not periodic[0]:
                a_indexes = a_indexes[1:,:,:,:]
            if i == self.fes.shape[0]-1 and not periodic[0]:
                a_indexes = a_indexes[:-1,:,:,:]
            if j == 0 and not periodic[1]:
                a_indexes = a_indexes[:,1:,:,:]
            if j == self.fes.shape[1]-1 and not periodic[1]:
                a_indexes = a_indexes[:,:-1,:,:]
            if k == 0 and not periodic[2]:
                a_indexes = a_indexes[:,:,1:,:]
            if k == self.fes.shape[2]-1 and not periodic[2]:
                a_indexes = a_indexes[:,:,:-1,:]
        return a_indexes
    
    def findminima2(self, temp=300.0, energy_unit="kJ/mol", max_iteration=10000, print_output=True):
        if self.cvs >= 1:
            cv1min = self.cv1min
            cv1max = self.cv1max
        if self.cvs >=2:
            cv2min = self.cv2min
            cv2max = self.cv2max 
        if self.cvs == 3:
            cv3min = self.cv3min
            cv3max = self.cv3max
        
        self.minima = []

        if print_output:
            print(f"Calculating gradients for FES with {self.fes.flatten().shape[0]} bins... ")
        
        if self.cvs == 1:
            m_fes = np.zeros((self.fes.shape))
            minima_count = 0
            dirs = np.zeros((self.fes.shape[0], 1))
            minima_list = []
            
            periodic=self.periodic
            
            for i in range(self.fes.shape[0]):
                a_indexes = self._get_indexes(i)
        
                okoli = np.zeros((a_indexes[:].shape))
                for ii in range(okoli.shape[0]):
                    okoli[ii] = self.fes[int(a_indexes[ii])]
                
                m_i = np.unravel_index(np.argmin(okoli), okoli.shape, order='C')
                
                dirs[i,0] = a_indexes[m_i]
                if self.fes[i] == self.fes[int(a_indexes[m_i])] and not np.all(okoli == np.min(okoli)):
                    minima_count += 1
                    m_fes[i] = minima_count
                    min_cv1 = (((i)/self.res)*(cv1max-cv1min))+cv1min
                    minima_list.append([self.fes[i],i, min_cv1])

            if print_output:
                print("Searching for the nearest local minima... ")
            iteration = 0
            while 0.0 in m_fes[:]:
                iteration += 1
                if iteration > max_iteration:
                    print("Warning: Maximum number of iterations reached when searching. ")
                    break
                for i in range(self.fes.shape[0]):
                    m_fes[i] = m_fes[int(dirs[i])]
            if iteration <= max_iteration and print_output:
                print("Done.")
            if 0.0 in m_fes[:]:
                if print_output:
                    print("Warning: some of the FES bins were not associated to any local minimum. ")

            self.minima = np.array(minima_list)
            self.m_fes = m_fes
            
            if energy_unit == "kJ/mol":
                prob = np.exp(-1000*self.fes/8.314/temp)
            elif energy_unit == "kcal_mol":
                prob = np.exp(-1000*4.184*self.fes/8.314/temp)

            m_sum_probabilities = np.zeros((self.minima[:,0].shape))
            for i in range(self.fes.shape[0]):
                if m_fes[i] > 0.0:
                    m_sum_probabilities[int(m_fes[i])-1] += prob[i]

            if energy_unit == "kJ/mol":
                m_energies = -8.314*temp*np.log(m_sum_probabilities)/1000
            elif energy_unit == "kcal_mol":
                m_energies = -8.314*temp*np.log(m_sum_probabilities)/1000/4.184

            m_energies = m_energies - np.min(m_energies)
            self.minima[:,0] = m_energies
            
        elif self.cvs == 2:
            m_fes = np.zeros((self.fes.shape))
            minima_count = 0
            dirs = np.zeros((self.fes.shape[0], self.fes.shape[1], 2))
            minima_list = []
            
            periodic=self.periodic
            
            for i in range(self.fes.shape[0]):
                for j in range(self.fes.shape[1]):
                    a_indexes = self._get_indexes(i,j)
            
                    okoli = np.zeros((a_indexes[:,:,0].shape))
                    for ii in range(okoli.shape[0]):
                        for jj in range(okoli.shape[1]):
                            okoli[ii,jj] = self.fes[int(a_indexes[ii,jj,0]),int(a_indexes[ii,jj,1])]
                    
                    m_i, m_j = np.unravel_index(np.argmin(okoli), okoli.shape, order='C')

                    if not np.all(okoli == np.min(okoli)):
                        dirs[i,j, 0] = a_indexes[m_i,m_j,0]
                        dirs[i,j, 1] = a_indexes[m_i,m_j,1]
                        if self.fes[i,j] == self.fes[int(a_indexes[m_i,m_j,0]), int(a_indexes[m_i, m_j, 1])]:
                            minima_count += 1
                            m_fes[i,j] = minima_count
                            min_cv1 = (((i)/self.res)*(cv1max-cv1min))+cv1min
                            min_cv2 = (((j)/self.res)*(cv2max-cv2min))+cv2min
                            minima_list.append([self.fes[i,j],i,j, min_cv1, min_cv2])
                    else:
                        dirs[i,j, 0] = i
                        dirs[i,j, 1] = j

            if print_output:
                print("Searching for the nearest local minima... ")
            iteration = 0
            while 0.0 in m_fes[:,:]:
                iteration += 1
                if iteration > max_iteration:
                    if print_output:
                        print("Warning: Maximum number of iterations reached when searching. ")
                    break
                for i in range(self.fes.shape[0]):
                    for j in range(self.fes.shape[1]):
                        m_fes[i,j] = m_fes[int(dirs[i,j,0]), int(dirs[i,j,1])]
            if iteration <= max_iteration:
                if print_output:
                    print("Done.")  
            if 0.0 in m_fes[:,:]:
                if print_output:
                    print("Warning: some of the FES bins were not associated to any local minimum. ")
            
            self.minima = np.array(minima_list)
            self.m_fes = m_fes
            
            if energy_unit == "kJ/mol":
                prob = np.exp(-1000*self.fes/8.314/temp)
            elif energy_unit == "kcal_mol":
                prob = np.exp(-1000*4.184*self.fes/8.314/temp)

            m_sum_probabilities = np.zeros((self.minima[:,0].shape))
            for i in range(self.fes.shape[0]):
                for j in range(self.fes.shape[1]):
                    if m_fes[i,j] > 0.0:
                        m_sum_probabilities[int(m_fes[i,j])-1] += prob[i,j]

            if energy_unit == "kJ/mol":
                m_energies = -8.314*temp*np.log(m_sum_probabilities)/1000
            elif energy_unit == "kcal_mol":
                m_energies = -8.314*temp*np.log(m_sum_probabilities)/1000/4.184

            m_energies = m_energies - np.min(m_energies)
            self.minima[:,0] = m_energies
            
        elif self.cvs == 3:
            m_fes = np.zeros((self.fes.shape))
            minima_count = 0
            dirs = np.zeros((self.fes.shape[0], self.fes.shape[1], self.fes.shape[2],3))
            minima_list = []
            
            periodic=self.periodic
            
            for i in range(self.fes.shape[0]):
                for j in range(self.fes.shape[1]):
                    for k in range(self.fes.shape[2]):
                        a_indexes = self._get_indexes(i,j,k)
                        
                        okoli = np.zeros((a_indexes[:,:,:,0].shape))
                        for ii in range(okoli.shape[0]):
                            for jj in range(okoli.shape[1]):
                                for kk in range(okoli.shape[2]):
                                    okoli[ii,jj, kk] = self.fes[int(a_indexes[ii,jj,kk,0]),int(a_indexes[ii,jj,kk,1]),int(a_indexes[ii,jj,kk,2])]
                        
                        m_i, m_j, m_k = np.unravel_index(np.argmin(okoli), okoli.shape, order='C')
                        
                        dirs[i,j,k,0] = a_indexes[m_i,m_j,m_k,0]
                        dirs[i,j,k,1] = a_indexes[m_i,m_j,m_k,1]
                        dirs[i,j,k,2] = a_indexes[m_i,m_j,m_k,2]
                        if self.fes[i,j,k] == self.fes[int(a_indexes[m_i,m_j,m_k,0]), int(a_indexes[m_i, m_j,m_k,1]), int(a_indexes[m_i, m_j,m_k,2])] and not np.all(okoli == np.min(okoli)):
                            minima_count += 1
                            m_fes[i,j,k] = minima_count
                            min_cv1 = (((i)/self.res)*(cv1max-cv1min))+cv1min
                            min_cv2 = (((j)/self.res)*(cv2max-cv2min))+cv2min
                            min_cv3 = (((k)/self.res)*(cv3max-cv3min))+cv3min
                            minima_list.append([self.fes[i,j,k],i,j,k,min_cv1,min_cv2,min_cv3])

            if print_output:
                print("Searching for the nearest local minima... ")
            iteration = 0
            while 0.0 in m_fes[:,:,:]:
                iteration += 1
                if iteration > max_iteration:
                    print("Warning: Maximum number of iterations reached when searching. ")
                    break
                for i in range(self.fes.shape[0]):
                    for j in range(self.fes.shape[1]):
                        for k in range(self.fes.shape[2]):
                            m_fes[i,j,k] = m_fes[int(dirs[i,j,k,0]), int(dirs[i,j,k,1]),int(dirs[i,j,k,2])]
            if iteration <= max_iteration:
                if print_output:
                    print("Done.")
            if 0.0 in m_fes[:,:,:]:
                if print_output:
                    print("Warning: some of the FES bins were not associated to any local minimum. ")

            self.minima = np.array(minima_list)
            self.m_fes = m_fes
            
            if energy_unit == "kJ/mol":
                prob = np.exp(-1000*self.fes/8.314/temp)
            elif energy_unit == "kcal_mol":
                prob = np.exp(-1000*4.184*self.fes/8.314/temp)

            m_sum_probabilities = np.zeros((self.minima[:,0].shape))
            for i in range(self.fes.shape[0]):
                for j in range(self.fes.shape[1]):
                    for k in range(self.fes.shape[2]):
                        if m_fes[i,j,k] > 0.0:
                            m_sum_probabilities[int(m_fes[i,j,k])-1] += prob[i,j,k]

            if energy_unit == "kJ/mol":
                m_energies = -8.314*temp*np.log(m_sum_probabilities)/1000
            elif energy_unit == "kcal_mol":
                m_energies = -8.314*temp*np.log(m_sum_probabilities)/1000/4.184

            m_energies = m_energies - np.min(m_energies)
            self.minima[:,0] = m_energies
        else:
            print("Fes object has unsupported number of CVs.")

        
        if len(self.minima.shape)>1:
            self.minima = self.minima[self.minima[:, 0].argsort()]

            # renumber minima indexes in m_fes according to their energies (and letters)
            new_m_fes = np.zeros((self.fes.shape))
            
            for m in range(self.minima.shape[0]):
                if self.cvs == 1:
                    m_old_id = self.m_fes[int(float(self.minima[m,1]))]
                if self.cvs == 2:
                    m_old_id = self.m_fes[int(float(self.minima[m,1])), int(float(self.minima[m,2]))]
                if self.cvs == 3:
                    m_old_id = self.m_fes[int(float(self.minima[m,1])), int(float(self.minima[m,2])), int(float(self.minima[m,3]))]
                new_m_fes[self.m_fes == m_old_id] = m + 1
            self.m_fes = new_m_fes

        
        letters = list(map(chr, range(65, 91)))
        for letter1 in range(65, 91):
            for letter2 in range(65, 91):
                letters.append(f"{chr(letter1)}{chr(letter2)}")
        if len(self.minima.shape) > 1:
            if self.minima.shape[0] < len(letters):
                self.minima = np.column_stack((letters[0:self.minima.shape[0]],self.minima))
            else:
                print("Error: Too many minima to assign letters. Try using keyword precise=False. ")
                return None
        elif len(self.minima.shape) == 1:
            self.minima = np.append("A", self.minima)
        
        if self.cvs == 1:
            if len(self.minima.shape)>1:
                self.minima = pd.DataFrame(np.array(self.minima), columns = ["Minimum", "free energy", "CV1bin", "CV1 - "+self.cv1_name])
            elif len(self.minima.shape) == 1:
                self.minima = pd.DataFrame([self.minima], columns = ["Minimum", "free energy", "CV1bin", "CV1 - "+self.cv1_name])
                
        elif self.cvs == 2:
            if len(self.minima.shape)>1:
                self.minima = pd.DataFrame(np.array(self.minima), columns = ["Minimum", "free energy", "CV1bin", "CV2bin", 
                                                               "CV1 - "+self.cv1_name, "CV2 - "+self.cv2_name])
            elif len(self.minima.shape) == 1:
                self.minima = pd.DataFrame([self.minima], columns = ["Minimum", "free energy", "CV1bin", "CV2bin", 
                                                               "CV1 - "+self.cv1_name, "CV2 - "+self.cv2_name])
        elif self.cvs == 3:
            if len(self.minima.shape)>1:
                self.minima = pd.DataFrame(np.array(self.minima), columns = ["Minimum", "free energy", "CV1bin", "CV2bin", "CV3bin", 
                                                               "CV1 - "+self.cv1_name, "CV2 - "+self.cv2_name,  "CV3 - "+self.cv3_name])
            elif len(self.minima.shape) == 1:
                self.minima = pd.DataFrame([self.minima], columns = ["Minimum", "free energy", "CV1bin", "CV2bin", "CV3bin", 
                                                               "CV1 - "+self.cv1_name, "CV2 - "+self.cv2_name,  "CV3 - "+self.cv3_name])


    def findminima(self, nbins=8, print_output=True):
        if int(nbins) != nbins:
            nbins = int(nbins)
            print(f"Number of bins must be an integer, it will be set to {nbins}.")
        if self.res%nbins != 0:
            print("Error: Resolution of FES must be divisible by number of bins.")
            return None
        if nbins > self.res/2:
            print("Error: Number of bins is too high.")
            return None
        bin_size = int(self.res/nbins)

        if self.cvs >= 1:
            cv1min = self.cv1min
            cv1max = self.cv1max
        if self.cvs >=2:
            cv2min = self.cv2min
            cv2max = self.cv2max 
        if self.cvs == 3:
            cv3min = self.cv3min
            cv3max = self.cv3max
        
        self.minima = []
        if self.cvs == 1:
            for bin1 in range(0,nbins):
                fes_slice = self.fes[bin1*bin_size:(bin1+1)*bin_size]
                bin_min = np.min(fes_slice)
                argmin = np.argmin(fes_slice)
                # indexes of global minimum of a bin
                bin_min_arg_cv1 = int(argmin%bin_size)
                # indexes of that minima in the original fes (indexes +1)
                min_cv1_b = int(bin_min_arg_cv1+bin1*bin_size)
                if (bin_min_arg_cv1 > 0 and bin_min_arg_cv1<(bin_size-1)):
                    min_cv1 = (((min_cv1_b)/self.res)*(cv1max-cv1min))+cv1min#min_cv1 = (((min_cv1_b+0.5)/self.res)*(cv1max-cv1min))+cv1min
                    if len(self.minima) == 0:
                        self.minima = np.array([round(bin_min, 6), int(min_cv1_b), round(min_cv1, 6)])
                    else:
                        self.minima = np.vstack((self.minima, np.array([round(bin_min, 6), int(min_cv1_b), round(min_cv1, 6)])))
                else:
                    around = []
                    min_cv1_b_low = min_cv1_b - 1
                    if min_cv1_b_low == -1:
                        if self.periodic[0]:
                            min_cv1_b_low = self.res - 1
                        else:
                            min_cv1_b_low = float("nan")

                    min_cv1_b_high = min_cv1_b + 1
                    if min_cv1_b_high == self.res:
                        if self.periodic[0]:
                            min_cv1_b_high = 0
                        else:
                            min_cv1_b_high = float("nan")

                    #1_b_low
                    if not(np.isnan(min_cv1_b_low)):
                        around.append(self.fes[min_cv1_b_low])
                    #1_b_high
                    if not(np.isnan(min_cv1_b_high)):
                        around.append(self.fes[min_cv1_b_high])
                    
                    if bin_min < np.min(around):
                        min_cv1 = (((min_cv1_b)/self.res)*(cv1max-cv1min))+cv1min#min_cv1 = (((min_cv1_b+0.5)/self.res)*(cv1max-cv1min))+cv1min
                        if len(self.minima) == 0:
                            self.minima=np.array([round(bin_min, 6), int(min_cv1_b), round(min_cv1, 6)])
                        else:
                            self.minima=np.vstack((self.minima, np.array([round(bin_min, 6), int(min_cv1_b), round(min_cv1, 6)])))
            
        elif self.cvs == 2:
            for bin1 in range(0,nbins):
                for bin2 in range(0,nbins):
                    fes_slice = self.fes[bin1*bin_size:(bin1+1)*bin_size,
                                         bin2*bin_size:(bin2+1)*bin_size]
                    bin_min = np.min(fes_slice)
                    argmin = np.argmin(fes_slice)
                    # indexes of global minimum of a bin
                    bin_min_arg = np.unravel_index(np.argmin(fes_slice), fes_slice.shape)
                    # indexes of that minima in the original fes (indexes +1)
                    min_cv1_b = int(bin_min_arg[0]+bin1*bin_size)
                    min_cv2_b = int(bin_min_arg[1]+bin2*bin_size)
                    if (bin_min_arg[0] > 0 and bin_min_arg[0]<(bin_size-1)) \
                                    and (bin_min_arg[1] > 0 and bin_min_arg[1]<(bin_size-1)):
                        min_cv1 = (((min_cv1_b)/self.res)*(cv1max-cv1min))+cv1min#min_cv1 = (((min_cv1_b+0.5)/self.res)*(cv1max-cv1min))+cv1min
                        min_cv2 = (((min_cv2_b)/self.res)*(cv2max-cv2min))+cv2min#min_cv1 = (((min_cv1_b+0.5)/self.res)*(cv1max-cv1min))+cv1min
                        if len(self.minima) == 0:
                            self.minima=np.array([round(bin_min, 6), int(min_cv1_b),\
                                                  int(min_cv2_b), round(min_cv1, 6), round(min_cv2, 6)])
                        else:
                            self.minima=np.vstack((self.minima, np.array([round(bin_min, 6), int(min_cv1_b), \
                                                                          int(min_cv2_b), round(min_cv1, 6), round(min_cv2, 6)])))
                    else:
                        around = []
                        min_cv1_b_low = min_cv1_b - 1
                        if min_cv1_b_low == -1:
                            if self.periodic[0]:
                                min_cv1_b_low = self.res - 1
                            else:
                                min_cv1_b_low = float("nan")

                        min_cv1_b_high = min_cv1_b + 1
                        if min_cv1_b_high == self.res:
                            if self.periodic[0]:
                                min_cv1_b_high = 0
                            else:
                                min_cv1_b_high = float("nan")

                        min_cv2_b_low = min_cv2_b - 1
                        if min_cv2_b_low == -1:
                            if self.periodic[0]:
                                min_cv2_b_low = self.res - 1
                            else:
                                min_cv2_b_low = float("nan")

                        min_cv2_b_high = min_cv2_b + 1
                        if min_cv2_b_high == self.res:
                            if self.periodic[0]:
                                min_cv2_b_high = 0
                            else:
                                min_cv2_b_high = float("nan")
                        #1_b_low
                        if not(np.isnan(min_cv1_b_low)):
                            if not(np.isnan(min_cv2_b_low)):
                                around.append(self.fes[min_cv1_b_low, min_cv2_b_low])
                            around.append(self.fes[min_cv1_b_low,min_cv2_b])
                            if not(np.isnan(min_cv2_b_high)):
                                around.append(self.fes[min_cv1_b_low, min_cv2_b_high])
                        #1_b
                        if not(np.isnan(min_cv2_b_low)):
                            around.append(self.fes[min_cv1_b, min_cv2_b_low])
                        if not(np.isnan(min_cv2_b_high)):
                            around.append(self.fes[min_cv1_b, min_cv2_b_high])
                        #1_b_high
                        if not(np.isnan(min_cv1_b_high)):
                            if not(np.isnan(min_cv2_b_low)):
                                around.append(self.fes[min_cv1_b_high, min_cv2_b_low])
                            around.append(self.fes[min_cv1_b_high, min_cv2_b])
                            if not(np.isnan(min_cv2_b_high)):
                                around.append(self.fes[min_cv1_b_high, min_cv2_b_high])
                        if bin_min < np.min(around):
                            min_cv1 = (((min_cv1_b)/self.res)*(cv1max-cv1min))+cv1min#min_cv1 = (((min_cv1_b+0.5)/self.res)*(cv1max-cv1min))+cv1min
                            min_cv2 = (((min_cv2_b)/self.res)*(cv2max-cv2min))+cv2min#min_cv1 = (((min_cv1_b+0.5)/self.res)*(cv1max-cv1min))+cv1min
                            if len(self.minima) == 0:
                                self.minima=np.array([round(bin_min, 6), int(min_cv1_b), int(min_cv2_b), \
                                                      round(min_cv1, 6), round(min_cv2, 6)])
                            else:
                                self.minima=np.vstack((self.minima, np.array([round(bin_min, 6), int(min_cv1_b), \
                                                                              int(min_cv2_b), round(min_cv1, 6), round(min_cv2, 6)])))
        elif self.cvs == 3:
            for bin1 in range(0,nbins):
                for bin2 in range(0,nbins):
                    for bin3 in range(0, nbins):
                        fes_slice = self.fes[bin1*bin_size:(bin1+1)*bin_size,
                                             bin2*bin_size:(bin2+1)*bin_size, 
                                             bin3*bin_size:(bin3+1)*bin_size]
                        bin_min = np.min(fes_slice)
                        argmin = np.argmin(fes_slice)
                        # indexes of global minimum of a bin
                        bin_min_arg = np.unravel_index(np.argmin(fes_slice), fes_slice.shape)
                        # indexes of that minima in the original fes (indexes +1)
                        min_cv1_b = int(bin_min_arg[0]+bin1*bin_size)
                        min_cv2_b = int(bin_min_arg[1]+bin2*bin_size)
                        min_cv3_b = int(bin_min_arg[2]+bin3*bin_size)
                        if (bin_min_arg[0] > 0 and bin_min_arg[0]<(bin_size-1)) \
                                        and (bin_min_arg[1] > 0 and bin_min_arg[1]<(bin_size-1))\
                                        and (bin_min_arg[2] > 0 and bin_min_arg[2]<(bin_size-1)):
                            min_cv1 = (((min_cv1_b)/self.res)*(cv1max-cv1min))+cv1min #min_cv1 = (((min_cv1_b+0.5)/self.res)*(cv1max-cv1min))+cv1min
                            min_cv2 = (((min_cv2_b)/self.res)*(cv2max-cv2min))+cv2min #min_cv2 = (((min_cv2_b+0.5)/self.res)*(cv2max-cv2min))+cv2min
                            min_cv3 = (((min_cv3_b)/self.res)*(cv3max-cv3min))+cv3min #min_cv3 = (((min_cv3_b+0.5)/self.res)*(cv3max-cv3min))+cv3min
                            if len(self.minima) == 0:
                                self.minima=np.array([round(bin_min, 6), int(min_cv1_b),\
                                                      int(min_cv2_b), int(min_cv3_b), round(min_cv1, 6), \
                                                      round(min_cv2, 6), round(min_cv3, 6)])
                            else:
                                self.minima=np.vstack((self.minima, np.array([round(bin_min, 6), int(min_cv1_b),\
                                                      int(min_cv2_b), int(min_cv3_b), round(min_cv1, 6), \
                                                      round(min_cv2, 6), round(min_cv3, 6)])))
                        else:
                            around = []
                            min_cv1_b_low = min_cv1_b - 1
                            if min_cv1_b_low == -1:
                                if self.periodic[0]:
                                    min_cv1_b_low = self.res - 1
                                else:
                                    min_cv1_b_low = float("nan")

                            min_cv1_b_high = min_cv1_b + 1
                            if min_cv1_b_high == self.res:
                                if self.periodic[0]:
                                    min_cv1_b_high = 0
                                else:
                                    min_cv1_b_high = float("nan")

                            min_cv2_b_low = min_cv2_b - 1
                            if min_cv2_b_low == -1:
                                if self.periodic[0]:
                                    min_cv2_b_low = self.res - 1
                                else:
                                    min_cv2_b_low = float("nan")

                            min_cv2_b_high = min_cv2_b + 1
                            if min_cv2_b_high == self.res:
                                if self.periodic[0]:
                                    min_cv2_b_high = 0
                                else:
                                    min_cv2_b_high = float("nan")
                                                       
                            min_cv3_b_low = min_cv3_b - 1
                            if min_cv3_b_low == -1:
                                if self.periodic[2]:
                                    min_cv3_b_low = self.res - 1
                                else:
                                    min_cv3_b_low = float("nan")

                            min_cv3_b_high = min_cv3_b + 1
                            if min_cv3_b_high == self.res:
                                if self.periodic[2]:
                                    min_cv3_b_high = 0
                                else:
                                    min_cv3_b_high = float("nan")

#cv3_b
                            #1_b_low
                            if not(np.isnan(min_cv1_b_low)):
                                if not(np.isnan(min_cv2_b_low)):
                                    around.append(self.fes[min_cv1_b_low,min_cv2_b_low,min_cv3_b])
                                around.append(self.fes[min_cv1_b_low,min_cv2_b,min_cv3_b])
                                if not(np.isnan(min_cv2_b_high)):
                                    around.append(self.fes[min_cv1_b_low,min_cv2_b_high,min_cv3_b])
                            #1_b
                            if not(np.isnan(min_cv2_b_low)):
                                around.append(self.fes[min_cv1_b,min_cv2_b_low,min_cv3_b])
                            if not(np.isnan(min_cv2_b_high)):
                                around.append(self.fes[min_cv1_b,min_cv2_b_high,min_cv3_b])
                            #1_b_high
                            if not(np.isnan(min_cv1_b_high)):
                                if not(np.isnan(min_cv2_b_low)):
                                    around.append(self.fes[min_cv1_b_high,min_cv2_b_low,min_cv3_b])
                                around.append(self.fes[min_cv1_b_high,min_cv2_b,min_cv3_b])
                                if not(np.isnan(min_cv2_b_high)):
                                    around.append(self.fes[min_cv1_b_high,min_cv2_b_high,min_cv3_b])
                           
                            if not(np.isnan(min_cv3_b_low)):
                            #1_b_low
                                if not(np.isnan(min_cv1_b_low)):
                                    if not(np.isnan(min_cv2_b_low)):
                                        around.append(self.fes[min_cv1_b_low,min_cv2_b_low,min_cv3_b_low])
                                    around.append(self.fes[min_cv1_b_low,min_cv2_b,min_cv3_b_low])
                                    if not(np.isnan(min_cv2_b_high)):
                                        around.append(self.fes[min_cv1_b_low,min_cv2_b_high,min_cv3_b_low])
                                #1_b
                                if not(np.isnan(min_cv2_b_low)):
                                    around.append(self.fes[min_cv1_b,min_cv2_b_low,min_cv3_b_low])
                                if not(np.isnan(min_cv2_b_high)):
                                    around.append(self.fes[min_cv1_b,min_cv2_b_high,min_cv3_b_low])
                                #1_b_high
                                if not(np.isnan(min_cv1_b_high)):
                                    if not(np.isnan(min_cv2_b_low)):
                                        around.append(self.fes[min_cv1_b_high,min_cv2_b_low,min_cv3_b_low])
                                    around.append(self.fes[min_cv1_b_high,min_cv2_b,min_cv3_b_low])
                                    if not(np.isnan(min_cv2_b_high)):
                                        around.append(self.fes[min_cv1_b_high,min_cv2_b_high,min_cv3_b_low])
                            
                            if not(np.isnan(min_cv2_b_high)):
                                #1_b_low
                                if not(np.isnan(min_cv1_b_low)):
                                    if not(np.isnan(min_cv2_b_low)):
                                        around.append(self.fes[min_cv1_b_low,min_cv2_b_low,min_cv3_b_high])
                                    around.append(self.fes[min_cv1_b_low,min_cv2_b,min_cv3_b_high])
                                    if not(np.isnan(min_cv2_b_high)):
                                        around.append(self.fes[min_cv1_b_low,min_cv2_b_high,min_cv3_b_high])
                                #1_b
                                if not(np.isnan(min_cv2_b_low)):
                                    around.append(self.fes[min_cv1_b,min_cv2_b_low,min_cv3_b_high])
                                if not(np.isnan(min_cv2_b_high)):
                                    around.append(self.fes[min_cv1_b,min_cv2_b_high,min_cv3_b_high])
                                #1_b_high
                                if not(np.isnan(min_cv1_b_high)):
                                    if not(np.isnan(min_cv2_b_low)):
                                        around.append(self.fes[min_cv1_b_high,min_cv2_b_low,min_cv3_b_high])
                                    around.append(self.fes[min_cv1_b_high,min_cv2_b,min_cv3_b_high])
                                    if not(np.isnan(min_cv2_b_high)):
                                        around.append(self.fes[min_cv1_b_high,min_cv2_b_high,min_cv3_b_high])
                            
                            if bin_min < np.min(around):
                                min_cv1 = (((min_cv1_b)/self.res)*(cv1max-cv1min))+cv1min #min_cv1 = (((min_cv1_b+0.5)/self.res)*(cv1max-cv1min))+cv1min
                                min_cv2 = (((min_cv2_b)/self.res)*(cv2max-cv2min))+cv2min #min_cv2 = (((min_cv2_b+0.5)/self.res)*(cv2max-cv2min))+cv2min
                                min_cv3 = (((min_cv3_b)/self.res)*(cv3max-cv3min))+cv3min #min_cv3 = (((min_cv3_b+0.5)/self.res)*(cv3max-cv3min))+cv3min
                                if len(self.minima) == 0:
                                    self.minima=np.array([round(bin_min, 6), int(min_cv1_b),\
                                                      int(min_cv2_b), int(min_cv3_b), round(min_cv1, 6), \
                                                      round(min_cv2, 6), round(min_cv3, 6)])
                                else:
                                    self.minima=np.vstack((self.minima, np.array([round(bin_min, 6), int(min_cv1_b),\
                                                      int(min_cv2_b), int(min_cv3_b), round(min_cv1, 6), \
                                                      round(min_cv2, 6), round(min_cv3, 6)])))
        
        else:
            print("Fes object has unsupported number of CVs.")
        
        if len(self.minima.shape)>1:
            self.minima = self.minima[self.minima[:, 0].argsort()]

        letters = list(map(chr, range(65, 91)))
        for letter1 in range(65, 91):
            for letter2 in range(65, 91):
                letters.append(f"{chr(letter1)}{chr(letter2)}")
        if len(self.minima.shape)>1:
            if self.minima.shape[1] < len(letters):
                self.minima = np.column_stack((letters[0:self.minima.shape[0]],self.minima))
            else:
                print("Error: Too many minima to assign letters.")
        elif len(self.minima.shape) == 1:
            self.minima = np.append("A", self.minima)
        
        if self.cvs == 1:
            if len(self.minima.shape)>1:
                self.minima = pd.DataFrame(np.array(self.minima), columns = ["Minimum", "free energy", "CV1bin", "CV1 - "+self.cv1_name])
            elif len(self.minima.shape) == 1:
                self.minima = pd.DataFrame([self.minima], columns = ["Minimum", "free energy", "CV1bin", "CV1 - "+self.cv1_name])
                
        elif self.cvs == 2:
            if len(self.minima.shape)>1:
                self.minima = pd.DataFrame(np.array(self.minima), columns = ["Minimum", "free energy", "CV1bin", "CV2bin", 
                                                               "CV1 - "+self.cv1_name, "CV2 - "+self.cv2_name])
            elif len(self.minima.shape) == 1:
                self.minima = pd.DataFrame([self.minima], columns = ["Minimum", "free energy", "CV1bin", "CV2bin", 
                                                               "CV1 - "+self.cv1_name, "CV2 - "+self.cv2_name])
        elif self.cvs == 3:
            if len(self.minima.shape)>1:
                self.minima = pd.DataFrame(np.array(self.minima), columns = ["Minimum", "free energy", "CV1bin", "CV2bin", "CV3bin", 
                                                               "CV1 - "+self.cv1_name, "CV2 - "+self.cv2_name,  "CV3 - "+self.cv3_name])
            elif len(self.minima.shape) == 1:
                self.minima = pd.DataFrame([self.minima], columns = ["Minimum", "free energy", "CV1bin", "CV2bin", "CV3bin", 
                                                               "CV1 - "+self.cv1_name, "CV2 - "+self.cv2_name,  "CV3 - "+self.cv3_name])
        

    def plot(self, png_name=None, contours=True, contours_spacing=0.0, aspect = 1.0, cmap = "RdYlBu_r", 
                 energy_unit="kJ/mol", xlabel=None, ylabel=None, zlabel=None, label_size=12, clabel_size = 12, image_size=None, image_size_unit="in", dpi=100, color=None, vmin = 0, vmax = None, opacity=0.2, levels=None, show_points=True, point_size=4.0, title = None, off_screen = False, xlim=[None, None], ylim=[None, None], return_fig=False):
        """
        The same function as for visualizing Fes objects, but this time 
        with the positions of local minima shown as letters on the graph.
        
        ```python
        minima.plot(png_name="minima.png")
        ```
        
        Parameters:
        
        * png_name = String. If this parameter is supplied, the picture of FES will be saved under this name to the current working directory.
        
        * contours (default=True) = whether contours should be shown on 2D FES
        
        * contours_spacing (default=0.0) = when a positive number is set, it will be used as spacing for contours on 2D FES. 
                Otherwise, if contours=True, there will be five equally spaced contour levels.
        
        * aspect (default = 1.0) = aspect ratio of the graph. Works with 1D and 2D FES. 
        
        * cmap (default = "RdYlBu_r") = Matplotlib colormap used to color 2D or 3D FES
        
        * energy_unit (default="kJ/mol") = String, used in description of colorbar

        * xlim, ylim (default = [None, None] for both) = list of two values specifying the range of x and y axes respectively. 
        None means that Matplotlib will choose appropriate range, so this keyword is useful when you need to overwrite it. 
        
        * xlabel, ylabel, zlabel = Strings, if provided, they will be used as labels for the graphs
        
        * label_size, clabel_size (default = 12) = size of text in labels or contours labels, respectively
        
        * image_size (default = [9,6]) = List of the width and height of the picture

        * image_size_unit (default = "in") = Units for width and height of the picture, accepts "in" as inches, "cm", "mm" or "px" as pixels.

        * dpi (default = 100) = DPI of the resulting image. 
        
        * color = string = name of color in matplotlib, if set, the color will be used for the letters. 
                If not set, the color should be automatically either black or white, 
                depending on what will be better visible on given place on FES with given colormap (for 2D FES).
        
        * vmin (default=0) = real number, lower bound for the colormap on 2D FES
        
        * vmax = real number, upper bound for the colormap on 2D FES
        
        * opacity (default=0.2) = number between 0 and 1, is the opacity of isosurfaces of 3D FES
        
        * levels = Here you can specify list of free energy values for isosurfaces on 3D FES. 
                If not provided, default values from contours parameters will be used instead. 
        
        * show_points (default=True) = boolean, tells if points should be visualized too, instead of just the letters. Only on 3D FES. 
        
        * point_size (default=4.0) = float, sets the size of points if show_points=True

        * title = optional, string that defines the title of the graph

        * offscreen (default = False) = for 3D FES only, grapf will not be shown after creation - used internally when making animations

        * return_fig (default=False) = whether the method should return the Matplotlib.Pyplot.figure object for further use. 
        In the case of plotting 3D FES, it returns Pyvista.plotter object instead. 
        """
        
        if vmax == None:
            vmax = np.max(self.fes)+0.01 # if the addition is smaller than 0.01, the 3d plot stops working. 
            
        if contours_spacing == 0.0:
            contours_spacing = (vmax-vmin)/5.0
        
        cmap = cm.get_cmap(cmap)
        
        cmap.set_over("white")
        cmap.set_under("white")
        
        color_set = True
        if color == None:
            color_set = False
        
        if self.cvs >= 1:
            cv1min = self.cv1min
            cv1max = self.cv1max 
        if self.cvs >=2:
            cv2min = self.cv2min
            cv2max = self.cv2max
        if self.cvs == 3:
            cv3min = self.cv3min
            cv3max = self.cv3max

        if image_size == None:
            image_size = [9,6]
        
        if image_size_unit == "cm":
            image_size[0] /= 2.54
            image_size[1] /= 2.54
        elif image_size_unit == "mm":
            image_size[0] /= 25.4
            image_size[1] /= 25.4
        elif image_size_unit == "px":
            image_size[0] /= dpi
            image_size[1] /= dpi
        elif image_size_unit != "in":
            print(f"Warning: unknown image_size_unit value: {image_size_unit}. Using inches instead. ")
            
        if self.cvs == 1:
            fig = plt.figure(figsize=(image_size[0],image_size[1]), dpi=dpi)
            X = np.linspace(cv1min, cv1max, self.res)
            plt.plot(X, self.fes)
            
            if not color_set:
                color = "black"
            
            fesrange = np.max(self.fes) - np.min(self.fes)
            
            if self.minima.shape[0] == 1:
                plt.text(float(self.minima.iloc[0,3]), float(self.minima.iloc[0,1])+fesrange*0.05, self.minima.iloc[0,0],
                             fontsize=label_size, horizontalalignment='center',
                             verticalalignment='bottom', c=color)
            elif self.minima.shape[0] > 1:
                for m in range(len(self.minima.iloc[:,0])):
                    plt.text(float(self.minima.iloc[m,3]), float(self.minima.iloc[m,1])+fesrange*0.05, self.minima.iloc[m,0],
                             fontsize=label_size, horizontalalignment='center',
                             verticalalignment='bottom', c=color)
            
            if xlabel == None:
                plt.xlabel(f'CV1 - {self.cv1_name}', size=label_size)
            else:
                plt.xlabel(xlabel, size=label_size)
            if ylabel == None:
                plt.ylabel(f'free energy ({energy_unit})', size=label_size)
            else:
                plt.ylabel(ylabel, size=label_size)
            
            if title != None:
                plt.title(title, size=label_size)
    
            plt.xticks(fontsize=label_size)
            plt.yticks(fontsize=label_size)

            ax = plt.gca()
            ax.set_xlim(xlim)
            ax.set_ylim(ylim)
        
            if png_name != None:
                plt.savefig(png_name, bbox_inches = 'tight')

            if return_fig:
                return fig
                
            
        elif self.cvs == 2:
            fig = plt.figure(figsize=(image_size[0],image_size[1]), dpi=dpi)
            plt.imshow(np.rot90(self.fes, axes=(0,1)), cmap=cmap, interpolation='nearest', 
                       extent=[cv1min, cv1max, cv2min, cv2max], 
                       aspect = (((cv1max-cv1min)/(cv2max-cv2min))/(aspect)),
                       vmin = vmin, vmax = vmax)
            cbar = plt.colorbar()
            cbar.ax.tick_params(labelsize=label_size) 
            cbar.set_label(energy_unit, size=label_size)

            if contours:
                if levels != None:
                    cont = plt.contour(np.rot90(self.fes, axes=(0,1)), 
                             levels = levels, 
                             extent=[cv1min, cv1max, cv2max, cv2min], 
                             colors = "k", linestyles = "solid")
                    plt.clabel(cont, levels = levels, fontsize=clabel_size)
                else:
                    cont = plt.contour(np.rot90(self.fes, axes=(0,1)), 
                             levels = np.arange(vmin, (vmax - 0.01), contours_spacing), 
                             extent=[cv1min, cv1max, cv2max, cv2min], 
                             colors = "k", linestyles = "solid")
                    plt.clabel(cont, levels = np.arange(vmin, (vmax - 0.01), contours_spacing), fontsize=clabel_size)
            
            if self.minima.shape[0] == 1:
                background = cmap((float(self.minima.iloc[0,1])-vmin)/(vmax-vmin))
                luma = background[0]*0.2126+background[1]*0.7152+background[3]*0.0722
                if luma > 0.6 and not color_set:
                    color = "black"
                elif luma <= 0.6 and not color_set:
                    color="white"
                plt.text(float(self.minima.iloc[0,4])+0.5*(cv1max-cv1min)/self.res, float(self.minima.iloc[0,5])+0.5*(cv2max-cv2min)/self.res, self.minima.iloc[0,0], fontsize=label_size, horizontalalignment='center',verticalalignment='center', c=color)
            elif self.minima.shape[0] > 1:
                for m in range(len(self.minima.iloc[:,0])):
                    background = cmap((float(self.minima.iloc[m,1])-vmin)/(vmax-vmin))
                    luma = background[0]*0.2126+background[1]*0.7152+background[3]*0.0722
                    if luma > 0.6 and not color_set:
                        color = "black"
                    elif luma <= 0.6 and not color_set:
                        color="white"
                    plt.text(float(self.minima.iloc[m,4])+0.5*(cv1max-cv1min)/self.res, float(self.minima.iloc[m,5])+0.5*(cv2max-cv2min)/self.res, self.minima.iloc[m,0], fontsize=label_size, horizontalalignment='center', verticalalignment='center', c=color)
            
            
            if xlabel == None:
                plt.xlabel(f'CV1 - {self.cv1_name}', size=label_size)
            else:
                plt.xlabel(xlabel, size=label_size)
            if ylabel == None:
                plt.ylabel(f'CV2 - {self.cv2_name}', size=label_size)
            else:
                plt.ylabel(ylabel, size=label_size)
            
            if title != None:
                plt.title(title, size=label_size)
    
            plt.xticks(fontsize=label_size)
            plt.yticks(fontsize=label_size)

            ax = plt.gca()
            ax.set_xlim(xlim)
            ax.set_ylim(ylim)

            ax.tick_params(axis='x', labelsize=label_size)
            ax.tick_params(axis='y', labelsize=label_size)
        
            if png_name != None:
                plt.savefig(png_name, bbox_inches = 'tight')

            if return_fig:
                return fig
            
            
        elif self.cvs == 3:
            if xlabel == None:
                xlabel = "CV1 - " + self.cv1_name
            if ylabel == None:
                ylabel = "CV2 - " + self.cv2_name
            if zlabel == None:
                zlabel = "CV3 - " + self.cv3_name
            
            min_ar = self.minima.iloc[:,5:8].values
            min_ar = min_ar.astype(np.float32)
            min_pv = pv.PolyData(min_ar)
            
            
            grid = pv.ImageData(
                dimensions=(self.res, self.res, self.res),
                spacing=((cv1max-cv1min)/self.res,(cv2max-cv2min)/self.res,(cv3max-cv3min)/self.res),
                origin=(cv1min, cv2min, cv3min)
            )
            grid["vol"] = self.fes.ravel(order="F")
            if levels == None:
                contours = grid.contour(np.arange(0, (vmax - 0.1), contours_spacing))
            else:
                contours = grid.contour(levels)
            fescolors = []
            for i in range(contours.points.shape[0]):
                fescolors.append(self.fes[int((contours.points[i,0]-cv1min)*self.res/(cv1max-cv1min)),
                                          int((contours.points[i,1]-cv2min)*self.res/(cv2max-cv2min)),
                                          int((contours.points[i,2]-cv3min)*self.res/(cv3max-cv3min))])
            #%% Visualization
            bounds = [cv1min, cv1max, cv2min, cv2max, cv3min, cv3max]
            pv.set_plot_theme('document')
            p = pv.Plotter()
            p.add_mesh(contours, scalars=fescolors, opacity=opacity, cmap=cmap, show_scalar_bar=False, interpolate_before_map=True)
            p.show_bounds(bounds=bounds, xtitle=xlabel, ytitle=ylabel, ztitle=zlabel, grid=True, location="outer")
            p.add_point_labels(min_pv, self.minima.iloc[:,0], 
                   show_points=show_points, always_visible = True, 
                   point_color="black", point_size=point_size, 
                   font_size=label_size, shape=None)
            
            if title != None:
                plt.title(title)
            if not off_screen:
                p.show()

            if png_name != None:
                p.save_graphic(png_name)

            if return_fig:
                return p
            

    def make_gif(self, gif_name=None, cmap = "RdYlBu_r", energy_unit="kJ/mol", 
                 xlabel=None, ylabel=None, zlabel=None, label_size=12, image_size=[10,7], 
                  opacity=0.2, levels=None, show_points=True, point_size=4.0, frames=64):
        """
        Function that generates animation of 3D FES showing different isosurfaces.
        
        ```python
        fes.make_gif(gif_name="FES.gif")
        ```
        
        Parameters:
        
        * gif_name (default="minima.gif") = String. Name of the gif that will be saved in the working directory.
        
        * cmap (default = "RdYlBu_r") = Matplotlib colormap used to color the 3D FES
        
        * xlabel, ylabel, zlabel = Strings, if provided, they will be used as labels for the graph
        
        * labelsize (default = 12) = size of text in labels
        
        * image_size (default = [10,7]) = List of the width and height of the picture
        
        * opacity (default = 0.2) = number between 0 and 1, is the opacity of isosurfaces of 3D FES
        
        * levels = Here you can specify list of free energy values for isosurfaces on 3D FES. 
                If not provided, default values from contours parameters will be used instead. 
        
        * frames (default = 64) = Number of frames the animation will be made of. 
        """
        if self.cvs == 3:
            values = np.linspace(np.min(self.fes)+0.1, np.max(self.fes), num=frames)
            grid = pv.ImageData(
                dimensions=(self.res, self.res, self.res),
                spacing=((self.cv1max-self.cv1min)/self.res,(self.cv2max-self.cv2min)/self.res,(self.cv3max-self.cv3min)/self.res),
                origin=(self.cv1min, self.cv2min, self.cv3min),
            )
            grid["vol"] = self.fes.ravel(order="F")
            surface = grid.contour(values[:1])
            surfaces = [grid.contour([v]) for v in values]
            surface = surfaces[0].copy()
            
            pv.set_plot_theme('document')
            plotter = pv.Plotter(off_screen=True)
            # Open a movie file
            if gif_name == None:
                gif_name = "minima_animation.gif"
            plotter.open_gif(gif_name)

            # Add initial mesh
            plotter.add_mesh(
                surface,
                opacity=0.3,
                clim=grid.get_data_range(),
                show_scalar_bar=False,
                cmap="RdYlBu_r"
            )
            plotter.add_mesh(grid.outline_corners(), color="k")
            
            if xlabel == None and ylabel == None and zlabel == None:
                plotter.show_grid(xtitle=f"CV1 - {self.cv1_name}", ytitle=f"CV2 - {self.cv2_name}", ztitle=f"CV3 - {self.cv3_name}")
            else:
                plotter.show_grid(xtitle=xlabel, ytitle=ylabel, ztitle=zlabel)
            if show_points:
                min_ar = self.minima.iloc[:,5:8].values
                min_ar = min_ar.astype(np.float32)
                min_pv = pv.PolyData(min_ar)
                plotter.add_point_labels(min_pv, self.minima.iloc[:,0], 
                               show_points=True, always_visible = True, 
                               pickable = True, point_color="black", 
                               point_size=4, font_size=label_size, shape=None)
            plotter.set_background('white')
            text = plotter.add_text(f"{values[0]:.2f}+kJ/mol", position='lower_right', font_size=label_size)
            plotter.show(auto_close=False)
            
            # Run through each frame
            for surf in range(len(surfaces)):
                surface.copy_from(surfaces[surf])
                plotter.remove_actor(text)
                text = plotter.add_text(f"{values[surf]:.2f} {energy_unit}", position='lower_right', font_size=label_size)
                plotter.write_frame()  # Write this frame
            # Run through backwards
            for surf in range(len(surfaces)-1,0,-1):
                surface.copy_from(surfaces[surf])
                plotter.remove_actor(text)
                text = plotter.add_text(f"{values[surf]:.2f} {energy_unit}", position='lower_right', font_size=12)
                plotter.write_frame()  # Write this frame

            # Be sure to close the plotter when finished
            plotter.close()
        else:
            print("Error: this method is only available for FES with 3 CVs.")


class FEProfile:
    """
    Free energy profile is a visualization of differences between local 
    minima points during metadynamics simulation. If the values seem 
    to converge to a mean value of the difference, it suggests, 
    but not fully prooves, that the obtained FES did converge to the correct shape.
    
    Command:
    ```python
    fep = metadynminer.FEProfile(minima, hillsfile)
    ```
    
    Parameters:
    
    * minima = metadynminer.Minima object
    
    * hillsfile = metadynminer.Hills object
    
    """
    
    __pdoc__["FEProfile.makefeprofile"] = False
    
    def __init__(self, minima, hills):
        self.cvs = minima.cvs
        self.res = minima.res
        self.minima = minima.minima
        self.periodic = minima.periodic
        self.heights = hills.get_heights()
        
        if self.cvs >= 1:
            self.cv1_name = minima.cv1_name
            self.cv1min = minima.cv1min
            self.cv1max = minima.cv1max
            self.cv1 = hills.get_cv1()
            self.s1 = hills.get_sigma1()
            self.cv1per = hills.get_cv1per()
            self.cv1_fes_range = minima.cv1_fes_range
        if self.cvs >= 2:
            self.cv2min = minima.cv2min
            self.cv2max = minima.cv2max
            self.cv2_name = minima.cv2_name
            self.cv2 = hills.get_cv2()
            self.s2 = hills.get_sigma2()
            self.cv2per = hills.get_cv2per()
            self.cv2_fes_range = minima.cv2_fes_range
        if self.cvs == 3:
            self.cv3min = minima.cv3min
            self.cv3max = minima.cv3max
            self.cv3_name = minima.cv3_name
            self.cv3 = hills.get_cv3()
            self.s3 = hills.get_sigma3()
            self.cv3per = hills.get_cv3per()
            self.cv3_fes_range = minima.cv3_fes_range
        
        if len(minima.minima.shape)>1:
            self.makefeprofile(hills)
        else: 
            print("There is only one local minimum on the free energy surface.")
    
    def makefeprofile(self, hills):
        """
        Internal method to calculate free energy profile. 
        """
        hillslenght = len(hills.get_cv1())
        
        if hillslenght < 256:
            profilelenght = hillslenght
            scantimes = np.array(range(hillslenght))
        else:
            profilelenght = 256
            scantimes = np.array(((hillslenght/(profilelenght))*np.array((range(1,profilelenght+1)))))
            scantimes[0:-1] -= 1
            #scantimes[-1] += 1
            scantimes = scantimes.astype(int)
        number_of_minima = self.minima.shape[0]
        
        self.feprofile = np.zeros((self.minima.Minimum.shape[0]+1))

        if self.cvs >= 1:
            cv1min = self.cv1min
            cv1max = self.cv1max         
        if self.cvs >= 2:
            cv2min = self.cv2min
            cv2max = self.cv2max           
        if self.cvs >= 3:     
            cv3min = self.cv3min
            cv3max = self.cv3max
        
        if self.cvs == 1:
            
            fes = np.zeros((self.res))
            
            lasttime = 0
            for time in scantimes:
                if time == scantimes[-1]:
                    time += 1
                for m in range(number_of_minima):#self.minima.iloc[:,3]
                    dist_cv1 = self.cv1[lasttime:time]-float(self.minima.iloc[m,3])
                    if self.periodic[0]:
                        dist_cv1[dist_cv1<-0.5*self.cv1_fes_range] += self.cv1_fes_range
                        dist_cv1[dist_cv1>+0.5*self.cv1_fes_range] -= self.cv1_fes_range

                    dp2 = dist_cv1**2/(2*self.s1[lasttime:time]**2)
                    tmp = np.zeros(dp2.shape)
                    heights = self.heights[lasttime:time]
                    tmp[dp2<6.25] = heights[dp2<6.25] * (np.exp(-dp2[dp2<6.25]) * 1.00193418799744762399 - 0.00193418799744762399)
                    #fes[int(float(self.minima.iloc[x,2]))] -= tmp.sum()
                    fes[int(float(self.minima.iloc[m,2]))] -= tmp.sum()
                #fes = fes - np.min(fes)
                profileline = [time-1]
                for m in range(number_of_minima):
                    profileline.append(fes[int(float(self.minima.iloc[m,2]))]-\
                                       fes[int(float(self.minima.iloc[0,2]))])
                self.feprofile = np.vstack([self.feprofile, profileline])

                lasttime = time
            
        elif self.cvs == 2:
            
            fes = np.zeros((self.res, self.res))
            
            lasttime = 0
            line = 0
            for time in scantimes:
                if time == scantimes[-1]:
                    time += 1
                for m in range(number_of_minima):
                    dist_cv1 = self.cv1[lasttime:time]-float(self.minima.iloc[m,4])
                    if self.periodic[0]:
                        dist_cv1[dist_cv1<-0.5*self.cv1_fes_range] += self.cv1_fes_range
                        dist_cv1[dist_cv1>+0.5*self.cv1_fes_range] -= self.cv1_fes_range
                    
                    dist_cv2 = self.cv2[lasttime:time]-float(self.minima.iloc[m,5])
                    if self.periodic[1]:
                        dist_cv2[dist_cv2<-0.5*self.cv2_fes_range] += self.cv2_fes_range
                        dist_cv2[dist_cv2>+0.5*self.cv2_fes_range] -= self.cv2_fes_range
                
                    dp2 = dist_cv1**2/(2*self.s1[lasttime:time]**2) + dist_cv2**2/(2*self.s2[lasttime:time]**2)
                    tmp = np.zeros(self.cv1[lasttime:time].shape)
                    heights = self.heights[lasttime:time]
                    tmp[dp2<6.25] = heights[dp2<6.25] * (np.exp(-dp2[dp2<6.25]) * 1.00193418799744762399 - 0.00193418799744762399)
                    fes[int(float(self.minima.iloc[m,2])),int(float(self.minima.iloc[m,3]))] -= tmp.sum()
                
                # save profile
                profileline = [time-1]
                for m in range(number_of_minima):
                    profileline.append(fes[int(float(self.minima.iloc[m,2])),int(float(self.minima.iloc[m,3]))]-\
                                       fes[int(float(self.minima.iloc[0,2])),int(float(self.minima.iloc[0,3]))])
                self.feprofile = np.vstack([self.feprofile, profileline])

                lasttime = time
        
        elif self.cvs == 3:
            
            fes = np.zeros((self.res, self.res, self.res))
            
            lasttime = 0
            for time in scantimes:
                if time == scantimes[-1]:
                    time += 1
                for m in range(number_of_minima):
                    dist_cv1 = self.cv1[lasttime:time]-float(self.minima.iloc[m,5])
                    if self.periodic[0]:
                        dist_cv1[dist_cv1<-0.5*self.cv1_fes_range] += self.cv1_fes_range
                        dist_cv1[dist_cv1>+0.5*self.cv1_fes_range] -= self.cv1_fes_range
                    
                    dist_cv2 = self.cv2[lasttime:time]-float(self.minima.iloc[m,6])
                    if self.periodic[1]:
                        dist_cv2[dist_cv2<-0.5*self.cv2_fes_range] += self.cv2_fes_range
                        dist_cv2[dist_cv2>+0.5*self.cv2_fes_range] -= self.cv2_fes_range
                    
                    dist_cv3 = self.cv3[lasttime:time]-float(self.minima.iloc[m,7])
                    if self.periodic[2]:
                        dist_cv3[dist_cv3<-0.5*self.cv3_fes_range] += self.cv3_fes_range
                        dist_cv3[dist_cv3>+0.5*self.cv3_fes_range] -= self.cv3_fes_range
            
                    dp2 = (dist_cv1**2/(2*self.s1[lasttime:time]**2) + 
                           dist_cv2**2/(2*self.s2[lasttime:time]**2) + 
                           dist_cv3**2/(2*self.s3[lasttime:time]**2))
                    tmp = np.zeros(dp2.shape)
                    heights = self.heights[lasttime:time]
                    tmp[dp2<6.25] = (heights[dp2<6.25] * (np.exp(-dp2[dp2<6.25]) * 1.00193418799744762399 - 0.00193418799744762399))
                    fes[int(float(self.minima.iloc[m,2])),
                        int(float(self.minima.iloc[m,3])),
                        int(float(self.minima.iloc[m,4]))] -= tmp.sum()
                
                # save profile
                profileline = [time-1]
                for m in range(number_of_minima):
                    profileline.append(fes[int(float(self.minima.iloc[m,2])),
                                           int(float(self.minima.iloc[m,3])),
                                           int(float(self.minima.iloc[m,4]))]-\
                                       fes[int(float(self.minima.iloc[0,2])),
                                           int(float(self.minima.iloc[0,3])),
                                           int(float(self.minima.iloc[0,4]))])
                self.feprofile = np.vstack([self.feprofile, profileline])

                lasttime = time
            
        else:
            print("Fes object doesn't have supported number of CVs.")
    
    def plot(self, png_name=None, image_size=None, image_size_unit="in", dpi=100, tu = "ps", xlabel=None, ylabel=None, label_size=12, cmap="RdYlBu_r", legend=True, xlim=[None, None], ylim=[None, None], title=None, return_fig=False):
        """
        Visualization function for free energy profiles. 
        
        ```python
        fep.plot(png_name="FEProfile.png")
        ```
        
        
        Parameters:
        
        
        * png_name (default=None) = name for image file to save the plot to
        
        * image_size (default = [9,6]) = list of the width and height of the picture
        
        * image_size_unit (default = "in") = Units for width and height of the picture, accepts "in" as inches, "cm", "mm" or "px" as pixels. 

        * dpi (default = 100) = DPI of the resulting image. 
        
        * tu (default = "ps") = string, time unit to be shown on x axis. Available options: "s", "ms", "us", "ns", "ps", "fs"
        
        * xlabel (default="time (ps)")
        
        * ylabel (default="free energy difference (kJ/mol)") 
        
        * label_size (default=12) = size of labels
        
        * cmap (default="RdYlBu_r") = matplotlib colormap used for coloring the line of the minima

        * legend (default=True) = whether there should be a matplotlib's legend in the graph

        * xlim, ylim (default = [None, None] for both) = list of two values specifying the range of x and y axes respectively. 
        None means that Matplotlib will choose appropriate range, so this keyword is useful when you need to overwrite it. 

        * title = optional, string that defines the title of the graph

        * return_fig (default=False) = whether the method should return the Matplotlib.Pyplot.figure object for further use. 
        
        """
        
        tu = TU(tu)

        if image_size == None:
            image_size = [9,6]
        
        if image_size_unit == "cm":
            image_size[0] /= 2.54
            image_size[1] /= 2.54
        elif image_size_unit == "mm":
            image_size[0] /= 25.4
            image_size[1] /= 25.4
        elif image_size_unit == "px":
            image_size[0] /= dpi
            image_size[1] /= dpi
        elif image_size_unit != "in":
            print(f"Warning: unknown image_size_unit value: {image_size_unit}. Using inches instead. ")
        
        fig = plt.figure(figsize=(image_size[0],image_size[1]), dpi=dpi)
        
        cmap=cm.get_cmap(cmap)
        
        #colors = cm.RdYlBu_r((self.minima.iloc[:,1].to_numpy()).astype(float)/\
        #                (np.max(self.minima.iloc[:,1].to_numpy().astype(float))))
        colors = cmap(np.linspace(0,1,self.minima.shape[0]))
        for m in range(self.minima.shape[0]):
            plt.plot(tu.intu(self.feprofile[:,0]), self.feprofile[:,m+1], color=colors[m])

        if xlabel == None:
            plt.xlabel(f'time ({tu.name})', size=label_size)
        else:
            plt.xlabel(xlabel, size=label_size)
        if ylabel == None:
            plt.ylabel('free energy difference (kJ/mol)', size=label_size)
        else:
            plt.ylabel(ylabel, size=label_size)
        if legend:
            plt.legend(self.minima.iloc[:,0], loc="lower right")
        
        if title != None:
            plt.title(title, size=label_size)
    
        plt.xticks(fontsize=label_size)
        plt.yticks(fontsize=label_size)
        
        ax = plt.gca()
        ax.set_xlim(xlim)
        ax.set_ylim(ylim)
        if png_name != None:
            plt.savefig(png_name, bbox_inches = 'tight')

        if return_fig:
            return fig

Classes

class FEProfile (minima, hills)

Free energy profile is a visualization of differences between local minima points during metadynamics simulation. If the values seem to converge to a mean value of the difference, it suggests, but not fully prooves, that the obtained FES did converge to the correct shape.

Command:

fep = metadynminer.FEProfile(minima, hillsfile)

Parameters:

  • minima = metadynminer.Minima object

  • hillsfile = metadynminer.Hills object

Expand source code
class FEProfile:
    """
    Free energy profile is a visualization of differences between local 
    minima points during metadynamics simulation. If the values seem 
    to converge to a mean value of the difference, it suggests, 
    but not fully prooves, that the obtained FES did converge to the correct shape.
    
    Command:
    ```python
    fep = metadynminer.FEProfile(minima, hillsfile)
    ```
    
    Parameters:
    
    * minima = metadynminer.Minima object
    
    * hillsfile = metadynminer.Hills object
    
    """
    
    __pdoc__["FEProfile.makefeprofile"] = False
    
    def __init__(self, minima, hills):
        self.cvs = minima.cvs
        self.res = minima.res
        self.minima = minima.minima
        self.periodic = minima.periodic
        self.heights = hills.get_heights()
        
        if self.cvs >= 1:
            self.cv1_name = minima.cv1_name
            self.cv1min = minima.cv1min
            self.cv1max = minima.cv1max
            self.cv1 = hills.get_cv1()
            self.s1 = hills.get_sigma1()
            self.cv1per = hills.get_cv1per()
            self.cv1_fes_range = minima.cv1_fes_range
        if self.cvs >= 2:
            self.cv2min = minima.cv2min
            self.cv2max = minima.cv2max
            self.cv2_name = minima.cv2_name
            self.cv2 = hills.get_cv2()
            self.s2 = hills.get_sigma2()
            self.cv2per = hills.get_cv2per()
            self.cv2_fes_range = minima.cv2_fes_range
        if self.cvs == 3:
            self.cv3min = minima.cv3min
            self.cv3max = minima.cv3max
            self.cv3_name = minima.cv3_name
            self.cv3 = hills.get_cv3()
            self.s3 = hills.get_sigma3()
            self.cv3per = hills.get_cv3per()
            self.cv3_fes_range = minima.cv3_fes_range
        
        if len(minima.minima.shape)>1:
            self.makefeprofile(hills)
        else: 
            print("There is only one local minimum on the free energy surface.")
    
    def makefeprofile(self, hills):
        """
        Internal method to calculate free energy profile. 
        """
        hillslenght = len(hills.get_cv1())
        
        if hillslenght < 256:
            profilelenght = hillslenght
            scantimes = np.array(range(hillslenght))
        else:
            profilelenght = 256
            scantimes = np.array(((hillslenght/(profilelenght))*np.array((range(1,profilelenght+1)))))
            scantimes[0:-1] -= 1
            #scantimes[-1] += 1
            scantimes = scantimes.astype(int)
        number_of_minima = self.minima.shape[0]
        
        self.feprofile = np.zeros((self.minima.Minimum.shape[0]+1))

        if self.cvs >= 1:
            cv1min = self.cv1min
            cv1max = self.cv1max         
        if self.cvs >= 2:
            cv2min = self.cv2min
            cv2max = self.cv2max           
        if self.cvs >= 3:     
            cv3min = self.cv3min
            cv3max = self.cv3max
        
        if self.cvs == 1:
            
            fes = np.zeros((self.res))
            
            lasttime = 0
            for time in scantimes:
                if time == scantimes[-1]:
                    time += 1
                for m in range(number_of_minima):#self.minima.iloc[:,3]
                    dist_cv1 = self.cv1[lasttime:time]-float(self.minima.iloc[m,3])
                    if self.periodic[0]:
                        dist_cv1[dist_cv1<-0.5*self.cv1_fes_range] += self.cv1_fes_range
                        dist_cv1[dist_cv1>+0.5*self.cv1_fes_range] -= self.cv1_fes_range

                    dp2 = dist_cv1**2/(2*self.s1[lasttime:time]**2)
                    tmp = np.zeros(dp2.shape)
                    heights = self.heights[lasttime:time]
                    tmp[dp2<6.25] = heights[dp2<6.25] * (np.exp(-dp2[dp2<6.25]) * 1.00193418799744762399 - 0.00193418799744762399)
                    #fes[int(float(self.minima.iloc[x,2]))] -= tmp.sum()
                    fes[int(float(self.minima.iloc[m,2]))] -= tmp.sum()
                #fes = fes - np.min(fes)
                profileline = [time-1]
                for m in range(number_of_minima):
                    profileline.append(fes[int(float(self.minima.iloc[m,2]))]-\
                                       fes[int(float(self.minima.iloc[0,2]))])
                self.feprofile = np.vstack([self.feprofile, profileline])

                lasttime = time
            
        elif self.cvs == 2:
            
            fes = np.zeros((self.res, self.res))
            
            lasttime = 0
            line = 0
            for time in scantimes:
                if time == scantimes[-1]:
                    time += 1
                for m in range(number_of_minima):
                    dist_cv1 = self.cv1[lasttime:time]-float(self.minima.iloc[m,4])
                    if self.periodic[0]:
                        dist_cv1[dist_cv1<-0.5*self.cv1_fes_range] += self.cv1_fes_range
                        dist_cv1[dist_cv1>+0.5*self.cv1_fes_range] -= self.cv1_fes_range
                    
                    dist_cv2 = self.cv2[lasttime:time]-float(self.minima.iloc[m,5])
                    if self.periodic[1]:
                        dist_cv2[dist_cv2<-0.5*self.cv2_fes_range] += self.cv2_fes_range
                        dist_cv2[dist_cv2>+0.5*self.cv2_fes_range] -= self.cv2_fes_range
                
                    dp2 = dist_cv1**2/(2*self.s1[lasttime:time]**2) + dist_cv2**2/(2*self.s2[lasttime:time]**2)
                    tmp = np.zeros(self.cv1[lasttime:time].shape)
                    heights = self.heights[lasttime:time]
                    tmp[dp2<6.25] = heights[dp2<6.25] * (np.exp(-dp2[dp2<6.25]) * 1.00193418799744762399 - 0.00193418799744762399)
                    fes[int(float(self.minima.iloc[m,2])),int(float(self.minima.iloc[m,3]))] -= tmp.sum()
                
                # save profile
                profileline = [time-1]
                for m in range(number_of_minima):
                    profileline.append(fes[int(float(self.minima.iloc[m,2])),int(float(self.minima.iloc[m,3]))]-\
                                       fes[int(float(self.minima.iloc[0,2])),int(float(self.minima.iloc[0,3]))])
                self.feprofile = np.vstack([self.feprofile, profileline])

                lasttime = time
        
        elif self.cvs == 3:
            
            fes = np.zeros((self.res, self.res, self.res))
            
            lasttime = 0
            for time in scantimes:
                if time == scantimes[-1]:
                    time += 1
                for m in range(number_of_minima):
                    dist_cv1 = self.cv1[lasttime:time]-float(self.minima.iloc[m,5])
                    if self.periodic[0]:
                        dist_cv1[dist_cv1<-0.5*self.cv1_fes_range] += self.cv1_fes_range
                        dist_cv1[dist_cv1>+0.5*self.cv1_fes_range] -= self.cv1_fes_range
                    
                    dist_cv2 = self.cv2[lasttime:time]-float(self.minima.iloc[m,6])
                    if self.periodic[1]:
                        dist_cv2[dist_cv2<-0.5*self.cv2_fes_range] += self.cv2_fes_range
                        dist_cv2[dist_cv2>+0.5*self.cv2_fes_range] -= self.cv2_fes_range
                    
                    dist_cv3 = self.cv3[lasttime:time]-float(self.minima.iloc[m,7])
                    if self.periodic[2]:
                        dist_cv3[dist_cv3<-0.5*self.cv3_fes_range] += self.cv3_fes_range
                        dist_cv3[dist_cv3>+0.5*self.cv3_fes_range] -= self.cv3_fes_range
            
                    dp2 = (dist_cv1**2/(2*self.s1[lasttime:time]**2) + 
                           dist_cv2**2/(2*self.s2[lasttime:time]**2) + 
                           dist_cv3**2/(2*self.s3[lasttime:time]**2))
                    tmp = np.zeros(dp2.shape)
                    heights = self.heights[lasttime:time]
                    tmp[dp2<6.25] = (heights[dp2<6.25] * (np.exp(-dp2[dp2<6.25]) * 1.00193418799744762399 - 0.00193418799744762399))
                    fes[int(float(self.minima.iloc[m,2])),
                        int(float(self.minima.iloc[m,3])),
                        int(float(self.minima.iloc[m,4]))] -= tmp.sum()
                
                # save profile
                profileline = [time-1]
                for m in range(number_of_minima):
                    profileline.append(fes[int(float(self.minima.iloc[m,2])),
                                           int(float(self.minima.iloc[m,3])),
                                           int(float(self.minima.iloc[m,4]))]-\
                                       fes[int(float(self.minima.iloc[0,2])),
                                           int(float(self.minima.iloc[0,3])),
                                           int(float(self.minima.iloc[0,4]))])
                self.feprofile = np.vstack([self.feprofile, profileline])

                lasttime = time
            
        else:
            print("Fes object doesn't have supported number of CVs.")
    
    def plot(self, png_name=None, image_size=None, image_size_unit="in", dpi=100, tu = "ps", xlabel=None, ylabel=None, label_size=12, cmap="RdYlBu_r", legend=True, xlim=[None, None], ylim=[None, None], title=None, return_fig=False):
        """
        Visualization function for free energy profiles. 
        
        ```python
        fep.plot(png_name="FEProfile.png")
        ```
        
        
        Parameters:
        
        
        * png_name (default=None) = name for image file to save the plot to
        
        * image_size (default = [9,6]) = list of the width and height of the picture
        
        * image_size_unit (default = "in") = Units for width and height of the picture, accepts "in" as inches, "cm", "mm" or "px" as pixels. 

        * dpi (default = 100) = DPI of the resulting image. 
        
        * tu (default = "ps") = string, time unit to be shown on x axis. Available options: "s", "ms", "us", "ns", "ps", "fs"
        
        * xlabel (default="time (ps)")
        
        * ylabel (default="free energy difference (kJ/mol)") 
        
        * label_size (default=12) = size of labels
        
        * cmap (default="RdYlBu_r") = matplotlib colormap used for coloring the line of the minima

        * legend (default=True) = whether there should be a matplotlib's legend in the graph

        * xlim, ylim (default = [None, None] for both) = list of two values specifying the range of x and y axes respectively. 
        None means that Matplotlib will choose appropriate range, so this keyword is useful when you need to overwrite it. 

        * title = optional, string that defines the title of the graph

        * return_fig (default=False) = whether the method should return the Matplotlib.Pyplot.figure object for further use. 
        
        """
        
        tu = TU(tu)

        if image_size == None:
            image_size = [9,6]
        
        if image_size_unit == "cm":
            image_size[0] /= 2.54
            image_size[1] /= 2.54
        elif image_size_unit == "mm":
            image_size[0] /= 25.4
            image_size[1] /= 25.4
        elif image_size_unit == "px":
            image_size[0] /= dpi
            image_size[1] /= dpi
        elif image_size_unit != "in":
            print(f"Warning: unknown image_size_unit value: {image_size_unit}. Using inches instead. ")
        
        fig = plt.figure(figsize=(image_size[0],image_size[1]), dpi=dpi)
        
        cmap=cm.get_cmap(cmap)
        
        #colors = cm.RdYlBu_r((self.minima.iloc[:,1].to_numpy()).astype(float)/\
        #                (np.max(self.minima.iloc[:,1].to_numpy().astype(float))))
        colors = cmap(np.linspace(0,1,self.minima.shape[0]))
        for m in range(self.minima.shape[0]):
            plt.plot(tu.intu(self.feprofile[:,0]), self.feprofile[:,m+1], color=colors[m])

        if xlabel == None:
            plt.xlabel(f'time ({tu.name})', size=label_size)
        else:
            plt.xlabel(xlabel, size=label_size)
        if ylabel == None:
            plt.ylabel('free energy difference (kJ/mol)', size=label_size)
        else:
            plt.ylabel(ylabel, size=label_size)
        if legend:
            plt.legend(self.minima.iloc[:,0], loc="lower right")
        
        if title != None:
            plt.title(title, size=label_size)
    
        plt.xticks(fontsize=label_size)
        plt.yticks(fontsize=label_size)
        
        ax = plt.gca()
        ax.set_xlim(xlim)
        ax.set_ylim(ylim)
        if png_name != None:
            plt.savefig(png_name, bbox_inches = 'tight')

        if return_fig:
            return fig

Methods

def plot(self, png_name=None, image_size=None, image_size_unit='in', dpi=100, tu='ps', xlabel=None, ylabel=None, label_size=12, cmap='RdYlBu_r', legend=True, xlim=[None, None], ylim=[None, None], title=None, return_fig=False)

Visualization function for free energy profiles.

fep.plot(png_name="FEProfile.png")

Parameters

  • png_name (default=None) = name for image file to save the plot to

  • image_size (default = [9,6]) = list of the width and height of the picture

  • image_size_unit (default = "in") = Units for width and height of the picture, accepts "in" as inches, "cm", "mm" or "px" as pixels.

  • dpi (default = 100) = DPI of the resulting image.

  • tu (default = "ps") = string, time unit to be shown on x axis. Available options: "s", "ms", "us", "ns", "ps", "fs"

  • xlabel (default="time (ps)")

  • ylabel (default="free energy difference (kJ/mol)")

  • label_size (default=12) = size of labels

  • cmap (default="RdYlBu_r") = matplotlib colormap used for coloring the line of the minima

  • legend (default=True) = whether there should be a matplotlib's legend in the graph

  • xlim, ylim (default = [None, None] for both) = list of two values specifying the range of x and y axes respectively. None means that Matplotlib will choose appropriate range, so this keyword is useful when you need to overwrite it.

  • title = optional, string that defines the title of the graph

  • return_fig (default=False) = whether the method should return the Matplotlib.Pyplot.figure object for further use.

Expand source code
def plot(self, png_name=None, image_size=None, image_size_unit="in", dpi=100, tu = "ps", xlabel=None, ylabel=None, label_size=12, cmap="RdYlBu_r", legend=True, xlim=[None, None], ylim=[None, None], title=None, return_fig=False):
    """
    Visualization function for free energy profiles. 
    
    ```python
    fep.plot(png_name="FEProfile.png")
    ```
    
    
    Parameters:
    
    
    * png_name (default=None) = name for image file to save the plot to
    
    * image_size (default = [9,6]) = list of the width and height of the picture
    
    * image_size_unit (default = "in") = Units for width and height of the picture, accepts "in" as inches, "cm", "mm" or "px" as pixels. 

    * dpi (default = 100) = DPI of the resulting image. 
    
    * tu (default = "ps") = string, time unit to be shown on x axis. Available options: "s", "ms", "us", "ns", "ps", "fs"
    
    * xlabel (default="time (ps)")
    
    * ylabel (default="free energy difference (kJ/mol)") 
    
    * label_size (default=12) = size of labels
    
    * cmap (default="RdYlBu_r") = matplotlib colormap used for coloring the line of the minima

    * legend (default=True) = whether there should be a matplotlib's legend in the graph

    * xlim, ylim (default = [None, None] for both) = list of two values specifying the range of x and y axes respectively. 
    None means that Matplotlib will choose appropriate range, so this keyword is useful when you need to overwrite it. 

    * title = optional, string that defines the title of the graph

    * return_fig (default=False) = whether the method should return the Matplotlib.Pyplot.figure object for further use. 
    
    """
    
    tu = TU(tu)

    if image_size == None:
        image_size = [9,6]
    
    if image_size_unit == "cm":
        image_size[0] /= 2.54
        image_size[1] /= 2.54
    elif image_size_unit == "mm":
        image_size[0] /= 25.4
        image_size[1] /= 25.4
    elif image_size_unit == "px":
        image_size[0] /= dpi
        image_size[1] /= dpi
    elif image_size_unit != "in":
        print(f"Warning: unknown image_size_unit value: {image_size_unit}. Using inches instead. ")
    
    fig = plt.figure(figsize=(image_size[0],image_size[1]), dpi=dpi)
    
    cmap=cm.get_cmap(cmap)
    
    #colors = cm.RdYlBu_r((self.minima.iloc[:,1].to_numpy()).astype(float)/\
    #                (np.max(self.minima.iloc[:,1].to_numpy().astype(float))))
    colors = cmap(np.linspace(0,1,self.minima.shape[0]))
    for m in range(self.minima.shape[0]):
        plt.plot(tu.intu(self.feprofile[:,0]), self.feprofile[:,m+1], color=colors[m])

    if xlabel == None:
        plt.xlabel(f'time ({tu.name})', size=label_size)
    else:
        plt.xlabel(xlabel, size=label_size)
    if ylabel == None:
        plt.ylabel('free energy difference (kJ/mol)', size=label_size)
    else:
        plt.ylabel(ylabel, size=label_size)
    if legend:
        plt.legend(self.minima.iloc[:,0], loc="lower right")
    
    if title != None:
        plt.title(title, size=label_size)

    plt.xticks(fontsize=label_size)
    plt.yticks(fontsize=label_size)
    
    ax = plt.gca()
    ax.set_xlim(xlim)
    ax.set_ylim(ylim)
    if png_name != None:
        plt.savefig(png_name, bbox_inches = 'tight')

    if return_fig:
        return fig
class Fes (hills=None, resolution=256, original=False, calculate_new_fes=True, cv1range=None, cv2range=None, cv3range=None, time_min=None, time_max=None, tu='ps', print_output=True, subtract_min=True)

Object of this class is created to compute the free energy surface corresponding to the provided Hills object. Command:

fes = metadynminer.Fes(hills=hillsfile)

parameters:

  • hills = Hills object

  • resolution (default=256) = should be positive integer, controls the resolution of FES

  • original (default=False) = boolean, if False, FES will be calculated using very fast, but not 'exact' Bias Sum Algorithm if True, FES will be calculated with slower algorithm, but it will be exactly the same as FES calculated with PLUMED sum_hills function

  • cv1range, cv2range, cv3range = lists of two numbers, defining lower and upper bound of the respective CV (in the units of the CVs)

  • time_min, time_max = Limit points for closed interval of times from the HILLS file from which the FES will be constructed. Useful for making animations of flooding of the FES during simulation. The values here should have the same units as those in the HILLS file, especially if ignoretime = False.

  • tu (default = "ps") = string, time unit for time_min and time_max parameters, if those are used. Available options: "s", "ms", "us", "ns", "ps", "fs"

  • print_output (default = True), tells whether the function should print messages like progress of calculation

  • subtract_min (deault = True), whether the global minimum value should be subtracted from the whole FES, so that global minimum has zero free energy and other states have higher values

Fes atributes:

  • Fes.fes = free energy surface

  • Fes.res = resolution of the FES

  • Fes.original = whether FES should be calculated by plumed's original precise algorithm

  • Fes.cvs = number of CVs

  • Fes.hills = array with values from the HILLS file

  • Fes.periodic = list of boolean values setting which CVs are periodic

  • Fes.cv1min, Fes.cv1max = minimum and maximum values on the FES, respectively

  • Fes.cv1_fes_range, Fes.cv2_fes_range, Fes.cv3_fes_range = FES range = maximum - minimum values on FES for given CV

  • Fes.cv1_name, Fes.cv2_name, Fes.cv3_name = CV names

Expand source code
class Fes: 
    """
    Object of this class is created to compute the free energy surface corresponding to the provided Hills object. 
    Command:
    ```python
    fes = metadynminer.Fes(hills=hillsfile)
    ```
    parameters:
    
    * hills = Hills object
    
    * resolution (default=256) = should be positive integer, controls the resolution of FES
    
    * original (default=False) = boolean, if False, FES will be calculated using very fast, but not
    'exact' Bias Sum Algorithm
                                        if True, FES will be calculated with slower algorithm, but it will be exactly the same as FES calculated 
                                        with PLUMED sum_hills function
                                        
    * cv1range, cv2range, cv3range = lists of two numbers, defining lower and upper bound of the respective CV (in the units of the CVs)
    
    * time_min, time_max = Limit points for closed interval of times from the HILLS file from which the FES will be constructed. Useful for making animations of flooding of the FES during simulation. The values here should have the same units as those in the HILLS file, especially if ignoretime = False. 

    * tu (default = "ps") = string, time unit for time_min and time_max parameters, if those are used. Available options: "s", "ms", "us", "ns", "ps", "fs"

    * print_output (default = True), tells whether the function should print messages like progress of calculation

    * subtract_min (deault = True), whether the global minimum value should be subtracted from the whole FES, so that global minimum has zero free energy and other states have higher values

    Fes atributes: 

    * Fes.fes = free energy surface
    
    * Fes.res = resolution of the FES

    * Fes.original = whether FES should be calculated by plumed's original precise algorithm

    * Fes.cvs = number of CVs

    * Fes.hills = array with values from the HILLS file

    * Fes.periodic = list of boolean values setting which CVs are periodic

    * Fes.cv1min, Fes.cv1max = minimum and maximum values on the FES, respectively

    * Fes.cv1_fes_range, Fes.cv2_fes_range, Fes.cv3_fes_range = FES range = maximum - minimum values on FES for given CV

    * Fes.cv1_name, Fes.cv2_name, Fes.cv3_name = CV names
    
    """
    
    __pdoc__["Fes.makefes"] = False
    __pdoc__["Fes.makefes2"] = False
    __pdoc__["Fes.set_fes"] = False
    
    def __init__(self, hills=None, resolution=256, original=False, \
                 calculate_new_fes=True, cv1range=None, cv2range=None, cv3range=None, \
                 time_min=None, time_max=None, tu="ps", print_output=True, subtract_min = True):
        self.res = resolution
        self.original = original
        self.cv1range = cv1range
        self.cv2range = cv2range
        self.cv3range = cv3range
        if hills != None:
            self.hills = hills
            self.cvs = hills.get_number_of_cvs()
            self.heights = hills.get_heights()
            self.periodic = hills.get_periodic()
            self.biasf = hills.biasf
            self.ignoretime = hills.ignoretime
            self.dt = hills.dt
            
            if cv1range!=None and len(cv1range) != 2:
                print("Error: You have to specify CV1 range as a list of two values. ")
            if cv2range!=None and len(cv2range) != 2:
                print("Error: You have to specify CV2 range as a list of two values. ")
            if cv3range!=None and len(cv3range) != 2:
                print("Error: You have to specify CV3 range as a list of two values. ")
            
            if self.cvs >= 1:
                self.cv1 = hills.get_cv1()
                self.s1 = hills.get_sigma1()

                self.cv1_name = hills.get_cv1_name()
                self.cv1per = hills.get_cv1per()

                self.fes = np.zeros((resolution))

                if self.periodic[0]:
                    cv1min = self.cv1per[0]
                    cv1max = self.cv1per[1]
                    if cv1range != None:
                        print("Warning: CV1 is specified as periodic, to change it's range you must specify the periodicity to cv1per parameter when loading the HILLS file. The cv1range parameter is ignored. ")
                else:
                    if cv1range == None:
                        cv1min = np.min(self.cv1) - 1e-8
                        cv1max = np.max(self.cv1) + 1e-8
                        cv1min -= (cv1max-cv1min)*0.15          
                        cv1max += (cv1max-cv1min)*0.15
                    else:
                        if cv1range[1] <= cv1range[0]:
                            print(f"Error: Wrong values of cv1range: {cv1range}. ")
                            return None
                        cv1min = cv1range[0]
                        cv1max = cv1range[1]
                self.cv1min = cv1min
                self.cv1max = cv1max
                self.cv1_fes_range = self.cv1max - self.cv1min
                
                
                if not original:
                    if ((np.max(self.s1)/np.min(self.s1))>1.00000001):
                        print("""Error: Bias sum algorithm only works for hills files 
                        in which all hills have the same width. 
                        For this file, you need the slower but exact, algorithm, to use it, 
                        specify the argument 'original=True'.""")
                        return None

            if self.cvs >= 2:
                self.cv2 = hills.get_cv2()
                self.s2 = hills.get_sigma2()
                
                self.cv2_name = hills.get_cv2_name()
                self.cv2per = hills.get_cv2per()

                self.fes = np.zeros((resolution, resolution))

                if self.periodic[1]:
                    cv2min = self.cv2per[0]
                    cv2max = self.cv2per[1]
                    if cv2range != None:
                        print("Warning: CV2 is specified as periodic, to change it's range you must specify the periodicity to cv2per parameter when loading the HILLS file. The cv2range parameter is ignored. ")
                else:
                    if cv2range == None:
                        cv2min = np.min(self.cv2) - 1e-8
                        cv2max = np.max(self.cv2) + 1e-8
                        cv2min -= (cv2max-cv2min)*0.15          
                        cv2max += (cv2max-cv2min)*0.15
                    else:
                        if cv2range[1] <= cv2range[0]:
                            print(f"Error: Wrong values of cv2range: {cv2range}. ")
                            return None
                        cv2min = cv2range[0]
                        cv2max = cv2range[1]
                self.cv2min = cv2min
                self.cv2max = cv2max
                self.cv2_fes_range = self.cv2max - self.cv2min


                
                if not original:
                    if ((np.max(self.s2)/np.min(self.s2))>1.00000001):
                        print("""Error: Bias sum algorithm only works for hills files 
                        in which all hills have the same width. 
                        For this file, you need the slower, but exact, algorithm, 
                        specify the argument 'original=True'.""")
                        return None

            if self.cvs == 3:
                self.cv3 = hills.get_cv3()
                self.s3 = hills.get_sigma3()

                self.cv3_name = hills.get_cv3_name()
                self.cv3per = hills.get_cv3per()

                self.fes = np.zeros((resolution, resolution, resolution))
                
                if self.periodic[2]:
                    cv3min = self.cv3per[0]
                    cv3max = self.cv3per[1]
                    if cv3range != None:
                        print("Warning: CV3 is specified as periodic, to change it's range you must specify the periodicity to cv3per parameter when loading the HILLS file. The cv3range is ignored. ")
                else:
                    if cv3range == None:
                        cv3min = np.min(self.cv3) - 1e-8
                        cv3max = np.max(self.cv3) + 1e-8
                        cv3min -= (cv3max-cv3min)*0.15          
                        cv3max += (cv3max-cv3min)*0.15
                    else:
                        if cv3range[1] <= cv3range[0]:
                            print(f"Error: Wrong values of cv3range: {cv3range}. ")
                            return None
                        cv3min = cv3range[0]
                        cv3max = cv3range[1]
                self.cv3min = cv3min
                self.cv3max = cv3max
                self.cv3_fes_range = self.cv3max - self.cv3min
                
                if not original:
                    if ((np.max(self.s3)/np.min(self.s3))>1.00000001):
                        print("""Error: Bias sum algorithm only works for hills files 
                        in which all hills have the same width of given CV. 
                        For this file, you need the exact algorithm, to do that, 
                        specify the argument 'original=True'.""")
                        return None
                        
            tu = TU(tu)
        
            if time_min == time_max == 0:
                print("Error: Values of start and end time are zero.")
                return None
            if time_min != None:
                #time_min = tu.inps(time_min)
                if time_min < 0:
                    print("Warning: Start time is lower than zero, it will be set to zero instead. ")
                    time_min = 0
                if tu.inps(time_min) < int(hills.hills[0,0]):
                    print(f"Warning: Start time {tu.inps(time_min)} is lower than the first time from HILLS file {int(hills.hills[0,0])}, which will be used instead. ")
                    time_min = tu.intu(int(hills.hills[0,0]))
            else:
                time_min = tu.intu(int(hills.hills[0,0]))
            if time_max != None:
                #time_max = int(tu.inps(time_max))
                if time_max < time_min:
                    print("Warning: End time is lower than start time. Values are flipped. ")
                    time_value = time_max
                    time_max = time_min
                    time_min = time_value
                if tu.inps(time_max) > int(hills.hills[-1,0]):
                    print(f"Warning: End time {tu.inps(time_max)} is higher than number of lines in HILLS file {int(hills.hills[-1,0])}, which will be used instead. ")
                    time_max = tu.intu(int(hills.hills[-1,0]))
            else:
                time_max = tu.intu(int(hills.hills[-1,0]))
            
            if not self.ignoretime:
                time_max = int(round(((time_max - hills.hills[0,0])/self.dt),0)) + 1
                time_min = int(round(((time_min - hills.hills[0,0])/self.dt),0)) + 1
                #print(f"Berofe fes: min {time_min}, max {time_max}")

            if calculate_new_fes:
                if original:
                    self.makefes2(resolution, int(tu.inps(time_min)), int(tu.inps(time_max)), 
                                  print_output=print_output, subtract_min = subtract_min)
                else:
                    self.makefes(resolution, int(tu.inps(time_min)), int(tu.inps(time_max)), 
                                 print_output=print_output, subtract_min = subtract_min)
        
    def makefes(self, resolution, time_min, time_max, print_output=True, subtract_min = True):
        """
        Function used internally for summing hills in Hills object with the fast Bias Sum Algorithm. 
        """
        #self.res = resolution
        #if self.res % 2 == 0:
        #    self.res += 1
        #print(f"min: {time_min}, max: {time_max}")
        
        if self.cvs == 1:
            cv1bin = np.ceil((self.cv1-self.cv1min)*self.res/(self.cv1_fes_range))
            cv1bin = cv1bin.astype(int)
            
            s1res = (self.s1[0]*self.res)/(self.cv1_fes_range)
            
            self.cv1bin = cv1bin
            
            gauss_res = 8*s1res
            gauss_res = int(gauss_res)
            if gauss_res%2 == 0:
                gauss_res += 1
            
            gauss_center = int((gauss_res-1)/2)+1
            gauss = np.zeros((gauss_res))
            for i in range(gauss_res):
                gauss[int(i)] = -np.exp(-((i+1)-gauss_center)**2/(2*s1res**2))
                
            fes = np.zeros((self.res))
            
            for line in range(time_min-1, time_max):
                if print_output and (((line) % 500 == 0) or (line == time_max-1)):
                    print(f"Constructing free energy surface: {((line+1-time_min+1)/(time_max-time_min+1)):.1%} finished", end="\r")
                
                gauss_center_to_end = int((gauss_res-1)/2)
                
                fes_to_edit_cv1 = [cv1bin[line]-1-gauss_center_to_end,
                                   cv1bin[line]-1+gauss_center_to_end]
                fes_crop_cv1 = [max(0,fes_to_edit_cv1[0]),min(self.res-1,fes_to_edit_cv1[1])]
                
                gauss_crop_cv1 = [max(0,gauss_center_to_end-(cv1bin[line]-1)),
                                      gauss_res-1-max(0,(cv1bin[line]-1)+gauss_center_to_end-self.res+1)]
                
                fes[fes_crop_cv1[0]:fes_crop_cv1[1]+1]\
                        += gauss[gauss_crop_cv1[0]:gauss_crop_cv1[1]+1]\
                        * self.heights[line]
                
                if self.periodic[0]:
                    if cv1bin[line] < gauss_center_to_end:
                        fes_crop_cv1_p = [self.res-1+(cv1bin[line]-gauss_center_to_end),self.res-1]
                        gauss_crop_cv1_p = [0,gauss_center_to_end-cv1bin[line]]
                        fes[fes_crop_cv1_p[0]:fes_crop_cv1_p[1]+1]\
                                    += gauss[gauss_crop_cv1_p[0]:gauss_crop_cv1_p[1]+1]\
                                    * self.heights[line]
                    
                    if cv1bin[line] > (self.res-gauss_center_to_end):
                        fes_crop_cv1_p = [0,gauss_center_to_end+cv1bin[line]-self.res-1]
                        gauss_crop_cv1_p = [gauss_res-(gauss_center_to_end+cv1bin[line]-self.res),gauss_res-1]
                        fes[fes_crop_cv1_p[0]:fes_crop_cv1_p[1]+1]\
                                    += gauss[gauss_crop_cv1_p[0]:gauss_crop_cv1_p[1]+1]\
                                    * self.heights[line]
            if print_output:
                print("\n")
            if subtract_min:
                fes = fes-np.min(fes)
            self.fes = np.array(fes)
            
        elif self.cvs == 2:
            cv1bin = np.ceil((self.cv1-self.cv1min)*self.res/(self.cv1_fes_range))
            cv2bin = np.ceil((self.cv2-self.cv2min)*self.res/(self.cv2_fes_range))
                        
            cv1bin = cv1bin.astype(int)
            cv2bin = cv2bin.astype(int)
            
            s1res = (self.s1[0]*self.res)/(self.cv1_fes_range)
            s2res = (self.s2[0]*self.res)/(self.cv2_fes_range)
            
            gauss_res = max(8*s1res, 8*s2res)
            gauss_res = int(gauss_res)
            if gauss_res%2 == 0:
                gauss_res += 1
            
            gauss_center = int((gauss_res-1)/2)+1
            gauss = np.zeros((gauss_res,gauss_res))
            for i in range(gauss_res):
                for j in range(gauss_res):
                    #dp2 = ((i+1)-gauss_center)**2/(2*s1res**2) + ((j+1)-gauss_center)**2/(2*s2res**2)
                    #if dp2 < 6.25:
                    #    gauss[int(i), int(j)] = -np.exp(-dp2) * 1.00193418799744762399 - 0.00193418799744762399
                    gauss[int(i), int(j)] = -np.exp(-(((i+1)-gauss_center)**2/(2*s1res**2) + ((j+1)-gauss_center)**2/(2*s2res**2)))
            
            fes = np.zeros((self.res,self.res))
            for line in range(time_min-1, time_max):
                if print_output and (((line) % 500 == 0) or (line == time_max-1)):
                    print(f"Constructing free energy surface: {((line+1-time_min+1)/(time_max-time_min+1)):.1%} finished.", end="\r")
                
                #fes_center = int((self.res-1)/2)
                gauss_center_to_end = int((gauss_res-1)/2)
                #print(f"\ng_res: {gauss_res}, gauss_center_to_end: {gauss_center_to_end}, cvib:{cv1bin[line]}, cv2b:{cv2bin[line]}")
                
                fes_to_edit_cv1 = [cv1bin[line]-1-gauss_center_to_end,
                                   cv1bin[line]-1+gauss_center_to_end]
                fes_to_edit_cv2 = [cv2bin[line]-1-gauss_center_to_end,
                                   cv2bin[line]-1+gauss_center_to_end]
                fes_crop_cv1 = [max(0,fes_to_edit_cv1[0]),min(self.res-1,fes_to_edit_cv1[1])]
                fes_crop_cv2 = [max(0,fes_to_edit_cv2[0]),min(self.res-1,fes_to_edit_cv2[1])]
                
                gauss_crop_cv1 = [max(0,gauss_center_to_end-(cv1bin[line]-1)),
                                      gauss_res-1-max(0,(cv1bin[line]-1)+gauss_center_to_end-self.res+1)]
                gauss_crop_cv2 = [max(0,gauss_center_to_end-(cv2bin[line]-1)),
                                      gauss_res-1-max(0,(cv2bin[line]-1)+gauss_center_to_end-self.res+1)]
                
                fes[fes_crop_cv1[0]:fes_crop_cv1[1]+1,fes_crop_cv2[0]:fes_crop_cv2[1]+1]\
                        += gauss[gauss_crop_cv1[0]:gauss_crop_cv1[1]+1,gauss_crop_cv2[0]:gauss_crop_cv2[1]+1]\
                        * self.heights[line]
                
                if self.periodic[0]:
                    if cv1bin[line] < gauss_center_to_end:
                        fes_crop_cv1_p = [self.res-1+(cv1bin[line]-gauss_center_to_end),self.res-1]
                        gauss_crop_cv1_p = [0,gauss_center_to_end-cv1bin[line]]
                        fes[fes_crop_cv1_p[0]:fes_crop_cv1_p[1]+1,fes_crop_cv2[0]:fes_crop_cv2[1]+1]\
                                    += gauss[gauss_crop_cv1_p[0]:gauss_crop_cv1_p[1]+1,gauss_crop_cv2[0]:gauss_crop_cv2[1]+1]\
                                    * self.heights[line]
                    
                    if cv1bin[line] > (self.res-gauss_center_to_end):
                        fes_crop_cv1_p = [0,gauss_center_to_end+cv1bin[line]-self.res-1]
                        gauss_crop_cv1_p = [gauss_res-(gauss_center_to_end+cv1bin[line]-self.res),gauss_res-1]
                        fes[fes_crop_cv1_p[0]:fes_crop_cv1_p[1]+1,fes_crop_cv2[0]:fes_crop_cv2[1]+1]\
                                    += gauss[gauss_crop_cv1_p[0]:gauss_crop_cv1_p[1]+1,gauss_crop_cv2[0]:gauss_crop_cv2[1]+1]\
                                    * self.heights[line]
                
                if self.periodic[1]:
                    if cv2bin[line] < gauss_center_to_end:
                        fes_crop_cv2_p = [self.res-1+(cv2bin[line]-gauss_center_to_end),self.res-1]
                        gauss_crop_cv2_p = [0,gauss_center_to_end-cv2bin[line]]
                        fes[fes_crop_cv1[0]:fes_crop_cv1[1]+1,fes_crop_cv2_p[0]:fes_crop_cv2_p[1]+1]\
                                    += gauss[gauss_crop_cv1[0]:gauss_crop_cv1[1]+1,gauss_crop_cv2_p[0]:gauss_crop_cv2_p[1]+1]\
                                    * self.heights[line]
                    
                    if cv2bin[line] > (self.res-gauss_center_to_end):
                        fes_crop_cv2_p = [0,gauss_center_to_end+cv2bin[line]-self.res-1]
                        gauss_crop_cv2_p = [gauss_res-(gauss_center_to_end+cv2bin[line]-self.res),gauss_res-1]
                        fes[fes_crop_cv1[0]:fes_crop_cv1[1]+1,fes_crop_cv2_p[0]:fes_crop_cv2_p[1]+1]\
                                    += gauss[gauss_crop_cv1[0]:gauss_crop_cv1[1]+1,gauss_crop_cv2_p[0]:gauss_crop_cv2_p[1]+1]\
                                    * self.heights[line]
                
                if self.periodic[0] and self.periodic[1]:
                    if ((cv1bin[line] < gauss_center_to_end) or (cv1bin[line] > (self.res-gauss_center_to_end))) \
                            and ((cv2bin[line] < gauss_center_to_end) or (cv2bin[line] > (self.res-gauss_center_to_end))):
                        fes[fes_crop_cv1_p[0]:fes_crop_cv1_p[1]+1,fes_crop_cv2_p[0]:fes_crop_cv2_p[1]+1]\
                                    += gauss[gauss_crop_cv1_p[0]:gauss_crop_cv1_p[1]+1,gauss_crop_cv2_p[0]:gauss_crop_cv2_p[1]+1]\
                                    * self.heights[line]
            if print_output:
                print("\n")
            if subtract_min:
                fes = fes-np.min(fes)
            self.fes = np.array(fes)
        elif self.cvs == 3:
            cv1bin = np.ceil((self.cv1-self.cv1min)*self.res/(self.cv1_fes_range))
            cv2bin = np.ceil((self.cv2-self.cv2min)*self.res/(self.cv2_fes_range))
            cv3bin = np.ceil((self.cv3-self.cv3min)*self.res/(self.cv3_fes_range))
                        
            cv1bin = cv1bin.astype(int)
            cv2bin = cv2bin.astype(int)
            cv3bin = cv3bin.astype(int)
            
            s1res = (self.s1[0]*self.res)/(self.cv1_fes_range)
            s2res = (self.s2[0]*self.res)/(self.cv2_fes_range)
            s3res = (self.s3[0]*self.res)/(self.cv3_fes_range)
            
            gauss_res = max(10*s1res, 10*s2res, 10*s3res)
            gauss_res = int(gauss_res)
            if gauss_res%2 == 0:
                gauss_res += 1
            gauss_center = int((gauss_res-1)/2)+1
            gauss = np.zeros((gauss_res,gauss_res,gauss_res))
            for i in range(gauss_res):
                for j in range(gauss_res):
                    for k in range(gauss_res):
                        #dp2 = ((i+1)-gauss_center)**2/(2*s1res**2) + ((j+1)-gauss_center)**2/(2*s2res**2) + ((k+1)-gauss_center)**2/(2*s3res**2)
                        #if dp2 < 6.25:
                        #    gauss[int(i), int(j), int(k)] = -np.exp(-dp2) * 1.00193418799744762399 - 0.00193418799744762399
                        gauss[int(i), int(j), int(k)] = -np.exp(-(((i+1)-gauss_center)**2/(2*s1res**2) + 
                                                                  ((j+1)-gauss_center)**2/(2*s2res**2) + 
                                                                  ((k+1)-gauss_center)**2/(2*s3res**2)))
            
            fes = np.zeros((self.res, self.res, self.res))
            
            for line in range(time_min-1, time_max):
                if print_output and (((line) % 500 == 0) or (line == time_max-1)):
                    print(f"Constructing free energy surface: {((line+1-time_min+1)/(time_max-time_min+1)):.1%} finished", end="\r")
                
                #fes_center = int((self.res-1)/2)
                gauss_center_to_end = int((gauss_res-1)/2)
                
                fes_to_edit_cv1 = [cv1bin[line]-1-gauss_center_to_end,
                                   cv1bin[line]-1+gauss_center_to_end]
                fes_to_edit_cv2 = [(cv2bin[line]-1)-gauss_center_to_end,
                                   (cv2bin[line]-1)+gauss_center_to_end]
                fes_to_edit_cv3 = [(cv3bin[line]-1)-gauss_center_to_end,
                                   (cv3bin[line]-1)+gauss_center_to_end]
                fes_crop_cv1 = [max(0,fes_to_edit_cv1[0]),min(self.res-1,fes_to_edit_cv1[1])]
                fes_crop_cv2 = [max(0,fes_to_edit_cv2[0]),min(self.res-1,fes_to_edit_cv2[1])]
                fes_crop_cv3 = [max(0,fes_to_edit_cv3[0]),min(self.res-1,fes_to_edit_cv3[1])]
                
                gauss_crop_cv1 = [max(0,gauss_center_to_end-(cv1bin[line]-1)),
                                      gauss_res-1-max(0,(cv1bin[line]-1)+gauss_center_to_end-self.res+1)]
                gauss_crop_cv2 = [max(0,gauss_center_to_end-(cv2bin[line]-1)),
                                      gauss_res-1-max(0,(cv2bin[line]-1)+gauss_center_to_end-self.res+1)]
                gauss_crop_cv3 = [max(0,gauss_center_to_end-(cv3bin[line]-1)),
                                      gauss_res-1-max(0,(cv3bin[line]-1)+gauss_center_to_end-self.res+1)]
                
                fes[fes_crop_cv1[0]:fes_crop_cv1[1]+1,\
                    fes_crop_cv2[0]:fes_crop_cv2[1]+1,\
                    fes_crop_cv3[0]:fes_crop_cv3[1]+1]\
                        += gauss[gauss_crop_cv1[0]:gauss_crop_cv1[1]+1,\
                                 gauss_crop_cv2[0]:gauss_crop_cv2[1]+1,\
                                 gauss_crop_cv3[0]:gauss_crop_cv3[1]+1]\
                        * self.heights[line]
                
                if self.periodic[0]:
                    if cv1bin[line] < gauss_center_to_end:
                        fes_crop_cv1_p = [self.res-1+(cv1bin[line]-gauss_center_to_end),self.res-1]
                        gauss_crop_cv1_p = [0,gauss_center_to_end-cv1bin[line]]
                        fes[fes_crop_cv1_p[0]:fes_crop_cv1_p[1]+1,\
                            fes_crop_cv2[0]:fes_crop_cv2[1]+1,\
                            fes_crop_cv3[0]:fes_crop_cv3[1]+1]\
                                    += gauss[gauss_crop_cv1_p[0]:gauss_crop_cv1_p[1]+1,\
                                             gauss_crop_cv2[0]:gauss_crop_cv2[1]+1,\
                                             gauss_crop_cv3[0]:gauss_crop_cv3[1]+1]\
                                    * self.heights[line]
                    
                    if cv1bin[line] > (self.res-gauss_center_to_end):
                        fes_crop_cv1_p = [0,gauss_center_to_end+cv1bin[line]-self.res-1]
                        gauss_crop_cv1_p = [gauss_res-(gauss_center_to_end+cv1bin[line]-self.res),gauss_res-1]
                        fes[fes_crop_cv1_p[0]:fes_crop_cv1_p[1]+1,\
                            fes_crop_cv2[0]:fes_crop_cv2[1]+1,\
                            fes_crop_cv3[0]:fes_crop_cv3[1]+1]\
                                    += gauss[gauss_crop_cv1_p[0]:gauss_crop_cv1_p[1]+1,\
                                             gauss_crop_cv2[0]:gauss_crop_cv2[1]+1,\
                                             gauss_crop_cv3[0]:gauss_crop_cv3[1]+1]\
                                    * self.heights[line]
                
                if self.periodic[1]:
                    if cv2bin[line] < gauss_center_to_end:
                        fes_crop_cv2_p = [self.res-1+(cv2bin[line]-gauss_center_to_end),self.res-1]
                        gauss_crop_cv2_p = [0,gauss_center_to_end-cv2bin[line]]
                        fes[fes_crop_cv1[0]:fes_crop_cv1[1]+1,\
                            fes_crop_cv2_p[0]:fes_crop_cv2_p[1]+1,\
                            fes_crop_cv3[0]:fes_crop_cv3[1]+1]\
                                    += gauss[gauss_crop_cv1[0]:gauss_crop_cv1[1]+1,\
                                             gauss_crop_cv2_p[0]:gauss_crop_cv2_p[1]+1,\
                                             gauss_crop_cv3[0]:gauss_crop_cv3[1]+1]\
                                    * self.heights[line]
                    
                    if cv2bin[line] > (self.res-gauss_center_to_end):
                        fes_crop_cv2_p = [0,gauss_center_to_end+cv2bin[line]-self.res-1]
                        gauss_crop_cv2_p = [gauss_res-(gauss_center_to_end+cv2bin[line]-self.res),gauss_res-1]
                        fes[fes_crop_cv1[0]:fes_crop_cv1[1]+1,\
                            fes_crop_cv2_p[0]:fes_crop_cv2_p[1]+1,\
                            fes_crop_cv3[0]:fes_crop_cv3[1]+1]\
                                    += gauss[gauss_crop_cv1[0]:gauss_crop_cv1[1]+1,\
                                             gauss_crop_cv2_p[0]:gauss_crop_cv2_p[1]+1,\
                                             gauss_crop_cv3[0]:gauss_crop_cv3[1]+1]\
                                    * self.heights[line]
                if self.periodic[2]:
                    if cv3bin[line] < gauss_center_to_end:
                        fes_crop_cv3_p = [self.res-1+(cv3bin[line]-gauss_center_to_end),self.res-1]
                        gauss_crop_cv3_p = [0,gauss_center_to_end-cv3bin[line]]
                        fes[fes_crop_cv1[0]:fes_crop_cv1[1]+1,\
                            fes_crop_cv2[0]:fes_crop_cv2[1]+1,\
                            fes_crop_cv3_p[0]:fes_crop_cv3_p[1]+1]\
                                    += gauss[gauss_crop_cv1[0]:gauss_crop_cv1[1]+1,\
                                             gauss_crop_cv2[0]:gauss_crop_cv2[1]+1,\
                                             gauss_crop_cv3_p[0]:gauss_crop_cv3_p[1]+1]\
                                    * self.heights[line]
                    
                    if cv3bin[line] > (self.res-gauss_center_to_end):
                        fes_crop_cv3_p = [0,gauss_center_to_end+cv3bin[line]-self.res-1]
                        gauss_crop_cv3_p = [gauss_res-(gauss_center_to_end+cv3bin[line]-self.res),gauss_res-1]
                        fes[fes_crop_cv1[0]:fes_crop_cv1[1]+1,\
                            fes_crop_cv2[0]:fes_crop_cv2[1]+1,\
                            fes_crop_cv3_p[0]:fes_crop_cv3_p[1]+1]\
                                    += gauss[gauss_crop_cv1[0]:gauss_crop_cv1[1]+1,\
                                             gauss_crop_cv2[0]:gauss_crop_cv2[1]+1,\
                                             gauss_crop_cv3_p[0]:gauss_crop_cv3_p[1]+1]\
                                    * self.heights[line]
                
                if self.periodic[0] and self.periodic[1]:
                    if ((cv1bin[line] < gauss_center_to_end) or (cv1bin[line] > (self.res-gauss_center_to_end))) \
                            and ((cv2bin[line] < gauss_center_to_end) or (cv2bin[line] > (self.res-gauss_center_to_end))):
                        fes[fes_crop_cv1_p[0]:fes_crop_cv1_p[1]+1,\
                            fes_crop_cv2_p[0]:fes_crop_cv2_p[1]+1,\
                            fes_crop_cv3[0]:fes_crop_cv3[1]+1]\
                                    += gauss[gauss_crop_cv1_p[0]:gauss_crop_cv1_p[1]+1,\
                                             gauss_crop_cv2_p[0]:gauss_crop_cv2_p[1]+1,\
                                             gauss_crop_cv3[0]:gauss_crop_cv3[1]+1]\
                                    * self.heights[line]
                
                if self.periodic[0] and self.periodic[2]:
                    if ((cv1bin[line] < gauss_center_to_end) or (cv1bin[line] > (self.res-gauss_center_to_end))) \
                            and ((cv3bin[line] < gauss_center_to_end) or (cv3bin[line] > (self.res-gauss_center_to_end))):
                        fes[fes_crop_cv1_p[0]:fes_crop_cv1_p[1]+1,\
                            fes_crop_cv2[0]:fes_crop_cv2[1]+1,\
                            fes_crop_cv3_p[0]:fes_crop_cv3_p[1]+1]\
                                    += gauss[gauss_crop_cv1_p[0]:gauss_crop_cv1_p[1]+1,\
                                             gauss_crop_cv2[0]:gauss_crop_cv2[1]+1,\
                                             gauss_crop_cv3_p[0]:gauss_crop_cv3_p[1]+1]\
                                    * self.heights[line]
                
                if self.periodic[1] and self.periodic[2]:
                    if ((cv2bin[line] < gauss_center_to_end) or (cv2bin[line] > (self.res-gauss_center_to_end))) \
                            and ((cv3bin[line] < gauss_center_to_end) or (cv3bin[line] > (self.res-gauss_center_to_end))):
                        fes[fes_crop_cv1[0]:fes_crop_cv1[1]+1,\
                            fes_crop_cv2_p[0]:fes_crop_cv2_p[1]+1,\
                            fes_crop_cv3_p[0]:fes_crop_cv3_p[1]+1]\
                                    += gauss[gauss_crop_cv1[0]:gauss_crop_cv1[1]+1,\
                                             gauss_crop_cv2_p[0]:gauss_crop_cv2_p[1]+1,\
                                             gauss_crop_cv3_p[0]:gauss_crop_cv3_p[1]+1]\
                                    * self.heights[line]
                
                if self.periodic[0] and self.periodic[1] and self.periodic[2]:
                    if ((cv1bin[line] < gauss_center_to_end) or (cv1bin[line] > (self.res-gauss_center_to_end)))\
                            and ((cv2bin[line] < gauss_center_to_end) or (cv2bin[line] > (self.res-gauss_center_to_end))) \
                            and ((cv3bin[line] < gauss_center_to_end) or (cv3bin[line] > (self.res-gauss_center_to_end))) :
                        fes[fes_crop_cv1_p[0]:fes_crop_cv1_p[1]+1,\
                            fes_crop_cv2_p[0]:fes_crop_cv2_p[1]+1,\
                            fes_crop_cv3_p[0]:fes_crop_cv3_p[1]+1]\
                                    += gauss[gauss_crop_cv1_p[0]:gauss_crop_cv1_p[1]+1,\
                                             gauss_crop_cv2_p[0]:gauss_crop_cv2_p[1]+1,\
                                             gauss_crop_cv3_p[0]:gauss_crop_cv3_p[1]+1]\
                                    * self.heights[line]
            
            if print_output:
                print("\n")
            if subtract_min:
                fes = fes-np.min(fes)
            self.fes = np.array(fes)
        else:
            print("Fes object doesn't have supported number of CVs.")
    
    def makefes2(self, resolution, time_min, time_max, print_output=True, subtract_min = True):
        """
        Function internally used to sum Hills in the same way as Plumed sum_hills. 
        """
        self.res = resolution
        
        if self.cvs == 1:
            fes = np.zeros((self.res))
            progress = 1
            max_progress = self.res ** self.cvs
            
            for x in range(self.res):
                progress += 1
                if print_output and (((progress) % 200 == 0) or (progress == max_progress)):
                    print(f"Constructing free energy surface: {(progress/max_progress):.2%} finished", end="\r")
                
                dist_cv1 = self.cv1[time_min-1:time_max]-(self.cv1min+(x)*self.cv1_fes_range/(self.res))
                if self.periodic[0]:
                    dist_cv1[dist_cv1<-0.5*self.cv1_fes_range] += self.cv1_fes_range
                    dist_cv1[dist_cv1>+0.5*self.cv1_fes_range] -= self.cv1_fes_range
                
                dp2 = dist_cv1**2/(2*self.s1[time_min-1:time_max]**2)
                tmp = np.zeros(time_max-time_min+1)
                heights = self.heights[time_min-1:time_max]
                tmp[dp2<6.25] = heights[dp2<6.25] * (np.exp(-dp2[dp2<6.25]) * 1.00193418799744762399 - 0.00193418799744762399)
                fes[x] = -tmp.sum()
                    
            if subtract_min:
                fes = fes - np.min(fes)
            self.fes = np.array(fes)
            if print_output:
                print("\n")
            
        elif self.cvs == 2:
            fes = np.zeros((self.res, self.res))
            progress = 0
            max_progress = self.res ** self.cvs
            
            for x in range(self.res):
                dist_cv1 = self.cv1[time_min-1:time_max]-(self.cv1min+(x)*self.cv1_fes_range/(self.res))
                if self.periodic[0]:
                    dist_cv1[dist_cv1<-0.5*self.cv1_fes_range] += self.cv1_fes_range
                    dist_cv1[dist_cv1>+0.5*self.cv1_fes_range] -= self.cv1_fes_range
                    
                for y in range(self.res):
                    progress += 1
                    if print_output and ((progress) % 200 == 0 or (progress == max_progress)):
                        print(f"Constructing free energy surface: {(progress/max_progress):.2%} finished", end="\r")
                    
                    dist_cv2 = self.cv2[time_min-1:time_max]-(self.cv2min+(y)*self.cv2_fes_range/(self.res))
                    if self.periodic[1]:
                        dist_cv2[dist_cv2<-0.5*self.cv2_fes_range] += self.cv2_fes_range
                        dist_cv2[dist_cv2>+0.5*self.cv2_fes_range] -= self.cv2_fes_range
                        
                    dp2 = dist_cv1**2/(2*self.s1[time_min-1:time_max]**2) + dist_cv2**2/(2*self.s2[time_min-1:time_max]**2)
                    tmp = np.zeros(time_max-time_min+1)
                    heights = self.heights[time_min-1:time_max]
                    tmp[dp2<6.25] = heights[dp2<6.25] * (np.exp(-dp2[dp2<6.25]) * 1.00193418799744762399 - 0.00193418799744762399)
                    fes[x,y] = -tmp.sum()
                    
            if subtract_min:
                fes = fes - np.min(fes)
            self.fes = np.array(fes)
            if print_output:
                print("\n")
            
        elif self.cvs == 3:
            fes = np.zeros((self.res, self.res, self.res))
            progress = 0
            max_progress = self.res ** self.cvs
            
            for x in range(self.res):
                dist_cv1 = self.cv1[time_min-1:time_max]-(self.cv1min+(x)*self.cv1_fes_range/(self.res))
                if self.periodic[0]:
                    dist_cv1[dist_cv1<-0.5*self.cv1_fes_range] += self.cv1_fes_range
                    dist_cv1[dist_cv1>+0.5*self.cv1_fes_range] -= self.cv1_fes_range
                    
                for y in range(self.res):
                    dist_cv2 = self.cv2[time_min-1:time_max]-(self.cv2min+(y)*self.cv2_fes_range/(self.res))
                    if self.periodic[1]:
                        dist_cv2[dist_cv2<-0.5*self.cv2_fes_range] += self.cv2_fes_range
                        dist_cv2[dist_cv2>+0.5*self.cv2_fes_range] -= self.cv2_fes_range
                        
                    for z in range(self.res):
                        progress += 1
                        if print_output and ((progress) % 200 == 0 or (progress == max_progress)):
                            print(f"Constructing free energy surface: {(progress/max_progress):.2%} finished", end="\r")
                        
                        dist_cv3 = self.cv3[time_min-1:time_max]-(self.cv3min+(z)*self.cv3_fes_range/(self.res))
                        if self.periodic[2]:
                            dist_cv3[dist_cv3<-0.5*self.cv3_fes_range] += self.cv3_fes_range
                            dist_cv3[dist_cv3>+0.5*self.cv3_fes_range] -= self.cv3_fes_range
                        
                        dp2 = dist_cv1**2/(2*self.s1[time_min-1:time_max]**2) + \
                              dist_cv2**2/(2*self.s2[time_min-1:time_max]**2) + \
                              dist_cv3**2/(2*self.s3[time_min-1:time_max]**2)
                        tmp = np.zeros(time_max-time_min+1)
                        heights = self.heights[time_min-1:time_max]
                        tmp[dp2<6.25] = heights[dp2<6.25] * (np.exp(-dp2[dp2<6.25]) * 1.00193418799744762399 - 0.00193418799744762399)
                        fes[x,y,z] = -tmp.sum()
                        
            if subtract_min:
                fes = fes - np.min(fes)
            self.fes = np.array(fes)
            if print_output:
                print("\n")
        else:
            print(f"Error: unsupported number of CVs: {self.cvs}.")
    
    def plot(self, png_name=None, contours=True, contours_spacing=0.0, aspect = 1.0, cmap = "RdYlBu_r", 
             energy_unit="kJ/mol", xlabel=None, ylabel=None, zlabel=None, label_size=12, clabel_size = 12,
             image_size=None, image_size_unit="in", dpi=100, vmin = 0, vmax = None, 
             opacity=0.2, levels=None, title = None, off_screen = False, 
             xlim=[None, None], ylim=[None, None], return_fig=False):
        """
        Function used to visualize FES, based on Matplotlib for 1D and 2D FES or PyVista for 3D FES. 
        
        ```python
        fes.plot(png_name="fes.png")
        ```
        
        Parameters:
        
        * png_name = String. If this parameter is supplied, the picture of FES will be saved under this name to the current working directory. 
        For 1D and 2D FES, the recommended format in '.png'. For 3D FES, the formats supported by PyVista are '.svg','.eps','.ps','.pdf' and '.tex'. 
        
        * contours (default=True) = whether contours should be shown on 2D FES
        
        * contours_spacing (default=0.0) = when a positive number is set, it will be used as spacing for contours on 2D FES. 
                Otherwise, if contours=True, there will be five equally spaced contour levels.
        
        * aspect (default = 1.0) = aspect ratio of the graph. Works with 1D and 2D FES. 
        
        * cmap (default = "RdYlBu_r") = Matplotlib colormap used to color 2D or 3D FES
        
        * energy_unit (default="kJ/mol") = String, used in description of colorbar

        * xlim, ylim (default = [None, None] for both) = list of two values specifying the range of x and y axes respectively. 
        None means that Matplotlib will choose appropriate range, so this keyword is useful when you need to overwrite it. 
        Does not work for 3D FES at the moment. 
        
        * xlabel, ylabel, zlabel = Strings, if provided, they will be used as labels for the graphs
        
        * labelsize (default = 12) = size of text in labels
        
        * image_size (default = [9,6]) = List of the width and height of the picture

        * image_size_unit (default = "in") = Units for width and height of the picture, accepts "in" as inches, "cm", "mm" or "px" as pixels. 

        * dpi (default = 100) = DPI of the resulting image (for 1D and 2D FES). 
        
        * vmin (default=0) = real number, lower bound for the colormap on 2D FES
        
        * vmax = real number, upper bound for the colormap on 2D FES
        
        * opacity (default=0.2) = number between 0 and 1, is the opacity of isosurfaces of 3D FES
        
        * levels = Here you can specify list of free energy values for isosurfaces on 3D FES. 
                        If not provided, default values from contours parameters will be used instead. 

        * title = optional, string that defines the title of the graph

        * offscreen (default = False) = for 3D FES only, grapf will not be shown after creation - used internally by metadynminer when making animations

        * return_fig (default=False) = whether the method should return the Matplotlib.Pyplot.figure object for further use. 
        In the case of plotting 3D FES, it returns Pyvista.plotter object instead. 
        """
        if vmax == None:
            vmax = np.max(self.fes)+0.01 # if the addition is smaller than 0.01, the 3d plot stops working. 
            
        if contours_spacing == 0.0:
            contours_spacing = (vmax-vmin)/5.0
        
        cmap = cm.get_cmap(cmap)
        
        cmap.set_over("white")
        cmap.set_under("white")
        
        if self.cvs >= 1:
            cv1min = self.cv1min
            cv1max = self.cv1max
        if self.cvs >=2:
            cv2min = self.cv2min
            cv2max = self.cv2max 
        if self.cvs == 3:
            cv3min = self.cv3min
            cv3max = self.cv3max
        
        if image_size == None:
            image_size = [9,6]    
        
        if image_size_unit == "cm":
            image_size[0] /= 2.54
            image_size[1] /= 2.54
        elif image_size_unit == "mm":
            image_size[0] /= 25.4
            image_size[1] /= 25.4
        elif image_size_unit == "px":
            image_size[0] /= dpi
            image_size[1] /= dpi
        elif image_size_unit != "in":
            print(f"Warning: unknown image_size_unit value: {image_size_unit}. Using inches instead. ")
            
        
        if self.cvs == 1:
            fig = plt.figure(figsize=(image_size[0],image_size[1]), dpi=dpi)
            X = np.linspace(cv1min, cv1max, self.res)
            plt.plot(X, self.fes)
            if xlabel == None:
                plt.xlabel(f'CV1 - {self.cv1_name}', size=label_size)
            else:
                plt.xlabel(xlabel, size=label_size)
            if ylabel == None:
                plt.ylabel(f'free energy ({energy_unit})', size=label_size)
            else:
                plt.ylabel(ylabel, size=label_size)
        
            if title != None:
                plt.title(title, size=label_size)
    
            plt.xticks(fontsize=label_size)
            plt.yticks(fontsize=label_size)
                
            ax = plt.gca()
            ax.set_xlim(xlim)
            ax.set_ylim(ylim)
            
            if png_name != None:
                plt.savefig(png_name, bbox_inches = 'tight')

            if return_fig:
                return fig
            
        if self.cvs == 2:
            fig = plt.figure(figsize=(image_size[0], image_size[1]), dpi=dpi)
            plt.imshow(np.rot90(self.fes, axes=(0,1)), cmap=cmap, interpolation='nearest', 
                       extent=[cv1min, cv1max, cv2min, cv2max], 
                       aspect = (((cv1max-cv1min)/(cv2max-cv2min))/(aspect)),
                       vmin = vmin, vmax = vmax)
            cbar = plt.colorbar()
            cbar.ax.tick_params(labelsize=label_size) 
            cbar.set_label(energy_unit, size=label_size)
            
            if contours:
                if levels != None:
                    cont = plt.contour(np.rot90(self.fes, axes=(0,1)), 
                             levels = levels, 
                             extent=[cv1min, cv1max, cv2max, cv2min], 
                             colors = "k", linestyles = "solid")
                    plt.clabel(cont, levels = levels, fontsize=clabel_size)
                else:
                    cont = plt.contour(np.rot90(self.fes, axes=(0,1)), 
                             levels = np.arange(vmin, (vmax - 0.01), contours_spacing), 
                             extent=[cv1min, cv1max, cv2max, cv2min], 
                             colors = "k", linestyles = "solid")
                    plt.clabel(cont, levels = np.arange(vmin, (vmax - 0.01), contours_spacing), fontsize=clabel_size)
            
            if xlabel == None:
                plt.xlabel(f'CV1 - {self.cv1_name}', size=label_size)
            else:
                plt.xlabel(xlabel, size=label_size)
                
            if ylabel == None:
                plt.ylabel(f'CV2 - {self.cv2_name}', size=label_size)
            else:
                plt.ylabel(ylabel, size=label_size)
            
            if title != None:
                plt.title(title, size=label_size)
    
            plt.xticks(fontsize=label_size)
            plt.yticks(fontsize=label_size)

            ax = plt.gca()
            ax.set_xlim(xlim)
            ax.set_ylim(ylim)

            ax.tick_params(axis='x', labelsize=label_size)
            ax.tick_params(axis='y', labelsize=label_size)
            
            if png_name != None:
                plt.savefig(png_name, bbox_inches = 'tight')
            
            if return_fig:
                return fig
        
        if self.cvs == 3:
            if xlabel == None:
                xlabel = "CV1 - " + self.cv1_name
            if ylabel == None:
                ylabel = "CV2 - " + self.cv2_name
            if zlabel == None:
                zlabel = "CV3 - " + self.cv3_name
            
            grid = pv.ImageData(
                dimensions=(self.res, self.res, self.res),
                spacing=((cv1max-cv1min)/self.res,(cv2max-cv2min)/self.res,(cv3max-cv3min)/self.res),
                origin=(cv1min, cv2min, cv3min)
            )
            grid["vol"] = self.fes.ravel(order="F")
            if levels == None:
                contours = grid.contour(np.arange(0, (vmax - 0.01), contours_spacing))
            else:
                contours = grid.contour(levels)
            fescolors = []
            for i in range(contours.points.shape[0]):
                fescolors.append(self.fes[int((contours.points[i,0]-cv1min)*self.res/(cv1max-cv1min)),
                                          int((contours.points[i,1]-cv2min)*self.res/(cv2max-cv2min)),
                                          int((contours.points[i,2]-cv3min)*self.res/(cv3max-cv3min))])
            #%% Visualization
            bounds = [cv1min, cv1max, cv2min, cv2max, cv3min, cv3max]
            pv.set_plot_theme('document')
            p = pv.Plotter()
            p.add_mesh(contours, scalars=fescolors, opacity=opacity, cmap=cmap, show_scalar_bar=False, interpolate_before_map=True)
            p.show_bounds(bounds=bounds, xtitle=xlabel, ytitle=ylabel, ztitle=zlabel, grid=True, location="outer")
            if title != None:
                p.add_title(title, font_size=label_size)
            if not off_screen:
                p.show()
            
            if png_name != None:
                p.save_graphic(png_name)
            
            if return_fig:
                return p
    
    def set_fes(self, fes):
        self.fes = fes
        
    def surface_plot(self, png_name=None, cmap = "RdYlBu_r", 
                     energy_unit="kJ/mol", xlabel=None, ylabel=None, zlabel=None, dpi=100, label_size=12, image_size=None, image_size_unit="in", rstride=1, cstride=1, vmin = 0, vmax = None, title=None, return_fig=False):
        """
        Function for visualization of 2D FES as 3D surface plot. For now, it is based on Matplotlib, but there are issues with interactivity. 
        
        It can be interacted with in jupyter notebook or jupyter lab in %matplotlib widget mode. Otherwise it is just static image of the 3D surface plot. 
        
        ```python
        %matplotlib widget
        fes.surface_plot()
        ```
        
        """
        if self.cvs == 2:
            cv1min = self.cv1min
            cv1max = self.cv1max
            cv2min = self.cv2min
            cv2max = self.cv2max 
            
            x = np.linspace(cv1min, cv1max, self.res)
            y = np.linspace(cv2min, cv2max, self.res)
            
            X, Y = np.meshgrid(x, y)
            Z = self.fes.T
    
            if image_size == None:
                image_size = [9,9]
            
            if image_size_unit == "cm":
                image_size[0] /= 2.54
                image_size[1] /= 2.54
            elif image_size_unit == "mm":
                image_size[0] /= 25.4
                image_size[1] /= 25.4
            elif image_size_unit == "px":
                image_size[0] /= dpi
                image_size[1] /= dpi
            elif image_size_unit != "in":
                print(f"Warning: unknown image_size_unit value: {image_size_unit}. Using inches instead. ")
            
            fig = plt.figure(figsize=(image_size[0],image_size[1]), dpi=dpi)
            ax = fig.add_subplot(projection="3d")
            ax.plot_surface(X,Y,Z, cmap=cmap, rstride=rstride, cstride=cstride)
            
            if xlabel == None:
                ax.set_xlabel(f'CV1 - {self.cv1_name}', size=label_size)
            else:
                ax.set_xlabel(xlabel, size=label_size)
            if ylabel == None:
                ax.set_ylabel(f'CV2 - {self.cv2_name}', size=label_size)
            else:
                ax.set_ylabel(ylabel, size=label_size)
            if zlabel == None:
                ax.set_zlabel(f'free energy ({energy_unit})', size=label_size)
            else:
                ax.set_zlabel(zlabel, size=label_size)
    
            if title != None:
                plt.title(title, size=label_size)
    
            plt.xticks(fontsize=label_size)
            plt.yticks(fontsize=label_size)
    
            if png_name != None:
                plt.savefig(png_name, bbox_inches = 'tight')
            
            if return_fig:
                return fig
                
        else:
            print(f"Error: Surface plot only works for FES with exactly two CVs, and this FES has {self.cvs} CV.")
    
    def remove_CV(self, CV=None, energy_unit="kJ/mol", temp=300.0):
        """
        This function is used to remove a CV from an existing FES. The function first recalculates the FES to an array of probabilities. The probabilities 
        are summed along the CV to be removed, and resulting probability distribution with 1 less dimension is converted back to FES. 

        ```python
        fes_cv1 = fes.remove_CV(CV=2)
        ```
        
        Parameters:
        
        * CV = integer, the CV to be removed
        
        * energy_unit (default="kJ/mol") = has to be either "kJ/mol" or "kcal/mol". Make sure to suply the correct energy unit, otherwise you will get wrong FES as a result. 
        
        * temp (default=300.0) = temperature of the simulation in Kelvins.
        """
        CV = int(float(CV))
        print(f"Removing CV {CV}.")
        if CV > self.cvs:
            print("Error: The CV to remove is not available in this FES object.")
            return None
        if self.cvs == 1:
            print("Error: You can not remove the only CV. ")
            return None
        elif self.cvs == 2:
            if energy_unit == "kJ/mol":
                probabilities = np.exp(-1000*self.fes/8.314/temp)
                if CV == 1:
                    new_prob = np.sum(probabilities, axis=0)
                    new_fes = Fes(hills=None)
                    new_fes.fes = -8.314*temp*np.log(new_prob)/1000
                    new_fes.fes = new_fes.fes - np.min(new_fes.fes)
                    new_fes.cvs = 1
                    new_fes.res = self.res
                    new_fes.periodic = [self.periodic[1]]
                    new_fes.cv1min = self.cv2min
                    new_fes.cv1max = self.cv2max
                    new_fes.cv1_name = self.cv2_name
                    new_fes.cv1per = self.cv2per
                    new_fes.cv1_fes_range = self.cv2_fes_range
                if CV == 2:
                    new_prob = np.sum(probabilities, axis=1)
                    new_fes = Fes(hills=None)
                    new_fes.fes = -8.314*temp*np.log(new_prob)/1000
                    new_fes.fes = new_fes.fes - np.min(new_fes.fes)
                    new_fes.cvs = 1
                    new_fes.res = self.res
                    new_fes.periodic = [self.periodic[0]]
                    new_fes.cv1min = self.cv1min
                    new_fes.cv1max = self.cv1max
                    new_fes.cv1_name = self.cv1_name
                    new_fes.cv1per = self.cv1per
                    new_fes.cv1_fes_range = self.cv1_fes_range
                return new_fes
            elif energy_unit == "kcal/mol":
                probabilities = np.exp(-1000*4.184*self.fes/8.314/temp)
                if CV == 1:
                    new_prob = np.sum(probabilities, axis=1)
                    new_fes = Fes(hills=None)
                    new_fes.fes = -8.314*temp*np.log(new_prob)/1000/4.184
                    new_fes.fes = new_fes.fes - np.min(new_fes.fes)
                    new_fes.cvs = 1
                    new_fes.res = self.res
                    new_fes.periodic = [self.periodic[1]]
                    new_fes.cv1min = self.cv2min
                    new_fes.cv1max = self.cv2max
                    new_fes.cv1_name = self.cv2_name
                    new_fes.cv1per = self.cv2per
                    new_fes.cv1_fes_range = self.cv2_fes_range
                if CV == 2:
                    new_prob = np.sum(probabilities, axis=0)
                    new_fes = Fes(hills=None)
                    new_fes.fes = -8.314*temp*np.log(new_prob)/1000/4.184
                    new_fes.fes = new_fes.fes - np.min(new_fes.fes)
                    new_fes.cvs = 1
                    new_fes.res = self.res
                    new_fes.periodic = [self.periodic[0]]
                    new_fes.cv1min = self.cv1min
                    new_fes.cv1max = self.cv1max
                    new_fes.cv1_name = self.cv1_name
                    new_fes.cv1per = self.cv1per
                    new_fes.cv1_fes_range = self.cv1_fes_range
                return new_fes
            else:
                print("Error: unknown energy unit")
                return None
        elif self.cvs == 3:
            if energy_unit == "kJ/mol":
                probabilities = np.exp(-1000*self.fes/8.314/temp)
                if CV == 1:
                    new_prob = np.sum(probabilities, axis=0)
                    new_fes = Fes(hills=None)
                    new_fes.fes = -8.314*temp*np.log(new_prob)/1000
                    new_fes.fes = new_fes.fes - np.min(new_fes.fes)
                    new_fes.cvs = 2
                    new_fes.res = self.res
                    new_fes.periodic = [self.periodic[1], self.periodic[2]]
                    new_fes.cv1min = self.cv2min
                    new_fes.cv1max = self.cv2max
                    new_fes.cv2min = self.cv3min
                    new_fes.cv2max = self.cv3max
                    new_fes.cv1_name = self.cv2_name
                    new_fes.cv2_name = self.cv3_name
                    new_fes.cv1per = self.cv2per
                    new_fes.cv2per = self.cv3per
                    new_fes.cv1_fes_range = self.cv2_fes_range
                    new_fes.cv2_fes_range = self.cv3_fes_range
                if CV == 2:
                    new_prob = np.sum(probabilities, axis=1)
                    new_fes = Fes(hills=None)
                    new_fes.fes = -8.314*temp*np.log(new_prob)/1000
                    new_fes.fes = new_fes.fes - np.min(new_fes.fes)
                    new_fes.cvs = 2
                    new_fes.res = self.res
                    new_fes.periodic = [self.periodic[0], self.periodic[2]]
                    new_fes.cv1min = self.cv1min
                    new_fes.cv1max = self.cv1max
                    new_fes.cv2min = self.cv3min
                    new_fes.cv2max = self.cv3max
                    new_fes.cv1_name = self.cv1_name
                    new_fes.cv2_name = self.cv3_name
                    new_fes.cv1per = self.cv1per
                    new_fes.cv2per = self.cv3per
                    new_fes.cv1_fes_range = self.cv1_fes_range
                    new_fes.cv2_fes_range = self.cv3_fes_range
                if CV == 3:
                    new_prob = np.sum(probabilities, axis=2)
                    new_fes = Fes(hills=None)
                    new_fes.fes = -8.314*temp*np.log(new_prob)/1000
                    new_fes.fes = new_fes.fes - np.min(new_fes.fes)
                    new_fes.cvs = 2
                    new_fes.res = self.res
                    new_fes.periodic = [self.periodic[0], self.periodic[1]]
                    new_fes.cv1min = self.cv1min
                    new_fes.cv1max = self.cv1max
                    new_fes.cv2min = self.cv2min
                    new_fes.cv2max = self.cv2max
                    new_fes.cv1_name = self.cv1_name
                    new_fes.cv2_name = self.cv2_name
                    new_fes.cv1per = self.cv1per
                    new_fes.cv2per = self.cv2per
                    new_fes.cv1_fes_range = self.cv1_fes_range
                    new_fes.cv2_fes_range = self.cv2_fes_range
                return new_fes
            elif energy_unit == "kcal/mol":
                probabilities = np.exp(-1000*4.184*self.fes/8.314/temp)
                if CV == 1:
                    new_prob = np.sum(probabilities, axis=0)
                    new_fes = Fes(hills=None)
                    new_fes.fes = -8.314*temp*np.log(new_prob)/1000/4.184
                    new_fes.fes = new_fes.fes - np.min(new_fes.fes)
                    new_fes.cvs = 2
                    new_fes.res = self.res
                    new_fes.periodic = [self.periodic[1], self.periodic[2]]
                    new_fes.cv1min = self.cv2min
                    new_fes.cv1max = self.cv2max
                    new_fes.cv2min = self.cv3min
                    new_fes.cv2max = self.cv3max
                    new_fes.cv1_name = self.cv2_name
                    new_fes.cv2_name = self.cv3_name
                    new_fes.cv1per = self.cv2per
                    new_fes.cv2per = self.cv3per
                    new_fes.cv1_fes_range = self.cv2_fes_range
                    new_fes.cv2_fes_range = self.cv3_fes_range
                if CV == 2:
                    new_prob = np.sum(probabilities, axis=1)
                    new_fes = Fes(hills=None)
                    new_fes.fes = -8.314*temp*np.log(new_prob)/1000/4.184
                    new_fes.fes = new_fes.fes - np.min(new_fes.fes)
                    new_fes.cvs = 2
                    new_fes.res = self.res
                    new_fes.periodic = [self.periodic[0], self.periodic[2]]
                    new_fes.cv1min = self.cv1min
                    new_fes.cv1max = self.cv1max
                    new_fes.cv2min = self.cv3min
                    new_fes.cv2max = self.cv3max
                    new_fes.cv1_name = self.cv1_name
                    new_fes.cv2_name = self.cv3_name
                    new_fes.cv1per = self.cv1per
                    new_fes.cv2per = self.cv3per
                    new_fes.cv1_fes_range = self.cv1_fes_range
                    new_fes.cv2_fes_range = self.cv3_fes_range
                if CV == 3:
                    new_prob = np.sum(probabilities, axis=2)
                    new_fes = Fes(hills=None)
                    new_fes.fes = -8.314*temp*np.log(new_prob)/1000/4.184
                    new_fes.fes = new_fes.fes - np.min(new_fes.fes)
                    new_fes.cvs = 2
                    new_fes.res = self.res
                    new_fes.periodic = [self.periodic[0], self.periodic[1]]
                    new_fes.cv1min = self.cv1min
                    new_fes.cv1max = self.cv1max
                    new_fes.cv2min = self.cv2min
                    new_fes.cv2max = self.cv2max
                    new_fes.cv1_name = self.cv1_name
                    new_fes.cv2_name = self.cv2_name
                    new_fes.cv1per = self.cv1per
                    new_fes.cv2per = self.cv2per
                    new_fes.cv1_fes_range = self.cv1_fes_range
                    new_fes.cv2_fes_range = self.cv2_fes_range
                return new_fes
            else:
                print("Error: unknown energy unit")
                return None

    def flooding_animation(self, gif_name = "flooding.gif", use_vmax_from_end = True, with_minima = True, use_minima_from_end=False, cmap="RdYlBu_r", xlabel=None, ylabel=None, zlabel=None, label_size=12, image_size=None, image_size_unit="in", dpi=100, tu = "ps", time_min=None, time_max=None, step=1000, contours_spacing = 20, levels=None, opacity = 0.2, vmin = 0, vmax = None, energy_unit="kJ/mol", clear_temporary_folder=True, temporary_folder_name="temporary_folder", time_unit="ps", fps=5, enable_loop = True, minima_precise = False, minima_nbins=8, temp=300.0, 
                           xlim=[None, None], ylim=[None, None]):
        """
        This method is used to make an animation that shows, how the FES was evolving during metadynamics simulation. It creates temporary folder and svaes plots of FES at different times during simulation, then it concatenates them to make a gif animation and removes the temporary files (remove can be switched off, if necessary). 

        Use:
        
        ```python
        fes.flooding_animation()
        ```
        Parameters:
        * gif_name (default="flooding.gif") = name for the animation file

        * optional parameters used when calling
        ```python
        fes.plot()
        ```
        or
        ```python
        minima.plot()
        ```
        can be used to adjust the graphs used to create animation

        * use_vmax_from_end (default=True), boolean value, if it is True and vmax is not specified, it will use the vmax value from the end point of the fes object for all frames of animation; then, for 2D FES, the colors will be comparable to each other

        * with_minima (default = True), boolean value, if True, graphs will be shown with letters at each local minima found at each frame of the animation

        * use _minima_from_end (default = True), boolean value, if True, the local minima from the end point of the fes object wil be depicted at each frame of animation

        * minima_precise (default=False) = whether the local minima should be calculated using the precise algorithm (see Minima for more info)

        * minima_nbins (default = 8) = the nbins keyword for minima localization

        * temp (default = 300.0), energy_unit (default = "kJ/mol") - keywords for minima localization in case minima_precise = True

        * step (default=1000), integer, frames for animation will be made at each n-th line in HILLS file

        * temporary_folder_name (default="temporary_folder"), name of the temporary folder where the individual graphs will be saved; directory with this name shouldn't be present in working irectory when calling the method, otherwise it will throw an error
        
        * clear_temporary_folder (default=True), if set to false, the graphs made for each frame of the simulation will not be removed afterwards, so they can be viewed later

        * fps (defalt=5) = how many frames per second the animation will have

        * enable_loop (default=True) = whether the animation should be running in loop
        """
        tu = TU(tu)
        
        current_directory = os.getcwd()
        final_directory = os.path.join(current_directory, temporary_folder_name)
        if os.path.exists(final_directory):
            print(f"Error: directory with the name {temporary_folder_name} already exists. Try using different name using temporary_folder_name keyword. ")
            return None
        else:
           os.makedirs(final_directory)
        flooding_fes = copy.deepcopy(self)
        step_fes = copy.deepcopy(self)
        
        if time_min == time_max == 0:
            print("Error: Values of start and end time are zero.")
            return None
        if time_min != None:
            time_min = tu.inps(time_min)
            if time_min < 0:
                print("Warning: Start time is lower than zero, it will be set to zero instead. ")
                time_min = 0
            if time_min < int(self.hills.hills[0,0]):
                print(f"Warning: Start time {tu.inps(time_min)} ps is lower than the first time from HILLS file {int(self.hills.hills[0,0])}, which will be used instead. ")
                time_min = int(self.hills.hills[0,0])
        else:
            time_min = int(self.hills.hills[0,0])
        if time_max != None:
            time_max = tu.inps(time_max)
            if time_max < time_min:
                print("Warning: End time is lower than start time. Values are flipped. ")
                time_value = time_max
                time_max = time_min
                time_min = time_value
            if time_max > int(self.hills.hills[-1,0]):
                print(f"Warning: End time {tu.inps(time_max)} ps is higher than number of lines in HILLS file {int(self.hills.hills[-1,0])}, which will be used instead. ")
                time_max = int(self.hills.hills[-1,0])
        else:
            time_max = int(self.hills.hills[-1,0])
        
        if not self.ignoretime:
            time_max = int(round(((time_max - self.hills.hills[0,0])/self.dt),0)) + 1
            time_min = int(round(((time_min - self.hills.hills[0,0])/self.dt),0)) + 1

        if time_min==None:
            time_min=1
        if time_max==None:
            time_max = int(self.hills.hills[-1,0])

        if (vmax == None) and use_vmax_from_end:
            vmax = np.max(self.fes)+0.1
        if self.cvs == 1 or self.cvs == 2:
            suffix="png"
        if self.cvs == 3:
            suffix="eps"
        
        times = np.array((range(time_min-1, time_max+1, step)))
        image_files = [f'{final_directory}/{times[i]}.{suffix}'.format(i) for i in range(1, len(times))]
        
        minima_final = Minima(self, precise=minima_precise, nbins=minima_nbins, energy_unit=energy_unit, temp=temp, print_output=False)
        
        for i in range(1,len(times)):
            print(f"Constructing flooding animation: {((i+1)/len(times)):.2%} finished", end="\r")
            if self.original==False:
                step_fes.makefes(flooding_fes.res, time_min=(times[i-1]+1), time_max=times[i], print_output=False)
            else:
                step_fes.makefes2(flooding_fes.res, time_min=(times[i-1]+1), time_max=times[i], print_output=False)
            if i == 1:
                flooding_fes.fes = step_fes.fes
            else:
                flooding_fes.fes += step_fes.fes
            flooding_fes.fes = flooding_fes.fes - np.min(flooding_fes.fes)
            try:
                if with_minima:
                    mf = Minima(flooding_fes, precise=minima_precise, print_output=False, nbins=minima_nbins, temp=temp, energy_unit=energy_unit)
                    if use_minima_from_end:
                        mf.minima = minima_final.minima
                    mf.plot(contours_spacing=contours_spacing, cmap = cmap, 
                            xlabel = xlabel, ylabel = ylabel, zlabel=zlabel, 
                            label_size=label_size, image_size=image_size, 
                            image_size_unit=image_size_unit, dpi=dpi, vmin = vmin, 
                            vmax = vmax, levels=levels, energy_unit=energy_unit, 
                            off_screen = True, opacity=opacity, 
                            png_name=f"{final_directory}/{times[i]}.{suffix}", title=f"{times[i]} {time_unit}", xlim=xlim, ylim=ylim)
                else:
                    flooding_fes.plot(contours_spacing=contours_spacing, cmap = cmap, xlabel = xlabel, ylabel = ylabel, zlabel=zlabel, label_size=label_size, image_size=image_size, image_size_unit=image_size_unit, dpi = dpi, vmin = vmin, vmax = vmax, levels=levels, energy_unit=energy_unit, off_screen = True, opacity=opacity, png_name=f"{final_directory}/{times[i]}.{suffix}", title=f"{times[i]} {time_unit}", xlim=xlim, ylim=ylim)
                plt.close()
            except ValueError:
                print("Warning: The first frame of animation would be blank with the current settings, but PyVista 3D plotter can not plot empty meshes. Try to increase the timestep between frames or decrease the spacing between isosurfaces.")
        print("\n")

        duration = 1000/fps
        if enable_loop:
            with imageio.get_writer(gif_name, format="GIF", duration=duration, loop=0) as writer:
                for image_file in image_files:
                    try:
                        image = imageio.imread(image_file)
                        writer.append_data(image)
                    except FileNotFoundError:
                        print("Warning: File for animation was not found.")
        else:
            with imageio.get_writer(gif_name, format="GIF", fps = fps) as writer:
                for image_file in image_files:
                    try:
                        image = imageio.imread(image_file)
                        writer.append_data(image)
                    except FileNotFoundError:
                        print("Warning: File for animation was not found.")

        if clear_temporary_folder:
            for image_file in image_files:
                try:
                    os.remove(image_file)
                except FileNotFoundError:
                        print("Warning: File for animation was not found.")
            try: 
                os.rmdir(final_directory)
            except OSError:
                print(f"Warning: directory{final_directory} is not empty and will not be removed. Metadynminer's temporary files inside were removed. ")
               

    
    def make_gif(self, gif_name=None, cmap = "RdYlBu_r", energy_unit="kJ/mol",
                 xlabel=None, ylabel=None, zlabel=None, label_size=12, image_size=[10,7], 
                  opacity=0.2, levels=None, frames=64):
        """
        Function that generates animation of 3D FES showing different isosurfaces.
        
        ```python
        fes.make_gif(gif_name="FES_animation.gif")
        ```
        
        Parameters:
        
        * gif_name (default="FES.gif") = String. Name of the gif of FES that will be saved in the current working directory.
        
        * cmap (default = "RdYlBu_r") = Matplotlib colormap used to color the 3D FES
        
        * xlabel, ylabel, zlabel = Strings, if provided, they will be used as labels for the graph
        
        * labelsize (default = 12) = size of text in labels
        
        * image_size (default = [10,7]) = List of the width and height of the picture
        
        * opacity (default = 0.2) = number between 0 and 1, is the opacity of isosurfaces of 3D FES
        
        * levels = Here you can specify list of free energy values for isosurfaces on 3D FES. 
                If not provided, default values from contours parameters will be used instead. 
        
        * frames (default = 64) = Number of frames the animation will be made of. 
        """
        if self.cvs == 3:
            cv1min = self.cv1min
            cv1max = self.cv1max
            cv2min = self.cv2min
            cv2max = self.cv2max
            cv3min = self.cv3min
            cv3max = self.cv3max
            
            values = np.linspace(np.min(self.fes)+0.1, np.max(self.fes), num=frames)
            grid = pv.ImageData(
                dimensions=(self.res, self.res, self.res),
                spacing=((cv1max-cv1min)/self.res,(cv2max-cv2min)/self.res,(cv3max-cv3min)/self.res),
                origin=(cv1min, cv2min, cv3min),
            )
            grid["vol"] = self.fes.ravel(order="F")
            surface = grid.contour(values[:1])
            surfaces = [grid.contour([v]) for v in values]
            surface = surfaces[0].copy()
            
            pv.set_plot_theme('document')
            plotter = pv.Plotter(off_screen=True)
            # Open a movie file
            if gif_name == None:
                gif_name = "FES_animation.gif"
            plotter.open_gif(gif_name)

            # Add initial mesh
            plotter.add_mesh(
                surface,
                opacity=opacity,
                clim=grid.get_data_range(),
                show_scalar_bar=False,
                cmap="RdYlBu_r"
            )
            plotter.add_mesh(grid.outline_corners(), color="k")
            text = plotter.add_text(f"{values[0]:.2f}+kJ/mol", position='lower_right', font_size=12)
            if xlabel == None and ylabel == None and zlabel == None:
                plotter.show_grid(xtitle=f"CV1 - {self.cv1_name}", ytitle=f"CV2 - {self.cv2_name}", ztitle=f"CV3 - {self.cv3_name}")
            else:
                plotter.show_grid(xtitle=xlabel, ytitle=ylabel, ztitle=zlabel)
            plotter.set_background('white')
            plotter.show(auto_close=False)

            # Run through each frame
            for surf in range(len(surfaces)):
                surface.copy_from(surfaces[surf])
                plotter.remove_actor(text)
                text = plotter.add_text(f"{values[surf]:.2f} {energy_unit}", position='lower_right', font_size=12)
                plotter.write_frame()  # Write this frame
            # Run through backwards
            for surf in range(len(surfaces)-1,0,-1):
                surface.copy_from(surfaces[surf])
                plotter.remove_actor(text)
                text = plotter.add_text(f"{values[surf]:.2f} {energy_unit}", position='lower_right', font_size=12)
                plotter.write_frame()  # Write this frame

            # Be sure to close the plotter when finished
            plotter.close()
        else:
            print("Error: this method is only available for FES with 3 CVs.")     

Methods

def flooding_animation(self, gif_name='flooding.gif', use_vmax_from_end=True, with_minima=True, use_minima_from_end=False, cmap='RdYlBu_r', xlabel=None, ylabel=None, zlabel=None, label_size=12, image_size=None, image_size_unit='in', dpi=100, tu='ps', time_min=None, time_max=None, step=1000, contours_spacing=20, levels=None, opacity=0.2, vmin=0, vmax=None, energy_unit='kJ/mol', clear_temporary_folder=True, temporary_folder_name='temporary_folder', time_unit='ps', fps=5, enable_loop=True, minima_precise=False, minima_nbins=8, temp=300.0, xlim=[None, None], ylim=[None, None])

This method is used to make an animation that shows, how the FES was evolving during metadynamics simulation. It creates temporary folder and svaes plots of FES at different times during simulation, then it concatenates them to make a gif animation and removes the temporary files (remove can be switched off, if necessary).

Use:

fes.flooding_animation()

Parameters: * gif_name (default="flooding.gif") = name for the animation file

  • optional parameters used when calling
fes.plot()

or

minima.plot()

can be used to adjust the graphs used to create animation

  • use_vmax_from_end (default=True), boolean value, if it is True and vmax is not specified, it will use the vmax value from the end point of the fes object for all frames of animation; then, for 2D FES, the colors will be comparable to each other

  • with_minima (default = True), boolean value, if True, graphs will be shown with letters at each local minima found at each frame of the animation

  • use _minima_from_end (default = True), boolean value, if True, the local minima from the end point of the fes object wil be depicted at each frame of animation

  • minima_precise (default=False) = whether the local minima should be calculated using the precise algorithm (see Minima for more info)

  • minima_nbins (default = 8) = the nbins keyword for minima localization

  • temp (default = 300.0), energy_unit (default = "kJ/mol") - keywords for minima localization in case minima_precise = True

  • step (default=1000), integer, frames for animation will be made at each n-th line in HILLS file

  • temporary_folder_name (default="temporary_folder"), name of the temporary folder where the individual graphs will be saved; directory with this name shouldn't be present in working irectory when calling the method, otherwise it will throw an error

  • clear_temporary_folder (default=True), if set to false, the graphs made for each frame of the simulation will not be removed afterwards, so they can be viewed later

  • fps (defalt=5) = how many frames per second the animation will have

  • enable_loop (default=True) = whether the animation should be running in loop

Expand source code
def flooding_animation(self, gif_name = "flooding.gif", use_vmax_from_end = True, with_minima = True, use_minima_from_end=False, cmap="RdYlBu_r", xlabel=None, ylabel=None, zlabel=None, label_size=12, image_size=None, image_size_unit="in", dpi=100, tu = "ps", time_min=None, time_max=None, step=1000, contours_spacing = 20, levels=None, opacity = 0.2, vmin = 0, vmax = None, energy_unit="kJ/mol", clear_temporary_folder=True, temporary_folder_name="temporary_folder", time_unit="ps", fps=5, enable_loop = True, minima_precise = False, minima_nbins=8, temp=300.0, 
                       xlim=[None, None], ylim=[None, None]):
    """
    This method is used to make an animation that shows, how the FES was evolving during metadynamics simulation. It creates temporary folder and svaes plots of FES at different times during simulation, then it concatenates them to make a gif animation and removes the temporary files (remove can be switched off, if necessary). 

    Use:
    
    ```python
    fes.flooding_animation()
    ```
    Parameters:
    * gif_name (default="flooding.gif") = name for the animation file

    * optional parameters used when calling
    ```python
    fes.plot()
    ```
    or
    ```python
    minima.plot()
    ```
    can be used to adjust the graphs used to create animation

    * use_vmax_from_end (default=True), boolean value, if it is True and vmax is not specified, it will use the vmax value from the end point of the fes object for all frames of animation; then, for 2D FES, the colors will be comparable to each other

    * with_minima (default = True), boolean value, if True, graphs will be shown with letters at each local minima found at each frame of the animation

    * use _minima_from_end (default = True), boolean value, if True, the local minima from the end point of the fes object wil be depicted at each frame of animation

    * minima_precise (default=False) = whether the local minima should be calculated using the precise algorithm (see Minima for more info)

    * minima_nbins (default = 8) = the nbins keyword for minima localization

    * temp (default = 300.0), energy_unit (default = "kJ/mol") - keywords for minima localization in case minima_precise = True

    * step (default=1000), integer, frames for animation will be made at each n-th line in HILLS file

    * temporary_folder_name (default="temporary_folder"), name of the temporary folder where the individual graphs will be saved; directory with this name shouldn't be present in working irectory when calling the method, otherwise it will throw an error
    
    * clear_temporary_folder (default=True), if set to false, the graphs made for each frame of the simulation will not be removed afterwards, so they can be viewed later

    * fps (defalt=5) = how many frames per second the animation will have

    * enable_loop (default=True) = whether the animation should be running in loop
    """
    tu = TU(tu)
    
    current_directory = os.getcwd()
    final_directory = os.path.join(current_directory, temporary_folder_name)
    if os.path.exists(final_directory):
        print(f"Error: directory with the name {temporary_folder_name} already exists. Try using different name using temporary_folder_name keyword. ")
        return None
    else:
       os.makedirs(final_directory)
    flooding_fes = copy.deepcopy(self)
    step_fes = copy.deepcopy(self)
    
    if time_min == time_max == 0:
        print("Error: Values of start and end time are zero.")
        return None
    if time_min != None:
        time_min = tu.inps(time_min)
        if time_min < 0:
            print("Warning: Start time is lower than zero, it will be set to zero instead. ")
            time_min = 0
        if time_min < int(self.hills.hills[0,0]):
            print(f"Warning: Start time {tu.inps(time_min)} ps is lower than the first time from HILLS file {int(self.hills.hills[0,0])}, which will be used instead. ")
            time_min = int(self.hills.hills[0,0])
    else:
        time_min = int(self.hills.hills[0,0])
    if time_max != None:
        time_max = tu.inps(time_max)
        if time_max < time_min:
            print("Warning: End time is lower than start time. Values are flipped. ")
            time_value = time_max
            time_max = time_min
            time_min = time_value
        if time_max > int(self.hills.hills[-1,0]):
            print(f"Warning: End time {tu.inps(time_max)} ps is higher than number of lines in HILLS file {int(self.hills.hills[-1,0])}, which will be used instead. ")
            time_max = int(self.hills.hills[-1,0])
    else:
        time_max = int(self.hills.hills[-1,0])
    
    if not self.ignoretime:
        time_max = int(round(((time_max - self.hills.hills[0,0])/self.dt),0)) + 1
        time_min = int(round(((time_min - self.hills.hills[0,0])/self.dt),0)) + 1

    if time_min==None:
        time_min=1
    if time_max==None:
        time_max = int(self.hills.hills[-1,0])

    if (vmax == None) and use_vmax_from_end:
        vmax = np.max(self.fes)+0.1
    if self.cvs == 1 or self.cvs == 2:
        suffix="png"
    if self.cvs == 3:
        suffix="eps"
    
    times = np.array((range(time_min-1, time_max+1, step)))
    image_files = [f'{final_directory}/{times[i]}.{suffix}'.format(i) for i in range(1, len(times))]
    
    minima_final = Minima(self, precise=minima_precise, nbins=minima_nbins, energy_unit=energy_unit, temp=temp, print_output=False)
    
    for i in range(1,len(times)):
        print(f"Constructing flooding animation: {((i+1)/len(times)):.2%} finished", end="\r")
        if self.original==False:
            step_fes.makefes(flooding_fes.res, time_min=(times[i-1]+1), time_max=times[i], print_output=False)
        else:
            step_fes.makefes2(flooding_fes.res, time_min=(times[i-1]+1), time_max=times[i], print_output=False)
        if i == 1:
            flooding_fes.fes = step_fes.fes
        else:
            flooding_fes.fes += step_fes.fes
        flooding_fes.fes = flooding_fes.fes - np.min(flooding_fes.fes)
        try:
            if with_minima:
                mf = Minima(flooding_fes, precise=minima_precise, print_output=False, nbins=minima_nbins, temp=temp, energy_unit=energy_unit)
                if use_minima_from_end:
                    mf.minima = minima_final.minima
                mf.plot(contours_spacing=contours_spacing, cmap = cmap, 
                        xlabel = xlabel, ylabel = ylabel, zlabel=zlabel, 
                        label_size=label_size, image_size=image_size, 
                        image_size_unit=image_size_unit, dpi=dpi, vmin = vmin, 
                        vmax = vmax, levels=levels, energy_unit=energy_unit, 
                        off_screen = True, opacity=opacity, 
                        png_name=f"{final_directory}/{times[i]}.{suffix}", title=f"{times[i]} {time_unit}", xlim=xlim, ylim=ylim)
            else:
                flooding_fes.plot(contours_spacing=contours_spacing, cmap = cmap, xlabel = xlabel, ylabel = ylabel, zlabel=zlabel, label_size=label_size, image_size=image_size, image_size_unit=image_size_unit, dpi = dpi, vmin = vmin, vmax = vmax, levels=levels, energy_unit=energy_unit, off_screen = True, opacity=opacity, png_name=f"{final_directory}/{times[i]}.{suffix}", title=f"{times[i]} {time_unit}", xlim=xlim, ylim=ylim)
            plt.close()
        except ValueError:
            print("Warning: The first frame of animation would be blank with the current settings, but PyVista 3D plotter can not plot empty meshes. Try to increase the timestep between frames or decrease the spacing between isosurfaces.")
    print("\n")

    duration = 1000/fps
    if enable_loop:
        with imageio.get_writer(gif_name, format="GIF", duration=duration, loop=0) as writer:
            for image_file in image_files:
                try:
                    image = imageio.imread(image_file)
                    writer.append_data(image)
                except FileNotFoundError:
                    print("Warning: File for animation was not found.")
    else:
        with imageio.get_writer(gif_name, format="GIF", fps = fps) as writer:
            for image_file in image_files:
                try:
                    image = imageio.imread(image_file)
                    writer.append_data(image)
                except FileNotFoundError:
                    print("Warning: File for animation was not found.")

    if clear_temporary_folder:
        for image_file in image_files:
            try:
                os.remove(image_file)
            except FileNotFoundError:
                    print("Warning: File for animation was not found.")
        try: 
            os.rmdir(final_directory)
        except OSError:
            print(f"Warning: directory{final_directory} is not empty and will not be removed. Metadynminer's temporary files inside were removed. ")
def make_gif(self, gif_name=None, cmap='RdYlBu_r', energy_unit='kJ/mol', xlabel=None, ylabel=None, zlabel=None, label_size=12, image_size=[10, 7], opacity=0.2, levels=None, frames=64)

Function that generates animation of 3D FES showing different isosurfaces.

fes.make_gif(gif_name="FES_animation.gif")

Parameters:

  • gif_name (default="FES.gif") = String. Name of the gif of FES that will be saved in the current working directory.

  • cmap (default = "RdYlBu_r") = Matplotlib colormap used to color the 3D FES

  • xlabel, ylabel, zlabel = Strings, if provided, they will be used as labels for the graph

  • labelsize (default = 12) = size of text in labels

  • image_size (default = [10,7]) = List of the width and height of the picture

  • opacity (default = 0.2) = number between 0 and 1, is the opacity of isosurfaces of 3D FES

  • levels = Here you can specify list of free energy values for isosurfaces on 3D FES. If not provided, default values from contours parameters will be used instead.

  • frames (default = 64) = Number of frames the animation will be made of.

Expand source code
def make_gif(self, gif_name=None, cmap = "RdYlBu_r", energy_unit="kJ/mol",
             xlabel=None, ylabel=None, zlabel=None, label_size=12, image_size=[10,7], 
              opacity=0.2, levels=None, frames=64):
    """
    Function that generates animation of 3D FES showing different isosurfaces.
    
    ```python
    fes.make_gif(gif_name="FES_animation.gif")
    ```
    
    Parameters:
    
    * gif_name (default="FES.gif") = String. Name of the gif of FES that will be saved in the current working directory.
    
    * cmap (default = "RdYlBu_r") = Matplotlib colormap used to color the 3D FES
    
    * xlabel, ylabel, zlabel = Strings, if provided, they will be used as labels for the graph
    
    * labelsize (default = 12) = size of text in labels
    
    * image_size (default = [10,7]) = List of the width and height of the picture
    
    * opacity (default = 0.2) = number between 0 and 1, is the opacity of isosurfaces of 3D FES
    
    * levels = Here you can specify list of free energy values for isosurfaces on 3D FES. 
            If not provided, default values from contours parameters will be used instead. 
    
    * frames (default = 64) = Number of frames the animation will be made of. 
    """
    if self.cvs == 3:
        cv1min = self.cv1min
        cv1max = self.cv1max
        cv2min = self.cv2min
        cv2max = self.cv2max
        cv3min = self.cv3min
        cv3max = self.cv3max
        
        values = np.linspace(np.min(self.fes)+0.1, np.max(self.fes), num=frames)
        grid = pv.ImageData(
            dimensions=(self.res, self.res, self.res),
            spacing=((cv1max-cv1min)/self.res,(cv2max-cv2min)/self.res,(cv3max-cv3min)/self.res),
            origin=(cv1min, cv2min, cv3min),
        )
        grid["vol"] = self.fes.ravel(order="F")
        surface = grid.contour(values[:1])
        surfaces = [grid.contour([v]) for v in values]
        surface = surfaces[0].copy()
        
        pv.set_plot_theme('document')
        plotter = pv.Plotter(off_screen=True)
        # Open a movie file
        if gif_name == None:
            gif_name = "FES_animation.gif"
        plotter.open_gif(gif_name)

        # Add initial mesh
        plotter.add_mesh(
            surface,
            opacity=opacity,
            clim=grid.get_data_range(),
            show_scalar_bar=False,
            cmap="RdYlBu_r"
        )
        plotter.add_mesh(grid.outline_corners(), color="k")
        text = plotter.add_text(f"{values[0]:.2f}+kJ/mol", position='lower_right', font_size=12)
        if xlabel == None and ylabel == None and zlabel == None:
            plotter.show_grid(xtitle=f"CV1 - {self.cv1_name}", ytitle=f"CV2 - {self.cv2_name}", ztitle=f"CV3 - {self.cv3_name}")
        else:
            plotter.show_grid(xtitle=xlabel, ytitle=ylabel, ztitle=zlabel)
        plotter.set_background('white')
        plotter.show(auto_close=False)

        # Run through each frame
        for surf in range(len(surfaces)):
            surface.copy_from(surfaces[surf])
            plotter.remove_actor(text)
            text = plotter.add_text(f"{values[surf]:.2f} {energy_unit}", position='lower_right', font_size=12)
            plotter.write_frame()  # Write this frame
        # Run through backwards
        for surf in range(len(surfaces)-1,0,-1):
            surface.copy_from(surfaces[surf])
            plotter.remove_actor(text)
            text = plotter.add_text(f"{values[surf]:.2f} {energy_unit}", position='lower_right', font_size=12)
            plotter.write_frame()  # Write this frame

        # Be sure to close the plotter when finished
        plotter.close()
    else:
        print("Error: this method is only available for FES with 3 CVs.")     
def plot(self, png_name=None, contours=True, contours_spacing=0.0, aspect=1.0, cmap='RdYlBu_r', energy_unit='kJ/mol', xlabel=None, ylabel=None, zlabel=None, label_size=12, clabel_size=12, image_size=None, image_size_unit='in', dpi=100, vmin=0, vmax=None, opacity=0.2, levels=None, title=None, off_screen=False, xlim=[None, None], ylim=[None, None], return_fig=False)

Function used to visualize FES, based on Matplotlib for 1D and 2D FES or PyVista for 3D FES.

fes.plot(png_name="fes.png")

Parameters:

  • png_name = String. If this parameter is supplied, the picture of FES will be saved under this name to the current working directory. For 1D and 2D FES, the recommended format in '.png'. For 3D FES, the formats supported by PyVista are '.svg','.eps','.ps','.pdf' and '.tex'.

  • contours (default=True) = whether contours should be shown on 2D FES

  • contours_spacing (default=0.0) = when a positive number is set, it will be used as spacing for contours on 2D FES. Otherwise, if contours=True, there will be five equally spaced contour levels.

  • aspect (default = 1.0) = aspect ratio of the graph. Works with 1D and 2D FES.

  • cmap (default = "RdYlBu_r") = Matplotlib colormap used to color 2D or 3D FES

  • energy_unit (default="kJ/mol") = String, used in description of colorbar

  • xlim, ylim (default = [None, None] for both) = list of two values specifying the range of x and y axes respectively. None means that Matplotlib will choose appropriate range, so this keyword is useful when you need to overwrite it. Does not work for 3D FES at the moment.

  • xlabel, ylabel, zlabel = Strings, if provided, they will be used as labels for the graphs

  • labelsize (default = 12) = size of text in labels

  • image_size (default = [9,6]) = List of the width and height of the picture

  • image_size_unit (default = "in") = Units for width and height of the picture, accepts "in" as inches, "cm", "mm" or "px" as pixels.

  • dpi (default = 100) = DPI of the resulting image (for 1D and 2D FES).

  • vmin (default=0) = real number, lower bound for the colormap on 2D FES

  • vmax = real number, upper bound for the colormap on 2D FES

  • opacity (default=0.2) = number between 0 and 1, is the opacity of isosurfaces of 3D FES

  • levels = Here you can specify list of free energy values for isosurfaces on 3D FES. If not provided, default values from contours parameters will be used instead.

  • title = optional, string that defines the title of the graph

  • offscreen (default = False) = for 3D FES only, grapf will not be shown after creation - used internally by metadynminer when making animations

  • return_fig (default=False) = whether the method should return the Matplotlib.Pyplot.figure object for further use. In the case of plotting 3D FES, it returns Pyvista.plotter object instead.

Expand source code
def plot(self, png_name=None, contours=True, contours_spacing=0.0, aspect = 1.0, cmap = "RdYlBu_r", 
         energy_unit="kJ/mol", xlabel=None, ylabel=None, zlabel=None, label_size=12, clabel_size = 12,
         image_size=None, image_size_unit="in", dpi=100, vmin = 0, vmax = None, 
         opacity=0.2, levels=None, title = None, off_screen = False, 
         xlim=[None, None], ylim=[None, None], return_fig=False):
    """
    Function used to visualize FES, based on Matplotlib for 1D and 2D FES or PyVista for 3D FES. 
    
    ```python
    fes.plot(png_name="fes.png")
    ```
    
    Parameters:
    
    * png_name = String. If this parameter is supplied, the picture of FES will be saved under this name to the current working directory. 
    For 1D and 2D FES, the recommended format in '.png'. For 3D FES, the formats supported by PyVista are '.svg','.eps','.ps','.pdf' and '.tex'. 
    
    * contours (default=True) = whether contours should be shown on 2D FES
    
    * contours_spacing (default=0.0) = when a positive number is set, it will be used as spacing for contours on 2D FES. 
            Otherwise, if contours=True, there will be five equally spaced contour levels.
    
    * aspect (default = 1.0) = aspect ratio of the graph. Works with 1D and 2D FES. 
    
    * cmap (default = "RdYlBu_r") = Matplotlib colormap used to color 2D or 3D FES
    
    * energy_unit (default="kJ/mol") = String, used in description of colorbar

    * xlim, ylim (default = [None, None] for both) = list of two values specifying the range of x and y axes respectively. 
    None means that Matplotlib will choose appropriate range, so this keyword is useful when you need to overwrite it. 
    Does not work for 3D FES at the moment. 
    
    * xlabel, ylabel, zlabel = Strings, if provided, they will be used as labels for the graphs
    
    * labelsize (default = 12) = size of text in labels
    
    * image_size (default = [9,6]) = List of the width and height of the picture

    * image_size_unit (default = "in") = Units for width and height of the picture, accepts "in" as inches, "cm", "mm" or "px" as pixels. 

    * dpi (default = 100) = DPI of the resulting image (for 1D and 2D FES). 
    
    * vmin (default=0) = real number, lower bound for the colormap on 2D FES
    
    * vmax = real number, upper bound for the colormap on 2D FES
    
    * opacity (default=0.2) = number between 0 and 1, is the opacity of isosurfaces of 3D FES
    
    * levels = Here you can specify list of free energy values for isosurfaces on 3D FES. 
                    If not provided, default values from contours parameters will be used instead. 

    * title = optional, string that defines the title of the graph

    * offscreen (default = False) = for 3D FES only, grapf will not be shown after creation - used internally by metadynminer when making animations

    * return_fig (default=False) = whether the method should return the Matplotlib.Pyplot.figure object for further use. 
    In the case of plotting 3D FES, it returns Pyvista.plotter object instead. 
    """
    if vmax == None:
        vmax = np.max(self.fes)+0.01 # if the addition is smaller than 0.01, the 3d plot stops working. 
        
    if contours_spacing == 0.0:
        contours_spacing = (vmax-vmin)/5.0
    
    cmap = cm.get_cmap(cmap)
    
    cmap.set_over("white")
    cmap.set_under("white")
    
    if self.cvs >= 1:
        cv1min = self.cv1min
        cv1max = self.cv1max
    if self.cvs >=2:
        cv2min = self.cv2min
        cv2max = self.cv2max 
    if self.cvs == 3:
        cv3min = self.cv3min
        cv3max = self.cv3max
    
    if image_size == None:
        image_size = [9,6]    
    
    if image_size_unit == "cm":
        image_size[0] /= 2.54
        image_size[1] /= 2.54
    elif image_size_unit == "mm":
        image_size[0] /= 25.4
        image_size[1] /= 25.4
    elif image_size_unit == "px":
        image_size[0] /= dpi
        image_size[1] /= dpi
    elif image_size_unit != "in":
        print(f"Warning: unknown image_size_unit value: {image_size_unit}. Using inches instead. ")
        
    
    if self.cvs == 1:
        fig = plt.figure(figsize=(image_size[0],image_size[1]), dpi=dpi)
        X = np.linspace(cv1min, cv1max, self.res)
        plt.plot(X, self.fes)
        if xlabel == None:
            plt.xlabel(f'CV1 - {self.cv1_name}', size=label_size)
        else:
            plt.xlabel(xlabel, size=label_size)
        if ylabel == None:
            plt.ylabel(f'free energy ({energy_unit})', size=label_size)
        else:
            plt.ylabel(ylabel, size=label_size)
    
        if title != None:
            plt.title(title, size=label_size)

        plt.xticks(fontsize=label_size)
        plt.yticks(fontsize=label_size)
            
        ax = plt.gca()
        ax.set_xlim(xlim)
        ax.set_ylim(ylim)
        
        if png_name != None:
            plt.savefig(png_name, bbox_inches = 'tight')

        if return_fig:
            return fig
        
    if self.cvs == 2:
        fig = plt.figure(figsize=(image_size[0], image_size[1]), dpi=dpi)
        plt.imshow(np.rot90(self.fes, axes=(0,1)), cmap=cmap, interpolation='nearest', 
                   extent=[cv1min, cv1max, cv2min, cv2max], 
                   aspect = (((cv1max-cv1min)/(cv2max-cv2min))/(aspect)),
                   vmin = vmin, vmax = vmax)
        cbar = plt.colorbar()
        cbar.ax.tick_params(labelsize=label_size) 
        cbar.set_label(energy_unit, size=label_size)
        
        if contours:
            if levels != None:
                cont = plt.contour(np.rot90(self.fes, axes=(0,1)), 
                         levels = levels, 
                         extent=[cv1min, cv1max, cv2max, cv2min], 
                         colors = "k", linestyles = "solid")
                plt.clabel(cont, levels = levels, fontsize=clabel_size)
            else:
                cont = plt.contour(np.rot90(self.fes, axes=(0,1)), 
                         levels = np.arange(vmin, (vmax - 0.01), contours_spacing), 
                         extent=[cv1min, cv1max, cv2max, cv2min], 
                         colors = "k", linestyles = "solid")
                plt.clabel(cont, levels = np.arange(vmin, (vmax - 0.01), contours_spacing), fontsize=clabel_size)
        
        if xlabel == None:
            plt.xlabel(f'CV1 - {self.cv1_name}', size=label_size)
        else:
            plt.xlabel(xlabel, size=label_size)
            
        if ylabel == None:
            plt.ylabel(f'CV2 - {self.cv2_name}', size=label_size)
        else:
            plt.ylabel(ylabel, size=label_size)
        
        if title != None:
            plt.title(title, size=label_size)

        plt.xticks(fontsize=label_size)
        plt.yticks(fontsize=label_size)

        ax = plt.gca()
        ax.set_xlim(xlim)
        ax.set_ylim(ylim)

        ax.tick_params(axis='x', labelsize=label_size)
        ax.tick_params(axis='y', labelsize=label_size)
        
        if png_name != None:
            plt.savefig(png_name, bbox_inches = 'tight')
        
        if return_fig:
            return fig
    
    if self.cvs == 3:
        if xlabel == None:
            xlabel = "CV1 - " + self.cv1_name
        if ylabel == None:
            ylabel = "CV2 - " + self.cv2_name
        if zlabel == None:
            zlabel = "CV3 - " + self.cv3_name
        
        grid = pv.ImageData(
            dimensions=(self.res, self.res, self.res),
            spacing=((cv1max-cv1min)/self.res,(cv2max-cv2min)/self.res,(cv3max-cv3min)/self.res),
            origin=(cv1min, cv2min, cv3min)
        )
        grid["vol"] = self.fes.ravel(order="F")
        if levels == None:
            contours = grid.contour(np.arange(0, (vmax - 0.01), contours_spacing))
        else:
            contours = grid.contour(levels)
        fescolors = []
        for i in range(contours.points.shape[0]):
            fescolors.append(self.fes[int((contours.points[i,0]-cv1min)*self.res/(cv1max-cv1min)),
                                      int((contours.points[i,1]-cv2min)*self.res/(cv2max-cv2min)),
                                      int((contours.points[i,2]-cv3min)*self.res/(cv3max-cv3min))])
        #%% Visualization
        bounds = [cv1min, cv1max, cv2min, cv2max, cv3min, cv3max]
        pv.set_plot_theme('document')
        p = pv.Plotter()
        p.add_mesh(contours, scalars=fescolors, opacity=opacity, cmap=cmap, show_scalar_bar=False, interpolate_before_map=True)
        p.show_bounds(bounds=bounds, xtitle=xlabel, ytitle=ylabel, ztitle=zlabel, grid=True, location="outer")
        if title != None:
            p.add_title(title, font_size=label_size)
        if not off_screen:
            p.show()
        
        if png_name != None:
            p.save_graphic(png_name)
        
        if return_fig:
            return p
def remove_CV(self, CV=None, energy_unit='kJ/mol', temp=300.0)

This function is used to remove a CV from an existing FES. The function first recalculates the FES to an array of probabilities. The probabilities are summed along the CV to be removed, and resulting probability distribution with 1 less dimension is converted back to FES.

fes_cv1 = fes.remove_CV(CV=2)

Parameters:

  • CV = integer, the CV to be removed

  • energy_unit (default="kJ/mol") = has to be either "kJ/mol" or "kcal/mol". Make sure to suply the correct energy unit, otherwise you will get wrong FES as a result.

  • temp (default=300.0) = temperature of the simulation in Kelvins.

Expand source code
def remove_CV(self, CV=None, energy_unit="kJ/mol", temp=300.0):
    """
    This function is used to remove a CV from an existing FES. The function first recalculates the FES to an array of probabilities. The probabilities 
    are summed along the CV to be removed, and resulting probability distribution with 1 less dimension is converted back to FES. 

    ```python
    fes_cv1 = fes.remove_CV(CV=2)
    ```
    
    Parameters:
    
    * CV = integer, the CV to be removed
    
    * energy_unit (default="kJ/mol") = has to be either "kJ/mol" or "kcal/mol". Make sure to suply the correct energy unit, otherwise you will get wrong FES as a result. 
    
    * temp (default=300.0) = temperature of the simulation in Kelvins.
    """
    CV = int(float(CV))
    print(f"Removing CV {CV}.")
    if CV > self.cvs:
        print("Error: The CV to remove is not available in this FES object.")
        return None
    if self.cvs == 1:
        print("Error: You can not remove the only CV. ")
        return None
    elif self.cvs == 2:
        if energy_unit == "kJ/mol":
            probabilities = np.exp(-1000*self.fes/8.314/temp)
            if CV == 1:
                new_prob = np.sum(probabilities, axis=0)
                new_fes = Fes(hills=None)
                new_fes.fes = -8.314*temp*np.log(new_prob)/1000
                new_fes.fes = new_fes.fes - np.min(new_fes.fes)
                new_fes.cvs = 1
                new_fes.res = self.res
                new_fes.periodic = [self.periodic[1]]
                new_fes.cv1min = self.cv2min
                new_fes.cv1max = self.cv2max
                new_fes.cv1_name = self.cv2_name
                new_fes.cv1per = self.cv2per
                new_fes.cv1_fes_range = self.cv2_fes_range
            if CV == 2:
                new_prob = np.sum(probabilities, axis=1)
                new_fes = Fes(hills=None)
                new_fes.fes = -8.314*temp*np.log(new_prob)/1000
                new_fes.fes = new_fes.fes - np.min(new_fes.fes)
                new_fes.cvs = 1
                new_fes.res = self.res
                new_fes.periodic = [self.periodic[0]]
                new_fes.cv1min = self.cv1min
                new_fes.cv1max = self.cv1max
                new_fes.cv1_name = self.cv1_name
                new_fes.cv1per = self.cv1per
                new_fes.cv1_fes_range = self.cv1_fes_range
            return new_fes
        elif energy_unit == "kcal/mol":
            probabilities = np.exp(-1000*4.184*self.fes/8.314/temp)
            if CV == 1:
                new_prob = np.sum(probabilities, axis=1)
                new_fes = Fes(hills=None)
                new_fes.fes = -8.314*temp*np.log(new_prob)/1000/4.184
                new_fes.fes = new_fes.fes - np.min(new_fes.fes)
                new_fes.cvs = 1
                new_fes.res = self.res
                new_fes.periodic = [self.periodic[1]]
                new_fes.cv1min = self.cv2min
                new_fes.cv1max = self.cv2max
                new_fes.cv1_name = self.cv2_name
                new_fes.cv1per = self.cv2per
                new_fes.cv1_fes_range = self.cv2_fes_range
            if CV == 2:
                new_prob = np.sum(probabilities, axis=0)
                new_fes = Fes(hills=None)
                new_fes.fes = -8.314*temp*np.log(new_prob)/1000/4.184
                new_fes.fes = new_fes.fes - np.min(new_fes.fes)
                new_fes.cvs = 1
                new_fes.res = self.res
                new_fes.periodic = [self.periodic[0]]
                new_fes.cv1min = self.cv1min
                new_fes.cv1max = self.cv1max
                new_fes.cv1_name = self.cv1_name
                new_fes.cv1per = self.cv1per
                new_fes.cv1_fes_range = self.cv1_fes_range
            return new_fes
        else:
            print("Error: unknown energy unit")
            return None
    elif self.cvs == 3:
        if energy_unit == "kJ/mol":
            probabilities = np.exp(-1000*self.fes/8.314/temp)
            if CV == 1:
                new_prob = np.sum(probabilities, axis=0)
                new_fes = Fes(hills=None)
                new_fes.fes = -8.314*temp*np.log(new_prob)/1000
                new_fes.fes = new_fes.fes - np.min(new_fes.fes)
                new_fes.cvs = 2
                new_fes.res = self.res
                new_fes.periodic = [self.periodic[1], self.periodic[2]]
                new_fes.cv1min = self.cv2min
                new_fes.cv1max = self.cv2max
                new_fes.cv2min = self.cv3min
                new_fes.cv2max = self.cv3max
                new_fes.cv1_name = self.cv2_name
                new_fes.cv2_name = self.cv3_name
                new_fes.cv1per = self.cv2per
                new_fes.cv2per = self.cv3per
                new_fes.cv1_fes_range = self.cv2_fes_range
                new_fes.cv2_fes_range = self.cv3_fes_range
            if CV == 2:
                new_prob = np.sum(probabilities, axis=1)
                new_fes = Fes(hills=None)
                new_fes.fes = -8.314*temp*np.log(new_prob)/1000
                new_fes.fes = new_fes.fes - np.min(new_fes.fes)
                new_fes.cvs = 2
                new_fes.res = self.res
                new_fes.periodic = [self.periodic[0], self.periodic[2]]
                new_fes.cv1min = self.cv1min
                new_fes.cv1max = self.cv1max
                new_fes.cv2min = self.cv3min
                new_fes.cv2max = self.cv3max
                new_fes.cv1_name = self.cv1_name
                new_fes.cv2_name = self.cv3_name
                new_fes.cv1per = self.cv1per
                new_fes.cv2per = self.cv3per
                new_fes.cv1_fes_range = self.cv1_fes_range
                new_fes.cv2_fes_range = self.cv3_fes_range
            if CV == 3:
                new_prob = np.sum(probabilities, axis=2)
                new_fes = Fes(hills=None)
                new_fes.fes = -8.314*temp*np.log(new_prob)/1000
                new_fes.fes = new_fes.fes - np.min(new_fes.fes)
                new_fes.cvs = 2
                new_fes.res = self.res
                new_fes.periodic = [self.periodic[0], self.periodic[1]]
                new_fes.cv1min = self.cv1min
                new_fes.cv1max = self.cv1max
                new_fes.cv2min = self.cv2min
                new_fes.cv2max = self.cv2max
                new_fes.cv1_name = self.cv1_name
                new_fes.cv2_name = self.cv2_name
                new_fes.cv1per = self.cv1per
                new_fes.cv2per = self.cv2per
                new_fes.cv1_fes_range = self.cv1_fes_range
                new_fes.cv2_fes_range = self.cv2_fes_range
            return new_fes
        elif energy_unit == "kcal/mol":
            probabilities = np.exp(-1000*4.184*self.fes/8.314/temp)
            if CV == 1:
                new_prob = np.sum(probabilities, axis=0)
                new_fes = Fes(hills=None)
                new_fes.fes = -8.314*temp*np.log(new_prob)/1000/4.184
                new_fes.fes = new_fes.fes - np.min(new_fes.fes)
                new_fes.cvs = 2
                new_fes.res = self.res
                new_fes.periodic = [self.periodic[1], self.periodic[2]]
                new_fes.cv1min = self.cv2min
                new_fes.cv1max = self.cv2max
                new_fes.cv2min = self.cv3min
                new_fes.cv2max = self.cv3max
                new_fes.cv1_name = self.cv2_name
                new_fes.cv2_name = self.cv3_name
                new_fes.cv1per = self.cv2per
                new_fes.cv2per = self.cv3per
                new_fes.cv1_fes_range = self.cv2_fes_range
                new_fes.cv2_fes_range = self.cv3_fes_range
            if CV == 2:
                new_prob = np.sum(probabilities, axis=1)
                new_fes = Fes(hills=None)
                new_fes.fes = -8.314*temp*np.log(new_prob)/1000/4.184
                new_fes.fes = new_fes.fes - np.min(new_fes.fes)
                new_fes.cvs = 2
                new_fes.res = self.res
                new_fes.periodic = [self.periodic[0], self.periodic[2]]
                new_fes.cv1min = self.cv1min
                new_fes.cv1max = self.cv1max
                new_fes.cv2min = self.cv3min
                new_fes.cv2max = self.cv3max
                new_fes.cv1_name = self.cv1_name
                new_fes.cv2_name = self.cv3_name
                new_fes.cv1per = self.cv1per
                new_fes.cv2per = self.cv3per
                new_fes.cv1_fes_range = self.cv1_fes_range
                new_fes.cv2_fes_range = self.cv3_fes_range
            if CV == 3:
                new_prob = np.sum(probabilities, axis=2)
                new_fes = Fes(hills=None)
                new_fes.fes = -8.314*temp*np.log(new_prob)/1000/4.184
                new_fes.fes = new_fes.fes - np.min(new_fes.fes)
                new_fes.cvs = 2
                new_fes.res = self.res
                new_fes.periodic = [self.periodic[0], self.periodic[1]]
                new_fes.cv1min = self.cv1min
                new_fes.cv1max = self.cv1max
                new_fes.cv2min = self.cv2min
                new_fes.cv2max = self.cv2max
                new_fes.cv1_name = self.cv1_name
                new_fes.cv2_name = self.cv2_name
                new_fes.cv1per = self.cv1per
                new_fes.cv2per = self.cv2per
                new_fes.cv1_fes_range = self.cv1_fes_range
                new_fes.cv2_fes_range = self.cv2_fes_range
            return new_fes
        else:
            print("Error: unknown energy unit")
            return None
def surface_plot(self, png_name=None, cmap='RdYlBu_r', energy_unit='kJ/mol', xlabel=None, ylabel=None, zlabel=None, dpi=100, label_size=12, image_size=None, image_size_unit='in', rstride=1, cstride=1, vmin=0, vmax=None, title=None, return_fig=False)

Function for visualization of 2D FES as 3D surface plot. For now, it is based on Matplotlib, but there are issues with interactivity.

It can be interacted with in jupyter notebook or jupyter lab in %matplotlib widget mode. Otherwise it is just static image of the 3D surface plot.

%matplotlib widget
fes.surface_plot()
Expand source code
def surface_plot(self, png_name=None, cmap = "RdYlBu_r", 
                 energy_unit="kJ/mol", xlabel=None, ylabel=None, zlabel=None, dpi=100, label_size=12, image_size=None, image_size_unit="in", rstride=1, cstride=1, vmin = 0, vmax = None, title=None, return_fig=False):
    """
    Function for visualization of 2D FES as 3D surface plot. For now, it is based on Matplotlib, but there are issues with interactivity. 
    
    It can be interacted with in jupyter notebook or jupyter lab in %matplotlib widget mode. Otherwise it is just static image of the 3D surface plot. 
    
    ```python
    %matplotlib widget
    fes.surface_plot()
    ```
    
    """
    if self.cvs == 2:
        cv1min = self.cv1min
        cv1max = self.cv1max
        cv2min = self.cv2min
        cv2max = self.cv2max 
        
        x = np.linspace(cv1min, cv1max, self.res)
        y = np.linspace(cv2min, cv2max, self.res)
        
        X, Y = np.meshgrid(x, y)
        Z = self.fes.T

        if image_size == None:
            image_size = [9,9]
        
        if image_size_unit == "cm":
            image_size[0] /= 2.54
            image_size[1] /= 2.54
        elif image_size_unit == "mm":
            image_size[0] /= 25.4
            image_size[1] /= 25.4
        elif image_size_unit == "px":
            image_size[0] /= dpi
            image_size[1] /= dpi
        elif image_size_unit != "in":
            print(f"Warning: unknown image_size_unit value: {image_size_unit}. Using inches instead. ")
        
        fig = plt.figure(figsize=(image_size[0],image_size[1]), dpi=dpi)
        ax = fig.add_subplot(projection="3d")
        ax.plot_surface(X,Y,Z, cmap=cmap, rstride=rstride, cstride=cstride)
        
        if xlabel == None:
            ax.set_xlabel(f'CV1 - {self.cv1_name}', size=label_size)
        else:
            ax.set_xlabel(xlabel, size=label_size)
        if ylabel == None:
            ax.set_ylabel(f'CV2 - {self.cv2_name}', size=label_size)
        else:
            ax.set_ylabel(ylabel, size=label_size)
        if zlabel == None:
            ax.set_zlabel(f'free energy ({energy_unit})', size=label_size)
        else:
            ax.set_zlabel(zlabel, size=label_size)

        if title != None:
            plt.title(title, size=label_size)

        plt.xticks(fontsize=label_size)
        plt.yticks(fontsize=label_size)

        if png_name != None:
            plt.savefig(png_name, bbox_inches = 'tight')
        
        if return_fig:
            return fig
            
    else:
        print(f"Error: Surface plot only works for FES with exactly two CVs, and this FES has {self.cvs} CV.")
class Hills (name='HILLS', encoding='utf8', ignoretime=True, periodic=None, cv1per=[-3.141592653589793, 3.141592653589793], cv2per=[-3.141592653589793, 3.141592653589793], cv3per=[-3.141592653589793, 3.141592653589793], timestep=None)

Object of Hills class are created for loading HILLS files, and obtaining the necessary information from them.

Hills files are loaded with command:

hillsfile = metadynminer.Hills(name="HILLS")

optional parameters:

  • name (default="HILLS") = string with name of HILLS file

  • ignoretime (default=True) = boolean, if set to False, it will save the time in the HILLS file; if set to True, and timestep is not set, each time value will be incremented by the same amount as the time of the first step.

  • timestep = numeric value of the time difference between hills, in picoseconds

  • periodic (default=[False, False]) = list of boolean values telling which CV is periodic.

  • cv1per, cv2per, cv3per (defaults = [-numpy.pi, numpy.pi]) = List of two numeric values defining the periodicity of given CV. Has to be provided for each periodic CV.

Hills attributes:

  • Hills.hills = array of values from the HILLS file

  • Hills.cvs = number of CVs

  • Hills.cv1_name = name of CV 1

  • Hills.cv1per = list of two boundaries of the perodicity interval for CV 1

  • Hills.dt = time step between hills in ps

  • Hills.sigma1 = widths of hills along the CV 1 axis

  • Hills.cv1 = list of CV 1 values during simulation

  • Hills.biasf = biasfactors

  • and analogous attributes for other CVs

Expand source code
class Hills:
    """
    Object of Hills class are created for loading HILLS files, and obtaining the necessary information from them. 

    Hills files are loaded with command:
    ```python
    hillsfile = metadynminer.Hills(name="HILLS")
    ```
    
    optional parameters:
    
    * name (default="HILLS") = string with name of HILLS file
    
    * ignoretime (default=True) = boolean, if set to False, it will save the time in the HILLS file;
                                if set to True, and timestep is not set, 
                                        each time value will be incremented by the same amount as the time of the first step.
                                        
    * timestep = numeric value of the time difference between hills, in picoseconds
    
    * periodic (default=[False, False]) = list of boolean values telling which CV is periodic.
    
    * cv1per, cv2per, cv3per (defaults = [-numpy.pi, numpy.pi]) = List of two numeric values defining the periodicity of given CV. 
                                        Has to be provided for each periodic CV.

    Hills attributes: 

    * Hills.hills = array of values from the HILLS file
    
    * Hills.cvs = number of CVs

    * Hills.cv1_name = name of CV 1

    * Hills.cv1per = list of two boundaries of the perodicity interval for CV 1

    * Hills.dt = time step between hills in ps

    * Hills.sigma1 = widths of hills along the CV 1 axis

    * Hills.cv1 = list of CV 1 values during simulation

    * Hills.biasf = biasfactors

    * and analogous attributes for other CVs
    """
    
    def __init__(self, name="HILLS", encoding="utf8", ignoretime=True, periodic=None, 
                 cv1per=[-np.pi, np.pi],cv2per=[-np.pi, np.pi],cv3per=[-np.pi, np.pi], timestep=None):
        self.read(name, encoding, ignoretime, periodic, 
                 cv1per=cv1per,cv2per=cv2per,cv3per=cv3per, timestep=timestep)
        self.hillsfilename = name
    
    def read(self, name="HILLS", encoding="utf8", ignoretime=True, periodic=None, 
                 cv1per=[-np.pi, np.pi],cv2per=[-np.pi, np.pi],cv3per=[-np.pi, np.pi], timestep=None):
        with open(name, 'r', encoding=encoding) as hillsfile:
            lines = hillsfile.readlines()
        columns = lines[0].split()
        number_of_columns_head = len(columns) - 2
        
        if number_of_columns_head == 5:
            self.cvs = 1
            self.cv1_name = lines[0].split()[3]
            self.cv1per = cv1per

        elif number_of_columns_head == 7:
            self.cvs = 2
            self.cv1_name = lines[0].split()[3]
            self.cv2_name = lines[0].split()[4]
            self.cv1per = cv1per
            self.cv2per = cv2per
        elif number_of_columns_head == 9:
            self.cvs = 3
            self.cv1_name = lines[0].split()[3]
            self.cv2_name = lines[0].split()[4]
            self.cv3_name = lines[0].split()[5]
            
            self.cv1per = cv1per
            self.cv2per = cv2per
            self.cv3per = cv3per
        else:
            print("Error: Unexpected number of columns in provided HILLS file.")
            return None
        
        self.ignoretime = ignoretime
        if not ignoretime:
            if timestep != None:
                dt = timestep
            else:
                for line in range(len(lines)):
                    if lines[line][0] != "#":
                        dt = round(float(lines[line+1].split()[0]),14) - round(float(lines[line].split()[0]),14)
                        break
        else:
            if timestep != None:
                dt = timestep
            else:
                dt = 1.0
        self.dt = dt
        t = 0
        for line in range(len(lines)):
            if lines[line][0] != "#":
                t += 1 
                if t == 1:
                    if self.cvs == 1:
                        self.sigma1 = float(lines[line].split()[2])
                        self.biasf = float(lines[line].split()[4])
                    elif self.cvs == 2:
                        self.sigma1 = float(lines[line].split()[3])
                        self.sigma2 = float(lines[line].split()[4])
                        self.biasf = float(lines[line].split()[6])
                    elif self.cvs == 3:
                        self.sigma1 = float(lines[line].split()[4])
                        self.sigma2 = float(lines[line].split()[5])
                        self.sigma3 = float(lines[line].split()[6])
                        self.biasf = float(lines[line].split()[8])
                    self.hills = [lines[line].split()]
                    if ignoretime:
                        self.hills[t-1][0] = t*dt
                else:
                    if self.cvs == 1 and len(lines[line].split()) == 5:
                        self.hills.append(lines[line].split())
                        if ignoretime:
                            self.hills[t-1][0] = t*dt
                    if self.cvs == 2 and len(lines[line].split()) == 7:
                        self.hills.append(lines[line].split())
                        if ignoretime:
                            self.hills[t-1][0] = t*dt
                    if self.cvs == 3 and len(lines[line].split()) == 9:
                        self.hills.append(lines[line].split())
                        if ignoretime:
                            self.hills[t-1][0] = t*dt
        
        self.hills = np.array(self.hills, dtype=np.double)
                
        if self.cvs == 1:
            self.cv1 = self.hills[:,1]
            self.sigma1 = self.hills[:,2]
            self.heights = self.hills[:,3]
            self.biasf = self.hills[:,4]
        elif self.cvs == 2:
            self.cv1 = self.hills[:,1]
            self.cv2 = self.hills[:,2]
            self.sigma1 = self.hills[:,3]
            self.sigma2 = self.hills[:,4]
            self.heights = self.hills[:,5]
            self.biasf = self.hills[:,6]
        elif self.cvs == 3:
            self.cv1 = self.hills[:,1]
            self.cv2 = self.hills[:,2]
            self.cv3 = self.hills[:,3]
            self.sigma1 = self.hills[:,4]
            self.sigma2 = self.hills[:,5]
            self.sigma3 = self.hills[:,6]
            self.heights = self.hills[:,7]
            self.biasf = self.hills[:,8]
        print(f"Loaded HILLS file named {name}. ")
        
        # detect periodicity
        if periodic == None:
            if self.cvs >= 1:
                if np.max(np.diff(self.cv1)) > 0.8*(np.max(self.cv1)-np.min(self.cv1)):
                    periodic = list([True])
                else:
                    periodic = list([False])
            if self.cvs >= 2:
                if np.max(np.diff(self.cv2)) > 0.8*(np.max(self.cv2)-np.min(self.cv2)):
                    periodic.append(True)
                else:
                    periodic.append(False)
            if self.cvs >= 3:
                if np.max(np.diff(self.cv3)) > 0.8*(np.max(self.cv3)-np.min(self.cv3)):
                    periodic.append(True)
                else:
                    periodic.append(False)
            print(f"Automatically detected which CVs are periodic: {periodic}. ")
            print("This detection can be overriden by specifying a list of boolean values to 'periodic' keyword. ")
        else:
            if self.cvs == 1:
                if len(periodic) != 1 or (type(periodic[0]) != type(True)):
                    print(f"Error: argument 'periodic' has wrong number of parameters({len(periodic)})")
            if self.cvs == 2:
                if len(periodic) != 2 or (type(periodic[0]) != type(True)) or (type(periodic[1]) != type(True)):
                    print(f"Error: argument 'periodic' has wrong number of parameters({len(periodic)})")
            if self.cvs == 3:
                if len(periodic) != 3 or (type(periodic[0]) != type(True)) or (type(periodic[1]) != type(True)) or (type(periodic[2]) != type(True)):
                    print(f"Error: argument 'periodic' has wrong number of parameters({len(periodic)})")
        
        self.periodic = periodic
        # check periodicity

        if self.cvs >= 1:
            if periodic[0] == False and np.max(np.diff(self.cv1))>0.8*(np.max(self.cv1)-np.min(self.cv1)):
                print(f"WARNING: It looks like CV 1 ({self.cv1_name}) is periodic, however you specified that it is not. This may be completely fine, however, if you forgot to specify the periodicity correctly, it can lead to errors later during FES calculation.")
            if periodic[0] == True and np.max(np.diff(self.cv1))<0.2*(np.max(self.cv1)-np.min(self.cv1)):
                print(f"WARNING: It looks like CV 1 ({self.cv1_name}) is not periodic, however you specified that it is. This may be completely fine, however, if you forgot to specify the periodicity correctly, it can lead to errors later during FES calculation.")
                
        if self.cvs >= 2:
            if periodic[1] == False and np.max(np.diff(self.cv2))>0.8*(np.max(self.cv2)-np.min(self.cv2)):
                print(f"WARNING: It looks like CV 2 ({self.cv2_name}) is periodic, however you specified that it is not. This may be completely fine, however, if you forgot to specify the periodicity correctly, it can lead to errors later during FES calculation.")
            if periodic[1] == True and np.max(np.diff(self.cv2))<0.2*(np.max(self.cv2)-np.min(self.cv2)):
                print(f"WARNING: It looks like CV 2 ({self.cv2_name}) is not periodic, however you specified that it is. This may be completely fine, however, if you forgot to specify the periodicity correctly, it can lead to errors later during FES calculation.")
        
        if self.cvs >= 3:
            if periodic[2] == False and np.max(np.diff(self.cv3))>0.8*(np.max(self.cv3)-np.min(self.cv3)):
                print(f"WARNING: It looks like CV 3 ({self.cv3_name}) is periodic, however you specified that it is not. This may be completely fine, however, if you forgot to specify the periodicity correctly, it can lead to errors later during FES calculation.")
            if periodic[2] == True and np.max(np.diff(self.cv3))<0.2*(np.max(self.cv3)-np.min(self.cv3)):
                print(f"WARNING: It looks like CV 3 ({self.cv3_name}) is not periodic, however you specified that it is. This may be completely fine, however, if you forgot to specify the periodicity correctly, it can lead to errors later during FES calculation.")
                
        return self
    
    
    def get_cv1(self):
        return self.cv1
    def get_cv2(self):
        return self.cv2
    def get_cv3(self):
        return self.cv3
    
    def get_cv1per(self):
        return self.cv1per
    def get_cv2per(self):
        return self.cv2per
    def get_cv3per(self):
        return self.cv3per
    
    def get_periodic(self):
        return self.periodic
    
    def get_cv1_name(self):
        return self.cv1_name
    def get_cv2_name(self):
        return self.cv2_name
    def get_cv3_name(self):
        return self.cv3_name
    
    def get_hills(self):
        return self.hills
    
    def get_number_of_cvs(self):
        return self.cvs
    
    def get_sigma1(self):
        return self.sigma1
    def get_sigma2(self):
        return self.sigma2
    def get_sigma3(self):
        return self.sigma3
    
    def get_heights(self):
        return(self.heights)
    
    __pdoc__["Hills.get_cv1"] = False
    __pdoc__["Hills.get_cv2"] = False
    __pdoc__["Hills.get_cv3"] = False
    __pdoc__["Hills.get_cv1per"] = False
    __pdoc__["Hills.get_cv2per"] = False
    __pdoc__["Hills.get_cv3per"] = False
    __pdoc__["Hills.get_cv1_name"] = False
    __pdoc__["Hills.get_cv2_name"] = False
    __pdoc__["Hills.get_cv3_name"] = False
    __pdoc__["Hills.get_periodic"] = False
    __pdoc__["Hills.get_hills"] = False
    __pdoc__["Hills.get_number_of_cvs"] = False
    __pdoc__["Hills.get_sigma1"] = False
    __pdoc__["Hills.get_sigma2"] = False
    __pdoc__["Hills.get_sigma3"] = False
    __pdoc__["Hills.get_heights"] = False
    __pdoc__["Hills.read"] = False

    def plot_heights(self, png_name=None, energy_unit="kJ/mol", xlabel=None, ylabel=None, label_size=12, image_size=None, image_size_unit="in", dpi=100, tu = "ps", time_min=None, time_max=None, xlim=[None, None], ylim=[None, None], title=None, return_fig=False):
        """
        Function used to visualize heights of the hills that were added during the simulation. 
        
        ```python
        hillsfile.plot_heights(png_name="picture.png")
        ```
        
        Parameters:
        
        * png_name = String. If this parameter is supplied, the picture of FES will be saved under this name to the current working directory.
        
        * energy_unit (default="kJ/mol") = String, used in description of the y axis

        * xlim, ylim (default = [None, None] for both) = list of two values specifying the range of x and y axes respectively. 
        None means that Matplotlib will choose appropriate range, so this keyword is useful when you need to overwrite it. 
        
        * xlabel, ylabel = Strings, if provided, they will be used as labels for the graphs
        
        * labelsize (default = 12) = size of text in labels
        
        * image_size (default = [9,6]) = list of the width and height of the picture

        * image_size_unit (default = "in") = Units for width and height of the picture, accepts "in" as inches, "cm", "mm" or "px" as pixels. 

        * dpi (default = 100) = DPI of the picture

        * tu (default = "ps") = string, time unit to be shown on x axis. Also aplies to time_min and time_max, if those are used. 
        Available options: "s", "ms", "us", "ns", "ps", "fs"

        * time_min, time_max = The time range for plot, closed interval, in the time unit specified by "tu"

        * title = optional, string that defines the title of the graph

        * return_fig (default=False) = whether the method should return the Matplotlib.Pyplot.figure object for further use
        """
        tu = TU(tu)
        
        if time_min == time_max == 0:
            print("Error: Values of start and end time are zero.")
            return None
        if time_min != None:
            #time_min = tu.inps(time_min)
            if tu.inps(time_min) < 0:
                print("Warning: Start time is lower than zero, it will be set to zero instead. ")
                time_min = 0
            if tu.inps(time_min) < int(self.hills[0,0]):
                print(f"Warning: Start time {tu.inps(time_min)} ps is lower than the first time from HILLS file {int(self.hills[0,0])} ps, which will be used instead. ")
                time_min = tu.intu(self.hills[0,0])
        else:
            time_min = tu.intu(self.hills[0,0])
        if time_max != None:
            #time_max = tu.inps(time_max)
            if time_max < time_min:
                print("Warning: End time is lower than start time. Values are flipped. ")
                time_value = time_max
                time_max = time_min
                time_min = time_value
            if tu.inps(time_max) > int(self.hills[-1,0]):
                print(f"Warning: End time {tu.inps(time_max)} ps is higher than number of lines in HILLS file {int(self.hills[-1,0])} ps, which will be used instead. ")
                time_max = tu.intu(self.hills[-1,0])
        else:
            time_max = tu.intu(self.hills[-1,0])
        #print(f"Berofe fes: min {time_min}, max {time_max}")
        
        if not self.ignoretime:
            time_max = int(round(((time_max - self.hills[0,0])/self.dt),0)) + 1
            time_min = int(round(((time_min - self.hills[0,0])/self.dt),0)) + 1

        if image_size == None:
            image_size = [9,6]
        
        if image_size_unit == "cm":
            image_size[0] /= 2.54
            image_size[1] /= 2.54
        elif image_size_unit == "mm":
            image_size[0] /= 25.4
            image_size[1] /= 25.4
        elif image_size_unit == "px":
            image_size[0] /= dpi
            image_size[1] /= dpi
        elif image_size_unit != "in":
            print(f"Warning: unknown image_size_unit value: {image_size_unit}. Using inches instead. ")
            
        fig = plt.figure(figsize=(image_size[0],image_size[1]), dpi=dpi)
        #plt.plot(tu.intu(np.array(range(len(self.heights))))[time_min-1:time_max], self.heights[time_min-1:time_max])
        plt.plot(tu.intu(np.arange(int(self.hills[0,0]), self.heights.shape[0]+1)[int(tu.inps(time_min)-1):int(tu.inps(time_max))]),
                 self.heights[int(tu.inps(time_min)-1):int(tu.inps(time_max))])
        if xlabel == None:
            plt.xlabel(f'time ({tu.name})', size=label_size)
        else:
            plt.xlabel(xlabel, size=label_size)
        if ylabel == None:
            plt.ylabel(f'free energy ({energy_unit})', size=label_size)
        else:
            plt.ylabel(ylabel, size=label_size)
        
        if title != None:
            plt.title(title, size=label_size)

        plt.xticks(fontsize=label_size)
        plt.yticks(fontsize=label_size)
        
        ax = plt.gca()
        ax.set_xlim(xlim)
        ax.set_ylim(ylim)
            
        if png_name != None:
            plt.savefig(png_name, bbox_inches = 'tight')
        
        if return_fig:
            return fig

    def plot_CV(self, png_name=None, CV=None, xlabel=None, ylabel=None, label_size=12, image_size=None, image_size_unit="in", dpi=100, tu = "ps", time_min=None, time_max=None, points = True, point_size=1, xlim=[None, None], ylim=[None, None], title=None, return_fig=False):
        """
        Function used to visualize CV values from the simulation. 
        
        ```python
        hillsfile.plot_heights(png_name="picture.png")
        ```
        
        Parameters:
        
        * png_name = String. If this parameter is supplied, the picture of FES will be saved under this name to the current working directory.
        
        * CV = Integer, number of the CV to plot (between 1-3)

        * xlim, ylim (default = [None, None] for both) = list of two values specifying the range of x and y axes respectively. 
        None means that Matplotlib will choose appropriate range, so this keyword is useful when you need to overwrite it. 
        
        * xlabel, ylabel = Strings, if provided, they will be used as labels for the graphs
        
        * labelsize (default = 12) = size of text in labels
        
        * image_size (default = [9,6]) = List of the width and height of the picture

        * image_size_unit (default = "in") = Units for width and height of the picture, accepts "in" as inches, "cm", "mm" or "px" as pixels. 

        * dpi (default = 100) = DPI of the resulting image. 
        
        * tu (default = "ps") = string, time unit to be shown on x axis. Also aplies to time_min and time_max, if those are used. Available options: "s", "ms", "us", "ns", "ps", "fs"

        * time_min, time_max = The time range for plot

        * points (default=True) = Boolean value; if True, plot type will be scatter plot, which is better for periodic CVs; if False, it will be line plot, which is sometimes more suitable for non-periodic CVs. 

        * point_size (default = 1) = The size of dots in the plot

        * title = optional, string that defines the title of the graph

        * return_fig (default=False) = whether the method should return the Matplotlib.Pyplot.figure object for further use
        """
        if CV==None:
            print("Error: CV was not chosen")
            return None
        if CV>self.cvs:
            print(f"Error: CV {CV} is not available. ")
            return None
        if CV==1.0:
            CV=1
        if CV==2.0:
            CV=2
        if CV==3.0:
            CV=3
        if CV!=1 and CV!=2 and CV!=3:
            print(f"Error: supplied value of CV {CV} is not correct value")
            return None
            
        tu = TU(tu)
        
        if time_min == time_max == 0:
            print("Error: Values of start and end time are zero.")
            return None
        if time_min != None:
            #time_min = tu.inps(time_min)
            if tu.inps(time_min) < 0:
                print("Warning: Start time is lower than zero, it will be set to zero instead. ")
                time_min = 0
            if tu.inps(time_min) < int(self.hills[0,0]):
                print(f"Warning: Start time {tu.inps(time_min)} ps is lower than the first time from HILLS file {int(self.hills[0,0])} ps, which will be used instead. ")
                time_min = tu.intu(self.hills[0,0])
        else:
            time_min = tu.intu(self.hills[0,0])
        if time_max != None:
            #time_max = tu.inps(time_max)
            if time_max < time_min:
                print("Warning: End time is lower than start time. Values are flipped. ")
                time_value = time_max
                time_max = time_min
                time_min = time_value
            if tu.inps(time_max) > int(self.hills[-1,0]):
                print(f"Warning: End time {tu.inps(time_max)} ps is higher than number of lines in HILLS file {int(self.hills[-1,0])} ps, which will be used instead. ")
                time_max = tu.intu(self.hills[-1,0])
        else:
            time_max = tu.intu(self.hills[-1,0])
        
        if not self.ignoretime:
            time_max = int(round(((time_max - self.hills[0,0])/self.dt),0)) + 1
            time_min = int(round(((time_min - self.hills[0,0])/self.dt),0)) + 1

        if image_size == None:
            image_size = [9,6]
        
        if image_size_unit == "cm":
            image_size[0] /= 2.54
            image_size[1] /= 2.54
        elif image_size_unit == "mm":
            image_size[0] /= 25.4
            image_size[1] /= 25.4
        elif image_size_unit == "px":
            image_size[0] /= dpi
            image_size[1] /= dpi
        elif image_size_unit != "in":
            print(f"Warning: unknown image_size_unit value: {image_size_unit}. Using inches instead. ")
        
        fig = plt.figure(figsize=(image_size[0],image_size[1]), dpi=dpi)
        if points:
            if CV==1:
                plt.scatter(tu.intu(np.array(range(int(round(tu.inps(time_min),0)),int(round(tu.inps(time_max)+1,0)),int(round(self.dt,0))))), 
                            self.cv1[int(tu.inps(time_min)-1):int(tu.inps(time_max))], s=point_size)
            if CV==2:
                plt.scatter(tu.intu(np.array(range(int(round(tu.inps(time_min),0)),int(round(tu.inps(time_max)+1,0)),int(round(self.dt,0))))), 
                            self.cv2[int(tu.inps(time_min)-1):int(tu.inps(time_max))], s=point_size)
            if CV==3:
                plt.scatter(tu.intu(np.array(range(int(round(tu.inps(time_min),0)),int(round(tu.inps(time_max)+1,0)),int(round(self.dt,0))))), 
                            self.cv3[int(tu.inps(time_min)-1):int(tu.inps(time_max))], s=point_size)
        else:
            if CV==1:
                plt.plot(tu.intu(np.array(range(int(round(tu.inps(time_min),0)),int(round(tu.inps(time_max)+1,0)),int(round(self.dt,0))))), 
                         self.cv1[int(tu.inps(time_min)-1):int(tu.inps(time_max))], lw=point_size)
            if CV==2:
                plt.plot(tu.intu(np.array(range(int(round(tu.inps(time_min),0)),int(round(tu.inps(time_max)+1,0)),int(round(self.dt,0))))), 
                         self.cv2[int(tu.inps(time_min)-1):int(tu.inps(time_max))], lw=point_size)
            if CV==3:
                plt.plot(tu.intu(np.array(range(int(round(tu.inps(time_min),0)),int(round(tu.inps(time_max)+1,0)),int(round(self.dt,0))))), 
                         self.cv3[int(tu.inps(time_min)-1):int(tu.inps(time_max))], lw=point_size)
            
        if xlabel == None:
            plt.xlabel(f'time ({tu.name})', size=label_size)
        else:
            plt.xlabel(xlabel, size=label_size)
        if ylabel == None:
            if CV==1:
                plt.ylabel(f'CV {CV} - {self.cv1_name}', size=label_size)
            if CV==2:
                plt.ylabel(f'CV {CV} - {self.cv2_name}', size=label_size)
            if CV==3:
                plt.ylabel(f'CV {CV} - {self.cv3_name}', size=label_size)
        else:
            plt.ylabel(ylabel, size=label_size)
        
        if title != None:
            plt.title(title, size=label_size)

        plt.xticks(fontsize=label_size)
        plt.yticks(fontsize=label_size)
                
        ax = plt.gca()
        ax.set_xlim(xlim)
        ax.set_ylim(ylim)
            
        if png_name != None:
            plt.savefig(png_name, bbox_inches = 'tight')
        
        if return_fig:
            return fig

Methods

def plot_CV(self, png_name=None, CV=None, xlabel=None, ylabel=None, label_size=12, image_size=None, image_size_unit='in', dpi=100, tu='ps', time_min=None, time_max=None, points=True, point_size=1, xlim=[None, None], ylim=[None, None], title=None, return_fig=False)

Function used to visualize CV values from the simulation.

hillsfile.plot_heights(png_name="picture.png")

Parameters:

  • png_name = String. If this parameter is supplied, the picture of FES will be saved under this name to the current working directory.

  • CV = Integer, number of the CV to plot (between 1-3)

  • xlim, ylim (default = [None, None] for both) = list of two values specifying the range of x and y axes respectively. None means that Matplotlib will choose appropriate range, so this keyword is useful when you need to overwrite it.

  • xlabel, ylabel = Strings, if provided, they will be used as labels for the graphs

  • labelsize (default = 12) = size of text in labels

  • image_size (default = [9,6]) = List of the width and height of the picture

  • image_size_unit (default = "in") = Units for width and height of the picture, accepts "in" as inches, "cm", "mm" or "px" as pixels.

  • dpi (default = 100) = DPI of the resulting image.

  • tu (default = "ps") = string, time unit to be shown on x axis. Also aplies to time_min and time_max, if those are used. Available options: "s", "ms", "us", "ns", "ps", "fs"

  • time_min, time_max = The time range for plot

  • points (default=True) = Boolean value; if True, plot type will be scatter plot, which is better for periodic CVs; if False, it will be line plot, which is sometimes more suitable for non-periodic CVs.

  • point_size (default = 1) = The size of dots in the plot

  • title = optional, string that defines the title of the graph

  • return_fig (default=False) = whether the method should return the Matplotlib.Pyplot.figure object for further use

Expand source code
def plot_CV(self, png_name=None, CV=None, xlabel=None, ylabel=None, label_size=12, image_size=None, image_size_unit="in", dpi=100, tu = "ps", time_min=None, time_max=None, points = True, point_size=1, xlim=[None, None], ylim=[None, None], title=None, return_fig=False):
    """
    Function used to visualize CV values from the simulation. 
    
    ```python
    hillsfile.plot_heights(png_name="picture.png")
    ```
    
    Parameters:
    
    * png_name = String. If this parameter is supplied, the picture of FES will be saved under this name to the current working directory.
    
    * CV = Integer, number of the CV to plot (between 1-3)

    * xlim, ylim (default = [None, None] for both) = list of two values specifying the range of x and y axes respectively. 
    None means that Matplotlib will choose appropriate range, so this keyword is useful when you need to overwrite it. 
    
    * xlabel, ylabel = Strings, if provided, they will be used as labels for the graphs
    
    * labelsize (default = 12) = size of text in labels
    
    * image_size (default = [9,6]) = List of the width and height of the picture

    * image_size_unit (default = "in") = Units for width and height of the picture, accepts "in" as inches, "cm", "mm" or "px" as pixels. 

    * dpi (default = 100) = DPI of the resulting image. 
    
    * tu (default = "ps") = string, time unit to be shown on x axis. Also aplies to time_min and time_max, if those are used. Available options: "s", "ms", "us", "ns", "ps", "fs"

    * time_min, time_max = The time range for plot

    * points (default=True) = Boolean value; if True, plot type will be scatter plot, which is better for periodic CVs; if False, it will be line plot, which is sometimes more suitable for non-periodic CVs. 

    * point_size (default = 1) = The size of dots in the plot

    * title = optional, string that defines the title of the graph

    * return_fig (default=False) = whether the method should return the Matplotlib.Pyplot.figure object for further use
    """
    if CV==None:
        print("Error: CV was not chosen")
        return None
    if CV>self.cvs:
        print(f"Error: CV {CV} is not available. ")
        return None
    if CV==1.0:
        CV=1
    if CV==2.0:
        CV=2
    if CV==3.0:
        CV=3
    if CV!=1 and CV!=2 and CV!=3:
        print(f"Error: supplied value of CV {CV} is not correct value")
        return None
        
    tu = TU(tu)
    
    if time_min == time_max == 0:
        print("Error: Values of start and end time are zero.")
        return None
    if time_min != None:
        #time_min = tu.inps(time_min)
        if tu.inps(time_min) < 0:
            print("Warning: Start time is lower than zero, it will be set to zero instead. ")
            time_min = 0
        if tu.inps(time_min) < int(self.hills[0,0]):
            print(f"Warning: Start time {tu.inps(time_min)} ps is lower than the first time from HILLS file {int(self.hills[0,0])} ps, which will be used instead. ")
            time_min = tu.intu(self.hills[0,0])
    else:
        time_min = tu.intu(self.hills[0,0])
    if time_max != None:
        #time_max = tu.inps(time_max)
        if time_max < time_min:
            print("Warning: End time is lower than start time. Values are flipped. ")
            time_value = time_max
            time_max = time_min
            time_min = time_value
        if tu.inps(time_max) > int(self.hills[-1,0]):
            print(f"Warning: End time {tu.inps(time_max)} ps is higher than number of lines in HILLS file {int(self.hills[-1,0])} ps, which will be used instead. ")
            time_max = tu.intu(self.hills[-1,0])
    else:
        time_max = tu.intu(self.hills[-1,0])
    
    if not self.ignoretime:
        time_max = int(round(((time_max - self.hills[0,0])/self.dt),0)) + 1
        time_min = int(round(((time_min - self.hills[0,0])/self.dt),0)) + 1

    if image_size == None:
        image_size = [9,6]
    
    if image_size_unit == "cm":
        image_size[0] /= 2.54
        image_size[1] /= 2.54
    elif image_size_unit == "mm":
        image_size[0] /= 25.4
        image_size[1] /= 25.4
    elif image_size_unit == "px":
        image_size[0] /= dpi
        image_size[1] /= dpi
    elif image_size_unit != "in":
        print(f"Warning: unknown image_size_unit value: {image_size_unit}. Using inches instead. ")
    
    fig = plt.figure(figsize=(image_size[0],image_size[1]), dpi=dpi)
    if points:
        if CV==1:
            plt.scatter(tu.intu(np.array(range(int(round(tu.inps(time_min),0)),int(round(tu.inps(time_max)+1,0)),int(round(self.dt,0))))), 
                        self.cv1[int(tu.inps(time_min)-1):int(tu.inps(time_max))], s=point_size)
        if CV==2:
            plt.scatter(tu.intu(np.array(range(int(round(tu.inps(time_min),0)),int(round(tu.inps(time_max)+1,0)),int(round(self.dt,0))))), 
                        self.cv2[int(tu.inps(time_min)-1):int(tu.inps(time_max))], s=point_size)
        if CV==3:
            plt.scatter(tu.intu(np.array(range(int(round(tu.inps(time_min),0)),int(round(tu.inps(time_max)+1,0)),int(round(self.dt,0))))), 
                        self.cv3[int(tu.inps(time_min)-1):int(tu.inps(time_max))], s=point_size)
    else:
        if CV==1:
            plt.plot(tu.intu(np.array(range(int(round(tu.inps(time_min),0)),int(round(tu.inps(time_max)+1,0)),int(round(self.dt,0))))), 
                     self.cv1[int(tu.inps(time_min)-1):int(tu.inps(time_max))], lw=point_size)
        if CV==2:
            plt.plot(tu.intu(np.array(range(int(round(tu.inps(time_min),0)),int(round(tu.inps(time_max)+1,0)),int(round(self.dt,0))))), 
                     self.cv2[int(tu.inps(time_min)-1):int(tu.inps(time_max))], lw=point_size)
        if CV==3:
            plt.plot(tu.intu(np.array(range(int(round(tu.inps(time_min),0)),int(round(tu.inps(time_max)+1,0)),int(round(self.dt,0))))), 
                     self.cv3[int(tu.inps(time_min)-1):int(tu.inps(time_max))], lw=point_size)
        
    if xlabel == None:
        plt.xlabel(f'time ({tu.name})', size=label_size)
    else:
        plt.xlabel(xlabel, size=label_size)
    if ylabel == None:
        if CV==1:
            plt.ylabel(f'CV {CV} - {self.cv1_name}', size=label_size)
        if CV==2:
            plt.ylabel(f'CV {CV} - {self.cv2_name}', size=label_size)
        if CV==3:
            plt.ylabel(f'CV {CV} - {self.cv3_name}', size=label_size)
    else:
        plt.ylabel(ylabel, size=label_size)
    
    if title != None:
        plt.title(title, size=label_size)

    plt.xticks(fontsize=label_size)
    plt.yticks(fontsize=label_size)
            
    ax = plt.gca()
    ax.set_xlim(xlim)
    ax.set_ylim(ylim)
        
    if png_name != None:
        plt.savefig(png_name, bbox_inches = 'tight')
    
    if return_fig:
        return fig
def plot_heights(self, png_name=None, energy_unit='kJ/mol', xlabel=None, ylabel=None, label_size=12, image_size=None, image_size_unit='in', dpi=100, tu='ps', time_min=None, time_max=None, xlim=[None, None], ylim=[None, None], title=None, return_fig=False)

Function used to visualize heights of the hills that were added during the simulation.

hillsfile.plot_heights(png_name="picture.png")

Parameters:

  • png_name = String. If this parameter is supplied, the picture of FES will be saved under this name to the current working directory.

  • energy_unit (default="kJ/mol") = String, used in description of the y axis

  • xlim, ylim (default = [None, None] for both) = list of two values specifying the range of x and y axes respectively. None means that Matplotlib will choose appropriate range, so this keyword is useful when you need to overwrite it.

  • xlabel, ylabel = Strings, if provided, they will be used as labels for the graphs

  • labelsize (default = 12) = size of text in labels

  • image_size (default = [9,6]) = list of the width and height of the picture

  • image_size_unit (default = "in") = Units for width and height of the picture, accepts "in" as inches, "cm", "mm" or "px" as pixels.

  • dpi (default = 100) = DPI of the picture

  • tu (default = "ps") = string, time unit to be shown on x axis. Also aplies to time_min and time_max, if those are used. Available options: "s", "ms", "us", "ns", "ps", "fs"

  • time_min, time_max = The time range for plot, closed interval, in the time unit specified by "tu"

  • title = optional, string that defines the title of the graph

  • return_fig (default=False) = whether the method should return the Matplotlib.Pyplot.figure object for further use

Expand source code
def plot_heights(self, png_name=None, energy_unit="kJ/mol", xlabel=None, ylabel=None, label_size=12, image_size=None, image_size_unit="in", dpi=100, tu = "ps", time_min=None, time_max=None, xlim=[None, None], ylim=[None, None], title=None, return_fig=False):
    """
    Function used to visualize heights of the hills that were added during the simulation. 
    
    ```python
    hillsfile.plot_heights(png_name="picture.png")
    ```
    
    Parameters:
    
    * png_name = String. If this parameter is supplied, the picture of FES will be saved under this name to the current working directory.
    
    * energy_unit (default="kJ/mol") = String, used in description of the y axis

    * xlim, ylim (default = [None, None] for both) = list of two values specifying the range of x and y axes respectively. 
    None means that Matplotlib will choose appropriate range, so this keyword is useful when you need to overwrite it. 
    
    * xlabel, ylabel = Strings, if provided, they will be used as labels for the graphs
    
    * labelsize (default = 12) = size of text in labels
    
    * image_size (default = [9,6]) = list of the width and height of the picture

    * image_size_unit (default = "in") = Units for width and height of the picture, accepts "in" as inches, "cm", "mm" or "px" as pixels. 

    * dpi (default = 100) = DPI of the picture

    * tu (default = "ps") = string, time unit to be shown on x axis. Also aplies to time_min and time_max, if those are used. 
    Available options: "s", "ms", "us", "ns", "ps", "fs"

    * time_min, time_max = The time range for plot, closed interval, in the time unit specified by "tu"

    * title = optional, string that defines the title of the graph

    * return_fig (default=False) = whether the method should return the Matplotlib.Pyplot.figure object for further use
    """
    tu = TU(tu)
    
    if time_min == time_max == 0:
        print("Error: Values of start and end time are zero.")
        return None
    if time_min != None:
        #time_min = tu.inps(time_min)
        if tu.inps(time_min) < 0:
            print("Warning: Start time is lower than zero, it will be set to zero instead. ")
            time_min = 0
        if tu.inps(time_min) < int(self.hills[0,0]):
            print(f"Warning: Start time {tu.inps(time_min)} ps is lower than the first time from HILLS file {int(self.hills[0,0])} ps, which will be used instead. ")
            time_min = tu.intu(self.hills[0,0])
    else:
        time_min = tu.intu(self.hills[0,0])
    if time_max != None:
        #time_max = tu.inps(time_max)
        if time_max < time_min:
            print("Warning: End time is lower than start time. Values are flipped. ")
            time_value = time_max
            time_max = time_min
            time_min = time_value
        if tu.inps(time_max) > int(self.hills[-1,0]):
            print(f"Warning: End time {tu.inps(time_max)} ps is higher than number of lines in HILLS file {int(self.hills[-1,0])} ps, which will be used instead. ")
            time_max = tu.intu(self.hills[-1,0])
    else:
        time_max = tu.intu(self.hills[-1,0])
    #print(f"Berofe fes: min {time_min}, max {time_max}")
    
    if not self.ignoretime:
        time_max = int(round(((time_max - self.hills[0,0])/self.dt),0)) + 1
        time_min = int(round(((time_min - self.hills[0,0])/self.dt),0)) + 1

    if image_size == None:
        image_size = [9,6]
    
    if image_size_unit == "cm":
        image_size[0] /= 2.54
        image_size[1] /= 2.54
    elif image_size_unit == "mm":
        image_size[0] /= 25.4
        image_size[1] /= 25.4
    elif image_size_unit == "px":
        image_size[0] /= dpi
        image_size[1] /= dpi
    elif image_size_unit != "in":
        print(f"Warning: unknown image_size_unit value: {image_size_unit}. Using inches instead. ")
        
    fig = plt.figure(figsize=(image_size[0],image_size[1]), dpi=dpi)
    #plt.plot(tu.intu(np.array(range(len(self.heights))))[time_min-1:time_max], self.heights[time_min-1:time_max])
    plt.plot(tu.intu(np.arange(int(self.hills[0,0]), self.heights.shape[0]+1)[int(tu.inps(time_min)-1):int(tu.inps(time_max))]),
             self.heights[int(tu.inps(time_min)-1):int(tu.inps(time_max))])
    if xlabel == None:
        plt.xlabel(f'time ({tu.name})', size=label_size)
    else:
        plt.xlabel(xlabel, size=label_size)
    if ylabel == None:
        plt.ylabel(f'free energy ({energy_unit})', size=label_size)
    else:
        plt.ylabel(ylabel, size=label_size)
    
    if title != None:
        plt.title(title, size=label_size)

    plt.xticks(fontsize=label_size)
    plt.yticks(fontsize=label_size)
    
    ax = plt.gca()
    ax.set_xlim(xlim)
    ax.set_ylim(ylim)
        
    if png_name != None:
        plt.savefig(png_name, bbox_inches = 'tight')
    
    if return_fig:
        return fig
class Minima (fes, nbins=8, precise=True, temp=300.0, energy_unit='kJ/mol', max_iteration=10000, print_output=True)

Object of Minima class is created to find local free energy minima on FES and calculates their free energy values.

The list of minima is stored as pandas dataframe.

Command:

minima = metadynminer.Minima(fes=f, nbins=8)

List of minima can be later shown like this:

print(minima.minima)

Parameters:

  • fes = Fes object to find the minima on

  • nbins (default = 8) = number of bins to divide the FES

  • precise (default=True) = if True, the local minima will use an algorithm which finds all local minima, even very shallow and probably unimportant minima, each point on the FES will be assigned to the minimum the system would most likely go to, if it only follows the gradient of free energy, and free energy value of minima will be calculated from each point on FES assigned to the respective minima. This results in more precise free energy values, as it accounts for the width of the minimum as well. For this calculation the unit of free energy and the thermodynamical temperature of the simulation must be supplied. This algorithm doesn't use the nbins keyword. If you set precise = False, the method will use the original algorithm from the metadynminer package for R. In this algorithm the FES is first divided to number of bins (can be set with option nbins, default is 8) and the absolute minima is found for each bin. Then the algorithm checks if this point is really a local minimum by comparing to the surrounding points of FES. This algorithm only accounts for the depth of each minima, which is less precise, but usually sufficient. In some cases, this algorithm is the prefered one, because on some free energy landscapes the total number of local free energy minima can reach tens of thousands, which makes the calculation using precise algorithm slow and impractical.

  • temp (default = 300.0) = thermodynamical temperature of the simulation. Used only if precise=True.

  • energy_unit (default = "kJ/mol") = energy unit of the free energy surface; must be either "kJ/mol" or "kcal/mol". Used only if precise=True.

  • max_iteration (default=10000), the maximum number of iteration the algorithm will last when assigning FES points to their respective local minima

Attributes:

Minima.fes = source free energy surface

Minima.minima = pandas dataframe with local minima and their properties

Minima.m_fes = if you use precise=True, this attribute contains the FES, but each bin contains the index of the minima to which it belongs

  • and other attributes analogous to those of Fes objects
Expand source code
class Minima():
    """
    Object of Minima class is created to find local free energy minima on FES and calculates their free energy values. 
    
    The list of minima is stored as pandas dataframe. 
    
    Command:
    ```python
    minima = metadynminer.Minima(fes=f, nbins=8)
    ```
    
    List of minima can be later shown like this:
    
    ```python
    print(minima.minima)
    ```
    
    Parameters:
    
    * fes = Fes object to find the minima on
    
    * nbins (default = 8) = number of bins to divide the FES

    * precise (default=True) = if True, the local minima will use an algorithm which finds all local minima, even very shallow and probably unimportant minima, each point on the FES will be assigned to the minimum the system would most likely go to, if it only follows the gradient of free energy, and free energy value of minima will be calculated from each point on FES assigned to the respective minima. This results in more precise free energy values, as it accounts for the width of the minimum as well. For this calculation the unit of free energy and the thermodynamical temperature of the simulation must be supplied. This algorithm doesn't use the nbins keyword. 
    If you set precise = False, the method will use the original algorithm from the metadynminer package for R. In this algorithm the FES is 
    first divided to number of bins (can be set with option nbins, default is 8)
    and the absolute minima is found for each bin. Then the algorithm checks 
    if this point is really a local minimum by comparing to the surrounding points of FES.
    This algorithm only accounts for the depth of each minima, which is less precise, but usually sufficient. 
    In some cases, this algorithm is the prefered one, 
    because on some free energy landscapes the total number of local free energy minima can reach tens of thousands, 
    which makes the calculation using precise algorithm slow and impractical. 

    * temp (default = 300.0) = thermodynamical temperature of the simulation. Used only if precise=True. 

    * energy_unit (default = "kJ/mol") = energy unit of the free energy surface; must be either "kJ/mol" or "kcal/mol". Used only if precise=True. 

    * max_iteration (default=10000), the maximum number of iteration the algorithm will last when assigning FES points to their respective local minima

    Attributes: 

    Minima.fes = source free energy surface 

    Minima.minima = pandas dataframe with local minima and their properties

    Minima.m_fes = if you use precise=True, this attribute contains the FES, but each bin contains the index of the minima to which it belongs

    * and other attributes analogous to those of Fes objects
    """
    
    def __init__(self, fes, nbins = 8, precise=True, temp=300.0, energy_unit="kJ/mol", max_iteration=10000, print_output=True):
        self.fes = fes.fes
        self.periodic = fes.periodic
        self.cvs = fes.cvs
        self.res = fes.res

        if self.cvs >= 1:
            self.cv1_name = fes.cv1_name
            self.cv1min = fes.cv1min
            self.cv1max = fes.cv1max
            self.cv1per = fes.cv1per
            self.cv1_fes_range = fes.cv1_fes_range
        if self.cvs >= 2:
            self.cv2min = fes.cv2min
            self.cv2max = fes.cv2max
            self.cv2_name = fes.cv2_name
            self.cv2per = fes.cv2per
            self.cv2_fes_range = fes.cv2_fes_range
        if self.cvs == 3:
            self.cv3min = fes.cv3min
            self.cv3max = fes.cv3max
            self.cv3_name = fes.cv3_name
            self.cv3per = fes.cv3per
            self.cv3_fes_range = fes.cv3_fes_range

        if precise:
            if energy_unit in ["kJ/mol", "kcal/mol"]:
                self.findminima2(temp=temp, energy_unit=energy_unit, max_iteration=max_iteration, print_output=print_output)
            else:
                print("Error: energy_unit must be either 'kJ/mol' or 'kcal/mol'. ")
        else:
            self.findminima(nbins=nbins)

    def _get_indexes(self, i=0, j=0, k=0):
        # returns array of indexes of the surroundings points of FES, respecting periodicity
        periodic = self.periodic
        if self.cvs == 1:
            a_indexes = np.empty((3))
        
            a_indexes[0] = np.array((i-1)) 
            a_indexes[1] = np.array((i))
            a_indexes[2] = np.array((i+1))
            
            if periodic[0]:
                if i == 0:
                    a_indexes[0] = (self.fes.shape[0]-1)
                elif i == self.fes.shape[0]-1:
                    a_indexes[2] = (0)
            
            # remove uninitialized parts in a_indexes - edges not periodic
            if i == 0 and not periodic[0]:
                a_indexes = a_indexes[1:]
            if i == self.fes.shape[0]-1 and not periodic[0]:
                a_indexes = a_indexes[:-1]
        
            return a_indexes.astype(int)
            
        elif self.cvs == 2:
            a_indexes = np.empty((3,3,2))
        
            a_indexes[0,:,0] = np.array((i-1,i-1,i-1)) 
            a_indexes[1,:,0] = np.array((i,i,i))
            a_indexes[2,:,0] = np.array((i+1,i+1,i+1))
            if i == 0:
                if periodic[0]:
                    a_indexes[0,:,0] = np.array((self.fes.shape[0]-1,self.fes.shape[0]-1,self.fes.shape[0]-1))
            elif i == self.fes.shape[0]-1:
                if periodic[0]:
                    a_indexes[2,:,0] = np.array((0,0,0))
        
            a_indexes[:,0,1] = np.array((j-1,j-1,j-1))
            a_indexes[:,1,1] = np.array((j,j,j))
            a_indexes[:,2,1] = np.array((j+1,j+1,j+1))
            
            if j == 0:
                if periodic[1]:
                    a_indexes[:,0,1] = np.array((self.fes.shape[1]-1,self.fes.shape[1]-1,self.fes.shape[1]-1))
            elif j == self.fes.shape[1]-1:
                if periodic[1]:
                    a_indexes[:,2,1] = np.array((0,0,0))
            
            # remove uninitialized parts in a_indexes - edges not periodic
            if i == 0 and not periodic[0]:
                a_indexes = a_indexes[1:,:,:]
            if i == self.fes.shape[0]-1 and not periodic[0]:
                a_indexes = a_indexes[:-1,:,:]
            if j == 0 and not periodic[1]:
                a_indexes = a_indexes[:,1:,:]
            if j == self.fes.shape[0]-1 and not periodic[1]:
                a_indexes = a_indexes[:,:-1,:]
        
            return a_indexes.astype(int)
        
        elif self.cvs == 3:
            a_indexes = np.empty((3,3,3,3))
        
            a_indexes[0,:,:,0] = np.ones((3,3)) * i-1
            a_indexes[1,:,:,0] = np.ones((3,3)) * i
            a_indexes[2,:,:,0] = np.ones((3,3)) * i+1
            if periodic[0]:
                if i == 0:
                    a_indexes[0,:,:,0] = np.ones((3,3)) * (self.fes.shape[0]-1)
                elif i == self.fes.shape[0]-1:
                    a_indexes[2,:,:,0] = np.zeros((3,3))
        
            a_indexes[:,0,:,1] = np.ones((3,3)) * j-1
            a_indexes[:,1,:,1] = np.ones((3,3)) * j
            a_indexes[:,2,:,1] = np.ones((3,3)) * j+1
            
            if j == 0:
                if periodic[1]:
                    a_indexes[:,0,:,1] = np.ones((3,3)) * (self.fes.shape[1]-1)
            elif j == self.fes.shape[1]-1:
                if periodic[1]:
                    a_indexes[:,2,:,1] = np.zeros((3,3))

            a_indexes[:,:,0,2] = np.ones((3,3)) * k-1
            a_indexes[:,:,1,2] = np.ones((3,3)) * k
            a_indexes[:,:,2,2] = np.ones((3,3)) * k+1
            
            if k == 0:
                if periodic[2]:
                    a_indexes[:,:,0,2] = np.ones((3,3)) * (self.fes.shape[2]-1)
            elif k == self.fes.shape[2]-1:
                if periodic[2]:
                    a_indexes[:,:,2,2] = np.zeros((3,3))
            
            # remove uninitialized parts in a_indexes - edges not periodic
            if i == 0 and not periodic[0]:
                a_indexes = a_indexes[1:,:,:,:]
            if i == self.fes.shape[0]-1 and not periodic[0]:
                a_indexes = a_indexes[:-1,:,:,:]
            if j == 0 and not periodic[1]:
                a_indexes = a_indexes[:,1:,:,:]
            if j == self.fes.shape[1]-1 and not periodic[1]:
                a_indexes = a_indexes[:,:-1,:,:]
            if k == 0 and not periodic[2]:
                a_indexes = a_indexes[:,:,1:,:]
            if k == self.fes.shape[2]-1 and not periodic[2]:
                a_indexes = a_indexes[:,:,:-1,:]
        return a_indexes
    
    def findminima2(self, temp=300.0, energy_unit="kJ/mol", max_iteration=10000, print_output=True):
        if self.cvs >= 1:
            cv1min = self.cv1min
            cv1max = self.cv1max
        if self.cvs >=2:
            cv2min = self.cv2min
            cv2max = self.cv2max 
        if self.cvs == 3:
            cv3min = self.cv3min
            cv3max = self.cv3max
        
        self.minima = []

        if print_output:
            print(f"Calculating gradients for FES with {self.fes.flatten().shape[0]} bins... ")
        
        if self.cvs == 1:
            m_fes = np.zeros((self.fes.shape))
            minima_count = 0
            dirs = np.zeros((self.fes.shape[0], 1))
            minima_list = []
            
            periodic=self.periodic
            
            for i in range(self.fes.shape[0]):
                a_indexes = self._get_indexes(i)
        
                okoli = np.zeros((a_indexes[:].shape))
                for ii in range(okoli.shape[0]):
                    okoli[ii] = self.fes[int(a_indexes[ii])]
                
                m_i = np.unravel_index(np.argmin(okoli), okoli.shape, order='C')
                
                dirs[i,0] = a_indexes[m_i]
                if self.fes[i] == self.fes[int(a_indexes[m_i])] and not np.all(okoli == np.min(okoli)):
                    minima_count += 1
                    m_fes[i] = minima_count
                    min_cv1 = (((i)/self.res)*(cv1max-cv1min))+cv1min
                    minima_list.append([self.fes[i],i, min_cv1])

            if print_output:
                print("Searching for the nearest local minima... ")
            iteration = 0
            while 0.0 in m_fes[:]:
                iteration += 1
                if iteration > max_iteration:
                    print("Warning: Maximum number of iterations reached when searching. ")
                    break
                for i in range(self.fes.shape[0]):
                    m_fes[i] = m_fes[int(dirs[i])]
            if iteration <= max_iteration and print_output:
                print("Done.")
            if 0.0 in m_fes[:]:
                if print_output:
                    print("Warning: some of the FES bins were not associated to any local minimum. ")

            self.minima = np.array(minima_list)
            self.m_fes = m_fes
            
            if energy_unit == "kJ/mol":
                prob = np.exp(-1000*self.fes/8.314/temp)
            elif energy_unit == "kcal_mol":
                prob = np.exp(-1000*4.184*self.fes/8.314/temp)

            m_sum_probabilities = np.zeros((self.minima[:,0].shape))
            for i in range(self.fes.shape[0]):
                if m_fes[i] > 0.0:
                    m_sum_probabilities[int(m_fes[i])-1] += prob[i]

            if energy_unit == "kJ/mol":
                m_energies = -8.314*temp*np.log(m_sum_probabilities)/1000
            elif energy_unit == "kcal_mol":
                m_energies = -8.314*temp*np.log(m_sum_probabilities)/1000/4.184

            m_energies = m_energies - np.min(m_energies)
            self.minima[:,0] = m_energies
            
        elif self.cvs == 2:
            m_fes = np.zeros((self.fes.shape))
            minima_count = 0
            dirs = np.zeros((self.fes.shape[0], self.fes.shape[1], 2))
            minima_list = []
            
            periodic=self.periodic
            
            for i in range(self.fes.shape[0]):
                for j in range(self.fes.shape[1]):
                    a_indexes = self._get_indexes(i,j)
            
                    okoli = np.zeros((a_indexes[:,:,0].shape))
                    for ii in range(okoli.shape[0]):
                        for jj in range(okoli.shape[1]):
                            okoli[ii,jj] = self.fes[int(a_indexes[ii,jj,0]),int(a_indexes[ii,jj,1])]
                    
                    m_i, m_j = np.unravel_index(np.argmin(okoli), okoli.shape, order='C')

                    if not np.all(okoli == np.min(okoli)):
                        dirs[i,j, 0] = a_indexes[m_i,m_j,0]
                        dirs[i,j, 1] = a_indexes[m_i,m_j,1]
                        if self.fes[i,j] == self.fes[int(a_indexes[m_i,m_j,0]), int(a_indexes[m_i, m_j, 1])]:
                            minima_count += 1
                            m_fes[i,j] = minima_count
                            min_cv1 = (((i)/self.res)*(cv1max-cv1min))+cv1min
                            min_cv2 = (((j)/self.res)*(cv2max-cv2min))+cv2min
                            minima_list.append([self.fes[i,j],i,j, min_cv1, min_cv2])
                    else:
                        dirs[i,j, 0] = i
                        dirs[i,j, 1] = j

            if print_output:
                print("Searching for the nearest local minima... ")
            iteration = 0
            while 0.0 in m_fes[:,:]:
                iteration += 1
                if iteration > max_iteration:
                    if print_output:
                        print("Warning: Maximum number of iterations reached when searching. ")
                    break
                for i in range(self.fes.shape[0]):
                    for j in range(self.fes.shape[1]):
                        m_fes[i,j] = m_fes[int(dirs[i,j,0]), int(dirs[i,j,1])]
            if iteration <= max_iteration:
                if print_output:
                    print("Done.")  
            if 0.0 in m_fes[:,:]:
                if print_output:
                    print("Warning: some of the FES bins were not associated to any local minimum. ")
            
            self.minima = np.array(minima_list)
            self.m_fes = m_fes
            
            if energy_unit == "kJ/mol":
                prob = np.exp(-1000*self.fes/8.314/temp)
            elif energy_unit == "kcal_mol":
                prob = np.exp(-1000*4.184*self.fes/8.314/temp)

            m_sum_probabilities = np.zeros((self.minima[:,0].shape))
            for i in range(self.fes.shape[0]):
                for j in range(self.fes.shape[1]):
                    if m_fes[i,j] > 0.0:
                        m_sum_probabilities[int(m_fes[i,j])-1] += prob[i,j]

            if energy_unit == "kJ/mol":
                m_energies = -8.314*temp*np.log(m_sum_probabilities)/1000
            elif energy_unit == "kcal_mol":
                m_energies = -8.314*temp*np.log(m_sum_probabilities)/1000/4.184

            m_energies = m_energies - np.min(m_energies)
            self.minima[:,0] = m_energies
            
        elif self.cvs == 3:
            m_fes = np.zeros((self.fes.shape))
            minima_count = 0
            dirs = np.zeros((self.fes.shape[0], self.fes.shape[1], self.fes.shape[2],3))
            minima_list = []
            
            periodic=self.periodic
            
            for i in range(self.fes.shape[0]):
                for j in range(self.fes.shape[1]):
                    for k in range(self.fes.shape[2]):
                        a_indexes = self._get_indexes(i,j,k)
                        
                        okoli = np.zeros((a_indexes[:,:,:,0].shape))
                        for ii in range(okoli.shape[0]):
                            for jj in range(okoli.shape[1]):
                                for kk in range(okoli.shape[2]):
                                    okoli[ii,jj, kk] = self.fes[int(a_indexes[ii,jj,kk,0]),int(a_indexes[ii,jj,kk,1]),int(a_indexes[ii,jj,kk,2])]
                        
                        m_i, m_j, m_k = np.unravel_index(np.argmin(okoli), okoli.shape, order='C')
                        
                        dirs[i,j,k,0] = a_indexes[m_i,m_j,m_k,0]
                        dirs[i,j,k,1] = a_indexes[m_i,m_j,m_k,1]
                        dirs[i,j,k,2] = a_indexes[m_i,m_j,m_k,2]
                        if self.fes[i,j,k] == self.fes[int(a_indexes[m_i,m_j,m_k,0]), int(a_indexes[m_i, m_j,m_k,1]), int(a_indexes[m_i, m_j,m_k,2])] and not np.all(okoli == np.min(okoli)):
                            minima_count += 1
                            m_fes[i,j,k] = minima_count
                            min_cv1 = (((i)/self.res)*(cv1max-cv1min))+cv1min
                            min_cv2 = (((j)/self.res)*(cv2max-cv2min))+cv2min
                            min_cv3 = (((k)/self.res)*(cv3max-cv3min))+cv3min
                            minima_list.append([self.fes[i,j,k],i,j,k,min_cv1,min_cv2,min_cv3])

            if print_output:
                print("Searching for the nearest local minima... ")
            iteration = 0
            while 0.0 in m_fes[:,:,:]:
                iteration += 1
                if iteration > max_iteration:
                    print("Warning: Maximum number of iterations reached when searching. ")
                    break
                for i in range(self.fes.shape[0]):
                    for j in range(self.fes.shape[1]):
                        for k in range(self.fes.shape[2]):
                            m_fes[i,j,k] = m_fes[int(dirs[i,j,k,0]), int(dirs[i,j,k,1]),int(dirs[i,j,k,2])]
            if iteration <= max_iteration:
                if print_output:
                    print("Done.")
            if 0.0 in m_fes[:,:,:]:
                if print_output:
                    print("Warning: some of the FES bins were not associated to any local minimum. ")

            self.minima = np.array(minima_list)
            self.m_fes = m_fes
            
            if energy_unit == "kJ/mol":
                prob = np.exp(-1000*self.fes/8.314/temp)
            elif energy_unit == "kcal_mol":
                prob = np.exp(-1000*4.184*self.fes/8.314/temp)

            m_sum_probabilities = np.zeros((self.minima[:,0].shape))
            for i in range(self.fes.shape[0]):
                for j in range(self.fes.shape[1]):
                    for k in range(self.fes.shape[2]):
                        if m_fes[i,j,k] > 0.0:
                            m_sum_probabilities[int(m_fes[i,j,k])-1] += prob[i,j,k]

            if energy_unit == "kJ/mol":
                m_energies = -8.314*temp*np.log(m_sum_probabilities)/1000
            elif energy_unit == "kcal_mol":
                m_energies = -8.314*temp*np.log(m_sum_probabilities)/1000/4.184

            m_energies = m_energies - np.min(m_energies)
            self.minima[:,0] = m_energies
        else:
            print("Fes object has unsupported number of CVs.")

        
        if len(self.minima.shape)>1:
            self.minima = self.minima[self.minima[:, 0].argsort()]

            # renumber minima indexes in m_fes according to their energies (and letters)
            new_m_fes = np.zeros((self.fes.shape))
            
            for m in range(self.minima.shape[0]):
                if self.cvs == 1:
                    m_old_id = self.m_fes[int(float(self.minima[m,1]))]
                if self.cvs == 2:
                    m_old_id = self.m_fes[int(float(self.minima[m,1])), int(float(self.minima[m,2]))]
                if self.cvs == 3:
                    m_old_id = self.m_fes[int(float(self.minima[m,1])), int(float(self.minima[m,2])), int(float(self.minima[m,3]))]
                new_m_fes[self.m_fes == m_old_id] = m + 1
            self.m_fes = new_m_fes

        
        letters = list(map(chr, range(65, 91)))
        for letter1 in range(65, 91):
            for letter2 in range(65, 91):
                letters.append(f"{chr(letter1)}{chr(letter2)}")
        if len(self.minima.shape) > 1:
            if self.minima.shape[0] < len(letters):
                self.minima = np.column_stack((letters[0:self.minima.shape[0]],self.minima))
            else:
                print("Error: Too many minima to assign letters. Try using keyword precise=False. ")
                return None
        elif len(self.minima.shape) == 1:
            self.minima = np.append("A", self.minima)
        
        if self.cvs == 1:
            if len(self.minima.shape)>1:
                self.minima = pd.DataFrame(np.array(self.minima), columns = ["Minimum", "free energy", "CV1bin", "CV1 - "+self.cv1_name])
            elif len(self.minima.shape) == 1:
                self.minima = pd.DataFrame([self.minima], columns = ["Minimum", "free energy", "CV1bin", "CV1 - "+self.cv1_name])
                
        elif self.cvs == 2:
            if len(self.minima.shape)>1:
                self.minima = pd.DataFrame(np.array(self.minima), columns = ["Minimum", "free energy", "CV1bin", "CV2bin", 
                                                               "CV1 - "+self.cv1_name, "CV2 - "+self.cv2_name])
            elif len(self.minima.shape) == 1:
                self.minima = pd.DataFrame([self.minima], columns = ["Minimum", "free energy", "CV1bin", "CV2bin", 
                                                               "CV1 - "+self.cv1_name, "CV2 - "+self.cv2_name])
        elif self.cvs == 3:
            if len(self.minima.shape)>1:
                self.minima = pd.DataFrame(np.array(self.minima), columns = ["Minimum", "free energy", "CV1bin", "CV2bin", "CV3bin", 
                                                               "CV1 - "+self.cv1_name, "CV2 - "+self.cv2_name,  "CV3 - "+self.cv3_name])
            elif len(self.minima.shape) == 1:
                self.minima = pd.DataFrame([self.minima], columns = ["Minimum", "free energy", "CV1bin", "CV2bin", "CV3bin", 
                                                               "CV1 - "+self.cv1_name, "CV2 - "+self.cv2_name,  "CV3 - "+self.cv3_name])


    def findminima(self, nbins=8, print_output=True):
        if int(nbins) != nbins:
            nbins = int(nbins)
            print(f"Number of bins must be an integer, it will be set to {nbins}.")
        if self.res%nbins != 0:
            print("Error: Resolution of FES must be divisible by number of bins.")
            return None
        if nbins > self.res/2:
            print("Error: Number of bins is too high.")
            return None
        bin_size = int(self.res/nbins)

        if self.cvs >= 1:
            cv1min = self.cv1min
            cv1max = self.cv1max
        if self.cvs >=2:
            cv2min = self.cv2min
            cv2max = self.cv2max 
        if self.cvs == 3:
            cv3min = self.cv3min
            cv3max = self.cv3max
        
        self.minima = []
        if self.cvs == 1:
            for bin1 in range(0,nbins):
                fes_slice = self.fes[bin1*bin_size:(bin1+1)*bin_size]
                bin_min = np.min(fes_slice)
                argmin = np.argmin(fes_slice)
                # indexes of global minimum of a bin
                bin_min_arg_cv1 = int(argmin%bin_size)
                # indexes of that minima in the original fes (indexes +1)
                min_cv1_b = int(bin_min_arg_cv1+bin1*bin_size)
                if (bin_min_arg_cv1 > 0 and bin_min_arg_cv1<(bin_size-1)):
                    min_cv1 = (((min_cv1_b)/self.res)*(cv1max-cv1min))+cv1min#min_cv1 = (((min_cv1_b+0.5)/self.res)*(cv1max-cv1min))+cv1min
                    if len(self.minima) == 0:
                        self.minima = np.array([round(bin_min, 6), int(min_cv1_b), round(min_cv1, 6)])
                    else:
                        self.minima = np.vstack((self.minima, np.array([round(bin_min, 6), int(min_cv1_b), round(min_cv1, 6)])))
                else:
                    around = []
                    min_cv1_b_low = min_cv1_b - 1
                    if min_cv1_b_low == -1:
                        if self.periodic[0]:
                            min_cv1_b_low = self.res - 1
                        else:
                            min_cv1_b_low = float("nan")

                    min_cv1_b_high = min_cv1_b + 1
                    if min_cv1_b_high == self.res:
                        if self.periodic[0]:
                            min_cv1_b_high = 0
                        else:
                            min_cv1_b_high = float("nan")

                    #1_b_low
                    if not(np.isnan(min_cv1_b_low)):
                        around.append(self.fes[min_cv1_b_low])
                    #1_b_high
                    if not(np.isnan(min_cv1_b_high)):
                        around.append(self.fes[min_cv1_b_high])
                    
                    if bin_min < np.min(around):
                        min_cv1 = (((min_cv1_b)/self.res)*(cv1max-cv1min))+cv1min#min_cv1 = (((min_cv1_b+0.5)/self.res)*(cv1max-cv1min))+cv1min
                        if len(self.minima) == 0:
                            self.minima=np.array([round(bin_min, 6), int(min_cv1_b), round(min_cv1, 6)])
                        else:
                            self.minima=np.vstack((self.minima, np.array([round(bin_min, 6), int(min_cv1_b), round(min_cv1, 6)])))
            
        elif self.cvs == 2:
            for bin1 in range(0,nbins):
                for bin2 in range(0,nbins):
                    fes_slice = self.fes[bin1*bin_size:(bin1+1)*bin_size,
                                         bin2*bin_size:(bin2+1)*bin_size]
                    bin_min = np.min(fes_slice)
                    argmin = np.argmin(fes_slice)
                    # indexes of global minimum of a bin
                    bin_min_arg = np.unravel_index(np.argmin(fes_slice), fes_slice.shape)
                    # indexes of that minima in the original fes (indexes +1)
                    min_cv1_b = int(bin_min_arg[0]+bin1*bin_size)
                    min_cv2_b = int(bin_min_arg[1]+bin2*bin_size)
                    if (bin_min_arg[0] > 0 and bin_min_arg[0]<(bin_size-1)) \
                                    and (bin_min_arg[1] > 0 and bin_min_arg[1]<(bin_size-1)):
                        min_cv1 = (((min_cv1_b)/self.res)*(cv1max-cv1min))+cv1min#min_cv1 = (((min_cv1_b+0.5)/self.res)*(cv1max-cv1min))+cv1min
                        min_cv2 = (((min_cv2_b)/self.res)*(cv2max-cv2min))+cv2min#min_cv1 = (((min_cv1_b+0.5)/self.res)*(cv1max-cv1min))+cv1min
                        if len(self.minima) == 0:
                            self.minima=np.array([round(bin_min, 6), int(min_cv1_b),\
                                                  int(min_cv2_b), round(min_cv1, 6), round(min_cv2, 6)])
                        else:
                            self.minima=np.vstack((self.minima, np.array([round(bin_min, 6), int(min_cv1_b), \
                                                                          int(min_cv2_b), round(min_cv1, 6), round(min_cv2, 6)])))
                    else:
                        around = []
                        min_cv1_b_low = min_cv1_b - 1
                        if min_cv1_b_low == -1:
                            if self.periodic[0]:
                                min_cv1_b_low = self.res - 1
                            else:
                                min_cv1_b_low = float("nan")

                        min_cv1_b_high = min_cv1_b + 1
                        if min_cv1_b_high == self.res:
                            if self.periodic[0]:
                                min_cv1_b_high = 0
                            else:
                                min_cv1_b_high = float("nan")

                        min_cv2_b_low = min_cv2_b - 1
                        if min_cv2_b_low == -1:
                            if self.periodic[0]:
                                min_cv2_b_low = self.res - 1
                            else:
                                min_cv2_b_low = float("nan")

                        min_cv2_b_high = min_cv2_b + 1
                        if min_cv2_b_high == self.res:
                            if self.periodic[0]:
                                min_cv2_b_high = 0
                            else:
                                min_cv2_b_high = float("nan")
                        #1_b_low
                        if not(np.isnan(min_cv1_b_low)):
                            if not(np.isnan(min_cv2_b_low)):
                                around.append(self.fes[min_cv1_b_low, min_cv2_b_low])
                            around.append(self.fes[min_cv1_b_low,min_cv2_b])
                            if not(np.isnan(min_cv2_b_high)):
                                around.append(self.fes[min_cv1_b_low, min_cv2_b_high])
                        #1_b
                        if not(np.isnan(min_cv2_b_low)):
                            around.append(self.fes[min_cv1_b, min_cv2_b_low])
                        if not(np.isnan(min_cv2_b_high)):
                            around.append(self.fes[min_cv1_b, min_cv2_b_high])
                        #1_b_high
                        if not(np.isnan(min_cv1_b_high)):
                            if not(np.isnan(min_cv2_b_low)):
                                around.append(self.fes[min_cv1_b_high, min_cv2_b_low])
                            around.append(self.fes[min_cv1_b_high, min_cv2_b])
                            if not(np.isnan(min_cv2_b_high)):
                                around.append(self.fes[min_cv1_b_high, min_cv2_b_high])
                        if bin_min < np.min(around):
                            min_cv1 = (((min_cv1_b)/self.res)*(cv1max-cv1min))+cv1min#min_cv1 = (((min_cv1_b+0.5)/self.res)*(cv1max-cv1min))+cv1min
                            min_cv2 = (((min_cv2_b)/self.res)*(cv2max-cv2min))+cv2min#min_cv1 = (((min_cv1_b+0.5)/self.res)*(cv1max-cv1min))+cv1min
                            if len(self.minima) == 0:
                                self.minima=np.array([round(bin_min, 6), int(min_cv1_b), int(min_cv2_b), \
                                                      round(min_cv1, 6), round(min_cv2, 6)])
                            else:
                                self.minima=np.vstack((self.minima, np.array([round(bin_min, 6), int(min_cv1_b), \
                                                                              int(min_cv2_b), round(min_cv1, 6), round(min_cv2, 6)])))
        elif self.cvs == 3:
            for bin1 in range(0,nbins):
                for bin2 in range(0,nbins):
                    for bin3 in range(0, nbins):
                        fes_slice = self.fes[bin1*bin_size:(bin1+1)*bin_size,
                                             bin2*bin_size:(bin2+1)*bin_size, 
                                             bin3*bin_size:(bin3+1)*bin_size]
                        bin_min = np.min(fes_slice)
                        argmin = np.argmin(fes_slice)
                        # indexes of global minimum of a bin
                        bin_min_arg = np.unravel_index(np.argmin(fes_slice), fes_slice.shape)
                        # indexes of that minima in the original fes (indexes +1)
                        min_cv1_b = int(bin_min_arg[0]+bin1*bin_size)
                        min_cv2_b = int(bin_min_arg[1]+bin2*bin_size)
                        min_cv3_b = int(bin_min_arg[2]+bin3*bin_size)
                        if (bin_min_arg[0] > 0 and bin_min_arg[0]<(bin_size-1)) \
                                        and (bin_min_arg[1] > 0 and bin_min_arg[1]<(bin_size-1))\
                                        and (bin_min_arg[2] > 0 and bin_min_arg[2]<(bin_size-1)):
                            min_cv1 = (((min_cv1_b)/self.res)*(cv1max-cv1min))+cv1min #min_cv1 = (((min_cv1_b+0.5)/self.res)*(cv1max-cv1min))+cv1min
                            min_cv2 = (((min_cv2_b)/self.res)*(cv2max-cv2min))+cv2min #min_cv2 = (((min_cv2_b+0.5)/self.res)*(cv2max-cv2min))+cv2min
                            min_cv3 = (((min_cv3_b)/self.res)*(cv3max-cv3min))+cv3min #min_cv3 = (((min_cv3_b+0.5)/self.res)*(cv3max-cv3min))+cv3min
                            if len(self.minima) == 0:
                                self.minima=np.array([round(bin_min, 6), int(min_cv1_b),\
                                                      int(min_cv2_b), int(min_cv3_b), round(min_cv1, 6), \
                                                      round(min_cv2, 6), round(min_cv3, 6)])
                            else:
                                self.minima=np.vstack((self.minima, np.array([round(bin_min, 6), int(min_cv1_b),\
                                                      int(min_cv2_b), int(min_cv3_b), round(min_cv1, 6), \
                                                      round(min_cv2, 6), round(min_cv3, 6)])))
                        else:
                            around = []
                            min_cv1_b_low = min_cv1_b - 1
                            if min_cv1_b_low == -1:
                                if self.periodic[0]:
                                    min_cv1_b_low = self.res - 1
                                else:
                                    min_cv1_b_low = float("nan")

                            min_cv1_b_high = min_cv1_b + 1
                            if min_cv1_b_high == self.res:
                                if self.periodic[0]:
                                    min_cv1_b_high = 0
                                else:
                                    min_cv1_b_high = float("nan")

                            min_cv2_b_low = min_cv2_b - 1
                            if min_cv2_b_low == -1:
                                if self.periodic[0]:
                                    min_cv2_b_low = self.res - 1
                                else:
                                    min_cv2_b_low = float("nan")

                            min_cv2_b_high = min_cv2_b + 1
                            if min_cv2_b_high == self.res:
                                if self.periodic[0]:
                                    min_cv2_b_high = 0
                                else:
                                    min_cv2_b_high = float("nan")
                                                       
                            min_cv3_b_low = min_cv3_b - 1
                            if min_cv3_b_low == -1:
                                if self.periodic[2]:
                                    min_cv3_b_low = self.res - 1
                                else:
                                    min_cv3_b_low = float("nan")

                            min_cv3_b_high = min_cv3_b + 1
                            if min_cv3_b_high == self.res:
                                if self.periodic[2]:
                                    min_cv3_b_high = 0
                                else:
                                    min_cv3_b_high = float("nan")

#cv3_b
                            #1_b_low
                            if not(np.isnan(min_cv1_b_low)):
                                if not(np.isnan(min_cv2_b_low)):
                                    around.append(self.fes[min_cv1_b_low,min_cv2_b_low,min_cv3_b])
                                around.append(self.fes[min_cv1_b_low,min_cv2_b,min_cv3_b])
                                if not(np.isnan(min_cv2_b_high)):
                                    around.append(self.fes[min_cv1_b_low,min_cv2_b_high,min_cv3_b])
                            #1_b
                            if not(np.isnan(min_cv2_b_low)):
                                around.append(self.fes[min_cv1_b,min_cv2_b_low,min_cv3_b])
                            if not(np.isnan(min_cv2_b_high)):
                                around.append(self.fes[min_cv1_b,min_cv2_b_high,min_cv3_b])
                            #1_b_high
                            if not(np.isnan(min_cv1_b_high)):
                                if not(np.isnan(min_cv2_b_low)):
                                    around.append(self.fes[min_cv1_b_high,min_cv2_b_low,min_cv3_b])
                                around.append(self.fes[min_cv1_b_high,min_cv2_b,min_cv3_b])
                                if not(np.isnan(min_cv2_b_high)):
                                    around.append(self.fes[min_cv1_b_high,min_cv2_b_high,min_cv3_b])
                           
                            if not(np.isnan(min_cv3_b_low)):
                            #1_b_low
                                if not(np.isnan(min_cv1_b_low)):
                                    if not(np.isnan(min_cv2_b_low)):
                                        around.append(self.fes[min_cv1_b_low,min_cv2_b_low,min_cv3_b_low])
                                    around.append(self.fes[min_cv1_b_low,min_cv2_b,min_cv3_b_low])
                                    if not(np.isnan(min_cv2_b_high)):
                                        around.append(self.fes[min_cv1_b_low,min_cv2_b_high,min_cv3_b_low])
                                #1_b
                                if not(np.isnan(min_cv2_b_low)):
                                    around.append(self.fes[min_cv1_b,min_cv2_b_low,min_cv3_b_low])
                                if not(np.isnan(min_cv2_b_high)):
                                    around.append(self.fes[min_cv1_b,min_cv2_b_high,min_cv3_b_low])
                                #1_b_high
                                if not(np.isnan(min_cv1_b_high)):
                                    if not(np.isnan(min_cv2_b_low)):
                                        around.append(self.fes[min_cv1_b_high,min_cv2_b_low,min_cv3_b_low])
                                    around.append(self.fes[min_cv1_b_high,min_cv2_b,min_cv3_b_low])
                                    if not(np.isnan(min_cv2_b_high)):
                                        around.append(self.fes[min_cv1_b_high,min_cv2_b_high,min_cv3_b_low])
                            
                            if not(np.isnan(min_cv2_b_high)):
                                #1_b_low
                                if not(np.isnan(min_cv1_b_low)):
                                    if not(np.isnan(min_cv2_b_low)):
                                        around.append(self.fes[min_cv1_b_low,min_cv2_b_low,min_cv3_b_high])
                                    around.append(self.fes[min_cv1_b_low,min_cv2_b,min_cv3_b_high])
                                    if not(np.isnan(min_cv2_b_high)):
                                        around.append(self.fes[min_cv1_b_low,min_cv2_b_high,min_cv3_b_high])
                                #1_b
                                if not(np.isnan(min_cv2_b_low)):
                                    around.append(self.fes[min_cv1_b,min_cv2_b_low,min_cv3_b_high])
                                if not(np.isnan(min_cv2_b_high)):
                                    around.append(self.fes[min_cv1_b,min_cv2_b_high,min_cv3_b_high])
                                #1_b_high
                                if not(np.isnan(min_cv1_b_high)):
                                    if not(np.isnan(min_cv2_b_low)):
                                        around.append(self.fes[min_cv1_b_high,min_cv2_b_low,min_cv3_b_high])
                                    around.append(self.fes[min_cv1_b_high,min_cv2_b,min_cv3_b_high])
                                    if not(np.isnan(min_cv2_b_high)):
                                        around.append(self.fes[min_cv1_b_high,min_cv2_b_high,min_cv3_b_high])
                            
                            if bin_min < np.min(around):
                                min_cv1 = (((min_cv1_b)/self.res)*(cv1max-cv1min))+cv1min #min_cv1 = (((min_cv1_b+0.5)/self.res)*(cv1max-cv1min))+cv1min
                                min_cv2 = (((min_cv2_b)/self.res)*(cv2max-cv2min))+cv2min #min_cv2 = (((min_cv2_b+0.5)/self.res)*(cv2max-cv2min))+cv2min
                                min_cv3 = (((min_cv3_b)/self.res)*(cv3max-cv3min))+cv3min #min_cv3 = (((min_cv3_b+0.5)/self.res)*(cv3max-cv3min))+cv3min
                                if len(self.minima) == 0:
                                    self.minima=np.array([round(bin_min, 6), int(min_cv1_b),\
                                                      int(min_cv2_b), int(min_cv3_b), round(min_cv1, 6), \
                                                      round(min_cv2, 6), round(min_cv3, 6)])
                                else:
                                    self.minima=np.vstack((self.minima, np.array([round(bin_min, 6), int(min_cv1_b),\
                                                      int(min_cv2_b), int(min_cv3_b), round(min_cv1, 6), \
                                                      round(min_cv2, 6), round(min_cv3, 6)])))
        
        else:
            print("Fes object has unsupported number of CVs.")
        
        if len(self.minima.shape)>1:
            self.minima = self.minima[self.minima[:, 0].argsort()]

        letters = list(map(chr, range(65, 91)))
        for letter1 in range(65, 91):
            for letter2 in range(65, 91):
                letters.append(f"{chr(letter1)}{chr(letter2)}")
        if len(self.minima.shape)>1:
            if self.minima.shape[1] < len(letters):
                self.minima = np.column_stack((letters[0:self.minima.shape[0]],self.minima))
            else:
                print("Error: Too many minima to assign letters.")
        elif len(self.minima.shape) == 1:
            self.minima = np.append("A", self.minima)
        
        if self.cvs == 1:
            if len(self.minima.shape)>1:
                self.minima = pd.DataFrame(np.array(self.minima), columns = ["Minimum", "free energy", "CV1bin", "CV1 - "+self.cv1_name])
            elif len(self.minima.shape) == 1:
                self.minima = pd.DataFrame([self.minima], columns = ["Minimum", "free energy", "CV1bin", "CV1 - "+self.cv1_name])
                
        elif self.cvs == 2:
            if len(self.minima.shape)>1:
                self.minima = pd.DataFrame(np.array(self.minima), columns = ["Minimum", "free energy", "CV1bin", "CV2bin", 
                                                               "CV1 - "+self.cv1_name, "CV2 - "+self.cv2_name])
            elif len(self.minima.shape) == 1:
                self.minima = pd.DataFrame([self.minima], columns = ["Minimum", "free energy", "CV1bin", "CV2bin", 
                                                               "CV1 - "+self.cv1_name, "CV2 - "+self.cv2_name])
        elif self.cvs == 3:
            if len(self.minima.shape)>1:
                self.minima = pd.DataFrame(np.array(self.minima), columns = ["Minimum", "free energy", "CV1bin", "CV2bin", "CV3bin", 
                                                               "CV1 - "+self.cv1_name, "CV2 - "+self.cv2_name,  "CV3 - "+self.cv3_name])
            elif len(self.minima.shape) == 1:
                self.minima = pd.DataFrame([self.minima], columns = ["Minimum", "free energy", "CV1bin", "CV2bin", "CV3bin", 
                                                               "CV1 - "+self.cv1_name, "CV2 - "+self.cv2_name,  "CV3 - "+self.cv3_name])
        

    def plot(self, png_name=None, contours=True, contours_spacing=0.0, aspect = 1.0, cmap = "RdYlBu_r", 
                 energy_unit="kJ/mol", xlabel=None, ylabel=None, zlabel=None, label_size=12, clabel_size = 12, image_size=None, image_size_unit="in", dpi=100, color=None, vmin = 0, vmax = None, opacity=0.2, levels=None, show_points=True, point_size=4.0, title = None, off_screen = False, xlim=[None, None], ylim=[None, None], return_fig=False):
        """
        The same function as for visualizing Fes objects, but this time 
        with the positions of local minima shown as letters on the graph.
        
        ```python
        minima.plot(png_name="minima.png")
        ```
        
        Parameters:
        
        * png_name = String. If this parameter is supplied, the picture of FES will be saved under this name to the current working directory.
        
        * contours (default=True) = whether contours should be shown on 2D FES
        
        * contours_spacing (default=0.0) = when a positive number is set, it will be used as spacing for contours on 2D FES. 
                Otherwise, if contours=True, there will be five equally spaced contour levels.
        
        * aspect (default = 1.0) = aspect ratio of the graph. Works with 1D and 2D FES. 
        
        * cmap (default = "RdYlBu_r") = Matplotlib colormap used to color 2D or 3D FES
        
        * energy_unit (default="kJ/mol") = String, used in description of colorbar

        * xlim, ylim (default = [None, None] for both) = list of two values specifying the range of x and y axes respectively. 
        None means that Matplotlib will choose appropriate range, so this keyword is useful when you need to overwrite it. 
        
        * xlabel, ylabel, zlabel = Strings, if provided, they will be used as labels for the graphs
        
        * label_size, clabel_size (default = 12) = size of text in labels or contours labels, respectively
        
        * image_size (default = [9,6]) = List of the width and height of the picture

        * image_size_unit (default = "in") = Units for width and height of the picture, accepts "in" as inches, "cm", "mm" or "px" as pixels.

        * dpi (default = 100) = DPI of the resulting image. 
        
        * color = string = name of color in matplotlib, if set, the color will be used for the letters. 
                If not set, the color should be automatically either black or white, 
                depending on what will be better visible on given place on FES with given colormap (for 2D FES).
        
        * vmin (default=0) = real number, lower bound for the colormap on 2D FES
        
        * vmax = real number, upper bound for the colormap on 2D FES
        
        * opacity (default=0.2) = number between 0 and 1, is the opacity of isosurfaces of 3D FES
        
        * levels = Here you can specify list of free energy values for isosurfaces on 3D FES. 
                If not provided, default values from contours parameters will be used instead. 
        
        * show_points (default=True) = boolean, tells if points should be visualized too, instead of just the letters. Only on 3D FES. 
        
        * point_size (default=4.0) = float, sets the size of points if show_points=True

        * title = optional, string that defines the title of the graph

        * offscreen (default = False) = for 3D FES only, grapf will not be shown after creation - used internally when making animations

        * return_fig (default=False) = whether the method should return the Matplotlib.Pyplot.figure object for further use. 
        In the case of plotting 3D FES, it returns Pyvista.plotter object instead. 
        """
        
        if vmax == None:
            vmax = np.max(self.fes)+0.01 # if the addition is smaller than 0.01, the 3d plot stops working. 
            
        if contours_spacing == 0.0:
            contours_spacing = (vmax-vmin)/5.0
        
        cmap = cm.get_cmap(cmap)
        
        cmap.set_over("white")
        cmap.set_under("white")
        
        color_set = True
        if color == None:
            color_set = False
        
        if self.cvs >= 1:
            cv1min = self.cv1min
            cv1max = self.cv1max 
        if self.cvs >=2:
            cv2min = self.cv2min
            cv2max = self.cv2max
        if self.cvs == 3:
            cv3min = self.cv3min
            cv3max = self.cv3max

        if image_size == None:
            image_size = [9,6]
        
        if image_size_unit == "cm":
            image_size[0] /= 2.54
            image_size[1] /= 2.54
        elif image_size_unit == "mm":
            image_size[0] /= 25.4
            image_size[1] /= 25.4
        elif image_size_unit == "px":
            image_size[0] /= dpi
            image_size[1] /= dpi
        elif image_size_unit != "in":
            print(f"Warning: unknown image_size_unit value: {image_size_unit}. Using inches instead. ")
            
        if self.cvs == 1:
            fig = plt.figure(figsize=(image_size[0],image_size[1]), dpi=dpi)
            X = np.linspace(cv1min, cv1max, self.res)
            plt.plot(X, self.fes)
            
            if not color_set:
                color = "black"
            
            fesrange = np.max(self.fes) - np.min(self.fes)
            
            if self.minima.shape[0] == 1:
                plt.text(float(self.minima.iloc[0,3]), float(self.minima.iloc[0,1])+fesrange*0.05, self.minima.iloc[0,0],
                             fontsize=label_size, horizontalalignment='center',
                             verticalalignment='bottom', c=color)
            elif self.minima.shape[0] > 1:
                for m in range(len(self.minima.iloc[:,0])):
                    plt.text(float(self.minima.iloc[m,3]), float(self.minima.iloc[m,1])+fesrange*0.05, self.minima.iloc[m,0],
                             fontsize=label_size, horizontalalignment='center',
                             verticalalignment='bottom', c=color)
            
            if xlabel == None:
                plt.xlabel(f'CV1 - {self.cv1_name}', size=label_size)
            else:
                plt.xlabel(xlabel, size=label_size)
            if ylabel == None:
                plt.ylabel(f'free energy ({energy_unit})', size=label_size)
            else:
                plt.ylabel(ylabel, size=label_size)
            
            if title != None:
                plt.title(title, size=label_size)
    
            plt.xticks(fontsize=label_size)
            plt.yticks(fontsize=label_size)

            ax = plt.gca()
            ax.set_xlim(xlim)
            ax.set_ylim(ylim)
        
            if png_name != None:
                plt.savefig(png_name, bbox_inches = 'tight')

            if return_fig:
                return fig
                
            
        elif self.cvs == 2:
            fig = plt.figure(figsize=(image_size[0],image_size[1]), dpi=dpi)
            plt.imshow(np.rot90(self.fes, axes=(0,1)), cmap=cmap, interpolation='nearest', 
                       extent=[cv1min, cv1max, cv2min, cv2max], 
                       aspect = (((cv1max-cv1min)/(cv2max-cv2min))/(aspect)),
                       vmin = vmin, vmax = vmax)
            cbar = plt.colorbar()
            cbar.ax.tick_params(labelsize=label_size) 
            cbar.set_label(energy_unit, size=label_size)

            if contours:
                if levels != None:
                    cont = plt.contour(np.rot90(self.fes, axes=(0,1)), 
                             levels = levels, 
                             extent=[cv1min, cv1max, cv2max, cv2min], 
                             colors = "k", linestyles = "solid")
                    plt.clabel(cont, levels = levels, fontsize=clabel_size)
                else:
                    cont = plt.contour(np.rot90(self.fes, axes=(0,1)), 
                             levels = np.arange(vmin, (vmax - 0.01), contours_spacing), 
                             extent=[cv1min, cv1max, cv2max, cv2min], 
                             colors = "k", linestyles = "solid")
                    plt.clabel(cont, levels = np.arange(vmin, (vmax - 0.01), contours_spacing), fontsize=clabel_size)
            
            if self.minima.shape[0] == 1:
                background = cmap((float(self.minima.iloc[0,1])-vmin)/(vmax-vmin))
                luma = background[0]*0.2126+background[1]*0.7152+background[3]*0.0722
                if luma > 0.6 and not color_set:
                    color = "black"
                elif luma <= 0.6 and not color_set:
                    color="white"
                plt.text(float(self.minima.iloc[0,4])+0.5*(cv1max-cv1min)/self.res, float(self.minima.iloc[0,5])+0.5*(cv2max-cv2min)/self.res, self.minima.iloc[0,0], fontsize=label_size, horizontalalignment='center',verticalalignment='center', c=color)
            elif self.minima.shape[0] > 1:
                for m in range(len(self.minima.iloc[:,0])):
                    background = cmap((float(self.minima.iloc[m,1])-vmin)/(vmax-vmin))
                    luma = background[0]*0.2126+background[1]*0.7152+background[3]*0.0722
                    if luma > 0.6 and not color_set:
                        color = "black"
                    elif luma <= 0.6 and not color_set:
                        color="white"
                    plt.text(float(self.minima.iloc[m,4])+0.5*(cv1max-cv1min)/self.res, float(self.minima.iloc[m,5])+0.5*(cv2max-cv2min)/self.res, self.minima.iloc[m,0], fontsize=label_size, horizontalalignment='center', verticalalignment='center', c=color)
            
            
            if xlabel == None:
                plt.xlabel(f'CV1 - {self.cv1_name}', size=label_size)
            else:
                plt.xlabel(xlabel, size=label_size)
            if ylabel == None:
                plt.ylabel(f'CV2 - {self.cv2_name}', size=label_size)
            else:
                plt.ylabel(ylabel, size=label_size)
            
            if title != None:
                plt.title(title, size=label_size)
    
            plt.xticks(fontsize=label_size)
            plt.yticks(fontsize=label_size)

            ax = plt.gca()
            ax.set_xlim(xlim)
            ax.set_ylim(ylim)

            ax.tick_params(axis='x', labelsize=label_size)
            ax.tick_params(axis='y', labelsize=label_size)
        
            if png_name != None:
                plt.savefig(png_name, bbox_inches = 'tight')

            if return_fig:
                return fig
            
            
        elif self.cvs == 3:
            if xlabel == None:
                xlabel = "CV1 - " + self.cv1_name
            if ylabel == None:
                ylabel = "CV2 - " + self.cv2_name
            if zlabel == None:
                zlabel = "CV3 - " + self.cv3_name
            
            min_ar = self.minima.iloc[:,5:8].values
            min_ar = min_ar.astype(np.float32)
            min_pv = pv.PolyData(min_ar)
            
            
            grid = pv.ImageData(
                dimensions=(self.res, self.res, self.res),
                spacing=((cv1max-cv1min)/self.res,(cv2max-cv2min)/self.res,(cv3max-cv3min)/self.res),
                origin=(cv1min, cv2min, cv3min)
            )
            grid["vol"] = self.fes.ravel(order="F")
            if levels == None:
                contours = grid.contour(np.arange(0, (vmax - 0.1), contours_spacing))
            else:
                contours = grid.contour(levels)
            fescolors = []
            for i in range(contours.points.shape[0]):
                fescolors.append(self.fes[int((contours.points[i,0]-cv1min)*self.res/(cv1max-cv1min)),
                                          int((contours.points[i,1]-cv2min)*self.res/(cv2max-cv2min)),
                                          int((contours.points[i,2]-cv3min)*self.res/(cv3max-cv3min))])
            #%% Visualization
            bounds = [cv1min, cv1max, cv2min, cv2max, cv3min, cv3max]
            pv.set_plot_theme('document')
            p = pv.Plotter()
            p.add_mesh(contours, scalars=fescolors, opacity=opacity, cmap=cmap, show_scalar_bar=False, interpolate_before_map=True)
            p.show_bounds(bounds=bounds, xtitle=xlabel, ytitle=ylabel, ztitle=zlabel, grid=True, location="outer")
            p.add_point_labels(min_pv, self.minima.iloc[:,0], 
                   show_points=show_points, always_visible = True, 
                   point_color="black", point_size=point_size, 
                   font_size=label_size, shape=None)
            
            if title != None:
                plt.title(title)
            if not off_screen:
                p.show()

            if png_name != None:
                p.save_graphic(png_name)

            if return_fig:
                return p
            

    def make_gif(self, gif_name=None, cmap = "RdYlBu_r", energy_unit="kJ/mol", 
                 xlabel=None, ylabel=None, zlabel=None, label_size=12, image_size=[10,7], 
                  opacity=0.2, levels=None, show_points=True, point_size=4.0, frames=64):
        """
        Function that generates animation of 3D FES showing different isosurfaces.
        
        ```python
        fes.make_gif(gif_name="FES.gif")
        ```
        
        Parameters:
        
        * gif_name (default="minima.gif") = String. Name of the gif that will be saved in the working directory.
        
        * cmap (default = "RdYlBu_r") = Matplotlib colormap used to color the 3D FES
        
        * xlabel, ylabel, zlabel = Strings, if provided, they will be used as labels for the graph
        
        * labelsize (default = 12) = size of text in labels
        
        * image_size (default = [10,7]) = List of the width and height of the picture
        
        * opacity (default = 0.2) = number between 0 and 1, is the opacity of isosurfaces of 3D FES
        
        * levels = Here you can specify list of free energy values for isosurfaces on 3D FES. 
                If not provided, default values from contours parameters will be used instead. 
        
        * frames (default = 64) = Number of frames the animation will be made of. 
        """
        if self.cvs == 3:
            values = np.linspace(np.min(self.fes)+0.1, np.max(self.fes), num=frames)
            grid = pv.ImageData(
                dimensions=(self.res, self.res, self.res),
                spacing=((self.cv1max-self.cv1min)/self.res,(self.cv2max-self.cv2min)/self.res,(self.cv3max-self.cv3min)/self.res),
                origin=(self.cv1min, self.cv2min, self.cv3min),
            )
            grid["vol"] = self.fes.ravel(order="F")
            surface = grid.contour(values[:1])
            surfaces = [grid.contour([v]) for v in values]
            surface = surfaces[0].copy()
            
            pv.set_plot_theme('document')
            plotter = pv.Plotter(off_screen=True)
            # Open a movie file
            if gif_name == None:
                gif_name = "minima_animation.gif"
            plotter.open_gif(gif_name)

            # Add initial mesh
            plotter.add_mesh(
                surface,
                opacity=0.3,
                clim=grid.get_data_range(),
                show_scalar_bar=False,
                cmap="RdYlBu_r"
            )
            plotter.add_mesh(grid.outline_corners(), color="k")
            
            if xlabel == None and ylabel == None and zlabel == None:
                plotter.show_grid(xtitle=f"CV1 - {self.cv1_name}", ytitle=f"CV2 - {self.cv2_name}", ztitle=f"CV3 - {self.cv3_name}")
            else:
                plotter.show_grid(xtitle=xlabel, ytitle=ylabel, ztitle=zlabel)
            if show_points:
                min_ar = self.minima.iloc[:,5:8].values
                min_ar = min_ar.astype(np.float32)
                min_pv = pv.PolyData(min_ar)
                plotter.add_point_labels(min_pv, self.minima.iloc[:,0], 
                               show_points=True, always_visible = True, 
                               pickable = True, point_color="black", 
                               point_size=4, font_size=label_size, shape=None)
            plotter.set_background('white')
            text = plotter.add_text(f"{values[0]:.2f}+kJ/mol", position='lower_right', font_size=label_size)
            plotter.show(auto_close=False)
            
            # Run through each frame
            for surf in range(len(surfaces)):
                surface.copy_from(surfaces[surf])
                plotter.remove_actor(text)
                text = plotter.add_text(f"{values[surf]:.2f} {energy_unit}", position='lower_right', font_size=label_size)
                plotter.write_frame()  # Write this frame
            # Run through backwards
            for surf in range(len(surfaces)-1,0,-1):
                surface.copy_from(surfaces[surf])
                plotter.remove_actor(text)
                text = plotter.add_text(f"{values[surf]:.2f} {energy_unit}", position='lower_right', font_size=12)
                plotter.write_frame()  # Write this frame

            # Be sure to close the plotter when finished
            plotter.close()
        else:
            print("Error: this method is only available for FES with 3 CVs.")

Methods

def findminima(self, nbins=8, print_output=True)
Expand source code
    def findminima(self, nbins=8, print_output=True):
        if int(nbins) != nbins:
            nbins = int(nbins)
            print(f"Number of bins must be an integer, it will be set to {nbins}.")
        if self.res%nbins != 0:
            print("Error: Resolution of FES must be divisible by number of bins.")
            return None
        if nbins > self.res/2:
            print("Error: Number of bins is too high.")
            return None
        bin_size = int(self.res/nbins)

        if self.cvs >= 1:
            cv1min = self.cv1min
            cv1max = self.cv1max
        if self.cvs >=2:
            cv2min = self.cv2min
            cv2max = self.cv2max 
        if self.cvs == 3:
            cv3min = self.cv3min
            cv3max = self.cv3max
        
        self.minima = []
        if self.cvs == 1:
            for bin1 in range(0,nbins):
                fes_slice = self.fes[bin1*bin_size:(bin1+1)*bin_size]
                bin_min = np.min(fes_slice)
                argmin = np.argmin(fes_slice)
                # indexes of global minimum of a bin
                bin_min_arg_cv1 = int(argmin%bin_size)
                # indexes of that minima in the original fes (indexes +1)
                min_cv1_b = int(bin_min_arg_cv1+bin1*bin_size)
                if (bin_min_arg_cv1 > 0 and bin_min_arg_cv1<(bin_size-1)):
                    min_cv1 = (((min_cv1_b)/self.res)*(cv1max-cv1min))+cv1min#min_cv1 = (((min_cv1_b+0.5)/self.res)*(cv1max-cv1min))+cv1min
                    if len(self.minima) == 0:
                        self.minima = np.array([round(bin_min, 6), int(min_cv1_b), round(min_cv1, 6)])
                    else:
                        self.minima = np.vstack((self.minima, np.array([round(bin_min, 6), int(min_cv1_b), round(min_cv1, 6)])))
                else:
                    around = []
                    min_cv1_b_low = min_cv1_b - 1
                    if min_cv1_b_low == -1:
                        if self.periodic[0]:
                            min_cv1_b_low = self.res - 1
                        else:
                            min_cv1_b_low = float("nan")

                    min_cv1_b_high = min_cv1_b + 1
                    if min_cv1_b_high == self.res:
                        if self.periodic[0]:
                            min_cv1_b_high = 0
                        else:
                            min_cv1_b_high = float("nan")

                    #1_b_low
                    if not(np.isnan(min_cv1_b_low)):
                        around.append(self.fes[min_cv1_b_low])
                    #1_b_high
                    if not(np.isnan(min_cv1_b_high)):
                        around.append(self.fes[min_cv1_b_high])
                    
                    if bin_min < np.min(around):
                        min_cv1 = (((min_cv1_b)/self.res)*(cv1max-cv1min))+cv1min#min_cv1 = (((min_cv1_b+0.5)/self.res)*(cv1max-cv1min))+cv1min
                        if len(self.minima) == 0:
                            self.minima=np.array([round(bin_min, 6), int(min_cv1_b), round(min_cv1, 6)])
                        else:
                            self.minima=np.vstack((self.minima, np.array([round(bin_min, 6), int(min_cv1_b), round(min_cv1, 6)])))
            
        elif self.cvs == 2:
            for bin1 in range(0,nbins):
                for bin2 in range(0,nbins):
                    fes_slice = self.fes[bin1*bin_size:(bin1+1)*bin_size,
                                         bin2*bin_size:(bin2+1)*bin_size]
                    bin_min = np.min(fes_slice)
                    argmin = np.argmin(fes_slice)
                    # indexes of global minimum of a bin
                    bin_min_arg = np.unravel_index(np.argmin(fes_slice), fes_slice.shape)
                    # indexes of that minima in the original fes (indexes +1)
                    min_cv1_b = int(bin_min_arg[0]+bin1*bin_size)
                    min_cv2_b = int(bin_min_arg[1]+bin2*bin_size)
                    if (bin_min_arg[0] > 0 and bin_min_arg[0]<(bin_size-1)) \
                                    and (bin_min_arg[1] > 0 and bin_min_arg[1]<(bin_size-1)):
                        min_cv1 = (((min_cv1_b)/self.res)*(cv1max-cv1min))+cv1min#min_cv1 = (((min_cv1_b+0.5)/self.res)*(cv1max-cv1min))+cv1min
                        min_cv2 = (((min_cv2_b)/self.res)*(cv2max-cv2min))+cv2min#min_cv1 = (((min_cv1_b+0.5)/self.res)*(cv1max-cv1min))+cv1min
                        if len(self.minima) == 0:
                            self.minima=np.array([round(bin_min, 6), int(min_cv1_b),\
                                                  int(min_cv2_b), round(min_cv1, 6), round(min_cv2, 6)])
                        else:
                            self.minima=np.vstack((self.minima, np.array([round(bin_min, 6), int(min_cv1_b), \
                                                                          int(min_cv2_b), round(min_cv1, 6), round(min_cv2, 6)])))
                    else:
                        around = []
                        min_cv1_b_low = min_cv1_b - 1
                        if min_cv1_b_low == -1:
                            if self.periodic[0]:
                                min_cv1_b_low = self.res - 1
                            else:
                                min_cv1_b_low = float("nan")

                        min_cv1_b_high = min_cv1_b + 1
                        if min_cv1_b_high == self.res:
                            if self.periodic[0]:
                                min_cv1_b_high = 0
                            else:
                                min_cv1_b_high = float("nan")

                        min_cv2_b_low = min_cv2_b - 1
                        if min_cv2_b_low == -1:
                            if self.periodic[0]:
                                min_cv2_b_low = self.res - 1
                            else:
                                min_cv2_b_low = float("nan")

                        min_cv2_b_high = min_cv2_b + 1
                        if min_cv2_b_high == self.res:
                            if self.periodic[0]:
                                min_cv2_b_high = 0
                            else:
                                min_cv2_b_high = float("nan")
                        #1_b_low
                        if not(np.isnan(min_cv1_b_low)):
                            if not(np.isnan(min_cv2_b_low)):
                                around.append(self.fes[min_cv1_b_low, min_cv2_b_low])
                            around.append(self.fes[min_cv1_b_low,min_cv2_b])
                            if not(np.isnan(min_cv2_b_high)):
                                around.append(self.fes[min_cv1_b_low, min_cv2_b_high])
                        #1_b
                        if not(np.isnan(min_cv2_b_low)):
                            around.append(self.fes[min_cv1_b, min_cv2_b_low])
                        if not(np.isnan(min_cv2_b_high)):
                            around.append(self.fes[min_cv1_b, min_cv2_b_high])
                        #1_b_high
                        if not(np.isnan(min_cv1_b_high)):
                            if not(np.isnan(min_cv2_b_low)):
                                around.append(self.fes[min_cv1_b_high, min_cv2_b_low])
                            around.append(self.fes[min_cv1_b_high, min_cv2_b])
                            if not(np.isnan(min_cv2_b_high)):
                                around.append(self.fes[min_cv1_b_high, min_cv2_b_high])
                        if bin_min < np.min(around):
                            min_cv1 = (((min_cv1_b)/self.res)*(cv1max-cv1min))+cv1min#min_cv1 = (((min_cv1_b+0.5)/self.res)*(cv1max-cv1min))+cv1min
                            min_cv2 = (((min_cv2_b)/self.res)*(cv2max-cv2min))+cv2min#min_cv1 = (((min_cv1_b+0.5)/self.res)*(cv1max-cv1min))+cv1min
                            if len(self.minima) == 0:
                                self.minima=np.array([round(bin_min, 6), int(min_cv1_b), int(min_cv2_b), \
                                                      round(min_cv1, 6), round(min_cv2, 6)])
                            else:
                                self.minima=np.vstack((self.minima, np.array([round(bin_min, 6), int(min_cv1_b), \
                                                                              int(min_cv2_b), round(min_cv1, 6), round(min_cv2, 6)])))
        elif self.cvs == 3:
            for bin1 in range(0,nbins):
                for bin2 in range(0,nbins):
                    for bin3 in range(0, nbins):
                        fes_slice = self.fes[bin1*bin_size:(bin1+1)*bin_size,
                                             bin2*bin_size:(bin2+1)*bin_size, 
                                             bin3*bin_size:(bin3+1)*bin_size]
                        bin_min = np.min(fes_slice)
                        argmin = np.argmin(fes_slice)
                        # indexes of global minimum of a bin
                        bin_min_arg = np.unravel_index(np.argmin(fes_slice), fes_slice.shape)
                        # indexes of that minima in the original fes (indexes +1)
                        min_cv1_b = int(bin_min_arg[0]+bin1*bin_size)
                        min_cv2_b = int(bin_min_arg[1]+bin2*bin_size)
                        min_cv3_b = int(bin_min_arg[2]+bin3*bin_size)
                        if (bin_min_arg[0] > 0 and bin_min_arg[0]<(bin_size-1)) \
                                        and (bin_min_arg[1] > 0 and bin_min_arg[1]<(bin_size-1))\
                                        and (bin_min_arg[2] > 0 and bin_min_arg[2]<(bin_size-1)):
                            min_cv1 = (((min_cv1_b)/self.res)*(cv1max-cv1min))+cv1min #min_cv1 = (((min_cv1_b+0.5)/self.res)*(cv1max-cv1min))+cv1min
                            min_cv2 = (((min_cv2_b)/self.res)*(cv2max-cv2min))+cv2min #min_cv2 = (((min_cv2_b+0.5)/self.res)*(cv2max-cv2min))+cv2min
                            min_cv3 = (((min_cv3_b)/self.res)*(cv3max-cv3min))+cv3min #min_cv3 = (((min_cv3_b+0.5)/self.res)*(cv3max-cv3min))+cv3min
                            if len(self.minima) == 0:
                                self.minima=np.array([round(bin_min, 6), int(min_cv1_b),\
                                                      int(min_cv2_b), int(min_cv3_b), round(min_cv1, 6), \
                                                      round(min_cv2, 6), round(min_cv3, 6)])
                            else:
                                self.minima=np.vstack((self.minima, np.array([round(bin_min, 6), int(min_cv1_b),\
                                                      int(min_cv2_b), int(min_cv3_b), round(min_cv1, 6), \
                                                      round(min_cv2, 6), round(min_cv3, 6)])))
                        else:
                            around = []
                            min_cv1_b_low = min_cv1_b - 1
                            if min_cv1_b_low == -1:
                                if self.periodic[0]:
                                    min_cv1_b_low = self.res - 1
                                else:
                                    min_cv1_b_low = float("nan")

                            min_cv1_b_high = min_cv1_b + 1
                            if min_cv1_b_high == self.res:
                                if self.periodic[0]:
                                    min_cv1_b_high = 0
                                else:
                                    min_cv1_b_high = float("nan")

                            min_cv2_b_low = min_cv2_b - 1
                            if min_cv2_b_low == -1:
                                if self.periodic[0]:
                                    min_cv2_b_low = self.res - 1
                                else:
                                    min_cv2_b_low = float("nan")

                            min_cv2_b_high = min_cv2_b + 1
                            if min_cv2_b_high == self.res:
                                if self.periodic[0]:
                                    min_cv2_b_high = 0
                                else:
                                    min_cv2_b_high = float("nan")
                                                       
                            min_cv3_b_low = min_cv3_b - 1
                            if min_cv3_b_low == -1:
                                if self.periodic[2]:
                                    min_cv3_b_low = self.res - 1
                                else:
                                    min_cv3_b_low = float("nan")

                            min_cv3_b_high = min_cv3_b + 1
                            if min_cv3_b_high == self.res:
                                if self.periodic[2]:
                                    min_cv3_b_high = 0
                                else:
                                    min_cv3_b_high = float("nan")

#cv3_b
                            #1_b_low
                            if not(np.isnan(min_cv1_b_low)):
                                if not(np.isnan(min_cv2_b_low)):
                                    around.append(self.fes[min_cv1_b_low,min_cv2_b_low,min_cv3_b])
                                around.append(self.fes[min_cv1_b_low,min_cv2_b,min_cv3_b])
                                if not(np.isnan(min_cv2_b_high)):
                                    around.append(self.fes[min_cv1_b_low,min_cv2_b_high,min_cv3_b])
                            #1_b
                            if not(np.isnan(min_cv2_b_low)):
                                around.append(self.fes[min_cv1_b,min_cv2_b_low,min_cv3_b])
                            if not(np.isnan(min_cv2_b_high)):
                                around.append(self.fes[min_cv1_b,min_cv2_b_high,min_cv3_b])
                            #1_b_high
                            if not(np.isnan(min_cv1_b_high)):
                                if not(np.isnan(min_cv2_b_low)):
                                    around.append(self.fes[min_cv1_b_high,min_cv2_b_low,min_cv3_b])
                                around.append(self.fes[min_cv1_b_high,min_cv2_b,min_cv3_b])
                                if not(np.isnan(min_cv2_b_high)):
                                    around.append(self.fes[min_cv1_b_high,min_cv2_b_high,min_cv3_b])
                           
                            if not(np.isnan(min_cv3_b_low)):
                            #1_b_low
                                if not(np.isnan(min_cv1_b_low)):
                                    if not(np.isnan(min_cv2_b_low)):
                                        around.append(self.fes[min_cv1_b_low,min_cv2_b_low,min_cv3_b_low])
                                    around.append(self.fes[min_cv1_b_low,min_cv2_b,min_cv3_b_low])
                                    if not(np.isnan(min_cv2_b_high)):
                                        around.append(self.fes[min_cv1_b_low,min_cv2_b_high,min_cv3_b_low])
                                #1_b
                                if not(np.isnan(min_cv2_b_low)):
                                    around.append(self.fes[min_cv1_b,min_cv2_b_low,min_cv3_b_low])
                                if not(np.isnan(min_cv2_b_high)):
                                    around.append(self.fes[min_cv1_b,min_cv2_b_high,min_cv3_b_low])
                                #1_b_high
                                if not(np.isnan(min_cv1_b_high)):
                                    if not(np.isnan(min_cv2_b_low)):
                                        around.append(self.fes[min_cv1_b_high,min_cv2_b_low,min_cv3_b_low])
                                    around.append(self.fes[min_cv1_b_high,min_cv2_b,min_cv3_b_low])
                                    if not(np.isnan(min_cv2_b_high)):
                                        around.append(self.fes[min_cv1_b_high,min_cv2_b_high,min_cv3_b_low])
                            
                            if not(np.isnan(min_cv2_b_high)):
                                #1_b_low
                                if not(np.isnan(min_cv1_b_low)):
                                    if not(np.isnan(min_cv2_b_low)):
                                        around.append(self.fes[min_cv1_b_low,min_cv2_b_low,min_cv3_b_high])
                                    around.append(self.fes[min_cv1_b_low,min_cv2_b,min_cv3_b_high])
                                    if not(np.isnan(min_cv2_b_high)):
                                        around.append(self.fes[min_cv1_b_low,min_cv2_b_high,min_cv3_b_high])
                                #1_b
                                if not(np.isnan(min_cv2_b_low)):
                                    around.append(self.fes[min_cv1_b,min_cv2_b_low,min_cv3_b_high])
                                if not(np.isnan(min_cv2_b_high)):
                                    around.append(self.fes[min_cv1_b,min_cv2_b_high,min_cv3_b_high])
                                #1_b_high
                                if not(np.isnan(min_cv1_b_high)):
                                    if not(np.isnan(min_cv2_b_low)):
                                        around.append(self.fes[min_cv1_b_high,min_cv2_b_low,min_cv3_b_high])
                                    around.append(self.fes[min_cv1_b_high,min_cv2_b,min_cv3_b_high])
                                    if not(np.isnan(min_cv2_b_high)):
                                        around.append(self.fes[min_cv1_b_high,min_cv2_b_high,min_cv3_b_high])
                            
                            if bin_min < np.min(around):
                                min_cv1 = (((min_cv1_b)/self.res)*(cv1max-cv1min))+cv1min #min_cv1 = (((min_cv1_b+0.5)/self.res)*(cv1max-cv1min))+cv1min
                                min_cv2 = (((min_cv2_b)/self.res)*(cv2max-cv2min))+cv2min #min_cv2 = (((min_cv2_b+0.5)/self.res)*(cv2max-cv2min))+cv2min
                                min_cv3 = (((min_cv3_b)/self.res)*(cv3max-cv3min))+cv3min #min_cv3 = (((min_cv3_b+0.5)/self.res)*(cv3max-cv3min))+cv3min
                                if len(self.minima) == 0:
                                    self.minima=np.array([round(bin_min, 6), int(min_cv1_b),\
                                                      int(min_cv2_b), int(min_cv3_b), round(min_cv1, 6), \
                                                      round(min_cv2, 6), round(min_cv3, 6)])
                                else:
                                    self.minima=np.vstack((self.minima, np.array([round(bin_min, 6), int(min_cv1_b),\
                                                      int(min_cv2_b), int(min_cv3_b), round(min_cv1, 6), \
                                                      round(min_cv2, 6), round(min_cv3, 6)])))
        
        else:
            print("Fes object has unsupported number of CVs.")
        
        if len(self.minima.shape)>1:
            self.minima = self.minima[self.minima[:, 0].argsort()]

        letters = list(map(chr, range(65, 91)))
        for letter1 in range(65, 91):
            for letter2 in range(65, 91):
                letters.append(f"{chr(letter1)}{chr(letter2)}")
        if len(self.minima.shape)>1:
            if self.minima.shape[1] < len(letters):
                self.minima = np.column_stack((letters[0:self.minima.shape[0]],self.minima))
            else:
                print("Error: Too many minima to assign letters.")
        elif len(self.minima.shape) == 1:
            self.minima = np.append("A", self.minima)
        
        if self.cvs == 1:
            if len(self.minima.shape)>1:
                self.minima = pd.DataFrame(np.array(self.minima), columns = ["Minimum", "free energy", "CV1bin", "CV1 - "+self.cv1_name])
            elif len(self.minima.shape) == 1:
                self.minima = pd.DataFrame([self.minima], columns = ["Minimum", "free energy", "CV1bin", "CV1 - "+self.cv1_name])
                
        elif self.cvs == 2:
            if len(self.minima.shape)>1:
                self.minima = pd.DataFrame(np.array(self.minima), columns = ["Minimum", "free energy", "CV1bin", "CV2bin", 
                                                               "CV1 - "+self.cv1_name, "CV2 - "+self.cv2_name])
            elif len(self.minima.shape) == 1:
                self.minima = pd.DataFrame([self.minima], columns = ["Minimum", "free energy", "CV1bin", "CV2bin", 
                                                               "CV1 - "+self.cv1_name, "CV2 - "+self.cv2_name])
        elif self.cvs == 3:
            if len(self.minima.shape)>1:
                self.minima = pd.DataFrame(np.array(self.minima), columns = ["Minimum", "free energy", "CV1bin", "CV2bin", "CV3bin", 
                                                               "CV1 - "+self.cv1_name, "CV2 - "+self.cv2_name,  "CV3 - "+self.cv3_name])
            elif len(self.minima.shape) == 1:
                self.minima = pd.DataFrame([self.minima], columns = ["Minimum", "free energy", "CV1bin", "CV2bin", "CV3bin", 
                                                               "CV1 - "+self.cv1_name, "CV2 - "+self.cv2_name,  "CV3 - "+self.cv3_name])
def findminima2(self, temp=300.0, energy_unit='kJ/mol', max_iteration=10000, print_output=True)
Expand source code
def findminima2(self, temp=300.0, energy_unit="kJ/mol", max_iteration=10000, print_output=True):
    if self.cvs >= 1:
        cv1min = self.cv1min
        cv1max = self.cv1max
    if self.cvs >=2:
        cv2min = self.cv2min
        cv2max = self.cv2max 
    if self.cvs == 3:
        cv3min = self.cv3min
        cv3max = self.cv3max
    
    self.minima = []

    if print_output:
        print(f"Calculating gradients for FES with {self.fes.flatten().shape[0]} bins... ")
    
    if self.cvs == 1:
        m_fes = np.zeros((self.fes.shape))
        minima_count = 0
        dirs = np.zeros((self.fes.shape[0], 1))
        minima_list = []
        
        periodic=self.periodic
        
        for i in range(self.fes.shape[0]):
            a_indexes = self._get_indexes(i)
    
            okoli = np.zeros((a_indexes[:].shape))
            for ii in range(okoli.shape[0]):
                okoli[ii] = self.fes[int(a_indexes[ii])]
            
            m_i = np.unravel_index(np.argmin(okoli), okoli.shape, order='C')
            
            dirs[i,0] = a_indexes[m_i]
            if self.fes[i] == self.fes[int(a_indexes[m_i])] and not np.all(okoli == np.min(okoli)):
                minima_count += 1
                m_fes[i] = minima_count
                min_cv1 = (((i)/self.res)*(cv1max-cv1min))+cv1min
                minima_list.append([self.fes[i],i, min_cv1])

        if print_output:
            print("Searching for the nearest local minima... ")
        iteration = 0
        while 0.0 in m_fes[:]:
            iteration += 1
            if iteration > max_iteration:
                print("Warning: Maximum number of iterations reached when searching. ")
                break
            for i in range(self.fes.shape[0]):
                m_fes[i] = m_fes[int(dirs[i])]
        if iteration <= max_iteration and print_output:
            print("Done.")
        if 0.0 in m_fes[:]:
            if print_output:
                print("Warning: some of the FES bins were not associated to any local minimum. ")

        self.minima = np.array(minima_list)
        self.m_fes = m_fes
        
        if energy_unit == "kJ/mol":
            prob = np.exp(-1000*self.fes/8.314/temp)
        elif energy_unit == "kcal_mol":
            prob = np.exp(-1000*4.184*self.fes/8.314/temp)

        m_sum_probabilities = np.zeros((self.minima[:,0].shape))
        for i in range(self.fes.shape[0]):
            if m_fes[i] > 0.0:
                m_sum_probabilities[int(m_fes[i])-1] += prob[i]

        if energy_unit == "kJ/mol":
            m_energies = -8.314*temp*np.log(m_sum_probabilities)/1000
        elif energy_unit == "kcal_mol":
            m_energies = -8.314*temp*np.log(m_sum_probabilities)/1000/4.184

        m_energies = m_energies - np.min(m_energies)
        self.minima[:,0] = m_energies
        
    elif self.cvs == 2:
        m_fes = np.zeros((self.fes.shape))
        minima_count = 0
        dirs = np.zeros((self.fes.shape[0], self.fes.shape[1], 2))
        minima_list = []
        
        periodic=self.periodic
        
        for i in range(self.fes.shape[0]):
            for j in range(self.fes.shape[1]):
                a_indexes = self._get_indexes(i,j)
        
                okoli = np.zeros((a_indexes[:,:,0].shape))
                for ii in range(okoli.shape[0]):
                    for jj in range(okoli.shape[1]):
                        okoli[ii,jj] = self.fes[int(a_indexes[ii,jj,0]),int(a_indexes[ii,jj,1])]
                
                m_i, m_j = np.unravel_index(np.argmin(okoli), okoli.shape, order='C')

                if not np.all(okoli == np.min(okoli)):
                    dirs[i,j, 0] = a_indexes[m_i,m_j,0]
                    dirs[i,j, 1] = a_indexes[m_i,m_j,1]
                    if self.fes[i,j] == self.fes[int(a_indexes[m_i,m_j,0]), int(a_indexes[m_i, m_j, 1])]:
                        minima_count += 1
                        m_fes[i,j] = minima_count
                        min_cv1 = (((i)/self.res)*(cv1max-cv1min))+cv1min
                        min_cv2 = (((j)/self.res)*(cv2max-cv2min))+cv2min
                        minima_list.append([self.fes[i,j],i,j, min_cv1, min_cv2])
                else:
                    dirs[i,j, 0] = i
                    dirs[i,j, 1] = j

        if print_output:
            print("Searching for the nearest local minima... ")
        iteration = 0
        while 0.0 in m_fes[:,:]:
            iteration += 1
            if iteration > max_iteration:
                if print_output:
                    print("Warning: Maximum number of iterations reached when searching. ")
                break
            for i in range(self.fes.shape[0]):
                for j in range(self.fes.shape[1]):
                    m_fes[i,j] = m_fes[int(dirs[i,j,0]), int(dirs[i,j,1])]
        if iteration <= max_iteration:
            if print_output:
                print("Done.")  
        if 0.0 in m_fes[:,:]:
            if print_output:
                print("Warning: some of the FES bins were not associated to any local minimum. ")
        
        self.minima = np.array(minima_list)
        self.m_fes = m_fes
        
        if energy_unit == "kJ/mol":
            prob = np.exp(-1000*self.fes/8.314/temp)
        elif energy_unit == "kcal_mol":
            prob = np.exp(-1000*4.184*self.fes/8.314/temp)

        m_sum_probabilities = np.zeros((self.minima[:,0].shape))
        for i in range(self.fes.shape[0]):
            for j in range(self.fes.shape[1]):
                if m_fes[i,j] > 0.0:
                    m_sum_probabilities[int(m_fes[i,j])-1] += prob[i,j]

        if energy_unit == "kJ/mol":
            m_energies = -8.314*temp*np.log(m_sum_probabilities)/1000
        elif energy_unit == "kcal_mol":
            m_energies = -8.314*temp*np.log(m_sum_probabilities)/1000/4.184

        m_energies = m_energies - np.min(m_energies)
        self.minima[:,0] = m_energies
        
    elif self.cvs == 3:
        m_fes = np.zeros((self.fes.shape))
        minima_count = 0
        dirs = np.zeros((self.fes.shape[0], self.fes.shape[1], self.fes.shape[2],3))
        minima_list = []
        
        periodic=self.periodic
        
        for i in range(self.fes.shape[0]):
            for j in range(self.fes.shape[1]):
                for k in range(self.fes.shape[2]):
                    a_indexes = self._get_indexes(i,j,k)
                    
                    okoli = np.zeros((a_indexes[:,:,:,0].shape))
                    for ii in range(okoli.shape[0]):
                        for jj in range(okoli.shape[1]):
                            for kk in range(okoli.shape[2]):
                                okoli[ii,jj, kk] = self.fes[int(a_indexes[ii,jj,kk,0]),int(a_indexes[ii,jj,kk,1]),int(a_indexes[ii,jj,kk,2])]
                    
                    m_i, m_j, m_k = np.unravel_index(np.argmin(okoli), okoli.shape, order='C')
                    
                    dirs[i,j,k,0] = a_indexes[m_i,m_j,m_k,0]
                    dirs[i,j,k,1] = a_indexes[m_i,m_j,m_k,1]
                    dirs[i,j,k,2] = a_indexes[m_i,m_j,m_k,2]
                    if self.fes[i,j,k] == self.fes[int(a_indexes[m_i,m_j,m_k,0]), int(a_indexes[m_i, m_j,m_k,1]), int(a_indexes[m_i, m_j,m_k,2])] and not np.all(okoli == np.min(okoli)):
                        minima_count += 1
                        m_fes[i,j,k] = minima_count
                        min_cv1 = (((i)/self.res)*(cv1max-cv1min))+cv1min
                        min_cv2 = (((j)/self.res)*(cv2max-cv2min))+cv2min
                        min_cv3 = (((k)/self.res)*(cv3max-cv3min))+cv3min
                        minima_list.append([self.fes[i,j,k],i,j,k,min_cv1,min_cv2,min_cv3])

        if print_output:
            print("Searching for the nearest local minima... ")
        iteration = 0
        while 0.0 in m_fes[:,:,:]:
            iteration += 1
            if iteration > max_iteration:
                print("Warning: Maximum number of iterations reached when searching. ")
                break
            for i in range(self.fes.shape[0]):
                for j in range(self.fes.shape[1]):
                    for k in range(self.fes.shape[2]):
                        m_fes[i,j,k] = m_fes[int(dirs[i,j,k,0]), int(dirs[i,j,k,1]),int(dirs[i,j,k,2])]
        if iteration <= max_iteration:
            if print_output:
                print("Done.")
        if 0.0 in m_fes[:,:,:]:
            if print_output:
                print("Warning: some of the FES bins were not associated to any local minimum. ")

        self.minima = np.array(minima_list)
        self.m_fes = m_fes
        
        if energy_unit == "kJ/mol":
            prob = np.exp(-1000*self.fes/8.314/temp)
        elif energy_unit == "kcal_mol":
            prob = np.exp(-1000*4.184*self.fes/8.314/temp)

        m_sum_probabilities = np.zeros((self.minima[:,0].shape))
        for i in range(self.fes.shape[0]):
            for j in range(self.fes.shape[1]):
                for k in range(self.fes.shape[2]):
                    if m_fes[i,j,k] > 0.0:
                        m_sum_probabilities[int(m_fes[i,j,k])-1] += prob[i,j,k]

        if energy_unit == "kJ/mol":
            m_energies = -8.314*temp*np.log(m_sum_probabilities)/1000
        elif energy_unit == "kcal_mol":
            m_energies = -8.314*temp*np.log(m_sum_probabilities)/1000/4.184

        m_energies = m_energies - np.min(m_energies)
        self.minima[:,0] = m_energies
    else:
        print("Fes object has unsupported number of CVs.")

    
    if len(self.minima.shape)>1:
        self.minima = self.minima[self.minima[:, 0].argsort()]

        # renumber minima indexes in m_fes according to their energies (and letters)
        new_m_fes = np.zeros((self.fes.shape))
        
        for m in range(self.minima.shape[0]):
            if self.cvs == 1:
                m_old_id = self.m_fes[int(float(self.minima[m,1]))]
            if self.cvs == 2:
                m_old_id = self.m_fes[int(float(self.minima[m,1])), int(float(self.minima[m,2]))]
            if self.cvs == 3:
                m_old_id = self.m_fes[int(float(self.minima[m,1])), int(float(self.minima[m,2])), int(float(self.minima[m,3]))]
            new_m_fes[self.m_fes == m_old_id] = m + 1
        self.m_fes = new_m_fes

    
    letters = list(map(chr, range(65, 91)))
    for letter1 in range(65, 91):
        for letter2 in range(65, 91):
            letters.append(f"{chr(letter1)}{chr(letter2)}")
    if len(self.minima.shape) > 1:
        if self.minima.shape[0] < len(letters):
            self.minima = np.column_stack((letters[0:self.minima.shape[0]],self.minima))
        else:
            print("Error: Too many minima to assign letters. Try using keyword precise=False. ")
            return None
    elif len(self.minima.shape) == 1:
        self.minima = np.append("A", self.minima)
    
    if self.cvs == 1:
        if len(self.minima.shape)>1:
            self.minima = pd.DataFrame(np.array(self.minima), columns = ["Minimum", "free energy", "CV1bin", "CV1 - "+self.cv1_name])
        elif len(self.minima.shape) == 1:
            self.minima = pd.DataFrame([self.minima], columns = ["Minimum", "free energy", "CV1bin", "CV1 - "+self.cv1_name])
            
    elif self.cvs == 2:
        if len(self.minima.shape)>1:
            self.minima = pd.DataFrame(np.array(self.minima), columns = ["Minimum", "free energy", "CV1bin", "CV2bin", 
                                                           "CV1 - "+self.cv1_name, "CV2 - "+self.cv2_name])
        elif len(self.minima.shape) == 1:
            self.minima = pd.DataFrame([self.minima], columns = ["Minimum", "free energy", "CV1bin", "CV2bin", 
                                                           "CV1 - "+self.cv1_name, "CV2 - "+self.cv2_name])
    elif self.cvs == 3:
        if len(self.minima.shape)>1:
            self.minima = pd.DataFrame(np.array(self.minima), columns = ["Minimum", "free energy", "CV1bin", "CV2bin", "CV3bin", 
                                                           "CV1 - "+self.cv1_name, "CV2 - "+self.cv2_name,  "CV3 - "+self.cv3_name])
        elif len(self.minima.shape) == 1:
            self.minima = pd.DataFrame([self.minima], columns = ["Minimum", "free energy", "CV1bin", "CV2bin", "CV3bin", 
                                                           "CV1 - "+self.cv1_name, "CV2 - "+self.cv2_name,  "CV3 - "+self.cv3_name])
def make_gif(self, gif_name=None, cmap='RdYlBu_r', energy_unit='kJ/mol', xlabel=None, ylabel=None, zlabel=None, label_size=12, image_size=[10, 7], opacity=0.2, levels=None, show_points=True, point_size=4.0, frames=64)

Function that generates animation of 3D FES showing different isosurfaces.

fes.make_gif(gif_name="FES.gif")

Parameters:

  • gif_name (default="minima.gif") = String. Name of the gif that will be saved in the working directory.

  • cmap (default = "RdYlBu_r") = Matplotlib colormap used to color the 3D FES

  • xlabel, ylabel, zlabel = Strings, if provided, they will be used as labels for the graph

  • labelsize (default = 12) = size of text in labels

  • image_size (default = [10,7]) = List of the width and height of the picture

  • opacity (default = 0.2) = number between 0 and 1, is the opacity of isosurfaces of 3D FES

  • levels = Here you can specify list of free energy values for isosurfaces on 3D FES. If not provided, default values from contours parameters will be used instead.

  • frames (default = 64) = Number of frames the animation will be made of.

Expand source code
def make_gif(self, gif_name=None, cmap = "RdYlBu_r", energy_unit="kJ/mol", 
             xlabel=None, ylabel=None, zlabel=None, label_size=12, image_size=[10,7], 
              opacity=0.2, levels=None, show_points=True, point_size=4.0, frames=64):
    """
    Function that generates animation of 3D FES showing different isosurfaces.
    
    ```python
    fes.make_gif(gif_name="FES.gif")
    ```
    
    Parameters:
    
    * gif_name (default="minima.gif") = String. Name of the gif that will be saved in the working directory.
    
    * cmap (default = "RdYlBu_r") = Matplotlib colormap used to color the 3D FES
    
    * xlabel, ylabel, zlabel = Strings, if provided, they will be used as labels for the graph
    
    * labelsize (default = 12) = size of text in labels
    
    * image_size (default = [10,7]) = List of the width and height of the picture
    
    * opacity (default = 0.2) = number between 0 and 1, is the opacity of isosurfaces of 3D FES
    
    * levels = Here you can specify list of free energy values for isosurfaces on 3D FES. 
            If not provided, default values from contours parameters will be used instead. 
    
    * frames (default = 64) = Number of frames the animation will be made of. 
    """
    if self.cvs == 3:
        values = np.linspace(np.min(self.fes)+0.1, np.max(self.fes), num=frames)
        grid = pv.ImageData(
            dimensions=(self.res, self.res, self.res),
            spacing=((self.cv1max-self.cv1min)/self.res,(self.cv2max-self.cv2min)/self.res,(self.cv3max-self.cv3min)/self.res),
            origin=(self.cv1min, self.cv2min, self.cv3min),
        )
        grid["vol"] = self.fes.ravel(order="F")
        surface = grid.contour(values[:1])
        surfaces = [grid.contour([v]) for v in values]
        surface = surfaces[0].copy()
        
        pv.set_plot_theme('document')
        plotter = pv.Plotter(off_screen=True)
        # Open a movie file
        if gif_name == None:
            gif_name = "minima_animation.gif"
        plotter.open_gif(gif_name)

        # Add initial mesh
        plotter.add_mesh(
            surface,
            opacity=0.3,
            clim=grid.get_data_range(),
            show_scalar_bar=False,
            cmap="RdYlBu_r"
        )
        plotter.add_mesh(grid.outline_corners(), color="k")
        
        if xlabel == None and ylabel == None and zlabel == None:
            plotter.show_grid(xtitle=f"CV1 - {self.cv1_name}", ytitle=f"CV2 - {self.cv2_name}", ztitle=f"CV3 - {self.cv3_name}")
        else:
            plotter.show_grid(xtitle=xlabel, ytitle=ylabel, ztitle=zlabel)
        if show_points:
            min_ar = self.minima.iloc[:,5:8].values
            min_ar = min_ar.astype(np.float32)
            min_pv = pv.PolyData(min_ar)
            plotter.add_point_labels(min_pv, self.minima.iloc[:,0], 
                           show_points=True, always_visible = True, 
                           pickable = True, point_color="black", 
                           point_size=4, font_size=label_size, shape=None)
        plotter.set_background('white')
        text = plotter.add_text(f"{values[0]:.2f}+kJ/mol", position='lower_right', font_size=label_size)
        plotter.show(auto_close=False)
        
        # Run through each frame
        for surf in range(len(surfaces)):
            surface.copy_from(surfaces[surf])
            plotter.remove_actor(text)
            text = plotter.add_text(f"{values[surf]:.2f} {energy_unit}", position='lower_right', font_size=label_size)
            plotter.write_frame()  # Write this frame
        # Run through backwards
        for surf in range(len(surfaces)-1,0,-1):
            surface.copy_from(surfaces[surf])
            plotter.remove_actor(text)
            text = plotter.add_text(f"{values[surf]:.2f} {energy_unit}", position='lower_right', font_size=12)
            plotter.write_frame()  # Write this frame

        # Be sure to close the plotter when finished
        plotter.close()
    else:
        print("Error: this method is only available for FES with 3 CVs.")
def plot(self, png_name=None, contours=True, contours_spacing=0.0, aspect=1.0, cmap='RdYlBu_r', energy_unit='kJ/mol', xlabel=None, ylabel=None, zlabel=None, label_size=12, clabel_size=12, image_size=None, image_size_unit='in', dpi=100, color=None, vmin=0, vmax=None, opacity=0.2, levels=None, show_points=True, point_size=4.0, title=None, off_screen=False, xlim=[None, None], ylim=[None, None], return_fig=False)

The same function as for visualizing Fes objects, but this time with the positions of local minima shown as letters on the graph.

minima.plot(png_name="minima.png")

Parameters:

  • png_name = String. If this parameter is supplied, the picture of FES will be saved under this name to the current working directory.

  • contours (default=True) = whether contours should be shown on 2D FES

  • contours_spacing (default=0.0) = when a positive number is set, it will be used as spacing for contours on 2D FES. Otherwise, if contours=True, there will be five equally spaced contour levels.

  • aspect (default = 1.0) = aspect ratio of the graph. Works with 1D and 2D FES.

  • cmap (default = "RdYlBu_r") = Matplotlib colormap used to color 2D or 3D FES

  • energy_unit (default="kJ/mol") = String, used in description of colorbar

  • xlim, ylim (default = [None, None] for both) = list of two values specifying the range of x and y axes respectively. None means that Matplotlib will choose appropriate range, so this keyword is useful when you need to overwrite it.

  • xlabel, ylabel, zlabel = Strings, if provided, they will be used as labels for the graphs

  • label_size, clabel_size (default = 12) = size of text in labels or contours labels, respectively

  • image_size (default = [9,6]) = List of the width and height of the picture

  • image_size_unit (default = "in") = Units for width and height of the picture, accepts "in" as inches, "cm", "mm" or "px" as pixels.

  • dpi (default = 100) = DPI of the resulting image.

  • color = string = name of color in matplotlib, if set, the color will be used for the letters. If not set, the color should be automatically either black or white, depending on what will be better visible on given place on FES with given colormap (for 2D FES).

  • vmin (default=0) = real number, lower bound for the colormap on 2D FES

  • vmax = real number, upper bound for the colormap on 2D FES

  • opacity (default=0.2) = number between 0 and 1, is the opacity of isosurfaces of 3D FES

  • levels = Here you can specify list of free energy values for isosurfaces on 3D FES. If not provided, default values from contours parameters will be used instead.

  • show_points (default=True) = boolean, tells if points should be visualized too, instead of just the letters. Only on 3D FES.

  • point_size (default=4.0) = float, sets the size of points if show_points=True

  • title = optional, string that defines the title of the graph

  • offscreen (default = False) = for 3D FES only, grapf will not be shown after creation - used internally when making animations

  • return_fig (default=False) = whether the method should return the Matplotlib.Pyplot.figure object for further use. In the case of plotting 3D FES, it returns Pyvista.plotter object instead.

Expand source code
def plot(self, png_name=None, contours=True, contours_spacing=0.0, aspect = 1.0, cmap = "RdYlBu_r", 
             energy_unit="kJ/mol", xlabel=None, ylabel=None, zlabel=None, label_size=12, clabel_size = 12, image_size=None, image_size_unit="in", dpi=100, color=None, vmin = 0, vmax = None, opacity=0.2, levels=None, show_points=True, point_size=4.0, title = None, off_screen = False, xlim=[None, None], ylim=[None, None], return_fig=False):
    """
    The same function as for visualizing Fes objects, but this time 
    with the positions of local minima shown as letters on the graph.
    
    ```python
    minima.plot(png_name="minima.png")
    ```
    
    Parameters:
    
    * png_name = String. If this parameter is supplied, the picture of FES will be saved under this name to the current working directory.
    
    * contours (default=True) = whether contours should be shown on 2D FES
    
    * contours_spacing (default=0.0) = when a positive number is set, it will be used as spacing for contours on 2D FES. 
            Otherwise, if contours=True, there will be five equally spaced contour levels.
    
    * aspect (default = 1.0) = aspect ratio of the graph. Works with 1D and 2D FES. 
    
    * cmap (default = "RdYlBu_r") = Matplotlib colormap used to color 2D or 3D FES
    
    * energy_unit (default="kJ/mol") = String, used in description of colorbar

    * xlim, ylim (default = [None, None] for both) = list of two values specifying the range of x and y axes respectively. 
    None means that Matplotlib will choose appropriate range, so this keyword is useful when you need to overwrite it. 
    
    * xlabel, ylabel, zlabel = Strings, if provided, they will be used as labels for the graphs
    
    * label_size, clabel_size (default = 12) = size of text in labels or contours labels, respectively
    
    * image_size (default = [9,6]) = List of the width and height of the picture

    * image_size_unit (default = "in") = Units for width and height of the picture, accepts "in" as inches, "cm", "mm" or "px" as pixels.

    * dpi (default = 100) = DPI of the resulting image. 
    
    * color = string = name of color in matplotlib, if set, the color will be used for the letters. 
            If not set, the color should be automatically either black or white, 
            depending on what will be better visible on given place on FES with given colormap (for 2D FES).
    
    * vmin (default=0) = real number, lower bound for the colormap on 2D FES
    
    * vmax = real number, upper bound for the colormap on 2D FES
    
    * opacity (default=0.2) = number between 0 and 1, is the opacity of isosurfaces of 3D FES
    
    * levels = Here you can specify list of free energy values for isosurfaces on 3D FES. 
            If not provided, default values from contours parameters will be used instead. 
    
    * show_points (default=True) = boolean, tells if points should be visualized too, instead of just the letters. Only on 3D FES. 
    
    * point_size (default=4.0) = float, sets the size of points if show_points=True

    * title = optional, string that defines the title of the graph

    * offscreen (default = False) = for 3D FES only, grapf will not be shown after creation - used internally when making animations

    * return_fig (default=False) = whether the method should return the Matplotlib.Pyplot.figure object for further use. 
    In the case of plotting 3D FES, it returns Pyvista.plotter object instead. 
    """
    
    if vmax == None:
        vmax = np.max(self.fes)+0.01 # if the addition is smaller than 0.01, the 3d plot stops working. 
        
    if contours_spacing == 0.0:
        contours_spacing = (vmax-vmin)/5.0
    
    cmap = cm.get_cmap(cmap)
    
    cmap.set_over("white")
    cmap.set_under("white")
    
    color_set = True
    if color == None:
        color_set = False
    
    if self.cvs >= 1:
        cv1min = self.cv1min
        cv1max = self.cv1max 
    if self.cvs >=2:
        cv2min = self.cv2min
        cv2max = self.cv2max
    if self.cvs == 3:
        cv3min = self.cv3min
        cv3max = self.cv3max

    if image_size == None:
        image_size = [9,6]
    
    if image_size_unit == "cm":
        image_size[0] /= 2.54
        image_size[1] /= 2.54
    elif image_size_unit == "mm":
        image_size[0] /= 25.4
        image_size[1] /= 25.4
    elif image_size_unit == "px":
        image_size[0] /= dpi
        image_size[1] /= dpi
    elif image_size_unit != "in":
        print(f"Warning: unknown image_size_unit value: {image_size_unit}. Using inches instead. ")
        
    if self.cvs == 1:
        fig = plt.figure(figsize=(image_size[0],image_size[1]), dpi=dpi)
        X = np.linspace(cv1min, cv1max, self.res)
        plt.plot(X, self.fes)
        
        if not color_set:
            color = "black"
        
        fesrange = np.max(self.fes) - np.min(self.fes)
        
        if self.minima.shape[0] == 1:
            plt.text(float(self.minima.iloc[0,3]), float(self.minima.iloc[0,1])+fesrange*0.05, self.minima.iloc[0,0],
                         fontsize=label_size, horizontalalignment='center',
                         verticalalignment='bottom', c=color)
        elif self.minima.shape[0] > 1:
            for m in range(len(self.minima.iloc[:,0])):
                plt.text(float(self.minima.iloc[m,3]), float(self.minima.iloc[m,1])+fesrange*0.05, self.minima.iloc[m,0],
                         fontsize=label_size, horizontalalignment='center',
                         verticalalignment='bottom', c=color)
        
        if xlabel == None:
            plt.xlabel(f'CV1 - {self.cv1_name}', size=label_size)
        else:
            plt.xlabel(xlabel, size=label_size)
        if ylabel == None:
            plt.ylabel(f'free energy ({energy_unit})', size=label_size)
        else:
            plt.ylabel(ylabel, size=label_size)
        
        if title != None:
            plt.title(title, size=label_size)

        plt.xticks(fontsize=label_size)
        plt.yticks(fontsize=label_size)

        ax = plt.gca()
        ax.set_xlim(xlim)
        ax.set_ylim(ylim)
    
        if png_name != None:
            plt.savefig(png_name, bbox_inches = 'tight')

        if return_fig:
            return fig
            
        
    elif self.cvs == 2:
        fig = plt.figure(figsize=(image_size[0],image_size[1]), dpi=dpi)
        plt.imshow(np.rot90(self.fes, axes=(0,1)), cmap=cmap, interpolation='nearest', 
                   extent=[cv1min, cv1max, cv2min, cv2max], 
                   aspect = (((cv1max-cv1min)/(cv2max-cv2min))/(aspect)),
                   vmin = vmin, vmax = vmax)
        cbar = plt.colorbar()
        cbar.ax.tick_params(labelsize=label_size) 
        cbar.set_label(energy_unit, size=label_size)

        if contours:
            if levels != None:
                cont = plt.contour(np.rot90(self.fes, axes=(0,1)), 
                         levels = levels, 
                         extent=[cv1min, cv1max, cv2max, cv2min], 
                         colors = "k", linestyles = "solid")
                plt.clabel(cont, levels = levels, fontsize=clabel_size)
            else:
                cont = plt.contour(np.rot90(self.fes, axes=(0,1)), 
                         levels = np.arange(vmin, (vmax - 0.01), contours_spacing), 
                         extent=[cv1min, cv1max, cv2max, cv2min], 
                         colors = "k", linestyles = "solid")
                plt.clabel(cont, levels = np.arange(vmin, (vmax - 0.01), contours_spacing), fontsize=clabel_size)
        
        if self.minima.shape[0] == 1:
            background = cmap((float(self.minima.iloc[0,1])-vmin)/(vmax-vmin))
            luma = background[0]*0.2126+background[1]*0.7152+background[3]*0.0722
            if luma > 0.6 and not color_set:
                color = "black"
            elif luma <= 0.6 and not color_set:
                color="white"
            plt.text(float(self.minima.iloc[0,4])+0.5*(cv1max-cv1min)/self.res, float(self.minima.iloc[0,5])+0.5*(cv2max-cv2min)/self.res, self.minima.iloc[0,0], fontsize=label_size, horizontalalignment='center',verticalalignment='center', c=color)
        elif self.minima.shape[0] > 1:
            for m in range(len(self.minima.iloc[:,0])):
                background = cmap((float(self.minima.iloc[m,1])-vmin)/(vmax-vmin))
                luma = background[0]*0.2126+background[1]*0.7152+background[3]*0.0722
                if luma > 0.6 and not color_set:
                    color = "black"
                elif luma <= 0.6 and not color_set:
                    color="white"
                plt.text(float(self.minima.iloc[m,4])+0.5*(cv1max-cv1min)/self.res, float(self.minima.iloc[m,5])+0.5*(cv2max-cv2min)/self.res, self.minima.iloc[m,0], fontsize=label_size, horizontalalignment='center', verticalalignment='center', c=color)
        
        
        if xlabel == None:
            plt.xlabel(f'CV1 - {self.cv1_name}', size=label_size)
        else:
            plt.xlabel(xlabel, size=label_size)
        if ylabel == None:
            plt.ylabel(f'CV2 - {self.cv2_name}', size=label_size)
        else:
            plt.ylabel(ylabel, size=label_size)
        
        if title != None:
            plt.title(title, size=label_size)

        plt.xticks(fontsize=label_size)
        plt.yticks(fontsize=label_size)

        ax = plt.gca()
        ax.set_xlim(xlim)
        ax.set_ylim(ylim)

        ax.tick_params(axis='x', labelsize=label_size)
        ax.tick_params(axis='y', labelsize=label_size)
    
        if png_name != None:
            plt.savefig(png_name, bbox_inches = 'tight')

        if return_fig:
            return fig
        
        
    elif self.cvs == 3:
        if xlabel == None:
            xlabel = "CV1 - " + self.cv1_name
        if ylabel == None:
            ylabel = "CV2 - " + self.cv2_name
        if zlabel == None:
            zlabel = "CV3 - " + self.cv3_name
        
        min_ar = self.minima.iloc[:,5:8].values
        min_ar = min_ar.astype(np.float32)
        min_pv = pv.PolyData(min_ar)
        
        
        grid = pv.ImageData(
            dimensions=(self.res, self.res, self.res),
            spacing=((cv1max-cv1min)/self.res,(cv2max-cv2min)/self.res,(cv3max-cv3min)/self.res),
            origin=(cv1min, cv2min, cv3min)
        )
        grid["vol"] = self.fes.ravel(order="F")
        if levels == None:
            contours = grid.contour(np.arange(0, (vmax - 0.1), contours_spacing))
        else:
            contours = grid.contour(levels)
        fescolors = []
        for i in range(contours.points.shape[0]):
            fescolors.append(self.fes[int((contours.points[i,0]-cv1min)*self.res/(cv1max-cv1min)),
                                      int((contours.points[i,1]-cv2min)*self.res/(cv2max-cv2min)),
                                      int((contours.points[i,2]-cv3min)*self.res/(cv3max-cv3min))])
        #%% Visualization
        bounds = [cv1min, cv1max, cv2min, cv2max, cv3min, cv3max]
        pv.set_plot_theme('document')
        p = pv.Plotter()
        p.add_mesh(contours, scalars=fescolors, opacity=opacity, cmap=cmap, show_scalar_bar=False, interpolate_before_map=True)
        p.show_bounds(bounds=bounds, xtitle=xlabel, ytitle=ylabel, ztitle=zlabel, grid=True, location="outer")
        p.add_point_labels(min_pv, self.minima.iloc[:,0], 
               show_points=show_points, always_visible = True, 
               point_color="black", point_size=point_size, 
               font_size=label_size, shape=None)
        
        if title != None:
            plt.title(title)
        if not off_screen:
            p.show()

        if png_name != None:
            p.save_graphic(png_name)

        if return_fig:
            return p