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.
- firecode/TEST_NOTEBOOK.ipynb +3940 -0
- firecode/__init__.py +0 -0
- firecode/__main__.py +118 -0
- firecode/_gaussian.py +97 -0
- firecode/algebra.py +405 -0
- firecode/ase_manipulations.py +879 -0
- firecode/atropisomer_module.py +516 -0
- firecode/automep.py +130 -0
- firecode/calculators/__init__.py +29 -0
- firecode/calculators/_gaussian.py +98 -0
- firecode/calculators/_mopac.py +242 -0
- firecode/calculators/_openbabel.py +154 -0
- firecode/calculators/_orca.py +129 -0
- firecode/calculators/_xtb.py +786 -0
- firecode/concurrent_test.py +119 -0
- firecode/embedder.py +2590 -0
- firecode/embedder_options.py +577 -0
- firecode/embeds.py +881 -0
- firecode/errors.py +65 -0
- firecode/graph_manipulations.py +333 -0
- firecode/hypermolecule_class.py +364 -0
- firecode/mep_relaxer.py +199 -0
- firecode/modify_settings.py +186 -0
- firecode/mprof.py +65 -0
- firecode/multiembed.py +148 -0
- firecode/nci.py +186 -0
- firecode/numba_functions.py +260 -0
- firecode/operators.py +776 -0
- firecode/optimization_methods.py +609 -0
- firecode/parameters.py +84 -0
- firecode/pka.py +275 -0
- firecode/profiler.py +17 -0
- firecode/pruning.py +421 -0
- firecode/pt.py +32 -0
- firecode/quotes.json +6651 -0
- firecode/quotes.py +9 -0
- firecode/reactive_atoms_classes.py +666 -0
- firecode/references.py +11 -0
- firecode/rmsd.py +74 -0
- firecode/settings.py +75 -0
- firecode/solvents.py +126 -0
- firecode/tests/C2F2H4.xyz +10 -0
- firecode/tests/C2H4.xyz +8 -0
- firecode/tests/CH3Cl.xyz +7 -0
- firecode/tests/HCOOH.xyz +7 -0
- firecode/tests/HCOOOH.xyz +8 -0
- firecode/tests/chelotropic.txt +3 -0
- firecode/tests/cyclical.txt +3 -0
- firecode/tests/dihedral.txt +2 -0
- firecode/tests/string.txt +3 -0
- firecode/tests/trimolecular.txt +9 -0
- firecode/tests.py +151 -0
- firecode/torsion_module.py +1035 -0
- firecode/utils.py +541 -0
- firecode-1.0.0.dist-info/LICENSE +165 -0
- firecode-1.0.0.dist-info/METADATA +321 -0
- firecode-1.0.0.dist-info/RECORD +59 -0
- firecode-1.0.0.dist-info/WHEEL +5 -0
- 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
|