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.
Files changed (246) hide show
  1. multioptpy/Calculator/__init__.py +0 -0
  2. multioptpy/Calculator/ase_calculation_tools.py +424 -0
  3. multioptpy/Calculator/ase_tools/__init__.py +0 -0
  4. multioptpy/Calculator/ase_tools/fairchem.py +28 -0
  5. multioptpy/Calculator/ase_tools/gamess.py +19 -0
  6. multioptpy/Calculator/ase_tools/gaussian.py +165 -0
  7. multioptpy/Calculator/ase_tools/mace.py +28 -0
  8. multioptpy/Calculator/ase_tools/mopac.py +19 -0
  9. multioptpy/Calculator/ase_tools/nwchem.py +31 -0
  10. multioptpy/Calculator/ase_tools/orca.py +22 -0
  11. multioptpy/Calculator/ase_tools/pygfn0.py +37 -0
  12. multioptpy/Calculator/dxtb_calculation_tools.py +344 -0
  13. multioptpy/Calculator/emt_calculation_tools.py +458 -0
  14. multioptpy/Calculator/gpaw_calculation_tools.py +183 -0
  15. multioptpy/Calculator/lj_calculation_tools.py +314 -0
  16. multioptpy/Calculator/psi4_calculation_tools.py +334 -0
  17. multioptpy/Calculator/pwscf_calculation_tools.py +189 -0
  18. multioptpy/Calculator/pyscf_calculation_tools.py +327 -0
  19. multioptpy/Calculator/sqm1_calculation_tools.py +611 -0
  20. multioptpy/Calculator/sqm2_calculation_tools.py +376 -0
  21. multioptpy/Calculator/tblite_calculation_tools.py +352 -0
  22. multioptpy/Calculator/tersoff_calculation_tools.py +818 -0
  23. multioptpy/Constraint/__init__.py +0 -0
  24. multioptpy/Constraint/constraint_condition.py +834 -0
  25. multioptpy/Coordinate/__init__.py +0 -0
  26. multioptpy/Coordinate/polar_coordinate.py +199 -0
  27. multioptpy/Coordinate/redundant_coordinate.py +638 -0
  28. multioptpy/IRC/__init__.py +0 -0
  29. multioptpy/IRC/converge_criteria.py +28 -0
  30. multioptpy/IRC/dvv.py +544 -0
  31. multioptpy/IRC/euler.py +439 -0
  32. multioptpy/IRC/hpc.py +564 -0
  33. multioptpy/IRC/lqa.py +540 -0
  34. multioptpy/IRC/modekill.py +662 -0
  35. multioptpy/IRC/rk4.py +579 -0
  36. multioptpy/Interpolation/__init__.py +0 -0
  37. multioptpy/Interpolation/adaptive_interpolation.py +283 -0
  38. multioptpy/Interpolation/binomial_interpolation.py +179 -0
  39. multioptpy/Interpolation/geodesic_interpolation.py +785 -0
  40. multioptpy/Interpolation/interpolation.py +156 -0
  41. multioptpy/Interpolation/linear_interpolation.py +473 -0
  42. multioptpy/Interpolation/savitzky_golay_interpolation.py +252 -0
  43. multioptpy/Interpolation/spline_interpolation.py +353 -0
  44. multioptpy/MD/__init__.py +0 -0
  45. multioptpy/MD/thermostat.py +185 -0
  46. multioptpy/MEP/__init__.py +0 -0
  47. multioptpy/MEP/pathopt_bneb_force.py +443 -0
  48. multioptpy/MEP/pathopt_dmf_force.py +448 -0
  49. multioptpy/MEP/pathopt_dneb_force.py +130 -0
  50. multioptpy/MEP/pathopt_ewbneb_force.py +207 -0
  51. multioptpy/MEP/pathopt_gpneb_force.py +512 -0
  52. multioptpy/MEP/pathopt_lup_force.py +113 -0
  53. multioptpy/MEP/pathopt_neb_force.py +225 -0
  54. multioptpy/MEP/pathopt_nesb_force.py +205 -0
  55. multioptpy/MEP/pathopt_om_force.py +153 -0
  56. multioptpy/MEP/pathopt_qsm_force.py +174 -0
  57. multioptpy/MEP/pathopt_qsmv2_force.py +304 -0
  58. multioptpy/ModelFunction/__init__.py +7 -0
  59. multioptpy/ModelFunction/avoiding_model_function.py +29 -0
  60. multioptpy/ModelFunction/binary_image_ts_search_model_function.py +47 -0
  61. multioptpy/ModelFunction/conical_model_function.py +26 -0
  62. multioptpy/ModelFunction/opt_meci.py +50 -0
  63. multioptpy/ModelFunction/opt_mesx.py +47 -0
  64. multioptpy/ModelFunction/opt_mesx_2.py +49 -0
  65. multioptpy/ModelFunction/seam_model_function.py +27 -0
  66. multioptpy/ModelHessian/__init__.py +0 -0
  67. multioptpy/ModelHessian/approx_hessian.py +147 -0
  68. multioptpy/ModelHessian/calc_params.py +227 -0
  69. multioptpy/ModelHessian/fischer.py +236 -0
  70. multioptpy/ModelHessian/fischerd3.py +360 -0
  71. multioptpy/ModelHessian/fischerd4.py +398 -0
  72. multioptpy/ModelHessian/gfn0xtb.py +633 -0
  73. multioptpy/ModelHessian/gfnff.py +709 -0
  74. multioptpy/ModelHessian/lindh.py +165 -0
  75. multioptpy/ModelHessian/lindh2007d2.py +707 -0
  76. multioptpy/ModelHessian/lindh2007d3.py +822 -0
  77. multioptpy/ModelHessian/lindh2007d4.py +1030 -0
  78. multioptpy/ModelHessian/morse.py +106 -0
  79. multioptpy/ModelHessian/schlegel.py +144 -0
  80. multioptpy/ModelHessian/schlegeld3.py +322 -0
  81. multioptpy/ModelHessian/schlegeld4.py +559 -0
  82. multioptpy/ModelHessian/shortrange.py +346 -0
  83. multioptpy/ModelHessian/swartd2.py +496 -0
  84. multioptpy/ModelHessian/swartd3.py +706 -0
  85. multioptpy/ModelHessian/swartd4.py +918 -0
  86. multioptpy/ModelHessian/tshess.py +40 -0
  87. multioptpy/Optimizer/QHAdam.py +61 -0
  88. multioptpy/Optimizer/__init__.py +0 -0
  89. multioptpy/Optimizer/abc_fire.py +83 -0
  90. multioptpy/Optimizer/adabelief.py +58 -0
  91. multioptpy/Optimizer/adabound.py +68 -0
  92. multioptpy/Optimizer/adadelta.py +65 -0
  93. multioptpy/Optimizer/adaderivative.py +56 -0
  94. multioptpy/Optimizer/adadiff.py +68 -0
  95. multioptpy/Optimizer/adafactor.py +70 -0
  96. multioptpy/Optimizer/adam.py +65 -0
  97. multioptpy/Optimizer/adamax.py +62 -0
  98. multioptpy/Optimizer/adamod.py +83 -0
  99. multioptpy/Optimizer/adamw.py +65 -0
  100. multioptpy/Optimizer/adiis.py +523 -0
  101. multioptpy/Optimizer/afire_neb.py +282 -0
  102. multioptpy/Optimizer/block_hessian_update.py +709 -0
  103. multioptpy/Optimizer/c2diis.py +491 -0
  104. multioptpy/Optimizer/component_wise_scaling.py +405 -0
  105. multioptpy/Optimizer/conjugate_gradient.py +82 -0
  106. multioptpy/Optimizer/conjugate_gradient_neb.py +345 -0
  107. multioptpy/Optimizer/coordinate_locking.py +405 -0
  108. multioptpy/Optimizer/dic_rsirfo.py +1015 -0
  109. multioptpy/Optimizer/ediis.py +417 -0
  110. multioptpy/Optimizer/eve.py +76 -0
  111. multioptpy/Optimizer/fastadabelief.py +61 -0
  112. multioptpy/Optimizer/fire.py +77 -0
  113. multioptpy/Optimizer/fire2.py +249 -0
  114. multioptpy/Optimizer/fire_neb.py +92 -0
  115. multioptpy/Optimizer/gan_step.py +486 -0
  116. multioptpy/Optimizer/gdiis.py +609 -0
  117. multioptpy/Optimizer/gediis.py +203 -0
  118. multioptpy/Optimizer/geodesic_step.py +433 -0
  119. multioptpy/Optimizer/gpmin.py +633 -0
  120. multioptpy/Optimizer/gpr_step.py +364 -0
  121. multioptpy/Optimizer/gradientdescent.py +78 -0
  122. multioptpy/Optimizer/gradientdescent_neb.py +52 -0
  123. multioptpy/Optimizer/hessian_update.py +433 -0
  124. multioptpy/Optimizer/hybrid_rfo.py +998 -0
  125. multioptpy/Optimizer/kdiis.py +625 -0
  126. multioptpy/Optimizer/lars.py +21 -0
  127. multioptpy/Optimizer/lbfgs.py +253 -0
  128. multioptpy/Optimizer/lbfgs_neb.py +355 -0
  129. multioptpy/Optimizer/linesearch.py +236 -0
  130. multioptpy/Optimizer/lookahead.py +40 -0
  131. multioptpy/Optimizer/nadam.py +64 -0
  132. multioptpy/Optimizer/newton.py +200 -0
  133. multioptpy/Optimizer/prodigy.py +70 -0
  134. multioptpy/Optimizer/purtubation.py +16 -0
  135. multioptpy/Optimizer/quickmin_neb.py +245 -0
  136. multioptpy/Optimizer/radam.py +75 -0
  137. multioptpy/Optimizer/rfo_neb.py +302 -0
  138. multioptpy/Optimizer/ric_rfo.py +842 -0
  139. multioptpy/Optimizer/rl_step.py +627 -0
  140. multioptpy/Optimizer/rmspropgrave.py +65 -0
  141. multioptpy/Optimizer/rsirfo.py +1647 -0
  142. multioptpy/Optimizer/rsprfo.py +1056 -0
  143. multioptpy/Optimizer/sadam.py +60 -0
  144. multioptpy/Optimizer/samsgrad.py +63 -0
  145. multioptpy/Optimizer/tr_lbfgs.py +678 -0
  146. multioptpy/Optimizer/trim.py +273 -0
  147. multioptpy/Optimizer/trust_radius.py +207 -0
  148. multioptpy/Optimizer/trust_radius_neb.py +121 -0
  149. multioptpy/Optimizer/yogi.py +60 -0
  150. multioptpy/OtherMethod/__init__.py +0 -0
  151. multioptpy/OtherMethod/addf.py +1150 -0
  152. multioptpy/OtherMethod/dimer.py +895 -0
  153. multioptpy/OtherMethod/elastic_image_pair.py +629 -0
  154. multioptpy/OtherMethod/modelfunction.py +456 -0
  155. multioptpy/OtherMethod/newton_traj.py +454 -0
  156. multioptpy/OtherMethod/twopshs.py +1095 -0
  157. multioptpy/PESAnalyzer/__init__.py +0 -0
  158. multioptpy/PESAnalyzer/calc_irc_curvature.py +125 -0
  159. multioptpy/PESAnalyzer/cmds_analysis.py +152 -0
  160. multioptpy/PESAnalyzer/koopman_analysis.py +268 -0
  161. multioptpy/PESAnalyzer/pca_analysis.py +314 -0
  162. multioptpy/Parameters/__init__.py +0 -0
  163. multioptpy/Parameters/atomic_mass.py +20 -0
  164. multioptpy/Parameters/atomic_number.py +22 -0
  165. multioptpy/Parameters/covalent_radii.py +44 -0
  166. multioptpy/Parameters/d2.py +61 -0
  167. multioptpy/Parameters/d3.py +63 -0
  168. multioptpy/Parameters/d4.py +103 -0
  169. multioptpy/Parameters/dreiding.py +34 -0
  170. multioptpy/Parameters/gfn0xtb_param.py +137 -0
  171. multioptpy/Parameters/gfnff_param.py +315 -0
  172. multioptpy/Parameters/gnb.py +104 -0
  173. multioptpy/Parameters/parameter.py +22 -0
  174. multioptpy/Parameters/uff.py +72 -0
  175. multioptpy/Parameters/unit_values.py +20 -0
  176. multioptpy/Potential/AFIR_potential.py +55 -0
  177. multioptpy/Potential/LJ_repulsive_potential.py +345 -0
  178. multioptpy/Potential/__init__.py +0 -0
  179. multioptpy/Potential/anharmonic_keep_potential.py +28 -0
  180. multioptpy/Potential/asym_elllipsoidal_potential.py +718 -0
  181. multioptpy/Potential/electrostatic_potential.py +69 -0
  182. multioptpy/Potential/flux_potential.py +30 -0
  183. multioptpy/Potential/gaussian_potential.py +101 -0
  184. multioptpy/Potential/idpp.py +516 -0
  185. multioptpy/Potential/keep_angle_potential.py +146 -0
  186. multioptpy/Potential/keep_dihedral_angle_potential.py +105 -0
  187. multioptpy/Potential/keep_outofplain_angle_potential.py +70 -0
  188. multioptpy/Potential/keep_potential.py +99 -0
  189. multioptpy/Potential/mechano_force_potential.py +74 -0
  190. multioptpy/Potential/nanoreactor_potential.py +52 -0
  191. multioptpy/Potential/potential.py +896 -0
  192. multioptpy/Potential/spacer_model_potential.py +221 -0
  193. multioptpy/Potential/switching_potential.py +258 -0
  194. multioptpy/Potential/universal_potential.py +34 -0
  195. multioptpy/Potential/value_range_potential.py +36 -0
  196. multioptpy/Potential/void_point_potential.py +25 -0
  197. multioptpy/SQM/__init__.py +0 -0
  198. multioptpy/SQM/sqm1/__init__.py +0 -0
  199. multioptpy/SQM/sqm1/sqm1_core.py +1792 -0
  200. multioptpy/SQM/sqm2/__init__.py +0 -0
  201. multioptpy/SQM/sqm2/calc_tools.py +95 -0
  202. multioptpy/SQM/sqm2/sqm2_basis.py +850 -0
  203. multioptpy/SQM/sqm2/sqm2_bond.py +119 -0
  204. multioptpy/SQM/sqm2/sqm2_core.py +303 -0
  205. multioptpy/SQM/sqm2/sqm2_data.py +1229 -0
  206. multioptpy/SQM/sqm2/sqm2_disp.py +65 -0
  207. multioptpy/SQM/sqm2/sqm2_eeq.py +243 -0
  208. multioptpy/SQM/sqm2/sqm2_overlapint.py +704 -0
  209. multioptpy/SQM/sqm2/sqm2_qm.py +578 -0
  210. multioptpy/SQM/sqm2/sqm2_rep.py +66 -0
  211. multioptpy/SQM/sqm2/sqm2_srb.py +70 -0
  212. multioptpy/Thermo/__init__.py +0 -0
  213. multioptpy/Thermo/normal_mode_analyzer.py +865 -0
  214. multioptpy/Utils/__init__.py +0 -0
  215. multioptpy/Utils/bond_connectivity.py +264 -0
  216. multioptpy/Utils/calc_tools.py +884 -0
  217. multioptpy/Utils/oniom.py +96 -0
  218. multioptpy/Utils/pbc.py +48 -0
  219. multioptpy/Utils/riemann_curvature.py +208 -0
  220. multioptpy/Utils/symmetry_analyzer.py +482 -0
  221. multioptpy/Visualization/__init__.py +0 -0
  222. multioptpy/Visualization/visualization.py +156 -0
  223. multioptpy/WFAnalyzer/MO_analysis.py +104 -0
  224. multioptpy/WFAnalyzer/__init__.py +0 -0
  225. multioptpy/Wrapper/__init__.py +0 -0
  226. multioptpy/Wrapper/autots.py +1239 -0
  227. multioptpy/Wrapper/ieip_wrapper.py +93 -0
  228. multioptpy/Wrapper/md_wrapper.py +92 -0
  229. multioptpy/Wrapper/neb_wrapper.py +94 -0
  230. multioptpy/Wrapper/optimize_wrapper.py +76 -0
  231. multioptpy/__init__.py +5 -0
  232. multioptpy/entrypoints.py +916 -0
  233. multioptpy/fileio.py +660 -0
  234. multioptpy/ieip.py +340 -0
  235. multioptpy/interface.py +1086 -0
  236. multioptpy/irc.py +529 -0
  237. multioptpy/moleculardynamics.py +432 -0
  238. multioptpy/neb.py +1267 -0
  239. multioptpy/optimization.py +1553 -0
  240. multioptpy/optimizer.py +709 -0
  241. multioptpy-1.20.2.dist-info/METADATA +438 -0
  242. multioptpy-1.20.2.dist-info/RECORD +246 -0
  243. multioptpy-1.20.2.dist-info/WHEEL +5 -0
  244. multioptpy-1.20.2.dist-info/entry_points.txt +9 -0
  245. multioptpy-1.20.2.dist-info/licenses/LICENSE +674 -0
  246. 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)