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.

Files changed (35) hide show
  1. rowan_mcp/__init__.py +14 -0
  2. rowan_mcp/__main__.py +14 -0
  3. rowan_mcp/functions/admet.py +94 -0
  4. rowan_mcp/functions/bde.py +113 -0
  5. rowan_mcp/functions/calculation_retrieve.py +89 -0
  6. rowan_mcp/functions/conformers.py +135 -0
  7. rowan_mcp/functions/descriptors.py +92 -0
  8. rowan_mcp/functions/docking.py +340 -0
  9. rowan_mcp/functions/docking_enhanced.py +174 -0
  10. rowan_mcp/functions/electronic_properties.py +263 -0
  11. rowan_mcp/functions/folder_management.py +137 -0
  12. rowan_mcp/functions/fukui.py +355 -0
  13. rowan_mcp/functions/hydrogen_bond_basicity.py +94 -0
  14. rowan_mcp/functions/irc.py +125 -0
  15. rowan_mcp/functions/macropka.py +195 -0
  16. rowan_mcp/functions/molecular_converter.py +423 -0
  17. rowan_mcp/functions/molecular_dynamics.py +191 -0
  18. rowan_mcp/functions/molecule_cache.db +0 -0
  19. rowan_mcp/functions/molecule_lookup.py +446 -0
  20. rowan_mcp/functions/multistage_opt.py +171 -0
  21. rowan_mcp/functions/pdb_handler.py +200 -0
  22. rowan_mcp/functions/pka.py +137 -0
  23. rowan_mcp/functions/redox_potential.py +352 -0
  24. rowan_mcp/functions/scan.py +536 -0
  25. rowan_mcp/functions/scan_analyzer.py +347 -0
  26. rowan_mcp/functions/solubility.py +277 -0
  27. rowan_mcp/functions/spin_states.py +747 -0
  28. rowan_mcp/functions/system_management.py +368 -0
  29. rowan_mcp/functions/tautomers.py +91 -0
  30. rowan_mcp/functions/workflow_management.py +422 -0
  31. rowan_mcp/server.py +169 -0
  32. rowan_mcp-0.1.0.dist-info/METADATA +216 -0
  33. rowan_mcp-0.1.0.dist-info/RECORD +35 -0
  34. rowan_mcp-0.1.0.dist-info/WHEEL +4 -0
  35. rowan_mcp-0.1.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,355 @@
1
+ """
2
+ Fukui Analysis for Rowan MCP Server
3
+
4
+ This module provides Fukui indices calculations for reactivity prediction including:
5
+ - f(+) indices for electrophilic attack sites
6
+ - f(-) indices for nucleophilic attack sites
7
+ - f(0) indices for radical attack sites
8
+ - Global electrophilicity index
9
+ """
10
+
11
+ import os
12
+ import logging
13
+ import time
14
+ from typing import Any, Dict, List, Optional
15
+
16
+ try:
17
+ import rowan
18
+ except ImportError:
19
+ rowan = None
20
+
21
+ # Configure logging
22
+ logging.basicConfig(level=logging.INFO)
23
+ logger = logging.getLogger(__name__)
24
+
25
+ # Setup API key
26
+ api_key = os.getenv("ROWAN_API_KEY")
27
+ if api_key and rowan:
28
+ rowan.api_key = api_key
29
+
30
+ def log_rowan_api_call(workflow_type: str, **kwargs):
31
+ """Log Rowan API calls with detailed parameters."""
32
+
33
+ try:
34
+ start_time = time.time()
35
+
36
+ if not rowan:
37
+ raise ImportError("Rowan package not available - please install with 'pip install rowan'")
38
+
39
+ logger.info(f"Calling Rowan {workflow_type} workflow")
40
+ for key, value in kwargs.items():
41
+ if key != 'ping_interval':
42
+ logger.info(f" {key}: {value}")
43
+
44
+ result = rowan.compute(workflow_type=workflow_type, **kwargs)
45
+
46
+ end_time = time.time()
47
+ duration = end_time - start_time
48
+ logger.info(f"Rowan {workflow_type} completed in {duration:.2f} seconds")
49
+
50
+ return result
51
+
52
+ except Exception as e:
53
+ logger.error(f"Rowan {workflow_type} failed: {str(e)}")
54
+ raise e
55
+
56
+ def lookup_molecule_smiles(molecule_name: str) -> str:
57
+ """Look up canonical SMILES for common molecule names."""
58
+ # Common molecule SMILES database
59
+ MOLECULE_SMILES = {
60
+ # Aromatics
61
+ "phenol": "Oc1ccccc1",
62
+ "benzene": "c1ccccc1",
63
+ "toluene": "Cc1ccccc1",
64
+ "aniline": "Nc1ccccc1",
65
+ "benzoic acid": "O=C(O)c1ccccc1",
66
+ "salicylic acid": "O=C(O)c1ccccc1O",
67
+ "aspirin": "CC(=O)Oc1ccccc1C(=O)O",
68
+
69
+ # Solvents
70
+ "water": "O",
71
+ "acetone": "CC(=O)C",
72
+ "dmso": "CS(=O)C",
73
+ "dmf": "CN(C)C=O",
74
+ "thf": "C1CCOC1",
75
+ "dioxane": "C1COCCO1",
76
+ "chloroform": "ClC(Cl)Cl",
77
+ "dichloromethane": "ClCCl",
78
+
79
+ # Others
80
+ "glucose": "OC[C@H]1OC(O)[C@H](O)[C@@H](O)[C@@H]1O",
81
+ "ethylene": "C=C",
82
+ "acetylene": "C#C",
83
+ "formaldehyde": "C=O",
84
+ "ammonia": "N",
85
+ "hydrogen peroxide": "OO",
86
+ "carbon dioxide": "O=C=O",
87
+ }
88
+
89
+ # Normalize the input (lowercase, strip whitespace)
90
+ normalized_name = molecule_name.lower().strip()
91
+
92
+ # Direct lookup
93
+ if normalized_name in MOLECULE_SMILES:
94
+ return MOLECULE_SMILES[normalized_name]
95
+
96
+ # Try partial matches for common variations
97
+ for name, smiles in MOLECULE_SMILES.items():
98
+ if normalized_name in name or name in normalized_name:
99
+ logger.info(f"SMILES Lookup (partial match): '{molecule_name}' → '{name}' → '{smiles}'")
100
+ return smiles
101
+
102
+ # If no match found, return the original input (assume it's already SMILES)
103
+ return molecule_name
104
+
105
+ def rowan_fukui(
106
+ name: str,
107
+ molecule: str,
108
+ optimize: bool = True,
109
+ opt_method: Optional[str] = None,
110
+ opt_basis_set: Optional[str] = None,
111
+ opt_engine: Optional[str] = None,
112
+ fukui_method: str = "gfn1_xtb",
113
+ fukui_basis_set: Optional[str] = None,
114
+ fukui_engine: Optional[str] = None,
115
+ charge: int = 0,
116
+ multiplicity: int = 1,
117
+ folder_uuid: Optional[str] = None,
118
+ blocking: bool = True,
119
+ ping_interval: int = 5
120
+ ) -> str:
121
+ """Calculate Fukui indices for reactivity prediction with comprehensive control.
122
+
123
+ Predicts sites of chemical reactivity by analyzing electron density changes upon
124
+ gaining/losing electrons. Uses a two-step process: optimization + Fukui calculation.
125
+
126
+ ** Fukui Index Types:**
127
+ - **f(+)**: Electrophilic attack sites (nucleophile reactivity)
128
+ - **f(-)**: Nucleophilic attack sites (electrophile reactivity)
129
+ - **f(0)**: Radical attack sites (average of f(+) and f(-))
130
+ - **Global Electrophilicity Index**: Overall electrophilic character
131
+
132
+ ** Key Features:**
133
+ - Optional geometry optimization before Fukui calculation
134
+ - Separate control over optimization and Fukui calculation methods
135
+ - Per-atom reactivity indices for site-specific analysis
136
+ - Global reactivity descriptors
137
+
138
+ Args:
139
+ name: Name for the calculation
140
+ molecule: Molecule SMILES string or common name (e.g., "phenol", "benzene")
141
+ optimize: Whether to optimize geometry before Fukui calculation (default: True)
142
+ opt_method: Method for optimization (default: None, uses engine default)
143
+ opt_basis_set: Basis set for optimization (default: None, uses engine default)
144
+ opt_engine: Engine for optimization (default: None, auto-selected)
145
+ fukui_method: Method for Fukui calculation (default: "gfn1_xtb")
146
+ fukui_basis_set: Basis set for Fukui calculation (default: None, uses method default)
147
+ fukui_engine: Engine for Fukui calculation (default: None, auto-selected)
148
+ charge: Molecular charge (default: 0)
149
+ multiplicity: Spin multiplicity (default: 1)
150
+ folder_uuid: Optional folder UUID for organization
151
+ blocking: Whether to wait for completion (default: True)
152
+ ping_interval: Check status interval in seconds (default: 5)
153
+
154
+ Returns:
155
+ Fukui indices and reactivity analysis with per-atom and global descriptors
156
+ """
157
+ # Look up SMILES if a common name was provided
158
+ canonical_smiles = lookup_molecule_smiles(molecule)
159
+
160
+ # Build optimization settings if requested
161
+ opt_settings = None
162
+ if optimize:
163
+ opt_settings = {
164
+ "charge": charge,
165
+ "multiplicity": multiplicity
166
+ }
167
+
168
+ # Add optimization method/basis/engine if specified
169
+ if opt_method:
170
+ opt_settings["method"] = opt_method.lower()
171
+ if opt_basis_set:
172
+ opt_settings["basis_set"] = opt_basis_set.lower()
173
+
174
+ # Default to fast optimization if no engine specified
175
+ if not opt_engine and not opt_method:
176
+ opt_settings["method"] = "gfn2_xtb" # Fast optimization
177
+ logger.info(f"No optimization method specified, defaulting to GFN2-xTB")
178
+
179
+ # Build Fukui calculation settings
180
+ fukui_settings = {
181
+ "method": fukui_method.lower(),
182
+ "charge": charge,
183
+ "multiplicity": multiplicity
184
+ }
185
+
186
+ # Add Fukui basis set if specified
187
+ if fukui_basis_set:
188
+ fukui_settings["basis_set"] = fukui_basis_set.lower()
189
+
190
+ # Validate Fukui method
191
+ valid_fukui_methods = ["gfn1_xtb", "gfn2_xtb", "hf", "b3lyp", "pbe", "m06-2x"]
192
+ if fukui_method.lower() not in valid_fukui_methods:
193
+ pass # Warning already logged by cleanup script
194
+
195
+ # Build parameters for Rowan API
196
+ fukui_params = {
197
+ "name": name,
198
+ "molecule": canonical_smiles,
199
+ "fukui_settings": fukui_settings,
200
+ "folder_uuid": folder_uuid,
201
+ "blocking": blocking,
202
+ "ping_interval": ping_interval
203
+ }
204
+
205
+ # Add optimization settings if enabled
206
+ if optimize and opt_settings:
207
+ fukui_params["opt_settings"] = opt_settings
208
+
209
+ # Add engines if specified
210
+ if opt_engine:
211
+ fukui_params["opt_engine"] = opt_engine.lower()
212
+ if fukui_engine:
213
+ fukui_params["fukui_engine"] = fukui_engine.lower()
214
+
215
+ result = log_rowan_api_call(
216
+ workflow_type="fukui",
217
+ **fukui_params
218
+ )
219
+
220
+ if blocking:
221
+ status = result.get('status', result.get('object_status', 'Unknown'))
222
+
223
+ if status == 2: # Completed successfully
224
+ formatted = f"Fukui analysis for '{name}' completed successfully!\n\n"
225
+ elif status == 3: # Failed
226
+ formatted = f"Fukui analysis for '{name}' failed!\n\n"
227
+ else:
228
+ formatted = f"Fukui analysis for '{name}' finished with status {status}\n\n"
229
+
230
+ formatted += f"Molecule: {molecule}\n"
231
+ formatted += f"SMILES: {canonical_smiles}\n"
232
+ formatted += f"Job UUID: {result.get('uuid', 'N/A')}\n"
233
+ formatted += f"Status: {status}\n"
234
+
235
+ # Computational settings summary
236
+ formatted += f"\nComputational Settings:\n"
237
+ formatted += f" Optimization: {'Enabled' if optimize else 'Disabled'}\n"
238
+ if optimize:
239
+ opt_method_display = opt_settings.get('method', 'default') if opt_settings else 'default'
240
+ formatted += f" Opt Method: {opt_method_display.upper()}\n"
241
+ if opt_engine:
242
+ formatted += f" Opt Engine: {opt_engine.upper()}\n"
243
+ formatted += f" Fukui Method: {fukui_method.upper()}\n"
244
+ if fukui_engine:
245
+ formatted += f" Fukui Engine: {fukui_engine.upper()}\n"
246
+ formatted += f" Charge: {charge}, Multiplicity: {multiplicity}\n"
247
+
248
+ # Try to extract Fukui results
249
+ if isinstance(result, dict) and 'object_data' in result and result['object_data']:
250
+ data = result['object_data']
251
+
252
+ # Global electrophilicity index
253
+ if 'global_electrophilicity_index' in data and data['global_electrophilicity_index'] is not None:
254
+ gei = data['global_electrophilicity_index']
255
+ formatted += f"\nGlobal Electrophilicity Index: {gei:.4f}\n"
256
+ if gei > 1.5:
257
+ formatted += f" → Strong electrophile (highly reactive towards nucleophiles)\n"
258
+ elif gei > 0.8:
259
+ formatted += f" → Moderate electrophile\n"
260
+ else:
261
+ formatted += f" → Weak electrophile\n"
262
+
263
+ # Fukui indices per atom
264
+ fukui_available = []
265
+ if 'fukui_positive' in data and data['fukui_positive']:
266
+ fukui_available.append("f(+)")
267
+ if 'fukui_negative' in data and data['fukui_negative']:
268
+ fukui_available.append("f(-)")
269
+ if 'fukui_zero' in data and data['fukui_zero']:
270
+ fukui_available.append("f(0)")
271
+
272
+ if fukui_available:
273
+ formatted += f"\nFukui Indices Available: {', '.join(fukui_available)}\n"
274
+
275
+ # Analyze most reactive sites
276
+ formatted += f"\nMost Reactive Sites:\n"
277
+
278
+ # f(+) - electrophilic attack sites
279
+ if 'fukui_positive' in data and data['fukui_positive']:
280
+ f_plus = data['fukui_positive']
281
+ if isinstance(f_plus, list) and len(f_plus) > 0:
282
+ # Find top 3 sites
283
+ indexed_values = [(i+1, val) for i, val in enumerate(f_plus) if val is not None]
284
+ top_f_plus = sorted(indexed_values, key=lambda x: x[1], reverse=True)[:3]
285
+ formatted += f" f(+) Top Sites (electrophilic attack): "
286
+ formatted += f"{', '.join([f'Atom {atom}({val:.3f})' for atom, val in top_f_plus])}\n"
287
+
288
+ # f(-) - nucleophilic attack sites
289
+ if 'fukui_negative' in data and data['fukui_negative']:
290
+ f_minus = data['fukui_negative']
291
+ if isinstance(f_minus, list) and len(f_minus) > 0:
292
+ indexed_values = [(i+1, val) for i, val in enumerate(f_minus) if val is not None]
293
+ top_f_minus = sorted(indexed_values, key=lambda x: x[1], reverse=True)[:3]
294
+ formatted += f" f(-) Top Sites (nucleophilic attack): "
295
+ formatted += f"{', '.join([f'Atom {atom}({val:.3f})' for atom, val in top_f_minus])}\n"
296
+
297
+ # f(0) - radical attack sites
298
+ if 'fukui_zero' in data and data['fukui_zero']:
299
+ f_zero = data['fukui_zero']
300
+ if isinstance(f_zero, list) and len(f_zero) > 0:
301
+ indexed_values = [(i+1, val) for i, val in enumerate(f_zero) if val is not None]
302
+ top_f_zero = sorted(indexed_values, key=lambda x: x[1], reverse=True)[:3]
303
+ formatted += f" f(0) Top Sites (radical attack): "
304
+ formatted += f"{', '.join([f'Atom {atom}({val:.3f})' for atom, val in top_f_zero])}\n"
305
+
306
+ # Status-specific guidance
307
+ formatted += f"\nNext Steps:\n"
308
+ if status == 2: # Completed
309
+ formatted += f"• Use rowan_workflow_management(action='retrieve', workflow_uuid='{result.get('uuid')}') for full per-atom data\n"
310
+ formatted += f"• Higher Fukui values indicate more reactive sites\n"
311
+ formatted += f"• f(+) predicts where nucleophiles will attack\n"
312
+ formatted += f"• f(-) predicts where electrophiles will attack\n"
313
+ formatted += f"• f(0) predicts radical reaction sites\n"
314
+ elif status == 3: # Failed
315
+ formatted += f"• Use rowan_workflow_management(action='retrieve', workflow_uuid='{result.get('uuid')}') for error details\n"
316
+ formatted += f"• Troubleshooting:\n"
317
+ formatted += f" - Try disabling optimization: optimize=False\n"
318
+ formatted += f" - Use faster Fukui method: fukui_method='gfn1_xtb'\n"
319
+ formatted += f" - Check if molecule SMILES is valid\n"
320
+ formatted += f" - Verify charge and multiplicity are correct\n"
321
+ elif status in [0, 1, 5]: # Running
322
+ formatted += f"• Check progress: rowan_workflow_management(action='status', workflow_uuid='{result.get('uuid')}')\n"
323
+ if optimize:
324
+ formatted += f"• Two-step process: optimization → Fukui calculation\n"
325
+ formatted += f"• Fukui analysis may take 5-20 minutes depending on method and molecule size\n"
326
+ elif status == 4: # Stopped
327
+ formatted += f"• Check why stopped: rowan_workflow_management(action='retrieve', workflow_uuid='{result.get('uuid')}')\n"
328
+ formatted += f"• You can restart with same or modified parameters\n"
329
+ else: # Unknown
330
+ formatted += f"• Check status: rowan_workflow_management(action='status', workflow_uuid='{result.get('uuid')}')\n"
331
+
332
+ return formatted
333
+ else:
334
+ formatted = f"Fukui analysis for '{name}' submitted!\n\n"
335
+ formatted += f"Molecule: {molecule}\n"
336
+ formatted += f"SMILES: {canonical_smiles}\n"
337
+ formatted += f"Job UUID: {result.get('uuid', 'N/A')}\n"
338
+ formatted += f"Status: {result.get('status', 'Submitted')}\n"
339
+ formatted += f"Optimization: {'Enabled' if optimize else 'Disabled'}\n"
340
+ formatted += f"Fukui Method: {fukui_method.upper()}\n"
341
+ formatted += f"Charge: {charge}, Multiplicity: {multiplicity}\n"
342
+ formatted += f"\nUse rowan_workflow_management tools to check progress and retrieve results\n"
343
+ return formatted
344
+
345
+ def test_fukui():
346
+ """Test the fukui function."""
347
+ return rowan_fukui(
348
+ name="test_fukui",
349
+ molecule="benzene",
350
+ fukui_method="gfn1_xtb",
351
+ blocking=True
352
+ )
353
+
354
+ if __name__ == "__main__":
355
+ print(test_fukui())
@@ -0,0 +1,94 @@
1
+ """
2
+ Calculate hydrogen bond basicity (pKBHX) values for molecules to predict H-bond acceptor strength - useful for queries about pyridine/imine nitrogen basicity, comparing acceptor sites, or understanding binding selectivity. Input: molecule (SMILES string).
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_hydrogen_bond_basicity(
32
+ name: str,
33
+ molecule: str,
34
+ do_csearch: bool = True,
35
+ do_optimization: bool = True,
36
+ folder_uuid: Optional[str] = None,
37
+ blocking: bool = True,
38
+ ping_interval: int = 5
39
+ ) -> str:
40
+ """Calculate hydrogen bond basicity (pKBHX) values for molecules.
41
+
42
+ Args:
43
+ name: Name for the calculation
44
+ molecule: Molecule SMILES string
45
+ do_csearch: Whether to perform conformational search (default: True)
46
+ do_optimization: Whether to perform optimization (default: True)
47
+ folder_uuid: UUID of folder to organize calculation in
48
+ blocking: Whether to wait for completion (default: True)
49
+ ping_interval: How often to check status in seconds (default: 5)
50
+
51
+ Returns:
52
+ Hydrogen bond basicity calculation results with pKBHX values
53
+ """
54
+ try:
55
+ result = log_rowan_api_call(
56
+ workflow_type="hydrogen_bond_basicity",
57
+ name=name,
58
+ molecule=molecule,
59
+ do_csearch=do_csearch,
60
+ do_optimization=do_optimization,
61
+ folder_uuid=folder_uuid,
62
+ blocking=blocking,
63
+ ping_interval=ping_interval
64
+ )
65
+
66
+ return str(result)
67
+
68
+ except Exception as e:
69
+ error_response = {
70
+ "error": f"Hydrogen bond basicity calculation failed: {str(e)}",
71
+ "name": name,
72
+ "molecule": molecule
73
+ }
74
+ return str(error_response)
75
+
76
+
77
+ def test_rowan_hydrogen_bond_basicity():
78
+ """Test the rowan_hydrogen_bond_basicity function."""
79
+ try:
80
+ # Test with pyridine
81
+ result = rowan_hydrogen_bond_basicity(
82
+ name="test_pyridine_basicity",
83
+ molecule="c1ccncc1"
84
+ )
85
+ print("✅ Hydrogen bond basicity test successful!")
86
+ print(f"Result: {result}")
87
+ return True
88
+ except Exception as e:
89
+ print(f"Hydrogen bond basicity test failed: {e}")
90
+ return False
91
+
92
+
93
+ if __name__ == "__main__":
94
+ test_rowan_hydrogen_bond_basicity()
@@ -0,0 +1,125 @@
1
+ """
2
+ Rowan IRC (Intrinsic Reaction Coordinate) function for MCP tool integration.
3
+ """
4
+
5
+ from typing import Any, Dict, List, Optional
6
+ import rowan
7
+ import logging
8
+ import os
9
+
10
+ # Set up logger
11
+ logger = logging.getLogger(__name__)
12
+
13
+ # Get API key from environment
14
+ api_key = os.environ.get("ROWAN_API_KEY")
15
+ if api_key:
16
+ rowan.api_key = api_key
17
+ else:
18
+ logger.warning("ROWAN_API_KEY not found in environment")
19
+
20
+ def rowan_irc(
21
+ name: str,
22
+ molecule: str,
23
+ mode: str = "rapid",
24
+ solvent: Optional[str] = None,
25
+ preopt: bool = False,
26
+ max_irc_steps: int = 10,
27
+ step_size: float = 0.05,
28
+ starting_ts: Optional[str] = None,
29
+ # Workflow parameters
30
+ folder_uuid: Optional[str] = None,
31
+ blocking: bool = True,
32
+ ping_interval: int = 5
33
+ ) -> str:
34
+ """Follow intrinsic reaction coordinates from transition states.
35
+
36
+ Traces reaction pathways from transition states to reactants and products.
37
+
38
+ Args:
39
+ name: Name for the calculation
40
+ molecule: Molecule SMILES string (should be a transition state)
41
+ mode: Calculation mode ("rapid", "careful", "meticulous")
42
+ solvent: Solvent for the calculation (optional)
43
+ preopt: Whether to pre-optimize the structure before IRC (default: False)
44
+ max_irc_steps: Maximum number of IRC steps to take (default: 10)
45
+ step_size: Step size for IRC in Angstroms (default: 0.05, range: 0.001-0.1)
46
+ starting_ts: UUID of a previous transition state calculation (optional)
47
+ folder_uuid: Optional folder UUID for organization
48
+ blocking: Whether to wait for completion (default: True)
49
+ ping_interval: Check status interval in seconds (default: 5)
50
+
51
+ Returns:
52
+ IRC pathway results
53
+ """
54
+ # Parameter validation
55
+ valid_modes = ["rapid", "careful", "meticulous"]
56
+ mode_lower = mode.lower()
57
+ if mode_lower not in valid_modes:
58
+ return f"Error: Invalid mode '{mode}'. Valid options: {', '.join(valid_modes)}"
59
+
60
+ # Validate step size (0.001 <= step_size <= 0.1)
61
+ if step_size < 0.001 or step_size > 0.1:
62
+ return f"Error: step_size must be between 0.001 and 0.1 Å (got {step_size})"
63
+
64
+ if max_irc_steps <= 0:
65
+ return f"Error: max_irc_steps must be positive (got {max_irc_steps})"
66
+
67
+ try:
68
+ # Build basic parameters for rowan.compute
69
+ compute_params = {
70
+ "name": name,
71
+ "molecule": molecule,
72
+ "workflow_type": "irc",
73
+ "mode": mode_lower,
74
+ "preopt": preopt,
75
+ "max_irc_steps": max_irc_steps,
76
+ "step_size": step_size,
77
+ "folder_uuid": folder_uuid,
78
+ "blocking": blocking,
79
+ "ping_interval": ping_interval
80
+ }
81
+
82
+ # Add optional parameters
83
+ if solvent:
84
+ compute_params["solvent"] = solvent
85
+
86
+ if starting_ts:
87
+ compute_params["starting_ts"] = starting_ts
88
+
89
+ # Submit IRC calculation
90
+ result = rowan.compute(**compute_params)
91
+
92
+ # Format results
93
+ uuid = result.get('uuid', 'N/A')
94
+ status = result.get('status', 'unknown')
95
+
96
+ if blocking:
97
+ if status == "success":
98
+ return f"IRC calculation '{name}' completed successfully!\nUUID: {uuid}"
99
+ else:
100
+ return f"IRC calculation failed\nUUID: {uuid}\nStatus: {status}"
101
+ else:
102
+ return f"IRC calculation '{name}' submitted!\nUUID: {uuid}\nStatus: Running..."
103
+
104
+ except Exception as e:
105
+ logger.error(f"Error in rowan_irc: {str(e)}")
106
+ return f"IRC calculation failed: {str(e)}"
107
+
108
+ def test_rowan_irc():
109
+ """Test the rowan_irc function."""
110
+ try:
111
+ result = rowan_irc(
112
+ name="test_irc",
113
+ molecule="C=C",
114
+ mode="rapid",
115
+ max_irc_steps=5,
116
+ blocking=False
117
+ )
118
+ print(f"IRC test result: {result}")
119
+ return True
120
+ except Exception as e:
121
+ print(f"IRC test failed: {e}")
122
+ return False
123
+
124
+ if __name__ == "__main__":
125
+ test_rowan_irc()