rowan-python 3.2.2__py3-none-any.whl → 3.2.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 CHANGED
@@ -15,6 +15,7 @@ from stjames import (
15
15
  Constraint,
16
16
  ConstraintType,
17
17
  ConformerClusteringSettings,
18
+ ConformerProperties,
18
19
  ConformerGenSettingsUnion,
19
20
  Correction,
20
21
  DNASequence,
@@ -27,7 +28,9 @@ from stjames import (
27
28
  iMTDSettings,
28
29
  InducedFitSettings,
29
30
  KMeansClusteringSettings,
31
+ MDRestraints,
30
32
  Method,
33
+ MixMDSettings,
31
34
  Mode,
32
35
  MSAFormat,
33
36
  MultiStageOptSettings,
@@ -38,9 +41,11 @@ from stjames import (
38
41
  OptimizationSettings,
39
42
  PBCDFTSettings,
40
43
  PeriodicCell,
44
+ PocketeerSettings,
41
45
  ProteinSequence,
42
46
  ProteinForceField,
43
47
  ResidueModification,
48
+ RestraintType,
44
49
  RNASequence,
45
50
  ScanSettings,
46
51
  Settings,
@@ -67,7 +72,7 @@ from stjames.engine_compatibility import (
67
72
  METHOD_ENGINES,
68
73
  get_supported_corrections,
69
74
  )
70
- from stjames.pdb import ResidueRef
75
+ from stjames.pdb import ResidueAtomRef, ResidueRef
71
76
  from stjames.optimization.band_method import NEBSettings
72
77
  from stjames.optimization.interpolation import Interpolation
73
78
  from stjames.optimization.string_method import FSMSettings, StringMethodSettings
@@ -83,5 +88,6 @@ from .types import ProteinUUID, RdkitMol, SMILES, StJamesMolecule, StructureInpu
83
88
  from .workflows import *
84
89
  from .project import *
85
90
  from .protein import *
91
+ from .spectra import HARTREE_TO_EV, broaden_spectrum
86
92
  from .user import *
87
93
  from .utils import *
rowan/spectra.py ADDED
@@ -0,0 +1,68 @@
1
+ """Broaden stick spectra into continuous line shapes."""
2
+
3
+ import math
4
+ from collections.abc import Sequence
5
+ from typing import Literal
6
+
7
+ HARTREE_TO_EV = 27.211386245988
8
+
9
+ _GAUSSIAN_FWHM_TO_SIGMA = 1 / (2 * math.sqrt(2 * math.log(2)))
10
+
11
+
12
+ def broaden_spectrum(
13
+ positions: Sequence[float],
14
+ intensities: Sequence[float],
15
+ grid: Sequence[float],
16
+ fwhm: float,
17
+ lineshape: Literal["gaussian", "lorentzian"] = "gaussian",
18
+ ) -> list[float]:
19
+ """Sum line shapes centered on each stick, each with area equal to its intensity.
20
+
21
+ Use it on the stick spectra of `ConformerSearchResult.ensemble_properties`, for
22
+ example excitation energies with oscillator or rotatory strengths for UV-vis or ECD,
23
+ or vibrational frequencies with IR intensities. `grid` and `fwhm` share the units of
24
+ `positions`; convert excitation energies from Hartree with `HARTREE_TO_EV` first to
25
+ broaden in eV.
26
+
27
+ Args:
28
+ positions: line positions
29
+ intensities: line intensities, parallel to `positions`
30
+ grid: points at which to evaluate the spectrum
31
+ fwhm: full width at half maximum of each line
32
+ lineshape: line shape of each stick
33
+
34
+ Returns:
35
+ spectrum at each point of `grid`
36
+
37
+ Raises:
38
+ ValueError: `fwhm` is not positive or `intensities` differs in length from `positions`
39
+
40
+ >>> [round(y, 4) for y in broaden_spectrum([0.0], [1.0], [0.0, 0.5], fwhm=1.0)]
41
+ [0.9394, 0.4697]
42
+ >>> [round(y, 4) for y in broaden_spectrum([0.0], [2.0], [0.0], 1.0, "lorentzian")]
43
+ [1.2732]
44
+ """
45
+ if fwhm <= 0:
46
+ raise ValueError(f"fwhm must be positive, got {fwhm}")
47
+ if len(positions) != len(intensities):
48
+ raise ValueError(
49
+ f"positions ({len(positions)}) and intensities ({len(intensities)}) "
50
+ "must have the same length"
51
+ )
52
+
53
+ if lineshape == "gaussian":
54
+ sigma = fwhm * _GAUSSIAN_FWHM_TO_SIGMA
55
+ norm = 1 / (sigma * math.sqrt(2 * math.pi))
56
+
57
+ def shape(dx: float) -> float:
58
+ return norm * math.exp(-0.5 * (dx / sigma) ** 2)
59
+
60
+ else:
61
+ gamma = fwhm / 2
62
+
63
+ def shape(dx: float) -> float:
64
+ return gamma / (math.pi * (dx**2 + gamma**2))
65
+
66
+ return [
67
+ sum(i * shape(x - p) for p, i in zip(positions, intensities, strict=True)) for x in grid
68
+ ]
rowan/user.py CHANGED
@@ -60,7 +60,6 @@ class User(BaseModel):
60
60
  email: email of the user
61
61
  firstname: first name of the user
62
62
  lastname: last name of the user
63
- weekly_credits: weekly credits of the user
64
63
  credits: credits of the user
65
64
  billing_name: billing name of the user
66
65
  billing_address: billing address of the user
@@ -79,7 +78,6 @@ class User(BaseModel):
79
78
  email: str
80
79
  firstname: str | None = None
81
80
  lastname: str | None = None
82
- weekly_credits: float | None = None
83
81
  credits: float | None = None
84
82
  billing_name: str | None = None
85
83
  billing_address: str | dict | None = None
@@ -124,7 +122,7 @@ class User(BaseModel):
124
122
  Returns:
125
123
  string showing available credits
126
124
  """
127
- individual_credits = f"Weekly Credits: {self.weekly_credits}\nCredits: {self.credits}"
125
+ individual_credits = f"Credits: {self.credits}"
128
126
  if self.organization is not None:
129
127
  return (
130
128
  individual_credits
rowan/utils.py CHANGED
@@ -4,7 +4,7 @@ from contextvars import ContextVar
4
4
  from dataclasses import dataclass
5
5
  from pathlib import Path
6
6
  from tempfile import NamedTemporaryFile
7
- from typing import Any, Generator, Literal
7
+ from typing import Any, Callable, ContextManager, Generator, Literal
8
8
 
9
9
  import httpx
10
10
  import stjames
@@ -167,24 +167,14 @@ def api_client() -> Generator[httpx.Client, None, None]:
167
167
  yield client
168
168
 
169
169
 
170
- def download_file(
171
- path: Path,
172
- method: Literal["GET", "POST"],
173
- url: str,
174
- *,
175
- params: dict[str, Any] | None = None,
176
- json: Any = None,
177
- ) -> Path:
178
- """Stream an authenticated API response into one local file atomically."""
170
+ def _stream_to_file(path: Path, stream: Callable[[], ContextManager[httpx.Response]]) -> Path:
171
+ """Write a streamed response into one local file atomically."""
179
172
  path.parent.mkdir(parents=True, exist_ok=True)
180
173
  temporary_path: Path | None = None
181
174
  try:
182
175
  with NamedTemporaryFile(dir=path.parent, prefix=f".{path.name}.", delete=False) as output:
183
176
  temporary_path = Path(output.name)
184
- with (
185
- api_client() as client,
186
- client.stream(method, url, params=params, json=json) as response,
187
- ):
177
+ with stream() as response:
188
178
  response.raise_for_status()
189
179
  for chunk in response.iter_bytes():
190
180
  output.write(chunk)
@@ -196,6 +186,65 @@ def download_file(
196
186
  return path
197
187
 
198
188
 
189
+ def download_file(
190
+ path: Path,
191
+ method: Literal["GET", "POST"],
192
+ url: str,
193
+ *,
194
+ params: dict[str, Any] | None = None,
195
+ json: Any = None,
196
+ ) -> Path:
197
+ """Stream an authenticated API response into one local file atomically."""
198
+
199
+ @contextmanager
200
+ def stream() -> Generator[httpx.Response, None, None]:
201
+ with (
202
+ api_client() as client,
203
+ client.stream(method, url, params=params, json=json) as response,
204
+ ):
205
+ yield response
206
+
207
+ return _stream_to_file(path, stream)
208
+
209
+
210
+ def download_presigned_files(
211
+ directory: Path,
212
+ url: str,
213
+ *,
214
+ params: dict[str, Any] | None = None,
215
+ json: Any = None,
216
+ ) -> list[Path]:
217
+ """Fetch presigned download links from the API and stream each file into `directory`.
218
+
219
+ Links are short-lived, so an expired one is refreshed once before giving up.
220
+ """
221
+
222
+ def fetch_links() -> list[dict[str, str]]:
223
+ with api_client() as client:
224
+ return client.post(url, params=params, json=json).json()
225
+
226
+ def download(link: dict[str, str]) -> Path:
227
+ # Plain client: the API key must never be sent to S3.
228
+ @contextmanager
229
+ def stream() -> Generator[httpx.Response, None, None]:
230
+ with httpx.Client(timeout=httpx.Timeout(120, read=None)) as client:
231
+ with client.stream("GET", link["url"]) as response:
232
+ yield response
233
+
234
+ return _stream_to_file(directory / Path(link["filename"]).name, stream)
235
+
236
+ links = fetch_links()
237
+ paths = []
238
+ for i, link in enumerate(links):
239
+ try:
240
+ paths.append(download(link))
241
+ except httpx.HTTPStatusError as error:
242
+ if error.response.status_code != 403:
243
+ raise
244
+ paths.append(download(fetch_links()[i]))
245
+ return paths
246
+
247
+
199
248
  ATOMIC_NUMBER_TO_ATOMIC_SYMBOL = {
200
249
  "1": "H",
201
250
  "2": "He",
@@ -102,7 +102,14 @@ from .multistage_optimization import (
102
102
  )
103
103
  from .nmr import NMRCoupling, NMRPeak, NMRResult, submit_nmr_workflow
104
104
  from .pka import pKaMicrostate, pKaResult, submit_pka_workflow
105
- from .pocket_detection import Pocket, PocketDetectionResult, submit_pocket_detection_workflow
105
+ from .pocket_detection import (
106
+ MixMDPocketDiagnostics,
107
+ MixMDReplicaDiagnostics,
108
+ MixMDResults,
109
+ Pocket,
110
+ PocketDetectionResult,
111
+ submit_pocket_detection_workflow,
112
+ )
106
113
  from .pose_analysis_md import PoseAnalysisMDResult, submit_pose_analysis_md_workflow
107
114
  from .protein_binder_design import (
108
115
  ProteinBinder,
@@ -8,7 +8,7 @@ from typing import Literal, Protocol
8
8
  from stjames.pdb import PDB
9
9
 
10
10
  from rowan.protein import Protein, retrieve_protein
11
- from rowan.utils import api_client, download_file
11
+ from rowan.utils import api_client, download_file, download_presigned_files
12
12
 
13
13
  from .base import WorkflowResult
14
14
 
@@ -222,7 +222,9 @@ class _MolecularDynamicsResult(WorkflowResult):
222
222
  name: str | None = None,
223
223
  path: Path | str | None = None,
224
224
  ) -> Path:
225
- """Download DCD trajectory files for selected replicates.
225
+ """Download DCD trajectory files for selected replicates as one `.tar.gz` archive.
226
+
227
+ Long trajectories can time out; use `download_trajectory_files` for those.
226
228
 
227
229
  Args:
228
230
  replicates: zero-based replicate indices
@@ -244,6 +246,29 @@ class _MolecularDynamicsResult(WorkflowResult):
244
246
  json=replicates,
245
247
  )
246
248
 
249
+ def download_trajectory_files(
250
+ self,
251
+ replicates: list[int],
252
+ path: Path | str | None = None,
253
+ ) -> list[Path]:
254
+ """Download each selected replicate's DCD file directly from storage.
255
+
256
+ Args:
257
+ replicates: zero-based replicate indices
258
+ path: output directory; defaults to the current directory
259
+
260
+ Returns:
261
+ paths to the downloaded `.dcd` files, one per replicate
262
+
263
+ Raises:
264
+ httpx.HTTPStatusError: API or storage request fails
265
+ """
266
+ return download_presigned_files(
267
+ Path(path) if path is not None else Path.cwd(),
268
+ f"/trajectory/{self.workflow_uuid}/trajectory_dcd_urls",
269
+ json=replicates,
270
+ )
271
+
247
272
  def _trajectory(self, replicate: int) -> _MolecularDynamicsTrajectory:
248
273
  """Return one trajectory with a consistent bounds error."""
249
274
  trajectories = self.trajectories
@@ -31,6 +31,8 @@ from .base import (
31
31
  )
32
32
  from .constants import to_relative_kcal
33
33
 
34
+ ConformerProperties = stjames.ConformerProperties
35
+
34
36
 
35
37
  @register_result("conformer_search")
36
38
  class ConformerSearchResult(WorkflowResult):
@@ -72,22 +74,59 @@ class ConformerSearchResult(WorkflowResult):
72
74
  return to_relative_kcal(energies) if relative else energies
73
75
 
74
76
  @property
75
- def radii_of_gyration(self) -> list[float]:
76
- """Radius of gyration for each conformer (A)."""
77
+ def radii_of_gyration(self) -> list[float | None]:
78
+ """Radius of gyration for each conformer (A), None where it failed."""
77
79
  return [p.radius_of_gyration for p in self._workflow.conformer_properties]
78
80
 
79
81
  @property
80
- def sasa(self) -> list[float]:
81
- """Solvent accessible surface area for each conformer (A^2)."""
82
+ def sasa(self) -> list[float | None]:
83
+ """Solvent accessible surface area for each conformer (A^2), None where it failed."""
82
84
  return [p.solvent_accessible_surface_area for p in self._workflow.conformer_properties]
83
85
 
84
86
  @property
85
- def polar_sasa(self) -> list[float]:
86
- """Polar solvent accessible surface area for each conformer (A^2)."""
87
+ def polar_sasa(self) -> list[float | None]:
88
+ """Polar solvent accessible surface area for each conformer (A^2), None where it failed."""
87
89
  return [
88
90
  p.polar_solvent_accessible_surface_area for p in self._workflow.conformer_properties
89
91
  ]
90
92
 
93
+ @property
94
+ def dipoles(self) -> list[float | None]:
95
+ """Dipole moment magnitude about the center of mass for each conformer (Debye).
96
+
97
+ None where the method reports no dipole.
98
+ """
99
+ return [p.dipole for p in self._workflow.conformer_properties]
100
+
101
+ @property
102
+ def ensemble_properties(self) -> ConformerProperties | None:
103
+ """Ensemble properties weighted by `boltzmann_weights`.
104
+
105
+ Each property is averaged over the conformers that have it: SASA, polar SASA, Rg,
106
+ the root-mean-square dipole (Debye), and per-atom Mulliken charges. When the run
107
+ computed them, it also holds stick spectra with every conformer's lines, each
108
+ intensity scaled by its conformer's population: `excitation_energies` (Hartree) with
109
+ `oscillator_strengths` (UV-vis) and `rotatory_strengths` (ECD), and
110
+ `vibrational_frequencies` (cm^-1) with `ir_intensities` (km/mol). Pass them to
111
+ `rowan.broaden_spectrum` for the spectrum. Rotatory strengths are omitted when the
112
+ conformers differ in stereochemistry, since mixing enantiomers cancels the ECD signal.
113
+ """
114
+ return self._workflow.ensemble_properties
115
+
116
+ @property
117
+ def boltzmann_weights(self) -> list[float]:
118
+ """Population of each conformer at the weighting temperature (sum to 1.0).
119
+
120
+ From free energies when every conformer has frequencies, else from ground-state
121
+ energies. Empty for workflows run before weights were reported.
122
+ """
123
+ return list(self._workflow.boltzmann_weights)
124
+
125
+ @property
126
+ def weighting_temperature(self) -> float:
127
+ """Temperature of `boltzmann_weights` and `ensemble_properties` (K)."""
128
+ return self._workflow.weighting_temperature
129
+
91
130
  def get_conformers(self, n: int | None = None) -> list[Molecule]:
92
131
  """Fetch conformer molecules.
93
132
 
@@ -156,6 +195,7 @@ def _mso_for_final_method(
156
195
  final_method: Method,
157
196
  solvent: SolventInput = None,
158
197
  transition_state: bool = False,
198
+ frequencies: bool = False,
159
199
  ) -> MultiStageOptSettings:
160
200
  """Build optimization settings for the final method.
161
201
 
@@ -163,9 +203,9 @@ def _mso_for_final_method(
163
203
  without a named preset.
164
204
 
165
205
  `solvent` and `transition_state` are merged into each stage's `Settings`
166
- when supplied; the named preset itself is independent of them (mirrors how
167
- tinbergen's form keeps the preset dropdown separate from the solvent / TS
168
- toggles).
206
+ when supplied, and `frequencies` runs on the optimization stage; the named
207
+ preset itself is independent of them (mirrors how tinbergen's form keeps the
208
+ preset dropdown separate from the solvent / TS toggles).
169
209
  """
170
210
  opt_method, sp_method = _FINAL_METHOD_TO_MSO_RECIPE.get(final_method, (final_method, None))
171
211
 
@@ -196,6 +236,7 @@ def _mso_for_final_method(
196
236
  return MultiStageOptSettings(
197
237
  optimization_settings=[opt_stage],
198
238
  singlepoint_settings=sp_stage,
239
+ frequencies=frequencies,
199
240
  )
200
241
 
201
242
 
@@ -224,6 +265,8 @@ def submit_conformer_search_workflow(
224
265
  final_method: Method | str = "aimnet2_wb97md3",
225
266
  solvent: SolventInput = None,
226
267
  transition_state: bool = False,
268
+ frequencies: bool = False,
269
+ weighting_temperature: float = 298.15,
227
270
  multistage_opt_settings: MultiStageOptSettings | None = None,
228
271
  conformer_clustering_settings: ConformerClusteringSettings | None = None,
229
272
  initial_conformers: Sequence[StructureInput] | None = None,
@@ -264,6 +307,11 @@ def submit_conformer_search_workflow(
264
307
  `multistage_opt_settings` is provided
265
308
  transition_state: whether to optimize the transition state. Ignored
266
309
  if `multistage_opt_settings` is provided
310
+ frequencies: compute vibrational frequencies of each conformer at the optimization
311
+ level, for the ensemble IR spectrum. Ignored if `multistage_opt_settings` is
312
+ provided; set its `frequencies` instead
313
+ weighting_temperature: temperature for the conformer populations, in Kelvin. With
314
+ frequencies, populations use free energies at this temperature
267
315
  multistage_opt_settings: optimization stages and singlepoint settings
268
316
  for ranking conformers. When provided, takes precedence over
269
317
  `final_method` / `solvent` / `transition_state`. When omitted, an MSO is
@@ -359,7 +407,10 @@ def submit_conformer_search_workflow(
359
407
  if isinstance(final_method, str):
360
408
  final_method = Method(final_method)
361
409
  multistage_opt_settings = _mso_for_final_method(
362
- final_method, solvent=solvent, transition_state=transition_state
410
+ final_method,
411
+ solvent=solvent,
412
+ transition_state=transition_state,
413
+ frequencies=frequencies,
363
414
  )
364
415
  multistage_opt_settings = _release_ts_optimization_constraints(multistage_opt_settings)
365
416
 
@@ -370,6 +421,7 @@ def submit_conformer_search_workflow(
370
421
  multistage_opt_settings=multistage_opt_settings,
371
422
  conf_gen_settings=conf_gen_settings,
372
423
  conformer_clustering_settings=conformer_clustering_settings,
424
+ weighting_temperature=weighting_temperature,
373
425
  )
374
426
 
375
427
  workflow_data = workflow.model_dump(serialize_as_any=True, mode="json")
@@ -3,27 +3,47 @@
3
3
  from dataclasses import dataclass
4
4
 
5
5
  import stjames
6
+ from stjames import MixMDSettings, PocketeerSettings
6
7
 
7
8
  from rowan.folder import Folder
8
- from rowan.protein import Protein
9
+ from rowan.protein import Protein, retrieve_protein
9
10
  from rowan.types import ProteinUUID
10
11
  from rowan.utils import api_client
11
12
 
12
- from .base import Workflow, WorkflowResult, register_result
13
+ from .base import Message, Workflow, WorkflowResult, parse_messages, register_result
14
+
15
+
16
+ @dataclass(frozen=True, slots=True)
17
+ class MixMDPocketDiagnostics:
18
+ """Probe enrichment for a pocket found by mixed-solvent MD.
19
+
20
+ Attributes:
21
+ density_center: density-weighted center of the enriched region, in Å
22
+ peak_enrichment: maximum probe density relative to bulk solvent
23
+ integrated_excess_density: probe molecules above bulk, integrated over the region
24
+ """
25
+
26
+ density_center: tuple[float, float, float]
27
+ peak_enrichment: float
28
+ integrated_excess_density: float
13
29
 
14
30
 
15
31
  @dataclass(frozen=True, slots=True)
16
32
  class Pocket:
17
33
  """A detected binding pocket.
18
34
 
35
+ Mixed-solvent MD pockets describe regions of enriched probe density, so their spheres
36
+ and volume outline that region rather than the cavity.
37
+
19
38
  Attributes:
20
39
  sphere_centers: centers of detected spheres, in Å
21
40
  sphere_radii: radii of detected spheres, in Å
22
41
  volume: pocket volume, in ų
23
- score: druggability/quality score; larger is better
42
+ score: method-specific ranking score; larger is better
24
43
  pocket_center: center of axis-aligned bounding box, in Å
25
44
  pocket_sides: side lengths of axis-aligned bounding box, in Å
26
45
  residue_numbers: residue numbers lining the pocket
46
+ mixmd: probe-enrichment details for mixed-solvent MD pockets
27
47
  """
28
48
 
29
49
  sphere_centers: tuple[tuple[float, float, float], ...]
@@ -33,6 +53,60 @@ class Pocket:
33
53
  pocket_center: tuple[float, float, float]
34
54
  pocket_sides: tuple[float, float, float]
35
55
  residue_numbers: tuple[int, ...]
56
+ mixmd: MixMDPocketDiagnostics | None = None
57
+
58
+
59
+ @dataclass(frozen=True, slots=True)
60
+ class MixMDReplicaDiagnostics:
61
+ """One mixed-solvent MD replica.
62
+
63
+ Attributes:
64
+ index: zero-based replica index
65
+ seed: replica random seed
66
+ num_frames: analyzed frames; zero for a failed replica
67
+ num_probes: probe molecules in the simulation
68
+ largest_probe_cluster_fraction: mean fraction of probes in the largest cluster
69
+ trajectory_uuid: UUID of the stored trajectory
70
+ topology_uuid: UUID of the trajectory topology protein
71
+ density_map_uuid: UUID of the stored probe-density map
72
+ """
73
+
74
+ index: int
75
+ seed: int
76
+ num_frames: int
77
+ num_probes: int
78
+ largest_probe_cluster_fraction: float | None
79
+ trajectory_uuid: str | None
80
+ topology_uuid: str | None
81
+ density_map_uuid: str | None
82
+
83
+
84
+ @dataclass(frozen=True, slots=True)
85
+ class MixMDResults:
86
+ """Mixed-solvent MD replicas and consensus artifacts.
87
+
88
+ Attributes:
89
+ reference_uuid: UUID of the protein defining pocket and map coordinates
90
+ density_map_uuid: UUID of the consensus probe-density map
91
+ replicas: per-replica results, including failed replicas
92
+ """
93
+
94
+ reference_uuid: str | None
95
+ density_map_uuid: str | None
96
+ replicas: tuple[MixMDReplicaDiagnostics, ...]
97
+
98
+
99
+ def _mixmd_diagnostics(
100
+ raw: stjames.MixMDPocketDiagnostics | None,
101
+ ) -> MixMDPocketDiagnostics | None:
102
+ """Convert stjames probe-enrichment diagnostics, if present."""
103
+ if raw is None:
104
+ return None
105
+ return MixMDPocketDiagnostics(
106
+ density_center=tuple(raw.density_center),
107
+ peak_enrichment=raw.peak_enrichment,
108
+ integrated_excess_density=raw.integrated_excess_density,
109
+ )
36
110
 
37
111
 
38
112
  @register_result("pocket_detection")
@@ -57,14 +131,59 @@ class PocketDetectionResult(WorkflowResult):
57
131
  pocket_center=tuple(p.pocket_center),
58
132
  pocket_sides=tuple(p.pocket_sides),
59
133
  residue_numbers=tuple(p.residue_numbers),
134
+ mixmd=_mixmd_diagnostics(p.mixmd),
60
135
  )
61
136
  for p in raw
62
137
  ]
63
138
 
139
+ @property
140
+ def mixmd(self) -> MixMDResults | None:
141
+ """Mixed-solvent MD replicas and artifacts, or `None` for Pocketeer runs."""
142
+ if not (raw := getattr(self._workflow, "mixmd", None)):
143
+ return None
144
+ return MixMDResults(
145
+ reference_uuid=raw.reference_uuid,
146
+ density_map_uuid=raw.density_map_uuid,
147
+ replicas=tuple(
148
+ MixMDReplicaDiagnostics(
149
+ index=r.index,
150
+ seed=r.seed,
151
+ num_frames=r.num_frames,
152
+ num_probes=r.num_probes,
153
+ largest_probe_cluster_fraction=r.largest_probe_cluster_fraction,
154
+ trajectory_uuid=r.trajectory_uuid,
155
+ topology_uuid=r.topology_uuid,
156
+ density_map_uuid=r.density_map_uuid,
157
+ )
158
+ for r in raw.replicas
159
+ ),
160
+ )
161
+
162
+ @property
163
+ def messages(self) -> list[Message]:
164
+ """Any messages or warnings from the workflow, including MixMD reliability warnings."""
165
+ return parse_messages(getattr(self._workflow, "messages", None))
166
+
167
+ def get_reference_protein(self) -> Protein | None:
168
+ """Fetch and cache the protein defining mixed-solvent MD pocket and map coordinates.
169
+
170
+ Returns:
171
+ protein object, or `None` for Pocketeer runs
172
+ """
173
+ if not (mixmd := self.mixmd) or not mixmd.reference_uuid:
174
+ return None
175
+ if "reference_protein" not in self._cache:
176
+ self._cache["reference_protein"] = retrieve_protein(
177
+ mixmd.reference_uuid, workflow_uuid=self.workflow_uuid
178
+ )
179
+ return self._cache["reference_protein"]
180
+
64
181
 
65
182
  def submit_pocket_detection_workflow(
66
183
  protein: Protein | ProteinUUID,
67
- merge_distance: float = 1.75,
184
+ merge_distance: float | None = None,
185
+ settings: PocketeerSettings | MixMDSettings | None = None,
186
+ validate_forcefield: bool = True,
68
187
  name: str = "Pocket Detection Workflow",
69
188
  folder_uuid: str | None = None,
70
189
  folder: Folder | None = None,
@@ -76,7 +195,13 @@ def submit_pocket_detection_workflow(
76
195
 
77
196
  Args:
78
197
  protein: protein to analyze. Can be a UUID or a Protein object
79
- merge_distance: distance for merging pocket spheres, in Å
198
+ merge_distance: Pocketeer distance for merging pocket spheres, in Å; defaults to 1.75.
199
+ Set it on `PocketeerSettings` instead when passing `settings`
200
+ settings: method settings; `PocketeerSettings` (default) for geometric detection or
201
+ `MixMDSettings` for mixed-solvent molecular dynamics
202
+ validate_forcefield: validate the protein forcefield compatibility before submitting a
203
+ mixed-solvent MD run. Raises an error early if the protein cannot be parameterized
204
+ or has clashing residues
80
205
  name: name of the workflow
81
206
  folder_uuid: UUID of the folder to place the workflow in
82
207
  folder: destination folder
@@ -88,23 +213,33 @@ def submit_pocket_detection_workflow(
88
213
  workflow object representing the submitted workflow
89
214
 
90
215
  Raises:
216
+ ValueError: both `merge_distance` and `settings`, or both `folder` and `folder_uuid`,
217
+ are given
91
218
  httpx.HTTPStatusError: request to the API fails
92
219
  """
93
220
  if folder and folder_uuid:
94
221
  raise ValueError("Provide either `folder` or `folder_uuid`, not both.")
222
+ if settings is not None and merge_distance is not None:
223
+ raise ValueError("Set `merge_distance` on `PocketeerSettings` when passing `settings`.")
95
224
  if folder:
96
225
  folder_uuid = folder.uuid
97
226
  if isinstance(protein, Protein):
98
227
  protein = protein.uuid
99
228
 
100
- workflow = stjames.PocketDetectionWorkflow(
101
- protein=protein,
102
- merge_distance=merge_distance,
103
- )
229
+ if settings is None:
230
+ settings = (
231
+ PocketeerSettings()
232
+ if merge_distance is None
233
+ else PocketeerSettings(merge_distance=merge_distance)
234
+ )
235
+ if validate_forcefield and isinstance(settings, MixMDSettings):
236
+ Protein(uuid=protein).validate_protein_forcefield()
237
+
238
+ workflow = stjames.PocketDetectionWorkflow(protein=protein, settings=settings)
104
239
 
105
240
  data = {
106
241
  "workflow_type": "pocket_detection",
107
- "workflow_data": workflow.model_dump(mode="json"),
242
+ "workflow_data": workflow.model_dump(serialize_as_any=True, mode="json"),
108
243
  "name": name,
109
244
  "folder_uuid": folder_uuid,
110
245
  "max_credits": max_credits,
@@ -8,6 +8,7 @@ from stjames import (
8
8
  Binder,
9
9
  GreedyClusteringSettings,
10
10
  KMeansClusteringSettings,
11
+ MDRestraints,
11
12
  ProteinForceField,
12
13
  WaterForceField,
13
14
  )
@@ -158,6 +159,7 @@ def submit_protein_md_workflow(
158
159
  max_credits: int | None = None,
159
160
  webhook_url: str | None = None,
160
161
  is_draft: bool = False,
162
+ restraints: list[MDRestraints] | None = None,
161
163
  ) -> Workflow[ProteinMDResult]:
162
164
  """Submits a Protein Molecular Dynamics (MD) workflow to the API.
163
165
 
@@ -192,7 +194,7 @@ def submit_protein_md_workflow(
192
194
  (one small molecule → heavy-atom RMSD; one binder chain → backbone N/CA/C/O RMSD);
193
195
  it is empty for multi-molecule, multi-chain, or combined chain+molecule binders
194
196
  protein_restraint_cutoff: cutoff distance from the binder past which Cα atoms are
195
- harmonically restrained, in Å; None disables restraints
197
+ harmonically restrained, in Å; None disables these Cα restraints
196
198
  protein_restraint_constant: force constant for Cα backbone restraints, in kcal/mol/Ų
197
199
  analysis_interval_ps: interval at which to compute per-frame SASA and polar SASA
198
200
  (default 100), in ps. None disables those analyses
@@ -210,6 +212,9 @@ def submit_protein_md_workflow(
210
212
  max_credits: maximum credits for the workflow
211
213
  webhook_url: URL that Rowan will POST to when the workflow completes
212
214
  is_draft: save as a draft without starting execution
215
+ restraints: atom or whole-residue restraints applied during minimization,
216
+ equilibration, and production. Atoms use their positions; residues use
217
+ their centers of mass. Cannot be combined with protein_restraint_cutoff
213
218
 
214
219
  Returns:
215
220
  submitted workflow
@@ -256,6 +261,7 @@ def submit_protein_md_workflow(
256
261
  binder=binder,
257
262
  protein_restraint_cutoff=protein_restraint_cutoff,
258
263
  protein_restraint_constant=protein_restraint_constant,
264
+ restraints=restraints or [],
259
265
  analysis_interval_ps=analysis_interval_ps,
260
266
  clustering=clustering,
261
267
  )
@@ -71,6 +71,7 @@ def submit_protein_preparation_workflow(
71
71
  max_credits: int | None = None,
72
72
  webhook_url: str | None = None,
73
73
  is_draft: bool = False,
74
+ retain_noncanonical_residues: bool = False,
74
75
  ) -> Workflow[ProteinPreparationResult]:
75
76
  """Submit a protein-preparation workflow to the API.
76
77
 
@@ -97,6 +98,8 @@ def submit_protein_preparation_workflow(
97
98
  max_credits: maximum credits for the workflow
98
99
  webhook_url: URL that Rowan will POST to when the workflow completes
99
100
  is_draft: save as a draft without starting execution
101
+ retain_noncanonical_residues: preserve modified polymer residues such as SEP/TPO/PTR;
102
+ False replaces them with their unmodified parent residues
100
103
 
101
104
  Returns:
102
105
  workflow object representing the submitted workflow
@@ -119,6 +122,7 @@ def submit_protein_preparation_workflow(
119
122
  protonation_method=protonation_method,
120
123
  pH=pH,
121
124
  retain_protonation=retain_protonation,
125
+ retain_noncanonical_residues=retain_noncanonical_residues,
122
126
  retain_non_polymer=(dict(retain_non_polymer) if retain_non_polymer is not None else None),
123
127
  )
124
128
 
@@ -12,7 +12,7 @@ from rowan.folder import Folder
12
12
  from rowan.molecule import Molecule
13
13
  from rowan.protein import Protein
14
14
  from rowan.types import ProteinUUID
15
- from rowan.utils import api_client, download_file
15
+ from rowan.utils import api_client, download_file, download_presigned_files
16
16
 
17
17
  from .base import Workflow, WorkflowResult, molecule_to_dict, register_result
18
18
  from .rbfe_graph import RelativeBindingFreeEnergyGraphEdge, RelativeBindingFreeEnergyGraphResult
@@ -143,7 +143,9 @@ class RelativeBindingFreeEnergyPerturbationResult(WorkflowResult):
143
143
  *,
144
144
  leg: Literal["complex", "solvent"] = "complex",
145
145
  ) -> Path:
146
- """Download DCD trajectory files for a specific perturbation edge.
146
+ """Download DCD trajectory files for a specific perturbation edge as one `.tar.gz` archive.
147
+
148
+ Long trajectories can time out; use `download_edge_trajectory_files` for those.
147
149
 
148
150
  Args:
149
151
  edge_index: index of the edge (0-based, matching `edges` order)
@@ -183,6 +185,40 @@ class RelativeBindingFreeEnergyPerturbationResult(WorkflowResult):
183
185
  json=lambda_vals,
184
186
  )
185
187
 
188
+ def download_edge_trajectory_files(
189
+ self,
190
+ edge_index: int,
191
+ lambda_vals: list[float] | None = None,
192
+ path: Path | str | None = None,
193
+ *,
194
+ leg: Literal["complex", "solvent"] = "complex",
195
+ ) -> list[Path]:
196
+ """Download each lambda window's DCD file for one edge directly from storage.
197
+
198
+ Args:
199
+ edge_index: index of the edge (0-based, matching `edges` order)
200
+ lambda_vals: lambda values to download. Defaults to all windows
201
+ path: directory to save the files to. Defaults to current directory
202
+ leg: thermodynamic leg whose trajectories should be downloaded
203
+
204
+ Returns:
205
+ paths to the downloaded `.dcd` files, one per lambda window
206
+
207
+ Raises:
208
+ IndexError: edge_index is out of range
209
+ httpx.HTTPStatusError: API or storage request fails
210
+ """
211
+ edges = self.edges
212
+ if edge_index < 0 or edge_index >= len(edges):
213
+ raise IndexError(f"Edge index {edge_index} out of range (0-{len(edges) - 1})")
214
+
215
+ return download_presigned_files(
216
+ Path(path) if path is not None else Path.cwd(),
217
+ f"/trajectory/{self.workflow_uuid}/rbfe_trajectory_dcd_urls",
218
+ params={"edge_index": edge_index, "leg": leg},
219
+ json=lambda_vals,
220
+ )
221
+
186
222
  def download_all_trajectories(
187
223
  self,
188
224
  path: Path | str | None = None,
@@ -205,6 +241,35 @@ class RelativeBindingFreeEnergyPerturbationResult(WorkflowResult):
205
241
  self.download_edge_trajectories(i, path=path, leg=leg) for i in range(len(self.edges))
206
242
  ]
207
243
 
244
+ def download_all_trajectory_files(
245
+ self,
246
+ path: Path | str | None = None,
247
+ *,
248
+ leg: Literal["complex", "solvent"] = "complex",
249
+ ) -> list[Path]:
250
+ """Download every edge's DCD files directly from storage.
251
+
252
+ Files for each edge go in an `edge_<index>` subdirectory of `path`.
253
+
254
+ Args:
255
+ path: directory to save the files to. Defaults to current directory
256
+ leg: thermodynamic leg whose trajectories should be downloaded
257
+
258
+ Returns:
259
+ paths to all downloaded `.dcd` files, in edge order
260
+
261
+ Raises:
262
+ httpx.HTTPStatusError: any API or storage request fails
263
+ """
264
+ directory = Path(path) if path is not None else Path.cwd()
265
+ return [
266
+ file
267
+ for i in range(len(self.edges))
268
+ for file in self.download_edge_trajectory_files(
269
+ i, path=directory / f"edge_{i}", leg=leg
270
+ )
271
+ ]
272
+
208
273
  @property
209
274
  def ligand_dg_results(self) -> dict[str, RelativeBindingFreeEnergyResult] | None:
210
275
  """Per-ligand binding free energy results, or None if not yet computed."""
@@ -287,6 +352,8 @@ def submit_relative_binding_free_energy_perturbation_workflow(
287
352
  max_credits: int | None = None,
288
353
  webhook_url: str | None = None,
289
354
  is_draft: bool = False,
355
+ *,
356
+ small_molecules: dict[str | int, str | None] | None = None,
290
357
  ) -> Workflow[RelativeBindingFreeEnergyPerturbationResult]:
291
358
  """Submits a relative binding free energy perturbation (RBFE) workflow to the API.
292
359
 
@@ -323,13 +390,16 @@ def submit_relative_binding_free_energy_perturbation_workflow(
323
390
  save_trajectories: whether to save DCD trajectories
324
391
  trajectory_save_interval: save every Nth frame when saving trajectories
325
392
  validate_forcefield: validate protein forcefield
326
- compatibility before submitting
393
+ compatibility before submitting, excluding cofactors supplied with SMILES
327
394
  name: name of the workflow
328
395
  folder_uuid: UUID of the folder to place the workflow in
329
396
  folder: destination folder
330
397
  max_credits: maximum credits for the workflow
331
398
  webhook_url: URL that Rowan will POST to when the workflow completes
332
399
  is_draft: save as a draft without starting execution
400
+ small_molecules: fixed cofactors in the prepared protein, keyed by residue name
401
+ or zero-based index among all residues ordered by lowest atom ID, and mapped
402
+ to SMILES. Known ions and waters may map to None to use existing residue templates
333
403
 
334
404
  Returns:
335
405
  submitted workflow
@@ -345,9 +415,6 @@ def submit_relative_binding_free_energy_perturbation_workflow(
345
415
 
346
416
  protein_uuid = protein.uuid if isinstance(protein, Protein) else protein
347
417
 
348
- if validate_forcefield:
349
- Protein(uuid=protein_uuid).validate_protein_forcefield()
350
-
351
418
  if graph_result.graph is None:
352
419
  raise ValueError("RBFEGraphResult has no graph - has the workflow completed successfully?")
353
420
 
@@ -381,9 +448,20 @@ def submit_relative_binding_free_energy_perturbation_workflow(
381
448
  graph=graph_result.graph,
382
449
  target=protein_uuid,
383
450
  protein=protein_uuid,
451
+ small_molecules=small_molecules,
384
452
  settings=settings,
385
453
  )
386
454
 
455
+ if validate_forcefield:
456
+ exclude_residues = [
457
+ residue
458
+ for residue, smiles in (workflow.small_molecules or {}).items()
459
+ if smiles is not None
460
+ ]
461
+ Protein(uuid=protein_uuid).validate_protein_forcefield(
462
+ exclude_residues=exclude_residues or None
463
+ )
464
+
387
465
  data = {
388
466
  "workflow_type": "relative_binding_free_energy_perturbation",
389
467
  "workflow_data": workflow.model_dump(mode="json"),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: rowan-python
3
- Version: 3.2.2
3
+ Version: 3.2.4
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
@@ -12,7 +12,7 @@ Requires-Dist: httpx
12
12
  Requires-Dist: nest-asyncio
13
13
  Requires-Dist: rdkit
14
14
  Requires-Dist: setuptools
15
- Requires-Dist: stjames>=0.0.281
15
+ Requires-Dist: stjames>=0.0.293
16
16
  Description-Content-Type: text/markdown
17
17
 
18
18
  # Rowan Python Library
@@ -1,4 +1,4 @@
1
- rowan/__init__.py,sha256=NBswhyW8dInEyeCwizI9zZoxhAEBhnP5i00-ETllcG8,2146
1
+ rowan/__init__.py,sha256=QHph9MW2q5LS0nLYJKIn62a5bcnHvfY0pGP8VSNklns,2319
2
2
  rowan/api_keys.py,sha256=ms8NAkLreFQcaEUPCf8lSf7T0x2OmrWCAT44N1cgocE,6955
3
3
  rowan/calculation.py,sha256=E1dAox4ScE3NHJXiKFZ3Lpmw2h7DKCVzcwRvGZ2H2cQ,3802
4
4
  rowan/config.py,sha256=ay6UxKLE9bbTxHFXxDfUXGRVJWJ1T-eS2_L4HF8mb_E,20927
@@ -8,11 +8,12 @@ rowan/molecule.py,sha256=1ueC_M2Yaubkt5eQlONCagShWPnoJfNNXgd0s7sslPM,15826
8
8
  rowan/project.py,sha256=R5uwpzg7GOaGQ_ebCvTysYvkUHE_hHxWAhujp7CBqU0,5770
9
9
  rowan/protein.py,sha256=AGHarvWfciSwWXfHnVjdUW47FML7vI4Kk4dYTeFfKAQ,19322
10
10
  rowan/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ rowan/spectra.py,sha256=dNmE5th6wpTVAfvZZNDkg2jlBRAmPtFT6yDtxex8APw,2307
11
12
  rowan/types.py,sha256=D0d6MW6MvLMs6UtNgGKlapFGOnPlzqFTk9EAgB9dwks,636
12
- rowan/user.py,sha256=wGwyp0uzifzJeC88yCC7pgxMhxuZ-LLWHfCcuPXcx44,6199
13
- rowan/utils.py,sha256=d5s7UzjgJNtZ_NDLjvXSeSVnEBrKOqQiyVy-7C13rm0,8020
14
- rowan/workflows/__init__.py,sha256=1JBuqs3KlizJEFhidvGCG7KtTdLOQKI2kspn0SK_KHw,5161
15
- rowan/workflows/_molecular_dynamics.py,sha256=ZkddNEo-aRzwFRopDh6LcPPRDL6Rrx4lwW-_roMW3Kc,9323
13
+ rowan/user.py,sha256=GWPh_d7PMhK7ShQ2NtFKl3YWyHI3hfYX-KPJGUCbGI4,6069
14
+ rowan/utils.py,sha256=n82jhtMrHwIPXjEX4i2JFuQTLsanwixk5HfOfzPpKko,9656
15
+ rowan/workflows/__init__.py,sha256=SmKpoLCsNzkkb6xxoG2DEbGOVGiIBpDcmn6ZQ_F7DT0,5253
16
+ rowan/workflows/_molecular_dynamics.py,sha256=fDQVAZam6Ic_Au1yz41bVWg2_NBfkPX2ZFJNDIbw55U,10204
16
17
  rowan/workflows/admet.py,sha256=92jEbZr9OuN0FzJtWmE4EdARzs6r0uiRwq3-kSPV7nE,2531
17
18
  rowan/workflows/analogue_docking.py,sha256=NZa-2rNzuyfVE_5WZm4pQspdtWFy8tEhxbqwOrXWGnU,10881
18
19
  rowan/workflows/base.py,sha256=DR2k5VFMmv2sa9v4hYU_Fvzf8k2unp5nhPqvOd7NX64,39520
@@ -20,7 +21,7 @@ rowan/workflows/basic_calculation.py,sha256=7o9jxEpDMiong2k3Ke1WCLwaSWZvKY5tqhbi
20
21
  rowan/workflows/batch_docking.py,sha256=AtCp2-D_LOVxzSPmQ144zYIz5kft9NeRljELdRDU6vY,4941
21
22
  rowan/workflows/bde.py,sha256=4Yw27U2RPsuUDf9tw-7wPbw_4gaUcvDlQP8je7VJktU,7099
22
23
  rowan/workflows/binding_affinity.py,sha256=CP5rsWNMhsdLGzLdPBIs89wZDW6sa8YwGEkqep0ls1Q,6821
23
- rowan/workflows/conformer_search.py,sha256=enx9HbFZVVYgHJP-HUFam63E0qCDq6jjGeXNuBXXlys,16311
24
+ rowan/workflows/conformer_search.py,sha256=PSufIThwh2LoqgP6x5gjFxuNW5DP5ClJ5caBCuJoDKU,18910
24
25
  rowan/workflows/constants.py,sha256=iM9-6DyIe0N2ydyq-lRYP7VDufFmMrN5tDjnpUSlxF8,588
25
26
  rowan/workflows/covalent_inhibitor_scan.py,sha256=VcZhwW3HRuFuGZNedMPZBWbzuhGvz_B5pNJSIPyi9Kw,6721
26
27
  rowan/workflows/descriptors.py,sha256=mLo_VnqVrnEbiMHT1-SrbKhy_Dk5jpIM4btNGrlmNio,3041
@@ -39,22 +40,22 @@ rowan/workflows/msa.py,sha256=oXCbhHjBrodZpu_PjF5JF9z7-6ShpwAwHe1RPt7j_vY,4688
39
40
  rowan/workflows/multistage_optimization.py,sha256=27XDEYvvqMPhHscyHRvEM7AWK5CFwP2QoIUXRLhm59s,7316
40
41
  rowan/workflows/nmr.py,sha256=6yWhweIkB4tKUmJzMPFczQfDKqPWW5DD0Z7YZrv5XKI,8413
41
42
  rowan/workflows/pka.py,sha256=stwjhNcoXYO3Ulh7Wl_3EDED3hRLeX5rNcuGTdVHdHU,8459
42
- rowan/workflows/pocket_detection.py,sha256=dFeCLB59v5-Izf54blrRzJhwUtBQX2SJyVKT_-_nOUs,3889
43
+ rowan/workflows/pocket_detection.py,sha256=lSzRsmIEGOzvUorpcXeLmWaW0yPP4VfcG8-udaNfkfg,9397
43
44
  rowan/workflows/pose_analysis_md.py,sha256=ndfGS8oP8oQ3EXe83Wgbx1H8ZFURlm8IJZN34aDpqeE,10599
44
45
  rowan/workflows/protein_binder_design.py,sha256=ee3qYGzTzQsca9otnRz9Qjwj1X66LTCKqtTQ5EOzBHk,9181
45
46
  rowan/workflows/protein_cofolding.py,sha256=s4bLxlix4kAO_NzS75tc2wiEYqUxpFofxziUnxI2iUY,17422
46
- rowan/workflows/protein_md.py,sha256=iDeKW0ONY3ZPKPsjmZVQhbFQbo0vmOp5M8ko2Hfc16U,12277
47
- rowan/workflows/protein_preparation.py,sha256=8C04rhR4AiGiYJWZ8V06O4kpV443tGihcTk8rnNgscE,5325
47
+ rowan/workflows/protein_md.py,sha256=d7ARvsejEy5ZKFtznlxPZn1auVHdTiyu8GCCLMpz344,12641
48
+ rowan/workflows/protein_preparation.py,sha256=N-IMUveGL7VxFSxWC-uW607PicYjoyYa4tUNuWt30JA,5604
48
49
  rowan/workflows/rbfe_graph.py,sha256=DBNxtmgw7BNSwOXFLpHDxJtfpheTpDPs8U3SYB2MXdE,8070
49
50
  rowan/workflows/redox_potential.py,sha256=0AV2nKTngEFw7VDDpDQvdeBo5u03Aklc2ak4bDWxUz0,5366
50
- rowan/workflows/relative_binding_free_energy_perturbation.py,sha256=dBQkM3wtbNVVkiXbB3RBBDUcme2OnWZkWFZdTUR8vlM,15872
51
+ rowan/workflows/relative_binding_free_energy_perturbation.py,sha256=ruNdOW6RWAcvPUWLAV-JpAhf_F1nBVBbWkhzC_5FIC8,18977
51
52
  rowan/workflows/scan.py,sha256=MqV0NcjfInwxGlegOyWvAt8pwN0BdpsKvgCgTRkzdzU,7440
52
53
  rowan/workflows/solubility.py,sha256=Lws0lk4ifmtj84W_jyeOuaTpt8KM8-fceOggHrEYnmc,11211
53
54
  rowan/workflows/solvent_dependent_conformers.py,sha256=1dYf8UBVMRFELdwHzv3MI0pe12qoCAfohkaMiW3ak50,7460
54
55
  rowan/workflows/spin_states.py,sha256=C3hJEY4hccE1fCW4UjI4nIqGZLw9l2C2VKWCLiopSlY,9044
55
56
  rowan/workflows/strain.py,sha256=hq0dOlxnN72V4Sohr3qNFbOrjGmV01tc5zcF0HrL2-Y,6372
56
57
  rowan/workflows/tautomer_search.py,sha256=o5iFXHfcb6DvvWH7itA05hvWIrzxrc3a_9IDikYftiM,6351
57
- rowan_python-3.2.2.dist-info/METADATA,sha256=xezscvm6Crm8AmqvJ0fXrjTPLcstG5WYq20pkIypqa8,3934
58
- rowan_python-3.2.2.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
59
- rowan_python-3.2.2.dist-info/licenses/LICENSE,sha256=i05z7xEhyrg6f8j0lR3XYjShnF-MJGFQ-DnpsZ8yiVI,1084
60
- rowan_python-3.2.2.dist-info/RECORD,,
58
+ rowan_python-3.2.4.dist-info/METADATA,sha256=EBP6kz8swDog3FXU89-1jiKnxJ7hLFs5f0p9w2cUbrk,3934
59
+ rowan_python-3.2.4.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
60
+ rowan_python-3.2.4.dist-info/licenses/LICENSE,sha256=i05z7xEhyrg6f8j0lR3XYjShnF-MJGFQ-DnpsZ8yiVI,1084
61
+ rowan_python-3.2.4.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.32.4
2
+ Generator: hatchling 1.33.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any