Skip to content

Commit b416aba

Browse files
Add Helix analysis widget (MDAnalysis#62)
1 parent 52f6e7b commit b416aba

4 files changed

Lines changed: 313 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ The rules for this file:
3333
- Added widget for Native Contacts (PR #59)
3434
- Added widget for Contacts within cutoff (PR #60)
3535
- Added widget for number of Hydrogen bonds (PR #61)
36+
- Added widget for Helix Analysis (PR #62)
3637

3738
### Fixed
3839

mdadash/backend/analyses/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
custom_code,
1010
dssp,
1111
energies,
12+
helix_analysis,
1213
hydrogen_bonds,
1314
janin,
1415
msd,
@@ -25,6 +26,7 @@
2526
"custom_code",
2627
"dssp",
2728
"energies",
29+
"helix_analysis",
2830
"hydrogen_bonds",
2931
"janin",
3032
"msd",
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
"""
2+
Helix Analysis
3+
"""
4+
5+
import logging
6+
import warnings
7+
from collections import deque
8+
from typing import ClassVar
9+
10+
import matplotlib.pyplot as plt
11+
from IPython.display import display
12+
from joblib import delayed
13+
from MDAnalysis.analysis.helix_analysis import HELANAL
14+
15+
from mdadash.backend.widgets.base import WidgetBase
16+
17+
logger = logging.getLogger(__name__)
18+
19+
20+
class HelixAnalysis(WidgetBase):
21+
"""
22+
23+
**Helix Analysis**
24+
25+
This widget uses `MDAnalysis.analysis.helix_analysis.HELANAL`_ to perform helix
26+
analysis and plot different computed properties as per the MDAnalysis
27+
`Helix analysis`_ examples.
28+
29+
.. _MDAnalysis.analysis.helix_analysis.HELANAL: https://docs.mdanalysis.org/
30+
stable/documentation_pages/analysis/helix_analysis.html
31+
#MDAnalysis.analysis.helix_analysis.HELANAL
32+
33+
.. _Helix analysis: https://userguide.mdanalysis.org/stable/examples/
34+
analysis/structure/helanal.html
35+
36+
"""
37+
38+
name = "Helix Analysis"
39+
description = "Helix analysis using HELANAL"
40+
41+
_inputs: ClassVar = [
42+
{
43+
"attribute": "_run_frequency",
44+
"name": "Run frequency",
45+
"description": "The frequency with which the widget is run",
46+
"type": "select",
47+
"items": [
48+
"every-frame",
49+
"batch",
50+
],
51+
},
52+
{
53+
"attribute": "_run_mode",
54+
"name": "Run mode",
55+
"description": "The mode in which the widget is run",
56+
"type": "select",
57+
"items": [
58+
"serial",
59+
"parallel",
60+
],
61+
},
62+
{
63+
"attribute": "selection",
64+
"name": "Selection",
65+
"description": "MDAnalysis selection phrase",
66+
"type": "str",
67+
"validations": ["required"],
68+
},
69+
{
70+
"attribute": "property",
71+
"name": "Property",
72+
"description": "Computed property to plot",
73+
"type": "select",
74+
"items": [
75+
"local_twists",
76+
"local_nres_per_turn",
77+
"local_bends",
78+
"local_heights",
79+
"local_screw_angles",
80+
],
81+
},
82+
{
83+
"attribute": "custom_title",
84+
"name": "Custom title",
85+
"description": "Custom title for the plot",
86+
"type": "str",
87+
},
88+
{
89+
"attribute": "maxlen",
90+
"name": "Max values",
91+
"description": "Max values to show in plot",
92+
"type": "int",
93+
},
94+
{
95+
"attribute": "x_type",
96+
"name": "X-axis",
97+
"type": "toggle",
98+
"options": [
99+
{"name": "Time", "value": "time"},
100+
{"name": "Step", "value": "step"},
101+
],
102+
},
103+
]
104+
105+
def __init__(self):
106+
super().__init__()
107+
self.selection = "resid 1:10"
108+
self.property = "local_twists"
109+
self.ha = None
110+
self.title = "Helix Analysis"
111+
self.custom_title = None
112+
self.default_maxlen = 100
113+
self.maxlen = self.default_maxlen
114+
self.x_type = "time"
115+
self.x_values = None
116+
self.y_labels = {
117+
"local_twists": "Average local twist (degrees)",
118+
"local_nres_per_turn": "Average residues per turn",
119+
"local_bends": "Average local bends (degrees)",
120+
"local_heights": "Average rise of each local helix (Å)",
121+
"local_screw_angles": "Average local screw angle (degrees)",
122+
}
123+
self._setup_plot()
124+
self._reset_plot_values()
125+
126+
def _setup_plot(self):
127+
"""Setup matplotlib plot"""
128+
self.fig, self.ax = plt.subplots()
129+
(self.plot,) = self.ax.plot([], [])
130+
self.ax.grid(True)
131+
self._set_title()
132+
133+
def _reset_plot_values(self):
134+
"""Reset plot values"""
135+
self.steps = deque(maxlen=self.maxlen)
136+
self.times = deque(maxlen=self.maxlen)
137+
self.y_values = deque(maxlen=self.maxlen)
138+
self.ax.set_ylabel(self.y_labels[self.property])
139+
self._set_x_values()
140+
141+
def _set_title(self):
142+
"""Set plot title"""
143+
self.ax.set_title(
144+
self.custom_title.replace("\\n", "\n") if self.custom_title else self.title
145+
)
146+
147+
def _set_x_values(self):
148+
"""Set the values for the x-axis"""
149+
if self.x_type == "step":
150+
x_label = "Step"
151+
self.x_values = self.steps
152+
else:
153+
x_label = "Time (ps)"
154+
self.x_values = self.times
155+
self.ax.set_xlabel(x_label)
156+
157+
def _create_ha(self):
158+
"""Update atom groups when selection phrases change"""
159+
self.ha = HELANAL(self.u, select=self.selection)
160+
self.title = f"Helix analysis of '{self.selection}'"
161+
self._set_title()
162+
163+
def on_post_create(self):
164+
"""on_post_create handler"""
165+
self._set_title()
166+
self._reset_plot_values()
167+
168+
def on_post_connect(self):
169+
"""on_post_connect handler"""
170+
self._create_ha()
171+
172+
def on_input_change(self, attribute, _old_value, new_value):
173+
"""on_input_change handler"""
174+
if attribute == "maxlen":
175+
if new_value < 0:
176+
self.maxlen = self.default_maxlen
177+
self._reset_plot_values()
178+
elif attribute == "x_type":
179+
self._set_x_values()
180+
elif attribute == "custom_title":
181+
self._set_title()
182+
elif attribute in ("selection", "property"):
183+
self._reset_plot_values()
184+
self._create_ha()
185+
186+
def _compute_current_frame(self):
187+
"""Compute values for current frame"""
188+
with warnings.catch_warnings():
189+
warnings.simplefilter("ignore", category=RuntimeWarning)
190+
self.ha.run(frames=[self.u.trajectory.frame])
191+
results = getattr(self.ha.results, self.property)
192+
mean_values = results.mean(axis=1)
193+
return (
194+
self.u.trajectory.ts.data["step"],
195+
self.u.trajectory.ts.data["time"],
196+
mean_values[0],
197+
)
198+
199+
def _compute_batch(self):
200+
"""Compute values for current batch"""
201+
with warnings.catch_warnings():
202+
warnings.simplefilter("ignore", category=RuntimeWarning)
203+
self.ha.run()
204+
results = getattr(self.ha.results, self.property)
205+
mean_values = results.mean(axis=1)
206+
values = []
207+
for i, v in enumerate(mean_values):
208+
_ = self.u.trajectory[i]
209+
values.append(
210+
(
211+
self.u.trajectory.ts.data["step"],
212+
self.u.trajectory.ts.data["time"],
213+
v,
214+
)
215+
)
216+
return values
217+
218+
def _update_plot(self, values):
219+
"""Append values and update plot"""
220+
if isinstance(values, tuple):
221+
values = [values]
222+
# update plot points
223+
for value in values:
224+
(steps, times, v) = value
225+
self.steps.append(steps)
226+
self.times.append(times)
227+
self.y_values.append(v)
228+
# update plot
229+
self.plot.set_data(self.x_values, self.y_values)
230+
self.ax.relim()
231+
self.ax.autoscale_view()
232+
self.fig.canvas.draw()
233+
display(self.fig)
234+
235+
def run_every_frame(self):
236+
"""every-frame run handler"""
237+
self._update_plot(self._compute_current_frame())
238+
239+
def run_batch(self):
240+
"""batch run handler"""
241+
self._update_plot(self._compute_batch())
242+
243+
def get_parallel_job(self):
244+
"""get parallel job handler"""
245+
if self._run_frequency == "batch":
246+
return delayed(self._compute_batch)()
247+
return delayed(self._compute_current_frame)()
248+
249+
def apply_parallel_results(self, values):
250+
"""apply parallel results handler"""
251+
self._update_plot(values)

mdadash/backend/tests/test_server.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -780,6 +780,65 @@ async def test_widget_run_hbonds_parallel_batch(_client, imd_server):
780780
await disconnect_from_simulation()
781781

782782

783+
async def test_widget_run_helix_analysis_serial_every_frame(_client, imd_server):
784+
uuid = await add_widget("Helix Analysis")
785+
await connect_to_simulation(imd_server)
786+
inputs = [
787+
("selection", "resid 1:10 and name CA"),
788+
("property", "local_twists"),
789+
("maxlen", -1),
790+
("x_type", "step"),
791+
("custom_title", "Title"),
792+
]
793+
await check_input_changes(uuid, inputs)
794+
await resume_simulation(imd_server)
795+
assert await sio_event_emitted(sio, "widgets:output", n=1)
796+
await remove_widget(uuid)
797+
await disconnect_from_simulation()
798+
799+
800+
async def test_widget_run_helix_analysis_serial_batch(_client, imd_server):
801+
uuid = await add_widget("Helix Analysis")
802+
await connect_to_simulation(imd_server)
803+
inputs = [
804+
("_run_frequency", "batch"),
805+
]
806+
await check_input_changes(uuid, inputs)
807+
await resume_simulation(imd_server)
808+
assert await sio_event_emitted(sio, "widgets:output", n=1)
809+
await remove_widget(uuid)
810+
await disconnect_from_simulation()
811+
812+
813+
async def test_widget_run_helix_analysis_parallel_every_frame(_client, imd_server):
814+
uuid = await add_widget("Helix Analysis")
815+
inputs = [
816+
("_run_mode", "parallel"),
817+
]
818+
await check_input_changes(uuid, inputs)
819+
await connect_to_simulation(imd_server)
820+
await resume_simulation(imd_server)
821+
timeout = 30 if sys.platform == "win32" else 20
822+
assert await sio_event_emitted(sio, "widgets:output", n=1, timeout=timeout)
823+
await remove_widget(uuid)
824+
await disconnect_from_simulation()
825+
826+
827+
async def test_widget_run_helix_analysis_parallel_batch(_client, imd_server):
828+
uuid = await add_widget("Helix Analysis")
829+
inputs = [
830+
("_run_frequency", "batch"),
831+
("_run_mode", "parallel"),
832+
]
833+
await check_input_changes(uuid, inputs)
834+
await connect_to_simulation(imd_server)
835+
await resume_simulation(imd_server)
836+
timeout = 30 if sys.platform == "win32" else 20
837+
assert await sio_event_emitted(sio, "widgets:output", n=1, timeout=timeout)
838+
await remove_widget(uuid)
839+
await disconnect_from_simulation()
840+
841+
783842
async def test_widget_run_dssp_serial_every_frame(_client, imd_server):
784843
await connect_to_simulation(imd_server)
785844
uuid = await add_widget("DSSP Analysis")

0 commit comments

Comments
 (0)