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,1150 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import numpy as np
|
|
3
|
+
import datetime
|
|
4
|
+
import os
|
|
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 ADDFlikeMethod:
|
|
13
|
+
def __init__(self, config):
|
|
14
|
+
"""
|
|
15
|
+
Implementation of ADD (Anharmonic Downward Distortion) 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
|
+
"""
|
|
20
|
+
self.config = config
|
|
21
|
+
self.addf_config = {
|
|
22
|
+
'step_number': int(config.addf_step_num),
|
|
23
|
+
'number_of_add': int(config.nadd),
|
|
24
|
+
'IOEsphereA_initial': 0.01, # Initial hypersphere radius
|
|
25
|
+
'IOEsphereA_dist': float(config.addf_step_size), # Increment for hypersphere radius
|
|
26
|
+
'IOEthreshold': 0.01, # Threshold for IOE
|
|
27
|
+
'minimize_threshold': 1.0e-5,# Threshold for minimization
|
|
28
|
+
}
|
|
29
|
+
self.energy_list_1 = []
|
|
30
|
+
self.energy_list_2 = []
|
|
31
|
+
self.gradient_list_1 = []
|
|
32
|
+
self.gradient_list_2 = []
|
|
33
|
+
self.init_displacement = 0.03 / self.get_unit_conversion() # Bohr
|
|
34
|
+
self.date = datetime.datetime.now().strftime("%Y_%m_%d_%H_%M_%S")
|
|
35
|
+
self.converge_criteria = 0.12
|
|
36
|
+
self.element_number_list = None
|
|
37
|
+
self.ADDths = [] # List to store ADD theta classes
|
|
38
|
+
self.optimized_structures = {} # Dictionary to store optimized structures by ADD ID
|
|
39
|
+
self.max_iterations = 10
|
|
40
|
+
|
|
41
|
+
def get_unit_conversion(self):
|
|
42
|
+
"""Return bohr to angstrom conversion factor"""
|
|
43
|
+
return UnitValueLib().bohr2angstroms # Approximate value for bohr2angstroms
|
|
44
|
+
|
|
45
|
+
def adjust_center2origin(self, coord):
|
|
46
|
+
"""Adjust coordinates to have center at origin"""
|
|
47
|
+
center = np.mean(coord, axis=0)
|
|
48
|
+
return coord - center
|
|
49
|
+
|
|
50
|
+
def SuperSphere_cartesian(self, A, thetalist, SQ, dim):
|
|
51
|
+
"""
|
|
52
|
+
Vector of super sphere by cartesian (basis transformation from polar to cartesian)
|
|
53
|
+
{sqrt(2*A), theta_1,..., theta_n-1} -> {q_1,..., q_n} -> {x_1, x_2,..., x_n}
|
|
54
|
+
"""
|
|
55
|
+
n_components = min(dim, SQ.shape[1] if SQ.ndim > 1 else dim)
|
|
56
|
+
|
|
57
|
+
qlist = np.zeros(n_components)
|
|
58
|
+
|
|
59
|
+
# Fill q-list using hyperspherical coordinates
|
|
60
|
+
a_k = np.sqrt(2.0 * A)
|
|
61
|
+
for i in range(min(len(thetalist), n_components-1)):
|
|
62
|
+
qlist[i] = a_k * np.cos(thetalist[i])
|
|
63
|
+
a_k *= np.sin(thetalist[i])
|
|
64
|
+
|
|
65
|
+
# Handle the last component
|
|
66
|
+
if n_components > 0:
|
|
67
|
+
qlist[n_components-1] = a_k
|
|
68
|
+
|
|
69
|
+
# Transform to original space
|
|
70
|
+
SSvec = np.dot(SQ, qlist)
|
|
71
|
+
|
|
72
|
+
return SSvec # This is a vector in the reduced space
|
|
73
|
+
|
|
74
|
+
def calctheta(self, vec, eigVlist, eigNlist):
|
|
75
|
+
"""
|
|
76
|
+
Calculate the polar coordinates (theta) from a vector
|
|
77
|
+
"""
|
|
78
|
+
# Get actual dimensions
|
|
79
|
+
n_features = eigVlist[0].shape[0] # Length of each eigenvector
|
|
80
|
+
n_components = min(len(eigVlist), len(eigNlist)) # Number of eigenvectors
|
|
81
|
+
|
|
82
|
+
# Check vector dimensions
|
|
83
|
+
if len(vec) != n_features:
|
|
84
|
+
# If dimensions don't match, truncate or pad the vector
|
|
85
|
+
if len(vec) > n_features:
|
|
86
|
+
vec = vec[:n_features] # Truncate
|
|
87
|
+
else:
|
|
88
|
+
padded_vec = np.zeros(n_features)
|
|
89
|
+
padded_vec[:len(vec)] = vec
|
|
90
|
+
vec = padded_vec
|
|
91
|
+
|
|
92
|
+
# Create SQ_inv matrix with correct dimensions
|
|
93
|
+
SQ_inv = np.zeros((n_components, n_features))
|
|
94
|
+
|
|
95
|
+
for i in range(n_components):
|
|
96
|
+
SQ_inv[i] = eigVlist[i] / np.sqrt(abs(eigNlist[i]))
|
|
97
|
+
|
|
98
|
+
# Perform the dot product
|
|
99
|
+
qvec = np.dot(SQ_inv, vec)
|
|
100
|
+
|
|
101
|
+
r = np.linalg.norm(qvec)
|
|
102
|
+
if r < 1e-10:
|
|
103
|
+
return np.zeros(n_components - 1)
|
|
104
|
+
|
|
105
|
+
thetalist = []
|
|
106
|
+
for i in range(len(qvec) - 1):
|
|
107
|
+
# Handle possible numerical issues with normalization
|
|
108
|
+
norm_q = np.linalg.norm(qvec[i:])
|
|
109
|
+
if norm_q < 1e-10:
|
|
110
|
+
theta = 0.0
|
|
111
|
+
else:
|
|
112
|
+
cos_theta = qvec[i] / norm_q
|
|
113
|
+
cos_theta = max(-1.0, min(1.0, cos_theta)) # Ensure within bounds
|
|
114
|
+
theta = np.arccos(cos_theta)
|
|
115
|
+
if i == len(qvec) - 2 and qvec[-1] < 0:
|
|
116
|
+
theta = 2*np.pi - theta
|
|
117
|
+
thetalist.append(theta)
|
|
118
|
+
|
|
119
|
+
return np.array(thetalist)
|
|
120
|
+
|
|
121
|
+
def SQaxes(self, eigNlist, eigVlist, dim):
|
|
122
|
+
"""Calculate the SQ matrix for transformation"""
|
|
123
|
+
# Get actual available dimensions
|
|
124
|
+
n_features = eigVlist[0].shape[0] # Length of each eigenvector
|
|
125
|
+
n_components = min(len(eigVlist), len(eigNlist), dim) # Number of eigenvectors to use
|
|
126
|
+
|
|
127
|
+
# Initialize with correct dimensions
|
|
128
|
+
SQ = np.zeros((n_features, n_components))
|
|
129
|
+
|
|
130
|
+
# Only iterate up to the available components
|
|
131
|
+
for i in range(n_components):
|
|
132
|
+
SQ[:, i] = eigVlist[i] * np.sqrt(abs(eigNlist[i]))
|
|
133
|
+
|
|
134
|
+
return SQ
|
|
135
|
+
|
|
136
|
+
def SQaxes_inv(self, eigNlist, eigVlist, dim):
|
|
137
|
+
"""Calculate the inverse SQ matrix for transformation"""
|
|
138
|
+
# Get actual available dimensions
|
|
139
|
+
n_features = eigVlist[0].shape[0] # Length of each eigenvector
|
|
140
|
+
n_components = min(len(eigVlist), len(eigNlist), dim) # Number of eigenvectors to use
|
|
141
|
+
|
|
142
|
+
# Initialize with correct dimensions
|
|
143
|
+
SQ_inv = np.zeros((n_components, n_features))
|
|
144
|
+
|
|
145
|
+
# Only iterate up to the available components
|
|
146
|
+
for i in range(n_components):
|
|
147
|
+
SQ_inv[i] = eigVlist[i] / np.sqrt(abs(eigNlist[i]))
|
|
148
|
+
|
|
149
|
+
return SQ_inv
|
|
150
|
+
|
|
151
|
+
def angle(self, v1, v2):
|
|
152
|
+
"""Calculate angle between two vectors"""
|
|
153
|
+
# Check for zero vectors or invalid inputs
|
|
154
|
+
if np.linalg.norm(v1) < 1e-10 or np.linalg.norm(v2) < 1e-10:
|
|
155
|
+
return 0.0
|
|
156
|
+
|
|
157
|
+
v1_u = v1 / np.linalg.norm(v1)
|
|
158
|
+
v2_u = v2 / np.linalg.norm(v2)
|
|
159
|
+
|
|
160
|
+
# Handle potential numerical issues
|
|
161
|
+
dot_product = np.clip(np.dot(v1_u, v2_u), -1.0, 1.0)
|
|
162
|
+
|
|
163
|
+
return np.arccos(dot_product)
|
|
164
|
+
|
|
165
|
+
def angle_SHS(self, v1, v2, SQ_inv):
|
|
166
|
+
"""Calculate angle between two vectors in SHS space"""
|
|
167
|
+
# Ensure vectors are flattened
|
|
168
|
+
v1_flat = v1.flatten() if hasattr(v1, 'flatten') else v1
|
|
169
|
+
v2_flat = v2.flatten() if hasattr(v2, 'flatten') else v2
|
|
170
|
+
|
|
171
|
+
# Handle potential dimension mismatches
|
|
172
|
+
min_dim = min(len(v1_flat), len(v2_flat), SQ_inv.shape[1])
|
|
173
|
+
v1_flat = v1_flat[:min_dim]
|
|
174
|
+
v2_flat = v2_flat[:min_dim]
|
|
175
|
+
|
|
176
|
+
q_v1 = np.dot(SQ_inv[:, :min_dim], v1_flat)
|
|
177
|
+
q_v2 = np.dot(SQ_inv[:, :min_dim], v2_flat)
|
|
178
|
+
return self.angle(q_v1, q_v2)
|
|
179
|
+
|
|
180
|
+
def calc_onHS(self, deltaTH, func, eqpoint, thetalist, IOEsphereA, A_eq):
|
|
181
|
+
"""
|
|
182
|
+
Calculate function value on hypersphere
|
|
183
|
+
"""
|
|
184
|
+
thetalist_new = thetalist + deltaTH
|
|
185
|
+
nADD = self.SuperSphere_cartesian(IOEsphereA, thetalist_new, self.SQ, self.dim)
|
|
186
|
+
x = eqpoint + nADD
|
|
187
|
+
x = self.periodicpoint(x)
|
|
188
|
+
|
|
189
|
+
result = func(x) - IOEsphereA - A_eq
|
|
190
|
+
result += self.IOE_total(nADD)
|
|
191
|
+
return result
|
|
192
|
+
|
|
193
|
+
def IOE_total(self, nADD):
|
|
194
|
+
"""Sum up all IOE illumination"""
|
|
195
|
+
result = 0.0
|
|
196
|
+
for ADDth in self.ADDths:
|
|
197
|
+
if self.current_id == ADDth.IDnum:
|
|
198
|
+
continue
|
|
199
|
+
if ADDth.ADDoptQ:
|
|
200
|
+
continue
|
|
201
|
+
if ADDth.ADD_IOE <= -1000000 or ADDth.ADD_IOE > 10000000:
|
|
202
|
+
continue
|
|
203
|
+
if ADDth.ADD <= self.current_ADD:
|
|
204
|
+
result -= self.IOE(nADD, ADDth)
|
|
205
|
+
return result
|
|
206
|
+
|
|
207
|
+
def IOE(self, nADD, neiborADDth):
|
|
208
|
+
"""Calculate IOE between current point and neighbor"""
|
|
209
|
+
deltaTH = self.angle_SHS(nADD, neiborADDth.nADD, self.SQ_inv)
|
|
210
|
+
if deltaTH <= np.pi * 0.5:
|
|
211
|
+
cosdamp = np.cos(deltaTH)
|
|
212
|
+
return neiborADDth.ADD_IOE * cosdamp * cosdamp * cosdamp
|
|
213
|
+
else:
|
|
214
|
+
return 0.0
|
|
215
|
+
|
|
216
|
+
def grad_hypersphere(self, f, grad, eqpoint, IOEsphereA, thetalist):
|
|
217
|
+
"""Calculate gradient on hypersphere"""
|
|
218
|
+
# Generate nADD in the reduced space (this will be a vector in the Hessian eigenspace)
|
|
219
|
+
nADD_reduced = self.SuperSphere_cartesian(IOEsphereA, thetalist, self.SQ, self.dim)
|
|
220
|
+
|
|
221
|
+
# We need to convert the reduced space vector back to the full coordinate space
|
|
222
|
+
# First create a zero vector in the full space
|
|
223
|
+
n_atoms = eqpoint.shape[0]
|
|
224
|
+
n_coords = n_atoms * 3
|
|
225
|
+
nADD_full = np.zeros(n_coords)
|
|
226
|
+
|
|
227
|
+
# Map the reduced vector to the full space (this is approximate)
|
|
228
|
+
for i in range(min(len(nADD_reduced), n_coords)):
|
|
229
|
+
nADD_full[i] = nADD_reduced[i]
|
|
230
|
+
|
|
231
|
+
# Reshape to molecular geometry format
|
|
232
|
+
nADD = nADD_full.reshape(n_atoms, 3)
|
|
233
|
+
|
|
234
|
+
# Calculate the normalized direction vector for projecting out later
|
|
235
|
+
EnADD = nADD_full / np.linalg.norm(nADD_full)
|
|
236
|
+
|
|
237
|
+
# Apply the displacement to the initial geometry
|
|
238
|
+
target_point = eqpoint + nADD
|
|
239
|
+
target_point = self.periodicpoint(target_point)
|
|
240
|
+
|
|
241
|
+
# Calculate gradient at this point
|
|
242
|
+
grad_x = grad(target_point)
|
|
243
|
+
if isinstance(grad_x, bool) and grad_x is False:
|
|
244
|
+
return False, False
|
|
245
|
+
|
|
246
|
+
# Flatten the gradient for vector operations
|
|
247
|
+
grad_x_flat = grad_x.flatten()
|
|
248
|
+
|
|
249
|
+
# Project gradient onto tangent space of hypersphere
|
|
250
|
+
# We remove the component along the displacement vector
|
|
251
|
+
returngrad_flat = grad_x_flat - np.dot(grad_x_flat, EnADD) * EnADD
|
|
252
|
+
|
|
253
|
+
# Apply IOE contributions (if implemented)
|
|
254
|
+
for ADDth in self.ADDths:
|
|
255
|
+
# Skip if this is the current ADD or if current_id is None
|
|
256
|
+
if self.current_id is not None and self.current_id == ADDth.IDnum:
|
|
257
|
+
continue
|
|
258
|
+
|
|
259
|
+
# Skip if this ADD is being optimized
|
|
260
|
+
if ADDth.ADDoptQ:
|
|
261
|
+
continue
|
|
262
|
+
|
|
263
|
+
# Only apply IOE for ADDs with lower energy (if current_ADD is set)
|
|
264
|
+
if self.current_ADD is not None and hasattr(ADDth, 'ADD') and ADDth.ADD is not None:
|
|
265
|
+
if ADDth.ADD <= self.current_ADD:
|
|
266
|
+
ioe_grad = self.IOE_grad(nADD_full, ADDth)
|
|
267
|
+
if ioe_grad is not None:
|
|
268
|
+
returngrad_flat -= ioe_grad
|
|
269
|
+
|
|
270
|
+
# Reshape gradient back to molecular geometry format for easier handling
|
|
271
|
+
returngrad = returngrad_flat.reshape(n_atoms, 3)
|
|
272
|
+
|
|
273
|
+
return target_point, returngrad
|
|
274
|
+
|
|
275
|
+
def IOE_grad(self, nADD, neiborADDth):
|
|
276
|
+
"""Calculate gradient of IOE"""
|
|
277
|
+
# Make sure we're working with flattened arrays
|
|
278
|
+
nADD_flat = nADD.flatten() if hasattr(nADD, 'flatten') else nADD
|
|
279
|
+
nADD_neibor = neiborADDth.nADD_full if hasattr(neiborADDth, 'nADD_full') else neiborADDth.nADD.flatten()
|
|
280
|
+
|
|
281
|
+
# Get minimum dimension we can work with
|
|
282
|
+
min_dim = min(len(nADD_flat), len(nADD_neibor), self.SQ_inv.shape[1])
|
|
283
|
+
nADD_flat = nADD_flat[:min_dim]
|
|
284
|
+
nADD_neibor = nADD_neibor[:min_dim]
|
|
285
|
+
|
|
286
|
+
# Transform to eigenspace
|
|
287
|
+
q_x = np.dot(self.SQ_inv[:, :min_dim], nADD_flat)
|
|
288
|
+
q_y = np.dot(self.SQ_inv[:, :min_dim], nADD_neibor)
|
|
289
|
+
|
|
290
|
+
# Check for valid vectors before calculating angle
|
|
291
|
+
if np.isnan(q_x).any() or np.isnan(q_y).any() or np.linalg.norm(q_x) < 1e-10 or np.linalg.norm(q_y) < 1e-10:
|
|
292
|
+
return None
|
|
293
|
+
|
|
294
|
+
# Calculate angle in eigenspace
|
|
295
|
+
deltaTH = self.angle(q_x, q_y)
|
|
296
|
+
|
|
297
|
+
# Check if deltaTH is valid (not None and not NaN)
|
|
298
|
+
if deltaTH is None or np.isnan(deltaTH):
|
|
299
|
+
return None
|
|
300
|
+
|
|
301
|
+
# Initialize gradient vector
|
|
302
|
+
returngrad = np.zeros(len(nADD_flat))
|
|
303
|
+
eps = 1.0e-3
|
|
304
|
+
|
|
305
|
+
# Calculate IOE gradient using finite differences
|
|
306
|
+
if deltaTH <= np.pi * 0.5:
|
|
307
|
+
cosdamp = np.cos(deltaTH)
|
|
308
|
+
for i in range(len(nADD_flat)):
|
|
309
|
+
nADD_eps = copy.copy(nADD_flat)
|
|
310
|
+
nADD_eps[i] += eps
|
|
311
|
+
|
|
312
|
+
# Transform to eigenspace
|
|
313
|
+
qx_i = np.dot(self.SQ_inv[:, :min_dim], nADD_eps[:min_dim])
|
|
314
|
+
deltaTH_eps = self.angle(qx_i, q_y)
|
|
315
|
+
|
|
316
|
+
# Check if the new angle is valid
|
|
317
|
+
if deltaTH_eps is None or np.isnan(deltaTH_eps):
|
|
318
|
+
continue
|
|
319
|
+
|
|
320
|
+
cosdamp_eps = np.cos(deltaTH_eps)
|
|
321
|
+
IOE_center = neiborADDth.ADD_IOE * cosdamp * cosdamp * cosdamp
|
|
322
|
+
IOE_eps = neiborADDth.ADD_IOE * cosdamp_eps * cosdamp_eps * cosdamp_eps
|
|
323
|
+
|
|
324
|
+
returngrad[i] = (IOE_eps - IOE_center) / eps
|
|
325
|
+
|
|
326
|
+
# Pad the gradient to the full space if needed
|
|
327
|
+
full_grad = np.zeros(self.n_coords if hasattr(self, 'n_coords') else len(nADD))
|
|
328
|
+
full_grad[:len(returngrad)] = returngrad
|
|
329
|
+
return full_grad
|
|
330
|
+
|
|
331
|
+
return None
|
|
332
|
+
|
|
333
|
+
def periodicpoint(self, point):
|
|
334
|
+
"""Apply periodic boundary conditions if needed"""
|
|
335
|
+
# Implement according to your specific requirements
|
|
336
|
+
return point
|
|
337
|
+
|
|
338
|
+
def minimizeTh_SD_SS(self, ADDth, initialpoint, f, grad, eqpoint, IOEsphereA):
|
|
339
|
+
"""
|
|
340
|
+
Steepest descent optimization on hypersphere with step size control
|
|
341
|
+
Following the implementation in SHS4py.ADD.py with added robustness
|
|
342
|
+
"""
|
|
343
|
+
whileN = 0
|
|
344
|
+
thetalist = ADDth.thetalist + initialpoint
|
|
345
|
+
stepsize = 0.001
|
|
346
|
+
n_atoms = eqpoint.shape[0]
|
|
347
|
+
n_coords = n_atoms * 3
|
|
348
|
+
|
|
349
|
+
# Generate initial nADD
|
|
350
|
+
nADD_reduced = self.SuperSphere_cartesian(IOEsphereA, thetalist, self.SQ, self.dim)
|
|
351
|
+
|
|
352
|
+
# Convert reduced space vector to full coordinate space with proper dimensions
|
|
353
|
+
nADD_full = np.zeros(n_coords)
|
|
354
|
+
for i in range(min(len(nADD_reduced), n_coords)):
|
|
355
|
+
nADD_full[i] = nADD_reduced[i]
|
|
356
|
+
|
|
357
|
+
# Reshape to match eqpoint dimensions
|
|
358
|
+
nADD = nADD_full.reshape(n_atoms, 3)
|
|
359
|
+
|
|
360
|
+
# Keep track of best solution
|
|
361
|
+
best_thetalist = thetalist.copy()
|
|
362
|
+
best_energy = float('inf')
|
|
363
|
+
|
|
364
|
+
# Initial point
|
|
365
|
+
tergetpoint = eqpoint + nADD
|
|
366
|
+
tergetpoint = self.periodicpoint(tergetpoint)
|
|
367
|
+
|
|
368
|
+
# Try to calculate initial energy
|
|
369
|
+
try:
|
|
370
|
+
initial_energy = f(tergetpoint)
|
|
371
|
+
if isinstance(initial_energy, (int, float)) and not np.isnan(initial_energy):
|
|
372
|
+
best_energy = initial_energy
|
|
373
|
+
except Exception:
|
|
374
|
+
pass # Continue even if initial energy calculation fails
|
|
375
|
+
|
|
376
|
+
# Main optimization loop
|
|
377
|
+
while whileN < self.max_iterations:
|
|
378
|
+
try:
|
|
379
|
+
# Get gradient at current point
|
|
380
|
+
grad_x = grad(tergetpoint)
|
|
381
|
+
|
|
382
|
+
# If gradient calculation fails, continue with smaller step or different approach
|
|
383
|
+
if grad_x is False:
|
|
384
|
+
# Try a random perturbation and continue
|
|
385
|
+
print(f"Gradient calculation failed at iteration {whileN}, trying random perturbation")
|
|
386
|
+
random_perturbation = np.random.rand(n_atoms, 3) * 0.01 - 0.005 # Small random perturbation
|
|
387
|
+
tergetpoint = tergetpoint + random_perturbation
|
|
388
|
+
tergetpoint = self.periodicpoint(tergetpoint)
|
|
389
|
+
|
|
390
|
+
# Calculate new nADD
|
|
391
|
+
nADD = tergetpoint - eqpoint
|
|
392
|
+
thetalist = self.calctheta(nADD.flatten(), self.eigVlist, self.eigNlist)
|
|
393
|
+
|
|
394
|
+
# Ensure we're on the hypersphere with correct radius
|
|
395
|
+
nADD_reduced = self.SuperSphere_cartesian(IOEsphereA, thetalist, self.SQ, self.dim)
|
|
396
|
+
nADD_full = np.zeros(n_coords)
|
|
397
|
+
for i in range(min(len(nADD_reduced), n_coords)):
|
|
398
|
+
nADD_full[i] = nADD_reduced[i]
|
|
399
|
+
nADD = nADD_full.reshape(n_atoms, 3)
|
|
400
|
+
|
|
401
|
+
tergetpoint = eqpoint + nADD
|
|
402
|
+
tergetpoint = self.periodicpoint(tergetpoint)
|
|
403
|
+
|
|
404
|
+
whileN += 1
|
|
405
|
+
continue
|
|
406
|
+
|
|
407
|
+
# Apply IOE contributions
|
|
408
|
+
grad_flat = grad_x.flatten()
|
|
409
|
+
for neiborADDth in self.ADDths:
|
|
410
|
+
if ADDth.IDnum == neiborADDth.IDnum:
|
|
411
|
+
continue
|
|
412
|
+
if neiborADDth.ADDoptQ:
|
|
413
|
+
continue
|
|
414
|
+
if neiborADDth.ADD <= ADDth.ADD:
|
|
415
|
+
ioe_grad = self.IOE_grad(nADD.flatten(), neiborADDth)
|
|
416
|
+
if ioe_grad is not None:
|
|
417
|
+
grad_flat = grad_flat - ioe_grad
|
|
418
|
+
|
|
419
|
+
# Reshape back to molecular geometry format
|
|
420
|
+
grad_x = grad_flat.reshape(n_atoms, 3)
|
|
421
|
+
|
|
422
|
+
# Project gradient onto tangent space
|
|
423
|
+
nADD_norm = np.linalg.norm(nADD.flatten())
|
|
424
|
+
if nADD_norm < 1e-10:
|
|
425
|
+
# If nADD is too small, generate a new one
|
|
426
|
+
print(f"nADD norm too small at iteration {whileN}, regenerating")
|
|
427
|
+
thetalist = self.calctheta(np.random.rand(n_coords) - 0.5, self.eigVlist, self.eigNlist)
|
|
428
|
+
nADD_reduced = self.SuperSphere_cartesian(IOEsphereA, thetalist, self.SQ, self.dim)
|
|
429
|
+
nADD_full = np.zeros(n_coords)
|
|
430
|
+
for i in range(min(len(nADD_reduced), n_coords)):
|
|
431
|
+
nADD_full[i] = nADD_reduced[i]
|
|
432
|
+
nADD = nADD_full.reshape(n_atoms, 3)
|
|
433
|
+
tergetpoint = eqpoint + nADD
|
|
434
|
+
tergetpoint = self.periodicpoint(tergetpoint)
|
|
435
|
+
whileN += 1
|
|
436
|
+
continue
|
|
437
|
+
|
|
438
|
+
nADD_unit = nADD.flatten() / nADD_norm
|
|
439
|
+
|
|
440
|
+
# Project gradient component along nADD
|
|
441
|
+
grad_along_nADD = np.dot(grad_x.flatten(), nADD_unit)
|
|
442
|
+
|
|
443
|
+
# Subtract this component to get the tangent space gradient
|
|
444
|
+
SSgrad_flat = grad_x.flatten() - grad_along_nADD * nADD_unit
|
|
445
|
+
SSgrad = SSgrad_flat.reshape(n_atoms, 3)
|
|
446
|
+
|
|
447
|
+
# Check convergence
|
|
448
|
+
if np.linalg.norm(SSgrad) < 1.0e-1:
|
|
449
|
+
# Update best solution if better
|
|
450
|
+
try:
|
|
451
|
+
current_energy = f(tergetpoint)
|
|
452
|
+
if isinstance(current_energy, (int, float)) and not np.isnan(current_energy):
|
|
453
|
+
if current_energy < best_energy:
|
|
454
|
+
best_energy = current_energy
|
|
455
|
+
best_thetalist = thetalist.copy()
|
|
456
|
+
except Exception:
|
|
457
|
+
pass # Just keep the current best if energy calculation fails
|
|
458
|
+
|
|
459
|
+
return thetalist # Converged successfully
|
|
460
|
+
|
|
461
|
+
# Store current point
|
|
462
|
+
_point_initial = copy.copy(tergetpoint)
|
|
463
|
+
|
|
464
|
+
# Line search
|
|
465
|
+
whileN2 = 0
|
|
466
|
+
stepsizedamp = stepsize
|
|
467
|
+
found_valid_step = False
|
|
468
|
+
|
|
469
|
+
# Try multiple step sizes
|
|
470
|
+
for whileN2 in range(1, 5): # Try up to 10 steps with varying sizes
|
|
471
|
+
try:
|
|
472
|
+
# Take step with dynamic step size
|
|
473
|
+
step_scale = whileN2 if whileN2 <= 5 else (whileN2 - 5) * 0.1
|
|
474
|
+
tergetpoint = _point_initial - step_scale * SSgrad / np.linalg.norm(SSgrad) * stepsizedamp
|
|
475
|
+
|
|
476
|
+
# Calculate new nADD
|
|
477
|
+
nADD2 = tergetpoint - eqpoint
|
|
478
|
+
|
|
479
|
+
# Convert to theta parameters
|
|
480
|
+
thetalist_new = self.calctheta(nADD2.flatten(), self.eigVlist, self.eigNlist)
|
|
481
|
+
|
|
482
|
+
# Ensure we're on the hypersphere with correct radius
|
|
483
|
+
nADD2_reduced = self.SuperSphere_cartesian(IOEsphereA, thetalist_new, self.SQ, self.dim)
|
|
484
|
+
|
|
485
|
+
# Convert reduced space vector to full coordinate space
|
|
486
|
+
nADD2_full = np.zeros(n_coords)
|
|
487
|
+
for i in range(min(len(nADD2_reduced), n_coords)):
|
|
488
|
+
nADD2_full[i] = nADD2_reduced[i]
|
|
489
|
+
|
|
490
|
+
# Reshape to match eqpoint dimensions
|
|
491
|
+
nADD2 = nADD2_full.reshape(n_atoms, 3)
|
|
492
|
+
|
|
493
|
+
# Calculate new point on hypersphere
|
|
494
|
+
new_point = eqpoint + nADD2
|
|
495
|
+
new_point = self.periodicpoint(new_point)
|
|
496
|
+
|
|
497
|
+
# Calculate step size
|
|
498
|
+
delta = np.linalg.norm(nADD.flatten() - nADD2.flatten())
|
|
499
|
+
|
|
500
|
+
# Calculate energy at new point to check improvement
|
|
501
|
+
try:
|
|
502
|
+
new_energy = f(new_point)
|
|
503
|
+
if isinstance(new_energy, (int, float)) and not np.isnan(new_energy):
|
|
504
|
+
# Accept step if it improves energy or makes reasonable movement
|
|
505
|
+
if new_energy < best_energy or delta > 0.005:
|
|
506
|
+
found_valid_step = True
|
|
507
|
+
if new_energy < best_energy:
|
|
508
|
+
best_energy = new_energy
|
|
509
|
+
best_thetalist = thetalist_new.copy()
|
|
510
|
+
tergetpoint = new_point
|
|
511
|
+
thetalist = thetalist_new
|
|
512
|
+
nADD = nADD2
|
|
513
|
+
break
|
|
514
|
+
except Exception:
|
|
515
|
+
# If energy calculation fails, accept step if it's a reasonable move
|
|
516
|
+
if delta > 0.005 and delta < 0.1:
|
|
517
|
+
found_valid_step = True
|
|
518
|
+
tergetpoint = new_point
|
|
519
|
+
thetalist = thetalist_new
|
|
520
|
+
nADD = nADD2
|
|
521
|
+
break
|
|
522
|
+
except Exception as e:
|
|
523
|
+
print(f"Step calculation error: {e}, trying different step")
|
|
524
|
+
continue
|
|
525
|
+
|
|
526
|
+
# If no valid step found, try a random perturbation
|
|
527
|
+
if not found_valid_step:
|
|
528
|
+
print(f"No valid step found at iteration {whileN}, trying random perturbation")
|
|
529
|
+
# Generate random perturbation but keep on hypersphere
|
|
530
|
+
random_theta = self.calctheta(np.random.rand(n_coords) - 0.5, self.eigVlist, self.eigNlist)
|
|
531
|
+
random_nADD = self.SuperSphere_cartesian(IOEsphereA, random_theta, self.SQ, self.dim)
|
|
532
|
+
|
|
533
|
+
# Interpolate between current point and random point
|
|
534
|
+
alpha = 0.1 # Small mixing factor
|
|
535
|
+
mixed_theta = thetalist * (1-alpha) + random_theta * alpha
|
|
536
|
+
|
|
537
|
+
# Generate new point
|
|
538
|
+
mixed_nADD = self.SuperSphere_cartesian(IOEsphereA, mixed_theta, self.SQ, self.dim)
|
|
539
|
+
mixed_nADD_full = np.zeros(n_coords)
|
|
540
|
+
for i in range(min(len(mixed_nADD), n_coords)):
|
|
541
|
+
mixed_nADD_full[i] = mixed_nADD[i]
|
|
542
|
+
mixed_nADD = mixed_nADD_full.reshape(n_atoms, 3)
|
|
543
|
+
|
|
544
|
+
# Update point
|
|
545
|
+
tergetpoint = eqpoint + mixed_nADD
|
|
546
|
+
tergetpoint = self.periodicpoint(tergetpoint)
|
|
547
|
+
nADD = mixed_nADD
|
|
548
|
+
thetalist = mixed_theta
|
|
549
|
+
|
|
550
|
+
# Increment counter
|
|
551
|
+
whileN += 1
|
|
552
|
+
|
|
553
|
+
# Print progress periodically
|
|
554
|
+
if whileN % 10 == 0:
|
|
555
|
+
print(f"Optimization step {whileN}: gradient norm = {np.linalg.norm(SSgrad):.6f}")
|
|
556
|
+
|
|
557
|
+
except Exception as e:
|
|
558
|
+
print(f"Error in optimization step {whileN}: {e}, continuing with best solution")
|
|
559
|
+
whileN += 1
|
|
560
|
+
|
|
561
|
+
# Try to recover with random perturbation
|
|
562
|
+
if whileN % 3 == 0: # Every third error, try a more drastic change
|
|
563
|
+
try:
|
|
564
|
+
# Generate a completely new point on hypersphere
|
|
565
|
+
random_theta = self.calctheta(np.random.rand(n_coords) - 0.5, self.eigVlist, self.eigNlist)
|
|
566
|
+
random_nADD = self.SuperSphere_cartesian(IOEsphereA, random_theta, self.SQ, self.dim)
|
|
567
|
+
|
|
568
|
+
# Create full vector
|
|
569
|
+
random_nADD_full = np.zeros(n_coords)
|
|
570
|
+
for i in range(min(len(random_nADD), n_coords)):
|
|
571
|
+
random_nADD_full[i] = random_nADD[i]
|
|
572
|
+
random_nADD = random_nADD_full.reshape(n_atoms, 3)
|
|
573
|
+
|
|
574
|
+
# Try the new point
|
|
575
|
+
new_point = eqpoint + random_nADD
|
|
576
|
+
new_point = self.periodicpoint(new_point)
|
|
577
|
+
|
|
578
|
+
# Check if it's better
|
|
579
|
+
try:
|
|
580
|
+
random_energy = f(new_point)
|
|
581
|
+
if isinstance(random_energy, (int, float)) and not np.isnan(random_energy):
|
|
582
|
+
if random_energy < best_energy:
|
|
583
|
+
best_energy = random_energy
|
|
584
|
+
best_thetalist = random_theta.copy()
|
|
585
|
+
tergetpoint = new_point
|
|
586
|
+
nADD = random_nADD
|
|
587
|
+
thetalist = random_theta
|
|
588
|
+
except Exception:
|
|
589
|
+
pass # Ignore failed energy calculations
|
|
590
|
+
except Exception:
|
|
591
|
+
pass # Ignore errors in recovery attempt
|
|
592
|
+
|
|
593
|
+
print(f"Optimization completed with {whileN} iterations")
|
|
594
|
+
# Return the best solution found
|
|
595
|
+
return best_thetalist if best_energy < float('inf') else thetalist
|
|
596
|
+
|
|
597
|
+
def detect_add(self, QMC):
|
|
598
|
+
"""Detect ADD directions from Hessian"""
|
|
599
|
+
coord_1 = self.get_coord()
|
|
600
|
+
coord_1 = self.adjust_center2origin(coord_1)
|
|
601
|
+
n_atoms = coord_1.shape[0]
|
|
602
|
+
n_coords = n_atoms * 3
|
|
603
|
+
|
|
604
|
+
element_number_list_1 = self.get_element_number_list()
|
|
605
|
+
print("### Checking whether initial structure is EQ. ###")
|
|
606
|
+
|
|
607
|
+
QMC.hessian_flag = True
|
|
608
|
+
self.init_energy, self.init_gradient, _, iscalculationfailed = QMC.single_point(
|
|
609
|
+
None, element_number_list_1, "", self.electric_charge_and_multiplicity,
|
|
610
|
+
self.method, coord_1
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
if iscalculationfailed:
|
|
614
|
+
print("Initial calculation failed.")
|
|
615
|
+
return False
|
|
616
|
+
|
|
617
|
+
# Apply bias potential if needed
|
|
618
|
+
BPC = BiasPotentialCalculation(self.config.iEIP_FOLDER_DIRECTORY)
|
|
619
|
+
_, bias_energy, bias_gradient, bias_hess = BPC.main(
|
|
620
|
+
self.init_energy, self.init_gradient, coord_1, element_number_list_1,
|
|
621
|
+
self.config.force_data
|
|
622
|
+
)
|
|
623
|
+
self.init_energy = bias_energy
|
|
624
|
+
self.init_gradient = bias_gradient
|
|
625
|
+
|
|
626
|
+
QMC.hessian_flag = False
|
|
627
|
+
self.init_geometry = coord_1 # Shape: (n_atoms, 3)
|
|
628
|
+
self.set_coord(coord_1)
|
|
629
|
+
|
|
630
|
+
if np.linalg.norm(self.init_gradient) > 1e-3:
|
|
631
|
+
print("Norm of gradient is too large. Structure is not at equilibrium.")
|
|
632
|
+
return False
|
|
633
|
+
|
|
634
|
+
print("Initial structure is EQ.")
|
|
635
|
+
print("### Start calculating Hessian matrix to detect ADD. ###")
|
|
636
|
+
|
|
637
|
+
hessian = QMC.Model_hess + bias_hess
|
|
638
|
+
|
|
639
|
+
# Project out translation and rotation
|
|
640
|
+
projection_hessian = Calculationtools().project_out_hess_tr_and_rot_for_coord(hessian, self.element_list, coord_1)
|
|
641
|
+
|
|
642
|
+
eigenvalues, eigenvectors = np.linalg.eigh(projection_hessian)
|
|
643
|
+
eigenvalues = eigenvalues.astype(np.float64)
|
|
644
|
+
|
|
645
|
+
# Filter out near-zero eigenvalues
|
|
646
|
+
nonzero_indices = np.where(np.abs(eigenvalues) > 1e-10)[0]
|
|
647
|
+
nonzero_eigenvectors = eigenvectors[:, nonzero_indices].astype(np.float64)
|
|
648
|
+
nonzero_eigenvalues = eigenvalues[nonzero_indices].astype(np.float64)
|
|
649
|
+
|
|
650
|
+
sorted_idx = np.argsort(nonzero_eigenvalues)
|
|
651
|
+
|
|
652
|
+
self.init_eigenvalues = nonzero_eigenvalues
|
|
653
|
+
self.init_eigenvectors = nonzero_eigenvectors
|
|
654
|
+
self.dim = len(nonzero_eigenvalues)
|
|
655
|
+
self.n_atoms = n_atoms
|
|
656
|
+
self.n_coords = n_coords
|
|
657
|
+
|
|
658
|
+
# Store flattened versions for matrix operations
|
|
659
|
+
self.init_geometry_flat = self.init_geometry.flatten()
|
|
660
|
+
|
|
661
|
+
# Prepare SQ matrices
|
|
662
|
+
self.SQ = self.SQaxes(nonzero_eigenvalues, nonzero_eigenvectors, self.dim)
|
|
663
|
+
self.SQ_inv = self.SQaxes_inv(nonzero_eigenvalues, nonzero_eigenvectors, self.dim)
|
|
664
|
+
|
|
665
|
+
self.eigNlist = nonzero_eigenvalues
|
|
666
|
+
self.eigVlist = nonzero_eigenvectors
|
|
667
|
+
|
|
668
|
+
# Calculate mode eigenvectors to try - focus on lower eigenvectors first
|
|
669
|
+
# (corresponding to softer modes that are more likely to lead to transition states)
|
|
670
|
+
search_idx = len(sorted_idx) # Start with more eigenvectors than needed
|
|
671
|
+
self.sorted_eigenvalues_idx = sorted_idx[0:search_idx]
|
|
672
|
+
|
|
673
|
+
# Initialize ADDths with initial directions
|
|
674
|
+
self.ADDths = []
|
|
675
|
+
IDnum = 0
|
|
676
|
+
|
|
677
|
+
print("### Checking ADD energy. ###")
|
|
678
|
+
with open(self.directory + "/add_energy_list.csv", "w") as f:
|
|
679
|
+
f.write("index_of_principal_axis,eigenvalue,direction,add_energy,abs_add_energy\n")
|
|
680
|
+
|
|
681
|
+
IOEsphereA = self.addf_config['IOEsphereA_initial']
|
|
682
|
+
|
|
683
|
+
# Create candidate ADDs for eigenvectors
|
|
684
|
+
candidate_ADDths = []
|
|
685
|
+
|
|
686
|
+
for idx in self.sorted_eigenvalues_idx:
|
|
687
|
+
for pm in [-1.0, 1.0]:
|
|
688
|
+
eigV = self.init_eigenvectors[:, idx]
|
|
689
|
+
|
|
690
|
+
# Create a new ADD point
|
|
691
|
+
ADDth = type('ADDthetaClass', (), {})
|
|
692
|
+
ADDth.IDnum = IDnum
|
|
693
|
+
ADDth.dim = self.dim
|
|
694
|
+
ADDth.SQ = self.SQ
|
|
695
|
+
ADDth.SQ_inv = self.SQ_inv
|
|
696
|
+
ADDth.thetalist = self.calctheta(pm * eigV, nonzero_eigenvectors, nonzero_eigenvalues)
|
|
697
|
+
|
|
698
|
+
# Generate nADD (this will be a flattened vector in the Hessian eigenspace)
|
|
699
|
+
ADDth.nADD_reduced = self.SuperSphere_cartesian(IOEsphereA, ADDth.thetalist, self.SQ, self.dim)
|
|
700
|
+
|
|
701
|
+
# We need to convert the reduced space vector back to the full coordinate space
|
|
702
|
+
# First create a zero vector in the full space
|
|
703
|
+
ADDth.nADD_full = np.zeros(n_coords)
|
|
704
|
+
|
|
705
|
+
# Map the reduced vector to the full space (this is approximate)
|
|
706
|
+
for i in range(min(len(ADDth.nADD_reduced), n_coords)):
|
|
707
|
+
ADDth.nADD_full[i] = ADDth.nADD_reduced[i]
|
|
708
|
+
|
|
709
|
+
# Reshape to molecular geometry format
|
|
710
|
+
ADDth.nADD = ADDth.nADD_full.reshape(n_atoms, 3)
|
|
711
|
+
|
|
712
|
+
# Apply the displacement to the initial geometry
|
|
713
|
+
ADDth.x = self.init_geometry + ADDth.nADD
|
|
714
|
+
ADDth.x = self.periodicpoint(ADDth.x)
|
|
715
|
+
|
|
716
|
+
# Calculate energy
|
|
717
|
+
energy, grad_x, _, iscalculationfailed = QMC.single_point(
|
|
718
|
+
None, element_number_list_1, "", self.electric_charge_and_multiplicity,
|
|
719
|
+
self.method, ADDth.x
|
|
720
|
+
)
|
|
721
|
+
|
|
722
|
+
if iscalculationfailed:
|
|
723
|
+
continue
|
|
724
|
+
|
|
725
|
+
# Apply bias if needed
|
|
726
|
+
_, bias_energy, bias_gradient, _ = BPC.main(
|
|
727
|
+
energy, grad_x, ADDth.x, element_number_list_1,
|
|
728
|
+
self.config.force_data
|
|
729
|
+
)
|
|
730
|
+
|
|
731
|
+
ADDth.A = bias_energy
|
|
732
|
+
ADDth.ADD = ADDth.A - IOEsphereA - self.init_energy
|
|
733
|
+
ADDth.ADD_IOE = ADDth.ADD # Initial value, will be updated later
|
|
734
|
+
ADDth.grad = bias_gradient
|
|
735
|
+
ADDth.grad_vec = np.dot(bias_gradient.flatten(), ADDth.nADD.flatten())
|
|
736
|
+
ADDth.grad_vec /= np.linalg.norm(ADDth.nADD.flatten())
|
|
737
|
+
ADDth.findTSQ = False
|
|
738
|
+
ADDth.ADDoptQ = False
|
|
739
|
+
ADDth.ADDremoveQ = False
|
|
740
|
+
|
|
741
|
+
# Add to candidate list
|
|
742
|
+
candidate_ADDths.append(ADDth)
|
|
743
|
+
IDnum += 1
|
|
744
|
+
|
|
745
|
+
with open(self.directory + "/add_energy_list.csv", "a") as f:
|
|
746
|
+
f.write(f"{idx},{self.init_eigenvalues[idx]},{pm},{ADDth.ADD},{abs(ADDth.ADD)}\n")
|
|
747
|
+
|
|
748
|
+
# Sort candidate ADDths by negative ADD value in descending order (-ADD value)
|
|
749
|
+
# This prioritizes more negative (favorable) paths first
|
|
750
|
+
candidate_ADDths.sort(key=lambda x: -x.ADD, reverse=True)
|
|
751
|
+
|
|
752
|
+
# Select only the top n ADDs according to config.nadd
|
|
753
|
+
num_add = min(self.addf_config['number_of_add'], len(candidate_ADDths))
|
|
754
|
+
self.ADDths = candidate_ADDths[:num_add]
|
|
755
|
+
|
|
756
|
+
# Reassign IDs to be sequential
|
|
757
|
+
for i, ADDth in enumerate(self.ADDths):
|
|
758
|
+
ADDth.IDnum = i
|
|
759
|
+
|
|
760
|
+
print(f"### Selected top {len(self.ADDths)} ADD paths (sign-inverted ADD values, most negative first) ###")
|
|
761
|
+
for ADDth in self.ADDths:
|
|
762
|
+
print(f"ADD {ADDth.IDnum}: {ADDth.ADD:.8f} (-ADD = {-ADDth.ADD:.8f})")
|
|
763
|
+
|
|
764
|
+
# Initialize the optimized structures dictionary
|
|
765
|
+
self.optimized_structures = {}
|
|
766
|
+
|
|
767
|
+
# Create directory for optimized structures
|
|
768
|
+
os.makedirs(os.path.join(self.directory, "optimized_structures"), exist_ok=True)
|
|
769
|
+
|
|
770
|
+
print("### ADD detection complete. ###")
|
|
771
|
+
return True
|
|
772
|
+
|
|
773
|
+
def save_optimized_structure(self, ADDth, sphere_num, IOEsphereA):
|
|
774
|
+
"""Save optimized structure for a specific ADD and sphere"""
|
|
775
|
+
# Create directory path for ADD-specific structures
|
|
776
|
+
add_dir = os.path.join(self.directory, "optimized_structures", f"ADD_{ADDth.IDnum}")
|
|
777
|
+
os.makedirs(add_dir, exist_ok=True)
|
|
778
|
+
|
|
779
|
+
# Create filename with radius information
|
|
780
|
+
radius = np.sqrt(IOEsphereA)
|
|
781
|
+
filename = f"optimized_r_{radius:.4f}.xyz"
|
|
782
|
+
filepath = os.path.join(add_dir, filename)
|
|
783
|
+
|
|
784
|
+
# Write XYZ file
|
|
785
|
+
with open(filepath, 'w') as f:
|
|
786
|
+
f.write(f"{len(self.element_list)}\n")
|
|
787
|
+
f.write(f"ADD_{ADDth.IDnum} Sphere {sphere_num} Radius {radius:.4f} Energy {ADDth.ADD:.6f}\n")
|
|
788
|
+
for i, (element, coord) in enumerate(zip(self.element_list, ADDth.x)):
|
|
789
|
+
f.write(f"{element} {coord[0]:.12f} {coord[1]:.12f} {coord[2]:.12f}\n")
|
|
790
|
+
|
|
791
|
+
# Store structure information in dictionary
|
|
792
|
+
if ADDth.IDnum not in self.optimized_structures:
|
|
793
|
+
self.optimized_structures[ADDth.IDnum] = []
|
|
794
|
+
|
|
795
|
+
self.optimized_structures[ADDth.IDnum].append({
|
|
796
|
+
'sphere': sphere_num,
|
|
797
|
+
'radius': radius,
|
|
798
|
+
'energy': ADDth.ADD,
|
|
799
|
+
'file': filepath,
|
|
800
|
+
'coords': ADDth.x.copy(),
|
|
801
|
+
'comment': f"ADD_{ADDth.IDnum} Sphere {sphere_num} Radius {radius:.4f} Energy {ADDth.ADD:.6f}"
|
|
802
|
+
})
|
|
803
|
+
|
|
804
|
+
def create_separate_xyz_files(self):
|
|
805
|
+
"""Create separate XYZ files for each ADD path"""
|
|
806
|
+
created_files = []
|
|
807
|
+
|
|
808
|
+
for add_id, structures in self.optimized_structures.items():
|
|
809
|
+
# Sort structures by sphere number
|
|
810
|
+
structures.sort(key=lambda x: x['sphere'])
|
|
811
|
+
|
|
812
|
+
# Path for the ADD-specific file
|
|
813
|
+
add_trajectory_file = os.path.join(self.directory, f"ADD_{add_id}_trajectory.xyz")
|
|
814
|
+
|
|
815
|
+
# Write the trajectory file
|
|
816
|
+
with open(add_trajectory_file, 'w') as f:
|
|
817
|
+
for structure in structures:
|
|
818
|
+
f.write(f"{len(self.element_list)}\n")
|
|
819
|
+
f.write(f"{structure['comment']}\n")
|
|
820
|
+
for i, (element, coord) in enumerate(zip(self.element_list, structure['coords'])):
|
|
821
|
+
f.write(f"{element} {coord[0]:.12f} {coord[1]:.12f} {coord[2]:.12f}\n")
|
|
822
|
+
|
|
823
|
+
created_files.append(add_trajectory_file)
|
|
824
|
+
|
|
825
|
+
if created_files:
|
|
826
|
+
paths_str = "\n".join(created_files)
|
|
827
|
+
print(f"Created {len(created_files)} ADD trajectory files:\n{paths_str}")
|
|
828
|
+
|
|
829
|
+
def Opt_hyper_sphere(self, ADDths, QMC, eqpoint, IOEsphereA, IOEsphereA_r, A_eq, sphereN):
|
|
830
|
+
"""Optimize points on the hypersphere"""
|
|
831
|
+
print(f"Starting optimization on sphere {sphereN} with radius {np.sqrt(IOEsphereA):.4f}")
|
|
832
|
+
|
|
833
|
+
# Reset optimization flags
|
|
834
|
+
for ADDth in ADDths:
|
|
835
|
+
if not ADDth.findTSQ and not ADDth.ADDremoveQ:
|
|
836
|
+
ADDth.ADDoptQ = True
|
|
837
|
+
else:
|
|
838
|
+
ADDth.ADDoptQ = False
|
|
839
|
+
|
|
840
|
+
optturnN = 0
|
|
841
|
+
newADDths = []
|
|
842
|
+
n_atoms = eqpoint.shape[0]
|
|
843
|
+
|
|
844
|
+
# Create a directory for intermediate optimization steps
|
|
845
|
+
sphere_dir = os.path.join(self.directory, "optimized_structures", f"sphere_{sphereN}")
|
|
846
|
+
os.makedirs(sphere_dir, exist_ok=True)
|
|
847
|
+
|
|
848
|
+
# Optimization loop
|
|
849
|
+
while any(ADDth.ADDoptQ for ADDth in ADDths):
|
|
850
|
+
optturnN += 1
|
|
851
|
+
if optturnN >= 100:
|
|
852
|
+
print(f"Optimization exceeded 100 iterations, breaking.")
|
|
853
|
+
break
|
|
854
|
+
|
|
855
|
+
print(f"Optimization iteration {optturnN}")
|
|
856
|
+
|
|
857
|
+
# Process each ADD point in order of negative ADD value (most negative first)
|
|
858
|
+
for ADDth in sorted(ADDths, key=lambda x: -x.ADD, reverse=True):
|
|
859
|
+
if not ADDth.ADDoptQ or ADDth.ADDremoveQ:
|
|
860
|
+
continue
|
|
861
|
+
|
|
862
|
+
# Optimize this ADD point
|
|
863
|
+
self.current_id = ADDth.IDnum
|
|
864
|
+
self.current_ADD = ADDth.ADD
|
|
865
|
+
|
|
866
|
+
# Starting from zero displacement
|
|
867
|
+
x_initial = np.zeros(len(ADDth.thetalist))
|
|
868
|
+
|
|
869
|
+
# Minimize on hypersphere using our modified steepest descent
|
|
870
|
+
thetalist = self.minimizeTh_SD_SS(
|
|
871
|
+
ADDth, x_initial,
|
|
872
|
+
lambda x: QMC.single_point(None, self.get_element_number_list(), "",
|
|
873
|
+
self.electric_charge_and_multiplicity, self.method, x)[0],
|
|
874
|
+
lambda x: self.calculate_gradient(QMC, x),
|
|
875
|
+
eqpoint, IOEsphereA
|
|
876
|
+
)
|
|
877
|
+
|
|
878
|
+
if thetalist is False:
|
|
879
|
+
ADDth.ADDremoveQ = True
|
|
880
|
+
print(f"ADD {ADDth.IDnum} optimization failed, marking for removal")
|
|
881
|
+
continue
|
|
882
|
+
|
|
883
|
+
# Update ADD point with optimized position
|
|
884
|
+
ADDth.thetalist = thetalist
|
|
885
|
+
|
|
886
|
+
# Generate nADD in the reduced space
|
|
887
|
+
ADDth.nADD_reduced = self.SuperSphere_cartesian(IOEsphereA, ADDth.thetalist, self.SQ, self.dim)
|
|
888
|
+
|
|
889
|
+
# Map to full coordinate space
|
|
890
|
+
ADDth.nADD_full = np.zeros(n_atoms * 3)
|
|
891
|
+
for i in range(min(len(ADDth.nADD_reduced), n_atoms * 3)):
|
|
892
|
+
ADDth.nADD_full[i] = ADDth.nADD_reduced[i]
|
|
893
|
+
|
|
894
|
+
# Reshape to molecular geometry
|
|
895
|
+
ADDth.nADD = ADDth.nADD_full.reshape(n_atoms, 3)
|
|
896
|
+
|
|
897
|
+
# Calculate new coordinates
|
|
898
|
+
ADDth.x = eqpoint + ADDth.nADD
|
|
899
|
+
ADDth.x = self.periodicpoint(ADDth.x)
|
|
900
|
+
|
|
901
|
+
# Calculate new energy
|
|
902
|
+
energy, grad, _, iscalculationfailed = QMC.single_point(
|
|
903
|
+
None, self.get_element_number_list(), "",
|
|
904
|
+
self.electric_charge_and_multiplicity, self.method, ADDth.x
|
|
905
|
+
)
|
|
906
|
+
|
|
907
|
+
if iscalculationfailed:
|
|
908
|
+
ADDth.ADDremoveQ = True
|
|
909
|
+
continue
|
|
910
|
+
|
|
911
|
+
BPC = BiasPotentialCalculation(self.config.iEIP_FOLDER_DIRECTORY)
|
|
912
|
+
_, bias_energy, bias_grad, _ = BPC.main(
|
|
913
|
+
energy, grad, ADDth.x, self.get_element_number_list(),
|
|
914
|
+
self.config.force_data
|
|
915
|
+
)
|
|
916
|
+
|
|
917
|
+
ADDth.A = bias_energy
|
|
918
|
+
ADDth.ADD = ADDth.A - IOEsphereA - A_eq
|
|
919
|
+
|
|
920
|
+
# Calculate ADD_IOE with IOE contributions
|
|
921
|
+
self.current_id = ADDth.IDnum
|
|
922
|
+
self.current_ADD = ADDth.ADD
|
|
923
|
+
ADDth.ADD_IOE = ADDth.ADD + self.IOE_total(ADDth.nADD_full)
|
|
924
|
+
|
|
925
|
+
# Mark as optimized
|
|
926
|
+
ADDth.ADDoptQ = False
|
|
927
|
+
|
|
928
|
+
print(f"ADD {ADDth.IDnum} optimized: ADD={ADDth.ADD:.4f} (-ADD = {-ADDth.ADD:.4f}), ADD_IOE={ADDth.ADD_IOE:.4f}")
|
|
929
|
+
print(f"Grad {np.linalg.norm(grad):.6f}")
|
|
930
|
+
print(f"Energy {energy:.6f}")
|
|
931
|
+
print()
|
|
932
|
+
|
|
933
|
+
# Save XYZ file after each ADD optimization step
|
|
934
|
+
# Use both iteration number and ADD ID in filename to ensure uniqueness
|
|
935
|
+
filename = f"iteration_{optturnN}_ADD_{ADDth.IDnum}.xyz"
|
|
936
|
+
filepath = os.path.join(sphere_dir, filename)
|
|
937
|
+
|
|
938
|
+
# Write XYZ file for this optimization step
|
|
939
|
+
with open(filepath, 'w') as f:
|
|
940
|
+
f.write(f"{len(self.element_list)}\n")
|
|
941
|
+
f.write(f"Sphere {sphereN} Iteration {optturnN} ADD_{ADDth.IDnum} Radius {np.sqrt(IOEsphereA):.4f} Energy {ADDth.ADD:.6f}\n")
|
|
942
|
+
for i, (element, coord) in enumerate(zip(self.element_list, ADDth.x)):
|
|
943
|
+
f.write(f"{element} {coord[0]:.6f} {coord[1]:.6f} {coord[2]:.6f}\n")
|
|
944
|
+
|
|
945
|
+
print(f"Saved intermediate structure to {filepath}")
|
|
946
|
+
|
|
947
|
+
# Check for similar points already found
|
|
948
|
+
for existing_add in newADDths:
|
|
949
|
+
if self.angle_SHS(ADDth.nADD_full, existing_add.nADD_full, self.SQ_inv) < 0.01:
|
|
950
|
+
ADDth.ADDremoveQ = True
|
|
951
|
+
print(f"ADD {ADDth.IDnum} too similar to ADD {existing_add.IDnum}, marking for removal")
|
|
952
|
+
break
|
|
953
|
+
|
|
954
|
+
if not ADDth.ADDremoveQ and ADDth.ADD_IOE < 0: # Only keep negative ADD_IOE points
|
|
955
|
+
newADDths.append(ADDth)
|
|
956
|
+
|
|
957
|
+
# Save optimized structure for this sphere (in the regular directory structure)
|
|
958
|
+
self.save_optimized_structure(ADDth, sphereN, IOEsphereA)
|
|
959
|
+
|
|
960
|
+
# Filter ADDths list
|
|
961
|
+
ADDths = [ADDth for ADDth in ADDths if not ADDth.ADDremoveQ]
|
|
962
|
+
|
|
963
|
+
# Sort by negative ADD value (most negative first) for next iteration
|
|
964
|
+
ADDths.sort(key=lambda x: -x.ADD, reverse=True)
|
|
965
|
+
|
|
966
|
+
# Sort the final ADDths list by negative ADD value (most negative first)
|
|
967
|
+
newADDths.sort(key=lambda x: -x.ADD, reverse=True)
|
|
968
|
+
|
|
969
|
+
return newADDths if newADDths else ADDths
|
|
970
|
+
|
|
971
|
+
def add_following(self, QMC):
|
|
972
|
+
"""Follow ADD paths to find transition states"""
|
|
973
|
+
print("### Start ADD Following. ###")
|
|
974
|
+
|
|
975
|
+
IOEsphereA = self.addf_config['IOEsphereA_initial']
|
|
976
|
+
IOEsphereA_r = self.addf_config['IOEsphereA_dist']
|
|
977
|
+
A_eq = self.init_energy
|
|
978
|
+
|
|
979
|
+
TSinitialpoints = []
|
|
980
|
+
sphereN = 0
|
|
981
|
+
|
|
982
|
+
# Main ADD following loop
|
|
983
|
+
while sphereN < self.addf_config["step_number"]: # Limit to prevent infinite loops
|
|
984
|
+
sphereN += 1
|
|
985
|
+
print(f"\n### Sphere {sphereN} with radius {np.sqrt(IOEsphereA):.4f} ###\n")
|
|
986
|
+
|
|
987
|
+
# Sort ADDths by absolute ADD value (largest magnitude first)
|
|
988
|
+
self.ADDths.sort(key=lambda x: abs(x.ADD), reverse=True)
|
|
989
|
+
|
|
990
|
+
# Optimize on current hypersphere
|
|
991
|
+
self.ADDths = self.Opt_hyper_sphere(
|
|
992
|
+
self.ADDths, QMC, self.init_geometry, IOEsphereA, IOEsphereA_r, A_eq, sphereN
|
|
993
|
+
)
|
|
994
|
+
|
|
995
|
+
# Check for TS points and update ADD status
|
|
996
|
+
for ADDth in self.ADDths:
|
|
997
|
+
if ADDth.ADDremoveQ:
|
|
998
|
+
continue
|
|
999
|
+
|
|
1000
|
+
# Calculate gradient at current point
|
|
1001
|
+
grad_x = self.calculate_gradient(QMC, ADDth.x)
|
|
1002
|
+
if grad_x is False:
|
|
1003
|
+
ADDth.ADDremoveQ = True
|
|
1004
|
+
continue
|
|
1005
|
+
|
|
1006
|
+
ADDth.grad = grad_x
|
|
1007
|
+
|
|
1008
|
+
# Calculate normalized displacement vector
|
|
1009
|
+
normalized_nADD = ADDth.nADD.flatten() / np.linalg.norm(ADDth.nADD.flatten())
|
|
1010
|
+
|
|
1011
|
+
# Calculate projection of gradient onto displacement vector
|
|
1012
|
+
ADDth.grad_vec = np.dot(ADDth.grad.flatten(), normalized_nADD)
|
|
1013
|
+
|
|
1014
|
+
# Check if we've found a TS (gradient points downward)
|
|
1015
|
+
if sphereN > 5 and ADDth.grad_vec < 0.0:
|
|
1016
|
+
print(f"New TS point found at ADD {ADDth.IDnum}")
|
|
1017
|
+
ADDth.findTSQ = True
|
|
1018
|
+
TSinitialpoints.append(ADDth.x)
|
|
1019
|
+
|
|
1020
|
+
# If all ADDs are done, exit
|
|
1021
|
+
if all(ADDth.findTSQ or ADDth.ADDremoveQ for ADDth in self.ADDths):
|
|
1022
|
+
print("All ADD paths complete.")
|
|
1023
|
+
break
|
|
1024
|
+
|
|
1025
|
+
# Increase sphere size for next iteration
|
|
1026
|
+
IOEsphereA = (np.sqrt(IOEsphereA) + IOEsphereA_r) ** 2
|
|
1027
|
+
print(f"Expanding sphere to radius {np.sqrt(IOEsphereA):.4f}")
|
|
1028
|
+
|
|
1029
|
+
# Save displacement vectors for debugging
|
|
1030
|
+
with open(os.path.join(self.directory, f"displacement_vectors_sphere_{sphereN}.csv"), "w") as f:
|
|
1031
|
+
f.write("ADD_ID,x,y,z,ADD,ADD_IOE\n")
|
|
1032
|
+
for ADDth in self.ADDths:
|
|
1033
|
+
if ADDth.ADDremoveQ:
|
|
1034
|
+
continue
|
|
1035
|
+
for i in range(len(ADDth.nADD)):
|
|
1036
|
+
f.write(f"{ADDth.IDnum},{ADDth.nADD[i][0]},{ADDth.nADD[i][1]},{ADDth.nADD[i][2]},{ADDth.ADD},{ADDth.ADD_IOE}\n")
|
|
1037
|
+
|
|
1038
|
+
# Create separate trajectory files for each ADD path
|
|
1039
|
+
self.create_separate_xyz_files()
|
|
1040
|
+
|
|
1041
|
+
# Write TS points
|
|
1042
|
+
if TSinitialpoints:
|
|
1043
|
+
print(f"Found {len(TSinitialpoints)} potential transition states.")
|
|
1044
|
+
self.write_ts_points(TSinitialpoints)
|
|
1045
|
+
|
|
1046
|
+
return len(TSinitialpoints) > 0
|
|
1047
|
+
|
|
1048
|
+
def calculate_gradient(self, QMC, x):
|
|
1049
|
+
"""Calculate gradient at point x"""
|
|
1050
|
+
element_number_list = self.get_element_number_list()
|
|
1051
|
+
_, grad_x, _, iscalculationfailed = QMC.single_point(
|
|
1052
|
+
None, element_number_list, "", self.electric_charge_and_multiplicity,
|
|
1053
|
+
self.method, x
|
|
1054
|
+
)
|
|
1055
|
+
|
|
1056
|
+
if iscalculationfailed:
|
|
1057
|
+
return False
|
|
1058
|
+
|
|
1059
|
+
# Apply bias if needed
|
|
1060
|
+
BPC = BiasPotentialCalculation(self.config.iEIP_FOLDER_DIRECTORY)
|
|
1061
|
+
_, _, bias_gradient, _ = BPC.main(
|
|
1062
|
+
0, grad_x, x, element_number_list, self.config.force_data
|
|
1063
|
+
)
|
|
1064
|
+
|
|
1065
|
+
return bias_gradient
|
|
1066
|
+
|
|
1067
|
+
def write_ts_points(self, ts_points):
|
|
1068
|
+
"""Write TS points to file"""
|
|
1069
|
+
with open(f"{self.directory}/TSpoints.xyz", "w") as f:
|
|
1070
|
+
for i, point in enumerate(ts_points):
|
|
1071
|
+
f.write(f"{len(self.element_list)}\n")
|
|
1072
|
+
f.write(f"TS candidate {i+1}\n")
|
|
1073
|
+
for j, (element, coord) in enumerate(zip(self.element_list, point)):
|
|
1074
|
+
f.write(f"{element} {coord[0]:.6f} {coord[1]:.6f} {coord[2]:.6f}\n")
|
|
1075
|
+
|
|
1076
|
+
# Getters and setters - keep as is
|
|
1077
|
+
def set_molecule(self, element_list, coords):
|
|
1078
|
+
self.element_list = element_list
|
|
1079
|
+
self.coords = coords
|
|
1080
|
+
|
|
1081
|
+
def set_gradient(self, gradient):
|
|
1082
|
+
self.gradient = gradient
|
|
1083
|
+
|
|
1084
|
+
def set_hessian(self, hessian):
|
|
1085
|
+
self.hessian = hessian
|
|
1086
|
+
|
|
1087
|
+
def set_energy(self, energy):
|
|
1088
|
+
self.energy = energy
|
|
1089
|
+
|
|
1090
|
+
def set_coords(self, coords):
|
|
1091
|
+
self.coords = coords
|
|
1092
|
+
|
|
1093
|
+
def set_element_list(self, element_list):
|
|
1094
|
+
self.element_list = element_list
|
|
1095
|
+
self.element_number_list = [element_number(i) for i in self.element_list]
|
|
1096
|
+
|
|
1097
|
+
def set_coord(self, coord):
|
|
1098
|
+
self.coords = coord
|
|
1099
|
+
|
|
1100
|
+
def get_coord(self):
|
|
1101
|
+
return self.coords
|
|
1102
|
+
|
|
1103
|
+
def get_element_list(self):
|
|
1104
|
+
return self.element_list
|
|
1105
|
+
|
|
1106
|
+
def get_element_number_list(self):
|
|
1107
|
+
if self.element_number_list is None:
|
|
1108
|
+
if self.element_list is None:
|
|
1109
|
+
raise ValueError('Element list is not set.')
|
|
1110
|
+
self.element_number_list = [element_number(i) for i in self.element_list]
|
|
1111
|
+
return self.element_number_list
|
|
1112
|
+
|
|
1113
|
+
def set_mole_info(self, base_file_name, electric_charge_and_multiplicity):
|
|
1114
|
+
coord, element_list, electric_charge_and_multiplicity = xyz2list(
|
|
1115
|
+
base_file_name + ".xyz", electric_charge_and_multiplicity)
|
|
1116
|
+
|
|
1117
|
+
if self.config.usextb != "None":
|
|
1118
|
+
self.method = self.config.usextb
|
|
1119
|
+
elif self.config.usedxtb != "None":
|
|
1120
|
+
self.method = self.config.usedxtb
|
|
1121
|
+
else:
|
|
1122
|
+
self.method = "None"
|
|
1123
|
+
|
|
1124
|
+
self.coords = np.array(coord, dtype="float64")
|
|
1125
|
+
self.element_list = element_list
|
|
1126
|
+
self.electric_charge_and_multiplicity = electric_charge_and_multiplicity
|
|
1127
|
+
|
|
1128
|
+
def run(self, file_directory, SP, electric_charge_and_multiplicity, FIO_img):
|
|
1129
|
+
print("### Start Anharmonic Downward Distortion (ADD) method ###")
|
|
1130
|
+
|
|
1131
|
+
# Preparation
|
|
1132
|
+
base_file_name = os.path.splitext(FIO_img.START_FILE)[0]
|
|
1133
|
+
self.set_mole_info(base_file_name, electric_charge_and_multiplicity)
|
|
1134
|
+
|
|
1135
|
+
self.directory = make_workspace(file_directory)
|
|
1136
|
+
|
|
1137
|
+
# Create main directory for optimized structures
|
|
1138
|
+
os.makedirs(os.path.join(self.directory, "optimized_structures"), exist_ok=True)
|
|
1139
|
+
|
|
1140
|
+
# Detect initial ADD directions
|
|
1141
|
+
success = self.detect_add(SP)
|
|
1142
|
+
if not success:
|
|
1143
|
+
return False
|
|
1144
|
+
|
|
1145
|
+
# Follow ADD paths to find transition states
|
|
1146
|
+
isConverged = self.add_following(SP)
|
|
1147
|
+
|
|
1148
|
+
print("### ADD Following is done. ###")
|
|
1149
|
+
|
|
1150
|
+
return isConverged
|