qcdata 0.17.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.
qcdata/models/data.py ADDED
@@ -0,0 +1,387 @@
1
+ """Structured output data objects."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import warnings
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING, Any, TypeVar, Union
8
+
9
+ import numpy as np
10
+ from pydantic import BaseModel, ValidationInfo, field_validator, model_validator
11
+ from typing_extensions import Self
12
+
13
+ from qcdata.helper_types import SerializableNDArray
14
+
15
+ from .base_models import CalcType, Files, QCDataBaseModel
16
+ from .inputs import ProgramInput
17
+ from .structure import Structure
18
+ from .utils import deprecated_class, to_multi_xyz
19
+
20
+ if TYPE_CHECKING:
21
+ from .outputs import ProgramOutput
22
+
23
+ __all__ = [
24
+ "Wavefunction",
25
+ "SinglePointData",
26
+ "OptimizationData",
27
+ "ConformerSearchData",
28
+ "StructuredData",
29
+ "StructuredDataType",
30
+ "Data",
31
+ "DataType",
32
+ "SinglePointResults",
33
+ "OptimizationResults",
34
+ "ConformerSearchResults",
35
+ ]
36
+
37
+
38
+ class Wavefunction(QCDataBaseModel):
39
+ """The wavefunction for a single point calculation.
40
+
41
+ Attributes:
42
+ scf_eigenvalues_a: The SCF alpha-spin orbital eigenvalues.
43
+ scf_eigenvalues_b: The SCF beta-spin orbital eigenvalues.
44
+ scf_occupations_a: The SCF alpha-spin orbital occupations.
45
+ scf_occupations_b: The SCF beta-spin orbital occupations.
46
+ """
47
+
48
+ scf_eigenvalues_a: SerializableNDArray | None = None
49
+ scf_eigenvalues_b: SerializableNDArray | None = None
50
+ scf_occupations_a: SerializableNDArray | None = None
51
+ scf_occupations_b: SerializableNDArray | None = None
52
+
53
+ @field_validator(
54
+ "scf_eigenvalues_a",
55
+ "scf_eigenvalues_b",
56
+ "scf_occupations_a",
57
+ "scf_occupations_b",
58
+ )
59
+ @classmethod
60
+ def to_numpy(cls, val, _info) -> np.ndarray | None:
61
+ return np.asarray(val) if val is not None else None
62
+
63
+
64
+ class CalcInfoData(BaseModel):
65
+ """Mixin for calcinfo attributes.
66
+
67
+ Attributes:
68
+ calcinfo_natoms: The number of atoms as computed by the program.
69
+ calcinfo_nalpha: The number of alpha electrons as computed by the program.
70
+ calcinfo_nbeta: The number of beta electrons as computed by the program.
71
+ calcinfo_nbasis: The number of basis functions in the calculation.
72
+ calcinfo_nmo: The number of molecular orbitals in the calculation.
73
+ """
74
+
75
+ calcinfo_natoms: int | None = None
76
+ calcinfo_nalpha: int | None = None
77
+ calcinfo_nbeta: int | None = None
78
+ calcinfo_nbasis: int | None = None
79
+ calcinfo_nmo: int | None = None
80
+
81
+
82
+ class SinglePointData(Files, CalcInfoData):
83
+ """The computed data from a single point calculation.
84
+
85
+ Attributes:
86
+ energy: The electronic energy of the structure in `Hartrees`.
87
+ gradient: The gradient of the structure in `Hartrees/Bohr`.
88
+ hessian: The hessian of the structure in `Hartrees/Bohr^2`.
89
+ nuclear_repulsion_energy: The nuclear repulsion energy of the structure in
90
+ Hartrees.
91
+
92
+ wavefunction: Wavefunction data from the calculation.
93
+
94
+ freqs_wavenumber: The frequencies of the structure in wavenumbers.
95
+ normal_modes_cartesian: 3D n_vibmodes x n_atoms x 3 array containing
96
+ un-mass-weighted Cartesian displacements of each normal mode in Bohr.
97
+ gibbs_free_energy: Gibbs free energy (i.e. thermochemical analysis) in Hartrees
98
+ of a system where translation / rotation / vibration degrees of freedom are
99
+ approximated using ideal gas / rigid rotor / harmonic oscillator
100
+ respectively.
101
+ scf_dipole_moment: The x, y, z component of the dipole moment of the structure
102
+ in units of e a0 (NOT Debye!).
103
+
104
+ """
105
+
106
+ energy: float | None = None
107
+ gradient: SerializableNDArray | None = None
108
+ hessian: SerializableNDArray | None = None
109
+ nuclear_repulsion_energy: float | None = None
110
+
111
+ wavefunction: Wavefunction | None = None
112
+
113
+ freqs_wavenumber: list[float] = []
114
+ normal_modes_cartesian: SerializableNDArray | None = None
115
+ gibbs_free_energy: float | None = None
116
+
117
+ scf_dipole_moment: list[float] | None = None
118
+
119
+ @field_validator("normal_modes_cartesian")
120
+ @classmethod
121
+ def _validate_normal_modes_cartesian_shape(
122
+ cls, v: SerializableNDArray, info: ValidationInfo
123
+ ):
124
+ if v is not None:
125
+ freqs = info.data.get("freqs_wavenumber")
126
+ n_normal_modes = len(freqs) if freqs else len(v)
127
+ return np.asarray(v).reshape(n_normal_modes, -1, 3)
128
+ return v
129
+
130
+ @field_validator("gradient")
131
+ @classmethod
132
+ def _validate_gradient_shape(cls, v: SerializableNDArray):
133
+ """Validate gradient is n x 3."""
134
+ if v is not None:
135
+ return np.asarray(v).reshape(-1, 3)
136
+
137
+ @field_validator("hessian")
138
+ @classmethod
139
+ def _validate_hessian_shape(cls, v: SerializableNDArray):
140
+ """Validate hessian is square."""
141
+ if v is not None:
142
+ v = np.asarray(v)
143
+ n = int(np.sqrt(v.size))
144
+ return v.reshape((n, n))
145
+
146
+ def return_result(self, calctype: CalcType) -> float | SerializableNDArray:
147
+ """Return the primary result of the calculation."""
148
+ return getattr(self, calctype.value)
149
+
150
+ @model_validator(mode="after")
151
+ def _ensure_results(self) -> Self:
152
+ """Ensure that at least one result is present."""
153
+ if all(
154
+ result is None
155
+ for result in [self.energy, self.gradient, self.hessian]
156
+ ):
157
+ raise ValueError(
158
+ "SinglePointResults requires either an energy, gradient, or hessian "
159
+ "value."
160
+ )
161
+ return self
162
+
163
+
164
+ class OptimizationData(Files, CalcInfoData):
165
+ """Computed data for an optimization (may be for a minimum or transition state).
166
+
167
+ Attributes:
168
+ energies: The energies for each step of the optimization.
169
+ structures: The Structure objects for each step of the optimization.
170
+ final_structure: The final, optimized Structure.
171
+ trajectory: The ProgramOutput objects for each step of the optimization.
172
+ """
173
+
174
+ trajectory: list[
175
+ "ProgramOutput[ProgramInput, SinglePointData] | ProgramOutput[ProgramInput, Files]"
176
+ ] = []
177
+
178
+ @property
179
+ def final_structure(self) -> Structure:
180
+ """The final Structure in the optimization."""
181
+ return self.structures[-1]
182
+
183
+ @property
184
+ def final_molecule(self) -> Structure:
185
+ warnings.warn(
186
+ ".final_molecule is being deprecated and will be removed in a future. "
187
+ "Please use .final_structure instead.",
188
+ category=FutureWarning,
189
+ stacklevel=2,
190
+ )
191
+ return self.final_structure
192
+
193
+ @property
194
+ def final_energy(self) -> float | None:
195
+ """
196
+ The final energy in the optimization. Is `np.nan` if final calculation failed.
197
+ """
198
+ return self.energies[-1]
199
+
200
+ @property
201
+ def energies(self) -> np.ndarray:
202
+ """The energies for each step of the optimization."""
203
+ return np.array(
204
+ [
205
+ output.data.energy if output.success else np.nan # type: ignore
206
+ for output in self.trajectory
207
+ ],
208
+ dtype=float,
209
+ )
210
+
211
+ @property
212
+ def structures(self) -> list[Structure]:
213
+ """The Structure objects for each step of the optimization."""
214
+ return [output.input_data.structure for output in self.trajectory]
215
+
216
+ def return_result(self, calctype: CalcType) -> Structure | None:
217
+ return self.final_structure
218
+
219
+ def __repr_args__(self):
220
+ """Custom repr to avoid printing the entire collection of objects."""
221
+ return [
222
+ ("final_structure", f"{self.final_structure}"),
223
+ ("trajectory", "[...]"),
224
+ ("energies", "[...]"),
225
+ ("structures", "[...]"),
226
+ ]
227
+
228
+ def to_xyz(self) -> str:
229
+ """Return the trajectory as an `xyz` string."""
230
+ return to_multi_xyz(
231
+ prog_output.input_data.structure for prog_output in self.trajectory
232
+ )
233
+
234
+ def save(
235
+ self,
236
+ filepath: Path | str,
237
+ exclude_none: bool = True,
238
+ exclude_unset: bool = True,
239
+ indent: int = 4,
240
+ **kwargs: dict[str, Any],
241
+ ) -> None:
242
+ """Save an OptimizationOutput to a file.
243
+
244
+ Args:
245
+ filepath: The path to save the molecule to.
246
+ exclude_none: If True, attributes with a value of None will not be written
247
+ to the file.
248
+ exclude_unset: If True, attributes that have not been set will not be
249
+ written to the file.
250
+ **kwargs: Additional keyword arguments to pass to the json serializer.
251
+
252
+ Note:
253
+ If the filepath has a `.xyz` extension, the trajectory will be saved to a
254
+ multi-structure `xyz` file.
255
+ """
256
+ filepath = Path(filepath)
257
+ if filepath.suffix == ".xyz":
258
+ filepath.write_text(self.to_xyz())
259
+ return
260
+ super().save(filepath, exclude_none, exclude_unset, indent, **kwargs)
261
+
262
+
263
+ class ConformerSearchData(Files):
264
+ """Data from a conformer search calculation.
265
+
266
+ Conformers and rotamers are sorted by energy.
267
+
268
+ Attributes:
269
+ conformers: The conformers found in the search.
270
+ conformer_energies: The energies for each conformer.
271
+ rotamers: The rotamers found in the search.
272
+ rotamer_energies: The energies for each rotamer.
273
+ """
274
+
275
+ conformers: list[Structure] = []
276
+ conformer_energies: SerializableNDArray = np.array([])
277
+ rotamers: list[Structure] = []
278
+ rotamer_energies: SerializableNDArray = np.array([])
279
+
280
+ @model_validator(mode="after")
281
+ def _energies_size(self) -> Self:
282
+ """Ensure the energies are the same size as the conformers and rotamers."""
283
+ if self.conformer_energies.size > 0 and (
284
+ self.conformer_energies.size != len(self.conformers)
285
+ ):
286
+ raise ValueError(
287
+ "The number of conformer energies must match the number of conformers."
288
+ )
289
+ if self.rotamer_energies.size > 0 and (
290
+ self.rotamer_energies.size != len(self.rotamers)
291
+ ):
292
+ raise ValueError(
293
+ "The number of rotamer energies must match the number of rotamers."
294
+ )
295
+ return self
296
+
297
+ @model_validator(mode="after")
298
+ def _sort_by_energy(self) -> Self:
299
+ """Sort conformers and rotamers by energy."""
300
+ if self.conformer_energies.size > 0:
301
+ sorted_indices = np.argsort(self.conformer_energies)
302
+ self.conformers[:] = [self.conformers[i] for i in sorted_indices]
303
+ self.conformer_energies[:] = self.conformer_energies[sorted_indices]
304
+
305
+ if self.rotamer_energies.size > 0:
306
+ sorted_indices = np.argsort(self.rotamer_energies)
307
+ self.rotamers[:] = [self.rotamers[i] for i in sorted_indices]
308
+ self.rotamer_energies[:] = self.rotamer_energies[sorted_indices]
309
+
310
+ return self
311
+
312
+ @property
313
+ def conformer_energies_relative(self) -> np.ndarray:
314
+ """The relative energies for each conformer in the search."""
315
+ if self.conformer_energies.size == 0:
316
+ return np.array([])
317
+ return self.conformer_energies - self.conformer_energies.min()
318
+
319
+ @property
320
+ def rotamer_energies_relative(self) -> np.ndarray:
321
+ """The relative energies for each rotamer in the search."""
322
+ if self.rotamer_energies.size == 0:
323
+ return np.array([])
324
+ return self.rotamer_energies - self.rotamer_energies.min()
325
+
326
+ def conformers_filtered(
327
+ self,
328
+ threshold: float = 1.0,
329
+ **rmsd_kwargs,
330
+ ) -> tuple[list[Structure], SerializableNDArray]:
331
+ """
332
+ !!! warning "Moved since *qcdata* 0.15.0"
333
+ This convenience method has moved to
334
+ [`qcinf.filter_conformers`][qcinf.filter_conformers]
335
+ and this stub will be **removed** from *qcdata* in a future release.
336
+
337
+ ```python
338
+ from qcinf import filter_conformers
339
+
340
+ filtered_csr = filter_conformers(
341
+ conformers=csr
342
+ threshold=1.0, # Bohr
343
+ backend="qcinf", # or "rdkit",
344
+ **rmsd_kwargs,
345
+ )
346
+ ```
347
+ """
348
+ warnings.warn(
349
+ "`ConformerSearchResults.conformers_filtered()` is deprecated. "
350
+ "Install *qcinf* and use `qcinf.filter_conformers` instead.",
351
+ DeprecationWarning,
352
+ stacklevel=2,
353
+ )
354
+ raise NotImplementedError(
355
+ "Method removed. Replace with:\n\n"
356
+ " from qcinf import filter_conformers\n\n"
357
+ " filtered_csr = filter_conformers(\n"
358
+ " prog_output.results,\n"
359
+ " threshold=1.0,\n"
360
+ " backend='qcinf',\n"
361
+ " **rmsd_kwargs\n"
362
+ " )"
363
+ )
364
+
365
+
366
+ StructuredData = Union[SinglePointData, OptimizationData, ConformerSearchData]
367
+ StructuredDataType = TypeVar("StructuredDataType", bound=StructuredData)
368
+ Data = Union[Files, StructuredData]
369
+ DataType = TypeVar("DataType", bound=Data)
370
+
371
+
372
+ @deprecated_class("SinglePointData")
373
+ class SinglePointResults(SinglePointData):
374
+ """This class is deprecated and will be removed in a future release. Please use
375
+ `SinglePointData` instead."""
376
+
377
+
378
+ @deprecated_class("OptimizationData")
379
+ class OptimizationResults(OptimizationData):
380
+ """This class is deprecated and will be removed in a future release. Please use
381
+ `OptimizationData` instead."""
382
+
383
+
384
+ @deprecated_class("ConformerSearchData")
385
+ class ConformerSearchResults(ConformerSearchData):
386
+ """This class is deprecated and will be removed in a future release. Please use
387
+ `ConformerSearchData` instead."""
@@ -0,0 +1,224 @@
1
+ """Input models for quantum chemistry calculations."""
2
+
3
+ import warnings
4
+ from pathlib import Path
5
+ from typing import Any, TypeVar, Union
6
+
7
+ from pydantic import BaseModel, field_serializer, model_validator
8
+ from typing_extensions import Self
9
+
10
+ from .base_models import CalcType, Files, Model
11
+ from .structure import Structure
12
+
13
+ __all__ = [
14
+ "FileInput",
15
+ "ProgramInput",
16
+ "DualProgramInput",
17
+ "ProgramArgs",
18
+ "ProgramArgsSub",
19
+ "Inputs",
20
+ "InputType",
21
+ "StructuredInputs",
22
+ ]
23
+
24
+
25
+ class FileInput(Files):
26
+ """File and command line argument inputs for a calculation.
27
+
28
+ Attributes:
29
+ files Files: A dict mapping filename to str or bytes data.
30
+ cmdline_args: A list of command line arguments to be passed to the program.
31
+ extras Dict[str, Any]: Additional information to bundle with the object. Use for
32
+ schema development and scratch space.
33
+ """
34
+
35
+ cmdline_args: list[str] = []
36
+
37
+ @classmethod
38
+ def from_directory(cls, directory: Path | str, **kwargs) -> Self:
39
+ """Create a new FileInput and collect all files in the directory."""
40
+ obj = cls(**kwargs)
41
+ directory = Path(directory)
42
+ obj.add_files(directory)
43
+ return obj
44
+
45
+
46
+ class _KeywordsMixin(BaseModel):
47
+ """Mixin for keywords attribute.
48
+
49
+ Attributes:
50
+ Keywords: dict[str, Any]: A dict of keywords to be passed to the program
51
+ excluding model and calctype. Defaults to an empty dict.
52
+ """
53
+
54
+ keywords: dict[str, Any] = {}
55
+
56
+
57
+ class _StructureKeywordsMixin(_KeywordsMixin):
58
+ """
59
+ Attributes:
60
+ structure: The structure to be used in the calculation.
61
+ """
62
+
63
+ structure: Structure
64
+
65
+ def __init__(self, **data: Any):
66
+ """Backwards compatibility for 'molecule' attribute."""
67
+
68
+ # TODO: Remove in future versions.
69
+ if "molecule" in data:
70
+ warnings.warn(
71
+ "Use of 'molecule' attribute is deprecated. Use 'structure' instead.",
72
+ FutureWarning,
73
+ stacklevel=2,
74
+ )
75
+ data["structure"] = data.pop("molecule")
76
+ super().__init__(**data)
77
+
78
+ @property
79
+ def molecule(self) -> Structure:
80
+ """Backwards compatibility for 'molecule' attribute."""
81
+ warnings.warn(
82
+ "Use of 'molecule' attribute is deprecated. Use 'structure' instead.",
83
+ FutureWarning,
84
+ stacklevel=2,
85
+ )
86
+ return self.structure
87
+
88
+
89
+ class ProgramArgs(FileInput, _KeywordsMixin):
90
+ """Core arguments for a calculation without a calctype or structure.
91
+
92
+ This class is used by `DualProgramInput` or multi-step calculations to
93
+ specify `subprogram_args` or basic program arguments for a multistep algorithm in
94
+ BigChem. It is not intended to be used directly for single-step calculations since
95
+ it lacks a `calctype` and `structure`.
96
+
97
+
98
+ Attributes:
99
+ model Model: The model for the quantum chemistry calculation.
100
+ keywords Dict[str, Any]: A dict of keywords to be passed to the program
101
+ excluding model and calctype. Defaults to an empty dict.
102
+ files Files: Files to be passed to the QC program.
103
+ extras Dict[str, Any]: Additional information to bundle with the object. Use
104
+ for schema development and scratch space.
105
+ """
106
+
107
+ model: Model
108
+
109
+
110
+ class ProgramArgsSub(FileInput, _KeywordsMixin):
111
+ """Generic arguments for a calculation that also calls a sub-calculation.
112
+
113
+ This class is needed for multi-step calculations where the calctype and structure
114
+ are specified only once for the entire calculation, e.g., multistep_opt in BigChem.
115
+
116
+ Attributes:
117
+ model: The model for the quantum chemistry calculation
118
+ keywords: A dict of keywords to be passed to the program excluding model and
119
+ calctype. Defaults to an empty dict.
120
+ files: Files to be passed to the QC program.
121
+ subprogram: The name of the subprogram to use.
122
+ subprogram_args: The ProgramArgs for the subprogram.
123
+ extras: Additional information to bundle with the object. Use for schema
124
+ development and scratch space.
125
+ """
126
+
127
+ model: Model | None = None
128
+ subprogram: str
129
+ subprogram_args: ProgramArgs
130
+
131
+ @model_validator(mode="before")
132
+ @classmethod
133
+ def _backcompat(cls, payload: dict[str, Any]) -> dict[str, Any]:
134
+ """Backwards compatibility for 'subprogram_args' attribute."""
135
+ if (
136
+ isinstance(payload, dict)
137
+ and "subprogram_args" in payload
138
+ and "subprogram_args" not in payload
139
+ ):
140
+ payload = dict(payload)
141
+ payload["subprogram_args"] = payload.pop("subprogram_args")
142
+ warnings.warn(
143
+ "'subprogram_args' has been renamed to 'subprogram_args' (ProgramArgs).",
144
+ FutureWarning,
145
+ stacklevel=2,
146
+ )
147
+ return payload
148
+
149
+
150
+ class ProgramInput(ProgramArgs, _StructureKeywordsMixin):
151
+ """Core input for a quantum chemistry calculation. This is the most common input type.
152
+
153
+ Attributes:
154
+ calctype CalcType: The type of calculation to perform.
155
+ model Model: The model for the quantum chemistry calculation.
156
+ keywords Dict[str, Any]: A dict of keywords to be passed to the program
157
+ excluding model and calctype. Defaults to an empty dict.
158
+ structure Structure: The structure to be used in the calculation.
159
+ files Files: Files to be passed to the QC program.
160
+ extras Dict[str, Any]: Additional information to bundle with the object. Use
161
+ for schema development and scratch space.
162
+
163
+ Example:
164
+ ```python
165
+ from qcdata.models import ProgramInput, Structure
166
+
167
+ struct = Structure.open("path/to/structure.xyz")
168
+
169
+ prog_inp = ProgramInput(
170
+ calctype = "energy",
171
+ structure = struct,
172
+ model = {"method": "hf", "basis": "6-31G"},
173
+ keywords = {"maxsteps": "250"}, # Optional
174
+ files = {"file1": b"binary data"} # Optional
175
+ )
176
+ ```
177
+ """
178
+
179
+ calctype: CalcType
180
+
181
+ @field_serializer("calctype")
182
+ def _serialize_calctype(self, calctype: CalcType, _info) -> str:
183
+ """Serialize CalcType to string"""
184
+ return calctype.value
185
+
186
+
187
+ class DualProgramInput(ProgramArgsSub, ProgramInput):
188
+ """Input for a two program calculation.
189
+
190
+ Attributes:
191
+ calctype CalcType: The type of calculation to perform.
192
+ model Model: The model for the quantum chemistry calculation.
193
+ keywords Dict[str, Any]: A dict of keywords to be passed to the program
194
+ excluding model and calctype. Defaults to an empty dict.
195
+ structure Structure: The structure to be used in the calculation.
196
+ files Files: Files to be passed to the QC program.
197
+ subprogram: The name of the subprogram to use.
198
+ subprogram_args ProgramArgs: The ProgramArgs for the subprogram.
199
+ extras Dict[str, Any]: Additional information to bundle with the object. Use
200
+ for schema development and scratch space.
201
+
202
+ Example:
203
+ ```python
204
+ from qcdata.models import DualProgramInput, ProgramArgs, Structure
205
+
206
+ struct = Structure.open("path/to/structure.xyz")
207
+
208
+ prog_inp = DualProgramInput(
209
+ calctype = "optimization",
210
+ structure = struct,
211
+ keywords = {"maxiter": "250"}, # Optional
212
+ subprogram = "orca",
213
+ subprogram_args = ProgramArgs(
214
+ model = {"method": "wb97x-d3", "basis": "def2-SVP"},
215
+ keywords = {"convthre": "1e-6"}, # Optional
216
+ )
217
+ )
218
+ ```
219
+ """
220
+
221
+
222
+ Inputs = Union[FileInput, ProgramInput, DualProgramInput]
223
+ InputType = TypeVar("InputType", bound=Inputs)
224
+ StructuredInputs = Union[ProgramInput, DualProgramInput]