firecode 1.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.
Files changed (59) hide show
  1. firecode/TEST_NOTEBOOK.ipynb +3940 -0
  2. firecode/__init__.py +0 -0
  3. firecode/__main__.py +118 -0
  4. firecode/_gaussian.py +97 -0
  5. firecode/algebra.py +405 -0
  6. firecode/ase_manipulations.py +879 -0
  7. firecode/atropisomer_module.py +516 -0
  8. firecode/automep.py +130 -0
  9. firecode/calculators/__init__.py +29 -0
  10. firecode/calculators/_gaussian.py +98 -0
  11. firecode/calculators/_mopac.py +242 -0
  12. firecode/calculators/_openbabel.py +154 -0
  13. firecode/calculators/_orca.py +129 -0
  14. firecode/calculators/_xtb.py +786 -0
  15. firecode/concurrent_test.py +119 -0
  16. firecode/embedder.py +2590 -0
  17. firecode/embedder_options.py +577 -0
  18. firecode/embeds.py +881 -0
  19. firecode/errors.py +65 -0
  20. firecode/graph_manipulations.py +333 -0
  21. firecode/hypermolecule_class.py +364 -0
  22. firecode/mep_relaxer.py +199 -0
  23. firecode/modify_settings.py +186 -0
  24. firecode/mprof.py +65 -0
  25. firecode/multiembed.py +148 -0
  26. firecode/nci.py +186 -0
  27. firecode/numba_functions.py +260 -0
  28. firecode/operators.py +776 -0
  29. firecode/optimization_methods.py +609 -0
  30. firecode/parameters.py +84 -0
  31. firecode/pka.py +275 -0
  32. firecode/profiler.py +17 -0
  33. firecode/pruning.py +421 -0
  34. firecode/pt.py +32 -0
  35. firecode/quotes.json +6651 -0
  36. firecode/quotes.py +9 -0
  37. firecode/reactive_atoms_classes.py +666 -0
  38. firecode/references.py +11 -0
  39. firecode/rmsd.py +74 -0
  40. firecode/settings.py +75 -0
  41. firecode/solvents.py +126 -0
  42. firecode/tests/C2F2H4.xyz +10 -0
  43. firecode/tests/C2H4.xyz +8 -0
  44. firecode/tests/CH3Cl.xyz +7 -0
  45. firecode/tests/HCOOH.xyz +7 -0
  46. firecode/tests/HCOOOH.xyz +8 -0
  47. firecode/tests/chelotropic.txt +3 -0
  48. firecode/tests/cyclical.txt +3 -0
  49. firecode/tests/dihedral.txt +2 -0
  50. firecode/tests/string.txt +3 -0
  51. firecode/tests/trimolecular.txt +9 -0
  52. firecode/tests.py +151 -0
  53. firecode/torsion_module.py +1035 -0
  54. firecode/utils.py +541 -0
  55. firecode-1.0.0.dist-info/LICENSE +165 -0
  56. firecode-1.0.0.dist-info/METADATA +321 -0
  57. firecode-1.0.0.dist-info/RECORD +59 -0
  58. firecode-1.0.0.dist-info/WHEEL +5 -0
  59. firecode-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,786 @@
1
+ # coding=utf-8
2
+ '''
3
+ FIRECODE: Filtering Refiner and Embedder for Conformationally Dense Ensembles
4
+ Copyright (C) 2021-2024 Nicolò Tampellini
5
+
6
+ SPDX-License-Identifier: LGPL-3.0-or-later
7
+
8
+ This program is free software: you can redistribute it and/or modify
9
+ it under the terms of the GNU Lesser General Public License as published by
10
+ the Free Software Foundation, either version 3 of the License, or
11
+ (at your option) any later version.
12
+
13
+ This program is distributed in the hope that it will be useful,
14
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
15
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
+ GNU Lesser General Public License for more details.
17
+
18
+ You should have received a copy of the GNU Lesser General Public License
19
+ along with this program. If not, see
20
+ https://www.gnu.org/licenses/lgpl-3.0.en.html#license-text.
21
+
22
+ '''
23
+
24
+ import os
25
+ import shutil
26
+ import sys
27
+ from subprocess import DEVNULL, STDOUT, CalledProcessError, check_call
28
+
29
+ import numpy as np
30
+
31
+ from firecode.algebra import norm, norm_of
32
+ from firecode.graph_manipulations import get_sum_graph
33
+ from firecode.utils import clean_directory, read_xyz, write_xyz
34
+ from firecode.calculators.__init__ import NewFolderContext
35
+
36
+
37
+ def xtb_opt(
38
+ coords,
39
+ atomnos,
40
+ constrained_indices=None,
41
+ constrained_distances=None,
42
+ constrained_dihedrals=None,
43
+ constrained_dih_angles=None,
44
+ method='GFN2-xTB',
45
+ maxiter=500,
46
+ solvent=None,
47
+ charge=0,
48
+ title='temp',
49
+ read_output=True,
50
+ procs=4,
51
+ opt=True,
52
+ conv_thr="tight",
53
+ assert_convergence=False,
54
+ constrain_string=None,
55
+ recursive_stepsize=0.3,
56
+ spring_constant=1,
57
+
58
+ debug=False,
59
+ **kwargs,
60
+ ):
61
+ '''
62
+ This function writes an XTB .inp file, runs it with the subprocess
63
+ module and reads its output.
64
+
65
+ coords: array of shape (n,3) with cartesian coordinates for atoms.
66
+
67
+ atomnos: array of atomic numbers for atoms.
68
+
69
+ constrained_indices: array of shape (n,2), with the indices
70
+ of atomic pairs to be constrained.
71
+
72
+ constrained_distances: optional, target distances for the specified
73
+ distance constraints.
74
+
75
+ constrained_dihedrals: quadruplets of atomic indices to constrain.
76
+
77
+ constrained_dih_angles: target dihedral angles for the dihedral constraints.
78
+
79
+ method: string, specifying the theory level to be used.
80
+
81
+ maxiter: maximum number of geometry optimization steps (maxcycle).
82
+
83
+ solvent: solvent to be used in the calculation (ALPB model).
84
+
85
+ charge: charge to be used in the calculation.
86
+
87
+ title: string, used as a file name and job title for the mopac input file.
88
+
89
+ read_output: Whether to read the output file and return anything.
90
+
91
+ procs: number of cores to be used for the calculation.
92
+
93
+ opt: if false, a single point energy calculation is carried.
94
+
95
+ conv_thr: tightness of convergence thresholds. See XTB ReadTheDocs.
96
+
97
+ assert_convergence: wheter to raise an error in case convergence is not
98
+ achieved by xtb.
99
+
100
+ constrain_string: string to be added to the end of the $geom section of
101
+ the input file.
102
+
103
+ recursive_stepsize: magnitude of step in recursive constrained optimizations.
104
+ The smaller, the slower - but potentially safer against scrambling.
105
+
106
+ spring_constant: stiffness of harmonic distance constraint (Hartrees/Bohrs^2)
107
+
108
+ '''
109
+ # create working folder and cd into it
110
+ with NewFolderContext(title, delete_after=(not debug)):
111
+
112
+ if constrained_indices is not None:
113
+ if len(constrained_indices) == 0:
114
+ constrained_indices = None
115
+
116
+ if constrained_distances is not None:
117
+ if len(constrained_distances) == 0:
118
+ constrained_distances = None
119
+
120
+ # recursive
121
+ if constrained_distances is not None:
122
+
123
+ try:
124
+
125
+ for i, (target_d, ci) in enumerate(zip(constrained_distances, constrained_indices)):
126
+
127
+ if target_d is None:
128
+ continue
129
+
130
+ if len(ci) == 2:
131
+ a, b = ci
132
+ else:
133
+ continue
134
+
135
+ d = norm_of(coords[b] - coords[a])
136
+ delta = d - target_d
137
+
138
+ if abs(delta) > recursive_stepsize:
139
+ recursive_c_d = constrained_distances.copy()
140
+ recursive_c_d[i] = target_d + (recursive_stepsize * np.sign(d-target_d))
141
+ # print(f"--------> d is {round(d, 3)}, target d is {round(target_d, 3)}, delta is {round(delta, 3)}, setting new pretarget at {recursive_c_d}")
142
+ coords, _, _ = xtb_opt(
143
+ coords,
144
+ atomnos,
145
+ constrained_indices,
146
+ constrained_distances=recursive_c_d,
147
+ method=method,
148
+ solvent=solvent,
149
+ charge=charge,
150
+ maxiter=50,
151
+ title=title,
152
+ procs=procs,
153
+ conv_thr='loose',
154
+ constrain_string=constrain_string,
155
+ recursive_stepsize=0.3,
156
+ spring_constant=0.25,
157
+ )
158
+
159
+ d = norm_of(coords[b] - coords[a])
160
+ delta = d - target_d
161
+ coords[b] -= norm(coords[b] - coords[a]) * delta
162
+ # print(f"--------> moved atoms from {round(d, 3)} A to {round(norm_of(coords[b] - coords[a]), 3)} A")
163
+
164
+ except RecursionError:
165
+ with open(f'{title}_crashed.xyz', 'w') as f:
166
+ write_xyz(coords, atomnos, f, title=title)
167
+ print("Recursion limit reached in constrained optimization - Crashed.")
168
+ sys.exit()
169
+
170
+ with open(f'{title}.xyz', 'w') as f:
171
+ write_xyz(coords, atomnos, f, title=title)
172
+
173
+ # outname = f'{title}_xtbopt.xyz' DOES NOT WORK - XTB ISSUE?
174
+ outname = 'xtbopt.xyz'
175
+ trajname = f'{title}_opt_log.xyz'
176
+ maxiter = maxiter if maxiter is not None else 0
177
+ s = f'$opt\n logfile={trajname}\n output={outname}\n maxcycle={maxiter}\n'
178
+
179
+ if constrained_indices is not None:
180
+ s += f'\n$constrain\n force constant={spring_constant}\n'
181
+
182
+ for (a, b), distance in zip(constrained_indices, constrained_distances):
183
+
184
+ distance = distance or 'auto'
185
+ s += f" distance: {a+1}, {b+1}, {distance}\n"
186
+
187
+ if constrained_dihedrals is not None:
188
+
189
+ assert len(constrained_dihedrals) == len(constrained_dih_angles)
190
+
191
+ if constrained_indices is None:
192
+ s += '\n$constrain\n'
193
+
194
+ for (a, b, c, d), angle in zip(constrained_dihedrals, constrained_dih_angles):
195
+ s += f" dihedral: {a+1}, {b+1}, {c+1}, {d+1}, {angle}\n"
196
+
197
+ if constrain_string is not None:
198
+ s += '\n$constrain\n'
199
+ s += constrain_string
200
+
201
+ if method.upper() in ('GFN-XTB', 'GFNXTB'):
202
+ s += '\n$gfn\n method=1\n'
203
+
204
+ elif method.upper() in ('GFN2-XTB', 'GFN2XTB'):
205
+ s += '\n$gfn\n method=2\n'
206
+
207
+ s += '\n$end'
208
+
209
+ s = ''.join(s)
210
+ with open(f'{title}.inp', 'w') as f:
211
+ f.write(s)
212
+
213
+ flags = '--norestart'
214
+
215
+ if opt:
216
+ flags += f' --opt {conv_thr}'
217
+ # specify convergence tightness
218
+
219
+ if method in ('GFN-FF', 'GFNFF'):
220
+
221
+ flags += ' --gfnff'
222
+ # declaring the use of FF instead of semiempirical
223
+
224
+ if charge != 0:
225
+ flags += f' --chrg {charge}'
226
+
227
+ if procs is not None:
228
+ flags += f' -P {procs}'
229
+
230
+ if solvent is not None:
231
+
232
+ if solvent == 'methanol':
233
+ flags += ' --gbsa methanol'
234
+
235
+ else:
236
+ flags += f' --alpb {solvent}'
237
+
238
+ elif method.upper() in ('GFN-FF', 'GFNFF'):
239
+ flags += ' --alpb ch2cl2'
240
+ # if using the GFN-FF force field, add CH2Cl2 solvation for increased accuracy
241
+
242
+ try:
243
+ with open(f"{title}.out", "w") as f:
244
+ check_call(f'xtb {title}.xyz --input {title}.inp {flags}'.split(), stdout=f, stderr=STDOUT)
245
+
246
+ # sometimes the SCC does not converge: only raise the error if specified
247
+ except CalledProcessError:
248
+ if assert_convergence:
249
+ raise CalledProcessError
250
+
251
+ except KeyboardInterrupt:
252
+ print('KeyboardInterrupt requested by user. Quitting.')
253
+ sys.exit()
254
+
255
+ if spring_constant > 0.25:
256
+ print()
257
+
258
+ if read_output:
259
+
260
+ if opt:
261
+
262
+ if trajname in os.listdir():
263
+ coords, energy = read_from_xtbtraj(trajname)
264
+
265
+ else:
266
+ energy = None
267
+
268
+ clean_directory((f'{title}.inp', f'{title}.xyz', f"{title}.out", trajname, outname))
269
+
270
+ else:
271
+ energy = energy_grepper(f"{title}.out", 'TOTAL ENERGY', 3)
272
+ # clean_directory((f'{title}.inp', f'{title}.xyz', f"{title}.out", trajname, outname))
273
+
274
+ for filename in ('gfnff_topo',
275
+ 'charges',
276
+ 'wbo',
277
+ 'xtbrestart',
278
+ 'xtbtopo.mol',
279
+ '.xtboptok',
280
+ 'gfnff_adjacency',
281
+ 'gfnff_charges',
282
+ ):
283
+ try:
284
+ os.remove(filename)
285
+ except FileNotFoundError:
286
+ pass
287
+
288
+ return coords, energy, True
289
+
290
+ def xtb_pre_opt(
291
+ coords,
292
+ atomnos,
293
+ graphs,
294
+ constrained_indices=None,
295
+ constrained_distances=None,
296
+ **kwargs,
297
+ ):
298
+ '''
299
+ Wrapper for xtb_opt that preserves the distance of every bond present in each subgraph provided
300
+
301
+ graphs: list of subgraphs that make up coords, in order
302
+
303
+ '''
304
+ sum_graph = get_sum_graph(graphs, extra_edges=constrained_indices)
305
+
306
+ # we have to check through a list this way, as I have not found
307
+ # an analogous way to check through an array for subarrays in a nice way
308
+ list_of_constr_ids = [[a,b] for a, b in constrained_indices] if constrained_indices is not None else []
309
+
310
+ constrain_string = "$constrain\n"
311
+ for constraint in [[a, b] for (a, b) in sum_graph.edges if a!=b]:
312
+
313
+ if constrained_distances is None:
314
+ distance = 'auto'
315
+
316
+ elif constraint in list_of_constr_ids:
317
+ distance = constrained_distances[list_of_constr_ids.index(constraint)]
318
+
319
+ else:
320
+ distance = 'auto'
321
+
322
+ indices_string = str([i+1 for i in constraint]).strip("[").strip("]")
323
+ constrain_string += f" distance: {indices_string}, {distance}\n"
324
+ constrain_string += "\n$end"
325
+
326
+ return xtb_opt(
327
+ coords,
328
+ atomnos,
329
+ constrained_indices=constrained_indices,
330
+ constrained_distances=constrained_distances,
331
+ constrain_string=constrain_string,
332
+ **kwargs,
333
+ )
334
+
335
+ def read_from_xtbtraj(filename):
336
+ '''
337
+ Read coordinates from a .xyz trajfile.
338
+
339
+ '''
340
+ with open(filename, 'r') as f:
341
+ lines = f.readlines()
342
+
343
+ # look for the last line containing the flag (iterate in reverse order)
344
+ # and extract the line at which coordinates start
345
+ first_coord_line = len(lines) - next(line_num for line_num, line in enumerate(reversed(lines)) if 'energy:' in line)
346
+ xyzblock = lines[first_coord_line:]
347
+
348
+ coords = np.array([line.split()[1:] for line in xyzblock], dtype=float)
349
+ energy = float(lines[first_coord_line-1].split()[1]) * 627.5096080305927 # Eh to kcal/mol
350
+
351
+ return coords, energy
352
+
353
+ def energy_grepper(filename, signal_string, position):
354
+ '''
355
+ returns a kcal/mol energy from a Eh energy in a textfile.
356
+ '''
357
+ with open(filename, 'r', encoding='utf-8') as f:
358
+ line = f.readline()
359
+ while True:
360
+ line = f.readline()
361
+ if signal_string in line:
362
+ return float(line.split()[position]) * 627.5096080305927 # Eh to kcal/mol
363
+ if not line:
364
+ raise Exception()
365
+
366
+ def xtb_get_free_energy(coords, atomnos, method='GFN2-xTB', solvent=None,
367
+ charge=0, title='temp', sph=False, **kwargs):
368
+ '''
369
+ Calculates free energy with XTB,
370
+ without optimizing the provided structure.
371
+ '''
372
+
373
+ with open(f'{title}.xyz', 'w') as f:
374
+ write_xyz(coords, atomnos, f, title=title)
375
+
376
+ outname = 'xtbopt.xyz'
377
+ trajname = f'{title}_opt_log.xyz'
378
+ s = f'$opt\n logfile={trajname}\n output={outname}\n maxcycle=1\n'
379
+
380
+
381
+ if method.upper() in ('GFN-XTB', 'GFNXTB'):
382
+ s += '\n$gfn\n method=1\n'
383
+
384
+ elif method.upper() in ('GFN2-XTB', 'GFN2XTB'):
385
+ s += '\n$gfn\n method=2\n'
386
+
387
+ s += '\n$end'
388
+
389
+ s = ''.join(s)
390
+ with open(f'{title}.inp', 'w') as f:
391
+ f.write(s)
392
+
393
+ if sph:
394
+ flags = '--bhess'
395
+ else:
396
+ flags = '--ohess'
397
+
398
+ if method in ('GFN-FF', 'GFNFF'):
399
+ flags += ' --gfnff'
400
+ # declaring the use of FF instead of semiempirical
401
+
402
+ if charge != 0:
403
+ flags += f' --chrg {charge}'
404
+
405
+ if solvent is not None:
406
+
407
+ if solvent == 'methanol':
408
+ flags += ' --gbsa methanol'
409
+
410
+ else:
411
+ flags += f' --alpb {solvent}'
412
+
413
+ try:
414
+ with open('temp_hess.log', 'w') as outfile:
415
+ check_call(f'xtb --input {title}.inp {title}.xyz {flags}'.split(), stdout=outfile, stderr=STDOUT)
416
+
417
+ except KeyboardInterrupt:
418
+ print('KeyboardInterrupt requested by user. Quitting.')
419
+ sys.exit()
420
+
421
+ try:
422
+ free_energy = read_xtb_free_energy('temp_hess.log')
423
+
424
+ clean_directory()
425
+ for filename in ('gfnff_topo', 'charges', 'wbo', 'xtbrestart', 'xtbtopo.mol', '.xtboptok',
426
+ 'hessian', 'g98.out', 'vibspectrum', 'wbo', 'xtbhess.xyz', 'charges', 'temp_hess.log'):
427
+ try:
428
+ os.remove(filename)
429
+ except FileNotFoundError:
430
+ pass
431
+
432
+ return free_energy
433
+
434
+ except FileNotFoundError:
435
+ # return 1E10
436
+ # print(f'temp_hess.log not present here - we are in', os.getcwd())
437
+ print(os.listdir())
438
+ sys.exit()
439
+
440
+ def read_xtb_free_energy(filename):
441
+ '''
442
+ returns free energy in kcal/mol from an XTB
443
+ .xyz result file (xtbopt.xyz)
444
+ '''
445
+ with open(filename, 'r') as f:
446
+ line = f.readline()
447
+ while True:
448
+ line = f.readline()
449
+ if 'TOTAL FREE ENERGY' in line:
450
+ return float(line.split()[4]) * 627.5096080305927 # Eh to kcal/mol
451
+ if not line:
452
+ raise Exception()
453
+
454
+ def xtb_metadyn_augmentation(coords, atomnos, constrained_indices=None, new_structures:int=5, title=0, debug=False):
455
+ '''
456
+ Runs a metadynamics simulation (MTD) through
457
+ the XTB program to obtain new conformations.
458
+ The GFN-FF force field is used.
459
+ '''
460
+ with open('temp.xyz', 'w') as f:
461
+ write_xyz(coords, atomnos, f, title='temp')
462
+
463
+ s = (
464
+ '$md\n'
465
+ ' time=%s\n' % (new_structures) +
466
+ ' step=1\n'
467
+ ' temp=300\n'
468
+ '$end\n'
469
+ '$metadyn\n'
470
+ ' save=%s\n' % (new_structures) +
471
+ '$end'
472
+ )
473
+
474
+ if constrained_indices is not None:
475
+ s += '\n$constrain\n'
476
+ for a, b in constrained_indices:
477
+ s += ' distance: %s, %s, %s\n' % (a+1, b+1, round(norm_of(coords[a]-coords[b]), 5))
478
+
479
+ s = ''.join(s)
480
+ with open('temp.inp', 'w') as f:
481
+ f.write(s)
482
+
483
+ try:
484
+ check_call(f'xtb --md --input temp.inp temp.xyz --gfnff > Structure{title}_MTD.log 2>&1'.split(), stdout=DEVNULL, stderr=STDOUT)
485
+
486
+ except KeyboardInterrupt:
487
+ print('KeyboardInterrupt requested by user. Quitting.')
488
+ sys.exit()
489
+
490
+ structures = [coords]
491
+ for n in range(1,new_structures):
492
+ name = 'scoord.'+str(n)
493
+ structures.append(parse_xtb_out(name))
494
+ os.remove(name)
495
+
496
+ for filename in ('gfnff_topo', 'xtbmdoc', 'mdrestart'):
497
+ try:
498
+ os.remove(filename)
499
+ except FileNotFoundError:
500
+ pass
501
+
502
+ # if debug:
503
+ os.rename('xtb.trj', f'Structure{title}_MTD_traj.xyz')
504
+
505
+ # else:
506
+ # os.remove('xtb.traj')
507
+
508
+ structures = np.array(structures)
509
+
510
+ return structures
511
+
512
+ def parse_xtb_out(filename):
513
+ '''
514
+ '''
515
+ with open(filename, 'r') as f:
516
+ lines = f.readlines()
517
+
518
+ coords = np.zeros((len(lines)-3,3))
519
+
520
+ for _l, line in enumerate(lines[1:-2]):
521
+ coords[_l] = line.split()[:-1]
522
+
523
+ return coords * 0.529177249 # Bohrs to Angstroms
524
+
525
+ def crest_mtd_search(
526
+ coords,
527
+ atomnos,
528
+ constrained_indices=None,
529
+ constrained_distances=None,
530
+ constrained_dihedrals=None,
531
+ constrained_dih_angles=None,
532
+ method='GFN2-XTB//GFN-FF',
533
+ solvent='CH2Cl2',
534
+ charge=0,
535
+ kcal=None,
536
+ ncimode=False,
537
+ title='temp',
538
+ procs=4,
539
+ threads=1,
540
+ ):
541
+ '''
542
+ This function runs a crest metadynamic conformational search and
543
+ returns its output.
544
+
545
+ coords: array of shape (n,3) with cartesian coordinates for atoms.
546
+
547
+ atomnos: array of atomic numbers for atoms.
548
+
549
+ constrained_indices: array of shape (n,2), with the indices
550
+ of atomic pairs to be constrained.
551
+
552
+ constrained_distances: optional, target distances for the specified
553
+ distance constraints.
554
+
555
+ constrained_dihedrals: quadruplets of atomic indices to constrain.
556
+
557
+ constrained_dih_angles: target dihedral angles for the dihedral constraints.
558
+
559
+ method: string, specifying the theory level to be used.
560
+
561
+ solvent: solvent to be used in the calculation (ALPB model).
562
+
563
+ charge: charge to be used in the calculation.
564
+
565
+ title: string, used as a file name and job title for the mopac input file.
566
+
567
+ procs: number of cores to be used for the calculation.
568
+
569
+ threads: number of parallel threads to be used by the process.
570
+
571
+ '''
572
+
573
+ # Remove title directory, if already present
574
+ if title in os.listdir():
575
+ shutil.rmtree(os.path.join(os.getcwd(), title))
576
+
577
+ # make title directory and cd into it
578
+ os.mkdir(title)
579
+ os.chdir(os.path.join(os.getcwd(), title))
580
+
581
+ if constrained_indices is not None:
582
+ if len(constrained_indices) == 0:
583
+ constrained_indices = None
584
+
585
+ if constrained_distances is not None:
586
+ if len(constrained_distances) == 0:
587
+ constrained_distances = None
588
+
589
+ with open(f'{title}.xyz', 'w') as f:
590
+ write_xyz(coords, atomnos, f, title=title)
591
+
592
+ s = '$opt\n '
593
+
594
+ if constrained_indices is not None:
595
+ s += '\n$constrain\n'
596
+ # s += ' atoms: '
597
+ # for i in np.unique(np.array(constrained_indices).flatten()):
598
+ # s += f"{i+1},"
599
+
600
+ for (c1, c2), cd in zip(constrained_indices, constrained_distances):
601
+ cd = "auto" if cd is None else cd
602
+ s += f" distance: {c1+1}, {c2+1}, {cd}\n"
603
+
604
+ if constrained_dihedrals is not None:
605
+ assert len(constrained_dihedrals) == len(constrained_dih_angles)
606
+ s += '\n$constrain\n' if constrained_indices is None else ''
607
+ for (a, b, c, d), angle in zip(constrained_dihedrals, constrained_dih_angles):
608
+ s += f" dihedral: {a+1}, {b+1}, {c+1}, {d+1}, {angle}\n"
609
+
610
+ s += "\n$metadyn\n atoms: "
611
+
612
+ constrained_atoms_cumulative = set()
613
+ if constrained_indices is not None:
614
+ for c1, c2 in constrained_indices:
615
+ constrained_atoms_cumulative.add(c1)
616
+ constrained_atoms_cumulative.add(c2)
617
+
618
+ if constrained_dihedrals is not None:
619
+ for c1, c2, c3, c4 in constrained_dihedrals:
620
+ constrained_atoms_cumulative.add(c1)
621
+ constrained_atoms_cumulative.add(c2)
622
+ constrained_atoms_cumulative.add(c3)
623
+ constrained_atoms_cumulative.add(c4)
624
+
625
+ # write atoms that need to be moved during metadynamics (all but constrained)
626
+ active_ids = np.array([i+1 for i, _ in enumerate(atomnos) if i not in constrained_atoms_cumulative])
627
+
628
+ while len(active_ids) > 2:
629
+ i = next((i for i, _ in enumerate(active_ids[:-2]) if active_ids[i+1]-active_ids[i]>1), len(active_ids)-1)
630
+ if active_ids[0] == active_ids[i]:
631
+ s += f"{active_ids[0]},"
632
+ else:
633
+ s += f"{active_ids[0]}-{active_ids[i]},"
634
+ active_ids = active_ids[i+1:]
635
+
636
+ # remove final comma
637
+ s = s[:-1]
638
+ s += '\n$end'
639
+
640
+ s = ''.join(s)
641
+ with open(f'{title}.inp', 'w') as f:
642
+ f.write(s)
643
+
644
+ # avoid restarting the run
645
+ flags = '--norestart'
646
+
647
+ # add method flag
648
+ if method.upper() in ('GFN-FF', 'GFNFF'):
649
+ flags += ' --gfnff'
650
+ # declaring the use of FF instead of semiempirical
651
+
652
+ elif method.upper() in ('GFN2-XTB', 'GFN2'):
653
+ flags += ' --gfn2'
654
+
655
+ elif method.upper() in ('GFN2-XTB//GFN-FF', 'GFN2//GFNFF'):
656
+ flags += ' --gfn2//gfnff'
657
+
658
+ # adding other options
659
+ if charge != 0:
660
+ flags += f' --chrg {charge}'
661
+
662
+ if procs is not None:
663
+ flags += f' -P {procs}'
664
+
665
+ if threads is not None:
666
+ flags += f' -T {threads}'
667
+
668
+ if solvent is not None:
669
+
670
+ if solvent == 'methanol':
671
+ flags += ' --gbsa methanol'
672
+
673
+ else:
674
+ flags += f' --alpb {solvent}'
675
+
676
+ if kcal is None:
677
+ kcal = 10
678
+ flags += f' --ewin {kcal}'
679
+
680
+ if ncimode:
681
+ flags += ' --nci'
682
+
683
+ flags += ' --noreftopo'
684
+
685
+ try:
686
+ with open(f"{title}.out", "w") as f:
687
+ check_call(f'crest {title}.xyz --cinp {title}.inp {flags}'.split(), stdout=f, stderr=STDOUT)
688
+
689
+ except KeyboardInterrupt:
690
+ print('KeyboardInterrupt requested by user. Quitting.')
691
+ sys.exit()
692
+
693
+ # if CREST crashes, cd into the parent folder before propagating the error
694
+ except CalledProcessError:
695
+ os.chdir(os.path.dirname(os.getcwd()))
696
+ raise CalledProcessError
697
+
698
+ new_coords = read_xyz('crest_conformers.xyz').atomcoords
699
+
700
+ # clean_directory((f'{title}.inp', f'{title}.xyz', f"{title}.out"))
701
+
702
+ for filename in ('gfnff_topo',
703
+ 'charges',
704
+ 'wbo',
705
+ 'xtbrestart',
706
+ 'xtbtopo.mol',
707
+ '.xtboptok',
708
+ 'gfnff_adjacency',
709
+ 'gfnff_charges',
710
+ ):
711
+ try:
712
+ os.remove(filename)
713
+ except FileNotFoundError:
714
+ pass
715
+
716
+ os.chdir(os.path.dirname(os.getcwd()))
717
+ # shutil.rmtree(os.path.join(os.getcwd(), title))
718
+
719
+ return new_coords
720
+
721
+ def xtb_gsolv(coords, atomnos, model='alpb', charge=0, solvent='ch2cl2', title='temp', assert_convergence=True):
722
+ '''
723
+ Returns the solvation free energy in kcal/mol, as computed by XTB.
724
+ Single-point energy calculation carried out with GFN-FF.
725
+
726
+ '''
727
+
728
+ # create working folder and cd into it
729
+ if title in os.listdir():
730
+ shutil.rmtree(os.path.join(os.getcwd(), title))
731
+
732
+ os.mkdir(title)
733
+ os.chdir(os.path.join(os.getcwd(), title))
734
+
735
+ with open(f'{title}.xyz', 'w') as f:
736
+ write_xyz(coords, atomnos, f, title=title)
737
+
738
+ # outname = f'{title}_xtbopt.xyz' DOES NOT WORK - XTB ISSUE?
739
+ outname = 'xtbopt.xyz'
740
+ flags = '--norestart'
741
+
742
+ # declaring the use of FF instead of semiempirical
743
+ flags += ' --gfnff'
744
+
745
+ if charge != 0:
746
+ flags += f' --chrg {charge}'
747
+
748
+ flags += f' --{model} {solvent}'
749
+
750
+ try:
751
+ with open(f"{title}.out", "w") as f:
752
+ check_call(f'xtb {title}.xyz {flags}'.split(), stdout=f, stderr=STDOUT)
753
+
754
+ # sometimes the SCC does not converge: only raise the error if specified
755
+ except CalledProcessError:
756
+ if assert_convergence:
757
+ raise CalledProcessError
758
+
759
+ except KeyboardInterrupt:
760
+ print('KeyboardInterrupt requested by user. Quitting.')
761
+ sys.exit()
762
+
763
+
764
+ else:
765
+ gsolv = energy_grepper(f"{title}.out", '-> Gsolv', 3)
766
+ clean_directory((f'{title}.inp', f'{title}.xyz', f"{title}.out", outname))
767
+
768
+ for filename in ('gfnff_topo',
769
+ 'charges',
770
+ 'wbo',
771
+ 'xtbrestart',
772
+ 'xtbtopo.mol',
773
+ '.xtboptok',
774
+ 'gfnff_adjacency',
775
+ 'gfnff_charges',
776
+ ):
777
+ try:
778
+ os.remove(filename)
779
+ except FileNotFoundError:
780
+ pass
781
+
782
+ # get out of working folder and delete it
783
+ os.chdir(os.path.dirname(os.getcwd()))
784
+ shutil.rmtree(os.path.join(os.getcwd(), title))
785
+
786
+ return gsolv