MultiOptPy 1.20.2__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/__init__.py +0 -0
- multioptpy/Calculator/ase_calculation_tools.py +424 -0
- multioptpy/Calculator/ase_tools/__init__.py +0 -0
- multioptpy/Calculator/ase_tools/fairchem.py +28 -0
- multioptpy/Calculator/ase_tools/gamess.py +19 -0
- multioptpy/Calculator/ase_tools/gaussian.py +165 -0
- multioptpy/Calculator/ase_tools/mace.py +28 -0
- multioptpy/Calculator/ase_tools/mopac.py +19 -0
- multioptpy/Calculator/ase_tools/nwchem.py +31 -0
- multioptpy/Calculator/ase_tools/orca.py +22 -0
- multioptpy/Calculator/ase_tools/pygfn0.py +37 -0
- multioptpy/Calculator/dxtb_calculation_tools.py +344 -0
- multioptpy/Calculator/emt_calculation_tools.py +458 -0
- multioptpy/Calculator/gpaw_calculation_tools.py +183 -0
- multioptpy/Calculator/lj_calculation_tools.py +314 -0
- multioptpy/Calculator/psi4_calculation_tools.py +334 -0
- multioptpy/Calculator/pwscf_calculation_tools.py +189 -0
- multioptpy/Calculator/pyscf_calculation_tools.py +327 -0
- multioptpy/Calculator/sqm1_calculation_tools.py +611 -0
- multioptpy/Calculator/sqm2_calculation_tools.py +376 -0
- multioptpy/Calculator/tblite_calculation_tools.py +352 -0
- multioptpy/Calculator/tersoff_calculation_tools.py +818 -0
- multioptpy/Constraint/__init__.py +0 -0
- multioptpy/Constraint/constraint_condition.py +834 -0
- multioptpy/Coordinate/__init__.py +0 -0
- multioptpy/Coordinate/polar_coordinate.py +199 -0
- multioptpy/Coordinate/redundant_coordinate.py +638 -0
- multioptpy/IRC/__init__.py +0 -0
- multioptpy/IRC/converge_criteria.py +28 -0
- multioptpy/IRC/dvv.py +544 -0
- multioptpy/IRC/euler.py +439 -0
- multioptpy/IRC/hpc.py +564 -0
- multioptpy/IRC/lqa.py +540 -0
- multioptpy/IRC/modekill.py +662 -0
- multioptpy/IRC/rk4.py +579 -0
- multioptpy/Interpolation/__init__.py +0 -0
- multioptpy/Interpolation/adaptive_interpolation.py +283 -0
- multioptpy/Interpolation/binomial_interpolation.py +179 -0
- multioptpy/Interpolation/geodesic_interpolation.py +785 -0
- multioptpy/Interpolation/interpolation.py +156 -0
- multioptpy/Interpolation/linear_interpolation.py +473 -0
- multioptpy/Interpolation/savitzky_golay_interpolation.py +252 -0
- multioptpy/Interpolation/spline_interpolation.py +353 -0
- multioptpy/MD/__init__.py +0 -0
- multioptpy/MD/thermostat.py +185 -0
- multioptpy/MEP/__init__.py +0 -0
- multioptpy/MEP/pathopt_bneb_force.py +443 -0
- multioptpy/MEP/pathopt_dmf_force.py +448 -0
- multioptpy/MEP/pathopt_dneb_force.py +130 -0
- multioptpy/MEP/pathopt_ewbneb_force.py +207 -0
- multioptpy/MEP/pathopt_gpneb_force.py +512 -0
- multioptpy/MEP/pathopt_lup_force.py +113 -0
- multioptpy/MEP/pathopt_neb_force.py +225 -0
- multioptpy/MEP/pathopt_nesb_force.py +205 -0
- multioptpy/MEP/pathopt_om_force.py +153 -0
- multioptpy/MEP/pathopt_qsm_force.py +174 -0
- multioptpy/MEP/pathopt_qsmv2_force.py +304 -0
- multioptpy/ModelFunction/__init__.py +7 -0
- multioptpy/ModelFunction/avoiding_model_function.py +29 -0
- multioptpy/ModelFunction/binary_image_ts_search_model_function.py +47 -0
- multioptpy/ModelFunction/conical_model_function.py +26 -0
- multioptpy/ModelFunction/opt_meci.py +50 -0
- multioptpy/ModelFunction/opt_mesx.py +47 -0
- multioptpy/ModelFunction/opt_mesx_2.py +49 -0
- multioptpy/ModelFunction/seam_model_function.py +27 -0
- multioptpy/ModelHessian/__init__.py +0 -0
- multioptpy/ModelHessian/approx_hessian.py +147 -0
- multioptpy/ModelHessian/calc_params.py +227 -0
- multioptpy/ModelHessian/fischer.py +236 -0
- multioptpy/ModelHessian/fischerd3.py +360 -0
- multioptpy/ModelHessian/fischerd4.py +398 -0
- multioptpy/ModelHessian/gfn0xtb.py +633 -0
- multioptpy/ModelHessian/gfnff.py +709 -0
- multioptpy/ModelHessian/lindh.py +165 -0
- multioptpy/ModelHessian/lindh2007d2.py +707 -0
- multioptpy/ModelHessian/lindh2007d3.py +822 -0
- multioptpy/ModelHessian/lindh2007d4.py +1030 -0
- multioptpy/ModelHessian/morse.py +106 -0
- multioptpy/ModelHessian/schlegel.py +144 -0
- multioptpy/ModelHessian/schlegeld3.py +322 -0
- multioptpy/ModelHessian/schlegeld4.py +559 -0
- multioptpy/ModelHessian/shortrange.py +346 -0
- multioptpy/ModelHessian/swartd2.py +496 -0
- multioptpy/ModelHessian/swartd3.py +706 -0
- multioptpy/ModelHessian/swartd4.py +918 -0
- multioptpy/ModelHessian/tshess.py +40 -0
- multioptpy/Optimizer/QHAdam.py +61 -0
- multioptpy/Optimizer/__init__.py +0 -0
- multioptpy/Optimizer/abc_fire.py +83 -0
- multioptpy/Optimizer/adabelief.py +58 -0
- multioptpy/Optimizer/adabound.py +68 -0
- multioptpy/Optimizer/adadelta.py +65 -0
- multioptpy/Optimizer/adaderivative.py +56 -0
- multioptpy/Optimizer/adadiff.py +68 -0
- multioptpy/Optimizer/adafactor.py +70 -0
- multioptpy/Optimizer/adam.py +65 -0
- multioptpy/Optimizer/adamax.py +62 -0
- multioptpy/Optimizer/adamod.py +83 -0
- multioptpy/Optimizer/adamw.py +65 -0
- multioptpy/Optimizer/adiis.py +523 -0
- multioptpy/Optimizer/afire_neb.py +282 -0
- multioptpy/Optimizer/block_hessian_update.py +709 -0
- multioptpy/Optimizer/c2diis.py +491 -0
- multioptpy/Optimizer/component_wise_scaling.py +405 -0
- multioptpy/Optimizer/conjugate_gradient.py +82 -0
- multioptpy/Optimizer/conjugate_gradient_neb.py +345 -0
- multioptpy/Optimizer/coordinate_locking.py +405 -0
- multioptpy/Optimizer/dic_rsirfo.py +1015 -0
- multioptpy/Optimizer/ediis.py +417 -0
- multioptpy/Optimizer/eve.py +76 -0
- multioptpy/Optimizer/fastadabelief.py +61 -0
- multioptpy/Optimizer/fire.py +77 -0
- multioptpy/Optimizer/fire2.py +249 -0
- multioptpy/Optimizer/fire_neb.py +92 -0
- multioptpy/Optimizer/gan_step.py +486 -0
- multioptpy/Optimizer/gdiis.py +609 -0
- multioptpy/Optimizer/gediis.py +203 -0
- multioptpy/Optimizer/geodesic_step.py +433 -0
- multioptpy/Optimizer/gpmin.py +633 -0
- multioptpy/Optimizer/gpr_step.py +364 -0
- multioptpy/Optimizer/gradientdescent.py +78 -0
- multioptpy/Optimizer/gradientdescent_neb.py +52 -0
- multioptpy/Optimizer/hessian_update.py +433 -0
- multioptpy/Optimizer/hybrid_rfo.py +998 -0
- multioptpy/Optimizer/kdiis.py +625 -0
- multioptpy/Optimizer/lars.py +21 -0
- multioptpy/Optimizer/lbfgs.py +253 -0
- multioptpy/Optimizer/lbfgs_neb.py +355 -0
- multioptpy/Optimizer/linesearch.py +236 -0
- multioptpy/Optimizer/lookahead.py +40 -0
- multioptpy/Optimizer/nadam.py +64 -0
- multioptpy/Optimizer/newton.py +200 -0
- multioptpy/Optimizer/prodigy.py +70 -0
- multioptpy/Optimizer/purtubation.py +16 -0
- multioptpy/Optimizer/quickmin_neb.py +245 -0
- multioptpy/Optimizer/radam.py +75 -0
- multioptpy/Optimizer/rfo_neb.py +302 -0
- multioptpy/Optimizer/ric_rfo.py +842 -0
- multioptpy/Optimizer/rl_step.py +627 -0
- multioptpy/Optimizer/rmspropgrave.py +65 -0
- multioptpy/Optimizer/rsirfo.py +1647 -0
- multioptpy/Optimizer/rsprfo.py +1056 -0
- multioptpy/Optimizer/sadam.py +60 -0
- multioptpy/Optimizer/samsgrad.py +63 -0
- multioptpy/Optimizer/tr_lbfgs.py +678 -0
- multioptpy/Optimizer/trim.py +273 -0
- multioptpy/Optimizer/trust_radius.py +207 -0
- multioptpy/Optimizer/trust_radius_neb.py +121 -0
- multioptpy/Optimizer/yogi.py +60 -0
- multioptpy/OtherMethod/__init__.py +0 -0
- multioptpy/OtherMethod/addf.py +1150 -0
- multioptpy/OtherMethod/dimer.py +895 -0
- multioptpy/OtherMethod/elastic_image_pair.py +629 -0
- multioptpy/OtherMethod/modelfunction.py +456 -0
- multioptpy/OtherMethod/newton_traj.py +454 -0
- multioptpy/OtherMethod/twopshs.py +1095 -0
- multioptpy/PESAnalyzer/__init__.py +0 -0
- multioptpy/PESAnalyzer/calc_irc_curvature.py +125 -0
- multioptpy/PESAnalyzer/cmds_analysis.py +152 -0
- multioptpy/PESAnalyzer/koopman_analysis.py +268 -0
- multioptpy/PESAnalyzer/pca_analysis.py +314 -0
- multioptpy/Parameters/__init__.py +0 -0
- multioptpy/Parameters/atomic_mass.py +20 -0
- multioptpy/Parameters/atomic_number.py +22 -0
- multioptpy/Parameters/covalent_radii.py +44 -0
- multioptpy/Parameters/d2.py +61 -0
- multioptpy/Parameters/d3.py +63 -0
- multioptpy/Parameters/d4.py +103 -0
- multioptpy/Parameters/dreiding.py +34 -0
- multioptpy/Parameters/gfn0xtb_param.py +137 -0
- multioptpy/Parameters/gfnff_param.py +315 -0
- multioptpy/Parameters/gnb.py +104 -0
- multioptpy/Parameters/parameter.py +22 -0
- multioptpy/Parameters/uff.py +72 -0
- multioptpy/Parameters/unit_values.py +20 -0
- multioptpy/Potential/AFIR_potential.py +55 -0
- multioptpy/Potential/LJ_repulsive_potential.py +345 -0
- multioptpy/Potential/__init__.py +0 -0
- multioptpy/Potential/anharmonic_keep_potential.py +28 -0
- multioptpy/Potential/asym_elllipsoidal_potential.py +718 -0
- multioptpy/Potential/electrostatic_potential.py +69 -0
- multioptpy/Potential/flux_potential.py +30 -0
- multioptpy/Potential/gaussian_potential.py +101 -0
- multioptpy/Potential/idpp.py +516 -0
- multioptpy/Potential/keep_angle_potential.py +146 -0
- multioptpy/Potential/keep_dihedral_angle_potential.py +105 -0
- multioptpy/Potential/keep_outofplain_angle_potential.py +70 -0
- multioptpy/Potential/keep_potential.py +99 -0
- multioptpy/Potential/mechano_force_potential.py +74 -0
- multioptpy/Potential/nanoreactor_potential.py +52 -0
- multioptpy/Potential/potential.py +896 -0
- multioptpy/Potential/spacer_model_potential.py +221 -0
- multioptpy/Potential/switching_potential.py +258 -0
- multioptpy/Potential/universal_potential.py +34 -0
- multioptpy/Potential/value_range_potential.py +36 -0
- multioptpy/Potential/void_point_potential.py +25 -0
- multioptpy/SQM/__init__.py +0 -0
- multioptpy/SQM/sqm1/__init__.py +0 -0
- multioptpy/SQM/sqm1/sqm1_core.py +1792 -0
- multioptpy/SQM/sqm2/__init__.py +0 -0
- multioptpy/SQM/sqm2/calc_tools.py +95 -0
- multioptpy/SQM/sqm2/sqm2_basis.py +850 -0
- multioptpy/SQM/sqm2/sqm2_bond.py +119 -0
- multioptpy/SQM/sqm2/sqm2_core.py +303 -0
- multioptpy/SQM/sqm2/sqm2_data.py +1229 -0
- multioptpy/SQM/sqm2/sqm2_disp.py +65 -0
- multioptpy/SQM/sqm2/sqm2_eeq.py +243 -0
- multioptpy/SQM/sqm2/sqm2_overlapint.py +704 -0
- multioptpy/SQM/sqm2/sqm2_qm.py +578 -0
- multioptpy/SQM/sqm2/sqm2_rep.py +66 -0
- multioptpy/SQM/sqm2/sqm2_srb.py +70 -0
- multioptpy/Thermo/__init__.py +0 -0
- multioptpy/Thermo/normal_mode_analyzer.py +865 -0
- multioptpy/Utils/__init__.py +0 -0
- multioptpy/Utils/bond_connectivity.py +264 -0
- multioptpy/Utils/calc_tools.py +884 -0
- multioptpy/Utils/oniom.py +96 -0
- multioptpy/Utils/pbc.py +48 -0
- multioptpy/Utils/riemann_curvature.py +208 -0
- multioptpy/Utils/symmetry_analyzer.py +482 -0
- multioptpy/Visualization/__init__.py +0 -0
- multioptpy/Visualization/visualization.py +156 -0
- multioptpy/WFAnalyzer/MO_analysis.py +104 -0
- multioptpy/WFAnalyzer/__init__.py +0 -0
- multioptpy/Wrapper/__init__.py +0 -0
- multioptpy/Wrapper/autots.py +1239 -0
- multioptpy/Wrapper/ieip_wrapper.py +93 -0
- multioptpy/Wrapper/md_wrapper.py +92 -0
- multioptpy/Wrapper/neb_wrapper.py +94 -0
- multioptpy/Wrapper/optimize_wrapper.py +76 -0
- multioptpy/__init__.py +5 -0
- multioptpy/entrypoints.py +916 -0
- multioptpy/fileio.py +660 -0
- multioptpy/ieip.py +340 -0
- multioptpy/interface.py +1086 -0
- multioptpy/irc.py +529 -0
- multioptpy/moleculardynamics.py +432 -0
- multioptpy/neb.py +1267 -0
- multioptpy/optimization.py +1553 -0
- multioptpy/optimizer.py +709 -0
- multioptpy-1.20.2.dist-info/METADATA +438 -0
- multioptpy-1.20.2.dist-info/RECORD +246 -0
- multioptpy-1.20.2.dist-info/WHEEL +5 -0
- multioptpy-1.20.2.dist-info/entry_points.txt +9 -0
- multioptpy-1.20.2.dist-info/licenses/LICENSE +674 -0
- multioptpy-1.20.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1095 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import datetime
|
|
3
|
+
import os
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
from multioptpy.Potential.potential import BiasPotentialCalculation
|
|
7
|
+
from multioptpy.Utils.calc_tools import Calculationtools
|
|
8
|
+
from multioptpy.fileio import make_workspace, xyz2list
|
|
9
|
+
from multioptpy.Parameters.parameter import UnitValueLib, element_number
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class twoPSHSlikeMethod:
|
|
13
|
+
def __init__(self, config):
|
|
14
|
+
"""
|
|
15
|
+
Implementation of 2PSHS method based on SHS4py approach
|
|
16
|
+
# ref. : Journal of chemical theory and computation 16.6 (2020): 3869-3878. (https://pubs.acs.org/doi/10.1021/acs.jctc.0c00010)
|
|
17
|
+
# ref. : Chem. Phys. Lett. 2004, 384 (4–6), 277–282.
|
|
18
|
+
# ref. : Chem. Phys. Lett. 2005, 404 (1–3), 95–99.
|
|
19
|
+
# ref. : Chemical Physics Letters 404 (2005) 95–99.
|
|
20
|
+
"""
|
|
21
|
+
self.config = config
|
|
22
|
+
self.addf_config = {
|
|
23
|
+
'step_number': int(config.addf_step_num),
|
|
24
|
+
'number_of_add': int(config.nadd),
|
|
25
|
+
'IOEsphereA_initial': 0.01, # Initial hypersphere radius (will be overridden)
|
|
26
|
+
'IOEsphereA_dist': float(config.addf_step_size), # Decrement for hypersphere radius
|
|
27
|
+
'IOEthreshold': 0.01, # Threshold for IOE
|
|
28
|
+
'minimize_threshold': 1.0e-5,# Threshold for minimization
|
|
29
|
+
}
|
|
30
|
+
self.energy_list_1 = []
|
|
31
|
+
self.energy_list_2 = []
|
|
32
|
+
self.gradient_list_1 = []
|
|
33
|
+
self.gradient_list_2 = []
|
|
34
|
+
self.init_displacement = 0.03 / self.get_unit_conversion() # Bohr
|
|
35
|
+
self.date = datetime.datetime.now().strftime("%Y_%m_%d_%H_%M_%S")
|
|
36
|
+
self.converge_criteria = 0.12 # Convergence criteria in Angstroms
|
|
37
|
+
self.element_number_list_1 = None
|
|
38
|
+
self.element_number_list_2 = None
|
|
39
|
+
self.ADDths = [] # List to store ADD theta classes
|
|
40
|
+
self.optimized_structures = {} # Dictionary to store optimized structures by ADD ID
|
|
41
|
+
self.max_iterations = 1 # Limit to 1 major iteration per hypersphere radius
|
|
42
|
+
self.max_inner_iterations = 20 # Maximum inner iterations for minimization
|
|
43
|
+
self.sp1_structure = None # Will store SP_1's structure
|
|
44
|
+
self.sp1_energy = None # Will store SP_1's energy
|
|
45
|
+
self.sp2_structure = None # Will store SP_2's structure
|
|
46
|
+
self.sp2_energy = None # Will store SP_2's energy
|
|
47
|
+
self.initial_distance = None # Will store distance between SP_1 and SP_2
|
|
48
|
+
self.stalled_count = 0 # Counter for detecting stalled optimization
|
|
49
|
+
|
|
50
|
+
def get_unit_conversion(self):
|
|
51
|
+
"""Return bohr to angstrom conversion factor"""
|
|
52
|
+
return UnitValueLib().bohr2angstroms # Approximate value for bohr2angstroms
|
|
53
|
+
|
|
54
|
+
def adjust_center2origin(self, coord):
|
|
55
|
+
"""Adjust coordinates to have center at origin"""
|
|
56
|
+
center = np.mean(coord, axis=0)
|
|
57
|
+
return coord - center
|
|
58
|
+
|
|
59
|
+
def SuperSphere_cartesian(self, A, thetalist, SQ, dim):
|
|
60
|
+
"""
|
|
61
|
+
Vector of super sphere by cartesian (basis transformation from polar to cartesian)
|
|
62
|
+
{sqrt(2*A), theta_1,..., theta_n-1} -> {q_1,..., q_n} -> {x_1, x_2,..., x_n}
|
|
63
|
+
"""
|
|
64
|
+
n_components = min(dim, SQ.shape[1] if SQ.ndim > 1 else dim)
|
|
65
|
+
|
|
66
|
+
qlist = np.zeros(n_components)
|
|
67
|
+
|
|
68
|
+
# Fill q-list using hyperspherical coordinates
|
|
69
|
+
a_k = np.sqrt(2.0 * A)
|
|
70
|
+
for i in range(min(len(thetalist), n_components-1)):
|
|
71
|
+
qlist[i] = a_k * np.cos(thetalist[i])
|
|
72
|
+
a_k *= np.sin(thetalist[i])
|
|
73
|
+
|
|
74
|
+
# Handle the last component
|
|
75
|
+
if n_components > 0:
|
|
76
|
+
qlist[n_components-1] = a_k
|
|
77
|
+
|
|
78
|
+
# Transform to original space
|
|
79
|
+
SSvec = np.dot(SQ, qlist)
|
|
80
|
+
|
|
81
|
+
return SSvec # This is a vector in the reduced space
|
|
82
|
+
|
|
83
|
+
def calctheta(self, vec, eigVlist, eigNlist):
|
|
84
|
+
"""
|
|
85
|
+
Calculate the polar coordinates (theta) from a vector
|
|
86
|
+
"""
|
|
87
|
+
# Get actual dimensions
|
|
88
|
+
n_features = eigVlist[0].shape[0] # Length of each eigenvector
|
|
89
|
+
n_components = min(len(eigVlist), len(eigNlist)) # Number of eigenvectors
|
|
90
|
+
|
|
91
|
+
# Check vector dimensions
|
|
92
|
+
if len(vec) != n_features:
|
|
93
|
+
# If dimensions don't match, truncate or pad the vector
|
|
94
|
+
if len(vec) > n_features:
|
|
95
|
+
vec = vec[:n_features] # Truncate
|
|
96
|
+
else:
|
|
97
|
+
padded_vec = np.zeros(n_features)
|
|
98
|
+
padded_vec[:len(vec)] = vec
|
|
99
|
+
vec = padded_vec
|
|
100
|
+
|
|
101
|
+
# Create SQ_inv matrix with correct dimensions
|
|
102
|
+
SQ_inv = np.zeros((n_components, n_features))
|
|
103
|
+
|
|
104
|
+
for i in range(n_components):
|
|
105
|
+
SQ_inv[i] = eigVlist[i] / np.sqrt(abs(eigNlist[i]))
|
|
106
|
+
|
|
107
|
+
# Perform the dot product
|
|
108
|
+
qvec = np.dot(SQ_inv, vec)
|
|
109
|
+
|
|
110
|
+
r = np.linalg.norm(qvec)
|
|
111
|
+
if r < 1e-10:
|
|
112
|
+
return np.zeros(n_components - 1)
|
|
113
|
+
|
|
114
|
+
thetalist = []
|
|
115
|
+
for i in range(len(qvec) - 1):
|
|
116
|
+
# Handle possible numerical issues with normalization
|
|
117
|
+
norm_q = np.linalg.norm(qvec[i:])
|
|
118
|
+
if norm_q < 1e-10:
|
|
119
|
+
theta = 0.0
|
|
120
|
+
else:
|
|
121
|
+
cos_theta = qvec[i] / norm_q
|
|
122
|
+
cos_theta = max(-1.0, min(1.0, cos_theta)) # Ensure within bounds
|
|
123
|
+
theta = np.arccos(cos_theta)
|
|
124
|
+
if i == len(qvec) - 2 and qvec[-1] < 0:
|
|
125
|
+
theta = 2*np.pi - theta
|
|
126
|
+
thetalist.append(theta)
|
|
127
|
+
|
|
128
|
+
return np.array(thetalist)
|
|
129
|
+
|
|
130
|
+
def SQaxes(self, eigNlist, eigVlist, dim):
|
|
131
|
+
"""Calculate the SQ matrix for transformation"""
|
|
132
|
+
# Get actual available dimensions
|
|
133
|
+
n_features = eigVlist[0].shape[0] # Length of each eigenvector
|
|
134
|
+
n_components = min(len(eigVlist), len(eigNlist), dim) # Number of eigenvectors to use
|
|
135
|
+
|
|
136
|
+
# Initialize with correct dimensions
|
|
137
|
+
SQ = np.zeros((n_features, n_components))
|
|
138
|
+
|
|
139
|
+
# Only iterate up to the available components
|
|
140
|
+
for i in range(n_components):
|
|
141
|
+
SQ[:, i] = eigVlist[i] * np.sqrt(abs(eigNlist[i]))
|
|
142
|
+
|
|
143
|
+
return SQ
|
|
144
|
+
|
|
145
|
+
def SQaxes_inv(self, eigNlist, eigVlist, dim):
|
|
146
|
+
"""Calculate the inverse SQ matrix for transformation"""
|
|
147
|
+
# Get actual available dimensions
|
|
148
|
+
n_features = eigVlist[0].shape[0] # Length of each eigenvector
|
|
149
|
+
n_components = min(len(eigVlist), len(eigNlist), dim) # Number of eigenvectors to use
|
|
150
|
+
|
|
151
|
+
# Initialize with correct dimensions
|
|
152
|
+
SQ_inv = np.zeros((n_components, n_features))
|
|
153
|
+
|
|
154
|
+
# Only iterate up to the available components
|
|
155
|
+
for i in range(n_components):
|
|
156
|
+
SQ_inv[i] = eigVlist[i] / np.sqrt(abs(eigNlist[i]))
|
|
157
|
+
|
|
158
|
+
return SQ_inv
|
|
159
|
+
|
|
160
|
+
def angle(self, v1, v2):
|
|
161
|
+
"""Calculate angle between two vectors"""
|
|
162
|
+
# Check for zero vectors or invalid inputs
|
|
163
|
+
if np.linalg.norm(v1) < 1e-10 or np.linalg.norm(v2) < 1e-10:
|
|
164
|
+
return 0.0
|
|
165
|
+
|
|
166
|
+
v1_u = v1 / np.linalg.norm(v1)
|
|
167
|
+
v2_u = v2 / np.linalg.norm(v2)
|
|
168
|
+
|
|
169
|
+
# Handle potential numerical issues
|
|
170
|
+
dot_product = np.clip(np.dot(v1_u, v2_u), -1.0, 1.0)
|
|
171
|
+
|
|
172
|
+
return np.arccos(dot_product)
|
|
173
|
+
|
|
174
|
+
def angle_SHS(self, v1, v2, SQ_inv):
|
|
175
|
+
"""Calculate angle between two vectors in SHS space"""
|
|
176
|
+
# Ensure vectors are flattened
|
|
177
|
+
v1_flat = v1.flatten() if hasattr(v1, 'flatten') else v1
|
|
178
|
+
v2_flat = v2.flatten() if hasattr(v2, 'flatten') else v2
|
|
179
|
+
|
|
180
|
+
# Handle potential dimension mismatches
|
|
181
|
+
min_dim = min(len(v1_flat), len(v2_flat), SQ_inv.shape[1])
|
|
182
|
+
v1_flat = v1_flat[:min_dim]
|
|
183
|
+
v2_flat = v2_flat[:min_dim]
|
|
184
|
+
|
|
185
|
+
q_v1 = np.dot(SQ_inv[:, :min_dim], v1_flat)
|
|
186
|
+
q_v2 = np.dot(SQ_inv[:, :min_dim], v2_flat)
|
|
187
|
+
return self.angle(q_v1, q_v2)
|
|
188
|
+
|
|
189
|
+
def calc_cartesian_distance(self, structure1, structure2):
|
|
190
|
+
"""Calculate the RMSD between two molecular structures in Cartesian coordinates"""
|
|
191
|
+
if structure1.shape != structure2.shape:
|
|
192
|
+
raise ValueError("Structures have different shapes")
|
|
193
|
+
|
|
194
|
+
# Calculate squared differences
|
|
195
|
+
squared_diff = np.sum((structure1 - structure2)**2)
|
|
196
|
+
|
|
197
|
+
# Return RMSD
|
|
198
|
+
return np.sqrt(squared_diff / structure1.shape[0])
|
|
199
|
+
|
|
200
|
+
def grad_hypersphere(self, f, grad, eqpoint, IOEsphereA, thetalist):
|
|
201
|
+
"""Calculate gradient on hypersphere - MODIFIED for 2PSHS to minimize ADD"""
|
|
202
|
+
# Generate nADD in the reduced space (this will be a vector in the Hessian eigenspace)
|
|
203
|
+
nADD_reduced = self.SuperSphere_cartesian(IOEsphereA, thetalist, self.SQ, self.dim)
|
|
204
|
+
|
|
205
|
+
# We need to convert the reduced space vector back to the full coordinate space
|
|
206
|
+
# First create a zero vector in the full space
|
|
207
|
+
n_atoms = eqpoint.shape[0]
|
|
208
|
+
n_coords = n_atoms * 3
|
|
209
|
+
nADD_full = np.zeros(n_coords)
|
|
210
|
+
|
|
211
|
+
# Map the reduced vector to the full space (this is approximate)
|
|
212
|
+
for i in range(min(len(nADD_reduced), n_coords)):
|
|
213
|
+
nADD_full[i] = nADD_reduced[i]
|
|
214
|
+
|
|
215
|
+
# Reshape to molecular geometry format
|
|
216
|
+
nADD = nADD_full.reshape(n_atoms, 3)
|
|
217
|
+
|
|
218
|
+
# Calculate the normalized direction vector for projecting out later
|
|
219
|
+
EnADD = nADD_full / np.linalg.norm(nADD_full)
|
|
220
|
+
|
|
221
|
+
# Apply the displacement to the initial geometry
|
|
222
|
+
target_point = eqpoint + nADD
|
|
223
|
+
target_point = self.periodicpoint(target_point)
|
|
224
|
+
|
|
225
|
+
# Calculate gradient at this point
|
|
226
|
+
grad_x = grad(target_point)
|
|
227
|
+
if isinstance(grad_x, bool) and grad_x is False:
|
|
228
|
+
return False, False
|
|
229
|
+
|
|
230
|
+
# Flatten the gradient for vector operations
|
|
231
|
+
grad_x_flat = grad_x.flatten()
|
|
232
|
+
|
|
233
|
+
# Project gradient onto tangent space of hypersphere
|
|
234
|
+
# We remove the component along the displacement vector
|
|
235
|
+
returngrad_flat = grad_x_flat - np.dot(grad_x_flat, EnADD) * EnADD
|
|
236
|
+
|
|
237
|
+
# 2PSHS modification: Calculate distance to SP_1 structure
|
|
238
|
+
distance_to_sp1 = self.calc_cartesian_distance(target_point, self.sp1_structure)
|
|
239
|
+
|
|
240
|
+
# Create a gradient component that points toward SP_1's structure
|
|
241
|
+
# This is the key modification for 2PSHS - we want to minimize ADD
|
|
242
|
+
direction_to_sp1 = self.sp1_structure - target_point
|
|
243
|
+
direction_to_sp1_flat = direction_to_sp1.flatten()
|
|
244
|
+
|
|
245
|
+
# Normalize the direction vector
|
|
246
|
+
direction_norm = np.linalg.norm(direction_to_sp1_flat)
|
|
247
|
+
if direction_norm > 1e-10:
|
|
248
|
+
direction_to_sp1_flat /= direction_norm
|
|
249
|
+
|
|
250
|
+
# Project this direction to be tangent to the hypersphere
|
|
251
|
+
direction_component_along_nADD = np.dot(direction_to_sp1_flat, EnADD)
|
|
252
|
+
direction_on_tangent = direction_to_sp1_flat - direction_component_along_nADD * EnADD
|
|
253
|
+
direction_on_tangent_norm = np.linalg.norm(direction_on_tangent)
|
|
254
|
+
|
|
255
|
+
if direction_on_tangent_norm > 1e-10:
|
|
256
|
+
direction_on_tangent /= direction_on_tangent_norm
|
|
257
|
+
|
|
258
|
+
# Add this component to our gradient
|
|
259
|
+
# Weight decreases as we get closer to SP_1
|
|
260
|
+
weight = min(1.0, distance_to_sp1 / self.converge_criteria)
|
|
261
|
+
returngrad_flat += direction_on_tangent * weight * np.linalg.norm(returngrad_flat)
|
|
262
|
+
|
|
263
|
+
# Reshape gradient back to molecular geometry format for easier handling
|
|
264
|
+
returngrad = returngrad_flat.reshape(n_atoms, 3)
|
|
265
|
+
|
|
266
|
+
return target_point, returngrad
|
|
267
|
+
|
|
268
|
+
def periodicpoint(self, point):
|
|
269
|
+
"""Apply periodic boundary conditions if needed"""
|
|
270
|
+
# Implement according to your specific requirements
|
|
271
|
+
return point
|
|
272
|
+
|
|
273
|
+
def minimizeTh_SD_SS(self, ADDth, initialpoint, f, grad, eqpoint, IOEsphereA):
|
|
274
|
+
"""
|
|
275
|
+
Steepest descent optimization on hypersphere to minimize ADD
|
|
276
|
+
For 2PSHS, we want to find minimum ADD on each hypersphere before reducing radius
|
|
277
|
+
"""
|
|
278
|
+
whileN = 0
|
|
279
|
+
thetalist = ADDth.thetalist + initialpoint
|
|
280
|
+
stepsize = 0.1
|
|
281
|
+
n_atoms = eqpoint.shape[0]
|
|
282
|
+
n_coords = n_atoms * 3
|
|
283
|
+
|
|
284
|
+
# Generate initial nADD
|
|
285
|
+
nADD_reduced = self.SuperSphere_cartesian(IOEsphereA, thetalist, self.SQ, self.dim)
|
|
286
|
+
|
|
287
|
+
# Convert reduced space vector to full coordinate space with proper dimensions
|
|
288
|
+
nADD_full = np.zeros(n_coords)
|
|
289
|
+
for i in range(min(len(nADD_reduced), n_coords)):
|
|
290
|
+
nADD_full[i] = nADD_reduced[i]
|
|
291
|
+
|
|
292
|
+
# Reshape to match eqpoint dimensions
|
|
293
|
+
nADD = nADD_full.reshape(n_atoms, 3)
|
|
294
|
+
|
|
295
|
+
# Keep track of best solution (for minimizing ADD)
|
|
296
|
+
best_thetalist = thetalist.copy()
|
|
297
|
+
best_add_value = float('inf') # We want to minimize ADD, so start with positive infinity
|
|
298
|
+
|
|
299
|
+
# Initial point
|
|
300
|
+
targetpoint = eqpoint + nADD
|
|
301
|
+
targetpoint = self.periodicpoint(targetpoint)
|
|
302
|
+
|
|
303
|
+
# Try to calculate initial energy and ADD
|
|
304
|
+
try:
|
|
305
|
+
initial_energy = f(targetpoint)
|
|
306
|
+
if isinstance(initial_energy, (int, float)) and not np.isnan(initial_energy):
|
|
307
|
+
# Calculate initial ADD
|
|
308
|
+
initial_add = initial_energy - IOEsphereA - self.sp1_energy
|
|
309
|
+
best_add_value = initial_add
|
|
310
|
+
except Exception:
|
|
311
|
+
pass # Continue even if initial energy calculation fails
|
|
312
|
+
|
|
313
|
+
# Variables to detect convergence
|
|
314
|
+
prev_add_values = []
|
|
315
|
+
no_improvement_count = 0
|
|
316
|
+
|
|
317
|
+
# Main optimization loop
|
|
318
|
+
while whileN < self.max_inner_iterations:
|
|
319
|
+
try:
|
|
320
|
+
# Get gradient at current point
|
|
321
|
+
grad_x = grad(targetpoint)
|
|
322
|
+
|
|
323
|
+
# If gradient calculation fails, continue with smaller step or different approach
|
|
324
|
+
if grad_x is False:
|
|
325
|
+
# Try a random perturbation and continue
|
|
326
|
+
print(f"Gradient calculation failed at iteration {whileN}, trying random perturbation")
|
|
327
|
+
random_perturbation = np.random.rand(n_atoms, 3) * 0.01 - 0.005 # Small random perturbation
|
|
328
|
+
targetpoint = targetpoint + random_perturbation
|
|
329
|
+
targetpoint = self.periodicpoint(targetpoint)
|
|
330
|
+
|
|
331
|
+
# Calculate new nADD
|
|
332
|
+
nADD = targetpoint - eqpoint
|
|
333
|
+
thetalist = self.calctheta(nADD.flatten(), self.eigVlist, self.eigNlist)
|
|
334
|
+
|
|
335
|
+
# Ensure we're on the hypersphere with correct radius
|
|
336
|
+
nADD_reduced = self.SuperSphere_cartesian(IOEsphereA, thetalist, self.SQ, self.dim)
|
|
337
|
+
nADD_full = np.zeros(n_coords)
|
|
338
|
+
for i in range(min(len(nADD_reduced), n_coords)):
|
|
339
|
+
nADD_full[i] = nADD_reduced[i]
|
|
340
|
+
nADD = nADD_full.reshape(n_atoms, 3)
|
|
341
|
+
|
|
342
|
+
targetpoint = eqpoint + nADD
|
|
343
|
+
targetpoint = self.periodicpoint(targetpoint)
|
|
344
|
+
|
|
345
|
+
whileN += 1
|
|
346
|
+
continue
|
|
347
|
+
|
|
348
|
+
# Calculate nADD norm
|
|
349
|
+
nADD_norm = np.linalg.norm(nADD.flatten())
|
|
350
|
+
if nADD_norm < 1e-10:
|
|
351
|
+
# If nADD is too small, generate a new one
|
|
352
|
+
print(f"nADD norm too small at iteration {whileN}, regenerating")
|
|
353
|
+
thetalist = self.calctheta(np.random.rand(n_coords) - 0.5, self.eigVlist, self.eigNlist)
|
|
354
|
+
nADD_reduced = self.SuperSphere_cartesian(IOEsphereA, thetalist, self.SQ, self.dim)
|
|
355
|
+
nADD_full = np.zeros(n_coords)
|
|
356
|
+
for i in range(min(len(nADD_reduced), n_coords)):
|
|
357
|
+
nADD_full[i] = nADD_reduced[i]
|
|
358
|
+
nADD = nADD_full.reshape(n_atoms, 3)
|
|
359
|
+
targetpoint = eqpoint + nADD
|
|
360
|
+
targetpoint = self.periodicpoint(targetpoint)
|
|
361
|
+
whileN += 1
|
|
362
|
+
continue
|
|
363
|
+
|
|
364
|
+
# Project gradient onto tangent space of hypersphere
|
|
365
|
+
nADD_unit = nADD.flatten() / nADD_norm
|
|
366
|
+
grad_along_nADD = np.dot(grad_x.flatten(), nADD_unit)
|
|
367
|
+
SSgrad_flat = grad_x.flatten() - grad_along_nADD * nADD_unit
|
|
368
|
+
|
|
369
|
+
# For minimizing ADD, we follow the negative gradient
|
|
370
|
+
# This is the key aspect - when minimizing, we move opposite to gradient direction
|
|
371
|
+
SSgrad = -1.0 * SSgrad_flat.reshape(n_atoms, 3)
|
|
372
|
+
|
|
373
|
+
# Calculate energy and current ADD
|
|
374
|
+
current_energy = f(targetpoint)
|
|
375
|
+
current_add = current_energy - IOEsphereA - self.sp1_energy
|
|
376
|
+
|
|
377
|
+
# Store current ADD value for convergence detection
|
|
378
|
+
prev_add_values.append(current_add)
|
|
379
|
+
if len(prev_add_values) > 3:
|
|
380
|
+
prev_add_values.pop(0)
|
|
381
|
+
|
|
382
|
+
# Check if gradient is small enough (local minimum on the hypersphere)
|
|
383
|
+
if np.linalg.norm(SSgrad) < 1.0e-3:
|
|
384
|
+
print(f"Small gradient: {np.linalg.norm(SSgrad):.6f}, potential minimum ADD found: {current_add:.6f}")
|
|
385
|
+
# If gradient is small, we may have found a local minimum
|
|
386
|
+
if current_add < best_add_value:
|
|
387
|
+
best_add_value = current_add
|
|
388
|
+
best_thetalist = thetalist.copy()
|
|
389
|
+
ADDth.converged = True
|
|
390
|
+
print(f"New best ADD value: {best_add_value:.6f}")
|
|
391
|
+
return thetalist
|
|
392
|
+
|
|
393
|
+
# Store current point
|
|
394
|
+
_point_initial = copy.copy(targetpoint)
|
|
395
|
+
|
|
396
|
+
# Line search
|
|
397
|
+
stepsizedamp = stepsize
|
|
398
|
+
found_valid_step = False
|
|
399
|
+
|
|
400
|
+
# Try multiple step sizes
|
|
401
|
+
for whileN2 in range(1, 5): # Try up to 4 steps with varying sizes
|
|
402
|
+
try:
|
|
403
|
+
# Take step with dynamic step size
|
|
404
|
+
step_scale = whileN2 if whileN2 <= 2 else (whileN2 - 2) * 0.1
|
|
405
|
+
|
|
406
|
+
# Follow the negative gradient to minimize ADD
|
|
407
|
+
targetpoint = _point_initial + step_scale * SSgrad / np.linalg.norm(SSgrad) * stepsizedamp
|
|
408
|
+
|
|
409
|
+
# Calculate new nADD
|
|
410
|
+
nADD2 = targetpoint - eqpoint
|
|
411
|
+
|
|
412
|
+
# Convert to theta parameters
|
|
413
|
+
thetalist_new = self.calctheta(nADD2.flatten(), self.eigVlist, self.eigNlist)
|
|
414
|
+
|
|
415
|
+
# Ensure we're on the hypersphere with correct radius
|
|
416
|
+
nADD2_reduced = self.SuperSphere_cartesian(IOEsphereA, thetalist_new, self.SQ, self.dim)
|
|
417
|
+
|
|
418
|
+
# Convert reduced space vector to full coordinate space
|
|
419
|
+
nADD2_full = np.zeros(n_coords)
|
|
420
|
+
for i in range(min(len(nADD2_reduced), n_coords)):
|
|
421
|
+
nADD2_full[i] = nADD2_reduced[i]
|
|
422
|
+
|
|
423
|
+
# Reshape to match eqpoint dimensions
|
|
424
|
+
nADD2 = nADD2_full.reshape(n_atoms, 3)
|
|
425
|
+
|
|
426
|
+
# Calculate new point on hypersphere
|
|
427
|
+
new_point = eqpoint + nADD2
|
|
428
|
+
new_point = self.periodicpoint(new_point)
|
|
429
|
+
|
|
430
|
+
# Calculate energy and ADD at new point
|
|
431
|
+
new_energy = f(new_point)
|
|
432
|
+
new_add = new_energy - IOEsphereA - self.sp1_energy
|
|
433
|
+
|
|
434
|
+
# Accept step if it decreases ADD (since we're minimizing)
|
|
435
|
+
if new_add < current_add:
|
|
436
|
+
found_valid_step = True
|
|
437
|
+
targetpoint = new_point
|
|
438
|
+
thetalist = thetalist_new
|
|
439
|
+
nADD = nADD2
|
|
440
|
+
|
|
441
|
+
# Update best solution if better
|
|
442
|
+
if new_add < best_add_value:
|
|
443
|
+
best_add_value = new_add
|
|
444
|
+
best_thetalist = thetalist_new.copy()
|
|
445
|
+
|
|
446
|
+
break
|
|
447
|
+
|
|
448
|
+
except Exception as e:
|
|
449
|
+
print(f"Step calculation error: {e}, trying different step")
|
|
450
|
+
continue
|
|
451
|
+
|
|
452
|
+
# If no valid step found, try random steps to escape local minima
|
|
453
|
+
if not found_valid_step:
|
|
454
|
+
# Check if we've been stuck for too long
|
|
455
|
+
if len(prev_add_values) >= 3 and all(abs(prev_add_values[i] - prev_add_values[i-1]) < 1e-4 for i in range(1, len(prev_add_values))):
|
|
456
|
+
no_improvement_count += 1
|
|
457
|
+
if no_improvement_count > 3:
|
|
458
|
+
print(f"No improvement in ADD for several iterations. Current ADD: {current_add:.6f}")
|
|
459
|
+
# If we're stuck, return the best solution found so far
|
|
460
|
+
if current_add < best_add_value:
|
|
461
|
+
best_add_value = current_add
|
|
462
|
+
best_thetalist = thetalist.copy()
|
|
463
|
+
ADDth.converged = True
|
|
464
|
+
return best_thetalist
|
|
465
|
+
|
|
466
|
+
# Try a random step
|
|
467
|
+
random_theta_offset = np.random.uniform(-0.1, 0.1, len(thetalist))
|
|
468
|
+
thetalist_new = thetalist + random_theta_offset
|
|
469
|
+
|
|
470
|
+
nADD_reduced = self.SuperSphere_cartesian(IOEsphereA, thetalist_new, self.SQ, self.dim)
|
|
471
|
+
nADD_full = np.zeros(n_coords)
|
|
472
|
+
for i in range(min(len(nADD_reduced), n_coords)):
|
|
473
|
+
nADD_full[i] = nADD_reduced[i]
|
|
474
|
+
nADD = nADD_full.reshape(n_atoms, 3)
|
|
475
|
+
|
|
476
|
+
targetpoint = eqpoint + nADD
|
|
477
|
+
targetpoint = self.periodicpoint(targetpoint)
|
|
478
|
+
thetalist = thetalist_new
|
|
479
|
+
|
|
480
|
+
# Check ADD at new random point
|
|
481
|
+
try:
|
|
482
|
+
new_energy = f(targetpoint)
|
|
483
|
+
new_add = new_energy - IOEsphereA - self.sp1_energy
|
|
484
|
+
|
|
485
|
+
if new_add < best_add_value:
|
|
486
|
+
best_add_value = new_add
|
|
487
|
+
best_thetalist = thetalist.copy()
|
|
488
|
+
except Exception:
|
|
489
|
+
pass
|
|
490
|
+
|
|
491
|
+
# Print progress periodically
|
|
492
|
+
if whileN % 5 == 0:
|
|
493
|
+
print(f"Iteration {whileN}: Current ADD = {current_add:.6f}, Best ADD = {best_add_value:.6f}")
|
|
494
|
+
|
|
495
|
+
whileN += 1
|
|
496
|
+
|
|
497
|
+
except Exception as e:
|
|
498
|
+
print(f"Error in optimization step {whileN}: {e}")
|
|
499
|
+
whileN += 1
|
|
500
|
+
# Try a random step to recover
|
|
501
|
+
random_theta_offset = np.random.uniform(-0.1, 0.1, len(thetalist))
|
|
502
|
+
thetalist = thetalist + random_theta_offset
|
|
503
|
+
|
|
504
|
+
nADD_reduced = self.SuperSphere_cartesian(IOEsphereA, thetalist, self.SQ, self.dim)
|
|
505
|
+
nADD_full = np.zeros(n_coords)
|
|
506
|
+
for i in range(min(len(nADD_reduced), n_coords)):
|
|
507
|
+
nADD_full[i] = nADD_reduced[i]
|
|
508
|
+
nADD = nADD_full.reshape(n_atoms, 3)
|
|
509
|
+
|
|
510
|
+
targetpoint = eqpoint + nADD
|
|
511
|
+
targetpoint = self.periodicpoint(targetpoint)
|
|
512
|
+
|
|
513
|
+
print(f"Max iterations ({self.max_inner_iterations}) reached, returning best solution with ADD = {best_add_value:.6f}")
|
|
514
|
+
# Return the best solution found based on minimizing ADD
|
|
515
|
+
return best_thetalist
|
|
516
|
+
|
|
517
|
+
def save_optimized_structure(self, ADDth, iteration_num, IOEsphereA):
|
|
518
|
+
"""Save optimized structure for a specific ADD and iteration"""
|
|
519
|
+
# Create directory path for ADD-specific structures
|
|
520
|
+
add_dir = os.path.join(self.directory, "optimized_structures", f"ADD_{ADDth.IDnum}")
|
|
521
|
+
os.makedirs(add_dir, exist_ok=True)
|
|
522
|
+
|
|
523
|
+
# Create filename with radius and iteration information
|
|
524
|
+
radius = np.sqrt(IOEsphereA)
|
|
525
|
+
filename = f"iteration_{iteration_num}_r_{radius:.4f}.xyz"
|
|
526
|
+
filepath = os.path.join(add_dir, filename)
|
|
527
|
+
|
|
528
|
+
# Calculate distance to SP_1
|
|
529
|
+
distance = self.calc_cartesian_distance(ADDth.x, self.sp1_structure)
|
|
530
|
+
|
|
531
|
+
# Write XYZ file
|
|
532
|
+
with open(filepath, 'w') as f:
|
|
533
|
+
f.write(f"{len(self.element_list_1)}\n")
|
|
534
|
+
f.write(f"ADD_{ADDth.IDnum} Iteration {iteration_num} Radius {radius:.4f} Distance_to_SP1 {distance:.6f}\n")
|
|
535
|
+
for i, (element, coord) in enumerate(zip(self.element_list_1, ADDth.x)):
|
|
536
|
+
f.write(f"{element} {coord[0]:.12f} {coord[1]:.12f} {coord[2]:.12f}\n")
|
|
537
|
+
|
|
538
|
+
# Store structure information in dictionary
|
|
539
|
+
if ADDth.IDnum not in self.optimized_structures:
|
|
540
|
+
self.optimized_structures[ADDth.IDnum] = []
|
|
541
|
+
|
|
542
|
+
self.optimized_structures[ADDth.IDnum].append({
|
|
543
|
+
'iteration': iteration_num,
|
|
544
|
+
'radius': radius,
|
|
545
|
+
'distance': distance,
|
|
546
|
+
'file': filepath,
|
|
547
|
+
'coords': ADDth.x.copy(),
|
|
548
|
+
'comment': f"ADD_{ADDth.IDnum} Iteration {iteration_num} Radius {radius:.4f} Distance_to_SP1 {distance:.6f}"
|
|
549
|
+
})
|
|
550
|
+
|
|
551
|
+
def create_separate_xyz_files(self):
|
|
552
|
+
"""Create separate XYZ files for each ADD path"""
|
|
553
|
+
created_files = []
|
|
554
|
+
|
|
555
|
+
for add_id, structures in self.optimized_structures.items():
|
|
556
|
+
# Sort structures by iteration number
|
|
557
|
+
structures.sort(key=lambda x: x['iteration'])
|
|
558
|
+
|
|
559
|
+
# Path for the ADD-specific file
|
|
560
|
+
add_trajectory_file = os.path.join(self.directory, f"ADD_{add_id}_trajectory.xyz")
|
|
561
|
+
|
|
562
|
+
# Write the trajectory file
|
|
563
|
+
with open(add_trajectory_file, 'w') as f:
|
|
564
|
+
for structure in structures:
|
|
565
|
+
f.write(f"{len(self.element_list_1)}\n")
|
|
566
|
+
f.write(f"{structure['comment']}\n")
|
|
567
|
+
for i, (element, coord) in enumerate(zip(self.element_list_1, structure['coords'])):
|
|
568
|
+
f.write(f"{element} {coord[0]:.12f} {coord[1]:.12f} {coord[2]:.12f}\n")
|
|
569
|
+
|
|
570
|
+
created_files.append(add_trajectory_file)
|
|
571
|
+
|
|
572
|
+
if created_files:
|
|
573
|
+
paths_str = "\n".join(created_files)
|
|
574
|
+
print(f"Created {len(created_files)} ADD trajectory files:\n{paths_str}")
|
|
575
|
+
|
|
576
|
+
def create_distance_plots(self):
|
|
577
|
+
"""Create CSV files with distance data for plotting"""
|
|
578
|
+
for add_id, structures in self.optimized_structures.items():
|
|
579
|
+
# Sort structures by iteration number
|
|
580
|
+
structures.sort(key=lambda x: x['iteration'])
|
|
581
|
+
|
|
582
|
+
# Path for the distance data file
|
|
583
|
+
distance_file = os.path.join(self.directory, f"ADD_{add_id}_distances.csv")
|
|
584
|
+
|
|
585
|
+
# Write the distance data
|
|
586
|
+
with open(distance_file, 'w') as f:
|
|
587
|
+
f.write("iteration,radius,distance_to_sp1\n")
|
|
588
|
+
for structure in structures:
|
|
589
|
+
f.write(f"{structure['iteration']},{structure['radius']:.4f},{structure['distance']:.6f}\n")
|
|
590
|
+
|
|
591
|
+
print(f"Created distance data file: {distance_file}")
|
|
592
|
+
|
|
593
|
+
def detect_add(self, SP_1, SP_2):
|
|
594
|
+
"""
|
|
595
|
+
Calculate coordinate axes from SP_1's Hessian for creating the hypersphere
|
|
596
|
+
Use the direction from SP_1 to SP_2 as the primary direction
|
|
597
|
+
"""
|
|
598
|
+
# Get coordinates for SP_1
|
|
599
|
+
coord_1 = self.coords_1
|
|
600
|
+
coord_1 = self.adjust_center2origin(coord_1)
|
|
601
|
+
n_atoms = coord_1.shape[0]
|
|
602
|
+
n_coords = n_atoms * 3
|
|
603
|
+
|
|
604
|
+
# Get coordinates for SP_2
|
|
605
|
+
coord_2 = self.coords_2
|
|
606
|
+
coord_2 = self.adjust_center2origin(coord_2)
|
|
607
|
+
|
|
608
|
+
# Check that both structures have the same number of atoms
|
|
609
|
+
if coord_1.shape != coord_2.shape:
|
|
610
|
+
print("SP_1 and SP_2 structures have different shapes")
|
|
611
|
+
return False
|
|
612
|
+
|
|
613
|
+
element_number_list_1 = self.get_element_number_list_1()
|
|
614
|
+
print("### Calculating SP_1 structure and Hessian to create coordinate system ###")
|
|
615
|
+
|
|
616
|
+
SP_1.hessian_flag = True
|
|
617
|
+
self.init_energy_1, self.init_gradient_1, _, iscalculationfailed = SP_1.single_point(
|
|
618
|
+
None, element_number_list_1, "", self.electric_charge_and_multiplicity_1,
|
|
619
|
+
self.method, coord_1
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
if iscalculationfailed:
|
|
623
|
+
print("Initial calculation with SP_1 failed.")
|
|
624
|
+
return False
|
|
625
|
+
|
|
626
|
+
# Apply bias potential if needed for SP_1
|
|
627
|
+
BPC = BiasPotentialCalculation(self.config.iEIP_FOLDER_DIRECTORY)
|
|
628
|
+
_, bias_energy_1, bias_gradient_1, bias_hess_1 = BPC.main(
|
|
629
|
+
self.init_energy_1, self.init_gradient_1, coord_1, element_number_list_1,
|
|
630
|
+
self.config.force_data
|
|
631
|
+
)
|
|
632
|
+
self.init_energy_1 = bias_energy_1
|
|
633
|
+
self.init_gradient_1 = bias_gradient_1
|
|
634
|
+
|
|
635
|
+
SP_1.hessian_flag = False
|
|
636
|
+
self.init_geometry = coord_1 # Shape: (n_atoms, 3)
|
|
637
|
+
|
|
638
|
+
# Store SP_1 structure and energy
|
|
639
|
+
self.sp1_structure = copy.deepcopy(coord_1)
|
|
640
|
+
self.sp1_energy = self.init_energy_1
|
|
641
|
+
|
|
642
|
+
print(f"SP_1 energy: {self.sp1_energy:.6f}")
|
|
643
|
+
print("### Calculating Hessian matrix to set up coordinate system ###")
|
|
644
|
+
|
|
645
|
+
hessian = SP_1.Model_hess + bias_hess_1
|
|
646
|
+
|
|
647
|
+
# Project out translation and rotation
|
|
648
|
+
projection_hessian = Calculationtools().project_out_hess_tr_and_rot_for_coord(hessian, self.element_list_1, coord_1)
|
|
649
|
+
|
|
650
|
+
eigenvalues, eigenvectors = np.linalg.eigh(projection_hessian)
|
|
651
|
+
eigenvalues = eigenvalues.astype(np.float64)
|
|
652
|
+
|
|
653
|
+
# Filter out near-zero eigenvalues
|
|
654
|
+
nonzero_indices = np.where(np.abs(eigenvalues) > 1e-10)[0]
|
|
655
|
+
nonzero_eigenvectors = eigenvectors[:, nonzero_indices].astype(np.float64)
|
|
656
|
+
nonzero_eigenvalues = eigenvalues[nonzero_indices].astype(np.float64)
|
|
657
|
+
|
|
658
|
+
sorted_idx = np.argsort(nonzero_eigenvalues)
|
|
659
|
+
|
|
660
|
+
self.init_eigenvalues = nonzero_eigenvalues
|
|
661
|
+
self.init_eigenvectors = nonzero_eigenvectors
|
|
662
|
+
self.dim = len(nonzero_eigenvalues)
|
|
663
|
+
self.n_atoms = n_atoms
|
|
664
|
+
self.n_coords = n_coords
|
|
665
|
+
|
|
666
|
+
# Store flattened versions for matrix operations
|
|
667
|
+
self.init_geometry_flat = self.init_geometry.flatten()
|
|
668
|
+
|
|
669
|
+
# Prepare SQ matrices
|
|
670
|
+
self.SQ = self.SQaxes(nonzero_eigenvalues, nonzero_eigenvectors, self.dim)
|
|
671
|
+
self.SQ_inv = self.SQaxes_inv(nonzero_eigenvalues, nonzero_eigenvectors, self.dim)
|
|
672
|
+
|
|
673
|
+
self.eigNlist = nonzero_eigenvalues
|
|
674
|
+
self.eigVlist = nonzero_eigenvectors
|
|
675
|
+
|
|
676
|
+
# Calculate mode eigenvectors to try - focus on lower eigenvectors first
|
|
677
|
+
search_idx = len(sorted_idx) # Start with all eigenvectors
|
|
678
|
+
self.sorted_eigenvalues_idx = sorted_idx[0:search_idx]
|
|
679
|
+
|
|
680
|
+
# Initialize ADDths with initial directions
|
|
681
|
+
self.ADDths = []
|
|
682
|
+
|
|
683
|
+
print("### Getting SP_2 structure and energy ###")
|
|
684
|
+
# Calculate SP_2's energy at its initial structure
|
|
685
|
+
element_number_list_2 = self.get_element_number_list_2()
|
|
686
|
+
sp2_energy, sp2_gradient, _, iscalculationfailed = SP_2.single_point(
|
|
687
|
+
None, element_number_list_2, "", self.electric_charge_and_multiplicity_2,
|
|
688
|
+
self.method, coord_2
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
if iscalculationfailed:
|
|
692
|
+
print("Initial calculation with SP_2 failed.")
|
|
693
|
+
return False
|
|
694
|
+
|
|
695
|
+
# Apply bias potential if needed for SP_2
|
|
696
|
+
_, bias_energy_2, bias_gradient_2, _ = BPC.main(
|
|
697
|
+
sp2_energy, sp2_gradient, coord_2, element_number_list_2,
|
|
698
|
+
self.config.force_data
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
self.sp2_structure = copy.deepcopy(coord_2)
|
|
702
|
+
self.sp2_energy = bias_energy_2
|
|
703
|
+
|
|
704
|
+
print(f"SP_2 energy: {self.sp2_energy:.6f}")
|
|
705
|
+
|
|
706
|
+
# Calculate vector from SP_1 to SP_2
|
|
707
|
+
direction_vector = self.sp1_structure - self.sp2_structure
|
|
708
|
+
direction_vector_flat = direction_vector.flatten()
|
|
709
|
+
|
|
710
|
+
# Calculate distance between SP_1 and SP_2
|
|
711
|
+
direction_norm = np.linalg.norm(direction_vector_flat)
|
|
712
|
+
if direction_norm < 1e-10:
|
|
713
|
+
print("SP_1 and SP_2 structures are too similar. Cannot establish a meaningful direction.")
|
|
714
|
+
return False
|
|
715
|
+
|
|
716
|
+
# Set the initial distance
|
|
717
|
+
self.initial_distance = direction_norm
|
|
718
|
+
|
|
719
|
+
# Normalize the direction vector
|
|
720
|
+
direction_vector_flat = direction_vector_flat / direction_norm
|
|
721
|
+
|
|
722
|
+
print("### Setting up initial point on the hypersphere ###")
|
|
723
|
+
print(f"Distance between SP_1 and SP_2: {direction_norm:.6f} Å")
|
|
724
|
+
|
|
725
|
+
with open(self.directory + "/direction_info.csv", "w") as f:
|
|
726
|
+
f.write("direction,distance_between_sp1_sp2\n")
|
|
727
|
+
f.write(f"SP_1_to_SP_2,{direction_norm:.6f}\n")
|
|
728
|
+
|
|
729
|
+
# Use the distance between SP_1 and SP_2 as the initial sphere radius
|
|
730
|
+
IOEsphereA = direction_norm ** 2 # Convert to A value (squared radius)
|
|
731
|
+
|
|
732
|
+
# Create a single ADD point based on the SP_1 to SP_2 direction
|
|
733
|
+
ADDth = type('ADDthetaClass', (), {})
|
|
734
|
+
ADDth.IDnum = 0
|
|
735
|
+
ADDth.dim = self.dim
|
|
736
|
+
ADDth.SQ = self.SQ
|
|
737
|
+
ADDth.SQ_inv = self.SQ_inv
|
|
738
|
+
|
|
739
|
+
# Calculate theta parameters from the direction vector
|
|
740
|
+
ADDth.thetalist = self.calctheta(direction_vector_flat, nonzero_eigenvectors, nonzero_eigenvalues)
|
|
741
|
+
|
|
742
|
+
# Generate nADD (this will be a flattened vector in the Hessian eigenspace)
|
|
743
|
+
ADDth.nADD_reduced = self.SuperSphere_cartesian(IOEsphereA, ADDth.thetalist, self.SQ, self.dim)
|
|
744
|
+
|
|
745
|
+
# Convert the reduced space vector back to the full coordinate space
|
|
746
|
+
ADDth.nADD_full = np.zeros(n_coords)
|
|
747
|
+
for i in range(min(len(ADDth.nADD_reduced), n_coords)):
|
|
748
|
+
ADDth.nADD_full[i] = ADDth.nADD_reduced[i]
|
|
749
|
+
|
|
750
|
+
# Reshape to molecular geometry format
|
|
751
|
+
ADDth.nADD = ADDth.nADD_full.reshape(n_atoms, 3)
|
|
752
|
+
|
|
753
|
+
# Apply the displacement to the initial geometry
|
|
754
|
+
ADDth.x = self.init_geometry + ADDth.nADD
|
|
755
|
+
ADDth.x = self.periodicpoint(ADDth.x)
|
|
756
|
+
|
|
757
|
+
# Initialize flags
|
|
758
|
+
ADDth.converged = False
|
|
759
|
+
ADDth.ADDoptQ = True
|
|
760
|
+
ADDth.ADDremoveQ = False
|
|
761
|
+
ADDth.last_distance = float('inf') # For tracking progress
|
|
762
|
+
|
|
763
|
+
# Store the reference direction
|
|
764
|
+
ADDth.direction_vector = direction_vector
|
|
765
|
+
|
|
766
|
+
# Add to ADDths list
|
|
767
|
+
self.ADDths = [ADDth]
|
|
768
|
+
|
|
769
|
+
print(f"### Primary direction established with initial radius {np.sqrt(IOEsphereA):.6f} ###")
|
|
770
|
+
|
|
771
|
+
# Initialize the optimized structures dictionary
|
|
772
|
+
self.optimized_structures = {}
|
|
773
|
+
|
|
774
|
+
# Create directory for optimized structures
|
|
775
|
+
os.makedirs(os.path.join(self.directory, "optimized_structures"), exist_ok=True)
|
|
776
|
+
|
|
777
|
+
print("### Coordinate system setup complete ###")
|
|
778
|
+
return True
|
|
779
|
+
|
|
780
|
+
def optimize_with_sp2(self, ADDths, SP_2, eqpoint, IOEsphereA, sphereN):
|
|
781
|
+
"""
|
|
782
|
+
Optimize points on the hypersphere using SP_2
|
|
783
|
+
Minimize ADD on the hypersphere
|
|
784
|
+
"""
|
|
785
|
+
print(f"Starting optimization on sphere {sphereN} with radius {np.sqrt(IOEsphereA):.4f}")
|
|
786
|
+
|
|
787
|
+
# Reset optimization flags
|
|
788
|
+
for ADDth in ADDths:
|
|
789
|
+
if not ADDth.converged and not ADDth.ADDremoveQ:
|
|
790
|
+
ADDth.ADDoptQ = True
|
|
791
|
+
else:
|
|
792
|
+
ADDth.ADDoptQ = False
|
|
793
|
+
|
|
794
|
+
# Create a directory for intermediate optimization steps
|
|
795
|
+
sphere_dir = os.path.join(self.directory, "optimized_structures", f"sphere_{sphereN}")
|
|
796
|
+
os.makedirs(sphere_dir, exist_ok=True)
|
|
797
|
+
|
|
798
|
+
# One iteration per hypersphere
|
|
799
|
+
iteration_num = 1
|
|
800
|
+
print(f"Iteration {iteration_num}")
|
|
801
|
+
|
|
802
|
+
# Process each ADD point
|
|
803
|
+
for ADDth in ADDths:
|
|
804
|
+
if not ADDth.ADDoptQ or ADDth.ADDremoveQ:
|
|
805
|
+
continue
|
|
806
|
+
|
|
807
|
+
# Optimize this ADD point
|
|
808
|
+
self.current_id = ADDth.IDnum
|
|
809
|
+
|
|
810
|
+
# Starting from zero displacement
|
|
811
|
+
x_initial = np.zeros(len(ADDth.thetalist))
|
|
812
|
+
|
|
813
|
+
# Minimize ADD on hypersphere using our modified steepest descent
|
|
814
|
+
thetalist = self.minimizeTh_SD_SS(
|
|
815
|
+
ADDth, x_initial,
|
|
816
|
+
lambda x: SP_2.single_point(None, self.get_element_number_list_2(), "",
|
|
817
|
+
self.electric_charge_and_multiplicity_2, self.method, x)[0],
|
|
818
|
+
lambda x: self.calculate_gradient(SP_2, x, self.element_number_list_2,
|
|
819
|
+
self.electric_charge_and_multiplicity_2),
|
|
820
|
+
eqpoint, IOEsphereA
|
|
821
|
+
)
|
|
822
|
+
|
|
823
|
+
if thetalist is False:
|
|
824
|
+
ADDth.ADDremoveQ = True
|
|
825
|
+
print(f"ADD {ADDth.IDnum} optimization failed, marking for removal")
|
|
826
|
+
continue
|
|
827
|
+
|
|
828
|
+
# Update ADD point with optimized position
|
|
829
|
+
ADDth.thetalist = thetalist
|
|
830
|
+
|
|
831
|
+
# Generate nADD in the reduced space
|
|
832
|
+
n_atoms = eqpoint.shape[0]
|
|
833
|
+
ADDth.nADD_reduced = self.SuperSphere_cartesian(IOEsphereA, ADDth.thetalist, self.SQ, self.dim)
|
|
834
|
+
|
|
835
|
+
# Map to full coordinate space
|
|
836
|
+
ADDth.nADD_full = np.zeros(n_atoms * 3)
|
|
837
|
+
for i in range(min(len(ADDth.nADD_reduced), n_atoms * 3)):
|
|
838
|
+
ADDth.nADD_full[i] = ADDth.nADD_reduced[i]
|
|
839
|
+
|
|
840
|
+
# Reshape to molecular geometry
|
|
841
|
+
ADDth.nADD = ADDth.nADD_full.reshape(n_atoms, 3)
|
|
842
|
+
|
|
843
|
+
# Calculate new coordinates
|
|
844
|
+
ADDth.x = eqpoint + ADDth.nADD
|
|
845
|
+
ADDth.x = self.periodicpoint(ADDth.x)
|
|
846
|
+
|
|
847
|
+
# Calculate new energy with SP_2
|
|
848
|
+
energy, grad, _, iscalculationfailed = SP_2.single_point(
|
|
849
|
+
None, self.get_element_number_list_2(), "",
|
|
850
|
+
self.electric_charge_and_multiplicity_2, self.method, ADDth.x
|
|
851
|
+
)
|
|
852
|
+
|
|
853
|
+
if iscalculationfailed:
|
|
854
|
+
ADDth.ADDremoveQ = True
|
|
855
|
+
continue
|
|
856
|
+
|
|
857
|
+
BPC = BiasPotentialCalculation(self.config.iEIP_FOLDER_DIRECTORY)
|
|
858
|
+
_, bias_energy, bias_grad, _ = BPC.main(
|
|
859
|
+
energy, grad, ADDth.x, self.get_element_number_list_2(),
|
|
860
|
+
self.config.force_data
|
|
861
|
+
)
|
|
862
|
+
|
|
863
|
+
ADDth.A = bias_energy
|
|
864
|
+
ADDth.ADD = ADDth.A - IOEsphereA - self.sp1_energy # Use SP_1's energy as reference
|
|
865
|
+
|
|
866
|
+
# Calculate distance to SP_1 structure (for information only)
|
|
867
|
+
distance_to_sp1 = self.calc_cartesian_distance(ADDth.x, self.sp1_structure)
|
|
868
|
+
ADDth.distance_to_sp1 = distance_to_sp1
|
|
869
|
+
|
|
870
|
+
# Print results
|
|
871
|
+
print(f"ADD {ADDth.IDnum}: Energy={energy:.6f}, ADD={ADDth.ADD:.6f}, ||Grad||={np.linalg.norm(grad):.6f}, Distance to SP_1={distance_to_sp1:.6f} Å")
|
|
872
|
+
|
|
873
|
+
# Save structure
|
|
874
|
+
self.save_optimized_structure(ADDth, iteration_num, IOEsphereA)
|
|
875
|
+
|
|
876
|
+
# Filter ADDths list
|
|
877
|
+
ADDths = [ADDth for ADDth in ADDths if not ADDth.ADDremoveQ]
|
|
878
|
+
|
|
879
|
+
# Create a summary of final ADD values
|
|
880
|
+
print("\n### Final ADD values for this hypersphere ###")
|
|
881
|
+
min_add = float('inf')
|
|
882
|
+
min_add_id = -1
|
|
883
|
+
|
|
884
|
+
for ADDth in ADDths:
|
|
885
|
+
if hasattr(ADDth, 'ADD'):
|
|
886
|
+
print(f"ADD {ADDth.IDnum}: {ADDth.ADD:.6f}")
|
|
887
|
+
if ADDth.ADD < min_add:
|
|
888
|
+
min_add = ADDth.ADD
|
|
889
|
+
min_add_id = ADDth.IDnum
|
|
890
|
+
|
|
891
|
+
if min_add_id >= 0:
|
|
892
|
+
print(f"\nMinimum ADD on this hypersphere: {min_add:.6f} (ADD {min_add_id})")
|
|
893
|
+
|
|
894
|
+
return ADDths
|
|
895
|
+
|
|
896
|
+
def calculate_gradient(self, SP, x, element_number_list, electric_charge_and_multiplicity):
|
|
897
|
+
|
|
898
|
+
"""Calculate gradient at point x"""
|
|
899
|
+
_, grad_x, _, iscalculationfailed = SP.single_point(
|
|
900
|
+
None, element_number_list, "", electric_charge_and_multiplicity,
|
|
901
|
+
self.method, x
|
|
902
|
+
)
|
|
903
|
+
|
|
904
|
+
if iscalculationfailed:
|
|
905
|
+
return False
|
|
906
|
+
|
|
907
|
+
# Apply bias if needed
|
|
908
|
+
BPC = BiasPotentialCalculation(self.config.iEIP_FOLDER_DIRECTORY)
|
|
909
|
+
_, _, bias_gradient, _ = BPC.main(
|
|
910
|
+
0, grad_x, x, element_number_list,
|
|
911
|
+
self.config.force_data
|
|
912
|
+
)
|
|
913
|
+
|
|
914
|
+
return bias_gradient
|
|
915
|
+
def run(self, file_directory, SP_1, SP_2, electric_charge_and_multiplicity, FIO_img_1, FIO_img_2):
|
|
916
|
+
"""
|
|
917
|
+
Main function to run the 2PSHS method.
|
|
918
|
+
SP_1 is used to create the hypersphere.
|
|
919
|
+
SP_2 moves on the hypersphere to minimize ADD.
|
|
920
|
+
The hypersphere radius starts at the distance between SP_1 and SP_2
|
|
921
|
+
and decreases gradually until it reaches zero.
|
|
922
|
+
"""
|
|
923
|
+
print("### Start Two-Point Scaled Hypersphere Search (2PSHS) method ###")
|
|
924
|
+
|
|
925
|
+
# Preparation
|
|
926
|
+
base_file_name_1 = os.path.splitext(FIO_img_1.START_FILE)[0]
|
|
927
|
+
base_file_name_2 = os.path.splitext(FIO_img_2.START_FILE)[0]
|
|
928
|
+
self.set_mole_info(base_file_name_1, base_file_name_2, electric_charge_and_multiplicity)
|
|
929
|
+
|
|
930
|
+
self.directory = make_workspace(file_directory)
|
|
931
|
+
|
|
932
|
+
# Create main directory for optimized structures
|
|
933
|
+
os.makedirs(os.path.join(self.directory, "optimized_structures"), exist_ok=True)
|
|
934
|
+
|
|
935
|
+
# Detect initial ADD directions using SP_1 and SP_2
|
|
936
|
+
print("Step 1: Using SP_1 and SP_2 to detect coordinate system and SP_1-SP_2 direction")
|
|
937
|
+
success = self.detect_add(SP_1, SP_2)
|
|
938
|
+
if not success:
|
|
939
|
+
print("Failed to establish SP_1-SP_2 direction.")
|
|
940
|
+
return False
|
|
941
|
+
|
|
942
|
+
# Start with radius equal to the distance between SP_1 and SP_2
|
|
943
|
+
radius = self.initial_distance
|
|
944
|
+
IOEsphereA = radius ** 2 # Convert to A value (squared radius)
|
|
945
|
+
|
|
946
|
+
print("\nStep 2: Optimizing SP_2 to minimize ADD on hypersphere")
|
|
947
|
+
|
|
948
|
+
# Main optimization loop - decrease the radius in each iteration
|
|
949
|
+
sphere_num = 1
|
|
950
|
+
best_add_value = float('inf')
|
|
951
|
+
best_structure = None
|
|
952
|
+
best_radius = None
|
|
953
|
+
|
|
954
|
+
while IOEsphereA > 0 and sphere_num <= self.addf_config['step_number'] and len(self.ADDths) > 0:
|
|
955
|
+
print(f"\nStep {sphere_num+1}: Using hypersphere with radius {np.sqrt(IOEsphereA):.4f}")
|
|
956
|
+
|
|
957
|
+
# Reset optimization flags
|
|
958
|
+
for ADDth in self.ADDths:
|
|
959
|
+
ADDth.converged = False
|
|
960
|
+
ADDth.ADDoptQ = True
|
|
961
|
+
|
|
962
|
+
# Optimize on the current sphere to minimize ADD
|
|
963
|
+
self.ADDths = self.optimize_with_sp2(
|
|
964
|
+
self.ADDths, SP_2, self.init_geometry, IOEsphereA, sphere_num
|
|
965
|
+
)
|
|
966
|
+
|
|
967
|
+
# Check if we found a better ADD value on this hypersphere
|
|
968
|
+
for ADDth in self.ADDths:
|
|
969
|
+
if hasattr(ADDth, 'ADD') and ADDth.ADD < best_add_value:
|
|
970
|
+
best_add_value = ADDth.ADD
|
|
971
|
+
best_structure = ADDth.x.copy()
|
|
972
|
+
best_radius = np.sqrt(IOEsphereA)
|
|
973
|
+
print(f"New best ADD value: {best_add_value:.6f} at radius {best_radius:.4f}")
|
|
974
|
+
|
|
975
|
+
# Decrease the radius for next iteration
|
|
976
|
+
radius -= self.addf_config['IOEsphereA_dist']
|
|
977
|
+
IOEsphereA = radius ** 2 # Convert to A value
|
|
978
|
+
|
|
979
|
+
# Check if the radius has become zero or negative
|
|
980
|
+
if radius <= 0:
|
|
981
|
+
print("Radius has reached zero or negative value. Stopping the search.")
|
|
982
|
+
break
|
|
983
|
+
|
|
984
|
+
sphere_num += 1
|
|
985
|
+
|
|
986
|
+
# Create separate trajectory files for each ADD path
|
|
987
|
+
self.create_separate_xyz_files()
|
|
988
|
+
|
|
989
|
+
# Create distance plots
|
|
990
|
+
self.create_distance_plots()
|
|
991
|
+
|
|
992
|
+
if best_structure is not None:
|
|
993
|
+
print(f"\n### Success! Found minimum ADD value {best_add_value:.6f} at radius {best_radius:.4f} ###")
|
|
994
|
+
# Save the best structure
|
|
995
|
+
best_file = os.path.join(self.directory, "best_add_structure.xyz")
|
|
996
|
+
with open(best_file, 'w') as f:
|
|
997
|
+
f.write(f"{len(self.element_list_1)}\n")
|
|
998
|
+
f.write(f"Best ADD structure with value {best_add_value:.6f} at radius {best_radius:.4f}\n")
|
|
999
|
+
for i, (element, coord) in enumerate(zip(self.element_list_1, best_structure)):
|
|
1000
|
+
f.write(f"{element} {coord[0]:.12f} {coord[1]:.12f} {coord[2]:.12f}\n")
|
|
1001
|
+
print(f"Best structure saved to: {best_file}")
|
|
1002
|
+
else:
|
|
1003
|
+
print("\n### Warning: Could not find a good ADD minimum. ###")
|
|
1004
|
+
|
|
1005
|
+
return best_structure is not None
|
|
1006
|
+
|
|
1007
|
+
# Getters and setters
|
|
1008
|
+
def set_molecule_1(self, element_list, coords):
|
|
1009
|
+
self.element_list_1 = element_list
|
|
1010
|
+
self.coords_1 = coords
|
|
1011
|
+
|
|
1012
|
+
def set_molecule_2(self, element_list, coords):
|
|
1013
|
+
self.element_list_2 = element_list
|
|
1014
|
+
self.coords_2 = coords
|
|
1015
|
+
|
|
1016
|
+
def set_gradient_1(self, gradient):
|
|
1017
|
+
self.gradient_1 = gradient
|
|
1018
|
+
|
|
1019
|
+
def set_gradient_2(self, gradient):
|
|
1020
|
+
self.gradient_2 = gradient
|
|
1021
|
+
|
|
1022
|
+
def set_hessian_1(self, hessian):
|
|
1023
|
+
self.hessian_1 = hessian
|
|
1024
|
+
|
|
1025
|
+
def set_hessian_2(self, hessian):
|
|
1026
|
+
self.hessian_2 = hessian
|
|
1027
|
+
|
|
1028
|
+
def set_energy_1(self, energy):
|
|
1029
|
+
self.energy_1 = energy
|
|
1030
|
+
|
|
1031
|
+
def set_energy_2(self, energy):
|
|
1032
|
+
self.energy_2 = energy
|
|
1033
|
+
|
|
1034
|
+
def set_coords_1(self, coords):
|
|
1035
|
+
self.coords_1 = coords
|
|
1036
|
+
|
|
1037
|
+
def set_coords_2(self, coords):
|
|
1038
|
+
self.coords_2 = coords
|
|
1039
|
+
|
|
1040
|
+
def set_element_list_1(self, element_list):
|
|
1041
|
+
self.element_list_1 = element_list
|
|
1042
|
+
self.element_number_list_1 = [element_number(i) for i in self.element_list_1]
|
|
1043
|
+
|
|
1044
|
+
def set_element_list_2(self, element_list):
|
|
1045
|
+
self.element_list_2 = element_list
|
|
1046
|
+
self.element_number_list_2 = [element_number(i) for i in self.element_list_2]
|
|
1047
|
+
|
|
1048
|
+
def get_coords_1(self):
|
|
1049
|
+
return self.coords_1
|
|
1050
|
+
|
|
1051
|
+
def get_coords_2(self):
|
|
1052
|
+
return self.coords_2
|
|
1053
|
+
|
|
1054
|
+
def get_element_list_1(self):
|
|
1055
|
+
return self.element_list_1
|
|
1056
|
+
|
|
1057
|
+
def get_element_list_2(self):
|
|
1058
|
+
return self.element_list_2
|
|
1059
|
+
|
|
1060
|
+
def get_element_number_list_1(self):
|
|
1061
|
+
if self.element_number_list_1 is None:
|
|
1062
|
+
if self.element_list_1 is None:
|
|
1063
|
+
raise ValueError('Element list 1 is not set.')
|
|
1064
|
+
self.element_number_list_1 = [element_number(i) for i in self.element_list_1]
|
|
1065
|
+
return self.element_number_list_1
|
|
1066
|
+
|
|
1067
|
+
def get_element_number_list_2(self):
|
|
1068
|
+
if self.element_number_list_2 is None:
|
|
1069
|
+
if self.element_list_2 is None:
|
|
1070
|
+
raise ValueError('Element list 2 is not set.')
|
|
1071
|
+
self.element_number_list_2 = [element_number(i) for i in self.element_list_2]
|
|
1072
|
+
return self.element_number_list_2
|
|
1073
|
+
|
|
1074
|
+
def set_mole_info(self, base_file_name_1, base_file_name_2, electric_charge_and_multiplicity):
|
|
1075
|
+
"""Load molecular information for both SP_1 and SP_2 structures"""
|
|
1076
|
+
coord_1, element_list_1, electric_charge_and_multiplicity = xyz2list(
|
|
1077
|
+
base_file_name_1 + ".xyz", electric_charge_and_multiplicity)
|
|
1078
|
+
|
|
1079
|
+
coord_2, element_list_2, _ = xyz2list(
|
|
1080
|
+
base_file_name_2 + ".xyz", electric_charge_and_multiplicity)
|
|
1081
|
+
|
|
1082
|
+
if self.config.usextb != "None":
|
|
1083
|
+
self.method = self.config.usextb
|
|
1084
|
+
elif self.config.usedxtb != "None":
|
|
1085
|
+
self.method = self.config.usedxtb
|
|
1086
|
+
else:
|
|
1087
|
+
self.method = "None"
|
|
1088
|
+
|
|
1089
|
+
self.coords_1 = np.array(coord_1, dtype="float64")
|
|
1090
|
+
self.element_list_1 = element_list_1
|
|
1091
|
+
self.electric_charge_and_multiplicity_1 = electric_charge_and_multiplicity
|
|
1092
|
+
|
|
1093
|
+
self.coords_2 = np.array(coord_2, dtype="float64")
|
|
1094
|
+
self.element_list_2 = element_list_2
|
|
1095
|
+
self.electric_charge_and_multiplicity_2 = electric_charge_and_multiplicity
|