rowan-mcp 2.0.0__py3-none-any.whl → 2.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of rowan-mcp might be problematic. Click here for more details.

Files changed (31) hide show
  1. rowan_mcp/functions/admet.py +89 -0
  2. rowan_mcp/functions/bde.py +106 -0
  3. rowan_mcp/functions/calculation_retrieve.py +89 -0
  4. rowan_mcp/functions/conformers.py +77 -0
  5. rowan_mcp/functions/descriptors.py +89 -0
  6. rowan_mcp/functions/docking.py +290 -0
  7. rowan_mcp/functions/docking_enhanced.py +174 -0
  8. rowan_mcp/functions/electronic_properties.py +202 -0
  9. rowan_mcp/functions/folder_management.py +130 -0
  10. rowan_mcp/functions/fukui.py +216 -0
  11. rowan_mcp/functions/hydrogen_bond_basicity.py +87 -0
  12. rowan_mcp/functions/irc.py +125 -0
  13. rowan_mcp/functions/macropka.py +120 -0
  14. rowan_mcp/functions/molecular_converter.py +423 -0
  15. rowan_mcp/functions/molecular_dynamics.py +191 -0
  16. rowan_mcp/functions/molecule_lookup.py +57 -0
  17. rowan_mcp/functions/multistage_opt.py +168 -0
  18. rowan_mcp/functions/pdb_handler.py +200 -0
  19. rowan_mcp/functions/pka.py +81 -0
  20. rowan_mcp/functions/redox_potential.py +349 -0
  21. rowan_mcp/functions/scan.py +536 -0
  22. rowan_mcp/functions/scan_analyzer.py +347 -0
  23. rowan_mcp/functions/solubility.py +277 -0
  24. rowan_mcp/functions/spin_states.py +747 -0
  25. rowan_mcp/functions/system_management.py +361 -0
  26. rowan_mcp/functions/tautomers.py +88 -0
  27. rowan_mcp/functions/workflow_management.py +422 -0
  28. {rowan_mcp-2.0.0.dist-info → rowan_mcp-2.0.1.dist-info}/METADATA +3 -18
  29. {rowan_mcp-2.0.0.dist-info → rowan_mcp-2.0.1.dist-info}/RECORD +31 -4
  30. {rowan_mcp-2.0.0.dist-info → rowan_mcp-2.0.1.dist-info}/WHEEL +0 -0
  31. {rowan_mcp-2.0.0.dist-info → rowan_mcp-2.0.1.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,290 @@
1
+ """
2
+ Rowan docking function for MCP tool integration.
3
+ Implements protein-ligand docking following the stjames-public workflow pattern.
4
+ """
5
+
6
+ from typing import Optional, Union, List, Tuple, Dict, Any
7
+ import rowan
8
+ import logging
9
+ import os
10
+ import requests
11
+
12
+ # Set up logging
13
+ logger = logging.getLogger(__name__)
14
+
15
+ # Get API key from environment
16
+ api_key = os.environ.get("ROWAN_API_KEY")
17
+ if api_key:
18
+ rowan.api_key = api_key
19
+ else:
20
+ logger.warning("ROWAN_API_KEY not found in environment")
21
+
22
+ def fetch_pdb_content(pdb_id: str) -> Optional[str]:
23
+ """Fetch PDB content from RCSB PDB database.
24
+
25
+ Args:
26
+ pdb_id: 4-character PDB ID (e.g., "1COX")
27
+
28
+ Returns:
29
+ PDB file content as string, or None if fetch fails
30
+ """
31
+ try:
32
+ url = f"https://files.rcsb.org/download/{pdb_id.upper()}.pdb"
33
+ response = requests.get(url, timeout=30)
34
+ response.raise_for_status()
35
+ return response.text
36
+ except Exception as e:
37
+ logger.error(f"Failed to fetch PDB {pdb_id}: {str(e)}")
38
+ return None
39
+
40
+ def rowan_docking(
41
+ name: str,
42
+ # Ligand specification (following stjames priority)
43
+ molecules: Optional[List[str]] = None,
44
+ smiles: Optional[Union[str, List[str]]] = None,
45
+ # Protein specification
46
+ target: Optional[str] = None,
47
+ target_uuid: Optional[str] = None,
48
+ # Pocket specification
49
+ pocket: Optional[Tuple[Tuple[float, float, float], Tuple[float, float, float]]] = None,
50
+ # Workflow parameters
51
+ do_csearch: bool = True,
52
+ do_optimization: bool = True,
53
+ do_pose_refinement: bool = True,
54
+ conformers: Optional[List[str]] = None,
55
+ # MCP workflow parameters
56
+ folder_uuid: Optional[str] = None,
57
+ blocking: bool = True,
58
+ ping_interval: int = 5
59
+ ) -> str:
60
+ """Perform protein-ligand docking using Rowan's ML workflow.
61
+
62
+ Performs comprehensive protein-ligand docking with conformer generation,
63
+ strain filtering, AutoDock Vina docking, and pose refinement.
64
+
65
+ IMPORTANT: For docking, you need to provide the protein target in one of two ways:
66
+ 1. target_uuid: UUID of a pre-uploaded protein in your Rowan account
67
+ 2. target: Reserved for future use (currently not supported via API)
68
+
69
+ To dock against a known protein like 1COX, you should:
70
+ 1. Upload the PDB file to your Rowan account via the web interface
71
+ 2. Get the protein UUID from your account
72
+ 3. Use that UUID with the target_uuid parameter
73
+
74
+ Args:
75
+ name: Name for the docking calculation
76
+ molecules: List of molecules to dock (SMILES strings)
77
+ smiles: SMILES string(s) of ligands (can be string or list)
78
+ target: Not currently supported - use target_uuid instead
79
+ target_uuid: UUID of pre-uploaded protein target from your Rowan account
80
+ pocket: Tuple of (center, size) where each is (x, y, z) coordinates
81
+ Example: ((10.0, 15.0, 20.0), (20.0, 20.0, 20.0))
82
+ do_csearch: Whether to perform conformer search (default: True)
83
+ do_optimization: Whether to optimize starting structures (default: True)
84
+ do_pose_refinement: Whether to optimize non-rotatable bonds in output poses (default: True)
85
+ conformers: List of pre-optimized conformer UUIDs (optional)
86
+ folder_uuid: Optional folder UUID to organize the calculation
87
+ blocking: Whether to wait for completion (default: True)
88
+ ping_interval: Interval in seconds to check calculation status
89
+
90
+ Returns:
91
+ Formatted string with workflow results or submission confirmation
92
+
93
+ Examples:
94
+ # Using pre-uploaded protein
95
+ rowan_docking(
96
+ name="aspirin_docking",
97
+ smiles="CC(=O)Oc1ccccc1C(=O)O",
98
+ target_uuid="your-protein-uuid-here",
99
+ pocket=((10.0, 15.0, 20.0), (20.0, 20.0, 20.0))
100
+ )
101
+
102
+ # Multiple molecules
103
+ rowan_docking(
104
+ name="multi_ligand_docking",
105
+ molecules=["CCO", "CC(C)O", "CCCO"],
106
+ target_uuid="your-protein-uuid-here",
107
+ pocket=((0.0, 0.0, 0.0), (25.0, 25.0, 25.0))
108
+ )
109
+ """
110
+
111
+ # Validate that we have ligand input
112
+ if molecules is None and smiles is None:
113
+ return "Error: Either 'molecules' or 'smiles' parameter must be provided"
114
+
115
+ # Validate that we have protein input
116
+ if target is None and target_uuid is None:
117
+ return "Error: Either 'target' or 'target_uuid' parameter must be provided"
118
+
119
+ # If target is provided, inform user it's not currently supported
120
+ if target is not None:
121
+ return ("Error: Direct PDB content upload is not currently supported through this API. "
122
+ "Please use one of these alternatives:\n"
123
+ "1. Upload your PDB file through the Rowan web interface at https://labs.rowansci.com\n"
124
+ "2. Get the protein UUID from your account\n"
125
+ "3. Use the 'target_uuid' parameter with that UUID\n\n"
126
+ "For known proteins like 1COX, 2COX, etc., they may already be available "
127
+ "in the Rowan protein library.")
128
+
129
+ # Validate pocket if provided
130
+ if pocket is not None:
131
+ try:
132
+ center, size = pocket
133
+ if len(center) != 3 or len(size) != 3:
134
+ return "Error: Pocket must be ((x,y,z), (x,y,z)) format"
135
+
136
+ # Validate that size dimensions are positive
137
+ if any(s <= 0 for s in size):
138
+ return "Error: Pocket size dimensions must be positive"
139
+
140
+ except (TypeError, ValueError) as e:
141
+ return f"Error: Invalid pocket format. Expected ((x,y,z), (x,y,z)). Got: {pocket}"
142
+ else:
143
+ # Default pocket if not provided
144
+ pocket = ((0.0, 0.0, 0.0), (20.0, 20.0, 20.0))
145
+ logger.info("Using default pocket: center=(0,0,0), size=(20,20,20)")
146
+
147
+ # Determine initial_molecule for rowan.compute()
148
+ # For docking, the 'molecule' parameter is the ligand to dock
149
+ initial_molecule = None
150
+ ligands_list = None
151
+
152
+ if molecules and len(molecules) > 0:
153
+ initial_molecule = molecules[0]
154
+ ligands_list = molecules
155
+ elif isinstance(smiles, str):
156
+ initial_molecule = smiles
157
+ ligands_list = [smiles]
158
+ elif isinstance(smiles, list) and len(smiles) > 0:
159
+ initial_molecule = smiles[0]
160
+ ligands_list = smiles
161
+ else:
162
+ return "Error: Could not determine initial molecule from inputs"
163
+
164
+ try:
165
+ # Build parameters following stjames DockingWorkflow structure
166
+ compute_params = {
167
+ "name": name,
168
+ "molecule": initial_molecule, # Required by rowan.compute() - this is the ligand
169
+ "workflow_type": "docking",
170
+ "folder_uuid": folder_uuid,
171
+ "blocking": blocking,
172
+ "ping_interval": ping_interval,
173
+ # DockingWorkflow specific parameters
174
+ "do_csearch": do_csearch,
175
+ "do_optimization": do_optimization,
176
+ "do_pose_refinement": do_pose_refinement,
177
+ "pocket": pocket,
178
+ }
179
+
180
+ # For multiple ligands, pass them as 'molecules' parameter
181
+ # This allows docking multiple ligands in one workflow
182
+ if ligands_list and len(ligands_list) > 1:
183
+ compute_params["molecules"] = ligands_list
184
+
185
+ # Add protein specification (only one of these should be present)
186
+ if target_uuid is not None:
187
+ compute_params["target_uuid"] = target_uuid
188
+ # Note: Direct PDB upload is not supported in current implementation
189
+ # Users must pre-upload proteins through the Rowan web interface
190
+
191
+ # Add conformers if provided
192
+ if conformers is not None:
193
+ compute_params["conformers"] = conformers
194
+
195
+ # Submit docking calculation and return raw result
196
+ result = rowan.compute(**compute_params)
197
+ return result
198
+
199
+ except Exception as e:
200
+ logger.error(f"Error in rowan_docking: {str(e)}")
201
+ return f"Docking calculation failed: {str(e)}"
202
+
203
+ def rowan_docking_pdb_id(
204
+ name: str,
205
+ pdb_id: str,
206
+ # Ligand specification
207
+ molecules: Optional[List[str]] = None,
208
+ smiles: Optional[Union[str, List[str]]] = None,
209
+ # Other parameters same as rowan_docking
210
+ pocket: Optional[Tuple[Tuple[float, float, float], Tuple[float, float, float]]] = None,
211
+ do_csearch: bool = True,
212
+ do_optimization: bool = True,
213
+ do_pose_refinement: bool = True,
214
+ conformers: Optional[List[str]] = None,
215
+ folder_uuid: Optional[str] = None,
216
+ blocking: bool = True,
217
+ ping_interval: int = 5
218
+ ) -> str:
219
+ """Helper function that explains how to dock with a PDB ID.
220
+
221
+ NOTE: Direct PDB upload is not currently supported through the API.
222
+ This function provides guidance on how to dock against known proteins.
223
+
224
+ Args:
225
+ name: Name for the docking calculation
226
+ pdb_id: 4-character PDB ID (e.g., "1COX", "2COX", "6COX")
227
+ (other parameters same as rowan_docking)
228
+
229
+ Returns:
230
+ Instructions on how to perform docking with the given PDB
231
+ """
232
+ # Check if we can fetch the PDB to verify it exists
233
+ logger.info(f"Checking if PDB {pdb_id} exists...")
234
+ pdb_content = fetch_pdb_content(pdb_id)
235
+
236
+ if pdb_content is None:
237
+ return (f"Error: PDB ID '{pdb_id}' not found in RCSB database. "
238
+ f"Please check that the PDB ID is valid.")
239
+
240
+ # Provide instructions for the user
241
+ ligand_param = ""
242
+ if molecules:
243
+ ligand_param = f" molecules={molecules},\n"
244
+ elif smiles:
245
+ if isinstance(smiles, str):
246
+ ligand_param = f' smiles="{smiles}",\n'
247
+ else:
248
+ ligand_param = f" smiles={smiles},\n"
249
+
250
+ return (f"PDB {pdb_id} found in RCSB database!\n\n"
251
+ f"To perform docking with this protein:\n\n"
252
+ f"1. Go to https://labs.rowansci.com\n"
253
+ f"2. Upload the PDB file for {pdb_id}\n"
254
+ f" - You can download it from: https://files.rcsb.org/download/{pdb_id.upper()}.pdb\n"
255
+ f"3. Once uploaded, find the protein UUID in your account\n"
256
+ f"4. Use the docking function with target_uuid:\n\n"
257
+ f"```python\n"
258
+ f"rowan_docking(\n"
259
+ f" name=\"{name}\",\n"
260
+ f"{ligand_param}"
261
+ f" target_uuid=\"your-protein-uuid-here\",\n"
262
+ f" pocket={pocket},\n"
263
+ f" do_csearch={do_csearch},\n"
264
+ f" do_optimization={do_optimization},\n"
265
+ f" do_pose_refinement={do_pose_refinement}\n"
266
+ f")\n"
267
+ f"```\n\n"
268
+ f"Note: Some common proteins may already be available in the Rowan protein library.\n"
269
+ f"Check your account for pre-uploaded structures.")
270
+
271
+ def test_rowan_docking():
272
+ """Test the rowan_docking function."""
273
+ try:
274
+ # Test with minimal parameters
275
+ result = rowan_docking(
276
+ name="test_docking",
277
+ smiles="CCO", # Ethanol
278
+ target_uuid="test-protein-uuid", # Would need real UUID
279
+ pocket=((0.0, 0.0, 0.0), (20.0, 20.0, 20.0)),
280
+ blocking=False
281
+ )
282
+ print("Docking test result:")
283
+ print(result)
284
+ return True
285
+ except Exception as e:
286
+ print(f"Docking test failed: {e}")
287
+ return False
288
+
289
+ if __name__ == "__main__":
290
+ test_rowan_docking()
@@ -0,0 +1,174 @@
1
+ """
2
+ Enhanced docking function that accepts PDB IDs directly
3
+ """
4
+
5
+ from typing import Optional, Union, List, Tuple
6
+ import logging
7
+ from .docking import rowan_docking
8
+ from .pdb_handler import handle_pdb_input
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ def rowan_docking_enhanced(
13
+ name: str,
14
+ # Ligand specification
15
+ smiles: Optional[Union[str, List[str]]] = None,
16
+ molecules: Optional[List[str]] = None,
17
+ # Protein specification - NEW: accepts PDB ID or UUID
18
+ target: Optional[str] = None, # Can be PDB ID (e.g., "4Z18") or UUID
19
+ # Pocket specification
20
+ pocket: Optional[Tuple[Tuple[float, float, float], Tuple[float, float, float]]] = None,
21
+ # Workflow parameters
22
+ do_csearch: bool = True,
23
+ do_optimization: bool = True,
24
+ do_pose_refinement: bool = True,
25
+ conformers: Optional[List[str]] = None,
26
+ # MCP workflow parameters
27
+ folder_uuid: Optional[str] = None,
28
+ blocking: bool = True,
29
+ ping_interval: int = 5
30
+ ) -> str:
31
+ """
32
+ Enhanced docking function that accepts PDB IDs directly.
33
+
34
+ This function intelligently handles protein input:
35
+ - If target looks like a UUID (36 chars with dashes), uses it directly
36
+ - If target looks like a PDB ID (e.g., "4Z18"), attempts to fetch and use it
37
+ - Provides clear instructions when manual upload is needed
38
+
39
+ Args:
40
+ name: Name for the docking calculation
41
+ smiles: SMILES string(s) of ligands
42
+ molecules: Alternative to smiles - list of molecules
43
+ target: PDB ID (e.g., "4Z18") or protein UUID
44
+ pocket: Pocket specification ((center), (size))
45
+ (other parameters same as rowan_docking)
46
+
47
+ Returns:
48
+ Docking results or clear instructions on what to do
49
+
50
+ Examples:
51
+ # Using PDB ID directly
52
+ rowan_docking_enhanced(
53
+ name="my_docking",
54
+ smiles="CC(=O)Oc1ccccc1C(=O)O", # Aspirin
55
+ target="4Z18", # PDB ID
56
+ pocket=((10.0, 15.0, 20.0), (20.0, 20.0, 20.0))
57
+ )
58
+
59
+ # Using UUID (if you already have one)
60
+ rowan_docking_enhanced(
61
+ name="my_docking",
62
+ smiles="CC(=O)Oc1ccccc1C(=O)O",
63
+ target="abc123de-f456-7890-ghij-klmnopqrstuv", # UUID
64
+ pocket=((10.0, 15.0, 20.0), (20.0, 20.0, 20.0))
65
+ )
66
+ """
67
+
68
+ # Validate ligand input
69
+ if smiles is None and molecules is None:
70
+ return "Error: Either 'smiles' or 'molecules' must be provided"
71
+
72
+ # Validate protein input
73
+ if target is None:
74
+ return "Error: 'target' parameter is required (PDB ID or protein UUID)"
75
+
76
+ # Handle the target input
77
+ logger.info(f"Processing target input: {target}")
78
+ pdb_result = handle_pdb_input(target, folder_uuid)
79
+
80
+ # If we successfully got a UUID, proceed with docking
81
+ if pdb_result['success'] and pdb_result['uuid']:
82
+ logger.info(f"Successfully resolved target to UUID: {pdb_result['uuid']}")
83
+
84
+ # Call the main docking function with the UUID
85
+ result = rowan_docking(
86
+ name=name,
87
+ smiles=smiles,
88
+ molecules=molecules,
89
+ target_uuid=pdb_result['uuid'],
90
+ pocket=pocket,
91
+ do_csearch=do_csearch,
92
+ do_optimization=do_optimization,
93
+ do_pose_refinement=do_pose_refinement,
94
+ conformers=conformers,
95
+ folder_uuid=folder_uuid,
96
+ blocking=blocking,
97
+ ping_interval=ping_interval
98
+ )
99
+
100
+ # Add context about PDB if it was used
101
+ if 'pdb_id' in pdb_result:
102
+ result = f"🧬 Using PDB {pdb_result['pdb_id']}:\n\n{result}"
103
+
104
+ return result
105
+
106
+ # If we couldn't get a UUID, provide helpful information
107
+ else:
108
+ pdb_id = pdb_result.get('pdb_id', target)
109
+
110
+ # Format a helpful response
111
+ response = f"🔍 PDB Input Detected: {pdb_id}\n\n"
112
+ response += f"❌ {pdb_result['message']}\n\n"
113
+
114
+ if pdb_result.get('instructions'):
115
+ response += pdb_result['instructions']
116
+ else:
117
+ response += f"""
118
+ 📋 To dock with PDB {pdb_id}:
119
+
120
+ 1. **Download the PDB file**:
121
+ https://files.rcsb.org/download/{pdb_id}.pdb
122
+
123
+ 2. **Upload to Rowan**:
124
+ https://labs.rowansci.com
125
+
126
+ 3. **Get the protein UUID** from your account
127
+
128
+ 4. **Run docking with the UUID**:
129
+ ```python
130
+ rowan_docking_enhanced(
131
+ name="{name}",
132
+ smiles={f'"{smiles}"' if isinstance(smiles, str) else smiles},
133
+ target="your-protein-uuid-here",
134
+ pocket={pocket}
135
+ )
136
+ ```
137
+
138
+ Note: Direct PDB upload through the API is not currently supported.
139
+ Proteins must be uploaded through the web interface.
140
+ """
141
+
142
+ return response
143
+
144
+ def test_enhanced_docking():
145
+ """Test the enhanced docking function"""
146
+
147
+ print("Testing enhanced docking with PDB ID...")
148
+
149
+ # Test with PDB ID
150
+ result = rowan_docking_enhanced(
151
+ name="test_4z18_docking",
152
+ smiles="CC(=O)Oc1ccccc1C(=O)O", # Aspirin
153
+ target="4Z18", # PDB ID
154
+ pocket=((10.0, 15.0, 20.0), (20.0, 20.0, 20.0)),
155
+ blocking=False
156
+ )
157
+
158
+ print(result)
159
+
160
+ # Test with UUID
161
+ print("\n" + "="*60 + "\n")
162
+ print("Testing with UUID...")
163
+
164
+ result = rowan_docking_enhanced(
165
+ name="test_uuid_docking",
166
+ smiles="CC(=O)Oc1ccccc1C(=O)O",
167
+ target="12345678-1234-5678-1234-567812345678", # Fake UUID format
168
+ blocking=False
169
+ )
170
+
171
+ print(result)
172
+
173
+ if __name__ == "__main__":
174
+ test_enhanced_docking()
@@ -0,0 +1,202 @@
1
+ """
2
+ Electronic Properties Analysis for Rowan MCP Server
3
+
4
+ This module provides electronic structure property calculations including:
5
+ - HOMO/LUMO energies and molecular orbitals
6
+ - Electron density and electrostatic potential
7
+ - Population analysis and bond orders
8
+ - Orbital visualization data
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
+
26
+
27
+ def log_rowan_api_call(workflow_type: str, **kwargs):
28
+ """Log Rowan API calls with detailed parameters."""
29
+
30
+ try:
31
+ start_time = time.time()
32
+
33
+ if not rowan:
34
+ raise ImportError("Rowan package not available - please install with 'pip install rowan'")
35
+
36
+ logger.info(f"Calling Rowan {workflow_type} workflow")
37
+ for key, value in kwargs.items():
38
+ if key != 'ping_interval':
39
+ logger.info(f" {key}: {value}")
40
+
41
+ result = rowan.compute(workflow_type=workflow_type, **kwargs)
42
+
43
+ end_time = time.time()
44
+ duration = end_time - start_time
45
+ logger.info(f"Rowan {workflow_type} completed in {duration:.2f} seconds")
46
+
47
+ return result
48
+
49
+ except Exception as e:
50
+ logger.error(f"Rowan {workflow_type} failed: {str(e)}")
51
+ raise e
52
+
53
+ def lookup_molecule_smiles(molecule_name: str) -> str:
54
+ """Look up canonical SMILES using the advanced molecule_lookup system.
55
+
56
+ Uses PubChemPy + SQLite caching + RDKit validation for scalable molecule lookup.
57
+ """
58
+ try:
59
+ # Import the advanced molecule lookup system
60
+ from .molecule_lookup import get_lookup_instance
61
+
62
+ lookup = get_lookup_instance()
63
+ smiles, source, metadata = lookup.get_smiles(molecule_name)
64
+
65
+ if smiles:
66
+ logger.info(f"Molecule lookup successful: '{molecule_name}' → '{smiles}' (source: {source})")
67
+ return smiles
68
+ else:
69
+ logger.warning(f"Molecule lookup failed for '{molecule_name}': {metadata.get('error', 'Unknown error')}")
70
+ # Return original input as fallback (might be valid SMILES)
71
+ return molecule_name
72
+
73
+ except ImportError as e:
74
+ logger.error(f"Could not import molecule_lookup: {e}")
75
+ # Fallback: return original input
76
+ return molecule_name
77
+ except Exception as e:
78
+ logger.error(f"Molecule lookup error for '{molecule_name}': {e}")
79
+ # Fallback: return original input
80
+ return molecule_name
81
+
82
+ def rowan_electronic_properties(
83
+ name: str,
84
+ molecule: str,
85
+ # Settings parameters (quantum chemistry calculation settings)
86
+ method: Optional[str] = None,
87
+ basis_set: Optional[str] = None,
88
+ engine: Optional[str] = None,
89
+ charge: int = 0,
90
+ multiplicity: int = 1,
91
+ # Cube computation control parameters
92
+ compute_density_cube: bool = True,
93
+ compute_electrostatic_potential_cube: bool = True,
94
+ compute_num_occupied_orbitals: int = 1,
95
+ compute_num_virtual_orbitals: int = 1,
96
+ # Workflow control parameters
97
+ mode: Optional[str] = None,
98
+ folder_uuid: Optional[str] = None,
99
+ blocking: bool = True,
100
+ ping_interval: int = 5
101
+ ) -> str:
102
+ """Calculate comprehensive electronic structure properties using Rowan's ElectronicPropertiesWorkflow.
103
+
104
+ Implements the ElectronicPropertiesWorkflow class for computing detailed electronic properties including:
105
+ - **Molecular Orbitals**: HOMO/LUMO energies, orbital cubes, occupation numbers
106
+ - **Electron Density**: Total, α/β spin densities, spin density differences
107
+ - **Electrostatic Properties**: Dipole moments, quadrupole moments, electrostatic potential
108
+ - **Population Analysis**: Mulliken charges, Löwdin charges
109
+ - **Bond Analysis**: Wiberg bond orders, Mayer bond orders
110
+ - **Visualization Data**: Cube files for density, ESP, and molecular orbitals
111
+
112
+ **Molecule Lookup**: Uses advanced PubChemPy + SQLite caching + RDKit validation system
113
+ for robust molecule identification and SMILES canonicalization.
114
+
115
+ Args:
116
+ name: Name for the calculation
117
+ molecule: Molecule name (e.g., "aspirin", "taxol") or SMILES string
118
+ method: QM method (default: b3lyp for electronic properties)
119
+ basis_set: Basis set (default: def2-svp for balanced accuracy)
120
+ engine: Computational engine (default: psi4)
121
+ charge: Molecular charge (default: 0)
122
+ multiplicity: Spin multiplicity (default: 1 for singlet)
123
+ compute_density_cube: Generate electron density cube (default: True)
124
+ compute_electrostatic_potential_cube: Generate ESP cube (default: True)
125
+ compute_num_occupied_orbitals: Number of occupied MOs to save (default: 1)
126
+ compute_num_virtual_orbitals: Number of virtual MOs to save (default: 1)
127
+ mode: Calculation mode/precision (optional)
128
+ folder_uuid: Optional folder UUID for organization
129
+ blocking: Whether to wait for completion (default: True)
130
+ ping_interval: Check status interval in seconds (default: 5)
131
+
132
+ Returns:
133
+ Comprehensive electronic properties results following ElectronicPropertiesWorkflow format
134
+ """
135
+ # Look up canonical SMILES using advanced molecule lookup (PubChemPy + caching + RDKit)
136
+ canonical_smiles = lookup_molecule_smiles(molecule)
137
+
138
+ # Apply smart defaults for electronic properties calculations
139
+ if method is None:
140
+ method = "b3lyp" # Good for electronic properties
141
+ if basis_set is None:
142
+ basis_set = "def2-svp" # Balanced accuracy/cost for properties
143
+ if engine is None:
144
+ engine = "psi4" # Robust for electronic properties
145
+
146
+ # Validate orbital count parameters
147
+ if compute_num_occupied_orbitals < 0:
148
+ return f"compute_num_occupied_orbitals must be non-negative (got {compute_num_occupied_orbitals})"
149
+ if compute_num_virtual_orbitals < 0:
150
+ return f"compute_num_virtual_orbitals must be non-negative (got {compute_num_virtual_orbitals})"
151
+
152
+ # Build parameters following ElectronicPropertiesWorkflow specification
153
+ electronic_params = {
154
+ "name": name,
155
+ "molecule": canonical_smiles, # API interface requirement
156
+ "initial_molecule": canonical_smiles, # ElectronicPropertiesWorkflow requirement
157
+ # Settings (quantum chemistry parameters) - exactly as in ElectronicPropertiesWorkflow
158
+ "settings": {
159
+ "method": method.lower(),
160
+ "basis_set": basis_set.lower(),
161
+ "engine": engine.lower(),
162
+ "charge": charge,
163
+ "multiplicity": multiplicity
164
+ },
165
+ # Cube computation control - exactly as in ElectronicPropertiesWorkflow
166
+ "compute_density_cube": compute_density_cube,
167
+ "compute_electrostatic_potential_cube": compute_electrostatic_potential_cube,
168
+ "compute_num_occupied_orbitals": compute_num_occupied_orbitals,
169
+ "compute_num_virtual_orbitals": compute_num_virtual_orbitals,
170
+ # Workflow parameters
171
+ "folder_uuid": folder_uuid,
172
+ "blocking": blocking,
173
+ "ping_interval": ping_interval
174
+ }
175
+
176
+ # Add mode if specified (optional in ElectronicPropertiesWorkflow)
177
+ if mode:
178
+ electronic_params["mode"] = mode
179
+
180
+ try:
181
+ result = log_rowan_api_call(
182
+ workflow_type="electronic_properties",
183
+ **electronic_params
184
+ )
185
+
186
+ return result
187
+
188
+ except Exception as e:
189
+ return f"Electronic properties calculation failed: {str(e)}"
190
+
191
+ def test_electronic_properties():
192
+ """Test the electronic properties function with advanced molecule lookup."""
193
+ return rowan_electronic_properties(
194
+ name="test_electronic_properties",
195
+ molecule="aspirin", # Test advanced lookup with a pharmaceutical
196
+ method="hf",
197
+ basis_set="sto-3g",
198
+ blocking=True
199
+ )
200
+
201
+ if __name__ == "__main__":
202
+ print(test_electronic_properties())