diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..6fed1a7a --- /dev/null +++ b/.coveragerc @@ -0,0 +1,14 @@ +[run] +branch = True +source = nglview + +[report] +exclude_lines = + pragma: no cover + def __repr__ + def __str__ + if self.debug + if __name__ == .__main__.: + +omit = + *_version.py diff --git a/.travis.yml b/.travis.yml index d3b16485..c1cfce40 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,7 @@ install: - python setup.py install script: - - nosetests -vs . + - nosetests --with-coverage --cover-package nglview -vs . after_success: - echo "after_success" diff --git a/devtools/travis-ci/setup_env.sh b/devtools/travis-ci/setup_env.sh index a6e0025e..03b888da 100755 --- a/devtools/travis-ci/setup_env.sh +++ b/devtools/travis-ci/setup_env.sh @@ -15,7 +15,7 @@ pip install conda conda install --yes conda-build jinja2 anaconda-client pip # create myenv -conda create -y -n myenv python=$PYTHON_VERSION jupyter notebook nose numpy mock +conda create -y -n myenv python=$PYTHON_VERSION jupyter notebook nose numpy mock coverage source activate myenv pip install pytraj diff --git a/nglview/__init__.py b/nglview/__init__.py index 8f08388f..31c8a1a3 100644 --- a/nglview/__init__.py +++ b/nglview/__init__.py @@ -9,7 +9,7 @@ import warnings import tempfile import ipywidgets as widgets -from traitlets import Unicode, Bool, Dict, List, Int, Float +from traitlets import Unicode, Bool, Dict, List, Int, Float, Any, Bytes from IPython.display import display, Javascript from notebook.nbextensions import install_nbextension @@ -25,10 +25,21 @@ from urllib2 import urlopen +import base64 + +def encode_numpy(arr, dtype='f4'): + arr = arr.astype(dtype) + return base64.b64encode(arr.data).decode('utf8') + +def decode_base64(data, shape, dtype='f4'): + import numpy as np + decoded_str = base64.b64decode(data) + return np.frombuffer(decoded_str, dtype=dtype).reshape(shape) + + ############## # Simple API - def show_pdbid(pdbid, **kwargs): '''Show PDB entry. @@ -181,8 +192,10 @@ class Trajectory(object): def __init__(self): pass - def get_coordinates_list(self, index): - # [ 1,1,1, 2,2,2 ] + def get_coordinates_dict(self): + raise NotImplementedError() + + def get_coordinates(self, index): raise NotImplementedError() @property @@ -214,10 +227,10 @@ def __init__(self, path): except Exception as e: raise e - def get_coordinates_list(self, index): + def get_coordinates(self, index): traj = self.traj_cache.get(os.path.abspath(self.path)) frame = traj.get_frame(int(index)) - return frame["coords"].flatten().tolist() + return frame["coords"] @property def n_frames(self): @@ -242,9 +255,12 @@ def __init__(self, trajectory): self.ext = "pdb" self.params = {} - def get_coordinates_list(self, index): - frame = self.trajectory[index].xyz * 10 # convert from nm to A - return frame.flatten().tolist() + def get_coordinates_dict(self): + return dict((index, encode_numpy(xyz)) + for index, xyz in enumerate(self.trajectory.xyz)) + + def get_coordinates(self, index): + return self.trajectory.xyz[index] @property def n_frames(self): @@ -275,13 +291,12 @@ def __init__(self, trajectory): self.ext = "pdb" self.params = {} - def get_coordinates_list(self, index): - # use trajectory[index] to use both in-memory - # (via pytraj.load method) - # and out-of-core trajectory - # (via pytraj.iterload method) - frame = self.trajectory[index].xyz - return frame.flatten().tolist() + def get_coordinates_dict(self): + return dict((index, encode_numpy(xyz)) + for index, xyz in enumerate(self.trajectory.xyz)) + + def get_coordinates(self, index): + return self.trajectory[index].xyz @property def n_frames(self): @@ -307,9 +322,12 @@ def __init__(self, trajectory): # only call get_coordinates once self._xyz = trajectory.get_coordinates() - def get_coordinates_list(self, index): - frame = self._xyz[index] - return frame.flatten().tolist() + def get_coordinates_dict(self): + return dict((index, encode_numpy(xyz)) + for index, xyz in enumerate(self._xyz)) + + def get_coordinates(self, index): + return self._xyz[index] @property def n_frames(self): @@ -344,10 +362,15 @@ def __init__(self, atomgroup): self.ext = "pdb" self.params = {} - def get_coordinates_list(self, index): + def get_coordinates_dict(self): + + return dict((index, encode_numpy(self.atomgroup.atoms.positions)) + for index, _ in enumerate(self.atomgroup.universe.trajectory)) + + def get_coordinates(self, index): self.atomgroup.universe.trajectory[index] - frame = self.atomgroup.atoms.positions - return frame.flatten().tolist() + xyz = self.atomgroup.atoms.positions + return xyz @property def n_frames(self): @@ -383,7 +406,9 @@ class NGLWidget(widgets.DOMWidget): selection = Unicode("*", sync=True) structure = Dict(sync=True) representations = List(sync=True) - coordinates = List(sync=True) + _coordinates_meta = Dict(sync=True) + coordinates_dict = Dict(sync=True) + cache = Bool(sync=True) picked = Dict(sync=True) frame = Int(sync=True) count = Int(sync=True) @@ -391,13 +416,17 @@ class NGLWidget(widgets.DOMWidget): def __init__(self, structure, trajectory=None, representations=None, parameters=None, **kwargs): + try: + self.cache = kwargs.pop('cache') + except KeyError: + self.cache = False super(NGLWidget, self).__init__(**kwargs) if parameters: self.parameters = parameters self.set_structure(structure) if trajectory: self.trajectory = trajectory - elif hasattr(structure, "get_coordinates_list"): + elif hasattr(structure, "get_coordinates_dict"): self.trajectory = structure if hasattr(self, "trajectory") and \ hasattr(self.trajectory, "n_frames"): @@ -413,9 +442,33 @@ def __init__(self, structure, trajectory=None, "sele": "hetero OR mol" }} ] - self._add_repr_method_shortcut() + + @property + def coordinates(self): + if self.cache: + return + else: + data = self._coordinates_meta['data'] + dtype = self._coordinates_meta['dtype'] + shape = self._coordinates_meta['shape'] + return decode_base64(data, dtype=dtype, shape=shape) + + @coordinates.setter + def coordinates(self, arr): + """return current coordinate + + Parameters + ---------- + arr : 2D array, shape=(n_atoms, 3) + """ + dtype = 'f4' + coordinates_meta = dict(data=encode_numpy(arr, dtype=dtype), + dtype=dtype, + shape=arr.shape) + self._coordinates_meta = coordinates_meta + def _add_repr_method_shortcut(self): # dynamically add method for NGLWidget repr_names = [ @@ -453,7 +506,25 @@ def func(this, selection='all', **kwd): fn = 'add_' + rep[0] from types import MethodType setattr(self, fn, MethodType(func, self)) - + + def caching(self): + if hasattr(self.trajectory, "get_coordinates_dict"): + # should use use coordinates_dict to sync? + # my molecule disappear. + # self.coordinates_dict = self.trajectory.get_coordinates_dict() + + self.cache = True + msg = dict(type='base64', + cache=self.cache, + data=self.trajectory.get_coordinates_dict()) + self.send(msg) + else: + print('warning: does not have get_coordinates_dict method, turn off cache') + self.cache = False + + def uncaching(self): + self.cache = False + def set_representations(self, representations): self.representations = representations @@ -465,14 +536,16 @@ def set_structure(self, structure): } def _set_coordinates(self, index): - if self.trajectory: - coordinates = self.trajectory.get_coordinates_list(index) + if self.trajectory and not self.cache: + coordinates = self.trajectory.get_coordinates(index) self.coordinates = coordinates else: print("no trajectory available") def _frame_changed(self): - self._set_coordinates(self.frame) + if not self.cache: + self._set_coordinates(self.frame) + def add_representation(self, repr_type, selection='all', **kwd): '''Add representation. diff --git a/nglview/html/static/widget_ngl.js b/nglview/html/static/widget_ngl.js index e8465a06..73aac687 100644 --- a/nglview/html/static/widget_ngl.js +++ b/nglview/html/static/widget_ngl.js @@ -14,7 +14,7 @@ require.config( { "jsfeat": "../nbextensions/nglview/svd.min", "signals": "../nbextensions/nglview/signals.min", "NGL": "../nbextensions/nglview/ngl", - "mdsrv": "../nbextensions/nglview/mdsrv" + "mdsrv": "../nbextensions/nglview/mdsrv", }, shim: { THREE: { exports: "THREE" }, @@ -68,12 +68,22 @@ define( [ this.model.on( "change:structure", this.structureChanged, this ); // init setting of coordinates - this.model.on( "change:coordinates", this.coordinatesChanged, this ); + this.model.on( "change:_coordinates_meta", this.coordinatesChanged, this ); + + // init setting of coordinates + this.model.on( "change:coordinates_dict", this.coordsDictChanged, this ); + + // init setting of frame + this.model.on( "change:frame", this.frameChanged, this ); // init parameters handling this.model.on( "change:parameters", this.parametersChanged, this ); + // init parameters handling + this.model.on( "change:cache", this.cacheChanged, this ); + // get message from Python + this.coordsDict = undefined; this.model.on( "msg:custom", function (msg) { this.on_msg( msg ); }, this); @@ -219,15 +229,93 @@ define( [ } }, + mydecode: function(base64) { + // lightly adapted from Niklas + + /* + * base64-arraybuffer + * https://github.com/niklasvh/base64-arraybuffer + * + * Copyright (c) 2012 Niklas von Hertzen + * Licensed under the MIT license. + */ + var chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + var bufferLength = base64.length * 0.75, + len = base64.length, + i, p = 0, + encoded1, encoded2, encoded3, encoded4; + + if (base64[base64.length - 1] === "=") { + bufferLength--; + if (base64[base64.length - 2] === "=") { + bufferLength--; + } + } + + var arraybuffer = new ArrayBuffer(bufferLength), + bytes = new Uint8Array(arraybuffer); + + for (i = 0; i < len; i += 4) { + encoded1 = chars.indexOf(base64[i]); + encoded2 = chars.indexOf(base64[i + 1]); + encoded3 = chars.indexOf(base64[i + 2]); + encoded4 = chars.indexOf(base64[i + 3]); + + bytes[p++] = (encoded1 << 2) | (encoded2 >> 4); + bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2); + bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63); + } + + return arraybuffer; + }, + + frameChanged: function(){ + if( this._cache ){ + var frame = this.model.get( "frame" ); + if( frame in this.coordsDict ) { + var coordinates = this.coordsDict[frame]; + this._update_coords(coordinates); + } // else: just wait + } + // else: listen to coordinatesChanged + }, + + coordinatesChanged: function(){ - var coordinates = this.model.get( "coordinates" ); + if (! this._cache ){ + var coordinates_meta = this.model.get( "_coordinates_meta" ); + + // not checking dtype yet + var coordinates = this.mydecode( coordinates_meta['data'] ); + this._update_coords(coordinates); + } + }, + + _update_coords: function( coordinates ) { + // coordinates must be ArrayBuffer (use this.mydecode) var component = this.structureComponent; if( coordinates && component ){ var coords = new Float32Array( coordinates ); component.structure.updatePosition( coords ); component.updateRepresentations( { "position": true } ); } + }, + coordsDictChanged: function(){ + this.coordsDict = this.model.get( "coordinates_dict" ); + var cdict = this.coordsDict + var clen = Object.keys(cdict).length + if ( clen != 0 ){ + this._cache = true; + }else{ + this._cache = false; + } + this.model.set( "cache", this._cache); + + for (var i = 0; i < Object.keys(coordsDict).length; i++) { + this.coordsDict[i] = this.mydecode( coordsDict[i]); + } }, setSize: function( width, height ){ @@ -241,6 +329,10 @@ define( [ this.stage.setParameters( parameters ); }, + cacheChanged: function(){ + this._cache = this.model.get( "cache" ); + }, + on_msg: function(msg){ if( msg.type == 'call_method' ){ console.log( "msg.args" ); @@ -266,9 +358,19 @@ define( [ var func = component[msg.methodName]; func.apply( component, new_args ); } - } - } - + }else if( msg.type == 'base64' ){ + console.log( "receiving base64 dict" ); + var base64Dict = msg.data; + this.coordsDict = {}; + if ( "cache" in msg ){ + this._cache = msg.cache; + this.model.set( "cache", this._cache ); + } + for (var i = 0; i < Object.keys(base64Dict).length; i++) { + this.coordsDict[i] = this.mydecode( base64Dict[i]); + } + } + }, } ); manager.WidgetManager.register_widget_view( 'NGLView', NGLView ); diff --git a/nglview/tests/notebooks/caching.ipynb b/nglview/tests/notebooks/caching.ipynb new file mode 100644 index 00000000..e013f4bd --- /dev/null +++ b/nglview/tests/notebooks/caching.ipynb @@ -0,0 +1,66 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "import pytraj as pt\n", + "import nglview as nv\n", + "\n", + "traj = pt.load(nv.datafiles.TRR, nv.datafiles.PDB)\n", + "\n", + "view = nv.show_pytraj(traj)\n", + "view.representations = []\n", + "view.add_cartoon()\n", + "view" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "view.caching()" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "view.add_licorice()" + ] + } + ], + "metadata": { + "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.4.4" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/nglview/tests/notebooks/data_sending_speed.ipynb b/nglview/tests/notebooks/data_sending_speed.ipynb new file mode 100644 index 00000000..a291cd6a --- /dev/null +++ b/nglview/tests/notebooks/data_sending_speed.ipynb @@ -0,0 +1,74 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "import pytraj as pt\n", + "import nglview as nv\n", + "\n", + "traj = pt.load(nv.datafiles.TRR, nv.datafiles.PDB)\n", + "view = nv.show_pytraj(traj)\n", + "view.representations = []\n", + "view.add_cartoon()\n", + "view" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "view.caching()" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "view.uncaching()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [] + } + ], + "metadata": { + "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.4.4" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/nglview/tests/notebooks/send_message.ipynb b/nglview/tests/notebooks/send_message.ipynb index 552efda2..365ae0d0 100644 --- a/nglview/tests/notebooks/send_message.ipynb +++ b/nglview/tests/notebooks/send_message.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 14, + "execution_count": 37, "metadata": { "collapsed": false }, @@ -11,18 +11,61 @@ "import pytraj as pt\n", "import nglview as nv\n", "\n", - "fn = '/home/haichit/pytraj_github/pytraj/tests/data/tz2.ortho.nc'\n", - "tn = '/home/haichit/pytraj_github/pytraj/tests/data/tz2.ortho.parm7'\n", - "\n", - "traj = pt.load([fn,], tn).superpose('@CA')\n", + "traj = pt.datafiles.load_tz2()\n", "view = nv.show_pytraj(traj)\n", "xyz = traj.xyz\n", + "msg_list = dict(data=xyz.flatten().tolist())\n", + "msg_bytes = xyz.ravel().tobytes()\n", + "msg_base64 = view.trajectory.get_coordinate_dict()\n", "view" ] }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 38, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1 loop, best of 3: 442 ms per loop\n" + ] + } + ], + "source": [ + "%%timeit\n", + "\n", + "\n", + "view.send(msg_list)" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "10 loops, best of 3: 102 ms per loop\n" + ] + } + ], + "source": [ + "%%timeit\n", + "\n", + "view.send(msg_bytes)" + ] + }, + { + "cell_type": "code", + "execution_count": 40, "metadata": { "collapsed": false }, @@ -31,16 +74,47 @@ "name": "stdout", "output_type": "stream", "text": [ - "CPU times: user 1.03 s, sys: 15 ms, total: 1.05 s\n", - "Wall time: 1.05 s\n" + "100 loops, best of 3: 5.02 ms per loop\n" ] } ], "source": [ - "%%time\n", + "%%timeit \n", "\n", - "msg = dict(data=xyz.flatten().tolist())\n", - "view.send(msg)" + "view.send(msg_base64)" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "view.caching()" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 43, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "view.cache" ] } ], diff --git a/nglview/tests/notebooks/test_mda_analysis.png b/nglview/tests/notebooks/test_mda_analysis.png new file mode 100644 index 00000000..66f099b6 Binary files /dev/null and b/nglview/tests/notebooks/test_mda_analysis.png differ diff --git a/nglview/tests/notebooks/test_mdanalysis.ipynb b/nglview/tests/notebooks/test_mdanalysis.ipynb new file mode 100644 index 00000000..2d4f684b --- /dev/null +++ b/nglview/tests/notebooks/test_mdanalysis.ipynb @@ -0,0 +1,103 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "import warnings\n", + "warnings.filterwarnings('ignore', category=DeprecationWarning)\n", + "\n", + "from MDAnalysis import Universe\n", + "from MDAnalysisTests import datafiles as mda_datafiles\n", + "import nglview as nv\n", + "\n", + "universe = Universe(mda_datafiles.PSF, mda_datafiles.DCD)\n", + "view = nv.show_mdanalysis(universe)\n", + "view.representations = []\n", + "view.add_licorice('not hydrogen')\n", + "view.add_cartoon('not hydrogen')\n", + "view" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/plain": [ + "False" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "view.cache" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "view.caching()" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "view.cache" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 2", + "language": "python", + "name": "python2" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.11" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/nglview/tests/test_widget.py b/nglview/tests/test_widget.py index 46b3e933..567b5d80 100644 --- a/nglview/tests/test_widget.py +++ b/nglview/tests/test_widget.py @@ -7,6 +7,8 @@ import nose.tools as nt import unittest +from numpy.testing import assert_equal as eq, assert_almost_equal as aa_eq +import numpy as np from ipykernel.comm import Comm import ipywidgets as widgets @@ -14,8 +16,12 @@ from traitlets import TraitError from ipywidgets import Widget + import pytraj as pt import nglview as nv +import mdtraj as md +import parmed as pmd +# wait until MDAnalysis supports PY3 from nglview.utils import PY2, PY3 @@ -93,3 +99,58 @@ def test_show_parmed(): fn = nv.datafiles.PDB parm = pmd.load_file(fn) view = nv.show_parmed(parm) + +def test_caching_bool(): + view = nv.show_pytraj(pt.datafiles.load_tz2()) + nt.assert_false(view.cache) + view.caching() + nt.assert_true(view.cache) + view.uncaching() + nt.assert_false(view.cache) + +def test_encode_and_decode(): + xyz = np.arange(100).astype('f4') + shape = xyz.shape + + b64_str = nv.encode_numpy(xyz) + new_xyz = nv.decode_base64(b64_str, dtype='f4', shape=shape) + aa_eq(xyz, new_xyz) + +def test_coordinates_meta(): + from mdtraj.testing import get_fn + fn, tn = [get_fn('frame0.pdb'),] * 2 + trajs = [pt.load(fn, tn), md.load(fn, top=tn), pmd.load_file(tn, fn)] + + N_FRAMES = trajs[0].n_frames + + if PY2: + from MDAnalysis import Universe + u = Universe(tn, fn) + trajs.append(Universe(tn, fn)) + + views = [nv.show_pytraj(trajs[0]), nv.show_mdtraj(trajs[1]), nv.show_parmed(trajs[2])] + + if PY2: + views.append(nv.show_mdanalysis(trajs[3])) + + for index, (view, traj) in enumerate(zip(views, trajs)): + view.frame = 3 + + _coordinates_meta = view._coordinates_meta + nt.assert_in('data', _coordinates_meta) + nt.assert_in('shape', _coordinates_meta) + nt.assert_in('dtype', _coordinates_meta) + nt.assert_equal(view._coordinates_meta['dtype'], 'f4') + nt.assert_equal(view.trajectory.n_frames, N_FRAMES) + nt.assert_equal(len(view.trajectory.get_coordinates_dict().keys()), N_FRAMES) + + if index in [0, 1]: + # pytraj, mdtraj + aa_eq(view.coordinates, traj.xyz[3], decimal=4) + view.coordinates = traj.xyz[2] + aa_eq(view.coordinates, traj.xyz[2], decimal=4) + + data = view._coordinates_meta['data'] + shape = view._coordinates_meta['shape'] + dtype = view._coordinates_meta['dtype'] + aa_eq(view.coordinates, nv.decode_base64(data, dtype=dtype, shape=shape))