Skip to content

Commit 5ffa496

Browse files
Add custom widgets documentation
1 parent b885886 commit 5ffa496

20 files changed

Lines changed: 427 additions & 113 deletions

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ The rules for this file:
3131

3232
- Added built-in widgets documentation (PR #63)
3333
- Added batching and parallel support for com distance widget (PR #64)
34+
- Minor widget enhancements (PR #65)
35+
- Added custom widgets documentation (PR #66)
3436

3537
### Fixed
3638

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
Adding Custom Widgets
2+
=====================
3+
4+
Custom Widgets share the same underlying framework used by :doc:`built_in_widgets`.
5+
6+
A Custom Widget has to derive from the :class:`~mdadash.backend.widgets.base.WidgetBase`
7+
base class and implement certain handlers as described here.
8+
9+
Widget Registration
10+
-------------------
11+
12+
A Widget class must have a unique ``name`` class attibute to be registered.
13+
14+
.. code-block:: python
15+
16+
class CustomWidget(WidgetBase)
17+
name = "Custom Widget"
18+
19+
20+
An error is raised when the ``name`` class attribute is missing or if it exists but
21+
is the same as an already registered widget. The uniqueness of the ``name`` exists to
22+
prevent accidental overwrite of existing Widget classes. During testing or for use in
23+
Notebooks, an option is provided to force re-registraion of a Widget class if a
24+
``_override_name`` class attribute set to ``True`` exists in the class defintion.
25+
26+
In the example below, the ``CustomWidget`` class overrides the built-in
27+
:class:`~mdadash.backend.analyses.energies.AbsoluteTemperature` widget because it uses
28+
the same name "Absolute Temperature".
29+
30+
.. code-block:: python
31+
32+
class CustomWidget(WidgetBase)
33+
name = "Absolute Temperature"
34+
_override_name = True
35+
36+
This also enables customization of :doc:`built_in_widgets` by cloning them in Notebooks
37+
and modifying them as needed.
38+
39+
An optional ``description`` class attribute can be used to specify more details about the
40+
Widget and this gets displayed along with the name in the list of available Widgets in
41+
the dashboard UI.
42+
43+
Run frequency and Run mode
44+
--------------------------
45+
46+
The :class:`~mdadash.backend.widgets.base.WidgetBase` base class specifies two attributes
47+
for all Widgets (defaults shown below):
48+
49+
.. code-block:: python
50+
51+
_run_frequency = "every-frame"
52+
_run_mode = "serial"
53+
54+
``_run_frequency`` specifies how often the widget is run. It takes one of two values:
55+
``every-frame`` or ``batch``.
56+
57+
``_run_mode`` specifies how the widget code is run. It takes one of two values:
58+
``serial`` or ``parallel``.
59+
60+
By default, all Widgets run every frame serially (due to defaults above) unless the above
61+
attributes are customized.
62+
63+
Both these attributes can be configured independent of each other. Which method(s) in
64+
the Widget class gets invoked depend on both these attributes as described below.
65+
66+
.. note::
67+
68+
A Widget can make these attributes dynamically changeable at runtime as well by making
69+
them as `Inputs`_, which then show corresponding options in the UI.
70+
71+
_run_frequency
72+
~~~~~~~~~~~~~~
73+
74+
This attribute specifies how often the widget is run.
75+
76+
When ``_run_frequency`` is ``every-frame``, a method is invoked for every frame of the
77+
trajectory iteration.
78+
79+
When ``_run_frequency`` is ``batch``, a method is invoked when a new batch of timesteps
80+
is full. A global "Buffer / batch size" under "Settings > Universe Configuration" in the
81+
dashboard controls the size of this timesteps buffer.
82+
83+
The method that is invoked depends on the ``_run_mode``.
84+
85+
If the ``_run_mode`` is ``parallel``, see the next section to see what gets invoked.
86+
87+
If the ``_run_mode`` is ``serial``:
88+
89+
* When ``_run_frequency`` is ``every-frame``,
90+
:meth:`~mdadash.backend.widgets.base.WidgetBase.run_every_frame` method is invoked.
91+
92+
* When ``_run_frequency`` is ``batch``,
93+
:meth:`~mdadash.backend.widgets.base.WidgetBase.run_batch` method is invoked.
94+
95+
_run_mode
96+
~~~~~~~~~
97+
98+
This attribute specifies how the widget analysis code is run.
99+
100+
If the ``_run_mode`` is ``parallel`` for a given widget instance, a
101+
:meth:`~mdadash.backend.widgets.base.WidgetBase.get_parallel_job` method is invoked to
102+
retrieve the parallel job (a ``joblib.delayed`` tuple). A global "Parallel Jobs" under
103+
"Settings > Dashboard Configuration" in the dasboard controls the total number of jobs
104+
run in parallel during each iteration (``n_jobs`` param for ``joblib.Parallel`` call).
105+
106+
If a widget has ``_run_mode`` as ``parallel``, after the parallel job is completed, a
107+
:meth:`~mdadash.backend.widgets.base.WidgetBase.apply_parallel_results` method is invoked
108+
where the results from the parallel job are passed back to the instance. The instance can
109+
apply the results back to its data structures (like updating it's values ``deque`` etc).
110+
111+
If a widget has ``_run_mode`` as ``serial``, one of the methods described in the previous
112+
section are invoked.
113+
114+
Lifecycle methods
115+
-----------------
116+
117+
There are several lifecycle methods that Widgets can implement (handlers) and these get
118+
invoked by the dashboard framework at those stages.
119+
120+
* :meth:`~mdadash.backend.widgets.base.WidgetBase.on_post_create`
121+
* :meth:`~mdadash.backend.widgets.base.WidgetBase.on_post_connect`
122+
* :meth:`~mdadash.backend.widgets.base.WidgetBase.on_post_disconnect`
123+
* :meth:`~mdadash.backend.widgets.base.WidgetBase.on_post_pause`
124+
* :meth:`~mdadash.backend.widgets.base.WidgetBase.on_pre_resume`
125+
* :meth:`~mdadash.backend.widgets.base.WidgetBase.on_input_change`
126+
127+
Inputs
128+
------
129+
130+
Widgets can specify certain instance variables as inputs. These inputs show up in the
131+
dashboard UI allowing users to configure and modify them at runtime.
132+
133+
An array of inputs is specified using the ``_inputs`` class attribute. Each item of this
134+
array is a dict that has at minimum the following keys:
135+
136+
* ``attribute``
137+
138+
* The attribute that will be get / set
139+
140+
* ``name``
141+
142+
* The name to display in the UI for this input
143+
144+
* ``description``
145+
146+
* An optional description to display as hint for the input in the UI
147+
148+
* ``type``
149+
150+
* The type of the input. The following types are supported:
151+
152+
* ``str`` - A text input
153+
* ``int`` - An integer number input
154+
* ``float`` - A decimal number input
155+
* ``bool`` - A switch input
156+
* ``select`` - A select dropdown with options
157+
* ``toggle`` - A binary toggle between two options
158+
* ``cell`` - A Notebook cell
159+
160+
Here is an example that creates a string input for the ``selection`` attribute:
161+
162+
.. code-block:: python
163+
164+
{
165+
"attribute": "selection",
166+
"name": "Selection",
167+
"description": "MDAnalysis selection phrase",
168+
"type": "str",
169+
},
170+
171+
Some of the input types take additonal keys as shown in the examples below:
172+
173+
A select dropdown with options:
174+
175+
.. code-block:: python
176+
177+
{
178+
"attribute": "physical_property",
179+
"name": "Physical property",
180+
"description": "Physical property to analyze",
181+
"type": "select",
182+
"items": [
183+
"velocity",
184+
"position",
185+
"force",
186+
],
187+
},
188+
189+
A toggle option:
190+
191+
.. code-block:: python
192+
193+
{
194+
"attribute": "x_type",
195+
"name": "X-axis",
196+
"type": "toggle",
197+
"options": [
198+
{"name": "Time", "value": "time"},
199+
{"name": "Step", "value": "step"},
200+
],
201+
},
202+
203+
The :mod:`~mdadash.backend.analyses.custom_code` Widget uses the ``cell`` input type as
204+
shown below:
205+
206+
.. code-block:: python
207+
208+
{
209+
"attribute": "setup_code",
210+
"name": "Setup code",
211+
"description": "This code will run once during widget creation",
212+
"type": "cell",
213+
},
214+
215+
The :meth:`~mdadash.backend.widgets.base.WidgetBase.on_input_change` handler gets invoked
216+
for any input change made from the dasboard UI. Any validation errors raised by the
217+
handler will show up as errors in the UI as well.
218+
219+
.. caution::
220+
221+
Widgets will not be run as long as there are input errors as shown in the dasboard UI.
222+
Users will need to fix the inputs after which they will automatically run as configured.
223+
224+
Utils
225+
-----
226+
227+
The following utils are available for Widgets to create alerts and pause the simulation
228+
if required when any custom conditions are met in their code.
229+
230+
* :meth:`~mdadash.backend.widgets.base.WidgetBase.alert`
231+
* :meth:`~mdadash.backend.widgets.base.WidgetBase.pause_simulation`
232+
233+
234+
Automatic refresh
235+
-----------------
236+
237+
All existing instances of a given Widget are automatically refreshed (re-created) when that
238+
Widget class gets updated (typically through a Notebook cell execution in the dashboard).
239+
All existing inputs are retained as is. This allows updates to the Widget class code reflect
240+
immediately in existing Widget outputs.
241+
242+
243+
----
244+
245+
.. tip::
246+
247+
:mod:`Custom Code <mdadash.backend.analyses.custom_code>` built-in Widget provides a
248+
quick way to run simpler custom code.
249+
250+
:doc:`built_in_widgets` can also be cloned into new Notebooks in the dasboard UI and
251+
customized as described in this document.
252+
253+
254+
If you are adding a custom Widget that could be useful for others in the community, you can
255+
create a `pull request <https://github.com/MDAnalysis/mdadash/pulls>`_ to make it part of the
256+
:doc:`built_in_widgets`.

docs/source/built_in_widgets.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ and can run in parallel:
2626
- ✅
2727
* - :mod:`~mdadash.backend.analyses.custom_code`
2828
- Custom user-defined code
29-
-
29+
-
3030
- —
3131
* - :mod:`~mdadash.backend.analyses.dssp`
3232
- DSSP Analysis

docs/source/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Source code and contributing instructions for this project can be found in the `
1414

1515
getting_started
1616
built_in_widgets
17+
adding_custom_widgets
1718
api
1819

1920

mdadash/backend/analyses/acf.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -265,16 +265,16 @@ def _create_acf(self):
265265
self._set_y_label()
266266

267267
def on_post_create(self):
268-
"""on_post_create handler"""
268+
""":meth:`~mdadash.backend.widgets.base.WidgetBase.on_post_create` handler"""
269269
self._set_title()
270270
self._set_y_label()
271271

272272
def on_post_connect(self):
273-
"""on_post_connect handler"""
273+
""":meth:`~mdadash.backend.widgets.base.WidgetBase.on_post_connect` handler"""
274274
self._create_acf()
275275

276276
def on_input_change(self, attribute, _old_value, new_value):
277-
"""on_input_change handler"""
277+
""":meth:`~mdadash.backend.widgets.base.WidgetBase.on_input_change` handler"""
278278
if attribute == "custom_title":
279279
self._set_title()
280280
elif attribute in ("normalized", "_run_mode"):
@@ -296,16 +296,16 @@ def _update_plot(self, x, y1, y2):
296296
display(self.fig)
297297

298298
def run_every_frame(self):
299-
"""every-frame run handler"""
299+
""":meth:`~mdadash.backend.widgets.base.WidgetBase.run_every_frame` handler"""
300300
x, y1, y2, _ = self._compute(normalized=self.normalized)
301301
self._update_plot(x, y1, y2)
302302

303303
def get_parallel_job(self):
304-
"""get parallel job handler"""
304+
""":meth:`~mdadash.backend.widgets.base.WidgetBase.get_parallel_job` handler"""
305305
return delayed(self._compute)(normalized=self.normalized, parallel=True)
306306

307307
def apply_parallel_results(self, values):
308-
"""apply parallel results handler"""
308+
""":meth:`~mdadash.backend.widgets.base.WidgetBase.apply_parallel_results` handler"""
309309
x, y1, y2, (v1, v2, v3, v4, v5, v6) = values
310310
self._update_plot(x, y1, y2)
311311
# update acf state

mdadash/backend/analyses/com_distance.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -244,16 +244,16 @@ def _update_selections(self):
244244
self._set_title()
245245

246246
def on_post_create(self):
247-
"""on_post_create handler"""
247+
""":meth:`~mdadash.backend.widgets.base.WidgetBase.on_post_create` handler"""
248248
self._set_title()
249249
self._reset_plot_values()
250250

251251
def on_post_connect(self):
252-
"""on_post_connect handler"""
252+
""":meth:`~mdadash.backend.widgets.base.WidgetBase.on_post_connect` handler"""
253253
self._update_selections()
254254

255255
def on_input_change(self, attribute, _old_value, new_value):
256-
"""on_input_change handler"""
256+
""":meth:`~mdadash.backend.widgets.base.WidgetBase.on_input_change` handler"""
257257
reset_plot = False
258258
if attribute == "maxlen":
259259
if new_value < 0:
@@ -320,19 +320,19 @@ def _update_plot(self, values):
320320
display(self.fig)
321321

322322
def run_every_frame(self):
323-
"""every-frame run handler"""
323+
""":meth:`~mdadash.backend.widgets.base.WidgetBase.run_every_frame` handler"""
324324
self._update_plot(self._compute_current_frame())
325325

326326
def run_batch(self):
327-
"""batch run handler"""
327+
""":meth:`~mdadash.backend.widgets.base.WidgetBase.run_batch` handler"""
328328
self._update_plot(self._compute_batch())
329329

330330
def get_parallel_job(self):
331-
"""get parallel job handler"""
331+
""":meth:`~mdadash.backend.widgets.base.WidgetBase.get_parallel_job` handler"""
332332
if self._run_frequency == "batch":
333333
return delayed(self._compute_batch)()
334334
return delayed(self._compute_current_frame)()
335335

336336
def apply_parallel_results(self, values):
337-
"""apply parallel results handler"""
337+
""":meth:`~mdadash.backend.widgets.base.WidgetBase.apply_parallel_results` handler"""
338338
self._update_plot(values)

0 commit comments

Comments
 (0)