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,186 @@
|
|
|
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
|
+
def run_setup():
|
|
25
|
+
'''
|
|
26
|
+
Invoked by the command
|
|
27
|
+
> python -m firecode -s (--setup)
|
|
28
|
+
|
|
29
|
+
Guides the user in setting up the calculation options
|
|
30
|
+
contained in the settings.py file.
|
|
31
|
+
'''
|
|
32
|
+
|
|
33
|
+
import os
|
|
34
|
+
os.chdir(os.path.dirname(os.path.realpath(__file__)))
|
|
35
|
+
from firecode.settings import DEFAULT_LEVELS, DEFAULT_FF_LEVELS, COMMANDS
|
|
36
|
+
|
|
37
|
+
properties = {
|
|
38
|
+
'FF_OPT_BOOL':False,
|
|
39
|
+
'FF_CALC':None,
|
|
40
|
+
'NEW_FF_DEFAULT':None,
|
|
41
|
+
'CALCULATOR':None,
|
|
42
|
+
'NEW_DEFAULT':None,
|
|
43
|
+
'NEW_COMMAND':None,
|
|
44
|
+
'PROCS':1,
|
|
45
|
+
'MEM_GB':1,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
def ask(text, accepted=('y','n'), default='n'):
|
|
49
|
+
text = '--> ' + text
|
|
50
|
+
if accepted is None:
|
|
51
|
+
answer = input(text)
|
|
52
|
+
print()
|
|
53
|
+
return answer
|
|
54
|
+
answer = None
|
|
55
|
+
while answer not in accepted:
|
|
56
|
+
answer = input(text)
|
|
57
|
+
answer = answer if answer != '' else default
|
|
58
|
+
print()
|
|
59
|
+
return answer
|
|
60
|
+
|
|
61
|
+
tag_dict = {
|
|
62
|
+
'mop':'MOPAC',
|
|
63
|
+
'orca':'ORCA',
|
|
64
|
+
'gau':'GAUSSIAN',
|
|
65
|
+
'xtb':'XTB',
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
print('\FIRECODE setup:\n')
|
|
69
|
+
|
|
70
|
+
#########################################################################################
|
|
71
|
+
|
|
72
|
+
answer = ask('What Force Field calculator would you like to use?\n- XTB -> xtb\n- Gaussian -> gau\n'
|
|
73
|
+
'- None -> none\n\nAnswer [xtb]/gau/none: ', accepted=('xtb', 'gau', 'none'), default='xtb')
|
|
74
|
+
|
|
75
|
+
if answer == 'xtb':
|
|
76
|
+
properties['FF_OPT_BOOL'] = True
|
|
77
|
+
properties['FF_CALC'] = 'XTB'
|
|
78
|
+
|
|
79
|
+
elif answer != 'none':
|
|
80
|
+
properties['FF_OPT_BOOL'] = True
|
|
81
|
+
properties['FF_CALC'] = tag_dict[answer]
|
|
82
|
+
|
|
83
|
+
answer = ask((f'The default level for {properties["FF_CALC"]} force field calculations is \'{DEFAULT_FF_LEVELS[properties["FF_CALC"]]}\'. ' +
|
|
84
|
+
'If you would like to change it, type it here, otherwise press enter : '), accepted=None)
|
|
85
|
+
if answer != '':
|
|
86
|
+
properties['NEW_FF_DEFAULT'] = answer
|
|
87
|
+
|
|
88
|
+
#########################################################################################
|
|
89
|
+
|
|
90
|
+
answer = ask('What main calculator would you like to use?\n- MOPAC -> mop\n- ORCA -> orca\n- Gaussian -> gau\n- XTB -> xtb\n\nAnswer mop/orca/gau/[xtb]: ',
|
|
91
|
+
accepted=('mop','orca','gau','xtb'), default='xtb')
|
|
92
|
+
|
|
93
|
+
properties['CALCULATOR'] = tag_dict[answer]
|
|
94
|
+
|
|
95
|
+
#########################################################################################
|
|
96
|
+
|
|
97
|
+
answer = ask((f'The default level for {properties["CALCULATOR"]} calculations is \'{DEFAULT_LEVELS[properties["CALCULATOR"]]}\'. ' +
|
|
98
|
+
'If you would like to change it, type it here, otherwise press enter : '), accepted=None)
|
|
99
|
+
if answer != '':
|
|
100
|
+
properties['NEW_DEFAULT'] = answer
|
|
101
|
+
|
|
102
|
+
#########################################################################################
|
|
103
|
+
|
|
104
|
+
if properties['CALCULATOR'] != 'XTB':
|
|
105
|
+
answer = ask((f'Current command to call {properties["CALCULATOR"]} is {COMMANDS[properties["CALCULATOR"]]}. ' +
|
|
106
|
+
'If you would like to change it, type it here, otherwise press enter : '), accepted=None)
|
|
107
|
+
if answer != '':
|
|
108
|
+
properties['NEW_COMMAND'] = answer
|
|
109
|
+
|
|
110
|
+
#########################################################################################
|
|
111
|
+
|
|
112
|
+
properties['PROCS'] = ask(f'How many cores should {properties['CALCULATOR']} jobs run on? [4] : ',
|
|
113
|
+
accepted=[str(n) for n in range(1,100)], default=4)
|
|
114
|
+
|
|
115
|
+
#########################################################################################
|
|
116
|
+
|
|
117
|
+
# if properties['CALCULATOR'] in ('GAUSSIAN', 'ORCA'):
|
|
118
|
+
properties['MEM_GB'] = int(ask('How much memory per core should a GAUSSIAN/ORCA job have, in GBs? [4] : ',
|
|
119
|
+
accepted=[str(n) for n in range(1,1000)], default='4'))
|
|
120
|
+
|
|
121
|
+
#########################################################################################
|
|
122
|
+
|
|
123
|
+
rank = {
|
|
124
|
+
'MOPAC':1,
|
|
125
|
+
'ORCA':2,
|
|
126
|
+
'GAUSSIAN':3,
|
|
127
|
+
'XTB':4,
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
q = "\'"
|
|
131
|
+
|
|
132
|
+
with open('settings.py', 'r') as f:
|
|
133
|
+
lines = f.readlines()
|
|
134
|
+
|
|
135
|
+
old_lines = lines.copy()
|
|
136
|
+
|
|
137
|
+
for _l, line in enumerate(old_lines):
|
|
138
|
+
|
|
139
|
+
if 'FF_OPT_BOOL =' in line:
|
|
140
|
+
lines[_l] = 'FF_OPT_BOOL = ' + str(properties['FF_OPT_BOOL']) + '\n'
|
|
141
|
+
FF_OPT_BOOL = properties['FF_OPT_BOOL']
|
|
142
|
+
|
|
143
|
+
if 'FF_CALC =' in line:
|
|
144
|
+
lines[_l] = 'FF_CALC = ' + q + str(properties['FF_CALC']) + q + '\n'
|
|
145
|
+
FF_CALC = properties['FF_CALC']
|
|
146
|
+
|
|
147
|
+
elif 'CALCULATOR =' in line:
|
|
148
|
+
lines[_l] = 'CALCULATOR = ' + q + properties['CALCULATOR'] + q + '\n'
|
|
149
|
+
CALCULATOR = properties['CALCULATOR']
|
|
150
|
+
|
|
151
|
+
elif 'DEFAULT_LEVELS = {' in line:
|
|
152
|
+
if properties['NEW_DEFAULT'] is not None:
|
|
153
|
+
lines[_l+rank[properties['CALCULATOR']]] = ' '*4 + q + properties['CALCULATOR'] + q + ':' + q + properties['NEW_DEFAULT'] + q + ',\n'
|
|
154
|
+
DEFAULT_LEVELS[CALCULATOR] = properties['NEW_DEFAULT']
|
|
155
|
+
|
|
156
|
+
elif 'DEFAULT_FF_LEVELS = {' in line:
|
|
157
|
+
if properties['NEW_FF_DEFAULT'] is not None:
|
|
158
|
+
lines[_l+rank[properties['FF_CALC']]] = ' '*4 + q + properties['FF_CALC'] + q + ':' + q + properties['NEW_FF_DEFAULT'] + q + ',\n'
|
|
159
|
+
DEFAULT_FF_LEVELS[FF_CALC] = properties['NEW_FF_DEFAULT']
|
|
160
|
+
|
|
161
|
+
elif 'COMMANDS = {' in line:
|
|
162
|
+
if properties['NEW_COMMAND'] is not None:
|
|
163
|
+
lines[_l+rank[properties['CALCULATOR']]] = ' '*4 + q + properties['CALCULATOR'] + q + ':' + q + properties['NEW_COMMAND'] + q + ',\n'
|
|
164
|
+
|
|
165
|
+
elif 'PROCS =' in line:
|
|
166
|
+
lines[_l] = 'PROCS = ' + str(properties['PROCS']) + '\n'
|
|
167
|
+
PROCS = properties['PROCS']
|
|
168
|
+
|
|
169
|
+
elif 'MEM_GB =' in line:
|
|
170
|
+
lines[_l] = 'MEM_GB = ' + str(properties['MEM_GB']) + '\n'
|
|
171
|
+
MEM_GB = properties['MEM_GB']
|
|
172
|
+
|
|
173
|
+
with open('settings.py', 'w') as f:
|
|
174
|
+
f.write(''.join(lines))
|
|
175
|
+
|
|
176
|
+
print('\nfirecode setup performed correctly.')
|
|
177
|
+
|
|
178
|
+
ff = f'{FF_CALC}/{DEFAULT_FF_LEVELS[FF_CALC]}' if FF_OPT_BOOL else 'Turned off'
|
|
179
|
+
opt = f'{CALCULATOR}/{DEFAULT_LEVELS[CALCULATOR]}'
|
|
180
|
+
s = f' FF : {ff}\n OPT : {opt}\n PROCS : {PROCS}'
|
|
181
|
+
s += f'\n MEM : {MEM_GB} GB'
|
|
182
|
+
|
|
183
|
+
print(s)
|
|
184
|
+
|
|
185
|
+
if __name__ == '__main__':
|
|
186
|
+
run_setup()
|
firecode/mprof.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
if __name__ == '__main__':
|
|
2
|
+
# ## Let's use malloc to see if we have a memory leak
|
|
3
|
+
|
|
4
|
+
# %%
|
|
5
|
+
|
|
6
|
+
from firecode.embedder import Embedder
|
|
7
|
+
|
|
8
|
+
embedder = Embedder(r'C:\Users\Nik\Desktop\debug\malloc\input', stamp='debug')
|
|
9
|
+
# embedder = Embedder(r'/mnt/c/Users/Nik/Desktop/debug/malloc/input', stamp='debug')
|
|
10
|
+
embedder.objects[0].atomcoords.shape
|
|
11
|
+
|
|
12
|
+
# %%
|
|
13
|
+
# does not seem to leak in the conformational search, maybe it does in the augmentation part
|
|
14
|
+
from firecode.embedder import RunEmbedding
|
|
15
|
+
import numpy as np
|
|
16
|
+
mol = embedder.objects[0]
|
|
17
|
+
|
|
18
|
+
# embedder.options.ff_calc = 'XTB'
|
|
19
|
+
# embedder.options.ff_level = 'GFN-FF'
|
|
20
|
+
|
|
21
|
+
re = RunEmbedding(embedder)
|
|
22
|
+
re.structures = np.array(embedder.objects[0].atomcoords)
|
|
23
|
+
re.atomnos = mol.atomnos
|
|
24
|
+
re.constrained_indices = np.array([[] for _ in re.structures])
|
|
25
|
+
re.energies = np.array([0 ,0])
|
|
26
|
+
|
|
27
|
+
re.structures.shape
|
|
28
|
+
|
|
29
|
+
# %%
|
|
30
|
+
|
|
31
|
+
# re.csearch_augmentation(text='warmup', max_structs=100)
|
|
32
|
+
|
|
33
|
+
# # %%
|
|
34
|
+
# tracemalloc.start()
|
|
35
|
+
# s = tracemalloc.take_snapshot()
|
|
36
|
+
|
|
37
|
+
# ############################################################
|
|
38
|
+
|
|
39
|
+
# try:
|
|
40
|
+
# # re.csearch_augmentation(text='tracemalloc', max_structs=100)
|
|
41
|
+
# re.csearch_augmentation_routine()
|
|
42
|
+
# except KeyboardInterrupt:
|
|
43
|
+
# pass
|
|
44
|
+
|
|
45
|
+
# ############################################################
|
|
46
|
+
|
|
47
|
+
# lines = []
|
|
48
|
+
# top_stats = tracemalloc.take_snapshot().compare_to(s, 'lineno')
|
|
49
|
+
# for stat in top_stats[:5]:
|
|
50
|
+
# lines.append(str(stat))
|
|
51
|
+
# print("\n".join(lines))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# %%
|
|
55
|
+
# from memory_profiler import profile
|
|
56
|
+
|
|
57
|
+
# @profile
|
|
58
|
+
def wrapper(*args, **kwargs):
|
|
59
|
+
return re.csearch_augmentation_routine()
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
# re.csearch_augmentation(text='tracemalloc', max_structs=100)
|
|
63
|
+
wrapper()
|
|
64
|
+
except KeyboardInterrupt:
|
|
65
|
+
pass
|
firecode/multiembed.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
from concurrent.futures import ProcessPoolExecutor, as_completed
|
|
4
|
+
from itertools import permutations
|
|
5
|
+
from shutil import copy, rmtree
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from firecode.errors import InputError, ZeroCandidatesError
|
|
10
|
+
from firecode.utils import (cartesian_product, suppress_stdout_stderr,
|
|
11
|
+
time_to_string, timing_wrapper)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def multiembed_dispatcher(embedder):
|
|
15
|
+
'''
|
|
16
|
+
Calls the appropriate multiembed subfunction
|
|
17
|
+
based on embedder attributes.
|
|
18
|
+
'''
|
|
19
|
+
|
|
20
|
+
if len(embedder.objects) == 2:
|
|
21
|
+
return multiembed_bifunctional(embedder)
|
|
22
|
+
|
|
23
|
+
raise InputError('The multiembed requested is currently unavailable.')
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def multiembed_bifunctional(embedder):
|
|
27
|
+
'''
|
|
28
|
+
Run multiple concurrent bifunctional cyclical embeds
|
|
29
|
+
exploring all relative arrangement of each pair of
|
|
30
|
+
reactive_indices between the two molecules.
|
|
31
|
+
'''
|
|
32
|
+
|
|
33
|
+
mol1, mol2 = embedder.objects
|
|
34
|
+
|
|
35
|
+
# get every possible combination of indices in the two molecules
|
|
36
|
+
pairs = cartesian_product(mol1.reactive_indices, mol2.reactive_indices)
|
|
37
|
+
|
|
38
|
+
# get every arrangement of interacting pairs not insisting on the same atom twice
|
|
39
|
+
arrangements = [((ix_1, ix_2), (iy_1, iy_2)) for ((ix_1, ix_2), (iy_1, iy_2)) in permutations(pairs, 2) if ix_1 != iy_1 and ix_2 != iy_2]
|
|
40
|
+
|
|
41
|
+
structures_out, constr_ids, processes = [], [], []
|
|
42
|
+
|
|
43
|
+
embedder.t_start_run = time.perf_counter()
|
|
44
|
+
embedder.log()
|
|
45
|
+
|
|
46
|
+
max_workers = embedder.avail_cpus or 1
|
|
47
|
+
embedder.log(f'--> Multiembed: running {len(arrangements)} embeds on {max_workers} threads')
|
|
48
|
+
|
|
49
|
+
with ProcessPoolExecutor(max_workers=max_workers) as executor:
|
|
50
|
+
|
|
51
|
+
# for each arrangement, perform a dedicated embed
|
|
52
|
+
for i, arrangement in enumerate(arrangements):
|
|
53
|
+
|
|
54
|
+
process = executor.submit(
|
|
55
|
+
timing_wrapper,
|
|
56
|
+
run_child_embedder,
|
|
57
|
+
mol1.filename,
|
|
58
|
+
mol2.filename,
|
|
59
|
+
constrained_indices=arrangement,
|
|
60
|
+
i=i,
|
|
61
|
+
options=embedder.options,
|
|
62
|
+
)
|
|
63
|
+
processes.append(process)
|
|
64
|
+
|
|
65
|
+
for i, process in enumerate(as_completed(processes)):
|
|
66
|
+
|
|
67
|
+
(structures, constrained_indices), elapsed = process.result()
|
|
68
|
+
|
|
69
|
+
embedder.log(f'--> Child process {i+1:3}/{len(arrangements):3}: generated {len(structures):4} candidates in {time_to_string(elapsed, verbose=True)}.')
|
|
70
|
+
|
|
71
|
+
if len(structures) > 0:
|
|
72
|
+
structures_out.append(structures)
|
|
73
|
+
constr_ids.append(constrained_indices)
|
|
74
|
+
|
|
75
|
+
structures_out = np.concatenate(structures_out)
|
|
76
|
+
|
|
77
|
+
embedder.log(f'\n--> Multiembed completed: generated {len(structures_out)} candidates in {time_to_string(time.perf_counter() - embedder.t_start_run, verbose=True)}.')
|
|
78
|
+
|
|
79
|
+
# only get interaction constraints, as the internal will be added later during refinement
|
|
80
|
+
embedder.constrained_indices = np.concatenate(constr_ids)
|
|
81
|
+
|
|
82
|
+
return structures_out
|
|
83
|
+
|
|
84
|
+
def run_child_embedder(
|
|
85
|
+
mol1_name,
|
|
86
|
+
mol2_name,
|
|
87
|
+
constrained_indices,
|
|
88
|
+
i,
|
|
89
|
+
options,
|
|
90
|
+
):
|
|
91
|
+
|
|
92
|
+
from firecode.embedder import Embedder, RunEmbedding
|
|
93
|
+
|
|
94
|
+
start_dir = os.getcwd()
|
|
95
|
+
foldername = f'firecode_embed{i+1}'
|
|
96
|
+
(ix_1, ix_2), (iy_1, iy_2) = constrained_indices
|
|
97
|
+
|
|
98
|
+
# create a dedicated folder
|
|
99
|
+
if not os.path.isdir(os.path.join(os.getcwd(), foldername)):
|
|
100
|
+
os.mkdir(foldername)
|
|
101
|
+
|
|
102
|
+
# copy structure files into it
|
|
103
|
+
copy(os.path.join(os.getcwd(), mol1_name),
|
|
104
|
+
os.path.join(os.getcwd(), foldername))
|
|
105
|
+
copy(os.path.join(os.getcwd(), mol2_name),
|
|
106
|
+
os.path.join(os.getcwd(), foldername))
|
|
107
|
+
|
|
108
|
+
os.chdir(foldername)
|
|
109
|
+
child_name = f'embed{i+1}_input.txt'
|
|
110
|
+
|
|
111
|
+
with open(child_name, 'w') as f:
|
|
112
|
+
extra = ''
|
|
113
|
+
extra += ' debug' if options.debug else ''
|
|
114
|
+
extra += ' simpleorbitals' if options.simpleorbitals else ''
|
|
115
|
+
extra += f' shrink={options.shrink_multiplier}' if options.shrink else ''
|
|
116
|
+
|
|
117
|
+
f.write(f'noopt rigid{extra}\n')
|
|
118
|
+
f.write(f'{mol1_name} {ix_1}x {iy_1}y\n')
|
|
119
|
+
f.write(f'{mol2_name} {ix_2}x {iy_2}y\n')
|
|
120
|
+
|
|
121
|
+
with suppress_stdout_stderr():
|
|
122
|
+
|
|
123
|
+
child_name = os.path.join(os.getcwd(), child_name)
|
|
124
|
+
child_embedder = Embedder(child_name, f'embed{i+1}')
|
|
125
|
+
child_embedder = RunEmbedding(child_embedder)
|
|
126
|
+
|
|
127
|
+
child_embedder._set_reactive_atoms_cumnums()
|
|
128
|
+
child_embedder.write_mol_info()
|
|
129
|
+
child_embedder.log(f'\n--> FIRECODE multiembed child process - arrangement {i+1}')
|
|
130
|
+
child_embedder.t_start_run = time.perf_counter()
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
child_embedder.generate_candidates()
|
|
134
|
+
child_embedder.compenetration_refining()
|
|
135
|
+
child_embedder.fitness_refining()
|
|
136
|
+
child_embedder.similarity_refining(rmsd=False, verbose=True)
|
|
137
|
+
child_embedder.write_structures('unoptimized', energies=False)
|
|
138
|
+
|
|
139
|
+
except ZeroCandidatesError:
|
|
140
|
+
child_embedder.structures = []
|
|
141
|
+
|
|
142
|
+
child_embedder.log(f'\n--> Child process terminated ({time_to_string(time.perf_counter() - child_embedder.t_start_run, verbose=True)})')
|
|
143
|
+
|
|
144
|
+
os.chdir(start_dir)
|
|
145
|
+
if not options.debug:
|
|
146
|
+
rmtree(os.path.join(os.getcwd(), foldername))
|
|
147
|
+
|
|
148
|
+
return child_embedder.structures, child_embedder.constrained_indices
|
firecode/nci.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
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 itertools import combinations
|
|
24
|
+
|
|
25
|
+
import numpy as np
|
|
26
|
+
|
|
27
|
+
from firecode.algebra import norm_of
|
|
28
|
+
from firecode.parameters import nci_dict
|
|
29
|
+
from firecode.pt import pt
|
|
30
|
+
from firecode.graph_manipulations import is_phenyl
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_nci(coords, atomnos, constrained_indices, ids):
|
|
34
|
+
'''
|
|
35
|
+
Returns a list of guesses for intermolecular non-covalent
|
|
36
|
+
interactions between molecular fragments/atoms. Used to get
|
|
37
|
+
a hint of the most prominent NCIs that drive stereo/regio selectivity.
|
|
38
|
+
'''
|
|
39
|
+
nci = []
|
|
40
|
+
print_list = []
|
|
41
|
+
|
|
42
|
+
symbols = [pt[i].symbol for i in atomnos]
|
|
43
|
+
constrained_indices = constrained_indices.ravel()
|
|
44
|
+
|
|
45
|
+
print_list, nci = _get_nci_atomic_pairs(coords, symbols, constrained_indices, ids)
|
|
46
|
+
# Initialize with atomic pairs NCIs
|
|
47
|
+
|
|
48
|
+
# Start checking group contributions
|
|
49
|
+
aromatic_centers = _get_aromatic_centers(coords, symbols, ids)
|
|
50
|
+
# print(f'structure has {len(aromatic_centers)} phenyl rings')
|
|
51
|
+
|
|
52
|
+
# checking phenyl-atom pairs and phenyl-phenyl pairs
|
|
53
|
+
pl, nc = _get_nci_aromatic_rings(coords, symbols, ids, aromatic_centers)
|
|
54
|
+
print_list += pl
|
|
55
|
+
nci += nc
|
|
56
|
+
|
|
57
|
+
return nci, print_list
|
|
58
|
+
|
|
59
|
+
def _get_nci_atomic_pairs(coords, symbols, constrained_indices, ids):
|
|
60
|
+
'''
|
|
61
|
+
'''
|
|
62
|
+
print_list = []
|
|
63
|
+
nci = []
|
|
64
|
+
|
|
65
|
+
cum_ids = np.cumsum(ids)
|
|
66
|
+
|
|
67
|
+
for i1, _ in enumerate(coords):
|
|
68
|
+
# check atomic pairs (O-H, N-H, ...)
|
|
69
|
+
|
|
70
|
+
start_of_next_mol = cum_ids[next(i for i,n in enumerate(cum_ids) if i1 < n)]
|
|
71
|
+
# ensures that we are only taking into account intermolecular NCIs
|
|
72
|
+
|
|
73
|
+
for i2, _ in enumerate(coords[start_of_next_mol:]):
|
|
74
|
+
i2 += start_of_next_mol
|
|
75
|
+
|
|
76
|
+
if (i1 not in constrained_indices) and (i2 not in constrained_indices):
|
|
77
|
+
# ignore atoms involved in constraints
|
|
78
|
+
|
|
79
|
+
s = ''.join(sorted([symbols[i1], symbols[i2]]))
|
|
80
|
+
# print(f'Checking pair {i1}/{i2}')
|
|
81
|
+
|
|
82
|
+
if s in nci_dict:
|
|
83
|
+
threshold, nci_type = nci_dict[s]
|
|
84
|
+
dist = norm_of(coords[i1]-coords[i2])
|
|
85
|
+
|
|
86
|
+
if dist < threshold:
|
|
87
|
+
|
|
88
|
+
print_list.append(nci_type + f' ({round(dist, 2)} A, indices {i1}/{i2})')
|
|
89
|
+
# string to be printed in log
|
|
90
|
+
|
|
91
|
+
nci.append((nci_type, i1, i2))
|
|
92
|
+
# tuple to be used in identifying the NCI
|
|
93
|
+
|
|
94
|
+
return print_list, nci
|
|
95
|
+
|
|
96
|
+
def _get_nci_aromatic_rings(coords, symbols, ids, aromatic_centers):
|
|
97
|
+
'''
|
|
98
|
+
'''
|
|
99
|
+
cum_ids = np.cumsum(ids)
|
|
100
|
+
print_list, nci = [], []
|
|
101
|
+
|
|
102
|
+
for owner, center in aromatic_centers:
|
|
103
|
+
for i, atom in enumerate(coords):
|
|
104
|
+
|
|
105
|
+
if i < cum_ids[0]:
|
|
106
|
+
atom_owner = 0
|
|
107
|
+
else:
|
|
108
|
+
atom_owner = next(i for i,n in enumerate(np.cumsum(ids)) if i < n)
|
|
109
|
+
|
|
110
|
+
if atom_owner != owner:
|
|
111
|
+
# if this atom belongs to a molecule different than the one that owns the phenyl
|
|
112
|
+
|
|
113
|
+
s = ''.join(sorted(['Ph', symbols[i]]))
|
|
114
|
+
if s in nci_dict:
|
|
115
|
+
|
|
116
|
+
threshold, nci_type = nci_dict[s]
|
|
117
|
+
dist = norm_of(center - atom)
|
|
118
|
+
|
|
119
|
+
if dist < threshold:
|
|
120
|
+
|
|
121
|
+
print_list.append(nci_type + f' ({round(dist, 2)} A, atom {i}/ring)')
|
|
122
|
+
# string to be printed in log
|
|
123
|
+
|
|
124
|
+
nci.append((nci_type, i, 'ring'))
|
|
125
|
+
# tuple to be used in identifying the NCI
|
|
126
|
+
|
|
127
|
+
# checking phenyl-phenyl pairs
|
|
128
|
+
for i, owner_center in enumerate(aromatic_centers):
|
|
129
|
+
owner1, center1 = owner_center
|
|
130
|
+
for owner2, center2 in aromatic_centers[i+1:]:
|
|
131
|
+
if owner1 != owner2:
|
|
132
|
+
# if this atom belongs to a molecule different than owner
|
|
133
|
+
|
|
134
|
+
threshold, nci_type = nci_dict['PhPh']
|
|
135
|
+
dist = norm_of(center1 - center2)
|
|
136
|
+
|
|
137
|
+
if dist < threshold:
|
|
138
|
+
|
|
139
|
+
print_list.append(nci_type + f' ({round(dist, 2)} A, ring/ring)')
|
|
140
|
+
# string to be printed in log
|
|
141
|
+
|
|
142
|
+
nci.append((nci_type, 'ring', 'ring'))
|
|
143
|
+
# tuple to be used in identifying the NCI
|
|
144
|
+
return print_list, nci
|
|
145
|
+
|
|
146
|
+
def _get_aromatic_centers(coords, symbols, ids):
|
|
147
|
+
'''
|
|
148
|
+
'''
|
|
149
|
+
cum_ids = np.cumsum(ids)
|
|
150
|
+
masks = []
|
|
151
|
+
|
|
152
|
+
for mol, _ in enumerate(ids):
|
|
153
|
+
|
|
154
|
+
if mol == 0:
|
|
155
|
+
mol_mask = slice(0, cum_ids[0])
|
|
156
|
+
filler = 0
|
|
157
|
+
else:
|
|
158
|
+
mol_mask = slice(cum_ids[mol-1], cum_ids[mol])
|
|
159
|
+
filler = cum_ids[mol-1]
|
|
160
|
+
|
|
161
|
+
aromatics_indices = np.array([i+filler for i, s in enumerate(symbols[mol_mask]) if s in ('C','N')])
|
|
162
|
+
|
|
163
|
+
if len(aromatics_indices) > 5:
|
|
164
|
+
# only check for phenyls in molecules with more than 5 C/N atoms
|
|
165
|
+
|
|
166
|
+
masks.append(list(combinations(aromatics_indices, 6)))
|
|
167
|
+
# all possible combinations of picking 6 C/N atoms from this molecule
|
|
168
|
+
|
|
169
|
+
aromatic_centers = []
|
|
170
|
+
|
|
171
|
+
if masks:
|
|
172
|
+
|
|
173
|
+
masks = np.concatenate(masks)
|
|
174
|
+
|
|
175
|
+
for mask in masks:
|
|
176
|
+
|
|
177
|
+
if is_phenyl(coords[mask]):
|
|
178
|
+
|
|
179
|
+
center = np.mean(coords[mask], axis=0)
|
|
180
|
+
|
|
181
|
+
owner = next(i for i,n in enumerate(np.cumsum(ids)) if np.all(mask < n))
|
|
182
|
+
# index of the molecule that owns that phenyl ring
|
|
183
|
+
|
|
184
|
+
aromatic_centers.append((owner, center))
|
|
185
|
+
|
|
186
|
+
return aromatic_centers
|