rowan-mcp 1.0.1__py3-none-any.whl → 2.0.0__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 +2 -2
- rowan_mcp/__main__.py +3 -5
- 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.1.dist-info → rowan_mcp-2.0.0.dist-info}/METADATA +49 -33
- rowan_mcp-2.0.0.dist-info/RECORD +42 -0
- rowan_mcp/functions/admet.py +0 -94
- rowan_mcp/functions/bde.py +0 -113
- rowan_mcp/functions/calculation_retrieve.py +0 -89
- rowan_mcp/functions/conformers.py +0 -80
- rowan_mcp/functions/descriptors.py +0 -92
- rowan_mcp/functions/docking.py +0 -340
- rowan_mcp/functions/docking_enhanced.py +0 -174
- rowan_mcp/functions/electronic_properties.py +0 -205
- rowan_mcp/functions/folder_management.py +0 -137
- rowan_mcp/functions/fukui.py +0 -219
- rowan_mcp/functions/hydrogen_bond_basicity.py +0 -94
- rowan_mcp/functions/irc.py +0 -125
- rowan_mcp/functions/macropka.py +0 -120
- rowan_mcp/functions/molecular_converter.py +0 -423
- rowan_mcp/functions/molecular_dynamics.py +0 -191
- rowan_mcp/functions/molecule_lookup.py +0 -57
- rowan_mcp/functions/multistage_opt.py +0 -171
- rowan_mcp/functions/pdb_handler.py +0 -200
- rowan_mcp/functions/pka.py +0 -137
- rowan_mcp/functions/redox_potential.py +0 -352
- rowan_mcp/functions/scan.py +0 -536
- rowan_mcp/functions/scan_analyzer.py +0 -347
- rowan_mcp/functions/solubility.py +0 -277
- rowan_mcp/functions/spin_states.py +0 -747
- rowan_mcp/functions/system_management.py +0 -368
- rowan_mcp/functions/tautomers.py +0 -91
- rowan_mcp/functions/workflow_management.py +0 -422
- rowan_mcp-1.0.1.dist-info/RECORD +0 -34
- {rowan_mcp-1.0.1.dist-info → rowan_mcp-2.0.0.dist-info}/WHEEL +0 -0
- {rowan_mcp-1.0.1.dist-info → rowan_mcp-2.0.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Rowan v2 API: Conformer Search Workflow
|
|
3
|
+
Search for low-energy molecular conformations using various methods.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any, Dict, Annotated
|
|
7
|
+
import rowan
|
|
8
|
+
import stjames
|
|
9
|
+
|
|
10
|
+
def submit_conformer_search_workflow(
|
|
11
|
+
initial_molecule: Annotated[str, "SMILES string representing the initial structure"],
|
|
12
|
+
conf_gen_mode: Annotated[str, "Conformer generation mode: 'rapid' (fast), 'careful' (balanced), or 'meticulous' (thorough)"] = "rapid",
|
|
13
|
+
final_method: Annotated[str, "Final optimization method (e.g., 'aimnet2_wb97md3', 'r2scan_3c', 'wb97x-d3_def2-tzvp')"] = "aimnet2_wb97md3",
|
|
14
|
+
solvent: Annotated[str, "Solvent for implicit solvation (SMILES or name). Empty string for vacuum"] = "",
|
|
15
|
+
transition_state: Annotated[bool, "Whether to search for transition state conformers"] = False,
|
|
16
|
+
name: Annotated[str, "Workflow name for identification and tracking"] = "Conformer Search 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 conformer search workflow using Rowan v2 API.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
initial_molecule: SMILES string representing the initial structure
|
|
24
|
+
conf_gen_mode: Conformer generation mode: 'rapid' (fast), 'careful' (balanced), or 'meticulous' (thorough)
|
|
25
|
+
final_method: Final optimization method (e.g., 'aimnet2_wb97md3', 'r2scan_3c', 'wb97x-d3_def2-tzvp')
|
|
26
|
+
solvent: Solvent for implicit solvation (e.g., 'water', 'ethanol', 'dmso'). Empty string for gas phase.
|
|
27
|
+
transition_state: Whether to search for transition state conformers
|
|
28
|
+
name: Workflow name for identification and tracking
|
|
29
|
+
folder_uuid: UUID of folder to organize this workflow. Empty string uses default folder.
|
|
30
|
+
max_credits: Maximum credits to spend on this calculation. 0 for no limit.
|
|
31
|
+
|
|
32
|
+
Conformer Generation Modes:
|
|
33
|
+
- 'rapid': RDKit/MMFF, 300 conformers, 0.10 Å RMSD cutoff (recommended for most work)
|
|
34
|
+
- 'careful': CREST/GFN-FF quick mode, 150 conformers max
|
|
35
|
+
- 'meticulous': CREST/GFN2-xTB normal mode, 500 conformers max
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Workflow object representing the submitted workflow
|
|
39
|
+
|
|
40
|
+
Examples:
|
|
41
|
+
# Simple diethyl ether conformer search
|
|
42
|
+
result = submit_conformer_search_workflow(
|
|
43
|
+
initial_molecule="CCOCC"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Basic butane conformer search with rapid mode
|
|
47
|
+
result = submit_conformer_search_workflow(
|
|
48
|
+
initial_molecule="CCCC",
|
|
49
|
+
conf_gen_mode="rapid"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# Careful search with solvent
|
|
53
|
+
result = submit_conformer_search_workflow(
|
|
54
|
+
initial_molecule="CC(C)CC(=O)O",
|
|
55
|
+
conf_gen_mode="careful",
|
|
56
|
+
solvent="water",
|
|
57
|
+
final_method="r2scan_3c"
|
|
58
|
+
)
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
# Convert initial_molecule to appropriate format
|
|
63
|
+
if isinstance(initial_molecule, str):
|
|
64
|
+
# Try to parse as SMILES
|
|
65
|
+
try:
|
|
66
|
+
mol = stjames.Molecule.from_smiles(initial_molecule)
|
|
67
|
+
initial_molecule = mol.model_dump()
|
|
68
|
+
except Exception:
|
|
69
|
+
# If fails, pass as dict with smiles
|
|
70
|
+
initial_molecule = {"smiles": initial_molecule}
|
|
71
|
+
elif isinstance(initial_molecule, dict):
|
|
72
|
+
# Already in dict format
|
|
73
|
+
pass
|
|
74
|
+
elif hasattr(initial_molecule, 'model_dump'):
|
|
75
|
+
# StJamesMolecule
|
|
76
|
+
initial_molecule = initial_molecule.model_dump()
|
|
77
|
+
else:
|
|
78
|
+
# Assume RdkitMol or similar
|
|
79
|
+
try:
|
|
80
|
+
from rdkit import Chem
|
|
81
|
+
mol = stjames.Molecule.from_rdkit(initial_molecule, cid=0)
|
|
82
|
+
initial_molecule = mol.model_dump()
|
|
83
|
+
except:
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
# Convert final_method to Method if it's a string
|
|
87
|
+
if isinstance(final_method, str):
|
|
88
|
+
final_method = stjames.Method(final_method)
|
|
89
|
+
|
|
90
|
+
# Determine solvent model based on method
|
|
91
|
+
solvent_model = None
|
|
92
|
+
if solvent:
|
|
93
|
+
solvent_model = "alpb" if final_method in stjames.XTB_METHODS else "cpcm"
|
|
94
|
+
|
|
95
|
+
# Create optimization settings following official API
|
|
96
|
+
opt_settings = stjames.Settings(
|
|
97
|
+
method=final_method,
|
|
98
|
+
tasks=["optimize"],
|
|
99
|
+
mode=stjames.Mode.AUTO,
|
|
100
|
+
solvent_settings={"solvent": solvent, "model": solvent_model} if solvent else None,
|
|
101
|
+
opt_settings={"transition_state": transition_state, "constraints": []},
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Create MultiStageOptSettings
|
|
105
|
+
msos = stjames.MultiStageOptSettings(
|
|
106
|
+
mode=stjames.Mode.MANUAL,
|
|
107
|
+
xtb_preopt=True,
|
|
108
|
+
optimization_settings=[opt_settings],
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Convert to dict and fix the soscf field
|
|
112
|
+
msos_dict = msos.model_dump()
|
|
113
|
+
|
|
114
|
+
# Fix soscf: convert boolean to string enum
|
|
115
|
+
if 'optimization_settings' in msos_dict:
|
|
116
|
+
for opt_setting in msos_dict['optimization_settings']:
|
|
117
|
+
if 'scf_settings' in opt_setting and 'soscf' in opt_setting['scf_settings']:
|
|
118
|
+
soscf_val = opt_setting['scf_settings']['soscf']
|
|
119
|
+
if isinstance(soscf_val, bool):
|
|
120
|
+
if soscf_val is False:
|
|
121
|
+
opt_setting['scf_settings']['soscf'] = 'never'
|
|
122
|
+
elif soscf_val is True:
|
|
123
|
+
opt_setting['scf_settings']['soscf'] = 'always'
|
|
124
|
+
elif soscf_val is None:
|
|
125
|
+
# Default to smart behavior
|
|
126
|
+
opt_setting['scf_settings']['soscf'] = 'upon_failure'
|
|
127
|
+
|
|
128
|
+
# Build workflow_data
|
|
129
|
+
workflow_data = {
|
|
130
|
+
"multistage_opt_settings": msos_dict,
|
|
131
|
+
"conf_gen_mode": conf_gen_mode,
|
|
132
|
+
"mso_mode": "manual",
|
|
133
|
+
"solvent": solvent if solvent else None,
|
|
134
|
+
"transition_state": transition_state,
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
# Build the API request
|
|
138
|
+
data = {
|
|
139
|
+
"name": name,
|
|
140
|
+
"folder_uuid": folder_uuid if folder_uuid else None,
|
|
141
|
+
"workflow_type": "conformer_search",
|
|
142
|
+
"workflow_data": workflow_data,
|
|
143
|
+
"initial_molecule": initial_molecule,
|
|
144
|
+
"max_credits": max_credits if max_credits > 0 else None,
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
# Submit to API
|
|
148
|
+
from rowan.utils import api_client
|
|
149
|
+
from rowan import Workflow
|
|
150
|
+
|
|
151
|
+
with api_client() as client:
|
|
152
|
+
response = client.post("/workflow", json=data)
|
|
153
|
+
response.raise_for_status()
|
|
154
|
+
return Workflow(**response.json())
|
|
155
|
+
|
|
156
|
+
except Exception as e:
|
|
157
|
+
# Re-raise the exception so MCP can handle it
|
|
158
|
+
raise e
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Rowan v2 API: Descriptors Workflow
|
|
3
|
+
Calculate molecular descriptors for QSAR and molecular analysis.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Annotated
|
|
7
|
+
import rowan
|
|
8
|
+
import stjames
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def submit_descriptors_workflow(
|
|
12
|
+
initial_molecule: Annotated[str, "SMILES string of the molecule to calculate descriptors for"],
|
|
13
|
+
name: Annotated[str, "Workflow name for identification and tracking"] = "Descriptors Workflow",
|
|
14
|
+
folder_uuid: Annotated[str, "UUID of folder to organize this workflow. Empty string uses default folder"] = "",
|
|
15
|
+
max_credits: Annotated[int, "Maximum credits to spend on this calculation. 0 for no limit"] = 0
|
|
16
|
+
):
|
|
17
|
+
"""Submit a molecular descriptors calculation workflow using Rowan v2 API.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
initial_molecule: SMILES string or molecule object for descriptor calculation
|
|
21
|
+
name: Workflow name for identification and tracking
|
|
22
|
+
folder_uuid: UUID of folder to organize this workflow. Empty string uses default folder.
|
|
23
|
+
max_credits: Maximum credits to spend on this calculation. 0 for no limit.
|
|
24
|
+
|
|
25
|
+
Calculates a comprehensive set of molecular descriptors including:
|
|
26
|
+
- Physical properties (MW, logP, TPSA, etc.)
|
|
27
|
+
- Electronic properties (HOMO/LUMO, dipole moment, etc.)
|
|
28
|
+
- Structural features (rotatable bonds, H-bond donors/acceptors, etc.)
|
|
29
|
+
- Topological indices
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Workflow object representing the submitted workflow
|
|
33
|
+
|
|
34
|
+
Example:
|
|
35
|
+
# Basic descriptor calculation
|
|
36
|
+
result = submit_descriptors_workflow(
|
|
37
|
+
initial_molecule="CC(=O)Nc1ccc(O)cc1"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# For complex molecule
|
|
41
|
+
result = submit_descriptors_workflow(
|
|
42
|
+
initial_molecule="CN1C=NC2=C1C(=O)N(C(=O)N2C)C",
|
|
43
|
+
name="Caffeine Descriptors"
|
|
44
|
+
)
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
return rowan.submit_descriptors_workflow(
|
|
48
|
+
initial_molecule=stjames.Molecule.from_smiles(initial_molecule),
|
|
49
|
+
name=name,
|
|
50
|
+
folder_uuid=folder_uuid if folder_uuid else None,
|
|
51
|
+
max_credits=max_credits if max_credits > 0 else None
|
|
52
|
+
)
|
|
@@ -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
|
+
)
|