rowan-mcp 1.0.2__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 +1 -1
  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.2.dist-info → rowan_mcp-2.0.0.dist-info}/METADATA +41 -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 -88
  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.2.dist-info/RECORD +0 -34
  69. {rowan_mcp-1.0.2.dist-info → rowan_mcp-2.0.0.dist-info}/WHEEL +0 -0
  70. {rowan_mcp-1.0.2.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
-