-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathfunctions.py
More file actions
318 lines (237 loc) · 9.35 KB
/
functions.py
File metadata and controls
318 lines (237 loc) · 9.35 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
"""
Functions used to facilitate Rotapy.
plot_structure: Plots the structure of a given Molecule in 3d.
center_on_atom: Translates the coordinates of all atoms in the mo so that
the selected atom is at the origin.
rotate_point_around_vector: Rotate a point around a selected vector by some number of degrees.
Uses a quaternion-ic rotation to avoid gimbal lock, and for ease of coding.
bonded_atom_search: Takes in a network and returns all nodes with a path to the starting atom,
excluding any blocked nodes.
verified_input: Verify that the user has input a value which can be converted to a specified type.
This function will not return until the user has input something which can be converted
to the type specified by 'verify'
"""
from os import getcwd, makedirs, path
import matplotlib
import matplotlib.pyplot as plt
import numpy as np
import pyquaternion as pq
from classes import Atom, Molecule
def generate_figure(mo: Molecule):
"""
Generate a figure of the molecule and bonds
Parameters
----------
mo: The molecule to be modeled
Return
------
fig: The matplotlib figure of the molecule.
ax: The matplotlib axes of the molecule.
"""
# Update the bond graph
mo.make_bond_graph()
# Make an empty, square, pseudo-3d figure
dpi = 300
pixels = 800
fig = plt.figure(figsize=(pixels / dpi, pixels / dpi), dpi=dpi, tight_layout=True)
ax = fig.add_subplot(projection="3d")
# Hide axis planes and axis lines
ax.set_axis_off()
# Relative size scalar for the atoms.
size = pixels / dpi * 50
# Bonds are slightly see through and gray.
alpha = 0.90
line_color = '0.5'
# Draw atoms as circles
for num, a in enumerate(mo.atoms):
x = a.pos[0]
y = a.pos[1]
z = a.pos[2]
# Size scales with cov_radius
ax.scatter(x, y, z, color=a.color, edgecolor=line_color, s=size * a.cov_radius)
# Number the as.
text_color = tuple(
1 - a for a in a.color
) # The text color is the negative of the color of the a
ax.text(x, y, z, num,
zorder=100,
color=text_color,
ha="center", # Horizontal Alignment
va="center", # Vertical Alignment
fontsize=size * a.cov_radius / 16,
)
# Draw bonds as lines
# This will draw duplicate lines on top of each other.
# For example, if two atoms are bonded to each other,
# there will be one bond from A to B, and another from B to A.
# This may be a problem with very large molecules.
for a in mo.bonds:
x1 = a.pos[0]
y1 = a.pos[1]
z1 = a.pos[2]
for bonded_a in mo.bonds[a]:
x2 = bonded_a.pos[0]
y2 = bonded_a.pos[1]
z2 = bonded_a.pos[2]
ax.plot((x1, x2), (y1, y2), (z1, z2), color=line_color, alpha=alpha)
# One liner used to force the axes to be equal in step width.
# For example, (0,1) may appear 20px in length, while (1,0) may appear 10px in length.
# With this line, they will both be length 20.
# I don't know how this works. I found it here:
# https://github.com/matplotlib/matplotlib/issues/17172#issuecomment-830139107
ax.set_box_aspect([ub - lb for lb, ub in (getattr(ax, f"get_{a}lim")() for a in "xyz")])
return fig, ax
def show_structure(mo, title: str = None):
"""Show the structure of a molecule in an interactive plot"""
matplotlib.use('tkAgg')
fig, ax = generate_figure(mo)
plt.title(title if title else mo.name, fontsize=5)
plt.show()
plt.close('all')
def save_structure(mo, title: str = None, directory: str = ""):
"""Save the structure of a molecule to a png file"""
matplotlib.use('agg')
fig, ax = generate_figure(mo)
directory = f"{directory}/{title if title else mo.name}.png"
plt.title(title if title else mo.name, fontsize=5)
plt.savefig(directory)
plt.close('all')
def center_on_atom(mo: Molecule, atom_number: int) -> Molecule:
"""
Translates the coordinates of all atoms in the mo so that
the selected atom is at the origin.
Parameters
----------
mo: The molecule to be shifted
atom_number: The index of the atom in mo.atoms to be designated as the new center atom.
Returns
-------
mo: A copy of the molecule shifted so that the atom indicated by atom_number is at (0,0,0).
"""
selected_atom = mo.atoms[atom_number]
dx = selected_atom.pos[0]
dy = selected_atom.pos[1]
dz = selected_atom.pos[2]
new_atoms = list()
for at in mo.atoms:
new_position = (at.pos[0] - dx, at.pos[1] - dy, at.pos[2] - dz)
new_at = Atom(name=at.name, pos=new_position)
new_atoms.append(new_at)
new_mo = Molecule(name=mo.name, atoms=new_atoms)
return new_mo
def rotate_point_around_vector(point: tuple[float, float, float],
vector: tuple[float, float, float],
deg: float) -> tuple[float, float, float]:
"""
Rotate a point around a selected vector by some number of degrees.
Uses a quaternion-ic rotation to avoid gimbal lock, and for ease of coding.
Parameters
----------
point: The (x, y, z) point to be rotated.
vector: The (x, y, z) vector to be used as a rotation axis. Center must be on the origin.
Does not need to be unit size.
deg: The angle to which the point will be rotated. Has units of degrees.
Returns
-------
new_vec: A tuple containing the (x, y, z) cartesian coordinates of the rotated point
"""
# This quaternion object will perform the rotation encoded into its initialization.
# The rotation vector is of unit length.
rotation_quaternion = pq.Quaternion(axis=vector, degrees=deg)
# Perform the rotation
new_vec = rotation_quaternion.rotate(point)
return new_vec
def bonded_atom_search(molecule: Molecule, start: Atom, wall: list) -> list:
"""
Takes in a network and returns all nodes with a path to the starting atom,
excluding any blocked nodes.
Parameters
----------
molecule: A Molecule object
start: An atom in the molecule that we want to find all the bonded atoms of
wall: A list of atoms which are not to be included in the search.
Returns
-------
important: The list of atoms which have a path back to the start atom,
excluding any paths through the wall atoms.
"""
molecule.make_bond_graph()
bonded = lambda x: molecule.bonds[x]
important = [start]
for atom in important:
bonds = bonded(atom)
for bond in bonds:
if bond not in wall and (bond not in important):
important.append(bond)
return important
def verified_input(prompt: str = "", verify: type = int):
"""
Verify that the user has input a value which can be converted to a specified type.
This function will not return until the user has input something which can be converted
to the type specified by 'verify'
Parameters
----------
prompt: The prompt for input for the user.
verify: The type for the input to be returned as
Returns
-------
data: Input from the user with the type, verify.
"""
while True:
data = input(prompt)
try:
data = verify(data)
break
except ValueError:
print(f"Error: Must be of type {verify.__name__}")
return data
def check_bonds(m1, m2):
"""Checks whether the two molecules have the same bonding structure
If the bond count lists are not exactly equal, a collision occurred
Only a very complex scenario would really defeat this detection method.
A complex problem == complex solution, thus, I procrastinate.
"""
# Update bond graphs
m1.make_bond_graph()
m2.make_bond_graph()
# Makes a list of the bond counts for each atom
m1struct = [len(m1.bonds[b]) for b in m1.bonds]
m2struct = [len(m2.bonds[b]) for b in m2.bonds]
m1struct.sort()
m2struct.sort()
bond_diff = abs(sum(m1struct) - sum(m2struct)) // 2
return bond_diff
def randomly_orient(mo):
"""Randomly orient a given molecule in 3d space using uniform distributions"""
mo = center_on_atom(mo, 0)
new_mo = Molecule(name=mo.name, atoms=list())
center_on_atom(mo, 0)
rand_axis = tuple(np.random.uniform(size=(3,)))
rand_deg = np.random.uniform() * 360
for atom in mo.atoms:
new_atom_pos = rotate_point_around_vector(point=atom.pos, vector=rand_axis, deg=rand_deg)
new_atom = Atom(atom.name, new_atom_pos)
new_mo.add_atom(new_atom)
return new_mo
def make_output_folder(sub: str = "") -> str:
"""
Makes a directory in the script location to output the downloaded files
Parameters
----------
sub: The name of the directory to be made.
Returns
-------
dir_path: The directory pointing to :sub:
"""
# Finds the current directory
dir_path = getcwd()
# Makes the path for the new folder
dir_path = dir_path + fr"\{sub}"
# If the folder doesn't exist, make it.
if not path.exists(dir_path):
try:
makedirs(dir_path)
except FileExistsError:
# Sometimes this error pops when using threading or multiprocessing.
pass
return dir_path