stjames 0.0.114__tar.gz → 0.0.135__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.
- {stjames-0.0.114/stjames.egg-info → stjames-0.0.135}/PKG-INFO +1 -1
- {stjames-0.0.114 → stjames-0.0.135}/pyproject.toml +1 -1
- {stjames-0.0.114 → stjames-0.0.135}/stjames/__init__.py +1 -0
- {stjames-0.0.114 → stjames-0.0.135}/stjames/atomium_stjames/mmcif.py +2 -2
- {stjames-0.0.114 → stjames-0.0.135}/stjames/atomium_stjames/pdb.py +1 -1
- {stjames-0.0.114 → stjames-0.0.135}/stjames/calculation.py +15 -2
- stjames-0.0.135/stjames/dna.py +13 -0
- {stjames-0.0.114 → stjames-0.0.135}/stjames/engine.py +1 -0
- {stjames-0.0.114 → stjames-0.0.135}/stjames/method.py +4 -3
- {stjames-0.0.114 → stjames-0.0.135}/stjames/molecule.py +96 -18
- stjames-0.0.135/stjames/periodic_cell.py +82 -0
- stjames-0.0.135/stjames/protein.py +15 -0
- stjames-0.0.135/stjames/rna.py +13 -0
- {stjames-0.0.114 → stjames-0.0.135}/stjames/settings.py +11 -2
- {stjames-0.0.114 → stjames-0.0.135}/stjames/solvent.py +2 -0
- {stjames-0.0.114 → stjames-0.0.135}/stjames/workflows/__init__.py +17 -0
- {stjames-0.0.114 → stjames-0.0.135}/stjames/workflows/basic_calculation.py +7 -2
- stjames-0.0.135/stjames/workflows/batch_docking.py +46 -0
- {stjames-0.0.114 → stjames-0.0.135}/stjames/workflows/bde.py +1 -1
- {stjames-0.0.114 → stjames-0.0.135}/stjames/workflows/conformer_search.py +172 -18
- {stjames-0.0.114 → stjames-0.0.135}/stjames/workflows/docking.py +12 -1
- {stjames-0.0.114 → stjames-0.0.135}/stjames/workflows/fukui.py +6 -3
- {stjames-0.0.114 → stjames-0.0.135}/stjames/workflows/molecular_dynamics.py +3 -1
- stjames-0.0.135/stjames/workflows/msa.py +24 -0
- {stjames-0.0.114 → stjames-0.0.135}/stjames/workflows/pka.py +4 -4
- {stjames-0.0.114 → stjames-0.0.135}/stjames/workflows/pose_analysis_md.py +4 -4
- stjames-0.0.135/stjames/workflows/protein_binder_design.py +303 -0
- {stjames-0.0.114 → stjames-0.0.135}/stjames/workflows/protein_cofolding.py +10 -5
- stjames-0.0.135/stjames/workflows/relative_binding_free_energy_perturbation.py +180 -0
- {stjames-0.0.114 → stjames-0.0.135}/stjames/workflows/scan.py +12 -5
- stjames-0.0.135/stjames/workflows/solvent_dependent_conformers.py +85 -0
- stjames-0.0.135/stjames/workflows/workflow.py +115 -0
- {stjames-0.0.114 → stjames-0.0.135/stjames.egg-info}/PKG-INFO +1 -1
- {stjames-0.0.114 → stjames-0.0.135}/stjames.egg-info/SOURCES.txt +8 -0
- stjames-0.0.135/tests/test_molecule.py +90 -0
- stjames-0.0.114/stjames/periodic_cell.py +0 -34
- stjames-0.0.114/stjames/workflows/workflow.py +0 -81
- stjames-0.0.114/tests/test_molecule.py +0 -39
- {stjames-0.0.114 → stjames-0.0.135}/LICENSE +0 -0
- {stjames-0.0.114 → stjames-0.0.135}/README.md +0 -0
- {stjames-0.0.114 → stjames-0.0.135}/setup.cfg +0 -0
- {stjames-0.0.114 → stjames-0.0.135}/stjames/_deprecated_solvent_settings.py +0 -0
- {stjames-0.0.114 → stjames-0.0.135}/stjames/atom.py +0 -0
- {stjames-0.0.114 → stjames-0.0.135}/stjames/atomium_stjames/__init__.py +0 -0
- {stjames-0.0.114 → stjames-0.0.135}/stjames/atomium_stjames/data.py +0 -0
- {stjames-0.0.114 → stjames-0.0.135}/stjames/atomium_stjames/utilities.py +0 -0
- {stjames-0.0.114 → stjames-0.0.135}/stjames/base.py +0 -0
- {stjames-0.0.114 → stjames-0.0.135}/stjames/basis_set.py +0 -0
- {stjames-0.0.114 → stjames-0.0.135}/stjames/compute_settings.py +0 -0
- {stjames-0.0.114 → stjames-0.0.135}/stjames/constraint.py +0 -0
- {stjames-0.0.114 → stjames-0.0.135}/stjames/correction.py +0 -0
- {stjames-0.0.114 → stjames-0.0.135}/stjames/data/__init__.py +0 -0
- {stjames-0.0.114 → stjames-0.0.135}/stjames/data/bragg_radii.json +0 -0
- {stjames-0.0.114 → stjames-0.0.135}/stjames/data/elements.py +0 -0
- {stjames-0.0.114 → stjames-0.0.135}/stjames/data/isotopes.json +0 -0
- {stjames-0.0.114 → stjames-0.0.135}/stjames/data/nist_isotopes.json +0 -0
- {stjames-0.0.114 → stjames-0.0.135}/stjames/data/read_nist_isotopes.py +0 -0
- {stjames-0.0.114 → stjames-0.0.135}/stjames/data/symbol_element.json +0 -0
- {stjames-0.0.114 → stjames-0.0.135}/stjames/message.py +0 -0
- {stjames-0.0.114 → stjames-0.0.135}/stjames/mode.py +0 -0
- {stjames-0.0.114 → stjames-0.0.135}/stjames/opt_settings.py +0 -0
- {stjames-0.0.114 → stjames-0.0.135}/stjames/optimization/__init__.py +0 -0
- {stjames-0.0.114 → stjames-0.0.135}/stjames/optimization/freezing_string_method.py +0 -0
- {stjames-0.0.114 → stjames-0.0.135}/stjames/pdb.py +0 -0
- {stjames-0.0.114 → stjames-0.0.135}/stjames/py.typed +0 -0
- {stjames-0.0.114 → stjames-0.0.135}/stjames/scf_settings.py +0 -0
- {stjames-0.0.114 → stjames-0.0.135}/stjames/status.py +0 -0
- {stjames-0.0.114 → stjames-0.0.135}/stjames/task.py +0 -0
- {stjames-0.0.114 → stjames-0.0.135}/stjames/thermochem_settings.py +0 -0
- {stjames-0.0.114 → stjames-0.0.135}/stjames/types.py +0 -0
- {stjames-0.0.114 → stjames-0.0.135}/stjames/workflows/admet.py +0 -0
- {stjames-0.0.114 → stjames-0.0.135}/stjames/workflows/conformer.py +0 -0
- {stjames-0.0.114 → stjames-0.0.135}/stjames/workflows/descriptors.py +0 -0
- {stjames-0.0.114 → stjames-0.0.135}/stjames/workflows/double_ended_ts_search.py +0 -0
- {stjames-0.0.114 → stjames-0.0.135}/stjames/workflows/electronic_properties.py +0 -0
- {stjames-0.0.114 → stjames-0.0.135}/stjames/workflows/hydrogen_bond_basicity.py +0 -0
- {stjames-0.0.114 → stjames-0.0.135}/stjames/workflows/ion_mobility.py +0 -0
- {stjames-0.0.114 → stjames-0.0.135}/stjames/workflows/irc.py +0 -0
- {stjames-0.0.114 → stjames-0.0.135}/stjames/workflows/macropka.py +0 -0
- {stjames-0.0.114 → stjames-0.0.135}/stjames/workflows/multistage_opt.py +0 -0
- {stjames-0.0.114 → stjames-0.0.135}/stjames/workflows/nmr.py +0 -0
- {stjames-0.0.114 → stjames-0.0.135}/stjames/workflows/redox_potential.py +0 -0
- {stjames-0.0.114 → stjames-0.0.135}/stjames/workflows/solubility.py +0 -0
- {stjames-0.0.114 → stjames-0.0.135}/stjames/workflows/spin_states.py +0 -0
- {stjames-0.0.114 → stjames-0.0.135}/stjames/workflows/strain.py +0 -0
- {stjames-0.0.114 → stjames-0.0.135}/stjames/workflows/tautomer.py +0 -0
- {stjames-0.0.114 → stjames-0.0.135}/stjames.egg-info/dependency_links.txt +0 -0
- {stjames-0.0.114 → stjames-0.0.135}/stjames.egg-info/requires.txt +0 -0
- {stjames-0.0.114 → stjames-0.0.135}/stjames.egg-info/top_level.txt +0 -0
- {stjames-0.0.114 → stjames-0.0.135}/tests/test_constraints.py +0 -0
- {stjames-0.0.114 → stjames-0.0.135}/tests/test_from_extxyz.py +0 -0
- {stjames-0.0.114 → stjames-0.0.135}/tests/test_pdb.py +0 -0
- {stjames-0.0.114 → stjames-0.0.135}/tests/test_rounding.py +0 -0
- {stjames-0.0.114 → stjames-0.0.135}/tests/test_settings.py +0 -0
|
@@ -512,7 +512,7 @@ def add_atom_to_polymer(atom: dict[str, Any], aniso: dict[int, Any], model: dict
|
|
|
512
512
|
try:
|
|
513
513
|
model["polymer"][mol_id]["residues"][res_id]["atoms"][int(atom["id"])] = atom_dict_to_atom_dict(atom, aniso)
|
|
514
514
|
except Exception:
|
|
515
|
-
name = atom
|
|
515
|
+
name = atom.get("auth_comp_id") or atom.get("label_comp_id") or "UNKNOWN"
|
|
516
516
|
try:
|
|
517
517
|
model["polymer"][mol_id]["residues"][res_id] = {
|
|
518
518
|
"name": name,
|
|
@@ -553,7 +553,7 @@ def add_atom_to_non_polymer(atom: dict[str, Any], aniso: dict[int, Any], model:
|
|
|
553
553
|
try:
|
|
554
554
|
model[mol_type][mol_id]["atoms"][int(atom["id"])] = atom_dict_to_atom_dict(atom, aniso)
|
|
555
555
|
except Exception:
|
|
556
|
-
name = atom
|
|
556
|
+
name = atom.get("auth_comp_id") or atom.get("label_comp_id") or "UNKNOWN"
|
|
557
557
|
model[mol_type][mol_id] = {
|
|
558
558
|
"name": name,
|
|
559
559
|
"full_name": names.get(name).upper() if names.get(name) is not None and names.get(name).lower() != "water" else None, # type: ignore [union-attr]
|
|
@@ -321,7 +321,7 @@ def assembly_lines_to_assembly_dict(lines: list[str]) -> dict[str, Any]:
|
|
|
321
321
|
(r"(.+)AREA OF THE COMPLEX: (.+) [A-Z]", "surface_area", float),
|
|
322
322
|
(r"(.+)FREE ENERGY: (.+) [A-Z]", "delta_energy", float),
|
|
323
323
|
]
|
|
324
|
-
t = None
|
|
324
|
+
t: Any = None
|
|
325
325
|
for line in lines:
|
|
326
326
|
for pattern, key, converter in patterns:
|
|
327
327
|
matches = re.findall(pattern, line)
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
from typing import Optional
|
|
1
|
+
from typing import Optional, Self
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from pydantic import model_validator
|
|
4
|
+
|
|
5
|
+
from .base import Base, LowercaseStrEnum, UniqueList
|
|
4
6
|
from .message import Message
|
|
5
7
|
from .molecule import Molecule
|
|
6
8
|
from .settings import Settings
|
|
7
9
|
from .status import Status
|
|
10
|
+
from .task import Task
|
|
8
11
|
from .types import UUID
|
|
9
12
|
|
|
10
13
|
|
|
@@ -20,6 +23,7 @@ class StJamesVersion(LowercaseStrEnum):
|
|
|
20
23
|
class Calculation(Base):
|
|
21
24
|
molecules: list[Molecule]
|
|
22
25
|
|
|
26
|
+
tasks: UniqueList[Task] = []
|
|
23
27
|
settings: Settings = Settings()
|
|
24
28
|
|
|
25
29
|
status: Status = Status.QUEUED
|
|
@@ -29,8 +33,17 @@ class Calculation(Base):
|
|
|
29
33
|
logfile: Optional[str] = None
|
|
30
34
|
messages: list[Message] = []
|
|
31
35
|
|
|
36
|
+
# DEPRECATED - moving into settings
|
|
32
37
|
engine: Optional[str] = "peregrine"
|
|
38
|
+
|
|
33
39
|
uuids: list[UUID | None] | None = None
|
|
34
40
|
|
|
35
41
|
# not to be changed by end users, diff. versions will have diff. defaults
|
|
36
42
|
json_format: str = StJamesVersion.V0
|
|
43
|
+
|
|
44
|
+
@model_validator(mode="after")
|
|
45
|
+
def populate_tasks(self) -> Self:
|
|
46
|
+
"""Set the tasks from the settings, so that we don't have to migrate old entries."""
|
|
47
|
+
if len(self.tasks) == 0:
|
|
48
|
+
self.tasks = self.settings.tasks
|
|
49
|
+
return self
|
|
@@ -65,6 +65,7 @@ class Method(LowercaseStrEnum):
|
|
|
65
65
|
|
|
66
66
|
# Force fields
|
|
67
67
|
OFF_SAGE_2_2_1 = "off_sage_2_2_1"
|
|
68
|
+
SMIRNOFF_2_2_1_AMBER_AM1BCC = "smirnoff_2_2_1_amber_am1bcc"
|
|
68
69
|
|
|
69
70
|
def default_engine(self, *, is_periodic: bool = False) -> Engine:
|
|
70
71
|
"""
|
|
@@ -93,7 +94,7 @@ class Method(LowercaseStrEnum):
|
|
|
93
94
|
return Engine.ORB
|
|
94
95
|
case method if method in XTB_METHODS:
|
|
95
96
|
return Engine.TBLITE if is_periodic else Engine.XTB
|
|
96
|
-
case Method.OFF_SAGE_2_2_1:
|
|
97
|
+
case Method.OFF_SAGE_2_2_1 | Method.SMIRNOFF_2_2_1_AMBER_AM1BCC:
|
|
97
98
|
return Engine.OPENFF
|
|
98
99
|
case Method.EGRET_1 | Method.EGRET_1E | Method.EGRET_1T:
|
|
99
100
|
return Engine.EGRET
|
|
@@ -140,8 +141,8 @@ XTB_METHODS = [Method.GFN_FF, Method.GFN0_XTB, Method.GFN1_XTB, Method.GFN2_XTB,
|
|
|
140
141
|
CompositeMethod = Literal[Method.HF3C, Method.B973C, Method.R2SCAN3C, Method.WB97X3C]
|
|
141
142
|
COMPOSITE_METHODS = [Method.HF3C, Method.B973C, Method.R2SCAN3C, Method.WB97X3C]
|
|
142
143
|
|
|
143
|
-
FFMethod = Literal[Method.OFF_SAGE_2_2_1]
|
|
144
|
-
FF_METHODS = [Method.OFF_SAGE_2_2_1]
|
|
144
|
+
FFMethod = Literal[Method.OFF_SAGE_2_2_1, Method.SMIRNOFF_2_2_1_AMBER_AM1BCC]
|
|
145
|
+
FF_METHODS = [Method.OFF_SAGE_2_2_1, Method.SMIRNOFF_2_2_1_AMBER_AM1BCC]
|
|
145
146
|
|
|
146
147
|
PrepackagedMethod = XTBMethod | CompositeMethod | PrepackagedNNPMethod | FFMethod
|
|
147
148
|
PREPACKAGED_METHODS = [*XTB_METHODS, *COMPOSITE_METHODS, *PREPACKAGED_NNP_METHODS, *FF_METHODS]
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
import re
|
|
2
3
|
from pathlib import Path
|
|
3
4
|
from typing import Annotated, Any, Iterable, Optional, Self, Sequence, TypeAlias, TypedDict, TypeVar
|
|
@@ -26,6 +27,8 @@ from .types import (
|
|
|
26
27
|
|
|
27
28
|
RdkitMol: TypeAlias = Chem.rdchem.Mol | Chem.rdchem.RWMol
|
|
28
29
|
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
29
32
|
|
|
30
33
|
class MoleculeReadError(RuntimeError):
|
|
31
34
|
pass
|
|
@@ -123,7 +126,7 @@ class Molecule(Base):
|
|
|
123
126
|
>>> mol = Molecule.from_xyz("H 0 0 0\nH 0 0 1")
|
|
124
127
|
>>> print(mol.translated((1, 0, 0)).to_xyz())
|
|
125
128
|
2
|
|
126
|
-
|
|
129
|
+
charge: 0; multiplicity: 1;
|
|
127
130
|
H 1.0000000000 0.0000000000 0.0000000000
|
|
128
131
|
H 1.0000000000 0.0000000000 1.0000000000
|
|
129
132
|
"""
|
|
@@ -131,9 +134,9 @@ class Molecule(Base):
|
|
|
131
134
|
def translated(position: Vector3D) -> Vector3D:
|
|
132
135
|
return tuple(q + v for q, v in zip(position, vector, strict=True)) # type: ignore [return-value]
|
|
133
136
|
|
|
134
|
-
atoms = [atom.
|
|
137
|
+
atoms = [atom.model_copy(update={"position": translated(atom.position)}) for atom in self.atoms]
|
|
135
138
|
|
|
136
|
-
return self.
|
|
139
|
+
return self.model_copy(update={"atoms": atoms})
|
|
137
140
|
|
|
138
141
|
@property
|
|
139
142
|
def atomic_numbers(self) -> list[NonNegativeInt]:
|
|
@@ -184,7 +187,7 @@ class Molecule(Base):
|
|
|
184
187
|
return self
|
|
185
188
|
|
|
186
189
|
@classmethod
|
|
187
|
-
def from_file(cls: type[Self], filename: Path | str, format: str | None = None, charge: int =
|
|
190
|
+
def from_file(cls: type[Self], filename: Path | str, format: str | None = None, charge: int | None = None, multiplicity: PositiveInt | None = None) -> Self:
|
|
188
191
|
r"""
|
|
189
192
|
Read a molecule from a file.
|
|
190
193
|
|
|
@@ -195,7 +198,7 @@ class Molecule(Base):
|
|
|
195
198
|
... mol = Molecule.from_file(f.name)
|
|
196
199
|
>>> print(mol.to_xyz())
|
|
197
200
|
2
|
|
198
|
-
|
|
201
|
+
charge: 0; multiplicity: 1;
|
|
199
202
|
H 0.0000000000 0.0000000000 0.0000000000
|
|
200
203
|
F 0.0000000000 0.0000000000 1.0000000000
|
|
201
204
|
"""
|
|
@@ -213,7 +216,7 @@ class Molecule(Base):
|
|
|
213
216
|
raise ValueError(f"Unsupported {format=}")
|
|
214
217
|
|
|
215
218
|
@classmethod
|
|
216
|
-
def from_xyz(cls: type[Self], xyz: str, charge: int =
|
|
219
|
+
def from_xyz(cls: type[Self], xyz: str, charge: int | None = None, multiplicity: PositiveInt | None = None) -> Self:
|
|
217
220
|
r"""
|
|
218
221
|
Generate a Molecule from an XYZ string.
|
|
219
222
|
|
|
@@ -225,38 +228,113 @@ class Molecule(Base):
|
|
|
225
228
|
return cls.from_xyz_lines(xyz.strip().splitlines(), charge=charge, multiplicity=multiplicity)
|
|
226
229
|
|
|
227
230
|
@classmethod
|
|
228
|
-
def from_xyz_lines(cls: type[Self], lines: Iterable[str], charge: int =
|
|
231
|
+
def from_xyz_lines(cls: type[Self], lines: Iterable[str], charge: int | None = None, multiplicity: PositiveInt | None = None) -> Self:
|
|
232
|
+
r"""
|
|
233
|
+
Read a molecule from a xyz lines.
|
|
234
|
+
|
|
235
|
+
>>> mol = Molecule.from_xyz_lines(["2", "charge: 0; multiplicity: 1; cell: [[1, 2e1, 3], [4, 5, 6], [7, 8, 9.1]]", "H 0 0 0", "F 0 0 1"])
|
|
236
|
+
>>> print(mol.to_xyz())
|
|
237
|
+
2
|
|
238
|
+
charge: 0; multiplicity: 1; cell: ((1.0, 20.0, 3.0), (4.0, 5.0, 6.0), (7.0, 8.0, 9.1)); is_periodic: (True, True, True);
|
|
239
|
+
H 0.0000000000 0.0000000000 0.0000000000
|
|
240
|
+
F 0.0000000000 0.0000000000 1.0000000000
|
|
241
|
+
>>> mol = Molecule.from_xyz_lines(["2", "energy: abc", "H 0 0 0", "F 0 0 1"])
|
|
242
|
+
>>> print(mol.to_xyz())
|
|
243
|
+
2
|
|
244
|
+
charge: 0; multiplicity: 1;
|
|
245
|
+
H 0.0000000000 0.0000000000 0.0000000000
|
|
246
|
+
F 0.0000000000 0.0000000000 1.0000000000
|
|
247
|
+
"""
|
|
229
248
|
lines = list(lines)
|
|
249
|
+
data: dict[str, Any] = {}
|
|
230
250
|
if len(lines[0].split()) == 1:
|
|
231
|
-
natoms = lines
|
|
232
|
-
if not natoms.isdigit() or (int(
|
|
233
|
-
raise MoleculeReadError(f"First line of XYZ file should be the number of atoms
|
|
234
|
-
|
|
251
|
+
natoms, comment, *lines = lines
|
|
252
|
+
if (not natoms.strip().isdigit()) or (int(natoms) != len(lines)):
|
|
253
|
+
raise MoleculeReadError(f"First line of XYZ file should be the number of atoms ({len(lines)}), got: {natoms}")
|
|
254
|
+
|
|
255
|
+
data = cls._parse_comment_line(comment)
|
|
256
|
+
|
|
257
|
+
charge = charge if charge is not None else data.get("charge", 0)
|
|
258
|
+
multiplicity = multiplicity or data.get("multiplicity", 1)
|
|
259
|
+
data |= {"charge": charge, "multiplicity": multiplicity}
|
|
260
|
+
|
|
261
|
+
try:
|
|
262
|
+
return cls(atoms=[Atom.from_xyz(line) for line in lines], **data)
|
|
263
|
+
except (ValueError, ValidationError):
|
|
264
|
+
pass
|
|
235
265
|
|
|
236
266
|
try:
|
|
237
267
|
return cls(atoms=[Atom.from_xyz(line) for line in lines], charge=charge, multiplicity=multiplicity)
|
|
238
268
|
except (ValueError, ValidationError) as e:
|
|
239
269
|
raise MoleculeReadError("Error reading molecule from xyz") from e
|
|
240
270
|
|
|
241
|
-
|
|
271
|
+
@classmethod
|
|
272
|
+
def _parse_comment_line(cls, comment: str) -> dict[str, Any]:
|
|
273
|
+
"""
|
|
274
|
+
Parse the comment line of an XYZ file.
|
|
275
|
+
|
|
276
|
+
:param comment: comment line from an XYZ file
|
|
277
|
+
|
|
278
|
+
>>> Molecule._parse_comment_line("charge: -1; multiplicity: 2; cell: [[1,2,3],[4,5,6],[7,8,9]]; is_periodic: (True, False, True); energy: -75.0")
|
|
279
|
+
{'charge': '-1', 'multiplicity': '2', 'energy': '-75.0', 'cell':\
|
|
280
|
+
PeriodicCell(lattice_vectors=((1.0, 2.0, 3.0), (4.0, 5.0, 6.0), (7.0, 8.0, 9.0)), is_periodic=(True, False, True))}
|
|
281
|
+
>>> Molecule._parse_comment_line(" energy: -0.320207535977 gnorm: 0.071552110436 xtb: 6.6.1 (8d0f1dd)") # Unfortunate
|
|
282
|
+
{'energy': '-0.320207535977 gnorm: 0.071552110436 xtb: 6.6.1 (8d0f1dd)'}
|
|
283
|
+
"""
|
|
284
|
+
data: dict[str, Any] = {}
|
|
285
|
+
for kv in comment.split(";"):
|
|
286
|
+
try:
|
|
287
|
+
key, value = kv.split(":", 1)
|
|
288
|
+
data[key.strip()] = value.strip()
|
|
289
|
+
except ValueError:
|
|
290
|
+
logger.error(f"Error parsing key/value: {kv}")
|
|
291
|
+
continue
|
|
292
|
+
|
|
293
|
+
if cell := data.pop("cell", None):
|
|
294
|
+
try:
|
|
295
|
+
data["cell"] = PeriodicCell.from_string(cell)
|
|
296
|
+
if is_periodic := data.pop("is_periodic", None):
|
|
297
|
+
x, y, z = is_periodic.strip(" ([)]").split(",")
|
|
298
|
+
true_values = {"true", "1", "yes"}
|
|
299
|
+
data["cell"].is_periodic = tuple(map(lambda v: v.strip().lower() in true_values, (x, y, z)))
|
|
300
|
+
except ValueError as e:
|
|
301
|
+
logger.error(f"Error parsing XYZ cell: {e}")
|
|
302
|
+
pass
|
|
303
|
+
|
|
304
|
+
return data
|
|
305
|
+
|
|
306
|
+
def to_xyz(self, comment: str | None = None, out_file: Path | str | None = None) -> str:
|
|
242
307
|
r"""
|
|
243
308
|
Generate an XYZ string.
|
|
244
309
|
|
|
245
|
-
|
|
246
|
-
|
|
310
|
+
:param comment: optional comment line (defaults to standard Rowan XYZ comment line format)
|
|
311
|
+
:param out_file: optional output file path to write the XYZ string to
|
|
312
|
+
:return: XYZ string
|
|
313
|
+
|
|
314
|
+
>>> mol = Molecule.from_xyz("2\nenergy: 1.0; smiles: HF;\nH 0 1 2\nF 1 2 3")
|
|
315
|
+
>>> print(mol.to_xyz())
|
|
247
316
|
2
|
|
248
|
-
HF
|
|
317
|
+
charge: 0; multiplicity: 1; energy: 1.0; smiles: HF;
|
|
249
318
|
H 0.0000000000 1.0000000000 2.0000000000
|
|
250
319
|
F 1.0000000000 2.0000000000 3.0000000000
|
|
251
320
|
>>> import tempfile
|
|
252
321
|
>>> with tempfile.TemporaryDirectory() as directory:
|
|
253
322
|
... file = Path(directory) / "mol.xyz"
|
|
254
|
-
... out = mol.to_xyz(
|
|
323
|
+
... out = mol.to_xyz(out_file=file)
|
|
255
324
|
... with file.open() as f:
|
|
256
|
-
... Molecule.from_xyz(f.read()).to_xyz(
|
|
325
|
+
... Molecule.from_xyz(f.read()).to_xyz() == out
|
|
257
326
|
True
|
|
258
327
|
"""
|
|
259
328
|
geom = "\n".join(map(str, self.atoms))
|
|
329
|
+
if comment is None:
|
|
330
|
+
data = self.model_dump(exclude_none=True, exclude={"atoms"})
|
|
331
|
+
|
|
332
|
+
if cell := data.pop("cell", None):
|
|
333
|
+
data["cell"] = cell["lattice_vectors"]
|
|
334
|
+
data["is_periodic"] = cell["is_periodic"]
|
|
335
|
+
|
|
336
|
+
comment = " ".join(f"{key}: {value};" for key, value in data.items())
|
|
337
|
+
|
|
260
338
|
out = f"{len(self)}\n{comment}\n{geom}"
|
|
261
339
|
|
|
262
340
|
if out_file:
|
|
@@ -442,7 +520,7 @@ def parse_extxyz_comment_line(line: str) -> EXTXYZMetadata:
|
|
|
442
520
|
:return: parsed properties
|
|
443
521
|
|
|
444
522
|
>>> parse_extxyz_comment_line('Lattice="6.0 0.0 0.0 6.0 0.0 0.0 6.0 0.0 0.0"Properties=species:S:1:pos:R:3')
|
|
445
|
-
{'cell': PeriodicCell(lattice_vectors=((6.0, 0.0, 0.0), (6.0, 0.0, 0.0), (6.0, 0.0, 0.0)), is_periodic=(True, True, True)
|
|
523
|
+
{'cell': PeriodicCell(lattice_vectors=((6.0, 0.0, 0.0), (6.0, 0.0, 0.0), (6.0, 0.0, 0.0)), is_periodic=(True, True, True)), 'properties': 'species:S:1:pos:R:3'}
|
|
446
524
|
""" # noqa: E501
|
|
447
525
|
|
|
448
526
|
# Regular expression to match key="value", key='value', or key=value
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
from typing import Annotated, Self, TypeAlias
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pydantic
|
|
6
|
+
|
|
7
|
+
from .base import Base
|
|
8
|
+
from .types import Matrix3x3, round_matrix3x3
|
|
9
|
+
|
|
10
|
+
Bool3: TypeAlias = tuple[bool, bool, bool]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PeriodicCell(Base):
|
|
14
|
+
lattice_vectors: Annotated[Matrix3x3, pydantic.AfterValidator(round_matrix3x3(6))]
|
|
15
|
+
is_periodic: Bool3 = (True, True, True)
|
|
16
|
+
|
|
17
|
+
def __repr__(self) -> str:
|
|
18
|
+
"""
|
|
19
|
+
Return a string representation of the PeriodicCell.
|
|
20
|
+
|
|
21
|
+
>>> PeriodicCell(lattice_vectors=[[1, 2, 3], [4, 5, 6], [7, 8, 9]])
|
|
22
|
+
PeriodicCell(lattice_vectors=((1.0, 2.0, 3.0), (4.0, 5.0, 6.0), (7.0, 8.0, 9.0)), is_periodic=(True, True, True))
|
|
23
|
+
"""
|
|
24
|
+
return f"PeriodicCell(lattice_vectors={self.lattice_vectors}, is_periodic={self.is_periodic})"
|
|
25
|
+
|
|
26
|
+
@pydantic.field_validator("lattice_vectors")
|
|
27
|
+
@classmethod
|
|
28
|
+
def check_tensor_3D(cls, v: Matrix3x3) -> Matrix3x3:
|
|
29
|
+
if len(v) != 3 or any(len(row) != 3 for row in v):
|
|
30
|
+
raise ValueError("Cell tensor must be a 3x3 list of floats")
|
|
31
|
+
|
|
32
|
+
return v
|
|
33
|
+
|
|
34
|
+
@pydantic.field_validator("is_periodic")
|
|
35
|
+
@classmethod
|
|
36
|
+
def check_pbc(cls, v: Bool3) -> Bool3:
|
|
37
|
+
if not any(v):
|
|
38
|
+
raise ValueError("For periodic boundary conditions, at least one dimension must be periodic!")
|
|
39
|
+
return v
|
|
40
|
+
|
|
41
|
+
@pydantic.computed_field # type: ignore[misc, prop-decorator, unused-ignore]
|
|
42
|
+
@property
|
|
43
|
+
def volume(self) -> float:
|
|
44
|
+
return float(np.abs(np.linalg.det(np.array(self.lattice_vectors))))
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def from_string(cls, string: str) -> Self:
|
|
48
|
+
"""
|
|
49
|
+
Create a PeriodicCell from a string representation.
|
|
50
|
+
|
|
51
|
+
>>> PeriodicCell.from_string("[(1, -2, 3.0), [4e1, +5, 6], (7.1, 8, 9)]")
|
|
52
|
+
PeriodicCell(lattice_vectors=((1.0, -2.0, 3.0), (40.0, 5.0, 6.0), (7.1, 8.0, 9.0)), is_periodic=(True, True, True))
|
|
53
|
+
>>> PeriodicCell.from_string("[(a, -2, 3.0), [4e1, +5, 6], (7.1, 8, 9)]")
|
|
54
|
+
Traceback (most recent call last):
|
|
55
|
+
...
|
|
56
|
+
ValueError: Could not parse cell from string: [(a, -2, 3.0), [4e1, +5, 6], (7.1, 8, 9)]
|
|
57
|
+
>>> PeriodicCell.from_string("[(1, -2, 3.0), [4e1, +5, 6], (7.1, 8, 9, 10)]")
|
|
58
|
+
Traceback (most recent call last):
|
|
59
|
+
...
|
|
60
|
+
ValueError: Cell must be a 3x3 matrix: got [(1, -2, 3.0), [4e1, +5, 6], (7.1, 8, 9, 10)]
|
|
61
|
+
>>> PeriodicCell.from_string("[('a', -2, 3.0), [4e1, +5, 6], (7.1, 8, 9)]")
|
|
62
|
+
Traceback (most recent call last):
|
|
63
|
+
ValueError: Cell must be a 3x3 matrix of numbers: got [('a', -2, 3.0), [4e1, +5, 6], (7.1, 8, 9)]
|
|
64
|
+
|
|
65
|
+
"""
|
|
66
|
+
try:
|
|
67
|
+
cell = ast.literal_eval(string)
|
|
68
|
+
except (ValueError, SyntaxError) as e:
|
|
69
|
+
raise ValueError(f"Could not parse cell from string: {string}") from e
|
|
70
|
+
|
|
71
|
+
if not isinstance(cell, (list, tuple)):
|
|
72
|
+
raise ValueError(f"Cell must be a list or tuple: got {string}")
|
|
73
|
+
if not len(cell) == 3:
|
|
74
|
+
raise ValueError(f"Cell must be a 3x3 matrix: got {string}")
|
|
75
|
+
if not all(isinstance(row, (list, tuple)) for row in cell):
|
|
76
|
+
raise ValueError(f"Cell must be a 3x3 matrix: got {string}")
|
|
77
|
+
if not all(len(row) == 3 for row in cell):
|
|
78
|
+
raise ValueError(f"Cell must be a 3x3 matrix: got {string}")
|
|
79
|
+
if not all(all(isinstance(x, (int, float)) for x in row) for row in cell):
|
|
80
|
+
raise ValueError(f"Cell must be a 3x3 matrix of numbers: got {string}")
|
|
81
|
+
|
|
82
|
+
return cls(lattice_vectors=cell)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Protein-related data models."""
|
|
2
|
+
|
|
3
|
+
from .base import Base
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ProteinSequence(Base):
|
|
7
|
+
"""
|
|
8
|
+
Protein sequence data.
|
|
9
|
+
|
|
10
|
+
:param sequence: amino-acid sequence string
|
|
11
|
+
:param cyclic: whether this sequence forms a cyclic peptide
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
sequence: str
|
|
15
|
+
cyclic: bool = False
|
|
@@ -6,6 +6,7 @@ from .base import Base, UniqueList
|
|
|
6
6
|
from .basis_set import BasisSet
|
|
7
7
|
from .compute_settings import ComputeSettings
|
|
8
8
|
from .correction import Correction
|
|
9
|
+
from .engine import Engine
|
|
9
10
|
from .method import CORRECTABLE_NNP_METHODS, METHODS_WITH_CORRECTION, PREPACKAGED_METHODS, Method
|
|
10
11
|
from .mode import Mode
|
|
11
12
|
from .opt_settings import OptimizationSettings
|
|
@@ -20,11 +21,13 @@ _T = TypeVar("_T")
|
|
|
20
21
|
class Settings(Base):
|
|
21
22
|
mode: Mode = Mode.AUTO
|
|
22
23
|
|
|
24
|
+
# DEPRECATED - specify tasks only in BasicCalculationWorkflow or Calculation now
|
|
25
|
+
tasks: UniqueList[Task] = [Task.ENERGY, Task.CHARGE, Task.DIPOLE]
|
|
26
|
+
|
|
23
27
|
method: Method = Method.HARTREE_FOCK
|
|
24
28
|
basis_set: Optional[BasisSet] = None
|
|
25
|
-
|
|
29
|
+
engine: Engine = None # type: ignore [assignment]
|
|
26
30
|
corrections: UniqueList[Correction] = []
|
|
27
|
-
|
|
28
31
|
solvent_settings: Optional[SolventSettings] = None
|
|
29
32
|
|
|
30
33
|
# scf/opt settings will be set automatically based on mode, but can be overridden manually
|
|
@@ -33,6 +36,12 @@ class Settings(Base):
|
|
|
33
36
|
thermochem_settings: ThermochemistrySettings = ThermochemistrySettings()
|
|
34
37
|
compute_settings: ComputeSettings = ComputeSettings()
|
|
35
38
|
|
|
39
|
+
@model_validator(mode="after")
|
|
40
|
+
def set_engine(self) -> Self:
|
|
41
|
+
"""Set the calculation engine."""
|
|
42
|
+
self.engine = self.engine or self.method.default_engine()
|
|
43
|
+
return self
|
|
44
|
+
|
|
36
45
|
# mypy has this dead wrong (https://docs.pydantic.dev/2.0/usage/computed_fields/)
|
|
37
46
|
# Python 3.12 narrows the reason for the ignore to prop-decorator
|
|
38
47
|
@computed_field # type: ignore[misc, prop-decorator, unused-ignore]
|
|
@@ -26,6 +26,7 @@ class Solvent(LowercaseStrEnum):
|
|
|
26
26
|
METHANOL = "methanol"
|
|
27
27
|
ETHANOL = "ethanol"
|
|
28
28
|
ISOPROPANOL = "isopropanol"
|
|
29
|
+
OCTANOL = "octanol"
|
|
29
30
|
DIMETHYLACETAMIDE = "dimethylacetamide"
|
|
30
31
|
DIMETHYLFORMAMIDE = "dimethylformamide"
|
|
31
32
|
N_METHYLPYRROLIDONE = "n_methylpyrrolidone"
|
|
@@ -39,6 +40,7 @@ class SolventModel(LowercaseStrEnum):
|
|
|
39
40
|
COSMO = "cosmo"
|
|
40
41
|
GBSA = "gbsa"
|
|
41
42
|
CPCMX = "cpcmx"
|
|
43
|
+
SMD = "SMD"
|
|
42
44
|
|
|
43
45
|
|
|
44
46
|
class SolventSettings(Base):
|
|
@@ -4,6 +4,7 @@ from typing import Literal
|
|
|
4
4
|
|
|
5
5
|
from .admet import *
|
|
6
6
|
from .basic_calculation import *
|
|
7
|
+
from .batch_docking import *
|
|
7
8
|
from .bde import *
|
|
8
9
|
from .conformer import *
|
|
9
10
|
from .conformer_search import *
|
|
@@ -17,12 +18,18 @@ from .ion_mobility import *
|
|
|
17
18
|
from .irc import *
|
|
18
19
|
from .macropka import *
|
|
19
20
|
from .molecular_dynamics import *
|
|
21
|
+
from .msa import *
|
|
20
22
|
from .multistage_opt import *
|
|
21
23
|
from .nmr import *
|
|
22
24
|
from .pka import *
|
|
23
25
|
from .pose_analysis_md import *
|
|
26
|
+
from .protein_binder_design import *
|
|
24
27
|
from .protein_cofolding import *
|
|
25
28
|
from .redox_potential import *
|
|
29
|
+
from .relative_binding_free_energy_perturbation import (
|
|
30
|
+
RBFEGraphWorkflow,
|
|
31
|
+
RelativeBindingFreeEnergyPerturbationWorkflow,
|
|
32
|
+
)
|
|
26
33
|
from .scan import *
|
|
27
34
|
from .solubility import *
|
|
28
35
|
from .spin_states import *
|
|
@@ -33,6 +40,7 @@ from .workflow import *
|
|
|
33
40
|
WORKFLOW_NAME = Literal[
|
|
34
41
|
"admet",
|
|
35
42
|
"basic_calculation",
|
|
43
|
+
"batch_docking",
|
|
36
44
|
"bde",
|
|
37
45
|
"conformers",
|
|
38
46
|
"conformer_search",
|
|
@@ -40,17 +48,21 @@ WORKFLOW_NAME = Literal[
|
|
|
40
48
|
"docking",
|
|
41
49
|
"double_ended_ts_search",
|
|
42
50
|
"electronic_properties",
|
|
51
|
+
"relative_binding_free_energy_perturbation",
|
|
43
52
|
"fukui",
|
|
44
53
|
"hydrogen_bond_basicity",
|
|
45
54
|
"ion_mobility",
|
|
46
55
|
"irc",
|
|
47
56
|
"macropka",
|
|
48
57
|
"molecular_dynamics",
|
|
58
|
+
"msa",
|
|
49
59
|
"multistage_opt",
|
|
50
60
|
"nmr",
|
|
51
61
|
"pka",
|
|
52
62
|
"pose_analysis_md",
|
|
53
63
|
"protein_cofolding",
|
|
64
|
+
"protein_binder_design",
|
|
65
|
+
"rbfe_graph",
|
|
54
66
|
"redox_potential",
|
|
55
67
|
"scan",
|
|
56
68
|
"solubility",
|
|
@@ -62,6 +74,7 @@ WORKFLOW_NAME = Literal[
|
|
|
62
74
|
WORKFLOW_MAPPING: dict[WORKFLOW_NAME, Workflow] = {
|
|
63
75
|
"admet": ADMETWorkflow, # type: ignore [dict-item]
|
|
64
76
|
"basic_calculation": BasicCalculationWorkflow, # type: ignore [dict-item]
|
|
77
|
+
"batch_docking": BatchDockingWorkflow, # type: ignore [dict-item]
|
|
65
78
|
"bde": BDEWorkflow, # type: ignore [dict-item]
|
|
66
79
|
"conformers": ConformerWorkflow, # type: ignore [dict-item]
|
|
67
80
|
"conformer_search": ConformerSearchWorkflow, # type: ignore [dict-item]
|
|
@@ -69,6 +82,7 @@ WORKFLOW_MAPPING: dict[WORKFLOW_NAME, Workflow] = {
|
|
|
69
82
|
"docking": DockingWorkflow, # type: ignore [dict-item]
|
|
70
83
|
"double_ended_ts_search": DoubleEndedTSSearchWorkflow, # type: ignore [dict-item]
|
|
71
84
|
"electronic_properties": ElectronicPropertiesWorkflow, # type: ignore [dict-item]
|
|
85
|
+
"relative_binding_free_energy_perturbation": RelativeBindingFreeEnergyPerturbationWorkflow, # type: ignore [dict-item]
|
|
72
86
|
"fukui": FukuiIndexWorkflow, # type: ignore [dict-item]
|
|
73
87
|
"hydrogen_bond_basicity": HydrogenBondBasicityWorkflow, # type: ignore [dict-item]
|
|
74
88
|
"ion_mobility": IonMobilityWorkflow, # type: ignore [dict-item]
|
|
@@ -76,10 +90,13 @@ WORKFLOW_MAPPING: dict[WORKFLOW_NAME, Workflow] = {
|
|
|
76
90
|
"macropka": MacropKaWorkflow, # type: ignore [dict-item]
|
|
77
91
|
"molecular_dynamics": MolecularDynamicsWorkflow, # type: ignore [dict-item]
|
|
78
92
|
"multistage_opt": MultiStageOptWorkflow, # type: ignore [dict-item]
|
|
93
|
+
"msa": MSAWorkflow, # type: ignore [dict-item]
|
|
79
94
|
"nmr": NMRSpectroscopyWorkflow, # type: ignore [dict-item]
|
|
80
95
|
"pka": pKaWorkflow, # type: ignore [dict-item]
|
|
81
96
|
"pose_analysis_md": PoseAnalysisMolecularDynamicsWorkflow, # type: ignore [dict-item]
|
|
82
97
|
"protein_cofolding": ProteinCofoldingWorkflow, # type: ignore [dict-item]
|
|
98
|
+
"protein_binder_design": ProteinBinderDesignWorkflow, # type: ignore [dict-item]
|
|
99
|
+
"rbfe_graph": RBFEGraphWorkflow, # type: ignore [dict-item]
|
|
83
100
|
"redox_potential": RedoxPotentialWorkflow, # type: ignore [dict-item]
|
|
84
101
|
"scan": ScanWorkflow, # type: ignore [dict-item]
|
|
85
102
|
"solubility": SolubilityWorkflow, # type: ignore [dict-item]
|
|
@@ -4,8 +4,10 @@ from typing import Self
|
|
|
4
4
|
|
|
5
5
|
from pydantic import model_validator
|
|
6
6
|
|
|
7
|
+
from ..base import UniqueList
|
|
7
8
|
from ..engine import Engine
|
|
8
9
|
from ..settings import Settings
|
|
10
|
+
from ..task import Task
|
|
9
11
|
from ..types import UUID
|
|
10
12
|
from .workflow import MoleculeWorkflow
|
|
11
13
|
|
|
@@ -25,12 +27,15 @@ class BasicCalculationWorkflow(MoleculeWorkflow):
|
|
|
25
27
|
"""
|
|
26
28
|
|
|
27
29
|
settings: Settings
|
|
28
|
-
|
|
30
|
+
tasks: UniqueList[Task] = [Task.ENERGY, Task.CHARGE, Task.DIPOLE]
|
|
29
31
|
calculation_uuid: UUID | None = None
|
|
30
32
|
|
|
33
|
+
# DEPRECATED - specify in settings now
|
|
34
|
+
engine: Engine = None # type: ignore [assignment]
|
|
35
|
+
|
|
31
36
|
@model_validator(mode="after")
|
|
32
37
|
def set_engine(self) -> Self:
|
|
33
38
|
"""Set the calculation engine."""
|
|
34
|
-
self.engine = self.engine or self.settings.
|
|
39
|
+
self.engine = self.engine or self.settings.engine
|
|
35
40
|
|
|
36
41
|
return self
|