Skip to content

Commit cc0d9be

Browse files
authored
Add files via upload
### 3. Integration of Multiple PES Information and Model Function Optimization (BITSS, etc.) #### Overview A feature to perform geometry optimization on an "effective potential" constructed by combining energies or gradients from multiple electronic states (PES). Specifically, version 1.20.3 implements the Binary-Image Transition State Search (BITSS) method. #### Implementation Details * **Independent Calculator Instances:** `ModelFunctionHandler` creates independent directories and calculators for State1 and State2 to prevent interference between states. * **BITSS Implementation:** * **6N-Dimensional Expansion:** Concatenates two structures (Image 1 & 2) and treats them as a $6N \times 6N$ Hessian and a $2N$ atom system. * **Second Derivative (Hessian):** Mathematically derives the second derivatives for distance and energy equality constraint terms and implements them as the `calc_hess` method. #### Command Line Arguments (`-mf`) Use the `-mf` argument to specify the type of model function and accompanying parameters (the file path of the reference structure in the case of BITSS). #### Usage Example: ```bash # Search for a pathway connecting two points using the BITSS method, with target.xyz as the target structure. multioptpy start.xyz -mf bitss target.xyz ``` ```bash # Minimum energy seam of crossing (MECI) using Seam Model Function (between charge 0, multiplicity 1 and 3 states) multioptpy input.xyz -mf opt_meci 0 3 -elec 0 -spin 3 ```
1 parent 44c749d commit cc0d9be

5 files changed

Lines changed: 180 additions & 64 deletions

File tree

multioptpy/ModelFunction/opt_meci.py

Lines changed: 94 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,49 +2,116 @@
22

33
class OptMECI:
44
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)
67

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}'
1311
return
1412

1513
def calc_energy(self, energy_1, energy_2):
1614
tot_energy = (energy_1 + energy_2) / 2.0
1715
print("energy_1:", energy_1, "hartree")
1816
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")
2018
return tot_energy
2119

2220
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)
2924

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
3232

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)
3449

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)
4270

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)
4394

4495
def calc_hess(self, hess_1, hess_2):
96+
# Approximate Hessian for GP method
97+
# Projects the mean Hessian onto the intersection space
4598
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)
4899

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
50117
return gp_hess

multioptpy/ModelFunction/opt_mesx.py

Lines changed: 47 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,46 +2,78 @@
22

33
class OptMESX:
44
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.
98
return
109

1110
def calc_energy(self, energy_1, energy_2):
11+
# The objective is to minimize the mean energy on the seam.
1212
tot_energy = (energy_1 + energy_2) / 2.0
1313
print("energy_1:", energy_1, "hartree")
1414
print("energy_2:", energy_2, "hartree")
1515
print("|energy_1 - energy_2|:", abs(energy_1 - energy_2), "hartree")
1616
return tot_energy
1717

1818
def calc_grad(self, energy_1, energy_2, grad_1, grad_2):
19+
# 1. Calculate Difference Gradient Vector (DGV) / f vector
1920
delta_grad = grad_1 - grad_2
2021
norm_delta_grad = np.linalg.norm(delta_grad)
22+
2123
if norm_delta_grad < 1e-8:
2224
dgv_vec = np.zeros_like(delta_grad)
2325
else:
2426
dgv_vec = delta_grad / norm_delta_grad
27+
28+
# Ensure correct shape for matrix operations
2529
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)
2637

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)
3040

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)
3350

3451
def calc_hess(self, grad_1, grad_2, hess_1, hess_2):
52+
# Approximate Hessian for GP method
3553
delta_grad = grad_1 - grad_2
3654
norm_delta_grad = np.linalg.norm(delta_grad)
55+
3756
if norm_delta_grad < 1e-8:
3857
dgv_vec = np.zeros_like(delta_grad)
3958
else:
4059
dgv_vec = delta_grad / norm_delta_grad
41-
delta_grad = delta_grad.reshape(-1, 1)
60+
4261
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

multioptpy/ModelFunction/opt_mesx_2.py

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,48 +2,65 @@
22

33
class OptMESX2:
44
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 .
99
return
1010

1111
def calc_energy(self, energy_1, energy_2):
1212
tot_energy = (energy_1 + energy_2) / 2.0
1313
print("energy_1:", energy_1, "hartree")
1414
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")
1616
return tot_energy
1717

1818
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)
2220
delta_grad = grad_1 - grad_2
2321
norm_delta_grad = np.linalg.norm(delta_grad)
22+
2423
if norm_delta_grad < 1e-8:
25-
projection = np.zeros_like(delta_grad)
24+
dgv_vec = np.zeros_like(delta_grad)
2625
else:
27-
projection = np.sum(grad_1 * delta_grad) / norm_delta_grad
26+
dgv_vec = delta_grad / norm_delta_grad
2827

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)
3034

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)
3237

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)
3546

3647
def calc_hess(self, grad_1, grad_2, hess_1, hess_2):
48+
# Robust Hessian construction for GP
3749
delta_grad = grad_1 - grad_2
3850
norm_delta_grad = np.linalg.norm(delta_grad)
3951
if norm_delta_grad < 1e-8:
4052
dgv_vec = np.zeros_like(delta_grad)
4153
else:
4254
dgv_vec = delta_grad / norm_delta_grad
43-
delta_grad = delta_grad.reshape(-1, 1)
55+
4456
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)
4757

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
4966
return gp_hess

multioptpy/fileio.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -399,9 +399,9 @@ def print_geometry_list(self, new_geometry, element_list, electric_charge_and_mu
399399
for num, geometry in enumerate(new_geometry):
400400
element = element_list[num]
401401
print(f"{element:2} {float(geometry[0]):>17.12f} {float(geometry[1]):>17.12f} {float(geometry[2]):>17.12f}")
402-
402+
print("\n")
403+
403404
geometry_list = [[electric_charge_and_multiplicity, *formatted_geometries]]
404-
print("\n")
405405

406406
return geometry_list
407407

multioptpy/optimization.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -562,11 +562,11 @@ def _run_calc(self, calc_inst, geom, elems, chg_mult, label, iter_idx):
562562

563563
def finalize_bitss_trajectory(self):
564564
if not self.is_bitss or not self.bitss_geom1_history: return
565-
filename = os.path.join(self.base_dir, f"{self.file_io.NOEXT_START_FILE}_bitss_path.xyz")
565+
filename = os.path.join(self.base_dir, f"{self.file_io.NOEXT_START_FILE}_traj.xyz")
566566
full_seq = self.bitss_geom1_history + self.bitss_geom2_history[::-1]
567567
with open(filename, 'w') as f:
568568
for s, g in enumerate(full_seq):
569-
f.write(f"{len(g)}\nStep {s}\n")
569+
f.write(f"{len(g)}\nBITSS_Step {s}\n")
570570
for i, atom in enumerate(g):
571571
f.write(f"{self.single_element_list[i]:2s} {atom[0]:12.8f} {atom[1]:12.8f} {atom[2]:12.8f}\n")
572572

0 commit comments

Comments
 (0)