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,141 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Rowan v2 API: Protein Management
|
|
3
|
+
Tools for creating, retrieving, and managing protein structures.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import List, Dict, Any, Annotated
|
|
7
|
+
import rowan
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def create_protein_from_pdb_id(
|
|
11
|
+
name: Annotated[str, "Name for the protein"],
|
|
12
|
+
code: Annotated[str, "PDB ID code (e.g., '1HCK')"]
|
|
13
|
+
) -> Dict[str, Any]:
|
|
14
|
+
"""Create a protein from a PDB ID.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
name: Name for the protein
|
|
18
|
+
code: PDB ID code (e.g., '1HCK')
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
Dictionary containing protein information
|
|
22
|
+
"""
|
|
23
|
+
protein = rowan.create_protein_from_pdb_id(name=name, code=code)
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
"uuid": protein.uuid,
|
|
27
|
+
"name": protein.name,
|
|
28
|
+
"sanitized": protein.sanitized,
|
|
29
|
+
"created_at": str(protein.created_at) if protein.created_at else None
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def retrieve_protein(
|
|
34
|
+
uuid: Annotated[str, "UUID of the protein to retrieve"]
|
|
35
|
+
) -> Dict[str, Any]:
|
|
36
|
+
"""Retrieve a protein by UUID.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
uuid: UUID of the protein to retrieve
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Dictionary containing the protein data
|
|
43
|
+
"""
|
|
44
|
+
protein = rowan.retrieve_protein(uuid)
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
"uuid": protein.uuid,
|
|
48
|
+
"name": protein.name,
|
|
49
|
+
"sanitized": protein.sanitized,
|
|
50
|
+
"created_at": str(protein.created_at) if protein.created_at else None
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def list_proteins(
|
|
55
|
+
page: Annotated[int, "Page number (0-indexed)"] = 0,
|
|
56
|
+
size: Annotated[int, "Number per page"] = 20
|
|
57
|
+
) -> List[Dict[str, Any]]:
|
|
58
|
+
"""List proteins.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
page: Page number (0-indexed)
|
|
62
|
+
size: Number per page
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
List of protein dictionaries
|
|
66
|
+
"""
|
|
67
|
+
proteins = rowan.list_proteins(page=page, size=size)
|
|
68
|
+
|
|
69
|
+
return [
|
|
70
|
+
{
|
|
71
|
+
"uuid": p.uuid,
|
|
72
|
+
"name": p.name,
|
|
73
|
+
"sanitized": p.sanitized,
|
|
74
|
+
"created_at": str(p.created_at) if p.created_at else None
|
|
75
|
+
}
|
|
76
|
+
for p in proteins
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def upload_protein(
|
|
81
|
+
name: Annotated[str, "Name for the protein"],
|
|
82
|
+
file_path: Annotated[str, "Path to PDB file"]
|
|
83
|
+
) -> Dict[str, Any]:
|
|
84
|
+
"""Upload a protein from a PDB file.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
name: Name for the protein
|
|
88
|
+
file_path: Path to PDB file
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Dictionary containing protein information
|
|
92
|
+
"""
|
|
93
|
+
from pathlib import Path
|
|
94
|
+
protein = rowan.upload_protein(name=name, file_path=Path(file_path))
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
"uuid": protein.uuid,
|
|
98
|
+
"name": protein.name,
|
|
99
|
+
"sanitized": protein.sanitized,
|
|
100
|
+
"created_at": str(protein.created_at) if protein.created_at else None
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def delete_protein(
|
|
105
|
+
uuid: Annotated[str, "UUID of the protein to delete"]
|
|
106
|
+
) -> Dict[str, str]:
|
|
107
|
+
"""Delete a protein.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
uuid: UUID of the protein to delete
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Dictionary with confirmation message
|
|
114
|
+
"""
|
|
115
|
+
protein = rowan.retrieve_protein(uuid)
|
|
116
|
+
protein.delete()
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
"message": f"Protein {uuid} deleted",
|
|
120
|
+
"uuid": uuid
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def sanitize_protein(
|
|
125
|
+
uuid: Annotated[str, "UUID of the protein to sanitize"]
|
|
126
|
+
) -> Dict[str, Any]:
|
|
127
|
+
"""Sanitize a protein for docking.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
uuid: UUID of the protein to sanitize
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Dictionary with sanitization status
|
|
134
|
+
"""
|
|
135
|
+
protein = rowan.retrieve_protein(uuid)
|
|
136
|
+
protein.sanitize()
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
"uuid": uuid,
|
|
140
|
+
"message": f"Protein {uuid} sanitized"
|
|
141
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Rowan v2 API: Basic Calculation Workflow
|
|
3
|
+
Submit basic quantum chemistry calculations with various methods and tasks.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
# Simplified imports - no complex typing needed
|
|
7
|
+
from typing import Annotated
|
|
8
|
+
import rowan
|
|
9
|
+
import stjames
|
|
10
|
+
import json
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def submit_basic_calculation_workflow(
|
|
14
|
+
initial_molecule: Annotated[str, "SMILES string or molecule JSON for quantum chemistry calculation"],
|
|
15
|
+
method: Annotated[str, "Computational method (e.g., 'gfn2-xtb', 'uma_m_omol', 'b3lyp-d3bj')"] = "uma_m_omol",
|
|
16
|
+
tasks: Annotated[str, "JSON array or comma-separated list of tasks (e.g., '[\"optimize\"]', 'optimize, frequencies')"] = "",
|
|
17
|
+
mode: Annotated[str, "Calculation mode: 'rapid', 'careful', 'meticulous', or 'auto'"] = "auto",
|
|
18
|
+
engine: Annotated[str, "Computational engine: 'omol25', 'xtb', 'psi4'"] = "omol25",
|
|
19
|
+
name: Annotated[str, "Workflow name for identification and tracking"] = "Basic Calculation Workflow",
|
|
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
|
+
"""Submit a basic calculation workflow using Rowan v2 API.
|
|
24
|
+
|
|
25
|
+
Performs fundamental quantum chemistry calculations with configurable methods
|
|
26
|
+
and computational tasks. Returns a workflow object for tracking progress.
|
|
27
|
+
|
|
28
|
+
Examples:
|
|
29
|
+
# Simple water optimization with GFN2-xTB
|
|
30
|
+
result = submit_basic_calculation_workflow(
|
|
31
|
+
initial_molecule="O",
|
|
32
|
+
method="gfn2-xtb",
|
|
33
|
+
tasks=["optimize"],
|
|
34
|
+
engine="xtb",
|
|
35
|
+
name="Water Optimization"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# Butane optimization from SMILES
|
|
39
|
+
result = submit_basic_calculation_workflow(
|
|
40
|
+
initial_molecule="CCCC",
|
|
41
|
+
method="gfn2-xtb",
|
|
42
|
+
tasks=["optimize"],
|
|
43
|
+
mode="rapid",
|
|
44
|
+
engine="xtb",
|
|
45
|
+
name="Butane Optimization"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Using a molecule dict (from data.json)
|
|
49
|
+
molecule_dict = {
|
|
50
|
+
"smiles": "CCCC",
|
|
51
|
+
"charge": 0,
|
|
52
|
+
"multiplicity": 1,
|
|
53
|
+
"atoms": [...] # atomic positions
|
|
54
|
+
}
|
|
55
|
+
result = submit_basic_calculation_workflow(
|
|
56
|
+
initial_molecule=molecule_dict,
|
|
57
|
+
method="gfn2-xtb",
|
|
58
|
+
tasks=["optimize"],
|
|
59
|
+
engine="xtb"
|
|
60
|
+
)
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
# Parse tasks parameter - handle string input
|
|
64
|
+
parsed_tasks = None
|
|
65
|
+
if tasks: # If not empty string
|
|
66
|
+
tasks = tasks.strip()
|
|
67
|
+
if tasks.startswith('[') and tasks.endswith(']'):
|
|
68
|
+
# JSON array format like '["optimize"]'
|
|
69
|
+
try:
|
|
70
|
+
parsed_tasks = json.loads(tasks)
|
|
71
|
+
except (json.JSONDecodeError, ValueError):
|
|
72
|
+
# Failed to parse as JSON, try as comma-separated
|
|
73
|
+
tasks = tasks.strip('[]').replace('"', '').replace("'", "")
|
|
74
|
+
parsed_tasks = [t.strip() for t in tasks.split(',') if t.strip()]
|
|
75
|
+
elif ',' in tasks:
|
|
76
|
+
# Comma-separated format like 'optimize, frequencies'
|
|
77
|
+
parsed_tasks = [t.strip() for t in tasks.split(',') if t.strip()]
|
|
78
|
+
else:
|
|
79
|
+
# Single task as string like 'optimize'
|
|
80
|
+
parsed_tasks = [tasks]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
# Handle initial_molecule parameter - could be JSON string, SMILES, or dict
|
|
85
|
+
if isinstance(initial_molecule, str):
|
|
86
|
+
# Check if it's a JSON string (starts with { or [)
|
|
87
|
+
initial_molecule_str = initial_molecule.strip()
|
|
88
|
+
if (initial_molecule_str.startswith('{') and initial_molecule_str.endswith('}')) or \
|
|
89
|
+
(initial_molecule_str.startswith('[') and initial_molecule_str.endswith(']')):
|
|
90
|
+
try:
|
|
91
|
+
# Parse the JSON string to dict
|
|
92
|
+
initial_molecule = json.loads(initial_molecule_str)
|
|
93
|
+
|
|
94
|
+
# Now handle as dict (fall through to dict handling below)
|
|
95
|
+
if isinstance(initial_molecule, dict) and 'smiles' in initial_molecule:
|
|
96
|
+
smiles = initial_molecule.get('smiles')
|
|
97
|
+
if smiles:
|
|
98
|
+
try:
|
|
99
|
+
initial_molecule = stjames.Molecule.from_smiles(smiles)
|
|
100
|
+
except Exception as e:
|
|
101
|
+
initial_molecule = smiles
|
|
102
|
+
except (json.JSONDecodeError, ValueError) as e:
|
|
103
|
+
# Not valid JSON, treat as SMILES string
|
|
104
|
+
try:
|
|
105
|
+
initial_molecule = stjames.Molecule.from_smiles(initial_molecule)
|
|
106
|
+
except Exception as e:
|
|
107
|
+
pass
|
|
108
|
+
else:
|
|
109
|
+
# Regular SMILES string
|
|
110
|
+
try:
|
|
111
|
+
initial_molecule = stjames.Molecule.from_smiles(initial_molecule)
|
|
112
|
+
except Exception as e:
|
|
113
|
+
pass
|
|
114
|
+
elif isinstance(initial_molecule, dict) and 'smiles' in initial_molecule:
|
|
115
|
+
# If we have a dict with SMILES, extract and use just the SMILES
|
|
116
|
+
smiles = initial_molecule.get('smiles')
|
|
117
|
+
if smiles:
|
|
118
|
+
try:
|
|
119
|
+
initial_molecule = stjames.Molecule.from_smiles(smiles)
|
|
120
|
+
except Exception as e:
|
|
121
|
+
initial_molecule = smiles
|
|
122
|
+
|
|
123
|
+
# Convert to appropriate format
|
|
124
|
+
if hasattr(initial_molecule, 'model_dump'):
|
|
125
|
+
initial_molecule_dict = initial_molecule.model_dump()
|
|
126
|
+
elif isinstance(initial_molecule, dict):
|
|
127
|
+
initial_molecule_dict = initial_molecule
|
|
128
|
+
else:
|
|
129
|
+
# Try to convert to StJamesMolecule if it's a string
|
|
130
|
+
try:
|
|
131
|
+
mol = stjames.Molecule.from_smiles(str(initial_molecule))
|
|
132
|
+
initial_molecule_dict = mol.model_dump()
|
|
133
|
+
except:
|
|
134
|
+
# If that fails, pass as-is
|
|
135
|
+
initial_molecule_dict = initial_molecule
|
|
136
|
+
|
|
137
|
+
# Convert method string to Method object to get the correct name
|
|
138
|
+
if isinstance(method, str):
|
|
139
|
+
# Handle common method name variations
|
|
140
|
+
method_map = {
|
|
141
|
+
'gfn2_xtb': 'gfn2-xtb',
|
|
142
|
+
'gfn1_xtb': 'gfn1-xtb',
|
|
143
|
+
'gfn0_xtb': 'gfn0-xtb',
|
|
144
|
+
'r2scan_3c': 'r2scan-3c',
|
|
145
|
+
'wb97x_d3': 'wb97x-d3',
|
|
146
|
+
'wb97m_d3bj': 'wb97m-d3bj',
|
|
147
|
+
'b3lyp_d3bj': 'b3lyp-d3bj',
|
|
148
|
+
'uma_m_omol': 'uma_m_omol', # This one stays the same
|
|
149
|
+
}
|
|
150
|
+
method = method_map.get(method, method)
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
method_obj = stjames.Method(method)
|
|
154
|
+
method_name = method_obj.name
|
|
155
|
+
except:
|
|
156
|
+
# If Method conversion fails, use the string as-is
|
|
157
|
+
method_name = method
|
|
158
|
+
else:
|
|
159
|
+
method_name = method
|
|
160
|
+
|
|
161
|
+
# Use parsed tasks or default
|
|
162
|
+
final_tasks = parsed_tasks if parsed_tasks else ["optimize"]
|
|
163
|
+
|
|
164
|
+
# Build workflow_data following the official API structure
|
|
165
|
+
workflow_data = {
|
|
166
|
+
"settings": {
|
|
167
|
+
"method": method_name,
|
|
168
|
+
"tasks": final_tasks,
|
|
169
|
+
"mode": mode,
|
|
170
|
+
},
|
|
171
|
+
"engine": engine,
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
# Build the API request
|
|
175
|
+
data = {
|
|
176
|
+
"name": name,
|
|
177
|
+
"folder_uuid": folder_uuid if folder_uuid else None,
|
|
178
|
+
"workflow_type": "basic_calculation",
|
|
179
|
+
"workflow_data": workflow_data,
|
|
180
|
+
"initial_molecule": initial_molecule_dict,
|
|
181
|
+
"max_credits": max_credits if max_credits > 0 else None,
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
# Submit directly to API
|
|
185
|
+
from rowan.utils import api_client
|
|
186
|
+
from rowan import Workflow
|
|
187
|
+
|
|
188
|
+
with api_client() as client:
|
|
189
|
+
response = client.post("/workflow", json=data)
|
|
190
|
+
response.raise_for_status()
|
|
191
|
+
return Workflow(**response.json())
|
|
192
|
+
|
|
193
|
+
except Exception as e:
|
|
194
|
+
# Re-raise the exception so MCP can handle it
|
|
195
|
+
raise
|
|
@@ -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
|
+
)
|