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,130 @@
1
+ """
2
+ Folder management operations for 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 rowan_folder_management(
16
+ action: str,
17
+ folder_uuid: Optional[str] = None,
18
+ name: Optional[str] = None,
19
+ parent_uuid: Optional[str] = None,
20
+ notes: Optional[str] = None,
21
+ starred: Optional[bool] = None,
22
+ public: Optional[bool] = None,
23
+ created_at: Optional[str] = None,
24
+ updated_at: Optional[str] = None,
25
+ is_root: Optional[bool] = None,
26
+ uuid: Optional[str] = None,
27
+ name_contains: Optional[str] = None,
28
+ page: int = 0,
29
+ size: int = 50
30
+ ) -> str:
31
+ """Unified folder management tool for all folder operations. Available actions: create, retrieve, update, delete, list.
32
+
33
+ **Available Actions:**
34
+ - **create**: Create a new folder (requires: name, optional: parent_uuid, notes, starred, public)
35
+ - **retrieve**: Get folder details (requires: folder_uuid)
36
+ - **update**: Update folder properties (requires: folder_uuid, optional: name, parent_uuid, notes, starred, public)
37
+ - **delete**: Delete a folder (requires: folder_uuid)
38
+ - **list**: List folders with filters (optional: name_contains, parent_uuid, starred, public, page, size)
39
+
40
+ Args:
41
+ action: Action to perform ('create', 'retrieve', 'update', 'delete', 'list')
42
+ folder_uuid: UUID of the folder (required for retrieve, update, delete)
43
+ name: Folder name (required for create, optional for update)
44
+ parent_uuid: Parent folder UUID (optional for create/update, if not provided creates in root)
45
+ notes: Folder notes (optional for create/update)
46
+ starred: Star the folder (optional for create/update)
47
+ public: Make folder public (optional for create/update)
48
+ created_at: The date and time at which this folder was created
49
+ updated_at: The date and time at which this folder was most recently updated
50
+ is_root: Whether or not this folder is the user's root folder
51
+ uuid: The UUID of this folder
52
+ name_contains: Filter by name containing text (optional for list)
53
+ page: Page number for pagination (default: 1, for list)
54
+ size: Results per page (default: 50, for list)
55
+
56
+ Returns:
57
+ Results of the folder operation
58
+ """
59
+
60
+ action = action.lower()
61
+
62
+ try:
63
+ if action == "create":
64
+ if not name:
65
+ return "Error: 'name' is required for creating a folder"
66
+
67
+ return rowan.Folder.create(
68
+ name=name,
69
+ parent_uuid=parent_uuid,
70
+ notes=notes or "",
71
+ starred=starred or False,
72
+ public=public or False
73
+ )
74
+
75
+ elif action == "retrieve":
76
+ if not folder_uuid:
77
+ return "Error: 'folder_uuid' is required for retrieving a folder"
78
+
79
+ return rowan.Folder.retrieve(uuid=folder_uuid)
80
+
81
+ elif action == "update":
82
+ if not folder_uuid:
83
+ return "Error: 'folder_uuid' is required for updating a folder"
84
+
85
+ return rowan.Folder.update(
86
+ uuid=folder_uuid,
87
+ name=name,
88
+ notes=notes,
89
+ starred=starred,
90
+ public=public,
91
+ parent_uuid=parent_uuid
92
+
93
+ )
94
+
95
+ elif action == "delete":
96
+ if not folder_uuid:
97
+ return "Error: 'folder_uuid' is required for deleting a folder"
98
+
99
+ rowan.Folder.delete(uuid=folder_uuid)
100
+ return "Folder deleted successfully"
101
+
102
+ elif action == "list":
103
+ return rowan.Folder.list(
104
+ name_contains=name_contains,
105
+ parent_uuid=parent_uuid,
106
+ starred=starred,
107
+ public=public,
108
+ page=page,
109
+ size=size
110
+ )
111
+
112
+ except Exception as e:
113
+ return f"Error in folder {action}: {str(e)}"
114
+
115
+
116
+ def test_rowan_folder_management():
117
+ """Test the rowan_folder_management function."""
118
+ try:
119
+ # Test listing folders
120
+ result = rowan_folder_management(action="list", size=5)
121
+ print("Folder management test successful!")
122
+ print(f"Result: {result[:200]}...") # Show first 200 chars
123
+ return True
124
+ except Exception as e:
125
+ print(f"Folder management test failed: {e}")
126
+ return False
127
+
128
+
129
+ if __name__ == "__main__":
130
+ test_rowan_folder_management()
@@ -0,0 +1,216 @@
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
+
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_fukui(
83
+ name: str,
84
+ molecule: str,
85
+ optimize: bool = True,
86
+ opt_method: Optional[str] = None,
87
+ opt_basis_set: Optional[str] = None,
88
+ opt_engine: Optional[str] = None,
89
+ fukui_method: str = "gfn1_xtb",
90
+ fukui_basis_set: Optional[str] = None,
91
+ fukui_engine: Optional[str] = None,
92
+ charge: int = 0,
93
+ multiplicity: int = 1,
94
+ folder_uuid: Optional[str] = None,
95
+ blocking: bool = True,
96
+ ping_interval: int = 5
97
+ ) -> str:
98
+ """Calculate Fukui indices for reactivity prediction with comprehensive control.
99
+
100
+ Predicts sites of chemical reactivity by analyzing electron density changes upon
101
+ gaining/losing electrons. Uses a two-step process: optimization + Fukui calculation.
102
+
103
+ ** Fukui Index Types:**
104
+ - **f(+)**: Electrophilic attack sites (nucleophile reactivity)
105
+ - **f(-)**: Nucleophilic attack sites (electrophile reactivity)
106
+ - **f(0)**: Radical attack sites (average of f(+) and f(-))
107
+ - **Global Electrophilicity Index**: Overall electrophilic character
108
+
109
+ ** Key Features:**
110
+ - Optional geometry optimization before Fukui calculation
111
+ - Separate control over optimization and Fukui calculation methods
112
+ - Per-atom reactivity indices for site-specific analysis
113
+ - Global reactivity descriptors
114
+
115
+ **Molecule Lookup**: Uses advanced PubChemPy + SQLite caching + RDKit validation system
116
+ for robust molecule identification and SMILES canonicalization.
117
+
118
+ Args:
119
+ name: Name for the calculation
120
+ molecule: Molecule name (e.g., "aspirin", "taxol") or SMILES string
121
+ optimize: Whether to optimize geometry before Fukui calculation (default: True)
122
+ opt_method: Method for optimization (default: None, uses engine default)
123
+ opt_basis_set: Basis set for optimization (default: None, uses engine default)
124
+ opt_engine: Engine for optimization (default: None, auto-selected)
125
+ fukui_method: Method for Fukui calculation (default: "gfn1_xtb")
126
+ fukui_basis_set: Basis set for Fukui calculation (default: None, uses method default)
127
+ fukui_engine: Engine for Fukui calculation (default: None, auto-selected)
128
+ charge: Molecular charge (default: 0)
129
+ multiplicity: Spin multiplicity (default: 1)
130
+ folder_uuid: Optional folder UUID for organization
131
+ blocking: Whether to wait for completion (default: True)
132
+ ping_interval: Check status interval in seconds (default: 5)
133
+
134
+ Returns:
135
+ Fukui indices and reactivity analysis with per-atom and global descriptors
136
+ """
137
+ # Look up SMILES if a common name was provided
138
+ canonical_smiles = lookup_molecule_smiles(molecule)
139
+
140
+ # Build optimization settings if requested
141
+ opt_settings = None
142
+ if optimize:
143
+ opt_settings = {
144
+ "charge": charge,
145
+ "multiplicity": multiplicity
146
+ }
147
+
148
+ # Add optimization method/basis/engine if specified
149
+ if opt_method:
150
+ opt_settings["method"] = opt_method.lower()
151
+ if opt_basis_set:
152
+ opt_settings["basis_set"] = opt_basis_set.lower()
153
+
154
+ # Default to fast optimization if no engine specified
155
+ if not opt_engine and not opt_method:
156
+ opt_settings["method"] = "gfn2_xtb" # Fast optimization
157
+ logger.info(f"No optimization method specified, defaulting to GFN2-xTB")
158
+
159
+ # Build Fukui calculation settings
160
+ fukui_settings = {
161
+ "method": fukui_method.lower(),
162
+ "charge": charge,
163
+ "multiplicity": multiplicity
164
+ }
165
+
166
+ # Add Fukui basis set if specified
167
+ if fukui_basis_set:
168
+ fukui_settings["basis_set"] = fukui_basis_set.lower()
169
+
170
+ # Validate Fukui method
171
+ valid_fukui_methods = ["gfn1_xtb", "gfn2_xtb", "hf", "b3lyp", "pbe", "m06-2x"]
172
+ if fukui_method.lower() not in valid_fukui_methods:
173
+ pass # Warning already logged by cleanup script
174
+
175
+ # Build parameters for Rowan API
176
+ fukui_params = {
177
+ "name": name,
178
+ "molecule": canonical_smiles,
179
+ "fukui_settings": fukui_settings,
180
+ "folder_uuid": folder_uuid,
181
+ "blocking": blocking,
182
+ "ping_interval": ping_interval
183
+ }
184
+
185
+ # Add optimization settings if enabled
186
+ if optimize and opt_settings:
187
+ fukui_params["opt_settings"] = opt_settings
188
+
189
+ # Add engines if specified
190
+ if opt_engine:
191
+ fukui_params["opt_engine"] = opt_engine.lower()
192
+ if fukui_engine:
193
+ fukui_params["fukui_engine"] = fukui_engine.lower()
194
+
195
+ try:
196
+ result = log_rowan_api_call(
197
+ workflow_type="fukui",
198
+ **fukui_params
199
+ )
200
+
201
+ return result
202
+
203
+ except Exception as e:
204
+ return f"Fukui analysis failed: {str(e)}"
205
+
206
+ def test_fukui():
207
+ """Test the fukui function."""
208
+ return rowan_fukui(
209
+ name="test_fukui",
210
+ molecule="benzene",
211
+ fukui_method="gfn1_xtb",
212
+ blocking=True
213
+ )
214
+
215
+ if __name__ == "__main__":
216
+ print(test_fukui())
@@ -0,0 +1,87 @@
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
+
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_hydrogen_bond_basicity(
25
+ name: str,
26
+ molecule: str,
27
+ do_csearch: bool = True,
28
+ do_optimization: bool = True,
29
+ folder_uuid: Optional[str] = None,
30
+ blocking: bool = True,
31
+ ping_interval: int = 5
32
+ ) -> str:
33
+ """Calculate hydrogen bond basicity (pKBHX) values for molecules.
34
+
35
+ Args:
36
+ name: Name for the calculation
37
+ molecule: Molecule SMILES string
38
+ do_csearch: Whether to perform conformational search (default: True)
39
+ do_optimization: Whether to perform optimization (default: True)
40
+ folder_uuid: UUID of folder to organize calculation in
41
+ blocking: Whether to wait for completion (default: True)
42
+ ping_interval: How often to check status in seconds (default: 5)
43
+
44
+ Returns:
45
+ Hydrogen bond basicity calculation results with pKBHX values
46
+ """
47
+ try:
48
+ result = log_rowan_api_call(
49
+ workflow_type="hydrogen_bond_basicity",
50
+ name=name,
51
+ molecule=molecule,
52
+ do_csearch=do_csearch,
53
+ do_optimization=do_optimization,
54
+ folder_uuid=folder_uuid,
55
+ blocking=blocking,
56
+ ping_interval=ping_interval
57
+ )
58
+
59
+ return str(result)
60
+
61
+ except Exception as e:
62
+ error_response = {
63
+ "error": f"Hydrogen bond basicity calculation failed: {str(e)}",
64
+ "name": name,
65
+ "molecule": molecule
66
+ }
67
+ return str(error_response)
68
+
69
+
70
+ def test_rowan_hydrogen_bond_basicity():
71
+ """Test the rowan_hydrogen_bond_basicity function."""
72
+ try:
73
+ # Test with pyridine
74
+ result = rowan_hydrogen_bond_basicity(
75
+ name="test_pyridine_basicity",
76
+ molecule="c1ccncc1"
77
+ )
78
+ print("✅ Hydrogen bond basicity test successful!")
79
+ print(f"Result: {result}")
80
+ return True
81
+ except Exception as e:
82
+ print(f"Hydrogen bond basicity test failed: {e}")
83
+ return False
84
+
85
+
86
+ if __name__ == "__main__":
87
+ 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()
@@ -0,0 +1,120 @@
1
+ """MacropKa workflow function for MCP server."""
2
+
3
+ import os
4
+ import logging
5
+ from typing import Optional, Union, List
6
+
7
+ import rowan
8
+
9
+ # Configure logging
10
+ logger = logging.getLogger(__name__)
11
+
12
+ # Get API key from environment
13
+ api_key = os.environ.get("ROWAN_API_KEY")
14
+ if api_key:
15
+ rowan.api_key = api_key
16
+ else:
17
+ logger.warning("ROWAN_API_KEY not found in environment")
18
+
19
+
20
+ def log_rowan_api_call(func_name: str, **kwargs):
21
+ """Log Rowan API calls for debugging."""
22
+ logger.debug(f"Calling {func_name} with args: {kwargs}")
23
+
24
+
25
+ def rowan_macropka(
26
+ name: str,
27
+ molecule: str,
28
+ min_pH: float = 0.0,
29
+ max_pH: float = 14.0,
30
+ max_charge: int = 2,
31
+ min_charge: int = -2,
32
+ compute_aqueous_solubility: bool = False,
33
+ compute_solvation_energy: bool = True,
34
+ folder_uuid: Optional[str] = None,
35
+ blocking: bool = True,
36
+ ping_interval: int = 5
37
+ ) -> str:
38
+ """
39
+ Calculate macroscopic pKa values and related properties for a molecule.
40
+
41
+ This workflow computes pKa values, microstates, isoelectric point, and optionally
42
+ solvation energy and aqueous solubility across different pH values.
43
+
44
+ Args:
45
+ name: Name for the calculation
46
+ molecule: SMILES string of the molecule
47
+ min_pH: Minimum pH for calculations (default: 0.0)
48
+ max_pH: Maximum pH for calculations (default: 14.0)
49
+ max_charge: Maximum charge to consider for microstates (default: 2)
50
+ min_charge: Minimum charge to consider for microstates (default: -2)
51
+ compute_aqueous_solubility: Whether to compute aqueous solubility by pH (default: False)
52
+ compute_solvation_energy: Whether to compute solvation energy for Kpuu (default: True)
53
+ folder_uuid: UUID of folder to save results in
54
+ blocking: Wait for calculation to complete (default: True)
55
+ ping_interval: How often to check status in blocking mode (default: 5 seconds)
56
+
57
+ Returns:
58
+ String with workflow UUID or results depending on blocking mode
59
+ """
60
+ try:
61
+ # Validate pH range
62
+ if min_pH >= max_pH:
63
+ return "Error: min_pH must be less than max_pH"
64
+
65
+ # Validate charge range
66
+ if min_charge >= max_charge:
67
+ return "Error: min_charge must be less than max_charge"
68
+
69
+ # Log the API call
70
+ log_rowan_api_call(
71
+ "rowan.compute",
72
+ workflow_type="macropka",
73
+ name=name,
74
+ molecule=molecule,
75
+ min_pH=min_pH,
76
+ max_pH=max_pH,
77
+ max_charge=max_charge,
78
+ min_charge=min_charge,
79
+ compute_aqueous_solubility=compute_aqueous_solubility,
80
+ compute_solvation_energy=compute_solvation_energy,
81
+ folder_uuid=folder_uuid,
82
+ blocking=blocking,
83
+ ping_interval=ping_interval
84
+ )
85
+
86
+ # Submit calculation
87
+ result = rowan.compute(
88
+ workflow_type="macropka",
89
+ name=name,
90
+ molecule=molecule, # Required by rowan.compute() API
91
+ folder_uuid=folder_uuid,
92
+ blocking=blocking,
93
+ ping_interval=ping_interval,
94
+ # Workflow-specific parameters for MacropKaWorkflow
95
+ initial_smiles=molecule, # Required by MacropKaWorkflow Pydantic model
96
+ min_pH=min_pH,
97
+ max_pH=max_pH,
98
+ max_charge=max_charge,
99
+ min_charge=min_charge,
100
+ compute_aqueous_solubility=compute_aqueous_solubility,
101
+ compute_solvation_energy=compute_solvation_energy
102
+ )
103
+
104
+ return result
105
+
106
+ except Exception as e:
107
+ logger.error(f"Error in rowan_macropka: {str(e)}")
108
+ return f"MacropKa calculation failed: {str(e)}"
109
+
110
+
111
+ # Test function
112
+ if __name__ == "__main__":
113
+ # Test with ethanol
114
+ result = rowan_macropka(
115
+ name="Ethanol MacropKa Test",
116
+ molecule="CCO",
117
+ compute_aqueous_solubility=True,
118
+ blocking=True
119
+ )
120
+ print(result)