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,662 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import os
|
|
3
|
+
import copy
|
|
4
|
+
import csv
|
|
5
|
+
|
|
6
|
+
from multioptpy.Utils.calc_tools import Calculationtools
|
|
7
|
+
from multioptpy.Optimizer.hessian_update import ModelHessianUpdate
|
|
8
|
+
from multioptpy.Potential.potential import BiasPotentialCalculation
|
|
9
|
+
from multioptpy.Parameters.parameter import UnitValueLib, atomic_mass
|
|
10
|
+
from multioptpy.IRC.converge_criteria import convergence_check
|
|
11
|
+
from multioptpy.Visualization.visualization import Graph
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ModeKill:
|
|
15
|
+
"""ModeKill class for removing specific imaginary frequencies
|
|
16
|
+
|
|
17
|
+
This class implements the ModeKill algorithm which selectively removes
|
|
18
|
+
specific unwanted imaginary frequency modes by stepping downhill along
|
|
19
|
+
those eigenvectors.
|
|
20
|
+
|
|
21
|
+
References
|
|
22
|
+
----------
|
|
23
|
+
Based on the concept from pysisyphus package
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, element_list, electric_charge_and_multiplicity, FC_count, file_directory,
|
|
27
|
+
final_directory, force_data, kill_inds=None, nu_thresh=-5.0, max_step=1000,
|
|
28
|
+
step_size=0.1, init_coord=None, init_hess=None, calc_engine=None,
|
|
29
|
+
xtb_method=None, do_hess=True, hessian_update=True):
|
|
30
|
+
"""Initialize ModeKill calculator
|
|
31
|
+
|
|
32
|
+
Parameters
|
|
33
|
+
----------
|
|
34
|
+
element_list : list
|
|
35
|
+
List of atomic elements
|
|
36
|
+
electric_charge_and_multiplicity : tuple
|
|
37
|
+
Charge and multiplicity for the system
|
|
38
|
+
FC_count : int
|
|
39
|
+
Frequency of full hessian recalculation
|
|
40
|
+
file_directory : str
|
|
41
|
+
Working directory
|
|
42
|
+
final_directory : str
|
|
43
|
+
Directory for final output
|
|
44
|
+
force_data : dict
|
|
45
|
+
Force field data for bias potential
|
|
46
|
+
kill_inds : list or numpy.ndarray, optional
|
|
47
|
+
Indices of modes to be removed (typically imaginary modes)
|
|
48
|
+
nu_thresh : float, optional
|
|
49
|
+
Threshold for considering a mode as imaginary (in cm^-1), default=-5.0
|
|
50
|
+
max_step : int, optional
|
|
51
|
+
Maximum number of steps
|
|
52
|
+
step_size : float, optional
|
|
53
|
+
Step size for the optimization
|
|
54
|
+
init_coord : numpy.ndarray, optional
|
|
55
|
+
Initial coordinates
|
|
56
|
+
init_hess : numpy.ndarray, optional
|
|
57
|
+
Initial hessian
|
|
58
|
+
calc_engine : object, optional
|
|
59
|
+
Calculator engine
|
|
60
|
+
xtb_method : str, optional
|
|
61
|
+
XTB method specification
|
|
62
|
+
do_hess : bool, optional
|
|
63
|
+
Whether to calculate final hessian, default=True
|
|
64
|
+
hessian_update : bool, optional
|
|
65
|
+
Whether to update hessian during optimization, default=True
|
|
66
|
+
"""
|
|
67
|
+
self.max_step = max_step
|
|
68
|
+
self.step_size = step_size
|
|
69
|
+
self.ModelHessianUpdate = ModelHessianUpdate()
|
|
70
|
+
self.CE = calc_engine
|
|
71
|
+
self.FC_count = FC_count
|
|
72
|
+
|
|
73
|
+
# Kill mode parameters
|
|
74
|
+
self.kill_inds = kill_inds # Will be set in run() if None
|
|
75
|
+
self.nu_thresh = float(nu_thresh)
|
|
76
|
+
self.do_hess = do_hess
|
|
77
|
+
self.hessian_update = hessian_update
|
|
78
|
+
self.ovlp_thresh = 0.3
|
|
79
|
+
|
|
80
|
+
# Initialize tracking variables
|
|
81
|
+
self.prev_full_eigenvectors = None
|
|
82
|
+
self.converged = False
|
|
83
|
+
self.mw_down_step = None
|
|
84
|
+
|
|
85
|
+
# initial condition
|
|
86
|
+
self.coords = init_coord
|
|
87
|
+
self.init_hess = init_hess
|
|
88
|
+
self.mw_hessian = init_hess # Mass-weighted hessian
|
|
89
|
+
self.xtb_method = xtb_method
|
|
90
|
+
|
|
91
|
+
# convergence criteria - using tight criteria
|
|
92
|
+
self.MAX_FORCE_THRESHOLD = 0.0004
|
|
93
|
+
self.RMS_FORCE_THRESHOLD = 0.0001
|
|
94
|
+
|
|
95
|
+
self.element_list = element_list
|
|
96
|
+
self.electric_charge_and_multiplicity = electric_charge_and_multiplicity
|
|
97
|
+
self.directory = file_directory
|
|
98
|
+
self.final_directory = final_directory
|
|
99
|
+
self.force_data = force_data
|
|
100
|
+
|
|
101
|
+
# Data storage for tracking the modes and their frequencies
|
|
102
|
+
self.indices = np.arange(len(self.element_list) * 3)
|
|
103
|
+
self.neg_nus = []
|
|
104
|
+
self.kill_modes = None
|
|
105
|
+
|
|
106
|
+
# IRC data storage for calculations
|
|
107
|
+
self.irc_bias_energy_list = []
|
|
108
|
+
self.irc_energy_list = []
|
|
109
|
+
self.irc_mw_coords = []
|
|
110
|
+
self.irc_mw_gradients = []
|
|
111
|
+
self.irc_mw_bias_gradients = []
|
|
112
|
+
self.path_bending_angle_list = []
|
|
113
|
+
|
|
114
|
+
# Create data files
|
|
115
|
+
self.create_csv_file()
|
|
116
|
+
self.create_xyz_file()
|
|
117
|
+
|
|
118
|
+
def create_csv_file(self):
|
|
119
|
+
"""Create CSV file for energy and gradient data"""
|
|
120
|
+
self.csv_filename = os.path.join(self.directory, "modekill_energies_gradients.csv")
|
|
121
|
+
with open(self.csv_filename, 'w', newline='') as csvfile:
|
|
122
|
+
writer = csv.writer(csvfile)
|
|
123
|
+
writer.writerow(['Step', 'Energy (Hartree)', 'Bias Energy (Hartree)',
|
|
124
|
+
'RMS Gradient', 'RMS Bias Gradient', 'Imaginary Frequencies'])
|
|
125
|
+
|
|
126
|
+
def create_xyz_file(self):
|
|
127
|
+
"""Create XYZ file for structure data"""
|
|
128
|
+
self.xyz_filename = os.path.join(self.directory, "modekill_structures.xyz")
|
|
129
|
+
# Create empty file (will be appended to later)
|
|
130
|
+
open(self.xyz_filename, 'w').close()
|
|
131
|
+
|
|
132
|
+
def save_to_csv(self, step, energy, bias_energy, gradient, bias_gradient, imaginary_freqs=None):
|
|
133
|
+
"""Save energy and gradient data to CSV file
|
|
134
|
+
|
|
135
|
+
Parameters
|
|
136
|
+
----------
|
|
137
|
+
step : int
|
|
138
|
+
Current step number
|
|
139
|
+
energy : float
|
|
140
|
+
Energy in Hartree
|
|
141
|
+
bias_energy : float
|
|
142
|
+
Bias energy in Hartree
|
|
143
|
+
gradient : numpy.ndarray
|
|
144
|
+
Gradient array
|
|
145
|
+
bias_gradient : numpy.ndarray
|
|
146
|
+
Bias gradient array
|
|
147
|
+
imaginary_freqs : list, optional
|
|
148
|
+
List of imaginary frequencies
|
|
149
|
+
"""
|
|
150
|
+
rms_grad = np.sqrt((gradient**2).mean())
|
|
151
|
+
rms_bias_grad = np.sqrt((bias_gradient**2).mean())
|
|
152
|
+
|
|
153
|
+
if imaginary_freqs is None:
|
|
154
|
+
imaginary_freqs = []
|
|
155
|
+
|
|
156
|
+
with open(self.csv_filename, 'a', newline='') as csvfile:
|
|
157
|
+
writer = csv.writer(csvfile)
|
|
158
|
+
writer.writerow([step, energy, bias_energy, rms_grad, rms_bias_grad, str(imaginary_freqs)])
|
|
159
|
+
|
|
160
|
+
def save_xyz_structure(self, step, coords):
|
|
161
|
+
"""Save molecular structure to XYZ file
|
|
162
|
+
|
|
163
|
+
Parameters
|
|
164
|
+
----------
|
|
165
|
+
step : int
|
|
166
|
+
Current step number
|
|
167
|
+
coords : numpy.ndarray
|
|
168
|
+
Atomic coordinates in Bohr
|
|
169
|
+
"""
|
|
170
|
+
# Convert coordinates to Angstroms
|
|
171
|
+
coords_angstrom = coords * UnitValueLib().bohr2angstroms
|
|
172
|
+
|
|
173
|
+
with open(self.xyz_filename, 'a') as f:
|
|
174
|
+
# Number of atoms and comment line
|
|
175
|
+
f.write(f"{len(coords)}\n")
|
|
176
|
+
f.write(f"ModeKill Step {step}\n")
|
|
177
|
+
|
|
178
|
+
# Atomic coordinates
|
|
179
|
+
for i, coord in enumerate(coords_angstrom):
|
|
180
|
+
f.write(f"{self.element_list[i]:<3} {coord[0]:15.10f} {coord[1]:15.10f} {coord[2]:15.10f}\n")
|
|
181
|
+
|
|
182
|
+
def get_mass_array(self):
|
|
183
|
+
"""Create arrays of atomic masses for mass-weighting operations"""
|
|
184
|
+
elem_mass_list = np.array([atomic_mass(elem) for elem in self.element_list], dtype="float64")
|
|
185
|
+
sqrt_mass_list = np.sqrt(elem_mass_list)
|
|
186
|
+
|
|
187
|
+
# Create arrays for 3D operations (x,y,z for each atom)
|
|
188
|
+
three_elem_mass_list = np.repeat(elem_mass_list, 3)
|
|
189
|
+
three_sqrt_mass_list = np.repeat(sqrt_mass_list, 3)
|
|
190
|
+
|
|
191
|
+
return elem_mass_list, sqrt_mass_list, three_elem_mass_list, three_sqrt_mass_list
|
|
192
|
+
|
|
193
|
+
def mass_weight_hessian(self, hessian, three_sqrt_mass_list):
|
|
194
|
+
"""Apply mass-weighting to the hessian matrix
|
|
195
|
+
|
|
196
|
+
Parameters
|
|
197
|
+
----------
|
|
198
|
+
hessian : numpy.ndarray
|
|
199
|
+
Hessian matrix in non-mass-weighted coordinates
|
|
200
|
+
three_sqrt_mass_list : numpy.ndarray
|
|
201
|
+
Array of sqrt(mass) values repeated for x,y,z per atom
|
|
202
|
+
|
|
203
|
+
Returns
|
|
204
|
+
-------
|
|
205
|
+
numpy.ndarray
|
|
206
|
+
Mass-weighted hessian
|
|
207
|
+
"""
|
|
208
|
+
mass_mat = np.diag(1.0 / three_sqrt_mass_list)
|
|
209
|
+
return np.dot(mass_mat, np.dot(hessian, mass_mat))
|
|
210
|
+
|
|
211
|
+
def mass_weight_coordinates(self, coordinates, sqrt_mass_list):
|
|
212
|
+
"""Convert coordinates to mass-weighted coordinates
|
|
213
|
+
|
|
214
|
+
Parameters
|
|
215
|
+
----------
|
|
216
|
+
coordinates : numpy.ndarray
|
|
217
|
+
Coordinates in non-mass-weighted form
|
|
218
|
+
sqrt_mass_list : numpy.ndarray
|
|
219
|
+
Array of sqrt(mass) values for each atom
|
|
220
|
+
|
|
221
|
+
Returns
|
|
222
|
+
-------
|
|
223
|
+
numpy.ndarray
|
|
224
|
+
Mass-weighted coordinates
|
|
225
|
+
"""
|
|
226
|
+
mw_coords = copy.deepcopy(coordinates)
|
|
227
|
+
for i in range(len(coordinates)):
|
|
228
|
+
mw_coords[i] = coordinates[i] * sqrt_mass_list[i]
|
|
229
|
+
return mw_coords
|
|
230
|
+
|
|
231
|
+
def mass_weight_gradient(self, gradient, sqrt_mass_list):
|
|
232
|
+
"""Convert gradient to mass-weighted form
|
|
233
|
+
|
|
234
|
+
Parameters
|
|
235
|
+
----------
|
|
236
|
+
gradient : numpy.ndarray
|
|
237
|
+
Gradient in non-mass-weighted form
|
|
238
|
+
sqrt_mass_list : numpy.ndarray
|
|
239
|
+
Array of sqrt(mass) values for each atom
|
|
240
|
+
|
|
241
|
+
Returns
|
|
242
|
+
-------
|
|
243
|
+
numpy.ndarray
|
|
244
|
+
Mass-weighted gradient
|
|
245
|
+
"""
|
|
246
|
+
mw_gradient = copy.deepcopy(gradient)
|
|
247
|
+
for i in range(len(gradient)):
|
|
248
|
+
mw_gradient[i] = gradient[i] / sqrt_mass_list[i]
|
|
249
|
+
return mw_gradient
|
|
250
|
+
|
|
251
|
+
def unmass_weight_step(self, step, sqrt_mass_list):
|
|
252
|
+
"""Convert a step vector from mass-weighted to non-mass-weighted coordinates
|
|
253
|
+
|
|
254
|
+
Parameters
|
|
255
|
+
----------
|
|
256
|
+
step : numpy.ndarray
|
|
257
|
+
Step in mass-weighted form
|
|
258
|
+
sqrt_mass_list : numpy.ndarray
|
|
259
|
+
Array of sqrt(mass) values for each atom
|
|
260
|
+
|
|
261
|
+
Returns
|
|
262
|
+
-------
|
|
263
|
+
numpy.ndarray
|
|
264
|
+
Step in non-mass-weighted coordinates
|
|
265
|
+
"""
|
|
266
|
+
unmw_step = copy.deepcopy(step)
|
|
267
|
+
for i in range(len(step)):
|
|
268
|
+
unmw_step[i] = step[i] / sqrt_mass_list[i]
|
|
269
|
+
return unmw_step
|
|
270
|
+
|
|
271
|
+
def eigval_to_wavenumber(self, eigenvalue):
|
|
272
|
+
"""Convert eigenvalue to wavenumber (cm^-1)
|
|
273
|
+
|
|
274
|
+
Parameters
|
|
275
|
+
----------
|
|
276
|
+
eigenvalue : float or numpy.ndarray
|
|
277
|
+
Eigenvalue(s) from hessian
|
|
278
|
+
|
|
279
|
+
Returns
|
|
280
|
+
-------
|
|
281
|
+
float or numpy.ndarray
|
|
282
|
+
Corresponding wavenumber(s) in cm^-1
|
|
283
|
+
"""
|
|
284
|
+
# Constants
|
|
285
|
+
au2rcm = 5140.48678
|
|
286
|
+
|
|
287
|
+
# Convert eigenvalue to wavenumber
|
|
288
|
+
sign = np.sign(eigenvalue)
|
|
289
|
+
return sign * np.sqrt(np.abs(eigenvalue)) * au2rcm
|
|
290
|
+
|
|
291
|
+
def update_mw_down_step(self, sqrt_mass_list, mw_gradient):
|
|
292
|
+
"""Update the downhill step direction based on current hessian
|
|
293
|
+
|
|
294
|
+
Parameters
|
|
295
|
+
----------
|
|
296
|
+
sqrt_mass_list : numpy.ndarray
|
|
297
|
+
Array of sqrt(mass) values for each atom
|
|
298
|
+
mw_gradient : numpy.ndarray
|
|
299
|
+
Mass-weighted gradient
|
|
300
|
+
"""
|
|
301
|
+
# Diagonalize the mass-weighted hessian
|
|
302
|
+
w, v = np.linalg.eigh(self.mw_hessian)
|
|
303
|
+
|
|
304
|
+
# Convert eigenvalues to wavenumbers
|
|
305
|
+
nus = self.eigval_to_wavenumber(w)
|
|
306
|
+
|
|
307
|
+
# Check if we have any negative eigenvalues below threshold
|
|
308
|
+
neg_inds = nus < self.nu_thresh
|
|
309
|
+
neg_nus = nus[neg_inds]
|
|
310
|
+
|
|
311
|
+
# Check if we have modes to track
|
|
312
|
+
if len(self.kill_inds) == 0:
|
|
313
|
+
self.converged = True
|
|
314
|
+
return
|
|
315
|
+
|
|
316
|
+
# First time initialization
|
|
317
|
+
if self.prev_full_eigenvectors is None:
|
|
318
|
+
# Verify the kill indices point to imaginary modes
|
|
319
|
+
try:
|
|
320
|
+
assert all(nus[self.kill_inds] < self.nu_thresh), \
|
|
321
|
+
"ModeKill is intended for removal of imaginary frequencies " \
|
|
322
|
+
f"below {self.nu_thresh} cm^-1! The specified indices " \
|
|
323
|
+
f"{self.kill_inds} contain modes with positive frequencies " \
|
|
324
|
+
f"({nus[self.kill_inds]} cm^-1). Please choose different kill_inds!"
|
|
325
|
+
except IndexError:
|
|
326
|
+
print("Warning: Kill indices out of range. Using available negative modes.")
|
|
327
|
+
self.kill_inds = np.where(nus < self.nu_thresh)[0]
|
|
328
|
+
if len(self.kill_inds) == 0:
|
|
329
|
+
print("No negative modes found. ModeKill will exit.")
|
|
330
|
+
self.converged = True
|
|
331
|
+
return
|
|
332
|
+
|
|
333
|
+
# Store the full eigenvector set for later comparison
|
|
334
|
+
self.prev_full_eigenvectors = v
|
|
335
|
+
self.kill_modes = v[:, self.kill_inds]
|
|
336
|
+
else:
|
|
337
|
+
# Calculate overlaps between previous eigenvectors and current eigenvectors
|
|
338
|
+
new_kill_inds = []
|
|
339
|
+
|
|
340
|
+
for idx in self.kill_inds:
|
|
341
|
+
# Get the original kill mode from previous eigenvectors
|
|
342
|
+
orig_mode = self.prev_full_eigenvectors[:, idx]
|
|
343
|
+
|
|
344
|
+
# Find current mode with maximum overlap with original mode
|
|
345
|
+
overlaps_with_orig = np.abs(np.dot(orig_mode, v))
|
|
346
|
+
|
|
347
|
+
# Only consider negative eigenvalues (imaginary frequencies)
|
|
348
|
+
neg_mask = w < 0
|
|
349
|
+
overlaps_with_orig[~neg_mask] = 0
|
|
350
|
+
|
|
351
|
+
# Find index with highest overlap
|
|
352
|
+
max_overlap_idx = np.argmax(overlaps_with_orig)
|
|
353
|
+
|
|
354
|
+
# Only keep the mode if it still has a significant overlap and is imaginary
|
|
355
|
+
if overlaps_with_orig[max_overlap_idx] > self.ovlp_thresh and w[max_overlap_idx] < 0:
|
|
356
|
+
new_kill_inds.append(max_overlap_idx)
|
|
357
|
+
print(f"Mode {idx} tracked to current mode {max_overlap_idx} with overlap {overlaps_with_orig[max_overlap_idx]:.4f}")
|
|
358
|
+
|
|
359
|
+
# If no modes to track anymore, we're done
|
|
360
|
+
if len(new_kill_inds) == 0:
|
|
361
|
+
print("No modes left to track. ModeKill will exit.")
|
|
362
|
+
self.converged = True
|
|
363
|
+
return
|
|
364
|
+
|
|
365
|
+
# Update kill indices for the current iteration
|
|
366
|
+
self.kill_inds = np.array(new_kill_inds, dtype=int)
|
|
367
|
+
self.prev_full_eigenvectors = v
|
|
368
|
+
self.kill_modes = v[:, self.kill_inds]
|
|
369
|
+
|
|
370
|
+
# Determine correct sign for eigenvectors based on gradient overlap
|
|
371
|
+
mw_grad_flatten = mw_gradient.flatten()
|
|
372
|
+
mw_grad_normed = mw_grad_flatten / np.linalg.norm(mw_grad_flatten)
|
|
373
|
+
overlaps = np.dot(self.kill_modes.T, mw_grad_normed)
|
|
374
|
+
print("Overlaps between gradient and eigenvectors:")
|
|
375
|
+
print(overlaps)
|
|
376
|
+
|
|
377
|
+
# Flip eigenvector signs if needed (we want negative overlaps for downhill direction)
|
|
378
|
+
flip = overlaps > 0
|
|
379
|
+
print("Eigenvector signs to be flipped:")
|
|
380
|
+
print(str(flip))
|
|
381
|
+
self.kill_modes[:, flip] *= -1
|
|
382
|
+
|
|
383
|
+
# Create the step as the sum of the downhill steps along the modes to remove
|
|
384
|
+
self.mw_down_step = (self.step_size * self.kill_modes).sum(axis=1)
|
|
385
|
+
|
|
386
|
+
# Log information about the current modes being killed
|
|
387
|
+
print("\nCurrent modes to kill:")
|
|
388
|
+
for i, mode_idx in enumerate(self.kill_inds):
|
|
389
|
+
print(f"Mode {mode_idx}: {nus[mode_idx]:.2f} cm^-1")
|
|
390
|
+
|
|
391
|
+
def get_additional_print(self):
|
|
392
|
+
"""Return additional information for printing during optimization
|
|
393
|
+
|
|
394
|
+
Returns
|
|
395
|
+
-------
|
|
396
|
+
str
|
|
397
|
+
String with additional information
|
|
398
|
+
"""
|
|
399
|
+
if len(self.neg_nus) > 0:
|
|
400
|
+
neg_nus = np.array2string(self.neg_nus[-1], precision=2)
|
|
401
|
+
return f"\timag. ῦ: {neg_nus} cm^-1"
|
|
402
|
+
return ""
|
|
403
|
+
|
|
404
|
+
def run(self):
|
|
405
|
+
"""Run the ModeKill calculation"""
|
|
406
|
+
print("ModeKill: Selective Imaginary Mode Removal")
|
|
407
|
+
geom_num_list = self.coords
|
|
408
|
+
CalcBiaspot = BiasPotentialCalculation(self.directory)
|
|
409
|
+
|
|
410
|
+
# Initialize the Hessian if needed to identify modes to kill
|
|
411
|
+
if self.mw_hessian is None or self.kill_inds is None:
|
|
412
|
+
# Calculate initial Hessian
|
|
413
|
+
self.CE.hessian_flag = True
|
|
414
|
+
e, g, geom_num_list, finish_frag = self.CE.single_point(
|
|
415
|
+
self.final_directory,
|
|
416
|
+
self.element_list,
|
|
417
|
+
0,
|
|
418
|
+
self.electric_charge_and_multiplicity,
|
|
419
|
+
self.xtb_method,
|
|
420
|
+
UnitValueLib().bohr2angstroms * geom_num_list
|
|
421
|
+
)
|
|
422
|
+
self.mw_hessian = self.CE.Model_hess
|
|
423
|
+
self.CE.hessian_flag = False
|
|
424
|
+
|
|
425
|
+
# Project out translation and rotation
|
|
426
|
+
self.mw_hessian = Calculationtools().project_out_hess_tr_and_rot(
|
|
427
|
+
self.mw_hessian, self.element_list, geom_num_list
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
# Find negative eigenvalues if kill_inds not specified
|
|
431
|
+
if self.kill_inds is None:
|
|
432
|
+
w, v = np.linalg.eigh(self.mw_hessian)
|
|
433
|
+
nus = self.eigval_to_wavenumber(w)
|
|
434
|
+
neg_inds = np.where(nus < self.nu_thresh)[0]
|
|
435
|
+
if len(neg_inds) > 0:
|
|
436
|
+
print(f"Found {len(neg_inds)} imaginary modes below {self.nu_thresh} cm^-1")
|
|
437
|
+
# By default, kill all imaginary modes except the first one (IRC mode)
|
|
438
|
+
if len(neg_inds) > 1:
|
|
439
|
+
self.kill_inds = neg_inds[1:]
|
|
440
|
+
else:
|
|
441
|
+
self.kill_inds = np.array([], dtype=int)
|
|
442
|
+
print("No secondary imaginary modes to remove.")
|
|
443
|
+
return
|
|
444
|
+
else:
|
|
445
|
+
self.kill_inds = np.array([], dtype=int)
|
|
446
|
+
print("No imaginary modes found. Nothing to remove.")
|
|
447
|
+
return
|
|
448
|
+
|
|
449
|
+
print(f"Will attempt to remove modes: {self.kill_inds}")
|
|
450
|
+
|
|
451
|
+
# First step is just preparation
|
|
452
|
+
cur_cycle = 0
|
|
453
|
+
|
|
454
|
+
# Get mass arrays for consistent mass-weighting
|
|
455
|
+
elem_mass_list, sqrt_mass_list, three_elem_mass_list, three_sqrt_mass_list = self.get_mass_array()
|
|
456
|
+
|
|
457
|
+
while not self.converged and cur_cycle < self.max_step:
|
|
458
|
+
cur_cycle += 1
|
|
459
|
+
print(f"# STEP: {cur_cycle}")
|
|
460
|
+
|
|
461
|
+
# Check for early termination file
|
|
462
|
+
exit_file_detect = os.path.exists(self.directory + "end.txt")
|
|
463
|
+
if exit_file_detect:
|
|
464
|
+
break
|
|
465
|
+
|
|
466
|
+
# Calculate energy, gradient and update geometry
|
|
467
|
+
e, g, geom_num_list, finish_frag = self.CE.single_point(
|
|
468
|
+
self.final_directory,
|
|
469
|
+
self.element_list,
|
|
470
|
+
cur_cycle,
|
|
471
|
+
self.electric_charge_and_multiplicity,
|
|
472
|
+
self.xtb_method,
|
|
473
|
+
UnitValueLib().bohr2angstroms * geom_num_list
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
# Calculate bias potential
|
|
477
|
+
_, B_e, B_g, BPA_hessian = CalcBiaspot.main(
|
|
478
|
+
e, g, geom_num_list, self.element_list,
|
|
479
|
+
self.force_data, g, cur_cycle-1, geom_num_list
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
if finish_frag:
|
|
483
|
+
break
|
|
484
|
+
|
|
485
|
+
# Recalculate Hessian if needed
|
|
486
|
+
if cur_cycle % self.FC_count == 0 or not self.hessian_update:
|
|
487
|
+
self.CE.hessian_flag = True
|
|
488
|
+
e, g, geom_num_list, finish_frag = self.CE.single_point(
|
|
489
|
+
self.final_directory,
|
|
490
|
+
self.element_list,
|
|
491
|
+
cur_cycle,
|
|
492
|
+
self.electric_charge_and_multiplicity,
|
|
493
|
+
self.xtb_method,
|
|
494
|
+
UnitValueLib().bohr2angstroms * geom_num_list
|
|
495
|
+
)
|
|
496
|
+
self.mw_hessian = self.CE.Model_hess
|
|
497
|
+
self.CE.hessian_flag = False
|
|
498
|
+
|
|
499
|
+
self.mw_hessian = Calculationtools().project_out_hess_tr_and_rot(
|
|
500
|
+
self.mw_hessian, self.element_list, geom_num_list
|
|
501
|
+
)
|
|
502
|
+
print("Recalculated exact hessian.")
|
|
503
|
+
elif self.hessian_update and cur_cycle > 1:
|
|
504
|
+
# Hessian update with mass-weighted values
|
|
505
|
+
if len(self.irc_mw_coords) >= 2 and len(self.irc_mw_gradients) >= 2:
|
|
506
|
+
dx = self.irc_mw_coords[-1] - self.irc_mw_coords[-2]
|
|
507
|
+
dg = self.irc_mw_gradients[-1] - self.irc_mw_gradients[-2]
|
|
508
|
+
|
|
509
|
+
dx_flat = dx.reshape(-1, 1)
|
|
510
|
+
dg_flat = dg.reshape(-1, 1)
|
|
511
|
+
|
|
512
|
+
# Only update if the step and gradient difference are meaningful
|
|
513
|
+
inner_prod = np.dot(dx_flat.T, dg_flat)[0, 0]
|
|
514
|
+
if inner_prod > 1e-10:
|
|
515
|
+
delta_hess = self.ModelHessianUpdate.BFGS_hessian_update(
|
|
516
|
+
self.mw_hessian, dx_flat, dg_flat
|
|
517
|
+
)
|
|
518
|
+
self.mw_hessian += delta_hess
|
|
519
|
+
|
|
520
|
+
norm_dx = np.linalg.norm(dx_flat)
|
|
521
|
+
norm_dg = np.linalg.norm(dg_flat)
|
|
522
|
+
print(f"Did BFGS hessian update: norm(dx)={norm_dx:.4e}, "
|
|
523
|
+
f"norm(dg)={norm_dg:.4e}.")
|
|
524
|
+
|
|
525
|
+
# Mass-weight the bias potential hessian
|
|
526
|
+
mw_BPA_hessian = self.mass_weight_hessian(BPA_hessian, three_sqrt_mass_list)
|
|
527
|
+
|
|
528
|
+
# Mass-weight the coordinates
|
|
529
|
+
mw_geom_num_list = self.mass_weight_coordinates(geom_num_list, sqrt_mass_list)
|
|
530
|
+
|
|
531
|
+
# Mass-weight the gradients
|
|
532
|
+
mw_g = self.mass_weight_gradient(g, sqrt_mass_list)
|
|
533
|
+
mw_B_g = self.mass_weight_gradient(B_g, sqrt_mass_list)
|
|
534
|
+
|
|
535
|
+
# Store data for next iteration
|
|
536
|
+
if len(self.irc_mw_coords) >= 2:
|
|
537
|
+
self.irc_mw_coords.pop(0)
|
|
538
|
+
self.irc_mw_gradients.pop(0)
|
|
539
|
+
self.irc_mw_bias_gradients.pop(0)
|
|
540
|
+
|
|
541
|
+
self.irc_mw_coords.append(mw_geom_num_list.flatten())
|
|
542
|
+
self.irc_mw_gradients.append(mw_g.flatten())
|
|
543
|
+
self.irc_mw_bias_gradients.append(mw_B_g.flatten())
|
|
544
|
+
|
|
545
|
+
# Save structure to XYZ file
|
|
546
|
+
self.save_xyz_structure(cur_cycle, geom_num_list)
|
|
547
|
+
|
|
548
|
+
# Update the downhill step
|
|
549
|
+
self.update_mw_down_step(sqrt_mass_list, mw_B_g.flatten())
|
|
550
|
+
|
|
551
|
+
# If converged flag was set in update_mw_down_step, break out
|
|
552
|
+
if self.converged:
|
|
553
|
+
print("All targeted modes have been removed.")
|
|
554
|
+
break
|
|
555
|
+
|
|
556
|
+
# Diagonalize the current hessian to check modes
|
|
557
|
+
w, v = np.linalg.eigh(self.mw_hessian)
|
|
558
|
+
|
|
559
|
+
# Calculate wavenumbers
|
|
560
|
+
nus = self.eigval_to_wavenumber(w)
|
|
561
|
+
neg_inds = nus <= self.nu_thresh
|
|
562
|
+
neg_nus = nus[neg_inds]
|
|
563
|
+
self.neg_nus.append(neg_nus)
|
|
564
|
+
|
|
565
|
+
# Save energy and gradient data to CSV
|
|
566
|
+
self.save_to_csv(cur_cycle, e, B_e, g, B_g, neg_nus.tolist())
|
|
567
|
+
|
|
568
|
+
# Take a step
|
|
569
|
+
if not self.converged and self.mw_down_step is not None:
|
|
570
|
+
# Step along the downhill direction
|
|
571
|
+
mw_step = self.mw_down_step
|
|
572
|
+
unmw_step = self.unmass_weight_step(
|
|
573
|
+
mw_step.reshape(len(geom_num_list), 3),
|
|
574
|
+
sqrt_mass_list
|
|
575
|
+
)
|
|
576
|
+
geom_num_list = geom_num_list + unmw_step
|
|
577
|
+
|
|
578
|
+
# Remove center of mass motion
|
|
579
|
+
geom_num_list -= Calculationtools().calc_center_of_mass(geom_num_list, self.element_list)
|
|
580
|
+
|
|
581
|
+
# Calculate path bending angle if we have enough points
|
|
582
|
+
if len(self.irc_mw_coords) >= 3:
|
|
583
|
+
bend_angle = Calculationtools().calc_multi_dim_vec_angle(
|
|
584
|
+
self.irc_mw_coords[0] - self.irc_mw_coords[1],
|
|
585
|
+
self.irc_mw_coords[2] - self.irc_mw_coords[1]
|
|
586
|
+
)
|
|
587
|
+
self.path_bending_angle_list.append(np.degrees(bend_angle))
|
|
588
|
+
print("Path bending angle: ", np.degrees(bend_angle))
|
|
589
|
+
|
|
590
|
+
# Check for convergence of gradients
|
|
591
|
+
if convergence_check(B_g, self.MAX_FORCE_THRESHOLD, self.RMS_FORCE_THRESHOLD) and len(neg_nus) == 0:
|
|
592
|
+
print("Converged: All imaginary modes removed and gradient criteria satisfied.")
|
|
593
|
+
self.converged = True
|
|
594
|
+
break
|
|
595
|
+
|
|
596
|
+
# Print current geometry and info
|
|
597
|
+
print()
|
|
598
|
+
for i in range(len(geom_num_list)):
|
|
599
|
+
x = geom_num_list[i][0] * UnitValueLib().bohr2angstroms
|
|
600
|
+
y = geom_num_list[i][1] * UnitValueLib().bohr2angstroms
|
|
601
|
+
z = geom_num_list[i][2] * UnitValueLib().bohr2angstroms
|
|
602
|
+
print(f"{self.element_list[i]:<3} {x:17.12f} {y:17.12f} {z:17.12f}")
|
|
603
|
+
|
|
604
|
+
# Display information
|
|
605
|
+
print()
|
|
606
|
+
print("Energy : ", e)
|
|
607
|
+
print("Bias Energy : ", B_e)
|
|
608
|
+
print("RMS B. grad : ", np.sqrt((B_g**2).mean()))
|
|
609
|
+
print(f"Imag. freqs : {neg_nus} cm^-1")
|
|
610
|
+
print()
|
|
611
|
+
|
|
612
|
+
# Final hessian calculation if requested
|
|
613
|
+
if self.do_hess and not finish_frag:
|
|
614
|
+
print("Calculating final hessian...")
|
|
615
|
+
self.CE.hessian_flag = True
|
|
616
|
+
e_final, g_final, geom_num_list_final, _ = self.CE.single_point(
|
|
617
|
+
self.final_directory,
|
|
618
|
+
self.element_list,
|
|
619
|
+
cur_cycle + 1,
|
|
620
|
+
self.electric_charge_and_multiplicity,
|
|
621
|
+
self.xtb_method,
|
|
622
|
+
UnitValueLib().bohr2angstroms * geom_num_list
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
# Print final frequencies
|
|
626
|
+
final_hess = self.CE.Model_hess
|
|
627
|
+
final_hess = Calculationtools().project_out_hess_tr_and_rot(
|
|
628
|
+
final_hess, self.element_list, geom_num_list
|
|
629
|
+
)
|
|
630
|
+
|
|
631
|
+
# Reset QM interface settings
|
|
632
|
+
self.CE.hessian_flag = False
|
|
633
|
+
self.CE.FC_COUNT = self.FC_count
|
|
634
|
+
|
|
635
|
+
# Calculate and print final frequencies
|
|
636
|
+
w_final, _ = np.linalg.eigh(final_hess)
|
|
637
|
+
nus_final = self.eigval_to_wavenumber(w_final)
|
|
638
|
+
neg_inds_final = nus_final <= self.nu_thresh
|
|
639
|
+
neg_nus_final = nus_final[neg_inds_final]
|
|
640
|
+
|
|
641
|
+
print(f"Final wavenumbers of imaginary modes (<= {self.nu_thresh} cm^-1):")
|
|
642
|
+
print(f"{neg_nus_final} cm^-1")
|
|
643
|
+
|
|
644
|
+
# Save final structure with frequencies
|
|
645
|
+
self.save_to_csv(cur_cycle + 1, e_final, 0.0, g_final, g_final, neg_nus_final.tolist())
|
|
646
|
+
self.save_xyz_structure(cur_cycle + 1, geom_num_list)
|
|
647
|
+
|
|
648
|
+
# Save bending angle plot if we have enough data
|
|
649
|
+
if len(self.path_bending_angle_list) > 0:
|
|
650
|
+
G = Graph(self.directory)
|
|
651
|
+
G.single_plot(
|
|
652
|
+
np.array(range(len(self.path_bending_angle_list))),
|
|
653
|
+
np.array(self.path_bending_angle_list),
|
|
654
|
+
self.directory,
|
|
655
|
+
atom_num=0,
|
|
656
|
+
axis_name_1="# STEP",
|
|
657
|
+
axis_name_2="bending angle [degrees]",
|
|
658
|
+
name="ModeKill_bending"
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
print("ModeKill calculation finished.")
|
|
662
|
+
return
|