gratinglab 0.2.1__tar.gz → 0.2.3__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.2.1/src/gratinglab.egg-info → gratinglab-0.2.3}/PKG-INFO +3 -3
- {gratinglab-0.2.1 → gratinglab-0.2.3}/pyproject.toml +2 -2
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/__init__.py +1 -1
- gratinglab-0.2.3/src/gratinglab/gui/__init__.py +30 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/app.py +4 -12
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/launcher.py +5 -13
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/qt/__init__.py +4 -5
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/qt/main_window.py +10 -9
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/qt/worker.py +15 -17
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/state.py +6 -7
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/theme.py +22 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/config.py +13 -17
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/gui/__init__.py +11 -13
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/gui/qt/__init__.py +8 -6
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/gui/qt/boundary_view.py +11 -20
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/gui/qt/canvas.py +18 -13
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/gui/qt/efficiency_dialog.py +3 -2
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/gui/qt/import_view.py +11 -21
- gratinglab-0.2.3/src/gratinglab/metrology/gui/qt/layout.py +46 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/gui/qt/main_window.py +11 -23
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/gui/qt/swath_dialog.py +3 -2
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/wiki/__init__.py +10 -20
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/solvers/base.py +8 -1
- {gratinglab-0.2.1 → gratinglab-0.2.3/src/gratinglab.egg-info}/PKG-INFO +3 -3
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab.egg-info/SOURCES.txt +1 -1
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab.egg-info/requires.txt +2 -2
- {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_gui_qt.py +35 -30
- {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_solver_progress.py +27 -0
- gratinglab-0.2.1/src/gratinglab/gui/qt/gc_guard.py +0 -102
- gratinglab-0.2.1/src/gratinglab/io/__init__.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/LICENSE +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/README.md +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/setup.cfg +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/checks.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/compare.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/convergence.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/geometry.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/cmethod_options.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/diagram.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/diagram3d.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/docs.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/efficiency_plot.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/icon.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/integral_options.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/mathtext.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/orders.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/provenance.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/qt/cmethod_tab.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/qt/geometry_panel.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/qt/geometry_tab.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/qt/integral_tab.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/qt/launcher.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/qt/profile_plot_panel.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/qt/rcwa_tab.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/qt/scalar_tab.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/qt/scene3d_panel.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/qt/setup_tab.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/qt/sizing.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/qt/theming.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/qt/theory_viewer.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/rcwa_options.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/richtext.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/gui/scalar_options.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/illumination.py +0 -0
- {gratinglab-0.2.1/src/gratinglab/gui → gratinglab-0.2.3/src/gratinglab/io}/__init__.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/io/boundary.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/io/efficiency_table.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/io/ggp.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/materials/__init__.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/materials/data/SOURCES.md +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/materials/fresnel.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/materials/optical.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/materials/refractiveindex.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/measured.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/__init__.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/analyzer.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/boundary/__init__.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/boundary/average.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/boundary/pipeline.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/boundary/swaths.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/cli.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/core/__init__.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/core/analysis.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/core/image_flatten.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/core/orientation.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/core/processing.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/core/region_mask.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/core/scars.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/core/tip.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/data/__init__.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/data/aggregation.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/gui/icon.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/gui/qt/app.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/gui/qt/wiki_view.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/gui/qt/worker.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/gui/state.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/io/__init__.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/io/file_io.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/io/ibw.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/io/metrics.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/io/spm.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/settings.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/stats/__init__.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/stats/analysis.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/stats/icc.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/visualization/__init__.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/visualization/diagnostics.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/visualization/profiles.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/visualization/statistics.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/wiki/facet-fitting.md +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/wiki/file-formats.md +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/wiki/flattening.md +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/wiki/groove-orientation.md +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/wiki/icc-correction.md +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/wiki/overview.md +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/wiki/reading-outputs.md +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/wiki/row-groups.md +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/wiki/scars-and-masking.md +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/wiki/swath-statistics.md +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/wiki/tip-correction.md +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/metrology/workflows.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/problem.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/profiles.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/resolution.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/result.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/solvers/__init__.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/solvers/_conditioning.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/solvers/_execution.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/solvers/cmethod/__init__.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/solvers/cmethod/_core.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/solvers/cmethod/_series.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/solvers/integral/__init__.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/solvers/integral/_boundary.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/solvers/integral/_core.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/solvers/integral/_finite.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/solvers/integral/_greens.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/solvers/integral/_kernels.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/solvers/integral/_nystrom.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/solvers/rcwa/__init__.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/solvers/rcwa/_core.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/solvers/rcwa/_stack.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab/solvers/scalar.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab.egg-info/dependency_links.txt +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab.egg-info/entry_points.txt +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/src/gratinglab.egg-info/top_level.txt +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_changelog.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_checks.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_cmethod.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_compare.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_conditioning.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_convergence.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_cross_method.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_efficiency_table.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_fresnel.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_geometry.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_ggp.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_gui_cmethod_options.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_gui_cmethod_tab.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_gui_diagram.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_gui_diagram3d.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_gui_docs.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_gui_integral_options.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_gui_launcher.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_gui_mathtext.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_gui_orders.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_gui_provenance.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_gui_rcwa_options.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_gui_richtext.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_gui_scalar_options.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_gui_state.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_gui_theme.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_illumination.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_integral_core.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_integral_corpus.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_integral_finite.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_integral_graded.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_integral_greens.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_integral_kernels.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_integral_operators.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_integral_solver.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_io_boundary.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_materials.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_measured.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_perturbation.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_profiles.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_rayleigh_anomaly.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_rcwa.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_rcwa_benchmarks.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_refractiveindex.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_resolution.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_result.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_scalar.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_solver_conformance.py +0 -0
- {gratinglab-0.2.1 → gratinglab-0.2.3}/tests/test_solver_execution.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gratinglab
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Open platform for diffraction grating groove metrology and efficiency analysis
|
|
5
5
|
Author: Jake McCoy, Aditi Gangadharan
|
|
6
6
|
License: BSD-3-Clause
|
|
@@ -22,7 +22,7 @@ License-File: LICENSE
|
|
|
22
22
|
Requires-Dist: numpy>=1.26
|
|
23
23
|
Requires-Dist: scipy>=1.11
|
|
24
24
|
Requires-Dist: pydantic>=2.6
|
|
25
|
-
Requires-Dist: endstation>=0.2.
|
|
25
|
+
Requires-Dist: endstation>=0.2.6
|
|
26
26
|
Requires-Dist: threadpoolctl>=3.1
|
|
27
27
|
Provides-Extra: dev
|
|
28
28
|
Requires-Dist: pytest>=7; extra == "dev"
|
|
@@ -34,7 +34,7 @@ Provides-Extra: gui
|
|
|
34
34
|
Requires-Dist: matplotlib>=3.8; extra == "gui"
|
|
35
35
|
Requires-Dist: PySide6-Essentials!=6.12.0,>=6.7; python_version < "3.12" and extra == "gui"
|
|
36
36
|
Requires-Dist: PySide6-Essentials>=6.7; python_version >= "3.12" and extra == "gui"
|
|
37
|
-
Requires-Dist: endstation[qt]>=0.2.
|
|
37
|
+
Requires-Dist: endstation[qt]>=0.2.6; extra == "gui"
|
|
38
38
|
Provides-Extra: metrology
|
|
39
39
|
Requires-Dist: matplotlib>=3.9; extra == "metrology"
|
|
40
40
|
Provides-Extra: refractiveindex
|
|
@@ -51,7 +51,7 @@ dependencies = [
|
|
|
51
51
|
# `dependencies = []` and has a test that fails if that stops being true,
|
|
52
52
|
# so this line adds no transitive weight -- the Qt and matplotlib parts sit
|
|
53
53
|
# behind its own extras, which our `gui` extra asks for by name.
|
|
54
|
-
"endstation>=0.2.
|
|
54
|
+
"endstation>=0.2.6",
|
|
55
55
|
# The rigorous solvers cap BLAS threads for the length of a solve
|
|
56
56
|
# (solvers/_execution.py): OpenBLAS's thread-per-core default made RCWA
|
|
57
57
|
# up to 135x slower on a busy machine, measured in benchmarks/
|
|
@@ -85,7 +85,7 @@ gui = [
|
|
|
85
85
|
"matplotlib>=3.8",
|
|
86
86
|
"PySide6-Essentials>=6.7,!=6.12.0; python_version < '3.12'",
|
|
87
87
|
"PySide6-Essentials>=6.7; python_version >= '3.12'",
|
|
88
|
-
"endstation[qt]>=0.2.
|
|
88
|
+
"endstation[qt]>=0.2.6",
|
|
89
89
|
]
|
|
90
90
|
# Groove metrology: reading AFM scans and averaging them into a boundary
|
|
91
91
|
# profile. An extra for the same reason the GUI is one -- `pip install
|
|
@@ -22,7 +22,7 @@ from .measured import MeasuredEfficiency, MeasurementProvenance
|
|
|
22
22
|
from .resolution import ResolvingPower, resolving_power
|
|
23
23
|
from .result import EfficiencyScan, OrderEfficiency, Provenance
|
|
24
24
|
|
|
25
|
-
__version__ = "0.2.
|
|
25
|
+
__version__ = "0.2.3"
|
|
26
26
|
|
|
27
27
|
__all__ = [
|
|
28
28
|
"Illumination",
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""The desktop layer: two windows and the chooser that opens them.
|
|
2
|
+
|
|
3
|
+
Only :mod:`gratinglab.gui.qt` may import a toolkit. This module stays
|
|
4
|
+
toolkit-free so the console-script entry points, which live beside it, can
|
|
5
|
+
explain a missing PySide6 before anything tries to import it.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from endstation.optional import MissingExtra, require_extra
|
|
11
|
+
|
|
12
|
+
__all__ = ["STILL_WORKS", "exit_without_qt"]
|
|
13
|
+
|
|
14
|
+
#: What a missing toolkit leaves working. Without it, a user who wanted only
|
|
15
|
+
#: the solvers reads an absent GUI extra as a broken install.
|
|
16
|
+
STILL_WORKS = "Everything except the GUI works without it."
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def exit_without_qt() -> None:
|
|
20
|
+
"""Exit with an install instruction if PySide6 is not installed.
|
|
21
|
+
|
|
22
|
+
The check is :func:`endstation.optional.require_extra`, a ``find_spec``
|
|
23
|
+
call rather than an import. Its :class:`~endstation.optional.MissingExtra`
|
|
24
|
+
becomes ``SystemExit`` here, so a console script prints the message and
|
|
25
|
+
not a traceback naming a package the user never asked for.
|
|
26
|
+
"""
|
|
27
|
+
try:
|
|
28
|
+
require_extra("PySide6", "gui", still_works=STILL_WORKS)
|
|
29
|
+
except MissingExtra as exc:
|
|
30
|
+
raise SystemExit(str(exc)) from exc
|
|
@@ -15,22 +15,14 @@ from __future__ import annotations
|
|
|
15
15
|
|
|
16
16
|
import sys
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
gratinglab-gui needs PySide6, which this install does not have.
|
|
20
|
-
|
|
21
|
-
pip install -e ".[gui]" (from a checkout)
|
|
22
|
-
pip install gratinglab[gui] (once published)
|
|
23
|
-
|
|
24
|
-
Everything except the GUI works without it.
|
|
25
|
-
"""
|
|
18
|
+
from . import exit_without_qt
|
|
26
19
|
|
|
27
20
|
|
|
28
21
|
def _require_qt():
|
|
29
22
|
"""Import PySide6 with an explanation instead of a traceback."""
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
raise SystemExit(_QT_HELP) from exc
|
|
23
|
+
exit_without_qt()
|
|
24
|
+
from PySide6.QtWidgets import QApplication
|
|
25
|
+
|
|
34
26
|
return QApplication
|
|
35
27
|
|
|
36
28
|
|
|
@@ -28,16 +28,9 @@ from __future__ import annotations
|
|
|
28
28
|
import sys
|
|
29
29
|
from dataclasses import dataclass
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
_QT_HELP = """\
|
|
34
|
-
gratinglab-app needs PySide6, which this install does not have.
|
|
35
|
-
|
|
36
|
-
pip install -e ".[gui]" (from a checkout)
|
|
37
|
-
pip install gratinglab[gui]
|
|
31
|
+
from . import exit_without_qt
|
|
38
32
|
|
|
39
|
-
|
|
40
|
-
"""
|
|
33
|
+
__all__ = ["Mode", "MODES", "mode", "main"]
|
|
41
34
|
|
|
42
35
|
|
|
43
36
|
@dataclass(frozen=True)
|
|
@@ -91,10 +84,9 @@ def mode(key: str) -> Mode:
|
|
|
91
84
|
|
|
92
85
|
def _require_qt():
|
|
93
86
|
"""Import PySide6 with an explanation instead of a traceback."""
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
raise SystemExit(_QT_HELP) from exc
|
|
87
|
+
exit_without_qt()
|
|
88
|
+
from PySide6.QtWidgets import QApplication
|
|
89
|
+
|
|
98
90
|
return QApplication
|
|
99
91
|
|
|
100
92
|
|
|
@@ -14,9 +14,8 @@ headlessly.
|
|
|
14
14
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
|
-
import os
|
|
18
|
-
|
|
19
17
|
# matplotlib's Qt shim picks a binding from sys.modules first and QT_API
|
|
20
|
-
# second. Without
|
|
21
|
-
# canvas would refuse to parent into a PySide6 window.
|
|
22
|
-
|
|
18
|
+
# second. Without QT_API, a stray PyQt5 in the environment would win and its
|
|
19
|
+
# canvas would refuse to parent into a PySide6 window. Importing endstation.qt
|
|
20
|
+
# sets it, with setdefault, before anything here imports matplotlib.
|
|
21
|
+
import endstation.qt # noqa: F401
|
|
@@ -25,6 +25,7 @@ from __future__ import annotations
|
|
|
25
25
|
|
|
26
26
|
import threading
|
|
27
27
|
|
|
28
|
+
from endstation.qt.tokens import StaleGuard
|
|
28
29
|
from PySide6.QtCore import Qt, QThread, QTimer, Signal
|
|
29
30
|
from PySide6.QtGui import QKeySequence
|
|
30
31
|
from PySide6.QtWidgets import (
|
|
@@ -131,12 +132,12 @@ class MainWindow(QMainWindow):
|
|
|
131
132
|
|
|
132
133
|
self._themes = theming.install(QApplication.instance())
|
|
133
134
|
# Solves run on a worker thread; keep the cycle collector off it.
|
|
134
|
-
from .gc_guard import guard
|
|
135
|
+
from endstation.qt.gc_guard import guard
|
|
135
136
|
|
|
136
137
|
guard(self)
|
|
137
138
|
|
|
138
139
|
self._parsed = None
|
|
139
|
-
self.
|
|
140
|
+
self._tokens = StaleGuard()
|
|
140
141
|
self._running = False
|
|
141
142
|
self._active_name: str | None = None
|
|
142
143
|
# Replaced per solve rather than reused, so a Cancel clicked between
|
|
@@ -441,7 +442,7 @@ class MainWindow(QMainWindow):
|
|
|
441
442
|
|
|
442
443
|
self._active_name = name
|
|
443
444
|
self._parsed = geometry
|
|
444
|
-
self.
|
|
445
|
+
token = self._tokens.issue()
|
|
445
446
|
self._cancel = threading.Event()
|
|
446
447
|
self._set_running(True)
|
|
447
448
|
tab.show_solving(len(geometry.wavelengths))
|
|
@@ -455,7 +456,7 @@ class MainWindow(QMainWindow):
|
|
|
455
456
|
_PROGRESS_DELAY_MS, tab, lambda: self._show_progress_if_still_running(tab)
|
|
456
457
|
)
|
|
457
458
|
signal = self._convergence_requested if sweep else self._requested
|
|
458
|
-
signal.emit(
|
|
459
|
+
signal.emit(token, name, geometry, options, self._cancel)
|
|
459
460
|
|
|
460
461
|
def cancel(self) -> None:
|
|
461
462
|
"""Stop the running solve.
|
|
@@ -475,13 +476,13 @@ class MainWindow(QMainWindow):
|
|
|
475
476
|
if not self._running:
|
|
476
477
|
return
|
|
477
478
|
self._cancel.set()
|
|
478
|
-
self.
|
|
479
|
+
self._tokens.invalidate()
|
|
479
480
|
self._set_running(False)
|
|
480
481
|
if self._active_name is not None:
|
|
481
482
|
self.tabs[self._active_name].show_cancelled()
|
|
482
483
|
|
|
483
484
|
def _on_solved(self, token: int, method: str, result) -> None:
|
|
484
|
-
if
|
|
485
|
+
if self._tokens.is_stale(token):
|
|
485
486
|
return # cancelled or superseded; the window has moved on
|
|
486
487
|
self._set_running(False)
|
|
487
488
|
# No `profile_panel.draw` here any more: the groove's shape owes
|
|
@@ -501,7 +502,7 @@ class MainWindow(QMainWindow):
|
|
|
501
502
|
be enough to move a bar backwards over a result the window has already
|
|
502
503
|
replaced.
|
|
503
504
|
"""
|
|
504
|
-
if
|
|
505
|
+
if self._tokens.is_stale(token) or self._active_name is None:
|
|
505
506
|
return
|
|
506
507
|
self.tabs[self._active_name].show_progress_value(done, total)
|
|
507
508
|
|
|
@@ -515,7 +516,7 @@ class MainWindow(QMainWindow):
|
|
|
515
516
|
"""
|
|
516
517
|
|
|
517
518
|
def _on_converged(self, token: int, method: str, result, report) -> None:
|
|
518
|
-
if
|
|
519
|
+
if self._tokens.is_stale(token):
|
|
519
520
|
return
|
|
520
521
|
self._set_running(False)
|
|
521
522
|
self.geometry_tab.show_geometry(self._parsed)
|
|
@@ -525,7 +526,7 @@ class MainWindow(QMainWindow):
|
|
|
525
526
|
self.solved.emit()
|
|
526
527
|
|
|
527
528
|
def _on_failed(self, token: int, method: str, message: str) -> None:
|
|
528
|
-
if
|
|
529
|
+
if self._tokens.is_stale(token):
|
|
529
530
|
return
|
|
530
531
|
self._set_running(False)
|
|
531
532
|
self.tabs[method].show_error(message)
|
|
@@ -10,7 +10,8 @@ only the latter, because ``sweep`` reaches a NumPy-bound call with no check
|
|
|
10
10
|
point and a Python thread cannot be killed, so there was nothing to interrupt
|
|
11
11
|
from here. `Capabilities.reports_progress` changed that: the solver calls back
|
|
12
12
|
once per wavelength, and a callback that raises
|
|
13
|
-
:class:`~
|
|
13
|
+
:class:`~endstation.progress.Cancelled` (or its subclass
|
|
14
|
+
:class:`~gratinglab.solvers.base.SolveCancelled`) unwinds it.
|
|
14
15
|
|
|
15
16
|
The token is still needed, because real cancellation does not remove the race
|
|
16
17
|
it was covering. The worker may already have finished when Cancel is clicked,
|
|
@@ -28,10 +29,9 @@ from __future__ import annotations
|
|
|
28
29
|
import threading
|
|
29
30
|
from typing import TYPE_CHECKING, NamedTuple
|
|
30
31
|
|
|
32
|
+
from endstation.progress import Cancelled, cancelling, throttled
|
|
31
33
|
from PySide6.QtCore import QObject, Signal, Slot
|
|
32
34
|
|
|
33
|
-
from ...solvers.base import SolveCancelled
|
|
34
|
-
|
|
35
35
|
if TYPE_CHECKING: # pragma: no cover - imports for type checkers only
|
|
36
36
|
from ...checks import EnergyReport
|
|
37
37
|
from ...result import EfficiencyScan
|
|
@@ -123,7 +123,7 @@ class SolveWorker(QObject):
|
|
|
123
123
|
progress=self._reporter(token, cancel),
|
|
124
124
|
)[0]
|
|
125
125
|
self.finished.emit(token, method, SolveResult(scan, _energy(scan)))
|
|
126
|
-
except
|
|
126
|
+
except Cancelled:
|
|
127
127
|
# Before the broad handler: the user asked for this, so
|
|
128
128
|
# it is not a failure and rendering it as one would be the same
|
|
129
129
|
# category mistake M8 fixed for the no-coating default.
|
|
@@ -176,7 +176,7 @@ class SolveWorker(QObject):
|
|
|
176
176
|
SolveResult(report.scan, _energy(report.scan)),
|
|
177
177
|
report,
|
|
178
178
|
)
|
|
179
|
-
except
|
|
179
|
+
except Cancelled:
|
|
180
180
|
self.cancelled.emit(token, method)
|
|
181
181
|
except Exception as exc: # noqa: BLE001 - see `run`
|
|
182
182
|
self.failed.emit(token, method, f"{type(exc).__name__}: {exc}")
|
|
@@ -190,16 +190,14 @@ class SolveWorker(QObject):
|
|
|
190
190
|
that could not have been seen. The cancellation check is not
|
|
191
191
|
throttled: it runs on every call, so a Cancel click takes effect at
|
|
192
192
|
the next wavelength instead of the next percent.
|
|
193
|
+
|
|
194
|
+
Both halves are endstation's: :func:`~endstation.progress.throttled`
|
|
195
|
+
for the bar and :func:`~endstation.progress.cancelling` for the stop,
|
|
196
|
+
which raises :class:`~endstation.progress.Cancelled`, the base class
|
|
197
|
+
of :class:`~gratinglab.solvers.base.SolveCancelled`.
|
|
193
198
|
"""
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if percent != last:
|
|
200
|
-
last = percent
|
|
201
|
-
self.progress.emit(token, done, total)
|
|
202
|
-
if cancel.is_set():
|
|
203
|
-
raise SolveCancelled(f"cancelled at {done} of {total}")
|
|
204
|
-
|
|
205
|
-
return report
|
|
199
|
+
|
|
200
|
+
def emit(done: int, total: int) -> None:
|
|
201
|
+
self.progress.emit(token, done, total)
|
|
202
|
+
|
|
203
|
+
return cancelling(throttled(emit), cancel.is_set)
|
|
@@ -20,6 +20,7 @@ from __future__ import annotations
|
|
|
20
20
|
from dataclasses import dataclass, replace
|
|
21
21
|
|
|
22
22
|
from endstation.forms import FieldError as _FieldError
|
|
23
|
+
from endstation.forms import FormErrors as _FormErrors
|
|
23
24
|
from pathlib import Path
|
|
24
25
|
from typing import Any
|
|
25
26
|
|
|
@@ -79,13 +80,11 @@ ANGLE_LABELS: dict[str, tuple[str, str | None]] = {
|
|
|
79
80
|
#: from the module it is used in.
|
|
80
81
|
FieldError = _FieldError
|
|
81
82
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
self.errors = errors
|
|
88
|
-
super().__init__("; ".join(str(e) for e in errors))
|
|
83
|
+
#: Raised by :func:`build`, and by each tab's ``build_options``, when a form
|
|
84
|
+
#: cannot be turned into a problem. endstation's, re-exported for the same
|
|
85
|
+
#: reason as `FieldError`. It derives from ``Exception``, not ``ValueError``
|
|
86
|
+
#: as this module's own copy did; nothing here caught it as a ValueError.
|
|
87
|
+
FormErrors = _FormErrors
|
|
89
88
|
|
|
90
89
|
|
|
91
90
|
@dataclass(frozen=True, slots=True)
|
|
@@ -37,6 +37,7 @@ __all__ = [
|
|
|
37
37
|
"order_colors",
|
|
38
38
|
"provenance_colors",
|
|
39
39
|
"rc_params",
|
|
40
|
+
"set_titles",
|
|
40
41
|
"style_axes",
|
|
41
42
|
"style_figure",
|
|
42
43
|
]
|
|
@@ -308,6 +309,27 @@ def style_axes(axes, theme: Theme) -> None:
|
|
|
308
309
|
axis.line.set_color(theme.ink)
|
|
309
310
|
|
|
310
311
|
|
|
312
|
+
#: Plot titles: what the panel is, at this size and weight, left-aligned.
|
|
313
|
+
TITLE_SIZE = 11
|
|
314
|
+
TITLE_WEIGHT = "semibold"
|
|
315
|
+
#: The figures a panel reports beside its title, smaller and in ``muted``.
|
|
316
|
+
DETAIL_SIZE = 9
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def set_titles(axes, title: str, detail: str | None = None, theme: Theme | None = None) -> None:
|
|
320
|
+
"""Title a panel: its name on the left, the numbers it reports on the right.
|
|
321
|
+
|
|
322
|
+
One style for every plot, so a title says what the panel is and never has
|
|
323
|
+
to carry a measurement, a setting or an explanation on a second line. The
|
|
324
|
+
detail is where those go, quieter than the title. Pass ``theme`` from a
|
|
325
|
+
caller that has one; otherwise the plain ``LIGHT`` colours are used.
|
|
326
|
+
"""
|
|
327
|
+
theme = theme or LIGHT
|
|
328
|
+
axes.set_title(title, loc="left", fontsize=TITLE_SIZE, fontweight=TITLE_WEIGHT,
|
|
329
|
+
color=theme.ink)
|
|
330
|
+
axes.set_title(detail or "", loc="right", fontsize=DETAIL_SIZE, color=theme.muted)
|
|
331
|
+
|
|
332
|
+
|
|
311
333
|
def style_figure(figure, theme: Theme) -> None:
|
|
312
334
|
"""Restyle an existing figure and every axes on it."""
|
|
313
335
|
figure.set_facecolor(theme.figure)
|
|
@@ -3,6 +3,8 @@ Configuration file for AFM blaze angle analysis
|
|
|
3
3
|
"""
|
|
4
4
|
import os
|
|
5
5
|
|
|
6
|
+
from endstation.paths import data_root, find_project_root
|
|
7
|
+
|
|
6
8
|
# ============ PROJECT PATHS ============
|
|
7
9
|
# All paths below are resolved relative to the project root, not the current
|
|
8
10
|
# working directory, so the analysis runs the same from anywhere.
|
|
@@ -16,22 +18,16 @@ import os
|
|
|
16
18
|
# An installed (non-editable) copy has no project root above it; fall back to the
|
|
17
19
|
# working directory so `pip install gratinglab[metrology]` still runs against a user's
|
|
18
20
|
# own data and results folders.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if parent == path:
|
|
30
|
-
return os.getcwd()
|
|
31
|
-
path = parent
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
PROJECT_ROOT = _find_project_root(__file__)
|
|
21
|
+
#
|
|
22
|
+
# pyproject.toml only, not endstation's default markers. Searching for a "data"
|
|
23
|
+
# directory looked reasonable and was wrong: this package contains its own data
|
|
24
|
+
# subpackage (gratinglab/metrology/data/aggregation.py), so the walk stopped
|
|
25
|
+
# immediately and DATA_DIR pointed inside the source tree. A `.git` marker is
|
|
26
|
+
# no better: an installed copy inside some other checkout would claim that
|
|
27
|
+
# checkout's root.
|
|
28
|
+
#
|
|
29
|
+
# Kept as a str, as it always was: every consumer builds paths with os.path.
|
|
30
|
+
PROJECT_ROOT = str(find_project_root(__file__, markers=("pyproject.toml",)))
|
|
35
31
|
RESULTS_DIR = os.path.join(PROJECT_ROOT, 'results')
|
|
36
32
|
|
|
37
33
|
# Scans live *outside* the checkout, and are not committed.
|
|
@@ -45,7 +41,7 @@ RESULTS_DIR = os.path.join(PROJECT_ROOT, 'results')
|
|
|
45
41
|
#
|
|
46
42
|
# Point GRATINGLAB_AFM_DIR somewhere else to override.
|
|
47
43
|
DEFAULT_AFM_DIR = os.path.join(os.path.expanduser('~'), 'Documents', 'afm_scans')
|
|
48
|
-
DATA_DIR =
|
|
44
|
+
DATA_DIR = str(data_root('GRATINGLAB_AFM_DIR', DEFAULT_AFM_DIR))
|
|
49
45
|
|
|
50
46
|
|
|
51
47
|
def resolve_path(name):
|
|
@@ -23,15 +23,16 @@ Importing `gui.qt` requires PySide6, which is an optional extra::
|
|
|
23
23
|
"""
|
|
24
24
|
from __future__ import annotations
|
|
25
25
|
|
|
26
|
+
from endstation.optional import missing_extra_message, require_extra
|
|
27
|
+
|
|
26
28
|
__all__ = ["main", "require_qt", "QT_MISSING_MESSAGE"]
|
|
27
29
|
|
|
28
|
-
|
|
29
|
-
"the Qt interface needs PySide6, which is an optional extra.\n"
|
|
30
|
-
"Install it with:\n\n"
|
|
31
|
-
" pip install -e '.[gui]'\n\n"
|
|
30
|
+
_STILL_WORKS = (
|
|
32
31
|
"The analysis, the .ggp export and the ICC diagnostic all work without it."
|
|
33
32
|
)
|
|
34
33
|
|
|
34
|
+
QT_MISSING_MESSAGE = missing_extra_message("PySide6", "gui", still_works=_STILL_WORKS)
|
|
35
|
+
|
|
35
36
|
|
|
36
37
|
def require_qt() -> None:
|
|
37
38
|
"""Raise something useful rather than a bare ImportError.
|
|
@@ -39,16 +40,13 @@ def require_qt() -> None:
|
|
|
39
40
|
``ModuleNotFoundError: No module named 'PySide6'`` tells a user nothing about
|
|
40
41
|
what to do, and this is the most likely first-run failure.
|
|
41
42
|
|
|
42
|
-
The check
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
The check is :func:`endstation.optional.require_extra`, a ``find_spec``
|
|
44
|
+
call rather than an import, because this module sits outside `gui/qt` and
|
|
45
|
+
the rule that only `gui/qt` touches a toolkit is enforced by
|
|
46
|
+
`tests/test_gui_boundary.py`. It raises
|
|
47
|
+
:class:`~endstation.optional.MissingExtra`, a ``ModuleNotFoundError``.
|
|
47
48
|
"""
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if find_spec("PySide6") is None: # pragma: no cover - depends on environment
|
|
51
|
-
raise ModuleNotFoundError(QT_MISSING_MESSAGE)
|
|
49
|
+
require_extra("PySide6", "gui", still_works=_STILL_WORKS)
|
|
52
50
|
|
|
53
51
|
|
|
54
52
|
def main(argv=None):
|
|
@@ -10,10 +10,12 @@ it belongs in `gui/state.py`, which is pure and tested headlessly.
|
|
|
10
10
|
"""
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
|
-
import os
|
|
14
|
-
|
|
15
13
|
# matplotlib's Qt shim picks a binding from sys.modules first and QT_API second.
|
|
16
|
-
# Without
|
|
17
|
-
# refuse to parent into a PySide6 window. This project shipped PyQt5
|
|
18
|
-
# recently, so that stray binding is a realistic thing to find installed.
|
|
19
|
-
|
|
14
|
+
# Without QT_API, a stray PyQt5 in the environment would win and its canvas
|
|
15
|
+
# would refuse to parent into a PySide6 window. This project shipped PyQt5
|
|
16
|
+
# until recently, so that stray binding is a realistic thing to find installed.
|
|
17
|
+
# Importing endstation.qt sets it, with setdefault; the modules here run
|
|
18
|
+
# `from . import *` first thing to get this import, so it exports nothing.
|
|
19
|
+
import endstation.qt # noqa: F401
|
|
20
|
+
|
|
21
|
+
__all__: list[str] = []
|
|
@@ -21,14 +21,16 @@ import os
|
|
|
21
21
|
from . import * # noqa: F401,F403 - sets QT_API before matplotlib's shim loads
|
|
22
22
|
|
|
23
23
|
from PySide6.QtWidgets import (
|
|
24
|
-
QCheckBox, QFileDialog,
|
|
25
|
-
QMessageBox, QPushButton,
|
|
24
|
+
QCheckBox, QFileDialog, QGroupBox, QHBoxLayout, QLabel,
|
|
25
|
+
QMessageBox, QPushButton, QSpinBox, QVBoxLayout, QWidget,
|
|
26
26
|
)
|
|
27
27
|
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
|
|
28
28
|
from matplotlib.figure import Figure
|
|
29
29
|
|
|
30
30
|
from gratinglab.gui.qt import theming
|
|
31
|
-
from gratinglab.gui.theme import style_figure
|
|
31
|
+
from gratinglab.gui.theme import set_titles, style_figure
|
|
32
|
+
|
|
33
|
+
from .layout import controls_beside
|
|
32
34
|
|
|
33
35
|
from ...boundary import build_boundary_profile
|
|
34
36
|
from ...core.tip import format_angles
|
|
@@ -65,7 +67,6 @@ class BoundaryView(QWidget):
|
|
|
65
67
|
root.setSpacing(8)
|
|
66
68
|
|
|
67
69
|
left = QWidget()
|
|
68
|
-
left.setFixedWidth(272)
|
|
69
70
|
column = QVBoxLayout(left)
|
|
70
71
|
column.setContentsMargins(0, 0, 0, 0)
|
|
71
72
|
column.setSpacing(8)
|
|
@@ -73,23 +74,12 @@ class BoundaryView(QWidget):
|
|
|
73
74
|
column.addWidget(self._build_metrics_group())
|
|
74
75
|
column.addStretch()
|
|
75
76
|
|
|
76
|
-
scroll = QScrollArea()
|
|
77
|
-
scroll.setWidget(left)
|
|
78
|
-
scroll.setWidgetResizable(True)
|
|
79
|
-
scroll.setFixedWidth(292)
|
|
80
|
-
scroll.setFrameShape(QFrame.NoFrame)
|
|
81
|
-
|
|
82
77
|
self.figure = Figure(figsize=(8, 6), tight_layout=True)
|
|
83
78
|
self.canvas = FigureCanvas(self.figure)
|
|
84
79
|
self._show_placeholder()
|
|
85
80
|
theming.subscribe(self._on_theme)
|
|
86
81
|
|
|
87
|
-
root.addWidget(
|
|
88
|
-
divider = QFrame()
|
|
89
|
-
divider.setFrameShape(QFrame.VLine)
|
|
90
|
-
divider.setFrameShadow(QFrame.Sunken)
|
|
91
|
-
root.addWidget(divider)
|
|
92
|
-
root.addWidget(self.canvas, stretch=1)
|
|
82
|
+
root.addWidget(controls_beside(left, self.canvas), stretch=1)
|
|
93
83
|
|
|
94
84
|
def _build_export_group(self):
|
|
95
85
|
group = QGroupBox("Boundary profile")
|
|
@@ -327,7 +317,8 @@ class BoundaryView(QWidget):
|
|
|
327
317
|
alpha=0.8 if dark else 0.6)
|
|
328
318
|
avg_ax.set_xlabel("Position (nm)", fontsize=10)
|
|
329
319
|
avg_ax.set_ylabel("Height (nm)", fontsize=10)
|
|
330
|
-
avg_ax
|
|
320
|
+
set_titles(avg_ax, "Averaged groove", f"{p.n_used} grooves",
|
|
321
|
+
theming.current())
|
|
331
322
|
avg_ax.legend(fontsize=8)
|
|
332
323
|
avg_ax.grid(True, alpha=0.3)
|
|
333
324
|
|
|
@@ -336,9 +327,9 @@ class BoundaryView(QWidget):
|
|
|
336
327
|
norm_ax.axhline(0, color=theming.current().ink, linestyle='--', linewidth=0.5)
|
|
337
328
|
norm_ax.set_xlabel("Normalised position (0 to 1 = one period)", fontsize=10)
|
|
338
329
|
norm_ax.set_ylabel("Height (fraction of period)", fontsize=10)
|
|
339
|
-
norm_ax
|
|
340
|
-
|
|
341
|
-
|
|
330
|
+
set_titles(norm_ax, "Exported profile",
|
|
331
|
+
f"{len(p.x_norm)} points, depth {p.metrics['groove_depth']:.4f} "
|
|
332
|
+
"of period", theming.current())
|
|
342
333
|
norm_ax.grid(True, alpha=0.3)
|
|
343
334
|
|
|
344
335
|
self.canvas.draw()
|
|
@@ -17,7 +17,7 @@ from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
|
|
|
17
17
|
from matplotlib.figure import Figure
|
|
18
18
|
|
|
19
19
|
from gratinglab.gui.qt import theming
|
|
20
|
-
from gratinglab.gui.theme import style_figure
|
|
20
|
+
from gratinglab.gui.theme import set_titles, style_figure
|
|
21
21
|
|
|
22
22
|
__all__ = ["PlotCanvas"]
|
|
23
23
|
|
|
@@ -71,7 +71,8 @@ class PlotCanvas(FigureCanvas):
|
|
|
71
71
|
ax.plot(disp_um, profile_nm, color='steelblue', linewidth=1.0)
|
|
72
72
|
ax.set_xlabel("Position (µm)", fontsize=11)
|
|
73
73
|
ax.set_ylabel("Height (nm)", fontsize=11)
|
|
74
|
-
ax
|
|
74
|
+
set_titles(ax, "Raw profile", f"scan width {scan_size_um:.3f} µm",
|
|
75
|
+
theming.current())
|
|
75
76
|
ax.grid(True, alpha=0.3)
|
|
76
77
|
self.draw()
|
|
77
78
|
|
|
@@ -84,7 +85,8 @@ class PlotCanvas(FigureCanvas):
|
|
|
84
85
|
self.fig.colorbar(im, ax=ax, label='Height (nm)')
|
|
85
86
|
ax.set_xlabel("X (µm)", fontsize=11)
|
|
86
87
|
ax.set_ylabel("Scan line", fontsize=11)
|
|
87
|
-
ax
|
|
88
|
+
set_titles(ax, "Topography", f"{data.shape[0]} scan lines",
|
|
89
|
+
theming.current())
|
|
88
90
|
self.draw()
|
|
89
91
|
|
|
90
92
|
@_remembered
|
|
@@ -102,9 +104,9 @@ class PlotCanvas(FigureCanvas):
|
|
|
102
104
|
linewidth=1.8, label=f'Mean (n={n_groups})', zorder=5)
|
|
103
105
|
ax.set_xlabel("Position (µm)", fontsize=11)
|
|
104
106
|
ax.set_ylabel("Height (nm)", fontsize=11)
|
|
105
|
-
ax
|
|
106
|
-
|
|
107
|
-
|
|
107
|
+
set_titles(ax, "Row-group profiles",
|
|
108
|
+
f"{n_groups} groups of {group_info['rows_per_group']} rows, "
|
|
109
|
+
f"scan width {scan_size_um:.3f} µm", theming.current())
|
|
108
110
|
ax.legend(fontsize=10)
|
|
109
111
|
ax.grid(True, alpha=0.3)
|
|
110
112
|
|
|
@@ -140,7 +142,8 @@ class PlotCanvas(FigureCanvas):
|
|
|
140
142
|
|
|
141
143
|
ax.set_xlabel("Position (µm)", fontsize=11)
|
|
142
144
|
ax.set_ylabel("Height (nm)", fontsize=11)
|
|
143
|
-
ax
|
|
145
|
+
set_titles(ax, "Groove detection", f"{len(centers)} grooves",
|
|
146
|
+
theming.current())
|
|
144
147
|
ax.legend(fontsize=9)
|
|
145
148
|
ax.grid(True, alpha=0.3)
|
|
146
149
|
self.draw()
|
|
@@ -160,8 +163,8 @@ class PlotCanvas(FigureCanvas):
|
|
|
160
163
|
label=f'Mean {mean:.2f}°')
|
|
161
164
|
ax1.set_xlabel("Blaze angle (°)", fontsize=10)
|
|
162
165
|
ax1.set_ylabel("Count", fontsize=10)
|
|
163
|
-
ax1
|
|
164
|
-
|
|
166
|
+
set_titles(ax1, "Distribution",
|
|
167
|
+
f"N = {len(angles)}, σ = {result['std_angle']:.2f}°", theming.current())
|
|
165
168
|
ax1.legend(fontsize=9)
|
|
166
169
|
ax1.grid(True, alpha=0.3)
|
|
167
170
|
|
|
@@ -169,16 +172,18 @@ class PlotCanvas(FigureCanvas):
|
|
|
169
172
|
ax2.hist(r2, bins=20, edgecolor=theming.current().ink, alpha=0.75, color='seagreen')
|
|
170
173
|
ax2.set_xlabel("Fit R²", fontsize=10)
|
|
171
174
|
ax2.set_ylabel("Count", fontsize=10)
|
|
172
|
-
ax2
|
|
175
|
+
set_titles(ax2, "Fit quality", f"worst R² {np.nanmin(r2):.4f}",
|
|
176
|
+
theming.current())
|
|
173
177
|
ax2.grid(True, alpha=0.3)
|
|
174
178
|
|
|
175
179
|
widths = np.array([q.get('blaze_width_nm', np.nan) for q in quality])
|
|
176
180
|
ax3.plot(widths, angles, 'o', markersize=4, alpha=0.45, color='darkorange')
|
|
177
181
|
ax3.set_xlabel("Blaze facet width (nm)", fontsize=10)
|
|
178
182
|
ax3.set_ylabel("Blaze angle (°)", fontsize=10)
|
|
179
|
-
ax3
|
|
180
|
-
|
|
183
|
+
set_titles(ax3, "Angle against facet width", "narrow facets fit poorly",
|
|
184
|
+
theming.current())
|
|
181
185
|
ax3.grid(True, alpha=0.3)
|
|
182
186
|
|
|
183
|
-
self.fig.suptitle(f"Blaze angle {mean:.2f}° ± {sem:.3f}° (SEM)",
|
|
187
|
+
self.fig.suptitle(f"Blaze angle {mean:.2f}° ± {sem:.3f}° (SEM)",
|
|
188
|
+
x=0.01, ha="left", fontsize=12, fontweight="semibold")
|
|
184
189
|
self.draw()
|