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