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 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,6 @@
1
+ """Scientific domain models, transforms, registries, and workspace logic."""
2
+
3
+ from bernardyn.core.models import * # noqa: F403
4
+ from bernardyn.core.transforms import TransformRegistry, builtin_transforms
5
+
6
+ __all__ = ["TransformRegistry", "builtin_transforms"]
@@ -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
+ ]