qccompute 0.13.0__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.
qccompute/__init__.py ADDED
@@ -0,0 +1,10 @@
1
+ from importlib import metadata as _metadata
2
+
3
+ try:
4
+ __version__ = _metadata.version(__name__)
5
+ except _metadata.PackageNotFoundError:
6
+ # Source tree / build hook / CI checkout
7
+ __version__ = "0.0.0+local"
8
+
9
+ from .main import compute, compute_args # noqa: F401
10
+ from .utils import get_adapter # noqa: F401
@@ -0,0 +1,12 @@
1
+ """Must import all adapters here for them to be found by the AdapterRegistry."""
2
+
3
+ from .base import * # noqa: F403
4
+ from .crest import CRESTAdapter # noqa: F401
5
+ from .file import FileAdapter # noqa: F401
6
+ from .geometric import GeometricAdapter # noqa: F401
7
+ from .orca import OrcaAdapter # noqa: F401
8
+ from .qcengine import QCEngineAdapter # noqa: F401
9
+ from .terachem import TeraChemAdapter # noqa: F401
10
+ from .terachem_fe import TeraChemFEAdapter # noqa: F401
11
+ from .terachem_pbs import TeraChemPBSAdapter # noqa: F401
12
+ from .xtb import XTBAdapter # noqa: F401
@@ -0,0 +1,325 @@
1
+ import traceback
2
+ from abc import ABC, abstractmethod
3
+ from collections.abc import Callable
4
+ from time import time
5
+ from typing import Any, Generic
6
+
7
+ from qcdata import (
8
+ CalcType,
9
+ Data,
10
+ DataType,
11
+ FileInput,
12
+ Files,
13
+ InputType,
14
+ ProgramOutput,
15
+ StructuredInputs,
16
+ )
17
+ from qcdata.helper_types import StrOrPath
18
+
19
+ from qccompute.exceptions import (
20
+ AdapterInputError,
21
+ ProgramNotFoundError,
22
+ QCComputeBaseError,
23
+ )
24
+
25
+ from .utils import construct_provenance, tmpdir
26
+
27
+ __all__ = ["BaseAdapter", "registry"]
28
+
29
+ # Registry for all Adaptors.
30
+ # NOTE: Registry stores class objects, not instances.
31
+ # Use registry[program]() to instantiate an adaptor
32
+ # Or use the higher level qccompute.utils.get_adapter() function.
33
+ registry = {}
34
+
35
+
36
+ class BaseAdapter(ABC, Generic[InputType, DataType]):
37
+ """Base class for all adapters."""
38
+
39
+ # Whether to this program reads or writes files to disk.
40
+ # If True, the adapter will write all files from input_data to a disk before executing
41
+ # the program. If False, the adapter must handle input files itself in some other
42
+ # way. Generally this should be True for most adapters unless the program can
43
+ # handle input files directly from memory, stdin, or some other mechanism that is
44
+ # more efficient than writing to disk.
45
+ uses_files = True
46
+ program: str # All subclasses must define this attribute.
47
+
48
+ def program_version(self, stdout: str | None) -> str | None:
49
+ """Return program version. Adapters should override this method.
50
+
51
+ Args:
52
+ stdout: The stdout from the program. Because running "program --version"
53
+ can be extremely slow for some programs, the stdout from the program
54
+ is passed in here so that the version can be extracted from it if
55
+ possible. If the version cannot be extracted from the stdout, then
56
+ this function should return the program version in some other way.
57
+ """
58
+ return None
59
+
60
+ @abstractmethod
61
+ def validate_input(self, input_data: InputType) -> None:
62
+ """Validate input_data to ensure compatibility with adapter.
63
+ Adapters should override this method.
64
+ """
65
+ raise NotImplementedError
66
+
67
+ @abstractmethod
68
+ def compute_data(
69
+ self,
70
+ input_data: InputType,
71
+ update_func: Callable | None = None,
72
+ update_interval: float | None = None,
73
+ **kwargs,
74
+ ) -> tuple[DataType, str]:
75
+ """Subclasses should implement this method with custom compute logic."""
76
+ raise NotImplementedError
77
+
78
+ def compute(
79
+ self,
80
+ input_data: InputType,
81
+ *,
82
+ scratch_dir: StrOrPath | None = None,
83
+ rm_scratch_dir: bool = True,
84
+ collect_logs: bool = True,
85
+ collect_files: bool = False,
86
+ collect_wfn: bool = False,
87
+ update_func: Callable | None = None,
88
+ update_interval: float | None = None,
89
+ print_logs: bool = False,
90
+ raise_exc: bool = True,
91
+ propagate_wfn: bool = False,
92
+ **adapter_kwargs,
93
+ ) -> ProgramOutput[InputType, DataType]:
94
+ """Compute the given input using the adapter's program.
95
+
96
+ Args:
97
+ input_data: A qcdata input object for a computation. E.g. A FileInput,
98
+ ProgramInput or DualProgramInput.
99
+ scratch_dir: The scratch directory for the program. If None, a new directory
100
+ is created in the system default temporary directory. If rm_scratch_dir
101
+ is True this directory will be deleted after the program finishes.
102
+ rm_scratch_dir: Delete the scratch directory after the program exits.
103
+ collect_logs: Whether to collect stdout/stderr from the program as output.
104
+ Failed computations will always collect stdout/stderr.
105
+ collect_files: Collect all files generated by the QC program as output.
106
+ collect_wfn: Collect the wavefunction file(s) from the calculation.
107
+ Not every program will support this. Use collect_files to collect
108
+ all files including the wavefunction.
109
+ update_func: A function to call as the program executes. The function must
110
+ accept the in-process stdout/stderr output as a string for its first
111
+ argument.
112
+ update_interval: The minimum time in seconds between calls to the
113
+ update_func.
114
+ print_logs: Whether to print stdout/stderr to the terminal in real time as
115
+ the program executes. Will be ignored if an update_func passed.
116
+ raise_exc: If False, qccompute will return a ProgramOutput object when the QC
117
+ program fails rather than raise an exception.
118
+ propagate_wfn: For any adapter performing a sequential task, such
119
+ as a geometry optimization, propagate the wavefunction from the previous
120
+ step to the next step. This is useful for accelerating convergence by
121
+ using a previously computed wavefunction as a starting guess. If an
122
+ adapter does not support wavefunction propagation, an AdapterInputError
123
+ will be raised.
124
+ **adapter_kwargs: Additional keyword arguments to pass to the adapter or
125
+ qcng.compute().
126
+
127
+ Returns:
128
+ A ProgramOutput object containing the results of the computation.
129
+
130
+ Raises:
131
+ AdapterNotFoundError: If the program is not supported (i.e., no Adapter
132
+ is implemented for the program in qccompute or qcengine).
133
+ ProgramNotFoundError: If the program executable is not found on the
134
+ system at execution time. This likely means the program is not installed
135
+ or not available on the $PATH.
136
+ AdapterInputError: If the input is invalid for the adapter.
137
+ ExternalProgramExecutionError: If the QC program fails during execution.
138
+ QCEngineError: If QCEngine performs the computation raises an error.
139
+ """
140
+ # Print stdout to terminal in real time as program executes
141
+ if print_logs and update_func is None:
142
+ update_func, update_interval = (
143
+ lambda _, stdout_new: print(stdout_new),
144
+ 0.1,
145
+ )
146
+
147
+ # cd to a temporary directory to run the program.
148
+ with tmpdir(self.uses_files, scratch_dir, rm_scratch_dir) as final_scratch_dir:
149
+ if self.uses_files: # Write non structured input files to disk.
150
+ input_data.save_files()
151
+
152
+ # Define outputs
153
+ prog_output_dict: dict[str, Any] = {}
154
+ logs: str | None = None
155
+ data: Data
156
+ exc: QCComputeBaseError | None = None
157
+ program_version: str | None = None
158
+
159
+ start = time()
160
+ try:
161
+ # Validate input object
162
+ self.validate_input(input_data)
163
+
164
+ # Execute the program. data will be Files for FileInput
165
+ data, logs = self.compute_data(
166
+ input_data,
167
+ update_func,
168
+ update_interval,
169
+ propagate_wfn=propagate_wfn,
170
+ **adapter_kwargs,
171
+ )
172
+ # None value covers FileInput case
173
+ prog_output_dict["success"] = True
174
+
175
+ # Optionally collect wavefunction file
176
+ if collect_wfn and not collect_files:
177
+ data.files.update(self.collect_wfn())
178
+
179
+ except QCComputeBaseError as e:
180
+ exc = e
181
+ prog_output_dict["success"] = False
182
+ # Any half-completed data
183
+ data = getattr(e, "data") or Files()
184
+ logs = getattr(e, "logs", logs) or logs
185
+ # For mypy because e.logs is not of a known type
186
+ logs = str(logs) if logs is not None else None
187
+ prog_output_dict["traceback"] = traceback.format_exc()
188
+
189
+ wall_time = time() - start
190
+
191
+ # Check for parsed version in extras at default location
192
+ program_version = data.extras.get("program_version")
193
+ if not program_version:
194
+ try:
195
+ program_version = self.program_version(logs)
196
+ except ProgramNotFoundError:
197
+ pass # program_version = None set above
198
+
199
+ # Construct Provenance object
200
+ provenance = construct_provenance(
201
+ self.program,
202
+ program_version,
203
+ final_scratch_dir,
204
+ wall_time,
205
+ )
206
+
207
+ # Always collect for failures; otherwise obey collect_logs
208
+ logs = logs if not prog_output_dict["success"] or collect_logs else None
209
+
210
+ # Construct ProgramOutput
211
+ prog_output_dict.update(
212
+ {
213
+ "input_data": input_data,
214
+ "logs": logs,
215
+ "data": data,
216
+ "provenance": provenance,
217
+ }
218
+ )
219
+ prog_output = ProgramOutput[InputType, DataType](**prog_output_dict)
220
+
221
+ # Collect files generated by the program
222
+ if self.uses_files and (collect_files or type(input_data) is FileInput):
223
+ prog_output.data.add_files(
224
+ final_scratch_dir,
225
+ recursive=True,
226
+ exclude=list(input_data.files.keys()),
227
+ )
228
+
229
+ # Append ProgramOutput to exception and raise if raise_exc=True
230
+ # Helpful for BigChem and ChemCloud exception handling
231
+ if raise_exc and exc:
232
+ exc.prog_output = prog_output
233
+ raise exc
234
+
235
+ return prog_output
236
+
237
+ def collect_wfn(self) -> dict[str, str | bytes]:
238
+ """Collect the wavefunction file(s) from the scratch_dir.
239
+
240
+ Returns:
241
+ Dictionary of filenames and file data. E.g. {"c0": b"filedata"}
242
+
243
+ """
244
+ # Collect wavefunction file from the calc_dir
245
+ raise AdapterInputError(
246
+ program=self.program,
247
+ message=f"Adapter for {self.program} does not support wavefunction collection.",
248
+ )
249
+
250
+
251
+ class ProgramAdapter(BaseAdapter, Generic[InputType, DataType]):
252
+ """Base adapter for all program adapters (all but FileAdaptor)."""
253
+
254
+ supported_calctypes: list[
255
+ CalcType
256
+ ] # All subclasses must specify supported calctypes
257
+
258
+ def __init_subclass__(cls, **kwargs):
259
+ super().__init_subclass__(**kwargs)
260
+ # Ensure that subclasses define the required class attributes
261
+ if not getattr(cls, "program", None):
262
+ raise NotImplementedError(
263
+ f"Subclasses of {ProgramAdapter.__name__} must define a program "
264
+ f"string. {cls.__name__} does not meet this requirement."
265
+ )
266
+
267
+ # Automatically register all subclasses
268
+ registry[cls.program] = cls
269
+
270
+ if not getattr(cls, "supported_calctypes", None):
271
+ raise NotImplementedError(
272
+ f"Subclasses of {ProgramAdapter.__name__} must define a nonempty "
273
+ f"supported_calctypes list. {cls.__name__} does not meet this "
274
+ "requirement."
275
+ )
276
+
277
+ @abstractmethod
278
+ def program_version(self, stdout: str | None) -> str:
279
+ """Get the version of the program.
280
+
281
+ Args:
282
+ stdout: The stdout from the program. Because running "program --version"
283
+ can be extremely slow for some programs, the stdout from the program
284
+ is passed in here so that the version can be extracted from it if
285
+ possible. If the version cannot be extracted from the stdout, then
286
+ this function should return the program version in some other way.
287
+ """
288
+
289
+ @abstractmethod
290
+ def compute_data(
291
+ self,
292
+ input_data: InputType,
293
+ update_func: Callable | None = None,
294
+ update_interval: float | None = None,
295
+ **kwargs,
296
+ ) -> tuple[DataType, str]:
297
+ """All ProgramAdapters must return a DataType."""
298
+ raise NotImplementedError
299
+
300
+ def validate_input(self, input_data: StructuredInputs) -> None:
301
+ """Validate the input object for compatibility with the adapter.
302
+
303
+ Args:
304
+ input_data: The input object to validate.
305
+
306
+ Raises:
307
+ AdapterInputError: If the input object's calctype is not supported.
308
+ """
309
+ if input_data.calctype not in self.supported_calctypes:
310
+ raise AdapterInputError(
311
+ program=self.program,
312
+ message=(
313
+ f"The {self.program} adapter does not yet support "
314
+ f"'{input_data.calctype.value}' calculations. This adaptor can "
315
+ f"compute: {[ct.value for ct in self.supported_calctypes]}"
316
+ ),
317
+ )
318
+ if input_data.files and not self.uses_files:
319
+ raise AdapterInputError(
320
+ program=self.program,
321
+ message=(
322
+ f"The {self.program} adapter does not support files as additional "
323
+ "inputs. Remove the files from your input."
324
+ ),
325
+ )
@@ -0,0 +1,134 @@
1
+ """Adapter for CREST package. https://crest-lab.github.io/crest-docs/"""
2
+
3
+ from collections.abc import Callable
4
+ from pathlib import Path
5
+
6
+ import qccodec
7
+ from qccodec.parsers.crest import parse_version
8
+ from qcdata import (
9
+ CalcType,
10
+ ConformerSearchResults,
11
+ OptimizationResults,
12
+ ProgramInput,
13
+ SinglePointData,
14
+ )
15
+
16
+ from qccompute.exceptions import AdapterInputError, ExternalProgramError
17
+
18
+ from .base import ProgramAdapter
19
+ from .utils import execute_subprocess
20
+
21
+
22
+ class CRESTAdapter(
23
+ ProgramAdapter[
24
+ ProgramInput,
25
+ SinglePointData | OptimizationResults | ConformerSearchResults,
26
+ ]
27
+ ):
28
+ """Adapter for CREST.
29
+
30
+ Note:
31
+ The `ProgramInput.keywords` attribute is used to create the input file for
32
+ CREST. This means that the structure of the `keywords` attribute should match
33
+ that of [CREST's input specification](https://crest-lab.github.io/crest-docs/page/documentation/inputfiles.html).
34
+ Keywords such as method, charge, and uhf (which are stored on the `Model` and
35
+ `Structure`; uhf is `multiplicity - 1`) will be added to the input file
36
+ automatically.
37
+
38
+ Warning:
39
+ CREST does not exit with a non-zero exit code on failure. Instead, it prints
40
+ "FAILED" in the stdout. This adapter will raise an ExternalProgramError if
41
+ "FAILED" is found in the stdout.
42
+
43
+ Warning:
44
+ CREST automatically translates the input geometry to the origin. This means
45
+ that the input geometry printed to CREST's stdout will not match the input
46
+ structure; however, all computed values (such as energies, gradients, etc.) are
47
+ still valid because they are translationally invariant.
48
+ """
49
+
50
+ supported_calctypes = [
51
+ CalcType.energy,
52
+ CalcType.gradient,
53
+ CalcType.hessian,
54
+ CalcType.optimization,
55
+ CalcType.conformer_search,
56
+ ]
57
+ """Supported calculation types."""
58
+ program = "crest"
59
+
60
+ def program_version(self, stdout: str | None = None) -> str:
61
+ """Get the program version.
62
+
63
+ Args:
64
+ stdout: The stdout from the program.
65
+
66
+ Returns:
67
+ The program version.
68
+ """
69
+ if not stdout:
70
+ stdout = execute_subprocess(self.program, ["--version"])
71
+ return parse_version(stdout)
72
+
73
+ def compute_data(
74
+ self,
75
+ input_data: ProgramInput,
76
+ update_func: Callable | None = None,
77
+ update_interval: float | None = None,
78
+ collect_rotamers: bool = False,
79
+ **kwargs,
80
+ ) -> tuple[SinglePointData | OptimizationResults | ConformerSearchResults, str]:
81
+ """Execute CREST on the given input.
82
+
83
+ Args:
84
+ input_data: The qcdata ProgramInput object for a computation.
85
+ update_func: A function to call with the stdout at regular intervals.
86
+ update_interval: The interval at which to call the update function.
87
+ collect_rotamers: Collect rotamers if doing a conformer_search. Defaults to
88
+ False since rotamers are usually not of interest and there will be many.
89
+
90
+ Returns:
91
+ A tuple of ConformerSearchResults and the stdout str.
92
+ """
93
+ # Create CREST native input files
94
+ try:
95
+ native_inp = qccodec.encode(input_data, self.program)
96
+ except qccodec.exceptions.EncoderError as e:
97
+ raise AdapterInputError(program=self.program) from e
98
+
99
+ # Write the input files to disk
100
+ inp_file, struct_file = Path("input.toml"), Path(native_inp.geometry_filename)
101
+ inp_file.write_text(native_inp.input_file)
102
+ struct_file.write_text(native_inp.geometry_file)
103
+
104
+ # Execute CREST
105
+ stdout = execute_subprocess(
106
+ self.program, [inp_file.name], update_func, update_interval
107
+ )
108
+
109
+ # CREST does not exit with a non-zero exit code on failure
110
+ if "FAILED" in stdout:
111
+ raise ExternalProgramError(
112
+ program=self.program,
113
+ message=f"CREST calculation failed. See the stdout for more information.",
114
+ logs=stdout,
115
+ )
116
+
117
+ # Parse the output
118
+ try:
119
+ results = qccodec.decode(
120
+ self.program,
121
+ input_data.calctype,
122
+ stdout=stdout,
123
+ directory=".",
124
+ input_data=input_data,
125
+ )
126
+ except qccodec.exceptions.ParserError as e:
127
+ raise ExternalProgramError(
128
+ program="qccodec",
129
+ message="Failed to parse CREST output.",
130
+ logs=stdout,
131
+ original_exception=e,
132
+ ) from e
133
+
134
+ return results, stdout
@@ -0,0 +1,49 @@
1
+ from collections.abc import Callable
2
+
3
+ from qcdata import FileInput, Files
4
+
5
+ from qccompute.adapters.base import BaseAdapter
6
+
7
+ from .utils import execute_subprocess
8
+
9
+
10
+ class FileAdapter(BaseAdapter[FileInput, Files]):
11
+ """adapter for running a program on files."""
12
+
13
+ def __init__(self, program: str) -> None:
14
+ super().__init__()
15
+ self.program = program
16
+
17
+ def validate_input(self, input_data: FileInput) -> None:
18
+ """No validation checks performed for FileAdapter"""
19
+ pass
20
+
21
+ def compute_data(
22
+ self,
23
+ input_data: FileInput,
24
+ update_func: Callable | None = None,
25
+ update_interval: float | None = None,
26
+ **kwargs,
27
+ ) -> tuple[Files, str]:
28
+ """Compute the given program on the given files.
29
+
30
+ Args:
31
+ input_data: The qcdata FileInput object for a computation.
32
+ update_func: A callback function to call as the program executes.
33
+ update_interval: The minimum time in seconds between calls to the
34
+ update_func.
35
+
36
+ Returns:
37
+ Tuple of a `Files` object and the program output string for a
38
+ computation. The returned `Files` instance is initially empty and
39
+ will be populated with file data by the :meth:`.compute` method.
40
+
41
+ Raises:
42
+ ProgramNotFoundException: If the program is not found.
43
+
44
+ """
45
+ stdout = execute_subprocess(
46
+ self.program, input_data.cmdline_args, update_func, update_interval
47
+ )
48
+ # Files will be added to this object by the .compute() method
49
+ return Files(), stdout