MultiOptPy 1.20.2__py3-none-any.whl → 1.20.3__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.
- multioptpy/Calculator/ase_calculation_tools.py +13 -0
- multioptpy/Calculator/ase_tools/fairchem.py +12 -7
- multioptpy/Constraint/constraint_condition.py +208 -245
- multioptpy/ModelFunction/binary_image_ts_search_model_function.py +111 -18
- multioptpy/ModelFunction/opt_meci.py +94 -27
- multioptpy/ModelFunction/opt_mesx.py +47 -15
- multioptpy/ModelFunction/opt_mesx_2.py +35 -18
- multioptpy/Optimizer/crsirfo.py +182 -0
- multioptpy/Optimizer/mf_rsirfo.py +266 -0
- multioptpy/Optimizer/mode_following.py +273 -0
- multioptpy/Utils/calc_tools.py +1 -0
- multioptpy/fileio.py +13 -6
- multioptpy/interface.py +3 -2
- multioptpy/optimization.py +2139 -1259
- multioptpy/optimizer.py +158 -6
- {multioptpy-1.20.2.dist-info → multioptpy-1.20.3.dist-info}/METADATA +497 -438
- {multioptpy-1.20.2.dist-info → multioptpy-1.20.3.dist-info}/RECORD +21 -18
- {multioptpy-1.20.2.dist-info → multioptpy-1.20.3.dist-info}/WHEEL +0 -0
- {multioptpy-1.20.2.dist-info → multioptpy-1.20.3.dist-info}/entry_points.txt +0 -0
- {multioptpy-1.20.2.dist-info → multioptpy-1.20.3.dist-info}/licenses/LICENSE +0 -0
- {multioptpy-1.20.2.dist-info → multioptpy-1.20.3.dist-info}/top_level.txt +0 -0
|
@@ -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
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 = (
|
|
23
|
-
|
|
24
|
-
|
|
39
|
+
unit_vec = diff_vec / (current_distance + 1e-10)
|
|
25
40
|
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.:
|
|
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.
|
|
8
|
-
self.
|
|
9
|
-
self.
|
|
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
|
|
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
|
-
|
|
24
|
-
|
|
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
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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.:
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
28
|
-
|
|
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
|
-
|
|
32
|
-
|
|
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
|
-
|
|
60
|
+
|
|
42
61
|
dgv_vec = dgv_vec.reshape(-1, 1)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
8
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
24
|
+
dgv_vec = np.zeros_like(delta_grad)
|
|
26
25
|
else:
|
|
27
|
-
|
|
26
|
+
dgv_vec = delta_grad / norm_delta_grad
|
|
28
27
|
|
|
29
|
-
|
|
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
|
-
|
|
35
|
+
# 3. Mean Gradient
|
|
36
|
+
mean_grad = 0.5 * (grad_1_flat + grad_2_flat)
|
|
32
37
|
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|