Skip to content

Commit 9588157

Browse files
committed
Build tabs and summary table
1 parent 29a216e commit 9588157

File tree

3 files changed

+311
-44
lines changed

3 files changed

+311
-44
lines changed

mlip_testing/app/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Dash application for benchmarks."""

mlip_testing/app/build_app.py

Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
"""Build main Dash application."""
2+
3+
from __future__ import annotations
4+
5+
from importlib import import_module
6+
from pathlib import Path
7+
8+
from dash import Dash, Input, Output, callback, ctx
9+
from dash.dash_table import DataTable
10+
from dash.dcc import Input as DCC_Input
11+
from dash.dcc import Slider, Store, Tab, Tabs
12+
from dash.html import H1, Button, Div, Label
13+
14+
from mlip_testing import app
15+
from mlip_testing.analysis.utils.utils import calc_ranks, calc_scores
16+
17+
18+
def get_tabs() -> tuple[dict[str, list[Div]], dict[str, DataTable]]:
19+
"""
20+
Get layout and register callbacks for all tab applications.
21+
22+
Returns
23+
-------
24+
tuple[dict[str, list[Div]], dict[str, DataTable]]
25+
Layouts and tables for all tabs.
26+
"""
27+
# Find Python files e.g. app_OC157.py in mlip_tesing.app module.
28+
tabs = Path(app.__file__).parent.glob("*/app*.py")
29+
layouts = {}
30+
tables = {}
31+
32+
# Build all layouts, and register all callbacks to main app.
33+
for tab in tabs:
34+
# Import tab application layout/callbacks
35+
tab_name = tab.parent.name
36+
tab_module = import_module(f"mlip_testing.app.{tab_name}.app_{tab_name}")
37+
tab_app = tab_module.get_app()
38+
39+
# Get layouts and tables for each tab
40+
layouts[tab_name] = tab_app.layout
41+
tables[tab_name] = tab_app.table
42+
43+
# Register tab callbacks
44+
tab_app.register_callbacks()
45+
46+
return layouts, tables
47+
48+
49+
def build_summary_table(tables: dict[str, DataTable]) -> DataTable:
50+
"""
51+
Build summary table.
52+
53+
Parameters
54+
----------
55+
tables
56+
Table from each tab.
57+
58+
Returns
59+
-------
60+
DataTable
61+
Summary table with score from each tab.
62+
"""
63+
summary_data = {}
64+
for tab_name, table in tables.items():
65+
summary_data = {row["MLIP"]: {} for row in table.data}
66+
for row in table.data:
67+
summary_data[row["MLIP"]][tab_name] = row["Score"]
68+
69+
data = []
70+
for mlip in summary_data:
71+
data.append({"MLIP": mlip} | summary_data[mlip])
72+
73+
data = calc_scores(data)
74+
data = calc_ranks(data)
75+
76+
columns_headers = ("MLIP",) + tuple(tables.keys()) + ("Score", "Rank")
77+
columns = [{"name": headers, "id": headers} for headers in columns_headers]
78+
79+
return DataTable(data=data, columns=columns)
80+
81+
82+
def build_slider(
83+
label: str, slider_id: str, input_id: str, default_value: float | None
84+
) -> Div:
85+
"""
86+
Build slider and input box.
87+
88+
Parameters
89+
----------
90+
label
91+
Slider label.
92+
slider_id
93+
ID for slider component.
94+
input_id
95+
ID for text box input component.
96+
default_value
97+
Default value for slider/text box input.
98+
99+
Returns
100+
-------
101+
Div
102+
Slider and input text box.
103+
"""
104+
return Div(
105+
[
106+
Label(label),
107+
Div(
108+
[
109+
Div(
110+
Slider(
111+
id=slider_id,
112+
min=0,
113+
max=5,
114+
step=0.1,
115+
value=default_value,
116+
tooltip={"always_visible": False},
117+
marks=None,
118+
),
119+
style={"flex": "1 1 80%"},
120+
),
121+
DCC_Input(
122+
id=input_id,
123+
type="number",
124+
value=default_value,
125+
step=0.1,
126+
style={"width": "80px"},
127+
),
128+
],
129+
style={"display": "flex", "gap": "10px", "alignItems": "center"},
130+
),
131+
]
132+
)
133+
134+
135+
def register_weight_callbacks(tab_name: str) -> None:
136+
"""
137+
Register all callbacks for weight inputs.
138+
139+
Parameters
140+
----------
141+
tab_name
142+
Name of tab.
143+
"""
144+
145+
# Callback to sync weights between slider, text, reset, and Store
146+
@callback(
147+
Output(f"{tab_name}-input", "value"),
148+
Output(f"{tab_name}-slider", "value"),
149+
Output(f"{tab_name}-weight-store", "data"),
150+
Input(f"{tab_name}-input", "value"),
151+
Input(f"{tab_name}-slider", "value"),
152+
Input(f"{tab_name}-weight-store", "data"),
153+
Input("reset-weights-button", "n_clicks"),
154+
prevent_initial_call=False,
155+
)
156+
def store_slider_value(
157+
input_value, slider_value, store, reset
158+
) -> tuple[float, float, float]:
159+
"""
160+
Store, reset, and sync weight values between slider and text input.
161+
162+
Parameters
163+
----------
164+
input_value
165+
Value from text box input.
166+
slider_value
167+
Value from slider.
168+
store
169+
Stored value.
170+
reset
171+
Number of clicks of reset button.
172+
173+
Returns
174+
-------
175+
tuple[float, float, float]
176+
Weights to set slider value, text input value and stored value.
177+
"""
178+
trigger_id = ctx.triggered_id
179+
180+
if trigger_id == f"{tab_name}-weight-store":
181+
weight = store
182+
elif trigger_id == f"{tab_name}-input":
183+
weight = input_value
184+
elif trigger_id == f"{tab_name}-slider":
185+
weight = slider_value
186+
elif trigger_id == "reset-weights-button":
187+
weight = 1
188+
else:
189+
raise ValueError("Invalid trigger. trigger_id: ", trigger_id)
190+
return weight, weight, weight
191+
192+
193+
def build_weight_components(tables: dict[str, DataTable]) -> Div:
194+
"""
195+
Build weight sliders, text boxes and reset button.
196+
197+
Parameters
198+
----------
199+
tables
200+
Table from each tab.
201+
202+
Returns
203+
-------
204+
Div
205+
Div containing weight sliders, text boxes and reset button.
206+
"""
207+
layout = [Div("Benchmark Weights")]
208+
209+
for tab_name in tables:
210+
layout.append(
211+
build_slider(
212+
label=tab_name,
213+
slider_id=f"{tab_name}-slider",
214+
input_id=f"{tab_name}-input",
215+
default_value=None, # Set by stored value/default
216+
)
217+
)
218+
219+
layout.append(
220+
Button(
221+
"Reset Weights",
222+
id="reset-weights-button",
223+
n_clicks=0,
224+
style={"marginTop": "20px"},
225+
),
226+
)
227+
228+
layout.append(
229+
Store(id=f"{tab_name}-weight-store", storage_type="session", data=1.0)
230+
)
231+
232+
for tab_name in tables:
233+
register_weight_callbacks(tab_name)
234+
235+
return Div(layout)
236+
237+
238+
def build_tabs(
239+
full_app: Dash,
240+
layouts: dict[str, list[Div]],
241+
summary_table: DataTable,
242+
weight_components: Div,
243+
) -> None:
244+
"""
245+
Build tab layouts and summary tab.
246+
247+
Parameters
248+
----------
249+
full_app
250+
Full application with all sub-apps.
251+
layouts
252+
Layouts for all tabs.
253+
summary_table
254+
Summary table with score from each tab.
255+
weight_components
256+
Weight sliders, text boxes and reset button.
257+
"""
258+
all_tabs = [Tab(label="Summary", value="summary-tab")] + [
259+
Tab(label=tab_name, value=tab_name) for tab_name in layouts
260+
]
261+
262+
tabs_layout = [
263+
H1("MLIP benchmarking"),
264+
Tabs(id="all-tabs", value="summary-tab", children=all_tabs),
265+
Div(id="tabs-content"),
266+
]
267+
268+
full_app.layout = Div(tabs_layout)
269+
270+
@callback(Output("tabs-content", "children"), Input("all-tabs", "value"))
271+
def select_tab(tab) -> Div:
272+
"""
273+
Select tab contents to be displayed.
274+
275+
Parameters
276+
----------
277+
tab
278+
Name of tab selected.
279+
280+
Returns
281+
-------
282+
Div
283+
Summary or tab contents to be displayed.
284+
"""
285+
if tab == "summary-tab":
286+
return Div(
287+
[
288+
H1("Benchmarks Summary"),
289+
summary_table,
290+
weight_components,
291+
]
292+
)
293+
return Div([layouts[tab]])
294+
295+
296+
def build_full_app(full_app: Dash) -> None:
297+
"""
298+
Build full app layout and register callbacks.
299+
300+
Parameters
301+
----------
302+
full_app
303+
Full application with all sub-apps.
304+
"""
305+
layouts, tables = get_tabs()
306+
summary_table = build_summary_table(tables)
307+
weight_components = build_weight_components(tables)
308+
build_tabs(full_app, layouts, summary_table, weight_components)

mlip_testing/app/run_app.py

Lines changed: 2 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2,63 +2,21 @@
22

33
from __future__ import annotations
44

5-
from importlib import import_module
65
import os
76
from pathlib import Path
87

98
from dash import Dash
10-
from dash.html import Div
119

12-
from mlip_testing import app
10+
from mlip_testing.app.build_app import build_full_app
1311

1412
DATA_PATH = Path(__file__).parent / "data"
1513

1614

17-
def build_tabs(full_app: Dash) -> list[Div]:
18-
"""
19-
Get layout and register callbacks for all tab applications.
20-
21-
Parameters
22-
----------
23-
full_app
24-
Full application with all sub-apps.
25-
26-
Returns
27-
-------
28-
list[Div]
29-
Layouts for all tabs.
30-
"""
31-
# Find Python files e.g. app_OC157.py in mlip_tesing.app module.
32-
tabs = Path(app.__file__).parent.glob("*/app*.py")
33-
layout = []
34-
35-
# Build all layouts, and register all callbacks to main app.
36-
for tab in tabs:
37-
tab_name = (tab.parent).name
38-
tab_app = import_module(f"mlip_testing.app.{tab_name}.app_{tab_name}")
39-
layout.append(tab_app.build_layout())
40-
tab_app.register_callbacks(full_app)
41-
42-
return layout
43-
44-
45-
def build_full_app(full_app: Dash):
46-
"""
47-
Build full app layout and register callbacks.
48-
49-
Parameters
50-
----------
51-
full_app
52-
Full application with all sub-apps.
53-
"""
54-
full_app.layout = build_tabs(full_app)
55-
56-
5715
if __name__ == "__main__":
5816
port = int(os.environ.get("PORT", 8050))
5917

6018
full_app = Dash(__name__, assets_folder=DATA_PATH)
6119
build_full_app(full_app)
6220

6321
print(f"Starting Dash app on port {port}...")
64-
full_app.run(host="0.0.0.0", port=port)
22+
full_app.run(host="0.0.0.0", port=port, debug=True)

0 commit comments

Comments
 (0)