Skip to content

Commit e085c34

Browse files
Add widget for RMSD (MDAnalysis#56)
1 parent 5508fdc commit e085c34

4 files changed

Lines changed: 212 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,15 @@ The rules for this file:
2929

3030
<!-- New added features -->
3131

32+
- Added widget for RMSD (PR #54)
33+
3234
### Fixed
3335

3436
<!-- Bug fixes -->
3537

38+
- Miscellaneous fixes (PR #50)
39+
- Minor UI fixes (PR #51)
40+
3641
### Changed
3742

3843
<!-- Changes in existing functionality -->

mdadash/backend/analyses/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
janin,
1212
msd,
1313
ramachandran,
14+
rmsd,
1415
rog,
1516
)
1617

@@ -23,5 +24,6 @@
2324
"janin",
2425
"msd",
2526
"ramachandran",
27+
"rmsd",
2628
"rog",
2729
]

mdadash/backend/analyses/rmsd.py

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
"""
2+
RMSD 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 MDAnalysis.analysis import rms
12+
13+
from mdadash.backend.widgets.base import WidgetBase
14+
15+
logger = logging.getLogger(__name__)
16+
17+
18+
class RMSD(WidgetBase):
19+
"""
20+
21+
**RMSD Analysis**
22+
23+
This widget uses `MDAnalysis.analysis.rms.rmsd`_ to calculate RMSD of a
24+
selection. The reference positions used by this widget are the initial
25+
positions of the selection when the widget instance is created or the
26+
initial positions whenever the selection is updated.
27+
28+
.. _MDAnalysis.analysis.rms.rmsd: https://docs.mdanalysis.org/stable/
29+
documentation_pages/analysis/rms.html#MDAnalysis.analysis.rms.rmsd
30+
31+
"""
32+
33+
name = "RMSD"
34+
description = "RMSD of a selection"
35+
36+
_notes = (
37+
"If simulations are performed under periodic boundary conditions "
38+
"then you must make your molecules whole before performing RMSD "
39+
"calculations so that the centers of mass of the mobile and reference "
40+
"structure are properly superimposed. You can add custom transformations "
41+
"to the universe in the Universe Configuration section in the Settings page.\n\n"
42+
"Note: The reference positions used by this widget are the initial positions "
43+
"of the selection when the widget instance is created or the initial positions "
44+
"whenever the selection is updated."
45+
)
46+
47+
_inputs: ClassVar = [
48+
{
49+
"attribute": "selection",
50+
"name": "Selection",
51+
"description": "MDAnalysis selection phrase",
52+
"type": "str",
53+
"validations": ["required"],
54+
},
55+
{
56+
"attribute": "center",
57+
"name": "Center",
58+
"description": "Subtract center of geometry before calculation",
59+
"type": "bool",
60+
},
61+
{
62+
"attribute": "superposition",
63+
"name": "Superposition",
64+
"description": (
65+
"Perform a rotational and translational superposition with the fast QCP algorithm"
66+
),
67+
"type": "bool",
68+
},
69+
{
70+
"attribute": "custom_title",
71+
"name": "Custom title",
72+
"description": "Custom title for the plot",
73+
"type": "str",
74+
},
75+
{
76+
"attribute": "maxlen",
77+
"name": "Max values",
78+
"description": "Max values to show in plot",
79+
"type": "int",
80+
},
81+
{
82+
"attribute": "x_type",
83+
"name": "X-axis",
84+
"type": "toggle",
85+
"options": [
86+
{"name": "Time", "value": "time"},
87+
{"name": "Step", "value": "step"},
88+
],
89+
},
90+
]
91+
92+
def __init__(self):
93+
super().__init__()
94+
self.selection = "protein"
95+
self.center = False
96+
self.superposition = False
97+
self.ag = None
98+
self.reference_positions = None
99+
self.title = "RMSD"
100+
self.custom_title = None
101+
self.default_maxlen = 100
102+
self.maxlen = self.default_maxlen
103+
self.x_type = "time"
104+
self.x_values = None
105+
self._setup_plot()
106+
self._reset_plot_values()
107+
108+
def _setup_plot(self):
109+
"""Setup matplotlib plot"""
110+
self.fig, self.ax = plt.subplots()
111+
(self.plot,) = self.ax.plot([], [])
112+
self.ax.set_ylabel("RMSD (Å)")
113+
self.ax.grid(True)
114+
self._set_title()
115+
116+
def _reset_plot_values(self):
117+
"""Reset plot values"""
118+
self.steps = deque(maxlen=self.maxlen)
119+
self.times = deque(maxlen=self.maxlen)
120+
self.y_values = deque(maxlen=self.maxlen)
121+
self._set_x_values()
122+
123+
def _set_title(self):
124+
"""Set plot title"""
125+
self.ax.set_title(self.custom_title if self.custom_title else self.title)
126+
127+
def _set_x_values(self):
128+
"""Set the values for the x-axis"""
129+
if self.x_type == "step":
130+
x_label = "Step"
131+
self.x_values = self.steps
132+
else:
133+
x_label = "Time (ps)"
134+
self.x_values = self.times
135+
self.ax.set_xlabel(x_label)
136+
137+
def _update_selection(self):
138+
"""Update atom groups when selection phrase changes"""
139+
self.ag = self.u.select_atoms(self.selection)
140+
self.reference_positions = self.ag.positions.copy()
141+
self.title = f"RMSD of '{self.selection}'"
142+
self._set_title()
143+
144+
def on_post_create(self):
145+
"""on_post_create handler"""
146+
self._set_title()
147+
self._reset_plot_values()
148+
149+
def on_post_connect(self):
150+
"""on_post_connect handler"""
151+
self._update_selection()
152+
153+
def on_input_change(self, attribute, _old_value, new_value):
154+
"""on_input_change handler"""
155+
reset_plot = False
156+
if attribute == "maxlen":
157+
if new_value < 0:
158+
self.maxlen = self.default_maxlen
159+
reset_plot = True
160+
elif attribute == "x_type":
161+
self._set_x_values()
162+
elif attribute == "custom_title":
163+
self._set_title()
164+
elif attribute in ("selection", "center", "superposition"):
165+
self._update_selection()
166+
reset_plot = True
167+
if reset_plot:
168+
self._reset_plot_values()
169+
170+
def run_every_frame(self):
171+
"""every-frame run handler"""
172+
rmsd_value = rms.rmsd(
173+
self.ag.positions,
174+
self.reference_positions,
175+
center=self.center,
176+
superposition=self.superposition,
177+
)
178+
self.y_values.append(rmsd_value)
179+
self.steps.append(self.u.trajectory.ts.data["step"])
180+
self.times.append(self.u.trajectory.ts.data["time"])
181+
# update plot
182+
self.plot.set_data(self.x_values, self.y_values)
183+
self.ax.relim()
184+
self.ax.autoscale_view()
185+
self.fig.canvas.draw()
186+
display(self.fig)

mdadash/backend/tests/test_server.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# pylint: disable=too-many-lines
12
import json
23
import sys
34
from unittest.mock import ANY, AsyncMock
@@ -984,3 +985,21 @@ async def test_utils_alert_pause(_client, imd_server):
984985
handler = sio.handlers["/"]["delete_all_alerts"]
985986
await run_task_until_done(handler("_sid"))
986987
await disconnect_from_simulation()
988+
989+
990+
async def test_widget_run_rmsd(_client, imd_server):
991+
uuid = await add_widget("RMSD")
992+
await connect_to_simulation(imd_server)
993+
inputs = [
994+
("selection", "protein"),
995+
("center", True),
996+
("superposition", True),
997+
("maxlen", -1),
998+
("x_type", "step"),
999+
("custom_title", "Title"),
1000+
]
1001+
await check_input_changes(uuid, inputs)
1002+
await resume_simulation(imd_server)
1003+
assert await sio_event_emitted(sio, "widgets:output", n=1)
1004+
await remove_widget(uuid)
1005+
await disconnect_from_simulation()

0 commit comments

Comments
 (0)