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