cubievis 0.0.1__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.
- cubievis-0.0.1/.gitignore +7 -0
- cubievis-0.0.1/CHANGELOG.md +59 -0
- cubievis-0.0.1/LICENSE +21 -0
- cubievis-0.0.1/PKG-INFO +74 -0
- cubievis-0.0.1/README.md +52 -0
- cubievis-0.0.1/pyproject.toml +43 -0
- cubievis-0.0.1/src/cubievis/__init__.py +1 -0
- cubievis-0.0.1/src/cubievis/__main__.py +5 -0
- cubievis-0.0.1/src/cubievis/app.py +15 -0
- cubievis-0.0.1/src/cubievis/estimate.py +50 -0
- cubievis-0.0.1/src/cubievis/inputs.py +118 -0
- cubievis-0.0.1/src/cubievis/jobs.py +116 -0
- cubievis-0.0.1/src/cubievis/picks.py +84 -0
- cubievis-0.0.1/src/cubievis/run.py +206 -0
- cubievis-0.0.1/src/cubievis/schema.py +300 -0
- cubievis-0.0.1/src/cubievis/session.py +58 -0
- cubievis-0.0.1/src/cubievis/session_store.py +53 -0
- cubievis-0.0.1/src/cubievis/store.py +361 -0
- cubievis-0.0.1/src/cubievis/systems.py +198 -0
- cubievis-0.0.1/src/cubievis/view_model.py +473 -0
- cubievis-0.0.1/src/cubievis/widgets/__init__.py +1 -0
- cubievis-0.0.1/src/cubievis/widgets/forms.py +187 -0
- cubievis-0.0.1/src/cubievis/widgets/inputs_pane.py +507 -0
- cubievis-0.0.1/src/cubievis/widgets/main_window.py +603 -0
- cubievis-0.0.1/src/cubievis/widgets/run_pane.py +205 -0
- cubievis-0.0.1/src/cubievis/widgets/settings_pane.py +85 -0
- cubievis-0.0.1/src/cubievis/widgets/system_window.py +384 -0
- cubievis-0.0.1/src/cubievis/widgets/viewer_pane.py +743 -0
- cubievis-0.0.1/tests/fixtures/basic_ode.cellml +23 -0
- cubievis-0.0.1/tests/gui/conftest.py +37 -0
- cubievis-0.0.1/tests/gui/test_main_window.py +282 -0
- cubievis-0.0.1/tests/gui/test_panes.py +284 -0
- cubievis-0.0.1/tests/gui/test_viewer_pane.py +294 -0
- cubievis-0.0.1/tests/test_estimate.py +58 -0
- cubievis-0.0.1/tests/test_inputs.py +67 -0
- cubievis-0.0.1/tests/test_jobs.py +53 -0
- cubievis-0.0.1/tests/test_picks.py +80 -0
- cubievis-0.0.1/tests/test_run.py +112 -0
- cubievis-0.0.1/tests/test_schema.py +93 -0
- cubievis-0.0.1/tests/test_session_store.py +49 -0
- cubievis-0.0.1/tests/test_store.py +219 -0
- cubievis-0.0.1/tests/test_systems.py +115 -0
- cubievis-0.0.1/tests/test_view_model.py +203 -0
- cubievis-0.0.1/tests/test_widgets.py +117 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.0.1](https://github.com/cubiepy/cubievis/compare/v0.1.0...v0.0.1) (2026-10-06)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* any dimension on any axis, colour and frame roles, animation and collapsed frames, dataset kinds ([9f4739f](https://github.com/cubiepy/cubievis/commit/9f4739f54b6019be589f1e93d3ea3314fe8051a1))
|
|
9
|
+
* axis limits are two-handled sliders across each axis's NaN-free data range ([01e020f](https://github.com/cubiepy/cubievis/commit/01e020f443d0e4e8e8519c6542ba98b2eb27bd60))
|
|
10
|
+
* axis limits are two-handled sliders across each axis's NaN-free data range ([97403e6](https://github.com/cubiepy/cubievis/commit/97403e6e702bc49a7c333453c205eedfc3a15fb9))
|
|
11
|
+
* batches are saved as a min/max pyramid store and viewed in a fastplotlib pane ([14464b3](https://github.com/cubiepy/cubievis/commit/14464b3b3a3f7361f82bca7aedf594b07d3d73c5))
|
|
12
|
+
* CubieVis GUI with a fastplotlib batch viewer over a min/max pyramid store ([ce940c3](https://github.com/cubiepy/cubievis/commit/ce940c3fb75a577c5419a28d75170fca7a09606d))
|
|
13
|
+
* CubieVis Qt GUI drives cubie from system definition to viewer ([b6e534e](https://github.com/cubiepy/cubievis/commit/b6e534e0d41e0ed09819bca831d5c1dadaaf4e4b))
|
|
14
|
+
* local trace viewer with a self-describing dataset format ([07255fe](https://github.com/cubiepy/cubievis/commit/07255fe9593a2b2fc0fb7a8b8d0ab1be2d36707f))
|
|
15
|
+
* point picking in the viewer feeds y0 ([#7](https://github.com/cubiepy/cubievis/issues/7)) ([ba74295](https://github.com/cubiepy/cubievis/commit/ba74295f44c76dd95b298c106fcafb331df1a2b9))
|
|
16
|
+
* right-hand panel of X, Y, Z and Sweep axes with log/lin, limits and collapse/animate; sliders for unassigned parameters ([684e027](https://github.com/cubiepy/cubievis/commit/684e027ec7a38b6467c20b9971d98b36ad920fed))
|
|
17
|
+
* run log lists each swept input's value count before solving ([eeacfca](https://github.com/cubiepy/cubievis/commit/eeacfcae6d2f69e9a37469bfd2bacff2aa21c154))
|
|
18
|
+
* the viewer plots first and second time derivatives, defaults log-spaced inputs to log axes, opens older dataset.json batches, and starts 3D at an oblique view ([6894ed1](https://github.com/cubiepy/cubievis/commit/6894ed11b1abe0c4166168d93d10f0d1fbb4777f))
|
|
19
|
+
* Y carries axis/collapse/animate modes; Sweep collapses or animates ([bb201c5](https://github.com/cubiepy/cubievis/commit/bb201c5169ec48de6a6904e5a5d671252f4aa6f4))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
### Bug Fixes
|
|
23
|
+
|
|
24
|
+
* an overwrite holds the batch path in the viewer cache until the write finishes ([0f7a455](https://github.com/cubiepy/cubievis/commit/0f7a455ddaa91004e3f5d1626a0b3683d1506b08))
|
|
25
|
+
* arrays save in slabs, overwriting a batch releases the viewer's memory maps, inputs are checked against the system precision, GUI flows are driven by pytest-qt ([881d37e](https://github.com/cubiepy/cubievis/commit/881d37e2bd611cfb1a3c7653b24336c0328e5924))
|
|
26
|
+
* closing the main window closes the viewer pane, so its render canvas stops drawing ([#11](https://github.com/cubiepy/cubievis/issues/11)) ([9b13fed](https://github.com/cubiepy/cubievis/commit/9b13fed3d9024de710c6e80fa5451607192c0ab5))
|
|
27
|
+
* curve coordinates pass to Plotly as typed arrays ([43361eb](https://github.com/cubiepy/cubievis/commit/43361eb0bf6adcf720d3a2b9428417667ca14e9a))
|
|
28
|
+
* favicon requests answer 204 ([13ae904](https://github.com/cubiepy/cubievis/commit/13ae904ee04a1afd8e4d7e7745a70c5e08a972ea))
|
|
29
|
+
* level pairs are stored in time order and span their blocks, and windows too short for a level draw raw samples ([#8](https://github.com/cubiepy/cubievis/issues/8)) ([5da5c52](https://github.com/cubiepy/cubievis/commit/5da5c526adc47e5f5232aac6db62257d2ce29653))
|
|
30
|
+
* returning the viewer to 3D restores orbit controls ([f06f134](https://github.com/cubiepy/cubievis/commit/f06f13489f87361dd1388c58da8e9b2b03b4f87f))
|
|
31
|
+
* store builds append each run chunk to its files in run order instead of writing through writable memmaps ([#9](https://github.com/cubiepy/cubievis/issues/9)) ([04a23a3](https://github.com/cubiepy/cubievis/commit/04a23a37ffe04a89b8f1aba1fafce95d8a9e05ac))
|
|
32
|
+
* the GUI is driveable end to end after the first hands-on review ([2c3e98e](https://github.com/cubiepy/cubievis/commit/2c3e98e9423f57cbd62c86d9d89fd5286b5bf789))
|
|
33
|
+
* the viewer keeps one canvas and switches its camera and controller, so returning to 3D restores orbiting ([83cb243](https://github.com/cubiepy/cubievis/commit/83cb243e00c6ff43e6417fefa6f4a51806e5b46c))
|
|
34
|
+
* the viewer stays responsive on large sweeps ([4ba5a4c](https://github.com/cubiepy/cubievis/commit/4ba5a4cc598284f341fc49ff97ec6d83a8ba8c70))
|
|
35
|
+
* trace requests read only what they return ([29781ca](https://github.com/cubiepy/cubievis/commit/29781cacced03c62722aab10332eea606a0bc2ce))
|
|
36
|
+
* trace requests return full-resolution windows ([bf6adf5](https://github.com/cubiepy/cubievis/commit/bf6adf56d8498f1b71de9cda2aa6ef395bf369f6))
|
|
37
|
+
* traces are POST-only and index files have one layout ([e43f131](https://github.com/cubiepy/cubievis/commit/e43f1315bf2d190afe193173fdfea637d1759463))
|
|
38
|
+
* typed defaults and category moves apply after the build, constants are editable per batch, cubie batches open with the swept input on Y and the first variable on Z ([9a039c5](https://github.com/cubiepy/cubievis/commit/9a039c51ab2394264d3a30f4665c07efd4215359))
|
|
39
|
+
* value cells take one-number expressions and name sweeps typed into them ([3ea6433](https://github.com/cubiepy/cubievis/commit/3ea6433a41b2660e7c0d353b95ec48c0e16af692))
|
|
40
|
+
* viewer page has no header, settings dialog groups advanced rows, run pane takes a directory and batch name, outputs split into saves and summaries ([4e4df52](https://github.com/cubiepy/cubievis/commit/4e4df52307c4753aaed560403e1e6807f9ca838b))
|
|
41
|
+
* viewer pane selects the opened dataset, category tables stretch their columns, number fields are line edits ([0d167e8](https://github.com/cubiepy/cubievis/commit/0d167e81f6d603bd663c4145099c2685bc20022b))
|
|
42
|
+
* viewer server never binds a port in use ([7c5e974](https://github.com/cubiepy/cubievis/commit/7c5e9743ae6288623634e99f73510b445d272710))
|
|
43
|
+
* wide views read the finest pyramid level, ticks follow every re-plan, and surfaces redraw in place ([a082178](https://github.com/cubiepy/cubievis/commit/a082178cb67dac49093f7433119cce74c264e903))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
### Documentation
|
|
47
|
+
|
|
48
|
+
* docstrings and guides state behaviour in one line each ([a141b90](https://github.com/cubiepy/cubievis/commit/a141b900f1da4a2e7e46842a4c75ee0b181d3a33))
|
|
49
|
+
* load_function docstring fits the line limit ([265042c](https://github.com/cubiepy/cubievis/commit/265042ce2816c97be0f2fecb0d15aee9d4383153))
|
|
50
|
+
* README and app header tightened ([43eae6e](https://github.com/cubiepy/cubievis/commit/43eae6ec49c812b82075dbdf5efeccd33547343a))
|
|
51
|
+
* test tiers for CPU and GPU-with-display runs; pytest-qt in the test extra ([eb9c11e](https://github.com/cubiepy/cubievis/commit/eb9c11e18db243db1ecec2532cbd551adc900b1e))
|
|
52
|
+
* worktree venv layering is described ([b93e92f](https://github.com/cubiepy/cubievis/commit/b93e92fa63d9ce0c93e9aa7d54e4f0753090d0be))
|
|
53
|
+
* worktree_setup docstring is one line ([567bd41](https://github.com/cubiepy/cubievis/commit/567bd4153a5cafc8869874e425ff1449314e5274))
|
|
54
|
+
* write_store docstring is one line ([c654edc](https://github.com/cubiepy/cubievis/commit/c654edc863f85c55bd932ff5f26fe78440917d2e))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
### Miscellaneous Chores
|
|
58
|
+
|
|
59
|
+
* first release is 0.0.1 ([0f230d6](https://github.com/cubiepy/cubievis/commit/0f230d65076e696da1967ac38df710b9951f8359))
|
cubievis-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Chris Cameron
|
|
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.
|
cubievis-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: cubievis
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Data visualisation and integrator GUI for Cubie
|
|
5
|
+
Project-URL: Homepage, https://github.com/cubiepy/cubievis
|
|
6
|
+
Project-URL: Issues, https://github.com/cubiepy/cubievis/issues
|
|
7
|
+
Author: Chris Cameron
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Python: >=3.11
|
|
11
|
+
Requires-Dist: attrs
|
|
12
|
+
Requires-Dist: cubie>=0.15.1
|
|
13
|
+
Requires-Dist: fastplotlib>=0.7
|
|
14
|
+
Requires-Dist: numba
|
|
15
|
+
Requires-Dist: numpy>=2.0
|
|
16
|
+
Requires-Dist: pyqt6
|
|
17
|
+
Requires-Dist: qtpy
|
|
18
|
+
Provides-Extra: test
|
|
19
|
+
Requires-Dist: pytest; extra == 'test'
|
|
20
|
+
Requires-Dist: pytest-qt; extra == 'test'
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# CubieVis
|
|
24
|
+
|
|
25
|
+
Qt GUI for [cubie](https://github.com/ccam80/cubie): define a system, pick an integrator, enter a batch, solve, view the traces.
|
|
26
|
+
|
|
27
|
+
## Install and run
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
uv venv; uv pip install -e .[test]
|
|
31
|
+
.venv/Scripts/python -m cubievis
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`cubie[cuda12]`/`cubie[cuda13]` adds a CUDA toolkit when the machine has none.
|
|
35
|
+
|
|
36
|
+
Worktrees share the main checkout's `.venv`: `python tools/worktree_setup.py` (run by orca through `orca.yaml`) layers a `.venv` that imports the worktree's `src`.
|
|
37
|
+
|
|
38
|
+
## Using it
|
|
39
|
+
|
|
40
|
+
1. **Add system…**: equations (`dx = ...`), a Python `f(t, y, p)`, or a `.txt`/`.py`/`.cellml` file; **Parse** fills the category tables (CellML shows its parsed equations); **Edit system…** reopens the current one; **Make constant**/**Make parameter** move selected rows, here and on the main window's tables.
|
|
41
|
+
2. Enter **y0** and **parameters** as numbers, lists or numpy expressions; pick grid type, variables to save/summarise (**All**/**None**), their cadences, and summary metrics (`peaks` takes `n` in its second column).
|
|
42
|
+
3. Pick an **integrator** (type to search) by **Go**; step sizes and tolerances sit beside it, **All settings…** holds the rest; a row passes only when its checkbox is set.
|
|
43
|
+
4. Set timing, directory and results folder; the label by **Go** estimates runs and disk size; the log shows phase, elapsed time, then the result path. **Set y0 from run** copies a run's final state into y0.
|
|
44
|
+
5. The viewer opens the batch; summaries are a second dataset in it. X, Y, Z and Sweep each take time, a swept input, or a variable or its first or second time derivative, with log/lin (log by default for log-spaced inputs) and limits. Y (holding time or an input) cycles axis/collapse/animate; Sweep cycles collapse/animate. **Lines/Surface** switches the drawing; inputs on no axis get sliders; zooming time in 2D reloads that span at finer resolution; hovering names the run. Older `dataset.json` batches are converted to a store when opened.
|
|
45
|
+
6. **File → Save/Load session…** keeps systems, inputs and settings in a JSON file.
|
|
46
|
+
|
|
47
|
+
## Layout
|
|
48
|
+
|
|
49
|
+
| Module | Role |
|
|
50
|
+
|---|---|
|
|
51
|
+
| `schema.py` | Solver settings read from cubie's attrs classes: names, kinds, choices, defaults, applicability per algorithm and controller. |
|
|
52
|
+
| `systems.py` | Build a `SymbolicODE` from a `SystemSpec` (equations, Python source, CellML); category tables. |
|
|
53
|
+
| `inputs.py` | Values text and data files to arrays; `DriverSamples`; final states from a saved batch. |
|
|
54
|
+
| `estimate.py` | Run count and on-disk size estimates shown beside Go. |
|
|
55
|
+
| `session_store.py` | Session save/load as JSON. |
|
|
56
|
+
| `jobs.py` | The one worker thread for cubie calls; phase and elapsed time from `default_timelogger.events`. |
|
|
57
|
+
| `run.py` | Go: `build_grid`, `solve`, write the store. |
|
|
58
|
+
| `store.py` | Batch store: raw (run, time) traces per variable plus min/max pyramids of the values and their first two time derivatives; windowed line queries; `dataset.json` import. |
|
|
59
|
+
| `view_model.py` | Axis choices to lines, frames, colours, world coordinates and ticks. |
|
|
60
|
+
| `session.py` | Built systems, the current solver, the worker. |
|
|
61
|
+
| `widgets/` | One Qt module per pane; no cubie calls outside the worker. The viewer pane draws with fastplotlib. |
|
|
62
|
+
|
|
63
|
+
## Tests
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
NUMBA_ENABLE_CUDASIM=1 .venv/Scripts/python -m pytest --ignore=tests/gui/test_main_window.py --ignore=tests/gui/test_viewer_pane.py
|
|
67
|
+
.venv/Scripts/python -m pytest tests/gui/test_main_window.py tests/gui/test_viewer_pane.py tests/test_run.py # GPU, needs a display
|
|
68
|
+
.venv/Scripts/python bench/speed_check.py store|render <batch> # store build and raw render timings
|
|
69
|
+
.venv/Scripts/python bench/viewer_check.py <batch> <store> # each viewer mode
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## AI slop warning
|
|
73
|
+
|
|
74
|
+
Any AI input into Cubie is reviewed line-by-line, so it is largely slop-free and the poor design choices are all human-made. This GUI did not get the same degree of human care - if it steers cubie, is usable, and looks ok, then it is clean enough. The code is therefore an AI wasteland - it will get cleaned up when it starts to fight development, not before, so read it at your peril.
|
cubievis-0.0.1/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# CubieVis
|
|
2
|
+
|
|
3
|
+
Qt GUI for [cubie](https://github.com/ccam80/cubie): define a system, pick an integrator, enter a batch, solve, view the traces.
|
|
4
|
+
|
|
5
|
+
## Install and run
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
uv venv; uv pip install -e .[test]
|
|
9
|
+
.venv/Scripts/python -m cubievis
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
`cubie[cuda12]`/`cubie[cuda13]` adds a CUDA toolkit when the machine has none.
|
|
13
|
+
|
|
14
|
+
Worktrees share the main checkout's `.venv`: `python tools/worktree_setup.py` (run by orca through `orca.yaml`) layers a `.venv` that imports the worktree's `src`.
|
|
15
|
+
|
|
16
|
+
## Using it
|
|
17
|
+
|
|
18
|
+
1. **Add system…**: equations (`dx = ...`), a Python `f(t, y, p)`, or a `.txt`/`.py`/`.cellml` file; **Parse** fills the category tables (CellML shows its parsed equations); **Edit system…** reopens the current one; **Make constant**/**Make parameter** move selected rows, here and on the main window's tables.
|
|
19
|
+
2. Enter **y0** and **parameters** as numbers, lists or numpy expressions; pick grid type, variables to save/summarise (**All**/**None**), their cadences, and summary metrics (`peaks` takes `n` in its second column).
|
|
20
|
+
3. Pick an **integrator** (type to search) by **Go**; step sizes and tolerances sit beside it, **All settings…** holds the rest; a row passes only when its checkbox is set.
|
|
21
|
+
4. Set timing, directory and results folder; the label by **Go** estimates runs and disk size; the log shows phase, elapsed time, then the result path. **Set y0 from run** copies a run's final state into y0.
|
|
22
|
+
5. The viewer opens the batch; summaries are a second dataset in it. X, Y, Z and Sweep each take time, a swept input, or a variable or its first or second time derivative, with log/lin (log by default for log-spaced inputs) and limits. Y (holding time or an input) cycles axis/collapse/animate; Sweep cycles collapse/animate. **Lines/Surface** switches the drawing; inputs on no axis get sliders; zooming time in 2D reloads that span at finer resolution; hovering names the run. Older `dataset.json` batches are converted to a store when opened.
|
|
23
|
+
6. **File → Save/Load session…** keeps systems, inputs and settings in a JSON file.
|
|
24
|
+
|
|
25
|
+
## Layout
|
|
26
|
+
|
|
27
|
+
| Module | Role |
|
|
28
|
+
|---|---|
|
|
29
|
+
| `schema.py` | Solver settings read from cubie's attrs classes: names, kinds, choices, defaults, applicability per algorithm and controller. |
|
|
30
|
+
| `systems.py` | Build a `SymbolicODE` from a `SystemSpec` (equations, Python source, CellML); category tables. |
|
|
31
|
+
| `inputs.py` | Values text and data files to arrays; `DriverSamples`; final states from a saved batch. |
|
|
32
|
+
| `estimate.py` | Run count and on-disk size estimates shown beside Go. |
|
|
33
|
+
| `session_store.py` | Session save/load as JSON. |
|
|
34
|
+
| `jobs.py` | The one worker thread for cubie calls; phase and elapsed time from `default_timelogger.events`. |
|
|
35
|
+
| `run.py` | Go: `build_grid`, `solve`, write the store. |
|
|
36
|
+
| `store.py` | Batch store: raw (run, time) traces per variable plus min/max pyramids of the values and their first two time derivatives; windowed line queries; `dataset.json` import. |
|
|
37
|
+
| `view_model.py` | Axis choices to lines, frames, colours, world coordinates and ticks. |
|
|
38
|
+
| `session.py` | Built systems, the current solver, the worker. |
|
|
39
|
+
| `widgets/` | One Qt module per pane; no cubie calls outside the worker. The viewer pane draws with fastplotlib. |
|
|
40
|
+
|
|
41
|
+
## Tests
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
NUMBA_ENABLE_CUDASIM=1 .venv/Scripts/python -m pytest --ignore=tests/gui/test_main_window.py --ignore=tests/gui/test_viewer_pane.py
|
|
45
|
+
.venv/Scripts/python -m pytest tests/gui/test_main_window.py tests/gui/test_viewer_pane.py tests/test_run.py # GPU, needs a display
|
|
46
|
+
.venv/Scripts/python bench/speed_check.py store|render <batch> # store build and raw render timings
|
|
47
|
+
.venv/Scripts/python bench/viewer_check.py <batch> <store> # each viewer mode
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## AI slop warning
|
|
51
|
+
|
|
52
|
+
Any AI input into Cubie is reviewed line-by-line, so it is largely slop-free and the poor design choices are all human-made. This GUI did not get the same degree of human care - if it steers cubie, is usable, and looks ok, then it is clean enough. The code is therefore an AI wasteland - it will get cleaned up when it starts to fight development, not before, so read it at your peril.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "cubievis"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
description = "Data visualisation and integrator GUI for Cubie"
|
|
9
|
+
authors = [
|
|
10
|
+
{ name="Chris Cameron" }
|
|
11
|
+
]
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
license = "MIT"
|
|
14
|
+
license-files = ["LICENSE"]
|
|
15
|
+
requires-python = ">=3.11"
|
|
16
|
+
dependencies = [
|
|
17
|
+
"numpy>=2.0",
|
|
18
|
+
"numba",
|
|
19
|
+
"attrs",
|
|
20
|
+
"cubie>=0.15.1",
|
|
21
|
+
"qtpy",
|
|
22
|
+
"PyQt6",
|
|
23
|
+
"fastplotlib>=0.7",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://github.com/cubiepy/cubievis"
|
|
28
|
+
Issues = "https://github.com/cubiepy/cubievis/issues"
|
|
29
|
+
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
test = ["pytest", "pytest-qt"]
|
|
32
|
+
|
|
33
|
+
[project.scripts]
|
|
34
|
+
cubievis = "cubievis.app:main"
|
|
35
|
+
|
|
36
|
+
[tool.hatch.build.targets.sdist]
|
|
37
|
+
exclude = [".claude", ".github", "bench", "tools", "orca.yaml", "AGENTS.md", "release-please-config.json", ".release-please-manifest.json"]
|
|
38
|
+
|
|
39
|
+
[tool.hatch.build.targets.wheel]
|
|
40
|
+
packages = ["src/cubievis"]
|
|
41
|
+
|
|
42
|
+
[tool.pytest.ini_options]
|
|
43
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CubieVis: a data visualisation and integrator GUI for Cubie."""
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Entry point: show the main window."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def main() -> int:
|
|
7
|
+
from qtpy.QtWidgets import QApplication
|
|
8
|
+
|
|
9
|
+
from cubievis.session import Session
|
|
10
|
+
from cubievis.widgets.main_window import MainWindow
|
|
11
|
+
|
|
12
|
+
app = QApplication(sys.argv)
|
|
13
|
+
window = MainWindow(Session())
|
|
14
|
+
window.show()
|
|
15
|
+
return app.exec()
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Pre-run estimates: how many runs a batch makes and its size on disk."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from cubievis.schema import TIME_DOMAIN_OUTPUTS, metric_output_size
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def run_count(inputs: dict, grid_type: str) -> int:
|
|
9
|
+
"""Runs the grid makes from ``{name: values}`` batch inputs."""
|
|
10
|
+
sizes = [np.asarray(values).size for values in inputs.values()]
|
|
11
|
+
if not sizes:
|
|
12
|
+
return 1
|
|
13
|
+
if grid_type == "verbatim":
|
|
14
|
+
return max(sizes)
|
|
15
|
+
count = 1
|
|
16
|
+
for size in sizes:
|
|
17
|
+
if size > 1:
|
|
18
|
+
count *= size
|
|
19
|
+
return count
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def batch_bytes(*, n_runs: int, duration: float, itemsize: int,
|
|
23
|
+
output_types: list, n_saved: int, n_summarised: int,
|
|
24
|
+
save_every: float = None, summarise_every: float = None,
|
|
25
|
+
dt: float = 0.01) -> int:
|
|
26
|
+
"""Approximate bytes on disk; metric output types may carry ``[n]``."""
|
|
27
|
+
total = 0
|
|
28
|
+
save_every = save_every or dt or 0.01
|
|
29
|
+
traces = any(t in ("state", "observables") for t in output_types)
|
|
30
|
+
if traces and n_saved and duration > 0:
|
|
31
|
+
n_time = int(duration / save_every) + 1
|
|
32
|
+
total += n_time * n_saved * n_runs * itemsize
|
|
33
|
+
total += n_time * 8 # the shared time column
|
|
34
|
+
metrics = [t for t in output_types
|
|
35
|
+
if t.partition("[")[0] not in TIME_DOMAIN_OUTPUTS]
|
|
36
|
+
if metrics and n_summarised and duration > 0:
|
|
37
|
+
height = sum(metric_output_size(m) for m in metrics) * n_summarised
|
|
38
|
+
windows = int(duration / summarise_every) if summarise_every else 1
|
|
39
|
+
total += max(windows, 1) * height * n_runs * itemsize
|
|
40
|
+
return total
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def human_size(n_bytes: int) -> str:
|
|
44
|
+
"""``n_bytes`` as a short human-readable size."""
|
|
45
|
+
size = float(n_bytes)
|
|
46
|
+
for unit in ("B", "kB", "MB", "GB"):
|
|
47
|
+
if size < 1000:
|
|
48
|
+
return f"{size:.0f} {unit}" if unit == "B" else f"{size:.1f} {unit}"
|
|
49
|
+
size /= 1000
|
|
50
|
+
return f"{size:.1f} TB"
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Turn typed values and data files into solver inputs."""
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from cubie import DriverSamples
|
|
9
|
+
|
|
10
|
+
from cubievis.store import Store
|
|
11
|
+
|
|
12
|
+
NAMESPACE = {
|
|
13
|
+
"np": np, "numpy": np, "pi": math.pi, "e": math.e,
|
|
14
|
+
"linspace": np.linspace, "logspace": np.logspace,
|
|
15
|
+
"geomspace": np.geomspace, "arange": np.arange, "array": np.array,
|
|
16
|
+
"zeros": np.zeros, "ones": np.ones, "full": np.full, "range": range,
|
|
17
|
+
"sin": np.sin, "cos": np.cos, "exp": np.exp, "log": np.log,
|
|
18
|
+
"sqrt": np.sqrt, "abs": np.abs,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def parse_values(text: str) -> np.ndarray:
|
|
23
|
+
"""A 1-D float array from a number, a list or a numpy expression."""
|
|
24
|
+
text = text.strip()
|
|
25
|
+
if not text:
|
|
26
|
+
raise ValueError("No values given")
|
|
27
|
+
try:
|
|
28
|
+
value = eval(text, {"__builtins__": {}}, NAMESPACE)
|
|
29
|
+
except SyntaxError:
|
|
30
|
+
value = eval(f"[{text}]", {"__builtins__": {}}, NAMESPACE)
|
|
31
|
+
array = np.asarray(value, dtype=np.float64).ravel()
|
|
32
|
+
if array.size == 0:
|
|
33
|
+
raise ValueError("No values given")
|
|
34
|
+
return array
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def load_array(path) -> np.ndarray:
|
|
38
|
+
"""Load ``.npy`` or delimited text as a float array."""
|
|
39
|
+
path = Path(path)
|
|
40
|
+
if path.suffix.lower() == ".npy":
|
|
41
|
+
return np.load(path)
|
|
42
|
+
text = path.read_text("utf-8")
|
|
43
|
+
delimiter = "," if "," in text.splitlines()[0] else None
|
|
44
|
+
return np.loadtxt(path, delimiter=delimiter, ndmin=1)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def values_from(text: str) -> np.ndarray:
|
|
48
|
+
"""``parse_values`` unless ``text`` names an existing file."""
|
|
49
|
+
text = text.strip()
|
|
50
|
+
if text and Path(text).is_file():
|
|
51
|
+
return load_array(text).ravel()
|
|
52
|
+
return parse_values(text)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def check_precision(inputs: dict, precision) -> tuple:
|
|
56
|
+
"""``(errors, warnings)`` for values that overflow or merge in ``precision``."""
|
|
57
|
+
errors, warnings = [], []
|
|
58
|
+
name = np.dtype(precision).name
|
|
59
|
+
with np.errstate(over="ignore"):
|
|
60
|
+
for label, values in inputs.items():
|
|
61
|
+
cast = np.asarray(values, dtype=precision)
|
|
62
|
+
bad = int(np.count_nonzero(~np.isfinite(cast)))
|
|
63
|
+
if bad:
|
|
64
|
+
errors.append(f"{label}: {bad} of {cast.size} values "
|
|
65
|
+
f"overflow {name}")
|
|
66
|
+
continue
|
|
67
|
+
distinct = np.unique(cast).size
|
|
68
|
+
if distinct < cast.size:
|
|
69
|
+
warnings.append(f"{label}: {cast.size} values are "
|
|
70
|
+
f"{distinct} distinct in {name}")
|
|
71
|
+
return errors, warnings
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def input_dict(rows) -> dict:
|
|
75
|
+
"""``{name: values}`` from ``(name, text)`` rows, skipping blanks."""
|
|
76
|
+
out = {}
|
|
77
|
+
for name, text in rows:
|
|
78
|
+
if text.strip():
|
|
79
|
+
out[name] = values_from(text)
|
|
80
|
+
return out
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def single_value(text: str, name: str = "") -> float:
|
|
84
|
+
"""One float from a number or expression; sweeps are refused by name."""
|
|
85
|
+
try:
|
|
86
|
+
array = parse_values(text)
|
|
87
|
+
except ValueError as error:
|
|
88
|
+
raise ValueError(f"{name}: {error}" if name else str(error))
|
|
89
|
+
if array.size != 1:
|
|
90
|
+
raise ValueError(
|
|
91
|
+
f"{name or text}: {array.size} values given but this field "
|
|
92
|
+
"takes one number; sweeps go in a values column"
|
|
93
|
+
)
|
|
94
|
+
return float(array[0])
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def final_states(path, run: int) -> dict:
|
|
98
|
+
"""``{saved variable: value}`` at the last sample of ``run``."""
|
|
99
|
+
return Store(path).final_values(run)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def make_driver_samples(columns: dict, time_text: str = "",
|
|
103
|
+
t0: float = 0.0, period: float = 0.0):
|
|
104
|
+
"""A ``DriverSamples`` from per-driver texts and a time base.
|
|
105
|
+
|
|
106
|
+
Parameters
|
|
107
|
+
----------
|
|
108
|
+
columns
|
|
109
|
+
Driver name to value text or data file path.
|
|
110
|
+
time_text
|
|
111
|
+
Sample times; empty uses ``t0`` and ``period``.
|
|
112
|
+
"""
|
|
113
|
+
samples = {name: values_from(text) for name, text in columns.items()}
|
|
114
|
+
if not samples:
|
|
115
|
+
return None
|
|
116
|
+
if time_text.strip():
|
|
117
|
+
return DriverSamples(samples, time=values_from(time_text))
|
|
118
|
+
return DriverSamples(samples, driver_sample_period=period, t0=t0)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""One worker thread runs every cubie call; the GUI polls its jobs."""
|
|
2
|
+
|
|
3
|
+
import queue
|
|
4
|
+
import threading
|
|
5
|
+
import time
|
|
6
|
+
import traceback
|
|
7
|
+
import warnings
|
|
8
|
+
|
|
9
|
+
import attrs
|
|
10
|
+
|
|
11
|
+
from cubie import default_timelogger
|
|
12
|
+
|
|
13
|
+
# Event-name prefix to the phase label shown while it is running.
|
|
14
|
+
PHASES = (
|
|
15
|
+
("symbolic_ode_parsing", "Parsing"),
|
|
16
|
+
("codegen", "Generating code"),
|
|
17
|
+
("compile", "Compiling"),
|
|
18
|
+
("solver_solve", "Solving"),
|
|
19
|
+
("solve_ivp", "Solving"),
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@attrs.define
|
|
24
|
+
class Job:
|
|
25
|
+
"""A queued call and everything it produced."""
|
|
26
|
+
|
|
27
|
+
fn: object
|
|
28
|
+
args: tuple = ()
|
|
29
|
+
kwargs: dict = attrs.field(factory=dict)
|
|
30
|
+
label: str = ""
|
|
31
|
+
state: str = "queued"
|
|
32
|
+
result: object = None
|
|
33
|
+
error: str = ""
|
|
34
|
+
warnings: list = attrs.field(factory=list)
|
|
35
|
+
started: float = 0.0
|
|
36
|
+
finished: float = 0.0
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def done(self) -> bool:
|
|
40
|
+
return self.state in ("done", "failed")
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def elapsed(self) -> float:
|
|
44
|
+
end = self.finished or time.perf_counter()
|
|
45
|
+
return end - self.started if self.started else 0.0
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def phase_label(name: str) -> str:
|
|
49
|
+
"""Phase label for a timelogger event name, or the name itself."""
|
|
50
|
+
for prefix, label in PHASES:
|
|
51
|
+
if name.startswith(prefix):
|
|
52
|
+
return label
|
|
53
|
+
return name
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def current_phase(logger=default_timelogger):
|
|
57
|
+
"""``(label, start_timestamp)`` of the innermost open event, or None."""
|
|
58
|
+
stopped = set()
|
|
59
|
+
for event in reversed(logger.events):
|
|
60
|
+
if event.event_type == "stop":
|
|
61
|
+
stopped.add(event.name)
|
|
62
|
+
elif event.event_type == "start" and event.name not in stopped:
|
|
63
|
+
return phase_label(event.name), event.timestamp
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def status_line(job: Job) -> str:
|
|
68
|
+
"""``"Compiling (3s)"``-style line for the job's current phase."""
|
|
69
|
+
if job.state == "queued":
|
|
70
|
+
return f"{job.label} queued"
|
|
71
|
+
if job.state == "failed":
|
|
72
|
+
return f"{job.label} failed ({job.elapsed:.0f}s)"
|
|
73
|
+
if job.state == "done":
|
|
74
|
+
return f"{job.label} done ({job.elapsed:.0f}s)"
|
|
75
|
+
phase = current_phase()
|
|
76
|
+
if phase is None:
|
|
77
|
+
return f"{job.label} ({job.elapsed:.0f}s)"
|
|
78
|
+
label, started = phase
|
|
79
|
+
return f"{label} ({time.perf_counter() - started:.0f}s)"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class CubieWorker(threading.Thread):
|
|
83
|
+
"""Daemon thread executing jobs one at a time, in submission order."""
|
|
84
|
+
|
|
85
|
+
def __init__(self):
|
|
86
|
+
super().__init__(daemon=True, name="cubie-worker")
|
|
87
|
+
self._queue = queue.Queue()
|
|
88
|
+
self.current = None
|
|
89
|
+
default_timelogger.set_verbosity("silent")
|
|
90
|
+
|
|
91
|
+
def submit(self, fn, *args, label="", **kwargs) -> Job:
|
|
92
|
+
"""Queue ``fn(*args, **kwargs)`` and return its job."""
|
|
93
|
+
job = Job(fn=fn, args=args, kwargs=kwargs, label=label or fn.__name__)
|
|
94
|
+
self._queue.put(job)
|
|
95
|
+
return job
|
|
96
|
+
|
|
97
|
+
def run(self):
|
|
98
|
+
while True:
|
|
99
|
+
job = self._queue.get()
|
|
100
|
+
self.current = job
|
|
101
|
+
job.state = "running"
|
|
102
|
+
job.started = time.perf_counter()
|
|
103
|
+
with warnings.catch_warnings(record=True) as caught:
|
|
104
|
+
warnings.simplefilter("always")
|
|
105
|
+
try:
|
|
106
|
+
job.result = job.fn(*job.args, **job.kwargs)
|
|
107
|
+
job.state = "done"
|
|
108
|
+
except Exception:
|
|
109
|
+
job.error = traceback.format_exc()
|
|
110
|
+
job.state = "failed"
|
|
111
|
+
job.warnings = list(dict.fromkeys(
|
|
112
|
+
str(item.message) for item in caught
|
|
113
|
+
if not issubclass(item.category, DeprecationWarning)
|
|
114
|
+
))
|
|
115
|
+
job.finished = time.perf_counter()
|
|
116
|
+
self.current = None
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Picked points: the run and sample a click hit, where it sits in a frame, its values."""
|
|
2
|
+
|
|
3
|
+
import attrs
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
from cubievis.view_model import TIME, kind, label, ref_name, ref_order
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@attrs.frozen
|
|
10
|
+
class Pick:
|
|
11
|
+
"""One data point: ``run`` at sample ``index`` of the store's time."""
|
|
12
|
+
|
|
13
|
+
run: int
|
|
14
|
+
index: int
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def nearest_index(times, t) -> int:
|
|
18
|
+
"""Index of the value in sorted ``times`` closest to ``t``."""
|
|
19
|
+
i = int(np.searchsorted(times, t))
|
|
20
|
+
if i <= 0:
|
|
21
|
+
return 0
|
|
22
|
+
if i >= len(times):
|
|
23
|
+
return len(times) - 1
|
|
24
|
+
return i if times[i] - t < t - times[i - 1] else i - 1
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def pick_at(store, plan, geo, frame: int, line: int, point: int) -> Pick:
|
|
28
|
+
"""The data point drawn at vertex ``point`` of ``line`` in ``frame``."""
|
|
29
|
+
runs = geo.runs[frame][line]
|
|
30
|
+
if plan.along == TIME:
|
|
31
|
+
return Pick(int(runs[0]), nearest_index(store.time, geo.times[point]))
|
|
32
|
+
return Pick(int(runs[min(point, len(runs) - 1)]),
|
|
33
|
+
geo.time_indices[frame])
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def locate(store, plan, geo, frame: int, pick: Pick):
|
|
37
|
+
"""``(line, point)`` of ``pick`` in ``frame``, or ``None`` when it is not drawn there."""
|
|
38
|
+
line = next((i for i, runs in enumerate(geo.runs[frame])
|
|
39
|
+
if pick.run in runs), None)
|
|
40
|
+
if line is None:
|
|
41
|
+
return None
|
|
42
|
+
if plan.along == TIME:
|
|
43
|
+
return line, nearest_index(geo.times, store.time[pick.index])
|
|
44
|
+
if geo.time_indices[frame] != pick.index:
|
|
45
|
+
return None
|
|
46
|
+
runs = list(geo.runs[frame][line])
|
|
47
|
+
return line, runs.index(pick.run)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def values_at(store, plan, pick: Pick) -> dict:
|
|
51
|
+
"""``{role: data value}`` of ``pick`` on each plotted axis."""
|
|
52
|
+
out = {}
|
|
53
|
+
for role, ref in plan.roles.items():
|
|
54
|
+
if ref == TIME:
|
|
55
|
+
out[role] = float(store.time[pick.index])
|
|
56
|
+
elif kind(ref) == "var":
|
|
57
|
+
out[role] = float(store.at(ref_name(ref), [pick.run], pick.index,
|
|
58
|
+
ref_order(ref))[0])
|
|
59
|
+
else:
|
|
60
|
+
out[role] = float(store.inputs[ref_name(ref)][pick.run])
|
|
61
|
+
return out
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def world_point(geo, values: dict) -> np.ndarray:
|
|
65
|
+
"""World ``(x, y, z)`` of ``values``; z is 0 on a 2D plot."""
|
|
66
|
+
point = np.zeros(3, np.float32)
|
|
67
|
+
for axis, role in enumerate(("x", "y", "z")):
|
|
68
|
+
if role in values:
|
|
69
|
+
point[axis] = geo.transforms[role].world(values[role])
|
|
70
|
+
return point
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def flag_text(plan, pick: Pick, values: dict) -> str:
|
|
74
|
+
"""The run and each axis value, one per line."""
|
|
75
|
+
lines = [f"run {pick.run}"]
|
|
76
|
+
lines += [f"{label(ref)} {values[role]:.4g}"
|
|
77
|
+
for role, ref in plan.roles.items()]
|
|
78
|
+
return "\n".join(lines)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def state_at(store, pick: Pick) -> dict:
|
|
82
|
+
"""``{saved variable: value}`` of ``pick``."""
|
|
83
|
+
return {name: float(store.raw(name)[pick.run, pick.index])
|
|
84
|
+
for name in store.variables}
|