Skip to content

Commit 6ba8620

Browse files
Add widget inputs support (#11)
- Add support for handling widget inputs - auto-generated UI based on widget inputs - support for server side validation and error display - Add COM distance and Radii of Gyration analysis widgets - Add support for widget lifecycle methods - post_connect, post_disconnect, post_pause, pre_resume - on_input_change - Limit emits from server to client session only when applicable - Tests for all new backend and frontend functionality - Verify the built wheel by running `mdadash -h` in CI
1 parent c410ff4 commit 6ba8620

24 files changed

Lines changed: 1483 additions & 186 deletions

.github/workflows/deploy.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,13 @@ jobs:
4848
echo "twine check $WHL"
4949
twine check $WHL
5050
51+
- name: Install from wheel and verify
52+
run: |
53+
WHL=$(ls -t1 dist/mdadash-*.whl | head -n 1)
54+
python -m pip uninstall -y mdadash || true
55+
python -m pip install $WHL
56+
mdadash -h
57+
5158
- name: Upload artifacts
5259
uses: actions/upload-artifact@v4
5360
with:

.github/workflows/gh-ci.yaml

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -150,14 +150,25 @@ jobs:
150150
151151
- name: Build package
152152
run: |
153-
python -m pipx run build --sdist
153+
python -m pipx run build
154154
155155
- name: Check package build
156156
run: |
157-
DISTRIBUTION=$(ls -t1 dist/mdadash-*.tar.gz | head -n 1)
158-
test -n "${DISTRIBUTION}" || { echo "no distribution dist/mdadash-*.tar.gz found"; exit 1; }
159-
echo "twine check $DISTRIBUTION"
160-
twine check $DISTRIBUTION
157+
SDIST=$(ls -t1 dist/mdadash-*.tar.gz | head -n 1)
158+
test -n "${SDIST}" || { echo "no source distribution dist/mdadash-*.tar.gz found"; exit 1; }
159+
echo "twine check $SDIST"
160+
twine check $SDIST
161+
WHL=$(ls -t1 dist/mdadash-*.whl | head -n 1)
162+
test -n "${WHL}" || { echo "no whl dist/mdadash-*.whl found"; exit 1; }
163+
echo "twine check $WHL"
164+
twine check $WHL
165+
166+
- name: Install from wheel and verify
167+
run: |
168+
WHL=$(ls -t1 dist/mdadash-*.whl | head -n 1)
169+
python -m pip uninstall -y mdadash || true
170+
python -m pip install $WHL
171+
mdadash -h
161172
162173
backend_lint_check:
163174
name: Backend lint check

.pylintrc

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -446,7 +446,10 @@ disable=raw-checker-failed,
446446
missing-class-docstring,
447447
missing-function-docstring,
448448
fixme,
449-
too-few-public-methods
449+
too-few-public-methods,
450+
protected-access,
451+
too-many-instance-attributes,
452+
similarities,
450453

451454
# Enable the message, report, category or checker with the given id(s). You can
452455
# either give multiple identifier separated by comma (,) or put this option

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 basic widget execution framework and energy widgets (PR #8)
3333
- Reduce package size by moving away from mdi/font to mdi/js (PR #9)
3434
- Added support to display imdclient session info (PR #10)
35+
- Added widget inputs support (PR #11)
3536

3637
### Fixed
3738

DEVELOPMENT.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ rm -rf mdadash.egg-info dist && python -m build
176176
To verify the created wheel in an isolated environment:
177177

178178
```sh
179-
uv run --refresh --with path.to.whl mdadash <options>
179+
uv run --no-project --refresh --with path.to.whl mdadash <options>
180180
```
181181

182182
To check the created distribution:

mdadash/backend/analyses/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
Module that has all the analyses widgets
33
"""
44

5-
from . import energies
5+
from . import com_distance, energies, rog
66

77
__all__ = [
88
"energies",
9+
"com_distance",
10+
"rog",
911
]
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
"""
2+
Distance between two center-of-masses
3+
"""
4+
5+
from collections import deque
6+
7+
import matplotlib.pyplot as plt
8+
import numpy as np
9+
10+
from mdadash.backend.widgets.base import WidgetBase
11+
12+
13+
class COMDistance(WidgetBase):
14+
"""COM Distance
15+
16+
Distance between two center-of-masses (COMs)
17+
18+
"""
19+
20+
name = "COMDistance"
21+
description = "Distance between two COMs"
22+
23+
_inputs = [
24+
{
25+
"attribute": "selection1",
26+
"name": "Selection 1",
27+
"description": "First MDAnalysis selection phrase",
28+
"type": "str",
29+
"validations": ["required"],
30+
},
31+
{
32+
"attribute": "selection2",
33+
"name": "Selection 2",
34+
"description": "Second MDAnalysis selection phrase",
35+
"type": "str",
36+
"validations": ["required"],
37+
},
38+
{
39+
"attribute": "periodic",
40+
"name": "Periodic",
41+
"description": "Select with periodic boundary conditions",
42+
"type": "switch",
43+
},
44+
{
45+
"attribute": "updating",
46+
"name": "Updating",
47+
"description": "Update selection during each timestep",
48+
"type": "switch",
49+
},
50+
{
51+
"attribute": "custom_title",
52+
"name": "Custom title",
53+
"description": "Custom title for the plot",
54+
"type": "str",
55+
},
56+
{
57+
"attribute": "maxlen",
58+
"name": "Max values",
59+
"description": "Max values to show in plot",
60+
"type": "int",
61+
},
62+
{
63+
"attribute": "max_distance",
64+
"name": "Max distance",
65+
"description": "Max distance for alert check",
66+
"type": "int",
67+
},
68+
{
69+
"attribute": "max_distance_alert",
70+
"name": "Alert if distance > 'Max distance'",
71+
"type": "switch",
72+
},
73+
{
74+
"attribute": "x_type",
75+
"name": "X-axis",
76+
"type": "toggle",
77+
"options": [
78+
{"name": "Step", "value": "step"},
79+
{"name": "Time", "value": "time"},
80+
],
81+
},
82+
]
83+
84+
def __init__(self):
85+
super().__init__()
86+
self.maxlen = 100
87+
self.steps = deque(maxlen=self.maxlen)
88+
self.times = deque(maxlen=self.maxlen)
89+
self.y_values = deque(maxlen=self.maxlen)
90+
self.selection1 = "protein"
91+
self.selection2 = "resid 1"
92+
self.periodic = True
93+
self.updating = False
94+
self.ag1 = None
95+
self.ag2 = None
96+
self.title = "Distance between COMs"
97+
self.custom_title = None
98+
self.max_distance = 5
99+
self.max_distance_alert = False
100+
self.x_type = "step"
101+
self.x_values = self.steps
102+
self.x_label = "Step"
103+
104+
def _update_selections(self, s1=False, s2=False):
105+
"""Update atom groups when selection phrases change"""
106+
if s1:
107+
self.ag1 = self.u.select_atoms(
108+
self.selection1, periodic=self.periodic, updating=self.updating
109+
)
110+
if s2:
111+
self.ag2 = self.u.select_atoms(
112+
self.selection2, periodic=self.periodic, updating=self.updating
113+
)
114+
self.title = f"{self.selection1} <-> {self.selection2}"
115+
116+
def _set_x_values(self):
117+
"""Set the values for the x-axis"""
118+
if self.x_type == "step":
119+
self.x_label = "Step"
120+
self.x_values = self.steps
121+
else:
122+
self.x_label = "Time (ps)"
123+
self.x_values = self.times
124+
125+
def post_connect(self):
126+
"""post_connect handler"""
127+
self._update_selections(s1=True, s2=True)
128+
129+
def on_input_change(self, attribute, _old_value, new_value):
130+
"""on_input_change handler"""
131+
reset_plot = False
132+
if attribute == "maxlen":
133+
reset_plot = True
134+
elif attribute == "x_type":
135+
self._set_x_values()
136+
elif attribute == "selection1":
137+
self._update_selections(s1=True)
138+
reset_plot = True
139+
elif attribute == "selection2":
140+
self._update_selections(s2=True)
141+
reset_plot = True
142+
elif attribute in ("periodic", "updating"):
143+
self._update_selections(s1=True, s2=True)
144+
if reset_plot:
145+
self.steps = deque(maxlen=self.maxlen)
146+
self.times = deque(maxlen=self.maxlen)
147+
self.y_values = deque(maxlen=self.maxlen)
148+
self._set_x_values()
149+
150+
def run(self):
151+
"""run handler"""
152+
com1 = self.ag1.center_of_mass()
153+
com2 = self.ag2.center_of_mass()
154+
self.y_values.append(np.linalg.norm(com1 - com2))
155+
self.steps.append(self.u.trajectory.ts.data["step"])
156+
self.times.append(self.u.trajectory.ts.data["time"])
157+
plt.plot(self.x_values, self.y_values)
158+
plt.ylabel("Distance (Å)")
159+
plt.xlabel(self.x_label)
160+
plt.title(self.custom_title if self.custom_title else self.title)
161+
plt.grid(True)
162+
plt.show()

mdadash/backend/analyses/energies.py

Lines changed: 62 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
from collections import deque
66

77
import matplotlib.pyplot as plt
8-
import MDAnalysis as mda
98

109
from mdadash.backend.widgets.base import WidgetBase
1110

@@ -17,21 +16,73 @@ class EnergyWidgetBase:
1716
data_key = ""
1817
y_label = "Energy ( kJ / mol )"
1918

20-
def __init__(self):
21-
self._steps = deque(maxlen=100)
22-
self._values = deque(maxlen=100)
19+
_inputs = [
20+
{
21+
"attribute": "maxlen",
22+
"name": "Max values",
23+
"description": "Max values to show in plot",
24+
"type": "int",
25+
},
26+
{
27+
"attribute": "title",
28+
"name": "Title",
29+
"description": "Title for the plot",
30+
"type": "str",
31+
},
32+
{
33+
"attribute": "x_type",
34+
"name": "X-axis",
35+
"type": "toggle",
36+
"options": [
37+
{"name": "Step", "value": "step"},
38+
{"name": "Time", "value": "time"},
39+
],
40+
},
41+
]
2342

24-
def run(self, u: mda.Universe):
25-
ts = u.trajectory.ts
43+
def __init__(self):
44+
super().__init__()
45+
self.title = self.name
46+
self.maxlen = 100
47+
self.steps = deque(maxlen=self.maxlen)
48+
self.times = deque(maxlen=self.maxlen)
49+
self.y_values = deque(maxlen=self.maxlen)
50+
self.x_type = "step"
51+
self.x_values = self.steps
52+
self.x_label = "Step"
53+
54+
def _set_x_values(self):
55+
"""Set the values for the x-axis"""
56+
if self.x_type == "step":
57+
self.x_label = "Step"
58+
self.x_values = self.steps
59+
else:
60+
self.x_label = "Time (ps)"
61+
self.x_values = self.times
62+
63+
def on_input_change(self, attribute, _old_value, new_value):
64+
"""on_input_change handler"""
65+
if attribute == "maxlen":
66+
self.steps = deque(maxlen=new_value)
67+
self.times = deque(maxlen=new_value)
68+
self.y_values = deque(maxlen=new_value)
69+
self._set_x_values()
70+
elif attribute == "x_type":
71+
self._set_x_values()
72+
73+
def run(self):
74+
"""run handler"""
75+
ts = getattr(self, "u").trajectory.ts
2676
if self.data_key not in ts.data:
2777
return # pragma no cover
28-
self._values.append(ts.data[self.data_key])
29-
self._steps.append(ts.data["step"])
78+
self.steps.append(ts.data["step"])
79+
self.times.append(ts.data["time"])
80+
self.y_values.append(ts.data[self.data_key])
3081
# create plot
31-
plt.plot(self._steps, self._values)
82+
plt.plot(self.x_values, self.y_values)
3283
plt.ylabel(self.y_label)
33-
plt.xlabel("Step")
34-
plt.title(self.name, y=1.05)
84+
plt.xlabel(self.x_label)
85+
plt.title(self.title, y=1.05)
3586
plt.grid(True)
3687
plt.show()
3788

0 commit comments

Comments
 (0)