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.
- rowan_mcp/__init__.py +2 -2
- rowan_mcp/__main__.py +3 -5
- rowan_mcp/functions_v2/BENCHMARK.md +86 -0
- rowan_mcp/functions_v2/molecule_lookup.py +232 -0
- rowan_mcp/functions_v2/protein_management.py +141 -0
- rowan_mcp/functions_v2/submit_basic_calculation_workflow.py +195 -0
- rowan_mcp/functions_v2/submit_conformer_search_workflow.py +158 -0
- rowan_mcp/functions_v2/submit_descriptors_workflow.py +52 -0
- rowan_mcp/functions_v2/submit_docking_workflow.py +244 -0
- rowan_mcp/functions_v2/submit_fukui_workflow.py +114 -0
- rowan_mcp/functions_v2/submit_irc_workflow.py +58 -0
- rowan_mcp/functions_v2/submit_macropka_workflow.py +99 -0
- rowan_mcp/functions_v2/submit_pka_workflow.py +72 -0
- rowan_mcp/functions_v2/submit_protein_cofolding_workflow.py +88 -0
- rowan_mcp/functions_v2/submit_redox_potential_workflow.py +55 -0
- rowan_mcp/functions_v2/submit_scan_workflow.py +82 -0
- rowan_mcp/functions_v2/submit_solubility_workflow.py +157 -0
- rowan_mcp/functions_v2/submit_tautomer_search_workflow.py +51 -0
- rowan_mcp/functions_v2/workflow_management_v2.py +382 -0
- rowan_mcp/server.py +109 -144
- rowan_mcp/tests/basic_calculation_from_json.py +0 -0
- rowan_mcp/tests/basic_calculation_with_constraint.py +33 -0
- rowan_mcp/tests/basic_calculation_with_solvent.py +0 -0
- rowan_mcp/tests/bde.py +37 -0
- rowan_mcp/tests/benchmark_queries.md +120 -0
- rowan_mcp/tests/cofolding_screen.py +131 -0
- rowan_mcp/tests/conformer_dependent_redox.py +37 -0
- rowan_mcp/tests/conformers.py +31 -0
- rowan_mcp/tests/data.json +189 -0
- rowan_mcp/tests/docking_screen.py +157 -0
- rowan_mcp/tests/irc.py +24 -0
- rowan_mcp/tests/macropka.py +13 -0
- rowan_mcp/tests/multistage_opt.py +13 -0
- rowan_mcp/tests/optimization.py +21 -0
- rowan_mcp/tests/phenol_pka.py +36 -0
- rowan_mcp/tests/pka.py +36 -0
- rowan_mcp/tests/protein_cofolding.py +17 -0
- rowan_mcp/tests/scan.py +28 -0
- {rowan_mcp-1.0.1.dist-info → rowan_mcp-2.0.0.dist-info}/METADATA +49 -33
- rowan_mcp-2.0.0.dist-info/RECORD +42 -0
- rowan_mcp/functions/admet.py +0 -94
- rowan_mcp/functions/bde.py +0 -113
- rowan_mcp/functions/calculation_retrieve.py +0 -89
- rowan_mcp/functions/conformers.py +0 -80
- rowan_mcp/functions/descriptors.py +0 -92
- rowan_mcp/functions/docking.py +0 -340
- rowan_mcp/functions/docking_enhanced.py +0 -174
- rowan_mcp/functions/electronic_properties.py +0 -205
- rowan_mcp/functions/folder_management.py +0 -137
- rowan_mcp/functions/fukui.py +0 -219
- rowan_mcp/functions/hydrogen_bond_basicity.py +0 -94
- rowan_mcp/functions/irc.py +0 -125
- rowan_mcp/functions/macropka.py +0 -120
- rowan_mcp/functions/molecular_converter.py +0 -423
- rowan_mcp/functions/molecular_dynamics.py +0 -191
- rowan_mcp/functions/molecule_lookup.py +0 -57
- rowan_mcp/functions/multistage_opt.py +0 -171
- rowan_mcp/functions/pdb_handler.py +0 -200
- rowan_mcp/functions/pka.py +0 -137
- rowan_mcp/functions/redox_potential.py +0 -352
- rowan_mcp/functions/scan.py +0 -536
- rowan_mcp/functions/scan_analyzer.py +0 -347
- rowan_mcp/functions/solubility.py +0 -277
- rowan_mcp/functions/spin_states.py +0 -747
- rowan_mcp/functions/system_management.py +0 -368
- rowan_mcp/functions/tautomers.py +0 -91
- rowan_mcp/functions/workflow_management.py +0 -422
- rowan_mcp-1.0.1.dist-info/RECORD +0 -34
- {rowan_mcp-1.0.1.dist-info → rowan_mcp-2.0.0.dist-info}/WHEEL +0 -0
- {rowan_mcp-1.0.1.dist-info → rowan_mcp-2.0.0.dist-info}/entry_points.txt +0 -0
|
@@ -1,747 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Rowan spin states calculation function for MCP tool integration.
|
|
3
|
-
Follows the exact workflow from: https://github.com/rowansci/stjames-public/blob/master/stjames/workflows/spin_states.py
|
|
4
|
-
Implements SpinStatesWorkflow(MoleculeWorkflow) pattern from stjames.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from typing import Optional, Union, List, Any
|
|
8
|
-
import logging
|
|
9
|
-
import rowan
|
|
10
|
-
from .molecular_converter import convert_to_smiles
|
|
11
|
-
|
|
12
|
-
logger = logging.getLogger(__name__)
|
|
13
|
-
|
|
14
|
-
# Mock stjames workflow classes to match their structure
|
|
15
|
-
class Mode:
|
|
16
|
-
"""Mock Mode enum from stjames."""
|
|
17
|
-
AUTO = "auto"
|
|
18
|
-
RAPID = "rapid"
|
|
19
|
-
CAREFUL = "careful"
|
|
20
|
-
METICULOUS = "meticulous"
|
|
21
|
-
RECKLESS = "reckless"
|
|
22
|
-
MANUAL = "manual"
|
|
23
|
-
|
|
24
|
-
class Molecule:
|
|
25
|
-
"""Mock Molecule class from stjames."""
|
|
26
|
-
def __init__(self, smiles: str, charge: int = 0, multiplicity: int = 1):
|
|
27
|
-
self.smiles = smiles
|
|
28
|
-
self.charge = charge
|
|
29
|
-
self.multiplicity = multiplicity
|
|
30
|
-
|
|
31
|
-
def __str__(self):
|
|
32
|
-
return self.smiles
|
|
33
|
-
|
|
34
|
-
class MoleculeWorkflow:
|
|
35
|
-
"""
|
|
36
|
-
Mock MoleculeWorkflow base class from stjames.
|
|
37
|
-
Base class for Workflows that operate on a single molecule.
|
|
38
|
-
"""
|
|
39
|
-
def __init__(self, initial_molecule: Union[str, Molecule], mode: str = "auto"):
|
|
40
|
-
if isinstance(initial_molecule, str):
|
|
41
|
-
self.initial_molecule = Molecule(initial_molecule)
|
|
42
|
-
else:
|
|
43
|
-
self.initial_molecule = initial_molecule
|
|
44
|
-
|
|
45
|
-
# Set mode to RAPID if AUTO is selected (stjames behavior)
|
|
46
|
-
if mode.lower() == "auto":
|
|
47
|
-
self.mode = "rapid"
|
|
48
|
-
else:
|
|
49
|
-
self.mode = mode.lower()
|
|
50
|
-
|
|
51
|
-
def __repr__(self):
|
|
52
|
-
return f"<{type(self).__name__} {self.mode.upper()}>"
|
|
53
|
-
|
|
54
|
-
class SpinStatesWorkflow(MoleculeWorkflow):
|
|
55
|
-
"""
|
|
56
|
-
Mock SpinStatesWorkflow from stjames that inherits from MoleculeWorkflow.
|
|
57
|
-
Workflow for computing spin states of molecules.
|
|
58
|
-
"""
|
|
59
|
-
def __init__(self, initial_molecule: Union[str, Molecule], states: List[int],
|
|
60
|
-
mode: str = "auto", solvent: Optional[str] = None,
|
|
61
|
-
xtb_preopt: bool = False, constraints: Optional[List] = None,
|
|
62
|
-
transition_state: bool = False, frequencies: bool = False):
|
|
63
|
-
super().__init__(initial_molecule, mode)
|
|
64
|
-
self.states = states
|
|
65
|
-
self.solvent = solvent
|
|
66
|
-
self.xtb_preopt = xtb_preopt
|
|
67
|
-
self.constraints = constraints or []
|
|
68
|
-
self.transition_state = transition_state
|
|
69
|
-
self.frequencies = frequencies
|
|
70
|
-
|
|
71
|
-
# Validate states (stjames validation logic)
|
|
72
|
-
self._validate_states()
|
|
73
|
-
|
|
74
|
-
def _validate_states(self):
|
|
75
|
-
"""Confirm that all spin states are valid (from stjames)."""
|
|
76
|
-
if not self.states:
|
|
77
|
-
raise ValueError("Expected at least one spin state.")
|
|
78
|
-
|
|
79
|
-
# Check multiplicities are consistent (all odd or all even)
|
|
80
|
-
if any((self.states[0] - mult) % 2 for mult in self.states):
|
|
81
|
-
raise ValueError(f"Inconsistent multiplicities found: {self.states}")
|
|
82
|
-
|
|
83
|
-
def __repr__(self):
|
|
84
|
-
if self.mode != "manual":
|
|
85
|
-
return f"<{type(self).__name__} {self.states} {self.mode.upper()}>"
|
|
86
|
-
return f"<{type(self).__name__} {self.states} {self.mode.upper()}>"
|
|
87
|
-
|
|
88
|
-
def __len__(self):
|
|
89
|
-
return len(self.states)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
# Removed old hardcoded conversion function - now using dynamic molecular_converter.py
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def _generate_3d_coordinates_for_coordination_complex(smiles: str, rdkit_mol):
|
|
96
|
-
"""Generate clean 3D coordinates for coordination complexes."""
|
|
97
|
-
try:
|
|
98
|
-
import numpy as np
|
|
99
|
-
|
|
100
|
-
# Get number of atoms
|
|
101
|
-
num_atoms = rdkit_mol.GetNumAtoms()
|
|
102
|
-
|
|
103
|
-
# Find metal center and ligands
|
|
104
|
-
metal_idx = None
|
|
105
|
-
ligand_indices = []
|
|
106
|
-
|
|
107
|
-
for i, atom in enumerate(rdkit_mol.GetAtoms()):
|
|
108
|
-
atomic_num = atom.GetAtomicNum()
|
|
109
|
-
if atomic_num in [25, 26, 27, 28, 29, 24, 46]: # Transition metals
|
|
110
|
-
metal_idx = i
|
|
111
|
-
else:
|
|
112
|
-
ligand_indices.append(i)
|
|
113
|
-
|
|
114
|
-
if metal_idx is None:
|
|
115
|
-
return None
|
|
116
|
-
|
|
117
|
-
# Initialize coordinates
|
|
118
|
-
coords = [[0.0, 0.0, 0.0] for _ in range(num_atoms)]
|
|
119
|
-
|
|
120
|
-
# Place metal at origin
|
|
121
|
-
coords[metal_idx] = [0.0, 0.0, 0.0]
|
|
122
|
-
|
|
123
|
-
# Set appropriate bond length for M-Cl bonds
|
|
124
|
-
bond_length = 2.4 # Standard M-Cl bond length in Å
|
|
125
|
-
|
|
126
|
-
# Generate geometry based on coordination number
|
|
127
|
-
num_ligands = len(ligand_indices)
|
|
128
|
-
|
|
129
|
-
if num_ligands == 4:
|
|
130
|
-
# Square planar for Pd(II) complexes
|
|
131
|
-
if any(atom.GetAtomicNum() == 46 for atom in rdkit_mol.GetAtoms()):
|
|
132
|
-
positions = np.array([
|
|
133
|
-
[bond_length, 0, 0],
|
|
134
|
-
[-bond_length, 0, 0],
|
|
135
|
-
[0, bond_length, 0],
|
|
136
|
-
[0, -bond_length, 0]
|
|
137
|
-
])
|
|
138
|
-
else:
|
|
139
|
-
# Tetrahedral for other metals
|
|
140
|
-
a = bond_length / np.sqrt(3)
|
|
141
|
-
positions = np.array([
|
|
142
|
-
[a, a, a],
|
|
143
|
-
[a, -a, -a],
|
|
144
|
-
[-a, a, -a],
|
|
145
|
-
[-a, -a, a]
|
|
146
|
-
])
|
|
147
|
-
|
|
148
|
-
elif num_ligands == 6:
|
|
149
|
-
# Octahedral geometry
|
|
150
|
-
positions = np.array([
|
|
151
|
-
[bond_length, 0, 0],
|
|
152
|
-
[-bond_length, 0, 0],
|
|
153
|
-
[0, bond_length, 0],
|
|
154
|
-
[0, -bond_length, 0],
|
|
155
|
-
[0, 0, bond_length],
|
|
156
|
-
[0, 0, -bond_length]
|
|
157
|
-
])
|
|
158
|
-
|
|
159
|
-
else:
|
|
160
|
-
# General spherical distribution
|
|
161
|
-
angles = np.linspace(0, 2*np.pi, num_ligands, endpoint=False)
|
|
162
|
-
positions = []
|
|
163
|
-
for angle in angles:
|
|
164
|
-
x = bond_length * np.cos(angle)
|
|
165
|
-
y = bond_length * np.sin(angle)
|
|
166
|
-
z = 0.0
|
|
167
|
-
positions.append([x, y, z])
|
|
168
|
-
positions = np.array(positions)
|
|
169
|
-
|
|
170
|
-
# Assign positions to ligands
|
|
171
|
-
for i, ligand_idx in enumerate(ligand_indices):
|
|
172
|
-
if i < len(positions):
|
|
173
|
-
coords[ligand_idx] = positions[i].tolist()
|
|
174
|
-
|
|
175
|
-
# Validate minimum distances
|
|
176
|
-
min_distance = float('inf')
|
|
177
|
-
for i in range(num_atoms):
|
|
178
|
-
for j in range(i+1, num_atoms):
|
|
179
|
-
dist = np.linalg.norm(np.array(coords[i]) - np.array(coords[j]))
|
|
180
|
-
min_distance = min(min_distance, dist)
|
|
181
|
-
|
|
182
|
-
if min_distance > 1.0: # At least 1.0 Å separation
|
|
183
|
-
return coords
|
|
184
|
-
else:
|
|
185
|
-
return None
|
|
186
|
-
|
|
187
|
-
except Exception as e:
|
|
188
|
-
logger.warning(f"Coordinate generation failed: {e}")
|
|
189
|
-
return None
|
|
190
|
-
|
|
191
|
-
# Removed old complex coordinate generation functions - simplified to single clean function above
|
|
192
|
-
|
|
193
|
-
def _calculate_molecular_charge(smiles: str) -> int:
|
|
194
|
-
"""Calculate the total molecular charge from SMILES string."""
|
|
195
|
-
total_charge = 0
|
|
196
|
-
|
|
197
|
-
# Count negative ions
|
|
198
|
-
if '[Cl-]' in smiles:
|
|
199
|
-
total_charge -= smiles.count('[Cl-]')
|
|
200
|
-
if '[F-]' in smiles:
|
|
201
|
-
total_charge -= smiles.count('[F-]')
|
|
202
|
-
if '[Br-]' in smiles:
|
|
203
|
-
total_charge -= smiles.count('[Br-]')
|
|
204
|
-
if '[I-]' in smiles:
|
|
205
|
-
total_charge -= smiles.count('[I-]')
|
|
206
|
-
|
|
207
|
-
# Count positive metal ions - common oxidation states
|
|
208
|
-
positive_ions = {
|
|
209
|
-
'[Pd+2]': 2, '[Pd+4]': 4,
|
|
210
|
-
'[Mn+2]': 2, '[Mn+3]': 3, '[Mn+4]': 4, '[Mn+7]': 7,
|
|
211
|
-
'[Fe+2]': 2, '[Fe+3]': 3,
|
|
212
|
-
'[Co+2]': 2, '[Co+3]': 3,
|
|
213
|
-
'[Ni+2]': 2, '[Ni+3]': 3,
|
|
214
|
-
'[Cu+1]': 1, '[Cu+2]': 2,
|
|
215
|
-
'[Cr+2]': 2, '[Cr+3]': 3, '[Cr+6]': 6,
|
|
216
|
-
'[V+2]': 2, '[V+3]': 3, '[V+4]': 4, '[V+5]': 5,
|
|
217
|
-
'[Ti+2]': 2, '[Ti+3]': 3, '[Ti+4]': 4,
|
|
218
|
-
'[Zn+2]': 2
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
for ion, charge in positive_ions.items():
|
|
222
|
-
if ion in smiles:
|
|
223
|
-
total_charge += charge * smiles.count(ion)
|
|
224
|
-
|
|
225
|
-
return total_charge
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
def rowan_spin_states(
|
|
229
|
-
name: str,
|
|
230
|
-
molecule: str,
|
|
231
|
-
states: Optional[Union[str, List[int]]] = None,
|
|
232
|
-
mode: str = "rapid",
|
|
233
|
-
solvent: Optional[str] = None,
|
|
234
|
-
xtb_preopt: bool = False,
|
|
235
|
-
constraints: Optional[str] = None,
|
|
236
|
-
transition_state: bool = False,
|
|
237
|
-
frequencies: bool = False,
|
|
238
|
-
folder_uuid: Optional[str] = None,
|
|
239
|
-
blocking: bool = False,
|
|
240
|
-
ping_interval: int = 5
|
|
241
|
-
) -> str:
|
|
242
|
-
"""Calculate electronic spin states for molecular systems.
|
|
243
|
-
|
|
244
|
-
This tool computes and compares different spin multiplicities for a molecule,
|
|
245
|
-
helping determine the ground state electronic configuration and relative energies
|
|
246
|
-
of different spin states. Essential for studying transition metals, radicals,
|
|
247
|
-
and systems with unpaired electrons.
|
|
248
|
-
|
|
249
|
-
Args:
|
|
250
|
-
name: Name for the spin states calculation
|
|
251
|
-
molecule: SMILES string of the molecule (e.g., "[Mn+3]", "O=O")
|
|
252
|
-
states: List of spin multiplicities to calculate (e.g., [1, 3, 5] for singlet, triplet, quintet)
|
|
253
|
-
Can be provided as comma-separated string "1,3,5" or list [1, 3, 5]
|
|
254
|
-
mode: Calculation mode ("rapid", "careful", "meticulous", "auto", "reckless", "manual")
|
|
255
|
-
solvent: Solvent for implicit solvation (e.g., "water", "hexane", "acetonitrile")
|
|
256
|
-
xtb_preopt: Whether to perform xTB pre-optimization before DFT
|
|
257
|
-
constraints: Geometric constraints during optimization (advanced feature)
|
|
258
|
-
transition_state: Whether to optimize for transition state instead of minimum
|
|
259
|
-
frequencies: Whether to calculate vibrational frequencies
|
|
260
|
-
folder_uuid: Optional folder UUID to organize the calculation
|
|
261
|
-
blocking: Whether to wait for completion (default: False to avoid timeouts)
|
|
262
|
-
ping_interval: Interval in seconds to check calculation status
|
|
263
|
-
|
|
264
|
-
Returns:
|
|
265
|
-
JSON string with workflow UUID and status (non-blocking) or full results (blocking)
|
|
266
|
-
|
|
267
|
-
Examples:
|
|
268
|
-
# Manganese atom (stjames-public test example)
|
|
269
|
-
rowan_spin_states(
|
|
270
|
-
name="manganese_atom_stjames",
|
|
271
|
-
molecule="[Mn]", # Neutral Mn atom
|
|
272
|
-
states=[2, 4, 6], # doublet, quartet, sextet
|
|
273
|
-
mode="rapid",
|
|
274
|
-
xtb_preopt=True
|
|
275
|
-
)
|
|
276
|
-
|
|
277
|
-
# Iron atom (stjames-public test example)
|
|
278
|
-
rowan_spin_states(
|
|
279
|
-
name="iron_atom_stjames",
|
|
280
|
-
molecule="[Fe]", # Neutral Fe atom
|
|
281
|
-
states=[1, 3, 5], # singlet, triplet, quintet
|
|
282
|
-
mode="careful",
|
|
283
|
-
frequencies=True,
|
|
284
|
-
xtb_preopt=True
|
|
285
|
-
)
|
|
286
|
-
|
|
287
|
-
# Manganese hexachloride complex (corrected format)
|
|
288
|
-
rowan_spin_states(
|
|
289
|
-
name="mn_hexachloride",
|
|
290
|
-
molecule="[Cl-].[Cl-].[Cl-].[Cl-].[Cl-].[Cl-].[Mn+2]",
|
|
291
|
-
states=[2, 4, 6],
|
|
292
|
-
mode="rapid",
|
|
293
|
-
solvent="water"
|
|
294
|
-
)
|
|
295
|
-
"""
|
|
296
|
-
|
|
297
|
-
# Convert input to SMILES format (handles XYZ coordinates, molecular formulas, etc.)
|
|
298
|
-
original_input = molecule
|
|
299
|
-
molecule = convert_to_smiles(molecule)
|
|
300
|
-
|
|
301
|
-
# Log the conversion if it happened
|
|
302
|
-
if molecule != original_input:
|
|
303
|
-
logger.info(f"Converted input '{original_input}' to SMILES: '{molecule}'")
|
|
304
|
-
|
|
305
|
-
# Check for unsupported formats
|
|
306
|
-
if molecule.startswith("UNSUPPORTED_"):
|
|
307
|
-
error_response = {
|
|
308
|
-
"success": False,
|
|
309
|
-
"error": f"Unsupported molecular input format: {original_input}",
|
|
310
|
-
"explanation": {
|
|
311
|
-
"detected_format": "XYZ coordinates or complex molecular structure",
|
|
312
|
-
"supported_formats": [
|
|
313
|
-
"SMILES strings: '[Mn]', '[Cl-].[Mn+2]'",
|
|
314
|
-
"Simple formulas: 'Mn(Cl)6', 'Fe(Cl)6'",
|
|
315
|
-
"Common names: 'water', 'methane'"
|
|
316
|
-
]
|
|
317
|
-
},
|
|
318
|
-
"suggestion": "Please provide the molecule in SMILES format or a supported formula",
|
|
319
|
-
"name": name
|
|
320
|
-
}
|
|
321
|
-
return str(error_response)
|
|
322
|
-
|
|
323
|
-
# First, validate that molecule is actually a molecule (SMILES) and not a PDB file
|
|
324
|
-
if molecule.lower().endswith('.pdb') or 'pdb' in molecule.lower():
|
|
325
|
-
error_response = {
|
|
326
|
-
"success": False,
|
|
327
|
-
"error": f"Invalid input: '{molecule}' appears to be a PDB file",
|
|
328
|
-
"correct_usage": {
|
|
329
|
-
"purpose": "Spin states calculation requires a SMILES molecule string, not a PDB file",
|
|
330
|
-
"explanation": "This tool calculates electronic spin states for individual molecules",
|
|
331
|
-
"what_you_provided": "PDB file (protein structure)",
|
|
332
|
-
"what_is_needed": "SMILES string representing a small molecule"
|
|
333
|
-
},
|
|
334
|
-
"examples": {
|
|
335
|
-
"transition_metals": {
|
|
336
|
-
"manganese_atom": "[Mn] (neutral manganese atom)",
|
|
337
|
-
"iron_atom": "[Fe] (neutral iron atom)",
|
|
338
|
-
"manganese_complex": "[Cl-].[Cl-].[Cl-].[Cl-].[Cl-].[Cl-].[Mn+2] (MnCl6 complex)"
|
|
339
|
-
},
|
|
340
|
-
"organic_molecules": {
|
|
341
|
-
"water": "O",
|
|
342
|
-
"methane": "C",
|
|
343
|
-
"benzene": "c1ccccc1",
|
|
344
|
-
"radical": "c1ccc(cc1)[CH2] (benzyl radical)"
|
|
345
|
-
}
|
|
346
|
-
},
|
|
347
|
-
"stjames_test_examples": {
|
|
348
|
-
"manganese": "molecule='[Mn]', states=[2,4,6], mode='rapid'",
|
|
349
|
-
"iron": "molecule='[Fe]', states=[1,3,5], mode='careful'"
|
|
350
|
-
},
|
|
351
|
-
"suggestion": "Use a different Rowan tool for protein analysis, or provide a SMILES string for molecular spin states",
|
|
352
|
-
"name": name,
|
|
353
|
-
"invalid_input": molecule
|
|
354
|
-
}
|
|
355
|
-
return str(error_response)
|
|
356
|
-
|
|
357
|
-
# Check if molecule looks like it needs charge/oxidation state specification
|
|
358
|
-
needs_charge_info = False
|
|
359
|
-
molecular_formula = None
|
|
360
|
-
|
|
361
|
-
# Detect if molecule needs proper SMILES formatting
|
|
362
|
-
# Check for invalid SMILES patterns like [MnCl6]+4 or simple formulas like Mn(Cl)6
|
|
363
|
-
invalid_patterns = [
|
|
364
|
-
# Pattern like [MnCl6]+4 where charge is outside brackets
|
|
365
|
-
(']' in molecule and ('+' in molecule[molecule.rfind(']'):] or '-' in molecule[molecule.rfind(']'):])),
|
|
366
|
-
# Simple molecular formula without proper charge specification
|
|
367
|
-
(not any(char in molecule for char in ['[', ']', '+', '-']) and
|
|
368
|
-
any(metal in molecule.upper() for metal in ['MN', 'FE', 'CO', 'NI', 'CU', 'CR', 'V', 'TI', 'PD'])),
|
|
369
|
-
# Invalid complex notation like [MnCl6] without proper ion separation
|
|
370
|
-
('[' in molecule and ']' in molecule and 'Cl' in molecule.upper() and
|
|
371
|
-
not ('.' in molecule and '[Cl-]' in molecule))
|
|
372
|
-
]
|
|
373
|
-
|
|
374
|
-
if any(invalid_patterns) and any(metal in molecule.upper() for metal in ['MN', 'FE', 'CO', 'NI', 'CU', 'CR', 'V', 'TI', 'PD']):
|
|
375
|
-
needs_charge_info = True
|
|
376
|
-
molecular_formula = molecule
|
|
377
|
-
|
|
378
|
-
# Ensure we have states - provide intelligent defaults based on electron parity rules
|
|
379
|
-
if states is None:
|
|
380
|
-
# Auto-assign reasonable default states based on the molecule and electron count
|
|
381
|
-
if any(metal in molecule.upper() for metal in ['MN', 'FE', 'CO', 'NI', 'CU', 'CR', 'V', 'TI', 'PD']):
|
|
382
|
-
# For transition metals, assign states based on electron parity rules
|
|
383
|
-
# Even electrons (Fe=26, Ni=28, Cr=24, Ti=22, V=23, Zn=30) need ODD multiplicities
|
|
384
|
-
# Odd electrons (Mn=25, Co=27, Cu=29) need EVEN multiplicities
|
|
385
|
-
|
|
386
|
-
if any(metal in molecule.upper() for metal in ['FE', 'NI', 'CR', 'TI', 'ZN', 'PD']):
|
|
387
|
-
# Even electron metals → odd multiplicities
|
|
388
|
-
states_list = [1, 3, 5] # Singlet, triplet, quintet
|
|
389
|
-
logger.info(f"Auto-assigned odd multiplicities for even-electron metal: {states_list}")
|
|
390
|
-
elif any(metal in molecule.upper() for metal in ['MN', 'CO', 'CU', 'V']):
|
|
391
|
-
# Odd electron metals → even multiplicities
|
|
392
|
-
states_list = [2, 4, 6] # Doublet, quartet, sextet
|
|
393
|
-
logger.info(f"Auto-assigned even multiplicities for odd-electron metal: {states_list}")
|
|
394
|
-
else:
|
|
395
|
-
# Default fallback for other transition metals
|
|
396
|
-
states_list = [1, 3, 5] # Conservative choice
|
|
397
|
-
logger.info(f"Auto-assigned default states for transition metal: {states_list}")
|
|
398
|
-
else:
|
|
399
|
-
# For organic molecules, use singlet/triplet (most have even electrons)
|
|
400
|
-
states_list = [1, 3] # Singlet, triplet
|
|
401
|
-
logger.info(f"Auto-assigned default states for organic molecule: {states_list}")
|
|
402
|
-
|
|
403
|
-
# Handle states input - convert to list of integers
|
|
404
|
-
if states is not None and isinstance(states, str):
|
|
405
|
-
# Handle special keywords
|
|
406
|
-
if states.lower() in ['all', 'comprehensive']:
|
|
407
|
-
states_list = [1, 2, 3, 4, 5, 6] # All reasonable multiplicities
|
|
408
|
-
elif states.lower() in ['common', 'typical']:
|
|
409
|
-
states_list = [2, 4, 6] # Common for transition metals
|
|
410
|
-
else:
|
|
411
|
-
try:
|
|
412
|
-
# Handle comma-separated string
|
|
413
|
-
states_list = [int(s.strip()) for s in states.split(',')]
|
|
414
|
-
except ValueError as e:
|
|
415
|
-
error_response = {
|
|
416
|
-
"success": False,
|
|
417
|
-
"error": f"Invalid states format: '{states}'. Expected comma-separated integers, list, or keywords.",
|
|
418
|
-
"valid_formats": [
|
|
419
|
-
"Comma-separated: '2,4,6' or '1,3,5'",
|
|
420
|
-
"List: [2, 4, 6] or [1, 3, 5]",
|
|
421
|
-
"Keywords: 'all' or 'comprehensive' for [1,2,3,4,5,6]",
|
|
422
|
-
"Keywords: 'common' or 'typical' for [2,4,6]"
|
|
423
|
-
],
|
|
424
|
-
"name": name,
|
|
425
|
-
"molecule": molecule
|
|
426
|
-
}
|
|
427
|
-
return str(error_response)
|
|
428
|
-
elif states is not None and isinstance(states, list):
|
|
429
|
-
try:
|
|
430
|
-
states_list = [int(s) for s in states]
|
|
431
|
-
except (ValueError, TypeError) as e:
|
|
432
|
-
error_response = {
|
|
433
|
-
"success": False,
|
|
434
|
-
"error": f"Invalid states list: {states}. All elements must be integers. Error: {e}",
|
|
435
|
-
"name": name,
|
|
436
|
-
"molecule": molecule
|
|
437
|
-
}
|
|
438
|
-
return str(error_response)
|
|
439
|
-
elif states is not None:
|
|
440
|
-
error_response = {
|
|
441
|
-
"success": False,
|
|
442
|
-
"error": f"Invalid states type: {type(states)}. Expected string or list.",
|
|
443
|
-
"name": name,
|
|
444
|
-
"molecule": molecule
|
|
445
|
-
}
|
|
446
|
-
return str(error_response)
|
|
447
|
-
|
|
448
|
-
# Validate states list
|
|
449
|
-
if not states_list:
|
|
450
|
-
error_response = {
|
|
451
|
-
"success": False,
|
|
452
|
-
"error": "States list cannot be empty. Provide at least one spin multiplicity.",
|
|
453
|
-
"name": name,
|
|
454
|
-
"molecule": molecule
|
|
455
|
-
}
|
|
456
|
-
return str(error_response)
|
|
457
|
-
|
|
458
|
-
# Validate states are positive
|
|
459
|
-
for state in states_list:
|
|
460
|
-
if state <= 0:
|
|
461
|
-
error_response = {
|
|
462
|
-
"success": False,
|
|
463
|
-
"error": f"Invalid spin multiplicity: {state}. Must be positive integer.",
|
|
464
|
-
"name": name,
|
|
465
|
-
"molecule": molecule
|
|
466
|
-
}
|
|
467
|
-
return str(error_response)
|
|
468
|
-
|
|
469
|
-
# Validate electron parity: even electrons need odd multiplicities, odd electrons need even multiplicities
|
|
470
|
-
# This prevents impossible electronic configurations
|
|
471
|
-
try:
|
|
472
|
-
# Try to estimate electron count from SMILES (basic approach)
|
|
473
|
-
# For complex molecules, we'll let Rowan handle the detailed validation
|
|
474
|
-
# But we can catch obvious cases
|
|
475
|
-
|
|
476
|
-
# Check if all states have the same parity (all odd or all even)
|
|
477
|
-
first_state_parity = states_list[0] % 2
|
|
478
|
-
mixed_parity = any((state % 2) != first_state_parity for state in states_list)
|
|
479
|
-
|
|
480
|
-
if mixed_parity:
|
|
481
|
-
error_response = {
|
|
482
|
-
"success": False,
|
|
483
|
-
"error": f"Inconsistent spin state parities: {states_list}. All multiplicities must be either odd (1,3,5...) or even (2,4,6...).",
|
|
484
|
-
"explanation": {
|
|
485
|
-
"electron_parity_rule": "Molecules with even electrons need odd multiplicities; molecules with odd electrons need even multiplicities",
|
|
486
|
-
"your_states": states_list,
|
|
487
|
-
"fix_suggestions": {
|
|
488
|
-
"for_even_electrons": "Use odd multiplicities like [1,3,5]",
|
|
489
|
-
"for_odd_electrons": "Use even multiplicities like [2,4,6]"
|
|
490
|
-
}
|
|
491
|
-
},
|
|
492
|
-
"name": name,
|
|
493
|
-
"molecule": molecule
|
|
494
|
-
}
|
|
495
|
-
return str(error_response)
|
|
496
|
-
except Exception as e:
|
|
497
|
-
# If electron counting fails, let Rowan handle the validation
|
|
498
|
-
logger.warning(f"Could not validate electron parity: {e}")
|
|
499
|
-
pass
|
|
500
|
-
|
|
501
|
-
# Validate mode
|
|
502
|
-
valid_modes = ["rapid", "careful", "meticulous", "auto", "reckless", "manual"]
|
|
503
|
-
if mode.lower() not in valid_modes:
|
|
504
|
-
error_response = {
|
|
505
|
-
"success": False,
|
|
506
|
-
"error": f"Invalid mode: {mode}. Valid modes: {valid_modes}",
|
|
507
|
-
"name": name,
|
|
508
|
-
"molecule": molecule
|
|
509
|
-
}
|
|
510
|
-
return str(error_response)
|
|
511
|
-
|
|
512
|
-
# Create SpinStatesWorkflow instance to validate parameters (stjames pattern)
|
|
513
|
-
try:
|
|
514
|
-
spin_workflow = SpinStatesWorkflow(
|
|
515
|
-
initial_molecule=molecule,
|
|
516
|
-
states=states_list,
|
|
517
|
-
mode=mode,
|
|
518
|
-
solvent=solvent,
|
|
519
|
-
xtb_preopt=xtb_preopt,
|
|
520
|
-
constraints=constraints.split(',') if isinstance(constraints, str) else constraints,
|
|
521
|
-
transition_state=transition_state,
|
|
522
|
-
frequencies=frequencies
|
|
523
|
-
)
|
|
524
|
-
logger.info(f"SpinStatesWorkflow created: {spin_workflow}")
|
|
525
|
-
except ValueError as e:
|
|
526
|
-
error_response = {
|
|
527
|
-
"success": False,
|
|
528
|
-
"error": f"Invalid workflow parameters: {str(e)}",
|
|
529
|
-
"name": name,
|
|
530
|
-
"molecule": molecule,
|
|
531
|
-
"states": states_list,
|
|
532
|
-
"validation_error": str(e)
|
|
533
|
-
}
|
|
534
|
-
return str(error_response)
|
|
535
|
-
|
|
536
|
-
logger.info(f"Starting spin states calculation: {name}")
|
|
537
|
-
logger.info(f"Molecule SMILES: {molecule}")
|
|
538
|
-
logger.info(f"Spin multiplicities: {states_list}")
|
|
539
|
-
logger.info(f"Mode: {mode}")
|
|
540
|
-
if solvent:
|
|
541
|
-
logger.info(f"Solvent: {solvent}")
|
|
542
|
-
if xtb_preopt:
|
|
543
|
-
logger.info("xTB pre-optimization: enabled")
|
|
544
|
-
if frequencies:
|
|
545
|
-
logger.info("Frequency calculation: enabled")
|
|
546
|
-
if transition_state:
|
|
547
|
-
logger.info("Transition state optimization: enabled")
|
|
548
|
-
|
|
549
|
-
# For spin states, we need to provide an initial multiplicity
|
|
550
|
-
# Use the first state in the list as the starting multiplicity
|
|
551
|
-
initial_multiplicity = states_list[0]
|
|
552
|
-
|
|
553
|
-
# CRITICAL FIX: Ensure initial_multiplicity follows electron parity rules
|
|
554
|
-
# Rowan uses initial_multiplicity as the starting point, so it must be valid
|
|
555
|
-
logger.info(f"Using initial_multiplicity = {initial_multiplicity} for states {states_list}")
|
|
556
|
-
|
|
557
|
-
# Prepare workflow parameters - rowan.compute() wants core params separate from workflow_data
|
|
558
|
-
workflow_params = {
|
|
559
|
-
# Core rowan.compute() parameters
|
|
560
|
-
"molecule": molecule, # SMILES string
|
|
561
|
-
"workflow_type": "spin_states",
|
|
562
|
-
"name": name,
|
|
563
|
-
"blocking": blocking,
|
|
564
|
-
"ping_interval": ping_interval,
|
|
565
|
-
|
|
566
|
-
# CRITICAL: Try multiple parameter names for initial multiplicity
|
|
567
|
-
"multiplicity": initial_multiplicity, # Most likely parameter name
|
|
568
|
-
"initial_multiplicity": initial_multiplicity, # Alternative name
|
|
569
|
-
"starting_multiplicity": initial_multiplicity, # Another alternative
|
|
570
|
-
"states": states_list, # List of spin multiplicities to calculate
|
|
571
|
-
"mode": mode.lower(), # Calculation mode
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
# Add optional parameters only if provided
|
|
575
|
-
if folder_uuid:
|
|
576
|
-
workflow_params["folder_uuid"] = folder_uuid
|
|
577
|
-
if solvent:
|
|
578
|
-
workflow_params["solvent"] = solvent
|
|
579
|
-
if xtb_preopt:
|
|
580
|
-
workflow_params["xtb_preopt"] = xtb_preopt
|
|
581
|
-
if transition_state:
|
|
582
|
-
workflow_params["transition_state"] = transition_state
|
|
583
|
-
if frequencies:
|
|
584
|
-
workflow_params["frequencies"] = frequencies
|
|
585
|
-
|
|
586
|
-
# Only add constraints if provided and ensure it's a list
|
|
587
|
-
if constraints is not None:
|
|
588
|
-
if isinstance(constraints, str):
|
|
589
|
-
# Convert string to list (basic parsing)
|
|
590
|
-
workflow_params["constraints"] = [constraints]
|
|
591
|
-
elif isinstance(constraints, list):
|
|
592
|
-
workflow_params["constraints"] = constraints
|
|
593
|
-
else:
|
|
594
|
-
workflow_params["constraints"] = [str(constraints)]
|
|
595
|
-
|
|
596
|
-
logger.info("Submitting spin states calculation to Rowan")
|
|
597
|
-
|
|
598
|
-
try:
|
|
599
|
-
# CRITICAL FIX: Create Molecule object with correct multiplicity instead of string
|
|
600
|
-
# This ensures Rowan uses the right starting multiplicity
|
|
601
|
-
try:
|
|
602
|
-
from stjames.molecule import Molecule as SjamesMolecule
|
|
603
|
-
from rdkit import Chem
|
|
604
|
-
|
|
605
|
-
# Create molecule with proper multiplicity by constructing atoms directly
|
|
606
|
-
# Parse SMILES to get atomic numbers and coordinates
|
|
607
|
-
rdkit_mol = Chem.MolFromSmiles(molecule)
|
|
608
|
-
if rdkit_mol is None:
|
|
609
|
-
raise ValueError(f"Invalid SMILES: {molecule}")
|
|
610
|
-
|
|
611
|
-
# Get atoms from RDKit molecule and generate realistic 3D coordinates
|
|
612
|
-
atoms = []
|
|
613
|
-
from stjames.molecule import Atom
|
|
614
|
-
|
|
615
|
-
# Generate realistic 3D coordinates using specialized coordination chemistry approach
|
|
616
|
-
coords_3d = _generate_3d_coordinates_for_coordination_complex(molecule, rdkit_mol)
|
|
617
|
-
|
|
618
|
-
for i, atom in enumerate(rdkit_mol.GetAtoms()):
|
|
619
|
-
# Create Atom objects with atomic number and realistic 3D positions
|
|
620
|
-
atoms.append(Atom(
|
|
621
|
-
atomic_number=atom.GetAtomicNum(),
|
|
622
|
-
position=coords_3d[i]
|
|
623
|
-
))
|
|
624
|
-
|
|
625
|
-
# Calculate total charge from SMILES string
|
|
626
|
-
total_charge = _calculate_molecular_charge(molecule)
|
|
627
|
-
|
|
628
|
-
logger.info(f"Calculated molecular charge: {total_charge} (from SMILES: {molecule})")
|
|
629
|
-
|
|
630
|
-
# Create Molecule object directly with correct multiplicity and charge
|
|
631
|
-
molecule_obj = SjamesMolecule(
|
|
632
|
-
atoms=atoms,
|
|
633
|
-
charge=total_charge, # Calculate proper charge from SMILES
|
|
634
|
-
multiplicity=initial_multiplicity, # This is the key fix!
|
|
635
|
-
smiles=molecule # Keep original SMILES
|
|
636
|
-
)
|
|
637
|
-
workflow_params["molecule"] = molecule_obj
|
|
638
|
-
logger.info(f"Created stjames.Molecule object with multiplicity={initial_multiplicity}")
|
|
639
|
-
except (ImportError, Exception) as e:
|
|
640
|
-
# If stjames not available, use string but log the limitation
|
|
641
|
-
logger.warning(f"stjames.molecule.Molecule creation failed: {e}")
|
|
642
|
-
logger.warning(f"Rowan may default to multiplicity=1 instead of {initial_multiplicity}")
|
|
643
|
-
|
|
644
|
-
# Submit spin states calculation to Rowan
|
|
645
|
-
result = rowan.compute(**workflow_params)
|
|
646
|
-
|
|
647
|
-
# Format the response based on blocking mode
|
|
648
|
-
if result:
|
|
649
|
-
workflow_uuid = result.get("uuid")
|
|
650
|
-
status = result.get("object_status", 0)
|
|
651
|
-
|
|
652
|
-
if blocking and status == 2: # Completed
|
|
653
|
-
# Extract spin states results for completed blocking calls
|
|
654
|
-
object_data = result.get("object_data", {})
|
|
655
|
-
if "spin_states" in object_data:
|
|
656
|
-
spin_states_data = object_data["spin_states"]
|
|
657
|
-
|
|
658
|
-
# Find ground state (lowest energy)
|
|
659
|
-
ground_state = None
|
|
660
|
-
min_energy = float('inf')
|
|
661
|
-
for state_data in spin_states_data:
|
|
662
|
-
energy = state_data.get("energy", float('inf'))
|
|
663
|
-
if energy < min_energy:
|
|
664
|
-
min_energy = energy
|
|
665
|
-
ground_state = state_data.get("multiplicity")
|
|
666
|
-
|
|
667
|
-
response = {
|
|
668
|
-
"success": True,
|
|
669
|
-
"workflow_uuid": workflow_uuid,
|
|
670
|
-
"name": name,
|
|
671
|
-
"molecule": molecule,
|
|
672
|
-
"status": "completed",
|
|
673
|
-
"spin_states_results": {
|
|
674
|
-
"ground_state_multiplicity": ground_state,
|
|
675
|
-
"ground_state_energy": min_energy,
|
|
676
|
-
"states_calculated": len(spin_states_data),
|
|
677
|
-
"spin_states": spin_states_data
|
|
678
|
-
},
|
|
679
|
-
"calculation_details": {
|
|
680
|
-
"mode": mode,
|
|
681
|
-
"solvent": solvent,
|
|
682
|
-
"xtb_preopt": xtb_preopt,
|
|
683
|
-
"frequencies_calculated": frequencies,
|
|
684
|
-
"transition_state": transition_state
|
|
685
|
-
},
|
|
686
|
-
"runtime_seconds": result.get("elapsed", 0),
|
|
687
|
-
"credits_charged": result.get("credits_charged", 0)
|
|
688
|
-
}
|
|
689
|
-
else:
|
|
690
|
-
response = {
|
|
691
|
-
"success": True,
|
|
692
|
-
"workflow_uuid": workflow_uuid,
|
|
693
|
-
"name": name,
|
|
694
|
-
"molecule": molecule,
|
|
695
|
-
"status": "completed",
|
|
696
|
-
"message": "Spin states calculation completed successfully",
|
|
697
|
-
"runtime_seconds": result.get("elapsed", 0),
|
|
698
|
-
"credits_charged": result.get("credits_charged", 0)
|
|
699
|
-
}
|
|
700
|
-
else:
|
|
701
|
-
# Non-blocking or still running - return workflow info for tracking
|
|
702
|
-
status_text = {0: "queued", 1: "running", 2: "completed", 3: "failed"}.get(status, "unknown")
|
|
703
|
-
response = {
|
|
704
|
-
"success": True,
|
|
705
|
-
"tracking_id": workflow_uuid, # Prominent tracking ID
|
|
706
|
-
"workflow_uuid": workflow_uuid, # Keep for backward compatibility
|
|
707
|
-
"name": name,
|
|
708
|
-
"molecule": molecule,
|
|
709
|
-
"status": status_text,
|
|
710
|
-
"message": f"Spin states calculation submitted successfully! Use tracking_id to monitor progress.",
|
|
711
|
-
"calculation_details": {
|
|
712
|
-
"spin_multiplicities": states_list,
|
|
713
|
-
"mode": mode,
|
|
714
|
-
"solvent": solvent,
|
|
715
|
-
"xtb_preopt": xtb_preopt,
|
|
716
|
-
"frequencies": frequencies,
|
|
717
|
-
"transition_state": transition_state,
|
|
718
|
-
"blocking_mode": blocking
|
|
719
|
-
},
|
|
720
|
-
"progress_tracking": {
|
|
721
|
-
"tracking_id": workflow_uuid,
|
|
722
|
-
"check_status": f"rowan_workflow_management(action='status', workflow_uuid='{workflow_uuid}')",
|
|
723
|
-
"get_results": f"rowan_workflow_management(action='retrieve', workflow_uuid='{workflow_uuid}')"
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
else:
|
|
727
|
-
response = {
|
|
728
|
-
"success": False,
|
|
729
|
-
"error": "No response received from Rowan API",
|
|
730
|
-
"name": name,
|
|
731
|
-
"molecule": molecule,
|
|
732
|
-
"states": states_list
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
return str(response)
|
|
736
|
-
|
|
737
|
-
except Exception as e:
|
|
738
|
-
error_response = {
|
|
739
|
-
"success": False,
|
|
740
|
-
"error": f"Spin states calculation failed: {str(e)}",
|
|
741
|
-
"name": name,
|
|
742
|
-
"molecule": molecule,
|
|
743
|
-
"states": states_list
|
|
744
|
-
}
|
|
745
|
-
logger.error(f"Spin states calculation failed: {str(e)}")
|
|
746
|
-
return str(error_response)
|
|
747
|
-
|