-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsimulation.py
215 lines (178 loc) · 8.21 KB
/
simulation.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
from autode import Reaction, Molecule
from autode.config import Config
from autode.wrappers.XTB import XTB
import os
import subprocess
import sys
def check_xtb_availability():
"""Check if XTB is available in the system"""
try:
result = subprocess.run(['which', 'xtb'], capture_output=True, text=True)
if result.returncode == 0:
return result.stdout.strip()
return None
except Exception:
return None
def validate_molecule(mol):
"""Validate molecule structure"""
if not mol.atoms or len(mol.atoms) == 0:
raise ValueError("No atoms found in molecule")
print(f"\nMolecule validation for {mol.name}:")
print(f"Number of atoms: {len(mol.atoms)}")
print(f"Charge: {mol.charge}")
print(f"Multiplicity: {mol.mult}")
# Check for obviously wrong geometries
for atom in mol.atoms:
if not hasattr(atom, 'coord') or atom.coord is None:
raise ValueError(f"Missing coordinates for atom {atom}")
def validate_optimization(mol, method):
"""Validate that a molecule has been properly optimized"""
if not hasattr(mol, 'energy') or mol.energy is None:
print(f"Attempting to re-optimize {mol.name}...")
try:
mol.optimise(method=method)
if mol.energy is None:
raise ValueError(f"Failed to optimize {mol.name}: Energy is None after optimization")
print(f"Successfully optimized {mol.name}. Energy: {mol.energy}")
except Exception as e:
raise ValueError(f"Failed to optimize {mol.name}: {str(e)}")
def simulate_reaction(reactant_smiles, product_smiles, name="reaction"):
"""
Simulate a chemical reaction from SMILES strings and save xyz files of the path
"""
# Check XTB availability
xtb_path = check_xtb_availability()
if not xtb_path:
raise RuntimeError("XTB executable not found in system path. Please install XTB and make sure it's in your PATH.")
# Configure autodE with more detailed settings
Config.G16.available = False
Config.ORCA.available = False
Config.XTB.available = True
Config.XTB.path = xtb_path
# More detailed configuration
Config.num_conformers = 1
Config.rmsd_threshold = 0.3
Config.max_atom_displacement = 1.0
Config.n_cores = 1 # Specify number of cores explicitly
print(f"Using XTB from: {Config.XTB.path}")
try:
# Create molecule objects with explicit charge and multiplicity
reactant = Molecule(smiles=reactant_smiles[0], name='reactant', charge=0, mult=1)
product = Molecule(smiles=product_smiles[0], name='product', charge=0, mult=1)
# Validate structures
validate_molecule(reactant)
validate_molecule(product)
# Create XTB method instance with specific settings
xtb = XTB()
# Generate and optimize conformers with better error handling
print("\nOptimizing reactant...")
reactant.populate_conformers(n_confs=1)
for conf in reactant.conformers:
try:
conf.optimise(method=xtb)
except Exception as e:
print(f"Error optimizing reactant conformer: {str(e)}")
raise
print("\nOptimizing product...")
product.populate_conformers(n_confs=1)
for conf in product.conformers:
try:
conf.optimise(method=xtb)
except Exception as e:
print(f"Error optimizing product conformer: {str(e)}")
raise
# Validate optimization and energy
print("\nValidating optimizations...")
validate_optimization(reactant, xtb)
validate_optimization(product, xtb)
# Store energies before conversion
reactant_energy = reactant.energy
product_energy = product.energy
print(f"\nInitial energies before conversion:")
print(f"Reactant energy: {reactant_energy}")
print(f"Product energy: {product_energy}")
# Convert to Reactant and Product types
reactant = reactant.to_reactant()
product = product.to_product()
# Ensure energies are preserved after conversion
reactant.energy = reactant_energy
product.energy = product_energy
print(f"\nEnergies after conversion:")
print(f"Reactant energy: {reactant.energy}")
print(f"Product energy: {product.energy}")
# Create the reaction object
reaction = Reaction(reactant, product, name=name)
reaction.type = 'unimolecular'
print("\nVerifying geometries before reaction profile...")
if reactant.energy is None:
raise ValueError("Reactant energy lost during conversion")
if product.energy is None:
raise ValueError("Product energy lost during conversion")
print("\nCalculating reaction profile...")
try:
# Set the method before calling locate_transition_state
reaction.method = xtb
# Add more detailed settings for transition state search
Config.ts_template_folder_path = None # Don't use templates
Config.min_step_size = 0.1 # Smaller step size for better convergence
Config.max_step_size = 0.2
# Add verbose output
print("Starting transition state search...")
reaction.locate_transition_state()
print("Transition state search completed successfully")
except Exception as e:
print(f"Detailed error during reaction profile calculation:")
print(f"Error type: {type(e).__name__}")
print(f"Error message: {str(e)}")
if hasattr(reaction, 'ts') and reaction.ts is not None:
print(f"TS energy: {reaction.ts.energy}")
raise
# Save results
os.makedirs('reaction_path', exist_ok=True)
xyz_files = []
if hasattr(reaction, 'reactant'):
r_path = f'reaction_path/{name}_reactants.xyz'
reaction.reactant.save_xyz_file(r_path)
xyz_files.append(r_path)
if reaction.transition_states is not None:
for i, ts in enumerate(reaction.transition_states):
ts_path = f'reaction_path/{name}_ts_{i}.xyz'
ts.save_xyz_file(ts_path)
xyz_files.append(ts_path)
if hasattr(reaction, 'product'):
p_path = f'reaction_path/{name}_products.xyz'
reaction.product.save_xyz_file(p_path)
xyz_files.append(p_path)
return xyz_files
except Exception as e:
print(f"\nError during simulation: {str(e)}", file=sys.stderr)
raise
def validate_energies(reaction):
"""Validate reaction energies at each step"""
print("\nValidating reaction energies:")
print(f"Reactant energy: {reaction.reactant.energy}")
print(f"Product energy: {reaction.product.energy}")
if hasattr(reaction, 'ts') and reaction.ts is not None:
print(f"TS energy: {reaction.ts.energy}")
if reaction.reactant.energy is None or reaction.product.energy is None:
raise ValueError("Missing reactant or product energies")
def check_geometry(mol, stage):
"""Check geometry at different stages of the calculation"""
print(f"\nGeometry check at {stage}:")
if hasattr(mol, 'atoms') and mol.atoms:
print(f"Number of atoms: {len(mol.atoms)}")
print(f"Energy: {mol.energy}")
# Check for any very short or long bonds
for i, atom1 in enumerate(mol.atoms):
for j, atom2 in enumerate(mol.atoms[i+1:], i+1):
dist = ((atom1.coord - atom2.coord)**2).sum()**0.5
if dist < 0.7 or dist > 3.0: # Å
print(f"Warning: Unusual distance {dist:.2f} Å between atoms {i} and {j}")
if __name__ == "__main__":
reactant_smiles = ["CCC(CC=C)C=C"]
product_smiles = ["CC/C=C/CCC=C"]
try:
xyz_files = simulate_reaction(reactant_smiles, product_smiles, "simple_cyclization")
print("Generated XYZ files:", xyz_files)
except Exception as e:
print(f"Simulation failed: {str(e)}")