Skip to content

Commit c1f133c

Browse files
Add widget for Native Contacts (MDAnalysis#59)
1 parent 0c387e5 commit c1f133c

11 files changed

Lines changed: 490 additions & 36 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ The rules for this file:
3030
<!-- New added features -->
3131

3232
- Added widget for RMSD (PR #54)
33+
- Added widget for Native Contacts (PR #59)
3334

3435
### Fixed
3536

@@ -42,6 +43,8 @@ The rules for this file:
4243

4344
<!-- Changes in existing functionality -->
4445

46+
- Changed color theme to MDA colors (PR #58)
47+
4548
### Deprecated
4649

4750
<!-- Soon-to-be removed features -->

mdadash/backend/analyses/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
energies,
1111
janin,
1212
msd,
13+
native_contacts,
1314
ramachandran,
1415
rmsd,
1516
rog,
@@ -23,6 +24,7 @@
2324
"energies",
2425
"janin",
2526
"msd",
27+
"native_contacts",
2628
"ramachandran",
2729
"rmsd",
2830
"rog",

mdadash/backend/analyses/acf.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,9 @@ def _set_title(self):
143143
)
144144
else:
145145
title = f"{self.physical_property.title()} ACF of '{self.selection}'"
146-
self.ax.set_title(self.custom_title if self.custom_title else title)
146+
self.ax.set_title(
147+
self.custom_title.replace("\\n", "\n") if self.custom_title else title
148+
)
147149

148150
def _set_y_label(self):
149151
"""Set plot y label"""

mdadash/backend/analyses/com_distance.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,9 @@ def _reset_plot_values(self):
129129

130130
def _set_title(self):
131131
"""Set plot title"""
132-
self.ax.set_title(self.custom_title if self.custom_title else self.title)
132+
self.ax.set_title(
133+
self.custom_title.replace("\\n", "\n") if self.custom_title else self.title
134+
)
133135

134136
def _set_x_values(self):
135137
"""Set the values for the x-axis"""

mdadash/backend/analyses/dssp.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,9 @@ def _reset_plot_values(self):
111111

112112
def _set_title(self):
113113
"""Set plot title"""
114-
self.ax.set_title(self.custom_title if self.custom_title else "", pad=40)
114+
self.ax.set_title(
115+
self.custom_title.replace("\\n", "\n") if self.custom_title else "", pad=40
116+
)
115117

116118
def _set_x_values(self):
117119
"""Set the values for the x-axis"""

mdadash/backend/analyses/msd.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,9 @@ def _set_title(self):
119119
title = f"Diffusion coefficient of '{self.selection}'"
120120
else:
121121
title = f"MSD of '{self.selection}'"
122-
self.ax.set_title(self.custom_title if self.custom_title else title)
122+
self.ax.set_title(
123+
self.custom_title.replace("\\n", "\n") if self.custom_title else title
124+
)
123125

124126
def _set_y_label(self):
125127
"""Set plot y label"""
Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
"""
2+
Native Contacts Analysis
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.analysis import contacts
13+
14+
from mdadash.backend.widgets.base import WidgetBase
15+
16+
logger = logging.getLogger(__name__)
17+
18+
19+
class NativeContacts(WidgetBase):
20+
"""
21+
22+
**Native Contacts Analysis**
23+
24+
This widget uses `MDAnalysis.analysis.contacts.Contacts`_ to calculate fraction
25+
of native contacts between two contacting groups. The two contacting AtomGroups
26+
in their reference conformation are created when this widget instance is created
27+
or whenever the inputs for the above Class are updated.
28+
29+
.. _MDAnalysis.analysis.contacts.Contacts: https://docs.mdanalysis.org/stable/
30+
documentation_pages/analysis/contacts.html#MDAnalysis.analysis.contacts.Contacts
31+
32+
"""
33+
34+
name = "Native Contacts"
35+
description = "Native Contacts Analysis"
36+
37+
_notes = (
38+
"The two contacting AtomGroups in their reference conformation are created "
39+
"when this widget instance is created or whenever the inputs for the "
40+
"MDAnalysis.analysis.contacts.Contacts class from below are updated."
41+
)
42+
43+
_inputs: ClassVar = [
44+
{
45+
"attribute": "_run_frequency",
46+
"name": "Run frequency",
47+
"description": "The frequency with which the widget is run",
48+
"type": "select",
49+
"items": [
50+
"every-frame",
51+
"batch",
52+
],
53+
},
54+
{
55+
"attribute": "_run_mode",
56+
"name": "Run mode",
57+
"description": "The mode in which the widget is run",
58+
"type": "select",
59+
"items": [
60+
"serial",
61+
"parallel",
62+
],
63+
},
64+
{
65+
"attribute": "selection1",
66+
"name": "Contacting Group 1",
67+
"description": "MDAnalysis selection phrase of first group",
68+
"type": "str",
69+
"validations": ["required"],
70+
},
71+
{
72+
"attribute": "selection2",
73+
"name": "Contacting Group 2",
74+
"description": "MDAnalysis selection phrase of second group",
75+
"type": "str",
76+
"validations": ["required"],
77+
},
78+
{
79+
"attribute": "radius",
80+
"name": "Radius",
81+
"description": "Radius within which contacts exist in refgroup",
82+
"type": "float",
83+
},
84+
{
85+
"attribute": "method",
86+
"name": "Method",
87+
"description": "Method to use for cut off",
88+
"type": "select",
89+
"items": [
90+
"hard_cut",
91+
"soft_cut",
92+
"radius_cut",
93+
],
94+
},
95+
{
96+
"attribute": "pbc",
97+
"name": "PBC",
98+
"description": "Uses periodic boundary conditions to calculate distances",
99+
"type": "bool",
100+
},
101+
{
102+
"attribute": "custom_title",
103+
"name": "Custom title",
104+
"description": "Custom title for the plot",
105+
"type": "str",
106+
},
107+
{
108+
"attribute": "maxlen",
109+
"name": "Max values",
110+
"description": "Max values to show in plot",
111+
"type": "int",
112+
},
113+
{
114+
"attribute": "x_type",
115+
"name": "X-axis",
116+
"type": "toggle",
117+
"options": [
118+
{"name": "Time", "value": "time"},
119+
{"name": "Step", "value": "step"},
120+
],
121+
},
122+
]
123+
124+
def __init__(self):
125+
super().__init__()
126+
self.selection1 = "protein and name CA"
127+
self.selection2 = "protein and name CA"
128+
self.radius = 4.5
129+
self.method = "hard_cut"
130+
self.pbc = True
131+
self.contacts = None
132+
self.refgroup_ag1 = None
133+
self.refgroup_ag2 = None
134+
self.title = "Native Contacts"
135+
self.custom_title = None
136+
self.default_maxlen = 100
137+
self.maxlen = self.default_maxlen
138+
self.x_type = "time"
139+
self.x_values = None
140+
self._setup_plot()
141+
self._reset_plot_values()
142+
143+
def _setup_plot(self):
144+
"""Setup matplotlib plot"""
145+
self.fig, self.ax = plt.subplots()
146+
(self.plot,) = self.ax.plot([], [])
147+
self.ax.set_ylabel("Fraction of contacts")
148+
self.ax.grid(True)
149+
self._set_title()
150+
151+
def _reset_plot_values(self):
152+
"""Reset plot values"""
153+
self.steps = deque(maxlen=self.maxlen)
154+
self.times = deque(maxlen=self.maxlen)
155+
self.y_values = deque(maxlen=self.maxlen)
156+
self._set_x_values()
157+
158+
def _set_title(self):
159+
"""Set plot title"""
160+
self.ax.set_title(
161+
self.custom_title.replace("\\n", "\n") if self.custom_title else self.title
162+
)
163+
164+
def _set_x_values(self):
165+
"""Set the values for the x-axis"""
166+
if self.x_type == "step":
167+
x_label = "Step"
168+
self.x_values = self.steps
169+
else:
170+
x_label = "Time (ps)"
171+
self.x_values = self.times
172+
self.ax.set_xlabel(x_label)
173+
174+
def _create_contacts(self):
175+
"""Update atom groups when selection phrases change"""
176+
self.refgroup_ag1 = self.u.select_atoms(self.selection1)
177+
self.refgroup_ag2 = self.u.select_atoms(self.selection2)
178+
self.contacts = contacts.Contacts(
179+
self.u,
180+
select=(self.selection1, self.selection2),
181+
refgroup=(self.refgroup_ag1, self.refgroup_ag2),
182+
radius=self.radius,
183+
method=self.method,
184+
pbc=self.pbc,
185+
)
186+
self.title = (
187+
f"Native contacts between\n'{self.selection1}' and '{self.selection2}'"
188+
)
189+
self._set_title()
190+
self._update_plot(self._compute_current_frame())
191+
192+
def on_post_create(self):
193+
"""on_post_create handler"""
194+
self._set_title()
195+
self._reset_plot_values()
196+
197+
def on_post_connect(self):
198+
"""on_post_connect handler"""
199+
self._create_contacts()
200+
201+
def on_input_change(self, attribute, _old_value, new_value):
202+
"""on_input_change handler"""
203+
if attribute == "maxlen":
204+
if new_value < 0:
205+
self.maxlen = self.default_maxlen
206+
self._reset_plot_values()
207+
elif attribute == "x_type":
208+
self._set_x_values()
209+
elif attribute == "custom_title":
210+
self._set_title()
211+
elif attribute in (
212+
"selection1",
213+
"selection2",
214+
"radius",
215+
"method",
216+
"pbc",
217+
):
218+
self._reset_plot_values()
219+
self._create_contacts()
220+
221+
def _compute_current_frame(self):
222+
"""Compute values for current frame"""
223+
self.contacts.run(frames=[self.u.trajectory.frame])
224+
return (
225+
self.u.trajectory.ts.data["step"],
226+
self.u.trajectory.ts.data["time"],
227+
self.contacts.results.timeseries[0][1],
228+
)
229+
230+
def _compute_batch(self):
231+
"""Compute values for current batch"""
232+
self.contacts.run()
233+
values = []
234+
for i, (_, q) in enumerate(self.contacts.results.timeseries):
235+
_ = self.u.trajectory[i]
236+
values.append(
237+
(
238+
self.u.trajectory.ts.data["step"],
239+
self.u.trajectory.ts.data["time"],
240+
q,
241+
)
242+
)
243+
return values
244+
245+
def _update_plot(self, values):
246+
"""Append values and update plot"""
247+
if isinstance(values, tuple):
248+
values = [values]
249+
# update plot points
250+
for value in values:
251+
(steps, times, v) = value
252+
self.steps.append(steps)
253+
self.times.append(times)
254+
self.y_values.append(v)
255+
# update plot
256+
self.plot.set_data(self.x_values, self.y_values)
257+
self.ax.relim()
258+
self.ax.autoscale_view()
259+
self.fig.canvas.draw()
260+
display(self.fig)
261+
262+
def run_every_frame(self):
263+
"""every-frame run handler"""
264+
self._update_plot(self._compute_current_frame())
265+
266+
def run_batch(self):
267+
"""batch run handler"""
268+
self._update_plot(self._compute_batch())
269+
270+
def get_parallel_job(self):
271+
"""get parallel job handler"""
272+
if self._run_frequency == "batch":
273+
return delayed(self._compute_batch)()
274+
return delayed(self._compute_current_frame)()
275+
276+
def apply_parallel_results(self, values):
277+
"""apply parallel results handler"""
278+
self._update_plot(values)

0 commit comments

Comments
 (0)