rowan-mcp 0.1.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 +14 -0
- rowan_mcp/__main__.py +14 -0
- rowan_mcp/functions/admet.py +94 -0
- rowan_mcp/functions/bde.py +113 -0
- rowan_mcp/functions/calculation_retrieve.py +89 -0
- rowan_mcp/functions/conformers.py +135 -0
- rowan_mcp/functions/descriptors.py +92 -0
- rowan_mcp/functions/docking.py +340 -0
- rowan_mcp/functions/docking_enhanced.py +174 -0
- rowan_mcp/functions/electronic_properties.py +263 -0
- rowan_mcp/functions/folder_management.py +137 -0
- rowan_mcp/functions/fukui.py +355 -0
- rowan_mcp/functions/hydrogen_bond_basicity.py +94 -0
- rowan_mcp/functions/irc.py +125 -0
- rowan_mcp/functions/macropka.py +195 -0
- rowan_mcp/functions/molecular_converter.py +423 -0
- rowan_mcp/functions/molecular_dynamics.py +191 -0
- rowan_mcp/functions/molecule_cache.db +0 -0
- rowan_mcp/functions/molecule_lookup.py +446 -0
- rowan_mcp/functions/multistage_opt.py +171 -0
- rowan_mcp/functions/pdb_handler.py +200 -0
- rowan_mcp/functions/pka.py +137 -0
- rowan_mcp/functions/redox_potential.py +352 -0
- rowan_mcp/functions/scan.py +536 -0
- rowan_mcp/functions/scan_analyzer.py +347 -0
- rowan_mcp/functions/solubility.py +277 -0
- rowan_mcp/functions/spin_states.py +747 -0
- rowan_mcp/functions/system_management.py +368 -0
- rowan_mcp/functions/tautomers.py +91 -0
- rowan_mcp/functions/workflow_management.py +422 -0
- rowan_mcp/server.py +169 -0
- rowan_mcp-0.1.0.dist-info/METADATA +216 -0
- rowan_mcp-0.1.0.dist-info/RECORD +35 -0
- rowan_mcp-0.1.0.dist-info/WHEEL +4 -0
- rowan_mcp-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Rowan multistage optimization function for geometry optimization.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import logging
|
|
7
|
+
import time
|
|
8
|
+
from typing import Optional, List, Dict, Any
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
import rowan
|
|
12
|
+
except ImportError:
|
|
13
|
+
rowan = None
|
|
14
|
+
|
|
15
|
+
# Setup logging
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
# Setup API key
|
|
19
|
+
api_key = os.getenv("ROWAN_API_KEY")
|
|
20
|
+
if rowan and api_key:
|
|
21
|
+
rowan.api_key = api_key
|
|
22
|
+
|
|
23
|
+
def log_rowan_api_call(workflow_type: str, **kwargs):
|
|
24
|
+
"""Log Rowan API calls with detailed parameters."""
|
|
25
|
+
|
|
26
|
+
# Special handling for long-running calculations
|
|
27
|
+
if workflow_type in ["multistage_opt", "conformer_search"]:
|
|
28
|
+
ping_interval = kwargs.get('ping_interval', 5)
|
|
29
|
+
blocking = kwargs.get('blocking', True)
|
|
30
|
+
if blocking:
|
|
31
|
+
if workflow_type == "multistage_opt":
|
|
32
|
+
logger.info(f" Multi-stage optimization may take several minutes...")
|
|
33
|
+
else:
|
|
34
|
+
logger.info(f" Conformer search may take several minutes...")
|
|
35
|
+
logger.info(f" Progress will be checked every {ping_interval} seconds")
|
|
36
|
+
else:
|
|
37
|
+
logger.info(f" {workflow_type.replace('_', ' ').title()} submitted without waiting")
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
start_time = time.time()
|
|
41
|
+
result = rowan.compute(workflow_type=workflow_type, **kwargs)
|
|
42
|
+
api_time = time.time() - start_time
|
|
43
|
+
|
|
44
|
+
if isinstance(result, dict) and 'uuid' in result:
|
|
45
|
+
job_status = result.get('status', result.get('object_status', 'Unknown'))
|
|
46
|
+
status_names = {0: "Queued", 1: "Running", 2: "Completed", 3: "Failed", 4: "Stopped", 5: "Awaiting Queue"}
|
|
47
|
+
status_text = status_names.get(job_status, f"Unknown ({job_status})")
|
|
48
|
+
|
|
49
|
+
return result
|
|
50
|
+
|
|
51
|
+
except Exception as e:
|
|
52
|
+
api_time = time.time() - start_time
|
|
53
|
+
raise e
|
|
54
|
+
|
|
55
|
+
def rowan_multistage_opt(
|
|
56
|
+
name: str,
|
|
57
|
+
molecule: str,
|
|
58
|
+
mode: str = "rapid",
|
|
59
|
+
solvent: Optional[str] = None,
|
|
60
|
+
xtb_preopt: bool = False,
|
|
61
|
+
constraints: Optional[List[Dict[str, Any]]] = None,
|
|
62
|
+
transition_state: bool = False,
|
|
63
|
+
frequencies: bool = False,
|
|
64
|
+
folder_uuid: Optional[str] = None,
|
|
65
|
+
blocking: bool = True,
|
|
66
|
+
ping_interval: int = 30
|
|
67
|
+
) -> str:
|
|
68
|
+
"""Run multi-level geometry optimization using stjames MultiStageOptWorkflow.
|
|
69
|
+
|
|
70
|
+
Performs hierarchical optimization using multiple levels of theory based on mode. There are only four valid modes:
|
|
71
|
+
|
|
72
|
+
**RAPID** (default): GFN2-xTB optimization → r²SCAN-3c single point
|
|
73
|
+
**RECKLESS**: GFN-FF optimization → GFN2-xTB single point
|
|
74
|
+
**CAREFUL**: GFN2-xTB preopt → r²SCAN-3c optimization → ωB97X-3c single point
|
|
75
|
+
**METICULOUS**: GFN2-xTB preopt → r²SCAN-3c opt → ωB97X-3c opt → ωB97M-D3BJ/def2-TZVPPD single point
|
|
76
|
+
|
|
77
|
+
This follows the exact stjames MultiStageOptWorkflow implementation for:
|
|
78
|
+
- High accuracy final structures
|
|
79
|
+
- Efficient computational cost
|
|
80
|
+
- Reliable convergence across chemical space
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
name: Name for the calculation
|
|
84
|
+
molecule: Molecule SMILES string
|
|
85
|
+
mode: Optimization mode - "rapid", "reckless", "careful", or "meticulous" (default: "rapid")
|
|
86
|
+
solvent: Solvent for single point calculation (e.g., "water", "dmso", "acetone")
|
|
87
|
+
xtb_preopt: Whether to include xTB pre-optimization step (default: False)
|
|
88
|
+
constraints: List of optimization constraints (default: None)
|
|
89
|
+
transition_state: Whether this is a transition state optimization (default: False)
|
|
90
|
+
frequencies: Whether to calculate vibrational frequencies (default: False)
|
|
91
|
+
folder_uuid: Optional folder UUID for organization
|
|
92
|
+
blocking: Whether to wait for completion (default: True)
|
|
93
|
+
ping_interval: Check status interval in seconds (default: 30)
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Comprehensive optimization results following MultiStageOptWorkflow format
|
|
97
|
+
|
|
98
|
+
Example:
|
|
99
|
+
# Basic optimization
|
|
100
|
+
result = rowan_multistage_opt("aspirin_opt", "CC(=O)Oc1ccccc1C(=O)O")
|
|
101
|
+
|
|
102
|
+
# With solvent and frequency analysis
|
|
103
|
+
result = rowan_multistage_opt(
|
|
104
|
+
"aspirin_water",
|
|
105
|
+
"CC(=O)Oc1ccccc1C(=O)O",
|
|
106
|
+
mode="careful",
|
|
107
|
+
solvent="water",
|
|
108
|
+
frequencies=True
|
|
109
|
+
)
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
# Validate mode parameter - only allow Rowan's supported modes
|
|
113
|
+
valid_modes = ["rapid", "reckless", "careful", "meticulous"]
|
|
114
|
+
if mode.lower() not in valid_modes:
|
|
115
|
+
error_msg = f"Invalid mode '{mode}'. Must be one of: {', '.join(valid_modes)}"
|
|
116
|
+
logger.error(f"Mode validation failed: {error_msg}")
|
|
117
|
+
return f"Error: {error_msg}"
|
|
118
|
+
|
|
119
|
+
# Prepare parameters following stjames MultiStageOptWorkflow structure
|
|
120
|
+
params = {
|
|
121
|
+
"name": name,
|
|
122
|
+
"molecule": molecule,
|
|
123
|
+
"initial_molecule": molecule, # Required by MultiStageOptWorkflow
|
|
124
|
+
"mode": mode.lower(),
|
|
125
|
+
"folder_uuid": folder_uuid,
|
|
126
|
+
"blocking": blocking,
|
|
127
|
+
"ping_interval": ping_interval
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
# Add optional parameters if specified
|
|
131
|
+
if solvent:
|
|
132
|
+
params["solvent"] = solvent
|
|
133
|
+
|
|
134
|
+
if xtb_preopt:
|
|
135
|
+
params["xtb_preopt"] = xtb_preopt
|
|
136
|
+
|
|
137
|
+
if constraints:
|
|
138
|
+
params["constraints"] = constraints
|
|
139
|
+
|
|
140
|
+
if transition_state:
|
|
141
|
+
params["transition_state"] = transition_state
|
|
142
|
+
|
|
143
|
+
if frequencies:
|
|
144
|
+
params["frequencies"] = frequencies
|
|
145
|
+
|
|
146
|
+
# Submit to Rowan using multistage_opt workflow
|
|
147
|
+
result = log_rowan_api_call(
|
|
148
|
+
workflow_type="multistage_opt",
|
|
149
|
+
**params
|
|
150
|
+
)
|
|
151
|
+
return str(result)
|
|
152
|
+
|
|
153
|
+
def test_rowan_multistage_opt():
|
|
154
|
+
"""Test the rowan_multistage_opt function with stjames parameters."""
|
|
155
|
+
try:
|
|
156
|
+
# Test with stjames-compatible parameters
|
|
157
|
+
result = rowan_multistage_opt(
|
|
158
|
+
name="test_stjames_opt",
|
|
159
|
+
molecule="CCO",
|
|
160
|
+
mode="rapid",
|
|
161
|
+
blocking=False
|
|
162
|
+
)
|
|
163
|
+
print("✅ Multistage optimization test successful!")
|
|
164
|
+
print(f"Result length: {len(result)} characters")
|
|
165
|
+
return True
|
|
166
|
+
except Exception as e:
|
|
167
|
+
print(f"Multistage optimization test failed: {e}")
|
|
168
|
+
return False
|
|
169
|
+
|
|
170
|
+
if __name__ == "__main__":
|
|
171
|
+
test_rowan_multistage_opt()
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PDB handler for Rowan docking - attempts to handle PDB input intelligently
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import logging
|
|
7
|
+
import requests
|
|
8
|
+
from typing import Optional, Dict, Any, Tuple
|
|
9
|
+
import rowan
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
def parse_pdb_content(pdb_content: str) -> Dict[str, Any]:
|
|
14
|
+
"""
|
|
15
|
+
Parse PDB content into a structured format that might work with Rowan.
|
|
16
|
+
|
|
17
|
+
This attempts to extract key information from PDB files that the
|
|
18
|
+
Rowan API might accept.
|
|
19
|
+
"""
|
|
20
|
+
lines = pdb_content.split('\n')
|
|
21
|
+
|
|
22
|
+
# Extract basic information
|
|
23
|
+
header = ""
|
|
24
|
+
title = ""
|
|
25
|
+
atoms = []
|
|
26
|
+
|
|
27
|
+
for line in lines:
|
|
28
|
+
if line.startswith("HEADER"):
|
|
29
|
+
header = line[10:50].strip()
|
|
30
|
+
elif line.startswith("TITLE"):
|
|
31
|
+
title += line[10:].strip() + " "
|
|
32
|
+
elif line.startswith("ATOM") or line.startswith("HETATM"):
|
|
33
|
+
atoms.append(line)
|
|
34
|
+
|
|
35
|
+
# Try to create a structure that might work with Rowan's PDB format
|
|
36
|
+
pdb_data = {
|
|
37
|
+
"description": {
|
|
38
|
+
"title": title.strip() or header,
|
|
39
|
+
"classification": header
|
|
40
|
+
},
|
|
41
|
+
"experiment": {
|
|
42
|
+
"technique": "X-RAY DIFFRACTION" # Default assumption
|
|
43
|
+
},
|
|
44
|
+
"geometry": {},
|
|
45
|
+
"contents": pdb_content, # Keep full content
|
|
46
|
+
"name": f"pdb_upload_{len(pdb_content)}"
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return pdb_data
|
|
50
|
+
|
|
51
|
+
def attempt_pdb_upload(pdb_id: str, pdb_content: str, folder_uuid: Optional[str] = None) -> Tuple[Optional[str], str]:
|
|
52
|
+
"""
|
|
53
|
+
Attempt to upload PDB content to Rowan.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Tuple of (uuid or None, status message)
|
|
57
|
+
"""
|
|
58
|
+
try:
|
|
59
|
+
# First, try the Protein.create method if it exists
|
|
60
|
+
if hasattr(rowan, 'Protein') and hasattr(rowan.Protein, 'create'):
|
|
61
|
+
logger.info(f"Attempting to create protein from PDB {pdb_id}")
|
|
62
|
+
result = rowan.Protein.create(
|
|
63
|
+
name=f"{pdb_id}_upload",
|
|
64
|
+
pdb_content=pdb_content,
|
|
65
|
+
folder_uuid=folder_uuid
|
|
66
|
+
)
|
|
67
|
+
if result and 'uuid' in result:
|
|
68
|
+
return result['uuid'], f"Successfully uploaded PDB {pdb_id}"
|
|
69
|
+
|
|
70
|
+
# Try alternative upload methods
|
|
71
|
+
if hasattr(rowan, 'upload_pdb'):
|
|
72
|
+
result = rowan.upload_pdb(pdb_content, name=pdb_id)
|
|
73
|
+
if result and 'uuid' in result:
|
|
74
|
+
return result['uuid'], f"Successfully uploaded PDB {pdb_id}"
|
|
75
|
+
|
|
76
|
+
# Try to create a file upload
|
|
77
|
+
if hasattr(rowan, 'File') and hasattr(rowan.File, 'create'):
|
|
78
|
+
result = rowan.File.create(
|
|
79
|
+
name=f"{pdb_id}.pdb",
|
|
80
|
+
content=pdb_content,
|
|
81
|
+
file_type="pdb",
|
|
82
|
+
folder_uuid=folder_uuid
|
|
83
|
+
)
|
|
84
|
+
if result and 'uuid' in result:
|
|
85
|
+
return result['uuid'], f"Uploaded PDB {pdb_id} as file"
|
|
86
|
+
|
|
87
|
+
except Exception as e:
|
|
88
|
+
logger.error(f"Failed to upload PDB {pdb_id}: {str(e)}")
|
|
89
|
+
|
|
90
|
+
return None, f"Cannot upload PDB through API - manual upload required"
|
|
91
|
+
|
|
92
|
+
def handle_pdb_input(pdb_input: str, folder_uuid: Optional[str] = None) -> Dict[str, Any]:
|
|
93
|
+
"""
|
|
94
|
+
Handle PDB input (either PDB ID or UUID) and return information for docking.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
pdb_input: Either a PDB ID (like "4Z18") or a UUID
|
|
98
|
+
folder_uuid: Optional folder for uploads
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Dictionary with:
|
|
102
|
+
- success: bool
|
|
103
|
+
- uuid: str (if successful)
|
|
104
|
+
- message: str (status/error message)
|
|
105
|
+
- pdb_id: str (if it was a PDB ID)
|
|
106
|
+
- instructions: str (what to do next)
|
|
107
|
+
"""
|
|
108
|
+
# Check if it's a UUID (36 chars with dashes)
|
|
109
|
+
if len(pdb_input) == 36 and pdb_input.count('-') == 4:
|
|
110
|
+
return {
|
|
111
|
+
"success": True,
|
|
112
|
+
"uuid": pdb_input,
|
|
113
|
+
"message": "Using provided protein UUID",
|
|
114
|
+
"instructions": None
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
# Assume it's a PDB ID
|
|
118
|
+
pdb_id = pdb_input.upper()
|
|
119
|
+
|
|
120
|
+
# Fetch PDB content
|
|
121
|
+
try:
|
|
122
|
+
url = f"https://files.rcsb.org/download/{pdb_id}.pdb"
|
|
123
|
+
response = requests.get(url, timeout=30)
|
|
124
|
+
response.raise_for_status()
|
|
125
|
+
pdb_content = response.text
|
|
126
|
+
logger.info(f"Successfully fetched PDB {pdb_id} ({len(pdb_content)} characters)")
|
|
127
|
+
except Exception as e:
|
|
128
|
+
return {
|
|
129
|
+
"success": False,
|
|
130
|
+
"uuid": None,
|
|
131
|
+
"message": f"Failed to fetch PDB {pdb_id}: {str(e)}",
|
|
132
|
+
"pdb_id": pdb_id,
|
|
133
|
+
"instructions": "Please verify the PDB ID is correct"
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
# Attempt to upload
|
|
137
|
+
uuid, upload_status = attempt_pdb_upload(pdb_id, pdb_content, folder_uuid)
|
|
138
|
+
|
|
139
|
+
if uuid:
|
|
140
|
+
return {
|
|
141
|
+
"success": True,
|
|
142
|
+
"uuid": uuid,
|
|
143
|
+
"message": upload_status,
|
|
144
|
+
"pdb_id": pdb_id,
|
|
145
|
+
"instructions": None
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
# Upload failed - provide instructions
|
|
149
|
+
instructions = f"""
|
|
150
|
+
To use PDB {pdb_id} for docking:
|
|
151
|
+
|
|
152
|
+
1. Download the PDB file:
|
|
153
|
+
https://files.rcsb.org/download/{pdb_id}.pdb
|
|
154
|
+
|
|
155
|
+
2. Upload to your Rowan account:
|
|
156
|
+
https://labs.rowansci.com
|
|
157
|
+
|
|
158
|
+
3. Get the protein UUID from your account
|
|
159
|
+
|
|
160
|
+
4. Use the UUID in your docking call:
|
|
161
|
+
rowan_docking(
|
|
162
|
+
target_uuid="your-protein-uuid-here",
|
|
163
|
+
...
|
|
164
|
+
)
|
|
165
|
+
"""
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
"success": False,
|
|
169
|
+
"uuid": None,
|
|
170
|
+
"message": upload_status,
|
|
171
|
+
"pdb_id": pdb_id,
|
|
172
|
+
"instructions": instructions
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
def enhance_docking_with_pdb_handler(
|
|
176
|
+
target_input: Optional[str] = None,
|
|
177
|
+
target_uuid: Optional[str] = None,
|
|
178
|
+
**kwargs
|
|
179
|
+
) -> Tuple[Optional[str], str]:
|
|
180
|
+
"""
|
|
181
|
+
Enhanced target handling for docking that accepts PDB IDs.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
Tuple of (target_uuid or None, message)
|
|
185
|
+
"""
|
|
186
|
+
# If UUID already provided, use it
|
|
187
|
+
if target_uuid:
|
|
188
|
+
return target_uuid, "Using provided target UUID"
|
|
189
|
+
|
|
190
|
+
# If target_input provided, try to handle it
|
|
191
|
+
if target_input:
|
|
192
|
+
result = handle_pdb_input(target_input, kwargs.get('folder_uuid'))
|
|
193
|
+
|
|
194
|
+
if result['success']:
|
|
195
|
+
return result['uuid'], result['message']
|
|
196
|
+
else:
|
|
197
|
+
# Return None but with helpful message
|
|
198
|
+
return None, result['instructions'] or result['message']
|
|
199
|
+
|
|
200
|
+
return None, "No protein target specified"
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Calculate pKa values for molecules using Rowan API.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import rowan
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
# Set up logging
|
|
10
|
+
import logging
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
# Configure rowan API key
|
|
14
|
+
if not hasattr(rowan, 'api_key') or not rowan.api_key:
|
|
15
|
+
api_key = os.getenv("ROWAN_API_KEY")
|
|
16
|
+
if api_key:
|
|
17
|
+
rowan.api_key = api_key
|
|
18
|
+
logger.info("🔑 Rowan API key configured")
|
|
19
|
+
else:
|
|
20
|
+
logger.error("No ROWAN_API_KEY found in environment")
|
|
21
|
+
|
|
22
|
+
def log_rowan_api_call(workflow_type: str, **kwargs):
|
|
23
|
+
"""Log Rowan API calls and let Rowan handle its own errors."""
|
|
24
|
+
|
|
25
|
+
# Simple logging for calculations
|
|
26
|
+
logger.info(f" Starting {workflow_type.replace('_', ' ')}...")
|
|
27
|
+
|
|
28
|
+
# Let Rowan handle everything - no custom error handling
|
|
29
|
+
return rowan.compute(workflow_type=workflow_type, **kwargs)
|
|
30
|
+
|
|
31
|
+
def rowan_pka(
|
|
32
|
+
name: str,
|
|
33
|
+
molecule: str,
|
|
34
|
+
folder_uuid: Optional[str] = None,
|
|
35
|
+
blocking: bool = True,
|
|
36
|
+
ping_interval: int = 5
|
|
37
|
+
) -> str:
|
|
38
|
+
"""Calculate pKa values for molecules.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
name: Name for the calculation
|
|
42
|
+
molecule: Molecule SMILES string or common name
|
|
43
|
+
folder_uuid: UUID of folder to organize calculation in
|
|
44
|
+
blocking: Whether to wait for completion (default: True)
|
|
45
|
+
ping_interval: How often to check status in seconds (default: 5)
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
pKa calculation results
|
|
49
|
+
"""
|
|
50
|
+
try:
|
|
51
|
+
result = log_rowan_api_call(
|
|
52
|
+
workflow_type="pka",
|
|
53
|
+
name=name,
|
|
54
|
+
molecule=molecule,
|
|
55
|
+
folder_uuid=folder_uuid,
|
|
56
|
+
blocking=blocking,
|
|
57
|
+
ping_interval=ping_interval
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Format results based on whether we waited or not
|
|
61
|
+
if blocking:
|
|
62
|
+
# We waited for completion - format actual results
|
|
63
|
+
status = result.get('status', result.get('object_status', 'Unknown'))
|
|
64
|
+
|
|
65
|
+
if status == 2: # Completed successfully
|
|
66
|
+
formatted = f" pKa calculation for '{name}' completed successfully!\n\n"
|
|
67
|
+
elif status == 3: # Failed
|
|
68
|
+
formatted = f" pKa calculation for '{name}' failed!\n\n"
|
|
69
|
+
else:
|
|
70
|
+
formatted = f" pKa calculation for '{name}' finished with status {status}\n\n"
|
|
71
|
+
|
|
72
|
+
formatted += f" Molecule: {molecule}\n"
|
|
73
|
+
formatted += f" Job UUID: {result.get('uuid', 'N/A')}\n"
|
|
74
|
+
formatted += f" Status: {status}\n"
|
|
75
|
+
|
|
76
|
+
# Try to extract pKa results
|
|
77
|
+
if isinstance(result, dict) and 'object_data' in result and result['object_data']:
|
|
78
|
+
data = result['object_data']
|
|
79
|
+
|
|
80
|
+
# Extract pKa values
|
|
81
|
+
if 'strongest_acid' in data:
|
|
82
|
+
if data['strongest_acid'] is not None:
|
|
83
|
+
formatted += f" Strongest Acid pKa: {data['strongest_acid']:.2f}\n"
|
|
84
|
+
else:
|
|
85
|
+
formatted += f" Strongest Acid pKa: N/A (no acidic sites found)\n"
|
|
86
|
+
|
|
87
|
+
if 'strongest_base' in data:
|
|
88
|
+
if data['strongest_base'] is not None:
|
|
89
|
+
formatted += f" Strongest Base pKa: {data['strongest_base']:.2f}\n"
|
|
90
|
+
else:
|
|
91
|
+
formatted += f" Strongest Base pKa: N/A (no basic sites found)\n"
|
|
92
|
+
if 'pka_values' in data and isinstance(data['pka_values'], list):
|
|
93
|
+
formatted += f" All pKa values: {', '.join([f'{val:.2f}' for val in data['pka_values']])}\n"
|
|
94
|
+
|
|
95
|
+
# Additional properties if available
|
|
96
|
+
if 'ionizable_sites' in data:
|
|
97
|
+
formatted += f" Ionizable sites found: {data['ionizable_sites']}\n"
|
|
98
|
+
|
|
99
|
+
# Basic guidance
|
|
100
|
+
if status == 2:
|
|
101
|
+
formatted += f"\n Use rowan_workflow_management(action='retrieve', workflow_uuid='{result.get('uuid')}') for detailed data\n"
|
|
102
|
+
else:
|
|
103
|
+
# Non-blocking mode - just submission confirmation
|
|
104
|
+
formatted = f" pKa calculation for '{name}' submitted!\n\n"
|
|
105
|
+
formatted += f" Molecule: {molecule}\n"
|
|
106
|
+
formatted += f" Job UUID: {result.get('uuid', 'N/A')}\n"
|
|
107
|
+
formatted += f" Status: {result.get('status', 'Submitted')}\n"
|
|
108
|
+
|
|
109
|
+
return formatted
|
|
110
|
+
|
|
111
|
+
except Exception as e:
|
|
112
|
+
error_response = {
|
|
113
|
+
"error": f"pKa calculation failed: {str(e)}",
|
|
114
|
+
"name": name,
|
|
115
|
+
"molecule": molecule
|
|
116
|
+
}
|
|
117
|
+
return str(error_response)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_rowan_pka():
|
|
121
|
+
"""Test the rowan_pka function."""
|
|
122
|
+
try:
|
|
123
|
+
# Test with minimal parameters
|
|
124
|
+
result = rowan_pka(
|
|
125
|
+
name="test_pka_water",
|
|
126
|
+
molecule="O"
|
|
127
|
+
)
|
|
128
|
+
print("✅ pKa test successful!")
|
|
129
|
+
print(f"Result: {result}")
|
|
130
|
+
return True
|
|
131
|
+
except Exception as e:
|
|
132
|
+
print(f"pKa test failed: {e}")
|
|
133
|
+
return False
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
if __name__ == "__main__":
|
|
137
|
+
test_rowan_pka()
|