arbok-inspector 0.0.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.
Potentially problematic release.
This version of arbok-inspector might be problematic. Click here for more details.
- arbok_inspector/__init__.py +1 -0
- arbok_inspector/analysis/analysis_base.py +29 -0
- arbok_inspector/analysis/prepare_data.py +118 -0
- arbok_inspector/classes/dim.py +26 -0
- arbok_inspector/classes/run.py +238 -0
- arbok_inspector/cli.py +4 -0
- arbok_inspector/dev.py +19 -0
- arbok_inspector/helpers/string_formaters.py +33 -0
- arbok_inspector/helpers/unit_formater.py +29 -0
- arbok_inspector/main.py +15 -0
- arbok_inspector/pages/__init__.py +2 -0
- arbok_inspector/pages/database_browser.py +159 -0
- arbok_inspector/pages/greeter.py +35 -0
- arbok_inspector/pages/run_view.py +280 -0
- arbok_inspector/state.py +56 -0
- arbok_inspector/test_main.py +65 -0
- arbok_inspector/widgets/build_xarray_grid.py +141 -0
- arbok_inspector/widgets/build_xarray_html.py +57 -0
- arbok_inspector/widgets/json_plot_settings_dialog.py +77 -0
- arbok_inspector/widgets/update_day_selecter.py +36 -0
- arbok_inspector/widgets/update_run_selecter.py +51 -0
- arbok_inspector-0.0.0.dist-info/METADATA +86 -0
- arbok_inspector-0.0.0.dist-info/RECORD +27 -0
- arbok_inspector-0.0.0.dist-info/WHEEL +5 -0
- arbok_inspector-0.0.0.dist-info/entry_points.txt +2 -0
- arbok_inspector-0.0.0.dist-info/licenses/LICENSE +21 -0
- arbok_inspector-0.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from nicegui import ui, app
|
|
3
|
+
from arbok_inspector.state import inspector
|
|
4
|
+
from arbok_inspector.pages.run_view import run_page
|
|
5
|
+
from arbok_inspector.widgets.update_day_selecter import update_day_selecter
|
|
6
|
+
from arbok_inspector.widgets.update_run_selecter import update_run_selecter
|
|
7
|
+
from arbok_inspector.classes.run import Run
|
|
8
|
+
|
|
9
|
+
DAY_GRID_COLUMN_DEFS = [
|
|
10
|
+
{'headerName': 'Day', 'field': 'day'},
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
small_col_width = 30
|
|
14
|
+
RUN_GRID_COLUMN_DEFS = [
|
|
15
|
+
{'headerName': 'Run ID', 'field': 'run_id', "width": small_col_width},
|
|
16
|
+
{'headerName': 'Name', 'field': 'name'},
|
|
17
|
+
{'headerName': 'Exp ID', 'field': 'exp_id', "width": small_col_width},
|
|
18
|
+
{'headerName': '# Results', 'field': 'result_counter', "width": small_col_width},
|
|
19
|
+
{'headerName': 'Started', 'field': 'run_timestamp', "width": small_col_width},
|
|
20
|
+
{'headerName': 'Finish', 'field': 'completed_timestamp', "width": small_col_width},
|
|
21
|
+
]
|
|
22
|
+
RUN_PARAM_DICT = {
|
|
23
|
+
'run_id': 'Run ID',
|
|
24
|
+
'exp_id': 'Experiment ID',
|
|
25
|
+
'result_counter': '# results',
|
|
26
|
+
'run_timestamp': 'Started',
|
|
27
|
+
'completed_timestamp': 'Completed',
|
|
28
|
+
}
|
|
29
|
+
AGGRID_STYLE = 'height: 95%; min-height: 0;'
|
|
30
|
+
EXPANSION_CLASSES = 'w-full p-0 gap-1 border border-gray-400 rounded-lg no-wrap items-start'
|
|
31
|
+
|
|
32
|
+
shared_data = {}
|
|
33
|
+
|
|
34
|
+
@ui.page('/browser')
|
|
35
|
+
async def database_browser_page():
|
|
36
|
+
_ = await ui.context.client.connected()
|
|
37
|
+
app.storage.general["avg_axis"] = None
|
|
38
|
+
app.storage.general["result_keywords"] = None
|
|
39
|
+
app.storage.tab["avg_axis_input"] = None
|
|
40
|
+
app.storage.tab["result_keyword_input"] = None
|
|
41
|
+
offset_minutes = await ui.run_javascript('new Date().getTimezoneOffset()')
|
|
42
|
+
offset_hours = -float(offset_minutes) / 60
|
|
43
|
+
app.storage.general["timezone"] = offset_hours
|
|
44
|
+
print(f"TIMEZONE: UTC{offset_hours}")
|
|
45
|
+
|
|
46
|
+
"""Database general page showing the selected database"""
|
|
47
|
+
grids = {'day': None, 'run': None}
|
|
48
|
+
with ui.column().classes('w-full h-screen'):
|
|
49
|
+
ui.add_head_html('<title>Arbok Inspector - Database general</title>')
|
|
50
|
+
with ui.row().classes('w-full items-center justify-between'):
|
|
51
|
+
ui.label('Arbok Inspector').classes('text-3xl font-bold mb-1')
|
|
52
|
+
|
|
53
|
+
with ui.expansion('Database info and settings', icon='info', value=True)\
|
|
54
|
+
.classes(EXPANSION_CLASSES).props('expand-separator'):
|
|
55
|
+
build_database_info_section(grids)
|
|
56
|
+
|
|
57
|
+
with ui.row().classes('w-full flex-1'):
|
|
58
|
+
build_day_selecter(grids)
|
|
59
|
+
build_run_selecter(grids)
|
|
60
|
+
|
|
61
|
+
def open_run_page(run_id: int):
|
|
62
|
+
# with ui.dialog() as dialog, ui.card():
|
|
63
|
+
# ui.label(f'loading run {run_id} ...').classes(
|
|
64
|
+
# 'text-3xl font-bold mb-1')
|
|
65
|
+
# app.storage.general["run"] = Run(run_id)
|
|
66
|
+
# time.sleep(2)
|
|
67
|
+
# dialog.close()
|
|
68
|
+
shared_data['test'] = "hello"
|
|
69
|
+
app.storage.general["avg_axis"] = app.storage.tab["avg_axis_input"].value
|
|
70
|
+
app.storage.general["result_keywords"] = app.storage.tab["result_keyword_input"].value
|
|
71
|
+
print(f"Result Keywords:")
|
|
72
|
+
print(app.storage.general['result_keywords'])
|
|
73
|
+
ui.navigate.to(f'/run/{run_id}', new_tab=True)
|
|
74
|
+
|
|
75
|
+
def build_database_info_section(grids):
|
|
76
|
+
"""Build the database information and settings section."""
|
|
77
|
+
with ui.row().classes('w-full'):
|
|
78
|
+
build_info_section()
|
|
79
|
+
build_settings_section(grids)
|
|
80
|
+
|
|
81
|
+
def build_info_section():
|
|
82
|
+
"""Build the database information section."""
|
|
83
|
+
with ui.column().classes('w-1/3'):
|
|
84
|
+
ui.label('Database Information').classes('text-xl font-semibold mb-4')
|
|
85
|
+
if inspector.database_path:
|
|
86
|
+
ui.label(f'Database Path: {str(inspector.database_path)}').classes()
|
|
87
|
+
else:
|
|
88
|
+
ui.label('No database selected').classes('text-lg text-red-500')
|
|
89
|
+
|
|
90
|
+
def build_settings_section(grids):
|
|
91
|
+
"""Build the database settings section."""
|
|
92
|
+
with ui.column().classes('w-1/2'):
|
|
93
|
+
with ui.row().classes('w-full'):
|
|
94
|
+
app.storage.tab["result_keyword_input"] = ui.input(
|
|
95
|
+
label = 'auto-plot keywords',
|
|
96
|
+
placeholder="e.g:\t\t[ ( 'Q1' , 'state' ), 'feedback' ]"
|
|
97
|
+
).props('rounded outlined dense')\
|
|
98
|
+
.classes('w-full')\
|
|
99
|
+
.tooltip("""
|
|
100
|
+
Selects all results that contain the specified keywords in their name.<br>
|
|
101
|
+
Can be a single keyword (string) or a tuple of keywords.<br>
|
|
102
|
+
The latter one requires all keywords to be present in the result name.<br>
|
|
103
|
+
|
|
104
|
+
The given example would select all results that contain 'Q1' and 'state' in their name<br>
|
|
105
|
+
or all results that contain 'feedback' in their name.
|
|
106
|
+
""").props('v-html')
|
|
107
|
+
app.storage.tab["avg_axis_input"] = ui.input(
|
|
108
|
+
label = 'average-axis keyword',
|
|
109
|
+
value = "iteration"
|
|
110
|
+
).props('rounded outlined dense')\
|
|
111
|
+
.classes('w-full')
|
|
112
|
+
with ui.row().classes('w-full justify-start'):
|
|
113
|
+
ui.button(
|
|
114
|
+
text = 'Select New Database',
|
|
115
|
+
on_click=lambda: ui.navigate.to('/'),
|
|
116
|
+
color='purple').classes()
|
|
117
|
+
ui.button(
|
|
118
|
+
text = 'Reload',
|
|
119
|
+
on_click=lambda: update_day_selecter(grids['day']),
|
|
120
|
+
color = '#4BA701'
|
|
121
|
+
).classes()
|
|
122
|
+
|
|
123
|
+
def build_day_selecter(grids):
|
|
124
|
+
"""Build the day selecter grid."""
|
|
125
|
+
with ui.column().style('width: 120px;').classes('h-full'):
|
|
126
|
+
grids['day'] = ui.aggrid(
|
|
127
|
+
{
|
|
128
|
+
'defaultColDef': {'flex': 1},
|
|
129
|
+
'columnDefs': DAY_GRID_COLUMN_DEFS,
|
|
130
|
+
'rowData': {},
|
|
131
|
+
'rowSelection': 'multiple',
|
|
132
|
+
},
|
|
133
|
+
theme = 'ag-theme-balham-dark')\
|
|
134
|
+
.classes('text-sm ag-theme-balham-dark')\
|
|
135
|
+
.style(AGGRID_STYLE)\
|
|
136
|
+
.on(
|
|
137
|
+
type = 'cellClicked',
|
|
138
|
+
handler = lambda event: update_run_selecter(
|
|
139
|
+
grids['run'], event.args["value"], RUN_GRID_COLUMN_DEFS)
|
|
140
|
+
)
|
|
141
|
+
update_day_selecter(grids['day'])
|
|
142
|
+
|
|
143
|
+
def build_run_selecter(grids):
|
|
144
|
+
"""Build the run selecter grid."""
|
|
145
|
+
with ui.column().classes('flex-1').classes('h-full'):
|
|
146
|
+
grids['run'] = ui.aggrid(
|
|
147
|
+
{
|
|
148
|
+
'defaultColDef': {'flex': 1},
|
|
149
|
+
'columnDefs': RUN_GRID_COLUMN_DEFS,
|
|
150
|
+
'rowData': {},
|
|
151
|
+
'rowSelection': 'multiple',
|
|
152
|
+
},
|
|
153
|
+
#theme = 'ag-theme-balham-dark'
|
|
154
|
+
).classes('ag-theme-balham-dark').style(
|
|
155
|
+
AGGRID_STYLE
|
|
156
|
+
).on(
|
|
157
|
+
'cellClicked',
|
|
158
|
+
lambda event: open_run_page(event.args['data']['run_id'])
|
|
159
|
+
)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
|
|
2
|
+
from nicegui import ui
|
|
3
|
+
from arbok_inspector.state import inspector
|
|
4
|
+
|
|
5
|
+
@ui.page('/')
|
|
6
|
+
async def greeter_page():
|
|
7
|
+
"""Main page that starts with file selection"""
|
|
8
|
+
ui.add_head_html('<title>Arbok Inspector 🐍</title>')
|
|
9
|
+
|
|
10
|
+
# Show initial file selection dialog
|
|
11
|
+
with ui.dialog() as dialog, ui.card().classes('w-96'):
|
|
12
|
+
# Store dialog reference so we can close it later
|
|
13
|
+
inspector.initial_dialog = dialog
|
|
14
|
+
dialog.props('persistent')
|
|
15
|
+
|
|
16
|
+
ui.label('Welcome to Arbok Inspector').classes('text-h6 mb-4')
|
|
17
|
+
ui.image('https://microsoft.github.io/Qcodes/_images/qcodes_logo.png')
|
|
18
|
+
ui.label('Please enter the path to your QCoDeS database file'
|
|
19
|
+
).classes('text-body1 mb-4')
|
|
20
|
+
ui.label('Database File Path').classes('text-subtitle2 mb-2')
|
|
21
|
+
path_input = ui.input(
|
|
22
|
+
label='Database file path',
|
|
23
|
+
placeholder='C:/path/to/your/database.db'
|
|
24
|
+
).classes('w-full mb-2')
|
|
25
|
+
ui.button(
|
|
26
|
+
text = 'Load Database',
|
|
27
|
+
on_click=lambda: inspector.handle_path_input(path_input),
|
|
28
|
+
icon='folder_open',
|
|
29
|
+
color='purple').classes('mb-4 w-full')
|
|
30
|
+
ui.separator()
|
|
31
|
+
ui.label('Supported formats: .db, .sqlite, .sqlite3'
|
|
32
|
+
).classes('text-caption text-grey')
|
|
33
|
+
|
|
34
|
+
# Auto-open the dialog
|
|
35
|
+
dialog.open()
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"""Run view page showing the data and plots for a specific run"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
import json
|
|
5
|
+
import importlib.resources as resources
|
|
6
|
+
|
|
7
|
+
from nicegui import ui, app
|
|
8
|
+
|
|
9
|
+
from arbok_inspector.widgets.build_xarray_grid import build_xarray_grid
|
|
10
|
+
from arbok_inspector.widgets.build_xarray_html import build_xarray_html
|
|
11
|
+
from arbok_inspector.widgets.json_plot_settings_dialog import JsonPlotSettingsDialog
|
|
12
|
+
from arbok_inspector.helpers.unit_formater import unit_formatter
|
|
13
|
+
from arbok_inspector.classes.run import Run
|
|
14
|
+
|
|
15
|
+
from arbok_inspector.classes.dim import Dim
|
|
16
|
+
|
|
17
|
+
RUN_TABLE_COLUMNS = [
|
|
18
|
+
{'field': 'name', 'filter': 'agTextColumnFilter', 'floatingFilter': True},
|
|
19
|
+
{'field': 'size'},
|
|
20
|
+
{'field': 'x', 'checkboxSelection': True},
|
|
21
|
+
{'field': 'y', 'checkboxSelection': True},
|
|
22
|
+
{'field': 'average', 'checkboxSelection': True},
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
AXIS_OPTIONS = ['average', 'select_value', 'y-axis', 'x-axis']
|
|
26
|
+
|
|
27
|
+
EXPANSION_CLASSES = 'w-full p-0 gap-1 border border-gray-400 rounded-lg no-wrap items-start'
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@ui.page('/run/{run_id}')
|
|
31
|
+
async def run_page(run_id: str):
|
|
32
|
+
"""
|
|
33
|
+
Page showing the details and plots for a specific run.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
run_id (str): ID of the run to display
|
|
37
|
+
"""
|
|
38
|
+
ui.page_title(f"{run_id}")
|
|
39
|
+
_ = await ui.context.client.connected()
|
|
40
|
+
# run = app.storage.general["run"] # Run(int(run_id))
|
|
41
|
+
run = Run(int(run_id))
|
|
42
|
+
|
|
43
|
+
app.storage.tab["placeholders"] = {'plots': None}
|
|
44
|
+
app.storage.tab["run"] = run
|
|
45
|
+
with resources.files("arbok_inspector.configurations").joinpath("1d_plot.json").open("r") as f:
|
|
46
|
+
app.storage.tab["plot_dict_1D"] = json.load(f)
|
|
47
|
+
with resources.files("arbok_inspector.configurations").joinpath("2d_plot.json").open("r") as f:
|
|
48
|
+
app.storage.tab["plot_dict_2D"] = json.load(f)
|
|
49
|
+
|
|
50
|
+
ui.label(f'Run Page for ID: {run_id}').classes('text-2xl font-bold')
|
|
51
|
+
with ui.column().classes('w-full gap-1'):
|
|
52
|
+
with ui.expansion('Coordinates and results', icon='checklist', value=True)\
|
|
53
|
+
.classes(EXPANSION_CLASSES).props('expand-separator'):
|
|
54
|
+
with ui.row().classes('w-full gap-4 no-wrap items-start'):
|
|
55
|
+
with ui.column().classes('w-2/3 gap-2'):
|
|
56
|
+
ui.label("Coordinates:").classes('text-lg font-semibold')
|
|
57
|
+
for i, _ in run.parallel_sweep_axes.items():
|
|
58
|
+
add_dim_dropdown(sweep_idx = i)
|
|
59
|
+
with ui.column().classes('w-1/3 gap-2'):
|
|
60
|
+
ui.label("Results:").classes('text-lg font-semibold')
|
|
61
|
+
for i, result in enumerate(run.full_data_set):
|
|
62
|
+
value = False
|
|
63
|
+
if result in run.plot_selection:
|
|
64
|
+
value = True
|
|
65
|
+
ui.checkbox(
|
|
66
|
+
text = result.replace("__", "."),
|
|
67
|
+
value = value,
|
|
68
|
+
on_change = lambda e, r=result: run.update_plot_selection(e.value, r),
|
|
69
|
+
).classes('text-sm h-4').props('color=purple')
|
|
70
|
+
with ui.expansion('Plots', icon='stacked_line_chart', value=True)\
|
|
71
|
+
.classes(EXPANSION_CLASSES):
|
|
72
|
+
with ui.row().classes('w-full p-2 gap-2 items-center rounded-md border border-neutral-600 bg-neutral-800 text-sm'):
|
|
73
|
+
|
|
74
|
+
ui.button(
|
|
75
|
+
text='Update',
|
|
76
|
+
icon='refresh',
|
|
77
|
+
color='green',
|
|
78
|
+
on_click=lambda: build_xarray_grid(),
|
|
79
|
+
).classes('h-8 px-2')
|
|
80
|
+
|
|
81
|
+
ui.button(
|
|
82
|
+
text='Debug',
|
|
83
|
+
icon='info',
|
|
84
|
+
color='red',
|
|
85
|
+
on_click=lambda: print_debug(run),
|
|
86
|
+
).classes('h-8 px-2')
|
|
87
|
+
|
|
88
|
+
dialog_1d = JsonPlotSettingsDialog('plot_dict_1D')
|
|
89
|
+
dialog_2d = JsonPlotSettingsDialog('plot_dict_2D')
|
|
90
|
+
|
|
91
|
+
ui.button(
|
|
92
|
+
text='1D settings',
|
|
93
|
+
color='pink',
|
|
94
|
+
on_click=dialog_1d.open,
|
|
95
|
+
).classes('h-8 px-2')
|
|
96
|
+
|
|
97
|
+
ui.button(
|
|
98
|
+
text='2D settings',
|
|
99
|
+
color='orange',
|
|
100
|
+
on_click=dialog_2d.open,
|
|
101
|
+
).classes('h-8 px-2')
|
|
102
|
+
|
|
103
|
+
ui.number(
|
|
104
|
+
label='# per col',
|
|
105
|
+
value=2,
|
|
106
|
+
format='%.0f',
|
|
107
|
+
on_change=lambda e: set_plots_per_column(e.value),
|
|
108
|
+
).props('dense outlined').classes('w-20 h-8 text-xs mb-2')
|
|
109
|
+
ui.button(
|
|
110
|
+
icon = 'file_download',
|
|
111
|
+
text = 'full dataset',
|
|
112
|
+
color = 'blue',
|
|
113
|
+
on_click=download_full_dataset,
|
|
114
|
+
).classes('h-8 px-2')
|
|
115
|
+
ui.button(
|
|
116
|
+
icon = 'file_download',
|
|
117
|
+
text = 'data selection',
|
|
118
|
+
color='darkblue',
|
|
119
|
+
on_click=download_data_selection,
|
|
120
|
+
).classes('h-8 px-2')
|
|
121
|
+
#.style('line-height: 1rem; padding-top: 0; padding-bottom: 0;')
|
|
122
|
+
app.storage.tab["placeholders"]["plots"] = ui.row().classes('w-full p-4')
|
|
123
|
+
build_xarray_grid()
|
|
124
|
+
with ui.expansion('xarray summary', icon='summarize', value=False)\
|
|
125
|
+
.classes(EXPANSION_CLASSES):
|
|
126
|
+
build_xarray_html()
|
|
127
|
+
with ui.expansion('analysis', icon='science', value=False)\
|
|
128
|
+
.classes(EXPANSION_CLASSES):
|
|
129
|
+
with ui.row():
|
|
130
|
+
ui.label("Working on it! -Andi").classes('text-lg font-semibold')
|
|
131
|
+
with ui.expansion('metadata', icon='numbers', value=False)\
|
|
132
|
+
.classes(EXPANSION_CLASSES):
|
|
133
|
+
ui.row().classes('w-full p-4')
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def add_dim_dropdown(sweep_idx: int):
|
|
137
|
+
"""
|
|
138
|
+
Add a dropdown to select the dimension option for a given sweep index.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
sweep_idx (int): Index of the sweep to add the dropdown for
|
|
142
|
+
"""
|
|
143
|
+
run = app.storage.tab["run"]
|
|
144
|
+
width = 'w-1/2' if run.together_sweeps else 'w-full'
|
|
145
|
+
dim = run.sweep_dict[sweep_idx]
|
|
146
|
+
local_placeholder = {"slider": None}
|
|
147
|
+
with ui.column().classes('w-full no-wrap items-center gap-1'):
|
|
148
|
+
ui_element = ui.select(
|
|
149
|
+
options = AXIS_OPTIONS,
|
|
150
|
+
value = str(dim.option),
|
|
151
|
+
label = f'{dim.name.replace("__", ".")}',
|
|
152
|
+
on_change = lambda e: update_dim_selection(
|
|
153
|
+
dim, e.value, local_placeholder["slider"])
|
|
154
|
+
).classes(f"{width} text-sm m-0 p-0").props('dense')
|
|
155
|
+
dim.ui_selector = ui_element
|
|
156
|
+
if run.together_sweeps:
|
|
157
|
+
dims_names = run.parallel_sweep_axes[sweep_idx]
|
|
158
|
+
ui.radio(
|
|
159
|
+
options = dims_names,
|
|
160
|
+
value=dim.name,
|
|
161
|
+
on_change = lambda e: update_sweep_dim_name(dim, e.value)
|
|
162
|
+
).classes(width).props('dense')
|
|
163
|
+
local_placeholder["slider"] = ui.column().classes('w-full')
|
|
164
|
+
if dim.option == 'select_value':
|
|
165
|
+
build_dim_slider(run, dim, local_placeholder["slider"])
|
|
166
|
+
|
|
167
|
+
def update_dim_selection(dim: Dim, value: str, slider_placeholder):
|
|
168
|
+
"""
|
|
169
|
+
Update the dimension/sweep selection and rebuild the plot grid.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
dim (Dim): The dimension object to update
|
|
173
|
+
value (str): The new selection value
|
|
174
|
+
slider_placeholder: The UI placeholder to update
|
|
175
|
+
"""
|
|
176
|
+
run = app.storage.tab["run"]
|
|
177
|
+
if slider_placeholder is not None:
|
|
178
|
+
slider_placeholder.clear()
|
|
179
|
+
print(value)
|
|
180
|
+
if value == 'average':
|
|
181
|
+
run.update_subset_dims(dim, 'average')
|
|
182
|
+
dim.option = 'average'
|
|
183
|
+
if value == 'select_value':
|
|
184
|
+
with slider_placeholder:
|
|
185
|
+
build_dim_slider(run, dim, slider_placeholder)
|
|
186
|
+
else:
|
|
187
|
+
run.update_subset_dims(dim, value)
|
|
188
|
+
dim.option = value
|
|
189
|
+
build_xarray_grid()
|
|
190
|
+
|
|
191
|
+
def build_dim_slider(run: Runm, dim: Dim, slider_placeholder):
|
|
192
|
+
"""
|
|
193
|
+
Build a slider for selecting the index of a dimension.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
dim (Dim): The dimension object
|
|
197
|
+
slider_placeholder: The UI placeholder to add the slider to
|
|
198
|
+
"""
|
|
199
|
+
dim_size = run.full_data_set.sizes[dim.name]
|
|
200
|
+
with ui.row().classes("w-full items-center"):
|
|
201
|
+
with ui.column().classes('flex-grow'):
|
|
202
|
+
slider = ui.slider(
|
|
203
|
+
min=0, max=dim_size - 1, step=1, value=0,
|
|
204
|
+
on_change=lambda e: run.update_subset_dims(dim, 'select_value', e.value),
|
|
205
|
+
).classes('flex-grow')\
|
|
206
|
+
.props('color="purple" markers label-always')
|
|
207
|
+
label = ui.html('').classes('shrink-0 text-right px-2 py-1 bg-purple text-white rounded-lg text-xs font-normal text-center')
|
|
208
|
+
update_value_from_dim_slider(label, slider, dim, plot = False)
|
|
209
|
+
slider.on(
|
|
210
|
+
'update:model-value',
|
|
211
|
+
lambda e: update_value_from_dim_slider(label, slider, dim),
|
|
212
|
+
throttle=0.2, leading_events=False)
|
|
213
|
+
|
|
214
|
+
def update_value_from_dim_slider(label, slider, dim: Dim, plot = True):
|
|
215
|
+
"""
|
|
216
|
+
Update the label next to the slider with the current value and unit.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
label: The UI label to update
|
|
220
|
+
slider: The UI slider to get the value from
|
|
221
|
+
dim (Dim): The dimension object
|
|
222
|
+
"""
|
|
223
|
+
run = app.storage.tab["run"]
|
|
224
|
+
label_txt = f' {unit_formatter(run, dim, slider.value)} '
|
|
225
|
+
label.set_content(label_txt)
|
|
226
|
+
if plot:
|
|
227
|
+
build_xarray_grid()
|
|
228
|
+
|
|
229
|
+
def set_plots_per_column(value: int):
|
|
230
|
+
"""
|
|
231
|
+
Set the number of plots to display per column.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
value (int): The number of plots per column
|
|
235
|
+
"""
|
|
236
|
+
run = app.storage.tab["run"]
|
|
237
|
+
ui.notify(f'Setting plots per column to {value}', position='top-right')
|
|
238
|
+
run.plots_per_column = int(value)
|
|
239
|
+
build_xarray_grid()
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def update_sweep_dim_name(dim: Dim, new_name: str):
|
|
243
|
+
"""
|
|
244
|
+
Update the name of the dimension in the sweep dict and the dim object.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
dim (Dim): The dimension object to update
|
|
248
|
+
new_name (str): The new name for the dimension
|
|
249
|
+
"""
|
|
250
|
+
run = app.storage.tab["run"]
|
|
251
|
+
dim.name = new_name
|
|
252
|
+
dim.ui_selector.label = new_name.replace("__", ".")
|
|
253
|
+
build_xarray_grid()
|
|
254
|
+
|
|
255
|
+
def print_debug(run: Run):
|
|
256
|
+
print("\nDebugging Run:")
|
|
257
|
+
for key, val in run.dim_axis_option.items():
|
|
258
|
+
if isinstance(val, list):
|
|
259
|
+
val_str = str([d.name for d in val])
|
|
260
|
+
elif isinstance(val, Dim):
|
|
261
|
+
val_str = val.name
|
|
262
|
+
else:
|
|
263
|
+
val_str = str(val)
|
|
264
|
+
print(f"{key}: \t {val_str}")
|
|
265
|
+
|
|
266
|
+
def download_full_dataset():
|
|
267
|
+
"""Download the full dataset as a NetCDF file."""
|
|
268
|
+
run = app.storage.tab["run"]
|
|
269
|
+
local_path = f'./run_{run.run_id}.nc'
|
|
270
|
+
run.full_data_set.to_netcdf(local_path)
|
|
271
|
+
ui.download.file(local_path)
|
|
272
|
+
os.remove(local_path)
|
|
273
|
+
|
|
274
|
+
def download_data_selection():
|
|
275
|
+
"""Download the current data selection as a NetCDF file."""
|
|
276
|
+
run = app.storage.tab["run"]
|
|
277
|
+
local_path = f'./run_{run.run_id}_selection.nc'
|
|
278
|
+
run.last_subset.to_netcdf(local_path)
|
|
279
|
+
ui.download.file(local_path)
|
|
280
|
+
os.remove(local_path)
|
arbok_inspector/state.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from nicegui import ui
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import Optional
|
|
5
|
+
import sqlite3
|
|
6
|
+
from qcodes.dataset import initialise_or_create_database_at
|
|
7
|
+
|
|
8
|
+
class ArbokInspector:
|
|
9
|
+
def __init__(self):
|
|
10
|
+
self.database_path: Optional[Path] = None
|
|
11
|
+
self.initial_dialog = None
|
|
12
|
+
self.csonn = None
|
|
13
|
+
self.cursor = None
|
|
14
|
+
|
|
15
|
+
def connect_database(self):
|
|
16
|
+
self.conn = sqlite3.connect(self.database_path)
|
|
17
|
+
self.conn.row_factory = sqlite3.Row
|
|
18
|
+
self.cursor = self.conn.cursor()
|
|
19
|
+
initialise_or_create_database_at(self.database_path)
|
|
20
|
+
|
|
21
|
+
def handle_path_input(self, path_input):
|
|
22
|
+
"""Handle manual path input"""
|
|
23
|
+
if path_input.value:
|
|
24
|
+
try:
|
|
25
|
+
file_path = Path(path_input.value)
|
|
26
|
+
if file_path.exists():
|
|
27
|
+
self.database_path = file_path
|
|
28
|
+
ui.notify(f'Database path set: {file_path.name}', type='positive')
|
|
29
|
+
# Only close dialog if path is valid and exists
|
|
30
|
+
try:
|
|
31
|
+
self.connect_database()
|
|
32
|
+
if self.initial_dialog:
|
|
33
|
+
self.initial_dialog.close()
|
|
34
|
+
self.show_database_path()
|
|
35
|
+
except sqlite3.Error as e:
|
|
36
|
+
ui.notify(f'Error connecting to database: {str(e)}', type='negative')
|
|
37
|
+
# Don't close dialog - let user try again
|
|
38
|
+
else:
|
|
39
|
+
ui.notify('File does not exist', type='negative')
|
|
40
|
+
# Don't close dialog - let user try again
|
|
41
|
+
except Exception as ex:
|
|
42
|
+
ui.notify(f'Error: {str(ex)}', type='negative')
|
|
43
|
+
# Don't close dialog on error
|
|
44
|
+
else:
|
|
45
|
+
ui.notify('Please enter a file path', type='warning')
|
|
46
|
+
# Don't close dialog if no path entered
|
|
47
|
+
|
|
48
|
+
def show_database_path(self):
|
|
49
|
+
"""Display the database path on the main page"""
|
|
50
|
+
# Navigate to a database browser page instead of clearing
|
|
51
|
+
ui.navigate.to('/browser')
|
|
52
|
+
|
|
53
|
+
# Initialize the application
|
|
54
|
+
inspector = ArbokInspector()
|
|
55
|
+
inspector.database_path = 'test.db' # For development purposes only
|
|
56
|
+
inspector.connect_database()
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import xarray as xr
|
|
3
|
+
import plotly.express as px
|
|
4
|
+
from nicegui import ui
|
|
5
|
+
|
|
6
|
+
# --- Create a sample 4D DataArray ---
|
|
7
|
+
data = np.random.rand(5, 10, 20, 30) # shape: time, depth, y, x
|
|
8
|
+
coords = {
|
|
9
|
+
'time': np.arange(5),
|
|
10
|
+
'depth': np.linspace(0, 100, 10),
|
|
11
|
+
'y': np.linspace(-5, 5, 20),
|
|
12
|
+
'x': np.linspace(-5, 5, 30),
|
|
13
|
+
}
|
|
14
|
+
array = xr.DataArray(data, dims=('time', 'depth', 'y', 'x'), coords=coords)
|
|
15
|
+
|
|
16
|
+
# --- Define axes to plot ---
|
|
17
|
+
x_dim = 'x'
|
|
18
|
+
y_dim = 'y'
|
|
19
|
+
slider_dims = [dim for dim in array.dims if dim not in (x_dim, y_dim)]
|
|
20
|
+
|
|
21
|
+
# --- UI State ---
|
|
22
|
+
slider_values = {dim: 0 for dim in slider_dims}
|
|
23
|
+
plot_container = ui.row().classes('w-full justify-center')
|
|
24
|
+
|
|
25
|
+
# --- Heatmap update function ---
|
|
26
|
+
def update_plot():
|
|
27
|
+
# Index the array using current slider values
|
|
28
|
+
sel = {dim: slider_values[dim] for dim in slider_dims}
|
|
29
|
+
slice_2d = array.isel(**sel)
|
|
30
|
+
|
|
31
|
+
# Convert to plotly figure
|
|
32
|
+
fig = px.imshow(
|
|
33
|
+
slice_2d.values,
|
|
34
|
+
labels={'x': x_dim, 'y': y_dim},
|
|
35
|
+
x=array.coords[x_dim].values,
|
|
36
|
+
y=array.coords[y_dim].values,
|
|
37
|
+
color_continuous_scale='Viridis',
|
|
38
|
+
)
|
|
39
|
+
fig.update_layout(title=f'{x_dim} vs {y_dim} | ' + ', '.join([f'{dim}={slider_values[dim]}' for dim in slider_dims]))
|
|
40
|
+
|
|
41
|
+
plot_container.clear()
|
|
42
|
+
with plot_container:
|
|
43
|
+
ui.plotly(fig).classes('max-w-3xl max-h-96')
|
|
44
|
+
|
|
45
|
+
# --- Create sliders for all non-plotted dimensions ---
|
|
46
|
+
for dim in slider_dims:
|
|
47
|
+
max_index = len(array.coords[dim]) - 1
|
|
48
|
+
def make_slider(d=dim):
|
|
49
|
+
def on_change(val):
|
|
50
|
+
slider_values[d] = int(val)
|
|
51
|
+
update_plot()
|
|
52
|
+
ui.slider(
|
|
53
|
+
min=0,
|
|
54
|
+
max=max_index,
|
|
55
|
+
value=0,
|
|
56
|
+
step=1,
|
|
57
|
+
on_change=on_change,
|
|
58
|
+
#abel=f'{d} ({array.coords[d].values[0]})'
|
|
59
|
+
).props('label-always').classes('w-full')
|
|
60
|
+
make_slider()
|
|
61
|
+
|
|
62
|
+
# --- Initial plot ---
|
|
63
|
+
update_plot()
|
|
64
|
+
|
|
65
|
+
ui.run()
|