Skip to content

Commit c9af408

Browse files
Add Contacts within cutoff widget (#60)
1 parent c1f133c commit c9af408

4 files changed

Lines changed: 294 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ The rules for this file:
3131

3232
- Added widget for RMSD (PR #54)
3333
- Added widget for Native Contacts (PR #59)
34+
- Added widget for Contacts within cutoff (PR #60)
3435

3536
### Fixed
3637

mdadash/backend/analyses/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from . import (
66
acf,
77
com_distance,
8+
contacts,
89
custom_code,
910
dssp,
1011
energies,
@@ -19,6 +20,7 @@
1920
__all__ = [
2021
"acf",
2122
"com_distance",
23+
"contacts",
2224
"custom_code",
2325
"dssp",
2426
"energies",
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
"""
2+
Contacts within a cutoff
3+
"""
4+
5+
import logging
6+
from collections import deque
7+
from typing import ClassVar
8+
9+
import matplotlib.pyplot as plt
10+
from IPython.display import display
11+
from joblib import delayed
12+
from MDAnalysis.lib.distances import capped_distance
13+
14+
from mdadash.backend.widgets.base import WidgetBase
15+
16+
logger = logging.getLogger(__name__)
17+
18+
19+
class Contacts(WidgetBase):
20+
"""
21+
22+
**Contacts within a cutoff**
23+
24+
This widget uses `MDAnalysis.lib.distances.capped_distance`_ to calculate number
25+
of contacts with a given cutoff between two groups.
26+
27+
.. _MDAnalysis.lib.distances.capped_distance: https://docs.mdanalysis.org/stable/
28+
documentation_pages/lib/distances.html#MDAnalysis.lib.distances.capped_distance
29+
30+
"""
31+
32+
name = "Contacts"
33+
description = "Contacts within a cutoff"
34+
35+
_inputs: ClassVar = [
36+
{
37+
"attribute": "_run_frequency",
38+
"name": "Run frequency",
39+
"description": "The frequency with which the widget is run",
40+
"type": "select",
41+
"items": [
42+
"every-frame",
43+
"batch",
44+
],
45+
},
46+
{
47+
"attribute": "_run_mode",
48+
"name": "Run mode",
49+
"description": "The mode in which the widget is run",
50+
"type": "select",
51+
"items": [
52+
"serial",
53+
"parallel",
54+
],
55+
},
56+
{
57+
"attribute": "selection1",
58+
"name": "Contacting Group 1",
59+
"description": "MDAnalysis selection phrase of first group",
60+
"type": "str",
61+
"validations": ["required"],
62+
},
63+
{
64+
"attribute": "selection2",
65+
"name": "Contacting Group 2",
66+
"description": "MDAnalysis selection phrase of second group",
67+
"type": "str",
68+
"validations": ["required"],
69+
},
70+
{
71+
"attribute": "radius",
72+
"name": "Radius",
73+
"description": "Radius within which contacts exist",
74+
"type": "float",
75+
},
76+
{
77+
"attribute": "custom_title",
78+
"name": "Custom title",
79+
"description": "Custom title for the plot",
80+
"type": "str",
81+
},
82+
{
83+
"attribute": "maxlen",
84+
"name": "Max values",
85+
"description": "Max values to show in plot",
86+
"type": "int",
87+
},
88+
{
89+
"attribute": "x_type",
90+
"name": "X-axis",
91+
"type": "toggle",
92+
"options": [
93+
{"name": "Time", "value": "time"},
94+
{"name": "Step", "value": "step"},
95+
],
96+
},
97+
]
98+
99+
def __init__(self):
100+
super().__init__()
101+
self.selection1 = "(resname ASP GLU) and (name OE* OD*)"
102+
self.selection2 = "(resname ARG LYS) and (name NH* NZ)"
103+
self.radius = 4.5
104+
self.ag1 = None
105+
self.ag2 = None
106+
self.title = "Contacts within cutoff"
107+
self.custom_title = None
108+
self.default_maxlen = 100
109+
self.maxlen = self.default_maxlen
110+
self.x_type = "time"
111+
self.x_values = None
112+
self._setup_plot()
113+
self._reset_plot_values()
114+
115+
def _setup_plot(self):
116+
"""Setup matplotlib plot"""
117+
self.fig, self.ax = plt.subplots()
118+
(self.plot,) = self.ax.plot([], [])
119+
self.ax.set_ylabel("Number of contacts")
120+
self.ax.grid(True)
121+
self._set_title()
122+
123+
def _reset_plot_values(self):
124+
"""Reset plot values"""
125+
self.steps = deque(maxlen=self.maxlen)
126+
self.times = deque(maxlen=self.maxlen)
127+
self.y_values = deque(maxlen=self.maxlen)
128+
self._set_x_values()
129+
130+
def _set_title(self):
131+
"""Set plot title"""
132+
self.ax.set_title(
133+
self.custom_title.replace("\\n", "\n") if self.custom_title else self.title
134+
)
135+
136+
def _set_x_values(self):
137+
"""Set the values for the x-axis"""
138+
if self.x_type == "step":
139+
x_label = "Step"
140+
self.x_values = self.steps
141+
else:
142+
x_label = "Time (ps)"
143+
self.x_values = self.times
144+
self.ax.set_xlabel(x_label)
145+
146+
def _update_selections(self):
147+
"""Update atom groups when selection phrases change"""
148+
self.ag1 = self.u.select_atoms(self.selection1)
149+
self.ag2 = self.u.select_atoms(self.selection2)
150+
self.title = f"Contacts between\n'{self.selection1}' and '{self.selection2}'"
151+
self._set_title()
152+
153+
def on_post_create(self):
154+
"""on_post_create handler"""
155+
self._set_title()
156+
self._reset_plot_values()
157+
158+
def on_post_connect(self):
159+
"""on_post_connect handler"""
160+
self._update_selections()
161+
162+
def on_input_change(self, attribute, _old_value, new_value):
163+
"""on_input_change handler"""
164+
if attribute == "maxlen":
165+
if new_value < 0:
166+
self.maxlen = self.default_maxlen
167+
self._reset_plot_values()
168+
elif attribute == "x_type":
169+
self._set_x_values()
170+
elif attribute == "custom_title":
171+
self._set_title()
172+
elif attribute in ("selection1", "selection2", "radius"):
173+
self._reset_plot_values()
174+
self._update_selections()
175+
176+
def _compute_current_frame(self):
177+
"""Compute values for current frame"""
178+
pairs = capped_distance(
179+
self.ag1.positions,
180+
self.ag2.positions,
181+
max_cutoff=self.radius,
182+
box=self.u.dimensions,
183+
return_distances=False,
184+
)
185+
return (
186+
self.u.trajectory.ts.data["step"],
187+
self.u.trajectory.ts.data["time"],
188+
len(pairs),
189+
)
190+
191+
def _compute_batch(self):
192+
"""Compute values for current batch"""
193+
values = []
194+
for i in range(self.u.trajectory.buffer_size):
195+
_ = self.u.trajectory[i]
196+
values.append(self._compute_current_frame())
197+
return values
198+
199+
def _update_plot(self, values):
200+
"""Append values and update plot"""
201+
if isinstance(values, tuple):
202+
values = [values]
203+
# update plot points
204+
for value in values:
205+
(steps, times, v) = value
206+
self.steps.append(steps)
207+
self.times.append(times)
208+
self.y_values.append(v)
209+
# update plot
210+
self.plot.set_data(self.x_values, self.y_values)
211+
self.ax.relim()
212+
self.ax.autoscale_view()
213+
self.fig.canvas.draw()
214+
display(self.fig)
215+
216+
def run_every_frame(self):
217+
"""every-frame run handler"""
218+
self._update_plot(self._compute_current_frame())
219+
220+
def run_batch(self):
221+
"""batch run handler"""
222+
self._update_plot(self._compute_batch())
223+
224+
def get_parallel_job(self):
225+
"""get parallel job handler"""
226+
if self._run_frequency == "batch":
227+
return delayed(self._compute_batch)()
228+
return delayed(self._compute_current_frame)()
229+
230+
def apply_parallel_results(self, values):
231+
"""apply parallel results handler"""
232+
self._update_plot(values)

mdadash/backend/tests/test_server.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -661,6 +661,65 @@ async def test_widget_run_native_contacts_parallel_batch(_client, imd_server):
661661
await disconnect_from_simulation()
662662

663663

664+
async def test_widget_run_contacts_serial_every_frame(_client, imd_server):
665+
uuid = await add_widget("Contacts")
666+
await connect_to_simulation(imd_server)
667+
inputs = [
668+
("selection1", "protein"),
669+
("selection2", "resid 1:10"),
670+
("maxlen", -1),
671+
("x_type", "step"),
672+
("custom_title", "Title"),
673+
]
674+
await check_input_changes(uuid, inputs)
675+
await resume_simulation(imd_server)
676+
assert await sio_event_emitted(sio, "widgets:output", n=1)
677+
await remove_widget(uuid)
678+
await disconnect_from_simulation()
679+
680+
681+
async def test_widget_run_contacts_serial_batch(_client, imd_server):
682+
uuid = await add_widget("Contacts")
683+
await connect_to_simulation(imd_server)
684+
inputs = [
685+
("_run_frequency", "batch"),
686+
]
687+
await check_input_changes(uuid, inputs)
688+
await resume_simulation(imd_server)
689+
assert await sio_event_emitted(sio, "widgets:output", n=1)
690+
await remove_widget(uuid)
691+
await disconnect_from_simulation()
692+
693+
694+
async def test_widget_run_contacts_parallel_every_frame(_client, imd_server):
695+
uuid = await add_widget("Contacts")
696+
inputs = [
697+
("_run_mode", "parallel"),
698+
]
699+
await check_input_changes(uuid, inputs)
700+
await connect_to_simulation(imd_server)
701+
await resume_simulation(imd_server)
702+
timeout = 30 if sys.platform == "win32" else 20
703+
assert await sio_event_emitted(sio, "widgets:output", n=1, timeout=timeout)
704+
await remove_widget(uuid)
705+
await disconnect_from_simulation()
706+
707+
708+
async def test_widget_run_contacts_parallel_batch(_client, imd_server):
709+
uuid = await add_widget("Contacts")
710+
inputs = [
711+
("_run_frequency", "batch"),
712+
("_run_mode", "parallel"),
713+
]
714+
await check_input_changes(uuid, inputs)
715+
await connect_to_simulation(imd_server)
716+
await resume_simulation(imd_server)
717+
timeout = 30 if sys.platform == "win32" else 20
718+
assert await sio_event_emitted(sio, "widgets:output", n=1, timeout=timeout)
719+
await remove_widget(uuid)
720+
await disconnect_from_simulation()
721+
722+
664723
async def test_widget_run_dssp_serial_every_frame(_client, imd_server):
665724
await connect_to_simulation(imd_server)
666725
uuid = await add_widget("DSSP Analysis")

0 commit comments

Comments
 (0)