MultiOptPy 1.20.2__py3-none-any.whl → 1.20.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,47 +1,140 @@
1
1
  import numpy as np
2
2
 
3
- #J. Chem. Phys. 157, 124107 (2022)
4
- #https://doi.org/10.1063/5.0102145
3
+ # J. Chem. Phys. 157, 124107 (2022)
4
+ # https://doi.org/10.1063/5.0102145
5
5
 
6
6
  class BITSSModelFunction:
7
7
  def __init__(self, geom_num_list_1, geom_num_list_2):
8
8
  self.f = 0.5
9
9
  self.alpha = 10.0
10
- self.beta = 0.1
11
- self.d = np.linalg.norm(geom_num_list_1 - geom_num_list_2)
10
+ # self.beta controls the strength of the distance constraint.
11
+ # Smaller beta -> Larger kappa_d -> Stronger attractive force.
12
+ # Default: 0.1 -> Modified: 0.02 for stronger constraint.
13
+ self.beta = 0.02
12
14
 
13
- return
15
+ # Initial distance
16
+ diff = geom_num_list_1 - geom_num_list_2
17
+ self.d = np.linalg.norm(diff)
14
18
 
19
+ # Initialize variables to avoid AttributeError if calc_hess/grad called before iter % 500 == 0
20
+ self.kappa_e = 0.0
21
+ self.kappa_d = 0.0
22
+ self.E_B = 0.0
23
+
24
+ # Store vector info for Hessian calculation
25
+ self.diff_vec = diff.reshape(-1, 1) # (3N, 1)
26
+ self.current_dist = self.d
27
+
15
28
  def calc_energy(self, energy_1, energy_2, geom_num_list_1, geom_num_list_2, gradient_1, gradient_2, iter):
16
- current_distance = np.linalg.norm(geom_num_list_1 - geom_num_list_2)
29
+ # Update distances
30
+ diff_vec = geom_num_list_1 - geom_num_list_2
31
+ current_distance = np.linalg.norm(diff_vec)
32
+
33
+ # Update parameters periodically
17
34
  if iter % 500 == 0:
18
-
19
35
  self.E_B = abs(energy_1 - energy_2)
36
+ # Avoid division by zero
20
37
  self.kappa_e = self.alpha / (2.0 * self.E_B + 1e-10)
21
38
 
22
- unit_vec = (geom_num_list_1 - geom_num_list_2) / current_distance
23
-
24
-
39
+ unit_vec = diff_vec / (current_distance + 1e-10)
25
40
 
26
- proj_grad_1 = gradient_1 * unit_vec * (-1)
27
- proj_grad_2 = gradient_2 * unit_vec
41
+ # Project gradients onto the distance vector direction
42
+ # grad_1 is (N, 3), unit_vec is (N, 3). Element-wise mult then sum gives dot product.
43
+ proj_grad_1 = np.sum(gradient_1 * (-1) * unit_vec)
44
+ proj_grad_2 = np.sum(gradient_2 * unit_vec)
28
45
 
29
- a = np.sqrt(np.linalg.norm(proj_grad_1) + np.linalg.norm(proj_grad_2)) / (2 ** 1.5 * self.beta * self.d + 1e-10)
46
+ # Eq. (5) logic
47
+ grad_norm_term = np.sqrt(proj_grad_1**2 + proj_grad_2**2)
48
+ a = grad_norm_term / (2 ** 1.5 * self.beta * self.d + 1e-10)
30
49
  b = self.E_B / (self.beta * self.d ** 2 + 1e-10)
31
50
  self.kappa_d = max(a, b)
32
- self.d = np.linalg.norm(geom_num_list_1 - geom_num_list_2)
51
+
52
+ # Reset target distance d to current distance at update step
53
+ self.d = current_distance
33
54
 
55
+ # Reduce target distance
34
56
  self.d = max((1.0 - self.f) * self.d, 1e-10)
35
- energy = energy_1 + energy_2 + self.kappa_e * (energy_1 + energy_2) ** 2 + self.kappa_d * (current_distance - self.d) ** 2
57
+
58
+ # Calculate BITSS Energy
59
+ # Formula: E1 + E2 + ke * (E1 - E2)^2 + kd * (d - d0)^2
60
+ energy = energy_1 + energy_2 + self.kappa_e * (energy_1 - energy_2) ** 2 + self.kappa_d * (current_distance - self.d) ** 2
36
61
 
37
62
  return energy
38
63
 
39
64
  def calc_grad(self, energy_1, energy_2, geom_num_list_1, geom_num_list_2, gradient_1, gradient_2):
65
+ # Calculate vector r = x1 - x2 and distance d
40
66
  current_vec = geom_num_list_1 - geom_num_list_2
41
67
  current_dist = np.linalg.norm(current_vec) + 1e-10
42
68
 
43
- bitss_grad_1 = gradient_1 + gradient_2 + 2.0 * self.kappa_e * (energy_1 - energy_2) * (gradient_1 - gradient_2) + current_vec * 2.0 * self.kappa_d * (current_dist - self.d) / current_dist
69
+ # Store for calc_hess (flattened for matrix ops)
70
+ self.diff_vec = current_vec.reshape(-1, 1)
71
+ self.current_dist = current_dist
72
+
73
+ # Common terms
74
+ delta_E = energy_1 - energy_2
75
+ dist_diff = current_dist - self.d
76
+
77
+ # Gradient term for distance: 2 * kd * (d - d0) * (r / d)
78
+ grad_dist_term = current_vec * 2.0 * self.kappa_d * dist_diff / current_dist
79
+
80
+ # Gradient term for energy: 2 * ke * (E1 - E2) * (g1 - g2)
81
+ # Total Gradient 1: g1 + 2*ke*dE*g1 + dist_term
82
+ bitss_grad_1 = gradient_1 * (1.0 + 2.0 * self.kappa_e * delta_E) + grad_dist_term
83
+
84
+ # Total Gradient 2: g2 + 2*ke*dE*(-g2) - dist_term (since d(r)/dx2 = -r/d)
85
+ bitss_grad_2 = gradient_2 * (1.0 - 2.0 * self.kappa_e * delta_E) - grad_dist_term
86
+
87
+ return bitss_grad_1, bitss_grad_2
44
88
 
45
- bitss_grad_2 = gradient_1 + gradient_2 + 2.0 * self.kappa_e * (energy_1 - energy_2) * (gradient_1 - gradient_2) - current_vec * 2.0 * self.kappa_d * (current_dist - self.d) / current_dist
89
+ def calc_hess(self, energy_1, energy_2, grad_1, grad_2, hess_1, hess_2):
90
+ """
91
+ Calculate the 6N x 6N Hessian matrix for BITSS.
92
+ H = [ H11 H12 ]
93
+ [ H21 H22 ]
94
+ """
95
+ # Ensure inputs are flattened (3N, 1) or (3N, 3N)
96
+ N3 = self.diff_vec.shape[0]
97
+ g1 = grad_1.reshape(N3, 1)
98
+ g2 = grad_2.reshape(N3, 1)
99
+
100
+ delta_E = energy_1 - energy_2
101
+ dist_diff = self.current_dist - self.d
102
+
103
+ # --- Distance Constraint Hessian Terms ---
104
+ # Vd = kd * (d - d0)^2
105
+ # P = r * r.T / d^2 (Projection onto bond axis)
106
+ r = self.diff_vec
107
+ d = self.current_dist
108
+ P = np.dot(r, r.T) / (d**2)
109
+ I = np.eye(N3)
110
+ # H_dist_block = 2*kd * [ P + (d-d0)/d * (I - P) ]
111
+ term_d = P + (dist_diff / d) * (I - P)
112
+ H_dist = 2.0 * self.kappa_d * term_d
113
+
114
+ # --- Total Hessian Blocks ---
115
+
116
+ # Block 11: d^2 E / dx1^2
117
+ # = H1 * (1 + 2*ke*dE) + 2*ke * g1 * g1.T + H_dist
118
+ H11 = hess_1 * (1.0 + 2.0 * self.kappa_e * delta_E) + \
119
+ 2.0 * self.kappa_e * np.dot(g1, g1.T) + \
120
+ H_dist
121
+
122
+ # Block 22: d^2 E / dx2^2
123
+ # = H2 * (1 - 2*ke*dE) + 2*ke * g2 * g2.T + H_dist
124
+ H22 = hess_2 * (1.0 - 2.0 * self.kappa_e * delta_E) + \
125
+ 2.0 * self.kappa_e * np.dot(g2, g2.T) + \
126
+ H_dist
127
+
128
+ # Block 12: d^2 E / dx1 dx2
129
+ # = -2*ke * g1 * g2.T - H_dist
130
+ H12 = -2.0 * self.kappa_e * np.dot(g1, g2.T) - H_dist
131
+
132
+ # Block 21: d^2 E / dx2 dx1
133
+ H21 = H12.T # Symmetric
134
+
135
+ # Construct Full Matrix
136
+ H_top = np.hstack((H11, H12))
137
+ H_bot = np.hstack((H21, H22))
138
+ H_total = np.vstack((H_top, H_bot))
46
139
 
47
- return bitss_grad_1, bitss_grad_2
140
+ return H_total
@@ -2,49 +2,116 @@ import numpy as np
2
2
 
3
3
  class OptMECI:
4
4
  def __init__(self):
5
- # ref.:https://doi.org/10.1021/ct1000268
5
+ # ref.: J. Am. Chem. Soc. 2015, 137, 3433-3445
6
+ # MECI optimization using GP method with Branching Plane Updating (BPU)
6
7
 
7
- self.switch_threshold = 5e-4
8
- self.alpha = 1e-3
9
- self.approx_cdv_vec = None
10
- self.prev_dgv_vec = None
11
- self.dgv_vec = None
12
-
8
+ self.approx_cdv_vec = None # Represents 'y' vector (orthogonal to dgv inside BP)
9
+ self.prev_dgv_vec = None # Represents 'x_{k-1}'
10
+ self.prev_y_vec = None # Represents 'y_{k-1}'
13
11
  return
14
12
 
15
13
  def calc_energy(self, energy_1, energy_2):
16
14
  tot_energy = (energy_1 + energy_2) / 2.0
17
15
  print("energy_1:", energy_1, "hartree")
18
16
  print("energy_2:", energy_2, "hartree")
19
- print("energy_1 - energy_2:", abs(energy_1 - energy_2), "hartree")
17
+ print("|energy_1 - energy_2|:", abs(energy_1 - energy_2), "hartree")
20
18
  return tot_energy
21
19
 
22
20
  def calc_grad(self, energy_1, energy_2, grad_1, grad_2):
23
- if self.approx_cdv_vec is None:
24
- self.approx_cdv_vec = np.ones((len(grad_1)*3, 1))
25
-
26
- delta_grad = grad_1 - grad_2
27
- dgv_vec = delta_grad / np.linalg.norm(delta_grad)
28
- dgv_vec = dgv_vec.reshape(-1, 1)
21
+ # Reshape inputs
22
+ grad_1_flat = grad_1.reshape(-1, 1)
23
+ grad_2_flat = grad_2.reshape(-1, 1)
29
24
 
30
- if self.prev_dgv_vec is None:
31
- self.prev_dgv_vec = dgv_vec
25
+ # 1. Calculate Difference Gradient Vector (x_k)
26
+ delta_grad = grad_1_flat - grad_2_flat
27
+ norm_delta_grad = np.linalg.norm(delta_grad)
28
+ if norm_delta_grad < 1e-8:
29
+ dgv_vec = np.zeros_like(delta_grad) # Avoid division by zero
30
+ else:
31
+ dgv_vec = delta_grad / norm_delta_grad # x_k
32
32
 
33
- self.approx_cdv_vec = (np.dot(self.approx_cdv_vec.T, dgv_vec) * self.prev_dgv_vec -1 * np.dot(self.prev_dgv_vec.T, dgv_vec) * self.approx_cdv_vec) / np.sqrt(np.dot(self.approx_cdv_vec.T, dgv_vec) ** 2 + np.dot(self.prev_dgv_vec.T, dgv_vec) ** 2)
33
+ # 2. Determine Approximate Coupling Vector (y_k) using BPU
34
+ if self.prev_dgv_vec is None:
35
+ # Initialization Step
36
+ # "A plane made of x0 and the mean energy gradient vector was used as an initial BP."
37
+ mean_grad = 0.5 * (grad_1_flat + grad_2_flat)
38
+
39
+ # Project mean_grad to be orthogonal to dgv_vec (Gram-Schmidt)
40
+ overlap = np.dot(mean_grad.T, dgv_vec)
41
+ ortho_vec = mean_grad - overlap * dgv_vec
42
+
43
+ norm_ortho = np.linalg.norm(ortho_vec)
44
+ if norm_ortho < 1e-8:
45
+ # Fallback if mean grad is parallel to diff grad (unlikely)
46
+ ortho_vec = np.random.rand(*dgv_vec.shape)
47
+ ortho_vec = ortho_vec - np.dot(ortho_vec.T, dgv_vec) * dgv_vec
48
+ norm_ortho = np.linalg.norm(ortho_vec)
34
49
 
35
- P_matrix = np.eye((len(dgv_vec))) -1 * np.dot(dgv_vec, dgv_vec.T) -1 * np.dot(self.approx_cdv_vec, self.approx_cdv_vec.T)
36
- P_matrix = 0.5 * (P_matrix + P_matrix.T)
37
- gp_grad = 2 * (energy_1 - energy_2) * dgv_vec + np.dot(P_matrix, 0.5 * (grad_1.reshape(-1, 1) + grad_2.reshape(-1, 1)))
38
-
39
- self.prev_dgv_vec = dgv_vec
40
- gp_grad = gp_grad.reshape(len(grad_1), 3)
41
- return gp_grad
50
+ self.approx_cdv_vec = ortho_vec / norm_ortho # Initial y_0
51
+
52
+ else:
53
+ # Update Step using Eq 4
54
+ # y_k = [ (y_{k-1}.x_k) * x_{k-1} - (x_{k-1}.x_k) * y_{k-1} ] / normalization
55
+
56
+ x_k = dgv_vec
57
+ x_prev = self.prev_dgv_vec
58
+ y_prev = self.prev_y_vec
59
+
60
+ dot_yx = np.dot(y_prev.T, x_k)
61
+ dot_xx = np.dot(x_prev.T, x_k)
62
+
63
+ numerator = dot_yx * x_prev - dot_xx * y_prev
64
+ norm_num = np.linalg.norm(numerator)
65
+
66
+ if norm_num < 1e-8:
67
+ # If x_k didn't change much, keep y_prev orthogonalized to x_k
68
+ numerator = y_prev - np.dot(y_prev.T, x_k) * x_k
69
+ norm_num = np.linalg.norm(numerator)
42
70
 
71
+ self.approx_cdv_vec = numerator / norm_num # y_k
72
+
73
+ # Store vectors for next step
74
+ self.prev_dgv_vec = dgv_vec.copy()
75
+ self.prev_y_vec = self.approx_cdv_vec.copy()
76
+
77
+ # 3. Construct Projection Matrix P for MECI
78
+ # Projects out BOTH dgv (x) and approx_cdv (y) directions
79
+ P_matrix = np.eye(len(dgv_vec)) \
80
+ - np.dot(dgv_vec, dgv_vec.T) \
81
+ - np.dot(self.approx_cdv_vec, self.approx_cdv_vec.T)
82
+
83
+ # 4. Compose Gradient Projection (GP) Gradient
84
+ # Force to reduce energy gap: 2 * (E1 - E2) * dgv
85
+ # Force to minimize mean energy on intersection space (N-2 dim): P * mean_grad
86
+ mean_grad = 0.5 * (grad_1_flat + grad_2_flat)
87
+
88
+ gap_force = 2.0 * (energy_1 - energy_2) * dgv_vec
89
+ seam_force = np.dot(P_matrix, mean_grad)
90
+
91
+ gp_grad = gap_force + seam_force
92
+
93
+ return gp_grad.reshape(len(grad_1), 3)
43
94
 
44
95
  def calc_hess(self, hess_1, hess_2):
96
+ # Approximate Hessian for GP method
97
+ # Projects the mean Hessian onto the intersection space
45
98
  mean_hess = 0.5 * (hess_1 + hess_2)
46
- P_matrix = np.eye((len(self.prev_dgv_vec))) -1 * np.dot(self.prev_dgv_vec, self.prev_dgv_vec.T) -1 * np.dot(self.approx_cdv_vec, self.approx_cdv_vec.T)
47
- P_matrix = 0.5 * (P_matrix + P_matrix.T)
48
99
 
49
- gp_hess = np.dot(P_matrix, np.dot(mean_hess, P_matrix))
100
+ # Need current P_matrix. Reconstruct it from stored vectors.
101
+ if self.approx_cdv_vec is None or self.prev_dgv_vec is None:
102
+ # Should not happen if calc_grad is called first
103
+ return mean_hess
104
+
105
+ dgv_vec = self.prev_dgv_vec
106
+ cdv_vec = self.approx_cdv_vec
107
+
108
+ P_matrix = np.eye(len(dgv_vec)) \
109
+ - np.dot(dgv_vec, dgv_vec.T) \
110
+ - np.dot(cdv_vec, cdv_vec.T)
111
+
112
+ # Projected Mean Hessian + Gap Penalty Curvature
113
+ proj_hess = np.dot(P_matrix, np.dot(mean_hess, P_matrix))
114
+ gap_curvature = 2.0 * np.dot(dgv_vec, dgv_vec.T)
115
+
116
+ gp_hess = proj_hess + gap_curvature
50
117
  return gp_hess
@@ -2,13 +2,13 @@ import numpy as np
2
2
 
3
3
  class OptMESX:
4
4
  def __init__(self):
5
- #ref.: Chemical Physics Letters 674 (2017) 141-145
6
-
7
- self.switch_threshold = 5e-4
8
- self.alpha = 1e-3
5
+ # ref.: J. Am. Chem. Soc. 2015, 137, 3433-3445
6
+ # MESX optimization using Gradient Projection (GP) method.
7
+ # Only the difference gradient vector (DG or f) is projected out.
9
8
  return
10
9
 
11
10
  def calc_energy(self, energy_1, energy_2):
11
+ # The objective is to minimize the mean energy on the seam.
12
12
  tot_energy = (energy_1 + energy_2) / 2.0
13
13
  print("energy_1:", energy_1, "hartree")
14
14
  print("energy_2:", energy_2, "hartree")
@@ -16,32 +16,64 @@ class OptMESX:
16
16
  return tot_energy
17
17
 
18
18
  def calc_grad(self, energy_1, energy_2, grad_1, grad_2):
19
+ # 1. Calculate Difference Gradient Vector (DGV) / f vector
19
20
  delta_grad = grad_1 - grad_2
20
21
  norm_delta_grad = np.linalg.norm(delta_grad)
22
+
21
23
  if norm_delta_grad < 1e-8:
22
24
  dgv_vec = np.zeros_like(delta_grad)
23
25
  else:
24
26
  dgv_vec = delta_grad / norm_delta_grad
27
+
28
+ # Ensure correct shape for matrix operations
25
29
  dgv_vec = dgv_vec.reshape(-1, 1)
30
+ grad_1 = grad_1.reshape(-1, 1)
31
+ grad_2 = grad_2.reshape(-1, 1)
32
+
33
+ # 2. Define Projection Matrix P for MESX
34
+ # Projects out the component along the difference vector (degenerate lifting direction)
35
+ # P = I - v * v.T
36
+ P_matrix = np.eye(len(dgv_vec)) - np.dot(dgv_vec, dgv_vec.T)
26
37
 
27
- P_matrix = np.eye((len(dgv_vec))) -1 * np.dot(dgv_vec, dgv_vec.T)
28
- P_matrix = 0.5 * (P_matrix + P_matrix.T)
29
- gp_grad = 2 * (energy_1 - energy_2) * dgv_vec + np.dot(P_matrix, 0.5 * (grad_1.reshape(-1, 1) + grad_2.reshape(-1, 1)))
38
+ # 3. Calculate Mean Gradient
39
+ mean_grad = 0.5 * (grad_1 + grad_2)
30
40
 
31
- gp_grad = gp_grad.reshape(len(grad_1), 3)
32
- return gp_grad
41
+ # 4. Compose Gradient Projection (GP) Gradient
42
+ # Force to reduce energy gap: 2 * (E1 - E2) * dgv
43
+ # Force to minimize mean energy on seam: P * mean_grad
44
+ gap_force = 2.0 * (energy_1 - energy_2) * dgv_vec
45
+ seam_force = np.dot(P_matrix, mean_grad)
46
+
47
+ gp_grad = gap_force + seam_force
48
+
49
+ return gp_grad.reshape(-1, 3)
33
50
 
34
51
  def calc_hess(self, grad_1, grad_2, hess_1, hess_2):
52
+ # Approximate Hessian for GP method
35
53
  delta_grad = grad_1 - grad_2
36
54
  norm_delta_grad = np.linalg.norm(delta_grad)
55
+
37
56
  if norm_delta_grad < 1e-8:
38
57
  dgv_vec = np.zeros_like(delta_grad)
39
58
  else:
40
59
  dgv_vec = delta_grad / norm_delta_grad
41
- delta_grad = delta_grad.reshape(-1, 1)
60
+
42
61
  dgv_vec = dgv_vec.reshape(-1, 1)
43
- P_matrix = np.eye((len(dgv_vec))) -1 * np.dot(dgv_vec, dgv_vec.T)
44
- P_matrix = 0.5 * (P_matrix + P_matrix.T)
45
- gp_hess = 2.0 * np.dot(delta_grad, dgv_vec.T) + np.dot(P_matrix, 0.5 * (hess_1 + hess_2))
46
- return gp_hess
47
-
62
+
63
+ # Projection Matrix
64
+ P_matrix = np.eye(len(dgv_vec)) - np.dot(dgv_vec, dgv_vec.T)
65
+
66
+ # Mean Hessian
67
+ mean_hess = 0.5 * (hess_1 + hess_2)
68
+
69
+ # Projected Mean Hessian
70
+ # This describes curvature along the seam.
71
+ proj_hess = np.dot(P_matrix, np.dot(mean_hess, P_matrix))
72
+
73
+ # Gap Curvature (Penalty term approximation)
74
+ # Adds large curvature along the difference vector to enforce the gap constraint strongly.
75
+ gap_curvature = 2.0 * np.dot(dgv_vec, dgv_vec.T)
76
+
77
+ gp_hess = proj_hess + gap_curvature
78
+
79
+ return gp_hess
@@ -2,48 +2,65 @@ import numpy as np
2
2
 
3
3
  class OptMESX2:
4
4
  def __init__(self):
5
- #ref.: Theor Chem Acc 99, 95–99 (1998)
6
-
7
- self.switch_threshold = 5e-4
8
- self.alpha = 1e-3
5
+ # ref.: Theor Chem Acc 99, 95–99 (1998)
6
+ # This reference describes the Gradient Projection method.
7
+ # The implementation has been corrected to follow the standard GP formulation
8
+ # as described in J. Am. Chem. Soc. 2015, 137, 3433-3445 .
9
9
  return
10
10
 
11
11
  def calc_energy(self, energy_1, energy_2):
12
12
  tot_energy = (energy_1 + energy_2) / 2.0
13
13
  print("energy_1:", energy_1, "hartree")
14
14
  print("energy_2:", energy_2, "hartree")
15
- print("energy_1 - energy_2:", abs(energy_1 - energy_2), "hartree")
15
+ print("|energy_1 - energy_2|:", abs(energy_1 - energy_2), "hartree")
16
16
  return tot_energy
17
17
 
18
18
  def calc_grad(self, energy_1, energy_2, grad_1, grad_2):
19
- grad_1 = grad_1.reshape(-1, 1)
20
- grad_2 = grad_2.reshape(-1, 1)
21
-
19
+ # 1. Difference Vector (normalized)
22
20
  delta_grad = grad_1 - grad_2
23
21
  norm_delta_grad = np.linalg.norm(delta_grad)
22
+
24
23
  if norm_delta_grad < 1e-8:
25
- projection = np.zeros_like(delta_grad)
24
+ dgv_vec = np.zeros_like(delta_grad)
26
25
  else:
27
- projection = np.sum(grad_1 * delta_grad) / norm_delta_grad
26
+ dgv_vec = delta_grad / norm_delta_grad
28
27
 
29
- parallel = grad_1 - delta_grad * projection / norm_delta_grad
28
+ dgv_vec = dgv_vec.reshape(-1, 1)
29
+ grad_1_flat = grad_1.reshape(-1, 1)
30
+ grad_2_flat = grad_2.reshape(-1, 1)
31
+
32
+ # 2. Projection Matrix (P = I - v v^T)
33
+ P_matrix = np.eye(len(dgv_vec)) - np.dot(dgv_vec, dgv_vec.T)
30
34
 
31
- gp_grad = (energy_1 - energy_2) * 140 * delta_grad + 1.0 * parallel
35
+ # 3. Mean Gradient
36
+ mean_grad = 0.5 * (grad_1_flat + grad_2_flat)
32
37
 
33
- gp_grad = gp_grad.reshape(-1, 3)
34
- return gp_grad
38
+ # 4. Recomposed Gradient
39
+ # Replaces the arbitrary '140' factor with the analytical gap force 2(E1-E2)
40
+ gap_force = 2.0 * (energy_1 - energy_2) * dgv_vec
41
+ seam_force = np.dot(P_matrix, mean_grad)
42
+
43
+ gp_grad = gap_force + seam_force
44
+
45
+ return gp_grad.reshape(-1, 3)
35
46
 
36
47
  def calc_hess(self, grad_1, grad_2, hess_1, hess_2):
48
+ # Robust Hessian construction for GP
37
49
  delta_grad = grad_1 - grad_2
38
50
  norm_delta_grad = np.linalg.norm(delta_grad)
39
51
  if norm_delta_grad < 1e-8:
40
52
  dgv_vec = np.zeros_like(delta_grad)
41
53
  else:
42
54
  dgv_vec = delta_grad / norm_delta_grad
43
- delta_grad = delta_grad.reshape(-1, 1)
55
+
44
56
  dgv_vec = dgv_vec.reshape(-1, 1)
45
- P_matrix = np.eye((len(dgv_vec))) -1 * np.dot(dgv_vec, dgv_vec.T)
46
- P_matrix = 0.5 * (P_matrix + P_matrix.T)
47
57
 
48
- gp_hess = 2.0 * np.dot(delta_grad, dgv_vec.T) + np.dot(P_matrix, 0.5 * (hess_1 + hess_2))
58
+ P_matrix = np.eye(len(dgv_vec)) - np.dot(dgv_vec, dgv_vec.T)
59
+ mean_hess = 0.5 * (hess_1 + hess_2)
60
+
61
+ # Projected Mean Hessian + Gap Curvature
62
+ proj_hess = np.dot(P_matrix, np.dot(mean_hess, P_matrix))
63
+ gap_curvature = 2.0 * np.dot(dgv_vec, dgv_vec.T)
64
+
65
+ gp_hess = proj_hess + gap_curvature
49
66
  return gp_hess
@@ -0,0 +1,182 @@
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}\nCRS-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.proj_grad_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.proj_grad_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)