arbok-inspector 0.0.0__tar.gz
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-0.0.0/LICENSE +21 -0
- arbok_inspector-0.0.0/PKG-INFO +86 -0
- arbok_inspector-0.0.0/README.md +69 -0
- arbok_inspector-0.0.0/arbok_inspector/__init__.py +1 -0
- arbok_inspector-0.0.0/arbok_inspector/analysis/analysis_base.py +29 -0
- arbok_inspector-0.0.0/arbok_inspector/analysis/prepare_data.py +118 -0
- arbok_inspector-0.0.0/arbok_inspector/classes/dim.py +26 -0
- arbok_inspector-0.0.0/arbok_inspector/classes/run.py +238 -0
- arbok_inspector-0.0.0/arbok_inspector/cli.py +4 -0
- arbok_inspector-0.0.0/arbok_inspector/dev.py +19 -0
- arbok_inspector-0.0.0/arbok_inspector/helpers/string_formaters.py +33 -0
- arbok_inspector-0.0.0/arbok_inspector/helpers/unit_formater.py +29 -0
- arbok_inspector-0.0.0/arbok_inspector/main.py +15 -0
- arbok_inspector-0.0.0/arbok_inspector/pages/__init__.py +2 -0
- arbok_inspector-0.0.0/arbok_inspector/pages/database_browser.py +159 -0
- arbok_inspector-0.0.0/arbok_inspector/pages/greeter.py +35 -0
- arbok_inspector-0.0.0/arbok_inspector/pages/run_view.py +280 -0
- arbok_inspector-0.0.0/arbok_inspector/state.py +56 -0
- arbok_inspector-0.0.0/arbok_inspector/test_main.py +65 -0
- arbok_inspector-0.0.0/arbok_inspector/widgets/build_xarray_grid.py +141 -0
- arbok_inspector-0.0.0/arbok_inspector/widgets/build_xarray_html.py +57 -0
- arbok_inspector-0.0.0/arbok_inspector/widgets/json_plot_settings_dialog.py +77 -0
- arbok_inspector-0.0.0/arbok_inspector/widgets/update_day_selecter.py +36 -0
- arbok_inspector-0.0.0/arbok_inspector/widgets/update_run_selecter.py +51 -0
- arbok_inspector-0.0.0/arbok_inspector.egg-info/PKG-INFO +86 -0
- arbok_inspector-0.0.0/arbok_inspector.egg-info/SOURCES.txt +30 -0
- arbok_inspector-0.0.0/arbok_inspector.egg-info/dependency_links.txt +1 -0
- arbok_inspector-0.0.0/arbok_inspector.egg-info/entry_points.txt +2 -0
- arbok_inspector-0.0.0/arbok_inspector.egg-info/requires.txt +6 -0
- arbok_inspector-0.0.0/arbok_inspector.egg-info/top_level.txt +1 -0
- arbok_inspector-0.0.0/pyproject.toml +32 -0
- arbok_inspector-0.0.0/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Andreas Nickl
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: arbok-inspector
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary: Browser based QCoDeS database inspector
|
|
5
|
+
License: MIT
|
|
6
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
7
|
+
Requires-Python: >=3.12
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: jupyterlab>=4.4.8
|
|
11
|
+
Requires-Dist: nicegui>=2.24.2
|
|
12
|
+
Requires-Dist: nodejs>=0.1.1
|
|
13
|
+
Requires-Dist: plotly>=6.3.0
|
|
14
|
+
Requires-Dist: qcodes>=0.53.0
|
|
15
|
+
Requires-Dist: xarray>=2025.9.0
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# arbok_inspector ๐
|
|
19
|
+
arbok_inspector is an browser based inspection and visualization utility for QCoDeS measurement
|
|
20
|
+
databases.
|
|
21
|
+
It provides a lightweight GUI and CLI to browse runs and visualize data.
|
|
22
|
+
|
|
23
|
+
## Features ๐
|
|
24
|
+
The most commonly used used tool to visualize QCoDeS databases is
|
|
25
|
+
[plottr](https://github.com/toolsforexperiments/plottr).
|
|
26
|
+
Plottr is a great tool to get started, but struggles with increasing abounts of data.
|
|
27
|
+
|
|
28
|
+
This is how arbok_inspector streamlines your data inspection:
|
|
29
|
+
- Fast browsing of measurement runs and their metadata
|
|
30
|
+
- Written with [nicegui](https://nicegui.io/) acting as a [tailwind](https://tailwindcss.com/) wrapper
|
|
31
|
+
- Browser based approach ensures cross system compatibily
|
|
32
|
+
- Selected runs are opened in a new tab and run on a separate thread
|
|
33
|
+
- this avoids blocking the entire application when loading big datasets
|
|
34
|
+
- plotting backend is plotly which natively returns html
|
|
35
|
+
- plotly plot customization is declarative and can therefore be tweaked in a simple json editor without implementing each customization by hand
|
|
36
|
+
- runs are only loaded on demand
|
|
37
|
+
- startup time in plottr can be several minutes for large databases
|
|
38
|
+
- SQL queries load only the given days upon database selection, only loads respective runs once day is selected
|
|
39
|
+
|
|
40
|
+
## Installation ๐ฒ
|
|
41
|
+
|
|
42
|
+
From pypi install using pip in your environment:
|
|
43
|
+
```bash
|
|
44
|
+
pip install arbok-inspector
|
|
45
|
+
```
|
|
46
|
+
Even better if you are using uv, a uv.lock file is included!
|
|
47
|
+
Launch from CLI:
|
|
48
|
+
```bash
|
|
49
|
+
arbok-inspector
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Project layout
|
|
53
|
+
|
|
54
|
+
- `main.py` โ app entrypoint and startup logic
|
|
55
|
+
- `state.py` โ application state & database handling
|
|
56
|
+
- `pages/` โ NiceGUI pages (database browser, run view, greeter, ...)
|
|
57
|
+
- `widgets/` โ reusable UI widgets (grid builders, selectors, dialogs)
|
|
58
|
+
- `analysis/` โ analysis and data-prep utilities
|
|
59
|
+
- `classes/` โ small domain objects used across the app
|
|
60
|
+
- `helpers/` โ formatting and utility helpers
|
|
61
|
+
|
|
62
|
+
Development & testing ๐ ๏ธ
|
|
63
|
+
|
|
64
|
+
Clone this git repository and navigate into it.
|
|
65
|
+
Use an editable install for local development to pick up changes immediately
|
|
66
|
+
```bash
|
|
67
|
+
pip install -e .
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
To launch the app in editable mode launch from dev.py file:
|
|
71
|
+
```bash
|
|
72
|
+
python -m arbok_inspector/dev.py
|
|
73
|
+
```
|
|
74
|
+
Contributing & help ๐
|
|
75
|
+
|
|
76
|
+
Contributions, bug reports, and small feature requests are welcome. If you want to add a visualization or a new page, use `pages/` and `widgets/` for examples of how UI components are composed. When opening a PR, please keep changes focused and include a short description of how to exercise the change locally.
|
|
77
|
+
|
|
78
|
+
License
|
|
79
|
+
|
|
80
|
+
See the `LICENSE` file in the project root for license details.
|
|
81
|
+
|
|
82
|
+
Notes & tips
|
|
83
|
+
|
|
84
|
+
- For exact runtime dependencies check `pyproject.toml` โ prefer using that manifest (and a virtual environment) for reproducible installs.
|
|
85
|
+
- If you want me to add a short walkthrough for common tasks (open a run, plot data, export CSV), tell me which task you'd like first and I can add a step-by-step example here. ๐
|
|
86
|
+
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# arbok_inspector ๐
|
|
2
|
+
arbok_inspector is an browser based inspection and visualization utility for QCoDeS measurement
|
|
3
|
+
databases.
|
|
4
|
+
It provides a lightweight GUI and CLI to browse runs and visualize data.
|
|
5
|
+
|
|
6
|
+
## Features ๐
|
|
7
|
+
The most commonly used used tool to visualize QCoDeS databases is
|
|
8
|
+
[plottr](https://github.com/toolsforexperiments/plottr).
|
|
9
|
+
Plottr is a great tool to get started, but struggles with increasing abounts of data.
|
|
10
|
+
|
|
11
|
+
This is how arbok_inspector streamlines your data inspection:
|
|
12
|
+
- Fast browsing of measurement runs and their metadata
|
|
13
|
+
- Written with [nicegui](https://nicegui.io/) acting as a [tailwind](https://tailwindcss.com/) wrapper
|
|
14
|
+
- Browser based approach ensures cross system compatibily
|
|
15
|
+
- Selected runs are opened in a new tab and run on a separate thread
|
|
16
|
+
- this avoids blocking the entire application when loading big datasets
|
|
17
|
+
- plotting backend is plotly which natively returns html
|
|
18
|
+
- plotly plot customization is declarative and can therefore be tweaked in a simple json editor without implementing each customization by hand
|
|
19
|
+
- runs are only loaded on demand
|
|
20
|
+
- startup time in plottr can be several minutes for large databases
|
|
21
|
+
- SQL queries load only the given days upon database selection, only loads respective runs once day is selected
|
|
22
|
+
|
|
23
|
+
## Installation ๐ฒ
|
|
24
|
+
|
|
25
|
+
From pypi install using pip in your environment:
|
|
26
|
+
```bash
|
|
27
|
+
pip install arbok-inspector
|
|
28
|
+
```
|
|
29
|
+
Even better if you are using uv, a uv.lock file is included!
|
|
30
|
+
Launch from CLI:
|
|
31
|
+
```bash
|
|
32
|
+
arbok-inspector
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Project layout
|
|
36
|
+
|
|
37
|
+
- `main.py` โ app entrypoint and startup logic
|
|
38
|
+
- `state.py` โ application state & database handling
|
|
39
|
+
- `pages/` โ NiceGUI pages (database browser, run view, greeter, ...)
|
|
40
|
+
- `widgets/` โ reusable UI widgets (grid builders, selectors, dialogs)
|
|
41
|
+
- `analysis/` โ analysis and data-prep utilities
|
|
42
|
+
- `classes/` โ small domain objects used across the app
|
|
43
|
+
- `helpers/` โ formatting and utility helpers
|
|
44
|
+
|
|
45
|
+
Development & testing ๐ ๏ธ
|
|
46
|
+
|
|
47
|
+
Clone this git repository and navigate into it.
|
|
48
|
+
Use an editable install for local development to pick up changes immediately
|
|
49
|
+
```bash
|
|
50
|
+
pip install -e .
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
To launch the app in editable mode launch from dev.py file:
|
|
54
|
+
```bash
|
|
55
|
+
python -m arbok_inspector/dev.py
|
|
56
|
+
```
|
|
57
|
+
Contributing & help ๐
|
|
58
|
+
|
|
59
|
+
Contributions, bug reports, and small feature requests are welcome. If you want to add a visualization or a new page, use `pages/` and `widgets/` for examples of how UI components are composed. When opening a PR, please keep changes focused and include a short description of how to exercise the change locally.
|
|
60
|
+
|
|
61
|
+
License
|
|
62
|
+
|
|
63
|
+
See the `LICENSE` file in the project root for license details.
|
|
64
|
+
|
|
65
|
+
Notes & tips
|
|
66
|
+
|
|
67
|
+
- For exact runtime dependencies check `pyproject.toml` โ prefer using that manifest (and a virtual environment) for reproducible installs.
|
|
68
|
+
- If you want me to add a short walkthrough for common tasks (open a run, plot data, export CSV), tell me which task you'd like first and I can add a step-by-step example here. ๐
|
|
69
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# from .database_browser import database_browser_page
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Module containing AnalysisBase class"""
|
|
2
|
+
|
|
3
|
+
class AnalysisBase:
|
|
4
|
+
"""Base class for analysis classes"""
|
|
5
|
+
run_id = None
|
|
6
|
+
xr_data = None
|
|
7
|
+
|
|
8
|
+
def find_axis_from_keyword(self, keyword: str) -> str:
|
|
9
|
+
"""
|
|
10
|
+
Find the axis corresponding to a keyword in the analysis
|
|
11
|
+
Args:
|
|
12
|
+
keyword (str): Keyword to search for
|
|
13
|
+
Returns:
|
|
14
|
+
axis (int): Axis corresponding to keyword
|
|
15
|
+
"""
|
|
16
|
+
axes = []
|
|
17
|
+
for axis in self.xr_data.dims:
|
|
18
|
+
if keyword in axis:
|
|
19
|
+
axes.append(axis)
|
|
20
|
+
if len(axes) == 0:
|
|
21
|
+
raise ValueError(
|
|
22
|
+
f"Axis not found for keyword {keyword}. "
|
|
23
|
+
f"Dims are {self.xr_data.dims}"
|
|
24
|
+
)
|
|
25
|
+
elif len(axes) > 1:
|
|
26
|
+
raise ValueError(
|
|
27
|
+
f"More than one axis found for keyword {keyword}: {axes}")
|
|
28
|
+
else:
|
|
29
|
+
return axes[0]
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Module containing prepare_data function for analysis tools"""
|
|
2
|
+
|
|
3
|
+
from matplotlib.pylab import f
|
|
4
|
+
from qcodes.dataset.data_set import load_by_id, DataSet
|
|
5
|
+
import xarray as xr
|
|
6
|
+
import numpy as np
|
|
7
|
+
import matplotlib.pyplot as plt
|
|
8
|
+
|
|
9
|
+
def prepare_and_avg_data(
|
|
10
|
+
run: int | DataSet | xr.Dataset | xr.DataArray,
|
|
11
|
+
readout_name: str,
|
|
12
|
+
avg_axes: str | list = 'auto'
|
|
13
|
+
) -> tuple[int | None, xr.DataArray, np.ndarray]:
|
|
14
|
+
"""
|
|
15
|
+
Prepares the data for plotting. Takes either a run id, a qcodes dataset,
|
|
16
|
+
an xarray dataset or an xarray data-array and returns the run id, the xarray
|
|
17
|
+
data-array and the numpy data-array.
|
|
18
|
+
This is done to allow different input types for the data while keeping the
|
|
19
|
+
same output format.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
run (int | DataSet | xr.Dataset | xr.DataArray): Run id, qcodes dataset'
|
|
23
|
+
xarray dataset or xarray data-array
|
|
24
|
+
readout_name (str): Name of the readout observable
|
|
25
|
+
"""
|
|
26
|
+
xdata_array = None
|
|
27
|
+
if avg_axes is None:
|
|
28
|
+
avg_axes = []
|
|
29
|
+
if isinstance(run, int):
|
|
30
|
+
data = load_by_id(run)
|
|
31
|
+
xdataset = data.to_xarray_dataset()
|
|
32
|
+
run_id = run
|
|
33
|
+
elif isinstance(run, DataSet):
|
|
34
|
+
data = run
|
|
35
|
+
run_id = data.run_id
|
|
36
|
+
xdataset = data.to_xarray_dataset()
|
|
37
|
+
elif isinstance(run, xr.Dataset):
|
|
38
|
+
xdataset = run
|
|
39
|
+
run_id = xdataset.attrs['run_id']
|
|
40
|
+
elif isinstance(run, xr.DataArray):
|
|
41
|
+
xdataset = None
|
|
42
|
+
xdata_array = run
|
|
43
|
+
run_id = None
|
|
44
|
+
else:
|
|
45
|
+
raise ValueError(
|
|
46
|
+
"Invalid input type for run. "
|
|
47
|
+
"Must be run-ID, DataSet or xr.Dataset or xr.DataArray. "
|
|
48
|
+
f"Is {type(run)}"
|
|
49
|
+
)
|
|
50
|
+
if xdataset is not None:
|
|
51
|
+
if readout_name not in xdataset.data_vars:
|
|
52
|
+
readout_name = find_data_variable_from_keyword(xdataset, readout_name)
|
|
53
|
+
xdata_array = xdataset[readout_name]
|
|
54
|
+
### Average over specified axes
|
|
55
|
+
xdata_array = avg_dataarray(xdata_array, avg_axes)
|
|
56
|
+
np_data = xdata_array.to_numpy()
|
|
57
|
+
return run_id, xdata_array, np_data
|
|
58
|
+
|
|
59
|
+
def find_data_variable_from_keyword(
|
|
60
|
+
xdata_array: xr.DataArray, keyword: str | tuple) -> str:
|
|
61
|
+
"""
|
|
62
|
+
Find the data variable corresponding to a keyword in the data-array.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
xdata_array (xr.DataArray): xarray data-array to search in
|
|
66
|
+
keyword (str): Keyword to search for
|
|
67
|
+
Returns:
|
|
68
|
+
data_variable (str): Data variable corresponding to keyword
|
|
69
|
+
"""
|
|
70
|
+
if isinstance(keyword, str):
|
|
71
|
+
keyword = (keyword,)
|
|
72
|
+
if not isinstance(keyword, tuple):
|
|
73
|
+
raise ValueError(
|
|
74
|
+
f"Keyword must be a string or a tuple. Is {type(keyword)}")
|
|
75
|
+
data_variables = []
|
|
76
|
+
for data_variable in xdata_array.data_vars:
|
|
77
|
+
if all([subkey in str(data_variable) for subkey in keyword]):
|
|
78
|
+
data_variables.append(data_variable)
|
|
79
|
+
if len(data_variables) == 0:
|
|
80
|
+
raise ValueError(
|
|
81
|
+
f"Data variable not found for keyword {keyword}. "
|
|
82
|
+
f"Data variables are {xdata_array.data_vars}"
|
|
83
|
+
)
|
|
84
|
+
elif len(data_variables) > 1:
|
|
85
|
+
raise ValueError(
|
|
86
|
+
f"More than one data variable found for keyword {keyword}: "
|
|
87
|
+
f"{[str(var) for var in data_variables]}")
|
|
88
|
+
else:
|
|
89
|
+
return data_variables[0]
|
|
90
|
+
|
|
91
|
+
def avg_dataarray(xdata_array: xr.DataArray, avg_axes: str | list = 'auto'):
|
|
92
|
+
"""
|
|
93
|
+
Averages the data-array over the specified axes. If no axes are specified
|
|
94
|
+
the data-array is averaged over all axes.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
xdata_array (xr.DataArray): xarray data-array to be averaged
|
|
98
|
+
avg_axes (str | list): Axes to average over
|
|
99
|
+
"""
|
|
100
|
+
if avg_axes is None:
|
|
101
|
+
avg_axes = []
|
|
102
|
+
if isinstance(avg_axes, str):
|
|
103
|
+
### If 'auto' is given, find all axes with 'iteration' in the name
|
|
104
|
+
if avg_axes == 'auto':
|
|
105
|
+
avg_axes = []
|
|
106
|
+
for dim in xdata_array.dims:
|
|
107
|
+
if 'iteration' in dim:
|
|
108
|
+
avg_axes.append(dim)
|
|
109
|
+
else:
|
|
110
|
+
avg_axes = [avg_axes]
|
|
111
|
+
### Average over specified axes
|
|
112
|
+
for axis in avg_axes:
|
|
113
|
+
if hasattr(xdata_array, axis):
|
|
114
|
+
xdata_array = xdata_array.mean(axis)
|
|
115
|
+
else:
|
|
116
|
+
raise KeyError(
|
|
117
|
+
f"Avg. axis {axis} not found in xarray data-array")
|
|
118
|
+
return xdata_array
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Module for the Dim class."""
|
|
2
|
+
|
|
3
|
+
class Dim:
|
|
4
|
+
"""
|
|
5
|
+
Class representing a dimension of the data
|
|
6
|
+
"""
|
|
7
|
+
def __init__(self, name):
|
|
8
|
+
"""
|
|
9
|
+
Constructor for Dim class
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
name (str): Name of the dimension
|
|
13
|
+
|
|
14
|
+
Attributes:
|
|
15
|
+
name (str): Name of the dimension
|
|
16
|
+
option (str): Option for the dimension (average, select_value, x-axis, y-axis)
|
|
17
|
+
select_index (int): Index of the selected value for select_value option
|
|
18
|
+
ui_selector: Reference to the UI element for the dimension
|
|
19
|
+
"""
|
|
20
|
+
self.name = name
|
|
21
|
+
self.option = None
|
|
22
|
+
self.select_index = 0
|
|
23
|
+
self.ui_selector = None
|
|
24
|
+
|
|
25
|
+
def __str__(self):
|
|
26
|
+
return self.name
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Run class representing a single run of the experiment.
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
import ast
|
|
8
|
+
import re
|
|
9
|
+
import json
|
|
10
|
+
from qcodes.dataset import load_by_id
|
|
11
|
+
from nicegui import ui, app
|
|
12
|
+
|
|
13
|
+
from arbok_inspector.classes.dim import Dim
|
|
14
|
+
from arbok_inspector.widgets.build_xarray_grid import build_xarray_grid
|
|
15
|
+
# from arbok_inspector.pages.database_browser import shared_data
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from qcodes.dataset.data_set import DataSet
|
|
19
|
+
from xarray import Dataset
|
|
20
|
+
AXIS_OPTIONS = ['average', 'select_value', 'y-axis', 'x-axis']
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Run:
|
|
24
|
+
"""
|
|
25
|
+
Class representing a run with its data and methods
|
|
26
|
+
"""
|
|
27
|
+
def __init__(self, run_id: int):
|
|
28
|
+
"""
|
|
29
|
+
Constructor for Run class
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
run_id (int): ID of the run
|
|
33
|
+
"""
|
|
34
|
+
self.run_id: int = run_id
|
|
35
|
+
self.title: str = f'Run ID: {run_id} (-> add experiment)'
|
|
36
|
+
self.dataset: DataSet = load_by_id(run_id)
|
|
37
|
+
self.full_data_set: Dataset = self.dataset.to_xarray_dataset()
|
|
38
|
+
self.last_subset: Dataset = self.full_data_set
|
|
39
|
+
|
|
40
|
+
self.together_sweeps: bool = False
|
|
41
|
+
self.parallel_sweep_axes: dict = {}
|
|
42
|
+
self.sweep_dict: dict[int, Dim] = {}
|
|
43
|
+
self.load_sweep_dict()
|
|
44
|
+
self.dims: list[Dim] = list(self.sweep_dict.values())
|
|
45
|
+
self.dim_axis_option: dict[str, str|list[Dim]] = self.set_dim_axis_option()
|
|
46
|
+
print(self.dims)
|
|
47
|
+
|
|
48
|
+
self.plot_selection: list[str] = self.select_results_by_keywords(
|
|
49
|
+
app.storage.general["result_keywords"]
|
|
50
|
+
)
|
|
51
|
+
print(f"Initial plot selection: {self.plot_selection}")
|
|
52
|
+
self.plots_per_column: int = 2
|
|
53
|
+
|
|
54
|
+
def load_sweep_dict(self):
|
|
55
|
+
"""
|
|
56
|
+
Load the sweep dictionary from the dataset
|
|
57
|
+
TODO: check metadata for sweep information!
|
|
58
|
+
Returns:
|
|
59
|
+
sweep_dict (dict): Dictionary with sweep information
|
|
60
|
+
is_together (bool): True if all sweeps are together, False otherwise
|
|
61
|
+
"""
|
|
62
|
+
if "parallel_sweep_axes" in self.dataset.metadata:
|
|
63
|
+
conf = self.dataset.metadata["parallel_sweep_axes"]
|
|
64
|
+
conf = conf.replace("'", '"') # Ensure JSON compatibility
|
|
65
|
+
print( conf)
|
|
66
|
+
conf = json.loads(conf)
|
|
67
|
+
self.parallel_sweep_axes = {int(i): sweeps for i, sweeps in conf.items()}
|
|
68
|
+
self.together_sweeps = True
|
|
69
|
+
else:
|
|
70
|
+
dims = self.full_data_set.dims
|
|
71
|
+
self.parallel_sweep_axes = {i: [dim] for i, dim in enumerate(dims)}
|
|
72
|
+
self.together_sweeps = False
|
|
73
|
+
self.sweep_dict = {
|
|
74
|
+
i: Dim(names[0]) for i, names in self.parallel_sweep_axes.items()
|
|
75
|
+
}
|
|
76
|
+
print(self.sweep_dict)
|
|
77
|
+
return self.sweep_dict
|
|
78
|
+
|
|
79
|
+
def set_dim_axis_option(self):
|
|
80
|
+
"""
|
|
81
|
+
Set the default dimension options for the run in 4 steps:
|
|
82
|
+
1. Set all iteration dims to 'average'
|
|
83
|
+
2. Set the innermost dim to 'x-axis' (the last one that is not averaged)
|
|
84
|
+
3. Set the next innermost dim to 'y-axis'
|
|
85
|
+
4. Set all remaining dims to 'select_value'
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
options (dict): Dictionary with keys 'average', 'select_value', 'y-axis',
|
|
89
|
+
"""
|
|
90
|
+
options = {x: [] for x in AXIS_OPTIONS}
|
|
91
|
+
print(f"Setting average to {app.storage.general['avg_axis']}")
|
|
92
|
+
for dim in self.dims:
|
|
93
|
+
if app.storage.general["avg_axis"] in dim.name:
|
|
94
|
+
dim.option = 'average'
|
|
95
|
+
options['average'].append(dim)
|
|
96
|
+
for dim in reversed(self.dims):
|
|
97
|
+
if dim not in options['average'] and dim != options['x-axis']:
|
|
98
|
+
dim.option = "x-axis"
|
|
99
|
+
options['x-axis'] = dim
|
|
100
|
+
print(f"Setting x-axis to {dim.name}")
|
|
101
|
+
break
|
|
102
|
+
for dim in reversed(self.dims):
|
|
103
|
+
if dim not in options['average'] and dim != options['x-axis']:
|
|
104
|
+
dim.option = 'y-axis'
|
|
105
|
+
options['y-axis'] = dim
|
|
106
|
+
print(f"Setting y-axis to {dim.name}")
|
|
107
|
+
break
|
|
108
|
+
for dim in self.dims:
|
|
109
|
+
if dim not in options['average'] and dim != options['x-axis'] and dim != options['y-axis']:
|
|
110
|
+
dim.option = 'select_value'
|
|
111
|
+
options['select_value'].append(dim)
|
|
112
|
+
dim.select_index = 0
|
|
113
|
+
print(f"Setting select_value to {dim.name}")
|
|
114
|
+
return options
|
|
115
|
+
|
|
116
|
+
def select_results_by_keywords(self, keywords: list[str|tuple]) -> list[str]:
|
|
117
|
+
"""
|
|
118
|
+
Select results by keywords in their name.
|
|
119
|
+
Args:
|
|
120
|
+
keywords (list): List of keywords to search for
|
|
121
|
+
Returns:
|
|
122
|
+
selected_results (list): List of selected result names
|
|
123
|
+
"""
|
|
124
|
+
print(f"using keywords: {keywords}")
|
|
125
|
+
if keywords is None or len(keywords) == 0 or keywords == '':
|
|
126
|
+
return [next(iter(self.full_data_set.data_vars))]
|
|
127
|
+
s_quoted = re.sub(r'\b([a-zA-Z_][a-zA-Z0-9_]*)\b', r'"\1"', keywords)
|
|
128
|
+
try:
|
|
129
|
+
keywords = ast.literal_eval(s_quoted)
|
|
130
|
+
except (SyntaxError, ValueError):
|
|
131
|
+
print(f"Error parsing keywords: {s_quoted}")
|
|
132
|
+
keywords = []
|
|
133
|
+
ui.notify(
|
|
134
|
+
f"Error parsing result keywords: {s_quoted}. Please use a valid Python list.",
|
|
135
|
+
color='red',
|
|
136
|
+
position='top-right'
|
|
137
|
+
)
|
|
138
|
+
if not isinstance(keywords, list):
|
|
139
|
+
keywords = [keywords]
|
|
140
|
+
selected_results = []
|
|
141
|
+
print(f"using keywords: {keywords}")
|
|
142
|
+
for result in self.full_data_set.data_vars:
|
|
143
|
+
for keyword in keywords:
|
|
144
|
+
if isinstance(keyword, str) and keyword in str(result):
|
|
145
|
+
selected_results.append(result)
|
|
146
|
+
elif isinstance(keyword, tuple) and all(
|
|
147
|
+
subkey in str(result) for subkey in keyword):
|
|
148
|
+
selected_results.append(result)
|
|
149
|
+
selected_results = list(set(selected_results)) # Remove duplicates
|
|
150
|
+
if len(selected_results) == 0:
|
|
151
|
+
selected_results = [next(iter(self.full_data_set.data_vars))]
|
|
152
|
+
print(f"Selected results: {selected_results}")
|
|
153
|
+
return selected_results
|
|
154
|
+
|
|
155
|
+
def update_subset_dims(self, dim: Dim, selection: str, index = None):
|
|
156
|
+
"""
|
|
157
|
+
Update the subset dimensions based on user selection.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
dim (Dim): The dimension object to update
|
|
161
|
+
selection (str): The new selection option
|
|
162
|
+
('average', 'select_value', 'x-axis', 'y-axis')
|
|
163
|
+
index (int, optional): The index for 'select_value' option. Defaults to None.
|
|
164
|
+
"""
|
|
165
|
+
text = f'Updating subset dims: {dim.name} to {selection}'
|
|
166
|
+
print(text)
|
|
167
|
+
ui.notify(text, position='top-right')
|
|
168
|
+
|
|
169
|
+
### First, remove old option this dim was on
|
|
170
|
+
for option in ['average', 'select_value']:
|
|
171
|
+
if dim in self.dim_axis_option[option]:
|
|
172
|
+
print(f"Removing {dim.name} from {option}")
|
|
173
|
+
self.dim_axis_option[option].remove(dim)
|
|
174
|
+
dim.option = None
|
|
175
|
+
if dim.option in ['x-axis', 'y-axis']:
|
|
176
|
+
print(f"Removing {dim.name} from {dim.option}")
|
|
177
|
+
self.dim_axis_option[dim.option] = None
|
|
178
|
+
|
|
179
|
+
### Now, set new option
|
|
180
|
+
if selection in ['average', 'select_value']:
|
|
181
|
+
# dim.ui_selector.value = selection
|
|
182
|
+
dim.select_index = index
|
|
183
|
+
self.dim_axis_option[selection].append(dim)
|
|
184
|
+
return
|
|
185
|
+
if selection in ['x-axis', 'y-axis']:
|
|
186
|
+
old_dim = self.dim_axis_option[selection]
|
|
187
|
+
self.dim_axis_option[selection] = dim
|
|
188
|
+
if old_dim is not None:
|
|
189
|
+
# Set previous dim (having this option) to 'select_value'
|
|
190
|
+
# Required since x and y axis ahve to be unique
|
|
191
|
+
print(f"Updating {old_dim.name} to {dim.name} on {selection}")
|
|
192
|
+
if old_dim.option in ['x-axis', 'y-axis']:
|
|
193
|
+
self.dim_axis_option['select_value'].append(old_dim)
|
|
194
|
+
old_dim.option = 'select_value'
|
|
195
|
+
old_dim.ui_selector.value = 'select_value'
|
|
196
|
+
self.update_subset_dims(old_dim, 'select_value', old_dim.select_index)
|
|
197
|
+
dim.ui_selector.update()
|
|
198
|
+
|
|
199
|
+
def generate_subset(self):
|
|
200
|
+
"""
|
|
201
|
+
Generate the subset of the full dataset based on the current dimension options.
|
|
202
|
+
Returns:
|
|
203
|
+
sub_set (xarray.Dataset): The subset of the full dataset
|
|
204
|
+
"""
|
|
205
|
+
# TODO: take the averaging out of this! We only want to average if necessary
|
|
206
|
+
# averaging can be computationally intensive!
|
|
207
|
+
sub_set = self.full_data_set
|
|
208
|
+
for avg_axis in self.dim_axis_option['average']:
|
|
209
|
+
sub_set = sub_set.mean(dim=avg_axis.name)
|
|
210
|
+
sel_dict = {d.name: d.select_index for d in self.dim_axis_option['select_value']}
|
|
211
|
+
sub_set = sub_set.isel(**sel_dict).squeeze()
|
|
212
|
+
self.last_subset = sub_set
|
|
213
|
+
return sub_set
|
|
214
|
+
|
|
215
|
+
def update_plot_selection(self, value: bool, readout_name: str):
|
|
216
|
+
"""
|
|
217
|
+
Update the plot selection based on user interaction.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
value (bool): True if the result is selected, False otherwise
|
|
221
|
+
readout_name (str): Name of the result to update
|
|
222
|
+
"""
|
|
223
|
+
print(f"{readout_name= } {value= }")
|
|
224
|
+
pretty_readout_name = readout_name.replace("__", ".")
|
|
225
|
+
if readout_name not in self.plot_selection:
|
|
226
|
+
self.plot_selection.append(readout_name)
|
|
227
|
+
ui.notify(
|
|
228
|
+
message=f'Result {pretty_readout_name} added to plot selection',
|
|
229
|
+
position='top-right'
|
|
230
|
+
)
|
|
231
|
+
else:
|
|
232
|
+
self.plot_selection.remove(readout_name)
|
|
233
|
+
ui.notify(
|
|
234
|
+
f'Result {pretty_readout_name} removed from plot selection',
|
|
235
|
+
position='top-right'
|
|
236
|
+
)
|
|
237
|
+
print(f"{self.plot_selection= }")
|
|
238
|
+
build_xarray_grid()
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from nicegui import ui
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from arbok_inspector.state import inspector
|
|
7
|
+
from arbok_inspector.pages import greeter, database_browser
|
|
8
|
+
|
|
9
|
+
def main():
|
|
10
|
+
ui.run(
|
|
11
|
+
title='Arbok Inspector',
|
|
12
|
+
favicon='๐',
|
|
13
|
+
dark=True,
|
|
14
|
+
show=True,
|
|
15
|
+
port=8090
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
if __name__ in {"__main__", "__mp_main__"}:
|
|
19
|
+
main()
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
|
|
2
|
+
import xarray as xr
|
|
3
|
+
from arbok_inspector.helpers.unit_formater import unit_formatter
|
|
4
|
+
|
|
5
|
+
def title_formater(run):
|
|
6
|
+
"""
|
|
7
|
+
Format title string for plots based on selected dimensions.
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
run: The Run object containing the data.
|
|
11
|
+
Returns:
|
|
12
|
+
A formatted title string.
|
|
13
|
+
"""
|
|
14
|
+
title = ""
|
|
15
|
+
for dim in run.dim_axis_option["select_value"]:
|
|
16
|
+
title += f"{dim.name } = {unit_formatter(run, dim, dim.select_index)}<br>"
|
|
17
|
+
return title
|
|
18
|
+
|
|
19
|
+
def axis_label_formater(ds: xr.DataArray, dim_name: str) -> str:
|
|
20
|
+
"""
|
|
21
|
+
Format axis label by replacing '__' with '.' and bolding the last part.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
dim_name: The dimension name string.
|
|
25
|
+
Returns:
|
|
26
|
+
A formatted axis label string.
|
|
27
|
+
"""
|
|
28
|
+
dim_list = dim_name.split('__')
|
|
29
|
+
print(f"{dim_list=}")
|
|
30
|
+
if len(dim_list) > 1:
|
|
31
|
+
return f"{'.'.join(dim_list[:-1])}.<b>{dim_list[-1]}</b> ({ds.coords[dim_name].unit})"
|
|
32
|
+
else:
|
|
33
|
+
return f"<b>{dim_list[0]}</b> ({ds.coords[dim_name].unit})"
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Helper functions for formatting units with SI prefixes."""
|
|
2
|
+
|
|
3
|
+
def unit_formatter(run, dim, index: int) -> str:
|
|
4
|
+
"""
|
|
5
|
+
If value is larger than 1e3, format with SI prefix.
|
|
6
|
+
Same if smaller than 1e-3.
|
|
7
|
+
|
|
8
|
+
Args:
|
|
9
|
+
run (Run): Run object containing the data
|
|
10
|
+
dim (Dim): Dimension object
|
|
11
|
+
index (int): Index of the value to format
|
|
12
|
+
"""
|
|
13
|
+
unit_tuples = [
|
|
14
|
+
('G', 1e9), ('M', 1e6), ('k', 1e3), ('m', 1e-3), ('ยต', 1e-6), ('n', 1e-9)]
|
|
15
|
+
try:
|
|
16
|
+
value = run.full_data_set[dim.name].values[index]
|
|
17
|
+
unit = run.full_data_set[dim.name].unit
|
|
18
|
+
if abs(value) >= 1e3 or (abs(value) < 1e-3 and value != 0):
|
|
19
|
+
for prefix, factor in unit_tuples:
|
|
20
|
+
if abs(value) >= factor or (abs(value) < 1e-3 and value != 0 and factor < 1):
|
|
21
|
+
scaled_value = value / factor
|
|
22
|
+
return f'{scaled_value:.3f} {prefix}<b>{unit}</b>'
|
|
23
|
+
if unit is None or unit == '':
|
|
24
|
+
return f'{value:.3f}'
|
|
25
|
+
else:
|
|
26
|
+
return f'{value:.3f} ({unit})'
|
|
27
|
+
except Exception as e:
|
|
28
|
+
print(f"Error in unit_formatter: {e}")
|
|
29
|
+
return 'N/A'
|