Skip to content

Commit ab7c768

Browse files
authored
Merge pull request #201 from duartegroup/v1.3.3
V1.3.3
2 parents 6a47099 + 184b717 commit ab7c768

18 files changed

+306
-106
lines changed

autode/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
- Merge when tests pass
3939
"""
4040

41-
__version__ = "1.3.2"
41+
__version__ = "1.3.3"
4242

4343

4444
__all__ = [

autode/calculations/executors.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,7 @@ def run(self) -> None:
360360
type_ = PRFOptimiser if self._calc_is_ts_opt else CRFOptimiser
361361

362362
self.optimiser = type_(
363-
init_alpha=0.1, # Å
363+
init_alpha=self._step_size,
364364
maxiter=self._max_opt_cycles,
365365
etol=self.etol,
366366
gtol=self.gtol,
@@ -433,7 +433,11 @@ def _max_opt_cycles(self) -> int:
433433
if isinstance(kwd, kws.MaxOptCycles)
434434
)
435435
except StopIteration:
436-
return 30
436+
return 50
437+
438+
@property
439+
def _step_size(self) -> float:
440+
return 0.05 if self._calc_is_ts_opt else 0.1
437441

438442
@property
439443
def _opt_trajectory_name(self) -> str:

autode/config.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,7 @@ class NWChem:
305305
grad=[def2svp, pbe0, "task dft gradient"],
306306
low_sp=[def2svp, pbe0, "task dft energy"],
307307
opt=[def2svp, pbe0, MaxOptCycles(100), "task dft gradient"],
308-
opt_ts=[def2svp, pbe0, "task dft gradient"],
308+
opt_ts=[def2svp, pbe0, MaxOptCycles(50), "task dft gradient"],
309309
hess=[def2svp, pbe0, "task dft freq"],
310310
sp=[def2tzvp, pbe0, "task dft energy"],
311311
ecp=def2ecp,

autode/opt/coordinates/cartesian.py

+6
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,9 @@ def to(self, value: str) -> OptCoordinates:
8989
raise ValueError(
9090
f"Cannot convert Cartesian coordinates to {value}"
9191
)
92+
93+
@property
94+
def expected_number_of_dof(self) -> int:
95+
"""Expected number of degrees of freedom for the system"""
96+
n_atoms = len(self.flatten()) // 3
97+
return 3 * n_atoms - 6

autode/opt/coordinates/dic.py

+18-1
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,13 @@ def _calc_U(primitives: PIC, x: "CartesianCoordinates") -> np.ndarray:
6969
# of 3N - 6 non-redundant internals for a system of N atoms
7070
idxs = np.where(np.abs(lambd) > 1e-10)[0]
7171

72+
if len(idxs) < x.expected_number_of_dof:
73+
raise RuntimeError(
74+
"Failed to create a complete set of delocalised internal "
75+
f"coordinates. {len(idxs)} < 3 N_atoms - 6. Likely due to "
76+
f"missing primitives"
77+
)
78+
7279
logger.info(f"Removed {len(lambd) - len(idxs)} redundant vectors")
7380
return u[:, idxs]
7481

@@ -248,6 +255,16 @@ def iadd(
248255
def _allow_unconverged_back_transform(self) -> bool:
249256
return True
250257

258+
@property
259+
def active_indexes(self) -> List[int]:
260+
"""A list of indexes for the active modes in this coordinate set"""
261+
return list(range(len(self)))
262+
263+
@property
264+
def inactive_indexes(self) -> List[int]:
265+
"""A list of indexes for the non-active modes in this coordinate set"""
266+
return []
267+
251268

252269
class DICWithConstraints(DIC):
253270
r"""
@@ -434,7 +451,7 @@ def _schmidt_orthogonalise(arr: np.ndarray, *indexes: int) -> np.ndarray:
434451
provide pure primitive coordinates, which can then be constrained simply
435452
"""
436453
logger.info(
437-
f"Schmidt-orthogonalizing. Using {indexes} as " f"orthonormal vectors"
454+
f"Schmidt-orthogonalizing. Using {indexes} as orthonormal vectors"
438455
)
439456

440457
u = np.zeros_like(arr)

autode/opt/optimisers/base.py

+42-6
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1+
import os.path
12
import numpy as np
23

34
from abc import ABC, abstractmethod
45
from typing import Union, Optional, Callable, Any
56
from autode.log import logger
67
from autode.utils import NumericStringDict
78
from autode.config import Config
8-
from autode.values import GradientRMS, PotentialEnergy
9+
from autode.values import GradientRMS, PotentialEnergy, method_string
910
from autode.opt.coordinates.base import OptCoordinates
1011
from autode.opt.optimisers.hessian_update import NullUpdate
12+
from autode.exceptions import CalculationException
1113

1214

1315
class BaseOptimiser(ABC):
@@ -213,7 +215,7 @@ def _update_gradient_and_energy(self) -> None:
213215
grad.clean_up(force=True, everything=True)
214216

215217
if self._species.gradient is None:
216-
raise RuntimeError(
218+
raise CalculationException(
217219
"Calculation failed to calculate a gradient. "
218220
"Cannot continue!"
219221
)
@@ -227,16 +229,43 @@ def _update_hessian_gradient_and_energy(self) -> None:
227229
Update the energy, gradient and Hessian using the method. Will
228230
transform from the current coordinates type to Cartesian coordinates
229231
to perform the calculation, then back. Uses a numerical Hessian if
230-
analytic Hessians are not implemented for this method
232+
analytic Hessians are not implemented for this method. Does not
233+
perform a Hessian evaluation if the molecule's energy is evaluated
234+
at the same level of theory that would be used for the Hessian
235+
evaluation.
231236
232237
-----------------------------------------------------------------------
233238
Raises:
234239
(autode.exceptions.CalculationException):
235240
"""
241+
should_calc_hessian = True
242+
243+
if (
244+
_energy_method_string(self._species)
245+
== method_string(self._method, self._method.keywords.hess)
246+
and self._species.hessian is not None
247+
):
248+
logger.info(
249+
"Have a calculated the energy at the same level of "
250+
"theory as this optimisation and a present Hessian. "
251+
"Not calculating a new Hessian"
252+
)
253+
should_calc_hessian = False
254+
236255
self._update_gradient_and_energy()
237256

257+
if should_calc_hessian:
258+
self._update_hessian()
259+
else:
260+
self._coords.update_h_from_cart_h(
261+
self._species.hessian.to("Ha Å^-2")
262+
)
263+
return None
264+
265+
def _update_hessian(self) -> None:
266+
"""Update the Hessian of a species"""
238267
species = self._species.new_species(
239-
name=f"{self._species.name}" f"_opt_{self.iteration}"
268+
name=f"{self._species.name}_opt_{self.iteration}"
240269
)
241270
species.coordinates = self._coords.to("cartesian")
242271

@@ -247,8 +276,7 @@ def _update_hessian_gradient_and_energy(self) -> None:
247276
)
248277

249278
self._species.hessian = species.hessian.copy()
250-
self._coords.update_h_from_cart_h(self._species.hessian)
251-
return None
279+
self._coords.update_h_from_cart_h(self._species.hessian.to("Ha Å^-2"))
252280

253281
@property
254282
def _space_has_degrees_of_freedom(self) -> bool:
@@ -547,6 +575,10 @@ def save(self, filename: str) -> None:
547575
f" maxiter = {self._maxiter}"
548576
)
549577

578+
if os.path.exists(filename):
579+
logger.warning(f"FIle {filename} existed. Overwriting")
580+
open(filename, "w").close()
581+
550582
for i, coordinates in enumerate(self._history):
551583

552584
energy = coordinates.e
@@ -839,3 +871,7 @@ def __call__(self, coordinates: OptCoordinates) -> Any:
839871

840872
logger.info("Calling callback function")
841873
return self._f(coordinates, **self._kwargs)
874+
875+
876+
def _energy_method_string(species: "Species") -> str:
877+
return "" if species.energy is None else species.energy.method_str

autode/opt/optimisers/crfo.py

+20-2
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,18 @@ def _build_internal_coordinates(self):
117117
)
118118

119119
cartesian_coords = CartesianCoordinates(self._species.coordinates)
120+
primitives = self._primitives
121+
122+
if len(primitives) < cartesian_coords.expected_number_of_dof:
123+
logger.info(
124+
"Had an incomplete set of primitives. Adding "
125+
"additional distances"
126+
)
127+
for i, j in combinations(range(self._species.n_atoms), 2):
128+
primitives.append(Distance(i, j))
129+
120130
self._coords = DICWithConstraints.from_cartesian(
121-
x=cartesian_coords, primitives=self._primitives
131+
x=cartesian_coords, primitives=primitives
122132
)
123133
self._coords.zero_lagrangian_multipliers()
124134
return None
@@ -127,7 +137,15 @@ def _build_internal_coordinates(self):
127137
def _primitives(self) -> PIC:
128138
"""Primitive internal coordinates in this molecule"""
129139
logger.info("Generating primitive internal coordinates")
130-
graph = self._species.graph
140+
graph = self._species.graph.copy()
141+
142+
# Any distance constraints should also count as 'bonds' when forming
143+
# the set of primitive internal coordinates, so that there is a
144+
# single molecule if those distances are approaching dissociation
145+
if self._species.constraints.distance is not None:
146+
logger.info("Adding distance constraints as primitives")
147+
for (i, j) in self._species.constraints.distance:
148+
graph.add_edge(i, j)
131149

132150
pic = PIC()
133151

autode/opt/optimisers/prfo.py

+38-62
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
"""Partitioned rational function optimisation"""
22
import numpy as np
33
from autode.log import logger
4-
from autode.opt.coordinates import CartesianCoordinates
5-
from autode.opt.optimisers.rfo import RFOptimiser
4+
from autode.opt.optimisers.crfo import CRFOptimiser
65
from autode.opt.optimisers.hessian_update import BofillUpdate
6+
from autode.opt.coordinates.cartesian import CartesianCoordinates
7+
from autode.exceptions import CalculationException
78

89

9-
class PRFOptimiser(RFOptimiser):
10+
class PRFOptimiser(CRFOptimiser):
1011
def __init__(
11-
self, init_alpha: float = 0.1, imag_mode_idx: int = 0, *args, **kwargs
12+
self,
13+
init_alpha: float = 0.05,
14+
recalc_hessian_every: int = 10,
15+
*args,
16+
**kwargs,
1217
):
1318
"""
1419
Partitioned rational function optimiser (PRFO) using a maximum step
@@ -17,7 +22,7 @@ def __init__(
1722
1823
-----------------------------------------------------------------------
1924
Arguments:
20-
init_alpha: Maximum step size
25+
init_alpha: Maximum step size (Å)
2126
2227
imag_mode_idx: Index of the imaginary mode to follow. Default = 0,
2328
the most imaginary mode (i.e. most negative
@@ -29,86 +34,57 @@ def __init__(
2934
super().__init__(*args, **kwargs)
3035

3136
self.alpha = float(init_alpha)
32-
self.imag_mode_idx = int(imag_mode_idx)
33-
37+
self.recalc_hessian_every = int(recalc_hessian_every)
3438
self._hessian_update_types = [BofillUpdate]
3539

3640
def _step(self) -> None:
3741
"""Partitioned rational function step"""
3842

39-
self._coords.h = self._updated_h()
40-
41-
# Eigenvalues (\tilde{H}_kk in ref [1]) and eigenvectors (V in ref [1])
42-
# of the Hessian matrix
43-
lmda, v = np.linalg.eigh(self._coords.h)
43+
if self.should_calculate_hessian:
44+
self._update_hessian()
45+
else:
46+
self._coords.h = self._updated_h()
4447

45-
if np.min(lmda) > 0:
46-
raise RuntimeError(
47-
"Hessian had no negative eigenvalues, cannot " "follow to a TS"
48-
)
48+
idxs = list(range(len(self._coords)))
4949

50+
b, u = np.linalg.eigh(self._coords.h[:, idxs][idxs, :])
51+
n_negative_eigenvalues = sum(lmda < 0 for lmda in b)
5052
logger.info(
51-
f"Maximising along mode {self.imag_mode_idx} with "
52-
f"λ={lmda[self.imag_mode_idx]:.4f}"
53-
)
54-
55-
# Gradient in the eigenbasis of the Hessian. egn 49 in ref. 50
56-
g_tilde = np.matmul(v.T, self._coords.g)
57-
58-
# Initialised step in the Hessian eigenbasis
59-
s_tilde = np.zeros_like(g_tilde)
60-
61-
# For a step in Cartesian coordinates the Hessian will have zero
62-
# eigenvalues for translation/rotation - keep track of them
63-
non_zero_lmda = np.where(np.abs(lmda) > 1e-8)[0]
64-
65-
# Augmented Hessian 1 along the imaginary mode to maximise, with the
66-
# form (see eqn. 59 in ref [1]):
67-
# (\tilde{H}_11 \tilde{g}_1) (\tilde{s}_1) = (\tilde{s}_1)
68-
# ( ) ( ) = ν_R ( )
69-
# (\tilde{g}_1 0 ) ( 1 ) ( 1 )
70-
#
71-
aug1 = np.array(
72-
[
73-
[lmda[self.imag_mode_idx], g_tilde[self.imag_mode_idx]],
74-
[g_tilde[self.imag_mode_idx], 0.0],
75-
]
53+
f"∇^2E has {n_negative_eigenvalues} negative "
54+
f"eigenvalue(s). Should have 1"
7655
)
77-
_, aug1_v = np.linalg.eigh(aug1)
7856

79-
# component of the step along the imaginary mode is the first element
80-
# of the eigenvector with the largest eigenvalue (1), scaled by the
81-
# final element
82-
s_tilde[self.imag_mode_idx] = aug1_v[0, 1] / aug1_v[1, 1]
57+
if n_negative_eigenvalues < 1:
58+
raise CalculationException("Lost imaginary (TS) mode")
8359

84-
# Augmented Hessian along all other modes with non-zero eigenvalues,
85-
# that are also not the imaginary mode to be followed
86-
non_mode_lmda = np.delete(non_zero_lmda, [self.imag_mode_idx])
60+
f = u.T.dot(self._coords.g[idxs])
61+
lambda_p = self._lambda_p_from_eigvals_and_gradient(b, f)
62+
lambda_n = self._lambda_n_from_eigvals_and_gradient(b, f)
63+
logger.info(f"Calculated λ_p=+{lambda_p:.8f}, λ_n={lambda_n:.8f}")
8764

88-
# see eqn. 60 in ref. [1] for the structure of this matrix!
89-
augn = np.diag(np.concatenate((lmda[non_mode_lmda], np.zeros(1))))
90-
augn[:-1, -1] = g_tilde[non_mode_lmda]
91-
augn[-1, :-1] = g_tilde[non_mode_lmda]
65+
delta_s = np.zeros(shape=(len(idxs),))
66+
delta_s -= f[0] * u[:, 0] / (b[0] - lambda_p)
9267

93-
_, augn_v = np.linalg.eigh(augn)
68+
for j in range(1, len(idxs)):
69+
delta_s -= f[j] * u[:, j] / (b[j] - lambda_n)
9470

95-
# The step along all other components is then the all but the final
96-
# component of the eigenvector with the smallest eigenvalue (0)
97-
s_tilde[non_mode_lmda] = augn_v[:-1, 0] / augn_v[-1, 0]
98-
99-
# Transform back from the eigenbasis with eqn. 52 in ref [1]
100-
delta_s = np.matmul(v, s_tilde)
101-
102-
self._take_step_within_trust_radius(delta_s)
71+
_ = self._take_step_within_trust_radius(delta_s)
10372
return None
10473

10574
def _initialise_run(self) -> None:
10675
"""
10776
Initialise running a partitioned rational function optimisation by
10877
setting the coordinates and Hessian
10978
"""
79+
# self._build_internal_coordinates()
11080
self._coords = CartesianCoordinates(self._species.coordinates).to(
11181
"dic"
11282
)
11383
self._update_hessian_gradient_and_energy()
11484
return None
85+
86+
@property
87+
def should_calculate_hessian(self) -> bool:
88+
"""Should an explicit Hessian calculation be performed?"""
89+
n = self.iteration
90+
return n > 1 and n % self.recalc_hessian_every == 0

0 commit comments

Comments
 (0)