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,98 @@
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 sys
25
+ from subprocess import DEVNULL, STDOUT, check_call
26
+
27
+ from firecode.settings import COMMANDS, MEM_GB
28
+ from firecode.solvents import get_solvent_line
29
+ from firecode.utils import clean_directory, pt, read_xyz
30
+
31
+
32
+ def gaussian_opt(coords, atomnos, constrained_indices=None, method='PM6', procs=1, solvent=None, title='temp', read_output=True, **kwargs):
33
+ '''
34
+ This function writes a Gaussian .inp file, runs it with the subprocess
35
+ module and reads its output.
36
+
37
+ :params coords: array of shape (n,3) with cartesian coordinates for atoms.
38
+ :params atomnos: array of atomic numbers for atoms.
39
+ :params constrained_indices: array of shape (n,2), with the indices
40
+ of atomic pairs to be constrained.
41
+ :params method: string, specifiyng the first line of keywords for the MOPAC input file.
42
+ :params title: string, used as a file name and job title for the mopac input file.
43
+ :params read_output: Whether to read the output file and return anything.
44
+ '''
45
+
46
+ s = ''
47
+
48
+ if MEM_GB is not None:
49
+ if MEM_GB < 1:
50
+ s += f'%mem={int(1000*MEM_GB)}MB\n'
51
+ else:
52
+ s += f'%mem={MEM_GB}GB\n'
53
+
54
+ if procs > 1:
55
+ s += f'%nprocshared={procs}\n'
56
+
57
+ s = '# opt ' if constrained_indices is not None else '# opt=modredundant '
58
+ s += method
59
+
60
+ if solvent is not None:
61
+ s += ' ' + get_solvent_line(solvent, 'GAUSSIAN', method)
62
+
63
+ s += '\n\nGaussian input generated by FIRECODE\n\n0 1\n'
64
+
65
+ for i, atom in enumerate(coords):
66
+ s += '%s % .6f % .6f % .6f\n' % (pt[atomnos[i]].symbol, atom[0], atom[1], atom[2])
67
+
68
+ s += '\n'
69
+
70
+ if constrained_indices is not None:
71
+
72
+ for a, b in constrained_indices:
73
+ s += 'B %s %s F\n' % (a+1, b+1) # Gaussian numbering starts at 1
74
+
75
+ s = ''.join(s)
76
+ with open(f'{title}.com', 'w') as f:
77
+ f.write(s)
78
+
79
+ try:
80
+ check_call(f'{COMMANDS["GAUSSIAN"]} {title}.com'.split(), stdout=DEVNULL, stderr=STDOUT)
81
+
82
+ except KeyboardInterrupt:
83
+ print('KeyboardInterrupt requested by user. Quitting.')
84
+ sys.exit()
85
+
86
+ if read_output:
87
+
88
+ try:
89
+ data = read_xyz(f'{title}.out')
90
+ opt_coords = data.atomcoords[0]
91
+ energy = data.scfenergies[-1] * 23.060548867 # eV to kcal/mol
92
+
93
+ clean_directory((f'{title}.com',))
94
+
95
+ return opt_coords, energy, True
96
+
97
+ except FileNotFoundError:
98
+ return None, None, False
@@ -0,0 +1,242 @@
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 sys
26
+ from subprocess import DEVNULL, STDOUT, check_call
27
+
28
+ import numpy as np
29
+
30
+ from firecode.algebra import dihedral, norm, norm_of, vec_angle
31
+ from firecode.errors import MopacReadError
32
+ from firecode.pt import pt
33
+ from firecode.numba_functions import scramble
34
+ from firecode.settings import COMMANDS
35
+ from firecode.solvents import get_solvent_line
36
+
37
+
38
+ def read_mop_out(filename):
39
+ '''
40
+ Reads a MOPAC output looking for optimized coordinates and energy.
41
+ :params filename: name of MOPAC filename (.out extension)
42
+ :return coords, energy: array of optimized coordinates and absolute energy, in kcal/mol
43
+ '''
44
+ coords = []
45
+ with open(filename, 'r') as f:
46
+ while True:
47
+ line = f.readline()
48
+
49
+ if 'Too many variables. By definition, at least one force constant is exactly zero' in line:
50
+ success = False
51
+ return None, 1E10, success
52
+
53
+ if not line:
54
+ break
55
+
56
+ if 'SCF FIELD WAS ACHIEVED' in line:
57
+ while True:
58
+ line = f.readline()
59
+ if not line:
60
+ break
61
+
62
+ if 'FINAL HEAT OF FORMATION' in line:
63
+ energy = float(line.split()[5])
64
+ # in kcal/mol
65
+
66
+ if 'CARTESIAN COORDINATES' in line:
67
+ line = f.readline()
68
+ line = f.readline()
69
+ while line != '\n':
70
+ splitted = line.split()
71
+ # symbols.append(splitted[1])
72
+ coords.append([float(splitted[2]),
73
+ float(splitted[3]),
74
+ float(splitted[4])])
75
+
76
+ line = f.readline()
77
+ if not line:
78
+ break
79
+ break
80
+ break
81
+
82
+ coords = np.array(coords)
83
+
84
+ if coords.shape[0] != 0:
85
+ success = True
86
+ return coords, energy, success
87
+
88
+ raise MopacReadError(f'Cannot read file {filename}: maybe a badly specified MOPAC keyword?')
89
+
90
+ def mopac_opt(coords, atomnos, constrained_indices=None, method='PM7', solvent=None, title='temp', read_output=True, **kwargs):
91
+ '''
92
+ This function writes a MOPAC .mop input, runs it with the subprocess
93
+ module and reads its output. Coordinates used are mixed
94
+ (cartesian and internal) to be able to constrain the reactive atoms
95
+ distances specified in constrained_indices.
96
+
97
+ :params coords: array of shape (n,3) with cartesian coordinates for atoms
98
+ :params atomnos: array of atomic numbers for atoms
99
+ :params constrained_indices: array of shape (n,2), with the indices
100
+ of atomic pairs to be constrained
101
+ :params method: string, specifiyng the first line of keywords for the MOPAC input file.
102
+ :params title: string, used as a file name and job title for the mopac input file.
103
+ :params read_output: Whether to read the output file and return anything.
104
+ '''
105
+
106
+ constrained_indices_list = constrained_indices.ravel() if constrained_indices is not None else []
107
+ constrained_indices = constrained_indices if constrained_indices is not None else []
108
+
109
+ if solvent is not None:
110
+ method += ' ' + get_solvent_line(solvent, 'MOPAC', method)
111
+
112
+ order = []
113
+ s = [method + '\n' + title + '\n\n']
114
+ for i, num in enumerate(atomnos):
115
+ if i not in constrained_indices:
116
+ order.append(i)
117
+ s.append(' {} {} 1 {} 1 {} 1\n'.format(pt[num].symbol, coords[i][0], coords[i][1], coords[i][2]))
118
+
119
+ free_indices = list(set(range(len(atomnos))) - set(constrained_indices_list))
120
+ # print('free indices are', free_indices, '\n')
121
+
122
+ if len(constrained_indices_list) == len(set(constrained_indices_list)):
123
+ # block pairs of atoms if no atom is involved in more than one distance constrain
124
+
125
+ for a, b in constrained_indices:
126
+
127
+ order.append(b)
128
+ order.append(a)
129
+
130
+ c, d = np.random.choice(free_indices, 2)
131
+ while c == d:
132
+ c, d = np.random.choice(free_indices, 2)
133
+ # indices of reference atoms, from unconstraind atoms set
134
+
135
+ dist = norm_of(coords[a] - coords[b]) # in Angstrom
136
+ # print(f'DIST - {dist} - between {a} {b}')
137
+
138
+ angle = vec_angle(norm(coords[a] - coords[b]), norm(coords[c] - coords[b]))
139
+ # print(f'ANGLE - {angle} - between {a} {b} {c}')
140
+
141
+ d_angle = dihedral([coords[a],
142
+ coords[b],
143
+ coords[c],
144
+ coords[d]])
145
+ d_angle += 360 if d_angle < 0 else 0
146
+ # print(f'D_ANGLE - {d_angle} - between {a} {b} {c} {d}')
147
+
148
+ list_len = len(s)
149
+ s.append(' {} {} 1 {} 1 {} 1\n'.format(pt[atomnos[b]].symbol, coords[b][0], coords[b][1], coords[b][2]))
150
+ s.append(' {} {} 0 {} 1 {} 1 {} {} {}\n'.format(pt[atomnos[a]].symbol, dist, angle, d_angle, list_len, free_indices.index(c)+1, free_indices.index(d)+1))
151
+ # print(f'Blocked bond between mopac ids {list_len} {list_len+1}\n')
152
+
153
+ elif len(set(constrained_indices_list)) == 3:
154
+ # three atoms, the central bound to the other two
155
+ # OTHERS[0]: cartesian
156
+ # CENTRAL: internal (self, others[0], two random)
157
+ # OTHERS[1]: internal (self, central, two random)
158
+
159
+ central = max(set(constrained_indices_list), key=lambda x: list(constrained_indices_list).count(x))
160
+ # index of the atom that is constrained to two other
161
+
162
+ others = list(set(constrained_indices_list) - {central})
163
+
164
+ # OTHERS[0]
165
+
166
+ order.append(others[0])
167
+ s.append(' {} {} 1 {} 1 {} 1\n'.format(pt[atomnos[others[0]]].symbol, coords[others[0]][0], coords[others[0]][1], coords[others[0]][2]))
168
+ # first atom is placed in cartesian coordinates, the other two have a distance constraint and are expressed in internal coordinates
169
+
170
+ #CENTRAL
171
+
172
+ order.append(central)
173
+ c, d = np.random.choice(free_indices, 2)
174
+ while c == d:
175
+ c, d = np.random.choice(free_indices, 2)
176
+ # indices of reference atoms, from unconstraind atoms set
177
+
178
+ dist = norm_of(coords[central] - coords[others[0]]) # in Angstrom
179
+
180
+ angle = vec_angle(norm(coords[central] - coords[others[0]]), norm(coords[others[0]] - coords[c]))
181
+
182
+ d_angle = dihedral([coords[central],
183
+ coords[others[0]],
184
+ coords[c],
185
+ coords[d]])
186
+ d_angle += 360 if d_angle < 0 else 0
187
+
188
+ list_len = len(s)
189
+ s.append(' {} {} 0 {} 1 {} 1 {} {} {}\n'.format(pt[atomnos[central]].symbol, dist, angle, d_angle, list_len-1, free_indices.index(c)+1, free_indices.index(d)+1))
190
+
191
+ #OTHERS[1]
192
+
193
+ order.append(others[1])
194
+ c1, d1 = np.random.choice(free_indices, 2)
195
+ while c1 == d1:
196
+ c1, d1 = np.random.choice(free_indices, 2)
197
+ # indices of reference atoms, from unconstraind atoms set
198
+
199
+ dist1 = norm_of(coords[others[1]] - coords[central]) # in Angstrom
200
+
201
+ angle1 = np.arccos(norm(coords[others[1]] - coords[central]) @ norm(coords[others[1]] - coords[c1]))*180/np.pi # in degrees
202
+
203
+ d_angle1 = dihedral([coords[others[1]],
204
+ coords[central],
205
+ coords[c1],
206
+ coords[d1]])
207
+ d_angle1 += 360 if d_angle < 0 else 0
208
+
209
+ list_len = len(s)
210
+ s.append(' {} {} 0 {} 1 {} 1 {} {} {}\n'.format(pt[atomnos[others[1]]].symbol, dist1, angle1, d_angle1, list_len-1, free_indices.index(c1)+1, free_indices.index(d1)+1))
211
+
212
+ else:
213
+ raise NotImplementedError('The constraints provided for MOPAC optimization are not yet supported')
214
+
215
+
216
+ s = ''.join(s)
217
+ with open(f'{title}.mop', 'w') as f:
218
+ f.write(s)
219
+
220
+ try:
221
+ check_call(f'{COMMANDS["MOPAC"]} {title}.mop'.split(), stdout=DEVNULL, stderr=STDOUT)
222
+ except KeyboardInterrupt:
223
+ print('KeyboardInterrupt requested by user. Quitting.')
224
+ sys.exit()
225
+
226
+ os.remove(f'{title}.mop')
227
+ # delete input, we do not need it anymore
228
+
229
+ if read_output:
230
+
231
+ inv_order = [order.index(i) for i, _ in enumerate(order)]
232
+ # undoing the atomic scramble that was needed by the mopac input requirements
233
+
234
+ opt_coords, energy, success = read_mop_out(f'{title}.out')
235
+ os.remove(f'{title}.out')
236
+
237
+ opt_coords = scramble(opt_coords, inv_order) if opt_coords is not None else coords
238
+ # If opt_coords is None, that is if TS seeking crashed,
239
+ # sets opt_coords to the old coords. If not, unscrambles
240
+ # coordinates read from mopac output.
241
+
242
+ return opt_coords, energy, success
@@ -0,0 +1,154 @@
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
+ # VERSION 0.4.4:
25
+ # THIS MODULE IS NOT INTERFACED WITH THE MAIN PROGRAM EMBEDDER ANYMORE.
26
+ # IT IS LEFT HERE AS AN EXTERNAL UTILITY TOOL AND FOR POTENTIAL FUTURE
27
+ # USE AS A FASTER, LESS ROBUST ALTERNATIVE TO THE XTB FF IMPLEMENTATION.
28
+
29
+ from firecode.utils import clean_directory, scramble_check, write_xyz, read_xyz
30
+ from firecode.algebra import norm, norm_of
31
+ from openbabel import openbabel as ob
32
+
33
+ def openbabel_opt(
34
+ structure,
35
+ atomnos,
36
+ constrained_indices,
37
+ constrained_distances=None,
38
+ tight_constraint=True,
39
+ graphs=None,
40
+ check=False,
41
+ method='UFF',
42
+ nsteps=1000,
43
+ title='temp_ob',
44
+ **kwargs,
45
+ ):
46
+ '''
47
+ tight_constraint: False uses the native implementation,
48
+ True uses a more accurate recursive one
49
+ return : MM-optimized structure (UFF/MMFF94)
50
+ '''
51
+
52
+ assert not check or graphs is not None, 'Either provide molecular graphs or do not check for scrambling.'
53
+ assert method in ('UFF', 'MMFF94', 'Ghemical', 'GAFF'), 'OpenBabel implements only the UFF, MMFF94, Ghemical and GAFF Force Fields.'
54
+
55
+ # If we have any target distance to impose,
56
+ # the most accurate way to do it is to manually
57
+ # move the second atom and then freeze both atom
58
+ # in place during optimization. If we would have
59
+ # to move the second atom too much we do that in
60
+ # small steps of 0.2 A, recursively, to avoid having
61
+ # openbabel come up with weird bonding topologies,
62
+ # ending in scrambling.
63
+
64
+ if constrained_distances is not None and tight_constraint:
65
+ for target_d, (a, b) in zip(constrained_distances, constrained_indices):
66
+ d = norm_of(structure[b] - structure[a])
67
+ delta = d - target_d
68
+
69
+ if abs(delta) > 0.2:
70
+ sign = (d > target_d)
71
+ recursive_c_d = [d + 0.2 * sign for d in constrained_distances]
72
+
73
+ structure, _, _ = openbabel_opt(
74
+ structure,
75
+ atomnos,
76
+ constrained_indices,
77
+ constrained_distances=recursive_c_d,
78
+ tight_constraint=True,
79
+ graphs=graphs,
80
+ check=check,
81
+ method=method,
82
+ nsteps=nsteps,
83
+ title=title,
84
+ **kwargs,
85
+ )
86
+
87
+ d = norm_of(structure[b] - structure[a])
88
+ delta = d - target_d
89
+ structure[b] -= norm(structure[b] - structure[a]) * delta
90
+
91
+ filename=f'{title}_in.xyz'
92
+
93
+ with open(filename, 'w') as f:
94
+ write_xyz(structure, atomnos, f)
95
+ # input()
96
+ outname = f'{title}_out.xyz'
97
+
98
+ # Standard openbabel molecule load
99
+ conv = ob.OBConversion()
100
+ conv.SetInAndOutFormats('xyz','xyz')
101
+ mol = ob.OBMol()
102
+ conv.ReadFile(mol, filename)
103
+ i = 0
104
+
105
+ # Define constraints
106
+ constraints = ob.OBFFConstraints()
107
+
108
+ for i, (a, b) in enumerate(constrained_indices):
109
+
110
+ # Adding a distance constraint does not lead to accurate results,
111
+ # so the backup solution is to freeze the atoms in place
112
+ if tight_constraint:
113
+ constraints.AddAtomConstraint(int(a+1))
114
+ constraints.AddAtomConstraint(int(b+1))
115
+
116
+ else:
117
+ if constrained_distances is None:
118
+ first_atom = mol.GetAtom(int(a+1))
119
+ length = first_atom.GetDistance(int(b+1))
120
+ else:
121
+ length = constrained_distances[i]
122
+
123
+ constraints.AddDistanceConstraint(int(a+1), int(b+1), length) # Angstroms
124
+
125
+ # constraints.AddAngleConstraint(1, 2, 3, 120.0) # Degrees
126
+ # constraints.AddTorsionConstraint(1, 2, 3, 4, 180.0) # Degrees
127
+
128
+ # Setup the force field with the constraints
129
+ forcefield = ob.OBForceField.FindForceField(method)
130
+ forcefield.Setup(mol, constraints)
131
+
132
+ # Set the strictness of the constraint
133
+ forcefield.SetConstraints(constraints)
134
+
135
+ # Do a nsteps conjugate gradient minimization
136
+ # (or less if converges) and save the coordinates to mol.
137
+ forcefield.ConjugateGradients(nsteps)
138
+ forcefield.GetCoordinates(mol)
139
+ energy = forcefield.Energy() * 0.2390057361376673 # kJ/mol to kcal/mol
140
+
141
+ # Write the mol to a file
142
+ conv.WriteFile(mol,outname)
143
+ conv.CloseOutFile()
144
+
145
+ opt_coords = read_xyz(outname).atomcoords[0]
146
+
147
+ clean_directory((f'{title}_in.xyz', f'{title}_out.xyz'))
148
+
149
+ if check:
150
+ success = scramble_check(opt_coords, atomnos, constrained_indices, graphs)
151
+ else:
152
+ success = True
153
+
154
+ return opt_coords, energy, success
@@ -0,0 +1,129 @@
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 sys
25
+ from subprocess import STDOUT, check_call
26
+
27
+ from firecode.settings import COMMANDS, MEM_GB
28
+ from firecode.solvents import get_solvent_line
29
+ from firecode.utils import clean_directory, pt, read_xyz
30
+
31
+
32
+ def orca_opt(coords,
33
+ atomnos,
34
+ constrained_indices=None,
35
+ method='PM3',
36
+ charge=0,
37
+ procs=1,
38
+ maxiter=None,
39
+ mem=MEM_GB,
40
+ solvent=None,
41
+ title='temp',
42
+ read_output=True,
43
+ **kwargs):
44
+ '''
45
+ This function writes an ORCA .inp file, runs it with the subprocess
46
+ module and reads its output.
47
+
48
+ :params coords: array of shape (n,3) with cartesian coordinates for atoms.
49
+ :params atomnos: array of atomic numbers for atoms.
50
+ :params constrained_indices: array of shape (n,2), with the indices
51
+ of atomic pairs to be constrained.
52
+ :params method: string, specifiyng the first line of keywords for the MOPAC input file.
53
+ :params title: string, used as a file name and job title for the mopac input file.
54
+ :params read_output: Whether to read the output file and return anything.
55
+ '''
56
+
57
+ s = '! %s Opt\n\n# ORCA input generated by FIRECODE\n\n' % (method)
58
+
59
+ if solvent is not None:
60
+ s += '\n' + get_solvent_line(solvent, 'ORCA', method) + '\n'
61
+
62
+ if procs > 1:
63
+ s += f'%pal nprocs {procs} end\n\n'
64
+
65
+ s += f'%maxcore {mem*1000}\n\n'
66
+
67
+ if constrained_indices is not None:
68
+ s += f'%{""}geom\nConstraints\n'
69
+ # weird f-string to prevent python misinterpreting %
70
+
71
+ for a, b in constrained_indices:
72
+ s += ' {B %s %s C}\n' % (a, b)
73
+
74
+ s += ' end\nend\n\n'
75
+
76
+ if maxiter is not None:
77
+ s += f'%{""}geom\n MaxIter {maxiter}\nend\n'
78
+ # weird f-string to prevent python misinterpreting %
79
+
80
+ s += f'*xyz {charge} 1\n'
81
+
82
+ for i, atom in enumerate(coords):
83
+ s += '%s % .6f % .6f % .6f\n' % (pt[atomnos[i]].symbol, atom[0], atom[1], atom[2])
84
+
85
+ s += '*\n'
86
+
87
+ s = ''.join(s)
88
+ with open(f'{title}.inp', 'w') as f:
89
+ f.write(s)
90
+
91
+ try:
92
+ with open(f"{title}.out", "w") as f:
93
+ check_call(f'{COMMANDS["ORCA"]} {title}.inp \"--oversubscribe\"'.split(), stdout=f, stderr=STDOUT)
94
+
95
+ except KeyboardInterrupt:
96
+ print('KeyboardInterrupt requested by user. Quitting.')
97
+ sys.exit()
98
+
99
+ if read_output:
100
+
101
+ try:
102
+ opt_coords = read_xyz(f'{title}.xyz').atomcoords[0]
103
+ energy = read_orca_property(f'{title}_property.txt')
104
+
105
+ clean_directory((f'{title}.inp',))
106
+
107
+ return opt_coords, energy, True
108
+
109
+ except FileNotFoundError:
110
+ return None, None, False
111
+
112
+ def read_orca_property(filename):
113
+ '''
114
+ Read energy from ORCA property output file
115
+ '''
116
+ energy = None
117
+
118
+ with open(filename, 'r') as f:
119
+
120
+ while True:
121
+ line = f.readline()
122
+
123
+ if not line:
124
+ break
125
+
126
+ if 'SCF Energy:' in line:
127
+ energy = float(line.split()[2])
128
+
129
+ return energy