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.
Files changed (94) hide show
  1. {stjames-0.0.114/stjames.egg-info → stjames-0.0.135}/PKG-INFO +1 -1
  2. {stjames-0.0.114 → stjames-0.0.135}/pyproject.toml +1 -1
  3. {stjames-0.0.114 → stjames-0.0.135}/stjames/__init__.py +1 -0
  4. {stjames-0.0.114 → stjames-0.0.135}/stjames/atomium_stjames/mmcif.py +2 -2
  5. {stjames-0.0.114 → stjames-0.0.135}/stjames/atomium_stjames/pdb.py +1 -1
  6. {stjames-0.0.114 → stjames-0.0.135}/stjames/calculation.py +15 -2
  7. stjames-0.0.135/stjames/dna.py +13 -0
  8. {stjames-0.0.114 → stjames-0.0.135}/stjames/engine.py +1 -0
  9. {stjames-0.0.114 → stjames-0.0.135}/stjames/method.py +4 -3
  10. {stjames-0.0.114 → stjames-0.0.135}/stjames/molecule.py +96 -18
  11. stjames-0.0.135/stjames/periodic_cell.py +82 -0
  12. stjames-0.0.135/stjames/protein.py +15 -0
  13. stjames-0.0.135/stjames/rna.py +13 -0
  14. {stjames-0.0.114 → stjames-0.0.135}/stjames/settings.py +11 -2
  15. {stjames-0.0.114 → stjames-0.0.135}/stjames/solvent.py +2 -0
  16. {stjames-0.0.114 → stjames-0.0.135}/stjames/workflows/__init__.py +17 -0
  17. {stjames-0.0.114 → stjames-0.0.135}/stjames/workflows/basic_calculation.py +7 -2
  18. stjames-0.0.135/stjames/workflows/batch_docking.py +46 -0
  19. {stjames-0.0.114 → stjames-0.0.135}/stjames/workflows/bde.py +1 -1
  20. {stjames-0.0.114 → stjames-0.0.135}/stjames/workflows/conformer_search.py +172 -18
  21. {stjames-0.0.114 → stjames-0.0.135}/stjames/workflows/docking.py +12 -1
  22. {stjames-0.0.114 → stjames-0.0.135}/stjames/workflows/fukui.py +6 -3
  23. {stjames-0.0.114 → stjames-0.0.135}/stjames/workflows/molecular_dynamics.py +3 -1
  24. stjames-0.0.135/stjames/workflows/msa.py +24 -0
  25. {stjames-0.0.114 → stjames-0.0.135}/stjames/workflows/pka.py +4 -4
  26. {stjames-0.0.114 → stjames-0.0.135}/stjames/workflows/pose_analysis_md.py +4 -4
  27. stjames-0.0.135/stjames/workflows/protein_binder_design.py +303 -0
  28. {stjames-0.0.114 → stjames-0.0.135}/stjames/workflows/protein_cofolding.py +10 -5
  29. stjames-0.0.135/stjames/workflows/relative_binding_free_energy_perturbation.py +180 -0
  30. {stjames-0.0.114 → stjames-0.0.135}/stjames/workflows/scan.py +12 -5
  31. stjames-0.0.135/stjames/workflows/solvent_dependent_conformers.py +85 -0
  32. stjames-0.0.135/stjames/workflows/workflow.py +115 -0
  33. {stjames-0.0.114 → stjames-0.0.135/stjames.egg-info}/PKG-INFO +1 -1
  34. {stjames-0.0.114 → stjames-0.0.135}/stjames.egg-info/SOURCES.txt +8 -0
  35. stjames-0.0.135/tests/test_molecule.py +90 -0
  36. stjames-0.0.114/stjames/periodic_cell.py +0 -34
  37. stjames-0.0.114/stjames/workflows/workflow.py +0 -81
  38. stjames-0.0.114/tests/test_molecule.py +0 -39
  39. {stjames-0.0.114 → stjames-0.0.135}/LICENSE +0 -0
  40. {stjames-0.0.114 → stjames-0.0.135}/README.md +0 -0
  41. {stjames-0.0.114 → stjames-0.0.135}/setup.cfg +0 -0
  42. {stjames-0.0.114 → stjames-0.0.135}/stjames/_deprecated_solvent_settings.py +0 -0
  43. {stjames-0.0.114 → stjames-0.0.135}/stjames/atom.py +0 -0
  44. {stjames-0.0.114 → stjames-0.0.135}/stjames/atomium_stjames/__init__.py +0 -0
  45. {stjames-0.0.114 → stjames-0.0.135}/stjames/atomium_stjames/data.py +0 -0
  46. {stjames-0.0.114 → stjames-0.0.135}/stjames/atomium_stjames/utilities.py +0 -0
  47. {stjames-0.0.114 → stjames-0.0.135}/stjames/base.py +0 -0
  48. {stjames-0.0.114 → stjames-0.0.135}/stjames/basis_set.py +0 -0
  49. {stjames-0.0.114 → stjames-0.0.135}/stjames/compute_settings.py +0 -0
  50. {stjames-0.0.114 → stjames-0.0.135}/stjames/constraint.py +0 -0
  51. {stjames-0.0.114 → stjames-0.0.135}/stjames/correction.py +0 -0
  52. {stjames-0.0.114 → stjames-0.0.135}/stjames/data/__init__.py +0 -0
  53. {stjames-0.0.114 → stjames-0.0.135}/stjames/data/bragg_radii.json +0 -0
  54. {stjames-0.0.114 → stjames-0.0.135}/stjames/data/elements.py +0 -0
  55. {stjames-0.0.114 → stjames-0.0.135}/stjames/data/isotopes.json +0 -0
  56. {stjames-0.0.114 → stjames-0.0.135}/stjames/data/nist_isotopes.json +0 -0
  57. {stjames-0.0.114 → stjames-0.0.135}/stjames/data/read_nist_isotopes.py +0 -0
  58. {stjames-0.0.114 → stjames-0.0.135}/stjames/data/symbol_element.json +0 -0
  59. {stjames-0.0.114 → stjames-0.0.135}/stjames/message.py +0 -0
  60. {stjames-0.0.114 → stjames-0.0.135}/stjames/mode.py +0 -0
  61. {stjames-0.0.114 → stjames-0.0.135}/stjames/opt_settings.py +0 -0
  62. {stjames-0.0.114 → stjames-0.0.135}/stjames/optimization/__init__.py +0 -0
  63. {stjames-0.0.114 → stjames-0.0.135}/stjames/optimization/freezing_string_method.py +0 -0
  64. {stjames-0.0.114 → stjames-0.0.135}/stjames/pdb.py +0 -0
  65. {stjames-0.0.114 → stjames-0.0.135}/stjames/py.typed +0 -0
  66. {stjames-0.0.114 → stjames-0.0.135}/stjames/scf_settings.py +0 -0
  67. {stjames-0.0.114 → stjames-0.0.135}/stjames/status.py +0 -0
  68. {stjames-0.0.114 → stjames-0.0.135}/stjames/task.py +0 -0
  69. {stjames-0.0.114 → stjames-0.0.135}/stjames/thermochem_settings.py +0 -0
  70. {stjames-0.0.114 → stjames-0.0.135}/stjames/types.py +0 -0
  71. {stjames-0.0.114 → stjames-0.0.135}/stjames/workflows/admet.py +0 -0
  72. {stjames-0.0.114 → stjames-0.0.135}/stjames/workflows/conformer.py +0 -0
  73. {stjames-0.0.114 → stjames-0.0.135}/stjames/workflows/descriptors.py +0 -0
  74. {stjames-0.0.114 → stjames-0.0.135}/stjames/workflows/double_ended_ts_search.py +0 -0
  75. {stjames-0.0.114 → stjames-0.0.135}/stjames/workflows/electronic_properties.py +0 -0
  76. {stjames-0.0.114 → stjames-0.0.135}/stjames/workflows/hydrogen_bond_basicity.py +0 -0
  77. {stjames-0.0.114 → stjames-0.0.135}/stjames/workflows/ion_mobility.py +0 -0
  78. {stjames-0.0.114 → stjames-0.0.135}/stjames/workflows/irc.py +0 -0
  79. {stjames-0.0.114 → stjames-0.0.135}/stjames/workflows/macropka.py +0 -0
  80. {stjames-0.0.114 → stjames-0.0.135}/stjames/workflows/multistage_opt.py +0 -0
  81. {stjames-0.0.114 → stjames-0.0.135}/stjames/workflows/nmr.py +0 -0
  82. {stjames-0.0.114 → stjames-0.0.135}/stjames/workflows/redox_potential.py +0 -0
  83. {stjames-0.0.114 → stjames-0.0.135}/stjames/workflows/solubility.py +0 -0
  84. {stjames-0.0.114 → stjames-0.0.135}/stjames/workflows/spin_states.py +0 -0
  85. {stjames-0.0.114 → stjames-0.0.135}/stjames/workflows/strain.py +0 -0
  86. {stjames-0.0.114 → stjames-0.0.135}/stjames/workflows/tautomer.py +0 -0
  87. {stjames-0.0.114 → stjames-0.0.135}/stjames.egg-info/dependency_links.txt +0 -0
  88. {stjames-0.0.114 → stjames-0.0.135}/stjames.egg-info/requires.txt +0 -0
  89. {stjames-0.0.114 → stjames-0.0.135}/stjames.egg-info/top_level.txt +0 -0
  90. {stjames-0.0.114 → stjames-0.0.135}/tests/test_constraints.py +0 -0
  91. {stjames-0.0.114 → stjames-0.0.135}/tests/test_from_extxyz.py +0 -0
  92. {stjames-0.0.114 → stjames-0.0.135}/tests/test_pdb.py +0 -0
  93. {stjames-0.0.114 → stjames-0.0.135}/tests/test_rounding.py +0 -0
  94. {stjames-0.0.114 → stjames-0.0.135}/tests/test_settings.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stjames
3
- Version: 0.0.114
3
+ Version: 0.0.135
4
4
  Summary: standardized JSON atom/molecule encoding scheme
5
5
  Author-email: Corin Wagen <corin@rowansci.com>
6
6
  Project-URL: Homepage, https://github.com/rowansci/stjames
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "stjames"
3
- version = "0.0.114"
3
+ version = "0.0.135"
4
4
  description = "standardized JSON atom/molecule encoding scheme"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -22,3 +22,4 @@ from .status import *
22
22
  from .constraint import *
23
23
  from .message import *
24
24
  from .types import *
25
+ from .engine import *
@@ -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["auth_comp_id"]
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["auth_comp_id"]
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 .base import Base, LowercaseStrEnum
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
@@ -0,0 +1,13 @@
1
+ """DNA-related data models."""
2
+
3
+ from .base import Base
4
+
5
+
6
+ class DNASequence(Base):
7
+ """
8
+ DNA sequence data.
9
+
10
+ :param sequence: nucleotide string
11
+ """
12
+
13
+ sequence: str
@@ -9,6 +9,7 @@ class Engine(LowercaseStrEnum):
9
9
  TBLITE = "tblite"
10
10
  XTB = "xtb"
11
11
  TERACHEM = "terachem"
12
+ GPU4PYSCF = "gpu4pyscf"
12
13
  PYSCF = "pyscf"
13
14
  PSI4 = "psi4"
14
15
  OPENFF = "openff"
@@ -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
- <BLANKLINE>
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.copy(update={"position": translated(atom.position)}) for atom in self.atoms]
137
+ atoms = [atom.model_copy(update={"position": translated(atom.position)}) for atom in self.atoms]
135
138
 
136
- return self.copy(update={"atoms": atoms})
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 = 0, multiplicity: PositiveInt = 1) -> Self:
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
- <BLANKLINE>
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 = 0, multiplicity: PositiveInt = 1) -> Self:
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 = 0, multiplicity: PositiveInt = 1) -> Self:
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[0].strip()
232
- if not natoms.isdigit() or (int(lines[0]) != len(lines) - 2):
233
- raise MoleculeReadError(f"First line of XYZ file should be the number of atoms, got: {lines[0]} != {len(lines) - 2}")
234
- lines = lines[2:]
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
- def to_xyz(self, comment: str = "", out_file: Path | str | None = None) -> str:
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
- >>> mol = Molecule.from_xyz("2\nComment\nH 0 1 2\nF 1 2 3")
246
- >>> print(mol.to_xyz(comment="HF"))
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(comment="HF", out_file=file)
323
+ ... out = mol.to_xyz(out_file=file)
255
324
  ... with file.open() as f:
256
- ... Molecule.from_xyz(f.read()).to_xyz("HF") == out
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), volume=0.0), 'properties': 'species:S:1:pos:R:3'}
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
@@ -0,0 +1,13 @@
1
+ """RNA-related data models."""
2
+
3
+ from .base import Base
4
+
5
+
6
+ class RNASequence(Base):
7
+ """
8
+ RNA sequence data.
9
+
10
+ :param sequence: nucleotide string
11
+ """
12
+
13
+ sequence: str
@@ -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
- tasks: UniqueList[Task] = [Task.ENERGY, Task.CHARGE, Task.DIPOLE]
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
- engine: Engine = None # type: ignore [assignment]
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.method.default_engine()
39
+ self.engine = self.engine or self.settings.engine
35
40
 
36
41
  return self