rowan-mcp 1.0.2__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 +1 -1
- 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.2.dist-info → rowan_mcp-2.0.0.dist-info}/METADATA +41 -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 -88
- 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.2.dist-info/RECORD +0 -34
- {rowan_mcp-1.0.2.dist-info → rowan_mcp-2.0.0.dist-info}/WHEEL +0 -0
- {rowan_mcp-1.0.2.dist-info → rowan_mcp-2.0.0.dist-info}/entry_points.txt +0 -0
|
@@ -1,347 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Rowan scan analysis functions for MCP tool integration.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
from typing import Optional, Dict, Any, Tuple, List
|
|
6
|
-
import rowan
|
|
7
|
-
|
|
8
|
-
def rowan_scan_analyzer(
|
|
9
|
-
scan_uuid: str,
|
|
10
|
-
action: str = "analyze",
|
|
11
|
-
energy_threshold: Optional[float] = None
|
|
12
|
-
) -> str:
|
|
13
|
-
"""Analyze scan results and extract key geometries for IRC workflows.
|
|
14
|
-
|
|
15
|
-
** Essential IRC Tool:**
|
|
16
|
-
- Analyzes completed scan workflows to extract transition state geometries
|
|
17
|
-
- Provides formatted results ready for IRC calculations
|
|
18
|
-
- Identifies energy maxima, minima, and barriers automatically
|
|
19
|
-
|
|
20
|
-
** Analysis Actions:**
|
|
21
|
-
- **analyze**: Complete analysis with energy profile and key points (default)
|
|
22
|
-
- **extract_ts**: Extract highest energy geometry (TS approximation for IRC)
|
|
23
|
-
- **extract_minima**: Extract low energy geometries
|
|
24
|
-
- **energy_profile**: Show energy vs coordinate data for plotting
|
|
25
|
-
|
|
26
|
-
** IRC Workflow Integration:**
|
|
27
|
-
1. Run scan → get scan_uuid
|
|
28
|
-
2. Use: rowan_scan_analyzer(scan_uuid, "extract_ts")
|
|
29
|
-
3. Copy TS geometry for transition state optimization
|
|
30
|
-
4. Run IRC from optimized TS
|
|
31
|
-
|
|
32
|
-
** Example Usage:**
|
|
33
|
-
- Full analysis: rowan_scan_analyzer("uuid-123", "analyze")
|
|
34
|
-
- Extract TS: rowan_scan_analyzer("uuid-123", "extract_ts")
|
|
35
|
-
- Find minima: rowan_scan_analyzer("uuid-123", "extract_minima", energy_threshold=2.0)
|
|
36
|
-
|
|
37
|
-
Args:
|
|
38
|
-
scan_uuid: UUID of the completed scan workflow to analyze
|
|
39
|
-
action: Analysis type ("analyze", "extract_ts", "extract_minima", "energy_profile")
|
|
40
|
-
energy_threshold: Energy threshold in kcal/mol above minimum for minima extraction (default: None)
|
|
41
|
-
|
|
42
|
-
Returns:
|
|
43
|
-
Analysis results with geometries, energies, and IRC preparation instructions
|
|
44
|
-
"""
|
|
45
|
-
|
|
46
|
-
action = action.lower()
|
|
47
|
-
|
|
48
|
-
try:
|
|
49
|
-
# Retrieve scan workflow results
|
|
50
|
-
workflow = rowan.Workflow.retrieve(uuid=scan_uuid)
|
|
51
|
-
|
|
52
|
-
# Check if workflow is completed
|
|
53
|
-
status = workflow.get('object_status', -1)
|
|
54
|
-
if status != 2: # Not completed
|
|
55
|
-
status_names = {0: "Queued", 1: "Running", 3: "Failed", 4: "Stopped"}
|
|
56
|
-
return f" Scan workflow is not completed. Status: {status_names.get(status, 'Unknown')}"
|
|
57
|
-
|
|
58
|
-
# Extract scan data from object_data
|
|
59
|
-
object_data = workflow.get('object_data', {})
|
|
60
|
-
if not object_data:
|
|
61
|
-
return " No scan data found in workflow results"
|
|
62
|
-
|
|
63
|
-
# Parse scan results
|
|
64
|
-
scan_results = parse_scan_data(object_data)
|
|
65
|
-
if not scan_results:
|
|
66
|
-
return " Could not parse scan data"
|
|
67
|
-
|
|
68
|
-
# Perform requested analysis
|
|
69
|
-
if action == "analyze":
|
|
70
|
-
return format_full_analysis(scan_results, scan_uuid)
|
|
71
|
-
elif action == "extract_ts":
|
|
72
|
-
return extract_ts_geometry(scan_results, scan_uuid)
|
|
73
|
-
elif action == "extract_minima":
|
|
74
|
-
return extract_minima_geometries(scan_results, energy_threshold)
|
|
75
|
-
elif action == "energy_profile":
|
|
76
|
-
return format_energy_profile(scan_results)
|
|
77
|
-
else:
|
|
78
|
-
return f" Unknown action '{action}'. Available: analyze, extract_ts, extract_minima, energy_profile"
|
|
79
|
-
|
|
80
|
-
except Exception as e:
|
|
81
|
-
return f" Error analyzing scan: {str(e)}"
|
|
82
|
-
|
|
83
|
-
def parse_scan_data(object_data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
84
|
-
"""Parse raw scan data into structured format."""
|
|
85
|
-
|
|
86
|
-
try:
|
|
87
|
-
# Look for scan points in various possible locations
|
|
88
|
-
scan_points = None
|
|
89
|
-
|
|
90
|
-
# Try different object_data structures
|
|
91
|
-
if 'scan_points' in object_data:
|
|
92
|
-
scan_points = object_data['scan_points']
|
|
93
|
-
elif 'results' in object_data and isinstance(object_data['results'], list):
|
|
94
|
-
scan_points = object_data['results']
|
|
95
|
-
elif 'calculations' in object_data:
|
|
96
|
-
scan_points = object_data['calculations']
|
|
97
|
-
else:
|
|
98
|
-
# Try to find array-like data
|
|
99
|
-
for key, value in object_data.items():
|
|
100
|
-
if isinstance(value, list) and len(value) > 1:
|
|
101
|
-
scan_points = value
|
|
102
|
-
break
|
|
103
|
-
|
|
104
|
-
if not scan_points:
|
|
105
|
-
return None
|
|
106
|
-
|
|
107
|
-
# Structure the scan data
|
|
108
|
-
parsed_data = {
|
|
109
|
-
'points': [],
|
|
110
|
-
'energies': [],
|
|
111
|
-
'coordinates': [],
|
|
112
|
-
'geometries': []
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
for i, point in enumerate(scan_points):
|
|
116
|
-
if isinstance(point, dict):
|
|
117
|
-
# Extract energy
|
|
118
|
-
energy = point.get('energy') or point.get('total_energy') or point.get('scf_energy')
|
|
119
|
-
if energy is not None:
|
|
120
|
-
parsed_data['energies'].append(energy)
|
|
121
|
-
|
|
122
|
-
# Extract coordinate value
|
|
123
|
-
coord_val = point.get('coordinate_value') or point.get('scan_coordinate') or i
|
|
124
|
-
parsed_data['coordinates'].append(coord_val)
|
|
125
|
-
|
|
126
|
-
# Extract geometry (XYZ coordinates)
|
|
127
|
-
geometry = extract_geometry_from_point(point)
|
|
128
|
-
parsed_data['geometries'].append(geometry)
|
|
129
|
-
|
|
130
|
-
parsed_data['points'].append({
|
|
131
|
-
'index': i,
|
|
132
|
-
'energy': energy,
|
|
133
|
-
'coordinate': coord_val,
|
|
134
|
-
'geometry': geometry
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
return parsed_data if parsed_data['energies'] else None
|
|
138
|
-
|
|
139
|
-
except Exception as e:
|
|
140
|
-
print(f"Error parsing scan data: {e}")
|
|
141
|
-
return None
|
|
142
|
-
|
|
143
|
-
def extract_geometry_from_point(point_data: Dict[str, Any]) -> Optional[str]:
|
|
144
|
-
"""Extract XYZ geometry from a scan point."""
|
|
145
|
-
|
|
146
|
-
try:
|
|
147
|
-
# Look for geometry in various formats
|
|
148
|
-
if 'geometry' in point_data:
|
|
149
|
-
geom = point_data['geometry']
|
|
150
|
-
if isinstance(geom, str):
|
|
151
|
-
return geom
|
|
152
|
-
elif isinstance(geom, dict) and 'xyz' in geom:
|
|
153
|
-
return geom['xyz']
|
|
154
|
-
|
|
155
|
-
# Look for molecule object
|
|
156
|
-
if 'molecule' in point_data:
|
|
157
|
-
mol = point_data['molecule']
|
|
158
|
-
if isinstance(mol, dict) and 'geometry' in mol:
|
|
159
|
-
return mol['geometry']
|
|
160
|
-
|
|
161
|
-
# Look for atoms/coordinates
|
|
162
|
-
if 'atoms' in point_data:
|
|
163
|
-
atoms = point_data['atoms']
|
|
164
|
-
if isinstance(atoms, list):
|
|
165
|
-
return format_atoms_to_xyz(atoms)
|
|
166
|
-
|
|
167
|
-
return None
|
|
168
|
-
|
|
169
|
-
except Exception:
|
|
170
|
-
return None
|
|
171
|
-
|
|
172
|
-
def format_atoms_to_xyz(atoms: List[Dict]) -> str:
|
|
173
|
-
"""Convert atoms list to XYZ format."""
|
|
174
|
-
|
|
175
|
-
try:
|
|
176
|
-
xyz_lines = [str(len(atoms)), ""] # Number of atoms + comment line
|
|
177
|
-
|
|
178
|
-
for atom in atoms:
|
|
179
|
-
symbol = atom.get('symbol') or atom.get('element')
|
|
180
|
-
coords = atom.get('coordinates') or atom.get('position') or [0, 0, 0]
|
|
181
|
-
|
|
182
|
-
if symbol and len(coords) >= 3:
|
|
183
|
-
xyz_lines.append(f"{symbol:2s} {coords[0]:12.6f} {coords[1]:12.6f} {coords[2]:12.6f}")
|
|
184
|
-
|
|
185
|
-
return '\n'.join(xyz_lines)
|
|
186
|
-
|
|
187
|
-
except Exception:
|
|
188
|
-
return ""
|
|
189
|
-
|
|
190
|
-
def extract_ts_geometry(scan_results: Dict[str, Any], scan_uuid: str) -> str:
|
|
191
|
-
"""Extract the highest energy geometry (TS approximation)."""
|
|
192
|
-
|
|
193
|
-
energies = scan_results['energies']
|
|
194
|
-
geometries = scan_results['geometries']
|
|
195
|
-
coordinates = scan_results['coordinates']
|
|
196
|
-
|
|
197
|
-
if not energies:
|
|
198
|
-
return " No energy data found in scan results"
|
|
199
|
-
|
|
200
|
-
# Find highest energy point
|
|
201
|
-
max_energy_idx = energies.index(max(energies))
|
|
202
|
-
max_energy = energies[max_energy_idx]
|
|
203
|
-
max_coord = coordinates[max_energy_idx]
|
|
204
|
-
ts_geometry = geometries[max_energy_idx]
|
|
205
|
-
|
|
206
|
-
# Calculate relative energy (kcal/mol above minimum)
|
|
207
|
-
min_energy = min(energies)
|
|
208
|
-
rel_energy = (max_energy - min_energy) * 627.509 # Hartree to kcal/mol
|
|
209
|
-
|
|
210
|
-
formatted = f" **Transition State Approximation Extracted**\n\n"
|
|
211
|
-
formatted += f"**Scan Point:** {max_energy_idx + 1} of {len(energies)}\n"
|
|
212
|
-
formatted += f" **Coordinate Value:** {max_coord:.3f}\n"
|
|
213
|
-
formatted += f" **Energy:** {max_energy:.6f} hartree\n"
|
|
214
|
-
formatted += f" **Barrier:** {rel_energy:.2f} kcal/mol above minimum\n\n"
|
|
215
|
-
|
|
216
|
-
if ts_geometry:
|
|
217
|
-
formatted += f" **XYZ Geometry:**\n```\n{ts_geometry}\n```\n\n"
|
|
218
|
-
formatted += f" **Next Steps:**\n"
|
|
219
|
-
formatted += f"1. Use this geometry for transition state optimization\n"
|
|
220
|
-
formatted += f"2. Run: `rowan_spin_states(name='TS_opt', molecule='<geometry>', transition_state=True)`\n"
|
|
221
|
-
formatted += f"3. Verify single imaginary frequency\n"
|
|
222
|
-
formatted += f"4. Use optimized TS for IRC calculation\n"
|
|
223
|
-
else:
|
|
224
|
-
formatted += f" **Warning:** Could not extract geometry data\n"
|
|
225
|
-
formatted += f" Check scan workflow {scan_uuid} manually for geometry data\n"
|
|
226
|
-
|
|
227
|
-
return formatted
|
|
228
|
-
|
|
229
|
-
def extract_minima_geometries(scan_results: Dict[str, Any], energy_threshold: Optional[float] = None) -> str:
|
|
230
|
-
"""Extract low energy geometries (minima)."""
|
|
231
|
-
|
|
232
|
-
energies = scan_results['energies']
|
|
233
|
-
geometries = scan_results['geometries']
|
|
234
|
-
coordinates = scan_results['coordinates']
|
|
235
|
-
|
|
236
|
-
if not energies:
|
|
237
|
-
return " No energy data found in scan results"
|
|
238
|
-
|
|
239
|
-
min_energy = min(energies)
|
|
240
|
-
threshold = energy_threshold or 2.0 # Default 2 kcal/mol above minimum
|
|
241
|
-
threshold_hartree = threshold / 627.509
|
|
242
|
-
|
|
243
|
-
# Find all points within energy threshold
|
|
244
|
-
minima_points = []
|
|
245
|
-
for i, energy in enumerate(energies):
|
|
246
|
-
if energy <= min_energy + threshold_hartree:
|
|
247
|
-
rel_energy = (energy - min_energy) * 627.509
|
|
248
|
-
minima_points.append({
|
|
249
|
-
'index': i,
|
|
250
|
-
'energy': energy,
|
|
251
|
-
'rel_energy': rel_energy,
|
|
252
|
-
'coordinate': coordinates[i],
|
|
253
|
-
'geometry': geometries[i]
|
|
254
|
-
})
|
|
255
|
-
|
|
256
|
-
formatted = f"**Low Energy Structures** (within {threshold:.1f} kcal/mol)\n\n"
|
|
257
|
-
formatted += f"Found {len(minima_points)} structures:\n\n"
|
|
258
|
-
|
|
259
|
-
for point in minima_points:
|
|
260
|
-
formatted += f"**Point {point['index'] + 1}:**\n"
|
|
261
|
-
formatted += f" Coordinate: {point['coordinate']:.3f}\n"
|
|
262
|
-
formatted += f" Energy: +{point['rel_energy']:.2f} kcal/mol\n"
|
|
263
|
-
if point['geometry']:
|
|
264
|
-
# Show first few lines of geometry
|
|
265
|
-
geom_lines = point['geometry'].split('\n')[:5]
|
|
266
|
-
formatted += f" Geometry: {' | '.join(geom_lines[:2])}\n"
|
|
267
|
-
formatted += "\n"
|
|
268
|
-
|
|
269
|
-
return formatted
|
|
270
|
-
|
|
271
|
-
def format_energy_profile(scan_results: Dict[str, Any]) -> str:
|
|
272
|
-
"""Format energy profile data for plotting/analysis."""
|
|
273
|
-
|
|
274
|
-
energies = scan_results['energies']
|
|
275
|
-
coordinates = scan_results['coordinates']
|
|
276
|
-
|
|
277
|
-
if not energies:
|
|
278
|
-
return " No energy data found"
|
|
279
|
-
|
|
280
|
-
min_energy = min(energies)
|
|
281
|
-
|
|
282
|
-
formatted = f" **Energy Profile Data**\n\n"
|
|
283
|
-
formatted += f"Coordinate | Energy (hartree) | Rel Energy (kcal/mol)\n"
|
|
284
|
-
formatted += f"-----------|------------------|--------------------\n"
|
|
285
|
-
|
|
286
|
-
for coord, energy in zip(coordinates, energies):
|
|
287
|
-
rel_energy = (energy - min_energy) * 627.509
|
|
288
|
-
formatted += f"{coord:10.3f} | {energy:15.6f} | {rel_energy:18.2f}\n"
|
|
289
|
-
|
|
290
|
-
formatted += f"\n **Energy Range:** {(max(energies) - min_energy) * 627.509:.2f} kcal/mol\n"
|
|
291
|
-
formatted += f" **Barrier Location:** Coordinate {coordinates[energies.index(max(energies))]:.3f}\n"
|
|
292
|
-
|
|
293
|
-
return formatted
|
|
294
|
-
|
|
295
|
-
def format_full_analysis(scan_results: Dict[str, Any], scan_uuid: str) -> str:
|
|
296
|
-
"""Provide complete scan analysis."""
|
|
297
|
-
|
|
298
|
-
energies = scan_results['energies']
|
|
299
|
-
coordinates = scan_results['coordinates']
|
|
300
|
-
|
|
301
|
-
if not energies:
|
|
302
|
-
return " No scan data to analyze"
|
|
303
|
-
|
|
304
|
-
min_energy = min(energies)
|
|
305
|
-
max_energy = max(energies)
|
|
306
|
-
energy_range = (max_energy - min_energy) * 627.509
|
|
307
|
-
|
|
308
|
-
# Find key points
|
|
309
|
-
min_idx = energies.index(min_energy)
|
|
310
|
-
max_idx = energies.index(max_energy)
|
|
311
|
-
|
|
312
|
-
formatted = f" **Complete Scan Analysis**\n\n"
|
|
313
|
-
formatted += f" **Overview:**\n"
|
|
314
|
-
formatted += f" • Total Points: {len(energies)}\n"
|
|
315
|
-
formatted += f" • Energy Range: {energy_range:.2f} kcal/mol\n"
|
|
316
|
-
formatted += f" • Coordinate Range: {min(coordinates):.3f} to {max(coordinates):.3f}\n\n"
|
|
317
|
-
|
|
318
|
-
formatted += f"🌊 **Global Minimum:**\n"
|
|
319
|
-
formatted += f" • Point {min_idx + 1}: {coordinates[min_idx]:.3f}\n"
|
|
320
|
-
formatted += f" • Energy: {min_energy:.6f} hartree\n\n"
|
|
321
|
-
|
|
322
|
-
formatted += f"⛰ **Energy Maximum (TS Approx):**\n"
|
|
323
|
-
formatted += f" • Point {max_idx + 1}: {coordinates[max_idx]:.3f}\n"
|
|
324
|
-
formatted += f" • Energy: {max_energy:.6f} hartree\n"
|
|
325
|
-
formatted += f" • Barrier: {energy_range:.2f} kcal/mol\n\n"
|
|
326
|
-
|
|
327
|
-
formatted += f" **Recommended Next Steps:**\n"
|
|
328
|
-
formatted += f"1. Extract TS geometry: `rowan_scan_analyzer('{scan_uuid}', 'extract_ts')`\n"
|
|
329
|
-
formatted += f"2. Optimize transition state with extracted geometry\n"
|
|
330
|
-
formatted += f"3. Run IRC from optimized TS\n"
|
|
331
|
-
|
|
332
|
-
return formatted
|
|
333
|
-
|
|
334
|
-
def test_rowan_scan_analyzer():
|
|
335
|
-
"""Test the scan analyzer function."""
|
|
336
|
-
try:
|
|
337
|
-
# Test with dummy UUID
|
|
338
|
-
result = rowan_scan_analyzer("test-scan-uuid", "analyze")
|
|
339
|
-
print(" Scan analyzer test completed!")
|
|
340
|
-
print(f"Result type: {type(result)}")
|
|
341
|
-
return True
|
|
342
|
-
except Exception as e:
|
|
343
|
-
print(f" Scan analyzer test failed: {e}")
|
|
344
|
-
return False
|
|
345
|
-
|
|
346
|
-
if __name__ == "__main__":
|
|
347
|
-
test_rowan_scan_analyzer()
|
|
@@ -1,277 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Rowan solubility prediction function for MCP tool integration.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
from typing import Optional, Union, List, Dict, Any
|
|
6
|
-
import rowan
|
|
7
|
-
|
|
8
|
-
def rowan_solubility(
|
|
9
|
-
name: str,
|
|
10
|
-
molecule: str,
|
|
11
|
-
solvents: Optional[Union[str, List[str]]] = None,
|
|
12
|
-
temperatures: Optional[Union[str, List[Union[str, float]]]] = None,
|
|
13
|
-
folder_uuid: Optional[str] = None,
|
|
14
|
-
blocking: bool = False, # Changed to False to avoid timeouts
|
|
15
|
-
ping_interval: int = 5
|
|
16
|
-
) -> str:
|
|
17
|
-
"""Predict molecular solubility in multiple solvents at various temperatures using machine learning.
|
|
18
|
-
|
|
19
|
-
This tool calculates solubility (log S) predictions for a molecule across different solvents
|
|
20
|
-
and temperatures. Returns immediately with workflow UUID for progress tracking.
|
|
21
|
-
|
|
22
|
-
Args:
|
|
23
|
-
name: Name for the solubility calculation
|
|
24
|
-
molecule: SMILES string of the molecule
|
|
25
|
-
solvents: Solvent specification. Supports:
|
|
26
|
-
- Comma-separated: "water,ethanol,hexane"
|
|
27
|
-
- JSON array: '["O", "CCO", "CCCCCC"]'
|
|
28
|
-
- Single: "water" or "O"
|
|
29
|
-
temperatures: Temperature specification in Celsius or Kelvin. Supports:
|
|
30
|
-
- Comma-separated: "25,37,50"
|
|
31
|
-
- JSON array: "[298.15, 310.15, 323.15]"
|
|
32
|
-
- Single: "25" or "298.15"
|
|
33
|
-
folder_uuid: Optional folder UUID to organize the calculation
|
|
34
|
-
blocking: Whether to wait for completion (default: False to avoid timeouts)
|
|
35
|
-
ping_interval: Interval in seconds to check calculation status
|
|
36
|
-
|
|
37
|
-
Returns:
|
|
38
|
-
JSON string with workflow UUID and status (non-blocking) or full results (blocking)
|
|
39
|
-
|
|
40
|
-
Examples:
|
|
41
|
-
# Comma-separated (user-friendly)
|
|
42
|
-
rowan_solubility(
|
|
43
|
-
name="acetaminophen_analysis",
|
|
44
|
-
molecule="CC(=O)Nc1ccc(O)cc1",
|
|
45
|
-
solvents="water,ethanol,hexane",
|
|
46
|
-
temperatures="25,37,50"
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
# JSON arrays (precise)
|
|
50
|
-
rowan_solubility(
|
|
51
|
-
name="precise_analysis",
|
|
52
|
-
molecule="CC(=O)Nc1ccc(O)cc1",
|
|
53
|
-
solvents='["O", "CCO", "CCCCCC"]',
|
|
54
|
-
temperatures='[298.15, 310.15, 323.15]'
|
|
55
|
-
)
|
|
56
|
-
"""
|
|
57
|
-
|
|
58
|
-
# Handle multiple solvent input formats
|
|
59
|
-
if solvents is not None and isinstance(solvents, str):
|
|
60
|
-
# Try to parse as JSON first (for MCP tool calls)
|
|
61
|
-
try:
|
|
62
|
-
import json
|
|
63
|
-
if solvents.startswith('[') and solvents.endswith(']'):
|
|
64
|
-
solvents = json.loads(solvents)
|
|
65
|
-
elif ',' in solvents:
|
|
66
|
-
# Handle comma-separated string
|
|
67
|
-
solvents = [s.strip() for s in solvents.split(',')]
|
|
68
|
-
else:
|
|
69
|
-
# Single solvent string
|
|
70
|
-
solvents = [solvents]
|
|
71
|
-
except json.JSONDecodeError:
|
|
72
|
-
# JSON parsing failed, try comma-separated
|
|
73
|
-
if ',' in solvents:
|
|
74
|
-
solvents = [s.strip() for s in solvents.split(',')]
|
|
75
|
-
else:
|
|
76
|
-
solvents = [solvents]
|
|
77
|
-
|
|
78
|
-
# Convert solvent names to SMILES if needed
|
|
79
|
-
if solvents is not None:
|
|
80
|
-
solvents = convert_solvent_names_to_smiles(solvents)
|
|
81
|
-
|
|
82
|
-
# Default temperatures if none provided (room temp to moderate heating)
|
|
83
|
-
if temperatures is None:
|
|
84
|
-
temperatures = [273.15, 298.15, 323.15, 348.15, 373.15] # K
|
|
85
|
-
|
|
86
|
-
# Handle multiple temperature input formats
|
|
87
|
-
if isinstance(temperatures, str):
|
|
88
|
-
# Try to parse as JSON first (for MCP tool calls)
|
|
89
|
-
try:
|
|
90
|
-
import json
|
|
91
|
-
if temperatures.strip().startswith('[') and temperatures.strip().endswith(']'):
|
|
92
|
-
parsed_temps = json.loads(temperatures.strip())
|
|
93
|
-
if isinstance(parsed_temps, list):
|
|
94
|
-
temperatures = parsed_temps
|
|
95
|
-
else:
|
|
96
|
-
temperatures = [parsed_temps]
|
|
97
|
-
elif ',' in temperatures:
|
|
98
|
-
# Handle comma-separated string
|
|
99
|
-
temperatures = [t.strip() for t in temperatures.split(',')]
|
|
100
|
-
else:
|
|
101
|
-
# Single temperature string
|
|
102
|
-
temperatures = [temperatures.strip()]
|
|
103
|
-
except (json.JSONDecodeError, ValueError) as e:
|
|
104
|
-
# JSON parsing failed, try comma-separated
|
|
105
|
-
if ',' in temperatures:
|
|
106
|
-
temperatures = [t.strip() for t in temperatures.split(',')]
|
|
107
|
-
else:
|
|
108
|
-
temperatures = [temperatures.strip()]
|
|
109
|
-
elif isinstance(temperatures, (float, int)):
|
|
110
|
-
temperatures = [temperatures]
|
|
111
|
-
elif isinstance(temperatures, list):
|
|
112
|
-
# Already a list, keep as is
|
|
113
|
-
pass
|
|
114
|
-
else:
|
|
115
|
-
raise ValueError(f"Invalid temperatures parameter type: {type(temperatures)}. Expected string, number, or list.")
|
|
116
|
-
|
|
117
|
-
# Convert temperature strings to floats and handle Celsius conversion
|
|
118
|
-
processed_temps = []
|
|
119
|
-
for temp in temperatures:
|
|
120
|
-
if isinstance(temp, str):
|
|
121
|
-
try:
|
|
122
|
-
temp = float(temp)
|
|
123
|
-
except ValueError as e:
|
|
124
|
-
raise ValueError(f"Invalid temperature value: '{temp}'. Expected a number, got: {e}")
|
|
125
|
-
elif isinstance(temp, (int, float)):
|
|
126
|
-
temp = float(temp)
|
|
127
|
-
else:
|
|
128
|
-
raise ValueError(f"Invalid temperature type: {type(temp)}. Expected string or number.")
|
|
129
|
-
|
|
130
|
-
# Assume Celsius if temperature is < 200, convert to Kelvin
|
|
131
|
-
if temp < 200:
|
|
132
|
-
temp += 273.15
|
|
133
|
-
processed_temps.append(temp)
|
|
134
|
-
|
|
135
|
-
# Prepare workflow parameters
|
|
136
|
-
workflow_params = {
|
|
137
|
-
"name": name,
|
|
138
|
-
"molecule": molecule, # Required by rowan.compute() API
|
|
139
|
-
"workflow_type": "solubility",
|
|
140
|
-
"folder_uuid": folder_uuid,
|
|
141
|
-
"blocking": blocking,
|
|
142
|
-
"ping_interval": ping_interval,
|
|
143
|
-
# Workflow-specific parameters for SolubilityWorkflow
|
|
144
|
-
"initial_smiles": molecule, # Required by SolubilityWorkflow Pydantic model
|
|
145
|
-
"solvents": solvents,
|
|
146
|
-
"temperatures": processed_temps
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
try:
|
|
150
|
-
# Submit solubility calculation to Rowan
|
|
151
|
-
result = rowan.compute(**workflow_params)
|
|
152
|
-
|
|
153
|
-
# Format the response based on blocking mode
|
|
154
|
-
if result:
|
|
155
|
-
workflow_uuid = result.get("uuid")
|
|
156
|
-
status = result.get("object_status", 0)
|
|
157
|
-
|
|
158
|
-
if blocking and status == 2: # Completed
|
|
159
|
-
# Extract solubility results for completed blocking calls
|
|
160
|
-
object_data = result.get("object_data", {})
|
|
161
|
-
if "solubilities" in object_data:
|
|
162
|
-
response = {
|
|
163
|
-
"success": True,
|
|
164
|
-
"workflow_uuid": workflow_uuid,
|
|
165
|
-
"name": name,
|
|
166
|
-
"molecule": molecule,
|
|
167
|
-
"status": "completed",
|
|
168
|
-
"solubility_results": object_data["solubilities"],
|
|
169
|
-
"summary": f"Completed solubility calculation for {len(solvents) if solvents else 'default'} solvents at {len(processed_temps)} temperatures",
|
|
170
|
-
"runtime_seconds": result.get("elapsed", 0),
|
|
171
|
-
"credits_charged": result.get("credits_charged", 0)
|
|
172
|
-
}
|
|
173
|
-
else:
|
|
174
|
-
response = {
|
|
175
|
-
"success": True,
|
|
176
|
-
"workflow_uuid": workflow_uuid,
|
|
177
|
-
"name": name,
|
|
178
|
-
"molecule": molecule,
|
|
179
|
-
"status": "completed",
|
|
180
|
-
"message": "Solubility calculation completed successfully",
|
|
181
|
-
"runtime_seconds": result.get("elapsed", 0),
|
|
182
|
-
"credits_charged": result.get("credits_charged", 0)
|
|
183
|
-
}
|
|
184
|
-
else:
|
|
185
|
-
# Non-blocking or still running - return workflow info for tracking
|
|
186
|
-
status_text = {0: "queued", 1: "running", 2: "completed", 3: "failed"}.get(status, "unknown")
|
|
187
|
-
response = {
|
|
188
|
-
"success": True,
|
|
189
|
-
"tracking_id": workflow_uuid, # Prominent tracking ID
|
|
190
|
-
"workflow_uuid": workflow_uuid, # Keep for backward compatibility
|
|
191
|
-
"name": name,
|
|
192
|
-
"molecule": molecule,
|
|
193
|
-
"status": status_text,
|
|
194
|
-
"message": f" Solubility calculation submitted successfully! Use tracking_id to monitor progress.",
|
|
195
|
-
"calculation_details": {
|
|
196
|
-
"solvents_count": len(solvents) if solvents else 0,
|
|
197
|
-
"temperatures_count": len(processed_temps),
|
|
198
|
-
"blocking_mode": blocking
|
|
199
|
-
},
|
|
200
|
-
"progress_tracking": {
|
|
201
|
-
"tracking_id": workflow_uuid,
|
|
202
|
-
"check_status": f"rowan_workflow_management(action='status', workflow_uuid='{workflow_uuid}')",
|
|
203
|
-
"get_results": f"rowan_workflow_management(action='retrieve', workflow_uuid='{workflow_uuid}')"
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
else:
|
|
207
|
-
response = {
|
|
208
|
-
"success": False,
|
|
209
|
-
"error": "No response received from Rowan API",
|
|
210
|
-
"name": name,
|
|
211
|
-
"molecule": molecule
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
return str(response)
|
|
215
|
-
|
|
216
|
-
except Exception as e:
|
|
217
|
-
error_response = {
|
|
218
|
-
"success": False,
|
|
219
|
-
"error": f"Solubility calculation failed: {str(e)}",
|
|
220
|
-
"name": name,
|
|
221
|
-
"molecule": molecule
|
|
222
|
-
}
|
|
223
|
-
return str(error_response)
|
|
224
|
-
|
|
225
|
-
# Solvent name to SMILES mapping for convenience
|
|
226
|
-
SOLVENT_SMILES = {
|
|
227
|
-
"water": "O",
|
|
228
|
-
"ethanol": "CCO",
|
|
229
|
-
"methanol": "CO",
|
|
230
|
-
"hexane": "CCCCCC",
|
|
231
|
-
"toluene": "CC1=CC=CC=C1",
|
|
232
|
-
"thf": "C1CCCO1",
|
|
233
|
-
"tetrahydrofuran": "C1CCCO1",
|
|
234
|
-
"ethyl_acetate": "CC(=O)OCC",
|
|
235
|
-
"acetonitrile": "CC#N",
|
|
236
|
-
"dmso": "CS(=O)C",
|
|
237
|
-
"acetone": "CC(=O)C",
|
|
238
|
-
"propanone": "CC(=O)C",
|
|
239
|
-
"chloroform": "ClCCl",
|
|
240
|
-
"dichloromethane": "ClCCl"
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
def convert_solvent_names_to_smiles(solvents: List[str]) -> List[str]:
|
|
244
|
-
"""
|
|
245
|
-
Convert common solvent names to SMILES strings.
|
|
246
|
-
|
|
247
|
-
Args:
|
|
248
|
-
solvents: List of solvent names or SMILES
|
|
249
|
-
|
|
250
|
-
Returns:
|
|
251
|
-
List of SMILES strings
|
|
252
|
-
"""
|
|
253
|
-
converted = []
|
|
254
|
-
for solvent in solvents:
|
|
255
|
-
# If it's already a SMILES (contains typical SMILES characters), keep as is
|
|
256
|
-
if any(char in solvent for char in ['=', '#', '(', ')', '[', ']']):
|
|
257
|
-
converted.append(solvent)
|
|
258
|
-
else:
|
|
259
|
-
# Try to convert from name to SMILES
|
|
260
|
-
solvent_lower = solvent.lower().replace(' ', '_')
|
|
261
|
-
converted.append(SOLVENT_SMILES.get(solvent_lower, solvent))
|
|
262
|
-
|
|
263
|
-
return converted
|
|
264
|
-
|
|
265
|
-
def test_rowan_solubility():
|
|
266
|
-
"""Test the rowan_solubility function with hardcoded values."""
|
|
267
|
-
try:
|
|
268
|
-
result = rowan_solubility("test_acetaminophen_solubility", "dummy_molecule")
|
|
269
|
-
print(" Solubility test successful!")
|
|
270
|
-
print(f"Result: {result}")
|
|
271
|
-
return True
|
|
272
|
-
except Exception as e:
|
|
273
|
-
print(f" Solubility test failed: {e}")
|
|
274
|
-
return False
|
|
275
|
-
|
|
276
|
-
if __name__ == "__main__":
|
|
277
|
-
test_rowan_solubility()
|