1
1
"""Partitioned rational function optimisation"""
2
2
import numpy as np
3
3
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
6
5
from autode .opt .optimisers .hessian_update import BofillUpdate
6
+ from autode .opt .coordinates .cartesian import CartesianCoordinates
7
+ from autode .exceptions import CalculationException
7
8
8
9
9
- class PRFOptimiser (RFOptimiser ):
10
+ class PRFOptimiser (CRFOptimiser ):
10
11
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 ,
12
17
):
13
18
"""
14
19
Partitioned rational function optimiser (PRFO) using a maximum step
@@ -17,7 +22,7 @@ def __init__(
17
22
18
23
-----------------------------------------------------------------------
19
24
Arguments:
20
- init_alpha: Maximum step size
25
+ init_alpha: Maximum step size (Å)
21
26
22
27
imag_mode_idx: Index of the imaginary mode to follow. Default = 0,
23
28
the most imaginary mode (i.e. most negative
@@ -29,86 +34,57 @@ def __init__(
29
34
super ().__init__ (* args , ** kwargs )
30
35
31
36
self .alpha = float (init_alpha )
32
- self .imag_mode_idx = int (imag_mode_idx )
33
-
37
+ self .recalc_hessian_every = int (recalc_hessian_every )
34
38
self ._hessian_update_types = [BofillUpdate ]
35
39
36
40
def _step (self ) -> None :
37
41
"""Partitioned rational function step"""
38
42
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 ()
44
47
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 )))
49
49
50
+ b , u = np .linalg .eigh (self ._coords .h [:, idxs ][idxs , :])
51
+ n_negative_eigenvalues = sum (lmda < 0 for lmda in b )
50
52
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"
76
55
)
77
- _ , aug1_v = np .linalg .eigh (aug1 )
78
56
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" )
83
59
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} " )
87
64
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 )
92
67
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 )
94
70
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 )
103
72
return None
104
73
105
74
def _initialise_run (self ) -> None :
106
75
"""
107
76
Initialise running a partitioned rational function optimisation by
108
77
setting the coordinates and Hessian
109
78
"""
79
+ # self._build_internal_coordinates()
110
80
self ._coords = CartesianCoordinates (self ._species .coordinates ).to (
111
81
"dic"
112
82
)
113
83
self ._update_hessian_gradient_and_energy ()
114
84
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