forked from duartegroup/autodE
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcalculation.py
328 lines (260 loc) · 10.2 KB
/
calculation.py
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
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
import autode.wrappers.keywords as kws
import autode.exceptions as ex
from copy import deepcopy
from typing import Optional, List, TYPE_CHECKING
from autode.point_charges import PointCharge
from autode.log import logger
from autode.calculations.types import CalculationType
from autode.calculations.executors import (
CalculationExecutor,
CalculationExecutorO,
CalculationExecutorG,
CalculationExecutorH,
)
if TYPE_CHECKING:
from autode.species.species import Species
from autode.wrappers.methods import Method
from autode.wrappers.keywords import Keywords
from autode.calculations.input import CalculationInput
from autode.calculations.output import CalculationOutput
from autode.calculations.executors import CalculationExecutor
from autode.opt.optimisers.base import BaseOptimiser
output_exts = (
".out",
".hess",
".xyz",
".inp",
".com",
".log",
".nw",
".pc",
".grad",
)
class Calculation:
def __init__(
self,
name: str,
molecule: "Species",
method: "Method",
keywords: "Keywords",
n_cores: int = 1,
point_charges: Optional[List[PointCharge]] = None,
):
"""
Calculation e.g. single point energy evaluation on a molecule. This
will update the molecule inplace. For example, an optimisation will
alter molecule.atoms.
-----------------------------------------------------------------------
Arguments:
name: Name of the calculation. Will be modified with a method
suffix
molecule: Molecule to be calculated. This may have a set of
associated cartesian or distance constraints
method: Wrapped electronic structure method, or other e.g.
forcefield capable of calculating energies and gradients
keywords: Keywords defining the type of calculation and e.g. what
basis set and functional to use.
n_cores: Number of cores available (default: {1})
point_charges: List of float of point charges
"""
self.name = name
self.n_cores = int(n_cores)
self.point_charges = point_charges
self._executor = self._executor_for(molecule, method, keywords)
self._check()
def _executor_for(
self,
molecule: "Species",
method: "Method",
keywords: "Keywords",
) -> "CalculationExecutor":
"""
Return a calculation executor depending on the calculation modes
implemented in the wrapped method. For instance if the method does not
implement any optimisation then use an executor that uses the in built
autodE optimisers (in autode/opt/). Equally if the method does not
implement way of calculating Hessians then use a numerical evaluation
of the Hessian
"""
_type = CalculationExecutor # base type, implements all calc types
if _are_opt(keywords) and not method.implements(CalculationType.opt):
_type = CalculationExecutorO
if _are_grad(keywords) and not method.implements(
CalculationType.gradient
):
_type = CalculationExecutorG
if _are_hess(keywords) and not method.implements(
CalculationType.hessian
):
_type = CalculationExecutorH
return _type(
self.name,
molecule,
method,
keywords,
self.n_cores,
self.point_charges,
)
def run(self) -> None:
"""Run the calculation using the EST method"""
logger.info(f"Running calculation: {self.name}")
self._executor.run()
self._check_properties_exist()
self._add_to_comp_methods()
return None
def clean_up(self, force: bool = False, everything: bool = False) -> None:
"""
Clean up input and output files, if Config.keep_input_files is False
(and not force=True)
-----------------------------------------------------------------------
Keyword Arguments:
force (bool): If True then override Config.keep_input_files
everything (bool): Remove both input and output files
"""
return self._executor.clean_up(force, everything)
def generate_input(self) -> None:
"""Generate the input required for this calculation"""
if not self.method.uses_external_io:
logger.warning(
"Calculation does not create an input file. No "
"input has been generated"
)
else:
self._executor.generate_input()
@property
def terminated_normally(self) -> bool:
"""
Determine if the calculation terminated without error
-----------------------------------------------------------------------
Returns:
(bool): Normal termination of the calculation?
"""
return self._executor.terminated_normally
@property
def input(self) -> "CalculationInput":
"""The input used to run this calculation"""
return self._executor.input
@property
def output(self) -> "CalculationOutput":
"""The output generated by this calculation"""
return self._executor.output
def set_output_filename(self, filename: str) -> None:
"""
Set the output filename. If it exists then the properties of
the molecule this calculation was created with from will be
set
"""
self._executor.output.filename = filename
self._executor.set_properties()
self._check_properties_exist()
return None
@property
def optimiser(self) -> "BaseOptimiser":
"""The optimiser used to run this calculation"""
return self._executor.optimiser
def copy(self) -> "Calculation":
return deepcopy(self)
@property
def molecule(self) -> "Species":
return self._executor.molecule
@molecule.setter
def molecule(self, value: "Species"):
self._executor.molecule = value
@property
def keywords(self) -> "Keywords":
return self._executor.input.keywords
@property
def method(self) -> "Method":
return self._executor.method
def _check(self) -> None:
"""
Ensure the molecule has the required properties and raise exceptions
if they are not present. Also ensure that the method has the requsted
solvent available.
-----------------------------------------------------------------------
Raises:
(ValueError | autode.exceptions.CalculationException):
"""
from autode.species.species import Species
assert isinstance(self.molecule, Species)
if self.molecule.atoms is None or self.molecule.n_atoms == 0:
raise ex.NoInputError("Have no atoms. Can't form a calculation")
if not self.molecule.has_valid_spin_state:
raise ex.CalculationException(
f"Cannot execute a calculation without a valid spin state: "
f"Spin multiplicity (2S+1) = {self.molecule.mult}"
)
return None
def _add_to_comp_methods(self) -> None:
"""Add the methods used in this calculation to the used methods list"""
from autode.log.methods import methods
methods.add(
f"Calculations were performed using {self.method.name} v. "
f"{self.method.version_in(self._executor)} "
f"({self.method.doi_str})."
)
# Type of calculation ----
if isinstance(self.input.keywords, kws.SinglePointKeywords):
string = "Single point "
elif isinstance(self.input.keywords, kws.OptKeywords):
string = "Optimisation "
else:
logger.warning(
"Not adding gradient or hessian to methods section "
"anticipating that they will be the same as opt"
)
# and have been already added to the methods section
return
# Level of theory ----
string += (
f"calculations performed at the "
f"{self.input.keywords.method_string} level"
)
basis = self.input.keywords.basis_set
if basis is not None:
string += (
f" in combination with the {str(basis)} "
f"({basis.doi_str}) basis set"
)
if (
self.molecule.solvent is not None
and self.molecule.solvent.is_implicit
):
solv_type = self.method.implicit_solvation_type
assert solv_type is not None, "Must have an implicit solvent type"
doi = solv_type.doi_str if hasattr(solv_type, "doi_str") else "?"
string += (
f" and {solv_type.upper()} ({doi}) "
f"solvation, with parameters appropriate for "
f"{self.molecule.solvent}"
)
methods.add(f"{string}.\n")
return None
def _check_properties_exist(self) -> None:
"""
Check that the requested properties, as defined by the type of keywords
that this calculation was requested with have been set.
-----------------------------------------------------------------------
Raises:
(CouldNotGetProperty): If the required property couldn't be found
"""
logger.info("Checking required properties exist")
if not self.terminated_normally:
logger.error(
f"Calculation of {self.molecule} did not terminate "
f"normally"
)
raise ex.CouldNotGetProperty()
if self.molecule.energy is None:
raise ex.CouldNotGetProperty(name="energy")
if _are_grad(self.keywords) and self.molecule.gradient is None:
raise ex.CouldNotGetProperty(name="gradient")
if _are_hess(self.keywords) and self.molecule.hessian is None:
raise ex.CouldNotGetProperty(name="Hessian")
return None
def _are_opt(keywords) -> bool:
return isinstance(keywords, kws.OptKeywords)
def _are_grad(keywords) -> bool:
return isinstance(keywords, kws.GradientKeywords)
def _are_hess(keywords) -> bool:
return isinstance(keywords, kws.HessianKeywords)