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.
- writhe_tools/__init__.py +0 -0
- writhe_tools/md_tools.py +428 -0
- writhe_tools/msm_tools.py +563 -0
- writhe_tools/plots.py +886 -0
- writhe_tools/stats.py +1098 -0
- writhe_tools/tcca.py +202 -0
- writhe_tools/utils/__init__.py +13 -0
- writhe_tools/utils/filing.py +47 -0
- writhe_tools/utils/graph_utils.py +95 -0
- writhe_tools/utils/indexing.py +300 -0
- writhe_tools/utils/misc.py +122 -0
- writhe_tools/utils/sorting.py +138 -0
- writhe_tools/utils/torch_utils.py +160 -0
- writhe_tools/writhe.py +635 -0
- writhe_tools/writhe_nn.py +823 -0
- writhe_tools/writhe_numba.py +129 -0
- writhe_tools/writhe_ray.py +91 -0
- writhe_tools-0.0.1.dist-info/METADATA +290 -0
- writhe_tools-0.0.1.dist-info/RECORD +22 -0
- writhe_tools-0.0.1.dist-info/WHEEL +5 -0
- writhe_tools-0.0.1.dist-info/licenses/LICENSE +674 -0
- writhe_tools-0.0.1.dist-info/top_level.txt +1 -0
writhe_tools/__init__.py
ADDED
|
File without changes
|
writhe_tools/md_tools.py
ADDED
|
@@ -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
|
+
|