stjames 0.0.42__tar.gz → 0.0.44__tar.gz
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.
Potentially problematic release.
This version of stjames might be problematic. Click here for more details.
- {stjames-0.0.42/stjames.egg-info → stjames-0.0.44}/PKG-INFO +1 -1
- {stjames-0.0.42 → stjames-0.0.44}/pyproject.toml +1 -1
- {stjames-0.0.42 → stjames-0.0.44}/stjames/atom.py +1 -1
- {stjames-0.0.42 → stjames-0.0.44}/stjames/molecule.py +89 -2
- stjames-0.0.44/stjames/settings.py +216 -0
- {stjames-0.0.42 → stjames-0.0.44}/stjames/workflows/bde.py +14 -25
- {stjames-0.0.42 → stjames-0.0.44}/stjames/workflows/molecular_dynamics.py +2 -0
- {stjames-0.0.42 → stjames-0.0.44}/stjames/workflows/multistage_opt.py +10 -4
- {stjames-0.0.42 → stjames-0.0.44}/stjames/workflows/spin_states.py +1 -13
- {stjames-0.0.42 → stjames-0.0.44}/stjames/workflows/workflow.py +18 -22
- {stjames-0.0.42 → stjames-0.0.44/stjames.egg-info}/PKG-INFO +1 -1
- {stjames-0.0.42 → stjames-0.0.44}/stjames.egg-info/SOURCES.txt +3 -1
- stjames-0.0.44/tests/test_from_extxyz.py +231 -0
- stjames-0.0.44/tests/test_settings.py +34 -0
- stjames-0.0.42/stjames/settings.py +0 -209
- {stjames-0.0.42 → stjames-0.0.44}/LICENSE +0 -0
- {stjames-0.0.42 → stjames-0.0.44}/README.md +0 -0
- {stjames-0.0.42 → stjames-0.0.44}/setup.cfg +0 -0
- {stjames-0.0.42 → stjames-0.0.44}/stjames/__init__.py +0 -0
- {stjames-0.0.42 → stjames-0.0.44}/stjames/_deprecated_solvent_settings.py +0 -0
- {stjames-0.0.42 → stjames-0.0.44}/stjames/base.py +0 -0
- {stjames-0.0.42 → stjames-0.0.44}/stjames/basis_set.py +0 -0
- {stjames-0.0.42 → stjames-0.0.44}/stjames/calculation.py +0 -0
- {stjames-0.0.42 → stjames-0.0.44}/stjames/constraint.py +0 -0
- {stjames-0.0.42 → stjames-0.0.44}/stjames/correction.py +0 -0
- {stjames-0.0.42 → stjames-0.0.44}/stjames/data/__init__.py +0 -0
- {stjames-0.0.42 → stjames-0.0.44}/stjames/data/bragg_radii.json +0 -0
- {stjames-0.0.42 → stjames-0.0.44}/stjames/data/elements.py +0 -0
- {stjames-0.0.42 → stjames-0.0.44}/stjames/data/isotopes.json +0 -0
- {stjames-0.0.42 → stjames-0.0.44}/stjames/data/nist_isotopes.json +0 -0
- {stjames-0.0.42 → stjames-0.0.44}/stjames/data/read_nist_isotopes.py +0 -0
- {stjames-0.0.42 → stjames-0.0.44}/stjames/data/symbol_element.json +0 -0
- {stjames-0.0.42 → stjames-0.0.44}/stjames/diis_settings.py +0 -0
- {stjames-0.0.42 → stjames-0.0.44}/stjames/grid_settings.py +0 -0
- {stjames-0.0.42 → stjames-0.0.44}/stjames/int_settings.py +0 -0
- {stjames-0.0.42 → stjames-0.0.44}/stjames/message.py +0 -0
- {stjames-0.0.42 → stjames-0.0.44}/stjames/method.py +0 -0
- {stjames-0.0.42 → stjames-0.0.44}/stjames/mode.py +0 -0
- {stjames-0.0.42 → stjames-0.0.44}/stjames/opt_settings.py +0 -0
- {stjames-0.0.42 → stjames-0.0.44}/stjames/periodic_cell.py +0 -0
- {stjames-0.0.42 → stjames-0.0.44}/stjames/py.typed +0 -0
- {stjames-0.0.42 → stjames-0.0.44}/stjames/scf_settings.py +0 -0
- {stjames-0.0.42 → stjames-0.0.44}/stjames/solvent.py +0 -0
- {stjames-0.0.42 → stjames-0.0.44}/stjames/status.py +0 -0
- {stjames-0.0.42 → stjames-0.0.44}/stjames/task.py +0 -0
- {stjames-0.0.42 → stjames-0.0.44}/stjames/thermochem_settings.py +0 -0
- {stjames-0.0.42 → stjames-0.0.44}/stjames/types.py +0 -0
- {stjames-0.0.42 → stjames-0.0.44}/stjames/workflows/__init__.py +0 -0
- {stjames-0.0.42 → stjames-0.0.44}/stjames/workflows/admet.py +0 -0
- {stjames-0.0.42 → stjames-0.0.44}/stjames/workflows/basic_calculation.py +0 -0
- {stjames-0.0.42 → stjames-0.0.44}/stjames/workflows/conformer.py +0 -0
- {stjames-0.0.42 → stjames-0.0.44}/stjames/workflows/descriptors.py +0 -0
- {stjames-0.0.42 → stjames-0.0.44}/stjames/workflows/fukui.py +0 -0
- {stjames-0.0.42 → stjames-0.0.44}/stjames/workflows/pka.py +0 -0
- {stjames-0.0.42 → stjames-0.0.44}/stjames/workflows/redox_potential.py +0 -0
- {stjames-0.0.42 → stjames-0.0.44}/stjames/workflows/scan.py +0 -0
- {stjames-0.0.42 → stjames-0.0.44}/stjames/workflows/tautomer.py +0 -0
- {stjames-0.0.42 → stjames-0.0.44}/stjames.egg-info/dependency_links.txt +0 -0
- {stjames-0.0.42 → stjames-0.0.44}/stjames.egg-info/requires.txt +0 -0
- {stjames-0.0.42 → stjames-0.0.44}/stjames.egg-info/top_level.txt +0 -0
- {stjames-0.0.42 → stjames-0.0.44}/tests/test_molecule.py +0 -0
|
@@ -60,7 +60,7 @@ class Atom(Base):
|
|
|
60
60
|
Atom(1, [0.00000, 0.00000, 0.00000])
|
|
61
61
|
"""
|
|
62
62
|
name, *xyz = xyz_line.split()
|
|
63
|
-
symbol = int(name) if name.isdigit() else SYMBOL_ELEMENT[name]
|
|
63
|
+
symbol = int(name) if name.isdigit() else SYMBOL_ELEMENT[name.title()]
|
|
64
64
|
if not len(xyz) == 3:
|
|
65
65
|
raise ValueError("XYZ file should have 3 coordinates per atom")
|
|
66
66
|
return cls(atomic_number=symbol, position=xyz)
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import re
|
|
1
2
|
from pathlib import Path
|
|
2
3
|
from typing import Iterable, Optional, Self
|
|
3
4
|
|
|
4
5
|
import pydantic
|
|
5
|
-
from pydantic import NonNegativeInt, PositiveInt
|
|
6
|
+
from pydantic import NonNegativeInt, PositiveInt, ValidationError
|
|
6
7
|
|
|
7
8
|
from .atom import Atom
|
|
8
9
|
from .base import Base
|
|
@@ -54,6 +55,8 @@ class Molecule(Base):
|
|
|
54
55
|
thermal_enthalpy_corr: Optional[float] = None
|
|
55
56
|
thermal_free_energy_corr: Optional[float] = None
|
|
56
57
|
|
|
58
|
+
smiles: Optional[str] = None
|
|
59
|
+
|
|
57
60
|
def __len__(self) -> int:
|
|
58
61
|
return len(self.atoms)
|
|
59
62
|
|
|
@@ -135,6 +138,8 @@ class Molecule(Base):
|
|
|
135
138
|
match format:
|
|
136
139
|
case "xyz":
|
|
137
140
|
return cls.from_xyz_lines(f.readlines(), charge=charge, multiplicity=multiplicity)
|
|
141
|
+
case "extxyz":
|
|
142
|
+
return cls.from_extxyz_lines(f.readlines(), charge=charge, multiplicity=multiplicity)
|
|
138
143
|
case _:
|
|
139
144
|
raise ValueError(f"Unsupported {format=}")
|
|
140
145
|
|
|
@@ -161,7 +166,7 @@ class Molecule(Base):
|
|
|
161
166
|
|
|
162
167
|
try:
|
|
163
168
|
return cls(atoms=[Atom.from_xyz(line) for line in lines], charge=charge, multiplicity=multiplicity)
|
|
164
|
-
except
|
|
169
|
+
except (ValueError, ValidationError) as e:
|
|
165
170
|
raise MoleculeReadError("Error reading molecule from xyz") from e
|
|
166
171
|
|
|
167
172
|
def to_xyz(self, comment: str = "", out_file: Path | str | None = None) -> str:
|
|
@@ -190,3 +195,85 @@ class Molecule(Base):
|
|
|
190
195
|
f.write(out)
|
|
191
196
|
|
|
192
197
|
return out
|
|
198
|
+
|
|
199
|
+
@classmethod
|
|
200
|
+
def from_extxyz(cls: type[Self], extxyz: str, charge: int = 0, multiplicity: PositiveInt = 1) -> Self:
|
|
201
|
+
r"""
|
|
202
|
+
Generate a Molecule from a EXTXYZ string. Currently only supporting Lattice and Properties fields.
|
|
203
|
+
|
|
204
|
+
>>> Molecule.from_extxyz('''
|
|
205
|
+
... 2
|
|
206
|
+
... Lattice="6.0 0.0 0.0 6.0 0.0 0.0 6.0 0.0 0.0"Properties=species:S:1:pos:R:3
|
|
207
|
+
... H 0 0 0
|
|
208
|
+
... H 0 0 1
|
|
209
|
+
... ''').cell.lattice_vectors
|
|
210
|
+
((6.0, 0.0, 0.0), (6.0, 0.0, 0.0), (6.0, 0.0, 0.0))
|
|
211
|
+
"""
|
|
212
|
+
|
|
213
|
+
return cls.from_extxyz_lines(extxyz.strip().splitlines(), charge=charge, multiplicity=multiplicity)
|
|
214
|
+
|
|
215
|
+
@classmethod
|
|
216
|
+
def from_extxyz_lines(cls: type[Self], lines: Iterable[str], charge: int = 0, multiplicity: PositiveInt = 1) -> Self:
|
|
217
|
+
# ensure first line is number of atoms
|
|
218
|
+
lines = list(lines)
|
|
219
|
+
if len(lines[0].split()) == 1:
|
|
220
|
+
natoms = lines[0].strip()
|
|
221
|
+
if not natoms.isdigit() or (int(lines[0]) != len(lines) - 2):
|
|
222
|
+
raise MoleculeReadError(f"First line of EXTXYZ file should be the number of atoms, got: {lines[0]} != {len(lines) - 2}")
|
|
223
|
+
lines = lines[1:]
|
|
224
|
+
else:
|
|
225
|
+
raise MoleculeReadError(f"First line of EXTXYZ should be only an int denoting number of atoms. Got {lines[0].split()}")
|
|
226
|
+
|
|
227
|
+
# ensure second line contains key-value pairs
|
|
228
|
+
if "=" not in lines[0]:
|
|
229
|
+
raise MoleculeReadError(f"Invalid property line, got {lines[0]}")
|
|
230
|
+
|
|
231
|
+
cell = parse_comment_line(lines[0])
|
|
232
|
+
lines = lines[1:]
|
|
233
|
+
|
|
234
|
+
try:
|
|
235
|
+
return cls(atoms=[Atom.from_xyz(line) for line in lines], cell=cell, charge=charge, multiplicity=multiplicity)
|
|
236
|
+
except (ValueError, ValidationError) as e:
|
|
237
|
+
raise MoleculeReadError("Error reading molecule from extxyz") from e
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def parse_comment_line(line: str) -> PeriodicCell:
|
|
241
|
+
"""
|
|
242
|
+
currently only supporting lattice and porperites fields from comment line
|
|
243
|
+
modify in future to support other fields from comment from_xyz_lines
|
|
244
|
+
ex: name, mulitplicity, charge, etc.
|
|
245
|
+
"""
|
|
246
|
+
cell = None
|
|
247
|
+
# Regular expression to match key="value", key='value', or key=value
|
|
248
|
+
pattern = r"(\S+?=(?:\".*?\"|\'.*?\'|\S+))"
|
|
249
|
+
pairs = re.findall(pattern, line)
|
|
250
|
+
|
|
251
|
+
prop_dict = {}
|
|
252
|
+
for pair in pairs:
|
|
253
|
+
key, value = pair.split("=", 1)
|
|
254
|
+
if key.lower() == "lattice":
|
|
255
|
+
value = value.strip("'\"").split()
|
|
256
|
+
if len(value) != 9:
|
|
257
|
+
raise MoleculeReadError(f"Lattice should have 9 entries got {len(value)}")
|
|
258
|
+
|
|
259
|
+
# Convert the value to a 3x3 tuple of tuples of floats
|
|
260
|
+
try:
|
|
261
|
+
cell = tuple(tuple(map(float, value[i : i + 3])) for i in range(0, 9, 3))
|
|
262
|
+
except ValueError:
|
|
263
|
+
raise MoleculeReadError(f"Lattice should be floats, got {value}")
|
|
264
|
+
|
|
265
|
+
prop_dict[key] = value
|
|
266
|
+
|
|
267
|
+
elif key.lower() == "properties":
|
|
268
|
+
if value.lower() != "species:s:1:pos:r:3":
|
|
269
|
+
raise MoleculeReadError(f"Only accepting properties of form species:S:1:pos:R:3, got {value}")
|
|
270
|
+
prop_dict[key] = value
|
|
271
|
+
else:
|
|
272
|
+
raise MoleculeReadError(f"Currently only accepting lattice and propery keys. Got {key}")
|
|
273
|
+
|
|
274
|
+
if cell is None:
|
|
275
|
+
raise MoleculeReadError("Lattice field is required but missing.")
|
|
276
|
+
|
|
277
|
+
if "properties" not in [key.lower() for key in prop_dict.keys()]:
|
|
278
|
+
raise MoleculeReadError(f"Property field is required, got keys {prop_dict.keys()}")
|
|
279
|
+
return PeriodicCell(lattice_vectors=cell)
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
from typing import Any, Optional, Self, TypeVar
|
|
2
|
+
|
|
3
|
+
from pydantic import computed_field, field_validator, model_validator
|
|
4
|
+
|
|
5
|
+
from .base import Base, UniqueList
|
|
6
|
+
from .basis_set import BasisSet
|
|
7
|
+
from .correction import Correction
|
|
8
|
+
from .method import METHODS_WITH_CORRECTION, PREPACKAGED_METHODS, Method
|
|
9
|
+
from .mode import Mode
|
|
10
|
+
from .opt_settings import OptimizationSettings
|
|
11
|
+
from .scf_settings import SCFSettings
|
|
12
|
+
from .solvent import SolventSettings
|
|
13
|
+
from .task import Task
|
|
14
|
+
from .thermochem_settings import ThermochemistrySettings
|
|
15
|
+
|
|
16
|
+
_T = TypeVar("_T")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Settings(Base):
|
|
20
|
+
mode: Mode = Mode.AUTO
|
|
21
|
+
|
|
22
|
+
method: Method = Method.HARTREE_FOCK
|
|
23
|
+
basis_set: Optional[BasisSet] = None
|
|
24
|
+
tasks: UniqueList[Task] = [Task.ENERGY, Task.CHARGE, Task.DIPOLE]
|
|
25
|
+
corrections: UniqueList[Correction] = []
|
|
26
|
+
|
|
27
|
+
solvent_settings: Optional[SolventSettings] = None
|
|
28
|
+
|
|
29
|
+
# scf/opt settings will be set automatically based on mode, but can be overridden manually
|
|
30
|
+
scf_settings: SCFSettings = SCFSettings()
|
|
31
|
+
opt_settings: OptimizationSettings = OptimizationSettings()
|
|
32
|
+
thermochem_settings: ThermochemistrySettings = ThermochemistrySettings()
|
|
33
|
+
|
|
34
|
+
# mypy has this dead wrong (https://docs.pydantic.dev/2.0/usage/computed_fields/)
|
|
35
|
+
# Python 3.12 narrows the reason for the ignore to prop-decorator
|
|
36
|
+
@computed_field # type: ignore[misc, prop-decorator, unused-ignore]
|
|
37
|
+
@property
|
|
38
|
+
def level_of_theory(self) -> str:
|
|
39
|
+
corrections = list(filter(lambda x: x not in (None, ""), self.corrections))
|
|
40
|
+
|
|
41
|
+
if self.method in PREPACKAGED_METHODS or self.basis_set is None:
|
|
42
|
+
method = self.method.value
|
|
43
|
+
elif self.method in METHODS_WITH_CORRECTION or len(corrections) == 0:
|
|
44
|
+
method = f"{self.method.value}/{self.basis_set.name.lower()}"
|
|
45
|
+
else:
|
|
46
|
+
method = f"{self.method.value}-{'-'.join([c.value for c in corrections])}/{self.basis_set.name.lower()}"
|
|
47
|
+
|
|
48
|
+
if self.solvent_settings is not None:
|
|
49
|
+
method += f"/{self.solvent_settings.model.value}({self.solvent_settings.solvent.value})"
|
|
50
|
+
|
|
51
|
+
return method
|
|
52
|
+
|
|
53
|
+
@field_validator("mode")
|
|
54
|
+
@classmethod
|
|
55
|
+
def set_mode_auto(cls, mode: Mode) -> Mode:
|
|
56
|
+
"""Set the mode to RAPID if AUTO is selected."""
|
|
57
|
+
if mode == Mode.AUTO:
|
|
58
|
+
return Mode.RAPID
|
|
59
|
+
|
|
60
|
+
return mode
|
|
61
|
+
|
|
62
|
+
@model_validator(mode="after")
|
|
63
|
+
def validate_and_build(self) -> Self:
|
|
64
|
+
if self.mode == Mode.AUTO:
|
|
65
|
+
self.mode = Mode.RAPID
|
|
66
|
+
|
|
67
|
+
self.scf_settings = _assign_scf_settings_by_mode(self.mode, self.scf_settings)
|
|
68
|
+
self.opt_settings = _assign_opt_settings_by_mode(self.mode, self.opt_settings)
|
|
69
|
+
|
|
70
|
+
return self
|
|
71
|
+
|
|
72
|
+
def model_post_init(self, __context: Any) -> None:
|
|
73
|
+
# figure out `optimize_ts`
|
|
74
|
+
if Task.OPTIMIZE_TS in self.tasks:
|
|
75
|
+
self.tasks.pop(self.tasks.index(Task.OPTIMIZE_TS))
|
|
76
|
+
self.tasks.append(Task.OPTIMIZE)
|
|
77
|
+
self.opt_settings.transition_state = True
|
|
78
|
+
|
|
79
|
+
# composite methods have their own basis sets, so overwrite user stuff
|
|
80
|
+
if self.method == Method.HF3C:
|
|
81
|
+
self.basis_set = BasisSet(name="minix")
|
|
82
|
+
elif self.method == Method.B973C:
|
|
83
|
+
self.basis_set = BasisSet(name="def2-mTZVP")
|
|
84
|
+
elif self.method == Method.R2SCAN3C:
|
|
85
|
+
self.basis_set = BasisSet(name="def2-mTZVPP")
|
|
86
|
+
elif self.method == Method.WB97X3C:
|
|
87
|
+
self.basis_set = BasisSet(name="vDZP")
|
|
88
|
+
|
|
89
|
+
@field_validator("basis_set", mode="before")
|
|
90
|
+
@classmethod
|
|
91
|
+
def parse_basis_set(cls, v: Any) -> BasisSet | dict[str, Any] | None:
|
|
92
|
+
"""Turn a string into a ``BasisSet`` object. (This is a little crude.)"""
|
|
93
|
+
if isinstance(v, BasisSet):
|
|
94
|
+
return None if v.name is None else v
|
|
95
|
+
elif isinstance(v, dict):
|
|
96
|
+
return None if v.get("name") is None else v
|
|
97
|
+
elif isinstance(v, str):
|
|
98
|
+
if len(v):
|
|
99
|
+
return BasisSet(name=v)
|
|
100
|
+
# "" is basically None, let's be real here...
|
|
101
|
+
return None
|
|
102
|
+
elif v is None:
|
|
103
|
+
return None
|
|
104
|
+
else:
|
|
105
|
+
raise ValueError(f"invalid value ``{v}`` for ``basis_set``")
|
|
106
|
+
|
|
107
|
+
@field_validator("corrections", mode="before")
|
|
108
|
+
@classmethod
|
|
109
|
+
def remove_empty_string(cls, v: list[_T]) -> list[_T]:
|
|
110
|
+
"""Remove empty string values."""
|
|
111
|
+
return [c for c in v if c] if v is not None else v
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _assign_scf_settings_by_mode(mode: Mode, scf_settings: SCFSettings) -> SCFSettings:
|
|
115
|
+
"""
|
|
116
|
+
Assign SCF settings based on the mode.
|
|
117
|
+
|
|
118
|
+
Values based off of the following sources:
|
|
119
|
+
QChem:
|
|
120
|
+
- https://manual.q-chem.com/5.2/Ch4.S3.SS2.html
|
|
121
|
+
- https://manual.q-chem.com/5.2/Ch4.S5.SS2.html
|
|
122
|
+
|
|
123
|
+
Gaussian:
|
|
124
|
+
- https://gaussian.com/integral/
|
|
125
|
+
- https://gaussian.com/overlay5/
|
|
126
|
+
|
|
127
|
+
Orca:
|
|
128
|
+
- manual 4.2.1, §9.6.1 and §9.7.3
|
|
129
|
+
|
|
130
|
+
Psi4:
|
|
131
|
+
- https://psicode.org/psi4manual/master/autodir_options_c/module__scf.html
|
|
132
|
+
- https://psicode.org/psi4manual/master/autodoc_glossary_options_c.html
|
|
133
|
+
|
|
134
|
+
TeraChem:
|
|
135
|
+
- Manual, it's easy to locate everything.
|
|
136
|
+
|
|
137
|
+
The below values are my best attempt at homogenizing various sources.
|
|
138
|
+
In general, eri_threshold should be 3 OOM lower than SCF convergence.
|
|
139
|
+
"""
|
|
140
|
+
if mode == Mode.MANUAL:
|
|
141
|
+
return scf_settings
|
|
142
|
+
|
|
143
|
+
match mode:
|
|
144
|
+
case Mode.RECKLESS:
|
|
145
|
+
scf_settings.energy_threshold = 1e-5
|
|
146
|
+
scf_settings.rms_error_threshold = 1e-7
|
|
147
|
+
scf_settings.max_error_threshold = 1e-5
|
|
148
|
+
scf_settings.rebuild_frequency = 100
|
|
149
|
+
scf_settings.int_settings.eri_threshold = 1e-8
|
|
150
|
+
scf_settings.int_settings.csam_multiplier = 3.0
|
|
151
|
+
scf_settings.int_settings.pair_overlap_threshold = 1e-8
|
|
152
|
+
case Mode.RAPID | Mode.CAREFUL:
|
|
153
|
+
scf_settings.energy_threshold = 1e-6
|
|
154
|
+
scf_settings.rms_error_threshold = 1e-9
|
|
155
|
+
scf_settings.max_error_threshold = 1e-7
|
|
156
|
+
scf_settings.rebuild_frequency = 10
|
|
157
|
+
scf_settings.int_settings.eri_threshold = 1e-10
|
|
158
|
+
scf_settings.int_settings.csam_multiplier = 1.0
|
|
159
|
+
scf_settings.int_settings.pair_overlap_threshold = 1e-10
|
|
160
|
+
case Mode.METICULOUS:
|
|
161
|
+
scf_settings.energy_threshold = 1e-8
|
|
162
|
+
scf_settings.rms_error_threshold = 1e-9
|
|
163
|
+
scf_settings.max_error_threshold = 1e-7
|
|
164
|
+
scf_settings.rebuild_frequency = 5
|
|
165
|
+
scf_settings.int_settings.eri_threshold = 1e-12
|
|
166
|
+
scf_settings.int_settings.csam_multiplier = 1.0
|
|
167
|
+
scf_settings.int_settings.pair_overlap_threshold = 1e-12
|
|
168
|
+
case Mode.DEBUG:
|
|
169
|
+
scf_settings.energy_threshold = 1e-9
|
|
170
|
+
scf_settings.rms_error_threshold = 1e-10
|
|
171
|
+
scf_settings.max_error_threshold = 1e-9
|
|
172
|
+
scf_settings.rebuild_frequency = 1
|
|
173
|
+
scf_settings.int_settings.eri_threshold = 1e-14
|
|
174
|
+
scf_settings.int_settings.csam_multiplier = 1e10 # in other words, disable CSAM
|
|
175
|
+
scf_settings.int_settings.pair_overlap_threshold = 1e-14
|
|
176
|
+
case _:
|
|
177
|
+
raise ValueError(f"Unknown mode ``{mode.value}``!")
|
|
178
|
+
|
|
179
|
+
return scf_settings
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _assign_opt_settings_by_mode(mode: Mode, opt_settings: OptimizationSettings) -> OptimizationSettings:
|
|
183
|
+
"""
|
|
184
|
+
Assign optimization settings based on the mode.
|
|
185
|
+
|
|
186
|
+
Constraints lead to a lot of noise, so we need to loosen the thresholds.
|
|
187
|
+
|
|
188
|
+
cf. DLFIND manual, and https://www.cup.uni-muenchen.de/ch/compchem/geom/basic.html
|
|
189
|
+
and the discussion at https://geometric.readthedocs.io/en/latest/how-it-works.html
|
|
190
|
+
in periodic systems, "normal" is 0.05 eV/Å ~= 2e-3 Hartree/Å, and "careful" is 0.01 ~= 4e-4
|
|
191
|
+
|
|
192
|
+
Note: thresholds here are in units of Hartree/Å, not Hartree/Bohr as listed in many places.
|
|
193
|
+
"""
|
|
194
|
+
opt_settings.energy_threshold = 1e-6
|
|
195
|
+
match mode:
|
|
196
|
+
case Mode.RECKLESS:
|
|
197
|
+
opt_settings.energy_threshold = 2e-5
|
|
198
|
+
opt_settings.max_gradient_threshold = 7e-3
|
|
199
|
+
opt_settings.rms_gradient_threshold = 6e-3
|
|
200
|
+
case Mode.RAPID:
|
|
201
|
+
opt_settings.energy_threshold = 5e-5
|
|
202
|
+
opt_settings.max_gradient_threshold = 5e-3
|
|
203
|
+
opt_settings.rms_gradient_threshold = 3.5e-3
|
|
204
|
+
case Mode.CAREFUL:
|
|
205
|
+
opt_settings.max_gradient_threshold = 9e-4
|
|
206
|
+
opt_settings.rms_gradient_threshold = 6e-4
|
|
207
|
+
case Mode.METICULOUS:
|
|
208
|
+
opt_settings.max_gradient_threshold = 3e-5
|
|
209
|
+
opt_settings.rms_gradient_threshold = 2e-5
|
|
210
|
+
case Mode.DEBUG:
|
|
211
|
+
opt_settings.max_gradient_threshold = 4e-6
|
|
212
|
+
opt_settings.rms_gradient_threshold = 2e-6
|
|
213
|
+
case _:
|
|
214
|
+
raise ValueError(f"Unknown mode ``{mode.value}``!")
|
|
215
|
+
|
|
216
|
+
return opt_settings
|
|
@@ -56,6 +56,7 @@ class BDEWorkflow(Workflow, MultiStageOptMixin):
|
|
|
56
56
|
|
|
57
57
|
Inherited:
|
|
58
58
|
:param initial_molecule: Molecule of interest
|
|
59
|
+
:param mode: Mode for workflow
|
|
59
60
|
:param multistage_opt_settings: set by mode unless mode=MANUAL (ignores additional settings if set)
|
|
60
61
|
:param solvent: solvent to use
|
|
61
62
|
:param xtb_preopt: pre-optimize with xtb (sets based on mode when None)
|
|
@@ -69,7 +70,6 @@ class BDEWorkflow(Workflow, MultiStageOptMixin):
|
|
|
69
70
|
:param transition_state: whether this is a transition state (not supported)
|
|
70
71
|
|
|
71
72
|
New:
|
|
72
|
-
:param mode: Mode for workflow
|
|
73
73
|
:param optimize_fragments: whether to optimize the fragments, or just the starting molecule (default depends on mode)
|
|
74
74
|
:param atoms: atoms to dissociate (1-indexed)
|
|
75
75
|
:param fragment_indices: fragments to dissociate (all fields feed into this, 1-indexed)
|
|
@@ -80,7 +80,6 @@ class BDEWorkflow(Workflow, MultiStageOptMixin):
|
|
|
80
80
|
:param bdes: BDE results
|
|
81
81
|
"""
|
|
82
82
|
|
|
83
|
-
mode: Mode
|
|
84
83
|
mso_mode: Mode = _sentinel_mso_mode # type: ignore [assignment]
|
|
85
84
|
frequencies: bool = False
|
|
86
85
|
optimize_fragments: bool = None # type: ignore [assignment]
|
|
@@ -107,15 +106,6 @@ class BDEWorkflow(Workflow, MultiStageOptMixin):
|
|
|
107
106
|
"""
|
|
108
107
|
return f"{type(self).__name__} {self.mode.name}\n" + "\n".join(map(str, self.fragment_indices))
|
|
109
108
|
|
|
110
|
-
def __repr__(self) -> str:
|
|
111
|
-
"""
|
|
112
|
-
Return a string representation of the BDE workflow.
|
|
113
|
-
|
|
114
|
-
>>> BDEWorkflow(initial_molecule=Molecule.from_xyz("He 0 0 0"), mode=Mode.METICULOUS, atoms=[])
|
|
115
|
-
<BDEWorkflow METICULOUS>
|
|
116
|
-
"""
|
|
117
|
-
return f"<{type(self).__name__} {self.mode.name}>"
|
|
118
|
-
|
|
119
109
|
@property
|
|
120
110
|
def energies(self) -> tuple[float | None, ...]:
|
|
121
111
|
return tuple(bde.energy for bde in self.bdes)
|
|
@@ -128,22 +118,21 @@ class BDEWorkflow(Workflow, MultiStageOptMixin):
|
|
|
128
118
|
|
|
129
119
|
return value
|
|
130
120
|
|
|
131
|
-
@field_validator("mode")
|
|
132
|
-
@classmethod
|
|
133
|
-
def set_mode_auto(cls, mode: Mode) -> Mode:
|
|
134
|
-
if mode == Mode.AUTO:
|
|
135
|
-
return Mode.RAPID
|
|
136
|
-
|
|
137
|
-
return mode
|
|
138
|
-
|
|
139
121
|
@field_validator("initial_molecule", mode="before")
|
|
140
122
|
@classmethod
|
|
141
|
-
def no_charge_or_spin(cls,
|
|
123
|
+
def no_charge_or_spin(cls, val: Molecule | dict[str, Any]) -> Molecule | dict[str, Any]:
|
|
142
124
|
"""Ensure the molecule has no charge or spin."""
|
|
125
|
+
if isinstance(val, dict):
|
|
126
|
+
mol = Molecule(**val)
|
|
127
|
+
elif isinstance(val, Molecule):
|
|
128
|
+
mol = val
|
|
129
|
+
else:
|
|
130
|
+
raise ValueError(f"{val=} is not a Molecule.")
|
|
131
|
+
|
|
143
132
|
if mol.charge != 0 or mol.multiplicity != 1:
|
|
144
133
|
raise ValueError("Charge and spin partitioning undefined for BDE, only neutral singlet molecules supported.")
|
|
145
134
|
|
|
146
|
-
return
|
|
135
|
+
return val
|
|
147
136
|
|
|
148
137
|
@model_validator(mode="before")
|
|
149
138
|
@classmethod
|
|
@@ -159,10 +148,10 @@ class BDEWorkflow(Workflow, MultiStageOptMixin):
|
|
|
159
148
|
self.fragment_indices = tuple(map(tuple, self.fragment_indices))
|
|
160
149
|
|
|
161
150
|
match self.mode:
|
|
162
|
-
case Mode.RECKLESS
|
|
163
|
-
#
|
|
164
|
-
self.optimize_fragments =
|
|
165
|
-
case Mode.CAREFUL | Mode.METICULOUS:
|
|
151
|
+
case Mode.RECKLESS:
|
|
152
|
+
# GFN-FF doesn't support open-shell species
|
|
153
|
+
self.optimize_fragments = False
|
|
154
|
+
case Mode.RAPID | Mode.CAREFUL | Mode.METICULOUS:
|
|
166
155
|
# Default on
|
|
167
156
|
self.optimize_fragments = self.optimize_fragments or self.optimize_fragments is None
|
|
168
157
|
case _:
|
|
@@ -22,9 +22,9 @@ class MultiStageOptSettings(BaseModel):
|
|
|
22
22
|
RAPID *default
|
|
23
23
|
r²SCAN-3c//GFN2-xTB with GFN0-xTB pre-opt (off by default)
|
|
24
24
|
CAREFUL
|
|
25
|
-
wB97X-3c//
|
|
25
|
+
wB97X-3c//r²SCAN-3c with GFN2-xTB pre-opt
|
|
26
26
|
METICULOUS
|
|
27
|
-
wB97M-D3BJ/def2-TZVPPD//wB97X-3c//
|
|
27
|
+
wB97M-D3BJ/def2-TZVPPD//wB97X-3c//r²SCAN-3c with GFN2-xTB pre-opt
|
|
28
28
|
|
|
29
29
|
Notes:
|
|
30
30
|
- No solvent in pre-opt
|
|
@@ -163,7 +163,7 @@ class MultiStageOptSettings(BaseModel):
|
|
|
163
163
|
self.xtb_preopt = (self.xtb_preopt is None) or self.xtb_preopt
|
|
164
164
|
self.optimization_settings = [
|
|
165
165
|
*gfn2_pre_opt * self.xtb_preopt,
|
|
166
|
-
opt(Method.
|
|
166
|
+
opt(Method.R2SCAN3C, solvent=self.solvent, freq=self.frequencies),
|
|
167
167
|
]
|
|
168
168
|
self.singlepoint_settings = sp(Method.WB97X3C, solvent=self.solvent)
|
|
169
169
|
|
|
@@ -171,7 +171,7 @@ class MultiStageOptSettings(BaseModel):
|
|
|
171
171
|
self.xtb_preopt = (self.xtb_preopt is None) or self.xtb_preopt
|
|
172
172
|
self.optimization_settings = [
|
|
173
173
|
*gfn2_pre_opt * self.xtb_preopt,
|
|
174
|
-
opt(Method.
|
|
174
|
+
opt(Method.R2SCAN3C, solvent=self.solvent),
|
|
175
175
|
opt(Method.WB97X3C, solvent=self.solvent, freq=self.frequencies),
|
|
176
176
|
]
|
|
177
177
|
self.singlepoint_settings = sp(Method.WB97MD3BJ, "def2-TZVPPD", solvent=self.solvent)
|
|
@@ -212,6 +212,12 @@ class MultiStageOptWorkflow(Workflow, MultiStageOptSettings):
|
|
|
212
212
|
# Populated while running the workflow
|
|
213
213
|
calculations: list[UUID | None] = Field(default_factory=list)
|
|
214
214
|
|
|
215
|
+
def __repr__(self) -> str:
|
|
216
|
+
if self.mode != Mode.MANUAL:
|
|
217
|
+
return f"<{type(self).__name__} {self.mode.name}>"
|
|
218
|
+
|
|
219
|
+
return f"<{type(self).__name__} {self.level_of_theory}>"
|
|
220
|
+
|
|
215
221
|
|
|
216
222
|
# the id of a mutable object may change, thus using object()
|
|
217
223
|
_sentinel_msos = object()
|
|
@@ -49,6 +49,7 @@ class SpinStatesWorkflow(Workflow, MultiStageOptMixin):
|
|
|
49
49
|
|
|
50
50
|
Inherited
|
|
51
51
|
:param initial_molecule: Molecule of interest
|
|
52
|
+
:param mode: Mode for workflow
|
|
52
53
|
:param multistage_opt_settings: set by mode unless mode=MANUAL (ignores additional settings if set)
|
|
53
54
|
:param solvent: solvent to use
|
|
54
55
|
:param xtb_preopt: pre-optimize with xtb (sets based on mode when None)
|
|
@@ -60,7 +61,6 @@ class SpinStatesWorkflow(Workflow, MultiStageOptMixin):
|
|
|
60
61
|
:param mso_mode: Mode for MultiStageOptSettings
|
|
61
62
|
|
|
62
63
|
New:
|
|
63
|
-
:param mode: Mode for workflow
|
|
64
64
|
:param states: multiplicities of the spin state targetted
|
|
65
65
|
:param spin_states: resulting spin states data
|
|
66
66
|
|
|
@@ -72,16 +72,12 @@ class SpinStatesWorkflow(Workflow, MultiStageOptMixin):
|
|
|
72
72
|
'<SpinStatesWorkflow [1, 3, 5] RAPID>'
|
|
73
73
|
"""
|
|
74
74
|
|
|
75
|
-
mode: Mode
|
|
76
75
|
mso_mode: Mode = _sentinel_mso_mode # type: ignore [assignment]
|
|
77
76
|
states: list[PositiveInt]
|
|
78
77
|
|
|
79
78
|
# Results
|
|
80
79
|
spin_states: list[SpinState] = Field(default_factory=list)
|
|
81
80
|
|
|
82
|
-
def __str__(self) -> str:
|
|
83
|
-
return repr(self)
|
|
84
|
-
|
|
85
81
|
def __repr__(self) -> str:
|
|
86
82
|
if self.mode != Mode.MANUAL:
|
|
87
83
|
return f"<{type(self).__name__} {self.states} {self.mode.name}>"
|
|
@@ -121,14 +117,6 @@ class SpinStatesWorkflow(Workflow, MultiStageOptMixin):
|
|
|
121
117
|
values["mso_mode"] = values["mode"]
|
|
122
118
|
return values
|
|
123
119
|
|
|
124
|
-
@field_validator("mode")
|
|
125
|
-
@classmethod
|
|
126
|
-
def set_mode_auto(cls, mode: Mode) -> Mode:
|
|
127
|
-
if mode == Mode.AUTO:
|
|
128
|
-
return Mode.RAPID
|
|
129
|
-
|
|
130
|
-
return mode
|
|
131
|
-
|
|
132
120
|
@field_validator("spin_states")
|
|
133
121
|
@classmethod
|
|
134
122
|
def validate_spin_states(cls, spin_states: list[SpinState]) -> list[SpinState]:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from pydantic import
|
|
1
|
+
from pydantic import field_validator
|
|
2
2
|
|
|
3
3
|
from ..base import Base
|
|
4
4
|
from ..message import Message
|
|
@@ -8,30 +8,17 @@ from ..types import UUID
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class Workflow(Base):
|
|
11
|
-
"""All workflows should have these properties."""
|
|
12
|
-
|
|
13
|
-
initial_molecule: Molecule
|
|
14
|
-
messages: list[Message] = []
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class DBCalculation(Base):
|
|
18
|
-
"""Encodes a calculation that's in the database. This isn't terribly useful by itself."""
|
|
19
|
-
|
|
20
|
-
uuid: UUID
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class WorkflowInput(BaseModel):
|
|
24
11
|
"""
|
|
25
|
-
|
|
12
|
+
Base class for Workflows.
|
|
26
13
|
|
|
27
14
|
:param initial_molecule: Molecule of interest
|
|
28
|
-
:param mode: Mode
|
|
15
|
+
:param mode: Mode to use
|
|
16
|
+
:param messages: messages to display
|
|
29
17
|
"""
|
|
30
18
|
|
|
31
|
-
model_config = ConfigDict(extra="forbid")
|
|
32
|
-
|
|
33
19
|
initial_molecule: Molecule
|
|
34
|
-
mode: Mode
|
|
20
|
+
mode: Mode = Mode.AUTO
|
|
21
|
+
messages: list[Message] = []
|
|
35
22
|
|
|
36
23
|
def __str__(self) -> str:
|
|
37
24
|
return repr(self)
|
|
@@ -39,8 +26,17 @@ class WorkflowInput(BaseModel):
|
|
|
39
26
|
def __repr__(self) -> str:
|
|
40
27
|
return f"<{type(self).__name__} {self.mode.name}>"
|
|
41
28
|
|
|
29
|
+
@field_validator("mode")
|
|
30
|
+
@classmethod
|
|
31
|
+
def set_mode_auto(cls, mode: Mode) -> Mode:
|
|
32
|
+
"""Set the mode to RAPID if AUTO is selected."""
|
|
33
|
+
if mode == Mode.AUTO:
|
|
34
|
+
return Mode.RAPID
|
|
35
|
+
|
|
36
|
+
return mode
|
|
42
37
|
|
|
43
|
-
class WorkflowResults(BaseModel):
|
|
44
|
-
"""Results of a workflow."""
|
|
45
38
|
|
|
46
|
-
|
|
39
|
+
class DBCalculation(Base):
|
|
40
|
+
"""Encodes a calculation that's in the database. This isn't terribly useful by itself."""
|
|
41
|
+
|
|
42
|
+
uuid: UUID
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from stjames import Atom, Molecule, MoleculeReadError, PeriodicCell
|
|
4
|
+
|
|
5
|
+
valid_extxyz = """
|
|
6
|
+
5
|
|
7
|
+
Lattice="6.0 0.0 0.0 0.0 6.0 0.0 0.0 0.0 6.0" Properties=species:S:1:pos:R:3
|
|
8
|
+
C 0.0 0.0 0.0
|
|
9
|
+
H 0.0 0.0 1.0
|
|
10
|
+
H 1.0 0.0 0.0
|
|
11
|
+
H 0.0 1.0 0.0
|
|
12
|
+
H 1.0 1.0 1.0
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
incorrect_num_atoms = """
|
|
16
|
+
6
|
|
17
|
+
Lattice="6.0 0.0 0.0 0.0 6.0 0.0 0.0 0.0 6.0" Properties=species:S:1:pos:R:3
|
|
18
|
+
C 0.0 0.0 0.0
|
|
19
|
+
H 0.0 0.0 1.0
|
|
20
|
+
H 1.0 0.0 0.0
|
|
21
|
+
H 0.0 1.0 0.0
|
|
22
|
+
H 1.0 1.0 1.0
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
not_digit_num_atoms = """
|
|
26
|
+
v
|
|
27
|
+
Lattice="6.0 0.0 0.0 0.0 6.0 0.0 0.0 0.0 6.0" Properties=species:S:1:pos:R:3
|
|
28
|
+
C 0.0 0.0 0.0
|
|
29
|
+
H 0.0 0.0 1.0
|
|
30
|
+
H 1.0 0.0 0.0
|
|
31
|
+
H 0.0 1.0 0.0
|
|
32
|
+
H 1.0 1.0 1.0
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
many_num_atoms = """
|
|
36
|
+
6 9
|
|
37
|
+
Lattice="6.0 0.0 0.0 0.0 6.0 0.0 0.0 0.0 6.0" Properties=species:S:1:pos:R:3
|
|
38
|
+
C 0.0 0.0 0.0
|
|
39
|
+
H 0.0 0.0 1.0
|
|
40
|
+
H 1.0 0.0 0.0
|
|
41
|
+
H 0.0 1.0 0.0
|
|
42
|
+
H 1.0 1.0 1.0
|
|
43
|
+
"""
|
|
44
|
+
no_num_atoms = """
|
|
45
|
+
Lattice="6.0 0.0 0.0 0.0 6.0 0.0 0.0 0.0 6.0" Properties=species:S:1:pos:R:3
|
|
46
|
+
C 0.0 0.0 0.0
|
|
47
|
+
H 0.0 0.0 1.0
|
|
48
|
+
H 1.0 0.0 0.0
|
|
49
|
+
H 0.0 1.0 0.0
|
|
50
|
+
H 1.0 1.0 1.0
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
xyz_style = """
|
|
54
|
+
5
|
|
55
|
+
Comment
|
|
56
|
+
C 0.0 0.0 0.0
|
|
57
|
+
H 0.0 0.0 1.0
|
|
58
|
+
H 1.0 0.0 0.0
|
|
59
|
+
H 0.0 1.0 0.0
|
|
60
|
+
H 1.0 1.0 1.0
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
missing_lattice = """
|
|
64
|
+
5
|
|
65
|
+
Properties=species:S:1:pos:R:3
|
|
66
|
+
C 0.0 0.0 0.0
|
|
67
|
+
H 0.0 0.0 1.0
|
|
68
|
+
H 1.0 0.0 0.0
|
|
69
|
+
H 0.0 1.0 0.0
|
|
70
|
+
H 1.0 1.0 1.0
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
missing_properties = """
|
|
74
|
+
5
|
|
75
|
+
Lattice="6.0 0.0 0.0 0.0 6.0 0.0 0.0 0.0 6.0"
|
|
76
|
+
C 0.0 0.0 0.0
|
|
77
|
+
H 0.0 0.0 1.0
|
|
78
|
+
H 1.0 0.0 0.0
|
|
79
|
+
H 0.0 1.0 0.0
|
|
80
|
+
H 1.0 1.0 1.0
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
incorrect_properites = """
|
|
84
|
+
5
|
|
85
|
+
Lattice="6.0 0.0 0.0 0.0 6.0 0.0 0.0 0.0 6.0" Properties=species:S:1:pos:R:3foo:1
|
|
86
|
+
C 0.0 0.0 0.0
|
|
87
|
+
H 0.0 0.0 1.0
|
|
88
|
+
H 1.0 0.0 0.0
|
|
89
|
+
H 0.0 1.0 0.0
|
|
90
|
+
H 1.0 1.0 1.0
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
incorrect_lattice_extra = """
|
|
94
|
+
5
|
|
95
|
+
Lattice="6.0 0.0 0.0 0.0 6.0 0.0 0.0 0.0 6.0 3.14" Properties=species:S:1:pos:R:3
|
|
96
|
+
C 0.0 0.0 0.0
|
|
97
|
+
H 0.0 0.0 1.0
|
|
98
|
+
H 1.0 0.0 0.0
|
|
99
|
+
H 0.0 1.0 0.0
|
|
100
|
+
H 1.0 1.0 1.0
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
incorrect_lattice_equals = """
|
|
104
|
+
5
|
|
105
|
+
Lattice="6.0 0.0 =0.0 0.0 6.0 0.0 0.0 0.0 6.0" Properties=species:S:1:pos:R:3
|
|
106
|
+
C 0.0 0.0 0.0
|
|
107
|
+
H 0.0 0.0 1.0
|
|
108
|
+
H 1.0 0.0 0.0
|
|
109
|
+
H 0.0 1.0 0.0
|
|
110
|
+
H 1.0 1.0 1.0
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
incorrect_lattice_str = """
|
|
114
|
+
5
|
|
115
|
+
Lattice="6.0 0.0 0.0 hi 6.0 0.0 0.0 0.0 6.0" Properties=species:S:1:pos:R:3
|
|
116
|
+
C 0.0 0.0 0.0
|
|
117
|
+
H 0.0 0.0 1.0
|
|
118
|
+
H 1.0 0.0 0.0
|
|
119
|
+
H 0.0 1.0 0.0
|
|
120
|
+
H 1.0 1.0 1.0
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
incorrect_lattice_extra_string = """
|
|
124
|
+
5
|
|
125
|
+
Lattice="6.0 0.0 0.0 0.0 sup 6.0 0.0 0.0 0.0 6.0" Properties=species:S:1:pos:R:3
|
|
126
|
+
C 0.0 0.0 0.0
|
|
127
|
+
H 0.0 0.0 1.0
|
|
128
|
+
H 1.0 0.0 0.0
|
|
129
|
+
H 0.0 1.0 0.0
|
|
130
|
+
H 1.0 1.0 1.0
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
incorrect_lattice_single_quote = """
|
|
135
|
+
5
|
|
136
|
+
Lattice="6.0 0.0 0.0 0.0 6.0 '0.0 0.0 0.0 6.0" Properties=species:S:1:pos:R:3
|
|
137
|
+
C 0.0 0.0 0.0
|
|
138
|
+
H 0.0 0.0 1.0
|
|
139
|
+
H 1.0 0.0 0.0
|
|
140
|
+
H 0.0 1.0 0.0
|
|
141
|
+
H 1.0 1.0 1.0
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
incorrect_lattice_double_quote = """
|
|
145
|
+
5
|
|
146
|
+
Lattice="6.0 0.0 0.0 0.0 "6.0 0.0 0.0 0.0 6.0" Properties=species:S:1:pos:R:3
|
|
147
|
+
C 0.0 0.0 0.0
|
|
148
|
+
H 0.0 0.0 1.0
|
|
149
|
+
H 1.0 0.0 0.0
|
|
150
|
+
H 0.0 1.0 0.0
|
|
151
|
+
H 1.0 1.0 1.0
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
incorrect_lattice_double_single_quote = """
|
|
155
|
+
5
|
|
156
|
+
Lattice="6.0 0.0 0.0 0.0 '6.0 0.0 0.0 '0.0 6.0" Properties=species:S:1:pos:R:3
|
|
157
|
+
C 0.0 0.0 0.0
|
|
158
|
+
H 0.0 0.0 1.0
|
|
159
|
+
H 1.0 0.0 0.0
|
|
160
|
+
H 0.0 1.0 0.0
|
|
161
|
+
H 1.0 1.0 1.0
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
incorrect_lattice_double_double_quote = """
|
|
165
|
+
5
|
|
166
|
+
Lattice="6.0 0.0 "0.0 0.0 6.0 0.0 0.0 "0.0 6.0" Properties=species:S:1:pos:R:3
|
|
167
|
+
C 0.0 0.0 0.0
|
|
168
|
+
H 0.0 0.0 1.0
|
|
169
|
+
H 1.0 0.0 0.0
|
|
170
|
+
H 0.0 1.0 0.0
|
|
171
|
+
H 1.0 1.0 1.0
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
expected_cell = (
|
|
176
|
+
(6.0, 0.0, 0.0),
|
|
177
|
+
(0.0, 6.0, 0.0),
|
|
178
|
+
(0.0, 0.0, 6.0),
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
expected_atoms = [
|
|
182
|
+
Atom(atomic_number=6, position=(0.0, 0.0, 0.0)), # C
|
|
183
|
+
Atom(atomic_number=1, position=(0.0, 0.0, 1.0)), # H
|
|
184
|
+
Atom(atomic_number=1, position=(1.0, 0.0, 0.0)), # H
|
|
185
|
+
Atom(atomic_number=1, position=(0.0, 1.0, 0.0)), # H
|
|
186
|
+
Atom(atomic_number=1, position=(1.0, 1.0, 1.0)), # H
|
|
187
|
+
]
|
|
188
|
+
|
|
189
|
+
expected_molecule = Molecule(
|
|
190
|
+
charge=0,
|
|
191
|
+
multiplicity=1,
|
|
192
|
+
atoms=expected_atoms,
|
|
193
|
+
cell=PeriodicCell(lattice_vectors=expected_cell),
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def test_molecule_from_extxyz_valid() -> None:
|
|
198
|
+
"""
|
|
199
|
+
Test case for valid extxyz string.
|
|
200
|
+
"""
|
|
201
|
+
molecule = Molecule.from_extxyz(valid_extxyz)
|
|
202
|
+
assert molecule == expected_molecule, f"Valid case failed: got {molecule}, expected {expected_molecule}"
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@pytest.mark.parametrize(
|
|
206
|
+
"invalid_extxyz",
|
|
207
|
+
[
|
|
208
|
+
incorrect_num_atoms,
|
|
209
|
+
no_num_atoms,
|
|
210
|
+
not_digit_num_atoms,
|
|
211
|
+
many_num_atoms,
|
|
212
|
+
xyz_style,
|
|
213
|
+
missing_lattice,
|
|
214
|
+
missing_properties,
|
|
215
|
+
incorrect_properites,
|
|
216
|
+
incorrect_lattice_extra,
|
|
217
|
+
incorrect_lattice_equals,
|
|
218
|
+
incorrect_lattice_str,
|
|
219
|
+
incorrect_lattice_extra_string,
|
|
220
|
+
incorrect_lattice_single_quote,
|
|
221
|
+
incorrect_lattice_double_quote,
|
|
222
|
+
incorrect_lattice_double_single_quote,
|
|
223
|
+
incorrect_lattice_double_double_quote,
|
|
224
|
+
],
|
|
225
|
+
)
|
|
226
|
+
def test_molecule_from_extxyz_invalid(invalid_extxyz: str) -> None:
|
|
227
|
+
"""
|
|
228
|
+
Test case for invalid extxyz strings, ensuring they raise MoleculeReadError.
|
|
229
|
+
"""
|
|
230
|
+
with pytest.raises(MoleculeReadError):
|
|
231
|
+
Molecule.from_extxyz(invalid_extxyz)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from stjames import Constraint, Mode, OptimizationSettings, Settings
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_set_mode_auto() -> None:
|
|
5
|
+
Settings()
|
|
6
|
+
assert Settings().mode == Mode.RAPID
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_opt_settings() -> None:
|
|
10
|
+
settings_rapid = Settings(mode=Mode.RAPID)
|
|
11
|
+
settings_meticulous = Settings(mode=Mode.METICULOUS)
|
|
12
|
+
|
|
13
|
+
cons = [Constraint(atoms=[1, 2], constraint_type="bond")]
|
|
14
|
+
settings_careful = Settings(mode=Mode.CAREFUL, opt_settings=OptimizationSettings(constraints=cons))
|
|
15
|
+
|
|
16
|
+
rap_opt_set = settings_rapid.opt_settings
|
|
17
|
+
car_opt_set = settings_careful.opt_settings
|
|
18
|
+
met_opt_set = settings_meticulous.opt_settings
|
|
19
|
+
|
|
20
|
+
assert not rap_opt_set.constraints
|
|
21
|
+
assert not met_opt_set.constraints
|
|
22
|
+
assert car_opt_set.constraints == cons
|
|
23
|
+
|
|
24
|
+
assert rap_opt_set.energy_threshold == 5e-5
|
|
25
|
+
assert rap_opt_set.max_gradient_threshold == 5e-3
|
|
26
|
+
assert rap_opt_set.rms_gradient_threshold == 3.5e-3
|
|
27
|
+
|
|
28
|
+
assert car_opt_set.energy_threshold == 1e-6
|
|
29
|
+
assert car_opt_set.max_gradient_threshold == 9e-4
|
|
30
|
+
assert car_opt_set.rms_gradient_threshold == 6e-4
|
|
31
|
+
|
|
32
|
+
assert met_opt_set.energy_threshold == 1e-6
|
|
33
|
+
assert met_opt_set.max_gradient_threshold == 3e-5
|
|
34
|
+
assert met_opt_set.rms_gradient_threshold == 2e-5
|
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
from typing import Any, Optional, TypeVar
|
|
2
|
-
|
|
3
|
-
import pydantic
|
|
4
|
-
|
|
5
|
-
from .base import Base, UniqueList
|
|
6
|
-
from .basis_set import BasisSet
|
|
7
|
-
from .correction import Correction
|
|
8
|
-
from .method import METHODS_WITH_CORRECTION, PREPACKAGED_METHODS, Method
|
|
9
|
-
from .mode import Mode
|
|
10
|
-
from .opt_settings import OptimizationSettings
|
|
11
|
-
from .scf_settings import SCFSettings
|
|
12
|
-
from .solvent import SolventSettings
|
|
13
|
-
from .task import Task
|
|
14
|
-
from .thermochem_settings import ThermochemistrySettings
|
|
15
|
-
|
|
16
|
-
_T = TypeVar("_T")
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class Settings(Base):
|
|
20
|
-
method: Method = Method.HARTREE_FOCK
|
|
21
|
-
basis_set: Optional[BasisSet] = None
|
|
22
|
-
tasks: UniqueList[Task] = [Task.ENERGY, Task.CHARGE, Task.DIPOLE]
|
|
23
|
-
corrections: UniqueList[Correction] = []
|
|
24
|
-
|
|
25
|
-
mode: Mode = Mode.AUTO
|
|
26
|
-
|
|
27
|
-
solvent_settings: Optional[SolventSettings] = None
|
|
28
|
-
|
|
29
|
-
# scf/opt settings will be set automatically based on mode, but can be overridden manually
|
|
30
|
-
scf_settings: SCFSettings = SCFSettings()
|
|
31
|
-
opt_settings: OptimizationSettings = OptimizationSettings()
|
|
32
|
-
thermochem_settings: ThermochemistrySettings = ThermochemistrySettings()
|
|
33
|
-
|
|
34
|
-
# mypy has this dead wrong (https://docs.pydantic.dev/2.0/usage/computed_fields/)
|
|
35
|
-
# Python 3.12 narrows the reason for the ignore to prop-decorator
|
|
36
|
-
@pydantic.computed_field # type: ignore[misc, prop-decorator, unused-ignore]
|
|
37
|
-
@property
|
|
38
|
-
def level_of_theory(self) -> str:
|
|
39
|
-
corrections = list(filter(lambda x: x not in (None, ""), self.corrections))
|
|
40
|
-
|
|
41
|
-
if self.method in PREPACKAGED_METHODS or self.basis_set is None:
|
|
42
|
-
method = self.method.value
|
|
43
|
-
elif self.method in METHODS_WITH_CORRECTION or len(corrections) == 0:
|
|
44
|
-
method = f"{self.method.value}/{self.basis_set.name.lower()}"
|
|
45
|
-
else:
|
|
46
|
-
method = f"{self.method.value}-{'-'.join([c.value for c in corrections])}/{self.basis_set.name.lower()}"
|
|
47
|
-
|
|
48
|
-
if self.solvent_settings is not None:
|
|
49
|
-
method += f"/{self.solvent_settings.model.value}({self.solvent_settings.solvent.value})"
|
|
50
|
-
|
|
51
|
-
return method
|
|
52
|
-
|
|
53
|
-
def model_post_init(self, __context: Any) -> None:
|
|
54
|
-
_assign_settings_by_mode(self)
|
|
55
|
-
|
|
56
|
-
# figure out `optimize_ts`
|
|
57
|
-
if Task.OPTIMIZE_TS in self.tasks:
|
|
58
|
-
self.tasks.pop(self.tasks.index(Task.OPTIMIZE_TS))
|
|
59
|
-
self.tasks.append(Task.OPTIMIZE)
|
|
60
|
-
self.opt_settings.transition_state = True
|
|
61
|
-
|
|
62
|
-
# composite methods have their own basis sets, so overwrite user stuff
|
|
63
|
-
if self.method == Method.HF3C:
|
|
64
|
-
self.basis_set = BasisSet(name="minix")
|
|
65
|
-
elif self.method == Method.B973C:
|
|
66
|
-
self.basis_set = BasisSet(name="def2-mTZVP")
|
|
67
|
-
elif self.method == Method.R2SCAN3C:
|
|
68
|
-
self.basis_set = BasisSet(name="def2-mTZVPP")
|
|
69
|
-
elif self.method == Method.WB97X3C:
|
|
70
|
-
self.basis_set = BasisSet(name="vDZP")
|
|
71
|
-
|
|
72
|
-
@pydantic.field_validator("basis_set", mode="before")
|
|
73
|
-
@classmethod
|
|
74
|
-
def parse_basis_set(cls, v: Any) -> BasisSet | dict[str, Any] | None:
|
|
75
|
-
"""Turn a string into a ``BasisSet`` object. (This is a little crude.)"""
|
|
76
|
-
if isinstance(v, BasisSet):
|
|
77
|
-
return None if v.name is None else v
|
|
78
|
-
elif isinstance(v, dict):
|
|
79
|
-
return None if v.get("name") is None else v
|
|
80
|
-
elif isinstance(v, str):
|
|
81
|
-
if len(v):
|
|
82
|
-
return BasisSet(name=v)
|
|
83
|
-
# "" is basically None, let's be real here...
|
|
84
|
-
return None
|
|
85
|
-
elif v is None:
|
|
86
|
-
return None
|
|
87
|
-
else:
|
|
88
|
-
raise ValueError(f"invalid value ``{v}`` for ``basis_set``")
|
|
89
|
-
|
|
90
|
-
@pydantic.field_validator("corrections", mode="before")
|
|
91
|
-
@classmethod
|
|
92
|
-
def remove_empty_string(cls, v: list[_T]) -> list[_T]:
|
|
93
|
-
"""Remove empty string values."""
|
|
94
|
-
return [c for c in v if c] if v is not None else v
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
def _assign_settings_by_mode(settings: Settings) -> None:
|
|
98
|
-
"""Modifies ``scf_settings`` and ``opt_settings`` based on preset ``mode``."""
|
|
99
|
-
mode = settings.mode
|
|
100
|
-
|
|
101
|
-
if mode == Mode.AUTO:
|
|
102
|
-
if (Task.OPTIMIZE in settings.tasks) or (Task.GRADIENT in settings.tasks) or (Task.FREQUENCIES in settings.tasks) or (Task.HESSIAN in settings.tasks):
|
|
103
|
-
# noisy gradient! struggles to converge
|
|
104
|
-
if settings.method == Method.AIMNET2_WB97MD3:
|
|
105
|
-
mode = Mode.RAPID
|
|
106
|
-
else:
|
|
107
|
-
mode = Mode.CAREFUL
|
|
108
|
-
else:
|
|
109
|
-
mode = Mode.RAPID
|
|
110
|
-
elif mode == Mode.MANUAL:
|
|
111
|
-
return
|
|
112
|
-
|
|
113
|
-
# modify scf settings!
|
|
114
|
-
#
|
|
115
|
-
# values based off of the following sources:
|
|
116
|
-
# qchem:
|
|
117
|
-
# https://manual.q-chem.com/5.2/Ch4.S3.SS2.html
|
|
118
|
-
# https://manual.q-chem.com/5.2/Ch4.S5.SS2.html
|
|
119
|
-
#
|
|
120
|
-
# gaussian:
|
|
121
|
-
# https://gaussian.com/integral/
|
|
122
|
-
# https://gaussian.com/overlay5/
|
|
123
|
-
#
|
|
124
|
-
# orca:
|
|
125
|
-
# manual 4.2.1, §9.6.1 and §9.7.3
|
|
126
|
-
#
|
|
127
|
-
# psi4:
|
|
128
|
-
# https://psicode.org/psi4manual/master/autodir_options_c/module__scf.html
|
|
129
|
-
# https://psicode.org/psi4manual/master/autodoc_glossary_options_c.html
|
|
130
|
-
#
|
|
131
|
-
# terachem:
|
|
132
|
-
# manual, it's easy to locate everything.
|
|
133
|
-
#
|
|
134
|
-
# the below values are my best attempt at homogenizing various sources.
|
|
135
|
-
# in general, eri_threshold should be 3 OOM lower than scf convergence
|
|
136
|
-
scf_settings = settings.scf_settings
|
|
137
|
-
if mode == Mode.RECKLESS:
|
|
138
|
-
scf_settings.energy_threshold = 1e-5
|
|
139
|
-
scf_settings.rms_error_threshold = 1e-7
|
|
140
|
-
scf_settings.max_error_threshold = 1e-5
|
|
141
|
-
scf_settings.rebuild_frequency = 100
|
|
142
|
-
scf_settings.int_settings.eri_threshold = 1e-8
|
|
143
|
-
scf_settings.int_settings.csam_multiplier = 3.0
|
|
144
|
-
scf_settings.int_settings.pair_overlap_threshold = 1e-8
|
|
145
|
-
elif mode == Mode.RAPID:
|
|
146
|
-
scf_settings.energy_threshold = 5e-5
|
|
147
|
-
scf_settings.rms_error_threshold = 1e-8
|
|
148
|
-
scf_settings.max_error_threshold = 1e-6
|
|
149
|
-
scf_settings.rebuild_frequency = 20
|
|
150
|
-
scf_settings.int_settings.eri_threshold = 1e-9
|
|
151
|
-
scf_settings.int_settings.csam_multiplier = 1.0
|
|
152
|
-
scf_settings.int_settings.pair_overlap_threshold = 1e-9
|
|
153
|
-
elif mode == Mode.CAREFUL:
|
|
154
|
-
scf_settings.energy_threshold = 1e-6
|
|
155
|
-
scf_settings.rms_error_threshold = 1e-9
|
|
156
|
-
scf_settings.max_error_threshold = 1e-7
|
|
157
|
-
scf_settings.rebuild_frequency = 10
|
|
158
|
-
scf_settings.int_settings.eri_threshold = 1e-10
|
|
159
|
-
scf_settings.int_settings.csam_multiplier = 1.0
|
|
160
|
-
scf_settings.int_settings.pair_overlap_threshold = 1e-10
|
|
161
|
-
elif mode == Mode.METICULOUS:
|
|
162
|
-
scf_settings.energy_threshold = 1e-8
|
|
163
|
-
scf_settings.rms_error_threshold = 1e-9
|
|
164
|
-
scf_settings.max_error_threshold = 1e-7
|
|
165
|
-
scf_settings.rebuild_frequency = 5
|
|
166
|
-
scf_settings.int_settings.eri_threshold = 1e-12
|
|
167
|
-
scf_settings.int_settings.csam_multiplier = 1.0
|
|
168
|
-
scf_settings.int_settings.pair_overlap_threshold = 1e-12
|
|
169
|
-
elif mode == Mode.DEBUG:
|
|
170
|
-
scf_settings.energy_threshold = 1e-9
|
|
171
|
-
scf_settings.rms_error_threshold = 1e-10
|
|
172
|
-
scf_settings.max_error_threshold = 1e-9
|
|
173
|
-
scf_settings.rebuild_frequency = 1
|
|
174
|
-
scf_settings.int_settings.eri_threshold = 1e-14
|
|
175
|
-
scf_settings.int_settings.csam_multiplier = 1e10 # in other words, disable CSAM
|
|
176
|
-
scf_settings.int_settings.pair_overlap_threshold = 1e-14
|
|
177
|
-
else:
|
|
178
|
-
raise ValueError(f"Unknown mode ``{mode.value}``!")
|
|
179
|
-
|
|
180
|
-
opt_settings = settings.opt_settings
|
|
181
|
-
|
|
182
|
-
# constrained optimizations warrant loosening the settings a bit
|
|
183
|
-
has_constraints = len(opt_settings.constraints) > 0
|
|
184
|
-
|
|
185
|
-
# cf. DLFIND manual, and https://www.cup.uni-muenchen.de/ch/compchem/geom/basic.html
|
|
186
|
-
# and the discussion at https://geometric.readthedocs.io/en/latest/how-it-works.html
|
|
187
|
-
# in periodic systems, "normal" is 0.05 eV/Å ~= 2e-3 Hartree/Å, and "careful" is 0.01 ~= 4e-4
|
|
188
|
-
if mode == Mode.RECKLESS:
|
|
189
|
-
opt_settings.energy_threshold = 2e-5
|
|
190
|
-
opt_settings.max_gradient_threshold = 7e-3
|
|
191
|
-
opt_settings.rms_gradient_threshold = 6e-3
|
|
192
|
-
elif mode == Mode.RAPID or (mode == Mode.CAREFUL and has_constraints):
|
|
193
|
-
opt_settings.energy_threshold = 5e-5
|
|
194
|
-
opt_settings.max_gradient_threshold = 5e-3
|
|
195
|
-
opt_settings.rms_gradient_threshold = 3.5e-3
|
|
196
|
-
elif mode == Mode.CAREFUL or (mode == Mode.METICULOUS and has_constraints):
|
|
197
|
-
opt_settings.energy_threshold = 1e-6
|
|
198
|
-
opt_settings.max_gradient_threshold = 9e-4
|
|
199
|
-
opt_settings.rms_gradient_threshold = 6e-4
|
|
200
|
-
elif mode == Mode.METICULOUS:
|
|
201
|
-
opt_settings.energy_threshold = 1e-6
|
|
202
|
-
opt_settings.max_gradient_threshold = 3e-5
|
|
203
|
-
opt_settings.rms_gradient_threshold = 2e-5
|
|
204
|
-
elif mode == Mode.DEBUG:
|
|
205
|
-
opt_settings.energy_threshold = 1e-6
|
|
206
|
-
opt_settings.max_gradient_threshold = 4e-6
|
|
207
|
-
opt_settings.rms_gradient_threshold = 2e-6
|
|
208
|
-
else:
|
|
209
|
-
raise ValueError(f"Unknown mode ``{mode.value}``!")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|