├── .gitignore ├── LICENSE ├── README.md ├── example.png ├── example.py └── neb ├── __init__.py ├── angle.py ├── atom.py ├── bond.py ├── interpolate ├── __init__.py ├── linear.py ├── path.py └── restart.py ├── methods ├── __init__.py ├── leps.py └── orca.py ├── minimizers ├── __init__.py └── steepestdescent.py ├── molecule.py ├── neb.py └── util.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Casper Steinmann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NEB 2 | 3 | ## Background 4 | This is a very basic python implementation of Nudged-Elastic Band in python based on the paper: [*"Improved tangent estimate in the nudged elastic band method for finding minimum energy paths and saddle points"*](http://dx.doi.org/10.1063/1.1323224) by Henkelmann et al. 5 | 6 | ## Usage 7 | Usage should be quite straightforward with the additional python classes included but please check the `example.py` script in the root directory for a use-case of the [3-atom LEPS potential](http://theory.cm.utexas.edu/henkelman/pubs/jonsson98_385.pdf). 8 | We also provide an example on how to use ORCA but it is very much provided *as is*. 9 | 10 | ![Example of NEB with ](example.png) 11 | 12 | Here, the two bold crosses are initial positions of two molecules. 13 | They are minimized to a threshold shown as black squares. 14 | A linear interpolated bath between the two (black circles) is then constructed and minimized using NEB (black cross with solid line). 15 | 16 | ### Philosophy behind this implementation 17 | The basic design philosophy for this implementation is that one constructs a path (currently limited to a linear interpolation between two molecules) and then the nudge elastic band operates on that path. 18 | -------------------------------------------------------------------------------- /example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cstein/neb/8bf59ef16819841d0839d6875c5935a681e0c405/example.png -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | """ Implementation of the 3-atom LEPS potential 2 | 3 | URL: http://theory.cm.utexas.edu/henkelman/pubs/jonsson98_385.pdf 4 | """ 5 | 6 | import numpy 7 | import matplotlib 8 | import matplotlib.pyplot as plt 9 | 10 | import neb 11 | from neb.util import idamax 12 | from neb.minimizers import SteepestDescent 13 | from neb.methods import LEPSEnergyAndGradient 14 | from neb.interpolate import Linear 15 | 16 | def minimize(_mol, nsteps, opttol, func, minimizer): 17 | """ Minimizes a single molecule 18 | 19 | Arguments: 20 | nstesp -- perform a maximum of nsteps steps 21 | opttol -- the maximum rms gradient shall be below this value 22 | func -- energy and gradient function 23 | minimizer -- a minimizer 24 | """ 25 | 26 | for k in range(nsteps): 27 | cc = _mol.getCoordinates() 28 | rab, rbc, v, g = func(_mol) 29 | _mol.setCoordinates(cc + minimizer.step(v, -g)) 30 | 31 | gval = numpy.ravel(g) 32 | i = idamax(gval) 33 | gmax = gval[i] 34 | grms = numpy.sqrt(gval.dot(gval)/9) 35 | 36 | s = "Step = {0:04d} rAB = {1:6.2f} rBC = {2:6.2f} E = {3:9.4f} Gmax = {4:9.4f} Grms = {5:9.4f}".format(k, rab, rbc, v, gmax, grms) 37 | if k % 100==0: 38 | print s 39 | 40 | if grms < opttol: 41 | break 42 | 43 | if gmax/3 >= grms: 44 | break 45 | 46 | print "------ OPTIMIZATION CONVERGED ------" 47 | print s 48 | print 49 | print 50 | return _mol 51 | 52 | if __name__ == '__main__': 53 | matplotlib.rcParams['contour.negative_linestyle'] = 'solid' 54 | 55 | VLeps = neb.methods.leps.VLeps 56 | # The following example code 57 | # 1) plots the potential energy surface 58 | # 2) creates two molecules on the LEPS potential energy surface (crosses) and minimizes them (squares) 59 | # 3) creates a linear interpolated path between them (squares connected by black line) 60 | # 4) minimizes that path using NEB 61 | # 62 | # NOTE: The code is a mess because we do plotting 63 | # at the same time as simulation which 64 | # should be considered an abhorrent thing 65 | # to do. Please do not take this code as 66 | # any form of embrace of good coding 67 | # practice. 68 | # 69 | 70 | # --------------------------------- 71 | # plot the potential energy surface 72 | nx = 60 73 | ny = 60 74 | x = numpy.linspace(0.4,4.0,nx) 75 | y = numpy.linspace(0.4,4.0,ny) 76 | z = numpy.zeros((nx,ny)) 77 | 78 | for ia, xa in enumerate(x): 79 | for ic, yc in enumerate(y): 80 | m = neb.Molecule() 81 | m.addAtoms(neb.Atom(1, xyz=[xa, 0.0, 0.0]), neb.Atom(1, xyz=[0.0, 0.0, 0.0]), neb.Atom(1, xyz=[0.0, yc, 0.0])) 82 | rab, rbc, v, g = VLeps(m) 83 | z[ia,ic] = v 84 | 85 | f = plt.figure() 86 | ax = f.add_subplot(111) 87 | ax.set_xlabel(r'$r_\mathrm{AB}$', fontsize=16) 88 | ax.set_ylabel(r'$r_\mathrm{BC}$', fontsize=16) 89 | ax.contourf(x,y,z, levels=numpy.linspace(-5.0, 0.0, 50), cmap='Blues_r') 90 | c = ax.contour(x,y,z, levels=numpy.linspace(-5.0, 0.0, 6), colors='white', alpha=0.3, linewidths=2) 91 | # --------------------------------- 92 | 93 | sd = SteepestDescent(stepsize=0.01) 94 | # --------------------------------- 95 | # Setup molecule 1, minimize it 96 | # and plot it's initial and minimized 97 | # coordinates 98 | m1 = neb.Molecule() 99 | xa = 0.6 100 | yc = 2.0 101 | m1.addAtoms( 102 | neb.Atom(1, xyz=[xa, 0.0, 0.0]), 103 | neb.Atom(1, xyz=[0.0, 0.0, 0.0]), 104 | neb.Atom(1, xyz=[0.0, yc, 0.0]) 105 | ) 106 | rab, rbc, v, g = VLeps(m1) 107 | ax.scatter([rab], [rbc], s=30, marker='x', linewidth=2, c='k') 108 | 109 | m1opt = minimize(m1, 1000, 0.02, VLeps, sd) 110 | rab, rbc, v, g = VLeps(m1opt) 111 | ax.scatter([rab], [rbc], s=20, marker='s', linewidth=0, c='k') 112 | 113 | # --------------------------------- 114 | # Setup molecule 2, minimize it 115 | # and plot it's initial and minimized 116 | # coordinates 117 | m2 = neb.Molecule() 118 | xa = 2.0 119 | yc = 0.8 120 | m2.addAtoms( 121 | neb.Atom(1, xyz=[xa, 0.0, 0.0]), 122 | neb.Atom(1, xyz=[0.0, 0.0, 0.0]), 123 | neb.Atom(1, xyz=[0.0, yc, 0.0]) 124 | ) 125 | rab, rbc, v, g = VLeps(m2) 126 | ax.scatter([rab], [rbc], s=30, marker='x', linewidth=2, c='k') 127 | 128 | m2opt = minimize(m2, 1000, 0.02, VLeps, sd) 129 | rab, rbc, v, g = VLeps(m2opt) 130 | ax.scatter([rab], [rbc], s=20, marker='s', linewidth=0, c='k') 131 | 132 | # --------------------------------- 133 | # linear interpolation between m1 and m2 134 | # optimized molecular geometries 135 | l = Linear(m1opt, m2opt, 20) 136 | n = neb.NEB(l, 1.0) 137 | RAB = [] 138 | RBC = [] 139 | for b in n.innerBeads(): 140 | rab, rbc, v, g = VLeps(b) 141 | RAB.append(rab) 142 | RBC.append(rbc) 143 | 144 | ax.plot(RAB, RBC, 'k--', marker='o') 145 | 146 | # --------------------------------- 147 | # minimize path using NEB 148 | n.minimize(200, 0.2, LEPSEnergyAndGradient, sd) 149 | 150 | RAB = [] 151 | RBC = [] 152 | for b in n.innerBeads(): 153 | rab, rbc, v, g = VLeps(b) 154 | RAB.append(rab) 155 | RBC.append(rbc) 156 | 157 | ax.plot(RAB, RBC, 'k-', marker='x') 158 | ax.set_xlim(0.5,4.0) 159 | ax.set_ylim(0.5,4.0) 160 | 161 | plt.show() 162 | -------------------------------------------------------------------------------- /neb/__init__.py: -------------------------------------------------------------------------------- 1 | from molecule import Molecule 2 | from atom import Atom 3 | 4 | from neb import NEB 5 | -------------------------------------------------------------------------------- /neb/angle.py: -------------------------------------------------------------------------------- 1 | 2 | class Angle(object): 3 | def __init__(self, id1, id2, id3): 4 | self._id1 = id1 5 | self._id2 = id2 6 | self._id3 = id3 7 | 8 | def __repr__(self): 9 | return("Angle({0:d},{1:d},{2:d})".format(self._id1, self._id2, self._id3)) 10 | -------------------------------------------------------------------------------- /neb/atom.py: -------------------------------------------------------------------------------- 1 | import numpy 2 | 3 | import util 4 | 5 | class Atom(object): 6 | """ An atom 7 | 8 | The minimum amount of information required is the 9 | nuclear charge Z of the atom. 10 | 11 | Arguments: 12 | Z -- nuclear charge of atom. This argument is mandatory. 13 | 14 | Keyword Arguments: 15 | mass -- the mass of the atom in atomic units. Default is specified by using the nuclear charge. 16 | coords -- the Cartesian coordinate of the atom in Angstrom. Default is origin. 17 | """ 18 | def __init__(self, Z, **kwargs): 19 | assert Z > 0, "Nuclear charge of atom must be greater than zero." 20 | self._z = Z 21 | self._c = numpy.array(kwargs.get('xyz', [0, 0, 0])) 22 | self._mass = kwargs.get('mass', util.MASSES[Z]) 23 | self._vdw_radius = kwargs.get('vwdradius', util.VDWRADII[Z]) 24 | self._cov_radius = kwargs.get('covradius', util.COVALENTRADII[Z]) 25 | self._coordination = kwargs.get('coordination', util.COORDINATION[Z]) 26 | self._label = util.Z2LABEL[Z] 27 | 28 | def getMass(self): 29 | return self._mass 30 | 31 | def getNuclearCharge(self): 32 | return self._z 33 | 34 | def getLabel(self): 35 | return self._label 36 | 37 | def getCoordinate(self): 38 | return self._c 39 | 40 | def setCoordinate(self, value): 41 | (n, ) = numpy.shape(value) 42 | assert n == 3, "Dimensions of data do not match. Expected 3 but got {}".format(n) 43 | self._c = value 44 | 45 | def getVDWRadius(self): 46 | return self._vdw_radius 47 | 48 | def getCovalentRadius(self): 49 | return self._cov_radius 50 | 51 | def setCoordination(self, value): 52 | if not isinstance(int, value): 53 | raise TypeError 54 | 55 | max_coordination = util.COORDINATION[self._z] 56 | if value > max_coordination: 57 | raise ValueError("Coordination number too large.") 58 | 59 | self._coordination = value 60 | 61 | def getCoordination(self): 62 | return self._coordination 63 | -------------------------------------------------------------------------------- /neb/bond.py: -------------------------------------------------------------------------------- 1 | import numpy 2 | 3 | 4 | class Bond(object): 5 | """ A bond between two Atom objects. 6 | 7 | Currently, there is no reference to the actual Atom objects but instead 8 | they are indexed with integers in from the parent molecule class. 9 | """ 10 | def __init__(self, id1, id2): 11 | self._id1 = id1 12 | self._id2 = id2 13 | assert self._id1 != -1 14 | assert self._id2 != -1 15 | assert self._id1 != self._id2, "indices cannot refer to same atom." 16 | 17 | def sharesAtom(self, other): 18 | """ Returns an atom index if two bonds shares an atom. 19 | If an atom is not found or the bonds are the same a -1 is returned. 20 | 21 | Arguments: 22 | other -- the other bond 23 | 24 | Returns: 25 | integer of atom the bonds share. -1 of None. 26 | """ 27 | if self == other: 28 | return -1 29 | 30 | if self._id1 == other._id1 and self._id2 != other._id2: 31 | return self._id1 32 | 33 | if self._id1 == other._id2 and self._id2 != other._id1: 34 | return self._id1 35 | 36 | if self._id2 == other._id1 and self._id1 != other._id2: 37 | return self._id2 38 | 39 | if self._id2 == other._id2 and self._id1 != other._id1: 40 | return self._id2 41 | 42 | return -1 43 | 44 | def getNbrAtomIdx(self, value): 45 | """ Returns the neighboring atom index in the bond """ 46 | if self._id1 == value: return self._id2 47 | if self._id2 == value: return self._id1 48 | raise ValueError("The atom index {0:d} is not in the bond.".format(value)) 49 | 50 | def __eq__(self, other): 51 | """ Bonds are equal if they refer to the same atoms """ 52 | c1 = self._id1 == other._id1 and self._id2 == other._id2 53 | c2 = self._id1 == other._id2 and self._id2 == other._id1 54 | return c1 or c2 55 | 56 | def __repr__(self): 57 | return("Bond({0:d},{1:d})".format(self._id1, self._id2)) 58 | -------------------------------------------------------------------------------- /neb/interpolate/__init__.py: -------------------------------------------------------------------------------- 1 | """ Different interpolation schemes to create initial beads 2 | """ 3 | 4 | from restart import Restart 5 | from linear import Linear 6 | 7 | -------------------------------------------------------------------------------- /neb/interpolate/linear.py: -------------------------------------------------------------------------------- 1 | from ..molecule import Molecule 2 | 3 | import path 4 | 5 | class Linear(path.Path): 6 | """ A linear interpolator that generates n-2 new molecules 7 | """ 8 | def __init__(self, initial, final, nsteps=10): 9 | path.Path.__init__(self) 10 | 11 | assert isinstance(nsteps, int) 12 | 13 | self._molecules = [initial] 14 | 15 | ci = initial.getCoordinates() 16 | cf = final.getCoordinates() 17 | delta = (cf - ci) / (nsteps - 1) 18 | 19 | # only generate the inner range 20 | for k in range(1, nsteps-1): 21 | m2 = Molecule.fromMolecule(initial) 22 | m2.setCoordinates(ci + k*delta) 23 | self._molecules.append(m2) 24 | 25 | self._molecules.append(final) 26 | assert self.getNumBeads() == nsteps 27 | -------------------------------------------------------------------------------- /neb/interpolate/path.py: -------------------------------------------------------------------------------- 1 | from .. import molecule 2 | 3 | class Path(object): 4 | """ Represents a path of molecules along some coordinate """ 5 | def __init__(self): 6 | self._molecules = [] 7 | 8 | def __iter__(self): 9 | for c in self._molecules: 10 | yield c 11 | 12 | def __getitem__(self, index): 13 | return self._molecules[index] 14 | 15 | def getNumBeads(self): 16 | return len(self._molecules) 17 | 18 | -------------------------------------------------------------------------------- /neb/interpolate/restart.py: -------------------------------------------------------------------------------- 1 | import path 2 | 3 | class Restart(path.Path): 4 | """ A path which uses molecules from a previous run """ 5 | def __init__(self, *args): 6 | path.Path.__init__(self) 7 | for _molecule in args: 8 | self._molecules.append(_molecule) 9 | 10 | -------------------------------------------------------------------------------- /neb/methods/__init__.py: -------------------------------------------------------------------------------- 1 | from leps import LEPSEnergyAndGradient 2 | from orca import OrcaEnergyAndGradient 3 | -------------------------------------------------------------------------------- /neb/methods/leps.py: -------------------------------------------------------------------------------- 1 | import numpy 2 | 3 | def VLeps(molecule): 4 | """ Energy and gradient of the LEPS potential """ 5 | 6 | def Q(r,d,dr): 7 | alpha = 1.942 8 | value = alpha * (r - 0.742) 9 | energy_factor = 1.5 * numpy.exp(-2.0*value) - numpy.exp(-value) 10 | gradient_factor = alpha*dr/r*(-3.0*numpy.exp(-2.0*value) + numpy.exp(-value)) 11 | return 0.5*d*energy_factor, 0.5*d*gradient_factor 12 | 13 | def J(r,d,dr): 14 | alpha = 1.942 15 | value = alpha * (r - 0.742) 16 | energy_factor = numpy.exp(-2.0*value) - 6.0*numpy.exp(-value) 17 | gradient_factor = alpha*dr/r*(6.0*numpy.exp(-value) - 2.0*numpy.exp(-2.0*value)) 18 | return 0.25*d*energy_factor, 0.25*d*gradient_factor 19 | 20 | c = molecule.getCoordinates() 21 | drab = c[1]-c[0] 22 | drbc = c[2]-c[1] 23 | drac = c[2]-c[0] 24 | 25 | rab = numpy.linalg.norm(drab) 26 | rbc = numpy.linalg.norm(drbc) 27 | rac = numpy.linalg.norm(drac) 28 | 29 | opai = 1.0 / (1.0 + 0.05) 30 | opbi = 1.0 / (1.0 + 0.30) 31 | opci = 1.0 / (1.0 + 0.05) 32 | opai2 = opai*opai 33 | opbi2 = opbi*opbi 34 | opci2 = opci*opci 35 | 36 | dab = 4.476 37 | dbc = 4.476 38 | dac = 3.445 39 | 40 | qab, gqab = Q(rab, dab, drab) 41 | qbc, gqbc = Q(rbc, dbc, drbc) 42 | qac, gqac = Q(rac, dac, drac) 43 | 44 | qvalue = qab*opai + qbc*opbi + qac*opci 45 | qgrad = gqab*opai + gqbc*opbi + gqac*opci 46 | 47 | jab, gjab = J(rab, dab, drab) 48 | jbc, gjbc = J(rbc, dbc, drbc) 49 | jac, gjac = J(rac, dac, drac) 50 | 51 | jvalue = jab*jab*opai2 52 | jvalue += jbc*jbc*opbi2 53 | jvalue += jac*jac*opci2 54 | jvalue -= jab*jbc*opai*opbi 55 | jvalue -= jbc*jac*opbi*opci 56 | jvalue -= jab*jac*opai*opci 57 | 58 | qgrad = numpy.zeros((3,3)) 59 | jgrad = numpy.zeros((3,3)) 60 | 61 | qgrad[0,:] = gqab*opai + gqac*opci 62 | qgrad[1,:] = gqbc*opbi - gqab*opai 63 | qgrad[2,:] =-gqbc*opbi - gqac*opci 64 | 65 | djab = gjab*opai*(2*jab*opai - jac*opci - jbc*opbi) 66 | djbc = gjbc*opbi*(2*jbc*opbi - jab*opai - jac*opci) 67 | djac = gjac*opci*(2*jac*opci - jbc*opbi - jab*opai) 68 | jgrad[0,:] = djab + djac 69 | jgrad[1,:] = djbc - djab 70 | jgrad[2,:] = -djbc - djac 71 | 72 | return rab, rbc, qvalue - numpy.sqrt(jvalue), -(qgrad - 0.5/numpy.sqrt(jvalue)*jgrad) 73 | 74 | def LEPSEnergyAndGradient(molecule): 75 | """ Wrapper for the LEPS potential 76 | 77 | """ 78 | rab, rbc, e, g = VLeps(molecule) 79 | return e, g 80 | -------------------------------------------------------------------------------- /neb/methods/orca.py: -------------------------------------------------------------------------------- 1 | def OrcaEnergyAndGradient(bead): 2 | """ Calculates the energy and gradient of a bead using ORCA 3 | 4 | NOTE: 5 | This method requires a folder called orca_scratch 6 | in the directory for scratch files. 7 | 8 | Arguments: 9 | bead -- the current bead / molecule to calculate 10 | """ 11 | 12 | def parse_gradient(file, n): 13 | """ Gradient parser """ 14 | g = numpy.zeros((n,3)) 15 | for k in range(n): 16 | tokens = (file.readline()).split() 17 | try: 18 | g[k] = numpy.array(map(float, tokens[3:])) 19 | except ValueError: 20 | print tokens 21 | raise 22 | 23 | return g * util.aa2au # Convert from Eh/bohr to Eh/AA 24 | 25 | 26 | """ Returns the gradient in Eh/angstrom """ 27 | (n,k) = numpy.shape(bead.getCoordinates()) 28 | 29 | if not os.path.exists('orca_scratch'): 30 | raise ValueError("ORCA scratch directory 'orca_scratch' does not exist.") 31 | 32 | e = 0.0 33 | g = numpy.zeros((n,k)) 34 | 35 | s = "! PM3 ENGRAD\n* xyz 0 1\n" 36 | for _atom in bead.getAtoms(): 37 | s += "{0:6>s}{1[0]:16.9f}{1[1]:16.9f}{1[2]:16.9f}\n".format(_atom.getLabel(), _atom.getCoordinate()) 38 | s += "*" 39 | with open('orca_scratch/bead.inp', 'w') as orcafile: 40 | orcafile.write(s) 41 | 42 | # go to scratch directory 43 | os.chdir('orca_scratch') 44 | orcacmd = shlex.split("orca bead.inp") # | grep -A16 \"The cartesian gradient:\"") 45 | orcajob = subprocess.Popen(orcacmd, stdout=open('bead.out', 'w')) # subprocess.PIPE) 46 | 47 | # to avoid the python code continuing before 48 | time.sleep(0.5) 49 | 50 | with open('bead.out', 'r') as orcafile: 51 | line = orcafile.readline() 52 | while True: 53 | line = orcafile.readline() 54 | if "TOTAL RUN TIME:" in line: 55 | break 56 | 57 | tokens = line.split() 58 | 59 | # Semi-Empirical gradient 60 | if "The cartesian gradient:" in line: 61 | g = parse_gradient(orcafile, n) 62 | 63 | # HF or DFT gradient 64 | if "CARTESIAN GRADIENT" in line: 65 | line = orcafile.readline() 66 | line = orcafile.readline() 67 | g = parse_gradient(orcafile, n) 68 | 69 | # energy 70 | if "Total Energy :" in line: 71 | e = float(tokens[3]) 72 | 73 | cleancmd = shlex.split("rm -f bead.*") 74 | subprocess.Popen(cleancmd) 75 | os.chdir('..') 76 | 77 | return e, g 78 | -------------------------------------------------------------------------------- /neb/minimizers/__init__.py: -------------------------------------------------------------------------------- 1 | """ Different minimizers to take steps in NEB calculations """ 2 | 3 | from steepestdescent import SteepestDescent 4 | -------------------------------------------------------------------------------- /neb/minimizers/steepestdescent.py: -------------------------------------------------------------------------------- 1 | import numpy 2 | 3 | class SteepestDescent(object): 4 | """ The Steepest Descent method takes a step along 5 | the direction of the force 6 | 7 | R_i+1 = R_i + k * F_i 8 | 9 | where k is the stepsize. 10 | """ 11 | def __init__(self, stepsize=1.0e-3, eps=1.0e-2, verbose=False): 12 | self._stepsize = stepsize 13 | self._eps = eps 14 | self._verbose = verbose 15 | 16 | def step(self, energy, force): 17 | return self._stepsize * force 18 | -------------------------------------------------------------------------------- /neb/molecule.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import numpy 3 | 4 | import atom 5 | import bond 6 | import angle 7 | 8 | class Molecule(object): 9 | """ A molecule. 10 | 11 | A molecule is at the minimum a collection of atoms. 12 | 13 | The molecule class can also be asked to identify all bonds. This can be 14 | quite costly since we use a brute force approach. 15 | """ 16 | _bond_threshold = 0.45 # Added threshold for bonds. Replicates openbabel 17 | 18 | def __init__(self): 19 | self._charge = 0 20 | self._multiplicity = 1 21 | self._atoms = [] 22 | self._bonds = [] 23 | self._name = "" 24 | 25 | # class methods 26 | @classmethod 27 | def fromMolecule(cls, m): 28 | M = cls() 29 | M.setCharge(m.getCharge()) 30 | M.setMultiplicity(m.getMultiplicity()) 31 | M.setName(m.getName()) 32 | if m.getNumAtoms() > 0: 33 | M.addAtoms(*m.getAtoms()) 34 | 35 | # currently we do not transfer bond information 36 | return M 37 | 38 | # getters and setters for various properties 39 | def addAtom(self, _atom): 40 | #assert isinstance(_atom, atom.Atom), "You attempted to add something that was not an atom." 41 | self._atoms.append(copy.deepcopy(_atom)) 42 | 43 | def addAtoms(self, *args): 44 | for _atom in args: 45 | self.addAtom(_atom) 46 | 47 | def getNumAtoms(self): 48 | """ Returns the number of atoms in the molecule """ 49 | return len(self._atoms) 50 | 51 | def getAtoms(self): 52 | for _atom in self._atoms: 53 | yield _atom 54 | 55 | def getBonds(self): 56 | """ Returns all bonds (as an iterator) in the molecule 57 | 58 | If the bond list has not been calculated before, the bonds are 59 | percieved through the percieveBonds method 60 | """ 61 | if len(self._bonds) == 0: 62 | self._bonds = list(self.percieveBonds()) 63 | 64 | for _bond in self._bonds: 65 | yield _bond 66 | 67 | def getName(self): 68 | return self._name 69 | 70 | def setName(self, value): 71 | assert isinstance(value, str) 72 | self._name = value 73 | 74 | def getCharge(self): 75 | return self._charge 76 | 77 | def setCharge(self, value): 78 | assert isinstance(value, int) 79 | self._charge = value 80 | 81 | def getMultiplicity(self): 82 | return self._multiplicity 83 | 84 | def setMultiplicity(self, value): 85 | assert isinstance(value, int) 86 | self._multiplicity = value 87 | 88 | # properties that are lazily evaluated such as bonds and angles 89 | def percieveBonds(self): 90 | """ This method attempts to percieve bonds 91 | 92 | It works by comparing atom distances to covalent radii of the atoms. 93 | It is not optimized in any way. 94 | """ 95 | 96 | for iat, atom1 in enumerate(self.getAtoms()): 97 | for jat, atom2 in enumerate(self.getAtoms()): 98 | if iat <= jat: continue 99 | dr = atom2.getCoordinate() - atom1.getCoordinate() 100 | R2 = dr.dot(dr) 101 | 102 | dr_cov = atom1.getCovalentRadius() + atom2.getCovalentRadius() + self._bond_threshold 103 | R2_cov = dr_cov**2 104 | if R2 < R2_cov: 105 | yield bond.Bond(id1=iat, id2=jat) 106 | 107 | def percieveAngles(self): 108 | """ This method attemps to percieve angles 109 | 110 | It works by iterating through all bonds in the molecule 111 | """ 112 | for ibd, bond1 in enumerate(self.getBonds()): 113 | for jbd, bond2 in enumerate(self.getBonds()): 114 | if ibd <= jbd: continue 115 | jatm = bond1.sharesAtom(bond2) 116 | if jatm >= 0: 117 | iatm = bond1.getNbrAtomIdx(jatm) 118 | katm = bond2.getNbrAtomIdx(jatm) 119 | yield angle.Angle(iatm, jatm, katm) 120 | 121 | # specialized options to extract information stored in 122 | # other classes related to molecule 123 | def getCoordinates(self): 124 | """ Returns a numpy array with all the coordinates 125 | of all the atoms in the molecule 126 | """ 127 | c = numpy.zeros((self.getNumAtoms(), 3)) 128 | for iat, _atom in enumerate(self.getAtoms()): 129 | c[iat] = _atom.getCoordinate() 130 | 131 | return c 132 | 133 | def setCoordinates(self, c): 134 | """ Sets the coordinates of all atoms in the molecule from 135 | the numpy array 136 | """ 137 | assert isinstance(c, numpy.ndarray) 138 | (n,k) = numpy.shape(c) 139 | assert n == self.getNumAtoms() 140 | for iat, _atom in enumerate(self.getAtoms()): 141 | _atom.setCoordinate(c[iat]) 142 | -------------------------------------------------------------------------------- /neb/neb.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import numpy 4 | 5 | class NEB(object): 6 | """ A Nudged Elastic Band implementation 7 | 8 | This NEB implementation is based on http://dx.doi.org/10.1063/1.1323224 9 | by Henkelman et al. 10 | """ 11 | def __init__(self, path, k): 12 | """ Initialize the NEB with a predefined path and force 13 | constants between images. 14 | 15 | Typical use-case might look like: 16 | 17 | >>> m1 = molecule_from_xyz('m1.xyz') 18 | >>> m2 = molecule_from_xyz('m2.xyz') 19 | >>> apath = neb.interpolate.Linear(m1, m2, 10) 20 | >>> neb = neb.Neb(apath, 5.0) 21 | >>> eandg = somefunction 22 | >>> minimizer = neb.minimizers.SteepestDescent 23 | >>> neb.minimize(100, 0.01, eandg, minimizer) 24 | 25 | Arguments: 26 | path -- Path between two endpoints to be optimized 27 | k -- force constant in units of eV / A^2 between each bead in the path 28 | """ 29 | self._path = path 30 | self._k = k 31 | 32 | # set bead energies, tangents, forces and spring forces to zero initially 33 | self._tangents = [] 34 | self._beadgradients = [] 35 | self._springforces = [] 36 | self._forces = [] 37 | self._energies = [] 38 | 39 | # accounting variables 40 | self._grms = [] 41 | 42 | for bead in path: 43 | (n, k) = numpy.shape(bead.getCoordinates()) 44 | self._tangents.append(numpy.zeros((n,k))) 45 | self._springforces.append(numpy.zeros((n,k))) 46 | self._beadgradients.append(numpy.zeros((n,k))) 47 | self._forces.append(numpy.zeros((n,k))) 48 | self._energies.append(0.0) 49 | self._grms.append(-1.0) 50 | 51 | # now we calculate the tangents and springforces 52 | # for the initial beads 53 | self._beadTangents() 54 | self._springForces() 55 | 56 | def innerBeads(self): 57 | """ an iterator over the inner beads """ 58 | n = self._path.getNumBeads() 59 | for i, bead in enumerate(self._path): 60 | if i > 0 and i < n-1: 61 | yield bead 62 | 63 | def innerBeadForces(self): 64 | """ iterator over the forces of the inner beads """ 65 | for i, bead in enumerate(self.innerBeads(), start=1): 66 | yield self._forces[i] 67 | 68 | def _beadTangents(self): 69 | """ Evaluates all tangents for all the inner beads """ 70 | for ibead, bead in enumerate(self.innerBeads(), start=1): 71 | self._tangents[ibead] = self._beadTangent(bead, self._path[ibead-1], self._path[ibead+1]) 72 | 73 | def _beadTangent(self, ibead, mbead, pbead): 74 | """ Calculates the tangent for ibead given the bead 75 | indexed by i-1 (mbead) and i+1 (pbead). 76 | 77 | Calculated according to eq 2 in http://dx.doi.org/10.1063/1.1323224 78 | 79 | Arguments: 80 | ibead -- the current (i'th) bead 81 | mbead -- the (i-1)'th bead to use in the calculation of the tanget 82 | pbead -- the (i+1)'th bead to use in the calculation of the tanget 83 | 84 | Returns: 85 | tanget of the bead 86 | """ 87 | Ri = ibead.getCoordinates() 88 | Rm = mbead.getCoordinates() 89 | Rp = pbead.getCoordinates() 90 | 91 | vm = Ri - Rm 92 | vp = Rp - Ri 93 | ti = vm / numpy.linalg.norm(numpy.ravel(vm)) + vp / numpy.linalg.norm(numpy.ravel(vp)); 94 | return ti / numpy.linalg.norm(ti) 95 | 96 | def _springForces(self): 97 | """ Evaluates all spring forces between the beads """ 98 | for ibead, bead in enumerate(self.innerBeads(), start=1): 99 | self._springforces[ibead] = self._springForce(bead, self._path[ibead-1], self._path[ibead+1], self._tangents[ibead]) 100 | 101 | def _springForce(self, ibead, mbead, pbead, tangent): 102 | """ Calculates the spring force for ibead given the bead 103 | indexed by i-1 (mbead) and i+1 (pbead). 104 | 105 | 106 | """ 107 | Ri = numpy.ravel(ibead.getCoordinates()) 108 | Rm = numpy.ravel(mbead.getCoordinates()) 109 | Rp = numpy.ravel(pbead.getCoordinates()) 110 | 111 | # old spring force calculated according 112 | # to eq 5 in http://dx.doi.org/10.1063/1.1323224 113 | r = numpy.dot(numpy.ravel(Rp + Rm - 2*Ri), numpy.ravel(tangent)) 114 | 115 | return self._k * r * tangent 116 | 117 | def _beadGradients(self, func): 118 | """ Calculates the forces on each bead using the func supplied 119 | 120 | Calculated according to eq 4 in http://dx.doi.org/10.1063/1.1323224 121 | 122 | Arguments: 123 | bead -- the bead whose internal force is to be evaluated 124 | func -- function that returns energy and forces for a bead 125 | 126 | Returns: 127 | e, g -- internal energy and force with component projected out 128 | """ 129 | if func is None: 130 | return 131 | 132 | for ibead, bead in enumerate(self.innerBeads(), start=1): 133 | energy, gradient = func(bead) 134 | tangent = self._tangents[ibead] 135 | 136 | grad_perp = numpy.dot(numpy.ravel(gradient), numpy.ravel(tangent)) 137 | 138 | # calculate regular NEB bead gradient 139 | self._beadgradients[ibead] = gradient - grad_perp * tangent 140 | 141 | self._energies[ibead] = energy 142 | 143 | def beadForces(self, func): 144 | """ Calculates the forces of all 'inner' beads 145 | 146 | Arguments: 147 | func -- function that returns energy and forces for a bead 148 | """ 149 | self._beadTangents() 150 | self._springForces() 151 | self._beadGradients(func) 152 | 153 | for ibead, bead in enumerate(self.innerBeads(), start=1): 154 | bead_force = - self._beadgradients[ibead] 155 | 156 | bead_force += self._springforces[ibead] 157 | 158 | self._forces[ibead] = bead_force[:] 159 | 160 | # Accounting and statistics 161 | f = numpy.ravel(bead_force) 162 | self._grms[ibead] = math.sqrt(f.dot(f)/len(f)) 163 | 164 | def minimize(self, nsteps, opttol, func, minimizer): 165 | """ Minimizes the NEB path 166 | 167 | The minimization is carried out for nsteps to a tolerance 168 | of opttol with the energy and gradients calculated 169 | for each bead by func. The minimizer used is suppplied 170 | via the minimizers argument. 171 | 172 | When the method ends, one can iterate over all the beads 173 | in this class to get the states and continue from there. 174 | 175 | NOTE: The opttol argument is not active 176 | 177 | Arguments: 178 | nstesp -- perform a maximum of nsteps steps 179 | opttol -- the maximum rms gradient shall be below this value 180 | func -- energy and gradient function 181 | minimizer -- a minimizer 182 | """ 183 | for i in range(1, nsteps): 184 | self.beadForces(func) 185 | 186 | s = "-"*89 + "\nI={0:3d} ENERGY={1:12.6f} G RMS={2:13.9f}" 187 | s2 = " E =" 188 | s3 = " F RMS =" 189 | s4 = " F SPR =" 190 | 191 | maxerg = max(self._energies[1:-1]) 192 | grms = 0.0 193 | grmsnrm = 0 194 | for ibead, bead in enumerate(self.innerBeads(), start=1): 195 | c = bead.getCoordinates() 196 | (n, k) = numpy.shape(c) 197 | bead.setCoordinates(c + minimizer.step(self._energies[ibead], self._forces[ibead])) 198 | 199 | f = numpy.ravel(self._forces[ibead]) 200 | grms += numpy.linalg.norm(f) 201 | grmsnrm += len(f) 202 | 203 | s2 += "{0:9.4f}".format(self._energies[ibead]) 204 | s3 += "{0:9.4f}".format(self._grms[ibead]) 205 | s4 += "{0:9.4f}".format(numpy.max(self._springforces[ibead])) 206 | 207 | print s.format(i, maxerg, math.sqrt(grms/grmsnrm)) 208 | print s2 209 | print s3 210 | print s4 211 | -------------------------------------------------------------------------------- /neb/util.py: -------------------------------------------------------------------------------- 1 | import numpy 2 | """ Utility variables and functions 3 | """ 4 | 5 | aa2au = 1.8897261249935897 # bohr / AA 6 | 7 | # converts nuclear charge to atom label 8 | Z2LABEL = { 9 | 1: 'H', 2: 'He', 10 | 3: 'Li', 4: 'Be', 5: 'B', 6: 'C', 7: 'N', 8: 'O', 9: 'F', 10: 'Ne', 11 | 11: 'NA', 12: 'Mg', 13: 'Al', 14: 'Si', 15: 'P', 16: 'S', 17: 'Cl', 18: 'Ar' 12 | } 13 | 14 | # converts an atomic label to a nuclear charge 15 | LABEL2Z = {} 16 | for key in Z2LABEL: 17 | LABEL2Z[Z2LABEL[key]] = key 18 | 19 | # masses from UIPAC: http://www.chem.qmul.ac.uk/iupac/AtWt/ 20 | MASSES = {0: 0.00, 21 | 1: 1.00784, 2: 4.002602, 22 | 3: 6.938, 4: 9.01218, 5: 10.806, 6: 12.0096, 7: 14.00643, 8: 15.99903, 9: 18.998403, 10: 20.1797, 23 | 11: 22.9898, 12: 24.304, 13: 26.9815, 14: 28.084, 15: 30.973, 16: 32.059, 17: 35.446, 18: 39.948 24 | } 25 | 26 | # Van der Waal radii from Alvarez (2013), DOI: 2013/dt/c3dt50599e 27 | # all values in Angstrom 28 | VDWRADII = {0: 0.00, 29 | 1: 1.20, 2: 1.43, 30 | 3: 2.12, 4: 1.98, 5: 1.91, 6: 1.77, 7: 1.66, 8: 1.50, 9: 1.46, 10: 1.58, 31 | 11: 2.50, 12: 2.51, 13: 2.25, 14: 2.19, 15: 1.90, 16: 1.89, 17: 1.82, 18: 1.83 32 | } 33 | 34 | # Covalent radii from Pykko and Atsumi (2009), DOI: 0.1002/chem.200800987 35 | # all values in Angstrom 36 | COVALENTRADII = {0: 0.00, 37 | 1: 0.32, 2: 0.46, 38 | 3: 1.33, 4: 1.02, 5: 0.85, 6: 0.75, 7: 0.71, 8: 0.63, 9: 0.64, 10: 0.67, 39 | 11: 1.55, 12: 1.39, 13: 1.26, 14: 1.16, 15: 1.11, 16: 1.03, 17: 0.99, 18: 0.96 40 | } 41 | 42 | # Coordination numbers from Pykko and Atsumi (2009), DOI: 0.1002/chem.200800987 43 | COORDINATION = {0: 0, 44 | 1: 1, 2: 1, 45 | 3: 1, 4: 2, 5: 3, 6: 4, 7: 3, 8: 2, 9: 1, 10: 1, 46 | 11: 1, 12: 2, 13: 3, 14: 4, 15: 3, 16: 2, 17: 1, 18: 1 47 | } 48 | 49 | 50 | def idamax(a): 51 | """ Returns the index of maximum absolute value (positive or negative) 52 | in the input array a. 53 | 54 | Note: Loosely based of a subroutine in GAMESS with the same name 55 | 56 | Arguments: 57 | a -- a numpy array where we are to find the maximum 58 | value in (either positive or negative) 59 | 60 | Returns: 61 | the index in the array where the maximum value is. 62 | """ 63 | idx = -1 64 | v = 0.0 65 | for i, value in enumerate(numpy.abs(a)): 66 | if value > v: 67 | idx = i 68 | v = value 69 | 70 | return idx 71 | 72 | def idamin(a): 73 | """ Returns the index of minimum absolute value (positive or negative) 74 | in the input array a. 75 | 76 | Arguments: 77 | a -- a numpy array where we are to find the minimum 78 | value in (either positive or negative) 79 | 80 | Returns: 81 | the index in the array where the maximum value is. 82 | """ 83 | idx = -1 84 | v = 1.0e30 85 | for i, value in enumerate(numpy.abs(a)): 86 | if value < v: 87 | idx = i 88 | v = value 89 | 90 | return idx 91 | --------------------------------------------------------------------------------