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.

Files changed (61) hide show
  1. {stjames-0.0.42/stjames.egg-info → stjames-0.0.44}/PKG-INFO +1 -1
  2. {stjames-0.0.42 → stjames-0.0.44}/pyproject.toml +1 -1
  3. {stjames-0.0.42 → stjames-0.0.44}/stjames/atom.py +1 -1
  4. {stjames-0.0.42 → stjames-0.0.44}/stjames/molecule.py +89 -2
  5. stjames-0.0.44/stjames/settings.py +216 -0
  6. {stjames-0.0.42 → stjames-0.0.44}/stjames/workflows/bde.py +14 -25
  7. {stjames-0.0.42 → stjames-0.0.44}/stjames/workflows/molecular_dynamics.py +2 -0
  8. {stjames-0.0.42 → stjames-0.0.44}/stjames/workflows/multistage_opt.py +10 -4
  9. {stjames-0.0.42 → stjames-0.0.44}/stjames/workflows/spin_states.py +1 -13
  10. {stjames-0.0.42 → stjames-0.0.44}/stjames/workflows/workflow.py +18 -22
  11. {stjames-0.0.42 → stjames-0.0.44/stjames.egg-info}/PKG-INFO +1 -1
  12. {stjames-0.0.42 → stjames-0.0.44}/stjames.egg-info/SOURCES.txt +3 -1
  13. stjames-0.0.44/tests/test_from_extxyz.py +231 -0
  14. stjames-0.0.44/tests/test_settings.py +34 -0
  15. stjames-0.0.42/stjames/settings.py +0 -209
  16. {stjames-0.0.42 → stjames-0.0.44}/LICENSE +0 -0
  17. {stjames-0.0.42 → stjames-0.0.44}/README.md +0 -0
  18. {stjames-0.0.42 → stjames-0.0.44}/setup.cfg +0 -0
  19. {stjames-0.0.42 → stjames-0.0.44}/stjames/__init__.py +0 -0
  20. {stjames-0.0.42 → stjames-0.0.44}/stjames/_deprecated_solvent_settings.py +0 -0
  21. {stjames-0.0.42 → stjames-0.0.44}/stjames/base.py +0 -0
  22. {stjames-0.0.42 → stjames-0.0.44}/stjames/basis_set.py +0 -0
  23. {stjames-0.0.42 → stjames-0.0.44}/stjames/calculation.py +0 -0
  24. {stjames-0.0.42 → stjames-0.0.44}/stjames/constraint.py +0 -0
  25. {stjames-0.0.42 → stjames-0.0.44}/stjames/correction.py +0 -0
  26. {stjames-0.0.42 → stjames-0.0.44}/stjames/data/__init__.py +0 -0
  27. {stjames-0.0.42 → stjames-0.0.44}/stjames/data/bragg_radii.json +0 -0
  28. {stjames-0.0.42 → stjames-0.0.44}/stjames/data/elements.py +0 -0
  29. {stjames-0.0.42 → stjames-0.0.44}/stjames/data/isotopes.json +0 -0
  30. {stjames-0.0.42 → stjames-0.0.44}/stjames/data/nist_isotopes.json +0 -0
  31. {stjames-0.0.42 → stjames-0.0.44}/stjames/data/read_nist_isotopes.py +0 -0
  32. {stjames-0.0.42 → stjames-0.0.44}/stjames/data/symbol_element.json +0 -0
  33. {stjames-0.0.42 → stjames-0.0.44}/stjames/diis_settings.py +0 -0
  34. {stjames-0.0.42 → stjames-0.0.44}/stjames/grid_settings.py +0 -0
  35. {stjames-0.0.42 → stjames-0.0.44}/stjames/int_settings.py +0 -0
  36. {stjames-0.0.42 → stjames-0.0.44}/stjames/message.py +0 -0
  37. {stjames-0.0.42 → stjames-0.0.44}/stjames/method.py +0 -0
  38. {stjames-0.0.42 → stjames-0.0.44}/stjames/mode.py +0 -0
  39. {stjames-0.0.42 → stjames-0.0.44}/stjames/opt_settings.py +0 -0
  40. {stjames-0.0.42 → stjames-0.0.44}/stjames/periodic_cell.py +0 -0
  41. {stjames-0.0.42 → stjames-0.0.44}/stjames/py.typed +0 -0
  42. {stjames-0.0.42 → stjames-0.0.44}/stjames/scf_settings.py +0 -0
  43. {stjames-0.0.42 → stjames-0.0.44}/stjames/solvent.py +0 -0
  44. {stjames-0.0.42 → stjames-0.0.44}/stjames/status.py +0 -0
  45. {stjames-0.0.42 → stjames-0.0.44}/stjames/task.py +0 -0
  46. {stjames-0.0.42 → stjames-0.0.44}/stjames/thermochem_settings.py +0 -0
  47. {stjames-0.0.42 → stjames-0.0.44}/stjames/types.py +0 -0
  48. {stjames-0.0.42 → stjames-0.0.44}/stjames/workflows/__init__.py +0 -0
  49. {stjames-0.0.42 → stjames-0.0.44}/stjames/workflows/admet.py +0 -0
  50. {stjames-0.0.42 → stjames-0.0.44}/stjames/workflows/basic_calculation.py +0 -0
  51. {stjames-0.0.42 → stjames-0.0.44}/stjames/workflows/conformer.py +0 -0
  52. {stjames-0.0.42 → stjames-0.0.44}/stjames/workflows/descriptors.py +0 -0
  53. {stjames-0.0.42 → stjames-0.0.44}/stjames/workflows/fukui.py +0 -0
  54. {stjames-0.0.42 → stjames-0.0.44}/stjames/workflows/pka.py +0 -0
  55. {stjames-0.0.42 → stjames-0.0.44}/stjames/workflows/redox_potential.py +0 -0
  56. {stjames-0.0.42 → stjames-0.0.44}/stjames/workflows/scan.py +0 -0
  57. {stjames-0.0.42 → stjames-0.0.44}/stjames/workflows/tautomer.py +0 -0
  58. {stjames-0.0.42 → stjames-0.0.44}/stjames.egg-info/dependency_links.txt +0 -0
  59. {stjames-0.0.42 → stjames-0.0.44}/stjames.egg-info/requires.txt +0 -0
  60. {stjames-0.0.42 → stjames-0.0.44}/stjames.egg-info/top_level.txt +0 -0
  61. {stjames-0.0.42 → stjames-0.0.44}/tests/test_molecule.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: stjames
3
- Version: 0.0.42
3
+ Version: 0.0.44
4
4
  Summary: standardized JSON atom/molecule encoding scheme
5
5
  Author-email: Corin Wagen <corin@rowansci.com>
6
6
  Project-URL: Homepage, https://github.com/rowansci/stjames
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "stjames"
3
- version = "0.0.42"
3
+ version = "0.0.44"
4
4
  description = "standardized JSON atom/molecule encoding scheme"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -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 Exception as e:
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, mol: Molecule) -> Molecule:
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 mol
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 | Mode.RAPID:
163
- # Default off
164
- self.optimize_fragments = self.optimize_fragments or False
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 _:
@@ -56,5 +56,7 @@ class MolecularDynamicsWorkflow(Workflow):
56
56
  calc_settings: Settings
57
57
  calc_engine: str | None = None
58
58
 
59
+ save_interval: PositiveInt = 10
60
+
59
61
  # uuids of scan points
60
62
  frames: list[Frame] = []
@@ -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//B97-3c with GFN2-xTB pre-opt
25
+ wB97X-3c//r²SCAN-3c with GFN2-xTB pre-opt
26
26
  METICULOUS
27
- wB97M-D3BJ/def2-TZVPPD//wB97X-3c//B97-3c with GFN2-xTB pre-opt
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.B973C, solvent=self.solvent, freq=self.frequencies),
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.B973C, solvent=self.solvent),
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 BaseModel, ConfigDict
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
- Input for a workflow.
12
+ Base class for Workflows.
26
13
 
27
14
  :param initial_molecule: Molecule of interest
28
- :param mode: Mode for workflow
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
- model_config = ConfigDict(extra="forbid", frozen=True)
39
+ class DBCalculation(Base):
40
+ """Encodes a calculation that's in the database. This isn't terribly useful by itself."""
41
+
42
+ uuid: UUID
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: stjames
3
- Version: 0.0.42
3
+ Version: 0.0.44
4
4
  Summary: standardized JSON atom/molecule encoding scheme
5
5
  Author-email: Corin Wagen <corin@rowansci.com>
6
6
  Project-URL: Homepage, https://github.com/rowansci/stjames
@@ -53,4 +53,6 @@ stjames/workflows/scan.py
53
53
  stjames/workflows/spin_states.py
54
54
  stjames/workflows/tautomer.py
55
55
  stjames/workflows/workflow.py
56
- tests/test_molecule.py
56
+ tests/test_from_extxyz.py
57
+ tests/test_molecule.py
58
+ tests/test_settings.py
@@ -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