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.
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()}