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,577 @@
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
+ from firecode.settings import (CALCULATOR, DEFAULT_FF_LEVELS, FF_CALC,
24
+ FF_OPT_BOOL)
25
+
26
+ # Known keywords and relative priority level:
27
+ # 1 : First to run, set some option
28
+ # 2 : Second to run, modify variables, dependence on priority 1 options
29
+ # 3 : Third to run, modify variables, dependence on priority 2 options
30
+
31
+ keywords_dict = {
32
+ 'BYPASS' : 1, # Debug keyword. Used to skip all pruning steps and
33
+ # directly output all the embedded geometries.
34
+
35
+ 'CALC' : 1, # Manually overrides the calculator in "settings.py"
36
+
37
+ 'CHARGE' : 1, # Specifies charge for the embedding
38
+
39
+ 'CHECK' : 1, # Visualize the input molecules through the ASE GUI,
40
+ # to check orbital positions or reading faults.
41
+
42
+ 'CONFS' : 1, # Maximum number of conformers generated by csearch
43
+
44
+ 'CLASHES' : 1, # Manually specify the max number of clashes and/or
45
+ # the distance threshold at which two atoms are considered
46
+ # clashing. The more forgiving, the more structures will reach
47
+ # the geometry optimization step. Syntax: `CLASHES(num=3,dist=1.2)`
48
+
49
+ 'CRESTNCI' : 1, # passes the "--nci" flag to CREST metadynamic conformational searches.
50
+
51
+ 'DEEP' : 1, # Performs a deeper search, retaining more starting points
52
+ # for calculations and smaller turning angles.
53
+
54
+ 'DEBUG' : 1, # DEBUG KEYWORD. Writes more stuff to file.
55
+
56
+ 'DIST' : 2, # Manually imposed distance between specified atom pairs,
57
+ # in Angstroms. Syntax uses parenthesis and commas:
58
+ # `DIST(a=2.345,b=3.67,c=2.1)`
59
+
60
+ 'DRYRUN' : 1, # skip any computing step - used to check for runtime errors during setup)
61
+
62
+ # 'ENANTIOMERS', # Do not discard enantiomeric structures.
63
+
64
+ 'EZPROT' : 1, # Double bond protection
65
+
66
+ 'FFOPT' : 1, #Manually turn on ``FF=ON`` or off ``FF=OFF`` the force
67
+ # field optimization step, overriding the value in ``settings.py``.
68
+
69
+ 'FFCALC' : 1, # Manually overrides the force field calculator in "settings.py"
70
+
71
+ 'FFLEVEL' : 1, # Manually set the theory level to be used.
72
+ # . Syntax: `FFLEVEL=UFF
73
+
74
+ 'IMAGES' : 1, # Number of images to be used in NEB and mep_relax> jobs
75
+
76
+ 'KCAL' : 1, # Trim output structures to a given value of relative energy.
77
+ # Syntax: `KCAL=n`, where n can be an integer or float.
78
+
79
+ 'LET' : 1, # Overrides safety checks that prevent the
80
+ # program from running too large calculations
81
+
82
+ 'LEVEL' : 1, # Manually set the theory level to be used.
83
+ # . Syntax: `LEVEL(PM7_EPS=6.15)
84
+
85
+ 'MTD' : 1, # Run conformational augmentation through metadynamic sampling (XTB)
86
+
87
+ 'NCI' : 1, # Estimate and print non-covalent interactions present in the generated poses.
88
+
89
+ 'NEB' : 1, # Perform an automatical climbing image nudged elastic band (CI-NEB)
90
+ # TS search after the partial optimization step, inferring reagents
91
+ # and products for each generated TS pose. These are guessed by
92
+ # approaching the reactive atoms until they are at the right distance,
93
+ # and then partially constrained (reagents) or free (products) optimizations
94
+ # are carried out to get the start and end points for a CI-NEB TS search.
95
+ # For trimolecular transition states, only the first imposed pairing (a)
96
+ # is approached - i.e. the C-C reactive distance in the example above.
97
+ # This NEB option is only really usable for those reactions in which two
98
+ # (or three) molecules are bound together (or strongly interacting) after
99
+ # the TS, with no additional species involved. For example, cycloaddition
100
+ # reactions are great candidates while atom transfer reactions
101
+ # (i.e. epoxidations) are not. Of course this implementation is not
102
+ # always reliable, and it is provided more as an experimenting tool
103
+ # than a definitive feature.
104
+
105
+ 'NEWBONDS' : 1, # Manually specify the maximum number of "new bonds" that a
106
+ # TS structure can have to be retained and not to be considered
107
+ # scrambled. Default is 1. Syntax: `NEWBONDS=1`
108
+
109
+ 'NOOPT' : 1, # Skip the optimization steps, directly writing structures to file.
110
+
111
+ 'ONLYREFINED' : 1, # Discard structures that do not successfully refine bonding distances.
112
+
113
+ 'PKA' : 1, # Set reference pKa for a specific compound
114
+
115
+ 'PROCS' : 1, # Set the number of parallel cores to be used by ORCA
116
+
117
+ 'REFINE' : 1, # Same as calling refine> on a single file
118
+
119
+ 'RIGID' : 1, # Does not apply to "string" embeds. Avoid
120
+ # bending structures to better build TSs.
121
+
122
+ 'ROTRANGE' : 1, # Does not apply to "string" embeds. Manually specify the rotation
123
+ # range to be explored around the structure pivot.
124
+ # Default is 120. Syntax: `ROTRANGE=120`
125
+
126
+ 'SADDLE' : 1, # After embed and refinement, optimize to first order saddle points
127
+
128
+ 'SHRINK' : 1, # Exaggerate orbital dimensions during embed, scaling them by a factor
129
+ # of one and a half. This makes it easier to perform the embed without
130
+ # having molecules clashing one another. Then, the correct distance between
131
+ # reactive atom pairs is achieved as for standard runs by spring constraints
132
+ # during MOPAC/ORCA optimization.
133
+
134
+ 'SIMPLEORBITALS' : 1, # Override the automatic orbital assignment, using "Single" type orbitals for
135
+ # every reactive atom
136
+
137
+ 'SOLVENT' : 1, # set the solvation model
138
+
139
+ 'STEPS' : 1, # Manually specify the number of steps to be taken in scanning rotations.
140
+ # For string embeds, the range to be explored is the full 360°, and the
141
+ # default `STEPS=24` will perform 15° turns. For cyclical and chelotropic
142
+ # embeds, the rotation range to be explored is +-`ROTRANGE` degrees.
143
+ # Therefore, the default value of `ROTRANGE=120 STEPS=12` will perform
144
+ # twelve 20 degrees turns.
145
+
146
+ 'SUPRAFAC' : 1, # Only retain suprafacial orbital configurations in cyclical TSs.
147
+ # Thought for Diels-Alder and other cycloaddition reactions.
148
+
149
+ 'RMSD' : 1, # RMSD threshold (Angstroms) for structure pruning. The smaller,
150
+ # the more retained structures. Default is 0.5 A.
151
+ # Syntax: `RMSD=n`, where n is a number.
152
+
153
+ 'TS' : 1, # Uses various scans/saddle algorithms to locate the TS
154
+ }
155
+
156
+ def get_keyword_suggestion(unknown_kw):
157
+ '''
158
+ Given an unknown keyword, return the known one
159
+ with the highest score, if similar enough,
160
+ else return None.
161
+ '''
162
+
163
+ scores_tuples = [(kw, kw_similarity_score(kw, unknown_kw)) for kw in keywords_dict.keys()]
164
+ best_match, best_score = sorted(scores_tuples, key=lambda x: x[1], reverse=True)[0]
165
+
166
+ if best_score > 0.5:
167
+ return best_match
168
+
169
+ return None
170
+
171
+ def kw_similarity_score(ref, kw):
172
+ '''
173
+ Simple scoring for string similarity
174
+ '''
175
+
176
+ score = 0
177
+ letters = []
178
+ for letter in kw:
179
+ if letter not in letters:
180
+ score += ref.count(letter)
181
+ letters.append(letter)
182
+
183
+ return score/len(ref)
184
+
185
+ class Truthy_struct:
186
+ def __bool__(self):
187
+ return True
188
+
189
+ class Options:
190
+
191
+ def __init__(self):
192
+
193
+ # only used by cyclical embeds, can be set here
194
+ self.rotation_range = 45
195
+
196
+ # Set later by the _setup() function based on embed type,
197
+ # since it is used by both cyclical and string embeds
198
+ self.rotation_steps = None
199
+
200
+ self.rmsd = 0.5
201
+ self.rigid = False
202
+ self.max_confs = 1000
203
+
204
+ self.max_clashes = 0
205
+ self.clash_thresh = 1.5
206
+
207
+ self.max_newbonds = 0
208
+
209
+ self.optimization = True
210
+ self.calculator = CALCULATOR
211
+ self.theory_level = None # set later in _calculator_setup()
212
+ self.solvent = None
213
+ self.charge = 0
214
+ self.ff_opt = FF_OPT_BOOL
215
+ self.ff_calc = FF_CALC
216
+
217
+ if self.ff_opt:
218
+ self.ff_level = DEFAULT_FF_LEVELS[FF_CALC]
219
+
220
+ self.neb = False
221
+ self.saddle = False
222
+ self.ts = False
223
+ self.nci = False
224
+ self.crestnci = False
225
+ self.shrink = False
226
+ self.shrink_multiplier = 1
227
+ self.metadynamics = False
228
+ self.suprafacial = False
229
+ self.simpleorbitals = False
230
+ self.only_refined = False
231
+ # self.keep_enantiomers = False
232
+ self.double_bond_protection = False
233
+ self.keep_hb = False
234
+ self.csearch_aug = False
235
+ self.dryrun = False
236
+ self.checkpoint_frequency = 50
237
+
238
+ self.fix_angles_in_deformation = False
239
+ # Not possible to set manually through a keyword.
240
+ # Monomolecular embeds have it on to prevent
241
+ # scrambling, but better to leave it off for
242
+ # less severe deformations, since convergence
243
+ # is faster
244
+
245
+ self.kcal_thresh = 10
246
+ self.bypass = False
247
+ self.debug = False
248
+ self.let = False
249
+ self.check_structures = False
250
+ self.noembed = False
251
+ # Default values, updated if _parse_input
252
+ # finds keywords and calls _set_options
253
+
254
+ self.operators = []
255
+ # this list will be filled with operator strings
256
+ # that need to be exectured before the run. i.e. ['csearch>mol.xyz']
257
+
258
+ self.operators_dict = {}
259
+ # Analogous dictionary that will contain the seuquences of operators for each molecule
260
+
261
+ def __repr__(self):
262
+ d = {var:self.__getattribute__(var) for var in dir(self) if var[0:2] != '__'}
263
+
264
+ repr_if_true = (
265
+ 'bypass',
266
+ 'check_structures',
267
+ 'csearch_aug',
268
+ 'crestnci',
269
+ 'debug',
270
+ 'let',
271
+ 'metadynamics',
272
+ 'nci',
273
+ 'neb',
274
+ 'saddle',
275
+ 'ts',
276
+ 'ff_opt',
277
+ 'noembed',
278
+ 'keep_hb',
279
+ 'operators',
280
+ 'keep_hb',
281
+ 'dryrun',
282
+ 'shrink',
283
+ 'rigid',
284
+ 'suprafacial',
285
+ 'simpleorbitals',
286
+ 'fix_angles_in_deformation',
287
+ 'double_bond_protection',
288
+ )
289
+
290
+ for name in repr_if_true:
291
+ if not d.get(name, True):
292
+ d.pop(name)
293
+
294
+ repr_if_not_none = (
295
+ 'kcal_thresh',
296
+ 'solvent',
297
+ )
298
+
299
+ for name in repr_if_not_none:
300
+ if d[name] is None:
301
+ d.pop(name)
302
+
303
+ if not FF_OPT_BOOL:
304
+ d.pop('ff_calc')
305
+
306
+ padding = 1 + max([len(var) for var in d])
307
+
308
+ return '\n'.join([f'{var}{" "*(padding-len(var))}: {d[var]}' for var in d])
309
+
310
+ class OptionSetter:
311
+
312
+ def __init__(self, embedder, *args):
313
+
314
+ embedder.kw_line = embedder.kw_line if hasattr(embedder, 'kw_line') else ''
315
+
316
+ self.keywords = [word.split('=')[0].upper() if '(' not in word
317
+ else word.split('(')[0].upper()
318
+ for word in embedder.kw_line.split()]
319
+
320
+ self.keywords_simple = [k.upper() for k in embedder.kw_line.split()]
321
+ self.keywords_simple_case_sensitive = embedder.kw_line.split()
322
+ self.embedder = embedder
323
+ self.args = args
324
+
325
+ # if not all(k in keywords_dict.keys() for k in self.keywords):
326
+ for k in self.keywords:
327
+ if k not in keywords_dict.keys():
328
+ guess = get_keyword_suggestion(k)
329
+ extra = '' if guess is None else f' Did you mean \"{guess}\"?'
330
+ raise SyntaxError(f'Keyword \"{k}\" was not understood. Please check your syntax.{extra}')
331
+
332
+ if self.keywords_simple:
333
+ embedder.log('\n--> Parsed keywords, in order of execution:\n ' + ' '.join(self.sorted_keywords()) + '\n')
334
+
335
+ def refine(self, options, *args):
336
+ if len(self.embedder.objects) > 1:
337
+ raise SystemExit(('REFINE keyword can only be used with one multimolecular file per run, '
338
+ f'in .xyz format. ({len(self.embedder.objects)} files found in input)'))
339
+
340
+ options.noembed = True
341
+
342
+ def _refine_operator_routine(self):
343
+ if len(self.embedder.objects) > 1:
344
+ raise SystemExit(('The refine> operator can only be used with one multimolecular file per run, '
345
+ f'in .xyz format. ({len(self.embedder.objects)} files found in input)'))
346
+
347
+ self.embedder._set_embedder_structures_from_mol()
348
+
349
+ if self.embedder.options.rmsd is None:
350
+ # set this only if user did not already specify a value
351
+ self.embedder.options.rmsd = 0.25
352
+
353
+ self.embedder.objects[0].compute_orbitals(override='Single' if self.embedder.options.simpleorbitals else None)
354
+
355
+ def bypass(self, options, *args):
356
+ options.bypass = True
357
+ options.optimization = False
358
+
359
+ def charge(self, options, *args):
360
+ kw = self.keywords_simple[self.keywords.index('CHARGE')]
361
+ options.charge = int(kw.split('=')[1])
362
+
363
+ def confs(self, options, *args):
364
+ kw = self.keywords_simple[self.keywords.index('CONFS')]
365
+ options.max_confs = int(kw.split('=')[1])
366
+
367
+ def crestnci(self, options, *args):
368
+ options.crestnci = True
369
+
370
+ def dryrun(self, options, *args):
371
+ options.dryrun = True
372
+
373
+ def suprafac(self, options, *args):
374
+ options.suprafac = True
375
+
376
+ def deep(self, options, *args):
377
+ options.options.rmsd = 0.1
378
+ options.rotation_steps = 72
379
+ options.max_clashes = 1
380
+ options.clash_thresh = 1.4
381
+
382
+ def rotrange(self, options, *args):
383
+ kw = self.keywords_simple[self.keywords.index('ROTRANGE')]
384
+ options.rotation_range = int(kw.split('=')[1])
385
+
386
+ def steps(self, options, *args):
387
+ kw = self.keywords_simple[self.keywords.index('STEPS')]
388
+ options.custom_rotation_steps = int(kw.split('=')[1])
389
+
390
+ def rmsd(self, options, *args):
391
+ kw = self.keywords_simple[self.keywords.index('RMSD')]
392
+ options.rmsd = float(kw.split('=')[1])
393
+
394
+ def noopt(self, options, *args):
395
+ options.optimization = False
396
+
397
+ def ffopt(self, options, *args):
398
+ kw = self.keywords_simple[self.keywords.index('FFOPT')]
399
+ value = kw.split('=')[1].upper()
400
+ if value not in ('ON', 'OFF'):
401
+ raise SystemExit('FFOPT keyword can only have value \'ON\' or \'OFF\' (i.e. \'FFOPT=OFF\')')
402
+
403
+ options.ff_opt = True if value == 'ON' else False
404
+
405
+ def images(self, options, *args):
406
+ kw = self.keywords_simple[self.keywords.index('IMAGES')]
407
+ options.images = int(kw.split('=')[1])
408
+
409
+ def dist(self, options, *args):
410
+ kw = self.keywords_simple_case_sensitive[self.keywords.index('DIST')]
411
+ orb_string = kw[5:-1].replace(' ','')
412
+ # orb_string looks like 'a=2.345,b=3.456,c=2.22'
413
+
414
+ embedder = args[0]
415
+ embedder._set_custom_orbs(orb_string)
416
+
417
+ def clashes(self, options, *args):
418
+ kw = self.keywords_simple[self.keywords.index('CLASHES')]
419
+ clashes_string = kw[8:-1].lower().replace(' ','')
420
+ # clashes_string now looks like 'num=3,dist=1.2'
421
+
422
+ for piece in clashes_string.split(','):
423
+ s = piece.split('=')
424
+ if s[0].lower() == 'num':
425
+ options.max_clashes = int(s[1])
426
+ elif s[0].lower() == 'dist':
427
+ options.clash_thresh = float(s[1])
428
+ else:
429
+ raise SyntaxError((f'Syntax error in CLASHES keyword -> CLASHES({clashes_string}).' +
430
+ 'Correct syntax looks like: CLASHES(num=3,dist=1.2)'))
431
+
432
+ def newbonds(self, options, *args):
433
+ kw = self.keywords_simple[self.keywords.index('NEWBONDS')]
434
+ options.max_newbonds = int(kw.split('=')[1])
435
+
436
+ def neb(self, options, *args):
437
+ options.neb = Truthy_struct()
438
+ options.neb.images = 6
439
+ options.neb.preopt = False
440
+
441
+ kw = self.keywords_simple[self.keywords.index('NEB')]
442
+ neb_options_string = kw[4:-1].lower().replace(' ','')
443
+ # neb_options_string now looks like 'images=8,preopt=true' or ''
444
+
445
+ if neb_options_string != '':
446
+ for piece in neb_options_string.split(','):
447
+ s = piece.split('=')
448
+ if s[0].lower() == 'images':
449
+ options.neb.images = int(s[1])
450
+ elif s[0].lower() == 'preopt':
451
+ if s[1].lower() == 'true':
452
+ options.neb.preopt = True
453
+ else:
454
+ raise SyntaxError((f'Syntax error in NEB keyword -> NEB({neb_options_string}). ' +
455
+ 'Correct syntax looks like: NEB(images=8,preopt=true)'))
456
+
457
+ def level(self, options, *args):
458
+ kw = self.keywords_simple[self.keywords.index('LEVEL')]
459
+ options.theory_level = kw.split('=')[1].upper().replace('_', ' ')
460
+
461
+ options.theory_level = options.theory_level.replace('[', '(').replace(']', ')')
462
+ # quick fix for testing: allows the use of square brackets
463
+ # in place of round, so that the LEVEL keyword is not
464
+ # mistaken for one with sub-arguments. To be better addressed
465
+ # when/if a major rewrite of the option setting happens.
466
+
467
+ def fflevel(self, options, *args):
468
+ kw = self.keywords_simple[self.keywords.index('FFLEVEL')]
469
+ options.ff_level = kw.split('=')[1].upper().replace('_', ' ')
470
+
471
+ def rigid(self, options, *args):
472
+ options.rigid = True
473
+
474
+ def nci(self, options, *args):
475
+ options.nci = True
476
+
477
+ def onlyrefined(self, options, *args):
478
+ options.only_refined = True
479
+
480
+ def let(self, options, *args):
481
+ options.let = True
482
+
483
+ def check(self, options, *args):
484
+ options.check_structures = True
485
+
486
+ def simpleorbitals(self, options, *args):
487
+ options.simpleorbitals = True
488
+
489
+ def kcal(self, options, *args):
490
+ kw = self.keywords_simple[self.keywords.index('KCAL')]
491
+ options.kcal_thresh = float(kw.split('=')[1])
492
+
493
+ def shrink(self, options, *args):
494
+ options.shrink = True
495
+ kw = self.keywords_simple[self.keywords.index('SHRINK')]
496
+
497
+ parsed = kw.split('=')
498
+ options.shrink_multiplier = float(parsed[1]) if len(parsed) > 1 else 1.5
499
+
500
+ # def enantiomers(self, options, *args):
501
+ # options.keep_enantiomers = True
502
+
503
+ def debug(self, options, *args):
504
+ options.debug = True
505
+
506
+ # open a dedicated debug logfile
507
+ debug_log_filename = f'firecode_{self.embedder.stamp}_debug.log'
508
+ self.embedder.debug_logfile = open(debug_log_filename, 'a', buffering=1, encoding="utf-8")
509
+
510
+ from logging import basicConfig
511
+ basicConfig(filename=debug_log_filename, filemode='a')
512
+
513
+
514
+ def procs(self, options, *args):
515
+ kw = self.keywords_simple[self.keywords.index('PROCS')]
516
+ self.embedder.procs = int(kw.split('=')[1])
517
+
518
+ def ezprot(self, options, *args):
519
+ options.double_bond_protection = True
520
+
521
+ def calc(self, options, *args):
522
+ kw = self.keywords_simple[self.keywords.index('CALC')]
523
+ options.calculator = kw.split('=')[1]
524
+
525
+ def ffcalc(self, options, *args):
526
+ kw = self.keywords_simple[self.keywords.index('FFCALC')]
527
+ options.ff_calc = kw.split('=')[1]
528
+
529
+ def mtd(self, options, *args):
530
+ if options.calculator != 'XTB':
531
+ raise SystemExit(('Metadynamics augmentation can only be run with the XTB calculator.\n'
532
+ 'Change it in settings.py or use the CALC=XTB keyword.\n'))
533
+ options.metadynamics = True
534
+
535
+ def saddle(self, options, *args):
536
+ if not options.optimization:
537
+ raise SystemExit('SADDLE keyword can only be used if optimization is turned on. (Not compatible with NOOPT).')
538
+ options.saddle = True
539
+
540
+ def solvent(self, options, *args):
541
+ from firecode.solvents import solvent_synonyms
542
+ kw = self.keywords_simple[self.keywords.index('SOLVENT')]
543
+ solvent = kw.split('=')[1].lower()
544
+ options.solvent = solvent_synonyms.get(solvent, solvent)
545
+
546
+ def pka(self, options, *args):
547
+ kw = self.keywords_simple_case_sensitive[self.keywords.index('PKA')]
548
+ pka_string, pka = kw.split('=')
549
+ molname = pka_string[4:-1].replace(' ','')
550
+
551
+ if molname in [mol.filename for mol in self.embedder.objects]:
552
+ if any([f'pka>{molname}' in op.replace(' ', '') for op in self.embedder.options.operators]):
553
+ self.embedder.pka_ref = (molname, float(pka))
554
+ return
555
+
556
+ raise SyntaxError(f'{molname} must be present in the molecule lines, along with the pka> operator. Syntax: pka(mol.xyz)=n')
557
+
558
+ def csearch(self, options, *args):
559
+ options.csearch_aug = True
560
+
561
+ def set_options(self):
562
+
563
+ # self.keywords = sorted(self.keywords, key=lambda x: __keywords__.index(x))
564
+
565
+ for kw in self.sorted_keywords():
566
+ setter_function = getattr(self, kw.lower())
567
+ setter_function(self.embedder.options, self.embedder, *self.args)
568
+
569
+ if any('refine>' in op for op in self.embedder.options.operators) or self.embedder.options.noembed:
570
+ self._refine_operator_routine()
571
+
572
+ def sorted_keywords(self):
573
+ '''
574
+ Returns all the keywords sorted in the optimal execution order.
575
+
576
+ '''
577
+ return sorted(self.keywords, key=keywords_dict.get)