bernardyn 0.0.1b1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bernardyn/__init__.py +23 -0
- bernardyn/app.py +31 -0
- bernardyn/core/__init__.py +6 -0
- bernardyn/core/controller.py +319 -0
- bernardyn/core/models.py +436 -0
- bernardyn/core/registry.py +57 -0
- bernardyn/core/transforms.py +309 -0
- bernardyn/data/__init__.py +5 -0
- bernardyn/diagnostics.py +96 -0
- bernardyn/export/__init__.py +6 -0
- bernardyn/gui/__init__.py +5 -0
- bernardyn/gui/dialogs.py +620 -0
- bernardyn/gui/graph_page.py +126 -0
- bernardyn/gui/inspector.py +921 -0
- bernardyn/gui/main_window.py +917 -0
- bernardyn/io/__init__.py +10 -0
- bernardyn/io/container.py +600 -0
- bernardyn/io/curve_export.py +83 -0
- bernardyn/io/file_browser.py +133 -0
- bernardyn/io/igor.py +50 -0
- bernardyn/io/sources.py +636 -0
- bernardyn/main.py +8 -0
- bernardyn/plot/__init__.py +5 -0
- bernardyn/py.typed +0 -0
- bernardyn/renderers/__init__.py +20 -0
- bernardyn/renderers/opengl.py +267 -0
- bernardyn/renderers/plot2d.py +618 -0
- bernardyn/renderers/registry.py +58 -0
- bernardyn/schemas/graph-package-v1.md +111 -0
- bernardyn/state.py +44 -0
- bernardyn/template/__init__.py +5 -0
- bernardyn/template/graph_templates.py +118 -0
- bernardyn/utils/__init__.py +1 -0
- bernardyn-0.0.1b1.dist-info/METADATA +129 -0
- bernardyn-0.0.1b1.dist-info/RECORD +39 -0
- bernardyn-0.0.1b1.dist-info/WHEEL +5 -0
- bernardyn-0.0.1b1.dist-info/entry_points.txt +5 -0
- bernardyn-0.0.1b1.dist-info/licenses/LICENSE +21 -0
- bernardyn-0.0.1b1.dist-info/top_level.txt +1 -0
bernardyn/__init__.py
ADDED
|
@@ -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"
|
bernardyn/app.py
ADDED
|
@@ -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
|
+
]
|