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 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
- with api_client() as client:
66
- response = client.get(f"/calculation/{self.uuid}/stjames")
67
- response.raise_for_status()
68
- data = response.json()
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
- with api_client() as client:
99
- response = client.get(f"/calculation/{uuid}/stjames")
100
- response.raise_for_status()
101
- data = response.json()
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
- name: str, code: str, project_uuid: str | Project | None = None
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
- # Deserialize the final JSON response into a Protein object and return it.
376
- return Protein(**final_response.json())
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=30,
80
+ timeout=120,
81
+ event_hooks={"response": [_raise_for_status]},
62
82
  ) as client:
63
83
  yield client
64
84
 
@@ -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
- response = client.post("/workflow", json=data)
704
- response.raise_for_status()
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=settings.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())
@@ -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
- "executable": executable,
197
- "exhaustiveness": exhaustiveness,
198
- "max_poses": max_poses,
199
- "scoring_function": scoring_function,
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": workflow.model_dump(serialize_as_any=True, mode="json"),
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. Use ``"alpb"`` or ``"gbsa"`` for xTB methods
72
- (the defaults ``gfn1_xtb`` / ``gfn2_xtb``); use ``"cpcm"`` or ``"smd"`` for
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 if hasattr(f, "value") else str(f) for f in fmts]
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
- if format is not None and format not in self._output_formats:
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 '{format}' was not requested. Available formats: {self._output_formats}"
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
- fmt_str: str = format.value if isinstance(format, stjames.MSAFormat) else format # type: ignore[assignment]
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 ("colabfold", "chai", "boltz").
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
- # Convert to stjames types
106
- protein_sequences = []
107
- for seq in initial_protein_sequences:
108
- if isinstance(seq, stjames.ProteinSequence):
109
- protein_sequences.append(seq)
110
- else:
111
- protein_sequences.append(stjames.ProteinSequence(sequence=seq))
112
-
113
- msa_formats = []
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 and solvent support
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 and input type don't match, or solvent is unsupported.
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)."""
@@ -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],
@@ -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
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.208
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=UhyjCkQh7YDTtemmetfelGD1d36WSJ6oaWct7CP2eyQ,1331
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=lZZ52DxPsuJWCTzFZXjhauHK6dV0KCUwzoxtmoxSY48,3442
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=eyKdZjHR-LsNK0yS8iedb0vPi3OB10gluuA1-Ee_JCg,9202
7
- rowan/molecule.py,sha256=LyneBgSbNS7ByQ2sI8hcRLOXwljZMZQtALgAYcqX2tM,10844
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=hfDqk_LgU8o39Zg2kHHWf8EqPcQOnT3Mo43Ik1gAO68,14856
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=c1s6Ze-OqLtfvrD23OV60otskejmj-CD88nNf8_nFcw,3636
14
- rowan/workflows/__init__.py,sha256=ohl2q-Nqgs4E9cl5KIGigQgE2ifiTmY5dib5R_qNxO4,4510
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=UI5KoraDCR9GX1eRBMEYCEb3mV737xP_5T0M_gfGqag,32176
18
- rowan/workflows/basic_calculation.py,sha256=nAQGwpZnlR9Bi_p4U79_NO6IbcY3gOXQxMpubNHB9AY,11249
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=iEljm5N9A9fJrqpX2_0YznGpN9z3__UDvYoP-5t8n3c,7914
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=MKfjbPkZ7EBQhDf0R4o79Np4ZuRPshhJJnqW6kQmF4o,5171
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=V3B1SyWPR8MT306hh9W-T9JTpi_E-XgAIeF9yRQZ7tI,5075
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=xQPaOHxPQqCY_zi97LW9brpXPn3xjyeF9RDE1Rz8HQk,8772
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=XJIfvn-H7GA6lVtw9uKjlgznVSSsr9bJiDt_PjKlPbA,11572
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=41S-GIHP91ahW5mIVxjAkpZbxept98IgausM5JoeckA,9276
43
- rowan/workflows/rbfe_graph.py,sha256=7UJA3ZBbvtaB9l1KQi9279Pc89moVnBHO-XJpxEr5uQ,7238
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.3.dist-info/METADATA,sha256=mv9y4fxN9Ds7WCnd2W-vZL-LdLCNkp_1XxWipE5Q2Yg,2052
53
- rowan_python-3.1.3.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
54
- rowan_python-3.1.3.dist-info/licenses/LICENSE,sha256=i05z7xEhyrg6f8j0lR3XYjShnF-MJGFQ-DnpsZ8yiVI,1084
55
- rowan_python-3.1.3.dist-info/RECORD,,
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,,