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,895 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import datetime
|
|
3
|
+
import os
|
|
4
|
+
import glob
|
|
5
|
+
|
|
6
|
+
from multioptpy.Potential.potential import BiasPotentialCalculation
|
|
7
|
+
from multioptpy.Visualization.visualization import Graph
|
|
8
|
+
from multioptpy.fileio import make_workspace, xyz2list
|
|
9
|
+
from multioptpy.Parameters.parameter import UnitValueLib, element_number
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DimerMethod:
|
|
13
|
+
def __init__(self, config):
|
|
14
|
+
"""
|
|
15
|
+
Implementation of the Dimer method for finding saddle points.
|
|
16
|
+
|
|
17
|
+
The Dimer method is a minimum mode following technique that uses
|
|
18
|
+
two points (a dimer) to find the lowest curvature mode without
|
|
19
|
+
explicitly calculating the Hessian matrix.
|
|
20
|
+
|
|
21
|
+
References:
|
|
22
|
+
- J. Chem. Phys. 111, 7010 (1999) - Original Dimer Method
|
|
23
|
+
- J. Chem. Phys. 121, 9776 (2004) - Improvements
|
|
24
|
+
- J. Chem. Phys. 123, 224101 (2005) - Additional improvements
|
|
25
|
+
- J. Chem. Phys. 128, 014106 (2008) - Force extrapolation scheme
|
|
26
|
+
"""
|
|
27
|
+
self.config = config
|
|
28
|
+
self.energy_list = []
|
|
29
|
+
self.gradient_list = []
|
|
30
|
+
self.curvature_list = []
|
|
31
|
+
self.init_displacement = 0.03 / self.get_unit_conversion() # Bohr
|
|
32
|
+
self.date = datetime.datetime.now().strftime("%Y_%m_%d_%H_%M_%S")
|
|
33
|
+
self.converge_criteria = 0.001 # Force convergence criteria in a.u.
|
|
34
|
+
self.element_number_list = None
|
|
35
|
+
self.optimized_structures = {} # Dictionary to store optimized structures
|
|
36
|
+
|
|
37
|
+
# Dimer method specific parameters
|
|
38
|
+
self.dimer_parameters = {
|
|
39
|
+
'dimer_separation': 0.0001, # Distance between dimer images (Bohr)
|
|
40
|
+
'trial_angle': np.pi / 32.0, # Trial rotation angle (radians)
|
|
41
|
+
'f_rot_min': 0.1, # Min rotational force for rotation
|
|
42
|
+
'f_rot_max': 1.0, # Max rotational force for only one rotation
|
|
43
|
+
'max_num_rot': 5, # Maximum number of rotations per step
|
|
44
|
+
'extrapolate_forces': True, # Use force extrapolation scheme
|
|
45
|
+
'max_iterations': 1000, # Maximum iterations for dimer optimization
|
|
46
|
+
'max_step': 0.1, # Maximum translation step size
|
|
47
|
+
'trial_step': 0.001, # Step size for curvature estimation
|
|
48
|
+
'cg_translation': True, # Use conjugate gradient for translation
|
|
49
|
+
'cg_rotation': False, # Use conjugate gradient for rotation
|
|
50
|
+
'quickmin_rotation': True, # Use quickmin for rotation
|
|
51
|
+
'max_rot_iterations': 64, # Max iterations for rotation
|
|
52
|
+
'max_force_rotation': 1e-3, # Max force for rotation convergence
|
|
53
|
+
'potim': 0.1, # Time step for quickmin
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# For CG optimization (translation)
|
|
57
|
+
self.cg_init_translation = True
|
|
58
|
+
self.old_direction_translation = None
|
|
59
|
+
self.cg_direction_translation = None
|
|
60
|
+
|
|
61
|
+
# For CG optimization (rotation)
|
|
62
|
+
self.cg_init_rotation = True
|
|
63
|
+
self.old_rot_force = None
|
|
64
|
+
self.old_rot_gradient = None
|
|
65
|
+
self.current_rot_gradient_unit = None
|
|
66
|
+
self.rot_velocity = None # For QuickMin rotation
|
|
67
|
+
|
|
68
|
+
def get_unit_conversion(self):
|
|
69
|
+
"""Return bohr to angstrom conversion factor"""
|
|
70
|
+
return UnitValueLib().bohr2angstroms # Approximate value for bohr2angstroms
|
|
71
|
+
|
|
72
|
+
def adjust_center2origin(self, coord):
|
|
73
|
+
"""Adjust coordinates to have center at origin"""
|
|
74
|
+
center = np.mean(coord, axis=0)
|
|
75
|
+
return coord - center
|
|
76
|
+
|
|
77
|
+
def normalize(self, vector):
|
|
78
|
+
"""Create a unit vector along *vector*"""
|
|
79
|
+
vector_flat = vector.flatten()
|
|
80
|
+
norm = np.linalg.norm(vector_flat)
|
|
81
|
+
if norm < 1e-10:
|
|
82
|
+
return vector # Return original vector if it's too small
|
|
83
|
+
return (vector_flat / norm).reshape(vector.shape)
|
|
84
|
+
|
|
85
|
+
def parallel_vector(self, vector, base):
|
|
86
|
+
"""Extract the components of *vector* that are parallel to *base*"""
|
|
87
|
+
vector_flat = vector.flatten()
|
|
88
|
+
base_flat = base.flatten()
|
|
89
|
+
base_norm = np.linalg.norm(base_flat)
|
|
90
|
+
if base_norm < 1e-10:
|
|
91
|
+
return np.zeros_like(vector)
|
|
92
|
+
base_unit = base_flat / base_norm
|
|
93
|
+
return (np.dot(vector_flat, base_unit) * base_unit).reshape(vector.shape)
|
|
94
|
+
|
|
95
|
+
def perpendicular_vector(self, vector, base):
|
|
96
|
+
"""Remove the components of *vector* that are parallel to *base*"""
|
|
97
|
+
return vector - self.parallel_vector(vector, base)
|
|
98
|
+
|
|
99
|
+
def rotate_vector_around_axis(self, vec_to_rotate, axis, angle):
|
|
100
|
+
"""Rotates a vector around a given axis by a specified angle (Rodrigues' rotation formula)"""
|
|
101
|
+
axis = self.normalize(axis)
|
|
102
|
+
k = axis.flatten()
|
|
103
|
+
v = vec_to_rotate.flatten()
|
|
104
|
+
|
|
105
|
+
v_rot = v * np.cos(angle) + np.cross(k, v) * np.sin(angle) + k * np.dot(k, v) * (1 - np.cos(angle))
|
|
106
|
+
return v_rot.reshape(vec_to_rotate.shape)
|
|
107
|
+
|
|
108
|
+
def print_status(self, iteration, energy, curvature, max_force, rot_force=None, rotation_angle=None):
|
|
109
|
+
"""Print status information during optimization"""
|
|
110
|
+
status = f"Iteration {iteration}: Energy = {energy:.6f}, Curvature = {curvature:.6f}, Max Force = {max_force:.6f}"
|
|
111
|
+
if rot_force is not None:
|
|
112
|
+
status += f", Rotational Force = {rot_force:.6f}"
|
|
113
|
+
if rotation_angle is not None:
|
|
114
|
+
status += f", Rotation Angle = {rotation_angle:.6f} rad"
|
|
115
|
+
print(status)
|
|
116
|
+
|
|
117
|
+
def get_cg_direction_translation(self, direction):
|
|
118
|
+
"""Apply Conjugate Gradient algorithm to step direction for translation"""
|
|
119
|
+
direction_shape = direction.shape
|
|
120
|
+
direction_flat = direction.flatten()
|
|
121
|
+
|
|
122
|
+
if self.cg_init_translation:
|
|
123
|
+
self.cg_init_translation = False
|
|
124
|
+
self.old_direction_translation = direction_flat.copy()
|
|
125
|
+
self.cg_direction_translation = direction_flat.copy()
|
|
126
|
+
|
|
127
|
+
old_norm = np.dot(self.old_direction_translation, self.old_direction_translation)
|
|
128
|
+
|
|
129
|
+
# Polak-Ribiere formula for conjugate gradient
|
|
130
|
+
if old_norm > 1e-10:
|
|
131
|
+
betaPR = np.dot(direction_flat,
|
|
132
|
+
(direction_flat - self.old_direction_translation)) / old_norm
|
|
133
|
+
else:
|
|
134
|
+
betaPR = 0.0
|
|
135
|
+
|
|
136
|
+
if betaPR < 0.0:
|
|
137
|
+
betaPR = 0.0
|
|
138
|
+
|
|
139
|
+
self.cg_direction_translation = direction_flat + self.cg_direction_translation * betaPR
|
|
140
|
+
self.old_direction_translation = direction_flat.copy()
|
|
141
|
+
|
|
142
|
+
return self.cg_direction_translation.reshape(direction_shape)
|
|
143
|
+
|
|
144
|
+
def calculate_gradient(self, QMC, x):
|
|
145
|
+
"""Calculate gradient at point x"""
|
|
146
|
+
element_number_list = self.get_element_number_list()
|
|
147
|
+
_, grad_x, _, iscalculationfailed = QMC.single_point(
|
|
148
|
+
None, element_number_list, "", self.electric_charge_and_multiplicity,
|
|
149
|
+
self.method, x
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
if iscalculationfailed:
|
|
153
|
+
return False
|
|
154
|
+
|
|
155
|
+
# Apply bias if needed
|
|
156
|
+
BPC = BiasPotentialCalculation(self.config.iEIP_FOLDER_DIRECTORY)
|
|
157
|
+
_, _, bias_gradient, _ = BPC.main(
|
|
158
|
+
0, grad_x, x, element_number_list, self.config.force_data
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
return bias_gradient
|
|
162
|
+
|
|
163
|
+
def dimer_rotate(self, SP_obj, center_coords, dimer_axis, element_list, charge_multiplicity, method):
|
|
164
|
+
"""
|
|
165
|
+
Perform dimer rotation to find the lowest curvature direction.
|
|
166
|
+
|
|
167
|
+
Parameters:
|
|
168
|
+
-----------
|
|
169
|
+
SP_obj : Calculation object
|
|
170
|
+
Object for performing single point calculations
|
|
171
|
+
center_coords : ndarray
|
|
172
|
+
Coordinates of the center point of the dimer
|
|
173
|
+
dimer_axis : ndarray
|
|
174
|
+
Current orientation of the dimer axis
|
|
175
|
+
element_list : list
|
|
176
|
+
List of element symbols
|
|
177
|
+
charge_multiplicity : list
|
|
178
|
+
[charge, multiplicity]
|
|
179
|
+
method : str
|
|
180
|
+
Calculation method
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
--------
|
|
184
|
+
ndarray, float, bool
|
|
185
|
+
New dimer axis, curvature along this axis, and success flag
|
|
186
|
+
"""
|
|
187
|
+
# Parameters for rotation
|
|
188
|
+
dR = self.dimer_parameters['dimer_separation']
|
|
189
|
+
trial_angle = self.dimer_parameters['trial_angle']
|
|
190
|
+
max_rot_iterations = self.dimer_parameters['max_rot_iterations']
|
|
191
|
+
max_force_rotation = self.dimer_parameters['max_force_rotation']
|
|
192
|
+
cg_rotation = self.dimer_parameters['cg_rotation']
|
|
193
|
+
quickmin_rotation = self.dimer_parameters['quickmin_rotation']
|
|
194
|
+
potim = self.dimer_parameters['potim']
|
|
195
|
+
|
|
196
|
+
# Ensure dimer_axis has correct shape and is normalized
|
|
197
|
+
dimer_axis = self.normalize(np.array(dimer_axis).reshape(center_coords.shape))
|
|
198
|
+
|
|
199
|
+
# Initial forces and energies at center_coords
|
|
200
|
+
energy_center, forces_center, _, failed = SP_obj.single_point(
|
|
201
|
+
None, element_list, "", charge_multiplicity, method, center_coords
|
|
202
|
+
)
|
|
203
|
+
if failed:
|
|
204
|
+
return None, None, True
|
|
205
|
+
|
|
206
|
+
# Calculate forces at the dimer endpoints
|
|
207
|
+
pos1 = center_coords + dimer_axis * dR
|
|
208
|
+
pos2 = center_coords - dimer_axis * dR
|
|
209
|
+
|
|
210
|
+
energy1, forces1, _, failed = SP_obj.single_point(
|
|
211
|
+
None, element_list, "", charge_multiplicity, method, pos1
|
|
212
|
+
)
|
|
213
|
+
if failed:
|
|
214
|
+
return None, None, True
|
|
215
|
+
|
|
216
|
+
energy2, forces2, _, failed = SP_obj.single_point(
|
|
217
|
+
None, element_list, "", charge_multiplicity, method, pos2
|
|
218
|
+
)
|
|
219
|
+
if failed:
|
|
220
|
+
return None, None, True
|
|
221
|
+
|
|
222
|
+
# Apply bias potential
|
|
223
|
+
BPC = BiasPotentialCalculation(self.config.iEIP_FOLDER_DIRECTORY)
|
|
224
|
+
_, bias_energy_center, bias_forces_center, _ = BPC.main(
|
|
225
|
+
energy_center, forces_center, center_coords, element_list, self.config.force_data
|
|
226
|
+
)
|
|
227
|
+
_, bias_energy1, bias_forces1, _ = BPC.main(
|
|
228
|
+
energy1, forces1, pos1, element_list, self.config.force_data
|
|
229
|
+
)
|
|
230
|
+
_, bias_energy2, bias_forces2, _ = BPC.main(
|
|
231
|
+
energy2, forces2, pos2, element_list, self.config.force_data
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
# Update with bias forces
|
|
235
|
+
forces_center = bias_forces_center
|
|
236
|
+
forces1 = bias_forces1
|
|
237
|
+
forces2 = bias_forces2
|
|
238
|
+
|
|
239
|
+
# Calculate rotational forces and curvature
|
|
240
|
+
fd1 = np.sum(forces1 * dimer_axis)
|
|
241
|
+
fd2 = np.sum(forces2 * dimer_axis)
|
|
242
|
+
fn1 = forces1 - dimer_axis * fd1
|
|
243
|
+
fn2 = forces2 - dimer_axis * fd2
|
|
244
|
+
|
|
245
|
+
rotational_force_gradient = (fn1 - fn2) / dR
|
|
246
|
+
rotational_force_magnitude = np.linalg.norm(rotational_force_gradient.flatten())
|
|
247
|
+
|
|
248
|
+
curvature = np.dot((forces2 - forces1).flatten(), dimer_axis.flatten()) / (2.0 * dR)
|
|
249
|
+
|
|
250
|
+
# Initialize for rotation
|
|
251
|
+
current_dimer_axis = dimer_axis.copy()
|
|
252
|
+
rot_iteration = 0
|
|
253
|
+
|
|
254
|
+
if quickmin_rotation:
|
|
255
|
+
# QuickMin for dimer rotation
|
|
256
|
+
if self.rot_velocity is None:
|
|
257
|
+
self.rot_velocity = np.zeros_like(center_coords)
|
|
258
|
+
|
|
259
|
+
while rot_iteration < max_rot_iterations:
|
|
260
|
+
# Calculate velocity update
|
|
261
|
+
dv_rot = rotational_force_gradient * (dR * potim)
|
|
262
|
+
vdv = np.sum(self.rot_velocity * dv_rot)
|
|
263
|
+
|
|
264
|
+
# Update velocity
|
|
265
|
+
if vdv > 0.0 and np.sum(dv_rot**2) > 1e-10:
|
|
266
|
+
self.rot_velocity = dv_rot * (1.0 + vdv / np.sum(dv_rot**2))
|
|
267
|
+
else:
|
|
268
|
+
self.rot_velocity = dv_rot
|
|
269
|
+
|
|
270
|
+
# Update dimer axis
|
|
271
|
+
current_r1 = center_coords + current_dimer_axis * dR
|
|
272
|
+
new_r1 = current_r1 + self.rot_velocity * potim
|
|
273
|
+
|
|
274
|
+
new_dimer_axis_unnormalized = new_r1 - center_coords
|
|
275
|
+
current_dimer_axis = self.normalize(new_dimer_axis_unnormalized)
|
|
276
|
+
|
|
277
|
+
# Recalculate forces for new dimer axis
|
|
278
|
+
pos1_new = center_coords + current_dimer_axis * dR
|
|
279
|
+
energy1_new, forces1_new, _, failed = SP_obj.single_point(
|
|
280
|
+
None, element_list, "", charge_multiplicity, method, pos1_new
|
|
281
|
+
)
|
|
282
|
+
if failed:
|
|
283
|
+
return None, None, True
|
|
284
|
+
|
|
285
|
+
# Apply bias potential
|
|
286
|
+
_, _, bias_forces1_new, _ = BPC.main(
|
|
287
|
+
energy1_new, forces1_new, pos1_new, element_list, self.config.force_data
|
|
288
|
+
)
|
|
289
|
+
forces1_new = bias_forces1_new
|
|
290
|
+
|
|
291
|
+
forces2_new = 2 * forces_center - forces1_new
|
|
292
|
+
|
|
293
|
+
fd1_new = np.sum(forces1_new * current_dimer_axis)
|
|
294
|
+
fd2_new = np.sum(forces2_new * current_dimer_axis)
|
|
295
|
+
fn1_new = forces1_new - current_dimer_axis * fd1_new
|
|
296
|
+
fn2_new = forces2_new - current_dimer_axis * fd2_new
|
|
297
|
+
rotational_force_gradient = (fn1_new - fn2_new) / dR
|
|
298
|
+
rotational_force_magnitude = np.linalg.norm(rotational_force_gradient.flatten())
|
|
299
|
+
|
|
300
|
+
# Update curvature
|
|
301
|
+
curvature = np.dot((forces2_new - forces1_new).flatten(), current_dimer_axis.flatten()) / (2.0 * dR)
|
|
302
|
+
|
|
303
|
+
self.print_status(rot_iteration, bias_energy_center, curvature,
|
|
304
|
+
np.max(np.abs(forces_center)), rotational_force_magnitude)
|
|
305
|
+
|
|
306
|
+
if rotational_force_magnitude < max_force_rotation or rot_iteration >= max_rot_iterations - 1:
|
|
307
|
+
break
|
|
308
|
+
|
|
309
|
+
rot_iteration += 1
|
|
310
|
+
|
|
311
|
+
elif cg_rotation:
|
|
312
|
+
# Conjugate Gradient for rotation
|
|
313
|
+
if self.cg_init_rotation:
|
|
314
|
+
self.old_rot_force = rotational_force_gradient.copy()
|
|
315
|
+
self.old_rot_gradient = rotational_force_gradient.copy()
|
|
316
|
+
self.current_rot_gradient_unit = self.normalize(rotational_force_gradient)
|
|
317
|
+
self.cg_init_rotation = False
|
|
318
|
+
|
|
319
|
+
while rot_iteration < max_rot_iterations:
|
|
320
|
+
# Polak-Ribiere formula
|
|
321
|
+
gam_n = 0.0
|
|
322
|
+
if np.linalg.norm(self.old_rot_force.flatten()) > 1e-10:
|
|
323
|
+
gam_n = np.dot(rotational_force_gradient.flatten(),
|
|
324
|
+
(rotational_force_gradient - self.old_rot_force).flatten()) / np.dot(self.old_rot_force.flatten(), self.old_rot_force.flatten())
|
|
325
|
+
if gam_n < 0:
|
|
326
|
+
gam_n = 0.0
|
|
327
|
+
|
|
328
|
+
# Update gradient
|
|
329
|
+
current_rot_gradient = rotational_force_gradient + self.old_rot_gradient * gam_n
|
|
330
|
+
self.current_rot_gradient_unit = self.normalize(current_rot_gradient)
|
|
331
|
+
|
|
332
|
+
# Calculate force components
|
|
333
|
+
fnp1 = self.current_rot_gradient_unit * np.sum(rotational_force_gradient * self.current_rot_gradient_unit)
|
|
334
|
+
fnrp1 = np.sum(fnp1 * self.current_rot_gradient_unit)
|
|
335
|
+
|
|
336
|
+
# Trial rotation
|
|
337
|
+
n_tmp = current_dimer_axis.copy()
|
|
338
|
+
gnu_tmp = self.current_rot_gradient_unit.copy()
|
|
339
|
+
rotated_n_trial = self.rotate_vector_around_axis(n_tmp, gnu_tmp, trial_angle)
|
|
340
|
+
|
|
341
|
+
# Calculate forces at trial position
|
|
342
|
+
pos1_trial = center_coords + rotated_n_trial * dR
|
|
343
|
+
energy1_trial, forces1_trial, _, failed = SP_obj.single_point(
|
|
344
|
+
None, element_list, "", charge_multiplicity, method, pos1_trial
|
|
345
|
+
)
|
|
346
|
+
if failed:
|
|
347
|
+
return None, None, True
|
|
348
|
+
|
|
349
|
+
# Apply bias potential
|
|
350
|
+
_, _, bias_forces1_trial, _ = BPC.main(
|
|
351
|
+
energy1_trial, forces1_trial, pos1_trial, element_list, self.config.force_data
|
|
352
|
+
)
|
|
353
|
+
forces1_trial = bias_forces1_trial
|
|
354
|
+
|
|
355
|
+
forces2_trial = 2 * forces_center - forces1_trial
|
|
356
|
+
|
|
357
|
+
# Calculate rotational force for trial
|
|
358
|
+
fd1_trial = np.sum(forces1_trial * rotated_n_trial)
|
|
359
|
+
fd2_trial = np.sum(forces2_trial * rotated_n_trial)
|
|
360
|
+
fn1_trial = forces1_trial - rotated_n_trial * fd1_trial
|
|
361
|
+
fn2_trial = forces2_trial - rotated_n_trial * fd2_trial
|
|
362
|
+
rotational_force_gradient_trial = (fn1_trial - fn2_trial) / dR
|
|
363
|
+
|
|
364
|
+
# Calculate optimal rotation angle
|
|
365
|
+
fnp2 = gnu_tmp * np.sum(rotational_force_gradient_trial * gnu_tmp)
|
|
366
|
+
fnrp2 = np.sum(fnp2 * gnu_tmp)
|
|
367
|
+
|
|
368
|
+
cth = (fnrp1 - fnrp2) / trial_angle
|
|
369
|
+
fnrp = (fnrp1 + fnrp2) / 2.0
|
|
370
|
+
|
|
371
|
+
rotation_angle = 0.0
|
|
372
|
+
if abs(cth) > 1e-10:
|
|
373
|
+
rotation_angle = np.arctan((fnrp / cth) * 2.0) / 2.0 + trial_angle / 2.0
|
|
374
|
+
if cth < 0:
|
|
375
|
+
rotation_angle += np.pi / 2.0
|
|
376
|
+
else:
|
|
377
|
+
rotation_angle = trial_angle / 2.0
|
|
378
|
+
|
|
379
|
+
# Apply optimal rotation
|
|
380
|
+
current_dimer_axis = self.rotate_vector_around_axis(n_tmp, gnu_tmp, rotation_angle)
|
|
381
|
+
current_dimer_axis = self.normalize(current_dimer_axis)
|
|
382
|
+
|
|
383
|
+
# Update gradient history
|
|
384
|
+
self.old_rot_force = rotational_force_gradient.copy()
|
|
385
|
+
self.old_rot_gradient = current_rot_gradient.copy()
|
|
386
|
+
|
|
387
|
+
# Recalculate forces for new axis
|
|
388
|
+
pos1_new = center_coords + current_dimer_axis * dR
|
|
389
|
+
energy1_new, forces1_new, _, failed = SP_obj.single_point(
|
|
390
|
+
None, element_list, "", charge_multiplicity, method, pos1_new
|
|
391
|
+
)
|
|
392
|
+
if failed:
|
|
393
|
+
return None, None, True
|
|
394
|
+
|
|
395
|
+
# Apply bias potential
|
|
396
|
+
_, _, bias_forces1_new, _ = BPC.main(
|
|
397
|
+
energy1_new, forces1_new, pos1_new, element_list, self.config.force_data
|
|
398
|
+
)
|
|
399
|
+
forces1_new = bias_forces1_new
|
|
400
|
+
|
|
401
|
+
forces2_new = 2 * forces_center - forces1_new
|
|
402
|
+
|
|
403
|
+
# Update rotational forces and curvature
|
|
404
|
+
fd1_new = np.sum(forces1_new * current_dimer_axis)
|
|
405
|
+
fd2_new = np.sum(forces2_new * current_dimer_axis)
|
|
406
|
+
fn1_new = forces1_new - current_dimer_axis * fd1_new
|
|
407
|
+
fn2_new = forces2_new - current_dimer_axis * fd2_new
|
|
408
|
+
rotational_force_gradient = (fn1_new - fn2_new) / dR
|
|
409
|
+
rotational_force_magnitude = np.linalg.norm(rotational_force_gradient.flatten())
|
|
410
|
+
|
|
411
|
+
curvature = np.dot((forces2_new - forces1_new).flatten(), current_dimer_axis.flatten()) / (2.0 * dR)
|
|
412
|
+
|
|
413
|
+
self.print_status(rot_iteration, bias_energy_center, curvature,
|
|
414
|
+
np.max(np.abs(forces_center)), rotational_force_magnitude, rotation_angle)
|
|
415
|
+
|
|
416
|
+
if rotational_force_magnitude < max_force_rotation or rot_iteration >= max_rot_iterations - 1:
|
|
417
|
+
break
|
|
418
|
+
|
|
419
|
+
rot_iteration += 1
|
|
420
|
+
|
|
421
|
+
else:
|
|
422
|
+
# Original dimer rotation algorithm
|
|
423
|
+
rotation_count = 0
|
|
424
|
+
forces1A = forces1.copy()
|
|
425
|
+
|
|
426
|
+
while rotation_count < max_rot_iterations:
|
|
427
|
+
rot_force = self.perpendicular_vector((forces1 - forces2), current_dimer_axis)
|
|
428
|
+
rot_force_magnitude = np.linalg.norm(rot_force.flatten())
|
|
429
|
+
|
|
430
|
+
if rot_force_magnitude <= self.dimer_parameters["f_rot_min"]:
|
|
431
|
+
break
|
|
432
|
+
if rot_force_magnitude <= self.dimer_parameters["f_rot_max"] and rotation_count > 0:
|
|
433
|
+
break
|
|
434
|
+
|
|
435
|
+
n_A = current_dimer_axis.copy()
|
|
436
|
+
rot_unit_A = self.normalize(rot_force)
|
|
437
|
+
|
|
438
|
+
c0 = curvature
|
|
439
|
+
c0d = np.dot((forces2 - forces1).flatten(), rot_unit_A.flatten()) / dR
|
|
440
|
+
|
|
441
|
+
# Trial rotation
|
|
442
|
+
n_B = self.rotate_vector_around_axis(n_A, rot_unit_A, trial_angle)
|
|
443
|
+
n_B = self.normalize(n_B)
|
|
444
|
+
|
|
445
|
+
pos1B = center_coords + n_B * dR
|
|
446
|
+
energy1B, forces1B, _, failed = SP_obj.single_point(
|
|
447
|
+
None, element_list, "", charge_multiplicity, method, pos1B
|
|
448
|
+
)
|
|
449
|
+
if failed:
|
|
450
|
+
return None, None, True
|
|
451
|
+
|
|
452
|
+
# Apply bias potential
|
|
453
|
+
_, _, bias_forces1B, _ = BPC.main(
|
|
454
|
+
energy1B, forces1B, pos1B, element_list, self.config.force_data
|
|
455
|
+
)
|
|
456
|
+
forces1B = bias_forces1B
|
|
457
|
+
|
|
458
|
+
forces2B = 2 * forces_center - forces1B
|
|
459
|
+
|
|
460
|
+
c1d = np.dot((forces2B - forces1B).flatten(), rot_unit_A.flatten()) / dR
|
|
461
|
+
|
|
462
|
+
# Calculate optimal rotation angle
|
|
463
|
+
a1 = c0d * np.cos(2 * trial_angle) - c1d / (2 * np.sin(2 * trial_angle))
|
|
464
|
+
b1 = 0.5 * c0d
|
|
465
|
+
a0 = 2 * (c0 - a1)
|
|
466
|
+
|
|
467
|
+
rotation_angle = 0.0
|
|
468
|
+
if abs(a1) > 1e-10:
|
|
469
|
+
rotation_angle = np.arctan(b1 / a1) / 2.0
|
|
470
|
+
|
|
471
|
+
cmin = a0 / 2.0 + a1 * np.cos(2 * rotation_angle) + b1 * np.sin(2 * rotation_angle)
|
|
472
|
+
if c0 < cmin:
|
|
473
|
+
rotation_angle += np.pi / 2.0
|
|
474
|
+
|
|
475
|
+
# Apply optimal rotation
|
|
476
|
+
current_dimer_axis = self.rotate_vector_around_axis(n_A, rot_unit_A, rotation_angle)
|
|
477
|
+
current_dimer_axis = self.normalize(current_dimer_axis)
|
|
478
|
+
|
|
479
|
+
curvature = cmin
|
|
480
|
+
|
|
481
|
+
# Calculate forces at new orientation
|
|
482
|
+
pos1 = center_coords + current_dimer_axis * dR
|
|
483
|
+
energy1, forces1, _, failed = SP_obj.single_point(
|
|
484
|
+
None, element_list, "", charge_multiplicity, method, pos1
|
|
485
|
+
)
|
|
486
|
+
if failed:
|
|
487
|
+
return None, None, True
|
|
488
|
+
|
|
489
|
+
# Apply bias potential
|
|
490
|
+
_, _, bias_forces1, _ = BPC.main(
|
|
491
|
+
energy1, forces1, pos1, element_list, self.config.force_data
|
|
492
|
+
)
|
|
493
|
+
forces1 = bias_forces1
|
|
494
|
+
|
|
495
|
+
forces2 = 2 * forces_center - forces1
|
|
496
|
+
|
|
497
|
+
self.print_status(rotation_count, bias_energy_center, curvature,
|
|
498
|
+
np.max(np.abs(forces_center)), rot_force_magnitude, rotation_angle)
|
|
499
|
+
|
|
500
|
+
rotation_count += 1
|
|
501
|
+
|
|
502
|
+
return current_dimer_axis, curvature, False
|
|
503
|
+
|
|
504
|
+
def dimer_translate(self, SP_obj, coords, dimer_axis, curvature, element_list, charge_multiplicity, method):
|
|
505
|
+
"""
|
|
506
|
+
Translate the dimer to find a saddle point.
|
|
507
|
+
|
|
508
|
+
Parameters:
|
|
509
|
+
-----------
|
|
510
|
+
SP_obj : Calculation object
|
|
511
|
+
Object for performing single point calculations
|
|
512
|
+
coords : ndarray
|
|
513
|
+
Current coordinates
|
|
514
|
+
dimer_axis : ndarray
|
|
515
|
+
Current dimer axis (normalized)
|
|
516
|
+
curvature : float
|
|
517
|
+
Current curvature along the dimer axis
|
|
518
|
+
element_list : list
|
|
519
|
+
List of element symbols
|
|
520
|
+
charge_multiplicity : list
|
|
521
|
+
[charge, multiplicity]
|
|
522
|
+
method : str
|
|
523
|
+
Calculation method
|
|
524
|
+
|
|
525
|
+
Returns:
|
|
526
|
+
--------
|
|
527
|
+
ndarray, float, bool
|
|
528
|
+
New coordinates, energy, and success flag
|
|
529
|
+
"""
|
|
530
|
+
# Parameters for translation
|
|
531
|
+
max_step = self.dimer_parameters["max_step"]
|
|
532
|
+
cg_translation = self.dimer_parameters["cg_translation"]
|
|
533
|
+
|
|
534
|
+
# Normalize dimer_axis
|
|
535
|
+
dimer_axis = self.normalize(np.array(dimer_axis).reshape(coords.shape))
|
|
536
|
+
|
|
537
|
+
# Get forces at current position
|
|
538
|
+
energy, forces, _, failed = SP_obj.single_point(
|
|
539
|
+
None, element_list, "", charge_multiplicity, method, coords
|
|
540
|
+
)
|
|
541
|
+
if failed:
|
|
542
|
+
return None, None, True
|
|
543
|
+
|
|
544
|
+
# Apply bias potential
|
|
545
|
+
BPC = BiasPotentialCalculation(self.config.iEIP_FOLDER_DIRECTORY)
|
|
546
|
+
_, bias_energy, bias_forces, _ = BPC.main(
|
|
547
|
+
energy, forces, coords, element_list, self.config.force_data
|
|
548
|
+
)
|
|
549
|
+
forces = bias_forces
|
|
550
|
+
|
|
551
|
+
# Modify forces according to dimer method
|
|
552
|
+
f_parallel = self.parallel_vector(forces, dimer_axis)
|
|
553
|
+
|
|
554
|
+
if curvature < 0:
|
|
555
|
+
# Invert component parallel to dimer axis
|
|
556
|
+
modified_forces = forces - 2 * f_parallel
|
|
557
|
+
else:
|
|
558
|
+
# Only consider inverted parallel component
|
|
559
|
+
modified_forces = -f_parallel
|
|
560
|
+
|
|
561
|
+
# Apply conjugate gradient if enabled
|
|
562
|
+
if cg_translation:
|
|
563
|
+
direction = self.get_cg_direction_translation(modified_forces)
|
|
564
|
+
else:
|
|
565
|
+
direction = modified_forces
|
|
566
|
+
|
|
567
|
+
# Normalize direction and apply step size
|
|
568
|
+
direction = self.normalize(direction)
|
|
569
|
+
step_size = max_step
|
|
570
|
+
|
|
571
|
+
# Calculate new coordinates
|
|
572
|
+
new_coords = coords + direction * step_size
|
|
573
|
+
|
|
574
|
+
# Calculate energy at new position
|
|
575
|
+
new_energy, _, _, failed = SP_obj.single_point(
|
|
576
|
+
None, element_list, "", charge_multiplicity, method, new_coords
|
|
577
|
+
)
|
|
578
|
+
if failed:
|
|
579
|
+
return None, None, True
|
|
580
|
+
|
|
581
|
+
# Apply bias potential
|
|
582
|
+
_, bias_new_energy, _, _ = BPC.main(
|
|
583
|
+
new_energy, np.zeros_like(coords), new_coords, element_list, self.config.force_data
|
|
584
|
+
)
|
|
585
|
+
new_energy = bias_new_energy
|
|
586
|
+
|
|
587
|
+
return new_coords, new_energy, False
|
|
588
|
+
|
|
589
|
+
def save_structure(self, coords, element_list, iteration, energy, curvature, label):
|
|
590
|
+
"""Save structure to XYZ file"""
|
|
591
|
+
# Create filename
|
|
592
|
+
filename = f"{label}_iter_{iteration}.xyz"
|
|
593
|
+
filepath = os.path.join(self.directory, "dimer_structures", filename)
|
|
594
|
+
|
|
595
|
+
# Convert coordinates to Angstroms
|
|
596
|
+
coords_ang = coords * self.get_unit_conversion()
|
|
597
|
+
|
|
598
|
+
# Write XYZ file
|
|
599
|
+
with open(filepath, 'w') as f:
|
|
600
|
+
f.write(f"{len(element_list)}\n")
|
|
601
|
+
f.write(f"Dimer {label} - Iteration {iteration} - Energy {energy:.6f} - Curvature {curvature:.6f}\n")
|
|
602
|
+
for i, (element, coord) in enumerate(zip(element_list, coords_ang)):
|
|
603
|
+
f.write(f"{element} {coord[0]:.12f} {coord[1]:.12f} {coord[2]:.12f}\n")
|
|
604
|
+
|
|
605
|
+
def save_eigenmode(self, eigenmode, element_list, iteration, curvature):
|
|
606
|
+
"""Save eigenmode to XYZ file"""
|
|
607
|
+
# Create filename
|
|
608
|
+
filename = f"eigenmode_iter_{iteration}.xyz"
|
|
609
|
+
filepath = os.path.join(self.directory, "dimer_structures", filename)
|
|
610
|
+
|
|
611
|
+
# Scale eigenmode for visualization
|
|
612
|
+
scale_factor = 0.3 / np.max(np.abs(eigenmode)) if np.max(np.abs(eigenmode)) > 1e-10 else 0.3
|
|
613
|
+
scaled_mode = eigenmode * scale_factor
|
|
614
|
+
|
|
615
|
+
# Write XYZ file
|
|
616
|
+
with open(filepath, 'w') as f:
|
|
617
|
+
f.write(f"{len(element_list)}\n")
|
|
618
|
+
f.write(f"Dimer eigenmode - Iteration {iteration} - Curvature {curvature:.6f}\n")
|
|
619
|
+
for i, (element, mode) in enumerate(zip(element_list, scaled_mode)):
|
|
620
|
+
f.write(f"{element} {mode[0]:.12f} {mode[1]:.12f} {mode[2]:.12f}\n")
|
|
621
|
+
|
|
622
|
+
def create_trajectory_file(self, element_list):
|
|
623
|
+
"""Create a trajectory file from all saved structures"""
|
|
624
|
+
# Get all structure files sorted by iteration
|
|
625
|
+
structure_files = glob.glob(os.path.join(self.directory, "dimer_structures", "optimization_iter_*.xyz"))
|
|
626
|
+
structure_files.sort(key=lambda x: int(x.split("_")[-1].split(".")[0]))
|
|
627
|
+
|
|
628
|
+
# If no files found, return
|
|
629
|
+
if not structure_files:
|
|
630
|
+
return
|
|
631
|
+
|
|
632
|
+
# Create trajectory file
|
|
633
|
+
trajectory_file = os.path.join(self.directory, "dimer_trajectory.xyz")
|
|
634
|
+
|
|
635
|
+
with open(trajectory_file, 'w') as outfile:
|
|
636
|
+
for file in structure_files:
|
|
637
|
+
with open(file, 'r') as infile:
|
|
638
|
+
outfile.write(infile.read())
|
|
639
|
+
|
|
640
|
+
print(f"Created trajectory file: {trajectory_file}")
|
|
641
|
+
|
|
642
|
+
def create_distance_plots(self):
|
|
643
|
+
"""Create CSV files with distance and energy data for plotting"""
|
|
644
|
+
iterations = range(len(self.energy_list))
|
|
645
|
+
|
|
646
|
+
# Path for the data file
|
|
647
|
+
data_file = os.path.join(self.directory, "dimer_results.csv")
|
|
648
|
+
|
|
649
|
+
# Write the data
|
|
650
|
+
with open(data_file, 'w') as f:
|
|
651
|
+
f.write("iteration,energy,curvature\n")
|
|
652
|
+
for i in iterations:
|
|
653
|
+
f.write(f"{i},{self.energy_list[i]:.6f},{self.curvature_list[i]:.6f}\n")
|
|
654
|
+
|
|
655
|
+
print(f"Created data file: {data_file}")
|
|
656
|
+
|
|
657
|
+
# Getters and setters - keep same style as ADDFlikeMethod
|
|
658
|
+
def set_molecule(self, element_list, coords):
|
|
659
|
+
self.element_list = element_list
|
|
660
|
+
self.coords = coords
|
|
661
|
+
|
|
662
|
+
def set_gradient(self, gradient):
|
|
663
|
+
self.gradient = gradient
|
|
664
|
+
|
|
665
|
+
def set_hessian(self, hessian):
|
|
666
|
+
self.hessian = hessian
|
|
667
|
+
|
|
668
|
+
def set_energy(self, energy):
|
|
669
|
+
self.energy = energy
|
|
670
|
+
|
|
671
|
+
def set_coords(self, coords):
|
|
672
|
+
self.coords = coords
|
|
673
|
+
|
|
674
|
+
def set_element_list(self, element_list):
|
|
675
|
+
self.element_list = element_list
|
|
676
|
+
self.element_number_list = [element_number(i) for i in self.element_list]
|
|
677
|
+
|
|
678
|
+
def set_coord(self, coord):
|
|
679
|
+
self.coords = coord
|
|
680
|
+
|
|
681
|
+
def get_coord(self):
|
|
682
|
+
return self.coords
|
|
683
|
+
|
|
684
|
+
def get_element_list(self):
|
|
685
|
+
return self.element_list
|
|
686
|
+
|
|
687
|
+
def get_element_number_list(self):
|
|
688
|
+
if self.element_number_list is None:
|
|
689
|
+
if self.element_list is None:
|
|
690
|
+
raise ValueError('Element list is not set.')
|
|
691
|
+
self.element_number_list = [element_number(i) for i in self.element_list]
|
|
692
|
+
return self.element_number_list
|
|
693
|
+
|
|
694
|
+
def set_mole_info(self, base_file_name, electric_charge_and_multiplicity):
|
|
695
|
+
"""Load molecular information from XYZ file"""
|
|
696
|
+
coord, element_list, electric_charge_and_multiplicity = xyz2list(
|
|
697
|
+
base_file_name + ".xyz", electric_charge_and_multiplicity)
|
|
698
|
+
|
|
699
|
+
if self.config.usextb != "None":
|
|
700
|
+
self.method = self.config.usextb
|
|
701
|
+
elif self.config.usedxtb != "None":
|
|
702
|
+
self.method = self.config.usedxtb
|
|
703
|
+
else:
|
|
704
|
+
self.method = "None"
|
|
705
|
+
|
|
706
|
+
self.coords = np.array(coord, dtype="float64")
|
|
707
|
+
self.element_list = element_list
|
|
708
|
+
self.electric_charge_and_multiplicity = electric_charge_and_multiplicity
|
|
709
|
+
|
|
710
|
+
def run(self, file_directory, SP, electric_charge_and_multiplicity, FIO_img):
|
|
711
|
+
"""
|
|
712
|
+
Main method to run Dimer optimization.
|
|
713
|
+
|
|
714
|
+
Parameters:
|
|
715
|
+
-----------
|
|
716
|
+
file_directory : str
|
|
717
|
+
Path to input file
|
|
718
|
+
SP : SinglePointCalculation object
|
|
719
|
+
Object for performing single point calculations
|
|
720
|
+
electric_charge_and_multiplicity : list
|
|
721
|
+
[charge, multiplicity]
|
|
722
|
+
FIO_img : FileIO object
|
|
723
|
+
Object for file I/O operations
|
|
724
|
+
|
|
725
|
+
Returns:
|
|
726
|
+
--------
|
|
727
|
+
bool
|
|
728
|
+
True if optimization succeeded, False otherwise
|
|
729
|
+
"""
|
|
730
|
+
print("### Start Dimer Method ###")
|
|
731
|
+
|
|
732
|
+
# Preparation
|
|
733
|
+
base_file_name = os.path.splitext(FIO_img.START_FILE)[0]
|
|
734
|
+
self.set_mole_info(base_file_name, electric_charge_and_multiplicity)
|
|
735
|
+
|
|
736
|
+
self.directory = make_workspace(file_directory)
|
|
737
|
+
|
|
738
|
+
# Create directory for dimer structures
|
|
739
|
+
os.makedirs(os.path.join(self.directory, "dimer_structures"), exist_ok=True)
|
|
740
|
+
|
|
741
|
+
# Initial coordinates
|
|
742
|
+
initial_coords = self.get_coord()
|
|
743
|
+
element_list = self.get_element_list()
|
|
744
|
+
|
|
745
|
+
# Initial energy and forces
|
|
746
|
+
energy, forces, _, failed = SP.single_point(
|
|
747
|
+
None, element_list, "", electric_charge_and_multiplicity, self.method, initial_coords
|
|
748
|
+
)
|
|
749
|
+
if failed:
|
|
750
|
+
print("Initial calculation failed.")
|
|
751
|
+
return False
|
|
752
|
+
|
|
753
|
+
# Apply bias potential
|
|
754
|
+
BPC = BiasPotentialCalculation(self.config.iEIP_FOLDER_DIRECTORY)
|
|
755
|
+
_, bias_energy, bias_forces, _ = BPC.main(
|
|
756
|
+
energy, forces, initial_coords, element_list, self.config.force_data
|
|
757
|
+
)
|
|
758
|
+
|
|
759
|
+
print(f"Initial energy: {bias_energy:.6f}, Max force: {np.max(np.abs(bias_forces)):.6f}")
|
|
760
|
+
|
|
761
|
+
# Save initial structure
|
|
762
|
+
self.save_structure(initial_coords, element_list, 0, bias_energy, 0.0, "initial")
|
|
763
|
+
|
|
764
|
+
# Initialize lists for tracking
|
|
765
|
+
self.energy_list = [bias_energy]
|
|
766
|
+
self.gradient_list = [np.linalg.norm(bias_forces)]
|
|
767
|
+
self.curvature_list = [0.0]
|
|
768
|
+
|
|
769
|
+
# Initialize dimer axis (random for now)
|
|
770
|
+
coords_flat = initial_coords.flatten()
|
|
771
|
+
dimer_axis = np.random.rand(*initial_coords.shape) - 0.5
|
|
772
|
+
dimer_axis = self.normalize(dimer_axis)
|
|
773
|
+
|
|
774
|
+
# Main iteration loop
|
|
775
|
+
max_iterations = self.dimer_parameters["max_iterations"]
|
|
776
|
+
converged = False
|
|
777
|
+
iteration = 0
|
|
778
|
+
current_coords = initial_coords.copy()
|
|
779
|
+
|
|
780
|
+
while iteration < max_iterations and not converged:
|
|
781
|
+
print(f"\n### Iteration {iteration+1} ###")
|
|
782
|
+
|
|
783
|
+
# Step 1: Rotate the dimer to find lowest curvature mode
|
|
784
|
+
print("Rotating dimer to find lowest curvature mode...")
|
|
785
|
+
dimer_axis, curvature, failed = self.dimer_rotate(
|
|
786
|
+
SP, current_coords, dimer_axis, element_list,
|
|
787
|
+
electric_charge_and_multiplicity, self.method
|
|
788
|
+
)
|
|
789
|
+
|
|
790
|
+
if failed:
|
|
791
|
+
print(f"Dimer rotation failed at iteration {iteration+1}")
|
|
792
|
+
break
|
|
793
|
+
|
|
794
|
+
print(f"After rotation: Curvature = {curvature:.6f}")
|
|
795
|
+
|
|
796
|
+
# Save the eigenmode
|
|
797
|
+
self.save_eigenmode(dimer_axis, element_list, iteration, curvature)
|
|
798
|
+
|
|
799
|
+
# Step 2: Translate the dimer
|
|
800
|
+
print("Translating dimer...")
|
|
801
|
+
new_coords, new_energy, failed = self.dimer_translate(
|
|
802
|
+
SP, current_coords, dimer_axis, curvature, element_list,
|
|
803
|
+
electric_charge_and_multiplicity, self.method
|
|
804
|
+
)
|
|
805
|
+
|
|
806
|
+
if failed:
|
|
807
|
+
print(f"Dimer translation failed at iteration {iteration+1}")
|
|
808
|
+
break
|
|
809
|
+
|
|
810
|
+
# Calculate forces at new position
|
|
811
|
+
_, new_forces, _, failed = SP.single_point(
|
|
812
|
+
None, element_list, "", electric_charge_and_multiplicity, self.method, new_coords
|
|
813
|
+
)
|
|
814
|
+
if failed:
|
|
815
|
+
print(f"Force calculation failed at iteration {iteration+1}")
|
|
816
|
+
break
|
|
817
|
+
|
|
818
|
+
# Apply bias potential
|
|
819
|
+
_, _, bias_new_forces, _ = BPC.main(
|
|
820
|
+
0, new_forces, new_coords, element_list, self.config.force_data
|
|
821
|
+
)
|
|
822
|
+
new_forces = bias_new_forces
|
|
823
|
+
|
|
824
|
+
# Calculate maximum force component
|
|
825
|
+
max_force = np.max(np.abs(new_forces))
|
|
826
|
+
|
|
827
|
+
# Store results for this iteration
|
|
828
|
+
self.energy_list.append(new_energy)
|
|
829
|
+
self.curvature_list.append(curvature)
|
|
830
|
+
self.gradient_list.append(max_force)
|
|
831
|
+
|
|
832
|
+
# Print status
|
|
833
|
+
energy_change = new_energy - self.energy_list[-2] if iteration > 0 else 0.0
|
|
834
|
+
print(f"After translation: Energy = {new_energy:.6f} (Δ = {energy_change:.6f})")
|
|
835
|
+
print(f" Max Force = {max_force:.6f}")
|
|
836
|
+
print(f" Curvature = {curvature:.6f}")
|
|
837
|
+
|
|
838
|
+
# Save structure for this iteration
|
|
839
|
+
self.save_structure(new_coords, element_list, iteration+1, new_energy, curvature, "optimization")
|
|
840
|
+
|
|
841
|
+
# Check convergence
|
|
842
|
+
if max_force < self.converge_criteria and curvature < 0:
|
|
843
|
+
converged = True
|
|
844
|
+
print("\n### Dimer method converged to a saddle point! ###")
|
|
845
|
+
self.save_structure(new_coords, element_list, iteration+1, new_energy, curvature, "final_saddle_point")
|
|
846
|
+
|
|
847
|
+
# Store this structure
|
|
848
|
+
structure_info = {
|
|
849
|
+
'iteration': iteration+1,
|
|
850
|
+
'energy': new_energy,
|
|
851
|
+
'curvature': curvature,
|
|
852
|
+
'max_force': max_force,
|
|
853
|
+
'coords': new_coords.copy(),
|
|
854
|
+
'comment': f"Dimer Iteration {iteration+1} Energy {new_energy:.6f} Curvature {curvature:.6f}"
|
|
855
|
+
}
|
|
856
|
+
self.optimized_structures[iteration+1] = structure_info
|
|
857
|
+
|
|
858
|
+
# Update for next iteration
|
|
859
|
+
current_coords = new_coords
|
|
860
|
+
|
|
861
|
+
# Reset CG for next step (for translation)
|
|
862
|
+
self.cg_init_translation = True
|
|
863
|
+
|
|
864
|
+
iteration += 1
|
|
865
|
+
|
|
866
|
+
# Create trajectory file
|
|
867
|
+
self.create_trajectory_file(element_list)
|
|
868
|
+
|
|
869
|
+
# Create data plots
|
|
870
|
+
self.create_distance_plots()
|
|
871
|
+
|
|
872
|
+
# Plot optimization progress using Graph class from ieip.py
|
|
873
|
+
G = Graph(self.config.iEIP_FOLDER_DIRECTORY)
|
|
874
|
+
iterations_plot = list(range(len(self.energy_list)))
|
|
875
|
+
|
|
876
|
+
G.single_plot(iterations_plot, self.energy_list, file_directory, "dimer_energy",
|
|
877
|
+
axis_name_2="Energy [Hartree]", name="dimer_energy")
|
|
878
|
+
G.single_plot(iterations_plot, self.curvature_list, file_directory, "dimer_curvature",
|
|
879
|
+
axis_name_2="Curvature", name="dimer_curvature")
|
|
880
|
+
G.single_plot(iterations_plot, self.gradient_list, file_directory, "dimer_gradient",
|
|
881
|
+
axis_name_2="Max Force [a.u.]", name="dimer_gradient")
|
|
882
|
+
|
|
883
|
+
if converged:
|
|
884
|
+
print(f"Dimer method converged after {iteration} iterations.")
|
|
885
|
+
print(f"Final energy: {new_energy:.6f}")
|
|
886
|
+
print(f"Final curvature: {curvature:.6f}")
|
|
887
|
+
print(f"Final max force: {max_force:.6f}")
|
|
888
|
+
return True
|
|
889
|
+
else:
|
|
890
|
+
print(f"Dimer method did not converge after {iteration} iterations.")
|
|
891
|
+
if iteration > 0:
|
|
892
|
+
print(f"Final energy: {new_energy:.6f}")
|
|
893
|
+
print(f"Final curvature: {curvature:.6f}")
|
|
894
|
+
print(f"Final max force: {max_force:.6f}")
|
|
895
|
+
return False
|