MasterMSM 2.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.
- mastermsm/__init__.py +1 -0
- mastermsm/fewsm/__init__.py +0 -0
- mastermsm/fewsm/fewsm.py +260 -0
- mastermsm/fewsm/fewsm_lib.py +106 -0
- mastermsm/msm/__init__.py +0 -0
- mastermsm/msm/msm.py +1059 -0
- mastermsm/msm/msm_lib.py +1347 -0
- mastermsm/test/__init__.py +0 -0
- mastermsm/test/download_data.py +72 -0
- mastermsm/test/test_fewsm.py +103 -0
- mastermsm/test/test_msm.py +1039 -0
- mastermsm/test/test_trajectory.py +1051 -0
- mastermsm/trajectory/__init__.py +0 -0
- mastermsm/trajectory/traj.py +704 -0
- mastermsm/trajectory/traj_lib.py +459 -0
- mastermsm-2.0.1.dist-info/METADATA +205 -0
- mastermsm-2.0.1.dist-info/RECORD +20 -0
- mastermsm-2.0.1.dist-info/WHEEL +5 -0
- mastermsm-2.0.1.dist-info/licenses/LICENSE +340 -0
- mastermsm-2.0.1.dist-info/top_level.txt +1 -0
|
File without changes
|
|
@@ -0,0 +1,704 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This file is part of the MasterMSM package.
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
import os
|
|
6
|
+
import numpy as np
|
|
7
|
+
import mdtraj as md
|
|
8
|
+
from ..trajectory import traj_lib
|
|
9
|
+
|
|
10
|
+
class TimeSeries(object):
|
|
11
|
+
""" A class for handling simulation trajectories.
|
|
12
|
+
When simulation trajectories are provided, frames are read
|
|
13
|
+
and discretized using mdtraj [1]_. Alternatively, a discrete
|
|
14
|
+
trajectory can be provided.
|
|
15
|
+
|
|
16
|
+
Attributes
|
|
17
|
+
----------
|
|
18
|
+
mdt :
|
|
19
|
+
An mdtraj Trajectory object.
|
|
20
|
+
file_name : str
|
|
21
|
+
The name of the trajectory file.
|
|
22
|
+
distraj : list
|
|
23
|
+
The assigned trajectory.
|
|
24
|
+
dt : float
|
|
25
|
+
The time step
|
|
26
|
+
|
|
27
|
+
References
|
|
28
|
+
----------
|
|
29
|
+
.. [1] McGibbon, RT., Beauchamp, KA., Harrigan, MP., Klein, C.,
|
|
30
|
+
Swails, JM., Hernandez, CX., Schwantes, CR., Wang, LP., Lane,
|
|
31
|
+
TJ. and Pande, VS." MDTraj: A Modern Open Library for the Analysis
|
|
32
|
+
of Molecular Dynamics Trajectories", Biophys. J. (2015).
|
|
33
|
+
|
|
34
|
+
"""
|
|
35
|
+
def __init__(self, top=None, xtc=None, dt=None, \
|
|
36
|
+
distraj=None, stride=None, traj=None):
|
|
37
|
+
"""
|
|
38
|
+
Parameters
|
|
39
|
+
----------
|
|
40
|
+
distraj : string
|
|
41
|
+
The discrete state trajectory file.
|
|
42
|
+
dt : float
|
|
43
|
+
The time step.
|
|
44
|
+
top : string
|
|
45
|
+
The topology file, may be a PDB or GRO file.
|
|
46
|
+
xtc : string or list
|
|
47
|
+
The trajectory filename(s).
|
|
48
|
+
stride : int
|
|
49
|
+
Only read every stride-th frame
|
|
50
|
+
traj : string or list
|
|
51
|
+
Alias for xtc (deprecated).
|
|
52
|
+
|
|
53
|
+
"""
|
|
54
|
+
if traj is not None and xtc is None:
|
|
55
|
+
xtc = traj
|
|
56
|
+
if distraj is not None:
|
|
57
|
+
# A discrete trajectory is provided
|
|
58
|
+
self.distraj, self.dt = self._read_distraj(distraj=distraj, dt=dt)
|
|
59
|
+
elif xtc is not None:
|
|
60
|
+
# An MD trajectory is provided
|
|
61
|
+
self.file_name = xtc
|
|
62
|
+
mdt = traj_lib.load_mdtraj(xtc=xtc, top=top, stride=stride)
|
|
63
|
+
self.mdt = mdt
|
|
64
|
+
self.dt = self.mdt.timestep
|
|
65
|
+
else:
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
def _read_distraj(self, distraj=None, dt=None):
|
|
69
|
+
""" Loads discrete trajectories directly.
|
|
70
|
+
|
|
71
|
+
Parameters
|
|
72
|
+
----------
|
|
73
|
+
distraj : str, list
|
|
74
|
+
File or list with discrete trajectory.
|
|
75
|
+
|
|
76
|
+
Returns
|
|
77
|
+
-------
|
|
78
|
+
mdtrajs : list
|
|
79
|
+
A list of mdtraj Trajectory objects.
|
|
80
|
+
|
|
81
|
+
"""
|
|
82
|
+
if isinstance(distraj, list):
|
|
83
|
+
cstates = distraj
|
|
84
|
+
if dt is None:
|
|
85
|
+
dt = 1.
|
|
86
|
+
return cstates, dt
|
|
87
|
+
|
|
88
|
+
elif os.path.isfile(distraj):
|
|
89
|
+
raw = open(distraj, "r").readlines()
|
|
90
|
+
try:
|
|
91
|
+
cstates = [x.split()[1] for x in raw]
|
|
92
|
+
dt = float(raw[2].split()[0]) - float(raw[1].split()[0])
|
|
93
|
+
try: # make them integers if you can
|
|
94
|
+
cstates = [int(x) for x in cstates]
|
|
95
|
+
except ValueError:
|
|
96
|
+
pass
|
|
97
|
+
return cstates, dt
|
|
98
|
+
except IndexError:
|
|
99
|
+
cstates = [x.split()[0] for x in raw]
|
|
100
|
+
return cstates, 1.
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def find_keys(self, exclude=['O']):
|
|
104
|
+
""" Finds out the discrete states in the trajectory
|
|
105
|
+
|
|
106
|
+
Parameters
|
|
107
|
+
----------
|
|
108
|
+
exclude : list
|
|
109
|
+
A list of strings with states to exclude.
|
|
110
|
+
|
|
111
|
+
"""
|
|
112
|
+
keys = []
|
|
113
|
+
for s in self.distraj:
|
|
114
|
+
if s not in keys and s not in exclude:
|
|
115
|
+
keys.append(s)
|
|
116
|
+
self.keys = sorted(keys)
|
|
117
|
+
|
|
118
|
+
def discretize(self, states=None, bounds=None, check=False):
|
|
119
|
+
""" Discretize an MD trajectory using Ramachandran dihedral angles.
|
|
120
|
+
|
|
121
|
+
Assigns each frame to a coarse state label based on (phi, psi) angles,
|
|
122
|
+
using the Buchete-Hummer procedure. Unassigned frames inherit the state
|
|
123
|
+
of the previous frame; leading frames recorded before every dihedral
|
|
124
|
+
has visited a core are dropped. The result is stored in self.distraj
|
|
125
|
+
as a list of strings (one character per dihedral pair per frame),
|
|
126
|
+
and the bounds used in self.bounds. The per-dihedral fraction of
|
|
127
|
+
unassigned frames is stored in self.unassigned and the number of
|
|
128
|
+
dropped frames in self.ndrop.
|
|
129
|
+
|
|
130
|
+
Parameters
|
|
131
|
+
----------
|
|
132
|
+
states : list
|
|
133
|
+
Ordered list of state labels to assign. Supported: 'A' (alpha-helix),
|
|
134
|
+
'E' (extended/beta), 'L' (left-handed alpha). Ignored if
|
|
135
|
+
bounds is given.
|
|
136
|
+
bounds : dict, optional
|
|
137
|
+
Maps state labels to [phi_min, phi_max, psi_min, psi_max]
|
|
138
|
+
in degrees (more groups of four add regions). Default is
|
|
139
|
+
the Buchete-Hummer bounds for states.
|
|
140
|
+
check : bool
|
|
141
|
+
Plot the Ramachandran map with the bounds and ask the user
|
|
142
|
+
to accept or redefine them (see traj_lib.confirm_bounds).
|
|
143
|
+
Default False.
|
|
144
|
+
|
|
145
|
+
"""
|
|
146
|
+
phi, psi = traj_lib.compute_rama(self)
|
|
147
|
+
if bounds is None:
|
|
148
|
+
bounds = traj_lib.default_bounds(states)
|
|
149
|
+
if check:
|
|
150
|
+
bounds = traj_lib.confirm_bounds(phi, psi, bounds)
|
|
151
|
+
self.bounds = bounds
|
|
152
|
+
self.distraj, self.unassigned, self.ndrop = \
|
|
153
|
+
traj_lib.discrete_rama(phi, psi, bounds=bounds,
|
|
154
|
+
return_unassigned=True)
|
|
155
|
+
|
|
156
|
+
def gc(self):
|
|
157
|
+
"""
|
|
158
|
+
Gets rid of the mdtraj attribute
|
|
159
|
+
|
|
160
|
+
"""
|
|
161
|
+
delattr(self, "mdt")
|
|
162
|
+
|
|
163
|
+
class Featurizer(object):
|
|
164
|
+
"""
|
|
165
|
+
A class for featurizing TimeSeries objects
|
|
166
|
+
|
|
167
|
+
Attributes:
|
|
168
|
+
timeseries : list of trajectory objects
|
|
169
|
+
|
|
170
|
+
"""
|
|
171
|
+
def __init__(self, timeseries=None):
|
|
172
|
+
if not isinstance(timeseries, list):
|
|
173
|
+
self.timeseries = [timeseries]
|
|
174
|
+
else:
|
|
175
|
+
self.timeseries = timeseries
|
|
176
|
+
self.n_trajs = len(self.timeseries)
|
|
177
|
+
|
|
178
|
+
def add_torsions(self, shift=False, sincos=False):
|
|
179
|
+
""" Adds torsions as features
|
|
180
|
+
|
|
181
|
+
Parameters
|
|
182
|
+
----------
|
|
183
|
+
shift : bool
|
|
184
|
+
Whether we want to shift the torsions or not
|
|
185
|
+
sincos : bool
|
|
186
|
+
Whether we are calculating the sines and cosines
|
|
187
|
+
|
|
188
|
+
"""
|
|
189
|
+
for tr in self.timeseries:
|
|
190
|
+
phi, psi = traj_lib.compute_rama(tr, shift=shift, sincos=sincos)
|
|
191
|
+
if hasattr(tr, 'features'):
|
|
192
|
+
tr.features = np.hstack([tr.features, \
|
|
193
|
+
np.column_stack((phi, psi))])
|
|
194
|
+
else:
|
|
195
|
+
tr.features = np.column_stack((phi, psi))
|
|
196
|
+
|
|
197
|
+
def add_contacts(self, scheme='closest-heavy', log=False):
|
|
198
|
+
""" Adds contacts as features
|
|
199
|
+
|
|
200
|
+
Parameters
|
|
201
|
+
----------
|
|
202
|
+
scheme : str
|
|
203
|
+
Scheme to determine the distance between two residues. Options are:
|
|
204
|
+
‘ca’, ‘closest’, ‘closest-heavy’, ‘sidechain’, ‘sidechain-heavy’
|
|
205
|
+
log : bool
|
|
206
|
+
Whether distances are added in log scale.
|
|
207
|
+
|
|
208
|
+
"""
|
|
209
|
+
for tr in self.timeseries:
|
|
210
|
+
if hasattr(tr, 'features'):
|
|
211
|
+
tr.features = np.hstack([tr.features, \
|
|
212
|
+
traj_lib.compute_contacts(tr.mdt, \
|
|
213
|
+
scheme=scheme, log=log)])
|
|
214
|
+
else:
|
|
215
|
+
tr.features = traj_lib.compute_contacts(tr.mdt, \
|
|
216
|
+
scheme=scheme, log=log)
|
|
217
|
+
|
|
218
|
+
class DimRed(object):
|
|
219
|
+
"""
|
|
220
|
+
A class for performing dimensionality reduction of a
|
|
221
|
+
set of features vectors.
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
"""
|
|
225
|
+
def __init__(self, timeseries=None):
|
|
226
|
+
if not isinstance(timeseries, list):
|
|
227
|
+
self.timeseries = [timeseries]
|
|
228
|
+
else:
|
|
229
|
+
self.timeseries = timeseries
|
|
230
|
+
self.n_trajs = len(self.timeseries)
|
|
231
|
+
|
|
232
|
+
def whiten(self):
|
|
233
|
+
"""
|
|
234
|
+
Preprocesses the feature vectors to be used in discretization.
|
|
235
|
+
removing the mean and scaling to unit variance.
|
|
236
|
+
|
|
237
|
+
"""
|
|
238
|
+
from sklearn.preprocessing import StandardScaler
|
|
239
|
+
# first we fit the standard scaler using all the data
|
|
240
|
+
scaler = StandardScaler()
|
|
241
|
+
X = np.vstack([tr.features for tr in self.timeseries])
|
|
242
|
+
scaler.fit(X)
|
|
243
|
+
# then we replace the feature vectors by their whitened versions
|
|
244
|
+
for tr in self.timeseries:
|
|
245
|
+
X = scaler.transform(tr.features)
|
|
246
|
+
tr.features = X
|
|
247
|
+
|
|
248
|
+
def doPCA(self, n=2):
|
|
249
|
+
"""
|
|
250
|
+
Runs PCA on feature space
|
|
251
|
+
|
|
252
|
+
Parameters
|
|
253
|
+
----------
|
|
254
|
+
n : int
|
|
255
|
+
Number of PCs
|
|
256
|
+
|
|
257
|
+
"""
|
|
258
|
+
from sklearn.decomposition import PCA
|
|
259
|
+
X = [tr.features for tr in self.timeseries]
|
|
260
|
+
Xcum = np.vstack(X)
|
|
261
|
+
|
|
262
|
+
sklearn_pca = PCA(n_components=n)
|
|
263
|
+
sklearn_pca.fit(Xcum)
|
|
264
|
+
|
|
265
|
+
Xt = [sklearn_pca.transform(x) for x in X]
|
|
266
|
+
expl_var = sklearn_pca.explained_variance_ratio_
|
|
267
|
+
|
|
268
|
+
print (" Run PCA on %i components"%n)
|
|
269
|
+
print (" Explained variance: \n", expl_var)
|
|
270
|
+
for i, tr in enumerate(self.timeseries):
|
|
271
|
+
tr.features = Xt[i]
|
|
272
|
+
|
|
273
|
+
def doTICA(self, lag=None, n=2):
|
|
274
|
+
"""
|
|
275
|
+
Runs time-lagged independent component analysis (TICA) on
|
|
276
|
+
feature space.
|
|
277
|
+
|
|
278
|
+
Covariance matrices are estimated from all pairs of frames
|
|
279
|
+
(t, t+lag) within each trajectory (pairs never straddle two
|
|
280
|
+
trajectories) and symmetrized, i.e. dynamics are assumed to
|
|
281
|
+
be reversible. The independent components are the solutions
|
|
282
|
+
of the generalized eigenvalue problem
|
|
283
|
+
C(lag) v = lambda C(0) v.
|
|
284
|
+
|
|
285
|
+
Parameters
|
|
286
|
+
----------
|
|
287
|
+
lag : int
|
|
288
|
+
Lag time in number of frames (must be positive).
|
|
289
|
+
n : int
|
|
290
|
+
Number of independent components to retain.
|
|
291
|
+
|
|
292
|
+
Raises
|
|
293
|
+
------
|
|
294
|
+
ValueError
|
|
295
|
+
For a bad lag or n, or if C(0) is singular: a feature
|
|
296
|
+
is constant, or features are linearly dependent (the
|
|
297
|
+
smallest eigenvalue of their correlation matrix is below
|
|
298
|
+
1e-10).
|
|
299
|
+
|
|
300
|
+
Notes
|
|
301
|
+
-----
|
|
302
|
+
Features are replaced by their projection onto the first n
|
|
303
|
+
components, as in doPCA. The following attributes are stored:
|
|
304
|
+
tica_mean, tica_eigenvalues, tica_eigenvectors (columns, all
|
|
305
|
+
components) and tica_timescales (in units of the time step,
|
|
306
|
+
times tr.dt if available).
|
|
307
|
+
|
|
308
|
+
"""
|
|
309
|
+
from scipy.linalg import eigh
|
|
310
|
+
if lag is None or int(lag) != lag or lag < 1:
|
|
311
|
+
raise ValueError("lag must be a positive integer (frames)")
|
|
312
|
+
lag = int(lag)
|
|
313
|
+
X = [np.asarray(tr.features) for tr in self.timeseries]
|
|
314
|
+
dim = X[0].shape[1]
|
|
315
|
+
if any(x.shape[1] != dim for x in X):
|
|
316
|
+
raise ValueError("all trajectories must have the same "
|
|
317
|
+
"number of features")
|
|
318
|
+
if any(x.shape[0] <= lag for x in X):
|
|
319
|
+
raise ValueError("lag (%i frames) is not shorter than "
|
|
320
|
+
"every trajectory"%lag)
|
|
321
|
+
if n < 1 or n > dim:
|
|
322
|
+
raise ValueError("n must be between 1 and %i"%dim)
|
|
323
|
+
|
|
324
|
+
# pairs (x_t, x_{t+lag}) within each trajectory
|
|
325
|
+
X0 = np.vstack([x[:-lag] for x in X])
|
|
326
|
+
Xt = np.vstack([x[lag:] for x in X])
|
|
327
|
+
N = X0.shape[0]
|
|
328
|
+
|
|
329
|
+
# symmetrized mean and covariances
|
|
330
|
+
mean = 0.5*(X0.mean(axis=0) + Xt.mean(axis=0))
|
|
331
|
+
X0 = X0 - mean
|
|
332
|
+
Xt = Xt - mean
|
|
333
|
+
c0 = 0.5*(X0.T.dot(X0) + Xt.T.dot(Xt))/N
|
|
334
|
+
ctau = X0.T.dot(Xt)/N
|
|
335
|
+
ctau = 0.5*(ctau + ctau.T)
|
|
336
|
+
|
|
337
|
+
# C(0) must be well conditioned. Roundoff can let a singular
|
|
338
|
+
# C(0) through the Cholesky step in eigh, so check explicitly,
|
|
339
|
+
# using the correlation matrix (independent of feature units)
|
|
340
|
+
var = np.diag(c0)
|
|
341
|
+
sd = np.sqrt(np.clip(var, 0, None))
|
|
342
|
+
const = sd <= 1e-8*np.maximum(np.abs(mean), sd)
|
|
343
|
+
if np.any(const):
|
|
344
|
+
raise ValueError("features %s are constant"\
|
|
345
|
+
%list(np.where(const)[0]))
|
|
346
|
+
corr = c0/np.outer(sd, sd)
|
|
347
|
+
lmin = np.linalg.eigvalsh(corr)[0]
|
|
348
|
+
if lmin < 1e-10:
|
|
349
|
+
raise ValueError("C(0) is singular (smallest eigenvalue "
|
|
350
|
+
"of the feature correlation matrix is %.1e); "
|
|
351
|
+
"features are linearly dependent"%lmin)
|
|
352
|
+
|
|
353
|
+
# generalized symmetric eigenproblem (c0 must be pos. def.)
|
|
354
|
+
try:
|
|
355
|
+
evals, evecs = eigh(ctau, c0)
|
|
356
|
+
except np.linalg.LinAlgError:
|
|
357
|
+
raise ValueError("C(0) is not positive definite; features "
|
|
358
|
+
"are linearly dependent or constant")
|
|
359
|
+
idx = np.argsort(evals)[::-1]
|
|
360
|
+
evals = evals[idx]
|
|
361
|
+
evecs = evecs[:, idx]
|
|
362
|
+
|
|
363
|
+
dt = getattr(self.timeseries[0], 'dt', None)
|
|
364
|
+
unit = lag*dt if dt is not None else lag
|
|
365
|
+
with np.errstate(divide='ignore', invalid='ignore'):
|
|
366
|
+
ts = np.where((evals > 0) & (evals < 1), \
|
|
367
|
+
-unit/np.log(evals), np.nan)
|
|
368
|
+
|
|
369
|
+
self.tica_mean = mean
|
|
370
|
+
self.tica_eigenvalues = evals
|
|
371
|
+
self.tica_eigenvectors = evecs
|
|
372
|
+
self.tica_timescales = ts
|
|
373
|
+
|
|
374
|
+
print (" Run TICA on %i components, lag = %i frames"%(n, lag))
|
|
375
|
+
print (" Eigenvalues: \n", evals[:n])
|
|
376
|
+
print (" Implied timescales: \n", ts[:n])
|
|
377
|
+
for tr in self.timeseries:
|
|
378
|
+
tr.features = (np.asarray(tr.features) - mean).dot(evecs[:, :n])
|
|
379
|
+
|
|
380
|
+
class Discretizer(object):
|
|
381
|
+
"""
|
|
382
|
+
A class for discretizing molecular trajectories.
|
|
383
|
+
Includes methods for assignment and clustering.
|
|
384
|
+
|
|
385
|
+
"""
|
|
386
|
+
def __init__(self, timeseries=None):
|
|
387
|
+
"""
|
|
388
|
+
|
|
389
|
+
Parameters
|
|
390
|
+
----------
|
|
391
|
+
timeseries : list
|
|
392
|
+
List of objects of the TimeSeries class
|
|
393
|
+
|
|
394
|
+
"""
|
|
395
|
+
if not isinstance(timeseries, list):
|
|
396
|
+
self.timeseries = [timeseries]
|
|
397
|
+
else:
|
|
398
|
+
self.timeseries = timeseries
|
|
399
|
+
|
|
400
|
+
def rama(self, states=None, bounds=None, check=False):
|
|
401
|
+
"""
|
|
402
|
+
Assign frames to coarse states from the Ramachandran angles,
|
|
403
|
+
following the Buchete-Hummer procedure. This is the
|
|
404
|
+
counterpart of kmeans and hdbscan for torsion-based
|
|
405
|
+
discretization. Labels are strings, one character per
|
|
406
|
+
dihedral pair. The same bounds are used for all
|
|
407
|
+
trajectories and stored in self.bounds.
|
|
408
|
+
|
|
409
|
+
Parameters
|
|
410
|
+
----------
|
|
411
|
+
states : list
|
|
412
|
+
State labels to use, from 'A', 'E' and 'L'. Default is all.
|
|
413
|
+
Ignored if bounds is given.
|
|
414
|
+
bounds : dict, optional
|
|
415
|
+
Maps state labels to [phi_min, phi_max, psi_min, psi_max]
|
|
416
|
+
in degrees. Default is the Buchete-Hummer bounds.
|
|
417
|
+
check : bool
|
|
418
|
+
Plot the Ramachandran map pooled over all trajectories
|
|
419
|
+
with the bounds, and ask the user to accept or redefine
|
|
420
|
+
them (see traj_lib.confirm_bounds). Default False.
|
|
421
|
+
|
|
422
|
+
"""
|
|
423
|
+
rama = [traj_lib.compute_rama(tr) for tr in self.timeseries]
|
|
424
|
+
if bounds is None:
|
|
425
|
+
bounds = traj_lib.default_bounds(states)
|
|
426
|
+
if check:
|
|
427
|
+
phi = np.concatenate([np.ravel(f) for f, _ in rama])
|
|
428
|
+
psi = np.concatenate([np.ravel(y) for _, y in rama])
|
|
429
|
+
bounds = traj_lib.confirm_bounds(phi, psi, bounds)
|
|
430
|
+
self.bounds = bounds
|
|
431
|
+
for tr, (phi, psi) in zip(self.timeseries, rama):
|
|
432
|
+
tr.bounds = bounds
|
|
433
|
+
tr.distraj, tr.unassigned, tr.ndrop = \
|
|
434
|
+
traj_lib.discrete_rama(phi, psi, bounds=bounds,
|
|
435
|
+
return_unassigned=True)
|
|
436
|
+
|
|
437
|
+
def kmeans(self, k=50, dim=2, random_state=None):
|
|
438
|
+
"""
|
|
439
|
+
Run kmeans on the feature space
|
|
440
|
+
|
|
441
|
+
Parameters
|
|
442
|
+
----------
|
|
443
|
+
k: int
|
|
444
|
+
Number of k-centers
|
|
445
|
+
dim: int
|
|
446
|
+
Number of dimensions from feature space to do the clustering.
|
|
447
|
+
random_state : int
|
|
448
|
+
Seed for the k-means initialization (for reproducibility).
|
|
449
|
+
Default is to use the global numpy random state.
|
|
450
|
+
|
|
451
|
+
"""
|
|
452
|
+
from sklearn.cluster import KMeans
|
|
453
|
+
X = np.vstack([tr.features[:,:dim] for tr in self.timeseries])
|
|
454
|
+
kmeans = KMeans(n_clusters=k, random_state=random_state).fit(X)
|
|
455
|
+
self.k_centers = kmeans.cluster_centers_
|
|
456
|
+
for tr in self.timeseries:
|
|
457
|
+
tr.distraj = kmeans.predict(tr.features[:,:dim])
|
|
458
|
+
|
|
459
|
+
def hdbscan(self, mcs=None, ms=None, dim=2):
|
|
460
|
+
"""
|
|
461
|
+
Run HDBSCAN on the feature space. This generates core
|
|
462
|
+
sets automatically.
|
|
463
|
+
|
|
464
|
+
Requires: https://github.com/scikit-learn-contrib/hdbscan
|
|
465
|
+
|
|
466
|
+
See documentation on how to set parameters:
|
|
467
|
+
https://hdbscan.readthedocs.io/en/latest/parameter_selection.html
|
|
468
|
+
|
|
469
|
+
Parameters
|
|
470
|
+
----------
|
|
471
|
+
mcs : int
|
|
472
|
+
min_cluster_size
|
|
473
|
+
ms : int
|
|
474
|
+
min_samples
|
|
475
|
+
dim : int
|
|
476
|
+
Number of dimensions from feature space to do the clustering.
|
|
477
|
+
|
|
478
|
+
"""
|
|
479
|
+
try:
|
|
480
|
+
import hdbscan
|
|
481
|
+
except ImportError:
|
|
482
|
+
raise ImportError("hdbscan is required for this method: pip install hdbscan")
|
|
483
|
+
X = np.vstack([tr.features[:,:dim] for tr in self.timeseries])
|
|
484
|
+
if not ms:
|
|
485
|
+
ms = mcs
|
|
486
|
+
hdb = hdbscan.HDBSCAN(min_cluster_size=mcs, min_samples=ms,\
|
|
487
|
+
prediction_data=True).fit(X)
|
|
488
|
+
self.hdb_clusters = [x for x in np.unique(hdb.labels_) if (x != -1)]
|
|
489
|
+
self.hdb_nclusters = len(self.hdb_clusters)
|
|
490
|
+
n_assigned, n_frames = 0, 0
|
|
491
|
+
for tr in self.timeseries:
|
|
492
|
+
labels, _ = hdbscan.approximate_predict(hdb, \
|
|
493
|
+
tr.features[:,:dim])
|
|
494
|
+
n_assigned += int(np.sum(labels >= 0))
|
|
495
|
+
n_frames += len(labels)
|
|
496
|
+
tr.distraj = traj_lib._filter_states(labels)
|
|
497
|
+
# fraction of frames inside a cluster (the rest inherit labels)
|
|
498
|
+
self.frac_assigned = n_assigned/float(n_frames)
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
class Discretization(object):
|
|
502
|
+
"""
|
|
503
|
+
The outcome of running a discretization protocol.
|
|
504
|
+
|
|
505
|
+
Attributes
|
|
506
|
+
----------
|
|
507
|
+
name : str
|
|
508
|
+
Name of the protocol.
|
|
509
|
+
spec : dict
|
|
510
|
+
The protocol specification that generated it.
|
|
511
|
+
timeseries : list
|
|
512
|
+
Copies of the input TimeSeries objects, with the discrete
|
|
513
|
+
trajectory in ``distraj`` (and the final feature space in
|
|
514
|
+
``features``, when the protocol uses one). The inputs are
|
|
515
|
+
never modified.
|
|
516
|
+
n_states : int
|
|
517
|
+
Number of distinct states visited.
|
|
518
|
+
frac_assigned : float
|
|
519
|
+
Fraction of frames assigned to a state by the clustering
|
|
520
|
+
itself (1.0 except for density-based methods, where the
|
|
521
|
+
rest inherit the previous label).
|
|
522
|
+
|
|
523
|
+
"""
|
|
524
|
+
def __init__(self, name, spec, timeseries, frac_assigned=1.0):
|
|
525
|
+
self.name = name
|
|
526
|
+
self.spec = spec
|
|
527
|
+
self.timeseries = timeseries
|
|
528
|
+
self.frac_assigned = frac_assigned
|
|
529
|
+
self.n_states = len(set(s for tr in timeseries \
|
|
530
|
+
for s in tr.distraj))
|
|
531
|
+
|
|
532
|
+
def __repr__(self):
|
|
533
|
+
return "Discretization(%r, n_states=%i, frac_assigned=%.3f)"\
|
|
534
|
+
%(self.name, self.n_states, self.frac_assigned)
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
def _run_step(spec, key):
|
|
538
|
+
""" Returns (name, kwargs) for an optional protocol step. """
|
|
539
|
+
step = spec.get(key)
|
|
540
|
+
if step is None:
|
|
541
|
+
return None, {}
|
|
542
|
+
if isinstance(step, str):
|
|
543
|
+
return step, {}
|
|
544
|
+
name, kwargs = step
|
|
545
|
+
return name, dict(kwargs)
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def discretize_protocol(timeseries, spec, name=None, seed=None):
|
|
549
|
+
"""
|
|
550
|
+
Runs a discretization protocol on TimeSeries objects without
|
|
551
|
+
modifying them, so that different protocols can be applied to
|
|
552
|
+
the same data and compared.
|
|
553
|
+
|
|
554
|
+
A protocol is a dictionary. Buchete-Hummer discretization from
|
|
555
|
+
the Ramachandran angles is selected with ``rama``; every other
|
|
556
|
+
protocol is a chain of featurization, dimensionality reduction
|
|
557
|
+
and clustering. All keys are optional except that a protocol
|
|
558
|
+
must have either ``rama`` or ``cluster``::
|
|
559
|
+
|
|
560
|
+
{'rama': ['A', 'E']}
|
|
561
|
+
|
|
562
|
+
{'rama': {'A': [-130, -40, -50, 20],
|
|
563
|
+
'E': [-180, -40, 125, 180]}, 'check': False}
|
|
564
|
+
|
|
565
|
+
{'features': [('torsions', {'sincos': True})],
|
|
566
|
+
'whiten': True,
|
|
567
|
+
'reduce': ('tica', {'lag': 200, 'n': 4}),
|
|
568
|
+
'cluster': ('kmeans', {'k': 100, 'dim': 4})}
|
|
569
|
+
|
|
570
|
+
Parameters
|
|
571
|
+
----------
|
|
572
|
+
timeseries : TimeSeries or list
|
|
573
|
+
The trajectories to discretize.
|
|
574
|
+
spec : dict
|
|
575
|
+
The protocol. Keys:
|
|
576
|
+
|
|
577
|
+
* ``rama``: list of states for Buchete-Hummer assignment,
|
|
578
|
+
or a dict of bounds in degrees (see ``Discretizer.rama``).
|
|
579
|
+
* ``check``: bool, only with ``rama``. Show the Ramachandran
|
|
580
|
+
map and ask the user to accept or redefine the bounds
|
|
581
|
+
(default False). The bounds used are stored in the
|
|
582
|
+
result's ``bounds`` attribute.
|
|
583
|
+
* ``features``: list of (name, kwargs) with name 'torsions'
|
|
584
|
+
or 'contacts'. If absent, the ``features`` already present
|
|
585
|
+
in the TimeSeries objects are used (copied).
|
|
586
|
+
* ``whiten``: bool, standardize features (default True when
|
|
587
|
+
there is a ``reduce`` or ``cluster`` step).
|
|
588
|
+
* ``reduce``: (name, kwargs) with name 'pca' or 'tica', or
|
|
589
|
+
None for no dimensionality reduction. Arguments are those
|
|
590
|
+
of ``DimRed.doPCA`` and ``DimRed.doTICA`` (the TICA lag
|
|
591
|
+
is in frames).
|
|
592
|
+
* ``cluster``: (name, kwargs) with name 'kmeans' or
|
|
593
|
+
'hdbscan'; arguments are those of the ``Discretizer``
|
|
594
|
+
methods.
|
|
595
|
+
name : str
|
|
596
|
+
Label for the result.
|
|
597
|
+
seed : int
|
|
598
|
+
Seed for k-means (overrides ``random_state`` in the spec).
|
|
599
|
+
|
|
600
|
+
Returns
|
|
601
|
+
-------
|
|
602
|
+
Discretization
|
|
603
|
+
With the discretized copies of the input trajectories.
|
|
604
|
+
|
|
605
|
+
"""
|
|
606
|
+
import copy
|
|
607
|
+
if not isinstance(timeseries, list):
|
|
608
|
+
timeseries = [timeseries]
|
|
609
|
+
spec = dict(spec)
|
|
610
|
+
known = {'rama', 'check', 'features', 'whiten', 'reduce', \
|
|
611
|
+
'cluster'}
|
|
612
|
+
unknown = set(spec) - known
|
|
613
|
+
if unknown:
|
|
614
|
+
raise ValueError("unknown protocol keys: %s"%sorted(unknown))
|
|
615
|
+
if ('rama' in spec) == ('cluster' in spec):
|
|
616
|
+
raise ValueError("protocol needs exactly one of 'rama' or "\
|
|
617
|
+
"'cluster'")
|
|
618
|
+
if 'check' in spec and 'rama' not in spec:
|
|
619
|
+
raise ValueError("'check' only applies to 'rama' protocols")
|
|
620
|
+
if name is None:
|
|
621
|
+
name = 'rama' if 'rama' in spec else 'protocol'
|
|
622
|
+
|
|
623
|
+
# shallow copies share the (large) mdtraj object, but all
|
|
624
|
+
# attributes we set are rebound, so the originals stay intact
|
|
625
|
+
trs = [copy.copy(tr) for tr in timeseries]
|
|
626
|
+
|
|
627
|
+
if 'rama' in spec:
|
|
628
|
+
rama = spec['rama']
|
|
629
|
+
disc = Discretizer(trs)
|
|
630
|
+
if isinstance(rama, dict):
|
|
631
|
+
disc.rama(bounds=rama, check=spec.get('check', False))
|
|
632
|
+
else:
|
|
633
|
+
disc.rama(states=rama, check=spec.get('check', False))
|
|
634
|
+
res = Discretization(name, spec, trs)
|
|
635
|
+
res.bounds = disc.bounds
|
|
636
|
+
return res
|
|
637
|
+
|
|
638
|
+
if 'features' in spec:
|
|
639
|
+
for tr in trs:
|
|
640
|
+
if hasattr(tr, 'features'):
|
|
641
|
+
del tr.features
|
|
642
|
+
feat = Featurizer(trs)
|
|
643
|
+
adders = {'torsions': feat.add_torsions, \
|
|
644
|
+
'contacts': feat.add_contacts}
|
|
645
|
+
for fname, kwargs in spec['features']:
|
|
646
|
+
if fname not in adders:
|
|
647
|
+
raise ValueError("unknown feature '%s'"%fname)
|
|
648
|
+
adders[fname](**kwargs)
|
|
649
|
+
else:
|
|
650
|
+
for tr in trs:
|
|
651
|
+
if not hasattr(tr, 'features'):
|
|
652
|
+
raise ValueError("no 'features' in protocol and "
|
|
653
|
+
"none in the TimeSeries")
|
|
654
|
+
tr.features = np.array(tr.features)
|
|
655
|
+
|
|
656
|
+
dr = DimRed(trs)
|
|
657
|
+
if spec.get('whiten', True):
|
|
658
|
+
dr.whiten()
|
|
659
|
+
|
|
660
|
+
rname, kwargs = _run_step(spec, 'reduce')
|
|
661
|
+
if rname == 'pca':
|
|
662
|
+
dr.doPCA(**kwargs)
|
|
663
|
+
elif rname == 'tica':
|
|
664
|
+
dr.doTICA(**kwargs)
|
|
665
|
+
elif rname is not None:
|
|
666
|
+
raise ValueError("unknown reduction '%s'"%rname)
|
|
667
|
+
|
|
668
|
+
cname, kwargs = _run_step(spec, 'cluster')
|
|
669
|
+
disc = Discretizer(trs)
|
|
670
|
+
frac = 1.0
|
|
671
|
+
if cname == 'kmeans':
|
|
672
|
+
if seed is not None:
|
|
673
|
+
kwargs['random_state'] = seed
|
|
674
|
+
disc.kmeans(**kwargs)
|
|
675
|
+
elif cname == 'hdbscan':
|
|
676
|
+
disc.hdbscan(**kwargs)
|
|
677
|
+
frac = disc.frac_assigned
|
|
678
|
+
else:
|
|
679
|
+
raise ValueError("unknown clustering '%s'"%cname)
|
|
680
|
+
return Discretization(name, spec, trs, frac_assigned=frac)
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
def compare_protocols(timeseries, protocols, seed=None):
|
|
684
|
+
"""
|
|
685
|
+
Runs several discretization protocols on the same data.
|
|
686
|
+
|
|
687
|
+
Parameters
|
|
688
|
+
----------
|
|
689
|
+
timeseries : TimeSeries or list
|
|
690
|
+
The trajectories to discretize (not modified).
|
|
691
|
+
protocols : dict
|
|
692
|
+
Maps a protocol name to its specification, as in
|
|
693
|
+
``discretize_protocol``.
|
|
694
|
+
seed : int
|
|
695
|
+
Seed for k-means, so that runs are reproducible.
|
|
696
|
+
|
|
697
|
+
Returns
|
|
698
|
+
-------
|
|
699
|
+
dict
|
|
700
|
+
Maps each protocol name to its ``Discretization``.
|
|
701
|
+
|
|
702
|
+
"""
|
|
703
|
+
return {name: discretize_protocol(timeseries, spec, name=name, \
|
|
704
|
+
seed=seed) for name, spec in protocols.items()}
|