Coverage for /builds/ase/ase/ase/utils/forcecurve.py: 78.63%
117 statements
« prev ^ index » next coverage.py v7.5.3, created at 2025-08-02 00:12 +0000
« prev ^ index » next coverage.py v7.5.3, created at 2025-08-02 00:12 +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(
47 np.array(
48 [
49 (1, s0, s0**2, s0**3),
50 (1, s1, s1**2, s1**3),
51 (0, 1, 2 * s0, 3 * s0**2),
52 (0, 1, 2 * s1, 3 * s1**2),
53 ]
54 ),
55 np.array([energies[i - 1], energies[i], lastslope, slope]),
56 )
57 y = c[0] + x * (c[1] + x * (c[2] + x * c[3]))
58 fit_path[(i - 1) * 20 : i * 20] = x
59 fit_energies[(i - 1) * 20 : i * 20] = y
61 lastslope = slope
63 fit_path[-1] = path[-1]
64 fit_energies[-1] = energies[-1]
65 return ForceFit(path, energies, fit_path, fit_energies, lines)
68class ForceFit(
69 namedtuple(
70 'ForceFit', ['path', 'energies', 'fit_path', 'fit_energies', 'lines']
71 )
72):
73 """Data container to hold fitting parameters for force curves."""
75 def plot(self, ax=None):
76 import matplotlib.pyplot as plt
78 if ax is None:
79 ax = plt.gca()
81 ax.plot(self.path, self.energies, 'o')
82 for x, y in self.lines:
83 ax.plot(x, y, '-g')
84 ax.plot(self.fit_path, self.fit_energies, 'k-')
85 ax.set_xlabel(r'path [Å]')
86 ax.set_ylabel('energy [eV]')
87 Ef = max(self.energies) - self.energies[0]
88 Er = max(self.energies) - self.energies[-1]
89 dE = self.energies[-1] - self.energies[0]
90 ax.set_title(
91 r'$E_\mathrm{{f}} \approx$ {:.3f} eV; '
92 r'$E_\mathrm{{r}} \approx$ {:.3f} eV; '
93 r'$\Delta E$ = {:.3f} eV'.format(Ef, Er, dE)
94 )
95 return ax
98def fit_images(images):
99 """Fits a series of images with a smoothed line for producing a standard
100 NEB plot. Returns a `ForceFit` data structure; the plot can be produced
101 by calling the `plot` method of `ForceFit`."""
102 R = [atoms.positions for atoms in images]
103 E = [atoms.get_potential_energy() for atoms in images]
104 F = [atoms.get_forces() for atoms in images] # XXX force consistent???
105 A = images[0].cell
106 pbc = images[0].pbc
107 return fit_raw(E, F, R, A, pbc)
110def force_curve(images, ax=None):
111 """Plot energies and forces as a function of accumulated displacements.
113 This is for testing whether a calculator's forces are consistent with
114 the energies on a set of geometries where energies and forces are
115 available."""
117 if ax is None:
118 import matplotlib.pyplot as plt
120 ax = plt.gca()
122 nim = len(images)
124 accumulated_distances = []
125 accumulated_distance = 0.0
127 # XXX force_consistent=True will work with some calculators,
128 # but won't work if images were loaded from a trajectory.
129 energies = [atoms.get_potential_energy() for atoms in images]
131 for i in range(nim):
132 atoms = images[i]
133 f_ac = atoms.get_forces()
135 if i < nim - 1:
136 rightpos = images[i + 1].positions
137 else:
138 rightpos = atoms.positions
140 if i > 0:
141 leftpos = images[i - 1].positions
142 else:
143 leftpos = atoms.positions
145 disp_ac, _ = find_mic(
146 rightpos - leftpos, cell=atoms.cell, pbc=atoms.pbc
147 )
149 def total_displacement(disp):
150 disp_a = (disp**2).sum(axis=1) ** 0.5
151 return sum(disp_a)
153 dE_fdotr = -0.5 * np.vdot(f_ac.ravel(), disp_ac.ravel())
155 linescale = 0.45
157 disp = 0.5 * total_displacement(disp_ac)
159 if i == 0 or i == nim - 1:
160 disp *= 2
161 dE_fdotr *= 2
163 x1 = accumulated_distance - disp * linescale
164 x2 = accumulated_distance + disp * linescale
165 y1 = energies[i] - dE_fdotr * linescale
166 y2 = energies[i] + dE_fdotr * linescale
168 ax.plot([x1, x2], [y1, y2], 'b-')
169 ax.plot(accumulated_distance, energies[i], 'bo')
170 ax.set_ylabel('Energy [eV]')
171 ax.set_xlabel('Accumulative distance [Å]')
172 accumulated_distances.append(accumulated_distance)
173 accumulated_distance += total_displacement(rightpos - atoms.positions)
175 ax.plot(accumulated_distances, energies, ':', zorder=-1, color='k')
176 return ax
179def plotfromfile(*fnames):
180 from ase.io import read
182 nplots = len(fnames)
184 for i, fname in enumerate(fnames):
185 images = read(fname, ':')
186 import matplotlib.pyplot as plt
188 plt.subplot(nplots, 1, 1 + i)
189 force_curve(images)
190 plt.show()
193if __name__ == '__main__':
194 import sys
196 fnames = sys.argv[1:]
197 plotfromfile(*fnames)