stjames 0.0.40__py3-none-any.whl → 0.0.41__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/__init__.py +3 -0
- stjames/atom.py +66 -0
- stjames/base.py +13 -13
- stjames/basis_set.py +3 -7
- stjames/calculation.py +2 -0
- stjames/constraint.py +22 -2
- stjames/data/__init__.py +1 -0
- stjames/data/elements.py +27 -0
- stjames/data/read_nist_isotopes.py +116 -0
- stjames/method.py +34 -1
- stjames/molecule.py +113 -15
- stjames/opt_settings.py +10 -5
- stjames/periodic_cell.py +34 -0
- stjames/settings.py +25 -38
- stjames/solvent.py +1 -0
- stjames/task.py +1 -0
- stjames/types.py +8 -0
- stjames/workflows/__init__.py +6 -0
- stjames/workflows/admet.py +7 -0
- stjames/workflows/basic_calculation.py +9 -0
- stjames/workflows/bde.py +269 -0
- stjames/workflows/descriptors.py +2 -1
- stjames/workflows/fukui.py +6 -7
- stjames/workflows/molecular_dynamics.py +60 -0
- stjames/workflows/multistage_opt.py +261 -0
- stjames/workflows/redox_potential.py +9 -8
- stjames/workflows/scan.py +6 -6
- stjames/workflows/spin_states.py +144 -0
- stjames/workflows/workflow.py +31 -1
- {stjames-0.0.40.dist-info → stjames-0.0.41.dist-info}/METADATA +3 -3
- stjames-0.0.41.dist-info/RECORD +48 -0
- {stjames-0.0.40.dist-info → stjames-0.0.41.dist-info}/WHEEL +1 -1
- stjames-0.0.40.dist-info/RECORD +0 -36
- {stjames-0.0.40.dist-info → stjames-0.0.41.dist-info}/LICENSE +0 -0
- {stjames-0.0.40.dist-info → stjames-0.0.41.dist-info}/top_level.txt +0 -0
stjames/settings.py
CHANGED
|
@@ -5,7 +5,7 @@ import pydantic
|
|
|
5
5
|
from .base import Base, UniqueList
|
|
6
6
|
from .basis_set import BasisSet
|
|
7
7
|
from .correction import Correction
|
|
8
|
-
from .method import Method
|
|
8
|
+
from .method import METHODS_WITH_CORRECTION, PREPACKAGED_METHODS, Method
|
|
9
9
|
from .mode import Mode
|
|
10
10
|
from .opt_settings import OptimizationSettings
|
|
11
11
|
from .scf_settings import SCFSettings
|
|
@@ -13,26 +13,7 @@ from .solvent import SolventSettings
|
|
|
13
13
|
from .task import Task
|
|
14
14
|
from .thermochem_settings import ThermochemistrySettings
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
Method.HF3C,
|
|
18
|
-
Method.B973C,
|
|
19
|
-
Method.R2SCAN3C,
|
|
20
|
-
Method.AIMNET2_WB97MD3,
|
|
21
|
-
Method.GFN2_XTB,
|
|
22
|
-
Method.GFN1_XTB,
|
|
23
|
-
Method.GFN0_XTB,
|
|
24
|
-
Method.GFN_FF,
|
|
25
|
-
]
|
|
26
|
-
|
|
27
|
-
METHODS_WITH_CORRECTION = [
|
|
28
|
-
Method.WB97XD3,
|
|
29
|
-
Method.WB97XV,
|
|
30
|
-
Method.WB97MV,
|
|
31
|
-
Method.WB97MD3BJ,
|
|
32
|
-
Method.DSDBLYPD3BJ,
|
|
33
|
-
]
|
|
34
|
-
|
|
35
|
-
T = TypeVar("T")
|
|
16
|
+
_T = TypeVar("_T")
|
|
36
17
|
|
|
37
18
|
|
|
38
19
|
class Settings(Base):
|
|
@@ -51,7 +32,8 @@ class Settings(Base):
|
|
|
51
32
|
thermochem_settings: ThermochemistrySettings = ThermochemistrySettings()
|
|
52
33
|
|
|
53
34
|
# mypy has this dead wrong (https://docs.pydantic.dev/2.0/usage/computed_fields/)
|
|
54
|
-
|
|
35
|
+
# Python 3.12 narrows the reason for the ignore to prop-decorator
|
|
36
|
+
@pydantic.computed_field # type: ignore[misc, prop-decorator, unused-ignore]
|
|
55
37
|
@property
|
|
56
38
|
def level_of_theory(self) -> str:
|
|
57
39
|
corrections = list(filter(lambda x: x not in (None, ""), self.corrections))
|
|
@@ -81,11 +63,15 @@ class Settings(Base):
|
|
|
81
63
|
if self.method == Method.HF3C:
|
|
82
64
|
self.basis_set = BasisSet(name="minix")
|
|
83
65
|
elif self.method == Method.B973C:
|
|
84
|
-
self.basis_set = BasisSet(name="mTZVP")
|
|
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")
|
|
85
71
|
|
|
86
72
|
@pydantic.field_validator("basis_set", mode="before")
|
|
87
73
|
@classmethod
|
|
88
|
-
def parse_basis_set(cls, v: Any) -> BasisSet | dict | None:
|
|
74
|
+
def parse_basis_set(cls, v: Any) -> BasisSet | dict[str, Any] | None:
|
|
89
75
|
"""Turn a string into a ``BasisSet`` object. (This is a little crude.)"""
|
|
90
76
|
if isinstance(v, BasisSet):
|
|
91
77
|
return None if v.name is None else v
|
|
@@ -103,7 +89,7 @@ class Settings(Base):
|
|
|
103
89
|
|
|
104
90
|
@pydantic.field_validator("corrections", mode="before")
|
|
105
91
|
@classmethod
|
|
106
|
-
def remove_empty_string(cls, v: list[
|
|
92
|
+
def remove_empty_string(cls, v: list[_T]) -> list[_T]:
|
|
107
93
|
"""Remove empty string values."""
|
|
108
94
|
return [c for c in v if c] if v is not None else v
|
|
109
95
|
|
|
@@ -198,25 +184,26 @@ def _assign_settings_by_mode(settings: Settings) -> None:
|
|
|
198
184
|
|
|
199
185
|
# cf. DLFIND manual, and https://www.cup.uni-muenchen.de/ch/compchem/geom/basic.html
|
|
200
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
|
|
201
188
|
if mode == Mode.RECKLESS:
|
|
202
|
-
opt_settings.energy_threshold =
|
|
203
|
-
opt_settings.max_gradient_threshold =
|
|
204
|
-
opt_settings.rms_gradient_threshold =
|
|
205
|
-
elif
|
|
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):
|
|
206
193
|
opt_settings.energy_threshold = 5e-5
|
|
207
|
-
opt_settings.max_gradient_threshold =
|
|
208
|
-
opt_settings.rms_gradient_threshold =
|
|
209
|
-
elif mode == Mode.CAREFUL or (mode ==
|
|
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):
|
|
210
197
|
opt_settings.energy_threshold = 1e-6
|
|
211
|
-
opt_settings.max_gradient_threshold =
|
|
212
|
-
opt_settings.rms_gradient_threshold =
|
|
198
|
+
opt_settings.max_gradient_threshold = 9e-4
|
|
199
|
+
opt_settings.rms_gradient_threshold = 6e-4
|
|
213
200
|
elif mode == Mode.METICULOUS:
|
|
214
201
|
opt_settings.energy_threshold = 1e-6
|
|
215
|
-
opt_settings.max_gradient_threshold =
|
|
216
|
-
opt_settings.rms_gradient_threshold =
|
|
202
|
+
opt_settings.max_gradient_threshold = 3e-5
|
|
203
|
+
opt_settings.rms_gradient_threshold = 2e-5
|
|
217
204
|
elif mode == Mode.DEBUG:
|
|
218
205
|
opt_settings.energy_threshold = 1e-6
|
|
219
|
-
opt_settings.max_gradient_threshold =
|
|
220
|
-
opt_settings.rms_gradient_threshold =
|
|
206
|
+
opt_settings.max_gradient_threshold = 4e-6
|
|
207
|
+
opt_settings.rms_gradient_threshold = 2e-6
|
|
221
208
|
else:
|
|
222
209
|
raise ValueError(f"Unknown mode ``{mode.value}``!")
|
stjames/solvent.py
CHANGED
stjames/task.py
CHANGED
stjames/types.py
ADDED
stjames/workflows/__init__.py
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
|
+
from .admet import *
|
|
2
|
+
from .basic_calculation import *
|
|
3
|
+
from .bde import *
|
|
1
4
|
from .conformer import *
|
|
2
5
|
from .descriptors import *
|
|
3
6
|
from .fukui import *
|
|
7
|
+
from .molecular_dynamics import *
|
|
8
|
+
from .multistage_opt import *
|
|
4
9
|
from .pka import *
|
|
5
10
|
from .redox_potential import *
|
|
6
11
|
from .scan import *
|
|
12
|
+
from .spin_states import *
|
|
7
13
|
from .tautomer import *
|
stjames/workflows/bde.py
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"""Bond Dissociation Energy (BDE) workflow."""
|
|
2
|
+
|
|
3
|
+
import itertools
|
|
4
|
+
from typing import Any, Iterable, Self, TypeVar
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field, PositiveInt, ValidationInfo, field_validator, model_validator
|
|
7
|
+
|
|
8
|
+
from ..mode import Mode
|
|
9
|
+
from ..molecule import Molecule
|
|
10
|
+
from ..types import UUID
|
|
11
|
+
from .multistage_opt import MultiStageOptMixin
|
|
12
|
+
from .workflow import Workflow
|
|
13
|
+
|
|
14
|
+
# the id of a mutable object may change, thus using object()
|
|
15
|
+
_sentinel_mso_mode = object()
|
|
16
|
+
_T = TypeVar("_T")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class BDE(BaseModel):
|
|
20
|
+
"""
|
|
21
|
+
Bond Dissociation Energy (BDE) result.
|
|
22
|
+
|
|
23
|
+
energy => (E_{fragment1} + E_{fragment2}) - E_{starting molecule}
|
|
24
|
+
|
|
25
|
+
:param fragment_idxs: indices of the atoms in the fragment that was dissociated (1-indexed)
|
|
26
|
+
:param energy: BDE in kcal/mol
|
|
27
|
+
:param fragment_energies: energy of fragments
|
|
28
|
+
:param calculations_uuids: calculation UUIDs
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
fragment_idxs: tuple[PositiveInt, ...]
|
|
32
|
+
energy: float | None
|
|
33
|
+
fragment_energies: tuple[float | None, float | None]
|
|
34
|
+
calculation_uuids: tuple[list[UUID | None], list[UUID | None]]
|
|
35
|
+
|
|
36
|
+
def __str__(self) -> str:
|
|
37
|
+
return repr(self)
|
|
38
|
+
|
|
39
|
+
def __repr__(self) -> str:
|
|
40
|
+
"""
|
|
41
|
+
Return a string representation of the BDE result.
|
|
42
|
+
|
|
43
|
+
>>> BDE(fragment_idxs=(1, 2), energy=1.0, fragment_energies=(4, 2), calculation_uuids=([], []))
|
|
44
|
+
<BDE (1, 2) 1.00>
|
|
45
|
+
"""
|
|
46
|
+
energy = "None" if self.energy is None else f"{self.energy:>5.2f}"
|
|
47
|
+
|
|
48
|
+
return f"<{type(self).__name__} {self.fragment_idxs} {energy}>"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class BDEWorkflow(Workflow, MultiStageOptMixin):
|
|
52
|
+
"""
|
|
53
|
+
Bond Dissociation Energy (BDE) workflow.
|
|
54
|
+
|
|
55
|
+
Uses the modes from MultiStageOptSettings to compute BDEs.
|
|
56
|
+
|
|
57
|
+
Inherited:
|
|
58
|
+
:param initial_molecule: Molecule of interest
|
|
59
|
+
:param multistage_opt_settings: set by mode unless mode=MANUAL (ignores additional settings if set)
|
|
60
|
+
:param solvent: solvent to use
|
|
61
|
+
:param xtb_preopt: pre-optimize with xtb (sets based on mode when None)
|
|
62
|
+
|
|
63
|
+
Overridden:
|
|
64
|
+
:param mso_mode: Mode for MultiStageOptSettings
|
|
65
|
+
:param frequencies: whether to calculate frequencies
|
|
66
|
+
|
|
67
|
+
Turned off:
|
|
68
|
+
:param constraints: constraints to add (not supported)
|
|
69
|
+
:param transition_state: whether this is a transition state (not supported)
|
|
70
|
+
|
|
71
|
+
New:
|
|
72
|
+
:param mode: Mode for workflow
|
|
73
|
+
:param optimize_fragments: whether to optimize the fragments, or just the starting molecule (default depends on mode)
|
|
74
|
+
:param atoms: atoms to dissociate (1-indexed)
|
|
75
|
+
:param fragment_indices: fragments to dissociate (all fields feed into this, 1-indexed)
|
|
76
|
+
:param all_CH: dissociate all C–H bonds
|
|
77
|
+
:param all_CX: dissociate all C–X bonds (X ∈ {F, Cl, Br, I, At, Ts})
|
|
78
|
+
:param optimization_calculation_uuids: UUIDs of initial optimization calculations
|
|
79
|
+
:param optimization_energy: energy of optimized initial molecule
|
|
80
|
+
:param bdes: BDE results
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
mode: Mode
|
|
84
|
+
mso_mode: Mode = _sentinel_mso_mode # type: ignore [assignment]
|
|
85
|
+
frequencies: bool = False
|
|
86
|
+
optimize_fragments: bool = None # type: ignore [assignment]
|
|
87
|
+
|
|
88
|
+
atoms: tuple[PositiveInt, ...] = Field(default_factory=tuple)
|
|
89
|
+
fragment_indices: tuple[tuple[PositiveInt, ...], ...] = Field(default_factory=tuple)
|
|
90
|
+
|
|
91
|
+
all_CH: bool = False
|
|
92
|
+
all_CX: bool = False
|
|
93
|
+
|
|
94
|
+
# Results
|
|
95
|
+
optimization_calculation_uuids: list[UUID | None] | None = None
|
|
96
|
+
optimization_energy: float | None = None
|
|
97
|
+
bdes: list[BDE] = Field(default_factory=list)
|
|
98
|
+
|
|
99
|
+
def __str__(self) -> str:
|
|
100
|
+
r"""
|
|
101
|
+
Return a string representation of the BDE workflow.
|
|
102
|
+
|
|
103
|
+
>>> print(BDEWorkflow(initial_molecule=Molecule.from_xyz("H 0 0 0\nF 0 0 1"), mode=Mode.METICULOUS, atoms=[1, 2]))
|
|
104
|
+
BDEWorkflow METICULOUS
|
|
105
|
+
(1,)
|
|
106
|
+
(2,)
|
|
107
|
+
"""
|
|
108
|
+
return f"{type(self).__name__} {self.mode.name}\n" + "\n".join(map(str, self.fragment_indices))
|
|
109
|
+
|
|
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
|
+
@property
|
|
120
|
+
def energies(self) -> tuple[float | None, ...]:
|
|
121
|
+
return tuple(bde.energy for bde in self.bdes)
|
|
122
|
+
|
|
123
|
+
@field_validator("constraints", "transition_state")
|
|
124
|
+
@classmethod
|
|
125
|
+
def turned_off(cls, value: _T, info: ValidationInfo) -> _T:
|
|
126
|
+
if value:
|
|
127
|
+
raise ValueError(f"{info.field_name} not supported in BDE workflows.")
|
|
128
|
+
|
|
129
|
+
return value
|
|
130
|
+
|
|
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
|
+
@field_validator("initial_molecule", mode="before")
|
|
140
|
+
@classmethod
|
|
141
|
+
def no_charge_or_spin(cls, mol: Molecule) -> Molecule:
|
|
142
|
+
"""Ensure the molecule has no charge or spin."""
|
|
143
|
+
if mol.charge != 0 or mol.multiplicity != 1:
|
|
144
|
+
raise ValueError("Charge and spin partitioning undefined for BDE, only neutral singlet molecules supported.")
|
|
145
|
+
|
|
146
|
+
return mol
|
|
147
|
+
|
|
148
|
+
@model_validator(mode="before")
|
|
149
|
+
@classmethod
|
|
150
|
+
def set_mso_mode(cls, values: dict[str, Any]) -> dict[str, Any]:
|
|
151
|
+
"""Set the MultiStageOptSettings mode to match current BDE mode."""
|
|
152
|
+
values["mso_mode"] = values["mode"]
|
|
153
|
+
return values
|
|
154
|
+
|
|
155
|
+
@model_validator(mode="after")
|
|
156
|
+
def validate_and_build(self) -> Self:
|
|
157
|
+
"""Validate atomic numbers and build the atoms field."""
|
|
158
|
+
self.atoms = tuple(self.atoms)
|
|
159
|
+
self.fragment_indices = tuple(map(tuple, self.fragment_indices))
|
|
160
|
+
|
|
161
|
+
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:
|
|
166
|
+
# Default on
|
|
167
|
+
self.optimize_fragments = self.optimize_fragments or self.optimize_fragments is None
|
|
168
|
+
case _:
|
|
169
|
+
raise NotImplementedError(f"{self.mode} not implemented.")
|
|
170
|
+
|
|
171
|
+
for atom in self.atoms:
|
|
172
|
+
if atom > len(self.initial_molecule):
|
|
173
|
+
raise ValueError(f"{atom=} is out of range.")
|
|
174
|
+
for fragment_idxs in self.fragment_indices:
|
|
175
|
+
if any(a > len(self.initial_molecule) for a in fragment_idxs):
|
|
176
|
+
raise ValueError(f"{fragment_idxs=} contains atoms that are out of range.")
|
|
177
|
+
|
|
178
|
+
if self.all_CH:
|
|
179
|
+
self.atoms = self.atoms + tuple(H for _C, H in find_CH_bonds(self.initial_molecule))
|
|
180
|
+
if self.all_CX:
|
|
181
|
+
self.atoms = self.atoms + tuple(X for _C, X in find_CX_bonds(self.initial_molecule))
|
|
182
|
+
|
|
183
|
+
# Combine atoms and fragments, remove duplicates, and sort
|
|
184
|
+
self.fragment_indices = self.fragment_indices + tuple((a,) for a in self.atoms)
|
|
185
|
+
assert isinstance(self.fragment_indices, tuple)
|
|
186
|
+
self.fragment_indices = tuple(sorted(set(self.fragment_indices)))
|
|
187
|
+
|
|
188
|
+
return self
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def atomic_number_indices(molecule: Molecule, atomic_numbers: set[PositiveInt] | PositiveInt) -> tuple[PositiveInt, ...]:
|
|
192
|
+
r"""
|
|
193
|
+
Return the indices of the atoms with the given atomic numbers.
|
|
194
|
+
|
|
195
|
+
:param molecule: Molecule of interest
|
|
196
|
+
:param atomic_numbers: atomic number(s) of interest
|
|
197
|
+
|
|
198
|
+
>>> H2O = Molecule.from_xyz("H 0 0 0\nO 0 0 1\nH 0 1 1")
|
|
199
|
+
>>> atomic_number_indices(H2O, 1)
|
|
200
|
+
(1, 3)
|
|
201
|
+
>>> atomic_number_indices(H2O, {8, 1})
|
|
202
|
+
(1, 2, 3)
|
|
203
|
+
>>> atomic_number_indices(H2O, {6, 9})
|
|
204
|
+
()
|
|
205
|
+
"""
|
|
206
|
+
if isinstance(atomic_numbers, int):
|
|
207
|
+
return tuple(i for i, an in enumerate(molecule.atomic_numbers, start=1) if an == atomic_numbers)
|
|
208
|
+
return tuple(i for i, an in enumerate(molecule.atomic_numbers, start=1) if an in atomic_numbers)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def find_CH_bonds(molecule: Molecule, distance_max: float = 1.2) -> Iterable[tuple[PositiveInt, PositiveInt]]:
|
|
212
|
+
r"""
|
|
213
|
+
Find all C–H bonds in the molecule.
|
|
214
|
+
|
|
215
|
+
:param molecule: Molecule of interest
|
|
216
|
+
:param distance_max: distance max for bond
|
|
217
|
+
|
|
218
|
+
>>> H2O = Molecule.from_xyz("H 0 0 0\nO 0 0 1\nH 0 1 1")
|
|
219
|
+
>>> CH4 = Molecule.from_xyz("C 0 0 0\nH 0 0 1\nH 0 1 0\nH 1 0 0\nH 0 0 -1")
|
|
220
|
+
>>> ethane = Molecule.from_xyz("C -1 0 0\nC 1 0 0\nH -1 0 1\nH -1 0 -1\nH -1 1 0\nH 1 0 1\nH 1 0 -1\nH 1 1 0")
|
|
221
|
+
>>> list(find_CH_bonds(H2O))
|
|
222
|
+
[]
|
|
223
|
+
>>> list(find_CH_bonds(CH4))
|
|
224
|
+
[(1, 2), (1, 3), (1, 4), (1, 5)]
|
|
225
|
+
>>> list(find_CH_bonds(ethane))
|
|
226
|
+
[(1, 3), (1, 4), (1, 5), (2, 6), (2, 7), (2, 8)]
|
|
227
|
+
"""
|
|
228
|
+
yield from find_AB_bonds(molecule, 6, 1, distance_max)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def find_CX_bonds(molecule: Molecule) -> Iterable[tuple[PositiveInt, PositiveInt]]:
|
|
232
|
+
r"""
|
|
233
|
+
Find all C–X bonds in the molecule.
|
|
234
|
+
|
|
235
|
+
:param molecule: Molecule of interest
|
|
236
|
+
:param distance_max: distance max for bond
|
|
237
|
+
|
|
238
|
+
>>> HCF = Molecule.from_xyz("H 0 0 0\nC 0 0 1\nF 0 1 1")
|
|
239
|
+
>>> list(find_CX_bonds(HCF))
|
|
240
|
+
[(2, 3)]
|
|
241
|
+
"""
|
|
242
|
+
halogens = {9: 2.0, 17: 2.2, 35: 2.5, 53: 2.8, 85: 3.0, 117: 4.0}
|
|
243
|
+
yield from itertools.chain.from_iterable(find_AB_bonds(molecule, 6, x, distance) for x, distance in halogens.items())
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def find_AB_bonds(molecule: Molecule, a: int, b: int, distance_max: float) -> Iterable[tuple[PositiveInt, PositiveInt]]:
|
|
247
|
+
r"""
|
|
248
|
+
Find all A–B bonds in the molecule.
|
|
249
|
+
|
|
250
|
+
:param molecule: Molecule of interest
|
|
251
|
+
:param a: atomic number of atom A
|
|
252
|
+
:param b: atomic number of atom B
|
|
253
|
+
:param distance_max: distance max for bond
|
|
254
|
+
|
|
255
|
+
>>> H2O = Molecule.from_xyz("H 0 0 0\nO 0 0 1\nH 0 1 1")
|
|
256
|
+
>>> list(find_AB_bonds(H2O, 8, 1, 1.1))
|
|
257
|
+
[(2, 1), (2, 3)]
|
|
258
|
+
"""
|
|
259
|
+
|
|
260
|
+
def close(idxs: tuple[PositiveInt, PositiveInt]) -> bool:
|
|
261
|
+
return molecule.distance(*idxs) < distance_max
|
|
262
|
+
|
|
263
|
+
yield from filter(
|
|
264
|
+
close,
|
|
265
|
+
itertools.product(
|
|
266
|
+
atomic_number_indices(molecule, a),
|
|
267
|
+
atomic_number_indices(molecule, b),
|
|
268
|
+
),
|
|
269
|
+
)
|
stjames/workflows/descriptors.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from ..types import UUID
|
|
1
2
|
from .workflow import Workflow
|
|
2
3
|
|
|
3
4
|
Descriptors = dict[str, dict[str, float] | tuple[float | None, ...] | float]
|
|
@@ -5,6 +6,6 @@ Descriptors = dict[str, dict[str, float] | tuple[float | None, ...] | float]
|
|
|
5
6
|
|
|
6
7
|
class DescriptorsWorkflow(Workflow):
|
|
7
8
|
# uuid of optimization
|
|
8
|
-
optimization:
|
|
9
|
+
optimization: UUID | None = None
|
|
9
10
|
|
|
10
11
|
descriptors: Descriptors | None = None
|
stjames/workflows/fukui.py
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
from
|
|
2
|
-
|
|
1
|
+
from ..types import UUID
|
|
3
2
|
from .workflow import Workflow
|
|
4
3
|
|
|
5
4
|
|
|
6
5
|
class FukuiIndexWorkflow(Workflow):
|
|
7
6
|
# uuid of optimization
|
|
8
|
-
optimization:
|
|
7
|
+
optimization: UUID | None = None
|
|
9
8
|
|
|
10
|
-
global_electrophilicity_index:
|
|
11
|
-
fukui_positive:
|
|
12
|
-
fukui_negative:
|
|
13
|
-
fukui_zero:
|
|
9
|
+
global_electrophilicity_index: float | None = None
|
|
10
|
+
fukui_positive: list[float] | None = None
|
|
11
|
+
fukui_negative: list[float] | None = None
|
|
12
|
+
fukui_zero: list[float] | None = None
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from typing import Self
|
|
2
|
+
|
|
3
|
+
from pydantic import PositiveFloat, PositiveInt, model_validator
|
|
4
|
+
|
|
5
|
+
from ..base import Base, LowercaseStrEnum
|
|
6
|
+
from ..constraint import PairwiseHarmonicConstraint, SphericalHarmonicConstraint
|
|
7
|
+
from ..settings import Settings
|
|
8
|
+
from ..types import UUID
|
|
9
|
+
from .workflow import Workflow
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ThermodynamicEnsemble(LowercaseStrEnum):
|
|
13
|
+
NPT = "npt"
|
|
14
|
+
NVT = "nvt"
|
|
15
|
+
NVE = "nve"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Frame(Base):
|
|
19
|
+
index: int # what number frame this is within the MD simulation
|
|
20
|
+
|
|
21
|
+
uuid: UUID | None = None # UUID of molecule
|
|
22
|
+
|
|
23
|
+
pressure: float
|
|
24
|
+
temperature: float
|
|
25
|
+
energy: float
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class MolecularDynamicsSettings(Base):
|
|
29
|
+
ensemble: ThermodynamicEnsemble = ThermodynamicEnsemble.NVT
|
|
30
|
+
|
|
31
|
+
timestep: PositiveFloat = 1.0 # fs
|
|
32
|
+
num_steps: PositiveInt = 500
|
|
33
|
+
|
|
34
|
+
confining_constraint: SphericalHarmonicConstraint | None = None
|
|
35
|
+
|
|
36
|
+
temperature: PositiveFloat | None = 300 # K
|
|
37
|
+
pressure: PositiveFloat | None = None # atm
|
|
38
|
+
|
|
39
|
+
langevin_thermostat_timescale: PositiveFloat = 100 # fs
|
|
40
|
+
berendsen_barostat_timescale: PositiveFloat = 1000 # fs
|
|
41
|
+
|
|
42
|
+
constraints: list[PairwiseHarmonicConstraint] = []
|
|
43
|
+
|
|
44
|
+
@model_validator(mode="after")
|
|
45
|
+
def validate_ensemble_settings(self) -> Self:
|
|
46
|
+
"""Check that NVT ensemble always has temperature defined, and that NPT has temp and pressure defined."""
|
|
47
|
+
if self.ensemble == ThermodynamicEnsemble.NVT and self.temperature is None:
|
|
48
|
+
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):
|
|
50
|
+
raise ValueError("NPT ensemble must have both temperature and pressure defined")
|
|
51
|
+
return self
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class MolecularDynamicsWorkflow(Workflow):
|
|
55
|
+
settings: MolecularDynamicsSettings
|
|
56
|
+
calc_settings: Settings
|
|
57
|
+
calc_engine: str | None = None
|
|
58
|
+
|
|
59
|
+
# uuids of scan points
|
|
60
|
+
frames: list[Frame] = []
|