rowan-python 0.0.5__py3-none-any.whl → 1.1.0__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.
- rowan/__init__.py +7 -1
- rowan/calculation.py +11 -0
- rowan/client.py +46 -151
- rowan/constants.py +1 -0
- rowan/folder.py +97 -0
- rowan/rowan_rdkit/__init__.py +3 -0
- rowan/rowan_rdkit/chem_utils.py +610 -0
- rowan/utils.py +22 -1
- rowan/workflow.py +139 -0
- {rowan_python-0.0.5.dist-info → rowan_python-1.1.0.dist-info}/METADATA +6 -9
- rowan_python-1.1.0.dist-info/RECORD +13 -0
- {rowan_python-0.0.5.dist-info → rowan_python-1.1.0.dist-info}/WHEEL +1 -2
- rowan_python-0.0.5.dist-info/RECORD +0 -8
- rowan_python-0.0.5.dist-info/top_level.txt +0 -1
- {rowan_python-0.0.5.dist-info → rowan_python-1.1.0.dist-info/licenses}/LICENSE +0 -0
rowan/__init__.py
CHANGED
rowan/calculation.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import stjames
|
|
2
|
+
|
|
3
|
+
from .utils import api_client
|
|
4
|
+
|
|
5
|
+
class Calculation:
|
|
6
|
+
@classmethod
|
|
7
|
+
def retrieve(cls, uuid: stjames.UUID) -> dict:
|
|
8
|
+
with api_client() as client:
|
|
9
|
+
response = client.get(f"/calculation/{uuid}/stjames")
|
|
10
|
+
response.raise_for_status()
|
|
11
|
+
return response.json()
|
rowan/client.py
CHANGED
|
@@ -1,155 +1,50 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
1
|
import cctk
|
|
4
|
-
import httpx
|
|
5
|
-
from typing import Optional
|
|
6
2
|
import stjames
|
|
7
|
-
from dataclasses import dataclass, field
|
|
8
3
|
import time
|
|
4
|
+
from typing import Optional
|
|
9
5
|
|
|
10
|
-
import
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
headers=self.headers,
|
|
56
|
-
json={
|
|
57
|
-
"json_data": calc.model_dump(mode="json"),
|
|
58
|
-
"folder_uuid": folder_uuid,
|
|
59
|
-
},
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
else:
|
|
63
|
-
response = client.post(
|
|
64
|
-
f"{API_URL}/workflow",
|
|
65
|
-
headers=self.headers,
|
|
66
|
-
json={
|
|
67
|
-
"initial_molecule": molecule.model_dump(mode="json"),
|
|
68
|
-
"workflow_type": type,
|
|
69
|
-
"name": name,
|
|
70
|
-
"folder_uuid": folder_uuid,
|
|
71
|
-
"workflow_data": options,
|
|
72
|
-
},
|
|
73
|
-
)
|
|
74
|
-
|
|
75
|
-
response.raise_for_status()
|
|
76
|
-
response_dict = response.json()
|
|
77
|
-
calc_uuid = response_dict["uuid"]
|
|
78
|
-
|
|
79
|
-
if self.blocking:
|
|
80
|
-
while not self.is_finished(calc_uuid, type):
|
|
81
|
-
time.sleep(self.ping_interval)
|
|
82
|
-
result = self.get(calc_uuid, type)
|
|
83
|
-
|
|
84
|
-
if self.delete_when_finished:
|
|
85
|
-
self.delete(calc_uuid, type)
|
|
86
|
-
|
|
87
|
-
return result
|
|
88
|
-
|
|
89
|
-
else:
|
|
90
|
-
return calc_uuid
|
|
91
|
-
|
|
92
|
-
def is_finished(self, calc_uuid: str, type: str = "calculation") -> bool:
|
|
93
|
-
with httpx.Client() as client:
|
|
94
|
-
if type == "calculation":
|
|
95
|
-
response = client.get(f"{API_URL}/calculation/{calc_uuid}", headers=self.headers)
|
|
96
|
-
response.raise_for_status()
|
|
97
|
-
response_dict = response.json()
|
|
98
|
-
status = response_dict["status"]
|
|
99
|
-
|
|
100
|
-
else:
|
|
101
|
-
response = client.get(f"{API_URL}/workflow/{calc_uuid}", headers=self.headers)
|
|
102
|
-
response.raise_for_status()
|
|
103
|
-
response_dict = response.json()
|
|
104
|
-
status = response_dict["object_status"]
|
|
105
|
-
|
|
106
|
-
return status in [2, 3, 4]
|
|
107
|
-
|
|
108
|
-
def get(self, calc_uuid: str, type: str = "calculation") -> dict:
|
|
109
|
-
with httpx.Client() as client:
|
|
110
|
-
if type == "calculation":
|
|
111
|
-
stj_response = client.get(f"{API_URL}/calculation/{calc_uuid}/stjames", headers=self.headers)
|
|
112
|
-
stj_response.raise_for_status()
|
|
113
|
-
stj_dict = stj_response.json()
|
|
114
|
-
|
|
115
|
-
response = client.get(f"{API_URL}/calculation/{calc_uuid}", headers=self.headers)
|
|
116
|
-
response.raise_for_status()
|
|
117
|
-
response_dict = response.json()
|
|
118
|
-
|
|
119
|
-
# reformat
|
|
120
|
-
del response_dict["settings"]
|
|
121
|
-
response_dict["data"] = stj_dict
|
|
122
|
-
|
|
123
|
-
return response_dict
|
|
124
|
-
|
|
125
|
-
else:
|
|
126
|
-
response = client.get(f"{API_URL}/workflow/{calc_uuid}", headers=self.headers)
|
|
127
|
-
response.raise_for_status()
|
|
128
|
-
response_dict = response.json()
|
|
129
|
-
|
|
130
|
-
# reformat
|
|
131
|
-
response_dict["data"] = response_dict["object_data"]
|
|
132
|
-
del response_dict["object_data"]
|
|
133
|
-
del response_dict["status"]
|
|
134
|
-
|
|
135
|
-
return response_dict
|
|
136
|
-
|
|
137
|
-
def stop(self, calc_uuid: str, type: str = "calculation") -> None:
|
|
138
|
-
with httpx.Client() as client:
|
|
139
|
-
if type == "calculation":
|
|
140
|
-
response = client.post(f"{API_URL}/calculation/{calc_uuid}/stop", headers=self.headers)
|
|
141
|
-
response.raise_for_status()
|
|
142
|
-
|
|
143
|
-
else:
|
|
144
|
-
response = client.post(f"{API_URL}/workflow/{calc_uuid}/stop", headers=self.headers)
|
|
145
|
-
response.raise_for_status()
|
|
146
|
-
|
|
147
|
-
def delete(self, calc_uuid: str, type: str = "calculation") -> None:
|
|
148
|
-
with httpx.Client() as client:
|
|
149
|
-
if type == "calculation":
|
|
150
|
-
response = client.delete(f"{API_URL}/calculation/{calc_uuid}", headers=self.headers)
|
|
151
|
-
response.raise_for_status()
|
|
152
|
-
|
|
153
|
-
else:
|
|
154
|
-
response = client.delete(f"{API_URL}/folder/{calc_uuid}", headers=self.headers)
|
|
155
|
-
response.raise_for_status()
|
|
6
|
+
from .utils import cctk_to_stjames, smiles_to_stjames
|
|
7
|
+
from .workflow import Workflow
|
|
8
|
+
|
|
9
|
+
""" A high-level interface to submitting a calculation. """
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def compute(
|
|
13
|
+
molecule: str | cctk.Molecule | stjames.Molecule,
|
|
14
|
+
workflow_type: str,
|
|
15
|
+
name: str = "",
|
|
16
|
+
folder_uuid: Optional[stjames.UUID] = None,
|
|
17
|
+
blocking: bool = True,
|
|
18
|
+
ping_interval: int = 5,
|
|
19
|
+
**workflow_data,
|
|
20
|
+
) -> dict:
|
|
21
|
+
"""High-level function to compute and return workflows."""
|
|
22
|
+
|
|
23
|
+
if isinstance(molecule, str):
|
|
24
|
+
stjmol = smiles_to_stjames(molecule)
|
|
25
|
+
elif isinstance(molecule, cctk.Molecule):
|
|
26
|
+
stjmol = cctk_to_stjames(molecule)
|
|
27
|
+
elif isinstance(molecule, stjames.Molecule):
|
|
28
|
+
stjmol = molecule
|
|
29
|
+
else:
|
|
30
|
+
raise ValueError("Invalid type for `molecule`!")
|
|
31
|
+
|
|
32
|
+
result = Workflow.submit(
|
|
33
|
+
initial_molecule=stjmol,
|
|
34
|
+
workflow_type=workflow_type,
|
|
35
|
+
name=name,
|
|
36
|
+
folder_uuid=folder_uuid,
|
|
37
|
+
workflow_data=workflow_data,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
if blocking:
|
|
41
|
+
uuid = result["uuid"]
|
|
42
|
+
|
|
43
|
+
while not Workflow.is_finished(uuid):
|
|
44
|
+
time.sleep(ping_interval)
|
|
45
|
+
|
|
46
|
+
completed_result = Workflow.retrieve(uuid)
|
|
47
|
+
return completed_result
|
|
48
|
+
|
|
49
|
+
else:
|
|
50
|
+
return result
|
rowan/constants.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
API_URL = "https://api.rowansci.com"
|
rowan/folder.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import stjames
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from .utils import api_client
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Folder:
|
|
8
|
+
@classmethod
|
|
9
|
+
def create(
|
|
10
|
+
cls,
|
|
11
|
+
name: str,
|
|
12
|
+
parent_uuid: Optional[stjames.UUID] = None,
|
|
13
|
+
notes: str = "",
|
|
14
|
+
starred: bool = False,
|
|
15
|
+
public: bool = False,
|
|
16
|
+
) -> dict:
|
|
17
|
+
with api_client() as client:
|
|
18
|
+
response = client.post(
|
|
19
|
+
"/folder",
|
|
20
|
+
json={
|
|
21
|
+
"name": name,
|
|
22
|
+
"parent_uuid": parent_uuid,
|
|
23
|
+
"notes": notes,
|
|
24
|
+
"starred": starred,
|
|
25
|
+
"public": public,
|
|
26
|
+
},
|
|
27
|
+
)
|
|
28
|
+
response.raise_for_status()
|
|
29
|
+
return response.json()
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def retrieve(cls, uuid: stjames.UUID) -> dict:
|
|
33
|
+
with api_client() as client:
|
|
34
|
+
response = client.get(f"/folder/{uuid}")
|
|
35
|
+
response.raise_for_status()
|
|
36
|
+
return response.json()
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def update(
|
|
40
|
+
cls,
|
|
41
|
+
uuid: stjames.UUID,
|
|
42
|
+
name: Optional[str] = None,
|
|
43
|
+
parent_uuid: Optional[stjames.UUID] = None,
|
|
44
|
+
notes: Optional[str] = None,
|
|
45
|
+
starred: Optional[bool] = None,
|
|
46
|
+
public: Optional[bool] = None,
|
|
47
|
+
) -> None:
|
|
48
|
+
old_data = cls.retrieve(uuid)
|
|
49
|
+
|
|
50
|
+
new_data = {}
|
|
51
|
+
new_data["name"] = name if name is not None else old_data["name"]
|
|
52
|
+
new_data["parent_uuid"] = (
|
|
53
|
+
parent_uuid if parent_uuid is not None else old_data["parent_uuid"]
|
|
54
|
+
)
|
|
55
|
+
new_data["notes"] = notes if notes is not None else old_data["notes"]
|
|
56
|
+
new_data["starred"] = starred if starred is not None else old_data["starred"]
|
|
57
|
+
new_data["public"] = public if public is not None else old_data["public"]
|
|
58
|
+
|
|
59
|
+
with api_client() as client:
|
|
60
|
+
response = client.post(f"/folder/{uuid}", json=new_data)
|
|
61
|
+
response.raise_for_status()
|
|
62
|
+
return response.json()
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def delete(cls, uuid: stjames.UUID) -> None:
|
|
66
|
+
with api_client() as client:
|
|
67
|
+
response = client.delete(f"/folder/{uuid}")
|
|
68
|
+
response.raise_for_status()
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def list(
|
|
72
|
+
cls,
|
|
73
|
+
parent_uuid: Optional[stjames.UUID] = None,
|
|
74
|
+
name_contains: Optional[str] = None,
|
|
75
|
+
public: Optional[bool] = None,
|
|
76
|
+
starred: Optional[bool] = None,
|
|
77
|
+
page: int = 0,
|
|
78
|
+
size: int = 10,
|
|
79
|
+
):
|
|
80
|
+
params = {"page": page, "size": size}
|
|
81
|
+
|
|
82
|
+
if parent_uuid is not None:
|
|
83
|
+
params["parent_uuid"] = parent_uuid
|
|
84
|
+
|
|
85
|
+
if name_contains is not None:
|
|
86
|
+
params["name_contains"] = name_contains
|
|
87
|
+
|
|
88
|
+
if public is not None:
|
|
89
|
+
params["public"] = public
|
|
90
|
+
|
|
91
|
+
if starred is not None:
|
|
92
|
+
params["starred"] = starred
|
|
93
|
+
|
|
94
|
+
with api_client() as client:
|
|
95
|
+
response = client.get("/folder", params=params)
|
|
96
|
+
response.raise_for_status()
|
|
97
|
+
return response.json()
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
from .chem_utils import pka, tautomers, conformers, energy, optimize, batch_pka, batch_tautomers, batch_energy, batch_optimize, batch_conformers
|
|
2
|
+
|
|
3
|
+
__all__ = ["pka", "tautomers", "energy", "conformers", "optimize", "batch_pka", "batch_tautomers", "batch_energy", "batch_optimize", "batch_conformers"]
|
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
from rdkit import Chem
|
|
2
|
+
from rdkit.Chem import AllChem
|
|
3
|
+
from typing import Literal, TypeAlias, Optional, List
|
|
4
|
+
import rowan
|
|
5
|
+
import copy
|
|
6
|
+
import asyncio
|
|
7
|
+
import logging
|
|
8
|
+
import math
|
|
9
|
+
from rowan.utils import get_api_key
|
|
10
|
+
import stjames
|
|
11
|
+
import time
|
|
12
|
+
import numpy as np
|
|
13
|
+
import cctk
|
|
14
|
+
|
|
15
|
+
RdkitMol: TypeAlias = Chem.rdchem.Mol | Chem.rdchem.RWMol
|
|
16
|
+
pKaMode = Literal["reckless", "rapid", "careful"]
|
|
17
|
+
TautomerMode = Literal["reckless", "rapid", "careful"]
|
|
18
|
+
ConformerMode = Literal["reckless", "rapid"]
|
|
19
|
+
FAST_METHODS: list[stjames.Method] = [
|
|
20
|
+
*stjames.method.XTB_METHODS,
|
|
21
|
+
*stjames.method.NNP_METHODS,
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
class ConversionError(ValueError):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
class NoConformersError(Exception):
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
class MethodTooSlowError(Exception):
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
def _get_rdkit_mol_from_uuid(calculation_uuid: str) -> RdkitMol:
|
|
34
|
+
stjames_mol_dict = rowan.Calculation.retrieve(calculation_uuid)["molecules"][-1]
|
|
35
|
+
stjames_mol = stjames.Molecule(**stjames_mol_dict)
|
|
36
|
+
rdkm = Chem.MolFromXYZBlock(stjames_mol.to_xyz())
|
|
37
|
+
return rdkm
|
|
38
|
+
|
|
39
|
+
def embed_rdkit_mol(rdkm: RdkitMol):
|
|
40
|
+
try:
|
|
41
|
+
AllChem.SanitizeMol(rdkm)
|
|
42
|
+
except Exception as e:
|
|
43
|
+
raise ValueError(f"Molecule could not be generated -- invalid chemistry!\n{e}")
|
|
44
|
+
|
|
45
|
+
rdkm = AllChem.AddHs(rdkm)
|
|
46
|
+
try:
|
|
47
|
+
status1 = AllChem.EmbedMolecule(rdkm, maxAttempts=200)
|
|
48
|
+
assert status1 >= 0
|
|
49
|
+
except Exception as e:
|
|
50
|
+
status1 = AllChem.EmbedMolecule(rdkm, maxAttempts=200, useRandomCoords=True)
|
|
51
|
+
if status1 < 0:
|
|
52
|
+
raise ValueError(f"Cannot embed molecule! Error: {e}")
|
|
53
|
+
try:
|
|
54
|
+
status2 = AllChem.MMFFOptimizeMolecule(rdkm, maxIters=200)
|
|
55
|
+
assert status2 >= 0
|
|
56
|
+
except AssertionError:
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
return rdkm
|
|
60
|
+
|
|
61
|
+
def rdkit_to_cctk(rdkm: RdkitMol, cid: int = 0) -> cctk.Molecule:
|
|
62
|
+
if len(rdkm.GetConformers()) == 0:
|
|
63
|
+
rdkm = embed_rdkit_mol(rdkm)
|
|
64
|
+
try:
|
|
65
|
+
nums = [atom.GetAtomicNum() for atom in rdkm.GetAtoms()]
|
|
66
|
+
geom = rdkm.GetConformers()[cid].GetPositions()
|
|
67
|
+
return cctk.Molecule(nums, geom, charge=Chem.GetFormalCharge(rdkm))
|
|
68
|
+
except IndexError as e:
|
|
69
|
+
raise ConversionError("RDKit molecule does not have a conformer with the given ID") from e
|
|
70
|
+
|
|
71
|
+
def cctk_to_stjames(cmol: cctk.Molecule) -> stjames.Molecule:
|
|
72
|
+
atomic_numbers = cmol.atomic_numbers.view(np.ndarray)
|
|
73
|
+
geometry = cmol.geometry.view(np.ndarray)
|
|
74
|
+
atoms = []
|
|
75
|
+
for i in range(cmol.num_atoms()):
|
|
76
|
+
atoms.append(stjames.Atom(atomic_number=atomic_numbers[i], position=geometry[i]))
|
|
77
|
+
|
|
78
|
+
return stjames.Molecule(atoms=atoms, charge=cmol.charge, multiplicity=cmol.multiplicity)
|
|
79
|
+
|
|
80
|
+
def rdkit_to_stjames(rdkm: RdkitMol, cid: int = 0) -> stjames.Molecule:
|
|
81
|
+
cmol = rdkit_to_cctk(rdkm, cid)
|
|
82
|
+
return cctk_to_stjames(cmol)
|
|
83
|
+
|
|
84
|
+
def pka(mol: RdkitMol,
|
|
85
|
+
mode: pKaMode = "rapid",
|
|
86
|
+
timeout: int = 600,
|
|
87
|
+
name: str = "pKa API Workflow",
|
|
88
|
+
pka_range: tuple[int, int] = (2, 12),
|
|
89
|
+
deprotonate_elements: list[int] = [7, 8, 16],
|
|
90
|
+
protonate_elements: list[int] = [7],
|
|
91
|
+
folder_uuid: Optional[stjames.UUID] = None)-> tuple[dict[int, float], dict[int, float]]:
|
|
92
|
+
return asyncio.run(_single_pka(mol, mode, timeout, name, pka_range, deprotonate_elements, protonate_elements, folder_uuid))
|
|
93
|
+
|
|
94
|
+
def batch_pka(mols: List[RdkitMol],
|
|
95
|
+
mode: pKaMode = "rapid",
|
|
96
|
+
timeout: int = 600,
|
|
97
|
+
name: str = "pKa API Workflow",
|
|
98
|
+
pka_range: tuple[int, int] = (2, 12),
|
|
99
|
+
deprotonate_elements: list[int] = [7, 8, 16],
|
|
100
|
+
protonate_elements: list[int] = [7],
|
|
101
|
+
folder_uuid: Optional[stjames.UUID] = None)-> tuple[dict[int, float], dict[int, float]]:
|
|
102
|
+
loop = asyncio.new_event_loop()
|
|
103
|
+
asyncio.set_event_loop(loop)
|
|
104
|
+
tasks = [
|
|
105
|
+
_single_pka(mol, mode, timeout, name, pka_range, deprotonate_elements, protonate_elements, folder_uuid)
|
|
106
|
+
for mol in mols
|
|
107
|
+
]
|
|
108
|
+
results = loop.run_until_complete(asyncio.gather(*tasks))
|
|
109
|
+
return results
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
async def _single_pka(mol: RdkitMol,
|
|
113
|
+
mode: pKaMode = "rapid",
|
|
114
|
+
timeout: int = 600,
|
|
115
|
+
name: str = "pKa API Workflow",
|
|
116
|
+
pka_range: tuple[int, int] = (2, 12),
|
|
117
|
+
deprotonate_elements: list[int] = [7, 8, 16],
|
|
118
|
+
protonate_elements: list[int] = [7],
|
|
119
|
+
folder_uuid: Optional[stjames.UUID] = None) -> tuple[dict[int, float], dict[int, float]]:
|
|
120
|
+
"""
|
|
121
|
+
Calculate the pKa of a molecule.
|
|
122
|
+
:param mol: RDKit molecule object
|
|
123
|
+
:return: dictionary of pKa values
|
|
124
|
+
"""
|
|
125
|
+
get_api_key()
|
|
126
|
+
post = rowan.Workflow.submit(
|
|
127
|
+
name=name,
|
|
128
|
+
workflow_type="pka",
|
|
129
|
+
initial_molecule=rdkit_to_stjames(mol),
|
|
130
|
+
workflow_data={
|
|
131
|
+
"pka_range": pka_range,
|
|
132
|
+
"deprotonate_elements": deprotonate_elements,
|
|
133
|
+
"deprotonate_atoms": [],
|
|
134
|
+
"protonate_elements": protonate_elements,
|
|
135
|
+
"protonate_atoms": [],
|
|
136
|
+
"mode": mode,
|
|
137
|
+
},
|
|
138
|
+
folder_uuid=folder_uuid
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
start = time.time()
|
|
142
|
+
while not rowan.Workflow.is_finished(post["uuid"]):
|
|
143
|
+
await asyncio.sleep(5)
|
|
144
|
+
if time.time() - start > timeout:
|
|
145
|
+
raise TimeoutError("Workflow timed out")
|
|
146
|
+
|
|
147
|
+
result = rowan.Workflow.retrieve(post["uuid"])
|
|
148
|
+
|
|
149
|
+
acidic_pkas = []
|
|
150
|
+
for microstate in result["object_data"]["conjugate_bases"]:
|
|
151
|
+
symbol = cctk.helper_functions.get_symbol(result["object_data"]["initial_molecule"]["atoms"][microstate["atom_index"]-1]["atomic_number"])
|
|
152
|
+
acidic_pkas.append({
|
|
153
|
+
"element": symbol,
|
|
154
|
+
"index": microstate["atom_index"],
|
|
155
|
+
"pKa": round(microstate["pka"], 2)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
basic_pkas = []
|
|
160
|
+
for microstate in result["object_data"]["conjugate_acids"]:
|
|
161
|
+
symbol = cctk.helper_functions.get_symbol(result["object_data"]["initial_molecule"]["atoms"][microstate["atom_index"]-1]["atomic_number"])
|
|
162
|
+
basic_pkas.append({
|
|
163
|
+
"element": symbol,
|
|
164
|
+
"index": microstate["atom_index"],
|
|
165
|
+
"pKa": round(microstate["pka"], 2)
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
return {"acidic_pkas": acidic_pkas, "basic_pkas": basic_pkas}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def tautomers(mol: RdkitMol,
|
|
172
|
+
mode: TautomerMode = "reckless",
|
|
173
|
+
timeout: int = 600,
|
|
174
|
+
name: str = "Tautomers API Workflow",
|
|
175
|
+
folder_uuid: Optional[stjames.UUID] = None) -> list[tuple[RdkitMol, float]]:
|
|
176
|
+
"""
|
|
177
|
+
Generate possible tautomers of a molecule.
|
|
178
|
+
:param mol: RDKit molecule object
|
|
179
|
+
:return: A list of tautomer dictionaries which include the RDKit molecule object, the relative energy, and the weight
|
|
180
|
+
"""
|
|
181
|
+
return asyncio.run(_single_tautomers(mol, mode, timeout, name, folder_uuid))
|
|
182
|
+
|
|
183
|
+
def batch_tautomers(mols: List[RdkitMol],
|
|
184
|
+
mode: TautomerMode = "reckless",
|
|
185
|
+
timeout: int = 600,
|
|
186
|
+
name: str = "Tautomers API Workflow",
|
|
187
|
+
folder_uuid: Optional[stjames.UUID] = None) -> list[tuple[RdkitMol, float]]:
|
|
188
|
+
"""
|
|
189
|
+
Generate possible tautomers of a molecule.
|
|
190
|
+
:param mol: RDKit molecule object
|
|
191
|
+
:return: A list of lists of tautomer dictionaries which include the RDKit molecule object, the relative energy, and the weight
|
|
192
|
+
"""
|
|
193
|
+
loop = asyncio.new_event_loop()
|
|
194
|
+
asyncio.set_event_loop(loop)
|
|
195
|
+
tasks = [_single_tautomers(mol, mode, timeout, name, folder_uuid) for mol in mols]
|
|
196
|
+
results = loop.run_until_complete(asyncio.gather(*tasks))
|
|
197
|
+
return results
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
async def _single_tautomers(mol: RdkitMol,
|
|
201
|
+
mode: TautomerMode = "reckless",
|
|
202
|
+
timeout: int = 600,
|
|
203
|
+
name: str = "Tautomers API Workflow",
|
|
204
|
+
folder_uuid: Optional[stjames.UUID] = None) -> list[tuple[RdkitMol, float]]:
|
|
205
|
+
"""
|
|
206
|
+
Generate possible tautomers of a molecule.
|
|
207
|
+
:param mol: RDKit molecule object
|
|
208
|
+
:return: A list of tautomer dictionaries which include the RDKit molecule object, the relative energy, and the weight
|
|
209
|
+
"""
|
|
210
|
+
get_api_key()
|
|
211
|
+
post = rowan.Workflow.submit(
|
|
212
|
+
name=name,
|
|
213
|
+
workflow_type="tautomers",
|
|
214
|
+
initial_molecule=rdkit_to_stjames(mol),
|
|
215
|
+
workflow_data={
|
|
216
|
+
"mode": mode,
|
|
217
|
+
},
|
|
218
|
+
folder_uuid=folder_uuid
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
start = time.time()
|
|
222
|
+
while not rowan.Workflow.is_finished(post["uuid"]):
|
|
223
|
+
await asyncio.sleep(5)
|
|
224
|
+
if time.time() - start > timeout:
|
|
225
|
+
raise TimeoutError("Workflow timed out")
|
|
226
|
+
|
|
227
|
+
result = rowan.Workflow.retrieve(post["uuid"])
|
|
228
|
+
|
|
229
|
+
tautomers = []
|
|
230
|
+
for tautomer in result["object_data"]["tautomers"]:
|
|
231
|
+
rdkit_mol = _get_rdkit_mol_from_uuid(tautomer["structures"][0]["uuid"])
|
|
232
|
+
tautomers.append({
|
|
233
|
+
"molecule": rdkit_mol,
|
|
234
|
+
"predicted_relative_energy": round(tautomer["predicted_relative_energy"], 2),
|
|
235
|
+
"weight": round(tautomer["weight"], 5)
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
#return relative weights too
|
|
239
|
+
return tautomers
|
|
240
|
+
|
|
241
|
+
def energy(
|
|
242
|
+
mol: RdkitMol,
|
|
243
|
+
method: str = "aimnet2_wb97md3",
|
|
244
|
+
engine: str = "aimnet2",
|
|
245
|
+
mode: str = "auto",
|
|
246
|
+
timeout: int = 600,
|
|
247
|
+
name: str = "Energy API Workflow",
|
|
248
|
+
folder_uuid: Optional[stjames.UUID] = None
|
|
249
|
+
):
|
|
250
|
+
"""
|
|
251
|
+
Computes the energy for the given molecule.
|
|
252
|
+
|
|
253
|
+
:param mol: the input molecule
|
|
254
|
+
:param method: the method with which to compute the molecule's energy
|
|
255
|
+
:raises: MethodTooSlowError if the method is invalid
|
|
256
|
+
:returns: a dictionary with the energy in Hartree and the conformer index
|
|
257
|
+
"""
|
|
258
|
+
return asyncio.run(_single_energy(mol, method, engine, mode, timeout, name, folder_uuid))
|
|
259
|
+
|
|
260
|
+
def batch_energy(
|
|
261
|
+
mols: List[RdkitMol],
|
|
262
|
+
method: str = "aimnet2_wb97md3",
|
|
263
|
+
engine: str = "aimnet2",
|
|
264
|
+
mode: str = "auto",
|
|
265
|
+
timeout: int = 600,
|
|
266
|
+
name: str = "Energy API Workflow",
|
|
267
|
+
folder_uuid: Optional[stjames.UUID] = None
|
|
268
|
+
):
|
|
269
|
+
"""
|
|
270
|
+
Computes the energy for the given molecule.
|
|
271
|
+
|
|
272
|
+
:param mol: the input molecule
|
|
273
|
+
:param method: the method with which to compute the molecule's energy
|
|
274
|
+
:raises: MethodTooSlowError if the method is invalid
|
|
275
|
+
:returns: a list of dictionaries with the energy in Hartree and the conformer index
|
|
276
|
+
"""
|
|
277
|
+
loop = asyncio.new_event_loop()
|
|
278
|
+
asyncio.set_event_loop(loop)
|
|
279
|
+
tasks = [_single_energy(mol, method, engine, mode, timeout, name, folder_uuid) for mol in mols]
|
|
280
|
+
results = loop.run_until_complete(asyncio.gather(*tasks))
|
|
281
|
+
return results
|
|
282
|
+
|
|
283
|
+
async def _single_energy(
|
|
284
|
+
mol: RdkitMol,
|
|
285
|
+
method: str = "aimnet2_wb97md3",
|
|
286
|
+
engine: str = "aimnet2",
|
|
287
|
+
mode: str = "auto",
|
|
288
|
+
timeout: int = 600,
|
|
289
|
+
name: str = "Energy API Workflow",
|
|
290
|
+
folder_uuid: Optional[stjames.UUID] = None
|
|
291
|
+
):
|
|
292
|
+
"""
|
|
293
|
+
Computes the energy for the given molecule.
|
|
294
|
+
|
|
295
|
+
:param mol: the input molecule
|
|
296
|
+
:param method: the method with which to compute the molecule's energy
|
|
297
|
+
:raises: MethodTooSlowError if the method is invalid
|
|
298
|
+
:returns: a dictionary with the energy in Hartree and the conformer index
|
|
299
|
+
"""
|
|
300
|
+
get_api_key()
|
|
301
|
+
|
|
302
|
+
method = stjames.Method(method)
|
|
303
|
+
|
|
304
|
+
if mol.GetNumConformers() == 0:
|
|
305
|
+
mol = embed_rdkit_mol(mol)
|
|
306
|
+
if mol.GetNumConformers() == 0:
|
|
307
|
+
raise NoConformersError("This molecule has no conformers")
|
|
308
|
+
|
|
309
|
+
if method not in FAST_METHODS:
|
|
310
|
+
raise MethodTooSlowError(
|
|
311
|
+
"This method is too slow; try running this through our web interface."
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
workflow_uuids = []
|
|
315
|
+
for conformer in mol.GetConformers():
|
|
316
|
+
cid = conformer.GetId()
|
|
317
|
+
stjames_mol = rdkit_to_stjames(mol, cid)
|
|
318
|
+
get_api_key()
|
|
319
|
+
post = rowan.Workflow.submit(
|
|
320
|
+
name=name,
|
|
321
|
+
workflow_type="basic_calculation",
|
|
322
|
+
initial_molecule=stjames_mol,
|
|
323
|
+
workflow_data={
|
|
324
|
+
"settings": {
|
|
325
|
+
"method": method.value,
|
|
326
|
+
"corrections": [],
|
|
327
|
+
"tasks": [
|
|
328
|
+
"energy"
|
|
329
|
+
],
|
|
330
|
+
"mode": mode,
|
|
331
|
+
"opt_settings": {
|
|
332
|
+
"constraints": []
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
"engine": engine
|
|
336
|
+
},
|
|
337
|
+
folder_uuid=folder_uuid
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
workflow_uuids.append(post["uuid"])
|
|
341
|
+
|
|
342
|
+
start = time.time()
|
|
343
|
+
while not all(rowan.Workflow.is_finished(uuid) for uuid in workflow_uuids):
|
|
344
|
+
await asyncio.sleep(5)
|
|
345
|
+
if time.time() - start > timeout:
|
|
346
|
+
raise TimeoutError("Workflow timed out")
|
|
347
|
+
|
|
348
|
+
workflow_results = [rowan.Workflow.retrieve(uuid) for uuid in workflow_uuids]
|
|
349
|
+
energies = [rowan.Calculation.retrieve(workflow["object_data"]["calculation_uuid"])["molecules"][-1]["energy"] for workflow in workflow_results]
|
|
350
|
+
|
|
351
|
+
return [{"conformer_index": index, "energy": energy} for index, energy in enumerate(energies)]
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def optimize(
|
|
355
|
+
mol: RdkitMol,
|
|
356
|
+
method: str = "aimnet2_wb97md3",
|
|
357
|
+
engine: str = "aimnet2",
|
|
358
|
+
mode: str = "auto",
|
|
359
|
+
return_energies: bool = False,
|
|
360
|
+
timeout: int = 600,
|
|
361
|
+
name: str = "Optimize API Workflow",
|
|
362
|
+
folder_uuid: Optional[stjames.UUID] = None
|
|
363
|
+
) -> RdkitMol | tuple[RdkitMol, dict[int, float]]:
|
|
364
|
+
"""
|
|
365
|
+
Optimize each of a molecule's conformers and then return the molecule.
|
|
366
|
+
|
|
367
|
+
:param mol: the input molecule
|
|
368
|
+
:param method: the method with which to compute the molecule's energy
|
|
369
|
+
:param return_energies: whether to return energies in Hartree too
|
|
370
|
+
:raises: MethodTooSlowError if the method is invalid
|
|
371
|
+
:returns: a dictionary with the molecule, with optimized conformers, and optionally a list of energies per conformer too
|
|
372
|
+
|
|
373
|
+
"""
|
|
374
|
+
return asyncio.run(_single_optimize(mol, method, engine, mode, return_energies, timeout, name, folder_uuid))
|
|
375
|
+
|
|
376
|
+
def batch_optimize(
|
|
377
|
+
mols: List[RdkitMol],
|
|
378
|
+
method: str = "aimnet2_wb97md3",
|
|
379
|
+
engine: str = "aimnet2",
|
|
380
|
+
mode: str = "auto",
|
|
381
|
+
return_energies: bool = False,
|
|
382
|
+
timeout: int = 600,
|
|
383
|
+
name: str = "Optimize API Workflow",
|
|
384
|
+
folder_uuid: Optional[stjames.UUID] = None
|
|
385
|
+
) -> RdkitMol | tuple[RdkitMol, dict[int, float]]:
|
|
386
|
+
"""
|
|
387
|
+
Optimize each of a molecule's conformers and then return the molecule.
|
|
388
|
+
|
|
389
|
+
:param mol: the input molecule
|
|
390
|
+
:param method: the method with which to compute the molecule's energy
|
|
391
|
+
:param return_energies: whether to return energies in Hartree too
|
|
392
|
+
:raises: MethodTooSlowError if the method is invalid
|
|
393
|
+
:returns: a list of dictionaries with the molecule, with optimized conformers, and optionally a list of energies per conformer too
|
|
394
|
+
|
|
395
|
+
"""
|
|
396
|
+
loop = asyncio.new_event_loop()
|
|
397
|
+
asyncio.set_event_loop(loop)
|
|
398
|
+
tasks = [_single_optimize(mol, method, engine, mode, return_energies, timeout, name, folder_uuid) for mol in mols]
|
|
399
|
+
results = loop.run_until_complete(asyncio.gather(*tasks))
|
|
400
|
+
return results
|
|
401
|
+
|
|
402
|
+
async def _single_optimize(
|
|
403
|
+
mol: RdkitMol,
|
|
404
|
+
method: str = "aimnet2_wb97md3",
|
|
405
|
+
engine: str = "aimnet2",
|
|
406
|
+
mode: str = "auto",
|
|
407
|
+
return_energies: bool = False,
|
|
408
|
+
timeout: int = 600,
|
|
409
|
+
name: str = "Optimize API Workflow",
|
|
410
|
+
folder_uuid: Optional[stjames.UUID] = None
|
|
411
|
+
) -> RdkitMol | tuple[RdkitMol, dict[int, float]]:
|
|
412
|
+
"""
|
|
413
|
+
Optimize each of a molecule's conformers and then return the molecule.
|
|
414
|
+
|
|
415
|
+
:param mol: the input molecule
|
|
416
|
+
:param method: the method with which to compute the molecule's energy
|
|
417
|
+
:param return_energies: whether to return energies in Hartree too
|
|
418
|
+
:raises: MethodTooSlowError if the method is invalid
|
|
419
|
+
:returns: a dictionary with the molecule, with optimized conformers, and optionally a list of energies per conformer too
|
|
420
|
+
|
|
421
|
+
"""
|
|
422
|
+
get_api_key()
|
|
423
|
+
|
|
424
|
+
method = stjames.Method(method)
|
|
425
|
+
|
|
426
|
+
if mol.GetNumConformers() == 0:
|
|
427
|
+
mol = embed_rdkit_mol(mol)
|
|
428
|
+
if mol.GetNumConformers() == 0:
|
|
429
|
+
raise NoConformersError("This molecule has no conformers")
|
|
430
|
+
|
|
431
|
+
if method not in FAST_METHODS:
|
|
432
|
+
raise MethodTooSlowError(
|
|
433
|
+
"This method is too slow; try running this through our web interface."
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
optimized_mol = copy.deepcopy(mol)
|
|
437
|
+
|
|
438
|
+
workflow_uuids = []
|
|
439
|
+
for conformer in mol.GetConformers():
|
|
440
|
+
cid = conformer.GetId()
|
|
441
|
+
stjames_mol = rdkit_to_stjames(mol, cid)
|
|
442
|
+
get_api_key()
|
|
443
|
+
post = rowan.Workflow.submit(
|
|
444
|
+
name=name,
|
|
445
|
+
workflow_type="basic_calculation",
|
|
446
|
+
initial_molecule=stjames_mol,
|
|
447
|
+
workflow_data={
|
|
448
|
+
"settings": {
|
|
449
|
+
"method": method.value,
|
|
450
|
+
"corrections": [],
|
|
451
|
+
"tasks": [
|
|
452
|
+
"optimize"
|
|
453
|
+
],
|
|
454
|
+
"mode": mode,
|
|
455
|
+
"opt_settings": {
|
|
456
|
+
"constraints": []
|
|
457
|
+
}
|
|
458
|
+
},
|
|
459
|
+
"engine": engine
|
|
460
|
+
},
|
|
461
|
+
folder_uuid=folder_uuid
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
workflow_uuids.append(post["uuid"])
|
|
465
|
+
|
|
466
|
+
start = time.time()
|
|
467
|
+
while not all(rowan.Workflow.is_finished(uuid) for uuid in workflow_uuids):
|
|
468
|
+
await asyncio.sleep(5)
|
|
469
|
+
if time.time() - start > timeout:
|
|
470
|
+
raise TimeoutError("Workflow timed out")
|
|
471
|
+
|
|
472
|
+
workflow_results = [rowan.Workflow.retrieve(uuid) for uuid in workflow_uuids]
|
|
473
|
+
calculations = [rowan.Calculation.retrieve(workflow["object_data"]["calculation_uuid"]) for workflow in workflow_results]
|
|
474
|
+
optimization_atoms = [cacluation["molecules"][-1]["atoms"] for cacluation in calculations]
|
|
475
|
+
optimized_positions = [[atom["position"] for atom in atoms] for atoms in optimization_atoms]
|
|
476
|
+
|
|
477
|
+
energies = [cacluation["molecules"][-1]["energy"] for cacluation in calculations]
|
|
478
|
+
|
|
479
|
+
for i, conformer in enumerate(optimized_mol.GetConformers()):
|
|
480
|
+
conformer.SetPositions(np.array(optimized_positions[i]))
|
|
481
|
+
|
|
482
|
+
return_dict = {"rdkit_molecule": mol}
|
|
483
|
+
|
|
484
|
+
if return_energies:
|
|
485
|
+
return_dict["energies"] = energies
|
|
486
|
+
|
|
487
|
+
return return_dict
|
|
488
|
+
|
|
489
|
+
def conformers(mol: RdkitMol, num_conformers=10,
|
|
490
|
+
method: str = "aimnet2_wb97md3",
|
|
491
|
+
mode: str = "rapid",
|
|
492
|
+
return_energies: bool = False,
|
|
493
|
+
timeout: int = 600,
|
|
494
|
+
name: str = "Conformer API Workflow",
|
|
495
|
+
folder_uuid: Optional[stjames.UUID] = None):
|
|
496
|
+
"""
|
|
497
|
+
Generate conformers for a molecule.
|
|
498
|
+
:param mol: RDKit molecule object
|
|
499
|
+
:param num_conformers: Number of conformers to generate
|
|
500
|
+
:return: A dictonary with the RDKit molecule with num_conformers lowest energy conformers and optional energies
|
|
501
|
+
"""
|
|
502
|
+
return asyncio.run(_single_conformers(mol, num_conformers, method, mode, return_energies, timeout, name, folder_uuid))
|
|
503
|
+
|
|
504
|
+
def batch_conformers(mols: List[RdkitMol], num_conformers=10,
|
|
505
|
+
method: str = "aimnet2_wb97md3",
|
|
506
|
+
mode: str = "rapid",
|
|
507
|
+
return_energies: bool = False,
|
|
508
|
+
timeout: int = 600,
|
|
509
|
+
name: str = "Conformer API Workflow",
|
|
510
|
+
folder_uuid: Optional[stjames.UUID] = None):
|
|
511
|
+
"""
|
|
512
|
+
Generate conformers for a molecule.
|
|
513
|
+
:param mol: RDKit molecule object
|
|
514
|
+
:param num_conformers: Number of conformers to generate
|
|
515
|
+
:return: A list of dictonaries with the RDKit molecule with num_conformers lowest energy conformers and optional energies
|
|
516
|
+
"""
|
|
517
|
+
loop = asyncio.new_event_loop()
|
|
518
|
+
asyncio.set_event_loop(loop)
|
|
519
|
+
tasks = [_single_conformers(mol, num_conformers, method, mode, return_energies, timeout, name, folder_uuid) for mol in mols]
|
|
520
|
+
results = loop.run_until_complete(asyncio.gather(*tasks))
|
|
521
|
+
return results
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
async def _single_conformers(mol: RdkitMol, num_conformers=10,
|
|
525
|
+
method: str = "aimnet2_wb97md3",
|
|
526
|
+
mode: str = "rapid",
|
|
527
|
+
return_energies: bool = False,
|
|
528
|
+
timeout: int = 600,
|
|
529
|
+
name: str = "Conformer API Workflow",
|
|
530
|
+
folder_uuid: Optional[stjames.UUID] = None):
|
|
531
|
+
"""
|
|
532
|
+
Generate conformers for a molecule.
|
|
533
|
+
:param mol: RDKit molecule object
|
|
534
|
+
:param num_conformers: Number of conformers to generate
|
|
535
|
+
:return: A dictonary with the RDKit molecule with num_conformers lowest energy conformers and optional energies
|
|
536
|
+
"""
|
|
537
|
+
|
|
538
|
+
get_api_key()
|
|
539
|
+
|
|
540
|
+
method = stjames.Method(method)
|
|
541
|
+
|
|
542
|
+
if mol.GetNumConformers() == 0:
|
|
543
|
+
mol = embed_rdkit_mol(mol)
|
|
544
|
+
if mol.GetNumConformers() == 0:
|
|
545
|
+
raise NoConformersError("This molecule has no conformers")
|
|
546
|
+
|
|
547
|
+
if method not in FAST_METHODS:
|
|
548
|
+
raise MethodTooSlowError(
|
|
549
|
+
"This method is too slow; try running this through our web interface."
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
post = rowan.Workflow.submit(
|
|
553
|
+
name=name,
|
|
554
|
+
workflow_type="conformer_search",
|
|
555
|
+
initial_molecule=rdkit_to_stjames(mol),
|
|
556
|
+
workflow_data={
|
|
557
|
+
"conf_gen_mode": "rapid",
|
|
558
|
+
"mode": mode,
|
|
559
|
+
"mso_mode": "manual",
|
|
560
|
+
"multistage_opt_settings": {
|
|
561
|
+
"mode": "manual",
|
|
562
|
+
"optimization_settings": [
|
|
563
|
+
{
|
|
564
|
+
"method": method.value,
|
|
565
|
+
"tasks": [
|
|
566
|
+
"optimize"
|
|
567
|
+
],
|
|
568
|
+
"corrections": [],
|
|
569
|
+
"mode": "auto"
|
|
570
|
+
}
|
|
571
|
+
],
|
|
572
|
+
"solvent": None,
|
|
573
|
+
"transition_state": False,
|
|
574
|
+
"constraints": []
|
|
575
|
+
}
|
|
576
|
+
},
|
|
577
|
+
folder_uuid=folder_uuid
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
start = time.time()
|
|
581
|
+
while not rowan.Workflow.is_finished(post["uuid"]):
|
|
582
|
+
await asyncio.sleep(5)
|
|
583
|
+
if time.time() - start > timeout:
|
|
584
|
+
raise TimeoutError("Workflow timed out")
|
|
585
|
+
|
|
586
|
+
result = rowan.Workflow.retrieve(post["uuid"])
|
|
587
|
+
|
|
588
|
+
sorted_data = sorted(zip(result["object_data"]["energies"], result["object_data"]["conformer_uuids"]), key=lambda x: x[0])
|
|
589
|
+
|
|
590
|
+
if len(sorted_data) < num_conformers:
|
|
591
|
+
logging.warning("Number of conformers requested is greater than number of conformers available")
|
|
592
|
+
num_conformers = min(num_conformers, len(sorted_data))
|
|
593
|
+
|
|
594
|
+
# Extract the UUIDs of the lowest n energies
|
|
595
|
+
lowest_n_uuids = [item[1][0] for item in sorted_data[:num_conformers]]
|
|
596
|
+
lowest_energies = [item[0] for item in sorted_data[:num_conformers]]
|
|
597
|
+
|
|
598
|
+
AllChem.EmbedMultipleConfs(mol, numConfs=num_conformers)
|
|
599
|
+
|
|
600
|
+
for i, conformer in enumerate(mol.GetConformers()):
|
|
601
|
+
atoms = rowan.Calculation.retrieve(lowest_n_uuids[i])["molecules"][-1]["atoms"]
|
|
602
|
+
pos = [atom["position"] for atom in atoms]
|
|
603
|
+
conformer.SetPositions(np.array(pos))
|
|
604
|
+
|
|
605
|
+
return_dict = {"rdkit_molecule": mol}
|
|
606
|
+
|
|
607
|
+
if return_energies:
|
|
608
|
+
return_dict["energies"] = lowest_energies
|
|
609
|
+
|
|
610
|
+
return return_dict
|
rowan/utils.py
CHANGED
|
@@ -2,9 +2,13 @@ import os
|
|
|
2
2
|
import cctk
|
|
3
3
|
import stjames
|
|
4
4
|
import numpy as np
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
import httpx
|
|
5
7
|
|
|
6
8
|
import rowan
|
|
7
9
|
|
|
10
|
+
from .constants import API_URL
|
|
11
|
+
|
|
8
12
|
|
|
9
13
|
def get_api_key() -> str:
|
|
10
14
|
api_key = os.environ.get("ROWAN_API_KEY")
|
|
@@ -24,10 +28,27 @@ def cctk_to_stjames(molecule: cctk.Molecule) -> stjames.Molecule:
|
|
|
24
28
|
|
|
25
29
|
atoms = list()
|
|
26
30
|
for i in range(molecule.num_atoms()):
|
|
27
|
-
atoms.append(
|
|
31
|
+
atoms.append(
|
|
32
|
+
stjames.Atom(atomic_number=atomic_numbers[i], position=geometry[i])
|
|
33
|
+
)
|
|
28
34
|
|
|
29
35
|
return stjames.Molecule(
|
|
30
36
|
atoms=atoms,
|
|
31
37
|
charge=molecule.charge,
|
|
32
38
|
multiplicity=molecule.multiplicity,
|
|
33
39
|
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def smiles_to_stjames(smiles: str) -> stjames.Molecule:
|
|
43
|
+
cmol = cctk.Molecule.new_from_smiles(smiles)
|
|
44
|
+
return cctk_to_stjames(cmol)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@contextmanager
|
|
48
|
+
def api_client():
|
|
49
|
+
"""Wraps `httpx.Client` with Rowan-specific kwargs."""
|
|
50
|
+
with httpx.Client(
|
|
51
|
+
base_url=API_URL,
|
|
52
|
+
headers={"X-API-Key": get_api_key()},
|
|
53
|
+
) as client:
|
|
54
|
+
yield client
|
rowan/workflow.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import stjames
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
from .utils import api_client
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Workflow:
|
|
9
|
+
@classmethod
|
|
10
|
+
def submit(
|
|
11
|
+
cls,
|
|
12
|
+
workflow_type: str,
|
|
13
|
+
initial_molecule: dict | stjames.Molecule,
|
|
14
|
+
workflow_data: dict,
|
|
15
|
+
name: Optional[str] = None,
|
|
16
|
+
folder_uuid: Optional[stjames.UUID] = None,
|
|
17
|
+
) -> dict:
|
|
18
|
+
if isinstance(initial_molecule, stjames.Molecule):
|
|
19
|
+
molecule_dict = initial_molecule.model_dump()
|
|
20
|
+
elif isinstance(initial_molecule, dict):
|
|
21
|
+
molecule_dict = initial_molecule
|
|
22
|
+
else:
|
|
23
|
+
raise ValueError("Invalid type for `initial_molecule`")
|
|
24
|
+
|
|
25
|
+
with api_client() as client:
|
|
26
|
+
response = client.post(
|
|
27
|
+
"/workflow",
|
|
28
|
+
json={
|
|
29
|
+
"name": name,
|
|
30
|
+
"folder_uuid": folder_uuid,
|
|
31
|
+
"initial_molecule": molecule_dict,
|
|
32
|
+
"workflow_type": workflow_type,
|
|
33
|
+
"workflow_data": workflow_data,
|
|
34
|
+
},
|
|
35
|
+
)
|
|
36
|
+
response.raise_for_status()
|
|
37
|
+
return response.json()
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def retrieve(cls, uuid: stjames.UUID) -> dict:
|
|
41
|
+
with api_client() as client:
|
|
42
|
+
response = client.get(f"/workflow/{uuid}")
|
|
43
|
+
response.raise_for_status()
|
|
44
|
+
return response.json()
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def update(
|
|
48
|
+
cls,
|
|
49
|
+
uuid: stjames.UUID,
|
|
50
|
+
name: Optional[str] = None,
|
|
51
|
+
parent_uuid: Optional[stjames.UUID] = None,
|
|
52
|
+
notes: Optional[str] = None,
|
|
53
|
+
starred: Optional[bool] = None,
|
|
54
|
+
email_when_complete: Optional[bool] = None,
|
|
55
|
+
public: Optional[bool] = None,
|
|
56
|
+
) -> None:
|
|
57
|
+
old_data = cls.retrieve(uuid)
|
|
58
|
+
|
|
59
|
+
new_data = {}
|
|
60
|
+
new_data["name"] = name if name is not None else old_data["name"]
|
|
61
|
+
new_data["parent_uuid"] = (
|
|
62
|
+
parent_uuid if parent_uuid is not None else old_data["parent_uuid"]
|
|
63
|
+
)
|
|
64
|
+
new_data["notes"] = notes if notes is not None else old_data["notes"]
|
|
65
|
+
new_data["starred"] = starred if starred is not None else old_data["starred"]
|
|
66
|
+
new_data["email_when_complete"] = (
|
|
67
|
+
email_when_complete
|
|
68
|
+
if email_when_complete is not None
|
|
69
|
+
else old_data["email_when_complete"]
|
|
70
|
+
)
|
|
71
|
+
new_data["public"] = public if public is not None else old_data["public"]
|
|
72
|
+
|
|
73
|
+
with api_client() as client:
|
|
74
|
+
response = client.post(f"/workflow/{uuid}", json=new_data)
|
|
75
|
+
response.raise_for_status()
|
|
76
|
+
return response.json()
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def status(cls, uuid: stjames.UUID) -> int:
|
|
80
|
+
data = cls.retrieve(uuid)
|
|
81
|
+
return data["object_status"]
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def is_finished(cls, uuid: stjames.UUID) -> bool:
|
|
85
|
+
status = cls.status(uuid)
|
|
86
|
+
return status in {
|
|
87
|
+
stjames.Status.COMPLETED_OK.value,
|
|
88
|
+
stjames.Status.FAILED.value,
|
|
89
|
+
stjames.Status.STOPPED.value,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@classmethod
|
|
93
|
+
def stop(cls, uuid: stjames.UUID) -> None:
|
|
94
|
+
with api_client() as client:
|
|
95
|
+
response = client.post(f"/workflow/{uuid}/stop")
|
|
96
|
+
response.raise_for_status()
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
def delete(cls, uuid: stjames.UUID) -> None:
|
|
100
|
+
with api_client() as client:
|
|
101
|
+
response = client.delete(f"/workflow/{uuid}")
|
|
102
|
+
response.raise_for_status()
|
|
103
|
+
|
|
104
|
+
@classmethod
|
|
105
|
+
def list(
|
|
106
|
+
cls,
|
|
107
|
+
parent_uuid: Optional[stjames.UUID] = None,
|
|
108
|
+
name_contains: Optional[str] = None,
|
|
109
|
+
public: Optional[bool] = None,
|
|
110
|
+
starred: Optional[bool] = None,
|
|
111
|
+
object_status: Optional[int] = None,
|
|
112
|
+
object_type: Optional[str] = None,
|
|
113
|
+
page: int = 0,
|
|
114
|
+
size: int = 10,
|
|
115
|
+
):
|
|
116
|
+
params = {"page": page, "size": size}
|
|
117
|
+
|
|
118
|
+
if parent_uuid is not None:
|
|
119
|
+
params["parent_uuid"] = parent_uuid
|
|
120
|
+
|
|
121
|
+
if name_contains is not None:
|
|
122
|
+
params["name_contains"] = name_contains
|
|
123
|
+
|
|
124
|
+
if public is not None:
|
|
125
|
+
params["public"] = public
|
|
126
|
+
|
|
127
|
+
if starred is not None:
|
|
128
|
+
params["starred"] = starred
|
|
129
|
+
|
|
130
|
+
if object_status is not None:
|
|
131
|
+
params["object_status"] = object_status
|
|
132
|
+
|
|
133
|
+
if object_type is not None:
|
|
134
|
+
params["object_type"] = object_type
|
|
135
|
+
|
|
136
|
+
with api_client() as client:
|
|
137
|
+
response = client.get("/workflow", params=params)
|
|
138
|
+
response.raise_for_status()
|
|
139
|
+
return response.json()
|
|
@@ -1,22 +1,19 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: rowan-python
|
|
3
|
-
Version:
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: Rowan Python Library
|
|
5
|
-
Author-email: Corin Wagen <corin@rowansci.com>
|
|
6
5
|
Project-URL: Homepage, https://github.com/rowansci/rowan-client
|
|
7
6
|
Project-URL: Bug Tracker, https://github.com/rowansci/rowan-client/issues
|
|
7
|
+
Author-email: Corin Wagen <corin@rowansci.com>
|
|
8
|
+
License-File: LICENSE
|
|
8
9
|
Requires-Python: >=3.8
|
|
9
10
|
Description-Content-Type: text/markdown
|
|
10
|
-
License-File: LICENSE
|
|
11
|
-
Requires-Dist: cctk >=0.2.18
|
|
12
|
-
Requires-Dist: httpx >=0.25
|
|
13
|
-
Requires-Dist: numpy >=1.24
|
|
14
|
-
Requires-Dist: stjames >=0.0.23
|
|
15
11
|
|
|
16
12
|
# Rowan Python Library
|
|
17
13
|
|
|
18
14
|
[](https://pypi.python.org/pypi/rowan-python)
|
|
19
|
-
[](https://pixi.sh)
|
|
16
|
+
[](https://github.com/charliermarsh/ruff)
|
|
20
17
|
|
|
21
18
|
The Rowan Python library provides convenient access to the Rowan API from applications written in the Python language.
|
|
22
19
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
rowan/__init__.py,sha256=9JmdQXDkbru5WDq4Slt-XWH2OP4888rYyhIoLQfKqGw,183
|
|
2
|
+
rowan/calculation.py,sha256=fdWbVF97bZ2BQOBEBERLmU8DpeFUrwkZttWDSMYqwk8,312
|
|
3
|
+
rowan/client.py,sha256=GQYwqZ3pZv7vH3QvfHKEWqLPklbr1QHbZtjKRpRmiIs,1282
|
|
4
|
+
rowan/constants.py,sha256=ZZvv3L0b2y3dMGlWGeaRmx40J5tBrpxNxvJgjP1TNjg,37
|
|
5
|
+
rowan/folder.py,sha256=W7-YnPxugqzIdw-t1sr-WjeSQa-x4IjZ2mV2DwIq3II,2965
|
|
6
|
+
rowan/utils.py,sha256=IMACnRJpjFns_DF-FZQDu8p8fbgu4C2dbDaxdGcSZQs,1405
|
|
7
|
+
rowan/workflow.py,sha256=An3CW9LlHxYByE4mRl1iYThYcIGry8TwTi5rgbAsEBc,4467
|
|
8
|
+
rowan/rowan_rdkit/__init__.py,sha256=31bFO_UzagQKXQlybrPc2wdFJ6lUGMmDzDqT-D6b0Fk,300
|
|
9
|
+
rowan/rowan_rdkit/chem_utils.py,sha256=3MRMuwi6PIr_xkKmswfMlB7P7qdJgSQs-Q3YVhGh4LY,22438
|
|
10
|
+
rowan_python-1.1.0.dist-info/METADATA,sha256=CKyKYywsk9M48GAZ6Rjk_Z7qjb6YpDkSti9AuOzOXl8,1030
|
|
11
|
+
rowan_python-1.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
12
|
+
rowan_python-1.1.0.dist-info/licenses/LICENSE,sha256=i7ehYBS-6gGmbTcgU4mgk28pyOx2kScJ0kcx8n7bWLM,1084
|
|
13
|
+
rowan_python-1.1.0.dist-info/RECORD,,
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
rowan/__init__.py,sha256=0O7Cmo2Io01Uqqx9iL8xcWW0t0YfgwBX7RKxDPXnbVU,61
|
|
2
|
-
rowan/client.py,sha256=HoJ7DWxpeHyMdnokKK9M3jTCqEGcwIwHmpa5EFkHg0g,5591
|
|
3
|
-
rowan/utils.py,sha256=AVldYWowm7g1Ffs7JhJY7X85goNuFHvMUel0zXguDrk,932
|
|
4
|
-
rowan_python-0.0.5.dist-info/LICENSE,sha256=i7ehYBS-6gGmbTcgU4mgk28pyOx2kScJ0kcx8n7bWLM,1084
|
|
5
|
-
rowan_python-0.0.5.dist-info/METADATA,sha256=Adimw59X6Bd1-Kp_B9qM-hEWwHtxame5-epgi1MG0ec,1067
|
|
6
|
-
rowan_python-0.0.5.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
7
|
-
rowan_python-0.0.5.dist-info/top_level.txt,sha256=0B0BJ1GvTwD5rxpsHctrermP7PVk4SFaQb2syJpGQl8,6
|
|
8
|
-
rowan_python-0.0.5.dist-info/RECORD,,
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
rowan
|
|
File without changes
|