gratinglab 0.1.13__tar.gz → 0.2.0__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.13/src/gratinglab.egg-info → gratinglab-0.2.0}/PKG-INFO +2 -1
- {gratinglab-0.1.13 → gratinglab-0.2.0}/pyproject.toml +7 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/__init__.py +1 -1
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/checks.py +5 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/gui/docs.py +13 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/gui/efficiency_plot.py +11 -2
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/gui/provenance.py +13 -1
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/gui/qt/main_window.py +9 -19
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/gui/qt/theming.py +48 -2
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/metrology/gui/qt/boundary_view.py +31 -7
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/metrology/gui/qt/canvas.py +46 -13
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/metrology/gui/qt/efficiency_dialog.py +26 -3
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/metrology/gui/qt/import_view.py +16 -3
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/metrology/gui/qt/main_window.py +16 -1
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/metrology/gui/qt/swath_dialog.py +18 -1
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/problem.py +27 -1
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/result.py +101 -35
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/solvers/__init__.py +6 -0
- gratinglab-0.2.0/src/gratinglab/solvers/_conditioning.py +131 -0
- gratinglab-0.2.0/src/gratinglab/solvers/_execution.py +153 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/solvers/base.py +12 -0
- gratinglab-0.2.0/src/gratinglab/solvers/cmethod/__init__.py +418 -0
- gratinglab-0.2.0/src/gratinglab/solvers/cmethod/_core.py +405 -0
- gratinglab-0.2.0/src/gratinglab/solvers/cmethod/_series.py +196 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/solvers/integral/__init__.py +155 -54
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/solvers/integral/_core.py +6 -2
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/solvers/integral/_finite.py +9 -3
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/solvers/integral/_nystrom.py +18 -0
- gratinglab-0.2.0/src/gratinglab/solvers/rcwa/__init__.py +415 -0
- gratinglab-0.2.0/src/gratinglab/solvers/rcwa/_core.py +437 -0
- gratinglab-0.2.0/src/gratinglab/solvers/rcwa/_stack.py +364 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0/src/gratinglab.egg-info}/PKG-INFO +2 -1
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab.egg-info/SOURCES.txt +14 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab.egg-info/requires.txt +1 -0
- gratinglab-0.2.0/tests/test_cmethod.py +523 -0
- gratinglab-0.2.0/tests/test_conditioning.py +201 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/tests/test_gui_qt.py +6 -2
- {gratinglab-0.1.13 → gratinglab-0.2.0}/tests/test_gui_theme.py +24 -0
- gratinglab-0.2.0/tests/test_rayleigh_anomaly.py +180 -0
- gratinglab-0.2.0/tests/test_rcwa.py +807 -0
- gratinglab-0.2.0/tests/test_rcwa_benchmarks.py +468 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/tests/test_result.py +45 -1
- {gratinglab-0.1.13 → gratinglab-0.2.0}/tests/test_solver_conformance.py +43 -5
- gratinglab-0.2.0/tests/test_solver_execution.py +252 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/LICENSE +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/README.md +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/setup.cfg +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/compare.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/convergence.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/geometry.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/gui/__init__.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/gui/app.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/gui/diagram.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/gui/diagram3d.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/gui/icon.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/gui/integral_options.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/gui/launcher.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/gui/mathtext.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/gui/orders.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/gui/qt/__init__.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/gui/qt/gc_guard.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/gui/qt/geometry_panel.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/gui/qt/geometry_tab.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/gui/qt/integral_tab.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/gui/qt/launcher.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/gui/qt/profile_plot_panel.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/gui/qt/scalar_tab.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/gui/qt/scene3d_panel.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/gui/qt/setup_tab.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/gui/qt/sizing.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/gui/qt/theory_viewer.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/gui/qt/worker.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/gui/richtext.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/gui/scalar_options.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/gui/state.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/gui/theme.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/illumination.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/io/__init__.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/io/boundary.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/io/efficiency_table.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/io/ggp.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/materials/__init__.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/materials/data/SOURCES.md +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/materials/fresnel.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/materials/optical.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/materials/refractiveindex.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/measured.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/metrology/__init__.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/metrology/analyzer.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/metrology/boundary/__init__.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/metrology/boundary/average.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/metrology/boundary/pipeline.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/metrology/boundary/swaths.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/metrology/cli.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/metrology/config.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/metrology/core/__init__.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/metrology/core/analysis.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/metrology/core/image_flatten.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/metrology/core/orientation.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/metrology/core/processing.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/metrology/core/region_mask.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/metrology/core/scars.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/metrology/core/tip.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/metrology/data/__init__.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/metrology/data/aggregation.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/metrology/gui/__init__.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/metrology/gui/icon.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/metrology/gui/qt/__init__.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/metrology/gui/qt/app.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/metrology/gui/qt/wiki_view.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/metrology/gui/qt/worker.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/metrology/gui/state.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/metrology/io/__init__.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/metrology/io/file_io.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/metrology/io/ibw.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/metrology/io/metrics.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/metrology/io/spm.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/metrology/settings.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/metrology/stats/__init__.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/metrology/stats/analysis.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/metrology/stats/icc.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/metrology/visualization/__init__.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/metrology/visualization/diagnostics.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/metrology/visualization/profiles.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/metrology/visualization/statistics.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/metrology/wiki/__init__.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/metrology/wiki/facet-fitting.md +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/metrology/wiki/file-formats.md +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/metrology/wiki/flattening.md +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/metrology/wiki/groove-orientation.md +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/metrology/wiki/icc-correction.md +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/metrology/wiki/overview.md +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/metrology/wiki/reading-outputs.md +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/metrology/wiki/row-groups.md +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/metrology/wiki/scars-and-masking.md +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/metrology/wiki/swath-statistics.md +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/metrology/wiki/tip-correction.md +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/metrology/workflows.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/profiles.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/resolution.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/solvers/integral/_boundary.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/solvers/integral/_greens.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/solvers/integral/_kernels.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab/solvers/scalar.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab.egg-info/dependency_links.txt +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab.egg-info/entry_points.txt +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/src/gratinglab.egg-info/top_level.txt +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/tests/test_changelog.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/tests/test_checks.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/tests/test_compare.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/tests/test_convergence.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/tests/test_cross_method.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/tests/test_efficiency_table.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/tests/test_fresnel.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/tests/test_geometry.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/tests/test_ggp.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/tests/test_gui_diagram.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/tests/test_gui_diagram3d.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/tests/test_gui_docs.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/tests/test_gui_integral_options.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/tests/test_gui_launcher.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/tests/test_gui_mathtext.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/tests/test_gui_orders.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/tests/test_gui_provenance.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/tests/test_gui_richtext.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/tests/test_gui_scalar_options.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/tests/test_gui_state.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/tests/test_illumination.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/tests/test_integral_core.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/tests/test_integral_corpus.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/tests/test_integral_finite.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/tests/test_integral_graded.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/tests/test_integral_greens.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/tests/test_integral_kernels.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/tests/test_integral_operators.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/tests/test_integral_solver.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/tests/test_io_boundary.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/tests/test_materials.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/tests/test_measured.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/tests/test_perturbation.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/tests/test_profiles.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/tests/test_refractiveindex.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/tests/test_resolution.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/tests/test_scalar.py +0 -0
- {gratinglab-0.1.13 → gratinglab-0.2.0}/tests/test_solver_progress.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gratinglab
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
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
|
|
@@ -23,6 +23,7 @@ Requires-Dist: numpy>=1.26
|
|
|
23
23
|
Requires-Dist: scipy>=1.11
|
|
24
24
|
Requires-Dist: pydantic>=2.6
|
|
25
25
|
Requires-Dist: endstation>=0.2.2
|
|
26
|
+
Requires-Dist: threadpoolctl>=3.1
|
|
26
27
|
Provides-Extra: dev
|
|
27
28
|
Requires-Dist: pytest>=7; extra == "dev"
|
|
28
29
|
Requires-Dist: pytest-qt>=4.5; extra == "dev"
|
|
@@ -52,6 +52,13 @@ dependencies = [
|
|
|
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
54
|
"endstation>=0.2.2",
|
|
55
|
+
# The rigorous solvers cap BLAS threads for the length of a solve
|
|
56
|
+
# (solvers/_execution.py): OpenBLAS's thread-per-core default made RCWA
|
|
57
|
+
# up to 135x slower on a busy machine, measured in benchmarks/
|
|
58
|
+
# solver_speed.py. NumPy and SciPy each load their own OpenBLAS, and
|
|
59
|
+
# this is the one maintained way to reach both. Pure Python, BSD-3, no
|
|
60
|
+
# dependencies of its own.
|
|
61
|
+
"threadpoolctl>=3.1",
|
|
55
62
|
]
|
|
56
63
|
|
|
57
64
|
[project.optional-dependencies]
|
|
@@ -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.
|
|
25
|
+
__version__ = "0.2.0"
|
|
26
26
|
|
|
27
27
|
__all__ = [
|
|
28
28
|
"Illumination",
|
|
@@ -238,6 +238,9 @@ def check_energy_balance(
|
|
|
238
238
|
|
|
239
239
|
Notes
|
|
240
240
|
-----
|
|
241
|
+
Transmitted orders, when the scan records them, are added to the sum: the
|
|
242
|
+
power a lossless substrate carries away is accounted for, not lost.
|
|
243
|
+
|
|
241
244
|
When the scan records an absorbed fraction (``scan.absorption``, from a
|
|
242
245
|
solver that computes one independently), the conserved quantity is the
|
|
243
246
|
sum plus the absorption, and that is what is checked. ``R + A = 1`` is a
|
|
@@ -253,6 +256,8 @@ def check_energy_balance(
|
|
|
253
256
|
reached 3.6.
|
|
254
257
|
"""
|
|
255
258
|
total = np.asarray(scan.total, dtype=np.float64)
|
|
259
|
+
if scan.transmission is not None:
|
|
260
|
+
total = total + scan.transmission.total
|
|
256
261
|
if scan.absorption is not None:
|
|
257
262
|
total = total + scan.absorption
|
|
258
263
|
return EnergyReport(
|
|
@@ -44,8 +44,10 @@ __all__ = [
|
|
|
44
44
|
#: name for anything not listed here, so a new solver still gets a menu entry
|
|
45
45
|
#: before anyone remembers to add a nicer title.
|
|
46
46
|
_TITLES = {
|
|
47
|
+
"cmethod": "C-method (Chandezon)",
|
|
47
48
|
"scalar": "Scalar (Kirchhoff)",
|
|
48
49
|
"integral": "Integral (boundary integral equation)",
|
|
50
|
+
"rcwa": "RCWA (Fourier modal method)",
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
#: Non-solver reference pages: (key, title, filename relative to docs/).
|
|
@@ -105,6 +107,17 @@ def display_title(name: str) -> str:
|
|
|
105
107
|
return _TITLES.get(name, name)
|
|
106
108
|
|
|
107
109
|
|
|
110
|
+
def short_title(name: str) -> str:
|
|
111
|
+
"""A solver's tab label: its display title without the bracketed gloss.
|
|
112
|
+
|
|
113
|
+
``"Integral (boundary integral equation)"`` becomes ``"Integral"``. The
|
|
114
|
+
full title was the tab label until 2026-10-27, and with four tabs plus the
|
|
115
|
+
geometry dock it already overflowed the bar, so every new solver made it
|
|
116
|
+
worse. The full title stays in the Help menu and the tab's tooltip.
|
|
117
|
+
"""
|
|
118
|
+
return display_title(name).split(" (", 1)[0]
|
|
119
|
+
|
|
120
|
+
|
|
108
121
|
def find_theory_root(start: Path | None = None) -> Path | None:
|
|
109
122
|
"""Search upward from ``start`` for a ``docs/theory`` directory.
|
|
110
123
|
|
|
@@ -50,6 +50,14 @@ def draw_efficiency(
|
|
|
50
50
|
scan.wavelengths, scan.total,
|
|
51
51
|
color=theme.ink, ls="--", lw=1.0, alpha=0.7, label="Σ",
|
|
52
52
|
)
|
|
53
|
+
transmitted = (
|
|
54
|
+
scan.transmission.total if scan.transmission is not None else 0.0
|
|
55
|
+
)
|
|
56
|
+
if scan.transmission is not None:
|
|
57
|
+
axes.plot(
|
|
58
|
+
scan.wavelengths, scan.transmission.total,
|
|
59
|
+
color=theme.ink, lw=1.0, ls=":", alpha=0.9, label="T",
|
|
60
|
+
)
|
|
53
61
|
if scan.absorption is not None:
|
|
54
62
|
# Under a finite-conductivity solve Σ alone is the reflected fraction
|
|
55
63
|
# and sits below the unity line by exactly the absorbed one. Drawing
|
|
@@ -61,8 +69,9 @@ def draw_efficiency(
|
|
|
61
69
|
color=theme.warn, lw=1.0, ls="-.", alpha=0.9, label="A",
|
|
62
70
|
)
|
|
63
71
|
axes.plot(
|
|
64
|
-
scan.wavelengths, scan.total + scan.absorption,
|
|
65
|
-
color=theme.ink, lw=1.0, ls="-", alpha=0.5,
|
|
72
|
+
scan.wavelengths, scan.total + transmitted + scan.absorption,
|
|
73
|
+
color=theme.ink, lw=1.0, ls="-", alpha=0.5,
|
|
74
|
+
label="Σ + T + A" if scan.transmission is not None else "Σ + A",
|
|
66
75
|
)
|
|
67
76
|
axes.axhline(1.0, color=theme.bad, lw=0.8, ls=":", alpha=0.8)
|
|
68
77
|
axes.set_xlabel("wavelength (nm)")
|
|
@@ -147,6 +147,16 @@ def provenance_lines(
|
|
|
147
147
|
lines.append(Line("normalization: ", "dim"))
|
|
148
148
|
lines.append(Line(f"{normalization} ({detail})\n", "dim"))
|
|
149
149
|
|
|
150
|
+
if scan.transmission is not None:
|
|
151
|
+
lines.append(Line("transmission: ", "dim"))
|
|
152
|
+
lines.append(
|
|
153
|
+
Line(
|
|
154
|
+
f"T ∈ [{scan.transmission.total.min():.4f}, "
|
|
155
|
+
f"{scan.transmission.total.max():.4f}]\n",
|
|
156
|
+
"dim",
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
|
|
150
160
|
if scan.absorption is not None:
|
|
151
161
|
# A physical result, not a problem: `dim`, never an alarm tag. It also
|
|
152
162
|
# explains the deficit in Σ below, which would otherwise read as a
|
|
@@ -163,7 +173,9 @@ def provenance_lines(
|
|
|
163
173
|
# `check_energy_balance` folds absorption into the total when a scan
|
|
164
174
|
# carries one, so the conserved quantity is R + A there. Naming it Σ
|
|
165
175
|
# anyway would collide with the Σ curve on the plot, which is R alone.
|
|
166
|
-
conserved = "Σ +
|
|
176
|
+
conserved = "Σ" + (" + T" if scan.transmission is not None else "") + (
|
|
177
|
+
" + A" if scan.absorption is not None else ""
|
|
178
|
+
)
|
|
167
179
|
lines.append(Line("energy balance: ", "dim"))
|
|
168
180
|
lines.append(
|
|
169
181
|
Line(
|
|
@@ -26,7 +26,7 @@ from __future__ import annotations
|
|
|
26
26
|
import threading
|
|
27
27
|
|
|
28
28
|
from PySide6.QtCore import Qt, QThread, QTimer, Signal
|
|
29
|
-
from PySide6.QtGui import
|
|
29
|
+
from PySide6.QtGui import QKeySequence
|
|
30
30
|
from PySide6.QtWidgets import (
|
|
31
31
|
QDockWidget,
|
|
32
32
|
QMainWindow,
|
|
@@ -38,7 +38,6 @@ from PySide6.QtWidgets import (
|
|
|
38
38
|
|
|
39
39
|
from .. import provenance
|
|
40
40
|
from ..state import FormErrors, build, validate
|
|
41
|
-
from ..theme import SYSTEM, THEMES
|
|
42
41
|
from . import theming
|
|
43
42
|
from .geometry_panel import GeometryPanel
|
|
44
43
|
from .geometry_tab import GeometryTab
|
|
@@ -189,7 +188,7 @@ class MainWindow(QMainWindow):
|
|
|
189
188
|
self.resizeDocks([dock], [300], Qt.Orientation.Horizontal)
|
|
190
189
|
|
|
191
190
|
def _build_tabs(self) -> QWidget:
|
|
192
|
-
from ..docs import display_title
|
|
191
|
+
from ..docs import display_title, short_title
|
|
193
192
|
from ...solvers import available_solvers
|
|
194
193
|
|
|
195
194
|
self.tabs: dict[str, QWidget] = {}
|
|
@@ -220,7 +219,8 @@ class MainWindow(QMainWindow):
|
|
|
220
219
|
# has nothing to sweep and should not carry the button.
|
|
221
220
|
converge.connect(lambda _checked=False, n=name: self.solve(n, sweep=True))
|
|
222
221
|
self.tabs[name] = tab
|
|
223
|
-
self._tab_widget.addTab(tab,
|
|
222
|
+
index = self._tab_widget.addTab(tab, short_title(name))
|
|
223
|
+
self._tab_widget.setTabToolTip(index, display_title(name))
|
|
224
224
|
|
|
225
225
|
# Setup is leftmost so it is easy to find, but a solver tab -- not
|
|
226
226
|
# the explanatory stub -- is what should be showing, and solving,
|
|
@@ -284,19 +284,11 @@ class MainWindow(QMainWindow):
|
|
|
284
284
|
self._view_menu.addSeparator()
|
|
285
285
|
|
|
286
286
|
# One exclusive group: System follows the OS, the rest are fixed.
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
self.
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
action = self._theme_menu.addAction(label)
|
|
293
|
-
action.setCheckable(True)
|
|
294
|
-
action.setData(key)
|
|
295
|
-
action.setChecked(key == self._themes.choice)
|
|
296
|
-
action.triggered.connect(lambda _checked=False, k=key: self.set_theme(k))
|
|
297
|
-
self._theme_actions.addAction(action)
|
|
298
|
-
if key == SYSTEM:
|
|
299
|
-
self._theme_menu.addSeparator()
|
|
287
|
+
# Shared with the AFM window, so both offer it and both stay checked
|
|
288
|
+
# in step whichever one the choice was made in.
|
|
289
|
+
self._theme_menu = theming.ThemeMenu(self)
|
|
290
|
+
self._view_menu.addMenu(self._theme_menu)
|
|
291
|
+
self._theme_actions = self._theme_menu._group
|
|
300
292
|
|
|
301
293
|
menu = self._help_menu = self._menu_bar.addMenu("&Help")
|
|
302
294
|
menu.addAction("About GratingLab", self.show_about)
|
|
@@ -320,8 +312,6 @@ class MainWindow(QMainWindow):
|
|
|
320
312
|
def set_theme(self, choice: str) -> None:
|
|
321
313
|
"""Switch theme by key, or ``"system"``; the menu calls this."""
|
|
322
314
|
self._themes.set_choice(choice)
|
|
323
|
-
for action in self._theme_actions.actions():
|
|
324
|
-
action.setChecked(action.data() == choice)
|
|
325
315
|
|
|
326
316
|
def _start_worker(self) -> None:
|
|
327
317
|
self._thread = QThread(self)
|
|
@@ -22,14 +22,16 @@ import os
|
|
|
22
22
|
from collections.abc import Callable
|
|
23
23
|
|
|
24
24
|
from PySide6.QtCore import QObject, QSettings, Qt, Signal
|
|
25
|
-
from PySide6.QtGui import
|
|
26
|
-
|
|
25
|
+
from PySide6.QtGui import (QAction, QActionGroup, QColor, QGuiApplication,
|
|
26
|
+
QIcon, QPalette, QPixmap)
|
|
27
|
+
from PySide6.QtWidgets import QApplication, QMenu
|
|
27
28
|
|
|
28
29
|
from ..theme import DARK, LIGHT, SYSTEM, THEMES, Theme, rc_params
|
|
29
30
|
|
|
30
31
|
__all__ = [
|
|
31
32
|
"QIcon",
|
|
32
33
|
"SETTINGS_ENV",
|
|
34
|
+
"ThemeMenu",
|
|
33
35
|
"ThemeManager",
|
|
34
36
|
"current",
|
|
35
37
|
"install",
|
|
@@ -187,6 +189,50 @@ def subscribe(callback: Callable[[Theme], None]) -> None:
|
|
|
187
189
|
found.changed.connect(callback)
|
|
188
190
|
|
|
189
191
|
|
|
192
|
+
class ThemeMenu(QMenu):
|
|
193
|
+
"""The View > Theme submenu, one per window, all kept in step.
|
|
194
|
+
|
|
195
|
+
Every window that has a menu bar carries one, built here so that none can
|
|
196
|
+
be left without it: the AFM window had no theme choice at all while the
|
|
197
|
+
efficiency window did. Choosing in one window re-checks the others,
|
|
198
|
+
because each menu follows the manager's ``changed`` signal rather than
|
|
199
|
+
its own clicks.
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
def __init__(self, parent=None) -> None:
|
|
203
|
+
super().__init__("&Theme", parent)
|
|
204
|
+
self._group = QActionGroup(self)
|
|
205
|
+
self._group.setExclusive(True)
|
|
206
|
+
choices = [(SYSTEM, "System")] + [(k, t.label) for k, t in THEMES.items()]
|
|
207
|
+
for key, label in choices:
|
|
208
|
+
action = self.addAction(label)
|
|
209
|
+
action.setCheckable(True)
|
|
210
|
+
action.setData(key)
|
|
211
|
+
action.triggered.connect(self._chosen)
|
|
212
|
+
self._group.addAction(action)
|
|
213
|
+
if key == SYSTEM:
|
|
214
|
+
self.addSeparator()
|
|
215
|
+
found = manager()
|
|
216
|
+
if found is not None:
|
|
217
|
+
found.changed.connect(self._sync)
|
|
218
|
+
self._sync()
|
|
219
|
+
|
|
220
|
+
def actions_by_key(self) -> dict[str, QAction]:
|
|
221
|
+
return {a.data(): a for a in self._group.actions()}
|
|
222
|
+
|
|
223
|
+
def _chosen(self) -> None:
|
|
224
|
+
action = self.sender()
|
|
225
|
+
found = manager()
|
|
226
|
+
if found is not None and isinstance(action, QAction):
|
|
227
|
+
found.set_choice(action.data())
|
|
228
|
+
|
|
229
|
+
def _sync(self, _theme=None) -> None:
|
|
230
|
+
found = manager()
|
|
231
|
+
choice = found.choice if found is not None else SYSTEM
|
|
232
|
+
for key, action in self.actions_by_key().items():
|
|
233
|
+
action.setChecked(key == choice)
|
|
234
|
+
|
|
235
|
+
|
|
190
236
|
def swatch(color: str, size: int = 12) -> QIcon:
|
|
191
237
|
"""A square of ``color``, for a list entry that names a plotted line."""
|
|
192
238
|
pixmap = QPixmap(size, size)
|
|
@@ -27,6 +27,9 @@ from PySide6.QtWidgets import (
|
|
|
27
27
|
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
|
|
28
28
|
from matplotlib.figure import Figure
|
|
29
29
|
|
|
30
|
+
from gratinglab.gui.qt import theming
|
|
31
|
+
from gratinglab.gui.theme import style_figure
|
|
32
|
+
|
|
30
33
|
from ...boundary import build_boundary_profile
|
|
31
34
|
from ...core.tip import format_angles
|
|
32
35
|
from ...config import RESULTS_DIR
|
|
@@ -79,6 +82,7 @@ class BoundaryView(QWidget):
|
|
|
79
82
|
self.figure = Figure(figsize=(8, 6), tight_layout=True)
|
|
80
83
|
self.canvas = FigureCanvas(self.figure)
|
|
81
84
|
self._show_placeholder()
|
|
85
|
+
theming.subscribe(self._on_theme)
|
|
82
86
|
|
|
83
87
|
root.addWidget(scroll)
|
|
84
88
|
divider = QFrame()
|
|
@@ -291,10 +295,11 @@ class BoundaryView(QWidget):
|
|
|
291
295
|
f"Max curvature : {m['max_curvature']:.4f}{rotation}{tip}")
|
|
292
296
|
|
|
293
297
|
def _show_placeholder(self, message="Load an AFM file in the Import tab"):
|
|
298
|
+
self._placeholder = message
|
|
294
299
|
self.figure.clear()
|
|
295
300
|
axes = self.figure.add_subplot(111)
|
|
296
301
|
axes.text(0.5, 0.5, message, ha='center', va='center', fontsize=12,
|
|
297
|
-
color=
|
|
302
|
+
color=theming.current().muted, transform=axes.transAxes)
|
|
298
303
|
axes.set_axis_off()
|
|
299
304
|
self.canvas.draw()
|
|
300
305
|
|
|
@@ -310,19 +315,25 @@ class BoundaryView(QWidget):
|
|
|
310
315
|
x_nm = p.x_avg_um * 1000
|
|
311
316
|
avg_ax.plot(x_nm, p.y_avg_nm, color='crimson', linewidth=1.8,
|
|
312
317
|
label=f'Average of {p.n_used} grooves')
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
318
|
+
# The band was crimson at alpha 0.2 alone, which vanishes against a
|
|
319
|
+
# dark axes, and a narrow band hides behind the line. Edges show its
|
|
320
|
+
# width even then.
|
|
321
|
+
low, high = p.y_avg_nm - p.y_std_nm, p.y_avg_nm + p.y_std_nm
|
|
322
|
+
dark = theming.current().dark
|
|
323
|
+
avg_ax.fill_between(x_nm, low, high, color='crimson',
|
|
324
|
+
alpha=0.35 if dark else 0.2, linewidth=0, label='±1σ')
|
|
325
|
+
for edge in (low, high):
|
|
326
|
+
avg_ax.plot(x_nm, edge, color='crimson', linewidth=0.6,
|
|
327
|
+
alpha=0.8 if dark else 0.6)
|
|
316
328
|
avg_ax.set_xlabel("Position (nm)", fontsize=10)
|
|
317
329
|
avg_ax.set_ylabel("Height (nm)", fontsize=10)
|
|
318
|
-
avg_ax.set_title(
|
|
319
|
-
f"Averaged groove: {os.path.basename(self._filename)}", fontsize=10)
|
|
330
|
+
avg_ax.set_title("Averaged groove", fontsize=10)
|
|
320
331
|
avg_ax.legend(fontsize=8)
|
|
321
332
|
avg_ax.grid(True, alpha=0.3)
|
|
322
333
|
|
|
323
334
|
# What lands in the file.
|
|
324
335
|
norm_ax.plot(p.x_norm, p.y_norm, color='steelblue', linewidth=1.6)
|
|
325
|
-
norm_ax.axhline(0, color=
|
|
336
|
+
norm_ax.axhline(0, color=theming.current().ink, linestyle='--', linewidth=0.5)
|
|
326
337
|
norm_ax.set_xlabel("Normalised position (0 to 1 = one period)", fontsize=10)
|
|
327
338
|
norm_ax.set_ylabel("Height (fraction of period)", fontsize=10)
|
|
328
339
|
norm_ax.set_title(
|
|
@@ -332,6 +343,19 @@ class BoundaryView(QWidget):
|
|
|
332
343
|
|
|
333
344
|
self.canvas.draw()
|
|
334
345
|
|
|
346
|
+
def _on_theme(self, theme):
|
|
347
|
+
"""Draw the figure again in the new theme.
|
|
348
|
+
|
|
349
|
+
Each draw clears the figure and builds fresh axes, which take their
|
|
350
|
+
colours from matplotlib's settings, so redrawing is the restyle. The
|
|
351
|
+
figure predates the change and needs its own background set first.
|
|
352
|
+
"""
|
|
353
|
+
style_figure(self.figure, theme)
|
|
354
|
+
if self._profile is not None:
|
|
355
|
+
self._draw()
|
|
356
|
+
else:
|
|
357
|
+
self._show_placeholder(self._placeholder)
|
|
358
|
+
|
|
335
359
|
def _set_actions_enabled(self, enabled: bool) -> None:
|
|
336
360
|
"""All four actions need only a profile, so they are enabled together."""
|
|
337
361
|
self.export_btn.setEnabled(enabled)
|
|
@@ -6,7 +6,7 @@ nothing here is ever called from another thread.
|
|
|
6
6
|
"""
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
-
import
|
|
9
|
+
import functools
|
|
10
10
|
|
|
11
11
|
import numpy as np
|
|
12
12
|
|
|
@@ -16,35 +16,66 @@ import matplotlib
|
|
|
16
16
|
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
|
|
17
17
|
from matplotlib.figure import Figure
|
|
18
18
|
|
|
19
|
+
from gratinglab.gui.qt import theming
|
|
20
|
+
from gratinglab.gui.theme import style_figure
|
|
21
|
+
|
|
19
22
|
__all__ = ["PlotCanvas"]
|
|
20
23
|
|
|
21
24
|
|
|
25
|
+
def _remembered(method):
|
|
26
|
+
"""Record the call, so a theme change can draw the same plot again."""
|
|
27
|
+
@functools.wraps(method)
|
|
28
|
+
def wrapper(self, *args, **kwargs):
|
|
29
|
+
self._last = (method, args, kwargs)
|
|
30
|
+
return method(self, *args, **kwargs)
|
|
31
|
+
return wrapper
|
|
32
|
+
|
|
33
|
+
|
|
22
34
|
class PlotCanvas(FigureCanvas):
|
|
23
35
|
def __init__(self, parent=None):
|
|
24
36
|
self.fig = Figure(figsize=(8, 5), tight_layout=True)
|
|
25
37
|
super().__init__(self.fig)
|
|
26
38
|
self.setParent(parent)
|
|
39
|
+
self._last = None
|
|
40
|
+
theming.subscribe(self._on_theme)
|
|
27
41
|
self.show_placeholder("Load an AFM file to see the raw profile")
|
|
28
42
|
|
|
43
|
+
def _on_theme(self, theme):
|
|
44
|
+
"""Redraw the last plot in the new theme.
|
|
45
|
+
|
|
46
|
+
Every plot clears the figure and builds fresh axes, which take their
|
|
47
|
+
colours from matplotlib's settings at that moment, so drawing it again
|
|
48
|
+
is the whole restyle. The figure itself predates the change and needs
|
|
49
|
+
its own background set first.
|
|
50
|
+
"""
|
|
51
|
+
style_figure(self.fig, theme)
|
|
52
|
+
if self._last is not None:
|
|
53
|
+
method, args, kwargs = self._last
|
|
54
|
+
method(self, *args, **kwargs)
|
|
55
|
+
else:
|
|
56
|
+
self.draw_idle()
|
|
57
|
+
|
|
58
|
+
@_remembered
|
|
29
59
|
def show_placeholder(self, message):
|
|
30
60
|
self.fig.clear()
|
|
31
61
|
ax = self.fig.add_subplot(111)
|
|
32
62
|
ax.text(0.5, 0.5, message, ha='center', va='center', fontsize=12,
|
|
33
|
-
color=
|
|
63
|
+
color=theming.current().muted, transform=ax.transAxes)
|
|
34
64
|
ax.set_axis_off()
|
|
35
65
|
self.draw()
|
|
36
66
|
|
|
67
|
+
@_remembered
|
|
37
68
|
def plot_raw(self, disp_um, profile_nm, filename, scan_size_um):
|
|
38
69
|
self.fig.clear()
|
|
39
70
|
ax = self.fig.add_subplot(111)
|
|
40
71
|
ax.plot(disp_um, profile_nm, color='steelblue', linewidth=1.0)
|
|
41
72
|
ax.set_xlabel("Position (µm)", fontsize=11)
|
|
42
73
|
ax.set_ylabel("Height (nm)", fontsize=11)
|
|
43
|
-
ax.set_title(f"Raw
|
|
44
|
-
f"Scan width: {scan_size_um:.3f} µm", fontsize=11)
|
|
74
|
+
ax.set_title(f"Raw profile, scan width {scan_size_um:.3f} µm", fontsize=11)
|
|
45
75
|
ax.grid(True, alpha=0.3)
|
|
46
76
|
self.draw()
|
|
47
77
|
|
|
78
|
+
@_remembered
|
|
48
79
|
def plot_2d(self, data, scan_size_um, filename):
|
|
49
80
|
self.fig.clear()
|
|
50
81
|
ax = self.fig.add_subplot(111)
|
|
@@ -53,9 +84,10 @@ class PlotCanvas(FigureCanvas):
|
|
|
53
84
|
self.fig.colorbar(im, ax=ax, label='Height (nm)')
|
|
54
85
|
ax.set_xlabel("X (µm)", fontsize=11)
|
|
55
86
|
ax.set_ylabel("Scan line", fontsize=11)
|
|
56
|
-
ax.set_title(
|
|
87
|
+
ax.set_title("Topography", fontsize=11)
|
|
57
88
|
self.draw()
|
|
58
89
|
|
|
90
|
+
@_remembered
|
|
59
91
|
def plot_row_groups(self, disp_um, profiles_nm, group_info, filename, scan_size_um):
|
|
60
92
|
self.fig.clear()
|
|
61
93
|
ax = self.fig.add_subplot(111)
|
|
@@ -66,11 +98,11 @@ class PlotCanvas(FigureCanvas):
|
|
|
66
98
|
ax.plot(disp_um, profile, color=cmap(i / max(n_groups - 1, 1)),
|
|
67
99
|
linewidth=0.8, alpha=0.6)
|
|
68
100
|
|
|
69
|
-
ax.plot(disp_um, np.mean(np.array(profiles_nm), axis=0), color=
|
|
101
|
+
ax.plot(disp_um, np.mean(np.array(profiles_nm), axis=0), color=theming.current().ink,
|
|
70
102
|
linewidth=1.8, label=f'Mean (n={n_groups})', zorder=5)
|
|
71
103
|
ax.set_xlabel("Position (µm)", fontsize=11)
|
|
72
104
|
ax.set_ylabel("Height (nm)", fontsize=11)
|
|
73
|
-
ax.set_title(
|
|
105
|
+
ax.set_title("Row-group profiles\n"
|
|
74
106
|
f"{n_groups} groups × {group_info['rows_per_group']} rows each | "
|
|
75
107
|
f"Scan width: {scan_size_um:.3f} µm", fontsize=10)
|
|
76
108
|
ax.legend(fontsize=10)
|
|
@@ -84,6 +116,7 @@ class PlotCanvas(FigureCanvas):
|
|
|
84
116
|
"Image row (top → bottom)", fontsize=9)
|
|
85
117
|
self.draw()
|
|
86
118
|
|
|
119
|
+
@_remembered
|
|
87
120
|
def plot_groove_detection(self, result, filename, settings):
|
|
88
121
|
"""Flattened profile with the grooves the analysis used"""
|
|
89
122
|
self.fig.clear()
|
|
@@ -91,7 +124,7 @@ class PlotCanvas(FigureCanvas):
|
|
|
91
124
|
x, y = result['raw_x'], result['flat_y']
|
|
92
125
|
centers = result['groove_centers']
|
|
93
126
|
|
|
94
|
-
ax.plot(x, y, '
|
|
127
|
+
ax.plot(x, y, '-', color=theming.current().ink, linewidth=0.7, label='Flattened profile')
|
|
95
128
|
ax.plot(x[centers], y[centers], 'ro', markersize=9,
|
|
96
129
|
label=f'Detected grooves (N={len(centers)})')
|
|
97
130
|
|
|
@@ -107,11 +140,12 @@ class PlotCanvas(FigureCanvas):
|
|
|
107
140
|
|
|
108
141
|
ax.set_xlabel("Position (µm)", fontsize=11)
|
|
109
142
|
ax.set_ylabel("Height (nm)", fontsize=11)
|
|
110
|
-
ax.set_title(
|
|
143
|
+
ax.set_title("Groove detection", fontsize=11)
|
|
111
144
|
ax.legend(fontsize=9)
|
|
112
145
|
ax.grid(True, alpha=0.3)
|
|
113
146
|
self.draw()
|
|
114
147
|
|
|
148
|
+
@_remembered
|
|
115
149
|
def plot_blaze_angles(self, result, filename):
|
|
116
150
|
"""Angle distribution, fit quality, and angle against facet width"""
|
|
117
151
|
self.fig.clear()
|
|
@@ -121,7 +155,7 @@ class PlotCanvas(FigureCanvas):
|
|
|
121
155
|
quality = result['quality']
|
|
122
156
|
mean, sem = result['mean_angle'], result.get('sem', 0.0)
|
|
123
157
|
|
|
124
|
-
ax1.hist(angles, bins=20, edgecolor=
|
|
158
|
+
ax1.hist(angles, bins=20, edgecolor=theming.current().ink, alpha=0.75, color='steelblue')
|
|
125
159
|
ax1.axvline(mean, color='r', linestyle='--', linewidth=2,
|
|
126
160
|
label=f'Mean {mean:.2f}°')
|
|
127
161
|
ax1.set_xlabel("Blaze angle (°)", fontsize=10)
|
|
@@ -132,7 +166,7 @@ class PlotCanvas(FigureCanvas):
|
|
|
132
166
|
ax1.grid(True, alpha=0.3)
|
|
133
167
|
|
|
134
168
|
r2 = np.array([q.get('blaze_r2', np.nan) for q in quality])
|
|
135
|
-
ax2.hist(r2, bins=20, edgecolor=
|
|
169
|
+
ax2.hist(r2, bins=20, edgecolor=theming.current().ink, alpha=0.75, color='seagreen')
|
|
136
170
|
ax2.set_xlabel("Fit R²", fontsize=10)
|
|
137
171
|
ax2.set_ylabel("Count", fontsize=10)
|
|
138
172
|
ax2.set_title(f"Fit quality\nworst R² = {np.nanmin(r2):.4f}", fontsize=10)
|
|
@@ -146,6 +180,5 @@ class PlotCanvas(FigureCanvas):
|
|
|
146
180
|
fontsize=10)
|
|
147
181
|
ax3.grid(True, alpha=0.3)
|
|
148
182
|
|
|
149
|
-
self.fig.suptitle(f"{
|
|
150
|
-
f"{mean:.2f}° ± {sem:.3f}° (SEM)", fontsize=11)
|
|
183
|
+
self.fig.suptitle(f"Blaze angle {mean:.2f}° ± {sem:.3f}° (SEM)", fontsize=11)
|
|
151
184
|
self.draw()
|
|
@@ -35,6 +35,9 @@ from PySide6.QtWidgets import (
|
|
|
35
35
|
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
|
|
36
36
|
from matplotlib.figure import Figure
|
|
37
37
|
|
|
38
|
+
from gratinglab.gui.qt import theming
|
|
39
|
+
from gratinglab.gui.theme import style_figure
|
|
40
|
+
|
|
38
41
|
from gratinglab import materials
|
|
39
42
|
from gratinglab.illumination import Illumination
|
|
40
43
|
from gratinglab.resolution import describe
|
|
@@ -89,6 +92,8 @@ class EfficiencyDialog(QDialog):
|
|
|
89
92
|
self.figure = Figure(figsize=(6, 4.6))
|
|
90
93
|
self.canvas = FigureCanvas(self.figure)
|
|
91
94
|
root.addWidget(self.canvas, stretch=1)
|
|
95
|
+
self._placeholder = None
|
|
96
|
+
theming.subscribe(self._on_theme)
|
|
92
97
|
|
|
93
98
|
def _build_geometry_group(self):
|
|
94
99
|
group = QGroupBox("Grating")
|
|
@@ -237,14 +242,31 @@ class EfficiencyDialog(QDialog):
|
|
|
237
242
|
# ── Draw ─────────────────────────────────────────────────────────────────
|
|
238
243
|
|
|
239
244
|
def _show_placeholder(self, message):
|
|
245
|
+
self._placeholder = message
|
|
240
246
|
self.figure.clear()
|
|
241
247
|
axes = self.figure.add_subplot(111)
|
|
242
248
|
axes.text(0.5, 0.5, message, ha='center', va='center', fontsize=11,
|
|
243
|
-
color=
|
|
249
|
+
color=theming.current().muted, transform=axes.transAxes, wrap=True)
|
|
244
250
|
axes.set_axis_off()
|
|
245
251
|
self.canvas.draw()
|
|
246
252
|
|
|
253
|
+
def _on_theme(self, theme):
|
|
254
|
+
"""Draw the figure again in the new theme.
|
|
255
|
+
|
|
256
|
+
Each draw clears the figure and builds fresh axes, which take their
|
|
257
|
+
colours from matplotlib's settings, so redrawing is the restyle. The
|
|
258
|
+
figure predates the change and needs its own background set first.
|
|
259
|
+
"""
|
|
260
|
+
style_figure(self.figure, theme)
|
|
261
|
+
if self._placeholder is None and getattr(self, '_scan', None) is not None:
|
|
262
|
+
self._draw()
|
|
263
|
+
elif self._placeholder is not None:
|
|
264
|
+
self._show_placeholder(self._placeholder)
|
|
265
|
+
else:
|
|
266
|
+
self.canvas.draw_idle()
|
|
267
|
+
|
|
247
268
|
def _draw(self):
|
|
269
|
+
self._placeholder = None
|
|
248
270
|
scan = self._scan
|
|
249
271
|
self.figure.clear()
|
|
250
272
|
axes = self.figure.add_subplot(111)
|
|
@@ -258,7 +280,8 @@ class EfficiencyDialog(QDialog):
|
|
|
258
280
|
|
|
259
281
|
for m, efficiency in carried[:8]:
|
|
260
282
|
axes.plot(scan.wavelengths, efficiency, lw=1.3, label=f"m = {m}")
|
|
261
|
-
axes.plot(scan.wavelengths, scan.total, '
|
|
283
|
+
axes.plot(scan.wavelengths, scan.total, '--', color=theming.current().ink,
|
|
284
|
+
lw=1.0, alpha=0.6,
|
|
262
285
|
label="total")
|
|
263
286
|
|
|
264
287
|
axes.set_xlabel("Wavelength (nm)")
|
|
@@ -270,7 +293,7 @@ class EfficiencyDialog(QDialog):
|
|
|
270
293
|
axes.legend(fontsize=8, ncol=2)
|
|
271
294
|
else:
|
|
272
295
|
axes.text(0.5, 0.5, "No order carries more than 0.5%",
|
|
273
|
-
ha='center', va='center', color=
|
|
296
|
+
ha='center', va='center', color=theming.current().muted,
|
|
274
297
|
transform=axes.transAxes)
|
|
275
298
|
self.figure.tight_layout()
|
|
276
299
|
self.canvas.draw()
|
|
@@ -27,6 +27,9 @@ from PySide6.QtWidgets import (
|
|
|
27
27
|
)
|
|
28
28
|
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
|
|
29
29
|
from matplotlib.figure import Figure
|
|
30
|
+
|
|
31
|
+
from gratinglab.gui.qt import theming
|
|
32
|
+
from gratinglab.gui.theme import style_figure
|
|
30
33
|
from matplotlib.patches import Rectangle
|
|
31
34
|
from matplotlib.widgets import RectangleSelector
|
|
32
35
|
|
|
@@ -112,6 +115,7 @@ class ImportView(QWidget):
|
|
|
112
115
|
self.figure = Figure(figsize=(8, 6), tight_layout=True)
|
|
113
116
|
self.canvas = FigureCanvas(self.figure)
|
|
114
117
|
self._show_placeholder()
|
|
118
|
+
theming.subscribe(self._on_theme)
|
|
115
119
|
|
|
116
120
|
root.addWidget(scroll)
|
|
117
121
|
divider = QFrame()
|
|
@@ -528,10 +532,20 @@ class ImportView(QWidget):
|
|
|
528
532
|
self.figure.clear()
|
|
529
533
|
axes = self.figure.add_subplot(111)
|
|
530
534
|
axes.text(0.5, 0.5, message, ha='center', va='center', fontsize=12,
|
|
531
|
-
color=
|
|
535
|
+
color=theming.current().muted, transform=axes.transAxes)
|
|
532
536
|
axes.set_axis_off()
|
|
533
537
|
self.canvas.draw()
|
|
534
538
|
|
|
539
|
+
def _on_theme(self, theme):
|
|
540
|
+
"""Draw the figure again in the new theme.
|
|
541
|
+
|
|
542
|
+
Each draw clears the figure and builds fresh axes, which take their
|
|
543
|
+
colours from matplotlib's settings, so redrawing is the restyle. The
|
|
544
|
+
figure predates the change and needs its own background set first.
|
|
545
|
+
"""
|
|
546
|
+
style_figure(self.figure, theme)
|
|
547
|
+
self._redraw()
|
|
548
|
+
|
|
535
549
|
def _redraw(self):
|
|
536
550
|
if self._flat is None:
|
|
537
551
|
self._show_placeholder()
|
|
@@ -561,8 +575,7 @@ class ImportView(QWidget):
|
|
|
561
575
|
props=dict(facecolor='crimson', edgecolor='crimson', alpha=0.25,
|
|
562
576
|
fill=True))
|
|
563
577
|
image_ax.set_title(
|
|
564
|
-
f"{
|
|
565
|
-
f"{self.image_method_combo.currentText()}", fontsize=10)
|
|
578
|
+
f"Image flattening: {self.image_method_combo.currentText()}", fontsize=10)
|
|
566
579
|
|
|
567
580
|
try:
|
|
568
581
|
x_um, profile_nm = raw_data(self._flat, self._scan_size)
|