stjames 0.0.76__tar.gz → 0.0.78__tar.gz

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

Potentially problematic release.


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

Files changed (79) hide show
  1. {stjames-0.0.76/stjames.egg-info → stjames-0.0.78}/PKG-INFO +2 -1
  2. {stjames-0.0.76 → stjames-0.0.78}/pyproject.toml +2 -1
  3. {stjames-0.0.76 → stjames-0.0.78}/stjames/atomium_stjames/pdb.py +56 -10
  4. {stjames-0.0.76 → stjames-0.0.78}/stjames/molecule.py +99 -10
  5. {stjames-0.0.76 → stjames-0.0.78}/stjames/workflows/hydrogen_bond_basicity.py +19 -3
  6. {stjames-0.0.76 → stjames-0.0.78}/stjames/workflows/multistage_opt.py +72 -1
  7. {stjames-0.0.76 → stjames-0.0.78}/stjames/workflows/protein_cofolding.py +12 -1
  8. {stjames-0.0.76 → stjames-0.0.78}/stjames/workflows/workflow.py +1 -0
  9. {stjames-0.0.76 → stjames-0.0.78/stjames.egg-info}/PKG-INFO +2 -1
  10. {stjames-0.0.76 → stjames-0.0.78}/stjames.egg-info/requires.txt +1 -0
  11. {stjames-0.0.76 → stjames-0.0.78}/LICENSE +0 -0
  12. {stjames-0.0.76 → stjames-0.0.78}/README.md +0 -0
  13. {stjames-0.0.76 → stjames-0.0.78}/setup.cfg +0 -0
  14. {stjames-0.0.76 → stjames-0.0.78}/stjames/__init__.py +0 -0
  15. {stjames-0.0.76 → stjames-0.0.78}/stjames/_deprecated_solvent_settings.py +0 -0
  16. {stjames-0.0.76 → stjames-0.0.78}/stjames/atom.py +0 -0
  17. {stjames-0.0.76 → stjames-0.0.78}/stjames/atomium_stjames/__init__.py +0 -0
  18. {stjames-0.0.76 → stjames-0.0.78}/stjames/atomium_stjames/data.py +0 -0
  19. {stjames-0.0.76 → stjames-0.0.78}/stjames/atomium_stjames/mmcif.py +0 -0
  20. {stjames-0.0.76 → stjames-0.0.78}/stjames/atomium_stjames/utilities.py +0 -0
  21. {stjames-0.0.76 → stjames-0.0.78}/stjames/base.py +0 -0
  22. {stjames-0.0.76 → stjames-0.0.78}/stjames/basis_set.py +0 -0
  23. {stjames-0.0.76 → stjames-0.0.78}/stjames/calculation.py +0 -0
  24. {stjames-0.0.76 → stjames-0.0.78}/stjames/compute_settings.py +0 -0
  25. {stjames-0.0.76 → stjames-0.0.78}/stjames/constraint.py +0 -0
  26. {stjames-0.0.76 → stjames-0.0.78}/stjames/correction.py +0 -0
  27. {stjames-0.0.76 → stjames-0.0.78}/stjames/data/__init__.py +0 -0
  28. {stjames-0.0.76 → stjames-0.0.78}/stjames/data/bragg_radii.json +0 -0
  29. {stjames-0.0.76 → stjames-0.0.78}/stjames/data/elements.py +0 -0
  30. {stjames-0.0.76 → stjames-0.0.78}/stjames/data/isotopes.json +0 -0
  31. {stjames-0.0.76 → stjames-0.0.78}/stjames/data/nist_isotopes.json +0 -0
  32. {stjames-0.0.76 → stjames-0.0.78}/stjames/data/read_nist_isotopes.py +0 -0
  33. {stjames-0.0.76 → stjames-0.0.78}/stjames/data/symbol_element.json +0 -0
  34. {stjames-0.0.76 → stjames-0.0.78}/stjames/diis_settings.py +0 -0
  35. {stjames-0.0.76 → stjames-0.0.78}/stjames/grid_settings.py +0 -0
  36. {stjames-0.0.76 → stjames-0.0.78}/stjames/int_settings.py +0 -0
  37. {stjames-0.0.76 → stjames-0.0.78}/stjames/message.py +0 -0
  38. {stjames-0.0.76 → stjames-0.0.78}/stjames/method.py +0 -0
  39. {stjames-0.0.76 → stjames-0.0.78}/stjames/mode.py +0 -0
  40. {stjames-0.0.76 → stjames-0.0.78}/stjames/opt_settings.py +0 -0
  41. {stjames-0.0.76 → stjames-0.0.78}/stjames/pdb.py +0 -0
  42. {stjames-0.0.76 → stjames-0.0.78}/stjames/periodic_cell.py +0 -0
  43. {stjames-0.0.76 → stjames-0.0.78}/stjames/py.typed +0 -0
  44. {stjames-0.0.76 → stjames-0.0.78}/stjames/scf_settings.py +0 -0
  45. {stjames-0.0.76 → stjames-0.0.78}/stjames/settings.py +0 -0
  46. {stjames-0.0.76 → stjames-0.0.78}/stjames/solvent.py +0 -0
  47. {stjames-0.0.76 → stjames-0.0.78}/stjames/status.py +0 -0
  48. {stjames-0.0.76 → stjames-0.0.78}/stjames/task.py +0 -0
  49. {stjames-0.0.76 → stjames-0.0.78}/stjames/thermochem_settings.py +0 -0
  50. {stjames-0.0.76 → stjames-0.0.78}/stjames/types.py +0 -0
  51. {stjames-0.0.76 → stjames-0.0.78}/stjames/workflows/__init__.py +0 -0
  52. {stjames-0.0.76 → stjames-0.0.78}/stjames/workflows/admet.py +0 -0
  53. {stjames-0.0.76 → stjames-0.0.78}/stjames/workflows/basic_calculation.py +0 -0
  54. {stjames-0.0.76 → stjames-0.0.78}/stjames/workflows/bde.py +0 -0
  55. {stjames-0.0.76 → stjames-0.0.78}/stjames/workflows/conformer.py +0 -0
  56. {stjames-0.0.76 → stjames-0.0.78}/stjames/workflows/conformer_search.py +0 -0
  57. {stjames-0.0.76 → stjames-0.0.78}/stjames/workflows/descriptors.py +0 -0
  58. {stjames-0.0.76 → stjames-0.0.78}/stjames/workflows/docking.py +0 -0
  59. {stjames-0.0.76 → stjames-0.0.78}/stjames/workflows/electronic_properties.py +0 -0
  60. {stjames-0.0.76 → stjames-0.0.78}/stjames/workflows/fukui.py +0 -0
  61. {stjames-0.0.76 → stjames-0.0.78}/stjames/workflows/ion_mobility.py +0 -0
  62. {stjames-0.0.76 → stjames-0.0.78}/stjames/workflows/irc.py +0 -0
  63. {stjames-0.0.76 → stjames-0.0.78}/stjames/workflows/macropka.py +0 -0
  64. {stjames-0.0.76 → stjames-0.0.78}/stjames/workflows/molecular_dynamics.py +0 -0
  65. {stjames-0.0.76 → stjames-0.0.78}/stjames/workflows/pka.py +0 -0
  66. {stjames-0.0.76 → stjames-0.0.78}/stjames/workflows/redox_potential.py +0 -0
  67. {stjames-0.0.76 → stjames-0.0.78}/stjames/workflows/scan.py +0 -0
  68. {stjames-0.0.76 → stjames-0.0.78}/stjames/workflows/solubility.py +0 -0
  69. {stjames-0.0.76 → stjames-0.0.78}/stjames/workflows/spin_states.py +0 -0
  70. {stjames-0.0.76 → stjames-0.0.78}/stjames/workflows/tautomer.py +0 -0
  71. {stjames-0.0.76 → stjames-0.0.78}/stjames.egg-info/SOURCES.txt +0 -0
  72. {stjames-0.0.76 → stjames-0.0.78}/stjames.egg-info/dependency_links.txt +0 -0
  73. {stjames-0.0.76 → stjames-0.0.78}/stjames.egg-info/top_level.txt +0 -0
  74. {stjames-0.0.76 → stjames-0.0.78}/tests/test_constraints.py +0 -0
  75. {stjames-0.0.76 → stjames-0.0.78}/tests/test_from_extxyz.py +0 -0
  76. {stjames-0.0.76 → stjames-0.0.78}/tests/test_molecule.py +0 -0
  77. {stjames-0.0.76 → stjames-0.0.78}/tests/test_pdb.py +0 -0
  78. {stjames-0.0.76 → stjames-0.0.78}/tests/test_rounding.py +0 -0
  79. {stjames-0.0.76 → stjames-0.0.78}/tests/test_settings.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stjames
3
- Version: 0.0.76
3
+ Version: 0.0.78
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
@@ -12,6 +12,7 @@ Requires-Dist: pydantic>=2.4
12
12
  Requires-Dist: numpy
13
13
  Requires-Dist: requests
14
14
  Requires-Dist: rdkit
15
+ Requires-Dist: more-itertools
15
16
  Dynamic: license-file
16
17
 
17
18
  # stjames
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "stjames"
3
- version = "0.0.76"
3
+ version = "0.0.78"
4
4
  description = "standardized JSON atom/molecule encoding scheme"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -13,6 +13,7 @@ dependencies = [
13
13
  "numpy",
14
14
  "requests",
15
15
  "rdkit",
16
+ "more-itertools",
16
17
  ]
17
18
 
18
19
  [build-system]
@@ -3,7 +3,7 @@
3
3
  import re
4
4
  from datetime import datetime
5
5
  from itertools import chain, groupby
6
- from typing import Any, Callable
6
+ from typing import Any, Callable, TypedDict
7
7
 
8
8
  from .data import CODES
9
9
  from .mmcif import add_secondary_structure_to_polymers
@@ -476,15 +476,17 @@ def add_atom_to_polymer(line: str, model: dict[Any, Any], chain_id: str, res_id:
476
476
  :param str res_id: the molecule ID to add to.
477
477
  :param dict aniso_dict: lookup dictionary for anisotropy information."""
478
478
 
479
+ atom = atom_line_to_dict(line, aniso_dict)
480
+
479
481
  try:
480
- model["polymer"][chain_id]["residues"][res_id]["atoms"][int(line[6:11])] = atom_line_to_dict(line, aniso_dict)
482
+ model["polymer"][chain_id]["residues"][res_id]["atoms"][int(line[6:11])] = atom
481
483
  except Exception:
482
484
  name = line[17:20].strip()
483
485
  try:
484
486
  model["polymer"][chain_id]["residues"][res_id] = {
485
487
  "name": name,
486
488
  "full_name": full_names.get(name),
487
- "atoms": {int(line[6:11]): atom_line_to_dict(line, aniso_dict)},
489
+ "atoms": {int(line[6:11]): atom},
488
490
  "number": len(model["polymer"][chain_id]["residues"]) + 1,
489
491
  }
490
492
  except Exception:
@@ -495,7 +497,7 @@ def add_atom_to_polymer(line: str, model: dict[Any, Any], chain_id: str, res_id:
495
497
  "residues": {
496
498
  res_id: {
497
499
  "name": line[17:20].strip(),
498
- "atoms": {int(line[6:11]): atom_line_to_dict(line, aniso_dict)},
500
+ "atoms": {int(line[6:11]): atom},
499
501
  "number": 1,
500
502
  "full_name": None,
501
503
  }
@@ -511,10 +513,11 @@ def add_atom_to_non_polymer(line: str, model: dict[Any, Any], res_id: str, aniso
511
513
  :param dict model: the model to update.
512
514
  :param str res_id: the molecule ID to add to.
513
515
  :param dict aniso_dict: lookup dictionary for anisotropy information."""
516
+ atom = atom_line_to_dict(line, aniso_dict)
514
517
 
515
518
  key = "water" if line[17:20] in ["HOH", "DOD"] else "non_polymer"
516
519
  try:
517
- model[key][res_id]["atoms"][int(line[6:11])] = atom_line_to_dict(line, aniso_dict)
520
+ model[key][res_id]["atoms"][int(line[6:11])] = atom
518
521
  except Exception:
519
522
  name = line[17:20].strip()
520
523
  model[key][res_id] = {
@@ -522,18 +525,55 @@ def add_atom_to_non_polymer(line: str, model: dict[Any, Any], res_id: str, aniso
522
525
  "full_name": full_names.get(name),
523
526
  "internal_id": line[21],
524
527
  "polymer": line[21],
525
- "atoms": {int(line[6:11]): atom_line_to_dict(line, aniso_dict)},
528
+ "atoms": {int(line[6:11]): atom},
526
529
  }
527
530
 
528
531
 
529
- def atom_line_to_dict(line: str, aniso_dict: dict[Any, Any]) -> dict[str, Any]:
530
- """Converts an ATOM or HETATM record to an atom dictionary.
532
+ def guess_element_from_name(atom_name: str) -> str | None:
533
+ atom_name = atom_name.strip()
534
+ if not atom_name:
535
+ return None
536
+
537
+ # Case 1: Atom name starts with a digit (e.g. '1HG1') → element is second character
538
+ if atom_name[0].isdigit() and len(atom_name) > 1:
539
+ return atom_name[1].upper()
540
+
541
+ # # Case 2: Atom name starts with a letter
542
+ # if len(atom_name) >= 2 and atom_name[:2].isalpha():
543
+ # possible = atom_name[:2].strip().capitalize()
544
+ # # Check for common two-letter elements
545
+ # if possible in {"Cl", "Br", "Fe", "Mg", "Zn", "Ca", "Na", "Cu", "Mn", "Co", "Ni"}:
546
+ # return possible
547
+ # Fallback to first letter
548
+ return atom_name[0].upper()
549
+
550
+
551
+ class AtomDict(TypedDict, total=False):
552
+ """A dictionary representing an atom in a PDB file."""
553
+
554
+ occupancy: float | None
555
+ bvalue: float | None
556
+ charge: int | None
557
+ anisotropy: float | None
558
+ is_hetatm: bool | None
559
+ name: str | None
560
+ alt_loc: str | None
561
+ x: float
562
+ y: float
563
+ z: float
564
+ element: str | None
565
+
566
+
567
+ def atom_line_to_dict(line: str, aniso_dict: dict[Any, Any]) -> AtomDict:
568
+ """
569
+ Converts an ATOM or HETATM record to an atom dictionary.
531
570
 
532
571
  :param str line: the record to convert.
533
572
  :param dict aniso_dict: the anisotropy dictionary to use.
534
- :rtype: ``dict``"""
573
+ :return: atom dictionary
574
+ """
535
575
 
536
- a = {"occupancy": 1, "bvalue": None, "charge": 0, "anisotropy": aniso_dict.get(int(line[6:11].strip()), None)}
576
+ a: AtomDict = {"occupancy": 1, "bvalue": None, "charge": 0, "anisotropy": aniso_dict.get(int(line[6:11].strip()), None)}
537
577
  a["is_hetatm"] = line[:6] == "HETATM"
538
578
  a["name"] = line[12:16].strip() or None
539
579
  a["alt_loc"] = line[16].strip() or None
@@ -545,6 +585,11 @@ def atom_line_to_dict(line: str, aniso_dict: dict[Any, Any]) -> dict[str, Any]:
545
585
  if line[60:66].strip():
546
586
  a["bvalue"] = float(line[60:66].strip())
547
587
  a["element"] = line[76:78].strip() or None
588
+ if not a["element"]:
589
+ if not a["name"]:
590
+ raise ValueError("Cannot guess element from empty name.")
591
+ assert isinstance(a["name"], str)
592
+ a["element"] = guess_element_from_name(a["name"])
548
593
  if line[78:80].strip():
549
594
  try:
550
595
  a["charge"] = int(line[78:80].strip())
@@ -561,6 +606,7 @@ def atom_line_to_dict(line: str, aniso_dict: dict[Any, Any]) -> dict[str, Any]:
561
606
  a["occupancy"] = None
562
607
  if a["name"] == a["element"]:
563
608
  a["name"] = None
609
+
564
610
  return a
565
611
 
566
612
 
@@ -2,6 +2,7 @@ import re
2
2
  from pathlib import Path
3
3
  from typing import Annotated, Any, Iterable, Optional, Self, Sequence, TypeAlias, TypedDict, TypeVar
4
4
 
5
+ import numpy as np
5
6
  import pydantic
6
7
  from pydantic import AfterValidator, NonNegativeInt, PositiveInt, ValidationError
7
8
  from rdkit import Chem
@@ -75,15 +76,41 @@ class Molecule(Base):
75
76
  def __len__(self) -> int:
76
77
  return len(self.atoms)
77
78
 
78
- def distance(self, atom1: PositiveInt, atom2: PositiveInt) -> float:
79
+ def distance(self, i: PositiveInt, j: PositiveInt) -> float:
79
80
  r"""
80
- Get the distance between atoms.
81
+ Calculate the distance between atoms.
81
82
 
82
83
  >>> mol = Molecule.from_xyz("H 0 1 0\nH 0 0 1")
83
84
  >>> mol.distance(1, 2)
84
85
  1.4142135623730951
85
86
  """
86
- return sum((q2 - q1) ** 2 for q1, q2 in zip(self.atoms[atom1 - 1].position, self.atoms[atom2 - 1].position)) ** 0.5 # type: ignore [no-any-return,unused-ignore]
87
+ return sum((q2 - q1) ** 2 for q1, q2 in zip(self.atoms[i - 1].position, self.atoms[j - 1].position)) ** 0.5 # type: ignore [no-any-return,unused-ignore]
88
+
89
+ def angle(self, i: PositiveInt, j: PositiveInt, k: PositiveInt, degrees: bool = True) -> float:
90
+ r"""
91
+ Calculate the angle between three atoms.
92
+
93
+ >>> Molecule.from_xyz("H 0 0 0\nO 0 0 1\nH 0 1 1").angle(1, 2, 3)
94
+ 90.0
95
+ """
96
+
97
+ return angle(self.coordinates[i - 1], self.coordinates[j - 1], self.coordinates[k - 1], degrees=degrees)
98
+
99
+ def dihedral(self, i: int, j: int, k: int, l: int, degrees: bool = True, positive_domain: bool = True) -> float:
100
+ r"""
101
+ Calculate the dihedral angle between four atoms.
102
+
103
+ >>> Molecule.from_xyz("H 0 0 0\nO 0 0 1\nO 0 1 1\nH 1 1 1").dihedral(1, 2, 3, 4)
104
+ 270.0
105
+ """
106
+ return dihedral(
107
+ self.coordinates[i - 1],
108
+ self.coordinates[j - 1],
109
+ self.coordinates[k - 1],
110
+ self.coordinates[l - 1],
111
+ degrees=degrees,
112
+ positive_domain=positive_domain,
113
+ )
87
114
 
88
115
  @property
89
116
  def coordinates(self) -> Vector3DPerAtom:
@@ -345,19 +372,17 @@ class Molecule(Base):
345
372
  return cls(atoms=atoms, cell=cell, charge=charge, multiplicity=multiplicity, energy=energy, gradient=gradients)
346
373
 
347
374
  @classmethod
348
- def from_rdkit(cls: type[Self], rdkm: RdkitMol, cid: int = 0) -> Self:
375
+ def from_rdkit(cls: type[Self], rdkm: RdkitMol, cid: int = 0, multiplicity: int = 1) -> Self:
349
376
  if len(rdkm.GetConformers()) == 0:
350
377
  rdkm = _embed_rdkit_mol(rdkm)
351
378
 
352
- atoms = []
353
379
  atomic_numbers = [atom.GetAtomicNum() for atom in rdkm.GetAtoms()] # type: ignore [no-untyped-call, unused-ignore]
354
- geom = rdkm.GetConformers()[cid].GetPositions()
355
-
356
- for i in range(len(atomic_numbers)):
357
- atoms.append(Atom(atomic_number=atomic_numbers[i], position=geom[i]))
380
+ atoms = [
381
+ Atom(atomic_number=atom, position=xyz) # keep open
382
+ for atom, xyz in zip(atomic_numbers, rdkm.GetConformers()[cid].GetPositions(), strict=True)
383
+ ]
358
384
 
359
385
  charge = Chem.GetFormalCharge(rdkm)
360
- multiplicity = 1
361
386
 
362
387
  return cls(atoms=atoms, charge=charge, multiplicity=multiplicity)
363
388
 
@@ -447,3 +472,67 @@ def parse_extxyz_comment_line(line: str) -> EXTXYZMetadata:
447
472
  prop_dict[key] = value # type: ignore [literal-required]
448
473
 
449
474
  return prop_dict
475
+
476
+
477
+ def angle(p0: Sequence[float], p1: Sequence[float], p2: Sequence[float], degrees: bool = True) -> float:
478
+ """
479
+ Angle between three points.
480
+
481
+ :param i, j, k: positions of points
482
+ :param degrees: whether to return in degrees
483
+ :return: angle in radians or degrees
484
+ """
485
+ a0, a1, a2 = map(np.asarray, (p0, p1, p2))
486
+ u = a1 - a0
487
+ v = a1 - a2
488
+
489
+ nu = np.linalg.norm(u)
490
+ nv = np.linalg.norm(v)
491
+ cos_theta = np.dot(u, v) / (nu * nv)
492
+ cos_theta = np.clip(cos_theta, -1.0, 1.0)
493
+
494
+ ang = np.arccos(cos_theta)
495
+ if degrees:
496
+ return float(np.degrees(ang))
497
+ return float(ang)
498
+
499
+
500
+ def dihedral(p0: Sequence[float], p1: Sequence[float], p2: Sequence[float], p3: Sequence[float], degrees: bool = True, positive_domain: bool = True) -> float:
501
+ """
502
+ Dihedral angle between four points.
503
+
504
+ :param p0, p1, p2, p3: points
505
+ :param degrees: whether to return in degrees
506
+ :param positive_domain: (0, 360] if True else (-180, 180]
507
+ :return: angle in degrees or radians (or nan if collinearities detected)
508
+
509
+ >>> a = [0, 0, 0]
510
+ >>> b = [0, 0, 1]
511
+ >>> c = [0, 1, 1]
512
+ >>> d1 = [0, 1, 2]
513
+ >>> d2 = [0.5, 1, 1.5]
514
+ >>> dihedral(a, b, c, d1)
515
+ 180.0
516
+ >>> dihedral(a, b, c, d2, positive_domain=False)
517
+ -135.0
518
+ """
519
+ a0, a1, a2, a3 = map(np.asarray, (p0, p1, p2, p3))
520
+ b0 = a1 - a0
521
+ b1 = a2 - a1
522
+ b2 = a3 - a2
523
+
524
+ b1 = b1 / np.linalg.norm(b1)
525
+
526
+ v = b1 * np.dot(b0, b1) - b0
527
+ w = b2 - b1 * np.dot(b2, b1)
528
+
529
+ x = np.dot(v, w)
530
+ y = np.dot(np.cross(b1, v), w)
531
+ ang = np.arctan2(y, x)
532
+
533
+ if positive_domain and ang < 0:
534
+ ang += 2 * np.pi
535
+
536
+ if degrees:
537
+ return float(np.degrees(ang))
538
+ return float(ang)
@@ -7,10 +7,10 @@ from .workflow import MoleculeWorkflow
7
7
 
8
8
  class HydrogenBondAcceptorSite(Base):
9
9
  """
10
- A hydrogen bond acceptor site.
10
+ A hydrogen-bond-acceptor site.
11
11
 
12
12
  :param atom_idx: index of the atom
13
- :param pkbhx: Hydrogen bond basicity
13
+ :param pkbhx: Hydrogen-bond basicity
14
14
  :param position: position of the atom
15
15
  :param name: name of the atom
16
16
  """
@@ -21,9 +21,23 @@ class HydrogenBondAcceptorSite(Base):
21
21
  name: str | None = None
22
22
 
23
23
 
24
+ class HydrogenBondDonorSite(Base):
25
+ """
26
+ A hydrogen-bond-donor site.
27
+
28
+ :param atom_idx: index of the atom
29
+ :param pk_alpha: Hydrogen-bond acidity
30
+ :param position: position of the atom
31
+ """
32
+
33
+ atom_idx: int # zero-indexed
34
+ pk_alpha: float
35
+ position: tuple[float, float, float]
36
+
37
+
24
38
  class HydrogenBondBasicityWorkflow(MoleculeWorkflow):
25
39
  """
26
- Workflow for calculating hydrogen bond basicity.
40
+ Workflow for calculating hydrogen-bond basicity and acidity.
27
41
 
28
42
  Inherited:
29
43
  :param initial_molecule: Molecule of interest
@@ -36,6 +50,7 @@ class HydrogenBondBasicityWorkflow(MoleculeWorkflow):
36
50
  Results:
37
51
  :param optimization: UUID of optimization
38
52
  :param hba_sites: hydrogen-bond-acceptor sites
53
+ :param hbd_sites: hydrogen-bond-donor sites
39
54
  """
40
55
 
41
56
  do_csearch: bool = True
@@ -43,3 +58,4 @@ class HydrogenBondBasicityWorkflow(MoleculeWorkflow):
43
58
 
44
59
  optimization: UUID | None = None
45
60
  hba_sites: list[HydrogenBondAcceptorSite] = [] # noqa: RUF012
61
+ hbd_sites: list[HydrogenBondDonorSite] = [] # noqa: RUF012
@@ -1,15 +1,19 @@
1
1
  """Multi-stage optimization workflow."""
2
2
 
3
+ import re
3
4
  from typing import Self, Sequence
4
5
 
6
+ import more_itertools as mit
5
7
  from pydantic import BaseModel, Field, model_validator
6
8
 
9
+ from stjames.correction import Correction
10
+
7
11
  from ..constraint import Constraint
8
12
  from ..method import XTB_METHODS, Method
9
13
  from ..mode import Mode
10
14
  from ..opt_settings import OptimizationSettings
11
15
  from ..settings import Settings
12
- from ..solvent import Solvent, SolventSettings
16
+ from ..solvent import Solvent, SolventModel, SolventSettings
13
17
  from ..task import Task
14
18
  from ..types import UUID
15
19
  from .workflow import MoleculeWorkflow
@@ -264,6 +268,73 @@ class MultiStageOptMixin(BaseModel):
264
268
  return self
265
269
 
266
270
 
271
+ def mso_settings_from_method_string(
272
+ methods: str,
273
+ solvent: Solvent | None = None,
274
+ use_solvent_for_opt: bool = False,
275
+ constraints: list[Constraint] | None = None,
276
+ transition_state: bool = False,
277
+ frequencies: bool = False,
278
+ ) -> MultiStageOptSettings:
279
+ """
280
+ Helper function to construct multi-stage opt settings objects from a method string.
281
+
282
+ >>> mso_settings_from_method_string("r2SCAN-3c/CPCM(Water)//B3LYP-D3/6-31G(d)/ALPB(Water)//GFN2-xTB/CPCM(Water)//GFN0-xTB").level_of_theory
283
+ 'r2scan_3c/cpcm(water)//b3lyp-d3/6-31g(d)/alpb(water)//gfn2_xtb/cpcm(water)//gfn0_xtb'
284
+ """
285
+ solvent_models = "|".join(model.name for model in SolventModel)
286
+
287
+ pattern = rf"""
288
+ (?P<method>[^/()]+) # Method + optional corrections
289
+ (?:/(?P<basis_set>(?!{solvent_models})[^/]+?))? # Optional basis_set, not starting with solvent model name
290
+ (?:/(?P<solvent_model>{solvent_models}) # Optional solvent model
291
+ \((?P<solvent>[^()]+)\))? # Solvent name in parentheses
292
+ (?:\/\/|$) # End or separator
293
+ """
294
+ constraints = constraints or []
295
+ opt_settings = OptimizationSettings(constraints=constraints, transition_state=transition_state)
296
+ OPT = [Task.OPTIMIZE if not transition_state else Task.OPTIMIZE_TS]
297
+
298
+ valid_corrections = {c.name.lower() for c in Correction} # Python3.11 hack
299
+
300
+ def process(match: re.Match[str]) -> Settings:
301
+ data = match.groupdict()
302
+
303
+ method, corrections = mit.partition(lambda x: x.lower() in valid_corrections, data["method"].split("-"))
304
+ solvent_settings = SolventSettings(solvent=data["solvent"], model=data["solvent_model"]) if data["solvent"] else None
305
+
306
+ return Settings(
307
+ method="-".join(method),
308
+ basis_set=data["basis_set"],
309
+ tasks=OPT,
310
+ solvent_settings=solvent_settings,
311
+ opt_settings=opt_settings,
312
+ corrections=list(corrections),
313
+ )
314
+
315
+ optimization_settings = [process(match) for match in re.finditer(pattern, methods, re.VERBOSE | re.IGNORECASE)]
316
+ if len(optimization_settings) > 1:
317
+ sp_settings = optimization_settings.pop(0)
318
+ sp_settings.tasks = [Task.ENERGY]
319
+ else:
320
+ sp_settings = None
321
+
322
+ optimization_settings = optimization_settings[::-1]
323
+ if frequencies:
324
+ optimization_settings[-1].tasks.append(Task.FREQUENCIES)
325
+
326
+ return MultiStageOptSettings(
327
+ mode=Mode.MANUAL,
328
+ optimization_settings=optimization_settings,
329
+ singlepoint_settings=sp_settings,
330
+ solvent=solvent,
331
+ xtb_preopt=False,
332
+ constraints=constraints,
333
+ transition_state=transition_state,
334
+ frequencies=frequencies,
335
+ )
336
+
337
+
267
338
  def build_mso_settings(
268
339
  sp_method: Method,
269
340
  sp_basis_set: str | None,
@@ -12,6 +12,7 @@ class CofoldingModel(LowercaseStrEnum):
12
12
 
13
13
  CHAI_1R = "chai_1r"
14
14
  BOLTZ_1 = "boltz_1"
15
+ BOLTZ_2 = "boltz_2"
15
16
 
16
17
 
17
18
  class CofoldingScores(BaseModel):
@@ -20,6 +21,15 @@ class CofoldingScores(BaseModel):
20
21
  iptm: float # interface predicted template modeling score
21
22
 
22
23
 
24
+ class AffinityScore(BaseModel):
25
+ pred_value: float
26
+ probability_binary: float
27
+ pred_value1: float
28
+ probability_binary1: float
29
+ pred_value2: float
30
+ probability_binary2: float
31
+
32
+
23
33
  class ProteinCofoldingWorkflow(FASTAWorkflow):
24
34
  """
25
35
  A workflow for predicting structures. Especially protein structures.
@@ -38,4 +48,5 @@ class ProteinCofoldingWorkflow(FASTAWorkflow):
38
48
  use_templates_server: bool = False
39
49
  predicted_structure_uuid: UUID | None = None
40
50
  scores: CofoldingScores | None = None
41
- model: CofoldingModel = CofoldingModel.CHAI_1R
51
+ model: CofoldingModel = CofoldingModel.BOLTZ_2
52
+ affinity_score: AffinityScore | None = None
@@ -32,6 +32,7 @@ class FASTAWorkflow(Workflow):
32
32
 
33
33
  initial_protein_sequences: list[str]
34
34
  initial_smiles_list: list[str] | None = None
35
+ ligand_binding_affinity_index: int | None = None
35
36
 
36
37
  def __repr__(self) -> str:
37
38
  return f"<{type(self).__name__} {self.initial_protein_sequences} {self.initial_smiles_list}>"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stjames
3
- Version: 0.0.76
3
+ Version: 0.0.78
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
@@ -12,6 +12,7 @@ Requires-Dist: pydantic>=2.4
12
12
  Requires-Dist: numpy
13
13
  Requires-Dist: requests
14
14
  Requires-Dist: rdkit
15
+ Requires-Dist: more-itertools
15
16
  Dynamic: license-file
16
17
 
17
18
  # stjames
@@ -2,3 +2,4 @@ pydantic>=2.4
2
2
  numpy
3
3
  requests
4
4
  rdkit
5
+ more-itertools
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes