Skip to content

Commit 52f6e7b

Browse files
Add number of hydrogen bonds widget (#61)
1 parent c9af408 commit 52f6e7b

5 files changed

Lines changed: 355 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ The rules for this file:
3232
- Added widget for RMSD (PR #54)
3333
- Added widget for Native Contacts (PR #59)
3434
- Added widget for Contacts within cutoff (PR #60)
35+
- Added widget for number of Hydrogen bonds (PR #61)
3536

3637
### Fixed
3738

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+
hydrogen_bonds,
1213
janin,
1314
msd,
1415
native_contacts,
@@ -24,6 +25,7 @@
2425
"custom_code",
2526
"dssp",
2627
"energies",
28+
"hydrogen_bonds",
2729
"janin",
2830
"msd",
2931
"native_contacts",
Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
"""
2+
Number of Hydrogen bonds
3+
"""
4+
5+
import logging
6+
from collections import deque
7+
from typing import ClassVar
8+
9+
import matplotlib.pyplot as plt
10+
import numpy as np
11+
from IPython.display import display
12+
from joblib import delayed
13+
from MDAnalysis.analysis.hydrogenbonds import HydrogenBondAnalysis
14+
15+
from mdadash.backend.widgets.base import WidgetBase
16+
17+
logger = logging.getLogger(__name__)
18+
19+
20+
class HydrogenBonds(WidgetBase):
21+
"""
22+
23+
**Number of Hydrogen bonds**
24+
25+
This widget uses `MDAnalysis.analysis.hydrogenbonds.hbond_analysis.HydrogenBondAnalysis`_
26+
to calculate the number of Hydrogen bonds based on donor, hydrogens and acceptor
27+
selections. If donors selection is empty, the Universe topology must contain bonding
28+
information. If hydrogens selection or acceptors selection is empty, they are guessed
29+
and the Universe must contain charge information for this to work.
30+
31+
It is highly recommended that a universe topology with bond information is used,
32+
as this is the only way that guarantees the correct identification of donor-hydrogen pairs.
33+
34+
The number of Hydrogen bonds over time in this widget is calculated based on the
35+
MDAnalysis `Calculating hydrogen bonds`_ example.
36+
37+
.. _MDAnalysis.analysis.hydrogenbonds.hbond_analysis.HydrogenBondAnalysis: https://
38+
docs.mdanalysis.org/stable/documentation_pages/analysis/hydrogenbonds.html
39+
#MDAnalysis.analysis.hydrogenbonds.hbond_analysis.HydrogenBondAnalysis
40+
41+
.. _Calculating hydrogen bonds: https://userguide.mdanalysis.org/stable/
42+
examples/analysis/hydrogen_bonds/hbonds.html
43+
44+
"""
45+
46+
name = "Hydrogen bonds"
47+
description = "Number of Hydrogen bonds"
48+
49+
_notes = (
50+
"It is highly recommended that a universe topology with bond information "
51+
"is used, as this is the only way that guarantees the correct identification "
52+
"of donor-hydrogen pairs."
53+
)
54+
55+
_inputs: ClassVar = [
56+
{
57+
"attribute": "_run_frequency",
58+
"name": "Run frequency",
59+
"description": "The frequency with which the widget is run",
60+
"type": "select",
61+
"items": [
62+
"every-frame",
63+
"batch",
64+
],
65+
},
66+
{
67+
"attribute": "_run_mode",
68+
"name": "Run mode",
69+
"description": "The mode in which the widget is run",
70+
"type": "select",
71+
"items": [
72+
"serial",
73+
"parallel",
74+
],
75+
},
76+
{
77+
"attribute": "donors_sel",
78+
"name": "Donor atoms",
79+
"description": "MDAnalysis selection phrase of donor atoms",
80+
"type": "str",
81+
},
82+
{
83+
"attribute": "hydrogens_sel",
84+
"name": "Hydrogen atoms",
85+
"description": "MDAnalysis selection phrase of hydrogen atoms",
86+
"type": "str",
87+
},
88+
{
89+
"attribute": "acceptors_sel",
90+
"name": "Acceptor atoms",
91+
"description": "MDAnalysis selection phrase of acceptor atoms",
92+
"type": "str",
93+
},
94+
{
95+
"attribute": "d_h_cutoff",
96+
"name": "d_h_cutoff",
97+
"description": "Distance cutoff used for finding donor-hydrogen pairs",
98+
"type": "float",
99+
},
100+
{
101+
"attribute": "d_a_cutoff",
102+
"name": "d_a_cutoff",
103+
"description": "Distance cutoff for hydrogen bonds",
104+
"type": "float",
105+
},
106+
{
107+
"attribute": "d_h_a_angle_cutoff",
108+
"name": "d_h_a_angle_cutoff",
109+
"description": "D-H-A angle cutoff for hydrogen bonds, in degrees",
110+
"type": "float",
111+
},
112+
{
113+
"attribute": "update_selections",
114+
"name": "Update selections",
115+
"description": "Whether or not to update the selections every frame",
116+
"type": "bool",
117+
},
118+
{
119+
"attribute": "custom_title",
120+
"name": "Custom title",
121+
"description": "Custom title for the plot",
122+
"type": "str",
123+
},
124+
{
125+
"attribute": "maxlen",
126+
"name": "Max values",
127+
"description": "Max values to show in plot",
128+
"type": "int",
129+
},
130+
{
131+
"attribute": "x_type",
132+
"name": "X-axis",
133+
"type": "toggle",
134+
"options": [
135+
{"name": "Time", "value": "time"},
136+
{"name": "Step", "value": "step"},
137+
],
138+
},
139+
]
140+
141+
def __init__(self):
142+
super().__init__()
143+
self.donors_sel = "name O* N*"
144+
self.hydrogens_sel = "name H*"
145+
self.acceptors_sel = "name O* N*"
146+
self.d_h_cutoff = 1.2
147+
self.d_a_cutoff = 3.0
148+
self.d_h_a_angle_cutoff = 150.0
149+
self.update_selections = True
150+
self.hba = None
151+
self.title = "Hydrogen bonds"
152+
self.custom_title = None
153+
self.default_maxlen = 100
154+
self.maxlen = self.default_maxlen
155+
self.x_type = "time"
156+
self.x_values = None
157+
self._setup_plot()
158+
self._reset_plot_values()
159+
160+
def _setup_plot(self):
161+
"""Setup matplotlib plot"""
162+
self.fig, self.ax = plt.subplots()
163+
(self.plot,) = self.ax.plot([], [])
164+
self.ax.set_ylabel("Number of Hydrogen bonds")
165+
self.ax.grid(True)
166+
self._set_title()
167+
168+
def _reset_plot_values(self):
169+
"""Reset plot values"""
170+
self.steps = deque(maxlen=self.maxlen)
171+
self.times = deque(maxlen=self.maxlen)
172+
self.y_values = deque(maxlen=self.maxlen)
173+
self._set_x_values()
174+
175+
def _set_title(self):
176+
"""Set plot title"""
177+
self.ax.set_title(
178+
self.custom_title.replace("\\n", "\n") if self.custom_title else self.title
179+
)
180+
181+
def _set_x_values(self):
182+
"""Set the values for the x-axis"""
183+
if self.x_type == "step":
184+
x_label = "Step"
185+
self.x_values = self.steps
186+
else:
187+
x_label = "Time (ps)"
188+
self.x_values = self.times
189+
self.ax.set_xlabel(x_label)
190+
191+
def _create_hba(self):
192+
"""Update atom groups when selection phrases change"""
193+
self.hba = HydrogenBondAnalysis(
194+
universe=self.u,
195+
donors_sel=self.donors_sel if self.donors_sel else None,
196+
hydrogens_sel=self.hydrogens_sel if self.hydrogens_sel else None,
197+
acceptors_sel=self.acceptors_sel if self.acceptors_sel else None,
198+
d_h_cutoff=self.d_h_cutoff,
199+
d_a_cutoff=self.d_a_cutoff,
200+
d_h_a_angle_cutoff=self.d_h_a_angle_cutoff,
201+
update_selections=False,
202+
)
203+
self._update_plot(self._compute_current_frame())
204+
205+
def on_post_create(self):
206+
"""on_post_create handler"""
207+
self._set_title()
208+
self._reset_plot_values()
209+
210+
def on_post_connect(self):
211+
"""on_post_connect handler"""
212+
self._create_hba()
213+
214+
def on_input_change(self, attribute, _old_value, new_value):
215+
"""on_input_change handler"""
216+
if attribute == "maxlen":
217+
if new_value < 0:
218+
self.maxlen = self.default_maxlen
219+
self._reset_plot_values()
220+
elif attribute == "x_type":
221+
self._set_x_values()
222+
elif attribute == "custom_title":
223+
self._set_title()
224+
elif attribute in ("_run_mode", "_run_frequency"):
225+
pass
226+
else:
227+
self._reset_plot_values()
228+
self._create_hba()
229+
230+
def _compute_current_frame(self):
231+
"""Compute values for current frame"""
232+
self.hba.run(frames=[self.u.trajectory.frame])
233+
return (
234+
self.u.trajectory.ts.data["step"],
235+
self.u.trajectory.ts.data["time"],
236+
self.hba.results.hbonds.shape[0],
237+
)
238+
239+
def _compute_batch(self):
240+
"""Compute values for current batch"""
241+
self.hba.run()
242+
values = []
243+
indices = np.searchsorted(
244+
self.hba.frames, self.hba.results.hbonds[:, 0].astype(int)
245+
)
246+
counts = np.bincount(indices, minlength=len(self.hba.times))
247+
for i, count in enumerate(counts):
248+
_ = self.u.trajectory[i]
249+
values.append(
250+
(
251+
self.u.trajectory.ts.data["step"],
252+
self.u.trajectory.ts.data["time"],
253+
count,
254+
)
255+
)
256+
return values
257+
258+
def _update_plot(self, values):
259+
"""Append values and update plot"""
260+
if isinstance(values, tuple):
261+
values = [values]
262+
# update plot points
263+
for value in values:
264+
(steps, times, v) = value
265+
self.steps.append(steps)
266+
self.times.append(times)
267+
self.y_values.append(v)
268+
# update plot
269+
self.plot.set_data(self.x_values, self.y_values)
270+
self.ax.relim()
271+
self.ax.autoscale_view()
272+
self.fig.canvas.draw()
273+
display(self.fig)
274+
275+
def run_every_frame(self):
276+
"""every-frame run handler"""
277+
self._update_plot(self._compute_current_frame())
278+
279+
def run_batch(self):
280+
"""batch run handler"""
281+
self._update_plot(self._compute_batch())
282+
283+
def get_parallel_job(self):
284+
"""get parallel job handler"""
285+
if self._run_frequency == "batch":
286+
return delayed(self._compute_batch)()
287+
return delayed(self._compute_current_frame)()
288+
289+
def apply_parallel_results(self, values):
290+
"""apply parallel results handler"""
291+
self._update_plot(values)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
*.npz
2+
*.lock

mdadash/backend/tests/test_server.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -720,6 +720,66 @@ async def test_widget_run_contacts_parallel_batch(_client, imd_server):
720720
await disconnect_from_simulation()
721721

722722

723+
async def test_widget_run_hbonds_serial_every_frame(_client, imd_server):
724+
uuid = await add_widget("Hydrogen bonds")
725+
await connect_to_simulation(imd_server)
726+
inputs = [
727+
("donors_sel", "name O* N*"),
728+
("hydrogens_sel", "name H*"),
729+
("acceptors_sel", "name O* N*"),
730+
("maxlen", -1),
731+
("x_type", "step"),
732+
("custom_title", "Title"),
733+
]
734+
await check_input_changes(uuid, inputs)
735+
await resume_simulation(imd_server)
736+
assert await sio_event_emitted(sio, "widgets:output", n=1)
737+
await remove_widget(uuid)
738+
await disconnect_from_simulation()
739+
740+
741+
async def test_widget_run_hbonds_serial_batch(_client, imd_server):
742+
uuid = await add_widget("Hydrogen bonds")
743+
await connect_to_simulation(imd_server)
744+
inputs = [
745+
("_run_frequency", "batch"),
746+
]
747+
await check_input_changes(uuid, inputs)
748+
await resume_simulation(imd_server)
749+
assert await sio_event_emitted(sio, "widgets:output", n=1)
750+
await remove_widget(uuid)
751+
await disconnect_from_simulation()
752+
753+
754+
async def test_widget_run_hbonds_parallel_every_frame(_client, imd_server):
755+
uuid = await add_widget("Hydrogen bonds")
756+
inputs = [
757+
("_run_mode", "parallel"),
758+
]
759+
await check_input_changes(uuid, inputs)
760+
await connect_to_simulation(imd_server)
761+
await resume_simulation(imd_server)
762+
timeout = 30 if sys.platform == "win32" else 20
763+
assert await sio_event_emitted(sio, "widgets:output", n=1, timeout=timeout)
764+
await remove_widget(uuid)
765+
await disconnect_from_simulation()
766+
767+
768+
async def test_widget_run_hbonds_parallel_batch(_client, imd_server):
769+
uuid = await add_widget("Hydrogen bonds")
770+
inputs = [
771+
("_run_frequency", "batch"),
772+
("_run_mode", "parallel"),
773+
]
774+
await check_input_changes(uuid, inputs)
775+
await connect_to_simulation(imd_server)
776+
await resume_simulation(imd_server)
777+
timeout = 30 if sys.platform == "win32" else 20
778+
assert await sio_event_emitted(sio, "widgets:output", n=1, timeout=timeout)
779+
await remove_widget(uuid)
780+
await disconnect_from_simulation()
781+
782+
723783
async def test_widget_run_dssp_serial_every_frame(_client, imd_server):
724784
await connect_to_simulation(imd_server)
725785
uuid = await add_widget("DSSP Analysis")

0 commit comments

Comments
 (0)