1+ import numpy as np
2+ import scipy .linalg
3+ from multioptpy .Optimizer .rsirfo import RSIRFO
4+
5+ class CRSIRFO (RSIRFO ):
6+ def __init__ (self , constraints = None , ** config ):
7+ """
8+ Constrained RS-I-RFO Optimizer (CRS-I-RFO)
9+ """
10+ super ().__init__ (** config )
11+ self .constraints_obj = constraints
12+ self .null_space_basis = None
13+ self .svd_threshold = config .get ("svd_threshold" , 1e-5 )
14+
15+ def _get_null_space_basis (self , geom ):
16+ if self .constraints_obj is None :
17+ return np .eye (len (geom ) * 3 )
18+
19+ geom_reshaped = geom .reshape (- 1 , 3 )
20+ B_mat = self .constraints_obj ._get_all_constraint_vectors (geom_reshaped )
21+
22+ if B_mat is None or len (B_mat ) == 0 :
23+ return np .eye (len (geom ) * 3 )
24+
25+ norms = np .linalg .norm (B_mat , axis = 1 )
26+ norms [norms < 1e-12 ] = 1.0
27+ B_mat_normalized = B_mat / norms [:, np .newaxis ]
28+
29+ try :
30+ U , S , Vt = scipy .linalg .svd (B_mat_normalized .T , full_matrices = True )
31+ max_s = S [0 ] if len (S ) > 0 else 1.0
32+ threshold = max (self .svd_threshold , max_s * 1e-6 )
33+ rank = np .sum (S > threshold )
34+ null_space_basis = U [:, rank :]
35+
36+ if null_space_basis .shape [1 ] == 0 :
37+ self .log ("Warning: System is fully constrained." , force = True )
38+ return np .zeros ((len (geom )* 3 , 0 ))
39+
40+ return null_space_basis
41+
42+ except np .linalg .LinAlgError :
43+ return np .eye (len (geom ) * 3 )
44+
45+ def run (self , geom_num_list , B_g , pre_B_g = [], pre_geom = [], B_e = 0.0 , pre_B_e = 0.0 , pre_move_vector = [], initial_geom_num_list = [], g = [], pre_g = []):
46+ self .log (f"\n { '=' * 50 } \n CRS-I-RFO Iteration { self .iteration } \n { '=' * 50 } " , force = True )
47+
48+ if self .Initialization :
49+ self .prev_eigvec_min = None
50+ self .prev_eigvec_size = None
51+ self .predicted_energy_changes = []
52+ self .actual_energy_changes = []
53+ self .prev_geometry = None
54+ self .prev_gradient = None
55+ self .prev_energy = None
56+ self .converged = False
57+ self .iteration = 0
58+ self .Initialization = False
59+
60+ # --- 0. SHAKE-like Correction & Gradient Transport ---
61+ gradient_full = np .asarray (B_g ).ravel ()
62+ original_shape = geom_num_list .shape
63+ geom_flat = geom_num_list .ravel ()
64+
65+ if self .constraints_obj is not None :
66+ geom_reshaped = geom_num_list .reshape (- 1 , 3 )
67+ corrected_geom_3d = self .constraints_obj .adjust_init_coord (geom_reshaped )
68+ corrected_geom_flat = corrected_geom_3d .ravel ()
69+
70+ shake_displacement = corrected_geom_flat - geom_flat
71+ diff_norm = np .linalg .norm (shake_displacement )
72+
73+ if diff_norm > 1e-6 :
74+ self .log (f"SHAKE Correction: { diff_norm :.6e} " , force = True )
75+ H_eff = self .hessian
76+ if self .bias_hessian is not None :
77+ H_eff += self .bias_hessian
78+ grad_correction = np .dot (H_eff , shake_displacement )
79+ gradient_full += grad_correction
80+
81+ geom_num_list = corrected_geom_3d .reshape (original_shape )
82+
83+ # --- 1. Hessian Update ---
84+ if self .prev_geometry is not None and self .prev_gradient is not None and len (pre_g ) > 0 and len (pre_geom ) > 0 :
85+ self .update_hessian (geom_num_list , g , pre_geom , pre_g )
86+
87+ hessian_full = self .hessian
88+ if self .bias_hessian is not None :
89+ hessian_full += self .bias_hessian
90+
91+ current_energy = B_e
92+
93+ # --- 2. Projection to Subspace ---
94+ U = self ._get_null_space_basis (geom_num_list .reshape (- 1 , 3 ))
95+
96+ if U .shape [1 ] == 0 :
97+ return np .zeros_like (gradient_full ).reshape (- 1 , 1 )
98+
99+ gradient_sub = np .dot (U .T , gradient_full )
100+ hessian_sub = np .dot (U .T , np .dot (hessian_full , U ))
101+
102+ subspace_dim = len (gradient_sub )
103+ grad_sub_norm = np .linalg .norm (gradient_sub )
104+
105+ self .log (f"Subspace Dim: { subspace_dim } , Projected Grad Norm: { grad_sub_norm :.6e} " , force = True )
106+
107+ # === CRITICAL FIX: Explicit Convergence Check in Subspace ===
108+ # If the projected gradient is effectively zero, we are done.
109+ # Don't try to calculate RFO step, it will be numerically unstable.
110+ if grad_sub_norm < self .gradient_norm_threshold :
111+ self .log (f"*** CONVERGED in Subspace (Grad: { grad_sub_norm :.6e} ) ***" , force = True )
112+ self .converged = True
113+
114+ # Reset history to clean state
115+ self .prev_geometry = geom_num_list
116+ self .prev_gradient = B_g
117+ self .prev_energy = current_energy
118+
119+ return np .zeros_like (gradient_full ).reshape (- 1 , 1 )
120+ # ============================================================
121+
122+ # --- 3. RFO in Subspace ---
123+ hessian_sub = 0.5 * (hessian_sub + hessian_sub .T )
124+
125+ eigvals_sub , eigvecs_sub = self .compute_eigendecomposition_with_shift (hessian_sub )
126+
127+ # Trust Radius
128+ if not self .Initialization and self .prev_energy is not None :
129+ actual_energy_change = B_e - self .prev_energy
130+ if len (self .actual_energy_changes ) >= 3 :
131+ self .actual_energy_changes .pop (0 )
132+ self .actual_energy_changes .append (actual_energy_change )
133+
134+ if self .predicted_energy_changes :
135+ min_eigval = eigvals_sub [0 ] if len (eigvals_sub ) > 0 else None
136+ self .adjust_trust_radius (
137+ actual_energy_change ,
138+ self .predicted_energy_changes [- 1 ],
139+ min_eigval ,
140+ grad_sub_norm
141+ )
142+
143+ P_rfo = np .eye (subspace_dim )
144+ root_num = 0
145+ i = 0
146+ while root_num < len (self .roots ) and i < len (eigvals_sub ):
147+ if np .abs (eigvals_sub [i ]) > 1e-10 :
148+ trans_vec = eigvecs_sub [:, i ]
149+ if self .NEB_mode :
150+ P_rfo -= np .outer (trans_vec , trans_vec )
151+ else :
152+ P_rfo -= 2 * np .outer (trans_vec , trans_vec )
153+ root_num += 1
154+ i += 1
155+
156+ H_star_sub = np .dot (P_rfo , hessian_sub )
157+ H_star_sub = 0.5 * (H_star_sub + H_star_sub .T )
158+ grad_star_sub = np .dot (P_rfo , gradient_sub )
159+
160+ eigvals_star , eigvecs_star = self .compute_eigendecomposition_with_shift (H_star_sub )
161+ eigvals_star , eigvecs_star = self .filter_small_eigvals (eigvals_star , eigvecs_star )
162+
163+ step_sub = self .get_rs_step (eigvals_star , eigvecs_star , grad_star_sub )
164+
165+ # --- 4. Reconstruct Step ---
166+ step_full = np .dot (U , step_sub )
167+
168+ predicted_energy_change = self .rfo_model (gradient_sub , hessian_sub , step_sub )
169+
170+ if len (self .predicted_energy_changes ) >= 3 :
171+ self .predicted_energy_changes .pop (0 )
172+ self .predicted_energy_changes .append (predicted_energy_change )
173+
174+ if self .actual_energy_changes and len (self .predicted_energy_changes ) > 1 :
175+ self .evaluate_step_quality ()
176+
177+ self .prev_geometry = geom_num_list
178+ self .prev_gradient = B_g
179+ self .prev_energy = current_energy
180+ self .iteration += 1
181+
182+ return - 1 * step_full .reshape (- 1 , 1 )
0 commit comments