Coverage for /builds/ericyuan00000/ase/ase/utils/forcecurve.py: 78.63%
117 statements
« prev ^ index » next coverage.py v7.5.3, created at 2025-06-18 01:20 +0000
« prev ^ index » next coverage.py v7.5.3, created at 2025-06-18 01:20 +0000
1# fmt: off
3from collections import namedtuple
5import numpy as np
7from ase.geometry import find_mic
10def fit_raw(energies, forces, positions, cell=None, pbc=None):
11 """Calculates parameters for fitting images to a band, as for
12 a NEB plot."""
13 energies = np.array(energies) - energies[0]
14 n_images = len(energies)
15 fit_energies = np.empty((n_images - 1) * 20 + 1)
16 fit_path = np.empty((n_images - 1) * 20 + 1)
18 path = [0]
19 for i in range(n_images - 1):
20 dR = positions[i + 1] - positions[i]
21 if cell is not None and pbc is not None:
22 dR, _ = find_mic(dR, cell, pbc)
23 path.append(path[i] + np.sqrt((dR**2).sum()))
25 lines = [] # tangent lines
26 lastslope = None
27 for i in range(n_images):
28 if i == 0:
29 direction = positions[i + 1] - positions[i]
30 dpath = 0.5 * path[1]
31 elif i == n_images - 1:
32 direction = positions[-1] - positions[-2]
33 dpath = 0.5 * (path[-1] - path[-2])
34 else:
35 direction = positions[i + 1] - positions[i - 1]
36 dpath = 0.25 * (path[i + 1] - path[i - 1])
38 direction /= np.linalg.norm(direction)
39 slope = -(forces[i] * direction).sum()
40 x = np.linspace(path[i] - dpath, path[i] + dpath, 3)
41 y = energies[i] + slope * (x - path[i])
42 lines.append((x, y))
44 if i > 0:
45 s0 = path[i - 1]
46 s1 = path[i]
47 x = np.linspace(s0, s1, 20, endpoint=False)
48 c = np.linalg.solve(np.array([(1, s0, s0**2, s0**3),
49 (1, s1, s1**2, s1**3),
50 (0, 1, 2 * s0, 3 * s0**2),
51 (0, 1, 2 * s1, 3 * s1**2)]),
52 np.array([energies[i - 1], energies[i],
53 lastslope, slope]))
54 y = c[0] + x * (c[1] + x * (c[2] + x * c[3]))
55 fit_path[(i - 1) * 20:i * 20] = x
56 fit_energies[(i - 1) * 20:i * 20] = y
58 lastslope = slope
60 fit_path[-1] = path[-1]
61 fit_energies[-1] = energies[-1]
62 return ForceFit(path, energies, fit_path, fit_energies, lines)
65class ForceFit(namedtuple('ForceFit', ['path', 'energies', 'fit_path',
66 'fit_energies', 'lines'])):
67 """Data container to hold fitting parameters for force curves."""
69 def plot(self, ax=None):
70 import matplotlib.pyplot as plt
71 if ax is None:
72 ax = plt.gca()
74 ax.plot(self.path, self.energies, 'o')
75 for x, y in self.lines:
76 ax.plot(x, y, '-g')
77 ax.plot(self.fit_path, self.fit_energies, 'k-')
78 ax.set_xlabel(r'path [Å]')
79 ax.set_ylabel('energy [eV]')
80 Ef = max(self.energies) - self.energies[0]
81 Er = max(self.energies) - self.energies[-1]
82 dE = self.energies[-1] - self.energies[0]
83 ax.set_title(r'$E_\mathrm{{f}} \approx$ {:.3f} eV; '
84 r'$E_\mathrm{{r}} \approx$ {:.3f} eV; '
85 r'$\Delta E$ = {:.3f} eV'.format(Ef, Er, dE))
86 return ax
89def fit_images(images):
90 """Fits a series of images with a smoothed line for producing a standard
91 NEB plot. Returns a `ForceFit` data structure; the plot can be produced
92 by calling the `plot` method of `ForceFit`."""
93 R = [atoms.positions for atoms in images]
94 E = [atoms.get_potential_energy() for atoms in images]
95 F = [atoms.get_forces() for atoms in images] # XXX force consistent???
96 A = images[0].cell
97 pbc = images[0].pbc
98 return fit_raw(E, F, R, A, pbc)
101def force_curve(images, ax=None):
102 """Plot energies and forces as a function of accumulated displacements.
104 This is for testing whether a calculator's forces are consistent with
105 the energies on a set of geometries where energies and forces are
106 available."""
108 if ax is None:
109 import matplotlib.pyplot as plt
110 ax = plt.gca()
112 nim = len(images)
114 accumulated_distances = []
115 accumulated_distance = 0.0
117 # XXX force_consistent=True will work with some calculators,
118 # but won't work if images were loaded from a trajectory.
119 energies = [atoms.get_potential_energy()
120 for atoms in images]
122 for i in range(nim):
123 atoms = images[i]
124 f_ac = atoms.get_forces()
126 if i < nim - 1:
127 rightpos = images[i + 1].positions
128 else:
129 rightpos = atoms.positions
131 if i > 0:
132 leftpos = images[i - 1].positions
133 else:
134 leftpos = atoms.positions
136 disp_ac, _ = find_mic(rightpos - leftpos, cell=atoms.cell,
137 pbc=atoms.pbc)
139 def total_displacement(disp):
140 disp_a = (disp**2).sum(axis=1)**.5
141 return sum(disp_a)
143 dE_fdotr = -0.5 * np.vdot(f_ac.ravel(), disp_ac.ravel())
145 linescale = 0.45
147 disp = 0.5 * total_displacement(disp_ac)
149 if i == 0 or i == nim - 1:
150 disp *= 2
151 dE_fdotr *= 2
153 x1 = accumulated_distance - disp * linescale
154 x2 = accumulated_distance + disp * linescale
155 y1 = energies[i] - dE_fdotr * linescale
156 y2 = energies[i] + dE_fdotr * linescale
158 ax.plot([x1, x2], [y1, y2], 'b-')
159 ax.plot(accumulated_distance, energies[i], 'bo')
160 ax.set_ylabel('Energy [eV]')
161 ax.set_xlabel('Accumulative distance [Å]')
162 accumulated_distances.append(accumulated_distance)
163 accumulated_distance += total_displacement(rightpos - atoms.positions)
165 ax.plot(accumulated_distances, energies, ':', zorder=-1, color='k')
166 return ax
169def plotfromfile(*fnames):
170 from ase.io import read
171 nplots = len(fnames)
173 for i, fname in enumerate(fnames):
174 images = read(fname, ':')
175 import matplotlib.pyplot as plt
176 plt.subplot(nplots, 1, 1 + i)
177 force_curve(images)
178 plt.show()
181if __name__ == '__main__':
182 import sys
183 fnames = sys.argv[1:]
184 plotfromfile(*fnames)