bernardyn 0.0.1b1__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.
- bernardyn-0.0.1b1/LICENSE +21 -0
- bernardyn-0.0.1b1/PKG-INFO +129 -0
- bernardyn-0.0.1b1/README.md +90 -0
- bernardyn-0.0.1b1/bernardyn/__init__.py +23 -0
- bernardyn-0.0.1b1/bernardyn/app.py +31 -0
- bernardyn-0.0.1b1/bernardyn/core/__init__.py +6 -0
- bernardyn-0.0.1b1/bernardyn/core/controller.py +319 -0
- bernardyn-0.0.1b1/bernardyn/core/models.py +436 -0
- bernardyn-0.0.1b1/bernardyn/core/registry.py +57 -0
- bernardyn-0.0.1b1/bernardyn/core/transforms.py +309 -0
- bernardyn-0.0.1b1/bernardyn/data/__init__.py +5 -0
- bernardyn-0.0.1b1/bernardyn/diagnostics.py +96 -0
- bernardyn-0.0.1b1/bernardyn/export/__init__.py +6 -0
- bernardyn-0.0.1b1/bernardyn/gui/__init__.py +5 -0
- bernardyn-0.0.1b1/bernardyn/gui/dialogs.py +620 -0
- bernardyn-0.0.1b1/bernardyn/gui/graph_page.py +126 -0
- bernardyn-0.0.1b1/bernardyn/gui/inspector.py +921 -0
- bernardyn-0.0.1b1/bernardyn/gui/main_window.py +917 -0
- bernardyn-0.0.1b1/bernardyn/io/__init__.py +10 -0
- bernardyn-0.0.1b1/bernardyn/io/container.py +600 -0
- bernardyn-0.0.1b1/bernardyn/io/curve_export.py +83 -0
- bernardyn-0.0.1b1/bernardyn/io/file_browser.py +133 -0
- bernardyn-0.0.1b1/bernardyn/io/igor.py +50 -0
- bernardyn-0.0.1b1/bernardyn/io/sources.py +636 -0
- bernardyn-0.0.1b1/bernardyn/main.py +8 -0
- bernardyn-0.0.1b1/bernardyn/plot/__init__.py +5 -0
- bernardyn-0.0.1b1/bernardyn/py.typed +0 -0
- bernardyn-0.0.1b1/bernardyn/renderers/__init__.py +20 -0
- bernardyn-0.0.1b1/bernardyn/renderers/opengl.py +267 -0
- bernardyn-0.0.1b1/bernardyn/renderers/plot2d.py +618 -0
- bernardyn-0.0.1b1/bernardyn/renderers/registry.py +58 -0
- bernardyn-0.0.1b1/bernardyn/schemas/graph-package-v1.md +111 -0
- bernardyn-0.0.1b1/bernardyn/state.py +44 -0
- bernardyn-0.0.1b1/bernardyn/template/__init__.py +5 -0
- bernardyn-0.0.1b1/bernardyn/template/graph_templates.py +118 -0
- bernardyn-0.0.1b1/bernardyn/utils/__init__.py +1 -0
- bernardyn-0.0.1b1/bernardyn.egg-info/PKG-INFO +129 -0
- bernardyn-0.0.1b1/bernardyn.egg-info/SOURCES.txt +50 -0
- bernardyn-0.0.1b1/bernardyn.egg-info/dependency_links.txt +1 -0
- bernardyn-0.0.1b1/bernardyn.egg-info/entry_points.txt +5 -0
- bernardyn-0.0.1b1/bernardyn.egg-info/requires.txt +14 -0
- bernardyn-0.0.1b1/bernardyn.egg-info/top_level.txt +1 -0
- bernardyn-0.0.1b1/pyproject.toml +71 -0
- bernardyn-0.0.1b1/setup.cfg +4 -0
- bernardyn-0.0.1b1/tests/test_container.py +228 -0
- bernardyn-0.0.1b1/tests/test_gui.py +401 -0
- bernardyn-0.0.1b1/tests/test_models_and_transforms.py +202 -0
- bernardyn-0.0.1b1/tests/test_performance.py +85 -0
- bernardyn-0.0.1b1/tests/test_real_fixtures.py +27 -0
- bernardyn-0.0.1b1/tests/test_sources.py +111 -0
- bernardyn-0.0.1b1/tests/test_state.py +12 -0
- bernardyn-0.0.1b1/tests/test_templates.py +49 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jan Ilavsky and Bernardyn contributors
|
|
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,129 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bernardyn
|
|
3
|
+
Version: 0.0.1b1
|
|
4
|
+
Summary: Publication-oriented plotting workbench for small-angle scattering data
|
|
5
|
+
Author: Jan Ilavsky
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/jilavsky/Bernardyn
|
|
8
|
+
Project-URL: Repository, https://github.com/jilavsky/Bernardyn
|
|
9
|
+
Project-URL: Issues, https://github.com/jilavsky/Bernardyn/issues
|
|
10
|
+
Keywords: SAXS,SANS,USAXS,WAXS,scattering,visualization
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Operating System :: MacOS
|
|
14
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
15
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
22
|
+
Requires-Python: <3.14,>=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: numpy>=2.0
|
|
26
|
+
Requires-Dist: h5py>=3.0
|
|
27
|
+
Requires-Dist: PyOpenGL>=3.1.7
|
|
28
|
+
Requires-Dist: pyirena[qtplot]<2,>=1.1.0b12
|
|
29
|
+
Requires-Dist: pyqtgraph>=0.13.7
|
|
30
|
+
Requires-Dist: PySide6!=6.9.1,>=6.8
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
33
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
34
|
+
Requires-Dist: pytest-qt>=4.2; extra == "dev"
|
|
35
|
+
Requires-Dist: ruff; extra == "dev"
|
|
36
|
+
Requires-Dist: build; extra == "dev"
|
|
37
|
+
Requires-Dist: twine; extra == "dev"
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
|
|
40
|
+
# Bernardyn
|
|
41
|
+
|
|
42
|
+
Bernardyn is a PySide6/PyQtGraph workbench for publication-oriented plotting of
|
|
43
|
+
small-angle scattering and diffraction curves. It keeps scientific arrays out
|
|
44
|
+
of widgets and stores complete, source-independent graphs in portable HDF5
|
|
45
|
+
packages.
|
|
46
|
+
|
|
47
|
+
## Current capabilities
|
|
48
|
+
|
|
49
|
+
- Browse all discoverable NXcanSAS/simple-HDF5 entries and 2–4 column text data.
|
|
50
|
+
- Keep a workspace-wide dataset catalog and independent graph tabs.
|
|
51
|
+
- Plot General I(Q), Guinier, rod/sheet Guinier, Kratky, dimensionless Kratky,
|
|
52
|
+
Porod, modified Porod, Zimm, and Debye–Bueche views.
|
|
53
|
+
- Style each curve, errors, labels, axes, legends, typography, and annotations.
|
|
54
|
+
- Render 3D line waterfalls and common-grid surfaces with an isolated,
|
|
55
|
+
lazily-loaded PyQtGraph OpenGL backend and 2D fallback.
|
|
56
|
+
- Save one graph or a deduplicated multi-graph workspace as
|
|
57
|
+
`*.bernardyn.h5`, including canonical data, resolved snapshots, configuration,
|
|
58
|
+
checksums, previews, and 3D renderer data.
|
|
59
|
+
- Export PNG/JPEG/SVG, CSV, Igor ITX, and canonical data through PyIrena's H5XP
|
|
60
|
+
writer. H5XP export is data interoperability, not Bernardyn graph persistence.
|
|
61
|
+
|
|
62
|
+
The native schema is documented in
|
|
63
|
+
[`bernardyn/schemas/graph-package-v1.md`](bernardyn/schemas/graph-package-v1.md).
|
|
64
|
+
User-facing workflow and file-format documentation is in
|
|
65
|
+
[`docs/`](docs/README.md).
|
|
66
|
+
|
|
67
|
+
## Install and run
|
|
68
|
+
|
|
69
|
+
Bernardyn targets Python 3.10–3.13 and shares its Qt plotting dependencies with
|
|
70
|
+
PyIrena. For development, keep the `Bernardyn` and `pyirena` repositories next
|
|
71
|
+
to each other and create the pinned Conda environment from the Bernardyn root:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
conda env create -f environment.yml
|
|
75
|
+
conda activate bernardyn
|
|
76
|
+
bernardyn-doctor
|
|
77
|
+
bernardyn
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The environment file installs both repositories in editable mode. If an older
|
|
81
|
+
`bernardyn` environment already exists with Python 3.14, remove and recreate it:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
conda deactivate
|
|
85
|
+
conda env remove -n bernardyn
|
|
86
|
+
conda env create -f environment.yml
|
|
87
|
+
conda activate bernardyn
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
For an already compatible Python 3.10–3.13 environment, installation can also
|
|
91
|
+
be performed manually:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
python -m pip install -e "../pyirena[qtplot]"
|
|
95
|
+
python -m pip install -e ".[dev]"
|
|
96
|
+
bernardyn
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
To diagnose an installation:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
bernardyn-doctor
|
|
103
|
+
bernardyn-doctor --json
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The OpenGL renderer needs a usable graphics context. If it cannot initialize,
|
|
107
|
+
Bernardyn displays a 2D offset-waterfall fallback and retains the 3D document
|
|
108
|
+
configuration.
|
|
109
|
+
|
|
110
|
+
## Development
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
pytest
|
|
114
|
+
ruff check bernardyn tests
|
|
115
|
+
python -m build
|
|
116
|
+
twine check dist/*
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Extension entry points are `bernardyn.sources`, `bernardyn.transforms`, and
|
|
120
|
+
`bernardyn.renderers`; see [`bernardyn/EXTENDING.md`](bernardyn/EXTENDING.md).
|
|
121
|
+
|
|
122
|
+
## Scope
|
|
123
|
+
|
|
124
|
+
Version 1.0 is a plotting and presentation application. Model fitting,
|
|
125
|
+
reduction, destructive source modification, movies, and calibrated 2D detector
|
|
126
|
+
workflows are intentionally outside 1.0. Existing experimental 0.1 containers
|
|
127
|
+
are not treated as compatible graph packages.
|
|
128
|
+
|
|
129
|
+
Bernardyn is distributed under the MIT License.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Bernardyn
|
|
2
|
+
|
|
3
|
+
Bernardyn is a PySide6/PyQtGraph workbench for publication-oriented plotting of
|
|
4
|
+
small-angle scattering and diffraction curves. It keeps scientific arrays out
|
|
5
|
+
of widgets and stores complete, source-independent graphs in portable HDF5
|
|
6
|
+
packages.
|
|
7
|
+
|
|
8
|
+
## Current capabilities
|
|
9
|
+
|
|
10
|
+
- Browse all discoverable NXcanSAS/simple-HDF5 entries and 2–4 column text data.
|
|
11
|
+
- Keep a workspace-wide dataset catalog and independent graph tabs.
|
|
12
|
+
- Plot General I(Q), Guinier, rod/sheet Guinier, Kratky, dimensionless Kratky,
|
|
13
|
+
Porod, modified Porod, Zimm, and Debye–Bueche views.
|
|
14
|
+
- Style each curve, errors, labels, axes, legends, typography, and annotations.
|
|
15
|
+
- Render 3D line waterfalls and common-grid surfaces with an isolated,
|
|
16
|
+
lazily-loaded PyQtGraph OpenGL backend and 2D fallback.
|
|
17
|
+
- Save one graph or a deduplicated multi-graph workspace as
|
|
18
|
+
`*.bernardyn.h5`, including canonical data, resolved snapshots, configuration,
|
|
19
|
+
checksums, previews, and 3D renderer data.
|
|
20
|
+
- Export PNG/JPEG/SVG, CSV, Igor ITX, and canonical data through PyIrena's H5XP
|
|
21
|
+
writer. H5XP export is data interoperability, not Bernardyn graph persistence.
|
|
22
|
+
|
|
23
|
+
The native schema is documented in
|
|
24
|
+
[`bernardyn/schemas/graph-package-v1.md`](bernardyn/schemas/graph-package-v1.md).
|
|
25
|
+
User-facing workflow and file-format documentation is in
|
|
26
|
+
[`docs/`](docs/README.md).
|
|
27
|
+
|
|
28
|
+
## Install and run
|
|
29
|
+
|
|
30
|
+
Bernardyn targets Python 3.10–3.13 and shares its Qt plotting dependencies with
|
|
31
|
+
PyIrena. For development, keep the `Bernardyn` and `pyirena` repositories next
|
|
32
|
+
to each other and create the pinned Conda environment from the Bernardyn root:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
conda env create -f environment.yml
|
|
36
|
+
conda activate bernardyn
|
|
37
|
+
bernardyn-doctor
|
|
38
|
+
bernardyn
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The environment file installs both repositories in editable mode. If an older
|
|
42
|
+
`bernardyn` environment already exists with Python 3.14, remove and recreate it:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
conda deactivate
|
|
46
|
+
conda env remove -n bernardyn
|
|
47
|
+
conda env create -f environment.yml
|
|
48
|
+
conda activate bernardyn
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
For an already compatible Python 3.10–3.13 environment, installation can also
|
|
52
|
+
be performed manually:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
python -m pip install -e "../pyirena[qtplot]"
|
|
56
|
+
python -m pip install -e ".[dev]"
|
|
57
|
+
bernardyn
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
To diagnose an installation:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
bernardyn-doctor
|
|
64
|
+
bernardyn-doctor --json
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The OpenGL renderer needs a usable graphics context. If it cannot initialize,
|
|
68
|
+
Bernardyn displays a 2D offset-waterfall fallback and retains the 3D document
|
|
69
|
+
configuration.
|
|
70
|
+
|
|
71
|
+
## Development
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pytest
|
|
75
|
+
ruff check bernardyn tests
|
|
76
|
+
python -m build
|
|
77
|
+
twine check dist/*
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Extension entry points are `bernardyn.sources`, `bernardyn.transforms`, and
|
|
81
|
+
`bernardyn.renderers`; see [`bernardyn/EXTENDING.md`](bernardyn/EXTENDING.md).
|
|
82
|
+
|
|
83
|
+
## Scope
|
|
84
|
+
|
|
85
|
+
Version 1.0 is a plotting and presentation application. Model fitting,
|
|
86
|
+
reduction, destructive source modification, movies, and calibrated 2D detector
|
|
87
|
+
workflows are intentionally outside 1.0. Existing experimental 0.1 containers
|
|
88
|
+
are not treated as compatible graph packages.
|
|
89
|
+
|
|
90
|
+
Bernardyn is distributed under the MIT License.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Bernardyn scientific plotting workbench."""
|
|
2
|
+
|
|
3
|
+
from bernardyn.core.models import (
|
|
4
|
+
Annotation,
|
|
5
|
+
Dataset,
|
|
6
|
+
DatasetKind,
|
|
7
|
+
GraphDocument,
|
|
8
|
+
PlotSeries,
|
|
9
|
+
SeriesView,
|
|
10
|
+
Workspace,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"Annotation",
|
|
15
|
+
"Dataset",
|
|
16
|
+
"DatasetKind",
|
|
17
|
+
"GraphDocument",
|
|
18
|
+
"PlotSeries",
|
|
19
|
+
"SeriesView",
|
|
20
|
+
"Workspace",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
__version__ = "0.0.1b1"
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""GUI application entry point."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
from PySide6.QtWidgets import QApplication
|
|
10
|
+
|
|
11
|
+
from bernardyn.gui.main_window import MainWindow
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main() -> int:
|
|
15
|
+
# BERNARDYN_LOG_LEVEL=DEBUG turns on the per-annotation placement trace
|
|
16
|
+
# and the renderer's coordinate diagnostics.
|
|
17
|
+
level = os.environ.get("BERNARDYN_LOG_LEVEL", "INFO").upper()
|
|
18
|
+
logging.basicConfig(
|
|
19
|
+
level=getattr(logging, level, logging.INFO),
|
|
20
|
+
format="%(levelname)s %(name)s: %(message)s",
|
|
21
|
+
)
|
|
22
|
+
app = QApplication.instance() or QApplication(sys.argv)
|
|
23
|
+
app.setApplicationName("Bernardyn")
|
|
24
|
+
app.setOrganizationName("Bernardyn")
|
|
25
|
+
window = MainWindow()
|
|
26
|
+
window.show()
|
|
27
|
+
return app.exec()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
if __name__ == "__main__":
|
|
31
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
"""Qt-independent application controller for workspaces and graph packages."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import replace
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Iterable, Mapping
|
|
8
|
+
|
|
9
|
+
from bernardyn.core.models import (
|
|
10
|
+
Dataset,
|
|
11
|
+
GraphDocument,
|
|
12
|
+
PlotSeries,
|
|
13
|
+
SeriesStyle,
|
|
14
|
+
SeriesView,
|
|
15
|
+
Workspace,
|
|
16
|
+
)
|
|
17
|
+
from bernardyn.core.transforms import TransformRegistry, builtin_transforms, resolve_series
|
|
18
|
+
from bernardyn.io.container import LoadedPackage, import_graphs, load_package, save_package
|
|
19
|
+
from bernardyn.io.sources import ScatteringLocation, SourceRegistry, builtin_sources
|
|
20
|
+
|
|
21
|
+
PALETTE = (
|
|
22
|
+
(31, 119, 180, 255),
|
|
23
|
+
(214, 39, 40, 255),
|
|
24
|
+
(44, 160, 44, 255),
|
|
25
|
+
(148, 103, 189, 255),
|
|
26
|
+
(255, 127, 14, 255),
|
|
27
|
+
(23, 190, 207, 255),
|
|
28
|
+
(140, 86, 75, 255),
|
|
29
|
+
(227, 119, 194, 255),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ApplicationController:
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
*,
|
|
37
|
+
transforms: TransformRegistry | None = None,
|
|
38
|
+
sources: SourceRegistry | None = None,
|
|
39
|
+
) -> None:
|
|
40
|
+
self.transforms = transforms or builtin_transforms()
|
|
41
|
+
self.sources = sources or builtin_sources()
|
|
42
|
+
self.workspace = Workspace()
|
|
43
|
+
self.snapshots: dict[str, dict[str, PlotSeries]] = {}
|
|
44
|
+
self.previews: dict[str, bytes] = {}
|
|
45
|
+
self.renderer_data: dict[str, dict[str, Any]] = {}
|
|
46
|
+
self.read_only_graphs: set[str] = set()
|
|
47
|
+
self.preview_only_graphs: set[str] = set()
|
|
48
|
+
self._graph_warnings: dict[str, list[str]] = {}
|
|
49
|
+
self.package_path: Path | None = None
|
|
50
|
+
self.warnings: list[str] = []
|
|
51
|
+
self.new_graph()
|
|
52
|
+
|
|
53
|
+
def new_workspace(self, title: str = "Untitled workspace") -> None:
|
|
54
|
+
self.workspace = Workspace(title=title)
|
|
55
|
+
self.snapshots.clear()
|
|
56
|
+
self.previews.clear()
|
|
57
|
+
self.renderer_data.clear()
|
|
58
|
+
self.read_only_graphs.clear()
|
|
59
|
+
self.preview_only_graphs.clear()
|
|
60
|
+
self._graph_warnings.clear()
|
|
61
|
+
self.package_path = None
|
|
62
|
+
self.warnings.clear()
|
|
63
|
+
self.new_graph()
|
|
64
|
+
|
|
65
|
+
def new_graph(self, renderer_id: str = "plot2d", title: str | None = None) -> GraphDocument:
|
|
66
|
+
number = len(self.workspace.graphs) + 1
|
|
67
|
+
graph = GraphDocument(
|
|
68
|
+
title=title or f"Scattering plot {number}",
|
|
69
|
+
renderer_id=renderer_id,
|
|
70
|
+
renderer_config={
|
|
71
|
+
"mode": "surface" if renderer_id == "opengl_surface" else "waterfall",
|
|
72
|
+
"renderer_version": "1.0",
|
|
73
|
+
"spacing": 1.0,
|
|
74
|
+
"normalization": "none",
|
|
75
|
+
"surface_samples": 512,
|
|
76
|
+
"series_axis_key": "",
|
|
77
|
+
"series_positions": {},
|
|
78
|
+
"projection": "perspective",
|
|
79
|
+
"show_grid": True,
|
|
80
|
+
"camera": {"distance": 40.0, "elevation": 25.0, "azimuth": -45.0},
|
|
81
|
+
}
|
|
82
|
+
if renderer_id.startswith("opengl")
|
|
83
|
+
else {"renderer_version": "1.0"},
|
|
84
|
+
)
|
|
85
|
+
self.workspace.add_graph(graph)
|
|
86
|
+
self.snapshots[graph.id] = {}
|
|
87
|
+
return graph
|
|
88
|
+
|
|
89
|
+
def close_graph(self, graph_id: str) -> None:
|
|
90
|
+
self.workspace.graphs = [graph for graph in self.workspace.graphs if graph.id != graph_id]
|
|
91
|
+
self.snapshots.pop(graph_id, None)
|
|
92
|
+
self.previews.pop(graph_id, None)
|
|
93
|
+
self.renderer_data.pop(graph_id, None)
|
|
94
|
+
self.read_only_graphs.discard(graph_id)
|
|
95
|
+
self.preview_only_graphs.discard(graph_id)
|
|
96
|
+
self._graph_warnings.pop(graph_id, None)
|
|
97
|
+
if not self.workspace.graphs:
|
|
98
|
+
self.new_graph()
|
|
99
|
+
self.workspace.active_graph_id = self.workspace.graphs[-1].id
|
|
100
|
+
self.workspace.dirty = True
|
|
101
|
+
|
|
102
|
+
def add_dataset(self, dataset: Dataset, *, graph_id: str | None = None) -> SeriesView:
|
|
103
|
+
self.workspace.add_dataset(dataset)
|
|
104
|
+
graph = self.workspace.graph(graph_id or self.workspace.active_graph_id or "")
|
|
105
|
+
series = SeriesView(
|
|
106
|
+
dataset_id=dataset.id,
|
|
107
|
+
legend_label=dataset.label,
|
|
108
|
+
style=SeriesStyle(color=PALETTE[len(graph.series) % len(PALETTE)]),
|
|
109
|
+
)
|
|
110
|
+
self.workspace.replace_graph(graph.replace_series((*graph.series, series)))
|
|
111
|
+
self.recompute_graph(graph.id)
|
|
112
|
+
return series
|
|
113
|
+
|
|
114
|
+
def load_location(
|
|
115
|
+
self,
|
|
116
|
+
location: ScatteringLocation,
|
|
117
|
+
*,
|
|
118
|
+
graph_id: str | None = None,
|
|
119
|
+
q_unit: str = "1/A",
|
|
120
|
+
error_fraction: float = 0.05,
|
|
121
|
+
) -> Dataset:
|
|
122
|
+
record = self.sources.load_location(
|
|
123
|
+
location, q_unit=q_unit, error_fraction=error_fraction
|
|
124
|
+
)
|
|
125
|
+
dataset = record.to_dataset()
|
|
126
|
+
self.add_dataset(dataset, graph_id=graph_id)
|
|
127
|
+
return dataset
|
|
128
|
+
|
|
129
|
+
def update_graph(self, graph: GraphDocument, *, recompute: bool = False) -> None:
|
|
130
|
+
if graph.id in self.read_only_graphs:
|
|
131
|
+
raise PermissionError("this graph is read-only because its canonical data are invalid")
|
|
132
|
+
self.workspace.replace_graph(graph)
|
|
133
|
+
if recompute:
|
|
134
|
+
self.recompute_graph(graph.id)
|
|
135
|
+
|
|
136
|
+
def recompute_graph(self, graph_id: str) -> dict[str, PlotSeries]:
|
|
137
|
+
if graph_id in self.read_only_graphs:
|
|
138
|
+
raise PermissionError("cannot recompute a graph with invalid canonical data")
|
|
139
|
+
graph = self.workspace.graph(graph_id)
|
|
140
|
+
resolved: dict[str, PlotSeries] = {}
|
|
141
|
+
for view in graph.series:
|
|
142
|
+
dataset = self.workspace.datasets[view.dataset_id]
|
|
143
|
+
resolved[view.id] = resolve_series(
|
|
144
|
+
dataset,
|
|
145
|
+
view,
|
|
146
|
+
self.transforms,
|
|
147
|
+
x_log=graph.x_axis.log,
|
|
148
|
+
y_log=graph.y_axis.log,
|
|
149
|
+
)
|
|
150
|
+
self.snapshots[graph_id] = resolved
|
|
151
|
+
self._graph_warnings[graph_id] = [
|
|
152
|
+
warning
|
|
153
|
+
for warning in self._graph_warnings.get(graph_id, [])
|
|
154
|
+
if "current version" not in warning and "was recomputed" not in warning
|
|
155
|
+
]
|
|
156
|
+
self.workspace.dirty = True
|
|
157
|
+
return resolved
|
|
158
|
+
|
|
159
|
+
def set_transform(
|
|
160
|
+
self,
|
|
161
|
+
graph_id: str,
|
|
162
|
+
transform_id: str,
|
|
163
|
+
parameters: Mapping[str, float] | None = None,
|
|
164
|
+
) -> None:
|
|
165
|
+
graph = self.workspace.graph(graph_id)
|
|
166
|
+
transform = self.transforms.get(transform_id)
|
|
167
|
+
series = tuple(
|
|
168
|
+
replace(
|
|
169
|
+
item,
|
|
170
|
+
transform_id=transform_id,
|
|
171
|
+
transform_parameters=dict(parameters or {}),
|
|
172
|
+
)
|
|
173
|
+
for item in graph.series
|
|
174
|
+
)
|
|
175
|
+
graph = replace(
|
|
176
|
+
graph,
|
|
177
|
+
series=series,
|
|
178
|
+
x_axis=replace(
|
|
179
|
+
graph.x_axis,
|
|
180
|
+
label=transform.default_x_label,
|
|
181
|
+
log=transform.default_x_log,
|
|
182
|
+
auto_range=True,
|
|
183
|
+
),
|
|
184
|
+
y_axis=replace(
|
|
185
|
+
graph.y_axis,
|
|
186
|
+
label=transform.default_y_label,
|
|
187
|
+
log=transform.default_y_log,
|
|
188
|
+
auto_range=True,
|
|
189
|
+
),
|
|
190
|
+
)
|
|
191
|
+
self.update_graph(graph, recompute=True)
|
|
192
|
+
|
|
193
|
+
def remove_series(self, graph_id: str, series_id: str) -> None:
|
|
194
|
+
graph = self.workspace.graph(graph_id)
|
|
195
|
+
self.update_graph(
|
|
196
|
+
graph.replace_series(item for item in graph.series if item.id != series_id),
|
|
197
|
+
recompute=True,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
def move_series(self, graph_id: str, series_id: str, offset: int) -> None:
|
|
201
|
+
graph = self.workspace.graph(graph_id)
|
|
202
|
+
items = list(graph.series)
|
|
203
|
+
old = next(index for index, item in enumerate(items) if item.id == series_id)
|
|
204
|
+
new = max(0, min(len(items) - 1, old + offset))
|
|
205
|
+
items.insert(new, items.pop(old))
|
|
206
|
+
self.update_graph(graph.replace_series(items), recompute=False)
|
|
207
|
+
|
|
208
|
+
def open_package(self, path: str | Path) -> LoadedPackage:
|
|
209
|
+
loaded = load_package(path)
|
|
210
|
+
self.workspace = loaded.workspace
|
|
211
|
+
self.snapshots = loaded.snapshots
|
|
212
|
+
self.previews = loaded.previews
|
|
213
|
+
self.renderer_data = loaded.renderer_data
|
|
214
|
+
self.read_only_graphs = loaded.read_only_graphs
|
|
215
|
+
self.preview_only_graphs.clear()
|
|
216
|
+
self._graph_warnings.clear()
|
|
217
|
+
self.package_path = loaded.path
|
|
218
|
+
self.warnings = loaded.warnings
|
|
219
|
+
recovered = False
|
|
220
|
+
for graph in self.workspace.graphs:
|
|
221
|
+
graph_warnings = self._graph_warnings.setdefault(graph.id, [])
|
|
222
|
+
for view in graph.series:
|
|
223
|
+
archived = self.snapshots.get(graph.id, {}).get(view.id)
|
|
224
|
+
try:
|
|
225
|
+
transform = self.transforms.get(view.transform_id)
|
|
226
|
+
except KeyError:
|
|
227
|
+
graph_warnings.append(
|
|
228
|
+
f"Unknown transform {view.transform_id!r}; showing the archived preview when available"
|
|
229
|
+
)
|
|
230
|
+
if graph.id in self.previews:
|
|
231
|
+
self.preview_only_graphs.add(graph.id)
|
|
232
|
+
continue
|
|
233
|
+
if archived is not None and archived.transform_version != transform.version:
|
|
234
|
+
graph_warnings.append(
|
|
235
|
+
f"{transform.name} snapshot uses version {archived.transform_version}; "
|
|
236
|
+
f"current version is {transform.version}. Use Recompute with Current Version explicitly."
|
|
237
|
+
)
|
|
238
|
+
continue
|
|
239
|
+
if archived is None:
|
|
240
|
+
dataset = self.workspace.datasets[view.dataset_id]
|
|
241
|
+
self.snapshots.setdefault(graph.id, {})[view.id] = resolve_series(
|
|
242
|
+
dataset,
|
|
243
|
+
view,
|
|
244
|
+
self.transforms,
|
|
245
|
+
x_log=graph.x_axis.log,
|
|
246
|
+
y_log=graph.y_axis.log,
|
|
247
|
+
)
|
|
248
|
+
graph_warnings.append(
|
|
249
|
+
f"Missing or corrupt snapshot for {dataset.label!r} was recomputed from embedded data"
|
|
250
|
+
)
|
|
251
|
+
recovered = True
|
|
252
|
+
if recovered:
|
|
253
|
+
self.workspace.dirty = True
|
|
254
|
+
return loaded
|
|
255
|
+
|
|
256
|
+
def import_from_package(
|
|
257
|
+
self, path: str | Path, graph_ids: Iterable[str] | None = None
|
|
258
|
+
) -> dict[str, str]:
|
|
259
|
+
loaded = load_package(path)
|
|
260
|
+
selected = list(graph_ids) if graph_ids is not None else [g.id for g in loaded.workspace.graphs]
|
|
261
|
+
graph_map, snapshots = import_graphs(self.workspace, loaded, selected)
|
|
262
|
+
self.snapshots.update(snapshots)
|
|
263
|
+
for old_id, new_id in graph_map.items():
|
|
264
|
+
if old_id in loaded.previews:
|
|
265
|
+
self.previews[new_id] = loaded.previews[old_id]
|
|
266
|
+
if old_id in loaded.renderer_data:
|
|
267
|
+
self.renderer_data[new_id] = loaded.renderer_data[old_id]
|
|
268
|
+
if old_id in loaded.read_only_graphs:
|
|
269
|
+
self.read_only_graphs.add(new_id)
|
|
270
|
+
self.warnings.extend(loaded.warnings)
|
|
271
|
+
return graph_map
|
|
272
|
+
|
|
273
|
+
def save(
|
|
274
|
+
self,
|
|
275
|
+
path: str | Path | None = None,
|
|
276
|
+
*,
|
|
277
|
+
graph_ids: Iterable[str] | None = None,
|
|
278
|
+
previews: Mapping[str, bytes] | None = None,
|
|
279
|
+
renderer_data: Mapping[str, Mapping[str, Any]] | None = None,
|
|
280
|
+
) -> Path:
|
|
281
|
+
destination = Path(path) if path is not None else self.package_path
|
|
282
|
+
if destination is None:
|
|
283
|
+
raise ValueError("a destination path is required for the first save")
|
|
284
|
+
chosen = list(graph_ids) if graph_ids is not None else None
|
|
285
|
+
selected_graph_ids = chosen or [graph.id for graph in self.workspace.graphs]
|
|
286
|
+
read_only = self.read_only_graphs.intersection(selected_graph_ids)
|
|
287
|
+
if read_only:
|
|
288
|
+
raise PermissionError(
|
|
289
|
+
"cannot save graphs whose canonical arrays failed validation; "
|
|
290
|
+
"retain the original package as the archival copy"
|
|
291
|
+
)
|
|
292
|
+
for graph_id in selected_graph_ids:
|
|
293
|
+
graph = self.workspace.graph(graph_id)
|
|
294
|
+
missing = [
|
|
295
|
+
series.id
|
|
296
|
+
for series in graph.series
|
|
297
|
+
if series.id not in self.snapshots.get(graph_id, {})
|
|
298
|
+
]
|
|
299
|
+
if missing and graph_id not in self.read_only_graphs:
|
|
300
|
+
self.recompute_graph(graph_id)
|
|
301
|
+
saved = save_package(
|
|
302
|
+
destination,
|
|
303
|
+
self.workspace,
|
|
304
|
+
self.snapshots,
|
|
305
|
+
graph_ids=chosen,
|
|
306
|
+
previews=previews or self.previews,
|
|
307
|
+
renderer_data=renderer_data or self.renderer_data,
|
|
308
|
+
)
|
|
309
|
+
if chosen is None:
|
|
310
|
+
self.package_path = saved
|
|
311
|
+
self.workspace.dirty = False
|
|
312
|
+
return saved
|
|
313
|
+
|
|
314
|
+
def graph_warnings(self, graph_id: str) -> list[str]:
|
|
315
|
+
return self._graph_warnings.get(graph_id, []) + [
|
|
316
|
+
warning
|
|
317
|
+
for snapshot in self.snapshots.get(graph_id, {}).values()
|
|
318
|
+
for warning in snapshot.warnings
|
|
319
|
+
]
|