deepboard 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- deepboard/__init__.py +1 -0
- deepboard/__version__.py +4 -0
- deepboard/gui/THEME.yml +28 -0
- deepboard/gui/__init__.py +0 -0
- deepboard/gui/assets/artefacts.css +108 -0
- deepboard/gui/assets/base.css +208 -0
- deepboard/gui/assets/base.js +77 -0
- deepboard/gui/assets/charts.css +188 -0
- deepboard/gui/assets/compare.css +90 -0
- deepboard/gui/assets/datagrid.css +120 -0
- deepboard/gui/assets/fileview.css +13 -0
- deepboard/gui/assets/right_panel.css +227 -0
- deepboard/gui/assets/theme.css +85 -0
- deepboard/gui/components/__init__.py +8 -0
- deepboard/gui/components/artefact_group.py +12 -0
- deepboard/gui/components/chart_type.py +22 -0
- deepboard/gui/components/legend.py +34 -0
- deepboard/gui/components/log_selector.py +22 -0
- deepboard/gui/components/modal.py +20 -0
- deepboard/gui/components/smoother.py +21 -0
- deepboard/gui/components/split_selector.py +21 -0
- deepboard/gui/components/stat_line.py +8 -0
- deepboard/gui/entry.py +21 -0
- deepboard/gui/main.py +93 -0
- deepboard/gui/pages/__init__.py +1 -0
- deepboard/gui/pages/compare_page/__init__.py +6 -0
- deepboard/gui/pages/compare_page/compare_page.py +22 -0
- deepboard/gui/pages/compare_page/components/__init__.py +4 -0
- deepboard/gui/pages/compare_page/components/card_list.py +19 -0
- deepboard/gui/pages/compare_page/components/chart.py +54 -0
- deepboard/gui/pages/compare_page/components/compare_setup.py +30 -0
- deepboard/gui/pages/compare_page/components/split_card.py +51 -0
- deepboard/gui/pages/compare_page/components/utils.py +20 -0
- deepboard/gui/pages/compare_page/routes.py +58 -0
- deepboard/gui/pages/main_page/__init__.py +4 -0
- deepboard/gui/pages/main_page/datagrid/__init__.py +5 -0
- deepboard/gui/pages/main_page/datagrid/compare_button.py +21 -0
- deepboard/gui/pages/main_page/datagrid/datagrid.py +67 -0
- deepboard/gui/pages/main_page/datagrid/handlers.py +54 -0
- deepboard/gui/pages/main_page/datagrid/header.py +43 -0
- deepboard/gui/pages/main_page/datagrid/routes.py +112 -0
- deepboard/gui/pages/main_page/datagrid/row.py +20 -0
- deepboard/gui/pages/main_page/datagrid/sortable_column_js.py +45 -0
- deepboard/gui/pages/main_page/datagrid/utils.py +9 -0
- deepboard/gui/pages/main_page/handlers.py +16 -0
- deepboard/gui/pages/main_page/main_page.py +21 -0
- deepboard/gui/pages/main_page/right_panel/__init__.py +12 -0
- deepboard/gui/pages/main_page/right_panel/config.py +57 -0
- deepboard/gui/pages/main_page/right_panel/fragments.py +133 -0
- deepboard/gui/pages/main_page/right_panel/hparams.py +25 -0
- deepboard/gui/pages/main_page/right_panel/images.py +358 -0
- deepboard/gui/pages/main_page/right_panel/run_info.py +86 -0
- deepboard/gui/pages/main_page/right_panel/scalars.py +251 -0
- deepboard/gui/pages/main_page/right_panel/template.py +151 -0
- deepboard/gui/pages/main_page/routes.py +25 -0
- deepboard/gui/pages/not_found.py +3 -0
- deepboard/gui/requirements.txt +5 -0
- deepboard/gui/utils.py +267 -0
- deepboard/resultTable/__init__.py +2 -0
- deepboard/resultTable/cursor.py +20 -0
- deepboard/resultTable/logwritter.py +667 -0
- deepboard/resultTable/resultTable.py +529 -0
- deepboard/resultTable/scalar.py +29 -0
- deepboard/resultTable/table_schema.py +135 -0
- deepboard/resultTable/utils.py +50 -0
- deepboard-0.2.0.dist-info/METADATA +164 -0
- deepboard-0.2.0.dist-info/RECORD +69 -0
- deepboard-0.2.0.dist-info/WHEEL +4 -0
- deepboard-0.2.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,358 @@
|
|
1
|
+
from fasthtml.common import *
|
2
|
+
from starlette.responses import Response
|
3
|
+
from typing import *
|
4
|
+
from deepboard.gui.components import Modal, SplitSelector, StatLine, ArtefactGroup
|
5
|
+
|
6
|
+
def _get_images_groups(socket, type: Literal["IMAGE", "PLOT"]):
|
7
|
+
if type == "IMAGE":
|
8
|
+
images = socket.read_images()
|
9
|
+
else:
|
10
|
+
images = socket.read_figures()
|
11
|
+
|
12
|
+
index = list({(img["step"], img["epoch"], img["run_rep"], img["split"]) for img in images})
|
13
|
+
|
14
|
+
splits = list({elem[3] for elem in index})
|
15
|
+
|
16
|
+
# Package images
|
17
|
+
images_groups = {}
|
18
|
+
for key in index:
|
19
|
+
cropped_key = key[:-1] # Remove split
|
20
|
+
if cropped_key not in images_groups:
|
21
|
+
images_groups[cropped_key] = {split: [] for split in splits}
|
22
|
+
|
23
|
+
for img in images:
|
24
|
+
key = img["step"], img["epoch"], img["run_rep"]
|
25
|
+
split = img["split"]
|
26
|
+
images_groups[key][split].append(img["id"])
|
27
|
+
|
28
|
+
# Sort image groups by step, and run_rep
|
29
|
+
return dict(sorted(images_groups.items(), key=lambda x: (x[0][0], x[0][2])))
|
30
|
+
|
31
|
+
|
32
|
+
def ImageComponent(image_id: int):
|
33
|
+
"""
|
34
|
+
Create a single image component with a specific style.
|
35
|
+
:param image: PIL Image object.
|
36
|
+
:return: Div containing the image.
|
37
|
+
"""
|
38
|
+
return Div(
|
39
|
+
A(
|
40
|
+
Img(src=f"/images/id={image_id}", alt="Image"),
|
41
|
+
hx_get=f"/images/open_modal?id={image_id}",
|
42
|
+
hx_target="#modal",
|
43
|
+
hx_swap="outerHTML",
|
44
|
+
style='cursor: pointer;',
|
45
|
+
),
|
46
|
+
cls="image",
|
47
|
+
)
|
48
|
+
|
49
|
+
def InteractiveImage(image_id: int):
|
50
|
+
return Div(
|
51
|
+
Script(
|
52
|
+
"""
|
53
|
+
// For image zoom and pan in modal
|
54
|
+
var scale = 1;
|
55
|
+
var translateX = 0;
|
56
|
+
var translateY = 0;
|
57
|
+
var zoomableDiv = document.getElementById('zoomableDiv');
|
58
|
+
var container = document.querySelector('.interactive-image-container');
|
59
|
+
|
60
|
+
// Mouse/touch state
|
61
|
+
var isDragging = false;
|
62
|
+
var lastX = 0;
|
63
|
+
var lastY = 0;
|
64
|
+
|
65
|
+
// Apply transform with both scale and translate
|
66
|
+
function applyTransform() {
|
67
|
+
zoomableDiv.style.transform = `scale(${scale}) translate(${translateX}px, ${translateY}px)`;
|
68
|
+
}
|
69
|
+
|
70
|
+
// Mouse wheel and trackpad zoom
|
71
|
+
var lastWheelTime = 0;
|
72
|
+
var wheelAccumulator = 0;
|
73
|
+
|
74
|
+
container.addEventListener('wheel', (e) => {
|
75
|
+
e.preventDefault();
|
76
|
+
|
77
|
+
const currentTime = Date.now();
|
78
|
+
const timeDelta = currentTime - lastWheelTime;
|
79
|
+
|
80
|
+
// Detect if this is likely a Mac trackpad (rapid events with ctrlKey)
|
81
|
+
const isMacTrackpad = e.ctrlKey || (timeDelta < 50 && Math.abs(e.deltaY) > 10);
|
82
|
+
|
83
|
+
let zoomFactor;
|
84
|
+
if (isMacTrackpad) {
|
85
|
+
// For Mac trackpad: use smaller, more gradual changes
|
86
|
+
// Accumulate small changes for smoother zooming
|
87
|
+
wheelAccumulator += e.deltaY;
|
88
|
+
|
89
|
+
// Only apply zoom when accumulator reaches threshold
|
90
|
+
if (Math.abs(wheelAccumulator) > 20) {
|
91
|
+
zoomFactor = wheelAccumulator > 0 ? 0.95 : 1.05;
|
92
|
+
wheelAccumulator *= 0.7; // Reduce accumulator but don't reset completely
|
93
|
+
} else {
|
94
|
+
lastWheelTime = currentTime;
|
95
|
+
return; // Skip this event
|
96
|
+
}
|
97
|
+
} else {
|
98
|
+
// For regular mouse wheel: use normal zoom steps
|
99
|
+
zoomFactor = e.deltaY > 0 ? 0.9 : 1.1;
|
100
|
+
wheelAccumulator = 0; // Reset accumulator for mouse wheel
|
101
|
+
}
|
102
|
+
|
103
|
+
const newScale = Math.max(0.5, Math.min(3, scale * zoomFactor));
|
104
|
+
|
105
|
+
// Get cursor position relative to container
|
106
|
+
const rect = container.getBoundingClientRect();
|
107
|
+
const mouseX = e.clientX - rect.left - rect.width / 2;
|
108
|
+
const mouseY = e.clientY - rect.top - rect.height / 2;
|
109
|
+
|
110
|
+
// Adjust translation to zoom towards cursor position
|
111
|
+
const scaleRatio = newScale / scale;
|
112
|
+
translateX = mouseX * (1 - scaleRatio) + translateX * scaleRatio;
|
113
|
+
translateY = mouseY * (1 - scaleRatio) + translateY * scaleRatio;
|
114
|
+
|
115
|
+
scale = newScale;
|
116
|
+
lastWheelTime = currentTime;
|
117
|
+
applyTransform();
|
118
|
+
});
|
119
|
+
|
120
|
+
// Mouse drag for panning
|
121
|
+
container.addEventListener('mousedown', (e) => {
|
122
|
+
if (scale > 1) { // Only allow panning when zoomed
|
123
|
+
isDragging = true;
|
124
|
+
lastX = e.clientX;
|
125
|
+
lastY = e.clientY;
|
126
|
+
container.style.cursor = 'grabbing';
|
127
|
+
e.preventDefault();
|
128
|
+
}
|
129
|
+
});
|
130
|
+
|
131
|
+
document.addEventListener('mousemove', (e) => {
|
132
|
+
if (isDragging) {
|
133
|
+
const deltaX = e.clientX - lastX;
|
134
|
+
const deltaY = e.clientY - lastY;
|
135
|
+
|
136
|
+
translateX += deltaX / scale; // Adjust for current scale
|
137
|
+
translateY += deltaY / scale;
|
138
|
+
|
139
|
+
lastX = e.clientX;
|
140
|
+
lastY = e.clientY;
|
141
|
+
|
142
|
+
applyTransform();
|
143
|
+
e.preventDefault();
|
144
|
+
}
|
145
|
+
});
|
146
|
+
|
147
|
+
document.addEventListener('mouseup', () => {
|
148
|
+
if (isDragging) {
|
149
|
+
isDragging = false;
|
150
|
+
container.style.cursor = scale > 1 ? 'grab' : 'default';
|
151
|
+
}
|
152
|
+
});
|
153
|
+
|
154
|
+
// Touch zoom (pinch)
|
155
|
+
var initialDistance = 0;
|
156
|
+
var initialScale = 1;
|
157
|
+
var touchStartX = 0;
|
158
|
+
var touchStartY = 0;
|
159
|
+
var initialTranslateX = 0;
|
160
|
+
var initialTranslateY = 0;
|
161
|
+
|
162
|
+
container.addEventListener('touchstart', (e) => {
|
163
|
+
if (e.touches.length === 2) {
|
164
|
+
// Two finger pinch zoom
|
165
|
+
initialDistance = getDistance(e.touches[0], e.touches[1]);
|
166
|
+
initialScale = scale;
|
167
|
+
e.preventDefault();
|
168
|
+
} else if (e.touches.length === 1 && scale > 1) {
|
169
|
+
// Single finger pan when zoomed
|
170
|
+
isDragging = true;
|
171
|
+
touchStartX = e.touches[0].clientX;
|
172
|
+
touchStartY = e.touches[0].clientY;
|
173
|
+
initialTranslateX = translateX;
|
174
|
+
initialTranslateY = translateY;
|
175
|
+
e.preventDefault();
|
176
|
+
}
|
177
|
+
});
|
178
|
+
|
179
|
+
container.addEventListener('touchmove', (e) => {
|
180
|
+
if (e.touches.length === 2) {
|
181
|
+
// Handle pinch zoom
|
182
|
+
const currentDistance = getDistance(e.touches[0], e.touches[1]);
|
183
|
+
const ratio = currentDistance / initialDistance;
|
184
|
+
const dampedRatio = 1 + (ratio - 1) * 0.5; // 50% sensitivity
|
185
|
+
scale = Math.max(0.5, Math.min(3, initialScale * dampedRatio));
|
186
|
+
applyTransform();
|
187
|
+
e.preventDefault();
|
188
|
+
} else if (e.touches.length === 1 && isDragging) {
|
189
|
+
// Handle single finger pan
|
190
|
+
const deltaX = e.touches[0].clientX - touchStartX;
|
191
|
+
const deltaY = e.touches[0].clientY - touchStartY;
|
192
|
+
|
193
|
+
translateX = initialTranslateX + deltaX / scale;
|
194
|
+
translateY = initialTranslateY + deltaY / scale;
|
195
|
+
|
196
|
+
applyTransform();
|
197
|
+
e.preventDefault();
|
198
|
+
}
|
199
|
+
});
|
200
|
+
|
201
|
+
container.addEventListener('touchend', (e) => {
|
202
|
+
if (e.touches.length === 0) {
|
203
|
+
isDragging = false;
|
204
|
+
}
|
205
|
+
});
|
206
|
+
|
207
|
+
// Reset on double click/tap
|
208
|
+
container.addEventListener('dblclick', () => {
|
209
|
+
scale = 1;
|
210
|
+
translateX = 0;
|
211
|
+
translateY = 0;
|
212
|
+
applyTransform();
|
213
|
+
container.style.cursor = 'default';
|
214
|
+
});
|
215
|
+
|
216
|
+
// Update cursor based on zoom level
|
217
|
+
container.addEventListener('mouseenter', () => {
|
218
|
+
container.style.cursor = scale > 1 ? 'grab' : 'default';
|
219
|
+
});
|
220
|
+
|
221
|
+
function getDistance(touch1, touch2) {
|
222
|
+
const dx = touch1.clientX - touch2.clientX;
|
223
|
+
const dy = touch1.clientY - touch2.clientY;
|
224
|
+
return Math.sqrt(dx * dx + dy * dy);
|
225
|
+
}
|
226
|
+
|
227
|
+
// Prevent context menu on long press for mobile
|
228
|
+
container.addEventListener('contextmenu', (e) => {
|
229
|
+
e.preventDefault();
|
230
|
+
});
|
231
|
+
"""
|
232
|
+
),
|
233
|
+
Div(
|
234
|
+
Div(
|
235
|
+
Img(src=f"/images/id={image_id}", alt="Image"),
|
236
|
+
cls="interactive-image",
|
237
|
+
id="zoomableDiv",
|
238
|
+
style="transition: transform 0.1s ease-out;"
|
239
|
+
),
|
240
|
+
cls="interactive-image-container",
|
241
|
+
style="overflow: hidden; position: relative; user-select: none; touch-action: none;"
|
242
|
+
)
|
243
|
+
)
|
244
|
+
|
245
|
+
|
246
|
+
|
247
|
+
def ImageCard(runID: int, step: int, epoch: Optional[int], run_rep: int, img_type: Literal["IMAGE", "PLOT"],
|
248
|
+
selected: Optional[str] = None):
|
249
|
+
from __main__ import rTable
|
250
|
+
|
251
|
+
socket = rTable.load_run(runID)
|
252
|
+
data = _get_images_groups(socket, type=img_type)
|
253
|
+
|
254
|
+
if (step, epoch, run_rep) not in data:
|
255
|
+
avail_splits = []
|
256
|
+
images = []
|
257
|
+
else:
|
258
|
+
images_splits = data[(step, epoch, run_rep)]
|
259
|
+
avail_splits = list(images_splits.keys())
|
260
|
+
avail_splits.sort()
|
261
|
+
if selected is None:
|
262
|
+
selected = avail_splits[0]
|
263
|
+
images = images_splits[selected]
|
264
|
+
|
265
|
+
return Div(
|
266
|
+
Div(
|
267
|
+
SplitSelector(runID, avail_splits, selected=selected, step=step, epoch=epoch, run_rep=run_rep,
|
268
|
+
type=img_type, path="/images/change_split"),
|
269
|
+
Div(
|
270
|
+
StatLine("Step", str(step)),
|
271
|
+
StatLine("Epoch", str(epoch) if epoch is not None else "N/A"),
|
272
|
+
StatLine("Run Repetition", str(run_rep)),
|
273
|
+
cls="artefact-stats-column"
|
274
|
+
),
|
275
|
+
cls="artefact-card-header",
|
276
|
+
),
|
277
|
+
ArtefactGroup(*[ImageComponent(image_id) for image_id in images]),
|
278
|
+
id=f"artefact-card-{step}-{epoch}-{run_rep}",
|
279
|
+
cls="artefact-card",
|
280
|
+
)
|
281
|
+
|
282
|
+
def ImageTab(session, runID, type: Literal["IMAGE", "PLOT"], swap: bool = False):
|
283
|
+
from __main__ import rTable
|
284
|
+
socket = rTable.load_run(runID)
|
285
|
+
|
286
|
+
images_groups = _get_images_groups(socket, type=type)
|
287
|
+
return Div(
|
288
|
+
*[
|
289
|
+
ImageCard(runID, step, epoch, run_rep, img_type=type)
|
290
|
+
for step, epoch, run_rep in images_groups.keys()
|
291
|
+
],
|
292
|
+
style="display; flex; flex-direction: column; align-items: center; justify-content: center;",
|
293
|
+
id="images-tab",
|
294
|
+
hx_swap_oob="true" if swap else None,
|
295
|
+
)
|
296
|
+
|
297
|
+
|
298
|
+
def images_enable(runID, type: Literal["IMAGES", "PLOT"]):
|
299
|
+
"""
|
300
|
+
Check if some scalars are logged and available for the runID. If not, we consider disable it.
|
301
|
+
:param runID: The runID to check.
|
302
|
+
:return: True if scalars are available, False otherwise.
|
303
|
+
"""
|
304
|
+
from __main__ import rTable
|
305
|
+
socket = rTable.load_run(runID)
|
306
|
+
if type == "IMAGES":
|
307
|
+
return len(socket.read_images()) > 0
|
308
|
+
else:
|
309
|
+
return len(socket.read_figures()) > 0
|
310
|
+
|
311
|
+
# routes
|
312
|
+
def build_images_routes(rt):
|
313
|
+
rt("/images/change_split")(change_split)
|
314
|
+
rt("/images/id={image_id}")(load_image)
|
315
|
+
rt("/images/open_modal")(open_image_modal)
|
316
|
+
|
317
|
+
|
318
|
+
def change_split(session, runID: int, step: int, epoch: Optional[int], run_rep: int, split_select: str, type: str):
|
319
|
+
"""
|
320
|
+
Change the split for the images.
|
321
|
+
:param session: The session object.
|
322
|
+
:param step: The step of the images.
|
323
|
+
:param epoch: The epoch of the images.
|
324
|
+
:param run_rep: The run repetition of the images.
|
325
|
+
:param split: The split to change to.
|
326
|
+
:param type: The type of split to change.
|
327
|
+
:return: The updated image card HTML.
|
328
|
+
"""
|
329
|
+
return ImageCard(
|
330
|
+
runID,
|
331
|
+
step,
|
332
|
+
epoch,
|
333
|
+
run_rep,
|
334
|
+
img_type=type,
|
335
|
+
selected=split_select,
|
336
|
+
)
|
337
|
+
|
338
|
+
def load_image(image_id: int):
|
339
|
+
from __main__ import rTable
|
340
|
+
img = rTable.get_image_by_id(image_id)
|
341
|
+
if img is None:
|
342
|
+
return Response(f"Image not found with id: {image_id}:(", status_code=404)
|
343
|
+
img_buffer = io.BytesIO()
|
344
|
+
img.save(img_buffer, format='PNG')
|
345
|
+
img_buffer.seek(0)
|
346
|
+
|
347
|
+
return Response(
|
348
|
+
content=img_buffer.getvalue(),
|
349
|
+
media_type="image/png"
|
350
|
+
)
|
351
|
+
|
352
|
+
def open_image_modal(session, id: int):
|
353
|
+
return Modal(
|
354
|
+
InteractiveImage(
|
355
|
+
id
|
356
|
+
),
|
357
|
+
active=True,
|
358
|
+
)
|
@@ -0,0 +1,86 @@
|
|
1
|
+
from typing import *
|
2
|
+
from datetime import datetime, timedelta
|
3
|
+
from fasthtml.common import *
|
4
|
+
from markupsafe import Markup
|
5
|
+
|
6
|
+
|
7
|
+
def CopyToClipboard(text: str, cls):
|
8
|
+
return Div(
|
9
|
+
Span(
|
10
|
+
I(cls=f'fas fa-copy copy-icon default-icon {cls}'),
|
11
|
+
I(cls=f'fas fa-check copy-icon check-icon {cls}'),
|
12
|
+
cls='copy-icon-container',
|
13
|
+
),
|
14
|
+
Span(text, cls='copy-text' + ' ' + cls),
|
15
|
+
onclick='copyToClipboard(this)',
|
16
|
+
cls='copy-container'
|
17
|
+
)
|
18
|
+
|
19
|
+
def Status(runID: int, status: Literal["running", "finished", "failed"], swap: bool = False):
|
20
|
+
swap_oob = dict(swap_oob="true") if swap else {}
|
21
|
+
return Select(
|
22
|
+
Option("Running", value="running", selected=status == "running", cls="run-status-option running"),
|
23
|
+
Option("Finished", value="finished", selected=status == "finished", cls="run-status-option finished"),
|
24
|
+
Option("Failed", value="failed", selected=status == "failed", cls="run-status-option failed"),
|
25
|
+
id=f"runstatus-select",
|
26
|
+
name="run_status",
|
27
|
+
hx_get=f"/runinfo/change_status?runID={runID}",
|
28
|
+
hx_target="#runstatus-select",
|
29
|
+
hx_trigger="change",
|
30
|
+
hx_swap="outerHTML",
|
31
|
+
hx_params="*",
|
32
|
+
**swap_oob,
|
33
|
+
cls="run-status-select" + " " + status,
|
34
|
+
)
|
35
|
+
|
36
|
+
def DiffView(diff: Optional[str]):
|
37
|
+
diff_parts = diff.splitlines() if diff else [""]
|
38
|
+
dff = []
|
39
|
+
for part in diff_parts:
|
40
|
+
dff.append(P(Markup(part), cls="config-part"))
|
41
|
+
return Div(
|
42
|
+
H2("Diff"),
|
43
|
+
Div(
|
44
|
+
*dff,
|
45
|
+
cls="file-view",
|
46
|
+
)
|
47
|
+
)
|
48
|
+
|
49
|
+
|
50
|
+
def InfoView(runID: int):
|
51
|
+
from __main__ import rTable
|
52
|
+
# Cli
|
53
|
+
row = rTable.fetch_experiment(runID)
|
54
|
+
# RunID, Exp name, cfg, cfg hash, cli, command, comment, start, status, commit, diff
|
55
|
+
start: datetime = datetime.fromisoformat(row[7])
|
56
|
+
status = row[8]
|
57
|
+
commit = row[9]
|
58
|
+
diff = row[10]
|
59
|
+
return (Table(
|
60
|
+
Tr(
|
61
|
+
Td(H3("Start", cls="info-label")),
|
62
|
+
Td(H3(start.strftime("%Y-%m-%d %H:%M:%S"), cls="info-value")),
|
63
|
+
),
|
64
|
+
Tr(
|
65
|
+
Td(H3("Status", cls="info-label")),
|
66
|
+
Td(Status(runID, status), cls="align-right"),
|
67
|
+
),
|
68
|
+
Tr(
|
69
|
+
Td(H3("Commit", cls="info-label")),
|
70
|
+
Td(CopyToClipboard(commit, cls="info-value"), cls="align-right"),
|
71
|
+
),
|
72
|
+
cls="info-table",
|
73
|
+
),
|
74
|
+
DiffView(diff))
|
75
|
+
|
76
|
+
|
77
|
+
|
78
|
+
# Routes
|
79
|
+
def build_info_routes(rt):
|
80
|
+
rt("/runinfo/change_status")(change_status)
|
81
|
+
|
82
|
+
def change_status(session, runID: int, run_status: str):
|
83
|
+
from __main__ import rTable
|
84
|
+
socket = rTable.load_run(runID)
|
85
|
+
socket.set_status(run_status)
|
86
|
+
return Status(runID, run_status, swap=True)
|
@@ -0,0 +1,251 @@
|
|
1
|
+
import plotly.graph_objects as go
|
2
|
+
import pandas as pd
|
3
|
+
from typing import *
|
4
|
+
from datetime import datetime, timedelta
|
5
|
+
from fasthtml.common import *
|
6
|
+
from fh_plotly import plotly2fasthtml
|
7
|
+
from deepboard.gui.components import Legend, Smoother, ChartType, LogSelector
|
8
|
+
from deepboard.gui.utils import get_lines, make_fig
|
9
|
+
|
10
|
+
def make_step_lines(socket, splits: set[str], metric: str, keys: set[tuple[str, str]]):
|
11
|
+
from __main__ import CONFIG
|
12
|
+
lines = []
|
13
|
+
for i, split in enumerate(splits):
|
14
|
+
tag = (split, metric)
|
15
|
+
if tag in keys:
|
16
|
+
reps = get_lines(socket, split, metric, key="step")
|
17
|
+
|
18
|
+
if len(reps) > 1:
|
19
|
+
for rep_idx, rep in enumerate(reps):
|
20
|
+
lines.append((
|
21
|
+
f'{split}_{rep_idx}',
|
22
|
+
rep["index"],
|
23
|
+
rep["value"],
|
24
|
+
CONFIG.COLORS[i % len(CONFIG.COLORS)],
|
25
|
+
rep["epoch"],
|
26
|
+
))
|
27
|
+
else:
|
28
|
+
lines.append((
|
29
|
+
f'{split}',
|
30
|
+
reps[0]["index"],
|
31
|
+
reps[0]["value"],
|
32
|
+
CONFIG.COLORS[i % len(CONFIG.COLORS)],
|
33
|
+
reps[0]["epoch"],
|
34
|
+
))
|
35
|
+
return lines
|
36
|
+
|
37
|
+
def make_time_lines(socket, splits: set[str], metric: str, keys: set[tuple[str, str]]):
|
38
|
+
from __main__ import CONFIG
|
39
|
+
lines = []
|
40
|
+
for i, split in enumerate(splits):
|
41
|
+
tag = (split, metric)
|
42
|
+
if tag in keys:
|
43
|
+
reps = get_lines(socket, split, metric, key="duration")
|
44
|
+
|
45
|
+
if len(reps) > 1:
|
46
|
+
for rep_idx, rep in enumerate(reps):
|
47
|
+
lines.append((
|
48
|
+
f'{split}_{rep_idx}',
|
49
|
+
rep["index"],
|
50
|
+
rep["value"],
|
51
|
+
CONFIG.COLORS[i % len(CONFIG.COLORS)],
|
52
|
+
rep["epoch"],
|
53
|
+
))
|
54
|
+
else:
|
55
|
+
lines.append((
|
56
|
+
f'{split}',
|
57
|
+
reps[0]["index"],
|
58
|
+
reps[0]["value"],
|
59
|
+
CONFIG.COLORS[i % len(CONFIG.COLORS)],
|
60
|
+
reps[0]["epoch"],
|
61
|
+
))
|
62
|
+
return lines
|
63
|
+
|
64
|
+
|
65
|
+
|
66
|
+
def Setup(session, labels: list[tuple]):
|
67
|
+
return Div(
|
68
|
+
H1("Setup", cls="chart-scalar-title"),
|
69
|
+
Div(
|
70
|
+
Div(
|
71
|
+
Smoother(session, path = "/scalars", selected_rows_key="datagrid", session_path="scalars"),
|
72
|
+
ChartType(session, path = "/scalars", selected_rows_key="datagrid", session_path="scalars"),
|
73
|
+
LogSelector(session, path = "/scalars", selected_rows_key="datagrid", session_path="scalars"),
|
74
|
+
style="width: 100%; margin-right: 1em; display: flex; flex-direction: column; align-items: flex-start",
|
75
|
+
),
|
76
|
+
Legend(session, labels, path = "/scalars", selected_rows_key="datagrid"),
|
77
|
+
cls="chart-setup-container",
|
78
|
+
),
|
79
|
+
cls="chart-setup",
|
80
|
+
)
|
81
|
+
# Components
|
82
|
+
def Chart(session, runID: int, metric: str, type: str = "step", running: bool = False, logscale: bool = False):
|
83
|
+
from __main__ import rTable
|
84
|
+
socket = rTable.load_run(runID)
|
85
|
+
keys = socket.formatted_scalars
|
86
|
+
# metrics = {label for split, label in keys}
|
87
|
+
splits = {split for split, label in keys}
|
88
|
+
hidden_lines = session["scalars"]["hidden_lines"] if "hidden_lines" in session["scalars"] else []
|
89
|
+
smoothness = session["scalars"]["smoother_value"] - 1 if "smoother_value" in session["scalars"] else 0
|
90
|
+
if type == "step":
|
91
|
+
lines = make_step_lines(socket, splits, metric, keys)
|
92
|
+
elif type == "time":
|
93
|
+
lines = make_time_lines(socket, splits, metric, keys)
|
94
|
+
else:
|
95
|
+
raise ValueError(f"Unknown plotting type: {type}")
|
96
|
+
|
97
|
+
# # Sort lines by label
|
98
|
+
lines.sort(key=lambda x: x[0])
|
99
|
+
# Hide lines if needed
|
100
|
+
lines = [line for line in lines if line[0] not in hidden_lines]
|
101
|
+
fig = make_fig(lines, type=type, smoothness=smoothness, log_scale=logscale)
|
102
|
+
|
103
|
+
if running:
|
104
|
+
update_params = dict(
|
105
|
+
hx_get=f"/scalars/chart?runID={runID}&metric={metric}&type={type}&running={running}&logscale={logscale}",
|
106
|
+
hx_target=f"#chart-container-{runID}-{metric}",
|
107
|
+
hx_trigger="every 10s",
|
108
|
+
hx_swap="outerHTML",
|
109
|
+
)
|
110
|
+
else:
|
111
|
+
update_params = {}
|
112
|
+
return Div(
|
113
|
+
plotly2fasthtml(fig, js_options=dict(responsive=True)),
|
114
|
+
cls="chart-container",
|
115
|
+
id=f"chart-container-{runID}-{metric}",
|
116
|
+
**update_params
|
117
|
+
)
|
118
|
+
|
119
|
+
def LoadingChart(session, runID: int, metric: str, type: str, running: bool = False, logscale: bool = False):
|
120
|
+
return Div(
|
121
|
+
Div(
|
122
|
+
H1(metric, cls="chart-title"),
|
123
|
+
cls="chart-header",
|
124
|
+
id=f"chart-header-{runID}-{metric}"
|
125
|
+
),
|
126
|
+
Div(
|
127
|
+
cls="chart-container",
|
128
|
+
id=f"chart-container-{runID}-{metric}",
|
129
|
+
hx_get=f"/scalars/chart?runID={runID}&metric={metric}&type={type}&running={running}&logscale={logscale}",
|
130
|
+
hx_target=f"#chart-container-{runID}-{metric}",
|
131
|
+
hx_trigger="load",
|
132
|
+
),
|
133
|
+
cls="chart",
|
134
|
+
id=f"chart-{runID}-{metric}",
|
135
|
+
)
|
136
|
+
|
137
|
+
def Charts(session, runID: int, swap: bool = False, status: Literal["running", "finished", "failed"] = "running"):
|
138
|
+
from __main__ import rTable
|
139
|
+
socket = rTable.load_run(runID)
|
140
|
+
keys = socket.formatted_scalars
|
141
|
+
metrics = {label for split, label in keys}
|
142
|
+
type = session["scalars"]["chart_type"] if "chart_type" in session["scalars"] else "step"
|
143
|
+
logscale = session["scalars"]["chart_scale"] == "log" if "chart_scale" in session["scalars"] else False
|
144
|
+
out = Div(
|
145
|
+
H1("Charts", cls="chart-scalar-title"),
|
146
|
+
Ul(
|
147
|
+
*[
|
148
|
+
Li(LoadingChart(session, runID, metric, type=type, running=status == "running", logscale=logscale), cls="chart-list-item")
|
149
|
+
for metric in metrics
|
150
|
+
],
|
151
|
+
cls="chart-list",
|
152
|
+
),
|
153
|
+
cls="chart-section",
|
154
|
+
id=f"charts-section",
|
155
|
+
hx_swap_oob="true" if swap else None,
|
156
|
+
)
|
157
|
+
return out
|
158
|
+
|
159
|
+
def ScalarTab(session, runID, swap: bool = False):
|
160
|
+
from __main__ import CONFIG, rTable
|
161
|
+
if 'hidden_lines' not in session["scalars"]:
|
162
|
+
session["scalars"]['hidden_lines'] = []
|
163
|
+
socket = rTable.load_run(runID)
|
164
|
+
keys = socket.formatted_scalars
|
165
|
+
splits = {split for split, label in keys}
|
166
|
+
# Get repetitions
|
167
|
+
available_rep = socket.get_repetitions()
|
168
|
+
if len(available_rep) > 1:
|
169
|
+
line_names = [(f'{split}_{rep}', CONFIG.COLORS[i % len(CONFIG.COLORS)], f'{split}_{rep}' in session["scalars"]['hidden_lines']) for i, split in enumerate(splits) for rep in
|
170
|
+
available_rep]
|
171
|
+
else:
|
172
|
+
line_names = [(f'{split}', CONFIG.COLORS[i % len(CONFIG.COLORS)], f'{split}' in session["scalars"]['hidden_lines']) for i, split in enumerate(splits)]
|
173
|
+
# Sort lines by label
|
174
|
+
line_names.sort(key=lambda x: x[0])
|
175
|
+
status = socket.status
|
176
|
+
return Div(
|
177
|
+
Setup(session, line_names),
|
178
|
+
Charts(session, runID, status=status),
|
179
|
+
style="display; flex; width: 40vw; flex-direction: column; align-items: center; justify-content: center;",
|
180
|
+
id="scalar-tab",
|
181
|
+
hx_swap_oob="true" if swap else None,
|
182
|
+
)
|
183
|
+
|
184
|
+
def scalar_enable(runID):
|
185
|
+
"""
|
186
|
+
Check if some scalars are logged and available for the runID. If not, we consider disable it.
|
187
|
+
:param runID: The runID to check.
|
188
|
+
:return: True if scalars are available, False otherwise.
|
189
|
+
"""
|
190
|
+
from __main__ import rTable
|
191
|
+
socket = rTable.load_run(runID)
|
192
|
+
keys = socket.formatted_scalars
|
193
|
+
return len(keys) > 0
|
194
|
+
|
195
|
+
def build_scalar_routes(rt):
|
196
|
+
rt("/scalars/change_chart")(change_chart_type)
|
197
|
+
rt("/scalars/hide_line")(hide_line)
|
198
|
+
rt("/scalars/show_line")(show_line)
|
199
|
+
rt("/scalars/change_smoother")(change_smoother)
|
200
|
+
rt("/scalars/change_scale")(change_chart_scale)
|
201
|
+
rt("/scalars/chart")(load_chart)
|
202
|
+
|
203
|
+
|
204
|
+
# Interactive Routes
|
205
|
+
def change_chart_type(session, runIDs: str, step: bool):
|
206
|
+
new_type = "time" if step else "step"
|
207
|
+
session["scalars"]["chart_type"] = new_type
|
208
|
+
runIds = runIDs.split(",")
|
209
|
+
runID = runIds[0]
|
210
|
+
return (
|
211
|
+
ChartType(session, path="/scalars", selected_rows_key="datagrid", session_path="scalars"), # We want to toggle it
|
212
|
+
Charts(session, int(runID), swap=True)
|
213
|
+
)
|
214
|
+
|
215
|
+
def hide_line(session, runIDs: str, label: str):
|
216
|
+
if 'hidden_lines' not in session["scalars"]:
|
217
|
+
session["scalars"]['hidden_lines'] = []
|
218
|
+
runIds = runIDs.split(",")
|
219
|
+
runID = runIds[0]
|
220
|
+
session["scalars"]['hidden_lines'].append(label)
|
221
|
+
return ScalarTab(session, runID, swap=True)
|
222
|
+
|
223
|
+
|
224
|
+
def show_line(session, runIDs: str, label: str):
|
225
|
+
if 'hidden_lines' not in session["scalars"]:
|
226
|
+
session["scalars"]['hidden_lines'] = []
|
227
|
+
runIds = runIDs.split(",")
|
228
|
+
runID = runIds[0]
|
229
|
+
if label in session["scalars"]['hidden_lines']:
|
230
|
+
session["scalars"]['hidden_lines'].remove(label)
|
231
|
+
|
232
|
+
return ScalarTab(session, runID, swap=True)
|
233
|
+
|
234
|
+
def change_smoother(session, runIDs: str, smoother: int):
|
235
|
+
session["scalars"]["smoother_value"] = smoother
|
236
|
+
runIds = runIDs.split(",")
|
237
|
+
runID = runIds[0]
|
238
|
+
return ScalarTab(session, runID, swap=True)
|
239
|
+
|
240
|
+
def change_chart_scale(session, runIDs: str, log: bool):
|
241
|
+
new_type = "default" if log else "log"
|
242
|
+
session["scalars"]["chart_scale"] = new_type
|
243
|
+
runIds = runIDs.split(",")
|
244
|
+
runID = runIds[0]
|
245
|
+
return (
|
246
|
+
LogSelector(session, path="/scalars", selected_rows_key="datagrid", session_path="scalars"), # We want to toggle it
|
247
|
+
Charts(session, int(runID), swap=True)
|
248
|
+
)
|
249
|
+
|
250
|
+
def load_chart(session, runID: int, metric: str, type: str, running: bool, logscale: bool):
|
251
|
+
return Chart(session, runID, metric, type, running, logscale=logscale)
|