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.

Files changed (70) hide show
  1. rowan_mcp/__init__.py +1 -1
  2. rowan_mcp/__main__.py +3 -5
  3. rowan_mcp/functions_v2/BENCHMARK.md +86 -0
  4. rowan_mcp/functions_v2/molecule_lookup.py +232 -0
  5. rowan_mcp/functions_v2/protein_management.py +141 -0
  6. rowan_mcp/functions_v2/submit_basic_calculation_workflow.py +195 -0
  7. rowan_mcp/functions_v2/submit_conformer_search_workflow.py +158 -0
  8. rowan_mcp/functions_v2/submit_descriptors_workflow.py +52 -0
  9. rowan_mcp/functions_v2/submit_docking_workflow.py +244 -0
  10. rowan_mcp/functions_v2/submit_fukui_workflow.py +114 -0
  11. rowan_mcp/functions_v2/submit_irc_workflow.py +58 -0
  12. rowan_mcp/functions_v2/submit_macropka_workflow.py +99 -0
  13. rowan_mcp/functions_v2/submit_pka_workflow.py +72 -0
  14. rowan_mcp/functions_v2/submit_protein_cofolding_workflow.py +88 -0
  15. rowan_mcp/functions_v2/submit_redox_potential_workflow.py +55 -0
  16. rowan_mcp/functions_v2/submit_scan_workflow.py +82 -0
  17. rowan_mcp/functions_v2/submit_solubility_workflow.py +157 -0
  18. rowan_mcp/functions_v2/submit_tautomer_search_workflow.py +51 -0
  19. rowan_mcp/functions_v2/workflow_management_v2.py +382 -0
  20. rowan_mcp/server.py +109 -144
  21. rowan_mcp/tests/basic_calculation_from_json.py +0 -0
  22. rowan_mcp/tests/basic_calculation_with_constraint.py +33 -0
  23. rowan_mcp/tests/basic_calculation_with_solvent.py +0 -0
  24. rowan_mcp/tests/bde.py +37 -0
  25. rowan_mcp/tests/benchmark_queries.md +120 -0
  26. rowan_mcp/tests/cofolding_screen.py +131 -0
  27. rowan_mcp/tests/conformer_dependent_redox.py +37 -0
  28. rowan_mcp/tests/conformers.py +31 -0
  29. rowan_mcp/tests/data.json +189 -0
  30. rowan_mcp/tests/docking_screen.py +157 -0
  31. rowan_mcp/tests/irc.py +24 -0
  32. rowan_mcp/tests/macropka.py +13 -0
  33. rowan_mcp/tests/multistage_opt.py +13 -0
  34. rowan_mcp/tests/optimization.py +21 -0
  35. rowan_mcp/tests/phenol_pka.py +36 -0
  36. rowan_mcp/tests/pka.py +36 -0
  37. rowan_mcp/tests/protein_cofolding.py +17 -0
  38. rowan_mcp/tests/scan.py +28 -0
  39. {rowan_mcp-1.0.2.dist-info → rowan_mcp-2.0.0.dist-info}/METADATA +41 -33
  40. rowan_mcp-2.0.0.dist-info/RECORD +42 -0
  41. rowan_mcp/functions/admet.py +0 -94
  42. rowan_mcp/functions/bde.py +0 -113
  43. rowan_mcp/functions/calculation_retrieve.py +0 -89
  44. rowan_mcp/functions/conformers.py +0 -80
  45. rowan_mcp/functions/descriptors.py +0 -92
  46. rowan_mcp/functions/docking.py +0 -340
  47. rowan_mcp/functions/docking_enhanced.py +0 -174
  48. rowan_mcp/functions/electronic_properties.py +0 -205
  49. rowan_mcp/functions/folder_management.py +0 -137
  50. rowan_mcp/functions/fukui.py +0 -219
  51. rowan_mcp/functions/hydrogen_bond_basicity.py +0 -94
  52. rowan_mcp/functions/irc.py +0 -125
  53. rowan_mcp/functions/macropka.py +0 -120
  54. rowan_mcp/functions/molecular_converter.py +0 -423
  55. rowan_mcp/functions/molecular_dynamics.py +0 -191
  56. rowan_mcp/functions/molecule_lookup.py +0 -57
  57. rowan_mcp/functions/multistage_opt.py +0 -171
  58. rowan_mcp/functions/pdb_handler.py +0 -200
  59. rowan_mcp/functions/pka.py +0 -88
  60. rowan_mcp/functions/redox_potential.py +0 -352
  61. rowan_mcp/functions/scan.py +0 -536
  62. rowan_mcp/functions/scan_analyzer.py +0 -347
  63. rowan_mcp/functions/solubility.py +0 -277
  64. rowan_mcp/functions/spin_states.py +0 -747
  65. rowan_mcp/functions/system_management.py +0 -368
  66. rowan_mcp/functions/tautomers.py +0 -91
  67. rowan_mcp/functions/workflow_management.py +0 -422
  68. rowan_mcp-1.0.2.dist-info/RECORD +0 -34
  69. {rowan_mcp-1.0.2.dist-info → rowan_mcp-2.0.0.dist-info}/WHEEL +0 -0
  70. {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()