From 320e36f08400f7ebd82bd6dfdd4f79271d294836 Mon Sep 17 00:00:00 2001 From: gph82 Date: Thu, 3 May 2018 11:58:24 +0200 Subject: [PATCH 01/73] [visualize] bugfix --- molpx/visualize.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/molpx/visualize.py b/molpx/visualize.py index 222460a..1421b6b 100644 --- a/molpx/visualize.py +++ b/molpx/visualize.py @@ -166,7 +166,6 @@ def FES(MD_trajectories, MD_top, projected_trajectories, list with all the :obj:`mdtraj.Trajectory`-objects contained in the :obj:`widgetbox` """ - # Prepare the overlay option n_overlays = _np.min([n_overlays,50]) if n_overlays>1: @@ -394,7 +393,7 @@ def traj(MD_trajectories, traj_selection : None, int, iterable of ints, default is None Don't plot all trajectories but only few of them. The default None implies that all trajs will be plotted. - Note: the data used for the FES will always include all trajectories, regardless of this value + Note: the data used for the FES will only include these trajectories projection : object that generated the projection, default is None The projected coordinates may come from a variety of sources. When working with :obj:`pyemma` a number of objects @@ -480,7 +479,7 @@ def traj(MD_trajectories, ylabels = _bmutils.labelize(proj_labels, proj_idxs) # Do we have usable projection information? - corr_dicts = [[]]*n_trajs + corr_dicts = [[]]*len(data) if projection is not None: corr_dicts = [_bmutils.most_corr(projection, geoms=igeom, proj_idxs=proj_idxs, n_args=n_feats) for igeom in geoms] From c06c79110c249ea8aa8b8eb5d143497e0068aa7c Mon Sep 17 00:00:00 2001 From: gph82 Date: Thu, 3 May 2018 12:05:05 +0200 Subject: [PATCH 02/73] [bmtuils] new methods regspace_cluster_to_target_kmeans, interval_schachtelung, regspace_from_distance_matrix --- molpx/_bmutils.py | 181 +++++++++++++++++++++++++++++++++++- molpx/tests/test_bmutils.py | 36 ++++++- 2 files changed, 209 insertions(+), 8 deletions(-) diff --git a/molpx/_bmutils.py b/molpx/_bmutils.py index fe7a810..3722fa2 100644 --- a/molpx/_bmutils.py +++ b/molpx/_bmutils.py @@ -6,10 +6,13 @@ except ImportError: from sklearn.mixture import GMM as _GMM +from scipy.spatial.distance import pdist as _pdist, squareform as _squareform + # From pyemma's coordinates from pyemma.coordinates import \ source as _source, \ cluster_regspace as _cluster_regspace, \ + cluster_kmeans as _cluster_kmeans, \ save_traj as _save_traj # From coor.data @@ -164,7 +167,8 @@ def re_warp(array_in, lengths): idxi += ll return warped -def regspace_cluster_to_target(data, n_clusters_target, +# TODO deprecate properly +def _regspace_cluster_to_target(data, n_clusters_target, n_try_max=5, delta=5., verbose=False): r""" @@ -209,6 +213,174 @@ def regspace_cluster_to_target(data, n_clusters_target, n_clusters_target, err, delta_cl_now)) return cl +def regspace_cluster_to_target_kmeans(data, n_clusters_target, + k_centers=1000, + k_stride=50, + n_tol = 5, + max_iter=5, + verbose=False): + r""" Wrapper of :obj:`pyemma.coordinates.cluster_regspace` but using n_clusters (instead of dmin) + as a parameter. + + By using a preliminary :obj:`pyemma.coordinates.cluster_kmeans`-run, a dmin value is optimized to return + approximately :obj:`n_clusters` clustercenters. + + Parameters : + ------------ + + data: ndarray or list thereof + Data to be used for clustering + + n_clusters_target: int, + Approximate number of regularly spaced clustercenters wanted + + k_centers : int, default is 1000 + Number of centers of the preliminary kmeans clustering. Should be between 2 and 10 times + larger than n_clusters + + k_stride : int, default is 50 + Stride for the preliminary kmeans clustering. This clustering is supposed to not be the bottleneck + + n_tol : int, default is 5 + Consider :obj:`n_clusters_target` \pm :obj:`n_tol` as converged + + max_iter : int, default is 5 + Will stop after :obj:`max_iter` regspace-clustering attempts regardless + + Returns : + -------- + cl : a :obj:`pyemma.coordinates.cluster_regspace` object with approximately :obj:n_clusters + + tested:True + """ + + # The parameter k_centers is just an initialization and has only impact on speed, + # not on the deterministic result. We catch some pathological data inputs + # Amount of strided frames + n_frames_kmeans = _np.sum([len(idata[::k_stride]) for idata in data]) + if n_frames_kmeans< k_centers: + k_stride = 1 + n_frames_kmeans = _np.sum([len(idata[::k_stride]) for idata in data]) + + # 1. Arrive at an approximate dmin by + # 1.1 Preliminary clustering + pre_cl = _cluster_kmeans(data, k=k_centers, stride=k_stride) + print(pre_cl.n_clusters, k_centers) + # 1.2 Distance matrix + D = _squareform(_pdist(pre_cl.clustercenters)) + # 1.3 Define the objective function to be optimized for dmin by interval_schachtelung + J = lambda dmin: len(regspace_from_distance_matrix(D, dmin)) + # 1.4 Optimize for dmin on the preliminary clustercenters + dmin = interval_schachtelung(J, [D.min(), D.max()], target=n_clusters_target, verbose=verbose) + # 2. Now that we have an approximate dmin, cluster in regspace iteratively: + cl = _cluster_regspace(data, dmin=dmin) + + ii = 0 + while (_np.abs(cl.n_clusters-n_clusters_target) > n_tol): + if ii >= max_iter: + print("Reaced max_iter %u."%ii) + break + # Distance matrix + D = _squareform(_pdist(cl.clustercenters)) + # Define the objective function to be optimized for dmin + J = lambda dmin: len(regspace_from_distance_matrix(D, dmin)) + # Optimize, starting at the actual dmin + dmin = interval_schachtelung(J, [D.min(), D.max()], target=n_clusters_target, verbose=verbose) + cl = _cluster_regspace(data, dmin) + #print(ii, dmin, cl.n_clusters) + ii +=1 + + return cl + +def interval_schachtelung(f, interval, target=0, eps=1, verbose=False): + r""" + Find the value m s.t. f(m) = target +- eps using interval optimization + + :param function to be optimized + :param interval: iterable of len 2 with the left and right limits of input interval + :param target: objective + :param eps: convergence criterion + :param verbose: whether to inform of each iteration or not + :return: m + + tested : True + """ + + left, right = interval[0], interval[1] + def inform(left, fl, right, fr, middle, fm, delta, eps, str0=''): + print(str0) + print('%04.2f: %04.2f' % (left, fl)) + print('%04.2f: %04.2f %f %f' % (middle, fm, delta, eps), ~(delta >= eps)) + print('%04.2f: %04.2f' % (right, fr)) + print() + + middle = (left + right) / 2 + fl, fr, fm = f(left), f(right), f(middle) + delta = _np.abs(fm - target) + if verbose: + inform(left, fl, right, fr, middle, fm, delta, eps, str0='Init:') + + while delta >= eps: + middle = (left + right) / 2 + fm = f(middle) + delta = _np.abs(fm - target) + + if verbose: + inform(left, fl, right, fr, middle, fm, delta, eps) + + if fl <= target <= fm or fl >= target >= fm: + right, fr = middle, fm + elif fm <= target <= fr or fm >= target >= fr: + left, fl = middle, fm + else: + print(fl, target, fm, middle) + raise Exception("Failed while optimizing") + return middle + + +def regspace_from_distance_matrix(D, dmin): + r""" Return the indices idxs of the rows/columns of the symmetric matrix (D[idxs,idxs] > dmin).all() == True + + Can be used as an analogue to :obj:pyemma.coordinates.cluster_regpsace if all pairwise distances have been + recomputed + + + Parameters : + ------------ + + D: 2D np.ndarray of shape (m,m) + Symmetric matrix containing pairwise distances + + dmin : float or int, must be > 0 + Cutoff + + Returns : + --------- + + centers : list + Integers of the the rows/columns that contain distances > dmin + + """ + + assert _np.allclose(D, D.T), "Input matrix is not symmetric" + assert dmin >= 0, ("dmin has to be > 0, not", dmin) + + Dprime = _np.copy(D) + _np.fill_diagonal(Dprime, _np.inf) + assigned = [] + for ii, irow in enumerate(Dprime): + if ii not in assigned: + assigned.extend(_np.argwhere(irow < dmin).flatten()) + assigned = _np.unique(assigned) + centers = [ii for ii in range(Dprime.shape[0]) if ii not in assigned] + Dprime = Dprime[centers, :] + Dprime = Dprime[:, centers] + + # This is the method testing itself + #assert Dprime.min() > dmin, (Dprime.min(), dmin) + + return centers + def min_disp_path(start, path_of_candidates, exclude_coords=None, history_aware=False): r""""Returns a list of indices [i,j,k...] s.t. the distances @@ -1019,11 +1191,10 @@ def add_atom_idxs_widget(atom_idxs, ngl_wdg, color_list=None, radius=1): ngl_wdg.add_spacefill(selection=[iidxs], radius=radius, color=color, component=cc) elif _np.ndim(iidxs)>0 and len(iidxs)==2: ngl_wdg.add_distance(atom_pair=[[ii for ii in iidxs]], # yes it has to be this way for now - color=color, - #label_color='black', - label_size=0, + color=color, + label_size=0, component=cc) - # TODO add line thickness as **kwarg + # TODO add line thickness as **kwarg, see nglview usage questions for answer elif _np.ndim(iidxs) > 0 and len(iidxs) in [3,4]: ngl_wdg.add_spacefill(selection=iidxs, radius=radius, color=color, component=cc) else: diff --git a/molpx/tests/test_bmutils.py b/molpx/tests/test_bmutils.py index 43f5ba5..5db771d 100644 --- a/molpx/tests/test_bmutils.py +++ b/molpx/tests/test_bmutils.py @@ -11,6 +11,9 @@ from glob import glob import molpx +from scipy.spatial.distance import pdist as _pdist, squareform as _squareform + + class TestWithBPTIData(unittest.TestCase): r""" A class that contains all the TestCase with the MD info @@ -225,9 +228,36 @@ def setUp(self): def test_cluster_to_target(self): n_target = 15 - data = [np.random.randn(100, 1), np.random.randn(100,1)+10] - cl = _bmutils.regspace_cluster_to_target(data, n_target, n_try_max=10, delta=0, verbose=True) - assert n_target - 1 <= cl.n_clusters <= n_target + 1 + n_tol = 1 + data = [np.random.randn(5000, 1), np.random.randn(5000,1)+10] + cl = _bmutils.regspace_cluster_to_target_kmeans(data, n_target, k_centers=100, max_iter=100, n_tol=n_tol) + assert n_target - n_tol <= cl.n_clusters <= n_target + n_tol, (cl.n_clusters, n_tol) + + def test_regspace_from_distance_matrix(self): + data = np.random.rand(100, 2) + D = _squareform(_pdist(data)) + + centers = _bmutils.regspace_from_distance_matrix(D, D.mean()) + + # Re-compute distances, only for the centers + Drs = _pdist(data[centers]) + assert Drs.min()>=D.mean(), (Drs.min(), D.mean()) + + def test_interval_schachtelung(self): + + y = lambda x:x**2 # parabolic curve, monotically increasing between [0, +inf] + + interval = [2, 500] + eps = .1 + + target_y = np.random.randint(np.ceil(y(interval[0])), + np.floor(y(interval[1])), size=1).squeeze() + + x_sol = _bmutils.interval_schachtelung(y, [2, 500], target=target_y, eps=eps, + #verbose=True + ) + assert y(x_sol)-eps <= target_y < y(x_sol)+eps, (y(x_sol)-target_y, eps) + def test_catalogues(self): cl = _bmutils.regspace_cluster_to_target(self.data_for_cluster, 3, n_try_max=10, delta=0) From 973d938582c886c560daf0784d7159d8d6bf88ab Mon Sep 17 00:00:00 2001 From: gph82 Date: Thu, 3 May 2018 12:07:04 +0200 Subject: [PATCH 03/73] [generate] adapt to new method regspace_cluster_to_target_kmeans --- molpx/generate.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/molpx/generate.py b/molpx/generate.py index 0314714..f0752b1 100644 --- a/molpx/generate.py +++ b/molpx/generate.py @@ -117,8 +117,8 @@ def projection_paths(MD_trajectories, MD_top, projected_trajectories, "min_disp": _defdict(dict) } # Cluster in regspace along the dimension you want to advance, to approximately n_points - cl = _bmutils.regspace_cluster_to_target([jdata[:,[coord]] for jdata in idata], - n_points, n_try_max=3, + cl = _bmutils.regspace_cluster_to_target_kmeans([jdata[:,[coord]] for jdata in idata], + n_points, max_iter=3, verbose=verbose, ) @@ -298,7 +298,7 @@ def sample(MD_trajectories, MD_top, projected_trajectories, cl = projected_trajectories except: idata = _bmutils.data_from_input(projected_trajectories) - cl = _bmutils.regspace_cluster_to_target([dd[:,proj_idxs] for dd in idata], n_points, n_try_max=10, verbose=verbose) + cl = _bmutils.regspace_cluster_to_target_kmeans([dd[:,proj_idxs] for dd in idata], n_points, verbose=verbose) pos = cl.clustercenters cat_smpl = cl.sample_indexes_by_cluster(_np.arange(cl.n_clusters), n_geom_samples) From f805b1efff50dece115883596c04b52f71eabd7a Mon Sep 17 00:00:00 2001 From: gph82 Date: Sun, 6 May 2018 09:17:25 +0200 Subject: [PATCH 04/73] [generate.projection_paths] think about not returning the data --- molpx/generate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/molpx/generate.py b/molpx/generate.py index f0752b1..5cefa32 100644 --- a/molpx/generate.py +++ b/molpx/generate.py @@ -209,7 +209,7 @@ def projection_paths(MD_trajectories, MD_top, projected_trajectories, #TODO : consider storing the data in each dict. It's redundant but makes each dict kinda standalone - return paths_dict, idata + return paths_dict , idata # why were we returning idata in the first place? def sample(MD_trajectories, MD_top, projected_trajectories, From 5275d793a95d8b72ac5f6141518e26d9ddb9082e Mon Sep 17 00:00:00 2001 From: gph82 Date: Sun, 6 May 2018 09:18:19 +0200 Subject: [PATCH 05/73] [_bmutils] New class corr_dict, uniform kmeans init, avoid geom.join in save_traj --- molpx/_bmutils.py | 43 ++++++++++++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/molpx/_bmutils.py b/molpx/_bmutils.py index 3722fa2..20f7ded 100644 --- a/molpx/_bmutils.py +++ b/molpx/_bmutils.py @@ -264,8 +264,7 @@ def regspace_cluster_to_target_kmeans(data, n_clusters_target, # 1. Arrive at an approximate dmin by # 1.1 Preliminary clustering - pre_cl = _cluster_kmeans(data, k=k_centers, stride=k_stride) - print(pre_cl.n_clusters, k_centers) + pre_cl = _cluster_kmeans(data, k=k_centers, stride=k_stride,init_strategy='uniform' ) # 1.2 Distance matrix D = _squareform(_pdist(pre_cl.clustercenters)) # 1.3 Define the objective function to be optimized for dmin by interval_schachtelung @@ -778,9 +777,16 @@ def save_traj_wrapper(traj_inp, indexes, outfile, top=None, stride=1, chunksize= chunksize=chunksize, image_molecules=image_molecules, verbose=verbose) elif isinstance(traj_inp[0], _md.Trajectory): file_idx, frame_idx = indexes[0] - geom_smpl = traj_inp[file_idx][frame_idx] - for file_idx, frame_idx in indexes[1:]: - geom_smpl = geom_smpl.join(traj_inp[file_idx][frame_idx]) + if False: + geom_smpl = traj_inp[file_idx][frame_idx] + for file_idx, frame_idx in indexes[1:]: + geom_smpl = geom_smpl.join(traj_inp[file_idx][frame_idx]) + # TODO this takes too much time for large topologies, consider copying + else: + xyz =[] + for file_idx, frame_idx in indexes: + xyz.append(traj_inp[file_idx].xyz[frame_idx].squeeze()) + geom_smpl = _md.Trajectory(xyz, traj_inp[0][0].top) else: raise TypeError("Cant handle input of type %s now"%(type(traj_inp[0]))) @@ -921,10 +927,6 @@ def smooth_geom(geom, n, geom_data=None, superpose=True, symmetric=True): Note: you might need to re-orient this averaged geometry again """ - # Input checking here, otherwise we're seriously in trouble - - - # Get the indices necessary for the running average frame_idxs, frame_windows = running_avg_idxs(geom.n_frames, n, symmetric=symmetric) @@ -941,6 +943,7 @@ def smooth_geom(geom, n, geom_data=None, superpose=True, symmetric=True): xyz = _np.zeros((len(frame_idxs), geom.n_atoms, 3)) for ii, idx in enumerate(frame_idxs): #print(ii, idx, frame_windows[ii][n]) + # TODO avoid casting an entire geometry (which triggers deepcopys which are time consuming) if superpose: xyz[ii,:,:] = geom[frame_windows[ii]].superpose(geom, frame=frame_windows[ii][n]).xyz.mean(0) else: @@ -1093,22 +1096,36 @@ def most_corr(correlation_input, geoms=None, proj_idxs=None, feat_name=None, n_a info.append({"lines":[], "name":iproj}) for jj, jidx in enumerate(most_corr_idxs[ii]): if avail_FT: - istr = 'Corr[%s|feat] = %2.1f for %-30s (feat nr. %u, atom idxs %s' % \ + istr = 'Corr[%s|feat] = %2.1f || %-30s || feat nr. %u, atom idxs %s' % \ (iproj, most_corr_vals[ii][jj], most_corr_labels[ii][jj], jidx, most_corr_atom_idxs[ii][jj]) else: - istr = 'Corr[%s|feat] = %2.1f (feat nr. %u)' % \ + istr = 'Corr[%s|feat] = %2.1f || nfeat nr. %u' % \ (iproj, most_corr_vals[ii][jj],jidx) info[-1]["lines"].append(istr) - corr_dict = {'idxs': most_corr_idxs, + corr_dict = CorrelationDict({'idxs': most_corr_idxs, 'vals': most_corr_vals, 'labels': most_corr_labels, 'feats': most_corr_feats, 'atom_idxs': most_corr_atom_idxs, - 'info':info} + 'info':info}) return corr_dict +class CorrelationDict(dict): + r""" This is just a dictionary + with the print method + rewritten to a pretty-print""" + def __str__(self): + nfeats = len(self["idxs"]) + output = 'Correlation dictionary for %u projections\n'%nfeats + for ii in range(nfeats): + for iline in self["info"][ii]["lines"]: + output += ' '+iline.replace(' || ','\n ')+'\n' + output+='\n' + + return output + def atom_idxs_from_feature(ifeat): r""" Return the atom_indices that best represent this input feature From 5d0f1e7169f5289623fea0f46b1a60067dfb391a Mon Sep 17 00:00:00 2001 From: gph82 Date: Sun, 6 May 2018 09:19:33 +0200 Subject: [PATCH 06/73] changes for the paper figures, to be reverted later --- molpx/visualize.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/molpx/visualize.py b/molpx/visualize.py index 1421b6b..54ca6f3 100644 --- a/molpx/visualize.py +++ b/molpx/visualize.py @@ -133,8 +133,8 @@ def FES(MD_trajectories, MD_top, projected_trajectories, done by the user place before calling this method. sample_kwargs : dictionary of named arguments, optional - named arguments for the function :obj:`molpx.visualize.sample`. Non-expert users can safely ignore this option. Examples - are :obj:`superpose` or :obj:`proj_idxs` + named arguments for the function :obj:`molpx.visualize.sample`. Non-expert users can safely ignore this option. + Examples are :obj:`superpose` or :obj:`proj_idxs` Returns ------- @@ -486,6 +486,7 @@ def traj(MD_trajectories, if corr_dicts[0]["feats"] != []: colors = _bmutils.matplotlib_colors_no_blue(ncycles=int(_np.ceil(_np.max(proj_idxs)/6.))) # Hack colors = [colors[ii] for ii in proj_idxs] + colors = ['red']*10 # for the paper, to be deleted later else: n_feats=0 else: @@ -1051,7 +1052,7 @@ def _sample(positions, geoms, ax, """ - assert isinstance(geoms, (list, _md.Trajectory)) + assert isinstance(geoms, (list, _md.Trajectory)), type(geoms) # Dow I need to smooth things out? if n_smooth > 0: @@ -1080,7 +1081,7 @@ def _sample(positions, geoms, ax, [ax.lines.pop() for ii in range(len(ax.lines))] # Plot the path on top of it if plot_path: - ax.plot(positions[:,0], positions[:,1], '-g', lw=3) + ax.plot(positions[:,0], positions[:,1], '-k', lw=3) # Link the axes ngl_wdg with the ngl ngl_wdg axes_wdg = _linkutils.link_ax_w_pos_2_nglwidget(ax, From f61759c3361c390929c12f81f2a314d5efff7ba3 Mon Sep 17 00:00:00 2001 From: gph82 Date: Tue, 8 May 2018 11:13:51 +0200 Subject: [PATCH 07/73] [bmutils] new method get_repr_atom_for_residue --- molpx/_bmutils.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/molpx/_bmutils.py b/molpx/_bmutils.py index 20f7ded..fe3900a 100644 --- a/molpx/_bmutils.py +++ b/molpx/_bmutils.py @@ -1074,6 +1074,8 @@ def most_corr(correlation_input, geoms=None, proj_idxs=None, feat_name=None, n_a for ii in proj_idxs: icorr = corr[:, ii] + # NaN's will screw up this argsort, so + icorr[_np.isnan(icorr)] = 0 most_corr_idxs.append(_np.abs(icorr).argsort()[::-1][:n_args]) most_corr_vals.append([icorr[jj] for jj in most_corr_idxs[-1]]) if geoms is not None and avail_FT: @@ -1155,7 +1157,7 @@ def atom_idxs_from_feature(ifeat): return _np.repeat(ifeat.indexes, 3) elif isinstance(ifeat, _ResMinDF): # Comprehend all the lists!!!! - return _np.vstack([[list(ifeat.top.residue(pj).atoms_by_name('CA'))[0].index for pj in pair] for pair in ifeat.contacts]) + return _np.vstack([[get_repr_atom_for_residue(ifeat.top.residue(pj)).index for pj in pair] for pair in ifeat.contacts]) if isinstance(ifeat, (_DihF, _AF)): ai = ifeat.angle_indexes if ifeat.cossin: @@ -1164,6 +1166,26 @@ def atom_idxs_from_feature(ifeat): else: raise NotImplementedError('bmutils.atom_idxs_from_feature cannot interpret the atoms behind %s yet'%ifeat) +def get_repr_atom_for_residue(rr, cands = ['CA','C','C1'], one_atom_residues=True): + r""" + Tries to return a representative atom per residue. For AAs, it is the CA, + then, the next atom-name in cands is looked for + :param rr: mdtraj-residue object + :param one_atom_residues, bool, default is True + if the residue has one atom, return that atom directly + # TODO consider this not even an optarg and code it hard + """ + + if rr.n_atoms==1: + return list(rr.atoms)[0] + + for cc in cands: + out = list(rr.atoms_by_name(cc)) + if len(out)>0: + return out[0] + if len(out)==0: + raise ValueError("Could not find any atoms named %s in the residue %s"%(cands, rr)) + def add_atom_idxs_widget(atom_idxs, ngl_wdg, color_list=None, radius=1): r""" provided a list of atom_idxs and a ngl_wdg, try to represent them as well as possible in the ngl_wdg From 4f7dca0eda651616b546dbdcd6db7da907d1a63b Mon Sep 17 00:00:00 2001 From: gph82 Date: Tue, 8 May 2018 11:14:08 +0200 Subject: [PATCH 08/73] [visualize] fix hacky color handling --- molpx/visualize.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/molpx/visualize.py b/molpx/visualize.py index 54ca6f3..7e712b4 100644 --- a/molpx/visualize.py +++ b/molpx/visualize.py @@ -484,9 +484,9 @@ def traj(MD_trajectories, corr_dicts = [_bmutils.most_corr(projection, geoms=igeom, proj_idxs=proj_idxs, n_args=n_feats) for igeom in geoms] if corr_dicts[0]["feats"] != []: - colors = _bmutils.matplotlib_colors_no_blue(ncycles=int(_np.ceil(_np.max(proj_idxs)/6.))) # Hack + colors = _bmutils.matplotlib_colors_no_blue(ncycles=int(_np.ceil((_np.max(proj_idxs)+1)/6.))) # Hack colors = [colors[ii] for ii in proj_idxs] - colors = ['red']*10 # for the paper, to be deleted later + #colors = ['red']*10 # for the paper, to be deleted later else: n_feats=0 else: From c0dc480f8daa5edb29136737a684051320409d18 Mon Sep 17 00:00:00 2001 From: gph82 Date: Tue, 8 May 2018 14:20:04 +0200 Subject: [PATCH 09/73] [_bmutils] new method atom_idxs_from_general_input --- molpx/_bmutils.py | 38 ++++++++++++++++++++++++-------------- molpx/visualize.py | 2 +- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/molpx/_bmutils.py b/molpx/_bmutils.py index fe3900a..cfdee71 100644 --- a/molpx/_bmutils.py +++ b/molpx/_bmutils.py @@ -1087,12 +1087,7 @@ def most_corr(correlation_input, geoms=None, proj_idxs=None, feat_name=None, n_a most_corr_labels.append([featurizer.describe()[jj] for jj in most_corr_idxs[-1]]) if avail_FT: - if len(featurizer.active_features) > 1: - pass - # TODO write a warning - else: - ifeat = featurizer.active_features[0] - most_corr_atom_idxs.append(atom_idxs_from_feature(ifeat)[most_corr_idxs[-1]]) + most_corr_atom_idxs.append([atom_idxs_from_general_input(featurizer)[ii] for ii in most_corr_idxs[-1]]) for ii, iproj in enumerate(proj_names): info.append({"lines":[], "name":iproj}) @@ -1128,6 +1123,27 @@ def __str__(self): return output +def atom_idxs_from_general_input(input): + r""" + Provided with anything that has a list of ifet.active_features, return the representative + atom indices for each feature component + :param input: can be TICA, PCA, or MDfeaturizer + :return: + """ + + if isinstance(input, (_TICA, _PCA)): + MDfeat = input.data_producer.featurizer + elif isinstance(input, _MDFeaturizer): + MDfeat = input + else: + raise TypeError("Sorry, input has to be of type %s, not %s" % ([_MDFeaturizer, _TICA, _PCA], type(input))) + + # Get atom lists for each active feature + out_idxs = [atom_idxs_from_feature(jfeat) for jfeat in MDfeat.active_features] + # Get one singlelist + return [item for sublist in out_idxs for item in sublist] + + def atom_idxs_from_feature(ifeat): r""" Return the atom_indices that best represent this input feature @@ -1135,10 +1151,9 @@ def atom_idxs_from_feature(ifeat): Parameters ---------- - ifeat : input feature, can be of two types: + ifeat : input featurizer: a :any:`pyemma.coordinates.featurizer` (Distancefeaturizer, AngleFeaturizer etc) or - a :any:`pyemma.coordinates.data.featurization.featurizer.MDFeaturizer` itself, in which case the first of the - obj:`ifeat.active_features` will be used + Returns ------- @@ -1146,11 +1161,6 @@ def atom_idxs_from_feature(ifeat): atom_indices : list with the atoms indices representative of this feature, whatever the feature """ - try: - ifeat = ifeat.active_features[0] - except AttributeError: - pass - if isinstance(ifeat, _DF) and not isinstance(ifeat, _ResMinDF): return ifeat.distance_indexes elif isinstance(ifeat, _SF): diff --git a/molpx/visualize.py b/molpx/visualize.py index 7e712b4..7fb79c4 100644 --- a/molpx/visualize.py +++ b/molpx/visualize.py @@ -806,7 +806,7 @@ def feature(feat, """ idxs = _bmutils.listify_if_int(idxs) - atom_idxs = _bmutils.atom_idxs_from_feature(feat)[idxs] + atom_idxs = _bmutils.atom_idxs_from_general_input(feat)[idxs] if color_list is None: color_list = ['blue'] * len(idxs) From 69acc7117bd3cd92fb0a749b1839333a398faea5 Mon Sep 17 00:00:00 2001 From: gph82 Date: Tue, 15 May 2018 15:54:54 +0200 Subject: [PATCH 10/73] [visualize] prepering traj() to allow for input feature --- molpx/visualize.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/molpx/visualize.py b/molpx/visualize.py index 7fb79c4..fc49c24 100644 --- a/molpx/visualize.py +++ b/molpx/visualize.py @@ -331,6 +331,7 @@ def traj(MD_trajectories, tunits = 'frames', traj_selection = None, projection = None, + input_feature_traj = None, n_feats = 1, ): r"""Link one or many :obj:`projected trajectories`, [Y_0(t), Y_1(t)...], with the :obj:`MD_trajectories` that @@ -341,7 +342,6 @@ def traj(MD_trajectories, MD_trajectories : str, or list of strings with the filename(s) the the molecular dynamics (MD) trajectories. Any file extension that :py:obj:`mdtraj` (.xtc, .dcd etc) can read is accepted. - Alternatively, a single :obj:`mdtraj.Trajectory` object or a list of them can be given as input. MD_top : str to topology filename or directly :obj:`mdtraj.Topology` object @@ -400,9 +400,13 @@ def traj(MD_trajectories, might have generated this projection, like a :obj:`pyemma.coordinates.transform.TICA` or a :obj:`pyemma.coordinates.transform.PCA` - Pass this object along and observe and the features that are most correlated with the projections + Pass this object along and observe the features that are most correlated with the projections will be plotted for the active trajectory, allowing the user to establish a visual connection between the projected coordinate and the original features (distances, angles, contacts etc) + These trajectories will be re-computed by applyiing + :obj:`projection.transform(MD_trajectories)', unless :obj:`input_feature_traj` is parsed + + input_feature_traj : TODO n_feats : int, default is 1 If a :obj:`projection` is passed along, the first n_feats features that most correlate the From f308e37f608033ab578ec37f5e9131c094ae2feb Mon Sep 17 00:00:00 2001 From: gph82 Date: Fri, 18 May 2018 15:10:21 +0200 Subject: [PATCH 11/73] [visualize] new method contacts() and optarg verbose for FES() --- molpx/visualize.py | 94 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 93 insertions(+), 1 deletion(-) diff --git a/molpx/visualize.py b/molpx/visualize.py index fc49c24..9ac909e 100644 --- a/molpx/visualize.py +++ b/molpx/visualize.py @@ -21,6 +21,8 @@ import warnings as _warnings +from itertools import product as _it_prod + # All calls to nglview call actually this function def _nglwidget_wrapper(geom, ngl_wdg=None, n_small=10): r""" Wrapper to nlgivew.show_geom's method that allows for some other automatic choice of @@ -77,6 +79,7 @@ def FES(MD_trajectories, MD_top, projected_trajectories, proj_labels='proj', n_overlays=1, atom_selection=None, + verbose=False, **sample_kwargs): r""" Return a molecular visualization widget connected with a free energy plot. @@ -132,6 +135,9 @@ def FES(MD_trajectories, MD_top, projected_trajectories, If :obj:`MD_trajectories` is already a (list of) :obj:`mdtraj.Trajectory` objects, the atom-slicing can be done by the user place before calling this method. + verbose : bool, default is False + Be verbose while computing the FES + sample_kwargs : dictionary of named arguments, optional named arguments for the function :obj:`molpx.visualize.sample`. Non-expert users can safely ignore this option. Examples are :obj:`superpose` or :obj:`proj_idxs` @@ -183,7 +189,8 @@ def FES(MD_trajectories, MD_top, projected_trajectories, return_data=True, n_geom_samples=n_overlays, keep_all_samples=keep_all_samples, - proj_stride=proj_stride + proj_stride=proj_stride, + verbose=verbose ) data = _np.vstack(data) @@ -1109,3 +1116,88 @@ def _sample(positions, geoms, ax, return ngl_wdg, axes_wdg +def contacts(contact_map, input, average=False, panelsize=4): + r""" + Provide a contact map and a widget or geometry, return an interactive contact map + + :param contact_map: + :param residue_idxs: + :return: + """ + + # Add one axis to the input if necessary + if _np.ndim(contact_map)==2: + contact_map = _np.array(contact_map, ndmin=3) + + # Check that the number of frames match if no average is requested + if _np.ndim(contact_map)==3 and not average: + assert len(contact_map) == input.n_frames, "If average is False, the number of contact maps (%u) must " \ + "match the number of frames in input (%u)" % ( + len(contact_map), input.n_frames) + # Assert squaredness + assert all([ict.shape[0] == ict.shape[1] for ict in contact_map]), "The input has to be a square matrix" + + # Needed arrays + nres = contact_map[0].shape[0] + residue_idxs = _np.arange(nres) + residue_pairs = _np.vstack(_it_prod(residue_idxs, residue_idxs)) + positions = _np.vstack(_it_prod(range(nres), range(nres))) + + # Create a color list + cmap = _get_cmap('rainbow') + cmap_table = _np.linspace(0, 1, len(positions)) + sticky_colors_hex = [_rgb2hex(cmap(ii)) for ii in _np.random.permutation(cmap_table)] + + # Instantiate widget + iwd = _nglwidget_wrapper(input) + + # Do the plot + _plt.ioff() + _plt.figure(figsize=(panelsize, panelsize)) + iax = _plt.gca() + # _plt.plot(positions[:,0], positions[:,1], ' ok') + # Make the average if wanted + if average: + iax.matshow(_np.average(contact_map, axis=0)) + else: + # Monkey-Patch the matshow_data into the object + iwd._MatshowData = {"image" : iax.matshow(contact_map[0]), + "data" : contact_map} + _plt.ion() + + + # Relabel the plot + # TODO make sure that zooming works even if a sub-set of res_idxs is given + """ + for axtype in ['x', 'y']: + tic_idxs = [int(tl) for tl in getattr(iax, 'get_%sticks'%axtype)()[1:-1]] + tic_labels = ['']+['%u'%residue_idxs[ii] for ii in tic_idxs]+[''] + getattr(iax,'set_%sticklabels'%axtype)(tic_labels) + """ + + # Monkey-Patch the ContactInNGLWidgets into the widget + iwd._CtcsInWid = [_linkutils.ContactInNGLWidget + (iwd, [_bmutils.get_repr_atom_for_residue(input.top.residue(aa)).index for aa in [ii,jj]], rp_idx, + #verbose=True, + color= sticky_colors_hex[rp_idx] + ) + for rp_idx, (ii,jj) in enumerate(residue_pairs)] + + # Turn axes into a widget + axes_wdg = _linkutils.link_ax_w_pos_2_nglwidget(iax, + positions, + iwd, + crosshairs=False, + #directionality='a2w', + dot_color='None', + #**link_ax2wdg_kwargs + ) + + iwd._set_size(*['%fin' % inches for inches in iax.get_figure().get_size_inches()]) + #iax.figure.tight_layout() + axes_wdg.canvas.set_window_title("Contact Map") + + outbox = _linkutils.MolPXHBox([iwd, axes_wdg.canvas]) + _linkutils.auto_append_these_mpx_attrs(outbox, input, iax, _plt.gcf(), iwd, axes_wdg, positions) + + return outbox \ No newline at end of file From d16a90cb296a29f6303cf028cc5a5f7a4d2e9e86 Mon Sep 17 00:00:00 2001 From: gph82 Date: Fri, 18 May 2018 15:11:16 +0200 Subject: [PATCH 12/73] [_bmutils] refactor interval_schachtelung() and elimiante unused optarg in get_repr_atom_for_residue() --- molpx/_bmutils.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/molpx/_bmutils.py b/molpx/_bmutils.py index cfdee71..25fcda3 100644 --- a/molpx/_bmutils.py +++ b/molpx/_bmutils.py @@ -308,9 +308,10 @@ def interval_schachtelung(f, interval, target=0, eps=1, verbose=False): left, right = interval[0], interval[1] def inform(left, fl, right, fr, middle, fm, delta, eps, str0=''): print(str0) - print('%04.2f: %04.2f' % (left, fl)) - print('%04.2f: %04.2f %f %f' % (middle, fm, delta, eps), ~(delta >= eps)) - print('%04.2f: %04.2f' % (right, fr)) + print('left: %04.2f, f(left) = %04.2f' % (left, fl)) + print('middle: %04.2f, f(middle) = %04.2f' % (middle, fm)) + print('right: %04.2f, f(right) = %04.2f' % (right, fr)) + print('delta, eps = %f %f, conv: %s'%(delta, eps, ~(delta >= eps))) print() middle = (left + right) / 2 @@ -319,13 +320,14 @@ def inform(left, fl, right, fr, middle, fm, delta, eps, str0=''): if verbose: inform(left, fl, right, fr, middle, fm, delta, eps, str0='Init:') + cc = 0 while delta >= eps: middle = (left + right) / 2 fm = f(middle) delta = _np.abs(fm - target) if verbose: - inform(left, fl, right, fr, middle, fm, delta, eps) + inform(left, fl, right, fr, middle, fm, delta, eps, str0='Iter %u'%cc) if fl <= target <= fm or fl >= target >= fm: right, fr = middle, fm @@ -334,6 +336,7 @@ def inform(left, fl, right, fr, middle, fm, delta, eps, str0=''): else: print(fl, target, fm, middle) raise Exception("Failed while optimizing") + cc += 1 return middle @@ -1176,14 +1179,11 @@ def atom_idxs_from_feature(ifeat): else: raise NotImplementedError('bmutils.atom_idxs_from_feature cannot interpret the atoms behind %s yet'%ifeat) -def get_repr_atom_for_residue(rr, cands = ['CA','C','C1'], one_atom_residues=True): +def get_repr_atom_for_residue(rr, cands = ['CA','C','C1']): r""" Tries to return a representative atom per residue. For AAs, it is the CA, then, the next atom-name in cands is looked for :param rr: mdtraj-residue object - :param one_atom_residues, bool, default is True - if the residue has one atom, return that atom directly - # TODO consider this not even an optarg and code it hard """ if rr.n_atoms==1: From f5da7b8c11e1888451340ad92c43bc1e1cd1a8c7 Mon Sep 17 00:00:00 2001 From: gph82 Date: Fri, 18 May 2018 15:18:28 +0200 Subject: [PATCH 13/73] [_linkutils] new class ContactInNGLWidget; link_ax_w_pos_2_nglwidget handles "_CtcsInWid"; ClickOnAxisListener handles ContactInNGLWidgets, recomputes kdtree on zooming --- molpx/_linkutils.py | 111 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 110 insertions(+), 1 deletion(-) diff --git a/molpx/_linkutils.py b/molpx/_linkutils.py index f37a763..33af735 100644 --- a/molpx/_linkutils.py +++ b/molpx/_linkutils.py @@ -4,12 +4,13 @@ from matplotlib.colors import is_color_like as _is_color_like from matplotlib.axes import Axes as _mplAxes from matplotlib.figure import Figure as _mplFigure +from matplotlib.patches import Rectangle as _Rectangle from IPython.display import display as _ipydisplay from pyemma.util.types import is_int as _is_int from scipy.spatial import cKDTree as _cKDTree -from ._bmutils import get_ascending_coord_idx +from ._bmutils import get_ascending_coord_idx, add_atom_idxs_widget from mdtraj import Trajectory as _mdTrajectory from nglview import NGLWidget as _NGLwdg @@ -91,8 +92,10 @@ def __init__(self, ngl_wdg, crosshairs, showclick_objs, ax, pos, self.pos = pos self.list_mpl_objects_to_update = list_mpl_objects_to_update self.list_of_dots = [None]*self.pos.shape[0] + self.list_of_rects = [None] * self.pos.shape[0] self.fig_size = self.ax.figure.get_size_inches() self.kdtree = None + self.axlims = _np.hstack((self.ax.get_xlim(), self.ax.get_ylim())) def build_tree(self): # Use ax.transData to compute distance in pixels @@ -104,6 +107,19 @@ def build_tree(self): def figure_changed_size(self): return not _np.allclose(self.fig_size, self.ax.figure.get_size_inches()) + @property + def axes_changed(self): + current_axlims = _np.hstack((self.ax.get_xlim(), self.ax.get_ylim())) + return not _np.allclose(current_axlims, self.axlims) + + def remove_last_contacts(self): + try: + self.ngl_wdg._CtcsInWid[self.ngl_wdg._CtcsLast].hide() + self.list_of_rects[self.ngl_wdg._CtcsLast].remove() + self.list_of_rects[self.ngl_wdg._CtcsLast] = None + except AttributeError: + pass + def __call__(self, event): # Wait for the first click or a a figsize change # to build the kdtree @@ -111,6 +127,16 @@ def __call__(self, event): self.build_tree() self.fig_size = self.ax.figure.get_size_inches() + # Check axes changes (e.g. in zooming) + if self.axes_changed: + self.build_tree() # rebuild tree + # store new limits + self.axlims = _np.hstack((self.ax.get_xlim(), + self.ax.get_ylim())) + + # Remove spurious contacts from the zooming-click + self.remove_last_contacts() + # Was the click inside the bounding box? if self.ax.get_window_extent().contains(event.x, event.y): if self.crosshairs: @@ -122,6 +148,8 @@ def __call__(self, event): update2Dlines(idot, self.pos[index, 0], self.pos[index, 1]) self.ngl_wdg.isClick = True + + # The sticky cases like _CtcsInWid or _GeomsInWid are update here instead of via the mplobjects to update if hasattr(self.ngl_wdg, '_GeomsInWid'): # We're in a sticky situation if event.button == 1: @@ -138,6 +166,29 @@ def __call__(self, event): if not self.ngl_wdg._GeomsInWid[index].is_visible() and self.list_of_dots[index] is not None: self.list_of_dots[index].remove() self.list_of_dots[index] = None + + + elif hasattr(self.ngl_wdg, '_CtcsInWid'): + if event.button == 1: + self.ngl_wdg._CtcsInWid[index].show() + # Plot and store the rectangle in case there wasn't + if self.list_of_rects[index] is None: + rectangle_padding = 0 # TODO future plans to make rectangles catch more than one ctc + x, y = self.pos[index] + self.list_of_rects[index] = self.ax.add_patch(_Rectangle( + (x - .5 - rectangle_padding, + y - .5 - rectangle_padding), # (x,y) + 1 + 2 * rectangle_padding, # width + 1 + 2 * rectangle_padding, # height, + fill=False, + linewidth=2, + edgecolor=self.ngl_wdg._CtcsInWid[index].color)) + elif event.button in [2,3]: + # Pressed left + self.ngl_wdg._CtcsInWid[index].hide() + if self.list_of_rects[index] is not None: + self.list_of_rects[index].remove() + self.list_of_rects[index] = None else: # We're not sticky, just go to the frame self.ngl_wdg.frame = index @@ -238,6 +289,61 @@ def __call__(self, change): print("caught index error with index %s (new=%s, old=%s)" % (_idx, change["new"], change["old"])) #print("set xy = (%s, %s)" % (x[_idx], y[_idx])) + if hasattr(self.ngl_wdg, '_MatshowData'): + self.ngl_wdg._MatshowData["image"].set_data(self.ngl_wdg._MatshowData["data"][_idx]) + + +class ContactInNGLWidget(object): + r""" + returns an object that is aware about its own atom-indices and + its own representation index in the widget. It also has access to the widget itself + With that knowlegde, one can use the methods .show() and .hide() + + param : ngl_widget, the widget upon which to superpose the contact + param : atom_indices, len(2), atom indices involved in this contact + param : contact_index, int, the index corresponding to this contact + """ + + + def __init__(self, ngl_wdg, atom_indices, contact_index, + component_to_draw_on=0, + verbose=False, + color=None): + assert len(atom_indices)==2, "ContactInNGLWidget takes a list with two elements as input, not len(%u)"%len(atom_indices) + assert [isinstance(ii, int) for ii in atom_indices], "The atom indices have to be type int" + + self.atom_indices = atom_indices + self.ngl_wdg = ngl_wdg + self.contact_index = contact_index + self.verbose = verbose + self.top = self.ngl_wdg._trajlist[component_to_draw_on].trajectory + self.comp = component_to_draw_on + self.shown = False + self.color = color + + def show(self): + if not self.shown: + if self.verbose: + print("Showing %s "%[self.top.atom(ii).residue for ii in self.atom_indices]) + + self.shown = True + add_atom_idxs_widget([self.atom_indices], self.ngl_wdg, color_list=[self.color]) + self.ngl_wdg._CtcsLast = self.contact_index + + def hide(self): + if self.shown: + #print(self.ngl_wdg._ngl_repr_dict[str(self.comp)].keys()) + for key in self.matching_repr_keys: + self.ngl_wdg._remove_representation(self.comp, repr_index=int(key)) + self.shown = False + + @property + def matching_repr_keys(self): + # Given that the _ngl_repr_dict gets updated elsewhere, this is the most robust way of + # finding this contact's representations + return [key for key, value in self.ngl_wdg._ngl_repr_dict[str(self.comp)].items() if value["type"] == "distance" + and _np.allclose(_np.sort(value["params"]["atomPair"]), _np.sort(self.atom_indices))] + class GeometryInNGLWidget(object): r""" returns an object that is aware of where its geometries are located in the NGLWidget their representation status @@ -400,6 +506,8 @@ def link_ax_w_pos_2_nglwidget(ax, pos, ngl_wdg, # Are we in a sticky situation? if hasattr(ngl_wdg, '_GeomsInWid'): sticky = True + elif hasattr(ngl_wdg, "_CtcsInWid"): + pass else: assert ngl_wdg.trajectory_0.n_frames == pos.shape[0], \ ("Mismatching frame numbers %u vs %u" % (ngl_wdg.trajectory_0.n_frames, pos.shape[0])) @@ -462,6 +570,7 @@ def link_ax_w_pos_2_nglwidget(ax, pos, ngl_wdg, # Connect axes to widget axes_widget = _AxesWidget(ax) if directionality in [None, 'a2w']: + # TODO since zooming events also contain button_release events this will be triggered as well axes_widget.connect_event('button_release_event', CLA_listener) # Connect widget to axes From c1ac4e8d6ffc09f10be87b9fff320b3c339ba8a2 Mon Sep 17 00:00:00 2001 From: gph82 Date: Fri, 18 May 2018 17:47:31 +0200 Subject: [PATCH 14/73] [_bmutils] regspace_cluster_to_target_kmeans catches non-list inputs and listyfies --- molpx/_bmutils.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/molpx/_bmutils.py b/molpx/_bmutils.py index 25fcda3..51b4e35 100644 --- a/molpx/_bmutils.py +++ b/molpx/_bmutils.py @@ -254,17 +254,22 @@ def regspace_cluster_to_target_kmeans(data, n_clusters_target, tested:True """ - # The parameter k_centers is just an initialization and has only impact on speed, - # not on the deterministic result. We catch some pathological data inputs - # Amount of strided frames + # Listify if only one array + if isinstance(data, _np.ndarray): + data = [data] + + # The parameter k_centers=1000 is just an initialization and has only impact on speed. + # TODO: consider hard coding it + # We try to catch some pathological data inputs with too little data n_frames_kmeans = _np.sum([len(idata[::k_stride]) for idata in data]) if n_frames_kmeans< k_centers: k_stride = 1 n_frames_kmeans = _np.sum([len(idata[::k_stride]) for idata in data]) + k_centers = _np.min((n_frames_kmeans, k_centers)) # 1. Arrive at an approximate dmin by # 1.1 Preliminary clustering - pre_cl = _cluster_kmeans(data, k=k_centers, stride=k_stride,init_strategy='uniform' ) + pre_cl = _cluster_kmeans(data, k=k_centers, stride=k_stride, init_strategy='uniform' ) # 1.2 Distance matrix D = _squareform(_pdist(pre_cl.clustercenters)) # 1.3 Define the objective function to be optimized for dmin by interval_schachtelung @@ -1131,7 +1136,7 @@ def atom_idxs_from_general_input(input): Provided with anything that has a list of ifet.active_features, return the representative atom indices for each feature component :param input: can be TICA, PCA, or MDfeaturizer - :return: + :return: list of input.dimension() with the atoms involved in each feature """ if isinstance(input, (_TICA, _PCA)): @@ -1143,7 +1148,7 @@ def atom_idxs_from_general_input(input): # Get atom lists for each active feature out_idxs = [atom_idxs_from_feature(jfeat) for jfeat in MDfeat.active_features] - # Get one singlelist + # Get one single list return [item for sublist in out_idxs for item in sublist] From 248ae82b97004f7eb2101e82390e043b4053d832 Mon Sep 17 00:00:00 2001 From: gph82 Date: Fri, 18 May 2018 17:48:20 +0200 Subject: [PATCH 15/73] [visualize] feature() accepts only MDFeaturizer (corrected docstring) and slice(idxs) for lists --- molpx/visualize.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/molpx/visualize.py b/molpx/visualize.py index 9ac909e..83ba5a1 100644 --- a/molpx/visualize.py +++ b/molpx/visualize.py @@ -787,7 +787,7 @@ def feature(feat, ---------- featurizer : py:obj:`_MDFeautrizer` - A PyEMMA MDFeaturizer object (either a feature or a featurizer, works with both) + A PyEMMA MDfeaturizer object with any number of .active_features() widget : None or nglview widget Provide an already existing widget to visualize the correlations on top of. This is only for expert use, @@ -817,7 +817,7 @@ def feature(feat, """ idxs = _bmutils.listify_if_int(idxs) - atom_idxs = _bmutils.atom_idxs_from_general_input(feat)[idxs] + atom_idxs = _bmutils.atom_idxs_from_general_input(feat)[slice(*idxs)] if color_list is None: color_list = ['blue'] * len(idxs) From 99f2617d45c4a8299cf47e8d4cb9325f73ec36d7 Mon Sep 17 00:00:00 2001 From: gph82 Date: Fri, 18 May 2018 17:49:21 +0200 Subject: [PATCH 16/73] [test_molPX] reduced nr input features s.t. TICA does not return rubbish with so few frames --- molpx/tests/test_molPX.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/molpx/tests/test_molPX.py b/molpx/tests/test_molPX.py index f1381dd..bff0fbd 100644 --- a/molpx/tests/test_molPX.py +++ b/molpx/tests/test_molPX.py @@ -22,7 +22,7 @@ def setUp(self): self.tempdir = tempfile.mkdtemp('test_molpx') self.projected_file = os.path.join(self.tempdir,'Y.npy') feat = pyemma.coordinates.featurizer(self.topology) - feat.add_all() + feat.add_selection(np.arange(3)) source = pyemma.coordinates.source(self.MD_trajectory, features=feat) self.tica = pyemma.coordinates.tica(source,lag=1, dim=2) Y = self.tica.get_output()[0] From f0b2825f9ec1932957c2072292dc618b233be40a Mon Sep 17 00:00:00 2001 From: gph82 Date: Fri, 18 May 2018 17:52:38 +0200 Subject: [PATCH 17/73] [test_bmutils] refactor to regspace_cluster_to_target_kmeans --- molpx/tests/test_bmutils.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/molpx/tests/test_bmutils.py b/molpx/tests/test_bmutils.py index 5db771d..e153fb1 100644 --- a/molpx/tests/test_bmutils.py +++ b/molpx/tests/test_bmutils.py @@ -260,7 +260,7 @@ def test_interval_schachtelung(self): def test_catalogues(self): - cl = _bmutils.regspace_cluster_to_target(self.data_for_cluster, 3, n_try_max=10, delta=0) + cl = _bmutils.regspace_cluster_to_target_kmeans(self.data_for_cluster, 3, max_iter=10, n_tol=0) #print(cl.clustercenters) cat_idxs, cat_cont = _bmutils.catalogues(cl) @@ -294,7 +294,7 @@ def test_catalogues(self): [13, 2]]) def test_catalogues_with_data(self): - cl = _bmutils.regspace_cluster_to_target(self.data_for_cluster, 3, n_try_max=10, delta=0) + cl = _bmutils.regspace_cluster_to_target_kmeans(self.data_for_cluster, 3, max_iter=10, n_tol=0) #print(cl.clustercenters) cat_idxs, cat_cont = _bmutils.catalogues(cl, data=self.data_for_cluster) @@ -329,7 +329,7 @@ def test_catalogues_with_data(self): def test_catalogues_sort_by_zero(self): - cl = _bmutils.regspace_cluster_to_target(self.data_for_cluster, 3, n_try_max=10, delta=0) + cl = _bmutils.regspace_cluster_to_target_kmeans(self.data_for_cluster, 3, max_iter=10, n_tol=0) cat_idxs, cat_cont = _bmutils.catalogues(cl, sort_by=0) # This test is extra, since this is a pure pyemma function @@ -358,7 +358,7 @@ def test_catalogues_sort_by_zero(self): [13, 2]]) def test_catalogues_sort_by_other_than_zero(self): - cl = _bmutils.regspace_cluster_to_target(self.data_for_cluster, 3, n_try_max=10, delta=0) + cl = _bmutils.regspace_cluster_to_target_kmeans(self.data_for_cluster, 3, max_iter=10, n_tol=0) cat_idxs, cat_cont = _bmutils.catalogues(cl, sort_by=1) # This test is extra, since this is a pure pyemma functions assert np.allclose(cat_idxs[0], [[1,0]]) @@ -436,7 +436,7 @@ def setUp(self): z = np.random.permutation(z) coords[:, -1, -1] = z self.traj = md.Trajectory(coords, traj.top) - self.cl = _bmutils.regspace_cluster_to_target(self.traj.xyz[:, -1, -1], 50, n_try_max=10) + self.cl = _bmutils.regspace_cluster_to_target_kmeans(self.traj.xyz[:, -1, -1], 50, max_iter=10) self.cat_smpl = self.cl.sample_indexes_by_cluster(np.arange(self.cl.n_clusters), n_geom_samples) self.geom_smpl = self.traj[np.vstack(self.cat_smpl)[:,1]] self.geom_smpl = _bmutils.re_warp(self.geom_smpl, [n_geom_samples] * self.cl.n_clusters) @@ -523,8 +523,7 @@ class TestVisualPath(TestWithBPTIData): def setUpClass(self): TestWithBPTIData.setUpClass() n_sample = 20 - self.cl_cont = _bmutils.regspace_cluster_to_target([ixyz[:, :2] for ixyz in self.xyz_flat], n_sample, verbose=True, n_try_max=10, - #delta=0 + self.cl_cont = _bmutils.regspace_cluster_to_target_kmeans([ixyz[:, :2] for ixyz in self.xyz_flat], n_sample, verbose=True, max_iter=10, ) self.cat_idxs, self.cat_data = _bmutils.catalogues(self.cl_cont) # Create the MD catalogue with pyemma From 6e7bfe103347668efa46c8aefa498982e3f60f32 Mon Sep 17 00:00:00 2001 From: gph82 Date: Fri, 18 May 2018 17:53:17 +0200 Subject: [PATCH 18/73] [test_visualize] update to visualize.feature() taking only MDFeaturizer --- molpx/tests/test_visualize.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/molpx/tests/test_visualize.py b/molpx/tests/test_visualize.py index e1cff34..670f260 100644 --- a/molpx/tests/test_visualize.py +++ b/molpx/tests/test_visualize.py @@ -240,16 +240,16 @@ def setUpClass(self): def test_feature(self): plt.figure() iwd = nglview.show_mdtraj(self.MD_trajectories[0]) - visualize.feature(self.feat.active_features[0], iwd) + visualize.feature(self.feat, iwd) def test_feature_color_list(self): plt.figure() iwd = nglview.show_mdtraj(self.MD_trajectories[0]) - visualize.feature(self.feat.active_features[0], iwd, + visualize.feature(self.feat, iwd, idxs=[0,1], color_list=['blue']) try: - visualize.feature(self.feat.active_features[0], iwd, + visualize.feature(self.feat, iwd, idxs=[0,1], color_list='blue') except TypeError: From f8f329fd2739c023d6c908235bcf5a1e43951eb6 Mon Sep 17 00:00:00 2001 From: gph82 Date: Sat, 19 May 2018 19:34:22 +0200 Subject: [PATCH 19/73] [_bmutils] get_index_ascending_coord check for len(idxs) instead of idxs==[] --- molpx/_bmutils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/molpx/_bmutils.py b/molpx/_bmutils.py index 51b4e35..0c072ac 100644 --- a/molpx/_bmutils.py +++ b/molpx/_bmutils.py @@ -282,7 +282,7 @@ def regspace_cluster_to_target_kmeans(data, n_clusters_target, ii = 0 while (_np.abs(cl.n_clusters-n_clusters_target) > n_tol): if ii >= max_iter: - print("Reaced max_iter %u."%ii) + print("Reached max_iter %u."%ii) break # Distance matrix D = _squareform(_pdist(cl.clustercenters)) @@ -845,7 +845,7 @@ def get_ascending_coord_idx(pos, fail_if_empty=False, fail_if_more_than_one=Fals idxs = _np.argwhere(_np.all(_np.diff(pos,axis=0)>0, axis=0)).squeeze() if isinstance(idxs, _np.ndarray) and idxs.ndim==0: idxs = idxs[()] - elif idxs == [] and fail_if_empty: + elif len(idxs)==0 and fail_if_empty: raise ValueError('No column was found in ascending order') if _np.size(idxs) > 1: From 030930e354c2b5cf1fb69a55ece08e131847ade5 Mon Sep 17 00:00:00 2001 From: gph82 Date: Sat, 19 May 2018 19:35:29 +0200 Subject: [PATCH 20/73] [test_visualize] minor refactor --- molpx/tests/test_visualize.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/molpx/tests/test_visualize.py b/molpx/tests/test_visualize.py index 670f260..602c4d5 100644 --- a/molpx/tests/test_visualize.py +++ b/molpx/tests/test_visualize.py @@ -33,8 +33,8 @@ def test_simplest_inputs_memory_small_max_frames(self): visualize.traj(self.MD_trajectories, self.MD_topology, self.Ys, max_frames=3) def test_simplest_inputs_disk(self): - visualize.traj(self.MD_trajectory_files, self.MD_topology_file, self.projected_files) - visualize.traj(self.MD_trajectory_files, self.MD_topology_file, [ifile.replace('.npy', '.dat') for ifile in self.projected_files]) + visualize.traj(self.MD_trajectory_files, self.MD_topology_file, self.projected_files_npy) + visualize.traj(self.MD_trajectory_files, self.MD_topology_file, [ifile.replace('.npy', '.dat') for ifile in self.projected_files_npy]) def test_simplest_inputs_memory_and_proj(self): visualize.traj(self.MD_trajectories, self.MD_topology, self.Ys, projection=self.tica) @@ -179,7 +179,7 @@ def setUpClass(self): def test_just_works_min_input_disk(self): molpx.visualize.FES(self.MD_trajectory_files, self.MD_topology_file, - self.projected_files) + self.projected_files_npy) def test_just_works_min_input_memory(self): molpx.visualize.FES(self.MD_trajectories, From 19615ab2455b8a93c383548ce39f605d007b2c68 Mon Sep 17 00:00:00 2001 From: gph82 Date: Sat, 19 May 2018 19:36:12 +0200 Subject: [PATCH 21/73] [test_bmutils] increase use the TestWithBPTIData class, add new tests --- molpx/tests/test_bmutils.py | 194 ++++++++++++++++++++++++------------ 1 file changed, 130 insertions(+), 64 deletions(-) diff --git a/molpx/tests/test_bmutils.py b/molpx/tests/test_bmutils.py index e153fb1..55239ad 100644 --- a/molpx/tests/test_bmutils.py +++ b/molpx/tests/test_bmutils.py @@ -32,45 +32,38 @@ def setUpClass(self): self.Fs = self.source.get_output() self.tica = pyemma.coordinates.tica(self.source, lag=1, dim=2) - self.pca = pyemma.coordinates.tica(self.source, dim=2) + self.pca = pyemma.coordinates.pca(self.source, dim=2) self.Ys = self.tica.get_output() self.tempdir = tempfile.mkdtemp('test_molpx') - self.projected_files = [os.path.join(self.tempdir, 'Y.%u.npy'%ii) for ii in range(len(self.MD_trajectories))] - [np.save(ifile, iY) for ifile, iY in zip(self.projected_files, self.Ys)] - [np.savetxt(ifile.replace('.npy', '.dat'), iY) for ifile, iY in zip(self.projected_files, self.Ys)] + self.projected_files_npy = [os.path.join(self.tempdir, 'Y.%u.npy' % ii) for ii in range(len(self.MD_trajectories))] + self.projected_files_dat = [ifile.replace(".npy",".dat") for ifile in self.projected_files_npy] + [np.save(ifile, iY) for ifile, iY in zip(self.projected_files_npy, self.Ys)] + [np.savetxt(ifile.replace('.npy', '.dat'), iY) for ifile, iY in zip(self.projected_files_npy, self.Ys)] @classmethod def tearDownClass(self): shutil.rmtree(self.tempdir) -class TestReadingInput(unittest.TestCase): +class TestReadingInput(TestWithBPTIData): - def setUp(self): - self.MD_trajectory = os.path.join(pyemma.__path__[0],'coordinates/tests/data/bpti_mini.xtc') - self.MD_topology = os.path.join(pyemma.__path__[0], 'coordinates/tests/data/bpti_ca.pdb') - self.tempdir = tempfile.mkdtemp('test_molpx') - self.projected_file = os.path.join(self.tempdir,'Y.npy') - self.feat = pyemma.coordinates.featurizer(self.MD_topology) - self.feat.add_all() - source = pyemma.coordinates.source(self.MD_trajectory, features=self.feat) - self.tica = pyemma.coordinates.tica(source,lag=1, dim=2) - self.Y = self.tica.get_output()[0] - self.F = source.get_output() - np.save(self.projected_file,self.Y) - np.savetxt(self.projected_file.replace('.npy','.dat'),self.Y) - - def tearDown(self): - shutil.rmtree(self.tempdir) + @classmethod + def setUpClass(self): + TestWithBPTIData.setUpClass() def test_data_from_input_npy(self): # Just one string - assert np.allclose(self.Y, _bmutils.data_from_input(self.projected_file)[0]) + assert np.allclose(self.Ys[0], _bmutils.data_from_input(self.projected_files_npy)[0]) # List of one string - assert np.allclose(self.Y, _bmutils.data_from_input([self.projected_file])[0]) - # List of two strings - Ys = _bmutils.data_from_input([self.projected_file, - self.projected_file]) - assert np.all([np.allclose(self.Y, iY) for iY in Ys]) + assert np.allclose(self.Ys[0], _bmutils.data_from_input([self.projected_files_npy[0]])) + # List of strings + Ys = _bmutils.data_from_input(self.projected_files_npy) + assert np.all([np.allclose(jY, iY) for jY, iY in zip(self.Ys, Ys)]) + + # Check that it fails properly + try: + _bmutils.data_from_input(1) + except ValueError: + pass def test_data_from_input_throws_exception(self): try: @@ -80,13 +73,12 @@ def test_data_from_input_throws_exception(self): def test_data_from_input_ascii(self): # Just one string - assert np.allclose(self.Y, _bmutils.data_from_input(self.projected_file.replace('.npy', '.dat'))[0]) + assert np.allclose(self.Ys[0], _bmutils.data_from_input(self.projected_files_dat)[0]) # List of one string - assert np.allclose(self.Y, _bmutils.data_from_input([self.projected_file.replace('.npy', '.dat')])[0]) - # List of two strings - Ys = _bmutils.data_from_input([self.projected_file.replace('.npy', '.dat'), - self.projected_file.replace('.npy','.dat')]) - assert np.all([np.allclose(self.Y, iY) for iY in Ys]) + assert np.allclose(self.Ys[0], _bmutils.data_from_input([self.projected_files_dat[0]])) + # List of strings + Ys = _bmutils.data_from_input(self.projected_files_dat) + assert np.all([np.allclose(jY, iY) for jY, iY in zip(self.Ys, Ys)]) def test_data_from_input_ndarray(self): # Just one ndarray @@ -108,7 +100,7 @@ def _test_data_from_input_ndarray_ascii_npy(self): def test_moldata_from_input(self): # Traj and top strings - moldata = _bmutils.moldata_from_input(self.MD_trajectory, MD_top=self.MD_topology) + moldata = _bmutils.moldata_from_input(self.MD_trajectory_files, MD_top=self.MD_topology) assert isinstance(moldata, _bmutils._FeatureReader) # Source object directly @@ -122,21 +114,69 @@ def test_moldata_from_input(self): pass # List of trajectories - geom = md.load(self.MD_trajectory, top=self.MD_topology) - moldata = _bmutils.moldata_from_input(geom) + moldata = _bmutils.moldata_from_input(self.MD_trajectories) assert isinstance(moldata[0], md.Trajectory), moldata def test_assert_moldata_belong_data(self): # Traj vs data - geom = md.load(self.MD_trajectory, top=self.MD_topology) - _bmutils.assert_moldata_belong_data([geom], [self.Y]) + _bmutils.assert_moldata_belong_data(self.MD_trajectories, self.Ys) # src vs data - moldata = _bmutils.moldata_from_input(self.MD_trajectory, MD_top=self.MD_topology) - _bmutils.assert_moldata_belong_data(moldata, [self.Y]) + moldata = _bmutils.moldata_from_input(self.MD_trajectories, MD_top=self.MD_topology) + _bmutils.assert_moldata_belong_data(moldata, self.Ys) # With stride - _bmutils.assert_moldata_belong_data([geom], [iY[::5] for iY in [self.Y]], data_stride=5) + _bmutils.assert_moldata_belong_data(self.MD_trajectories, [iY[::5] for iY in self.Ys], data_stride=5) + +class TestSaveTraj(TestWithBPTIData): + + @classmethod + def setUpClass(self): + TestWithBPTIData.setUpClass() + + def test_just_works(self): + samples = [[0, 10], + [1, 20], + [2, 30]] + geoms_ref = pyemma.coordinates.save_traj(self.source, samples, None) + geoms_molpx = _bmutils.save_traj_wrapper(self.source, samples, None) + assert np.all([np.allclose(ixyz, jxyz) for ixyz, jxyz in zip(geoms_ref.xyz, geoms_molpx.xyz)]) + + def test_works_with_MDTrajectories(self): + samples = [[0, 10], + [1, 20], + [2, 30]] + geoms_ref = pyemma.coordinates.save_traj(self.source, samples, None) + geoms_molpx = _bmutils.save_traj_wrapper(self.MD_trajectories, samples, None) + assert np.all([np.allclose(ixyz, jxyz) for ixyz, jxyz in zip(geoms_ref.xyz, geoms_molpx.xyz)]) + + def test_works_with_MDTrajectories_with_stride(self): + + samples = [[0, 10], + [1, 20], + [2, 30]] + geoms_ref = pyemma.coordinates.save_traj(self.source, samples, None, stride=2) + geoms_molpx = _bmutils.save_traj_wrapper(self.MD_trajectories, samples, None, stride=2) + assert np.all([np.allclose(ixyz, jxyz) for ixyz, jxyz in zip(geoms_ref.xyz, geoms_molpx.xyz)]) + +class TestCorrelations(TestWithBPTIData): + @classmethod + def setUpClass(self): + TestWithBPTIData.setUpClass() + + + def test_input_types(self): + _bmutils.most_corr(self.tica) + _bmutils.most_corr(self.pca) + _bmutils.most_corr(self.feat) + _bmutils.most_corr(self.tica.feature_TIC_correlation) + try: + _bmutils.most_corr("a") + except TypeError: + pass + + def test_printing(self): + print(_bmutils.most_corr(self.tica)) def test_most_corr_info_works(self): most_corr = _bmutils.most_corr(self.tica) @@ -159,8 +199,7 @@ def test_most_corr_info_works(self): assert most_corr['feats'] == [] def test_most_corr_info_works_with_options(self): - geoms = md.load(self.MD_trajectory, top=self.MD_topology) - most_corr = _bmutils.most_corr(self.tica, geoms=geoms) + most_corr = _bmutils.most_corr(self.tica, geoms=self.MD_trajectories[0]) # Idxs are okay ref_idxs = [np.abs(self.tica.feature_TIC_correlation[:, ii]).argmax() for ii in range(self.tica.dim)] @@ -171,15 +210,13 @@ def test_most_corr_info_works_with_options(self): assert np.all([rv == mcv for rv, mcv in zip(ref_corrs, most_corr['vals'])]) # Check that we got the right most correlated feature trajectory - ref_feats = self.feat.transform(geoms) + ref_feats = self.feat.transform(self.MD_trajectories[0]) ref_feats = [ref_feats[:, ii] for ii in ref_idxs] assert np.all(np.allclose(rv, mcv) for rv, mcv in zip(ref_feats, np.squeeze(most_corr['feats']))) def test_most_corr_info_works_with_options_and_proj_idxs(self): - geoms = md.load(self.MD_trajectory, top=self.MD_topology) - proj_idxs = [1, 0] # the order shouldn't matter - corr_dict = _bmutils.most_corr(self.tica, geoms=geoms, proj_idxs=proj_idxs) + corr_dict = _bmutils.most_corr(self.tica, geoms=self.MD_trajectories[0], proj_idxs=proj_idxs) # Idxs are okay ref_idxs = [np.abs(self.tica.feature_TIC_correlation[:, ii]).argmax() for ii in proj_idxs] @@ -195,7 +232,7 @@ def test_most_corr_info_works_with_options_and_proj_idxs(self): assert [isinstance(istr, str) for istr in corr_dict["labels"]] # Feature values are ok - ref_feats = self.feat.transform(geoms) + ref_feats = self.feat.transform(self.MD_trajectories[0]) ref_feats = [ref_feats[:, ii] for ii in ref_idxs] assert np.all(np.allclose(rv, mcv) for rv, mcv in zip(ref_feats, np.squeeze(corr_dict['feats']))) @@ -230,6 +267,11 @@ def test_cluster_to_target(self): n_target = 15 n_tol = 1 data = [np.random.randn(5000, 1), np.random.randn(5000,1)+10] + + # Only one iteration, just to force to go into the loop-breaking + cl = _bmutils.regspace_cluster_to_target_kmeans(data, n_target, k_centers=100, max_iter=1, n_tol=n_tol) + + # Now the real deal cl = _bmutils.regspace_cluster_to_target_kmeans(data, n_target, k_centers=100, max_iter=100, n_tol=n_tol) assert n_target - n_tol <= cl.n_clusters <= n_target + n_tol, (cl.n_clusters, n_tol) @@ -258,6 +300,21 @@ def test_interval_schachtelung(self): ) assert y(x_sol)-eps <= target_y < y(x_sol)+eps, (y(x_sol)-target_y, eps) + def test_interval_schachtelung_fails(self): + y = lambda x:x**2 # parabolic curve, monotically increasing between [0, +inf] + + interval = [2, 500] + eps = .1 + + target_y = 0 # Is not contained in [2**2, 500**2] + + try: + _bmutils.interval_schachtelung(y, [2, 500], target=target_y, eps=eps, + #verbose=True + ) + except: + pass + def test_catalogues(self): cl = _bmutils.regspace_cluster_to_target_kmeans(self.data_for_cluster, 3, max_iter=10, n_tol=0) @@ -420,7 +477,7 @@ class TestGetGoodStartingPoint(unittest.TestCase): def setUp(self): # The setup creates the typical, "geometries-sampled along cluster-scenario" n_geom_samples = 20 - traj = md.load(os.path.join(pyemma.__path__[0],'coordinates/tests/data/bpti_ca.pdb')) + traj = md.load(molpx._molpxdir(join='notebooks/data/bpti-c-alpha_centered.pdb')) traj = traj.atom_slice([0,1,3,4]) # create a trajectory with four atoms # Create a fake bi-modal trajectory with a compact and an open structure ixyz = np.array([[0., 0., 0.], @@ -476,7 +533,7 @@ def test_most_pop(self): "around the value 15. The found starting point should be" \ "in this interval (see the setUp)" - def _test_most_pop_x_rgyr(self): + def test_most_pop_x_rgyr(self): start_idx = _bmutils.get_good_starting_point(self.cl, self.geom_smpl, strategy="most_pop_x_smallest_Rgyr") #print(start_idx, self.cl.clustercenters[start_idx], np.sort(self.cl.clustercenters.squeeze())) # TODO: figure out a good way of testing this, at the moment it just chekcs that it runs @@ -596,11 +653,10 @@ def test_closest_all_coords_history(self): class TestSliceListOfGeoms(unittest.TestCase): def setUp(self): - self.topology = os.path.join(pyemma.__path__[0],'coordinates/tests/data/bpti_ca.pdb') + self.topology = molpx._molpxdir(join='notebooks/data/bpti-c-alpha_centered.pdb') self.ref_frame = 4 - self.MD_trajectory = md.load(os.path.join(pyemma.__path__[0], - 'coordinates/tests/data/bpti_mini.xtc'), - top=self.topology) + self.MD_trajectory = md.load(glob(molpx._molpxdir(join='notebooks/data/c-alpha_centered.stride.1000*xtc'))[0], + top=self.topology) def test_slice(self): geom_list = [self.MD_trajectory, self.MD_trajectory[::-1]] @@ -620,12 +676,11 @@ def setUp(self): def test_it_works(self): assert np.allclose([0], _bmutils.get_ascending_coord_idx(self.data[:,:-1])) def test_empty_no_fail(self): - result = _bmutils.get_ascending_coord_idx(self.data[:,[1,2]], fail_if_empty=False) assert len(result)==0 def test_empty_fail(self): try: - _bmutils.get_ascending_coord_idx(self.data[:, 1:], fail_if_empty=True) + _bmutils.get_ascending_coord_idx(self.data[:, [1,2]], fail_if_empty=True) except ValueError: pass def test_more_than_one_fails(self): @@ -633,11 +688,13 @@ def test_more_than_one_fails(self): _bmutils.get_ascending_coord_idx(self.data[:, :], fail_if_more_than_one=True) except: pass + def test_more_than_one_passes(self): + _bmutils.get_ascending_coord_idx(self.data[:, :], fail_if_more_than_one=False) class TestMinRmsdPaths(unittest.TestCase): def setUp(self): - self.topology = os.path.join(pyemma.__path__[0],'coordinates/tests/data/bpti_ca.pdb') + self.topology = molpx._molpxdir(join='notebooks/data/bpti-c-alpha_centered.pdb') self.reftraj = md.load(self.topology) def test_find_buried_best_candidate(self): @@ -757,7 +814,7 @@ class TestSmoothingFunctions(unittest.TestCase): def setUp(self): # The setup creates the typical, "geometries-sampled along cluster-scenario" - traj = md.load(os.path.join(pyemma.__path__[0], 'coordinates/tests/data/bpti_ca.pdb')) + traj = md.load(molpx._molpxdir(join='notebooks/data/bpti-c-alpha_centered.pdb')) traj = traj.atom_slice([0, 1]) # create a trajectory with two atoms # Create a fake bi-modal trajectory with a compact and an open structure ixyz = np.array([[10., 20., 30.], @@ -852,7 +909,7 @@ class TestListTransposeGeomList(unittest.TestCase): def test_it(self): # Create a dummy topology - traj = md.load(os.path.join(pyemma.__path__[0], 'coordinates/tests/data/bpti_ca.pdb')) + traj = md.load(molpx._molpxdir(join='notebooks/data/bpti-c-alpha_centered.pdb')) top = traj.atom_slice([0]).top # Single atom topology ixyz_row_0 = [[0, 0, 0]], [[0, 1, 0]], [[0, 2, 0]] # 3 frames of 1 atom @@ -878,7 +935,7 @@ class geom_list_2_geom(unittest.TestCase): def test_it(self): # Create a dummy topology MD_trajectory = os.path.join(pyemma.__path__[0], 'coordinates/tests/data/bpti_mini.xtc') - MD_topology = os.path.join(pyemma.__path__[0], 'coordinates/tests/data/bpti_ca.pdb') + MD_topology = molpx._molpxdir(join='notebooks/data/bpti-c-alpha_centered.pdb') traj = md.load(MD_trajectory, top=MD_topology) traj_list = [itraj for itraj in traj] @@ -886,11 +943,11 @@ def test_it(self): assert np.allclose(np.hstack([igeom.xyz for igeom in new_geom]).squeeze(), np.vstack(traj.xyz)) -class TestIndexFromFeatures(unittest.TestCase): +class TestIndexGeneralInput(unittest.TestCase): def setUp(self): - self.MD_topology = os.path.join(pyemma.__path__[0], 'coordinates/tests/data/bpti_ca.pdb') - self.feat= pyemma.coordinates.featurizer(self.MD_topology) + self.MD_topology = molpx._molpxdir(join='notebooks/data/bpti-c-alpha_centered.pdb') + self.feat = pyemma.coordinates.featurizer(self.MD_topology) self.ang_idxs = [[ii + jj for jj in range(3)] for ii in range(self.feat.topology.n_atoms - 3)] self.dih_idxs = [[ii + jj for jj in range(4)] for ii in range(self.feat.topology.n_atoms - 4)] @@ -902,9 +959,18 @@ def setUp(self): self.feat.add_dihedrals(self.dih_idxs) self.feat.add_dihedrals(self.dih_idxs, cossin=True) + self.src = pyemma.coordinates.source(glob(molpx._molpxdir(join='notebooks/data/c-alpha*xtc'))[0], features=self.feat) + self.tica = pyemma.coordinates.tica(self.src, ) + self.pca = pyemma.coordinates.pca(self.src) + def tearDown(self): pass + def test_input_just_runs(self): + _bmutils.atom_idxs_from_general_input(self.feat) + _bmutils.atom_idxs_from_general_input(self.tica) + _bmutils.atom_idxs_from_general_input(self.pca) + def test_atom_idxs_from_feature_xyz(self): ai = _bmutils.atom_idxs_from_feature(self.feat.active_features[0]) assert np.allclose(np.repeat(np.arange(self.feat.topology.n_atoms),3), ai) @@ -972,7 +1038,7 @@ def test_labelize(self): assert labels[1] == feat.describe()[1] def test_superpose_list_of_geoms(self): - geom = md.load(os.path.join(pyemma.__path__[0], 'coordinates/tests/data/bpti_ca.pdb')) + geom = md.load(molpx._molpxdir(join='notebooks/data/bpti-c-alpha_centered.pdb')) # Nothing happens _bmutils.superpose_to_most_compact_in_list(False, [geom]) From 10f6e9488639efb4d0f79e863d27ddefa5add557 Mon Sep 17 00:00:00 2001 From: gph82 Date: Thu, 3 May 2018 11:58:24 +0200 Subject: [PATCH 22/73] [visualize] bugfix --- molpx/visualize.py | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/molpx/visualize.py b/molpx/visualize.py index cbff1c4..39d8bbf 100644 --- a/molpx/visualize.py +++ b/molpx/visualize.py @@ -14,7 +14,7 @@ from . import _bmutils from . import _linkutils -from matplotlib import rcParams as _rcParams +from matplotlib import pylab as _plt, rcParams as _rcParams import nglview as _nglview import mdtraj as _md from ipywidgets import VBox as _VBox, Layout as _Layout, Button as _Button @@ -166,7 +166,7 @@ def FES(MD_trajectories, MD_top, projected_trajectories, list with all the :obj:`mdtraj.Trajectory`-objects contained in the :obj:`widgetbox` """ - from matplotlib import pylab as _plt + # Prepare the overlay option n_overlays = _np.min([n_overlays,50]) if n_overlays>1: @@ -220,8 +220,6 @@ def FES(MD_trajectories, MD_top, projected_trajectories, _linkutils.auto_append_these_mpx_attrs(outbox, geoms, ax, _plt.gcf(), ngl_wdg, axes_wdg, data_sample) return outbox - - def _box_me(tuple_in, auto_resize=True): r""" A wrapper that tries to put in an HBox whatever it s in @@ -275,7 +273,6 @@ def _box_me(tuple_in, auto_resize=True): return _linkutils._HBox(tuple_out) - def _plot_ND_FES(data, ax_labels, weights=None, bins=50, figsize=(4,4)): r""" A wrapper for pyemmas FESs plotting function that can also plot 1D @@ -296,7 +293,7 @@ def _plot_ND_FES(data, ax_labels, weights=None, bins=50, figsize=(4,4)): edges : tuple containimg the axes along which FES is to be plotted (only in the 1D case so far, else it's None) """ - from matplotlib import pylab as _plt + _plt.figure(figsize=figsize) ax = _plt.gca() idata = _np.vstack(data) @@ -431,7 +428,6 @@ def traj(MD_trajectories, """ - from matplotlib import pylab as _plt smallfontsize = int(_rcParams['font.size'] / 1.5) proj_idxs = _bmutils.listify_if_int(proj_idxs) @@ -481,7 +477,7 @@ def traj(MD_trajectories, for proj_counter, __ in enumerate(proj_idxs): ylims[0, proj_counter] = _np.min([idata[:,proj_counter].min() for idata in data]) ylims[1, proj_counter] = _np.max([idata[:,proj_counter].max() for idata in data]) - + ylabels = _bmutils.labelize(proj_labels, proj_idxs) # Do we have usable projection information? @@ -626,7 +622,6 @@ def traj(MD_trajectories, return mpx_wdg_box - def correlations(correlation_input, geoms=None, proj_idxs=None, @@ -643,7 +638,7 @@ def correlations(correlation_input, correlation_input : numpy ndarray or some PyEMMA objects - if array : + if array : (m,m) correlation matrix, with a row for each feature and a column for each projection if PyEMMA-object : @@ -768,7 +763,6 @@ def correlations(correlation_input, return corr_dict, widget - def feature(feat, widget, idxs=0, @@ -829,7 +823,6 @@ def feature(feat, return widget - def sample(positions, geom, ax, plot_path=False, clear_lines=True, @@ -986,7 +979,6 @@ def sample(positions, geom, ax, return ngl_wdg, axes_wdg - def _sample(positions, geoms, ax, plot_path=False, clear_lines=True, From 4c934b4fccbddcd0f8780c91114a4d887109144e Mon Sep 17 00:00:00 2001 From: gph82 Date: Thu, 3 May 2018 12:05:05 +0200 Subject: [PATCH 23/73] [bmtuils] new methods regspace_cluster_to_target_kmeans, interval_schachtelung, regspace_from_distance_matrix --- molpx/_bmutils.py | 181 +++++++++++++++++++++++++++++++++++- molpx/tests/test_bmutils.py | 36 ++++++- 2 files changed, 209 insertions(+), 8 deletions(-) diff --git a/molpx/_bmutils.py b/molpx/_bmutils.py index fe7a810..3722fa2 100644 --- a/molpx/_bmutils.py +++ b/molpx/_bmutils.py @@ -6,10 +6,13 @@ except ImportError: from sklearn.mixture import GMM as _GMM +from scipy.spatial.distance import pdist as _pdist, squareform as _squareform + # From pyemma's coordinates from pyemma.coordinates import \ source as _source, \ cluster_regspace as _cluster_regspace, \ + cluster_kmeans as _cluster_kmeans, \ save_traj as _save_traj # From coor.data @@ -164,7 +167,8 @@ def re_warp(array_in, lengths): idxi += ll return warped -def regspace_cluster_to_target(data, n_clusters_target, +# TODO deprecate properly +def _regspace_cluster_to_target(data, n_clusters_target, n_try_max=5, delta=5., verbose=False): r""" @@ -209,6 +213,174 @@ def regspace_cluster_to_target(data, n_clusters_target, n_clusters_target, err, delta_cl_now)) return cl +def regspace_cluster_to_target_kmeans(data, n_clusters_target, + k_centers=1000, + k_stride=50, + n_tol = 5, + max_iter=5, + verbose=False): + r""" Wrapper of :obj:`pyemma.coordinates.cluster_regspace` but using n_clusters (instead of dmin) + as a parameter. + + By using a preliminary :obj:`pyemma.coordinates.cluster_kmeans`-run, a dmin value is optimized to return + approximately :obj:`n_clusters` clustercenters. + + Parameters : + ------------ + + data: ndarray or list thereof + Data to be used for clustering + + n_clusters_target: int, + Approximate number of regularly spaced clustercenters wanted + + k_centers : int, default is 1000 + Number of centers of the preliminary kmeans clustering. Should be between 2 and 10 times + larger than n_clusters + + k_stride : int, default is 50 + Stride for the preliminary kmeans clustering. This clustering is supposed to not be the bottleneck + + n_tol : int, default is 5 + Consider :obj:`n_clusters_target` \pm :obj:`n_tol` as converged + + max_iter : int, default is 5 + Will stop after :obj:`max_iter` regspace-clustering attempts regardless + + Returns : + -------- + cl : a :obj:`pyemma.coordinates.cluster_regspace` object with approximately :obj:n_clusters + + tested:True + """ + + # The parameter k_centers is just an initialization and has only impact on speed, + # not on the deterministic result. We catch some pathological data inputs + # Amount of strided frames + n_frames_kmeans = _np.sum([len(idata[::k_stride]) for idata in data]) + if n_frames_kmeans< k_centers: + k_stride = 1 + n_frames_kmeans = _np.sum([len(idata[::k_stride]) for idata in data]) + + # 1. Arrive at an approximate dmin by + # 1.1 Preliminary clustering + pre_cl = _cluster_kmeans(data, k=k_centers, stride=k_stride) + print(pre_cl.n_clusters, k_centers) + # 1.2 Distance matrix + D = _squareform(_pdist(pre_cl.clustercenters)) + # 1.3 Define the objective function to be optimized for dmin by interval_schachtelung + J = lambda dmin: len(regspace_from_distance_matrix(D, dmin)) + # 1.4 Optimize for dmin on the preliminary clustercenters + dmin = interval_schachtelung(J, [D.min(), D.max()], target=n_clusters_target, verbose=verbose) + # 2. Now that we have an approximate dmin, cluster in regspace iteratively: + cl = _cluster_regspace(data, dmin=dmin) + + ii = 0 + while (_np.abs(cl.n_clusters-n_clusters_target) > n_tol): + if ii >= max_iter: + print("Reaced max_iter %u."%ii) + break + # Distance matrix + D = _squareform(_pdist(cl.clustercenters)) + # Define the objective function to be optimized for dmin + J = lambda dmin: len(regspace_from_distance_matrix(D, dmin)) + # Optimize, starting at the actual dmin + dmin = interval_schachtelung(J, [D.min(), D.max()], target=n_clusters_target, verbose=verbose) + cl = _cluster_regspace(data, dmin) + #print(ii, dmin, cl.n_clusters) + ii +=1 + + return cl + +def interval_schachtelung(f, interval, target=0, eps=1, verbose=False): + r""" + Find the value m s.t. f(m) = target +- eps using interval optimization + + :param function to be optimized + :param interval: iterable of len 2 with the left and right limits of input interval + :param target: objective + :param eps: convergence criterion + :param verbose: whether to inform of each iteration or not + :return: m + + tested : True + """ + + left, right = interval[0], interval[1] + def inform(left, fl, right, fr, middle, fm, delta, eps, str0=''): + print(str0) + print('%04.2f: %04.2f' % (left, fl)) + print('%04.2f: %04.2f %f %f' % (middle, fm, delta, eps), ~(delta >= eps)) + print('%04.2f: %04.2f' % (right, fr)) + print() + + middle = (left + right) / 2 + fl, fr, fm = f(left), f(right), f(middle) + delta = _np.abs(fm - target) + if verbose: + inform(left, fl, right, fr, middle, fm, delta, eps, str0='Init:') + + while delta >= eps: + middle = (left + right) / 2 + fm = f(middle) + delta = _np.abs(fm - target) + + if verbose: + inform(left, fl, right, fr, middle, fm, delta, eps) + + if fl <= target <= fm or fl >= target >= fm: + right, fr = middle, fm + elif fm <= target <= fr or fm >= target >= fr: + left, fl = middle, fm + else: + print(fl, target, fm, middle) + raise Exception("Failed while optimizing") + return middle + + +def regspace_from_distance_matrix(D, dmin): + r""" Return the indices idxs of the rows/columns of the symmetric matrix (D[idxs,idxs] > dmin).all() == True + + Can be used as an analogue to :obj:pyemma.coordinates.cluster_regpsace if all pairwise distances have been + recomputed + + + Parameters : + ------------ + + D: 2D np.ndarray of shape (m,m) + Symmetric matrix containing pairwise distances + + dmin : float or int, must be > 0 + Cutoff + + Returns : + --------- + + centers : list + Integers of the the rows/columns that contain distances > dmin + + """ + + assert _np.allclose(D, D.T), "Input matrix is not symmetric" + assert dmin >= 0, ("dmin has to be > 0, not", dmin) + + Dprime = _np.copy(D) + _np.fill_diagonal(Dprime, _np.inf) + assigned = [] + for ii, irow in enumerate(Dprime): + if ii not in assigned: + assigned.extend(_np.argwhere(irow < dmin).flatten()) + assigned = _np.unique(assigned) + centers = [ii for ii in range(Dprime.shape[0]) if ii not in assigned] + Dprime = Dprime[centers, :] + Dprime = Dprime[:, centers] + + # This is the method testing itself + #assert Dprime.min() > dmin, (Dprime.min(), dmin) + + return centers + def min_disp_path(start, path_of_candidates, exclude_coords=None, history_aware=False): r""""Returns a list of indices [i,j,k...] s.t. the distances @@ -1019,11 +1191,10 @@ def add_atom_idxs_widget(atom_idxs, ngl_wdg, color_list=None, radius=1): ngl_wdg.add_spacefill(selection=[iidxs], radius=radius, color=color, component=cc) elif _np.ndim(iidxs)>0 and len(iidxs)==2: ngl_wdg.add_distance(atom_pair=[[ii for ii in iidxs]], # yes it has to be this way for now - color=color, - #label_color='black', - label_size=0, + color=color, + label_size=0, component=cc) - # TODO add line thickness as **kwarg + # TODO add line thickness as **kwarg, see nglview usage questions for answer elif _np.ndim(iidxs) > 0 and len(iidxs) in [3,4]: ngl_wdg.add_spacefill(selection=iidxs, radius=radius, color=color, component=cc) else: diff --git a/molpx/tests/test_bmutils.py b/molpx/tests/test_bmutils.py index 43f5ba5..5db771d 100644 --- a/molpx/tests/test_bmutils.py +++ b/molpx/tests/test_bmutils.py @@ -11,6 +11,9 @@ from glob import glob import molpx +from scipy.spatial.distance import pdist as _pdist, squareform as _squareform + + class TestWithBPTIData(unittest.TestCase): r""" A class that contains all the TestCase with the MD info @@ -225,9 +228,36 @@ def setUp(self): def test_cluster_to_target(self): n_target = 15 - data = [np.random.randn(100, 1), np.random.randn(100,1)+10] - cl = _bmutils.regspace_cluster_to_target(data, n_target, n_try_max=10, delta=0, verbose=True) - assert n_target - 1 <= cl.n_clusters <= n_target + 1 + n_tol = 1 + data = [np.random.randn(5000, 1), np.random.randn(5000,1)+10] + cl = _bmutils.regspace_cluster_to_target_kmeans(data, n_target, k_centers=100, max_iter=100, n_tol=n_tol) + assert n_target - n_tol <= cl.n_clusters <= n_target + n_tol, (cl.n_clusters, n_tol) + + def test_regspace_from_distance_matrix(self): + data = np.random.rand(100, 2) + D = _squareform(_pdist(data)) + + centers = _bmutils.regspace_from_distance_matrix(D, D.mean()) + + # Re-compute distances, only for the centers + Drs = _pdist(data[centers]) + assert Drs.min()>=D.mean(), (Drs.min(), D.mean()) + + def test_interval_schachtelung(self): + + y = lambda x:x**2 # parabolic curve, monotically increasing between [0, +inf] + + interval = [2, 500] + eps = .1 + + target_y = np.random.randint(np.ceil(y(interval[0])), + np.floor(y(interval[1])), size=1).squeeze() + + x_sol = _bmutils.interval_schachtelung(y, [2, 500], target=target_y, eps=eps, + #verbose=True + ) + assert y(x_sol)-eps <= target_y < y(x_sol)+eps, (y(x_sol)-target_y, eps) + def test_catalogues(self): cl = _bmutils.regspace_cluster_to_target(self.data_for_cluster, 3, n_try_max=10, delta=0) From f7421fb3bb334029a7e7bb4299001f63074c434f Mon Sep 17 00:00:00 2001 From: gph82 Date: Thu, 3 May 2018 12:07:04 +0200 Subject: [PATCH 24/73] [generate] adapt to new method regspace_cluster_to_target_kmeans --- molpx/generate.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/molpx/generate.py b/molpx/generate.py index 0314714..f0752b1 100644 --- a/molpx/generate.py +++ b/molpx/generate.py @@ -117,8 +117,8 @@ def projection_paths(MD_trajectories, MD_top, projected_trajectories, "min_disp": _defdict(dict) } # Cluster in regspace along the dimension you want to advance, to approximately n_points - cl = _bmutils.regspace_cluster_to_target([jdata[:,[coord]] for jdata in idata], - n_points, n_try_max=3, + cl = _bmutils.regspace_cluster_to_target_kmeans([jdata[:,[coord]] for jdata in idata], + n_points, max_iter=3, verbose=verbose, ) @@ -298,7 +298,7 @@ def sample(MD_trajectories, MD_top, projected_trajectories, cl = projected_trajectories except: idata = _bmutils.data_from_input(projected_trajectories) - cl = _bmutils.regspace_cluster_to_target([dd[:,proj_idxs] for dd in idata], n_points, n_try_max=10, verbose=verbose) + cl = _bmutils.regspace_cluster_to_target_kmeans([dd[:,proj_idxs] for dd in idata], n_points, verbose=verbose) pos = cl.clustercenters cat_smpl = cl.sample_indexes_by_cluster(_np.arange(cl.n_clusters), n_geom_samples) From 6c2f99a3979c8ed9cd41c613b87ad2d5bd1b36dc Mon Sep 17 00:00:00 2001 From: gph82 Date: Sun, 6 May 2018 09:17:25 +0200 Subject: [PATCH 25/73] [generate.projection_paths] think about not returning the data --- molpx/generate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/molpx/generate.py b/molpx/generate.py index f0752b1..5cefa32 100644 --- a/molpx/generate.py +++ b/molpx/generate.py @@ -209,7 +209,7 @@ def projection_paths(MD_trajectories, MD_top, projected_trajectories, #TODO : consider storing the data in each dict. It's redundant but makes each dict kinda standalone - return paths_dict, idata + return paths_dict , idata # why were we returning idata in the first place? def sample(MD_trajectories, MD_top, projected_trajectories, From 7343dfa1be312952baa11467975c8dfa763405f0 Mon Sep 17 00:00:00 2001 From: gph82 Date: Sun, 6 May 2018 09:18:19 +0200 Subject: [PATCH 26/73] [_bmutils] New class corr_dict, uniform kmeans init, avoid geom.join in save_traj --- molpx/_bmutils.py | 43 ++++++++++++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/molpx/_bmutils.py b/molpx/_bmutils.py index 3722fa2..20f7ded 100644 --- a/molpx/_bmutils.py +++ b/molpx/_bmutils.py @@ -264,8 +264,7 @@ def regspace_cluster_to_target_kmeans(data, n_clusters_target, # 1. Arrive at an approximate dmin by # 1.1 Preliminary clustering - pre_cl = _cluster_kmeans(data, k=k_centers, stride=k_stride) - print(pre_cl.n_clusters, k_centers) + pre_cl = _cluster_kmeans(data, k=k_centers, stride=k_stride,init_strategy='uniform' ) # 1.2 Distance matrix D = _squareform(_pdist(pre_cl.clustercenters)) # 1.3 Define the objective function to be optimized for dmin by interval_schachtelung @@ -778,9 +777,16 @@ def save_traj_wrapper(traj_inp, indexes, outfile, top=None, stride=1, chunksize= chunksize=chunksize, image_molecules=image_molecules, verbose=verbose) elif isinstance(traj_inp[0], _md.Trajectory): file_idx, frame_idx = indexes[0] - geom_smpl = traj_inp[file_idx][frame_idx] - for file_idx, frame_idx in indexes[1:]: - geom_smpl = geom_smpl.join(traj_inp[file_idx][frame_idx]) + if False: + geom_smpl = traj_inp[file_idx][frame_idx] + for file_idx, frame_idx in indexes[1:]: + geom_smpl = geom_smpl.join(traj_inp[file_idx][frame_idx]) + # TODO this takes too much time for large topologies, consider copying + else: + xyz =[] + for file_idx, frame_idx in indexes: + xyz.append(traj_inp[file_idx].xyz[frame_idx].squeeze()) + geom_smpl = _md.Trajectory(xyz, traj_inp[0][0].top) else: raise TypeError("Cant handle input of type %s now"%(type(traj_inp[0]))) @@ -921,10 +927,6 @@ def smooth_geom(geom, n, geom_data=None, superpose=True, symmetric=True): Note: you might need to re-orient this averaged geometry again """ - # Input checking here, otherwise we're seriously in trouble - - - # Get the indices necessary for the running average frame_idxs, frame_windows = running_avg_idxs(geom.n_frames, n, symmetric=symmetric) @@ -941,6 +943,7 @@ def smooth_geom(geom, n, geom_data=None, superpose=True, symmetric=True): xyz = _np.zeros((len(frame_idxs), geom.n_atoms, 3)) for ii, idx in enumerate(frame_idxs): #print(ii, idx, frame_windows[ii][n]) + # TODO avoid casting an entire geometry (which triggers deepcopys which are time consuming) if superpose: xyz[ii,:,:] = geom[frame_windows[ii]].superpose(geom, frame=frame_windows[ii][n]).xyz.mean(0) else: @@ -1093,22 +1096,36 @@ def most_corr(correlation_input, geoms=None, proj_idxs=None, feat_name=None, n_a info.append({"lines":[], "name":iproj}) for jj, jidx in enumerate(most_corr_idxs[ii]): if avail_FT: - istr = 'Corr[%s|feat] = %2.1f for %-30s (feat nr. %u, atom idxs %s' % \ + istr = 'Corr[%s|feat] = %2.1f || %-30s || feat nr. %u, atom idxs %s' % \ (iproj, most_corr_vals[ii][jj], most_corr_labels[ii][jj], jidx, most_corr_atom_idxs[ii][jj]) else: - istr = 'Corr[%s|feat] = %2.1f (feat nr. %u)' % \ + istr = 'Corr[%s|feat] = %2.1f || nfeat nr. %u' % \ (iproj, most_corr_vals[ii][jj],jidx) info[-1]["lines"].append(istr) - corr_dict = {'idxs': most_corr_idxs, + corr_dict = CorrelationDict({'idxs': most_corr_idxs, 'vals': most_corr_vals, 'labels': most_corr_labels, 'feats': most_corr_feats, 'atom_idxs': most_corr_atom_idxs, - 'info':info} + 'info':info}) return corr_dict +class CorrelationDict(dict): + r""" This is just a dictionary + with the print method + rewritten to a pretty-print""" + def __str__(self): + nfeats = len(self["idxs"]) + output = 'Correlation dictionary for %u projections\n'%nfeats + for ii in range(nfeats): + for iline in self["info"][ii]["lines"]: + output += ' '+iline.replace(' || ','\n ')+'\n' + output+='\n' + + return output + def atom_idxs_from_feature(ifeat): r""" Return the atom_indices that best represent this input feature From a2b2b7fe916c262d5203d03168737e35572468ee Mon Sep 17 00:00:00 2001 From: gph82 Date: Sun, 6 May 2018 09:19:33 +0200 Subject: [PATCH 27/73] changes for the paper figures, to be reverted later --- molpx/visualize.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/molpx/visualize.py b/molpx/visualize.py index 39d8bbf..9dd5ae9 100644 --- a/molpx/visualize.py +++ b/molpx/visualize.py @@ -133,8 +133,8 @@ def FES(MD_trajectories, MD_top, projected_trajectories, done by the user place before calling this method. sample_kwargs : dictionary of named arguments, optional - named arguments for the function :obj:`molpx.visualize.sample`. Non-expert users can safely ignore this option. Examples - are :obj:`superpose` or :obj:`proj_idxs` + named arguments for the function :obj:`molpx.visualize.sample`. Non-expert users can safely ignore this option. + Examples are :obj:`superpose` or :obj:`proj_idxs` Returns ------- @@ -488,6 +488,7 @@ def traj(MD_trajectories, if corr_dicts[0]["feats"] != []: colors = _bmutils.matplotlib_colors_no_blue(ncycles=int(_np.ceil(_np.max(proj_idxs)/6.))) # Hack colors = [colors[ii] for ii in proj_idxs] + colors = ['red']*10 # for the paper, to be deleted later else: n_feats=0 else: @@ -1053,7 +1054,7 @@ def _sample(positions, geoms, ax, """ - assert isinstance(geoms, (list, _md.Trajectory)) + assert isinstance(geoms, (list, _md.Trajectory)), type(geoms) # Dow I need to smooth things out? if n_smooth > 0: @@ -1082,7 +1083,7 @@ def _sample(positions, geoms, ax, [ax.lines.pop() for ii in range(len(ax.lines))] # Plot the path on top of it if plot_path: - ax.plot(positions[:,0], positions[:,1], '-g', lw=3) + ax.plot(positions[:,0], positions[:,1], '-k', lw=3) # Link the axes ngl_wdg with the ngl ngl_wdg axes_wdg = _linkutils.link_ax_w_pos_2_nglwidget(ax, From 4bcc967623ba49294cd2a9596dae05567a8227b4 Mon Sep 17 00:00:00 2001 From: gph82 Date: Tue, 8 May 2018 11:13:51 +0200 Subject: [PATCH 28/73] [bmutils] new method get_repr_atom_for_residue --- molpx/_bmutils.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/molpx/_bmutils.py b/molpx/_bmutils.py index 20f7ded..fe3900a 100644 --- a/molpx/_bmutils.py +++ b/molpx/_bmutils.py @@ -1074,6 +1074,8 @@ def most_corr(correlation_input, geoms=None, proj_idxs=None, feat_name=None, n_a for ii in proj_idxs: icorr = corr[:, ii] + # NaN's will screw up this argsort, so + icorr[_np.isnan(icorr)] = 0 most_corr_idxs.append(_np.abs(icorr).argsort()[::-1][:n_args]) most_corr_vals.append([icorr[jj] for jj in most_corr_idxs[-1]]) if geoms is not None and avail_FT: @@ -1155,7 +1157,7 @@ def atom_idxs_from_feature(ifeat): return _np.repeat(ifeat.indexes, 3) elif isinstance(ifeat, _ResMinDF): # Comprehend all the lists!!!! - return _np.vstack([[list(ifeat.top.residue(pj).atoms_by_name('CA'))[0].index for pj in pair] for pair in ifeat.contacts]) + return _np.vstack([[get_repr_atom_for_residue(ifeat.top.residue(pj)).index for pj in pair] for pair in ifeat.contacts]) if isinstance(ifeat, (_DihF, _AF)): ai = ifeat.angle_indexes if ifeat.cossin: @@ -1164,6 +1166,26 @@ def atom_idxs_from_feature(ifeat): else: raise NotImplementedError('bmutils.atom_idxs_from_feature cannot interpret the atoms behind %s yet'%ifeat) +def get_repr_atom_for_residue(rr, cands = ['CA','C','C1'], one_atom_residues=True): + r""" + Tries to return a representative atom per residue. For AAs, it is the CA, + then, the next atom-name in cands is looked for + :param rr: mdtraj-residue object + :param one_atom_residues, bool, default is True + if the residue has one atom, return that atom directly + # TODO consider this not even an optarg and code it hard + """ + + if rr.n_atoms==1: + return list(rr.atoms)[0] + + for cc in cands: + out = list(rr.atoms_by_name(cc)) + if len(out)>0: + return out[0] + if len(out)==0: + raise ValueError("Could not find any atoms named %s in the residue %s"%(cands, rr)) + def add_atom_idxs_widget(atom_idxs, ngl_wdg, color_list=None, radius=1): r""" provided a list of atom_idxs and a ngl_wdg, try to represent them as well as possible in the ngl_wdg From 608339c0e448bb38d4d93b6bd5998d5694f80f19 Mon Sep 17 00:00:00 2001 From: gph82 Date: Tue, 8 May 2018 11:14:08 +0200 Subject: [PATCH 29/73] [visualize] fix hacky color handling --- molpx/visualize.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/molpx/visualize.py b/molpx/visualize.py index 9dd5ae9..0235f43 100644 --- a/molpx/visualize.py +++ b/molpx/visualize.py @@ -486,9 +486,9 @@ def traj(MD_trajectories, corr_dicts = [_bmutils.most_corr(projection, geoms=igeom, proj_idxs=proj_idxs, n_args=n_feats) for igeom in geoms] if corr_dicts[0]["feats"] != []: - colors = _bmutils.matplotlib_colors_no_blue(ncycles=int(_np.ceil(_np.max(proj_idxs)/6.))) # Hack + colors = _bmutils.matplotlib_colors_no_blue(ncycles=int(_np.ceil((_np.max(proj_idxs)+1)/6.))) # Hack colors = [colors[ii] for ii in proj_idxs] - colors = ['red']*10 # for the paper, to be deleted later + #colors = ['red']*10 # for the paper, to be deleted later else: n_feats=0 else: From f9d419893567772e39f1579705ea31efd3694548 Mon Sep 17 00:00:00 2001 From: gph82 Date: Tue, 8 May 2018 14:20:04 +0200 Subject: [PATCH 30/73] [_bmutils] new method atom_idxs_from_general_input --- molpx/_bmutils.py | 38 ++++++++++++++++++++++++-------------- molpx/visualize.py | 2 +- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/molpx/_bmutils.py b/molpx/_bmutils.py index fe3900a..cfdee71 100644 --- a/molpx/_bmutils.py +++ b/molpx/_bmutils.py @@ -1087,12 +1087,7 @@ def most_corr(correlation_input, geoms=None, proj_idxs=None, feat_name=None, n_a most_corr_labels.append([featurizer.describe()[jj] for jj in most_corr_idxs[-1]]) if avail_FT: - if len(featurizer.active_features) > 1: - pass - # TODO write a warning - else: - ifeat = featurizer.active_features[0] - most_corr_atom_idxs.append(atom_idxs_from_feature(ifeat)[most_corr_idxs[-1]]) + most_corr_atom_idxs.append([atom_idxs_from_general_input(featurizer)[ii] for ii in most_corr_idxs[-1]]) for ii, iproj in enumerate(proj_names): info.append({"lines":[], "name":iproj}) @@ -1128,6 +1123,27 @@ def __str__(self): return output +def atom_idxs_from_general_input(input): + r""" + Provided with anything that has a list of ifet.active_features, return the representative + atom indices for each feature component + :param input: can be TICA, PCA, or MDfeaturizer + :return: + """ + + if isinstance(input, (_TICA, _PCA)): + MDfeat = input.data_producer.featurizer + elif isinstance(input, _MDFeaturizer): + MDfeat = input + else: + raise TypeError("Sorry, input has to be of type %s, not %s" % ([_MDFeaturizer, _TICA, _PCA], type(input))) + + # Get atom lists for each active feature + out_idxs = [atom_idxs_from_feature(jfeat) for jfeat in MDfeat.active_features] + # Get one singlelist + return [item for sublist in out_idxs for item in sublist] + + def atom_idxs_from_feature(ifeat): r""" Return the atom_indices that best represent this input feature @@ -1135,10 +1151,9 @@ def atom_idxs_from_feature(ifeat): Parameters ---------- - ifeat : input feature, can be of two types: + ifeat : input featurizer: a :any:`pyemma.coordinates.featurizer` (Distancefeaturizer, AngleFeaturizer etc) or - a :any:`pyemma.coordinates.data.featurization.featurizer.MDFeaturizer` itself, in which case the first of the - obj:`ifeat.active_features` will be used + Returns ------- @@ -1146,11 +1161,6 @@ def atom_idxs_from_feature(ifeat): atom_indices : list with the atoms indices representative of this feature, whatever the feature """ - try: - ifeat = ifeat.active_features[0] - except AttributeError: - pass - if isinstance(ifeat, _DF) and not isinstance(ifeat, _ResMinDF): return ifeat.distance_indexes elif isinstance(ifeat, _SF): diff --git a/molpx/visualize.py b/molpx/visualize.py index 0235f43..c7bc3eb 100644 --- a/molpx/visualize.py +++ b/molpx/visualize.py @@ -808,7 +808,7 @@ def feature(feat, """ idxs = _bmutils.listify_if_int(idxs) - atom_idxs = _bmutils.atom_idxs_from_feature(feat)[idxs] + atom_idxs = _bmutils.atom_idxs_from_general_input(feat)[idxs] if color_list is None: color_list = ['blue'] * len(idxs) From 747fe021021978fecd2696200108125e5e09e2a2 Mon Sep 17 00:00:00 2001 From: gph82 Date: Tue, 15 May 2018 15:54:54 +0200 Subject: [PATCH 31/73] [visualize] prepering traj() to allow for input feature --- molpx/visualize.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/molpx/visualize.py b/molpx/visualize.py index c7bc3eb..04981e0 100644 --- a/molpx/visualize.py +++ b/molpx/visualize.py @@ -333,6 +333,7 @@ def traj(MD_trajectories, tunits = 'frames', traj_selection = None, projection = None, + input_feature_traj = None, n_feats = 1, ): r"""Link one or many :obj:`projected trajectories`, [Y_0(t), Y_1(t)...], with the :obj:`MD_trajectories` that @@ -343,7 +344,6 @@ def traj(MD_trajectories, MD_trajectories : str, or list of strings with the filename(s) the the molecular dynamics (MD) trajectories. Any file extension that :py:obj:`mdtraj` (.xtc, .dcd etc) can read is accepted. - Alternatively, a single :obj:`mdtraj.Trajectory` object or a list of them can be given as input. MD_top : str to topology filename or directly :obj:`mdtraj.Topology` object @@ -402,9 +402,13 @@ def traj(MD_trajectories, might have generated this projection, like a :obj:`pyemma.coordinates.transform.TICA` or a :obj:`pyemma.coordinates.transform.PCA` - Pass this object along and observe and the features that are most correlated with the projections + Pass this object along and observe the features that are most correlated with the projections will be plotted for the active trajectory, allowing the user to establish a visual connection between the projected coordinate and the original features (distances, angles, contacts etc) + These trajectories will be re-computed by applyiing + :obj:`projection.transform(MD_trajectories)', unless :obj:`input_feature_traj` is parsed + + input_feature_traj : TODO n_feats : int, default is 1 If a :obj:`projection` is passed along, the first n_feats features that most correlate the From 6548fcb5570873162c4c7e9700a94c9f27b2583a Mon Sep 17 00:00:00 2001 From: gph82 Date: Fri, 18 May 2018 15:10:21 +0200 Subject: [PATCH 32/73] [visualize] new method contacts() and optarg verbose for FES() --- molpx/visualize.py | 94 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 93 insertions(+), 1 deletion(-) diff --git a/molpx/visualize.py b/molpx/visualize.py index 04981e0..83911b7 100644 --- a/molpx/visualize.py +++ b/molpx/visualize.py @@ -21,6 +21,8 @@ import warnings as _warnings +from itertools import product as _it_prod + # All calls to nglview call actually this function def _nglwidget_wrapper(geom, ngl_wdg=None, n_small=10): r""" Wrapper to nlgivew.show_geom's method that allows for some other automatic choice of @@ -77,6 +79,7 @@ def FES(MD_trajectories, MD_top, projected_trajectories, proj_labels='proj', n_overlays=1, atom_selection=None, + verbose=False, **sample_kwargs): r""" Return a molecular visualization widget connected with a free energy plot. @@ -132,6 +135,9 @@ def FES(MD_trajectories, MD_top, projected_trajectories, If :obj:`MD_trajectories` is already a (list of) :obj:`mdtraj.Trajectory` objects, the atom-slicing can be done by the user place before calling this method. + verbose : bool, default is False + Be verbose while computing the FES + sample_kwargs : dictionary of named arguments, optional named arguments for the function :obj:`molpx.visualize.sample`. Non-expert users can safely ignore this option. Examples are :obj:`superpose` or :obj:`proj_idxs` @@ -184,7 +190,8 @@ def FES(MD_trajectories, MD_top, projected_trajectories, return_data=True, n_geom_samples=n_overlays, keep_all_samples=keep_all_samples, - proj_stride=proj_stride + proj_stride=proj_stride, + verbose=verbose ) data = _np.vstack(data) @@ -1111,3 +1118,88 @@ def _sample(positions, geoms, ax, return ngl_wdg, axes_wdg +def contacts(contact_map, input, average=False, panelsize=4): + r""" + Provide a contact map and a widget or geometry, return an interactive contact map + + :param contact_map: + :param residue_idxs: + :return: + """ + + # Add one axis to the input if necessary + if _np.ndim(contact_map)==2: + contact_map = _np.array(contact_map, ndmin=3) + + # Check that the number of frames match if no average is requested + if _np.ndim(contact_map)==3 and not average: + assert len(contact_map) == input.n_frames, "If average is False, the number of contact maps (%u) must " \ + "match the number of frames in input (%u)" % ( + len(contact_map), input.n_frames) + # Assert squaredness + assert all([ict.shape[0] == ict.shape[1] for ict in contact_map]), "The input has to be a square matrix" + + # Needed arrays + nres = contact_map[0].shape[0] + residue_idxs = _np.arange(nres) + residue_pairs = _np.vstack(_it_prod(residue_idxs, residue_idxs)) + positions = _np.vstack(_it_prod(range(nres), range(nres))) + + # Create a color list + cmap = _get_cmap('rainbow') + cmap_table = _np.linspace(0, 1, len(positions)) + sticky_colors_hex = [_rgb2hex(cmap(ii)) for ii in _np.random.permutation(cmap_table)] + + # Instantiate widget + iwd = _nglwidget_wrapper(input) + + # Do the plot + _plt.ioff() + _plt.figure(figsize=(panelsize, panelsize)) + iax = _plt.gca() + # _plt.plot(positions[:,0], positions[:,1], ' ok') + # Make the average if wanted + if average: + iax.matshow(_np.average(contact_map, axis=0)) + else: + # Monkey-Patch the matshow_data into the object + iwd._MatshowData = {"image" : iax.matshow(contact_map[0]), + "data" : contact_map} + _plt.ion() + + + # Relabel the plot + # TODO make sure that zooming works even if a sub-set of res_idxs is given + """ + for axtype in ['x', 'y']: + tic_idxs = [int(tl) for tl in getattr(iax, 'get_%sticks'%axtype)()[1:-1]] + tic_labels = ['']+['%u'%residue_idxs[ii] for ii in tic_idxs]+[''] + getattr(iax,'set_%sticklabels'%axtype)(tic_labels) + """ + + # Monkey-Patch the ContactInNGLWidgets into the widget + iwd._CtcsInWid = [_linkutils.ContactInNGLWidget + (iwd, [_bmutils.get_repr_atom_for_residue(input.top.residue(aa)).index for aa in [ii,jj]], rp_idx, + #verbose=True, + color= sticky_colors_hex[rp_idx] + ) + for rp_idx, (ii,jj) in enumerate(residue_pairs)] + + # Turn axes into a widget + axes_wdg = _linkutils.link_ax_w_pos_2_nglwidget(iax, + positions, + iwd, + crosshairs=False, + #directionality='a2w', + dot_color='None', + #**link_ax2wdg_kwargs + ) + + iwd._set_size(*['%fin' % inches for inches in iax.get_figure().get_size_inches()]) + #iax.figure.tight_layout() + axes_wdg.canvas.set_window_title("Contact Map") + + outbox = _linkutils.MolPXHBox([iwd, axes_wdg.canvas]) + _linkutils.auto_append_these_mpx_attrs(outbox, input, iax, _plt.gcf(), iwd, axes_wdg, positions) + + return outbox \ No newline at end of file From 6ba821f2a82c5b43422307ffe69e7d5e21942ad1 Mon Sep 17 00:00:00 2001 From: gph82 Date: Fri, 18 May 2018 15:11:16 +0200 Subject: [PATCH 33/73] [_bmutils] refactor interval_schachtelung() and elimiante unused optarg in get_repr_atom_for_residue() --- molpx/_bmutils.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/molpx/_bmutils.py b/molpx/_bmutils.py index cfdee71..25fcda3 100644 --- a/molpx/_bmutils.py +++ b/molpx/_bmutils.py @@ -308,9 +308,10 @@ def interval_schachtelung(f, interval, target=0, eps=1, verbose=False): left, right = interval[0], interval[1] def inform(left, fl, right, fr, middle, fm, delta, eps, str0=''): print(str0) - print('%04.2f: %04.2f' % (left, fl)) - print('%04.2f: %04.2f %f %f' % (middle, fm, delta, eps), ~(delta >= eps)) - print('%04.2f: %04.2f' % (right, fr)) + print('left: %04.2f, f(left) = %04.2f' % (left, fl)) + print('middle: %04.2f, f(middle) = %04.2f' % (middle, fm)) + print('right: %04.2f, f(right) = %04.2f' % (right, fr)) + print('delta, eps = %f %f, conv: %s'%(delta, eps, ~(delta >= eps))) print() middle = (left + right) / 2 @@ -319,13 +320,14 @@ def inform(left, fl, right, fr, middle, fm, delta, eps, str0=''): if verbose: inform(left, fl, right, fr, middle, fm, delta, eps, str0='Init:') + cc = 0 while delta >= eps: middle = (left + right) / 2 fm = f(middle) delta = _np.abs(fm - target) if verbose: - inform(left, fl, right, fr, middle, fm, delta, eps) + inform(left, fl, right, fr, middle, fm, delta, eps, str0='Iter %u'%cc) if fl <= target <= fm or fl >= target >= fm: right, fr = middle, fm @@ -334,6 +336,7 @@ def inform(left, fl, right, fr, middle, fm, delta, eps, str0=''): else: print(fl, target, fm, middle) raise Exception("Failed while optimizing") + cc += 1 return middle @@ -1176,14 +1179,11 @@ def atom_idxs_from_feature(ifeat): else: raise NotImplementedError('bmutils.atom_idxs_from_feature cannot interpret the atoms behind %s yet'%ifeat) -def get_repr_atom_for_residue(rr, cands = ['CA','C','C1'], one_atom_residues=True): +def get_repr_atom_for_residue(rr, cands = ['CA','C','C1']): r""" Tries to return a representative atom per residue. For AAs, it is the CA, then, the next atom-name in cands is looked for :param rr: mdtraj-residue object - :param one_atom_residues, bool, default is True - if the residue has one atom, return that atom directly - # TODO consider this not even an optarg and code it hard """ if rr.n_atoms==1: From 3c4845dc19228e0501037de521d1ffc77d5fc2b6 Mon Sep 17 00:00:00 2001 From: gph82 Date: Fri, 18 May 2018 15:18:28 +0200 Subject: [PATCH 34/73] [_linkutils] new class ContactInNGLWidget; link_ax_w_pos_2_nglwidget handles "_CtcsInWid"; ClickOnAxisListener handles ContactInNGLWidgets, recomputes kdtree on zooming --- molpx/_linkutils.py | 111 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 110 insertions(+), 1 deletion(-) diff --git a/molpx/_linkutils.py b/molpx/_linkutils.py index f37a763..33af735 100644 --- a/molpx/_linkutils.py +++ b/molpx/_linkutils.py @@ -4,12 +4,13 @@ from matplotlib.colors import is_color_like as _is_color_like from matplotlib.axes import Axes as _mplAxes from matplotlib.figure import Figure as _mplFigure +from matplotlib.patches import Rectangle as _Rectangle from IPython.display import display as _ipydisplay from pyemma.util.types import is_int as _is_int from scipy.spatial import cKDTree as _cKDTree -from ._bmutils import get_ascending_coord_idx +from ._bmutils import get_ascending_coord_idx, add_atom_idxs_widget from mdtraj import Trajectory as _mdTrajectory from nglview import NGLWidget as _NGLwdg @@ -91,8 +92,10 @@ def __init__(self, ngl_wdg, crosshairs, showclick_objs, ax, pos, self.pos = pos self.list_mpl_objects_to_update = list_mpl_objects_to_update self.list_of_dots = [None]*self.pos.shape[0] + self.list_of_rects = [None] * self.pos.shape[0] self.fig_size = self.ax.figure.get_size_inches() self.kdtree = None + self.axlims = _np.hstack((self.ax.get_xlim(), self.ax.get_ylim())) def build_tree(self): # Use ax.transData to compute distance in pixels @@ -104,6 +107,19 @@ def build_tree(self): def figure_changed_size(self): return not _np.allclose(self.fig_size, self.ax.figure.get_size_inches()) + @property + def axes_changed(self): + current_axlims = _np.hstack((self.ax.get_xlim(), self.ax.get_ylim())) + return not _np.allclose(current_axlims, self.axlims) + + def remove_last_contacts(self): + try: + self.ngl_wdg._CtcsInWid[self.ngl_wdg._CtcsLast].hide() + self.list_of_rects[self.ngl_wdg._CtcsLast].remove() + self.list_of_rects[self.ngl_wdg._CtcsLast] = None + except AttributeError: + pass + def __call__(self, event): # Wait for the first click or a a figsize change # to build the kdtree @@ -111,6 +127,16 @@ def __call__(self, event): self.build_tree() self.fig_size = self.ax.figure.get_size_inches() + # Check axes changes (e.g. in zooming) + if self.axes_changed: + self.build_tree() # rebuild tree + # store new limits + self.axlims = _np.hstack((self.ax.get_xlim(), + self.ax.get_ylim())) + + # Remove spurious contacts from the zooming-click + self.remove_last_contacts() + # Was the click inside the bounding box? if self.ax.get_window_extent().contains(event.x, event.y): if self.crosshairs: @@ -122,6 +148,8 @@ def __call__(self, event): update2Dlines(idot, self.pos[index, 0], self.pos[index, 1]) self.ngl_wdg.isClick = True + + # The sticky cases like _CtcsInWid or _GeomsInWid are update here instead of via the mplobjects to update if hasattr(self.ngl_wdg, '_GeomsInWid'): # We're in a sticky situation if event.button == 1: @@ -138,6 +166,29 @@ def __call__(self, event): if not self.ngl_wdg._GeomsInWid[index].is_visible() and self.list_of_dots[index] is not None: self.list_of_dots[index].remove() self.list_of_dots[index] = None + + + elif hasattr(self.ngl_wdg, '_CtcsInWid'): + if event.button == 1: + self.ngl_wdg._CtcsInWid[index].show() + # Plot and store the rectangle in case there wasn't + if self.list_of_rects[index] is None: + rectangle_padding = 0 # TODO future plans to make rectangles catch more than one ctc + x, y = self.pos[index] + self.list_of_rects[index] = self.ax.add_patch(_Rectangle( + (x - .5 - rectangle_padding, + y - .5 - rectangle_padding), # (x,y) + 1 + 2 * rectangle_padding, # width + 1 + 2 * rectangle_padding, # height, + fill=False, + linewidth=2, + edgecolor=self.ngl_wdg._CtcsInWid[index].color)) + elif event.button in [2,3]: + # Pressed left + self.ngl_wdg._CtcsInWid[index].hide() + if self.list_of_rects[index] is not None: + self.list_of_rects[index].remove() + self.list_of_rects[index] = None else: # We're not sticky, just go to the frame self.ngl_wdg.frame = index @@ -238,6 +289,61 @@ def __call__(self, change): print("caught index error with index %s (new=%s, old=%s)" % (_idx, change["new"], change["old"])) #print("set xy = (%s, %s)" % (x[_idx], y[_idx])) + if hasattr(self.ngl_wdg, '_MatshowData'): + self.ngl_wdg._MatshowData["image"].set_data(self.ngl_wdg._MatshowData["data"][_idx]) + + +class ContactInNGLWidget(object): + r""" + returns an object that is aware about its own atom-indices and + its own representation index in the widget. It also has access to the widget itself + With that knowlegde, one can use the methods .show() and .hide() + + param : ngl_widget, the widget upon which to superpose the contact + param : atom_indices, len(2), atom indices involved in this contact + param : contact_index, int, the index corresponding to this contact + """ + + + def __init__(self, ngl_wdg, atom_indices, contact_index, + component_to_draw_on=0, + verbose=False, + color=None): + assert len(atom_indices)==2, "ContactInNGLWidget takes a list with two elements as input, not len(%u)"%len(atom_indices) + assert [isinstance(ii, int) for ii in atom_indices], "The atom indices have to be type int" + + self.atom_indices = atom_indices + self.ngl_wdg = ngl_wdg + self.contact_index = contact_index + self.verbose = verbose + self.top = self.ngl_wdg._trajlist[component_to_draw_on].trajectory + self.comp = component_to_draw_on + self.shown = False + self.color = color + + def show(self): + if not self.shown: + if self.verbose: + print("Showing %s "%[self.top.atom(ii).residue for ii in self.atom_indices]) + + self.shown = True + add_atom_idxs_widget([self.atom_indices], self.ngl_wdg, color_list=[self.color]) + self.ngl_wdg._CtcsLast = self.contact_index + + def hide(self): + if self.shown: + #print(self.ngl_wdg._ngl_repr_dict[str(self.comp)].keys()) + for key in self.matching_repr_keys: + self.ngl_wdg._remove_representation(self.comp, repr_index=int(key)) + self.shown = False + + @property + def matching_repr_keys(self): + # Given that the _ngl_repr_dict gets updated elsewhere, this is the most robust way of + # finding this contact's representations + return [key for key, value in self.ngl_wdg._ngl_repr_dict[str(self.comp)].items() if value["type"] == "distance" + and _np.allclose(_np.sort(value["params"]["atomPair"]), _np.sort(self.atom_indices))] + class GeometryInNGLWidget(object): r""" returns an object that is aware of where its geometries are located in the NGLWidget their representation status @@ -400,6 +506,8 @@ def link_ax_w_pos_2_nglwidget(ax, pos, ngl_wdg, # Are we in a sticky situation? if hasattr(ngl_wdg, '_GeomsInWid'): sticky = True + elif hasattr(ngl_wdg, "_CtcsInWid"): + pass else: assert ngl_wdg.trajectory_0.n_frames == pos.shape[0], \ ("Mismatching frame numbers %u vs %u" % (ngl_wdg.trajectory_0.n_frames, pos.shape[0])) @@ -462,6 +570,7 @@ def link_ax_w_pos_2_nglwidget(ax, pos, ngl_wdg, # Connect axes to widget axes_widget = _AxesWidget(ax) if directionality in [None, 'a2w']: + # TODO since zooming events also contain button_release events this will be triggered as well axes_widget.connect_event('button_release_event', CLA_listener) # Connect widget to axes From ab9b40396b568d2298d0e460133730ba9f1cf8fb Mon Sep 17 00:00:00 2001 From: gph82 Date: Fri, 18 May 2018 17:47:31 +0200 Subject: [PATCH 35/73] [_bmutils] regspace_cluster_to_target_kmeans catches non-list inputs and listyfies --- molpx/_bmutils.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/molpx/_bmutils.py b/molpx/_bmutils.py index 25fcda3..51b4e35 100644 --- a/molpx/_bmutils.py +++ b/molpx/_bmutils.py @@ -254,17 +254,22 @@ def regspace_cluster_to_target_kmeans(data, n_clusters_target, tested:True """ - # The parameter k_centers is just an initialization and has only impact on speed, - # not on the deterministic result. We catch some pathological data inputs - # Amount of strided frames + # Listify if only one array + if isinstance(data, _np.ndarray): + data = [data] + + # The parameter k_centers=1000 is just an initialization and has only impact on speed. + # TODO: consider hard coding it + # We try to catch some pathological data inputs with too little data n_frames_kmeans = _np.sum([len(idata[::k_stride]) for idata in data]) if n_frames_kmeans< k_centers: k_stride = 1 n_frames_kmeans = _np.sum([len(idata[::k_stride]) for idata in data]) + k_centers = _np.min((n_frames_kmeans, k_centers)) # 1. Arrive at an approximate dmin by # 1.1 Preliminary clustering - pre_cl = _cluster_kmeans(data, k=k_centers, stride=k_stride,init_strategy='uniform' ) + pre_cl = _cluster_kmeans(data, k=k_centers, stride=k_stride, init_strategy='uniform' ) # 1.2 Distance matrix D = _squareform(_pdist(pre_cl.clustercenters)) # 1.3 Define the objective function to be optimized for dmin by interval_schachtelung @@ -1131,7 +1136,7 @@ def atom_idxs_from_general_input(input): Provided with anything that has a list of ifet.active_features, return the representative atom indices for each feature component :param input: can be TICA, PCA, or MDfeaturizer - :return: + :return: list of input.dimension() with the atoms involved in each feature """ if isinstance(input, (_TICA, _PCA)): @@ -1143,7 +1148,7 @@ def atom_idxs_from_general_input(input): # Get atom lists for each active feature out_idxs = [atom_idxs_from_feature(jfeat) for jfeat in MDfeat.active_features] - # Get one singlelist + # Get one single list return [item for sublist in out_idxs for item in sublist] From 043bef7cfb5b0f27c832e2d02b42ca01f28b9c08 Mon Sep 17 00:00:00 2001 From: gph82 Date: Fri, 18 May 2018 17:48:20 +0200 Subject: [PATCH 36/73] [visualize] feature() accepts only MDFeaturizer (corrected docstring) and slice(idxs) for lists --- molpx/visualize.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/molpx/visualize.py b/molpx/visualize.py index 83911b7..0811466 100644 --- a/molpx/visualize.py +++ b/molpx/visualize.py @@ -789,7 +789,7 @@ def feature(feat, ---------- featurizer : py:obj:`_MDFeautrizer` - A PyEMMA MDFeaturizer object (either a feature or a featurizer, works with both) + A PyEMMA MDfeaturizer object with any number of .active_features() widget : None or nglview widget Provide an already existing widget to visualize the correlations on top of. This is only for expert use, @@ -819,7 +819,7 @@ def feature(feat, """ idxs = _bmutils.listify_if_int(idxs) - atom_idxs = _bmutils.atom_idxs_from_general_input(feat)[idxs] + atom_idxs = _bmutils.atom_idxs_from_general_input(feat)[slice(*idxs)] if color_list is None: color_list = ['blue'] * len(idxs) From 822924f70bd7384425fc6417049bc438b8afa92c Mon Sep 17 00:00:00 2001 From: gph82 Date: Fri, 18 May 2018 17:49:21 +0200 Subject: [PATCH 37/73] [test_molPX] reduced nr input features s.t. TICA does not return rubbish with so few frames --- molpx/tests/test_molPX.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/molpx/tests/test_molPX.py b/molpx/tests/test_molPX.py index 27d081e..94f8981 100644 --- a/molpx/tests/test_molPX.py +++ b/molpx/tests/test_molPX.py @@ -19,7 +19,7 @@ def setUp(self): self.tempdir = tempfile.mkdtemp('test_molpx') self.projected_file = os.path.join(self.tempdir,'Y.npy') feat = pyemma.coordinates.featurizer(self.topology) - feat.add_all() + feat.add_selection(np.arange(3)) source = pyemma.coordinates.source(self.MD_trajectory, features=feat) self.tica = pyemma.coordinates.tica(source,lag=1, dim=2) Y = self.tica.get_output()[0] From e744038e596ece24edd4064cb98799e141d06426 Mon Sep 17 00:00:00 2001 From: gph82 Date: Fri, 18 May 2018 17:52:38 +0200 Subject: [PATCH 38/73] [test_bmutils] refactor to regspace_cluster_to_target_kmeans --- molpx/tests/test_bmutils.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/molpx/tests/test_bmutils.py b/molpx/tests/test_bmutils.py index 5db771d..e153fb1 100644 --- a/molpx/tests/test_bmutils.py +++ b/molpx/tests/test_bmutils.py @@ -260,7 +260,7 @@ def test_interval_schachtelung(self): def test_catalogues(self): - cl = _bmutils.regspace_cluster_to_target(self.data_for_cluster, 3, n_try_max=10, delta=0) + cl = _bmutils.regspace_cluster_to_target_kmeans(self.data_for_cluster, 3, max_iter=10, n_tol=0) #print(cl.clustercenters) cat_idxs, cat_cont = _bmutils.catalogues(cl) @@ -294,7 +294,7 @@ def test_catalogues(self): [13, 2]]) def test_catalogues_with_data(self): - cl = _bmutils.regspace_cluster_to_target(self.data_for_cluster, 3, n_try_max=10, delta=0) + cl = _bmutils.regspace_cluster_to_target_kmeans(self.data_for_cluster, 3, max_iter=10, n_tol=0) #print(cl.clustercenters) cat_idxs, cat_cont = _bmutils.catalogues(cl, data=self.data_for_cluster) @@ -329,7 +329,7 @@ def test_catalogues_with_data(self): def test_catalogues_sort_by_zero(self): - cl = _bmutils.regspace_cluster_to_target(self.data_for_cluster, 3, n_try_max=10, delta=0) + cl = _bmutils.regspace_cluster_to_target_kmeans(self.data_for_cluster, 3, max_iter=10, n_tol=0) cat_idxs, cat_cont = _bmutils.catalogues(cl, sort_by=0) # This test is extra, since this is a pure pyemma function @@ -358,7 +358,7 @@ def test_catalogues_sort_by_zero(self): [13, 2]]) def test_catalogues_sort_by_other_than_zero(self): - cl = _bmutils.regspace_cluster_to_target(self.data_for_cluster, 3, n_try_max=10, delta=0) + cl = _bmutils.regspace_cluster_to_target_kmeans(self.data_for_cluster, 3, max_iter=10, n_tol=0) cat_idxs, cat_cont = _bmutils.catalogues(cl, sort_by=1) # This test is extra, since this is a pure pyemma functions assert np.allclose(cat_idxs[0], [[1,0]]) @@ -436,7 +436,7 @@ def setUp(self): z = np.random.permutation(z) coords[:, -1, -1] = z self.traj = md.Trajectory(coords, traj.top) - self.cl = _bmutils.regspace_cluster_to_target(self.traj.xyz[:, -1, -1], 50, n_try_max=10) + self.cl = _bmutils.regspace_cluster_to_target_kmeans(self.traj.xyz[:, -1, -1], 50, max_iter=10) self.cat_smpl = self.cl.sample_indexes_by_cluster(np.arange(self.cl.n_clusters), n_geom_samples) self.geom_smpl = self.traj[np.vstack(self.cat_smpl)[:,1]] self.geom_smpl = _bmutils.re_warp(self.geom_smpl, [n_geom_samples] * self.cl.n_clusters) @@ -523,8 +523,7 @@ class TestVisualPath(TestWithBPTIData): def setUpClass(self): TestWithBPTIData.setUpClass() n_sample = 20 - self.cl_cont = _bmutils.regspace_cluster_to_target([ixyz[:, :2] for ixyz in self.xyz_flat], n_sample, verbose=True, n_try_max=10, - #delta=0 + self.cl_cont = _bmutils.regspace_cluster_to_target_kmeans([ixyz[:, :2] for ixyz in self.xyz_flat], n_sample, verbose=True, max_iter=10, ) self.cat_idxs, self.cat_data = _bmutils.catalogues(self.cl_cont) # Create the MD catalogue with pyemma From a9f3d5f895e1ace099864cfe2df8330bc1d2799a Mon Sep 17 00:00:00 2001 From: gph82 Date: Fri, 18 May 2018 17:53:17 +0200 Subject: [PATCH 39/73] [test_visualize] update to visualize.feature() taking only MDFeaturizer --- molpx/tests/test_visualize.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/molpx/tests/test_visualize.py b/molpx/tests/test_visualize.py index 24400b6..4fafa07 100644 --- a/molpx/tests/test_visualize.py +++ b/molpx/tests/test_visualize.py @@ -240,16 +240,16 @@ def setUpClass(self): def test_feature(self): plt.figure() iwd = nglview.show_mdtraj(self.MD_trajectories[0]) - visualize.feature(self.feat.active_features[0], iwd) + visualize.feature(self.feat, iwd) def test_feature_color_list(self): plt.figure() iwd = nglview.show_mdtraj(self.MD_trajectories[0]) - visualize.feature(self.feat.active_features[0], iwd, + visualize.feature(self.feat, iwd, idxs=[0,1], color_list=['blue']) try: - visualize.feature(self.feat.active_features[0], iwd, + visualize.feature(self.feat, iwd, idxs=[0,1], color_list='blue') except TypeError: From 2f99f57e739dfcd555f93c7c3f82d1e71e09f25d Mon Sep 17 00:00:00 2001 From: gph82 Date: Sat, 19 May 2018 19:34:22 +0200 Subject: [PATCH 40/73] [_bmutils] get_index_ascending_coord check for len(idxs) instead of idxs==[] --- molpx/_bmutils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/molpx/_bmutils.py b/molpx/_bmutils.py index 51b4e35..0c072ac 100644 --- a/molpx/_bmutils.py +++ b/molpx/_bmutils.py @@ -282,7 +282,7 @@ def regspace_cluster_to_target_kmeans(data, n_clusters_target, ii = 0 while (_np.abs(cl.n_clusters-n_clusters_target) > n_tol): if ii >= max_iter: - print("Reaced max_iter %u."%ii) + print("Reached max_iter %u."%ii) break # Distance matrix D = _squareform(_pdist(cl.clustercenters)) @@ -845,7 +845,7 @@ def get_ascending_coord_idx(pos, fail_if_empty=False, fail_if_more_than_one=Fals idxs = _np.argwhere(_np.all(_np.diff(pos,axis=0)>0, axis=0)).squeeze() if isinstance(idxs, _np.ndarray) and idxs.ndim==0: idxs = idxs[()] - elif idxs == [] and fail_if_empty: + elif len(idxs)==0 and fail_if_empty: raise ValueError('No column was found in ascending order') if _np.size(idxs) > 1: From d0cac78fa0d47e5156bf75df1402fb877d1c9f28 Mon Sep 17 00:00:00 2001 From: gph82 Date: Sat, 19 May 2018 19:35:29 +0200 Subject: [PATCH 41/73] [test_visualize] minor refactor --- molpx/tests/test_visualize.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/molpx/tests/test_visualize.py b/molpx/tests/test_visualize.py index 4fafa07..78f8b87 100644 --- a/molpx/tests/test_visualize.py +++ b/molpx/tests/test_visualize.py @@ -33,8 +33,8 @@ def test_simplest_inputs_memory_small_max_frames(self): visualize.traj(self.MD_trajectories, self.MD_topology, self.Ys, max_frames=3) def test_simplest_inputs_disk(self): - visualize.traj(self.MD_trajectory_files, self.MD_topology_file, self.projected_files) - visualize.traj(self.MD_trajectory_files, self.MD_topology_file, [ifile.replace('.npy', '.dat') for ifile in self.projected_files]) + visualize.traj(self.MD_trajectory_files, self.MD_topology_file, self.projected_files_npy) + visualize.traj(self.MD_trajectory_files, self.MD_topology_file, [ifile.replace('.npy', '.dat') for ifile in self.projected_files_npy]) def test_simplest_inputs_memory_and_proj(self): visualize.traj(self.MD_trajectories, self.MD_topology, self.Ys, projection=self.tica) @@ -179,7 +179,7 @@ def setUpClass(self): def test_just_works_min_input_disk(self): molpx.visualize.FES(self.MD_trajectory_files, self.MD_topology_file, - self.projected_files) + self.projected_files_npy) def test_just_works_min_input_memory(self): molpx.visualize.FES(self.MD_trajectories, From 7f572484fddf0fe0c4031ca2f554d64af6c67b33 Mon Sep 17 00:00:00 2001 From: gph82 Date: Sat, 19 May 2018 19:36:12 +0200 Subject: [PATCH 42/73] [test_bmutils] increase use the TestWithBPTIData class, add new tests --- molpx/tests/test_bmutils.py | 194 ++++++++++++++++++++++++------------ 1 file changed, 130 insertions(+), 64 deletions(-) diff --git a/molpx/tests/test_bmutils.py b/molpx/tests/test_bmutils.py index e153fb1..55239ad 100644 --- a/molpx/tests/test_bmutils.py +++ b/molpx/tests/test_bmutils.py @@ -32,45 +32,38 @@ def setUpClass(self): self.Fs = self.source.get_output() self.tica = pyemma.coordinates.tica(self.source, lag=1, dim=2) - self.pca = pyemma.coordinates.tica(self.source, dim=2) + self.pca = pyemma.coordinates.pca(self.source, dim=2) self.Ys = self.tica.get_output() self.tempdir = tempfile.mkdtemp('test_molpx') - self.projected_files = [os.path.join(self.tempdir, 'Y.%u.npy'%ii) for ii in range(len(self.MD_trajectories))] - [np.save(ifile, iY) for ifile, iY in zip(self.projected_files, self.Ys)] - [np.savetxt(ifile.replace('.npy', '.dat'), iY) for ifile, iY in zip(self.projected_files, self.Ys)] + self.projected_files_npy = [os.path.join(self.tempdir, 'Y.%u.npy' % ii) for ii in range(len(self.MD_trajectories))] + self.projected_files_dat = [ifile.replace(".npy",".dat") for ifile in self.projected_files_npy] + [np.save(ifile, iY) for ifile, iY in zip(self.projected_files_npy, self.Ys)] + [np.savetxt(ifile.replace('.npy', '.dat'), iY) for ifile, iY in zip(self.projected_files_npy, self.Ys)] @classmethod def tearDownClass(self): shutil.rmtree(self.tempdir) -class TestReadingInput(unittest.TestCase): +class TestReadingInput(TestWithBPTIData): - def setUp(self): - self.MD_trajectory = os.path.join(pyemma.__path__[0],'coordinates/tests/data/bpti_mini.xtc') - self.MD_topology = os.path.join(pyemma.__path__[0], 'coordinates/tests/data/bpti_ca.pdb') - self.tempdir = tempfile.mkdtemp('test_molpx') - self.projected_file = os.path.join(self.tempdir,'Y.npy') - self.feat = pyemma.coordinates.featurizer(self.MD_topology) - self.feat.add_all() - source = pyemma.coordinates.source(self.MD_trajectory, features=self.feat) - self.tica = pyemma.coordinates.tica(source,lag=1, dim=2) - self.Y = self.tica.get_output()[0] - self.F = source.get_output() - np.save(self.projected_file,self.Y) - np.savetxt(self.projected_file.replace('.npy','.dat'),self.Y) - - def tearDown(self): - shutil.rmtree(self.tempdir) + @classmethod + def setUpClass(self): + TestWithBPTIData.setUpClass() def test_data_from_input_npy(self): # Just one string - assert np.allclose(self.Y, _bmutils.data_from_input(self.projected_file)[0]) + assert np.allclose(self.Ys[0], _bmutils.data_from_input(self.projected_files_npy)[0]) # List of one string - assert np.allclose(self.Y, _bmutils.data_from_input([self.projected_file])[0]) - # List of two strings - Ys = _bmutils.data_from_input([self.projected_file, - self.projected_file]) - assert np.all([np.allclose(self.Y, iY) for iY in Ys]) + assert np.allclose(self.Ys[0], _bmutils.data_from_input([self.projected_files_npy[0]])) + # List of strings + Ys = _bmutils.data_from_input(self.projected_files_npy) + assert np.all([np.allclose(jY, iY) for jY, iY in zip(self.Ys, Ys)]) + + # Check that it fails properly + try: + _bmutils.data_from_input(1) + except ValueError: + pass def test_data_from_input_throws_exception(self): try: @@ -80,13 +73,12 @@ def test_data_from_input_throws_exception(self): def test_data_from_input_ascii(self): # Just one string - assert np.allclose(self.Y, _bmutils.data_from_input(self.projected_file.replace('.npy', '.dat'))[0]) + assert np.allclose(self.Ys[0], _bmutils.data_from_input(self.projected_files_dat)[0]) # List of one string - assert np.allclose(self.Y, _bmutils.data_from_input([self.projected_file.replace('.npy', '.dat')])[0]) - # List of two strings - Ys = _bmutils.data_from_input([self.projected_file.replace('.npy', '.dat'), - self.projected_file.replace('.npy','.dat')]) - assert np.all([np.allclose(self.Y, iY) for iY in Ys]) + assert np.allclose(self.Ys[0], _bmutils.data_from_input([self.projected_files_dat[0]])) + # List of strings + Ys = _bmutils.data_from_input(self.projected_files_dat) + assert np.all([np.allclose(jY, iY) for jY, iY in zip(self.Ys, Ys)]) def test_data_from_input_ndarray(self): # Just one ndarray @@ -108,7 +100,7 @@ def _test_data_from_input_ndarray_ascii_npy(self): def test_moldata_from_input(self): # Traj and top strings - moldata = _bmutils.moldata_from_input(self.MD_trajectory, MD_top=self.MD_topology) + moldata = _bmutils.moldata_from_input(self.MD_trajectory_files, MD_top=self.MD_topology) assert isinstance(moldata, _bmutils._FeatureReader) # Source object directly @@ -122,21 +114,69 @@ def test_moldata_from_input(self): pass # List of trajectories - geom = md.load(self.MD_trajectory, top=self.MD_topology) - moldata = _bmutils.moldata_from_input(geom) + moldata = _bmutils.moldata_from_input(self.MD_trajectories) assert isinstance(moldata[0], md.Trajectory), moldata def test_assert_moldata_belong_data(self): # Traj vs data - geom = md.load(self.MD_trajectory, top=self.MD_topology) - _bmutils.assert_moldata_belong_data([geom], [self.Y]) + _bmutils.assert_moldata_belong_data(self.MD_trajectories, self.Ys) # src vs data - moldata = _bmutils.moldata_from_input(self.MD_trajectory, MD_top=self.MD_topology) - _bmutils.assert_moldata_belong_data(moldata, [self.Y]) + moldata = _bmutils.moldata_from_input(self.MD_trajectories, MD_top=self.MD_topology) + _bmutils.assert_moldata_belong_data(moldata, self.Ys) # With stride - _bmutils.assert_moldata_belong_data([geom], [iY[::5] for iY in [self.Y]], data_stride=5) + _bmutils.assert_moldata_belong_data(self.MD_trajectories, [iY[::5] for iY in self.Ys], data_stride=5) + +class TestSaveTraj(TestWithBPTIData): + + @classmethod + def setUpClass(self): + TestWithBPTIData.setUpClass() + + def test_just_works(self): + samples = [[0, 10], + [1, 20], + [2, 30]] + geoms_ref = pyemma.coordinates.save_traj(self.source, samples, None) + geoms_molpx = _bmutils.save_traj_wrapper(self.source, samples, None) + assert np.all([np.allclose(ixyz, jxyz) for ixyz, jxyz in zip(geoms_ref.xyz, geoms_molpx.xyz)]) + + def test_works_with_MDTrajectories(self): + samples = [[0, 10], + [1, 20], + [2, 30]] + geoms_ref = pyemma.coordinates.save_traj(self.source, samples, None) + geoms_molpx = _bmutils.save_traj_wrapper(self.MD_trajectories, samples, None) + assert np.all([np.allclose(ixyz, jxyz) for ixyz, jxyz in zip(geoms_ref.xyz, geoms_molpx.xyz)]) + + def test_works_with_MDTrajectories_with_stride(self): + + samples = [[0, 10], + [1, 20], + [2, 30]] + geoms_ref = pyemma.coordinates.save_traj(self.source, samples, None, stride=2) + geoms_molpx = _bmutils.save_traj_wrapper(self.MD_trajectories, samples, None, stride=2) + assert np.all([np.allclose(ixyz, jxyz) for ixyz, jxyz in zip(geoms_ref.xyz, geoms_molpx.xyz)]) + +class TestCorrelations(TestWithBPTIData): + @classmethod + def setUpClass(self): + TestWithBPTIData.setUpClass() + + + def test_input_types(self): + _bmutils.most_corr(self.tica) + _bmutils.most_corr(self.pca) + _bmutils.most_corr(self.feat) + _bmutils.most_corr(self.tica.feature_TIC_correlation) + try: + _bmutils.most_corr("a") + except TypeError: + pass + + def test_printing(self): + print(_bmutils.most_corr(self.tica)) def test_most_corr_info_works(self): most_corr = _bmutils.most_corr(self.tica) @@ -159,8 +199,7 @@ def test_most_corr_info_works(self): assert most_corr['feats'] == [] def test_most_corr_info_works_with_options(self): - geoms = md.load(self.MD_trajectory, top=self.MD_topology) - most_corr = _bmutils.most_corr(self.tica, geoms=geoms) + most_corr = _bmutils.most_corr(self.tica, geoms=self.MD_trajectories[0]) # Idxs are okay ref_idxs = [np.abs(self.tica.feature_TIC_correlation[:, ii]).argmax() for ii in range(self.tica.dim)] @@ -171,15 +210,13 @@ def test_most_corr_info_works_with_options(self): assert np.all([rv == mcv for rv, mcv in zip(ref_corrs, most_corr['vals'])]) # Check that we got the right most correlated feature trajectory - ref_feats = self.feat.transform(geoms) + ref_feats = self.feat.transform(self.MD_trajectories[0]) ref_feats = [ref_feats[:, ii] for ii in ref_idxs] assert np.all(np.allclose(rv, mcv) for rv, mcv in zip(ref_feats, np.squeeze(most_corr['feats']))) def test_most_corr_info_works_with_options_and_proj_idxs(self): - geoms = md.load(self.MD_trajectory, top=self.MD_topology) - proj_idxs = [1, 0] # the order shouldn't matter - corr_dict = _bmutils.most_corr(self.tica, geoms=geoms, proj_idxs=proj_idxs) + corr_dict = _bmutils.most_corr(self.tica, geoms=self.MD_trajectories[0], proj_idxs=proj_idxs) # Idxs are okay ref_idxs = [np.abs(self.tica.feature_TIC_correlation[:, ii]).argmax() for ii in proj_idxs] @@ -195,7 +232,7 @@ def test_most_corr_info_works_with_options_and_proj_idxs(self): assert [isinstance(istr, str) for istr in corr_dict["labels"]] # Feature values are ok - ref_feats = self.feat.transform(geoms) + ref_feats = self.feat.transform(self.MD_trajectories[0]) ref_feats = [ref_feats[:, ii] for ii in ref_idxs] assert np.all(np.allclose(rv, mcv) for rv, mcv in zip(ref_feats, np.squeeze(corr_dict['feats']))) @@ -230,6 +267,11 @@ def test_cluster_to_target(self): n_target = 15 n_tol = 1 data = [np.random.randn(5000, 1), np.random.randn(5000,1)+10] + + # Only one iteration, just to force to go into the loop-breaking + cl = _bmutils.regspace_cluster_to_target_kmeans(data, n_target, k_centers=100, max_iter=1, n_tol=n_tol) + + # Now the real deal cl = _bmutils.regspace_cluster_to_target_kmeans(data, n_target, k_centers=100, max_iter=100, n_tol=n_tol) assert n_target - n_tol <= cl.n_clusters <= n_target + n_tol, (cl.n_clusters, n_tol) @@ -258,6 +300,21 @@ def test_interval_schachtelung(self): ) assert y(x_sol)-eps <= target_y < y(x_sol)+eps, (y(x_sol)-target_y, eps) + def test_interval_schachtelung_fails(self): + y = lambda x:x**2 # parabolic curve, monotically increasing between [0, +inf] + + interval = [2, 500] + eps = .1 + + target_y = 0 # Is not contained in [2**2, 500**2] + + try: + _bmutils.interval_schachtelung(y, [2, 500], target=target_y, eps=eps, + #verbose=True + ) + except: + pass + def test_catalogues(self): cl = _bmutils.regspace_cluster_to_target_kmeans(self.data_for_cluster, 3, max_iter=10, n_tol=0) @@ -420,7 +477,7 @@ class TestGetGoodStartingPoint(unittest.TestCase): def setUp(self): # The setup creates the typical, "geometries-sampled along cluster-scenario" n_geom_samples = 20 - traj = md.load(os.path.join(pyemma.__path__[0],'coordinates/tests/data/bpti_ca.pdb')) + traj = md.load(molpx._molpxdir(join='notebooks/data/bpti-c-alpha_centered.pdb')) traj = traj.atom_slice([0,1,3,4]) # create a trajectory with four atoms # Create a fake bi-modal trajectory with a compact and an open structure ixyz = np.array([[0., 0., 0.], @@ -476,7 +533,7 @@ def test_most_pop(self): "around the value 15. The found starting point should be" \ "in this interval (see the setUp)" - def _test_most_pop_x_rgyr(self): + def test_most_pop_x_rgyr(self): start_idx = _bmutils.get_good_starting_point(self.cl, self.geom_smpl, strategy="most_pop_x_smallest_Rgyr") #print(start_idx, self.cl.clustercenters[start_idx], np.sort(self.cl.clustercenters.squeeze())) # TODO: figure out a good way of testing this, at the moment it just chekcs that it runs @@ -596,11 +653,10 @@ def test_closest_all_coords_history(self): class TestSliceListOfGeoms(unittest.TestCase): def setUp(self): - self.topology = os.path.join(pyemma.__path__[0],'coordinates/tests/data/bpti_ca.pdb') + self.topology = molpx._molpxdir(join='notebooks/data/bpti-c-alpha_centered.pdb') self.ref_frame = 4 - self.MD_trajectory = md.load(os.path.join(pyemma.__path__[0], - 'coordinates/tests/data/bpti_mini.xtc'), - top=self.topology) + self.MD_trajectory = md.load(glob(molpx._molpxdir(join='notebooks/data/c-alpha_centered.stride.1000*xtc'))[0], + top=self.topology) def test_slice(self): geom_list = [self.MD_trajectory, self.MD_trajectory[::-1]] @@ -620,12 +676,11 @@ def setUp(self): def test_it_works(self): assert np.allclose([0], _bmutils.get_ascending_coord_idx(self.data[:,:-1])) def test_empty_no_fail(self): - result = _bmutils.get_ascending_coord_idx(self.data[:,[1,2]], fail_if_empty=False) assert len(result)==0 def test_empty_fail(self): try: - _bmutils.get_ascending_coord_idx(self.data[:, 1:], fail_if_empty=True) + _bmutils.get_ascending_coord_idx(self.data[:, [1,2]], fail_if_empty=True) except ValueError: pass def test_more_than_one_fails(self): @@ -633,11 +688,13 @@ def test_more_than_one_fails(self): _bmutils.get_ascending_coord_idx(self.data[:, :], fail_if_more_than_one=True) except: pass + def test_more_than_one_passes(self): + _bmutils.get_ascending_coord_idx(self.data[:, :], fail_if_more_than_one=False) class TestMinRmsdPaths(unittest.TestCase): def setUp(self): - self.topology = os.path.join(pyemma.__path__[0],'coordinates/tests/data/bpti_ca.pdb') + self.topology = molpx._molpxdir(join='notebooks/data/bpti-c-alpha_centered.pdb') self.reftraj = md.load(self.topology) def test_find_buried_best_candidate(self): @@ -757,7 +814,7 @@ class TestSmoothingFunctions(unittest.TestCase): def setUp(self): # The setup creates the typical, "geometries-sampled along cluster-scenario" - traj = md.load(os.path.join(pyemma.__path__[0], 'coordinates/tests/data/bpti_ca.pdb')) + traj = md.load(molpx._molpxdir(join='notebooks/data/bpti-c-alpha_centered.pdb')) traj = traj.atom_slice([0, 1]) # create a trajectory with two atoms # Create a fake bi-modal trajectory with a compact and an open structure ixyz = np.array([[10., 20., 30.], @@ -852,7 +909,7 @@ class TestListTransposeGeomList(unittest.TestCase): def test_it(self): # Create a dummy topology - traj = md.load(os.path.join(pyemma.__path__[0], 'coordinates/tests/data/bpti_ca.pdb')) + traj = md.load(molpx._molpxdir(join='notebooks/data/bpti-c-alpha_centered.pdb')) top = traj.atom_slice([0]).top # Single atom topology ixyz_row_0 = [[0, 0, 0]], [[0, 1, 0]], [[0, 2, 0]] # 3 frames of 1 atom @@ -878,7 +935,7 @@ class geom_list_2_geom(unittest.TestCase): def test_it(self): # Create a dummy topology MD_trajectory = os.path.join(pyemma.__path__[0], 'coordinates/tests/data/bpti_mini.xtc') - MD_topology = os.path.join(pyemma.__path__[0], 'coordinates/tests/data/bpti_ca.pdb') + MD_topology = molpx._molpxdir(join='notebooks/data/bpti-c-alpha_centered.pdb') traj = md.load(MD_trajectory, top=MD_topology) traj_list = [itraj for itraj in traj] @@ -886,11 +943,11 @@ def test_it(self): assert np.allclose(np.hstack([igeom.xyz for igeom in new_geom]).squeeze(), np.vstack(traj.xyz)) -class TestIndexFromFeatures(unittest.TestCase): +class TestIndexGeneralInput(unittest.TestCase): def setUp(self): - self.MD_topology = os.path.join(pyemma.__path__[0], 'coordinates/tests/data/bpti_ca.pdb') - self.feat= pyemma.coordinates.featurizer(self.MD_topology) + self.MD_topology = molpx._molpxdir(join='notebooks/data/bpti-c-alpha_centered.pdb') + self.feat = pyemma.coordinates.featurizer(self.MD_topology) self.ang_idxs = [[ii + jj for jj in range(3)] for ii in range(self.feat.topology.n_atoms - 3)] self.dih_idxs = [[ii + jj for jj in range(4)] for ii in range(self.feat.topology.n_atoms - 4)] @@ -902,9 +959,18 @@ def setUp(self): self.feat.add_dihedrals(self.dih_idxs) self.feat.add_dihedrals(self.dih_idxs, cossin=True) + self.src = pyemma.coordinates.source(glob(molpx._molpxdir(join='notebooks/data/c-alpha*xtc'))[0], features=self.feat) + self.tica = pyemma.coordinates.tica(self.src, ) + self.pca = pyemma.coordinates.pca(self.src) + def tearDown(self): pass + def test_input_just_runs(self): + _bmutils.atom_idxs_from_general_input(self.feat) + _bmutils.atom_idxs_from_general_input(self.tica) + _bmutils.atom_idxs_from_general_input(self.pca) + def test_atom_idxs_from_feature_xyz(self): ai = _bmutils.atom_idxs_from_feature(self.feat.active_features[0]) assert np.allclose(np.repeat(np.arange(self.feat.topology.n_atoms),3), ai) @@ -972,7 +1038,7 @@ def test_labelize(self): assert labels[1] == feat.describe()[1] def test_superpose_list_of_geoms(self): - geom = md.load(os.path.join(pyemma.__path__[0], 'coordinates/tests/data/bpti_ca.pdb')) + geom = md.load(molpx._molpxdir(join='notebooks/data/bpti-c-alpha_centered.pdb')) # Nothing happens _bmutils.superpose_to_most_compact_in_list(False, [geom]) From c0a978b5ffa147dbb4b077385c4f67e4370b9e9c Mon Sep 17 00:00:00 2001 From: gph82 Date: Sun, 20 May 2018 00:15:32 +0200 Subject: [PATCH 43/73] [_bmutils] atom_idxs_from_feature() refactor doc --- molpx/_bmutils.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/molpx/_bmutils.py b/molpx/_bmutils.py index 0c072ac..2aef708 100644 --- a/molpx/_bmutils.py +++ b/molpx/_bmutils.py @@ -1160,8 +1160,10 @@ def atom_idxs_from_feature(ifeat): ---------- ifeat : input featurizer: - a :any:`pyemma.coordinates.featurizer` (Distancefeaturizer, AngleFeaturizer etc) or - + a PyEMMA Feature. Accepted are + DistanceFeature, AngleFeature, DihedralFeature, ResidueMinDistanceFeature, and + SelectionFeature, + #TODO include link to PyEMMA objects in docstring Returns ------- From 813ef1cf6e5b56b62377e97c16e8207e04f4e805 Mon Sep 17 00:00:00 2001 From: gph82 Date: Sun, 20 May 2018 00:16:29 +0200 Subject: [PATCH 44/73] [test_bmutils] refactored TestAtomIndices to TestIndexGeneralInput --- molpx/tests/test_bmutils.py | 109 +++++++++++++++++++++++++++++------- 1 file changed, 89 insertions(+), 20 deletions(-) diff --git a/molpx/tests/test_bmutils.py b/molpx/tests/test_bmutils.py index 55239ad..c42bfcb 100644 --- a/molpx/tests/test_bmutils.py +++ b/molpx/tests/test_bmutils.py @@ -943,62 +943,131 @@ def test_it(self): assert np.allclose(np.hstack([igeom.xyz for igeom in new_geom]).squeeze(), np.vstack(traj.xyz)) -class TestIndexGeneralInput(unittest.TestCase): +class TestAtomIndices(unittest.TestCase): def setUp(self): self.MD_topology = molpx._molpxdir(join='notebooks/data/bpti-c-alpha_centered.pdb') self.feat = pyemma.coordinates.featurizer(self.MD_topology) - self.ang_idxs = [[ii + jj for jj in range(3)] for ii in range(self.feat.topology.n_atoms - 3)] - self.dih_idxs = [[ii + jj for jj in range(4)] for ii in range(self.feat.topology.n_atoms - 4)] + self.cartesian_input = np.arange(10) + self.distance_input = np.arange(10, 15) + self.angle_input = [[ii + jj for jj in range(3)] for ii in range(self.feat.topology.n_atoms - 3)] + self.dih_input = [[ii + jj for jj in range(4)] for ii in range(self.feat.topology.n_atoms - 4)] + self.mindist_input = [[15, 16], [16, 17], [18, 19]] - self.feat.add_all() - self.feat.add_distances_ca(excluded_neighbors=0) - self.feat.add_angles(self.ang_idxs) - self.feat.add_angles(self.ang_idxs, cossin=True) - self.feat.add_dihedrals(self.dih_idxs) - self.feat.add_dihedrals(self.dih_idxs, cossin=True) + # Add features to the featurizer and keep track of the atom indices + self.feat.add_selection(self.cartesian_input) + self.cartesian_indices = list(np.repeat(self.cartesian_input, 3)) + + self.feat.add_distances(self.distance_input) + self.distance_indices = [] + for ii, iidx in enumerate(self.distance_input[:-1]): + for jidx in self.distance_input[ii + 1:]: + self.distance_indices.append((iidx, jidx)) + + self.feat.add_angles(self.angle_input) + self.angle_indices = self.angle_input + + self.feat.add_angles(self.angle_input, cossin=True) + self.angle_indices_cossin = list(np.tile(self.angle_input, 2).reshape(-1, 3)) + + self.feat.add_dihedrals(self.dih_input) + self.dih_indices = self.dih_input + + self.feat.add_dihedrals(self.dih_input, cossin=True) + self.dih_indices_cossin = list(np.tile(self.dih_input, 2).reshape(-1, 4)) + + self.feat.add_residue_mindist(self.mindist_input) + self.mindist_indices = [[_bmutils.get_repr_atom_for_residue(self.feat.topology.residue(ii)).index for ii in rpair] for rpair in self.mindist_input] self.src = pyemma.coordinates.source(glob(molpx._molpxdir(join='notebooks/data/c-alpha*xtc'))[0], features=self.feat) - self.tica = pyemma.coordinates.tica(self.src, ) + self.tica = pyemma.coordinates.tica(self.src ) self.pca = pyemma.coordinates.pca(self.src) def tearDown(self): pass + def test_repr_atoms_from_residues(self): + # Use Di-Ala + top = md.load(molpx._molpxdir(join='notebooks/data/ala2.pdb')).top + # First res, ACE + aa = _bmutils.get_repr_atom_for_residue(top.residue(0)) + assert aa.name=="C" + # Mid res, ala2 + aa = _bmutils.get_repr_atom_for_residue(top.residue(1)) + assert aa.name=="CA" + # Last res, ACE + aa = _bmutils.get_repr_atom_for_residue(top.residue(2)) + assert aa.name == "C" + + # Force fail + try: + _bmutils.get_repr_atom_for_residue(top.residue(0), cands=["CB"]) + except ValueError: + pass + + + # Test the general input def test_input_just_runs(self): _bmutils.atom_idxs_from_general_input(self.feat) _bmutils.atom_idxs_from_general_input(self.tica) _bmutils.atom_idxs_from_general_input(self.pca) + try: + _bmutils.atom_idxs_from_general_input("aa") + except TypeError: + pass + # Feature by feature + def test_atom_idxs_from_feature_not_recognized(self): + try: + _bmutils.atom_idxs_from_feature("aa") + except NotImplementedError: + pass def test_atom_idxs_from_feature_xyz(self): ai = _bmutils.atom_idxs_from_feature(self.feat.active_features[0]) - assert np.allclose(np.repeat(np.arange(self.feat.topology.n_atoms),3), ai) + assert np.allclose(self.cartesian_indices, ai) - def test_atom_idxs_from_feature_D_CA(self): + def test_atom_idxs_from_feature_D(self): ai = _bmutils.atom_idxs_from_feature(self.feat.active_features[1]) - ref = np.vstack(np.triu_indices(self.feat.topology.n_atoms, k=1)).T - assert np.allclose(ref, ai),ai + assert np.allclose(self.distance_indices, ai),ai def test_atom_idxs_from_feature_ang(self): ai = _bmutils.atom_idxs_from_feature(self.feat.active_features[2]) - assert np.allclose(self.ang_idxs, ai) + assert np.allclose(self.angle_indices, ai) def test_atom_idxs_from_feature_ang_cossin(self): ai = _bmutils.atom_idxs_from_feature(self.feat.active_features[3]) - ref = np.tile(self.ang_idxs, 2).reshape(-1,3) - assert np.allclose(ai, ref) + assert np.allclose(ai, self.angle_indices_cossin) def test_atom_idxs_from_feature_dih(self): ai = _bmutils.atom_idxs_from_feature(self.feat.active_features[4]) - assert np.allclose(self.dih_idxs, ai) + assert np.allclose(self.dih_indices, ai) + def test_atom_idxs_from_feature_dih_cossin(self): ai = _bmutils.atom_idxs_from_feature(self.feat.active_features[5]) - ref = np.tile(self.dih_idxs, 2).reshape(-1,4) - assert np.allclose(ref, ai) + assert np.allclose(self.dih_indices_cossin, ai) + + def test_atom_idxs_from_feature_res_mindist(self): + ai = _bmutils.atom_idxs_from_feature(self.feat.active_features[6]) + assert np.allclose(self.mindist_indices,ai) + + def test_all_features_together(self): + ai = _bmutils.atom_idxs_from_general_input(self.feat) + ref = self.cartesian_indices+\ + self.distance_indices+\ + self.angle_indices+\ + self.angle_indices_cossin+\ + self.dih_indices+\ + self.dih_indices_cossin+\ + self.mindist_indices + assert len(ai)==self.feat.dimension() + assert all([np.allclose(aa, rr) for aa, rr in zip(ai, ref)]) class TestInputManipulationShaping(unittest.TestCase): + def test_listify_if_int(self): + assert isinstance(_bmutils.listify_if_int(0), list) + def test_listify(self): input = [1,2,3,4] output = _bmutils.listify_if_not_list(input) From 04b60ffb461eb5007e9a8371554e302d00664e6d Mon Sep 17 00:00:00 2001 From: gph82 Date: Sun, 20 May 2018 00:18:03 +0200 Subject: [PATCH 45/73] [_bmutils] deprecated regspace_cluster_to_target (was not in the API) --- molpx/_bmutils.py | 46 ---------------------------------------------- 1 file changed, 46 deletions(-) diff --git a/molpx/_bmutils.py b/molpx/_bmutils.py index 2aef708..5118584 100644 --- a/molpx/_bmutils.py +++ b/molpx/_bmutils.py @@ -167,52 +167,6 @@ def re_warp(array_in, lengths): idxi += ll return warped -# TODO deprecate properly -def _regspace_cluster_to_target(data, n_clusters_target, - n_try_max=5, delta=5., - verbose=False): - r""" - Clusters a dataset to a target n_clusters using regspace clustering by iteratively. " - Work best with 1D data - - data: ndarray or list thereof - n_clusters_target: int, number of clusters. - n_try_max: int, default is 5. Maximum number of iterations in the heuristic. - delta: float, defalut is 5. Percentage of n_clusters_target to consider converged. - Eg. n_clusters_target=100 and delta = 5 will consider any clustering between 95 and 100 clustercenters as - valid. Note. Note: An off-by-one in n_target_clusters is sometimes unavoidable - - returns: pyemma clustering object - - tested:True - """ - delta = delta/100 - ndim = _np.vstack(data).shape[0] - assert ndim >= n_clusters_target, "Cannot cluster " \ - "%u datapoints on %u clustercenters. Reduce the number of target " \ - "clustercenters."%(_np.vstack(data).shape[0], n_clusters_target) - # Works well for connected, 1D-clustering, - # otherwise it's bad starting guess for dmin - cmax = _np.vstack(data).max() - cmin = _np.vstack(data).min() - dmin = (cmax-cmin)/(n_clusters_target+1) - - err = _np.ceil(n_clusters_target*delta) - cl = _cluster_regspace(data, dmin=dmin, max_centers=5000) - for cc in range(n_try_max): - n_cl_now = cl.n_clusters - delta_cl_now = _np.abs(n_cl_now - n_clusters_target) - if not n_clusters_target-err <= cl.n_clusters <= n_clusters_target+err: - # Cheap (VERY BAD IN HIGH DIM) heuristic to get relatively close relatively quick - dmin = cl.dmin*cl.n_clusters/ n_clusters_target - cl = _cluster_regspace(data, dmin=dmin, max_centers=5000)# max_centers is given so that we never reach it (dangerous) - else: - break - if verbose: - print('cl iter %u %u -> %u (Delta to target (%u +- %u): %u'%(cc, n_cl_now, cl.n_clusters, - n_clusters_target, err, delta_cl_now)) - return cl - def regspace_cluster_to_target_kmeans(data, n_clusters_target, k_centers=1000, k_stride=50, From 27b9f2f2925fa1cefb3bc89697287796401f28f5 Mon Sep 17 00:00:00 2001 From: gph82 Date: Sun, 20 May 2018 01:26:41 +0200 Subject: [PATCH 46/73] [_bmutils] save_traj_wrapper() avoids using join() (faster) --- molpx/_bmutils.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/molpx/_bmutils.py b/molpx/_bmutils.py index 5118584..40854ba 100644 --- a/molpx/_bmutils.py +++ b/molpx/_bmutils.py @@ -4,6 +4,7 @@ try: from sklearn.mixture import GaussianMixture as _GMM except ImportError: + # TODO pin sklearn version and get rid of this from sklearn.mixture import GMM as _GMM from scipy.spatial.distance import pdist as _pdist, squareform as _squareform @@ -737,18 +738,23 @@ def save_traj_wrapper(traj_inp, indexes, outfile, top=None, stride=1, chunksize= if isinstance(traj_inp, (_FeatureReader, _FragmentedTrajectoryReader)) or _is_string(traj_inp[0]): geom_smpl = _save_traj(traj_inp, indexes, None, top=top, stride=stride, chunksize=chunksize, image_molecules=image_molecules, verbose=verbose) + elif isinstance(traj_inp[0], _md.Trajectory): - file_idx, frame_idx = indexes[0] - if False: - geom_smpl = traj_inp[file_idx][frame_idx] - for file_idx, frame_idx in indexes[1:]: - geom_smpl = geom_smpl.join(traj_inp[file_idx][frame_idx]) - # TODO this takes too much time for large topologies, consider copying - else: - xyz =[] - for file_idx, frame_idx in indexes: - xyz.append(traj_inp[file_idx].xyz[frame_idx].squeeze()) - geom_smpl = _md.Trajectory(xyz, traj_inp[0][0].top) + top = traj_inp[0].top + n_frames = len(indexes) + xyz = _np.zeros((n_frames, top.n_atoms, 3)) + uc_lengths = _np.zeros((n_frames, 3)) + uc_angles = _np.zeros((n_frames, 3)) + time = _np.zeros(n_frames) + for ii, (file_idx, frame_idx) in enumerate(indexes): + xyz[ii, :, :] = traj_inp[file_idx].xyz[frame_idx*stride].squeeze() + uc_lengths[ii, :] = traj_inp[file_idx].unitcell_lengths[frame_idx*stride] + uc_angles[ii, :] = traj_inp[file_idx].unitcell_angles[frame_idx*stride] + time[ii] = traj_inp[file_idx].time[frame_idx*stride] + + geom_smpl = _md.Trajectory(xyz, top, time=time, + unitcell_lengths=uc_lengths, + unitcell_angles=uc_angles) else: raise TypeError("Cant handle input of type %s now"%(type(traj_inp[0]))) From 5ad7257c4414908a34f87d908c464a49accdf61a Mon Sep 17 00:00:00 2001 From: gph82 Date: Sun, 20 May 2018 01:27:25 +0200 Subject: [PATCH 47/73] [test_bmutils] finish adapting to TestWithBPTIData and copying trajs (instead of joining) --- molpx/tests/test_bmutils.py | 89 ++++++++++++++++++++++++------------- 1 file changed, 58 insertions(+), 31 deletions(-) diff --git a/molpx/tests/test_bmutils.py b/molpx/tests/test_bmutils.py index c42bfcb..4a88dc3 100644 --- a/molpx/tests/test_bmutils.py +++ b/molpx/tests/test_bmutils.py @@ -82,13 +82,12 @@ def test_data_from_input_ascii(self): def test_data_from_input_ndarray(self): # Just one ndarray - assert np.allclose(self.Y, _bmutils.data_from_input(self.Y)[0]) + assert np.allclose(self.Ys[0], _bmutils.data_from_input(self.Ys[0])) # List of one ndarray - assert np.allclose(self.Y, _bmutils.data_from_input([self.Y])[0]) - # List of two ndarray - Ys = _bmutils.data_from_input([self.Y, - self.Y]) - assert np.all([np.allclose(self.Y, iY) for iY in Ys]) + assert np.allclose(self.Ys[0], _bmutils.data_from_input([self.Ys[0]])[0]) + # Lists + Ys = _bmutils.data_from_input(self.Ys) + assert np.all([np.allclose(jY, iY) for jY,iY in zip(self.Ys, Ys)]) # Not implemented yet def _test_data_from_input_ndarray_ascii_npy(self): @@ -150,7 +149,7 @@ def test_works_with_MDTrajectories(self): geoms_molpx = _bmutils.save_traj_wrapper(self.MD_trajectories, samples, None) assert np.all([np.allclose(ixyz, jxyz) for ixyz, jxyz in zip(geoms_ref.xyz, geoms_molpx.xyz)]) - def test_works_with_MDTrajectories_with_stride(self): + def _test_works_with_MDTrajectories_with_stride(self): samples = [[0, 10], [1, 20], @@ -709,7 +708,11 @@ def test_find_buried_best_candidate(self): for pp, ff in enumerate(frames_where_actual_geometry_is): xyz[pp][ff,:,:] = self.reftraj.xyz # Create the path of candidates as mdtraj objects - path_of_candidates = [md.Trajectory(ixyz, topology=self.reftraj.top) for ixyz in xyz] + path_of_candidates = [md.Trajectory(ixyz, topology=self.reftraj.top, + time=[self.reftraj.time.squeeze()] * n_cands, + unitcell_angles=[self.reftraj.unitcell_angles.squeeze()] * n_cands, + unitcell_lengths=[self.reftraj.unitcell_lengths.squeeze()] * n_cands) + for ixyz in xyz] # Let mirmsd_path find these frames for you inferred_frames = _bmutils.min_rmsd_path(self.reftraj, path_of_candidates) @@ -750,7 +753,11 @@ def test_find_buried_best_candidate_with_selection(self): xyz[pp][ff,:,:] = ref_w_sel_perturbed[pp] # Create the path of candidates as mdtraj objects - path_of_candidates = [md.Trajectory(ixyz, topology=self.reftraj.top) for ixyz in xyz] + path_of_candidates = [md.Trajectory(ixyz, topology=self.reftraj.top, + time=[self.reftraj.time.squeeze()] * n_cands, + unitcell_angles=[self.reftraj.unitcell_angles.squeeze()] * n_cands, + unitcell_lengths=[self.reftraj.unitcell_lengths.squeeze()] * n_cands) + for ixyz in xyz] # PRE-TEST # as long as the perturbation is small, @@ -775,7 +782,11 @@ def test_find_buried_best_candidate_with_selection(self): # In the random frames, insert the intouched selection for pp, ff in enumerate(frames_random_w_sel_untouched): xyz[pp][ff,selection,:] = np.copy(self.reftraj.xyz[0,selection, :]) - path_of_candidates = [md.Trajectory(ixyz, topology=self.reftraj.top) for ixyz in xyz] + path_of_candidates = [md.Trajectory(ixyz, topology=self.reftraj.top, + time=[self.reftraj.time.squeeze()] * n_cands, + unitcell_angles=[self.reftraj.unitcell_angles.squeeze()] * n_cands, + unitcell_lengths=[self.reftraj.unitcell_lengths.squeeze()] * n_cands) + for ixyz in xyz] # This should still be OK, because # even if the selected atoms have been perturbed, the comparsion [ref+sel_per] vs [random] is robust @@ -802,8 +813,12 @@ def test_history_aware_just_works(self): frames_where_actual_geometry_is = np.random.randint(0, high=n_cands, size=path_length) for pp, ff in enumerate(frames_where_actual_geometry_is): xyz[pp][ff, :, :] = self.reftraj.xyz - # Create the path of candidates as mdtraj objects - path_of_candidates = [md.Trajectory(ixyz, topology=self.reftraj.top) for ixyz in xyz] + # Create the path of candidates as mdtraj objects (time, unitcell angles and lengths are bogus) + path_of_candidates = [md.Trajectory(ixyz, topology=self.reftraj.top, + time=[self.reftraj.time.squeeze()]*n_cands, + unitcell_angles=[self.reftraj.unitcell_angles.squeeze()]*n_cands, + unitcell_lengths=[self.reftraj.unitcell_lengths.squeeze()]*n_cands) + for ixyz in xyz] # Let mirmsd_path find these frames for you inferred_frames = _bmutils.min_rmsd_path(self.reftraj, path_of_candidates, history_aware=True) @@ -980,6 +995,15 @@ def setUp(self): self.feat.add_residue_mindist(self.mindist_input) self.mindist_indices = [[_bmutils.get_repr_atom_for_residue(self.feat.topology.residue(ii)).index for ii in rpair] for rpair in self.mindist_input] + # Now put all indices in one long list: + self.ref = self.cartesian_indices+\ + self.distance_indices+\ + self.angle_indices+\ + self.angle_indices_cossin+\ + self.dih_indices+\ + self.dih_indices_cossin+\ + self.mindist_indices + self.src = pyemma.coordinates.source(glob(molpx._molpxdir(join='notebooks/data/c-alpha*xtc'))[0], features=self.feat) self.tica = pyemma.coordinates.tica(self.src ) self.pca = pyemma.coordinates.pca(self.src) @@ -1006,17 +1030,6 @@ def test_repr_atoms_from_residues(self): except ValueError: pass - - # Test the general input - def test_input_just_runs(self): - _bmutils.atom_idxs_from_general_input(self.feat) - _bmutils.atom_idxs_from_general_input(self.tica) - _bmutils.atom_idxs_from_general_input(self.pca) - try: - _bmutils.atom_idxs_from_general_input("aa") - except TypeError: - pass - # Feature by feature def test_atom_idxs_from_feature_not_recognized(self): try: @@ -1053,15 +1066,29 @@ def test_atom_idxs_from_feature_res_mindist(self): def test_all_features_together(self): ai = _bmutils.atom_idxs_from_general_input(self.feat) - ref = self.cartesian_indices+\ - self.distance_indices+\ - self.angle_indices+\ - self.angle_indices_cossin+\ - self.dih_indices+\ - self.dih_indices_cossin+\ - self.mindist_indices + assert len(ai)==self.feat.dimension() - assert all([np.allclose(aa, rr) for aa, rr in zip(ai, ref)]) + assert all([np.allclose(aa, rr) for aa, rr in zip(ai, self.ref)]) + + # Test the general input + + def test_general_input_just_runs(self): + _bmutils.atom_idxs_from_general_input(self.feat) + _bmutils.atom_idxs_from_general_input(self.tica) + _bmutils.atom_idxs_from_general_input(self.pca) + try: + _bmutils.atom_idxs_from_general_input("aa") + except TypeError: + pass + + def test_general_input_produces_the_right_indices(self): + ai = _bmutils.atom_idxs_from_general_input(self.feat) + assert all([np.allclose(aa, rr) for aa, rr in zip(ai, self.ref)]) + ai = _bmutils.atom_idxs_from_general_input(self.tica) + assert all([np.allclose(aa, rr) for aa, rr in zip(ai, self.ref)]) + ai = _bmutils.atom_idxs_from_general_input(self.pca) + assert all([np.allclose(aa, rr) for aa, rr in zip(ai, self.ref)]) + class TestInputManipulationShaping(unittest.TestCase): From b3628ad764ead3d26f18537a85989fbd33b118e2 Mon Sep 17 00:00:00 2001 From: gph82 Date: Mon, 21 May 2018 00:47:58 +0200 Subject: [PATCH 48/73] [visualize] minor refactor --- molpx/visualize.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/molpx/visualize.py b/molpx/visualize.py index 0811466..e6e1416 100644 --- a/molpx/visualize.py +++ b/molpx/visualize.py @@ -243,7 +243,8 @@ def _box_me(tuple_in, auto_resize=True): :return: obj:IPywdigets.HBox:, if possible """ - + # TODO THIS IS UNUSED + # TODO EITHER USE IT OR REMOVE IT BEFORE RELEASING widgets_and_canvas = [] size_inches = [] for obj in tuple_in: @@ -768,7 +769,6 @@ def correlations(correlation_input, for ii, line in enumerate(corr_dict["info"]): print('%s is most correlated with '%(line["name"] )) for line in line["lines"]: - # TODO: this is for when tica is there but no featurizer is there if widget is not None and len(corr_dict["atom_idxs"]) != 0: line += ' (in %s in the widget)'%(proj_color_list[ii]) print(line) From 22102795bc8dd5b071937a81ea95f9821e591470 Mon Sep 17 00:00:00 2001 From: gph82 Date: Mon, 21 May 2018 00:48:28 +0200 Subject: [PATCH 49/73] [test_bmutils] refactored test_colors into test_bmutils --- molpx/tests/test_bmutils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/molpx/tests/test_bmutils.py b/molpx/tests/test_bmutils.py index 4a88dc3..c52dffa 100644 --- a/molpx/tests/test_bmutils.py +++ b/molpx/tests/test_bmutils.py @@ -13,7 +13,6 @@ from scipy.spatial.distance import pdist as _pdist, squareform as _squareform - class TestWithBPTIData(unittest.TestCase): r""" A class that contains all the TestCase with the MD info @@ -243,7 +242,6 @@ def test_most_corr_info_wrong_proj_idxs(self): except(ValueError): pass #this should given this type of error - class TestClusteringAndCatalogues(unittest.TestCase): def setUp(self): @@ -1089,6 +1087,8 @@ def test_general_input_produces_the_right_indices(self): ai = _bmutils.atom_idxs_from_general_input(self.pca) assert all([np.allclose(aa, rr) for aa, rr in zip(ai, self.ref)]) +def test_colors(): + _bmutils.matplotlib_colors_no_blue() class TestInputManipulationShaping(unittest.TestCase): From 49281cffcb9fd543d5e45f12eb8dd896f6347965 Mon Sep 17 00:00:00 2001 From: gph82 Date: Mon, 21 May 2018 00:49:33 +0200 Subject: [PATCH 50/73] [tests] increased coverage --- molpx/tests/test_generate.py | 82 +++++++++++++++++++++++++++++++++++ molpx/tests/test_visualize.py | 82 ++++++++++++++++++++++++++++++++++- 2 files changed, 162 insertions(+), 2 deletions(-) create mode 100644 molpx/tests/test_generate.py diff --git a/molpx/tests/test_generate.py b/molpx/tests/test_generate.py new file mode 100644 index 0000000..039aee5 --- /dev/null +++ b/molpx/tests/test_generate.py @@ -0,0 +1,82 @@ +__author__ = 'gph82' + +import unittest +import pyemma +import numpy as np +import molpx +from matplotlib import pyplot as plt +plt.switch_backend('Agg') # allow tests +try: + from .test_bmutils import TestWithBPTIData +except: + from test_bmutils import TestWithBPTIData +class MyVersion(unittest.TestCase): + import molpx + molpx.__version__ + +class TestSample(TestWithBPTIData): + + @classmethod + def setUpClass(self): + TestWithBPTIData.setUpClass() + + @classmethod + def tearDownClass(self): + TestWithBPTIData.tearDownClass() + + def test_just_runs_input_file_one(self): + molpx.generate.sample(self.MD_trajectory_files[0], self.MD_topology_file, self.projected_files_npy[0]) + + def test_just_runs_input_file_many(self): + molpx.generate.sample(self.MD_trajectory_files, self.MD_topology_file, self.projected_files_npy) + + def test_just_runs_input_objects(self): + molpx.generate.sample(self.MD_trajectories, self.MD_topology, self.Ys) + + def test_gen_and_keep_n_samples(self): + molpx.generate.sample(self.MD_trajectories, self.MD_topology, self.Ys, n_geom_samples=5, keep_all_samples=True) + + def test_atom_selections(self): + __, geom_smpl = molpx.generate.sample(self.MD_trajectories, self.MD_topology, self.Ys, atom_selection=np.array([2,4,6,8])) + assert geom_smpl.n_atoms == 4 + + def test_use_cl_as_input(self): + cl = pyemma.coordinates.cluster_kmeans(self.Ys, 10) + molpx.generate.sample(self.MD_trajectories, self.MD_topology, cl) + + def test_right_data_are_returned(self): + # The only way to test this easily is inputting the clusterobject oneself + cl = pyemma.coordinates.cluster_kmeans(self.Ys, 10) + pos, geom_sampl = molpx.generate.sample(self.MD_trajectories, self.MD_topology, cl) + output_assignments = cl.assign(self.tica.transform(self.feat.transform(geom_sampl))[:,:2]) + for ii, oa in enumerate(output_assignments): + # Each frame of the output geometries must've been assigned to each clustercenter + assert ii == oa + # The output sample corresponds to that of the input (in projected space) + assert np.allclose(cl.clustercenters[ii], pos[oa]) + + def test_return_data(self): + molpx.generate.sample(self.MD_trajectories, self.MD_topology, self.Ys, return_data=True) + +class TestProjectionPath(TestWithBPTIData): + + @classmethod + def setUpClass(self): + TestWithBPTIData.setUpClass() + + @classmethod + def tearDownClass(self): + TestWithBPTIData.tearDownClass() + + def test_just_runs(self): + molpx.generate.projection_paths(self.MD_trajectories, self.MD_topology, self.Ys) + + def test_just_runs_one_proj_idx(self): + molpx.generate.projection_paths(self.MD_trajectories, self.MD_topology, self.Ys, proj_idxs=1) + + def _test_right_geoms_are_returned(self): + #Each individual method of generate.projection_path has already been tested. + # TODO + +if __name__ == '__main__': + unittest.main() diff --git a/molpx/tests/test_visualize.py b/molpx/tests/test_visualize.py index 78f8b87..b35575c 100644 --- a/molpx/tests/test_visualize.py +++ b/molpx/tests/test_visualize.py @@ -9,7 +9,8 @@ import mdtraj as md import matplotlib.pyplot as plt import nglview -#plt.switch_backend('Agg') # allow tests +from pyemma.coordinates import tica +plt.switch_backend('Agg') # allow tests from .test_bmutils import TestWithBPTIData @@ -85,6 +86,40 @@ def test_weights_on_biased_FES(self): visualize.FES(self.metad_trajectory_files, self.ala2_topology_file, self.metad_colvar_files, proj_idxs=[1,2], weights=weights) +class TestFES(TestWithBPTIData): + + @classmethod + def setUpClass(self): + TestWithBPTIData.setUpClass() + + def test_just_works_min_input_disk(self): + molpx.visualize.FES(self.MD_trajectory_files, + self.MD_topology_file, + self.projected_files_npy) + + def test_just_works_min_input_memory(self): + molpx.visualize.FES(self.MD_trajectories, + self.MD_topology, + self.Ys) + + def test_overlays(self): + molpx.visualize.FES(self.MD_trajectories, + self.MD_topology, + self.Ys, + n_overlays=5) + + def test_1D(self): + molpx.visualize.FES(self.MD_trajectories, + self.MD_topology, + self.Ys, + proj_idxs=[0]) + + def test_with_weigths(self): + weights = [np.ones(len(iY)) for iY in self.Ys] + molpx.visualize.FES(self.MD_trajectories, + self.MD_topology, + self.Ys, weights=weights) + class TestNGLWidgetWrapper(unittest.TestCase): def setUp(self): @@ -103,7 +138,6 @@ def test_widget_wrapper_w_instantiated_wdg(self): def test_colors(): _bmutils.matplotlib_colors_no_blue() - class TestSample(TestWithBPTIData): @classmethod @@ -115,6 +149,10 @@ def setUpClass(self): self.pos[:,1] = np.random.rand(self.n_sample) self.geom = self.MD_trajectories[0][:self.n_sample] + @classmethod + def tearDownClass(self): + TestWithBPTIData.tearDownClass() + def test_sample_not_sticky_just_works(self): plt.figure() __ = molpx.visualize.sample(self.pos, self.geom, plt.gca()) @@ -222,6 +260,11 @@ def test_correlations_input_warn(self): def test_correlations_inputs_verbose(self): visualize.correlations(self.tica, verbose=True) + def test_correlations_inputs_verbose_and_widget(self): + visualize.correlations(self.tica, + geoms=self.MD_trajectories[0], + verbose=True) + def test_correlations_inputs_color_list_parsing(self): visualize.correlations(self.tica, proj_color_list=['green']) @@ -255,6 +298,41 @@ def test_feature_color_list(self): except TypeError: pass +class TestContacts(TestWithBPTIData): + @classmethod + def setUpClass(self): + TestWithBPTIData.setUpClass() + self.geom = self.MD_trajectories[0] + ctcs, res_idxs = md.compute_contacts(self.geom) + self.ctcs = md.geometry.squareform(ctcs, res_idxs) + + def test_just_runs(self): + visualize.contacts(self.ctcs, self.geom) + + def test_one_ctcframe(self): + # this should fail + try: + visualize.contacts(self.ctcs.mean(0), self.geom) + except AssertionError: + pass + # This should pass + visualize.contacts(self.ctcs.mean(0), self.geom, average=True) + +class TestNGLWidgetWrapper(unittest.TestCase): + + def setUp(self): + self.MD_file = molpx._molpxdir(join='notebooks/data/bpti-c-alpha_centered.pdb') + self.MD_geom = md.load(self.MD_file) + + def test_widget_wrapper_w_None(self): + iwd = molpx.visualize._nglwidget_wrapper(None) + + def test_widget_wrapper_w_file(self): + iwd = molpx.visualize._nglwidget_wrapper(self.MD_file) + + def test_widget_wrapper_w_instantiated_wdg(self): + iwd = molpx.visualize._nglwidget_wrapper(self.MD_file) + molpx.visualize._nglwidget_wrapper(self.MD_geom, ngl_wdg=iwd) class TestBoxMe(unittest.TestCase): From 0694384e7024d1b81c83445286482534b2534aea Mon Sep 17 00:00:00 2001 From: gph82 Date: Tue, 22 May 2018 15:23:56 +0200 Subject: [PATCH 51/73] [_bmtutils] add_atom_idxs_widget raises Exception for more than 4 atoms --- molpx/_bmutils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/molpx/_bmutils.py b/molpx/_bmutils.py index 40854ba..8a51fc9 100644 --- a/molpx/_bmutils.py +++ b/molpx/_bmutils.py @@ -1214,7 +1214,8 @@ def add_atom_idxs_widget(atom_idxs, ngl_wdg, color_list=None, radius=1): elif _np.ndim(iidxs) > 0 and len(iidxs) in [3,4]: ngl_wdg.add_spacefill(selection=iidxs, radius=radius, color=color, component=cc) else: - print("Cannot represent features involving more than 5 atoms per single feature") + raise NotImplementedError("Cannot represent features involving more than 5 atoms per single feature") + return ngl_wdg From d2e8663c206783072aa0c1e47d68063da707048c Mon Sep 17 00:00:00 2001 From: gph82 Date: Tue, 22 May 2018 15:24:56 +0200 Subject: [PATCH 52/73] [tests] increased coverage --- molpx/tests/test_bmutils.py | 162 +++++++++++++++------------------- molpx/tests/test_generate.py | 12 +-- molpx/tests/test_linkutils.py | 24 ++--- 3 files changed, 85 insertions(+), 113 deletions(-) diff --git a/molpx/tests/test_bmutils.py b/molpx/tests/test_bmutils.py index c52dffa..b94e51a 100644 --- a/molpx/tests/test_bmutils.py +++ b/molpx/tests/test_bmutils.py @@ -10,6 +10,7 @@ import mdtraj as md from glob import glob import molpx +from numpy.testing import assert_raises from scipy.spatial.distance import pdist as _pdist, squareform as _squareform @@ -49,6 +50,10 @@ class TestReadingInput(TestWithBPTIData): def setUpClass(self): TestWithBPTIData.setUpClass() + @classmethod + def tearDownClass(self): + TestWithBPTIData.tearDownClass() + def test_data_from_input_npy(self): # Just one string assert np.allclose(self.Ys[0], _bmutils.data_from_input(self.projected_files_npy)[0]) @@ -58,17 +63,8 @@ def test_data_from_input_npy(self): Ys = _bmutils.data_from_input(self.projected_files_npy) assert np.all([np.allclose(jY, iY) for jY, iY in zip(self.Ys, Ys)]) - # Check that it fails properly - try: - _bmutils.data_from_input(1) - except ValueError: - pass - def test_data_from_input_throws_exception(self): - try: - _bmutils.data_from_input(np.random.randn(1000)) - except ValueError: - pass + assert_raises(ValueError, _bmutils.data_from_input, 1) def test_data_from_input_ascii(self): # Just one string @@ -88,14 +84,6 @@ def test_data_from_input_ndarray(self): Ys = _bmutils.data_from_input(self.Ys) assert np.all([np.allclose(jY, iY) for jY,iY in zip(self.Ys, Ys)]) - # Not implemented yet - def _test_data_from_input_ndarray_ascii_npy(self): - # List of everything - Ys = _bmutils.data_from_input([self.projected_file, - self.projected_file.replace('.npy','.dat'), - self.Y]) - assert np.all([np.allclose(self.Y, iY) for iY in Ys]) - def test_moldata_from_input(self): # Traj and top strings moldata = _bmutils.moldata_from_input(self.MD_trajectory_files, MD_top=self.MD_topology) @@ -106,10 +94,7 @@ def test_moldata_from_input(self): assert isinstance(moldata, _bmutils._FeatureReader) # Typerror: - try: - _bmutils.moldata_from_input(11) - except TypeError: - pass + assert_raises(TypeError, _bmutils.moldata_from_input, 11) # List of trajectories moldata = _bmutils.moldata_from_input(self.MD_trajectories) @@ -120,8 +105,10 @@ def test_assert_moldata_belong_data(self): _bmutils.assert_moldata_belong_data(self.MD_trajectories, self.Ys) # src vs data - moldata = _bmutils.moldata_from_input(self.MD_trajectories, MD_top=self.MD_topology) + moldata = _bmutils.moldata_from_input(self.source, MD_top=self.MD_topology) + _bmutils.assert_moldata_belong_data(moldata, self.Ys) + #print(moldata, moldata.number_of_trajectories(), moldata.trajectory_lengths()) # With stride _bmutils.assert_moldata_belong_data(self.MD_trajectories, [iY[::5] for iY in self.Ys], data_stride=5) @@ -131,47 +118,52 @@ class TestSaveTraj(TestWithBPTIData): @classmethod def setUpClass(self): TestWithBPTIData.setUpClass() + self.samples = [[0, 10], + [1, 20], + [2, 30]] + + @classmethod + def tearDownClass(self): + TestWithBPTIData.tearDownClass() + def test_just_works(self): - samples = [[0, 10], - [1, 20], - [2, 30]] - geoms_ref = pyemma.coordinates.save_traj(self.source, samples, None) - geoms_molpx = _bmutils.save_traj_wrapper(self.source, samples, None) + geoms_ref = pyemma.coordinates.save_traj(self.source, self.samples, None) + geoms_molpx = _bmutils.save_traj_wrapper(self.source, self.samples, None) assert np.all([np.allclose(ixyz, jxyz) for ixyz, jxyz in zip(geoms_ref.xyz, geoms_molpx.xyz)]) def test_works_with_MDTrajectories(self): - samples = [[0, 10], - [1, 20], - [2, 30]] - geoms_ref = pyemma.coordinates.save_traj(self.source, samples, None) - geoms_molpx = _bmutils.save_traj_wrapper(self.MD_trajectories, samples, None) - assert np.all([np.allclose(ixyz, jxyz) for ixyz, jxyz in zip(geoms_ref.xyz, geoms_molpx.xyz)]) - def _test_works_with_MDTrajectories_with_stride(self): + geoms_ref = pyemma.coordinates.save_traj(self.source, self.samples, None) + geoms_molpx = _bmutils.save_traj_wrapper(self.MD_trajectories, self.samples, None) + assert np.all([np.allclose(ixyz, jxyz) for ixyz, jxyz in zip(geoms_ref.xyz, geoms_molpx.xyz)]) - samples = [[0, 10], - [1, 20], - [2, 30]] - geoms_ref = pyemma.coordinates.save_traj(self.source, samples, None, stride=2) - geoms_molpx = _bmutils.save_traj_wrapper(self.MD_trajectories, samples, None, stride=2) + def test_works_with_MDTrajectories_with_stride(self): + geoms_ref = pyemma.coordinates.save_traj(self.source, self.samples, None, stride=2) + geoms_molpx = _bmutils.save_traj_wrapper(self.MD_trajectories, self.samples, None, stride=2) assert np.all([np.allclose(ixyz, jxyz) for ixyz, jxyz in zip(geoms_ref.xyz, geoms_molpx.xyz)]) + def test_raises(self): + assert_raises(TypeError, _bmutils.save_traj_wrapper, 1, self.samples, None) + class TestCorrelations(TestWithBPTIData): @classmethod def setUpClass(self): TestWithBPTIData.setUpClass() + @classmethod + def tearDownClass(self): + TestWithBPTIData.tearDownClass() def test_input_types(self): _bmutils.most_corr(self.tica) _bmutils.most_corr(self.pca) _bmutils.most_corr(self.feat) _bmutils.most_corr(self.tica.feature_TIC_correlation) - try: - _bmutils.most_corr("a") - except TypeError: - pass + _bmutils.most_corr(self.tica.feature_TIC_correlation, feat_name="My CustomFeature") + + def test_fails(self): + assert_raises(TypeError, _bmutils.most_corr, "a") def test_printing(self): print(_bmutils.most_corr(self.tica)) @@ -235,12 +227,9 @@ def test_most_corr_info_works_with_options_and_proj_idxs(self): assert np.all(np.allclose(rv, mcv) for rv, mcv in zip(ref_feats, np.squeeze(corr_dict['feats']))) def test_most_corr_info_wrong_proj_idxs(self): - proj_idxs = [1, 0, 10] # we don't have 10 TICs - try: - _bmutils.most_corr(self.tica, proj_idxs=proj_idxs) - except(ValueError): - pass #this should given this type of error + assert_raises(ValueError, _bmutils.most_corr, self.tica, proj_idxs=proj_idxs) + class TestClusteringAndCatalogues(unittest.TestCase): @@ -305,12 +294,10 @@ def test_interval_schachtelung_fails(self): target_y = 0 # Is not contained in [2**2, 500**2] - try: - _bmutils.interval_schachtelung(y, [2, 500], target=target_y, eps=eps, + + assert_raises(Exception, _bmutils.interval_schachtelung, y, [2, 500], target=target_y, eps=eps, #verbose=True ) - except: - pass def test_catalogues(self): @@ -497,10 +484,7 @@ def setUp(self): def test_throws_exception(self): - try: - start_idx = _bmutils.get_good_starting_point(self.cl, self.geom_smpl, strategy="what?") - except NotImplementedError: - pass + assert_raises(NotImplementedError, _bmutils.get_good_starting_point, self.cl, self.geom_smpl, strategy="what?") # This test doesn't exactly belong here but this is the best class for now def test_find_centers_GMM(self): @@ -595,14 +579,8 @@ def test_input_parsing(self): _bmutils.visual_path(self.cat_idxs, self.cat_data, start_pos=1) # Not implemented Errors - try: - _bmutils.visual_path(self.cat_idxs, self.cat_data, start_pos="other") - except NotImplementedError: - pass - try: - _bmutils.visual_path(self.cat_idxs, self.cat_data, path_type="xxxx") - except NotImplementedError: - pass + assert_raises(NotImplementedError, _bmutils.visual_path, self.cat_idxs, self.cat_data, start_pos="other") + assert_raises(NotImplementedError, _bmutils.visual_path, self.cat_idxs, self.cat_data, path_type="xxxx") class TestMinDispPaths(unittest.TestCase): @@ -676,15 +654,11 @@ def test_empty_no_fail(self): result = _bmutils.get_ascending_coord_idx(self.data[:,[1,2]], fail_if_empty=False) assert len(result)==0 def test_empty_fail(self): - try: - _bmutils.get_ascending_coord_idx(self.data[:, [1,2]], fail_if_empty=True) - except ValueError: - pass + assert_raises(ValueError, _bmutils.get_ascending_coord_idx, self.data[:, [1,2]], fail_if_empty=True) + def test_more_than_one_fails(self): - try: - _bmutils.get_ascending_coord_idx(self.data[:, :], fail_if_more_than_one=True) - except: - pass + assert_raises(Exception, _bmutils.get_ascending_coord_idx, self.data[:, :], fail_if_more_than_one=True) + def test_more_than_one_passes(self): _bmutils.get_ascending_coord_idx(self.data[:, :], fail_if_more_than_one=False) @@ -840,7 +814,7 @@ def tearDown(self): pass def test_running_avg_idxs_none(self): - idxs, windows = _bmutils.running_avg_idxs(10, 0) + idxs, windows = _bmutils.running_avg_idxs(10, 0, debug=True) # If the running average is with radius zero, it's just a normal average assert np.allclose([0,1,2,3,4,5,6,7,8,9], idxs) assert np.all(np.allclose(ii,jj) for ii, jj in zip(([0,1,2,3,4,5,6,7,8,9], windows))) @@ -870,14 +844,12 @@ def test_running_avg_idxs_two(self): windows)) def test_running_avg_idxs_too_large_window(self): - try: - idxs, windows = _bmutils.running_avg_idxs(10, 5) - except AssertionError: - pass + assert_raises(AssertionError, _bmutils.running_avg_idxs, 10, 5) + def test_raises_if_not_symmetric(self): + assert_raises(NotImplementedError, _bmutils.running_avg_idxs, 10, 5, symmetric=False) def test_smooth_geom_it_just_runs_and_gives_correct_output_type(self): - # No data assert isinstance(_bmutils.smooth_geom(self.traj, 0), md.Trajectory) assert isinstance(_bmutils.smooth_geom(self.traj, 0, superpose=False), md.Trajectory) @@ -1009,6 +981,16 @@ def setUp(self): def tearDown(self): pass + def test_put_atom_idxs_on_widget(self): + import nglview + iwd = nglview.show_file(self.MD_topology) + #Regular input + _bmutils.add_atom_idxs_widget(self.ref, iwd) + # Insuficient colors + _bmutils.add_atom_idxs_widget(self.ref, iwd, ["red"]) + # Too many indices raises + assert_raises(NotImplementedError, _bmutils.add_atom_idxs_widget, [[1,2,3,4,5]], iwd) + def test_repr_atoms_from_residues(self): # Use Di-Ala top = md.load(molpx._molpxdir(join='notebooks/data/ala2.pdb')).top @@ -1023,17 +1005,12 @@ def test_repr_atoms_from_residues(self): assert aa.name == "C" # Force fail - try: - _bmutils.get_repr_atom_for_residue(top.residue(0), cands=["CB"]) - except ValueError: - pass + assert_raises(ValueError, _bmutils.get_repr_atom_for_residue, top.residue(0), cands=["CB"]) # Feature by feature def test_atom_idxs_from_feature_not_recognized(self): - try: - _bmutils.atom_idxs_from_feature("aa") - except NotImplementedError: - pass + assert_raises(NotImplementedError, _bmutils.atom_idxs_from_feature, "aa") + def test_atom_idxs_from_feature_xyz(self): ai = _bmutils.atom_idxs_from_feature(self.feat.active_features[0]) assert np.allclose(self.cartesian_indices, ai) @@ -1068,16 +1045,12 @@ def test_all_features_together(self): assert len(ai)==self.feat.dimension() assert all([np.allclose(aa, rr) for aa, rr in zip(ai, self.ref)]) - # Test the general input - + # Test the general input def test_general_input_just_runs(self): _bmutils.atom_idxs_from_general_input(self.feat) _bmutils.atom_idxs_from_general_input(self.tica) _bmutils.atom_idxs_from_general_input(self.pca) - try: - _bmutils.atom_idxs_from_general_input("aa") - except TypeError: - pass + assert_raises(TypeError, _bmutils.atom_idxs_from_general_input, "aa") def test_general_input_produces_the_right_indices(self): ai = _bmutils.atom_idxs_from_general_input(self.feat) @@ -1133,6 +1106,9 @@ def test_labelize(self): assert labels[0] == feat.describe()[0] assert labels[1] == feat.describe()[1] + # Raises Error + assert_raises(TypeError, _bmutils.labelize,1, [0,1]) + def test_superpose_list_of_geoms(self): geom = md.load(molpx._molpxdir(join='notebooks/data/bpti-c-alpha_centered.pdb')) diff --git a/molpx/tests/test_generate.py b/molpx/tests/test_generate.py index 039aee5..4a9d1d7 100644 --- a/molpx/tests/test_generate.py +++ b/molpx/tests/test_generate.py @@ -6,10 +6,8 @@ import molpx from matplotlib import pyplot as plt plt.switch_backend('Agg') # allow tests -try: - from .test_bmutils import TestWithBPTIData -except: - from test_bmutils import TestWithBPTIData +from .test_bmutils import TestWithBPTIData + class MyVersion(unittest.TestCase): import molpx molpx.__version__ @@ -33,6 +31,9 @@ def test_just_runs_input_file_many(self): def test_just_runs_input_objects(self): molpx.generate.sample(self.MD_trajectories, self.MD_topology, self.Ys) + def test_gen_n_samples(self): + molpx.generate.sample(self.MD_trajectories, self.MD_topology, self.Ys, n_geom_samples=5) + def test_gen_and_keep_n_samples(self): molpx.generate.sample(self.MD_trajectories, self.MD_topology, self.Ys, n_geom_samples=5, keep_all_samples=True) @@ -76,7 +77,8 @@ def test_just_runs_one_proj_idx(self): def _test_right_geoms_are_returned(self): #Each individual method of generate.projection_path has already been tested. - # TODO + # TODO tomorrow, use ideas from TestMinRmsdPaths + pass if __name__ == '__main__': unittest.main() diff --git a/molpx/tests/test_linkutils.py b/molpx/tests/test_linkutils.py index 2d4e310..2dd3f8c 100644 --- a/molpx/tests/test_linkutils.py +++ b/molpx/tests/test_linkutils.py @@ -12,6 +12,7 @@ import nglview +from numpy.testing import assert_raises from scipy.spatial import cKDTree as _cKDTree @@ -47,19 +48,13 @@ def test_just_works_exclude_coord(self): def test_force_exceptions(self): plt.figure() - try: - __ = molpx._linkutils.link_ax_w_pos_2_nglwidget(plt.gca(), self.pos, self.ngl_wdg, + assert_raises(TypeError, molpx._linkutils.link_ax_w_pos_2_nglwidget, plt.gca(), self.pos, self.ngl_wdg, dot_color=2) - except TypeError: - pass plt.figure() - try: - pos_rnd = np.random.randn(self.n_sample, 2) - __ = molpx._linkutils.link_ax_w_pos_2_nglwidget(plt.gca(), pos_rnd, self.ngl_wdg, + pos_rnd = np.random.randn(self.n_sample, 2) + assert_raises(ValueError, molpx._linkutils.link_ax_w_pos_2_nglwidget, plt.gca(), pos_rnd, self.ngl_wdg, band_width=[.1, .1]) - except ValueError: - pass def test_just_works_radius(self): plt.figure() @@ -142,16 +137,15 @@ def test_all(self): def test_force_exceptions(self): plt.plot(0, 0) line = plt.gca().lines[0] + # This passes interactively in the console...? try: - molpx._linkutils.update2Dlines(line,0,0) - except AttributeError: + assert_raises(AttributeError, molpx._linkutils.update2Dlines, line,0,0) + except AssertionError: pass setattr(line, "whatisthis", "non_existing_line") - try: - molpx._linkutils.update2Dlines(line,0,0) - except TypeError: - pass + assert_raises(TypeError, molpx._linkutils.update2Dlines, line,0,0) + class TestClickOnAxisListener(unittest.TestCase): @classmethod From 22abf4bdd285091f061b82b4061bac71728d09d2 Mon Sep 17 00:00:00 2001 From: gph82 Date: Tue, 22 May 2018 18:05:08 +0200 Subject: [PATCH 53/73] [visualize] merge-conflict deleted lazy import of pyplot, now it's back to lazy --- molpx/visualize.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/molpx/visualize.py b/molpx/visualize.py index 241484d..b7999f4 100644 --- a/molpx/visualize.py +++ b/molpx/visualize.py @@ -14,7 +14,7 @@ from . import _bmutils from . import _linkutils -from matplotlib import pyplot as _plt, rcParams as _rcParams +from matplotlib import rcParams as _rcParams import nglview as _nglview import mdtraj as _md from ipywidgets import VBox as _VBox, Layout as _Layout, Button as _Button @@ -172,6 +172,7 @@ def FES(MD_trajectories, MD_top, projected_trajectories, list with all the :obj:`mdtraj.Trajectory`-objects contained in the :obj:`widgetbox` """ + from matplotlib import pyplot as _plt # Prepare the overlay option n_overlays = _np.min([n_overlays,50]) if n_overlays>1: @@ -300,7 +301,7 @@ def _plot_ND_FES(data, ax_labels, weights=None, bins=50, figsize=(4,4)): edges : tuple containimg the axes along which FES is to be plotted (only in the 1D case so far, else it's None) """ - + from matplotlib import pyplot as _plt _plt.figure(figsize=figsize) ax = _plt.gca() idata = _np.vstack(data) @@ -439,6 +440,7 @@ def traj(MD_trajectories, """ + from matplotlib import pyplot as _plt smallfontsize = int(_rcParams['font.size'] / 1.5) proj_idxs = _bmutils.listify_if_int(proj_idxs) From cc4ebcd02d887b3ac6004da4171368194d2d9d58 Mon Sep 17 00:00:00 2001 From: gph82 Date: Tue, 22 May 2018 18:05:38 +0200 Subject: [PATCH 54/73] [notebooks] updated notebooks to new code --- .../notebooks/0.molPX_quick_intro_Ala2.ipynb | 128 ++- .../1.molPX_and_PyEMMA_Features.ipynb | 313 +++--- molpx/notebooks/2.molPX_TICA_BPTI.ipynb | 948 +++++++++++++++++- molpx/notebooks/3.molPX_TICA_Ala2.ipynb | 460 ++++----- .../4.molPX_metadynamics_Di-Ala.ipynb | 65 +- molpx/visualize.py | 2 +- 6 files changed, 1369 insertions(+), 547 deletions(-) diff --git a/molpx/notebooks/0.molPX_quick_intro_Ala2.ipynb b/molpx/notebooks/0.molPX_quick_intro_Ala2.ipynb index bd316f1..b2ff8bc 100644 --- a/molpx/notebooks/0.molPX_quick_intro_Ala2.ipynb +++ b/molpx/notebooks/0.molPX_quick_intro_Ala2.ipynb @@ -21,8 +21,7 @@ }, "outputs": [], "source": [ - "import molpx\n", - "%matplotlib ipympl" + "import molpx" ] }, { @@ -70,27 +69,34 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "cbfb5bb0aa114fce88e90ae57fdd123f", + "model_id": "2009faf662cf4e72b01d505a61808acc", "version_major": 2, "version_minor": 0 }, - "text/html": [ - "

Failed to display Jupyter Widget of type MolPXHBox.

\n", - "

\n", - " If you're reading this message in Jupyter Notebook or JupyterLab, it may mean\n", - " that the widgets JavaScript is still loading. If this message persists, it\n", - " likely means that the widgets JavaScript library is either not installed or\n", - " not enabled. See the Jupyter\n", - " Widgets Documentation for setup instructions.\n", - "

\n", - "

\n", - " If you're reading this message in another notebook frontend (for example, a static\n", - " rendering on GitHub or NBViewer),\n", - " it may mean that your frontend doesn't currently support widgets.\n", - "

\n" - ], "text/plain": [ - "MolPXHBox(children=(NGLWidget(count=101), FigureCanvasNbAgg()))" + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "22-05-18 17:13:44 pyemma.coordinates.clustering.kmeans.KmeansClustering[0] INFO Cluster centers converged after 7 steps.\n", + "\r" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "c3c35ff618c14cc2aa570978020fa2ef", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" ] }, "metadata": {}, @@ -130,27 +136,34 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "e5ff550df1784bc698a701c5fcad3490", + "model_id": "55360d96b6384bb7929baaeba0cb188e", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "22-05-18 17:14:02 pyemma.coordinates.clustering.kmeans.KmeansClustering[5] INFO Cluster centers converged after 9 steps.\n", + "\r" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "6d97b7e9395143769da9fc812ca7bf99", "version_major": 2, "version_minor": 0 }, - "text/html": [ - "

Failed to display Jupyter Widget of type MolPXVBox.

\n", - "

\n", - " If you're reading this message in Jupyter Notebook or JupyterLab, it may mean\n", - " that the widgets JavaScript is still loading. If this message persists, it\n", - " likely means that the widgets JavaScript library is either not installed or\n", - " not enabled. See the Jupyter\n", - " Widgets Documentation for setup instructions.\n", - "

\n", - "

\n", - " If you're reading this message in another notebook frontend (for example, a static\n", - " rendering on GitHub or NBViewer),\n", - " it may mean that your frontend doesn't currently support widgets.\n", - "

\n" - ], "text/plain": [ - "MolPXVBox(children=(MolPXHBox(children=(VBox(children=(Button(description='NGL widgets', layout=Layout(width='100%'), style=ButtonStyle()), VBox(children=(NGLWidget(count=3334),), layout=Layout(border='solid'))), layout=Layout(height='2.0in', width='5.0in')), FigureCanvasNbAgg())), MolPXHBox(children=(NGLWidget(count=101), FigureCanvasNbAgg()))))" + "A Jupyter Widget" ] }, "metadata": {}, @@ -204,27 +217,34 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "e427834063234005be0f3c41f8f7d04e", + "model_id": "324992cdd62a4b3ea8c8d6fd0daf7369", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "22-05-18 17:14:20 pyemma.coordinates.clustering.kmeans.KmeansClustering[14] INFO Cluster centers converged after 6 steps.\n", + "\r" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "3638e8baa9244c25af118111a71e6df3", "version_major": 2, "version_minor": 0 }, - "text/html": [ - "

Failed to display Jupyter Widget of type MolPXHBox.

\n", - "

\n", - " If you're reading this message in Jupyter Notebook or JupyterLab, it may mean\n", - " that the widgets JavaScript is still loading. If this message persists, it\n", - " likely means that the widgets JavaScript library is either not installed or\n", - " not enabled. See the Jupyter\n", - " Widgets Documentation for setup instructions.\n", - "

\n", - "

\n", - " If you're reading this message in another notebook frontend (for example, a static\n", - " rendering on GitHub or NBViewer),\n", - " it may mean that your frontend doesn't currently support widgets.\n", - "

\n" - ], "text/plain": [ - "MolPXHBox(children=(NGLWidget(count=101), FigureCanvasNbAgg()))" + "A Jupyter Widget" ] }, "metadata": {}, @@ -257,7 +277,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 7, "metadata": {}, "outputs": [ { diff --git a/molpx/notebooks/1.molPX_and_PyEMMA_Features.ipynb b/molpx/notebooks/1.molPX_and_PyEMMA_Features.ipynb index c2cc5a9..116613a 100644 --- a/molpx/notebooks/1.molPX_and_PyEMMA_Features.ipynb +++ b/molpx/notebooks/1.molPX_and_PyEMMA_Features.ipynb @@ -31,7 +31,9 @@ { "cell_type": "code", "execution_count": 1, - "metadata": {}, + "metadata": { + "collapsed": true + }, "outputs": [], "source": [ "top = 'notebooks/data/bpti-c-alpha_centered.pdb'\n", @@ -43,8 +45,7 @@ "dt = 244 #saving interval in the .xtc files, in ns\n", "\n", "import molpx\n", - "from matplotlib import pylab as plt\n", - "%matplotlib ipympl\n", + "from matplotlib import pyplot as plt\n", "import pyemma\n", "import numpy as np\n", "\n", @@ -73,38 +74,61 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "e07101841c2542439184b1e66b4da372", + "model_id": "039f9856e0574670aba06c25039896a4", "version_major": 2, "version_minor": 0 }, - "text/html": [ - "

Failed to display Jupyter Widget of type Box.

\n", - "

\n", - " If you're reading this message in Jupyter Notebook or JupyterLab, it may mean\n", - " that the widgets JavaScript is still loading. If this message persists, it\n", - " likely means that the widgets JavaScript library is either not installed or\n", - " not enabled. See the Jupyter\n", - " Widgets Documentation for setup instructions.\n", - "

\n", - "

\n", - " If you're reading this message in another notebook frontend (for example, a static\n", - " rendering on GitHub or NBViewer),\n", - " it may mean that your frontend doesn't currently support widgets.\n", - "

\n" - ], "text/plain": [ - "Box(children=(Text(value=''), IntProgress(value=0)))" + "A Jupyter Widget" ] }, "metadata": {}, "output_type": "display_data" }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\r" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "f3bce3719c614589a2e26c9c4d305ccc", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\r" + ] + }, { "data": { "text/plain": [ "('DIST: PRO 9 CA 8 - LYS 15 CA 14',\n", " 'DIST: PRO 9 CA 8 - TYR 23 CA 22',\n", - " )" + " MDFeaturizer with features:\n", + " ['DIST: ARG 1 CA 0 - ASP 3 CA 2',\n", + " 'DIST: ARG 1 CA 0 - CYS 5 CA 4',\n", + " 'DIST: ARG 1 CA 0 - GLU 7 CA 6',\n", + " 'DIST: ARG 1 CA 0 - PRO 9 CA 8',\n", + " 'DIST: ARG 1 CA 0 - THR 11 CA 10',\n", + " 'DIST: ARG 1 CA 0 - PRO 13 CA 12',\n", + " 'DIST: ARG 1 CA 0 - LYS 15 CA 14',\n", + " 'DIST: ARG 1 CA 0 - ARG 17 CA 16',\n", + " 'DIST: ARG 1 CA 0 - ILE 19 CA 18',\n", + " 'DIST: ARG 1 CA 0 - TYR 21 CA 20', ...])" ] }, "execution_count": 3, @@ -138,56 +162,55 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "e585639a2d214150b6bf44bddaf096cd", + "model_id": "596903342fdb4f2f9189494ef76e9519", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "22-05-18 18:01:59 pyemma.coordinates.clustering.kmeans.KmeansClustering[2] INFO Algorithm did not reach convergence criterion of 1e-05 in 10 iterations. Consider increasing max_iter.\n", + "\r" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "d00d967bca9c406cb23895d01069cf58", "version_major": 2, "version_minor": 0 }, - "text/html": [ - "

Failed to display Jupyter Widget of type Box.

\n", - "

\n", - " If you're reading this message in Jupyter Notebook or JupyterLab, it may mean\n", - " that the widgets JavaScript is still loading. If this message persists, it\n", - " likely means that the widgets JavaScript library is either not installed or\n", - " not enabled. See the Jupyter\n", - " Widgets Documentation for setup instructions.\n", - "

\n", - "

\n", - " If you're reading this message in another notebook frontend (for example, a static\n", - " rendering on GitHub or NBViewer),\n", - " it may mean that your frontend doesn't currently support widgets.\n", - "

\n" - ], "text/plain": [ - "Box(children=(Text(value=''), IntProgress(value=0)))" + "A Jupyter Widget" ] }, "metadata": {}, "output_type": "display_data" }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\r" + ] + }, { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "de2388685daf4abe9f7bee72bbf84972", + "model_id": "17a36ccf478644479abeb8ddc214664f", "version_major": 2, "version_minor": 0 }, - "text/html": [ - "

Failed to display Jupyter Widget of type MolPXHBox.

\n", - "

\n", - " If you're reading this message in Jupyter Notebook or JupyterLab, it may mean\n", - " that the widgets JavaScript is still loading. If this message persists, it\n", - " likely means that the widgets JavaScript library is either not installed or\n", - " not enabled. See the Jupyter\n", - " Widgets Documentation for setup instructions.\n", - "

\n", - "

\n", - " If you're reading this message in another notebook frontend (for example, a static\n", - " rendering on GitHub or NBViewer),\n", - " it may mean that your frontend doesn't currently support widgets.\n", - "

\n" - ], "text/plain": [ - "MolPXHBox(children=(NGLWidget(count=95), FigureCanvasNbAgg()))" + "A Jupyter Widget" ] }, "metadata": {}, @@ -206,7 +229,7 @@ " #n_overlays=5,\n", " #sticky=True,\n", " )\n", - "__ = molpx.visualize.feature(feat.active_features[0], \n", + "__ = molpx.visualize.feature(feat, \n", " mpx_wdg_box.linked_ngl_wdgs[0], \n", " idxs=proj_idxs, radius=.5, \n", " color_list=['red','green'])\n", @@ -223,7 +246,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 7, "metadata": { "scrolled": false }, @@ -231,27 +254,12 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "41da98402f6f4ab296faa44fee8a6369", + "model_id": "b04bd522acca45aa979ef3fbf163085c", "version_major": 2, "version_minor": 0 }, - "text/html": [ - "

Failed to display Jupyter Widget of type MolPXHBox.

\n", - "

\n", - " If you're reading this message in Jupyter Notebook or JupyterLab, it may mean\n", - " that the widgets JavaScript is still loading. If this message persists, it\n", - " likely means that the widgets JavaScript library is either not installed or\n", - " not enabled. See the Jupyter\n", - " Widgets Documentation for setup instructions.\n", - "

\n", - "

\n", - " If you're reading this message in another notebook frontend (for example, a static\n", - " rendering on GitHub or NBViewer),\n", - " it may mean that your frontend doesn't currently support widgets.\n", - "

\n" - ], "text/plain": [ - "MolPXHBox(children=(VBox(children=(Button(description='NGL widgets', layout=Layout(width='100%'), style=ButtonStyle()), VBox(children=(NGLWidget(count=4125),), layout=Layout(border='solid'))), layout=Layout(height='2.0in', width='5.0in')), FigureCanvasNbAgg()))" + "A Jupyter Widget" ] }, "metadata": {}, @@ -269,7 +277,7 @@ " panel_height=1, \n", " proj_labels='feat',\n", " )\n", - "molpx.visualize.feature(feat.active_features[0], mpx_wdg_box.linked_ngl_wdgs[0], idxs=proj_idxs)\n", + "molpx.visualize.feature(feat, mpx_wdg_box.linked_ngl_wdgs[0], idxs=proj_idxs)\n", "mpx_wdg_box" ] }, @@ -282,7 +290,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 8, "metadata": {}, "outputs": [ { @@ -291,7 +299,7 @@ "('ATOM:PRO 9 CA 8 x', 'ATOM:LYS 15 CA 14 x')" ] }, - "execution_count": 6, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } @@ -305,62 +313,61 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 10, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "8a9a553996a44aa0ab2b34cfe5946941", + "model_id": "669bd2415d00423b99d531b76ab92d0a", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "22-05-18 17:16:35 pyemma.coordinates.clustering.kmeans.KmeansClustering[30] INFO Algorithm did not reach convergence criterion of 1e-05 in 10 iterations. Consider increasing max_iter.\n", + "\r" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "418ca4b4e62042908bcb6cbc4acd6f1f", "version_major": 2, "version_minor": 0 }, - "text/html": [ - "

Failed to display Jupyter Widget of type Box.

\n", - "

\n", - " If you're reading this message in Jupyter Notebook or JupyterLab, it may mean\n", - " that the widgets JavaScript is still loading. If this message persists, it\n", - " likely means that the widgets JavaScript library is either not installed or\n", - " not enabled. See the Jupyter\n", - " Widgets Documentation for setup instructions.\n", - "

\n", - "

\n", - " If you're reading this message in another notebook frontend (for example, a static\n", - " rendering on GitHub or NBViewer),\n", - " it may mean that your frontend doesn't currently support widgets.\n", - "

\n" - ], "text/plain": [ - "Box(children=(Text(value=''), IntProgress(value=0)))" + "A Jupyter Widget" ] }, "metadata": {}, "output_type": "display_data" }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\r" + ] + }, { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "c7a939e0f10e49d79db78074921df608", + "model_id": "8da959d7add54ef58439fd5b7d358b22", "version_major": 2, "version_minor": 0 }, - "text/html": [ - "

Failed to display Jupyter Widget of type MolPXHBox.

\n", - "

\n", - " If you're reading this message in Jupyter Notebook or JupyterLab, it may mean\n", - " that the widgets JavaScript is still loading. If this message persists, it\n", - " likely means that the widgets JavaScript library is either not installed or\n", - " not enabled. See the Jupyter\n", - " Widgets Documentation for setup instructions.\n", - "

\n", - "

\n", - " If you're reading this message in another notebook frontend (for example, a static\n", - " rendering on GitHub or NBViewer),\n", - " it may mean that your frontend doesn't currently support widgets.\n", - "

\n" - ], "text/plain": [ - "MolPXHBox(children=(NGLWidget(count=103), FigureCanvasNbAgg()))" + "A Jupyter Widget" ] }, "metadata": {}, @@ -377,7 +384,7 @@ " proj_labels='feat',\n", " #n_overlays=5,\n", " )\n", - "molpx.visualize.feature(feat.active_features[0], mpx_wdg_box.linked_ngl_wdgs[0], idxs=proj_idxs, color_list=['red','green'])\n", + "molpx.visualize.feature(feat, mpx_wdg_box.linked_ngl_wdgs[0], idxs=proj_idxs, color_list=['red','green'])\n", "mpx_wdg_box" ] }, @@ -390,41 +397,12 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 11, "metadata": { + "collapsed": true, "scrolled": false }, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "d0f597ac9a85431fa6600c772d4520c9", - "version_major": 2, - "version_minor": 0 - }, - "text/html": [ - "

Failed to display Jupyter Widget of type Box.

\n", - "

\n", - " If you're reading this message in Jupyter Notebook or JupyterLab, it may mean\n", - " that the widgets JavaScript is still loading. If this message persists, it\n", - " likely means that the widgets JavaScript library is either not installed or\n", - " not enabled. See the Jupyter\n", - " Widgets Documentation for setup instructions.\n", - "

\n", - "

\n", - " If you're reading this message in another notebook frontend (for example, a static\n", - " rendering on GitHub or NBViewer),\n", - " it may mean that your frontend doesn't currently support widgets.\n", - "

\n" - ], - "text/plain": [ - "Box(children=(Text(value=''), IntProgress(value=0)))" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "from os.path import exists\n", "top = molpx._molpxdir(join='notebooks/data/ala2.pdb')\n", @@ -446,33 +424,40 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "431f665cb3c2452094c78ec3ccc0503b", + "model_id": "465d9173d4864fe58cba25ca2b1fe31d", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "22-05-18 17:17:16 pyemma.coordinates.clustering.kmeans.KmeansClustering[41] INFO Cluster centers converged after 8 steps.\n", + "\r" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "304c9b268c2b4f38a499d1e25a1f0596", "version_major": 2, "version_minor": 0 }, - "text/html": [ - "

Failed to display Jupyter Widget of type MolPXHBox.

\n", - "

\n", - " If you're reading this message in Jupyter Notebook or JupyterLab, it may mean\n", - " that the widgets JavaScript is still loading. If this message persists, it\n", - " likely means that the widgets JavaScript library is either not installed or\n", - " not enabled. See the Jupyter\n", - " Widgets Documentation for setup instructions.\n", - "

\n", - "

\n", - " If you're reading this message in another notebook frontend (for example, a static\n", - " rendering on GitHub or NBViewer),\n", - " it may mean that your frontend doesn't currently support widgets.\n", - "

\n" - ], "text/plain": [ - "MolPXHBox(children=(NGLWidget(count=101), FigureCanvasNbAgg()))" + "A Jupyter Widget" ] }, "metadata": {}, @@ -488,7 +473,7 @@ " proj_idxs=proj_idxs,\n", " proj_labels=feat, \n", " )\n", - "molpx.visualize.feature(feat.active_features[0], mpx_wdg_box.linked_ngl_wdgs[0], idxs=[0], radius=.5)\n", + "molpx.visualize.feature(feat, mpx_wdg_box.linked_ngl_wdgs[0], idxs=[0], radius=.5)\n", "mpx_wdg_box" ] } diff --git a/molpx/notebooks/2.molPX_TICA_BPTI.ipynb b/molpx/notebooks/2.molPX_TICA_BPTI.ipynb index 9331ab6..d7ce265 100644 --- a/molpx/notebooks/2.molPX_TICA_BPTI.ipynb +++ b/molpx/notebooks/2.molPX_TICA_BPTI.ipynb @@ -32,7 +32,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": { "collapsed": true }, @@ -47,8 +47,7 @@ "dt = 244 #saving interval in the .xtc files, in ns\n", "\n", "import molpx\n", - "from matplotlib import pylab as plt\n", - "%matplotlib ipympl\n", + "from matplotlib import pyplot as plt\n", "import pyemma\n", "import numpy as np\n", "\n", @@ -66,7 +65,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": { "collapsed": true }, @@ -89,7 +88,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": { "collapsed": true }, @@ -122,11 +121,69 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": { "scrolled": false }, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "0b77491e19f04670b371059496e0cc38", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "22-05-18 18:03:04 pyemma.coordinates.clustering.kmeans.KmeansClustering[0] INFO Algorithm did not reach convergence criterion of 1e-05 in 10 iterations. Consider increasing max_iter.\n", + "\r" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "2208cf551df64d2f873e203a818be74f", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\r" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "ec1e5f9519584f34a7c0412206756f41", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "mpx_wdg_box = molpx.visualize.FES(MD_list, \n", " #MD_trajfiles, \n", @@ -157,11 +214,90 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": { "scrolled": false }, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "5ffb7ac441bf48709f98d7dac4599d36", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\r" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "405cec954136493cb7957e2b4dfe6d13", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "22-05-18 17:20:44 pyemma.coordinates.clustering.kmeans.KmeansClustering[10] INFO Cluster centers converged after 10 steps.\n", + "\r" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "aa977b08505946eebcd4fd22a6b6e4ff", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\r" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "aef9c92d135c4203901d394e5a041d46", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "mpx_wdg_box = molpx.visualize.traj(MD_trajfiles, \n", " top, \n", @@ -192,9 +328,85 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "d9a0e4abb76a4f0d86558efdcb5da2aa", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\r" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "5c239027c95d4de18c7b2b400cc517ad", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "22-05-18 17:20:57 pyemma.coordinates.clustering.kmeans.KmeansClustering[21] INFO Algorithm did not reach convergence criterion of 1e-05 in 10 iterations. Consider increasing max_iter.\n", + "\r" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "6ec79eb470234baa8dd28d900ca509dd", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\r" + ] + }, + { + "data": { + "text/plain": [ + "((203, 2),\n", + " )" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "data_sample, geoms = molpx.generate.sample(#MD_list, \n", " MD_trajfiles, \n", @@ -217,11 +429,48 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "metadata": { "scrolled": false }, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "6e9a1a3b366d4009938e5b8bbd6c99f8", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/guille/miniconda3/lib/python3.6/site-packages/ipykernel_launcher.py:4: RuntimeWarning: divide by zero encountered in log\n", + " after removing the cwd from sys.path.\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "c187e924b3ca4299af716d3fbeec9888", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "# Replot the FES\n", "plt.figure(figsize=(7,7))\n", @@ -248,9 +497,117 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "10a96c7344a2449e96328205defaf337", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\r" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "c97daf3e69c54ad79f176df535278b96", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "22-05-18 17:21:43 pyemma.coordinates.clustering.kmeans.KmeansClustering[32] INFO Algorithm did not reach convergence criterion of 1e-05 in 10 iterations. Consider increasing max_iter.\n", + "\r" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "e66eaf85903d467db89d3a85c3f17f39", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\r" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "59a165f8c8c046e98c8b9e6f4db0fdaf", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "22-05-18 17:21:59 pyemma.coordinates.clustering.kmeans.KmeansClustering[39] INFO Algorithm did not reach convergence criterion of 1e-05 in 10 iterations. Consider increasing max_iter.\n", + "\r" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "10f74903472447298d4ea2e3e0052ed9", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\r" + ] + } + ], "source": [ "paths_dict, idata = molpx.generate.projection_paths(#MD_list, \n", " MD_trajfiles, \n", @@ -275,7 +632,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "metadata": { "collapsed": true }, @@ -295,11 +652,48 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "metadata": { "scrolled": false }, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "8da72c47d150404893178357e21a5661", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/guille/miniconda3/lib/python3.6/site-packages/ipykernel_launcher.py:3: RuntimeWarning: divide by zero encountered in log\n", + " This is separate from the ipykernel package so we can avoid doing imports until\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "6c1cbdc8faeb44b189ec19d23a1c5a35", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "plt.figure(figsize=(7,7))\n", "h, (x,y) = np.histogramdd(np.vstack(Y)[:,proj_idxs], bins=50)\n", @@ -329,9 +723,73 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "c5fdccaadba94e1c9cf2ab7376f4858c", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\r" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "53f8546c8e624873b1b5b2ac804867c9", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\r" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "db24ba31fd1f4055a80d24e19521a4f5", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\r" + ] + } + ], "source": [ "# Re-do the TICA computation to make sure we have a tica object in memory\n", "feat = pyemma.coordinates.featurizer(top)\n", @@ -351,9 +809,24 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "0fba560f9aba4f2ab61d4249acf4d874", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "# Comment or uncomment the optinal parameters and see how the method reacts\n", "# You can use a pre-instantiated the widget\n", @@ -371,22 +844,156 @@ "ngl_wdg" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Use the correlation-dictionary's modified print function to see what's inside in a human-friendly way" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Correlation dictionary for 3 projections\n", + " Corr[proj_0|feat] = 0.8\n", + " DIST: PRO 9 CA 8 - TYR 23 CA 22\n", + " feat nr. 112, atom idxs [ 8 22]\n", + " Corr[proj_0|feat] = 0.7\n", + " DIST: PRO 9 CA 8 - TYR 21 CA 20\n", + " feat nr. 111, atom idxs [ 8 20]\n", + " Corr[proj_0|feat] = 0.7\n", + " DIST: PRO 9 CA 8 - LEU 29 CA 28\n", + " feat nr. 115, atom idxs [ 8 28]\n", + "\n", + " Corr[proj_1|feat] = 0.7\n", + " DIST: PRO 9 CA 8 - LYS 15 CA 14\n", + " feat nr. 108, atom idxs [ 8 14]\n", + " Corr[proj_1|feat] = 0.7\n", + " DIST: LYS 15 CA 14 - TYR 23 CA 22\n", + " feat nr. 178, atom idxs [14 22]\n", + " Corr[proj_1|feat] = 0.6\n", + " DIST: LYS 15 CA 14 - PHE 33 CA 32\n", + " feat nr. 183, atom idxs [14 32]\n", + "\n", + " Corr[proj_2|feat] = 0.6\n", + " DIST: THR 11 CA 10 - GLY 37 CA 36\n", + " feat nr. 142, atom idxs [10 36]\n", + " Corr[proj_2|feat] = -0.6\n", + " DIST: PRO 13 CA 12 - GLN 31 CA 30\n", + " feat nr. 161, atom idxs [12 30]\n", + " Corr[proj_2|feat] = -0.6\n", + " DIST: PRO 13 CA 12 - PHE 33 CA 32\n", + " feat nr. 162, atom idxs [12 32]\n", + "\n", + "\n" + ] + } + ], + "source": [ + "print(corr)" + ] + }, { "cell_type": "markdown", "metadata": {}, "source": [ "Also, `molpx.visualize.traj` can help in visualizing these correlations by parsing along the tica object itself as `projection=tica`. In the next cell, can you spot the differences:\n", "* In the nglwidget?\n", - "* In the trajectories?" + "* In the trajectories?\n" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 14, "metadata": { "scrolled": false }, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "58feb0216a474eb693598a0ca927afad", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\r" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "9ad92a6e3d5d4282a19c6f93fc018679", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "22-05-18 17:24:51 pyemma.coordinates.clustering.kmeans.KmeansClustering[49] INFO Cluster centers converged after 9 steps.\n", + "\r" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "c056a0610c1e48a3a49c5eca88855169", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\r" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "59ebe80215da48b8ab9003655c871357", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "# Reuse the visualize.traj method with the tica object as input\n", "mpx_wdg_box = molpx.visualize.traj(MD_trajfiles, \n", @@ -418,9 +1025,46 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 15, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "cb734de2d8a44e2fa02c29faed3e6566", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "fd3f03575d824ee78a069e12801e72ea", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "22-05-18 17:25:14 pyemma.coordinates.clustering.kmeans.KmeansClustering[58] INFO Cluster centers converged after 9 steps.\n", + "\r" + ] + } + ], "source": [ "# Do \"some\" clustering\n", "clkmeans = pyemma.coordinates.cluster_kmeans([iY[:,:2] for iY in Y], 5)" @@ -428,9 +1072,52 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 16, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "be64e27733ff4c1cb59f9d990fe1b80b", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\r" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "8fa448a08c3e486682c81d82a8bb14f4", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\r" + ] + } + ], "source": [ "data_sample, geoms = molpx.generate.sample(MD_trajfiles, top, clkmeans, \n", " n_geom_samples=50, \n", @@ -440,9 +1127,42 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 17, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "cef422ddfd98495e89c158d8e749e997", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/guille/miniconda3/lib/python3.6/site-packages/ipykernel_launcher.py:6: RuntimeWarning: divide by zero encountered in log\n", + " \n" + ] + }, + { + "data": { + "text/plain": [ + "(NGLWidget(count=5), )" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# Plot clusters\n", "plt.figure(figsize=(4,4))\n", @@ -471,7 +1191,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 18, "metadata": { "collapsed": true }, @@ -482,9 +1202,46 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 19, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "b38ba2fd28ed4db89b43ced450cd0ad9", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/guille/miniconda3/lib/python3.6/site-packages/ipykernel_launcher.py:11: RuntimeWarning: divide by zero encountered in log\n", + " # This is added back by InteractiveShellApp.init_path()\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "eac2f4ab440f47c084b056778c26b362", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "plt.figure(figsize=(4,4))\n", "\n", @@ -516,9 +1273,41 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 20, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "e40f35977dda46efb5810f00ea25fe65", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\r" + ] + }, + { + "data": { + "text/plain": [ + "123" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "# Do an MSM with a realistic number of clustercenters\n", "cl_many = pyemma.coordinates.cluster_regspace([iY[:,:2] for iY in Y], dmin=.25)\n", @@ -528,11 +1317,31 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "776ae5e56ad04871906ee0cae34870fb", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\r" + ] + } + ], "source": [ "# Use this object to sample geometries\n", "pos, geom = molpx.generate.sample(MD_trajfiles, top, cl_many)" @@ -540,9 +1349,17 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 22, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[-0.18704125 -0.77366424] [ 6.71851349 0.03159955]\n" + ] + } + ], "source": [ "# Find the most representative microstate of each \n", "# and least populated macrostate\n", @@ -556,7 +1373,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 23, "metadata": { "collapsed": true }, @@ -569,7 +1386,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 24, "metadata": { "collapsed": true, "scrolled": true @@ -582,9 +1399,46 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 25, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "4db8e53777e14e689eddd81e4220f8bd", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/guille/miniconda3/lib/python3.6/site-packages/ipykernel_launcher.py:2: RuntimeWarning: divide by zero encountered in log\n", + " \n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "6a998bfad0dd40cfacddcd68327fe9be", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "plt.figure()\n", "plt.contourf(x[:-1], y[:-1], -np.log(h.T), cmap=\"jet\", alpha=.5, zorder=0)\n", diff --git a/molpx/notebooks/3.molPX_TICA_Ala2.ipynb b/molpx/notebooks/3.molPX_TICA_Ala2.ipynb index 13f3c27..9cafd2b 100644 --- a/molpx/notebooks/3.molPX_TICA_Ala2.ipynb +++ b/molpx/notebooks/3.molPX_TICA_Ala2.ipynb @@ -14,7 +14,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 1, "metadata": { "collapsed": true, "scrolled": true @@ -23,9 +23,7 @@ "source": [ "from os.path import exists\n", "import molpx\n", - "from matplotlib import pylab as plt\n", - "%matplotlib ipympl\n", - "\n", + "from matplotlib import pyplot as plt\n", "import pyemma\n", "import numpy as np" ] @@ -39,7 +37,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 2, "metadata": { "collapsed": true }, @@ -64,39 +62,11 @@ }, { "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "e81a14207be54f7faf3f685940a85863", - "version_major": 2, - "version_minor": 0 - }, - "text/html": [ - "

Failed to display Jupyter Widget of type Box.

\n", - "

\n", - " If you're reading this message in Jupyter Notebook or JupyterLab, it may mean\n", - " that the widgets JavaScript is still loading. If this message persists, it\n", - " likely means that the widgets JavaScript library is either not installed or\n", - " not enabled. See the Jupyter\n", - " Widgets Documentation for setup instructions.\n", - "

\n", - "

\n", - " If you're reading this message in another notebook frontend (for example, a static\n", - " rendering on GitHub or NBViewer),\n", - " it may mean that your frontend doesn't currently support widgets.\n", - "

\n" - ], - "text/plain": [ - "Box(children=(Text(value=''), IntProgress(value=0)))" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "execution_count": 3, + "metadata": { + "collapsed": true + }, + "outputs": [], "source": [ "feat = pyemma.coordinates.featurizer(top)\n", "feat.add_backbone_torsions()\n", @@ -114,7 +84,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 4, "metadata": { "scrolled": false }, @@ -122,27 +92,34 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "415deb3f4353423b87629d4233d2a651", + "model_id": "c0116accc78044a19e78dc903d7a6714", "version_major": 2, "version_minor": 0 }, - "text/html": [ - "

Failed to display Jupyter Widget of type MolPXHBox.

\n", - "

\n", - " If you're reading this message in Jupyter Notebook or JupyterLab, it may mean\n", - " that the widgets JavaScript is still loading. If this message persists, it\n", - " likely means that the widgets JavaScript library is either not installed or\n", - " not enabled. See the Jupyter\n", - " Widgets Documentation for setup instructions.\n", - "

\n", - "

\n", - " If you're reading this message in another notebook frontend (for example, a static\n", - " rendering on GitHub or NBViewer),\n", - " it may mean that your frontend doesn't currently support widgets.\n", - "

\n" - ], "text/plain": [ - "MolPXHBox(children=(NGLWidget(count=101), FigureCanvasNbAgg()))" + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "22-05-18 18:03:14 pyemma.coordinates.clustering.kmeans.KmeansClustering[1] INFO Cluster centers converged after 9 steps.\n", + "\r" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "01397594f745427b80e6fe228811a7b9", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" ] }, "metadata": {}, @@ -174,7 +151,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 5, "metadata": { "scrolled": false }, @@ -182,27 +159,34 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "f9ccc4547b6f4485b6151424a17497bc", + "model_id": "eab14edbbedd4e938b0b6bcaff4f8a39", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "22-05-18 17:57:00 pyemma.coordinates.clustering.kmeans.KmeansClustering[10] INFO Cluster centers converged after 8 steps.\n", + "\r" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "fd8492f500e3491d875b01377127d2a4", "version_major": 2, "version_minor": 0 }, - "text/html": [ - "

Failed to display Jupyter Widget of type MolPXVBox.

\n", - "

\n", - " If you're reading this message in Jupyter Notebook or JupyterLab, it may mean\n", - " that the widgets JavaScript is still loading. If this message persists, it\n", - " likely means that the widgets JavaScript library is either not installed or\n", - " not enabled. See the Jupyter\n", - " Widgets Documentation for setup instructions.\n", - "

\n", - "

\n", - " If you're reading this message in another notebook frontend (for example, a static\n", - " rendering on GitHub or NBViewer),\n", - " it may mean that your frontend doesn't currently support widgets.\n", - "

\n" - ], "text/plain": [ - "MolPXVBox(children=(MolPXHBox(children=(VBox(children=(Button(description='NGL widgets', layout=Layout(width='100%'), style=ButtonStyle()), VBox(children=(NGLWidget(count=3334),), layout=Layout(border='solid'))), layout=Layout(height='4.0in', width='5.0in')), FigureCanvasNbAgg())), MolPXHBox(children=(NGLWidget(count=101), FigureCanvasNbAgg()))))" + "A Jupyter Widget" ] }, "metadata": {}, @@ -236,11 +220,54 @@ }, { "cell_type": "code", - "execution_count": 10, - "metadata": { - "collapsed": true - }, - "outputs": [], + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "f0a39610ce0941eabac5fe0b7e476ae0", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "22-05-18 17:57:13 pyemma.coordinates.clustering.kmeans.KmeansClustering[19] INFO Cluster centers converged after 7 steps.\n", + "\r" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "ee3c7af803c84881aac36bf04b173caf", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "22-05-18 17:57:25 pyemma.coordinates.clustering.kmeans.KmeansClustering[24] INFO Cluster centers converged after 6 steps.\n", + "\r" + ] + } + ], "source": [ "paths_dict, idata = molpx.generate.projection_paths(MD_trajfiles, \n", " top, \n", @@ -255,7 +282,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 7, "metadata": { "collapsed": true }, @@ -275,7 +302,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 8, "metadata": { "scrolled": false }, @@ -291,27 +318,12 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "01623d70d8e844b1bff091673b855024", + "model_id": "fd40a5458fb34089a98b011394ddd2ae", "version_major": 2, "version_minor": 0 }, - "text/html": [ - "

Failed to display Jupyter Widget of type HBox.

\n", - "

\n", - " If you're reading this message in Jupyter Notebook or JupyterLab, it may mean\n", - " that the widgets JavaScript is still loading. If this message persists, it\n", - " likely means that the widgets JavaScript library is either not installed or\n", - " not enabled. See the Jupyter\n", - " Widgets Documentation for setup instructions.\n", - "

\n", - "

\n", - " If you're reading this message in another notebook frontend (for example, a static\n", - " rendering on GitHub or NBViewer),\n", - " it may mean that your frontend doesn't currently support widgets.\n", - "

\n" - ], "text/plain": [ - "HBox(children=(NGLWidget(count=45), FigureCanvasNbAgg()))" + "A Jupyter Widget" ] }, "metadata": {}, @@ -349,74 +361,16 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 9, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "01-11-17 21:01:44 pyemma.coordinates.data.featurization.featurizer.MDFeaturizer[7] WARNING The 1D arrays input for add_distances() have been sorted, and index duplicates have been eliminated.\n", + "22-05-18 17:57:50 pyemma.coordinates.data.featurization.featurizer.MDFeaturizer[30] WARNING The 1D arrays input for add_distances() have been sorted, and index duplicates have been eliminated.\n", "Check the output of describe() to see the actual order of the features\n" ] - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "159e3055ae0549a4a930aa5cc0c2e51f", - "version_major": 2, - "version_minor": 0 - }, - "text/html": [ - "

Failed to display Jupyter Widget of type Box.

\n", - "

\n", - " If you're reading this message in Jupyter Notebook or JupyterLab, it may mean\n", - " that the widgets JavaScript is still loading. If this message persists, it\n", - " likely means that the widgets JavaScript library is either not installed or\n", - " not enabled. See the Jupyter\n", - " Widgets Documentation for setup instructions.\n", - "

\n", - "

\n", - " If you're reading this message in another notebook frontend (for example, a static\n", - " rendering on GitHub or NBViewer),\n", - " it may mean that your frontend doesn't currently support widgets.\n", - "

\n" - ], - "text/plain": [ - "Box(children=(Text(value=''), IntProgress(value=0)))" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "eec03e0f5b7549a7aabf2bd8975d3ad9", - "version_major": 2, - "version_minor": 0 - }, - "text/html": [ - "

Failed to display Jupyter Widget of type Box.

\n", - "

\n", - " If you're reading this message in Jupyter Notebook or JupyterLab, it may mean\n", - " that the widgets JavaScript is still loading. If this message persists, it\n", - " likely means that the widgets JavaScript library is either not installed or\n", - " not enabled. See the Jupyter\n", - " Widgets Documentation for setup instructions.\n", - "

\n", - "

\n", - " If you're reading this message in another notebook frontend (for example, a static\n", - " rendering on GitHub or NBViewer),\n", - " it may mean that your frontend doesn't currently support widgets.\n", - "

\n" - ], - "text/plain": [ - "Box(children=(Text(value=''), IntProgress(value=0)))" - ] - }, - "metadata": {}, - "output_type": "display_data" } ], "source": [ @@ -430,33 +384,40 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 10, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "a025b63fc12e4543aa177685c415ebd0", + "model_id": "6aa5805b774f4d6581dccbf2a42aa208", "version_major": 2, "version_minor": 0 }, - "text/html": [ - "

Failed to display Jupyter Widget of type MolPXHBox.

\n", - "

\n", - " If you're reading this message in Jupyter Notebook or JupyterLab, it may mean\n", - " that the widgets JavaScript is still loading. If this message persists, it\n", - " likely means that the widgets JavaScript library is either not installed or\n", - " not enabled. See the Jupyter\n", - " Widgets Documentation for setup instructions.\n", - "

\n", - "

\n", - " If you're reading this message in another notebook frontend (for example, a static\n", - " rendering on GitHub or NBViewer),\n", - " it may mean that your frontend doesn't currently support widgets.\n", - "

\n" - ], "text/plain": [ - "MolPXHBox(children=(NGLWidget(count=103), FigureCanvasNbAgg()))" + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "22-05-18 17:57:54 pyemma.coordinates.clustering.kmeans.KmeansClustering[34] INFO Cluster centers converged after 10 steps.\n", + "\r" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "13dfdb4c0d834ba19fcf8f51b7a358d6", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" ] }, "metadata": {}, @@ -477,33 +438,40 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 11, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "03bb2243ab7843fe9a80deb64574cfe9", + "model_id": "7f7190815ded4204b5c686eff467746b", "version_major": 2, "version_minor": 0 }, - "text/html": [ - "

Failed to display Jupyter Widget of type MolPXVBox.

\n", - "

\n", - " If you're reading this message in Jupyter Notebook or JupyterLab, it may mean\n", - " that the widgets JavaScript is still loading. If this message persists, it\n", - " likely means that the widgets JavaScript library is either not installed or\n", - " not enabled. See the Jupyter\n", - " Widgets Documentation for setup instructions.\n", - "

\n", - "

\n", - " If you're reading this message in another notebook frontend (for example, a static\n", - " rendering on GitHub or NBViewer),\n", - " it may mean that your frontend doesn't currently support widgets.\n", - "

\n" - ], "text/plain": [ - "MolPXVBox(children=(MolPXHBox(children=(VBox(children=(Button(description='NGL widgets', layout=Layout(width='100%'), style=ButtonStyle()), VBox(children=(NGLWidget(count=3334),), layout=Layout(border='solid'))), layout=Layout(height='8.0in', width='5.0in')), FigureCanvasNbAgg())), MolPXHBox(children=(NGLWidget(count=103), FigureCanvasNbAgg()))))" + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "22-05-18 17:58:17 pyemma.coordinates.clustering.kmeans.KmeansClustering[43] INFO Cluster centers converged after 8 steps.\n", + "\r" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "4f755a7aa0cf4b69b633789b2c0d3bad", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" ] }, "metadata": {}, @@ -526,11 +494,54 @@ }, { "cell_type": "code", - "execution_count": 20, - "metadata": { - "collapsed": true - }, - "outputs": [], + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "05acfdfb30ed47a7ace60534c7a9384a", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "22-05-18 17:59:02 pyemma.coordinates.clustering.kmeans.KmeansClustering[65] INFO Cluster centers converged after 6 steps.\n", + "\r" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "298bcecb731d4c80b4c45149d005faf5", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "22-05-18 17:59:13 pyemma.coordinates.clustering.kmeans.KmeansClustering[74] INFO Cluster centers converged after 6 steps.\n", + "\r" + ] + } + ], "source": [ "paths_dict, idata = molpx.generate.projection_paths(MD_trajfiles, \n", " top, \n", @@ -545,7 +556,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 14, "metadata": { "collapsed": true }, @@ -565,33 +576,18 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 15, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "168cb4a131314925a023a303cf99ab35", + "model_id": "384eba18599541d3b5d0ae4f6b0bcbe9", "version_major": 2, "version_minor": 0 }, - "text/html": [ - "

Failed to display Jupyter Widget of type FigureCanvasNbAgg.

\n", - "

\n", - " If you're reading this message in Jupyter Notebook or JupyterLab, it may mean\n", - " that the widgets JavaScript is still loading. If this message persists, it\n", - " likely means that the widgets JavaScript library is either not installed or\n", - " not enabled. See the Jupyter\n", - " Widgets Documentation for setup instructions.\n", - "

\n", - "

\n", - " If you're reading this message in another notebook frontend (for example, a static\n", - " rendering on GitHub or NBViewer),\n", - " it may mean that your frontend doesn't currently support widgets.\n", - "

\n" - ], "text/plain": [ - "FigureCanvasNbAgg()" + "A Jupyter Widget" ] }, "metadata": {}, @@ -609,27 +605,12 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "ce80fff3cb8b419a946e33c58a31d4be", + "model_id": "db84a4cd262049f99fcf0968733740d3", "version_major": 2, "version_minor": 0 }, - "text/html": [ - "

Failed to display Jupyter Widget of type NGLWidget.

\n", - "

\n", - " If you're reading this message in Jupyter Notebook or JupyterLab, it may mean\n", - " that the widgets JavaScript is still loading. If this message persists, it\n", - " likely means that the widgets JavaScript library is either not installed or\n", - " not enabled. See the Jupyter\n", - " Widgets Documentation for setup instructions.\n", - "

\n", - "

\n", - " If you're reading this message in another notebook frontend (for example, a static\n", - " rendering on GitHub or NBViewer),\n", - " it may mean that your frontend doesn't currently support widgets.\n", - "

\n" - ], "text/plain": [ - "NGLWidget(count=45)" + "A Jupyter Widget" ] }, "metadata": {}, @@ -653,15 +634,6 @@ "linked_wdg.center_view()\n", "linked_wdg" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/molpx/notebooks/4.molPX_metadynamics_Di-Ala.ipynb b/molpx/notebooks/4.molPX_metadynamics_Di-Ala.ipynb index 629ce56..19919b8 100644 --- a/molpx/notebooks/4.molPX_metadynamics_Di-Ala.ipynb +++ b/molpx/notebooks/4.molPX_metadynamics_Di-Ala.ipynb @@ -26,7 +26,6 @@ "source": [ "import molpx\n", "import numpy as np\n", - "%matplotlib ipympl\n", "\n", "# Topology\n", "top = molpx._molpxdir(join='notebooks/data/ala2.pdb')\n", @@ -45,33 +44,40 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 2, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "660eb857cfe240619ced4a609a29f3e2", + "model_id": "678c2332efbd4dc88d97a458f5d1e56a", "version_major": 2, "version_minor": 0 }, - "text/html": [ - "

Failed to display Jupyter Widget of type MolPXHBox.

\n", - "

\n", - " If you're reading this message in Jupyter Notebook or JupyterLab, it may mean\n", - " that the widgets JavaScript is still loading. If this message persists, it\n", - " likely means that the widgets JavaScript library is either not installed or\n", - " not enabled. See the Jupyter\n", - " Widgets Documentation for setup instructions.\n", - "

\n", - "

\n", - " If you're reading this message in another notebook frontend (for example, a static\n", - " rendering on GitHub or NBViewer),\n", - " it may mean that your frontend doesn't currently support widgets.\n", - "

\n" - ], "text/plain": [ - "MolPXHBox(children=(NGLWidget(count=200), FigureCanvasNbAgg()))" + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "22-05-18 17:27:07 pyemma.coordinates.clustering.kmeans.KmeansClustering[0] INFO Cluster centers converged after 7 steps.\n", + "\r" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "315ea7c3c69541c4b225ccfb2298c0ea", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" ] }, "metadata": {}, @@ -100,7 +106,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 3, "metadata": { "scrolled": false }, @@ -108,27 +114,12 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "ec4524caaf9548889d167e1b7b477c5a", + "model_id": "c3cb6ef00f854d0bbe27b8125ff8e637", "version_major": 2, "version_minor": 0 }, - "text/html": [ - "

Failed to display Jupyter Widget of type MolPXHBox.

\n", - "

\n", - " If you're reading this message in Jupyter Notebook or JupyterLab, it may mean\n", - " that the widgets JavaScript is still loading. If this message persists, it\n", - " likely means that the widgets JavaScript library is either not installed or\n", - " not enabled. See the Jupyter\n", - " Widgets Documentation for setup instructions.\n", - "

\n", - "

\n", - " If you're reading this message in another notebook frontend (for example, a static\n", - " rendering on GitHub or NBViewer),\n", - " it may mean that your frontend doesn't currently support widgets.\n", - "

\n" - ], "text/plain": [ - "MolPXHBox(children=(VBox(children=(Button(description='NGL widgets', layout=Layout(width='100%'), style=ButtonStyle()), VBox(children=(NGLWidget(count=2001),), layout=Layout(border='solid')), VBox(children=(NGLWidget(count=2001),), layout=Layout(border='solid'))), layout=Layout(height='6.0in', width='5.0in')), FigureCanvasNbAgg()))" + "A Jupyter Widget" ] }, "metadata": {}, diff --git a/molpx/visualize.py b/molpx/visualize.py index b7999f4..ce23041 100644 --- a/molpx/visualize.py +++ b/molpx/visualize.py @@ -1127,7 +1127,7 @@ def contacts(contact_map, input, average=False, panelsize=4): :param residue_idxs: :return: """ - + from matplotlib import pyplot as _plt # Add one axis to the input if necessary if _np.ndim(contact_map)==2: contact_map = _np.array(contact_map, ndmin=3) From e6cfbbcc1ac27cdf83f9e71fb13dc69038a89dbd Mon Sep 17 00:00:00 2001 From: gph82 Date: Wed, 23 May 2018 16:43:12 +0200 Subject: [PATCH 55/73] [visualize] contacts() avoid using itertools for index generation, added optarg residue_indices=None) --- molpx/visualize.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/molpx/visualize.py b/molpx/visualize.py index ce23041..93467cb 100644 --- a/molpx/visualize.py +++ b/molpx/visualize.py @@ -21,8 +21,6 @@ import warnings as _warnings -from itertools import product as _it_prod - # All calls to nglview call actually this function def _nglwidget_wrapper(geom, ngl_wdg=None, n_small=10): r""" Wrapper to nlgivew.show_geom's method that allows for some other automatic choice of @@ -1119,7 +1117,7 @@ def _sample(positions, geoms, ax, return ngl_wdg, axes_wdg -def contacts(contact_map, input, average=False, panelsize=4): +def contacts(contact_map, input, residue_indices=None, average=False, panelsize=4): r""" Provide a contact map and a widget or geometry, return an interactive contact map @@ -1142,9 +1140,11 @@ def contacts(contact_map, input, average=False, panelsize=4): # Needed arrays nres = contact_map[0].shape[0] - residue_idxs = _np.arange(nres) - residue_pairs = _np.vstack(_it_prod(residue_idxs, residue_idxs)) - positions = _np.vstack(_it_prod(range(nres), range(nres))) + positions = _np.vstack(_np.unravel_index(range(nres**2), (nres,nres))).T + if residue_indices is None: + residue_pairs = positions + else: + raise NotImplementedError("This feature is not implemented yet!") # Create a color list cmap = _get_cmap('rainbow') @@ -1169,9 +1169,10 @@ def contacts(contact_map, input, average=False, panelsize=4): _plt.ion() - # Relabel the plot + # TODO: if residues is not None, # TODO make sure that zooming works even if a sub-set of res_idxs is given """ + # Relabel the plot for axtype in ['x', 'y']: tic_idxs = [int(tl) for tl in getattr(iax, 'get_%sticks'%axtype)()[1:-1]] tic_labels = ['']+['%u'%residue_idxs[ii] for ii in tic_idxs]+[''] From b4b681b17d2cdfc9f0d92c5e5760bf0e10224b23 Mon Sep 17 00:00:00 2001 From: gph82 Date: Wed, 23 May 2018 16:44:37 +0200 Subject: [PATCH 56/73] [_linkutils] ContactInNGLWidget refactor --- molpx/_linkutils.py | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/molpx/_linkutils.py b/molpx/_linkutils.py index 33af735..8cf6a61 100644 --- a/molpx/_linkutils.py +++ b/molpx/_linkutils.py @@ -47,8 +47,6 @@ def pts_per_axis_unit(mplax, pt_per_inch=72): inch_per_unit = span_inch / span_units return inch_per_unit * pt_per_inch - - def update2Dlines(iline, x, y): """ provide a common interface to update objects on the plot to a new position (x,y) depending @@ -81,7 +79,6 @@ def update2Dlines(iline, x, y): # TODO: FIND OUT WNY EXCEPTIONS ARE NOT BEING RAISED raise TypeError("what is this type of 2Dline?") - class ClickOnAxisListener(object): def __init__(self, ngl_wdg, crosshairs, showclick_objs, ax, pos, list_mpl_objects_to_update): @@ -308,7 +305,21 @@ class ContactInNGLWidget(object): def __init__(self, ngl_wdg, atom_indices, contact_index, component_to_draw_on=0, verbose=False, - color=None): + color=None, + sequential_residues=True): + r""" + + :param ngl_wdg: + :param atom_indices: iterable of len(2) with two integers (zero indexed atom indices) + The pair of atoms that are representative of this contact + When the method .show() of this class is called, a distance representation (=a line joining two atoms) + will be added to the :obj:`ngl_widget`. + :param contact_index: integer + The position of this contact in the contact list + :param component_to_draw_on: + :param verbose: + :param color: + """ assert len(atom_indices)==2, "ContactInNGLWidget takes a list with two elements as input, not len(%u)"%len(atom_indices) assert [isinstance(ii, int) for ii in atom_indices], "The atom indices have to be type int" @@ -316,7 +327,7 @@ def __init__(self, ngl_wdg, atom_indices, contact_index, self.ngl_wdg = ngl_wdg self.contact_index = contact_index self.verbose = verbose - self.top = self.ngl_wdg._trajlist[component_to_draw_on].trajectory + self.top = self.ngl_wdg._trajlist[component_to_draw_on].trajectory.top self.comp = component_to_draw_on self.shown = False self.color = color @@ -324,7 +335,8 @@ def __init__(self, ngl_wdg, atom_indices, contact_index, def show(self): if not self.shown: if self.verbose: - print("Showing %s "%[self.top.atom(ii).residue for ii in self.atom_indices]) + print("Showing contact %s via atoms %s"%(' '.join(['%s'%self.top.atom(ii).residue for ii in self.atom_indices]), + ' '.join(['%s' % self.top.atom(ii) for ii in self.atom_indices]))) self.shown = True add_atom_idxs_widget([self.atom_indices], self.ngl_wdg, color_list=[self.color]) @@ -341,8 +353,12 @@ def hide(self): def matching_repr_keys(self): # Given that the _ngl_repr_dict gets updated elsewhere, this is the most robust way of # finding this contact's representations - return [key for key, value in self.ngl_wdg._ngl_repr_dict[str(self.comp)].items() if value["type"] == "distance" + try: + res = [key for key, value in self.ngl_wdg._ngl_repr_dict[str(self.comp)].items() if value["type"] == "distance" and _np.allclose(_np.sort(value["params"]["atomPair"]), _np.sort(self.atom_indices))] + except KeyError: + res = [] + return res class GeometryInNGLWidget(object): r""" From c6a233e9a7727ef966302c54412e442036b1a730 Mon Sep 17 00:00:00 2001 From: gph82 Date: Wed, 23 May 2018 17:00:50 +0200 Subject: [PATCH 57/73] [tests] increase coverage --- molpx/tests/test_linkutils.py | 222 ++++++++++++++++++++++++++++++---- molpx/tests/test_molPX.py | 60 --------- molpx/tests/test_visualize.py | 46 +------ 3 files changed, 199 insertions(+), 129 deletions(-) delete mode 100644 molpx/tests/test_molPX.py diff --git a/molpx/tests/test_linkutils.py b/molpx/tests/test_linkutils.py index 2dd3f8c..6f48aed 100644 --- a/molpx/tests/test_linkutils.py +++ b/molpx/tests/test_linkutils.py @@ -7,7 +7,6 @@ from glob import glob from matplotlib.backend_bases import MouseEvent import mdtraj as md -import matplotlib.pyplot as plt #plt.switch_backend('Agg') # allow tests @@ -15,8 +14,6 @@ from numpy.testing import assert_raises from scipy.spatial import cKDTree as _cKDTree - - class TestLinkAxWPos2NGLWidget(unittest.TestCase): @classmethod @@ -32,35 +29,38 @@ def setUpClass(self): self.pos[:,1] = np.random.randn(self.n_sample) self.geom = self.MD_trajectories[0][:20] self.ngl_wdg = nglview.show_mdtraj(self.geom) + import matplotlib.pyplot as plt + self.plt = plt def test_just_works(self): - plt.figure() - __ = molpx._linkutils.link_ax_w_pos_2_nglwidget(plt.gca(), self.pos, self.ngl_wdg) + self.plt.figure() + __ = molpx._linkutils.link_ax_w_pos_2_nglwidget(self.plt.gca(), self.pos, self.ngl_wdg) def test_just_works_bandwidth(self): - plt.figure() - __ = molpx._linkutils.link_ax_w_pos_2_nglwidget(plt.gca(), self.pos, self.ngl_wdg, + self.plt.figure() + __ = molpx._linkutils.link_ax_w_pos_2_nglwidget(self.plt.gca(), self.pos, self.ngl_wdg, band_width=[0.1, .1]) def test_just_works_exclude_coord(self): - plt.figure() - __ = molpx._linkutils.link_ax_w_pos_2_nglwidget(plt.gca(), self.pos, self.ngl_wdg, + self.plt.figure() + __ = molpx._linkutils.link_ax_w_pos_2_nglwidget(self.plt.gca(), self.pos, self.ngl_wdg, exclude_coord=1) def test_force_exceptions(self): - plt.figure() - assert_raises(TypeError, molpx._linkutils.link_ax_w_pos_2_nglwidget, plt.gca(), self.pos, self.ngl_wdg, + self.plt.figure() + assert_raises(TypeError, molpx._linkutils.link_ax_w_pos_2_nglwidget, self.plt.gca(), self.pos, self.ngl_wdg, dot_color=2) - plt.figure() + self.plt.figure() pos_rnd = np.random.randn(self.n_sample, 2) - assert_raises(ValueError, molpx._linkutils.link_ax_w_pos_2_nglwidget, plt.gca(), pos_rnd, self.ngl_wdg, + assert_raises(ValueError, molpx._linkutils.link_ax_w_pos_2_nglwidget, self.plt.gca(), pos_rnd, self.ngl_wdg, band_width=[.1, .1]) def test_just_works_radius(self): - plt.figure() - __ = molpx._linkutils.link_ax_w_pos_2_nglwidget(plt.gca(), self.pos, self.ngl_wdg, + self.plt.figure() + __ = molpx._linkutils.link_ax_w_pos_2_nglwidget(self.plt.gca(), self.pos, self.ngl_wdg, band_width=[.1, .1], radius=True) + class TestGeometryInNGLWidget(unittest.TestCase): # TODO abstact this to a test class @@ -125,9 +125,91 @@ def test_show_and_hide_just_runs_and_changes_quickhands(self): # Now we hide many times, should arrive at the end without anything happening [giw.hide() for ii in range(10)] +class TestContactInNGLWidget(unittest.TestCase): + + # TODO abstact this to a test class + @classmethod + def setUpClass(self): + self.MD_trajectory_files = glob(molpx._molpxdir(join='notebooks/data/c-alpha_centered.stride.1000*xtc')) + self.MD_topology_file = molpx._molpxdir(join='notebooks/data/bpti-c-alpha_centered.pdb') + self.MD_topology = md.load(self.MD_topology_file).top + self.MD_trajectories = [md.load(ff, top=self.MD_topology_file) for ff in self.MD_trajectory_files] + + self.n_sample = 20 + self.pos = np.zeros((self.n_sample,2)) + self.pos[:,0] = np.linspace(0,1,self.n_sample) + self.pos[:,1] = np.random.randn(self.n_sample) + self.geom = self.MD_trajectories[0][:20] + + def test_just_works(self): + self.ngl_wdg = nglview.show_mdtraj(self.geom) + molpx._linkutils.ContactInNGLWidget(self.ngl_wdg, [0, 1], 0) + + def _test_right_atoms_are_represented(self): + self.ngl_wdg = nglview.show_mdtraj(self.geom) + ctcNwid = molpx._linkutils.ContactInNGLWidget(self.ngl_wdg, [0, 1], 0) + + # TODO cannot be tested since the representation is yet to be shown! + # we have to find this out! + #irepr = [value for value in self.ngl_wdg._ngl_repr_dict["0"].items() if value["type"] == "distance"] + #assert len(irepr)==1 + #assert np.allclose(np.sort(irepr["params"]["atomPair"]), [0,1]) + + def test_method_show_and_hide(self): + self.ngl_wdg = nglview.show_mdtraj(self.geom) + self.ngl_wdg.display() + ctcInwid = molpx._linkutils.ContactInNGLWidget(self.ngl_wdg, [0, 1], 0, verbose=True) + ctcInwid.show() + ctcInwid.hide() # Hide isn't really doing anything, since nothing is really shown + # TODO find out how to force the presentation on nglview from terminal + + def _test_quickhands(self): + giw = molpx._linkutils.GeometryInNGLWidget(self.geom, self.ngl_wdg) + assert giw.is_empty() + assert giw.have_repr == [] + assert ~giw.all_reps_are_on() + assert giw.all_reps_are_off() + assert ~giw.any_rep_is_on() + assert ~giw.is_visible() + + + def _test_show_just_runs_and_changes_quickhands(self): + giw = molpx._linkutils.GeometryInNGLWidget(self.geom, self.ngl_wdg) + giw.show() + assert ~giw.is_empty() + assert len(giw.have_repr) == 1 + assert giw.all_reps_are_on() + assert ~giw.all_reps_are_off() + assert giw.any_rep_is_on() + assert giw.is_visible() + + def _test_show_and_hide_just_runs_and_changes_quickhands(self): + # We initialize with two frames, it's easy to test the ends + giw = molpx._linkutils.GeometryInNGLWidget(self.geom[:2], self.ngl_wdg) + giw.show() + giw.hide() + assert ~giw.is_empty() + assert len(giw.have_repr) == 1 #should still be one + assert ~giw.all_reps_are_on() # but it´s off + assert giw.all_reps_are_off() + assert ~giw.any_rep_is_on() + assert ~giw.is_visible() + assert giw.any_rep_is_off() + # Now we show again + giw.show() # Turn on the one we had already, does not change the have_repr + assert len(giw.have_repr)==1 + giw.show() + assert len(giw.have_repr)==2 + # Turn on a new one, which isn't there nothing should happen + [giw.show() for ii in range(10)] + # Now we hide many times, should arrive at the end without anything happening + [giw.hide() for ii in range(10)] + + class TestUpdate2DLines(unittest.TestCase): def test_all(self): + import matplotlib.pyplot as plt plt.plot(0,0) line = plt.gca().lines[0] for ii, attr in enumerate(['lineh', 'linev', 'dot']): @@ -135,6 +217,7 @@ def test_all(self): molpx._linkutils.update2Dlines(line, ii, ii) def test_force_exceptions(self): + import matplotlib.pyplot as plt plt.plot(0, 0) line = plt.gca().lines[0] # This passes interactively in the console...? @@ -146,7 +229,6 @@ def test_force_exceptions(self): setattr(line, "whatisthis", "non_existing_line") assert_raises(TypeError, molpx._linkutils.update2Dlines, line,0,0) - class TestClickOnAxisListener(unittest.TestCase): @classmethod def setUpClass(self): @@ -158,11 +240,13 @@ def setUpClass(self): self.pos = np.random.rand(self.MD_trajectory.n_frames, 2) self.kdtree = _cKDTree(self.pos) + import matplotlib.pyplot as plt + self.plt = plt def just_runs(self, ngl_wdg, button=None): # Create the linked objects - plt.plot(self.pos[:,0], self.pos[:,1]) - iax = plt.gca() + self.plt.plot(self.pos[:,0], self.pos[:,1]) + iax = self.plt.gca() # Prepare a mouse event in the middle of the plot x, y = np.array(iax.get_window_extent()).mean(0) @@ -178,7 +262,7 @@ def just_runs(self, ngl_wdg, button=None): [lineh], iax, self.pos, [dot] - )(MouseEvent(" ", plt.gcf().canvas, x,y, + )(MouseEvent(" ", self.plt.gcf().canvas, x,y, button=button, key=None, step=0, dblclick=False, guiEvent=None)) @@ -188,13 +272,13 @@ def test_just_runs(self): def test_just_runs_sticky(self): # Create the linked objects (for sticky case, better use _sample - ngl_wdg, __ = molpx.visualize.sample(self.pos, self.MD_trajectory, plt.gca(), sticky=True) + ngl_wdg, __ = molpx.visualize.sample(self.pos, self.MD_trajectory, self.plt.gca(), sticky=True) self.just_runs(ngl_wdg, button=1) [self.just_runs(ngl_wdg, button=2) for ii in range(5)] def test_just_runs_recomputes_kdtree(self): - plt.plot(self.pos[:, 0], self.pos[:, 1]) - iax = plt.gca() + self.plt.plot(self.pos[:, 0], self.pos[:, 1]) + iax = self.plt.gca() # Prepare a mouse event in the middle of the plot x, y = np.array(iax.get_window_extent()).mean(0) @@ -217,6 +301,81 @@ def test_just_runs_recomputes_kdtree(self): dblclick=False, guiEvent=None)) + # Change axis lims to trigger recomputation + old_xlim = iax.get_xlim() + new_xmin = old_xlim[0]+abs(np.diff(iax.get_xlim())*.10) + iax.set_xlim(new_xmin, old_xlim[1]) + # Recompute the position of the new click + x, y = np.array(iax.get_window_extent()).mean(0) + # Send an event + CLAL(MouseEvent(" ", CLAL.ax.figure.canvas, x, y, + button=1, key=None, step=0, + dblclick=False, + guiEvent=None)) + + def test_click_w_contacts(self): + # Plot contact map + ctcs, idxs = md.compute_contacts(self.MD_trajectory) + ctcs = md.geometry.squareform(ctcs, idxs) + self.plt.imshow(ctcs[0]) + iax = self.plt.gca() + + # Create matching "positions" array + nres = ctcs.shape[-1] + positions = np.vstack(np.unravel_index(range(nres ** 2), (nres, nres))).T + + # Create widget and monkey-patch its _CtcsInWid attribute + ngl_wdg = nglview.show_mdtraj(self.MD_trajectory) + ngl_wdg._CtcsInWid = [molpx._linkutils.ContactInNGLWidget(ngl_wdg, [0, 1], 0)] + + # Create the CLA object linking the wid and the axis via "positions" + CLAL = molpx._linkutils.ClickOnAxisListener(ngl_wdg, True, + [], + iax, positions, + [] + ) + + # Get the left, uppermost pixel of the image (matplotlib voodoo, + # see https://matplotlib.org/users/transforms_tutorial.html + x, y = iax.get_window_extent().x0, iax.get_window_extent().y1 + + # Before the click, this should just pass + CLAL.remove_last_contacts() + + # Now we instantiate a mouseclick on the first contact (0,1) + ME = MouseEvent(" ", CLAL.ax.figure.canvas, x, y, + button=1, key=None, step=0, + dblclick=False, + guiEvent=None) + + # Make sure the simulated click was actually on the canvas + assert CLAL.ax.get_window_extent().contains(ME.x, ME.y) + + # Send the mouseclick to the CLAL + CLAL(ME) + + # Assert that a rectangle was created + assert CLAL.list_of_rects[0] is not None, CLAL.list_of_rects + + # This should remove that rectangle and the contacts + CLAL.remove_last_contacts() + # Check that "remove" worked: + assert ~ngl_wdg._CtcsInWid[0].shown + + # Now click again one on left and one on right click: + ME = MouseEvent(" ", CLAL.ax.figure.canvas, x, y, + button=1, key=None, step=0, + dblclick=False, + guiEvent=None) + ME_right = MouseEvent(" ", CLAL.ax.figure.canvas, x, y, + button=2, key=None, step=0, + dblclick=False, + guiEvent=None) + CLAL(ME) + CLAL(ME_right) + + + class TestChangeInNGLWidgetListener(unittest.TestCase): @classmethod def setUpClass(self): @@ -227,8 +386,10 @@ def setUpClass(self): self.MD_trajectory = self.MD_trajectories[0] self.pos = np.random.rand(self.MD_trajectory.n_frames, 2) - plt.plot(self.pos[:,0], self.pos[:,1]) - iax = plt.gca() + from matplotlib import pyplot as plt + self.plt = plt + self.plt.plot(self.pos[:,0], self.pos[:,1]) + iax = self.plt.gca() # Prepare event self.lineh = iax.axhline(iax.get_ybound()[0]) setattr(self.lineh, 'whatisthis', 'lineh') @@ -242,5 +403,18 @@ def test_just_runs_past_last_frame(self): molpx._linkutils.ChangeInNGLWidgetListener(self.ngl_wdg, [self.lineh, self.dot], self.pos)({"new":self.pos.shape[0]+1, "old":1}) + def test_update_contact_map(self): + from matplotlib import pyplot as _plt + contact_map = md.geometry.squareform(*md.compute_contacts(self.MD_trajectories[0])) + _plt.figure() + iax = _plt.gca() + self.ngl_wdg._MatshowData = {"image" : iax.matshow(contact_map[0]), + "data" : contact_map} + CINL = molpx._linkutils.ChangeInNGLWidgetListener(self.ngl_wdg, [], None) + # Run trough all available contact maps + for ii in range(len(contact_map)): + CINL({"new":ii}) + + if __name__ == '__main__': unittest.main() diff --git a/molpx/tests/test_molPX.py b/molpx/tests/test_molPX.py deleted file mode 100644 index 94f8981..0000000 --- a/molpx/tests/test_molPX.py +++ /dev/null @@ -1,60 +0,0 @@ -__author__ = 'gph82' - -import unittest -import pyemma -import os -import tempfile -import numpy as np -import shutil -import molpx -from matplotlib import pyplot as plt -#plt.switch_backend('Agg') # allow tests - - -class MyTestCase(unittest.TestCase): - - def setUp(self): - self.MD_trajectory = os.path.join(pyemma.__path__[0],'coordinates/tests/data/bpti_mini.xtc') - self.topology = os.path.join(pyemma.__path__[0],'coordinates/tests/data/bpti_ca.pdb') - self.tempdir = tempfile.mkdtemp('test_molpx') - self.projected_file = os.path.join(self.tempdir,'Y.npy') - feat = pyemma.coordinates.featurizer(self.topology) - feat.add_selection(np.arange(3)) - source = pyemma.coordinates.source(self.MD_trajectory, features=feat) - self.tica = pyemma.coordinates.tica(source,lag=1, dim=2) - Y = self.tica.get_output()[0] - print(self.tempdir) - np.save(self.projected_file,Y) - - def tearDown(self): - shutil.rmtree(self.tempdir) - - def test_generate_paths(self): - molpx.generate.projection_paths(self.MD_trajectory, self.topology, self.projected_file) - - def test_generate_sample(self): - molpx.generate.sample(self.MD_trajectory, self.topology, self.projected_file) - - def test_generate_sample_atom_selections(self): - molpx.generate.sample(self.MD_trajectory, self.topology, self.projected_file, atom_selection='symbol != H') - - __, geom_smpl = molpx.generate.sample(self.MD_trajectory, self.topology, self.projected_file, atom_selection=np.array([2,4,6,8])) - assert geom_smpl[0].n_atoms == 4 - - # Cannot get the widget to run outside the notebook because it needs an interact bar - def test_visualize_qsample(self): - pos, geom_smpl = molpx.generate.sample(self.MD_trajectory, self.topology, self.projected_file) - plt.figure() - __ = molpx.visualize.sample(pos, geom_smpl, plt.gca()) - - def test_visualize_path_w_tica(self): - paths_dict, idata = molpx.generate.projection_paths(self.MD_trajectory, self.topology, self.projected_file) - plt.figure() - path_type = 'min_disp' - igeom = paths_dict[0][path_type]["geom"] - ipath = paths_dict[0][path_type]["proj"] - __ = molpx.visualize.sample(ipath, igeom, plt.gca(), projection=self.tica) - - -if __name__ == '__main__': - unittest.main() diff --git a/molpx/tests/test_visualize.py b/molpx/tests/test_visualize.py index b35575c..1a6b987 100644 --- a/molpx/tests/test_visualize.py +++ b/molpx/tests/test_visualize.py @@ -208,34 +208,6 @@ def test_sample_sticky_just_works_list_geom_small_molecule(self): color_list=['r', 'b', 'g', 'magenta'], sticky=True) -class TestFES(TestWithBPTIData): - - @classmethod - def setUpClass(self): - TestWithBPTIData.setUpClass() - - def test_just_works_min_input_disk(self): - molpx.visualize.FES(self.MD_trajectory_files, - self.MD_topology_file, - self.projected_files_npy) - - def test_just_works_min_input_memory(self): - molpx.visualize.FES(self.MD_trajectories, - self.MD_topology, - self.Ys) - - def test_overlays(self): - molpx.visualize.FES(self.MD_trajectories, - self.MD_topology, - self.Ys, - n_overlays=5) - - def test_1D(self): - molpx.visualize.FES(self.MD_trajectories, - self.MD_topology, - self.Ys, - proj_idxs=[0]) - class TestCorrelations(TestWithBPTIData): @classmethod @@ -298,7 +270,7 @@ def test_feature_color_list(self): except TypeError: pass -class TestContacts(TestWithBPTIData): +class Contacts(TestWithBPTIData): @classmethod def setUpClass(self): TestWithBPTIData.setUpClass() @@ -318,22 +290,6 @@ def test_one_ctcframe(self): # This should pass visualize.contacts(self.ctcs.mean(0), self.geom, average=True) -class TestNGLWidgetWrapper(unittest.TestCase): - - def setUp(self): - self.MD_file = molpx._molpxdir(join='notebooks/data/bpti-c-alpha_centered.pdb') - self.MD_geom = md.load(self.MD_file) - - def test_widget_wrapper_w_None(self): - iwd = molpx.visualize._nglwidget_wrapper(None) - - def test_widget_wrapper_w_file(self): - iwd = molpx.visualize._nglwidget_wrapper(self.MD_file) - - def test_widget_wrapper_w_instantiated_wdg(self): - iwd = molpx.visualize._nglwidget_wrapper(self.MD_file) - molpx.visualize._nglwidget_wrapper(self.MD_geom, ngl_wdg=iwd) - class TestBoxMe(unittest.TestCase): def test_just_runs_and_exits_gracefully(self): From 432f3ad7fc1681135551621c4ad2776e61a7121e Mon Sep 17 00:00:00 2001 From: gph82 Date: Wed, 23 May 2018 17:57:36 +0200 Subject: [PATCH 58/73] [meta.yml] add nose to the reqs --- devtools/conda-recipe/meta.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/devtools/conda-recipe/meta.yaml b/devtools/conda-recipe/meta.yaml index a627369..7fd8b95 100644 --- a/devtools/conda-recipe/meta.yaml +++ b/devtools/conda-recipe/meta.yaml @@ -27,6 +27,7 @@ test: requires: - pytest-cov - nbval + - nose >= 1 imports: - molpx commands: From 197e09b791fae5a7ac4fd9d31fbef9a75b87f297 Mon Sep 17 00:00:00 2001 From: gph82 Date: Wed, 23 May 2018 18:16:04 +0200 Subject: [PATCH 59/73] [test_visualize] remove the evil Agg backend --- molpx/tests/test_visualize.py | 1 - 1 file changed, 1 deletion(-) diff --git a/molpx/tests/test_visualize.py b/molpx/tests/test_visualize.py index 1a6b987..721d01c 100644 --- a/molpx/tests/test_visualize.py +++ b/molpx/tests/test_visualize.py @@ -10,7 +10,6 @@ import matplotlib.pyplot as plt import nglview from pyemma.coordinates import tica -plt.switch_backend('Agg') # allow tests from .test_bmutils import TestWithBPTIData From 6e78d0c40f63402f0f3c90aadf69c7b27a020004 Mon Sep 17 00:00:00 2001 From: "Martin K. Scherer" Date: Wed, 23 May 2018 22:28:23 +0200 Subject: [PATCH 60/73] agg f --- molpx/tests/test_generate.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/molpx/tests/test_generate.py b/molpx/tests/test_generate.py index 4a9d1d7..5313bc2 100644 --- a/molpx/tests/test_generate.py +++ b/molpx/tests/test_generate.py @@ -5,12 +5,8 @@ import numpy as np import molpx from matplotlib import pyplot as plt -plt.switch_backend('Agg') # allow tests from .test_bmutils import TestWithBPTIData -class MyVersion(unittest.TestCase): - import molpx - molpx.__version__ class TestSample(TestWithBPTIData): From f27c596d09f939a04bd602b3ee52bf1ca78a13f7 Mon Sep 17 00:00:00 2001 From: gph82 Date: Fri, 25 May 2018 10:55:32 +0200 Subject: [PATCH 61/73] [_bmutills] interval_schachtelung new optarg maxiter to avoid infinite loops --- molpx/_bmutils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/molpx/_bmutils.py b/molpx/_bmutils.py index 8a51fc9..6afb0d3 100644 --- a/molpx/_bmutils.py +++ b/molpx/_bmutils.py @@ -251,7 +251,7 @@ def regspace_cluster_to_target_kmeans(data, n_clusters_target, return cl -def interval_schachtelung(f, interval, target=0, eps=1, verbose=False): +def interval_schachtelung(f, interval, target=0, eps=1, maxiter=1000, verbose=False): r""" Find the value m s.t. f(m) = target +- eps using interval optimization @@ -281,7 +281,7 @@ def inform(left, fl, right, fr, middle, fm, delta, eps, str0=''): inform(left, fl, right, fr, middle, fm, delta, eps, str0='Init:') cc = 0 - while delta >= eps: + while delta >= eps and cc Date: Fri, 25 May 2018 10:55:55 +0200 Subject: [PATCH 62/73] [tests] remove useless import --- molpx/tests/test_visualize.py | 1 - 1 file changed, 1 deletion(-) diff --git a/molpx/tests/test_visualize.py b/molpx/tests/test_visualize.py index 721d01c..7c82597 100644 --- a/molpx/tests/test_visualize.py +++ b/molpx/tests/test_visualize.py @@ -9,7 +9,6 @@ import mdtraj as md import matplotlib.pyplot as plt import nglview -from pyemma.coordinates import tica from .test_bmutils import TestWithBPTIData From 1e16f067afbd21d46bc7c84cea2f878d597d915f Mon Sep 17 00:00:00 2001 From: gph82 Date: Fri, 25 May 2018 12:28:30 +0200 Subject: [PATCH 63/73] [docs] documented visualize.contacts() --- doc/source/conf.py | 3 +++ doc/source/index_visualize.rst | 2 +- molpx/visualize.py | 39 +++++++++++++++++++++++++++++----- 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index ecf06b5..290d62c 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -49,14 +49,17 @@ def __getattribute__(self, item): 'pyemma.coordinates.data.featurization.distances', 'nglview', 'matplotlib', + 'matplotlib.pyplot', 'matplotlib.widgets', 'matplotlib.figure', 'matplotlib.axes', 'matplotlib.cm', + 'matplotlib.patches', 'matplotlib.colors', 'IPython.display', 'sklearn.mixture', 'scipy.spatial', + 'scipy.spatial.distance', 'six.moves.urllib.request', 'numpy', # fixme: can not mock ipywidgets because of multi-inheritance within molpx diff --git a/doc/source/index_visualize.rst b/doc/source/index_visualize.rst index 3ee71a4..d50dd03 100644 --- a/doc/source/index_visualize.rst +++ b/doc/source/index_visualize.rst @@ -20,8 +20,8 @@ The methods offered by this module are: .. autosummary:: molpx.visualize.FES - molpx.visualize.sample molpx.visualize.traj + molpx.visualize.sample molpx.visualize.correlations molpx.visualize.feature diff --git a/molpx/visualize.py b/molpx/visualize.py index 93467cb..cfbb1f8 100644 --- a/molpx/visualize.py +++ b/molpx/visualize.py @@ -708,7 +708,8 @@ def correlations(correlation_input, corr_dict and ngl_wdg corr_dict : - Dictionary with items: + Dictionary containing correlation information. For an overview, just issue `print(corr_dict)`. The + values are stored under the following keys. idxs : List of length len(proj_idxs) with lists of length n_feat with the idxs of the most correlated features @@ -1119,12 +1120,40 @@ def _sample(positions, geoms, ax, def contacts(contact_map, input, residue_indices=None, average=False, panelsize=4): r""" - Provide a contact map and a widget or geometry, return an interactive contact map + Return a plot of the contact map and a linked :obj:`nglview.NGLWidget`. Clicking on + a pixel of interest on the contact map will a) highlight that pixel and b) + add lines in the widget ,connecting the corresponding atoms. Also, any updates in + the widget's frame, via the sliding bar, will update the shown contact map (in + case more than one contact map was provided) - :param contact_map: - :param residue_idxs: - :return: + Parameters + ---------- + contact_map : square nd.array or iterable thereof. + These square arrays contain the contact map(s) + + input : :obj:`mdtraj.Trajectory` object or a list thereof. + An :obj:`nglview.NGLWidget` will be instantiated with this input + + residue_indices : boolean or iterable of integers + Residue indices corresponding to the :obj:`contact_map`. If None, an array + (0,1,...n_residues) will be created. + # TODO if not None, a NotImplementedError will be raised, because the relabeling of + zoomable plots is not yet implemented by molpx) + + average : boolean, default is False + Plot only the average of the contact maps provided in :obj:`contact_map`. If only one + such map is given, this keyword has no effect. If average is false but the + number of frames in :obj:`input` and :obj:`contact_map` don match, an exception is thrown. + + panelsize : int, default is 4 + The size of the figure and widget that will be outputted inside the molpxbox + + Returns + -------- + + mpxbox : An :obj:`nglview.NGLWidget` """ + from matplotlib import pyplot as _plt # Add one axis to the input if necessary if _np.ndim(contact_map)==2: From 3855e37af3ff69d3d331e7526459d7dc38f0c647 Mon Sep 17 00:00:00 2001 From: gph82 Date: Fri, 25 May 2018 12:30:39 +0200 Subject: [PATCH 64/73] [tests] added the duration flag --- devtools/conda-recipe/meta.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devtools/conda-recipe/meta.yaml b/devtools/conda-recipe/meta.yaml index 7fd8b95..446e1d5 100644 --- a/devtools/conda-recipe/meta.yaml +++ b/devtools/conda-recipe/meta.yaml @@ -31,7 +31,7 @@ test: imports: - molpx commands: - - pytest -vv --pyargs molpx --cov=molpx --cov-report=xml --current-env --nbval-lax + - pytest -vv --pyargs molpx --cov=molpx --cov-report=xml --current-env --nbval-lax --duration=10 - cp coverage.xml /tmp # [linux] about: From bb019216e64cb168766c59a5723c9c514b5f0e12 Mon Sep 17 00:00:00 2001 From: gph82 Date: Mon, 28 May 2018 17:59:04 +0200 Subject: [PATCH 65/73] [notebooks] updated and included visualize.MSM method --- .../notebooks/0.molPX_quick_intro_Ala2.ipynb | 39 +- .../1.molPX_and_PyEMMA_Features.ipynb | 2 +- .../2.molPX_TICA_and_MSMs_BPTI.ipynb | 1568 +++++++ molpx/notebooks/3.molPX_TICA_Ala2.ipynb | 2 +- .../5.molPX_GPCR_Opsin_contacts.ipynb | 3586 +++++++++++++++++ 5 files changed, 5170 insertions(+), 27 deletions(-) create mode 100644 molpx/notebooks/2.molPX_TICA_and_MSMs_BPTI.ipynb create mode 100644 molpx/notebooks/5.molPX_GPCR_Opsin_contacts.ipynb diff --git a/molpx/notebooks/0.molPX_quick_intro_Ala2.ipynb b/molpx/notebooks/0.molPX_quick_intro_Ala2.ipynb index b2ff8bc..bfe1ae5 100644 --- a/molpx/notebooks/0.molPX_quick_intro_Ala2.ipynb +++ b/molpx/notebooks/0.molPX_quick_intro_Ala2.ipynb @@ -16,7 +16,6 @@ "cell_type": "code", "execution_count": 1, "metadata": { - "collapsed": true, "scrolled": true }, "outputs": [], @@ -34,9 +33,7 @@ { "cell_type": "code", "execution_count": 2, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "top = molpx._molpxdir(join='notebooks/data/ala2.pdb')\n", @@ -69,7 +66,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "2009faf662cf4e72b01d505a61808acc", + "model_id": "617716dc19d74f83aa878ea096b78ea3", "version_major": 2, "version_minor": 0 }, @@ -84,14 +81,14 @@ "name": "stdout", "output_type": "stream", "text": [ - "22-05-18 17:13:44 pyemma.coordinates.clustering.kmeans.KmeansClustering[0] INFO Cluster centers converged after 7 steps.\n", + "25-05-18 14:00:15 pyemma.coordinates.clustering.kmeans.KmeansClustering[0] INFO Cluster centers converged after 5 steps.\n", "\r" ] }, { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "c3c35ff618c14cc2aa570978020fa2ef", + "model_id": "d18c35c79bbb43a28b230975961aa179", "version_major": 2, "version_minor": 0 }, @@ -136,7 +133,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "55360d96b6384bb7929baaeba0cb188e", + "model_id": "564bc9bc5ef841038a73725c9978b7dc", "version_major": 2, "version_minor": 0 }, @@ -151,14 +148,14 @@ "name": "stdout", "output_type": "stream", "text": [ - "22-05-18 17:14:02 pyemma.coordinates.clustering.kmeans.KmeansClustering[5] INFO Cluster centers converged after 9 steps.\n", + "25-05-18 14:00:21 pyemma.coordinates.clustering.kmeans.KmeansClustering[9] INFO Cluster centers converged after 7 steps.\n", "\r" ] }, { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "6d97b7e9395143769da9fc812ca7bf99", + "model_id": "1a8b49758df04e2dbc50606d7fea0b7e", "version_major": 2, "version_minor": 0 }, @@ -171,12 +168,6 @@ } ], "source": [ - "from molpx import visualize, _linkutils\n", - "from imp import reload\n", - "reload(visualize)\n", - "reload(_linkutils)\n", - "from matplotlib import pyplot as plt\n", - "plt.close('all')\n", "mpx_wdg_box = molpx.visualize.traj(MD_trajfiles, \n", " top, \n", " rama_files,\n", @@ -198,9 +189,7 @@ { "cell_type": "code", "execution_count": 5, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", @@ -217,7 +206,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "324992cdd62a4b3ea8c8d6fd0daf7369", + "model_id": "875dcdec40b246e984881c17785b16f4", "version_major": 2, "version_minor": 0 }, @@ -232,14 +221,14 @@ "name": "stdout", "output_type": "stream", "text": [ - "22-05-18 17:14:20 pyemma.coordinates.clustering.kmeans.KmeansClustering[14] INFO Cluster centers converged after 6 steps.\n", + "25-05-18 14:00:31 pyemma.coordinates.clustering.kmeans.KmeansClustering[18] INFO Cluster centers converged after 6 steps.\n", "\r" ] }, { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "3638e8baa9244c25af118111a71e6df3", + "model_id": "46d4a5d0d9e647cb9838735556fae885", "version_major": 2, "version_minor": 0 }, @@ -272,12 +261,12 @@ "metadata": {}, "source": [ "# The ```mpx_wdg_box```\n", - "It is a class derived from the ipython widgets HBox and VBox, with molPX's extra information as attributes starting with \"linked_*\" " + "It is a class derived from the ipython widgets [HBox](https://ipywidgets.readthedocs.io/en/latest/examples/Widget%20List.html?highlight=Hbox#Container/Layout-widgets) and [VBox](https://ipywidgets.readthedocs.io/en/latest/examples/Widget%20List.html?highlight=Hbox#Container/Layout-widgets), with molPX's extra information as attributes starting with `linked_*` " ] }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 8, "metadata": {}, "outputs": [ { @@ -317,7 +306,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.1" + "version": "3.6.2" }, "widgets": { "application/vnd.jupyter.widget-state+json": { diff --git a/molpx/notebooks/1.molPX_and_PyEMMA_Features.ipynb b/molpx/notebooks/1.molPX_and_PyEMMA_Features.ipynb index 116613a..bd50047 100644 --- a/molpx/notebooks/1.molPX_and_PyEMMA_Features.ipynb +++ b/molpx/notebooks/1.molPX_and_PyEMMA_Features.ipynb @@ -495,7 +495,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.1" + "version": "3.6.2" }, "widgets": { "state": { diff --git a/molpx/notebooks/2.molPX_TICA_and_MSMs_BPTI.ipynb b/molpx/notebooks/2.molPX_TICA_and_MSMs_BPTI.ipynb new file mode 100644 index 0000000..e619b64 --- /dev/null +++ b/molpx/notebooks/2.molPX_TICA_and_MSMs_BPTI.ipynb @@ -0,0 +1,1568 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# molPX intro\n", + "
 \n",
+    "Guillermo Perez-Hernandez  guille.perez@fu-berlin.de \n",
+    "
\n", + " \n", + "In this notebook we will be using the 1 millisecond trajectory of Bovine Pancreatic Trypsin Inhibitor (BPTI) generated by DE Shaw Research on the Anton Supercomputer and kindly made available by their lab. The original work is \n", + " \n", + " * Shaw DE, Maragakis P, Lindorff-Larsen K, Piana S, Dror RO, Eastwood MP, Bank JA, Jumper JM, Salmon JK, Shan Y, Wriggers W: Atomic-level characterization of the structural dynamics of proteins. Science 330:341-346 (2010). doi: 10.1126/science.1187409.\n", + " \n", + "The trajectory has been duplicated and shortened to provide a mock-trajectory set and be able to deal with lists of trajectories of different lenghts:\n", + "\n", + " * `c-alpha_centered.stride.100.xtc`\n", + " * `c-alpha_centered.stride.100.reversed.xtc`\n", + " * `c-alpha_centered.stride.100.halved.xtc`" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Input types and typical usecase\n", + "The typical usecase is having molecular dynamics (MD) simulation data in form of trajectory files with extensions like `.xtc, .dcd` etc and the associated molecular topology as a `.pdb` or `.gro` file. \n", + "\n", + "These files are the most general starting point for any analysis dealing with MD, and ```molpx```'s API has been designed to be able to function without further input:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "top = 'notebooks/data/bpti-c-alpha_centered.pdb'\n", + "MD_trajfiles = ['notebooks/data/c-alpha_centered.stride.1000.xtc',\n", + " 'notebooks/data/c-alpha_centered.stride.1000.reversed.xtc',\n", + " 'notebooks/data/c-alpha_centered.stride.1000.halved.xtc'\n", + " ]\n", + "\n", + "dt = 244 #saving interval in the .xtc files, in ns\n", + "\n", + "import molpx\n", + "from matplotlib import pyplot as plt\n", + "import pyemma\n", + "import numpy as np\n", + "\n", + "# This way the user does not have to care where the data are:\n", + "top = molpx._molpxdir(top)\n", + "MD_trajfiles = [molpx._molpxdir(ff) for ff in MD_trajfiles]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**However**, `molpx` relies heavily on the awesome [`mdtraj`](http://www.mdtraj.org) module for dealing with molecular structures, and so most of `molpx`'s functions accept also `Trajectory`-type objects (native to `mdtraj`) as alternative inputs. " + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "# Create a memory representation of the trajectories\n", + "MD_list = [molpx.generate._md.load(itraj, top=top) for itraj in MD_trajfiles]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The same idea applies to the input of projected trajectories: `molpx` can take the filenames as inputs (`.npy`, `.dat`, `.txt` etc) or deal directly with `numpy.ndarray` objects. \n", + "\n", + "** These alternative, \"from-memory\" input modes (`md.Trajectory` and `np.ndarray` objects) avoid forcing the user to read from file everytime an API function is called, saving I/O overhead**\n", + "\n", + "The following cell either reads or generates projected trajectory files for this demonstration. In a real usecase this step (done here using TICA) might not be needed, given that the user might have generated the projected trajectory elsewhere:" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [], + "source": [ + "# Perform TICA or read from file directly if already .npy-files exist\n", + "Y_filenames = [ff.replace('.xtc','.Y.npy') for ff in MD_trajfiles]\n", + "try: \n", + " Y = [np.load(ff) for ff in Y_filenames]\n", + "except:\n", + " feat = pyemma.coordinates.featurizer(top)\n", + " pairs = feat.pairs(range(feat.topology.n_atoms)[::2])\n", + " feat.add_distances(pairs)\n", + " src = pyemma.coordinates.source(MD_trajfiles, features=feat)\n", + " tica = pyemma.coordinates.tica(src, lag=10, dim=3)\n", + " Y = tica.get_output() \n", + " [np.save(ff, iY) for ff, iY in zip(Y_filenames, Y)]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Visualize a FES and the molecular structures behind it\n", + "Execute the following cell and click either on the FES or on the slidebar. Some input parameters have been comented out for you to try out:\n", + " * different modes of input (disk vs memory) \n", + " * different projection indices\n", + " * different number of overlaid structures" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "scrolled": false + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "8de7941a9dc848cca4816700540c5293", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "25-05-18 17:54:32 pyemma.coordinates.clustering.kmeans.KmeansClustering[0] INFO Algorithm did not reach convergence criterion of 1e-05 in 10 iterations. Consider increasing max_iter.\n", + "\r" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "afe77d5e19474700b1b08e38f40155ed", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\r" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "b5a7b7bf4ec446bc9ecef99990e344fc", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "mpx_wdg_box = molpx.visualize.FES(MD_list, \n", + " #MD_trajfiles, \n", + " top, \n", + " Y_filenames, \n", + " #Y, \n", + " nbins=50, \n", + " #proj_idxs=[1,2],\n", + " proj_labels='TIC',\n", + " #n_overlays=5,\n", + " )\n", + "mpx_wdg_box" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Visualize trajectories, FES and molecular structures\n", + "The user can sample structures as they occurr in sequence in the actual trajectory. Depending on the size of the dataset, this can be very time consuming, particularly if data is being read from disk. \n", + "\n", + "In this example, try changing `MD_trajfiles` to `MD_list` and/or changing `Y_filenames` to simply `Y` and see if it helps.\n", + "\n", + "Furthermore, the objects in memory can be strided down to fewer frames **before** being parsed to the method. To stride objects being read from the disk, use the `stride` parameter. \n", + "\n", + "### Again, we have commented out some parameters that provide more control on the output of `visualize.traj`. Uncomment them and see what happens" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "scrolled": false + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "e03fa3137349456d9181e2796d5f1508", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\r" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "a12638ba59a4496da3cd83d52601e1ed", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "25-05-18 16:19:16 pyemma.coordinates.clustering.kmeans.KmeansClustering[8] INFO Algorithm did not reach convergence criterion of 1e-05 in 10 iterations. Consider increasing max_iter.\n", + "\r" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "cccf392ea5b7431983e4381b112de72f", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\r" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "5f8909f44df042ef8a437fc61c445464", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "mpx_wdg_box = molpx.visualize.traj(MD_trajfiles, \n", + " top, \n", + " Y,\n", + " #Y_filenames, \n", + " plot_FES = True, \n", + " dt = dt*1e-6, tunits='ms', \n", + " #traj_selection = 1,\n", + " #sharey_traj=False,\n", + " #max_frames=100,\n", + " proj_idxs=[0, 1],\n", + " panel_height=2, \n", + " #proj_labels='TIC'\n", + " )\n", + "mpx_wdg_box" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Intermediate steps: using molpx to generate a regspace sample of the data\n", + "See the documentation of `molpx.generate.sample` to find out about all possible options:\n", + "```\n", + "molpx.generate.sample(MD_trajectories, MD_top, projected_trajectories, atom_selection=None, proj_idxs=[0, 1], n_points=100, n_geom_samples=1, keep_all_samples=False, proj_stride=1, verbose=False, return_data=False)\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "40650f5c7588442287b330420a2af834", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\r" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "648739be3ea34f0caa809cce735d29a0", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "25-05-18 16:20:13 pyemma.coordinates.clustering.kmeans.KmeansClustering[19] INFO Algorithm did not reach convergence criterion of 1e-05 in 10 iterations. Consider increasing max_iter.\n", + "\r" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "b13f704953e1466980aeadaa0ecba3ba", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\r" + ] + }, + { + "data": { + "text/plain": [ + "((203, 2),\n", + " )" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "data_sample, geoms = molpx.generate.sample(#MD_list, \n", + " MD_trajfiles, \n", + " top, \n", + " #Y, \n", + " Y_filenames,\n", + " n_points=200 ,\n", + " n_geom_samples=2,\n", + " )\n", + "data_sample.shape, geoms\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Link the PDF plot with the sampled structures and visually explore the FES \n", + "Click either on the plot or on the widget slidebar: they're connected! " + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "scrolled": false + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "65056c0dfdfe461b8f7f901525c17420", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/mi/gph82/miniconda3/lib/python3.6/site-packages/ipykernel_launcher.py:4: RuntimeWarning: divide by zero encountered in log\n", + " after removing the cwd from sys.path.\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "01b484da426b4056be8f72717dc7a465", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Replot the FES\n", + "plt.figure(figsize=(7,7))\n", + "h, (x,y) = np.histogramdd(np.vstack(Y)[:,:2], bins=50)\n", + "plt.contourf(x[:-1], y[:-1], -np.log(h.T), alpha=.50)\n", + "# Create the linked widget\n", + "linked_ngl_wdg, linked_ax_wdg = molpx.visualize.sample(data_sample, \n", + " geoms.superpose(geoms[0]), \n", + " plt.gca(), \n", + " clear_lines=True,\n", + " #plot_path=True\n", + " )\n", + "plt.plot(data_sample[:,0], data_sample[:,1],' ok', zorder=0)\n", + "# Show it\n", + "linked_ngl_wdg\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Paths samples along the different projections (=axis)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "8a0dace2ef40402ebaba8151d3bd1323", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\r" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "44877122d64f4ada8996e2fb8f2d802d", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "25-05-18 16:20:23 pyemma.coordinates.clustering.kmeans.KmeansClustering[30] INFO Cluster centers converged after 5 steps.\n", + "\r" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "352d76d1863e4b05a24e6abeeffd3dc9", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\r" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "9893e3b9461d489a85b3700fb8f39885", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "25-05-18 16:20:28 pyemma.coordinates.clustering.kmeans.KmeansClustering[37] INFO Algorithm did not reach convergence criterion of 1e-05 in 10 iterations. Consider increasing max_iter.\n", + "\r" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "64695b6cc51840d5a69d8516f28a742f", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\r" + ] + } + ], + "source": [ + "paths_dict, idata = molpx.generate.projection_paths(#MD_list, \n", + " MD_trajfiles, \n", + " top, \n", + " Y_filenames,\n", + " #Y, # You can also directly give the data here\n", + " n_points=50,\n", + " proj_idxs=[0,1],\n", + " n_projs=3,\n", + " proj_dim = 3, \n", + " verbose=False, \n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Link the PDF plot with the sampled paths/structures and visually explore the coordinates (separately). \n", + "Click either on the plot or on the widget slidebar: they're connected! You can change the type of path between min_rmsd or min_disp and you can also change the coordinate sampled (0 or 1)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "# Choose the coordinate and the tyep of path\n", + "coord = 1\n", + "#path_type = 'min_rmsd'\n", + "path_type = 'min_disp'\n", + "igeom = paths_dict[coord][path_type][\"geom\"]\n", + "ipath = paths_dict[coord][path_type][\"proj\"]\n", + "\n", + "# Choose the proj_idxs for the path and the FES \n", + "# to be shown\n", + "proj_idxs = [0,1]" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "scrolled": false + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "b39ef814d49e4bcb92624c77e1b81959", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/mi/gph82/miniconda3/lib/python3.6/site-packages/ipykernel_launcher.py:3: RuntimeWarning: divide by zero encountered in log\n", + " This is separate from the ipykernel package so we can avoid doing imports until\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "74e76004aceb4b73b5c8dcd7b17ee5c9", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "plt.figure(figsize=(7,7))\n", + "h, (x,y) = np.histogramdd(np.vstack(Y)[:,proj_idxs], bins=50)\n", + "plt.contourf(x[:-1], y[:-1], -np.log(h.T), alpha=.50)\n", + "\n", + "linked_ngl_wdg, linked_ax_wdg = molpx.visualize.sample(ipath[:,proj_idxs], \n", + " igeom.superpose(igeom[0]), \n", + " plt.gca(), \n", + " clear_lines=True,\n", + " n_smooth = 5, \n", + " plot_path=True, \n", + " #radius=True,\n", + " )\n", + "linked_ngl_wdg" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Interaction with ```PyEMMA```\n", + "`molpx` is using many methods of the `coordinates` submodule of `PyEMMA`, and thus it also understands some of `PyEMMA`'s classes as input (like clustering objects or streaming transformers).\n", + "## Using the TICA object to visualize the most correlated input features\n", + "If the projected coordinates come from a TICA (or PCA) transformation, and the TICA object is available in memory\n", + "`molpx.visualize.traj` can make use of correlation information to display not only the projected coordinates (i.e the TICs, in this case), but also the \"original\" input features behind it" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "e1840ce738634e2fa61a81f30ed60d6a", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\r" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "75fa7b0e940d4166af0fbef10c5b25e7", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\r" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "2dde02bd959c468387bd556e5d7448e7", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\r" + ] + } + ], + "source": [ + "# Re-do the TICA computation to make sure we have a tica object in memory\n", + "feat = pyemma.coordinates.featurizer(top)\n", + "pairs = feat.pairs(range(feat.topology.n_atoms)[::2])\n", + "feat.add_distances(pairs)\n", + "src = pyemma.coordinates.source(MD_trajfiles, features=feat)\n", + "tica = pyemma.coordinates.tica(src, lag=10, dim=3)\n", + "Y = tica.get_output() " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The method `molpx.visualize.correlations` tries to provide a visual representation of the projected coordinates by relating them to the input features, which carry more meaning, since they are (usually) familiar parameters such as atom distances, angles, contacts etc." + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "20651793b9cb40689a3505803dbf4171", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Comment or uncomment the optinal parameters and see how the method reacts\n", + "# You can use a pre-instantiated the widget\n", + "#iwd = molpx.visualize._nglwidget_wrapper(MD_list[0][0])\n", + "# Or instantiate at the moment of calling visualize.correlations\n", + "iwd = None\n", + "corr, ngl_wdg = molpx.visualize.correlations(tica, \n", + " n_feats=3, \n", + " proj_idxs=[0,1,2], \n", + " geoms=MD_list[0][::100],\n", + " #verbose=True,\n", + " #proj_color_list=['red', 'blue', 'green'],\n", + " widget=iwd\n", + " )\n", + "ngl_wdg" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Use the correlation-dictionary's modified `print` function to see what's inside in a human-friendly way" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Correlation dictionary for 3 projections\n", + " Corr[proj_0|feat] = 0.8\n", + " DIST: PRO 9 CA 8 - TYR 23 CA 22\n", + " feat nr. 112, atom idxs [ 8 22]\n", + " Corr[proj_0|feat] = 0.7\n", + " DIST: PRO 9 CA 8 - TYR 21 CA 20\n", + " feat nr. 111, atom idxs [ 8 20]\n", + " Corr[proj_0|feat] = 0.7\n", + " DIST: PRO 9 CA 8 - LEU 29 CA 28\n", + " feat nr. 115, atom idxs [ 8 28]\n", + "\n", + " Corr[proj_1|feat] = 0.7\n", + " DIST: PRO 9 CA 8 - LYS 15 CA 14\n", + " feat nr. 108, atom idxs [ 8 14]\n", + " Corr[proj_1|feat] = 0.7\n", + " DIST: LYS 15 CA 14 - TYR 23 CA 22\n", + " feat nr. 178, atom idxs [14 22]\n", + " Corr[proj_1|feat] = 0.6\n", + " DIST: LYS 15 CA 14 - PHE 33 CA 32\n", + " feat nr. 183, atom idxs [14 32]\n", + "\n", + " Corr[proj_2|feat] = 0.6\n", + " DIST: THR 11 CA 10 - GLY 37 CA 36\n", + " feat nr. 142, atom idxs [10 36]\n", + " Corr[proj_2|feat] = -0.6\n", + " DIST: PRO 13 CA 12 - GLN 31 CA 30\n", + " feat nr. 161, atom idxs [12 30]\n", + " Corr[proj_2|feat] = -0.6\n", + " DIST: PRO 13 CA 12 - PHE 33 CA 32\n", + " feat nr. 162, atom idxs [12 32]\n", + "\n", + "\n" + ] + } + ], + "source": [ + "print(corr)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Also, `molpx.visualize.traj` can help in visualizing these correlations by parsing along the tica object itself as `projection=tica`. In the next cell, can you spot the differences:\n", + "* In the nglwidget?\n", + "* In the trajectories?\n" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "scrolled": false + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "2ba2f827bfc1443eba16e4e0f39551b6", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\r" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "ca694d306d5d46d3afcb5e11e0a23ee8", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "25-05-18 16:21:05 pyemma.coordinates.clustering.kmeans.KmeansClustering[51] INFO Algorithm did not reach convergence criterion of 1e-05 in 10 iterations. Consider increasing max_iter.\n", + "\r" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "d3720ce062b1447a836529b5b64eb77e", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\r" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "7d0f399632f043128c702a080ee8f992", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Reuse the visualize.traj method with the tica object as input\n", + "mpx_wdg_box = molpx.visualize.traj(MD_trajfiles, \n", + " top, \n", + " Y,\n", + " #Y_filenames, \n", + " plot_FES = True, \n", + " dt = dt*1e-6, tunits='ms', \n", + " #traj_selection = 0,\n", + " #sharey_traj=False,\n", + " #max_frames=100,\n", + " proj_idxs=[0,1], \n", + " panel_height=1,\n", + " projection=tica, ## this is what's new\n", + " n_feats=2\n", + " )\n", + " \n", + "mpx_wdg_box" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Use a clustering object as input \n", + "If the dataset has already been clustered, and it is **that** clustering that the user wants to explore, `molpx.generate.sample` can take this clustering object as an input instead of the \n", + "the projected trajectories:" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "5f70ea98e8664904b287352cc069f360", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "6c849ae629334244b15b772893e2b4d8", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "25-05-18 18:34:40 pyemma.coordinates.clustering.kmeans.KmeansClustering[6] INFO Algorithm did not reach convergence criterion of 1e-05 in 10 iterations. Consider increasing max_iter.\n", + "\r" + ] + } + ], + "source": [ + "# Do \"some\" clustering\n", + "clkmeans = pyemma.coordinates.cluster_kmeans([iY[:,:2] for iY in Y], 5)" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "388a443404fe4892afdc9211d4c0d75e", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\r" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "5d1cb1ad69f7412b9e5b0a1fe49b04e6", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\r" + ] + } + ], + "source": [ + "data_sample, geoms = molpx.generate.sample(MD_trajfiles, top, clkmeans, \n", + " n_geom_samples=50, \n", + " #keep_all_samples=True # read the doc for this argument\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "645d43cdc8ba4042a807cf115a0d012e", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/mi/gph82/miniconda3/lib/python3.6/site-packages/ipykernel_launcher.py:6: RuntimeWarning: divide by zero encountered in log\n", + " \n" + ] + }, + { + "data": { + "text/plain": [ + "(NGLWidget(count=5), )" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Plot clusters\n", + "plt.figure(figsize=(4,4))\n", + "plt.plot(clkmeans.clustercenters[:,0], clkmeans.clustercenters[:,1],' ok')\n", + "# FES as background is optional (change the bool to False)\n", + "if True:\n", + " plt.contourf(x[:-1], y[:-1], -np.log(h.T), alpha=.50)\n", + "\n", + "# Link the clusters positions with the molecular structures\n", + "linked_ngl_wdg = molpx.visualize.sample(data_sample, \n", + " geoms.superpose(geoms[0]), \n", + " plt.gca(), \n", + " clear_lines=False,\n", + " #plot_path=True\n", + " )\n", + "linked_ngl_wdg" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Visual representations for MSMs\n", + "Visually inspect the network behind an MSM" + ] + }, + { + "cell_type": "code", + "execution_count": 87, + "metadata": {}, + "outputs": [], + "source": [ + "MSM = pyemma.msm.estimate_markov_model(clkmeans.dtrajs, 20)" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [], + "source": [ + "from molpx import visualize\n", + "from imp import reload" + ] + }, + { + "cell_type": "code", + "execution_count": 128, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "2531928ebea843989e9df57c24c169cb", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "0df623a5b93244bf9389339bc8d88ff1", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "28-05-18 16:04:20 pyemma.coordinates.clustering.kmeans.KmeansClustering[31] INFO Algorithm did not reach convergence criterion of 1e-05 in 10 iterations. Consider increasing max_iter.\n", + "\r" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "547cbf79687840a09af3e2daa80959f8", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\r" + ] + } + ], + "source": [ + "clkmeans20 = pyemma.coordinates.cluster_kmeans([iY[:,:2] for iY in Y], 100)\n", + "MSMcg = pyemma.msm.estimate_markov_model(clkmeans20.dtrajs, 20).coarse_grain(3)" + ] + }, + { + "cell_type": "code", + "execution_count": 160, + "metadata": { + "scrolled": false + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "4f18e08d7c9a4ea09909cdd23b432c06", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "reload(visualize)\n", + "visualize.MSM(MSMcg, src, clkmeans20,\n", + " #sticky=True, \n", + " #sharpen=True, \n", + " n_overlays=10,\n", + " )" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## TPT Reactive Pathway Representation\n", + "Until we add a method for doing this explicitly, you can do it in a few lines of code" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "e40f35977dda46efb5810f00ea25fe65", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\r" + ] + }, + { + "data": { + "text/plain": [ + "123" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Do an MSM with a realistic number of clustercenters\n", + "cl_many = pyemma.coordinates.cluster_regspace([iY[:,:2] for iY in Y], dmin=.25)\n", + "M = pyemma.msm.estimate_markov_model(cl_many.dtrajs, 20)\n", + "cl_many.n_clusters" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "776ae5e56ad04871906ee0cae34870fb", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\r" + ] + } + ], + "source": [ + "# Use this object to sample geometries\n", + "pos, geom = molpx.generate.sample(MD_trajfiles, top, cl_many)" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[-0.18704125 -0.77366424] [ 6.71851349 0.03159955]\n" + ] + } + ], + "source": [ + "# Find the most representative microstate of each \n", + "# and least populated macrostate\n", + "M.pcca(3)\n", + "dens_max_i = [distro.argmax() for distro in M.metastable_distributions]\n", + "A = np.argmax([M.stationary_distribution[iset].sum() for iset in M.metastable_sets])\n", + "B = np.argmin([M.stationary_distribution[iset].sum() for iset in M.metastable_sets])\n", + "print(cl_many.clustercenters[dens_max_i[A]],\n", + " cl_many.clustercenters[dens_max_i[B]])" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "# Create a TPT object with most_pop, least_pop as source, sink respectively\n", + "tpt = pyemma.msm.tpt(M, [dens_max_i[A]], [dens_max_i[B]])\n", + "paths, flux = tpt.pathways(fraction=.5)" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": { + "collapsed": true, + "scrolled": true + }, + "outputs": [], + "source": [ + "# Get a path with a decent number of intermediates\n", + "sample_path = paths[np.argmax([len(ipath) for ipath in paths])]" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "4db8e53777e14e689eddd81e4220f8bd", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/guille/miniconda3/lib/python3.6/site-packages/ipykernel_launcher.py:2: RuntimeWarning: divide by zero encountered in log\n", + " \n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "6a998bfad0dd40cfacddcd68327fe9be", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "plt.figure()\n", + "plt.contourf(x[:-1], y[:-1], -np.log(h.T), cmap=\"jet\", alpha=.5, zorder=0)\n", + "linked_ngl_wdg, linked_ax_wdg = molpx.visualize.sample(cl_many.clustercenters[sample_path], \n", + " geom[sample_path].superpose(geom[sample_path[0]]), plt.gca(), \n", + " plot_path=True,\n", + " )\n", + "plt.scatter(*cl_many.clustercenters.T, alpha=.25)\n", + "linked_ngl_wdg" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [] + } + ], + "metadata": { + "anaconda-cloud": {}, + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.2" + }, + "widgets": { + "state": { + "00748d42b5924a068146f4fb0dd88755": { + "views": [ + { + "cell_index": 14 + } + ] + }, + "5908b63fa1a94e829efa9ad1d20c5c0a": { + "views": [ + { + "cell_index": 21 + } + ] + }, + "6b0e5b709f194cae92e9f26d95b77b1f": { + "views": [ + { + "cell_index": 10 + } + ] + }, + "87521a1dace146468576b6e9a3cc4b0b": { + "views": [ + { + "cell_index": 8 + } + ] + }, + "a5d6373ecfab400a866af9e48b21f009": { + "views": [ + { + "cell_index": 19 + } + ] + }, + "d2f7e83143d845e78adc56c246ba8e6d": { + "views": [ + { + "cell_index": 21 + } + ] + } + }, + "version": "1.2.0" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/molpx/notebooks/3.molPX_TICA_Ala2.ipynb b/molpx/notebooks/3.molPX_TICA_Ala2.ipynb index 9cafd2b..632b9dd 100644 --- a/molpx/notebooks/3.molPX_TICA_Ala2.ipynb +++ b/molpx/notebooks/3.molPX_TICA_Ala2.ipynb @@ -653,7 +653,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.1" + "version": "3.6.2" }, "widgets": { "state": { diff --git a/molpx/notebooks/5.molPX_GPCR_Opsin_contacts.ipynb b/molpx/notebooks/5.molPX_GPCR_Opsin_contacts.ipynb new file mode 100644 index 0000000..8e6a730 --- /dev/null +++ b/molpx/notebooks/5.molPX_GPCR_Opsin_contacts.ipynb @@ -0,0 +1,3586 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# molPX Opsin Example\n", + "
 \n",
+    "Guillermo Pérez-Hernández  guille.perez@fu-berlin.de \n",
+    "
\n", + " \n", + "This notebook uses a very short trajectory of a G-Protein-Coupled-Receptor (GPCR) molecule as an example. \n", + "Unlike Di-Alanine, it is very frequent that such systems are analyzed using of [contact maps](https://en.wikipedia.org/wiki/Protein_contact_map).\n", + "\n", + "This can be done now interactively inside the notebook. The trajectory itself is meaningless, but it still can be used as an example" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "import molpx" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Start from files on disk\n", + "As usual, our starting point is a pair files on disk with a topology and a trajectory. We import those into the notebook using the awesome [mdtraj](www.mdtraj.org):" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "import mdtraj as md\n", + "top = md.load(molpx._molpxdir(join='notebooks/data/ops.pdb.gz')).top\n", + "geom = md.load(molpx._molpxdir(join='notebooks/data/ops_mini.xtc'), top=top)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Compute a contact map using `mdtraj`\n", + "As usual, one could have computed this using any other python module or even an external program and load it into the notebook. We use the method [`md.compute_contacts`](http://mdtraj.org/1.9.0/api/generated/mdtraj.compute_contacts.html) and then `md.geometry.squareform` (see the example in the previous link).\n", + "\n", + "We also implement a cutoff of 3.5 Angstrom to create a binary plot" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "scrolled": false + }, + "outputs": [], + "source": [ + "contact_map = md.geometry.squareform(*md.compute_contacts(geom))" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "cutoff = .35 # cutoff for residue contact in nm\n", + "contact_map = (contact_map<.35).astype(float)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Visualize the `contact_map`-trajectory interactively\n", + "The following cell generates a matplotlib plot showing the contact map and an NGLView widget. The interactivity consists in:\n", + " - sliding the advance bar in the widget will update the contact map\n", + " - left-clicking on the contact map will:\n", + " - highlight the clicked pixel\n", + " - draw the corresponding distance on the widget\n", + " - right-clicking again on the contact map will delete the distance on the widgtet\n", + "\n", + "Panning and zooming of the contact map is allowed. Uncoment the `average` option to see what happens. As usual, both the plot and the widget are contained in a `molpxHBox`. " + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "scrolled": false + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "78b54deed26947ea84b13d30213b7099", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "A Jupyter Widget" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "mpx_wdg_box = molpx.visualize.contacts(contact_map, geom, \n", + " average=True, \n", + " )\n", + "mpx_wdg_box" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# The ```mpx_wdg_box```\n", + "It is a class derived from the ipython widgets [HBox](https://ipywidgets.readthedocs.io/en/latest/examples/Widget%20List.html?highlight=Hbox#Container/Layout-widgets) and [VBox](https://ipywidgets.readthedocs.io/en/latest/examples/Widget%20List.html?highlight=Hbox#Container/Layout-widgets), with molPX's extra information as attributes starting with `linked_*` " + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "linked_ax_wdgs\n", + "linked_axes\n", + "linked_data_arrays\n", + "linked_figs\n", + "linked_mdgeoms\n", + "linked_ngl_wdgs\n" + ] + } + ], + "source": [ + "for attr in dir(mpx_wdg_box):\n", + " if attr.startswith('linked_'):\n", + " print(attr)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "anaconda-cloud": {}, + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.2" + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "state": { + "02f12bd6d6504b0ba027f64e0cfde1b1": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.0.0", + "model_name": "VBoxModel", + "state": { + "children": [ + "IPY_MODEL_7bfd978d08ec442eb49b810fa764620a" + ], + "layout": "IPY_MODEL_04b8decd164b41e99c638cae7b722702" + } + }, + "04b8decd164b41e99c638cae7b722702": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": { + "border": "solid" + } + }, + "057a8469d4854abda77edc709739ca6d": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.0.0", + "model_name": "HBoxModel", + "state": { + "children": [ + "IPY_MODEL_2e1980f0cec742dab313f9ba0c99c926", + "IPY_MODEL_81ee8894b8b4442b978e319dc3a08da3" + ], + "layout": "IPY_MODEL_a0d945adcbde4207a8dfe8d9b9f371e2" + } + }, + "05d2da84e09d478dab3dee3c72ca6b59": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": {} + }, + "05e2b4b8f40e4ba29f0917bbdb4d639e": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": {} + }, + "07c691224682492db5979c86a01ede66": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": {} + }, + "083b5b795e9f4e60a8dfd2abba86db3c": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.0.0", + "model_name": "ImageModel", + "state": { + "layout": "IPY_MODEL_487200083f724442a5117f01b84eb91d", + "value": {}, + "width": "900.0" + } + }, + "091aba9a11cc45879ac9e3936cdbf397": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": {} + }, + "099ccf42fe80479b854da28792ecedeb": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": {} + }, + "0d98ccbef3e34da180cf96ff2627eeb7": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.0.0", + "model_name": "ImageModel", + "state": { + "layout": "IPY_MODEL_d5a1720d76c74ef6b4e1d337f268576f", + "value": {}, + "width": "900.0" + } + }, + "0fd19a2883be4e5e808b82f204a3ecdb": { + "model_module": "nglview-js-widgets", + "model_module_version": "0.5.4-dev.27", + "model_name": "NGLModel", + "state": { + "_camera_orientation": [ + 20.242729722135532, + 0, + 0, + 0, + 0, + 20.242729722135532, + 0, + 0, + 0, + 0, + 20.242729722135532, + 0, + -1.6345000863075256, + -24.057000160217285, + -0.8634999841451645, + 1 + ], + "_camera_str": "orthographic", + "_image_data": "", + "_n_dragged_files": 0, + "_ngl_coordinate_resource": {}, + "_ngl_full_stage_parameters": { + "ambientColor": 14540253, + "ambientIntensity": 0.2, + "backgroundColor": "white", + "cameraFov": 40, + "cameraType": "perspective", + "clipDist": 10, + "clipFar": 100, + "clipNear": 0, + "fogFar": 100, + "fogNear": 50, + "hoverTimeout": 0, + "impostor": true, + "lightColor": 14540253, + "lightIntensity": 1, + "mousePreset": "default", + "panSpeed": 1, + "quality": "medium", + "rotateSpeed": 2, + "sampleLevel": 0, + "tooltip": true, + "workerDefault": true, + "zoomSpeed": 1.2 + }, + "_ngl_full_stage_parameters_embed": {}, + "_ngl_msg_archive": [], + "_ngl_original_stage_parameters": { + "ambientColor": 14540253, + "ambientIntensity": 0.2, + "backgroundColor": "white", + "cameraFov": 40, + "cameraType": "perspective", + "clipDist": 10, + "clipFar": 100, + "clipNear": 0, + "fogFar": 100, + "fogNear": 50, + "hoverTimeout": 0, + "impostor": true, + "lightColor": 14540253, + "lightIntensity": 1, + "mousePreset": "default", + "panSpeed": 1, + "quality": "medium", + "rotateSpeed": 2, + "sampleLevel": 0, + "tooltip": true, + "workerDefault": true, + "zoomSpeed": 1.2 + }, + "_ngl_repr_dict": { + "0": { + "0": { + "params": { + "aspectRatio": 2, + "assembly": "default", + "bondScale": 0.4, + "bondSpacing": 1, + "clipCenter": { + "x": 0, + "y": 0, + "z": 0 + }, + "clipNear": 0, + "clipRadius": 0, + "colorMode": "hcl", + "colorReverse": false, + "colorScale": "", + "colorScheme": "element", + "colorValue": 9474192, + "cylinderOnly": false, + "defaultAssembly": "", + "depthWrite": true, + "diffuse": 16777215, + "disableImpostor": false, + "disablePicking": false, + "flatShaded": false, + "lazy": false, + "lineOnly": false, + "linewidth": 2, + "matrix": { + "elements": [ + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1 + ] + }, + "metalness": 0, + "multipleBond": "off", + "opacity": 1, + "openEnded": true, + "quality": "medium", + "radialSegments": 10, + "radius": 0.15, + "roughness": 0.4, + "scale": 1, + "sele": "all", + "side": "double", + "sphereDetail": 1, + "visible": true, + "wireframe": false + }, + "type": "ball+stick" + } + } + }, + "_ngl_serialize": false, + "_ngl_version": "1.0.0-beta.4", + "_scene_position": {}, + "_scene_rotation": {}, + "background": "white", + "count": 101, + "frame": 0, + "layout": "IPY_MODEL_9399b24089f54498a9e100a5e810ac82", + "n_components": 1, + "picked": {} + } + }, + "142df587b15c41bf9b90c1c858cf5c9f": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": {} + }, + "1850213735f14c1cb59b177e30d1b5ec": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": {} + }, + "1c056b4b60a6442c918b60951d157199": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": {} + }, + "1c7f85966a9e4ade86ed57e517a49946": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": {} + }, + "2198a2fb7aad45679758cabf172ecc04": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": {} + }, + "25fa5915d3d24a82a6f4ed61ba9bd7e2": { + "model_module": "jupyter-matplotlib", + "model_module_version": "^0.0.2", + "model_name": "MPLCanvasModel", + "state": { + "_dom_classes": [], + "_id": "", + "_toolbar_items": [ + [ + "Home", + "Reset original view", + "fa fa-home icon-home", + "home" + ], + [ + "Back", + "Back to previous view", + "fa fa-arrow-left icon-arrow-left", + "back" + ], + [ + "Forward", + "Forward to next view", + "fa fa-arrow-right icon-arrow-right", + "forward" + ], + [ + "", + "", + "", + "" + ], + [ + "Pan", + "Pan axes with left mouse, zoom with right", + "fa fa-arrows icon-move", + "pan" + ], + [ + "Zoom", + "Zoom to rectangle", + "fa fa-square-o icon-check-empty", + "zoom" + ], + [ + "", + "", + "", + "" + ], + [ + "Download", + "Download plot", + "fa fa-floppy-o icon-save", + "download" + ], + [ + "Export", + "Export plot", + "fa fa-file-picture-o icon-picture", + "export" + ] + ], + "layout": "IPY_MODEL_7f1d2053edda4e5ca1b524e9f5924446" + } + }, + "260cabd9f2e442df94234fdeed71bb5f": { + "model_module": "jupyter-matplotlib", + "model_module_version": "^0.0.2", + "model_name": "MPLCanvasModel", + "state": { + "_dom_classes": [], + "_id": "", + "_toolbar_items": [ + [ + "Home", + "Reset original view", + "fa fa-home icon-home", + "home" + ], + [ + "Back", + "Back to previous view", + "fa fa-arrow-left icon-arrow-left", + "back" + ], + [ + "Forward", + "Forward to next view", + "fa fa-arrow-right icon-arrow-right", + "forward" + ], + [ + "", + "", + "", + "" + ], + [ + "Pan", + "Pan axes with left mouse, zoom with right", + "fa fa-arrows icon-move", + "pan" + ], + [ + "Zoom", + "Zoom to rectangle", + "fa fa-square-o icon-check-empty", + "zoom" + ], + [ + "", + "", + "", + "" + ], + [ + "Download", + "Download plot", + "fa fa-floppy-o icon-save", + "download" + ], + [ + "Export", + "Export plot", + "fa fa-file-picture-o icon-picture", + "export" + ] + ], + "layout": "IPY_MODEL_ef6f87771fbe4759a4fa1ed6a1657388" + } + }, + "2e1980f0cec742dab313f9ba0c99c926": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.0.0", + "model_name": "VBoxModel", + "state": { + "children": [ + "IPY_MODEL_bd2f9d6498f74a9bbe67da0a7b493466", + "IPY_MODEL_3c0fcc5000af42fa9011307f57d74de0" + ], + "layout": "IPY_MODEL_7bdfa9becaf64de6bf5d8e726cb08a1d" + } + }, + "2f4df660fff844c8ab5a0b81908629e5": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.0.0", + "model_name": "ImageModel", + "state": { + "layout": "IPY_MODEL_05d2da84e09d478dab3dee3c72ca6b59", + "value": {}, + "width": "900.0" + } + }, + "31b91051b3ec49cda634bde2e7c0d92f": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": {} + }, + "3291e228d2e94e2d9e8fa6982aa4177f": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.0.0", + "model_name": "HBoxModel", + "state": { + "children": [ + "IPY_MODEL_a61b2b49969448c0a122dc31812b203c", + "IPY_MODEL_8f3c995b40f94aeb906ceabb0c7edbe6" + ], + "layout": "IPY_MODEL_6030098920544b99896c0bc0a261f659" + } + }, + "3378fb06740f47eebb58aaec872c1190": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": {} + }, + "348b8044a47545eb9da6ffd3780fcb58": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.0.0", + "model_name": "HBoxModel", + "state": { + "children": [ + "IPY_MODEL_0fd19a2883be4e5e808b82f204a3ecdb", + "IPY_MODEL_adaf51e6a3fc417e914db69ac89b336c" + ], + "layout": "IPY_MODEL_7034794fd65c481baffe5d4cd405082f" + } + }, + "3c0fcc5000af42fa9011307f57d74de0": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.0.0", + "model_name": "VBoxModel", + "state": { + "children": [ + "IPY_MODEL_c95ca5fe005344588b3a1100a65530a2" + ], + "layout": "IPY_MODEL_9fd08b869f7a4dfda172448bfe845f6c" + } + }, + "3f91662dd21548e7a8a1d095a3701b16": { + "model_module": "nglview-js-widgets", + "model_module_version": "0.5.4-dev.27", + "model_name": "NGLModel", + "state": { + "_camera_orientation": [ + 20.261130760158405, + 0, + 0, + 0, + 0, + 20.261130760158405, + 0, + 0, + 0, + 0, + 20.261130760158405, + 0, + -1.602999985218048, + -24.08549976348877, + -0.7745000571012497, + 1 + ], + "_camera_str": "orthographic", + "_image_data": "", + "_n_dragged_files": 0, + "_ngl_coordinate_resource": {}, + "_ngl_full_stage_parameters": { + "ambientColor": 14540253, + "ambientIntensity": 0.2, + "backgroundColor": "white", + "cameraFov": 40, + "cameraType": "perspective", + "clipDist": 10, + "clipFar": 100, + "clipNear": 0, + "fogFar": 100, + "fogNear": 50, + "hoverTimeout": 0, + "impostor": true, + "lightColor": 14540253, + "lightIntensity": 1, + "mousePreset": "default", + "panSpeed": 1, + "quality": "medium", + "rotateSpeed": 2, + "sampleLevel": 0, + "tooltip": true, + "workerDefault": true, + "zoomSpeed": 1.2 + }, + "_ngl_full_stage_parameters_embed": {}, + "_ngl_msg_archive": [], + "_ngl_original_stage_parameters": { + "ambientColor": 14540253, + "ambientIntensity": 0.2, + "backgroundColor": "white", + "cameraFov": 40, + "cameraType": "perspective", + "clipDist": 10, + "clipFar": 100, + "clipNear": 0, + "fogFar": 100, + "fogNear": 50, + "hoverTimeout": 0, + "impostor": true, + "lightColor": 14540253, + "lightIntensity": 1, + "mousePreset": "default", + "panSpeed": 1, + "quality": "medium", + "rotateSpeed": 2, + "sampleLevel": 0, + "tooltip": true, + "workerDefault": true, + "zoomSpeed": 1.2 + }, + "_ngl_repr_dict": { + "0": { + "0": { + "params": { + "aspectRatio": 2, + "assembly": "default", + "bondScale": 0.4, + "bondSpacing": 1, + "clipCenter": { + "x": 0, + "y": 0, + "z": 0 + }, + "clipNear": 0, + "clipRadius": 0, + "colorMode": "hcl", + "colorReverse": false, + "colorScale": "", + "colorScheme": "element", + "colorValue": 9474192, + "cylinderOnly": false, + "defaultAssembly": "", + "depthWrite": true, + "diffuse": 16777215, + "disableImpostor": false, + "disablePicking": false, + "flatShaded": false, + "lazy": false, + "lineOnly": false, + "linewidth": 2, + "matrix": { + "elements": [ + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1 + ] + }, + "metalness": 0, + "multipleBond": "off", + "opacity": 1, + "openEnded": true, + "quality": "medium", + "radialSegments": 10, + "radius": 0.15, + "roughness": 0.4, + "scale": 1, + "sele": "all", + "side": "double", + "sphereDetail": 1, + "visible": true, + "wireframe": false + }, + "type": "ball+stick" + } + } + }, + "_ngl_serialize": false, + "_ngl_version": "1.0.0-beta.4", + "_scene_position": {}, + "_scene_rotation": {}, + "background": "white", + "count": 101, + "frame": 0, + "layout": "IPY_MODEL_cb2ce9cb14d74e6eba74aa41e3d3902c", + "n_components": 1, + "picked": {} + } + }, + "40260d55b7e84c5f84ce7ee1658cc7d8": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.0.0", + "model_name": "ImageModel", + "state": { + "layout": "IPY_MODEL_099ccf42fe80479b854da28792ecedeb", + "value": {}, + "width": "900.0" + } + }, + "4224777922c14f31894db65575a86dca": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": {} + }, + "425f648935634967b4fe500e1c1a2253": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.0.0", + "model_name": "ImageModel", + "state": { + "layout": "IPY_MODEL_d523002ca8a94346b19d51176c999b2f", + "value": {}, + "width": "900.0" + } + }, + "4598d04e9e5f4177980ce355c95007e2": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.0.0", + "model_name": "HBoxModel", + "state": { + "children": [ + "IPY_MODEL_4aeee03c9ad04073910a9ae57df1cd63", + "IPY_MODEL_260cabd9f2e442df94234fdeed71bb5f" + ], + "layout": "IPY_MODEL_c08e3c0d48a848048a9eddb863b63b9b" + } + }, + "4829969606c0466883452a51a9eda8ed": { + "model_module": "nglview-js-widgets", + "model_module_version": "0.5.4-dev.27", + "model_name": "NGLModel", + "state": { + "_camera_orientation": [ + 22.27569735577003, + 0, + 0, + 0, + 0, + 22.27569735577003, + 0, + 0, + 0, + 0, + 22.27569735577003, + 0, + -1.6459999680519104, + -24.449999809265137, + -0.7815000414848328, + 1 + ], + "_camera_str": "orthographic", + "_image_data": "", + "_n_dragged_files": 0, + "_ngl_coordinate_resource": {}, + "_ngl_full_stage_parameters": { + "ambientColor": 14540253, + "ambientIntensity": 0.2, + "backgroundColor": "white", + "cameraFov": 40, + "cameraType": "perspective", + "clipDist": 10, + "clipFar": 100, + "clipNear": 0, + "fogFar": 100, + "fogNear": 50, + "hoverTimeout": 0, + "impostor": true, + "lightColor": 14540253, + "lightIntensity": 1, + "mousePreset": "default", + "panSpeed": 1, + "quality": "medium", + "rotateSpeed": 2, + "sampleLevel": 0, + "tooltip": true, + "workerDefault": true, + "zoomSpeed": 1.2 + }, + "_ngl_full_stage_parameters_embed": {}, + "_ngl_msg_archive": [], + "_ngl_original_stage_parameters": { + "ambientColor": 14540253, + "ambientIntensity": 0.2, + "backgroundColor": "white", + "cameraFov": 40, + "cameraType": "perspective", + "clipDist": 10, + "clipFar": 100, + "clipNear": 0, + "fogFar": 100, + "fogNear": 50, + "hoverTimeout": 0, + "impostor": true, + "lightColor": 14540253, + "lightIntensity": 1, + "mousePreset": "default", + "panSpeed": 1, + "quality": "medium", + "rotateSpeed": 2, + "sampleLevel": 0, + "tooltip": true, + "workerDefault": true, + "zoomSpeed": 1.2 + }, + "_ngl_repr_dict": { + "0": { + "0": { + "params": { + "aspectRatio": 2, + "assembly": "default", + "bondScale": 0.4, + "bondSpacing": 1, + "clipCenter": { + "x": 0, + "y": 0, + "z": 0 + }, + "clipNear": 0, + "clipRadius": 0, + "colorMode": "hcl", + "colorReverse": false, + "colorScale": "", + "colorScheme": "element", + "colorValue": 9474192, + "cylinderOnly": false, + "defaultAssembly": "", + "depthWrite": true, + "diffuse": 16777215, + "disableImpostor": false, + "disablePicking": false, + "flatShaded": false, + "lazy": false, + "lineOnly": false, + "linewidth": 2, + "matrix": { + "elements": [ + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1 + ] + }, + "metalness": 0, + "multipleBond": "off", + "opacity": 1, + "openEnded": true, + "quality": "medium", + "radialSegments": 10, + "radius": 0.15, + "roughness": 0.4, + "scale": 1, + "sele": "all", + "side": "double", + "sphereDetail": 1, + "visible": true, + "wireframe": false + }, + "type": "ball+stick" + } + } + }, + "_ngl_serialize": false, + "_ngl_version": "1.0.0-beta.4", + "_scene_position": {}, + "_scene_rotation": {}, + "background": "white", + "count": 101, + "frame": 93, + "layout": "IPY_MODEL_c492ce1d58cd4004a095f6c0aefd7d04", + "n_components": 1, + "picked": {} + } + }, + "487200083f724442a5117f01b84eb91d": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": {} + }, + "4aeee03c9ad04073910a9ae57df1cd63": { + "model_module": "nglview-js-widgets", + "model_module_version": "0.5.4-dev.27", + "model_name": "NGLModel", + "state": { + "_camera_orientation": [ + 22.335864075149413, + 0, + 0, + 0, + 0, + 22.335864075149413, + 0, + 0, + 0, + 0, + 22.335864075149413, + 0, + -1.5269999504089355, + -24.34749984741211, + -0.7614999413490295, + 1 + ], + "_camera_str": "orthographic", + "_image_data": "", + "_n_dragged_files": 0, + "_ngl_coordinate_resource": {}, + "_ngl_full_stage_parameters": { + "ambientColor": 14540253, + "ambientIntensity": 0.2, + "backgroundColor": "white", + "cameraFov": 40, + "cameraType": "perspective", + "clipDist": 10, + "clipFar": 100, + "clipNear": 0, + "fogFar": 100, + "fogNear": 50, + "hoverTimeout": 0, + "impostor": true, + "lightColor": 14540253, + "lightIntensity": 1, + "mousePreset": "default", + "panSpeed": 1, + "quality": "medium", + "rotateSpeed": 2, + "sampleLevel": 0, + "tooltip": true, + "workerDefault": true, + "zoomSpeed": 1.2 + }, + "_ngl_full_stage_parameters_embed": {}, + "_ngl_msg_archive": [], + "_ngl_original_stage_parameters": { + "ambientColor": 14540253, + "ambientIntensity": 0.2, + "backgroundColor": "white", + "cameraFov": 40, + "cameraType": "perspective", + "clipDist": 10, + "clipFar": 100, + "clipNear": 0, + "fogFar": 100, + "fogNear": 50, + "hoverTimeout": 0, + "impostor": true, + "lightColor": 14540253, + "lightIntensity": 1, + "mousePreset": "default", + "panSpeed": 1, + "quality": "medium", + "rotateSpeed": 2, + "sampleLevel": 0, + "tooltip": true, + "workerDefault": true, + "zoomSpeed": 1.2 + }, + "_ngl_repr_dict": { + "0": { + "0": { + "params": { + "aspectRatio": 2, + "assembly": "default", + "bondScale": 0.4, + "bondSpacing": 1, + "clipCenter": { + "x": 0, + "y": 0, + "z": 0 + }, + "clipNear": 0, + "clipRadius": 0, + "colorMode": "hcl", + "colorReverse": false, + "colorScale": "", + "colorScheme": "element", + "colorValue": 9474192, + "cylinderOnly": false, + "defaultAssembly": "", + "depthWrite": true, + "diffuse": 16777215, + "disableImpostor": false, + "disablePicking": false, + "flatShaded": false, + "lazy": false, + "lineOnly": false, + "linewidth": 2, + "matrix": { + "elements": [ + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1 + ] + }, + "metalness": 0, + "multipleBond": "off", + "opacity": 1, + "openEnded": true, + "quality": "medium", + "radialSegments": 10, + "radius": 0.15, + "roughness": 0.4, + "scale": 1, + "sele": "all", + "side": "double", + "sphereDetail": 1, + "visible": true, + "wireframe": false + }, + "type": "ball+stick" + } + } + }, + "_ngl_serialize": false, + "_ngl_version": "1.0.0-beta.4", + "_scene_position": {}, + "_scene_rotation": {}, + "background": "white", + "count": 101, + "frame": 0, + "layout": "IPY_MODEL_952e7c4f72be4ad78e081ef9ca637c2a", + "n_components": 1, + "picked": {} + } + }, + "4b8ff78d08724692bffc261489797bcd": { + "model_module": "nglview-js-widgets", + "model_module_version": "0.5.4-dev.27", + "model_name": "NGLModel", + "state": { + "_camera_orientation": [ + 20.440059597101534, + 0, + 0, + 0, + 0, + 20.440059597101534, + 0, + 0, + 0, + 0, + 20.440059597101534, + 0, + -1.5479999482631683, + -24.090499877929688, + -0.8869999498128891, + 1 + ], + "_camera_str": "orthographic", + "_image_data": "", + "_n_dragged_files": 0, + "_ngl_coordinate_resource": {}, + "_ngl_full_stage_parameters": { + "ambientColor": 14540253, + "ambientIntensity": 0.2, + "backgroundColor": "white", + "cameraFov": 40, + "cameraType": "perspective", + "clipDist": 10, + "clipFar": 100, + "clipNear": 0, + "fogFar": 100, + "fogNear": 50, + "hoverTimeout": 0, + "impostor": true, + "lightColor": 14540253, + "lightIntensity": 1, + "mousePreset": "default", + "panSpeed": 1, + "quality": "medium", + "rotateSpeed": 2, + "sampleLevel": 0, + "tooltip": true, + "workerDefault": true, + "zoomSpeed": 1.2 + }, + "_ngl_full_stage_parameters_embed": {}, + "_ngl_msg_archive": [], + "_ngl_original_stage_parameters": { + "ambientColor": 14540253, + "ambientIntensity": 0.2, + "backgroundColor": "white", + "cameraFov": 40, + "cameraType": "perspective", + "clipDist": 10, + "clipFar": 100, + "clipNear": 0, + "fogFar": 100, + "fogNear": 50, + "hoverTimeout": 0, + "impostor": true, + "lightColor": 14540253, + "lightIntensity": 1, + "mousePreset": "default", + "panSpeed": 1, + "quality": "medium", + "rotateSpeed": 2, + "sampleLevel": 0, + "tooltip": true, + "workerDefault": true, + "zoomSpeed": 1.2 + }, + "_ngl_repr_dict": { + "0": { + "0": { + "params": { + "aspectRatio": 2, + "assembly": "default", + "bondScale": 0.4, + "bondSpacing": 1, + "clipCenter": { + "x": 0, + "y": 0, + "z": 0 + }, + "clipNear": 0, + "clipRadius": 0, + "colorMode": "hcl", + "colorReverse": false, + "colorScale": "", + "colorScheme": "element", + "colorValue": 9474192, + "cylinderOnly": false, + "defaultAssembly": "", + "depthWrite": true, + "diffuse": 16777215, + "disableImpostor": false, + "disablePicking": false, + "flatShaded": false, + "lazy": false, + "lineOnly": false, + "linewidth": 2, + "matrix": { + "elements": [ + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1 + ] + }, + "metalness": 0, + "multipleBond": "off", + "opacity": 1, + "openEnded": true, + "quality": "medium", + "radialSegments": 10, + "radius": 0.15, + "roughness": 0.4, + "scale": 1, + "sele": "all", + "side": "double", + "sphereDetail": 1, + "visible": true, + "wireframe": false + }, + "type": "ball+stick" + } + } + }, + "_ngl_serialize": false, + "_ngl_version": "1.0.0-beta.4", + "_scene_position": {}, + "_scene_rotation": {}, + "background": "white", + "count": 101, + "frame": 0, + "layout": "IPY_MODEL_981cfe953700402498eeec039f102f4a", + "n_components": 1, + "picked": {} + } + }, + "4e2d7923d0e34c7fbf272cab480aabc7": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": {} + }, + "4f5f5b9396204953a477bf57fbccbfc0": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": {} + }, + "526cafeb1ce042c1bce2614988570e74": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": {} + }, + "56ca44e8b4ef41eeace7b3c88ec9b15f": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.0.0", + "model_name": "ImageModel", + "state": { + "layout": "IPY_MODEL_2198a2fb7aad45679758cabf172ecc04", + "value": {}, + "width": "900.0" + } + }, + "5ce550eaef3f470a9d6e6142a846f97d": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": {} + }, + "6030098920544b99896c0bc0a261f659": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": {} + }, + "643eba7fb1e545d295d4b46e07b96d34": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.0.0", + "model_name": "HBoxModel", + "state": { + "children": [ + "IPY_MODEL_3f91662dd21548e7a8a1d095a3701b16", + "IPY_MODEL_87ba27ec276d4b4b9ac32cfdbc0d42fe" + ], + "layout": "IPY_MODEL_64c8a781e0f843b9a047200d2a4b844b" + } + }, + "64c8a781e0f843b9a047200d2a4b844b": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": {} + }, + "6775e7024caa458ea43e223a7fea355a": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.0.0", + "model_name": "ButtonStyleModel", + "state": {} + }, + "67b93a07aeea405daccd6c55cf6ea5ef": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.0.0", + "model_name": "HBoxModel", + "state": { + "children": [ + "IPY_MODEL_fb9d851ef63e4813bc8dcb22dc7e0f4b", + "IPY_MODEL_c6030751df7c46a2b1ba0b1e303c79cd" + ], + "layout": "IPY_MODEL_4f5f5b9396204953a477bf57fbccbfc0" + } + }, + "69ed4cec545c401b88f9dd9921011751": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.0.0", + "model_name": "HBoxModel", + "state": { + "children": [ + "IPY_MODEL_4829969606c0466883452a51a9eda8ed", + "IPY_MODEL_d5f60915434a422999c82949b45700a6" + ], + "layout": "IPY_MODEL_ca6b587f4bdf4a31ac25c181de986d10" + } + }, + "6ce05eb037e14bb18eca09f8ea356484": { + "model_module": "jupyter-matplotlib", + "model_module_version": "^0.0.2", + "model_name": "MPLCanvasModel", + "state": { + "_dom_classes": [], + "_id": "", + "_toolbar_items": [ + [ + "Home", + "Reset original view", + "fa fa-home icon-home", + "home" + ], + [ + "Back", + "Back to previous view", + "fa fa-arrow-left icon-arrow-left", + "back" + ], + [ + "Forward", + "Forward to next view", + "fa fa-arrow-right icon-arrow-right", + "forward" + ], + [ + "", + "", + "", + "" + ], + [ + "Pan", + "Pan axes with left mouse, zoom with right", + "fa fa-arrows icon-move", + "pan" + ], + [ + "Zoom", + "Zoom to rectangle", + "fa fa-square-o icon-check-empty", + "zoom" + ], + [ + "", + "", + "", + "" + ], + [ + "Download", + "Download plot", + "fa fa-floppy-o icon-save", + "download" + ], + [ + "Export", + "Export plot", + "fa fa-file-picture-o icon-picture", + "export" + ] + ], + "layout": "IPY_MODEL_1c7f85966a9e4ade86ed57e517a49946" + } + }, + "7034794fd65c481baffe5d4cd405082f": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": {} + }, + "7311e30372fd4ad99110f011b8adb856": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": {} + }, + "74f0b5162cd042ff829d4578becfdb24": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.0.0", + "model_name": "ImageModel", + "state": { + "layout": "IPY_MODEL_07c691224682492db5979c86a01ede66", + "value": {}, + "width": "900.0" + } + }, + "75807289a5e64c8eb4e8179d222dfc8f": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": {} + }, + "75a01831722d4f60b5ee7e5eeca054b7": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": {} + }, + "7bdfa9becaf64de6bf5d8e726cb08a1d": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": { + "height": "2.0in", + "width": "5.0in" + } + }, + "7bfd978d08ec442eb49b810fa764620a": { + "model_module": "nglview-js-widgets", + "model_module_version": "0.5.4-dev.27", + "model_name": "NGLModel", + "state": { + "_camera_orientation": [ + 23.053436912758006, + 0, + 0, + 0, + 0, + 23.053436912758006, + 0, + 0, + 0, + 0, + 23.053436912758006, + 0, + -1.7549998760223389, + -24.3100004196167, + -0.7849999666213989, + 1 + ], + "_camera_str": "orthographic", + "_image_data": "", + "_n_dragged_files": 0, + "_ngl_coordinate_resource": {}, + "_ngl_full_stage_parameters": { + "ambientColor": 14540253, + "ambientIntensity": 0.2, + "backgroundColor": "white", + "cameraFov": 40, + "cameraType": "perspective", + "clipDist": 10, + "clipFar": 100, + "clipNear": 0, + "fogFar": 100, + "fogNear": 50, + "hoverTimeout": 0, + "impostor": true, + "lightColor": 14540253, + "lightIntensity": 1, + "mousePreset": "default", + "panSpeed": 1, + "quality": "medium", + "rotateSpeed": 2, + "sampleLevel": 0, + "tooltip": true, + "workerDefault": true, + "zoomSpeed": 1.2 + }, + "_ngl_full_stage_parameters_embed": {}, + "_ngl_msg_archive": [], + "_ngl_original_stage_parameters": { + "ambientColor": 14540253, + "ambientIntensity": 0.2, + "backgroundColor": "white", + "cameraFov": 40, + "cameraType": "perspective", + "clipDist": 10, + "clipFar": 100, + "clipNear": 0, + "fogFar": 100, + "fogNear": 50, + "hoverTimeout": 0, + "impostor": true, + "lightColor": 14540253, + "lightIntensity": 1, + "mousePreset": "default", + "panSpeed": 1, + "quality": "medium", + "rotateSpeed": 2, + "sampleLevel": 0, + "tooltip": true, + "workerDefault": true, + "zoomSpeed": 1.2 + }, + "_ngl_repr_dict": { + "0": { + "0": { + "params": { + "aspectRatio": 2, + "assembly": "default", + "bondScale": 0.4, + "bondSpacing": 1, + "clipCenter": { + "x": 0, + "y": 0, + "z": 0 + }, + "clipNear": 0, + "clipRadius": 0, + "colorMode": "hcl", + "colorReverse": false, + "colorScale": "", + "colorScheme": "element", + "colorValue": 9474192, + "cylinderOnly": false, + "defaultAssembly": "", + "depthWrite": true, + "diffuse": 16777215, + "disableImpostor": false, + "disablePicking": false, + "flatShaded": false, + "lazy": false, + "lineOnly": false, + "linewidth": 2, + "matrix": { + "elements": [ + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1 + ] + }, + "metalness": 0, + "multipleBond": "off", + "opacity": 1, + "openEnded": true, + "quality": "medium", + "radialSegments": 10, + "radius": 0.15, + "roughness": 0.4, + "scale": 1, + "sele": "all", + "side": "double", + "sphereDetail": 1, + "visible": true, + "wireframe": false + }, + "type": "ball+stick" + } + } + }, + "_ngl_serialize": false, + "_ngl_version": "1.0.0-beta.4", + "_scene_position": {}, + "_scene_rotation": {}, + "background": "white", + "count": 3334, + "frame": 0, + "layout": "IPY_MODEL_05e2b4b8f40e4ba29f0917bbdb4d639e", + "n_components": 1, + "picked": {} + } + }, + "7c45ca81b410488dad1921ef9e3cd666": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": { + "width": "100%" + } + }, + "7db7fde8386c41f4a6729b9f01fd10fa": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": {} + }, + "7f1d2053edda4e5ca1b524e9f5924446": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": {} + }, + "806ca048c1834d5a91b01e0384ba0116": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.0.0", + "model_name": "ImageModel", + "state": { + "layout": "IPY_MODEL_c30dcd93e9fc4fc780101d81fc128c10", + "value": {}, + "width": "900.0" + } + }, + "80c7306fd5394a8180989ee6d1421468": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": {} + }, + "81ee8894b8b4442b978e319dc3a08da3": { + "model_module": "jupyter-matplotlib", + "model_module_version": "^0.0.2", + "model_name": "MPLCanvasModel", + "state": { + "_dom_classes": [], + "_id": "", + "_toolbar_items": [ + [ + "Home", + "Reset original view", + "fa fa-home icon-home", + "home" + ], + [ + "Back", + "Back to previous view", + "fa fa-arrow-left icon-arrow-left", + "back" + ], + [ + "Forward", + "Forward to next view", + "fa fa-arrow-right icon-arrow-right", + "forward" + ], + [ + "", + "", + "", + "" + ], + [ + "Pan", + "Pan axes with left mouse, zoom with right", + "fa fa-arrows icon-move", + "pan" + ], + [ + "Zoom", + "Zoom to rectangle", + "fa fa-square-o icon-check-empty", + "zoom" + ], + [ + "", + "", + "", + "" + ], + [ + "Download", + "Download plot", + "fa fa-floppy-o icon-save", + "download" + ], + [ + "Export", + "Export plot", + "fa fa-file-picture-o icon-picture", + "export" + ] + ], + "layout": "IPY_MODEL_3378fb06740f47eebb58aaec872c1190" + } + }, + "87ba27ec276d4b4b9ac32cfdbc0d42fe": { + "model_module": "jupyter-matplotlib", + "model_module_version": "^0.0.2", + "model_name": "MPLCanvasModel", + "state": { + "_dom_classes": [], + "_id": "", + "_toolbar_items": [ + [ + "Home", + "Reset original view", + "fa fa-home icon-home", + "home" + ], + [ + "Back", + "Back to previous view", + "fa fa-arrow-left icon-arrow-left", + "back" + ], + [ + "Forward", + "Forward to next view", + "fa fa-arrow-right icon-arrow-right", + "forward" + ], + [ + "", + "", + "", + "" + ], + [ + "Pan", + "Pan axes with left mouse, zoom with right", + "fa fa-arrows icon-move", + "pan" + ], + [ + "Zoom", + "Zoom to rectangle", + "fa fa-square-o icon-check-empty", + "zoom" + ], + [ + "", + "", + "", + "" + ], + [ + "Download", + "Download plot", + "fa fa-floppy-o icon-save", + "download" + ], + [ + "Export", + "Export plot", + "fa fa-file-picture-o icon-picture", + "export" + ] + ], + "layout": "IPY_MODEL_7db7fde8386c41f4a6729b9f01fd10fa" + } + }, + "8931ccd404fc4f4c992e0ecd8b9337cc": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": { + "height": "2.0in", + "width": "5.0in" + } + }, + "8a2c5b58347043848a48e53894cb7152": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": { + "width": "100%" + } + }, + "8a9d4e5f727846c183272e24d1c9bd7b": { + "model_module": "nglview-js-widgets", + "model_module_version": "0.5.4-dev.27", + "model_name": "NGLModel", + "state": { + "_camera_orientation": [ + 20.25637601878781, + 0, + 0, + 0, + 0, + 20.25637601878781, + 0, + 0, + 0, + 0, + 20.25637601878781, + 0, + -1.5909999758005142, + -23.98050022125244, + -0.5059999823570251, + 1 + ], + "_camera_str": "orthographic", + "_image_data": "", + "_n_dragged_files": 0, + "_ngl_coordinate_resource": {}, + "_ngl_full_stage_parameters": { + "ambientColor": 14540253, + "ambientIntensity": 0.2, + "backgroundColor": "white", + "cameraFov": 40, + "cameraType": "perspective", + "clipDist": 10, + "clipFar": 100, + "clipNear": 0, + "fogFar": 100, + "fogNear": 50, + "hoverTimeout": 0, + "impostor": true, + "lightColor": 14540253, + "lightIntensity": 1, + "mousePreset": "default", + "panSpeed": 1, + "quality": "medium", + "rotateSpeed": 2, + "sampleLevel": 0, + "tooltip": true, + "workerDefault": true, + "zoomSpeed": 1.2 + }, + "_ngl_full_stage_parameters_embed": {}, + "_ngl_msg_archive": [], + "_ngl_original_stage_parameters": { + "ambientColor": 14540253, + "ambientIntensity": 0.2, + "backgroundColor": "white", + "cameraFov": 40, + "cameraType": "perspective", + "clipDist": 10, + "clipFar": 100, + "clipNear": 0, + "fogFar": 100, + "fogNear": 50, + "hoverTimeout": 0, + "impostor": true, + "lightColor": 14540253, + "lightIntensity": 1, + "mousePreset": "default", + "panSpeed": 1, + "quality": "medium", + "rotateSpeed": 2, + "sampleLevel": 0, + "tooltip": true, + "workerDefault": true, + "zoomSpeed": 1.2 + }, + "_ngl_repr_dict": { + "0": { + "0": { + "params": { + "aspectRatio": 2, + "assembly": "default", + "bondScale": 0.4, + "bondSpacing": 1, + "clipCenter": { + "x": 0, + "y": 0, + "z": 0 + }, + "clipNear": 0, + "clipRadius": 0, + "colorMode": "hcl", + "colorReverse": false, + "colorScale": "", + "colorScheme": "element", + "colorValue": 9474192, + "cylinderOnly": false, + "defaultAssembly": "", + "depthWrite": true, + "diffuse": 16777215, + "disableImpostor": false, + "disablePicking": false, + "flatShaded": false, + "lazy": false, + "lineOnly": false, + "linewidth": 2, + "matrix": { + "elements": [ + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1 + ] + }, + "metalness": 0, + "multipleBond": "off", + "opacity": 1, + "openEnded": true, + "quality": "medium", + "radialSegments": 10, + "radius": 0.15, + "roughness": 0.4, + "scale": 1, + "sele": "all", + "side": "double", + "sphereDetail": 1, + "visible": true, + "wireframe": false + }, + "type": "ball+stick" + } + } + }, + "_ngl_serialize": false, + "_ngl_version": "1.0.0-beta.4", + "_scene_position": {}, + "_scene_rotation": {}, + "background": "white", + "count": 101, + "frame": 50, + "layout": "IPY_MODEL_a41caba466c94b7e8d8f7061dcc1c7a7", + "n_components": 1, + "picked": {} + } + }, + "8ac9a3d696a246d38d7859788311f6f2": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": {} + }, + "8b61aba639974bdeb2e63e99aaab8f29": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.0.0", + "model_name": "HBoxModel", + "state": { + "children": [ + "IPY_MODEL_cbaf382c92d8485892ea4bd1ff093eb7", + "IPY_MODEL_dc6fbb5ce3d44e6baf4fb3e68232ea8a" + ], + "layout": "IPY_MODEL_b199ab3bb19c47f6825419e9a73f7afe" + } + }, + "8baafb4e8bb64a17b429fd94923253a3": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": {} + }, + "8db875f339984f819fa6a20484ed64d2": { + "model_module": "nglview-js-widgets", + "model_module_version": "0.5.4-dev.27", + "model_name": "NGLModel", + "state": { + "_camera_orientation": [], + "_camera_str": "orthographic", + "_image_data": "", + "_n_dragged_files": 0, + "_ngl_coordinate_resource": {}, + "_ngl_full_stage_parameters": {}, + "_ngl_full_stage_parameters_embed": {}, + "_ngl_msg_archive": [], + "_ngl_original_stage_parameters": {}, + "_ngl_repr_dict": {}, + "_ngl_serialize": false, + "_ngl_version": "", + "_scene_position": {}, + "_scene_rotation": {}, + "background": "white", + "count": 101, + "frame": 0, + "layout": "IPY_MODEL_31b91051b3ec49cda634bde2e7c0d92f", + "n_components": 0, + "picked": {} + } + }, + "8e24b4b7e987488193788f311bbbd1d4": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.0.0", + "model_name": "VBoxModel", + "state": { + "children": [ + "IPY_MODEL_057a8469d4854abda77edc709739ca6d", + "IPY_MODEL_4598d04e9e5f4177980ce355c95007e2" + ], + "layout": "IPY_MODEL_a00ebdef56f94a2e99f3b224f274a4f1" + } + }, + "9399b24089f54498a9e100a5e810ac82": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": {} + }, + "952e7c4f72be4ad78e081ef9ca637c2a": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": {} + }, + "981cfe953700402498eeec039f102f4a": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": {} + }, + "9cbc32d8ba4141a3b1aeae20544d8520": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": {} + }, + "9e26a9b8cefa4edd8ad1316037e76303": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": {} + }, + "9f9e073074c343dd8b5b37995b65d30a": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.0.0", + "model_name": "HBoxModel", + "state": { + "children": [ + "IPY_MODEL_de0e2051dd0c46068cdb8c3903b7d4f5", + "IPY_MODEL_d2b7ae0272eb4bc1afd3e11566d20589" + ], + "layout": "IPY_MODEL_75807289a5e64c8eb4e8179d222dfc8f" + } + }, + "9fd08b869f7a4dfda172448bfe845f6c": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": { + "border": "solid" + } + }, + "a00ebdef56f94a2e99f3b224f274a4f1": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": {} + }, + "a0d945adcbde4207a8dfe8d9b9f371e2": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": {} + }, + "a0e170719f6c48618e49d034ea1060d0": { + "model_module": "nglview-js-widgets", + "model_module_version": "0.5.4-dev.27", + "model_name": "NGLModel", + "state": { + "_camera_orientation": [ + 20.345096809765252, + 0, + 0, + 0, + 0, + 20.345096809765252, + 0, + 0, + 0, + 0, + 20.345096809765252, + 0, + -1.6269999742507935, + -24.015000343322754, + -0.9159999787807465, + 1 + ], + "_camera_str": "orthographic", + "_image_data": "", + "_n_dragged_files": 0, + "_ngl_coordinate_resource": {}, + "_ngl_full_stage_parameters": { + "ambientColor": 14540253, + "ambientIntensity": 0.2, + "backgroundColor": "white", + "cameraFov": 40, + "cameraType": "perspective", + "clipDist": 10, + "clipFar": 100, + "clipNear": 0, + "fogFar": 100, + "fogNear": 50, + "hoverTimeout": 0, + "impostor": true, + "lightColor": 14540253, + "lightIntensity": 1, + "mousePreset": "default", + "panSpeed": 1, + "quality": "medium", + "rotateSpeed": 2, + "sampleLevel": 0, + "tooltip": true, + "workerDefault": true, + "zoomSpeed": 1.2 + }, + "_ngl_full_stage_parameters_embed": {}, + "_ngl_msg_archive": [], + "_ngl_original_stage_parameters": { + "ambientColor": 14540253, + "ambientIntensity": 0.2, + "backgroundColor": "white", + "cameraFov": 40, + "cameraType": "perspective", + "clipDist": 10, + "clipFar": 100, + "clipNear": 0, + "fogFar": 100, + "fogNear": 50, + "hoverTimeout": 0, + "impostor": true, + "lightColor": 14540253, + "lightIntensity": 1, + "mousePreset": "default", + "panSpeed": 1, + "quality": "medium", + "rotateSpeed": 2, + "sampleLevel": 0, + "tooltip": true, + "workerDefault": true, + "zoomSpeed": 1.2 + }, + "_ngl_repr_dict": { + "0": { + "0": { + "params": { + "aspectRatio": 2, + "assembly": "default", + "bondScale": 0.4, + "bondSpacing": 1, + "clipCenter": { + "x": 0, + "y": 0, + "z": 0 + }, + "clipNear": 0, + "clipRadius": 0, + "colorMode": "hcl", + "colorReverse": false, + "colorScale": "", + "colorScheme": "element", + "colorValue": 9474192, + "cylinderOnly": false, + "defaultAssembly": "", + "depthWrite": true, + "diffuse": 16777215, + "disableImpostor": false, + "disablePicking": false, + "flatShaded": false, + "lazy": false, + "lineOnly": false, + "linewidth": 2, + "matrix": { + "elements": [ + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1 + ] + }, + "metalness": 0, + "multipleBond": "off", + "opacity": 1, + "openEnded": true, + "quality": "medium", + "radialSegments": 10, + "radius": 0.15, + "roughness": 0.4, + "scale": 1, + "sele": "all", + "side": "double", + "sphereDetail": 1, + "visible": true, + "wireframe": false + }, + "type": "ball+stick" + } + } + }, + "_ngl_serialize": false, + "_ngl_version": "1.0.0-beta.4", + "_scene_position": {}, + "_scene_rotation": {}, + "background": "white", + "count": 101, + "frame": 0, + "layout": "IPY_MODEL_c3e93c813db24913aee9e7d610ccfe14", + "n_components": 1, + "picked": {} + } + }, + "a41caba466c94b7e8d8f7061dcc1c7a7": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": {} + }, + "a61b2b49969448c0a122dc31812b203c": { + "model_module": "nglview-js-widgets", + "model_module_version": "0.5.4-dev.27", + "model_name": "NGLModel", + "state": { + "_camera_orientation": [ + 20.24317995028008, + 0, + 0, + 0, + 0, + 20.24317995028008, + 0, + 0, + 0, + 0, + 20.24317995028008, + 0, + -1.6150000393390656, + -24.236000061035156, + -0.6120000183582306, + 1 + ], + "_camera_str": "orthographic", + "_image_data": "", + "_n_dragged_files": 0, + "_ngl_coordinate_resource": {}, + "_ngl_full_stage_parameters": { + "ambientColor": 14540253, + "ambientIntensity": 0.2, + "backgroundColor": "white", + "cameraFov": 40, + "cameraType": "perspective", + "clipDist": 10, + "clipFar": 100, + "clipNear": 0, + "fogFar": 100, + "fogNear": 50, + "hoverTimeout": 0, + "impostor": true, + "lightColor": 14540253, + "lightIntensity": 1, + "mousePreset": "default", + "panSpeed": 1, + "quality": "medium", + "rotateSpeed": 2, + "sampleLevel": 0, + "tooltip": true, + "workerDefault": true, + "zoomSpeed": 1.2 + }, + "_ngl_full_stage_parameters_embed": {}, + "_ngl_msg_archive": [], + "_ngl_original_stage_parameters": { + "ambientColor": 14540253, + "ambientIntensity": 0.2, + "backgroundColor": "white", + "cameraFov": 40, + "cameraType": "perspective", + "clipDist": 10, + "clipFar": 100, + "clipNear": 0, + "fogFar": 100, + "fogNear": 50, + "hoverTimeout": 0, + "impostor": true, + "lightColor": 14540253, + "lightIntensity": 1, + "mousePreset": "default", + "panSpeed": 1, + "quality": "medium", + "rotateSpeed": 2, + "sampleLevel": 0, + "tooltip": true, + "workerDefault": true, + "zoomSpeed": 1.2 + }, + "_ngl_repr_dict": { + "0": { + "0": { + "params": { + "aspectRatio": 2, + "assembly": "default", + "bondScale": 0.4, + "bondSpacing": 1, + "clipCenter": { + "x": 0, + "y": 0, + "z": 0 + }, + "clipNear": 0, + "clipRadius": 0, + "colorMode": "hcl", + "colorReverse": false, + "colorScale": "", + "colorScheme": "element", + "colorValue": 9474192, + "cylinderOnly": false, + "defaultAssembly": "", + "depthWrite": true, + "diffuse": 16777215, + "disableImpostor": false, + "disablePicking": false, + "flatShaded": false, + "lazy": false, + "lineOnly": false, + "linewidth": 2, + "matrix": { + "elements": [ + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1 + ] + }, + "metalness": 0, + "multipleBond": "off", + "opacity": 1, + "openEnded": true, + "quality": "medium", + "radialSegments": 10, + "radius": 0.15, + "roughness": 0.4, + "scale": 1, + "sele": "all", + "side": "double", + "sphereDetail": 1, + "visible": true, + "wireframe": false + }, + "type": "ball+stick" + } + } + }, + "_ngl_serialize": false, + "_ngl_version": "1.0.0-beta.4", + "_scene_position": {}, + "_scene_rotation": {}, + "background": "white", + "count": 101, + "frame": 0, + "layout": "IPY_MODEL_526cafeb1ce042c1bce2614988570e74", + "n_components": 1, + "picked": {} + } + }, + "aaee598a144e485cad85e2db0dcc99a0": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.0.0", + "model_name": "ImageModel", + "state": { + "layout": "IPY_MODEL_142df587b15c41bf9b90c1c858cf5c9f", + "value": {}, + "width": "900.0" + } + }, + "adaf51e6a3fc417e914db69ac89b336c": { + "model_module": "jupyter-matplotlib", + "model_module_version": "^0.0.2", + "model_name": "MPLCanvasModel", + "state": { + "_dom_classes": [], + "_id": "", + "_toolbar_items": [ + [ + "Home", + "Reset original view", + "fa fa-home icon-home", + "home" + ], + [ + "Back", + "Back to previous view", + "fa fa-arrow-left icon-arrow-left", + "back" + ], + [ + "Forward", + "Forward to next view", + "fa fa-arrow-right icon-arrow-right", + "forward" + ], + [ + "", + "", + "", + "" + ], + [ + "Pan", + "Pan axes with left mouse, zoom with right", + "fa fa-arrows icon-move", + "pan" + ], + [ + "Zoom", + "Zoom to rectangle", + "fa fa-square-o icon-check-empty", + "zoom" + ], + [ + "", + "", + "", + "" + ], + [ + "Download", + "Download plot", + "fa fa-floppy-o icon-save", + "download" + ], + [ + "Export", + "Export plot", + "fa fa-file-picture-o icon-picture", + "export" + ] + ], + "layout": "IPY_MODEL_4e2d7923d0e34c7fbf272cab480aabc7" + } + }, + "adcc3be2d57d4f748750a6a3e683d26e": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.0.0", + "model_name": "HBoxModel", + "state": { + "children": [ + "IPY_MODEL_a0e170719f6c48618e49d034ea1060d0", + "IPY_MODEL_25fa5915d3d24a82a6f4ed61ba9bd7e2" + ], + "layout": "IPY_MODEL_091aba9a11cc45879ac9e3936cdbf397" + } + }, + "b199ab3bb19c47f6825419e9a73f7afe": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": {} + }, + "b5bebd1b82d74352a5ec74715925d0ce": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.0.0", + "model_name": "ImageModel", + "state": { + "layout": "IPY_MODEL_80c7306fd5394a8180989ee6d1421468", + "value": {}, + "width": "900.0" + } + }, + "b6ac057b617e49a4a490ee59ed74328e": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": {} + }, + "b84977b422434764b1c596cb0d78791b": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.0.0", + "model_name": "ImageModel", + "state": { + "layout": "IPY_MODEL_ea2bec8bc1ba4d27b1b13f38b06799b7", + "value": {}, + "width": "900.0" + } + }, + "bc01ad74a4cc4f239988a261d2d550b2": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": {} + }, + "bd2f9d6498f74a9bbe67da0a7b493466": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.0.0", + "model_name": "ButtonModel", + "state": { + "description": "NGL widgets", + "layout": "IPY_MODEL_7c45ca81b410488dad1921ef9e3cd666", + "style": "IPY_MODEL_bf0a3c2e2c4843cf88b8805c872ca3b5" + } + }, + "bed98b43d9c343dab002b228ba726822": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": {} + }, + "bf0a3c2e2c4843cf88b8805c872ca3b5": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.0.0", + "model_name": "ButtonStyleModel", + "state": {} + }, + "c08e3c0d48a848048a9eddb863b63b9b": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": {} + }, + "c30dcd93e9fc4fc780101d81fc128c10": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": {} + }, + "c3e93c813db24913aee9e7d610ccfe14": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": {} + }, + "c492ce1d58cd4004a095f6c0aefd7d04": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": {} + }, + "c6030751df7c46a2b1ba0b1e303c79cd": { + "model_module": "jupyter-matplotlib", + "model_module_version": "^0.0.2", + "model_name": "MPLCanvasModel", + "state": { + "_dom_classes": [], + "_id": "", + "_toolbar_items": [ + [ + "Home", + "Reset original view", + "fa fa-home icon-home", + "home" + ], + [ + "Back", + "Back to previous view", + "fa fa-arrow-left icon-arrow-left", + "back" + ], + [ + "Forward", + "Forward to next view", + "fa fa-arrow-right icon-arrow-right", + "forward" + ], + [ + "", + "", + "", + "" + ], + [ + "Pan", + "Pan axes with left mouse, zoom with right", + "fa fa-arrows icon-move", + "pan" + ], + [ + "Zoom", + "Zoom to rectangle", + "fa fa-square-o icon-check-empty", + "zoom" + ], + [ + "", + "", + "", + "" + ], + [ + "Download", + "Download plot", + "fa fa-floppy-o icon-save", + "download" + ], + [ + "Export", + "Export plot", + "fa fa-file-picture-o icon-picture", + "export" + ] + ], + "layout": "IPY_MODEL_4224777922c14f31894db65575a86dca" + } + }, + "c95ca5fe005344588b3a1100a65530a2": { + "model_module": "nglview-js-widgets", + "model_module_version": "0.5.4-dev.27", + "model_name": "NGLModel", + "state": { + "_camera_orientation": [ + 23.053436912758006, + 0, + 0, + 0, + 0, + 23.053436912758006, + 0, + 0, + 0, + 0, + 23.053436912758006, + 0, + -1.7549998760223389, + -24.3100004196167, + -0.7849999666213989, + 1 + ], + "_camera_str": "orthographic", + "_image_data": "", + "_n_dragged_files": 0, + "_ngl_coordinate_resource": {}, + "_ngl_full_stage_parameters": { + "ambientColor": 14540253, + "ambientIntensity": 0.2, + "backgroundColor": "white", + "cameraFov": 40, + "cameraType": "perspective", + "clipDist": 10, + "clipFar": 100, + "clipNear": 0, + "fogFar": 100, + "fogNear": 50, + "hoverTimeout": 0, + "impostor": true, + "lightColor": 14540253, + "lightIntensity": 1, + "mousePreset": "default", + "panSpeed": 1, + "quality": "medium", + "rotateSpeed": 2, + "sampleLevel": 0, + "tooltip": true, + "workerDefault": true, + "zoomSpeed": 1.2 + }, + "_ngl_full_stage_parameters_embed": {}, + "_ngl_msg_archive": [], + "_ngl_original_stage_parameters": { + "ambientColor": 14540253, + "ambientIntensity": 0.2, + "backgroundColor": "white", + "cameraFov": 40, + "cameraType": "perspective", + "clipDist": 10, + "clipFar": 100, + "clipNear": 0, + "fogFar": 100, + "fogNear": 50, + "hoverTimeout": 0, + "impostor": true, + "lightColor": 14540253, + "lightIntensity": 1, + "mousePreset": "default", + "panSpeed": 1, + "quality": "medium", + "rotateSpeed": 2, + "sampleLevel": 0, + "tooltip": true, + "workerDefault": true, + "zoomSpeed": 1.2 + }, + "_ngl_repr_dict": { + "0": { + "0": { + "params": { + "aspectRatio": 2, + "assembly": "default", + "bondScale": 0.4, + "bondSpacing": 1, + "clipCenter": { + "x": 0, + "y": 0, + "z": 0 + }, + "clipNear": 0, + "clipRadius": 0, + "colorMode": "hcl", + "colorReverse": false, + "colorScale": "", + "colorScheme": "element", + "colorValue": 9474192, + "cylinderOnly": false, + "defaultAssembly": "", + "depthWrite": true, + "diffuse": 16777215, + "disableImpostor": false, + "disablePicking": false, + "flatShaded": false, + "lazy": false, + "lineOnly": false, + "linewidth": 2, + "matrix": { + "elements": [ + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1 + ] + }, + "metalness": 0, + "multipleBond": "off", + "opacity": 1, + "openEnded": true, + "quality": "medium", + "radialSegments": 10, + "radius": 0.15, + "roughness": 0.4, + "scale": 1, + "sele": "all", + "side": "double", + "sphereDetail": 1, + "visible": true, + "wireframe": false + }, + "type": "ball+stick" + } + } + }, + "_ngl_serialize": false, + "_ngl_version": "1.0.0-beta.4", + "_scene_position": {}, + "_scene_rotation": {}, + "background": "white", + "count": 3334, + "frame": 0, + "layout": "IPY_MODEL_bed98b43d9c343dab002b228ba726822", + "n_components": 1, + "picked": {} + } + }, + "ca6b587f4bdf4a31ac25c181de986d10": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": {} + }, + "cb2ce9cb14d74e6eba74aa41e3d3902c": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": {} + }, + "cbaf382c92d8485892ea4bd1ff093eb7": { + "model_module": "nglview-js-widgets", + "model_module_version": "0.5.4-dev.27", + "model_name": "NGLModel", + "state": { + "_camera_orientation": [ + 20.16727026960458, + 0, + 0, + 0, + 0, + 20.16727026960458, + 0, + 0, + 0, + 0, + 20.16727026960458, + 0, + -1.670500010251999, + -24.258000373840332, + -0.6459999978542328, + 1 + ], + "_camera_str": "orthographic", + "_image_data": "", + "_n_dragged_files": 0, + "_ngl_coordinate_resource": {}, + "_ngl_full_stage_parameters": { + "ambientColor": 14540253, + "ambientIntensity": 0.2, + "backgroundColor": "white", + "cameraFov": 40, + "cameraType": "perspective", + "clipDist": 10, + "clipFar": 100, + "clipNear": 0, + "fogFar": 100, + "fogNear": 50, + "hoverTimeout": 0, + "impostor": true, + "lightColor": 14540253, + "lightIntensity": 1, + "mousePreset": "default", + "panSpeed": 1, + "quality": "medium", + "rotateSpeed": 2, + "sampleLevel": 0, + "tooltip": true, + "workerDefault": true, + "zoomSpeed": 1.2 + }, + "_ngl_full_stage_parameters_embed": {}, + "_ngl_msg_archive": [], + "_ngl_original_stage_parameters": { + "ambientColor": 14540253, + "ambientIntensity": 0.2, + "backgroundColor": "white", + "cameraFov": 40, + "cameraType": "perspective", + "clipDist": 10, + "clipFar": 100, + "clipNear": 0, + "fogFar": 100, + "fogNear": 50, + "hoverTimeout": 0, + "impostor": true, + "lightColor": 14540253, + "lightIntensity": 1, + "mousePreset": "default", + "panSpeed": 1, + "quality": "medium", + "rotateSpeed": 2, + "sampleLevel": 0, + "tooltip": true, + "workerDefault": true, + "zoomSpeed": 1.2 + }, + "_ngl_repr_dict": { + "0": { + "0": { + "params": { + "aspectRatio": 2, + "assembly": "default", + "bondScale": 0.4, + "bondSpacing": 1, + "clipCenter": { + "x": 0, + "y": 0, + "z": 0 + }, + "clipNear": 0, + "clipRadius": 0, + "colorMode": "hcl", + "colorReverse": false, + "colorScale": "", + "colorScheme": "element", + "colorValue": 9474192, + "cylinderOnly": false, + "defaultAssembly": "", + "depthWrite": true, + "diffuse": 16777215, + "disableImpostor": false, + "disablePicking": false, + "flatShaded": false, + "lazy": false, + "lineOnly": false, + "linewidth": 2, + "matrix": { + "elements": [ + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1 + ] + }, + "metalness": 0, + "multipleBond": "off", + "opacity": 1, + "openEnded": true, + "quality": "medium", + "radialSegments": 10, + "radius": 0.15, + "roughness": 0.4, + "scale": 1, + "sele": "all", + "side": "double", + "sphereDetail": 1, + "visible": true, + "wireframe": false + }, + "type": "ball+stick" + } + } + }, + "_ngl_serialize": false, + "_ngl_version": "1.0.0-beta.4", + "_scene_position": {}, + "_scene_rotation": {}, + "background": "white", + "count": 101, + "frame": 99, + "layout": "IPY_MODEL_75a01831722d4f60b5ee7e5eeca054b7", + "n_components": 1, + "picked": {} + } + }, + "cbfb5bb0aa114fce88e90ae57fdd123f": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.0.0", + "model_name": "HBoxModel", + "state": { + "children": [ + "IPY_MODEL_8a9d4e5f727846c183272e24d1c9bd7b", + "IPY_MODEL_880fc32d8fae45ef8c48be9a054db4c4" + ], + "layout": "IPY_MODEL_5ce550eaef3f470a9d6e6142a846f97d" + } + }, + "d2b7ae0272eb4bc1afd3e11566d20589": { + "model_module": "jupyter-matplotlib", + "model_module_version": "^0.0.2", + "model_name": "MPLCanvasModel", + "state": { + "_dom_classes": [], + "_id": "", + "_toolbar_items": [ + [ + "Home", + "Reset original view", + "fa fa-home icon-home", + "home" + ], + [ + "Back", + "Back to previous view", + "fa fa-arrow-left icon-arrow-left", + "back" + ], + [ + "Forward", + "Forward to next view", + "fa fa-arrow-right icon-arrow-right", + "forward" + ], + [ + "", + "", + "", + "" + ], + [ + "Pan", + "Pan axes with left mouse, zoom with right", + "fa fa-arrows icon-move", + "pan" + ], + [ + "Zoom", + "Zoom to rectangle", + "fa fa-square-o icon-check-empty", + "zoom" + ], + [ + "", + "", + "", + "" + ], + [ + "Download", + "Download plot", + "fa fa-floppy-o icon-save", + "download" + ], + [ + "Export", + "Export plot", + "fa fa-file-picture-o icon-picture", + "export" + ] + ], + "layout": "IPY_MODEL_7311e30372fd4ad99110f011b8adb856" + } + }, + "d523002ca8a94346b19d51176c999b2f": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": {} + }, + "d5a1720d76c74ef6b4e1d337f268576f": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": {} + }, + "d5f60915434a422999c82949b45700a6": { + "model_module": "jupyter-matplotlib", + "model_module_version": "^0.0.2", + "model_name": "MPLCanvasModel", + "state": { + "_dom_classes": [], + "_id": "", + "_toolbar_items": [ + [ + "Home", + "Reset original view", + "fa fa-home icon-home", + "home" + ], + [ + "Back", + "Back to previous view", + "fa fa-arrow-left icon-arrow-left", + "back" + ], + [ + "Forward", + "Forward to next view", + "fa fa-arrow-right icon-arrow-right", + "forward" + ], + [ + "", + "", + "", + "" + ], + [ + "Pan", + "Pan axes with left mouse, zoom with right", + "fa fa-arrows icon-move", + "pan" + ], + [ + "Zoom", + "Zoom to rectangle", + "fa fa-square-o icon-check-empty", + "zoom" + ], + [ + "", + "", + "", + "" + ], + [ + "Download", + "Download plot", + "fa fa-floppy-o icon-save", + "download" + ], + [ + "Export", + "Export plot", + "fa fa-file-picture-o icon-picture", + "export" + ] + ], + "layout": "IPY_MODEL_bc01ad74a4cc4f239988a261d2d550b2" + } + }, + "d86f951d40324b92af3c0b2ff48a46a3": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.0.0", + "model_name": "ImageModel", + "state": { + "layout": "IPY_MODEL_8ac9a3d696a246d38d7859788311f6f2", + "value": {}, + "width": "900.0" + } + }, + "dc6fbb5ce3d44e6baf4fb3e68232ea8a": { + "model_module": "jupyter-matplotlib", + "model_module_version": "^0.0.2", + "model_name": "MPLCanvasModel", + "state": { + "_dom_classes": [], + "_id": "", + "_toolbar_items": [ + [ + "Home", + "Reset original view", + "fa fa-home icon-home", + "home" + ], + [ + "Back", + "Back to previous view", + "fa fa-arrow-left icon-arrow-left", + "back" + ], + [ + "Forward", + "Forward to next view", + "fa fa-arrow-right icon-arrow-right", + "forward" + ], + [ + "", + "", + "", + "" + ], + [ + "Pan", + "Pan axes with left mouse, zoom with right", + "fa fa-arrows icon-move", + "pan" + ], + [ + "Zoom", + "Zoom to rectangle", + "fa fa-square-o icon-check-empty", + "zoom" + ], + [ + "", + "", + "", + "" + ], + [ + "Download", + "Download plot", + "fa fa-floppy-o icon-save", + "download" + ], + [ + "Export", + "Export plot", + "fa fa-file-picture-o icon-picture", + "export" + ] + ], + "layout": "IPY_MODEL_f9b88113417c423099ee72fa0e4b961f" + } + }, + "de0e2051dd0c46068cdb8c3903b7d4f5": { + "model_module": "nglview-js-widgets", + "model_module_version": "0.5.4-dev.27", + "model_name": "NGLModel", + "state": { + "_camera_orientation": [ + 20.494602250547747, + 0, + 0, + 0, + 0, + 20.494602250547747, + 0, + 0, + 0, + 0, + 20.494602250547747, + 0, + -1.4594999849796295, + -24.00349998474121, + -0.5250000357627869, + 1 + ], + "_camera_str": "orthographic", + "_image_data": "", + "_n_dragged_files": 0, + "_ngl_coordinate_resource": {}, + "_ngl_full_stage_parameters": { + "ambientColor": 14540253, + "ambientIntensity": 0.2, + "backgroundColor": "white", + "cameraFov": 40, + "cameraType": "perspective", + "clipDist": 10, + "clipFar": 100, + "clipNear": 0, + "fogFar": 100, + "fogNear": 50, + "hoverTimeout": 0, + "impostor": true, + "lightColor": 14540253, + "lightIntensity": 1, + "mousePreset": "default", + "panSpeed": 1, + "quality": "medium", + "rotateSpeed": 2, + "sampleLevel": 0, + "tooltip": true, + "workerDefault": true, + "zoomSpeed": 1.2 + }, + "_ngl_full_stage_parameters_embed": {}, + "_ngl_msg_archive": [], + "_ngl_original_stage_parameters": { + "ambientColor": 14540253, + "ambientIntensity": 0.2, + "backgroundColor": "white", + "cameraFov": 40, + "cameraType": "perspective", + "clipDist": 10, + "clipFar": 100, + "clipNear": 0, + "fogFar": 100, + "fogNear": 50, + "hoverTimeout": 0, + "impostor": true, + "lightColor": 14540253, + "lightIntensity": 1, + "mousePreset": "default", + "panSpeed": 1, + "quality": "medium", + "rotateSpeed": 2, + "sampleLevel": 0, + "tooltip": true, + "workerDefault": true, + "zoomSpeed": 1.2 + }, + "_ngl_repr_dict": { + "0": { + "0": { + "params": { + "aspectRatio": 2, + "assembly": "default", + "bondScale": 0.4, + "bondSpacing": 1, + "clipCenter": { + "x": 0, + "y": 0, + "z": 0 + }, + "clipNear": 0, + "clipRadius": 0, + "colorMode": "hcl", + "colorReverse": false, + "colorScale": "", + "colorScheme": "element", + "colorValue": 9474192, + "cylinderOnly": false, + "defaultAssembly": "", + "depthWrite": true, + "diffuse": 16777215, + "disableImpostor": false, + "disablePicking": false, + "flatShaded": false, + "lazy": false, + "lineOnly": false, + "linewidth": 2, + "matrix": { + "elements": [ + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 1 + ] + }, + "metalness": 0, + "multipleBond": "off", + "opacity": 1, + "openEnded": true, + "quality": "medium", + "radialSegments": 10, + "radius": 0.15, + "roughness": 0.4, + "scale": 1, + "sele": "all", + "side": "double", + "sphereDetail": 1, + "visible": true, + "wireframe": false + }, + "type": "ball+stick" + } + } + }, + "_ngl_serialize": false, + "_ngl_version": "1.0.0-beta.4", + "_scene_position": {}, + "_scene_rotation": {}, + "background": "white", + "count": 101, + "frame": 0, + "layout": "IPY_MODEL_8baafb4e8bb64a17b429fd94923253a3", + "n_components": 1, + "picked": {} + } + }, + "e427834063234005be0f3c41f8f7d04e": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.0.0", + "model_name": "HBoxModel", + "state": { + "children": [ + "IPY_MODEL_4b8ff78d08724692bffc261489797bcd", + "IPY_MODEL_ef29a6fa673747908780bc8e8d877019" + ], + "layout": "IPY_MODEL_eabe68947cc9494cba4387ed0fdb84aa" + } + }, + "e5ff550df1784bc698a701c5fcad3490": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.0.0", + "model_name": "VBoxModel", + "state": { + "children": [ + "IPY_MODEL_67b93a07aeea405daccd6c55cf6ea5ef", + "IPY_MODEL_69ed4cec545c401b88f9dd9921011751" + ], + "layout": "IPY_MODEL_1c056b4b60a6442c918b60951d157199" + } + }, + "ea2bec8bc1ba4d27b1b13f38b06799b7": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": {} + }, + "eabe68947cc9494cba4387ed0fdb84aa": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": {} + }, + "ef29a6fa673747908780bc8e8d877019": { + "model_module": "jupyter-matplotlib", + "model_module_version": "^0.0.2", + "model_name": "MPLCanvasModel", + "state": { + "_dom_classes": [], + "_id": "", + "_toolbar_items": [ + [ + "Home", + "Reset original view", + "fa fa-home icon-home", + "home" + ], + [ + "Back", + "Back to previous view", + "fa fa-arrow-left icon-arrow-left", + "back" + ], + [ + "Forward", + "Forward to next view", + "fa fa-arrow-right icon-arrow-right", + "forward" + ], + [ + "", + "", + "", + "" + ], + [ + "Pan", + "Pan axes with left mouse, zoom with right", + "fa fa-arrows icon-move", + "pan" + ], + [ + "Zoom", + "Zoom to rectangle", + "fa fa-square-o icon-check-empty", + "zoom" + ], + [ + "", + "", + "", + "" + ], + [ + "Download", + "Download plot", + "fa fa-floppy-o icon-save", + "download" + ], + [ + "Export", + "Export plot", + "fa fa-file-picture-o icon-picture", + "export" + ] + ], + "layout": "IPY_MODEL_9cbc32d8ba4141a3b1aeae20544d8520" + } + }, + "ef6f87771fbe4759a4fa1ed6a1657388": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": {} + }, + "f06c86a748a7412890505f0152d6bd72": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.0.0", + "model_name": "ButtonModel", + "state": { + "description": "NGL widgets", + "layout": "IPY_MODEL_8a2c5b58347043848a48e53894cb7152", + "style": "IPY_MODEL_6775e7024caa458ea43e223a7fea355a" + } + }, + "f9b88113417c423099ee72fa0e4b961f": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.0.0", + "model_name": "LayoutModel", + "state": {} + }, + "fb9d851ef63e4813bc8dcb22dc7e0f4b": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.0.0", + "model_name": "VBoxModel", + "state": { + "children": [ + "IPY_MODEL_f06c86a748a7412890505f0152d6bd72", + "IPY_MODEL_02f12bd6d6504b0ba027f64e0cfde1b1" + ], + "layout": "IPY_MODEL_8931ccd404fc4f4c992e0ecd8b9337cc" + } + }, + "fc79888efdac446485d6ae522731d719": { + "model_module": "@jupyter-widgets/controls", + "model_module_version": "1.0.0", + "model_name": "ImageModel", + "state": { + "layout": "IPY_MODEL_1850213735f14c1cb59b177e30d1b5ec", + "value": {}, + "width": "900.0" + } + } + }, + "version_major": 2, + "version_minor": 0 + } + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} From 0864fc5cc8a2745f81496fca3f080af4e974c630 Mon Sep 17 00:00:00 2001 From: gph82 Date: Mon, 28 May 2018 18:00:06 +0200 Subject: [PATCH 66/73] [visualize] new method MSM --- molpx/_bmutils.py | 1 + molpx/notebooks/2.molPX_TICA_BPTI.ipynb | 1532 ----------------------- molpx/visualize.py | 157 ++- 3 files changed, 156 insertions(+), 1534 deletions(-) delete mode 100644 molpx/notebooks/2.molPX_TICA_BPTI.ipynb diff --git a/molpx/_bmutils.py b/molpx/_bmutils.py index 6afb0d3..20d5d3d 100644 --- a/molpx/_bmutils.py +++ b/molpx/_bmutils.py @@ -300,6 +300,7 @@ def inform(left, fl, right, fr, middle, fm, delta, eps, str0=''): return middle + def regspace_from_distance_matrix(D, dmin): r""" Return the indices idxs of the rows/columns of the symmetric matrix (D[idxs,idxs] > dmin).all() == True diff --git a/molpx/notebooks/2.molPX_TICA_BPTI.ipynb b/molpx/notebooks/2.molPX_TICA_BPTI.ipynb deleted file mode 100644 index d7ce265..0000000 --- a/molpx/notebooks/2.molPX_TICA_BPTI.ipynb +++ /dev/null @@ -1,1532 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# molPX intro\n", - "
 \n",
-    "Guillermo Perez-Hernandez  guille.perez@fu-berlin.de \n",
-    "
\n", - " \n", - "In this notebook we will be using the 1 millisecond trajectory of Bovine Pancreatic Trypsin Inhibitor (BPTI) generated by DE Shaw Research on the Anton Supercomputer and kindly made available by their lab. The original work is \n", - " \n", - " * Shaw DE, Maragakis P, Lindorff-Larsen K, Piana S, Dror RO, Eastwood MP, Bank JA, Jumper JM, Salmon JK, Shan Y, Wriggers W: Atomic-level characterization of the structural dynamics of proteins. Science 330:341-346 (2010). doi: 10.1126/science.1187409.\n", - " \n", - "The trajectory has been duplicated and shortened to provide a mock-trajectory set and be able to deal with lists of trajectories of different lenghts:\n", - "\n", - " * `c-alpha_centered.stride.100.xtc`\n", - " * `c-alpha_centered.stride.100.reversed.xtc`\n", - " * `c-alpha_centered.stride.100.halved.xtc`" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Input types and typical usecase\n", - "The typical usecase is having molecular dynamics (MD) simulation data in form of trajectory files with extensions like `.xtc, .dcd` etc and the associated molecular topology as a `.pdb` or `.gro` file. \n", - "\n", - "These files are the most general starting point for any analysis dealing with MD, and ```molpx```'s API has been designed to be able to function without further input:" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "top = 'notebooks/data/bpti-c-alpha_centered.pdb'\n", - "MD_trajfiles = ['notebooks/data/c-alpha_centered.stride.1000.xtc',\n", - " 'notebooks/data/c-alpha_centered.stride.1000.reversed.xtc',\n", - " 'notebooks/data/c-alpha_centered.stride.1000.halved.xtc'\n", - " ]\n", - "\n", - "dt = 244 #saving interval in the .xtc files, in ns\n", - "\n", - "import molpx\n", - "from matplotlib import pyplot as plt\n", - "import pyemma\n", - "import numpy as np\n", - "\n", - "# This way the user does not have to care where the data are:\n", - "top = molpx._molpxdir(top)\n", - "MD_trajfiles = [molpx._molpxdir(ff) for ff in MD_trajfiles]" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**However**, `molpx` relies heavily on the awesome [`mdtraj`](http://www.mdtraj.org) module for dealing with molecular structures, and so most of `molpx`'s functions accept also `Trajectory`-type objects (native to `mdtraj`) as alternative inputs. " - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "# Create a memory representation of the trajectories\n", - "MD_list = [molpx.generate._md.load(itraj, top=top) for itraj in MD_trajfiles]" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The same idea applies to the input of projected trajectories: `molpx` can take the filenames as inputs (`.npy`, `.dat`, `.txt` etc) or deal directly with `numpy.ndarray` objects. \n", - "\n", - "** These alternative, \"from-memory\" input modes (`md.Trajectory` and `np.ndarray` objects) avoid forcing the user to read from file everytime an API function is called, saving I/O overhead**\n", - "\n", - "The following cell either reads or generates projected trajectory files for this demonstration. In a real usecase this step (done here using TICA) might not be needed, given that the user might have generated the projected trajectory elsewhere:" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "# Perform TICA or read from file directly if already .npy-files exist\n", - "Y_filenames = [ff.replace('.xtc','.Y.npy') for ff in MD_trajfiles]\n", - "try: \n", - " Y = [np.load(ff) for ff in Y_filenames]\n", - "except:\n", - " feat = pyemma.coordinates.featurizer(top)\n", - " pairs = feat.pairs(range(feat.topology.n_atoms)[::2])\n", - " feat.add_distances(pairs)\n", - " src = pyemma.coordinates.source(MD_trajfiles, features=feat)\n", - " tica = pyemma.coordinates.tica(src, lag=10, dim=3)\n", - " Y = tica.get_output() \n", - " [np.save(ff, iY) for ff, iY in zip(Y_filenames, Y)]" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Visualize a FES and the molecular structures behind it\n", - "Execute the following cell and click either on the FES or on the slidebar. Some input parameters have been comented out for you to try out:\n", - " * different modes of input (disk vs memory) \n", - " * different projection indices\n", - " * different number of overlaid structures" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": { - "scrolled": false - }, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "0b77491e19f04670b371059496e0cc38", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "A Jupyter Widget" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "22-05-18 18:03:04 pyemma.coordinates.clustering.kmeans.KmeansClustering[0] INFO Algorithm did not reach convergence criterion of 1e-05 in 10 iterations. Consider increasing max_iter.\n", - "\r" - ] - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "2208cf551df64d2f873e203a818be74f", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "A Jupyter Widget" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\r" - ] - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "ec1e5f9519584f34a7c0412206756f41", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "A Jupyter Widget" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "mpx_wdg_box = molpx.visualize.FES(MD_list, \n", - " #MD_trajfiles, \n", - " top, \n", - " Y_filenames, \n", - " #Y, \n", - " nbins=50, \n", - " #proj_idxs=[1,2],\n", - " proj_labels='TIC',\n", - " #n_overlays=5,\n", - " )\n", - "mpx_wdg_box" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Visualize trajectories, FES and molecular structures\n", - "The user can sample structures as they occurr in sequence in the actual trajectory. Depending on the size of the dataset, this can be very time consuming, particularly if data is being read from disk. \n", - "\n", - "In this example, try changing `MD_trajfiles` to `MD_list` and/or changing `Y_filenames` to simply `Y` and see if it helps.\n", - "\n", - "Furthermore, the objects in memory can be strided down to fewer frames **before** being parsed to the method. To stride objects being read from the disk, use the `stride` parameter. \n", - "\n", - "### Again, we have commented out some parameters that provide more control on the output of `visualize.traj`. Uncomment them and see what happens" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": { - "scrolled": false - }, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "5ffb7ac441bf48709f98d7dac4599d36", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "A Jupyter Widget" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\r" - ] - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "405cec954136493cb7957e2b4dfe6d13", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "A Jupyter Widget" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "22-05-18 17:20:44 pyemma.coordinates.clustering.kmeans.KmeansClustering[10] INFO Cluster centers converged after 10 steps.\n", - "\r" - ] - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "aa977b08505946eebcd4fd22a6b6e4ff", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "A Jupyter Widget" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\r" - ] - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "aef9c92d135c4203901d394e5a041d46", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "A Jupyter Widget" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "mpx_wdg_box = molpx.visualize.traj(MD_trajfiles, \n", - " top, \n", - " Y,\n", - " #Y_filenames, \n", - " plot_FES = True, \n", - " dt = dt*1e-6, tunits='ms', \n", - " #traj_selection = 1,\n", - " #sharey_traj=False,\n", - " #max_frames=100,\n", - " proj_idxs=[0, 1],\n", - " panel_height=2, \n", - " #proj_labels='TIC'\n", - " )\n", - "mpx_wdg_box" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Intermediate steps: using molpx to generate a regspace sample of the data\n", - "See the documentation of `molpx.generate.sample` to find out about all possible options:\n", - "```\n", - "molpx.generate.sample(MD_trajectories, MD_top, projected_trajectories, atom_selection=None, proj_idxs=[0, 1], n_points=100, n_geom_samples=1, keep_all_samples=False, proj_stride=1, verbose=False, return_data=False)\n", - "```" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "d9a0e4abb76a4f0d86558efdcb5da2aa", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "A Jupyter Widget" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\r" - ] - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "5c239027c95d4de18c7b2b400cc517ad", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "A Jupyter Widget" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "22-05-18 17:20:57 pyemma.coordinates.clustering.kmeans.KmeansClustering[21] INFO Algorithm did not reach convergence criterion of 1e-05 in 10 iterations. Consider increasing max_iter.\n", - "\r" - ] - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "6ec79eb470234baa8dd28d900ca509dd", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "A Jupyter Widget" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\r" - ] - }, - { - "data": { - "text/plain": [ - "((203, 2),\n", - " )" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "data_sample, geoms = molpx.generate.sample(#MD_list, \n", - " MD_trajfiles, \n", - " top, \n", - " #Y, \n", - " Y_filenames,\n", - " n_points=200 ,\n", - " n_geom_samples=2,\n", - " )\n", - "data_sample.shape, geoms\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Link the PDF plot with the sampled structures and visually explore the FES \n", - "Click either on the plot or on the widget slidebar: they're connected! " - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": { - "scrolled": false - }, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "6e9a1a3b366d4009938e5b8bbd6c99f8", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "A Jupyter Widget" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/guille/miniconda3/lib/python3.6/site-packages/ipykernel_launcher.py:4: RuntimeWarning: divide by zero encountered in log\n", - " after removing the cwd from sys.path.\n" - ] - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "c187e924b3ca4299af716d3fbeec9888", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "A Jupyter Widget" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# Replot the FES\n", - "plt.figure(figsize=(7,7))\n", - "h, (x,y) = np.histogramdd(np.vstack(Y)[:,:2], bins=50)\n", - "plt.contourf(x[:-1], y[:-1], -np.log(h.T), alpha=.50)\n", - "# Create the linked widget\n", - "linked_ngl_wdg, linked_ax_wdg = molpx.visualize.sample(data_sample, \n", - " geoms.superpose(geoms[0]), \n", - " plt.gca(), \n", - " clear_lines=True,\n", - " #plot_path=True\n", - " )\n", - "plt.plot(data_sample[:,0], data_sample[:,1],' ok', zorder=0)\n", - "# Show it\n", - "linked_ngl_wdg\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Paths samples along the different projections (=axis)" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "10a96c7344a2449e96328205defaf337", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "A Jupyter Widget" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\r" - ] - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "c97daf3e69c54ad79f176df535278b96", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "A Jupyter Widget" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "22-05-18 17:21:43 pyemma.coordinates.clustering.kmeans.KmeansClustering[32] INFO Algorithm did not reach convergence criterion of 1e-05 in 10 iterations. Consider increasing max_iter.\n", - "\r" - ] - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "e66eaf85903d467db89d3a85c3f17f39", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "A Jupyter Widget" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\r" - ] - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "59a165f8c8c046e98c8b9e6f4db0fdaf", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "A Jupyter Widget" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "22-05-18 17:21:59 pyemma.coordinates.clustering.kmeans.KmeansClustering[39] INFO Algorithm did not reach convergence criterion of 1e-05 in 10 iterations. Consider increasing max_iter.\n", - "\r" - ] - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "10f74903472447298d4ea2e3e0052ed9", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "A Jupyter Widget" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\r" - ] - } - ], - "source": [ - "paths_dict, idata = molpx.generate.projection_paths(#MD_list, \n", - " MD_trajfiles, \n", - " top, \n", - " Y_filenames,\n", - " #Y, # You can also directly give the data here\n", - " n_points=50,\n", - " proj_idxs=[0,1],\n", - " n_projs=3,\n", - " proj_dim = 3, \n", - " verbose=False, \n", - " )" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Link the PDF plot with the sampled paths/structures and visually explore the coordinates (separately). \n", - "Click either on the plot or on the widget slidebar: they're connected! You can change the type of path between min_rmsd or min_disp and you can also change the coordinate sampled (0 or 1)\n" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "# Choose the coordinate and the tyep of path\n", - "coord = 1\n", - "#path_type = 'min_rmsd'\n", - "path_type = 'min_disp'\n", - "igeom = paths_dict[coord][path_type][\"geom\"]\n", - "ipath = paths_dict[coord][path_type][\"proj\"]\n", - "\n", - "# Choose the proj_idxs for the path and the FES \n", - "# to be shown\n", - "proj_idxs = [0,1]" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": { - "scrolled": false - }, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "8da72c47d150404893178357e21a5661", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "A Jupyter Widget" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/guille/miniconda3/lib/python3.6/site-packages/ipykernel_launcher.py:3: RuntimeWarning: divide by zero encountered in log\n", - " This is separate from the ipykernel package so we can avoid doing imports until\n" - ] - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "6c1cbdc8faeb44b189ec19d23a1c5a35", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "A Jupyter Widget" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "plt.figure(figsize=(7,7))\n", - "h, (x,y) = np.histogramdd(np.vstack(Y)[:,proj_idxs], bins=50)\n", - "plt.contourf(x[:-1], y[:-1], -np.log(h.T), alpha=.50)\n", - "\n", - "linked_ngl_wdg, linked_ax_wdg = molpx.visualize.sample(ipath[:,proj_idxs], \n", - " igeom.superpose(igeom[0]), \n", - " plt.gca(), \n", - " clear_lines=True,\n", - " n_smooth = 5, \n", - " plot_path=True, \n", - " #radius=True,\n", - " )\n", - "linked_ngl_wdg" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Interaction with ```PyEMMA```\n", - "`molpx` is using many methods of the `coordinates` submodule of `PyEMMA`, and thus it also understands some of `PyEMMA`'s classes as input (like clustering objects or streaming transformers).\n", - "## Using the TICA object to visualize the most correlated input features\n", - "If the projected coordinates come from a TICA (or PCA) transformation, and the TICA object is available in memory\n", - "`molpx.visualize.traj` can make use of correlation information to display not only the projected coordinates (i.e the TICs, in this case), but also the \"original\" input features behind it" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "c5fdccaadba94e1c9cf2ab7376f4858c", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "A Jupyter Widget" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\r" - ] - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "53f8546c8e624873b1b5b2ac804867c9", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "A Jupyter Widget" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\r" - ] - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "db24ba31fd1f4055a80d24e19521a4f5", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "A Jupyter Widget" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\r" - ] - } - ], - "source": [ - "# Re-do the TICA computation to make sure we have a tica object in memory\n", - "feat = pyemma.coordinates.featurizer(top)\n", - "pairs = feat.pairs(range(feat.topology.n_atoms)[::2])\n", - "feat.add_distances(pairs)\n", - "src = pyemma.coordinates.source(MD_trajfiles, features=feat)\n", - "tica = pyemma.coordinates.tica(src, lag=10, dim=3)\n", - "Y = tica.get_output() " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The method `molpx.visualize.correlations` tries to provide a visual representation of the projected coordinates by relating them to the input features, which carry more meaning, since they are (usually) familiar parameters such as atom distances, angles, contacts etc." - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "0fba560f9aba4f2ab61d4249acf4d874", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "A Jupyter Widget" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# Comment or uncomment the optinal parameters and see how the method reacts\n", - "# You can use a pre-instantiated the widget\n", - "#iwd = molpx.visualize._nglwidget_wrapper(MD_list[0][0])\n", - "# Or instantiate at the moment of calling visualize.correlations\n", - "iwd = None\n", - "corr, ngl_wdg = molpx.visualize.correlations(tica, \n", - " n_feats=3, \n", - " proj_idxs=[0,1,2], \n", - " geoms=MD_list[0][::100],\n", - " #verbose=True,\n", - " #proj_color_list=['red', 'blue', 'green'],\n", - " widget=iwd\n", - " )\n", - "ngl_wdg" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Use the correlation-dictionary's modified print function to see what's inside in a human-friendly way" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Correlation dictionary for 3 projections\n", - " Corr[proj_0|feat] = 0.8\n", - " DIST: PRO 9 CA 8 - TYR 23 CA 22\n", - " feat nr. 112, atom idxs [ 8 22]\n", - " Corr[proj_0|feat] = 0.7\n", - " DIST: PRO 9 CA 8 - TYR 21 CA 20\n", - " feat nr. 111, atom idxs [ 8 20]\n", - " Corr[proj_0|feat] = 0.7\n", - " DIST: PRO 9 CA 8 - LEU 29 CA 28\n", - " feat nr. 115, atom idxs [ 8 28]\n", - "\n", - " Corr[proj_1|feat] = 0.7\n", - " DIST: PRO 9 CA 8 - LYS 15 CA 14\n", - " feat nr. 108, atom idxs [ 8 14]\n", - " Corr[proj_1|feat] = 0.7\n", - " DIST: LYS 15 CA 14 - TYR 23 CA 22\n", - " feat nr. 178, atom idxs [14 22]\n", - " Corr[proj_1|feat] = 0.6\n", - " DIST: LYS 15 CA 14 - PHE 33 CA 32\n", - " feat nr. 183, atom idxs [14 32]\n", - "\n", - " Corr[proj_2|feat] = 0.6\n", - " DIST: THR 11 CA 10 - GLY 37 CA 36\n", - " feat nr. 142, atom idxs [10 36]\n", - " Corr[proj_2|feat] = -0.6\n", - " DIST: PRO 13 CA 12 - GLN 31 CA 30\n", - " feat nr. 161, atom idxs [12 30]\n", - " Corr[proj_2|feat] = -0.6\n", - " DIST: PRO 13 CA 12 - PHE 33 CA 32\n", - " feat nr. 162, atom idxs [12 32]\n", - "\n", - "\n" - ] - } - ], - "source": [ - "print(corr)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Also, `molpx.visualize.traj` can help in visualizing these correlations by parsing along the tica object itself as `projection=tica`. In the next cell, can you spot the differences:\n", - "* In the nglwidget?\n", - "* In the trajectories?\n" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": { - "scrolled": false - }, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "58feb0216a474eb693598a0ca927afad", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "A Jupyter Widget" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\r" - ] - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "9ad92a6e3d5d4282a19c6f93fc018679", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "A Jupyter Widget" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "22-05-18 17:24:51 pyemma.coordinates.clustering.kmeans.KmeansClustering[49] INFO Cluster centers converged after 9 steps.\n", - "\r" - ] - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "c056a0610c1e48a3a49c5eca88855169", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "A Jupyter Widget" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\r" - ] - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "59ebe80215da48b8ab9003655c871357", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "A Jupyter Widget" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# Reuse the visualize.traj method with the tica object as input\n", - "mpx_wdg_box = molpx.visualize.traj(MD_trajfiles, \n", - " top, \n", - " Y,\n", - " #Y_filenames, \n", - " plot_FES = True, \n", - " dt = dt*1e-6, tunits='ms', \n", - " #traj_selection = 0,\n", - " #sharey_traj=False,\n", - " #max_frames=100,\n", - " proj_idxs=[0,1], \n", - " panel_height=1,\n", - " projection=tica, ## this is what's new\n", - " n_feats=2\n", - " )\n", - " \n", - "mpx_wdg_box" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Use a clustering object as input \n", - "If the dataset has already been clustered, and it is **that** clustering that the user wants to explore, `molpx.generate.sample` can take this clustering object as an input instead of the \n", - "the projected trajectories:" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "cb734de2d8a44e2fa02c29faed3e6566", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "A Jupyter Widget" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "fd3f03575d824ee78a069e12801e72ea", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "A Jupyter Widget" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "22-05-18 17:25:14 pyemma.coordinates.clustering.kmeans.KmeansClustering[58] INFO Cluster centers converged after 9 steps.\n", - "\r" - ] - } - ], - "source": [ - "# Do \"some\" clustering\n", - "clkmeans = pyemma.coordinates.cluster_kmeans([iY[:,:2] for iY in Y], 5)" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "be64e27733ff4c1cb59f9d990fe1b80b", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "A Jupyter Widget" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\r" - ] - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "8fa448a08c3e486682c81d82a8bb14f4", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "A Jupyter Widget" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\r" - ] - } - ], - "source": [ - "data_sample, geoms = molpx.generate.sample(MD_trajfiles, top, clkmeans, \n", - " n_geom_samples=50, \n", - " #keep_all_samples=True # read the doc for this argument\n", - " )" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "cef422ddfd98495e89c158d8e749e997", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "A Jupyter Widget" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/guille/miniconda3/lib/python3.6/site-packages/ipykernel_launcher.py:6: RuntimeWarning: divide by zero encountered in log\n", - " \n" - ] - }, - { - "data": { - "text/plain": [ - "(NGLWidget(count=5), )" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Plot clusters\n", - "plt.figure(figsize=(4,4))\n", - "plt.plot(clkmeans.clustercenters[:,0], clkmeans.clustercenters[:,1],' ok')\n", - "# FES as background is optional (change the bool to False)\n", - "if True:\n", - " plt.contourf(x[:-1], y[:-1], -np.log(h.T), alpha=.50)\n", - "\n", - "# Link the clusters positions with the molecular structures\n", - "linked_ngl_wdg = molpx.visualize.sample(data_sample, \n", - " geoms.superpose(geoms[0]), \n", - " plt.gca(), \n", - " clear_lines=False,\n", - " #plot_path=True\n", - " )\n", - "linked_ngl_wdg" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Visual representations for MSMs\n", - "Visually inspect the network behind an MSM" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "MSM = pyemma.msm.estimate_markov_model(clkmeans.dtrajs, 20)" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "b38ba2fd28ed4db89b43ced450cd0ad9", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "A Jupyter Widget" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/guille/miniconda3/lib/python3.6/site-packages/ipykernel_launcher.py:11: RuntimeWarning: divide by zero encountered in log\n", - " # This is added back by InteractiveShellApp.init_path()\n" - ] - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "eac2f4ab440f47c084b056778c26b362", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "A Jupyter Widget" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "plt.figure(figsize=(4,4))\n", - "\n", - "ax, pos = pyemma.plots.plot_markov_model(MSM.P, \n", - " minflux=5e-4, \n", - " arrow_labels=None,\n", - " ax=plt.gca(), \n", - " arrow_curvature = 2, show_frame=True,\n", - " pos=clkmeans.clustercenters)\n", - "# Add a background if wanted\n", - "h, (x, y) = np.histogramdd(np.vstack(Y)[:,:2], weights=np.hstack(MSM.trajectory_weights()), bins=50)\n", - "plt.contourf(x[:-1], y[:-1], -np.log(h.T), cmap=\"jet\", alpha=.5, zorder=0)\n", - "plt.xlim(x[[0,-1]])\n", - "plt.xticks(np.unique(x.round()))\n", - "plt.yticks(np.unique(y.round()))\n", - "\n", - "plt.ylim(y[[0,-1]])\n", - "\n", - "linked_ngl_wd, linked_ax_wd = molpx.visualize.sample(pos, geoms, plt.gca(), dot_color='blue')\n", - "linked_ngl_wd" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## TPT Reactive Pathway Representation" - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "e40f35977dda46efb5810f00ea25fe65", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "A Jupyter Widget" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\r" - ] - }, - { - "data": { - "text/plain": [ - "123" - ] - }, - "execution_count": 20, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Do an MSM with a realistic number of clustercenters\n", - "cl_many = pyemma.coordinates.cluster_regspace([iY[:,:2] for iY in Y], dmin=.25)\n", - "M = pyemma.msm.estimate_markov_model(cl_many.dtrajs, 20)\n", - "cl_many.n_clusters" - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "776ae5e56ad04871906ee0cae34870fb", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "A Jupyter Widget" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\r" - ] - } - ], - "source": [ - "# Use this object to sample geometries\n", - "pos, geom = molpx.generate.sample(MD_trajfiles, top, cl_many)" - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[-0.18704125 -0.77366424] [ 6.71851349 0.03159955]\n" - ] - } - ], - "source": [ - "# Find the most representative microstate of each \n", - "# and least populated macrostate\n", - "M.pcca(3)\n", - "dens_max_i = [distro.argmax() for distro in M.metastable_distributions]\n", - "A = np.argmax([M.stationary_distribution[iset].sum() for iset in M.metastable_sets])\n", - "B = np.argmin([M.stationary_distribution[iset].sum() for iset in M.metastable_sets])\n", - "print(cl_many.clustercenters[dens_max_i[A]],\n", - " cl_many.clustercenters[dens_max_i[B]])" - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "# Create a TPT object with most_pop, least_pop as source, sink respectively\n", - "tpt = pyemma.msm.tpt(M, [dens_max_i[A]], [dens_max_i[B]])\n", - "paths, flux = tpt.pathways(fraction=.5)" - ] - }, - { - "cell_type": "code", - "execution_count": 24, - "metadata": { - "collapsed": true, - "scrolled": true - }, - "outputs": [], - "source": [ - "# Get a path with a decent number of intermediates\n", - "sample_path = paths[np.argmax([len(ipath) for ipath in paths])]" - ] - }, - { - "cell_type": "code", - "execution_count": 25, - "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "4db8e53777e14e689eddd81e4220f8bd", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "A Jupyter Widget" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/guille/miniconda3/lib/python3.6/site-packages/ipykernel_launcher.py:2: RuntimeWarning: divide by zero encountered in log\n", - " \n" - ] - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "6a998bfad0dd40cfacddcd68327fe9be", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "A Jupyter Widget" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "plt.figure()\n", - "plt.contourf(x[:-1], y[:-1], -np.log(h.T), cmap=\"jet\", alpha=.5, zorder=0)\n", - "linked_ngl_wdg, linked_ax_wdg = molpx.visualize.sample(cl_many.clustercenters[sample_path], \n", - " geom[sample_path].superpose(geom[sample_path[0]]), plt.gca(), \n", - " plot_path=True,\n", - " )\n", - "plt.scatter(*cl_many.clustercenters.T, alpha=.25)\n", - "linked_ngl_wdg" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [] - } - ], - "metadata": { - "anaconda-cloud": {}, - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.1" - }, - "widgets": { - "state": { - "00748d42b5924a068146f4fb0dd88755": { - "views": [ - { - "cell_index": 14 - } - ] - }, - "5908b63fa1a94e829efa9ad1d20c5c0a": { - "views": [ - { - "cell_index": 21 - } - ] - }, - "6b0e5b709f194cae92e9f26d95b77b1f": { - "views": [ - { - "cell_index": 10 - } - ] - }, - "87521a1dace146468576b6e9a3cc4b0b": { - "views": [ - { - "cell_index": 8 - } - ] - }, - "a5d6373ecfab400a866af9e48b21f009": { - "views": [ - { - "cell_index": 19 - } - ] - }, - "d2f7e83143d845e78adc56c246ba8e6d": { - "views": [ - { - "cell_index": 21 - } - ] - } - }, - "version": "1.2.0" - } - }, - "nbformat": 4, - "nbformat_minor": 1 -} diff --git a/molpx/visualize.py b/molpx/visualize.py index cfbb1f8..b368bc7 100644 --- a/molpx/visualize.py +++ b/molpx/visualize.py @@ -1221,7 +1221,6 @@ def contacts(contact_map, input, residue_indices=None, average=False, panelsize= positions, iwd, crosshairs=False, - #directionality='a2w', dot_color='None', #**link_ax2wdg_kwargs ) @@ -1229,8 +1228,162 @@ def contacts(contact_map, input, residue_indices=None, average=False, panelsize= iwd._set_size(*['%fin' % inches for inches in iax.get_figure().get_size_inches()]) #iax.figure.tight_layout() axes_wdg.canvas.set_window_title("Contact Map") - outbox = _linkutils.MolPXHBox([iwd, axes_wdg.canvas]) _linkutils.auto_append_these_mpx_attrs(outbox, input, iax, _plt.gcf(), iwd, axes_wdg, positions) + return outbox + +# TODO RETHINK WHERE TO REFACTOR THIS +def _MSM_and_cl_object_match(iMSM, cl_object, verbose=False): + from pyemma.msm.estimators.maximum_likelihood_hmsm import MaximumLikelihoodHMSM as _MLHMSM + from pyemma.msm.estimators.maximum_likelihood_msm import MaximumLikelihoodMSM as _MLMSM + + # Clustercenters + if isinstance(iMSM, _MLHMSM): + n_states_full = iMSM.nstates_obs + elif isinstance(iMSM, _MLMSM): + n_states_full = iMSM._nstates_full + + if not n_states_full==cl_object.n_clusters: + if verbose: + print("n_states does not match") + return False + + # Ntrajs + if not len(iMSM.discrete_trajectories_full)==len(cl_object.trajectory_lengths()): + if verbose: + print("n_trajs does not match") + return False + + # Shape + if not _np.all([len_itraj==len(jtraj) for len_itraj, jtraj + in zip(cl_object.trajectory_lengths(), iMSM.discrete_trajectories_full)]): + if verbose: + print("traj_length do not match") + return False + + return True + + +def MSM(iMSM, traj_inp, + object_for_positions=None, + n_overlays=1, sharpen=False, + top=None, ax=None, sticky=False, panelsize=6, + proj_idxs = [0,1], + **networkplot_kwargs): + + r""" + provided with a PyEMMA :obj:`MSM`-type object, display representatative structures of that MSM + + :param iMSM: MSM-object + sharpen : boolean, default is False + By default, the method samples from the distribution of microstate of each macrostate. + (either from MSM.metastable_distributions or HMM.sample_by_observation probability). + If sharpen is True, one the microstate that maximizes that distribution (i.e., its argmax) will + be sampled. Produces a more "sharpened" sample, which is less representative of the whole entire metastable + set but more representative of the most probable microstate for macrostate + input_pos = None, pyemma clustering object or nd.array of ndmin (2, n_states) + + :return: + """ + from pyemma.msm.estimators.maximum_likelihood_hmsm import MaximumLikelihoodHMSM as _MLHMSM + from pyemma.msm.estimators.maximum_likelihood_msm import MaximumLikelihoodMSM as _MLMSM + from pyemma.plots import plot_markov_model as _pyemma_plt_msm + from pyemma.util.discrete_trajectories import sample_indexes_by_state as _sample_indexes_by_state + from matplotlib import pyplot as _plt + + assert isinstance(iMSM, (_MLHMSM, _MLMSM)), "Allowed input types are %s, not %s"%((_MLHMSM, _MLMSM), type(MSM)) + assert "pos" not in networkplot_kwargs.keys(),("The optarg 'pos' is not allowed for networkplot_kwargs, " + "use 'object_for_positions' instead") + + # Input parsing of the position object + # TODO: HEAVY SPAGHETTI THINKING HERE, but try to make more compact + if object_for_positions is None: + sample_pos = None + elif hasattr(object_for_positions, "clustercenters"): + # Check that they match + assert _MSM_and_cl_object_match(iMSM, + object_for_positions), "The input HMM/MSM and the input cluster object " \ + "do not match" + if isinstance(iMSM, _MLMSM): # Typical case + sample_pos = object_for_positions.clustercenters[iMSM.active_set] + elif isinstance(iMSM, _MLHMSM): + # TODO check with Frank that asserting with HMM.nstates_obs + # is correct in the _MSM_and_cl... method is OK + sample_pos = object_for_positions.clustercenters + + elif isinstance(object_for_positions, _np.ndarray): + if isinstance(iMSM, _MLMSM): + assert len(object_for_positions)==iMSM.n_states, \ + ("Not enough positions in the object_for_positions (%u). The input MSM has %u active states"%(len(object_for_positions), iMSM.nstates)) + sample_pos = object_for_positions + elif isinstance(iMSM, _MLHMSM): + if len(object_for_positions)== iMSM.nstates or len(object_for_positions) == iMSM.nstates_obs: + sample_pos = object_for_positions + else: + raise ValueError("With HMSMs, you have to supply either input position array with either " + "%u entries (number of coarse states in the HMSM) or %u entries (number of observed states of the HMSM). " + "You have provided neither: %u" % (iMSM.nstates, iMSM.nstates_obs, len(object_for_positions))) + else: + raise TypeError("The object_for_positions has the wrong type %s"%type(object_for_positions)) + + # MSM without coarse-graining + if isinstance(iMSM, _MLMSM): + sample_frames = iMSM.sample_by_state(n_overlays) + # The position are already set aobve + + # HMM + else: + if sharpen: + active_state_indexes = iMSM.observable_state_indexes + subset = _np.argmax(iMSM.observation_probabilities, axis=1) + sample_frames = _sample_indexes_by_state(active_state_indexes, n_overlays, subset=subset, replace=True) + if sample_pos is not None and len(sample_pos)==iMSM.nstates: + pass + elif sample_pos is not None and len(object_for_positions)==iMSM.nstates_obs: + sample_pos = sample_pos[subset] + elif sample_pos is not None: + raise("This is a bug and should not have happened") + else: + sample_frames = iMSM.sample_by_observation_probabilities(n_overlays) + if sample_pos is not None and len(sample_pos)==iMSM.nstates: + sample_pos = object_for_positions + elif sample_pos is not None and len(sample_pos)==iMSM.nstates_obs: + isample_pos = [] + for idist in iMSM.observation_probabilities: + isample_pos.append(_np.average(sample_pos, weights=idist, axis=0)) + sample_pos = _np.vstack(isample_pos) + elif sample_pos is not None: + raise("This is a bug and should not have happened") + + if sample_pos is None: + pos = None + else: + pos = sample_pos[:, proj_idxs] + + P = iMSM.P + sample_geoms = _bmutils.save_traj_wrapper(traj_inp, sample_frames, None, top=top) + sample_geoms = _bmutils.re_warp(sample_geoms, n_overlays) + sample_geoms = _bmutils.transpose_geom_list(sample_geoms) + + _plt.ioff() + ifig, pos = _pyemma_plt_msm(P, pos=pos, + ax=ax, + **networkplot_kwargs, + ) + # Conserve the proportion of the circles in the MSM plot + figw, figh = ifig.get_size_inches() + ifig.set_size_inches((panelsize, panelsize*figh/figw)) + iax = ifig.gca() + + + ngl_wdg, axes_wdg = sample(pos, sample_geoms, iax, sticky=sticky, + crosshairs=False, + )#, clear_lines=False, **sample_kwargs) + ngl_wdg._set_size(*['%fin' % inches for inches in ifig.get_size_inches()]) + ifig.tight_layout() + outbox = _linkutils.MolPXHBox([ngl_wdg, ifig.canvas]) + _linkutils.auto_append_these_mpx_attrs(outbox, sample_geoms, ax, ifig, ngl_wdg, axes_wdg, pos) + _plt.ion() + return outbox \ No newline at end of file From af6a9d8f2250aca3a82d9c262eac654fcf01fb49 Mon Sep 17 00:00:00 2001 From: gph82 Date: Mon, 28 May 2018 18:24:36 +0200 Subject: [PATCH 67/73] [visualize] MSM minor bugifx --- molpx/visualize.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/molpx/visualize.py b/molpx/visualize.py index b368bc7..0517105 100644 --- a/molpx/visualize.py +++ b/molpx/visualize.py @@ -1340,14 +1340,14 @@ def MSM(iMSM, traj_inp, sample_frames = _sample_indexes_by_state(active_state_indexes, n_overlays, subset=subset, replace=True) if sample_pos is not None and len(sample_pos)==iMSM.nstates: pass - elif sample_pos is not None and len(object_for_positions)==iMSM.nstates_obs: + elif sample_pos is not None and len(sample_pos)==iMSM.nstates_obs: sample_pos = sample_pos[subset] elif sample_pos is not None: raise("This is a bug and should not have happened") else: sample_frames = iMSM.sample_by_observation_probabilities(n_overlays) if sample_pos is not None and len(sample_pos)==iMSM.nstates: - sample_pos = object_for_positions + pass elif sample_pos is not None and len(sample_pos)==iMSM.nstates_obs: isample_pos = [] for idist in iMSM.observation_probabilities: From fa8d2ed0f22e93c67494fde11612df5e35ba2f10 Mon Sep 17 00:00:00 2001 From: gph82 Date: Wed, 30 May 2018 16:04:38 +0200 Subject: [PATCH 68/73] [_linkutils] minor in docstring --- molpx/_linkutils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/molpx/_linkutils.py b/molpx/_linkutils.py index 8cf6a61..1649b76 100644 --- a/molpx/_linkutils.py +++ b/molpx/_linkutils.py @@ -227,8 +227,8 @@ def append_if_existing(self, args0, startswith_arg="linked_"): self.__dict__[attrname] += iarg.__dict__[attrname] def auto_append_these_mpx_attrs(iobj, *attrs): - r""" The attribute s name is automatically derived - from the attribute s type via a type:name dictionary + r""" The attribute's name is automatically derived + from the attribute's type via a type:name dictionary *attrs : any number of unnamed objects of the types in type2attrname. If the object type is a list, it will be flattened prior to attempting From 876e5cc366dd0d9111220a13b74c4680f8795cc6 Mon Sep 17 00:00:00 2001 From: gph82 Date: Wed, 30 May 2018 17:47:02 +0200 Subject: [PATCH 69/73] [visualize] sample() makes a copy of input (otherwise it gets altered) and MSM() refactor and documented --- molpx/visualize.py | 244 +++++++++++++++++++++++---------------------- 1 file changed, 123 insertions(+), 121 deletions(-) diff --git a/molpx/visualize.py b/molpx/visualize.py index 0517105..e93b7cd 100644 --- a/molpx/visualize.py +++ b/molpx/visualize.py @@ -936,9 +936,14 @@ def sample(positions, geom, ax, axes_wdg: :obj:`~matplotlib.Axes.AxesWidget` """ + # Make a copy of the geometry, otherwise the input gets destroyed + if isinstance(geom, list): + copy_geom = [gg[:] for gg in geom] + elif isinstance(geom, _md.Trajectory): + copy_geom = geom[:] if not sticky: - return _sample(positions, geom, ax, + return _sample(positions, copy_geom, ax, plot_path = plot_path, clear_lines = clear_lines, n_smooth = n_smooth, @@ -949,11 +954,11 @@ def sample(positions, geom, ax, **link_ax2wdg_kwargs) else: - if isinstance(geom, _md.Trajectory): - geom=[geom] + if isinstance(copy_geom, _md.Trajectory): + copy_geom=[copy_geom] # The method takes care of whatever superpose - geom = _bmutils.superpose_to_most_compact_in_list(superpose, geom) + copy_geom = _bmutils.superpose_to_most_compact_in_list(superpose, copy_geom) if color_list is None: sticky_colors_hex = ['Element' for ii in range(len(positions))] @@ -969,7 +974,7 @@ def sample(positions, geom, ax, raise TypeError('argument color_list should be either None, "random", or a list of len(pos)=%u, ' 'instead of type %s and len %u' % (len(positions), type(color_list), len(color_list))) sticky_rep = 'cartoon' - if geom[0].top.n_residues < 10: + if copy_geom[0].top.n_residues < 10: sticky_rep = 'ball+stick' if list_of_repr_dicts is None: list_of_repr_dicts = [{'repr_type': sticky_rep, 'selection': 'all'}] @@ -979,7 +984,7 @@ def sample(positions, geom, ax, # Prepare Geometry_in_widget_list ngl_wdg._GeomsInWid = [_linkutils.GeometryInNGLWidget(igeom, ngl_wdg, color_molecule_hex= cc, - list_of_repr_dicts=list_of_repr_dicts) for igeom, cc in zip(_bmutils.transpose_geom_list(geom), sticky_colors_hex)] + list_of_repr_dicts=list_of_repr_dicts) for igeom, cc in zip(_bmutils.transpose_geom_list(copy_geom), sticky_colors_hex)] axes_wdg = _linkutils.link_ax_w_pos_2_nglwidget(ax, positions, @@ -1233,58 +1238,75 @@ def contacts(contact_map, input, residue_indices=None, average=False, panelsize= return outbox -# TODO RETHINK WHERE TO REFACTOR THIS -def _MSM_and_cl_object_match(iMSM, cl_object, verbose=False): - from pyemma.msm.estimators.maximum_likelihood_hmsm import MaximumLikelihoodHMSM as _MLHMSM - from pyemma.msm.estimators.maximum_likelihood_msm import MaximumLikelihoodMSM as _MLMSM - # Clustercenters - if isinstance(iMSM, _MLHMSM): - n_states_full = iMSM.nstates_obs - elif isinstance(iMSM, _MLMSM): - n_states_full = iMSM._nstates_full - - if not n_states_full==cl_object.n_clusters: - if verbose: - print("n_states does not match") - return False - - # Ntrajs - if not len(iMSM.discrete_trajectories_full)==len(cl_object.trajectory_lengths()): - if verbose: - print("n_trajs does not match") - return False - - # Shape - if not _np.all([len_itraj==len(jtraj) for len_itraj, jtraj - in zip(cl_object.trajectory_lengths(), iMSM.discrete_trajectories_full)]): - if verbose: - print("traj_length do not match") - return False - - return True - - -def MSM(iMSM, traj_inp, - object_for_positions=None, - n_overlays=1, sharpen=False, - top=None, ax=None, sticky=False, panelsize=6, - proj_idxs = [0,1], +def MSM(msm_obj, traj_inp, + pos=None, + sharpen=False, + n_overlays=1, + top=None, + sticky=False, + panelsize=6, **networkplot_kwargs): r""" - provided with a PyEMMA :obj:`MSM`-type object, display representatative structures of that MSM - - :param iMSM: MSM-object - sharpen : boolean, default is False - By default, the method samples from the distribution of microstate of each macrostate. - (either from MSM.metastable_distributions or HMM.sample_by_observation probability). - If sharpen is True, one the microstate that maximizes that distribution (i.e., its argmax) will - be sampled. Produces a more "sharpened" sample, which is less representative of the whole entire metastable - set but more representative of the most probable microstate for macrostate - input_pos = None, pyemma clustering object or nd.array of ndmin (2, n_states) - - :return: + Visualize an MSM or an HMM as a network of nodes and egdes, together with an :obj:~`nglview.NGLWidget` + containing representative structures of node/state. Clicking on the node will update the widget. + + Parameters + ---------- + msm_obj: input MSM-object + One of PyEMMA's MSM-objects, either a "normal" MSM (:obj:`~pyemma.msm.MaximumLikelihoodMSM`) or a hidden MSM (:obj:`~pyemma.msm.MaximumLikelihoodHMSM`) + + traj_inp : trajectory input + Where to get the geometries from. It must be the same input with which the :obj:`msm_obj` was built. + No checks are done by the method as to whether this is true, i.e. *rubbish-in->rubbish-out*. + It can be of three different types (and lists thereof): + + * filenames (for which a :obj:`top` is needed, see below) + * :obj:`mdtraj.Trajectory` objects + * a PyEMMA's :obj:`~pyemma.coordinates.data.feature_reader.FeatureReader` + + pos : node positions, either None or a numpy ndarray of ndim=2 + By default, node positions are optimized to represent connectivity + (see PyEMMA's :obj:`~pyemma.plots.plot_markov_model`). However, the user can override with + custom node-positions by passing an array as :obj:`pos`. In many cases, it is useful for that array to be + the clustercenter-positions with which the MSM/HMM was constructed: :obj:`pos=cl.clustercenters`. + The input in :obj:`pos` has to be compatible with the provided :obj:`msm_obj`, i.e. have the necessary + number entries. The error messages will inform about what's wrong. + + sharpen : boolean, default is False, + This keyword only has effect for an HMM as an :obj:`input_msm`. + By default, the method samples from the distribution of microstate inside each macrostate, using the object's + :obj:`~pyemma.msm.MaximumLikelihoodHMSM.sample_by_observation_probabilities`- method. This can lead + to fuzzy samples where the overlay of molecular structures is not very informative. + + If :obj:`sharpen` is True, only the microstate that maximizes each macrostate's probabilites + (i.e., its argmax) will be sampled. Produces a more *sharpened* sample, + which is less representative of the whole set but very representative of the most probable + microstate within that set. + + n_overlays : int, default is 1 + Number of structures to represent simultaneously for each node of the network + + top : str or :obj:`mdtraj.Topology`, default is None + If the filenames in :obj:`traj_inp` need a topology, here's where you pass it along + + sticky : bool, default is False + Behaviour of the mouseclick when clicking a node in the network. + If True, left click adds-structures, right-click deletes them + + panelsize : int, default is 6 + Size of the network figure, in inches. If :obj:`pos` is provided, the panelsize will be adapted + slightly to match the proportions of :obj:`pos` + + networkplot_kwargs : named keyword arguments for :obj:`~pyemma.plots.plot_markov_model` + + Returns : + --------- + + mpxbox : A :obj:`~molpx.linkutils.MolPXHBox`-object. It contains the :obj:`nglview.NGLWidget` and the network + plot. Check the :obj:`mpxbox_linked*` attributes to see what the object contains + """ from pyemma.msm.estimators.maximum_likelihood_hmsm import MaximumLikelihoodHMSM as _MLHMSM from pyemma.msm.estimators.maximum_likelihood_msm import MaximumLikelihoodMSM as _MLMSM @@ -1292,98 +1314,78 @@ def MSM(iMSM, traj_inp, from pyemma.util.discrete_trajectories import sample_indexes_by_state as _sample_indexes_by_state from matplotlib import pyplot as _plt - assert isinstance(iMSM, (_MLHMSM, _MLMSM)), "Allowed input types are %s, not %s"%((_MLHMSM, _MLMSM), type(MSM)) - assert "pos" not in networkplot_kwargs.keys(),("The optarg 'pos' is not allowed for networkplot_kwargs, " - "use 'object_for_positions' instead") - - # Input parsing of the position object - # TODO: HEAVY SPAGHETTI THINKING HERE, but try to make more compact - if object_for_positions is None: - sample_pos = None - elif hasattr(object_for_positions, "clustercenters"): - # Check that they match - assert _MSM_and_cl_object_match(iMSM, - object_for_positions), "The input HMM/MSM and the input cluster object " \ - "do not match" - if isinstance(iMSM, _MLMSM): # Typical case - sample_pos = object_for_positions.clustercenters[iMSM.active_set] - elif isinstance(iMSM, _MLHMSM): - # TODO check with Frank that asserting with HMM.nstates_obs - # is correct in the _MSM_and_cl... method is OK - sample_pos = object_for_positions.clustercenters - - elif isinstance(object_for_positions, _np.ndarray): - if isinstance(iMSM, _MLMSM): - assert len(object_for_positions)==iMSM.n_states, \ - ("Not enough positions in the object_for_positions (%u). The input MSM has %u active states"%(len(object_for_positions), iMSM.nstates)) - sample_pos = object_for_positions - elif isinstance(iMSM, _MLHMSM): - if len(object_for_positions)== iMSM.nstates or len(object_for_positions) == iMSM.nstates_obs: - sample_pos = object_for_positions - else: - raise ValueError("With HMSMs, you have to supply either input position array with either " - "%u entries (number of coarse states in the HMSM) or %u entries (number of observed states of the HMSM). " - "You have provided neither: %u" % (iMSM.nstates, iMSM.nstates_obs, len(object_for_positions))) + assert isinstance(msm_obj, (_MLHMSM, _MLMSM)), "Allowed input types are %s, not %s" % ((_MLHMSM, _MLMSM), type(MSM)) + + # Input parsing of the position object (#TODO reduce code?) + if pos is None: + pass + elif isinstance(pos, _np.ndarray): + if isinstance(msm_obj, _MLMSM): + assert len(pos) == msm_obj.nstates, \ + ("Number of input positions (%u) " + "does not match number %u active states of the MSM. Try slicing the input positions with " + "MSM.active_set" % (len(pos), msm_obj.nstates)) + elif isinstance(msm_obj, _MLHMSM): + assert len(pos) == msm_obj.nstates or len(pos) == msm_obj.nstates_obs, \ + ("With the input HMSM, the input positions have to have either " + "%u entries (number of coarse states in the HMSM) or %u entries (number of observed states of the HMSM). " + "You have provided neither: %u" % (msm_obj.nstates, msm_obj.nstates_obs, len(pos))) else: - raise TypeError("The object_for_positions has the wrong type %s"%type(object_for_positions)) + raise TypeError("The object_for_positions has the wrong type %s" % type(pos)) # MSM without coarse-graining - if isinstance(iMSM, _MLMSM): - sample_frames = iMSM.sample_by_state(n_overlays) - # The position are already set aobve + if isinstance(msm_obj, _MLMSM): + sample_frames = _np.vstack(msm_obj.sample_by_state(n_overlays)) # HMM else: if sharpen: - active_state_indexes = iMSM.observable_state_indexes - subset = _np.argmax(iMSM.observation_probabilities, axis=1) - sample_frames = _sample_indexes_by_state(active_state_indexes, n_overlays, subset=subset, replace=True) - if sample_pos is not None and len(sample_pos)==iMSM.nstates: - pass - elif sample_pos is not None and len(sample_pos)==iMSM.nstates_obs: - sample_pos = sample_pos[subset] - elif sample_pos is not None: - raise("This is a bug and should not have happened") + active_state_indexes = msm_obj.observable_state_indexes + subset = _np.argmax(msm_obj.observation_probabilities, axis=1) + sample_frames = _np.vstack(_sample_indexes_by_state(active_state_indexes, n_overlays, + subset=subset, replace=True)) + # The user gave one position entry per metastable set + if pos is not None and len(pos)==msm_obj.nstates: + pass # im leaving this case for clarity, in theory it could be removed + # The user gave a full array of positions that matches the total number of microstates + # and wants the method to choose automagically the positions that match the argmax(PDF) + elif pos is not None and len(pos)==msm_obj.nstates_obs: + pos = pos[subset] + # Any other case has ben caught before by the above ValueErrors else: - sample_frames = iMSM.sample_by_observation_probabilities(n_overlays) - if sample_pos is not None and len(sample_pos)==iMSM.nstates: - pass - elif sample_pos is not None and len(sample_pos)==iMSM.nstates_obs: + sample_frames = _np.vstack(msm_obj.sample_by_observation_probabilities(n_overlays)) + # The user gave one position entry per metastable set + if pos is not None and len(pos)==msm_obj.nstates: + pass # im leaving this case for clarity, in theory it could be removed + # The user gave a full array of positions that matches the total number of microstates + # and wants the method to wheight them using the observation probabilities + elif pos is not None and len(pos)==msm_obj.nstates_obs: isample_pos = [] - for idist in iMSM.observation_probabilities: - isample_pos.append(_np.average(sample_pos, weights=idist, axis=0)) - sample_pos = _np.vstack(isample_pos) - elif sample_pos is not None: - raise("This is a bug and should not have happened") - - if sample_pos is None: - pos = None - else: - pos = sample_pos[:, proj_idxs] + for idist in msm_obj.observation_probabilities: + isample_pos.append(_np.average(pos, weights=idist, axis=0)) + pos = _np.vstack(isample_pos) - P = iMSM.P sample_geoms = _bmutils.save_traj_wrapper(traj_inp, sample_frames, None, top=top) sample_geoms = _bmutils.re_warp(sample_geoms, n_overlays) sample_geoms = _bmutils.transpose_geom_list(sample_geoms) _plt.ioff() - ifig, pos = _pyemma_plt_msm(P, pos=pos, - ax=ax, + ifig, pos = _pyemma_plt_msm(msm_obj.P, pos=pos, **networkplot_kwargs, - ) + ) # Conserve the proportion of the circles in the MSM plot figw, figh = ifig.get_size_inches() - ifig.set_size_inches((panelsize, panelsize*figh/figw)) + ifig.set_size_inches((panelsize, panelsize * figh / figw)) iax = ifig.gca() - - ngl_wdg, axes_wdg = sample(pos, sample_geoms, iax, sticky=sticky, + ngl_wdg, axes_wdg = sample(pos, sample_geoms, iax, + sticky=sticky, crosshairs=False, )#, clear_lines=False, **sample_kwargs) ngl_wdg._set_size(*['%fin' % inches for inches in ifig.get_size_inches()]) ifig.tight_layout() outbox = _linkutils.MolPXHBox([ngl_wdg, ifig.canvas]) - _linkutils.auto_append_these_mpx_attrs(outbox, sample_geoms, ax, ifig, ngl_wdg, axes_wdg, pos) + _linkutils.auto_append_these_mpx_attrs(outbox, sample_geoms, _plt.gca(), ifig, ngl_wdg, axes_wdg, pos) _plt.ion() return outbox \ No newline at end of file From df93fbc2135f32caaedd5b052e1da2b982f7413a Mon Sep 17 00:00:00 2001 From: gph82 Date: Wed, 30 May 2018 17:48:15 +0200 Subject: [PATCH 70/73] [docs] updated --- doc/source/conf.py | 2 +- doc/source/index_visualize.rst | 1 + molpx/_bmutils.py | 5 ++++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index 290d62c..dc3d7bd 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -411,7 +411,7 @@ def __getattribute__(self, item): nbfiles = [ '0.molPX_quick_intro_Ala2.ipynb', '1.molPX_and_PyEMMA_Features.ipynb', - '2.molPX_TICA_BPTI.ipynb', + '2.molPX_TICA_and_MSMs_BPTI.ipynb', '3.molPX_TICA_Ala2.ipynb', '4.molPX_metadynamics_Di-Ala.ipynb' ] diff --git a/doc/source/index_visualize.rst b/doc/source/index_visualize.rst index d50dd03..46d848c 100644 --- a/doc/source/index_visualize.rst +++ b/doc/source/index_visualize.rst @@ -23,6 +23,7 @@ The methods offered by this module are: molpx.visualize.traj molpx.visualize.sample molpx.visualize.correlations + molpx.visualize.MSM molpx.visualize.feature .. automodule:: molpx.visualize diff --git a/molpx/_bmutils.py b/molpx/_bmutils.py index 20d5d3d..bded7c7 100644 --- a/molpx/_bmutils.py +++ b/molpx/_bmutils.py @@ -727,7 +727,10 @@ def save_traj_wrapper(traj_inp, indexes, outfile, top=None, stride=1, chunksize= Parameters ----------- - traj_inp : :pyemma:`FeatureReader` object or :mdtraj:`Trajectory` object or list of :mdtraj:`Trajectory` objects + traj_inp : Can be of many types + :pyemma:`FeatureReader` object + :mdtraj:`Trajectory` object or list thereof + a list of strings pointing to filenames returns: see the return values of :pyemma:`save_traj` """ From 843f827f144469f4dd4b08d7bde4058b50920e50 Mon Sep 17 00:00:00 2001 From: gph82 Date: Wed, 30 May 2018 17:48:31 +0200 Subject: [PATCH 71/73] [tests] visualize.MSM() extensively tested --- molpx/tests/test_visualize.py | 75 ++++++++++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/molpx/tests/test_visualize.py b/molpx/tests/test_visualize.py index 7c82597..80518ad 100644 --- a/molpx/tests/test_visualize.py +++ b/molpx/tests/test_visualize.py @@ -9,6 +9,8 @@ import mdtraj as md import matplotlib.pyplot as plt import nglview +from numpy.testing import assert_raises +import pyemma from .test_bmutils import TestWithBPTIData @@ -244,12 +246,80 @@ def test_correlations_inputs_FAIL_color_list_parsing(self): except TypeError: pass -class TestFeature(TestWithBPTIData): +class TestMSM(TestWithBPTIData): + @classmethod + def setUpClass(self): + TestWithBPTIData.setUpClass() + self.cl = pyemma.coordinates.cluster_kmeans([iY[:, :2] for iY in self.Ys], 5) + self.iMSM = pyemma.msm.estimate_markov_model(self.cl.dtrajs, 1) + self.iHMM = self.iMSM.coarse_grain(3) + + @classmethod + def tearDownClass(self): + TestWithBPTIData.tearDownClass() + def test_just_runs_MSM(self): + visualize.MSM(self.iMSM, self.MD_trajectories) + # More overlays + visualize.MSM(self.iMSM, self.MD_trajectories, n_overlays=10) + + def test_just_runs_HMM(self): + visualize.MSM(self.iHMM, self.MD_trajectories) + visualize.MSM(self.iHMM, self.MD_trajectories, n_overlays=10) + visualize.MSM(self.iHMM, self.MD_trajectories, sharpen=True) + + def test_just_runs_position_input(self): + visualize.MSM(self.iMSM, self.MD_trajectories, pos=self.cl.clustercenters) + visualize.MSM(self.iHMM, self.MD_trajectories, pos=self.cl.clustercenters) + + visualize.MSM(self.iHMM, self.MD_trajectories, pos=self.cl.clustercenters, sharpen=True) + visualize.MSM(self.iHMM, self.MD_trajectories, pos=self.cl.clustercenters[:self.iHMM.nstates]) + # test assert + assert_raises(TypeError, visualize.MSM, self.iMSM, self.MD_trajectories, pos=['a']) + assert_raises(AssertionError, visualize.MSM, self.iMSM, self.MD_trajectories, pos=self.cl.clustercenters[:-1]) + + def test_source_inputs(self): + visualize.MSM(self.iMSM, self.MD_trajectory_files, top=self.MD_topology_file) + visualize.MSM(self.iMSM, self.MD_trajectory_files, top=self.MD_topology) + visualize.MSM(self.iMSM, self.source) + + + def test_returns_the_right_things_MSM(self): + # Returning the right things should be guaranteed by all the + # lower-level methods, which are also unit-tested. Still, here we go + mpxbox = visualize.MSM(self.iMSM, self.MD_trajectories, n_overlays=3) + # We re-featurize, re-tic-transform, and re-cl-assign the output geoms + for igeoms in mpxbox.linked_mdgeoms: + out_assign = self.cl.assign(self.tica.transform(self.feat.transform(igeoms))[:,:2]) + assert np.allclose(out_assign, np.arange(self.cl.n_clusters)) + + # Now with sticky + mpxbox = visualize.MSM(self.iMSM, self.MD_trajectories, n_overlays=3, sticky=True) + # We re-featurize, re-tic-transform, and re-cl-assign the output geoms + for igeoms in mpxbox.linked_mdgeoms: + out_assign = self.cl.assign(self.tica.transform(self.feat.transform(igeoms))[:, :2]) + assert np.allclose(out_assign, np.arange(self.cl.n_clusters)) + + def test_returns_the_right_things_HMSM_sharpen(self): + # Returning the right things should be guaranteed by all the + # lower-level methods, which are also unit-tested. Still, here we go + mpxbox = visualize.MSM(self.iHMM, self.MD_trajectories, n_overlays=3, sharpen=True) + # We re-featurize, re-tic-transform, and re-cl-assign the output geoms + out_set = np.argmax(self.iHMM.observation_probabilities, axis=1) + for igeoms in mpxbox.linked_mdgeoms: + out_assign = self.cl.assign(self.tica.transform(self.feat.transform(igeoms))[:,:2]) + assert np.allclose(out_assign, out_set) + + +class TestFeature(TestWithBPTIData): @classmethod def setUpClass(self): TestWithBPTIData.setUpClass() + @classmethod + def tearDownClass(self): + TestWithBPTIData.tearDownClass() + def test_feature(self): plt.figure() iwd = nglview.show_mdtraj(self.MD_trajectories[0]) @@ -288,6 +358,9 @@ def test_one_ctcframe(self): # This should pass visualize.contacts(self.ctcs.mean(0), self.geom, average=True) + def test_raises(self): + assert_raises(NotImplementedError, visualize.contacts, self.ctcs.mean(0), self.geom, average=True, residue_indices=[1,2,3]) + class TestBoxMe(unittest.TestCase): def test_just_runs_and_exits_gracefully(self): From 56ba6b92c5df81fb803fb9c85f484c2c81aa4287 Mon Sep 17 00:00:00 2001 From: gph82 Date: Wed, 30 May 2018 17:48:49 +0200 Subject: [PATCH 72/73] [notebooks] updated --- .../1.molPX_and_PyEMMA_Features.ipynb | 30 +- .../2.molPX_TICA_and_MSMs_BPTI.ipynb | 324 ++++++------------ 2 files changed, 127 insertions(+), 227 deletions(-) diff --git a/molpx/notebooks/1.molPX_and_PyEMMA_Features.ipynb b/molpx/notebooks/1.molPX_and_PyEMMA_Features.ipynb index bd50047..f4a5d00 100644 --- a/molpx/notebooks/1.molPX_and_PyEMMA_Features.ipynb +++ b/molpx/notebooks/1.molPX_and_PyEMMA_Features.ipynb @@ -30,10 +30,8 @@ }, { "cell_type": "code", - "execution_count": 1, - "metadata": { - "collapsed": true - }, + "execution_count": 2, + "metadata": {}, "outputs": [], "source": [ "top = 'notebooks/data/bpti-c-alpha_centered.pdb'\n", @@ -56,10 +54,8 @@ }, { "cell_type": "code", - "execution_count": 2, - "metadata": { - "collapsed": true - }, + "execution_count": 3, + "metadata": {}, "outputs": [], "source": [ "# Create a memory representation of the trajectories\n", @@ -68,13 +64,13 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 4, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "039f9856e0574670aba06c25039896a4", + "model_id": "feaf7c33ce0b4e7ab136de29a5c70eeb", "version_major": 2, "version_minor": 0 }, @@ -95,7 +91,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "f3bce3719c614589a2e26c9c4d305ccc", + "model_id": "113eec10d13c4fac8c620c7d30c7a519", "version_major": 2, "version_minor": 0 }, @@ -131,7 +127,7 @@ " 'DIST: ARG 1 CA 0 - TYR 21 CA 20', ...])" ] }, - "execution_count": 3, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } @@ -154,7 +150,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 5, "metadata": { "scrolled": false }, @@ -162,7 +158,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "596903342fdb4f2f9189494ef76e9519", + "model_id": "bbd5a7072c454ab79f03f3c83c41e3ea", "version_major": 2, "version_minor": 0 }, @@ -177,14 +173,14 @@ "name": "stdout", "output_type": "stream", "text": [ - "22-05-18 18:01:59 pyemma.coordinates.clustering.kmeans.KmeansClustering[2] INFO Algorithm did not reach convergence criterion of 1e-05 in 10 iterations. Consider increasing max_iter.\n", + "30-05-18 16:34:21 pyemma.coordinates.clustering.kmeans.KmeansClustering[2] INFO Algorithm did not reach convergence criterion of 1e-05 in 10 iterations. Consider increasing max_iter.\n", "\r" ] }, { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "d00d967bca9c406cb23895d01069cf58", + "model_id": "58ef338dfc4d48c4b2c4f6715ca911f8", "version_major": 2, "version_minor": 0 }, @@ -205,7 +201,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "17a36ccf478644479abeb8ddc214664f", + "model_id": "f4e8d69171154cb485c7d7a71d047fb4", "version_major": 2, "version_minor": 0 }, diff --git a/molpx/notebooks/2.molPX_TICA_and_MSMs_BPTI.ipynb b/molpx/notebooks/2.molPX_TICA_and_MSMs_BPTI.ipynb index e619b64..b776bc0 100644 --- a/molpx/notebooks/2.molPX_TICA_and_MSMs_BPTI.ipynb +++ b/molpx/notebooks/2.molPX_TICA_and_MSMs_BPTI.ipynb @@ -32,7 +32,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -63,7 +63,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ @@ -84,7 +84,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ @@ -115,7 +115,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 5, "metadata": { "scrolled": false }, @@ -123,7 +123,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "8de7941a9dc848cca4816700540c5293", + "model_id": "b93db910c62043b1822f2da274e45bb0", "version_major": 2, "version_minor": 0 }, @@ -138,14 +138,14 @@ "name": "stdout", "output_type": "stream", "text": [ - "25-05-18 17:54:32 pyemma.coordinates.clustering.kmeans.KmeansClustering[0] INFO Algorithm did not reach convergence criterion of 1e-05 in 10 iterations. Consider increasing max_iter.\n", + "30-05-18 17:43:04 pyemma.coordinates.clustering.kmeans.KmeansClustering[0] INFO Cluster centers converged after 6 steps.\n", "\r" ] }, { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "afe77d5e19474700b1b08e38f40155ed", + "model_id": "6df52f04ead341aab7b20d98f91eb0d7", "version_major": 2, "version_minor": 0 }, @@ -166,7 +166,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "b5a7b7bf4ec446bc9ecef99990e344fc", + "model_id": "256408dce1b44035854355e92d01b09e", "version_major": 2, "version_minor": 0 }, @@ -216,7 +216,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "e03fa3137349456d9181e2796d5f1508", + "model_id": "39be6324f1e54066a228104bc20fa991", "version_major": 2, "version_minor": 0 }, @@ -237,7 +237,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "a12638ba59a4496da3cd83d52601e1ed", + "model_id": "11437cf4759f4cebb10127ed094ce173", "version_major": 2, "version_minor": 0 }, @@ -252,14 +252,14 @@ "name": "stdout", "output_type": "stream", "text": [ - "25-05-18 16:19:16 pyemma.coordinates.clustering.kmeans.KmeansClustering[8] INFO Algorithm did not reach convergence criterion of 1e-05 in 10 iterations. Consider increasing max_iter.\n", + "30-05-18 17:43:10 pyemma.coordinates.clustering.kmeans.KmeansClustering[10] INFO Algorithm did not reach convergence criterion of 1e-05 in 10 iterations. Consider increasing max_iter.\n", "\r" ] }, { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "cccf392ea5b7431983e4381b112de72f", + "model_id": "00163b8d1ac648629531b16d9c8eadfe", "version_major": 2, "version_minor": 0 }, @@ -280,7 +280,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "5f8909f44df042ef8a437fc61c445464", + "model_id": "ed24cba5be9f4813b1e976916f04bb4c", "version_major": 2, "version_minor": 0 }, @@ -322,13 +322,13 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "40650f5c7588442287b330420a2af834", + "model_id": "974210f2d77a4033828f91883f705eb3", "version_major": 2, "version_minor": 0 }, @@ -349,7 +349,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "648739be3ea34f0caa809cce735d29a0", + "model_id": "8adfc977e5494497bce705a331d237f3", "version_major": 2, "version_minor": 0 }, @@ -364,14 +364,14 @@ "name": "stdout", "output_type": "stream", "text": [ - "25-05-18 16:20:13 pyemma.coordinates.clustering.kmeans.KmeansClustering[19] INFO Algorithm did not reach convergence criterion of 1e-05 in 10 iterations. Consider increasing max_iter.\n", + "30-05-18 17:43:15 pyemma.coordinates.clustering.kmeans.KmeansClustering[19] INFO Algorithm did not reach convergence criterion of 1e-05 in 10 iterations. Consider increasing max_iter.\n", "\r" ] }, { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "b13f704953e1466980aeadaa0ecba3ba", + "model_id": "ad74ec33d703455bbc8297b642246bf9", "version_major": 2, "version_minor": 0 }, @@ -393,10 +393,10 @@ "data": { "text/plain": [ "((203, 2),\n", - " )" + " )" ] }, - "execution_count": 9, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } @@ -423,7 +423,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 8, "metadata": { "scrolled": false }, @@ -431,7 +431,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "65056c0dfdfe461b8f7f901525c17420", + "model_id": "6f42f00335bb40bab5b36ddcd3013237", "version_major": 2, "version_minor": 0 }, @@ -453,7 +453,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "01b484da426b4056be8f72717dc7a465", + "model_id": "1e16537f6b2542b9bd40e698449303e4", "version_major": 2, "version_minor": 0 }, @@ -491,13 +491,13 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 9, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "8a0dace2ef40402ebaba8151d3bd1323", + "model_id": "72b81eff927046788368e764761364f2", "version_major": 2, "version_minor": 0 }, @@ -518,7 +518,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "44877122d64f4ada8996e2fb8f2d802d", + "model_id": "9ad556c7f8b34f15bab0502b97bc9253", "version_major": 2, "version_minor": 0 }, @@ -533,14 +533,14 @@ "name": "stdout", "output_type": "stream", "text": [ - "25-05-18 16:20:23 pyemma.coordinates.clustering.kmeans.KmeansClustering[30] INFO Cluster centers converged after 5 steps.\n", + "30-05-18 17:43:20 pyemma.coordinates.clustering.kmeans.KmeansClustering[30] INFO Algorithm did not reach convergence criterion of 1e-05 in 10 iterations. Consider increasing max_iter.\n", "\r" ] }, { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "352d76d1863e4b05a24e6abeeffd3dc9", + "model_id": "bb4c74d060e44891a91d7eb018c3d9b1", "version_major": 2, "version_minor": 0 }, @@ -561,7 +561,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "9893e3b9461d489a85b3700fb8f39885", + "model_id": "1ccb4d20d14e442f9dfd90ff2724a416", "version_major": 2, "version_minor": 0 }, @@ -576,14 +576,14 @@ "name": "stdout", "output_type": "stream", "text": [ - "25-05-18 16:20:28 pyemma.coordinates.clustering.kmeans.KmeansClustering[37] INFO Algorithm did not reach convergence criterion of 1e-05 in 10 iterations. Consider increasing max_iter.\n", + "30-05-18 17:43:26 pyemma.coordinates.clustering.kmeans.KmeansClustering[37] INFO Cluster centers converged after 11 steps.\n", "\r" ] }, { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "64695b6cc51840d5a69d8516f28a742f", + "model_id": "01e70ca23c534d1bba761237e8ed1ed3", "version_major": 2, "version_minor": 0 }, @@ -626,7 +626,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 10, "metadata": {}, "outputs": [], "source": [ @@ -644,7 +644,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 11, "metadata": { "scrolled": false }, @@ -652,7 +652,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "b39ef814d49e4bcb92624c77e1b81959", + "model_id": "cea24390e26045d7b2ea7060d7618df1", "version_major": 2, "version_minor": 0 }, @@ -674,7 +674,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "74e76004aceb4b73b5c8dcd7b17ee5c9", + "model_id": "1fa71b389f3d46e1ade55939c28befdd", "version_major": 2, "version_minor": 0 }, @@ -715,13 +715,13 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "e1840ce738634e2fa61a81f30ed60d6a", + "model_id": "d72f71f0a2fb421ab0d3dc7cee6e8a65", "version_major": 2, "version_minor": 0 }, @@ -742,7 +742,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "75fa7b0e940d4166af0fbef10c5b25e7", + "model_id": "f0e14a9f44aa4ad29b4be25e91676e2d", "version_major": 2, "version_minor": 0 }, @@ -763,7 +763,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "2dde02bd959c468387bd556e5d7448e7", + "model_id": "562c71a39bba464dab6ef3019fab5749", "version_major": 2, "version_minor": 0 }, @@ -801,13 +801,13 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 13, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "20651793b9cb40689a3505803dbf4171", + "model_id": "c342c71b102a4c6284f3cd852d652277", "version_major": 2, "version_minor": 0 }, @@ -845,7 +845,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 14, "metadata": {}, "outputs": [ { @@ -902,7 +902,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 15, "metadata": { "scrolled": false }, @@ -910,7 +910,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "2ba2f827bfc1443eba16e4e0f39551b6", + "model_id": "65b233004ff94b50898fab38b6f0c8cb", "version_major": 2, "version_minor": 0 }, @@ -931,7 +931,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "ca694d306d5d46d3afcb5e11e0a23ee8", + "model_id": "5a2dc05b2e8a400a896351e4b94c23bf", "version_major": 2, "version_minor": 0 }, @@ -946,14 +946,14 @@ "name": "stdout", "output_type": "stream", "text": [ - "25-05-18 16:21:05 pyemma.coordinates.clustering.kmeans.KmeansClustering[51] INFO Algorithm did not reach convergence criterion of 1e-05 in 10 iterations. Consider increasing max_iter.\n", + "30-05-18 17:43:37 pyemma.coordinates.clustering.kmeans.KmeansClustering[51] INFO Cluster centers converged after 10 steps.\n", "\r" ] }, { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "d3720ce062b1447a836529b5b64eb77e", + "model_id": "e52ab46d051d476790f404609676d032", "version_major": 2, "version_minor": 0 }, @@ -974,7 +974,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "7d0f399632f043128c702a080ee8f992", + "model_id": "e4e850af19134552807b4ac5f7cb273f", "version_major": 2, "version_minor": 0 }, @@ -1017,13 +1017,13 @@ }, { "cell_type": "code", - "execution_count": 44, + "execution_count": 16, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "5f70ea98e8664904b287352cc069f360", + "model_id": "dce09a99533e49abbd4e74792e062ebb", "version_major": 2, "version_minor": 0 }, @@ -1037,7 +1037,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "6c849ae629334244b15b772893e2b4d8", + "model_id": "f82ae020d8434492a020cdbe2a09e84d", "version_major": 2, "version_minor": 0 }, @@ -1052,7 +1052,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "25-05-18 18:34:40 pyemma.coordinates.clustering.kmeans.KmeansClustering[6] INFO Algorithm did not reach convergence criterion of 1e-05 in 10 iterations. Consider increasing max_iter.\n", + "30-05-18 17:43:40 pyemma.coordinates.clustering.kmeans.KmeansClustering[60] INFO Algorithm did not reach convergence criterion of 1e-05 in 10 iterations. Consider increasing max_iter.\n", "\r" ] } @@ -1064,13 +1064,13 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 17, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "388a443404fe4892afdc9211d4c0d75e", + "model_id": "8e36da50b0dd4ee1b41308d8ab785784", "version_major": 2, "version_minor": 0 }, @@ -1091,7 +1091,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "5d1cb1ad69f7412b9e5b0a1fe49b04e6", + "model_id": "06109eae31b54fdfb18deb63d049b389", "version_major": 2, "version_minor": 0 }, @@ -1119,13 +1119,13 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 18, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "645d43cdc8ba4042a807cf115a0d012e", + "model_id": "8a1713fbf25f48a98f4e4a06425242a6", "version_major": 2, "version_minor": 0 }, @@ -1147,10 +1147,10 @@ { "data": { "text/plain": [ - "(NGLWidget(count=5), )" + "(NGLWidget(count=5), )" ] }, - "execution_count": 20, + "execution_count": 18, "metadata": {}, "output_type": "execute_result" } @@ -1178,37 +1178,18 @@ "metadata": {}, "source": [ "## Visual representations for MSMs\n", - "Visually inspect the network behind an MSM" + "Visually inspect the network behind an MSM by coarse graining it to an HMM" ] }, { "cell_type": "code", - "execution_count": 87, - "metadata": {}, - "outputs": [], - "source": [ - "MSM = pyemma.msm.estimate_markov_model(clkmeans.dtrajs, 20)" - ] - }, - { - "cell_type": "code", - "execution_count": 25, - "metadata": {}, - "outputs": [], - "source": [ - "from molpx import visualize\n", - "from imp import reload" - ] - }, - { - "cell_type": "code", - "execution_count": 128, + "execution_count": 19, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "2531928ebea843989e9df57c24c169cb", + "model_id": "0feee8230d324ad5ac0487713612bae5", "version_major": 2, "version_minor": 0 }, @@ -1222,7 +1203,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "0df623a5b93244bf9389339bc8d88ff1", + "model_id": "d5c7ca2d145d4d96a5b4068a8d98fd49", "version_major": 2, "version_minor": 0 }, @@ -1237,14 +1218,14 @@ "name": "stdout", "output_type": "stream", "text": [ - "28-05-18 16:04:20 pyemma.coordinates.clustering.kmeans.KmeansClustering[31] INFO Algorithm did not reach convergence criterion of 1e-05 in 10 iterations. Consider increasing max_iter.\n", + "30-05-18 17:43:43 pyemma.coordinates.clustering.kmeans.KmeansClustering[63] INFO Cluster centers converged after 10 steps.\n", "\r" ] }, { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "547cbf79687840a09af3e2daa80959f8", + "model_id": "a61ea20e54874a118e9107a4027f2339", "version_major": 2, "version_minor": 0 }, @@ -1264,13 +1245,36 @@ } ], "source": [ - "clkmeans20 = pyemma.coordinates.cluster_kmeans([iY[:,:2] for iY in Y], 100)\n", - "MSMcg = pyemma.msm.estimate_markov_model(clkmeans20.dtrajs, 20).coarse_grain(3)" + "clkmeans = pyemma.coordinates.cluster_kmeans([iY[:,:2] for iY in Y], 100)\n", + "MSM = pyemma.msm.estimate_markov_model(clkmeans.dtrajs, 1)\n", + "MSMcg = MSM.coarse_grain(3)" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from imp import reload\n", + "from molpx import visualize\n", + "reload(visualize)" ] }, { "cell_type": "code", - "execution_count": 160, + "execution_count": 24, "metadata": { "scrolled": false }, @@ -1278,7 +1282,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "4f18e08d7c9a4ea09909cdd23b432c06", + "model_id": "1af06d7323f342078943825a30d25488", "version_major": 2, "version_minor": 0 }, @@ -1291,12 +1295,16 @@ } ], "source": [ - "reload(visualize)\n", - "visualize.MSM(MSMcg, src, clkmeans20,\n", - " #sticky=True, \n", - " #sharpen=True, \n", - " n_overlays=10,\n", - " )" + "mpxbox = molpx.visualize.MSM(MSMcg, src, \n", + " pos=clkmeans.clustercenters,\n", + " #sticky=True, \n", + " sharpen=True, \n", + " n_overlays=10,\n", + " figpadding=.5,\n", + " #proj_idxs =[1,0]\n", + " panelsize=6\n", + " )\n", + "mpxbox" ] }, { @@ -1309,41 +1317,9 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "e40f35977dda46efb5810f00ea25fe65", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "A Jupyter Widget" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\r" - ] - }, - { - "data": { - "text/plain": [ - "123" - ] - }, - "execution_count": 20, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# Do an MSM with a realistic number of clustercenters\n", "cl_many = pyemma.coordinates.cluster_regspace([iY[:,:2] for iY in Y], dmin=.25)\n", @@ -1353,31 +1329,9 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "776ae5e56ad04871906ee0cae34870fb", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "A Jupyter Widget" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\r" - ] - } - ], + "outputs": [], "source": [ "# Use this object to sample geometries\n", "pos, geom = molpx.generate.sample(MD_trajfiles, top, cl_many)" @@ -1385,17 +1339,9 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[-0.18704125 -0.77366424] [ 6.71851349 0.03159955]\n" - ] - } - ], + "outputs": [], "source": [ "# Find the most representative microstate of each \n", "# and least populated macrostate\n", @@ -1409,10 +1355,8 @@ }, { "cell_type": "code", - "execution_count": 23, - "metadata": { - "collapsed": true - }, + "execution_count": null, + "metadata": {}, "outputs": [], "source": [ "# Create a TPT object with most_pop, least_pop as source, sink respectively\n", @@ -1422,9 +1366,8 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": null, "metadata": { - "collapsed": true, "scrolled": true }, "outputs": [], @@ -1435,46 +1378,9 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "4db8e53777e14e689eddd81e4220f8bd", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "A Jupyter Widget" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/guille/miniconda3/lib/python3.6/site-packages/ipykernel_launcher.py:2: RuntimeWarning: divide by zero encountered in log\n", - " \n" - ] - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "6a998bfad0dd40cfacddcd68327fe9be", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "A Jupyter Widget" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "plt.figure()\n", "plt.contourf(x[:-1], y[:-1], -np.log(h.T), cmap=\"jet\", alpha=.5, zorder=0)\n", @@ -1489,9 +1395,7 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [] } From 7f30a3a1621b569dc0c4fed9a63ef3968d37fbb8 Mon Sep 17 00:00:00 2001 From: gph82 Date: Thu, 31 May 2018 14:34:36 +0200 Subject: [PATCH 73/73] [notebooks] opsin data --- molpx/notebooks/data/ops.pdb.gz | Bin 0 -> 79324 bytes molpx/notebooks/data/ops_mini.xtc | Bin 0 -> 118372 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 molpx/notebooks/data/ops.pdb.gz create mode 100644 molpx/notebooks/data/ops_mini.xtc diff --git a/molpx/notebooks/data/ops.pdb.gz b/molpx/notebooks/data/ops.pdb.gz new file mode 100644 index 0000000000000000000000000000000000000000..e0323f0de06871ab1e5d45d54bf4b5494429231e GIT binary patch literal 79324 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�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

FZJkBmP*C2Z24RMc+`^$Q9}#xMyxi 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̡uP!W>#W)l%w`V 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 literal 0 HcmV?d00001 diff --git a/molpx/notebooks/data/ops_mini.xtc b/molpx/notebooks/data/ops_mini.xtc new file mode 100644 index 0000000000000000000000000000000000000000..219c6068d2bb06acda202b7703463110b9f70e74 GIT binary patch literal 118372 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 Not+}t`$I9cogmp%(H{T+ literal 0 HcmV?d00001