gratinglab 0.1.3__tar.gz → 0.1.4__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.
- {gratinglab-0.1.3/src/gratinglab.egg-info → gratinglab-0.1.4}/PKG-INFO +6 -2
- {gratinglab-0.1.3 → gratinglab-0.1.4}/README.md +5 -1
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/__init__.py +1 -1
- gratinglab-0.1.4/src/gratinglab/gui/qt/gc_guard.py +102 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/gui/qt/geometry_panel.py +32 -5
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/gui/qt/main_window.py +14 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/gui/state.py +5 -2
- gratinglab-0.1.4/src/gratinglab/io/boundary.py +270 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/metrology/gui/qt/boundary_view.py +121 -16
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/metrology/gui/qt/main_window.py +82 -1
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/metrology/gui/state.py +13 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/metrology/wiki/reading-outputs.md +15 -2
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/metrology/wiki/tip-correction.md +13 -1
- {gratinglab-0.1.3 → gratinglab-0.1.4/src/gratinglab.egg-info}/PKG-INFO +6 -2
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab.egg-info/SOURCES.txt +3 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/tests/test_gui_qt.py +127 -0
- gratinglab-0.1.4/tests/test_io_boundary.py +156 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/LICENSE +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/pyproject.toml +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/setup.cfg +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/checks.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/compare.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/convergence.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/geometry.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/gui/__init__.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/gui/app.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/gui/diagram.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/gui/diagram3d.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/gui/docs.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/gui/icon.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/gui/integral_options.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/gui/mathtext.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/gui/orders.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/gui/provenance.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/gui/qt/__init__.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/gui/qt/geometry_tab.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/gui/qt/integral_tab.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/gui/qt/profile_plot_panel.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/gui/qt/scalar_tab.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/gui/qt/scene3d_panel.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/gui/qt/setup_tab.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/gui/qt/sizing.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/gui/qt/theory_viewer.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/gui/qt/worker.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/gui/richtext.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/gui/scalar_options.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/illumination.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/io/__init__.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/io/efficiency_table.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/io/ggp.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/materials/__init__.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/materials/data/SOURCES.md +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/materials/fresnel.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/materials/optical.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/materials/refractiveindex.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/metrology/__init__.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/metrology/analyzer.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/metrology/boundary/__init__.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/metrology/boundary/average.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/metrology/boundary/pipeline.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/metrology/cli.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/metrology/config.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/metrology/core/__init__.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/metrology/core/analysis.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/metrology/core/image_flatten.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/metrology/core/processing.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/metrology/core/tip.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/metrology/data/__init__.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/metrology/data/aggregation.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/metrology/gui/__init__.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/metrology/gui/icon.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/metrology/gui/qt/__init__.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/metrology/gui/qt/app.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/metrology/gui/qt/canvas.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/metrology/gui/qt/efficiency_dialog.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/metrology/gui/qt/import_view.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/metrology/gui/qt/wiki_view.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/metrology/gui/qt/worker.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/metrology/io/__init__.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/metrology/io/file_io.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/metrology/io/metrics.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/metrology/io/spm.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/metrology/settings.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/metrology/stats/__init__.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/metrology/stats/analysis.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/metrology/stats/icc.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/metrology/visualization/__init__.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/metrology/visualization/diagnostics.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/metrology/visualization/profiles.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/metrology/visualization/statistics.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/metrology/wiki/__init__.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/metrology/wiki/facet-fitting.md +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/metrology/wiki/file-formats.md +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/metrology/wiki/flattening.md +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/metrology/wiki/icc-correction.md +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/metrology/wiki/overview.md +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/metrology/wiki/row-groups.md +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/metrology/workflows.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/problem.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/profiles.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/resolution.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/result.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/solvers/__init__.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/solvers/base.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/solvers/integral/__init__.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/solvers/integral/_boundary.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/solvers/integral/_core.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/solvers/integral/_finite.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/solvers/integral/_greens.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/solvers/integral/_kernels.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/solvers/integral/_nystrom.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab/solvers/scalar.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab.egg-info/dependency_links.txt +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab.egg-info/entry_points.txt +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab.egg-info/requires.txt +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/src/gratinglab.egg-info/top_level.txt +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/tests/test_checks.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/tests/test_compare.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/tests/test_convergence.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/tests/test_cross_method.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/tests/test_efficiency_table.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/tests/test_fresnel.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/tests/test_geometry.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/tests/test_ggp.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/tests/test_gui_diagram.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/tests/test_gui_diagram3d.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/tests/test_gui_docs.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/tests/test_gui_integral_options.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/tests/test_gui_mathtext.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/tests/test_gui_orders.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/tests/test_gui_provenance.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/tests/test_gui_richtext.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/tests/test_gui_scalar_options.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/tests/test_gui_state.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/tests/test_illumination.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/tests/test_integral_core.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/tests/test_integral_corpus.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/tests/test_integral_finite.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/tests/test_integral_greens.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/tests/test_integral_kernels.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/tests/test_integral_operators.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/tests/test_integral_solver.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/tests/test_materials.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/tests/test_perturbation.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/tests/test_profiles.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/tests/test_refractiveindex.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/tests/test_resolution.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/tests/test_result.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/tests/test_scalar.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/tests/test_solver_conformance.py +0 -0
- {gratinglab-0.1.3 → gratinglab-0.1.4}/tests/test_solver_progress.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gratinglab
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Open platform for diffraction grating groove metrology and efficiency analysis
|
|
5
5
|
Author: Jake McCoy
|
|
6
6
|
License: BSD-3-Clause
|
|
@@ -74,7 +74,11 @@ The two halves are one package because a file between them loses information. A
|
|
|
74
74
|
PCGrate `.ggp` boundary carries a *shape* and not a grating: it cannot hold the period,
|
|
75
75
|
so the period gets retyped, guessed, or recovered from a sidecar later.
|
|
76
76
|
`BoundaryProfile.to_problem()` carries the period the scan measured, and the fitted
|
|
77
|
-
facet angle with it.
|
|
77
|
+
facet angle with it. To save a profile without losing that, write it as gratinglab's
|
|
78
|
+
own `.json` (`gratinglab.io.boundary.write_boundary_json`): reading it back gives the
|
|
79
|
+
same problem, and the efficiency window's "Load boundary profile" fills in the
|
|
80
|
+
period from it. The metrology window's Boundary tab exports `.ggp`, `.json` or
|
|
81
|
+
`.csv`, and its "Send to efficiency window" button opens the groove there directly.
|
|
78
82
|
|
|
79
83
|
```python
|
|
80
84
|
from gratinglab.metrology.boundary import build_boundary_profile
|
|
@@ -35,7 +35,11 @@ The two halves are one package because a file between them loses information. A
|
|
|
35
35
|
PCGrate `.ggp` boundary carries a *shape* and not a grating: it cannot hold the period,
|
|
36
36
|
so the period gets retyped, guessed, or recovered from a sidecar later.
|
|
37
37
|
`BoundaryProfile.to_problem()` carries the period the scan measured, and the fitted
|
|
38
|
-
facet angle with it.
|
|
38
|
+
facet angle with it. To save a profile without losing that, write it as gratinglab's
|
|
39
|
+
own `.json` (`gratinglab.io.boundary.write_boundary_json`): reading it back gives the
|
|
40
|
+
same problem, and the efficiency window's "Load boundary profile" fills in the
|
|
41
|
+
period from it. The metrology window's Boundary tab exports `.ggp`, `.json` or
|
|
42
|
+
`.csv`, and its "Send to efficiency window" button opens the groove there directly.
|
|
39
43
|
|
|
40
44
|
```python
|
|
41
45
|
from gratinglab.metrology.boundary import build_boundary_profile
|
|
@@ -21,7 +21,7 @@ from .illumination import Illumination, Polarization
|
|
|
21
21
|
from .resolution import ResolvingPower, resolving_power
|
|
22
22
|
from .result import EfficiencyScan, OrderEfficiency, Provenance
|
|
23
23
|
|
|
24
|
-
__version__ = "0.1.
|
|
24
|
+
__version__ = "0.1.4"
|
|
25
25
|
|
|
26
26
|
__all__ = [
|
|
27
27
|
"Illumination",
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Run Python's cycle collector on the GUI thread, never on a worker's.
|
|
2
|
+
|
|
3
|
+
Both windows do their heavy work on a ``QThread``: the metrology window's
|
|
4
|
+
analysis and the main window's solves. Python's automatic collector runs on
|
|
5
|
+
whichever thread happens to allocate past its threshold, so it regularly ran
|
|
6
|
+
*inside* those workers, and when the garbage it found held Qt objects (a
|
|
7
|
+
closed window, a figure from an earlier preview) it destroyed them off the GUI
|
|
8
|
+
thread. Qt does not allow that, and on macOS it segfaults: CI run 39835186938
|
|
9
|
+
died with ``Fatal Python error: Segmentation fault`` and "Garbage-collecting"
|
|
10
|
+
at the top of the worker thread's stack, in the middle of an ordinary
|
|
11
|
+
``genfromtxt``. The same thing can happen to a user: close some windows, press
|
|
12
|
+
Run, and the application crashes.
|
|
13
|
+
|
|
14
|
+
The remedy is the usual one for PySide and PyQt applications. While any window
|
|
15
|
+
holds the guard, automatic collection is off, and a timer on the GUI thread
|
|
16
|
+
runs the collector whenever the generation counts say it is due, so garbage is
|
|
17
|
+
still collected promptly and always on the one thread allowed to free a Qt
|
|
18
|
+
object. When the last window releases it, automatic collection is restored,
|
|
19
|
+
so code that runs with no window open behaves exactly as before.
|
|
20
|
+
|
|
21
|
+
A window that is closed but never deleted never releases it, so the guard
|
|
22
|
+
also restores collection at interpreter exit, through ``atexit``, and collects
|
|
23
|
+
once while Qt still exists. Without that, Python's own final collection freed
|
|
24
|
+
the accumulated Qt garbage after Qt was gone: the ubuntu / py3.11 CI leg passed
|
|
25
|
+
every test and then aborted with ``Fatal Python error: bool_dealloc``, twice,
|
|
26
|
+
with one window still holding the guard at the end of the session.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import atexit
|
|
32
|
+
import gc
|
|
33
|
+
|
|
34
|
+
from PySide6.QtCore import QObject, QTimer
|
|
35
|
+
from PySide6.QtWidgets import QApplication
|
|
36
|
+
|
|
37
|
+
__all__ = ["guard", "is_active"]
|
|
38
|
+
|
|
39
|
+
#: How often the GUI thread checks whether a collection is due. Checking is
|
|
40
|
+
#: three integer comparisons; collecting happens only past the thresholds.
|
|
41
|
+
INTERVAL_MS = 500
|
|
42
|
+
|
|
43
|
+
_collector: "_Collector | None" = None
|
|
44
|
+
_holders = 0
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class _Collector(QObject):
|
|
48
|
+
def __init__(self, parent: QObject) -> None:
|
|
49
|
+
super().__init__(parent)
|
|
50
|
+
self._timer = QTimer(self)
|
|
51
|
+
self._timer.timeout.connect(self.collect_if_due)
|
|
52
|
+
self._timer.start(INTERVAL_MS)
|
|
53
|
+
|
|
54
|
+
def collect_if_due(self) -> None:
|
|
55
|
+
"""What the automatic collector would have done, on this thread."""
|
|
56
|
+
count0, count1, count2 = gc.get_count()
|
|
57
|
+
limit0, limit1, limit2 = gc.get_threshold()
|
|
58
|
+
if count0 > limit0:
|
|
59
|
+
generation = 2 if count2 > limit2 else 1 if count1 > limit1 else 0
|
|
60
|
+
gc.collect(generation)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def guard(window: QObject) -> None:
|
|
64
|
+
"""Keep the collector on the GUI thread for as long as ``window`` exists.
|
|
65
|
+
|
|
66
|
+
Call from the window's constructor, on the GUI thread. Each call is
|
|
67
|
+
released by that window's ``destroyed`` signal; the last release restores
|
|
68
|
+
automatic collection.
|
|
69
|
+
"""
|
|
70
|
+
global _collector, _holders
|
|
71
|
+
if _holders == 0:
|
|
72
|
+
app = QApplication.instance()
|
|
73
|
+
# Whatever garbage already exists is collected here and now, on this
|
|
74
|
+
# thread, before any worker can start allocating.
|
|
75
|
+
gc.collect()
|
|
76
|
+
gc.disable()
|
|
77
|
+
_collector = _Collector(app if app is not None else window)
|
|
78
|
+
_holders += 1
|
|
79
|
+
window.destroyed.connect(_release)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _release(*_args) -> None:
|
|
83
|
+
global _collector, _holders
|
|
84
|
+
_holders = max(0, _holders - 1)
|
|
85
|
+
if _holders == 0 and _collector is not None:
|
|
86
|
+
_collector._timer.stop()
|
|
87
|
+
_collector.deleteLater()
|
|
88
|
+
_collector = None
|
|
89
|
+
gc.enable()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@atexit.register
|
|
93
|
+
def _restore_at_exit() -> None:
|
|
94
|
+
"""Collect while Qt is still alive, and hand collection back to Python."""
|
|
95
|
+
if _holders > 0:
|
|
96
|
+
gc.enable()
|
|
97
|
+
gc.collect()
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def is_active() -> bool:
|
|
101
|
+
"""True while some window holds the guard, so automatic collection is off."""
|
|
102
|
+
return _holders > 0
|
|
@@ -216,13 +216,40 @@ class GeometryPanel(QWidget):
|
|
|
216
216
|
|
|
217
217
|
def load_profile(self) -> None:
|
|
218
218
|
path, _ = QFileDialog.getOpenFileName(
|
|
219
|
-
self, "Load boundary profile", "",
|
|
219
|
+
self, "Load boundary profile", "",
|
|
220
|
+
"Boundary profiles (*.json *.ggp);;"
|
|
221
|
+
"gratinglab boundary, with period (*.json);;"
|
|
222
|
+
"PCGrate boundary (*.ggp);;All files (*)",
|
|
220
223
|
)
|
|
221
224
|
if path:
|
|
222
|
-
self.
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
225
|
+
self.load_boundary_file(path)
|
|
226
|
+
|
|
227
|
+
def load_boundary_file(self, path: "str | Path") -> None:
|
|
228
|
+
"""Use a boundary file as the profile, and its period if it has one.
|
|
229
|
+
|
|
230
|
+
A gratinglab ``.json`` carries the period it was measured at, so the
|
|
231
|
+
period field is set from it; a ``.ggp`` cannot, so the field is left as
|
|
232
|
+
it is, which is the one case where the user still has to supply it.
|
|
233
|
+
Also what the metrology window's "Send to efficiency window" calls.
|
|
234
|
+
"""
|
|
235
|
+
path = Path(path)
|
|
236
|
+
note = ""
|
|
237
|
+
if path.suffix.lower() == ".json":
|
|
238
|
+
from ...io.boundary import read_boundary_json
|
|
239
|
+
|
|
240
|
+
try:
|
|
241
|
+
saved = read_boundary_json(path)
|
|
242
|
+
except (OSError, ValueError):
|
|
243
|
+
pass # the form's own validation reports the reason
|
|
244
|
+
else:
|
|
245
|
+
self._fields["period"].setText(f"{saved.period_nm:.6g}")
|
|
246
|
+
note = f" (period {saved.period_nm:.6g} nm from the file)"
|
|
247
|
+
kind = self._fields["profile_kind"]
|
|
248
|
+
kind.setCurrentIndex(kind.findText("From file"))
|
|
249
|
+
self._fields["profile_path"].setText(str(path))
|
|
250
|
+
self._path_label.setText(path.name + note)
|
|
251
|
+
self.changed.emit()
|
|
252
|
+
self.solve_requested.emit()
|
|
226
253
|
|
|
227
254
|
|
|
228
255
|
def _value(widget: QWidget) -> str:
|
|
@@ -103,6 +103,10 @@ class MainWindow(QMainWindow):
|
|
|
103
103
|
super().__init__(parent)
|
|
104
104
|
self.setWindowTitle("GratingLab")
|
|
105
105
|
self.resize(1180, 820)
|
|
106
|
+
# Solves run on a worker thread; keep the cycle collector off it.
|
|
107
|
+
from .gc_guard import guard
|
|
108
|
+
|
|
109
|
+
guard(self)
|
|
106
110
|
|
|
107
111
|
self._parsed = None
|
|
108
112
|
self._token = 0
|
|
@@ -313,6 +317,16 @@ class MainWindow(QMainWindow):
|
|
|
313
317
|
|
|
314
318
|
# -- actions ---------------------------------------------------------
|
|
315
319
|
|
|
320
|
+
def open_boundary(self, path) -> None:
|
|
321
|
+
"""Load a boundary profile file as the grating, as if chosen by hand.
|
|
322
|
+
|
|
323
|
+
The metrology window's "Send to efficiency window" calls this with a
|
|
324
|
+
gratinglab ``.json``, so the measured period arrives with the shape.
|
|
325
|
+
"""
|
|
326
|
+
self.geometry.load_boundary_file(path)
|
|
327
|
+
if not self.geometry_dock.isVisible():
|
|
328
|
+
self.geometry_dock.show()
|
|
329
|
+
|
|
316
330
|
def _geometry_edited(self) -> None:
|
|
317
331
|
"""A field was edited. Schedule a redraw; solve nothing.
|
|
318
332
|
|
|
@@ -246,9 +246,12 @@ def _build_profile(form: FormState, errors: list[FieldError]):
|
|
|
246
246
|
errors.append(FieldError("profile_path", f"no such file: {path}"))
|
|
247
247
|
return None
|
|
248
248
|
try:
|
|
249
|
-
|
|
249
|
+
# A gratinglab .json or a PCGrate .ggp, by suffix. The JSON's period is
|
|
250
|
+
# applied to the form when the file is loaded (GeometryPanel), not here:
|
|
251
|
+
# the period field stays the one place the period is read from.
|
|
252
|
+
from ..io.boundary import read_profile_file
|
|
250
253
|
|
|
251
|
-
return
|
|
254
|
+
return read_profile_file(path)
|
|
252
255
|
except Exception as exc: # the reader raises ValueError with a useful message
|
|
253
256
|
errors.append(FieldError("profile_path", str(exc).split("\n")[0]))
|
|
254
257
|
return None
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""Saving a measured groove profile with its scale and where it came from.
|
|
2
|
+
|
|
3
|
+
A ``.ggp`` holds a groove's *shape* and nothing else: it is normalised to one
|
|
4
|
+
period and has no room for the period itself, so a profile written to one and
|
|
5
|
+
read back has lost the number the solver's phase term scales by. That is the
|
|
6
|
+
gap ``docs/roadmap.md`` item 5 names. Two formats close it here:
|
|
7
|
+
|
|
8
|
+
``.json``, gratinglab's own
|
|
9
|
+
The normalised profile at full precision, the measured period in nm, the
|
|
10
|
+
groove count, the profile metrics, and provenance: the gratinglab version,
|
|
11
|
+
the scan it came from, how many grooves were detected and averaged, and
|
|
12
|
+
the analysis settings. :func:`read_boundary_json` reads it back, and its
|
|
13
|
+
:meth:`BoundaryFile.to_problem` gives the same :class:`~gratinglab.problem.Problem`
|
|
14
|
+
the in-process ``BoundaryProfile.to_problem`` does, so saving loses nothing.
|
|
15
|
+
The main window's "Load boundary profile" reads it and fills in the period.
|
|
16
|
+
|
|
17
|
+
``.csv``, for everything else
|
|
18
|
+
One row per point, ``t`` and ``y`` as fractions of the period plus the same
|
|
19
|
+
points in nm, under ``#`` comment lines carrying the period, groove counts
|
|
20
|
+
and source. For a spreadsheet, MATLAB, or another code. Write-only: the
|
|
21
|
+
JSON is the one to read back.
|
|
22
|
+
|
|
23
|
+
Both writers take any object with the fields of
|
|
24
|
+
``gratinglab.metrology.boundary.BoundaryProfile`` (``x_norm``, ``y_norm``,
|
|
25
|
+
``period_nm``, ``n_grooves``, ``n_used``, ``n_edge_rejected``, ``metrics``)
|
|
26
|
+
rather than importing it, because the metrology package needs the plotting
|
|
27
|
+
extra and this module must not.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import dataclasses
|
|
33
|
+
import json
|
|
34
|
+
import math
|
|
35
|
+
from dataclasses import dataclass, field
|
|
36
|
+
from datetime import datetime, timezone
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
from typing import Any
|
|
39
|
+
|
|
40
|
+
import numpy as np
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
"FORMAT",
|
|
44
|
+
"VERSION",
|
|
45
|
+
"BoundaryFile",
|
|
46
|
+
"read_boundary_json",
|
|
47
|
+
"read_profile_file",
|
|
48
|
+
"write_boundary_csv",
|
|
49
|
+
"write_boundary_json",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
#: Identifies the file, so a reader can refuse an unrelated JSON by name
|
|
53
|
+
#: rather than failing on a missing key three lines later.
|
|
54
|
+
FORMAT = "gratinglab-boundary"
|
|
55
|
+
|
|
56
|
+
#: Bumped when a reader of the previous version could misread a new file.
|
|
57
|
+
#: Adding an optional key is not such a change.
|
|
58
|
+
VERSION = 1
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True)
|
|
62
|
+
class BoundaryFile:
|
|
63
|
+
"""A groove profile read back from a gratinglab ``.json``."""
|
|
64
|
+
|
|
65
|
+
period_nm: float
|
|
66
|
+
#: Fractions of one period, as :class:`~gratinglab.profiles.FromProfileData` takes them.
|
|
67
|
+
t: tuple[float, ...]
|
|
68
|
+
y: tuple[float, ...]
|
|
69
|
+
#: Grooves averaged into this profile, carried onto the problem.
|
|
70
|
+
n_grooves: int | None = None
|
|
71
|
+
#: A fitted facet angle in degrees, if the writer had one.
|
|
72
|
+
blaze_angle: float | None = None
|
|
73
|
+
metrics: dict = field(default_factory=dict)
|
|
74
|
+
provenance: dict = field(default_factory=dict)
|
|
75
|
+
|
|
76
|
+
def profile(self):
|
|
77
|
+
"""The shape, as the solvers take it."""
|
|
78
|
+
from ..profiles import FromProfileData
|
|
79
|
+
|
|
80
|
+
return FromProfileData(t=self.t, y=self.y, blaze_angle=self.blaze_angle)
|
|
81
|
+
|
|
82
|
+
def to_problem(self, *, coating=None, substrate=None, roughness=0.0):
|
|
83
|
+
"""This groove as an efficiency problem, with the period it was measured at.
|
|
84
|
+
|
|
85
|
+
The same problem ``BoundaryProfile.to_problem`` builds in process,
|
|
86
|
+
which is the point of the format: see ``tests/test_io_boundary.py``.
|
|
87
|
+
Roughness is not in the file for the reason that method's docstring
|
|
88
|
+
gives: nothing here measures surface microroughness.
|
|
89
|
+
"""
|
|
90
|
+
from ..problem import Problem
|
|
91
|
+
|
|
92
|
+
return Problem(
|
|
93
|
+
period=self.period_nm,
|
|
94
|
+
profile=self.profile(),
|
|
95
|
+
coating=coating,
|
|
96
|
+
substrate=substrate,
|
|
97
|
+
roughness=roughness,
|
|
98
|
+
n_grooves=self.n_grooves,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def write_boundary_json(
|
|
103
|
+
path: "str | Path",
|
|
104
|
+
profile: Any,
|
|
105
|
+
*,
|
|
106
|
+
source: "str | Path | None" = None,
|
|
107
|
+
settings: Any = None,
|
|
108
|
+
blaze_angle: float | None = None,
|
|
109
|
+
) -> Path:
|
|
110
|
+
"""Write ``profile`` (a ``BoundaryProfile``) as a gratinglab ``.json``.
|
|
111
|
+
|
|
112
|
+
``source`` names the scan it came from and ``settings`` is the
|
|
113
|
+
``AnalysisSettings`` it was built with; both land in the provenance and
|
|
114
|
+
neither is required. Floats are written at full precision, so reading the
|
|
115
|
+
file back reproduces the profile exactly rather than to six decimals as a
|
|
116
|
+
``.ggp`` does.
|
|
117
|
+
"""
|
|
118
|
+
path = Path(path)
|
|
119
|
+
t, y = _points(profile)
|
|
120
|
+
document = {
|
|
121
|
+
"format": FORMAT,
|
|
122
|
+
"version": VERSION,
|
|
123
|
+
"period_nm": float(profile.period_nm),
|
|
124
|
+
"profile": {
|
|
125
|
+
"units": "t and y are fractions of one period",
|
|
126
|
+
"t": t,
|
|
127
|
+
"y": y,
|
|
128
|
+
},
|
|
129
|
+
"n_grooves": int(profile.n_used),
|
|
130
|
+
"blaze_angle_deg": None if blaze_angle is None else float(blaze_angle),
|
|
131
|
+
"metrics": _plain(profile.metrics),
|
|
132
|
+
"provenance": _provenance(profile, source, settings),
|
|
133
|
+
}
|
|
134
|
+
text = json.dumps(document, indent=1, allow_nan=False) + "\n"
|
|
135
|
+
path.write_bytes(text.encode("utf-8"))
|
|
136
|
+
return path
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def read_boundary_json(path: "str | Path") -> BoundaryFile:
|
|
140
|
+
"""Read a gratinglab ``.json`` boundary profile.
|
|
141
|
+
|
|
142
|
+
Refuses, by name, a JSON that is not one, a newer version than this reader
|
|
143
|
+
knows, and a profile whose two columns differ in length or whose period is
|
|
144
|
+
not a positive number. The profile's own checks (at least two points, and
|
|
145
|
+
so on) run when :meth:`BoundaryFile.profile` builds it.
|
|
146
|
+
"""
|
|
147
|
+
path = Path(path)
|
|
148
|
+
try:
|
|
149
|
+
document = json.loads(path.read_bytes().decode("utf-8"))
|
|
150
|
+
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
151
|
+
raise ValueError(f"{path}: not a JSON file ({exc})") from exc
|
|
152
|
+
if not isinstance(document, dict) or document.get("format") != FORMAT:
|
|
153
|
+
raise ValueError(f"{path}: not a gratinglab boundary profile (no format: {FORMAT!r})")
|
|
154
|
+
version = document.get("version")
|
|
155
|
+
if not isinstance(version, int) or version > VERSION:
|
|
156
|
+
raise ValueError(
|
|
157
|
+
f"{path}: boundary format version {version!r}; this gratinglab reads "
|
|
158
|
+
f"up to {VERSION}, so upgrade it to read this file")
|
|
159
|
+
period = document.get("period_nm")
|
|
160
|
+
if not isinstance(period, (int, float)) or not math.isfinite(period) or period <= 0:
|
|
161
|
+
raise ValueError(f"{path}: period_nm must be a positive number, got {period!r}")
|
|
162
|
+
points = document.get("profile") or {}
|
|
163
|
+
t, y = points.get("t"), points.get("y")
|
|
164
|
+
if not isinstance(t, list) or not isinstance(y, list) or len(t) != len(y):
|
|
165
|
+
raise ValueError(f"{path}: profile t and y must be two lists of equal length")
|
|
166
|
+
blaze = document.get("blaze_angle_deg")
|
|
167
|
+
n_grooves = document.get("n_grooves")
|
|
168
|
+
return BoundaryFile(
|
|
169
|
+
period_nm=float(period),
|
|
170
|
+
t=tuple(float(v) for v in t),
|
|
171
|
+
y=tuple(float(v) for v in y),
|
|
172
|
+
n_grooves=None if n_grooves is None else int(n_grooves),
|
|
173
|
+
blaze_angle=None if blaze is None else float(blaze),
|
|
174
|
+
metrics=document.get("metrics") or {},
|
|
175
|
+
provenance=document.get("provenance") or {},
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def write_boundary_csv(
|
|
180
|
+
path: "str | Path",
|
|
181
|
+
profile: Any,
|
|
182
|
+
*,
|
|
183
|
+
source: "str | Path | None" = None,
|
|
184
|
+
settings: Any = None,
|
|
185
|
+
) -> Path:
|
|
186
|
+
"""Write ``profile`` as a CSV table under ``#`` comment lines.
|
|
187
|
+
|
|
188
|
+
Columns ``t, y`` are fractions of one period and ``x_nm, y_nm`` the same
|
|
189
|
+
points scaled by the measured period, so a reader needs no other number to
|
|
190
|
+
plot it at true size. ``settings`` is accepted for symmetry with
|
|
191
|
+
:func:`write_boundary_json`; only the JSON records them in full.
|
|
192
|
+
"""
|
|
193
|
+
path = Path(path)
|
|
194
|
+
t, y = _points(profile)
|
|
195
|
+
period = float(profile.period_nm)
|
|
196
|
+
prov = _provenance(profile, source, None)
|
|
197
|
+
# The column header comes FIRST and the comments after it: numpy's
|
|
198
|
+
# genfromtxt(names=True) takes names from line one even when it is a
|
|
199
|
+
# comment, so a comment there containing a comma became two junk column
|
|
200
|
+
# names. Header-first reads cleanly in numpy, pandas (comment="#") and a
|
|
201
|
+
# spreadsheet, whose first row is then the column names.
|
|
202
|
+
lines = [
|
|
203
|
+
"t,y,x_nm,y_nm",
|
|
204
|
+
f"# gratinglab boundary profile written by gratinglab {prov['gratinglab_version']}",
|
|
205
|
+
f"# period_nm: {period!r}",
|
|
206
|
+
f"# grooves: {prov['grooves_averaged']} averaged of {prov['grooves_detected']} detected",
|
|
207
|
+
]
|
|
208
|
+
if prov.get("source"):
|
|
209
|
+
lines.append(f"# source: {prov['source']}")
|
|
210
|
+
lines.append(
|
|
211
|
+
"# t and y are fractions of one period; x_nm and y_nm are the same points times the period")
|
|
212
|
+
lines += [f"{ti!r},{yi!r},{ti * period!r},{yi * period!r}" for ti, yi in zip(t, y)]
|
|
213
|
+
path.write_bytes(("\n".join(lines) + "\n").encode("utf-8"))
|
|
214
|
+
return path
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def read_profile_file(path: "str | Path"):
|
|
218
|
+
"""A profile from either a gratinglab ``.json`` or a PCGrate ``.ggp``.
|
|
219
|
+
|
|
220
|
+
By suffix, because the two are unrelated formats and sniffing would only
|
|
221
|
+
turn a clear refusal into a confusing one. Only the JSON carries a period;
|
|
222
|
+
read it with :func:`read_boundary_json` to get that too.
|
|
223
|
+
"""
|
|
224
|
+
path = Path(path)
|
|
225
|
+
if path.suffix.lower() == ".json":
|
|
226
|
+
return read_boundary_json(path).profile()
|
|
227
|
+
from .ggp import read_profile
|
|
228
|
+
|
|
229
|
+
return read_profile(path)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _points(profile: Any) -> tuple[list[float], list[float]]:
|
|
233
|
+
t = [float(v) for v in np.asarray(profile.x_norm, dtype=float)]
|
|
234
|
+
y = [float(v) for v in np.asarray(profile.y_norm, dtype=float)]
|
|
235
|
+
if len(t) != len(y):
|
|
236
|
+
raise ValueError("profile x_norm and y_norm differ in length")
|
|
237
|
+
return t, y
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _provenance(profile: Any, source: "str | Path | None", settings: Any) -> dict:
|
|
241
|
+
from .. import __version__
|
|
242
|
+
|
|
243
|
+
record = {
|
|
244
|
+
"gratinglab_version": __version__,
|
|
245
|
+
"created_utc": datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
|
246
|
+
"source": None if source is None else str(source),
|
|
247
|
+
"grooves_detected": int(profile.n_grooves),
|
|
248
|
+
"grooves_averaged": int(profile.n_used),
|
|
249
|
+
"grooves_edge_rejected": int(profile.n_edge_rejected),
|
|
250
|
+
}
|
|
251
|
+
if settings is not None:
|
|
252
|
+
record["settings"] = _plain(
|
|
253
|
+
dataclasses.asdict(settings) if dataclasses.is_dataclass(settings) else dict(settings)
|
|
254
|
+
)
|
|
255
|
+
return record
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _plain(value: Any) -> Any:
|
|
259
|
+
"""JSON-safe: numpy scalars and arrays to Python, tuples to lists, NaN refused."""
|
|
260
|
+
if isinstance(value, dict):
|
|
261
|
+
return {str(k): _plain(v) for k, v in value.items()}
|
|
262
|
+
if isinstance(value, (list, tuple)):
|
|
263
|
+
return [_plain(v) for v in value]
|
|
264
|
+
if isinstance(value, np.ndarray):
|
|
265
|
+
return [_plain(v) for v in value.tolist()]
|
|
266
|
+
if isinstance(value, np.generic):
|
|
267
|
+
value = value.item()
|
|
268
|
+
if isinstance(value, float) and not math.isfinite(value):
|
|
269
|
+
return None
|
|
270
|
+
return value
|