gratinglab 0.1.12__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.12/src/gratinglab.egg-info → gratinglab-0.2.0}/PKG-INFO +6 -3
- {gratinglab-0.1.12 → gratinglab-0.2.0}/README.md +3 -1
- {gratinglab-0.1.12 → gratinglab-0.2.0}/pyproject.toml +8 -1
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/__init__.py +1 -1
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/checks.py +5 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/gui/docs.py +13 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/gui/efficiency_plot.py +11 -2
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/gui/launcher.py +4 -5
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/gui/provenance.py +13 -1
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/gui/qt/launcher.py +91 -7
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/gui/qt/main_window.py +9 -19
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/gui/qt/theming.py +48 -2
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/io/boundary.py +5 -2
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/io/ggp.py +34 -14
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/metrology/analyzer.py +143 -46
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/metrology/boundary/pipeline.py +47 -13
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/metrology/config.py +34 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/metrology/core/image_flatten.py +24 -3
- gratinglab-0.2.0/src/gratinglab/metrology/core/orientation.py +475 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/metrology/core/processing.py +192 -17
- gratinglab-0.2.0/src/gratinglab/metrology/core/region_mask.py +232 -0
- gratinglab-0.2.0/src/gratinglab/metrology/core/scars.py +345 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/metrology/gui/qt/boundary_view.py +63 -12
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/metrology/gui/qt/canvas.py +46 -13
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/metrology/gui/qt/efficiency_dialog.py +26 -3
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/metrology/gui/qt/import_view.py +262 -17
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/metrology/gui/qt/main_window.py +26 -3
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/metrology/gui/qt/swath_dialog.py +18 -1
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/metrology/gui/state.py +55 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/metrology/io/file_io.py +13 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/metrology/io/ibw.py +34 -1
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/metrology/io/metrics.py +24 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/metrology/io/spm.py +96 -1
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/metrology/settings.py +81 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/metrology/wiki/__init__.py +2 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/metrology/wiki/file-formats.md +9 -0
- gratinglab-0.2.0/src/gratinglab/metrology/wiki/groove-orientation.md +114 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/metrology/wiki/overview.md +6 -1
- gratinglab-0.2.0/src/gratinglab/metrology/wiki/scars-and-masking.md +190 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/metrology/wiki/swath-statistics.md +6 -3
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/metrology/workflows.py +35 -3
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/problem.py +27 -1
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/profiles.py +28 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/result.py +101 -35
- {gratinglab-0.1.12 → 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.12 → 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.12 → gratinglab-0.2.0}/src/gratinglab/solvers/integral/__init__.py +155 -54
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/solvers/integral/_boundary.py +33 -10
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/solvers/integral/_core.py +6 -2
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/solvers/integral/_finite.py +9 -3
- {gratinglab-0.1.12 → 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.12 → gratinglab-0.2.0/src/gratinglab.egg-info}/PKG-INFO +6 -3
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab.egg-info/SOURCES.txt +19 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab.egg-info/requires.txt +1 -0
- gratinglab-0.2.0/tests/test_changelog.py +88 -0
- gratinglab-0.2.0/tests/test_cmethod.py +523 -0
- gratinglab-0.2.0/tests/test_conditioning.py +201 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/tests/test_cross_method.py +81 -3
- {gratinglab-0.1.12 → gratinglab-0.2.0}/tests/test_ggp.py +28 -2
- {gratinglab-0.1.12 → gratinglab-0.2.0}/tests/test_gui_launcher.py +144 -1
- {gratinglab-0.1.12 → gratinglab-0.2.0}/tests/test_gui_qt.py +6 -2
- {gratinglab-0.1.12 → gratinglab-0.2.0}/tests/test_gui_theme.py +24 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/tests/test_integral_core.py +7 -2
- {gratinglab-0.1.12 → gratinglab-0.2.0}/tests/test_integral_corpus.py +9 -2
- {gratinglab-0.1.12 → gratinglab-0.2.0}/tests/test_integral_graded.py +6 -2
- {gratinglab-0.1.12 → gratinglab-0.2.0}/tests/test_integral_solver.py +6 -1
- {gratinglab-0.1.12 → gratinglab-0.2.0}/tests/test_io_boundary.py +7 -2
- 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.12 → gratinglab-0.2.0}/tests/test_result.py +45 -1
- {gratinglab-0.1.12 → 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.12/tests/test_changelog.py +0 -34
- {gratinglab-0.1.12 → gratinglab-0.2.0}/LICENSE +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/setup.cfg +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/compare.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/convergence.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/geometry.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/gui/__init__.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/gui/app.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/gui/diagram.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/gui/diagram3d.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/gui/icon.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/gui/integral_options.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/gui/mathtext.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/gui/orders.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/gui/qt/__init__.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/gui/qt/gc_guard.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/gui/qt/geometry_panel.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/gui/qt/geometry_tab.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/gui/qt/integral_tab.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/gui/qt/profile_plot_panel.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/gui/qt/scalar_tab.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/gui/qt/scene3d_panel.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/gui/qt/setup_tab.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/gui/qt/sizing.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/gui/qt/theory_viewer.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/gui/qt/worker.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/gui/richtext.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/gui/scalar_options.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/gui/state.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/gui/theme.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/illumination.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/io/__init__.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/io/efficiency_table.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/materials/__init__.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/materials/data/SOURCES.md +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/materials/fresnel.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/materials/optical.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/materials/refractiveindex.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/measured.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/metrology/__init__.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/metrology/boundary/__init__.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/metrology/boundary/average.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/metrology/boundary/swaths.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/metrology/cli.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/metrology/core/__init__.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/metrology/core/analysis.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/metrology/core/tip.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/metrology/data/__init__.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/metrology/data/aggregation.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/metrology/gui/__init__.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/metrology/gui/icon.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/metrology/gui/qt/__init__.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/metrology/gui/qt/app.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/metrology/gui/qt/wiki_view.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/metrology/gui/qt/worker.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/metrology/io/__init__.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/metrology/stats/__init__.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/metrology/stats/analysis.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/metrology/stats/icc.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/metrology/visualization/__init__.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/metrology/visualization/diagnostics.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/metrology/visualization/profiles.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/metrology/visualization/statistics.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/metrology/wiki/facet-fitting.md +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/metrology/wiki/flattening.md +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/metrology/wiki/icc-correction.md +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/metrology/wiki/reading-outputs.md +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/metrology/wiki/row-groups.md +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/metrology/wiki/tip-correction.md +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/resolution.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/solvers/integral/_greens.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/solvers/integral/_kernels.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab/solvers/scalar.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab.egg-info/dependency_links.txt +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab.egg-info/entry_points.txt +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/src/gratinglab.egg-info/top_level.txt +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/tests/test_checks.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/tests/test_compare.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/tests/test_convergence.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/tests/test_efficiency_table.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/tests/test_fresnel.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/tests/test_geometry.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/tests/test_gui_diagram.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/tests/test_gui_diagram3d.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/tests/test_gui_docs.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/tests/test_gui_integral_options.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/tests/test_gui_mathtext.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/tests/test_gui_orders.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/tests/test_gui_provenance.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/tests/test_gui_richtext.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/tests/test_gui_scalar_options.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/tests/test_gui_state.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/tests/test_illumination.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/tests/test_integral_finite.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/tests/test_integral_greens.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/tests/test_integral_kernels.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/tests/test_integral_operators.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/tests/test_materials.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/tests/test_measured.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/tests/test_perturbation.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/tests/test_profiles.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/tests/test_refractiveindex.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/tests/test_resolution.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/tests/test_scalar.py +0 -0
- {gratinglab-0.1.12 → gratinglab-0.2.0}/tests/test_solver_progress.py +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
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
|
-
Author: Jake McCoy
|
|
5
|
+
Author: Jake McCoy, Aditi Gangadharan
|
|
6
6
|
License: BSD-3-Clause
|
|
7
7
|
Project-URL: Homepage, https://github.com/jamccoy/gratinglab
|
|
8
8
|
Project-URL: Changelog, https://github.com/jamccoy/gratinglab/blob/master/CHANGELOG.md
|
|
@@ -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"
|
|
@@ -271,7 +272,9 @@ gratinglab-metrology-gui # straight to AFM import, blaze angles, boundary
|
|
|
271
272
|
```
|
|
272
273
|
|
|
273
274
|
There are two windows, and `gratinglab-app` is the front door that asks which one
|
|
274
|
-
you want.
|
|
275
|
+
you want. Neither is a dead end: each carries a **Go** menu that switches to the other
|
|
276
|
+
and returns to the chooser (`Ctrl+Shift+M`), and switching reuses a window that is
|
|
277
|
+
already open rather than starting a second empty copy of it. Both halves are this one package — the metrology measures a groove and the
|
|
275
278
|
solvers compute an efficiency from the result — but the desktop layer used to ship
|
|
276
279
|
them as two commands and two application bundles, the AFM one still named after the
|
|
277
280
|
repository it was merged from, which made the half in regular use the half that was
|
|
@@ -227,7 +227,9 @@ gratinglab-metrology-gui # straight to AFM import, blaze angles, boundary
|
|
|
227
227
|
```
|
|
228
228
|
|
|
229
229
|
There are two windows, and `gratinglab-app` is the front door that asks which one
|
|
230
|
-
you want.
|
|
230
|
+
you want. Neither is a dead end: each carries a **Go** menu that switches to the other
|
|
231
|
+
and returns to the chooser (`Ctrl+Shift+M`), and switching reuses a window that is
|
|
232
|
+
already open rather than starting a second empty copy of it. Both halves are this one package — the metrology measures a groove and the
|
|
231
233
|
solvers compute an efficiency from the result — but the desktop layer used to ship
|
|
232
234
|
them as two commands and two application bundles, the AFM one still named after the
|
|
233
235
|
repository it was merged from, which made the half in regular use the half that was
|
|
@@ -13,7 +13,7 @@ description = "Open platform for diffraction grating groove metrology and effici
|
|
|
13
13
|
readme = "README.md"
|
|
14
14
|
requires-python = ">=3.11"
|
|
15
15
|
license = { text = "BSD-3-Clause" }
|
|
16
|
-
authors = [{ name = "Jake McCoy" }]
|
|
16
|
+
authors = [{ name = "Jake McCoy" }, { name = "Aditi Gangadharan" }]
|
|
17
17
|
# Wavelength-neutral on purpose. The off-plane work this package grew out of is
|
|
18
18
|
# soft x-ray, and that band is still named, but nothing in the solvers or the
|
|
19
19
|
# metrology assumes one: the grating equation carries no band, and the
|
|
@@ -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)")
|
|
@@ -101,7 +101,7 @@ def _require_qt():
|
|
|
101
101
|
def main(argv=None) -> int:
|
|
102
102
|
"""Console-script entry point for ``gratinglab-app``."""
|
|
103
103
|
QApplication = _require_qt()
|
|
104
|
-
from .qt.launcher import
|
|
104
|
+
from .qt.launcher import show_chooser
|
|
105
105
|
|
|
106
106
|
app = QApplication.instance() or QApplication(list(argv or sys.argv))
|
|
107
107
|
# Fusion for the same reason the metrology window asks for it: the native
|
|
@@ -111,10 +111,9 @@ def main(argv=None) -> int:
|
|
|
111
111
|
|
|
112
112
|
install(app)
|
|
113
113
|
|
|
114
|
-
|
|
115
|
-
window.
|
|
116
|
-
|
|
117
|
-
window.activateWindow()
|
|
114
|
+
# show_chooser, not LauncherWindow(), so that "Main Menu" in either
|
|
115
|
+
# window brings this same chooser back instead of a second copy.
|
|
116
|
+
show_chooser()
|
|
118
117
|
return app.exec()
|
|
119
118
|
|
|
120
119
|
|
|
@@ -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(
|
|
@@ -11,6 +11,7 @@ from functools import partial
|
|
|
11
11
|
from importlib import import_module
|
|
12
12
|
|
|
13
13
|
from PySide6.QtCore import Qt
|
|
14
|
+
from PySide6.QtGui import QKeySequence
|
|
14
15
|
from PySide6.QtWidgets import (
|
|
15
16
|
QFrame,
|
|
16
17
|
QHBoxLayout,
|
|
@@ -23,7 +24,19 @@ from PySide6.QtWidgets import (
|
|
|
23
24
|
|
|
24
25
|
from ..launcher import MODES
|
|
25
26
|
|
|
26
|
-
__all__ = [
|
|
27
|
+
__all__ = [
|
|
28
|
+
"LauncherWindow",
|
|
29
|
+
"SWITCH_MENU_TITLE",
|
|
30
|
+
"install_switch_menu",
|
|
31
|
+
"open_window",
|
|
32
|
+
"show_chooser",
|
|
33
|
+
"window_for",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
#: Title of the menu each window gets, through which the other mode and the
|
|
37
|
+
#: chooser are reachable. Not "Window": macOS has conventions for a menu of
|
|
38
|
+
#: that name and these are modes, not windows of one document.
|
|
39
|
+
SWITCH_MENU_TITLE = "&Go"
|
|
27
40
|
|
|
28
41
|
#: Windows opened from the chooser, kept alive deliberately.
|
|
29
42
|
#:
|
|
@@ -34,17 +47,86 @@ __all__ = ["LauncherWindow", "open_window"]
|
|
|
34
47
|
_OPEN_WINDOWS: list[QWidget] = []
|
|
35
48
|
|
|
36
49
|
|
|
50
|
+
#: The chooser, kept so that "Main Menu" shows the same one rather than a
|
|
51
|
+
#: second copy. Closing a window does not destroy it while a reference is
|
|
52
|
+
#: held, so this one can be shown again after it closed itself.
|
|
53
|
+
_CHOOSER: list = []
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def window_for(chosen):
|
|
57
|
+
"""The window already open for a mode, or None.
|
|
58
|
+
|
|
59
|
+
Opening a second window for a mode would be worse than useless: the
|
|
60
|
+
metrology window holds a loaded scan and its settings, so a "switch" that
|
|
61
|
+
quietly produced an empty second copy would look like losing the work.
|
|
62
|
+
"""
|
|
63
|
+
for window in _OPEN_WINDOWS:
|
|
64
|
+
if getattr(window, "_gratinglab_mode", None) == chosen.key:
|
|
65
|
+
return window
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _raise(window):
|
|
70
|
+
"""Show a window that may be open behind something, or closed."""
|
|
71
|
+
window.show()
|
|
72
|
+
window.raise_()
|
|
73
|
+
window.activateWindow()
|
|
74
|
+
return window
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def install_switch_menu(window, current):
|
|
78
|
+
"""Give a window a menu leading to the other mode and to the chooser.
|
|
79
|
+
|
|
80
|
+
Added from here rather than by each window, so that neither window needs
|
|
81
|
+
to know the other exists and a third mode is still one entry in MODES.
|
|
82
|
+
|
|
83
|
+
The menu and its actions are held on the window. `gui/qt/main_window.py`
|
|
84
|
+
records why: under PySide6 a QMenu reached back through
|
|
85
|
+
``menuBar().actions()[i].menu()`` is not reliably usable, so whatever
|
|
86
|
+
builds a menu must keep it.
|
|
87
|
+
"""
|
|
88
|
+
menu = window.menuBar().addMenu(SWITCH_MENU_TITLE)
|
|
89
|
+
window._gratinglab_switch_menu = menu
|
|
90
|
+
|
|
91
|
+
actions = []
|
|
92
|
+
for other in MODES:
|
|
93
|
+
if other.key == current.key:
|
|
94
|
+
continue
|
|
95
|
+
action = menu.addAction(other.title)
|
|
96
|
+
action.triggered.connect(partial(open_window, other))
|
|
97
|
+
actions.append(action)
|
|
98
|
+
|
|
99
|
+
menu.addSeparator()
|
|
100
|
+
back = menu.addAction("Main Menu")
|
|
101
|
+
back.setShortcut(QKeySequence("Ctrl+Shift+M"))
|
|
102
|
+
back.triggered.connect(show_chooser)
|
|
103
|
+
actions.append(back)
|
|
104
|
+
|
|
105
|
+
window._gratinglab_switch_actions = actions
|
|
106
|
+
return menu
|
|
107
|
+
|
|
108
|
+
|
|
37
109
|
def open_window(chosen):
|
|
38
|
-
"""
|
|
110
|
+
"""Show the window a mode names, reusing it if it is already open."""
|
|
111
|
+
existing = window_for(chosen)
|
|
112
|
+
if existing is not None:
|
|
113
|
+
return _raise(existing)
|
|
114
|
+
|
|
39
115
|
module_name, _, attribute = chosen.window.partition(":")
|
|
40
116
|
window_class = getattr(import_module(module_name), attribute)
|
|
41
117
|
|
|
42
118
|
window = window_class()
|
|
119
|
+
window._gratinglab_mode = chosen.key
|
|
120
|
+
install_switch_menu(window, chosen)
|
|
43
121
|
_OPEN_WINDOWS.append(window)
|
|
44
|
-
window
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
122
|
+
return _raise(window)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def show_chooser():
|
|
126
|
+
"""Bring the chooser back, the same one if it has been up before."""
|
|
127
|
+
if not _CHOOSER:
|
|
128
|
+
_CHOOSER.append(LauncherWindow())
|
|
129
|
+
return _raise(_CHOOSER[0])
|
|
48
130
|
|
|
49
131
|
|
|
50
132
|
class LauncherWindow(QWidget):
|
|
@@ -80,7 +162,9 @@ class LauncherWindow(QWidget):
|
|
|
80
162
|
layout.addStretch(1)
|
|
81
163
|
|
|
82
164
|
hint = QLabel(
|
|
83
|
-
"
|
|
165
|
+
"Either window's <b>Go</b> menu switches to the other and comes "
|
|
166
|
+
"back here, so this is not a one-way door. Each is also a command "
|
|
167
|
+
"of its own: "
|
|
84
168
|
+ ", ".join(f"<code>{m.script}</code>" for m in MODES)
|
|
85
169
|
+ "."
|
|
86
170
|
)
|
|
@@ -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)
|
|
@@ -63,7 +63,8 @@ class BoundaryFile:
|
|
|
63
63
|
"""A groove profile read back from a gratinglab ``.json``."""
|
|
64
64
|
|
|
65
65
|
period_nm: float
|
|
66
|
-
#: Fractions of one period
|
|
66
|
+
#: Fractions of one period along ``+d-hat``, as ``x_norm`` and a ``.ggp``
|
|
67
|
+
#: hold them; :meth:`profile` reflects them into the profile parameter.
|
|
67
68
|
t: tuple[float, ...]
|
|
68
69
|
y: tuple[float, ...]
|
|
69
70
|
#: Grooves averaged into this profile, carried onto the problem.
|
|
@@ -77,7 +78,9 @@ class BoundaryFile:
|
|
|
77
78
|
"""The shape, as the solvers take it."""
|
|
78
79
|
from ..profiles import FromProfileData
|
|
79
80
|
|
|
80
|
-
return FromProfileData(
|
|
81
|
+
return FromProfileData.along_dispersion(
|
|
82
|
+
self.t, self.y, blaze_angle=self.blaze_angle
|
|
83
|
+
)
|
|
81
84
|
|
|
82
85
|
def to_problem(self, *, coating=None, substrate=None, roughness=0.0):
|
|
83
86
|
"""This groove as an efficiency problem, with the period it was measured at.
|
|
@@ -2,8 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
A ``.ggp`` holds a groove profile normalised to the period: ``x`` runs 0 to 1
|
|
4
4
|
across exactly one period and ``y`` is height as a fraction of that period.
|
|
5
|
-
|
|
6
|
-
needed.
|
|
5
|
+
The scale is the one :mod:`gratinglab.profiles` uses, so no rescaling is
|
|
6
|
+
needed. The direction is not: ``x`` advances along the dispersion direction
|
|
7
|
+
and a profile's ``t`` against it (``docs/conventions.md``, "Direction of the
|
|
8
|
+
profile parameter"), so :func:`to_profile` and :func:`write_ggp` reflect the
|
|
9
|
+
points through :meth:`~gratinglab.profiles.FromProfileData.along_dispersion`
|
|
10
|
+
and its inverse. The PCGrate corpus is what fixes the file's direction: the
|
|
11
|
+
TASTE run matches only with ``x`` along ``+d-hat``.
|
|
7
12
|
|
|
8
13
|
Canonical form::
|
|
9
14
|
|
|
@@ -196,9 +201,10 @@ def to_profile(ggp: GgpFile) -> FromProfileData:
|
|
|
196
201
|
"""Convert to a :class:`~gratinglab.profiles.FromProfileData`.
|
|
197
202
|
|
|
198
203
|
Heights are referenced to the profile minimum so the valley sits at zero,
|
|
199
|
-
matching every analytic profile in :mod:`gratinglab.profiles`.
|
|
204
|
+
matching every analytic profile in :mod:`gratinglab.profiles`. The file's
|
|
205
|
+
``x`` runs along ``+d-hat``, so the points are reflected into ``t``.
|
|
200
206
|
"""
|
|
201
|
-
return FromProfileData(
|
|
207
|
+
return FromProfileData.along_dispersion(ggp.t, ggp.y - ggp.y.min())
|
|
202
208
|
|
|
203
209
|
|
|
204
210
|
def read_profile(path: str | Path) -> FromProfileData:
|
|
@@ -213,14 +219,26 @@ def write_ggp(
|
|
|
213
219
|
t: ArrayLike | None = None,
|
|
214
220
|
y: ArrayLike | None = None,
|
|
215
221
|
fmt: str = "%f",
|
|
222
|
+
header: bool = True,
|
|
216
223
|
) -> Path:
|
|
217
224
|
"""Write a profile in the canonical, PCGrate-loadable form.
|
|
218
225
|
|
|
219
|
-
Pass either a ``profile`` or explicit ``t``/``y`` arrays.
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
226
|
+
Pass either a ``profile`` or explicit ``t``/``y`` arrays. A profile is
|
|
227
|
+
reflected onto the file's ``+d-hat`` axis, the inverse of
|
|
228
|
+
:func:`to_profile`. Explicit arrays are written as given, so they are
|
|
229
|
+
file coordinates already, as the metrology pipeline's ``x_norm`` is.
|
|
230
|
+
|
|
231
|
+
Values are written to six decimals, single-space separated, one point per
|
|
232
|
+
line, with no line break after the last point. PCGrate rejects a file
|
|
233
|
+
that ends in one, as ``np.savetxt`` output does. The format is pinned by
|
|
234
|
+
``tests/test_ggp.py``; PCGrate rejects variations on it.
|
|
235
|
+
|
|
236
|
+
``header=False`` leaves out the two :data:`GGP_HEADER` lines and writes
|
|
237
|
+
the points alone, as ``np.savetxt`` with no ``header`` does. It exists to
|
|
238
|
+
test the claim in the module docstring that PCGrate needs the header:
|
|
239
|
+
that claim comes from the files found on disk, not from PCGrate's
|
|
240
|
+
documentation, and Aditi's script writes headerless files (2026-10-27).
|
|
241
|
+
Reading such a file back reports ``header_variant == "none"``.
|
|
224
242
|
"""
|
|
225
243
|
if (profile is None) == (t is None):
|
|
226
244
|
raise ValueError("pass either a profile or both t and y, not both or neither")
|
|
@@ -231,8 +249,7 @@ def write_ggp(
|
|
|
231
249
|
"cannot write an undercut profile as .ggp: PCGrate's polygonal "
|
|
232
250
|
"border expects a boundary that advances monotonically in x"
|
|
233
251
|
)
|
|
234
|
-
t_values =
|
|
235
|
-
y_values = np.asarray(profile.y, dtype=np.float64)
|
|
252
|
+
t_values, y_values = profile.dispersion_axis()
|
|
236
253
|
else:
|
|
237
254
|
t_values = np.asarray(t, dtype=np.float64)
|
|
238
255
|
y_values = np.asarray(y, dtype=np.float64)
|
|
@@ -243,8 +260,11 @@ def write_ggp(
|
|
|
243
260
|
|
|
244
261
|
path = Path(path)
|
|
245
262
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
263
|
+
points = (f"{fmt % x_value} {fmt % height}"
|
|
264
|
+
for x_value, height in zip(t_values, y_values))
|
|
246
265
|
with path.open("w", encoding="utf-8") as handle:
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
266
|
+
# No line break after the last point: PCGrate reads a trailing one as
|
|
267
|
+
# an extra, empty point and rejects the file (found by Aditi,
|
|
268
|
+
# 2026-10-27).
|
|
269
|
+
handle.write("\n".join([GGP_HEADER, *points] if header else points))
|
|
250
270
|
return path
|