rowan-mcp 1.0.1__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 +2 -2
  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.1.dist-info → rowan_mcp-2.0.0.dist-info}/METADATA +49 -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 -137
  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.1.dist-info/RECORD +0 -34
  69. {rowan_mcp-1.0.1.dist-info → rowan_mcp-2.0.0.dist-info}/WHEEL +0 -0
  70. {rowan_mcp-1.0.1.dist-info → rowan_mcp-2.0.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,158 @@
1
+ """
2
+ Rowan v2 API: Conformer Search Workflow
3
+ Search for low-energy molecular conformations using various methods.
4
+ """
5
+
6
+ from typing import Any, Dict, Annotated
7
+ import rowan
8
+ import stjames
9
+
10
+ def submit_conformer_search_workflow(
11
+ initial_molecule: Annotated[str, "SMILES string representing the initial structure"],
12
+ conf_gen_mode: Annotated[str, "Conformer generation mode: 'rapid' (fast), 'careful' (balanced), or 'meticulous' (thorough)"] = "rapid",
13
+ final_method: Annotated[str, "Final optimization method (e.g., 'aimnet2_wb97md3', 'r2scan_3c', 'wb97x-d3_def2-tzvp')"] = "aimnet2_wb97md3",
14
+ solvent: Annotated[str, "Solvent for implicit solvation (SMILES or name). Empty string for vacuum"] = "",
15
+ transition_state: Annotated[bool, "Whether to search for transition state conformers"] = False,
16
+ name: Annotated[str, "Workflow name for identification and tracking"] = "Conformer Search Workflow",
17
+ folder_uuid: Annotated[str, "UUID of folder to organize this workflow. Empty string uses default folder"] = "",
18
+ max_credits: Annotated[int, "Maximum credits to spend on this calculation. 0 for no limit"] = 0
19
+ ):
20
+ """Submit a conformer search workflow using Rowan v2 API.
21
+
22
+ Args:
23
+ initial_molecule: SMILES string representing the initial structure
24
+ conf_gen_mode: Conformer generation mode: 'rapid' (fast), 'careful' (balanced), or 'meticulous' (thorough)
25
+ final_method: Final optimization method (e.g., 'aimnet2_wb97md3', 'r2scan_3c', 'wb97x-d3_def2-tzvp')
26
+ solvent: Solvent for implicit solvation (e.g., 'water', 'ethanol', 'dmso'). Empty string for gas phase.
27
+ transition_state: Whether to search for transition state conformers
28
+ name: Workflow name for identification and tracking
29
+ folder_uuid: UUID of folder to organize this workflow. Empty string uses default folder.
30
+ max_credits: Maximum credits to spend on this calculation. 0 for no limit.
31
+
32
+ Conformer Generation Modes:
33
+ - 'rapid': RDKit/MMFF, 300 conformers, 0.10 Å RMSD cutoff (recommended for most work)
34
+ - 'careful': CREST/GFN-FF quick mode, 150 conformers max
35
+ - 'meticulous': CREST/GFN2-xTB normal mode, 500 conformers max
36
+
37
+ Returns:
38
+ Workflow object representing the submitted workflow
39
+
40
+ Examples:
41
+ # Simple diethyl ether conformer search
42
+ result = submit_conformer_search_workflow(
43
+ initial_molecule="CCOCC"
44
+ )
45
+
46
+ # Basic butane conformer search with rapid mode
47
+ result = submit_conformer_search_workflow(
48
+ initial_molecule="CCCC",
49
+ conf_gen_mode="rapid"
50
+ )
51
+
52
+ # Careful search with solvent
53
+ result = submit_conformer_search_workflow(
54
+ initial_molecule="CC(C)CC(=O)O",
55
+ conf_gen_mode="careful",
56
+ solvent="water",
57
+ final_method="r2scan_3c"
58
+ )
59
+ """
60
+
61
+ try:
62
+ # Convert initial_molecule to appropriate format
63
+ if isinstance(initial_molecule, str):
64
+ # Try to parse as SMILES
65
+ try:
66
+ mol = stjames.Molecule.from_smiles(initial_molecule)
67
+ initial_molecule = mol.model_dump()
68
+ except Exception:
69
+ # If fails, pass as dict with smiles
70
+ initial_molecule = {"smiles": initial_molecule}
71
+ elif isinstance(initial_molecule, dict):
72
+ # Already in dict format
73
+ pass
74
+ elif hasattr(initial_molecule, 'model_dump'):
75
+ # StJamesMolecule
76
+ initial_molecule = initial_molecule.model_dump()
77
+ else:
78
+ # Assume RdkitMol or similar
79
+ try:
80
+ from rdkit import Chem
81
+ mol = stjames.Molecule.from_rdkit(initial_molecule, cid=0)
82
+ initial_molecule = mol.model_dump()
83
+ except:
84
+ pass
85
+
86
+ # Convert final_method to Method if it's a string
87
+ if isinstance(final_method, str):
88
+ final_method = stjames.Method(final_method)
89
+
90
+ # Determine solvent model based on method
91
+ solvent_model = None
92
+ if solvent:
93
+ solvent_model = "alpb" if final_method in stjames.XTB_METHODS else "cpcm"
94
+
95
+ # Create optimization settings following official API
96
+ opt_settings = stjames.Settings(
97
+ method=final_method,
98
+ tasks=["optimize"],
99
+ mode=stjames.Mode.AUTO,
100
+ solvent_settings={"solvent": solvent, "model": solvent_model} if solvent else None,
101
+ opt_settings={"transition_state": transition_state, "constraints": []},
102
+ )
103
+
104
+ # Create MultiStageOptSettings
105
+ msos = stjames.MultiStageOptSettings(
106
+ mode=stjames.Mode.MANUAL,
107
+ xtb_preopt=True,
108
+ optimization_settings=[opt_settings],
109
+ )
110
+
111
+ # Convert to dict and fix the soscf field
112
+ msos_dict = msos.model_dump()
113
+
114
+ # Fix soscf: convert boolean to string enum
115
+ if 'optimization_settings' in msos_dict:
116
+ for opt_setting in msos_dict['optimization_settings']:
117
+ if 'scf_settings' in opt_setting and 'soscf' in opt_setting['scf_settings']:
118
+ soscf_val = opt_setting['scf_settings']['soscf']
119
+ if isinstance(soscf_val, bool):
120
+ if soscf_val is False:
121
+ opt_setting['scf_settings']['soscf'] = 'never'
122
+ elif soscf_val is True:
123
+ opt_setting['scf_settings']['soscf'] = 'always'
124
+ elif soscf_val is None:
125
+ # Default to smart behavior
126
+ opt_setting['scf_settings']['soscf'] = 'upon_failure'
127
+
128
+ # Build workflow_data
129
+ workflow_data = {
130
+ "multistage_opt_settings": msos_dict,
131
+ "conf_gen_mode": conf_gen_mode,
132
+ "mso_mode": "manual",
133
+ "solvent": solvent if solvent else None,
134
+ "transition_state": transition_state,
135
+ }
136
+
137
+ # Build the API request
138
+ data = {
139
+ "name": name,
140
+ "folder_uuid": folder_uuid if folder_uuid else None,
141
+ "workflow_type": "conformer_search",
142
+ "workflow_data": workflow_data,
143
+ "initial_molecule": initial_molecule,
144
+ "max_credits": max_credits if max_credits > 0 else None,
145
+ }
146
+
147
+ # Submit to API
148
+ from rowan.utils import api_client
149
+ from rowan import Workflow
150
+
151
+ with api_client() as client:
152
+ response = client.post("/workflow", json=data)
153
+ response.raise_for_status()
154
+ return Workflow(**response.json())
155
+
156
+ except Exception as e:
157
+ # Re-raise the exception so MCP can handle it
158
+ raise e
@@ -0,0 +1,52 @@
1
+ """
2
+ Rowan v2 API: Descriptors Workflow
3
+ Calculate molecular descriptors for QSAR and molecular analysis.
4
+ """
5
+
6
+ from typing import Annotated
7
+ import rowan
8
+ import stjames
9
+
10
+
11
+ def submit_descriptors_workflow(
12
+ initial_molecule: Annotated[str, "SMILES string of the molecule to calculate descriptors for"],
13
+ name: Annotated[str, "Workflow name for identification and tracking"] = "Descriptors Workflow",
14
+ folder_uuid: Annotated[str, "UUID of folder to organize this workflow. Empty string uses default folder"] = "",
15
+ max_credits: Annotated[int, "Maximum credits to spend on this calculation. 0 for no limit"] = 0
16
+ ):
17
+ """Submit a molecular descriptors calculation workflow using Rowan v2 API.
18
+
19
+ Args:
20
+ initial_molecule: SMILES string or molecule object for descriptor calculation
21
+ name: Workflow name for identification and tracking
22
+ folder_uuid: UUID of folder to organize this workflow. Empty string uses default folder.
23
+ max_credits: Maximum credits to spend on this calculation. 0 for no limit.
24
+
25
+ Calculates a comprehensive set of molecular descriptors including:
26
+ - Physical properties (MW, logP, TPSA, etc.)
27
+ - Electronic properties (HOMO/LUMO, dipole moment, etc.)
28
+ - Structural features (rotatable bonds, H-bond donors/acceptors, etc.)
29
+ - Topological indices
30
+
31
+ Returns:
32
+ Workflow object representing the submitted workflow
33
+
34
+ Example:
35
+ # Basic descriptor calculation
36
+ result = submit_descriptors_workflow(
37
+ initial_molecule="CC(=O)Nc1ccc(O)cc1"
38
+ )
39
+
40
+ # For complex molecule
41
+ result = submit_descriptors_workflow(
42
+ initial_molecule="CN1C=NC2=C1C(=O)N(C(=O)N2C)C",
43
+ name="Caffeine Descriptors"
44
+ )
45
+ """
46
+
47
+ return rowan.submit_descriptors_workflow(
48
+ initial_molecule=stjames.Molecule.from_smiles(initial_molecule),
49
+ name=name,
50
+ folder_uuid=folder_uuid if folder_uuid else None,
51
+ max_credits=max_credits if max_credits > 0 else None
52
+ )
@@ -0,0 +1,244 @@
1
+ """
2
+ Rowan v2 API: Docking Workflow
3
+ Perform molecular docking simulations for drug discovery.
4
+ """
5
+
6
+ from typing import Dict, Any, Union, List, Annotated
7
+ import rowan
8
+ import stjames
9
+ import json
10
+ from stjames.pdb import PDB, read_pdb
11
+
12
+ def submit_docking_workflow(
13
+ protein: Annotated[str, "Protein UUID or PDB content/path for docking target"],
14
+ pocket: Annotated[str, "JSON string defining binding pocket coordinates or 'auto' for automatic detection"],
15
+ initial_molecule: Annotated[str, "SMILES string of the ligand molecule to dock"],
16
+ do_csearch: Annotated[bool, "Whether to perform conformer search before docking"] = True,
17
+ do_optimization: Annotated[bool, "Whether to optimize docked poses"] = True,
18
+ name: Annotated[str, "Workflow name for identification and tracking"] = "Docking Workflow",
19
+ folder_uuid: Annotated[str, "UUID of folder to organize this workflow. Empty string uses default folder"] = "",
20
+ max_credits: Annotated[int, "Maximum credits to spend on this calculation. 0 for no limit"] = 0,
21
+ blocking: Annotated[bool, "Whether to wait for workflow completion before returning"] = False
22
+ ):
23
+ """Submits a Docking workflow to the API.
24
+
25
+ Args:
26
+ protein: Protein for docking. Can be: 1) PDB ID string (e.g., '1HCK'), 2) Protein UUID string, 3) JSON string dict with 'pdb_id' and optional 'name'
27
+ pocket: Binding pocket as JSON string "[[x1,y1,z1], [x2,y2,z2]]" defining box corners for docking site
28
+ initial_molecule: SMILES string representing the ligand
29
+ do_csearch: Whether to perform conformational search on the ligand before docking
30
+ do_optimization: Whether to optimize the ligand geometry before docking
31
+ name: Workflow name for identification and tracking
32
+ folder_uuid: UUID of folder to organize this workflow. Empty string uses default folder.
33
+ max_credits: Maximum credits to spend on this calculation. 0 for no limit.
34
+ blocking: Whether to wait for workflow completion before returning
35
+
36
+ Automatically handles protein creation from PDB ID and sanitization if needed.
37
+
38
+ Returns:
39
+ Workflow object representing the submitted workflow
40
+
41
+ Examples:
42
+ # Example 1: Using PDB ID directly
43
+ result = submit_docking_workflow(
44
+ protein="1HCK", # PDB ID
45
+ pocket="[[103.55, 100.59, 82.99], [27.76, 32.67, 48.79]]",
46
+ initial_molecule="CCC(C)(C)NC1=NCC2(CCC(=O)C2C)N1",
47
+ name="CDK2 Docking"
48
+ )
49
+
50
+ # Example 2: Using dict with PDB ID and custom name
51
+ result = submit_docking_workflow(
52
+ protein='{"pdb_id": "1HCK", "name": "My CDK2 Protein"}',
53
+ pocket="[[103.55, 100.59, 82.99], [27.76, 32.67, 48.79]]",
54
+ initial_molecule="CCC(C)(C)NC1=NCC2(CCC(=O)C2C)N1"
55
+ )
56
+
57
+ # Example 3: Using existing protein UUID
58
+ result = submit_docking_workflow(
59
+ protein="abc123-def456-...", # Protein UUID
60
+ pocket="[[103.55, 100.59, 82.99], [27.76, 32.67, 48.79]]",
61
+ initial_molecule="CCC(C)(C)NC1=NCC2(CCC(=O)C2C)N1"
62
+ )
63
+ """
64
+ import logging
65
+ logger = logging.getLogger(__name__)
66
+
67
+ # Handle protein parameter
68
+ protein_obj = None
69
+
70
+ # Try to parse protein as JSON first
71
+ try:
72
+ protein_dict = json.loads(protein)
73
+ if isinstance(protein_dict, dict):
74
+ protein = protein_dict
75
+ except (json.JSONDecodeError, ValueError):
76
+ # Not JSON, keep as string
77
+ pass
78
+
79
+ # Check if protein is a PDB ID or dict with PDB ID
80
+ if isinstance(protein, str):
81
+ # Check if it's a UUID (36 chars with dashes) or PDB ID (4 chars)
82
+ if len(protein) == 36 and '-' in protein:
83
+ # It's a UUID, retrieve the protein
84
+ logger.info(f"Using existing protein UUID: {protein}")
85
+ protein_obj = rowan.retrieve_protein(protein)
86
+ elif len(protein) <= 6: # PDB IDs are typically 4 characters
87
+ # It's a PDB ID, create protein from it
88
+ logger.info(f"Creating protein from PDB ID: {protein}")
89
+
90
+ # Get or create a project (REQUIRED for v2.1.1)
91
+ project_uuid = None
92
+ try:
93
+ # Try to get default project
94
+ project = rowan.default_project()
95
+ project_uuid = project.uuid
96
+ logger.info(f"Using default project: {project_uuid}")
97
+ except Exception as e:
98
+ logger.info(f"Could not get default project: {e}")
99
+ try:
100
+ # List existing projects and use the first one
101
+ projects = rowan.list_projects(size=1)
102
+ if projects:
103
+ project_uuid = projects[0].uuid
104
+ logger.info(f"Using existing project: {project_uuid}")
105
+ else:
106
+ # Create a new project if none exist
107
+ new_project = rowan.create_project(name="Docking Project")
108
+ project_uuid = new_project.uuid
109
+ logger.info(f"Created new project: {project_uuid}")
110
+ except Exception as e2:
111
+ logger.error(f"Failed to get/create project: {e2}")
112
+ raise ValueError(f"Cannot create protein without a valid project. Error: {e2}")
113
+
114
+ # Create protein with REQUIRED project_uuid
115
+ protein_obj = rowan.create_protein_from_pdb_id(
116
+ name=f"Protein from {protein}",
117
+ code=protein,
118
+ project_uuid=project_uuid
119
+ )
120
+ logger.info(f"Created protein with UUID: {protein_obj.uuid}")
121
+
122
+ # Sanitize the protein for docking
123
+ logger.info("Sanitizing protein for docking...")
124
+ try:
125
+ protein_obj.sanitize()
126
+
127
+ # Wait for sanitization to complete
128
+ import time
129
+ max_wait = 30
130
+ start_time = time.time()
131
+ while time.time() - start_time < max_wait:
132
+ time.sleep(2)
133
+ protein_obj.refresh()
134
+ if protein_obj.sanitized and protein_obj.sanitized != 0:
135
+ logger.info(f"Protein sanitized successfully (sanitized={protein_obj.sanitized})")
136
+ break
137
+ else:
138
+ logger.warning(f"Sanitization may not be complete after {max_wait} seconds")
139
+ except Exception as e:
140
+ logger.warning(f"Sanitization failed: {e}")
141
+ logger.warning("Proceeding without sanitization - docking may fail if protein needs sanitization")
142
+ else:
143
+ raise ValueError(f"Invalid protein parameter: {protein}. Expected PDB ID (4 chars) or UUID (36 chars)")
144
+
145
+ elif isinstance(protein, dict):
146
+ # Dict with PDB ID and optional name
147
+ pdb_id = protein.get('pdb_id')
148
+ protein_name = protein.get('name', f"Protein from {pdb_id}")
149
+
150
+ if not pdb_id:
151
+ raise ValueError("Dict protein parameter must include 'pdb_id' key")
152
+
153
+ logger.info(f"Creating protein '{protein_name}' from PDB ID: {pdb_id}")
154
+
155
+ # Get or create a project (REQUIRED for v2.1.1)
156
+ project_uuid = None
157
+ try:
158
+ # Try to get default project
159
+ project = rowan.default_project()
160
+ project_uuid = project.uuid
161
+ logger.info(f"Using default project: {project_uuid}")
162
+ except Exception as e:
163
+ logger.info(f"Could not get default project: {e}")
164
+ try:
165
+ # List existing projects and use the first one
166
+ projects = rowan.list_projects(size=1)
167
+ if projects:
168
+ project_uuid = projects[0].uuid
169
+ logger.info(f"Using existing project: {project_uuid}")
170
+ else:
171
+ # Create a new project if none exist
172
+ new_project = rowan.create_project(name="Docking Project")
173
+ project_uuid = new_project.uuid
174
+ logger.info(f"Created new project: {project_uuid}")
175
+ except Exception as e2:
176
+ logger.error(f"Failed to get/create project: {e2}")
177
+ raise ValueError(f"Cannot create protein without a valid project. Error: {e2}")
178
+
179
+ # Create protein with REQUIRED project_uuid
180
+ protein_obj = rowan.create_protein_from_pdb_id(
181
+ name=protein_name,
182
+ code=pdb_id,
183
+ project_uuid=project_uuid
184
+ )
185
+ logger.info(f"Created protein with UUID: {protein_obj.uuid}")
186
+
187
+ # Sanitize the protein
188
+ logger.info("Sanitizing protein for docking...")
189
+ try:
190
+ protein_obj.sanitize()
191
+
192
+ # Wait for sanitization
193
+ import time
194
+ max_wait = 30
195
+ start_time = time.time()
196
+ while time.time() - start_time < max_wait:
197
+ time.sleep(2)
198
+ protein_obj.refresh()
199
+ if protein_obj.sanitized and protein_obj.sanitized != 0:
200
+ logger.info(f"Protein sanitized successfully (sanitized={protein_obj.sanitized})")
201
+ break
202
+ else:
203
+ logger.warning(f"Sanitization may not be complete after {max_wait} seconds")
204
+ except Exception as e:
205
+ logger.warning(f"Sanitization failed: {e}")
206
+ logger.warning("Proceeding without sanitization - docking may fail if protein needs sanitization")
207
+
208
+ else:
209
+ # Assume it's already a protein object
210
+ protein_obj = protein
211
+
212
+ # Parse pocket parameter (always a string in simplified version)
213
+ try:
214
+ pocket = json.loads(pocket)
215
+ except (json.JSONDecodeError, ValueError) as e:
216
+ raise ValueError(f"Invalid pocket format: {pocket}. Expected JSON string like \"[[x1,y1,z1], [x2,y2,z2]]\"")
217
+
218
+ # Ensure pocket is a list of lists
219
+ if not isinstance(pocket, list) or len(pocket) != 2:
220
+ raise ValueError(f"Pocket must be a list with exactly 2 coordinate lists")
221
+
222
+ # Ensure each element is a list of floats
223
+ pocket = [list(coord) for coord in pocket]
224
+
225
+ # Submit the workflow
226
+ logger.info(f"Submitting docking workflow: {name}")
227
+ workflow = rowan.submit_docking_workflow(
228
+ protein=protein_obj,
229
+ pocket=pocket,
230
+ initial_molecule=stjames.Molecule.from_smiles(initial_molecule),
231
+ do_csearch=do_csearch,
232
+ do_optimization=do_optimization,
233
+ name=name,
234
+ folder_uuid=folder_uuid if folder_uuid else None,
235
+ max_credits=max_credits if max_credits > 0 else None
236
+ )
237
+
238
+ logger.info(f"Docking workflow submitted with UUID: {workflow.uuid}")
239
+
240
+ # If blocking, wait for completion
241
+ if blocking:
242
+ workflow.wait_for_result()
243
+
244
+ return workflow
@@ -0,0 +1,114 @@
1
+ """
2
+ Rowan v2 API: Fukui Workflow
3
+ Calculate Fukui indices for reactivity analysis.
4
+ """
5
+
6
+ from typing import Dict, Any, Annotated
7
+ import rowan
8
+ import stjames
9
+ import json
10
+
11
+ def submit_fukui_workflow(
12
+ initial_molecule: Annotated[str, "SMILES string of the molecule to calculate Fukui indices for"],
13
+ optimization_method: Annotated[str, "Method for geometry optimization (e.g., 'gfn2_xtb', 'uma_m_omol')"] = "gfn2_xtb",
14
+ fukui_method: Annotated[str, "Method for Fukui indices calculation (e.g., 'gfn1_xtb', 'gfn2_xtb')"] = "gfn1_xtb",
15
+ solvent_settings: Annotated[str, "JSON string for solvent settings. Empty string for vacuum"] = "",
16
+ name: Annotated[str, "Workflow name for identification and tracking"] = "Fukui Workflow",
17
+ folder_uuid: Annotated[str, "UUID of folder to organize this workflow. Empty string uses default folder"] = "",
18
+ max_credits: Annotated[int, "Maximum credits to spend on this calculation. 0 for no limit"] = 0
19
+ ):
20
+ """Submit a Fukui indices calculation workflow using Rowan v2 API.
21
+
22
+ Args:
23
+ initial_molecule: SMILES string for Fukui analysis
24
+ optimization_method: Method for geometry optimization. Options: 'gfn2_xtb', 'r2scan_3c', 'aimnet2_wb97md3'
25
+ fukui_method: Method for Fukui calculation. Options: 'gfn1_xtb', 'gfn2_xtb'
26
+ solvent_settings: Solvent configuration JSON string, e.g., '{"solvent": "water", "model": "alpb"}'. Empty for gas phase.
27
+ name: Workflow name for identification and tracking
28
+ folder_uuid: UUID of folder to organize this workflow. Empty string uses default folder.
29
+ max_credits: Maximum credits to spend on this calculation. 0 for no limit.
30
+
31
+ Calculates Fukui indices to predict molecular reactivity at different sites.
32
+ Fukui indices indicate susceptibility to nucleophilic/electrophilic attack.
33
+
34
+ Returns:
35
+ Workflow object representing the submitted workflow
36
+
37
+ Example:
38
+ # Basic Fukui indices
39
+ result = submit_fukui_workflow(
40
+ initial_molecule="CC(=O)O"
41
+ )
42
+
43
+ # With solvent and advanced methods
44
+ result = submit_fukui_workflow(
45
+ initial_molecule="c1ccccc1N",
46
+ optimization_method="r2scan_3c",
47
+ fukui_method="gfn2_xtb",
48
+ solvent_settings='{"solvent": "water", "model": "alpb"}'
49
+ )
50
+ """
51
+ # Parse solvent_settings if provided
52
+ parsed_solvent_settings = None
53
+ if solvent_settings:
54
+ try:
55
+ parsed_solvent_settings = json.loads(solvent_settings)
56
+ except (json.JSONDecodeError, ValueError):
57
+ pass
58
+
59
+ try:
60
+ # Convert initial_molecule to StJamesMolecule
61
+ molecule = stjames.Molecule.from_smiles(initial_molecule)
62
+ initial_molecule_dict = molecule.model_dump()
63
+
64
+ # Create Settings objects
65
+ optimization_settings = stjames.Settings(method=optimization_method)
66
+ fukui_settings = stjames.Settings(method=fukui_method, solvent_settings=parsed_solvent_settings)
67
+
68
+ # Serialize to dicts
69
+ opt_settings_dict = optimization_settings.model_dump(mode="json")
70
+ fukui_settings_dict = fukui_settings.model_dump(mode="json")
71
+
72
+ # Fix soscf boolean to string enum conversion for optimization settings
73
+ if 'scf_settings' in opt_settings_dict and 'soscf' in opt_settings_dict['scf_settings']:
74
+ soscf_val = opt_settings_dict['scf_settings']['soscf']
75
+ if isinstance(soscf_val, bool):
76
+ if soscf_val is False:
77
+ opt_settings_dict['scf_settings']['soscf'] = 'never'
78
+ elif soscf_val is True:
79
+ opt_settings_dict['scf_settings']['soscf'] = 'always'
80
+
81
+ # Fix soscf boolean to string enum conversion for fukui settings
82
+ if 'scf_settings' in fukui_settings_dict and 'soscf' in fukui_settings_dict['scf_settings']:
83
+ soscf_val = fukui_settings_dict['scf_settings']['soscf']
84
+ if isinstance(soscf_val, bool):
85
+ if soscf_val is False:
86
+ fukui_settings_dict['scf_settings']['soscf'] = 'never'
87
+ elif soscf_val is True:
88
+ fukui_settings_dict['scf_settings']['soscf'] = 'always'
89
+
90
+ workflow_data = {
91
+ "opt_settings": opt_settings_dict,
92
+ "opt_engine": stjames.Method(optimization_method).default_engine(),
93
+ "fukui_settings": fukui_settings_dict,
94
+ "fukui_engine": stjames.Method(fukui_method).default_engine(),
95
+ }
96
+
97
+ # Build the API request payload
98
+ data = {
99
+ "name": name,
100
+ "folder_uuid": folder_uuid if folder_uuid else None,
101
+ "workflow_type": "fukui",
102
+ "workflow_data": workflow_data,
103
+ "initial_molecule": initial_molecule_dict,
104
+ "max_credits": max_credits if max_credits > 0 else None,
105
+ }
106
+
107
+ # Submit to API
108
+ with rowan.api_client() as client:
109
+ response = client.post("/workflow", json=data)
110
+ response.raise_for_status()
111
+ return rowan.Workflow(**response.json())
112
+
113
+ except Exception as e:
114
+ raise e
@@ -0,0 +1,58 @@
1
+ """
2
+ Rowan v2 API: IRC Workflow
3
+ Perform Intrinsic Reaction Coordinate calculations to trace reaction paths.
4
+ """
5
+
6
+ from typing import Annotated
7
+ import rowan
8
+ import stjames
9
+
10
+ def submit_irc_workflow(
11
+ initial_molecule: Annotated[str, "SMILES string for IRC calculation"],
12
+ method: Annotated[str, "Computational method for IRC (e.g., 'uma_m_omol', 'gfn2_xtb', 'r2scan_3c')"] = "uma_m_omol",
13
+ engine: Annotated[str, "Computational engine: 'omol25', 'xtb', 'psi4'"] = "omol25",
14
+ preopt: Annotated[bool, "Whether to pre-optimize the transition state before IRC step"] = True,
15
+ step_size: Annotated[float, "Step size for IRC path tracing in Bohr (typically 0.03-0.1)"] = 0.05,
16
+ max_irc_steps: Annotated[int, "Maximum number of IRC steps in each direction from TS"] = 30,
17
+ name: Annotated[str, "Workflow name for identification and tracking"] = "IRC Workflow",
18
+ folder_uuid: Annotated[str, "UUID of folder to organize this workflow. Empty string uses default folder"] = "",
19
+ max_credits: Annotated[int, "Maximum credits to spend on this calculation. 0 for no limit"] = 0
20
+ ):
21
+ """Submits an Intrinsic Reaction Coordinate (IRC) workflow to the API.
22
+
23
+ Args:
24
+ initial_molecule: SMILES string for IRC calculation
25
+ method: Computational method for IRC. Options: 'uma_m_omol', 'gfn2_xtb', 'r2scan_3c'
26
+ engine: Computational engine. Options: 'omol25', 'xtb', 'psi4'
27
+ preopt: Whether to pre-optimize the transition state before IRC
28
+ step_size: Step size for IRC path tracing in Bohr (typically 0.03-0.1)
29
+ max_irc_steps: Maximum number of IRC steps in each direction from TS
30
+ name: Workflow name for identification and tracking
31
+ folder_uuid: UUID of folder to organize this workflow. Empty string uses default folder.
32
+ max_credits: Maximum credits to spend on this calculation. 0 for no limit.
33
+
34
+ Returns:
35
+ Workflow object representing the submitted IRC workflow
36
+
37
+ Example:
38
+ # IRC from SMILES
39
+ result = submit_irc_workflow(
40
+ initial_molecule="N=C([O-])[OH2+]", # Transition state SMILES
41
+ name="HNCO + H₂O - IRC",
42
+ preopt=True, # Pre-optimize TS
43
+ method="gfn2_xtb",
44
+ engine="xtb"
45
+ )
46
+ """
47
+
48
+ return rowan.submit_irc_workflow(
49
+ initial_molecule=stjames.Molecule.from_smiles(initial_molecule),
50
+ method=method,
51
+ engine=engine,
52
+ preopt=preopt,
53
+ step_size=step_size,
54
+ max_irc_steps=max_irc_steps,
55
+ name=name,
56
+ folder_uuid=folder_uuid if folder_uuid else None,
57
+ max_credits=max_credits if max_credits > 0 else None
58
+ )