rowan-python 3.1.7__py3-none-any.whl → 3.1.9__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
@@ -24,6 +24,7 @@ from stjames import (
24
24
  Mode,
25
25
  MSAFormat,
26
26
  MultiStageOptSettings,
27
+ NMR_SUPPORTED_SOLVENTS,
27
28
  OpenConfSettings,
28
29
  OptimizationSettings,
29
30
  PBCDFTSettings,
@@ -38,7 +39,11 @@ from stjames import (
38
39
  VibrationalMode,
39
40
  VinaSettings,
40
41
  )
41
- from stjames.workflows.relative_binding_free_energy_perturbation import RBFEGraph, RBFEGraphEdge
42
+ from stjames.workflows.relative_binding_free_energy_perturbation import (
43
+ RBFEGraph,
44
+ RBFEGraphEdge,
45
+ TMDRBFESettings,
46
+ )
42
47
  from stjames.excited_state_settings import TDDFTSettings
43
48
  from stjames.pbc_dft_settings import PBCDFTSmearing
44
49
  from stjames.engine_compatibility import (
rowan/molecule.py CHANGED
@@ -144,6 +144,15 @@ class Molecule(BaseModel):
144
144
 
145
145
  # -- Properties --
146
146
 
147
+ @property
148
+ def name(self) -> str | None:
149
+ """Optional molecule name."""
150
+ return self._stjames.name
151
+
152
+ @name.setter
153
+ def name(self, value: str | None) -> None:
154
+ self._stjames.name = value
155
+
147
156
  @property
148
157
  def charge(self) -> int:
149
158
  """Molecular charge."""
rowan/project.py CHANGED
@@ -126,6 +126,29 @@ def create_project(
126
126
  return Project(**project_data)
127
127
 
128
128
 
129
+ def get_project(name: str, create: bool = False) -> Project:
130
+ """
131
+ Get a project by exact name, optionally creating it if it does not exist.
132
+
133
+ The project analogue of :func:`get_folder`. Unlike ``get_folder``, ``create``
134
+ defaults to ``False``: a project is a top-level container, so a typo should
135
+ raise rather than silently spawn a new one. Does not change the active project -
136
+ assign ``rowan.project_uuid`` or use :func:`set_project` for that.
137
+
138
+ :param name: Exact name of the project.
139
+ :param create: If True, create the project when no exact match exists.
140
+ :returns: Matched (or newly created) project.
141
+ :raises ValueError: If no match is found and ``create`` is False.
142
+ """
143
+ matches = list_projects(name_contains=name, size=100)
144
+ project = next((p for p in matches if p.name == name), None)
145
+ if project is not None:
146
+ return project
147
+ if create:
148
+ return create_project(name)
149
+ raise ValueError(f"Project {name!r} not found")
150
+
151
+
129
152
  def set_project(name: str) -> Project:
130
153
  """
131
154
  Set the active project by name for all subsequent API calls.
@@ -142,10 +165,7 @@ def set_project(name: str) -> Project:
142
165
  :returns: Matched project.
143
166
  :raises ValueError: If no project with that name is found.
144
167
  """
145
- matches = list_projects(name_contains=name, size=100)
146
- project = next((p for p in matches if p.name == name), None)
147
- if project is None:
148
- raise ValueError(f"Project {name!r} not found")
168
+ project = get_project(name)
149
169
  rowan.project_uuid = project.uuid
150
170
  return project
151
171
 
@@ -98,15 +98,26 @@ class AnalogueDockingResult(WorkflowResult):
98
98
  Best docked pose per analogue, keyed by SMILES.
99
99
 
100
100
  Fetches the final geometry from each analogue's top-scoring pose.
101
- Analogues with no successful poses are excluded.
101
+ Analogues with no successful poses are excluded. When ``analogue_names``
102
+ were supplied at submission, each pose's ``name`` is set to the analogue
103
+ name, so poses can be re-keyed by name (e.g. ``{p.name: p for p in ...}``).
102
104
 
103
105
  :returns: Dictionary mapping SMILES to docked ``Molecule`` with 3D coordinates.
104
106
  """
105
- return {
106
- smiles: self.get_pose(smiles).molecules[-1]
107
- for smiles, scores in self.analogue_scores.items()
108
- if scores
109
- }
107
+ # analogue_names is empty or parallel to analogues; zip drops the tail when empty.
108
+ name_by_smiles = dict(
109
+ zip(self._workflow.analogues, self._workflow.analogue_names, strict=False)
110
+ )
111
+ poses = {}
112
+ for smiles, scores in self.analogue_scores.items():
113
+ if not scores:
114
+ continue
115
+ pose = self.get_pose(smiles).molecules[-1]
116
+ # Keep any name the pose already carries; otherwise fall back to analogue_names.
117
+ if not pose.name:
118
+ pose.name = name_by_smiles.get(smiles)
119
+ poses[smiles] = pose
120
+ return poses
110
121
 
111
122
  def get_poses(self, smiles: str) -> list[Calculation]:
112
123
  """
@@ -175,6 +186,7 @@ def submit_analogue_docking_workflow(
175
186
  analogues: list[str],
176
187
  initial_molecule: StructureInput,
177
188
  protein: str | Protein,
189
+ analogue_names: list[str] | None = None,
178
190
  scoring_function: Literal["vina", "vinardo"] = "vinardo",
179
191
  exhaustiveness: int = 8,
180
192
  max_poses: int = 4,
@@ -194,6 +206,9 @@ def submit_analogue_docking_workflow(
194
206
  :param analogues: SMILES strings to dock.
195
207
  :param initial_molecule: Template to which to align molecules to.
196
208
  :param protein: Protein to dock. Can be input as a uuid or a Protein object.
209
+ :param analogue_names: Names parallel to `analogues`. When provided, each docked
210
+ pose's `Molecule.name` is set to its analogue name (see `best_poses`), and must
211
+ be the same length as `analogues`. If omitted, poses are identified by SMILES.
197
212
  :param scoring_function: Docking scoring function: "vina" or "vinardo".
198
213
  :param exhaustiveness: How many times Vina attempts to find a pose for each conformer.
199
214
  :param max_poses: Maximum number of poses generated per input conformer.
@@ -210,6 +225,11 @@ def submit_analogue_docking_workflow(
210
225
  :raises requests.HTTPError: if the request to the API fails.
211
226
  """
212
227
  require_coordinates(initial_molecule)
228
+ if analogue_names is not None and len(analogue_names) != len(analogues):
229
+ raise ValueError(
230
+ f"analogue_names must match analogues in length "
231
+ f"(got {len(analogue_names)} names for {len(analogues)} analogues)."
232
+ )
213
233
  if folder and folder_uuid:
214
234
  raise ValueError("Provide either `folder` or `folder_uuid`, not both.")
215
235
  if folder:
@@ -228,6 +248,7 @@ def submit_analogue_docking_workflow(
228
248
 
229
249
  workflow = stjames.AnalogueDockingWorkflow(
230
250
  analogues=analogues,
251
+ analogue_names=analogue_names or [],
231
252
  initial_molecule=mol_dict,
232
253
  protein=protein,
233
254
  docking_settings=docking_settings,
rowan/workflows/nmr.py CHANGED
@@ -1,6 +1,7 @@
1
1
  """NMR workflow - predict Nuclear Magnetic Resonance spectra."""
2
2
 
3
3
  from dataclasses import dataclass
4
+ from typing import Any
4
5
 
5
6
  import stjames
6
7
 
@@ -98,6 +99,32 @@ class NMRResult(WorkflowResult):
98
99
  return self._workflow.symmetry_equivalent_nuclei
99
100
 
100
101
 
102
+ def _nmr_multistage_opt_settings(solvent: SolventInput) -> stjames.MultiStageOptSettings:
103
+ """
104
+ Build NMR optimization settings, adding a solvated AIMNet2 singlepoint.
105
+
106
+ Optimization runs gas-phase (matching MagNet); the solvated singlepoint, whose solvent model
107
+ is looked up from stjames.NMR_SOLVENT_MODELS, reweights the conformer ensemble.
108
+
109
+ :param solvent: solvent for the prediction
110
+ :returns: multi-stage optimization settings
111
+ """
112
+ singlepoint_settings = stjames.Settings(
113
+ method=stjames.Method.AIMNET2_WB97MD3,
114
+ tasks=[stjames.Task.ENERGY],
115
+ solvent_settings=stjames.SolventSettings(
116
+ solvent=solvent, model=stjames.NMR_SOLVENT_MODELS[stjames.Solvent(solvent)]
117
+ ),
118
+ )
119
+
120
+ return stjames.MultiStageOptSettings(
121
+ optimization_settings=[
122
+ stjames.Settings(method=stjames.Method.AIMNET2_WB97MD3, tasks=[stjames.Task.OPTIMIZE])
123
+ ],
124
+ singlepoint_settings=singlepoint_settings,
125
+ )
126
+
127
+
101
128
  def submit_nmr_workflow(
102
129
  initial_molecule: StructureInput,
103
130
  solvent: SolventInput = "chloroform",
@@ -114,7 +141,10 @@ def submit_nmr_workflow(
114
141
  Submits a Nuclear Magnetic Resonance (NMR) prediction workflow to the API.
115
142
 
116
143
  :param initial_molecule: Molecule to predict NMR spectra for.
117
- :param solvent: Solvent for NMR calculation (default: chloroform).
144
+ :param solvent: Solvent for NMR calculation (default: chloroform). Must be an NMR-supported
145
+ solvent (see rowan.NMR_SUPPORTED_SOLVENTS); others raise ValueError. A solvated AIMNet2
146
+ singlepoint reweights the conformer ensemble, using CPCM-X where supported and
147
+ otherwise ALPB.
118
148
  :param do_csearch: Whether to perform a conformational search. Requires do_optimization.
119
149
  :param do_optimization: Whether to optimize conformer geometries.
120
150
  :param name: Name of the workflow.
@@ -127,6 +157,12 @@ def submit_nmr_workflow(
127
157
  :raises requests.HTTPError: if the request to the API fails.
128
158
  """
129
159
  require_coordinates(initial_molecule)
160
+ if stjames.Solvent(solvent) not in stjames.NMR_SUPPORTED_SOLVENTS:
161
+ supported = ", ".join(sorted(s.value for s in stjames.NMR_SUPPORTED_SOLVENTS))
162
+ raise ValueError(
163
+ f"{stjames.Solvent(solvent).value!r} is not an NMR-supported solvent. "
164
+ f"NMR-supported solvents: {supported}."
165
+ )
130
166
  if do_csearch and not do_optimization:
131
167
  raise ValueError(
132
168
  "`do_optimization` must be True when `do_csearch` is True; the conformers from "
@@ -138,13 +174,15 @@ def submit_nmr_workflow(
138
174
  folder_uuid = folder.uuid
139
175
  mol_dict = molecule_to_dict(initial_molecule)
140
176
 
141
- workflow_data = {"initial_molecule": mol_dict, "solvent": solvent}
177
+ workflow_data: dict[str, Any] = {"initial_molecule": mol_dict, "solvent": solvent}
142
178
 
143
179
  if not do_csearch:
144
180
  workflow_data["conf_gen_settings"] = None
145
181
 
146
182
  if not do_optimization:
147
183
  workflow_data["multistage_opt_settings"] = None
184
+ else:
185
+ workflow_data["multistage_opt_settings"] = _nmr_multistage_opt_settings(solvent)
148
186
 
149
187
  workflow = stjames.NMRSpectroscopyWorkflow.model_validate(workflow_data)
150
188
 
@@ -1,5 +1,6 @@
1
1
  """RBFE perturbation workflow - run relative binding free energy FEP simulations."""
2
2
 
3
+ import csv
3
4
  from dataclasses import dataclass
4
5
  from pathlib import Path
5
6
  from typing import Literal
@@ -91,6 +92,11 @@ class RelativeBindingFreeEnergyPerturbationResult(WorkflowResult):
91
92
  """Prepared protein structure used as the simulation target."""
92
93
  return Protein(uuid=str(self._workflow.protein))
93
94
 
95
+ @property
96
+ def settings(self) -> TMDRBFESettings:
97
+ """FEP settings used for the simulation."""
98
+ return self._workflow.settings
99
+
94
100
  @property
95
101
  def graph(self) -> RBFEGraph | None:
96
102
  """The RBFE graph with per-edge results, or None if not constructed."""
@@ -187,6 +193,34 @@ class RelativeBindingFreeEnergyPerturbationResult(WorkflowResult):
187
193
  k: RelativeBindingFreeEnergyResult(dg=v.dg, dg_err=v.dg_err) for k, v in raw.items()
188
194
  }
189
195
 
196
+ def write_ligand_results_csv(self, path: Path | str = "ligand_results.csv") -> Path:
197
+ """
198
+ Write per-ligand results to a CSV, matching the platform's ligand-results export.
199
+
200
+ Columns are ``Ligand name``, ``ΔG``, ``ΔG error``, ``SMILES``, quoted and
201
+ BOM-prefixed for spreadsheet compatibility, identical to the download in the web UI.
202
+
203
+ :param path: destination CSV path.
204
+ :returns: path written to.
205
+ :raises ValueError: if per-ligand results are not yet available.
206
+ """
207
+ if (results := self.ligand_dg_results) is None:
208
+ raise ValueError("No per-ligand results yet - has the workflow completed?")
209
+
210
+ # Match JS Number->string: whole numbers render without a trailing ".0" (0.0 -> "0").
211
+ def num(x: float) -> str:
212
+ return str(int(x)) if x == int(x) else str(x)
213
+
214
+ ligands = self.ligands
215
+ path = Path(path)
216
+ with path.open("w", newline="", encoding="utf-8-sig") as f:
217
+ writer = csv.writer(f, quoting=csv.QUOTE_ALL, lineterminator="\n")
218
+ writer.writerow(["Ligand name", "ΔG", "ΔG error", "SMILES"])
219
+ for name, res in results.items():
220
+ smiles = ligands[name].smiles if name in ligands else None
221
+ writer.writerow([name, num(res.dg), num(res.dg_err), smiles or ""])
222
+ return path
223
+
190
224
  @property
191
225
  def diagnostics(self) -> RelativeBindingFreeEnergyDiagnostics | None:
192
226
  """Aggregate QC metrics from the FEP simulation."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rowan-python
3
- Version: 3.1.7
3
+ Version: 3.1.9
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.228
14
+ Requires-Dist: stjames>=0.0.236
15
15
  Description-Content-Type: text/markdown
16
16
 
17
17
  # Rowan Python Library
@@ -1,11 +1,11 @@
1
- rowan/__init__.py,sha256=95VjN9XTgEICFLCcB4oUbT_nsU_EvulDqMDJjBuSTI8,1714
1
+ rowan/__init__.py,sha256=XMe16awgKPEe8rIrA4h6xq7bQ-Pz5cTyusIxw9rWF68,1776
2
2
  rowan/api_keys.py,sha256=TvG5l5MmQ3Qt8Z3Y7jCA_lcrwEHuI7xHy-KLbIdQ8_A,4793
3
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
6
  rowan/folder.py,sha256=RRg68dObluJ2RCTKX3rb7RxziOXOQUYxkfQSU2R5VMk,13708
7
- rowan/molecule.py,sha256=1l-3YCN_A75Uz_Q-dMWuhkeF8rhD1z2JQM7AENyjKYM,15512
8
- rowan/project.py,sha256=RtxYE9jv3Yz6fH5I56iDGRG5EbwlOHiSK-HF1uxc0d4,4582
7
+ rowan/molecule.py,sha256=MFx0D3_LJm1G3UiDogkfCHvae31gR6xo_N0LzrNC-ks,15733
8
+ rowan/project.py,sha256=4f1XGRe_EADjbd5uFGPvsVXz9s3t4ZiL8loTBlahn7A,5404
9
9
  rowan/protein.py,sha256=uFMF2BXgIvn6KOw3cdE-LDvZPPbjAxqqwgxakLd85XA,17763
10
10
  rowan/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  rowan/test_utils.py,sha256=DTuZqftx6ycIatajAxSGWgDaGglLvTw2TYnVJHiiNdo,1936
@@ -14,7 +14,7 @@ rowan/user.py,sha256=rOCYVhSoRY71fCG_OCSD7cLAWWnAMANWGitF3ogfq30,6212
14
14
  rowan/utils.py,sha256=J-3o9_ZlOrl4_ZuuC1yjEOPdD1IkKO0BOXBncV3Ij7c,5670
15
15
  rowan/workflows/__init__.py,sha256=pkmAl8aPDznu15bSuRW0j8id-B7Xmzy_tq3LgZfQ7NQ,4786
16
16
  rowan/workflows/admet.py,sha256=jDx1Kkgmp0XaeojPzaCFY_yUwvqtSM7Cezps-93cqPw,2865
17
- rowan/workflows/analogue_docking.py,sha256=TshOw71RmK5XaaTIHz_Q1opiDU9x1L38tXwxoYUZaxY,9756
17
+ rowan/workflows/analogue_docking.py,sha256=ngv5IFbOVFe42uSFkeEtM2qt-zAJFfJyF8ducexGK5U,11020
18
18
  rowan/workflows/base.py,sha256=k858otXLuorw4MZ1hvqZI_m0n_38htC7Hy3OMCX1TOA,32961
19
19
  rowan/workflows/basic_calculation.py,sha256=EovymT6fV6mxmt4G4kRtiskSPGB7w7KAHqBaGtgQI2Q,12941
20
20
  rowan/workflows/batch_docking.py,sha256=ePnb1hcsmnUfffl1-k9HNkOC09RjY9bZS9XcqAGNeFs,3621
@@ -36,7 +36,7 @@ rowan/workflows/macropka.py,sha256=IxGwr9LziIwj_x6pXEhJN6SywS_KULziuYoETNqM370,5
36
36
  rowan/workflows/membrane_permeability.py,sha256=wHZ1IPlXwBB343uVAm7EuhedcMPd-Uy4jMqHfXRHwqY,4680
37
37
  rowan/workflows/msa.py,sha256=k_FlxxtdgvmxDN3LyLDEQeFfI8_lyBJz1KFtz3S4oNo,4816
38
38
  rowan/workflows/multistage_optimization.py,sha256=04kZPC3SmVOxqEQOwyBrS3DlelM-b8e531T_LuVt3tU,7385
39
- rowan/workflows/nmr.py,sha256=68be_Xgiy1AykGdIL4fR1pqKllD1nLm3IidsStzKtz4,5521
39
+ rowan/workflows/nmr.py,sha256=V0-vHEadlbHk5dUmOZg-DSqkDXm4sF-xNsUZRTk1arM,7209
40
40
  rowan/workflows/pka.py,sha256=NZ-3i-cH_b7YOHj2iq0roXEcZmC8SYapmzMOX6ObRqw,8546
41
41
  rowan/workflows/pocket_detection.py,sha256=aGHY0puxekp4c4nsNYHcvKCe1fsetygL04BcSvNFvE8,3864
42
42
  rowan/workflows/pose_analysis_md.py,sha256=5TRsCBFD0QObE5tpmCOxi137xsKMfQrRO6ef8TLrnkk,11767
@@ -45,14 +45,14 @@ rowan/workflows/protein_cofolding.py,sha256=haT0liJorxyHgO02WX8WslqAu7Dc7QOSs6Vb
45
45
  rowan/workflows/protein_md.py,sha256=zqKEtL514uchgbxsyO4vhokzTU-X4lTFA1BM5O8HvTU,10117
46
46
  rowan/workflows/rbfe_graph.py,sha256=0eMfvwtQhyajUC8T-bhdcYNKi6Pkja6x6dHjCnEwxPw,8081
47
47
  rowan/workflows/redox_potential.py,sha256=bBeT1K9XGVpjCFZeeuv7Vtr3EYHN2okJ4LqYc3gMV04,5427
48
- rowan/workflows/relative_binding_free_energy_perturbation.py,sha256=vi5Qj5aDAERtjcHDsgzI6vRV_BZJnJ8boOj5aG6GXzo,13817
48
+ rowan/workflows/relative_binding_free_energy_perturbation.py,sha256=D8WNFWAm4VwO_Gn50XHQS4kUAqu-KiJFHAOLx5VGvdk,15347
49
49
  rowan/workflows/scan.py,sha256=R5Y6Md3ohPoFVJ4NYvx3_k3JNwMnVw1CDsNPD9S8sr4,7518
50
50
  rowan/workflows/solubility.py,sha256=ulZwdG8JRl0RCwWPatYUZGMhlAZeb6wqjVbvzQYFKmE,8555
51
51
  rowan/workflows/solvent_dependent_conformers.py,sha256=Z2xESmcM8WcTsR2TSTUa--RrhrQ-5ri_WagD9wJeVwc,6812
52
52
  rowan/workflows/spin_states.py,sha256=rjkgie2-XVNIN7O6P93yn6EPv9Ogjy8iTj3ufZIsUgY,9331
53
53
  rowan/workflows/strain.py,sha256=kCW_BlX__sdQG1JVbFZuB-57rkpnxa_JCw3h4uKdGDk,6425
54
54
  rowan/workflows/tautomer_search.py,sha256=mbRl0ZJ7wibueRF8c8_idhXJ1rtwg7LBt--5QwQ_Cck,5767
55
- rowan_python-3.1.7.dist-info/METADATA,sha256=mCUORIhOBiBBn69iu-_EXm3QUnUuv6wwDKKK84cio3o,2436
56
- rowan_python-3.1.7.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
57
- rowan_python-3.1.7.dist-info/licenses/LICENSE,sha256=i05z7xEhyrg6f8j0lR3XYjShnF-MJGFQ-DnpsZ8yiVI,1084
58
- rowan_python-3.1.7.dist-info/RECORD,,
55
+ rowan_python-3.1.9.dist-info/METADATA,sha256=DERGY6Uore4bP-ogOwngz2ZUlzLnv06a6_blp6l_h8I,2436
56
+ rowan_python-3.1.9.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
57
+ rowan_python-3.1.9.dist-info/licenses/LICENSE,sha256=i05z7xEhyrg6f8j0lR3XYjShnF-MJGFQ-DnpsZ8yiVI,1084
58
+ rowan_python-3.1.9.dist-info/RECORD,,