stjames 0.0.44__py3-none-any.whl → 0.0.46__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of stjames might be problematic. Click here for more details.

stjames/constraint.py CHANGED
@@ -1,4 +1,6 @@
1
- from pydantic import PositiveFloat, PositiveInt
1
+ from typing import Optional, Self
2
+
3
+ from pydantic import PositiveFloat, PositiveInt, model_validator
2
4
 
3
5
  from .base import Base, LowercaseStrEnum
4
6
 
@@ -9,28 +11,64 @@ class ConstraintType(LowercaseStrEnum):
9
11
  BOND = "bond"
10
12
  ANGLE = "angle"
11
13
  DIHEDRAL = "dihedral"
14
+ FREEZE_ATOMS = "freeze_atoms"
12
15
 
13
16
 
14
17
  class Constraint(Base):
15
- """Represents a single (absolute) constraint."""
18
+ """
19
+ Represents a single (absolute) constraint.
20
+
21
+ :param constraint_type: which type
22
+ :param atoms: the atoms in question. n.b. - these are 1-indexed!
23
+ :param value: the value to constrain this to, leaving this blank sets the current value
24
+ """
16
25
 
17
26
  constraint_type: ConstraintType
18
27
  atoms: list[PositiveInt] # 1-indexed
28
+ value: Optional[float] = None
29
+
30
+ @model_validator(mode="after")
31
+ def check_atom_list_length(self) -> Self:
32
+ match self.constraint_type:
33
+ case ConstraintType.BOND:
34
+ if len(self.atoms) != 2:
35
+ raise ValueError("Bond constraint needs two atom indices!")
36
+ case ConstraintType.ANGLE:
37
+ if len(self.atoms) != 3:
38
+ raise ValueError("Angle constraint needs three atom indices!")
39
+ case ConstraintType.DIHEDRAL:
40
+ if len(self.atoms) != 4:
41
+ raise ValueError("Dihedral constraint needs four atom indices!")
42
+ case ConstraintType.FREEZE_ATOMS:
43
+ if len(self.atoms) == 0:
44
+ raise ValueError("Can't freeze atoms without any atoms to freeze!")
45
+ case _:
46
+ raise ValueError("Unknown constraint_type!")
47
+
48
+ return self
19
49
 
20
50
 
21
51
  class PairwiseHarmonicConstraint(Base):
22
52
  """
23
53
  Represents a harmonic constraint, with a characteristic spring constant.
54
+
55
+ :param atoms: which atoms to apply to
56
+ :param force_constant: the strength of the attraction, in kcal/mol/Å
57
+ :param equilibrium: the distance at which force is zero
24
58
  """
25
59
 
26
60
  atoms: tuple[PositiveInt, PositiveInt] # 1-indexed
27
- spring_constant: PositiveFloat # kcal/mol / Å**2
61
+ force_constant: PositiveFloat # kcal/mol / Å**2
62
+ equilibrium: PositiveFloat # Å
28
63
 
29
64
 
30
65
  class SphericalHarmonicConstraint(Base):
31
66
  """
32
67
  Represents a spherical harmonic constraint to keep a system near the origin.
68
+
69
+ :param confining radius: the confining radius, in Å
70
+ :param force_constant: the strength of the confinement, in kcal/mol/Å
33
71
  """
34
72
 
35
73
  confining_radius: PositiveFloat
36
- confining_force_constant: PositiveFloat = 10 # kcal/mol / Å**2
74
+ force_constant: PositiveFloat = 10 # kcal/mol / Å**2
stjames/correction.py CHANGED
@@ -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"
stjames/diis_settings.py CHANGED
@@ -4,7 +4,7 @@ from .base import Base, LowercaseStrEnum
4
4
 
5
5
 
6
6
  class DIISStrategy(LowercaseStrEnum):
7
- # regular pulay DIIS
7
+ # regular Pulay DIIS
8
8
  DIIS = "diis"
9
9
 
10
10
  # Hu/Yang JCP 2010 - ADIIS
stjames/method.py CHANGED
@@ -1,3 +1,5 @@
1
+ from typing import Literal
2
+
1
3
  from .base import LowercaseStrEnum
2
4
 
3
5
 
@@ -7,6 +9,7 @@ class Method(LowercaseStrEnum):
7
9
 
8
10
  PBE = "pbe"
9
11
  B973C = "b97_3c"
12
+ B97D3BJ = "b97_d3bj"
10
13
  R2SCAN = "r2scan"
11
14
  R2SCAN3C = "r2scan_3c"
12
15
  TPSS = "tpss"
@@ -28,6 +31,9 @@ class Method(LowercaseStrEnum):
28
31
  DSDBLYPD3BJ = "dsd_blyp_d3bj"
29
32
 
30
33
  AIMNET2_WB97MD3 = "aimnet2_wb97md3"
34
+ MACE_MP_0 = "mace_mp_0"
35
+ OCP24_S = "ocp24_s"
36
+ OCP24_L = "ocp24_l"
31
37
 
32
38
  GFN_FF = "gfn_ff"
33
39
  GFN0_XTB = "gfn0_xtb"
@@ -38,34 +44,17 @@ class Method(LowercaseStrEnum):
38
44
  BP86 = "bp86"
39
45
 
40
46
 
41
- MLFF = [
42
- Method.AIMNET2_WB97MD3,
43
- ]
44
-
45
- XTB_METHODS = [
46
- Method.GFN_FF,
47
- Method.GFN0_XTB,
48
- Method.GFN1_XTB,
49
- Method.GFN2_XTB,
50
- ]
51
-
52
- COMPOSITE_METHODS = [
53
- Method.HF3C,
54
- Method.B973C,
55
- Method.R2SCAN3C,
56
- Method.WB97X3C,
57
- ]
58
-
59
- PREPACKAGED_METHODS = [
60
- *MLFF,
61
- *XTB_METHODS,
62
- *COMPOSITE_METHODS,
63
- ]
64
-
65
- METHODS_WITH_CORRECTION = [
66
- Method.WB97XD3,
67
- Method.WB97XV,
68
- Method.WB97MV,
69
- Method.WB97MD3BJ,
70
- Method.DSDBLYPD3BJ,
71
- ]
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]
stjames/molecule.py CHANGED
@@ -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
stjames/scf_settings.py CHANGED
@@ -20,7 +20,7 @@ class SCFInitMethod(LowercaseStrEnum):
20
20
  class OrthonormalizationMethod(LowercaseStrEnum):
21
21
  SYMMETRIC = "symmetric"
22
22
  CANONICAL = "canonical"
23
- # cholesky, in future?
23
+ # Cholesky, in future?
24
24
 
25
25
 
26
26
  class SCFSettings(Base):
stjames/types.py CHANGED
@@ -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]
@@ -1,8 +1,13 @@
1
+ # ruff: noqa: F405
2
+ from typing import Literal
3
+
1
4
  from .admet import *
2
5
  from .basic_calculation import *
3
6
  from .bde import *
4
7
  from .conformer import *
8
+ from .conformer_search import *
5
9
  from .descriptors import *
10
+ from .electronic_properties import *
6
11
  from .fukui import *
7
12
  from .molecular_dynamics import *
8
13
  from .multistage_opt import *
@@ -11,3 +16,40 @@ from .redox_potential import *
11
16
  from .scan import *
12
17
  from .spin_states import *
13
18
  from .tautomer import *
19
+ from .workflow import Workflow
20
+
21
+ WORKFLOW_NAME = Literal[
22
+ "admet",
23
+ "basic_calculation",
24
+ "bde",
25
+ "conformers",
26
+ "conformer_search",
27
+ "descriptors",
28
+ "electronic_properties",
29
+ "fukui",
30
+ "molecular_dynamics",
31
+ "multistage_opt",
32
+ "pka",
33
+ "redox_potential",
34
+ "scan",
35
+ "spin_states",
36
+ "tautomers",
37
+ ]
38
+
39
+ WORKFLOW_MAPPING: dict[str, Workflow] = {
40
+ "admet": ADMETWorkflow, # type: ignore [dict-item]
41
+ "basic_calculation": BasicCalculationWorkflow, # type: ignore [dict-item]
42
+ "bde": BDEWorkflow, # type: ignore [dict-item]
43
+ "conformers": ConformerWorkflow, # type: ignore [dict-item]
44
+ "conformer_search": ConformerSearchWorkflow, # type: ignore [dict-item]
45
+ "descriptors": DescriptorsWorkflow, # type: ignore [dict-item]
46
+ "electronic_properties": ElectronicPropertiesWorkflow, # type: ignore [dict-item]
47
+ "fukui": FukuiIndexWorkflow, # type: ignore [dict-item]
48
+ "molecular_dynamics": MolecularDynamicsWorkflow, # type: ignore [dict-item]
49
+ "multistage_opt": MultiStageOptWorkflow, # type: ignore [dict-item]
50
+ "pka": pKaWorkflow, # type: ignore [dict-item]
51
+ "redox_potential": RedoxPotentialWorkflow, # type: ignore [dict-item]
52
+ "scan": ScanWorkflow, # type: ignore [dict-item]
53
+ "spin_states": SpinStatesWorkflow, # type: ignore [dict-item]
54
+ "tautomers": TautomerWorkflow, # type: ignore [dict-item]
55
+ }
stjames/workflows/bde.py CHANGED
@@ -58,12 +58,12 @@ class BDEWorkflow(Workflow, MultiStageOptMixin):
58
58
  :param initial_molecule: Molecule of interest
59
59
  :param mode: Mode for workflow
60
60
  :param multistage_opt_settings: set by mode unless mode=MANUAL (ignores additional settings if set)
61
- :param solvent: solvent to use
61
+ :param solvent: solvent to use for singlepoint
62
62
  :param xtb_preopt: pre-optimize with xtb (sets based on mode when None)
63
+ :param frequencies: whether to calculate frequencies
63
64
 
64
65
  Overridden:
65
66
  :param mso_mode: Mode for MultiStageOptSettings
66
- :param frequencies: whether to calculate frequencies
67
67
 
68
68
  Turned off:
69
69
  :param constraints: constraints to add (not supported)
@@ -81,7 +81,6 @@ class BDEWorkflow(Workflow, MultiStageOptMixin):
81
81
  """
82
82
 
83
83
  mso_mode: Mode = _sentinel_mso_mode # type: ignore [assignment]
84
- frequencies: bool = False
85
84
  optimize_fragments: bool = None # type: ignore [assignment]
86
85
 
87
86
  atoms: tuple[PositiveInt, ...] = Field(default_factory=tuple)
@@ -106,6 +105,10 @@ class BDEWorkflow(Workflow, MultiStageOptMixin):
106
105
  """
107
106
  return f"{type(self).__name__} {self.mode.name}\n" + "\n".join(map(str, self.fragment_indices))
108
107
 
108
+ @property
109
+ def level_of_theory(self) -> str:
110
+ return self.multistage_opt_settings.level_of_theory
111
+
109
112
  @property
110
113
  def energies(self) -> tuple[float | None, ...]:
111
114
  return tuple(bde.energy for bde in self.bdes)
@@ -0,0 +1,352 @@
1
+ """Conformer Search Workflow."""
2
+
3
+ from abc import ABC
4
+ from typing import Self, Sequence, TypeVar
5
+
6
+ from pydantic import BaseModel, Field, field_validator, model_validator
7
+
8
+ from ..base import LowercaseStrEnum
9
+ from ..constraint import Constraint
10
+ from ..method import Method, XTBMethod
11
+ from ..mode import Mode
12
+ from ..types import UUID
13
+ from .multistage_opt import MultiStageOptMixin
14
+ from .workflow import Workflow
15
+
16
+ _sentinel = object()
17
+
18
+ _T = TypeVar("_T")
19
+ _U = TypeVar("_U")
20
+
21
+
22
+ def check_sentinel(value: _T, default: _U) -> _T | _U:
23
+ """Return value unless _sentinel, then return default."""
24
+ return default if value is _sentinel else value
25
+
26
+
27
+ class ScreeningSettings(BaseModel):
28
+ """
29
+ Settings for determining unique and useful conformers.
30
+
31
+ :param energy_threshold: maximum relative energy for screening
32
+ :param rotational_constants_threshold: maximum difference in rotational constants for screening
33
+ :param rmsd: Cartesian RMSD for screening
34
+ :param max_confs: maximum number of conformers to keep
35
+ """
36
+
37
+ energy_threshold: float | None = None # kcal/mol
38
+ rotational_constants_threshold: float | None = 0.02
39
+ rmsd: float | None = 0.25
40
+ max_confs: int | None = None
41
+
42
+
43
+ class ConformerGenSettings(BaseModel):
44
+ """
45
+ Conformer generation settings.
46
+
47
+ Conformers are generated and an initial screening is performed to remove duplicates and high-energy conformers.
48
+
49
+ :param mode: Mode for calculations
50
+ :param conf_opt_method: method for the optimization
51
+ :param screening: post-generation screening settings
52
+ :param constraints: constraints for conformer generation
53
+ :param nci: add a constraining potential for non-covalent interactions
54
+ :param max_confs: maximum number of conformers to keep
55
+ """
56
+
57
+ mode: Mode = Mode.RAPID
58
+ conf_opt_method: XTBMethod = Method.GFN_FF
59
+ screening: ScreeningSettings | None = None
60
+ constraints: Sequence[Constraint] = tuple()
61
+ nci: bool = False
62
+ max_confs: int | None = None
63
+
64
+ def __str__(self) -> str:
65
+ """Return a string representation of the ConformerGenSettings."""
66
+ return repr(self)
67
+
68
+ def __repr__(self) -> str:
69
+ """Return a string representation of the ConformerGenSettings."""
70
+ return f"<{type(self).__name__} {self.mode.name}>"
71
+
72
+
73
+ class ETKDGSettings(ConformerGenSettings):
74
+ """
75
+ Settings for ETKDG conformer generation.
76
+
77
+ Inherited:
78
+ :param mode: Mode for calculations
79
+ :param conf_opt_method: method for the optimization
80
+ :param screening: post-generation screening settings
81
+ :param constraints: constraints for conformer generation
82
+ :param nci: add a constraining potential for non-covalent interactions (not supported in ETKDG)
83
+ :param max_confs: maximum number of conformers to keep
84
+
85
+ New:
86
+ :param num_initial_confs: number of initial conformers to generate
87
+ :param num_confs_considered: number of conformers to consider for optimization
88
+ :param num_confs_taken: number of final conformers to take
89
+ :param max_mmff_energy: MMFF energy cutoff
90
+ :param max_mmff_iterations: MMFF optimization iterations
91
+ """
92
+
93
+ num_initial_confs: int = 300
94
+ num_confs_considered: int = 100
95
+ max_mmff_iterations: int = 500
96
+ max_mmff_energy: float | None = 30
97
+
98
+ @field_validator("constraints")
99
+ def check_constraints(cls, constraints: Sequence[Constraint]) -> Sequence[Constraint]:
100
+ if constraints:
101
+ raise ValueError("ETKDG does not support constraints")
102
+
103
+ return tuple(constraints)
104
+
105
+ @field_validator("nci")
106
+ def check_nci(cls, nci: bool) -> bool:
107
+ if nci:
108
+ raise ValueError("ETKDG does not support NCI")
109
+
110
+ return nci
111
+
112
+ @model_validator(mode="after")
113
+ def validate_and_build(self) -> Self:
114
+ match self.mode:
115
+ case Mode.MANUAL:
116
+ pass
117
+ case Mode.RECKLESS:
118
+ self.num_initial_confs = 200
119
+ self.num_confs_considered = 50
120
+ self.max_confs = 20 if self.max_confs is None else self.max_confs
121
+ self.max_mmff_energy = 20
122
+ case Mode.RAPID:
123
+ self.max_confs = 50 if self.max_confs is None else self.max_confs
124
+ self.conf_opt_method = Method.GFN0_XTB
125
+ case _:
126
+ raise NotImplementedError(f"Unsupported mode: {self.mode}")
127
+
128
+ return self
129
+
130
+
131
+ class iMTDSpeeds(LowercaseStrEnum):
132
+ MEGAQUICK = "megaquick"
133
+ SUPERQUICK = "superquick"
134
+ QUICK = "quick"
135
+ NORMAL = "normal"
136
+ EXTENSIVE = "extensive"
137
+
138
+
139
+ class iMTDSettings(ConformerGenSettings, ABC):
140
+ """
141
+ Settings for iMTD style conformer generation.
142
+
143
+ RECKLESS:
144
+ - GFN-FF//MTD(GFN-FF)
145
+ - Megaquick
146
+ - No GC
147
+ - No rotamer metadynamics
148
+ - Energy window = 5.0
149
+ - Run scaling factor = 0.5
150
+ - 6 MTD runs
151
+ RAPID:
152
+ - GFN0//MTD(GFN-FF)
153
+ - Superquick
154
+ - No GC
155
+ - No rotamer metadynamics
156
+ - Energy window = 5.0
157
+ - Run scaling factor = 0.5
158
+ - 6 MTD runs
159
+ CAREFUL:
160
+ - GFN2//MTD(GFN-FF)
161
+ - Quick
162
+ - GC (for iMTD-GC)
163
+ - Rotamer metadynamics (for iMTD-GC)
164
+ - Energy window = 5.0
165
+ - Run scaling factor = 0.5
166
+ - 6 MTD runs
167
+ METICULOUS:
168
+ - GFN2//MTD(GFN2)
169
+ - "Normal"
170
+ - GC (for iMTD-GC)
171
+ - Rotamer metadynamics (for iMTD-GC)
172
+ - Energy window = 6.0
173
+ - Run scaling factor = 1
174
+ - 14 MTD runs (2 with extreme values)
175
+
176
+
177
+ See https://github.com/crest-lab/crest/blob/5ca82feb2ec4df30a0129db957163c934f085952/src/choose_settings.f90#L202
178
+ and https://github.com/crest-lab/crest/blob/5ca82feb2ec4df30a0129db957163c934f085952/src/confparse.f90#L825
179
+ for how quick, superquick, and megaquick are defined.
180
+
181
+ Additional notes:
182
+ Extensive mode
183
+ - GC
184
+ - Rotamer metadynamics
185
+ - Energy window = 8.0
186
+ - Run scaling factor = 2
187
+ - 14 MTD runs (2 with extreme values)
188
+
189
+ --NCI may switch things to QUICK?
190
+
191
+ Inherited:
192
+ :param mode: Mode for calculations
193
+ :param conf_opt_method: method for the optimization
194
+ :param screening: post-generation screening settings (not used)
195
+ :param constraints: constraints to add
196
+ :param nci: add an ellipsoide potential around the input structure
197
+ :param max_confs: maximum number of conformers to keep
198
+
199
+ New:
200
+ :param mtd_method: method for the metadynamics
201
+ :param speed: speed of the calculations (CREST specific setting)
202
+ :param reopt: re-optimize conformers (corrects for the lack of rotamer metadynamics and GC)
203
+ :param free_energy_weights: calculate frequencies and re-weight based on free energies
204
+ """
205
+
206
+ mtd_method: XTBMethod = Method.GFN_FF
207
+ mtd_runtype: str = "imtd-gc"
208
+
209
+ speed: iMTDSpeeds = iMTDSpeeds.QUICK
210
+ reopt: bool = _sentinel # type: ignore [assignment]
211
+ free_energy_weights: bool = False
212
+
213
+ @model_validator(mode="after")
214
+ def validate_and_build_imtdgc_settings(self) -> Self:
215
+ match self.mode:
216
+ case Mode.MANUAL:
217
+ if self.reopt is _sentinel:
218
+ raise ValueError("Must specify reopt with MANUAL mode")
219
+ case Mode.RECKLESS: # GFN-FF//MTD(GFN-FF)
220
+ self.max_confs = 20 if self.max_confs is None else self.max_confs
221
+ self.speed = iMTDSpeeds.MEGAQUICK
222
+ self.reopt = check_sentinel(self.reopt, True)
223
+ case Mode.RAPID: # GFN0//MTD(GFN-FF)
224
+ self.max_confs = 50 if self.max_confs is None else self.max_confs
225
+ self.speed = iMTDSpeeds.SUPERQUICK
226
+ self.conf_opt_method = Method.GFN0_XTB
227
+ self.reopt = check_sentinel(self.reopt, True)
228
+ case Mode.CAREFUL: # GFN2//MTD(GFN-FF)
229
+ self.speed = iMTDSpeeds.QUICK
230
+ self.conf_opt_method = Method.GFN2_XTB
231
+ self.reopt = check_sentinel(self.reopt, False)
232
+ case Mode.METICULOUS: # GFN2//MTD(GFN2)
233
+ self.speed = iMTDSpeeds.NORMAL
234
+ self.mtd_method = Method.GFN2_XTB
235
+ self.conf_opt_method = Method.GFN2_XTB
236
+ self.reopt = check_sentinel(self.reopt, False)
237
+ # case Mode.EXTREME: # GFN2//MTD(GFN2)
238
+ # self.mtd_method = Method.GFN2_XTB
239
+ # self.conf_opt_method = Method.GFN2_XTB
240
+ # self.speed = iMTDSpeeds.EXTENSIVE
241
+ # self.reopt = check_sentinel(self.reopt, False)
242
+ case _:
243
+ raise NotImplementedError(f"Unsupported mode: {self.mode}")
244
+
245
+ return self
246
+
247
+
248
+ class iMTDGCSettings(iMTDSettings):
249
+ run_type: str = "imtdgc"
250
+
251
+
252
+ class iMTDsMTDSettings(iMTDSettings):
253
+ run_type: str = "imtd-smtd"
254
+
255
+
256
+ class ConformerGenMixin(BaseModel):
257
+ """
258
+ Mixin for workflows that need conformer generation.
259
+
260
+ :param conf_gen_mode: Mode for calculations
261
+ :param conf_gen_settings: settings for conformer generation
262
+ :param constraints: constraints to add
263
+ :param nci: add a constraining potential for non-covalent interactions
264
+ :param max_confs: maximum number of conformers to keep
265
+ """
266
+
267
+ conf_gen_mode: Mode = Mode.RAPID
268
+ conf_gen_settings: ConformerGenSettings = _sentinel # type: ignore [assignment]
269
+ constraints: Sequence[Constraint] = tuple()
270
+ nci: bool = False
271
+ max_confs: int | None = None
272
+
273
+ @model_validator(mode="after")
274
+ def validate_and_build_conf_gen_settings(self) -> Self:
275
+ """Validate and build the ConformerGenSettings."""
276
+ if self.conf_gen_settings is not _sentinel and self.conf_gen_mode != Mode.MANUAL:
277
+ raise ValueError("Cannot specify conf_gen_settings with non-MANUAL mode")
278
+
279
+ match self.conf_gen_mode:
280
+ case Mode.MANUAL:
281
+ if self.conf_gen_settings is _sentinel:
282
+ raise ValueError("Must specify conf_gen_settings with MANUAL mode")
283
+
284
+ case Mode.RECKLESS | Mode.RAPID:
285
+ # ETKDGSettings will error if constraints or nci are set
286
+ self.conf_gen_settings = ETKDGSettings(mode=self.conf_gen_mode, constraints=self.constraints, nci=self.nci, max_confs=self.max_confs)
287
+ case Mode.CAREFUL | Mode.METICULOUS:
288
+ self.conf_gen_settings = iMTDSettings(mode=self.conf_gen_mode, constraints=self.constraints, nci=self.nci, max_confs=self.max_confs)
289
+
290
+ case _:
291
+ raise NotImplementedError(f"Unsupported mode: {self.conf_gen_mode}")
292
+
293
+ return self
294
+
295
+
296
+ class ConformerSearchMixin(ConformerGenMixin, MultiStageOptMixin):
297
+ """
298
+ Mixin for workflows that need conformer search—a combination of conformer generation and optimization.
299
+
300
+ Inherited (ConformerGenMixin):
301
+ :param conf_gen_mode: Mode for conformer generation
302
+ :param mso_mode: Mode for MultiStageOptSettings
303
+ :param conf_gen_settings: settings for conformer generation
304
+ :param nci: add a constraining potential for non-covalent interactions
305
+
306
+ Inherited (MultiStageOptMixin):
307
+ :param multistage_opt_settings: set by mso_mode unless mode=MANUAL (ignores additional settings if set)
308
+ :param solvent: solvent to use
309
+ :param xtb_preopt: pre-optimize with xtb (sets based on mode when None)
310
+ :param transition_state: whether this is a transition state
311
+ :param frequencies: whether to calculate frequencies
312
+
313
+ Inherited (Both):
314
+ :param constraints: constraints to add (diamond inheritance, works as expected)
315
+ """
316
+
317
+ def __str__(self) -> str:
318
+ """Return a string representation of the ConformerSearch workflow."""
319
+ return repr(self)
320
+
321
+ def __repr__(self) -> str:
322
+ """Return a string representation of the ConformerSearch workflow."""
323
+ return f"<{type(self).__name__} {self.conf_gen_mode.name} {self.mso_mode.name}>"
324
+
325
+
326
+ class ConformerSearchWorkflow(ConformerSearchMixin, Workflow):
327
+ """
328
+ ConformerSearch Workflow.
329
+
330
+ Inherited:
331
+ :param initial_molecule: Molecule of interest
332
+ :param conf_gen_mode: Mode for calculations
333
+ :param conf_gen_settings: settings for conformer generation
334
+ :param mso_mode: Mode for MultiStageOptSettings
335
+ :param multistage_opt_settings: set by mode unless mode=MANUAL (ignores additional settings if set)
336
+ :param solvent: solvent to use
337
+ :param xtb_preopt: pre-optimize with xtb (sets based on mode when None)
338
+ :param constraints: constraints to add
339
+ :param transition_state: whether this is a transition state
340
+ :param frequencies: whether to calculate frequencies
341
+
342
+ Ignored:
343
+ :param mode: Mode to use (not used)
344
+
345
+ New:
346
+ :param conformer_uuids: list of UUIDs of the Molecules generated
347
+ :param energies: energies of the molecules
348
+ """
349
+
350
+ # Results
351
+ conformer_uuids: list[list[UUID | None]] = Field(default_factory=list)
352
+ energies: list[float] = Field(default_factory=list)
@@ -5,7 +5,7 @@ Descriptors = dict[str, dict[str, float] | tuple[float | None, ...] | float]
5
5
 
6
6
 
7
7
  class DescriptorsWorkflow(Workflow):
8
- # uuid of optimization
8
+ # UUID of optimization
9
9
  optimization: UUID | None = None
10
10
 
11
11
  descriptors: Descriptors | None = None
@@ -0,0 +1,96 @@
1
+ from pydantic import NonNegativeFloat, NonNegativeInt
2
+
3
+ from ..base import Base
4
+ from ..settings import Settings
5
+ from ..types import UUID, FloatPerAtom, Matrix3x3, Vector3D
6
+ from .workflow import Workflow
7
+
8
+
9
+ class PropertyCubePoint(Base):
10
+ x: float
11
+ y: float
12
+ z: float
13
+ val: float
14
+
15
+
16
+ class PropertyCube(Base):
17
+ """Represents a "cubefile" of some property."""
18
+
19
+ cube_points: list[PropertyCubePoint]
20
+
21
+
22
+ class MolecularOrbitalCube(PropertyCube):
23
+ """
24
+ Cube of a molecular orbital.
25
+
26
+ Inherits `cube_data`.
27
+ """
28
+
29
+ occupation: NonNegativeInt
30
+ energy: float
31
+
32
+
33
+ class ElectronicPropertiesWorkflow(Workflow):
34
+ """
35
+ Workflow for computing electronic properties.
36
+
37
+ Inherited
38
+ :param initial_molecule: Molecule of interest
39
+
40
+ Config settings:
41
+ :param settings: settings for the calculation
42
+ :param compute_density_cube: whether to compute the density cube
43
+ :param compute_electrostatic_potential_cube: whether to compute the electrostatic potential cube
44
+ :param compute_num_occupied_orbitals: number of occupied orbitals to save
45
+ :param compute_num_virtual_orbitals: number of virtual orbitals to save
46
+
47
+ Populated while running:
48
+ :param calc_uuid: UUID of the calculation
49
+ :param dipole: dipole moment
50
+ :param quadrupole: quadrupole moment
51
+ :param lowdin_charges: Löwdin charges
52
+ :param mulliken_charges: Mulliken charges
53
+ :param wiberg_bond_orders: Wiberg bond orders (`atom1`, `atom2`, `order`)
54
+ :param mayer_bond_orders: Mayer bond orders (`atom1`, `atom2`, `order`)
55
+
56
+ :param density_cube: electron density, as a cube
57
+ :param density_cube_alpha: α electron density, as a cube
58
+ :param density_cube_beta: β electron density, as a cube
59
+ :param density_cube_difference: difference spin densities, as a cube
60
+
61
+ :param electrostatic_potential_cube: electrostatic potential, as a cube
62
+
63
+ :param molecular_orbitals: MOs, key is absolute orbital index (for closed-shell species (RHF))
64
+ :param molecular_orbitals_alpha: α MOs, key is absolute orbital index (for open-shell species (UHF/ROHF))
65
+ :param molecular_orbitals_beta: β MOs, key is absolute orbital index (for open-shell species (UHF/ROHF))
66
+ """
67
+
68
+ # Config settings
69
+ settings: Settings
70
+ compute_density_cube: bool = True
71
+ compute_electrostatic_potential_cube: bool = True
72
+ compute_num_occupied_orbitals: NonNegativeInt = 1
73
+ compute_num_virtual_orbitals: NonNegativeInt = 1
74
+
75
+ # Results
76
+ calc_uuid: UUID | None = None
77
+
78
+ dipole: Vector3D | None = None
79
+ quadrupole: Matrix3x3 | None = None
80
+
81
+ mulliken_charges: FloatPerAtom | None = None
82
+ lowdin_charges: FloatPerAtom | None = None
83
+
84
+ wiberg_bond_orders: list[tuple[NonNegativeInt, NonNegativeInt, NonNegativeFloat]] = []
85
+ mayer_bond_orders: list[tuple[NonNegativeInt, NonNegativeInt, NonNegativeFloat]] = []
86
+
87
+ density_cube: PropertyCube | None = None
88
+ density_cube_alpha: PropertyCube | None = None
89
+ density_cube_beta: PropertyCube | None = None
90
+ density_cube_difference: PropertyCube | None = None
91
+
92
+ electrostatic_potential_cube: PropertyCube | None = None
93
+
94
+ molecular_orbitals: dict[NonNegativeInt, MolecularOrbitalCube] = {}
95
+ molecular_orbitals_alpha: dict[NonNegativeInt, MolecularOrbitalCube] = {}
96
+ molecular_orbitals_beta: dict[NonNegativeInt, MolecularOrbitalCube] = {}
@@ -3,7 +3,7 @@ from .workflow import Workflow
3
3
 
4
4
 
5
5
  class FukuiIndexWorkflow(Workflow):
6
- # uuid of optimization
6
+ # UUID of optimization
7
7
  optimization: UUID | None = None
8
8
 
9
9
  global_electrophilicity_index: float | None = None
@@ -9,6 +9,12 @@ from ..types import UUID
9
9
  from .workflow import Workflow
10
10
 
11
11
 
12
+ class MolecularDynamicsInitialization(LowercaseStrEnum):
13
+ RANDOM = "random"
14
+ QUASICLASSICAL = "quasiclassical"
15
+ READ = "read"
16
+
17
+
12
18
  class ThermodynamicEnsemble(LowercaseStrEnum):
13
19
  NPT = "npt"
14
20
  NVT = "nvt"
@@ -18,22 +24,25 @@ class ThermodynamicEnsemble(LowercaseStrEnum):
18
24
  class Frame(Base):
19
25
  index: int # what number frame this is within the MD simulation
20
26
 
21
- uuid: UUID | None = None # UUID of molecule
27
+ calculation_uuid: UUID | None = None # UUID of calculation
22
28
 
23
29
  pressure: float
24
30
  temperature: float
31
+ volume: float
25
32
  energy: float
26
33
 
27
34
 
28
35
  class MolecularDynamicsSettings(Base):
29
36
  ensemble: ThermodynamicEnsemble = ThermodynamicEnsemble.NVT
37
+ initialization: MolecularDynamicsInitialization = MolecularDynamicsInitialization.RANDOM
30
38
 
31
39
  timestep: PositiveFloat = 1.0 # fs
32
40
  num_steps: PositiveInt = 500
41
+ save_interval: PositiveInt = 10
33
42
 
34
43
  confining_constraint: SphericalHarmonicConstraint | None = None
35
44
 
36
- temperature: PositiveFloat | None = 300 # K
45
+ temperature: PositiveFloat = 300 # K
37
46
  pressure: PositiveFloat | None = None # atm
38
47
 
39
48
  langevin_thermostat_timescale: PositiveFloat = 100 # fs
@@ -46,7 +55,7 @@ class MolecularDynamicsSettings(Base):
46
55
  """Check that NVT ensemble always has temperature defined, and that NPT has temp and pressure defined."""
47
56
  if self.ensemble == ThermodynamicEnsemble.NVT and self.temperature is None:
48
57
  raise ValueError("NVT ensemble must have a temperature defined")
49
- if self.ensemble == ThermodynamicEnsemble.NPT and (self.temperature is None or self.pressure is None):
58
+ elif self.ensemble == ThermodynamicEnsemble.NPT and (self.temperature is None or self.pressure is None):
50
59
  raise ValueError("NPT ensemble must have both temperature and pressure defined")
51
60
  return self
52
61
 
@@ -56,7 +65,5 @@ class MolecularDynamicsWorkflow(Workflow):
56
65
  calc_settings: Settings
57
66
  calc_engine: str | None = None
58
67
 
59
- save_interval: PositiveInt = 10
60
-
61
- # uuids of scan points
68
+ # UUIDs of scan points
62
69
  frames: list[Frame] = []
@@ -27,14 +27,14 @@ class MultiStageOptSettings(BaseModel):
27
27
  wB97M-D3BJ/def2-TZVPPD//wB97X-3c//r²SCAN-3c with GFN2-xTB pre-opt
28
28
 
29
29
  Notes:
30
- - No solvent in pre-opt
30
+ - No solvent in any optimizations when using Modes
31
31
  - If solvent: xTB singlepoints use CPCMX, xTB optimizations use ALBP, all else use CPCM
32
32
  - Allows a single point to be called with no optimization
33
33
 
34
34
  :param mode: Mode for settings
35
35
  :param optimization_settings: list of opt settings to apply successively
36
36
  :param singlepoint_settings: final single point settings
37
- :param solvent: solvent to use
37
+ :param solvent: solvent to use for singlepoint
38
38
  :param xtb_preopt: pre-optimize with xtb (sets based on mode when None)
39
39
  :param constraints: constraints for optimization
40
40
  :param transition_state: whether this is a transition state
@@ -44,17 +44,17 @@ class MultiStageOptSettings(BaseModel):
44
44
  >>> msos
45
45
  <MultiStageOptSettings RAPID>
46
46
  >>> msos.level_of_theory
47
- 'r2scan_3c/cpcm(water)//gfn2_xtb/alpb(water)'
47
+ 'r2scan_3c/cpcm(water)//gfn2_xtb'
48
48
  """
49
49
 
50
50
  mode: Mode
51
51
  optimization_settings: Sequence[Settings] = tuple()
52
52
  singlepoint_settings: Settings | None = None
53
53
  solvent: Solvent | None = None
54
- xtb_preopt: bool | None = None
54
+ xtb_preopt: bool = False
55
55
  constraints: Sequence[Constraint] = tuple()
56
56
  transition_state: bool = False
57
- frequencies: bool = True
57
+ frequencies: bool = False
58
58
 
59
59
  def __str__(self) -> str:
60
60
  return repr(self)
@@ -78,7 +78,7 @@ class MultiStageOptSettings(BaseModel):
78
78
 
79
79
  >>> msos = MultiStageOptSettings(mode=Mode.RAPID, solvent="hexane")
80
80
  >>> msos.level_of_theory
81
- 'r2scan_3c/cpcm(hexane)//gfn2_xtb/alpb(hexane)'
81
+ 'r2scan_3c/cpcm(hexane)//gfn2_xtb'
82
82
  """
83
83
  methods = [self.singlepoint_settings] if self.singlepoint_settings else []
84
84
  methods += reversed(self.optimization_settings)
@@ -148,39 +148,34 @@ class MultiStageOptSettings(BaseModel):
148
148
  match mode:
149
149
  case Mode.RECKLESS:
150
150
  self.xtb_preopt = False
151
- self.optimization_settings = [opt(Method.GFN_FF, solvent=self.solvent, freq=self.frequencies)]
151
+ self.optimization_settings = [opt(Method.GFN_FF, freq=self.frequencies)]
152
152
  self.singlepoint_settings = sp(Method.GFN2_XTB, solvent=self.solvent)
153
153
 
154
154
  case Mode.RAPID:
155
- self.xtb_preopt = bool(self.xtb_preopt)
156
155
  self.optimization_settings = [
157
156
  *gfn0_pre_opt * self.xtb_preopt,
158
- opt(Method.GFN2_XTB, solvent=self.solvent, freq=self.frequencies),
157
+ opt(Method.GFN2_XTB, freq=self.frequencies),
159
158
  ]
160
159
  self.singlepoint_settings = sp(Method.R2SCAN3C, solvent=self.solvent)
161
160
 
162
161
  case Mode.CAREFUL:
163
- self.xtb_preopt = (self.xtb_preopt is None) or self.xtb_preopt
164
162
  self.optimization_settings = [
165
163
  *gfn2_pre_opt * self.xtb_preopt,
166
- opt(Method.R2SCAN3C, solvent=self.solvent, freq=self.frequencies),
164
+ opt(Method.R2SCAN3C, freq=self.frequencies),
167
165
  ]
168
166
  self.singlepoint_settings = sp(Method.WB97X3C, solvent=self.solvent)
169
167
 
170
168
  case Mode.METICULOUS:
171
- self.xtb_preopt = (self.xtb_preopt is None) or self.xtb_preopt
172
169
  self.optimization_settings = [
173
170
  *gfn2_pre_opt * self.xtb_preopt,
174
- opt(Method.R2SCAN3C, solvent=self.solvent),
175
- opt(Method.WB97X3C, solvent=self.solvent, freq=self.frequencies),
171
+ opt(Method.R2SCAN3C),
172
+ opt(Method.WB97X3C, freq=self.frequencies),
176
173
  ]
177
174
  self.singlepoint_settings = sp(Method.WB97MD3BJ, "def2-TZVPPD", solvent=self.solvent)
178
175
 
179
176
  case mode:
180
177
  raise NotImplementedError(f"Cannot assign settings for {mode=}")
181
178
 
182
- assert self.xtb_preopt is not None
183
-
184
179
 
185
180
  class MultiStageOptWorkflow(Workflow, MultiStageOptSettings):
186
181
  """
@@ -191,7 +186,7 @@ class MultiStageOptWorkflow(Workflow, MultiStageOptSettings):
191
186
  :param mode: Mode for workflow
192
187
  :param optimization_settings: list of opt settings to apply successively
193
188
  :param singlepoint_settings: final single point settings
194
- :param solvent: solvent to use
189
+ :param solvent: solvent to use for singlepoint
195
190
  :param xtb_preopt: pre-optimize with xtb (sets based on mode when None)
196
191
  :param constraints: constraints for optimization
197
192
  :param transition_state: whether this is a transition state
@@ -206,7 +201,7 @@ class MultiStageOptWorkflow(Workflow, MultiStageOptSettings):
206
201
  >>> msow
207
202
  <MultiStageOptWorkflow RAPID>
208
203
  >>> msow.level_of_theory
209
- 'r2scan_3c/cpcm(water)//gfn2_xtb/alpb(water)'
204
+ 'r2scan_3c/cpcm(water)//gfn2_xtb'
210
205
  """
211
206
 
212
207
  # Populated while running the workflow
@@ -228,14 +223,14 @@ class MultiStageOptMixin(BaseModel):
228
223
  Mixin for workflows that use MultiStageOptSettings.
229
224
  """
230
225
 
231
- mso_mode: Mode
226
+ mso_mode: Mode = Mode.AUTO
232
227
  # Need to use a sentinel object to make both mypy and pydantic happy
233
228
  multistage_opt_settings: MultiStageOptSettings = _sentinel_msos # type: ignore [assignment]
234
229
  solvent: Solvent | None = None
235
- xtb_preopt: bool | None = None
230
+ xtb_preopt: bool = False
236
231
  constraints: Sequence[Constraint] = tuple()
237
232
  transition_state: bool = False
238
- frequencies: bool = True
233
+ frequencies: bool = False
239
234
 
240
235
  @model_validator(mode="after")
241
236
  def set_mso_settings(self) -> Self:
@@ -265,3 +260,74 @@ class MultiStageOptMixin(BaseModel):
265
260
  )
266
261
 
267
262
  return self
263
+
264
+
265
+ def build_mso_settings(
266
+ sp_method: Method,
267
+ sp_basis_set: str | None,
268
+ opt_methods: list[Method],
269
+ opt_basis_sets: list[str | None],
270
+ mode: Mode = Mode.MANUAL,
271
+ solvent: Solvent | None = None,
272
+ use_solvent_for_opt: bool = False,
273
+ constraints: list[Constraint] | None = None,
274
+ transition_state: bool = False,
275
+ frequencies: bool = False,
276
+ ) -> MultiStageOptSettings:
277
+ """
278
+ Helper function to construct multi-stage opt settings objects manually.
279
+
280
+ There's no xTB pre-optimization here - add that yourself!
281
+
282
+ :param optimization_settings: optimization settings to apply successively
283
+ :param singlepoint_settings: final single point settings
284
+ :param mode: Mode for settings, defaults to `MANUAL`
285
+ :param solvent: solvent to use
286
+ :param use_solvent_for_opt: whether to conduct opts with solvent
287
+ :param constraints: constraints for optimization
288
+ :param transition_state: whether this is a transition state
289
+ :param frequencies: whether to calculate frequencies
290
+ :returns: MultiStageOptSettings
291
+ """
292
+ if constraints is None:
293
+ constraints = []
294
+
295
+ opt_settings = OptimizationSettings(constraints=constraints, transition_state=transition_state)
296
+
297
+ OPT = [Task.OPTIMIZE if not transition_state else Task.OPTIMIZE_TS]
298
+
299
+ def opt(method: Method, basis_set: str | None = None, solvent: Solvent | None = None, freq: bool = False) -> Settings:
300
+ """Generates optimization settings."""
301
+ model = "alpb" if method in XTB_METHODS else "cpcm"
302
+
303
+ return Settings(
304
+ method=method,
305
+ basis_set=basis_set,
306
+ tasks=OPT + [Task.FREQUENCIES] * freq,
307
+ solvent_settings=SolventSettings(solvent=solvent, model=model) if (solvent and use_solvent_for_opt) else None,
308
+ opt_settings=opt_settings,
309
+ )
310
+
311
+ def sp(method: Method, basis_set: str | None = None, solvent: Solvent | None = None) -> Settings:
312
+ """Generate singlepoint settings."""
313
+ model = "cpcmx" if method in XTB_METHODS else "cpcm"
314
+
315
+ return Settings(
316
+ method=method,
317
+ basis_set=basis_set,
318
+ tasks=[Task.ENERGY],
319
+ solvent_settings=SolventSettings(solvent=solvent, model=model) if solvent else None,
320
+ )
321
+
322
+ return MultiStageOptSettings(
323
+ mode=mode,
324
+ optimization_settings=[
325
+ opt(method=method, basis_set=basis_set, solvent=solvent, freq=frequencies) for method, basis_set in zip(opt_methods, opt_basis_sets, strict=True)
326
+ ],
327
+ singlepoint_settings=sp(method=sp_method, basis_set=sp_basis_set, solvent=solvent),
328
+ solvent=solvent,
329
+ xtb_preopt=False,
330
+ constraints=constraints,
331
+ transition_state=transition_state,
332
+ frequencies=frequencies,
333
+ )
@@ -1,22 +1,59 @@
1
- from typing import Any
1
+ from typing import Any, TypeVar
2
+
3
+ from pydantic import ValidationInfo, field_validator, model_validator
2
4
 
3
5
  from ..mode import Mode
4
6
  from ..solvent import Solvent
5
7
  from ..types import UUID
8
+ from .multistage_opt import MultiStageOptMixin
6
9
  from .workflow import Workflow
7
10
 
11
+ _T = TypeVar("_T")
12
+
13
+
14
+ class RedoxPotentialWorkflow(Workflow, MultiStageOptMixin):
15
+ """
16
+ Workflow for computing spin states of molecules.
17
+
18
+ Uses the modes from MultiStageOptSettings.
19
+
20
+ Inherited
21
+ :param initial_molecule: Molecule of interest
22
+ :param mode: Mode for workflow
23
+ :param multistage_opt_settings: set by mode unless mode=MANUAL (ignores additional settings if set)
24
+ :param xtb_preopt: pre-optimize with xtb (sets based on mode when None)
25
+ :param constraints: constraints to add
26
+ :param transition_state: whether this is a transition state
27
+ :param frequencies: whether to calculate frequencies
28
+
29
+ Overridden:
30
+ :param mso_mode: Mode for MultiStageOptSettings
31
+ :param solvent: solvent to use for optimization
32
+
33
+ New:
34
+ :param reduction: whether or not to calculate the reduction half-reaction
35
+ :param oxidation: whether or not to calculate the oxidation half-reaction
36
+ :param neutral_molecule: UUID of the calculation for the neutral molecule
37
+ :param anion_molecule: UUID of the calculation for the anion molecule
38
+ :param cation_molecule: UUID of the calculation for the cation molecule
39
+ :param reduction_potential: the final potential, in V
40
+ :param oxidation_potential: the final potential, in V
41
+
42
+ Legacy:
43
+ :param redox_type: one of "reduction" or "oxidation"
44
+ :param redox_potential: the corresponding potential, in V
45
+ """
8
46
 
9
- class RedoxPotentialWorkflow(Workflow):
10
- mode: Mode = Mode.RAPID
11
47
  solvent: Solvent = Solvent.ACETONITRILE
48
+
12
49
  reduction: bool = True
13
50
  oxidation: bool = True
14
51
 
15
52
  # legacy values - remove in future release!
16
- redox_type: UUID | None = None
53
+ redox_type: str | None = None
17
54
  redox_potential: float | None = None
18
55
 
19
- # uuids
56
+ # UUIDs
20
57
  neutral_molecule: UUID | None = None
21
58
  anion_molecule: UUID | None = None
22
59
  cation_molecule: UUID | None = None
@@ -24,6 +61,33 @@ class RedoxPotentialWorkflow(Workflow):
24
61
  reduction_potential: float | None = None
25
62
  oxidation_potential: float | None = None
26
63
 
64
+ @field_validator("solvent", mode="before")
65
+ @classmethod
66
+ def only_mecn_please(cls, val: Solvent | None) -> Solvent:
67
+ """Only MeCN please!"""
68
+ if val != Solvent.ACETONITRILE:
69
+ raise ValueError("Only acetonitrile permitted!")
70
+
71
+ return val
72
+
73
+ @field_validator("constraints", "transition_state")
74
+ @classmethod
75
+ def turned_off(cls, value: _T, info: ValidationInfo) -> _T:
76
+ if value:
77
+ raise ValueError(f"{info.field_name} not supported in redox potential workflows.")
78
+
79
+ return value
80
+
81
+ @model_validator(mode="before")
82
+ @classmethod
83
+ def set_mode_and_mso_mode(cls, values: dict[str, Any]) -> dict[str, Any]:
84
+ """Set the MultiStageOptSettings mode to match current redox potential mode, and select mode if `Auto`."""
85
+ if ("mode" not in values) or (values["mode"] == Mode.AUTO):
86
+ values["mode"] = Mode.RAPID
87
+
88
+ values["mso_mode"] = values["mode"]
89
+ return values
90
+
27
91
  def model_post_init(self, __context: Any) -> None:
28
92
  """Keep back-compatible with old schema."""
29
93
  if self.redox_type == "oxidation":
stjames/workflows/scan.py CHANGED
@@ -34,5 +34,5 @@ class ScanWorkflow(Workflow):
34
34
  calc_settings: Settings
35
35
  calc_engine: str
36
36
 
37
- # uuids of scan points
37
+ # UUIDs of scan points
38
38
  scan_points: list[UUID | None] = []
@@ -51,8 +51,8 @@ class SpinStatesWorkflow(Workflow, MultiStageOptMixin):
51
51
  :param initial_molecule: Molecule of interest
52
52
  :param mode: Mode for workflow
53
53
  :param multistage_opt_settings: set by mode unless mode=MANUAL (ignores additional settings if set)
54
- :param solvent: solvent to use
55
- :param xtb_preopt: pre-optimize with xtb (sets based on mode when None)
54
+ :param solvent: solvent to use for optimization
55
+ :param xtb_preopt: pre-optimize with xtb
56
56
  :param constraints: constraints to add
57
57
  :param transition_state: whether this is a transition state
58
58
  :param frequencies: whether to calculate frequencies
@@ -10,7 +10,7 @@ class Tautomer(Base):
10
10
  weight: Optional[float] = None
11
11
  predicted_relative_energy: Optional[float] = None
12
12
 
13
- # uuids, optionally
13
+ # UUIDs, optionally
14
14
  structures: list[DBCalculation] = []
15
15
 
16
16
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: stjames
3
- Version: 0.0.44
3
+ Version: 0.0.46
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
@@ -8,7 +8,7 @@ Project-URL: Bug Tracker, https://github.com/rowansci/stjames/issues
8
8
  Requires-Python: >=3.11
9
9
  Description-Content-Type: text/markdown
10
10
  License-File: LICENSE
11
- Requires-Dist: pydantic >=2.4
11
+ Requires-Dist: pydantic>=2.4
12
12
  Requires-Dist: numpy
13
13
 
14
14
  # stjames
@@ -4,25 +4,25 @@ stjames/atom.py,sha256=w7q-x9xpBw4sJ1WGrWt65WAaStxhz-m7dugXCYEOpq4,2064
4
4
  stjames/base.py,sha256=9PvUjBeVSkmA3TaruaB0uvjFMbWYTGKXECISNGAj_AU,1201
5
5
  stjames/basis_set.py,sha256=wI3M2q9uPf9jhKpAi4E2DrsyKzloDGLRjAlk7krdYgc,949
6
6
  stjames/calculation.py,sha256=O2LwwQ_cOLmDOGXTHA9J71YbUZXigUSbvbLA-fSVm3w,915
7
- stjames/constraint.py,sha256=IQsQPGbjaoWXZkAOPPBH0_-EGtWwGkkHBhuPg53v5-M,890
8
- stjames/correction.py,sha256=_pNG3qSylfx0iyUxqwx9HPU0m032YwP6wSPCjbJrD94,358
9
- stjames/diis_settings.py,sha256=QHc7L-hktkbOWBYr29byTdqL8lWJzKJiY9XW8ha4Qzo,552
7
+ stjames/constraint.py,sha256=B6oV0rYjmAWr8gpi5f03gRy_uuqjUURVDVwoez5Cfbg,2442
8
+ stjames/correction.py,sha256=ZVErCcj4TPyZeKrdvXVjHa0tFynsCaoy96QZUVxWFM8,413
9
+ stjames/diis_settings.py,sha256=4m1EQQWBlpHhMnWopix8qOqJv7QCluvdnV9jSKJDFtE,552
10
10
  stjames/grid_settings.py,sha256=WrSNGc-8_f87YBZYt9Hh7RbhM4MweADoVzwBMcSqcsE,640
11
11
  stjames/int_settings.py,sha256=5HXp8opt5ZyY1UpmfaK7NVloWVLM5jkG0elEEqpVLUo,896
12
12
  stjames/message.py,sha256=Rq6QqmHZKecWxYH8fVyXmuoCCPZv8YinvgykSeorXSU,216
13
- stjames/method.py,sha256=xnfphxyiWZotxQcmgvpFMJDmGEM2B-_5cbPkgYZBIws,1245
13
+ stjames/method.py,sha256=a6QQff-0YsutkOTuOcGrdDW76x9ZexiNLdKzzoE1Vcw,1698
14
14
  stjames/mode.py,sha256=xw46Cc7f3eTS8i35qECi-8DocAlANhayK3w4akD4HBU,496
15
- stjames/molecule.py,sha256=QV_8vscvF48wadWtnt_no5S7j0kHgUbbFr7EnlDnzfk,10558
15
+ stjames/molecule.py,sha256=v8NikFHfwOahXSo4VKGSqeHKI2HIoRdNjGE0GkZgNS4,10554
16
16
  stjames/opt_settings.py,sha256=gxXGtjy9l-Q5Wen9eO6T6HHRCuS8rfOofdVQIJj0JcI,550
17
17
  stjames/periodic_cell.py,sha256=JDCyynpamggTNi_HnTnnotRbeSMBfYc-srhD-IwUnrg,996
18
18
  stjames/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
- stjames/scf_settings.py,sha256=xMMCQ0hVB4nNFSiWesNQZUa_aLsozSZGYWweAPPDGBg,2356
19
+ stjames/scf_settings.py,sha256=WotVgVrayQ_8PUHP39zVtG7iLT9PV41lpzruttFACP8,2356
20
20
  stjames/settings.py,sha256=tfgEYns6WdsheQ6wpR6uyI8O4s2iTqyH7YWtNQ36k74,8666
21
21
  stjames/solvent.py,sha256=u037tmu-9oa21s-WEDZ7VC7nuNVjkqR2ML4JWjWSME4,1158
22
22
  stjames/status.py,sha256=wTKNcNxStoEHrxxgr_zTyN90NITa3rxMQZzOgrCifEw,332
23
23
  stjames/task.py,sha256=OLINRqe66o7t8arffilwmggrF_7TH0L79u6DhGruxV8,329
24
24
  stjames/thermochem_settings.py,sha256=ZTLz31v8Ltutde5Nfm0vH5YahWjcfFWfr_R856KffxE,517
25
- stjames/types.py,sha256=2FBtEwpaqBIgN7fmVRORJfkv-am0Lssr8e7ho7Fui8w,206
25
+ stjames/types.py,sha256=CPKR0g_kdFejMjGdKBjtuJRQqfmAZ-uIaSuGR1vBzCQ,245
26
26
  stjames/data/__init__.py,sha256=O59Ksp7AIqwOELCWymfCx7YeBzwNOGCMlGQi7tNLqiE,24
27
27
  stjames/data/bragg_radii.json,sha256=hhbn-xyZNSdmnULIjN2Cvq-_BGIZIqG243Ls_mey61w,1350
28
28
  stjames/data/elements.py,sha256=9BW01LZlyJ0H5s7Q26vUmjZIST41fwOYYrGvmPd7q0w,858
@@ -30,23 +30,25 @@ stjames/data/isotopes.json,sha256=5ba8QnLrHD_Ypv2xekv2cIRwYrX3MQ19-1FOFtt0RuU,83
30
30
  stjames/data/nist_isotopes.json,sha256=d5DNk1dX0iB1waEYIRR6JMHuA7AuYwSBEgBvb4EKyhM,14300
31
31
  stjames/data/read_nist_isotopes.py,sha256=y10FNjW43QpC45qib7VHsIghEwT7GG5rsNwHdc9osRI,3309
32
32
  stjames/data/symbol_element.json,sha256=vl_buFusTqBd-muYQtMLtTDLy2OtBI6KkBeqkaWRQrg,1186
33
- stjames/workflows/__init__.py,sha256=TQwTrX8hzUKBcCV4C05IvePjnEotsAWTbXv4-b8zDRk,331
33
+ stjames/workflows/__init__.py,sha256=IRcfBNaFWVMmixQHjPKcBJkcUYWRnXNwrx2MUFNiQKM,1843
34
34
  stjames/workflows/admet.py,sha256=V8noO0Eb7h2bDFSnj6Pxv4ILm0lGxyVRCi13hE0zmEQ,149
35
35
  stjames/workflows/basic_calculation.py,sha256=q48bpab7ZqmRTR4PsGC6bWkuxqkVdJRM8gysevTYXP0,212
36
- stjames/workflows/bde.py,sha256=c_4gGDFSjHvw4CBW21i6ErSAK2Tpypvld2yair1KYXo,9568
36
+ stjames/workflows/bde.py,sha256=iNrBiAUJA-VaAB-eFddApUO2xIc5PyPYXNtC2stQ_OU,9667
37
37
  stjames/workflows/conformer.py,sha256=YYwL3l7OaVeea4N9-ihghwa_ieKY6hia9LNbiTraMb0,2732
38
- stjames/workflows/descriptors.py,sha256=jQ3RuMi7xk799JZ_AL1ARL3yQfWLG03L_VVsK4KIMeY,281
39
- stjames/workflows/fukui.py,sha256=F5tw5jTqBimo_GiXuThhRpoxauZE5YadZjObLFDCba8,348
40
- stjames/workflows/molecular_dynamics.py,sha256=YsDGkpI_FbB02sVkF1xiUK141cMQaHBjYyvTJarjBaU,1954
41
- stjames/workflows/multistage_opt.py,sha256=aJGKqsTfVOyZk-dNsM9Z7IEOlpfYw7qvSmxo-u5IeEE,10252
38
+ stjames/workflows/conformer_search.py,sha256=zmGaSuka0VClmX36AkKvJAN565-hyp2ZJQIjhkAMQRM,12924
39
+ stjames/workflows/descriptors.py,sha256=lRRCsGzad3nIg0wI1090ffaXB0FVh0nRRb2lNxCY0kI,281
40
+ stjames/workflows/electronic_properties.py,sha256=DFrzU49rux13Fy5q7pgvuYNjiyC2KwMtqU6FTAxg9jo,3339
41
+ stjames/workflows/fukui.py,sha256=CsJ3_gvzqEqcxwYN7bnNIa37F3KlLm7obsU77TGmDgo,348
42
+ stjames/workflows/molecular_dynamics.py,sha256=wx633IhPjRwEYvfyuRnSb0c84lN2WtZJET2q-SJv5DY,2211
43
+ stjames/workflows/multistage_opt.py,sha256=FXSm-adv9TIiE6ftWPO50Yq9ypoJ7GSPkJKpnC0r4kQ,12686
42
44
  stjames/workflows/pka.py,sha256=zpR90Yv2L-D56o2mGArM8027DWpnFFnay31UR9Xh5Nc,774
43
- stjames/workflows/redox_potential.py,sha256=u6QThnqheJp6EDuWiJApJEh-fp0TKGfSyKfa8ykf85g,1211
44
- stjames/workflows/scan.py,sha256=hL4Hco3Ns0dntjh2G2HhhWmED1mbt0gA_hsglPQ5Vjg,814
45
- stjames/workflows/spin_states.py,sha256=hzxDG8pBlmae8EUEizl0sn6nsXYk4ClmeH-cFZ4bSIc,4606
46
- stjames/workflows/tautomer.py,sha256=kZSCHo2Q7LzqtQjF_WyyxjECkndG49T9QOM12hsUkx8,421
45
+ stjames/workflows/redox_potential.py,sha256=e7WOyTIC_1NPfh7amvW7YzqQezcswX9YaXT-qPiePAo,3651
46
+ stjames/workflows/scan.py,sha256=Wqimw1nMErKOYHcC7U1hNC8LZb5XPtS-nE1a-c_6_88,814
47
+ stjames/workflows/spin_states.py,sha256=TXHqB7ClTkkCy1Yfcsv99v2woAhT8oG1-uaF20-oMbQ,4592
48
+ stjames/workflows/tautomer.py,sha256=owZOOfGlohxlaOG1pGpx7dVPvas8ZEnl_9Ms5F0Kpms,421
47
49
  stjames/workflows/workflow.py,sha256=tIu5naADYgYS7kdW8quvGEWHWosBcrIdcD7L86v-uMQ,976
48
- stjames-0.0.44.dist-info/LICENSE,sha256=i7ehYBS-6gGmbTcgU4mgk28pyOx2kScJ0kcx8n7bWLM,1084
49
- stjames-0.0.44.dist-info/METADATA,sha256=WxNrF-JxZt7kqljgnu2Ou8i-1lSWefeX3q2GNFDe6Io,1628
50
- stjames-0.0.44.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
51
- stjames-0.0.44.dist-info/top_level.txt,sha256=FYCwxl6quhYOAgG-mnPQcCK8vsVM7B8rIUrO-WrQ_PI,8
52
- stjames-0.0.44.dist-info/RECORD,,
50
+ stjames-0.0.46.dist-info/LICENSE,sha256=i7ehYBS-6gGmbTcgU4mgk28pyOx2kScJ0kcx8n7bWLM,1084
51
+ stjames-0.0.46.dist-info/METADATA,sha256=sagkuDdeazTP1wvoGMsletsSSFzY_8Qjv0shhmcj17w,1627
52
+ stjames-0.0.46.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
53
+ stjames-0.0.46.dist-info/top_level.txt,sha256=FYCwxl6quhYOAgG-mnPQcCK8vsVM7B8rIUrO-WrQ_PI,8
54
+ stjames-0.0.46.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.1.0)
2
+ Generator: setuptools (75.6.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5