diff --git a/contact_map/__init__.py b/contact_map/__init__.py index 33de73c..0d73c2e 100644 --- a/contact_map/__init__.py +++ b/contact_map/__init__.py @@ -11,4 +11,6 @@ from .min_dist import NearestAtoms, MinimumDistanceCounter +from .dask_runner import DaskContactFrequency + # import concurrence diff --git a/contact_map/contact_map.py b/contact_map/contact_map.py index b8a61fc..5f8c80c 100644 --- a/contact_map/contact_map.py +++ b/contact_map/contact_map.py @@ -724,6 +724,23 @@ def __init__(self, trajectory, query=None, haystack=None, cutoff=0.45, contacts = self._build_contact_map(trajectory) (self._atom_contacts, self._residue_contacts) = contacts + def __hash__(self): + return hash((super(ContactFrequency, self).__hash__(), + tuple(self._atom_contacts.items()), + tuple(self._residue_contacts.items()), + self.n_frames)) + + def __eq__(self, other): + is_equal = (super(ContactFrequency, self).__eq__(other) + and self._atom_contacts == other._atom_contacts + and self._residue_contacts == other._residue_contacts + and self.n_frames == other.n_frames) + return is_equal + + def to_dict(self): + dct = super(ContactFrequency, self).to_dict() + dct.update({'n_frames': self.n_frames}) + return dct def _build_contact_map(self, trajectory): # We actually build the contact map on a per-residue basis, although diff --git a/contact_map/dask_runner.py b/contact_map/dask_runner.py new file mode 100644 index 0000000..27742f9 --- /dev/null +++ b/contact_map/dask_runner.py @@ -0,0 +1,73 @@ +""" +Implementation of ContactFrequency parallelization using dask.distributed +""" + +from . import frequency_task +from .contact_map import ContactFrequency, ContactObject +import mdtraj as md + + +def dask_run(trajectory, client, run_info): + """ + Runs dask version of ContactFrequency. Note that this API on this will + definitely change before the release. + + Parameters + ---------- + trajectory : mdtraj.trajectory + client : dask.distributed.Client + path to dask scheduler file + run_info : dict + keys are 'trajectory_file' (trajectory filename), 'load_kwargs' + (additional kwargs passed to md.load), and 'parameters' (dict of + kwargs for the ContactFrequency object) + + Returns + ------- + :class:`.ContactFrequency` : + total contact frequency for the trajectory + """ + slices = frequency_task.default_slices(n_total=len(trajectory), + n_workers=len(client.ncores())) + + subtrajs = client.map(frequency_task.load_trajectory_task, slices, + file_name=run_info['trajectory_file'], + **run_info['load_kwargs']) + maps = client.map(frequency_task.map_task, subtrajs, + parameters=run_info['parameters']) + freq = client.submit(frequency_task.reduce_all_results, maps) + + return freq.result() + +class DaskContactFrequency(ContactFrequency): + def __init__(self, client, filename, query=None, haystack=None, + cutoff=0.45, n_neighbors_ignored=2, **kwargs): + self.client = client + self.filename = filename + trajectory = md.load(filename, **kwargs) + + self.frames = range(len(trajectory)) + self.kwargs = kwargs + + ContactObject.__init__(self, trajectory.topology, query, haystack, + cutoff, n_neighbors_ignored) + + freq = dask_run(trajectory, client, self.run_info) + self._n_frames = freq.n_frames + self._atom_contacts = freq._atom_contacts + self._residue_contacts = freq._residue_contacts + + @property + def parameters(self): + return {'query': self.query, + 'haystack': self.haystack, + 'cutoff': self.cutoff, + 'n_neighbors_ignored': self.n_neighbors_ignored} + + @property + def run_info(self): + return {'parameters': self.parameters, + 'trajectory_file': self.filename, + 'load_kwargs': self.kwargs} + + diff --git a/contact_map/frequency_task.py b/contact_map/frequency_task.py new file mode 100644 index 0000000..0866a81 --- /dev/null +++ b/contact_map/frequency_task.py @@ -0,0 +1,132 @@ +""" +Task-based implementation of :class:`.ContactFrequency`. + +The overall algorithm is: + +1. Identify how we're going to slice up the trajectory into task-based + chunks (:meth:`block_slices`, :meth:`default_slices`) +2. On each node + a. Load the trajectory segment (:meth:`load_trajectory_task`) + b. Run the analysis on the segment (:meth:`map_task`) +3. Once all the results have been collected, combine them + (:meth:`reduce_all_results`) + +Notes +----- +Includes versions where messages are Python objects and versions (labelled +with _json) where messages have been JSON-serialized. However, we don't yet +have a solution for JSON serialization of MDTraj objects, so if JSON +serialization is the communication method, the loading of the trajectory and +the calculation of the contacts must be combined into a single task. +""" + +import mdtraj as md +from contact_map import ContactFrequency + +def block_slices(n_total, n_per_block): + """Determine slices for splitting the input array. + + Parameters + ---------- + n_total : int + total length of array + n_per_block : int + maximum number of items per block + + Returns + ------- + list of slice + slices to be applied to the array + """ + n_full_blocks = n_total // n_per_block + slices = [slice(i*n_per_block, (i+1)*n_per_block) + for i in range(n_full_blocks)] + if n_total % n_per_block: + slices.append(slice(n_full_blocks*n_per_block, n_total)) + return slices + +def default_slices(n_total, n_workers): + """Calculate default slices from number of workers. + + Default behavior is (approximately) one task per worker. + + Parameters + ---------- + n_total : int + total number of items in array + n_workers : int + number of workers + + Returns + ------- + list of slice + slices to be applied to the array + """ + n_frames_per_task = max(1, n_total // n_workers) + return block_slices(n_total, n_frames_per_task) + + +def load_trajectory_task(subslice, file_name, **kwargs): + """ + Task for loading file. Reordered for to take per-task variable first. + + Parameters + ---------- + subslice : slice + the slice of the trajectory to use + file_name : str + trajectory file name + kwargs : + other parameters to mdtraj.load + + Returns + ------- + md.Trajectory : + subtrajectory for this slice + """ + return md.load(file_name, **kwargs)[subslice] + +def map_task(subtrajectory, parameters): + """Task to be mapped to all subtrajectories. Run ContactFrequency + + Parameters + ---------- + subtrajectory : mdtraj.Trajectory + single trajectory segment to calculate ContactFrequency for + parameters : dict + kwargs-style dict for the :class:`.ContactFrequency` object + + Returns + ------- + :class:`.ContactFrequency` : + contact frequency for the subtrajectory + """ + return ContactFrequency(subtrajectory, **parameters) + +def reduce_all_results(contacts): + """Combine multiple :class:`.ContactFrequency` objects into one + + Parameters + ---------- + contacts : iterable of :class:`.ContactFrequency` + the individual (partial) contact frequencies + + Returns + ------- + :class:`.ContactFrequency` : + total of all input contact frequencies (summing them) + """ + accumulator = contacts[0] + for contact in contacts[1:]: + accumulator.add_contact_frequency(contact) + return accumulator + + +def map_task_json(subtrajectory, parameters): + """JSON-serialized version of :meth:`map_task`""" + return map_task(subtrajectory, parameters).to_json() + +def reduce_all_results_json(results_of_map): + """JSON-serialized version of :meth:`reduce_all_results`""" + contacts = [ContactFrequency.from_json(res) for res in results_of_map] + return reduce_all_results(contacts) diff --git a/contact_map/tests/test_contact_map.py b/contact_map/tests/test_contact_map.py index 1919b06..6bd1f0e 100644 --- a/contact_map/tests/test_contact_map.py +++ b/contact_map/tests/test_contact_map.py @@ -329,6 +329,17 @@ def test_frames_parameter(self): } assert contacts.residue_contacts.counter == expected_residue_count + def test_hash(self): + map2 = ContactFrequency(trajectory=traj, + cutoff=0.075, + n_neighbors_ignored=0) + map3 = ContactFrequency(trajectory=traj[:2], + cutoff=0.075, + n_neighbors_ignored=0) + + assert hash(self.map) == hash(map2) + assert hash(self.map) != hash(map3) + def test_saving(self): m = self.map m.save_to_file(test_file) diff --git a/contact_map/tests/test_dask_runner.py b/contact_map/tests/test_dask_runner.py new file mode 100644 index 0000000..0566dcc --- /dev/null +++ b/contact_map/tests/test_dask_runner.py @@ -0,0 +1,17 @@ +from .utils import * + +from contact_map.dask_runner import * + +class TestDaskContactFrequency(object): + def test_dask_integration(self): + # this is an integration test to check that dask works + dask = pytest.importorskip('dask') + distributed = pytest.importorskip('dask.distributed') + + client = distributed.Client() + filename = find_testfile("trajectory.pdb") + + dask_freq = DaskContactFrequency(client, filename, cutoff=0.075, + n_neighbors_ignored=0) + client.close() + assert dask_freq.n_frames == 5 diff --git a/contact_map/tests/test_frequency_task.py b/contact_map/tests/test_frequency_task.py new file mode 100644 index 0000000..6a79038 --- /dev/null +++ b/contact_map/tests/test_frequency_task.py @@ -0,0 +1,93 @@ +import os +import collections + +from .utils import * +from .test_contact_map import traj + +from contact_map.frequency_task import * +from contact_map import ContactFrequency + +class TestSlicing(object): + # tests for block_slices and default_slices + @pytest.mark.parametrize("inputs, results", [ + ((100, 25), + [slice(0, 25), slice(25, 50), slice(50, 75), slice(75, 100)]), + ((85, 25), + [slice(0, 25), slice(25, 50), slice(50, 75), slice(75, 85)]) + ]) + def test_block_slices(self, inputs, results): + n_total, n_per_block = inputs + assert block_slices(n_total, n_per_block) == results + + @pytest.mark.parametrize("inputs, results", [ + ((100, 4), + [slice(0, 25), slice(25, 50), slice(50, 75), slice(75, 100)]), + ((77, 3), + [slice(0, 25), slice(25, 50), slice(50, 75), slice(75, 77)]), + ((2, 20), + [slice(0, 1), slice(1, 2)]) + ]) + def test_default_slice_even_split(self, inputs, results): + n_total, n_workers = inputs + assert default_slices(n_total, n_workers) == results + +class TestTasks(object): + def setup(self): + self.contact_freq_0_4 = ContactFrequency(traj, cutoff=0.075, + n_neighbors_ignored=0, + frames=range(4)) + self.contact_freq_4 = ContactFrequency(traj, cutoff=0.075, + n_neighbors_ignored=0, + frames=[4]) + self.total_contact_freq = ContactFrequency(traj, cutoff=0.075, + n_neighbors_ignored=0) + self.parameters = {'cutoff': 0.075, 'n_neighbors_ignored': 0} + + def test_load_trajectory_task(self): + subslice = slice(0, 4) + file_name = find_testfile("trajectory.pdb") + trajectory = load_trajectory_task(subslice, file_name) + assert trajectory.xyz.shape == (4, 10, 3) + + def test_map_task(self): + trajectory = traj[:4] + mapped = map_task(trajectory, parameters=self.parameters) + assert mapped == self.contact_freq_0_4 + + def test_reduce_task(self): + reduced = reduce_all_results([self.contact_freq_0_4, + self.contact_freq_4]) + assert reduced == self.total_contact_freq + + def test_map_task_json(self): + # check the json objects by converting them back to full objects + trajectory = traj[:4] + mapped = map_task_json(trajectory, parameters=self.parameters) + assert ContactFrequency.from_json(mapped) == self.contact_freq_0_4 + + def test_reduce_all_results_json(self): + reduced = reduce_all_results_json([self.contact_freq_0_4.to_json(), + self.contact_freq_4.to_json()]) + assert reduced == self.total_contact_freq + + def test_integration_object_based(self): + file_name = find_testfile("trajectory.pdb") + slices = default_slices(len(traj), n_workers=3) + trajs = [load_trajectory_task(subslice=sl, + file_name=file_name) + for sl in slices] + mapped = [map_task(subtraj, self.parameters) for subtraj in trajs] + result = reduce_all_results(mapped) + assert result == self.total_contact_freq + + def test_integration_json_based(self): + file_name = find_testfile("trajectory.pdb") + slices = default_slices(len(traj), n_workers=3) + trajs = [load_trajectory_task(subslice=sl, + file_name=file_name) + for sl in slices] + mapped = [map_task_json(subtraj, self.parameters) + for subtraj in trajs] + result = reduce_all_results_json(mapped) + assert result == self.total_contact_freq + diff --git a/docs/api.rst b/docs/api.rst index 7aaabf8..aa3634d 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -28,3 +28,12 @@ Minimum Distance (and related) MinimumDistanceCounter NearestAtoms + +Parallelization of ``ContactFrequency`` +--------------------------------------- + +.. autosummary:: + :toctree: api/generated/ + + frequency_task + dask_runner diff --git a/examples/dask_contact_frequency.ipynb b/examples/dask_contact_frequency.ipynb new file mode 100644 index 0000000..bc291c3 --- /dev/null +++ b/examples/dask_contact_frequency.ipynb @@ -0,0 +1,232 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Parallel `ContactFrequency` with Dask\n", + "\n", + "In principle, each frame that makes up a `ContactFrequency` can have its contact map calculated in parallel. This shows how to use [`dask.distributed`](https://distributed.readthedocs.io/) to do this.\n", + "\n", + "This will use the same example data as the main contact maps example (data from https://figshare.com/s/453b1b215cf2f9270769). See that example, `contact_map.ipynb`, for details." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib inline\n", + "import mdtraj as md\n", + "# dask and distributed are extra installs\n", + "from dask.distributed import Client, LocalCluster\n", + "from contact_map.dask_runner import dask_run" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "trajectory = md.load(\"5550217/kras.xtc\", top=\"5550217/kras.pdb\")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "# TODO: we need a more user-friendly approach for this than what we see here\n", + "run_info = {\n", + " 'trajectory_file': \"5550217/kras.xtc\",\n", + " 'load_kwargs': {'top': \"5550217/kras.pdb\"},\n", + " 'parameters': {}\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next we need to connect a client to a dask network.\n", + "\n", + "Note that there are several ways to set up the dask computer network and then connect a client to it. See https://distributed.readthedocs.io/en/latest/setup.html. The approach used here creates a `LocalCluster`. Large scale simulations would need other approaches. For clusters, you can manually run a `dask-scheduler` and multiple `dask-worker` commands. By using the same `sched.json`, it is easy to have different workers in different jobs on the cluster's scheduling system." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "c = LocalCluster()\n", + "client = Client(c)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "# if you started on a cluster and the scheduler file is called sched.json\n", + "#client = Client(scheduler_file=\"./sched.json\")" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "
\n", + "

Client

\n", + "\n", + "
\n", + "

Cluster

\n", + "
    \n", + "
  • Workers: 4
  • \n", + "
  • Cores: 4
  • \n", + "
  • Memory: 17.18 GB
  • \n", + "
\n", + "
" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "client" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 329 ms, sys: 226 ms, total: 554 ms\n", + "Wall time: 4.88 s\n" + ] + } + ], + "source": [ + "%%time\n", + "freq = dask_run(trajectory=trajectory, client=client, run_info=run_info)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note that on a single machine (shared memory) this may not improve performance. That is because the single-frame aspect of this calculation is already parallelized with OpenMP, and will therefore use all cores on the machine.\n", + "\n", + "Next we check that we're still getting the same results:" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "101" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# did it add up to give us the right number of frames?\n", + "freq.n_frames" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# do we get a familiar-looking residue map?\n", + "fig, ax = freq.residue_contacts.plot()" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "# Something like this is supposed to shut down the workers and the scheduler\n", + "# I get it to shut down workers, but not scheduler... and does it all with lots of warnings\n", + "#client.loop.add_callback(client.scheduler.retire_workers, close_workers=True)\n", + "#client.loop.add_callback(client.scheduler.terminate)\n", + "#client.run_on_scheduler(lambda dask_scheduler: dask_scheduler.loop.stop())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "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.14" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/optional_installs.txt b/optional_installs.txt index 6ccafc3..9dba780 100644 --- a/optional_installs.txt +++ b/optional_installs.txt @@ -1 +1,3 @@ matplotlib +dask +distributed