rowan-python 3.1.3__py3-none-any.whl → 3.1.5__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 +10 -0
- rowan/calculation.py +16 -8
- rowan/folder.py +120 -1
- rowan/molecule.py +117 -0
- rowan/protein.py +92 -13
- rowan/utils.py +21 -1
- rowan/workflows/__init__.py +5 -0
- rowan/workflows/base.py +22 -3
- rowan/workflows/basic_calculation.py +49 -1
- rowan/workflows/binding_affinity.py +140 -0
- rowan/workflows/docking.py +10 -7
- rowan/workflows/fukui.py +2 -19
- rowan/workflows/msa.py +15 -22
- rowan/workflows/pka.py +2 -6
- rowan/workflows/pose_analysis_md.py +5 -0
- rowan/workflows/protein_md.py +22 -0
- rowan/workflows/rbfe_graph.py +12 -0
- {rowan_python-3.1.3.dist-info → rowan_python-3.1.5.dist-info}/METADATA +2 -2
- {rowan_python-3.1.3.dist-info → rowan_python-3.1.5.dist-info}/RECORD +21 -20
- {rowan_python-3.1.3.dist-info → rowan_python-3.1.5.dist-info}/WHEEL +0 -0
- {rowan_python-3.1.3.dist-info → rowan_python-3.1.5.dist-info}/licenses/LICENSE +0 -0
rowan/__init__.py
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
from . import constants
|
|
3
3
|
from stjames import (
|
|
4
4
|
Atom,
|
|
5
|
+
BandStructure,
|
|
6
|
+
BindingPoseContact,
|
|
7
|
+
HydrationBridgeResidue,
|
|
8
|
+
HydrationSite,
|
|
9
|
+
ConformerGenSettings,
|
|
5
10
|
Constraint,
|
|
6
11
|
ConstraintType,
|
|
7
12
|
ConformerClusteringSettings,
|
|
@@ -14,15 +19,20 @@ from stjames import (
|
|
|
14
19
|
KMeansClusteringSettings,
|
|
15
20
|
Method,
|
|
16
21
|
Mode,
|
|
22
|
+
MSAFormat,
|
|
17
23
|
MultiStageOptSettings,
|
|
18
24
|
OpenConfSettings,
|
|
19
25
|
OptimizationSettings,
|
|
20
26
|
PBCDFTSettings,
|
|
21
27
|
PeriodicCell,
|
|
28
|
+
ProteinSequence,
|
|
29
|
+
ScanSettings,
|
|
22
30
|
Settings,
|
|
23
31
|
Solvent,
|
|
24
32
|
SolventSettings,
|
|
33
|
+
SinglePointEnergySettings,
|
|
25
34
|
Task,
|
|
35
|
+
VibrationalMode,
|
|
26
36
|
)
|
|
27
37
|
from stjames.workflows.relative_binding_free_energy_perturbation import RBFEGraph, RBFEGraphEdge
|
|
28
38
|
from stjames.excited_state_settings import TDDFTSettings
|
rowan/calculation.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from typing import Self
|
|
4
4
|
|
|
5
|
+
import httpx
|
|
5
6
|
import stjames
|
|
6
7
|
from pydantic import BaseModel, ConfigDict, Field
|
|
7
8
|
|
|
@@ -62,10 +63,13 @@ class Calculation(BaseModel):
|
|
|
62
63
|
:param in_place: If True, update this instance in-place. If False, return new instance.
|
|
63
64
|
:returns: Updated Calculation object.
|
|
64
65
|
"""
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
try:
|
|
67
|
+
with api_client() as client:
|
|
68
|
+
response = client.get(f"/calculation/{self.uuid}/stjames")
|
|
69
|
+
response.raise_for_status()
|
|
70
|
+
data = response.json()
|
|
71
|
+
except httpx.TimeoutException as e:
|
|
72
|
+
raise TimeoutError(f"Timed out fetching calculation {self.uuid}.") from e
|
|
69
73
|
|
|
70
74
|
molecules = _parse_molecules(data.get("molecules", []))
|
|
71
75
|
|
|
@@ -94,11 +98,15 @@ def retrieve_calculation(uuid: str) -> Calculation:
|
|
|
94
98
|
:param uuid: UUID of the calculation to retrieve.
|
|
95
99
|
:returns: Calculation object with the fetched data.
|
|
96
100
|
:raises requests.HTTPError: If the API request fails.
|
|
101
|
+
:raises TimeoutError: If the response times out.
|
|
97
102
|
"""
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
103
|
+
try:
|
|
104
|
+
with api_client() as client:
|
|
105
|
+
response = client.get(f"/calculation/{uuid}/stjames")
|
|
106
|
+
response.raise_for_status()
|
|
107
|
+
data = response.json()
|
|
108
|
+
except httpx.TimeoutException as e:
|
|
109
|
+
raise TimeoutError(f"Timed out fetching calculation {uuid}.") from e
|
|
102
110
|
|
|
103
111
|
molecules = _parse_molecules(data.get("molecules", []))
|
|
104
112
|
|
rowan/folder.py
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from datetime import datetime
|
|
2
|
-
from typing import Any, Self
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Self
|
|
3
5
|
|
|
4
6
|
from pydantic import BaseModel
|
|
5
7
|
|
|
6
8
|
from .project import default_project, retrieve_project
|
|
7
9
|
from .utils import api_client, get_project_uuid
|
|
8
10
|
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from .workflows.base import Workflow
|
|
13
|
+
|
|
9
14
|
|
|
10
15
|
class Folder(BaseModel):
|
|
11
16
|
"""
|
|
@@ -121,6 +126,77 @@ class Folder(BaseModel):
|
|
|
121
126
|
"""
|
|
122
127
|
print_folder_tree(self.uuid, max_depth, show_uuids)
|
|
123
128
|
|
|
129
|
+
def children(self, size: int = 100) -> list[Folder]:
|
|
130
|
+
"""
|
|
131
|
+
List all child folders directly inside this folder.
|
|
132
|
+
|
|
133
|
+
:param size: Maximum number of child folders to return.
|
|
134
|
+
:returns: List of child Folder objects.
|
|
135
|
+
:raises HTTPError: If the API request fails.
|
|
136
|
+
"""
|
|
137
|
+
return list_folders(parent_uuid=self.uuid, size=size)
|
|
138
|
+
|
|
139
|
+
def workflows(self, size: int = 100) -> list[Workflow]:
|
|
140
|
+
"""
|
|
141
|
+
List all workflows directly inside this folder.
|
|
142
|
+
|
|
143
|
+
:param size: Maximum number of workflows to return.
|
|
144
|
+
:returns: List of Workflow objects.
|
|
145
|
+
:raises HTTPError: If the API request fails.
|
|
146
|
+
"""
|
|
147
|
+
from .workflows.base import list_workflows
|
|
148
|
+
|
|
149
|
+
return list_workflows(parent_uuid=self.uuid, size=size)
|
|
150
|
+
|
|
151
|
+
def contents(self, size: int = 100) -> list[Folder | Workflow]:
|
|
152
|
+
"""
|
|
153
|
+
List everything directly inside this folder, both child folders and workflows.
|
|
154
|
+
|
|
155
|
+
Folders come first, followed by workflows. For a single type, use :func:`children`
|
|
156
|
+
or :func:`workflows`.
|
|
157
|
+
|
|
158
|
+
:param size: Maximum number of items of each type to return.
|
|
159
|
+
:returns: List of Folder and Workflow objects.
|
|
160
|
+
:raises HTTPError: If the API request fails.
|
|
161
|
+
"""
|
|
162
|
+
return [*self.children(size=size), *self.workflows(size=size)]
|
|
163
|
+
|
|
164
|
+
def parent(self) -> Folder | None:
|
|
165
|
+
"""
|
|
166
|
+
Retrieve the parent folder, or None if this is a root folder.
|
|
167
|
+
|
|
168
|
+
:returns: Parent Folder, or None if there is no parent.
|
|
169
|
+
:raises HTTPError: If the API request fails.
|
|
170
|
+
"""
|
|
171
|
+
if self.parent_uuid is None:
|
|
172
|
+
return None
|
|
173
|
+
return retrieve_folder(self.parent_uuid)
|
|
174
|
+
|
|
175
|
+
def __truediv__(self, name: str) -> Folder:
|
|
176
|
+
"""
|
|
177
|
+
Traverse into a child folder by name using the ``/`` operator.
|
|
178
|
+
|
|
179
|
+
Example::
|
|
180
|
+
|
|
181
|
+
root = rowan.root_folder()
|
|
182
|
+
subfolder = root / "CDK2" / "docking"
|
|
183
|
+
|
|
184
|
+
:param name: Exact name of the child folder to navigate into.
|
|
185
|
+
:returns: Child Folder with the given name.
|
|
186
|
+
:raises ValueError: If no child with that name exists, or if multiple children share
|
|
187
|
+
the same name (use :func:`children` and select by UUID to disambiguate).
|
|
188
|
+
"""
|
|
189
|
+
matches = [f for f in self.children(size=200) if f.name == name]
|
|
190
|
+
if not matches:
|
|
191
|
+
raise ValueError(f"No child folder named {name!r} in {self.name!r} ({self.uuid})")
|
|
192
|
+
if len(matches) > 1:
|
|
193
|
+
uuids = ", ".join(f.uuid for f in matches)
|
|
194
|
+
raise ValueError(
|
|
195
|
+
f"Multiple child folders named {name!r} in {self.name!r} ({self.uuid}). "
|
|
196
|
+
f"Use retrieve_folder() with one of these UUIDs to disambiguate: {uuids}"
|
|
197
|
+
)
|
|
198
|
+
return matches[0]
|
|
199
|
+
|
|
124
200
|
|
|
125
201
|
def retrieve_folder(uuid: str) -> Folder:
|
|
126
202
|
"""
|
|
@@ -147,6 +223,9 @@ def list_folders(
|
|
|
147
223
|
"""
|
|
148
224
|
Retrieve a list of folders based on the specified criteria.
|
|
149
225
|
|
|
226
|
+
If no ``parent_uuid`` is given and a project is active (via :func:`set_project` or
|
|
227
|
+
``rowan.project_uuid``), lists folders rooted at that project's root folder.
|
|
228
|
+
|
|
150
229
|
:param parent_uuid: UUID of the parent folder to filter by.
|
|
151
230
|
:param name_contains: Substring to search for in folder names.
|
|
152
231
|
:param public: Filter folders by their public status.
|
|
@@ -156,6 +235,11 @@ def list_folders(
|
|
|
156
235
|
:returns: List of Folder objects that match the search criteria.
|
|
157
236
|
:raises requests.HTTPError: if the request to the API fails.
|
|
158
237
|
"""
|
|
238
|
+
if parent_uuid is None:
|
|
239
|
+
if project_uuid := get_project_uuid():
|
|
240
|
+
parent_uuid = retrieve_project(project_uuid).root_folder_uuid
|
|
241
|
+
else:
|
|
242
|
+
parent_uuid = default_project().root_folder_uuid
|
|
159
243
|
|
|
160
244
|
params: dict[str, Any] = {
|
|
161
245
|
"page": page,
|
|
@@ -189,6 +273,9 @@ def create_folder(
|
|
|
189
273
|
"""
|
|
190
274
|
Create a new folder.
|
|
191
275
|
|
|
276
|
+
If no ``parent_uuid`` is given and a project is active (via :func:`set_project` or
|
|
277
|
+
``rowan.project_uuid``), the folder is created inside that project's root folder.
|
|
278
|
+
|
|
192
279
|
:param name: Name of the folder.
|
|
193
280
|
:param parent_uuid: UUID of the parent folder.
|
|
194
281
|
:param notes: Description of the folder.
|
|
@@ -196,6 +283,12 @@ def create_folder(
|
|
|
196
283
|
:param public: Whether the folder is public.
|
|
197
284
|
:returns: Newly created folder.
|
|
198
285
|
"""
|
|
286
|
+
if parent_uuid is None:
|
|
287
|
+
if project_uuid := get_project_uuid():
|
|
288
|
+
parent_uuid = retrieve_project(project_uuid).root_folder_uuid
|
|
289
|
+
else:
|
|
290
|
+
parent_uuid = default_project().root_folder_uuid
|
|
291
|
+
|
|
199
292
|
data = {
|
|
200
293
|
"name": name,
|
|
201
294
|
"parent_uuid": parent_uuid,
|
|
@@ -210,6 +303,32 @@ def create_folder(
|
|
|
210
303
|
return Folder(**folder_data)
|
|
211
304
|
|
|
212
305
|
|
|
306
|
+
def root_folder() -> Folder:
|
|
307
|
+
"""
|
|
308
|
+
Get the root folder of the active project.
|
|
309
|
+
|
|
310
|
+
The root folder is the top of the folder tree you navigate and store workflows in. Use the
|
|
311
|
+
active project set via :func:`set_project` (or ``rowan.project_uuid``), falling back to the
|
|
312
|
+
default project.
|
|
313
|
+
|
|
314
|
+
Example::
|
|
315
|
+
|
|
316
|
+
root = rowan.root_folder()
|
|
317
|
+
for child in root.children():
|
|
318
|
+
print(child.name)
|
|
319
|
+
batch = root / "CDK2" / "docking"
|
|
320
|
+
|
|
321
|
+
:returns: Root Folder of the active or default project.
|
|
322
|
+
:raises HTTPError: If the API request fails.
|
|
323
|
+
"""
|
|
324
|
+
if project_uuid := get_project_uuid():
|
|
325
|
+
root_uuid = retrieve_project(project_uuid).root_folder_uuid
|
|
326
|
+
else:
|
|
327
|
+
root_uuid = default_project().root_folder_uuid
|
|
328
|
+
assert root_uuid is not None
|
|
329
|
+
return retrieve_folder(root_uuid)
|
|
330
|
+
|
|
331
|
+
|
|
213
332
|
def get_folder(path: str, create: bool = True) -> Folder:
|
|
214
333
|
"""
|
|
215
334
|
Get a folder by name or nested path within the default project.
|
rowan/molecule.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""Molecule class for representing molecular structures and computed properties."""
|
|
2
2
|
|
|
3
|
+
import math
|
|
4
|
+
import random
|
|
3
5
|
from pathlib import Path
|
|
4
6
|
from typing import Any, Self
|
|
5
7
|
|
|
@@ -235,6 +237,50 @@ class Molecule(BaseModel):
|
|
|
235
237
|
"""Nuclear gradient (Hartree/Bohr)."""
|
|
236
238
|
return self._stjames.gradient
|
|
237
239
|
|
|
240
|
+
@property
|
|
241
|
+
def symmetry(self) -> str | int | None:
|
|
242
|
+
"""Space group number (1–230) or point group symbol."""
|
|
243
|
+
return self._stjames.symmetry
|
|
244
|
+
|
|
245
|
+
@property
|
|
246
|
+
def xrd_peaks(self) -> list[tuple[int, int, int, float]] | None:
|
|
247
|
+
"""XRD reflections as (h, k, l, intensity) tuples."""
|
|
248
|
+
return self._stjames.x_ray_diffraction_peaks
|
|
249
|
+
|
|
250
|
+
@property
|
|
251
|
+
def band_structure(self) -> "stjames.BandStructure | None":
|
|
252
|
+
"""Electronic band structure and DOS (periodic systems only)."""
|
|
253
|
+
return self._stjames.band_structure
|
|
254
|
+
|
|
255
|
+
@property
|
|
256
|
+
def band_gap(self) -> float | None:
|
|
257
|
+
"""Band gap (Hartree)."""
|
|
258
|
+
bs = self._stjames.band_structure
|
|
259
|
+
return bs.band_gap if bs is not None else None
|
|
260
|
+
|
|
261
|
+
@property
|
|
262
|
+
def density_of_states(self) -> list[tuple[float, float]] | None:
|
|
263
|
+
"""Total DOS as (energy in Hartree, k-weighted count) pairs (Fermi level = 0)."""
|
|
264
|
+
bs = self._stjames.band_structure
|
|
265
|
+
return bs.total_density_of_states if bs is not None else None
|
|
266
|
+
|
|
267
|
+
@property
|
|
268
|
+
def elastic_tensor(
|
|
269
|
+
self,
|
|
270
|
+
) -> (
|
|
271
|
+
tuple[
|
|
272
|
+
tuple[float, float, float, float, float, float],
|
|
273
|
+
tuple[float, float, float, float, float, float],
|
|
274
|
+
tuple[float, float, float, float, float, float],
|
|
275
|
+
tuple[float, float, float, float, float, float],
|
|
276
|
+
tuple[float, float, float, float, float, float],
|
|
277
|
+
tuple[float, float, float, float, float, float],
|
|
278
|
+
]
|
|
279
|
+
| None
|
|
280
|
+
):
|
|
281
|
+
"""Elastic stiffness matrix in GPa (Voigt order: xx yy zz yz xz xy)."""
|
|
282
|
+
return self._stjames.elastic_tensor
|
|
283
|
+
|
|
238
284
|
# -- Geometric utilities --
|
|
239
285
|
|
|
240
286
|
def distance(self, i: int, j: int) -> float:
|
|
@@ -272,6 +318,77 @@ class Molecule(BaseModel):
|
|
|
272
318
|
"""
|
|
273
319
|
return self._stjames.dihedral(i, j, k, l, degrees=degrees)
|
|
274
320
|
|
|
321
|
+
def perturb(self, stddev: float = 0.005) -> "Molecule":
|
|
322
|
+
"""
|
|
323
|
+
Return a copy with random Gaussian noise added to each atom position.
|
|
324
|
+
|
|
325
|
+
Useful for breaking symmetry before resubmitting an optimization (e.g. when
|
|
326
|
+
a calculation is stuck in a saddle point or converges to an unwanted geometry).
|
|
327
|
+
|
|
328
|
+
:param stddev: standard deviation of the Gaussian displacement (Angstroms)
|
|
329
|
+
:returns: new Molecule with perturbed coordinates
|
|
330
|
+
"""
|
|
331
|
+
|
|
332
|
+
def _gauss() -> float:
|
|
333
|
+
u1 = random.random()
|
|
334
|
+
u2 = random.random()
|
|
335
|
+
return math.sqrt(-2.0 * math.log(u1)) * math.cos(2.0 * math.pi * u2) * stddev
|
|
336
|
+
|
|
337
|
+
new_atoms = [
|
|
338
|
+
stjames.Atom(
|
|
339
|
+
atomic_number=atom.atomic_number,
|
|
340
|
+
position=(
|
|
341
|
+
atom.position[0] + _gauss(),
|
|
342
|
+
atom.position[1] + _gauss(),
|
|
343
|
+
atom.position[2] + _gauss(),
|
|
344
|
+
),
|
|
345
|
+
mass=atom.mass,
|
|
346
|
+
)
|
|
347
|
+
for atom in self._stjames.atoms
|
|
348
|
+
]
|
|
349
|
+
new_mol = self._stjames.model_copy(update={"atoms": new_atoms})
|
|
350
|
+
return Molecule(_stjames=new_mol)
|
|
351
|
+
|
|
352
|
+
def displace_along_mode(self, mode: stjames.VibrationalMode, displacement: float) -> "Molecule":
|
|
353
|
+
"""
|
|
354
|
+
Return a copy with atom positions displaced along a vibrational normal mode.
|
|
355
|
+
|
|
356
|
+
Requires a prior frequency calculation. For a transition state, the imaginary
|
|
357
|
+
mode has a negative frequency and points toward the reactant or product.
|
|
358
|
+
|
|
359
|
+
:param mode: vibrational mode to displace along, from ``vibrational_modes``
|
|
360
|
+
:param displacement: displacement distance along the normalized mode (Angstroms)
|
|
361
|
+
:returns: new Molecule with displaced coordinates
|
|
362
|
+
:raises ValueError: if the mode has no displacements or zero-norm displacements
|
|
363
|
+
"""
|
|
364
|
+
raw = mode.displacements
|
|
365
|
+
if not raw:
|
|
366
|
+
raise ValueError(
|
|
367
|
+
"Vibrational mode has no displacements. "
|
|
368
|
+
"Rerun the calculation with frequencies=True to displace along a mode."
|
|
369
|
+
)
|
|
370
|
+
norm = math.sqrt(sum(x**2 + y**2 + z**2 for x, y, z in raw))
|
|
371
|
+
if norm == 0:
|
|
372
|
+
raise ValueError(
|
|
373
|
+
"Vibrational mode has zero-norm displacements. "
|
|
374
|
+
"This indicates malformed frequency output, try displacing along a different mode."
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
new_atoms = [
|
|
378
|
+
stjames.Atom(
|
|
379
|
+
atomic_number=atom.atomic_number,
|
|
380
|
+
position=(
|
|
381
|
+
atom.position[0] + (dx / norm) * displacement,
|
|
382
|
+
atom.position[1] + (dy / norm) * displacement,
|
|
383
|
+
atom.position[2] + (dz / norm) * displacement,
|
|
384
|
+
),
|
|
385
|
+
mass=atom.mass,
|
|
386
|
+
)
|
|
387
|
+
for atom, (dx, dy, dz) in zip(self._stjames.atoms, raw, strict=True)
|
|
388
|
+
]
|
|
389
|
+
new_mol = self._stjames.model_copy(update={"atoms": new_atoms})
|
|
390
|
+
return Molecule(_stjames=new_mol)
|
|
391
|
+
|
|
275
392
|
|
|
276
393
|
def load_named_ligands(path: Path | str) -> dict[str, Molecule]:
|
|
277
394
|
"""
|
rowan/protein.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import time
|
|
2
|
+
import warnings
|
|
2
3
|
from datetime import datetime
|
|
3
4
|
from pathlib import Path
|
|
4
5
|
from typing import Any, Self
|
|
@@ -39,6 +40,85 @@ class Protein(BaseModel):
|
|
|
39
40
|
def __repr__(self) -> str:
|
|
40
41
|
return f"<Protein name='{self.name}' created_at='{self.created_at}' uuid='{self.uuid}'>"
|
|
41
42
|
|
|
43
|
+
@property
|
|
44
|
+
def chains(self) -> list[str]:
|
|
45
|
+
"""Polymer chain IDs present in this protein."""
|
|
46
|
+
if not self.data:
|
|
47
|
+
return []
|
|
48
|
+
model = self.data["models"][0]
|
|
49
|
+
chain_ids: set[str] = set(model["polymer"].keys())
|
|
50
|
+
for section in ("non_polymer", "water", "branched"):
|
|
51
|
+
for entity in model.get(section, {}).values():
|
|
52
|
+
if chain := entity.get("polymer"):
|
|
53
|
+
chain_ids.add(chain)
|
|
54
|
+
return list(chain_ids)
|
|
55
|
+
|
|
56
|
+
def select_chains(self, chains: list[str]) -> "Protein":
|
|
57
|
+
"""
|
|
58
|
+
Create a new protein record containing only the specified chains.
|
|
59
|
+
|
|
60
|
+
:param chains: Chain IDs to keep (e.g. ``["A"]``).
|
|
61
|
+
:returns: New Protein object with only the selected chains.
|
|
62
|
+
:raises ValueError: If any requested chain is not present.
|
|
63
|
+
:raises requests.HTTPError: If the API request fails.
|
|
64
|
+
"""
|
|
65
|
+
if not self.data:
|
|
66
|
+
raise ValueError("Protein data not loaded — call refresh() first.")
|
|
67
|
+
available = self.chains
|
|
68
|
+
missing = [c for c in chains if c not in available]
|
|
69
|
+
if missing:
|
|
70
|
+
raise ValueError(f"Chain(s) {missing} not found. Available: {available}")
|
|
71
|
+
|
|
72
|
+
chain_set = set(chains)
|
|
73
|
+
model = self.data["models"][0]
|
|
74
|
+
|
|
75
|
+
filtered_polymer = {k: v for k, v in model["polymer"].items() if k in chain_set}
|
|
76
|
+
filtered_non_polymer = {
|
|
77
|
+
k: v for k, v in model["non_polymer"].items() if v.get("polymer") in chain_set
|
|
78
|
+
}
|
|
79
|
+
filtered_water = {k: v for k, v in model["water"].items() if v.get("polymer") in chain_set}
|
|
80
|
+
filtered_branched = {
|
|
81
|
+
k: v for k, v in model["branched"].items() if v.get("polymer") in chain_set
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
valid_atom_ids: set[int] = set()
|
|
85
|
+
for polymer in filtered_polymer.values():
|
|
86
|
+
for residue in polymer["residues"].values():
|
|
87
|
+
valid_atom_ids.update(int(a) for a in residue["atoms"])
|
|
88
|
+
for entity in filtered_non_polymer.values():
|
|
89
|
+
valid_atom_ids.update(int(a) for a in entity.get("atoms", {}))
|
|
90
|
+
for entity in filtered_water.values():
|
|
91
|
+
valid_atom_ids.update(int(a) for a in entity.get("atoms", {}))
|
|
92
|
+
for entity in filtered_branched.values():
|
|
93
|
+
valid_atom_ids.update(int(a) for a in entity.get("atoms", {}))
|
|
94
|
+
|
|
95
|
+
filtered_connections = []
|
|
96
|
+
for row in model.get("connections", []):
|
|
97
|
+
if row[0] not in valid_atom_ids:
|
|
98
|
+
continue
|
|
99
|
+
targets = [t for t in row[1:] if t in valid_atom_ids]
|
|
100
|
+
if targets:
|
|
101
|
+
filtered_connections.append([row[0], *targets])
|
|
102
|
+
|
|
103
|
+
filtered_model = {
|
|
104
|
+
"polymer": filtered_polymer,
|
|
105
|
+
"non_polymer": filtered_non_polymer,
|
|
106
|
+
"water": filtered_water,
|
|
107
|
+
"branched": filtered_branched,
|
|
108
|
+
"connections": filtered_connections,
|
|
109
|
+
}
|
|
110
|
+
filtered_data = {**self.data, "models": [filtered_model]}
|
|
111
|
+
|
|
112
|
+
with api_client() as client:
|
|
113
|
+
payload = {
|
|
114
|
+
"name": f"{self.name} (chains {','.join(chains)})",
|
|
115
|
+
"protein_data": filtered_data,
|
|
116
|
+
"ancestor_uuid": self.uuid,
|
|
117
|
+
}
|
|
118
|
+
response = client.post("/protein", json=payload)
|
|
119
|
+
response.raise_for_status()
|
|
120
|
+
return Protein(**response.json())
|
|
121
|
+
|
|
42
122
|
def refresh(self, in_place: bool = True) -> Self:
|
|
43
123
|
"""
|
|
44
124
|
Loads protein data
|
|
@@ -323,7 +403,6 @@ def upload_protein(
|
|
|
323
403
|
# Step 1: Read the file and post it to the conversion endpoint.
|
|
324
404
|
conversion_payload = {"name": name, "text": file_path.read_text()}
|
|
325
405
|
conversion_response = client.post("/convert/pdb_file_to_protein", json=conversion_payload)
|
|
326
|
-
conversion_response.raise_for_status() # Ensure the request was successful
|
|
327
406
|
|
|
328
407
|
# Extract the JSON data from the conversion response.
|
|
329
408
|
protein_data = conversion_response.json()
|
|
@@ -335,20 +414,19 @@ def upload_protein(
|
|
|
335
414
|
"project_uuid": project_uuid,
|
|
336
415
|
}
|
|
337
416
|
final_response = client.post("/protein", json=creation_payload)
|
|
338
|
-
final_response.raise_for_status()
|
|
339
417
|
|
|
340
418
|
# Deserialize the final JSON response into a Protein object and return it.
|
|
341
419
|
return Protein(**final_response.json())
|
|
342
420
|
|
|
343
421
|
|
|
344
422
|
def create_protein_from_pdb_id(
|
|
345
|
-
|
|
423
|
+
code: str, name: str | None = None, project_uuid: str | Project | None = None
|
|
346
424
|
) -> Protein:
|
|
347
425
|
"""
|
|
348
426
|
Creates a protein from a PDB ID.
|
|
349
427
|
|
|
350
|
-
:param name: Name of the protein to create
|
|
351
428
|
:param code: PDB ID of the protein to create
|
|
429
|
+
:param name: Name of the protein. Defaults to the PDB ID.
|
|
352
430
|
:param project_uuid: UUID of the project to create the protein in
|
|
353
431
|
:returns: Protein object representing the created protein
|
|
354
432
|
:raises requests.HTTPError: if the request to the API fails
|
|
@@ -356,21 +434,22 @@ def create_protein_from_pdb_id(
|
|
|
356
434
|
if isinstance(project_uuid, Project):
|
|
357
435
|
project_uuid = project_uuid.uuid
|
|
358
436
|
with api_client() as client:
|
|
359
|
-
# Step 1: Read the file and post it to the conversion endpoint.
|
|
360
437
|
conversion_response = client.post(f"/convert/pdb_id_to_protein?pdb_id={code}")
|
|
361
|
-
conversion_response.raise_for_status() # Ensure the request was successful
|
|
362
|
-
|
|
363
|
-
# Extract the JSON data from the conversion response.
|
|
364
438
|
protein_data = conversion_response.json()
|
|
365
439
|
|
|
366
|
-
# Step 2: Use the converted data to create the final protein object.
|
|
367
440
|
creation_payload = {
|
|
368
|
-
"name": name,
|
|
441
|
+
"name": name or code,
|
|
369
442
|
"protein_data": protein_data,
|
|
370
443
|
"project_uuid": project_uuid,
|
|
371
444
|
}
|
|
372
445
|
final_response = client.post("/protein", json=creation_payload)
|
|
373
|
-
final_response.raise_for_status()
|
|
374
446
|
|
|
375
|
-
|
|
376
|
-
|
|
447
|
+
protein = Protein(**final_response.json())
|
|
448
|
+
chains = protein.chains
|
|
449
|
+
if len(chains) > 1:
|
|
450
|
+
warnings.warn(
|
|
451
|
+
f"{code} has multiple chains {chains}. Select one with "
|
|
452
|
+
f"protein.select_chains(['{chains[0]}']).",
|
|
453
|
+
stacklevel=2,
|
|
454
|
+
)
|
|
455
|
+
return protein
|
rowan/utils.py
CHANGED
|
@@ -52,13 +52,33 @@ def smiles_to_stjames(smiles: str) -> stjames.Molecule:
|
|
|
52
52
|
return stjames.Molecule.from_smiles(smiles)
|
|
53
53
|
|
|
54
54
|
|
|
55
|
+
def _raise_for_status(response: httpx.Response) -> None:
|
|
56
|
+
"""Response hook that raises HTTPStatusError with the API's detail message if available."""
|
|
57
|
+
try:
|
|
58
|
+
response.raise_for_status()
|
|
59
|
+
except httpx.HTTPStatusError as e:
|
|
60
|
+
try:
|
|
61
|
+
response.read()
|
|
62
|
+
detail = response.json().get("detail")
|
|
63
|
+
except Exception:
|
|
64
|
+
detail = None
|
|
65
|
+
if detail:
|
|
66
|
+
raise httpx.HTTPStatusError(
|
|
67
|
+
f"{e.response.status_code} {detail}",
|
|
68
|
+
request=e.request,
|
|
69
|
+
response=e.response,
|
|
70
|
+
) from None
|
|
71
|
+
raise
|
|
72
|
+
|
|
73
|
+
|
|
55
74
|
@contextmanager
|
|
56
75
|
def api_client() -> Generator[httpx.Client, None, None]:
|
|
57
76
|
"""Wraps `httpx.Client` with Rowan-specific kwargs."""
|
|
58
77
|
with httpx.Client(
|
|
59
78
|
base_url=API_URL,
|
|
60
79
|
headers={"X-API-Key": get_api_key()},
|
|
61
|
-
timeout=
|
|
80
|
+
timeout=120,
|
|
81
|
+
event_hooks={"response": [_raise_for_status]},
|
|
62
82
|
) as client:
|
|
63
83
|
yield client
|
|
64
84
|
|
rowan/workflows/__init__.py
CHANGED
|
@@ -47,6 +47,11 @@ from .bde import (
|
|
|
47
47
|
find_cx_bonds,
|
|
48
48
|
submit_bde_workflow,
|
|
49
49
|
)
|
|
50
|
+
from .binding_affinity import (
|
|
51
|
+
BindingAffinityResult,
|
|
52
|
+
BindingAffinityScore,
|
|
53
|
+
submit_binding_affinity_workflow,
|
|
54
|
+
)
|
|
50
55
|
from .conformer_search import ConformerSearchResult, submit_conformer_search_workflow
|
|
51
56
|
from .descriptors import DescriptorsResult, submit_descriptors_workflow
|
|
52
57
|
from .docking import DockingResult, DockingScore, submit_docking_workflow
|
rowan/workflows/base.py
CHANGED
|
@@ -9,14 +9,16 @@ from datetime import datetime
|
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
from typing import Any, Callable, ClassVar, Self
|
|
11
11
|
|
|
12
|
+
import httpx
|
|
12
13
|
import stjames
|
|
13
14
|
from pydantic import BaseModel, ConfigDict, Field, ValidationError
|
|
14
15
|
from rdkit import Chem
|
|
15
16
|
|
|
16
17
|
from ..folder import Folder
|
|
17
18
|
from ..molecule import Molecule as RowanMolecule
|
|
19
|
+
from ..project import default_project, retrieve_project
|
|
18
20
|
from ..types import SMILES, StructureInput
|
|
19
|
-
from ..utils import api_client
|
|
21
|
+
from ..utils import api_client, get_project_uuid
|
|
20
22
|
|
|
21
23
|
logger = logging.getLogger(__name__)
|
|
22
24
|
logger.setLevel(logging.INFO)
|
|
@@ -648,6 +650,9 @@ def molecule_to_dict(mol: StructureInput | dict[str, Any]) -> dict[str, Any]:
|
|
|
648
650
|
raise TypeError(f"Cannot convert {type(mol)} to molecule dict")
|
|
649
651
|
|
|
650
652
|
|
|
653
|
+
_FEATURE_GATE_DETAIL = "You do not have access to this feature."
|
|
654
|
+
|
|
655
|
+
|
|
651
656
|
def submit_workflow(
|
|
652
657
|
workflow_type: stjames.WORKFLOW_NAME,
|
|
653
658
|
workflow_data: dict[str, Any] | None = None,
|
|
@@ -700,8 +705,16 @@ def submit_workflow(
|
|
|
700
705
|
raise ValueError("You must provide either `initial_smiles` or a valid `initial_molecule`.")
|
|
701
706
|
|
|
702
707
|
with api_client() as client:
|
|
703
|
-
|
|
704
|
-
|
|
708
|
+
try:
|
|
709
|
+
response = client.post("/workflow", json=data)
|
|
710
|
+
except httpx.HTTPStatusError as e:
|
|
711
|
+
if _FEATURE_GATE_DETAIL in str(e):
|
|
712
|
+
raise PermissionError(
|
|
713
|
+
f"{e} Visit https://labs.rowansci.com/account/settings to upgrade your account"
|
|
714
|
+
" or contact us for access. Call rowan.whoami() to see your current"
|
|
715
|
+
" .enabled_workflows and .feature_list."
|
|
716
|
+
) from None
|
|
717
|
+
raise
|
|
705
718
|
return Workflow(**response.json())
|
|
706
719
|
|
|
707
720
|
|
|
@@ -792,6 +805,12 @@ def list_workflows(
|
|
|
792
805
|
:returns: List of Workflow objects that match the search criteria.
|
|
793
806
|
:raises requests.HTTPError: if the request to the API fails.
|
|
794
807
|
"""
|
|
808
|
+
if parent_uuid is None:
|
|
809
|
+
if project_uuid := get_project_uuid():
|
|
810
|
+
parent_uuid = retrieve_project(project_uuid).root_folder_uuid
|
|
811
|
+
else:
|
|
812
|
+
parent_uuid = default_project().root_folder_uuid
|
|
813
|
+
|
|
795
814
|
params: dict[str, Any] = {"page": page, "size": size}
|
|
796
815
|
|
|
797
816
|
if parent_uuid is not None:
|
|
@@ -136,6 +136,54 @@ class BasicCalculationResult(WorkflowResult):
|
|
|
136
136
|
mol = self.molecule
|
|
137
137
|
return mol.frequencies if mol else None
|
|
138
138
|
|
|
139
|
+
@property
|
|
140
|
+
def symmetry(self) -> str | int | None:
|
|
141
|
+
"""Space group number (1–230) or point group symbol."""
|
|
142
|
+
mol = self.molecule
|
|
143
|
+
return mol.symmetry if mol else None
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def xrd_peaks(self) -> list[tuple[int, int, int, float]] | None:
|
|
147
|
+
"""XRD reflections as (h, k, l, intensity) tuples."""
|
|
148
|
+
mol = self.molecule
|
|
149
|
+
return mol.xrd_peaks if mol else None
|
|
150
|
+
|
|
151
|
+
@property
|
|
152
|
+
def band_structure(self) -> stjames.BandStructure | None:
|
|
153
|
+
"""Electronic band structure and DOS (periodic systems only)."""
|
|
154
|
+
mol = self.molecule
|
|
155
|
+
return mol.band_structure if mol else None
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def band_gap(self) -> float | None:
|
|
159
|
+
"""Band gap (Hartree)."""
|
|
160
|
+
mol = self.molecule
|
|
161
|
+
return mol.band_gap if mol else None
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def density_of_states(self) -> list[tuple[float, float]] | None:
|
|
165
|
+
"""Total DOS as (energy in Hartree, k-weighted count) pairs (Fermi level = 0)."""
|
|
166
|
+
mol = self.molecule
|
|
167
|
+
return mol.density_of_states if mol else None
|
|
168
|
+
|
|
169
|
+
@property
|
|
170
|
+
def elastic_tensor(
|
|
171
|
+
self,
|
|
172
|
+
) -> (
|
|
173
|
+
tuple[
|
|
174
|
+
tuple[float, float, float, float, float, float],
|
|
175
|
+
tuple[float, float, float, float, float, float],
|
|
176
|
+
tuple[float, float, float, float, float, float],
|
|
177
|
+
tuple[float, float, float, float, float, float],
|
|
178
|
+
tuple[float, float, float, float, float, float],
|
|
179
|
+
tuple[float, float, float, float, float, float],
|
|
180
|
+
]
|
|
181
|
+
| None
|
|
182
|
+
):
|
|
183
|
+
"""Elastic stiffness matrix in GPa (Voigt order: xx yy zz yz xz xy)."""
|
|
184
|
+
mol = self.molecule
|
|
185
|
+
return mol.elastic_tensor if mol else None
|
|
186
|
+
|
|
139
187
|
|
|
140
188
|
def settings_from_preset(preset: PresetName, **overrides: Any) -> stjames.Settings:
|
|
141
189
|
"""
|
|
@@ -271,7 +319,7 @@ def submit_basic_calculation_workflow(
|
|
|
271
319
|
workflow = stjames.BasicCalculationWorkflow(
|
|
272
320
|
initial_molecule=mol_dict,
|
|
273
321
|
settings=settings,
|
|
274
|
-
tasks=
|
|
322
|
+
tasks=tasks,
|
|
275
323
|
engine=settings.engine,
|
|
276
324
|
)
|
|
277
325
|
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Binding affinity workflow — SQM-based scoring of protein–ligand complexes."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
import stjames
|
|
6
|
+
from stjames import SinglePointEnergySettings
|
|
7
|
+
|
|
8
|
+
from ..folder import Folder
|
|
9
|
+
from ..protein import Protein
|
|
10
|
+
from ..types import StructureInput
|
|
11
|
+
from ..utils import api_client
|
|
12
|
+
from .base import (
|
|
13
|
+
Message,
|
|
14
|
+
Workflow,
|
|
15
|
+
WorkflowResult,
|
|
16
|
+
molecule_to_dict,
|
|
17
|
+
parse_messages,
|
|
18
|
+
register_result,
|
|
19
|
+
require_coordinates,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True, slots=True)
|
|
24
|
+
class BindingAffinityScore:
|
|
25
|
+
"""
|
|
26
|
+
Binding affinity score for a single pose.
|
|
27
|
+
|
|
28
|
+
:param binding_affinity: binding affinity in kcal/mol
|
|
29
|
+
:param strain: strain energy in kcal/mol, or None if not computed
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
binding_affinity: float
|
|
33
|
+
strain: float | None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@register_result("binding_affinity")
|
|
37
|
+
class BindingAffinityResult(WorkflowResult):
|
|
38
|
+
"""Result from a binding affinity workflow."""
|
|
39
|
+
|
|
40
|
+
_stjames_class = stjames.BindingAffinityWorkflow
|
|
41
|
+
|
|
42
|
+
def __repr__(self) -> str:
|
|
43
|
+
n = len(self.scores)
|
|
44
|
+
return f"<BindingAffinityResult scores={n}>"
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def scores(self) -> list[BindingAffinityScore]:
|
|
48
|
+
"""Binding affinity scores for each scored pose."""
|
|
49
|
+
return [
|
|
50
|
+
BindingAffinityScore(
|
|
51
|
+
binding_affinity=r.binding_affinity,
|
|
52
|
+
strain=r.strain,
|
|
53
|
+
)
|
|
54
|
+
for r in (self._workflow.binding_affinity_results or [])
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def messages(self) -> list[Message]:
|
|
59
|
+
"""Any messages or warnings from the workflow."""
|
|
60
|
+
return parse_messages(self._workflow.messages)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def submit_binding_affinity_workflow(
|
|
64
|
+
protein: str | Protein,
|
|
65
|
+
ligand_residue_name: str | None = None,
|
|
66
|
+
ligand_structures: list[StructureInput] | None = None,
|
|
67
|
+
binding_affinity_settings: SinglePointEnergySettings | None = None,
|
|
68
|
+
name: str = "Binding Affinity Workflow",
|
|
69
|
+
folder_uuid: str | None = None,
|
|
70
|
+
folder: Folder | None = None,
|
|
71
|
+
max_credits: int | None = None,
|
|
72
|
+
webhook_url: str | None = None,
|
|
73
|
+
is_draft: bool = False,
|
|
74
|
+
) -> Workflow:
|
|
75
|
+
"""
|
|
76
|
+
Submits a binding affinity workflow to the API.
|
|
77
|
+
|
|
78
|
+
Scores ligand poses using SQM-based energies. Two submission modes:
|
|
79
|
+
|
|
80
|
+
**Mode 1 — holo protein:** protein already contains the bound ligand. Pass
|
|
81
|
+
``ligand_residue_name`` to identify which residue is the ligand vs. the receptor.
|
|
82
|
+
Do not pass ``ligand_structures``.
|
|
83
|
+
|
|
84
|
+
**Mode 2 — apo protein + external poses:** protein has no bound ligand. Pass
|
|
85
|
+
``ligand_structures`` with poses that are already in the protein's coordinate frame.
|
|
86
|
+
Do not pass ``ligand_residue_name``. When scoring multiple ligands, prefer this mode
|
|
87
|
+
over separate per-ligand workflows — all poses share the same pocket geometry.
|
|
88
|
+
|
|
89
|
+
:param protein: protein structure. Can be input as a UUID or a Protein object.
|
|
90
|
+
:param ligand_residue_name: residue name identifying the ligand in a holo protein PDB
|
|
91
|
+
(mode 1 only).
|
|
92
|
+
:param ligand_structures: external ligand poses to score, already in the protein's
|
|
93
|
+
coordinate frame. Must have 3D coordinates (mode 2 only).
|
|
94
|
+
:param binding_affinity_settings: SQM settings controlling geometry optimization and
|
|
95
|
+
energy evaluation. Defaults to PM6-D3H4X/COSMO optimization followed by
|
|
96
|
+
PM6-D3H4X/COSMO2 single-point in water.
|
|
97
|
+
:param name: name of the workflow.
|
|
98
|
+
:param folder_uuid: UUID of the folder to place the workflow in.
|
|
99
|
+
:param folder: Folder object to store the workflow in.
|
|
100
|
+
:param max_credits: maximum number of credits to use for the workflow.
|
|
101
|
+
:param webhook_url: URL that Rowan will POST to when the workflow completes.
|
|
102
|
+
:param is_draft: if True, submit the workflow as a draft without starting execution.
|
|
103
|
+
:returns: Workflow object representing the submitted workflow.
|
|
104
|
+
:raises ValueError: if folder arguments conflict.
|
|
105
|
+
:raises requests.HTTPError: if the request to the API fails.
|
|
106
|
+
"""
|
|
107
|
+
if folder and folder_uuid:
|
|
108
|
+
raise ValueError("Provide either `folder` or `folder_uuid`, not both.")
|
|
109
|
+
if folder:
|
|
110
|
+
folder_uuid = folder.uuid
|
|
111
|
+
if isinstance(protein, Protein):
|
|
112
|
+
protein = protein.uuid
|
|
113
|
+
|
|
114
|
+
mol_dicts: list[dict] = []
|
|
115
|
+
if ligand_structures:
|
|
116
|
+
for mol in ligand_structures:
|
|
117
|
+
require_coordinates(mol)
|
|
118
|
+
mol_dicts.append(molecule_to_dict(mol))
|
|
119
|
+
|
|
120
|
+
workflow = stjames.BindingAffinityWorkflow(
|
|
121
|
+
protein=protein,
|
|
122
|
+
ligand_residue_name=ligand_residue_name,
|
|
123
|
+
ligand_structures=mol_dicts or [],
|
|
124
|
+
binding_affinity_settings=binding_affinity_settings or SinglePointEnergySettings(),
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
data = {
|
|
128
|
+
"workflow_type": "binding_affinity",
|
|
129
|
+
"workflow_data": workflow.model_dump(serialize_as_any=True, mode="json"),
|
|
130
|
+
"name": name,
|
|
131
|
+
"folder_uuid": folder_uuid,
|
|
132
|
+
"max_credits": max_credits,
|
|
133
|
+
"webhook_url": webhook_url,
|
|
134
|
+
"is_draft": is_draft,
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
with api_client() as client:
|
|
138
|
+
response = client.post("/workflow", json=data)
|
|
139
|
+
response.raise_for_status()
|
|
140
|
+
return Workflow(**response.json())
|
rowan/workflows/docking.py
CHANGED
|
@@ -192,12 +192,12 @@ def submit_docking_workflow(
|
|
|
192
192
|
if isinstance(protein, Protein):
|
|
193
193
|
protein = protein.uuid
|
|
194
194
|
|
|
195
|
-
docking_settings =
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
195
|
+
docking_settings = stjames.VinaSettings(
|
|
196
|
+
executable=executable,
|
|
197
|
+
exhaustiveness=exhaustiveness,
|
|
198
|
+
max_poses=max_poses,
|
|
199
|
+
scoring_function=scoring_function,
|
|
200
|
+
)
|
|
201
201
|
|
|
202
202
|
workflow = stjames.DockingWorkflow(
|
|
203
203
|
initial_molecule=mol_dict,
|
|
@@ -209,9 +209,12 @@ def submit_docking_workflow(
|
|
|
209
209
|
docking_settings=docking_settings,
|
|
210
210
|
)
|
|
211
211
|
|
|
212
|
+
workflow_data = workflow.model_dump(serialize_as_any=True, mode="json")
|
|
213
|
+
workflow_data["docking_settings"].setdefault("settings_type", "vina")
|
|
214
|
+
|
|
212
215
|
data = {
|
|
213
216
|
"workflow_type": "docking",
|
|
214
|
-
"workflow_data":
|
|
217
|
+
"workflow_data": workflow_data,
|
|
215
218
|
"initial_molecule": mol_dict,
|
|
216
219
|
"name": name,
|
|
217
220
|
"folder_uuid": folder_uuid,
|
rowan/workflows/fukui.py
CHANGED
|
@@ -68,9 +68,8 @@ def submit_fukui_workflow(
|
|
|
68
68
|
|
|
69
69
|
- ``"solvent"``: solvent name string (e.g. ``"water"``, ``"dichloromethane"``,
|
|
70
70
|
``"dmso"``). See ``rowan.Solvent`` for all valid values.
|
|
71
|
-
- ``"model"``: solvation model.
|
|
72
|
-
|
|
73
|
-
DFT methods.
|
|
71
|
+
- ``"model"``: solvation model (e.g. ``"alpb"``, ``"gbsa"``, ``"cpcmx"`` for xTB;
|
|
72
|
+
``"cpcm"``, ``"pcm"`` for DFT). Must be compatible with the engine for the chosen method.
|
|
74
73
|
|
|
75
74
|
Example: ``solvent_settings={"solvent": "water", "model": "alpb"}``
|
|
76
75
|
:param name: Name of the workflow.
|
|
@@ -80,7 +79,6 @@ def submit_fukui_workflow(
|
|
|
80
79
|
:param webhook_url: URL that Rowan will POST to when the workflow completes.
|
|
81
80
|
:param is_draft: If True, submit the workflow as a draft without starting execution.
|
|
82
81
|
:returns: Workflow object representing the submitted workflow.
|
|
83
|
-
:raises ValueError: If the solvent model is incompatible with the chosen method.
|
|
84
82
|
:raises requests.HTTPError: if the request to the API fails.
|
|
85
83
|
"""
|
|
86
84
|
require_coordinates(initial_molecule)
|
|
@@ -88,21 +86,6 @@ def submit_fukui_workflow(
|
|
|
88
86
|
raise ValueError("Provide either `folder` or `folder_uuid`, not both.")
|
|
89
87
|
if folder:
|
|
90
88
|
folder_uuid = folder.uuid
|
|
91
|
-
if solvent_settings is not None:
|
|
92
|
-
model = solvent_settings.get("model")
|
|
93
|
-
is_xtb = stjames.Method(fukui_method) in stjames.XTB_METHODS
|
|
94
|
-
xtb_models = {"alpb", "gbsa"}
|
|
95
|
-
dft_models = {"pcm", "cpcm", "cosmo", "cpcmx", "smd"}
|
|
96
|
-
if is_xtb and model not in xtb_models:
|
|
97
|
-
raise ValueError(
|
|
98
|
-
f"xTB Fukui methods require 'alpb' or 'gbsa' solvation model, got '{model}'"
|
|
99
|
-
)
|
|
100
|
-
if not is_xtb and model not in dft_models:
|
|
101
|
-
raise ValueError(
|
|
102
|
-
f"DFT Fukui methods require 'cpcm', 'smd', 'pcm', 'cosmo', or 'cpcmx' "
|
|
103
|
-
f"solvation model, got '{model}'"
|
|
104
|
-
)
|
|
105
|
-
|
|
106
89
|
mol_dict = molecule_to_dict(initial_molecule)
|
|
107
90
|
|
|
108
91
|
optimization_settings = stjames.Settings(method=optimization_method)
|
rowan/workflows/msa.py
CHANGED
|
@@ -25,7 +25,7 @@ class MSAResult(WorkflowResult):
|
|
|
25
25
|
def _output_formats(self) -> list[str]:
|
|
26
26
|
"""Output formats requested for the MSA."""
|
|
27
27
|
fmts = getattr(self._workflow, "output_formats", []) or []
|
|
28
|
-
return [f.value
|
|
28
|
+
return [f.value for f in fmts]
|
|
29
29
|
|
|
30
30
|
def download_files(
|
|
31
31
|
self,
|
|
@@ -41,17 +41,16 @@ class MSAResult(WorkflowResult):
|
|
|
41
41
|
:raises ValueError: If the requested format wasn't in the original output_formats.
|
|
42
42
|
:raises HTTPError: If the API request fails.
|
|
43
43
|
"""
|
|
44
|
-
|
|
44
|
+
fmt_str: str | None = format.value if isinstance(format, stjames.MSAFormat) else format
|
|
45
|
+
if fmt_str is not None and fmt_str not in self._output_formats:
|
|
45
46
|
raise ValueError(
|
|
46
|
-
f"Format '{
|
|
47
|
+
f"Format '{fmt_str}' was not requested. Available formats: {self._output_formats}"
|
|
47
48
|
)
|
|
48
49
|
|
|
49
50
|
path = Path(path) if path is not None else Path.cwd()
|
|
50
|
-
|
|
51
51
|
path.mkdir(parents=True, exist_ok=True)
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
formats_to_download = [fmt_str] if format is not None else self._output_formats
|
|
53
|
+
formats_to_download = [fmt_str] if fmt_str is not None else self._output_formats
|
|
55
54
|
downloaded_paths = []
|
|
56
55
|
|
|
57
56
|
for fmt in formats_to_download:
|
|
@@ -84,8 +83,7 @@ def submit_msa_workflow(
|
|
|
84
83
|
Submits a Multiple Sequence Alignment (MSA) workflow to the API.
|
|
85
84
|
|
|
86
85
|
:param initial_protein_sequences: List of protein sequences to align (amino acid strings).
|
|
87
|
-
:param output_formats: Output formats for the MSA files
|
|
88
|
-
Defaults to {"colabfold"}.
|
|
86
|
+
:param output_formats: Output formats for the MSA files. Defaults to {MSAFormat.COLABFOLD}.
|
|
89
87
|
:param name: Name to assign to the workflow.
|
|
90
88
|
:param folder_uuid: UUID of the folder where the workflow will be stored.
|
|
91
89
|
:param folder: Folder object to store the workflow in.
|
|
@@ -102,20 +100,15 @@ def submit_msa_workflow(
|
|
|
102
100
|
if output_formats is None:
|
|
103
101
|
output_formats = {"colabfold"}
|
|
104
102
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
for fmt in output_formats:
|
|
115
|
-
if isinstance(fmt, stjames.MSAFormat):
|
|
116
|
-
msa_formats.append(fmt)
|
|
117
|
-
else:
|
|
118
|
-
msa_formats.append(stjames.MSAFormat(fmt))
|
|
103
|
+
protein_sequences = [
|
|
104
|
+
seq if isinstance(seq, stjames.ProteinSequence) else stjames.ProteinSequence(sequence=seq)
|
|
105
|
+
for seq in initial_protein_sequences
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
msa_formats = [
|
|
109
|
+
fmt if isinstance(fmt, stjames.MSAFormat) else stjames.MSAFormat(fmt)
|
|
110
|
+
for fmt in output_formats
|
|
111
|
+
]
|
|
119
112
|
|
|
120
113
|
workflow = stjames.MSAWorkflow(
|
|
121
114
|
initial_protein_sequences=protein_sequences,
|
rowan/workflows/pka.py
CHANGED
|
@@ -43,10 +43,9 @@ class pKaMicrostate:
|
|
|
43
43
|
uncertainty: float | None = None
|
|
44
44
|
|
|
45
45
|
|
|
46
|
-
# Methods grouped by input type
|
|
46
|
+
# Methods grouped by input type
|
|
47
47
|
_PKA_3D_METHODS = {"aimnet2_wagen2024", "gxtb_wagen2026"}
|
|
48
48
|
_PKA_SMILES_METHODS = {"chemprop_nevolianis2025", "starling"}
|
|
49
|
-
_PKA_WATER_ONLY_METHODS = {"aimnet2_wagen2024", "gxtb_wagen2026", "starling"}
|
|
50
49
|
|
|
51
50
|
|
|
52
51
|
@register_result("pka")
|
|
@@ -163,7 +162,7 @@ def submit_pka_workflow(
|
|
|
163
162
|
:param webhook_url: URL that Rowan will POST to when the workflow completes.
|
|
164
163
|
:param is_draft: If True, submit the workflow as a draft without starting execution.
|
|
165
164
|
:returns: Workflow object representing the submitted workflow.
|
|
166
|
-
:raises ValueError: If method
|
|
165
|
+
:raises ValueError: If method/input type mismatch, or chemprop used with protonate_elements.
|
|
167
166
|
:raises requests.HTTPError: if the request to the API fails.
|
|
168
167
|
"""
|
|
169
168
|
if folder and folder_uuid:
|
|
@@ -187,9 +186,6 @@ def submit_pka_workflow(
|
|
|
187
186
|
raise ValueError(
|
|
188
187
|
f"{method} requires a SMILES string. Provide a SMILES string, not a 3D structure."
|
|
189
188
|
)
|
|
190
|
-
if method in _PKA_WATER_ONLY_METHODS and solvent != "water":
|
|
191
|
-
raise ValueError(f"{method} only supports water as solvent.")
|
|
192
|
-
|
|
193
189
|
if method == "chemprop_nevolianis2025" and protonate_elements:
|
|
194
190
|
raise ValueError(
|
|
195
191
|
"chemprop_nevolianis2025 was only trained on deprotonation data; "
|
|
@@ -73,6 +73,11 @@ class PoseAnalysisMDResult(WorkflowResult):
|
|
|
73
73
|
for t in raw
|
|
74
74
|
]
|
|
75
75
|
|
|
76
|
+
@property
|
|
77
|
+
def hydration_sites(self) -> list[stjames.HydrationSite]:
|
|
78
|
+
"""Hydration sites identified across all trajectories."""
|
|
79
|
+
return self._workflow.hydration_sites or []
|
|
80
|
+
|
|
76
81
|
@property
|
|
77
82
|
def average_rmsds(self) -> list[float | None]:
|
|
78
83
|
"""Average ligand RMSD per trajectory (Angstrom)."""
|
rowan/workflows/protein_md.py
CHANGED
|
@@ -101,6 +101,28 @@ class ProteinMDResult(WorkflowResult):
|
|
|
101
101
|
"""Any messages or warnings from the workflow."""
|
|
102
102
|
return parse_messages(getattr(self._workflow, "messages", None))
|
|
103
103
|
|
|
104
|
+
def get_atom_distances(
|
|
105
|
+
self,
|
|
106
|
+
atom_pairs: list[tuple[int, int]],
|
|
107
|
+
replicate: int = 0,
|
|
108
|
+
) -> list[list[float]]:
|
|
109
|
+
"""
|
|
110
|
+
Fetch interatomic distances over the trajectory for specified atom pairs.
|
|
111
|
+
|
|
112
|
+
:param atom_pairs: List of (atom_i, atom_j) index pairs (0-indexed).
|
|
113
|
+
:param replicate: Trajectory replicate index (default 0).
|
|
114
|
+
:returns: List of distance arrays, one per pair, over all frames (Angstrom).
|
|
115
|
+
:raises HTTPError: If the API request fails.
|
|
116
|
+
"""
|
|
117
|
+
with api_client() as client:
|
|
118
|
+
response = client.post(
|
|
119
|
+
f"/trajectory/{self.workflow_uuid}/atom_trajectories",
|
|
120
|
+
params={"replicate": replicate},
|
|
121
|
+
json=atom_pairs,
|
|
122
|
+
)
|
|
123
|
+
response.raise_for_status()
|
|
124
|
+
return response.json()
|
|
125
|
+
|
|
104
126
|
def download_trajectories(
|
|
105
127
|
self,
|
|
106
128
|
replicates: list[int],
|
rowan/workflows/rbfe_graph.py
CHANGED
|
@@ -97,6 +97,9 @@ def submit_relative_binding_free_energy_graph_workflow(
|
|
|
97
97
|
greedy_k_min_cut: int = 3,
|
|
98
98
|
refine_cutoff: float | None = None,
|
|
99
99
|
seed_graph: RBFEGraph | None = None,
|
|
100
|
+
generate_intermediate_ligands: bool = False,
|
|
101
|
+
intermediate_max_dummy_atoms: int = 25,
|
|
102
|
+
intermediate_min_dummy_atom_improvement: int = 5,
|
|
100
103
|
name: str = "RBFE Graph",
|
|
101
104
|
folder_uuid: str | None = None,
|
|
102
105
|
folder: Folder | None = None,
|
|
@@ -120,6 +123,12 @@ def submit_relative_binding_free_energy_graph_workflow(
|
|
|
120
123
|
:param seed_graph: RBFE graph from a prior run to extend, as returned by a
|
|
121
124
|
completed result's ``graph``. Its existing edges (and any computed results)
|
|
122
125
|
are preserved, and only edges for newly added ligands are built.
|
|
126
|
+
:param generate_intermediate_ligands: Generate virtual intermediate ligands to make
|
|
127
|
+
difficult edges easier.
|
|
128
|
+
:param intermediate_max_dummy_atoms: An edge with more dummy atoms than this is split
|
|
129
|
+
when a generated intermediate brings both resulting legs back to at most this value.
|
|
130
|
+
:param intermediate_min_dummy_atom_improvement: Minimum reduction in the worst leg's
|
|
131
|
+
dummy-atom count required for a generated intermediate to be accepted.
|
|
123
132
|
:param name: Name of the workflow.
|
|
124
133
|
:param folder_uuid: UUID of the folder to place the workflow in.
|
|
125
134
|
:param folder: Folder object to store the workflow in.
|
|
@@ -169,6 +178,9 @@ def submit_relative_binding_free_energy_graph_workflow(
|
|
|
169
178
|
greedy_k_min_cut=greedy_k_min_cut,
|
|
170
179
|
refine_cutoff=refine_cutoff,
|
|
171
180
|
seed_graph=seed_graph,
|
|
181
|
+
generate_intermediate_ligands=generate_intermediate_ligands,
|
|
182
|
+
intermediate_max_dummy_atoms=intermediate_max_dummy_atoms,
|
|
183
|
+
intermediate_min_dummy_atom_improvement=intermediate_min_dummy_atom_improvement,
|
|
172
184
|
)
|
|
173
185
|
|
|
174
186
|
data = {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rowan-python
|
|
3
|
-
Version: 3.1.
|
|
3
|
+
Version: 3.1.5
|
|
4
4
|
Summary: Rowan Python Library
|
|
5
5
|
Project-URL: Homepage, https://github.com/rowansci/rowan-client
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/rowansci/rowan-client/issues
|
|
@@ -11,7 +11,7 @@ Requires-Dist: httpx
|
|
|
11
11
|
Requires-Dist: nest-asyncio
|
|
12
12
|
Requires-Dist: rdkit
|
|
13
13
|
Requires-Dist: setuptools
|
|
14
|
-
Requires-Dist: stjames>=0.0.
|
|
14
|
+
Requires-Dist: stjames>=0.0.217
|
|
15
15
|
Description-Content-Type: text/markdown
|
|
16
16
|
|
|
17
17
|
# Rowan Python Library
|
|
@@ -1,46 +1,47 @@
|
|
|
1
|
-
rowan/__init__.py,sha256=
|
|
1
|
+
rowan/__init__.py,sha256=ui5-72WoXjhvvKU3XqOZAvaz-BXkDT13z1m62fISsvg,1553
|
|
2
2
|
rowan/api_keys.py,sha256=TvG5l5MmQ3Qt8Z3Y7jCA_lcrwEHuI7xHy-KLbIdQ8_A,4793
|
|
3
|
-
rowan/calculation.py,sha256=
|
|
3
|
+
rowan/calculation.py,sha256=nuUtLTtn9D-FCM9TJl3h5euBgIYqa5fr5r_PSNBS7Xo,3809
|
|
4
4
|
rowan/config.py,sha256=TejQKSxnzNKKTNL9-2bCLq6RvAh54oVA5Ivl1p_ZT8Q,20899
|
|
5
5
|
rowan/constants.py,sha256=emCH4m9OL2Hm5E-6mJGM_FgzrK_JrZT-FiKJ6pMNQ4Y,84
|
|
6
|
-
rowan/folder.py,sha256=
|
|
7
|
-
rowan/molecule.py,sha256=
|
|
6
|
+
rowan/folder.py,sha256=RRg68dObluJ2RCTKX3rb7RxziOXOQUYxkfQSU2R5VMk,13708
|
|
7
|
+
rowan/molecule.py,sha256=1l-3YCN_A75Uz_Q-dMWuhkeF8rhD1z2JQM7AENyjKYM,15512
|
|
8
8
|
rowan/project.py,sha256=RtxYE9jv3Yz6fH5I56iDGRG5EbwlOHiSK-HF1uxc0d4,4582
|
|
9
|
-
rowan/protein.py,sha256=
|
|
9
|
+
rowan/protein.py,sha256=T2JAqfncP80yV5gEWYA-TlpNY3WeWaNHPE0Xg5voU24,17968
|
|
10
10
|
rowan/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
11
|
rowan/types.py,sha256=MvcYvEYr6N9RXLal44MqsnMxoWfcBa-VLD_rGsW4HOc,591
|
|
12
12
|
rowan/user.py,sha256=rOCYVhSoRY71fCG_OCSD7cLAWWnAMANWGitF3ogfq30,6212
|
|
13
|
-
rowan/utils.py,sha256=
|
|
14
|
-
rowan/workflows/__init__.py,sha256=
|
|
13
|
+
rowan/utils.py,sha256=DBphY1he8Vl3RNf6CbLp5BSX9RG5EfHzhMs-9TmcE_g,4299
|
|
14
|
+
rowan/workflows/__init__.py,sha256=qQZrfkB0f8B82WyNP6a9YeP6I3Cmfans9HvqD2amw28,4635
|
|
15
15
|
rowan/workflows/admet.py,sha256=jDx1Kkgmp0XaeojPzaCFY_yUwvqtSM7Cezps-93cqPw,2865
|
|
16
16
|
rowan/workflows/analogue_docking.py,sha256=IxVaYD8ITsLFfvZ6N7v4KmvIS25DcUQkv-_tMhEfG_4,9707
|
|
17
|
-
rowan/workflows/base.py,sha256=
|
|
18
|
-
rowan/workflows/basic_calculation.py,sha256=
|
|
17
|
+
rowan/workflows/base.py,sha256=k858otXLuorw4MZ1hvqZI_m0n_38htC7Hy3OMCX1TOA,32961
|
|
18
|
+
rowan/workflows/basic_calculation.py,sha256=EovymT6fV6mxmt4G4kRtiskSPGB7w7KAHqBaGtgQI2Q,12941
|
|
19
19
|
rowan/workflows/batch_docking.py,sha256=ePnb1hcsmnUfffl1-k9HNkOC09RjY9bZS9XcqAGNeFs,3621
|
|
20
20
|
rowan/workflows/bde.py,sha256=1ieYUeu-5Lg637eSjhbRr_6qSIiIcjhsLOzaA4zbg3o,6681
|
|
21
|
+
rowan/workflows/binding_affinity.py,sha256=Pengz5gmhYvRd3P6yTe1p7nLZnDENp_gwDJo2q5kiEg,5089
|
|
21
22
|
rowan/workflows/conformer_search.py,sha256=9_DpRfmeNWIVm68Y95zcHCK5Q0yt9le6W70rMeIQndY,15467
|
|
22
23
|
rowan/workflows/constants.py,sha256=el8jWE9gnGTLNWn5_n_V0H362vIRneOqgy7BOQ8CScg,575
|
|
23
24
|
rowan/workflows/descriptors.py,sha256=QjjpzPDpAO_EhPOK--gle6sGpVHMpdAs_yrvC0oiseY,3092
|
|
24
|
-
rowan/workflows/docking.py,sha256=
|
|
25
|
+
rowan/workflows/docking.py,sha256=VTauRB6knysUsAzGhEhI95F6BQLAuZbNEA468wByohc,8031
|
|
25
26
|
rowan/workflows/double_ended_ts_search.py,sha256=eZNEXhfh5W5j5JcEzKnKMMf9HPbIM6HDLnzoh_vVFXk,8115
|
|
26
27
|
rowan/workflows/electronic_properties.py,sha256=MXbsI4OHyXWt3f4Qc5g7-SNjyyEoYzocWVdcP1H06sY,8436
|
|
27
|
-
rowan/workflows/fukui.py,sha256=
|
|
28
|
+
rowan/workflows/fukui.py,sha256=Lyx3FgSdidzPMj-PpSzOb9uXCZj2S_52p8zBYxBbprg,4412
|
|
28
29
|
rowan/workflows/hydrogen_bond_donor_acceptor_strength.py,sha256=xgFFemIZpIOkgkiYx20TmCexWNxNgRZl4H4t-zfQqUE,5254
|
|
29
30
|
rowan/workflows/interaction_energy_decomposition.py,sha256=wK9Mc9uGmFBG8H03LRRRz3nZzM6p7Z2ol1yiZbUB5ec,6176
|
|
30
31
|
rowan/workflows/ion_mobility.py,sha256=GqVVPRffaBJvygB3mwbMJq2aW7y19J3AnkDx_ZMIRss,4021
|
|
31
32
|
rowan/workflows/irc.py,sha256=Zf1zh-UjTrQnZ4fwQ4jDJKh80nsyMymAPK6qUoOShKI,10075
|
|
32
33
|
rowan/workflows/macropka.py,sha256=IxGwr9LziIwj_x6pXEhJN6SywS_KULziuYoETNqM370,5684
|
|
33
34
|
rowan/workflows/membrane_permeability.py,sha256=wHZ1IPlXwBB343uVAm7EuhedcMPd-Uy4jMqHfXRHwqY,4680
|
|
34
|
-
rowan/workflows/msa.py,sha256=
|
|
35
|
+
rowan/workflows/msa.py,sha256=k_FlxxtdgvmxDN3LyLDEQeFfI8_lyBJz1KFtz3S4oNo,4816
|
|
35
36
|
rowan/workflows/multistage_optimization.py,sha256=04kZPC3SmVOxqEQOwyBrS3DlelM-b8e531T_LuVt3tU,7385
|
|
36
37
|
rowan/workflows/nmr.py,sha256=68be_Xgiy1AykGdIL4fR1pqKllD1nLm3IidsStzKtz4,5521
|
|
37
|
-
rowan/workflows/pka.py,sha256=
|
|
38
|
+
rowan/workflows/pka.py,sha256=NZ-3i-cH_b7YOHj2iq0roXEcZmC8SYapmzMOX6ObRqw,8546
|
|
38
39
|
rowan/workflows/pocket_detection.py,sha256=aGHY0puxekp4c4nsNYHcvKCe1fsetygL04BcSvNFvE8,3864
|
|
39
|
-
rowan/workflows/pose_analysis_md.py,sha256=
|
|
40
|
+
rowan/workflows/pose_analysis_md.py,sha256=5TRsCBFD0QObE5tpmCOxi137xsKMfQrRO6ef8TLrnkk,11767
|
|
40
41
|
rowan/workflows/protein_binder_design.py,sha256=pgywTQDuXHplZYka-61_S6CC4WTPDCwJrbupp825eC4,9281
|
|
41
42
|
rowan/workflows/protein_cofolding.py,sha256=CV13VUHC0NLDDaS1-GGTQ3RySkzEE30xzu1_4oPu6lo,16239
|
|
42
|
-
rowan/workflows/protein_md.py,sha256=
|
|
43
|
-
rowan/workflows/rbfe_graph.py,sha256=
|
|
43
|
+
rowan/workflows/protein_md.py,sha256=zqKEtL514uchgbxsyO4vhokzTU-X4lTFA1BM5O8HvTU,10117
|
|
44
|
+
rowan/workflows/rbfe_graph.py,sha256=0eMfvwtQhyajUC8T-bhdcYNKi6Pkja6x6dHjCnEwxPw,8081
|
|
44
45
|
rowan/workflows/redox_potential.py,sha256=bBeT1K9XGVpjCFZeeuv7Vtr3EYHN2okJ4LqYc3gMV04,5427
|
|
45
46
|
rowan/workflows/relative_binding_free_energy_perturbation.py,sha256=vi5Qj5aDAERtjcHDsgzI6vRV_BZJnJ8boOj5aG6GXzo,13817
|
|
46
47
|
rowan/workflows/scan.py,sha256=R5Y6Md3ohPoFVJ4NYvx3_k3JNwMnVw1CDsNPD9S8sr4,7518
|
|
@@ -49,7 +50,7 @@ rowan/workflows/solvent_dependent_conformers.py,sha256=Z2xESmcM8WcTsR2TSTUa--Rrh
|
|
|
49
50
|
rowan/workflows/spin_states.py,sha256=rjkgie2-XVNIN7O6P93yn6EPv9Ogjy8iTj3ufZIsUgY,9331
|
|
50
51
|
rowan/workflows/strain.py,sha256=kCW_BlX__sdQG1JVbFZuB-57rkpnxa_JCw3h4uKdGDk,6425
|
|
51
52
|
rowan/workflows/tautomer_search.py,sha256=mbRl0ZJ7wibueRF8c8_idhXJ1rtwg7LBt--5QwQ_Cck,5767
|
|
52
|
-
rowan_python-3.1.
|
|
53
|
-
rowan_python-3.1.
|
|
54
|
-
rowan_python-3.1.
|
|
55
|
-
rowan_python-3.1.
|
|
53
|
+
rowan_python-3.1.5.dist-info/METADATA,sha256=g-Ttz5kgCuMcbHCBKoHlsBOJJLEvEdcTtornkx9eIQA,2052
|
|
54
|
+
rowan_python-3.1.5.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
55
|
+
rowan_python-3.1.5.dist-info/licenses/LICENSE,sha256=i05z7xEhyrg6f8j0lR3XYjShnF-MJGFQ-DnpsZ8yiVI,1084
|
|
56
|
+
rowan_python-3.1.5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|