rowan-mcp 1.0.2__py3-none-any.whl → 2.0.1__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.
Potentially problematic release.
This version of rowan-mcp might be problematic. Click here for more details.
- rowan_mcp/__init__.py +1 -1
- rowan_mcp/__main__.py +3 -5
- rowan_mcp/functions/admet.py +0 -5
- rowan_mcp/functions/bde.py +1 -8
- rowan_mcp/functions/conformers.py +1 -4
- rowan_mcp/functions/descriptors.py +1 -4
- rowan_mcp/functions/docking.py +6 -56
- rowan_mcp/functions/electronic_properties.py +1 -4
- rowan_mcp/functions/folder_management.py +1 -8
- rowan_mcp/functions/fukui.py +1 -4
- rowan_mcp/functions/hydrogen_bond_basicity.py +1 -8
- rowan_mcp/functions/multistage_opt.py +1 -4
- rowan_mcp/functions/pka.py +1 -8
- rowan_mcp/functions/redox_potential.py +2 -5
- rowan_mcp/functions/system_management.py +1 -8
- rowan_mcp/functions/tautomers.py +1 -4
- rowan_mcp/functions_v2/BENCHMARK.md +86 -0
- rowan_mcp/functions_v2/molecule_lookup.py +232 -0
- rowan_mcp/functions_v2/protein_management.py +141 -0
- rowan_mcp/functions_v2/submit_basic_calculation_workflow.py +195 -0
- rowan_mcp/functions_v2/submit_conformer_search_workflow.py +158 -0
- rowan_mcp/functions_v2/submit_descriptors_workflow.py +52 -0
- rowan_mcp/functions_v2/submit_docking_workflow.py +244 -0
- rowan_mcp/functions_v2/submit_fukui_workflow.py +114 -0
- rowan_mcp/functions_v2/submit_irc_workflow.py +58 -0
- rowan_mcp/functions_v2/submit_macropka_workflow.py +99 -0
- rowan_mcp/functions_v2/submit_pka_workflow.py +72 -0
- rowan_mcp/functions_v2/submit_protein_cofolding_workflow.py +88 -0
- rowan_mcp/functions_v2/submit_redox_potential_workflow.py +55 -0
- rowan_mcp/functions_v2/submit_scan_workflow.py +82 -0
- rowan_mcp/functions_v2/submit_solubility_workflow.py +157 -0
- rowan_mcp/functions_v2/submit_tautomer_search_workflow.py +51 -0
- rowan_mcp/functions_v2/workflow_management_v2.py +382 -0
- rowan_mcp/server.py +109 -144
- rowan_mcp/tests/basic_calculation_from_json.py +0 -0
- rowan_mcp/tests/basic_calculation_with_constraint.py +33 -0
- rowan_mcp/tests/basic_calculation_with_solvent.py +0 -0
- rowan_mcp/tests/bde.py +37 -0
- rowan_mcp/tests/benchmark_queries.md +120 -0
- rowan_mcp/tests/cofolding_screen.py +131 -0
- rowan_mcp/tests/conformer_dependent_redox.py +37 -0
- rowan_mcp/tests/conformers.py +31 -0
- rowan_mcp/tests/data.json +189 -0
- rowan_mcp/tests/docking_screen.py +157 -0
- rowan_mcp/tests/irc.py +24 -0
- rowan_mcp/tests/macropka.py +13 -0
- rowan_mcp/tests/multistage_opt.py +13 -0
- rowan_mcp/tests/optimization.py +21 -0
- rowan_mcp/tests/phenol_pka.py +36 -0
- rowan_mcp/tests/pka.py +36 -0
- rowan_mcp/tests/protein_cofolding.py +17 -0
- rowan_mcp/tests/scan.py +28 -0
- {rowan_mcp-1.0.2.dist-info → rowan_mcp-2.0.1.dist-info}/METADATA +38 -45
- rowan_mcp-2.0.1.dist-info/RECORD +69 -0
- rowan_mcp-1.0.2.dist-info/RECORD +0 -34
- {rowan_mcp-1.0.2.dist-info → rowan_mcp-2.0.1.dist-info}/WHEEL +0 -0
- {rowan_mcp-1.0.2.dist-info → rowan_mcp-2.0.1.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Rowan v2 API: Docking Workflow
|
|
3
|
+
Perform molecular docking simulations for drug discovery.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Dict, Any, Union, List, Annotated
|
|
7
|
+
import rowan
|
|
8
|
+
import stjames
|
|
9
|
+
import json
|
|
10
|
+
from stjames.pdb import PDB, read_pdb
|
|
11
|
+
|
|
12
|
+
def submit_docking_workflow(
|
|
13
|
+
protein: Annotated[str, "Protein UUID or PDB content/path for docking target"],
|
|
14
|
+
pocket: Annotated[str, "JSON string defining binding pocket coordinates or 'auto' for automatic detection"],
|
|
15
|
+
initial_molecule: Annotated[str, "SMILES string of the ligand molecule to dock"],
|
|
16
|
+
do_csearch: Annotated[bool, "Whether to perform conformer search before docking"] = True,
|
|
17
|
+
do_optimization: Annotated[bool, "Whether to optimize docked poses"] = True,
|
|
18
|
+
name: Annotated[str, "Workflow name for identification and tracking"] = "Docking Workflow",
|
|
19
|
+
folder_uuid: Annotated[str, "UUID of folder to organize this workflow. Empty string uses default folder"] = "",
|
|
20
|
+
max_credits: Annotated[int, "Maximum credits to spend on this calculation. 0 for no limit"] = 0,
|
|
21
|
+
blocking: Annotated[bool, "Whether to wait for workflow completion before returning"] = False
|
|
22
|
+
):
|
|
23
|
+
"""Submits a Docking workflow to the API.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
protein: Protein for docking. Can be: 1) PDB ID string (e.g., '1HCK'), 2) Protein UUID string, 3) JSON string dict with 'pdb_id' and optional 'name'
|
|
27
|
+
pocket: Binding pocket as JSON string "[[x1,y1,z1], [x2,y2,z2]]" defining box corners for docking site
|
|
28
|
+
initial_molecule: SMILES string representing the ligand
|
|
29
|
+
do_csearch: Whether to perform conformational search on the ligand before docking
|
|
30
|
+
do_optimization: Whether to optimize the ligand geometry before docking
|
|
31
|
+
name: Workflow name for identification and tracking
|
|
32
|
+
folder_uuid: UUID of folder to organize this workflow. Empty string uses default folder.
|
|
33
|
+
max_credits: Maximum credits to spend on this calculation. 0 for no limit.
|
|
34
|
+
blocking: Whether to wait for workflow completion before returning
|
|
35
|
+
|
|
36
|
+
Automatically handles protein creation from PDB ID and sanitization if needed.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Workflow object representing the submitted workflow
|
|
40
|
+
|
|
41
|
+
Examples:
|
|
42
|
+
# Example 1: Using PDB ID directly
|
|
43
|
+
result = submit_docking_workflow(
|
|
44
|
+
protein="1HCK", # PDB ID
|
|
45
|
+
pocket="[[103.55, 100.59, 82.99], [27.76, 32.67, 48.79]]",
|
|
46
|
+
initial_molecule="CCC(C)(C)NC1=NCC2(CCC(=O)C2C)N1",
|
|
47
|
+
name="CDK2 Docking"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Example 2: Using dict with PDB ID and custom name
|
|
51
|
+
result = submit_docking_workflow(
|
|
52
|
+
protein='{"pdb_id": "1HCK", "name": "My CDK2 Protein"}',
|
|
53
|
+
pocket="[[103.55, 100.59, 82.99], [27.76, 32.67, 48.79]]",
|
|
54
|
+
initial_molecule="CCC(C)(C)NC1=NCC2(CCC(=O)C2C)N1"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Example 3: Using existing protein UUID
|
|
58
|
+
result = submit_docking_workflow(
|
|
59
|
+
protein="abc123-def456-...", # Protein UUID
|
|
60
|
+
pocket="[[103.55, 100.59, 82.99], [27.76, 32.67, 48.79]]",
|
|
61
|
+
initial_molecule="CCC(C)(C)NC1=NCC2(CCC(=O)C2C)N1"
|
|
62
|
+
)
|
|
63
|
+
"""
|
|
64
|
+
import logging
|
|
65
|
+
logger = logging.getLogger(__name__)
|
|
66
|
+
|
|
67
|
+
# Handle protein parameter
|
|
68
|
+
protein_obj = None
|
|
69
|
+
|
|
70
|
+
# Try to parse protein as JSON first
|
|
71
|
+
try:
|
|
72
|
+
protein_dict = json.loads(protein)
|
|
73
|
+
if isinstance(protein_dict, dict):
|
|
74
|
+
protein = protein_dict
|
|
75
|
+
except (json.JSONDecodeError, ValueError):
|
|
76
|
+
# Not JSON, keep as string
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
# Check if protein is a PDB ID or dict with PDB ID
|
|
80
|
+
if isinstance(protein, str):
|
|
81
|
+
# Check if it's a UUID (36 chars with dashes) or PDB ID (4 chars)
|
|
82
|
+
if len(protein) == 36 and '-' in protein:
|
|
83
|
+
# It's a UUID, retrieve the protein
|
|
84
|
+
logger.info(f"Using existing protein UUID: {protein}")
|
|
85
|
+
protein_obj = rowan.retrieve_protein(protein)
|
|
86
|
+
elif len(protein) <= 6: # PDB IDs are typically 4 characters
|
|
87
|
+
# It's a PDB ID, create protein from it
|
|
88
|
+
logger.info(f"Creating protein from PDB ID: {protein}")
|
|
89
|
+
|
|
90
|
+
# Get or create a project (REQUIRED for v2.1.1)
|
|
91
|
+
project_uuid = None
|
|
92
|
+
try:
|
|
93
|
+
# Try to get default project
|
|
94
|
+
project = rowan.default_project()
|
|
95
|
+
project_uuid = project.uuid
|
|
96
|
+
logger.info(f"Using default project: {project_uuid}")
|
|
97
|
+
except Exception as e:
|
|
98
|
+
logger.info(f"Could not get default project: {e}")
|
|
99
|
+
try:
|
|
100
|
+
# List existing projects and use the first one
|
|
101
|
+
projects = rowan.list_projects(size=1)
|
|
102
|
+
if projects:
|
|
103
|
+
project_uuid = projects[0].uuid
|
|
104
|
+
logger.info(f"Using existing project: {project_uuid}")
|
|
105
|
+
else:
|
|
106
|
+
# Create a new project if none exist
|
|
107
|
+
new_project = rowan.create_project(name="Docking Project")
|
|
108
|
+
project_uuid = new_project.uuid
|
|
109
|
+
logger.info(f"Created new project: {project_uuid}")
|
|
110
|
+
except Exception as e2:
|
|
111
|
+
logger.error(f"Failed to get/create project: {e2}")
|
|
112
|
+
raise ValueError(f"Cannot create protein without a valid project. Error: {e2}")
|
|
113
|
+
|
|
114
|
+
# Create protein with REQUIRED project_uuid
|
|
115
|
+
protein_obj = rowan.create_protein_from_pdb_id(
|
|
116
|
+
name=f"Protein from {protein}",
|
|
117
|
+
code=protein,
|
|
118
|
+
project_uuid=project_uuid
|
|
119
|
+
)
|
|
120
|
+
logger.info(f"Created protein with UUID: {protein_obj.uuid}")
|
|
121
|
+
|
|
122
|
+
# Sanitize the protein for docking
|
|
123
|
+
logger.info("Sanitizing protein for docking...")
|
|
124
|
+
try:
|
|
125
|
+
protein_obj.sanitize()
|
|
126
|
+
|
|
127
|
+
# Wait for sanitization to complete
|
|
128
|
+
import time
|
|
129
|
+
max_wait = 30
|
|
130
|
+
start_time = time.time()
|
|
131
|
+
while time.time() - start_time < max_wait:
|
|
132
|
+
time.sleep(2)
|
|
133
|
+
protein_obj.refresh()
|
|
134
|
+
if protein_obj.sanitized and protein_obj.sanitized != 0:
|
|
135
|
+
logger.info(f"Protein sanitized successfully (sanitized={protein_obj.sanitized})")
|
|
136
|
+
break
|
|
137
|
+
else:
|
|
138
|
+
logger.warning(f"Sanitization may not be complete after {max_wait} seconds")
|
|
139
|
+
except Exception as e:
|
|
140
|
+
logger.warning(f"Sanitization failed: {e}")
|
|
141
|
+
logger.warning("Proceeding without sanitization - docking may fail if protein needs sanitization")
|
|
142
|
+
else:
|
|
143
|
+
raise ValueError(f"Invalid protein parameter: {protein}. Expected PDB ID (4 chars) or UUID (36 chars)")
|
|
144
|
+
|
|
145
|
+
elif isinstance(protein, dict):
|
|
146
|
+
# Dict with PDB ID and optional name
|
|
147
|
+
pdb_id = protein.get('pdb_id')
|
|
148
|
+
protein_name = protein.get('name', f"Protein from {pdb_id}")
|
|
149
|
+
|
|
150
|
+
if not pdb_id:
|
|
151
|
+
raise ValueError("Dict protein parameter must include 'pdb_id' key")
|
|
152
|
+
|
|
153
|
+
logger.info(f"Creating protein '{protein_name}' from PDB ID: {pdb_id}")
|
|
154
|
+
|
|
155
|
+
# Get or create a project (REQUIRED for v2.1.1)
|
|
156
|
+
project_uuid = None
|
|
157
|
+
try:
|
|
158
|
+
# Try to get default project
|
|
159
|
+
project = rowan.default_project()
|
|
160
|
+
project_uuid = project.uuid
|
|
161
|
+
logger.info(f"Using default project: {project_uuid}")
|
|
162
|
+
except Exception as e:
|
|
163
|
+
logger.info(f"Could not get default project: {e}")
|
|
164
|
+
try:
|
|
165
|
+
# List existing projects and use the first one
|
|
166
|
+
projects = rowan.list_projects(size=1)
|
|
167
|
+
if projects:
|
|
168
|
+
project_uuid = projects[0].uuid
|
|
169
|
+
logger.info(f"Using existing project: {project_uuid}")
|
|
170
|
+
else:
|
|
171
|
+
# Create a new project if none exist
|
|
172
|
+
new_project = rowan.create_project(name="Docking Project")
|
|
173
|
+
project_uuid = new_project.uuid
|
|
174
|
+
logger.info(f"Created new project: {project_uuid}")
|
|
175
|
+
except Exception as e2:
|
|
176
|
+
logger.error(f"Failed to get/create project: {e2}")
|
|
177
|
+
raise ValueError(f"Cannot create protein without a valid project. Error: {e2}")
|
|
178
|
+
|
|
179
|
+
# Create protein with REQUIRED project_uuid
|
|
180
|
+
protein_obj = rowan.create_protein_from_pdb_id(
|
|
181
|
+
name=protein_name,
|
|
182
|
+
code=pdb_id,
|
|
183
|
+
project_uuid=project_uuid
|
|
184
|
+
)
|
|
185
|
+
logger.info(f"Created protein with UUID: {protein_obj.uuid}")
|
|
186
|
+
|
|
187
|
+
# Sanitize the protein
|
|
188
|
+
logger.info("Sanitizing protein for docking...")
|
|
189
|
+
try:
|
|
190
|
+
protein_obj.sanitize()
|
|
191
|
+
|
|
192
|
+
# Wait for sanitization
|
|
193
|
+
import time
|
|
194
|
+
max_wait = 30
|
|
195
|
+
start_time = time.time()
|
|
196
|
+
while time.time() - start_time < max_wait:
|
|
197
|
+
time.sleep(2)
|
|
198
|
+
protein_obj.refresh()
|
|
199
|
+
if protein_obj.sanitized and protein_obj.sanitized != 0:
|
|
200
|
+
logger.info(f"Protein sanitized successfully (sanitized={protein_obj.sanitized})")
|
|
201
|
+
break
|
|
202
|
+
else:
|
|
203
|
+
logger.warning(f"Sanitization may not be complete after {max_wait} seconds")
|
|
204
|
+
except Exception as e:
|
|
205
|
+
logger.warning(f"Sanitization failed: {e}")
|
|
206
|
+
logger.warning("Proceeding without sanitization - docking may fail if protein needs sanitization")
|
|
207
|
+
|
|
208
|
+
else:
|
|
209
|
+
# Assume it's already a protein object
|
|
210
|
+
protein_obj = protein
|
|
211
|
+
|
|
212
|
+
# Parse pocket parameter (always a string in simplified version)
|
|
213
|
+
try:
|
|
214
|
+
pocket = json.loads(pocket)
|
|
215
|
+
except (json.JSONDecodeError, ValueError) as e:
|
|
216
|
+
raise ValueError(f"Invalid pocket format: {pocket}. Expected JSON string like \"[[x1,y1,z1], [x2,y2,z2]]\"")
|
|
217
|
+
|
|
218
|
+
# Ensure pocket is a list of lists
|
|
219
|
+
if not isinstance(pocket, list) or len(pocket) != 2:
|
|
220
|
+
raise ValueError(f"Pocket must be a list with exactly 2 coordinate lists")
|
|
221
|
+
|
|
222
|
+
# Ensure each element is a list of floats
|
|
223
|
+
pocket = [list(coord) for coord in pocket]
|
|
224
|
+
|
|
225
|
+
# Submit the workflow
|
|
226
|
+
logger.info(f"Submitting docking workflow: {name}")
|
|
227
|
+
workflow = rowan.submit_docking_workflow(
|
|
228
|
+
protein=protein_obj,
|
|
229
|
+
pocket=pocket,
|
|
230
|
+
initial_molecule=stjames.Molecule.from_smiles(initial_molecule),
|
|
231
|
+
do_csearch=do_csearch,
|
|
232
|
+
do_optimization=do_optimization,
|
|
233
|
+
name=name,
|
|
234
|
+
folder_uuid=folder_uuid if folder_uuid else None,
|
|
235
|
+
max_credits=max_credits if max_credits > 0 else None
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
logger.info(f"Docking workflow submitted with UUID: {workflow.uuid}")
|
|
239
|
+
|
|
240
|
+
# If blocking, wait for completion
|
|
241
|
+
if blocking:
|
|
242
|
+
workflow.wait_for_result()
|
|
243
|
+
|
|
244
|
+
return workflow
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Rowan v2 API: Fukui Workflow
|
|
3
|
+
Calculate Fukui indices for reactivity analysis.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Dict, Any, Annotated
|
|
7
|
+
import rowan
|
|
8
|
+
import stjames
|
|
9
|
+
import json
|
|
10
|
+
|
|
11
|
+
def submit_fukui_workflow(
|
|
12
|
+
initial_molecule: Annotated[str, "SMILES string of the molecule to calculate Fukui indices for"],
|
|
13
|
+
optimization_method: Annotated[str, "Method for geometry optimization (e.g., 'gfn2_xtb', 'uma_m_omol')"] = "gfn2_xtb",
|
|
14
|
+
fukui_method: Annotated[str, "Method for Fukui indices calculation (e.g., 'gfn1_xtb', 'gfn2_xtb')"] = "gfn1_xtb",
|
|
15
|
+
solvent_settings: Annotated[str, "JSON string for solvent settings. Empty string for vacuum"] = "",
|
|
16
|
+
name: Annotated[str, "Workflow name for identification and tracking"] = "Fukui Workflow",
|
|
17
|
+
folder_uuid: Annotated[str, "UUID of folder to organize this workflow. Empty string uses default folder"] = "",
|
|
18
|
+
max_credits: Annotated[int, "Maximum credits to spend on this calculation. 0 for no limit"] = 0
|
|
19
|
+
):
|
|
20
|
+
"""Submit a Fukui indices calculation workflow using Rowan v2 API.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
initial_molecule: SMILES string for Fukui analysis
|
|
24
|
+
optimization_method: Method for geometry optimization. Options: 'gfn2_xtb', 'r2scan_3c', 'aimnet2_wb97md3'
|
|
25
|
+
fukui_method: Method for Fukui calculation. Options: 'gfn1_xtb', 'gfn2_xtb'
|
|
26
|
+
solvent_settings: Solvent configuration JSON string, e.g., '{"solvent": "water", "model": "alpb"}'. Empty for gas phase.
|
|
27
|
+
name: Workflow name for identification and tracking
|
|
28
|
+
folder_uuid: UUID of folder to organize this workflow. Empty string uses default folder.
|
|
29
|
+
max_credits: Maximum credits to spend on this calculation. 0 for no limit.
|
|
30
|
+
|
|
31
|
+
Calculates Fukui indices to predict molecular reactivity at different sites.
|
|
32
|
+
Fukui indices indicate susceptibility to nucleophilic/electrophilic attack.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Workflow object representing the submitted workflow
|
|
36
|
+
|
|
37
|
+
Example:
|
|
38
|
+
# Basic Fukui indices
|
|
39
|
+
result = submit_fukui_workflow(
|
|
40
|
+
initial_molecule="CC(=O)O"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# With solvent and advanced methods
|
|
44
|
+
result = submit_fukui_workflow(
|
|
45
|
+
initial_molecule="c1ccccc1N",
|
|
46
|
+
optimization_method="r2scan_3c",
|
|
47
|
+
fukui_method="gfn2_xtb",
|
|
48
|
+
solvent_settings='{"solvent": "water", "model": "alpb"}'
|
|
49
|
+
)
|
|
50
|
+
"""
|
|
51
|
+
# Parse solvent_settings if provided
|
|
52
|
+
parsed_solvent_settings = None
|
|
53
|
+
if solvent_settings:
|
|
54
|
+
try:
|
|
55
|
+
parsed_solvent_settings = json.loads(solvent_settings)
|
|
56
|
+
except (json.JSONDecodeError, ValueError):
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
# Convert initial_molecule to StJamesMolecule
|
|
61
|
+
molecule = stjames.Molecule.from_smiles(initial_molecule)
|
|
62
|
+
initial_molecule_dict = molecule.model_dump()
|
|
63
|
+
|
|
64
|
+
# Create Settings objects
|
|
65
|
+
optimization_settings = stjames.Settings(method=optimization_method)
|
|
66
|
+
fukui_settings = stjames.Settings(method=fukui_method, solvent_settings=parsed_solvent_settings)
|
|
67
|
+
|
|
68
|
+
# Serialize to dicts
|
|
69
|
+
opt_settings_dict = optimization_settings.model_dump(mode="json")
|
|
70
|
+
fukui_settings_dict = fukui_settings.model_dump(mode="json")
|
|
71
|
+
|
|
72
|
+
# Fix soscf boolean to string enum conversion for optimization settings
|
|
73
|
+
if 'scf_settings' in opt_settings_dict and 'soscf' in opt_settings_dict['scf_settings']:
|
|
74
|
+
soscf_val = opt_settings_dict['scf_settings']['soscf']
|
|
75
|
+
if isinstance(soscf_val, bool):
|
|
76
|
+
if soscf_val is False:
|
|
77
|
+
opt_settings_dict['scf_settings']['soscf'] = 'never'
|
|
78
|
+
elif soscf_val is True:
|
|
79
|
+
opt_settings_dict['scf_settings']['soscf'] = 'always'
|
|
80
|
+
|
|
81
|
+
# Fix soscf boolean to string enum conversion for fukui settings
|
|
82
|
+
if 'scf_settings' in fukui_settings_dict and 'soscf' in fukui_settings_dict['scf_settings']:
|
|
83
|
+
soscf_val = fukui_settings_dict['scf_settings']['soscf']
|
|
84
|
+
if isinstance(soscf_val, bool):
|
|
85
|
+
if soscf_val is False:
|
|
86
|
+
fukui_settings_dict['scf_settings']['soscf'] = 'never'
|
|
87
|
+
elif soscf_val is True:
|
|
88
|
+
fukui_settings_dict['scf_settings']['soscf'] = 'always'
|
|
89
|
+
|
|
90
|
+
workflow_data = {
|
|
91
|
+
"opt_settings": opt_settings_dict,
|
|
92
|
+
"opt_engine": stjames.Method(optimization_method).default_engine(),
|
|
93
|
+
"fukui_settings": fukui_settings_dict,
|
|
94
|
+
"fukui_engine": stjames.Method(fukui_method).default_engine(),
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# Build the API request payload
|
|
98
|
+
data = {
|
|
99
|
+
"name": name,
|
|
100
|
+
"folder_uuid": folder_uuid if folder_uuid else None,
|
|
101
|
+
"workflow_type": "fukui",
|
|
102
|
+
"workflow_data": workflow_data,
|
|
103
|
+
"initial_molecule": initial_molecule_dict,
|
|
104
|
+
"max_credits": max_credits if max_credits > 0 else None,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
# Submit to API
|
|
108
|
+
with rowan.api_client() as client:
|
|
109
|
+
response = client.post("/workflow", json=data)
|
|
110
|
+
response.raise_for_status()
|
|
111
|
+
return rowan.Workflow(**response.json())
|
|
112
|
+
|
|
113
|
+
except Exception as e:
|
|
114
|
+
raise e
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Rowan v2 API: IRC Workflow
|
|
3
|
+
Perform Intrinsic Reaction Coordinate calculations to trace reaction paths.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Annotated
|
|
7
|
+
import rowan
|
|
8
|
+
import stjames
|
|
9
|
+
|
|
10
|
+
def submit_irc_workflow(
|
|
11
|
+
initial_molecule: Annotated[str, "SMILES string for IRC calculation"],
|
|
12
|
+
method: Annotated[str, "Computational method for IRC (e.g., 'uma_m_omol', 'gfn2_xtb', 'r2scan_3c')"] = "uma_m_omol",
|
|
13
|
+
engine: Annotated[str, "Computational engine: 'omol25', 'xtb', 'psi4'"] = "omol25",
|
|
14
|
+
preopt: Annotated[bool, "Whether to pre-optimize the transition state before IRC step"] = True,
|
|
15
|
+
step_size: Annotated[float, "Step size for IRC path tracing in Bohr (typically 0.03-0.1)"] = 0.05,
|
|
16
|
+
max_irc_steps: Annotated[int, "Maximum number of IRC steps in each direction from TS"] = 30,
|
|
17
|
+
name: Annotated[str, "Workflow name for identification and tracking"] = "IRC Workflow",
|
|
18
|
+
folder_uuid: Annotated[str, "UUID of folder to organize this workflow. Empty string uses default folder"] = "",
|
|
19
|
+
max_credits: Annotated[int, "Maximum credits to spend on this calculation. 0 for no limit"] = 0
|
|
20
|
+
):
|
|
21
|
+
"""Submits an Intrinsic Reaction Coordinate (IRC) workflow to the API.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
initial_molecule: SMILES string for IRC calculation
|
|
25
|
+
method: Computational method for IRC. Options: 'uma_m_omol', 'gfn2_xtb', 'r2scan_3c'
|
|
26
|
+
engine: Computational engine. Options: 'omol25', 'xtb', 'psi4'
|
|
27
|
+
preopt: Whether to pre-optimize the transition state before IRC
|
|
28
|
+
step_size: Step size for IRC path tracing in Bohr (typically 0.03-0.1)
|
|
29
|
+
max_irc_steps: Maximum number of IRC steps in each direction from TS
|
|
30
|
+
name: Workflow name for identification and tracking
|
|
31
|
+
folder_uuid: UUID of folder to organize this workflow. Empty string uses default folder.
|
|
32
|
+
max_credits: Maximum credits to spend on this calculation. 0 for no limit.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Workflow object representing the submitted IRC workflow
|
|
36
|
+
|
|
37
|
+
Example:
|
|
38
|
+
# IRC from SMILES
|
|
39
|
+
result = submit_irc_workflow(
|
|
40
|
+
initial_molecule="N=C([O-])[OH2+]", # Transition state SMILES
|
|
41
|
+
name="HNCO + H₂O - IRC",
|
|
42
|
+
preopt=True, # Pre-optimize TS
|
|
43
|
+
method="gfn2_xtb",
|
|
44
|
+
engine="xtb"
|
|
45
|
+
)
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
return rowan.submit_irc_workflow(
|
|
49
|
+
initial_molecule=stjames.Molecule.from_smiles(initial_molecule),
|
|
50
|
+
method=method,
|
|
51
|
+
engine=engine,
|
|
52
|
+
preopt=preopt,
|
|
53
|
+
step_size=step_size,
|
|
54
|
+
max_irc_steps=max_irc_steps,
|
|
55
|
+
name=name,
|
|
56
|
+
folder_uuid=folder_uuid if folder_uuid else None,
|
|
57
|
+
max_credits=max_credits if max_credits > 0 else None
|
|
58
|
+
)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Rowan v2 API: MacropKa Workflow
|
|
3
|
+
Calculate macroscopic pKa values across a pH range.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Annotated
|
|
7
|
+
import rowan
|
|
8
|
+
|
|
9
|
+
def submit_macropka_workflow(
|
|
10
|
+
initial_smiles: Annotated[str, "SMILES string of the molecule for macropKa calculation"],
|
|
11
|
+
min_pH: Annotated[int, "Minimum pH value for the calculation range"] = 0,
|
|
12
|
+
max_pH: Annotated[int, "Maximum pH value for the calculation range"] = 14,
|
|
13
|
+
min_charge: Annotated[int, "Minimum molecular charge to consider"] = -2,
|
|
14
|
+
max_charge: Annotated[int, "Maximum molecular charge to consider"] = 2,
|
|
15
|
+
compute_solvation_energy: Annotated[bool, "Whether to compute solvation energy corrections"] = True,
|
|
16
|
+
name: Annotated[str, "Workflow name for identification and tracking"] = "Macropka Workflow",
|
|
17
|
+
folder_uuid: Annotated[str, "UUID of folder to organize this workflow. Empty string uses default folder"] = "",
|
|
18
|
+
max_credits: Annotated[int, "Maximum credits to spend on this calculation. 0 for no limit"] = 0
|
|
19
|
+
):
|
|
20
|
+
"""Submit a MacropKa workflow using Rowan v2 API.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
initial_smiles: SMILES string of the molecule for macropKa calculation
|
|
24
|
+
min_pH: Minimum pH value for the calculation range
|
|
25
|
+
max_pH: Maximum pH value for the calculation range
|
|
26
|
+
min_charge: Minimum molecular charge to consider
|
|
27
|
+
max_charge: Maximum molecular charge to consider
|
|
28
|
+
compute_solvation_energy: Whether to compute solvation energy for each species
|
|
29
|
+
name: Workflow name for identification and tracking
|
|
30
|
+
folder_uuid: UUID of folder to organize this workflow. Empty string uses default folder.
|
|
31
|
+
max_credits: Maximum credits to spend on this calculation. 0 for no limit.
|
|
32
|
+
|
|
33
|
+
Calculates macroscopic pKa values across a pH range, determining the
|
|
34
|
+
protonation states and their populations at different pH values.
|
|
35
|
+
|
|
36
|
+
The workflow will:
|
|
37
|
+
1. Identify all ionizable sites in the molecule
|
|
38
|
+
2. Calculate microscopic pKa values for each site
|
|
39
|
+
3. Determine macroscopic pKa values and species populations
|
|
40
|
+
4. Optionally compute solvation energies
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Workflow object representing the submitted workflow
|
|
44
|
+
|
|
45
|
+
Examples:
|
|
46
|
+
# Simple molecule macropKa
|
|
47
|
+
result = submit_macropka_workflow(
|
|
48
|
+
initial_smiles="CC(=O)O", # Acetic acid
|
|
49
|
+
min_pH=0,
|
|
50
|
+
max_pH=14
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Complex molecule with custom charge range
|
|
54
|
+
result = submit_macropka_workflow(
|
|
55
|
+
initial_smiles="CC(C)CC(C(=O)O)N", # Leucine
|
|
56
|
+
min_pH=0,
|
|
57
|
+
max_pH=14,
|
|
58
|
+
min_charge=-3,
|
|
59
|
+
max_charge=3,
|
|
60
|
+
compute_solvation_energy=True
|
|
61
|
+
)
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
# Build workflow_data
|
|
66
|
+
workflow_data = {
|
|
67
|
+
"min_pH": min_pH,
|
|
68
|
+
"max_pH": max_pH,
|
|
69
|
+
"min_charge": min_charge,
|
|
70
|
+
"max_charge": max_charge,
|
|
71
|
+
"compute_solvation_energy": compute_solvation_energy,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# Build the API request
|
|
75
|
+
data = {
|
|
76
|
+
"name": name,
|
|
77
|
+
"folder_uuid": folder_uuid,
|
|
78
|
+
"workflow_type": "macropka",
|
|
79
|
+
"workflow_data": workflow_data,
|
|
80
|
+
"initial_smiles": initial_smiles,
|
|
81
|
+
"max_credits": max_credits,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# Submit to API using rowan module
|
|
85
|
+
return rowan.submit_macropka_workflow(
|
|
86
|
+
initial_smiles=initial_smiles,
|
|
87
|
+
min_pH=min_pH,
|
|
88
|
+
max_pH=max_pH,
|
|
89
|
+
min_charge=min_charge,
|
|
90
|
+
max_charge=max_charge,
|
|
91
|
+
compute_solvation_energy=compute_solvation_energy,
|
|
92
|
+
name=name,
|
|
93
|
+
folder_uuid=folder_uuid if folder_uuid else None,
|
|
94
|
+
max_credits=max_credits if max_credits > 0 else None
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
except Exception as e:
|
|
98
|
+
# Re-raise the exception so MCP can handle it
|
|
99
|
+
raise e
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Rowan v2 API: pKa Workflow
|
|
3
|
+
Predict acid dissociation constants for ionizable groups in molecules.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import List, Dict, Any, Annotated
|
|
7
|
+
import rowan
|
|
8
|
+
import json
|
|
9
|
+
import stjames
|
|
10
|
+
|
|
11
|
+
def submit_pka_workflow(
|
|
12
|
+
initial_molecule: Annotated[str, "SMILES string of the molecule to calculate pKa"],
|
|
13
|
+
pka_range: Annotated[List[float], "pKa range [min, max] to search (e.g., [2, 12])"] = [2, 12],
|
|
14
|
+
deprotonate_elements: Annotated[str, "Comma-separated elements for deprotonation (e.g., 'N,O,S'). Empty for auto-detect"] = "",
|
|
15
|
+
protonate_elements: Annotated[str, "Comma-separated elements for protonation (e.g., 'N,O'). Empty for auto-detect"] = "",
|
|
16
|
+
mode: Annotated[str, "Calculation mode: 'rapid', 'careful', or 'meticulous'"] = "careful",
|
|
17
|
+
name: Annotated[str, "Workflow name for identification and tracking"] = "pKa Workflow",
|
|
18
|
+
folder_uuid: Annotated[str, "UUID of folder to organize this workflow. Empty string uses default folder"] = "",
|
|
19
|
+
max_credits: Annotated[int, "Maximum credits to spend on this calculation. 0 for no limit"] = 0
|
|
20
|
+
):
|
|
21
|
+
"""Submit a pKa prediction workflow using Rowan v2 API.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
initial_molecule: The molecule to calculate the pKa of. SMILES string.
|
|
25
|
+
pka_range: pKa range [min, max] to search, e.g., [2, 12]
|
|
26
|
+
deprotonate_elements: Atomic numbers to consider for deprotonation, e.g., "[7, 8, 16]" for N, O, S. Empty string uses defaults.
|
|
27
|
+
protonate_elements: Atomic numbers to consider for protonation, e.g., "[7, 8]" for N, O. Empty string uses defaults.
|
|
28
|
+
mode: Calculation mode: 'rapid' (fast), 'careful' (balanced), or 'meticulous' (thorough)
|
|
29
|
+
name: Workflow name for identification and tracking
|
|
30
|
+
folder_uuid: UUID of folder to organize this workflow. Empty string uses default folder.
|
|
31
|
+
max_credits: Maximum credits to spend on this calculation. 0 for no limit.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Workflow object representing the submitted workflow
|
|
35
|
+
|
|
36
|
+
Examples:
|
|
37
|
+
# Phenol pKa
|
|
38
|
+
result = submit_pka_workflow(
|
|
39
|
+
initial_molecule="Oc1ccccc1",
|
|
40
|
+
name="pKa phenol",
|
|
41
|
+
deprotonate_elements="[8]" # Only consider oxygen
|
|
42
|
+
)
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
# Handle JSON string inputs for element lists
|
|
46
|
+
parsed_deprotonate_elements = None
|
|
47
|
+
if deprotonate_elements:
|
|
48
|
+
try:
|
|
49
|
+
parsed_deprotonate_elements = json.loads(deprotonate_elements)
|
|
50
|
+
except json.JSONDecodeError:
|
|
51
|
+
pass # Keep as None if not valid JSON
|
|
52
|
+
|
|
53
|
+
parsed_protonate_elements = None
|
|
54
|
+
if protonate_elements:
|
|
55
|
+
try:
|
|
56
|
+
parsed_protonate_elements = json.loads(protonate_elements)
|
|
57
|
+
except json.JSONDecodeError:
|
|
58
|
+
pass # Keep as None if not valid JSON
|
|
59
|
+
|
|
60
|
+
# Convert List[float] to Tuple[float, float] for Rowan SDK compatibility
|
|
61
|
+
pka_range_tuple = tuple(pka_range) if len(pka_range) == 2 else (pka_range[0], pka_range[1] if len(pka_range) > 1 else pka_range[0])
|
|
62
|
+
|
|
63
|
+
return rowan.submit_pka_workflow(
|
|
64
|
+
initial_molecule=stjames.Molecule.from_smiles(initial_molecule),
|
|
65
|
+
pka_range=pka_range_tuple,
|
|
66
|
+
deprotonate_elements=parsed_deprotonate_elements,
|
|
67
|
+
protonate_elements=parsed_protonate_elements,
|
|
68
|
+
mode=mode,
|
|
69
|
+
name=name,
|
|
70
|
+
folder_uuid=folder_uuid if folder_uuid else None,
|
|
71
|
+
max_credits=max_credits if max_credits > 0 else None
|
|
72
|
+
)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Rowan v2 API: Protein Cofolding Workflow
|
|
3
|
+
Simulate protein-protein interactions and cofolding.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import List, Annotated
|
|
7
|
+
import rowan
|
|
8
|
+
import stjames
|
|
9
|
+
import json
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def submit_protein_cofolding_workflow(
|
|
13
|
+
initial_protein_sequences: Annotated[str, "JSON string list of protein sequences for cofolding (e.g., '[\"MKLLV...\", \"MAHQR...\"]')"],
|
|
14
|
+
initial_smiles_list: Annotated[str, "JSON string list of SMILES for ligands to include in cofolding (e.g., '[\"CCO\", \"CC(=O)O\"]'). Empty for protein-only"] = "",
|
|
15
|
+
ligand_binding_affinity_index: Annotated[str, "JSON string mapping ligand indices to protein binding sites. Empty for automatic detection"] = "",
|
|
16
|
+
use_msa_server: Annotated[bool, "Whether to use multiple sequence alignment server for better structure prediction"] = True,
|
|
17
|
+
use_potentials: Annotated[bool, "Whether to include additional potentials in the calculation"] = False,
|
|
18
|
+
name: Annotated[str, "Workflow name for identification and tracking"] = "Cofolding Workflow",
|
|
19
|
+
model: Annotated[str, "Structure prediction model to use (e.g., 'boltz_2', 'alphafold3')"] = "boltz_2",
|
|
20
|
+
folder_uuid: Annotated[str, "UUID of folder to organize this workflow. Empty string uses default folder"] = "",
|
|
21
|
+
max_credits: Annotated[int, "Maximum credits to spend on this calculation. 0 for no limit"] = 0
|
|
22
|
+
):
|
|
23
|
+
"""Submits a protein cofolding workflow to the API.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
initial_protein_sequences: JSON string list of protein sequences (amino acid strings) to cofold
|
|
27
|
+
initial_smiles_list: JSON string list of ligand SMILES strings to include in cofolding. Empty string for protein-only
|
|
28
|
+
ligand_binding_affinity_index: Index of ligand in initial_smiles_list for binding affinity calculation (e.g., "0"). Empty string skips affinity
|
|
29
|
+
use_msa_server: Whether to use MSA (Multiple Sequence Alignment) server for improved accuracy
|
|
30
|
+
use_potentials: Whether to use statistical potentials in the calculation
|
|
31
|
+
name: Workflow name for identification and tracking
|
|
32
|
+
model: Cofolding model to use (e.g., 'boltz_2', 'alphafold3')
|
|
33
|
+
folder_uuid: UUID of folder to organize this workflow. Empty string uses default folder.
|
|
34
|
+
max_credits: Maximum credits to spend on this calculation. 0 for no limit.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Workflow object representing the submitted workflow
|
|
38
|
+
|
|
39
|
+
Example:
|
|
40
|
+
# Protein-ligand cofolding with CDK2 kinase (from test)
|
|
41
|
+
result = submit_protein_cofolding_workflow(
|
|
42
|
+
initial_protein_sequences='["MENFQKVEKIGEGTYGVVYKARNKLTGEVVALKKIRLDTETEGVPSTAIREISLLKELNHPNIVKLLDVIHTENKLYLVFEFLHQDLKKFMDASALTGIPLPLIKSYLFQLLQGLAFCHSHRVLHRDLKPQNLLINTEGAIKLADFGLARAFGVPVRTYTHEVVTLWYRAPEILLGCKYYSTAVDIWSLGCIFAEMVTRRALFPGDSEIDQLFRIFRTLGTPDEVVWPGVTSMPDYKPSFPKWARQDFSKVVPPLDEDGRSLLSQMLHYDPNKRISAKAALAHPFFQDVTKPVPHLRL"]',
|
|
43
|
+
initial_smiles_list='["CCC(C)(C)NC1=NCC2(CCC(=O)C2C)N1"]',
|
|
44
|
+
ligand_binding_affinity_index="0",
|
|
45
|
+
name="Cofolding CDK2 with ligand"
|
|
46
|
+
)
|
|
47
|
+
"""
|
|
48
|
+
# Parse initial_protein_sequences (always a string in simplified version)
|
|
49
|
+
try:
|
|
50
|
+
initial_protein_sequences = json.loads(initial_protein_sequences)
|
|
51
|
+
except (json.JSONDecodeError, ValueError):
|
|
52
|
+
# Try to parse as comma-separated
|
|
53
|
+
if ',' in initial_protein_sequences:
|
|
54
|
+
initial_protein_sequences = [s.strip() for s in initial_protein_sequences.split(',') if s.strip()]
|
|
55
|
+
else:
|
|
56
|
+
initial_protein_sequences = [initial_protein_sequences.strip()]
|
|
57
|
+
|
|
58
|
+
# Parse initial_smiles_list if provided
|
|
59
|
+
parsed_initial_smiles_list = None
|
|
60
|
+
if initial_smiles_list:
|
|
61
|
+
try:
|
|
62
|
+
parsed_initial_smiles_list = json.loads(initial_smiles_list)
|
|
63
|
+
except (json.JSONDecodeError, ValueError):
|
|
64
|
+
# Try to parse as comma-separated
|
|
65
|
+
if ',' in initial_smiles_list:
|
|
66
|
+
parsed_initial_smiles_list = [s.strip() for s in initial_smiles_list.split(',') if s.strip()]
|
|
67
|
+
else:
|
|
68
|
+
parsed_initial_smiles_list = [initial_smiles_list.strip()]
|
|
69
|
+
|
|
70
|
+
# Parse ligand_binding_affinity_index if provided
|
|
71
|
+
parsed_ligand_index = None
|
|
72
|
+
if ligand_binding_affinity_index:
|
|
73
|
+
try:
|
|
74
|
+
parsed_ligand_index = int(ligand_binding_affinity_index)
|
|
75
|
+
except (ValueError, TypeError):
|
|
76
|
+
raise ValueError(f"Invalid ligand_binding_affinity_index: '{ligand_binding_affinity_index}' must be an integer")
|
|
77
|
+
|
|
78
|
+
return rowan.submit_protein_cofolding_workflow(
|
|
79
|
+
initial_protein_sequences=initial_protein_sequences,
|
|
80
|
+
initial_smiles_list=parsed_initial_smiles_list,
|
|
81
|
+
ligand_binding_affinity_index=parsed_ligand_index,
|
|
82
|
+
use_msa_server=use_msa_server,
|
|
83
|
+
use_potentials=use_potentials,
|
|
84
|
+
name=name,
|
|
85
|
+
model=model,
|
|
86
|
+
folder_uuid=folder_uuid if folder_uuid else None,
|
|
87
|
+
max_credits=max_credits if max_credits > 0 else None
|
|
88
|
+
)
|