writhe-tools 0.0.1__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.
File without changes
@@ -0,0 +1,428 @@
1
+ import numpy as np
2
+ import mdtraj as md
3
+ import ray
4
+ import warnings
5
+ import multiprocessing
6
+ from .utils.filing import save_dict, load_dict
7
+ from .utils.misc import to_numpy
8
+ from .utils.indexing import (triu_flat_indices,
9
+ combinations,
10
+ product,)
11
+ from .utils.sorting import filter_strs, lsdir
12
+ from .plots import plot_distance_matrix, build_grid_plot
13
+ from numba import njit, prange
14
+
15
+
16
+ @njit
17
+ def norm(a):
18
+ return np.sqrt(a[:, 0] * a[:, 0] + a[:, 1] * a[:,1] + a[:, 2] * a[:, 2])
19
+
20
+ @njit
21
+ def distance_pair(xyz, pair, length=None):
22
+ displacements = xyz[:,pair[1]] - xyz[:,pair[0]]
23
+ # if we have periodic boundary conditions
24
+ if length is not None:
25
+ displacements -= length * np.round(displacements / length)
26
+ return norm(displacements) # return only the Euclidean distance
27
+
28
+
29
+ @njit
30
+ def parallel_distances(xyz, pairs, length=None):
31
+ distances = np.zeros((len(pairs),len(xyz)))
32
+ for i in prange(len(pairs)):
33
+
34
+ distances[i] = distance_pair(xyz, pairs[i], length)
35
+ return distances
36
+
37
+
38
+ class ResidueDistances:
39
+ def __init__(self,
40
+ index_0: np.ndarray = None,
41
+ traj=None,
42
+ index_1: np.ndarray = None,
43
+ chain_id_0: str = None,
44
+ chain_id_1: str = None,
45
+ contact_cutoff=0.5,
46
+ args: "dict or path (str) to saved dict" = None):
47
+
48
+ # restore the class from dictionary that it saves
49
+ if args is not None:
50
+ if isinstance(args, str):
51
+ args = load_dict(args)
52
+ assert isinstance(args, dict), "args must be dict or path to saved dict"
53
+ self.__dict__.update(args)
54
+
55
+ # set up new instance, compute distances
56
+ else:
57
+ assert all(arg is not None for arg in (index_0, traj)), (
58
+ "Minimum inputs are MDTraj object and index_0 (residue indices array)"
59
+ " if an args dict saved by this class is not provided")
60
+ self.contact_cutoff = contact_cutoff
61
+
62
+ if index_1 is None:
63
+ self.chain_id_0, self.chain_id_1 = [chain_id_0] * 2
64
+ self.residues_0, self.residues_1 = [get_residues(traj, index_0)] * 2
65
+ self.n = len(index_0)
66
+ self.m = None
67
+ self.prefix = "Intra"
68
+ self.distances = residue_distances(traj, index_0)[0]
69
+
70
+ else:
71
+ self.chain_id_0, self.chain_id_1 = chain_id_0, chain_id_1
72
+ self.residues_0, self.residues_1 = get_residues(traj, [index_0, index_1])
73
+ self.n, self.m = map(len, (index_0, index_1))
74
+ self.prefix = "Inter"
75
+ self.distances = residue_distances(traj, index_0, index_1)[0]
76
+
77
+ pass
78
+
79
+ def save(self, file: str):
80
+ args = {attr: getattr(self, attr) for attr in filter(lambda x: hasattr(self, x),
81
+ ["distances", "index_0", "index_1",
82
+ "chain_id_0", "chain_id_1", "residues_0",
83
+ "residues_1", "n", "m", "prefix",
84
+ "contact_cutoff"])}
85
+ save_dict(file, args)
86
+
87
+ return None
88
+
89
+ @classmethod
90
+ def load(cls, file):
91
+ return cls(args=load_dict(file))
92
+
93
+ def sub_diag(self, d):
94
+ assert self.prefix == "Intra", "Must be intra molecular distances to subsample distances"
95
+ return self.distances[:, triu_flat_indices(self.n, 1, d)]
96
+
97
+ def matrix(self, contacts: bool = False,
98
+ cut_off: float = None,
99
+ distances: np.ndarray=None):
100
+
101
+ distances = self.distances if distances is None else distances
102
+
103
+ cut_off = self.contact_cutoff if cut_off is None else cut_off
104
+
105
+ if contacts:
106
+ assert cut_off is not None, "Must provide contact cutoff in init statement or as an argument"
107
+ return to_contact_matrix(to_distance_matrix(distances, self.n, self.m),
108
+ cut_off=cut_off)
109
+ else:
110
+ return to_distance_matrix(distances, self.n, self.m)
111
+
112
+ def plot(self, index: "list, int, str" = None,
113
+ contacts: bool = False,
114
+ contact_cut_off: float = None,
115
+ dscr: str = "",
116
+ label_stride: int = 3,
117
+ font_scale: int = 1,
118
+ cmap: str = "jet",
119
+ line_plot_args: dict = None,
120
+ ax=None):
121
+
122
+ if contacts:
123
+ __dtype = "Contacts"
124
+ unit = ""
125
+ else:
126
+ __dtype = "Distances"
127
+ unit = " (nm)"
128
+
129
+ if index is None:
130
+ matrix = self.matrix(contacts,
131
+ contact_cut_off,
132
+ distances=self.distances.mean(0))
133
+ __stype = "Average "
134
+
135
+ elif isinstance(index, (int, float)):
136
+ index = int(index)
137
+ matrix = self.matrix(contacts,
138
+ contact_cut_off,
139
+ self.distances[index])
140
+ __stype = ""
141
+ dscr = f"Frame {index}"
142
+
143
+ else:
144
+ index = to_numpy(index).astype(int)
145
+ matrix = self.matrix(contacts,
146
+ contact_cut_off,
147
+ self.distances[index].mean(0))
148
+ __stype = "Average "
149
+
150
+ if dscr == "":
151
+ warnings.warn(("Taking the average over a subset of indices."
152
+ "The option, 'dscr', (type:str) should be set to provide \
153
+ a description of the indices. "
154
+ "Otherwise, the plotted data is ambiguous"))
155
+
156
+ if dscr != "":
157
+ dscr = f" : {dscr}"
158
+
159
+ title = f"{__stype}{self.prefix}molecular {__dtype}{dscr}"
160
+ cbar_label = f"{__stype}{__dtype}{unit}"
161
+
162
+ matrix_plot_args = {"matrix": matrix,
163
+ "ylabel": self.chain_id_0,
164
+ "yticks": self.residues_0,
165
+ "xlabel": self.chain_id_1,
166
+ "xticks": self.residues_1,
167
+ "title": title,
168
+ "cbar_label": cbar_label,
169
+ "label_stride": label_stride,
170
+ "font_scale": font_scale,
171
+ "ax": ax,
172
+ "cmap": cmap}
173
+
174
+ if line_plot_args is None:
175
+ return plot_distance_matrix(**matrix_plot_args)
176
+
177
+ else:
178
+ line_plot_args["xticks"] = self.residues_1
179
+ line_plot_args["x"] = np.arange(len(self.residues_1))
180
+ line_plot_args["font_scale"] = 1
181
+ line_plot_args["hide_title"] = True
182
+
183
+ matrix_plot_args["font_scale"] = 1.2
184
+ matrix_plot_args["hide_x"] = True
185
+ matrix_plot_args["xlabel"] = None
186
+ matrix_plot_args["xticks"] = None
187
+ matrix_plot_args["aspect"] = "auto"
188
+
189
+ build_grid_plot(matrix_plot_args, line_plot_args)
190
+
191
+ return
192
+
193
+ def rmsd_sort(indices: np.ndarray,
194
+ traj: md.Trajectory,
195
+ target_index: int = 0,
196
+ target_structure: md.Trajectory = None):
197
+
198
+ target = target_structure if target_structure is not None else traj[indices[target_index]]
199
+ return indices[md.rmsd(traj[indices], target).argsort()]
200
+
201
+
202
+ def load_traj(dir: str,
203
+ traj_keyword: str = None,
204
+ pdb_keyword: str = None,
205
+ keyword: str = None,
206
+ exclude: str = None,
207
+ selection: str = None,
208
+ traj_ext: str = "dcd",
209
+ top_ext: str = "pdb",
210
+ stride: int = 1):
211
+ def check(x: list):
212
+ if len(x) == 1:
213
+ return x[0]
214
+ else:
215
+ raise Exception("Keyword search returned multiple files, cannot load without ambiguity")
216
+
217
+ extensions = [traj_ext, top_ext]
218
+
219
+ files = lsdir(dir, keyword=extensions, exclude=exclude, match=any)
220
+
221
+ if all(i is not None and isinstance(i, str) for i in (traj_keyword, pdb_keyword)):
222
+
223
+ dcd, pdb = [check(filter_strs(files, [ext, kw], match=all))
224
+ for ext, kw in zip(extensions, [traj_keyword, pdb_keyword])]
225
+
226
+ else:
227
+
228
+ if keyword is None:
229
+ dcd, pdb = [check(filter_strs(files, keyword=ext, match=all))
230
+ for ext in extensions]
231
+ else:
232
+
233
+ assert isinstance(keyword, (list, str)), "keyword must be list or str"
234
+
235
+ keyword = [keyword] if isinstance(keyword, str) else keyword
236
+
237
+ print(keyword)
238
+
239
+ dcd, pdb = [check(filter_strs(files, [ext] + keyword, match=all))
240
+ for ext in extensions]
241
+
242
+ indices = None if selection is None else md.load(pdb).top.select(selection)
243
+
244
+ print(f"Loading Trajectory File : {dcd} with top file : {pdb}")
245
+
246
+ return md.load(dcd, top=pdb, atom_indices=indices, stride=stride), dcd, pdb
247
+
248
+
249
+ def calc_sa(trj: "str to traj file or mdtraj Trajectory object",
250
+ helix: "str to helix pdb file or mdtraj object",
251
+ ):
252
+
253
+ trj, helix = (traj_slice(j, "name CA").center_coordinates() for j in
254
+ (md.load(i) if isinstance(i, str) else i for i in (trj, helix)))
255
+
256
+ assert trj.n_atoms == helix.n_atoms, \
257
+ "trj and helix trajectories do not contain the same number of CA atoms"
258
+
259
+ selections = [f"resid {i} to {i + 5}" for i in range(0, trj.n_atoms - 5)]
260
+
261
+ rmsd = np.asarray([md.rmsd(trj, helix, atom_indices=helix.topology.select(selection))
262
+ for selection in selections])
263
+
264
+ sa = (1.0 - (rmsd / 0.08) ** 8) / (1 - (rmsd / 0.08) ** 12)
265
+
266
+ return sa.T
267
+
268
+
269
+ residue_volumes = dict(zip(['A', 'R', 'N', 'D', 'C', 'E', 'Q', 'G', 'H', 'I', 'L', 'K',
270
+ 'M', 'F', 'P', 'S', 'T', 'W', 'Y', 'V'],
271
+ [121.0, 265.0, 187.0, 187.0, 148.0, 214.0, 214.0, 97.0,
272
+ 216.0, 195.0, 191.0, 230.0, 203.0, 228.0, 154.0, 143.0,
273
+ 163.0, 264.0, 255.0, 165.0]))
274
+
275
+
276
+ def calc_rsa(traj: "traj object or str",
277
+ pdb: str = None,
278
+ file_name: str = None,
279
+ parallel=False,
280
+ cpus_per_job: int = 1,
281
+ ca_indices: np.ndarray = None):
282
+ if isinstance(traj, str):
283
+ assert pdb is not None, "If traj is a file, must pass pdb file to load traj"
284
+ traj = md.load(traj, top=pdb)
285
+
286
+ # helper functions to avoid making copies of the traj and
287
+ # implement iterations smoothly
288
+ traj_slice_ = lambda string: traj.atom_slice(traj.top.select(string))
289
+
290
+ # determine protein alpha carbon indices
291
+ if ca_indices is None:
292
+ ca_indices = traj.top.select("protein and name CA")
293
+
294
+ # find residue indices and codes of alpha carbons and atoms per residue
295
+ ## this approach avoids selecting ligand atoms and caps
296
+ ### (volume norms only available for the 20 canonical residues)
297
+
298
+ index, code, count = np.array([[getattr(traj.top.atom(int(i)).residue, attr)
299
+ for attr in ["index", "code", "n_atoms"]]
300
+ for i in ca_indices]).T
301
+ count = count.astype(int)
302
+
303
+ # make selection for residues of CA atoms
304
+ selection = " or ".join(map("resid {}".format, index))
305
+
306
+ # find normalization for particular sequence
307
+ norm = np.vectorize(residue_volumes.__getitem__)(code)
308
+
309
+ # compute sasa over all atoms of CA residues
310
+ if parallel:
311
+
312
+ # start ray instance
313
+ ray.init()
314
+
315
+ traj_ref = ray.put(traj_slice_(selection))
316
+
317
+ fxn = ray.remote(lambda traj, index: md.shrake_rupley(traj[index]))
318
+
319
+ # chunk indices
320
+ chunks = np.array_split(np.arange(traj.n_frames),
321
+ int(multiprocessing.cpu_count() / cpus_per_job))
322
+ # run parallelized code
323
+ sasa = np.concatenate(ray.get([fxn.remote(traj=traj_ref, index=index)
324
+ for index in chunks]))
325
+
326
+
327
+ # free memory, shutdown ray instance
328
+ ray.internal.free(traj_ref)
329
+ ray.shutdown()
330
+
331
+ else:
332
+ sasa = md.shrake_rupley(traj_slice_(selection))
333
+
334
+ # MDTraj computes sasa by atom, we have to sum over residues
335
+ # MDTraj can organize by residues, but we don't trust MDTraj to get the right atoms
336
+ split = np.cumsum(count)[:-1]
337
+ residue_sasa = np.stack([s.sum(1) for s in np.array_split(sasa, split, 1)], 1)
338
+
339
+ # normalize and account for angstrom to nm conversion
340
+ rsa = 100 * residue_sasa / norm
341
+
342
+ # save data
343
+ if file_name is not None:
344
+ np.save(file_name, rsa)
345
+
346
+ return rsa
347
+
348
+
349
+ def Rg(x: np.ndarray) -> float:
350
+ """
351
+ Unweighted radius of gyration
352
+ Args
353
+ x : (n_frames, n_atoms, 3) np.ndarray
354
+ """
355
+
356
+ return np.power(np.linalg.norm(x - x.mean(-1, keepdims=True), axis=-1), 2).mean(-1) ** (1/2)
357
+
358
+
359
+ def traj_slice(traj, selection):
360
+ return traj.atom_slice(traj.top.select(selection))
361
+
362
+
363
+ def get_residues(traj,
364
+ indices: "iterable of arrays or array" = None,
365
+ atoms: bool = False,
366
+ cat: bool = False):
367
+ indices = np.arange(len(traj.top.select("name CA"))) if indices is None else indices
368
+ assert isinstance(indices, (np.ndarray, list)), "indices must be np.ndarray or list of np.ndarrays"
369
+
370
+ func = (lambda index: traj.top.select(f"resid {int(index)}")) if atoms else (
371
+ lambda index: str(traj.top.residue(int(index))))
372
+
373
+ if isinstance(indices, np.ndarray):
374
+ return func(indices) if len(indices) == 1 else \
375
+ np.concatenate(list(map(func, indices))) if (cat and atoms) else \
376
+ list(map(func, indices))
377
+ else:
378
+ return [np.concatenate(list(map(func, i))) if (len(i) != 1 and cat and atoms) else
379
+ list(map(func, i)) if len(i) != 1 else func(i) for i in indices]
380
+
381
+
382
+ def to_distance_matrix(distances: np.ndarray,
383
+ n: int,
384
+ m: int = None,
385
+ d0: int = 1):
386
+ distances = distances.squeeze()
387
+ assert distances.ndim < 3, "distances must be either 1 or two dimensional"
388
+ distances = distances.reshape(1, -1) if distances.ndim < 2 else distances
389
+
390
+ # info about flattened distance matrix
391
+ N, d = distances.shape
392
+
393
+ # intra molecular distances
394
+ if m is None:
395
+ matrix = np.zeros([N] + [n] * 2)
396
+ i, j = np.triu_indices(n, d0)
397
+ matrix[:, i, j] = distances
398
+ return (matrix + matrix.transpose(0, 2, 1)).squeeze()
399
+
400
+ else:
401
+ assert d == n * m, \
402
+ "Given dimensions (n,m) do not correspond to the dimension of the flattened distances"
403
+
404
+ return distances.reshape(-1, n, m).squeeze()
405
+
406
+
407
+ def residue_distances(traj,
408
+ index_0: np.ndarray,
409
+ index_1: np.ndarray = None,
410
+ periodic: bool = True,
411
+ parallel: bool = False):
412
+ # intra distance case
413
+ if index_1 is None:
414
+ indices = combinations(index_0)
415
+ return md.compute_contacts(traj, indices, periodic=periodic)[0], indices if not parallel\
416
+ else parallel_distances(traj.xyz, indices, length=traj.unitcell_lengths if periodic else None)
417
+
418
+ # inter distance case
419
+ else:
420
+ indices = product(index_0, index_1)
421
+ return md.compute_contacts(traj, indices, periodic=periodic)[0], indices if not parallel \
422
+ else parallel_distances(traj.xyz, indices, length=traj.unitcell_lengths if periodic else None)
423
+
424
+
425
+ def to_contact_matrix(distances: np.ndarray, cut_off: float = 0.5):
426
+ return np.where(distances < cut_off, 1, 0)
427
+
428
+