gratinglab 0.2.5__tar.gz → 0.2.6__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.5/src/gratinglab.egg-info → gratinglab-0.2.6}/PKG-INFO +1 -1
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/__init__.py +1 -1
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/io/boundary.py +4 -2
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/metrology/boundary/__init__.py +4 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/metrology/boundary/average.py +116 -16
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/metrology/boundary/pipeline.py +60 -12
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/metrology/gui/qt/boundary_view.py +7 -1
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/metrology/io/metrics.py +10 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6/src/gratinglab.egg-info}/PKG-INFO +1 -1
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab.egg-info/SOURCES.txt +1 -0
- gratinglab-0.2.6/tests/test_four_way.py +217 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/tests/test_gui_docs.py +7 -2
- {gratinglab-0.2.5 → gratinglab-0.2.6}/LICENSE +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/README.md +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/pyproject.toml +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/setup.cfg +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/checks.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/compare.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/convergence.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/geometry.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/gui/__init__.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/gui/app.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/gui/cmethod_options.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/gui/diagram.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/gui/diagram3d.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/gui/docs.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/gui/efficiency_plot.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/gui/icon.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/gui/integral_options.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/gui/launcher.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/gui/mathtext.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/gui/orders.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/gui/provenance.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/gui/qt/__init__.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/gui/qt/cmethod_tab.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/gui/qt/geometry_panel.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/gui/qt/geometry_tab.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/gui/qt/integral_tab.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/gui/qt/launcher.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/gui/qt/main_window.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/gui/qt/profile_plot_panel.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/gui/qt/rcwa_tab.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/gui/qt/scalar_tab.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/gui/qt/scene3d_panel.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/gui/qt/setup_tab.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/gui/qt/sizing.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/gui/qt/theming.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/gui/qt/theory_viewer.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/gui/qt/worker.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/gui/rcwa_options.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/gui/richtext.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/gui/scalar_options.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/gui/state.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/gui/theme.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/illumination.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/io/__init__.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/io/efficiency_table.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/io/ggp.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/materials/__init__.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/materials/data/SOURCES.md +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/materials/fresnel.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/materials/optical.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/materials/refractiveindex.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/measured.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/metrology/__init__.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/metrology/analyzer.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/metrology/boundary/swaths.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/metrology/cli.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/metrology/config.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/metrology/core/__init__.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/metrology/core/analysis.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/metrology/core/image_flatten.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/metrology/core/orientation.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/metrology/core/processing.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/metrology/core/region_mask.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/metrology/core/scars.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/metrology/core/tip.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/metrology/data/__init__.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/metrology/data/aggregation.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/metrology/gui/__init__.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/metrology/gui/icon.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/metrology/gui/qt/__init__.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/metrology/gui/qt/app.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/metrology/gui/qt/canvas.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/metrology/gui/qt/efficiency_dialog.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/metrology/gui/qt/import_view.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/metrology/gui/qt/layout.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/metrology/gui/qt/main_window.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/metrology/gui/qt/swath_dialog.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/metrology/gui/qt/wiki_view.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/metrology/gui/qt/worker.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/metrology/gui/state.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/metrology/io/__init__.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/metrology/io/file_io.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/metrology/io/ibw.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/metrology/io/spm.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/metrology/settings.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/metrology/stats/__init__.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/metrology/stats/analysis.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/metrology/stats/icc.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/metrology/visualization/__init__.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/metrology/visualization/diagnostics.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/metrology/visualization/profiles.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/metrology/visualization/statistics.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/metrology/wiki/__init__.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/metrology/wiki/facet-fitting.md +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/metrology/wiki/file-formats.md +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/metrology/wiki/flattening.md +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/metrology/wiki/groove-orientation.md +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/metrology/wiki/icc-correction.md +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/metrology/wiki/overview.md +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/metrology/wiki/reading-outputs.md +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/metrology/wiki/row-groups.md +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/metrology/wiki/scars-and-masking.md +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/metrology/wiki/swath-statistics.md +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/metrology/wiki/tip-correction.md +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/metrology/workflows.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/problem.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/profiles.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/resolution.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/result.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/solvers/__init__.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/solvers/_conditioning.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/solvers/_execution.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/solvers/base.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/solvers/cmethod/__init__.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/solvers/cmethod/_core.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/solvers/cmethod/_series.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/solvers/integral/__init__.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/solvers/integral/_boundary.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/solvers/integral/_core.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/solvers/integral/_finite.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/solvers/integral/_greens.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/solvers/integral/_kernels.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/solvers/integral/_nystrom.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/solvers/rcwa/__init__.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/solvers/rcwa/_core.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/solvers/rcwa/_stack.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab/solvers/scalar.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab.egg-info/dependency_links.txt +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab.egg-info/entry_points.txt +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab.egg-info/requires.txt +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/src/gratinglab.egg-info/top_level.txt +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/tests/test_changelog.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/tests/test_checks.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/tests/test_cmethod.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/tests/test_compare.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/tests/test_conditioning.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/tests/test_convergence.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/tests/test_cross_method.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/tests/test_efficiency_table.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/tests/test_fresnel.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/tests/test_geometry.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/tests/test_ggp.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/tests/test_gui_cmethod_options.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/tests/test_gui_cmethod_tab.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/tests/test_gui_diagram.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/tests/test_gui_diagram3d.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/tests/test_gui_integral_options.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/tests/test_gui_launcher.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/tests/test_gui_mathtext.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/tests/test_gui_orders.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/tests/test_gui_provenance.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/tests/test_gui_qt.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/tests/test_gui_rcwa_options.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/tests/test_gui_richtext.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/tests/test_gui_scalar_options.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/tests/test_gui_state.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/tests/test_gui_theme.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/tests/test_illumination.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/tests/test_integral_core.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/tests/test_integral_corpus.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/tests/test_integral_finite.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/tests/test_integral_graded.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/tests/test_integral_greens.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/tests/test_integral_kernels.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/tests/test_integral_operators.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/tests/test_integral_solver.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/tests/test_io_boundary.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/tests/test_materials.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/tests/test_measured.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/tests/test_perturbation.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/tests/test_profiles.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/tests/test_rayleigh_anomaly.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/tests/test_rcwa.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/tests/test_rcwa_benchmarks.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/tests/test_refractiveindex.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/tests/test_resolution.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/tests/test_result.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/tests/test_scalar.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/tests/test_solver_conformance.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/tests/test_solver_execution.py +0 -0
- {gratinglab-0.2.5 → gratinglab-0.2.6}/tests/test_solver_progress.py +0 -0
|
@@ -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.6"
|
|
26
26
|
|
|
27
27
|
__all__ = [
|
|
28
28
|
"Illumination",
|
|
@@ -88,8 +88,10 @@ class BoundaryFile:
|
|
|
88
88
|
This is the same problem ``BoundaryProfile.to_problem`` builds in
|
|
89
89
|
process, which is the purpose of the format; see
|
|
90
90
|
``tests/test_io_boundary.py``.
|
|
91
|
-
Roughness is not
|
|
92
|
-
gives
|
|
91
|
+
Roughness is not applied, for the reason that method's docstring
|
|
92
|
+
gives. The measured groove residual travels in
|
|
93
|
+
``metrics['groove_residual_rms_nm']`` and is passed only on request,
|
|
94
|
+
as ``roughness=``.
|
|
93
95
|
"""
|
|
94
96
|
from ..problem import Problem
|
|
95
97
|
|
|
@@ -14,8 +14,10 @@ from .swaths import SwathStatistics, swath_statistics
|
|
|
14
14
|
from .average import (
|
|
15
15
|
flatten_endpoints,
|
|
16
16
|
average_grooves,
|
|
17
|
+
groove_residuals,
|
|
17
18
|
normalize_profile,
|
|
18
19
|
profile_metrics,
|
|
20
|
+
select_grooves,
|
|
19
21
|
)
|
|
20
22
|
|
|
21
23
|
__all__ = [
|
|
@@ -25,6 +27,8 @@ __all__ = [
|
|
|
25
27
|
'swath_statistics',
|
|
26
28
|
'flatten_endpoints',
|
|
27
29
|
'average_grooves',
|
|
30
|
+
'groove_residuals',
|
|
28
31
|
'normalize_profile',
|
|
29
32
|
'profile_metrics',
|
|
33
|
+
'select_grooves',
|
|
30
34
|
]
|
|
@@ -7,7 +7,7 @@ profile front-end now comes from gratinglab.metrology.core instead of a private
|
|
|
7
7
|
copy.
|
|
8
8
|
"""
|
|
9
9
|
import numpy as np
|
|
10
|
-
from scipy.interpolate import interp1d
|
|
10
|
+
from scipy.interpolate import interp1d, make_interp_spline
|
|
11
11
|
from scipy.ndimage import uniform_filter1d
|
|
12
12
|
|
|
13
13
|
from ..core.processing import extract_single_groove
|
|
@@ -30,6 +30,36 @@ def flatten_endpoints(x, y):
|
|
|
30
30
|
return y - line
|
|
31
31
|
|
|
32
32
|
|
|
33
|
+
def select_grooves(x, y, groove_centers, period_nm, margin=0.0,
|
|
34
|
+
min_half_width=10, require_full_period=True):
|
|
35
|
+
"""
|
|
36
|
+
The grooves `average_grooves` averages, each centred on its own axis in µm.
|
|
37
|
+
|
|
38
|
+
Separate so that `groove_residuals` measures exactly the set that went
|
|
39
|
+
into the average, chosen by the same rules rather than a second copy of
|
|
40
|
+
them. The parameters mean what they mean in `average_grooves`.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
a list of (x_g, y_g), possibly empty
|
|
44
|
+
"""
|
|
45
|
+
grooves = []
|
|
46
|
+
dx_nm = (x[1] - x[0]) * 1000
|
|
47
|
+
half_width = int(round((period_nm / 2) * (1 + margin) / dx_nm))
|
|
48
|
+
|
|
49
|
+
for center in groove_centers:
|
|
50
|
+
L = min(half_width, center, len(x) - 1 - center)
|
|
51
|
+
if require_full_period and L < half_width:
|
|
52
|
+
continue # the scan edge cuts into this groove's period
|
|
53
|
+
if L > min_half_width:
|
|
54
|
+
x_g, y_g = extract_single_groove(x, y, center, period_nm, margin,
|
|
55
|
+
allow_asymmetric=False)
|
|
56
|
+
# Re-create the exactly symmetric axis the original used, so that
|
|
57
|
+
# floating point matches to the last bit.
|
|
58
|
+
x_g = np.linspace(-L * dx_nm, L * dx_nm, len(y_g)) / 1000
|
|
59
|
+
grooves.append((x_g, y_g))
|
|
60
|
+
return grooves
|
|
61
|
+
|
|
62
|
+
|
|
33
63
|
def average_grooves(x, y, groove_centers, period_nm, margin=0.0, n_points=2000,
|
|
34
64
|
min_half_width=10, require_full_period=True):
|
|
35
65
|
"""
|
|
@@ -53,21 +83,9 @@ def average_grooves(x, y, groove_centers, period_nm, margin=0.0, n_points=2000,
|
|
|
53
83
|
Returns:
|
|
54
84
|
x_common, y_avg, y_std, n_used
|
|
55
85
|
"""
|
|
56
|
-
grooves =
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
for center in groove_centers:
|
|
61
|
-
L = min(half_width, center, len(x) - 1 - center)
|
|
62
|
-
if require_full_period and L < half_width:
|
|
63
|
-
continue # the scan edge cuts into this groove's period
|
|
64
|
-
if L > min_half_width:
|
|
65
|
-
x_g, y_g = extract_single_groove(x, y, center, period_nm, margin,
|
|
66
|
-
allow_asymmetric=False)
|
|
67
|
-
# Re-create the exactly symmetric axis the original used, so that
|
|
68
|
-
# floating point matches to the last bit.
|
|
69
|
-
x_g = np.linspace(-L * dx_nm, L * dx_nm, len(y_g)) / 1000
|
|
70
|
-
grooves.append((x_g, y_g))
|
|
86
|
+
grooves = select_grooves(x, y, groove_centers, period_nm, margin=margin,
|
|
87
|
+
min_half_width=min_half_width,
|
|
88
|
+
require_full_period=require_full_period)
|
|
71
89
|
|
|
72
90
|
if len(grooves) == 0:
|
|
73
91
|
raise ValueError(
|
|
@@ -90,6 +108,88 @@ def average_grooves(x, y, groove_centers, period_nm, margin=0.0, n_points=2000,
|
|
|
90
108
|
return x_common, np.mean(y_aligned, axis=0), np.std(y_aligned, axis=0), len(grooves)
|
|
91
109
|
|
|
92
110
|
|
|
111
|
+
def groove_residuals(grooves, x_common, y_avg, n_passes=3, n_steps=6):
|
|
112
|
+
"""
|
|
113
|
+
How far each groove departs from the average, once registered to it.
|
|
114
|
+
|
|
115
|
+
This is the RMS residual of each averaged groove about the average: the
|
|
116
|
+
part of the measured profile that does not repeat from one groove to the
|
|
117
|
+
next. It is reported, not applied. `BoundaryProfile.to_problem` and
|
|
118
|
+
docs/theory/metrology.md section 4 say why it is not a Nevot-Croce sigma.
|
|
119
|
+
|
|
120
|
+
A plain difference against `y_avg` does not measure that. Each groove is
|
|
121
|
+
centred on the sample nearest its minimum, so the set is misregistered by
|
|
122
|
+
up to half a pixel, and on a facet steeper than 45 degrees half a pixel of
|
|
123
|
+
shift is several nanometres of height. On the synthetic sawtooth with no
|
|
124
|
+
noise at all, the plain difference reads 2.1 nm. So each groove is first
|
|
125
|
+
registered to the average by a sub-pixel shift and a height offset, fitted
|
|
126
|
+
by Gauss-Newton on a cubic spline of its own samples, and the average is
|
|
127
|
+
recomputed from the registered grooves, `n_passes` times over. The shift is
|
|
128
|
+
bounded to one pixel either way, and the window is the aligned window the
|
|
129
|
+
average uses less one pixel at each end, so a shifted groove is never
|
|
130
|
+
extrapolated.
|
|
131
|
+
|
|
132
|
+
The residual is then taken at each groove's own samples rather than on the
|
|
133
|
+
dense common axis, because interpolating onto that axis smooths pixel-scale
|
|
134
|
+
structure: it read white noise 10 to 15% low. It is scaled by
|
|
135
|
+
sqrt(N / (N - 1)), since the average contains the groove it is compared
|
|
136
|
+
with, and is undefined for a single groove.
|
|
137
|
+
|
|
138
|
+
`y_avg` itself is not changed. It is still the unregistered mean, and is
|
|
139
|
+
what is exported.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
dict with
|
|
143
|
+
rms_nm: over every groove, or None for fewer than two grooves
|
|
144
|
+
per_groove_nm: one RMS per groove, in the order of `grooves`
|
|
145
|
+
shift_px: each groove's registration shift, in pixels
|
|
146
|
+
"""
|
|
147
|
+
n = len(grooves)
|
|
148
|
+
if n < 2:
|
|
149
|
+
return {'rms_nm': None, 'per_groove_nm': np.zeros(0),
|
|
150
|
+
'shift_px': np.zeros(0)}
|
|
151
|
+
|
|
152
|
+
dx = grooves[0][0][1] - grooves[0][0][0]
|
|
153
|
+
inner = (x_common > x_common[0] + dx) & (x_common < x_common[-1] - dx)
|
|
154
|
+
xi = x_common[inner]
|
|
155
|
+
reference = np.asarray(y_avg, dtype=float)[inner]
|
|
156
|
+
splines = [make_interp_spline(g[0], g[1], k=3) for g in grooves]
|
|
157
|
+
slopes = [spline.derivative() for spline in splines]
|
|
158
|
+
shifts = np.zeros(n)
|
|
159
|
+
|
|
160
|
+
for _ in range(n_passes):
|
|
161
|
+
registered = np.empty((n, len(xi)))
|
|
162
|
+
for k, (spline, slope) in enumerate(zip(splines, slopes)):
|
|
163
|
+
d = shifts[k]
|
|
164
|
+
for _ in range(n_steps):
|
|
165
|
+
r = spline(xi + d) - reference
|
|
166
|
+
# Least squares in (shift, offset). The offset's normal
|
|
167
|
+
# equation removes the means, which leaves one unknown.
|
|
168
|
+
g = slope(xi + d)
|
|
169
|
+
g = g - g.mean()
|
|
170
|
+
denom = np.dot(g, g)
|
|
171
|
+
if denom == 0:
|
|
172
|
+
break
|
|
173
|
+
d = float(np.clip(d - np.dot(g, r - r.mean()) / denom, -dx, dx))
|
|
174
|
+
shifts[k] = d
|
|
175
|
+
curve = spline(xi + d)
|
|
176
|
+
registered[k] = curve - np.mean(curve - reference)
|
|
177
|
+
reference = registered.mean(axis=0)
|
|
178
|
+
|
|
179
|
+
at_reference = make_interp_spline(xi, reference, k=3)
|
|
180
|
+
mean_square = np.empty(n)
|
|
181
|
+
for k, (x_g, y_g) in enumerate(grooves):
|
|
182
|
+
xs = x_g - shifts[k]
|
|
183
|
+
keep = (xs >= xi[0]) & (xs <= xi[-1])
|
|
184
|
+
r = y_g[keep] - at_reference(xs[keep])
|
|
185
|
+
mean_square[k] = np.mean((r - r.mean()) ** 2)
|
|
186
|
+
mean_square *= n / (n - 1)
|
|
187
|
+
|
|
188
|
+
return {'rms_nm': float(np.sqrt(np.mean(mean_square))),
|
|
189
|
+
'per_groove_nm': np.sqrt(mean_square),
|
|
190
|
+
'shift_px': shifts / dx}
|
|
191
|
+
|
|
192
|
+
|
|
93
193
|
def normalize_profile(x_avg, y_avg, period_nm, apply_smoothing=True,
|
|
94
194
|
smoothing_window=5):
|
|
95
195
|
"""
|
|
@@ -25,8 +25,8 @@ from ..core.region_mask import (GROOVE_REACH_PERIODS, bridge_nan,
|
|
|
25
25
|
describe_mask, groove_spacings_px, has_mask,
|
|
26
26
|
reject_grooves_near_nan,
|
|
27
27
|
tip_correct_around_nan)
|
|
28
|
-
from .average import (average_grooves, flatten_endpoints,
|
|
29
|
-
profile_metrics)
|
|
28
|
+
from .average import (average_grooves, flatten_endpoints, groove_residuals,
|
|
29
|
+
normalize_profile, profile_metrics, select_grooves)
|
|
30
30
|
|
|
31
31
|
__all__ = ["BoundaryProfile", "build_boundary_profile"]
|
|
32
32
|
|
|
@@ -66,6 +66,16 @@ class BoundaryProfile:
|
|
|
66
66
|
f"period {self.period_nm:.2f} nm, "
|
|
67
67
|
f"depth {self.metrics['groove_depth']:.4f} of period")
|
|
68
68
|
|
|
69
|
+
@property
|
|
70
|
+
def groove_residual_rms_nm(self):
|
|
71
|
+
"""RMS residual of the grooves about their average, in nm.
|
|
72
|
+
|
|
73
|
+
None when fewer than two grooves were averaged. What it measures, and
|
|
74
|
+
why `to_problem` does not use it unless asked to, is in that method's
|
|
75
|
+
docstring and in docs/theory/metrology.md section 4.
|
|
76
|
+
"""
|
|
77
|
+
return self.metrics.get('groove_residual_rms_nm')
|
|
78
|
+
|
|
69
79
|
def to_problem(self, *, coating=None, substrate=None, roughness=0.0,
|
|
70
80
|
blaze_angle=None):
|
|
71
81
|
"""This measured groove as an efficiency problem.
|
|
@@ -87,9 +97,11 @@ class BoundaryProfile:
|
|
|
87
97
|
Parameters:
|
|
88
98
|
coating, substrate: material names, resolved by
|
|
89
99
|
`gratinglab.materials` at solve time.
|
|
90
|
-
roughness: RMS surface roughness in nm
|
|
91
|
-
|
|
92
|
-
|
|
100
|
+
roughness: RMS surface roughness in nm, the Nevot-Croce sigma.
|
|
101
|
+
0.0 means "not supplied", not "smooth". It stays 0.0 unless
|
|
102
|
+
the caller passes a number, including the groove residual
|
|
103
|
+
this class measures; the note below says why that is not
|
|
104
|
+
the default.
|
|
93
105
|
blaze_angle: fitted facet angle in degrees, if the same scan was
|
|
94
106
|
also run through `core.analysis.extract_blaze_angle`. Without
|
|
95
107
|
it the scalar solver evaluates reflection on the mean surface,
|
|
@@ -98,13 +110,39 @@ class BoundaryProfile:
|
|
|
98
110
|
|
|
99
111
|
A note on roughness: `Problem.roughness` means high-spatial-frequency
|
|
100
112
|
surface roughness, the quantity with which Nevot-Croce damps a Fresnel
|
|
101
|
-
coefficient.
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
113
|
+
coefficient. `y_std_nm` is the groove-to-groove form spread and
|
|
114
|
+
`metrics['rms_slope']` is a slope statistic, and neither is that.
|
|
115
|
+
|
|
116
|
+
`groove_residual_rms_nm` is the nearest thing this class measures: the
|
|
117
|
+
RMS of each groove about the average once registered to it, which is
|
|
118
|
+
the part of the profile that does not repeat with the period. It is
|
|
119
|
+
still not passed by default, for four reasons, each of which makes it
|
|
120
|
+
a different number from the sigma Nevot-Croce wants:
|
|
121
|
+
|
|
122
|
+
- It is measured on the row-averaged profile. Structure that varies
|
|
123
|
+
along a groove averages away over the scan lines, and for isotropic
|
|
124
|
+
roughness with a correlation length well under the scan height that
|
|
125
|
+
is nearly all of it. The number keeps only what is coherent along
|
|
126
|
+
the groove for the height of the scan, so it is a lower bound on the
|
|
127
|
+
surface's roughness in its band.
|
|
128
|
+
- Its band is not the sigma's band. It sees nothing finer than the
|
|
129
|
+
pixel pitch or the tip, a few nanometres, while the structure that
|
|
130
|
+
damps a soft X-ray reflection extends down to wavelengths near the
|
|
131
|
+
X-ray wavelength itself. At the other end it includes groove-scale
|
|
132
|
+
form error, whose correlation length is about a period, where
|
|
133
|
+
Nevot-Croce assumes a short one.
|
|
134
|
+
- Groove-to-groove form error removes light from the orders by
|
|
135
|
+
scattering it between them, a Debye-Waller factor on each order's
|
|
136
|
+
own momentum transfer, not a damping of the Fresnel coefficient at
|
|
137
|
+
the facet. Folding it into the facet reflectivity applies the wrong
|
|
138
|
+
factor to the right effect.
|
|
139
|
+
- It carries instrument terms: AFM noise divided by the square root of
|
|
140
|
+
the number of scan lines, and a floor where the surface has corners
|
|
141
|
+
sharper than a pixel (0.21 nm on the synthetic sawtooth).
|
|
142
|
+
|
|
143
|
+
A caller who accepts those can pass it, and the name then appears in
|
|
144
|
+
their own code, which is the point:
|
|
145
|
+
`profile.to_problem(roughness=profile.groove_residual_rms_nm)`.
|
|
108
146
|
"""
|
|
109
147
|
# Imported here rather than at module scope: `boundary` is reachable
|
|
110
148
|
# from the GUI preview, which has no reason to pull the solver stack in
|
|
@@ -217,6 +255,16 @@ def build_boundary_profile(data, scan_x_size, settings, orientation=None) -> Bou
|
|
|
217
255
|
smoothing_window=settings.ggp_smoothing_window)
|
|
218
256
|
|
|
219
257
|
metrics = profile_metrics(x_norm, y_norm, period_nm, n_used)
|
|
258
|
+
# The same grooves average_grooves used, chosen by the same rules.
|
|
259
|
+
residual = groove_residuals(
|
|
260
|
+
select_grooves(raw_x, flat_y, groove_centers, period_nm, margin=0.0,
|
|
261
|
+
min_half_width=settings.ggp_min_half_width),
|
|
262
|
+
x_avg, y_avg)
|
|
263
|
+
metrics['groove_residual_rms_nm'] = residual['rms_nm']
|
|
264
|
+
metrics['groove_residual_per_groove_nm'] = residual['per_groove_nm'].tolist()
|
|
265
|
+
metrics['groove_residual_max_shift_px'] = (
|
|
266
|
+
float(np.max(np.abs(residual['shift_px'])))
|
|
267
|
+
if residual['rms_nm'] is not None else None)
|
|
220
268
|
if orientation is not None:
|
|
221
269
|
metrics.update(orientation.as_metrics())
|
|
222
270
|
metrics['tip_correction'] = settings.tip_correction
|
|
@@ -275,6 +275,11 @@ class BoundaryView(QWidget):
|
|
|
275
275
|
f"+/- {m['groove_rotation_stderr_deg']:.3f} deg"
|
|
276
276
|
+ (", corrected" if m.get('groove_rotation_corrected')
|
|
277
277
|
else ", not corrected"))
|
|
278
|
+
rms = m.get('groove_residual_rms_nm')
|
|
279
|
+
# Labelled as a residual, not a roughness: it is not what to_problem
|
|
280
|
+
# applies, and the panel should not suggest otherwise.
|
|
281
|
+
residual = ("not measured, one groove" if rms is None
|
|
282
|
+
else f"{rms:.3f} nm, not applied")
|
|
278
283
|
self.metrics_label.setText(
|
|
279
284
|
f"Period : {p.period_nm:.2f} nm\n"
|
|
280
285
|
f"Grooves averaged: {p.n_used} of {p.n_grooves}{edge}\n"
|
|
@@ -282,7 +287,8 @@ class BoundaryView(QWidget):
|
|
|
282
287
|
f"Peak-to-valley : {m['peak_to_valley']:.4f} of period\n"
|
|
283
288
|
f"RMS slope : {m['rms_slope']:.4f}\n"
|
|
284
289
|
f"Max sidewall : {m['max_angle_deg']:.2f} deg\n"
|
|
285
|
-
f"Max curvature : {m['max_curvature']:.4f}
|
|
290
|
+
f"Max curvature : {m['max_curvature']:.4f}\n"
|
|
291
|
+
f"Groove residual : {residual}{rotation}{tip}")
|
|
286
292
|
|
|
287
293
|
def _show_placeholder(self, message="Load an AFM file in the Import tab"):
|
|
288
294
|
self._placeholder = message
|
|
@@ -25,6 +25,16 @@ def write_profile_metrics(path, metrics):
|
|
|
25
25
|
f.write(f"RMS slope: {metrics['rms_slope']:.4f} (normalized units)\n")
|
|
26
26
|
f.write(f"Max slope magnitude: {metrics['max_slope']:.4f}\n")
|
|
27
27
|
f.write(f"Max curvature: {metrics['max_curvature']:.6f} (normalized units)\n")
|
|
28
|
+
# Written only by a pipeline that measures it, so a metrics dict from
|
|
29
|
+
# before it existed still writes. Not applied as roughness: see
|
|
30
|
+
# BoundaryProfile.to_problem.
|
|
31
|
+
if 'groove_residual_rms_nm' in metrics:
|
|
32
|
+
rms = metrics['groove_residual_rms_nm']
|
|
33
|
+
if rms is None:
|
|
34
|
+
f.write("Groove residual RMS: not measured (one groove)\n")
|
|
35
|
+
else:
|
|
36
|
+
f.write(f"Groove residual RMS: {rms:.3f} nm (about the average, "
|
|
37
|
+
f"not applied as roughness)\n")
|
|
28
38
|
# Present only when the pipeline ran a tip correction. Stated even so:
|
|
29
39
|
# a corrected depth and an uncorrected one are different measurements,
|
|
30
40
|
# and this sidecar is where a reader learns which this file holds.
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""All four solvers on one problem, through ``compare.sweep``.
|
|
2
|
+
|
|
3
|
+
The pairwise files (``test_rcwa.py``, ``test_cmethod.py``,
|
|
4
|
+
``test_cross_method.py``) each meet the integral solver on a problem chosen to
|
|
5
|
+
suit the method under test. This file does the thing the project exists for:
|
|
6
|
+
one ``Problem``, one ``Illumination``, every registered method, one
|
|
7
|
+
``Comparison``. The three rigorous methods are held to each other order by
|
|
8
|
+
order; scalar theory is held only to what it promises.
|
|
9
|
+
|
|
10
|
+
The case is a shallow gold sinusoid in grazing incidence, the founding
|
|
11
|
+
application's regime: period 40 nm, depth fraction 0.05, alpha = 80 deg, TE,
|
|
12
|
+
2 to 6 nm, absolute efficiencies with absorption from the vendored CXRO table
|
|
13
|
+
(``materials/data/Au.txt``, in the repository and therefore in CI, never in
|
|
14
|
+
the wheel; see ``SOURCES.md``). Forty orders propagate at 2 nm and fourteen at
|
|
15
|
+
6 nm.
|
|
16
|
+
|
|
17
|
+
How the tolerances were set
|
|
18
|
+
---------------------------
|
|
19
|
+
From a convergence ladder, not from one run. Measured 2026-10-28 on MC1 as
|
|
20
|
+
the largest difference over the five wavelengths against the C-method at
|
|
21
|
+
truncation 80, which is converged: truncation 24, 40 and 80 agree to 2e-15,
|
|
22
|
+
and the integral solver closes on it as its mesh is refined.
|
|
23
|
+
|
|
24
|
+
================== ========== ========== ========== ==========
|
|
25
|
+
knob order 0 order 1 orders 2+ absorbed
|
|
26
|
+
================== ========== ========== ========== ==========
|
|
27
|
+
integral 600 1.05e-6 4.6e-7 1.6e-7 1.9e-6
|
|
28
|
+
integral 800 4.8e-7 2.2e-7 7.7e-8 9.4e-7
|
|
29
|
+
integral 1600 8.1e-8 4.2e-8 1.5e-8 1.7e-7
|
|
30
|
+
rcwa 45 4.1e-6 4.1e-7 2.2e-7 3.6e-6
|
|
31
|
+
rcwa 60 2.6e-6 2.7e-7 1.4e-7 2.3e-6
|
|
32
|
+
rcwa 120 9.1e-7 9.7e-8 5.1e-8 7.8e-7
|
|
33
|
+
================== ========== ========== ========== ==========
|
|
34
|
+
|
|
35
|
+
Both fall as their own knob is refined, so the differences are discretisation
|
|
36
|
+
error, not a disagreement about the physics. The test runs integral 800,
|
|
37
|
+
RCWA 60 and C-method 40, which is the 2026-10-28 run, and its tolerances sit
|
|
38
|
+
about three times above those rows. RCWA's zero order is its weakest number
|
|
39
|
+
here because a sinusoid is a staircase to it.
|
|
40
|
+
|
|
41
|
+
The perfect-conductor variants need no table. RCWA cannot take part: it
|
|
42
|
+
expands a permittivity, and a perfect conductor has none, so it refuses, and
|
|
43
|
+
that refusal is asserted so that the day it learns the case, this file hears.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
import numpy as np
|
|
47
|
+
import pytest
|
|
48
|
+
|
|
49
|
+
from gratinglab.checks import check_energy_balance
|
|
50
|
+
from gratinglab.compare import align, sweep
|
|
51
|
+
from gratinglab.illumination import Illumination
|
|
52
|
+
from gratinglab.problem import Problem
|
|
53
|
+
from gratinglab.profiles import Sinusoidal
|
|
54
|
+
from gratinglab.solvers import UnsupportedConfiguration, get_solver
|
|
55
|
+
|
|
56
|
+
WAVELENGTHS = np.linspace(2.0, 6.0, 5)
|
|
57
|
+
METHODS = ["scalar", "integral", "rcwa", "cmethod"]
|
|
58
|
+
RIGOROUS = ("integral", "rcwa", "cmethod")
|
|
59
|
+
SINE = Sinusoidal(depth_fraction=0.05)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def worst(comparison, a, b):
|
|
63
|
+
"""Largest ``|a - b|`` in order 0, order 1 and the rest, over the grid."""
|
|
64
|
+
delta = np.abs(comparison.difference(a, b))
|
|
65
|
+
zero = int(np.flatnonzero(comparison.orders == 0)[0])
|
|
66
|
+
first = int(np.flatnonzero(comparison.orders == 1)[0])
|
|
67
|
+
rest = np.delete(delta, [zero, first], axis=1)
|
|
68
|
+
return delta[:, zero].max(), delta[:, first].max(), rest.max()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@pytest.fixture(scope="module")
|
|
72
|
+
def gold():
|
|
73
|
+
"""The 2026-10-28 run: about 30 s, so it is solved once for the module."""
|
|
74
|
+
problem = Problem(period=40.0, profile=SINE, coating="Au")
|
|
75
|
+
illumination = Illumination.classical(alpha=80.0, polarization="TE")
|
|
76
|
+
scans = sweep(
|
|
77
|
+
problem,
|
|
78
|
+
illumination,
|
|
79
|
+
WAVELENGTHS,
|
|
80
|
+
METHODS,
|
|
81
|
+
{
|
|
82
|
+
"integral": {"conductivity": "tabulated", "boundary_points": 800},
|
|
83
|
+
"cmethod": {"conductivity": "tabulated", "truncation": 40},
|
|
84
|
+
"rcwa": {"truncation": 60},
|
|
85
|
+
},
|
|
86
|
+
)
|
|
87
|
+
return problem, illumination, dict(zip(METHODS, scans)), align(scans)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class TestGoldInGrazingIncidence:
|
|
91
|
+
def test_every_method_finds_the_same_propagating_orders(self, gold):
|
|
92
|
+
_, _, scans, comparison = gold
|
|
93
|
+
assert comparison.methods == tuple(METHODS)
|
|
94
|
+
reference = scans["integral"]
|
|
95
|
+
for name in METHODS:
|
|
96
|
+
assert np.array_equal(scans[name].orders, reference.orders), name
|
|
97
|
+
assert np.array_equal(scans[name].propagating, reference.propagating), name
|
|
98
|
+
assert reference.propagating.sum(axis=1).tolist() == [40, 27, 20, 16, 14]
|
|
99
|
+
|
|
100
|
+
def test_the_cmethod_is_converged_at_this_truncation(self, gold):
|
|
101
|
+
"""What makes the C-method the anchor below: its own change from
|
|
102
|
+
truncation 24 to 40 is far under any tolerance in this file, so the
|
|
103
|
+
C-method-to-integral gap is the integral solver's error."""
|
|
104
|
+
problem, illumination, scans, _ = gold
|
|
105
|
+
(coarse,) = sweep(
|
|
106
|
+
problem, illumination, WAVELENGTHS, ["cmethod"],
|
|
107
|
+
{"cmethod": {"conductivity": "tabulated", "truncation": 24}},
|
|
108
|
+
)
|
|
109
|
+
assert np.abs(coarse.efficiency - scans["cmethod"].efficiency).max() < 1e-9
|
|
110
|
+
|
|
111
|
+
def test_the_cmethod_matches_the_integral_solver_order_by_order(self, gold):
|
|
112
|
+
_, _, scans, comparison = gold
|
|
113
|
+
zero, first, rest = worst(comparison, "cmethod", "integral")
|
|
114
|
+
assert zero < 1.5e-6 # measured 4.8e-7
|
|
115
|
+
assert first < 7e-7 # measured 2.2e-7
|
|
116
|
+
assert rest < 3e-7 # measured 7.7e-8
|
|
117
|
+
absorbed = np.abs(scans["cmethod"].absorption - scans["integral"].absorption)
|
|
118
|
+
assert absorbed.max() < 3e-6 # measured 9.4e-7
|
|
119
|
+
|
|
120
|
+
def test_rcwa_matches_the_integral_solver_order_by_order(self, gold):
|
|
121
|
+
_, _, scans, comparison = gold
|
|
122
|
+
zero, first, rest = worst(comparison, "rcwa", "integral")
|
|
123
|
+
assert zero < 1e-5 # measured 3.1e-6
|
|
124
|
+
assert first < 1e-6 # measured 2.6e-7
|
|
125
|
+
assert rest < 5e-7 # measured 1.1e-7
|
|
126
|
+
absorbed = np.abs(scans["rcwa"].absorption - scans["integral"].absorption)
|
|
127
|
+
assert absorbed.max() < 1e-5 # measured 2.4e-6
|
|
128
|
+
|
|
129
|
+
def test_each_rigorous_method_conserves_energy(self, gold):
|
|
130
|
+
"""``R + A = 1`` for a one-interface absorbing grating, with ``A``
|
|
131
|
+
computed independently of ``R`` by all three. The integral solver's
|
|
132
|
+
deviation is its mesh error, measured 1.7e-6 at 800 points; the
|
|
133
|
+
modal methods balance to rounding."""
|
|
134
|
+
_, _, scans, _ = gold
|
|
135
|
+
assert check_energy_balance(scans["integral"], lossless=True, tolerance=5e-6).passed
|
|
136
|
+
assert check_energy_balance(scans["rcwa"], lossless=True, tolerance=1e-10).passed
|
|
137
|
+
assert check_energy_balance(scans["cmethod"], lossless=True, tolerance=1e-10).passed
|
|
138
|
+
for name in RIGOROUS:
|
|
139
|
+
assert scans[name].provenance.notes["normalization"] == "absolute", name
|
|
140
|
+
|
|
141
|
+
def test_scalar_theory_is_an_approximation_here_and_says_so(self, gold):
|
|
142
|
+
"""Out of its regime: the reduced ratio reaches 0.15 and the facet
|
|
143
|
+
graze exceeds gold's critical angle. It still gets what the grating
|
|
144
|
+
equation decides, the orders, and lands within a factor of two of the
|
|
145
|
+
rigorous first order (measured 0.60 to 0.86 of it), and it warns that
|
|
146
|
+
per-order numbers want a rigorous cross-check. Nothing tighter is
|
|
147
|
+
asserted, because nothing tighter is promised."""
|
|
148
|
+
_, _, scans, comparison = gold
|
|
149
|
+
scalar = scans["scalar"]
|
|
150
|
+
assert check_energy_balance(scalar).passed # passive: sum <= 1
|
|
151
|
+
ratio = comparison.order(1)["scalar"] / comparison.order(1)["cmethod"]
|
|
152
|
+
assert np.all((ratio > 0.5) & (ratio < 2.0)), ratio
|
|
153
|
+
assert any("reduced ratio" in w for w in scalar.provenance.warnings)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class TestPerfectConductor:
|
|
157
|
+
"""The same groove and mount with no material, so no table and no
|
|
158
|
+
absorption: efficiencies relative to a perfect reflector, summing to one.
|
|
159
|
+
|
|
160
|
+
Measured 2026-10-28 against the C-method at truncation 80: TE with 400
|
|
161
|
+
boundary points 2.0e-7, falling to 1.1e-8 at 1600; TM with 800 points
|
|
162
|
+
7.1e-6, falling to 1.5e-6 at 1600. TM converges more slowly in the
|
|
163
|
+
integral solver, hence the extra points and the wider tolerance.
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
PROBLEM = Problem(period=40.0, profile=SINE)
|
|
167
|
+
|
|
168
|
+
@pytest.mark.parametrize(
|
|
169
|
+
"polarization, boundary_points, tolerance, balance",
|
|
170
|
+
[("TE", 400, 1e-6, 1e-9), ("TM", 800, 3e-5, 4e-5)],
|
|
171
|
+
)
|
|
172
|
+
def test_three_methods_agree(self, polarization, boundary_points, tolerance, balance):
|
|
173
|
+
illumination = Illumination.classical(alpha=80.0, polarization=polarization)
|
|
174
|
+
scans = sweep(
|
|
175
|
+
self.PROBLEM,
|
|
176
|
+
illumination,
|
|
177
|
+
WAVELENGTHS,
|
|
178
|
+
["scalar", "integral", "cmethod"],
|
|
179
|
+
{"integral": {"boundary_points": boundary_points}, "cmethod": {"truncation": 40}},
|
|
180
|
+
)
|
|
181
|
+
_, integral, cmethod = scans
|
|
182
|
+
comparison = align(scans)
|
|
183
|
+
|
|
184
|
+
for scan in scans:
|
|
185
|
+
assert np.array_equal(scan.propagating, integral.propagating)
|
|
186
|
+
assert scan.provenance.notes["normalization"].startswith("relative")
|
|
187
|
+
assert comparison.max_abs_difference("cmethod", "integral") < tolerance
|
|
188
|
+
# Measured deviation 4.5e-11 (TE) and 1.2e-5 (TM) for the integral solver.
|
|
189
|
+
assert check_energy_balance(integral, lossless=True, tolerance=balance).passed
|
|
190
|
+
assert check_energy_balance(cmethod, lossless=True, tolerance=1e-10).passed
|
|
191
|
+
|
|
192
|
+
def test_scalar_cannot_see_the_polarization_the_rigorous_methods_do(self):
|
|
193
|
+
"""Scalar theory is polarization-blind by construction, so its TE and
|
|
194
|
+
TM scans are identical, while the rigorous first order differs by 45%
|
|
195
|
+
to 216% between them here (0.281 against 0.407 at 2 nm, 0.086 against
|
|
196
|
+
0.273 at 6 nm). Which is why no agreement is asserted for it beyond
|
|
197
|
+
the orders."""
|
|
198
|
+
te, tm = (
|
|
199
|
+
sweep(
|
|
200
|
+
self.PROBLEM,
|
|
201
|
+
Illumination.classical(alpha=80.0, polarization=p),
|
|
202
|
+
WAVELENGTHS,
|
|
203
|
+
["scalar", "cmethod"],
|
|
204
|
+
{"cmethod": {"truncation": 40}},
|
|
205
|
+
)
|
|
206
|
+
for p in ("TE", "TM")
|
|
207
|
+
)
|
|
208
|
+
assert np.array_equal(te[0].efficiency, tm[0].efficiency)
|
|
209
|
+
first = int(np.flatnonzero(te[1].orders == 1)[0])
|
|
210
|
+
rigorous_te, rigorous_tm = te[1].efficiency[:, first], tm[1].efficiency[:, first]
|
|
211
|
+
assert np.all(np.abs(rigorous_tm - rigorous_te) / rigorous_te > 0.2)
|
|
212
|
+
|
|
213
|
+
def test_rcwa_refuses_a_perfect_conductor(self):
|
|
214
|
+
with pytest.raises(UnsupportedConfiguration, match="perfect conductor"):
|
|
215
|
+
get_solver("rcwa").solve(
|
|
216
|
+
self.PROBLEM, Illumination.classical(alpha=80.0), WAVELENGTHS
|
|
217
|
+
)
|
|
@@ -117,10 +117,15 @@ class TestGeneralPages:
|
|
|
117
117
|
assert page.path is not None and page.path.name == "metrology.md"
|
|
118
118
|
|
|
119
119
|
def test_metrology_page_states_that_roughness_is_not_measured(self):
|
|
120
|
-
"""The assumption most likely to be silently assumed away.
|
|
120
|
+
"""The assumption most likely to be silently assumed away.
|
|
121
|
+
|
|
122
|
+
The groove residual is measured now, so the page has to say that it is
|
|
123
|
+
not the roughness sigma and is not applied by default.
|
|
124
|
+
"""
|
|
121
125
|
page = self._page("metrology")
|
|
122
126
|
assert "RMS surface roughness" in page.text
|
|
123
|
-
assert "
|
|
127
|
+
assert "package measures that σ" in page.text
|
|
128
|
+
assert "not passed as the Névot–Croce σ by default" in page.text
|
|
124
129
|
|
|
125
130
|
def test_general_pages_are_never_banner_flagged(self):
|
|
126
131
|
"""These are geometry references, not a solver's approximation --
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|