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,865 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import numpy as np
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from multioptpy.Utils.symmetry_analyzer import SymmetryAnalyzer
|
|
6
|
+
from multioptpy.Parameters.parameter import atomic_mass, UnitValueLib
|
|
7
|
+
from multioptpy.Utils.calc_tools import Calculationtools
|
|
8
|
+
|
|
9
|
+
# Physical constants
|
|
10
|
+
HARTREE_TO_J = UnitValueLib().hartree2j
|
|
11
|
+
AVOGADRO = UnitValueLib().mol2au
|
|
12
|
+
KB = UnitValueLib().boltzmann_constant
|
|
13
|
+
PLANCK = UnitValueLib().planck_constant
|
|
14
|
+
BOHR = UnitValueLib().bohr2m
|
|
15
|
+
ATOMIC_MASS = UnitValueLib().amu2kg
|
|
16
|
+
LIGHT_SPEED = UnitValueLib().vacume_light_speed
|
|
17
|
+
LINDEP_THRESHOLD = 1e-7
|
|
18
|
+
BOHR2ANGSTROM = UnitValueLib().bohr2angstroms
|
|
19
|
+
|
|
20
|
+
def format_number_sequence(values, start_idx, end_idx):
|
|
21
|
+
"""Format a sequence of numbers for aligned output.
|
|
22
|
+
|
|
23
|
+
Parameters
|
|
24
|
+
----------
|
|
25
|
+
values : array_like
|
|
26
|
+
Array of values to format
|
|
27
|
+
start_idx : int
|
|
28
|
+
Starting index
|
|
29
|
+
end_idx : int
|
|
30
|
+
Ending index
|
|
31
|
+
|
|
32
|
+
Returns
|
|
33
|
+
-------
|
|
34
|
+
str
|
|
35
|
+
Formatted string of values
|
|
36
|
+
"""
|
|
37
|
+
return ''.join('%20.4f' % values[i] for i in range(start_idx, end_idx))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def format_mode_vectors(mode_array, atom_idx, start_idx, end_idx):
|
|
41
|
+
"""Format a row of normal mode vectors for output.
|
|
42
|
+
|
|
43
|
+
Parameters
|
|
44
|
+
----------
|
|
45
|
+
mode_array : array_like
|
|
46
|
+
3D array of mode vectors
|
|
47
|
+
atom_idx : int
|
|
48
|
+
Index of the atom
|
|
49
|
+
start_idx : int
|
|
50
|
+
Starting mode index
|
|
51
|
+
end_idx : int
|
|
52
|
+
Ending mode index
|
|
53
|
+
|
|
54
|
+
Returns
|
|
55
|
+
-------
|
|
56
|
+
str
|
|
57
|
+
Formatted string of mode vectors
|
|
58
|
+
"""
|
|
59
|
+
return ' '.join('%9.5f %9.5f %9.5f' % (mode_array[i, atom_idx, 0],
|
|
60
|
+
mode_array[i, atom_idx, 1],
|
|
61
|
+
mode_array[i, atom_idx, 2])
|
|
62
|
+
for i in range(start_idx, end_idx))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def output_to_console_and_file(text, console_output=sys.stdout, file_path=None):
|
|
66
|
+
"""Output text to both console and file if specified.
|
|
67
|
+
|
|
68
|
+
Parameters
|
|
69
|
+
----------
|
|
70
|
+
text : str
|
|
71
|
+
Text to output
|
|
72
|
+
console_output : file object
|
|
73
|
+
Console output stream (defaults to sys.stdout)
|
|
74
|
+
file_path : str or None
|
|
75
|
+
Path to output file (if None, file output is skipped)
|
|
76
|
+
"""
|
|
77
|
+
print(text, file=console_output)
|
|
78
|
+
if file_path is not None:
|
|
79
|
+
with open(file_path, 'a') as f:
|
|
80
|
+
f.write(text + '\n')
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def convert_energy_units(results, property_prefix, component_keys, unit):
|
|
84
|
+
"""Convert thermochemistry results for output.
|
|
85
|
+
|
|
86
|
+
Parameters
|
|
87
|
+
----------
|
|
88
|
+
results : dict
|
|
89
|
+
Results dictionary
|
|
90
|
+
property_prefix : str
|
|
91
|
+
Property prefix (e.g., 'S', 'H', 'G')
|
|
92
|
+
component_keys : tuple
|
|
93
|
+
Component keys (e.g., 'elec', 'trans', 'rot', 'vib')
|
|
94
|
+
unit : str
|
|
95
|
+
Current unit
|
|
96
|
+
|
|
97
|
+
Returns
|
|
98
|
+
-------
|
|
99
|
+
str
|
|
100
|
+
Formatted string with converted values
|
|
101
|
+
"""
|
|
102
|
+
# Only use atomic units (no conversion)
|
|
103
|
+
return ' '.join('%20.10f' % (results.get(f"{property_prefix}_{key}", (0,))[0])
|
|
104
|
+
for key in component_keys)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def output_thermodynamic_property(results, title, property_prefix, component_keys,
|
|
108
|
+
console_output, file_path=None):
|
|
109
|
+
"""Write a full line of thermochemistry output.
|
|
110
|
+
|
|
111
|
+
Parameters
|
|
112
|
+
----------
|
|
113
|
+
results : dict
|
|
114
|
+
Results dictionary
|
|
115
|
+
title : str
|
|
116
|
+
Property title
|
|
117
|
+
property_prefix : str
|
|
118
|
+
Property prefix (e.g., 'S', 'H', 'G')
|
|
119
|
+
component_keys : tuple
|
|
120
|
+
Component keys (e.g., 'elec', 'trans', 'rot', 'vib')
|
|
121
|
+
console_output : file object
|
|
122
|
+
Console output stream
|
|
123
|
+
file_path : str or None
|
|
124
|
+
Path to output file (if None, file output is skipped)
|
|
125
|
+
"""
|
|
126
|
+
total_value, unit = results[f"{property_prefix}_tot"]
|
|
127
|
+
formatted_values = convert_energy_units(results, property_prefix, component_keys, unit)
|
|
128
|
+
# Always display in atomic units (Eh)
|
|
129
|
+
output_line = '%-20s %s' % (f"{title} [{unit}]", formatted_values)
|
|
130
|
+
output_to_console_and_file(output_line, console_output, file_path)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class MolecularVibrations:
|
|
134
|
+
"""
|
|
135
|
+
Comprehensive analyzer for molecular vibrations, thermochemistry, and vibrational animations.
|
|
136
|
+
|
|
137
|
+
This class integrates normal mode analysis, thermochemistry calculations, and
|
|
138
|
+
visualization of vibrational modes for molecular systems.
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
def __init__(self, atoms, coordinates, hessian, symm_tolerance=1e-4, max_symm_fold=6):
|
|
142
|
+
"""
|
|
143
|
+
Parameters
|
|
144
|
+
----------
|
|
145
|
+
atoms : list of str
|
|
146
|
+
List of atomic symbols.
|
|
147
|
+
coordinates : np.ndarray
|
|
148
|
+
Atomic coordinates (n_atoms, 3).
|
|
149
|
+
hessian : np.ndarray
|
|
150
|
+
Hessian matrix (3*n_atoms, 3*n_atoms) in atomic units.
|
|
151
|
+
symm_tolerance : float
|
|
152
|
+
Distance tolerance for symmetry operations.
|
|
153
|
+
max_symm_fold : int
|
|
154
|
+
Maximum n-fold rotation to check.
|
|
155
|
+
"""
|
|
156
|
+
self.atoms = atoms
|
|
157
|
+
self.coordinates = np.array(coordinates)
|
|
158
|
+
self.hessian = np.array(hessian)
|
|
159
|
+
self.n_atoms = len(atoms)
|
|
160
|
+
self.symm_tolerance = symm_tolerance
|
|
161
|
+
self.max_symm_fold = max_symm_fold
|
|
162
|
+
|
|
163
|
+
# Initialize symmetry analyzer
|
|
164
|
+
self.symmetry_analyzer = SymmetryAnalyzer(atoms, coordinates, tol=symm_tolerance, max_n_fold=max_symm_fold)
|
|
165
|
+
self.point_group = self.symmetry_analyzer.analyze()
|
|
166
|
+
|
|
167
|
+
# Atomic masses in atomic mass units
|
|
168
|
+
self.mass = np.array([self._get_atomic_mass(atom) for atom in atoms])
|
|
169
|
+
|
|
170
|
+
# Center the molecule at its center of mass
|
|
171
|
+
self.com = np.einsum('z,zx->x', self.mass, self.coordinates) / self.mass.sum()
|
|
172
|
+
self.coordinates -= self.com
|
|
173
|
+
|
|
174
|
+
# Analysis results will be stored here
|
|
175
|
+
self.results = {}
|
|
176
|
+
|
|
177
|
+
def _get_atomic_mass(self, atom):
|
|
178
|
+
"""Returns atomic mass for a given element symbol."""
|
|
179
|
+
atomic_weights = atomic_mass(atom)
|
|
180
|
+
return atomic_weights
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def analyze_normal_modes(self, exclude_trans_and_rot=True, imaginary_freq=True):
|
|
184
|
+
"""
|
|
185
|
+
Perform normal mode analysis.
|
|
186
|
+
|
|
187
|
+
Parameters
|
|
188
|
+
----------
|
|
189
|
+
exclude_trans_and_rot : bool
|
|
190
|
+
Whether to exclude translational and rotational modes.
|
|
191
|
+
imaginary_freq : bool
|
|
192
|
+
Whether to represent imaginary frequencies as complex numbers.
|
|
193
|
+
|
|
194
|
+
Returns
|
|
195
|
+
-------
|
|
196
|
+
dict
|
|
197
|
+
Results of normal mode analysis.
|
|
198
|
+
"""
|
|
199
|
+
results = {}
|
|
200
|
+
|
|
201
|
+
if exclude_trans_and_rot:
|
|
202
|
+
# Project out translation and rotation from Hessian
|
|
203
|
+
h = Calculationtools().project_out_hess_tr_and_rot(
|
|
204
|
+
self.hessian,
|
|
205
|
+
self.atoms,
|
|
206
|
+
self.coordinates,
|
|
207
|
+
display_eigval=False
|
|
208
|
+
)
|
|
209
|
+
else:
|
|
210
|
+
# Just mass-weight the Hessian without projection
|
|
211
|
+
mass_vec = np.repeat(self.mass, 3) ** -0.5
|
|
212
|
+
h = self.hessian * np.outer(mass_vec, mass_vec)
|
|
213
|
+
|
|
214
|
+
# Diagonalize the projected Hessian to get eigenvalues and eigenvectors
|
|
215
|
+
force_const_au, mode = np.linalg.eigh(h)
|
|
216
|
+
|
|
217
|
+
freq_au = np.lib.scimath.sqrt(force_const_au)
|
|
218
|
+
results['freq_error'] = np.count_nonzero(freq_au.imag > 0)
|
|
219
|
+
if not imaginary_freq and np.iscomplexobj(freq_au):
|
|
220
|
+
freq_au = freq_au.real - np.abs(freq_au.imag)
|
|
221
|
+
|
|
222
|
+
results['freq_au'] = freq_au
|
|
223
|
+
au2hz = (HARTREE_TO_J / (ATOMIC_MASS * BOHR ** 2)) ** 0.5 / (2 * np.pi)
|
|
224
|
+
results['freq_wavenumber'] = freq_au * au2hz / LIGHT_SPEED * 1e-2
|
|
225
|
+
|
|
226
|
+
# Reshape mode vectors to (n_modes, n_atoms, 3)
|
|
227
|
+
mode_reshape = mode.T.reshape(-1, self.n_atoms, 3)
|
|
228
|
+
|
|
229
|
+
# Mass-weight the mode vectors - vectorized version
|
|
230
|
+
mass_sqrt_inv = 1.0 / np.sqrt(self.mass).reshape(1, -1, 1) # Shape (1, n_atoms, 1)
|
|
231
|
+
norm_mode = mode_reshape * mass_sqrt_inv # Broadcasting applies to all modes and coordinates
|
|
232
|
+
|
|
233
|
+
results['norm_mode'] = norm_mode
|
|
234
|
+
|
|
235
|
+
# Calculate reduced mass - vectorized version
|
|
236
|
+
reduced_mass = 1.0 / np.sum(np.sum(norm_mode * norm_mode, axis=2), axis=1)
|
|
237
|
+
results['reduced_mass'] = reduced_mass
|
|
238
|
+
|
|
239
|
+
# Vibrational temperature
|
|
240
|
+
results['vib_temperature'] = freq_au * au2hz * PLANCK / KB
|
|
241
|
+
|
|
242
|
+
# Force constants
|
|
243
|
+
dyne = 1e-2 * HARTREE_TO_J / BOHR ** 2
|
|
244
|
+
results['force_const_au'] = force_const_au
|
|
245
|
+
results['force_const_dyne'] = results['reduced_mass'] * force_const_au * dyne
|
|
246
|
+
|
|
247
|
+
self.results.update(results)
|
|
248
|
+
return results
|
|
249
|
+
|
|
250
|
+
def calculate_thermochemistry(self, e_tot=0.0, temperature=298.15, pressure=101325):
|
|
251
|
+
"""
|
|
252
|
+
Calculate thermochemical properties.
|
|
253
|
+
|
|
254
|
+
Parameters
|
|
255
|
+
----------
|
|
256
|
+
e_tot : float
|
|
257
|
+
Total electronic energy in Hartree.
|
|
258
|
+
temperature : float
|
|
259
|
+
Temperature in Kelvin.
|
|
260
|
+
pressure : float
|
|
261
|
+
Pressure in Pascal.
|
|
262
|
+
|
|
263
|
+
Returns
|
|
264
|
+
-------
|
|
265
|
+
dict
|
|
266
|
+
Thermochemistry results.
|
|
267
|
+
"""
|
|
268
|
+
if 'freq_au' not in self.results:
|
|
269
|
+
self.analyze_normal_modes()
|
|
270
|
+
|
|
271
|
+
results = {}
|
|
272
|
+
R_Eh = KB * AVOGADRO / (HARTREE_TO_J * AVOGADRO)
|
|
273
|
+
|
|
274
|
+
results['temperature'] = (temperature, 'K')
|
|
275
|
+
results['pressure'] = (pressure, 'Pa')
|
|
276
|
+
results['E0'] = (e_tot, 'Eh')
|
|
277
|
+
|
|
278
|
+
multiplicity = 1 # This could be a parameter
|
|
279
|
+
results['S_elec'] = (R_Eh * np.log(multiplicity), 'Eh/K')
|
|
280
|
+
results['Cv_elec'] = results['Cp_elec'] = (0, 'Eh/K')
|
|
281
|
+
results['E_elec'] = results['H_elec'] = (e_tot, 'Eh')
|
|
282
|
+
|
|
283
|
+
total_mass = self.mass.sum() * ATOMIC_MASS
|
|
284
|
+
q_trans = ((2.0 * np.pi * total_mass * KB * temperature / PLANCK ** 2) ** 1.5
|
|
285
|
+
* KB * temperature / pressure)
|
|
286
|
+
results['S_trans'] = (R_Eh * (2.5 + np.log(q_trans)), 'Eh/K')
|
|
287
|
+
results['Cv_trans'] = (1.5 * R_Eh, 'Eh/K')
|
|
288
|
+
results['Cp_trans'] = (2.5 * R_Eh, 'Eh/K')
|
|
289
|
+
results['E_trans'] = (1.5 * R_Eh * temperature, 'Eh')
|
|
290
|
+
results['H_trans'] = (2.5 * R_Eh * temperature, 'Eh')
|
|
291
|
+
|
|
292
|
+
rot_const = self.get_rotational_constants('GHz')
|
|
293
|
+
results['rot_const'] = (rot_const, 'GHz')
|
|
294
|
+
rotor_type = self._get_rotor_type(rot_const)
|
|
295
|
+
|
|
296
|
+
sym_number = self.get_rotational_symmetry_number()
|
|
297
|
+
results['sym_number'] = (sym_number, '')
|
|
298
|
+
|
|
299
|
+
if rotor_type == 'ATOM':
|
|
300
|
+
results['S_rot'] = (0, 'Eh/K')
|
|
301
|
+
results['Cv_rot'] = results['Cp_rot'] = (0, 'Eh/K')
|
|
302
|
+
results['E_rot'] = results['H_rot'] = (0, 'Eh')
|
|
303
|
+
elif rotor_type == 'LINEAR':
|
|
304
|
+
B = rot_const[1] * 1e9
|
|
305
|
+
q_rot = KB * temperature / (sym_number * PLANCK * B)
|
|
306
|
+
results['S_rot'] = (R_Eh * (1 + np.log(q_rot)), 'Eh/K')
|
|
307
|
+
results['Cv_rot'] = results['Cp_rot'] = (R_Eh, 'Eh/K')
|
|
308
|
+
results['E_rot'] = results['H_rot'] = (R_Eh * temperature, 'Eh')
|
|
309
|
+
else:
|
|
310
|
+
ABC = rot_const * 1e9
|
|
311
|
+
q_rot = ((KB * temperature / PLANCK) ** 1.5 * np.pi ** .5
|
|
312
|
+
/ (sym_number * np.prod(ABC) ** .5))
|
|
313
|
+
results['S_rot'] = (R_Eh * (1.5 + np.log(q_rot)), 'Eh/K')
|
|
314
|
+
results['Cv_rot'] = results['Cp_rot'] = (1.5 * R_Eh, 'Eh/K')
|
|
315
|
+
results['E_rot'] = results['H_rot'] = (1.5 * R_Eh * temperature, 'Eh')
|
|
316
|
+
|
|
317
|
+
freq_au = self.results['freq_au']
|
|
318
|
+
au2hz = (HARTREE_TO_J / (ATOMIC_MASS * BOHR ** 2)) ** 0.5 / (2 * np.pi)
|
|
319
|
+
# Use np.where to safely filter positive frequencies
|
|
320
|
+
pos_idx = np.where(freq_au.real > 0)[0]
|
|
321
|
+
vib_temperature = freq_au.real[pos_idx] * au2hz * PLANCK / KB
|
|
322
|
+
rt = vib_temperature / max(1e-14, temperature)
|
|
323
|
+
exp_neg_rt = np.exp(-rt)
|
|
324
|
+
|
|
325
|
+
ZPE = R_Eh * 0.5 * vib_temperature.sum()
|
|
326
|
+
|
|
327
|
+
results['ZPE'] = (ZPE, 'Eh')
|
|
328
|
+
|
|
329
|
+
tmp_denom = 1 - exp_neg_rt
|
|
330
|
+
mask = np.abs(tmp_denom) < 1e-10
|
|
331
|
+
tmp_denom[mask] = 1e-10
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
results['S_vib'] = (R_Eh * (rt * exp_neg_rt / tmp_denom - np.log(tmp_denom)).sum(), 'Eh/K')
|
|
335
|
+
results['Cv_vib'] = results['Cp_vib'] = (R_Eh * (exp_neg_rt * rt ** 2 / tmp_denom ** 2).sum(), 'Eh/K')
|
|
336
|
+
results['E_vib'] = results['H_vib'] = (
|
|
337
|
+
ZPE + R_Eh * temperature * (rt * exp_neg_rt / tmp_denom).sum(), 'Eh')
|
|
338
|
+
|
|
339
|
+
results['G_elec'] = (results['H_elec'][0] - temperature * results['S_elec'][0], 'Eh')
|
|
340
|
+
results['G_trans'] = (results['H_trans'][0] - temperature * results['S_trans'][0], 'Eh')
|
|
341
|
+
results['G_rot'] = (results['H_rot'][0] - temperature * results['S_rot'][0], 'Eh')
|
|
342
|
+
results['G_vib'] = (results['H_vib'][0] - temperature * results['S_vib'][0], 'Eh')
|
|
343
|
+
|
|
344
|
+
# Calculate total thermodynamic properties
|
|
345
|
+
keys = ('elec', 'trans', 'rot', 'vib')
|
|
346
|
+
for prop in ['S', 'Cv', 'Cp', 'E', 'H', 'G']:
|
|
347
|
+
results[f'{prop}_tot'] = (
|
|
348
|
+
sum(results.get(f"{prop}_{key}", (0,))[0] for key in keys),
|
|
349
|
+
'Eh' if prop in ['E', 'H', 'G'] else 'Eh/K'
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
results['E_0K'] = (e_tot + ZPE, 'Eh')
|
|
353
|
+
|
|
354
|
+
self.results.update(results)
|
|
355
|
+
return results
|
|
356
|
+
|
|
357
|
+
def get_rotational_constants(self, unit='GHz'):
|
|
358
|
+
"""
|
|
359
|
+
Calculate rotational constants.
|
|
360
|
+
|
|
361
|
+
Parameters
|
|
362
|
+
----------
|
|
363
|
+
unit : str
|
|
364
|
+
Unit for rotational constants ('GHz' or 'wavenumber').
|
|
365
|
+
|
|
366
|
+
Returns
|
|
367
|
+
-------
|
|
368
|
+
np.ndarray
|
|
369
|
+
Rotational constants.
|
|
370
|
+
"""
|
|
371
|
+
r = self.coordinates - self.com
|
|
372
|
+
inertia_tensor = np.einsum('z,zr,zs->rs', self.mass, r, r)
|
|
373
|
+
inertia_tensor = np.eye(3) * inertia_tensor.trace() - inertia_tensor
|
|
374
|
+
eigvals = np.sort(np.linalg.eigvalsh(inertia_tensor))
|
|
375
|
+
|
|
376
|
+
unit_inertia = ATOMIC_MASS * BOHR ** 2
|
|
377
|
+
unit_hz = PLANCK / (4 * np.pi * unit_inertia)
|
|
378
|
+
|
|
379
|
+
with np.errstate(divide='ignore'):
|
|
380
|
+
if unit.lower() == 'ghz':
|
|
381
|
+
eigvals = unit_hz / eigvals * 1e-9
|
|
382
|
+
elif unit.lower() == 'wavenumber':
|
|
383
|
+
eigvals = unit_hz / eigvals / LIGHT_SPEED * 1e-2
|
|
384
|
+
else:
|
|
385
|
+
raise ValueError(f"Unsupported unit {unit}")
|
|
386
|
+
return eigvals
|
|
387
|
+
|
|
388
|
+
def _get_rotor_type(self, rot_const):
|
|
389
|
+
"""Determine the rotor type from rotational constants."""
|
|
390
|
+
if np.all(rot_const > 1e8):
|
|
391
|
+
rotor_type = 'ATOM'
|
|
392
|
+
elif rot_const[0] > 1e8 and (rot_const[1] - rot_const[2] < 1e-3):
|
|
393
|
+
rotor_type = 'LINEAR'
|
|
394
|
+
else:
|
|
395
|
+
rotor_type = 'REGULAR'
|
|
396
|
+
return rotor_type
|
|
397
|
+
|
|
398
|
+
def get_rotational_symmetry_number(self):
|
|
399
|
+
"""
|
|
400
|
+
Determine the rotational symmetry number based on the point group.
|
|
401
|
+
|
|
402
|
+
Returns
|
|
403
|
+
-------
|
|
404
|
+
int
|
|
405
|
+
Symmetry number.
|
|
406
|
+
"""
|
|
407
|
+
group = self.point_group
|
|
408
|
+
|
|
409
|
+
if group == 'C∞v':
|
|
410
|
+
sigma = 1
|
|
411
|
+
elif group == 'D∞h':
|
|
412
|
+
sigma = 2
|
|
413
|
+
elif group in ['T', 'Td']:
|
|
414
|
+
sigma = 12
|
|
415
|
+
elif group == 'Oh':
|
|
416
|
+
sigma = 24
|
|
417
|
+
elif group == 'Ih':
|
|
418
|
+
sigma = 60
|
|
419
|
+
elif group.startswith('C'):
|
|
420
|
+
n = ''.join(filter(str.isdigit, group))
|
|
421
|
+
sigma = int(n) if n else 1
|
|
422
|
+
elif group.startswith('D'):
|
|
423
|
+
n = ''.join(filter(str.isdigit, group))
|
|
424
|
+
sigma = 2 * int(n) if n else 2
|
|
425
|
+
elif group.startswith('S'):
|
|
426
|
+
n = ''.join(filter(str.isdigit, group))
|
|
427
|
+
sigma = int(n) // 2 if n else 1
|
|
428
|
+
elif group in ['C1', 'Ci', 'Cs']:
|
|
429
|
+
sigma = 1
|
|
430
|
+
else:
|
|
431
|
+
sigma = 1
|
|
432
|
+
return sigma
|
|
433
|
+
|
|
434
|
+
def print_normal_modes(self, output_stream=sys.stdout, output_file=None, include_imag=True, cutoff_freq=0.1):
|
|
435
|
+
"""
|
|
436
|
+
Print normal mode information to both console and file (if output_file is given).
|
|
437
|
+
|
|
438
|
+
Parameters
|
|
439
|
+
----------
|
|
440
|
+
output_stream : file object
|
|
441
|
+
Console output stream (defaults to sys.stdout).
|
|
442
|
+
output_file : str or None
|
|
443
|
+
Path to the file to append the output (if None, file output is skipped).
|
|
444
|
+
include_imag : bool
|
|
445
|
+
Whether to include imaginary frequencies in the output.
|
|
446
|
+
cutoff_freq : float
|
|
447
|
+
Cutoff frequency in cm^-1. Modes with absolute frequency below this value are considered
|
|
448
|
+
translational/rotational and will be excluded.
|
|
449
|
+
"""
|
|
450
|
+
# Check if key exists instead of evaluating the array
|
|
451
|
+
if 'freq_wavenumber' not in self.results:
|
|
452
|
+
self.analyze_normal_modes()
|
|
453
|
+
|
|
454
|
+
freq_wn = self.results['freq_wavenumber']
|
|
455
|
+
|
|
456
|
+
# Filter out modes based on cutoff frequency (exclude small values that are likely translational/rotational)
|
|
457
|
+
if include_imag:
|
|
458
|
+
# Include both real and imaginary frequencies, but filter out near-zero values
|
|
459
|
+
idx = np.where((np.abs(freq_wn.real) > cutoff_freq) | (freq_wn.imag > cutoff_freq))[0]
|
|
460
|
+
else:
|
|
461
|
+
# Only positive real frequencies above cutoff
|
|
462
|
+
idx = np.where(freq_wn.real > cutoff_freq)[0]
|
|
463
|
+
|
|
464
|
+
# Sort modes: real frequencies first in ascending order, then imaginary in descending magnitude
|
|
465
|
+
sort_idx = np.argsort(freq_wn[idx].real)
|
|
466
|
+
idx = idx[sort_idx]
|
|
467
|
+
|
|
468
|
+
freq_wn_filtered = freq_wn[idx]
|
|
469
|
+
nfreq = len(idx)
|
|
470
|
+
|
|
471
|
+
# Filter other arrays based on selected frequencies
|
|
472
|
+
r_mass = self.results['reduced_mass'][idx]
|
|
473
|
+
force = self.results['force_const_dyne'][idx]
|
|
474
|
+
vib_t = self.results['vib_temperature'][idx]
|
|
475
|
+
mode = self.results['norm_mode'][idx]
|
|
476
|
+
|
|
477
|
+
# Check if any frequencies are imaginary
|
|
478
|
+
is_imag = np.zeros(nfreq, dtype=bool)
|
|
479
|
+
if include_imag:
|
|
480
|
+
is_imag = freq_wn_filtered.imag > 0
|
|
481
|
+
|
|
482
|
+
for col0, col1 in self._chunk_iterator(0, nfreq, 3):
|
|
483
|
+
header = 'Mode %s' % ''.join('%20d' % i for i in range(col0, col1))
|
|
484
|
+
output_to_console_and_file(header, output_stream, output_file)
|
|
485
|
+
|
|
486
|
+
freq_line = 'Freq [cm^-1] '
|
|
487
|
+
for i in range(col0, col1):
|
|
488
|
+
if i < nfreq:
|
|
489
|
+
if is_imag[i]:
|
|
490
|
+
# Imaginary frequency
|
|
491
|
+
freq_value = -np.abs(freq_wn_filtered[i])
|
|
492
|
+
freq_line += f'{freq_value.real:20.4f}'
|
|
493
|
+
else:
|
|
494
|
+
# Real frequency
|
|
495
|
+
freq_line += f'{freq_wn_filtered[i].real:20.4f}'
|
|
496
|
+
else:
|
|
497
|
+
freq_line += ' ' * 20
|
|
498
|
+
|
|
499
|
+
output_to_console_and_file(freq_line, output_stream, output_file)
|
|
500
|
+
|
|
501
|
+
# For imaginary frequencies, some values might not be physical
|
|
502
|
+
line = 'Reduced mass [au] %s' % format_number_sequence(r_mass.real, col0, col1)
|
|
503
|
+
output_to_console_and_file(line, output_stream, output_file)
|
|
504
|
+
|
|
505
|
+
line = 'Force const [Dyne/A] %s' % format_number_sequence(force.real, col0, col1)
|
|
506
|
+
output_to_console_and_file(line, output_stream, output_file)
|
|
507
|
+
|
|
508
|
+
line = 'Char temp [K] %s' % format_number_sequence(vib_t.real, col0, col1)
|
|
509
|
+
output_to_console_and_file(line, output_stream, output_file)
|
|
510
|
+
|
|
511
|
+
line = 'Normal mode %s' % (' x y z ' * (col1 - col0))
|
|
512
|
+
output_to_console_and_file(line, output_stream, output_file)
|
|
513
|
+
|
|
514
|
+
for j, atom in enumerate(self.atoms):
|
|
515
|
+
line = ' %4s %s' % (atom, format_mode_vectors(mode.real, j, col0, col1))
|
|
516
|
+
output_to_console_and_file(line, output_stream, output_file)
|
|
517
|
+
|
|
518
|
+
output_to_console_and_file('', output_stream, output_file)
|
|
519
|
+
|
|
520
|
+
def print_thermochemistry(self, output_stream=sys.stdout, output_file=None):
|
|
521
|
+
"""
|
|
522
|
+
Print thermochemistry information to both console and file (if output_file is given).
|
|
523
|
+
|
|
524
|
+
Parameters
|
|
525
|
+
----------
|
|
526
|
+
output_stream : file object
|
|
527
|
+
Console output stream (defaults to sys.stdout).
|
|
528
|
+
output_file : str or None
|
|
529
|
+
Path to the file to append the output (if None, file output is skipped).
|
|
530
|
+
"""
|
|
531
|
+
# Changed: Check if key exists instead of evaluating the array
|
|
532
|
+
if 'S_tot' not in self.results:
|
|
533
|
+
self.calculate_thermochemistry()
|
|
534
|
+
|
|
535
|
+
results = self.results
|
|
536
|
+
keys = ('tot', 'elec', 'trans', 'rot', 'vib')
|
|
537
|
+
|
|
538
|
+
output_to_console_and_file('Point group: %s' % self.point_group, output_stream, output_file)
|
|
539
|
+
output_to_console_and_file('Temperature %.4f [%s]' % results['temperature'], output_stream, output_file)
|
|
540
|
+
output_to_console_and_file('Pressure %.2f [%s]' % results['pressure'], output_stream, output_file)
|
|
541
|
+
output_to_console_and_file('Rotational constants [%s] %.5f %.5f %.5f' %
|
|
542
|
+
((results['rot_const'][1],) + tuple(results['rot_const'][0])), output_stream, output_file)
|
|
543
|
+
output_to_console_and_file('Symmetry number %d' % results['sym_number'][0], output_stream, output_file)
|
|
544
|
+
output_to_console_and_file('Zero-point energy (ZPE) %.10f [Eh]' % results['ZPE'][0], output_stream, output_file)
|
|
545
|
+
output_to_console_and_file(' %s' % ' '.join('%20s' % x for x in keys),
|
|
546
|
+
output_stream, output_file)
|
|
547
|
+
|
|
548
|
+
output_thermodynamic_property(results, 'Entropy', 'S', keys, output_stream, output_file)
|
|
549
|
+
output_thermodynamic_property(results, 'Cv', 'Cv', keys, output_stream, output_file)
|
|
550
|
+
output_thermodynamic_property(results, 'Cp', 'Cp', keys, output_stream, output_file)
|
|
551
|
+
|
|
552
|
+
output_to_console_and_file('Internal energy [Eh] %20.10f %20.10f %20.10f %20.10f %20.10f' %
|
|
553
|
+
(results['E_tot'][0], results['E_elec'][0],
|
|
554
|
+
results['E_trans'][0], results['E_rot'][0],
|
|
555
|
+
results['E_vib'][0]),
|
|
556
|
+
output_stream, output_file)
|
|
557
|
+
output_to_console_and_file('Total internal energy [Eh] %.10f' % results['E_tot'][0],
|
|
558
|
+
output_stream, output_file)
|
|
559
|
+
output_to_console_and_file('Electronic energy [Eh] %.10f' % results['E0'][0],
|
|
560
|
+
output_stream, output_file)
|
|
561
|
+
|
|
562
|
+
output_to_console_and_file('Enthalpy [Eh] %20.10f %20.10f %20.10f %20.10f %20.10f' %
|
|
563
|
+
(results['H_tot'][0], results['H_elec'][0],
|
|
564
|
+
results['H_trans'][0], results['H_rot'][0],
|
|
565
|
+
results['H_vib'][0]),
|
|
566
|
+
output_stream, output_file)
|
|
567
|
+
output_to_console_and_file('Total enthalpy [Eh] %.10f' % results['H_tot'][0],
|
|
568
|
+
output_stream, output_file)
|
|
569
|
+
|
|
570
|
+
output_to_console_and_file('Gibbs free energy [Eh] %20.10f %20.10f %20.10f %20.10f %20.10f' %
|
|
571
|
+
(results['G_tot'][0], results['G_elec'][0],
|
|
572
|
+
results['G_trans'][0], results['G_rot'][0],
|
|
573
|
+
results['G_vib'][0]),
|
|
574
|
+
output_stream, output_file)
|
|
575
|
+
output_to_console_and_file('Total Gibbs free energy [Eh] %.10f' % results['G_tot'][0],
|
|
576
|
+
output_stream, output_file)
|
|
577
|
+
|
|
578
|
+
def create_vibration_animation(self, mode_indices=None, n_frames=20, amplitude=3.0, output_dir=None,
|
|
579
|
+
include_imag=True, cutoff_freq=10.0):
|
|
580
|
+
"""
|
|
581
|
+
Create animations of normal modes and output to xyz files.
|
|
582
|
+
|
|
583
|
+
Parameters
|
|
584
|
+
----------
|
|
585
|
+
mode_indices : list or int or None
|
|
586
|
+
Indices of modes to animate.
|
|
587
|
+
Animates all supported modes if None.
|
|
588
|
+
n_frames : int
|
|
589
|
+
Number of frames in each animation.
|
|
590
|
+
amplitude : float
|
|
591
|
+
Amplitude of vibration (in Angstroms).
|
|
592
|
+
output_dir : str
|
|
593
|
+
Output directory (current directory if None).
|
|
594
|
+
include_imag : bool
|
|
595
|
+
Whether to include imaginary frequencies.
|
|
596
|
+
cutoff_freq : float
|
|
597
|
+
Cutoff frequency in cm^-1. Modes with absolute frequency below this value
|
|
598
|
+
will be excluded.
|
|
599
|
+
|
|
600
|
+
Returns
|
|
601
|
+
-------
|
|
602
|
+
list
|
|
603
|
+
List of paths to the output files.
|
|
604
|
+
"""
|
|
605
|
+
animator = _VibrationalModeAnimator(self, output_dir, include_imag, cutoff_freq)
|
|
606
|
+
if mode_indices is None:
|
|
607
|
+
# Animate all modes
|
|
608
|
+
return animator.create_all_animations(n_frames, amplitude)
|
|
609
|
+
elif isinstance(mode_indices, int):
|
|
610
|
+
# Animate a single mode
|
|
611
|
+
return [animator.create_animation(mode_indices, n_frames, amplitude)]
|
|
612
|
+
else:
|
|
613
|
+
# Animate specified modes
|
|
614
|
+
results = []
|
|
615
|
+
for idx in mode_indices:
|
|
616
|
+
results.append(animator.create_animation(idx, n_frames, amplitude))
|
|
617
|
+
return results
|
|
618
|
+
|
|
619
|
+
def _chunk_iterator(self, start, end, step):
|
|
620
|
+
"""Helper function for iterating in chunks for printing."""
|
|
621
|
+
for i in range(start, end, step):
|
|
622
|
+
yield i, min(i + step, end)
|
|
623
|
+
|
|
624
|
+
class _VibrationalModeAnimator:
|
|
625
|
+
"""
|
|
626
|
+
Internal helper class to create vibrational mode animations and output to xyz files.
|
|
627
|
+
"""
|
|
628
|
+
|
|
629
|
+
def __init__(self, analyzer, output_dir=None, include_imag=True, cutoff_freq=0.0001):
|
|
630
|
+
"""
|
|
631
|
+
Parameters
|
|
632
|
+
----------
|
|
633
|
+
analyzer : MolecularVibrations
|
|
634
|
+
Analyzer object that performed normal mode analysis.
|
|
635
|
+
output_dir : str
|
|
636
|
+
Output directory (current directory if None).
|
|
637
|
+
include_imag : bool
|
|
638
|
+
Whether to include imaginary frequencies.
|
|
639
|
+
cutoff_freq : float
|
|
640
|
+
Cutoff frequency in cm^-1. Modes with absolute frequency below this value
|
|
641
|
+
will be excluded.
|
|
642
|
+
"""
|
|
643
|
+
self.analyzer = analyzer
|
|
644
|
+
self.output_dir = output_dir or os.getcwd()
|
|
645
|
+
if not os.path.exists(self.output_dir):
|
|
646
|
+
os.makedirs(self.output_dir, exist_ok=True)
|
|
647
|
+
|
|
648
|
+
# Check if key exists instead of evaluating the array
|
|
649
|
+
if 'freq_wavenumber' not in analyzer.results:
|
|
650
|
+
analyzer.analyze_normal_modes()
|
|
651
|
+
|
|
652
|
+
# Prepare data for animation
|
|
653
|
+
self._prepare_animation_data(include_imag, cutoff_freq)
|
|
654
|
+
|
|
655
|
+
def _prepare_animation_data(self, include_imag=True, cutoff_freq=10.0):
|
|
656
|
+
"""
|
|
657
|
+
Prepare data for animation.
|
|
658
|
+
|
|
659
|
+
Parameters
|
|
660
|
+
----------
|
|
661
|
+
include_imag : bool
|
|
662
|
+
Whether to include imaginary frequencies.
|
|
663
|
+
cutoff_freq : float
|
|
664
|
+
Cutoff frequency in cm^-1. Modes with absolute frequency below this value
|
|
665
|
+
will be excluded.
|
|
666
|
+
"""
|
|
667
|
+
self.atoms = self.analyzer.atoms
|
|
668
|
+
self.coordinates = self.analyzer.coordinates
|
|
669
|
+
freq_wn = self.analyzer.results['freq_wavenumber']
|
|
670
|
+
|
|
671
|
+
# Filter out modes based on cutoff frequency
|
|
672
|
+
if include_imag:
|
|
673
|
+
# Include both real and imaginary frequencies, but filter out near-zero values
|
|
674
|
+
idx = np.where((np.abs(freq_wn.real) > cutoff_freq) | (freq_wn.imag > cutoff_freq))[0]
|
|
675
|
+
else:
|
|
676
|
+
# Only positive real frequencies above cutoff
|
|
677
|
+
idx = np.where(freq_wn.real > cutoff_freq)[0]
|
|
678
|
+
|
|
679
|
+
# Sort modes: real frequencies first in ascending order, then imaginary in descending magnitude
|
|
680
|
+
sort_idx = np.argsort(freq_wn[idx].real)
|
|
681
|
+
idx = idx[sort_idx]
|
|
682
|
+
|
|
683
|
+
self.freq_wn = freq_wn[idx]
|
|
684
|
+
self.is_imag = self.freq_wn.imag > 0
|
|
685
|
+
self.norm_mode = self.analyzer.results['norm_mode'][idx]
|
|
686
|
+
self.n_modes = len(idx)
|
|
687
|
+
|
|
688
|
+
def create_animation(self, mode_index, n_frames=20, amplitude=1.0, filename=None):
|
|
689
|
+
"""
|
|
690
|
+
Create an animation for a specified mode and output to an xyz file.
|
|
691
|
+
|
|
692
|
+
Parameters
|
|
693
|
+
----------
|
|
694
|
+
mode_index : int
|
|
695
|
+
Mode index (starting from 0).
|
|
696
|
+
n_frames : int
|
|
697
|
+
Number of frames in the animation.
|
|
698
|
+
amplitude : float
|
|
699
|
+
Amplitude of vibration (in Angstroms).
|
|
700
|
+
filename : str
|
|
701
|
+
Output filename (auto-generated if None).
|
|
702
|
+
|
|
703
|
+
Returns
|
|
704
|
+
-------
|
|
705
|
+
str
|
|
706
|
+
Path to the output file.
|
|
707
|
+
"""
|
|
708
|
+
if mode_index >= self.n_modes or mode_index < 0:
|
|
709
|
+
raise ValueError(f"Mode index must be between 0 and {self.n_modes-1}")
|
|
710
|
+
|
|
711
|
+
is_imag = self.is_imag[mode_index] if hasattr(self, 'is_imag') else False
|
|
712
|
+
freq_display = self.freq_wn[mode_index]
|
|
713
|
+
if is_imag:
|
|
714
|
+
freq_str = f"{abs(freq_display.imag):.0f}i"
|
|
715
|
+
else:
|
|
716
|
+
freq_str = f"{freq_display.real:.0f}"
|
|
717
|
+
|
|
718
|
+
if filename is None:
|
|
719
|
+
filename = f"mode_{mode_index+1}_{freq_str}_wave_number.xyz"
|
|
720
|
+
|
|
721
|
+
filepath = os.path.join(self.output_dir, filename)
|
|
722
|
+
|
|
723
|
+
mode_vector = self.norm_mode[mode_index].real # Use real part of mode vector
|
|
724
|
+
|
|
725
|
+
with open(filepath, 'w') as f:
|
|
726
|
+
for frame in range(n_frames):
|
|
727
|
+
phase = 2 * np.pi * frame / (n_frames - 1)
|
|
728
|
+
displacement = amplitude * np.sin(phase)
|
|
729
|
+
displaced_coords = self.coordinates.copy()
|
|
730
|
+
for atom_idx in range(len(self.atoms)):
|
|
731
|
+
displaced_coords[atom_idx] += displacement * mode_vector[atom_idx]
|
|
732
|
+
f.write(f"{len(self.atoms)}\n")
|
|
733
|
+
f.write(f"Mode {mode_index+1}, Freq: {freq_str} cm-1, Frame: {frame+1}/{n_frames}\n")
|
|
734
|
+
for atom_idx, atom in enumerate(self.atoms):
|
|
735
|
+
x, y, z = displaced_coords[atom_idx] * BOHR2ANGSTROM
|
|
736
|
+
f.write(f"{atom} {x:.6f} {y:.6f} {z:.6f}\n")
|
|
737
|
+
return filepath
|
|
738
|
+
|
|
739
|
+
def create_all_animations(self, n_frames=20, amplitude=0.5, base_filename=None):
|
|
740
|
+
"""
|
|
741
|
+
Create animations for all vibrational modes.
|
|
742
|
+
|
|
743
|
+
Parameters
|
|
744
|
+
----------
|
|
745
|
+
n_frames : int
|
|
746
|
+
Number of frames for each animation.
|
|
747
|
+
amplitude : float
|
|
748
|
+
Amplitude of vibration (in Angstroms).
|
|
749
|
+
base_filename : str
|
|
750
|
+
Base name for output files (default is "mode_").
|
|
751
|
+
|
|
752
|
+
Returns
|
|
753
|
+
-------
|
|
754
|
+
list
|
|
755
|
+
List of paths to the output files.
|
|
756
|
+
"""
|
|
757
|
+
base_filename = base_filename or "mode_"
|
|
758
|
+
output_files = []
|
|
759
|
+
for i in range(self.n_modes):
|
|
760
|
+
self.is_imag[i] = self.freq_wn[i].imag > 0
|
|
761
|
+
if self.is_imag[i]:
|
|
762
|
+
freq_str = f"{abs(self.freq_wn[i].imag):.0f}i"
|
|
763
|
+
else:
|
|
764
|
+
freq_str = f"{self.freq_wn[i].real:.0f}"
|
|
765
|
+
|
|
766
|
+
filename = f"{base_filename}{i+1}_{freq_str}_wave_number.xyz"
|
|
767
|
+
output_path = self.create_animation(i, n_frames, amplitude, filename)
|
|
768
|
+
output_files.append(output_path)
|
|
769
|
+
return output_files
|
|
770
|
+
|
|
771
|
+
def analyze_molecular_vibrations(atoms, coordinates, hessian, symm_tolerance=0.25, max_symm_fold=6):
|
|
772
|
+
"""
|
|
773
|
+
Analyze normal modes of a molecular system.
|
|
774
|
+
|
|
775
|
+
Parameters
|
|
776
|
+
----------
|
|
777
|
+
atoms : list of str
|
|
778
|
+
List of atomic symbols.
|
|
779
|
+
coordinates : np.ndarray
|
|
780
|
+
Atomic coordinates (n_atoms, 3).
|
|
781
|
+
hessian : np.ndarray
|
|
782
|
+
Hessian matrix (3*n_atoms, 3*n_atoms) in atomic units.
|
|
783
|
+
symm_tolerance : float
|
|
784
|
+
Distance tolerance for symmetry operations.
|
|
785
|
+
max_symm_fold : int
|
|
786
|
+
Maximum n-fold rotation to check.
|
|
787
|
+
|
|
788
|
+
Returns
|
|
789
|
+
-------
|
|
790
|
+
dict
|
|
791
|
+
Results of normal mode analysis.
|
|
792
|
+
"""
|
|
793
|
+
analyzer = MolecularVibrations(atoms, coordinates, hessian, symm_tolerance, max_symm_fold)
|
|
794
|
+
return analyzer.analyze_normal_modes()
|
|
795
|
+
|
|
796
|
+
|
|
797
|
+
def calculate_molecular_thermochemistry(atoms, coordinates, hessian, e_tot=0.0, temperature=298.15,
|
|
798
|
+
pressure=101325, symm_tolerance=0.25, max_symm_fold=6):
|
|
799
|
+
"""
|
|
800
|
+
Calculate thermochemical properties for a molecular system.
|
|
801
|
+
|
|
802
|
+
Parameters
|
|
803
|
+
----------
|
|
804
|
+
atoms : list of str
|
|
805
|
+
List of atomic symbols.
|
|
806
|
+
coordinates : np.ndarray
|
|
807
|
+
Atomic coordinates (n_atoms, 3).
|
|
808
|
+
hessian : np.ndarray
|
|
809
|
+
Hessian matrix (3*n_atoms, 3*n_atoms) in atomic units.
|
|
810
|
+
e_tot : float
|
|
811
|
+
Total electronic energy in Hartree.
|
|
812
|
+
temperature : float
|
|
813
|
+
Temperature in Kelvin.
|
|
814
|
+
pressure : float
|
|
815
|
+
Pressure in Pascal.
|
|
816
|
+
symm_tolerance : float
|
|
817
|
+
Distance tolerance for symmetry operations.
|
|
818
|
+
max_symm_fold : int
|
|
819
|
+
Maximum n-fold rotation to check.
|
|
820
|
+
|
|
821
|
+
Returns
|
|
822
|
+
-------
|
|
823
|
+
dict
|
|
824
|
+
Thermochemistry results.
|
|
825
|
+
"""
|
|
826
|
+
analyzer = MolecularVibrations(atoms, coordinates, hessian, symm_tolerance, max_symm_fold)
|
|
827
|
+
analyzer.analyze_normal_modes()
|
|
828
|
+
return analyzer.calculate_thermochemistry(e_tot, temperature, pressure)
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
def generate_vibration_animation(atoms, coordinates, hessian, mode_index=None,
|
|
832
|
+
n_frames=20, amplitude=3.0, output_dir=None,
|
|
833
|
+
symm_tolerance=0.25, max_symm_fold=6):
|
|
834
|
+
"""
|
|
835
|
+
Generate animation files for molecular vibrations.
|
|
836
|
+
|
|
837
|
+
Parameters
|
|
838
|
+
----------
|
|
839
|
+
atoms : list of str
|
|
840
|
+
List of atomic symbols.
|
|
841
|
+
coordinates : np.ndarray
|
|
842
|
+
Atomic coordinates (n_atoms, 3).
|
|
843
|
+
hessian : np.ndarray
|
|
844
|
+
Hessian matrix (3*n_atoms, 3*n_atoms) in atomic units.
|
|
845
|
+
mode_index : int or list or None
|
|
846
|
+
Index or indices of modes to animate (None for all modes)
|
|
847
|
+
n_frames : int
|
|
848
|
+
Number of frames in each animation.
|
|
849
|
+
amplitude : float
|
|
850
|
+
Amplitude of vibration (in Angstroms).
|
|
851
|
+
output_dir : str
|
|
852
|
+
Output directory (current directory if None).
|
|
853
|
+
symm_tolerance : float
|
|
854
|
+
Distance tolerance for symmetry operations.
|
|
855
|
+
max_symm_fold : int
|
|
856
|
+
Maximum n-fold rotation to check.
|
|
857
|
+
|
|
858
|
+
Returns
|
|
859
|
+
-------
|
|
860
|
+
list
|
|
861
|
+
List of paths to the animation files.
|
|
862
|
+
"""
|
|
863
|
+
analyzer = MolecularVibrations(atoms, coordinates, hessian, symm_tolerance, max_symm_fold)
|
|
864
|
+
analyzer.analyze_normal_modes()
|
|
865
|
+
return analyzer.create_vibration_animation(mode_index, n_frames, amplitude, output_dir)
|