def create_lst_images(freactant, fproduct, nimages=11, mic=False): """create images with original LST algorithm without NEB force projection as in IDPP Parameters ---------- freactant : path to initial atoms fproduct : path to final atoms nimages : the number of images to be interpolated including two endpoints mic : apply mic or not (PBC) """ from ase.neb import IDPP from ase.optimize import BFGS from ase.build import minimize_rotation_and_translation # create linearly interpolated images images = _create_images(freactant, fproduct, nimages) neb = NEB(images, remove_rotation_and_translation=True) neb.interpolate() # refine images with LST algorithm d1 = images[0].get_all_distances(mic=mic) d2 = images[-1].get_all_distances(mic=mic) d = (d2 - d1) / (nimages - 1) for i, image in enumerate(images): image.set_calculator(IDPP(d1 + i * d, mic=mic)) qn = BFGS(image) qn.run(fmax=0.1) # apply optimal translation and rotation for i in range(nimages - 1): minimize_rotation_and_translation(images[i], images[i + 1]) return images
def interpolate(self, method='linear', mic=False, apply_constraint=None): """Interpolate the positions of the interior images between the initial state (image 0) and final state (image -1). method: str Method by which to interpolate: 'linear' or 'idpp'. linear provides a standard straight-line interpolation, while idpp uses an image-dependent pair potential. mic: bool Use the minimum-image convention when interpolating. apply_constraint: bool Controls if the constraints attached to the images are ignored or applied when setting the interpolated positions. Default value is None, in this case the resulting constrained positions (apply_constraint=True) are compared with unconstrained positions (apply_constraint=False), if the positions are not the same the user is required to specify the desired behaviour by setting up apply_constraint keyword argument to False or True. """ if self.remove_rotation_and_translation: minimize_rotation_and_translation(self.images[0], self.images[-1]) interpolate(self.images, mic, apply_constraint=apply_constraint) if method == 'idpp': idpp_interpolate(images=self, traj=None, log=None, mic=mic)
def interpolate(self, method='linear', mic=False): if self.remove_rotation_and_translation: minimize_rotation_and_translation(self.images[0], self.images[-1]) interpolate(self.images, mic) if method == 'idpp': self.idpp_interpolate(traj=None, log=None, mic=mic)
def interpolate(self, method='linear', mic=False): if self.remove_rotation_and_translation: minimize_rotation_and_translation(self.images[0], self.images[-1]) interpolate(self.images, mic) if method == 'idpp': self.idpp_interpolate(traj=None, log=None, mic=mic)
def interpolate(self, method='linear', mic=False): """Interpolate the positions of the interior images between the initial state (image 0) and final state (image -1). method: str Method by which to interpolate: 'linear' or 'idpp'. linear provides a standard straight-line interpolation, while idpp uses an image-dependent pair potential. mic: bool Use the minimum-image convention when interpolating. """ if self.remove_rotation_and_translation: minimize_rotation_and_translation(self.images[0], self.images[-1]) interpolate(self.images, mic) if method == 'idpp': idpp_interpolate(images=self, traj=None, log=None, mic=mic)
def get_terminations(self, slab, layers): ''' determine how many terminations for a surface index for a slab with SrO, TiO2, SrO, TiO2 ... ordering, there will be two terminations 'SrO' and 'TiO2' Search from the top to the bottom ''' from ase.build import minimize_rotation_and_translation from ase.calculators.calculator import compare_atoms, equal import pprint nlayer = max(layers) + 1 terminations = {} natoms = len(slab) for i in range(nlayer - 2, 0, -1): atoms = slab[layers == i] formula = atoms.get_chemical_formula(mode='hill') print(formula) inds = [ j for j in range(natoms) if layers[j] in range(i, i - self.nlayer, -1) ] atoms = slab[inds] atoms.wrap() if not terminations: terminations['%s-%s' % (formula, i)] = atoms.copy() else: flag = True atoms1 = atoms.copy() for ter, atoms2 in terminations.items(): if len(atoms1) != len(atoms2): flag = False else: minimize_rotation_and_translation(atoms2, atoms1) if equal(atoms1.arrays, atoms2.arrays, tol=0.1): flag = False if flag: terminations['%s-%s' % (formula, i)] = atoms.copy() print(formula, ter) # view([atoms1, atoms2]) pprint.pprint(terminations) # view(terminations.values()) return terminations
def get_forces(self): """Evaluate and return the forces.""" images = self.images calculators = [image.calc for image in images if image.calc is not None] if len(set(calculators)) != len(calculators): msg = ('One or more NEB images share the same calculator. ' 'Each image must have its own calculator. ' 'You may wish to use the ase.neb.SingleCalculatorNEB ' 'class instead, although using separate calculators ' 'is recommended.') raise ValueError(msg) forces = np.empty(((self.nimages - 2), self.natoms, 3)) energies = np.empty(self.nimages) if self.remove_rotation_and_translation: # Remove translation and rotation between # images before computing forces: for i in range(1, self.nimages): minimize_rotation_and_translation(images[i - 1], images[i]) if self.method != 'aseneb': energies[0] = images[0].get_potential_energy() energies[-1] = images[-1].get_potential_energy() if not self.parallel: # Do all images - one at a time: for i in range(1, self.nimages - 1): energies[i] = images[i].get_potential_energy() forces[i - 1] = images[i].get_forces() elif self.world.size == 1: def run(image, energies, forces): energies[:] = image.get_potential_energy() forces[:] = image.get_forces() threads = [threading.Thread(target=run, args=(images[i], energies[i:i + 1], forces[i - 1:i])) for i in range(1, self.nimages - 1)] for thread in threads: thread.start() for thread in threads: thread.join() else: # Parallelize over images: i = self.world.rank * (self.nimages - 2) // self.world.size + 1 try: energies[i] = images[i].get_potential_energy() forces[i - 1] = images[i].get_forces() except: # Make sure other images also fail: error = self.world.sum(1.0) raise else: error = self.world.sum(0.0) if error: raise RuntimeError('Parallel NEB failed!') for i in range(1, self.nimages - 1): root = (i - 1) * self.world.size // (self.nimages - 2) self.world.broadcast(energies[i:i + 1], root) self.world.broadcast(forces[i - 1], root) imax = 1 + np.argsort(energies[1:-1])[-1] self.emax = energies[imax] t1 = find_mic(images[1].get_positions() - images[0].get_positions(), images[0].get_cell(), images[0].pbc)[0] if self.method == 'eb': beeline = (images[self.nimages - 1].get_positions() - images[0].get_positions()) beelinelength = np.linalg.norm(beeline) eqlength = beelinelength / (self.nimages - 1) nt1 = np.linalg.norm(t1) for i in range(1, self.nimages - 1): t2 = find_mic(images[i + 1].get_positions() - images[i].get_positions(), images[i].get_cell(), images[i].pbc)[0] nt2 = np.linalg.norm(t2) if self.method == 'eb': # Tangents are bisections of spring-directions # (formula C8 of paper III) tangent = t1 / nt1 + t2 / nt2 # Normalize the tangent vector tangent /= np.linalg.norm(tangent) elif self.method == 'improvedtangent': # Tangents are improved according to formulas 8, 9, 10, # and 11 of paper I. if energies[i + 1] > energies[i] > energies[i - 1]: tangent = t2.copy() elif energies[i + 1] < energies[i] < energies[i - 1]: tangent = t1.copy() else: deltavmax = max(abs(energies[i + 1] - energies[i]), abs(energies[i - 1] - energies[i])) deltavmin = min(abs(energies[i + 1] - energies[i]), abs(energies[i - 1] - energies[i])) if energies[i + 1] > energies[i - 1]: tangent = t2 * deltavmax + t1 * deltavmin else: tangent = t2 * deltavmin + t1 * deltavmax # Normalize the tangent vector tangent /= np.linalg.norm(tangent) else: if i < imax: tangent = t2 elif i > imax: tangent = t1 else: tangent = t1 + t2 tt = np.vdot(tangent, tangent) f = forces[i - 1] ft = np.vdot(f, tangent) if i == imax and self.climb: # imax not affected by the spring forces. The full force # with component along the elestic band converted # (formula 5 of Paper II) if self.method == 'aseneb': f -= 2 * ft / tt * tangent else: f -= 2 * ft * tangent elif self.method == 'eb': f -= ft * tangent # Spring forces # (formula C1, C5, C6 and C7 of Paper III) f1 = -(nt1 - eqlength) * t1 / nt1 * self.k[i - 1] f2 = (nt2 - eqlength) * t2 / nt2 * self.k[i] if self.climb and abs(i - imax) == 1: deltavmax = max(abs(energies[i + 1] - energies[i]), abs(energies[i - 1] - energies[i])) deltavmin = min(abs(energies[i + 1] - energies[i]), abs(energies[i - 1] - energies[i])) f += (f1 + f2) * deltavmin / deltavmax else: f += f1 + f2 elif self.method == 'improvedtangent': f -= ft * tangent # Improved parallel spring force (formula 12 of paper I) f += (nt2 * self.k[i] - nt1 * self.k[i - 1]) * tangent else: f -= ft / tt * tangent f -= np.vdot(t1 * self.k[i - 1] - t2 * self.k[i], tangent) / tt * tangent t1 = t2 nt1 = nt2 return forces.reshape((-1, 3))
def get_forces(self): """Evaluate and return the forces.""" images = self.images if not self.allow_shared_calculator: calculators = [ image.calc for image in images if image.calc is not None ] if len(set(calculators)) != len(calculators): msg = ('One or more NEB images share the same calculator. ' 'Each image must have its own calculator. ' 'You may wish to use the ase.neb.SingleCalculatorNEB ' 'class instead, although using separate calculators ' 'is recommended.') raise ValueError(msg) forces = np.empty(((self.nimages - 2), self.natoms, 3)) energies = np.empty(self.nimages) if self.remove_rotation_and_translation: for i in range(1, self.nimages): minimize_rotation_and_translation(images[i - 1], images[i]) if self.method != 'aseneb': energies[0] = images[0].get_potential_energy() energies[-1] = images[-1].get_potential_energy() if not self.parallel: # Do all images - one at a time: for i in range(1, self.nimages - 1): energies[i] = images[i].get_potential_energy() forces[i - 1] = images[i].get_forces() elif self.world.size == 1: def run(image, energies, forces): energies[:] = image.get_potential_energy() forces[:] = image.get_forces() threads = [ threading.Thread(target=run, args=(images[i], energies[i:i + 1], forces[i - 1:i])) for i in range(1, self.nimages - 1) ] for thread in threads: thread.start() for thread in threads: thread.join() else: # Parallelize over images: i = self.world.rank * (self.nimages - 2) // self.world.size + 1 try: energies[i] = images[i].get_potential_energy() forces[i - 1] = images[i].get_forces() except Exception: # Make sure other images also fail: error = self.world.sum(1.0) raise else: error = self.world.sum(0.0) if error: raise RuntimeError('Parallel NEB failed!') for i in range(1, self.nimages - 1): root = (i - 1) * self.world.size // (self.nimages - 2) self.world.broadcast(energies[i:i + 1], root) self.world.broadcast(forces[i - 1], root) # Save for later use in iterimages: self.energies = energies self.real_forces = np.zeros((self.nimages, self.natoms, 3)) self.real_forces[1:-1] = forces state = NEBState(self, images, energies) # Can we get rid of self.energies, self.imax, self.emax etc.? self.imax = state.imax self.emax = state.emax spring1 = state.spring(0) for i in range(1, self.nimages - 1): spring2 = state.spring(i) tangent = self.neb_method.get_tangent(state, spring1, spring2, i) imgforce = forces[i - 1] # Get overlap between PES-derived force and tangent tangential_force = np.vdot(imgforce, tangent) if i == self.imax and self.climb: """The climbing image, imax, is not affected by the spring forces. This image feels the full PES-derived force, but the tangential component is inverted: see Eq. 5 in paper II.""" if self.method == 'aseneb': tangent_mag = np.vdot(tangent, tangent) # For normalizing imgforce -= 2 * tangential_force / tangent_mag * tangent else: imgforce -= 2 * tangential_force * tangent else: self.neb_method.add_image_force(state, tangential_force, tangent, imgforce, spring1, spring2, i) spring1 = spring2 return forces.reshape((-1, 3))
def get_forces(self): """Evaluate and return the forces.""" print() print("Enter MyNEB: get_forces()") print() images = self.images calculators = [ image.calc for image in images if image.calc is not None ] if len(set(calculators)) != len(calculators): msg = ('One or more NEB images share the same calculator. ' 'Each image must have its own calculator. ' 'You may wish to use the ase.neb.SingleCalculatorNEB ' 'class instead, although using separate calculators ' 'is recommended.') raise ValueError(msg) forces = np.empty(((self.nimages - 2), self.natoms, 3)) energies = np.empty(self.nimages) if self.remove_rotation_and_translation: # Remove translation and rotation between # images before computing forces: for i in range(1, self.nimages): minimize_rotation_and_translation(images[i - 1], images[i]) if self.method != 'aseneb': energies[0] = images[0].get_potential_energy() energies[-1] = images[-1].get_potential_energy() # Do all images - one at a time: for i in range(1, self.nimages - 1): energies[i] = images[i].get_potential_energy() forces[i - 1] = images[i].get_forces() # Save for later use in iterimages: self.energies = energies self.real_forces = np.zeros((self.nimages, self.natoms, 3)) self.real_forces[1:-1] = forces self.imax = 1 + np.argsort(energies[1:-1])[-1] self.emax = energies[self.imax] print("imax = ", self.imax + 1) print("emax = ", self.emax) t1 = find_mic(images[1].get_positions() - images[0].get_positions(), images[0].get_cell(), images[0].pbc)[0] if self.method == 'eb': beeline = (images[self.nimages - 1].get_positions() - images[0].get_positions()) beelinelength = np.linalg.norm(beeline) eqlength = beelinelength / (self.nimages - 1) nt1 = np.linalg.norm(t1) for i in range(1, self.nimages - 1): t2 = find_mic( images[i + 1].get_positions() - images[i].get_positions(), images[i].get_cell(), images[i].pbc)[0] nt2 = np.linalg.norm(t2) if self.method == 'eb': # Tangents are bisections of spring-directions # (formula C8 of paper III) tangent = t1 / nt1 + t2 / nt2 # Normalize the tangent vector tangent /= np.linalg.norm(tangent) elif self.method == 'improvedtangent': # Tangents are improved according to formulas 8, 9, 10, # and 11 of paper I. if energies[i + 1] > energies[i] > energies[i - 1]: tangent = t2.copy() elif energies[i + 1] < energies[i] < energies[i - 1]: tangent = t1.copy() else: deltavmax = max(abs(energies[i + 1] - energies[i]), abs(energies[i - 1] - energies[i])) deltavmin = min(abs(energies[i + 1] - energies[i]), abs(energies[i - 1] - energies[i])) if energies[i + 1] > energies[i - 1]: tangent = t2 * deltavmax + t1 * deltavmin else: tangent = t2 * deltavmin + t1 * deltavmax # Normalize the tangent vector tangent /= np.linalg.norm(tangent) else: if i < self.imax: tangent = t2 elif i > self.imax: tangent = t1 else: tangent = t1 + t2 tt = np.vdot(tangent, tangent) f = forces[i - 1] ft = np.vdot(f, tangent) if i == self.imax and self.climb: # imax not affected by the spring forces. The full force # with component along the elestic band converted # (formula 5 of Paper II) if self.method == 'aseneb': f -= 2 * ft / tt * tangent else: f -= 2 * ft * tangent elif self.method == 'eb': f -= ft * tangent # Spring forces # (formula C1, C5, C6 and C7 of Paper III) f1 = -(nt1 - eqlength) * t1 / nt1 * self.k[i - 1] f2 = (nt2 - eqlength) * t2 / nt2 * self.k[i] if self.climb and abs(i - self.imax) == 1: deltavmax = max(abs(energies[i + 1] - energies[i]), abs(energies[i - 1] - energies[i])) deltavmin = min(abs(energies[i + 1] - energies[i]), abs(energies[i - 1] - energies[i])) f += (f1 + f2) * deltavmin / deltavmax else: f += f1 + f2 elif self.method == 'improvedtangent': f -= ft * tangent # Improved parallel spring force (formula 12 of paper I) f += (nt2 * self.k[i] - nt1 * self.k[i - 1]) * tangent else: f -= ft / tt * tangent f -= np.vdot(t1 * self.k[i - 1] - t2 * self.k[i], tangent) / tt * tangent tx = tangent[0, 0] ty = tangent[0, 1] fx = forces[i - 1][0, 0] fy = forces[i - 1][0, 1] print("image = %3d tangent = %18.10f %18.10f" % ((i + 1), tx, ty)) print(" forces = %18.10f %18.10f" % (fx, fy)) t1 = t2 nt1 = nt2 if self.dynamic_relaxation: n = self.natoms k = i - 1 n1 = n * k n2 = n1 + n force_i = np.sqrt((forces.reshape( (-1, 3))[n1:n2]**2.).sum(axis=1)).max() n1_imax = (self.imax - 1) * n positions = self.get_positions() pos_imax = positions[n1_imax:n1_imax + n] rel_pos = np.sqrt(((positions[n1:n2] - pos_imax)**2).sum()) if force_i < self.fmax * (1 + rel_pos * self.scale_fmax): if k == self.imax - 1: pass else: forces[k, :, :] = np.zeros((1, self.natoms, 3)) print() print("MyNEB: end of get_forces()") print() return forces.reshape((-1, 3))
def get_forces(self): """Evaluate and return the forces.""" images = self.images forces = np.empty(((self.nimages - 2), self.natoms, 3)) energies = np.empty(self.nimages - 2) if self.remove_rotation_and_translation: # Remove translation and rotation between # images before computing forces: for i in range(1, self.nimages): minimize_rotation_and_translation(images[i - 1], images[i]) if not self.parallel: # Do all images - one at a time: for i in range(1, self.nimages - 1): energies[i - 1] = images[i].get_potential_energy() forces[i - 1] = images[i].get_forces() elif self.world.size == 1: def run(image, energies, forces): energies[:] = image.get_potential_energy() forces[:] = image.get_forces() threads = [threading.Thread(target=run, args=(images[i], energies[i - 1:i], forces[i - 1:i])) for i in range(1, self.nimages - 1)] for thread in threads: thread.start() for thread in threads: thread.join() else: # Parallelize over images: i = self.world.rank * (self.nimages - 2) // self.world.size + 1 try: energies[i - 1] = images[i].get_potential_energy() forces[i - 1] = images[i].get_forces() except: # Make sure other images also fail: error = self.world.sum(1.0) raise else: error = self.world.sum(0.0) if error: raise RuntimeError('Parallel NEB failed!') for i in range(1, self.nimages - 1): root = (i - 1) * self.world.size // (self.nimages - 2) self.world.broadcast(energies[i - 1:i], root) self.world.broadcast(forces[i - 1], root) imax = 1 + np.argsort(energies)[-1] self.emax = energies[imax - 1] tangent1 = find_mic(images[1].get_positions() - images[0].get_positions(), images[0].get_cell(), images[0].pbc)[0] for i in range(1, self.nimages - 1): tangent2 = find_mic(images[i + 1].get_positions() - images[i].get_positions(), images[i].get_cell(), images[i].pbc)[0] if i < imax: tangent = tangent2 elif i > imax: tangent = tangent1 else: tangent = tangent1 + tangent2 tt = np.vdot(tangent, tangent) f = forces[i - 1] ft = np.vdot(f, tangent) if i == imax and self.climb: f -= 2 * ft / tt * tangent else: f -= ft / tt * tangent f -= np.vdot(tangent1 * self.k[i - 1] - tangent2 * self.k[i], tangent) / tt * tangent tangent1 = tangent2 return forces.reshape((-1, 3))
def interp_PES(images, flatten=True): """Interpolate between an series of images. The images should be specified by a dictionary, with keys, file The name of the file to read the image from. format The format of the file for the image, according to the requirement of the ``read`` function in the ASE IO module. atoms The ASE atoms object for the image point. If it is present, the file will not be attempted to be read. dupl The number of duplication of the image. interp The dictionary of arguments to the function :py:func:`interp_by_multiscale_neb`. For each of the images, it gives the method of interpolation between the current image and its successor. No interpolation is performed in the absence of it. reorient If the image point should be reorientated with respect to the previous image point. """ FILE = 'file' FORMAT = 'format' ATOMS = 'atoms' DUPL = 'dupl' INTERP = 'interp' REORIENT = 'reorient' # To hold the duplication of each image as well as the path to its # successor. FRAMES = '_frames' # Read the images if it is needed. imgs = [] for i in images: img = dict(i) if ATOMS in i: atoms = i[ATOMS] else: if FORMAT not in i: atoms = read(i[FILE]) else: atoms = read(i[FILE], format=i[FORMAT]) if REORIENT in i: minimize_rotation_and_translation(imgs[-1][ATOMS], atoms) img[ATOMS] = atoms img[FRAMES] = [atoms, ] imgs.append(img) continue # Make duplication for the images. for i in imgs: if DUPL in i: i[FRAMES].extend(aug_point(i[ATOMS], i[DUPL])) continue # Interpolate between the stationary points. for curr, succ in zip(imgs, imgs[1:]): if INTERP not in curr: continue else: curr[FRAMES].extend(interp_by_multiscale_neb( curr[ATOMS], succ[ATOMS], **curr[INTERP] )) frame_sets = (i[FRAMES] for i in imgs) if flatten: return list(itertools.chain.from_iterable(frame_sets)) else: return list(frame_sets)
def get_forces(self): """Evaluate and return the forces.""" images = self.images forces = np.empty(((self.nimages - 2), self.natoms, 3)) energies = np.empty(self.nimages) if self.remove_rotation_and_translation: # Remove translation and rotation between # images before computing forces: for i in range(1, self.nimages): minimize_rotation_and_translation(images[i - 1], images[i]) if self.method != 'aseneb': energies[0] = images[0].get_potential_energy() energies[-1] = images[-1].get_potential_energy() if not self.parallel: # Do all images - one at a time: for i in range(1, self.nimages - 1): energies[i] = images[i].get_potential_energy() forces[i - 1] = images[i].get_forces() elif self.world.size == 1: def run(image, energies, forces): energies[:] = image.get_potential_energy() forces[:] = image.get_forces() threads = [threading.Thread(target=run, args=(images[i], energies[i:i + 1], forces[i - 1:i])) for i in range(1, self.nimages - 1)] for thread in threads: thread.start() for thread in threads: thread.join() else: # Parallelize over images: i = self.world.rank * (self.nimages - 2) // self.world.size + 1 try: energies[i] = images[i].get_potential_energy() forces[i - 1] = images[i].get_forces() except: # Make sure other images also fail: error = self.world.sum(1.0) raise else: error = self.world.sum(0.0) if error: raise RuntimeError('Parallel NEB failed!') for i in range(1, self.nimages - 1): root = (i - 1) * self.world.size // (self.nimages - 2) self.world.broadcast(energies[i:i + 1], root) self.world.broadcast(forces[i - 1], root) imax = 1 + np.argsort(energies[1:-1])[-1] self.emax = energies[imax] t1 = find_mic(images[1].get_positions() - images[0].get_positions(), images[0].get_cell(), images[0].pbc)[0] if self.method == 'eb': beeline = (images[self.nimages - 1].get_positions() - images[0].get_positions()) beelinelength = np.linalg.norm(beeline) eqlength = beelinelength / (self.nimages - 1) nt1 = np.linalg.norm(t1) for i in range(1, self.nimages - 1): t2 = find_mic(images[i + 1].get_positions() - images[i].get_positions(), images[i].get_cell(), images[i].pbc)[0] nt2 = np.linalg.norm(t2) if self.method == 'eb': # Tangents are bisections of spring-directions # (formula C8 of paper III) tangent = t1 / nt1 + t2 / nt2 # Normalize the tangent vector tangent /= np.linalg.norm(tangent) elif self.method == 'improvedtangent': # Tangents are improved according to formulas 8, 9, 10, # and 11 of paper I. if energies[i + 1] > energies[i] > energies[i - 1]: tangent = t2.copy() elif energies[i + 1] < energies[i] < energies[i - 1]: tangent = t1.copy() else: deltavmax = max(abs(energies[i + 1] - energies[i]), abs(energies[i - 1] - energies[i])) deltavmin = min(abs(energies[i + 1] - energies[i]), abs(energies[i - 1] - energies[i])) if energies[i + 1] > energies[i - 1]: tangent = t2 * deltavmax + t1 * deltavmin else: tangent = t2 * deltavmin + t1 * deltavmax # Normalize the tangent vector tangent /= np.linalg.norm(tangent) else: if i < imax: tangent = t2 elif i > imax: tangent = t1 else: tangent = t1 + t2 tt = np.vdot(tangent, tangent) f = forces[i - 1] ft = np.vdot(f, tangent) if i == imax and self.climb: # imax not affected by the spring forces. The full force # with component along the elestic band converted # (formula 5 of Paper II) if self.method == 'aseneb': f -= 2 * ft / tt * tangent else: f -= 2 * ft * tangent elif self.method == 'eb': f -= ft * tangent # Spring forces # (formula C1, C5, C6 and C7 of Paper III) f1 = -(nt1 - eqlength) * t1 / nt1 * self.k[i - 1] f2 = (nt2 - eqlength) * t2 / nt2 * self.k[i] if self.climb and abs(i - imax) == 1: deltavmax = max(abs(energies[i + 1] - energies[i]), abs(energies[i - 1] - energies[i])) deltavmin = min(abs(energies[i + 1] - energies[i]), abs(energies[i - 1] - energies[i])) f += (f1 + f2) * deltavmin / deltavmax else: f += f1 + f2 elif self.method == 'improvedtangent': f -= ft * tangent # Improved parallel spring force (formula 12 of paper I) f += (nt2 * self.k[i] - nt1 * self.k[i - 1]) * tangent else: f -= ft / tt * tangent f -= np.vdot(t1 * self.k[i - 1] - t2 * self.k[i], tangent) / tt * tangent t1 = t2 nt1 = nt2 return forces.reshape((-1, 3))