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.
- rowan_mcp/functions/admet.py +89 -0
- rowan_mcp/functions/bde.py +106 -0
- rowan_mcp/functions/calculation_retrieve.py +89 -0
- rowan_mcp/functions/conformers.py +77 -0
- rowan_mcp/functions/descriptors.py +89 -0
- rowan_mcp/functions/docking.py +290 -0
- rowan_mcp/functions/docking_enhanced.py +174 -0
- rowan_mcp/functions/electronic_properties.py +202 -0
- rowan_mcp/functions/folder_management.py +130 -0
- rowan_mcp/functions/fukui.py +216 -0
- rowan_mcp/functions/hydrogen_bond_basicity.py +87 -0
- rowan_mcp/functions/irc.py +125 -0
- rowan_mcp/functions/macropka.py +120 -0
- rowan_mcp/functions/molecular_converter.py +423 -0
- rowan_mcp/functions/molecular_dynamics.py +191 -0
- rowan_mcp/functions/molecule_lookup.py +57 -0
- rowan_mcp/functions/multistage_opt.py +168 -0
- rowan_mcp/functions/pdb_handler.py +200 -0
- rowan_mcp/functions/pka.py +81 -0
- rowan_mcp/functions/redox_potential.py +349 -0
- rowan_mcp/functions/scan.py +536 -0
- rowan_mcp/functions/scan_analyzer.py +347 -0
- rowan_mcp/functions/solubility.py +277 -0
- rowan_mcp/functions/spin_states.py +747 -0
- rowan_mcp/functions/system_management.py +361 -0
- rowan_mcp/functions/tautomers.py +88 -0
- rowan_mcp/functions/workflow_management.py +422 -0
- {rowan_mcp-2.0.0.dist-info → rowan_mcp-2.0.1.dist-info}/METADATA +3 -18
- {rowan_mcp-2.0.0.dist-info → rowan_mcp-2.0.1.dist-info}/RECORD +31 -4
- {rowan_mcp-2.0.0.dist-info → rowan_mcp-2.0.1.dist-info}/WHEEL +0 -0
- {rowan_mcp-2.0.0.dist-info → rowan_mcp-2.0.1.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Rowan scan function for potential energy surface scans and IRC workflows.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Optional, Union, List, Dict, Any
|
|
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
|
+
# QC Constants needed for validation
|
|
21
|
+
QC_ENGINES = {
|
|
22
|
+
"psi4": "Hartree–Fock and density-functional theory",
|
|
23
|
+
"terachem": "Hartree–Fock and density-functional theory",
|
|
24
|
+
"pyscf": "Hartree–Fock and density-functional theory",
|
|
25
|
+
"xtb": "Semiempirical calculations",
|
|
26
|
+
"aimnet2": "Machine-learned interatomic potential calculations"
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
QC_METHODS = {
|
|
30
|
+
# Hartree-Fock
|
|
31
|
+
"hf": "Hartree-Fock (unrestricted for open-shell systems)",
|
|
32
|
+
|
|
33
|
+
# Pure Functionals - LDA
|
|
34
|
+
"lsda": "Local Spin Density Approximation (Slater exchange + VWN correlation)",
|
|
35
|
+
|
|
36
|
+
# Pure Functionals - GGA
|
|
37
|
+
"pbe": "Perdew-Burke-Ernzerhof (1996) GGA functional",
|
|
38
|
+
"blyp": "Becke 1988 exchange + Lee-Yang-Parr correlation",
|
|
39
|
+
"bp86": "Becke 1988 exchange + Perdew 1988 correlation",
|
|
40
|
+
"b97-d3": "Grimme's 2006 B97 reparameterization with D3 dispersion",
|
|
41
|
+
|
|
42
|
+
# Pure Functionals - Meta-GGA
|
|
43
|
+
"r2scan": "Furness and Sun's 2020 r2SCAN meta-GGA functional",
|
|
44
|
+
"tpss": "Tao-Perdew-Staroverov-Scuseria meta-GGA (2003)",
|
|
45
|
+
"m06l": "Zhao and Truhlar's 2006 local meta-GGA functional",
|
|
46
|
+
|
|
47
|
+
# Hybrid Functionals - Global
|
|
48
|
+
"pbe0": "PBE0 hybrid functional (25% HF exchange)",
|
|
49
|
+
"b3lyp": "B3LYP hybrid functional (20% HF exchange)",
|
|
50
|
+
"b3pw91": "B3PW91 hybrid functional (20% HF exchange)",
|
|
51
|
+
|
|
52
|
+
# Hybrid Functionals - Range-Separated
|
|
53
|
+
"camb3lyp": "CAM-B3LYP range-separated hybrid (19-65% HF exchange)",
|
|
54
|
+
"wb97x_d3": "ωB97X-D3 range-separated hybrid with D3 dispersion (20-100% HF exchange)",
|
|
55
|
+
"wb97x_v": "ωB97X-V with VV10 nonlocal dispersion (17-100% HF exchange)",
|
|
56
|
+
"wb97m_v": "ωB97M-V meta-GGA with VV10 dispersion (15-100% HF exchange)"
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
QC_BASIS_SETS = {
|
|
60
|
+
# Pople's STO-nG minimal basis sets
|
|
61
|
+
"sto-2g": "STO-2G minimal basis set",
|
|
62
|
+
"sto-3g": "STO-3G minimal basis set (default if none specified)",
|
|
63
|
+
"sto-4g": "STO-4G minimal basis set",
|
|
64
|
+
"sto-5g": "STO-5G minimal basis set",
|
|
65
|
+
"sto-6g": "STO-6G minimal basis set",
|
|
66
|
+
|
|
67
|
+
# Pople's 6-31 basis sets (double-zeta)
|
|
68
|
+
"6-31g": "6-31G split-valence double-zeta basis set",
|
|
69
|
+
"6-31g*": "6-31G(d) - 6-31G with polarization on heavy atoms",
|
|
70
|
+
"6-31g(d)": "6-31G with d polarization on heavy atoms",
|
|
71
|
+
"6-31g(d,p)": "6-31G with polarization on all atoms",
|
|
72
|
+
"6-31+g(d,p)": "6-31G with diffuse and polarization functions",
|
|
73
|
+
"6-311+g(2d,p)": "6-311+G(2d,p) - larger basis for single-point calculations",
|
|
74
|
+
|
|
75
|
+
# Jensen's pcseg-n basis sets (recommended for DFT)
|
|
76
|
+
"pcseg-0": "Jensen pcseg-0 minimal basis set",
|
|
77
|
+
"pcseg-1": "Jensen pcseg-1 double-zeta (better than 6-31G(d))",
|
|
78
|
+
"pcseg-2": "Jensen pcseg-2 triple-zeta basis set",
|
|
79
|
+
"pcseg-3": "Jensen pcseg-3 quadruple-zeta basis set",
|
|
80
|
+
"pcseg-4": "Jensen pcseg-4 quintuple-zeta basis set",
|
|
81
|
+
"aug-pcseg-1": "Augmented Jensen pcseg-1 double-zeta",
|
|
82
|
+
"aug-pcseg-2": "Augmented Jensen pcseg-2 triple-zeta",
|
|
83
|
+
|
|
84
|
+
# Dunning's cc-PVNZ basis sets (use seg-opt variants for speed)
|
|
85
|
+
"cc-pvdz": "Correlation-consistent double-zeta (generally contracted - slow)",
|
|
86
|
+
"cc-pvtz": "Correlation-consistent triple-zeta (generally contracted - slow)",
|
|
87
|
+
"cc-pvqz": "Correlation-consistent quadruple-zeta (generally contracted - slow)",
|
|
88
|
+
"cc-pvdz(seg-opt)": "cc-pVDZ segmented-optimized (preferred over cc-pVDZ)",
|
|
89
|
+
"cc-pvtz(seg-opt)": "cc-pVTZ segmented-optimized (preferred over cc-pVTZ)",
|
|
90
|
+
"cc-pvqz(seg-opt)": "cc-pVQZ segmented-optimized (preferred over cc-pVQZ)",
|
|
91
|
+
|
|
92
|
+
# Ahlrichs's def2 basis sets
|
|
93
|
+
"def2-sv(p)": "Ahlrichs def2-SV(P) split-valence polarized",
|
|
94
|
+
"def2-svp": "Ahlrichs def2-SVP split-valence polarized",
|
|
95
|
+
"def2-tzvp": "Ahlrichs def2-TZVP triple-zeta valence polarized",
|
|
96
|
+
|
|
97
|
+
# Truhlar's efficient basis sets
|
|
98
|
+
"midi!": "MIDI!/MIDIX polarized minimal basis set (very efficient)",
|
|
99
|
+
"midix": "MIDI!/MIDIX polarized minimal basis set (very efficient)"
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
QC_CORRECTIONS = {
|
|
103
|
+
"d3bj": "Grimme's D3 dispersion correction with Becke-Johnson damping",
|
|
104
|
+
"d3": "Grimme's D3 dispersion correction (automatically applied for B97-D3, ωB97X-D3)"
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
def lookup_molecule_smiles(molecule_name: str) -> str:
|
|
108
|
+
"""Simple molecule name to SMILES lookup for common molecules."""
|
|
109
|
+
common_molecules = {
|
|
110
|
+
# Simple molecules
|
|
111
|
+
"water": "O",
|
|
112
|
+
"methane": "C",
|
|
113
|
+
"ethane": "CC",
|
|
114
|
+
"propane": "CCC",
|
|
115
|
+
"butane": "CCCC",
|
|
116
|
+
"ethylene": "C=C",
|
|
117
|
+
"acetylene": "C#C",
|
|
118
|
+
"benzene": "c1ccccc1",
|
|
119
|
+
"toluene": "Cc1ccccc1",
|
|
120
|
+
"phenol": "Oc1ccccc1",
|
|
121
|
+
|
|
122
|
+
# Peroxides and simple radicals
|
|
123
|
+
"hydrogen peroxide": "OO",
|
|
124
|
+
"h2o2": "OO",
|
|
125
|
+
"peroxide": "OO",
|
|
126
|
+
|
|
127
|
+
# Common solvents
|
|
128
|
+
"methanol": "CO",
|
|
129
|
+
"ethanol": "CCO",
|
|
130
|
+
"acetone": "CC(=O)C",
|
|
131
|
+
"dmso": "CS(=O)C",
|
|
132
|
+
"thf": "C1CCOC1",
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
# Check if it's already a SMILES string (contains specific characters)
|
|
136
|
+
if any(char in molecule_name.lower() for char in ['=', '#', '[', '(', ')', '@']):
|
|
137
|
+
return molecule_name
|
|
138
|
+
|
|
139
|
+
# Look up common name
|
|
140
|
+
lookup_name = molecule_name.lower().strip()
|
|
141
|
+
smiles = common_molecules.get(lookup_name)
|
|
142
|
+
|
|
143
|
+
if smiles:
|
|
144
|
+
logger.info(f"Molecule lookup: '{molecule_name}' → '{smiles}'")
|
|
145
|
+
return smiles
|
|
146
|
+
else:
|
|
147
|
+
return molecule_name
|
|
148
|
+
|
|
149
|
+
def rowan_scan(
|
|
150
|
+
name: str,
|
|
151
|
+
molecule: str,
|
|
152
|
+
coordinate_type: str,
|
|
153
|
+
atoms: Union[List[int], str],
|
|
154
|
+
start: float,
|
|
155
|
+
stop: float,
|
|
156
|
+
num: int,
|
|
157
|
+
method: Optional[str] = None,
|
|
158
|
+
basis_set: Optional[str] = None,
|
|
159
|
+
engine: Optional[str] = None,
|
|
160
|
+
corrections: Optional[List[str]] = None,
|
|
161
|
+
charge: int = 0,
|
|
162
|
+
multiplicity: int = 1,
|
|
163
|
+
mode: Optional[str] = None,
|
|
164
|
+
constraints: Optional[List[Dict[str, Any]]] = None,
|
|
165
|
+
# New parameters from ScanWorkflow
|
|
166
|
+
coordinate_type_2d: Optional[str] = None,
|
|
167
|
+
atoms_2d: Optional[Union[List[int], str]] = None,
|
|
168
|
+
start_2d: Optional[float] = None,
|
|
169
|
+
stop_2d: Optional[float] = None,
|
|
170
|
+
num_2d: Optional[int] = None,
|
|
171
|
+
wavefront_propagation: bool = True,
|
|
172
|
+
concerted_coordinates: Optional[List[Dict[str, Any]]] = None,
|
|
173
|
+
folder_uuid: Optional[str] = None,
|
|
174
|
+
blocking: bool = True,
|
|
175
|
+
ping_interval: int = 10
|
|
176
|
+
) -> str:
|
|
177
|
+
"""Run potential energy surface scans with full parameter control including 2D and concerted scans.
|
|
178
|
+
|
|
179
|
+
Performs constrained optimizations along reaction coordinates using Rowan's
|
|
180
|
+
wavefront propagation method to avoid local minima. Essential for:
|
|
181
|
+
- Mapping reaction pathways and mechanisms
|
|
182
|
+
- Finding transition state approximations
|
|
183
|
+
- Studying conformational preferences and rotational barriers
|
|
184
|
+
- Analyzing atropisomerism and molecular flexibility
|
|
185
|
+
- 2D potential energy surfaces
|
|
186
|
+
- Concerted coordinate changes
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
name: Name for the scan calculation
|
|
190
|
+
molecule: Molecule SMILES string or common name (e.g., "butane", "phenol")
|
|
191
|
+
coordinate_type: Type of coordinate to scan - "bond", "angle", or "dihedral"
|
|
192
|
+
atoms: List of 1-indexed atom numbers or comma-separated string (e.g., [1,2,3,4] or "1,2,3,4")
|
|
193
|
+
start: Starting value of the coordinate (Å for bonds, degrees for angles/dihedrals)
|
|
194
|
+
stop: Ending value of the coordinate
|
|
195
|
+
num: Number of scan points to calculate
|
|
196
|
+
method: QC method (default: "hf-3c" for speed, use "b3lyp" for accuracy)
|
|
197
|
+
basis_set: Basis set (default: auto-selected based on method)
|
|
198
|
+
engine: Computational engine (default: "psi4")
|
|
199
|
+
corrections: List of corrections like ["d3bj"] for dispersion
|
|
200
|
+
charge: Molecular charge (default: 0)
|
|
201
|
+
multiplicity: Spin multiplicity (default: 1 for singlet)
|
|
202
|
+
mode: Calculation precision - "reckless", "rapid", "careful", "meticulous" (default: "rapid")
|
|
203
|
+
constraints: Additional coordinate constraints during optimization
|
|
204
|
+
coordinate_type_2d: Type of second coordinate for 2D scan (optional)
|
|
205
|
+
atoms_2d: Atoms for second coordinate in 2D scan (optional)
|
|
206
|
+
start_2d: Starting value of second coordinate (optional)
|
|
207
|
+
stop_2d: Ending value of second coordinate (optional)
|
|
208
|
+
num_2d: Number of points for second coordinate (must equal num for 2D grid, optional)
|
|
209
|
+
wavefront_propagation: Use wavefront propagation for smoother scans (default: True)
|
|
210
|
+
concerted_coordinates: List of coordinate dictionaries for concerted scans (optional)
|
|
211
|
+
folder_uuid: Optional folder UUID for organization
|
|
212
|
+
blocking: Whether to wait for completion (default: True)
|
|
213
|
+
ping_interval: Check status interval in seconds (default: 10, longer for scans)
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
Scan results with energy profile and structural data
|
|
217
|
+
"""
|
|
218
|
+
|
|
219
|
+
# Look up SMILES if a common name was provided
|
|
220
|
+
canonical_smiles = lookup_molecule_smiles(molecule)
|
|
221
|
+
|
|
222
|
+
# Validate coordinate type
|
|
223
|
+
valid_coord_types = ["bond", "angle", "dihedral"]
|
|
224
|
+
coord_type_lower = coordinate_type.lower()
|
|
225
|
+
if coord_type_lower not in valid_coord_types:
|
|
226
|
+
return f"Invalid coordinate_type '{coordinate_type}'. Valid types: {', '.join(valid_coord_types)}"
|
|
227
|
+
|
|
228
|
+
# Handle string input for atoms (common format: "1,2,3,4")
|
|
229
|
+
if isinstance(atoms, str):
|
|
230
|
+
try:
|
|
231
|
+
# Parse comma-separated string to list of integers
|
|
232
|
+
atoms = [int(x.strip()) for x in atoms.split(",")]
|
|
233
|
+
except ValueError as e:
|
|
234
|
+
return f"Invalid atoms string '{atoms}'. Use format '1,2,3,4' or pass as list [1,2,3,4]. Error: {e}"
|
|
235
|
+
|
|
236
|
+
# Ensure atoms is a list
|
|
237
|
+
if not isinstance(atoms, list):
|
|
238
|
+
return f"Atoms must be a list of integers or comma-separated string. Got: {type(atoms).__name__}"
|
|
239
|
+
|
|
240
|
+
# Validate atom count for coordinate type
|
|
241
|
+
expected_atoms = {"bond": 2, "angle": 3, "dihedral": 4}
|
|
242
|
+
expected_count = expected_atoms[coord_type_lower]
|
|
243
|
+
if len(atoms) != expected_count:
|
|
244
|
+
return f"{coordinate_type} requires exactly {expected_count} atoms, got {len(atoms)}. Use format: [1,2,3,4] or '1,2,3,4'"
|
|
245
|
+
|
|
246
|
+
# Validate atoms are positive integers
|
|
247
|
+
if not all(isinstance(atom, int) and atom > 0 for atom in atoms):
|
|
248
|
+
return f"All atom indices must be positive integers (1-indexed). Got: {atoms}. Use format: [1,2,3,4] or '1,2,3,4'"
|
|
249
|
+
|
|
250
|
+
# Validate scan range
|
|
251
|
+
if num < 2:
|
|
252
|
+
return f"Number of scan points must be at least 2, got {num}"
|
|
253
|
+
|
|
254
|
+
if start >= stop:
|
|
255
|
+
return f"Start value ({start}) must be less than stop value ({stop})"
|
|
256
|
+
|
|
257
|
+
# Handle 2D scan validation
|
|
258
|
+
is_2d_scan = any([coordinate_type_2d, atoms_2d, start_2d is not None, stop_2d is not None, num_2d is not None])
|
|
259
|
+
|
|
260
|
+
if is_2d_scan:
|
|
261
|
+
# For 2D scan, all 2D parameters must be provided
|
|
262
|
+
if not all([coordinate_type_2d, atoms_2d, start_2d is not None, stop_2d is not None, num_2d is not None]):
|
|
263
|
+
return f"For 2D scans, all 2D parameters must be provided: coordinate_type_2d, atoms_2d, start_2d, stop_2d, num_2d"
|
|
264
|
+
|
|
265
|
+
# Validate 2D coordinate type
|
|
266
|
+
coord_type_2d_lower = coordinate_type_2d.lower()
|
|
267
|
+
if coord_type_2d_lower not in valid_coord_types:
|
|
268
|
+
return f"Invalid coordinate_type_2d '{coordinate_type_2d}'. Valid types: {', '.join(valid_coord_types)}"
|
|
269
|
+
|
|
270
|
+
# Handle string input for 2D atoms
|
|
271
|
+
if isinstance(atoms_2d, str):
|
|
272
|
+
try:
|
|
273
|
+
atoms_2d = [int(x.strip()) for x in atoms_2d.split(",")]
|
|
274
|
+
except ValueError as e:
|
|
275
|
+
return f"Invalid atoms_2d string '{atoms_2d}'. Use format '1,2,3,4' or pass as list [1,2,3,4]. Error: {e}"
|
|
276
|
+
|
|
277
|
+
# Validate 2D atom count
|
|
278
|
+
expected_count_2d = expected_atoms[coord_type_2d_lower]
|
|
279
|
+
if len(atoms_2d) != expected_count_2d:
|
|
280
|
+
return f"{coordinate_type_2d} requires exactly {expected_count_2d} atoms, got {len(atoms_2d)}"
|
|
281
|
+
|
|
282
|
+
# Validate 2D atoms are positive integers
|
|
283
|
+
if not all(isinstance(atom, int) and atom > 0 for atom in atoms_2d):
|
|
284
|
+
return f"All 2D atom indices must be positive integers (1-indexed). Got: {atoms_2d}"
|
|
285
|
+
|
|
286
|
+
# Validate 2D scan range
|
|
287
|
+
if num_2d < 2:
|
|
288
|
+
return f"Number of 2D scan points must be at least 2, got {num_2d}"
|
|
289
|
+
|
|
290
|
+
if start_2d >= stop_2d:
|
|
291
|
+
return f"2D start value ({start_2d}) must be less than stop value ({stop_2d})"
|
|
292
|
+
|
|
293
|
+
# Handle concerted scan validation
|
|
294
|
+
if concerted_coordinates:
|
|
295
|
+
# Validate each coordinate in the concerted scan
|
|
296
|
+
for i, coord in enumerate(concerted_coordinates):
|
|
297
|
+
required_keys = {"coordinate_type", "atoms", "start", "stop", "num"}
|
|
298
|
+
if not all(key in coord for key in required_keys):
|
|
299
|
+
return f"Concerted coordinate {i+1} missing required keys: {required_keys}"
|
|
300
|
+
|
|
301
|
+
# All concerted coordinates must have same number of steps
|
|
302
|
+
if coord["num"] != num:
|
|
303
|
+
return f"All concerted scan coordinates must have same number of steps. Got {coord['num']} vs {num}"
|
|
304
|
+
|
|
305
|
+
# Validate coordinate type
|
|
306
|
+
if coord["coordinate_type"].lower() not in valid_coord_types:
|
|
307
|
+
return f"Invalid coordinate_type in concerted coordinate {i+1}: '{coord['coordinate_type']}'"
|
|
308
|
+
|
|
309
|
+
# ENFORCE REQUIRED PARAMETERS FOR SCANWORKFLOW - Always provide robust defaults
|
|
310
|
+
if method is None:
|
|
311
|
+
method = "hf-3c" # Fast, reliable default for scans
|
|
312
|
+
|
|
313
|
+
if engine is None:
|
|
314
|
+
engine = "psi4" # Required by ScanWorkflow
|
|
315
|
+
|
|
316
|
+
if mode is None:
|
|
317
|
+
mode = "rapid" # Good balance for scans
|
|
318
|
+
|
|
319
|
+
# Ensure all required defaults are lowercase for API consistency
|
|
320
|
+
method = method.lower()
|
|
321
|
+
engine = engine.lower()
|
|
322
|
+
mode = mode.lower()
|
|
323
|
+
|
|
324
|
+
# Validate QC parameters if provided
|
|
325
|
+
if method and method.lower() not in QC_METHODS and method.lower() != "hf-3c":
|
|
326
|
+
available_methods = ", ".join(list(QC_METHODS.keys()) + ["hf-3c"])
|
|
327
|
+
return f"Invalid method '{method}'. Available methods: {available_methods}"
|
|
328
|
+
|
|
329
|
+
if basis_set and basis_set.lower() not in QC_BASIS_SETS:
|
|
330
|
+
available_basis = ", ".join(QC_BASIS_SETS.keys())
|
|
331
|
+
return f"Invalid basis set '{basis_set}'. Available basis sets: {available_basis}"
|
|
332
|
+
|
|
333
|
+
if engine and engine.lower() not in QC_ENGINES:
|
|
334
|
+
available_engines = ", ".join(QC_ENGINES.keys())
|
|
335
|
+
return f"Invalid engine '{engine}'. Available engines: {available_engines}"
|
|
336
|
+
|
|
337
|
+
if corrections:
|
|
338
|
+
invalid_corrections = [corr for corr in corrections if corr.lower() not in QC_CORRECTIONS]
|
|
339
|
+
if invalid_corrections:
|
|
340
|
+
available_corrections = ", ".join(QC_CORRECTIONS.keys())
|
|
341
|
+
return f"Invalid corrections {invalid_corrections}. Available corrections: {available_corrections}"
|
|
342
|
+
|
|
343
|
+
# Validate mode
|
|
344
|
+
valid_modes = ["reckless", "rapid", "careful", "meticulous"]
|
|
345
|
+
if mode and mode.lower() not in valid_modes:
|
|
346
|
+
return f"Invalid mode '{mode}'. Valid modes: {', '.join(valid_modes)}"
|
|
347
|
+
|
|
348
|
+
# Log the scan parameters
|
|
349
|
+
logger.info(f"Name: {name}")
|
|
350
|
+
logger.info(f"Input: {molecule}")
|
|
351
|
+
if is_2d_scan:
|
|
352
|
+
logger.info(f"2D Grid Size: {num} × {num_2d} = {num * num_2d} total points")
|
|
353
|
+
if concerted_coordinates:
|
|
354
|
+
logger.info(f"Concerted scan with {len(concerted_coordinates) + 1} coordinates")
|
|
355
|
+
logger.info(f"Mode: {mode}")
|
|
356
|
+
logger.info(f"Wavefront Propagation: {wavefront_propagation}")
|
|
357
|
+
|
|
358
|
+
try:
|
|
359
|
+
# Build scan coordinate specification based on scan type
|
|
360
|
+
scan_coordinates = []
|
|
361
|
+
|
|
362
|
+
# BUILD PRIMARY SCAN_SETTINGS - EXACTLY MATCHING STJAMES SCANSETTINGS
|
|
363
|
+
primary_coord = {
|
|
364
|
+
"type": coord_type_lower, # Required: matches stjames ScanSettings.type
|
|
365
|
+
"atoms": atoms, # Required: list of 1-indexed atom numbers
|
|
366
|
+
"start": float(start), # Required: starting coordinate value
|
|
367
|
+
"stop": float(stop), # Required: ending coordinate value
|
|
368
|
+
"num": int(num) # Required: number of scan points
|
|
369
|
+
}
|
|
370
|
+
scan_coordinates.append(primary_coord)
|
|
371
|
+
|
|
372
|
+
# ADD 2D COORDINATE IF SPECIFIED - EXACT STJAMES FORMAT
|
|
373
|
+
if is_2d_scan:
|
|
374
|
+
secondary_coord = {
|
|
375
|
+
"type": coord_type_2d_lower, # Required: matches stjames ScanSettings.type
|
|
376
|
+
"atoms": atoms_2d, # Required: list of 1-indexed atom numbers
|
|
377
|
+
"start": float(start_2d), # Required: starting coordinate value
|
|
378
|
+
"stop": float(stop_2d), # Required: ending coordinate value
|
|
379
|
+
"num": int(num_2d) # Required: number of scan points
|
|
380
|
+
}
|
|
381
|
+
scan_coordinates.append(secondary_coord)
|
|
382
|
+
|
|
383
|
+
# ADD CONCERTED COORDINATES IF SPECIFIED - EXACT STJAMES FORMAT
|
|
384
|
+
if concerted_coordinates:
|
|
385
|
+
for i, coord in enumerate(concerted_coordinates):
|
|
386
|
+
concerted_coord = {
|
|
387
|
+
"type": coord["coordinate_type"].lower(), # Required: matches stjames ScanSettings.type
|
|
388
|
+
"atoms": coord["atoms"], # Required: list of 1-indexed atom numbers
|
|
389
|
+
"start": float(coord["start"]), # Required: starting coordinate value
|
|
390
|
+
"stop": float(coord["stop"]), # Required: ending coordinate value
|
|
391
|
+
"num": int(coord["num"]) # Required: number of scan points
|
|
392
|
+
}
|
|
393
|
+
scan_coordinates.append(concerted_coord)
|
|
394
|
+
|
|
395
|
+
# Build parameters for rowan.compute call
|
|
396
|
+
compute_params = {
|
|
397
|
+
"name": name,
|
|
398
|
+
"molecule": canonical_smiles, # Required by rowan.compute() API
|
|
399
|
+
"folder_uuid": folder_uuid,
|
|
400
|
+
"blocking": blocking,
|
|
401
|
+
"ping_interval": ping_interval,
|
|
402
|
+
# Add initial_molecule parameter for MoleculeWorkflow compatibility
|
|
403
|
+
"initial_molecule": canonical_smiles # Required by stjames MoleculeWorkflow
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
# SCAN_SETTINGS CONSTRUCTION - STRICTLY FOLLOWING STJAMES SCANWORKFLOW REQUIREMENTS
|
|
407
|
+
if len(scan_coordinates) == 1:
|
|
408
|
+
# SINGLE COORDINATE SCAN: scan_settings is a ScanSettings object
|
|
409
|
+
compute_params["scan_settings"] = scan_coordinates[0]
|
|
410
|
+
else:
|
|
411
|
+
if is_2d_scan:
|
|
412
|
+
# 2D SCAN: separate scan_settings and scan_settings_2d fields
|
|
413
|
+
compute_params["scan_settings"] = scan_coordinates[0] # Primary coordinate
|
|
414
|
+
compute_params["scan_settings_2d"] = scan_coordinates[1] # Secondary coordinate
|
|
415
|
+
else:
|
|
416
|
+
# CONCERTED SCAN: scan_settings is a list of ScanSettings
|
|
417
|
+
compute_params["scan_settings"] = scan_coordinates
|
|
418
|
+
|
|
419
|
+
# Add wavefront propagation setting
|
|
420
|
+
compute_params["wavefront_propagation"] = wavefront_propagation
|
|
421
|
+
|
|
422
|
+
# BUILD REQUIRED CALC_SETTINGS - ALWAYS COMPLETE
|
|
423
|
+
calc_settings = {
|
|
424
|
+
"method": method, # Always present due to defaults above
|
|
425
|
+
"mode": mode, # Always present due to defaults above
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
# Add charge/multiplicity only if non-default
|
|
429
|
+
if charge != 0:
|
|
430
|
+
calc_settings["charge"] = charge
|
|
431
|
+
if multiplicity != 1:
|
|
432
|
+
calc_settings["multiplicity"] = multiplicity
|
|
433
|
+
|
|
434
|
+
# Add optional parameters if provided
|
|
435
|
+
if basis_set:
|
|
436
|
+
calc_settings["basis_set"] = basis_set.lower()
|
|
437
|
+
if corrections:
|
|
438
|
+
calc_settings["corrections"] = [corr.lower() for corr in corrections]
|
|
439
|
+
if constraints:
|
|
440
|
+
calc_settings["constraints"] = constraints
|
|
441
|
+
|
|
442
|
+
# REQUIRED FIELDS - ScanWorkflow validation will fail without these
|
|
443
|
+
compute_params["calc_settings"] = calc_settings
|
|
444
|
+
compute_params["calc_engine"] = engine # Always present due to defaults above
|
|
445
|
+
|
|
446
|
+
# Submit the scan workflow
|
|
447
|
+
result = rowan.compute(workflow_type="scan", **compute_params)
|
|
448
|
+
|
|
449
|
+
# Format results based on status
|
|
450
|
+
uuid = result.get('uuid', 'N/A')
|
|
451
|
+
status = result.get('status', 'unknown')
|
|
452
|
+
|
|
453
|
+
# Determine scan type for display
|
|
454
|
+
scan_type = "1D Scan"
|
|
455
|
+
total_points = num
|
|
456
|
+
if is_2d_scan:
|
|
457
|
+
scan_type = "2D Grid Scan"
|
|
458
|
+
total_points = num * num_2d
|
|
459
|
+
elif concerted_coordinates:
|
|
460
|
+
scan_type = f"Concerted Scan ({len(concerted_coordinates) + 1} coordinates)"
|
|
461
|
+
total_points = num
|
|
462
|
+
|
|
463
|
+
# Format response
|
|
464
|
+
if blocking:
|
|
465
|
+
# Blocking mode - check if successful
|
|
466
|
+
if status == "success":
|
|
467
|
+
formatted = f"✅ {scan_type} '{name}' completed successfully!\n"
|
|
468
|
+
formatted += f"🔖 Workflow UUID: {uuid}\n"
|
|
469
|
+
formatted += f"📊 Status: {status}\n\n"
|
|
470
|
+
|
|
471
|
+
# Extract scan results if available
|
|
472
|
+
object_data = result.get("object_data", {})
|
|
473
|
+
scan_points = object_data.get("scan_points", [])
|
|
474
|
+
|
|
475
|
+
if scan_points:
|
|
476
|
+
formatted += f"📈 Scan Results: {len(scan_points)} points calculated\n"
|
|
477
|
+
|
|
478
|
+
# Show first few scan points
|
|
479
|
+
for i, point in enumerate(scan_points[:3]):
|
|
480
|
+
if isinstance(point, dict):
|
|
481
|
+
energy = point.get("energy", "N/A")
|
|
482
|
+
formatted += f" Point {i+1}: Energy = {energy}\n"
|
|
483
|
+
|
|
484
|
+
if len(scan_points) > 3:
|
|
485
|
+
formatted += f" ... and {len(scan_points) - 3} more points\n"
|
|
486
|
+
else:
|
|
487
|
+
formatted += "📈 Results: Check workflow details for scan data\n"
|
|
488
|
+
|
|
489
|
+
return formatted
|
|
490
|
+
else:
|
|
491
|
+
# Failed calculation
|
|
492
|
+
return f"❌ {scan_type} calculation failed\n🔖 UUID: {uuid}\n📋 Status: {status}\n💬 Check workflow details for more information"
|
|
493
|
+
else:
|
|
494
|
+
# Non-blocking mode - just submission confirmation
|
|
495
|
+
formatted = f"📋 {scan_type} '{name}' submitted!\n"
|
|
496
|
+
formatted += f"🔖 Workflow UUID: {uuid}\n"
|
|
497
|
+
formatted += f"⏳ Status: Running...\n"
|
|
498
|
+
formatted += f"💡 Use rowan_workflow_management to check status\n\n"
|
|
499
|
+
|
|
500
|
+
formatted += f"Scan Details:\n"
|
|
501
|
+
formatted += f"🧬 Molecule: {canonical_smiles}\n"
|
|
502
|
+
formatted += f"📐 Coordinate: {coordinate_type.upper()} on atoms {atoms} ({start} to {stop}, {num} points)\n"
|
|
503
|
+
if is_2d_scan:
|
|
504
|
+
formatted += f"📐 Secondary: {coordinate_type_2d.upper()} on atoms {atoms_2d} ({start_2d} to {stop_2d}, {num_2d} points)\n"
|
|
505
|
+
formatted += f"📊 Total Points: {total_points}\n"
|
|
506
|
+
formatted += f"⚙️ Method: {method}, Engine: {engine}\n"
|
|
507
|
+
|
|
508
|
+
return formatted
|
|
509
|
+
|
|
510
|
+
except Exception as e:
|
|
511
|
+
logger.error(f"Error in rowan_scan: {str(e)}")
|
|
512
|
+
return f"PES scan submission failed: {str(e)}"
|
|
513
|
+
|
|
514
|
+
def test_rowan_scan():
|
|
515
|
+
"""Test the rowan_scan function."""
|
|
516
|
+
try:
|
|
517
|
+
# Test with minimal parameters - should use all defaults
|
|
518
|
+
result = rowan_scan(
|
|
519
|
+
name="test_scan_ethane",
|
|
520
|
+
molecule="CC",
|
|
521
|
+
coordinate_type="bond",
|
|
522
|
+
atoms=[1, 2],
|
|
523
|
+
start=1.4,
|
|
524
|
+
stop=1.8,
|
|
525
|
+
num=3, # Very short for testing
|
|
526
|
+
blocking=False
|
|
527
|
+
)
|
|
528
|
+
print("✅ Scan test successful!")
|
|
529
|
+
print(f"Result: {result[:200]}..." if len(result) > 200 else result)
|
|
530
|
+
return True
|
|
531
|
+
except Exception as e:
|
|
532
|
+
print(f"❌ Scan test failed: {e}")
|
|
533
|
+
return False
|
|
534
|
+
|
|
535
|
+
if __name__ == "__main__":
|
|
536
|
+
test_rowan_scan()
|