rowan-python 3.1.2__py3-none-any.whl → 3.1.4__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 +6 -0
- rowan/calculation.py +16 -8
- rowan/folder.py +120 -1
- rowan/molecule.py +86 -2
- rowan/protein.py +92 -13
- rowan/utils.py +21 -1
- rowan/workflows/base.py +22 -3
- rowan/workflows/basic_calculation.py +1 -1
- 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/protein_md.py +22 -0
- {rowan_python-3.1.2.dist-info → rowan_python-3.1.4.dist-info}/METADATA +1 -1
- {rowan_python-3.1.2.dist-info → rowan_python-3.1.4.dist-info}/RECORD +17 -17
- {rowan_python-3.1.2.dist-info → rowan_python-3.1.4.dist-info}/WHEEL +0 -0
- {rowan_python-3.1.2.dist-info → rowan_python-3.1.4.dist-info}/licenses/LICENSE +0 -0
rowan/__init__.py
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
from . import constants
|
|
3
3
|
from stjames import (
|
|
4
4
|
Atom,
|
|
5
|
+
BindingPoseContact,
|
|
6
|
+
ConformerGenSettings,
|
|
5
7
|
Constraint,
|
|
6
8
|
ConstraintType,
|
|
7
9
|
ConformerClusteringSettings,
|
|
@@ -14,15 +16,19 @@ from stjames import (
|
|
|
14
16
|
KMeansClusteringSettings,
|
|
15
17
|
Method,
|
|
16
18
|
Mode,
|
|
19
|
+
MSAFormat,
|
|
17
20
|
MultiStageOptSettings,
|
|
18
21
|
OpenConfSettings,
|
|
19
22
|
OptimizationSettings,
|
|
20
23
|
PBCDFTSettings,
|
|
21
24
|
PeriodicCell,
|
|
25
|
+
ProteinSequence,
|
|
26
|
+
ScanSettings,
|
|
22
27
|
Settings,
|
|
23
28
|
Solvent,
|
|
24
29
|
SolventSettings,
|
|
25
30
|
Task,
|
|
31
|
+
VibrationalMode,
|
|
26
32
|
)
|
|
27
33
|
from stjames.workflows.relative_binding_free_energy_perturbation import RBFEGraph, RBFEGraphEdge
|
|
28
34
|
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
|
|
|
@@ -272,6 +274,77 @@ class Molecule(BaseModel):
|
|
|
272
274
|
"""
|
|
273
275
|
return self._stjames.dihedral(i, j, k, l, degrees=degrees)
|
|
274
276
|
|
|
277
|
+
def perturb(self, stddev: float = 0.005) -> "Molecule":
|
|
278
|
+
"""
|
|
279
|
+
Return a copy with random Gaussian noise added to each atom position.
|
|
280
|
+
|
|
281
|
+
Useful for breaking symmetry before resubmitting an optimization (e.g. when
|
|
282
|
+
a calculation is stuck in a saddle point or converges to an unwanted geometry).
|
|
283
|
+
|
|
284
|
+
:param stddev: standard deviation of the Gaussian displacement (Angstroms)
|
|
285
|
+
:returns: new Molecule with perturbed coordinates
|
|
286
|
+
"""
|
|
287
|
+
|
|
288
|
+
def _gauss() -> float:
|
|
289
|
+
u1 = random.random()
|
|
290
|
+
u2 = random.random()
|
|
291
|
+
return math.sqrt(-2.0 * math.log(u1)) * math.cos(2.0 * math.pi * u2) * stddev
|
|
292
|
+
|
|
293
|
+
new_atoms = [
|
|
294
|
+
stjames.Atom(
|
|
295
|
+
atomic_number=atom.atomic_number,
|
|
296
|
+
position=(
|
|
297
|
+
atom.position[0] + _gauss(),
|
|
298
|
+
atom.position[1] + _gauss(),
|
|
299
|
+
atom.position[2] + _gauss(),
|
|
300
|
+
),
|
|
301
|
+
mass=atom.mass,
|
|
302
|
+
)
|
|
303
|
+
for atom in self._stjames.atoms
|
|
304
|
+
]
|
|
305
|
+
new_mol = self._stjames.model_copy(update={"atoms": new_atoms})
|
|
306
|
+
return Molecule(_stjames=new_mol)
|
|
307
|
+
|
|
308
|
+
def displace_along_mode(self, mode: stjames.VibrationalMode, displacement: float) -> "Molecule":
|
|
309
|
+
"""
|
|
310
|
+
Return a copy with atom positions displaced along a vibrational normal mode.
|
|
311
|
+
|
|
312
|
+
Requires a prior frequency calculation. For a transition state, the imaginary
|
|
313
|
+
mode has a negative frequency and points toward the reactant or product.
|
|
314
|
+
|
|
315
|
+
:param mode: vibrational mode to displace along, from ``vibrational_modes``
|
|
316
|
+
:param displacement: displacement distance along the normalized mode (Angstroms)
|
|
317
|
+
:returns: new Molecule with displaced coordinates
|
|
318
|
+
:raises ValueError: if the mode has no displacements or zero-norm displacements
|
|
319
|
+
"""
|
|
320
|
+
raw = mode.displacements
|
|
321
|
+
if not raw:
|
|
322
|
+
raise ValueError(
|
|
323
|
+
"Vibrational mode has no displacements. "
|
|
324
|
+
"Rerun the calculation with frequencies=True to displace along a mode."
|
|
325
|
+
)
|
|
326
|
+
norm = math.sqrt(sum(x**2 + y**2 + z**2 for x, y, z in raw))
|
|
327
|
+
if norm == 0:
|
|
328
|
+
raise ValueError(
|
|
329
|
+
"Vibrational mode has zero-norm displacements. "
|
|
330
|
+
"This indicates malformed frequency output, try displacing along a different mode."
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
new_atoms = [
|
|
334
|
+
stjames.Atom(
|
|
335
|
+
atomic_number=atom.atomic_number,
|
|
336
|
+
position=(
|
|
337
|
+
atom.position[0] + (dx / norm) * displacement,
|
|
338
|
+
atom.position[1] + (dy / norm) * displacement,
|
|
339
|
+
atom.position[2] + (dz / norm) * displacement,
|
|
340
|
+
),
|
|
341
|
+
mass=atom.mass,
|
|
342
|
+
)
|
|
343
|
+
for atom, (dx, dy, dz) in zip(self._stjames.atoms, raw, strict=True)
|
|
344
|
+
]
|
|
345
|
+
new_mol = self._stjames.model_copy(update={"atoms": new_atoms})
|
|
346
|
+
return Molecule(_stjames=new_mol)
|
|
347
|
+
|
|
275
348
|
|
|
276
349
|
def load_named_ligands(path: Path | str) -> dict[str, Molecule]:
|
|
277
350
|
"""
|
|
@@ -280,7 +353,8 @@ def load_named_ligands(path: Path | str) -> dict[str, Molecule]:
|
|
|
280
353
|
Molecule names are read from the title field of each record. Use this when
|
|
281
354
|
ligand identity needs to be preserved - for example, building a ligand dict
|
|
282
355
|
for an RBFE workflow where names are used as keys throughout submission and
|
|
283
|
-
results.
|
|
356
|
+
results. Every record must therefore have a unique name; duplicates raise
|
|
357
|
+
rather than silently collapsing into one ligand.
|
|
284
358
|
|
|
285
359
|
Supported formats (all carry per-molecule name fields):
|
|
286
360
|
- SDF / MOL (``.sdf``, ``.mol``) - name from the title line
|
|
@@ -288,7 +362,8 @@ def load_named_ligands(path: Path | str) -> dict[str, Molecule]:
|
|
|
288
362
|
|
|
289
363
|
:param path: Path to an SDF, MOL, or MOL2 file.
|
|
290
364
|
:returns: Dict mapping ligand name to Molecule, in file order.
|
|
291
|
-
:raises ValueError: If no valid molecules are found
|
|
365
|
+
:raises ValueError: If no valid molecules are found, the format is
|
|
366
|
+
unsupported, or two records share a name.
|
|
292
367
|
"""
|
|
293
368
|
path = Path(path)
|
|
294
369
|
suffix = path.suffix.lower()
|
|
@@ -311,4 +386,13 @@ def load_named_ligands(path: Path | str) -> dict[str, Molecule]:
|
|
|
311
386
|
if not pairs:
|
|
312
387
|
raise ValueError(f"No valid molecules found in {path}")
|
|
313
388
|
|
|
389
|
+
# require every record to have a unique name rather than dropping data
|
|
390
|
+
names = [name for name, _ in pairs]
|
|
391
|
+
duplicates = sorted({name for name in names if names.count(name) > 1})
|
|
392
|
+
if duplicates:
|
|
393
|
+
raise ValueError(
|
|
394
|
+
f"Ligand names must be unique, but {path} repeats: {', '.join(duplicates)}. "
|
|
395
|
+
"Rename the duplicate records before loading."
|
|
396
|
+
)
|
|
397
|
+
|
|
314
398
|
return {name: Molecule(_stjames=stjames.Molecule.from_rdkit(rdkm)) for name, rdkm in pairs}
|
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/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:
|
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; "
|
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],
|
|
@@ -1,45 +1,45 @@
|
|
|
1
|
-
rowan/__init__.py,sha256=
|
|
1
|
+
rowan/__init__.py,sha256=AfjV9IsjgykiU39Eh9vjUP_3at-OSqqhSgNhPxutma0,1456
|
|
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=W6eBuIQAjZs5JiQFmyRnbcbj2Qy2XbZnm_J0WuMslG4,13881
|
|
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=
|
|
13
|
+
rowan/utils.py,sha256=DBphY1he8Vl3RNf6CbLp5BSX9RG5EfHzhMs-9TmcE_g,4299
|
|
14
14
|
rowan/workflows/__init__.py,sha256=ohl2q-Nqgs4E9cl5KIGigQgE2ifiTmY5dib5R_qNxO4,4510
|
|
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=CnAnbRYrnjbxgcPAZxPUARUjaqPCnAAjf-RFku8R1r0,11240
|
|
19
19
|
rowan/workflows/batch_docking.py,sha256=ePnb1hcsmnUfffl1-k9HNkOC09RjY9bZS9XcqAGNeFs,3621
|
|
20
20
|
rowan/workflows/bde.py,sha256=1ieYUeu-5Lg637eSjhbRr_6qSIiIcjhsLOzaA4zbg3o,6681
|
|
21
21
|
rowan/workflows/conformer_search.py,sha256=9_DpRfmeNWIVm68Y95zcHCK5Q0yt9le6W70rMeIQndY,15467
|
|
22
22
|
rowan/workflows/constants.py,sha256=el8jWE9gnGTLNWn5_n_V0H362vIRneOqgy7BOQ8CScg,575
|
|
23
23
|
rowan/workflows/descriptors.py,sha256=QjjpzPDpAO_EhPOK--gle6sGpVHMpdAs_yrvC0oiseY,3092
|
|
24
|
-
rowan/workflows/docking.py,sha256=
|
|
24
|
+
rowan/workflows/docking.py,sha256=VTauRB6knysUsAzGhEhI95F6BQLAuZbNEA468wByohc,8031
|
|
25
25
|
rowan/workflows/double_ended_ts_search.py,sha256=eZNEXhfh5W5j5JcEzKnKMMf9HPbIM6HDLnzoh_vVFXk,8115
|
|
26
26
|
rowan/workflows/electronic_properties.py,sha256=MXbsI4OHyXWt3f4Qc5g7-SNjyyEoYzocWVdcP1H06sY,8436
|
|
27
|
-
rowan/workflows/fukui.py,sha256=
|
|
27
|
+
rowan/workflows/fukui.py,sha256=Lyx3FgSdidzPMj-PpSzOb9uXCZj2S_52p8zBYxBbprg,4412
|
|
28
28
|
rowan/workflows/hydrogen_bond_donor_acceptor_strength.py,sha256=xgFFemIZpIOkgkiYx20TmCexWNxNgRZl4H4t-zfQqUE,5254
|
|
29
29
|
rowan/workflows/interaction_energy_decomposition.py,sha256=wK9Mc9uGmFBG8H03LRRRz3nZzM6p7Z2ol1yiZbUB5ec,6176
|
|
30
30
|
rowan/workflows/ion_mobility.py,sha256=GqVVPRffaBJvygB3mwbMJq2aW7y19J3AnkDx_ZMIRss,4021
|
|
31
31
|
rowan/workflows/irc.py,sha256=Zf1zh-UjTrQnZ4fwQ4jDJKh80nsyMymAPK6qUoOShKI,10075
|
|
32
32
|
rowan/workflows/macropka.py,sha256=IxGwr9LziIwj_x6pXEhJN6SywS_KULziuYoETNqM370,5684
|
|
33
33
|
rowan/workflows/membrane_permeability.py,sha256=wHZ1IPlXwBB343uVAm7EuhedcMPd-Uy4jMqHfXRHwqY,4680
|
|
34
|
-
rowan/workflows/msa.py,sha256=
|
|
34
|
+
rowan/workflows/msa.py,sha256=k_FlxxtdgvmxDN3LyLDEQeFfI8_lyBJz1KFtz3S4oNo,4816
|
|
35
35
|
rowan/workflows/multistage_optimization.py,sha256=04kZPC3SmVOxqEQOwyBrS3DlelM-b8e531T_LuVt3tU,7385
|
|
36
36
|
rowan/workflows/nmr.py,sha256=68be_Xgiy1AykGdIL4fR1pqKllD1nLm3IidsStzKtz4,5521
|
|
37
|
-
rowan/workflows/pka.py,sha256=
|
|
37
|
+
rowan/workflows/pka.py,sha256=NZ-3i-cH_b7YOHj2iq0roXEcZmC8SYapmzMOX6ObRqw,8546
|
|
38
38
|
rowan/workflows/pocket_detection.py,sha256=aGHY0puxekp4c4nsNYHcvKCe1fsetygL04BcSvNFvE8,3864
|
|
39
39
|
rowan/workflows/pose_analysis_md.py,sha256=XJIfvn-H7GA6lVtw9uKjlgznVSSsr9bJiDt_PjKlPbA,11572
|
|
40
40
|
rowan/workflows/protein_binder_design.py,sha256=pgywTQDuXHplZYka-61_S6CC4WTPDCwJrbupp825eC4,9281
|
|
41
41
|
rowan/workflows/protein_cofolding.py,sha256=CV13VUHC0NLDDaS1-GGTQ3RySkzEE30xzu1_4oPu6lo,16239
|
|
42
|
-
rowan/workflows/protein_md.py,sha256=
|
|
42
|
+
rowan/workflows/protein_md.py,sha256=zqKEtL514uchgbxsyO4vhokzTU-X4lTFA1BM5O8HvTU,10117
|
|
43
43
|
rowan/workflows/rbfe_graph.py,sha256=7UJA3ZBbvtaB9l1KQi9279Pc89moVnBHO-XJpxEr5uQ,7238
|
|
44
44
|
rowan/workflows/redox_potential.py,sha256=bBeT1K9XGVpjCFZeeuv7Vtr3EYHN2okJ4LqYc3gMV04,5427
|
|
45
45
|
rowan/workflows/relative_binding_free_energy_perturbation.py,sha256=vi5Qj5aDAERtjcHDsgzI6vRV_BZJnJ8boOj5aG6GXzo,13817
|
|
@@ -49,7 +49,7 @@ rowan/workflows/solvent_dependent_conformers.py,sha256=Z2xESmcM8WcTsR2TSTUa--Rrh
|
|
|
49
49
|
rowan/workflows/spin_states.py,sha256=rjkgie2-XVNIN7O6P93yn6EPv9Ogjy8iTj3ufZIsUgY,9331
|
|
50
50
|
rowan/workflows/strain.py,sha256=kCW_BlX__sdQG1JVbFZuB-57rkpnxa_JCw3h4uKdGDk,6425
|
|
51
51
|
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.
|
|
52
|
+
rowan_python-3.1.4.dist-info/METADATA,sha256=qogpVoI_aoX_24Z2diW8JdOR2sphEYp-HuLtTFvvN-w,2052
|
|
53
|
+
rowan_python-3.1.4.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
54
|
+
rowan_python-3.1.4.dist-info/licenses/LICENSE,sha256=i05z7xEhyrg6f8j0lR3XYjShnF-MJGFQ-DnpsZ8yiVI,1084
|
|
55
|
+
rowan_python-3.1.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|