Coverage for ase / optimize / basin.py: 97.65%
85 statements
« prev ^ index » next coverage.py v7.13.3, created at 2026-02-04 10:20 +0000
« prev ^ index » next coverage.py v7.13.3, created at 2026-02-04 10:20 +0000
1# fmt: off
3from typing import IO, Type, Union
5import numpy as np
7from ase import Atoms, units
8from ase.io.trajectory import Trajectory
9from ase.optimize.fire import FIRE
10from ase.optimize.optimize import Dynamics, Optimizer
11from ase.parallel import world
14class BasinHopping(Dynamics):
15 """Basin hopping algorithm.
17 After Wales and Doye, J. Phys. Chem. A, vol 101 (1997) 5111-5116
19 and
21 David J. Wales and Harold A. Scheraga, Science, Vol. 285, 1368 (1999)
22 """
24 def __init__(
25 self,
26 atoms: Atoms,
27 temperature: float = 100 * units.kB,
28 optimizer: Type[Optimizer] = FIRE,
29 fmax: float = 0.1,
30 dr: float = 0.1,
31 logfile: Union[IO, str] = '-',
32 trajectory: str = 'lowest.traj',
33 optimizer_logfile: str = '-',
34 local_minima_trajectory: str = 'local_minima.traj',
35 adjust_cm: bool = True,
36 ):
37 """Parameters:
39 atoms: Atoms object
40 The Atoms object to operate on.
42 trajectory: string
43 Trajectory file used to store optimisation path.
45 logfile: file object or str
46 If *logfile* is a string, a file with that name will be opened.
47 Use '-' for stdout.
48 """
49 self.kT = temperature
50 self.optimizer = optimizer
51 self.fmax = fmax
52 self.dr = dr
53 if adjust_cm:
54 self.cm = atoms.get_center_of_mass()
55 else:
56 self.cm = None
58 self.optimizer_logfile = optimizer_logfile
59 self.lm_trajectory = local_minima_trajectory
60 if isinstance(local_minima_trajectory, str):
61 self.lm_trajectory = self.closelater(
62 Trajectory(local_minima_trajectory, 'w', atoms))
64 Dynamics.__init__(self, atoms, logfile, trajectory)
65 self.initialize()
67 def todict(self):
68 d = {'type': 'optimization',
69 'optimizer': self.__class__.__name__,
70 'local-minima-optimizer': self.optimizer.__name__,
71 'temperature': self.kT,
72 'max-force': self.fmax,
73 'maximal-step-width': self.dr}
74 return d
76 def initialize(self):
77 positions = self.optimizable.get_x().reshape(-1, 3)
78 self.positions = np.zeros_like(positions)
79 self.Emin = self.get_energy(positions) or 1.e32
80 self.rmin = self.optimizable.get_x().reshape(-1, 3)
81 self.positions = self.optimizable.get_x().reshape(-1, 3)
82 self.call_observers()
83 self.log(-1, self.Emin, self.Emin)
85 def run(self, steps):
86 """Hop the basins for defined number of steps."""
88 ro = self.positions
89 Eo = self.get_energy(ro)
91 for step in range(steps):
92 En = None
93 while En is None:
94 rn = self.move(ro)
95 En = self.get_energy(rn)
97 if En < self.Emin:
98 # new minimum found
99 self.Emin = En
100 self.rmin = self.optimizable.get_x().reshape(-1, 3)
101 self.call_observers()
102 self.log(step, En, self.Emin)
104 accept = np.exp((Eo - En) / self.kT) > np.random.uniform()
105 if accept:
106 ro = rn.copy()
107 Eo = En
109 def log(self, step, En, Emin):
110 if self.logfile is None:
111 return
112 name = self.__class__.__name__
113 self.logfile.write('%s: step %d, energy %15.6f, emin %15.6f\n'
114 % (name, step, En, Emin))
116 def _atoms(self):
117 from ase.optimize.optimize import OptimizableAtoms
118 assert isinstance(self.optimizable, OptimizableAtoms)
119 # Some parts of the basin code cannot work on Filter objects.
120 # They evidently need an actual Atoms object - at least until
121 # someone changes the code so it doesn't need that.
122 return self.optimizable.atoms
124 def move(self, ro):
125 """Move atoms by a random step."""
126 atoms = self._atoms()
127 # displace coordinates
128 disp = np.random.uniform(-1., 1., (len(atoms), 3))
129 rn = ro + self.dr * disp
130 atoms.set_positions(rn)
131 if self.cm is not None:
132 cm = atoms.get_center_of_mass()
133 atoms.translate(self.cm - cm)
134 rn = atoms.get_positions()
135 world.broadcast(rn, 0)
136 atoms.set_positions(rn)
137 return atoms.get_positions()
139 def get_minimum(self):
140 """Return minimal energy and configuration."""
141 atoms = self._atoms().copy()
142 atoms.set_positions(self.rmin)
143 return self.Emin, atoms
145 def get_energy(self, positions):
146 """Return the energy of the nearest local minimum."""
147 if np.any(self.positions != positions):
148 self.positions = positions
149 self.optimizable.set_x(positions.ravel())
151 with self.optimizer(self.optimizable,
152 logfile=self.optimizer_logfile) as opt:
153 opt.run(fmax=self.fmax)
154 if self.lm_trajectory is not None:
155 self.lm_trajectory.write(self.optimizable)
157 self.energy = self.optimizable.get_value()
159 return self.energy