stjames 0.0.43__tar.gz → 0.0.45__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 (67) hide show
  1. {stjames-0.0.43/stjames.egg-info → stjames-0.0.45}/PKG-INFO +1 -1
  2. {stjames-0.0.43 → stjames-0.0.45}/pyproject.toml +1 -1
  3. stjames-0.0.45/stjames/constraint.py +70 -0
  4. {stjames-0.0.43 → stjames-0.0.45}/stjames/correction.py +3 -0
  5. stjames-0.0.45/stjames/method.py +60 -0
  6. {stjames-0.0.43 → stjames-0.0.45}/stjames/molecule.py +7 -7
  7. stjames-0.0.45/stjames/settings.py +216 -0
  8. {stjames-0.0.43 → stjames-0.0.45}/stjames/types.py +2 -0
  9. {stjames-0.0.43 → stjames-0.0.45}/stjames/workflows/__init__.py +1 -0
  10. {stjames-0.0.43 → stjames-0.0.45}/stjames/workflows/bde.py +18 -25
  11. stjames-0.0.45/stjames/workflows/conformer_search.py +345 -0
  12. stjames-0.0.45/stjames/workflows/electronic_properties.py +86 -0
  13. {stjames-0.0.43 → stjames-0.0.45}/stjames/workflows/molecular_dynamics.py +12 -3
  14. {stjames-0.0.43 → stjames-0.0.45}/stjames/workflows/multistage_opt.py +91 -14
  15. stjames-0.0.45/stjames/workflows/redox_potential.py +102 -0
  16. {stjames-0.0.43 → stjames-0.0.45}/stjames/workflows/spin_states.py +2 -14
  17. {stjames-0.0.43 → stjames-0.0.45}/stjames/workflows/workflow.py +18 -22
  18. {stjames-0.0.43 → stjames-0.0.45/stjames.egg-info}/PKG-INFO +1 -1
  19. {stjames-0.0.43 → stjames-0.0.45}/stjames.egg-info/SOURCES.txt +5 -1
  20. stjames-0.0.45/tests/test_constraints.py +29 -0
  21. stjames-0.0.45/tests/test_settings.py +34 -0
  22. stjames-0.0.43/stjames/constraint.py +0 -36
  23. stjames-0.0.43/stjames/method.py +0 -71
  24. stjames-0.0.43/stjames/settings.py +0 -209
  25. stjames-0.0.43/stjames/workflows/redox_potential.py +0 -38
  26. {stjames-0.0.43 → stjames-0.0.45}/LICENSE +0 -0
  27. {stjames-0.0.43 → stjames-0.0.45}/README.md +0 -0
  28. {stjames-0.0.43 → stjames-0.0.45}/setup.cfg +0 -0
  29. {stjames-0.0.43 → stjames-0.0.45}/stjames/__init__.py +0 -0
  30. {stjames-0.0.43 → stjames-0.0.45}/stjames/_deprecated_solvent_settings.py +0 -0
  31. {stjames-0.0.43 → stjames-0.0.45}/stjames/atom.py +0 -0
  32. {stjames-0.0.43 → stjames-0.0.45}/stjames/base.py +0 -0
  33. {stjames-0.0.43 → stjames-0.0.45}/stjames/basis_set.py +0 -0
  34. {stjames-0.0.43 → stjames-0.0.45}/stjames/calculation.py +0 -0
  35. {stjames-0.0.43 → stjames-0.0.45}/stjames/data/__init__.py +0 -0
  36. {stjames-0.0.43 → stjames-0.0.45}/stjames/data/bragg_radii.json +0 -0
  37. {stjames-0.0.43 → stjames-0.0.45}/stjames/data/elements.py +0 -0
  38. {stjames-0.0.43 → stjames-0.0.45}/stjames/data/isotopes.json +0 -0
  39. {stjames-0.0.43 → stjames-0.0.45}/stjames/data/nist_isotopes.json +0 -0
  40. {stjames-0.0.43 → stjames-0.0.45}/stjames/data/read_nist_isotopes.py +0 -0
  41. {stjames-0.0.43 → stjames-0.0.45}/stjames/data/symbol_element.json +0 -0
  42. {stjames-0.0.43 → stjames-0.0.45}/stjames/diis_settings.py +0 -0
  43. {stjames-0.0.43 → stjames-0.0.45}/stjames/grid_settings.py +0 -0
  44. {stjames-0.0.43 → stjames-0.0.45}/stjames/int_settings.py +0 -0
  45. {stjames-0.0.43 → stjames-0.0.45}/stjames/message.py +0 -0
  46. {stjames-0.0.43 → stjames-0.0.45}/stjames/mode.py +0 -0
  47. {stjames-0.0.43 → stjames-0.0.45}/stjames/opt_settings.py +0 -0
  48. {stjames-0.0.43 → stjames-0.0.45}/stjames/periodic_cell.py +0 -0
  49. {stjames-0.0.43 → stjames-0.0.45}/stjames/py.typed +0 -0
  50. {stjames-0.0.43 → stjames-0.0.45}/stjames/scf_settings.py +0 -0
  51. {stjames-0.0.43 → stjames-0.0.45}/stjames/solvent.py +0 -0
  52. {stjames-0.0.43 → stjames-0.0.45}/stjames/status.py +0 -0
  53. {stjames-0.0.43 → stjames-0.0.45}/stjames/task.py +0 -0
  54. {stjames-0.0.43 → stjames-0.0.45}/stjames/thermochem_settings.py +0 -0
  55. {stjames-0.0.43 → stjames-0.0.45}/stjames/workflows/admet.py +0 -0
  56. {stjames-0.0.43 → stjames-0.0.45}/stjames/workflows/basic_calculation.py +0 -0
  57. {stjames-0.0.43 → stjames-0.0.45}/stjames/workflows/conformer.py +0 -0
  58. {stjames-0.0.43 → stjames-0.0.45}/stjames/workflows/descriptors.py +0 -0
  59. {stjames-0.0.43 → stjames-0.0.45}/stjames/workflows/fukui.py +0 -0
  60. {stjames-0.0.43 → stjames-0.0.45}/stjames/workflows/pka.py +0 -0
  61. {stjames-0.0.43 → stjames-0.0.45}/stjames/workflows/scan.py +0 -0
  62. {stjames-0.0.43 → stjames-0.0.45}/stjames/workflows/tautomer.py +0 -0
  63. {stjames-0.0.43 → stjames-0.0.45}/stjames.egg-info/dependency_links.txt +0 -0
  64. {stjames-0.0.43 → stjames-0.0.45}/stjames.egg-info/requires.txt +0 -0
  65. {stjames-0.0.43 → stjames-0.0.45}/stjames.egg-info/top_level.txt +0 -0
  66. {stjames-0.0.43 → stjames-0.0.45}/tests/test_from_extxyz.py +0 -0
  67. {stjames-0.0.43 → stjames-0.0.45}/tests/test_molecule.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: stjames
3
- Version: 0.0.43
3
+ Version: 0.0.45
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.43"
3
+ version = "0.0.45"
4
4
  description = "standardized JSON atom/molecule encoding scheme"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -0,0 +1,70 @@
1
+ from typing import Optional, Self
2
+
3
+ from pydantic import PositiveFloat, PositiveInt, model_validator
4
+
5
+ from .base import Base, LowercaseStrEnum
6
+
7
+
8
+ class ConstraintType(LowercaseStrEnum):
9
+ """Different sorts of constraints."""
10
+
11
+ BOND = "bond"
12
+ ANGLE = "angle"
13
+ DIHEDRAL = "dihedral"
14
+
15
+
16
+ class Constraint(Base):
17
+ """
18
+ Represents a single (absolute) constraint.
19
+
20
+ :param constraint_type: which type
21
+ :param atoms: the atoms in question
22
+ :param value: the value to constrain this to, leaving this blank sets the current value
23
+ """
24
+
25
+ constraint_type: ConstraintType
26
+ atoms: list[PositiveInt] # 1-indexed
27
+ value: Optional[float] = None
28
+
29
+ @model_validator(mode="after")
30
+ def check_atom_list_length(self) -> Self:
31
+ match self.constraint_type:
32
+ case ConstraintType.BOND:
33
+ if len(self.atoms) != 2:
34
+ raise ValueError("Bond constraint needs two atom indices!")
35
+ case ConstraintType.ANGLE:
36
+ if len(self.atoms) != 3:
37
+ raise ValueError("Angle constraint needs three atom indices!")
38
+ case ConstraintType.DIHEDRAL:
39
+ if len(self.atoms) != 4:
40
+ raise ValueError("Dihedral constraint needs four atom indices!")
41
+ case _:
42
+ raise ValueError("Unknown constraint_type!")
43
+
44
+ return self
45
+
46
+
47
+ class PairwiseHarmonicConstraint(Base):
48
+ """
49
+ Represents a harmonic constraint, with a characteristic spring constant.
50
+
51
+ :param atoms: whch atoms to apply to
52
+ :param force_constant: the strength of the attraction, in kcal/mol/Å
53
+ :param equilibrium: the distance at which force is zero
54
+ """
55
+
56
+ atoms: tuple[PositiveInt, PositiveInt] # 1-indexed
57
+ force_constant: PositiveFloat # kcal/mol / Å**2
58
+ equilibrium: PositiveFloat # Å
59
+
60
+
61
+ class SphericalHarmonicConstraint(Base):
62
+ """
63
+ Represents a spherical harmonic constraint to keep a system near the origin.
64
+
65
+ :param confining radius: the confining radius, in Å
66
+ :param force_constant: the strength of the confinement, in kcal/mol/Å
67
+ """
68
+
69
+ confining_radius: PositiveFloat
70
+ force_constant: PositiveFloat = 10 # kcal/mol / Å**2
@@ -10,5 +10,8 @@ class Correction(LowercaseStrEnum):
10
10
  # Grimme's D3 dispersion correction, *without* Becke–Johnson damping
11
11
  D3 = "d3"
12
12
 
13
+ # Grimme's D4 dispersion correction
14
+ D4 = "d4"
15
+
13
16
  # Grimme's geometric counterpoise correction
14
17
  GCP = "gcp"
@@ -0,0 +1,60 @@
1
+ from typing import Literal
2
+
3
+ from .base import LowercaseStrEnum
4
+
5
+
6
+ class Method(LowercaseStrEnum):
7
+ HARTREE_FOCK = "hf"
8
+ HF3C = "hf_3c"
9
+
10
+ PBE = "pbe"
11
+ B973C = "b97_3c"
12
+ B97D3BJ = "b97_d3bj"
13
+ R2SCAN = "r2scan"
14
+ R2SCAN3C = "r2scan_3c"
15
+ TPSS = "tpss"
16
+ M06L = "m06l"
17
+
18
+ PBE0 = "pbe0"
19
+ B3LYP = "b3lyp"
20
+ TPSSH = "tpssh"
21
+ M06 = "m06"
22
+ M062X = "m062x"
23
+
24
+ CAMB3LYP = "camb3lyp"
25
+ WB97XD3 = "wb97x_d3"
26
+ WB97XV = "wb97x_v"
27
+ WB97MV = "wb97m_v"
28
+ WB97MD3BJ = "wb97m_d3bj"
29
+ WB97X3C = "wb97x_3c"
30
+
31
+ DSDBLYPD3BJ = "dsd_blyp_d3bj"
32
+
33
+ AIMNET2_WB97MD3 = "aimnet2_wb97md3"
34
+ MACE_MP_0 = "mace_mp_0"
35
+ OCP24_S = "ocp24_s"
36
+ OCP24_L = "ocp24_l"
37
+
38
+ GFN_FF = "gfn_ff"
39
+ GFN0_XTB = "gfn0_xtb"
40
+ GFN1_XTB = "gfn1_xtb"
41
+ GFN2_XTB = "gfn2_xtb"
42
+
43
+ # this was going to be removed, but Jonathon wrote such a nice basis set test... it's off the front end.
44
+ BP86 = "bp86"
45
+
46
+
47
+ NNPMethod = Literal[Method.AIMNET2_WB97MD3]
48
+ NNP_METHODS = [Method.AIMNET2_WB97MD3]
49
+
50
+ XTBMethod = Literal[Method.GFN_FF, Method.GFN0_XTB, Method.GFN1_XTB, Method.GFN2_XTB]
51
+ XTB_METHODS = [Method.GFN_FF, Method.GFN0_XTB, Method.GFN1_XTB, Method.GFN2_XTB]
52
+
53
+ CompositeMethod = Literal[Method.HF3C, Method.B973C, Method.R2SCAN3C, Method.WB97X3C]
54
+ COMPOSITE_METHODS = [Method.HF3C, Method.B973C, Method.R2SCAN3C, Method.WB97X3C]
55
+
56
+ PrepackagedMethod = XTBMethod | CompositeMethod | NNPMethod
57
+ PREPACKAGED_METHODS = [*XTB_METHODS, *COMPOSITE_METHODS]
58
+
59
+ MethodWithCorrection = Literal[Method.WB97XD3, Method.WB97XV, Method.WB97MV, Method.WB97MD3BJ, Method.DSDBLYPD3BJ]
60
+ METHODS_WITH_CORRECTION = [Method.WB97XD3, Method.WB97XV, Method.WB97MV, Method.WB97MD3BJ, Method.DSDBLYPD3BJ, Method.B97D3BJ]
@@ -8,7 +8,7 @@ from pydantic import NonNegativeInt, PositiveInt, ValidationError
8
8
  from .atom import Atom
9
9
  from .base import Base
10
10
  from .periodic_cell import PeriodicCell
11
- from .types import Matrix3x3, Vector3D, Vector3DPerAtom
11
+ from .types import FloatPerAtom, Matrix3x3, Vector3D, Vector3DPerAtom
12
12
 
13
13
 
14
14
  class MoleculeReadError(RuntimeError):
@@ -18,10 +18,8 @@ class MoleculeReadError(RuntimeError):
18
18
  class VibrationalMode(Base):
19
19
  frequency: float # in cm-1
20
20
  reduced_mass: float # amu
21
-
22
- # todo - check units here?
23
- force_constant: float
24
- displacements: Vector3DPerAtom
21
+ force_constant: float # mDyne/Å
22
+ displacements: Vector3DPerAtom # Å
25
23
 
26
24
 
27
25
  class Molecule(Base):
@@ -44,8 +42,8 @@ class Molecule(Base):
44
42
 
45
43
  velocities: Optional[Vector3DPerAtom] = None # Å/fs
46
44
 
47
- mulliken_charges: Optional[list[float]] = None
48
- mulliken_spin_densities: Optional[list[float]] = None
45
+ mulliken_charges: FloatPerAtom | None = None
46
+ mulliken_spin_densities: FloatPerAtom | None = None
49
47
  dipole: Optional[Vector3D] = None # in Debye
50
48
 
51
49
  vibrational_modes: Optional[list[VibrationalMode]] = None
@@ -55,6 +53,8 @@ class Molecule(Base):
55
53
  thermal_enthalpy_corr: Optional[float] = None
56
54
  thermal_free_energy_corr: Optional[float] = None
57
55
 
56
+ smiles: Optional[str] = None
57
+
58
58
  def __len__(self) -> int:
59
59
  return len(self.atoms)
60
60
 
@@ -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
@@ -5,4 +5,6 @@ UUID: TypeAlias = str
5
5
  Vector3D: TypeAlias = tuple[float, float, float]
6
6
  Vector3DPerAtom: TypeAlias = list[Vector3D]
7
7
 
8
+ FloatPerAtom: TypeAlias = list[float]
9
+
8
10
  Matrix3x3: TypeAlias = tuple[Vector3D, Vector3D, Vector3D]
@@ -2,6 +2,7 @@ from .admet import *
2
2
  from .basic_calculation import *
3
3
  from .bde import *
4
4
  from .conformer import *
5
+ from .conformer_search import *
5
6
  from .descriptors import *
6
7
  from .fukui import *
7
8
  from .molecular_dynamics import *
@@ -56,8 +56,9 @@ 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
- :param solvent: solvent to use
61
+ :param solvent: solvent to use for singlepoint
61
62
  :param xtb_preopt: pre-optimize with xtb (sets based on mode when None)
62
63
 
63
64
  Overridden:
@@ -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,14 +106,9 @@ 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}>"
109
+ @property
110
+ def level_of_theory(self) -> str:
111
+ return self.multistage_opt_settings.level_of_theory
118
112
 
119
113
  @property
120
114
  def energies(self) -> tuple[float | None, ...]:
@@ -128,22 +122,21 @@ class BDEWorkflow(Workflow, MultiStageOptMixin):
128
122
 
129
123
  return value
130
124
 
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
125
  @field_validator("initial_molecule", mode="before")
140
126
  @classmethod
141
- def no_charge_or_spin(cls, mol: Molecule) -> Molecule:
127
+ def no_charge_or_spin(cls, val: Molecule | dict[str, Any]) -> Molecule | dict[str, Any]:
142
128
  """Ensure the molecule has no charge or spin."""
129
+ if isinstance(val, dict):
130
+ mol = Molecule(**val)
131
+ elif isinstance(val, Molecule):
132
+ mol = val
133
+ else:
134
+ raise ValueError(f"{val=} is not a Molecule.")
135
+
143
136
  if mol.charge != 0 or mol.multiplicity != 1:
144
137
  raise ValueError("Charge and spin partitioning undefined for BDE, only neutral singlet molecules supported.")
145
138
 
146
- return mol
139
+ return val
147
140
 
148
141
  @model_validator(mode="before")
149
142
  @classmethod
@@ -159,10 +152,10 @@ class BDEWorkflow(Workflow, MultiStageOptMixin):
159
152
  self.fragment_indices = tuple(map(tuple, self.fragment_indices))
160
153
 
161
154
  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:
155
+ case Mode.RECKLESS:
156
+ # GFN-FF doesn't support open-shell species
157
+ self.optimize_fragments = False
158
+ case Mode.RAPID | Mode.CAREFUL | Mode.METICULOUS:
166
159
  # Default on
167
160
  self.optimize_fragments = self.optimize_fragments or self.optimize_fragments is None
168
161
  case _: