-
Notifications
You must be signed in to change notification settings - Fork 260
Expand file tree
/
Copy pathadapter.py
More file actions
205 lines (183 loc) · 8.47 KB
/
Copy pathadapter.py
File metadata and controls
205 lines (183 loc) · 8.47 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
#!/usr/bin/env python3
###############################################################################
# #
# RMG - Reaction Mechanism Generator #
# #
# Copyright (c) 2002-2026 Prof. William H. Green (whgreen@mit.edu), #
# Prof. Richard H. West (r.west@neu.edu) and the RMG Team (rmg_dev@mit.edu) #
# #
# Permission is hereby granted, free of charge, to any person obtaining a #
# copy of this software and associated documentation files (the 'Software'), #
# to deal in the Software without restriction, including without limitation #
# the rights to use, copy, modify, merge, publish, distribute, sublicense, #
# and/or sell copies of the Software, and to permit persons to whom the #
# Software is furnished to do so, subject to the following conditions: #
# #
# The above copyright notice and this permission notice shall be included in #
# all copies or substantial portions of the Software. #
# #
# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING #
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER #
# DEALINGS IN THE SOFTWARE. #
# #
###############################################################################
"""
A module for the abstract ESSAdapter class
"""
from abc import ABC, abstractmethod
import logging
import os
import shutil
from rmgpy.qm.qmdata import QMData
from rmgpy.qm.symmetry import PointGroupCalculator
class ESSAdapter(ABC):
"""
An abstract ESS Adapter class
"""
def __init__(self, path, check_for_errors=True, scratch_directory=None):
self.path = path
if check_for_errors:
self.check_for_errors()
self.scratch_directory = scratch_directory if scratch_directory is not None else os.path.join(os.path.abspath('.'), str('scratch'))
@abstractmethod
def check_for_errors(self):
"""
Checks the log file for common errors.
Optionally runs when the class is initialized to catch errors before parsing relevant information.
"""
pass
@abstractmethod
def get_number_of_atoms(self):
"""
Return the number of atoms in the molecular configuration.
"""
pass
@abstractmethod
def load_force_constant_matrix(self):
"""
Return the force constant matrix (in Cartesian coordinates). If multiple such matrices
are identified, only the last is returned. The units of the returned force constants
are J/m^2. If no force constant matrix can be found in the log file, ``None`` is returned.
"""
pass
@abstractmethod
def load_geometry(self):
"""
Return the optimum geometry of the molecular configuration.
If multiple such geometries are identified, only the last is returned.
"""
pass
@abstractmethod
def load_conformer(self, symmetry=None, spin_multiplicity=0, optical_isomers=None, label=''):
"""
Return the optimum geometry of the molecular configuration.
"""
pass
@abstractmethod
def load_energy(self, zpe_scale_factor=1.):
"""
Load the energy in J/mol from a log file. Only the last energy in the file is returned.
The zero-point energy is *not* included in the returned value.
"""
pass
@abstractmethod
def load_zero_point_energy(self):
"""
Load the unscaled zero-point energy in J/mol from a log file.
"""
pass
@abstractmethod
def load_scan_energies(self):
"""
Extract the optimized energies in J/mol from a torsional scan log file.
"""
pass
@abstractmethod
def load_negative_frequency(self):
"""
Return the imaginary frequency from a transition state frequency
calculation in cm^-1.
"""
pass
@abstractmethod
def load_scan_pivot_atoms(self):
"""
Extract the atom numbers which the rotor scan pivots around.
Return a list of atom numbers starting with the first atom as 1.
"""
pass
@abstractmethod
def load_scan_frozen_atoms(self):
"""
Extract the atom numbers which were frozen during the scan.
Return a list of list of atom numbers starting with the first atom as 1.
Each element of the outer lists represents a frozen bond.
Inner lists with length 2 represent frozen bond lengths.
Inner lists with length 3 represent frozen bond angles.
Inner lists with length 4 represent frozen dihedral angles.
"""
pass
def get_software(self):
"""
Return the name of the software. Should correspond to the class
name without 'Log'.
"""
return self.__class__.__name__.replace('Log', '')
def get_D1_diagnostic(self):
"""
Returns the D1 diagnostic from output log.
If multiple occurrences exist, returns the last occurrence.
Should be implemented by the relevant subclass.
"""
raise NotImplementedError(f"get_D1_diagnostic failed for {self.path} "
f"since the method is not implemented for all ESSAdapter subclasses.")
def get_T1_diagnostic(self):
"""
Returns the T1 diagnostic from output log.
If multiple occurrences exist, returns the last occurrence.
Should be implemented by the relevant subclass.
"""
raise NotImplementedError(f"get_T1_diagnostic failed for {self.path} "
f"since the method is not implemented for all ESSAdapter subclasses.")
def get_symmetry_properties(self):
"""
This method uses the symmetry package from RMG's QM module
and returns a tuple where the first element is the number
of optical isomers, the second element is the symmetry number,
and the third element is the point group identified.
"""
coordinates, atom_numbers, _ = self.load_geometry()
unique_id = '0' # Just some name that the SYMMETRY code gives to one of its jobs
# Scratch directory that the SYMMETRY code writes its files in:
scr_dir = self.scratch_directory
if not os.path.exists(scr_dir):
os.makedirs(scr_dir)
try:
qmdata = QMData(
groundStateDegeneracy=1, # Only needed to check if valid QMData
numberOfAtoms=len(atom_numbers),
atomicNumbers=atom_numbers,
atomCoords=(coordinates, str('angstrom')),
energy=(0.0, str('kcal/mol')) # Only needed to avoid error
)
# Dynamically create custom class to store the settings needed for the point group calculation
# Normally, it expects an rmgpy.qm.main.QMSettings object, but we don't need all of those settings
settings = type(str(''), (),
dict(symmetryPath=str('symmetry'), scratchDirectory=scr_dir))()
pgc = PointGroupCalculator(settings, unique_id, qmdata)
pg = pgc.calculate()
if pg is not None:
optical_isomers = 2 if pg.chiral else 1
symmetry = pg.symmetry_number
logging.debug("Symmetry algorithm found {0} optical isomers and a symmetry number of {1}".format(
optical_isomers, symmetry))
else:
logging.error('Symmetry algorithm errored when computing point group\nfor log file located at{0}.\n'
'Manually provide values in Arkane input.'.format(self.path))
return optical_isomers, symmetry, pg.point_group
finally:
shutil.rmtree(scr_dir)