mdinterface 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.
- mdinterface/__init__.py +10 -0
- mdinterface/core/__init__.py +8 -0
- mdinterface/core/specie.py +507 -0
- mdinterface/core/topology.py +220 -0
- mdinterface/io/__init__.py +8 -0
- mdinterface/io/lammpswriter.py +261 -0
- mdinterface/io/packmol.py +35 -0
- mdinterface/io/read.py +126 -0
- mdinterface/simulationbox.py +621 -0
- mdinterface/utils/__init__.py +8 -0
- mdinterface/utils/auxiliary.py +262 -0
- mdinterface/utils/database.py +111 -0
- mdinterface/utils/map.py +269 -0
- mdinterface-1.0.0.dist-info/LICENSE +201 -0
- mdinterface-1.0.0.dist-info/METADATA +76 -0
- mdinterface-1.0.0.dist-info/RECORD +18 -0
- mdinterface-1.0.0.dist-info/WHEEL +5 -0
- mdinterface-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Created on Fri Apr 12 14:04:43 2024
|
|
5
|
+
|
|
6
|
+
@author: roncofaber
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
import collections
|
|
11
|
+
import numpy as np
|
|
12
|
+
import networkx as nx
|
|
13
|
+
|
|
14
|
+
import ase
|
|
15
|
+
from ase import neighborlist
|
|
16
|
+
#%%
|
|
17
|
+
|
|
18
|
+
def label_to_element(atostr, atomss):
|
|
19
|
+
"""
|
|
20
|
+
Attempts to determine the chemical element symbol corresponding to a given
|
|
21
|
+
string and atomic mass.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
atostr (str): A string potentially representing a chemical element.
|
|
25
|
+
atomss (float): The approximate atomic mass of the element.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
str: The inferred chemical element symbol.
|
|
29
|
+
|
|
30
|
+
Raises:
|
|
31
|
+
ValueError: If the function cannot determine a valid element from the input.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
new_label = re.sub(r'[^A-Za-z]', '', atostr).capitalize() # Clean up input
|
|
36
|
+
|
|
37
|
+
# load ase info
|
|
38
|
+
atomic_masses = ase.data.atomic_masses
|
|
39
|
+
elements = ase.data.chemical_symbols
|
|
40
|
+
|
|
41
|
+
# initialize variables
|
|
42
|
+
is_ready = False # Flag to track if the element is found
|
|
43
|
+
tried_last_resort = False
|
|
44
|
+
|
|
45
|
+
while not is_ready:
|
|
46
|
+
try:
|
|
47
|
+
try_atom = ase.Atom(new_label) # Attempt to create an Atom object
|
|
48
|
+
existent = True
|
|
49
|
+
except:
|
|
50
|
+
existent = False
|
|
51
|
+
|
|
52
|
+
if existent and np.abs(try_atom.mass - atomss) < 1: # Check mass match
|
|
53
|
+
is_ready = True
|
|
54
|
+
|
|
55
|
+
if not is_ready:
|
|
56
|
+
new_label = new_label[:-1] # Shorten the label for the next attempt
|
|
57
|
+
|
|
58
|
+
if not new_label: # If the label is empty, try a last-resort approach
|
|
59
|
+
|
|
60
|
+
new_label = elements[np.argmin(np.abs(atomss - atomic_masses))]
|
|
61
|
+
|
|
62
|
+
if tried_last_resort:
|
|
63
|
+
raise ValueError("{} is not a valid element".format(new_label))
|
|
64
|
+
|
|
65
|
+
tried_last_resort = True
|
|
66
|
+
|
|
67
|
+
return new_label
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# return copy of input as list if not one
|
|
71
|
+
def as_list(inp):
|
|
72
|
+
if inp is None:
|
|
73
|
+
return []
|
|
74
|
+
elif isinstance(inp, int) or isinstance(inp, np.int64):
|
|
75
|
+
return [inp]
|
|
76
|
+
elif isinstance(inp, collections.abc.Iterable) and not isinstance(inp, str):
|
|
77
|
+
# Handles lists, tuples, NumPy arrays, etc. (Excludes strings)
|
|
78
|
+
return list(inp)
|
|
79
|
+
else:
|
|
80
|
+
return [inp] # prone to error?
|
|
81
|
+
# raise TypeError(f"Cannot convert type {type(inp)} to list")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def find_smallest_missing(data, start=0):
|
|
85
|
+
"""Finds the next smallest integer that is not in the list.
|
|
86
|
+
|
|
87
|
+
This function efficiently finds the next smallest integer that is not present in the input list.
|
|
88
|
+
It leverages sets for fast membership checks.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
data: A list of integers.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
The next smallest integer that is not in the list.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
data_set = set(data) # Convert the list to a set for efficient membership checks
|
|
98
|
+
smallest = start # Start with the smallest possible positive integer
|
|
99
|
+
while smallest in data_set: # Check if 'smallest' is in the set
|
|
100
|
+
smallest += 1 # If found, increment 'smallest'
|
|
101
|
+
return smallest # Return the first integer not found in the set
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def remove_inverted_tuples(list_of_tuples):
|
|
105
|
+
seen = set()
|
|
106
|
+
for i in range(len(list_of_tuples) - 1, -1, -1): # Iterate backwards
|
|
107
|
+
tup = list_of_tuples[i]
|
|
108
|
+
reversed_tup = tup[::-1]
|
|
109
|
+
if reversed_tup in seen:
|
|
110
|
+
del list_of_tuples[i] # Remove the tuple
|
|
111
|
+
else:
|
|
112
|
+
seen.add(tup)
|
|
113
|
+
|
|
114
|
+
# return list of indexes from mixed input of indexes and string (elements)
|
|
115
|
+
def atoms_to_indexes(system, symbols):
|
|
116
|
+
|
|
117
|
+
# check if symbols is a list of strings
|
|
118
|
+
if isinstance(symbols, str):
|
|
119
|
+
if symbols == 'all':
|
|
120
|
+
return list(range(len(system.get_chemical_symbols())))
|
|
121
|
+
|
|
122
|
+
symbols = as_list(symbols)
|
|
123
|
+
|
|
124
|
+
indexes = []
|
|
125
|
+
for symbol in symbols:
|
|
126
|
+
if not isinstance(symbol, str):
|
|
127
|
+
indexes.append(symbol)
|
|
128
|
+
else:
|
|
129
|
+
for cc, atom in enumerate(system.get_chemical_symbols()):
|
|
130
|
+
if atom == symbol:
|
|
131
|
+
indexes.append(cc)
|
|
132
|
+
return np.unique(indexes).tolist()
|
|
133
|
+
|
|
134
|
+
def chunker(seq, size):
|
|
135
|
+
return (seq[pos:pos + size] for pos in range(0, len(seq), size))
|
|
136
|
+
|
|
137
|
+
def get_nth_neighbors(graph, start_node, n):
|
|
138
|
+
visited = set()
|
|
139
|
+
queue = collections.deque([(start_node, 0)])
|
|
140
|
+
neighbors = []
|
|
141
|
+
|
|
142
|
+
while queue:
|
|
143
|
+
current_node, depth = queue.popleft()
|
|
144
|
+
if depth > n:
|
|
145
|
+
break
|
|
146
|
+
if current_node not in visited:
|
|
147
|
+
visited.add(current_node)
|
|
148
|
+
if depth > 0: # Exclude the start_node itself
|
|
149
|
+
neighbors.append(current_node)
|
|
150
|
+
for neighbor in graph.neighbors(current_node):
|
|
151
|
+
if neighbor not in visited:
|
|
152
|
+
queue.append((neighbor, depth + 1))
|
|
153
|
+
|
|
154
|
+
return neighbors
|
|
155
|
+
|
|
156
|
+
def molecule_to_graph(molecule, cutoff_scale=1.0):
|
|
157
|
+
|
|
158
|
+
# Generate cutoff
|
|
159
|
+
cutOff = cutoff_scale*np.array(neighborlist.natural_cutoffs(molecule))
|
|
160
|
+
ignore_atoms = "" # Assuming ignore_atoms is an empty string
|
|
161
|
+
cutOff[atoms_to_indexes(molecule, ignore_atoms)] = 0
|
|
162
|
+
|
|
163
|
+
# Calculate neighbor list
|
|
164
|
+
neighborList = neighborlist.NeighborList(cutOff, self_interaction=False, bothways=True)
|
|
165
|
+
neighborList.update(molecule)
|
|
166
|
+
|
|
167
|
+
# Check if it's a polymer
|
|
168
|
+
is_polymer = "is_connected" in molecule.arrays
|
|
169
|
+
|
|
170
|
+
# Generate graph
|
|
171
|
+
G = nx.Graph()
|
|
172
|
+
G.add_nodes_from(list(range(len(molecule))))
|
|
173
|
+
|
|
174
|
+
# Iterate through neighbors to add edges
|
|
175
|
+
for atom_index, bonded_atoms in enumerate(neighborList.nl.neighbors):
|
|
176
|
+
for neighbor_index in bonded_atoms:
|
|
177
|
+
if is_polymer:
|
|
178
|
+
# Skip if the atoms belong to different monomers and are not connected
|
|
179
|
+
if molecule.arrays["mon_id"][atom_index] != molecule.arrays["mon_id"][neighbor_index]:
|
|
180
|
+
if not (molecule.arrays["is_connected"][atom_index] and molecule.arrays["is_connected"][neighbor_index]):
|
|
181
|
+
continue
|
|
182
|
+
# Add edge between the atoms
|
|
183
|
+
G.add_edge(atom_index, neighbor_index)
|
|
184
|
+
|
|
185
|
+
return G
|
|
186
|
+
|
|
187
|
+
def find_atom_types(molecule, max_depth=1):
|
|
188
|
+
|
|
189
|
+
G = molecule_to_graph(molecule)
|
|
190
|
+
|
|
191
|
+
# Get chemical symbols
|
|
192
|
+
symbols = molecule.get_chemical_symbols()
|
|
193
|
+
|
|
194
|
+
# Create a dictionary to store the unique atom types and their IDs
|
|
195
|
+
atom_types = {}
|
|
196
|
+
type_id = 0
|
|
197
|
+
|
|
198
|
+
# Create an array to store the type ID of each atom
|
|
199
|
+
atom_type_ids = np.zeros(len(molecule), dtype=int)
|
|
200
|
+
|
|
201
|
+
# Iterate over each node in the graph
|
|
202
|
+
for node in G.nodes:
|
|
203
|
+
# Get the element of the current atom
|
|
204
|
+
element = symbols[node]
|
|
205
|
+
|
|
206
|
+
# Get the elements of the nth nearest neighboring atoms
|
|
207
|
+
nth_neighbors = get_nth_neighbors(G, node, max_depth)
|
|
208
|
+
neighbor_elements = sorted([symbols[neighbor] for neighbor in nth_neighbors])
|
|
209
|
+
|
|
210
|
+
# Create a unique identifier for the atom type
|
|
211
|
+
atom_type = (element, tuple(neighbor_elements))
|
|
212
|
+
|
|
213
|
+
# Assign an ID to the atom type if it is not already in the dictionary
|
|
214
|
+
if atom_type not in atom_types:
|
|
215
|
+
atom_types[atom_type] = type_id
|
|
216
|
+
type_id += 1
|
|
217
|
+
|
|
218
|
+
# Store the type ID in the array
|
|
219
|
+
atom_type_ids[node] = atom_types[atom_type]
|
|
220
|
+
|
|
221
|
+
return atom_type_ids, {v: k for k, v in atom_types.items()}
|
|
222
|
+
|
|
223
|
+
def find_unique_paths_of_length(graph, length):
|
|
224
|
+
def dfs(current_node, current_path):
|
|
225
|
+
if len(current_path) == length + 1:
|
|
226
|
+
# Check if the reverse of the path already exists
|
|
227
|
+
if tuple(current_path[::-1]) not in paths:
|
|
228
|
+
paths.add(tuple(current_path))
|
|
229
|
+
return
|
|
230
|
+
for neighbor in graph.neighbors(current_node):
|
|
231
|
+
if neighbor not in current_path: # Avoid cycles
|
|
232
|
+
dfs(neighbor, current_path + [neighbor])
|
|
233
|
+
|
|
234
|
+
paths = set()
|
|
235
|
+
for node in graph.nodes:
|
|
236
|
+
dfs(node, [node])
|
|
237
|
+
|
|
238
|
+
# Convert set of tuples back to list of lists
|
|
239
|
+
paths = [list(path) for path in paths]
|
|
240
|
+
|
|
241
|
+
# Sort paths lexicographically
|
|
242
|
+
paths.sort()
|
|
243
|
+
|
|
244
|
+
return np.array(paths, dtype=int)
|
|
245
|
+
|
|
246
|
+
def find_improper_idxs(graph):
|
|
247
|
+
nodes = [node for node, degree in dict(graph.degree()).items() if degree == 3]
|
|
248
|
+
|
|
249
|
+
# Find all nodes connected to those nodes
|
|
250
|
+
improper_idxs = []
|
|
251
|
+
for node in nodes:
|
|
252
|
+
improper_idxs.append(sorted([node,* graph.neighbors(node)]))
|
|
253
|
+
|
|
254
|
+
return improper_idxs
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def same_rev_check(list1, list2):
|
|
258
|
+
if list(list1) == list(list2):
|
|
259
|
+
return True
|
|
260
|
+
elif list(list1) == list(reversed(list2)):
|
|
261
|
+
return True
|
|
262
|
+
return False
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Created on Mon Aug 5 17:13:50 2024
|
|
5
|
+
|
|
6
|
+
@author: roncofaber
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
# internal modules
|
|
12
|
+
from mdinterface.core.specie import Specie
|
|
13
|
+
from mdinterface.core.topology import Bond, Angle, Atom, Dihedral, Improper
|
|
14
|
+
|
|
15
|
+
import ase.build
|
|
16
|
+
|
|
17
|
+
#%%
|
|
18
|
+
|
|
19
|
+
# graphene https://onlinelibrary.wiley.com/doi/10.1002/adma.201705791
|
|
20
|
+
class Graphene(Specie):
|
|
21
|
+
def __init__(self, **kwargs):
|
|
22
|
+
system = ase.build.graphene()
|
|
23
|
+
system.cell[-1][-1] = 3.35
|
|
24
|
+
g_b = Bond("C", "C", kr=469, r0=1.4)
|
|
25
|
+
g_d = Dihedral("C", "C", "C", "C", A1=7.25, A2=0, A3=-7.25, A4=0, A5=0)
|
|
26
|
+
g_i = Improper(a1="C", K=1.1, d=-1, n=2)
|
|
27
|
+
g_a = Angle("C", "C", "C", kr=63, theta0=120)
|
|
28
|
+
lj = {"C": [0.07, 3.54996412]}
|
|
29
|
+
|
|
30
|
+
super().__init__(system, bonds=g_b, dihedrals=g_d, impropers=g_i, angles=g_a, lj=lj, **kwargs)
|
|
31
|
+
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
# solvent https://docs.lammps.org/Howto_tip3p.html (Ewald model)
|
|
35
|
+
class Water(Specie):
|
|
36
|
+
def __init__(self, model="ewald", **kwargs):
|
|
37
|
+
|
|
38
|
+
if model.lower() == "ewald":
|
|
39
|
+
b1 = Bond("O", "H", kr=450, r0=0.9572)
|
|
40
|
+
a1 = Angle("H", "O", "H", kr=55, theta0=104.52)
|
|
41
|
+
charges = [-0.83, 0.415, 0.415]
|
|
42
|
+
lj = {"O": [0.102, 3.188], "H": [0.0, 1.0]}
|
|
43
|
+
|
|
44
|
+
super().__init__("H2O", charges=charges, bonds=b1, angles=a1, lj=lj, **kwargs)
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
#oxygen https://pubs.acs.org/doi/10.1021/acs.jctc.0c01132 /!\: divide sig by 2**(1/6)
|
|
49
|
+
class Oxygen(Specie):
|
|
50
|
+
def __init__(self, **kwargs):
|
|
51
|
+
|
|
52
|
+
b1 = Bond("O", "O", kr=1640.4, r0=1.2074)
|
|
53
|
+
lj = {"O" : [0.1047, 2.9373]}
|
|
54
|
+
|
|
55
|
+
super().__init__("O2", charges = 0.0, lj=lj, bonds=b1, **kwargs)
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
#hydrogen https://pubs.acs.org/doi/10.1021/acs.jctc.0c01132 /!\: divide sig by 2**(1/6)
|
|
59
|
+
class Hydrogen(Specie):
|
|
60
|
+
def __init__(self, Hset="std", **kwargs):
|
|
61
|
+
|
|
62
|
+
b1 = Bond("H", "H", kr=700, r0=0.7414)
|
|
63
|
+
|
|
64
|
+
if Hset.lower() == "std": # standard 12-6 set
|
|
65
|
+
lj = {"H" : [0.0153, 2.5996]}
|
|
66
|
+
elif Hset.lower() == "alt": # alternative 12-6 set
|
|
67
|
+
lj = {"H" : [0.0145, 2.8001]}
|
|
68
|
+
|
|
69
|
+
super().__init__("H2", charges = 0.0, lj=lj, bonds=b1, **kwargs)
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
#nitrogen https://pubs.acs.org/doi/10.1021/acs.jctc.0c01132 /!\: divide sig by 2**(1/6)
|
|
73
|
+
class Nitrogen(Specie):
|
|
74
|
+
def __init__(self, **kwargs):
|
|
75
|
+
|
|
76
|
+
b1 = Bond("N", "N", kr=3190, r0=1.0977)
|
|
77
|
+
lj = {"N" : [0.0797, 3.2197]}
|
|
78
|
+
|
|
79
|
+
super().__init__("N2", charges = 0.0, lj=lj, bonds=b1, **kwargs)
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
#perchlorate https://pubs.acs.org/doi/full/10.1021/jp801280s
|
|
83
|
+
class Perchlorate(Specie):
|
|
84
|
+
def __init__(self, **kwargs):
|
|
85
|
+
|
|
86
|
+
# Bond length
|
|
87
|
+
bl = 1.506
|
|
88
|
+
|
|
89
|
+
# Coordinates of ClO4^- anion
|
|
90
|
+
coordinates = [
|
|
91
|
+
(0.0, 0.0, 0.0), # Chlorine atom at the origin
|
|
92
|
+
(bl * np.sqrt(8/9), 0.0, -bl / 3), # Oxygen atom 1
|
|
93
|
+
(-bl * np.sqrt(2/9), bl * np.sqrt(2/3), -bl / 3), # Oxygen atom 2
|
|
94
|
+
(-bl * np.sqrt(2/9), -bl * np.sqrt(2/3), -bl / 3), # Oxygen atom 3
|
|
95
|
+
(0.0, 0.0, bl) # Oxygen atom 4
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
charges = [1.176, -0.544, -0.544, -0.544, -0.544]
|
|
99
|
+
|
|
100
|
+
pclo = ase.Atoms("ClO4", positions=coordinates, charges=charges)
|
|
101
|
+
|
|
102
|
+
b1 = Bond("Cl", "O", kr=757.286, r0=1.506)
|
|
103
|
+
b2 = Bond("O", "O", kr=33.445, r0=2.459)
|
|
104
|
+
a1 = Angle("O", "Cl", "O", kr=207.9, theta0=109.5)
|
|
105
|
+
lj = {
|
|
106
|
+
"Cl" : [0.1177, 3.5000],
|
|
107
|
+
"O" : [0.2099, 2.9000]
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
super().__init__(atoms=pclo, lj=lj, bonds=[b1, b2], angles=a1, cutoff=1.5, **kwargs)
|
|
111
|
+
return
|
mdinterface/utils/map.py
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Created on Mon Oct 28 11:30:04 2024
|
|
5
|
+
|
|
6
|
+
@author: roncofaber
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import mdinterface.utils.auxiliary as aux
|
|
11
|
+
from mdinterface.core.topology import Bond, Angle, Dihedral, Improper
|
|
12
|
+
|
|
13
|
+
#%%
|
|
14
|
+
|
|
15
|
+
def map_atoms(atoms):
|
|
16
|
+
|
|
17
|
+
# Create an array to store the type ID of each atom
|
|
18
|
+
atom_type_ids = []
|
|
19
|
+
type_id = 0
|
|
20
|
+
atoms_map = {}
|
|
21
|
+
atoms_list = []
|
|
22
|
+
|
|
23
|
+
for cc, atom in enumerate(atoms):
|
|
24
|
+
if atom not in atoms_list:
|
|
25
|
+
atom_type_ids.append(atom.label)
|
|
26
|
+
atoms_list.append(atom)
|
|
27
|
+
atoms_map[atom.label] = type_id
|
|
28
|
+
type_id += 1
|
|
29
|
+
|
|
30
|
+
else:
|
|
31
|
+
idx = atoms_list.index(atom)
|
|
32
|
+
atom_type_ids.append(atom.label)
|
|
33
|
+
atoms_map[atom.label] = idx
|
|
34
|
+
|
|
35
|
+
atom_type_ids = np.array(atom_type_ids)
|
|
36
|
+
|
|
37
|
+
return atoms_list, atoms_map, atom_type_ids
|
|
38
|
+
|
|
39
|
+
def map_bonds(bonds):
|
|
40
|
+
bond_type_ids = []
|
|
41
|
+
type_id = 0
|
|
42
|
+
bond_map = {}
|
|
43
|
+
bonds_list = []
|
|
44
|
+
|
|
45
|
+
for cc, bond in enumerate(bonds):
|
|
46
|
+
if bond not in bonds_list:
|
|
47
|
+
bond_type_ids.append(type_id)
|
|
48
|
+
bonds_list.append(bond)
|
|
49
|
+
bond_map[bond.symbols] = type_id
|
|
50
|
+
type_id += 1
|
|
51
|
+
else:
|
|
52
|
+
idx = bonds_list.index(bond)
|
|
53
|
+
bond_type_ids.append(idx)
|
|
54
|
+
bond_map[bond.symbols] = idx
|
|
55
|
+
|
|
56
|
+
bond_type_ids = np.array(bond_type_ids)
|
|
57
|
+
return bonds_list, bond_map, bond_type_ids
|
|
58
|
+
|
|
59
|
+
def map_angles(angles):
|
|
60
|
+
angle_type_ids = []
|
|
61
|
+
type_id = 0
|
|
62
|
+
angle_map = {}
|
|
63
|
+
angles_list = []
|
|
64
|
+
|
|
65
|
+
for cc, angle in enumerate(angles):
|
|
66
|
+
if angle not in angles_list:
|
|
67
|
+
angle_type_ids.append(type_id)
|
|
68
|
+
angles_list.append(angle)
|
|
69
|
+
angle_map[angle.symbols] = type_id
|
|
70
|
+
type_id += 1
|
|
71
|
+
else:
|
|
72
|
+
idx = angles_list.index(angle)
|
|
73
|
+
angle_type_ids.append(idx)
|
|
74
|
+
angle_map[angle.symbols] = idx
|
|
75
|
+
|
|
76
|
+
angle_type_ids = np.array(angle_type_ids)
|
|
77
|
+
return angles_list, angle_map, angle_type_ids
|
|
78
|
+
|
|
79
|
+
def map_dihedrals(dihedrals):
|
|
80
|
+
dihedral_type_ids = []
|
|
81
|
+
type_id = 0
|
|
82
|
+
dihedral_map = {}
|
|
83
|
+
dihedrals_list = []
|
|
84
|
+
|
|
85
|
+
for cc, dihedral in enumerate(dihedrals):
|
|
86
|
+
if dihedral not in dihedrals_list:
|
|
87
|
+
dihedral_type_ids.append(type_id)
|
|
88
|
+
dihedrals_list.append(dihedral)
|
|
89
|
+
dihedral_map[dihedral.symbols] = type_id
|
|
90
|
+
type_id += 1
|
|
91
|
+
else:
|
|
92
|
+
idx = dihedrals_list.index(dihedral)
|
|
93
|
+
dihedral_type_ids.append(idx)
|
|
94
|
+
dihedral_map[dihedral.symbols] = idx
|
|
95
|
+
|
|
96
|
+
dihedral_type_ids = np.array(dihedral_type_ids)
|
|
97
|
+
return dihedrals_list, dihedral_map, dihedral_type_ids
|
|
98
|
+
|
|
99
|
+
def map_impropers(impropers):
|
|
100
|
+
|
|
101
|
+
if impropers is None:
|
|
102
|
+
return None, None
|
|
103
|
+
|
|
104
|
+
improper_type_ids = []
|
|
105
|
+
type_id = 0
|
|
106
|
+
improper_map = {}
|
|
107
|
+
impropers_list = []
|
|
108
|
+
|
|
109
|
+
for cc, improper in enumerate(impropers):
|
|
110
|
+
if improper not in impropers_list:
|
|
111
|
+
improper_type_ids.append(type_id)
|
|
112
|
+
impropers_list.append(improper)
|
|
113
|
+
improper_map[improper.symbols] = type_id
|
|
114
|
+
type_id += 1
|
|
115
|
+
else:
|
|
116
|
+
idx = impropers_list.index(improper)
|
|
117
|
+
improper_type_ids.append(idx)
|
|
118
|
+
improper_map[improper.symbols] = idx
|
|
119
|
+
|
|
120
|
+
improper_type_ids = np.array(improper_type_ids)
|
|
121
|
+
return impropers_list, improper_map, improper_type_ids
|
|
122
|
+
|
|
123
|
+
#%%
|
|
124
|
+
|
|
125
|
+
def find_missing_bonds(nas):
|
|
126
|
+
tmp_bonds, _ = nas.bonds
|
|
127
|
+
all_bonds = aux.find_unique_paths_of_length(nas.graph, 1)
|
|
128
|
+
|
|
129
|
+
# Convert tmp_bonds to a set of tuples for efficient membership checking
|
|
130
|
+
tmp_bonds_set = set(tuple(bond) for bond in tmp_bonds)
|
|
131
|
+
tmp_bonds_set.update(tuple(reversed(bond)) for bond in tmp_bonds)
|
|
132
|
+
|
|
133
|
+
missing_bonds = []
|
|
134
|
+
for tmp_bond in all_bonds:
|
|
135
|
+
tmp_bond_tuple = tuple(tmp_bond)
|
|
136
|
+
tmp_bond_tuple_rev = tuple(reversed(tmp_bond_tuple))
|
|
137
|
+
if tmp_bond_tuple not in tmp_bonds_set and tmp_bond_tuple_rev not in tmp_bonds_set:
|
|
138
|
+
missing_bonds.append(tuple(nas._sids[ii] for ii in tmp_bond_tuple))
|
|
139
|
+
|
|
140
|
+
return missing_bonds
|
|
141
|
+
|
|
142
|
+
def find_missing_angles(nas):
|
|
143
|
+
tmp_angles, _ = nas.angles
|
|
144
|
+
all_angles = aux.find_unique_paths_of_length(nas.graph, 2)
|
|
145
|
+
|
|
146
|
+
# Convert tmp_angles to a set of tuples for efficient membership checking
|
|
147
|
+
tmp_angles_set = set(tuple(angle) for angle in tmp_angles)
|
|
148
|
+
tmp_angles_set.update(tuple(reversed(angle)) for angle in tmp_angles)
|
|
149
|
+
|
|
150
|
+
missing = []
|
|
151
|
+
for tmp_ang in all_angles:
|
|
152
|
+
tmp_ang_tuple = tuple(tmp_ang)
|
|
153
|
+
tmp_ang_tuple_rev = tuple(reversed(tmp_ang_tuple))
|
|
154
|
+
if tmp_ang_tuple not in tmp_angles_set and tmp_ang_tuple_rev not in tmp_angles_set:
|
|
155
|
+
missing.append(tuple(nas._sids[ii] for ii in tmp_ang_tuple))
|
|
156
|
+
return missing
|
|
157
|
+
|
|
158
|
+
def find_missing_dihedrals(nas):
|
|
159
|
+
tmp_dihedrals, _ = nas.dihedrals
|
|
160
|
+
all_dihedrals = aux.find_unique_paths_of_length(nas.graph, 3)
|
|
161
|
+
|
|
162
|
+
# Convert tmp_dihedrals to a set of tuples for efficient membership checking
|
|
163
|
+
tmp_dihedrals_set = set(tuple(dihedral) for dihedral in tmp_dihedrals)
|
|
164
|
+
tmp_dihedrals_set.update(tuple(reversed(dihedral)) for dihedral in tmp_dihedrals)
|
|
165
|
+
|
|
166
|
+
missing_dihedrals = []
|
|
167
|
+
for tmp_dihedral in all_dihedrals:
|
|
168
|
+
tmp_dihedral_tuple = tuple(tmp_dihedral)
|
|
169
|
+
tmp_dihedral_tuple_rev = tuple(reversed(tmp_dihedral_tuple))
|
|
170
|
+
if tmp_dihedral_tuple not in tmp_dihedrals_set and tmp_dihedral_tuple_rev not in tmp_dihedrals_set:
|
|
171
|
+
missing_dihedrals.append(tuple(nas._sids[ii] for ii in tmp_dihedral_tuple))
|
|
172
|
+
|
|
173
|
+
return missing_dihedrals
|
|
174
|
+
|
|
175
|
+
def find_missing_impropers(nas):
|
|
176
|
+
tmp_impropers, _ = nas.impropers
|
|
177
|
+
all_impropers = aux.find_unique_paths_of_length(nas.graph, 3) # Assuming path length 3 for impropers
|
|
178
|
+
|
|
179
|
+
# Convert tmp_impropers to a set of tuples for efficient membership checking
|
|
180
|
+
tmp_impropers_set = set(tuple(improper) for improper in tmp_impropers)
|
|
181
|
+
tmp_impropers_set.update(tuple(reversed(improper)) for improper in tmp_impropers)
|
|
182
|
+
|
|
183
|
+
missing_impropers = []
|
|
184
|
+
for tmp_improper in all_impropers:
|
|
185
|
+
tmp_improper_tuple = tuple(tmp_improper)
|
|
186
|
+
tmp_improper_tuple_rev = tuple(reversed(tmp_improper_tuple))
|
|
187
|
+
if tmp_improper_tuple not in tmp_impropers_set and tmp_improper_tuple_rev not in tmp_impropers_set:
|
|
188
|
+
missing_impropers.append(tuple(nas._sids[ii] for ii in tmp_improper_tuple))
|
|
189
|
+
|
|
190
|
+
return missing_impropers
|
|
191
|
+
|
|
192
|
+
#%%
|
|
193
|
+
|
|
194
|
+
def generate_missing_interactions(nas, interaction_type):
|
|
195
|
+
mss_interactions = nas.suggest_missing_interactions(interaction_type)
|
|
196
|
+
|
|
197
|
+
new_interactions = []
|
|
198
|
+
interaction_type_map = {}
|
|
199
|
+
|
|
200
|
+
# Determine the appropriate attributes based on the interaction type
|
|
201
|
+
if interaction_type == "bonds":
|
|
202
|
+
interaction_list = nas._btype
|
|
203
|
+
num_atoms = 2
|
|
204
|
+
elif interaction_type == "angles":
|
|
205
|
+
interaction_list = nas._atype
|
|
206
|
+
num_atoms = 3
|
|
207
|
+
elif interaction_type == "dihedrals":
|
|
208
|
+
interaction_list = nas._dtype
|
|
209
|
+
num_atoms = 4
|
|
210
|
+
elif interaction_type == "impropers":
|
|
211
|
+
interaction_list = nas._itype
|
|
212
|
+
num_atoms = 4
|
|
213
|
+
else:
|
|
214
|
+
raise ValueError("Invalid interaction type")
|
|
215
|
+
|
|
216
|
+
# Create a mapping of interaction types for faster lookup
|
|
217
|
+
for itype in interaction_list:
|
|
218
|
+
ctypes = tuple(nas._smap[ii] for ii in itype.symbols)
|
|
219
|
+
interaction_type_map[ctypes] = itype
|
|
220
|
+
interaction_type_map[ctypes[::-1]] = itype # Add the reversed tuple as well
|
|
221
|
+
|
|
222
|
+
for mss_interaction in mss_interactions:
|
|
223
|
+
found_interaction = False
|
|
224
|
+
stypes = tuple(nas._smap[ii] for ii in mss_interaction)
|
|
225
|
+
stypes_rev = stypes[::-1]
|
|
226
|
+
|
|
227
|
+
# topology attribute already existing in map
|
|
228
|
+
if stypes in interaction_type_map or stypes_rev in interaction_type_map:
|
|
229
|
+
if found_interaction:
|
|
230
|
+
raise ValueError(f"More than one possible {interaction_type[:-1]} found, abort!")
|
|
231
|
+
|
|
232
|
+
if stypes in interaction_type_map:
|
|
233
|
+
ninteraction = interaction_type_map[stypes].copy()
|
|
234
|
+
|
|
235
|
+
else:
|
|
236
|
+
ninteraction = interaction_type_map[stypes_rev].copy()
|
|
237
|
+
mss_interaction = mss_interaction[::-1]
|
|
238
|
+
|
|
239
|
+
a1, a2, *rest = mss_interaction
|
|
240
|
+
|
|
241
|
+
ninteraction._a1 = a1
|
|
242
|
+
ninteraction._a2 = a2
|
|
243
|
+
|
|
244
|
+
if num_atoms > 2:
|
|
245
|
+
ninteraction._a3 = rest[0]
|
|
246
|
+
if num_atoms > 3:
|
|
247
|
+
ninteraction._a4 = rest[1]
|
|
248
|
+
new_interactions.append(ninteraction)
|
|
249
|
+
found_interaction = True
|
|
250
|
+
|
|
251
|
+
# new topology attribute
|
|
252
|
+
else:
|
|
253
|
+
if found_interaction:
|
|
254
|
+
raise ValueError(f"More than one possible {interaction_type[:-1]} found, abort!")
|
|
255
|
+
|
|
256
|
+
a1, a2, *rest = mss_interaction
|
|
257
|
+
|
|
258
|
+
if num_atoms == 2:
|
|
259
|
+
ninteraction = Bond(a1, a2)
|
|
260
|
+
elif num_atoms == 3:
|
|
261
|
+
ninteraction = Angle(a1, a2, rest[0])
|
|
262
|
+
elif num_atoms == 4:
|
|
263
|
+
ninteraction = Dihedral(a1, a2, rest[0], rest[1])
|
|
264
|
+
|
|
265
|
+
new_interactions.append(ninteraction)
|
|
266
|
+
found_interaction = True
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
return new_interactions
|